1
0
mirror of https://github.com/SirusDoma/VoxCharger.git synced 2024-12-01 02:27:25 +01:00
VoxCharger/Sources/Vox/VoxChart.cs
CXO2 b8f49e3e2d Update 0.9.12c
- Fix naming and other code style issues
- Fix `float` and `double` parsing issue when current System Culture comma separator is not `.` - This fixes issue when parsing BPM and other floating numbers.
- Fix song selection when the program doesn't support the latest datecode in the future - It will disable Game Version and Infinite Version selection.
- Replaced ancient `FolderBrowserDialog` with `CommonFileDialog`
- Update default GameVersion and BackgroundId to latest version
- Implement Radar information in `LevelEditorForm`
- Implement built-in 2DX Encoder
- Start Offset / TimeStamp and Fader effect for audio preview is now supported
- Audio preview files is no longer divided into multiple files by default when audio files are unique. It still possible to specify unique preview via `LevelEditorForm`
- Implement advanced configuration when importing .ksh chart
- Add draft codes for Effect Mapping configurations and S3V support
- Other Bugfixes and QoL improvements
2023-01-06 20:59:21 +07:00

693 lines
26 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
namespace VoxCharger
{
public partial class VoxChart
{
private static readonly Encoding DefaultEncoding = Encoding.GetEncoding("shift_jis");
private const string VolCodes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmno";
private readonly string[] _filters = new string[] { "peak", "lpf1", "lpf1", "hpf1", "hpf1", "lbic", "nof" };
public int Version { get; private set; } = 10;
public string Lyric { get; private set; }
public EventCollection Events { get; private set; } = new EventCollection();
public List<Effect> Effects { get; private set; } = new List<Effect>();
public Time EndPosition { get; private set; }
public VoxChart()
{
}
public void Import(Ksh chart)
{
// Ksh events is already adjusted to be compatible with vox upon parsing
Events = chart.Events;
}
public void Parse(string fileName)
{
var current = Section.NoState;
var lines = File.ReadAllLines(fileName, DefaultEncoding);
Event.Stop stop = null;
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i].Trim().Trim('\t');
if (string.IsNullOrEmpty(line) || line == "//")
continue;
// Identify current section
if (line.StartsWith("#"))
{
if (current == Section.NoState)
{
var candidate = Section.NoState;
string header = line.Substring(1).Trim().Replace(' ', '_');
foreach (Section section in Enum.GetValues(typeof(Section)))
{
// e.g there's BPM and BPM_INFO, make sure simple BPM isn't registered as BPM_INFO
string name = Enum.GetName(typeof(Section), section);
if (header == name)
{
current = section;
break;
}
else if (header.StartsWith(name))
candidate = section;
}
if (current == Section.NoState)
current = candidate;
}
else
{
if (line == "#END")
current = Section.NoState;
}
continue;
}
#region --- TIME ---
/**
* Legends:
* mmm = measure
* bbb = beat
* ccc = cell
*/
var data = line.Trim().Split('\t').Select(p => p.Trim()).ToArray();
var time = Time.FromString(data[0]);
#endregion
#region --- FORMAT VERSION ---
if (current == Section.FormatVersion)
{
/*
* Format Version
* Parse version of Vox
*/
if (int.TryParse(line, out int ver))
Version = ver;
else
Debug.WriteLine($"[FORMAT] {i+1:D4}: Invalid version format");
}
#endregion
#region --- BEAT INFO ---
else if (current == Section.BeatInfo)
{
/*
* Beat Info (a.k.a Signature)
* Parse signature changes throughout the music, first signature change may referred as main signature
*
* Format:
* mmm,bbb,ccc b n
*
* * Arguments:
* b = beat
* n = note
*
* Note:
* signature = beat/note (e.g: 1/4)
*/
if (data.Length != 3 || time == null)
{
Debug.WriteLine($"[BEAT_INFO] {i+1:D4}: Invalid event format");
continue;
}
if (!int.TryParse(data[1], out int measure) || !int.TryParse(data[2], out int beat))
{
Debug.WriteLine($"[BEAT_INFO] {i+1:D4}: Invalid signature format");
continue;
}
if (time.Beat != 1 || time.Offset != 0)
{
Debug.WriteLine($"[BEAT_INFO] {i+1:D4}: Invalid Signature change position");
continue;
}
var signature = new Event.TimeSignature(time, measure, beat);
Events.Add(signature);
}
#endregion
#region --- BPM (Simple) ---
else if (current == Section.Bpm)
{
/*
* BPM (Simple)
* Parse intial BPM of music
*
* Format:
* ttt.tt
*
* Arguments:
* t = bpm
*
* Note:
* - BPM time offset is always positioned at 1st measure and 1st beat (001,001,000)
* - BPM Format is {000.####}
*/
// TODO: Process certain instructions
if (line.Contains("BAROFF"))
{
Debug.WriteLine($"[BPM_INFO] {i+1:D4}: \"BAROFF\" instruction skipped");
continue;
}
if (!float.TryParse(line, out float value))
Debug.WriteLine($"[BPM] {i:D4}: Invalid BPM value");
else
Events.Add(new Event.Bpm(new Time(1, 1, 0), value));
}
#endregion
#region --- BPM (Extended) ---
else if (current == Section.BpmInfo)
{
/*
* BPM (Extended)
* Parse intial BPM of music
*
* Format:
* mmm,bbb,ccc ttt.tt s(-)
*
* Arguments:
* t = bpm
* s = signature
*
* Note:
* - signature can be ended with minus sign (-) which indicate bpm stop
* - stop event duration can be obtained by calculating difference of time offset of the next bpm event occurence
* - signature most of the time is always 4
* - BPM Format is {000.####}
*/
if (data.Length != 3 || time == null)
{
Debug.WriteLine($"[BPM_INFO] {i+1:D4}: Invalid event format");
continue;
}
if (data[1] == "BAROFF")
{
Debug.WriteLine($"[BPM_INFO] {i+1:D4}: \"BAR\" instruction skipped"); // TODO: Process certain instructions
continue;
}
if (!float.TryParse(data[1], out float value))
{
Debug.WriteLine($"[BPM_INFO] {i+1:D4}: Invalid BPM value");
continue;
}
if (!int.TryParse(data[2].Replace("-", string.Empty), out int beat))
Debug.WriteLine($"[BPM_INFO] {i+1:D4}: Invalid beat division");
if (beat != 4)
Debug.WriteLine($"[BPM_INFO] {i+1:D4}: Non-4 beat division ({beat})");
// Handle stop event
if (data[2].EndsWith("-"))
{
if (stop != null)
Debug.WriteLine($"[BPM_INFO] {i+1:D4}: Duplicate stop event");
stop = new Event.Stop(time);
}
else
{
// Handle stop event if previously assigned
if (stop != null)
{
// Retrieve last signature for current measure
var signature = Events.GetTimeSignature(stop.Time);
if (signature == null)
{
Debug.WriteLine($"[BPM_INFO] {i+1:D4}: No valid signature for stop event");
stop = null;
continue;
}
stop.Duration = time.Difference(stop.Time, signature);
Events.Add(stop);
stop = null;
}
}
Events.Add(new Event.Bpm(time, value));
}
#endregion
#region --- TILT MODE INFO ---
else if (current == Section.Tilt)
{
/*
* Tilt Mode
* Parse camera tilt event
*
* Format:
* mmm,bbb,ccc m
*
* Arguments:
* m = mode
*
* Note:
* Refer to TiltMode for value mapping
*/
if (data.Length != 2 && time == null)
{
Debug.WriteLine($"[TILT_MODE_INFO] {i+1:D4}: Invalid event format");
continue;
}
if (!int.TryParse(data[1], out int tilt))
{
Debug.WriteLine($"[TILT_MODE_INFO] {i+1:D4}: Invalid tilt code format");
continue;
}
if (!Enum.IsDefined(typeof(Event.TiltType), tilt))
{
Debug.WriteLine($"[TILT_MODE_INFO] {i+1:D4}: Invalid tilt mode value");
continue;
}
Events.Add(new Event.TiltMode(time, (Event.TiltType)tilt));
}
#endregion
#region --- LYRIC INFO ---
else if (current == Section.Lyric)
{
/*
* Lyric Info
* Parse Lyric information
*
* Format:
* ???
*
* Note:
* Assumed has no format and lyric under plain text
*/
if (!string.IsNullOrEmpty(line))
Lyric += line + '\n';
}
#endregion
#region --- END POSITION ---
else if (current == Section.EndPosition)
{
/*
* End Position
* Parse end position; the last measure of music
*
* Format:
* mmm,bbb,ccc
*/
if (time == null)
Debug.WriteLine($"[END_POSITION] {i+1:D4}: Invalid event format");
else
EndPosition = time;
}
#endregion
#region --- TAB EFFECT INFO ---
else if (current == Section.TabEffect)
{
/*
* Tab Effect Info
* Parse Tab Effect info
*
* Format:
* ?, ???.??, ???.??, ??????.??, ?.??
*/
Debug.WriteLine($"[TAB_EFFECT] {i+1:D4}: {line}");
}
#endregion
#region --- FXBUTTON EFFECT INFO ---
else if (current == Section.FxbuttonEffect)
{
/*
* FxButton Effect Info
* Parse FxButton Effect Mapping
*
* Format:
* Depends on Fx, check implementation class for details
*/
if (line.StartsWith("0"))
continue; // Skip padding
var fx = Effect.FromVox(line);
if (fx == null || fx.Type == FxType.None)
Debug.WriteLine($"[FXBUTTON_EFFECT] {i+1:D4}: Invalid FX data");
else
{
fx.Id = Effects.Count;
Effects.Add(fx);
}
}
#endregion
#region --- TAB PARAM ASSIGN INFO ---
else if (current == Section.TabParam)
{
/*
* Tab Param Info
* Parse Tab Param Assign Info
*
* Format:
* ?, ?, ?.??, ?.??
*/
Debug.WriteLine($"[TAB_PARAM] {i+1:D4}: {line}");
}
#endregion
#region --- REVERB EFFECT PARAM ---
else if (current == Section.Reverb)
{
/*
* Reverb Effect
* Parse Reverb Effect
*/
Debug.WriteLine($"[REVERB_EFFECT] {i+1:D4}: {line}");
}
#endregion
#region --- SPCONTROLLER INFO ---
else if (current == Section.Spcontroler)
{
/*
* SP Controller Info
* Parse camera works
*
* Format:
* Depends on type, refer to Camera
*/
Debug.WriteLine($"[SPCONTROLLER_INFO] {i+1:D4}: {line}");
}
#endregion
#region --- TRACK ---
else if (IsTrackSection(current))
{
int trackId = GetTrackNumber(current);
if (trackId <= 0)
{
Debug.WriteLine($"[TRACK] {i+1:D4}: Invalid track id");
continue;
}
// Laser Channel
if (trackId == (int)Event.LaserTrack.Left || trackId == (int)Event.LaserTrack.Right)
{
/*
* Track - Laser
* Parse Laser Info
*
* Format:
* mmm,bbb,ccc o f t x r ?
*
* Arguments:
* o = Offset
* f = Flag
* t = Tick Tempo
* x = Filter
* r = Range
*
* Note:
* Slam defined as 2 laser events with same time and side, the offset of start laser must be less than offset of end laser
*/
if (data.Length < 5 || time == null)
{
Debug.WriteLine($"[TRACK_{trackId}] {i+1:D4}: Invalid event format");
continue;
}
if (!int.TryParse(data[1], out int offset))
{
Debug.WriteLine($"[TRACK_{trackId}] {i+1:D4}: Invalid laser offset");
continue;
}
if (!Enum.TryParse(data[2], out Event.LaserFlag flag))
{
Debug.WriteLine($"[TRACK_{trackId}] {i+1:D4}: Invalid laser flag");
continue;
}
var impact = Event.SlamImpact.None;
if (data.Length > 5)
{
if (!Enum.TryParse(data[3], out impact))
Debug.WriteLine($"[TRACK_{trackId}] {i + 1:D4}: Invalid laser tick tempo");
}
var laser = new Event.Laser(time, (Event.LaserTrack)trackId, offset, flag, impact);
string filterStr = data.Length > 5 ? data[4] : data[3];
string rangeStr = data.Length > 5 ? data[5] : data[4];
if (int.TryParse(filterStr, out int filterId))
{
switch (filterId)
{
case 1:
case 2: laser.Filter = Event.LaserFilter.LowPass; break;
case 3:
case 4: laser.Filter = Event.LaserFilter.HighPass; break;
case 5: laser.Filter = Event.LaserFilter.BitCrusher; break;
default: laser.Filter = Event.LaserFilter.Peak; break;
}
}
else
Debug.WriteLine($"[TRACK_{trackId}] {i + 1:D4}: Invalid laser filter");
if (!int.TryParse(rangeStr, out int range))
Debug.WriteLine($"[TRACK_{trackId}] {i + 1:D4}: Invalid laser range");
else
laser.Range = range;
// Locate slam
var slam = Events[time].FirstOrDefault(ev =>
(ev is Event.Laser l && l.Track == laser.Track) ||
(ev is Event.Slam s && s.Track == laser.Track)
);
if (slam != null)
{
// Duplicate slam, handle gracefully
if (slam is Event.Slam s)
slam = s.End;
var start = slam as Event.Laser;
var end = laser;
if (start.Offset == end.Offset)
{
Debug.WriteLine($"[TRACK_{trackId}] {i+1:D4}: Invalid slam offset");
continue;
}
if (start.Track != end.Track)
{
Debug.WriteLine($"[TRACK_{trackId}] {i+1:D4}: Invalid slam track");
continue;
}
Events.Add(new Event.Slam(time, start, end));
}
else
Events.Add(laser);
}
else
{
var track = (Event.ButtonTrack)trackId;
if (!int.TryParse(data[1], out int holdLength))
Debug.WriteLine($"[TRACK_{trackId}] {i+1:D4}: Invalid chip hold length");
var chip = new Event.Button(time, track, holdLength, null);
if (chip.IsFx)
{
// Hold Fx
if (holdLength > 0)
{
if (int.TryParse(data[2], out int fxIndex) && fxIndex - 2 < Effects.Count)
chip.HoldFx = Effects[fxIndex - 2];
else
Debug.WriteLine($"[TRACK_{trackId}] {i+1:D4}: Invalid chip hold effect index");
}
else
{
// Clap, snare, etc
if (Enum.TryParse(data[2], out Event.ChipFx hitFx))
chip.HitFx = hitFx;
else
Debug.WriteLine($"[TRACK_{trackId}] {i+1:D4}: Invalid chip hit effect value");
}
}
Events.Add(chip);
}
}
#endregion
}
}
public void Serialize(string filename)
{
string result = string.Empty;
result += "//====================================\n";
result += "// SOUND VOLTEX OUTPUT TEXT FILE\n";
result += "//====================================\n\n";
result += "#FORMAT VERSION\n";
result += $"{Version}\n";
result += "#END\n\n";
result += "#BEAT INFO\n";
foreach (var ev in Events)
{
if (ev is Event.TimeSignature signature)
result += signature.ToString() + "\n";
}
result += "#END\n\n";
result += "#BPM INFO\n";
Event.Bpm lastBpm = null;
foreach (var ev in Events)
{
if (ev is Event.Bpm bpm)
{
lastBpm = bpm;
result += bpm.ToString() + "\n";
}
}
result += "#END\n\n";
result += "#TILT MODE INFO\n";
foreach (var ev in Events)
{
if (ev is Event.TiltMode tilt)
result += tilt.ToString() + "\n";
}
result += "#END\n\n";
result += "#LYRIC INFO\n";
result += Lyric;
result += "#END\n\n";
result += "#END POSITION\n";
if (EndPosition == null)
EndPosition = new Time(Events.Max(ev => ev.Time.Measure) + 1, 1, 0);
result += EndPosition + "\n";
result += "#END\n\n";
result += "#TAB EFFECT INFO\n";
result += "1,\t90.00,\t400.00,\t18000.00,\t0.70\n";
result += "1,\t90.00,\t600.00,\t15000.00,\t5.00\n";
result += "2,\t50.00,\t40.00,\t5000.00,\t0.70\n";
result += "2,\t90.00,\t40.00,\t2000.00,\t3.00\n";
result += "3,\t100.00,\t30\n";
result += "#END\n\n";
var fxInfo = new List<Effect>();
var padding = new Effect();
foreach (var ev in Events)
{
if (ev is Event.Button bt && bt.HoldFx != null && fxInfo.Find(f => f.Id == bt.HoldFx.Id) == null)
fxInfo.Add(bt.HoldFx);
}
result += "#FXBUTTON EFFECT INFO\n";
fxInfo.Sort((a, b) => a.Id.CompareTo(b.Id));
foreach (var fx in fxInfo)
{
result += fx.ToString() + "\n";
result += padding.ToString() + "\n\n";
}
result += "#END\n\n";
result += "#TAB PARAM ASSIGN INFO\n";
for (int i = 0; i < 12; i++)
{
for (int j = 0; j < 2; j++)
result += $"{i},\t0,\t0.00,\t0.00\n";
}
result += "#END\n\n";
result += "#REVERB EFFECT PARAM\n";
result += "#END\n\n";
result += "//====================================\n";
result += "// TRACK INFO\n";
result += "//====================================\n\n";
for (int trackId = 1; trackId <= 8; trackId++)
{
result += $"#TRACK{trackId}\n";
if (trackId == (int)Event.LaserTrack.Left || trackId == (int)Event.LaserTrack.Right)
{
var track = (Event.LaserTrack)trackId;
foreach (var ev in Events)
{
if (ev is Event.Laser laser && laser.Track == track)
result += laser.ToString() + "\n";
}
}
else
{
var track = (Event.ButtonTrack)trackId;
foreach (var ev in Events)
{
if (ev is Event.Button button && button.Track == track)
result += button.ToString() + "\n";
}
}
result += "#END\n\n";
result += "//====================================\n\n";
}
result += "#TRACK AUTO TAB\n";
result += "#END\n\n";
result += "//====================================\n\n";
result += "#SPCONTROLER\n";
result += "001,01,00\tRealize\t3\t0\t36.12\t60.12\t110.12\t0.00\n";
result += "001,01,00\tRealize\t4\t0\t0.62\t0.72\t1.03\t0.00\n";
result += "001,01,00\tAIRL_ScaX\t1\t0\t0.00\t1.00\t0.00\t0.00\n";
result += "001,01,00\tAIRR_ScaX\t1\t0\t0.00\t2.00\t0.00\t0.00\n";
foreach (Camera.WorkType work in Enum.GetValues(typeof(Camera.WorkType)))
{
foreach (var ev in Events)
{
if (ev is Camera camera && camera.Work == work)
result += $"{camera}\n";
}
}
result += "#END\n\n";
result += "//====================================\n\n";
File.WriteAllText(filename, result, DefaultEncoding);
}
}
}