diff --git a/README.md b/README.md index 90f41771..83a0af92 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ If you want to talk or would like a game added to our configs, join our [Discord ### General * Stich991 - Italian translation * tuku473 - Design suggestions, colors, Spanish translation +* Lachesis - French translation +* Delusional Moonlight - Russian translation ### AlphaDream Engine * irdkwia - Finding games that used the engine diff --git a/VG Music Studio - Core/ADPCMDecoder.cs b/VG Music Studio - Core/ADPCMDecoder.cs new file mode 100644 index 00000000..894ea768 --- /dev/null +++ b/VG Music Studio - Core/ADPCMDecoder.cs @@ -0,0 +1,94 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core; + +internal struct ADPCMDecoder +{ + private static ReadOnlySpan IndexTable => new short[8] + { + -1, -1, -1, -1, 2, 4, 6, 8, + }; + private static ReadOnlySpan StepTable => new short[89] + { + 00007, 00008, 00009, 00010, 00011, 00012, 00013, 00014, + 00016, 00017, 00019, 00021, 00023, 00025, 00028, 00031, + 00034, 00037, 00041, 00045, 00050, 00055, 00060, 00066, + 00073, 00080, 00088, 00097, 00107, 00118, 00130, 00143, + 00157, 00173, 00190, 00209, 00230, 00253, 00279, 00307, + 00337, 00371, 00408, 00449, 00494, 00544, 00598, 00658, + 00724, 00796, 00876, 00963, 01060, 01166, 01282, 01411, + 01552, 01707, 01878, 02066, 02272, 02499, 02749, 03024, + 03327, 03660, 04026, 04428, 04871, 05358, 05894, 06484, + 07132, 07845, 08630, 09493, 10442, 11487, 12635, 13899, + 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767, + }; + + private byte[] _data; + public short LastSample; + public short StepIndex; + public int DataOffset; + public bool OnSecondNibble; + + public void Init(byte[] data) + { + _data = data; + LastSample = (short)(data[0] | (data[1] << 8)); + StepIndex = (short)((data[2] | (data[3] << 8)) & 0x7F); + DataOffset = 4; + OnSecondNibble = false; + } + + public static short[] ADPCMToPCM16(byte[] data) + { + var decoder = new ADPCMDecoder(); + decoder.Init(data); + + short[] buffer = new short[(data.Length - 4) * 2]; + for (int i = 0; i < buffer.Length; i++) + { + buffer[i] = decoder.GetSample(); + } + return buffer; + } + + public short GetSample() + { + int val = (_data[DataOffset] >> (OnSecondNibble ? 4 : 0)) & 0xF; + short step = StepTable[StepIndex]; + int diff = + (step / 8) + + (step / 4 * (val & 1)) + + (step / 2 * ((val >> 1) & 1)) + + (step * ((val >> 2) & 1)); + + int a = (diff * ((((val >> 3) & 1) == 1) ? -1 : 1)) + LastSample; + if (a < short.MinValue) + { + a = short.MinValue; + } + else if (a > short.MaxValue) + { + a = short.MaxValue; + } + LastSample = (short)a; + + a = StepIndex + IndexTable[val & 7]; + if (a < 0) + { + a = 0; + } + else if (a > 88) + { + a = 88; + } + StepIndex = (short)a; + + if (OnSecondNibble) + { + DataOffset++; + } + OnSecondNibble = !OnSecondNibble; + return LastSample; + } +} diff --git a/VG Music Studio/AlphaDream.yaml b/VG Music Studio - Core/AlphaDream.yaml similarity index 100% rename from VG Music Studio/AlphaDream.yaml rename to VG Music Studio - Core/AlphaDream.yaml diff --git a/VG Music Studio - Core/Assembler.cs b/VG Music Studio - Core/Assembler.cs new file mode 100644 index 00000000..f8220184 --- /dev/null +++ b/VG Music Studio - Core/Assembler.cs @@ -0,0 +1,368 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core; + +internal sealed class Assembler : IDisposable +{ + private sealed class Pair // Must be a class + { + public bool Global; + public int Offset; + } + private struct Pointer + { + public string Label; + public int BinaryOffset; + } + private const string _fileErrorFormat = "{0}{3}{3}Error reading file included in line {1}:{3}{2}"; + private const string _mathErrorFormat = "{0}{3}{3}Error parsing value in line {1} (Are you missing a definition?):{3}{2}"; + private const string _cmdErrorFormat = "{0}{3}{3}Unknown command in line {1}:{3}\"{2}\""; + + private static readonly CultureInfo _enUS = new("en-US"); + + public int BaseOffset { get; private set; } + private readonly List _loaded; + private readonly Dictionary _defines; + + private readonly Dictionary _labels; + private readonly List _lPointers; + private readonly MemoryStream _stream; + private readonly EndianBinaryWriter _writer; + + public string FileName { get; } + public Endianness Endianness { get; } + public int this[string Label] => _labels[FixLabel(Label)].Offset; + public int BinaryLength => (int)_stream.Length; + + public Assembler(string fileName, int baseOffset, Endianness endianness, Dictionary? initialDefines = null) + { + FileName = fileName; + Endianness = endianness; + _defines = initialDefines ?? new Dictionary(); + _lPointers = new List(); + _labels = new Dictionary(); + _loaded = new List(); + + _stream = new MemoryStream(); + _writer = new EndianBinaryWriter(_stream, endianness: endianness); + + string status = Read(fileName); + Debug.WriteLine(status); + SetBaseOffset(baseOffset); + } + + public void SetBaseOffset(int baseOffset) + { + Span span = stackalloc byte[4]; + foreach (Pointer p in _lPointers) + { + // Our example label is SEQ_STUFF at the binary offset 0x1000, curBaseOffset is 0x500, baseOffset is 0x1800 + // There is a pointer (p) to SEQ_STUFF at the binary offset 0x1DFC + _stream.Position = p.BinaryOffset; + _stream.Read(span); + int oldPointer = EndianBinaryPrimitives.ReadInt32(span, Endianness); // If there was a pointer to "SEQ_STUFF+4", the pointer would be 0x1504, at binary offset 0x1DFC + int labelOffset = oldPointer - BaseOffset; // Then labelOffset is 0x1004 (SEQ_STUFF+4) + + _stream.Position = p.BinaryOffset; + _writer.WriteInt32(baseOffset + labelOffset); // Copy the new pointer to binary offset 0x1DF4 + } + BaseOffset = baseOffset; + } + + public static string FixLabel(string label) + { + string ret = ""; + for (int i = 0; i < label.Length; i++) + { + char c = label[i]; + if ((c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9' && i > 0)) + { + ret += c; + } + else + { + ret += '_'; + } + } + return ret; + } + + // Returns a status + private string Read(string fileName) + { + if (_loaded.Contains(fileName)) + { + return $"{fileName} was already loaded"; + } + + string[] file = File.ReadAllLines(fileName); + _loaded.Add(fileName); + + for (int i = 0; i < file.Length; i++) + { + string line = file[i]; + if (string.IsNullOrWhiteSpace(line)) + { + continue; // Skip empty lines + } + + bool readingCMD = false; // If it's reading the command + string? cmd = null; + var args = new List(); + string str = string.Empty; + foreach (char c in line) + { + if (c == '@') // Ignore comments from this point + { + break; + } + if (c == '.' && cmd is null) + { + readingCMD = true; + } + else if (c == ':') // Labels + { + if (!_labels.ContainsKey(str)) + { + _labels.Add(str, new Pair()); + } + _labels[str].Offset = BinaryLength; + str = string.Empty; + } + else if (char.IsWhiteSpace(c)) + { + if (readingCMD) // If reading the command, otherwise do nothing + { + cmd = str; + readingCMD = false; + str = string.Empty; + } + } + else if (c == ',') + { + args.Add(str); + str = string.Empty; + } + else + { + str += c; + } + } + if (cmd is null) + { + continue; // Commented line + } + + args.Add(str); // Add last string before the newline + + switch (cmd.ToLower()) + { + case "include": + { + try + { + Read(args[0].Replace("\"", string.Empty)); + } + catch + { + throw new IOException(string.Format(_fileErrorFormat, fileName, i, args[0], Environment.NewLine)); + } + break; + } + case "equ": + { + try + { + _defines.Add(args[0], ParseInt(args[1])); + } + catch + { + throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); + } + break; + } + case "global": + { + if (!_labels.TryGetValue(args[0], out Pair? pair)) + { + pair = new Pair(); + _labels.Add(args[0], pair); + } + pair.Global = true; + break; + } + case "align": + { + int align = ParseInt(args[0]); + for (int a = BinaryLength % align; a < align; a++) + { + _writer.WriteByte(0); + } + break; + } + case "byte": + { + try + { + foreach (string a in args) + { + _writer.WriteByte((byte)ParseInt(a)); + } + } + catch + { + throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); + } + break; + } + case "hword": + { + try + { + foreach (string a in args) + { + _writer.WriteInt16((short)ParseInt(a)); + } + } + catch + { + throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); + } + break; + } + case "int": + case "word": + { + try + { + foreach (string a in args) + { + _writer.WriteInt32(ParseInt(a)); + } + } + catch + { + throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); + } + break; + } + case "end": + { + goto end; + } + case "section": // Ignore + { + break; + } + default: throw new NotSupportedException(string.Format(_cmdErrorFormat, fileName, i, cmd, Environment.NewLine)); + } + } + end: + return $"{fileName} loaded with no issues"; + } + + private int ParseInt(string value) + { + // First try regular values like "40" and "0x20" + if (value.StartsWith("0x") && int.TryParse(value.AsSpan(2), NumberStyles.HexNumber, _enUS, out int hex)) + { + return hex; + } + if (int.TryParse(value, NumberStyles.Integer, _enUS, out int dec)) + { + return dec; + } + // Then check if it's defined + if (_defines.TryGetValue(value, out int def)) + { + return def; + } + if (_labels.TryGetValue(value, out Pair? pair)) + { + _lPointers.Add(new Pointer { Label = value, BinaryOffset = BinaryLength }); + return pair.Offset; + } + + // Then check if it's math + bool foundMath = false; + string str = string.Empty; + int ret = 0; + bool add = true; // Add first, so the initial value is set + bool sub = false; + bool mul = false; + bool div = false; + for (int i = 0; i < value.Length; i++) + { + char c = value[i]; + + if (char.IsWhiteSpace(c)) // White space does nothing here + { + continue; + } + if (c == '+' || c == '-' || c == '*' || c == '/') + { + if (add) + { + ret += ParseInt(str); + } + else if (sub) + { + ret -= ParseInt(str); + } + else if (mul) + { + ret *= ParseInt(str); + } + else if (div) + { + ret /= ParseInt(str); + } + add = c == '+'; + sub = c == '-'; + mul = c == '*'; + div = c == '/'; + str = string.Empty; + foundMath = true; + } + else + { + str += c; + } + } + + if (foundMath) + { + if (add) // Handle last + { + ret += ParseInt(str); + } + else if (sub) + { + ret -= ParseInt(str); + } + else if (mul) + { + ret *= ParseInt(str); + } + else if (div) + { + ret /= ParseInt(str); + } + return ret; + } + + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + + public void Dispose() + { + _stream.Dispose(); + } +} diff --git a/VG Music Studio - Core/Config.cs b/VG Music Studio - Core/Config.cs new file mode 100644 index 00000000..2f26ad57 --- /dev/null +++ b/VG Music Studio - Core/Config.cs @@ -0,0 +1,92 @@ +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core; + +public abstract class Config : IDisposable +{ + public readonly struct Song + { + public readonly int Index; + public readonly string Name; + + public Song(int index, string name) + { + Index = index; + Name = name; + } + + public static bool operator ==(Song left, Song right) + { + return left.Equals(right); + } + public static bool operator !=(Song left, Song right) + { + return !(left == right); + } + + public override bool Equals(object? obj) + { + return obj is Song other && other.Index == Index; + } + public override int GetHashCode() + { + return Index.GetHashCode(); + } + public override string ToString() + { + return Name; + } + } + public sealed class Playlist + { + public string Name; + public List Songs; + + public Playlist(string name, List songs) + { + Name = name; + Songs = songs; + } + + public override string ToString() + { + int num = Songs.Count; + return string.Format("{0} - ({1:N0} {2})", Name, num, LanguageUtils.HandlePlural(num, Strings.Song_s_)); + } + } + + public readonly List Playlists; + + protected Config() + { + Playlists = new List(); + } + + public bool TryGetFirstSong(int index, out Song song) + { + foreach (Playlist p in Playlists) + { + foreach (Song s in p.Songs) + { + if (s.Index == index) + { + song = s; + return true; + } + } + } + song = default; + return false; + } + + public abstract string GetGameName(); + public abstract string GetSongName(int index); + + public virtual void Dispose() + { + // + } +} diff --git a/VG Music Studio - Core/Config.yaml b/VG Music Studio - Core/Config.yaml new file mode 100644 index 00000000..c7c809c9 --- /dev/null +++ b/VG Music Studio - Core/Config.yaml @@ -0,0 +1,137 @@ +TaskbarProgress: True # True or False # Whether the taskbar will show the song progress +RefreshRate: 30 # RefreshRate >= 1 and RefreshRate <= 1000 # How many times a second the visual updates +CenterIndicators: False # True or False # Whether lines should be drawn for the center of panpot in the visual +PanpotIndicators: False # True or False # Whether lines should be drawn for the track's panpot in the visual +PlaylistMode: "Random" # "Random" or "Sequential" # The way the playlist will behave +PlaylistSongLoops: 0 # Loops >= 0 and Loops <= 9223372036854775807 # How many times a song should loop before fading out +PlaylistFadeOutMilliseconds: 10000 # Milliseconds >= 0 and Milliseconds <= 9223372036854775807 # How many milliseconds it should take to fade out of a song +MiddleCOctave: 4 # Octave >= --128 and Octave <= 127 # The octave that holds middle C. Used in the visual and track viewer +Colors: # Each color must be a RGB hex code + 0: "CF7FFF" + 1: "BF6CFF" + 2: "A750FF" + 3: "6C00B7" + 4: "5C1FFF" + 5: "7750FF" + 6: "FFF06A" + 7: "FF701C" + 8: "C8AAFF" + 9: "3FFFFF" + 10: "28FFDF" + 11: "6CFFB4" + 12: "98FF6C" + 13: "AAFFB0" + 14: "DD008C" + 15: "FFDFAA" + 16: "FF3F8C" + 17: "DF00FF" + 18: "C900AB" + 19: "FF1394" + 20: "FF7FF5" + 21: "004FD4" + 22: "0075EB" + 23: "0039FF" + 24: "FF7A68" + 25: "FF674C" + 26: "FF965D" + 27: "FF6524" + 28: "FF552A" + 29: "FF0606" + 30: "940028" + 31: "BD0004" + 32: "E9B96A" + 33: "E59A4E" + 34: "E48845" + 35: "FFEE5B" + 36: "E4A845" + 37: "BD7B0C" + 38: "BFBFBF" + 39: "BF00BF" + 40: "B900D4" + 41: "9400C5" + 42: "5F00BF" + 43: "6F3FFF" + 44: "1C00BD" + 45: "FF6AD9" + 46: "FF8AD6" + 47: "CE7F50" + 48: "155BFF" + 49: "3700AA" + 50: "3200C9" + 51: "5500D4" + 52: "FFECC9" + 53: "FFDFBF" + 54: "FFD9D4" + 55: "FF5C3F" + 56: "FF991D" + 57: "FFA715" + 58: "FFFF00" + 59: "FFB304" + 60: "FF6B08" + 61: "FA4400" + 62: "C6FF50" + 63: "9EFF1B" + 64: "FFE79F" + 65: "FFCA83" + 66: "FFDD54" + 67: "FFD015" + 68: "FFEE1F" + 69: "FF7330" + 70: "0075B4" + 71: "0097C9" + 72: "5FFFFF" + 73: "00D8FF" + 74: "00B4D4" + 75: "54BFFF" + 76: "8CFFF9" + 77: "0087D8" + 78: "00D4AF" + 79: "7FFF54" + 80: "19FF24" + 81: "FF90B4" + 82: "2AFFA4" + 83: "AFFF5F" + 84: "FF5F63" + 85: "74F4FF" + 86: "FF35CC" + 87: "ACFF00" + 88: "4AFFD1" + 89: "B4FBFF" + 90: "C90074" + 91: "002F6A" + 92: "00BF5F" + 93: "006DBF" + 94: "AE00F8" + 95: "BFBFFF" + 96: "AAE9FF" + 97: "AA00A1" + 98: "FF5454" + 99: "E9AF00" + 100: "BFEFFF" + 101: "139F00" + 102: "D9B4FF" + 103: "CC9012" + 104: "EED045" + 105: "F5E51E" + 106: "E3FF1F" + 107: "CBFF74" + 108: "8AFFB5" + 109: "DCFF94" + 110: "009FFF" + 111: "E9DE00" + 112: "FF6AB4" + 113: "7FBFBF" + 114: "AFD7E4" + 115: "7F3F3F" + 116: "C6844D" + 117: "A4765A" + 118: "9B4D9B" + 119: "AFBFCF" + 120: "BF2F00" + 121: "25A4AF" + 122: "00A9D4" + 123: "FFFF7F" + 124: "AF0F13" + 125: "A0A3A8" + 126: "C6A28D" + 127: "7F7FBF" \ No newline at end of file diff --git a/VG Music Studio - Core/Dependencies/DLS2.dll b/VG Music Studio - Core/Dependencies/DLS2.dll new file mode 100644 index 00000000..12414d5c Binary files /dev/null and b/VG Music Studio - Core/Dependencies/DLS2.dll differ diff --git a/VG Music Studio - Core/Dependencies/KMIDI.deps.json b/VG Music Studio - Core/Dependencies/KMIDI.deps.json new file mode 100644 index 00000000..7feb759b --- /dev/null +++ b/VG Music Studio - Core/Dependencies/KMIDI.deps.json @@ -0,0 +1,41 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v7.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v7.0": { + "KMIDI/1.0.0": { + "dependencies": { + "EndianBinaryIO": "2.1.0" + }, + "runtime": { + "KMIDI.dll": {} + } + }, + "EndianBinaryIO/2.1.0": { + "runtime": { + "lib/net7.0/EndianBinaryIO.dll": { + "assemblyVersion": "2.1.0.0", + "fileVersion": "2.1.0.0" + } + } + } + } + }, + "libraries": { + "KMIDI/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "EndianBinaryIO/2.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-OzcYSj5h37lj8PJAcROuYIW+FEO/it3Famh3cduziKQzE2ZKDgirNUJNnDCYkHgBxc2CRc//GV2ChRSqlXhbjQ==", + "path": "endianbinaryio/2.1.0", + "hashPath": "endianbinaryio.2.1.0.nupkg.sha512" + } + } +} \ No newline at end of file diff --git a/VG Music Studio - Core/Dependencies/KMIDI.dll b/VG Music Studio - Core/Dependencies/KMIDI.dll new file mode 100644 index 00000000..372b9e32 Binary files /dev/null and b/VG Music Studio - Core/Dependencies/KMIDI.dll differ diff --git a/VG Music Studio - Core/Dependencies/KMIDI.xml b/VG Music Studio - Core/Dependencies/KMIDI.xml new file mode 100644 index 00000000..23588353 --- /dev/null +++ b/VG Music Studio - Core/Dependencies/KMIDI.xml @@ -0,0 +1,77 @@ + + + + KMIDI + + + + Includes the end of track event + + + + + + If there are other events at , will be inserted after them. + + + Length 4 + + + Contains a single multi-channel track + + + Contains one or more simultaneous tracks + + + Contains one or more independent single-track patterns + + + Used with + + + Used with + + + Reserved for ASCII treatment + + + Reserved for ASCII treatment + + + Reserved for ASCII treatment + + + Reserved for ASCII treatment + + + Reserved for ASCII treatment + + + Reserved for ASCII treatment + + + Not optional + + + Used with + + + Used with + + + Used with + + + How many ticks are between this event and the previous one. If this is the first event in the track, then it is equal to + + + Returns a value in the range [-8_192, 8_191] + + + + + + Middle C + + + diff --git a/VG Music Studio - Core/Dependencies/SoundFont2.dll b/VG Music Studio - Core/Dependencies/SoundFont2.dll new file mode 100644 index 00000000..2bcfc3e4 Binary files /dev/null and b/VG Music Studio - Core/Dependencies/SoundFont2.dll differ diff --git a/VG Music Studio - Core/Engine.cs b/VG Music Studio - Core/Engine.cs new file mode 100644 index 00000000..a37f0e03 --- /dev/null +++ b/VG Music Studio - Core/Engine.cs @@ -0,0 +1,20 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core; + +public abstract class Engine : IDisposable +{ + public static Engine? Instance { get; protected set; } + + public abstract Config Config { get; } + public abstract Mixer Mixer { get; } + public abstract Player Player { get; } + + public virtual void Dispose() + { + Config.Dispose(); + Mixer.Dispose(); + Player.Dispose(); + Instance = null; + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamCommands.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamCommands.cs new file mode 100644 index 00000000..b27f327b --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamCommands.cs @@ -0,0 +1,113 @@ +using Kermalis.VGMusicStudio.Core.Util; +using System.Drawing; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +internal sealed class FinishCommand : ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Finish"; + public string Arguments => string.Empty; +} +internal sealed class FreeNoteHamtaroCommand : ICommand // TODO: When optimization comes, get rid of free note vs note and just have the label differ +{ + public Color Color => Color.SkyBlue; + public string Label => "Free Note"; + public string Arguments => $"{ConfigUtils.GetKeyName(Note)} {Volume} {Duration}"; + + public byte Note { get; set; } + public byte Volume { get; set; } + public byte Duration { get; set; } +} +internal sealed class FreeNoteMLSSCommand : ICommand +{ + public Color Color => Color.SkyBlue; + public string Label => "Free Note"; + public string Arguments => $"{ConfigUtils.GetKeyName(Note)} {Duration}"; + + public byte Note { get; set; } + public byte Duration { get; set; } +} +internal sealed class JumpCommand : ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Jump"; + public string Arguments => $"0x{Offset:X7}"; + + public int Offset { get; set; } +} +internal sealed class NoteHamtaroCommand : ICommand +{ + public Color Color => Color.SkyBlue; + public string Label => "Note"; + public string Arguments => $"{ConfigUtils.GetKeyName(Note)} {Volume} {Duration}"; + + public byte Note { get; set; } + public byte Volume { get; set; } + public byte Duration { get; set; } +} +internal sealed class NoteMLSSCommand : ICommand +{ + public Color Color => Color.SkyBlue; + public string Label => "Note"; + public string Arguments => $"{ConfigUtils.GetKeyName(Note)} {Duration}"; + + public byte Note { get; set; } + public byte Duration { get; set; } +} +internal sealed class PanpotCommand : ICommand +{ + public Color Color => Color.GreenYellow; + public string Label => "Panpot"; + public string Arguments => Panpot.ToString(); + + public sbyte Panpot { get; set; } +} +internal sealed class PitchBendCommand : ICommand +{ + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend"; + public string Arguments => Bend.ToString(); + + public sbyte Bend { get; set; } +} +internal sealed class PitchBendRangeCommand : ICommand +{ + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend Range"; + public string Arguments => Range.ToString(); + + public byte Range { get; set; } +} +internal sealed class RestCommand : ICommand +{ + public Color Color => Color.PaleVioletRed; + public string Label => "Rest"; + public string Arguments => Rest.ToString(); + + public byte Rest { get; set; } +} +internal sealed class TrackTempoCommand : ICommand +{ + public Color Color => Color.DeepSkyBlue; + public string Label => "Track Tempo"; + public string Arguments => Tempo.ToString(); + + public byte Tempo { get; set; } +} +internal sealed class VoiceCommand : ICommand +{ + public Color Color => Color.DarkSalmon; + public string Label => "Voice"; + public string Arguments => Voice.ToString(); + + public byte Voice { get; set; } +} +internal sealed class VolumeCommand : ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Volume"; + public string Arguments => Volume.ToString(); + + public byte Volume { get; set; } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamConfig.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamConfig.cs new file mode 100644 index 00000000..750ae76b --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamConfig.cs @@ -0,0 +1,228 @@ +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using YamlDotNet.Core; +using YamlDotNet.RepresentationModel; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +public sealed class AlphaDreamConfig : Config +{ + private const string CONFIG_FILE = "AlphaDream.yaml"; + + internal readonly byte[] ROM; + internal readonly EndianBinaryReader Reader; // TODO: Need? + internal readonly string GameCode; + internal readonly byte Version; + + internal readonly string Name; + internal readonly AudioEngineVersion AudioEngineVersion; + internal readonly int[] SongTableOffsets; + public readonly long[] SongTableSizes; + internal readonly int VoiceTableOffset; + internal readonly int SampleTableOffset; + internal readonly long SampleTableSize; + + internal AlphaDreamConfig(byte[] rom) + { + using (StreamReader fileStream = File.OpenText(ConfigUtils.CombineWithBaseDirectory(CONFIG_FILE))) + { + string gcv = string.Empty; + try + { + ROM = rom; + Reader = new EndianBinaryReader(new MemoryStream(rom), ascii: true); + Reader.Stream.Position = 0xAC; + GameCode = Reader.ReadString_Count(4); + Reader.Stream.Position = 0xBC; + Version = Reader.ReadByte(); + gcv = $"{GameCode}_{Version:X2}"; + var yaml = new YamlStream(); + yaml.Load(fileStream); + + var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; + YamlMappingNode game; + try + { + game = (YamlMappingNode)mapping.Children.GetValue(gcv); + } + catch (BetterKeyNotFoundException) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KMissingGameCode, gcv))); + } + + YamlNode? nameNode = null, + audioEngineVersionNode = null, + songTableOffsetsNode = null, + voiceTableOffsetNode = null, + sampleTableOffsetNode = null, + songTableSizesNode = null, + sampleTableSizeNode = null; + void Load(YamlMappingNode gameToLoad) + { + if (gameToLoad.Children.TryGetValue("Copy", out YamlNode? node)) + { + YamlMappingNode copyGame; + try + { + copyGame = (YamlMappingNode)mapping.Children.GetValue(node); + } + catch (BetterKeyNotFoundException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KCopyInvalidGameCode, ex.Key))); + } + Load(copyGame); + } + if (gameToLoad.Children.TryGetValue(nameof(Name), out node)) + { + nameNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(AudioEngineVersion), out node)) + { + audioEngineVersionNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SongTableOffsets), out node)) + { + songTableOffsetsNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SongTableSizes), out node)) + { + songTableSizesNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(VoiceTableOffset), out node)) + { + voiceTableOffsetNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SampleTableOffset), out node)) + { + sampleTableOffsetNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SampleTableSize), out node)) + { + sampleTableSizeNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(Playlists), out node)) + { + var playlists = (YamlMappingNode)node; + foreach (KeyValuePair kvp in playlists) + { + string name = kvp.Key.ToString(); + var songs = new List(); + foreach (KeyValuePair song in (YamlMappingNode)kvp.Value) + { + int songIndex = (int)ConfigUtils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Playlists)), song.Key.ToString(), 0, int.MaxValue); + if (songs.Any(s => s.Index == songIndex)) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongRepeated, name, songIndex))); + } + songs.Add(new Song(songIndex, song.Value.ToString())); + } + Playlists.Add(new Playlist(name, songs)); + } + } + } + + Load(game); + + if (nameNode is null) + { + throw new BetterKeyNotFoundException(nameof(Name), null); + } + Name = nameNode.ToString(); + + if (audioEngineVersionNode is null) + { + throw new BetterKeyNotFoundException(nameof(AudioEngineVersion), null); + } + AudioEngineVersion = ConfigUtils.ParseEnum(nameof(AudioEngineVersion), audioEngineVersionNode.ToString()); + + if (songTableOffsetsNode is null) + { + throw new BetterKeyNotFoundException(nameof(SongTableOffsets), null); + } + string[] songTables = songTableOffsetsNode.ToString().Split(' ', options: StringSplitOptions.RemoveEmptyEntries); + int numSongTables = songTables.Length; + if (numSongTables == 0) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigKeyNoEntries, nameof(SongTableOffsets)))); + } + + if (songTableSizesNode is null) + { + throw new BetterKeyNotFoundException(nameof(SongTableSizes), null); + } + string[] songTableSizes = songTableSizesNode.ToString().Split(' ', options: StringSplitOptions.RemoveEmptyEntries); + if (songTableSizes.Length != numSongTables) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongTableCounts, nameof(SongTableSizes), nameof(SongTableOffsets)))); + } + SongTableOffsets = new int[numSongTables]; + SongTableSizes = new long[numSongTables]; + int maxOffset = rom.Length - 1; + for (int i = 0; i < numSongTables; i++) + { + SongTableOffsets[i] = (int)ConfigUtils.ParseValue(nameof(SongTableOffsets), songTables[i], 0, maxOffset); + SongTableSizes[i] = ConfigUtils.ParseValue(nameof(SongTableSizes), songTableSizes[i], 1, maxOffset); + } + + if (voiceTableOffsetNode is null) + { + throw new BetterKeyNotFoundException(nameof(VoiceTableOffset), null); + } + VoiceTableOffset = (int)ConfigUtils.ParseValue(nameof(VoiceTableOffset), voiceTableOffsetNode.ToString(), 0, maxOffset); + + if (sampleTableOffsetNode is null) + { + throw new BetterKeyNotFoundException(nameof(SampleTableOffset), null); + } + SampleTableOffset = (int)ConfigUtils.ParseValue(nameof(SampleTableOffset), sampleTableOffsetNode.ToString(), 0, maxOffset); + + if (sampleTableSizeNode is null) + { + throw new BetterKeyNotFoundException(nameof(SampleTableSize), null); + } + SampleTableSize = ConfigUtils.ParseValue(nameof(SampleTableSize), sampleTableSizeNode.ToString(), 0, maxOffset); + + // The complete playlist + if (!Playlists.Any(p => p.Name == "Music")) + { + Playlists.Insert(0, new Playlist(Strings.PlaylistMusic, Playlists.SelectMany(p => p.Songs).Distinct().OrderBy(s => s.Index).ToList())); + } + } + catch (BetterKeyNotFoundException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigKeyMissing, ex.Key))); + } + catch (InvalidValueException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + ex.Message)); + } + catch (YamlException ex) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + ex.Message)); + } + } + } + + public override string GetGameName() + { + return Name; + } + public override string GetSongName(int index) + { + if (TryGetFirstSong(index, out Song s)) + { + return s.Name; + } + return index.ToString(); + } + + public override void Dispose() + { + Reader.Stream.Dispose(); + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs new file mode 100644 index 00000000..fdee70e4 --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs @@ -0,0 +1,33 @@ +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +public sealed class AlphaDreamEngine : Engine +{ + public static AlphaDreamEngine? AlphaDreamInstance { get; private set; } + + public override AlphaDreamConfig Config { get; } + public override AlphaDreamMixer Mixer { get; } + public override AlphaDreamPlayer Player { get; } + + public AlphaDreamEngine(byte[] rom) + { + if (rom.Length > GBAUtils.CARTRIDGE_CAPACITY) + { + throw new InvalidDataException($"The ROM is too large. Maximum size is 0x{GBAUtils.CARTRIDGE_CAPACITY:X7} bytes."); + } + + Config = new AlphaDreamConfig(rom); + Mixer = new AlphaDreamMixer(Config); + Player = new AlphaDreamPlayer(Config, Mixer); + + AlphaDreamInstance = this; + Instance = this; + } + + public override void Dispose() + { + base.Dispose(); + AlphaDreamInstance = null; + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEnums.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEnums.cs new file mode 100644 index 00000000..3698f7ff --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEnums.cs @@ -0,0 +1,15 @@ +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +internal enum AudioEngineVersion : byte +{ + Hamtaro, + MLSS, +} + +internal enum EnvelopeState : byte +{ + Attack, + Decay, + Sustain, + Release, +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamException.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamException.cs new file mode 100644 index 00000000..3fd92e5b --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamException.cs @@ -0,0 +1,17 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +public sealed class AlphaDreamInvalidCMDException : Exception +{ + public byte TrackIndex { get; } + public int CmdOffset { get; } + public byte Cmd { get; } + + internal AlphaDreamInvalidCMDException(byte trackIndex, int cmdOffset, byte cmd) + { + TrackIndex = trackIndex; + CmdOffset = cmdOffset; + Cmd = cmd; + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong.cs new file mode 100644 index 00000000..f81833ae --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong.cs @@ -0,0 +1,41 @@ +using Kermalis.EndianBinaryIO; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +internal sealed partial class AlphaDreamLoadedSong : ILoadedSong +{ + public List?[] Events { get; } + public long MaxTicks { get; private set; } + public int LongestTrack; + + private readonly AlphaDreamPlayer _player; + + public AlphaDreamLoadedSong(AlphaDreamPlayer player, int songOffset) + { + _player = player; + + Events = new List[AlphaDreamPlayer.NUM_TRACKS]; + songOffset -= GBAUtils.CARTRIDGE_OFFSET; + EndianBinaryReader r = player.Config.Reader; + r.Stream.Position = songOffset; + ushort trackBits = r.ReadUInt16(); + int usedTracks = 0; + for (byte trackIndex = 0; trackIndex < AlphaDreamPlayer.NUM_TRACKS; trackIndex++) + { + AlphaDreamTrack track = player.Tracks[trackIndex]; + if ((trackBits & (1 << trackIndex)) == 0) + { + track.IsEnabled = false; + track.StartOffset = 0; + continue; + } + + track.IsEnabled = true; + r.Stream.Position = songOffset + 2 + (2 * usedTracks++); + track.StartOffset = songOffset + r.ReadInt16(); + + AddTrackEvents(trackIndex, track.StartOffset); + } + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong_Events.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong_Events.cs new file mode 100644 index 00000000..66ae3413 --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong_Events.cs @@ -0,0 +1,266 @@ +using Kermalis.EndianBinaryIO; +using System.Collections.Generic; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +internal sealed partial class AlphaDreamLoadedSong +{ + private void AddEvent(byte trackIndex, long cmdOffset, ICommand command) + { + Events[trackIndex]!.Add(new SongEvent(cmdOffset, command)); + } + private bool EventExists(byte trackIndex, long cmdOffset) + { + return Events[trackIndex]!.Exists(e => e.Offset == cmdOffset); + } + + private void AddTrackEvents(byte trackIndex, int trackStart) + { + Events[trackIndex] = new List(); + AddEvents(trackIndex, trackStart); + } + private void AddEvents(byte trackIndex, int startOffset) + { + EndianBinaryReader r = _player.Config.Reader; + r.Stream.Position = startOffset; + + bool cont = true; + while (cont) + { + long cmdOffset = r.Stream.Position; + byte cmd = r.ReadByte(); + switch (cmd) + { + case 0x00: + { + byte keyArg = r.ReadByte(); + switch (_player.Config.AudioEngineVersion) + { + case AudioEngineVersion.Hamtaro: + { + byte volume = r.ReadByte(); + byte duration = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new FreeNoteHamtaroCommand { Note = (byte)(keyArg - 0x80), Volume = volume, Duration = duration }); + } + break; + } + case AudioEngineVersion.MLSS: + { + byte duration = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new FreeNoteMLSSCommand { Note = (byte)(keyArg - 0x80), Duration = duration }); + } + break; + } + } + break; + } + case 0xF0: + { + byte voice = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VoiceCommand { Voice = voice }); + } + break; + } + case 0xF1: + { + byte volume = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VolumeCommand { Volume = volume }); + } + break; + } + case 0xF2: + { + byte panArg = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PanpotCommand { Panpot = (sbyte)(panArg - 0x80) }); + } + break; + } + case 0xF4: + { + byte range = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PitchBendRangeCommand { Range = range }); + } + break; + } + case 0xF5: + { + sbyte bend = r.ReadSByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PitchBendCommand { Bend = bend }); + } + break; + } + case 0xF6: + { + byte rest = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new RestCommand { Rest = rest }); + } + break; + } + case 0xF8: + { + short jumpOffset = r.ReadInt16(); + if (!EventExists(trackIndex, cmdOffset)) + { + int off = (int)(r.Stream.Position + jumpOffset); + AddEvent(trackIndex, cmdOffset, new JumpCommand { Offset = off }); + if (!EventExists(trackIndex, off)) + { + AddEvents(trackIndex, off); + } + } + cont = false; + break; + } + case 0xF9: + { + byte tempoArg = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new TrackTempoCommand { Tempo = tempoArg }); + } + break; + } + case 0xFF: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new FinishCommand()); + } + cont = false; + break; + } + default: + { + if (cmd >= 0xE0) + { + throw new AlphaDreamInvalidCMDException(trackIndex, (int)cmdOffset, cmd); + } + + byte key = r.ReadByte(); + switch (_player.Config.AudioEngineVersion) + { + case AudioEngineVersion.Hamtaro: + { + byte volume = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new NoteHamtaroCommand { Note = key, Volume = volume, Duration = cmd }); + } + break; + } + case AudioEngineVersion.MLSS: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new NoteMLSSCommand { Note = key, Duration = cmd }); + } + break; + } + } + break; + } + } + } + } + + public void SetTicks() + { + MaxTicks = 0; + bool u = false; + for (int trackIndex = 0; trackIndex < AlphaDreamPlayer.NUM_TRACKS; trackIndex++) + { + List? evs = Events[trackIndex]; + if (evs is null) + { + continue; + } + + evs.Sort((e1, e2) => e1.Offset.CompareTo(e2.Offset)); + + AlphaDreamTrack track = _player.Tracks[trackIndex]; + track.Init(); + + long elapsedTicks = 0; + while (true) + { + SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); + if (e.Ticks.Count > 0) + { + break; + } + + e.Ticks.Add(elapsedTicks); + ExecuteNext(track, ref u); + if (track.Stopped) + { + break; + } + + elapsedTicks += track.Rest; + track.Rest = 0; + } + if (elapsedTicks > MaxTicks) + { + LongestTrack = trackIndex; + MaxTicks = elapsedTicks; + } + track.NoteDuration = 0; + } + } + internal void SetCurTick(long ticks) + { + bool u = false; + while (true) + { + if (_player.ElapsedTicks == ticks) + { + goto finish; + } + + while (_player.TempoStack >= 75) + { + _player.TempoStack -= 75; + for (int trackIndex = 0; trackIndex < AlphaDreamPlayer.NUM_TRACKS; trackIndex++) + { + AlphaDreamTrack track = _player.Tracks[trackIndex]; + if (track.IsEnabled && !track.Stopped) + { + track.Tick(); + while (track.Rest == 0 && !track.Stopped) + { + ExecuteNext(track, ref u); + } + } + } + _player.ElapsedTicks++; + if (_player.ElapsedTicks == ticks) + { + goto finish; + } + } + _player.TempoStack += _player.Tempo; + } + finish: + for (int i = 0; i < AlphaDreamPlayer.NUM_TRACKS; i++) + { + _player.Tracks[i].NoteDuration = 0; + } + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong_Runtime.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong_Runtime.cs new file mode 100644 index 00000000..5b71b596 --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong_Runtime.cs @@ -0,0 +1,160 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +internal sealed partial class AlphaDreamLoadedSong +{ + private static bool TryGetVoiceEntry(byte[] rom, int voiceTableOffset, byte voice, byte key, out VoiceEntry e) + { + short voiceOffset = ReadInt16LittleEndian(rom.AsSpan(voiceTableOffset + (voice * 2))); + short nextVoiceOffset = ReadInt16LittleEndian(rom.AsSpan(voiceTableOffset + ((voice + 1) * 2))); + if (voiceOffset == nextVoiceOffset) + { + e = default; + return false; + } + + int pos = voiceTableOffset + voiceOffset; // Prevent object creation in the last iteration + ref readonly var refE = ref VoiceEntry.Get(rom.AsSpan(pos)); + while (refE.MinKey > key || refE.MaxKey < key) + { + pos += 8; + if (pos == nextVoiceOffset) + { + e = default; + return false; + } + refE = ref VoiceEntry.Get(rom.AsSpan(pos)); + } + e = refE; + return true; + } + private void PlayNote(AlphaDreamTrack track, byte key, byte duration) + { + AlphaDreamConfig cfg = _player.Config; + if (!TryGetVoiceEntry(cfg.ROM, cfg.VoiceTableOffset, track.Voice, key, out VoiceEntry entry)) + { + return; + } + + track.NoteDuration = duration; + if (track.Index >= 8) + { + // TODO: "Sample" byte in VoiceEntry + var sqr = (AlphaDreamSquareChannel)track.Channel; + sqr.Init(key, new ADSR { A = 0xFF, D = 0x00, S = 0xFF, R = 0x00 }, track.Volume, track.Panpot, track.GetPitch()); + } + else + { + int sto = cfg.SampleTableOffset; + int sampleOffset = ReadInt32LittleEndian(cfg.ROM.AsSpan(sto + (entry.Sample * 4))); // Some entries are 0. If you play them, are they silent, or does it not care if they are 0? + + var pcm = (AlphaDreamPCMChannel)track.Channel; + pcm.Init(key, new ADSR { A = 0xFF, D = 0x00, S = 0xFF, R = 0x00 }, sto + sampleOffset, entry.IsFixedFrequency == VoiceEntry.FIXED_FREQ_TRUE); + pcm.SetVolume(track.Volume, track.Panpot); + pcm.SetPitch(track.GetPitch()); + } + } + public void ExecuteNext(AlphaDreamTrack track, ref bool update) + { + byte[] rom = _player.Config.ROM; + byte cmd = rom[track.DataOffset++]; + switch (cmd) + { + case 0x00: // Free Note + { + byte note = (byte)(rom[track.DataOffset++] - 0x80); + if (_player.Config.AudioEngineVersion == AudioEngineVersion.Hamtaro) + { + track.Volume = rom[track.DataOffset++]; + update = true; + } + + byte duration = rom[track.DataOffset++]; + track.Rest += duration; + if (track.PrevCommand == 0 && track.Channel.Key == note) + { + track.NoteDuration += duration; + } + else + { + PlayNote(track, note, duration); + } + break; + } + case <= 0xDF: // Note + { + byte key = rom[track.DataOffset++]; + if (_player.Config.AudioEngineVersion == AudioEngineVersion.Hamtaro) + { + track.Volume = rom[track.DataOffset++]; + update = true; + } + + track.Rest += cmd; + if (track.PrevCommand == 0 && track.Channel.Key == key) + { + track.NoteDuration += cmd; + } + else + { + PlayNote(track, key, cmd); + } + break; + } + case 0xF0: // Voice + { + track.Voice = rom[track.DataOffset++]; + break; + } + case 0xF1: // Volume + { + track.Volume = rom[track.DataOffset++]; + update = true; + break; + } + case 0xF2: // Panpot + { + track.Panpot = (sbyte)(rom[track.DataOffset++] - 0x80); + update = true; + break; + } + case 0xF4: // Pitch Bend Range + { + track.PitchBendRange = rom[track.DataOffset++]; + update = true; + break; + } + case 0xF5: // Pitch Bend + { + track.PitchBend = (sbyte)rom[track.DataOffset++]; + update = true; + break; + } + case 0xF6: // Rest + { + track.Rest = rom[track.DataOffset++]; + break; + } + case 0xF8: // Jump + { + track.DataOffset += 2 + ReadInt16LittleEndian(rom.AsSpan(track.DataOffset)); + break; + } + case 0xF9: // Track Tempo + { + _player.Tempo = rom[track.DataOffset++]; // TODO: Implement per track + break; + } + case 0xFF: // Finish + { + track.Stopped = true; + break; + } + default: throw new AlphaDreamInvalidCMDException(track.Index, track.DataOffset - 1, cmd); + } + + track.PrevCommand = cmd; + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs new file mode 100644 index 00000000..1cc823c5 --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs @@ -0,0 +1,127 @@ +using Kermalis.VGMusicStudio.Core.Util; +using NAudio.Wave; +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +public sealed class AlphaDreamMixer : Mixer +{ + public readonly float SampleRateReciprocal; + private readonly float _samplesReciprocal; + public readonly int SamplesPerBuffer; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; + + public readonly AlphaDreamConfig Config; + private readonly WaveBuffer _audio; + private readonly float[][] _trackBuffers = new float[AlphaDreamPlayer.NUM_TRACKS][]; + private readonly BufferedWaveProvider _buffer; + + protected override WaveFormat WaveFormat => _buffer.WaveFormat; + + internal AlphaDreamMixer(AlphaDreamConfig config) + { + Config = config; + const int sampleRate = 13_379; // TODO: Actual value unknown + SamplesPerBuffer = 224; // TODO + SampleRateReciprocal = 1f / sampleRate; + _samplesReciprocal = 1f / SamplesPerBuffer; + + int amt = SamplesPerBuffer * 2; + _audio = new WaveBuffer(amt * sizeof(float)) { FloatBufferCount = amt }; + for (int i = 0; i < AlphaDreamPlayer.NUM_TRACKS; i++) + { + _trackBuffers[i] = new float[amt]; + } + _buffer = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, 2)) // TODO + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64 + }; + Init(_buffer); + } + + internal void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * GBAUtils.AGB_FPS); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * GBAUtils.AGB_FPS); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal bool IsFading() + { + return _isFading; + } + internal bool IsFadeDone() + { + return _isFading && _fadeMicroFramesLeft == 0; + } + internal void ResetFade() + { + _isFading = false; + _fadeMicroFramesLeft = 0; + } + + internal void Process(AlphaDreamTrack[] tracks, bool output, bool recording) + { + _audio.Clear(); + float masterStep; + float masterLevel; + if (_isFading && _fadeMicroFramesLeft == 0) + { + masterStep = 0; + masterLevel = 0; + } + else + { + float fromMaster = 1f; + float toMaster = 1f; + if (_fadeMicroFramesLeft > 0) + { + const float scale = 10f / 6f; + fromMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadePos += _fadeStepPerMicroframe; + toMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadeMicroFramesLeft--; + } + masterStep = (toMaster - fromMaster) * _samplesReciprocal; + masterLevel = fromMaster; + } + for (int i = 0; i < AlphaDreamPlayer.NUM_TRACKS; i++) + { + AlphaDreamTrack track = tracks[i]; + if (!track.IsEnabled || track.NoteDuration == 0 || track.Channel.Stopped || Mutes[i]) + { + continue; + } + + float level = masterLevel; + float[] buf = _trackBuffers[i]; + Array.Clear(buf, 0, buf.Length); + track.Channel.Process(buf); + for (int j = 0; j < SamplesPerBuffer; j++) + { + _audio.FloatBuffer[j * 2] += buf[j * 2] * level; + _audio.FloatBuffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; + level += masterStep; + } + } + if (output) + { + _buffer.AddSamples(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + } + if (recording) + { + _waveWriter!.Write(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + } + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs new file mode 100644 index 00000000..e202a387 --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs @@ -0,0 +1,167 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +public sealed class AlphaDreamPlayer : Player +{ + internal const int NUM_TRACKS = 12; // 8 PCM, 4 PSG + + protected override string Name => "AlphaDream Player"; + + internal readonly AlphaDreamTrack[] Tracks; + internal readonly AlphaDreamConfig Config; + private readonly AlphaDreamMixer _mixer; + private AlphaDreamLoadedSong? _loadedSong; + + internal byte Tempo; + internal int TempoStack; + private long _elapsedLoops; + + public override ILoadedSong? LoadedSong => _loadedSong; + protected override Mixer Mixer => _mixer; + + internal AlphaDreamPlayer(AlphaDreamConfig config, AlphaDreamMixer mixer) + : base(GBAUtils.AGB_FPS) + { + Config = config; + _mixer = mixer; + + Tracks = new AlphaDreamTrack[NUM_TRACKS]; + for (byte i = 0; i < NUM_TRACKS; i++) + { + Tracks[i] = new AlphaDreamTrack(i, mixer); + } + } + + public override void LoadSong(int index) + { + if (_loadedSong is not null) + { + _loadedSong = null; + } + + int songPtr = Config.SongTableOffsets[0] + (index * 4); + int songOffset = ReadInt32LittleEndian(Config.ROM.AsSpan(songPtr)); + if (songOffset == 0) + { + return; + } + + // If there's an exception, this will remain null + _loadedSong = new AlphaDreamLoadedSong(this, songOffset); + _loadedSong.SetTicks(); + } + public override void UpdateSongState(SongState info) + { + info.Tempo = Tempo; + for (int i = 0; i < NUM_TRACKS; i++) + { + AlphaDreamTrack track = Tracks[i]; + if (track.IsEnabled) + { + track.UpdateSongState(info.Tracks[i]); + } + } + } + internal override void InitEmulation() + { + Tempo = 120; // Player tempo is set to 75 on init, but I did not separate player and track tempo yet + TempoStack = 0; + _elapsedLoops = 0; + ElapsedTicks = 0; + _mixer.ResetFade(); + for (int i = 0; i < NUM_TRACKS; i++) + { + Tracks[i].Init(); + } + } + protected override void SetCurTick(long ticks) + { + _loadedSong!.SetCurTick(ticks); + } + protected override void OnStopped() + { + // + } + + protected override bool Tick(bool playing, bool recording) + { + bool allDone = false; // TODO: Individual track tempo + while (!allDone && TempoStack >= 75) + { + TempoStack -= 75; + allDone = true; + for (int i = 0; i < NUM_TRACKS; i++) + { + AlphaDreamTrack track = Tracks[i]; + if (track.IsEnabled) + { + TickTrack(track, ref allDone); + } + } + if (_mixer.IsFadeDone()) + { + allDone = true; + } + } + if (!allDone) + { + TempoStack += Tempo; + } + _mixer.Process(Tracks, playing, recording); + return allDone; + } + private void TickTrack(AlphaDreamTrack track, ref bool allDone) + { + byte prevDuration = track.NoteDuration; + track.Tick(); + bool update = false; + while (track.Rest == 0 && !track.Stopped) + { + _loadedSong!.ExecuteNext(track, ref update); + } + if (track.Index == _loadedSong!.LongestTrack) + { + HandleTicksAndLoop(_loadedSong, track); + } + if (prevDuration == 1 && track.NoteDuration == 0) // Note was not renewed + { + track.Channel.State = EnvelopeState.Release; + } + if (track.NoteDuration != 0) // A note is playing + { + allDone = false; + if (update) + { + track.Channel.SetVolume(track.Volume, track.Panpot); + track.Channel.SetPitch(track.GetPitch()); + } + } + if (!track.Stopped) + { + allDone = false; + } + } + private void HandleTicksAndLoop(AlphaDreamLoadedSong s, AlphaDreamTrack track) + { + if (ElapsedTicks != s.MaxTicks) + { + ElapsedTicks++; + return; + } + + // Track reached the detected end, update loops/ticks accordingly + if (track.Stopped) + { + return; + } + + _elapsedLoops++; + UpdateElapsedTicksAfterLoop(s.Events[track.Index]!, track.DataOffset, track.Rest); + if (ShouldFadeOut && _elapsedLoops > NumLoops && !_mixer.IsFading()) + { + _mixer.BeginFadeOut(); + } + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_DLS.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_DLS.cs new file mode 100644 index 00000000..606f9b58 --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_DLS.cs @@ -0,0 +1,183 @@ +using Kermalis.DLS2; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +public static class AlphaDreamSoundFontSaver_DLS +{ + // Since every key will use the same articulation data, just store one instance + private static readonly Level2ArticulatorChunk _art2 = new() + { + new Level2ArticulatorConnectionBlock { Destination = Level2ArticulatorDestination.LFOFrequency, Scale = 2786, }, + new Level2ArticulatorConnectionBlock { Destination = Level2ArticulatorDestination.VIBFrequency, Scale = 2786, }, + new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.KeyNumber, Destination = Level2ArticulatorDestination.Pitch, }, + new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.Vibrato, Control = Level2ArticulatorSource.Modulation_CC1, Destination = Level2ArticulatorDestination.Pitch, BipolarSource = true, Scale = 0x320000, }, + new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.Vibrato, Control = Level2ArticulatorSource.ChannelPressure, Destination = Level2ArticulatorDestination.Pitch, BipolarSource = true, Scale = 0x320000, }, + new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.Pan_CC10, Destination = Level2ArticulatorDestination.Pan, BipolarSource = true, Scale = 0xFE0000, }, + new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.ChorusSend_CC91, Destination = Level2ArticulatorDestination.Reverb, Scale = 0xC80000, }, + new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.Reverb_SendCC93, Destination = Level2ArticulatorDestination.Chorus, Scale = 0xC80000, }, + }; + + public static void Save(AlphaDreamConfig config, string path) + { + var dls = new DLS(); + AddInfo(config, dls); + Dictionary sampleDict = AddSamples(config, dls); + AddInstruments(config, dls, sampleDict); + dls.Save(path); + } + + private static void AddInfo(AlphaDreamConfig config, DLS dls) + { + var info = new ListChunk("INFO"); + dls.Add(info); + info.Add(new InfoSubChunk("INAM", config.Name)); + //info.Add(new InfoSubChunk("ICOP", config.Creator)); + info.Add(new InfoSubChunk("IENG", "Kermalis")); + info.Add(new InfoSubChunk("ISFT", ConfigUtils.PROGRAM_NAME)); + } + + private static Dictionary AddSamples(AlphaDreamConfig config, DLS dls) + { + ListChunk waves = dls.WavePool; + var sampleDict = new Dictionary((int)config.SampleTableSize); + for (int i = 0; i < config.SampleTableSize; i++) + { + int ofs = BinaryPrimitives.ReadInt32LittleEndian(config.ROM.AsSpan(config.SampleTableOffset + (i * 4))); + if (ofs == 0) + { + continue; // Skip null samples + } + + ofs += config.SampleTableOffset; + var sh = new SampleHeader(config.ROM, ofs, out int sampleOffset); + + // Create format chunk + var fmt = new FormatChunk(WaveFormat.PCM); + fmt.WaveInfo.Channels = 1; + fmt.WaveInfo.SamplesPerSec = (uint)sh.SampleRate >> 10; + fmt.WaveInfo.AvgBytesPerSec = fmt.WaveInfo.SamplesPerSec; + fmt.WaveInfo.BlockAlign = 1; + fmt.FormatInfo.BitsPerSample = 8; + // Create wave sample chunk and add loop if there is one + var wsmp = new WaveSampleChunk + { + UnityNote = 60, + Options = WaveSampleOptions.NoTruncation | WaveSampleOptions.NoCompression, + }; + if (sh.DoesLoop == SampleHeader.LOOP_TRUE) + { + wsmp.Loop = new WaveSampleLoop + { + LoopStart = (uint)sh.LoopOffset, + LoopLength = (uint)(sh.Length - sh.LoopOffset), + LoopType = LoopType.Forward, + }; + } + // Get PCM sample + byte[] pcm = new byte[sh.Length]; + Array.Copy(config.ROM, sampleOffset, pcm, 0, sh.Length); + + // Add + int dlsIndex = waves.Count; + waves.Add(new ListChunk("wave") + { + fmt, + wsmp, + new DataChunk(pcm), + new ListChunk("INFO") + { + new InfoSubChunk("INAM", $"Sample {i}"), + }, + }); + sampleDict.Add(i, (wsmp, dlsIndex)); + } + return sampleDict; + } + + private static void AddInstruments(AlphaDreamConfig config, DLS dls, Dictionary sampleDict) + { + ListChunk lins = dls.InstrumentList; + for (int v = 0; v < 256; v++) + { + short off = BinaryPrimitives.ReadInt16LittleEndian(config.ROM.AsSpan(config.VoiceTableOffset + (v * 2))); + short nextOff = BinaryPrimitives.ReadInt16LittleEndian(config.ROM.AsSpan(config.VoiceTableOffset + ((v + 1) * 2))); + int numEntries = (nextOff - off) / 8; // Each entry is 8 bytes + if (numEntries == 0) + { + continue; // Skip empty entries + } + + var ins = new ListChunk("ins "); + ins.Add(new InstrumentHeaderChunk(new MIDILocale(0, (byte)(v / 128), false, (byte)(v % 128))) + { + NumRegions = (uint)numEntries, + }); + var lrgn = new ListChunk("lrgn"); + ins.Add(lrgn); + ins.Add(new ListChunk("INFO") + { + new InfoSubChunk("INAM", $"Instrument {v}") + }); + lins.Add(ins); + for (int e = 0; e < numEntries; e++) + { + ref readonly var entry = ref VoiceEntry.Get(config.ROM.AsSpan(config.VoiceTableOffset + off + (e * 8))); + // Sample + if (entry.Sample >= config.SampleTableSize) + { + Debug.WriteLine(string.Format("Voice {0} uses an invalid sample id ({1})", v, entry.Sample)); + continue; + } + if (!sampleDict.TryGetValue(entry.Sample, out (WaveSampleChunk, int) value)) + { + Debug.WriteLine(string.Format("Voice {0} uses a null sample id ({1})", v, entry.Sample)); + continue; + } + + void Add(ushort low, ushort high, ushort baseNote) + { + var rgnh = new RegionHeaderChunk(); + rgnh.KeyRange.Low = low; + rgnh.KeyRange.High = high; + lrgn.Add(new ListChunk("rgn2") + { + rgnh, + new WaveSampleChunk + { + UnityNote = baseNote, + Options = WaveSampleOptions.NoTruncation | WaveSampleOptions.NoCompression, + Loop = value.Item1.Loop, + }, + new WaveLinkChunk + { + Channels = WaveLinkChannels.Left, + TableIndex = (uint)value.Item2, + }, + new ListChunk("lar2") + { + _art2, + } + }); + } + + // Fixed frequency - Since DLS does not support it, we need to manually add every key with its own base note + if (entry.IsFixedFrequency == VoiceEntry.FIXED_FREQ_TRUE) + { + for (ushort i = entry.MinKey; i <= entry.MaxKey; i++) + { + Add(i, i, i); + } + } + else + { + Add(entry.MinKey, entry.MaxKey, 60); + } + } + } + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_SF2.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_SF2.cs new file mode 100644 index 00000000..30c0529f --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_SF2.cs @@ -0,0 +1,105 @@ +using Kermalis.SoundFont2; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +public static class AlphaDreamSoundFontSaver_SF2 +{ + public static void Save(string path, AlphaDreamConfig cfg) + { + Save(path, cfg.ROM, cfg.Name, cfg.SampleTableOffset, (int)cfg.SampleTableSize, cfg.VoiceTableOffset); + } + private static void Save(string path, + byte[] rom, string romName, + int sampleTableOffset, int sampleTableSize, int voiceTableOffset) + { + var sf2 = new SF2(); + AddInfo(romName, sf2.InfoChunk); + Dictionary sampleDict = AddSamples(rom, sampleTableOffset, sampleTableSize, sf2); + AddInstruments(rom, voiceTableOffset, sampleTableSize, sf2, sampleDict); + sf2.Save(path); + } + + private static void AddInfo(string romName, InfoListChunk chunk) + { + chunk.Bank = romName; + //chunk.Copyright = config.Creator; + chunk.Tools = ConfigUtils.PROGRAM_NAME + " by Kermalis"; + } + + private static Dictionary AddSamples(byte[] rom, int sampleTableOffset, int sampleTableSize, SF2 sf2) + { + var sampleDict = new Dictionary(sampleTableSize); + for (int i = 0; i < sampleTableSize; i++) + { + int ofs = ReadInt32LittleEndian(rom.AsSpan(sampleTableOffset + (i * 4))); + if (ofs == 0) + { + continue; + } + + ofs += sampleTableOffset; + var sh = new SampleHeader(rom, ofs, out int sampleOffset); + + short[] pcm16 = new short[sh.Length]; + SampleUtils.PCMU8ToPCM16(rom.AsSpan(sampleOffset), pcm16); + int sf2Index = (int)sf2.AddSample(pcm16, $"Sample {i}", sh.DoesLoop == SampleHeader.LOOP_TRUE, (uint)sh.LoopOffset, (uint)sh.SampleRate >> 10, 60, 0); + sampleDict.Add(i, (sh, sf2Index)); + } + return sampleDict; + } + private static void AddInstruments(byte[] rom, int voiceTableOffset, int sampleTableSize, SF2 sf2, Dictionary sampleDict) + { + for (ushort v = 0; v < 256; v++) + { + short off = ReadInt16LittleEndian(rom.AsSpan(voiceTableOffset + (v * 2))); + short nextOff = ReadInt16LittleEndian(rom.AsSpan(voiceTableOffset + ((v + 1) * 2))); + int numEntries = (nextOff - off) / 8; // Each entry is 8 bytes + if (numEntries == 0) + { + continue; + } + + string name = "Instrument " + v; + sf2.AddPreset(name, v, 0); + sf2.AddPresetBag(); + sf2.AddPresetGenerator(SF2Generator.Instrument, new SF2GeneratorAmount { Amount = (short)sf2.AddInstrument(name) }); + for (int e = 0; e < numEntries; e++) + { + ref readonly var entry = ref VoiceEntry.Get(rom.AsSpan(voiceTableOffset + off + (e * 8))); + sf2.AddInstrumentBag(); + // Key range + if (entry.MinKey != 0 || entry.MaxKey != 0x7F) + { + sf2.AddInstrumentGenerator(SF2Generator.KeyRange, new SF2GeneratorAmount { LowByte = entry.MinKey, HighByte = entry.MaxKey }); + } + // Fixed frequency + if (entry.IsFixedFrequency == VoiceEntry.FIXED_FREQ_TRUE) + { + sf2.AddInstrumentGenerator(SF2Generator.ScaleTuning, new SF2GeneratorAmount { Amount = 0 }); + } + // Sample + if (entry.Sample < sampleTableSize) + { + if (!sampleDict.TryGetValue(entry.Sample, out (SampleHeader, int) value)) + { + Debug.WriteLine(string.Format("Voice {0} uses a null sample id ({1})", v, entry.Sample)); + } + else + { + sf2.AddInstrumentGenerator(SF2Generator.SampleModes, new SF2GeneratorAmount { Amount = (short)(value.Item1.DoesLoop == SampleHeader.LOOP_TRUE ? 1 : 0), }); + sf2.AddInstrumentGenerator(SF2Generator.SampleID, new SF2GeneratorAmount { UAmount = (ushort)value.Item2, }); + } + } + else + { + Debug.WriteLine(string.Format("Voice {0} uses an invalid sample id ({1})", v, entry.Sample)); + } + } + } + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamStructs.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamStructs.cs new file mode 100644 index 00000000..720c72eb --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamStructs.cs @@ -0,0 +1,67 @@ +using System; +using System.Runtime.InteropServices; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = SIZE)] +internal readonly struct SampleHeader +{ + public const int SIZE = 16; + public const int LOOP_TRUE = 0x40_000_000; + + /// 0x40_000_000 if True + public readonly int DoesLoop; + /// Right shift 10 for value + public readonly int SampleRate; + public readonly int LoopOffset; + public readonly int Length; + // byte[Length] Sample; + + public SampleHeader(byte[] rom, int offset, out int sampleOffset) + { + ReadOnlySpan data = rom.AsSpan(offset, SIZE); + if (BitConverter.IsLittleEndian) + { + this = MemoryMarshal.AsRef(data); + } + else + { + DoesLoop = ReadInt32LittleEndian(data.Slice(0, 4)); + SampleRate = ReadInt32LittleEndian(data.Slice(4, 4)); + LoopOffset = ReadInt32LittleEndian(data.Slice(8, 4)); + Length = ReadInt32LittleEndian(data.Slice(12, 4)); + } + sampleOffset = offset + SIZE; + } +} +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = SIZE)] +internal readonly struct VoiceEntry +{ + public const int SIZE = 8; + public const byte FIXED_FREQ_TRUE = 0x80; + + public readonly byte MinKey; + public readonly byte MaxKey; + public readonly byte Sample; + /// 0x80 if True + public readonly byte IsFixedFrequency; + public readonly byte Unknown1; + public readonly byte Unknown2; + public readonly byte Unknown3; + public readonly byte Unknown4; + + public static ref readonly VoiceEntry Get(ReadOnlySpan src) + { + return ref MemoryMarshal.AsRef(src); + } +} + +internal struct ChannelVolume +{ + public float LeftVol, RightVol; +} +internal struct ADSR // TODO +{ + public byte A, D, S, R; +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamTrack.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamTrack.cs new file mode 100644 index 00000000..ded6a340 --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamTrack.cs @@ -0,0 +1,92 @@ +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +internal sealed class AlphaDreamTrack +{ + public readonly byte Index; + public readonly string Type; + public readonly AlphaDreamChannel Channel; + + public byte Voice; + public byte PitchBendRange; + public byte Volume; + public byte Rest; + public byte NoteDuration; + public sbyte PitchBend; + public sbyte Panpot; + public bool IsEnabled; + public bool Stopped; + public int StartOffset; + public int DataOffset; + public byte PrevCommand; + + public int GetPitch() + { + return PitchBend * (PitchBendRange / 2); + } + + public AlphaDreamTrack(byte i, AlphaDreamMixer mixer) + { + Index = i; + if (i >= 8) + { + Type = GBAUtils.PSGTypes[i & 3]; + Channel = new AlphaDreamSquareChannel(mixer); // TODO: PSG Channels 3 and 4 + } + else + { + Type = "PCM8"; + Channel = new AlphaDreamPCMChannel(mixer); + } + } + // 0x819B040 + public void Init() + { + Voice = 0; + Rest = 1; // Unsure why Rest starts at 1 + PitchBendRange = 2; + NoteDuration = 0; + PitchBend = 0; + Panpot = 0; // Start centered; ROM sets this to 0x7F since it's unsigned there + DataOffset = StartOffset; + Stopped = false; + Volume = 200; + PrevCommand = 0xFF; + //Tempo = 120; + //TempoStack = 0; + } + public void Tick() + { + if (Rest != 0) + { + Rest--; + } + if (NoteDuration > 0) + { + NoteDuration--; + } + } + + public void UpdateSongState(SongState.Track tin) + { + tin.Position = DataOffset; + tin.Rest = Rest; + tin.Voice = Voice; + tin.Type = Type; + tin.Volume = Volume; + tin.PitchBend = GetPitch(); + tin.Panpot = Panpot; + if (NoteDuration != 0 && !Channel.Stopped) + { + tin.Keys[0] = Channel.Key; + ChannelVolume vol = Channel.GetVolume(); + tin.LeftVolume = vol.LeftVol; + tin.RightVolume = vol.RightVol; + } + else + { + tin.Keys[0] = byte.MaxValue; + tin.LeftVolume = 0f; + tin.RightVolume = 0f; + } + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamChannel.cs b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamChannel.cs new file mode 100644 index 00000000..471fdb48 --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamChannel.cs @@ -0,0 +1,41 @@ +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +internal abstract class AlphaDreamChannel +{ + protected readonly AlphaDreamMixer _mixer; + public EnvelopeState State; + public byte Key; + public bool Stopped; + + protected ADSR _adsr; + + protected byte _velocity; + protected int _pos; + protected float _interPos; + protected float _frequency; + protected byte _leftVol; + protected byte _rightVol; + + protected AlphaDreamChannel(AlphaDreamMixer mixer) + { + _mixer = mixer; + } + + public ChannelVolume GetVolume() + { + const float MAX = 1f / 0x10000; + return new ChannelVolume + { + LeftVol = _leftVol * _velocity * MAX, + RightVol = _rightVol * _velocity * MAX, + }; + } + public void SetVolume(byte vol, sbyte pan) + { + _leftVol = (byte)((vol * (-pan + 0x80)) >> 8); + _rightVol = (byte)((vol * (pan + 0x80)) >> 8); + } + public abstract void SetPitch(int pitch); + + public abstract void Process(float[] buffer); +} \ No newline at end of file diff --git a/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamPCMChannel.cs b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamPCMChannel.cs new file mode 100644 index 00000000..455dc77e --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamPCMChannel.cs @@ -0,0 +1,110 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +internal sealed class AlphaDreamPCMChannel : AlphaDreamChannel +{ + private SampleHeader _sampleHeader; + private int _sampleOffset; + private bool _bFixed; + + public AlphaDreamPCMChannel(AlphaDreamMixer mixer) : base(mixer) + { + // + } + public void Init(byte key, ADSR adsr, int sampleOffset, bool bFixed) + { + _velocity = adsr.A; + State = EnvelopeState.Attack; + _pos = 0; _interPos = 0; + Key = key; + _adsr = adsr; + + _sampleHeader = new SampleHeader(_mixer.Config.ROM, sampleOffset, out _sampleOffset); + _bFixed = bFixed; + Stopped = false; + } + + public override void SetPitch(int pitch) + { + _frequency = (_sampleHeader.SampleRate >> 10) * MathF.Pow(2, ((Key - 60) / 12f) + (pitch / 768f)); + } + + private void StepEnvelope() + { + switch (State) + { + case EnvelopeState.Attack: + { + int nextVel = _velocity + _adsr.A; + if (nextVel >= 0xFF) + { + State = EnvelopeState.Decay; + _velocity = 0xFF; + } + else + { + _velocity = (byte)nextVel; + } + break; + } + case EnvelopeState.Decay: + { + int nextVel = (_velocity * _adsr.D) >> 8; + if (nextVel <= _adsr.S) + { + State = EnvelopeState.Sustain; + _velocity = _adsr.S; + } + else + { + _velocity = (byte)nextVel; + } + break; + } + case EnvelopeState.Release: + { + int next = (_velocity * _adsr.R) >> 8; + if (next < 0) + { + next = 0; + } + _velocity = (byte)next; + break; + } + } + } + + public override void Process(float[] buffer) + { + StepEnvelope(); + + ChannelVolume vol = GetVolume(); + float interStep = (_bFixed ? _sampleHeader.SampleRate >> 10 : _frequency) * _mixer.SampleRateReciprocal; + int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = (_mixer.Config.ROM[_pos + _sampleOffset] - 0x80) / (float)0x80; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos += posDelta; + if (_pos >= _sampleHeader.Length) + { + if (_sampleHeader.DoesLoop == 0x40000000) + { + _pos = _sampleHeader.LoopOffset; + } + else + { + Stopped = true; + break; + } + } + } while (--samplesPerBuffer > 0); + } +} \ No newline at end of file diff --git a/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamSquareChannel.cs b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamSquareChannel.cs new file mode 100644 index 00000000..a627bf07 --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamSquareChannel.cs @@ -0,0 +1,96 @@ +using Kermalis.VGMusicStudio.Core.GBA.MP2K; +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +internal sealed class AlphaDreamSquareChannel : AlphaDreamChannel +{ + private float[] _pat; + + public AlphaDreamSquareChannel(AlphaDreamMixer mixer) + : base(mixer) + { + _pat = null!; + } + public void Init(byte key, ADSR env, byte vol, sbyte pan, int pitch) + { + _pat = MP2KUtils.SquareD50; // TODO: Which square pattern? + Key = key; + _adsr = env; + SetVolume(vol, pan); + SetPitch(pitch); + State = EnvelopeState.Attack; + } + + public override void SetPitch(int pitch) + { + _frequency = 3_520 * MathF.Pow(2, ((Key - 69) / 12f) + (pitch / 768f)); + } + + private void StepEnvelope() + { + switch (State) + { + case EnvelopeState.Attack: + { + int next = _velocity + _adsr.A; + if (next >= 0xF) + { + State = EnvelopeState.Decay; + _velocity = 0xF; + } + else + { + _velocity = (byte)next; + } + break; + } + case EnvelopeState.Decay: + { + int next = (_velocity * _adsr.D) >> 3; + if (next <= _adsr.S) + { + State = EnvelopeState.Sustain; + _velocity = _adsr.S; + } + else + { + _velocity = (byte)next; + } + break; + } + case EnvelopeState.Release: + { + int next = (_velocity * _adsr.R) >> 3; + if (next < 0) + { + next = 0; + } + _velocity = (byte)next; + break; + } + } + } + + public override void Process(float[] buffer) + { + StepEnvelope(); + + ChannelVolume vol = GetVolume(); + float interStep = _frequency * _mixer.SampleRateReciprocal; + + int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = _pat[_pos]; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos = (_pos + posDelta) & 0x7; + } while (--samplesPerBuffer > 0); + } +} \ No newline at end of file diff --git a/VG Music Studio - Core/GBA/GBAUtils.cs b/VG Music Studio - Core/GBA/GBAUtils.cs new file mode 100644 index 00000000..ca4ecf1d --- /dev/null +++ b/VG Music Studio - Core/GBA/GBAUtils.cs @@ -0,0 +1,14 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA; + +internal static class GBAUtils +{ + public const double AGB_FPS = 59.7275; + public const int SYSTEM_CLOCK = 16_777_216; // 16.777216 MHz (16*1024*1024 Hz) + + public const int CARTRIDGE_OFFSET = 0x08_000_000; + public const int CARTRIDGE_CAPACITY = 0x02_000_000; + + public static ReadOnlySpan PSGTypes => new string[4] { "Square 1", "Square 2", "PCM4", "Noise" }; +} diff --git a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KChannel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KChannel.cs new file mode 100644 index 00000000..3d352412 --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KChannel.cs @@ -0,0 +1,62 @@ +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal abstract class MP2KChannel +{ + public EnvelopeState State; + public MP2KTrack? Owner; + protected readonly MP2KMixer _mixer; + + public NoteInfo Note; + protected ADSR _adsr; + protected int _instPan; + + protected byte _velocity; + protected int _pos; + protected float _interPos; + protected float _frequency; + + protected MP2KChannel(MP2KMixer mixer) + { + _mixer = mixer; + State = EnvelopeState.Dead; + } + + public abstract ChannelVolume GetVolume(); + public abstract void SetVolume(byte vol, sbyte pan); + public abstract void SetPitch(int pitch); + public virtual void Release() + { + if (State < EnvelopeState.Releasing) + { + State = EnvelopeState.Releasing; + } + } + + public abstract void Process(float[] buffer); + + /// Returns whether the note is active or not + public virtual bool TickNote() + { + if (State >= EnvelopeState.Releasing) + { + return false; + } + + if (Note.Duration > 0) + { + Note.Duration--; + if (Note.Duration == 0) + { + State = EnvelopeState.Releasing; + return false; + } + } + return true; + } + public void Stop() + { + State = EnvelopeState.Dead; + Owner?.Channels.Remove(this); + Owner = null; + } +} \ No newline at end of file diff --git a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KNoiseChannel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KNoiseChannel.cs new file mode 100644 index 00000000..43893524 --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KNoiseChannel.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal sealed class MP2KNoiseChannel : MP2KPSGChannel +{ + private BitArray _pat; + + public MP2KNoiseChannel(MP2KMixer mixer) + : base(mixer) + { + _pat = null!; + } + public void Init(MP2KTrack owner, NoteInfo note, ADSR env, int instPan, NoisePattern pattern) + { + Init(owner, note, env, instPan); + _pat = pattern == NoisePattern.Fine ? MP2KUtils.NoiseFine : MP2KUtils.NoiseRough; + } + + public override void SetPitch(int pitch) + { + int key = Note.Note + (int)MathF.Round(pitch / 64f); + if (key <= 20) + { + key = 0; + } + else + { + key -= 21; + if (key > 59) + { + key = 59; + } + } + byte v = MP2KUtils.NoiseFrequencyTable[key]; + // The following emulates 0x0400007C - SOUND4CNT_H + int r = v & 7; // Bits 0-2 + int s = v >> 4; // Bits 4-7 + _frequency = 524_288f / (r == 0 ? 0.5f : r) / MathF.Pow(2, s + 1); + } + + public override void Process(float[] buffer) + { + StepEnvelope(); + if (State == EnvelopeState.Dead) + { + return; + } + + ChannelVolume vol = GetVolume(); + float interStep = _frequency * _mixer.SampleRateReciprocal; + + int bufPos = 0; + int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = _pat[_pos & (_pat.Length - 1)] ? 0.5f : -0.5f; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos = (_pos + posDelta) & (_pat.Length - 1); + } while (--samplesPerBuffer > 0); + } +} \ No newline at end of file diff --git a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM4Channel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM4Channel.cs new file mode 100644 index 00000000..90ba63ba --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM4Channel.cs @@ -0,0 +1,51 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal sealed class MP2KPCM4Channel : MP2KPSGChannel +{ + private readonly float[] _sample; + + public MP2KPCM4Channel(MP2KMixer mixer) + : base(mixer) + { + _sample = new float[0x20]; + } + public void Init(MP2KTrack owner, NoteInfo note, ADSR env, int instPan, int sampleOffset) + { + Init(owner, note, env, instPan); + MP2KUtils.PCM4ToFloat(_mixer.Config.ROM.AsSpan(sampleOffset), _sample); + } + + public override void SetPitch(int pitch) + { + _frequency = 7_040 * MathF.Pow(2, ((Note.Note - 69) / 12f) + (pitch / 768f)); + } + + public override void Process(float[] buffer) + { + StepEnvelope(); + if (State == EnvelopeState.Dead) + { + return; + } + + ChannelVolume vol = GetVolume(); + float interStep = _frequency * _mixer.SampleRateReciprocal; + + int bufPos = 0; + int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = _sample[_pos]; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos = (_pos + posDelta) & 0x1F; + } while (--samplesPerBuffer > 0); + } +} \ No newline at end of file diff --git a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM8Channel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM8Channel.cs new file mode 100644 index 00000000..552e230b --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM8Channel.cs @@ -0,0 +1,294 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal sealed class MP2KPCM8Channel : MP2KChannel +{ + private SampleHeader _sampleHeader; + private int _sampleOffset; + private GoldenSunPSG _gsPSG; + private bool _bFixed; + private bool _bGoldenSun; + private bool _bCompressed; + private byte _leftVol; + private byte _rightVol; + private sbyte[]? _decompressedSample; + + public MP2KPCM8Channel(MP2KMixer mixer) + : base(mixer) + { + // + } + public void Init(MP2KTrack owner, NoteInfo note, ADSR adsr, int sampleOffset, byte vol, sbyte pan, int instPan, int pitch, bool bFixed, bool bCompressed) + { + State = EnvelopeState.Initializing; + _pos = 0; + _interPos = 0; + if (Owner is not null) + { + Owner.Channels.Remove(this); + } + Owner = owner; + Owner.Channels.Add(this); + Note = note; + _adsr = adsr; + _instPan = instPan; + byte[] rom = _mixer.Config.ROM; + _sampleHeader = SampleHeader.Get(rom, sampleOffset, out _sampleOffset); + _bFixed = bFixed; + _bCompressed = bCompressed; + _decompressedSample = bCompressed ? MP2KUtils.Decompress(rom.AsSpan(_sampleOffset), _sampleHeader.Length) : null; + _bGoldenSun = _mixer.Config.HasGoldenSunSynths && _sampleHeader.Length == 0 && _sampleHeader.DoesLoop == SampleHeader.LOOP_TRUE && _sampleHeader.LoopOffset == 0; + if (_bGoldenSun) + { + _gsPSG = GoldenSunPSG.Get(rom.AsSpan(_sampleOffset)); + } + SetVolume(vol, pan); + SetPitch(pitch); + } + + public override ChannelVolume GetVolume() + { + const float MAX = 0x10_000; + return new ChannelVolume + { + LeftVol = _leftVol * _velocity / MAX * _mixer.PCM8MasterVolume, + RightVol = _rightVol * _velocity / MAX * _mixer.PCM8MasterVolume + }; + } + public override void SetVolume(byte vol, sbyte pan) + { + int combinedPan = pan + _instPan; + if (combinedPan > 63) + { + combinedPan = 63; + } + else if (combinedPan < -64) + { + combinedPan = -64; + } + const int fix = 0x2000; + if (State < EnvelopeState.Releasing) + { + int a = Note.Velocity * vol; + _leftVol = (byte)(a * (-combinedPan + 0x40) / fix); + _rightVol = (byte)(a * (combinedPan + 0x40) / fix); + } + } + public override void SetPitch(int pitch) + { + _frequency = (_sampleHeader.SampleRate >> 10) * MathF.Pow(2, ((Note.Note - 60) / 12f) + (pitch / 768f)); + } + + private void StepEnvelope() + { + switch (State) + { + case EnvelopeState.Initializing: + { + _velocity = _adsr.A; + State = EnvelopeState.Rising; + break; + } + case EnvelopeState.Rising: + { + int nextVel = _velocity + _adsr.A; + if (nextVel >= 0xFF) + { + State = EnvelopeState.Decaying; + _velocity = 0xFF; + } + else + { + _velocity = (byte)nextVel; + } + break; + } + case EnvelopeState.Decaying: + { + int nextVel = (_velocity * _adsr.D) >> 8; + if (nextVel <= _adsr.S) + { + State = EnvelopeState.Playing; + _velocity = _adsr.S; + } + else + { + _velocity = (byte)nextVel; + } + break; + } + case EnvelopeState.Playing: + { + break; + } + case EnvelopeState.Releasing: + { + int nextVel = (_velocity * _adsr.R) >> 8; + if (nextVel <= 0) + { + State = EnvelopeState.Dying; + _velocity = 0; + } + else + { + _velocity = (byte)nextVel; + } + break; + } + case EnvelopeState.Dying: + { + Stop(); + break; + } + } + } + + public override void Process(float[] buffer) + { + StepEnvelope(); + if (State == EnvelopeState.Dead) + { + return; + } + + ChannelVolume vol = GetVolume(); + float interStep = _bFixed && !_bGoldenSun ? _mixer.SampleRate * _mixer.SampleRateReciprocal : _frequency * _mixer.SampleRateReciprocal; + if (_bGoldenSun) // Most Golden Sun processing is thanks to ipatix + { + Process_GS(buffer, vol, interStep); + } + else if (_bCompressed) + { + Process_Compressed(buffer, vol, interStep); + } + else + { + Process_Standard(buffer, vol, interStep, _mixer.Config.ROM); + } + } + private void Process_GS(float[] buffer, ChannelVolume vol, float interStep) + { + interStep /= 0x40; + switch (_gsPSG.Type) + { + case GoldenSunPSGType.Square: + { + _pos += _gsPSG.CycleSpeed << 24; + int iThreshold = (_gsPSG.MinimumCycle << 24) + _pos; + iThreshold = (iThreshold < 0 ? ~iThreshold : iThreshold) >> 8; + iThreshold = (iThreshold * _gsPSG.CycleAmplitude) + (_gsPSG.InitialCycle << 24); + float threshold = iThreshold / (float)0x100_000_000; + + int bufPos = 0; + int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = _interPos < threshold ? 0.5f : -0.5f; + samp += 0.5f - threshold; + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + if (_interPos >= 1) + { + _interPos--; + } + } while (--samplesPerBuffer > 0); + break; + } + case GoldenSunPSGType.Saw: + { + const int FIX = 0x70; + + int bufPos = 0; + int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + _interPos += interStep; + if (_interPos >= 1) + { + _interPos--; + } + int var1 = (int)(_interPos * 0x100) - FIX; + int var2 = (int)(_interPos * 0x10000) << 17; + int var3 = var1 - (var2 >> 27); + _pos = var3 + (_pos >> 1); + + float samp = _pos / (float)0x100; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + } while (--samplesPerBuffer > 0); + break; + } + case GoldenSunPSGType.Triangle: + { + int bufPos = 0; + int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + _interPos += interStep; + if (_interPos >= 1) + { + _interPos--; + } + float samp = _interPos < 0.5f ? (_interPos * 4) - 1 : 3 - (_interPos * 4); + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + } while (--samplesPerBuffer > 0); + break; + } + } + } + private void Process_Compressed(float[] buffer, ChannelVolume vol, float interStep) + { + int bufPos = 0; + int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = _decompressedSample![_pos] / (float)0x80; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos += posDelta; + if (_pos >= _decompressedSample.Length) + { + Stop(); + break; + } + } while (--samplesPerBuffer > 0); + } + private void Process_Standard(float[] buffer, ChannelVolume vol, float interStep, byte[] rom) + { + int bufPos = 0; + int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = (sbyte)rom[_pos + _sampleOffset] / (float)0x80; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos += posDelta; + if (_pos >= _sampleHeader.Length) + { + if (_sampleHeader.DoesLoop != SampleHeader.LOOP_TRUE) + { + Stop(); + return; + } + + _pos = _sampleHeader.LoopOffset; + } + } while (--samplesPerBuffer > 0); + } +} \ No newline at end of file diff --git a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPSGChannel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPSGChannel.cs new file mode 100644 index 00000000..bc795dff --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPSGChannel.cs @@ -0,0 +1,282 @@ +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal abstract class MP2KPSGChannel : MP2KChannel +{ + protected enum GBPan : byte + { + Left, + Center, + Right, + } + + private byte _processStep; + private EnvelopeState _nextState; + private byte _peakVelocity; + private byte _sustainVelocity; + protected GBPan _panpot = GBPan.Center; + + public MP2KPSGChannel(MP2KMixer mixer) + : base(mixer) + { + // + } + protected void Init(MP2KTrack owner, NoteInfo note, ADSR env, int instPan) + { + State = EnvelopeState.Initializing; + Owner?.Channels.Remove(this); + Owner = owner; + Owner.Channels.Add(this); + Note = note; + _adsr.A = (byte)(env.A & 0x7); + _adsr.D = (byte)(env.D & 0x7); + _adsr.S = (byte)(env.S & 0xF); + _adsr.R = (byte)(env.R & 0x7); + _instPan = instPan; + } + + public override void Release() + { + if (State < EnvelopeState.Releasing) + { + if (_adsr.R == 0) + { + _velocity = 0; + Stop(); + } + else if (_velocity == 0) + { + Stop(); + } + else + { + _nextState = EnvelopeState.Releasing; + } + } + } + public override bool TickNote() + { + if (State >= EnvelopeState.Releasing) + { + return false; + } + if (Note.Duration <= 0) + { + return true; + } + + Note.Duration--; + if (Note.Duration == 0) + { + if (_velocity == 0) + { + Stop(); + } + else + { + State = EnvelopeState.Releasing; + } + return false; + } + return true; + } + + public override ChannelVolume GetVolume() + { + const float MAX = 0x20; + return new ChannelVolume + { + LeftVol = _panpot == GBPan.Right ? 0 : _velocity / MAX, + RightVol = _panpot == GBPan.Left ? 0 : _velocity / MAX + }; + } + public override void SetVolume(byte vol, sbyte pan) + { + int combinedPan = pan + _instPan; + if (combinedPan > 63) + { + combinedPan = 63; + } + else if (combinedPan < -64) + { + combinedPan = -64; + } + if (State < EnvelopeState.Releasing) + { + _panpot = combinedPan < -21 ? GBPan.Left : combinedPan > 20 ? GBPan.Right : GBPan.Center; + _peakVelocity = (byte)((Note.Velocity * vol) >> 10); + _sustainVelocity = (byte)(((_peakVelocity * _adsr.S) + 0xF) >> 4); // TODO + if (State == EnvelopeState.Playing) + { + _velocity = _sustainVelocity; + } + } + } + + protected void StepEnvelope() + { + void dec() + { + _processStep = 0; + if (_velocity - 1 <= _sustainVelocity) + { + _velocity = _sustainVelocity; + _nextState = EnvelopeState.Playing; + } + else if (_velocity != 0) + { + _velocity--; + } + } + void sus() + { + _processStep = 0; + } + void rel() + { + if (_adsr.R == 0) + { + _velocity = 0; + Stop(); + } + else + { + _processStep = 0; + if (_velocity - 1 <= 0) + { + _nextState = EnvelopeState.Dying; + _velocity = 0; + } + else + { + _velocity--; + } + } + } + + switch (State) + { + case EnvelopeState.Initializing: + { + _nextState = EnvelopeState.Rising; + _processStep = 0; + if ((_adsr.A | _adsr.D) == 0 || (_sustainVelocity == 0 && _peakVelocity == 0)) + { + State = EnvelopeState.Playing; + _velocity = _sustainVelocity; + return; + } + else if (_adsr.A == 0 && _adsr.S < 0xF) + { + State = EnvelopeState.Decaying; + int next = _peakVelocity - 1; + if (next < 0) + { + next = 0; + } + _velocity = (byte)next; + if (_velocity < _sustainVelocity) + { + _velocity = _sustainVelocity; + } + return; + } + else if (_adsr.A == 0) + { + State = EnvelopeState.Playing; + _velocity = _sustainVelocity; + return; + } + else + { + State = EnvelopeState.Rising; + _velocity = 1; + return; + } + } + case EnvelopeState.Rising: + { + if (++_processStep >= _adsr.A) + { + if (_nextState == EnvelopeState.Decaying) + { + State = EnvelopeState.Decaying; + dec(); return; + } + if (_nextState == EnvelopeState.Playing) + { + State = EnvelopeState.Playing; + sus(); return; + } + if (_nextState == EnvelopeState.Releasing) + { + State = EnvelopeState.Releasing; + rel(); return; + } + _processStep = 0; + if (++_velocity >= _peakVelocity) + { + if (_adsr.D == 0) + { + _nextState = EnvelopeState.Playing; + } + else if (_peakVelocity == _sustainVelocity) + { + _nextState = EnvelopeState.Playing; + _velocity = _peakVelocity; + } + else + { + _velocity = _peakVelocity; + _nextState = EnvelopeState.Decaying; + } + } + } + break; + } + case EnvelopeState.Decaying: + { + if (++_processStep >= _adsr.D) + { + if (_nextState == EnvelopeState.Playing) + { + State = EnvelopeState.Playing; + sus(); return; + } + if (_nextState == EnvelopeState.Releasing) + { + State = EnvelopeState.Releasing; + rel(); return; + } + dec(); + } + break; + } + case EnvelopeState.Playing: + { + if (++_processStep >= 1) + { + if (_nextState == EnvelopeState.Releasing) + { + State = EnvelopeState.Releasing; + rel(); return; + } + sus(); + } + break; + } + case EnvelopeState.Releasing: + { + if (++_processStep >= _adsr.R) + { + if (_nextState == EnvelopeState.Dying) + { + Stop(); + return; + } + rel(); + } + break; + } + } + } +} \ No newline at end of file diff --git a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KSquareChannel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KSquareChannel.cs new file mode 100644 index 00000000..4e655ffe --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KSquareChannel.cs @@ -0,0 +1,57 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal sealed class MP2KSquareChannel : MP2KPSGChannel +{ + private float[] _pat; + + public MP2KSquareChannel(MP2KMixer mixer) + : base(mixer) + { + _pat = null!; + } + public void Init(MP2KTrack owner, NoteInfo note, ADSR env, int instPan, SquarePattern pattern) + { + Init(owner, note, env, instPan); + _pat = pattern switch + { + SquarePattern.D12 => MP2KUtils.SquareD12, + SquarePattern.D25 => MP2KUtils.SquareD25, + SquarePattern.D50 => MP2KUtils.SquareD50, + _ => MP2KUtils.SquareD75, + }; + } + + public override void SetPitch(int pitch) + { + _frequency = 3_520 * MathF.Pow(2, ((Note.Note - 69) / 12f) + (pitch / 768f)); + } + + public override void Process(float[] buffer) + { + StepEnvelope(); + if (State == EnvelopeState.Dead) + { + return; + } + + ChannelVolume vol = GetVolume(); + float interStep = _frequency * _mixer.SampleRateReciprocal; + + int bufPos = 0; + int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = _pat[_pos]; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos = (_pos + posDelta) & 0x7; + } while (--samplesPerBuffer > 0); + } +} \ No newline at end of file diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KCommands.cs b/VG Music Studio - Core/GBA/MP2K/MP2KCommands.cs new file mode 100644 index 00000000..4959a5be --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KCommands.cs @@ -0,0 +1,193 @@ +using Kermalis.VGMusicStudio.Core.Util; +using System.Drawing; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal sealed class CallCommand : ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Call"; + public string Arguments => $"0x{Offset:X7}"; + + public int Offset { get; set; } +} +internal sealed class EndOfTieCommand : ICommand +{ + public Color Color => Color.SkyBlue; + public string Label => "End Of Tie"; + public string Arguments => Note == -1 ? "All Ties" : ConfigUtils.GetKeyName(Note); + + public int Note { get; set; } +} +internal sealed class FinishCommand : ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Finish"; + public string Arguments => Prev ? "Resume previous track" : "End track"; + + public bool Prev { get; set; } +} +internal sealed class JumpCommand : ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Jump"; + public string Arguments => $"0x{Offset:X7}"; + + public int Offset { get; set; } +} +internal sealed class LFODelayCommand : ICommand +{ + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Delay"; + public string Arguments => Delay.ToString(); + + public byte Delay { get; set; } +} +internal sealed class LFODepthCommand : ICommand +{ + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Depth"; + public string Arguments => Depth.ToString(); + + public byte Depth { get; set; } +} +internal sealed class LFOSpeedCommand : ICommand +{ + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Speed"; + public string Arguments => Speed.ToString(); + + public byte Speed { get; set; } +} +internal sealed class LFOTypeCommand : ICommand +{ + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Type"; + public string Arguments => Type.ToString(); + + public LFOType Type { get; set; } +} +internal sealed class LibraryCommand : ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Library Call"; + public string Arguments => $"{Command}, {Argument}"; + + public byte Command { get; set; } + public byte Argument { get; set; } +} +internal sealed class MemoryAccessCommand : ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Memory Access"; + public string Arguments => $"{Operator}, {Address}, {Data}"; + + public byte Operator { get; set; } + public byte Address { get; set; } + public byte Data { get; set; } +} +internal sealed class NoteCommand : ICommand +{ + public Color Color => Color.SkyBlue; + public string Label => "Note"; + public string Arguments => $"{ConfigUtils.GetKeyName(Note)} {Velocity} {Duration}"; + + public byte Note { get; set; } + public byte Velocity { get; set; } + public int Duration { get; set; } +} +internal sealed class PanpotCommand : ICommand +{ + public Color Color => Color.GreenYellow; + public string Label => "Panpot"; + public string Arguments => Panpot.ToString(); + + public sbyte Panpot { get; set; } +} +internal sealed class PitchBendCommand : ICommand +{ + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend"; + public string Arguments => Bend.ToString(); + + public sbyte Bend { get; set; } +} +internal sealed class PitchBendRangeCommand : ICommand +{ + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend Range"; + public string Arguments => Range.ToString(); + + public byte Range { get; set; } +} +internal sealed class PriorityCommand : ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Priority"; + public string Arguments => Priority.ToString(); + + public byte Priority { get; set; } +} +internal sealed class RepeatCommand : ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Repeat"; + public string Arguments => $"{Times}, 0x{Offset:X7}"; + + public byte Times { get; set; } + public int Offset { get; set; } +} +internal sealed class RestCommand : ICommand +{ + public Color Color => Color.PaleVioletRed; + public string Label => "Rest"; + public string Arguments => Rest.ToString(); + + public byte Rest { get; set; } +} +internal sealed class ReturnCommand : ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Return"; + public string Arguments => string.Empty; +} +internal sealed class TempoCommand : ICommand +{ + public Color Color => Color.DeepSkyBlue; + public string Label => "Tempo"; + public string Arguments => Tempo.ToString(); + + public ushort Tempo { get; set; } +} +internal sealed class TransposeCommand : ICommand +{ + public Color Color => Color.SkyBlue; + public string Label => "Transpose"; + public string Arguments => Transpose.ToString(); + + public sbyte Transpose { get; set; } +} +internal sealed class TuneCommand : ICommand +{ + public Color Color => Color.MediumPurple; + public string Label => "Fine Tune"; + public string Arguments => Tune.ToString(); + + public sbyte Tune { get; set; } +} +internal sealed class VoiceCommand : ICommand +{ + public Color Color => Color.DarkSalmon; + public string Label => "Voice"; + public string Arguments => Voice.ToString(); + + public byte Voice { get; set; } +} +internal sealed class VolumeCommand : ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Volume"; + public string Arguments => Volume.ToString(); + + public byte Volume { get; set; } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs b/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs new file mode 100644 index 00000000..97eb5406 --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs @@ -0,0 +1,243 @@ +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using YamlDotNet.RepresentationModel; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +public sealed class MP2KConfig : Config +{ + private const string CONFIG_FILE = "MP2K.yaml"; + + internal readonly byte[] ROM; + internal readonly string GameCode; + internal readonly byte Version; + + internal readonly string Name; + internal readonly int[] SongTableOffsets; + public readonly long[] SongTableSizes; + internal readonly int SampleRate; + internal readonly ReverbType ReverbType; + internal readonly byte Reverb; + internal readonly byte Volume; + internal readonly bool HasGoldenSunSynths; + internal readonly bool HasPokemonCompression; + + internal MP2KConfig(byte[] rom) + { + using (StreamReader fileStream = File.OpenText(ConfigUtils.CombineWithBaseDirectory(CONFIG_FILE))) + using (var ms = new MemoryStream(rom)) + { + string gcv = string.Empty; + try + { + ROM = rom; + var r = new EndianBinaryReader(ms, ascii: true); + r.Stream.Position = 0xAC; + GameCode = r.ReadString_Count(4); + r.Stream.Position = 0xBC; + Version = r.ReadByte(); + gcv = $"{GameCode}_{Version:X2}"; + var yaml = new YamlStream(); + yaml.Load(fileStream); + + var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; + YamlMappingNode game; + try + { + game = (YamlMappingNode)mapping.Children.GetValue(gcv); + } + catch (BetterKeyNotFoundException) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KMissingGameCode, gcv))); + } + + YamlNode? nameNode = null, + songTableOffsetsNode = null, + songTableSizesNode = null, + sampleRateNode = null, + reverbTypeNode = null, + reverbNode = null, + volumeNode = null, + hasGoldenSunSynthsNode = null, + hasPokemonCompression = null; + void Load(YamlMappingNode gameToLoad) + { + if (gameToLoad.Children.TryGetValue("Copy", out YamlNode? node)) + { + YamlMappingNode copyGame; + try + { + copyGame = (YamlMappingNode)mapping.Children.GetValue(node); + } + catch (BetterKeyNotFoundException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KCopyInvalidGameCode, ex.Key))); + } + Load(copyGame); + } + if (gameToLoad.Children.TryGetValue(nameof(Name), out node)) + { + nameNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SongTableOffsets), out node)) + { + songTableOffsetsNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SongTableSizes), out node)) + { + songTableSizesNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SampleRate), out node)) + { + sampleRateNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(ReverbType), out node)) + { + reverbTypeNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(Reverb), out node)) + { + reverbNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(Volume), out node)) + { + volumeNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(HasGoldenSunSynths), out node)) + { + hasGoldenSunSynthsNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(HasPokemonCompression), out node)) + { + hasPokemonCompression = node; + } + if (gameToLoad.Children.TryGetValue(nameof(Playlists), out node)) + { + var playlists = (YamlMappingNode)node; + foreach (KeyValuePair kvp in playlists) + { + string name = kvp.Key.ToString(); + var songs = new List(); + foreach (KeyValuePair song in (YamlMappingNode)kvp.Value) + { + int songIndex = (int)ConfigUtils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Playlists)), song.Key.ToString(), 0, int.MaxValue); + if (songs.Any(s => s.Index == songIndex)) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongRepeated, name, songIndex))); + } + songs.Add(new Song(songIndex, song.Value.ToString())); + } + Playlists.Add(new Playlist(name, songs)); + } + } + } + + Load(game); + + if (nameNode is null) + { + throw new BetterKeyNotFoundException(nameof(Name), null); + } + Name = nameNode.ToString(); + + if (songTableOffsetsNode is null) + { + throw new BetterKeyNotFoundException(nameof(SongTableOffsets), null); + } + string[] songTables = songTableOffsetsNode.ToString().Split(' ', options: StringSplitOptions.RemoveEmptyEntries); + int numSongTables = songTables.Length; + if (numSongTables == 0) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigKeyNoEntries, nameof(SongTableOffsets)))); + } + + if (songTableSizesNode is null) + { + throw new BetterKeyNotFoundException(nameof(SongTableSizes), null); + } + string[] sizes = songTableSizesNode.ToString().Split(' ', options: StringSplitOptions.RemoveEmptyEntries); + if (sizes.Length != numSongTables) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongTableCounts, nameof(SongTableSizes), nameof(SongTableOffsets)))); + } + SongTableOffsets = new int[numSongTables]; + SongTableSizes = new long[numSongTables]; + int maxOffset = rom.Length - 1; + for (int i = 0; i < numSongTables; i++) + { + SongTableSizes[i] = ConfigUtils.ParseValue(nameof(SongTableSizes), sizes[i], 1, maxOffset); + SongTableOffsets[i] = (int)ConfigUtils.ParseValue(nameof(SongTableOffsets), songTables[i], 0, maxOffset); + } + + if (sampleRateNode is null) + { + throw new BetterKeyNotFoundException(nameof(SampleRate), null); + } + SampleRate = (int)ConfigUtils.ParseValue(nameof(SampleRate), sampleRateNode.ToString(), 0, MP2KUtils.FrequencyTable.Length - 1); + + if (reverbTypeNode is null) + { + throw new BetterKeyNotFoundException(nameof(ReverbType), null); + } + ReverbType = ConfigUtils.ParseEnum(nameof(ReverbType), reverbTypeNode.ToString()); + + if (reverbNode is null) + { + throw new BetterKeyNotFoundException(nameof(Reverb), null); + } + Reverb = (byte)ConfigUtils.ParseValue(nameof(Reverb), reverbNode.ToString(), byte.MinValue, byte.MaxValue); + + if (volumeNode is null) + { + throw new BetterKeyNotFoundException(nameof(Volume), null); + } + Volume = (byte)ConfigUtils.ParseValue(nameof(Volume), volumeNode.ToString(), 0, 15); + + if (hasGoldenSunSynthsNode is null) + { + throw new BetterKeyNotFoundException(nameof(HasGoldenSunSynths), null); + } + HasGoldenSunSynths = ConfigUtils.ParseBoolean(nameof(HasGoldenSunSynths), hasGoldenSunSynthsNode.ToString()); + + if (hasPokemonCompression is null) + { + throw new BetterKeyNotFoundException(nameof(HasPokemonCompression), null); + } + HasPokemonCompression = ConfigUtils.ParseBoolean(nameof(HasPokemonCompression), hasPokemonCompression.ToString()); + + // The complete playlist + ConfigUtils.TryCreateMasterPlaylist(Playlists); + } + catch (BetterKeyNotFoundException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigKeyMissing, ex.Key))); + } + catch (InvalidValueException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + ex.Message)); + } + catch (YamlDotNet.Core.YamlException ex) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + ex.Message)); + } + } + } + + public override string GetGameName() + { + return Name; + } + public override string GetSongName(int index) + { + if (TryGetFirstSong(index, out Song s)) + { + return s.Name; + } + return index.ToString(); + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs b/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs new file mode 100644 index 00000000..43c40baa --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs @@ -0,0 +1,33 @@ +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +public sealed class MP2KEngine : Engine +{ + public static MP2KEngine? MP2KInstance { get; private set; } + + public override MP2KConfig Config { get; } + public override MP2KMixer Mixer { get; } + public override MP2KPlayer Player { get; } + + public MP2KEngine(byte[] rom) + { + if (rom.Length > GBAUtils.CARTRIDGE_CAPACITY) + { + throw new InvalidDataException($"The ROM is too large. Maximum size is 0x{GBAUtils.CARTRIDGE_CAPACITY:X7} bytes."); + } + + Config = new MP2KConfig(rom); + Mixer = new MP2KMixer(Config); + Player = new MP2KPlayer(Config, Mixer); + + MP2KInstance = this; + Instance = this; + } + + public override void Dispose() + { + base.Dispose(); + MP2KInstance = null; + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KEnums.cs b/VG Music Studio - Core/GBA/MP2K/MP2KEnums.cs new file mode 100644 index 00000000..86029a67 --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KEnums.cs @@ -0,0 +1,75 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal enum EnvelopeState : byte +{ + Initializing, + Rising, + Decaying, + Playing, + Releasing, + Dying, + Dead, +} +internal enum ReverbType : byte +{ + None, + Normal, + Camelot1, + Camelot2, + MGAT, +} + +internal enum GoldenSunPSGType : byte +{ + Square, + Saw, + Triangle, +} +internal enum LFOType : byte +{ + Pitch, + Volume, + Panpot, +} +internal enum SquarePattern : byte +{ + D12, + D25, + D50, + D75, +} +internal enum NoisePattern : byte +{ + Fine, + Rough, +} +internal enum VoiceType : byte +{ + PCM8, + Square1, + Square2, + PCM4, + Noise, + Invalid5, + Invalid6, + Invalid7, +} +[Flags] +internal enum VoiceFlags : byte +{ + // These are flags that apply to the types + /// PCM8 + Fixed = 0x08, + /// Square1, Square2, PCM4, Noise + OffWithNoise = 0x08, + /// PCM8 + Reversed = 0x10, + /// PCM8 (Only in Pokémon main series games) + Compressed = 0x20, + + // These are flags that cancel out every other bit after them if set so they should only be checked with equality + KeySplit = 0x40, + Drum = 0x80, +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KExceptions.cs b/VG Music Studio - Core/GBA/MP2K/MP2KExceptions.cs new file mode 100644 index 00000000..f43e2dae --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KExceptions.cs @@ -0,0 +1,41 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +public sealed class MP2KInvalidCMDException : Exception +{ + public byte TrackIndex { get; } + public int CmdOffset { get; } + public byte Cmd { get; } + + internal MP2KInvalidCMDException(byte trackIndex, int cmdOffset, byte cmd) + { + TrackIndex = trackIndex; + CmdOffset = cmdOffset; + Cmd = cmd; + } +} + +public sealed class MP2KInvalidRunningStatusCMDException : Exception +{ + public byte TrackIndex { get; } + public int CmdOffset { get; } + public byte RunCmd { get; } + + internal MP2KInvalidRunningStatusCMDException(byte trackIndex, int cmdOffset, byte runCmd) + { + TrackIndex = trackIndex; + CmdOffset = cmdOffset; + RunCmd = runCmd; + } +} + +public sealed class MP2KTooManyNestedCallsException : Exception +{ + public byte TrackIndex { get; } + + internal MP2KTooManyNestedCallsException(byte trackIndex) + { + TrackIndex = trackIndex; + } +} \ No newline at end of file diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong.cs b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong.cs new file mode 100644 index 00000000..b336371b --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal sealed partial class MP2KLoadedSong : ILoadedSong +{ + public List[] Events { get; } + public long MaxTicks { get; private set; } + public int LongestTrack; + + private readonly MP2KPlayer _player; + private readonly int _voiceTableOffset; + public readonly MP2KTrack[] Tracks; + + public MP2KLoadedSong(MP2KPlayer player, int index) + { + _player = player; + + MP2KConfig cfg = player.Config; + var entry = SongEntry.Get(cfg.ROM, cfg.SongTableOffsets[0], index); + int headerOffset = entry.HeaderOffset - GBAUtils.CARTRIDGE_OFFSET; + + var header = SongHeader.Get(cfg.ROM, headerOffset, out int tracksOffset); + _voiceTableOffset = header.VoiceTableOffset - GBAUtils.CARTRIDGE_OFFSET; + + Tracks = new MP2KTrack[header.NumTracks]; + Events = new List[header.NumTracks]; + for (byte trackIndex = 0; trackIndex < header.NumTracks; trackIndex++) + { + int trackStart = SongHeader.GetTrackOffset(cfg.ROM, tracksOffset, trackIndex) - GBAUtils.CARTRIDGE_OFFSET; + Tracks[trackIndex] = new MP2KTrack(trackIndex, trackStart); + + AddTrackEvents(trackIndex, trackStart); + } + } + + public void CheckVoiceTypeCache(ref int? old, string?[] voiceTypeCache) + { + if (old != _voiceTableOffset) + { + old = _voiceTableOffset; + Array.Clear(voiceTypeCache); + } + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Events.cs b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Events.cs new file mode 100644 index 00000000..ab62db76 --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Events.cs @@ -0,0 +1,546 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal sealed partial class MP2KLoadedSong +{ + private void AddEvent(byte trackIndex, long cmdOffset, ICommand command) + { + Events[trackIndex].Add(new SongEvent(cmdOffset, command)); + } + private bool EventExists(byte trackIndex, long cmdOffset) + { + return Events[trackIndex].Exists(e => e.Offset == cmdOffset); + } + + private void EmulateNote(byte trackIndex, long cmdOffset, byte key, byte velocity, byte addedDuration, ref byte runCmd, ref byte prevKey, ref byte prevVelocity) + { + prevKey = key; + prevVelocity = velocity; + if (EventExists(trackIndex, cmdOffset)) + { + return; + } + + AddEvent(trackIndex, cmdOffset, new NoteCommand + { + Note = key, + Velocity = velocity, + Duration = runCmd == 0xCF ? -1 : (MP2KUtils.RestTable[runCmd - 0xCF] + addedDuration), + }); + } + + private void AddTrackEvents(byte trackIndex, long trackStart) + { + Events[trackIndex] = new List(); + byte runCmd = 0; + byte prevKey = 0; + byte prevVelocity = 0x7F; + int callStackDepth = 0; + AddEvents(trackIndex, trackStart, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); + } + private void AddEvents(byte trackIndex, long startOffset, ref byte runCmd, ref byte prevKey, ref byte prevVelocity, ref int callStackDepth) + { + using (var ms = new MemoryStream(_player.Config.ROM)) + { + var r = new EndianBinaryReader(ms, ascii: true); + r.Stream.Position = startOffset; + + Span peek = stackalloc byte[3]; + bool cont = true; + while (cont) + { + long offset = r.Stream.Position; + + byte cmd = r.ReadByte(); + if (cmd >= 0xBD) // Commands that work within running status + { + runCmd = cmd; + } + + #region TIE & Notes + + if (runCmd >= 0xCF && cmd <= 0x7F) // Within running status + { + byte velocity, addedDuration; + r.PeekBytes(peek.Slice(0, 2)); + if (peek[0] > 0x7F) + { + velocity = prevVelocity; + addedDuration = 0; + } + else if (peek[1] > 3) + { + velocity = r.ReadByte(); + addedDuration = 0; + } + else + { + velocity = r.ReadByte(); + addedDuration = r.ReadByte(); + } + EmulateNote(trackIndex, offset, cmd, velocity, addedDuration, ref runCmd, ref prevKey, ref prevVelocity); + } + else if (cmd >= 0xCF) + { + byte key, velocity, addedDuration; + r.PeekBytes(peek); + if (peek[0] > 0x7F) + { + key = prevKey; + velocity = prevVelocity; + addedDuration = 0; + } + else if (peek[1] > 0x7F) + { + key = r.ReadByte(); + velocity = prevVelocity; + addedDuration = 0; + } + // TIE (0xCF) cannot have an added duration so it needs to stop here + else if (cmd == 0xCF || peek[2] > 3) + { + key = r.ReadByte(); + velocity = r.ReadByte(); + addedDuration = 0; + } + else + { + key = r.ReadByte(); + velocity = r.ReadByte(); + addedDuration = r.ReadByte(); + } + EmulateNote(trackIndex, offset, key, velocity, addedDuration, ref runCmd, ref prevKey, ref prevVelocity); + } + + #endregion + + #region Rests + + else if (cmd is >= 0x80 and <= 0xB0) + { + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new RestCommand { Rest = MP2KUtils.RestTable[cmd - 0x80] }); + } + } + + #endregion + + #region Commands + + else if (runCmd < 0xCF && cmd <= 0x7F) + { + switch (runCmd) + { + case 0xBD: + { + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new VoiceCommand { Voice = cmd }); + } + break; + } + case 0xBE: + { + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new VolumeCommand { Volume = cmd }); + } + break; + } + case 0xBF: + { + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new PanpotCommand { Panpot = (sbyte)(cmd - 0x40) }); + } + break; + } + case 0xC0: + { + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new PitchBendCommand { Bend = (sbyte)(cmd - 0x40) }); + } + break; + } + case 0xC1: + { + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new PitchBendRangeCommand { Range = cmd }); + } + break; + } + case 0xC2: + { + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFOSpeedCommand { Speed = cmd }); + } + break; + } + case 0xC3: + { + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFODelayCommand { Delay = cmd }); + } + break; + } + case 0xC4: + { + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFODepthCommand { Depth = cmd }); + } + break; + } + case 0xC5: + { + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFOTypeCommand { Type = (LFOType)cmd }); + } + break; + } + case 0xC8: + { + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new TuneCommand { Tune = (sbyte)(cmd - 0x40) }); + } + break; + } + case 0xCD: + { + byte arg = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LibraryCommand { Command = cmd, Argument = arg }); + } + break; + } + case 0xCE: + { + prevKey = cmd; + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new EndOfTieCommand { Note = cmd }); + } + break; + } + default: throw new MP2KInvalidRunningStatusCMDException(trackIndex, (int)offset, runCmd); + } + } + else if (cmd is > 0xB0 and < 0xCF) + { + switch (cmd) + { + case 0xB1: + case 0xB6: + { + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new FinishCommand { Prev = cmd == 0xB6 }); + } + cont = false; + break; + } + case 0xB2: + { + int jumpOffset = r.ReadInt32() - GBAUtils.CARTRIDGE_OFFSET; + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new JumpCommand { Offset = jumpOffset }); + if (!EventExists(trackIndex, jumpOffset)) + { + AddEvents(trackIndex, jumpOffset, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); + } + } + cont = false; + break; + } + case 0xB3: + { + int callOffset = r.ReadInt32() - GBAUtils.CARTRIDGE_OFFSET; + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new CallCommand { Offset = callOffset }); + } + if (callStackDepth < 3) + { + long backup = r.Stream.Position; + callStackDepth++; + AddEvents(trackIndex, callOffset, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); + r.Stream.Position = backup; + } + else + { + throw new MP2KTooManyNestedCallsException(trackIndex); + } + break; + } + case 0xB4: + { + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new ReturnCommand()); + } + if (callStackDepth != 0) + { + cont = false; + callStackDepth--; + } + break; + } + /*case 0xB5: // TODO: Logic so this isn't an infinite loop + { + byte times = config.Reader.ReadByte(); + int repeatOffset = config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; + if (!EventExists(offset, trackEvents)) + { + AddEvent(new RepeatCommand { Times = times, Offset = repeatOffset }); + } + break; + }*/ + case 0xB9: + { + byte op = r.ReadByte(); + byte address = r.ReadByte(); + byte data = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new MemoryAccessCommand { Operator = op, Address = address, Data = data }); + } + break; + } + case 0xBA: + { + byte priority = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new PriorityCommand { Priority = priority }); + } + break; + } + case 0xBB: + { + byte tempoArg = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new TempoCommand { Tempo = (ushort)(tempoArg * 2) }); + } + break; + } + case 0xBC: + { + sbyte transpose = r.ReadSByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new TransposeCommand { Transpose = transpose }); + } + break; + } + // Commands that work within running status: + case 0xBD: + { + byte voice = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new VoiceCommand { Voice = voice }); + } + break; + } + case 0xBE: + { + byte volume = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new VolumeCommand { Volume = volume }); + } + break; + } + case 0xBF: + { + byte panArg = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); + } + break; + } + case 0xC0: + { + byte bendArg = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new PitchBendCommand { Bend = (sbyte)(bendArg - 0x40) }); + } + break; + } + case 0xC1: + { + byte range = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new PitchBendRangeCommand { Range = range }); + } + break; + } + case 0xC2: + { + byte speed = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFOSpeedCommand { Speed = speed }); + } + break; + } + case 0xC3: + { + byte delay = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFODelayCommand { Delay = delay }); + } + break; + } + case 0xC4: + { + byte depth = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFODepthCommand { Depth = depth }); + } + break; + } + case 0xC5: + { + byte type = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFOTypeCommand { Type = (LFOType)type }); + } + break; + } + case 0xC8: + { + byte tuneArg = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new TuneCommand { Tune = (sbyte)(tuneArg - 0x40) }); + } + break; + } + case 0xCD: + { + byte command = r.ReadByte(); + byte arg = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LibraryCommand { Command = command, Argument = arg }); + } + break; + } + case 0xCE: + { + int key = r.PeekByte() <= 0x7F ? (prevKey = r.ReadByte()) : -1; + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new EndOfTieCommand { Note = key }); + } + break; + } + default: throw new MP2KInvalidCMDException(trackIndex, (int)offset, cmd); + } + } + + #endregion + } + } + } + + public void SetTicks() + { + MaxTicks = 0; + bool u = false; + for (int trackIndex = 0; trackIndex < Events.Length; trackIndex++) + { + List evs = Events[trackIndex]; + evs.Sort((e1, e2) => e1.Offset.CompareTo(e2.Offset)); + + MP2KTrack track = Tracks[trackIndex]; + track.Init(); + + _player.ElapsedTicks = 0; + while (true) + { + SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); + if (track.CallStackDepth == 0 && e.Ticks.Count > 0) + { + break; + } + + e.Ticks.Add(_player.ElapsedTicks); + ExecuteNext(track, ref u); + if (track.Stopped) + { + break; + } + + _player.ElapsedTicks += track.Rest; + track.Rest = 0; + } + if (_player.ElapsedTicks > MaxTicks) + { + LongestTrack = trackIndex; + MaxTicks = _player.ElapsedTicks; + } + track.StopAllChannels(); + } + } + internal void SetCurTick(long ticks) + { + bool u = false; + while (true) + { + if (_player.ElapsedTicks == ticks) + { + goto finish; + } + while (_player.TempoStack >= 150) + { + _player.TempoStack -= 150; + for (int trackIndex = 0; trackIndex < Tracks.Length; trackIndex++) + { + MP2KTrack track = Tracks[trackIndex]; + if (!track.Stopped) + { + track.Tick(); + while (track.Rest == 0 && !track.Stopped) + { + ExecuteNext(track, ref u); + } + } + } + _player.ElapsedTicks++; + if (_player.ElapsedTicks == ticks) + { + goto finish; + } + } + _player.TempoStack += _player.Tempo; + } + finish: + for (int i = 0; i < Tracks.Length; i++) + { + Tracks[i].StopAllChannels(); + } + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs new file mode 100644 index 00000000..1d6c4f0f --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs @@ -0,0 +1,269 @@ +using Kermalis.MIDI; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +public sealed class MIDISaveArgs +{ + public bool SaveCommandsBeforeTranspose; // TODO: I forgor why I would want this + public bool ReverseVolume; + public (int AbsoluteTick, (byte Numerator, byte Denominator))[] TimeSignatures; + + public MIDISaveArgs(bool saveCmdsBeforeTranspose, bool reverseVol, (int, (byte, byte))[] timeSignatures) + { + SaveCommandsBeforeTranspose = saveCmdsBeforeTranspose; + ReverseVolume = reverseVol; + TimeSignatures = timeSignatures; + } +} + +internal sealed partial class MP2KLoadedSong +{ + // TODO: Don't use events, read from rom + public void SaveAsMIDI(string fileName, MIDISaveArgs args) + { + // TODO: FINE vs PREV + // TODO: https://github.com/Kermalis/VGMusicStudio/issues/36 + // TODO: Nested calls + // TODO: REPT + + // These TODO shouldn't affect matching because they are unsupported anyway: + // TODO: Drums that use more than 127 notes need to use bank select + // TODO: Use bank select with voices above 127 + + byte baseVolume = 0x7F; + if (args.ReverseVolume) + { + baseVolume = Events.SelectMany(e => e).Where(e => e.Command is VolumeCommand).Select(e => ((VolumeCommand)e.Command).Volume).Max(); + Debug.WriteLine($"Reversing volume back from {baseVolume}."); + } + + var midi = new MIDIFile(MIDIFormat.Format1, TimeDivisionValue.CreatePPQN(24), Events.Length + 1); + var metaTrack = new MIDITrackChunk(); + midi.AddChunk(metaTrack); + + foreach ((int AbsoluteTick, (byte Numerator, byte Denominator)) e in args.TimeSignatures) + { + metaTrack.InsertMessage(e.AbsoluteTick, MetaMessage.CreateTimeSignatureMessage(e.Item2.Numerator, e.Item2.Denominator)); + } + + for (byte trackIndex = 0; trackIndex < Events.Length; trackIndex++) + { + var track = new MIDITrackChunk(); + midi.AddChunk(track); + + bool foundTranspose = false; + int endOfPattern = 0; + long startOfPatternTicks = 0; + long endOfPatternTicks = 0; + sbyte transpose = 0; + int? endTicks = null; + var playing = new List(); + List trackEvents = Events[trackIndex]; + for (int i = 0; i < trackEvents.Count; i++) + { + SongEvent e = trackEvents[i]; + int ticks = (int)(e.Ticks[0] + (endOfPatternTicks - startOfPatternTicks)); + + // Preliminary check for saving events before transpose + switch (e.Command) + { + case TransposeCommand c: + { + foundTranspose = true; + break; + } + default: // If we should not save before transpose then skip this event + { + if (!args.SaveCommandsBeforeTranspose && !foundTranspose) + { + continue; + } + break; + } + } + + // Now do the event magic... + switch (e.Command) + { + case CallCommand c: + { + int callCmd = trackEvents.FindIndex(ev => ev.Offset == c.Offset); + endOfPattern = i; + endOfPatternTicks = e.Ticks[0]; + i = callCmd - 1; // -1 for incoming ++ + startOfPatternTicks = trackEvents[callCmd].Ticks[0]; + break; + } + case EndOfTieCommand c: + { + NoteCommand? nc = c.Note == -1 ? playing.LastOrDefault() : playing.LastOrDefault(no => no.Note == c.Note); + if (nc is not null) + { + int key = nc.Note + transpose; + if (key < 0) + { + key = 0; + } + else if (key > 0x7F) + { + key = 0x7F; + } + track.InsertMessage(ticks, new NoteOnMessage(trackIndex, (MIDINote)key, 0)); + //track.InsertMessage(ticks, new NoteOffMessage(trackIndex, (MIDINote)key, 0)); + playing.Remove(nc); + } + break; + } + case FinishCommand _: + { + endTicks = ticks; + goto endOfTrack; + } + case JumpCommand c: + { + if (trackIndex == 0) + { + int jumpCmd = trackEvents.FindIndex(ev => ev.Offset == c.Offset); + metaTrack.InsertMessage((int)trackEvents[jumpCmd].Ticks[0], MetaMessage.CreateTextMessage(MetaMessageType.Marker, "[")); + metaTrack.InsertMessage(ticks, MetaMessage.CreateTextMessage(MetaMessageType.Marker, "]")); + } + break; + } + case LFODelayCommand c: + { + track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)26, c.Delay)); + break; + } + case LFODepthCommand c: + { + track.InsertMessage(ticks, new ControllerMessage(trackIndex, ControllerType.ModulationWheel, c.Depth)); + break; + } + case LFOSpeedCommand c: + { + track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)21, c.Speed)); + break; + } + case LFOTypeCommand c: + { + track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)22, (byte)c.Type)); + break; + } + case LibraryCommand c: + { + track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)30, c.Command)); + track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)29, c.Argument)); + break; + } + case MemoryAccessCommand c: + { + track.InsertMessage(ticks, new ControllerMessage(trackIndex, ControllerType.EffectControl2, c.Operator)); + track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)14, c.Address)); + track.InsertMessage(ticks, new ControllerMessage(trackIndex, ControllerType.EffectControl1, c.Data)); + break; + } + case NoteCommand c: + { + int note = c.Note + transpose; + if (note < 0) + { + note = 0; + } + else if (note > 0x7F) + { + note = 0x7F; + } + track.InsertMessage(ticks, new NoteOnMessage(trackIndex, (MIDINote)note, c.Velocity)); + if (c.Duration != -1) + { + track.InsertMessage(ticks + c.Duration, new NoteOnMessage(trackIndex, (MIDINote)note, 0)); + //track.InsertMessage(ticks + c.Duration, new NoteOffMessage(trackIndex, (MIDINote)note, 0)); + } + else + { + playing.Add(c); + } + break; + } + case PanpotCommand c: + { + track.InsertMessage(ticks, new ControllerMessage(trackIndex, ControllerType.Pan, (byte)(c.Panpot + 0x40))); + break; + } + case PitchBendCommand c: + { + track.InsertMessage(ticks, new PitchBendMessage(trackIndex, 0, (byte)(c.Bend + 0x40))); + break; + } + case PitchBendRangeCommand c: + { + track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)20, c.Range)); + break; + } + case PriorityCommand c: + { + track.InsertMessage(ticks, new ControllerMessage(trackIndex, ControllerType.ChannelVolumeLSB, c.Priority)); + break; + } + case ReturnCommand _: + { + if (endOfPattern != 0) + { + i = endOfPattern; + endOfPattern = 0; + startOfPatternTicks = 0; + endOfPatternTicks = 0; + } + break; + } + case TempoCommand c: + { + metaTrack.InsertMessage(ticks, MetaMessage.CreateTempoMessage(c.Tempo)); + break; + } + case TransposeCommand c: + { + transpose = c.Transpose; + break; + } + case TuneCommand c: + { + track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)24, (byte)(c.Tune + 0x40))); + break; + } + case VoiceCommand c: + { + track.InsertMessage(ticks, new ProgramChangeMessage(trackIndex, (MIDIProgram)c.Voice)); + break; + } + case VolumeCommand c: + { + double d = baseVolume / (double)0x7F; + int volume = (int)(c.Volume / d); + // If there are rounding errors, fix them (happens if baseVolume is not 127 and baseVolume is not vol.Volume) + if (volume * baseVolume / 0x7F == c.Volume - 1) + { + volume++; + } + track.InsertMessage(ticks, new ControllerMessage(trackIndex, ControllerType.ChannelVolume, (byte)volume)); + break; + } + } + } + endOfTrack: + track.InsertMessage(endTicks ?? track.NumTicks, new MetaMessage(MetaMessageType.EndOfTrack, Array.Empty())); + } + + metaTrack.InsertMessage(metaTrack.NumTicks, new MetaMessage(MetaMessageType.EndOfTrack, Array.Empty())); + + using (FileStream fs = File.Create(fileName)) + { + midi.Save(fs); + } + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Runtime.cs b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Runtime.cs new file mode 100644 index 00000000..b3c038c5 --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Runtime.cs @@ -0,0 +1,458 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal sealed partial class MP2KLoadedSong +{ + private void TryPlayNote(MP2KTrack track, byte note, byte velocity, byte addedDuration) + { + int n = note + track.Transpose; + if (n < 0) + { + n = 0; + } + else if (n > 0x7F) + { + n = 0x7F; + } + note = (byte)n; + track.PrevNote = note; + track.PrevVelocity = velocity; + // Tracks do not play unless they have had a voice change event + if (track.Ready) + { + PlayNote(_player.Config.ROM, track, note, velocity, addedDuration); + } + } + private void PlayNote(byte[] rom, MP2KTrack track, byte note, byte velocity, byte addedDuration) + { + bool fromDrum = false; + int offset = _voiceTableOffset + (track.Voice * 12); + while (true) + { + var v = new VoiceEntry(rom.AsSpan(offset)); + if (v.Type == (int)VoiceFlags.KeySplit) + { + fromDrum = false; // In case there is a multi within a drum + byte inst = rom[v.Int8 - GBAUtils.CARTRIDGE_OFFSET + note]; + offset = v.Int4 - GBAUtils.CARTRIDGE_OFFSET + (inst * 12); + } + else if (v.Type == (int)VoiceFlags.Drum) + { + fromDrum = true; + offset = v.Int4 - GBAUtils.CARTRIDGE_OFFSET + (note * 12); + } + else + { + var ni = new NoteInfo + { + Duration = track.RunCmd == 0xCF ? -1 : (MP2KUtils.RestTable[track.RunCmd - 0xCF] + addedDuration), + Velocity = velocity, + OriginalNote = note, + Note = fromDrum ? v.RootNote : note, + }; + var type = (VoiceType)(v.Type & 0x7); + int instPan = v.Pan; + instPan = (instPan & 0x80) != 0 ? instPan - 0xC0 : 0; + switch (type) + { + case VoiceType.PCM8: + { + bool bFixed = (v.Type & (int)VoiceFlags.Fixed) != 0; + bool bCompressed = _player.Config.HasPokemonCompression && ((v.Type & (int)VoiceFlags.Compressed) != 0); + _player.MMixer.AllocPCM8Channel(track, v.ADSR, ni, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + bFixed, bCompressed, v.Int4 - GBAUtils.CARTRIDGE_OFFSET); + return; + } + case VoiceType.Square1: + case VoiceType.Square2: + { + _player.MMixer.AllocPSGChannel(track, v.ADSR, ni, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + type, (SquarePattern)v.Int4); + return; + } + case VoiceType.PCM4: + { + _player.MMixer.AllocPSGChannel(track, v.ADSR, ni, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + type, v.Int4 - GBAUtils.CARTRIDGE_OFFSET); + return; + } + case VoiceType.Noise: + { + _player.MMixer.AllocPSGChannel(track, v.ADSR, ni, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + type, (NoisePattern)v.Int4); + return; + } + } + return; // Prevent infinite loop with invalid instruments + } + } + } + public void ExecuteNext(MP2KTrack track, ref bool update) + { + byte[] rom = _player.Config.ROM; + byte cmd = rom[track.DataOffset++]; + if (cmd >= 0xBD) // Commands that work within running status + { + track.RunCmd = cmd; + } + + if (track.RunCmd >= 0xCF && cmd <= 0x7F) // Within running status + { + byte peek0 = rom[track.DataOffset]; + byte peek1 = rom[track.DataOffset + 1]; + byte velocity, addedDuration; + if (peek0 > 0x7F) + { + velocity = track.PrevVelocity; + addedDuration = 0; + } + else if (peek1 > 3) + { + track.DataOffset++; + velocity = peek0; + addedDuration = 0; + } + else + { + track.DataOffset += 2; + velocity = peek0; + addedDuration = peek1; + } + TryPlayNote(track, cmd, velocity, addedDuration); + } + else if (cmd >= 0xCF) + { + byte peek0 = rom[track.DataOffset]; + byte peek1 = rom[track.DataOffset + 1]; + byte peek2 = rom[track.DataOffset + 2]; + byte key, velocity, addedDuration; + if (peek0 > 0x7F) + { + key = track.PrevNote; + velocity = track.PrevVelocity; + addedDuration = 0; + } + else if (peek1 > 0x7F) + { + track.DataOffset++; + key = peek0; + velocity = track.PrevVelocity; + addedDuration = 0; + } + else if (cmd == 0xCF || peek2 > 3) + { + track.DataOffset += 2; + key = peek0; + velocity = peek1; + addedDuration = 0; + } + else + { + track.DataOffset += 3; + key = peek0; + velocity = peek1; + addedDuration = peek2; + } + TryPlayNote(track, key, velocity, addedDuration); + } + else if (cmd >= 0x80 && cmd <= 0xB0) + { + track.Rest = MP2KUtils.RestTable[cmd - 0x80]; + } + else if (track.RunCmd < 0xCF && cmd <= 0x7F) + { + switch (track.RunCmd) + { + case 0xBD: + { + track.Voice = cmd; + //track.Ready = true; // This is unnecessary because if we're in running status of a voice command, then Ready was already set + break; + } + case 0xBE: + { + track.Volume = cmd; + update = true; + break; + } + case 0xBF: + { + track.Panpot = (sbyte)(cmd - 0x40); + update = true; + break; + } + case 0xC0: + { + track.PitchBend = (sbyte)(cmd - 0x40); + update = true; + break; + } + case 0xC1: + { + track.PitchBendRange = cmd; + update = true; + break; + } + case 0xC2: + { + track.LFOSpeed = cmd; + track.LFOPhase = 0; + track.LFODelayCount = 0; + update = true; + break; + } + case 0xC3: + { + track.LFODelay = cmd; + track.LFOPhase = 0; + track.LFODelayCount = 0; + update = true; + break; + } + case 0xC4: + { + track.LFODepth = cmd; + update = true; + break; + } + case 0xC5: + { + track.LFOType = (LFOType)cmd; + update = true; + break; + } + case 0xC8: + { + track.Tune = (sbyte)(cmd - 0x40); + update = true; + break; + } + case 0xCD: + { + track.DataOffset++; + break; + } + case 0xCE: + { + track.PrevNote = cmd; + int k = cmd + track.Transpose; + if (k < 0) + { + k = 0; + } + else if (k > 0x7F) + { + k = 0x7F; + } + track.ReleaseChannels(k); + break; + } + default: throw new MP2KInvalidRunningStatusCMDException(track.Index, track.DataOffset - 1, track.RunCmd); + } + } + else if (cmd > 0xB0 && cmd < 0xCF) + { + switch (cmd) + { + case 0xB1: + case 0xB6: + { + track.Stopped = true; + //track.ReleaseAllTieingChannels(); // Necessary? + break; + } + case 0xB2: + { + track.DataOffset = (rom[track.DataOffset++] | (rom[track.DataOffset++] << 8) | (rom[track.DataOffset++] << 16) | (rom[track.DataOffset++] << 24)) - GBAUtils.CARTRIDGE_OFFSET; + break; + } + case 0xB3: + { + if (track.CallStackDepth >= 3) + { + throw new MP2KTooManyNestedCallsException(track.Index); + } + + int callOffset = (rom[track.DataOffset++] | (rom[track.DataOffset++] << 8) | (rom[track.DataOffset++] << 16) | (rom[track.DataOffset++] << 24)) - GBAUtils.CARTRIDGE_OFFSET; + track.CallStack[track.CallStackDepth] = track.DataOffset; + track.CallStackDepth++; + track.DataOffset = callOffset; + break; + } + case 0xB4: + { + if (track.CallStackDepth != 0) + { + track.CallStackDepth--; + track.DataOffset = track.CallStack[track.CallStackDepth]; + } + break; + } + /*case 0xB5: // TODO: Logic so this isn't an infinite loop + { + byte times = config.Reader.ReadByte(); + int repeatOffset = config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; + if (!EventExists(offset)) + { + AddEvent(new RepeatCommand { Times = times, Offset = repeatOffset }); + } + break; + }*/ + case 0xB9: + { + track.DataOffset += 3; + break; + } + case 0xBA: + { + track.Priority = rom[track.DataOffset++]; + break; + } + case 0xBB: + { + _player.Tempo = (ushort)(rom[track.DataOffset++] * 2); + break; + } + case 0xBC: + { + track.Transpose = (sbyte)rom[track.DataOffset++]; + break; + } + // Commands that work within running status: + case 0xBD: + { + track.Voice = rom[track.DataOffset++]; + track.Ready = true; + break; + } + case 0xBE: + { + track.Volume = rom[track.DataOffset++]; + update = true; + break; + } + case 0xBF: + { + track.Panpot = (sbyte)(rom[track.DataOffset++] - 0x40); + update = true; + break; + } + case 0xC0: + { + track.PitchBend = (sbyte)(rom[track.DataOffset++] - 0x40); + update = true; + break; + } + case 0xC1: + { + track.PitchBendRange = rom[track.DataOffset++]; + update = true; + break; + } + case 0xC2: + { + track.LFOSpeed = rom[track.DataOffset++]; + track.LFOPhase = 0; + track.LFODelayCount = 0; + update = true; + break; + } + case 0xC3: + { + track.LFODelay = rom[track.DataOffset++]; + track.LFOPhase = 0; + track.LFODelayCount = 0; + update = true; + break; + } + case 0xC4: + { + track.LFODepth = rom[track.DataOffset++]; + update = true; + break; + } + case 0xC5: + { + track.LFOType = (LFOType)rom[track.DataOffset++]; + update = true; + break; + } + case 0xC8: + { + track.Tune = (sbyte)(rom[track.DataOffset++] - 0x40); + update = true; + break; + } + case 0xCD: + { + track.DataOffset += 2; + break; + } + case 0xCE: + { + byte peek = rom[track.DataOffset]; + if (peek > 0x7F) + { + track.ReleaseChannels(track.PrevNote); + } + else + { + track.DataOffset++; + track.PrevNote = peek; + int k = peek + track.Transpose; + if (k < 0) + { + k = 0; + } + else if (k > 0x7F) + { + k = 0x7F; + } + track.ReleaseChannels(k); + } + break; + } + default: throw new MP2KInvalidCMDException(track.Index, track.DataOffset - 1, cmd); + } + } + } + + public void UpdateInstrumentCache(byte voice, out string str) + { + byte t = _player.Config.ROM[_voiceTableOffset + (voice * 12)]; + if (t == (byte)VoiceFlags.KeySplit) + { + str = "Key Split"; + } + else if (t == (byte)VoiceFlags.Drum) + { + str = "Drum"; + } + else + { + switch ((VoiceType)(t & 0x7)) // Disregard the other flags + { + case VoiceType.PCM8: str = "PCM8"; break; + case VoiceType.Square1: str = "Square 1"; break; + case VoiceType.Square2: str = "Square 2"; break; + case VoiceType.PCM4: str = "PCM4"; break; + case VoiceType.Noise: str = "Noise"; break; + case VoiceType.Invalid5: str = "Invalid 5"; break; + case VoiceType.Invalid6: str = "Invalid 6"; break; + default: str = "Invalid 7"; break; // VoiceType.Invalid7 + } + } + } + public void UpdateSongState(SongState info, string?[] voiceTypeCache) + { + for (int trackIndex = 0; trackIndex < Tracks.Length; trackIndex++) + { + Tracks[trackIndex].UpdateSongState(info.Tracks[trackIndex], this, voiceTypeCache); + } + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs b/VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs new file mode 100644 index 00000000..8997f706 --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs @@ -0,0 +1,265 @@ +using Kermalis.VGMusicStudio.Core.Util; +using NAudio.Wave; +using System; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +public sealed class MP2KMixer : Mixer +{ + internal readonly int SampleRate; + internal readonly int SamplesPerBuffer; + internal readonly float SampleRateReciprocal; + private readonly float _samplesReciprocal; + internal readonly float PCM8MasterVolume; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; + + internal readonly MP2KConfig Config; + private readonly WaveBuffer _audio; + private readonly float[][] _trackBuffers; + private readonly MP2KPCM8Channel[] _pcm8Channels; + private readonly MP2KSquareChannel _sq1; + private readonly MP2KSquareChannel _sq2; + private readonly MP2KPCM4Channel _pcm4; + private readonly MP2KNoiseChannel _noise; + private readonly MP2KPSGChannel[] _psgChannels; + private readonly BufferedWaveProvider _buffer; + + protected override WaveFormat WaveFormat => _buffer.WaveFormat; + + internal MP2KMixer(MP2KConfig config) + { + Config = config; + (SampleRate, SamplesPerBuffer) = MP2KUtils.FrequencyTable[config.SampleRate]; + SampleRateReciprocal = 1f / SampleRate; + _samplesReciprocal = 1f / SamplesPerBuffer; + PCM8MasterVolume = config.Volume / 15f; + + _pcm8Channels = new MP2KPCM8Channel[24]; + for (int i = 0; i < _pcm8Channels.Length; i++) + { + _pcm8Channels[i] = new MP2KPCM8Channel(this); + } + _psgChannels = new MP2KPSGChannel[4] { _sq1 = new MP2KSquareChannel(this), _sq2 = new MP2KSquareChannel(this), _pcm4 = new MP2KPCM4Channel(this), _noise = new MP2KNoiseChannel(this), }; + + int amt = SamplesPerBuffer * 2; + _audio = new WaveBuffer(amt * sizeof(float)) { FloatBufferCount = amt }; + _trackBuffers = new float[0x10][]; + for (int i = 0; i < _trackBuffers.Length; i++) + { + _trackBuffers[i] = new float[amt]; + } + _buffer = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(SampleRate, 2)) + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64, + }; + Init(_buffer); + } + + internal MP2KPCM8Channel? AllocPCM8Channel(MP2KTrack owner, ADSR env, NoteInfo note, byte vol, sbyte pan, int instPan, int pitch, bool bFixed, bool bCompressed, int sampleOffset) + { + MP2KPCM8Channel? nChn = null; + IOrderedEnumerable byOwner = _pcm8Channels.OrderByDescending(c => c.Owner is null ? 0xFF : c.Owner.Index); + foreach (MP2KPCM8Channel i in byOwner) // Find free + { + if (i.State == EnvelopeState.Dead || i.Owner is null) + { + nChn = i; + break; + } + } + if (nChn is null) // Find releasing + { + foreach (MP2KPCM8Channel i in byOwner) + { + if (i.State == EnvelopeState.Releasing) + { + nChn = i; + break; + } + } + } + if (nChn is null) // Find prioritized + { + foreach (MP2KPCM8Channel i in byOwner) + { + if (owner.Priority > i.Owner!.Priority) + { + nChn = i; + break; + } + } + } + if (nChn is null) // None available + { + MP2KPCM8Channel lowest = byOwner.First(); // Kill lowest track's instrument if the track is lower than this one + if (lowest.Owner!.Index >= owner.Index) + { + nChn = lowest; + } + } + if (nChn is not null) // Could still be null from the above if + { + nChn.Init(owner, note, env, sampleOffset, vol, pan, instPan, pitch, bFixed, bCompressed); + } + return nChn; + } + internal MP2KPSGChannel? AllocPSGChannel(MP2KTrack owner, ADSR env, NoteInfo note, byte vol, sbyte pan, int instPan, int pitch, VoiceType type, object arg) + { + MP2KPSGChannel nChn; + switch (type) + { + case VoiceType.Square1: + { + nChn = _sq1; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) + { + return null; + } + _sq1.Init(owner, note, env, instPan, (SquarePattern)arg); + break; + } + case VoiceType.Square2: + { + nChn = _sq2; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) + { + return null; + } + _sq2.Init(owner, note, env, instPan, (SquarePattern)arg); + break; + } + case VoiceType.PCM4: + { + nChn = _pcm4; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) + { + return null; + } + _pcm4.Init(owner, note, env, instPan, (int)arg); + break; + } + case VoiceType.Noise: + { + nChn = _noise; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) + { + return null; + } + _noise.Init(owner, note, env, instPan, (NoisePattern)arg); + break; + } + default: return null; + } + nChn.SetVolume(vol, pan); + nChn.SetPitch(pitch); + return nChn; + } + + internal void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * GBAUtils.AGB_FPS); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * GBAUtils.AGB_FPS); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal bool IsFading() + { + return _isFading; + } + internal bool IsFadeDone() + { + return _isFading && _fadeMicroFramesLeft == 0; + } + internal void ResetFade() + { + _isFading = false; + _fadeMicroFramesLeft = 0; + } + + internal void Process(bool output, bool recording) + { + for (int i = 0; i < _trackBuffers.Length; i++) + { + float[] buf = _trackBuffers[i]; + Array.Clear(buf, 0, buf.Length); + } + _audio.Clear(); + + for (int i = 0; i < _pcm8Channels.Length; i++) + { + MP2KPCM8Channel c = _pcm8Channels[i]; + if (c.Owner is not null) + { + c.Process(_trackBuffers[c.Owner.Index]); + } + } + + for (int i = 0; i < _psgChannels.Length; i++) + { + MP2KPSGChannel c = _psgChannels[i]; + if (c.Owner is not null) + { + c.Process(_trackBuffers[c.Owner.Index]); + } + } + + float masterStep; + float masterLevel; + if (_isFading && _fadeMicroFramesLeft == 0) + { + masterStep = 0; + masterLevel = 0; + } + else + { + float fromMaster = 1f; + float toMaster = 1f; + if (_fadeMicroFramesLeft > 0) + { + const float scale = 10f / 6f; + fromMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadePos += _fadeStepPerMicroframe; + toMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadeMicroFramesLeft--; + } + masterStep = (toMaster - fromMaster) * _samplesReciprocal; + masterLevel = fromMaster; + } + for (int i = 0; i < _trackBuffers.Length; i++) + { + if (Mutes[i]) + { + continue; + } + + float level = masterLevel; + float[] buf = _trackBuffers[i]; + for (int j = 0; j < SamplesPerBuffer; j++) + { + _audio.FloatBuffer[j * 2] += buf[j * 2] * level; + _audio.FloatBuffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; + level += masterStep; + } + } + if (output) + { + _buffer.AddSamples(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + } + if (recording) + { + _waveWriter!.Write(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + } + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs b/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs new file mode 100644 index 00000000..7ebe510e --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs @@ -0,0 +1,155 @@ +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +public sealed partial class MP2KPlayer : Player +{ + protected override string Name => "MP2K Player"; + + private readonly string?[] _voiceTypeCache; + internal readonly MP2KConfig Config; + internal readonly MP2KMixer MMixer; + private MP2KLoadedSong? _loadedSong; + + internal ushort Tempo; + internal int TempoStack; + private long _elapsedLoops; + + private int? _prevVoiceTableOffset; + + public override ILoadedSong? LoadedSong => _loadedSong; + protected override Mixer Mixer => MMixer; + + internal MP2KPlayer(MP2KConfig config, MP2KMixer mixer) + : base(GBAUtils.AGB_FPS) + { + Config = config; + MMixer = mixer; + + _voiceTypeCache = new string[256]; + } + + public override void LoadSong(int index) + { + if (_loadedSong is not null) + { + _loadedSong = null; + } + + // If there's an exception, this will remain null + _loadedSong = new MP2KLoadedSong(this, index); + if (_loadedSong.Events.Length == 0) + { + _loadedSong = null; + return; + } + + _loadedSong.CheckVoiceTypeCache(ref _prevVoiceTableOffset, _voiceTypeCache); + _loadedSong.SetTicks(); + } + public override void UpdateSongState(SongState info) + { + info.Tempo = Tempo; + _loadedSong!.UpdateSongState(info, _voiceTypeCache); + } + internal override void InitEmulation() + { + Tempo = 150; + TempoStack = 0; + _elapsedLoops = 0; + ElapsedTicks = 0; + MMixer.ResetFade(); + MP2KTrack[] tracks = _loadedSong!.Tracks; + for (int i = 0; i < tracks.Length; i++) + { + tracks[i].Init(); + } + } + protected override void SetCurTick(long ticks) + { + _loadedSong!.SetCurTick(ticks); + } + protected override void OnStopped() + { + MP2KTrack[] tracks = _loadedSong!.Tracks; + for (int i = 0; i < tracks.Length; i++) + { + tracks[i].StopAllChannels(); + } + } + + protected override bool Tick(bool playing, bool recording) + { + MP2KLoadedSong s = _loadedSong!; + + bool allDone = false; + while (!allDone && TempoStack >= 150) + { + TempoStack -= 150; + allDone = true; + for (int i = 0; i < s.Tracks.Length; i++) + { + TickTrack(s, s.Tracks[i], ref allDone); + } + if (MMixer.IsFadeDone()) + { + allDone = true; + } + } + if (!allDone) + { + TempoStack += Tempo; + } + MMixer.Process(playing, recording); + return allDone; + } + private void TickTrack(MP2KLoadedSong s, MP2KTrack track, ref bool allDone) + { + track.Tick(); + bool update = false; + while (track.Rest == 0 && !track.Stopped) + { + s.ExecuteNext(track, ref update); + } + if (track.Index == s.LongestTrack) + { + HandleTicksAndLoop(s, track); + } + if (!track.Stopped) + { + allDone = false; + } + if (track.Channels.Count > 0) + { + allDone = false; + if (update || track.LFODepth > 0) + { + track.UpdateChannels(); + } + } + } + private void HandleTicksAndLoop(MP2KLoadedSong s, MP2KTrack track) + { + if (ElapsedTicks != s.MaxTicks) + { + ElapsedTicks++; + return; + } + + // Track reached the detected end, update loops/ticks accordingly + if (track.Stopped) + { + return; + } + + _elapsedLoops++; + UpdateElapsedTicksAfterLoop(s.Events[track.Index], track.DataOffset, track.Rest); + if (ShouldFadeOut && _elapsedLoops > NumLoops && !MMixer.IsFading()) + { + MMixer.BeginFadeOut(); + } + } + + public void SaveAsMIDI(string fileName, MIDISaveArgs args) + { + _loadedSong!.SaveAsMIDI(fileName, args); + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KStructs.cs b/VG Music Studio - Core/GBA/MP2K/MP2KStructs.cs new file mode 100644 index 00000000..5b33542c --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KStructs.cs @@ -0,0 +1,187 @@ +using System; +using System.Runtime.InteropServices; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = SIZE)] +internal readonly struct SongEntry +{ + public const int SIZE = 8; + + public readonly int HeaderOffset; + public readonly short Player; + public readonly byte Unknown1; + public readonly byte Unknown2; + + public SongEntry(ReadOnlySpan src) + { + if (BitConverter.IsLittleEndian) + { + this = MemoryMarshal.AsRef(src); + } + else + { + HeaderOffset = ReadInt32LittleEndian(src.Slice(0)); + Player = ReadInt16LittleEndian(src.Slice(4)); + Unknown1 = src[6]; + Unknown2 = src[7]; + } + } + + public static SongEntry Get(byte[] rom, int songTableOffset, int songNum) + { + return new SongEntry(rom.AsSpan(songTableOffset + (songNum * SIZE))); + } +} +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = SIZE)] +internal readonly struct SongHeader +{ + public const int SIZE = 8; + + public readonly byte NumTracks; + public readonly byte NumBlocks; + public readonly byte Priority; + public readonly byte Reverb; + public readonly int VoiceTableOffset; + // int[NumTracks] TrackOffset; + + public SongHeader(ReadOnlySpan src) + { + if (BitConverter.IsLittleEndian) + { + this = MemoryMarshal.AsRef(src); + } + else + { + NumTracks = src[0]; + NumBlocks = src[1]; + Priority = src[2]; + Reverb = src[3]; + VoiceTableOffset = ReadInt32LittleEndian(src.Slice(4)); + } + } + + public static SongHeader Get(byte[] rom, int offset, out int tracksOffset) + { + tracksOffset = offset + SIZE; + return new SongHeader(rom.AsSpan(offset)); + } + public static int GetTrackOffset(byte[] rom, int tracksOffset, int trackIndex) + { + return ReadInt32LittleEndian(rom.AsSpan(tracksOffset + (trackIndex * 4))); + } +} +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = SIZE)] +internal readonly struct VoiceEntry +{ + public const int SIZE = 12; + + public readonly byte Type; // 0 + public readonly byte RootNote; // 1 + public readonly byte Unknown; // 2 + public readonly byte Pan; // 3 + /// SquarePattern for Square1/Square2, NoisePattern for Noise, Address for PCM8/PCM4/KeySplit/Drum + public readonly int Int4; // 4 + /// ADSR for PCM8/Square1/Square2/PCM4/Noise, KeysAddress for KeySplit + public readonly ADSR ADSR; // 8 + + public int Int8 => (ADSR.R << 24) | (ADSR.S << 16) | (ADSR.D << 8) | (ADSR.A); + + public VoiceEntry(ReadOnlySpan src) + { + if (BitConverter.IsLittleEndian) + { + this = MemoryMarshal.AsRef(src); + } + else + { + Type = src[0]; + RootNote = src[1]; + Unknown = src[2]; + Pan = src[3]; + Int4 = ReadInt32LittleEndian(src.Slice(4)); + ADSR = ADSR.Get(src.Slice(8)); + } + } +} +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = SIZE)] +internal struct ADSR +{ + public const int SIZE = 4; + + public byte A; + public byte D; + public byte S; + public byte R; + + public static ref readonly ADSR Get(ReadOnlySpan src) + { + return ref MemoryMarshal.AsRef(src); + } +} +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = SIZE)] +internal readonly struct GoldenSunPSG +{ + public const int SIZE = 6; + + /// Always 0x80 + public readonly byte Unknown; + public readonly GoldenSunPSGType Type; + public readonly byte InitialCycle; + public readonly byte CycleSpeed; + public readonly byte CycleAmplitude; + public readonly byte MinimumCycle; + + public static ref readonly GoldenSunPSG Get(ReadOnlySpan src) + { + return ref MemoryMarshal.AsRef(src); + } +} +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = SIZE)] +internal struct SampleHeader +{ + public const int SIZE = 16; + public const int LOOP_TRUE = 0x40_000_000; + + /// 0x40_000_000 if True + public int DoesLoop; + /// Right shift 10 for value + public int SampleRate; + public int LoopOffset; + public int Length; + // byte[Length] Sample; + + public SampleHeader(ReadOnlySpan src) + { + if (BitConverter.IsLittleEndian) + { + this = MemoryMarshal.AsRef(src); + } + else + { + DoesLoop = ReadInt32LittleEndian(src.Slice(0, 4)); + SampleRate = ReadInt32LittleEndian(src.Slice(4, 4)); + LoopOffset = ReadInt32LittleEndian(src.Slice(8, 4)); + Length = ReadInt32LittleEndian(src.Slice(12, 4)); + } + } + + public static SampleHeader Get(byte[] rom, int offset, out int sampleOffset) + { + sampleOffset = offset + SIZE; + return new SampleHeader(rom.AsSpan(offset)); + } +} + +internal struct ChannelVolume +{ + public float LeftVol, RightVol; +} +internal struct NoteInfo +{ + public byte Note, OriginalNote; + public byte Velocity; + /// -1 if forever + public int Duration; +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KTrack.cs b/VG Music Studio - Core/GBA/MP2K/MP2KTrack.cs new file mode 100644 index 00000000..e612465d --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KTrack.cs @@ -0,0 +1,224 @@ +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal sealed class MP2KTrack +{ + public readonly byte Index; + private readonly int _startOffset; + public byte Voice; + public byte PitchBendRange; + public byte Priority; + public byte Volume; + public byte Rest; + public byte LFOPhase; + public byte LFODelayCount; + public byte LFOSpeed; + public byte LFODelay; + public byte LFODepth; + public LFOType LFOType; + public sbyte PitchBend; + public sbyte Tune; + public sbyte Panpot; + public sbyte Transpose; + public bool Ready; + public bool Stopped; + public int DataOffset; + public int[] CallStack = new int[3]; + public byte CallStackDepth; + public byte RunCmd; + public byte PrevNote; + public byte PrevVelocity; + + public readonly List Channels = new(); + + public int GetPitch() + { + int lfo = LFOType == LFOType.Pitch ? (MP2KUtils.Tri(LFOPhase) * LFODepth) >> 8 : 0; + return (PitchBend * PitchBendRange) + Tune + lfo; + } + public byte GetVolume() + { + int lfo = LFOType == LFOType.Volume ? (MP2KUtils.Tri(LFOPhase) * LFODepth * 3 * Volume) >> 19 : 0; + int v = Volume + lfo; + if (v < 0) + { + v = 0; + } + else if (v > 0x7F) + { + v = 0x7F; + } + return (byte)v; + } + public sbyte GetPanpot() + { + int lfo = LFOType == LFOType.Panpot ? (MP2KUtils.Tri(LFOPhase) * LFODepth * 3) >> 12 : 0; + int p = Panpot + lfo; + if (p < -0x40) + { + p = -0x40; + } + else if (p > 0x3F) + { + p = 0x3F; + } + return (sbyte)p; + } + + public MP2KTrack(byte i, int startOffset) + { + Index = i; + _startOffset = startOffset; + } + public void Init() + { + Voice = 0; + Priority = 0; + Rest = 0; + LFODelay = 0; + LFODelayCount = 0; + LFOPhase = 0; + LFODepth = 0; + CallStackDepth = 0; + PitchBend = 0; + Tune = 0; + Panpot = 0; + Transpose = 0; + DataOffset = _startOffset; + RunCmd = 0; + PrevNote = 0; + PrevVelocity = 0x7F; + PitchBendRange = 2; + LFOType = LFOType.Pitch; + Ready = false; + Stopped = false; + LFOSpeed = 22; + Volume = 100; + StopAllChannels(); + } + public void Tick() + { + if (Rest != 0) + { + Rest--; + } + if (LFODepth > 0) + { + LFOPhase += LFOSpeed; + } + else + { + LFOPhase = 0; + } + int active = 0; + MP2KChannel[] chans = Channels.ToArray(); + for (int i = 0; i < chans.Length; i++) + { + if (chans[i].TickNote()) + { + active++; + } + } + if (active != 0) + { + if (LFODelayCount > 0) + { + LFODelayCount--; + LFOPhase = 0; + } + } + else + { + LFODelayCount = LFODelay; + } + if ((LFODelay == LFODelayCount && LFODelay != 0) || LFOSpeed == 0) + { + LFOPhase = 0; + } + } + + public void ReleaseChannels(int key) + { + MP2KChannel[] chans = Channels.ToArray(); + for (int i = 0; i < chans.Length; i++) + { + MP2KChannel c = chans[i]; + if (c.Note.OriginalNote == key && c.Note.Duration == -1) + { + c.Release(); + } + } + } + public void StopAllChannels() + { + MP2KChannel[] chans = Channels.ToArray(); + for (int i = 0; i < chans.Length; i++) + { + chans[i].Stop(); + } + } + public void UpdateChannels() + { + byte vol = GetVolume(); + sbyte pan = GetPanpot(); + int pitch = GetPitch(); + for (int i = 0; i < Channels.Count; i++) + { + MP2KChannel c = Channels[i]; + c.SetVolume(vol, pan); + c.SetPitch(pitch); + } + } + + public void UpdateSongState(SongState.Track tin, MP2KLoadedSong loadedSong, string?[] voiceTypeCache) + { + tin.Position = DataOffset; + tin.Rest = Rest; + tin.Voice = Voice; + tin.LFO = LFODepth; + ref string? cache = ref voiceTypeCache[Voice]; + if (cache is null) + { + loadedSong.UpdateInstrumentCache(Voice, out cache); + } + tin.Type = cache; + tin.Volume = GetVolume(); + tin.PitchBend = GetPitch(); + tin.Panpot = GetPanpot(); + + MP2KChannel[] channels = Channels.ToArray(); + if (channels.Length == 0) + { + tin.Keys[0] = byte.MaxValue; + tin.LeftVolume = 0f; + tin.RightVolume = 0f; + } + else + { + int numKeys = 0; + float left = 0f; + float right = 0f; + for (int j = 0; j < channels.Length; j++) + { + MP2KChannel c = channels[j]; + if (c.State < EnvelopeState.Releasing) + { + tin.Keys[numKeys++] = c.Note.OriginalNote; + } + ChannelVolume vol = c.GetVolume(); + if (vol.LeftVol > left) + { + left = vol.LeftVol; + } + if (vol.RightVol > right) + { + right = vol.RightVol; + } + } + tin.Keys[numKeys] = byte.MaxValue; // There's no way for numKeys to be after the last index in the array + tin.LeftVolume = left; + tin.RightVolume = right; + } + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs b/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs new file mode 100644 index 00000000..6b3b0f6f --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal static partial class MP2KUtils +{ + public static ReadOnlySpan RestTable => new byte[49] + { + 00, 01, 02, 03, 04, 05, 06, 07, + 08, 09, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 28, 30, 32, 36, 40, 42, 44, + 48, 52, 54, 56, 60, 64, 66, 68, + 72, 76, 78, 80, 84, 88, 90, 92, + 96, + }; + public static ReadOnlySpan<(int sampleRate, int samplesPerBuffer)> FrequencyTable => new (int, int)[12] + { + (05734, 096), // 59.72916666666667 + (07884, 132), // 59.72727272727273 + (10512, 176), // 59.72727272727273 + (13379, 224), // 59.72767857142857 + (15768, 264), // 59.72727272727273 + (18157, 304), // 59.72697368421053 + (21024, 352), // 59.72727272727273 + (26758, 448), // 59.72767857142857 + (31536, 528), // 59.72727272727273 + (36314, 608), // 59.72697368421053 + (40137, 672), // 59.72767857142857 + (42048, 704), // 59.72727272727273 + }; + + // Squares (Use arrays since they are stored as references in MP2KSquareChannel) + public static readonly float[] SquareD12 = new float[8] { 0.875f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f, }; + public static readonly float[] SquareD25 = new float[8] { 0.750f, 0.750f, -0.250f, -0.250f, -0.250f, -0.250f, -0.250f, -0.250f, }; + public static readonly float[] SquareD50 = new float[8] { 0.500f, 0.500f, 0.500f, 0.500f, -0.500f, -0.500f, -0.500f, -0.500f, }; + public static readonly float[] SquareD75 = new float[8] { 0.250f, 0.250f, 0.250f, 0.250f, 0.250f, 0.250f, -0.750f, -0.750f, }; + + // Noises + public static readonly BitArray NoiseFine; + public static readonly BitArray NoiseRough; + public static ReadOnlySpan NoiseFrequencyTable => new byte[60] + { + 0xD7, 0xD6, 0xD5, 0xD4, + 0xC7, 0xC6, 0xC5, 0xC4, + 0xB7, 0xB6, 0xB5, 0xB4, + 0xA7, 0xA6, 0xA5, 0xA4, + 0x97, 0x96, 0x95, 0x94, + 0x87, 0x86, 0x85, 0x84, + 0x77, 0x76, 0x75, 0x74, + 0x67, 0x66, 0x65, 0x64, + 0x57, 0x56, 0x55, 0x54, + 0x47, 0x46, 0x45, 0x44, + 0x37, 0x36, 0x35, 0x34, + 0x27, 0x26, 0x25, 0x24, + 0x17, 0x16, 0x15, 0x14, + 0x07, 0x06, 0x05, 0x04, + 0x03, 0x02, 0x01, 0x00, + }; + + // PCM4 + /// dest must be 0x20 bytes + public static void PCM4ToFloat(ReadOnlySpan src, Span dest) + { + float sum = 0; + for (int i = 0; i < 0x10; i++) + { + byte b = src[i]; + float first = (b >> 4) / 16f; + float second = (b & 0xF) / 16f; + sum += dest[i * 2] = first; + sum += dest[(i * 2) + 1] = second; + } + float dcCorrection = sum / 0x20; + for (int i = 0; i < 0x20; i++) + { + dest[i] -= dcCorrection; + } + } + + static MP2KUtils() + { + NoiseFine = new BitArray(0x8_000); + int reg = 0x4_000; + for (int i = 0; i < NoiseFine.Length; i++) + { + if ((reg & 1) == 1) + { + reg >>= 1; + reg ^= 0x6_000; + NoiseFine[i] = true; + } + else + { + reg >>= 1; + NoiseFine[i] = false; + } + } + NoiseRough = new BitArray(0x80); + reg = 0x40; + for (int i = 0; i < NoiseRough.Length; i++) + { + if ((reg & 1) == 1) + { + reg >>= 1; + reg ^= 0x60; + NoiseRough[i] = true; + } + else + { + reg >>= 1; + NoiseRough[i] = false; + } + } + } + public static int Tri(int index) + { + index = (index - 64) & 0xFF; + return (index < 128) ? (index * 12) - 768 : 2_304 - (index * 12); + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KUtils_PkmnCompress.cs b/VG Music Studio - Core/GBA/MP2K/MP2KUtils_PkmnCompress.cs new file mode 100644 index 00000000..ae259587 --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KUtils_PkmnCompress.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +partial class MP2KUtils +{ + private static ReadOnlySpan CompressionLookup => new sbyte[16] + { + 0, 1, 4, 9, 16, 25, 36, 49, -64, -49, -36, -25, -16, -9, -4, -1, + }; + + // TODO: Do runtime + // TODO: How large is the decompress buffer in-game? + public static sbyte[] Decompress(ReadOnlySpan src, int sampleLength) + { + var samples = new List(); + sbyte compressionLevel = 0; + int compressionByte = 0, compressionIdx = 0; + + for (int i = 0; true; i++) + { + byte b = src[i]; + if (compressionByte == 0) + { + compressionByte = 0x20; + compressionLevel = (sbyte)b; + samples.Add(compressionLevel); + if (++compressionIdx >= sampleLength) + { + break; + } + } + else + { + if (compressionByte < 0x20) + { + compressionLevel += CompressionLookup[b >> 4]; + samples.Add(compressionLevel); + if (++compressionIdx >= sampleLength) + { + break; + } + } + // Potential for 2 samples to be added here at the same time + compressionByte--; + compressionLevel += CompressionLookup[b & 0xF]; + samples.Add(compressionLevel); + if (++compressionIdx >= sampleLength) + { + break; + } + } + } + + return samples.ToArray(); + } +} diff --git a/VG Music Studio/MP2K.yaml b/VG Music Studio - Core/MP2K.yaml similarity index 91% rename from VG Music Studio/MP2K.yaml rename to VG Music Studio - Core/MP2K.yaml index 1347ea8b..d73128cd 100644 --- a/VG Music Studio/MP2K.yaml +++ b/VG Music Studio - Core/MP2K.yaml @@ -1,3 +1,21 @@ +A2NE_00: + Name: "Sonic Advance 2 (USA)" + SongTableOffsets: 0xAD4F4C + SongTableSizes: 507 + SampleRate: 2 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +A2NP_00: + Name: "Sonic Advance 2 (Europe)" + SongTableOffsets: 0xAD4F4C + Copy: "A2NE_00" +A2NJ_00: + Name: "Sonic Advance 2 (Japan)" + SongTableOffsets: 0xAD4B14 + Copy: "A2NE_00" A2UJ_00: Name: "Mother 1 + 2 (Japan)" SongTableOffsets: 0x10B530 @@ -223,6 +241,20 @@ AFXP_00: Name: "Final Fantasy Tactics Advance (Europe)" SongTableOffsets: 0x14F540 Copy: "AFXE_00" +AFZE_00: + Name: "F-Zero: Maximum Velocity (USA)" + SongTableOffsets: 0x54BF8 + SongTableSizes: 72 + SampleRate: 2 + ReverbType: "Normal" + Reverb: 0 + Volume: 10 + HasGoldenSunSynths: False + HasPokemonCompression: False +AFZJ_00: + Name: "F-Zero: Maximum Velocity (Japan)" + SongTableOffsets: 0x58324 + Copy: "AFZE_00" AGFD_00: Name: "Golden Sun: The Lost Age (Germany)" Copy: "AGFE_00" @@ -961,6 +993,24 @@ B24J_00: B24P_00: Name: "Pokémon Mystery Dungeon: Red Rescue Team (Europe)" Copy: "B24E_00" +B8KE_00: + Name: "Kirby & The Amazing Mirror (USA)" + SongTableOffsets: 0xB59ED0 + SongTableSizes: 620 + SampleRate: 4 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +B8KJ_01: + Name: "Kirby & The Amazing Mirror (Japan)" + SongTableOffsets: 0xB253E0 + Copy: "B8KE_00" +B8KP_00: + Name: "Kirby & The Amazing Mirror (Europe/Australia)" + SongTableOffsets: 0xB64334 + Copy: "B8KE_00" BE8E_00: Name: "Fire Emblem: The Sacred Stones (USA)" SongTableOffsets: 0x224470 @@ -979,6 +1029,52 @@ BE8P_00: Name: "Fire Emblem: The Sacred Stones (Europe)" SongTableOffsets: 0x42FFB0 Copy: "BE8E_00" +BFFE_00: + Name: "Final Fantasy I & II - Dawn of Souls (USA)" + SongTableOffsets: 0x8D3058 + SongTableSizes: 717 + SampleRate: 5 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +BFFJ_01: + Name: "Final Fantasy I & II Advance (Japan)" + SongTableOffsets: 0x8F7BA8 + Copy: "BFFE_00" +BFFP_00: + Name: "Final Fantasy I & II - Dawn of Souls (Europe)" + SongTableOffsets: 0x974660 + Copy: "BFFE_00" +BFTJ_00: + Name: "F-Zero Climax (Japan)" + SongTableOffsets: 0x9EE84 + SongTableSizes: 439 + SampleRate: 5 + ReverbType: "Normal" + Reverb: 0 + Volume: 14 + HasGoldenSunSynths: False + HasPokemonCompression: False +BFZE_00: + Name: "F-Zero: GP Legend (USA)" + SongTableOffsets: 0x97B44 + SongTableSizes: 352 + SampleRate: 5 + ReverbType: "Normal" + Reverb: 0 + Volume: 14 + HasGoldenSunSynths: False + HasPokemonCompression: False +BFZJ_00: + Name: "F-Zero: GP Legend (Japan)" + SongTableOffsets: 0xA3BEC + Copy: "BFZE_00" +BFZP_00: + Name: "F-Zero: GP Legend (Europe)" + SongTableOffsets: 0xA21D4 + Copy: "BFZE_00" BMXC_00: Name: "Metroid: Zero Mission (China)" SongTableOffsets: 0xA8AAC @@ -1518,6 +1614,63 @@ BPRS_00: Name: "Pokémon Fire Red Version (Spain)" SongTableOffsets: 0x499BC8 Copy: "BPRE_00" +BZ4E_00: + Name: "Final Fantasy IV Advance (USA)" + SongTableOffsets: 0x3DC894 + SongTableSizes: 221 + SampleRate: 5 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +BZ4J_00: + Name: "Final Fantasy IV Advance (Japan)" + SongTableOffsets: 0x416D0C + Copy: "BZ4E_00" +BZ4J_01: + SongTableOffsets: 0x3FA87C + Copy: "BZ4J_00" +BZ4P_00: + Name: "Final Fantasy IV Advance (Europe)" + SongTableOffsets: 0x4F5A5C + Copy: "BZ4E_00" +BZ5E_00: + Name: "Final Fantasy V Advance (USA)" + SongTableOffsets: 0x3F2CB4 + SongTableSizes: 290 + SampleRate: 5 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +BZ5J_00: + Name: "Final Fantasy V Advance (Japan)" + SongTableOffsets: 0x41524C + Copy: "BZ5E_00" +BZ5P_00: + Name: "Final Fantasy V Advance (Europe)" + SongTableOffsets: 0x540620 + Copy: "BZ5E_00" +BZ6E_00: + Name: "Final Fantasy VI Advance (USA)" + SongTableOffsets: 0x1C6A94 + SongTableSizes: 371 + SampleRate: 5 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +BZ6J_00: + Name: "Final Fantasy VI Advance (Japan)" + SongTableOffsets: 0x1EA5DC + Copy: "BZ6E_00" +BZ6P_00: + Name: "Final Fantasy VI Advance (Europe)" + SongTableOffsets: 0x16F41C + Copy: "BZ6E_00" BZME_00: Name: "The Legend of Zelda: The Minish Cap (USA)" SongTableOffsets: 0xA11DBC @@ -1536,6 +1689,24 @@ BZMP_00: Name: "The Legend of Zelda: The Minish Cap (Europe)" SongTableOffsets: 0xB1D414 Copy: "BZME_00" +KYGE_00: + Name: "Yoshi Topsy-Turvy (USA)" + SongTableOffsets: 0x4A5E94 + SongTableSizes: 347 + SampleRate: 2 + ReverbType: "Normal" + Reverb: 0 + Volume: 10 + HasGoldenSunSynths: False + HasPokemonCompression: False +KYGJ_00: + Name: "Yoshi Topsy-Turvy (Japan)" + SongTableOffsets: 0x4A79D8 + Copy: "KYGE_00" +KYGP_00: + Name: "Yoshi Topsy-Turvy (Europe)" + SongTableOffsets: 0x619658 + Copy: "KYGE_00" U32E_00: Name: "Boktai 2 - Solar Boy Django (USA)" SongTableOffsets: 0x25EEA8 diff --git a/VG Music Studio/MPlayDef.s b/VG Music Studio - Core/MPlayDef.s similarity index 100% rename from VG Music Studio/MPlayDef.s rename to VG Music Studio - Core/MPlayDef.s diff --git a/VG Music Studio - Core/Mixer.cs b/VG Music Studio - Core/Mixer.cs new file mode 100644 index 00000000..7b8d4c55 --- /dev/null +++ b/VG Music Studio - Core/Mixer.cs @@ -0,0 +1,109 @@ +using NAudio.CoreAudioApi; +using NAudio.CoreAudioApi.Interfaces; +using NAudio.Wave; +using System; + +namespace Kermalis.VGMusicStudio.Core; + +public abstract class Mixer : IAudioSessionEventsHandler, IDisposable +{ + public static event Action? VolumeChanged; + + public readonly bool[] Mutes; + private IWavePlayer _out; + private AudioSessionControl _appVolume; + + private bool _shouldSendVolUpdateEvent = true; + + protected WaveFileWriter? _waveWriter; + protected abstract WaveFormat WaveFormat { get; } + + protected Mixer() + { + Mutes = new bool[SongState.MAX_TRACKS]; + _out = null!; + _appVolume = null!; + } + + protected void Init(IWaveProvider waveProvider) + { + _out = new WasapiOut(); + _out.Init(waveProvider); + using (var en = new MMDeviceEnumerator()) + { + SessionCollection sessions = en.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia).AudioSessionManager.Sessions; + int id = Environment.ProcessId; + for (int i = 0; i < sessions.Count; i++) + { + AudioSessionControl session = sessions[i]; + if (session.GetProcessID == id) + { + _appVolume = session; + _appVolume.RegisterEventClient(this); + break; + } + } + } + _out.Play(); + } + + public void CreateWaveWriter(string fileName) + { + _waveWriter = new WaveFileWriter(fileName, WaveFormat); + } + public void CloseWaveWriter() + { + _waveWriter!.Dispose(); + _waveWriter = null; + } + + public void OnVolumeChanged(float volume, bool isMuted) + { + if (_shouldSendVolUpdateEvent) + { + VolumeChanged?.Invoke(volume); + } + _shouldSendVolUpdateEvent = true; + } + public void OnDisplayNameChanged(string displayName) + { + throw new NotImplementedException(); + } + public void OnIconPathChanged(string iconPath) + { + throw new NotImplementedException(); + } + public void OnChannelVolumeChanged(uint channelCount, IntPtr newVolumes, uint channelIndex) + { + throw new NotImplementedException(); + } + public void OnGroupingParamChanged(ref Guid groupingId) + { + throw new NotImplementedException(); + } + // Fires on @out.Play() and @out.Stop() + public void OnStateChanged(AudioSessionState state) + { + if (state == AudioSessionState.AudioSessionStateActive) + { + OnVolumeChanged(_appVolume.SimpleAudioVolume.Volume, _appVolume.SimpleAudioVolume.Mute); + } + } + public void OnSessionDisconnected(AudioSessionDisconnectReason disconnectReason) + { + throw new NotImplementedException(); + } + public void SetVolume(float volume) + { + _shouldSendVolUpdateEvent = false; + _appVolume.SimpleAudioVolume.Volume = volume; + } + + public virtual void Dispose() + { + GC.SuppressFinalize(this); + _out.Stop(); + _out.Dispose(); + _appVolume.Dispose(); + } +} diff --git a/VG Music Studio - Core/NDS/DSE/DSEChannel.cs b/VG Music Studio - Core/NDS/DSE/DSEChannel.cs new file mode 100644 index 00000000..11670ce0 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSEChannel.cs @@ -0,0 +1,373 @@ +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +internal sealed class DSEChannel +{ + public readonly byte Index; + + public DSETrack? Owner; + public EnvelopeState State; + public byte RootKey; + public byte Key; + public byte NoteVelocity; + public sbyte Panpot; // Not necessary + public ushort BaseTimer; + public ushort Timer; + public uint NoteLength; + public byte Volume; + + private int _pos; + private short _prevLeft; + private short _prevRight; + + private int _envelopeTimeLeft; + private int _volumeIncrement; + private int _velocity; // From 0-0x3FFFFFFF ((128 << 23) - 1) + private byte _targetVolume; + + private byte _attackVolume; + private byte _attack; + private byte _decay; + private byte _sustain; + private byte _hold; + private byte _decay2; + private byte _release; + + // PCM8, PCM16, ADPCM + private SWD.SampleBlock _sample; + // PCM8, PCM16 + private int _dataOffset; + // ADPCM + private ADPCMDecoder _adpcmDecoder; + private short _adpcmLoopLastSample; + private short _adpcmLoopStepIndex; + + public DSEChannel(byte i) + { + _sample = null!; + Index = i; + } + + public bool StartPCM(SWD localswd, SWD masterswd, byte voice, int key, uint noteLength) + { + SWD.IProgramInfo? programInfo = localswd.Programs?.ProgramInfos[voice]; + if (programInfo is null) + { + return false; + } + + for (int i = 0; i < programInfo.SplitEntries.Length; i++) + { + SWD.ISplitEntry split = programInfo.SplitEntries[i]; + if (key < split.LowKey || key > split.HighKey) + { + continue; + } + + _sample = masterswd.Samples![split.SampleId]; + Key = (byte)key; + RootKey = split.SampleRootKey; + BaseTimer = (ushort)(NDSUtils.ARM7_CLOCK / _sample.WavInfo.SampleRate); + if (_sample.WavInfo.SampleFormat == SampleFormat.ADPCM) + { + _adpcmDecoder.Init(_sample.Data); + } + //attackVolume = sample.WavInfo.AttackVolume == 0 ? split.AttackVolume : sample.WavInfo.AttackVolume; + //attack = sample.WavInfo.Attack == 0 ? split.Attack : sample.WavInfo.Attack; + //decay = sample.WavInfo.Decay == 0 ? split.Decay : sample.WavInfo.Decay; + //sustain = sample.WavInfo.Sustain == 0 ? split.Sustain : sample.WavInfo.Sustain; + //hold = sample.WavInfo.Hold == 0 ? split.Hold : sample.WavInfo.Hold; + //decay2 = sample.WavInfo.Decay2 == 0 ? split.Decay2 : sample.WavInfo.Decay2; + //release = sample.WavInfo.Release == 0 ? split.Release : sample.WavInfo.Release; + //attackVolume = split.AttackVolume == 0 ? sample.WavInfo.AttackVolume : split.AttackVolume; + //attack = split.Attack == 0 ? sample.WavInfo.Attack : split.Attack; + //decay = split.Decay == 0 ? sample.WavInfo.Decay : split.Decay; + //sustain = split.Sustain == 0 ? sample.WavInfo.Sustain : split.Sustain; + //hold = split.Hold == 0 ? sample.WavInfo.Hold : split.Hold; + //decay2 = split.Decay2 == 0 ? sample.WavInfo.Decay2 : split.Decay2; + //release = split.Release == 0 ? sample.WavInfo.Release : split.Release; + _attackVolume = split.AttackVolume == 0 ? _sample.WavInfo.AttackVolume == 0 ? (byte)0x7F : _sample.WavInfo.AttackVolume : split.AttackVolume; + _attack = split.Attack == 0 ? _sample.WavInfo.Attack == 0 ? (byte)0x7F : _sample.WavInfo.Attack : split.Attack; + _decay = split.Decay == 0 ? _sample.WavInfo.Decay == 0 ? (byte)0x7F : _sample.WavInfo.Decay : split.Decay; + _sustain = split.Sustain == 0 ? _sample.WavInfo.Sustain == 0 ? (byte)0x7F : _sample.WavInfo.Sustain : split.Sustain; + _hold = split.Hold == 0 ? _sample.WavInfo.Hold == 0 ? (byte)0x7F : _sample.WavInfo.Hold : split.Hold; + _decay2 = split.Decay2 == 0 ? _sample.WavInfo.Decay2 == 0 ? (byte)0x7F : _sample.WavInfo.Decay2 : split.Decay2; + _release = split.Release == 0 ? _sample.WavInfo.Release == 0 ? (byte)0x7F : _sample.WavInfo.Release : split.Release; + DetermineEnvelopeStartingPoint(); + _pos = 0; + _prevLeft = _prevRight = 0; + NoteLength = noteLength; + return true; + } + return false; + } + + public void Stop() + { + Owner?.Channels.Remove(this); + Owner = null; + Volume = 0; + } + + private bool CMDB1___sub_2074CA0() + { + bool b = true; + bool ge = _sample.WavInfo.EnvMult >= 0x7F; + bool ee = _sample.WavInfo.EnvMult == 0x7F; + if (_sample.WavInfo.EnvMult > 0x7F) + { + ge = _attackVolume >= 0x7F; + ee = _attackVolume == 0x7F; + } + if (!ee & ge + && _attack > 0x7F + && _decay > 0x7F + && _sustain > 0x7F + && _hold > 0x7F + && _decay2 > 0x7F + && _release > 0x7F) + { + b = false; + } + return b; + } + private void DetermineEnvelopeStartingPoint() + { + State = EnvelopeState.Two; // This isn't actually placed in this func + bool atLeastOneThingIsValid = CMDB1___sub_2074CA0(); // Neither is this + if (atLeastOneThingIsValid) + { + if (_attack != 0) + { + _velocity = _attackVolume << 23; + State = EnvelopeState.Hold; + UpdateEnvelopePlan(0x7F, _attack); + } + else + { + _velocity = 0x7F << 23; + if (_hold != 0) + { + UpdateEnvelopePlan(0x7F, _hold); + State = EnvelopeState.Decay; + } + else if (_decay != 0) + { + UpdateEnvelopePlan(_sustain, _decay); + State = EnvelopeState.Decay2; + } + else + { + UpdateEnvelopePlan(0, _release); + State = EnvelopeState.Six; + } + } + // Unk1E = 1 + } + else if (State != EnvelopeState.One) // What should it be? + { + State = EnvelopeState.Zero; + _velocity = 0x7F << 23; + } + } + public void SetEnvelopePhase7_2074ED8() + { + if (State != EnvelopeState.Zero) + { + UpdateEnvelopePlan(0, _release); + State = EnvelopeState.Seven; + } + } + public int StepEnvelope() + { + if (State > EnvelopeState.Two) + { + if (_envelopeTimeLeft != 0) + { + _envelopeTimeLeft--; + _velocity += _volumeIncrement; + if (_velocity < 0) + { + _velocity = 0; + } + else if (_velocity > 0x3FFFFFFF) + { + _velocity = 0x3FFFFFFF; + } + } + else + { + _velocity = _targetVolume << 23; + switch (State) + { + default: return _velocity >> 23; // case 8 + case EnvelopeState.Hold: + { + if (_hold == 0) + { + goto LABEL_6; + } + else + { + UpdateEnvelopePlan(0x7F, _hold); + State = EnvelopeState.Decay; + } + break; + } + case EnvelopeState.Decay: + LABEL_6: + { + if (_decay == 0) + { + _velocity = _sustain << 23; + goto LABEL_9; + } + else + { + UpdateEnvelopePlan(_sustain, _decay); + State = EnvelopeState.Decay2; + } + break; + } + case EnvelopeState.Decay2: + LABEL_9: + { + if (_decay2 == 0) + { + goto LABEL_11; + } + else + { + UpdateEnvelopePlan(0, _decay2); + State = EnvelopeState.Six; + } + break; + } + case EnvelopeState.Six: + LABEL_11: + { + UpdateEnvelopePlan(0, 0); + State = EnvelopeState.Two; + break; + } + case EnvelopeState.Seven: + { + State = EnvelopeState.Eight; + _velocity = 0; + _envelopeTimeLeft = 0; + break; + } + } + } + } + return _velocity >> 23; + } + private void UpdateEnvelopePlan(byte targetVolume, int envelopeParam) + { + if (envelopeParam == 0x7F) + { + _volumeIncrement = 0; + _envelopeTimeLeft = int.MaxValue; + } + else + { + _targetVolume = targetVolume; + _envelopeTimeLeft = _sample.WavInfo.EnvMult == 0 + ? DSEUtils.Duration32[envelopeParam] * 1_000 / 10_000 + : DSEUtils.Duration16[envelopeParam] * _sample.WavInfo.EnvMult * 1_000 / 10_000; + _volumeIncrement = _envelopeTimeLeft == 0 ? 0 : ((targetVolume << 23) - _velocity) / _envelopeTimeLeft; + } + } + + public void Process(out short left, out short right) + { + if (Timer == 0) + { + left = _prevLeft; + right = _prevRight; + return; + } + + int numSamples = (_pos + 0x100) / Timer; + _pos = (_pos + 0x100) % Timer; + // prevLeft and prevRight are stored because numSamples can be 0. + for (int i = 0; i < numSamples; i++) + { + short samp; + switch (_sample.WavInfo.SampleFormat) + { + case SampleFormat.PCM8: + { + // If hit end + if (_dataOffset >= _sample.Data.Length) + { + if (_sample.WavInfo.Loop) + { + _dataOffset = (int)(_sample.WavInfo.LoopStart * 4); + } + else + { + left = right = _prevLeft = _prevRight = 0; + Stop(); + return; + } + } + samp = (short)((sbyte)_sample.Data[_dataOffset++] << 8); + break; + } + case SampleFormat.PCM16: + { + // If hit end + if (_dataOffset >= _sample.Data.Length) + { + if (_sample.WavInfo.Loop) + { + _dataOffset = (int)(_sample.WavInfo.LoopStart * 4); + } + else + { + left = right = _prevLeft = _prevRight = 0; + Stop(); + return; + } + } + samp = (short)(_sample.Data[_dataOffset++] | (_sample.Data[_dataOffset++] << 8)); + break; + } + case SampleFormat.ADPCM: + { + // If just looped + if (_adpcmDecoder.DataOffset == _sample.WavInfo.LoopStart * 4 && !_adpcmDecoder.OnSecondNibble) + { + _adpcmLoopLastSample = _adpcmDecoder.LastSample; + _adpcmLoopStepIndex = _adpcmDecoder.StepIndex; + } + // If hit end + if (_adpcmDecoder.DataOffset >= _sample.Data.Length && !_adpcmDecoder.OnSecondNibble) + { + if (_sample.WavInfo.Loop) + { + _adpcmDecoder.DataOffset = (int)(_sample.WavInfo.LoopStart * 4); + _adpcmDecoder.StepIndex = _adpcmLoopStepIndex; + _adpcmDecoder.LastSample = _adpcmLoopLastSample; + _adpcmDecoder.OnSecondNibble = false; + } + else + { + left = right = _prevLeft = _prevRight = 0; + Stop(); + return; + } + } + samp = _adpcmDecoder.GetSample(); + break; + } + default: samp = 0; break; + } + samp = (short)(samp * Volume / 0x7F); + _prevLeft = (short)(samp * (-Panpot + 0x40) / 0x80); + _prevRight = (short)(samp * (Panpot + 0x40) / 0x80); + } + left = _prevLeft; + right = _prevRight; + } +} diff --git a/VG Music Studio - Core/NDS/DSE/DSECommands.cs b/VG Music Studio - Core/NDS/DSE/DSECommands.cs new file mode 100644 index 00000000..76e8e5bd --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSECommands.cs @@ -0,0 +1,130 @@ +using Kermalis.VGMusicStudio.Core.Util; +using System.Drawing; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +internal sealed class ExpressionCommand : ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Expression"; + public string Arguments => Expression.ToString(); + + public byte Expression { get; set; } +} +internal sealed class FinishCommand : ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Finish"; + public string Arguments => string.Empty; +} +internal sealed class InvalidCommand : ICommand +{ + public Color Color => Color.MediumVioletRed; + public string Label => $"Invalid 0x{Command:X}"; + public string Arguments => string.Empty; + + public byte Command { get; set; } +} +internal sealed class LoopStartCommand : ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Loop Start"; + public string Arguments => $"0x{Offset:X}"; + + public long Offset { get; set; } +} +internal sealed class NoteCommand : ICommand +{ + public Color Color => Color.SkyBlue; + public string Label => "Note"; + public string Arguments => $"{ConfigUtils.GetKeyName(Note)} {OctaveChange} {Velocity} {Duration}"; + + public byte Note { get; set; } + public sbyte OctaveChange { get; set; } + public byte Velocity { get; set; } + public uint Duration { get; set; } +} +internal sealed class OctaveAddCommand : ICommand +{ + public Color Color => Color.SkyBlue; + public string Label => "Add To Octave"; + public string Arguments => OctaveChange.ToString(); + + public sbyte OctaveChange { get; set; } +} +internal sealed class OctaveSetCommand : ICommand +{ + public Color Color => Color.SkyBlue; + public string Label => "Set Octave"; + public string Arguments => Octave.ToString(); + + public byte Octave { get; set; } +} +internal sealed class PanpotCommand : ICommand +{ + public Color Color => Color.GreenYellow; + public string Label => "Panpot"; + public string Arguments => Panpot.ToString(); + + public sbyte Panpot { get; set; } +} +internal sealed class PitchBendCommand : ICommand +{ + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend"; + public string Arguments => $"{(sbyte)Bend}, {(sbyte)(Bend >> 8)}"; + + public ushort Bend { get; set; } +} +internal sealed class RestCommand : ICommand +{ + public Color Color => Color.PaleVioletRed; + public string Label => "Rest"; + public string Arguments => Rest.ToString(); + + public uint Rest { get; set; } +} +internal sealed class SkipBytesCommand : ICommand +{ + public Color Color => Color.MediumVioletRed; + public string Label => $"Skip 0x{Command:X}"; + public string Arguments => string.Join(", ", SkippedBytes.Select(b => $"0x{b:X}")); + + public byte Command { get; set; } + public byte[] SkippedBytes { get; set; } = null!; +} +internal sealed class TempoCommand : ICommand +{ + public Color Color => Color.DeepSkyBlue; + public string Label => $"Tempo {Command - 0xA3}"; // The two possible tempo commands are 0xA4 and 0xA5 + public string Arguments => Tempo.ToString(); + + public byte Command { get; set; } + public byte Tempo { get; set; } +} +internal sealed class UnknownCommand : ICommand +{ + public Color Color => Color.MediumVioletRed; + public string Label => $"Unknown 0x{Command:X}"; + public string Arguments => string.Join(", ", Args.Select(b => $"0x{b:X}")); + + public byte Command { get; set; } + public byte[] Args { get; set; } = null!; +} +internal sealed class VoiceCommand : ICommand +{ + public Color Color => Color.DarkSalmon; + public string Label => "Voice"; + public string Arguments => Voice.ToString(); + + public byte Voice { get; set; } +} +internal sealed class VolumeCommand : ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Volume"; + public string Arguments => Volume.ToString(); + + public byte Volume { get; set; } +} diff --git a/VG Music Studio - Core/NDS/DSE/DSEConfig.cs b/VG Music Studio - Core/NDS/DSE/DSEConfig.cs new file mode 100644 index 00000000..6a68eed5 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSEConfig.cs @@ -0,0 +1,48 @@ +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Properties; +using System.Collections.Generic; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +public sealed class DSEConfig : Config +{ + public readonly string BGMPath; + public readonly string[] BGMFiles; + + internal DSEConfig(string bgmPath) + { + BGMPath = bgmPath; + BGMFiles = Directory.GetFiles(bgmPath, "bgm*.smd", SearchOption.TopDirectoryOnly); + if (BGMFiles.Length == 0) + { + throw new DSENoSequencesException(bgmPath); + } + + // TODO: Big endian files + var songs = new List(BGMFiles.Length); + for (int i = 0; i < BGMFiles.Length; i++) + { + using (FileStream stream = File.OpenRead(BGMFiles[i])) + { + var r = new EndianBinaryReader(stream, ascii: true); + SMD.Header header = r.ReadObject(); + char[] chars = header.Label.ToCharArray(); + EndianBinaryPrimitives.TrimNullTerminators(ref chars); + songs.Add(new Song(i, $"{Path.GetFileNameWithoutExtension(BGMFiles[i])} - {new string(chars)}")); + } + } + Playlists.Add(new Playlist(Strings.PlaylistMusic, songs)); + } + + public override string GetGameName() + { + return "DSE"; + } + public override string GetSongName(int index) + { + return index < 0 || index >= BGMFiles.Length + ? index.ToString() + : '\"' + BGMFiles[index] + '\"'; + } +} diff --git a/VG Music Studio - Core/NDS/DSE/DSEEngine.cs b/VG Music Studio - Core/NDS/DSE/DSEEngine.cs new file mode 100644 index 00000000..a7a933ed --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSEEngine.cs @@ -0,0 +1,26 @@ +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +public sealed class DSEEngine : Engine +{ + public static DSEEngine? DSEInstance { get; private set; } + + public override DSEConfig Config { get; } + public override DSEMixer Mixer { get; } + public override DSEPlayer Player { get; } + + public DSEEngine(string bgmPath) + { + Config = new DSEConfig(bgmPath); + Mixer = new DSEMixer(); + Player = new DSEPlayer(Config, Mixer); + + DSEInstance = this; + Instance = this; + } + + public override void Dispose() + { + base.Dispose(); + DSEInstance = null; + } +} diff --git a/VG Music Studio - Core/NDS/DSE/DSEEnums.cs b/VG Music Studio - Core/NDS/DSE/DSEEnums.cs new file mode 100644 index 00000000..911c5f0e --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSEEnums.cs @@ -0,0 +1,21 @@ +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +internal enum EnvelopeState : byte +{ + Zero = 0, + One = 1, + Two = 2, + Hold = 3, + Decay = 4, + Decay2 = 5, + Six = 6, + Seven = 7, + Eight = 8, +} + +internal enum SampleFormat : ushort +{ + PCM8 = 0x000, + PCM16 = 0x100, + ADPCM = 0x200, +} diff --git a/VG Music Studio - Core/NDS/DSE/DSEExceptions.cs b/VG Music Studio - Core/NDS/DSE/DSEExceptions.cs new file mode 100644 index 00000000..82c22e9c --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSEExceptions.cs @@ -0,0 +1,51 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +public sealed class DSENoSequencesException : Exception +{ + public string BGMPath { get; } + + internal DSENoSequencesException(string bgmPath) + { + BGMPath = bgmPath; + } +} + +public sealed class DSEInvalidHeaderVersionException : Exception +{ + public ushort Version { get; } + + internal DSEInvalidHeaderVersionException(ushort version) + { + Version = version; + } +} + +public sealed class DSEInvalidNoteException : Exception +{ + public byte TrackIndex { get; } + public int Offset { get; } + public int Note { get; } + + internal DSEInvalidNoteException(byte trackIndex, int offset, int note) + { + TrackIndex = trackIndex; + Offset = offset; + Note = note; + } +} + +public sealed class DSEInvalidCMDException : Exception +{ + public byte TrackIndex { get; } + public int CmdOffset { get; } + public byte Cmd { get; } + + internal DSEInvalidCMDException(byte trackIndex, int cmdOffset, byte cmd) + { + TrackIndex = trackIndex; + CmdOffset = cmdOffset; + Cmd = cmd; + } +} \ No newline at end of file diff --git a/VG Music Studio - Core/NDS/DSE/DSELoadedSong.cs b/VG Music Studio - Core/NDS/DSE/DSELoadedSong.cs new file mode 100644 index 00000000..2cee1061 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSELoadedSong.cs @@ -0,0 +1,62 @@ +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Util; +using System.Collections.Generic; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +internal sealed partial class DSELoadedSong : ILoadedSong +{ + public List[] Events { get; } + public long MaxTicks { get; private set; } + public int LongestTrack; + + private readonly DSEPlayer _player; + private readonly SWD LocalSWD; + private readonly byte[] SMDFile; + public readonly DSETrack[] Tracks; + + public DSELoadedSong(DSEPlayer player, string bgm) + { + _player = player; + + LocalSWD = new SWD(Path.ChangeExtension(bgm, "swd")); + SMDFile = File.ReadAllBytes(bgm); + using (var stream = new MemoryStream(SMDFile)) + { + var r = new EndianBinaryReader(stream, ascii: true); + SMD.Header header = r.ReadObject(); + SMD.ISongChunk songChunk; + switch (header.Version) + { + case 0x402: + { + songChunk = r.ReadObject(); + break; + } + case 0x415: + { + songChunk = r.ReadObject(); + break; + } + default: throw new DSEInvalidHeaderVersionException(header.Version); + } + + Tracks = new DSETrack[songChunk.NumTracks]; + Events = new List[songChunk.NumTracks]; + for (byte trackIndex = 0; trackIndex < songChunk.NumTracks; trackIndex++) + { + long chunkStart = r.Stream.Position; + r.Stream.Position += 0x14; // Skip header + Tracks[trackIndex] = new DSETrack(trackIndex, (int)r.Stream.Position); + + AddTrackEvents(trackIndex, r); + + r.Stream.Position = chunkStart + 0xC; + uint chunkLength = r.ReadUInt32(); + r.Stream.Position += chunkLength; + r.Stream.Align(16); + } + } + } +} diff --git a/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Events.cs b/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Events.cs new file mode 100644 index 00000000..b37dde91 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Events.cs @@ -0,0 +1,461 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +internal sealed partial class DSELoadedSong +{ + private void AddEvent(byte trackIndex, long cmdOffset, ICommand command) + { + Events[trackIndex].Add(new SongEvent(cmdOffset, command)); + } + private bool EventExists(byte trackIndex, long cmdOffset) + { + return Events[trackIndex].Exists(e => e.Offset == cmdOffset); + } + + private void AddTrackEvents(byte trackIndex, EndianBinaryReader r) + { + Events[trackIndex] = new List(); + + uint lastNoteDuration = 0; + uint lastRest = 0; + bool cont = true; + while (cont) + { + long cmdOffset = r.Stream.Position; + byte cmd = r.ReadByte(); + if (cmd <= 0x7F) + { + byte arg = r.ReadByte(); + int numParams = (arg & 0xC0) >> 6; + int oct = ((arg & 0x30) >> 4) - 2; + int n = arg & 0xF; + if (n >= 12) + { + throw new DSEInvalidNoteException(trackIndex, (int)cmdOffset, n); + } + + uint duration; + if (numParams == 0) + { + duration = lastNoteDuration; + } + else // Big Endian reading of 8, 16, or 24 bits + { + duration = 0; + for (int b = 0; b < numParams; b++) + { + duration = (duration << 8) | r.ReadByte(); + } + lastNoteDuration = duration; + } + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new NoteCommand { Note = (byte)n, OctaveChange = (sbyte)oct, Velocity = cmd, Duration = duration }); + } + } + else if (cmd >= 0x80 && cmd <= 0x8F) + { + lastRest = DSEUtils.FixedRests[cmd - 0x80]; + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new RestCommand { Rest = lastRest }); + } + } + else // 0x90-0xFF + { + // TODO: 0x95 - a rest that may or may not repeat depending on some condition within channels + // TODO: 0x9E - may or may not jump somewhere else depending on an unknown structure + switch (cmd) + { + case 0x90: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new RestCommand { Rest = lastRest }); + } + break; + } + case 0x91: + { + lastRest = (uint)(lastRest + r.ReadSByte()); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new RestCommand { Rest = lastRest }); + } + break; + } + case 0x92: + { + lastRest = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new RestCommand { Rest = lastRest }); + } + break; + } + case 0x93: + { + lastRest = r.ReadUInt16(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new RestCommand { Rest = lastRest }); + } + break; + } + case 0x94: + { + lastRest = (uint)(r.ReadByte() | (r.ReadByte() << 8) | (r.ReadByte() << 16)); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new RestCommand { Rest = lastRest }); + } + break; + } + case 0x96: + case 0x97: + case 0x9A: + case 0x9B: + case 0x9F: + case 0xA2: + case 0xA3: + case 0xA6: + case 0xA7: + case 0xAD: + case 0xAE: + case 0xB7: + case 0xB8: + case 0xB9: + case 0xBA: + case 0xBB: + case 0xBD: + case 0xC1: + case 0xC2: + case 0xC4: + case 0xC5: + case 0xC6: + case 0xC7: + case 0xC8: + case 0xC9: + case 0xCA: + case 0xCC: + case 0xCD: + case 0xCE: + case 0xCF: + case 0xD9: + case 0xDA: + case 0xDE: + case 0xE6: + case 0xEB: + case 0xEE: + case 0xF4: + case 0xF5: + case 0xF7: + case 0xF9: + case 0xFA: + case 0xFB: + case 0xFC: + case 0xFD: + case 0xFE: + case 0xFF: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0x98: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new FinishCommand()); + } + cont = false; + break; + } + case 0x99: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LoopStartCommand { Offset = r.Stream.Position }); + } + break; + } + case 0xA0: + { + byte octave = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new OctaveSetCommand { Octave = octave }); + } + break; + } + case 0xA1: + { + sbyte change = r.ReadSByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new OctaveAddCommand { OctaveChange = change }); + } + break; + } + case 0xA4: + case 0xA5: // The code for these two is identical + { + byte tempoArg = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new TempoCommand { Command = cmd, Tempo = tempoArg }); + } + break; + } + case 0xAB: + { + byte[] bytes = new byte[1]; + r.ReadBytes(bytes); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); + } + break; + } + case 0xAC: + { + byte voice = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VoiceCommand { Voice = voice }); + } + break; + } + case 0xCB: + case 0xF8: + { + byte[] bytes = new byte[2]; + r.ReadBytes(bytes); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); + } + break; + } + case 0xD7: + { + ushort bend = r.ReadUInt16(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PitchBendCommand { Bend = bend }); + } + break; + } + case 0xE0: + { + byte volume = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VolumeCommand { Volume = volume }); + } + break; + } + case 0xE3: + { + byte expression = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ExpressionCommand { Expression = expression }); + } + break; + } + case 0xE8: + { + byte panArg = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); + } + break; + } + case 0x9D: + case 0xB0: + case 0xC0: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new UnknownCommand { Command = cmd, Args = Array.Empty() }); + } + break; + } + case 0x9C: + case 0xA9: + case 0xAA: + case 0xB1: + case 0xB2: + case 0xB3: + case 0xB5: + case 0xB6: + case 0xBC: + case 0xBE: + case 0xBF: + case 0xC3: + case 0xD0: + case 0xD1: + case 0xD2: + case 0xDB: + case 0xDF: + case 0xE1: + case 0xE7: + case 0xE9: + case 0xEF: + case 0xF6: + { + byte[] args = new byte[1]; + r.ReadBytes(args); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new UnknownCommand { Command = cmd, Args = args }); + } + break; + } + case 0xA8: + case 0xB4: + case 0xD3: + case 0xD5: + case 0xD6: + case 0xD8: + case 0xF2: + { + byte[] args = new byte[2]; + r.ReadBytes(args); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new UnknownCommand { Command = cmd, Args = args }); + } + break; + } + case 0xAF: + case 0xD4: + case 0xE2: + case 0xEA: + case 0xF3: + { + byte[] args = new byte[3]; + r.ReadBytes(args); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new UnknownCommand { Command = cmd, Args = args }); + } + break; + } + case 0xDD: + case 0xE5: + case 0xED: + case 0xF1: + { + byte[] args = new byte[4]; + r.ReadBytes(args); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new UnknownCommand { Command = cmd, Args = args }); + } + break; + } + case 0xDC: + case 0xE4: + case 0xEC: + case 0xF0: + { + byte[] args = new byte[5]; + r.ReadBytes(args); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new UnknownCommand { Command = cmd, Args = args }); + } + break; + } + default: throw new DSEInvalidCMDException(trackIndex, (int)cmdOffset, cmd); + } + } + } + } + + public void SetTicks() + { + MaxTicks = 0; + for (int trackIndex = 0; trackIndex < Events.Length; trackIndex++) + { + List evs = Events[trackIndex]; + evs.Sort((e1, e2) => e1.Offset.CompareTo(e2.Offset)); + + DSETrack track = Tracks[trackIndex]; + track.Init(); + + long elapsedTicks = 0; + while (true) + { + SongEvent e = evs.Single(ev => ev.Offset == track.CurOffset); + if (e.Ticks.Count > 0) + { + break; + } + + e.Ticks.Add(elapsedTicks); + ExecuteNext(track); + if (track.Stopped) + { + break; + } + + elapsedTicks += track.Rest; + track.Rest = 0; + } + if (elapsedTicks > MaxTicks) + { + LongestTrack = trackIndex; + MaxTicks = elapsedTicks; + } + track.StopAllChannels(); + } + } + internal void SetCurTick(long ticks) + { + while (true) + { + if (_player.ElapsedTicks == ticks) + { + goto finish; + } + + while (_player.TempoStack >= 240) + { + _player.TempoStack -= 240; + for (int trackIndex = 0; trackIndex < Tracks.Length; trackIndex++) + { + DSETrack track = Tracks[trackIndex]; + if (!track.Stopped) + { + track.Tick(); + while (track.Rest == 0 && !track.Stopped) + { + ExecuteNext(track); + } + } + } + _player.ElapsedTicks++; + if (_player.ElapsedTicks == ticks) + { + goto finish; + } + } + _player.TempoStack += _player.Tempo; + } + finish: + for (int i = 0; i < Tracks.Length; i++) + { + Tracks[i].StopAllChannels(); + } + } +} diff --git a/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs b/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs new file mode 100644 index 00000000..90e30e4a --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs @@ -0,0 +1,285 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +internal sealed partial class DSELoadedSong +{ + public void UpdateSongState(SongState info) + { + for (int trackIndex = 0; trackIndex < Tracks.Length; trackIndex++) + { + Tracks[trackIndex].UpdateSongState(info.Tracks[trackIndex]); + } + } + + public void ExecuteNext(DSETrack track) + { + byte cmd = SMDFile[track.CurOffset++]; + if (cmd <= 0x7F) + { + byte arg = SMDFile[track.CurOffset++]; + int numParams = (arg & 0xC0) >> 6; + int oct = ((arg & 0x30) >> 4) - 2; + int n = arg & 0xF; + if (n >= 12) + { + throw new DSEInvalidNoteException(track.Index, track.CurOffset - 2, n); + } + + uint duration; + if (numParams == 0) + { + duration = track.LastNoteDuration; + } + else + { + duration = 0; + for (int b = 0; b < numParams; b++) + { + duration = (duration << 8) | SMDFile[track.CurOffset++]; + } + track.LastNoteDuration = duration; + } + DSEChannel channel = _player.DMixer.AllocateChannel() + ?? throw new Exception("Not enough channels"); + + channel.Stop(); + track.Octave = (byte)(track.Octave + oct); + if (channel.StartPCM(LocalSWD, _player.MasterSWD, track.Voice, n + (12 * track.Octave), duration)) + { + channel.NoteVelocity = cmd; + channel.Owner = track; + track.Channels.Add(channel); + } + } + else if (cmd is >= 0x80 and <= 0x8F) + { + track.LastRest = DSEUtils.FixedRests[cmd - 0x80]; + track.Rest = track.LastRest; + } + else // 0x90-0xFF + { + // TODO: 0x95, 0x9E + switch (cmd) + { + case 0x90: + { + track.Rest = track.LastRest; + break; + } + case 0x91: + { + track.LastRest = (uint)(track.LastRest + (sbyte)SMDFile[track.CurOffset++]); + track.Rest = track.LastRest; + break; + } + case 0x92: + { + track.LastRest = SMDFile[track.CurOffset++]; + track.Rest = track.LastRest; + break; + } + case 0x93: + { + track.LastRest = (uint)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.Rest = track.LastRest; + break; + } + case 0x94: + { + track.LastRest = (uint)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8) | (SMDFile[track.CurOffset++] << 16)); + track.Rest = track.LastRest; + break; + } + case 0x96: + case 0x97: + case 0x9A: + case 0x9B: + case 0x9F: + case 0xA2: + case 0xA3: + case 0xA6: + case 0xA7: + case 0xAD: + case 0xAE: + case 0xB7: + case 0xB8: + case 0xB9: + case 0xBA: + case 0xBB: + case 0xBD: + case 0xC1: + case 0xC2: + case 0xC4: + case 0xC5: + case 0xC6: + case 0xC7: + case 0xC8: + case 0xC9: + case 0xCA: + case 0xCC: + case 0xCD: + case 0xCE: + case 0xCF: + case 0xD9: + case 0xDA: + case 0xDE: + case 0xE6: + case 0xEB: + case 0xEE: + case 0xF4: + case 0xF5: + case 0xF7: + case 0xF9: + case 0xFA: + case 0xFB: + case 0xFC: + case 0xFD: + case 0xFE: + case 0xFF: + { + track.Stopped = true; + break; + } + case 0x98: + { + if (track.LoopOffset == -1) + { + track.Stopped = true; + } + else + { + track.CurOffset = track.LoopOffset; + } + break; + } + case 0x99: + { + track.LoopOffset = track.CurOffset; + break; + } + case 0xA0: + { + track.Octave = SMDFile[track.CurOffset++]; + break; + } + case 0xA1: + { + track.Octave = (byte)(track.Octave + (sbyte)SMDFile[track.CurOffset++]); + break; + } + case 0xA4: + case 0xA5: + { + _player.Tempo = SMDFile[track.CurOffset++]; + break; + } + case 0xAB: + { + track.CurOffset++; + break; + } + case 0xAC: + { + track.Voice = SMDFile[track.CurOffset++]; + break; + } + case 0xCB: + case 0xF8: + { + track.CurOffset += 2; + break; + } + case 0xD7: + { + track.PitchBend = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + break; + } + case 0xE0: + { + track.Volume = SMDFile[track.CurOffset++]; + break; + } + case 0xE3: + { + track.Expression = SMDFile[track.CurOffset++]; + break; + } + case 0xE8: + { + track.Panpot = (sbyte)(SMDFile[track.CurOffset++] - 0x40); + break; + } + case 0x9D: + case 0xB0: + case 0xC0: + { + break; + } + case 0x9C: + case 0xA9: + case 0xAA: + case 0xB1: + case 0xB2: + case 0xB3: + case 0xB5: + case 0xB6: + case 0xBC: + case 0xBE: + case 0xBF: + case 0xC3: + case 0xD0: + case 0xD1: + case 0xD2: + case 0xDB: + case 0xDF: + case 0xE1: + case 0xE7: + case 0xE9: + case 0xEF: + case 0xF6: + { + track.CurOffset++; + break; + } + case 0xA8: + case 0xB4: + case 0xD3: + case 0xD5: + case 0xD6: + case 0xD8: + case 0xF2: + { + track.CurOffset += 2; + break; + } + case 0xAF: + case 0xD4: + case 0xE2: + case 0xEA: + case 0xF3: + { + track.CurOffset += 3; + break; + } + case 0xDD: + case 0xE5: + case 0xED: + case 0xF1: + { + track.CurOffset += 4; + break; + } + case 0xDC: + case 0xE4: + case 0xEC: + case 0xF0: + { + track.CurOffset += 5; + break; + } + default: throw new DSEInvalidCMDException(track.Index, track.CurOffset - 1, cmd); + } + } + } +} diff --git a/VG Music Studio - Core/NDS/DSE/DSEMixer.cs b/VG Music Studio - Core/NDS/DSE/DSEMixer.cs new file mode 100644 index 00000000..89f6c529 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSEMixer.cs @@ -0,0 +1,214 @@ +using Kermalis.VGMusicStudio.Core.NDS.SDAT; +using Kermalis.VGMusicStudio.Core.Util; +using NAudio.Wave; +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +public sealed class DSEMixer : Mixer +{ + private const int NUM_CHANNELS = 0x20; // Actual value unknown for now + + private readonly float _samplesReciprocal; + private readonly int _samplesPerBuffer; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; + + private readonly DSEChannel[] _channels; + private readonly BufferedWaveProvider _buffer; + + protected override WaveFormat WaveFormat => _buffer.WaveFormat; + + public DSEMixer() + { + // The sampling frequency of the mixer is 1.04876 MHz with an amplitude resolution of 24 bits, but the sampling frequency after mixing with PWM modulation is 32.768 kHz with an amplitude resolution of 10 bits. + // - gbatek + // I'm not using either of those because the samples per buffer leads to an overflow eventually + const int sampleRate = 65_456; + _samplesPerBuffer = 341; // TODO + _samplesReciprocal = 1f / _samplesPerBuffer; + + _channels = new DSEChannel[NUM_CHANNELS]; + for (byte i = 0; i < NUM_CHANNELS; i++) + { + _channels[i] = new DSEChannel(i); + } + + _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + { + DiscardOnBufferOverflow = true, + BufferLength = _samplesPerBuffer * 64, + }; + Init(_buffer); + } + + internal DSEChannel? AllocateChannel() + { + static int GetScore(DSEChannel c) + { + // Free channels should be used before releasing channels + return c.Owner is null ? -2 : DSEUtils.IsStateRemovable(c.State) ? -1 : 0; + } + DSEChannel? nChan = null; + for (int i = 0; i < NUM_CHANNELS; i++) + { + DSEChannel c = _channels[i]; + if (nChan is null) + { + nChan = c; + } + else + { + int nScore = GetScore(nChan); + int cScore = GetScore(c); + if (cScore <= nScore && (cScore < nScore || c.Volume <= nChan.Volume)) + { + nChan = c; + } + } + } + return nChan is not null && 0 >= GetScore(nChan) ? nChan : null; + } + + internal void ChannelTick() + { + for (int i = 0; i < NUM_CHANNELS; i++) + { + DSEChannel chan = _channels[i]; + if (chan.Owner is null) + { + continue; + } + + chan.Volume = (byte)chan.StepEnvelope(); + if (chan.NoteLength == 0 && !DSEUtils.IsStateRemovable(chan.State)) + { + chan.SetEnvelopePhase7_2074ED8(); + } + int vol = SDATUtils.SustainTable[chan.NoteVelocity] + SDATUtils.SustainTable[chan.Volume] + SDATUtils.SustainTable[chan.Owner.Volume] + SDATUtils.SustainTable[chan.Owner.Expression]; + //int pitch = ((chan.Key - chan.BaseKey) << 6) + chan.SweepMain() + chan.Owner.GetPitch(); // "<< 6" is "* 0x40" + int pitch = (chan.Key - chan.RootKey) << 6; // "<< 6" is "* 0x40" + if (DSEUtils.IsStateRemovable(chan.State) && vol <= -92544) + { + chan.Stop(); + } + else + { + chan.Volume = SDATUtils.GetChannelVolume(vol); + chan.Panpot = chan.Owner.Panpot; + chan.Timer = SDATUtils.GetChannelTimer(chan.BaseTimer, pitch); + } + } + } + + internal void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal bool IsFading() + { + return _isFading; + } + internal bool IsFadeDone() + { + return _isFading && _fadeMicroFramesLeft == 0; + } + internal void ResetFade() + { + _isFading = false; + _fadeMicroFramesLeft = 0; + } + + private readonly byte[] _b = new byte[4]; + internal void Process(bool output, bool recording) + { + float masterStep; + float masterLevel; + if (_isFading && _fadeMicroFramesLeft == 0) + { + masterStep = 0; + masterLevel = 0; + } + else + { + float fromMaster = 1f; + float toMaster = 1f; + if (_fadeMicroFramesLeft > 0) + { + const float scale = 10f / 6f; + fromMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadePos += _fadeStepPerMicroframe; + toMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadeMicroFramesLeft--; + } + masterStep = (toMaster - fromMaster) * _samplesReciprocal; + masterLevel = fromMaster; + } + for (int i = 0; i < _samplesPerBuffer; i++) + { + int left = 0, + right = 0; + for (int j = 0; j < NUM_CHANNELS; j++) + { + DSEChannel chan = _channels[j]; + if (chan.Owner is null) + { + continue; + } + + bool muted = Mutes[chan.Owner.Index]; // Get mute first because chan.Process() can call chan.Stop() which sets chan.Owner to null + chan.Process(out short channelLeft, out short channelRight); + if (!muted) + { + left += channelLeft; + right += channelRight; + } + } + float f = left * masterLevel; + if (f < short.MinValue) + { + f = short.MinValue; + } + else if (f > short.MaxValue) + { + f = short.MaxValue; + } + left = (int)f; + _b[0] = (byte)left; + _b[1] = (byte)(left >> 8); + f = right * masterLevel; + if (f < short.MinValue) + { + f = short.MinValue; + } + else if (f > short.MaxValue) + { + f = short.MaxValue; + } + right = (int)f; + _b[2] = (byte)right; + _b[3] = (byte)(right >> 8); + masterLevel += masterStep; + if (output) + { + _buffer.AddSamples(_b, 0, 4); + } + if (recording) + { + _waveWriter!.Write(_b, 0, 4); + } + } + } +} diff --git a/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs b/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs new file mode 100644 index 00000000..bfdcda23 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs @@ -0,0 +1,135 @@ +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +public sealed class DSEPlayer : Player +{ + protected override string Name => "DSE Player"; + + private readonly DSEConfig _config; + internal readonly DSEMixer DMixer; + internal readonly SWD MasterSWD; + private DSELoadedSong? _loadedSong; + + internal byte Tempo; + internal int TempoStack; + private long _elapsedLoops; + + public override ILoadedSong? LoadedSong => _loadedSong; + protected override Mixer Mixer => DMixer; + + public DSEPlayer(DSEConfig config, DSEMixer mixer) + : base(192) + { + DMixer = mixer; + _config = config; + + MasterSWD = new SWD(Path.Combine(config.BGMPath, "bgm.swd")); + } + + public override void LoadSong(int index) + { + if (_loadedSong is not null) + { + _loadedSong = null; + } + + // If there's an exception, this will remain null + _loadedSong = new DSELoadedSong(this, _config.BGMFiles[index]); + _loadedSong.SetTicks(); + } + public override void UpdateSongState(SongState info) + { + info.Tempo = Tempo; + _loadedSong!.UpdateSongState(info); + } + internal override void InitEmulation() + { + Tempo = 120; + TempoStack = 0; + _elapsedLoops = 0; + ElapsedTicks = 0; + DMixer.ResetFade(); + DSETrack[] tracks = _loadedSong!.Tracks; + for (int i = 0; i < tracks.Length; i++) + { + tracks[i].Init(); + } + } + protected override void SetCurTick(long ticks) + { + _loadedSong!.SetCurTick(ticks); + } + protected override void OnStopped() + { + DSETrack[] tracks = _loadedSong!.Tracks; + for (int i = 0; i < tracks.Length; i++) + { + tracks[i].StopAllChannels(); + } + } + + protected override bool Tick(bool playing, bool recording) + { + DSELoadedSong s = _loadedSong!; + + bool allDone = false; + while (!allDone && TempoStack >= 240) + { + TempoStack -= 240; + allDone = true; + for (int i = 0; i < s.Tracks.Length; i++) + { + TickTrack(s, s.Tracks[i], ref allDone); + } + if (DMixer.IsFadeDone()) + { + allDone = true; + } + } + if (!allDone) + { + TempoStack += Tempo; + } + DMixer.ChannelTick(); + DMixer.Process(playing, recording); + return allDone; + } + private void TickTrack(DSELoadedSong s, DSETrack track, ref bool allDone) + { + track.Tick(); + while (track.Rest == 0 && !track.Stopped) + { + s.ExecuteNext(track); + } + if (track.Index == s.LongestTrack) + { + HandleTicksAndLoop(s, track); + } + if (!track.Stopped || track.Channels.Count != 0) + { + allDone = false; + } + } + private void HandleTicksAndLoop(DSELoadedSong s, DSETrack track) + { + if (ElapsedTicks != s.MaxTicks) + { + ElapsedTicks++; + return; + } + + // Track reached the detected end, update loops/ticks accordingly + if (track.Stopped) + { + return; + } + + _elapsedLoops++; + UpdateElapsedTicksAfterLoop(s.Events[track.Index], track.CurOffset, track.Rest); + if (ShouldFadeOut && _elapsedLoops > NumLoops && !DMixer.IsFading()) + { + DMixer.BeginFadeOut(); + } + } +} diff --git a/VG Music Studio - Core/NDS/DSE/DSETrack.cs b/VG Music Studio - Core/NDS/DSE/DSETrack.cs new file mode 100644 index 00000000..a15e380a --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSETrack.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +internal sealed class DSETrack +{ + public readonly byte Index; + private readonly int _startOffset; + public byte Octave; + public byte Voice; + public byte Expression; + public byte Volume; + public sbyte Panpot; + public uint Rest; + public ushort PitchBend; + public int CurOffset; + public int LoopOffset; + public bool Stopped; + public uint LastNoteDuration; + public uint LastRest; + + public readonly List Channels = new(0x10); + + public DSETrack(byte i, int startOffset) + { + Index = i; + _startOffset = startOffset; + } + + public void Init() + { + Expression = 0; + Voice = 0; + Volume = 0; + Octave = 4; + Panpot = 0; + Rest = 0; + PitchBend = 0; + CurOffset = _startOffset; + LoopOffset = -1; + Stopped = false; + LastNoteDuration = 0; + LastRest = 0; + StopAllChannels(); + } + + public void Tick() + { + if (Rest > 0) + { + Rest--; + } + for (int i = 0; i < Channels.Count; i++) + { + DSEChannel c = Channels[i]; + if (c.NoteLength > 0) + { + c.NoteLength--; + } + } + } + + public void StopAllChannels() + { + DSEChannel[] chans = Channels.ToArray(); + for (int i = 0; i < chans.Length; i++) + { + chans[i].Stop(); + } + } + + public void UpdateSongState(SongState.Track tin) + { + tin.Position = CurOffset; + tin.Rest = Rest; + tin.Voice = Voice; + tin.Type = "PCM"; + tin.Volume = Volume; + tin.PitchBend = PitchBend; + tin.Extra = Octave; + tin.Panpot = Panpot; + + DSEChannel[] channels = Channels.ToArray(); + if (channels.Length == 0) + { + tin.Keys[0] = byte.MaxValue; + tin.LeftVolume = 0f; + tin.RightVolume = 0f; + //tin.Type = string.Empty; + } + else + { + int numKeys = 0; + float left = 0f; + float right = 0f; + for (int j = 0; j < channels.Length; j++) + { + DSEChannel c = channels[j]; + if (!DSEUtils.IsStateRemovable(c.State)) + { + tin.Keys[numKeys++] = c.Key; + } + float a = (float)(-c.Panpot + 0x40) / 0x80 * c.Volume / 0x7F; + if (a > left) + { + left = a; + } + a = (float)(c.Panpot + 0x40) / 0x80 * c.Volume / 0x7F; + if (a > right) + { + right = a; + } + } + tin.Keys[numKeys] = byte.MaxValue; // There's no way for numKeys to be after the last index in the array + tin.LeftVolume = left; + tin.RightVolume = right; + //tin.Type = string.Join(", ", channels.Select(c => c.State.ToString())); + } + } +} diff --git a/VG Music Studio - Core/NDS/DSE/DSEUtils.cs b/VG Music Studio - Core/NDS/DSE/DSEUtils.cs new file mode 100644 index 00000000..8264b315 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSEUtils.cs @@ -0,0 +1,54 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +internal static class DSEUtils +{ + public static ReadOnlySpan Duration16 => new short[128] + { + 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, + 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, + 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, + 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F, + 0x0020, 0x0023, 0x0028, 0x002D, 0x0033, 0x0039, 0x0040, 0x0048, + 0x0050, 0x0058, 0x0062, 0x006D, 0x0078, 0x0083, 0x0090, 0x009E, + 0x00AC, 0x00BC, 0x00CC, 0x00DE, 0x00F0, 0x0104, 0x0119, 0x012F, + 0x0147, 0x0160, 0x017A, 0x0196, 0x01B3, 0x01D2, 0x01F2, 0x0214, + 0x0238, 0x025E, 0x0285, 0x02AE, 0x02D9, 0x0307, 0x0336, 0x0367, + 0x039B, 0x03D1, 0x0406, 0x0442, 0x047E, 0x04C4, 0x0500, 0x0546, + 0x058C, 0x0622, 0x0672, 0x06CC, 0x071C, 0x0776, 0x07DA, 0x0834, + 0x0898, 0x0906, 0x096A, 0x09D8, 0x0A50, 0x0ABE, 0x0B40, 0x0BB8, + 0x0C3A, 0x0CBC, 0x0D48, 0x0DDE, 0x0E6A, 0x0F00, 0x0FA0, 0x1040, + 0x10EA, 0x1194, 0x123E, 0x12F2, 0x13B0, 0x146E, 0x1536, 0x15FE, + 0x16D0, 0x17A2, 0x187E, 0x195A, 0x1A40, 0x1B30, 0x1C20, 0x1D1A, + 0x1E1E, 0x1F22, 0x2030, 0x2148, 0x2260, 0x2382, 0x2710, 0x7FFF, + }; + public static ReadOnlySpan Duration32 => new int[128] + { + 0x00000000, 0x00000004, 0x00000007, 0x0000000A, 0x0000000F, 0x00000015, 0x0000001C, 0x00000024, + 0x0000002E, 0x0000003A, 0x00000048, 0x00000057, 0x00000068, 0x0000007B, 0x00000091, 0x000000A8, + 0x00000185, 0x000001BE, 0x000001FC, 0x0000023F, 0x00000288, 0x000002D6, 0x0000032A, 0x00000385, + 0x000003E5, 0x0000044C, 0x000004BA, 0x0000052E, 0x000005A9, 0x0000062C, 0x000006B5, 0x00000746, + 0x00000BCF, 0x00000CC0, 0x00000DBD, 0x00000EC6, 0x00000FDC, 0x000010FF, 0x0000122F, 0x0000136C, + 0x000014B6, 0x0000160F, 0x00001775, 0x000018EA, 0x00001A6D, 0x00001BFF, 0x00001DA0, 0x00001F51, + 0x00002C16, 0x00002E80, 0x00003100, 0x00003395, 0x00003641, 0x00003902, 0x00003BDB, 0x00003ECA, + 0x000041D0, 0x000044EE, 0x00004824, 0x00004B73, 0x00004ED9, 0x00005259, 0x000055F2, 0x000059A4, + 0x000074CC, 0x000079AB, 0x00007EAC, 0x000083CE, 0x00008911, 0x00008E77, 0x000093FF, 0x000099AA, + 0x00009F78, 0x0000A56A, 0x0000AB80, 0x0000B1BB, 0x0000B81A, 0x0000BE9E, 0x0000C547, 0x0000CC17, + 0x0000FD42, 0x000105CB, 0x00010E82, 0x00011768, 0x0001207E, 0x000129C4, 0x0001333B, 0x00013CE2, + 0x000146BB, 0x000150C5, 0x00015B02, 0x00016572, 0x00017015, 0x00017AEB, 0x000185F5, 0x00019133, + 0x0001E16D, 0x0001EF07, 0x0001FCE0, 0x00020AF7, 0x0002194F, 0x000227E6, 0x000236BE, 0x000245D7, + 0x00025532, 0x000264CF, 0x000274AE, 0x000284D0, 0x00029536, 0x0002A5E0, 0x0002B6CE, 0x0002C802, + 0x000341B0, 0x000355F8, 0x00036A90, 0x00037F79, 0x000394B4, 0x0003AA41, 0x0003C021, 0x0003D654, + 0x0003ECDA, 0x000403B5, 0x00041AE5, 0x0004326A, 0x00044A45, 0x00046277, 0x00047B00, 0x7FFFFFFF, + }; + public static ReadOnlySpan FixedRests => new byte[0x10] + { + 96, 72, 64, 48, 36, 32, 24, 18, 16, 12, 9, 8, 6, 4, 3, 2, + }; + + public static bool IsStateRemovable(EnvelopeState state) + { + return state is EnvelopeState.Two or >= EnvelopeState.Seven; + } +} diff --git a/VG Music Studio - Core/NDS/DSE/SMD.cs b/VG Music Studio - Core/NDS/DSE/SMD.cs new file mode 100644 index 00000000..e9a90839 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/SMD.cs @@ -0,0 +1,60 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +internal sealed class SMD +{ + public sealed class Header // Size 0x40 + { + [BinaryStringFixedLength(4)] + public string Type { get; set; } = null!; // "smdb" or "smdl" + [BinaryArrayFixedLength(4)] + public byte[] Unknown1 { get; set; } = null!; + public uint Length { get; set; } + public ushort Version { get; set; } + [BinaryArrayFixedLength(10)] + public byte[] Unknown2 { get; set; } = null!; + public ushort Year { get; set; } + public byte Month { get; set; } + public byte Day { get; set; } + public byte Hour { get; set; } + public byte Minute { get; set; } + public byte Second { get; set; } + public byte Centisecond { get; set; } + [BinaryStringFixedLength(16)] + public string Label { get; set; } = null!; + [BinaryArrayFixedLength(16)] + public byte[] Unknown3 { get; set; } = null!; + } + + public interface ISongChunk + { + byte NumTracks { get; } + } + public sealed class SongChunk_V402 : ISongChunk // Size 0x20 + { + [BinaryStringFixedLength(4)] + public string Type { get; set; } = null!; + [BinaryArrayFixedLength(16)] + public byte[] Unknown1 { get; set; } = null!; + public byte NumTracks { get; set; } + public byte NumChannels { get; set; } + [BinaryArrayFixedLength(4)] + public byte[] Unknown2 { get; set; } = null!; + public sbyte MasterVolume { get; set; } + public sbyte MasterPanpot { get; set; } + [BinaryArrayFixedLength(4)] + public byte[] Unknown3 { get; set; } = null!; + } + public sealed class SongChunk_V415 : ISongChunk // Size 0x40 + { + [BinaryStringFixedLength(4)] + public string Type { get; set; } = null!; + [BinaryArrayFixedLength(18)] + public byte[] Unknown1 { get; set; } = null!; + public byte NumTracks { get; set; } + public byte NumChannels { get; set; } + [BinaryArrayFixedLength(40)] + public byte[] Unknown2 { get; set; } = null!; + } +} diff --git a/VG Music Studio - Core/NDS/DSE/SWD.cs b/VG Music Studio - Core/NDS/DSE/SWD.cs new file mode 100644 index 00000000..90c28ad9 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/SWD.cs @@ -0,0 +1,493 @@ +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Diagnostics; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +internal sealed class SWD +{ + public interface IHeader + { + // + } + private sealed class Header_V402 : IHeader // Size 0x40 + { + [BinaryArrayFixedLength(8)] + public byte[] Unknown1 { get; set; } = null!; + public ushort Year { get; set; } + public byte Month { get; set; } + public byte Day { get; set; } + public byte Hour { get; set; } + public byte Minute { get; set; } + public byte Second { get; set; } + public byte Centisecond { get; set; } + [BinaryStringFixedLength(16)] + public string Label { get; set; } = null!; + [BinaryArrayFixedLength(22)] + public byte[] Unknown2 { get; set; } = null!; + public byte NumWAVISlots { get; set; } + public byte NumPRGISlots { get; set; } + public byte NumKeyGroups { get; set; } + [BinaryArrayFixedLength(7)] + public byte[] Padding { get; set; } = null!; + } + private sealed class Header_V415 : IHeader // Size 0x40 + { + [BinaryArrayFixedLength(8)] + public byte[] Unknown1 { get; set; } = null!; + public ushort Year { get; set; } + public byte Month { get; set; } + public byte Day { get; set; } + public byte Hour { get; set; } + public byte Minute { get; set; } + public byte Second { get; set; } + public byte Centisecond { get; set; } + [BinaryStringFixedLength(16)] + public string Label { get; set; } = null!; + [BinaryArrayFixedLength(16)] + public byte[] Unknown2 { get; set; } = null!; + public uint PCMDLength { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown3 { get; set; } = null!; + public ushort NumWAVISlots { get; set; } + public ushort NumPRGISlots { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown4 { get; set; } = null!; + public uint WAVILength { get; set; } + } + + public interface ISplitEntry + { + byte LowKey { get; } + byte HighKey { get; } + int SampleId { get; } + byte SampleRootKey { get; } + sbyte SampleTranspose { get; } + byte AttackVolume { get; set; } + byte Attack { get; set; } + byte Decay { get; set; } + byte Sustain { get; set; } + byte Hold { get; set; } + byte Decay2 { get; set; } + byte Release { get; set; } + } + public sealed class SplitEntry_V402 : ISplitEntry // Size 0x30 + { + public ushort Id { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown1 { get; set; } = null!; + public byte LowKey { get; set; } + public byte HighKey { get; set; } + public byte LowKey2 { get; set; } + public byte HighKey2 { get; set; } + public byte LowVelocity { get; set; } + public byte HighVelocity { get; set; } + public byte LowVelocity2 { get; set; } + public byte HighVelocity2 { get; set; } + [BinaryArrayFixedLength(5)] + public byte[] Unknown2 { get; set; } = null!; + public byte SampleId { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown3 { get; set; } = null!; + public byte SampleRootKey { get; set; } + public sbyte SampleTranspose { get; set; } + public byte SampleVolume { get; set; } + public sbyte SamplePanpot { get; set; } + public byte KeyGroupId { get; set; } + [BinaryArrayFixedLength(15)] + public byte[] Unknown4 { get; set; } = null!; + public byte AttackVolume { get; set; } + public byte Attack { get; set; } + public byte Decay { get; set; } + public byte Sustain { get; set; } + public byte Hold { get; set; } + public byte Decay2 { get; set; } + public byte Release { get; set; } + public byte Unknown5 { get; set; } + + [BinaryIgnore] + int ISplitEntry.SampleId => SampleId; + } + public sealed class SplitEntry_V415 : ISplitEntry // 0x30 + { + public ushort Id { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown1 { get; set; } = null!; + public byte LowKey { get; set; } + public byte HighKey { get; set; } + public byte LowKey2 { get; set; } + public byte HighKey2 { get; set; } + public byte LowVelocity { get; set; } + public byte HighVelocity { get; set; } + public byte LowVelocity2 { get; set; } + public byte HighVelocity2 { get; set; } + [BinaryArrayFixedLength(6)] + public byte[] Unknown2 { get; set; } = null!; + public ushort SampleId { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown3 { get; set; } = null!; + public byte SampleRootKey { get; set; } + public sbyte SampleTranspose { get; set; } + public byte SampleVolume { get; set; } + public sbyte SamplePanpot { get; set; } + public byte KeyGroupId { get; set; } + [BinaryArrayFixedLength(13)] + public byte[] Unknown4 { get; set; } = null!; + public byte AttackVolume { get; set; } + public byte Attack { get; set; } + public byte Decay { get; set; } + public byte Sustain { get; set; } + public byte Hold { get; set; } + public byte Decay2 { get; set; } + public byte Release { get; set; } + public byte Unknown5 { get; set; } + + [BinaryIgnore] + int ISplitEntry.SampleId => SampleId; + } + + public interface IProgramInfo + { + ISplitEntry[] SplitEntries { get; } + } + public sealed class ProgramInfo_V402 : IProgramInfo + { + public byte Id { get; set; } + public byte NumSplits { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown1 { get; set; } = null!; + public byte Volume { get; set; } + public byte Panpot { get; set; } + [BinaryArrayFixedLength(5)] + public byte[] Unknown2 { get; set; } = null!; + public byte NumLFOs { get; set; } + [BinaryArrayFixedLength(4)] + public byte[] Unknown3 { get; set; } = null!; + [BinaryArrayFixedLength(16)] + public KeyGroup[] KeyGroups { get; set; } = null!; + [BinaryArrayVariableLength(nameof(NumLFOs))] + public LFOInfo LFOInfos { get; set; } = null!; + [BinaryArrayVariableLength(nameof(NumSplits))] + public SplitEntry_V402[] SplitEntries { get; set; } = null!; + + [BinaryIgnore] + ISplitEntry[] IProgramInfo.SplitEntries => SplitEntries; + } + public sealed class ProgramInfo_V415 : IProgramInfo + { + public ushort Id { get; set; } + public ushort NumSplits { get; set; } + public byte Volume { get; set; } + public byte Panpot { get; set; } + [BinaryArrayFixedLength(5)] + public byte[] Unknown1 { get; set; } = null!; + public byte NumLFOs { get; set; } + [BinaryArrayFixedLength(4)] + public byte[] Unknown2 { get; set; } = null!; + [BinaryArrayVariableLength(nameof(NumLFOs))] + public LFOInfo[] LFOInfos { get; set; } = null!; + [BinaryArrayFixedLength(16)] + public byte[] Unknown3 { get; set; } = null!; + [BinaryArrayVariableLength(nameof(NumSplits))] + public SplitEntry_V415[] SplitEntries { get; set; } = null!; + + [BinaryIgnore] + ISplitEntry[] IProgramInfo.SplitEntries => SplitEntries; + } + + public interface IWavInfo + { + byte RootNote { get; } + sbyte Transpose { get; } + SampleFormat SampleFormat { get; } + bool Loop { get; } + uint SampleRate { get; } + uint SampleOffset { get; } + uint LoopStart { get; } + uint LoopEnd { get; } + byte EnvMult { get; } + byte AttackVolume { get; } + byte Attack { get; } + byte Decay { get; } + byte Sustain { get; } + byte Hold { get; } + byte Decay2 { get; } + byte Release { get; } + } + public sealed class WavInfo_V402 : IWavInfo // Size 0x40 + { + public byte Unknown1 { get; set; } + public byte Id { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown2 { get; set; } = null!; + public byte RootNote { get; set; } + public sbyte Transpose { get; set; } + public byte Volume { get; set; } + public sbyte Panpot { get; set; } + public SampleFormat SampleFormat { get; set; } + [BinaryArrayFixedLength(7)] + public byte[] Unknown3 { get; set; } = null!; + public bool Loop { get; set; } + public uint SampleRate { get; set; } + public uint SampleOffset { get; set; } + public uint LoopStart { get; set; } + public uint LoopEnd { get; set; } + [BinaryArrayFixedLength(16)] + public byte[] Unknown4 { get; set; } = null!; + public byte EnvOn { get; set; } + public byte EnvMult { get; set; } + [BinaryArrayFixedLength(6)] + public byte[] Unknown5 { get; set; } = null!; + public byte AttackVolume { get; set; } + public byte Attack { get; set; } + public byte Decay { get; set; } + public byte Sustain { get; set; } + public byte Hold { get; set; } + public byte Decay2 { get; set; } + public byte Release { get; set; } + public byte Unknown6 { get; set; } + } + public sealed class WavInfo_V415 : IWavInfo // 0x40 + { + [BinaryArrayFixedLength(2)] + public byte[] Unknown1 { get; set; } = null!; + public ushort Id { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown2 { get; set; } = null!; + public byte RootNote { get; set; } + public sbyte Transpose { get; set; } + public byte Volume { get; set; } + public sbyte Panpot { get; set; } + [BinaryArrayFixedLength(6)] + public byte[] Unknown3 { get; set; } = null!; + public ushort Version { get; set; } + public SampleFormat SampleFormat { get; set; } + public byte Unknown4 { get; set; } + public bool Loop { get; set; } + public byte Unknown5 { get; set; } + public byte SamplesPer32Bits { get; set; } + public byte Unknown6 { get; set; } + public byte BitDepth { get; set; } + [BinaryArrayFixedLength(6)] + public byte[] Unknown7 { get; set; } = null!; + public uint SampleRate { get; set; } + public uint SampleOffset { get; set; } + public uint LoopStart { get; set; } + public uint LoopEnd { get; set; } + public byte EnvOn { get; set; } + public byte EnvMult { get; set; } + [BinaryArrayFixedLength(6)] + public byte[] Unknown8 { get; set; } = null!; + public byte AttackVolume { get; set; } + public byte Attack { get; set; } + public byte Decay { get; set; } + public byte Sustain { get; set; } + public byte Hold { get; set; } + public byte Decay2 { get; set; } + public byte Release { get; set; } + public byte Unknown9 { get; set; } + } + + public class SampleBlock + { + public IWavInfo WavInfo = null!; + public byte[] Data = null!; + } + public class ProgramBank + { + public IProgramInfo?[] ProgramInfos = null!; + public KeyGroup[] KeyGroups = null!; + } + public class KeyGroup // Size 0x8 + { + public ushort Id { get; set; } + public byte Poly { get; set; } + public byte Priority { get; set; } + public byte LowNote { get; set; } + public byte HighNote { get; set; } + public ushort Unknown { get; set; } + } + public sealed class LFOInfo + { + public byte Unknown1 { get; set; } + public byte HasData { get; set; } + public byte Type { get; set; } // LFOType enum + public byte CallbackType { get; set; } + public uint Unknown4 { get; set; } + public ushort Unknown8 { get; set; } + public ushort UnknownA { get; set; } + public ushort UnknownC { get; set; } + public byte UnknownE { get; set; } + public byte UnknownF { get; set; } + } + + public string Type; // "swdb" or "swdl" + public byte[] Unknown1; + public uint Length; + public ushort Version; + public IHeader Header; + public byte[] Unknown2; + + public ProgramBank? Programs; + public SampleBlock[]? Samples; + + public SWD(string path) + { + using (FileStream stream = File.OpenRead(path)) + { + var r = new EndianBinaryReader(stream, ascii: true); + + Type = r.ReadString_Count(4); + Unknown1 = new byte[4]; + r.ReadBytes(Unknown1); + Length = r.ReadUInt32(); + Version = r.ReadUInt16(); + Unknown2 = new byte[2]; + r.ReadBytes(Unknown2); + + switch (Version) + { + case 0x402: + { + Header_V402 header = r.ReadObject(); + Header = header; + Programs = ReadPrograms(r, header.NumPRGISlots); + Samples = ReadSamples(r, header.NumWAVISlots); + break; + } + case 0x415: + { + Header_V415 header = r.ReadObject(); + Header = header; + Programs = ReadPrograms(r, header.NumPRGISlots); + if (header.PCMDLength != 0 && (header.PCMDLength & 0xFFFF0000) != 0xAAAA0000) + { + Samples = ReadSamples(r, header.NumWAVISlots); + } + break; + } + default: throw new InvalidDataException(); + } + } + } + + private static long FindChunk(EndianBinaryReader r, string chunk) + { + long pos = -1; + long oldPosition = r.Stream.Position; + r.Stream.Position = 0; + + while (r.Stream.Position < r.Stream.Length) + { + string str = r.ReadString_Count(4); + if (str == chunk) + { + pos = r.Stream.Position - 4; + break; + } + + switch (str) + { + case "swdb": + case "swdl": + { + r.Stream.Position += 0x4C; + break; + } + default: + { + Debug.WriteLine($"Ignoring {str} chunk"); + r.Stream.Position += 0x8; + uint length = r.ReadUInt32(); + r.Stream.Position += length; + r.Stream.Align(16); + break; + } + } + } + + r.Stream.Position = oldPosition; + return pos; + } + + private static SampleBlock[] ReadSamples(EndianBinaryReader r, int numWAVISlots) + where T : IWavInfo, new() + { + long waviChunkOffset = FindChunk(r, "wavi"); + long pcmdChunkOffset = FindChunk(r, "pcmd"); + if (waviChunkOffset == -1 || pcmdChunkOffset == -1) + { + throw new InvalidDataException(); + } + else + { + waviChunkOffset += 0x10; + pcmdChunkOffset += 0x10; + var samples = new SampleBlock[numWAVISlots]; + for (int i = 0; i < numWAVISlots; i++) + { + r.Stream.Position = waviChunkOffset + (2 * i); + ushort offset = r.ReadUInt16(); + if (offset != 0) + { + r.Stream.Position = offset + waviChunkOffset; + T wavInfo = r.ReadObject(); + samples[i] = new SampleBlock + { + WavInfo = wavInfo, + Data = new byte[(int)((wavInfo.LoopStart + wavInfo.LoopEnd) * 4)], + }; + r.Stream.Position = pcmdChunkOffset + wavInfo.SampleOffset; + r.ReadBytes(samples[i].Data); + } + } + return samples; + } + } + private static ProgramBank? ReadPrograms(EndianBinaryReader r, int numPRGISlots) + where T : IProgramInfo, new() + { + long chunkOffset = FindChunk(r, "prgi"); + if (chunkOffset == -1) + { + return null; + } + + chunkOffset += 0x10; + var programInfos = new IProgramInfo?[numPRGISlots]; + for (int i = 0; i < programInfos.Length; i++) + { + r.Stream.Position = chunkOffset + (2 * i); + ushort offset = r.ReadUInt16(); + if (offset != 0) + { + r.Stream.Position = offset + chunkOffset; + programInfos[i] = r.ReadObject(); + } + } + return new ProgramBank + { + ProgramInfos = programInfos, + KeyGroups = ReadKeyGroups(r), + }; + } + private static KeyGroup[] ReadKeyGroups(EndianBinaryReader r) + { + long chunkOffset = FindChunk(r, "kgrp"); + if (chunkOffset == -1) + { + return Array.Empty(); + } + + r.Stream.Position = chunkOffset + 0xC; + uint chunkLength = r.ReadUInt32(); + var keyGroups = new KeyGroup[chunkLength / 8]; // 8 is the size of a KeyGroup + for (int i = 0; i < keyGroups.Length; i++) + { + keyGroups[i] = r.ReadObject(); + } + return keyGroups; + } +} diff --git a/VG Music Studio - Core/NDS/NDSUtils.cs b/VG Music Studio - Core/NDS/NDSUtils.cs new file mode 100644 index 00000000..2bfd9b5a --- /dev/null +++ b/VG Music Studio - Core/NDS/NDSUtils.cs @@ -0,0 +1,6 @@ +namespace Kermalis.VGMusicStudio.Core.NDS; + +internal static class NDSUtils +{ + public const int ARM7_CLOCK = 16_756_991; // (33.513982 MHz / 2) == 16.756991 MHz == 16,756,991 Hz +} diff --git a/VG Music Studio - Core/NDS/SDAT/SBNK.cs b/VG Music Studio - Core/NDS/SDAT/SBNK.cs new file mode 100644 index 00000000..953f065d --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SBNK.cs @@ -0,0 +1,195 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +internal sealed class SBNK +{ + public sealed class InstrumentData + { + public sealed class DataParam + { + public ushort[] Info; + public byte BaseNote; + public byte Attack; + public byte Decay; + public byte Sustain; + public byte Release; + public byte Pan; + + public DataParam(EndianBinaryReader er) + { + Info = new ushort[2]; + er.ReadUInt16s(Info); + BaseNote = er.ReadByte(); + Attack = er.ReadByte(); + Decay = er.ReadByte(); + Sustain = er.ReadByte(); + Release = er.ReadByte(); + Pan = er.ReadByte(); + } + } + + public InstrumentType Type; + public byte Padding; + public DataParam Param; + + public InstrumentData(InstrumentType type, DataParam param) + { + Type = type; + Param = param; + } + public InstrumentData(EndianBinaryReader er) + { + Type = er.ReadEnum(); + Padding = er.ReadByte(); + Param = new DataParam(er); + } + } + public sealed class Instrument + { + public sealed class DrumSetData + { + public byte MinNote; + public byte MaxNote; + public InstrumentData[] SubInstruments; + + public DrumSetData(EndianBinaryReader er) + { + MinNote = er.ReadByte(); + MaxNote = er.ReadByte(); + SubInstruments = new InstrumentData[MaxNote - MinNote + 1]; + for (int i = 0; i < SubInstruments.Length; i++) + { + SubInstruments[i] = new InstrumentData(er); + } + } + } + public sealed class KeySplitData + { + public byte[] KeyRegions; + public InstrumentData[] SubInstruments; + + public KeySplitData(EndianBinaryReader er) + { + KeyRegions = new byte[8]; + er.ReadBytes(KeyRegions); + + int numSubInstruments = 0; + for (int i = 0; i < 8; i++) + { + if (KeyRegions[i] == 0) + { + break; + } + numSubInstruments++; + } + + SubInstruments = new InstrumentData[numSubInstruments]; + for (int i = 0; i < numSubInstruments; i++) + { + SubInstruments[i] = new InstrumentData(er); + } + } + } + + public InstrumentType Type; + public ushort DataOffset; + public byte Padding; + + public object? Data; + + public Instrument(EndianBinaryReader er) + { + Type = er.ReadEnum(); + DataOffset = er.ReadUInt16(); + Padding = er.ReadByte(); + + long p = er.Stream.Position; + switch (Type) + { + case InstrumentType.PCM: + case InstrumentType.PSG: + case InstrumentType.Noise: er.Stream.Position = DataOffset; Data = new InstrumentData.DataParam(er); break; + case InstrumentType.Drum: er.Stream.Position = DataOffset; Data = new DrumSetData(er); break; + case InstrumentType.KeySplit: er.Stream.Position = DataOffset; Data = new KeySplitData(er); break; + default: break; + } + er.Stream.Position = p; + } + } + + public SDATFileHeader FileHeader; // "SBNK" + public string BlockType; // "DATA" + public int BlockSize; + public byte[] Padding; + public int NumInstruments; + public Instrument[] Instruments; + + public SWAR[] SWARs { get; } + + public SBNK(byte[] bytes) + { + using (var stream = new MemoryStream(bytes)) + { + var er = new EndianBinaryReader(stream, ascii: true); + FileHeader = new SDATFileHeader(er); + BlockType = er.ReadString_Count(4); + BlockSize = er.ReadInt32(); + Padding = new byte[32]; + er.ReadBytes(Padding); + NumInstruments = er.ReadInt32(); + Instruments = new Instrument[NumInstruments]; + for (int i = 0; i < Instruments.Length; i++) + { + Instruments[i] = new Instrument(er); + } + } + + SWARs = new SWAR[4]; + } + + public InstrumentData? GetInstrumentData(int voice, int note) + { + if (voice >= NumInstruments) + { + return null; + } + + switch (Instruments[voice].Type) + { + case InstrumentType.PCM: + case InstrumentType.PSG: + case InstrumentType.Noise: + { + var d = (InstrumentData.DataParam)Instruments[voice].Data!; + // TODO: Better way? + return new InstrumentData(Instruments[voice].Type, d); + } + case InstrumentType.Drum: + { + var d = (Instrument.DrumSetData)Instruments[voice].Data!; + return note < d.MinNote || note > d.MaxNote ? null : d.SubInstruments[note - d.MinNote]; + } + case InstrumentType.KeySplit: + { + var d = (Instrument.KeySplitData)Instruments[voice].Data!; + for (int i = 0; i < 8; i++) + { + if (note <= d.KeyRegions[i]) + { + return d.SubInstruments[i]; + } + } + return null; + } + default: return null; + } + } + + public SWAR.SWAV? GetSWAV(int swarIndex, int swavIndex) + { + SWAR swar = SWARs[swarIndex]; + return swar is null || swavIndex >= swar.NumWaves ? null : swar.Waves[swavIndex]; + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDAT.cs b/VG Music Studio - Core/NDS/SDAT/SDAT.cs new file mode 100644 index 00000000..2b31cf4c --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDAT.cs @@ -0,0 +1,266 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +public sealed class SDAT +{ + public sealed class SYMB + { + public sealed class Record + { + public int NumEntries; + public int[] EntryOffsets; + + public string?[] Entries; + + public Record(EndianBinaryReader er, long baseOffset) + { + NumEntries = er.ReadInt32(); + EntryOffsets = new int[NumEntries]; + er.ReadInt32s(EntryOffsets); + + long p = er.Stream.Position; + Entries = new string[NumEntries]; + for (int i = 0; i < NumEntries; i++) + { + if (EntryOffsets[i] != 0) + { + er.Stream.Position = baseOffset + EntryOffsets[i]; + Entries[i] = er.ReadString_NullTerminated(); + } + } + er.Stream.Position = p; + } + } + + public string BlockType; // "SYMB" + public int BlockSize; + public int[] RecordOffsets; + public byte[] Padding; + + public Record SequenceSymbols; + //SequenceArchiveSymbols; + public Record BankSymbols; + public Record WaveArchiveSymbols; + //PlayerSymbols; + //GroupSymbols; + //StreamPlayerSymbols; + //StreamSymbols; + + public SYMB(EndianBinaryReader er, long baseOffset) + { + er.Stream.Position = baseOffset; + + BlockType = er.ReadString_Count(4); + BlockSize = er.ReadInt32(); + RecordOffsets = new int[8]; + er.ReadInt32s(RecordOffsets); + Padding = new byte[24]; + er.ReadBytes(Padding); + + er.Stream.Position = baseOffset + RecordOffsets[0]; + SequenceSymbols = new Record(er, baseOffset); + + er.Stream.Position = baseOffset + RecordOffsets[2]; + BankSymbols = new Record(er, baseOffset); + + er.Stream.Position = baseOffset + RecordOffsets[3]; + WaveArchiveSymbols = new Record(er, baseOffset); + } + } + + public sealed class INFO + { + public sealed class Record where T : new() + { + public int NumEntries; + public int[] EntryOffsets; + + public T?[] Entries; + + public Record(EndianBinaryReader er, long baseOffset) + { + NumEntries = er.ReadInt32(); + EntryOffsets = new int[NumEntries]; + er.ReadInt32s(EntryOffsets); + + long p = er.Stream.Position; + Entries = new T?[NumEntries]; + for (int i = 0; i < NumEntries; i++) + { + if (EntryOffsets[i] != 0) + { + er.Stream.Position = baseOffset + EntryOffsets[i]; + Entries[i] = er.ReadObject(); + } + } + er.Stream.Position = p; + } + } + + public sealed class SequenceInfo + { + public ushort FileId { get; set; } + public byte Unknown1 { get; set; } + public byte Unknown2 { get; set; } + public ushort Bank { get; set; } + public byte Volume { get; set; } + public byte ChannelPriority { get; set; } + public byte PlayerPriority { get; set; } + public byte PlayerNum { get; set; } + public byte Unknown3 { get; set; } + public byte Unknown4 { get; set; } + + internal SSEQ GetSSEQ(SDAT sdat) + { + return new SSEQ(sdat.FATBlock.Entries[FileId].Data); + } + internal SBNK GetSBNK(SDAT sdat) + { + BankInfo bankInfo = sdat.INFOBlock.BankInfos.Entries[Bank]!; + var sbnk = new SBNK(sdat.FATBlock.Entries[bankInfo.FileId].Data); + for (int i = 0; i < 4; i++) + { + if (bankInfo.SWARs[i] != 0xFFFF) + { + sbnk.SWARs[i] = new SWAR(sdat.FATBlock.Entries[sdat.INFOBlock.WaveArchiveInfos.Entries[bankInfo.SWARs[i]]!.FileId].Data); + } + } + return sbnk; + } + } + public sealed class BankInfo + { + public ushort FileId { get; set; } + public byte Unknown1 { get; set; } + public byte Unknown2 { get; set; } + [BinaryArrayFixedLength(4)] + public ushort[] SWARs { get; set; } = null!; + } + public sealed class WaveArchiveInfo + { + public ushort FileId { get; set; } + public byte Unknown1 { get; set; } + public byte Unknown2 { get; set; } + } + + public string BlockType; // "INFO" + public int BlockSize; + public int[] InfoOffsets; + public byte[] Padding; + + public Record SequenceInfos; + //SequenceArchiveInfos; + public Record BankInfos; + public Record WaveArchiveInfos; + //PlayerInfos; + //GroupInfos; + //StreamPlayerInfos; + //StreamInfos; + + public INFO(EndianBinaryReader er, long baseOffset) + { + er.Stream.Position = baseOffset; + + BlockType = er.ReadString_Count(4); + BlockSize = er.ReadInt32(); + InfoOffsets = new int[8]; + er.ReadInt32s(InfoOffsets); + Padding = new byte[24]; + er.ReadBytes(Padding); + + er.Stream.Position = baseOffset + InfoOffsets[0]; + SequenceInfos = new Record(er, baseOffset); + + er.Stream.Position = baseOffset + InfoOffsets[2]; + BankInfos = new Record(er, baseOffset); + + er.Stream.Position = baseOffset + InfoOffsets[3]; + WaveArchiveInfos = new Record(er, baseOffset); + } + } + + public sealed class FAT + { + public sealed class FATEntry + { + public int DataOffset; + public int DataLength; + public byte[] Padding; + + public byte[] Data; + + public FATEntry(EndianBinaryReader er) + { + DataOffset = er.ReadInt32(); + DataLength = er.ReadInt32(); + Padding = new byte[8]; + er.ReadBytes(Padding); + + long p = er.Stream.Position; + Data = new byte[DataLength]; + er.Stream.Position = DataOffset; + er.ReadBytes(Data); + er.Stream.Position = p; + } + } + + public string BlockType; // "FAT " + public int BlockSize; + public int NumEntries; + public FATEntry[] Entries; + + public FAT(EndianBinaryReader er) + { + BlockType = er.ReadString_Count(4); + BlockSize = er.ReadInt32(); + NumEntries = er.ReadInt32(); + Entries = new FATEntry[NumEntries]; + for (int i = 0; i < Entries.Length; i++) + { + Entries[i] = new FATEntry(er); + } + } + } + + public SDATFileHeader FileHeader; // "SDAT" + public int SYMBOffset; + public int SYMBLength; + public int INFOOffset; + public int INFOLength; + public int FATOffset; + public int FATLength; + public int FILEOffset; + public int FILELength; + public byte[] Padding; + + public SYMB? SYMBBlock; + public INFO INFOBlock; + public FAT FATBlock; + //FILEBlock + + public SDAT(Stream stream) + { + var er = new EndianBinaryReader(stream, ascii: true); + FileHeader = new SDATFileHeader(er); + SYMBOffset = er.ReadInt32(); + SYMBLength = er.ReadInt32(); + INFOOffset = er.ReadInt32(); + INFOLength = er.ReadInt32(); + FATOffset = er.ReadInt32(); + FATLength = er.ReadInt32(); + FILEOffset = er.ReadInt32(); + FILELength = er.ReadInt32(); + Padding = new byte[16]; + er.ReadBytes(Padding); + + if (SYMBOffset != 0 && SYMBLength != 0) + { + SYMBBlock = new SYMB(er, SYMBOffset); + } + INFOBlock = new INFO(er, INFOOffset); + stream.Position = FATOffset; + FATBlock = new FAT(er); + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATChannel.cs b/VG Music Studio - Core/NDS/SDAT/SDATChannel.cs new file mode 100644 index 00000000..a9251764 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATChannel.cs @@ -0,0 +1,391 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +internal sealed class SDATChannel +{ + public readonly byte Index; + + public SDATTrack? Owner; + public InstrumentType Type; + public EnvelopeState State; + public bool AutoSweep; + public byte BaseNote; + public byte Note; + public byte NoteVelocity; + public sbyte StartingPan; + public sbyte Pan; + public int SweepCounter; + public int SweepLength; + public short SweepPitch; + /// The SEQ Player treats 0 as the 100% amplitude value and -92544 (-723*128) as the 0% amplitude value. The starting ampltitude is 0% (-92544) + public int Velocity; + /// From 0x00-0x7F (Calculated from Utils) + public byte Volume; + public ushort BaseTimer; + public ushort Timer; + public int NoteDuration; + + private byte _attack; + private int _sustain; + private ushort _decay; + private ushort _release; + public byte LFORange; + public byte LFOSpeed; + public byte LFODepth; + public ushort LFODelay; + public ushort LFOPhase; + public int LFOParam; + public ushort LFODelayCount; + public LFOType LFOType; + public byte Priority; + + private int _pos; + private short _prevLeft; + private short _prevRight; + + // PCM8, PCM16, ADPCM + private SWAR.SWAV? _swav; + // PCM8, PCM16 + private int _dataOffset; + // ADPCM + private ADPCMDecoder _adpcmDecoder; + private short _adpcmLoopLastSample; + private short _adpcmLoopStepIndex; + // PSG + private byte _psgDuty; + private int _psgCounter; + // Noise + private ushort _noiseCounter; + + public SDATChannel(byte i) + { + Index = i; + } + + public void StartPCM(SWAR.SWAV swav, int noteDuration) + { + Type = InstrumentType.PCM; + _dataOffset = 0; + _swav = swav; + if (swav.Format == SWAVFormat.ADPCM) + { + _adpcmDecoder.Init(swav.Samples); + } + BaseTimer = swav.Timer; + Start(noteDuration); + } + public void StartPSG(byte duty, int noteDuration) + { + Type = InstrumentType.PSG; + _psgCounter = 0; + _psgDuty = duty; + BaseTimer = 8006; // NDSUtils.ARM7_CLOCK / 2093 + Start(noteDuration); + } + public void StartNoise(int noteLength) + { + Type = InstrumentType.Noise; + _noiseCounter = 0x7FFF; + BaseTimer = 8006; // NDSUtils.ARM7_CLOCK / 2093 + Start(noteLength); + } + + private void Start(int noteDuration) + { + State = EnvelopeState.Attack; + Velocity = -92544; + _pos = 0; + _prevLeft = _prevRight = 0; + NoteDuration = noteDuration; + } + + public void Stop() + { + Owner?.Channels.Remove(this); + Owner = null; + Volume = 0; + Priority = 0; + } + + public int SweepMain() + { + if (SweepPitch == 0 || SweepCounter >= SweepLength) + { + return 0; + } + + int sweep = (int)(Math.BigMul(SweepPitch, SweepLength - SweepCounter) / SweepLength); + if (AutoSweep) + { + SweepCounter++; + } + return sweep; + } + public void LFOTick() + { + if (LFODelayCount > 0) + { + LFODelayCount--; + LFOPhase = 0; + } + else + { + int param = LFORange * SDATUtils.Sin(LFOPhase >> 8) * LFODepth; + if (LFOType == LFOType.Volume) + { + param = (param * 60) >> 14; + } + else + { + param >>= 8; + } + LFOParam = param; + int counter = LFOPhase + (LFOSpeed << 6); // "<< 6" is "* 0x40" + while (counter >= 0x8000) + { + counter -= 0x8000; + } + LFOPhase = (ushort)counter; + } + } + + public void SetAttack(int a) + { + _attack = SDATUtils.AttackTable[a]; + } + public void SetDecay(int d) + { + _decay = SDATUtils.DecayTable[d]; + } + public void SetSustain(byte s) + { + _sustain = SDATUtils.SustainTable[s]; + } + public void SetRelease(int r) + { + _release = SDATUtils.DecayTable[r]; + } + public void StepEnvelope() + { + switch (State) + { + case EnvelopeState.Attack: + { + Velocity = _attack * Velocity / 0xFF; + if (Velocity == 0) + { + State = EnvelopeState.Decay; + } + break; + } + case EnvelopeState.Decay: + { + Velocity -= _decay; + if (Velocity <= _sustain) + { + State = EnvelopeState.Sustain; + Velocity = _sustain; + } + break; + } + case EnvelopeState.Release: + { + Velocity -= _release; + if (Velocity < -92544) + { + Velocity = -92544; + } + break; + } + } + } + + /// EmulateProcess doesn't care about samples that loop; it only cares about ones that force the track to wait for them to end + public void EmulateProcess() + { + if (Timer == 0) + { + return; + } + + int numSamples = (_pos + 0x100) / Timer; + _pos = (_pos + 0x100) % Timer; + for (int i = 0; i < numSamples; i++) + { + if (Type != InstrumentType.PCM || _swav!.DoesLoop) + { + continue; + } + + switch (_swav.Format) + { + case SWAVFormat.PCM8: + { + if (_dataOffset >= _swav.Samples.Length) + { + Stop(); + } + else + { + _dataOffset++; + } + return; + } + case SWAVFormat.PCM16: + { + if (_dataOffset >= _swav.Samples.Length) + { + Stop(); + } + else + { + _dataOffset += 2; + } + return; + } + case SWAVFormat.ADPCM: + { + if (_adpcmDecoder.DataOffset >= _swav.Samples.Length && !_adpcmDecoder.OnSecondNibble) + { + Stop(); + } + else + { + // This is a faster emulation of adpcmDecoder.GetSample() without caring about the sample + if (_adpcmDecoder.OnSecondNibble) + { + _adpcmDecoder.DataOffset++; + } + _adpcmDecoder.OnSecondNibble = !_adpcmDecoder.OnSecondNibble; + } + return; + } + } + } + } + public void Process(out short left, out short right) + { + if (Timer == 0) + { + left = _prevLeft; + right = _prevRight; + return; + } + + int numSamples = (_pos + 0x100) / Timer; + _pos = (_pos + 0x100) % Timer; + // numSamples can be 0 + for (int i = 0; i < numSamples; i++) + { + short samp; + switch (Type) + { + case InstrumentType.PCM: + { + switch (_swav!.Format) + { + case SWAVFormat.PCM8: + { + // If hit end + if (_dataOffset >= _swav.Samples.Length) + { + if (_swav.DoesLoop) + { + _dataOffset = _swav.LoopOffset * 4; + } + else + { + left = right = _prevLeft = _prevRight = 0; + Stop(); + return; + } + } + samp = (short)((sbyte)_swav.Samples[_dataOffset++] << 8); + break; + } + case SWAVFormat.PCM16: + { + // If hit end + if (_dataOffset >= _swav.Samples.Length) + { + if (_swav.DoesLoop) + { + _dataOffset = _swav.LoopOffset * 4; + } + else + { + left = right = _prevLeft = _prevRight = 0; + Stop(); + return; + } + } + samp = (short)(_swav.Samples[_dataOffset++] | (_swav.Samples[_dataOffset++] << 8)); + break; + } + case SWAVFormat.ADPCM: + { + // If just looped + if (_swav.DoesLoop && _adpcmDecoder.DataOffset == _swav.LoopOffset * 4 && !_adpcmDecoder.OnSecondNibble) + { + _adpcmLoopLastSample = _adpcmDecoder.LastSample; + _adpcmLoopStepIndex = _adpcmDecoder.StepIndex; + } + // If hit end + if (_adpcmDecoder.DataOffset >= _swav.Samples.Length && !_adpcmDecoder.OnSecondNibble) + { + if (_swav.DoesLoop) + { + _adpcmDecoder.DataOffset = _swav.LoopOffset * 4; + _adpcmDecoder.StepIndex = _adpcmLoopStepIndex; + _adpcmDecoder.LastSample = _adpcmLoopLastSample; + _adpcmDecoder.OnSecondNibble = false; + } + else + { + left = right = _prevLeft = _prevRight = 0; + Stop(); + return; + } + } + samp = _adpcmDecoder.GetSample(); + break; + } + default: samp = 0; break; + } + break; + } + case InstrumentType.PSG: + { + samp = _psgCounter <= _psgDuty ? short.MinValue : short.MaxValue; + _psgCounter++; + if (_psgCounter >= 8) + { + _psgCounter = 0; + } + break; + } + case InstrumentType.Noise: + { + if ((_noiseCounter & 1) != 0) + { + _noiseCounter = (ushort)((_noiseCounter >> 1) ^ 0x6000); + samp = -0x7FFF; + } + else + { + _noiseCounter = (ushort)(_noiseCounter >> 1); + samp = 0x7FFF; + } + break; + } + default: samp = 0; break; + } + samp = (short)(samp * Volume / 0x7F); + _prevLeft = (short)(samp * (-Pan + 0x40) / 0x80); + _prevRight = (short)(samp * (Pan + 0x40) / 0x80); + } + left = _prevLeft; + right = _prevRight; + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATCommands.cs b/VG Music Studio - Core/NDS/SDAT/SDATCommands.cs new file mode 100644 index 00000000..e7fe9e17 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATCommands.cs @@ -0,0 +1,439 @@ +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Drawing; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +internal abstract class SDATCommand +{ + public bool RandMod { get; set; } + public bool VarMod { get; set; } + + protected string GetValues(int value, string ifNot) + { + return RandMod ? $"[{(short)value}, {(short)(value >> 16)}]" + : VarMod ? $"[{(byte)value}]" + : ifNot; + } +} + +internal sealed class AllocTracksCommand : SDATCommand, ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Alloc Tracks"; + public string Arguments => $"{Convert.ToString(Tracks, 2).PadLeft(16, '0')}b"; + + public ushort Tracks { get; set; } +} +internal sealed class CallCommand : SDATCommand, ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Call"; + public string Arguments => $"0x{Offset:X4}"; + + public int Offset { get; set; } +} +internal sealed class FinishCommand : SDATCommand, ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Finish"; + public string Arguments => string.Empty; +} +internal sealed class ForceAttackCommand : SDATCommand, ICommand +{ + public Color Color => Color.LightSteelBlue; + public string Label => "Force Attack"; + public string Arguments => GetValues(Attack, Attack.ToString()); + + public int Attack { get; set; } +} +internal sealed class ForceDecayCommand : SDATCommand, ICommand +{ + public Color Color => Color.LightSteelBlue; + public string Label => "Force Decay"; + public string Arguments => GetValues(Decay, Decay.ToString()); + + public int Decay { get; set; } +} +internal sealed class ForceReleaseCommand : SDATCommand, ICommand +{ + public Color Color => Color.LightSteelBlue; + public string Label => "Force Release"; + public string Arguments => GetValues(Release, Release.ToString()); + + public int Release { get; set; } +} +internal sealed class ForceSustainCommand : SDATCommand, ICommand +{ + public Color Color => Color.LightSteelBlue; + public string Label => "Force Sustain"; + public string Arguments => GetValues(Sustain, Sustain.ToString()); + + public int Sustain { get; set; } +} +internal sealed class JumpCommand : SDATCommand, ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Jump"; + public string Arguments => $"0x{Offset:X4}"; + + public int Offset { get; set; } +} +internal sealed class LFODelayCommand : SDATCommand, ICommand +{ + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Delay"; + public string Arguments => GetValues(Delay, Delay.ToString()); + + public int Delay { get; set; } +} +internal sealed class LFODepthCommand : SDATCommand, ICommand +{ + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Depth"; + public string Arguments => GetValues(Depth, Depth.ToString()); + + public int Depth { get; set; } +} +internal sealed class LFORangeCommand : SDATCommand, ICommand +{ + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Range"; + public string Arguments => GetValues(Range, Range.ToString()); + + public int Range { get; set; } +} +internal sealed class LFOSpeedCommand : SDATCommand, ICommand +{ + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Speed"; + public string Arguments => GetValues(Speed, Speed.ToString()); + + public int Speed { get; set; } +} +internal sealed class LFOTypeCommand : SDATCommand, ICommand +{ + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Type"; + public string Arguments => GetValues(Type, Type.ToString()); + + public int Type { get; set; } +} +internal sealed class LoopEndCommand : SDATCommand, ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Loop End"; + public string Arguments => string.Empty; +} +internal sealed class LoopStartCommand : SDATCommand, ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Loop Start"; + public string Arguments => GetValues(NumLoops, NumLoops.ToString()); + + public int NumLoops { get; set; } +} +internal sealed class ModIfCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "If Modifier"; + public string Arguments => string.Empty; +} +internal sealed class ModRandCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Rand Modifier"; + public string Arguments => string.Empty; +} +internal sealed class ModVarCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var Modifier"; + public string Arguments => string.Empty; +} +internal sealed class MonophonyCommand : SDATCommand, ICommand +{ + public Color Color => Color.SkyBlue; + public string Label => "Monophony Toggle"; + public string Arguments => GetValues(Mono, (Mono == 1).ToString()); + + public int Mono { get; set; } +} +internal sealed class NoteComand : SDATCommand, ICommand +{ + public Color Color => Color.SkyBlue; + public string Label => "Note"; + public string Arguments => $"{ConfigUtils.GetKeyName(Note)}, {Velocity}, {GetValues(Duration, Duration.ToString())}"; + + public byte Note { get; set; } + public byte Velocity { get; set; } + public int Duration { get; set; } +} +internal sealed class OpenTrackCommand : SDATCommand, ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Open Track"; + public string Arguments => $"{Track}, 0x{Offset:X4}"; + + public byte Track { get; set; } + public int Offset { get; set; } +} +internal sealed class PanpotCommand : SDATCommand, ICommand +{ + public Color Color => Color.GreenYellow; + public string Label => "Panpot"; + public string Arguments => GetValues(Panpot, Panpot.ToString()); + + public int Panpot { get; set; } +} +internal sealed class PitchBendCommand : SDATCommand, ICommand +{ + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend"; + public string Arguments => GetValues(Bend, Bend.ToString()); + + public int Bend { get; set; } +} +internal sealed class PitchBendRangeCommand : SDATCommand, ICommand +{ + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend Range"; + public string Arguments => GetValues(Range, Range.ToString()); + + public int Range { get; set; } +} +internal sealed class PlayerVolumeCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Player Volume"; + public string Arguments => GetValues(Volume, Volume.ToString()); + + public int Volume { get; set; } +} +internal sealed class PortamentoControlCommand : SDATCommand, ICommand +{ + public Color Color => Color.HotPink; + public string Label => "Portamento Control"; + public string Arguments => GetValues(Portamento, Portamento.ToString()); + + public int Portamento { get; set; } +} +internal sealed class PortamentoToggleCommand : SDATCommand, ICommand +{ + public Color Color => Color.HotPink; + public string Label => "Portamento Toggle"; + public string Arguments => GetValues(Portamento, (Portamento == 1).ToString()); + + public int Portamento { get; set; } +} +internal sealed class PortamentoTimeCommand : SDATCommand, ICommand +{ + public Color Color => Color.HotPink; + public string Label => "Portamento Time"; + public string Arguments => GetValues(Time, Time.ToString()); + + public int Time { get; set; } +} +internal sealed class PriorityCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Priority"; + public string Arguments => GetValues(Priority, Priority.ToString()); + + public int Priority { get; set; } +} +internal sealed class RestCommand : SDATCommand, ICommand +{ + public Color Color => Color.PaleVioletRed; + public string Label => "Rest"; + public string Arguments => GetValues(Rest, Rest.ToString()); + + public int Rest { get; set; } +} +internal sealed class ReturnCommand : SDATCommand, ICommand +{ + public Color Color => Color.MediumSpringGreen; + public string Label => "Return"; + public string Arguments => string.Empty; +} +internal sealed class SweepPitchCommand : SDATCommand, ICommand +{ + public Color Color => Color.MediumPurple; + public string Label => "Sweep Pitch"; + public string Arguments => GetValues(Pitch, Pitch.ToString()); + + public int Pitch { get; set; } +} +internal sealed class TempoCommand : SDATCommand, ICommand +{ + public Color Color => Color.DeepSkyBlue; + public string Label => "Tempo"; + public string Arguments => GetValues(Tempo, Tempo.ToString()); + + public int Tempo { get; set; } +} +internal sealed class TieCommand : SDATCommand, ICommand +{ + public Color Color => Color.SkyBlue; + public string Label => "Tie"; + public string Arguments => GetValues(Tie, (Tie == 1).ToString()); + + public int Tie { get; set; } +} +internal sealed class TrackExpressionCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Track Expression"; + public string Arguments => GetValues(Expression, Expression.ToString()); + + public int Expression { get; set; } +} +internal sealed class TrackVolumeCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Track Volume"; + public string Arguments => GetValues(Volume, Volume.ToString()); + + public int Volume { get; set; } +} +internal sealed class TransposeCommand : SDATCommand, ICommand +{ + public Color Color => Color.SkyBlue; + public string Label => "Transpose"; + public string Arguments => GetValues(Transpose, Transpose.ToString()); + + public int Transpose { get; set; } +} +internal sealed class VarAddCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var Add"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } +} +internal sealed class VarCmpEECommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var =="; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } +} +internal sealed class VarCmpGECommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var >="; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } +} +internal sealed class VarCmpGGCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var >"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } +} +internal sealed class VarCmpLECommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var <="; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } +} +internal sealed class VarCmpLLCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var <"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } +} +internal sealed class VarCmpNECommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var !="; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } +} +internal sealed class VarDivCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var Div"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } +} +internal sealed class VarMulCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var Mul"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } +} +internal sealed class VarPrintCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var Print"; + public string Arguments => GetValues(Variable, Variable.ToString()); + + public int Variable { get; set; } +} +internal sealed class VarRandCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var Rand"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } +} +internal sealed class VarSetCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var Set"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } +} +internal sealed class VarShiftCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var Shift"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } +} +internal sealed class VarSubCommand : SDATCommand, ICommand +{ + public Color Color => Color.SteelBlue; + public string Label => "Var Sub"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } +} +internal sealed class VoiceCommand : SDATCommand, ICommand +{ + public Color Color => Color.DarkSalmon; + public string Label => "Voice"; + public string Arguments => GetValues(Voice, Voice.ToString()); + + public int Voice { get; set; } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATConfig.cs b/VG Music Studio - Core/NDS/SDAT/SDATConfig.cs new file mode 100644 index 00000000..ce0c21c1 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATConfig.cs @@ -0,0 +1,40 @@ +using Kermalis.VGMusicStudio.Core.Properties; +using System; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +public sealed class SDATConfig : Config +{ + public readonly SDAT SDAT; + + internal SDATConfig(SDAT sdat) + { + if (sdat.INFOBlock.SequenceInfos.NumEntries == 0) + { + throw new Exception(Strings.ErrorSDATNoSequences); + } + + SDAT = sdat; + var songs = new List(sdat.INFOBlock.SequenceInfos.NumEntries); + for (int i = 0; i < sdat.INFOBlock.SequenceInfos.NumEntries; i++) + { + if (sdat.INFOBlock.SequenceInfos.Entries[i] is not null) + { + songs.Add(new Song(i, sdat.SYMBBlock?.SequenceSymbols.Entries[i] ?? i.ToString())); + } + } + Playlists.Add(new Playlist(Strings.PlaylistMusic, songs)); + } + + public override string GetGameName() + { + return "SDAT"; + } + public override string GetSongName(int index) + { + return SDAT.SYMBBlock is null || index < 0 || index >= SDAT.SYMBBlock.SequenceSymbols.NumEntries + ? index.ToString() + : '\"' + SDAT.SYMBBlock.SequenceSymbols.Entries[index] + '\"'; + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATEngine.cs b/VG Music Studio - Core/NDS/SDAT/SDATEngine.cs new file mode 100644 index 00000000..7611c7f6 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATEngine.cs @@ -0,0 +1,26 @@ +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +public sealed class SDATEngine : Engine +{ + public static SDATEngine? SDATInstance { get; private set; } + + public override SDATConfig Config { get; } + public override SDATMixer Mixer { get; } + public override SDATPlayer Player { get; } + + public SDATEngine(SDAT sdat) + { + Config = new SDATConfig(sdat); + Mixer = new SDATMixer(); + Player = new SDATPlayer(Config, Mixer); + + SDATInstance = this; + Instance = this; + } + + public override void Dispose() + { + base.Dispose(); + SDATInstance = null; + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATEnums.cs b/VG Music Studio - Core/NDS/SDAT/SDATEnums.cs new file mode 100644 index 00000000..62f10b9a --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATEnums.cs @@ -0,0 +1,39 @@ +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +internal enum EnvelopeState : byte +{ + Attack, + Decay, + Sustain, + Release, +} +internal enum ArgType : byte +{ + None, + Byte, + Short, + VarLen, + Rand, + PlayerVar, +} + +internal enum LFOType : byte +{ + Pitch, + Volume, + Panpot, +} +internal enum InstrumentType : byte +{ + PCM = 0x1, + PSG = 0x2, + Noise = 0x3, + Drum = 0x10, + KeySplit = 0x11, +} +internal enum SWAVFormat : byte +{ + PCM8, + PCM16, + ADPCM, +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATExceptions.cs b/VG Music Studio - Core/NDS/SDAT/SDATExceptions.cs new file mode 100644 index 00000000..e79c0d45 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATExceptions.cs @@ -0,0 +1,27 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +public sealed class SDATInvalidCMDException : Exception +{ + public byte TrackIndex { get; } + public int CmdOffset { get; } + public byte Cmd { get; } + + internal SDATInvalidCMDException(byte trackIndex, int cmdOffset, byte cmd) + { + TrackIndex = trackIndex; + CmdOffset = cmdOffset; + Cmd = cmd; + } +} + +public sealed class SDATTooManyNestedCallsException : Exception +{ + public byte TrackIndex { get; } + + internal SDATTooManyNestedCallsException(byte trackIndex) + { + TrackIndex = trackIndex; + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATFileHeader.cs b/VG Music Studio - Core/NDS/SDAT/SDATFileHeader.cs new file mode 100644 index 00000000..dc742dca --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATFileHeader.cs @@ -0,0 +1,25 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +public sealed class SDATFileHeader +{ + public string FileType; + public ushort FileEndianness; + public ushort Version; + public int FileSize; + public ushort HeaderSize; // 16 + public ushort NumBlocks; + + public SDATFileHeader(EndianBinaryReader er) + { + FileType = er.ReadString_Count(4); + er.Endianness = Endianness.BigEndian; + FileEndianness = er.ReadUInt16(); + er.Endianness = FileEndianness == 0xFFFE ? Endianness.LittleEndian : Endianness.BigEndian; + Version = er.ReadUInt16(); + FileSize = er.ReadInt32(); + HeaderSize = er.ReadUInt16(); + NumBlocks = er.ReadUInt16(); + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong.cs b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong.cs new file mode 100644 index 00000000..ff00ed23 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +internal sealed partial class SDATLoadedSong : ILoadedSong +{ + public List?[] Events { get; } + public long MaxTicks { get; internal set; } + public int LongestTrack; + + private readonly SDATPlayer _player; + private readonly int _randSeed; + private Random? _rand; + public readonly SDAT.INFO.SequenceInfo SEQInfo; // TODO: Not public + private readonly SSEQ _sseq; + private readonly SBNK _sbnk; + + public SDATLoadedSong(SDATPlayer player, SDAT.INFO.SequenceInfo seqInfo) + { + _player = player; + SEQInfo = seqInfo; + + SDAT sdat = player.Config.SDAT; + _sseq = seqInfo.GetSSEQ(sdat); + _sbnk = seqInfo.GetSBNK(sdat); + _randSeed = Random.Shared.Next(); + // Cannot set random seed without creating a new object which is dumb + + Events = new List[0x10]; + AddTrackEvents(0, 0); + } + + private static SDATInvalidCMDException Invalid(byte trackIndex, int cmdOffset, byte cmd) + { + return new SDATInvalidCMDException(trackIndex, cmdOffset, cmd); + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs new file mode 100644 index 00000000..6d690097 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs @@ -0,0 +1,744 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +internal sealed partial class SDATLoadedSong +{ + private void AddEvent(byte trackIndex, long cmdOffset, T command, ArgType argOverrideType) + where T : SDATCommand, ICommand + { + command.RandMod = argOverrideType == ArgType.Rand; + command.VarMod = argOverrideType == ArgType.PlayerVar; + Events[trackIndex]!.Add(new SongEvent(cmdOffset, command)); + } + private bool EventExists(byte trackIndex, long cmdOffset) + { + return Events[trackIndex]!.Exists(e => e.Offset == cmdOffset); + } + + private int ReadArg(ref int dataOffset, ArgType type) + { + switch (type) + { + case ArgType.Byte: + { + return _sseq.Data[dataOffset++]; + } + case ArgType.Short: + { + short s = ReadInt16LittleEndian(_sseq.Data.AsSpan(dataOffset)); + dataOffset += 2; + return s; + } + case ArgType.VarLen: + { + int numRead = 0; + int value = 0; + byte b; + do + { + b = _sseq.Data[dataOffset++]; + value = (value << 7) | (b & 0x7F); + numRead++; + } + while (numRead < 4 && (b & 0x80) != 0); + return value; + } + case ArgType.Rand: + { + // Combine min and max into one int + int minMax = ReadInt32LittleEndian(_sseq.Data.AsSpan(dataOffset)); + dataOffset += 4; + return minMax; + } + case ArgType.PlayerVar: + { + return _sseq.Data[dataOffset++]; // Return var index + } + default: throw new Exception(); + } + } + + private void AddTrackEvents(byte trackIndex, int trackStartOffset) + { + ref List? trackEvents = ref Events[trackIndex]; + trackEvents ??= new List(); + + int callStackDepth = 0; + AddEvents(trackIndex, trackStartOffset, ref callStackDepth); + } + private void AddEvents(byte trackIndex, int startOffset, ref int callStackDepth) + { + int dataOffset = startOffset; + bool cont = true; + while (cont) + { + bool @if = false; + int cmdOffset = dataOffset; + ArgType argOverrideType = ArgType.None; + again: + byte cmd = _sseq.Data[dataOffset++]; + + if (cmd <= 0x7F) + { + HandleNoteEvent(trackIndex, ref dataOffset, cmdOffset, cmd, argOverrideType); + } + else + { + switch (cmd & 0xF0) + { + case 0x80: HandleCmdGroup0x80(trackIndex, ref dataOffset, cmdOffset, cmd, argOverrideType); break; + case 0x90: HandleCmdGroup0x90(trackIndex, ref dataOffset, ref callStackDepth, cmdOffset, cmd, argOverrideType, ref @if, ref cont); break; + case 0xA0: + { + if (HandleCmdGroup0xA0(trackIndex, ref cmdOffset, cmd, ref argOverrideType, ref @if)) + { + goto again; + } + break; + } + case 0xB0: HandleCmdGroup0xB0(trackIndex, ref dataOffset, cmdOffset, cmd, argOverrideType); break; + case 0xC0: HandleCmdGroup0xC0(trackIndex, ref dataOffset, cmdOffset, cmd, argOverrideType); break; + case 0xD0: HandleCmdGroup0xD0(trackIndex, ref dataOffset, cmdOffset, cmd, argOverrideType); break; + case 0xE0: HandleCmdGroup0xE0(trackIndex, ref dataOffset, cmdOffset, cmd, argOverrideType); break; + default: HandleCmdGroup0xF0(trackIndex, ref dataOffset, ref callStackDepth, cmdOffset, cmd, argOverrideType, ref @if, ref cont); break; + } + } + } + } + + private void HandleNoteEvent(byte trackIndex, ref int dataOffset, int cmdOffset, byte cmd, ArgType argOverrideType) + { + byte velocity = _sseq.Data[dataOffset++]; + int duration = ReadArg(ref dataOffset, argOverrideType == ArgType.None ? ArgType.VarLen : argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new NoteComand { Note = cmd, Velocity = velocity, Duration = duration }, argOverrideType); + } + } + private void HandleCmdGroup0x80(byte trackIndex, ref int dataOffset, int cmdOffset, byte cmd, ArgType argOverrideType) + { + int arg = ReadArg(ref dataOffset, argOverrideType == ArgType.None ? ArgType.VarLen : argOverrideType); + switch (cmd) + { + case 0x80: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new RestCommand { Rest = arg }, argOverrideType); + } + break; + } + case 0x81: // RAND PROGRAM: [BW2 (2249)] + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VoiceCommand { Voice = arg }, argOverrideType); // TODO: Bank change + } + break; + } + default: throw Invalid(trackIndex, cmdOffset, cmd); + } + } + private void HandleCmdGroup0x90(byte trackIndex, ref int dataOffset, ref int callStackDepth, int cmdOffset, byte cmd, ArgType argOverrideType, ref bool @if, ref bool cont) + { + switch (cmd) + { + case 0x93: + { + byte openTrackIndex = _sseq.Data[dataOffset++]; + int offset24bit = _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new OpenTrackCommand { Track = openTrackIndex, Offset = offset24bit }, argOverrideType); + AddTrackEvents(openTrackIndex, offset24bit); + } + break; + } + case 0x94: + { + int offset24bit = _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new JumpCommand { Offset = offset24bit }, argOverrideType); + if (!EventExists(trackIndex, offset24bit)) + { + AddEvents(trackIndex, offset24bit, ref callStackDepth); + } + } + if (!@if) + { + cont = false; + } + break; + } + case 0x95: + { + int offset24bit = _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new CallCommand { Offset = offset24bit }, argOverrideType); + } + if (callStackDepth < 3) + { + if (!EventExists(trackIndex, offset24bit)) + { + callStackDepth++; + AddEvents(trackIndex, offset24bit, ref callStackDepth); + } + } + else + { + throw new SDATTooManyNestedCallsException(trackIndex); + } + break; + } + default: throw Invalid(trackIndex, cmdOffset, cmd); + } + } + private bool HandleCmdGroup0xA0(byte trackIndex, ref int cmdOffset, byte cmd, ref ArgType argOverrideType, ref bool @if) + { + switch (cmd) + { + case 0xA0: // [New Super Mario Bros (BGM_AMB_CHIKA)] [BW2 (1917, 1918)] + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ModRandCommand(), argOverrideType); + } + argOverrideType = ArgType.Rand; + cmdOffset++; + return true; + } + case 0xA1: // [New Super Mario Bros (BGM_AMB_SABAKU)] + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ModVarCommand(), argOverrideType); + } + argOverrideType = ArgType.PlayerVar; + cmdOffset++; + return true; + } + case 0xA2: // [Mario Kart DS (75)] [BW2 (1917, 1918)] + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ModIfCommand(), argOverrideType); + } + @if = true; + cmdOffset++; + return true; + } + default: throw Invalid(trackIndex, cmdOffset, cmd); + } + } + private void HandleCmdGroup0xB0(byte trackIndex, ref int dataOffset, int cmdOffset, byte cmd, ArgType argOverrideType) + { + byte varIndex = _sseq.Data[dataOffset++]; + int arg = ReadArg(ref dataOffset, argOverrideType == ArgType.None ? ArgType.Short : argOverrideType); + switch (cmd) + { + case 0xB0: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarSetCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; + } + case 0xB1: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarAddCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; + } + case 0xB2: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarSubCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; + } + case 0xB3: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarMulCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; + } + case 0xB4: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarDivCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; + } + case 0xB5: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarShiftCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; + } + case 0xB6: // [Mario Kart DS (75)] + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarRandCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; + } + case 0xB8: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarCmpEECommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; + } + case 0xB9: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarCmpGECommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; + } + case 0xBA: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarCmpGGCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; + } + case 0xBB: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarCmpLECommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; + } + case 0xBC: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarCmpLLCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; + } + case 0xBD: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarCmpNECommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; + } + default: throw Invalid(trackIndex, cmdOffset, cmd); + } + } + private void HandleCmdGroup0xC0(byte trackIndex, ref int dataOffset, int cmdOffset, byte cmd, ArgType argOverrideType) + { + int arg = ReadArg(ref dataOffset, argOverrideType == ArgType.None ? ArgType.Byte : argOverrideType); + switch (cmd) + { + case 0xC0: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PanpotCommand { Panpot = arg }, argOverrideType); + } + break; + } + case 0xC1: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new TrackVolumeCommand { Volume = arg }, argOverrideType); + } + break; + } + case 0xC2: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PlayerVolumeCommand { Volume = arg }, argOverrideType); + } + break; + } + case 0xC3: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new TransposeCommand { Transpose = arg }, argOverrideType); + } + break; + } + case 0xC4: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PitchBendCommand { Bend = arg }, argOverrideType); + } + break; + } + case 0xC5: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PitchBendRangeCommand { Range = arg }, argOverrideType); + } + break; + } + case 0xC6: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PriorityCommand { Priority = arg }, argOverrideType); + } + break; + } + case 0xC7: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new MonophonyCommand { Mono = arg }, argOverrideType); + } + break; + } + case 0xC8: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new TieCommand { Tie = arg }, argOverrideType); + } + break; + } + case 0xC9: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PortamentoControlCommand { Portamento = arg }, argOverrideType); + } + break; + } + case 0xCA: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LFODepthCommand { Depth = arg }, argOverrideType); + } + break; + } + case 0xCB: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LFOSpeedCommand { Speed = arg }, argOverrideType); + } + break; + } + case 0xCC: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LFOTypeCommand { Type = arg }, argOverrideType); + } + break; + } + case 0xCD: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LFORangeCommand { Range = arg }, argOverrideType); + } + break; + } + case 0xCE: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PortamentoToggleCommand { Portamento = arg }, argOverrideType); + } + break; + } + case 0xCF: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PortamentoTimeCommand { Time = arg }, argOverrideType); + } + break; + } + } + } + private void HandleCmdGroup0xD0(byte trackIndex, ref int dataOffset, int cmdOffset, byte cmd, ArgType argOverrideType) + { + int arg = ReadArg(ref dataOffset, argOverrideType == ArgType.None ? ArgType.Byte : argOverrideType); + switch (cmd) + { + case 0xD0: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ForceAttackCommand { Attack = arg }, argOverrideType); + } + break; + } + case 0xD1: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ForceDecayCommand { Decay = arg }, argOverrideType); + } + break; + } + case 0xD2: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ForceSustainCommand { Sustain = arg }, argOverrideType); + } + break; + } + case 0xD3: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ForceReleaseCommand { Release = arg }, argOverrideType); + } + break; + } + case 0xD4: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LoopStartCommand { NumLoops = arg }, argOverrideType); + } + break; + } + case 0xD5: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new TrackExpressionCommand { Expression = arg }, argOverrideType); + } + break; + } + case 0xD6: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarPrintCommand { Variable = arg }, argOverrideType); + } + break; + } + default: throw Invalid(trackIndex, cmdOffset, cmd); + } + } + private void HandleCmdGroup0xE0(byte trackIndex, ref int dataOffset, int cmdOffset, byte cmd, ArgType argOverrideType) + { + int arg = ReadArg(ref dataOffset, argOverrideType == ArgType.None ? ArgType.Short : argOverrideType); + switch (cmd) + { + case 0xE0: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LFODelayCommand { Delay = arg }, argOverrideType); + } + break; + } + case 0xE1: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new TempoCommand { Tempo = arg }, argOverrideType); + } + break; + } + case 0xE3: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SweepPitchCommand { Pitch = arg }, argOverrideType); + } + break; + } + default: throw Invalid(trackIndex, cmdOffset, cmd); + } + } + private void HandleCmdGroup0xF0(byte trackIndex, ref int dataOffset, ref int callStackDepth, int cmdOffset, byte cmd, ArgType argOverrideType, ref bool @if, ref bool cont) + { + switch (cmd) + { + case 0xFC: // [HGSS(1353)] + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LoopEndCommand(), argOverrideType); + } + break; + } + case 0xFD: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ReturnCommand(), argOverrideType); + } + if (!@if && callStackDepth != 0) + { + cont = false; + callStackDepth--; + } + break; + } + case 0xFE: + { + ushort bits = (ushort)ReadArg(ref dataOffset, ArgType.Short); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new AllocTracksCommand { Tracks = bits }, argOverrideType); + } + break; + } + case 0xFF: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new FinishCommand(), argOverrideType); + } + if (!@if) + { + cont = false; + } + break; + } + default: throw Invalid(trackIndex, cmdOffset, cmd); + } + } + + public void SetTicks() + { + // TODO: (NSMB 81) (Spirit Tracks 18) does not count all ticks because the songs keep jumping backwards while changing vars and then using ModIfCommand to change events + // Should evaluate all branches if possible + MaxTicks = 0; + for (int i = 0; i < 0x10; i++) + { + ref List? evs = ref Events[i]; + evs?.Sort((e1, e2) => e1.Offset.CompareTo(e2.Offset)); + } + _player.InitEmulation(); + + bool[] done = new bool[0x10]; // We use this instead of track.Stopped just to be certain that emulating Monophony works as intended + while (Array.Exists(_player.Tracks, t => t.Allocated && t.Enabled && !done[t.Index])) + { + while (_player.TempoStack >= 240) + { + _player.TempoStack -= 240; + for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) + { + SDATTrack track = _player.Tracks[trackIndex]; + List evs = Events[trackIndex]!; + if (!track.Enabled || track.Stopped) + { + continue; + } + + track.Tick(); + while (track.Rest == 0 && !track.WaitingForNoteToFinishBeforeContinuingXD && !track.Stopped) + { + SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); + ExecuteNext(track); + if (done[trackIndex]) + { + continue; + } + + e.Ticks.Add(_player.ElapsedTicks); + bool b; + if (track.Stopped) + { + b = true; + } + else + { + SongEvent newE = evs.Single(ev => ev.Offset == track.DataOffset); + b = (track.CallStackDepth == 0 && newE.Ticks.Count > 0) // If we already counted the tick of this event and we're not looping/calling + || (track.CallStackDepth != 0 && track.CallStackLoops.All(l => l == 0) && newE.Ticks.Count > 0); // If we have "LoopStart (0)" and already counted the tick of this event + } + if (b) + { + done[trackIndex] = true; + if (_player.ElapsedTicks > MaxTicks) + { + LongestTrack = trackIndex; + MaxTicks = _player.ElapsedTicks; + } + } + } + } + _player.ElapsedTicks++; + } + _player.TempoStack += _player.Tempo; + _player.SMixer.ChannelTick(); + _player.SMixer.EmulateProcess(); + } + for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) + { + _player.Tracks[trackIndex].StopAllChannels(); + } + } + internal void SetCurTick(long ticks) + { + while (true) + { + if (_player.ElapsedTicks == ticks) + { + goto finish; + } + + while (_player.TempoStack >= 240) + { + _player.TempoStack -= 240; + for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) + { + SDATTrack track = _player.Tracks[trackIndex]; + if (track.Enabled && !track.Stopped) + { + track.Tick(); + while (track.Rest == 0 && !track.WaitingForNoteToFinishBeforeContinuingXD && !track.Stopped) + { + ExecuteNext(track); + } + } + } + _player.ElapsedTicks++; + if (_player.ElapsedTicks == ticks) + { + goto finish; + } + } + _player.TempoStack += _player.Tempo; + _player.SMixer.ChannelTick(); + _player.SMixer.EmulateProcess(); + } + finish: + for (int i = 0; i < 0x10; i++) + { + _player.Tracks[i].StopAllChannels(); + } + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs new file mode 100644 index 00000000..c9a87d02 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs @@ -0,0 +1,787 @@ +using System; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +internal sealed partial class SDATLoadedSong +{ + public void InitEmulation() + { + _player.Volume = SEQInfo.Volume; + _rand = new Random(_randSeed); + } + + public void UpdateInstrumentCache(byte voice, out string str) + { + if (_sbnk.NumInstruments <= voice) + { + str = "Empty"; + } + else + { + InstrumentType t = _sbnk.Instruments[voice].Type; + switch (t) + { + case InstrumentType.PCM: str = "PCM"; break; + case InstrumentType.PSG: str = "PSG"; break; + case InstrumentType.Noise: str = "Noise"; break; + case InstrumentType.Drum: str = "Drum"; break; + case InstrumentType.KeySplit: str = "Key Split"; break; + default: str = "Invalid " + (byte)t; break; + } + } + } + + private int ReadArg(SDATTrack track, ArgType type) + { + if (track.ArgOverrideType != ArgType.None) + { + type = track.ArgOverrideType; + } + switch (type) + { + case ArgType.Byte: + { + return _sseq.Data[track.DataOffset++]; + } + case ArgType.Short: + { + return _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8); + } + case ArgType.VarLen: + { + int read = 0, value = 0; + byte b; + do + { + b = _sseq.Data[track.DataOffset++]; + value = (value << 7) | (b & 0x7F); + read++; + } + while (read < 4 && (b & 0x80) != 0); + return value; + } + case ArgType.Rand: + { + short min = (short)(_sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8)); + short max = (short)(_sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8)); + return _rand!.Next(min, max + 1); + } + case ArgType.PlayerVar: + { + byte varIndex = _sseq.Data[track.DataOffset++]; + return _player.Vars[varIndex]; + } + default: throw new Exception(); + } + } + private void TryStartChannel(SBNK.InstrumentData inst, SDATTrack track, byte note, byte velocity, int duration, out SDATChannel? channel) + { + InstrumentType type = inst.Type; + channel = _player.SMixer.AllocateChannel(type, track); + if (channel is null) + { + return; + } + + if (track.Tie) + { + duration = -1; + } + SBNK.InstrumentData.DataParam param = inst.Param; + byte release = param.Release; + if (release == 0xFF) + { + duration = -1; + release = 0; + } + bool started = false; + switch (type) + { + case InstrumentType.PCM: + { + ushort[] info = param.Info; + SWAR.SWAV? swav = _sbnk.GetSWAV(info[1], info[0]); + if (swav is not null) + { + channel.StartPCM(swav, duration); + started = true; + } + break; + } + case InstrumentType.PSG: + { + channel.StartPSG((byte)param.Info[0], duration); + started = true; + break; + } + case InstrumentType.Noise: + { + channel.StartNoise(duration); + started = true; + break; + } + } + channel.Stop(); + if (!started) + { + return; + } + + channel.Note = note; + byte baseNote = param.BaseNote; + channel.BaseNote = type != InstrumentType.PCM && baseNote == 0x7F ? (byte)60 : baseNote; + channel.NoteVelocity = velocity; + channel.SetAttack(param.Attack); + channel.SetDecay(param.Decay); + channel.SetSustain(param.Sustain); + channel.SetRelease(release); + channel.StartingPan = (sbyte)(param.Pan - 0x40); + channel.Owner = track; + channel.Priority = track.Priority; + track.Channels.Add(channel); + } + private void PlayNote(SDATTrack track, byte note, byte velocity, int duration) + { + SDATChannel? channel = null; + if (track.Tie && track.Channels.Count != 0) + { + channel = track.Channels.Last(); + channel.Note = note; + channel.NoteVelocity = velocity; + } + else + { + SBNK.InstrumentData? inst = _sbnk.GetInstrumentData(track.Voice, note); + if (inst is not null) + { + TryStartChannel(inst, track, note, velocity, duration, out channel); + } + + if (channel is null) + { + return; + } + } + + if (track.Attack != 0xFF) + { + channel.SetAttack(track.Attack); + } + if (track.Decay != 0xFF) + { + channel.SetDecay(track.Decay); + } + if (track.Sustain != 0xFF) + { + channel.SetSustain(track.Sustain); + } + if (track.Release != 0xFF) + { + channel.SetRelease(track.Release); + } + channel.SweepPitch = track.SweepPitch; + if (track.Portamento) + { + channel.SweepPitch += (short)((track.PortamentoNote - note) << 6); // "<< 6" is "* 0x40" + } + if (track.PortamentoTime != 0) + { + channel.SweepLength = (track.PortamentoTime * track.PortamentoTime * Math.Abs(channel.SweepPitch)) >> 11; // ">> 11" is "/ 0x800" + channel.AutoSweep = true; + } + else + { + channel.SweepLength = duration; + channel.AutoSweep = false; + } + channel.SweepCounter = 0; + } + + internal void ExecuteNext(SDATTrack track) + { + bool resetOverride = true; + bool resetCmdWork = true; + byte cmd = _sseq.Data[track.DataOffset++]; + if (cmd < 0x80) + { + ExecuteNoteEvent(track, cmd); + } + else + { + switch (cmd & 0xF0) + { + case 0x80: ExecuteCmdGroup0x80(track, cmd); break; + case 0x90: ExecuteCmdGroup0x90(track, cmd); break; + case 0xA0: ExecuteCmdGroup0xA0(track, cmd, ref resetOverride, ref resetCmdWork); break; + case 0xB0: ExecuteCmdGroup0xB0(track, cmd); break; + case 0xC0: ExecuteCmdGroup0xC0(track, cmd); break; + case 0xD0: ExecuteCmdGroup0xD0(track, cmd); break; + case 0xE0: ExecuteCmdGroup0xE0(track, cmd); break; + default: ExecuteCmdGroup0xF0(track, cmd); break; + } + } + if (resetOverride) + { + track.ArgOverrideType = ArgType.None; + } + if (resetCmdWork) + { + track.DoCommandWork = true; + } + } + + private void ExecuteNoteEvent(SDATTrack track, byte cmd) + { + byte velocity = _sseq.Data[track.DataOffset++]; + int duration = ReadArg(track, ArgType.VarLen); + if (!track.DoCommandWork) + { + return; + } + + int n = cmd + track.Transpose; + if (n < 0) + { + n = 0; + } + else if (n > 0x7F) + { + n = 0x7F; + } + byte note = (byte)n; + PlayNote(track, note, velocity, duration); + track.PortamentoNote = note; + if (track.Mono) + { + track.Rest = duration; + if (duration == 0) + { + track.WaitingForNoteToFinishBeforeContinuingXD = true; + } + } + } + private void ExecuteCmdGroup0x80(SDATTrack track, byte cmd) + { + int arg = ReadArg(track, ArgType.VarLen); + + switch (cmd) + { + case 0x80: // Rest + { + if (track.DoCommandWork) + { + track.Rest = arg; + } + break; + } + case 0x81: // Program Change + { + if (track.DoCommandWork && arg <= byte.MaxValue) + { + track.Voice = (byte)arg; + } + break; + } + throw Invalid(track.Index, track.DataOffset - 1, cmd); + } + } + private void ExecuteCmdGroup0x90(SDATTrack track, byte cmd) + { + switch (cmd) + { + case 0x93: // Open Track + { + int index = _sseq.Data[track.DataOffset++]; + int offset24bit = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8) | (_sseq.Data[track.DataOffset++] << 16); + if (track.DoCommandWork && track.Index == 0) + { + SDATTrack other = _player.Tracks[index]; + if (other.Allocated && !other.Enabled) + { + other.Enabled = true; + other.DataOffset = offset24bit; + } + } + break; + } + case 0x94: // Jump + { + int offset24bit = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8) | (_sseq.Data[track.DataOffset++] << 16); + if (track.DoCommandWork) + { + track.DataOffset = offset24bit; + } + break; + } + case 0x95: // Call + { + int offset24bit = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8) | (_sseq.Data[track.DataOffset++] << 16); + if (track.DoCommandWork && track.CallStackDepth < 3) + { + track.CallStack[track.CallStackDepth] = track.DataOffset; + track.CallStackLoops[track.CallStackDepth] = byte.MaxValue; // This is only necessary for SetTicks() to deal with LoopStart (0) + track.CallStackDepth++; + track.DataOffset = offset24bit; + } + break; + } + default: throw Invalid(track.Index, track.DataOffset - 1, cmd); + } + } + private static void ExecuteCmdGroup0xA0(SDATTrack track, byte cmd, ref bool resetOverride, ref bool resetCmdWork) + { + switch (cmd) + { + case 0xA0: // Rand Mod + { + if (track.DoCommandWork) + { + track.ArgOverrideType = ArgType.Rand; + resetOverride = false; + } + break; + } + case 0xA1: // Var Mod + { + if (track.DoCommandWork) + { + track.ArgOverrideType = ArgType.PlayerVar; + resetOverride = false; + } + break; + } + case 0xA2: // If Mod + { + if (track.DoCommandWork) + { + track.DoCommandWork = track.VariableFlag; + resetCmdWork = false; + } + break; + } + default: throw Invalid(track.Index, track.DataOffset - 1, cmd); + } + } + private void ExecuteCmdGroup0xB0(SDATTrack track, byte cmd) + { + byte varIndex = _sseq.Data[track.DataOffset++]; + short mathArg = (short)ReadArg(track, ArgType.Short); + switch (cmd) + { + case 0xB0: // VarSet + { + if (track.DoCommandWork) + { + _player.Vars[varIndex] = mathArg; + } + break; + } + case 0xB1: // VarAdd + { + if (track.DoCommandWork) + { + _player.Vars[varIndex] += mathArg; + } + break; + } + case 0xB2: // VarSub + { + if (track.DoCommandWork) + { + _player.Vars[varIndex] -= mathArg; + } + break; + } + case 0xB3: // VarMul + { + if (track.DoCommandWork) + { + _player.Vars[varIndex] *= mathArg; + } + break; + } + case 0xB4: // VarDiv + { + if (track.DoCommandWork && mathArg != 0) + { + _player.Vars[varIndex] /= mathArg; + } + break; + } + case 0xB5: // VarShift + { + if (track.DoCommandWork) + { + ref short v = ref _player.Vars[varIndex]; + v = mathArg < 0 ? (short)(v >> -mathArg) : (short)(v << mathArg); + } + break; + } + case 0xB6: // VarRand + { + if (track.DoCommandWork) + { + bool negate = false; + if (mathArg < 0) + { + negate = true; + mathArg = (short)-mathArg; + } + short val = (short)_rand!.Next(mathArg + 1); + if (negate) + { + val = (short)-val; + } + _player.Vars[varIndex] = val; + } + break; + } + case 0xB8: // VarCmpEE + { + if (track.DoCommandWork) + { + track.VariableFlag = _player.Vars[varIndex] == mathArg; + } + break; + } + case 0xB9: // VarCmpGE + { + if (track.DoCommandWork) + { + track.VariableFlag = _player.Vars[varIndex] >= mathArg; + } + break; + } + case 0xBA: // VarCmpGG + { + if (track.DoCommandWork) + { + track.VariableFlag = _player.Vars[varIndex] > mathArg; + } + break; + } + case 0xBB: // VarCmpLE + { + if (track.DoCommandWork) + { + track.VariableFlag = _player.Vars[varIndex] <= mathArg; + } + break; + } + case 0xBC: // VarCmpLL + { + if (track.DoCommandWork) + { + track.VariableFlag = _player.Vars[varIndex] < mathArg; + } + break; + } + case 0xBD: // VarCmpNE + { + if (track.DoCommandWork) + { + track.VariableFlag = _player.Vars[varIndex] != mathArg; + } + break; + } + default: throw Invalid(track.Index, track.DataOffset - 1, cmd); + } + } + private void ExecuteCmdGroup0xC0(SDATTrack track, byte cmd) + { + int cmdArg = ReadArg(track, ArgType.Byte); + switch (cmd) + { + case 0xC0: // Panpot + { + if (track.DoCommandWork) + { + track.Panpot = (sbyte)(cmdArg - 0x40); + } + break; + } + case 0xC1: // Track Volume + { + if (track.DoCommandWork) + { + track.Volume = (byte)cmdArg; + } + break; + } + case 0xC2: // Player Volume + { + if (track.DoCommandWork) + { + _player.Volume = (byte)cmdArg; + } + break; + } + case 0xC3: // Transpose + { + if (track.DoCommandWork) + { + track.Transpose = (sbyte)cmdArg; + } + break; + } + case 0xC4: // Pitch Bend + { + if (track.DoCommandWork) + { + track.PitchBend = (sbyte)cmdArg; + } + break; + } + case 0xC5: // Pitch Bend Range + { + if (track.DoCommandWork) + { + track.PitchBendRange = (byte)cmdArg; + } + break; + } + case 0xC6: // Priority + { + if (track.DoCommandWork) + { + track.Priority = (byte)(_player.Priority + (byte)cmdArg); + } + break; + } + case 0xC7: // Mono + { + if (track.DoCommandWork) + { + track.Mono = cmdArg == 1; + } + break; + } + case 0xC8: // Tie + { + if (track.DoCommandWork) + { + track.Tie = cmdArg == 1; + track.StopAllChannels(); + } + break; + } + case 0xC9: // Portamento Control + { + if (track.DoCommandWork) + { + int k = cmdArg + track.Transpose; + if (k < 0) + { + k = 0; + } + else if (k > 0x7F) + { + k = 0x7F; + } + track.PortamentoNote = (byte)k; + track.Portamento = true; + } + break; + } + case 0xCA: // LFO Depth + { + if (track.DoCommandWork) + { + track.LFODepth = (byte)cmdArg; + } + break; + } + case 0xCB: // LFO Speed + { + if (track.DoCommandWork) + { + track.LFOSpeed = (byte)cmdArg; + } + break; + } + case 0xCC: // LFO Type + { + if (track.DoCommandWork) + { + track.LFOType = (LFOType)cmdArg; + } + break; + } + case 0xCD: // LFO Range + { + if (track.DoCommandWork) + { + track.LFORange = (byte)cmdArg; + } + break; + } + case 0xCE: // Portamento Toggle + { + if (track.DoCommandWork) + { + track.Portamento = cmdArg == 1; + } + break; + } + case 0xCF: // Portamento Time + { + if (track.DoCommandWork) + { + track.PortamentoTime = (byte)cmdArg; + } + break; + } + } + } + private void ExecuteCmdGroup0xD0(SDATTrack track, byte cmd) + { + int cmdArg = ReadArg(track, ArgType.Byte); + switch (cmd) + { + case 0xD0: // Forced Attack + { + if (track.DoCommandWork) + { + track.Attack = (byte)cmdArg; + } + break; + } + case 0xD1: // Forced Decay + { + if (track.DoCommandWork) + { + track.Decay = (byte)cmdArg; + } + break; + } + case 0xD2: // Forced Sustain + { + if (track.DoCommandWork) + { + track.Sustain = (byte)cmdArg; + } + break; + } + case 0xD3: // Forced Release + { + if (track.DoCommandWork) + { + track.Release = (byte)cmdArg; + } + break; + } + case 0xD4: // Loop Start + { + if (track.DoCommandWork && track.CallStackDepth < 3) + { + track.CallStack[track.CallStackDepth] = track.DataOffset; + track.CallStackLoops[track.CallStackDepth] = (byte)cmdArg; + track.CallStackDepth++; + } + break; + } + case 0xD5: // Track Expression + { + if (track.DoCommandWork) + { + track.Expression = (byte)cmdArg; + } + break; + } + default: throw Invalid(track.Index, track.DataOffset - 1, cmd); + } + } + private void ExecuteCmdGroup0xE0(SDATTrack track, byte cmd) + { + int cmdArg = ReadArg(track, ArgType.Short); + switch (cmd) + { + case 0xE0: // LFO Delay + { + if (track.DoCommandWork) + { + track.LFODelay = (ushort)cmdArg; + } + break; + } + case 0xE1: // Tempo + { + if (track.DoCommandWork) + { + _player.Tempo = (ushort)cmdArg; + } + break; + } + case 0xE3: // Sweep Pitch + { + if (track.DoCommandWork) + { + track.SweepPitch = (short)cmdArg; + } + break; + } + } + } + private void ExecuteCmdGroup0xF0(SDATTrack track, byte cmd) + { + switch (cmd) + { + case 0xFC: // Loop End + { + if (track.DoCommandWork && track.CallStackDepth != 0) + { + byte count = track.CallStackLoops[track.CallStackDepth - 1]; + if (count != 0) + { + count--; + track.CallStackLoops[track.CallStackDepth - 1] = count; + if (count == 0) + { + track.CallStackDepth--; + break; + } + } + track.DataOffset = track.CallStack[track.CallStackDepth - 1]; + } + break; + } + case 0xFD: // Return + { + if (track.DoCommandWork && track.CallStackDepth != 0) + { + track.CallStackDepth--; + track.DataOffset = track.CallStack[track.CallStackDepth]; + track.CallStackLoops[track.CallStackDepth] = 0; // This is only necessary for SetTicks() to deal with LoopStart (0) + } + break; + } + case 0xFE: // Alloc Tracks + { + // Must be in the beginning of the first track to work + if (track.DoCommandWork && track.Index == 0 && track.DataOffset == 1) // == 1 because we read cmd already + { + // Track 1 enabled = bit 1 set, Track 4 enabled = bit 4 set, etc + int trackBits = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8); + for (int i = 0; i < 0x10; i++) + { + if ((trackBits & (1 << i)) != 0) + { + _player.Tracks[i].Allocated = true; + } + } + } + break; + } + case 0xFF: // Finish + { + if (track.DoCommandWork) + { + track.Stopped = true; + } + break; + } + default: throw Invalid(track.Index, track.DataOffset - 1, cmd); + } + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATMixer.cs b/VG Music Studio - Core/NDS/SDAT/SDATMixer.cs new file mode 100644 index 00000000..e516e150 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATMixer.cs @@ -0,0 +1,244 @@ +using Kermalis.VGMusicStudio.Core.Util; +using NAudio.Wave; +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +public sealed class SDATMixer : Mixer +{ + private readonly float _samplesReciprocal; + private readonly int _samplesPerBuffer; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; + + internal SDATChannel[] Channels; + private readonly BufferedWaveProvider _buffer; + + protected override WaveFormat WaveFormat => _buffer.WaveFormat; + + internal SDATMixer() + { + // The sampling frequency of the mixer is 1.04876 MHz with an amplitude resolution of 24 bits, but the sampling frequency after mixing with PWM modulation is 32.768 kHz with an amplitude resolution of 10 bits. + // - gbatek + // I'm not using either of those because the samples per buffer leads to an overflow eventually + const int sampleRate = 65456; + _samplesPerBuffer = 341; // TODO + _samplesReciprocal = 1f / _samplesPerBuffer; + + Channels = new SDATChannel[0x10]; + for (byte i = 0; i < 0x10; i++) + { + Channels[i] = new SDATChannel(i); + } + + _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + { + DiscardOnBufferOverflow = true, + BufferLength = _samplesPerBuffer * 64 + }; + Init(_buffer); + } + + private static readonly int[] _pcmChanOrder = new int[] { 4, 5, 6, 7, 2, 0, 3, 1, 8, 9, 10, 11, 14, 12, 15, 13 }; + private static readonly int[] _psgChanOrder = new int[] { 8, 9, 10, 11, 12, 13 }; + private static readonly int[] _noiseChanOrder = new int[] { 14, 15 }; + internal SDATChannel? AllocateChannel(InstrumentType type, SDATTrack track) + { + int[] allowedChannels; + switch (type) + { + case InstrumentType.PCM: allowedChannels = _pcmChanOrder; break; + case InstrumentType.PSG: allowedChannels = _psgChanOrder; break; + case InstrumentType.Noise: allowedChannels = _noiseChanOrder; break; + default: return null; + } + SDATChannel? nChan = null; + for (int i = 0; i < allowedChannels.Length; i++) + { + SDATChannel c = Channels[allowedChannels[i]]; + if (nChan is not null && c.Priority >= nChan.Priority && (c.Priority != nChan.Priority || nChan.Volume <= c.Volume)) + { + continue; + } + nChan = c; + } + if (nChan is null || track.Priority < nChan.Priority) + { + return null; + } + return nChan; + } + + internal void ChannelTick() + { + for (int i = 0; i < 0x10; i++) + { + SDATChannel chan = Channels[i]; + if (chan.Owner is null) + { + continue; + } + + chan.StepEnvelope(); + if (chan.NoteDuration == 0 && !chan.Owner.WaitingForNoteToFinishBeforeContinuingXD) + { + chan.Priority = 1; + chan.State = EnvelopeState.Release; + } + int vol = SDATUtils.SustainTable[chan.NoteVelocity] + chan.Velocity + chan.Owner.GetVolume(); + int pitch = ((chan.Note - chan.BaseNote) << 6) + chan.SweepMain() + chan.Owner.GetPitch(); // "<< 6" is "* 0x40" + int pan = 0; + chan.LFOTick(); + switch (chan.LFOType) + { + case LFOType.Pitch: pitch += chan.LFOParam; break; + case LFOType.Volume: vol += chan.LFOParam; break; + case LFOType.Panpot: pan += chan.LFOParam; break; + } + if (chan.State == EnvelopeState.Release && vol <= -92544) + { + chan.Stop(); + } + else + { + chan.Volume = SDATUtils.GetChannelVolume(vol); + chan.Timer = SDATUtils.GetChannelTimer(chan.BaseTimer, pitch); + int p = chan.StartingPan + chan.Owner.GetPan() + pan; + if (p < -0x40) + { + p = -0x40; + } + else if (p > 0x3F) + { + p = 0x3F; + } + chan.Pan = (sbyte)p; + } + } + } + + internal void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * 192); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * 192); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal bool IsFading() + { + return _isFading; + } + internal bool IsFadeDone() + { + return _isFading && _fadeMicroFramesLeft == 0; + } + internal void ResetFade() + { + _isFading = false; + _fadeMicroFramesLeft = 0; + } + + internal void EmulateProcess() + { + for (int i = 0; i < _samplesPerBuffer; i++) + { + for (int j = 0; j < 0x10; j++) + { + SDATChannel chan = Channels[j]; + if (chan.Owner is not null) + { + chan.EmulateProcess(); + } + } + } + } + private readonly byte[] _b = new byte[4]; + internal void Process(bool output, bool recording) + { + float masterStep; + float masterLevel; + if (_isFading && _fadeMicroFramesLeft == 0) + { + masterStep = 0; + masterLevel = 0; + } + else + { + float fromMaster = 1f; + float toMaster = 1f; + if (_fadeMicroFramesLeft > 0) + { + const float scale = 10f / 6f; + fromMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadePos += _fadeStepPerMicroframe; + toMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadeMicroFramesLeft--; + } + masterStep = (toMaster - fromMaster) * _samplesReciprocal; + masterLevel = fromMaster; + } + for (int i = 0; i < _samplesPerBuffer; i++) + { + int left = 0, + right = 0; + for (int j = 0; j < 0x10; j++) + { + SDATChannel chan = Channels[j]; + if (chan.Owner is null) + { + continue; + } + + bool muted = Mutes[chan.Owner.Index]; // Get mute first because chan.Process() can call chan.Stop() which sets chan.Owner to null + chan.Process(out short channelLeft, out short channelRight); + if (!muted) + { + left += channelLeft; + right += channelRight; + } + } + float f = left * masterLevel; + if (f < short.MinValue) + { + f = short.MinValue; + } + else if (f > short.MaxValue) + { + f = short.MaxValue; + } + left = (int)f; + _b[0] = (byte)left; + _b[1] = (byte)(left >> 8); + f = right * masterLevel; + if (f < short.MinValue) + { + f = short.MinValue; + } + else if (f > short.MaxValue) + { + f = short.MaxValue; + } + right = (int)f; + _b[2] = (byte)right; + _b[3] = (byte)(right >> 8); + masterLevel += masterStep; + if (output) + { + _buffer.AddSamples(_b, 0, 4); + } + if (recording) + { + _waveWriter!.Write(_b, 0, 4); + } + } + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs b/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs new file mode 100644 index 00000000..97fb6ae7 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +public sealed class SDATPlayer : Player +{ + protected override string Name => "SDAT Player"; + + internal readonly byte Priority = 0x40; + internal readonly short[] Vars = new short[0x20]; // 16 player variables, then 16 global variables + internal readonly SDATTrack[] Tracks = new SDATTrack[0x10]; + private readonly string?[] _voiceTypeCache = new string?[256]; + internal readonly SDATConfig Config; + internal readonly SDATMixer SMixer; + private SDATLoadedSong? _loadedSong; + + internal byte Volume; + internal ushort Tempo; + internal int TempoStack; + private long _elapsedLoops; + + private ushort? _prevBank; + + public override ILoadedSong? LoadedSong => _loadedSong; + protected override Mixer Mixer => SMixer; + + internal SDATPlayer(SDATConfig config, SDATMixer mixer) + : base(192) + { + Config = config; + SMixer = mixer; + + for (byte i = 0; i < 0x10; i++) + { + Tracks[i] = new SDATTrack(i, this); + } + } + + public override void LoadSong(int index) + { + if (_loadedSong is not null) + { + _loadedSong = null; + } + + SDAT.INFO.SequenceInfo? seqInfo = Config.SDAT.INFOBlock.SequenceInfos.Entries[index]; + if (seqInfo is null) + { + return; + } + + // If there's an exception, this will remain null + _loadedSong = new SDATLoadedSong(this, seqInfo); + _loadedSong.SetTicks(); + + ushort? old = _prevBank; + ushort nu = _loadedSong.SEQInfo.Bank; + if (old != nu) + { + _prevBank = nu; + Array.Clear(_voiceTypeCache); + } + } + public override void UpdateSongState(SongState info) + { + info.Tempo = Tempo; + for (int i = 0; i < 0x10; i++) + { + SDATTrack track = Tracks[i]; + if (track.Enabled) + { + track.UpdateSongState(info.Tracks[i], _loadedSong!, _voiceTypeCache); + } + } + } + internal override void InitEmulation() + { + Tempo = 120; // Confirmed: default tempo is 120 (MKDS 75) + TempoStack = 0; + _elapsedLoops = 0; + ElapsedTicks = 0; + SMixer.ResetFade(); + _loadedSong!.InitEmulation(); + for (int i = 0; i < 0x10; i++) + { + Tracks[i].Init(); + } + // Initialize player and global variables. Global variables should not have a global effect in this program. + for (int i = 0; i < 0x20; i++) + { + Vars[i] = i % 8 == 0 ? short.MaxValue : (short)0; + } + } + protected override void SetCurTick(long ticks) + { + _loadedSong!.SetCurTick(ticks); + } + protected override void OnStopped() + { + for (int i = 0; i < 0x10; i++) + { + Tracks[i].StopAllChannels(); + } + } + + protected override bool Tick(bool playing, bool recording) + { + bool allDone = false; + while (!allDone && TempoStack >= 240) + { + TempoStack -= 240; + allDone = true; + for (int i = 0; i < 0x10; i++) + { + TickTrack(i, ref allDone); + } + if (SMixer.IsFadeDone()) + { + allDone = true; + } + } + if (!allDone) + { + TempoStack += Tempo; + } + for (int i = 0; i < 0x10; i++) + { + SDATTrack track = Tracks[i]; + if (track.Enabled) + { + track.UpdateChannels(); + } + } + SMixer.ChannelTick(); + SMixer.Process(playing, recording); + return allDone; + } + private void TickTrack(int trackIndex, ref bool allDone) + { + SDATTrack track = Tracks[trackIndex]; + if (!track.Enabled) + { + return; + } + + track.Tick(); + SDATLoadedSong s = _loadedSong!; + while (track.Rest == 0 && !track.WaitingForNoteToFinishBeforeContinuingXD && !track.Stopped) + { + s.ExecuteNext(track); + } + if (trackIndex == s.LongestTrack) + { + HandleTicksAndLoop(s, track); + } + if (!track.Stopped || track.Channels.Count != 0) + { + allDone = false; + } + } + private void HandleTicksAndLoop(SDATLoadedSong s, SDATTrack track) + { + if (ElapsedTicks != s.MaxTicks) + { + ElapsedTicks++; + return; + } + + // Track reached the detected end, update loops/ticks accordingly + if (track.Stopped) + { + return; + } + + _elapsedLoops++; + //UpdateElapsedTicksAfterLoop(s.Events[track.Index], track.DataOffset, track.Rest); // TODO + // Prevent crashes with songs that don't load all ticks yet (See SetTicks()) + List evs = s.Events[track.Index]!; + for (int i = 0; i < evs.Count; i++) + { + SongEvent ev = evs[i]; + if (ev.Offset == track.DataOffset) + { + //ElapsedTicks = ev.Ticks[0] - track.Rest; + ElapsedTicks = ev.Ticks.Count == 0 ? 0 : ev.Ticks[0] - track.Rest; + break; + } + } + if (ShouldFadeOut && _elapsedLoops > NumLoops && !SMixer.IsFading()) + { + SMixer.BeginFadeOut(); + } + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATTrack.cs b/VG Music Studio - Core/NDS/SDAT/SDATTrack.cs new file mode 100644 index 00000000..87be5b51 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATTrack.cs @@ -0,0 +1,248 @@ +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +internal sealed class SDATTrack +{ + public readonly byte Index; + private readonly SDATPlayer _player; + + public bool Allocated; + public bool Enabled; + public bool Stopped; + public bool Tie; + public bool Mono; + public bool Portamento; + public bool WaitingForNoteToFinishBeforeContinuingXD; // TODO: Is this necessary? + public byte Voice; + public byte Priority; + public byte Volume; + public byte Expression; + public byte PitchBendRange; + public byte LFORange; + public byte LFOSpeed; + public byte LFODepth; + public ushort LFODelay; + public ushort LFOPhase; + public int LFOParam; + public ushort LFODelayCount; + public LFOType LFOType; + public sbyte PitchBend; + public sbyte Panpot; + public sbyte Transpose; + public byte Attack; + public byte Decay; + public byte Sustain; + public byte Release; + public byte PortamentoNote; + public byte PortamentoTime; + public short SweepPitch; + public int Rest; + public readonly int[] CallStack; + public readonly byte[] CallStackLoops; + public byte CallStackDepth; + public int DataOffset; + public bool VariableFlag; // Set by variable commands (0xB0 - 0xBD) + public bool DoCommandWork; + public ArgType ArgOverrideType; + + public readonly List Channels = new(0x10); + + public SDATTrack(byte i, SDATPlayer player) + { + Index = i; + _player = player; + + CallStack = new int[3]; + CallStackLoops = new byte[3]; + } + public void Init() + { + Stopped = Tie = WaitingForNoteToFinishBeforeContinuingXD = Portamento = false; + Allocated = Enabled = Index == 0; + DataOffset = 0; + ArgOverrideType = ArgType.None; + Mono = VariableFlag = DoCommandWork = true; + CallStackDepth = 0; + Voice = LFODepth = 0; + PitchBend = Panpot = Transpose = 0; + LFOPhase = LFODelay = LFODelayCount = 0; + LFORange = 1; + LFOSpeed = 0x10; + Priority = (byte)(_player.Priority + 0x40); + Volume = Expression = 0x7F; + Attack = Decay = Sustain = Release = 0xFF; + PitchBendRange = 2; + PortamentoNote = 60; + PortamentoTime = 0; + SweepPitch = 0; + LFOType = LFOType.Pitch; + Rest = 0; + StopAllChannels(); + } + public void LFOTick() + { + if (Channels.Count == 0) + { + LFOPhase = 0; + LFOParam = 0; + LFODelayCount = LFODelay; + } + else + { + if (LFODelayCount > 0) + { + LFODelayCount--; + LFOPhase = 0; + } + else + { + int param = LFORange * SDATUtils.Sin(LFOPhase >> 8) * LFODepth; + if (LFOType == LFOType.Volume) + { + param = (int)(((long)param * 60) >> 14); + } + else + { + param >>= 8; + } + LFOParam = param; + int counter = LFOPhase + (LFOSpeed << 6); // "<< 6" is "* 0x40" + while (counter >= 0x8000) + { + counter -= 0x8000; + } + LFOPhase = (ushort)counter; + } + } + } + public void Tick() + { + if (Rest > 0) + { + Rest--; + } + if (Channels.Count != 0) + { + // TickNotes: + for (int i = 0; i < Channels.Count; i++) + { + SDATChannel c = Channels[i]; + if (c.NoteDuration > 0) + { + c.NoteDuration--; + } + if (!c.AutoSweep && c.SweepCounter < c.SweepLength) + { + c.SweepCounter++; + } + } + } + else + { + WaitingForNoteToFinishBeforeContinuingXD = false; + } + } + public void UpdateChannels() + { + for (int i = 0; i < Channels.Count; i++) + { + SDATChannel c = Channels[i]; + c.LFOType = LFOType; + c.LFOSpeed = LFOSpeed; + c.LFODepth = LFODepth; + c.LFORange = LFORange; + c.LFODelay = LFODelay; + } + } + + public void StopAllChannels() + { + SDATChannel[] chans = Channels.ToArray(); + for (int i = 0; i < chans.Length; i++) + { + chans[i].Stop(); + } + } + + public int GetPitch() + { + //int lfo = LFOType == LFOType.Pitch ? LFOParam : 0; + int lfo = 0; + return (PitchBend * PitchBendRange / 2) + lfo; + } + public int GetVolume() + { + //int lfo = LFOType == LFOType.Volume ? LFOParam : 0; + int lfo = 0; + return SDATUtils.SustainTable[_player.Volume] + SDATUtils.SustainTable[Volume] + SDATUtils.SustainTable[Expression] + lfo; + } + public sbyte GetPan() + { + //int lfo = LFOType == LFOType.Panpot ? LFOParam : 0; + int lfo = 0; + int p = Panpot + lfo; + if (p < -0x40) + { + p = -0x40; + } + else if (p > 0x3F) + { + p = 0x3F; + } + return (sbyte)p; + } + + public void UpdateSongState(SongState.Track tin, SDATLoadedSong loadedSong, string?[] voiceTypeCache) + { + tin.Position = DataOffset; + tin.Rest = Rest; + tin.Voice = Voice; + tin.LFO = LFODepth * LFORange; + ref string? cache = ref voiceTypeCache[Voice]; + if (cache is null) + { + loadedSong.UpdateInstrumentCache(Voice, out cache); + } + tin.Type = cache; + tin.Volume = Volume; + tin.PitchBend = GetPitch(); + tin.Extra = Portamento ? PortamentoTime : (byte)0; + tin.Panpot = GetPan(); + + SDATChannel[] channels = Channels.ToArray(); + if (channels.Length == 0) + { + tin.Keys[0] = byte.MaxValue; + tin.LeftVolume = 0f; + tin.RightVolume = 0f; + } + else + { + int numKeys = 0; + float left = 0f; + float right = 0f; + for (int j = 0; j < channels.Length; j++) + { + SDATChannel c = channels[j]; + if (c.State != EnvelopeState.Release) + { + tin.Keys[numKeys++] = c.Note; + } + float a = (float)(-c.Pan + 0x40) / 0x80 * c.Volume / 0x7F; + if (a > left) + { + left = a; + } + a = (float)(c.Pan + 0x40) / 0x80 * c.Volume / 0x7F; + if (a > right) + { + right = a; + } + } + tin.Keys[numKeys] = byte.MaxValue; // There's no way for numKeys to be after the last index in the array + tin.LeftVolume = left; + tin.RightVolume = right; + } + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATUtils.cs b/VG Music Studio - Core/NDS/SDAT/SDATUtils.cs new file mode 100644 index 00000000..e86d328a --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATUtils.cs @@ -0,0 +1,344 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +internal static class SDATUtils +{ + public static ReadOnlySpan AttackTable => new byte[128] + { + 255, 254, 253, 252, 251, 250, 249, 248, + 247, 246, 245, 244, 243, 242, 241, 240, + 239, 238, 237, 236, 235, 234, 233, 232, + 231, 230, 229, 228, 227, 226, 225, 224, + 223, 222, 221, 220, 219, 218, 217, 216, + 215, 214, 213, 212, 211, 210, 209, 208, + 207, 206, 205, 204, 203, 202, 201, 200, + 199, 198, 197, 196, 195, 194, 193, 192, + 191, 190, 189, 188, 187, 186, 185, 184, + 183, 182, 181, 180, 179, 178, 177, 176, + 175, 174, 173, 172, 171, 170, 169, 168, + 167, 166, 165, 164, 163, 162, 161, 160, + 159, 158, 157, 156, 155, 154, 153, 152, + 151, 150, 149, 148, 147, 143, 137, 132, + 127, 123, 116, 109, 100, 92, 84, 73, + 63, 51, 38, 26, 14, 5, 1, 0, + }; + public static ReadOnlySpan DecayTable => new ushort[128] + { + 1, 3, 5, 7, 9, 11, 13, 15, + 17, 19, 21, 23, 25, 27, 29, 31, + 33, 35, 37, 39, 41, 43, 45, 47, + 49, 51, 53, 55, 57, 59, 61, 63, + 65, 67, 69, 71, 73, 75, 77, 79, + 81, 83, 85, 87, 89, 91, 93, 95, + 97, 99, 101, 102, 104, 105, 107, 108, + 110, 111, 113, 115, 116, 118, 120, 122, + 124, 126, 128, 130, 132, 135, 137, 140, + 142, 145, 148, 151, 154, 157, 160, 163, + 167, 171, 175, 179, 183, 187, 192, 197, + 202, 208, 213, 219, 226, 233, 240, 248, + 256, 265, 274, 284, 295, 307, 320, 334, + 349, 366, 384, 404, 427, 452, 480, 512, + 549, 591, 640, 698, 768, 853, 960, 1097, + 1280, 1536, 1920, 2560, 3840, 7680, 15360, 65535, + }; + public static ReadOnlySpan SustainTable => new int[128] + { + -92544, -92416, -92288, -83328, -76928, -71936, -67840, -64384, + -61440, -58880, -56576, -54400, -52480, -50688, -49024, -47488, + -46080, -44672, -43392, -42240, -41088, -40064, -39040, -38016, + -36992, -36096, -35328, -34432, -33664, -32896, -32128, -31360, + -30592, -29952, -29312, -28672, -28032, -27392, -26880, -26240, + -25728, -25088, -24576, -24064, -23552, -23040, -22528, -22144, + -21632, -21120, -20736, -20224, -19840, -19456, -19072, -18560, + -18176, -17792, -17408, -17024, -16640, -16256, -16000, -15616, + -15232, -14848, -14592, -14208, -13952, -13568, -13184, -12928, + -12672, -12288, -12032, -11648, -11392, -11136, -10880, -10496, + -10240, -9984, -9728, -9472, -9216, -8960, -8704, -8448, + -8192, -7936, -7680, -7424, -7168, -6912, -6656, -6400, + -6272, -6016, -5760, -5504, -5376, -5120, -4864, -4608, + -4480, -4224, -3968, -3840, -3584, -3456, -3200, -2944, + -2816, -2560, -2432, -2176, -2048, -1792, -1664, -1408, + -1280, -1024, -896, -768, -512, -384, -128, 0, + }; + + private static ReadOnlySpan SinTable => new sbyte[33] + { + 000, 006, 012, 019, 025, 031, 037, 043, + 049, 054, 060, 065, 071, 076, 081, 085, + 090, 094, 098, 102, 106, 109, 112, 115, + 117, 120, 122, 123, 125, 126, 126, 127, + 127, + }; + public static int Sin(int index) + { + if (index < 0x20) + { + return SinTable[index]; + } + if (index < 0x40) + { + return SinTable[0x20 - (index - 0x20)]; + } + if (index < 0x60) + { + return -SinTable[index - 0x40]; + } + // < 0x80 + return -SinTable[0x20 - (index - 0x60)]; + } + + private static ReadOnlySpan PitchTable => new ushort[768] + { + 0, 59, 118, 178, 237, 296, 356, 415, + 475, 535, 594, 654, 714, 773, 833, 893, + 953, 1013, 1073, 1134, 1194, 1254, 1314, 1375, + 1435, 1496, 1556, 1617, 1677, 1738, 1799, 1859, + 1920, 1981, 2042, 2103, 2164, 2225, 2287, 2348, + 2409, 2471, 2532, 2593, 2655, 2716, 2778, 2840, + 2902, 2963, 3025, 3087, 3149, 3211, 3273, 3335, + 3397, 3460, 3522, 3584, 3647, 3709, 3772, 3834, + 3897, 3960, 4022, 4085, 4148, 4211, 4274, 4337, + 4400, 4463, 4526, 4590, 4653, 4716, 4780, 4843, + 4907, 4971, 5034, 5098, 5162, 5226, 5289, 5353, + 5417, 5481, 5546, 5610, 5674, 5738, 5803, 5867, + 5932, 5996, 6061, 6125, 6190, 6255, 6320, 6384, + 6449, 6514, 6579, 6645, 6710, 6775, 6840, 6906, + 6971, 7037, 7102, 7168, 7233, 7299, 7365, 7431, + 7496, 7562, 7628, 7694, 7761, 7827, 7893, 7959, + 8026, 8092, 8159, 8225, 8292, 8358, 8425, 8492, + 8559, 8626, 8693, 8760, 8827, 8894, 8961, 9028, + 9096, 9163, 9230, 9298, 9366, 9433, 9501, 9569, + 9636, 9704, 9772, 9840, 9908, 9976, 10045, 10113, + 10181, 10250, 10318, 10386, 10455, 10524, 10592, 10661, + 10730, 10799, 10868, 10937, 11006, 11075, 11144, 11213, + 11283, 11352, 11421, 11491, 11560, 11630, 11700, 11769, + 11839, 11909, 11979, 12049, 12119, 12189, 12259, 12330, + 12400, 12470, 12541, 12611, 12682, 12752, 12823, 12894, + 12965, 13036, 13106, 13177, 13249, 13320, 13391, 13462, + 13533, 13605, 13676, 13748, 13819, 13891, 13963, 14035, + 14106, 14178, 14250, 14322, 14394, 14467, 14539, 14611, + 14684, 14756, 14829, 14901, 14974, 15046, 15119, 15192, + 15265, 15338, 15411, 15484, 15557, 15630, 15704, 15777, + 15850, 15924, 15997, 16071, 16145, 16218, 16292, 16366, + 16440, 16514, 16588, 16662, 16737, 16811, 16885, 16960, + 17034, 17109, 17183, 17258, 17333, 17408, 17483, 17557, + 17633, 17708, 17783, 17858, 17933, 18009, 18084, 18160, + 18235, 18311, 18387, 18462, 18538, 18614, 18690, 18766, + 18842, 18918, 18995, 19071, 19147, 19224, 19300, 19377, + 19454, 19530, 19607, 19684, 19761, 19838, 19915, 19992, + 20070, 20147, 20224, 20302, 20379, 20457, 20534, 20612, + 20690, 20768, 20846, 20924, 21002, 21080, 21158, 21236, + 21315, 21393, 21472, 21550, 21629, 21708, 21786, 21865, + 21944, 22023, 22102, 22181, 22260, 22340, 22419, 22498, + 22578, 22658, 22737, 22817, 22897, 22977, 23056, 23136, + 23216, 23297, 23377, 23457, 23537, 23618, 23698, 23779, + 23860, 23940, 24021, 24102, 24183, 24264, 24345, 24426, + 24507, 24589, 24670, 24752, 24833, 24915, 24996, 25078, + 25160, 25242, 25324, 25406, 25488, 25570, 25652, 25735, + 25817, 25900, 25982, 26065, 26148, 26230, 26313, 26396, + 26479, 26562, 26645, 26729, 26812, 26895, 26979, 27062, + 27146, 27230, 27313, 27397, 27481, 27565, 27649, 27733, + 27818, 27902, 27986, 28071, 28155, 28240, 28324, 28409, + 28494, 28579, 28664, 28749, 28834, 28919, 29005, 29090, + 29175, 29261, 29346, 29432, 29518, 29604, 29690, 29776, + 29862, 29948, 30034, 30120, 30207, 30293, 30380, 30466, + 30553, 30640, 30727, 30814, 30900, 30988, 31075, 31162, + 31249, 31337, 31424, 31512, 31599, 31687, 31775, 31863, + 31951, 32039, 32127, 32215, 32303, 32392, 32480, 32568, + 32657, 32746, 32834, 32923, 33012, 33101, 33190, 33279, + 33369, 33458, 33547, 33637, 33726, 33816, 33906, 33995, + 34085, 34175, 34265, 34355, 34446, 34536, 34626, 34717, + 34807, 34898, 34988, 35079, 35170, 35261, 35352, 35443, + 35534, 35626, 35717, 35808, 35900, 35991, 36083, 36175, + 36267, 36359, 36451, 36543, 36635, 36727, 36820, 36912, + 37004, 37097, 37190, 37282, 37375, 37468, 37561, 37654, + 37747, 37841, 37934, 38028, 38121, 38215, 38308, 38402, + 38496, 38590, 38684, 38778, 38872, 38966, 39061, 39155, + 39250, 39344, 39439, 39534, 39629, 39724, 39819, 39914, + 40009, 40104, 40200, 40295, 40391, 40486, 40582, 40678, + 40774, 40870, 40966, 41062, 41158, 41255, 41351, 41448, + 41544, 41641, 41738, 41835, 41932, 42029, 42126, 42223, + 42320, 42418, 42515, 42613, 42710, 42808, 42906, 43004, + 43102, 43200, 43298, 43396, 43495, 43593, 43692, 43790, + 43889, 43988, 44087, 44186, 44285, 44384, 44483, 44583, + 44682, 44781, 44881, 44981, 45081, 45180, 45280, 45381, + 45481, 45581, 45681, 45782, 45882, 45983, 46083, 46184, + 46285, 46386, 46487, 46588, 46690, 46791, 46892, 46994, + 47095, 47197, 47299, 47401, 47503, 47605, 47707, 47809, + 47912, 48014, 48117, 48219, 48322, 48425, 48528, 48631, + 48734, 48837, 48940, 49044, 49147, 49251, 49354, 49458, + 49562, 49666, 49770, 49874, 49978, 50082, 50187, 50291, + 50396, 50500, 50605, 50710, 50815, 50920, 51025, 51131, + 51236, 51341, 51447, 51552, 51658, 51764, 51870, 51976, + 52082, 52188, 52295, 52401, 52507, 52614, 52721, 52827, + 52934, 53041, 53148, 53256, 53363, 53470, 53578, 53685, + 53793, 53901, 54008, 54116, 54224, 54333, 54441, 54549, + 54658, 54766, 54875, 54983, 55092, 55201, 55310, 55419, + 55529, 55638, 55747, 55857, 55966, 56076, 56186, 56296, + 56406, 56516, 56626, 56736, 56847, 56957, 57068, 57179, + 57289, 57400, 57511, 57622, 57734, 57845, 57956, 58068, + 58179, 58291, 58403, 58515, 58627, 58739, 58851, 58964, + 59076, 59189, 59301, 59414, 59527, 59640, 59753, 59866, + 59979, 60092, 60206, 60319, 60433, 60547, 60661, 60774, + 60889, 61003, 61117, 61231, 61346, 61460, 61575, 61690, + 61805, 61920, 62035, 62150, 62265, 62381, 62496, 62612, + 62727, 62843, 62959, 63075, 63191, 63308, 63424, 63540, + 63657, 63774, 63890, 64007, 64124, 64241, 64358, 64476, + 64593, 64711, 64828, 64946, 65064, 65182, 65300, 65418, + }; + private static ReadOnlySpan VolumeTable => new byte[724] + { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, + 4, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, + 5, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 10, 10, 10, + 10, 10, 10, 10, 10, 11, 11, 11, + 11, 11, 11, 11, 11, 12, 12, 12, + 12, 12, 12, 12, 13, 13, 13, 13, + 13, 13, 13, 14, 14, 14, 14, 14, + 14, 15, 15, 15, 15, 15, 16, 16, + 16, 16, 16, 16, 17, 17, 17, 17, + 17, 18, 18, 18, 18, 19, 19, 19, + 19, 19, 20, 20, 20, 20, 21, 21, + 21, 21, 22, 22, 22, 22, 23, 23, + 23, 23, 24, 24, 24, 25, 25, 25, + 25, 26, 26, 26, 27, 27, 27, 28, + 28, 28, 29, 29, 29, 30, 30, 30, + 31, 31, 31, 32, 32, 33, 33, 33, + 34, 34, 35, 35, 35, 36, 36, 37, + 37, 38, 38, 38, 39, 39, 40, 40, + 41, 41, 42, 42, 43, 43, 44, 44, + 45, 45, 46, 46, 47, 47, 48, 48, + 49, 50, 50, 51, 51, 52, 52, 53, + 54, 54, 55, 56, 56, 57, 58, 58, + 59, 60, 60, 61, 62, 62, 63, 64, + 65, 66, 66, 67, 68, 69, 70, 70, + 71, 72, 73, 74, 75, 75, 76, 77, + 78, 79, 80, 81, 82, 83, 84, 85, + 86, 87, 88, 89, 90, 91, 92, 93, + 94, 95, 96, 97, 98, 99, 101, 102, + 103, 104, 105, 106, 108, 109, 110, 111, + 113, 114, 115, 117, 118, 119, 121, 122, + 124, 125, 126, 127, + }; + + public static ushort GetChannelTimer(ushort baseTimer, int pitch) + { + int shift = 0; + pitch = -pitch; + + while (pitch < 0) + { + shift--; + pitch += 0x300; + } + + while (pitch >= 0x300) + { + shift++; + pitch -= 0x300; + } + + ulong timer = (PitchTable[pitch] + 0x10000uL) * baseTimer; + shift -= 16; + if (shift <= 0) + { + timer >>= -shift; + } + else if (shift < 32) + { + if ((timer & (ulong.MaxValue << (32 - shift))) != 0) + { + return ushort.MaxValue; + } + timer <<= shift; + } + else + { + return ushort.MaxValue; + } + + if (timer < 0x10) + { + return 0x10; + } + if (timer > ushort.MaxValue) + { + timer = ushort.MaxValue; + } + return (ushort)timer; + } + public static byte GetChannelVolume(int vol) + { + int a = vol / 0x80; + if (a < -723) + { + a = -723; + } + else if (a > 0) + { + a = 0; + } + return VolumeTable[a + 723]; + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SSEQ.cs b/VG Music Studio - Core/NDS/SDAT/SSEQ.cs new file mode 100644 index 00000000..22068c78 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SSEQ.cs @@ -0,0 +1,30 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +internal sealed class SSEQ +{ + public SDATFileHeader FileHeader; // "SSEQ" + public string BlockType; // "DATA" + public int BlockSize; + public int DataOffset; + + public byte[] Data; + + public SSEQ(byte[] bytes) + { + using (var stream = new MemoryStream(bytes)) + { + var er = new EndianBinaryReader(stream, ascii: true); + FileHeader = new SDATFileHeader(er); + BlockType = er.ReadString_Count(4); + BlockSize = er.ReadInt32(); + DataOffset = er.ReadInt32(); + + Data = new byte[FileHeader.FileSize - DataOffset]; + stream.Position = DataOffset; + er.ReadBytes(Data); + } + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SWAR.cs b/VG Music Studio - Core/NDS/SDAT/SWAR.cs new file mode 100644 index 00000000..5a5e64de --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SWAR.cs @@ -0,0 +1,65 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +internal sealed class SWAR +{ + public sealed class SWAV + { + public SWAVFormat Format; + public bool DoesLoop; + public ushort SampleRate; + /// / + public ushort Timer; + public ushort LoopOffset; + public int Length; + + public byte[] Samples; + + public SWAV(EndianBinaryReader er) + { + Format = er.ReadEnum(); + DoesLoop = er.ReadBoolean(); + SampleRate = er.ReadUInt16(); + Timer = er.ReadUInt16(); + LoopOffset = er.ReadUInt16(); + Length = er.ReadInt32(); + + Samples = new byte[(LoopOffset * 4) + (Length * 4)]; + er.ReadBytes(Samples); + } + } + + public SDATFileHeader FileHeader; // "SWAR" + public string BlockType; // "DATA" + public int BlockSize; + public byte[] Padding; + public int NumWaves; + public int[] WaveOffsets; + + public SWAV[] Waves; + + public SWAR(byte[] bytes) + { + using (var stream = new MemoryStream(bytes)) + { + var er = new EndianBinaryReader(stream, ascii: true); + FileHeader = new SDATFileHeader(er); + BlockType = er.ReadString_Count(4); + BlockSize = er.ReadInt32(); + Padding = new byte[32]; + er.ReadBytes(Padding); + NumWaves = er.ReadInt32(); + WaveOffsets = new int[NumWaves]; + er.ReadInt32s(WaveOffsets); + + Waves = new SWAV[NumWaves]; + for (int i = 0; i < NumWaves; i++) + { + stream.Position = WaveOffsets[i]; + Waves[i] = new SWAV(er); + } + } + } +} diff --git a/VG Music Studio - Core/Player.cs b/VG Music Studio - Core/Player.cs new file mode 100644 index 00000000..33bd9b07 --- /dev/null +++ b/VG Music Studio - Core/Player.cs @@ -0,0 +1,196 @@ +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace Kermalis.VGMusicStudio.Core; + +public enum PlayerState : byte +{ + Stopped, + Playing, + Paused, + Recording, + ShutDown, +} + +public interface ILoadedSong +{ + List?[] Events { get; } + long MaxTicks { get; } +} + +public abstract class Player : IDisposable +{ + protected abstract string Name { get; } + protected abstract Mixer Mixer { get; } + + public abstract ILoadedSong? LoadedSong { get; } + public bool ShouldFadeOut { get; set; } + public long NumLoops { get; set; } + + public long ElapsedTicks { get; internal set; } + public PlayerState State { get; protected set; } + public event Action? SongEnded; + + private readonly TimeBarrier _time; + private Thread? _thread; + + protected Player(double ticksPerSecond) + { + _time = new TimeBarrier(ticksPerSecond); + } + + public abstract void LoadSong(int index); + public abstract void UpdateSongState(SongState info); + internal abstract void InitEmulation(); + protected abstract void SetCurTick(long ticks); + protected abstract void OnStopped(); + + protected abstract bool Tick(bool playing, bool recording); + + protected void CreateThread() + { + _thread = new Thread(TimerTick) { Name = Name + " Tick" }; + _thread.Start(); + } + protected void WaitThread() + { + if (_thread is not null && (_thread.ThreadState is ThreadState.Running or ThreadState.WaitSleepJoin)) + { + _thread.Join(); + } + } + protected void UpdateElapsedTicksAfterLoop(List evs, long trackEventOffset, long trackRest) + { + for (int i = 0; i < evs.Count; i++) + { + SongEvent ev = evs[i]; + if (ev.Offset == trackEventOffset) + { + ElapsedTicks = ev.Ticks[0] - trackRest; + return; + } + } + throw new InvalidDataException("No loop point found"); + } + + public void Play() + { + if (LoadedSong is null) + { + SongEnded?.Invoke(); + return; + } + + if (State is not PlayerState.ShutDown) + { + Stop(); + InitEmulation(); + State = PlayerState.Playing; + CreateThread(); + } + } + public void TogglePlaying() + { + switch (State) + { + case PlayerState.Playing: + { + State = PlayerState.Paused; + WaitThread(); + break; + } + case PlayerState.Paused: + case PlayerState.Stopped: + { + State = PlayerState.Playing; + CreateThread(); + break; + } + } + } + public void Stop() + { + if (State is PlayerState.Playing or PlayerState.Paused) + { + State = PlayerState.Stopped; + WaitThread(); + OnStopped(); + } + } + public void Record(string fileName) + { + Mixer.CreateWaveWriter(fileName); + + InitEmulation(); + State = PlayerState.Recording; + CreateThread(); + WaitThread(); + + Mixer.CloseWaveWriter(); + } + public void SetSongPosition(long ticks) + { + if (LoadedSong is null) + { + SongEnded?.Invoke(); + return; + } + + if (State is not PlayerState.Playing and not PlayerState.Paused and not PlayerState.Stopped) + { + return; + } + + if (State is PlayerState.Playing) + { + TogglePlaying(); + } + InitEmulation(); + SetCurTick(ticks); + TogglePlaying(); + } + + private void TimerTick() + { + _time.Start(); + while (true) + { + PlayerState state = State; + bool playing = state == PlayerState.Playing; + bool recording = state == PlayerState.Recording; + if (!playing && !recording) + { + break; + } + + bool allDone = Tick(playing, recording); + if (allDone) + { + // TODO: lock state + _time.Stop(); // TODO: Don't need timer if recording + State = PlayerState.Stopped; + SongEnded?.Invoke(); + return; + } + if (playing) + { + _time.Wait(); + } + } + _time.Stop(); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + if (State != PlayerState.ShutDown) + { + State = PlayerState.ShutDown; + WaitThread(); + } + SongEnded = null; + } +} diff --git a/VG Music Studio/Properties/Strings.Designer.cs b/VG Music Studio - Core/Properties/Strings.Designer.cs similarity index 82% rename from VG Music Studio/Properties/Strings.Designer.cs rename to VG Music Studio - Core/Properties/Strings.Designer.cs index 7153b371..eea96fb7 100644 --- a/VG Music Studio/Properties/Strings.Designer.cs +++ b/VG Music Studio - Core/Properties/Strings.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace Kermalis.VGMusicStudio.Properties { +namespace Kermalis.VGMusicStudio.Core.Properties { using System; @@ -19,10 +19,10 @@ namespace Kermalis.VGMusicStudio.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Strings { + public class Strings { private static global::System.Resources.ResourceManager resourceMan; @@ -36,10 +36,10 @@ internal Strings() { /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Kermalis.VGMusicStudio.Properties.Strings", typeof(Strings).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Kermalis.VGMusicStudio.Core.Properties.Strings", typeof(Strings).Assembly); resourceMan = temp; } return resourceMan; @@ -51,7 +51,7 @@ internal Strings() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -63,7 +63,7 @@ internal Strings() { /// /// Looks up a localized string similar to {0} key. /// - internal static string ConfigKeySubkey { + public static string ConfigKeySubkey { get { return ResourceManager.GetString("ConfigKeySubkey", resourceCulture); } @@ -72,7 +72,7 @@ internal static string ConfigKeySubkey { /// /// Looks up a localized string similar to Would you like to stop playing the current playlist?. /// - internal static string EndPlaylistBody { + public static string EndPlaylistBody { get { return ResourceManager.GetString("EndPlaylistBody", resourceCulture); } @@ -81,7 +81,7 @@ internal static string EndPlaylistBody { /// /// Looks up a localized string similar to Invalid command in track {0} at 0x{1:X}: 0x{2:X}. /// - internal static string ErrorAlphaDreamDSEMP2KSDATInvalidCommand { + public static string ErrorAlphaDreamDSEMP2KSDATInvalidCommand { get { return ResourceManager.GetString("ErrorAlphaDreamDSEMP2KSDATInvalidCommand", resourceCulture); } @@ -90,7 +90,7 @@ internal static string ErrorAlphaDreamDSEMP2KSDATInvalidCommand { /// /// Looks up a localized string similar to Cannot copy invalid game code "{0}". /// - internal static string ErrorAlphaDreamMP2KCopyInvalidGameCode { + public static string ErrorAlphaDreamMP2KCopyInvalidGameCode { get { return ResourceManager.GetString("ErrorAlphaDreamMP2KCopyInvalidGameCode", resourceCulture); } @@ -99,7 +99,7 @@ internal static string ErrorAlphaDreamMP2KCopyInvalidGameCode { /// /// Looks up a localized string similar to Game code "{0}" is missing.. /// - internal static string ErrorAlphaDreamMP2KMissingGameCode { + public static string ErrorAlphaDreamMP2KMissingGameCode { get { return ResourceManager.GetString("ErrorAlphaDreamMP2KMissingGameCode", resourceCulture); } @@ -108,7 +108,7 @@ internal static string ErrorAlphaDreamMP2KMissingGameCode { /// /// Looks up a localized string similar to Error parsing game code "{0}" in "{1}"{2}. /// - internal static string ErrorAlphaDreamMP2KParseGameCode { + public static string ErrorAlphaDreamMP2KParseGameCode { get { return ResourceManager.GetString("ErrorAlphaDreamMP2KParseGameCode", resourceCulture); } @@ -117,7 +117,7 @@ internal static string ErrorAlphaDreamMP2KParseGameCode { /// /// Looks up a localized string similar to Playlist "{0}" has song {1} defined more than once between decimal and hexadecimal.. /// - internal static string ErrorAlphaDreamMP2KSongRepeated { + public static string ErrorAlphaDreamMP2KSongRepeated { get { return ResourceManager.GetString("ErrorAlphaDreamMP2KSongRepeated", resourceCulture); } @@ -126,7 +126,7 @@ internal static string ErrorAlphaDreamMP2KSongRepeated { /// /// Looks up a localized string similar to "{0}" count must be the same as "{1}" count.. /// - internal static string ErrorAlphaDreamMP2KSongTableCounts { + public static string ErrorAlphaDreamMP2KSongTableCounts { get { return ResourceManager.GetString("ErrorAlphaDreamMP2KSongTableCounts", resourceCulture); } @@ -135,25 +135,16 @@ internal static string ErrorAlphaDreamMP2KSongTableCounts { /// /// Looks up a localized string similar to "{0}" must be True or False.. /// - internal static string ErrorBoolParse { + public static string ErrorBoolParse { get { return ResourceManager.GetString("ErrorBoolParse", resourceCulture); } } - /// - /// Looks up a localized string similar to Color {0} has an invalid key.. - /// - internal static string ErrorConfigColorInvalidKey { - get { - return ResourceManager.GetString("ErrorConfigColorInvalidKey", resourceCulture); - } - } - /// /// Looks up a localized string similar to Color {0} is not defined.. /// - internal static string ErrorConfigColorMissing { + public static string ErrorConfigColorMissing { get { return ResourceManager.GetString("ErrorConfigColorMissing", resourceCulture); } @@ -162,7 +153,7 @@ internal static string ErrorConfigColorMissing { /// /// Looks up a localized string similar to Color {0} is defined more than once between decimal and hexadecimal.. /// - internal static string ErrorConfigColorRepeated { + public static string ErrorConfigColorRepeated { get { return ResourceManager.GetString("ErrorConfigColorRepeated", resourceCulture); } @@ -171,7 +162,7 @@ internal static string ErrorConfigColorRepeated { /// /// Looks up a localized string similar to "{0}" is invalid.. /// - internal static string ErrorConfigKeyInvalid { + public static string ErrorConfigKeyInvalid { get { return ResourceManager.GetString("ErrorConfigKeyInvalid", resourceCulture); } @@ -180,7 +171,7 @@ internal static string ErrorConfigKeyInvalid { /// /// Looks up a localized string similar to "{0}" is missing.. /// - internal static string ErrorConfigKeyMissing { + public static string ErrorConfigKeyMissing { get { return ResourceManager.GetString("ErrorConfigKeyMissing", resourceCulture); } @@ -189,7 +180,7 @@ internal static string ErrorConfigKeyMissing { /// /// Looks up a localized string similar to "{0}" must have at least one entry.. /// - internal static string ErrorConfigKeyNoEntries { + public static string ErrorConfigKeyNoEntries { get { return ResourceManager.GetString("ErrorConfigKeyNoEntries", resourceCulture); } @@ -198,7 +189,7 @@ internal static string ErrorConfigKeyNoEntries { /// /// Looks up a localized string similar to Unknown header version: 0x{0:X}. /// - internal static string ErrorDSEInvalidHeaderVersion { + public static string ErrorDSEInvalidHeaderVersion { get { return ResourceManager.GetString("ErrorDSEInvalidHeaderVersion", resourceCulture); } @@ -207,7 +198,7 @@ internal static string ErrorDSEInvalidHeaderVersion { /// /// Looks up a localized string similar to Invalid key in track {0} at 0x{1:X}: {2}. /// - internal static string ErrorDSEInvalidKey { + public static string ErrorDSEInvalidKey { get { return ResourceManager.GetString("ErrorDSEInvalidKey", resourceCulture); } @@ -216,7 +207,7 @@ internal static string ErrorDSEInvalidKey { /// /// Looks up a localized string similar to There are no "bgm(NNNN).smd" files.. /// - internal static string ErrorDSENoSequences { + public static string ErrorDSENoSequences { get { return ResourceManager.GetString("ErrorDSENoSequences", resourceCulture); } @@ -225,7 +216,7 @@ internal static string ErrorDSENoSequences { /// /// Looks up a localized string similar to Error Loading Global Config. /// - internal static string ErrorGlobalConfig { + public static string ErrorGlobalConfig { get { return ResourceManager.GetString("ErrorGlobalConfig", resourceCulture); } @@ -234,7 +225,7 @@ internal static string ErrorGlobalConfig { /// /// Looks up a localized string similar to Error Loading Song {0}. /// - internal static string ErrorLoadSong { + public static string ErrorLoadSong { get { return ResourceManager.GetString("ErrorLoadSong", resourceCulture); } @@ -243,7 +234,7 @@ internal static string ErrorLoadSong { /// /// Looks up a localized string similar to Invalid running status command in track {0} at 0x{1:X}: 0x{2:X}. /// - internal static string ErrorMP2KInvalidRunningStatusCommand { + public static string ErrorMP2KInvalidRunningStatusCommand { get { return ResourceManager.GetString("ErrorMP2KInvalidRunningStatusCommand", resourceCulture); } @@ -252,7 +243,7 @@ internal static string ErrorMP2KInvalidRunningStatusCommand { /// /// Looks up a localized string similar to Too many nested call events in track {0}. /// - internal static string ErrorMP2KSDATNestedCalls { + public static string ErrorMP2KSDATNestedCalls { get { return ResourceManager.GetString("ErrorMP2KSDATNestedCalls", resourceCulture); } @@ -261,7 +252,7 @@ internal static string ErrorMP2KSDATNestedCalls { /// /// Looks up a localized string similar to Error Loading GBA ROM (AlphaDream). /// - internal static string ErrorOpenAlphaDream { + public static string ErrorOpenAlphaDream { get { return ResourceManager.GetString("ErrorOpenAlphaDream", resourceCulture); } @@ -270,7 +261,7 @@ internal static string ErrorOpenAlphaDream { /// /// Looks up a localized string similar to Error Loading DSE Folder. /// - internal static string ErrorOpenDSE { + public static string ErrorOpenDSE { get { return ResourceManager.GetString("ErrorOpenDSE", resourceCulture); } @@ -279,7 +270,7 @@ internal static string ErrorOpenDSE { /// /// Looks up a localized string similar to Error Loading GBA ROM (MP2K). /// - internal static string ErrorOpenMP2K { + public static string ErrorOpenMP2K { get { return ResourceManager.GetString("ErrorOpenMP2K", resourceCulture); } @@ -288,7 +279,7 @@ internal static string ErrorOpenMP2K { /// /// Looks up a localized string similar to Error Loading SDAT File. /// - internal static string ErrorOpenSDAT { + public static string ErrorOpenSDAT { get { return ResourceManager.GetString("ErrorOpenSDAT", resourceCulture); } @@ -297,7 +288,7 @@ internal static string ErrorOpenSDAT { /// /// Looks up a localized string similar to Error parsing "{0}"{1}. /// - internal static string ErrorParseConfig { + public static string ErrorParseConfig { get { return ResourceManager.GetString("ErrorParseConfig", resourceCulture); } @@ -306,7 +297,7 @@ internal static string ErrorParseConfig { /// /// Looks up a localized string similar to Error Exporting DLS. /// - internal static string ErrorSaveDLS { + public static string ErrorSaveDLS { get { return ResourceManager.GetString("ErrorSaveDLS", resourceCulture); } @@ -315,7 +306,7 @@ internal static string ErrorSaveDLS { /// /// Looks up a localized string similar to Error Exporting MIDI. /// - internal static string ErrorSaveMIDI { + public static string ErrorSaveMIDI { get { return ResourceManager.GetString("ErrorSaveMIDI", resourceCulture); } @@ -324,7 +315,7 @@ internal static string ErrorSaveMIDI { /// /// Looks up a localized string similar to Error Exporting SF2. /// - internal static string ErrorSaveSF2 { + public static string ErrorSaveSF2 { get { return ResourceManager.GetString("ErrorSaveSF2", resourceCulture); } @@ -333,7 +324,7 @@ internal static string ErrorSaveSF2 { /// /// Looks up a localized string similar to Error Exporting WAV. /// - internal static string ErrorSaveWAV { + public static string ErrorSaveWAV { get { return ResourceManager.GetString("ErrorSaveWAV", resourceCulture); } @@ -342,7 +333,7 @@ internal static string ErrorSaveWAV { /// /// Looks up a localized string similar to This SDAT archive has no sequences.. /// - internal static string ErrorSDATNoSequences { + public static string ErrorSDATNoSequences { get { return ResourceManager.GetString("ErrorSDATNoSequences", resourceCulture); } @@ -351,7 +342,7 @@ internal static string ErrorSDATNoSequences { /// /// Looks up a localized string similar to "{0}" is not an integer value.. /// - internal static string ErrorValueParse { + public static string ErrorValueParse { get { return ResourceManager.GetString("ErrorValueParse", resourceCulture); } @@ -360,7 +351,7 @@ internal static string ErrorValueParse { /// /// Looks up a localized string similar to "{0}" must be between {1} and {2}.. /// - internal static string ErrorValueParseRanged { + public static string ErrorValueParseRanged { get { return ResourceManager.GetString("ErrorValueParseRanged", resourceCulture); } @@ -369,7 +360,7 @@ internal static string ErrorValueParseRanged { /// /// Looks up a localized string similar to GBA Files. /// - internal static string FilterOpenGBA { + public static string FilterOpenGBA { get { return ResourceManager.GetString("FilterOpenGBA", resourceCulture); } @@ -378,7 +369,7 @@ internal static string FilterOpenGBA { /// /// Looks up a localized string similar to SDAT Files. /// - internal static string FilterOpenSDAT { + public static string FilterOpenSDAT { get { return ResourceManager.GetString("FilterOpenSDAT", resourceCulture); } @@ -387,7 +378,7 @@ internal static string FilterOpenSDAT { /// /// Looks up a localized string similar to DLS Files. /// - internal static string FilterSaveDLS { + public static string FilterSaveDLS { get { return ResourceManager.GetString("FilterSaveDLS", resourceCulture); } @@ -396,7 +387,7 @@ internal static string FilterSaveDLS { /// /// Looks up a localized string similar to MIDI Files. /// - internal static string FilterSaveMIDI { + public static string FilterSaveMIDI { get { return ResourceManager.GetString("FilterSaveMIDI", resourceCulture); } @@ -405,7 +396,7 @@ internal static string FilterSaveMIDI { /// /// Looks up a localized string similar to SF2 Files. /// - internal static string FilterSaveSF2 { + public static string FilterSaveSF2 { get { return ResourceManager.GetString("FilterSaveSF2", resourceCulture); } @@ -414,7 +405,7 @@ internal static string FilterSaveSF2 { /// /// Looks up a localized string similar to WAV Files. /// - internal static string FilterSaveWAV { + public static string FilterSaveWAV { get { return ResourceManager.GetString("FilterSaveWAV", resourceCulture); } @@ -423,7 +414,7 @@ internal static string FilterSaveWAV { /// /// Looks up a localized string similar to Data. /// - internal static string MenuData { + public static string MenuData { get { return ResourceManager.GetString("MenuData", resourceCulture); } @@ -432,7 +423,7 @@ internal static string MenuData { /// /// Looks up a localized string similar to End Current Playlist. /// - internal static string MenuEndPlaylist { + public static string MenuEndPlaylist { get { return ResourceManager.GetString("MenuEndPlaylist", resourceCulture); } @@ -441,7 +432,7 @@ internal static string MenuEndPlaylist { /// /// Looks up a localized string similar to File. /// - internal static string MenuFile { + public static string MenuFile { get { return ResourceManager.GetString("MenuFile", resourceCulture); } @@ -450,7 +441,7 @@ internal static string MenuFile { /// /// Looks up a localized string similar to Open GBA ROM (AlphaDream). /// - internal static string MenuOpenAlphaDream { + public static string MenuOpenAlphaDream { get { return ResourceManager.GetString("MenuOpenAlphaDream", resourceCulture); } @@ -459,7 +450,7 @@ internal static string MenuOpenAlphaDream { /// /// Looks up a localized string similar to Open DSE Folder. /// - internal static string MenuOpenDSE { + public static string MenuOpenDSE { get { return ResourceManager.GetString("MenuOpenDSE", resourceCulture); } @@ -468,7 +459,7 @@ internal static string MenuOpenDSE { /// /// Looks up a localized string similar to Open GBA ROM (MP2K). /// - internal static string MenuOpenMP2K { + public static string MenuOpenMP2K { get { return ResourceManager.GetString("MenuOpenMP2K", resourceCulture); } @@ -477,7 +468,7 @@ internal static string MenuOpenMP2K { /// /// Looks up a localized string similar to Open SDAT File. /// - internal static string MenuOpenSDAT { + public static string MenuOpenSDAT { get { return ResourceManager.GetString("MenuOpenSDAT", resourceCulture); } @@ -486,7 +477,7 @@ internal static string MenuOpenSDAT { /// /// Looks up a localized string similar to Playlist. /// - internal static string MenuPlaylist { + public static string MenuPlaylist { get { return ResourceManager.GetString("MenuPlaylist", resourceCulture); } @@ -495,7 +486,7 @@ internal static string MenuPlaylist { /// /// Looks up a localized string similar to Export VoiceTable as DLS. /// - internal static string MenuSaveDLS { + public static string MenuSaveDLS { get { return ResourceManager.GetString("MenuSaveDLS", resourceCulture); } @@ -504,7 +495,7 @@ internal static string MenuSaveDLS { /// /// Looks up a localized string similar to Export Song as MIDI. /// - internal static string MenuSaveMIDI { + public static string MenuSaveMIDI { get { return ResourceManager.GetString("MenuSaveMIDI", resourceCulture); } @@ -513,7 +504,7 @@ internal static string MenuSaveMIDI { /// /// Looks up a localized string similar to Export VoiceTable as SF2. /// - internal static string MenuSaveSF2 { + public static string MenuSaveSF2 { get { return ResourceManager.GetString("MenuSaveSF2", resourceCulture); } @@ -522,25 +513,16 @@ internal static string MenuSaveSF2 { /// /// Looks up a localized string similar to Export Song as WAV. /// - internal static string MenuSaveWAV { + public static string MenuSaveWAV { get { return ResourceManager.GetString("MenuSaveWAV", resourceCulture); } } - /// - /// Looks up a localized string similar to C;C#;D;D#;E;F;F#;G;G#;A;A#;B. - /// - internal static string Notes { - get { - return ResourceManager.GetString("Notes", resourceCulture); - } - } - /// /// Looks up a localized string similar to Next Song. /// - internal static string PlayerNextSong { + public static string PlayerNextSong { get { return ResourceManager.GetString("PlayerNextSong", resourceCulture); } @@ -549,7 +531,7 @@ internal static string PlayerNextSong { /// /// Looks up a localized string similar to Notes. /// - internal static string PlayerNotes { + public static string PlayerNotes { get { return ResourceManager.GetString("PlayerNotes", resourceCulture); } @@ -558,7 +540,7 @@ internal static string PlayerNotes { /// /// Looks up a localized string similar to Pause. /// - internal static string PlayerPause { + public static string PlayerPause { get { return ResourceManager.GetString("PlayerPause", resourceCulture); } @@ -567,7 +549,7 @@ internal static string PlayerPause { /// /// Looks up a localized string similar to Play. /// - internal static string PlayerPlay { + public static string PlayerPlay { get { return ResourceManager.GetString("PlayerPlay", resourceCulture); } @@ -576,7 +558,7 @@ internal static string PlayerPlay { /// /// Looks up a localized string similar to Position. /// - internal static string PlayerPosition { + public static string PlayerPosition { get { return ResourceManager.GetString("PlayerPosition", resourceCulture); } @@ -585,7 +567,7 @@ internal static string PlayerPosition { /// /// Looks up a localized string similar to Previous Song. /// - internal static string PlayerPreviousSong { + public static string PlayerPreviousSong { get { return ResourceManager.GetString("PlayerPreviousSong", resourceCulture); } @@ -594,7 +576,7 @@ internal static string PlayerPreviousSong { /// /// Looks up a localized string similar to Rest. /// - internal static string PlayerRest { + public static string PlayerRest { get { return ResourceManager.GetString("PlayerRest", resourceCulture); } @@ -603,7 +585,7 @@ internal static string PlayerRest { /// /// Looks up a localized string similar to Stop. /// - internal static string PlayerStop { + public static string PlayerStop { get { return ResourceManager.GetString("PlayerStop", resourceCulture); } @@ -612,7 +594,7 @@ internal static string PlayerStop { /// /// Looks up a localized string similar to Tempo. /// - internal static string PlayerTempo { + public static string PlayerTempo { get { return ResourceManager.GetString("PlayerTempo", resourceCulture); } @@ -621,7 +603,7 @@ internal static string PlayerTempo { /// /// Looks up a localized string similar to Type. /// - internal static string PlayerType { + public static string PlayerType { get { return ResourceManager.GetString("PlayerType", resourceCulture); } @@ -630,7 +612,7 @@ internal static string PlayerType { /// /// Looks up a localized string similar to Unpause. /// - internal static string PlayerUnpause { + public static string PlayerUnpause { get { return ResourceManager.GetString("PlayerUnpause", resourceCulture); } @@ -639,7 +621,7 @@ internal static string PlayerUnpause { /// /// Looks up a localized string similar to Music. /// - internal static string PlaylistMusic { + public static string PlaylistMusic { get { return ResourceManager.GetString("PlaylistMusic", resourceCulture); } @@ -648,16 +630,25 @@ internal static string PlaylistMusic { /// /// Looks up a localized string similar to Would you like to play the following playlist?{0}. /// - internal static string PlayPlaylistBody { + public static string PlayPlaylistBody { get { return ResourceManager.GetString("PlayPlaylistBody", resourceCulture); } } + /// + /// Looks up a localized string similar to songs|0_0|song|1_1|songs|2_*|. + /// + public static string Song_s_ { + get { + return ResourceManager.GetString("Song(s)", resourceCulture); + } + } + /// /// Looks up a localized string similar to VoiceTable saved to {0}.. /// - internal static string SuccessSaveDLS { + public static string SuccessSaveDLS { get { return ResourceManager.GetString("SuccessSaveDLS", resourceCulture); } @@ -666,7 +657,7 @@ internal static string SuccessSaveDLS { /// /// Looks up a localized string similar to MIDI saved to {0}.. /// - internal static string SuccessSaveMIDI { + public static string SuccessSaveMIDI { get { return ResourceManager.GetString("SuccessSaveMIDI", resourceCulture); } @@ -675,7 +666,7 @@ internal static string SuccessSaveMIDI { /// /// Looks up a localized string similar to VoiceTable saved to {0}.. /// - internal static string SuccessSaveSF2 { + public static string SuccessSaveSF2 { get { return ResourceManager.GetString("SuccessSaveSF2", resourceCulture); } @@ -684,7 +675,7 @@ internal static string SuccessSaveSF2 { /// /// Looks up a localized string similar to WAV saved to {0}.. /// - internal static string SuccessSaveWAV { + public static string SuccessSaveWAV { get { return ResourceManager.GetString("SuccessSaveWAV", resourceCulture); } @@ -693,7 +684,7 @@ internal static string SuccessSaveWAV { /// /// Looks up a localized string similar to Arguments. /// - internal static string TrackViewerArguments { + public static string TrackViewerArguments { get { return ResourceManager.GetString("TrackViewerArguments", resourceCulture); } @@ -702,7 +693,7 @@ internal static string TrackViewerArguments { /// /// Looks up a localized string similar to Event. /// - internal static string TrackViewerEvent { + public static string TrackViewerEvent { get { return ResourceManager.GetString("TrackViewerEvent", resourceCulture); } @@ -711,7 +702,7 @@ internal static string TrackViewerEvent { /// /// Looks up a localized string similar to Offset. /// - internal static string TrackViewerOffset { + public static string TrackViewerOffset { get { return ResourceManager.GetString("TrackViewerOffset", resourceCulture); } @@ -720,7 +711,7 @@ internal static string TrackViewerOffset { /// /// Looks up a localized string similar to Ticks. /// - internal static string TrackViewerTicks { + public static string TrackViewerTicks { get { return ResourceManager.GetString("TrackViewerTicks", resourceCulture); } @@ -729,7 +720,7 @@ internal static string TrackViewerTicks { /// /// Looks up a localized string similar to Track Viewer. /// - internal static string TrackViewerTitle { + public static string TrackViewerTitle { get { return ResourceManager.GetString("TrackViewerTitle", resourceCulture); } @@ -738,7 +729,7 @@ internal static string TrackViewerTitle { /// /// Looks up a localized string similar to Track {0}. /// - internal static string TrackViewerTrackX { + public static string TrackViewerTrackX { get { return ResourceManager.GetString("TrackViewerTrackX", resourceCulture); } diff --git a/VG Music Studio/Properties/Strings.es.resx b/VG Music Studio - Core/Properties/Strings.es.resx similarity index 96% rename from VG Music Studio/Properties/Strings.es.resx rename to VG Music Studio - Core/Properties/Strings.es.resx index d98bb266..c524dfd2 100644 --- a/VG Music Studio/Properties/Strings.es.resx +++ b/VG Music Studio - Core/Properties/Strings.es.resx @@ -118,10 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Quisiera detener la Lista de Reproducción actual? + ¿Quisiera detener la Lista de Repoducción actual? - Error al Cargar Canción {0} + Error al Cargar la Canción {0} Error al Abrir Carpeta DSE @@ -169,14 +169,11 @@ Abrir Archivo SDAT - Playlist + Lista de Reproducción Exportar Canción como MIDI - - Do;Do#;Re;Re#;Mi;Fa;Fa#;Sol;Sol#;La;La#;Si - Siguiente Canción @@ -214,7 +211,7 @@ Música - Quisiera reproducir la siguiente Lista de Reproducción? {0} + ¿Quisiera reproducir la siguiente Lista de Reproducción? MIDI guardado en {0}. @@ -243,9 +240,6 @@ "{0}" debe ser Verdadero o Falso. - - El color {0} tiene una clave inválida. - El color {0} no está definido. @@ -274,7 +268,7 @@ No hay ningún archivo "bgm(NNNN).smd". - Error al Cargar Configuración Global + Error al Cargar la Configuración Global No se puede copiar, el código del juego "{0}" es inválido. @@ -345,4 +339,7 @@ DLS guardado en {0}. + + canciones|0_0|canción|1_1|canciones|2_*| + \ No newline at end of file diff --git a/VG Music Studio - Core/Properties/Strings.fr.resx b/VG Music Studio - Core/Properties/Strings.fr.resx new file mode 100644 index 00000000..0befad50 --- /dev/null +++ b/VG Music Studio - Core/Properties/Strings.fr.resx @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Echec de chargement du titre {0} + + + Echec du chargement de la ROM GBA (AlphaDream) + + + Echec de l'exportation en MIDI + + + Fichiers GBA + + + Fichiers MIDI + + + Données + + + Fichier + + + Ouvrir ROM GBA (MP2K) + + + Exporter le titre en MIDI + + + Silence + + + Notes + + + Pause + + + Lecture + + + Position + + + Stop + + + Tempo + + + Type + + + Dé-pause + + + MIDI enregistré sous {0}. + + + Voulez-vous lancer la playlist suivante ?{0} + + + Titre suivant + + + Titre précédent + + + Voulez-vous arrêter la lecture de la playlist en cours ? + + + Echec de chargement du dossier DSE + + + Echec de chargement de la ROM GBA (MP2K) + + + Echec de lecture du fichier SDAT + + + Fichiers SDAT + + + Arrêter la playlist en cours + + + Ouvrir dossier DSE + + + Ouvrir ROM GBA (AlphaDream) + + + Ouvrir fichier SDAT + + + Playlist + + + Musique + + + Arguments + + + Event + + + Offset + + + Ticks + + + Visualiseur de pistes + + + Piste {0} + + + Clé {0} + + + "{0}" doit être Vrai ou Faux + + + La couleur {0} n'est pas définie. + + + La couleur {0} est définie plus d'une fois entre le décimal et l'hexadécimal. + + + "{0}" est invalide. + + + "{0}" est manquante. + + + "{0}" doit avoir au moins une entrée. + + + Version d'en-tête inconnue: 0x{0:X} + + + Clé invalide pour la piste {0} à l'adresse 0x{1:X}: {2} + + + Commande invalide pour la piste {0} à l'adresse 0x{1:X}: 0x{2:X} + + + Il n'y a pas de fichiers "bgm(NNNN).smd". + + + Echec du chargement de la configuration globale. + + + Impossible de copier le code de jeu invalide "{0}" + + + Le code de jeu "{0}" est manquant. + + + Erreur au parsage du code de jeu "{0}" dans "{1}"{2} + + + Le titre {1} de la playlist "{0}" est défini plus d'une fois entre le décimal et l'hexadécimal. + + + Le compte de "{0}" doit être identique au compte de "{1}". + + + Commande de statut de lecture invalide sur la piste {0} à l'adresse 0x{1:X}: 0x{2:X} + + + Trop d'events d'appel imbriqués sur la piste {0} + + + Echec du parsage de "{0}"{1} + + + Cet archive SDAT n'a pas de séquences. + + + "{0}" n'est pas un entier. + + + "{0}" doit être entre {1} et {2}. + + + Echec de l'exportation en WAV + + + Fichiers WAV + + + Exporter le titre en WAV + + + WAV sauvegardé sous {0}. + + + Echec de l'exportation en SF2 + + + Fichiers SF2 + + + Exporter la VoiceTable en SF2 + + + VoiceTable sauvegardée sous {0}. + + + Echec de l'exportation en DLS + + + Fichiers DLS + + + Exporter la VoiceTable en DLS + + + VoiceTable sauvegardée sous {0}. + + + titres|0_0|titre|1_1|titres|2_*| + + \ No newline at end of file diff --git a/VG Music Studio/Properties/Strings.it.resx b/VG Music Studio - Core/Properties/Strings.it.resx similarity index 98% rename from VG Music Studio/Properties/Strings.it.resx rename to VG Music Studio - Core/Properties/Strings.it.resx index 935f17e8..6da605e1 100644 --- a/VG Music Studio/Properties/Strings.it.resx +++ b/VG Music Studio - Core/Properties/Strings.it.resx @@ -144,9 +144,6 @@ Esporta Brano in MIDI - - Do;Do#;Re;Re#;Mi;Fa;Fa#;Sol;Sol#;La;La#;Si - Pausa @@ -243,9 +240,6 @@ "{0}" deve essere Vero o Falso. - - Il colore {0} non ha una chiave valida. - Il colore {0} non è definito. @@ -345,4 +339,7 @@ VoiceTable salvata in {0}. + + canzoni|0_0|canzone|1_1|canzoni|2_*| + \ No newline at end of file diff --git a/VG Music Studio/Properties/Strings.resx b/VG Music Studio - Core/Properties/Strings.resx similarity index 98% rename from VG Music Studio/Properties/Strings.resx rename to VG Music Studio - Core/Properties/Strings.resx index 8e4ae4a1..916279d2 100644 --- a/VG Music Studio/Properties/Strings.resx +++ b/VG Music Studio - Core/Properties/Strings.resx @@ -145,9 +145,6 @@ Export Song as MIDI - - C;C#;D;D#;E;F;F#;G;G#;A;A#;B - Rest @@ -249,10 +246,6 @@ "{0}" must be True or False. {0} is the value name. - - Color {0} has an invalid key. - {0} is the color number. - Color {0} is not defined. {0} is the color number. @@ -373,4 +366,7 @@ VoiceTable saved to {0}. {0} is the file name. + + songs|0_0|song|1_1|songs|2_*| + \ No newline at end of file diff --git a/VG Music Studio - Core/Properties/Strings.ru.resx b/VG Music Studio - Core/Properties/Strings.ru.resx new file mode 100644 index 00000000..b35e0748 --- /dev/null +++ b/VG Music Studio - Core/Properties/Strings.ru.resx @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Ошибка во время загрузки мелодии {0} + + + Ошибка во время загрузки GBA-ROMа (AlphaDream) + + + Ошибка во время экспорта MIDI + + + GBA-файлы + + + MIDI-файлы + + + Данные + + + Файл + + + Открыть GBA-ROM (MP2K) + + + Экспортировать мелодию в формате MIDI + + + Длительность + + + Ноты + + + Пауза + + + Проиграть + + + Адрес + + + Остановить + + + Темп + + + Тип + + + Продолжить + + + Мелодия успешно сохранена в MIDI-файл "{0}". + + + Хотите прослушать данный плейлист?{0} + + + Следующая мелодия + + + Предыдущая мелодия + + + Хотите прервать воспроизведение плейлиста? + + + При открытии DSE-папки произошла ошибка + + + При открытии GBA-ROMа (MP2K) произошла ошибка + + + При открытии SDAT-файла произошла ошибка + + + SDAT-файлы + + + Отключить плейлист + + + Открыть DSE-папку + + + Открыть GBA-ROM (AlphaDream) + + + Открыть SDAT-файл + + + Плейлист + + + Музыка + + + Аргументы + + + Событие + + + Адрес + + + Такты + + + Просмотр трека + + + Трек {0} + + + Родительский ключ {0} + + + "{0}" должно принимать значения True или False. + + + Цвет {0} не указан. + + + Цвет {0} указан несколько раз среди десятичных и шестнадцатеричных значений. + + + Родительский ключ "{0}" недопустим. + + + Родительский ключ "{0}" отсутствует. + + + Родительский ключ "{0}" должен содержать хотя бы одно значение. + + + Неизвестная версия заголовка: 0x{0:X} + + + Недопустимый родительский ключ в треке {0} по адресу 0x{1:X}: {2} + + + Недопустимая команда в треке {0} по адресу 0x{1:X}: 0x{2:X} + + + Файлы типа "bgm(NNNN).smd" отсутствуют. + + + Ошибка загрузки глобальной конфигурвции + + + Невозможно скопировать недопустимый игровой код "{0}" + + + Игровой код "{0}" отсутствует. + + + Ошибка во время анализа игрового кода "{0}" в файле "{1}"{2} + + + В плейлисте "{0}" содержится мелодия {1}, которая указана несколько раз среди десятичных и шестнадцатеричных значений. + + + Значения родительских ключей "{0}" и "{1}" должны совпадать. + + + Недопустимая команда состояния в треке {0} по адресу 0x{1:X}: 0x{2:X} + + + Слишком много случаев вызовов вложенных функций в треке {0} + + + Ошибка анализа файла "{0}"{1} + + + В указанном SDAT-архиве отсутствуют секвенции. + + + Значение "{0}" должно содержать целое число. + + + Значение "{0}" должно находиться в диапазоне от "{1}" до "{2}". + + + При экспорте WAV-файла произошла ошибка + + + WAV-файлы + + + Экспортировать мелодию в формате WAV + + + Мелодия успешно сохранена в WAV-файл "{0}". + + + При экспорте SF2-файла произошла ошибка + + + SF2-файлы + + + Экспортировать таблицу семплов в формате SF2 + + + Таблица семплов успешно сохранена в файл "{0}". + + + При экспорте DLS-файла произошла ошибка + + + DLS-файлы + + + Экспортировать таблицу семплов в формате DLS + + + Таблица семплов успешно сохранена в файл "{0}". + + + мелодий|0_0|мелодия|1_1|мелодии|2_4|мелодий|5_*| + + \ No newline at end of file diff --git a/VG Music Studio - Core/SongEvent.cs b/VG Music Studio - Core/SongEvent.cs new file mode 100644 index 00000000..6d703227 --- /dev/null +++ b/VG Music Studio - Core/SongEvent.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Drawing; + +namespace Kermalis.VGMusicStudio.Core; + +public interface ICommand +{ + Color Color { get; } + string Label { get; } + string Arguments { get; } +} +public sealed class SongEvent +{ + public long Offset { get; } + public List Ticks { get; } + public ICommand Command { get; } + + internal SongEvent(long offset, ICommand command) + { + Offset = offset; + Ticks = new List(); + Command = command; + } +} diff --git a/VG Music Studio - Core/SongState.cs b/VG Music Studio - Core/SongState.cs new file mode 100644 index 00000000..02d5e92e --- /dev/null +++ b/VG Music Studio - Core/SongState.cs @@ -0,0 +1,73 @@ +namespace Kermalis.VGMusicStudio.Core; + +public sealed class SongState +{ + public sealed class Track + { + public long Position; + public byte Voice; + public byte Volume; + public int LFO; + public long Rest; + public sbyte Panpot; + public float LeftVolume; + public float RightVolume; + public int PitchBend; + public byte Extra; + public string Type; + public byte[] Keys; + + public int PreviousKeysTime; // TODO: Fix + public string PreviousKeys; + + public Track() + { + Keys = new byte[MAX_KEYS]; + for (int i = 0; i < MAX_KEYS; i++) + { + Keys[i] = byte.MaxValue; + } + + Type = null!; + PreviousKeys = null!; + } + + public void Reset() + { + Position = Rest = 0; + Voice = Volume = Extra = 0; + LFO = PitchBend = PreviousKeysTime = 0; + Panpot = 0; + LeftVolume = RightVolume = 0f; + Type = PreviousKeys = null!; + for (int i = 0; i < MAX_KEYS; i++) + { + Keys[i] = byte.MaxValue; + } + } + } + + public const int MAX_KEYS = 32 + 1; // DSE is currently set to use 32 channels + public const int MAX_TRACKS = 18; // PMD2 has a few songs with 18 tracks + + public ushort Tempo; + public readonly Track[] Tracks; + + public SongState() + { + Tracks = new Track[MAX_TRACKS]; + for (int i = 0; i < MAX_TRACKS; i++) + { + Tracks[i] = new Track(); + } + } + + public void Reset() + { + Tempo = 0; + for (int i = 0; i < MAX_TRACKS; i++) + { + Tracks[i].Reset(); + } + } +} diff --git a/VG Music Studio - Core/Util/BetterExceptions.cs b/VG Music Studio - Core/Util/BetterExceptions.cs new file mode 100644 index 00000000..e0d7054f --- /dev/null +++ b/VG Music Studio - Core/Util/BetterExceptions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.Util; + +internal sealed class InvalidValueException : Exception +{ + public object Value { get; } + + public InvalidValueException(object value, string message) + : base(message) + { + Value = value; + } +} +internal sealed class BetterKeyNotFoundException : KeyNotFoundException +{ + public object Key { get; } + + public BetterKeyNotFoundException(object key, Exception? innerException) + : base($"\"{key}\" was not present in the dictionary.", innerException) + { + Key = key; + } +} diff --git a/VG Music Studio - Core/Util/ConfigUtils.cs b/VG Music Studio - Core/Util/ConfigUtils.cs new file mode 100644 index 00000000..5238c515 --- /dev/null +++ b/VG Music Studio - Core/Util/ConfigUtils.cs @@ -0,0 +1,157 @@ +using Kermalis.VGMusicStudio.Core.Properties; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using YamlDotNet.RepresentationModel; + +namespace Kermalis.VGMusicStudio.Core.Util; + +public static class ConfigUtils +{ + public const string PROGRAM_NAME = "VG Music Studio"; + private static ReadOnlySpan Notes => new string[12] { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" }; + private static readonly CultureInfo _enUS = new("en-US"); + private static readonly Dictionary _keyCache = new(128); + + public static bool TryParseValue(string value, long minValue, long maxValue, out long outValue) + { + try + { + outValue = ParseValue(string.Empty, value, minValue, maxValue); + return true; + } + catch + { + outValue = default; + return false; + } + } + /// + public static long ParseValue(string valueName, string value, long minValue, long maxValue) + { + string GetMessage() + { + return string.Format(Strings.ErrorValueParseRanged, valueName, minValue, maxValue); + } + + if (value.StartsWith("0x") && long.TryParse(value.AsSpan(2), NumberStyles.HexNumber, _enUS, out long hexp)) + { + if (hexp < minValue || hexp > maxValue) + { + throw new InvalidValueException(hexp, GetMessage()); + } + return hexp; + } + else if (long.TryParse(value, NumberStyles.Integer, _enUS, out long dec)) + { + if (dec < minValue || dec > maxValue) + { + throw new InvalidValueException(dec, GetMessage()); + } + return dec; + } + else if (long.TryParse(value, NumberStyles.HexNumber, _enUS, out long hex)) + { + if (hex < minValue || hex > maxValue) + { + throw new InvalidValueException(hex, GetMessage()); + } + return hex; + } + throw new InvalidValueException(value, string.Format(Strings.ErrorValueParse, valueName)); + } + /// + public static bool ParseBoolean(string valueName, string value) + { + if (!bool.TryParse(value, out bool result)) + { + throw new InvalidValueException(value, string.Format(Strings.ErrorBoolParse, valueName)); + } + return result; + } + /// + public static TEnum ParseEnum(string valueName, string value) + where TEnum : unmanaged + { + if (!Enum.TryParse(value, out TEnum result)) + { + throw new InvalidValueException(value, string.Format(Strings.ErrorConfigKeyInvalid, valueName)); + } + return result; + } + /// + public static TValue GetValue(this IDictionary dictionary, TKey key) + where TKey : notnull + { + try + { + return dictionary[key]; + } + catch (KeyNotFoundException ex) + { + throw new BetterKeyNotFoundException(key, ex.InnerException); + } + } + /// + /// + public static long GetValidValue(this YamlMappingNode yamlNode, string key, long minRange, long maxRange) + { + return ParseValue(key, yamlNode.Children.GetValue(key).ToString(), minRange, maxRange); + } + /// + /// + public static bool GetValidBoolean(this YamlMappingNode yamlNode, string key) + { + return ParseBoolean(key, yamlNode.Children.GetValue(key).ToString()); + } + /// + /// + public static TEnum GetValidEnum(this YamlMappingNode yamlNode, string key) + where TEnum : unmanaged + { + return ParseEnum(key, yamlNode.Children.GetValue(key).ToString()); + } + + public static void TryCreateMasterPlaylist(List playlists) + { + if (playlists.Exists(p => p.Name == "Music")) + { + return; + } + + var songs = new List(); + foreach (Config.Playlist p in playlists) + { + foreach (Config.Song s in p.Songs) + { + if (!songs.Exists(s1 => s1.Index == s.Index)) + { + songs.Add(s); + } + } + } + songs.Sort((s1, s2) => s1.Index.CompareTo(s2.Index)); + playlists.Insert(0, new Config.Playlist(Strings.PlaylistMusic, songs)); + } + + public static string CombineWithBaseDirectory(string path) + { + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path); + } + + public static string GetNoteName(int note) + { + return Notes[note]; + } + public static string GetKeyName(int midiNote) + { + if (!_keyCache.TryGetValue(midiNote, out string? str)) + { + // {C} + {5} = "C5" + str = Notes[midiNote % 12] + ((midiNote / 12) + (GlobalConfig.Instance.MiddleCOctave - 5)); + _keyCache.Add(midiNote, str); + } + return str; + } +} diff --git a/VG Music Studio - Core/Util/DataUtils.cs b/VG Music Studio - Core/Util/DataUtils.cs new file mode 100644 index 00000000..fe161806 --- /dev/null +++ b/VG Music Studio - Core/Util/DataUtils.cs @@ -0,0 +1,14 @@ +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.Util; + +internal static class DataUtils +{ + public static void Align(this Stream s, int num) + { + while (s.Position % num != 0) + { + s.Position++; + } + } +} diff --git a/VG Music Studio - Core/Util/GlobalConfig.cs b/VG Music Studio - Core/Util/GlobalConfig.cs new file mode 100644 index 00000000..87ee284b --- /dev/null +++ b/VG Music Studio - Core/Util/GlobalConfig.cs @@ -0,0 +1,90 @@ +using Kermalis.VGMusicStudio.Core.Properties; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using YamlDotNet.Core; +using YamlDotNet.RepresentationModel; + +namespace Kermalis.VGMusicStudio.Core.Util; + +public enum PlaylistMode : byte +{ + Random, + Sequential +} + +public sealed class GlobalConfig +{ + private const string CONFIG_FILE = "Config.yaml"; + + public static GlobalConfig Instance { get; private set; } = null!; + + public readonly bool TaskbarProgress; + public readonly ushort RefreshRate; + public readonly bool CenterIndicators; + public readonly bool PanpotIndicators; + public readonly PlaylistMode PlaylistMode; + public readonly long PlaylistSongLoops; + public readonly long PlaylistFadeOutMilliseconds; + public readonly sbyte MiddleCOctave; + public readonly Color[] Colors; + + private GlobalConfig() + { + using (StreamReader fileStream = File.OpenText(ConfigUtils.CombineWithBaseDirectory(CONFIG_FILE))) + { + try + { + var yaml = new YamlStream(); + yaml.Load(fileStream); + + var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; + TaskbarProgress = mapping.GetValidBoolean(nameof(TaskbarProgress)); + RefreshRate = (ushort)mapping.GetValidValue(nameof(RefreshRate), 1, 1000); + CenterIndicators = mapping.GetValidBoolean(nameof(CenterIndicators)); + PanpotIndicators = mapping.GetValidBoolean(nameof(PanpotIndicators)); + PlaylistMode = mapping.GetValidEnum(nameof(PlaylistMode)); + PlaylistSongLoops = mapping.GetValidValue(nameof(PlaylistSongLoops), 0, long.MaxValue); + PlaylistFadeOutMilliseconds = mapping.GetValidValue(nameof(PlaylistFadeOutMilliseconds), 0, long.MaxValue); + MiddleCOctave = (sbyte)mapping.GetValidValue(nameof(MiddleCOctave), sbyte.MinValue, sbyte.MaxValue); + + var cmap = (YamlMappingNode)mapping.Children[nameof(Colors)]; + Colors = new Color[256]; + foreach (KeyValuePair c in cmap) + { + int i = (int)ConfigUtils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Colors)), c.Key.ToString(), 0, 127); + if (!Colors[i].IsEmpty) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigColorRepeated, i))); + } + + string valueName = string.Format(Strings.ConfigKeySubkey, string.Format("{0} {1}", nameof(Colors), i)); + var co = Color.FromArgb((int)(0xFF000000 + (uint)ConfigUtils.ParseValue(valueName, c.Value.ToString(), 0x000000, 0xFFFFFF))); + Colors[i] = co; + Colors[i + 128] = co; + } + for (int i = 0; i < Colors.Length; i++) + { + if (Colors[i].IsEmpty) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigColorMissing, i))); + } + } + } + catch (BetterKeyNotFoundException ex) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigKeyMissing, ex.Key))); + } + catch (Exception ex) when (ex is InvalidValueException or YamlException) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + ex.Message)); + } + } + } + + public static void Init() + { + Instance = new GlobalConfig(); + } +} diff --git a/VG Music Studio - Core/Util/HSLColor.cs b/VG Music Studio - Core/Util/HSLColor.cs new file mode 100644 index 00000000..b2b7ee7c --- /dev/null +++ b/VG Music Studio - Core/Util/HSLColor.cs @@ -0,0 +1,148 @@ +using System; +using System.Drawing; + +namespace Kermalis.VGMusicStudio.Core.Util; + +// https://www.rapidtables.com/convert/color/rgb-to-hsl.html +// https://www.rapidtables.com/convert/color/hsl-to-rgb.html +// Not really used right now, but will be very useful if we are going to use OpenGL +public readonly struct HSLColor +{ + /// [0, 1) + public readonly double Hue; + /// [0, 1] + public readonly double Saturation; + /// [0, 1] + public readonly double Lightness; + + public HSLColor(double h, double s, double l) + { + Hue = h; + Saturation = s; + Lightness = l; + } + public HSLColor(in Color c) + { + double nR = c.R / 255.0; + double nG = c.G / 255.0; + double nB = c.B / 255.0; + + double max = Math.Max(Math.Max(nR, nG), nB); + double min = Math.Min(Math.Min(nR, nG), nB); + double delta = max - min; + + Lightness = (min + max) * 0.5; + + if (delta == 0) + { + Hue = 0; + } + else if (max == nR) + { + Hue = (nG - nB) / delta % 6 / 6; + } + else if (max == nG) + { + Hue = (((nB - nR) / delta) + 2) / 6; + } + else // max == nB + { + Hue = (((nR - nG) / delta) + 4) / 6; + } + + if (delta == 0) + { + Saturation = 0; + } + else + { + Saturation = delta / (1 - Math.Abs((2 * Lightness) - 1)); + } + } + + public Color ToColor() + { + return ToColor(Hue, Saturation, Lightness); + } + public static void ToRGB(double h, double s, double l, out double r, out double g, out double b) + { + h *= 360; + + double c = (1 - Math.Abs((2 * l) - 1)) * s; + double x = c * (1 - Math.Abs((h / 60 % 2) - 1)); + double m = l - (c * 0.5); + + if (h < 60) + { + r = c; + g = x; + b = 0; + } + else if (h < 120) + { + r = x; + g = c; + b = 0; + } + else if (h < 180) + { + r = 0; + g = c; + b = x; + } + else if (h < 240) + { + r = 0; + g = x; + b = c; + } + else if (h < 300) + { + r = x; + g = 0; + b = c; + } + else // h < 360 + { + r = c; + g = 0; + b = x; + } + + r += m; + g += m; + b += m; + } + public static Color ToColor(double h, double s, double l) + { + ToRGB(h, s, l, out double r, out double g, out double b); + return Color.FromArgb((int)(r * 255), (int)(g * 255), (int)(b * 255)); + } + + public override bool Equals(object? obj) + { + if (obj is HSLColor other) + { + return Hue == other.Hue && Saturation == other.Saturation && Lightness == other.Lightness; + } + return false; + } + public override int GetHashCode() + { + return HashCode.Combine(Hue, Saturation, Lightness); + } + + public override string ToString() + { + return $"{Hue * 360}° {Saturation:P} {Lightness:P}"; + } + + public static bool operator ==(HSLColor left, HSLColor right) + { + return left.Equals(right); + } + public static bool operator !=(HSLColor left, HSLColor right) + { + return !(left == right); + } +} diff --git a/VG Music Studio - Core/Util/LanguageUtils.cs b/VG Music Studio - Core/Util/LanguageUtils.cs new file mode 100644 index 00000000..ad1bc507 --- /dev/null +++ b/VG Music Studio - Core/Util/LanguageUtils.cs @@ -0,0 +1,30 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.Util; + +internal static class LanguageUtils +{ + // Try to handle lang strings like "мелодий|0_0|мелодия|1_1|мелодии|2_4|мелодий|5_*|" + public static string HandlePlural(int count, string str) + { + string[] split = str.Split('|', StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < split.Length; i += 2) + { + string text = split[i]; + string range = split[i + 1]; + + int rangeSplit = range.IndexOf('_'); + int rangeStart = GetPluralRangeValue(range.AsSpan(0, rangeSplit), int.MinValue); + int rangeEnd = GetPluralRangeValue(range.AsSpan(rangeSplit + 1), int.MaxValue); + if (count >= rangeStart && count <= rangeEnd) + { + return text; + } + } + throw new ArgumentOutOfRangeException(nameof(str), str, "Could not find plural entry"); + } + private static int GetPluralRangeValue(ReadOnlySpan chars, int star) + { + return chars.Length == 1 && chars[0] == '*' ? star : int.Parse(chars); + } +} diff --git a/VG Music Studio - Core/Util/SampleUtils.cs b/VG Music Studio - Core/Util/SampleUtils.cs new file mode 100644 index 00000000..cbce3fb3 --- /dev/null +++ b/VG Music Studio - Core/Util/SampleUtils.cs @@ -0,0 +1,15 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.Util; + +internal static class SampleUtils +{ + public static void PCMU8ToPCM16(ReadOnlySpan src, Span dest) + { + for (int i = 0; i < src.Length; i++) + { + byte b = src[i]; + dest[i] = (short)((b - 0x80) << 8); + } + } +} diff --git a/VG Music Studio - Core/Util/TimeBarrier.cs b/VG Music Studio - Core/Util/TimeBarrier.cs new file mode 100644 index 00000000..53793579 --- /dev/null +++ b/VG Music Studio - Core/Util/TimeBarrier.cs @@ -0,0 +1,60 @@ +using System.Diagnostics; +using System.Threading; + +namespace Kermalis.VGMusicStudio.Core.Util; + +// Credit to ipatix +// TODO: High resolution timer instead. +internal sealed class TimeBarrier +{ + private readonly Stopwatch _sw; + private readonly double _timerInterval; + private readonly double _waitInterval; + private double _lastTimeStamp; + private bool _started; + + public TimeBarrier(double ticksPerSecond) + { + _waitInterval = 1.0 / ticksPerSecond; + _started = false; + _sw = new Stopwatch(); + _timerInterval = 1.0 / Stopwatch.Frequency; + } + + public void Wait() + { + if (!_started) + { + return; + } + double totalElapsed = _sw.ElapsedTicks * _timerInterval; + double desiredTimeStamp = _lastTimeStamp + _waitInterval; + double timeToWait = desiredTimeStamp - totalElapsed; + if (timeToWait > 0) + { + Thread.Sleep((int)(timeToWait * 1_000)); + } + _lastTimeStamp = desiredTimeStamp; + } + + public void Start() + { + if (_started) + { + return; + } + _started = true; + _lastTimeStamp = 0; + _sw.Restart(); + } + + public void Stop() + { + if (!_started) + { + return; + } + _started = false; + _sw.Stop(); + } +} diff --git a/VG Music Studio - Core/VG Music Studio - Core.csproj b/VG Music Studio - Core/VG Music Studio - Core.csproj new file mode 100644 index 00000000..1d8bb4ea --- /dev/null +++ b/VG Music Studio - Core/VG Music Studio - Core.csproj @@ -0,0 +1,41 @@ + + + + net7.0 + Library + latest + Kermalis.VGMusicStudio.Core + enable + true + CA1069 + + + + + + + + + Dependencies\DLS2.dll + + + Dependencies\KMIDI.dll + + + Dependencies\SoundFont2.dll + + + + + + True + True + Strings.resx + + + PublicResXFileCodeGenerator + Strings.Designer.cs + + + + diff --git a/VG Music Studio - WinForms/MainForm.cs b/VG Music Studio - WinForms/MainForm.cs new file mode 100644 index 00000000..8820c082 --- /dev/null +++ b/VG Music Studio - WinForms/MainForm.cs @@ -0,0 +1,784 @@ +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.GBA.AlphaDream; +using Kermalis.VGMusicStudio.Core.GBA.MP2K; +using Kermalis.VGMusicStudio.Core.NDS.DSE; +using Kermalis.VGMusicStudio.Core.NDS.SDAT; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using Kermalis.VGMusicStudio.WinForms.Properties; +using Kermalis.VGMusicStudio.WinForms.Util; +using Microsoft.WindowsAPICodePack.Taskbar; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms; + +[DesignerCategory("")] +internal sealed class MainForm : ThemedForm +{ + private const int TARGET_WIDTH = 675; + private const int TARGET_HEIGHT = 675 + 1 + 125 + 24; + + public static MainForm Instance { get; } = new MainForm(); + + public readonly bool[] PianoTracks; + + private PlayingPlaylist? _playlist; + private int _curSong = -1; + + private TrackViewer? _trackViewer; + + private bool _songEnded = false; + private bool _positionBarFree = true; + private bool _autoplay = false; + + #region Controls + + private readonly MenuStrip _mainMenu; + private readonly ToolStripMenuItem _fileItem, _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem, + _dataItem, _trackViewerItem, _exportDLSItem, _exportMIDIItem, _exportSF2Item, _exportWAVItem, + _playlistItem, _endPlaylistItem; + private readonly Timer _timer; + private readonly ThemedNumeric _songNumerical; + private readonly ThemedButton _playButton, _pauseButton, _stopButton; + private readonly SplitContainer _splitContainer; + private readonly PianoControl _piano; + private readonly ColorSlider _volumeBar, _positionBar; + private readonly SongInfoControl _songInfo; + private readonly ImageComboBox _songsComboBox; + private readonly TaskbarPlayerButtons? _taskbar; + + #endregion + + private MainForm() + { + PianoTracks = new bool[SongState.MAX_TRACKS]; + for (int i = 0; i < SongState.MAX_TRACKS; i++) + { + PianoTracks[i] = true; + } + + Mixer.VolumeChanged += Mixer_VolumeChanged; + + // File Menu + _openDSEItem = new ToolStripMenuItem { Text = Strings.MenuOpenDSE }; + _openDSEItem.Click += OpenDSE; + _openAlphaDreamItem = new ToolStripMenuItem { Text = Strings.MenuOpenAlphaDream }; + _openAlphaDreamItem.Click += OpenAlphaDream; + _openMP2KItem = new ToolStripMenuItem { Text = Strings.MenuOpenMP2K }; + _openMP2KItem.Click += OpenMP2K; + _openSDATItem = new ToolStripMenuItem { Text = Strings.MenuOpenSDAT }; + _openSDATItem.Click += OpenSDAT; + _fileItem = new ToolStripMenuItem { Text = Strings.MenuFile }; + _fileItem.DropDownItems.AddRange(new ToolStripItem[] { _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem }); + + // Data Menu + _trackViewerItem = new ToolStripMenuItem { ShortcutKeys = Keys.Control | Keys.T, Text = Strings.TrackViewerTitle }; + _trackViewerItem.Click += OpenTrackViewer; + _exportDLSItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveDLS }; + _exportDLSItem.Click += ExportDLS; + _exportMIDIItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveMIDI }; + _exportMIDIItem.Click += ExportMIDI; + _exportSF2Item = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveSF2 }; + _exportSF2Item.Click += ExportSF2; + _exportWAVItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveWAV }; + _exportWAVItem.Click += ExportWAV; + _dataItem = new ToolStripMenuItem { Text = Strings.MenuData }; + _dataItem.DropDownItems.AddRange(new ToolStripItem[] { _trackViewerItem, _exportDLSItem, _exportMIDIItem, _exportSF2Item, _exportWAVItem }); + + // Playlist Menu + _endPlaylistItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuEndPlaylist }; + _endPlaylistItem.Click += EndCurrentPlaylist; + _playlistItem = new ToolStripMenuItem { Text = Strings.MenuPlaylist }; + _playlistItem.DropDownItems.AddRange(new ToolStripItem[] { _endPlaylistItem }); + + // Main Menu + _mainMenu = new MenuStrip { Size = new Size(TARGET_WIDTH, 24) }; + _mainMenu.Items.AddRange(new ToolStripItem[] { _fileItem, _dataItem, _playlistItem }); + + // Buttons + _playButton = new ThemedButton { Enabled = false, ForeColor = Color.MediumSpringGreen, Text = Strings.PlayerPlay }; + _playButton.Click += PlayButton_Click; + _pauseButton = new ThemedButton { Enabled = false, ForeColor = Color.DeepSkyBlue, Text = Strings.PlayerPause }; + _pauseButton.Click += PauseButton_Click; + _stopButton = new ThemedButton { Enabled = false, ForeColor = Color.MediumVioletRed, Text = Strings.PlayerStop }; + _stopButton.Click += StopButton_Click; + + // Numerical + _songNumerical = new ThemedNumeric { Enabled = false, Minimum = 0, Visible = false }; + _songNumerical.ValueChanged += SongNumerical_ValueChanged; + + // Timer + _timer = new Timer(); + _timer.Tick += Timer_Tick; + + // Piano + _piano = new PianoControl(); + + // Volume bar + _volumeBar = new ColorSlider { Enabled = false, LargeChange = 20, Maximum = 100, SmallChange = 5 }; + _volumeBar.ValueChanged += VolumeBar_ValueChanged; + + // Position bar + _positionBar = new ColorSlider { AcceptKeys = false, Enabled = false, Maximum = 0 }; + _positionBar.MouseUp += PositionBar_MouseUp; + _positionBar.MouseDown += PositionBar_MouseDown; + + // Playlist box + _songsComboBox = new ImageComboBox { Enabled = false }; + _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; + + // Track info + _songInfo = new SongInfoControl { Dock = DockStyle.Fill }; + + // Split container + _splitContainer = new SplitContainer { BackColor = Theme.TitleBar, Dock = DockStyle.Fill, IsSplitterFixed = true, Orientation = Orientation.Horizontal, SplitterWidth = 1 }; + _splitContainer.Panel1.Controls.AddRange(new Control[] { _playButton, _pauseButton, _stopButton, _songNumerical, _songsComboBox, _piano, _volumeBar, _positionBar }); + _splitContainer.Panel2.Controls.Add(_songInfo); + + // MainForm + ClientSize = new Size(TARGET_WIDTH, TARGET_HEIGHT); + Controls.AddRange(new Control[] { _splitContainer, _mainMenu }); + MainMenuStrip = _mainMenu; + MinimumSize = new Size(TARGET_WIDTH + (Width - TARGET_WIDTH), TARGET_HEIGHT + (Height - TARGET_HEIGHT)); // Borders + Resize += OnResize; + Text = ConfigUtils.PROGRAM_NAME; + + // Taskbar Buttons + if (TaskbarManager.IsPlatformSupported) + { + _taskbar = new TaskbarPlayerButtons(Handle); + } + + OnResize(null, EventArgs.Empty); + } + + private void SongNumerical_ValueChanged(object? sender, EventArgs e) + { + _songsComboBox.SelectedIndexChanged -= SongsComboBox_SelectedIndexChanged; + + int index = (int)_songNumerical.Value; + Stop(); + Text = ConfigUtils.PROGRAM_NAME; + _songsComboBox.SelectedIndex = 0; + _songInfo.Reset(); + + Player player = Engine.Instance!.Player; + Config cfg = Engine.Instance.Config; + try + { + player.LoadSong(index); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, string.Format(Strings.ErrorLoadSong, cfg.GetSongName(index))); + } + + _trackViewer?.UpdateTracks(); + ILoadedSong? loadedSong = player.LoadedSong; // LoadedSong is still null when there are no tracks + if (loadedSong is not null) + { + List songs = cfg.Playlists[0].Songs; // Complete "Music" playlist is present in all configs at index 0 + int songIndex = songs.FindIndex(s => s.Index == index); + if (songIndex != -1) + { + Text = $"{ConfigUtils.PROGRAM_NAME} ― {songs[songIndex].Name}"; // TODO: Make this a func + _songsComboBox.SelectedIndex = songIndex + 1; // + 1 because the "Music" playlist is first in the combobox + } + _positionBar.Maximum = loadedSong.MaxTicks; + _positionBar.LargeChange = _positionBar.Maximum / 10; + _positionBar.SmallChange = _positionBar.LargeChange / 4; + _songInfo.SetNumTracks(loadedSong.Events.Length); + if (_autoplay) + { + Play(); + } + _positionBar.Enabled = true; + _exportWAVItem.Enabled = true; + _exportMIDIItem.Enabled = MP2KEngine.MP2KInstance is not null; + _exportDLSItem.Enabled = _exportSF2Item.Enabled = AlphaDreamEngine.AlphaDreamInstance is not null; + } + else + { + _songInfo.SetNumTracks(0); + _positionBar.Enabled = false; + _exportWAVItem.Enabled = false; + _exportMIDIItem.Enabled = false; + _exportDLSItem.Enabled = false; + _exportSF2Item.Enabled = false; + } + + _autoplay = true; + _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; + } + private void SongsComboBox_SelectedIndexChanged(object? sender, EventArgs e) + { + var item = (ImageComboBoxItem)_songsComboBox.SelectedItem; + switch (item.Item) + { + case Config.Song song: + { + SetAndLoadSong(song.Index); + break; + } + case Config.Playlist playlist: + { + if (playlist.Songs.Count > 0 + && FlexibleMessageBox.Show(string.Format(Strings.PlayPlaylistBody, Environment.NewLine + playlist), Strings.MenuPlaylist, MessageBoxButtons.YesNo) == DialogResult.Yes) + { + ResetPlaylistStuff(false); + Engine.Instance!.Player.ShouldFadeOut = true; + Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; + _endPlaylistItem.Enabled = true; + _playlist = new PlayingPlaylist(playlist); + _playlist.SetAndLoadNextSong(); + } + break; + } + } + } + private void ResetPlaylistStuff(bool numericalAndComboboxEnabled) + { + if (Engine.Instance is not null) + { + Engine.Instance.Player.ShouldFadeOut = false; + } + _curSong = -1; + _playlist = null; + _endPlaylistItem.Enabled = false; + _songNumerical.Enabled = numericalAndComboboxEnabled; + _songsComboBox.Enabled = numericalAndComboboxEnabled; + } + private void EndCurrentPlaylist(object? sender, EventArgs e) + { + if (FlexibleMessageBox.Show(Strings.EndPlaylistBody, Strings.MenuPlaylist, MessageBoxButtons.YesNo) == DialogResult.Yes) + { + ResetPlaylistStuff(true); + } + } + + private void OpenDSE(object? sender, EventArgs e) + { + var d = new FolderBrowserDialog + { + Description = Strings.MenuOpenDSE, + UseDescriptionForTitle = true, + }; + if (d.ShowDialog() != DialogResult.OK) + { + return; + } + + DisposeEngine(); + try + { + _ = new DSEEngine(d.SelectedPath); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorOpenDSE); + return; + } + + DSEConfig config = DSEEngine.DSEInstance!.Config; + FinishLoading(config.BGMFiles.Length); + _songNumerical.Visible = false; + _exportDLSItem.Visible = false; + _exportMIDIItem.Visible = false; + _exportSF2Item.Visible = false; + } + private void OpenAlphaDream(object? sender, EventArgs e) + { + string? inFile = WinFormsUtils.CreateLoadDialog(".gba", Strings.MenuOpenAlphaDream, Strings.FilterOpenGBA + " (*.gba)|*.gba"); + if (inFile is null) + { + return; + } + + DisposeEngine(); + try + { + _ = new AlphaDreamEngine(File.ReadAllBytes(inFile)); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorOpenAlphaDream); + return; + } + + AlphaDreamConfig config = AlphaDreamEngine.AlphaDreamInstance!.Config; + FinishLoading(config.SongTableSizes[0]); + _songNumerical.Visible = true; + _exportDLSItem.Visible = true; + _exportMIDIItem.Visible = false; + _exportSF2Item.Visible = true; + } + private void OpenMP2K(object? sender, EventArgs e) + { + string? inFile = WinFormsUtils.CreateLoadDialog(".gba", Strings.MenuOpenMP2K, Strings.FilterOpenGBA + " (*.gba)|*.gba"); + if (inFile is null) + { + return; + } + + DisposeEngine(); + try + { + _ = new MP2KEngine(File.ReadAllBytes(inFile)); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorOpenMP2K); + return; + } + + MP2KConfig config = MP2KEngine.MP2KInstance!.Config; + FinishLoading(config.SongTableSizes[0]); + _songNumerical.Visible = true; + _exportDLSItem.Visible = false; + _exportMIDIItem.Visible = true; + _exportSF2Item.Visible = false; + } + private void OpenSDAT(object? sender, EventArgs e) + { + string? inFile = WinFormsUtils.CreateLoadDialog(".sdat", Strings.MenuOpenSDAT, Strings.FilterOpenSDAT + " (*.sdat)|*.sdat"); + if (inFile is null) + { + return; + } + + DisposeEngine(); + try + { + using (FileStream stream = File.OpenRead(inFile)) + { + _ = new SDATEngine(new SDAT(stream)); + } + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorOpenSDAT); + return; + } + + SDATConfig config = SDATEngine.SDATInstance!.Config; + FinishLoading(config.SDAT.INFOBlock.SequenceInfos.NumEntries); + _songNumerical.Visible = true; + _exportDLSItem.Visible = false; + _exportMIDIItem.Visible = false; + _exportSF2Item.Visible = false; + } + + private void ExportDLS(object? sender, EventArgs e) + { + AlphaDreamConfig cfg = AlphaDreamEngine.AlphaDreamInstance!.Config; + string? outFile = WinFormsUtils.CreateSaveDialog(cfg.GetGameName(), ".dls", Strings.MenuSaveDLS, Strings.FilterSaveDLS + " (*.dls)|*.dls"); + if (outFile is null) + { + return; + } + + try + { + AlphaDreamSoundFontSaver_DLS.Save(cfg, outFile); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveDLS, outFile), Text); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveDLS); + } + } + private void ExportMIDI(object? sender, EventArgs e) + { + string songName = Engine.Instance!.Config.GetSongName((int)_songNumerical.Value); + string? outFile = WinFormsUtils.CreateSaveDialog(songName, ".mid", Strings.MenuSaveMIDI, Strings.FilterSaveMIDI + " (*.mid;*.midi)|*.mid;*.midi"); + if (outFile is null) + { + return; + } + + MP2KPlayer p = MP2KEngine.MP2KInstance!.Player; + var args = new MIDISaveArgs(true, false, new (int AbsoluteTick, (byte Numerator, byte Denominator))[] + { + (0, (4, 4)), + }); + + try + { + p.SaveAsMIDI(outFile, args); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveMIDI, outFile), Text); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveMIDI); + } + } + private void ExportSF2(object? sender, EventArgs e) + { + AlphaDreamConfig cfg = AlphaDreamEngine.AlphaDreamInstance!.Config; + string? outFile = WinFormsUtils.CreateSaveDialog(cfg.GetGameName(), ".sf2", Strings.MenuSaveSF2, Strings.FilterSaveSF2 + " (*.sf2)|*.sf2"); + if (outFile is null) + { + return; + } + + try + { + AlphaDreamSoundFontSaver_SF2.Save(outFile, cfg); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveSF2, outFile), Text); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveSF2); + } + } + private void ExportWAV(object? sender, EventArgs e) + { + string songName = Engine.Instance!.Config.GetSongName((int)_songNumerical.Value); + string? outFile = WinFormsUtils.CreateSaveDialog(songName, ".wav", Strings.MenuSaveWAV, Strings.FilterSaveWAV + " (*.wav)|*.wav"); + if (outFile is null) + { + return; + } + + Stop(); + + Player player = Engine.Instance.Player; + bool oldFade = player.ShouldFadeOut; + long oldLoops = player.NumLoops; + player.ShouldFadeOut = true; + player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; + + try + { + player.Record(outFile); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveWAV, outFile), Text); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveWAV); + } + + player.ShouldFadeOut = oldFade; + player.NumLoops = oldLoops; + _songEnded = false; // Don't make UI do anything about the song ended event + } + + private void Play() + { + Engine.Instance!.Player.Play(); + LetUIKnowPlayerIsPlaying(); + } + private void Pause() + { + Engine.Instance!.Player.TogglePlaying(); + if (Engine.Instance.Player.State == PlayerState.Paused) + { + _pauseButton.Text = Strings.PlayerUnpause; + _timer.Stop(); + } + else + { + _pauseButton.Text = Strings.PlayerPause; + _timer.Start(); + } + TaskbarPlayerButtons.UpdateState(); + UpdateTaskbarButtons(); + } + private void Stop() + { + Engine.Instance!.Player.Stop(); + _pauseButton.Enabled = false; + _stopButton.Enabled = false; + _pauseButton.Text = Strings.PlayerPause; + _timer.Stop(); + _songInfo.Reset(); + _piano.UpdateKeys(_songInfo.Info.Tracks, PianoTracks); + UpdatePositionIndicators(0L); + TaskbarPlayerButtons.UpdateState(); + UpdateTaskbarButtons(); + } + + private void FinishLoading(long numSongs) + { + Engine.Instance!.Player.SongEnded += Player_SongEnded; + foreach (Config.Playlist playlist in Engine.Instance.Config.Playlists) + { + _songsComboBox.Items.Add(new ImageComboBoxItem(playlist, Resources.IconPlaylist, 0)); + _songsComboBox.Items.AddRange(playlist.Songs.Select(s => new ImageComboBoxItem(s, Resources.IconSong, 1)).ToArray()); + } + _songNumerical.Maximum = numSongs - 1; +#if DEBUG + //VGMSDebug.EventScan(Engine.Instance.Config.Playlists[0].Songs, numericalVisible); +#endif + _autoplay = false; + SetAndLoadSong(Engine.Instance.Config.Playlists[0].Songs.Count == 0 ? 0 : Engine.Instance.Config.Playlists[0].Songs[0].Index); + _songsComboBox.Enabled = _songNumerical.Enabled = _playButton.Enabled = _volumeBar.Enabled = true; + UpdateTaskbarButtons(); + } + private void DisposeEngine() + { + if (Engine.Instance is not null) + { + Stop(); + Engine.Instance.Dispose(); + } + + Text = ConfigUtils.PROGRAM_NAME; + _trackViewer?.UpdateTracks(); + _taskbar?.DisableAll(); + _songsComboBox.Enabled = false; + _songNumerical.Enabled = false; + _playButton.Enabled = false; + _volumeBar.Enabled = false; + _positionBar.Enabled = false; + _songInfo.SetNumTracks(0); + _songInfo.ResetMutes(); + ResetPlaylistStuff(false); + UpdatePositionIndicators(0L); + TaskbarPlayerButtons.UpdateState(); + + _songsComboBox.SelectedIndexChanged -= SongsComboBox_SelectedIndexChanged; + _songNumerical.ValueChanged -= SongNumerical_ValueChanged; + + _songNumerical.Visible = false; + _songsComboBox.SelectedItem = null; + _songsComboBox.Items.Clear(); + + _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; + _songNumerical.ValueChanged += SongNumerical_ValueChanged; + } + private void UpdatePositionIndicators(long ticks) + { + if (_positionBarFree) + { + _positionBar.Value = ticks; + } + if (GlobalConfig.Instance.TaskbarProgress && TaskbarManager.IsPlatformSupported) + { + TaskbarManager.Instance.SetProgressValue((int)ticks, (int)_positionBar.Maximum); + } + } + private void UpdateTaskbarButtons() + { + _taskbar?.UpdateButtons(_playlist, _curSong, (int)_songNumerical.Maximum); + } + + private void OpenTrackViewer(object? sender, EventArgs e) + { + if (_trackViewer is not null) + { + _trackViewer.Focus(); + return; + } + + _trackViewer = new TrackViewer { Owner = this }; + _trackViewer.FormClosed += TrackViewer_FormClosed; + _trackViewer.Show(); + } + + public void TogglePlayback() + { + switch (Engine.Instance!.Player.State) + { + case PlayerState.Stopped: Play(); break; + case PlayerState.Paused: + case PlayerState.Playing: Pause(); break; + } + } + public void PlayPreviousSong() + { + if (_playlist is not null) + { + _playlist.UndoThenSetAndLoadPrevSong(_curSong); + } + else + { + SetAndLoadSong((int)_songNumerical.Value - 1); + } + } + public void PlayNextSong() + { + if (_playlist is not null) + { + _playlist.AdvanceThenSetAndLoadNextSong(_curSong); + } + else + { + SetAndLoadSong((int)_songNumerical.Value + 1); + } + } + public void LetUIKnowPlayerIsPlaying() + { + if (_timer.Enabled) + { + return; + } + + _pauseButton.Enabled = true; + _stopButton.Enabled = true; + _pauseButton.Text = Strings.PlayerPause; + _timer.Interval = (int)(1_000.0 / GlobalConfig.Instance.RefreshRate); + _timer.Start(); + TaskbarPlayerButtons.UpdateState(); + UpdateTaskbarButtons(); + } + public void SetAndLoadSong(int index) + { + _curSong = index; + if (_songNumerical.Value == index) + { + SongNumerical_ValueChanged(null, EventArgs.Empty); + } + else + { + _songNumerical.Value = index; + } + } + + protected override void OnFormClosing(FormClosingEventArgs e) + { + DisposeEngine(); + base.OnFormClosing(e); + } + private void OnResize(object? sender, EventArgs e) + { + if (WindowState == FormWindowState.Minimized) + { + return; + } + + _splitContainer.SplitterDistance = (int)(ClientSize.Height / 5.5) - 25; // -25 for menustrip (24) and itself (1) + + int w1 = (int)(_splitContainer.Panel1.Width / 2.35); + int h1 = (int)(_splitContainer.Panel1.Height / 5.0); + + int xoff = _splitContainer.Panel1.Width / 83; + int yoff = _splitContainer.Panel1.Height / 25; + int a, b, c, d; + + // Buttons + a = (w1 / 3) - xoff; + b = (xoff / 2) + 1; + _playButton.Location = new Point(xoff + b, yoff); + _pauseButton.Location = new Point((xoff * 2) + a + b, yoff); + _stopButton.Location = new Point((xoff * 3) + (a * 2) + b, yoff); + _playButton.Size = _pauseButton.Size = _stopButton.Size = new Size(a, h1); + c = yoff + ((h1 - 21) / 2); + _songNumerical.Location = new Point((xoff * 4) + (a * 3) + b, c); + _songNumerical.Size = new Size((int)(a / 1.175), 21); + // Song combobox + d = _splitContainer.Panel1.Width - w1 - xoff; + _songsComboBox.Location = new Point(d, c); + _songsComboBox.Size = new Size(w1, 21); + + // Volume bar + c = (int)(_splitContainer.Panel1.Height / 3.5); + _volumeBar.Location = new Point(xoff, c); + _volumeBar.Size = new Size(w1, h1); + // Position bar + _positionBar.Location = new Point(d, c); + _positionBar.Size = new Size(w1, h1); + + // Piano + _piano.Size = new Size(_splitContainer.Panel1.Width, (int)(_splitContainer.Panel1.Height / 2.5)); // Force it to initialize piano keys again + _piano.Location = new Point((_splitContainer.Panel1.Width - (_piano.WhiteKeyWidth * PianoControl.WHITE_KEY_COUNT)) / 2, _splitContainer.Panel1.Height - _piano.Height - 1); + } + protected override bool ProcessCmdKey(ref Message msg, Keys keyData) + { + if (keyData == Keys.Space && _playButton.Enabled && !_songsComboBox.Focused) + { + TogglePlayback(); + return true; + } + return base.ProcessCmdKey(ref msg, keyData); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _timer.Dispose(); + } + base.Dispose(disposing); + } + + private void Timer_Tick(object? sender, EventArgs e) + { + if (_songEnded) + { + _songEnded = false; + if (_playlist is not null) + { + _playlist.AdvanceThenSetAndLoadNextSong(_curSong); + } + else + { + Stop(); + } + } + else + { + Player player = Engine.Instance!.Player; + if (WindowState != FormWindowState.Minimized) + { + SongState info = _songInfo.Info; + player.UpdateSongState(info); + _piano.UpdateKeys(info.Tracks, PianoTracks); + _songInfo.Invalidate(); + } + UpdatePositionIndicators(player.ElapsedTicks); + } + } + private void Mixer_VolumeChanged(float volume) + { + _volumeBar.ValueChanged -= VolumeBar_ValueChanged; + _volumeBar.Value = (int)(volume * _volumeBar.Maximum); + _volumeBar.ValueChanged += VolumeBar_ValueChanged; + } + private void Player_SongEnded() + { + _songEnded = true; + } + private void VolumeBar_ValueChanged(object? sender, EventArgs e) + { + Engine.Instance!.Mixer.SetVolume(_volumeBar.Value / (float)_volumeBar.Maximum); + } + private void PositionBar_MouseUp(object? sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + Engine.Instance!.Player.SetSongPosition(_positionBar.Value); + _positionBarFree = true; + LetUIKnowPlayerIsPlaying(); + } + } + private void PositionBar_MouseDown(object? sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + _positionBarFree = false; + } + } + private void PlayButton_Click(object? sender, EventArgs e) + { + Play(); + } + private void PauseButton_Click(object? sender, EventArgs e) + { + Pause(); + } + private void StopButton_Click(object? sender, EventArgs e) + { + Stop(); + } + private void TrackViewer_FormClosed(object? sender, FormClosedEventArgs e) + { + _trackViewer = null; + } +} diff --git a/VG Music Studio - WinForms/PianoControl.cs b/VG Music Studio - WinForms/PianoControl.cs new file mode 100644 index 00000000..9c87c2d2 --- /dev/null +++ b/VG Music Studio - WinForms/PianoControl.cs @@ -0,0 +1,157 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms; + +[DesignerCategory("")] +internal sealed partial class PianoControl : Control +{ + private enum KeyType : byte + { + Black, + White, + } + + private const double BLACK_KEY_SCALE = 2.0 / 3; + public const int WHITE_KEY_COUNT = 75; + + private static ReadOnlySpan KeyTypeTable => new KeyType[12] + { + // C C# D D# E + KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White, + // F F# G G# A A# B + KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White, + }; + + private readonly PianoKey[] _keys; + public int WhiteKeyWidth; + + public PianoControl() + { + SetStyle(ControlStyles.Selectable, false); + + _keys = new PianoKey[0x80]; + for (byte k = 0; k <= 0x7F; k++) + { + var key = new PianoKey(k); + _keys[k] = key; + if (KeyTypeTable[k % 12] == KeyType.Black) + { + key.BringToFront(); + } + Controls.Add(key); + } + SetKeySizes(); + } + private void SetKeySizes() + { + WhiteKeyWidth = Width / WHITE_KEY_COUNT; + int blackKeyWidth = (int)(WhiteKeyWidth * BLACK_KEY_SCALE); + int blackKeyHeight = (int)(Height * BLACK_KEY_SCALE); + int offset = WhiteKeyWidth - (blackKeyWidth / 2); + int w = 0; + for (int k = 0; k <= 0x7F; k++) + { + PianoKey key = _keys[k]; + if (KeyTypeTable[k % 12] == KeyType.White) + { + key.Height = Height; + key.Width = WhiteKeyWidth; + key.Location = new Point(w * WhiteKeyWidth, 0); + w++; + } + else + { + key.Height = blackKeyHeight; + key.Width = blackKeyWidth; + key.Location = new Point(offset + ((w - 1) * WhiteKeyWidth)); + key.BringToFront(); + } + } + } + + public void UpdateKeys(SongState.Track[] tracks, bool[] enabledTracks) + { + for (int k = 0; k <= 0x7F; k++) + { + PianoKey key = _keys[k]; + key.PrevIsHeld = key.IsHeld; + key.IsHeld = false; + } + for (int i = SongState.MAX_TRACKS - 1; i >= 0; i--) + { + if (!enabledTracks[i]) + { + continue; + } + + SongState.Track track = tracks[i]; + for (int nk = 0; nk < SongState.MAX_KEYS; nk++) + { + byte k = track.Keys[nk]; + if (k == byte.MaxValue) + { + break; + } + + PianoKey key = _keys[k]; + key.OnBrush.Color = GlobalConfig.Instance.Colors[track.Voice]; + key.IsHeld = true; + } + } + for (int k = 0; k <= 0x7F; k++) + { + PianoKey key = _keys[k]; + if (key.IsHeld != key.PrevIsHeld) + { + key.Invalidate(); + } + } + } + + protected override void OnResize(EventArgs e) + { + SetKeySizes(); + base.OnResize(e); + } + protected override void Dispose(bool disposing) + { + if (disposing) + { + for (int k = 0; k < 0x80; k++) + { + _keys[k].Dispose(); + } + } + base.Dispose(disposing); + } +} diff --git a/VG Music Studio - WinForms/PianoControl_PianoKey.cs b/VG Music Studio - WinForms/PianoControl_PianoKey.cs new file mode 100644 index 00000000..5fb41f39 --- /dev/null +++ b/VG Music Studio - WinForms/PianoControl_PianoKey.cs @@ -0,0 +1,86 @@ +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms; + +partial class PianoControl +{ + private sealed class PianoKey : Control + { + public bool PrevIsHeld; + public bool IsHeld; + + public readonly SolidBrush OnBrush; + private readonly SolidBrush _offBrush; + + private readonly string? _cName; + + public PianoKey(byte k) + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + SetStyle(ControlStyles.Selectable, false); + + OnBrush = new(Color.Transparent); + byte c; + if (KeyTypeTable[k % 12] == KeyType.White) + { + if (k / 12 % 2 == 0) + { + c = 255; + } + else + { + c = 127; + } + } + else + { + c = 0; + } + _offBrush = new SolidBrush(Color.FromArgb(c, c, c)); + + if (k % 12 == 0) + { + _cName = ConfigUtils.GetKeyName(k); + Font = new Font(Font.FontFamily, GetFontSize()); + } + } + + private float GetFontSize() + { + return Math.Max(1, Width / 2.75f); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + OnBrush.Dispose(); + _offBrush.Dispose(); + } + base.Dispose(disposing); + } + protected override void OnPaint(PaintEventArgs e) + { + e.Graphics.FillRectangle(IsHeld ? OnBrush : _offBrush, 1, 1, Width - 2, Height - 2); + e.Graphics.DrawRectangle(Pens.Black, 0, 0, Width - 1, Height - 1); + + if (_cName is not null) + { + SizeF strSize = e.Graphics.MeasureString(_cName, Font); + float x = (Width - strSize.Width) / 2f; + float y = Height - strSize.Height - 2; + e.Graphics.DrawString(_cName, Font, Brushes.Black, new RectangleF(x, y, 0, 0)); + } + + base.OnPaint(e); + } + protected override void OnResize(EventArgs e) + { + Font = new Font(Font.FontFamily, GetFontSize()); + base.OnResize(e); + } + } +} diff --git a/VG Music Studio - WinForms/PlayingPlaylist.cs b/VG Music Studio - WinForms/PlayingPlaylist.cs new file mode 100644 index 00000000..100f88f2 --- /dev/null +++ b/VG Music Studio - WinForms/PlayingPlaylist.cs @@ -0,0 +1,49 @@ +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.Util; +using Kermalis.VGMusicStudio.WinForms.Util; +using System.Collections.Generic; +using System.Linq; + +namespace Kermalis.VGMusicStudio.WinForms; + +internal sealed class PlayingPlaylist +{ + public readonly List _playedSongs; + public readonly List _remainingSongs; + public readonly Config.Playlist _curPlaylist; + + public PlayingPlaylist(Config.Playlist play) + { + _playedSongs = new List(); + _remainingSongs = new List(); + _curPlaylist = play; + } + + public void AdvanceThenSetAndLoadNextSong(int curSong) + { + _playedSongs.Add(curSong); + SetAndLoadNextSong(); + } + public void UndoThenSetAndLoadPrevSong(int curSong) + { + int prevIndex = _playedSongs.Count - 1; + int prevSong = _playedSongs[prevIndex]; + _playedSongs.RemoveAt(prevIndex); + _remainingSongs.Insert(0, curSong); + MainForm.Instance.SetAndLoadSong(prevSong); + } + public void SetAndLoadNextSong() + { + if (_remainingSongs.Count == 0) + { + _remainingSongs.AddRange(_curPlaylist.Songs.Select(s => s.Index)); + if (GlobalConfig.Instance.PlaylistMode == PlaylistMode.Random) + { + _remainingSongs.Shuffle(); + } + } + int nextSong = _remainingSongs[0]; + _remainingSongs.RemoveAt(0); + MainForm.Instance.SetAndLoadSong(nextSong); + } +} diff --git a/VG Music Studio - WinForms/Program.cs b/VG Music Studio - WinForms/Program.cs new file mode 100644 index 00000000..24f9e896 --- /dev/null +++ b/VG Music Studio - WinForms/Program.cs @@ -0,0 +1,35 @@ +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using Kermalis.VGMusicStudio.WinForms.Util; +using System; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms; + +internal static class Program +{ + [STAThread] + private static void Main() + { +#if DEBUG + //VGMSDebug.SimulateLanguage("en"); + //VGMSDebug.SimulateLanguage("es"); + //VGMSDebug.SimulateLanguage("fr"); + //VGMSDebug.SimulateLanguage("it"); + //VGMSDebug.SimulateLanguage("ru"); + //VGMSDebug.GBAGameCodeScan(@"C:\Users\Kermalis\Documents\Emulation\GBA\Games"); +#endif + try + { + GlobalConfig.Init(); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorGlobalConfig); + return; + } + + ApplicationConfiguration.Initialize(); + Application.Run(MainForm.Instance); + } +} \ No newline at end of file diff --git a/VG Music Studio/Properties/Icon.ico b/VG Music Studio - WinForms/Properties/Icon.ico similarity index 100% rename from VG Music Studio/Properties/Icon.ico rename to VG Music Studio - WinForms/Properties/Icon.ico diff --git a/VG Music Studio/Properties/Icon16.png b/VG Music Studio - WinForms/Properties/Icon16.png similarity index 100% rename from VG Music Studio/Properties/Icon16.png rename to VG Music Studio - WinForms/Properties/Icon16.png diff --git a/VG Music Studio/Properties/Icon24.png b/VG Music Studio - WinForms/Properties/Icon24.png similarity index 100% rename from VG Music Studio/Properties/Icon24.png rename to VG Music Studio - WinForms/Properties/Icon24.png diff --git a/VG Music Studio/Properties/Icon32.png b/VG Music Studio - WinForms/Properties/Icon32.png similarity index 100% rename from VG Music Studio/Properties/Icon32.png rename to VG Music Studio - WinForms/Properties/Icon32.png diff --git a/VG Music Studio/Properties/Icon48.png b/VG Music Studio - WinForms/Properties/Icon48.png similarity index 100% rename from VG Music Studio/Properties/Icon48.png rename to VG Music Studio - WinForms/Properties/Icon48.png diff --git a/VG Music Studio/Properties/Icon528.png b/VG Music Studio - WinForms/Properties/Icon528.png similarity index 100% rename from VG Music Studio/Properties/Icon528.png rename to VG Music Studio - WinForms/Properties/Icon528.png diff --git a/VG Music Studio/Properties/Next.ico b/VG Music Studio - WinForms/Properties/Next.ico similarity index 100% rename from VG Music Studio/Properties/Next.ico rename to VG Music Studio - WinForms/Properties/Next.ico diff --git a/VG Music Studio/Properties/Next.png b/VG Music Studio - WinForms/Properties/Next.png similarity index 100% rename from VG Music Studio/Properties/Next.png rename to VG Music Studio - WinForms/Properties/Next.png diff --git a/VG Music Studio/Properties/Pause.ico b/VG Music Studio - WinForms/Properties/Pause.ico similarity index 100% rename from VG Music Studio/Properties/Pause.ico rename to VG Music Studio - WinForms/Properties/Pause.ico diff --git a/VG Music Studio/Properties/Pause.png b/VG Music Studio - WinForms/Properties/Pause.png similarity index 100% rename from VG Music Studio/Properties/Pause.png rename to VG Music Studio - WinForms/Properties/Pause.png diff --git a/VG Music Studio/Properties/Play.ico b/VG Music Studio - WinForms/Properties/Play.ico similarity index 100% rename from VG Music Studio/Properties/Play.ico rename to VG Music Studio - WinForms/Properties/Play.ico diff --git a/VG Music Studio/Properties/Play.png b/VG Music Studio - WinForms/Properties/Play.png similarity index 100% rename from VG Music Studio/Properties/Play.png rename to VG Music Studio - WinForms/Properties/Play.png diff --git a/VG Music Studio/Properties/Playlist.png b/VG Music Studio - WinForms/Properties/Playlist.png similarity index 100% rename from VG Music Studio/Properties/Playlist.png rename to VG Music Studio - WinForms/Properties/Playlist.png diff --git a/VG Music Studio/Properties/Previous.ico b/VG Music Studio - WinForms/Properties/Previous.ico similarity index 100% rename from VG Music Studio/Properties/Previous.ico rename to VG Music Studio - WinForms/Properties/Previous.ico diff --git a/VG Music Studio/Properties/Previous.png b/VG Music Studio - WinForms/Properties/Previous.png similarity index 100% rename from VG Music Studio/Properties/Previous.png rename to VG Music Studio - WinForms/Properties/Previous.png diff --git a/VG Music Studio/Properties/Resources.Designer.cs b/VG Music Studio - WinForms/Properties/Resources.Designer.cs similarity index 97% rename from VG Music Studio/Properties/Resources.Designer.cs rename to VG Music Studio - WinForms/Properties/Resources.Designer.cs index 1aa1551a..e9c83556 100644 --- a/VG Music Studio/Properties/Resources.Designer.cs +++ b/VG Music Studio - WinForms/Properties/Resources.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace Kermalis.VGMusicStudio.Properties { +namespace Kermalis.VGMusicStudio.WinForms.Properties { using System; @@ -39,7 +39,7 @@ internal Resources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Kermalis.VGMusicStudio.Properties.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Kermalis.VGMusicStudio.WinForms.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; diff --git a/VG Music Studio/Properties/Resources.resx b/VG Music Studio - WinForms/Properties/Resources.resx similarity index 100% rename from VG Music Studio/Properties/Resources.resx rename to VG Music Studio - WinForms/Properties/Resources.resx diff --git a/VG Music Studio/Properties/Song.png b/VG Music Studio - WinForms/Properties/Song.png similarity index 100% rename from VG Music Studio/Properties/Song.png rename to VG Music Studio - WinForms/Properties/Song.png diff --git a/VG Music Studio - WinForms/SongInfoControl.cs b/VG Music Studio - WinForms/SongInfoControl.cs new file mode 100644 index 00000000..36cfe499 --- /dev/null +++ b/VG Music Studio - WinForms/SongInfoControl.cs @@ -0,0 +1,374 @@ +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using Kermalis.VGMusicStudio.WinForms.Util; +using System; +using System.ComponentModel; +using System.Drawing; +using System.Text; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms; + +[DesignerCategory("")] +internal sealed class SongInfoControl : Control +{ + private const int CHECKBOX_SIZE = 15; + + private readonly CheckBox[] _mutes; + private readonly CheckBox[] _pianos; + private readonly SolidBrush _solidBrush; + private readonly Pen _pen; + + public readonly SongState Info; + private int _numTracksToDraw; + + private readonly StringBuilder _keysCache; + + private float _infoHeight, _infoY, _positionX, _keysX, _delayX, _typeEndX, _typeX, _voicesX, _row2ElementAdditionX, _yMargin, _trackHeight, _row2Offset, _tempoX; + private int _barHeight, _barStartX, _barWidth, _bwd, _barRightBoundX, _barCenterX; + + public SongInfoControl() + { + _keysCache = new StringBuilder(); + _solidBrush = new(Theme.PlayerColor); + _pen = new(Color.Transparent); + + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + SetStyle(ControlStyles.Selectable, false); + Font = new Font("Segoe UI", 10.5f, FontStyle.Regular, GraphicsUnit.Point); + Size = new Size(675, 675); + + _pianos = new CheckBox[SongState.MAX_TRACKS + 1]; + _mutes = new CheckBox[SongState.MAX_TRACKS + 1]; + for (int i = 0; i < SongState.MAX_TRACKS + 1; i++) + { + _pianos[i] = new CheckBox + { + BackColor = Color.Transparent, + Checked = true, + Size = new Size(CHECKBOX_SIZE, CHECKBOX_SIZE), + TabStop = false + }; + _pianos[i].CheckStateChanged += TogglePiano; + _mutes[i] = new CheckBox + { + BackColor = Color.Transparent, + Checked = true, + Size = new Size(CHECKBOX_SIZE, CHECKBOX_SIZE), + TabStop = false + }; + _mutes[i].CheckStateChanged += ToggleMute; + } + Controls.AddRange(_pianos); + Controls.AddRange(_mutes); + + Info = new SongState(); + SetNumTracks(0); + } + + private void TogglePiano(object? sender, EventArgs e) + { + var check = (CheckBox)sender!; + CheckBox master = _pianos[SongState.MAX_TRACKS]; + if (check == master) + { + bool b = check.CheckState != CheckState.Unchecked; + for (int i = 0; i < SongState.MAX_TRACKS; i++) + { + _pianos[i].Checked = b; + } + } + else + { + int numChecked = 0; + for (int i = 0; i < SongState.MAX_TRACKS; i++) + { + if (_pianos[i] == check) + { + MainForm.Instance.PianoTracks[i] = _pianos[i].Checked; + } + if (_pianos[i].Checked) + { + numChecked++; + } + } + master.CheckStateChanged -= TogglePiano; + master.CheckState = numChecked == SongState.MAX_TRACKS ? CheckState.Checked : (numChecked == 0 ? CheckState.Unchecked : CheckState.Indeterminate); + master.CheckStateChanged += TogglePiano; + } + } + private void ToggleMute(object? sender, EventArgs e) + { + var check = (CheckBox)sender!; + CheckBox master = _mutes[SongState.MAX_TRACKS]; + if (check == master) + { + bool b = check.CheckState != CheckState.Unchecked; + for (int i = 0; i < SongState.MAX_TRACKS; i++) + { + _mutes[i].Checked = b; + } + } + else + { + int numChecked = 0; + for (int i = 0; i < SongState.MAX_TRACKS; i++) + { + if (_mutes[i] == check) + { + Engine.Instance!.Mixer.Mutes[i] = !check.Checked; + } + if (_mutes[i].Checked) + { + numChecked++; + } + } + master.CheckStateChanged -= ToggleMute; + master.CheckState = numChecked == SongState.MAX_TRACKS ? CheckState.Checked : (numChecked == 0 ? CheckState.Unchecked : CheckState.Indeterminate); + master.CheckStateChanged += ToggleMute; + } + } + + public void Reset() + { + Info.Reset(); + Invalidate(); + } + public void SetNumTracks(int num) + { + _numTracksToDraw = num; + bool visible = num > 0; + _pianos[SongState.MAX_TRACKS].Enabled = visible; + _mutes[SongState.MAX_TRACKS].Enabled = visible; + for (int i = 0; i < SongState.MAX_TRACKS; i++) + { + visible = i < num; + _pianos[i].Visible = visible; + _mutes[i].Visible = visible; + } + OnResize(EventArgs.Empty); + } + public void ResetMutes() + { + for (int i = 0; i < SongState.MAX_TRACKS + 1; i++) + { + CheckBox mute = _mutes[i]; + mute.CheckStateChanged -= ToggleMute; + mute.CheckState = CheckState.Checked; + mute.CheckStateChanged += ToggleMute; + } + } + + protected override void OnResize(EventArgs e) + { + if (_mutes is null) + { + return; // This can run before init is finished + } + + _infoHeight = Height / 30f; + _infoY = _infoHeight - (TextRenderer.MeasureText("A", Font).Height * 1.125f); + _positionX = (CHECKBOX_SIZE * 2) + 2; + int fWidth = Width - (int)_positionX; // Width between checkboxes' edges and the window edge + _keysX = _positionX + (fWidth / 4.4f); + _delayX = _positionX + (fWidth / 7.5f); + _typeEndX = _positionX + fWidth - (fWidth / 100f); + _typeX = _typeEndX - TextRenderer.MeasureText(Strings.PlayerType, Font).Width; + _voicesX = _positionX + (fWidth / 25f); + _row2ElementAdditionX = fWidth / 15f; + + _yMargin = Height / 200f; + _trackHeight = (Height - _yMargin) / ((_numTracksToDraw < 16 ? 16 : _numTracksToDraw) * 1.04f); + _row2Offset = _trackHeight / 2.5f; + _barHeight = (int)(Height / 30.3f); + _barStartX = (int)(_positionX + (fWidth / 2.35f)); + _barWidth = (int)(fWidth / 2.95f); + _bwd = _barWidth % 2; // Add/Subtract by 1 if the bar width is odd + _barRightBoundX = _barStartX + _barWidth - _bwd; + _barCenterX = _barStartX + (_barWidth / 2); + + _tempoX = _barCenterX - (TextRenderer.MeasureText(string.Format("{0} - 999", Strings.PlayerTempo), Font).Width / 2); + + int x1 = 3; + int x2 = CHECKBOX_SIZE + 4; + int y = (int)_infoY + 3; + _mutes[SongState.MAX_TRACKS].Location = new Point(x1, y); + _pianos[SongState.MAX_TRACKS].Location = new Point(x2, y); + for (int i = 0; i < _numTracksToDraw; i++) + { + float r1y = _infoHeight + _yMargin + (i * _trackHeight); + y = (int)r1y + 4; + _mutes[i].Location = new Point(x1, y); + _pianos[i].Location = new Point(x2, y); + } + + base.OnResize(e); + } + + // TODO: This stuff shouldn't be calculated every frame (multiple ToString(), the colors, etc). + // It should be calculated after being retrieved from the player + #region Drawing + + protected override void OnPaint(PaintEventArgs e) + { + Graphics g = e.Graphics; + + _solidBrush.Color = Theme.PlayerColor; + g.FillRectangle(_solidBrush, e.ClipRectangle); + + DrawTopRow(g); + + for (int i = 0; i < _numTracksToDraw; i++) + { + SongState.Track track = Info.Tracks[i]; + + // Set color + Color color = GlobalConfig.Instance.Colors[track.Voice]; + _solidBrush.Color = color; + _pen.Color = color; + + float row1Y = _infoHeight + _yMargin + (i * _trackHeight); + float row2Y = row1Y + _row2Offset; + + DrawLeftInfo(g, track, row1Y, row2Y); + + int vBarY1 = (int)(row1Y + _yMargin); + int vBarY2 = vBarY1 + _barHeight; + + // The "Type" string has a special place alone on the right and resizes + g.DrawString(track.Type, Font, Brushes.DeepPink, _typeEndX - g.MeasureString(track.Type, Font).Width, vBarY1 + (_row2Offset / (Font.Size / 2.5f))); + + DrawVerticalBars(g, track, vBarY1, vBarY2, color); + + DrawHeldKeys(g, track, row1Y); + } + base.OnPaint(e); + } + + private void DrawTopRow(Graphics g) + { + g.DrawString(Strings.PlayerPosition, Font, Brushes.Lime, _positionX, _infoY); // Position + g.DrawString(Strings.PlayerRest, Font, Brushes.Crimson, _delayX, _infoY); // Rest + g.DrawString(Strings.PlayerNotes, Font, Brushes.Turquoise, _keysX, _infoY); // Notes + g.DrawString("L", Font, Brushes.GreenYellow, _barStartX - 5, _infoY); // L + g.DrawString(string.Format("{0} - {1}", Strings.PlayerTempo, Info.Tempo), Font, Brushes.Cyan, _tempoX, _infoY); // Tempo + g.DrawString("R", Font, Brushes.GreenYellow, _barRightBoundX - 5, _infoY); // R + g.DrawString(Strings.PlayerType, Font, Brushes.DeepPink, _typeX, _infoY); // Type + + g.DrawLine(Pens.Gold, 0, _infoHeight, Width, _infoHeight); + } + private void DrawLeftInfo(Graphics g, SongState.Track track, float row1Y, float row2Y) + { + g.DrawString(string.Format("0x{0:X}", track.Position), Font, Brushes.Lime, _positionX, row1Y); + g.DrawString(track.Rest.ToString(), Font, Brushes.Crimson, _delayX, row1Y); + + g.DrawString(track.Voice.ToString(), Font, _solidBrush, _voicesX, row2Y); + g.DrawString(track.Panpot.ToString(), Font, Brushes.OrangeRed, _voicesX + _row2ElementAdditionX, row2Y); + g.DrawString(track.Volume.ToString(), Font, Brushes.LightSeaGreen, _voicesX + (_row2ElementAdditionX * 2), row2Y); + g.DrawString(track.LFO.ToString(), Font, Brushes.SkyBlue, _voicesX + (_row2ElementAdditionX * 3), row2Y); + g.DrawString(track.PitchBend.ToString(), Font, Brushes.Purple, _voicesX + (_row2ElementAdditionX * 4), row2Y); + g.DrawString(track.Extra.ToString(), Font, Brushes.HotPink, _voicesX + (_row2ElementAdditionX * 5), row2Y); + } + private void DrawVerticalBars(Graphics g, SongState.Track track, int vBarY1, int vBarY2, in Color color) + { + g.DrawLine(Pens.GreenYellow, _barStartX, vBarY1, _barStartX, vBarY2); // Left bounds + g.DrawLine(Pens.GreenYellow, _barRightBoundX, vBarY1, _barRightBoundX, vBarY2); // Right bounds + + // Draw pan bar before velocity bar + if (GlobalConfig.Instance.PanpotIndicators) + { + int panBarX = (int)(_barStartX + (_barWidth / 2) + (_barWidth / 2 * (track.Panpot / (float)0x40))); + g.DrawLine(Pens.OrangeRed, panBarX, vBarY1, panBarX, vBarY2); + } + + // Try to draw velocity bar + var rect = new Rectangle((int)(_barStartX + (_barWidth / 2) - (track.LeftVolume * _barWidth / 2)) + _bwd, + vBarY1, + (int)((track.LeftVolume + track.RightVolume) * _barWidth / 2), + _barHeight); + if (rect.Width > 0) + { + float velocity = track.LeftVolume + track.RightVolume; + int alpha; + if (velocity >= 2f) + { + alpha = 255; + } + else + { + const int DELTA = 125; + alpha = (int)WinFormsUtils.Lerp(velocity * 0.5f, 0f, DELTA); + alpha += 255 - DELTA; + } + _solidBrush.Color = Color.FromArgb(alpha, color); + g.FillRectangle(_solidBrush, rect); + g.DrawRectangle(_pen, rect); + //_solidBrush.Color = color; + } + + // Draw center bar last + if (GlobalConfig.Instance.CenterIndicators) + { + g.DrawLine(_pen, _barCenterX, vBarY1, _barCenterX, vBarY2); + } + } + private void DrawHeldKeys(Graphics g, SongState.Track track, float row1Y) + { + string keys; + if (track.Keys[0] == byte.MaxValue) + { + if (track.PreviousKeysTime != 0) + { + track.PreviousKeysTime--; + keys = track.PreviousKeys; + } + else + { + keys = string.Empty; + } + } + else // Keys are held down + { + _keysCache.Clear(); + for (int nk = 0; nk < SongState.MAX_KEYS; nk++) + { + byte k = track.Keys[nk]; + if (k == byte.MaxValue) + { + break; + } + + string noteName = ConfigUtils.GetKeyName(k); + if (nk != 0) + { + _keysCache.Append(' ' + noteName); + } + else + { + _keysCache.Append(noteName); + } + } + keys = _keysCache.ToString(); + + track.PreviousKeysTime = GlobalConfig.Instance.RefreshRate << 2; + track.PreviousKeys = keys; + } + if (keys.Length != 0) + { + g.DrawString(keys, Font, Brushes.Turquoise, _keysX, row1Y); + } + } + + #endregion + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _solidBrush.Dispose(); + _pen.Dispose(); + } + base.Dispose(disposing); + } +} diff --git a/VG Music Studio - WinForms/TaskbarPlayerButtons.cs b/VG Music Studio - WinForms/TaskbarPlayerButtons.cs new file mode 100644 index 00000000..d6185e4f --- /dev/null +++ b/VG Music Studio - WinForms/TaskbarPlayerButtons.cs @@ -0,0 +1,81 @@ +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using Kermalis.VGMusicStudio.WinForms.Properties; +using Microsoft.WindowsAPICodePack.Taskbar; +using System; + +namespace Kermalis.VGMusicStudio.WinForms; + +internal sealed class TaskbarPlayerButtons +{ + private readonly ThumbnailToolBarButton _prevTButton, _toggleTButton, _nextTButton; + + public TaskbarPlayerButtons(IntPtr handle) + { + _prevTButton = new ThumbnailToolBarButton(Resources.IconPrevious, Strings.PlayerPreviousSong); + _prevTButton.Click += PrevTButton_Click; + _toggleTButton = new ThumbnailToolBarButton(Resources.IconPlay, Strings.PlayerPlay); + _toggleTButton.Click += ToggleTButton_Click; + _nextTButton = new ThumbnailToolBarButton(Resources.IconNext, Strings.PlayerNextSong); + _nextTButton.Click += NextTButton_Click; + _prevTButton.Enabled = _toggleTButton.Enabled = _nextTButton.Enabled = false; + TaskbarManager.Instance.ThumbnailToolBars.AddButtons(handle, _prevTButton, _toggleTButton, _nextTButton); + } + + private void PrevTButton_Click(object? sender, ThumbnailButtonClickedEventArgs e) + { + MainForm.Instance.PlayPreviousSong(); + } + private void ToggleTButton_Click(object? sender, ThumbnailButtonClickedEventArgs e) + { + MainForm.Instance.TogglePlayback(); + } + private void NextTButton_Click(object? sender, ThumbnailButtonClickedEventArgs e) + { + MainForm.Instance.PlayNextSong(); + } + + public void DisableAll() + { + _prevTButton.Enabled = false; + _toggleTButton.Enabled = false; + _nextTButton.Enabled = false; + } + public void UpdateButtons(PlayingPlaylist? playlist, int curSong, int maxSong) + { + if (playlist is not null) + { + _prevTButton.Enabled = playlist._playedSongs.Count > 0; + _nextTButton.Enabled = true; + } + else + { + _prevTButton.Enabled = curSong > 0; + _nextTButton.Enabled = curSong < maxSong; + } + switch (Engine.Instance!.Player.State) + { + case PlayerState.Stopped: _toggleTButton.Icon = Resources.IconPlay; _toggleTButton.Tooltip = Strings.PlayerPlay; break; + case PlayerState.Playing: _toggleTButton.Icon = Resources.IconPause; _toggleTButton.Tooltip = Strings.PlayerPause; break; + case PlayerState.Paused: _toggleTButton.Icon = Resources.IconPlay; _toggleTButton.Tooltip = Strings.PlayerUnpause; break; + } + _toggleTButton.Enabled = true; + } + public static void UpdateState() + { + if (!GlobalConfig.Instance.TaskbarProgress || !TaskbarManager.IsPlatformSupported) + { + return; + } + + TaskbarProgressBarState state; + switch (Engine.Instance?.Player.State) + { + case PlayerState.Playing: state = TaskbarProgressBarState.Normal; break; + case PlayerState.Paused: state = TaskbarProgressBarState.Paused; break; + default: state = TaskbarProgressBarState.NoProgress; break; + } + TaskbarManager.Instance.SetProgressState(state); + } +} diff --git a/VG Music Studio - WinForms/Theme.cs b/VG Music Studio - WinForms/Theme.cs new file mode 100644 index 00000000..c8803e6b --- /dev/null +++ b/VG Music Studio - WinForms/Theme.cs @@ -0,0 +1,163 @@ +using Kermalis.VGMusicStudio.Core.Util; +using Kermalis.VGMusicStudio.WinForms.Properties; +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms; + +internal static class Theme +{ + public static readonly Font Font = new("Segoe UI", 8f, FontStyle.Bold); + public static readonly Color + BackColor = Color.FromArgb(33, 33, 39), + BackColorDisabled = Color.FromArgb(35, 42, 47), + BackColorMouseOver = Color.FromArgb(32, 37, 47), + BorderColor = Color.FromArgb(25, 120, 186), + BorderColorDisabled = Color.FromArgb(47, 55, 60), + ForeColor = Color.FromArgb(94, 159, 230), + PlayerColor = Color.FromArgb(8, 8, 8), + SelectionColor = Color.FromArgb(7, 51, 141), + TitleBar = Color.FromArgb(16, 40, 63); + + public static Color DrainColor(Color c) + { + var hsl = new HSLColor(c); + return HSLColor.ToColor(hsl.Hue, hsl.Saturation / 2.5, hsl.Lightness); + } +} + +internal sealed class ThemedButton : Button +{ + public ThemedButton() + { + FlatAppearance.MouseOverBackColor = Theme.BackColorMouseOver; + FlatStyle = FlatStyle.Flat; + Font = Theme.Font; + ForeColor = Theme.ForeColor; + } + protected override void OnEnabledChanged(EventArgs e) + { + base.OnEnabledChanged(e); + BackColor = Enabled ? Theme.BackColor : Theme.BackColorDisabled; + FlatAppearance.BorderColor = Enabled ? Theme.BorderColor : Theme.BorderColorDisabled; + } + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + if (!Enabled) + { + TextRenderer.DrawText(e.Graphics, Text, Font, ClientRectangle, Theme.DrainColor(ForeColor), BackColor); + } + } + protected override bool ShowFocusCues => false; +} +internal sealed class ThemedLabel : Label +{ + public ThemedLabel() + { + Font = Theme.Font; + ForeColor = Theme.ForeColor; + } +} +internal class ThemedForm : Form +{ + public ThemedForm() + { + BackColor = Theme.BackColor; + Icon = Resources.Icon; + } +} +internal class ThemedPanel : Panel +{ + public ThemedPanel() + { + SetStyle(ControlStyles.UserPaint | ControlStyles.ResizeRedraw | ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); + } + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + using (var b = new SolidBrush(BackColor)) + { + e.Graphics.FillRectangle(b, e.ClipRectangle); + } + using (var b = new SolidBrush(Theme.BorderColor)) + using (var p = new Pen(b, 2)) + { + e.Graphics.DrawRectangle(p, e.ClipRectangle); + } + } + private const int WM_PAINT = 0xF; + protected override void WndProc(ref Message m) + { + if (m.Msg == WM_PAINT) + { + Invalidate(); + } + base.WndProc(ref m); + } +} +internal class ThemedTextBox : TextBox +{ + public ThemedTextBox() + { + BackColor = Theme.BackColor; + Font = Theme.Font; + ForeColor = Theme.ForeColor; + } + [DllImport("user32.dll")] + private static extern IntPtr GetWindowDC(IntPtr hWnd); + [DllImport("user32.dll")] + private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC); + [DllImport("user32.dll")] + private static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprc, IntPtr hrgn, uint flags); + private const int WM_NCPAINT = 0x85; + private const uint RDW_INVALIDATE = 0x1; + private const uint RDW_IUPDATENOW = 0x100; + private const uint RDW_FRAME = 0x400; + protected override void WndProc(ref Message m) + { + base.WndProc(ref m); + if (m.Msg == WM_NCPAINT && BorderStyle == BorderStyle.Fixed3D) + { + IntPtr hdc = GetWindowDC(Handle); + using (var g = Graphics.FromHdcInternal(hdc)) + using (var p = new Pen(Theme.BorderColor)) + { + g.DrawRectangle(p, new Rectangle(0, 0, Width - 1, Height - 1)); + } + ReleaseDC(Handle, hdc); + } + } + protected override void OnSizeChanged(EventArgs e) + { + base.OnSizeChanged(e); + RedrawWindow(Handle, IntPtr.Zero, IntPtr.Zero, RDW_FRAME | RDW_IUPDATENOW | RDW_INVALIDATE); + } +} +internal sealed class ThemedRichTextBox : RichTextBox +{ + public ThemedRichTextBox() + { + BackColor = Theme.BackColor; + Font = Theme.Font; + ForeColor = Theme.ForeColor; + SelectionColor = Theme.SelectionColor; + } +} +internal sealed class ThemedNumeric : NumericUpDown +{ + public ThemedNumeric() + { + BackColor = Theme.BackColor; + Font = new Font(Theme.Font.FontFamily, 7.5f, Theme.Font.Style); + ForeColor = Theme.ForeColor; + TextAlign = HorizontalAlignment.Center; + } + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Enabled ? Theme.BorderColor : Theme.BorderColorDisabled, ButtonBorderStyle.Solid); + } +} diff --git a/VG Music Studio - WinForms/TrackViewer.cs b/VG Music Studio - WinForms/TrackViewer.cs new file mode 100644 index 00000000..81520cc4 --- /dev/null +++ b/VG Music Studio - WinForms/TrackViewer.cs @@ -0,0 +1,112 @@ +using BrightIdeasSoftware; +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms; + +[DesignerCategory("")] +internal sealed class TrackViewer : ThemedForm +{ + private readonly ObjectListView _listView; + private readonly ComboBox _tracksBox; + + public TrackViewer() + { + const int W = (600 / 2) - 12 - 6; + const int H = 400 - 12 - 11; + + _listView = new ObjectListView + { + FullRowSelect = true, + HeaderStyle = ColumnHeaderStyle.Nonclickable, + HideSelection = false, + Location = new Point(12, 12), + MultiSelect = false, + RowFormatter = RowColorer, + ShowGroups = false, + Size = new Size(W, H), + UseFiltering = true, + UseFilterIndicator = true + }; + OLVColumn c1, c2, c3, c4; + c1 = new OLVColumn(Strings.TrackViewerEvent, "Command.Label"); + c2 = new OLVColumn(Strings.TrackViewerArguments, "Command.Arguments") { UseFiltering = false }; + c3 = new OLVColumn(Strings.TrackViewerOffset, "Offset") { AspectToStringFormat = "0x{0:X}", UseFiltering = false }; + c4 = new OLVColumn(Strings.TrackViewerTicks, "Ticks") { AspectGetter = (o) => string.Join(", ", ((SongEvent)o).Ticks), UseFiltering = false }; + c1.Width = c2.Width = c3.Width = 72; + c4.Width = 47; + c1.Hideable = c2.Hideable = c3.Hideable = c4.Hideable = false; + c1.TextAlign = c2.TextAlign = c3.TextAlign = c4.TextAlign = HorizontalAlignment.Center; + _listView.AllColumns.AddRange(new OLVColumn[] { c1, c2, c3, c4 }); + _listView.RebuildColumns(); + _listView.ItemActivate += ListView_ItemActivate; + + var panel1 = new ThemedPanel { Location = new Point(306, 12), Size = new Size(W, H) }; + _tracksBox = new ComboBox + { + Enabled = false, + Location = new Point(4, 4), + Size = new Size(100, 21) + }; + _tracksBox.SelectedIndexChanged += TracksBox_SelectedIndexChanged; + panel1.Controls.AddRange(new Control[] { _tracksBox }); + + ClientSize = new Size(600, 400); + Controls.AddRange(new Control[] { _listView, panel1 }); + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + Text = $"{ConfigUtils.PROGRAM_NAME} ― {Strings.TrackViewerTitle}"; + + UpdateTracks(); + } + + private void ListView_ItemActivate(object? sender, EventArgs e) + { + List list = ((SongEvent)_listView.SelectedItem.RowObject).Ticks; + if (list.Count > 0) + { + Engine.Instance!.Player.SetSongPosition(list[0]); + MainForm.Instance.LetUIKnowPlayerIsPlaying(); + } + } + + private void RowColorer(OLVListItem item) + { + item.BackColor = ((SongEvent)item.RowObject).Command.Color; + } + + private void TracksBox_SelectedIndexChanged(object? sender, EventArgs? e) + { + int i = _tracksBox.SelectedIndex; + if (i != -1) + { + _listView.SetObjects(Engine.Instance!.Player.LoadedSong!.Events[i]); + } + else + { + _listView.Items.Clear(); + } + } + public void UpdateTracks() + { + int numTracks = Engine.Instance?.Player.LoadedSong?.Events.Length ?? 0; + bool tracks = numTracks > 0; + _tracksBox.Enabled = tracks; + if (tracks) + { + // Track 0, Track 1, ... + _tracksBox.DataSource = Enumerable.Range(0, numTracks).Select(i => string.Format(Strings.TrackViewerTrackX, i)).ToList(); + } + else + { + _tracksBox.DataSource = null; + } + } +} \ No newline at end of file diff --git a/VG Music Studio - WinForms/Util/ColorSlider.cs b/VG Music Studio - WinForms/Util/ColorSlider.cs new file mode 100644 index 00000000..fba15168 --- /dev/null +++ b/VG Music Studio - WinForms/Util/ColorSlider.cs @@ -0,0 +1,468 @@ +#region License + +/* Copyright (c) 2017 Fabrice Lacharme + * This code is inspired from Michal Brylka + * https://www.codeproject.com/Articles/17395/Owner-drawn-trackbar-slider + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms.Util; + +[DesignerCategory(""), ToolboxBitmap(typeof(TrackBar))] +internal sealed class ColorSlider : Control +{ + private const int THUMB_SIZE = 14; + private Rectangle thumbRect; + + private long _value = 0L; + public long Value + { + get => _value; + set + { + if (value < _minimum || value > _maximum) + { + throw new ArgumentOutOfRangeException(nameof(Value), $"{nameof(Value)} must be between {nameof(Minimum)} and {nameof(Maximum)}."); + } + _value = value; + ValueChanged?.Invoke(this, EventArgs.Empty); + Invalidate(); + } + } + private long _minimum = 0L; + public long Minimum + { + get => _minimum; + set + { + if (value > _maximum) + { + throw new ArgumentOutOfRangeException(nameof(Minimum), $"{nameof(Minimum)} cannot be higher than {nameof(Maximum)}."); + } + _minimum = value; + if (_value < _minimum) + { + _value = _minimum; + ValueChanged?.Invoke(this, new EventArgs()); + } + Invalidate(); + } + } + private long _maximum = 10L; + public long Maximum + { + get => _maximum; + set + { + if (value < _minimum) + { + throw new ArgumentOutOfRangeException(nameof(Maximum), $"{nameof(Maximum)} cannot be lower than {nameof(Minimum)}."); + } + _maximum = value; + if (_value > _maximum) + { + _value = _maximum; + ValueChanged?.Invoke(this, new EventArgs()); + } + Invalidate(); + } + } + private long _smallChange = 1L; + public long SmallChange + { + get => _smallChange; + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(SmallChange), $"{nameof(SmallChange)} must be greater than or equal to 0."); + } + _smallChange = value; + } + } + private long _largeChange = 5L; + public long LargeChange + { + get => _largeChange; + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(LargeChange), $"{nameof(LargeChange)} must be greater than or equal to 0."); + } + _largeChange = value; + } + } + private bool _acceptKeys = true; + public bool AcceptKeys + { + get => _acceptKeys; + set + { + _acceptKeys = value; + SetStyle(ControlStyles.Selectable, value); + } + } + + public event EventHandler? ValueChanged; + + private readonly Color _thumbOuterColor = Color.White; + private readonly Color _thumbInnerColor = Color.White; + private readonly Color _thumbPenColor = Color.FromArgb(125, 125, 125); + private readonly Color _barInnerColor = Theme.BackColorMouseOver; + private readonly Color _elapsedPenColorTop = Theme.ForeColor; + private readonly Color _elapsedPenColorBottom = Theme.ForeColor; + private readonly Color _barPenColorTop = Color.FromArgb(85, 90, 104); + private readonly Color _barPenColorBottom = Color.FromArgb(117, 124, 140); + private readonly Color _elapsedInnerColor = Theme.BorderColor; + private readonly Color _tickColor = Color.White; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + pen.Dispose(); + } + base.Dispose(disposing); + } + public ColorSlider() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw | ControlStyles.Selectable | + ControlStyles.SupportsTransparentBackColor | ControlStyles.UserMouse | + ControlStyles.UserPaint, true); + Size = new Size(200, 48); + } + + protected override void OnPaint(PaintEventArgs e) + { + if (!Enabled) + { + Color[] c = DesaturateColors(_thumbOuterColor, _thumbInnerColor, _thumbPenColor, + _barInnerColor, + _elapsedPenColorTop, _elapsedPenColorBottom, + _barPenColorTop, _barPenColorBottom, + _elapsedInnerColor); + Draw(e, + c[0], c[1], c[2], + c[3], + c[4], c[5], + c[6], c[7], + c[8]); + } + else + { + if (mouseInRegion) + { + Color[] c = LightenColors(_thumbOuterColor, _thumbInnerColor, _thumbPenColor, + _barInnerColor, + _elapsedPenColorTop, _elapsedPenColorBottom, + _barPenColorTop, _barPenColorBottom, + _elapsedInnerColor); + Draw(e, + c[0], c[1], c[2], + c[3], + c[4], c[5], + c[6], c[7], + c[8]); + } + else + { + Draw(e, + _thumbOuterColor, _thumbInnerColor, _thumbPenColor, + _barInnerColor, + _elapsedPenColorTop, _elapsedPenColorBottom, + _barPenColorTop, _barPenColorBottom, + _elapsedInnerColor); + } + } + } + private readonly Pen pen = new(Color.Transparent); + private void Draw(PaintEventArgs e, + Color thumbOuterColorPaint, Color thumbInnerColorPaint, Color thumbPenColorPaint, + Color barInnerColorPaint, + Color elapsedTopPenColorPaint, Color elapsedBottomPenColorPaint, + Color barTopPenColorPaint, Color barBottomPenColorPaint, + Color elapsedInnerColorPaint) + { + if (Focused) + { + ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Color.FromArgb(50, elapsedTopPenColorPaint), ButtonBorderStyle.Dashed); + } + + long a = _maximum - _minimum; + long x = a == 0 ? 0 : (_value - _minimum) * (ClientRectangle.Width - THUMB_SIZE) / a; + thumbRect = new Rectangle((int)x, ClientRectangle.Y + ClientRectangle.Height / 2 - THUMB_SIZE / 2, THUMB_SIZE, THUMB_SIZE); + Rectangle barRect = ClientRectangle; + barRect.Inflate(-1, -barRect.Height / 3); + Rectangle elapsedRect = barRect; + elapsedRect.Width = thumbRect.Left + THUMB_SIZE / 2; + + pen.Color = barInnerColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y + barRect.Height / 2, barRect.X + barRect.Width, barRect.Y + barRect.Height / 2); + pen.Color = elapsedInnerColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y + barRect.Height / 2, barRect.X + elapsedRect.Width, barRect.Y + barRect.Height / 2); + pen.Color = elapsedTopPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y - 1 + barRect.Height / 2, barRect.X + elapsedRect.Width, barRect.Y - 1 + barRect.Height / 2); + pen.Color = elapsedBottomPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y + 1 + barRect.Height / 2, barRect.X + elapsedRect.Width, barRect.Y + 1 + barRect.Height / 2); + pen.Color = barTopPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X + elapsedRect.Width, barRect.Y - 1 + barRect.Height / 2, barRect.X + barRect.Width, barRect.Y - 1 + barRect.Height / 2); + pen.Color = barBottomPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X + elapsedRect.Width, barRect.Y + 1 + barRect.Height / 2, barRect.X + barRect.Width, barRect.Y + 1 + barRect.Height / 2); + pen.Color = barTopPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y - 1 + barRect.Height / 2, barRect.X, barRect.Y + barRect.Height / 2 + 1); + pen.Color = barBottomPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X + barRect.Width, barRect.Y - 1 + barRect.Height / 2, barRect.X + barRect.Width, barRect.Y + 1 + barRect.Height / 2); + + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + Color newthumbOuterColorPaint = thumbOuterColorPaint, + newthumbInnerColorPaint = thumbInnerColorPaint; + if (busyMouse) + { + newthumbOuterColorPaint = Color.FromArgb(175, thumbOuterColorPaint); + newthumbInnerColorPaint = Color.FromArgb(175, thumbInnerColorPaint); + } + using (GraphicsPath thumbPath = CreateRoundRectPath(thumbRect, THUMB_SIZE)) + { + using (var lgbThumb = new LinearGradientBrush(thumbRect, newthumbOuterColorPaint, newthumbInnerColorPaint, LinearGradientMode.Vertical) { WrapMode = WrapMode.TileFlipXY }) + { + e.Graphics.FillPath(lgbThumb, thumbPath); + } + Color newThumbPenColor = thumbPenColorPaint; + if (busyMouse || mouseInThumbRegion) + { + newThumbPenColor = ControlPaint.Dark(newThumbPenColor); + } + pen.Color = newThumbPenColor; + e.Graphics.DrawPath(pen, thumbPath); + } + + const int numTicks = 1 + 10 * (5 + 1); + int interval = 0; + int start = thumbRect.Width / 2; + int w = barRect.Width - thumbRect.Width; + int idx = 0; + pen.Color = _tickColor; + for (int i = 0; i <= 10; i++) + { + e.Graphics.DrawLine(pen, start + barRect.X + interval, ClientRectangle.Y + ClientRectangle.Height, start + barRect.X + interval, ClientRectangle.Y + ClientRectangle.Height - 5); + if (i < 10) + { + for (int j = 0; j <= 5; j++) + { + idx++; + interval = idx * w / (numTicks - 1); + } + } + } + } + + private bool mouseInRegion = false; + private bool mouseInThumbRegion = false; + private bool busyMouse = false; + private void SetValueFromPoint(Point p) + { + int x = p.X; + int margin = THUMB_SIZE / 2; + x -= margin; + _value = (long)(x * ((_maximum - _minimum) / (ClientSize.Width - 2f * margin)) + _minimum); + if (_value < _minimum) + { + _value = _minimum; + } + else if (_value > _maximum) + { + _value = _maximum; + } + ValueChanged?.Invoke(this, new EventArgs()); + } + protected override void OnEnabledChanged(EventArgs e) + { + base.OnEnabledChanged(e); + Invalidate(); + } + protected override void OnMouseEnter(EventArgs e) + { + base.OnMouseEnter(e); + mouseInRegion = true; + Invalidate(); + } + protected override void OnMouseLeave(EventArgs e) + { + base.OnMouseLeave(e); + mouseInRegion = false; + mouseInThumbRegion = false; + Invalidate(); + } + protected override void OnMouseDown(MouseEventArgs e) + { + base.OnMouseDown(e); + mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); + busyMouse = (MouseButtons & MouseButtons.Left) != MouseButtons.None; + if (busyMouse) + { + SetValueFromPoint(e.Location); + } + Invalidate(); + } + protected override void OnMouseMove(MouseEventArgs e) + { + base.OnMouseMove(e); + mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); + if (busyMouse) + { + SetValueFromPoint(e.Location); + } + Invalidate(); + } + protected override void OnMouseUp(MouseEventArgs e) + { + base.OnMouseUp(e); + mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); + bool old = busyMouse; + busyMouse = (!old || e.Button != MouseButtons.Left) && old; + Invalidate(); + } + protected override void OnGotFocus(EventArgs e) + { + base.OnGotFocus(e); + Invalidate(); + } + protected override void OnLostFocus(EventArgs e) + { + base.OnLostFocus(e); + Invalidate(); + } + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (_acceptKeys && !busyMouse) + { + switch (e.KeyCode) + { + case Keys.Down: + case Keys.Left: + { + long newVal = _value - _smallChange; + if (newVal < _minimum) + { + newVal = _minimum; + } + Value = newVal; + break; + } + case Keys.Up: + case Keys.Right: + { + long newVal = _value + _smallChange; + if (newVal > _maximum) + { + newVal = _maximum; + } + Value = newVal; + break; + } + case Keys.Home: + { + Value = _minimum; + break; + } + case Keys.End: + { + Value = _maximum; + break; + } + case Keys.PageDown: + { + long newVal = _value - _largeChange; + if (newVal < _minimum) + { + newVal = _minimum; + } + Value = newVal; + break; + } + case Keys.PageUp: + { + long newVal = _value + _largeChange; + if (newVal > _maximum) + { + newVal = _maximum; + } + Value = newVal; + break; + } + } + } + } + protected override bool ProcessDialogKey(Keys keyData) + { + return (!_acceptKeys || keyData == Keys.Tab || ModifierKeys == Keys.Shift) && base.ProcessDialogKey(keyData); + } + + private static GraphicsPath CreateRoundRectPath(Rectangle rect, int size) + { + var gp = new GraphicsPath(); + gp.AddLine(rect.Left + size / 2, rect.Top, rect.Right - size / 2, rect.Top); + gp.AddArc(rect.Right - size, rect.Top, size, size, 270, 90); + + gp.AddLine(rect.Right, rect.Top + size / 2, rect.Right, rect.Bottom - size / 2); + gp.AddArc(rect.Right - size, rect.Bottom - size, size, size, 0, 90); + + gp.AddLine(rect.Right - size / 2, rect.Bottom, rect.Left + size / 2, rect.Bottom); + gp.AddArc(rect.Left, rect.Bottom - size, size, size, 90, 90); + + gp.AddLine(rect.Left, rect.Bottom - size / 2, rect.Left, rect.Top + size / 2); + gp.AddArc(rect.Left, rect.Top, size, size, 180, 90); + return gp; + } + private static Color[] DesaturateColors(params Color[] colors) + { + var ret = new Color[colors.Length]; + for (int i = 0; i < colors.Length; i++) + { + int gray = (int)(colors[i].R * 0.3 + colors[i].G * 0.6 + colors[i].B * 0.1); + ret[i] = Color.FromArgb(-0x010101 * (255 - gray) - 1); + } + return ret; + } + private static Color[] LightenColors(params Color[] colors) + { + var ret = new Color[colors.Length]; + for (int i = 0; i < colors.Length; i++) + { + ret[i] = ControlPaint.Light(colors[i]); + } + return ret; + } + private static bool IsPointInRect(Point p, Rectangle rect) + { + return p.X > rect.Left & p.X < rect.Right & p.Y > rect.Top & p.Y < rect.Bottom; + } +} diff --git a/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs b/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs new file mode 100644 index 00000000..05c12e9b --- /dev/null +++ b/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs @@ -0,0 +1,708 @@ +using Kermalis.VGMusicStudio.WinForms.Properties; +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Globalization; +using System.Linq; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms.Util; + +/* FlexibleMessageBox – A flexible replacement for the .NET MessageBox + * + * Author: Jörg Reichert (public@jreichert.de) + * Contributors: Thanks to: David Hall, Roink + * Version: 1.3 + * Published at: http://www.codeproject.com/Articles/601900/FlexibleMessageBox + * + ************************************************************************************************************ + * Features: + * - It can be simply used instead of MessageBox since all important static "Show"-Functions are supported + * - It is small, only one source file, which could be added easily to each solution + * - It can be resized and the content is correctly word-wrapped + * - It tries to auto-size the width to show the longest text row + * - It never exceeds the current desktop working area + * - It displays a vertical scrollbar when needed + * - It does support hyperlinks in text + * + * Because the interface is identical to MessageBox, you can add this single source file to your project + * and use the FlexibleMessageBox almost everywhere you use a standard MessageBox. + * The goal was NOT to produce as many features as possible but to provide a simple replacement to fit my + * own needs. Feel free to add additional features on your own, but please left my credits in this class. + * + ************************************************************************************************************ + * Usage examples: + * + * FlexibleMessageBox.Show("Just a text"); + * + * FlexibleMessageBox.Show("A text", + * "A caption"); + * + * FlexibleMessageBox.Show("Some text with a link: www.google.com", + * "Some caption", + * MessageBoxButtons.AbortRetryIgnore, + * MessageBoxIcon.Information, + * MessageBoxDefaultButton.Button2); + * + * var dialogResult = FlexibleMessageBox.Show("Do you know the answer to life the universe and everything?", + * "One short question", + * MessageBoxButtons.YesNo); + * + ************************************************************************************************************ + * THE SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS", WITHOUT WARRANTY + * OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL THE AUTHOR BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF THIS + * SOFTWARE. + * + ************************************************************************************************************ + * History: + * Version 1.3 - 19.Dezember 2014 + * - Added refactoring function GetButtonText() + * - Used CurrentUICulture instead of InstalledUICulture + * - Added more button localizations. Supported languages are now: ENGLISH, GERMAN, SPANISH, ITALIAN + * - Added standard MessageBox handling for "copy to clipboard" with + and + + * - Tab handling is now corrected (only tabbing over the visible buttons) + * - Added standard MessageBox handling for ALT-Keyboard shortcuts + * - SetDialogSizes: Refactored completely: Corrected sizing and added caption driven sizing + * + * Version 1.2 - 10.August 2013 + * - Do not ShowInTaskbar anymore (original MessageBox is also hidden in taskbar) + * - Added handling for Escape-Button + * - Adapted top right close button (red X) to behave like MessageBox (but hidden instead of deactivated) + * + * Version 1.1 - 14.June 2013 + * - Some Refactoring + * - Added internal form class + * - Added missing code comments, etc. + * + * Version 1.0 - 15.April 2013 + * - Initial Version + */ + +internal sealed class FlexibleMessageBox +{ + #region Public statics + + /// + /// Defines the maximum width for all FlexibleMessageBox instances in percent of the working area. + /// + /// Allowed values are 0.2 - 1.0 where: + /// 0.2 means: The FlexibleMessageBox can be at most half as wide as the working area. + /// 1.0 means: The FlexibleMessageBox can be as wide as the working area. + /// + /// Default is: 70% of the working area width. + /// + public static double MAX_WIDTH_FACTOR = 0.7; + + /// + /// Defines the maximum height for all FlexibleMessageBox instances in percent of the working area. + /// + /// Allowed values are 0.2 - 1.0 where: + /// 0.2 means: The FlexibleMessageBox can be at most half as high as the working area. + /// 1.0 means: The FlexibleMessageBox can be as high as the working area. + /// + /// Default is: 90% of the working area height. + /// + public static double MAX_HEIGHT_FACTOR = 0.9; + + /// + /// Defines the font for all FlexibleMessageBox instances. + /// + /// Default is: Theme.Font + /// + public static Font FONT = Theme.Font; + + #endregion + + #region Public show functions + + public static DialogResult Show(string text) + { + return FlexibleMessageBoxForm.Show(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(IWin32Window owner, string text) + { + return FlexibleMessageBoxForm.Show(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(string text, string caption) + { + return FlexibleMessageBoxForm.Show(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(Exception ex, string caption) + { + return FlexibleMessageBoxForm.Show(null, string.Format("Error Details:{1}{1}{0}{1}{2}", ex.Message, Environment.NewLine, ex.StackTrace), caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(IWin32Window owner, string text, string caption) + { + return FlexibleMessageBoxForm.Show(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(string text, string caption, MessageBoxButtons buttons) + { + return FlexibleMessageBoxForm.Show(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons) + { + return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) + { + return FlexibleMessageBoxForm.Show(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) + { + return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) + { + return FlexibleMessageBoxForm.Show(null, text, caption, buttons, icon, defaultButton); + } + public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) + { + return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, icon, defaultButton); + } + + #endregion + + #region Internal form class + + private sealed class FlexibleMessageBoxForm : ThemedForm + { + IContainer components; + + protected override void Dispose(bool disposing) + { + if (disposing && components is not null) + { + components.Dispose(); + } + base.Dispose(disposing); + } + void InitializeComponent() + { + components = new Container(); + button1 = new ThemedButton(); + richTextBoxMessage = new ThemedRichTextBox(); + FlexibleMessageBoxFormBindingSource = new BindingSource(components); + panel1 = new ThemedPanel(); + pictureBoxForIcon = new PictureBox(); + button2 = new ThemedButton(); + button3 = new ThemedButton(); + ((ISupportInitialize)FlexibleMessageBoxFormBindingSource).BeginInit(); + panel1.SuspendLayout(); + ((ISupportInitialize)pictureBoxForIcon).BeginInit(); + SuspendLayout(); + // + // button1 + // + button1.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + button1.AutoSize = true; + button1.DialogResult = DialogResult.OK; + button1.Location = new Point(11, 67); + button1.MinimumSize = new Size(0, 24); + button1.Name = "button1"; + button1.Size = new Size(75, 24); + button1.TabIndex = 2; + button1.Text = "OK"; + button1.UseVisualStyleBackColor = true; + button1.Visible = false; + // + // richTextBoxMessage + // + richTextBoxMessage.Anchor = AnchorStyles.Top | AnchorStyles.Bottom + | AnchorStyles.Left + | AnchorStyles.Right; + richTextBoxMessage.BorderStyle = BorderStyle.None; + richTextBoxMessage.DataBindings.Add(new Binding("Text", FlexibleMessageBoxFormBindingSource, "MessageText", true, DataSourceUpdateMode.OnPropertyChanged)); + richTextBoxMessage.Font = new Font(Theme.Font.FontFamily, 9); + richTextBoxMessage.Location = new Point(50, 26); + richTextBoxMessage.Margin = new Padding(0); + richTextBoxMessage.Name = "richTextBoxMessage"; + richTextBoxMessage.ReadOnly = true; + richTextBoxMessage.ScrollBars = RichTextBoxScrollBars.Vertical; + richTextBoxMessage.Size = new Size(200, 20); + richTextBoxMessage.TabIndex = 0; + richTextBoxMessage.TabStop = false; + richTextBoxMessage.Text = ""; + richTextBoxMessage.LinkClicked += new LinkClickedEventHandler(LinkClicked); + // + // panel1 + // + panel1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom + | AnchorStyles.Left + | AnchorStyles.Right; + panel1.Controls.Add(pictureBoxForIcon); + panel1.Controls.Add(richTextBoxMessage); + panel1.Location = new Point(-3, -4); + panel1.Name = "panel1"; + panel1.Size = new Size(268, 59); + panel1.TabIndex = 1; + // + // pictureBoxForIcon + // + pictureBoxForIcon.BackColor = Color.Transparent; + pictureBoxForIcon.Location = new Point(15, 19); + pictureBoxForIcon.Name = "pictureBoxForIcon"; + pictureBoxForIcon.Size = new Size(32, 32); + pictureBoxForIcon.TabIndex = 8; + pictureBoxForIcon.TabStop = false; + // + // button2 + // + button2.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + button2.DialogResult = DialogResult.OK; + button2.Location = new Point(92, 67); + button2.MinimumSize = new Size(0, 24); + button2.Name = "button2"; + button2.Size = new Size(75, 24); + button2.TabIndex = 3; + button2.Text = "OK"; + button2.UseVisualStyleBackColor = true; + button2.Visible = false; + // + // button3 + // + button3.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + button3.AutoSize = true; + button3.DialogResult = DialogResult.OK; + button3.Location = new Point(173, 67); + button3.MinimumSize = new Size(0, 24); + button3.Name = "button3"; + button3.Size = new Size(75, 24); + button3.TabIndex = 0; + button3.Text = "OK"; + button3.UseVisualStyleBackColor = true; + button3.Visible = false; + // + // FlexibleMessageBoxForm + // + AutoScaleDimensions = new SizeF(6F, 13F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(260, 102); + Controls.Add(button3); + Controls.Add(button2); + Controls.Add(panel1); + Controls.Add(button1); + DataBindings.Add(new Binding("Text", FlexibleMessageBoxFormBindingSource, "CaptionText", true)); + Icon = Resources.Icon; + MaximizeBox = false; + MinimizeBox = false; + MinimumSize = new Size(276, 140); + Name = "FlexibleMessageBoxForm"; + SizeGripStyle = SizeGripStyle.Show; + StartPosition = FormStartPosition.CenterParent; + Text = ""; + Shown += new EventHandler(FlexibleMessageBoxForm_Shown); + ((ISupportInitialize)FlexibleMessageBoxFormBindingSource).EndInit(); + panel1.ResumeLayout(false); + ((ISupportInitialize)pictureBoxForIcon).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + ThemedButton button1, button2, button3; + private BindingSource FlexibleMessageBoxFormBindingSource; + ThemedRichTextBox richTextBoxMessage; + ThemedPanel panel1; + private PictureBox pictureBoxForIcon; + + #region Private constants + + //These separators are used for the "copy to clipboard" standard operation, triggered by Ctrl + C (behavior and clipboard format is like in a standard MessageBox) + static readonly string STANDARD_MESSAGEBOX_SEPARATOR_LINES = "---------------------------\n"; + static readonly string STANDARD_MESSAGEBOX_SEPARATOR_SPACES = " "; + + //These are the possible buttons (in a standard MessageBox) + private enum ButtonID { OK = 0, CANCEL, YES, NO, ABORT, RETRY, IGNORE }; + + //These are the buttons texts for different languages. + //If you want to add a new language, add it here and in the GetButtonText-Function + private enum TwoLetterISOLanguageID { en, de, es, it }; + static readonly string[] BUTTON_TEXTS_ENGLISH_EN = { "OK", "Cancel", "&Yes", "&No", "&Abort", "&Retry", "&Ignore" }; //Note: This is also the fallback language + static readonly string[] BUTTON_TEXTS_GERMAN_DE = { "OK", "Abbrechen", "&Ja", "&Nein", "&Abbrechen", "&Wiederholen", "&Ignorieren" }; + static readonly string[] BUTTON_TEXTS_SPANISH_ES = { "Aceptar", "Cancelar", "&Sí", "&No", "&Abortar", "&Reintentar", "&Ignorar" }; + static readonly string[] BUTTON_TEXTS_ITALIAN_IT = { "OK", "Annulla", "&Sì", "&No", "&Interrompi", "&Riprova", "&Ignora" }; + + #endregion + + #region Private members + + MessageBoxDefaultButton defaultButton; + int visibleButtonsCount; + readonly TwoLetterISOLanguageID languageID = TwoLetterISOLanguageID.en; + + #endregion + + #region Private constructor + + private FlexibleMessageBoxForm() + { + components = null!; + button1 = null!; + button2 = null!; + button3 = null!; + FlexibleMessageBoxFormBindingSource = null!; + richTextBoxMessage = null!; + panel1 = null!; + pictureBoxForIcon = null!; + CaptionText = null!; + MessageText = null!; + + InitializeComponent(); + + //Try to evaluate the language. If this fails, the fallback language English will be used + _ = Enum.TryParse(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, out languageID); + + KeyPreview = true; + KeyUp += FlexibleMessageBoxForm_KeyUp; + } + + #endregion + + #region Private helper functions + + static string[]? GetStringRows(string message) + { + if (string.IsNullOrEmpty(message)) + { + return null; + } + + string[] messageRows = message.Split(new char[] { '\n' }, StringSplitOptions.None); + return messageRows; + } + + string GetButtonText(ButtonID buttonID) + { + int buttonTextArrayIndex = Convert.ToInt32(buttonID); + + switch (languageID) + { + case TwoLetterISOLanguageID.de: return BUTTON_TEXTS_GERMAN_DE[buttonTextArrayIndex]; + case TwoLetterISOLanguageID.es: return BUTTON_TEXTS_SPANISH_ES[buttonTextArrayIndex]; + case TwoLetterISOLanguageID.it: return BUTTON_TEXTS_ITALIAN_IT[buttonTextArrayIndex]; + + default: return BUTTON_TEXTS_ENGLISH_EN[buttonTextArrayIndex]; + } + } + + static double GetCorrectedWorkingAreaFactor(double workingAreaFactor) + { + const double MIN_FACTOR = 0.2; + const double MAX_FACTOR = 1.0; + + if (workingAreaFactor < MIN_FACTOR) + { + return MIN_FACTOR; + } + + if (workingAreaFactor > MAX_FACTOR) + { + return MAX_FACTOR; + } + + return workingAreaFactor; + } + + static void SetDialogStartPosition(FlexibleMessageBoxForm flexibleMessageBoxForm, IWin32Window? owner) + { + // If no owner given: Center on current screen + if (owner is null) + { + var screen = Screen.FromPoint(Cursor.Position); + flexibleMessageBoxForm.StartPosition = FormStartPosition.Manual; + flexibleMessageBoxForm.Left = screen.Bounds.Left + (screen.Bounds.Width / 2) - (flexibleMessageBoxForm.Width / 2); + flexibleMessageBoxForm.Top = screen.Bounds.Top + (screen.Bounds.Height / 2) - (flexibleMessageBoxForm.Height / 2); + } + } + + static void SetDialogSizes(FlexibleMessageBoxForm flexibleMessageBoxForm, string text, string caption) + { + //First set the bounds for the maximum dialog size + flexibleMessageBoxForm.MaximumSize = new Size(Convert.ToInt32(SystemInformation.WorkingArea.Width * GetCorrectedWorkingAreaFactor(MAX_WIDTH_FACTOR)), + Convert.ToInt32(SystemInformation.WorkingArea.Height * GetCorrectedWorkingAreaFactor(MAX_HEIGHT_FACTOR))); + + //Get rows. Exit if there are no rows to render... + string[]? stringRows = GetStringRows(text); + if (stringRows is null) + { + return; + } + + //Calculate whole text height + int textHeight = TextRenderer.MeasureText(text, FONT).Height; + + //Calculate width for longest text line + const int SCROLLBAR_WIDTH_OFFSET = 15; + int longestTextRowWidth = stringRows.Max(textForRow => TextRenderer.MeasureText(textForRow, FONT).Width); + int captionWidth = TextRenderer.MeasureText(caption, SystemFonts.CaptionFont).Width; + int textWidth = Math.Max(longestTextRowWidth + SCROLLBAR_WIDTH_OFFSET, captionWidth); + + //Calculate margins + int marginWidth = flexibleMessageBoxForm.Width - flexibleMessageBoxForm.richTextBoxMessage.Width; + int marginHeight = flexibleMessageBoxForm.Height - flexibleMessageBoxForm.richTextBoxMessage.Height; + + //Set calculated dialog size (if the calculated values exceed the maximums, they were cut by windows forms automatically) + flexibleMessageBoxForm.Size = new Size(textWidth + marginWidth, + textHeight + marginHeight); + } + + static void SetDialogIcon(FlexibleMessageBoxForm flexibleMessageBoxForm, MessageBoxIcon icon) + { + switch (icon) + { + case MessageBoxIcon.Information: + flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Information.ToBitmap(); + break; + case MessageBoxIcon.Warning: + flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Warning.ToBitmap(); + break; + case MessageBoxIcon.Error: + flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Error.ToBitmap(); + break; + case MessageBoxIcon.Question: + flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Question.ToBitmap(); + break; + default: + //When no icon is used: Correct placement and width of rich text box. + flexibleMessageBoxForm.pictureBoxForIcon.Visible = false; + flexibleMessageBoxForm.richTextBoxMessage.Left -= flexibleMessageBoxForm.pictureBoxForIcon.Width; + flexibleMessageBoxForm.richTextBoxMessage.Width += flexibleMessageBoxForm.pictureBoxForIcon.Width; + break; + } + } + + static void SetDialogButtons(FlexibleMessageBoxForm flexibleMessageBoxForm, MessageBoxButtons buttons, MessageBoxDefaultButton defaultButton) + { + //Set the buttons visibilities and texts + switch (buttons) + { + case MessageBoxButtons.AbortRetryIgnore: + flexibleMessageBoxForm.visibleButtonsCount = 3; + + flexibleMessageBoxForm.button1.Visible = true; + flexibleMessageBoxForm.button1.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.ABORT); + flexibleMessageBoxForm.button1.DialogResult = DialogResult.Abort; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.RETRY); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.Retry; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.IGNORE); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.Ignore; + + flexibleMessageBoxForm.ControlBox = false; + break; + + case MessageBoxButtons.OKCancel: + flexibleMessageBoxForm.visibleButtonsCount = 2; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.OK); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.OK; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; + + flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + + case MessageBoxButtons.RetryCancel: + flexibleMessageBoxForm.visibleButtonsCount = 2; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.RETRY); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.Retry; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; + + flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + + case MessageBoxButtons.YesNo: + flexibleMessageBoxForm.visibleButtonsCount = 2; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.YES); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.Yes; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.NO); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.No; + + flexibleMessageBoxForm.ControlBox = false; + break; + + case MessageBoxButtons.YesNoCancel: + flexibleMessageBoxForm.visibleButtonsCount = 3; + + flexibleMessageBoxForm.button1.Visible = true; + flexibleMessageBoxForm.button1.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.YES); + flexibleMessageBoxForm.button1.DialogResult = DialogResult.Yes; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.NO); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.No; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; + + flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + + case MessageBoxButtons.OK: + default: + flexibleMessageBoxForm.visibleButtonsCount = 1; + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.OK); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.OK; + + flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + } + + //Set default button (used in FlexibleMessageBoxForm_Shown) + flexibleMessageBoxForm.defaultButton = defaultButton; + } + + #endregion + + #region Private event handlers + + void FlexibleMessageBoxForm_Shown(object? sender, EventArgs e) + { + int buttonIndexToFocus; + Button buttonToFocus; + + //Set the default button... + switch (defaultButton) + { + case MessageBoxDefaultButton.Button1: + default: + buttonIndexToFocus = 1; + break; + case MessageBoxDefaultButton.Button2: + buttonIndexToFocus = 2; + break; + case MessageBoxDefaultButton.Button3: + buttonIndexToFocus = 3; + break; + } + + if (buttonIndexToFocus > visibleButtonsCount) + { + buttonIndexToFocus = visibleButtonsCount; + } + + if (buttonIndexToFocus == 3) + { + buttonToFocus = button3; + } + else if (buttonIndexToFocus == 2) + { + buttonToFocus = button2; + } + else + { + buttonToFocus = button1; + } + + buttonToFocus.Focus(); + } + + void LinkClicked(object? sender, LinkClickedEventArgs e) + { + try + { + Cursor.Current = Cursors.WaitCursor; + Process.Start(e.LinkText!); + } + catch (Exception) + { + // Let the caller of FlexibleMessageBoxForm decide what to do with this exception... + throw; + } + finally + { + Cursor.Current = Cursors.Default; + } + } + + void FlexibleMessageBoxForm_KeyUp(object? sender, KeyEventArgs e) + { + //Handle standard key strikes for clipboard copy: "Ctrl + C" and "Ctrl + Insert" + if (e.Control && (e.KeyCode == Keys.C || e.KeyCode == Keys.Insert)) + { + string buttonsTextLine = (button1.Visible ? button1.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty) + + (button2.Visible ? button2.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty) + + (button3.Visible ? button3.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty); + + //Build same clipboard text like the standard .Net MessageBox + string textForClipboard = STANDARD_MESSAGEBOX_SEPARATOR_LINES + + Text + Environment.NewLine + + STANDARD_MESSAGEBOX_SEPARATOR_LINES + + richTextBoxMessage.Text + Environment.NewLine + + STANDARD_MESSAGEBOX_SEPARATOR_LINES + + buttonsTextLine.Replace("&", string.Empty) + Environment.NewLine + + STANDARD_MESSAGEBOX_SEPARATOR_LINES; + + //Set text in clipboard + Clipboard.SetText(textForClipboard); + } + } + + #endregion + + #region Properties (only used for binding) + + public string CaptionText { get; set; } + public string MessageText { get; set; } + + #endregion + + #region Public show function + + public static DialogResult Show(IWin32Window? owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) + { + //Create a new instance of the FlexibleMessageBox form + var flexibleMessageBoxForm = new FlexibleMessageBoxForm + { + ShowInTaskbar = false, + + //Bind the caption and the message text + CaptionText = caption, + MessageText = text + }; + flexibleMessageBoxForm.FlexibleMessageBoxFormBindingSource.DataSource = flexibleMessageBoxForm; + + //Set the buttons visibilities and texts. Also set a default button. + SetDialogButtons(flexibleMessageBoxForm, buttons, defaultButton); + + //Set the dialogs icon. When no icon is used: Correct placement and width of rich text box. + SetDialogIcon(flexibleMessageBoxForm, icon); + + //Set the font for all controls + flexibleMessageBoxForm.Font = FONT; + flexibleMessageBoxForm.richTextBoxMessage.Font = FONT; + + //Calculate the dialogs start size (Try to auto-size width to show longest text row). Also set the maximum dialog size. + SetDialogSizes(flexibleMessageBoxForm, text, caption); + + //Set the dialogs start position when given. Otherwise center the dialog on the current screen. + SetDialogStartPosition(flexibleMessageBoxForm, owner); + + //Show the dialog + return flexibleMessageBoxForm.ShowDialog(owner); + } + + #endregion + } //class FlexibleMessageBoxForm + + #endregion +} diff --git a/VG Music Studio - WinForms/Util/ImageComboBox.cs b/VG Music Studio - WinForms/Util/ImageComboBox.cs new file mode 100644 index 00000000..bfcb4ff5 --- /dev/null +++ b/VG Music Studio - WinForms/Util/ImageComboBox.cs @@ -0,0 +1,61 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms.Util; + +internal sealed class ImageComboBox : ComboBox +{ + private const int IMG_SIZE = 15; + private bool _open = false; + + public ImageComboBox() + { + DrawMode = DrawMode.OwnerDrawFixed; + DropDownStyle = ComboBoxStyle.DropDown; + } + + protected override void OnDrawItem(DrawItemEventArgs e) + { + e.DrawBackground(); + e.DrawFocusRectangle(); + + if (e.Index >= 0) + { + ImageComboBoxItem item = Items[e.Index] as ImageComboBoxItem ?? throw new InvalidCastException($"Item was not of type \"{nameof(ImageComboBoxItem)}\""); + int indent = _open ? item.IndentLevel : 0; + e.Graphics.DrawImage(item.Image, e.Bounds.Left + (indent * IMG_SIZE), e.Bounds.Top, IMG_SIZE, IMG_SIZE); + e.Graphics.DrawString(item.ToString(), e.Font!, new SolidBrush(e.ForeColor), e.Bounds.Left + (indent * IMG_SIZE) + IMG_SIZE, e.Bounds.Top); + } + + base.OnDrawItem(e); + } + protected override void OnDropDown(EventArgs e) + { + _open = true; + base.OnDropDown(e); + } + protected override void OnDropDownClosed(EventArgs e) + { + _open = false; + base.OnDropDownClosed(e); + } +} +internal sealed class ImageComboBoxItem +{ + public object Item { get; } + public Image Image { get; } + public int IndentLevel { get; } + + public ImageComboBoxItem(object item, Image image, int indentLevel) + { + Item = item; + Image = image; + IndentLevel = indentLevel; + } + + public override string? ToString() + { + return Item.ToString(); + } +} diff --git a/VG Music Studio - WinForms/Util/VGMSDebug.cs b/VG Music Studio - WinForms/Util/VGMSDebug.cs new file mode 100644 index 00000000..9b3c7e8d --- /dev/null +++ b/VG Music Studio - WinForms/Util/VGMSDebug.cs @@ -0,0 +1,134 @@ +#if DEBUG +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Kermalis.VGMusicStudio.WinForms.Util; + +internal static class VGMSDebug +{ + // TODO: Update + /*public static void MIDIVolumeMerger(string f1, string f2) + { + var midi1 = new MIDIFile(f1); + var midi2 = new MIDIFile(f2); + var baby = new MIDIFile(midi1.Division); + + for (int i = 0; i < midi1.Count; i++) + { + Track midi1Track = midi1[i]; + Track midi2Track = midi2[i]; + var babyTrack = new Track(); + baby.Add(babyTrack); + + for (int j = 0; j < midi1Track.Count; j++) + { + MidiEvent e1 = midi1Track.GetMidiEvent(j); + if (e1.MidiMessage is ChannelMessage cm1 && cm1.Command == ChannelCommand.Controller && cm1.Data1 == (int)ControllerType.Volume) + { + MidiEvent e2 = midi2Track.GetMidiEvent(j); + var cm2 = (ChannelMessage)e2.MidiMessage; + babyTrack.Insert(e1.AbsoluteTicks, new ChannelMessage(ChannelCommand.Controller, cm1.MidiChannel, (int)ControllerType.Volume, Math.Max(cm1.Data2, cm2.Data2))); + } + else + { + babyTrack.Insert(e1.AbsoluteTicks, e1.MidiMessage); + } + } + } + + baby.Save(f1); + baby.Save(f2); + }*/ + + public static void EventScan(List songs, bool showIndexes) + { + Console.WriteLine($"{nameof(EventScan)} started."); + + var scans = new Dictionary>(); + Player player = Engine.Instance!.Player; + foreach (Config.Song song in songs) + { + try + { + player.LoadSong(song.Index); + } + catch (Exception ex) + { + Console.WriteLine("Exception loading {0} - {1}", showIndexes ? $"song {song.Index}" : $"\"{song.Name}\"", ex.Message); + continue; + } + + if (player.LoadedSong is null) + { + continue; + } + + foreach (string cmd in player.LoadedSong.Events.Where(ev => ev is not null).SelectMany(ev => ev!).Select(ev => ev.Command.Label).Distinct()) + { + if (!scans.TryGetValue(cmd, out List? list)) + { + list = new List(); + scans.Add(cmd, list); + } + list.Add(song); + } + } + + foreach (KeyValuePair> kvp in scans.OrderBy(k => k.Key)) + { + Console.WriteLine("{0} ({1})", kvp.Key, showIndexes ? string.Join(", ", kvp.Value.Select(s => s.Index)) : string.Join(", ", kvp.Value.Select(s => s.Name))); + } + Console.WriteLine($"{nameof(EventScan)} ended."); + } + + public static void GBAGameCodeScan(string path) + { + Console.WriteLine($"{nameof(GBAGameCodeScan)} started."); + + string[] files = Directory.GetFiles(path, "*.gba", SearchOption.AllDirectories); + for (int i = 0; i < files.Length; i++) + { + string file = files[i]; + try + { + using (FileStream stream = File.OpenRead(file)) + { + var r = new EndianBinaryReader(stream, ascii: true); + + stream.Position = 0xAC; + string gameCode = r.ReadString_Count(3); + stream.Position = 0xAF; + char regionCode = r.ReadChar(); + stream.Position = 0xBC; + byte version = r.ReadByte(); + + files[i] = string.Format("Code: {0}\tRegion: {1}\tVersion: {2}\tFile: {3}", + gameCode, regionCode, version, file); + } + } + catch (Exception ex) + { + Console.WriteLine("Exception loading \"{0}\" - {1}", file, ex.Message); + } + } + + Array.Sort(files); + for (int i = 0; i < files.Length; i++) + { + Console.WriteLine(files[i]); + } + Console.WriteLine($"{nameof(GBAGameCodeScan)} ended."); + } + + public static void SimulateLanguage(string lang) + { + Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(lang); + } +} +#endif \ No newline at end of file diff --git a/VG Music Studio - WinForms/Util/WinFormsUtils.cs b/VG Music Studio - WinForms/Util/WinFormsUtils.cs new file mode 100644 index 00000000..33a07665 --- /dev/null +++ b/VG Music Studio - WinForms/Util/WinFormsUtils.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms.Util; + +internal static class WinFormsUtils +{ + private static readonly Random _rng = new(); + + public static string Print(this IEnumerable source, bool parenthesis = true) + { + string str = parenthesis ? "( " : ""; + str += string.Join(", ", source); + str += parenthesis ? " )" : ""; + return str; + } + /// Fisher-Yates Shuffle + public static void Shuffle(this IList source) + { + for (int a = 0; a < source.Count - 1; a++) + { + int b = _rng.Next(a, source.Count); + (source[b], source[a]) = (source[a], source[b]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static float Lerp(float progress, float from, float to) + { + return from + ((to - from) * progress); + } + /// Maps a value in the range [a1, a2] to [b1, b2]. Divide by zero occurs if a1 and a2 are equal + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static float Lerp(float value, float a1, float a2, float b1, float b2) + { + return b1 + ((value - a1) / (a2 - a1) * (b2 - b1)); + } + + public static string? CreateLoadDialog(string extension, string title, string filter) + { + var d = new OpenFileDialog + { + DefaultExt = extension, + ValidateNames = true, + CheckFileExists = true, + CheckPathExists = true, + Title = title, + Filter = $"{filter}|All files (*.*)|*.*", + }; + if (d.ShowDialog() == DialogResult.OK) + { + return d.FileName; + } + return null; + } + public static string? CreateSaveDialog(string fileName, string extension, string title, string filter) + { + var d = new SaveFileDialog + { + FileName = fileName, + DefaultExt = extension, + AddExtension = true, + ValidateNames = true, + CheckPathExists = true, + Title = title, + Filter = $"{filter}|All files (*.*)|*.*", + }; + if (d.ShowDialog() == DialogResult.OK) + { + return d.FileName; + } + return null; + } +} diff --git a/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj new file mode 100644 index 00000000..cd3552b0 --- /dev/null +++ b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj @@ -0,0 +1,55 @@ + + + + net7.0-windows + WinExe + latest + Kermalis.VGMusicStudio.WinForms + enable + true + true + ..\Build + + Kermalis + Kermalis + VG Music Studio + VG Music Studio + VG Music Studio + 0.3.0 + Properties\Icon.ico + False + + + + + + + + NU1701 + + + NU1701 + + + NU1701 + + + + + + + + Always + + + Always + + + Always + + + Always + + + + \ No newline at end of file diff --git a/VG Music Studio - WinForms/ValueTextBox.cs b/VG Music Studio - WinForms/ValueTextBox.cs new file mode 100644 index 00000000..f05190f1 --- /dev/null +++ b/VG Music Studio - WinForms/ValueTextBox.cs @@ -0,0 +1,100 @@ +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms; + +internal sealed class ValueTextBox : ThemedTextBox +{ + public event EventHandler? ValueChanged; + + private bool _hex = false; + public bool Hexadecimal + { + get => _hex; + set + { + _hex = value; + OnTextChanged(EventArgs.Empty); + SelectionStart = Text.Length; + } + } + private long _max = long.MaxValue; + public long Maximum + { + get => _max; + set + { + _max = value; + OnTextChanged(EventArgs.Empty); + } + } + private long _min = 0; + public long Minimum + { + get => _min; + set + { + _min = value; + OnTextChanged(EventArgs.Empty); + } + } + public long Value + { + get + { + if (TextLength > 0) + { + if (ConfigUtils.TryParseValue(Text, _min, _max, out long l)) + { + return l; + } + } + return _min; + } + set + { + int i = SelectionStart; + Text = Hexadecimal ? ("0x" + value.ToString("X")) : value.ToString(); + SelectionStart = i; + OnValueChanged(EventArgs.Empty); + } + } + + protected override void WndProc(ref Message m) + { + const int WM_NOTIFY = 0x0282; + if (m.Msg == WM_NOTIFY && m.WParam == new IntPtr(0xB)) + { + if (Hexadecimal && SelectionStart < 2) + { + SelectionStart = 2; + } + } + base.WndProc(ref m); + } + protected override void OnKeyPress(KeyPressEventArgs e) + { + e.Handled = true; // Don't pay attention to this event unless: + + if ((char.IsControl(e.KeyChar) && !(Hexadecimal && SelectionStart <= 2 && SelectionLength == 0 && e.KeyChar == (char)Keys.Back)) || // Backspace isn't used on the "0x" prefix + char.IsDigit(e.KeyChar) || // It is a digit + (e.KeyChar >= 'a' && e.KeyChar <= 'f') || // It is a letter that shows in hex + (e.KeyChar >= 'A' && e.KeyChar <= 'F')) + { + e.Handled = false; + } + base.OnKeyPress(e); + } + protected override void OnTextChanged(EventArgs e) + { + base.OnTextChanged(e); + long old = Value; + Value = old; + } + + private void OnValueChanged(EventArgs e) + { + ValueChanged?.Invoke(this, e); + } +} diff --git a/VG Music Studio.sln b/VG Music Studio.sln index 660de872..31bb2a27 100644 --- a/VG Music Studio.sln +++ b/VG Music Studio.sln @@ -1,9 +1,11 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27428.2002 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32819.101 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VG Music Studio", "VG Music Studio\VG Music Studio.csproj", "{97C8ACF8-66A3-4321-91D6-3E94EACA577F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VG Music Studio - WinForms", "VG Music Studio - WinForms\VG Music Studio - WinForms.csproj", "{646D3254-F214-4F33-991F-5D5DEB7219AA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VG Music Studio - Core", "VG Music Studio - Core\VG Music Studio - Core.csproj", "{5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +13,14 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {97C8ACF8-66A3-4321-91D6-3E94EACA577F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {97C8ACF8-66A3-4321-91D6-3E94EACA577F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {97C8ACF8-66A3-4321-91D6-3E94EACA577F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {97C8ACF8-66A3-4321-91D6-3E94EACA577F}.Release|Any CPU.Build.0 = Release|Any CPU + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Release|Any CPU.Build.0 = Release|Any CPU + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/VG Music Studio/Config.yaml b/VG Music Studio/Config.yaml deleted file mode 100644 index d4bdf7b8..00000000 --- a/VG Music Studio/Config.yaml +++ /dev/null @@ -1,137 +0,0 @@ -TaskbarProgress: True # True or False # Whether the taskbar will show the song progress -RefreshRate: 30 # RefreshRate >= 1 and RefreshRate <= 1000 # How many times a second the visual updates -CenterIndicators: False # True or False # Whether lines should be drawn for the center of panpot in the visual -PanpotIndicators: False # True or False # Whether lines should be drawn for the track's panpot in the visual -PlaylistMode: "Random" # "Random" or "Sequential" # The way the playlist will behave -PlaylistSongLoops: 0 # Loops >= 0 and Loops <= 9223372036854775807 # How many times a song should loop before fading out -PlaylistFadeOutMilliseconds: 10000 # Milliseconds >= 0 and Milliseconds <= 9223372036854775807 # How many milliseconds it should take to fade out of a song -MiddleCOctave: 4 # Octave >= --128 and Octave <= 127 # The octave that holds middle C. Used in the visual and track viewer -Colors: - 0: {H: 185, S: 240, L: 180} - 1: {H: 183, S: 240, L: 170} - 2: {H: 180, S: 240, L: 157} - 3: {H: 184, S: 240, L: 85} - 4: {H: 171, S: 240, L: 134} - 5: {H: 168, S: 240, L: 159} - 6: {H: 36, S: 240, L: 170} - 7: {H: 15, S: 240, L: 134} - 8: {H: 175, S: 240, L: 200} - 9: {H: 120, S: 240, L: 150} - 10: {H: 114, S: 240, L: 138} - 11: {H: 99, S: 240, L: 171} - 12: {H: 68, S: 240, L: 171} - 13: {H: 83, S: 240, L: 200} - 14: {H: 215, S: 240, L: 104} - 15: {H: 25, S: 240, L: 200} - 16: {H: 224, S: 240, L: 150} - 17: {H: 195, S: 240, L: 120} - 18: {H: 206, S: 240, L: 95} - 19: {H: 218, S: 240, L: 129} - 20: {H: 203, S: 240, L: 180} - 21: {H: 145, S: 240, L: 100} - 22: {H: 140, S: 240, L: 111} - 23: {H: 151, S: 240, L: 120} - 24: {H: 5, S: 240, L: 169} - 25: {H: 6, S: 240, L: 156} - 26: {H: 14, S: 240, L: 164} - 27: {H: 12, S: 240, L: 137} - 28: {H: 8, S: 240, L: 140} - 29: {H: 0, S: 240, L: 123} - 30: {H: 229, S: 240, L: 70} - 31: {H: 239, S: 240, L: 89} - 32: {H: 25, S: 180, L: 160} - 33: {H: 20, S: 180, L: 145} - 34: {H: 17, S: 180, L: 140} - 35: {H: 36, S: 240, L: 163} - 36: {H: 25, S: 180, L: 140} - 37: {H: 25, S: 210, L: 95} - 38: {H: 160, S: 0, L: 180} - 39: {H: 200, S: 240, L: 90} - 40: {H: 195, S: 240, L: 100} - 41: {H: 190, S: 240, L: 93} - 42: {H: 180, S: 240, L: 90} - 43: {H: 170, S: 240, L: 150} - 44: {H: 166, S: 240, L: 89} - 45: {H: 210, S: 240, L: 170} - 46: {H: 214, S: 240, L: 185} - 47: {H: 15, S: 135, L: 135} - 48: {H: 148, S: 240, L: 130} - 49: {H: 173, S: 240, L: 80} - 50: {H: 170, S: 240, L: 95} - 51: {H: 176, S: 240, L: 100} - 52: {H: 26, S: 240, L: 215} - 53: {H: 20, S: 240, L: 210} - 54: {H: 5, S: 240, L: 220} - 55: {H: 6, S: 240, L: 150} - 56: {H: 22, S: 240, L: 134} - 57: {H: 25, S: 240, L: 130} - 58: {H: 40, S: 240, L: 120} - 59: {H: 28, S: 240, L: 122} - 60: {H: 16, S: 240, L: 124} - 61: {H: 11, S: 240, L: 118} - 62: {H: 53, S: 240, L: 158} - 63: {H: 57, S: 240, L: 133} - 64: {H: 30, S: 240, L: 195} - 65: {H: 23, S: 240, L: 182} - 66: {H: 32, S: 240, L: 160} - 67: {H: 32, S: 240, L: 130} - 68: {H: 37, S: 240, L: 135} - 69: {H: 13, S: 240, L: 143} - 70: {H: 134, S: 240, L: 85} - 71: {H: 130, S: 240, L: 95} - 72: {H: 120, S: 240, L: 165} - 73: {H: 126, S: 240, L: 120} - 74: {H: 126, S: 240, L: 100} - 75: {H: 135, S: 240, L: 160} - 76: {H: 118, S: 240, L: 186} - 77: {H: 135, S: 240, L: 102} - 78: {H: 113, S: 240, L: 100} - 79: {H: 70, S: 240, L: 160} - 80: {H: 82, S: 240, L: 132} - 81: {H: 227, S: 240, L: 188} - 82: {H: 103, S: 240, L: 140} - 83: {H: 60, S: 240, L: 165} - 84: {H: 239, S: 240, L: 165} - 85: {H: 123, S: 240, L: 175} - 86: {H: 210, S: 240, L: 145} - 87: {H: 53, S: 240, L: 120} - 88: {H: 110, S: 240, L: 155} - 89: {H: 122, S: 240, L: 205} - 90: {H: 217, S: 240, L: 95} - 91: {H: 142, S: 240, L: 50} - 92: {H: 100, S: 240, L: 90} - 93: {H: 137, S: 240, L: 90} - 94: {H: 188, S: 240, L: 117} - 95: {H: 160, S: 240, L: 210} - 96: {H: 130, S: 240, L: 200} - 97: {H: 202, S: 240, L: 80} - 98: {H: 0, S: 240, L: 160} - 99: {H: 30, S: 240, L: 110} - 100: {H: 130, S: 240, L: 210} - 101: {H: 75, S: 240, L: 75} - 102: {H: 180, S: 240, L: 205} - 103: {H: 27, S: 200, L: 105} - 104: {H: 33, S: 200, L: 145} - 105: {H: 37, S: 220, L: 130} - 106: {H: 45, S: 240, L: 135} - 107: {H: 55, S: 240, L: 175} - 108: {H: 95, S: 240, L: 185} - 109: {H: 53, S: 240, L: 190} - 110: {H: 135, S: 240, L: 120} - 111: {H: 38, S: 240, L: 110} - 112: {H: 220, S: 240, L: 170} - 113: {H: 120, S: 80, L: 150} - 114: {H: 130, S: 120, L: 190} - 115: {H: 0, S: 80, L: 90} - 116: {H: 18, S: 125, L: 130} - 117: {H: 15, S: 70, L: 120} - 118: {H: 200, S: 80, L: 110} - 119: {H: 140, S: 60, L: 180} - 120: {H: 10, S: 240, L: 90} - 121: {H: 123, S: 156, L: 100} - 122: {H: 128, S: 240, L: 100} - 123: {H: 40, S: 240, L: 180} - 124: {H: 239, S: 200, L: 90} - 125: {H: 145, S: 10, L: 155} - 126: {H: 15, S: 80, L: 160} - 127: {H: 160, S: 80, L: 150} \ No newline at end of file diff --git a/VG Music Studio/Core/ADPCMDecoder.cs b/VG Music Studio/Core/ADPCMDecoder.cs deleted file mode 100644 index 9c8a4e94..00000000 --- a/VG Music Studio/Core/ADPCMDecoder.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core -{ - internal class ADPCMDecoder - { - private static readonly short[] _indexTable = new short[8] - { - -1, -1, -1, -1, 2, 4, 6, 8 - }; - private static readonly short[] _stepTable = new short[89] - { - 00007, 00008, 00009, 00010, 00011, 00012, 00013, 00014, - 00016, 00017, 00019, 00021, 00023, 00025, 00028, 00031, - 00034, 00037, 00041, 00045, 00050, 00055, 00060, 00066, - 00073, 00080, 00088, 00097, 00107, 00118, 00130, 00143, - 00157, 00173, 00190, 00209, 00230, 00253, 00279, 00307, - 00337, 00371, 00408, 00449, 00494, 00544, 00598, 00658, - 00724, 00796, 00876, 00963, 01060, 01166, 01282, 01411, - 01552, 01707, 01878, 02066, 02272, 02499, 02749, 03024, - 03327, 03660, 04026, 04428, 04871, 05358, 05894, 06484, - 07132, 07845, 08630, 09493, 10442, 11487, 12635, 13899, - 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, - 32767 - }; - - private readonly byte[] _data; - public short LastSample; - public short StepIndex; - public int DataOffset; - public bool OnSecondNibble; - - public ADPCMDecoder(byte[] data) - { - LastSample = (short)(data[0] | (data[1] << 8)); - StepIndex = (short)((data[2] | (data[3] << 8)) & 0x7F); - DataOffset = 4; - _data = data; - } - - public static short[] ADPCMToPCM16(byte[] data) - { - var decoder = new ADPCMDecoder(data); - short[] buffer = new short[(data.Length - 4) * 2]; - for (int i = 0; i < buffer.Length; i++) - { - buffer[i] = decoder.GetSample(); - } - return buffer; - } - - public short GetSample() - { - int val = (_data[DataOffset] >> (OnSecondNibble ? 4 : 0)) & 0xF; - short step = _stepTable[StepIndex]; - int diff = - (step / 8) + - (step / 4 * (val & 1)) + - (step / 2 * ((val >> 1) & 1)) + - (step * ((val >> 2) & 1)); - - int a = (diff * ((((val >> 3) & 1) == 1) ? -1 : 1)) + LastSample; - if (a < short.MinValue) - { - a = short.MinValue; - } - else if (a > short.MaxValue) - { - a = short.MaxValue; - } - LastSample = (short)a; - - a = StepIndex + _indexTable[val & 7]; - if (a < 0) - { - a = 0; - } - else if (a > 88) - { - a = 88; - } - StepIndex = (short)a; - - if (OnSecondNibble) - { - DataOffset++; - } - OnSecondNibble = !OnSecondNibble; - return LastSample; - } - } -} diff --git a/VG Music Studio/Core/Assembler.cs b/VG Music Studio/Core/Assembler.cs deleted file mode 100644 index 8da670d3..00000000 --- a/VG Music Studio/Core/Assembler.cs +++ /dev/null @@ -1,351 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; - -namespace Kermalis.VGMusicStudio.Core -{ - internal sealed class Assembler - { - private class Pair - { - public bool Global; - public int Offset; - } - private class Pointer - { - public string Label; - public int BinaryOffset; - } - private const string _fileErrorFormat = "{0}{3}{3}Error reading file included in line {1}:{3}{2}"; - private const string _mathErrorFormat = "{0}{3}{3}Error parsing value in line {1} (Are you missing a definition?):{3}{2}"; - private const string _cmdErrorFormat = "{0}{3}{3}Unknown command in line {1}:{3}\"{2}\""; - - public int BaseOffset { get; private set; } - private readonly List _loaded = new List(); - private readonly Dictionary _defines; - - private readonly Dictionary _labels = new Dictionary(); - private readonly List _lPointers = new List(); - private readonly List _bytes = new List(); - - public string FileName { get; } - public Endianness Endianness { get; } - public int this[string Label] => _labels[FixLabel(Label)].Offset; - public byte[] Binary => _bytes.ToArray(); - public int BinaryLength => _bytes.Count; - - public Assembler(string fileName, int baseOffset, Endianness endianness, Dictionary initialDefines = null) - { - FileName = fileName; - Endianness = endianness; - _defines = initialDefines ?? new Dictionary(); - Debug.WriteLine(Read(fileName)); - SetBaseOffset(baseOffset); - } - - public void SetBaseOffset(int baseOffset) - { - foreach (Pointer p in _lPointers) - { - // Our example label is SEQ_STUFF at the binary offset 0x1000, curBaseOffset is 0x500, baseOffset is 0x1800 - // There is a pointer (p) to SEQ_STUFF at the binary offset 0x1DFC - int oldPointer = EndianBitConverter.BytesToInt32(Binary, p.BinaryOffset, Endianness); // If there was a pointer to "SEQ_STUFF+4", the pointer would be 0x1504, at binary offset 0x1DFC - int labelOffset = oldPointer - BaseOffset; // Then labelOffset is 0x1004 (SEQ_STUFF+4) - byte[] newPointerBytes = EndianBitConverter.Int32ToBytes(baseOffset + labelOffset, Endianness); // b will contain {0x04, 0x28, 0x00, 0x00} [0x2804] (SEQ_STUFF+4 + baseOffset) - for (int i = 0; i < 4; i++) - { - _bytes[p.BinaryOffset + i] = newPointerBytes[i]; // Copy the new pointer to binary offset 0x1DF4 - } - } - BaseOffset = baseOffset; - } - - public static string FixLabel(string label) - { - string ret = ""; - for (int i = 0; i < label.Length; i++) - { - char c = label[i]; - if ((c >= 'a' && c <= 'z') || - (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9' && i > 0)) - { - ret += c; - } - else - { - ret += '_'; - } - } - return ret; - } - - // Returns a status - private string Read(string fileName) - { - if (_loaded.Contains(fileName)) - { - return $"{fileName} was already loaded"; - } - - string[] file = File.ReadAllLines(fileName); - _loaded.Add(fileName); - - for (int i = 0; i < file.Length; i++) - { - string line = file[i]; - if (string.IsNullOrWhiteSpace(line)) - { - continue; // Skip empty lines - } - - bool readingCMD = false; // If it's reading the command - string cmd = null; - var args = new List(); - string str = string.Empty; - foreach (char c in line) - { - if (c == '@') // Ignore comments from this point - { - break; - } - else if (c == '.' && cmd == null) - { - readingCMD = true; - } - else if (c == ':') // Labels - { - if (!_labels.ContainsKey(str)) - { - _labels.Add(str, new Pair()); - } - _labels[str].Offset = _bytes.Count; - str = string.Empty; - } - else if (char.IsWhiteSpace(c)) - { - if (readingCMD) // If reading the command, otherwise do nothing - { - cmd = str; - readingCMD = false; - str = string.Empty; - } - } - else if (c == ',') - { - args.Add(str); - str = string.Empty; - } - else - { - str += c; - } - } - if (cmd == null) - { - continue; // Commented line - } - - args.Add(str); // Add last string before the newline - - switch (cmd.ToLower()) - { - case "include": - { - try - { - Read(args[0].Replace("\"", string.Empty)); - } - catch - { - throw new IOException(string.Format(_fileErrorFormat, fileName, i, args[0], Environment.NewLine)); - } - break; - } - case "equ": - { - try - { - _defines.Add(args[0], ParseInt(args[1])); - } - catch - { - throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); - } - break; - } - case "global": - { - if (!_labels.ContainsKey(args[0])) - { - _labels.Add(args[0], new Pair()); - } - _labels[args[0]].Global = true; - break; - } - case "align": - { - int align = ParseInt(args[0]); - for (int a = BinaryLength % align; a < align; a++) - { - _bytes.Add(0); - } - break; - } - case "byte": - { - try - { - foreach (string a in args) - { - _bytes.Add((byte)ParseInt(a)); - } - } - catch - { - throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); - } - break; - } - case "hword": - { - try - { - foreach (string a in args) - { - _bytes.AddRange(EndianBitConverter.Int16ToBytes((short)ParseInt(a), Endianness)); - } - } - catch - { - throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); - } - break; - } - case "int": - case "word": - { - try - { - foreach (string a in args) - { - _bytes.AddRange(EndianBitConverter.Int32ToBytes(ParseInt(a), Endianness)); - } - } - catch - { - throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); - } - break; - } - case "end": - { - goto end; - } - case "section": // Ignore - { - break; - } - default: throw new NotSupportedException(string.Format(_cmdErrorFormat, fileName, i, cmd, Environment.NewLine)); - } - } - end: - return $"{fileName} loaded with no issues"; - } - - private static readonly CultureInfo _enUS = new CultureInfo("en-US"); - private int ParseInt(string value) - { - // First try regular values like "40" and "0x20" - if (value.StartsWith("0x") && int.TryParse(value.Substring(2), NumberStyles.HexNumber, _enUS, out int hex)) - { - return hex; - } - if (int.TryParse(value, NumberStyles.Integer, _enUS, out int dec)) - { - return dec; - } - // Then check if it's defined - if (_defines.TryGetValue(value, out int def)) - { - return def; - } - if (_labels.TryGetValue(value, out Pair pair)) - { - _lPointers.Add(new Pointer { Label = value, BinaryOffset = _bytes.Count }); - return pair.Offset; - } - - // Then check if it's math - bool foundMath = false; - string str = string.Empty; - int ret = 0; - bool add = true, // Add first, so the initial value is set - sub = false, - mul = false, - div = false; - for (int i = 0; i < value.Length; i++) - { - char c = value[i]; - - if (char.IsWhiteSpace(c)) // White space does nothing here - { - continue; - } - if (c == '+' || c == '-' || c == '*' || c == '/') - { - if (add) - { - ret += ParseInt(str); - } - else if (sub) - { - ret -= ParseInt(str); - } - else if (mul) - { - ret *= ParseInt(str); - } - else if (div) - { - ret /= ParseInt(str); - } - add = c == '+'; - sub = c == '-'; - mul = c == '*'; - div = c == '/'; - str = string.Empty; - foundMath = true; - } - else - { - str += c; - } - } - if (foundMath) - { - if (add) // Handle last - { - ret += ParseInt(str); - } - else if (sub) - { - ret -= ParseInt(str); - } - else if (mul) - { - ret *= ParseInt(str); - } - else if (div) - { - ret /= ParseInt(str); - } - return ret; - } - throw new ArgumentOutOfRangeException(nameof(value)); - } - } -} diff --git a/VG Music Studio/Core/Config.cs b/VG Music Studio/Core/Config.cs deleted file mode 100644 index 81bfbf62..00000000 --- a/VG Music Studio/Core/Config.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; - -namespace Kermalis.VGMusicStudio.Core -{ - internal abstract class Config : IDisposable - { - public class Song - { - public long Index; - public string Name; - - public Song(long index, string name) - { - Index = index; Name = name; - } - - public override bool Equals(object obj) - { - return obj is Song other && other.Index == Index; - } - public override int GetHashCode() - { - return Index.GetHashCode(); - } - public override string ToString() - { - return Name; - } - } - public class Playlist - { - public string Name; - public List Songs; - - public Playlist(string name, IEnumerable songs) - { - Name = name; Songs = songs.ToList(); - } - - public override string ToString() - { - int songCount = Songs.Count; - CultureInfo cul = System.Threading.Thread.CurrentThread.CurrentUICulture; - if (cul.TwoLetterISOLanguageName == "it") // Italian - { - // PlaylistName - (1 Canzone) - // PlaylistName - (2 Canzoni) - return $"{Name} - ({songCount} {(songCount == 1 ? "Canzone" : "Canzoni")})"; - } - else if (cul.TwoLetterISOLanguageName == "es") // Spanish - { - // PlaylistName - (1 Canción) - // PlaylistName - (2 Canciones) - return $"{Name} - ({songCount} {(songCount == 1 ? "Canción" : "Canciones")})"; - } - else // Fallback to en-US - { - // PlaylistName - (1 Song) - // PlaylistName - (2 Songs) - return $"{Name} - ({songCount} {(songCount == 1 ? "Song" : "Songs")})"; - } - } - } - - public List Playlists = new List(); - - public Song GetFirstSong(long index) - { - foreach (Playlist p in Playlists) - { - foreach (Song s in p.Songs) - { - if (s.Index == index) - { - return s; - } - } - } - return null; - } - - public abstract string GetGameName(); - public abstract string GetSongName(long index); - - public virtual void Dispose() { } - } -} diff --git a/VG Music Studio/Core/Engine.cs b/VG Music Studio/Core/Engine.cs deleted file mode 100644 index 57200db2..00000000 --- a/VG Music Studio/Core/Engine.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; - -namespace Kermalis.VGMusicStudio.Core -{ - internal class Engine : IDisposable - { - public enum EngineType : byte - { - None, - GBA_AlphaDream, - GBA_MP2K, - NDS_DSE, - NDS_SDAT - } - - public static Engine Instance { get; private set; } - - public EngineType Type { get; } - public Config Config { get; private set; } - public Mixer Mixer { get; private set; } - public IPlayer Player { get; private set; } - - public Engine(EngineType type, object playerArg) - { - switch (type) - { - case EngineType.GBA_AlphaDream: - { - byte[] rom = (byte[])playerArg; - if (rom.Length > GBA.Utils.CartridgeCapacity) - { - throw new Exception($"The ROM is too large. Maximum size is 0x{GBA.Utils.CartridgeCapacity:X7} bytes."); - } - var config = new GBA.AlphaDream.Config(rom); - Config = config; - var mixer = new GBA.AlphaDream.Mixer(config); - Mixer = mixer; - Player = new GBA.AlphaDream.Player(mixer, config); - break; - } - case EngineType.GBA_MP2K: - { - byte[] rom = (byte[])playerArg; - if (rom.Length > GBA.Utils.CartridgeCapacity) - { - throw new Exception($"The ROM is too large. Maximum size is 0x{GBA.Utils.CartridgeCapacity:X7} bytes."); - } - var config = new GBA.MP2K.Config(rom); - Config = config; - var mixer = new GBA.MP2K.Mixer(config); - Mixer = mixer; - Player = new GBA.MP2K.Player(mixer, config); - break; - } - case EngineType.NDS_DSE: - { - string bgmPath = (string)playerArg; - var config = new NDS.DSE.Config(bgmPath); - Config = config; - var mixer = new NDS.DSE.Mixer(); - Mixer = mixer; - Player = new NDS.DSE.Player(mixer, config); - break; - } - case EngineType.NDS_SDAT: - { - var sdat = (NDS.SDAT.SDAT)playerArg; - var config = new NDS.SDAT.Config(sdat); - Config = config; - var mixer = new NDS.SDAT.Mixer(); - Mixer = mixer; - Player = new NDS.SDAT.Player(mixer, config); - break; - } - default: throw new ArgumentOutOfRangeException(nameof(type)); - } - Type = type; - Instance = this; - } - - public void Dispose() - { - Config.Dispose(); - Config = null; - Mixer.Dispose(); - Mixer = null; - Player.Dispose(); - Player = null; - Instance = null; - } - } -} diff --git a/VG Music Studio/Core/GBA/AlphaDream/Channel.cs b/VG Music Studio/Core/GBA/AlphaDream/Channel.cs deleted file mode 100644 index 4eba3ac1..00000000 --- a/VG Music Studio/Core/GBA/AlphaDream/Channel.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal abstract class Channel - { - protected readonly Mixer _mixer; - public EnvelopeState State; - public byte Key; - public bool Stopped; - - protected ADSR _adsr; - - protected byte _velocity; - protected int _pos; - protected float _interPos; - protected float _frequency; - protected byte _leftVol; - protected byte _rightVol; - - protected Channel(Mixer mixer) - { - _mixer = mixer; - } - - public ChannelVolume GetVolume() - { - const float max = 0x10000; - return new ChannelVolume - { - LeftVol = _leftVol * _velocity / max, - RightVol = _rightVol * _velocity / max - }; - } - public void SetVolume(byte vol, sbyte pan) - { - _leftVol = (byte)((vol * (-pan + 0x80)) >> 8); - _rightVol = (byte)((vol * (pan + 0x80)) >> 8); - } - public abstract void SetPitch(int pitch); - - public abstract void Process(float[] buffer); - } - internal class PCMChannel : Channel - { - private SampleHeader _sampleHeader; - private int _sampleOffset; - private bool _bFixed; - - public PCMChannel(Mixer mixer) : base(mixer) { } - public void Init(byte key, ADSR adsr, int sampleOffset, bool bFixed) - { - _velocity = adsr.A; - State = EnvelopeState.Attack; - _pos = 0; _interPos = 0; - Key = key; - _adsr = adsr; - _sampleHeader = _mixer.Config.Reader.ReadObject(sampleOffset); - _sampleOffset = sampleOffset + 0x10; - _bFixed = bFixed; - Stopped = false; - } - - public override void SetPitch(int pitch) - { - if (_sampleHeader != null) - { - _frequency = (_sampleHeader.SampleRate >> 10) * (float)Math.Pow(2, ((Key - 60) / 12f) + (pitch / 768f)); - } - } - - private void StepEnvelope() - { - switch (State) - { - case EnvelopeState.Attack: - { - int nextVel = _velocity + _adsr.A; - if (nextVel >= 0xFF) - { - State = EnvelopeState.Decay; - _velocity = 0xFF; - } - else - { - _velocity = (byte)nextVel; - } - break; - } - case EnvelopeState.Decay: - { - int nextVel = (_velocity * _adsr.D) >> 8; - if (nextVel <= _adsr.S) - { - State = EnvelopeState.Sustain; - _velocity = _adsr.S; - } - else - { - _velocity = (byte)nextVel; - } - break; - } - case EnvelopeState.Release: - { - int next = (_velocity * _adsr.R) >> 8; - if (next < 0) - { - next = 0; - } - _velocity = (byte)next; - break; - } - } - } - - public override void Process(float[] buffer) - { - StepEnvelope(); - - ChannelVolume vol = GetVolume(); - float interStep = (_bFixed ? _sampleHeader.SampleRate >> 10 : _frequency) * _mixer.SampleRateReciprocal; - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = (_mixer.Config.ROM[_pos + _sampleOffset] - 0x80) / (float)0x80; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos += posDelta; - if (_pos >= _sampleHeader.Length) - { - if (_sampleHeader.DoesLoop == 0x40000000) - { - _pos = _sampleHeader.LoopOffset; - } - else - { - Stopped = true; - break; - } - } - } while (--samplesPerBuffer > 0); - } - } - internal class SquareChannel : Channel - { - private float[] _pat; - - public SquareChannel(Mixer mixer) : base(mixer) { } - public void Init(byte key, ADSR env, byte vol, sbyte pan, int pitch) - { - _pat = MP2K.Utils.SquareD50; // TODO - Key = key; - _adsr = env; - SetVolume(vol, pan); - SetPitch(pitch); - State = EnvelopeState.Attack; - } - - public override void SetPitch(int pitch) - { - _frequency = 3520 * (float)Math.Pow(2, ((Key - 69) / 12f) + (pitch / 768f)); - } - - private void StepEnvelope() - { - switch (State) - { - case EnvelopeState.Attack: - { - int next = _velocity + _adsr.A; - if (next >= 0xF) - { - State = EnvelopeState.Decay; - _velocity = 0xF; - } - else - { - _velocity = (byte)next; - } - break; - } - case EnvelopeState.Decay: - { - int next = (_velocity * _adsr.D) >> 3; - if (next <= _adsr.S) - { - State = EnvelopeState.Sustain; - _velocity = _adsr.S; - } - else - { - _velocity = (byte)next; - } - break; - } - case EnvelopeState.Release: - { - int next = (_velocity * _adsr.R) >> 3; - if (next < 0) - { - next = 0; - } - _velocity = (byte)next; - break; - } - } - } - - public override void Process(float[] buffer) - { - StepEnvelope(); - - ChannelVolume vol = GetVolume(); - float interStep = _frequency * _mixer.SampleRateReciprocal; - - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = _pat[_pos]; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos = (_pos + posDelta) & 0x7; - } while (--samplesPerBuffer > 0); - } - } -} diff --git a/VG Music Studio/Core/GBA/AlphaDream/Commands.cs b/VG Music Studio/Core/GBA/AlphaDream/Commands.cs deleted file mode 100644 index 50991f73..00000000 --- a/VG Music Studio/Core/GBA/AlphaDream/Commands.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Drawing; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal class FinishCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Finish"; - public string Arguments => string.Empty; - } - internal class FreeNoteHamtaroCommand : ICommand // TODO: When optimization comes, get rid of free note vs note and just have the label differ - { - public Color Color => Color.SkyBlue; - public string Label => "Free Note"; - public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Volume} {Duration}"; - - public byte Key { get; set; } - public byte Volume { get; set; } - public byte Duration { get; set; } - } - internal class FreeNoteMLSSCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Free Note"; - public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Duration}"; - - public byte Key { get; set; } - public byte Duration { get; set; } - } - internal class JumpCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Jump"; - public string Arguments => $"0x{Offset:X7}"; - - public int Offset { get; set; } - } - internal class NoteHamtaroCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Note"; - public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Volume} {Duration}"; - - public byte Key { get; set; } - public byte Volume { get; set; } - public byte Duration { get; set; } - } - internal class NoteMLSSCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Note"; - public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Duration}"; - - public byte Key { get; set; } - public byte Duration { get; set; } - } - internal class PanpotCommand : ICommand - { - public Color Color => Color.GreenYellow; - public string Label => "Panpot"; - public string Arguments => Panpot.ToString(); - - public sbyte Panpot { get; set; } - } - internal class PitchBendCommand : ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Pitch Bend"; - public string Arguments => Bend.ToString(); - - public sbyte Bend { get; set; } - } - internal class PitchBendRangeCommand : ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Pitch Bend Range"; - public string Arguments => Range.ToString(); - - public byte Range { get; set; } - } - internal class RestCommand : ICommand - { - public Color Color => Color.PaleVioletRed; - public string Label => "Rest"; - public string Arguments => Rest.ToString(); - - public byte Rest { get; set; } - } - internal class TrackTempoCommand : ICommand - { - public Color Color => Color.DeepSkyBlue; - public string Label => "Track Tempo"; - public string Arguments => Tempo.ToString(); - - public byte Tempo { get; set; } - } - internal class VoiceCommand : ICommand - { - public Color Color => Color.DarkSalmon; - public string Label => "Voice"; - public string Arguments => Voice.ToString(); - - public byte Voice { get; set; } - } - internal class VolumeCommand : ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Volume"; - public string Arguments => Volume.ToString(); - - public byte Volume { get; set; } - } -} diff --git a/VG Music Studio/Core/GBA/AlphaDream/Config.cs b/VG Music Studio/Core/GBA/AlphaDream/Config.cs deleted file mode 100644 index c1689303..00000000 --- a/VG Music Studio/Core/GBA/AlphaDream/Config.cs +++ /dev/null @@ -1,220 +0,0 @@ -using Kermalis.EndianBinaryIO; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using YamlDotNet.RepresentationModel; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal class Config : Core.Config - { - public readonly byte[] ROM; - public readonly EndianBinaryReader Reader; - public readonly string GameCode; - public readonly byte Version; - - public readonly string Name; - public readonly AudioEngineVersion AudioEngineVersion; - public readonly int[] SongTableOffsets; - public readonly long[] SongTableSizes; - public readonly int VoiceTableOffset; - public readonly int SampleTableOffset; - public readonly long SampleTableSize; - - public Config(byte[] rom) - { - const string configFile = "AlphaDream.yaml"; - using (StreamReader fileStream = File.OpenText(Util.Utils.CombineWithBaseDirectory(configFile))) - { - string gcv = string.Empty; - try - { - ROM = rom; - Reader = new EndianBinaryReader(new MemoryStream(rom)); - GameCode = Reader.ReadString(4, false, 0xAC); - Version = Reader.ReadByte(0xBC); - gcv = $"{GameCode}_{Version:X2}"; - var yaml = new YamlStream(); - yaml.Load(fileStream); - - var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; - YamlMappingNode game; - try - { - game = (YamlMappingNode)mapping.Children.GetValue(gcv); - } - catch (BetterKeyNotFoundException) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KMissingGameCode, gcv))); - } - - YamlNode nameNode = null, - audioEngineVersionNode = null, - songTableOffsetsNode = null, - voiceTableOffsetNode = null, - sampleTableOffsetNode = null, - songTableSizesNode = null, - sampleTableSizeNode = null; - void Load(YamlMappingNode gameToLoad) - { - if (gameToLoad.Children.TryGetValue("Copy", out YamlNode node)) - { - YamlMappingNode copyGame; - try - { - copyGame = (YamlMappingNode)mapping.Children.GetValue(node); - } - catch (BetterKeyNotFoundException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KCopyInvalidGameCode, ex.Key))); - } - Load(copyGame); - } - if (gameToLoad.Children.TryGetValue(nameof(Name), out node)) - { - nameNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(AudioEngineVersion), out node)) - { - audioEngineVersionNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SongTableOffsets), out node)) - { - songTableOffsetsNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SongTableSizes), out node)) - { - songTableSizesNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(VoiceTableOffset), out node)) - { - voiceTableOffsetNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SampleTableOffset), out node)) - { - sampleTableOffsetNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SampleTableSize), out node)) - { - sampleTableSizeNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(Playlists), out node)) - { - var playlists = (YamlMappingNode)node; - foreach (KeyValuePair kvp in playlists) - { - string name = kvp.Key.ToString(); - var songs = new List(); - foreach (KeyValuePair song in (YamlMappingNode)kvp.Value) - { - long songIndex = Util.Utils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Playlists)), song.Key.ToString(), 0, long.MaxValue); - if (songs.Any(s => s.Index == songIndex)) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongRepeated, name, songIndex))); - } - songs.Add(new Song(songIndex, song.Value.ToString())); - } - Playlists.Add(new Playlist(name, songs)); - } - } - } - - Load(game); - - if (nameNode == null) - { - throw new BetterKeyNotFoundException(nameof(Name), null); - } - Name = nameNode.ToString(); - if (audioEngineVersionNode == null) - { - throw new BetterKeyNotFoundException(nameof(AudioEngineVersion), null); - } - AudioEngineVersion = Util.Utils.ParseEnum(nameof(AudioEngineVersion), audioEngineVersionNode.ToString()); - if (songTableOffsetsNode == null) - { - throw new BetterKeyNotFoundException(nameof(SongTableOffsets), null); - } - string[] songTables = songTableOffsetsNode.ToString().SplitSpace(StringSplitOptions.RemoveEmptyEntries); - int numSongTables = songTables.Length; - if (numSongTables == 0) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyNoEntries, nameof(SongTableOffsets)))); - } - if (songTableSizesNode == null) - { - throw new BetterKeyNotFoundException(nameof(SongTableSizes), null); - } - string[] songTableSizes = songTableSizesNode.ToString().SplitSpace(StringSplitOptions.RemoveEmptyEntries); - if (songTableSizes.Length != numSongTables) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongTableCounts, nameof(SongTableSizes), nameof(SongTableOffsets)))); - } - SongTableOffsets = new int[numSongTables]; - SongTableSizes = new long[numSongTables]; - int maxOffset = rom.Length - 1; - for (int i = 0; i < numSongTables; i++) - { - SongTableOffsets[i] = (int)Util.Utils.ParseValue(nameof(SongTableOffsets), songTables[i], 0, maxOffset); - SongTableSizes[i] = Util.Utils.ParseValue(nameof(SongTableSizes), songTableSizes[i], 1, maxOffset); - } - if (voiceTableOffsetNode == null) - { - throw new BetterKeyNotFoundException(nameof(VoiceTableOffset), null); - } - VoiceTableOffset = (int)Util.Utils.ParseValue(nameof(VoiceTableOffset), voiceTableOffsetNode.ToString(), 0, maxOffset); - if (sampleTableOffsetNode == null) - { - throw new BetterKeyNotFoundException(nameof(SampleTableOffset), null); - } - SampleTableOffset = (int)Util.Utils.ParseValue(nameof(SampleTableOffset), sampleTableOffsetNode.ToString(), 0, maxOffset); - if (sampleTableSizeNode == null) - { - throw new BetterKeyNotFoundException(nameof(SampleTableSize), null); - } - SampleTableSize = Util.Utils.ParseValue(nameof(SampleTableSize), sampleTableSizeNode.ToString(), 0, maxOffset); - - // The complete playlist - if (!Playlists.Any(p => p.Name == "Music")) - { - Playlists.Insert(0, new Playlist(Strings.PlaylistMusic, Playlists.SelectMany(p => p.Songs).Distinct().OrderBy(s => s.Index))); - } - } - catch (BetterKeyNotFoundException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyMissing, ex.Key))); - } - catch (InvalidValueException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + ex.Message)); - } - catch (YamlDotNet.Core.YamlException ex) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + ex.Message)); - } - } - } - - public override string GetGameName() - { - return Name; - } - public override string GetSongName(long index) - { - Song s = GetFirstSong(index); - if (s != null) - { - return s.Name; - } - return index.ToString(); - } - - public override void Dispose() - { - Reader.Dispose(); - } - } -} diff --git a/VG Music Studio/Core/GBA/AlphaDream/Enums.cs b/VG Music Studio/Core/GBA/AlphaDream/Enums.cs deleted file mode 100644 index 7c1a9e14..00000000 --- a/VG Music Studio/Core/GBA/AlphaDream/Enums.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal enum AudioEngineVersion : byte - { - Hamtaro, - MLSS - } - - internal enum EnvelopeState : byte - { - Attack, - Decay, - Sustain, - Release - } -} diff --git a/VG Music Studio/Core/GBA/AlphaDream/Mixer.cs b/VG Music Studio/Core/GBA/AlphaDream/Mixer.cs deleted file mode 100644 index de980045..00000000 --- a/VG Music Studio/Core/GBA/AlphaDream/Mixer.cs +++ /dev/null @@ -1,137 +0,0 @@ -using NAudio.Wave; -using System; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal class Mixer : Core.Mixer - { - public readonly float SampleRateReciprocal; - private readonly float _samplesReciprocal; - public readonly int SamplesPerBuffer; - private bool _isFading; - private long _fadeMicroFramesLeft; - private float _fadePos; - private float _fadeStepPerMicroframe; - - public readonly Config Config; - private readonly WaveBuffer _audio; - private readonly float[][] _trackBuffers = new float[Player.NumTracks][]; - private readonly BufferedWaveProvider _buffer; - - public Mixer(Config config) - { - Config = config; - const int sampleRate = 13379; // TODO: Actual value unknown - SamplesPerBuffer = 224; // TODO - SampleRateReciprocal = 1f / sampleRate; - _samplesReciprocal = 1f / SamplesPerBuffer; - - int amt = SamplesPerBuffer * 2; - _audio = new WaveBuffer(amt * sizeof(float)) { FloatBufferCount = amt }; - for (int i = 0; i < Player.NumTracks; i++) - { - _trackBuffers[i] = new float[amt]; - } - _buffer = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, 2)) // TODO - { - DiscardOnBufferOverflow = true, - BufferLength = SamplesPerBuffer * 64 - }; - Init(_buffer); - } - public override void Dispose() - { - base.Dispose(); - CloseWaveWriter(); - } - - public void BeginFadeIn() - { - _fadePos = 0f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * Utils.AGB_FPS); - _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; - _isFading = true; - } - public void BeginFadeOut() - { - _fadePos = 1f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * Utils.AGB_FPS); - _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; - _isFading = true; - } - public bool IsFading() - { - return _isFading; - } - public bool IsFadeDone() - { - return _isFading && _fadeMicroFramesLeft == 0; - } - public void ResetFade() - { - _isFading = false; - _fadeMicroFramesLeft = 0; - } - - private WaveFileWriter _waveWriter; - public void CreateWaveWriter(string fileName) - { - _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); - } - public void CloseWaveWriter() - { - _waveWriter?.Dispose(); - } - public void Process(Track[] tracks, bool output, bool recording) - { - _audio.Clear(); - float masterStep; - float masterLevel; - if (_isFading && _fadeMicroFramesLeft == 0) - { - masterStep = 0; - masterLevel = 0; - } - else - { - float fromMaster = 1f; - float toMaster = 1f; - if (_fadeMicroFramesLeft > 0) - { - const float scale = 10f / 6f; - fromMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadePos += _fadeStepPerMicroframe; - toMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadeMicroFramesLeft--; - } - masterStep = (toMaster - fromMaster) * _samplesReciprocal; - masterLevel = fromMaster; - } - for (int i = 0; i < Player.NumTracks; i++) - { - Track track = tracks[i]; - if (track.Enabled && track.NoteDuration != 0 && !track.Channel.Stopped && !Mutes[i]) - { - float level = masterLevel; - float[] buf = _trackBuffers[i]; - Array.Clear(buf, 0, buf.Length); - track.Channel.Process(buf); - for (int j = 0; j < SamplesPerBuffer; j++) - { - _audio.FloatBuffer[j * 2] += buf[j * 2] * level; - _audio.FloatBuffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; - level += masterStep; - } - } - } - if (output) - { - _buffer.AddSamples(_audio.ByteBuffer, 0, _audio.ByteBufferCount); - } - if (recording) - { - _waveWriter.Write(_audio.ByteBuffer, 0, _audio.ByteBufferCount); - } - } - } -} diff --git a/VG Music Studio/Core/GBA/AlphaDream/Player.cs b/VG Music Studio/Core/GBA/AlphaDream/Player.cs deleted file mode 100644 index d075b8cc..00000000 --- a/VG Music Studio/Core/GBA/AlphaDream/Player.cs +++ /dev/null @@ -1,696 +0,0 @@ -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal class Player : IPlayer - { - public const int NumTracks = 12; // 8 PCM, 4 PSG - private readonly Track[] _tracks = new Track[NumTracks]; - private readonly Mixer _mixer; - private readonly Config _config; - private readonly TimeBarrier _time; - private Thread _thread; - private byte _tempo; - private int _tempoStack; - private long _elapsedLoops; - - public List[] Events { get; private set; } - public long MaxTicks { get; private set; } - public long ElapsedTicks { get; private set; } - public bool ShouldFadeOut { get; set; } - public long NumLoops { get; set; } - private int _longestTrack; - - public PlayerState State { get; private set; } - public event SongEndedEvent SongEnded; - - public Player(Mixer mixer, Config config) - { - for (byte i = 0; i < NumTracks; i++) - { - _tracks[i] = new Track(i, mixer); - } - _mixer = mixer; - _config = config; - - _time = new TimeBarrier(Utils.AGB_FPS); - } - private void CreateThread() - { - _thread = new Thread(Tick) { Name = "AlphaDream Player Tick" }; - _thread.Start(); - } - private void WaitThread() - { - if (_thread != null && (_thread.ThreadState == ThreadState.Running || _thread.ThreadState == ThreadState.WaitSleepJoin)) - { - _thread.Join(); - } - } - - private void InitEmulation() - { - _tempo = 120; // Player tempo is set to 75 on init, but I did not separate player and track tempo yet - _tempoStack = 0; - _elapsedLoops = 0; - ElapsedTicks = 0; - _mixer.ResetFade(); - for (int i = 0; i < NumTracks; i++) - { - _tracks[i].Init(); - } - } - private void SetTicks() - { - MaxTicks = 0; - bool u = false; - for (int trackIndex = 0; trackIndex < NumTracks; trackIndex++) - { - if (Events[trackIndex] == null) - { - continue; - } - - Events[trackIndex] = Events[trackIndex].OrderBy(e => e.Offset).ToList(); - List evs = Events[trackIndex]; - Track track = _tracks[trackIndex]; - track.Init(); - ElapsedTicks = 0; - while (true) - { - SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); - if (e.Ticks.Count > 0) - { - break; - } - - e.Ticks.Add(ElapsedTicks); - ExecuteNext(track, ref u); - if (track.Stopped) - { - break; - } - - ElapsedTicks += track.Rest; - track.Rest = 0; - } - if (ElapsedTicks > MaxTicks) - { - _longestTrack = trackIndex; - MaxTicks = ElapsedTicks; - } - track.NoteDuration = 0; - } - } - public void LoadSong(long index) - { - int songOffset = _config.Reader.ReadInt32(_config.SongTableOffsets[0] + (index * 4)); - if (songOffset == 0) - { - Events = null; - } - else - { - Events = new List[NumTracks]; - songOffset -= Utils.CartridgeOffset; - ushort trackBits = _config.Reader.ReadUInt16(songOffset); - for (int i = 0, usedTracks = 0; i < NumTracks; i++) - { - Track track = _tracks[i]; - if ((trackBits & (1 << i)) == 0) - { - track.Enabled = false; - track.StartOffset = 0; - } - else - { - track.Enabled = true; - Events[i] = new List(); - bool EventExists(long offset) - { - return Events[i].Any(e => e.Offset == offset); - } - - AddEvents(track.StartOffset = songOffset + _config.Reader.ReadInt16(songOffset + 2 + (2 * usedTracks++))); - void AddEvents(int startOffset) - { - _config.Reader.BaseStream.Position = startOffset; - bool cont = true; - while (cont) - { - long offset = _config.Reader.BaseStream.Position; - void AddEvent(ICommand command) - { - Events[i].Add(new SongEvent(offset, command)); - } - byte cmd = _config.Reader.ReadByte(); - switch (cmd) - { - case 0x00: - { - byte keyArg = _config.Reader.ReadByte(); - switch (_config.AudioEngineVersion) - { - case AudioEngineVersion.Hamtaro: - { - byte volume = _config.Reader.ReadByte(); - byte duration = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new FreeNoteHamtaroCommand { Key = (byte)(keyArg - 0x80), Volume = volume, Duration = duration }); - } - break; - } - case AudioEngineVersion.MLSS: - { - byte duration = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new FreeNoteMLSSCommand { Key = (byte)(keyArg - 0x80), Duration = duration }); - } - break; - } - } - break; - } - case 0xF0: - { - byte voice = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new VoiceCommand { Voice = voice }); - } - break; - } - case 0xF1: - { - byte volume = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new VolumeCommand { Volume = volume }); - } - break; - } - case 0xF2: - { - byte panArg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new PanpotCommand { Panpot = (sbyte)(panArg - 0x80) }); - } - break; - } - case 0xF4: - { - byte range = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new PitchBendRangeCommand { Range = range }); - } - break; - } - case 0xF5: - { - sbyte bend = _config.Reader.ReadSByte(); - if (!EventExists(offset)) - { - AddEvent(new PitchBendCommand { Bend = bend }); - } - break; - } - case 0xF6: - { - byte rest = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = rest }); - } - break; - } - case 0xF8: - { - short jumpOffset = _config.Reader.ReadInt16(); - if (!EventExists(offset)) - { - int off = (int)(_config.Reader.BaseStream.Position + jumpOffset); - AddEvent(new JumpCommand { Offset = off }); - if (!EventExists(off)) - { - AddEvents(off); - } - } - cont = false; - break; - } - case 0xF9: - { - byte tempoArg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new TrackTempoCommand { Tempo = tempoArg }); - } - break; - } - case 0xFF: - { - if (!EventExists(offset)) - { - AddEvent(new FinishCommand()); - } - cont = false; - break; - } - default: - { - if (cmd <= 0xDF) - { - byte key = _config.Reader.ReadByte(); - switch (_config.AudioEngineVersion) - { - case AudioEngineVersion.Hamtaro: - { - byte volume = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new NoteHamtaroCommand { Key = key, Volume = volume, Duration = cmd }); - } - break; - } - case AudioEngineVersion.MLSS: - { - if (!EventExists(offset)) - { - AddEvent(new NoteMLSSCommand { Key = key, Duration = cmd }); - } - break; - } - } - } - else - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, i, offset, cmd)); - } - break; - } - } - } - } - } - } - SetTicks(); - } - } - public void SetCurrentPosition(long ticks) - { - if (Events == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - if (State == PlayerState.Playing) - { - Pause(); - } - InitEmulation(); - bool u = false; - while (true) - { - if (ElapsedTicks == ticks) - { - goto finish; - } - - while (_tempoStack >= 75) - { - _tempoStack -= 75; - for (int trackIndex = 0; trackIndex < NumTracks; trackIndex++) - { - Track track = _tracks[trackIndex]; - if (track.Enabled && !track.Stopped) - { - track.Tick(); - while (track.Rest == 0 && !track.Stopped) - { - ExecuteNext(track, ref u); - } - } - } - ElapsedTicks++; - if (ElapsedTicks == ticks) - { - goto finish; - } - } - _tempoStack += _tempo; - } - finish: - for (int i = 0; i < NumTracks; i++) - { - _tracks[i].NoteDuration = 0; - } - Pause(); - } - } - public void Play() - { - if (Events == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - Stop(); - InitEmulation(); - State = PlayerState.Playing; - CreateThread(); - } - } - public void Pause() - { - if (State == PlayerState.Playing) - { - State = PlayerState.Paused; - WaitThread(); - } - else if (State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.Playing; - CreateThread(); - } - } - public void Stop() - { - if (State == PlayerState.Playing || State == PlayerState.Paused) - { - State = PlayerState.Stopped; - WaitThread(); - } - } - public void Record(string fileName) - { - _mixer.CreateWaveWriter(fileName); - InitEmulation(); - State = PlayerState.Recording; - CreateThread(); - WaitThread(); - _mixer.CloseWaveWriter(); - } - public void Dispose() - { - if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.ShutDown; - WaitThread(); - } - } - public void GetSongState(UI.SongInfoControl.SongInfo info) - { - info.Tempo = _tempo; - for (int i = 0; i < NumTracks; i++) - { - Track track = _tracks[i]; - if (track.Enabled) - { - UI.SongInfoControl.SongInfo.Track tin = info.Tracks[i]; - tin.Position = track.DataOffset; - tin.Rest = track.Rest; - tin.Voice = track.Voice; - tin.Type = track.Type; - tin.Volume = track.Volume; - tin.PitchBend = track.GetPitch(); - tin.Panpot = track.Panpot; - if (track.NoteDuration != 0 && !track.Channel.Stopped) - { - tin.Keys[0] = track.Channel.Key; - ChannelVolume vol = track.Channel.GetVolume(); - tin.LeftVolume = vol.LeftVol; - tin.RightVolume = vol.RightVol; - } - else - { - tin.Keys[0] = byte.MaxValue; - tin.LeftVolume = 0f; - tin.RightVolume = 0f; - } - } - } - } - - private VoiceEntry GetVoiceEntry(byte voice, byte key) - { - int vto = _config.VoiceTableOffset; - short voiceOffset = _config.Reader.ReadInt16(vto + (voice * 2)); - short nextVoiceOffset = _config.Reader.ReadInt16(vto + ((voice + 1) * 2)); - if (voiceOffset == nextVoiceOffset) - { - return null; - } - else - { - long pos = vto + voiceOffset; // Prevent object creation in the last iteration - VoiceEntry e = _config.Reader.ReadObject(pos); - while (e.MinKey > key || e.MaxKey < key) - { - pos += 8; - if (pos == nextVoiceOffset) - { - return null; - } - e = _config.Reader.ReadObject(); - } - return e; - } - } - private void PlayNote(Track track, byte key, byte duration) - { - VoiceEntry entry = GetVoiceEntry(track.Voice, key); - if (entry != null) - { - track.NoteDuration = duration; - if (track.Index >= 8) - { - // TODO: "Sample" byte in VoiceEntry - ((SquareChannel)track.Channel).Init(key, new ADSR { A = 0xFF, D = 0x00, S = 0xFF, R = 0x00 }, track.Volume, track.Panpot, track.GetPitch()); - } - else - { - int sto = _config.SampleTableOffset; - int sampleOffset = _config.Reader.ReadInt32(sto + (entry.Sample * 4)); // Some entries are 0. If you play them, are they silent, or does it not care if they are 0? - ((PCMChannel)track.Channel).Init(key, new ADSR { A = 0xFF, D = 0x00, S = 0xFF, R = 0x00 }, sto + sampleOffset, entry.IsFixedFrequency == 0x80); - track.Channel.SetVolume(track.Volume, track.Panpot); - track.Channel.SetPitch(track.GetPitch()); - } - } - } - private void ExecuteNext(Track track, ref bool update) - { - byte cmd = _config.ROM[track.DataOffset++]; - switch (cmd) - { - case 0x00: // Free Note - { - byte key = (byte)(_config.ROM[track.DataOffset++] - 0x80); - if (_config.AudioEngineVersion == AudioEngineVersion.Hamtaro) - { - track.Volume = _config.ROM[track.DataOffset++]; - update = true; - } - byte duration = _config.ROM[track.DataOffset++]; - track.Rest += duration; - if (track.PrevCommand == 0 && track.Channel.Key == key) - { - track.NoteDuration += duration; - } - else - { - PlayNote(track, key, duration); - } - break; - } - case 0xF0: // Voice - { - track.Voice = _config.ROM[track.DataOffset++]; - break; - } - case 0xF1: // Volume - { - track.Volume = _config.ROM[track.DataOffset++]; - update = true; - break; - } - case 0xF2: // Panpot - { - track.Panpot = (sbyte)(_config.ROM[track.DataOffset++] - 0x80); - update = true; - break; - } - case 0xF4: // Pitch Bend Range - { - track.PitchBendRange = _config.ROM[track.DataOffset++]; - update = true; - break; - } - case 0xF5: // Pitch Bend - { - track.PitchBend = (sbyte)_config.ROM[track.DataOffset++]; - update = true; - break; - } - case 0xF6: // Rest - { - track.Rest = _config.ROM[track.DataOffset++]; - break; - } - case 0xF8: // Jump - { - short ofs = (short)(_config.ROM[track.DataOffset++] | (_config.ROM[track.DataOffset++] << 8)); // Cast to short is necessary - track.DataOffset += ofs; - break; - } - case 0xF9: // Track Tempo - { - _tempo = _config.ROM[track.DataOffset++]; - break; - } - case 0xFF: // Finish - { - track.Stopped = true; - break; - } - default: - { - if (cmd <= 0xDF) // Note - { - byte key = _config.ROM[track.DataOffset++]; - if (_config.AudioEngineVersion == AudioEngineVersion.Hamtaro) - { - track.Volume = _config.ROM[track.DataOffset++]; - update = true; - } - track.Rest += cmd; - if (track.PrevCommand == 0 && track.Channel.Key == key) - { - track.NoteDuration += cmd; - } - else - { - PlayNote(track, key, cmd); - } - } - break; - } - } - - track.PrevCommand = cmd; - } - - private void Tick() - { - _time.Start(); - while (true) - { - PlayerState state = State; - bool playing = state == PlayerState.Playing; - bool recording = state == PlayerState.Recording; - if (!playing && !recording) - { - goto stop; - } - - void MixerProcess() - { - _mixer.Process(_tracks, playing, recording); - } - - while (_tempoStack >= 75) - { - _tempoStack -= 75; - bool allDone = true; - for (int trackIndex = 0; trackIndex < NumTracks; trackIndex++) - { - Track track = _tracks[trackIndex]; - if (track.Enabled) - { - byte prevDuration = track.NoteDuration; - track.Tick(); - bool update = false; - while (track.Rest == 0 && !track.Stopped) - { - ExecuteNext(track, ref update); - } - if (trackIndex == _longestTrack) - { - if (ElapsedTicks == MaxTicks) - { - if (!track.Stopped) - { - List evs = Events[trackIndex]; - for (int i = 0; i < evs.Count; i++) - { - SongEvent ev = evs[i]; - if (ev.Offset == track.DataOffset) - { - ElapsedTicks = ev.Ticks[0] - track.Rest; - break; - } - } - _elapsedLoops++; - if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) - { - _mixer.BeginFadeOut(); - } - } - } - else - { - ElapsedTicks++; - } - } - if (prevDuration == 1 && track.NoteDuration == 0) // Note was not renewed - { - track.Channel.State = EnvelopeState.Release; - } - if (!track.Stopped) - { - allDone = false; - } - if (track.NoteDuration != 0) - { - allDone = false; - if (update) - { - track.Channel.SetVolume(track.Volume, track.Panpot); - track.Channel.SetPitch(track.GetPitch()); - } - } - } - } - if (_mixer.IsFadeDone()) - { - allDone = true; - } - if (allDone) - { - MixerProcess(); - State = PlayerState.Stopped; - SongEnded?.Invoke(); - } - } - _tempoStack += _tempo; - MixerProcess(); - if (playing) - { - _time.Wait(); - } - } - stop: - _time.Stop(); - } - } -} diff --git a/VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs b/VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs deleted file mode 100644 index 222ee3fb..00000000 --- a/VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs +++ /dev/null @@ -1,181 +0,0 @@ -using Kermalis.DLS2; -using System; -using System.Collections.Generic; -using System.Diagnostics; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal sealed class SoundFontSaver_DLS - { - // Since every key will use the same articulation data, just store one instance - private static readonly Level2ArticulatorChunk _art2 = new Level2ArticulatorChunk - { - new Level2ArticulatorConnectionBlock { Destination = Level2ArticulatorDestination.LFOFrequency, Scale = 2786 }, - new Level2ArticulatorConnectionBlock { Destination = Level2ArticulatorDestination.VIBFrequency, Scale = 2786 }, - new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.KeyNumber, Destination = Level2ArticulatorDestination.Pitch }, - new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.Vibrato, Control = Level2ArticulatorSource.Modulation_CC1, Destination = Level2ArticulatorDestination.Pitch, BipolarSource = true, Scale = 0x320000 }, - new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.Vibrato, Control = Level2ArticulatorSource.ChannelPressure, Destination = Level2ArticulatorDestination.Pitch, BipolarSource = true, Scale = 0x320000 }, - new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.Pan_CC10, Destination = Level2ArticulatorDestination.Pan, BipolarSource = true, Scale = 0xFE0000 }, - new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.ChorusSend_CC91, Destination = Level2ArticulatorDestination.Reverb, Scale = 0xC80000 }, - new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.Reverb_SendCC93, Destination = Level2ArticulatorDestination.Chorus, Scale = 0xC80000 } - }; - - public static void Save(Config config, string path) - { - var dls = new DLS(); - AddInfo(config, dls); - Dictionary sampleDict = AddSamples(config, dls); - AddInstruments(config, dls, sampleDict); - dls.Save(path); - } - - private static void AddInfo(Config config, DLS dls) - { - var info = new ListChunk("INFO"); - dls.Add(info); - info.Add(new InfoSubChunk("INAM", config.Name)); - //info.Add(new InfoSubChunk("ICOP", config.Creator)); - info.Add(new InfoSubChunk("IENG", "Kermalis")); - info.Add(new InfoSubChunk("ISFT", Util.Utils.ProgramName)); - } - - private static Dictionary AddSamples(Config config, DLS dls) - { - ListChunk waves = dls.WavePool; - var sampleDict = new Dictionary((int)config.SampleTableSize); - for (int i = 0; i < config.SampleTableSize; i++) - { - int ofs = config.Reader.ReadInt32(config.SampleTableOffset + (i * 4)); - if (ofs == 0) - { - continue; // Skip null samples - } - - ofs += config.SampleTableOffset; - SampleHeader sh = config.Reader.ReadObject(ofs); - - // Create format chunk - var fmt = new FormatChunk(WaveFormat.PCM); - fmt.WaveInfo.Channels = 1; - fmt.WaveInfo.SamplesPerSec = (uint)(sh.SampleRate >> 10); - fmt.WaveInfo.AvgBytesPerSec = fmt.WaveInfo.SamplesPerSec; - fmt.WaveInfo.BlockAlign = 1; - fmt.FormatInfo.BitsPerSample = 8; - // Create wave sample chunk and add loop if there is one - var wsmp = new WaveSampleChunk - { - UnityNote = 60, - Options = WaveSampleOptions.NoTruncation | WaveSampleOptions.NoCompression - }; - if (sh.DoesLoop == 0x40000000) - { - wsmp.Loop = new WaveSampleLoop - { - LoopStart = (uint)sh.LoopOffset, - LoopLength = (uint)(sh.Length - sh.LoopOffset), - LoopType = LoopType.Forward - }; - } - // Get PCM sample - byte[] pcm = new byte[sh.Length]; - Array.Copy(config.ROM, ofs + 0x10, pcm, 0, sh.Length); - - // Add - int dlsIndex = waves.Count; - waves.Add(new ListChunk("wave") - { - fmt, - wsmp, - new DataChunk(pcm), - new ListChunk("INFO") - { - new InfoSubChunk("INAM", $"Sample {i}") - } - }); - sampleDict.Add(i, (wsmp, dlsIndex)); - } - return sampleDict; - } - - private static void AddInstruments(Config config, DLS dls, Dictionary sampleDict) - { - ListChunk lins = dls.InstrumentList; - for (int v = 0; v < 256; v++) - { - short off = config.Reader.ReadInt16(config.VoiceTableOffset + (v * 2)); - short nextOff = config.Reader.ReadInt16(config.VoiceTableOffset + ((v + 1) * 2)); - int numEntries = (nextOff - off) / 8; // Each entry is 8 bytes - if (numEntries == 0) - { - continue; // Skip empty entries - } - - var ins = new ListChunk("ins "); - ins.Add(new InstrumentHeaderChunk - { - NumRegions = (uint)numEntries, - Locale = new MIDILocale(0, (byte)(v / 128), false, (byte)(v % 128)) - }); - var lrgn = new ListChunk("lrgn"); - ins.Add(lrgn); - ins.Add(new ListChunk("INFO") - { - new InfoSubChunk("INAM", $"Instrument {v}") - }); - lins.Add(ins); - for (int e = 0; e < numEntries; e++) - { - VoiceEntry entry = config.Reader.ReadObject(config.VoiceTableOffset + off + (e * 8)); - // Sample - if (entry.Sample >= config.SampleTableSize) - { - Debug.WriteLine(string.Format("Voice {0} uses an invalid sample id ({1})", v, entry.Sample)); - continue; - } - if (!sampleDict.TryGetValue(entry.Sample, out (WaveSampleChunk, int) value)) - { - Debug.WriteLine(string.Format("Voice {0} uses a null sample id ({1})", v, entry.Sample)); - continue; - } - void Add(ushort low, ushort high, ushort baseKey) - { - var rgnh = new RegionHeaderChunk(); - rgnh.KeyRange.Low = low; - rgnh.KeyRange.High = high; - lrgn.Add(new ListChunk("rgn2") - { - rgnh, - new WaveSampleChunk - { - UnityNote = baseKey, - Options = WaveSampleOptions.NoTruncation | WaveSampleOptions.NoCompression, - Loop = value.Item1.Loop - }, - new WaveLinkChunk - { - Channels = WaveLinkChannels.Left, - TableIndex = (uint)value.Item2 - }, - new ListChunk("lar2") - { - _art2 - } - }); - } - // Fixed frequency - Since DLS does not support it, we need to manually add every key with its own base note - if (entry.IsFixedFrequency == 0x80) - { - for (ushort i = entry.MinKey; i <= entry.MaxKey; i++) - { - Add(i, i, i); - } - } - else - { - Add(entry.MinKey, entry.MaxKey, 60); - } - } - } - } - } -} diff --git a/VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs b/VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs deleted file mode 100644 index 8fda0b97..00000000 --- a/VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Kermalis.SoundFont2; -using Kermalis.VGMusicStudio.Util; -using System.Collections.Generic; -using System.Diagnostics; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal sealed class SoundFontSaver_SF2 - { - public static void Save(Config config, string path) - { - var sf2 = new SF2(); - AddInfo(config, sf2.InfoChunk); - Dictionary sampleDict = AddSamples(config, sf2); - AddInstruments(config, sf2, sampleDict); - sf2.Save(path); - } - - private static void AddInfo(Config config, InfoListChunk chunk) - { - chunk.Bank = config.Name; - //chunk.Copyright = config.Creator; - chunk.Tools = Util.Utils.ProgramName + " by Kermalis"; - } - - private static Dictionary AddSamples(Config config, SF2 sf2) - { - var sampleDict = new Dictionary((int)config.SampleTableSize); - for (int i = 0; i < config.SampleTableSize; i++) - { - int ofs = config.Reader.ReadInt32(config.SampleTableOffset + (i * 4)); - if (ofs == 0) - { - continue; - } - - ofs += config.SampleTableOffset; - SampleHeader sh = config.Reader.ReadObject(ofs); - - short[] pcm16 = SampleUtils.PCMU8ToPCM16(config.ROM, ofs + 0x10, sh.Length); - int sf2Index = (int)sf2.AddSample(pcm16, $"Sample {i}", sh.DoesLoop == 0x40000000, (uint)sh.LoopOffset, (uint)(sh.SampleRate >> 10), 60, 0); - sampleDict.Add(i, (sh, sf2Index)); - } - return sampleDict; - } - private static void AddInstruments(Config config, SF2 sf2, Dictionary sampleDict) - { - for (int v = 0; v < 256; v++) - { - short off = config.Reader.ReadInt16(config.VoiceTableOffset + (v * 2)); - short nextOff = config.Reader.ReadInt16(config.VoiceTableOffset + ((v + 1) * 2)); - int numEntries = (nextOff - off) / 8; // Each entry is 8 bytes - if (numEntries == 0) - { - continue; - } - - string name = "Instrument " + v; - sf2.AddPreset(name, (ushort)v, 0); - sf2.AddPresetBag(); - sf2.AddPresetGenerator(SF2Generator.Instrument, new SF2GeneratorAmount { Amount = (short)sf2.AddInstrument(name) }); - for (int e = 0; e < numEntries; e++) - { - VoiceEntry entry = config.Reader.ReadObject(config.VoiceTableOffset + off + (e * 8)); - sf2.AddInstrumentBag(); - // Key range - if (!(entry.MinKey == 0 && entry.MaxKey == 0x7F)) - { - sf2.AddInstrumentGenerator(SF2Generator.KeyRange, new SF2GeneratorAmount { LowByte = entry.MinKey, HighByte = entry.MaxKey }); - } - // Fixed frequency - if (entry.IsFixedFrequency == 0x80) - { - sf2.AddInstrumentGenerator(SF2Generator.ScaleTuning, new SF2GeneratorAmount { Amount = 0 }); - } - // Sample - if (entry.Sample < config.SampleTableSize) - { - if (!sampleDict.TryGetValue(entry.Sample, out (SampleHeader, int) value)) - { - Debug.WriteLine(string.Format("Voice {0} uses a null sample id ({1})", v, entry.Sample)); - } - else - { - sf2.AddInstrumentGenerator(SF2Generator.SampleModes, new SF2GeneratorAmount { Amount = (short)(value.Item1.DoesLoop == 0x40000000 ? 1 : 0) }); - sf2.AddInstrumentGenerator(SF2Generator.SampleID, new SF2GeneratorAmount { UAmount = (ushort)value.Item2 }); - } - } - else - { - Debug.WriteLine(string.Format("Voice {0} uses an invalid sample id ({1})", v, entry.Sample)); - } - } - } - } - } -} diff --git a/VG Music Studio/Core/GBA/AlphaDream/Structs.cs b/VG Music Studio/Core/GBA/AlphaDream/Structs.cs deleted file mode 100644 index ac3b2bb3..00000000 --- a/VG Music Studio/Core/GBA/AlphaDream/Structs.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal class SampleHeader - { - /// 0x40000000 if True - public int DoesLoop { get; set; } - /// Right shift 10 for value - public int SampleRate { get; set; } - public int LoopOffset { get; set; } - public int Length { get; set; } - } - internal class VoiceEntry - { - public byte MinKey { get; set; } - public byte MaxKey { get; set; } - public byte Sample { get; set; } - /// 0x80 if True - public byte IsFixedFrequency { get; set; } - [BinaryArrayFixedLength(4)] - public byte[] Unknown { get; set; } - } - - internal struct ChannelVolume - { - public float LeftVol, RightVol; - } - internal class ADSR // TODO - { - public byte A, D, S, R; - } -} diff --git a/VG Music Studio/Core/GBA/AlphaDream/Track.cs b/VG Music Studio/Core/GBA/AlphaDream/Track.cs deleted file mode 100644 index 296a2840..00000000 --- a/VG Music Studio/Core/GBA/AlphaDream/Track.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal class Track - { - public readonly byte Index; - public readonly string Type; - public readonly Channel Channel; - - public byte Voice; - public byte PitchBendRange; - public byte Volume; - public byte Rest; - public byte NoteDuration; - public sbyte PitchBend; - public sbyte Panpot; - public bool Enabled; - public bool Stopped; - public int StartOffset; - public int DataOffset; - public byte PrevCommand; - - public int GetPitch() - { - return PitchBend * (PitchBendRange / 2); - } - - public Track(byte i, Mixer mixer) - { - Index = i; - if (i >= 8) - { - Type = Utils.PSGTypes[i & 3]; - Channel = new SquareChannel(mixer); // TODO: PSG Channels 3 and 4 - } - else - { - Type = "PCM8"; - Channel = new PCMChannel(mixer); - } - } - // 0x819B040 - public void Init() - { - Voice = 0; - Rest = 1; // Unsure why Rest starts at 1 - PitchBendRange = 2; - NoteDuration = 0; - PitchBend = 0; - Panpot = 0; // Start centered; ROM sets this to 0x7F since it's unsigned there - DataOffset = StartOffset; - Stopped = false; - Volume = 200; - PrevCommand = 0xFF; - //Tempo = 120; - //TempoStack = 0; - } - public void Tick() - { - if (Rest != 0) - { - Rest--; - } - if (NoteDuration > 0) - { - NoteDuration--; - } - } - } -} diff --git a/VG Music Studio/Core/GBA/MP2K/Channel.cs b/VG Music Studio/Core/GBA/MP2K/Channel.cs deleted file mode 100644 index faa606d1..00000000 --- a/VG Music Studio/Core/GBA/MP2K/Channel.cs +++ /dev/null @@ -1,777 +0,0 @@ -using System; -using System.Collections; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal abstract class Channel - { - public EnvelopeState State = EnvelopeState.Dead; - public Track Owner; - protected readonly Mixer _mixer; - - public Note Note; // Must be a struct & field - protected ADSR _adsr; - protected int _instPan; - - protected byte _velocity; - protected int _pos; - protected float _interPos; - protected float _frequency; - - protected Channel(Mixer mixer) - { - _mixer = mixer; - } - - public abstract ChannelVolume GetVolume(); - public abstract void SetVolume(byte vol, sbyte pan); - public abstract void SetPitch(int pitch); - public virtual void Release() - { - if (State < EnvelopeState.Releasing) - { - State = EnvelopeState.Releasing; - } - } - - public abstract void Process(float[] buffer); - - // Returns whether the note is active or not - public virtual bool TickNote() - { - if (State < EnvelopeState.Releasing) - { - if (Note.Duration > 0) - { - Note.Duration--; - if (Note.Duration == 0) - { - State = EnvelopeState.Releasing; - return false; - } - return true; - } - else - { - return true; - } - } - else - { - return false; - } - } - public void Stop() - { - State = EnvelopeState.Dead; - if (Owner != null) - { - Owner.Channels.Remove(this); - } - Owner = null; - } - } - internal class PCM8Channel : Channel - { - private SampleHeader _sampleHeader; - private int _sampleOffset; - private GoldenSunPSG _gsPSG; - private bool _bFixed; - private bool _bGoldenSun; - private bool _bCompressed; - private byte _leftVol; - private byte _rightVol; - private sbyte[] _decompressedSample; - - public PCM8Channel(Mixer mixer) : base(mixer) { } - public void Init(Track owner, Note note, ADSR adsr, int sampleOffset, byte vol, sbyte pan, int instPan, int pitch, bool bFixed, bool bCompressed) - { - State = EnvelopeState.Initializing; - _pos = 0; _interPos = 0; - if (Owner != null) - { - Owner.Channels.Remove(this); - } - Owner = owner; - Owner.Channels.Add(this); - Note = note; - _adsr = adsr; - _instPan = instPan; - _sampleHeader = _mixer.Config.Reader.ReadObject(sampleOffset); - _sampleOffset = sampleOffset + 0x10; - _bFixed = bFixed; - _bCompressed = bCompressed; - _decompressedSample = bCompressed ? Utils.Decompress(_sampleOffset, _sampleHeader.Length) : null; - _bGoldenSun = _mixer.Config.HasGoldenSunSynths && _sampleHeader.DoesLoop == 0x40000000 && _sampleHeader.LoopOffset == 0 && _sampleHeader.Length == 0; - if (_bGoldenSun) - { - _gsPSG = _mixer.Config.Reader.ReadObject(_sampleOffset); - } - SetVolume(vol, pan); - SetPitch(pitch); - } - - public override ChannelVolume GetVolume() - { - const float max = 0x10000; - return new ChannelVolume - { - LeftVol = _leftVol * _velocity / max * _mixer.PCM8MasterVolume, - RightVol = _rightVol * _velocity / max * _mixer.PCM8MasterVolume - }; - } - public override void SetVolume(byte vol, sbyte pan) - { - int combinedPan = pan + _instPan; - if (combinedPan > 63) - { - combinedPan = 63; - } - else if (combinedPan < -64) - { - combinedPan = -64; - } - const int fix = 0x2000; - if (State < EnvelopeState.Releasing) - { - int a = Note.Velocity * vol; - _leftVol = (byte)(a * (-combinedPan + 0x40) / fix); - _rightVol = (byte)(a * (combinedPan + 0x40) / fix); - } - } - public override void SetPitch(int pitch) - { - _frequency = (_sampleHeader.SampleRate >> 10) * (float)Math.Pow(2, ((Note.Key - 60) / 12f) + (pitch / 768f)); - } - - private void StepEnvelope() - { - switch (State) - { - case EnvelopeState.Initializing: - { - _velocity = _adsr.A; - State = EnvelopeState.Rising; - break; - } - case EnvelopeState.Rising: - { - int nextVel = _velocity + _adsr.A; - if (nextVel >= 0xFF) - { - State = EnvelopeState.Decaying; - _velocity = 0xFF; - } - else - { - _velocity = (byte)nextVel; - } - break; - } - case EnvelopeState.Decaying: - { - int nextVel = (_velocity * _adsr.D) >> 8; - if (nextVel <= _adsr.S) - { - State = EnvelopeState.Playing; - _velocity = _adsr.S; - } - else - { - _velocity = (byte)nextVel; - } - break; - } - case EnvelopeState.Playing: - { - break; - } - case EnvelopeState.Releasing: - { - int nextVel = (_velocity * _adsr.R) >> 8; - if (nextVel <= 0) - { - State = EnvelopeState.Dying; - _velocity = 0; - } - else - { - _velocity = (byte)nextVel; - } - break; - } - case EnvelopeState.Dying: - { - Stop(); - break; - } - } - } - - public override void Process(float[] buffer) - { - StepEnvelope(); - if (State == EnvelopeState.Dead) - { - return; - } - - ChannelVolume vol = GetVolume(); - float interStep = _bFixed && !_bGoldenSun ? _mixer.SampleRate * _mixer.SampleRateReciprocal : _frequency * _mixer.SampleRateReciprocal; - if (_bGoldenSun) // Most Golden Sun processing is thanks to ipatix - { - interStep /= 0x40; - switch (_gsPSG.Type) - { - case GoldenSunPSGType.Square: - { - _pos += _gsPSG.CycleSpeed << 24; - int iThreshold = (_gsPSG.MinimumCycle << 24) + _pos; - iThreshold = (iThreshold < 0 ? ~iThreshold : iThreshold) >> 8; - iThreshold = (iThreshold * _gsPSG.CycleAmplitude) + (_gsPSG.InitialCycle << 24); - float threshold = iThreshold / (float)0x100000000; - - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = _interPos < threshold ? 0.5f : -0.5f; - samp += 0.5f - threshold; - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - if (_interPos >= 1) - { - _interPos--; - } - } while (--samplesPerBuffer > 0); - break; - } - case GoldenSunPSGType.Saw: - { - const int fix = 0x70; - - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - _interPos += interStep; - if (_interPos >= 1) - { - _interPos--; - } - int var1 = (int)(_interPos * 0x100) - fix; - int var2 = (int)(_interPos * 0x10000) << 17; - int var3 = var1 - (var2 >> 27); - _pos = var3 + (_pos >> 1); - - float samp = _pos / (float)0x100; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - } while (--samplesPerBuffer > 0); - break; - } - case GoldenSunPSGType.Triangle: - { - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - _interPos += interStep; - if (_interPos >= 1) - { - _interPos--; - } - float samp = _interPos < 0.5f ? (_interPos * 4) - 1 : 3 - (_interPos * 4); - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - } while (--samplesPerBuffer > 0); - break; - } - } - } - else if (_bCompressed) - { - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = _decompressedSample[_pos] / (float)0x80; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos += posDelta; - if (_pos >= _decompressedSample.Length) - { - Stop(); - break; - } - } while (--samplesPerBuffer > 0); - } - else - { - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = (sbyte)_mixer.Config.ROM[_pos + _sampleOffset] / (float)0x80; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos += posDelta; - if (_pos >= _sampleHeader.Length) - { - if (_sampleHeader.DoesLoop == 0x40000000) - { - _pos = _sampleHeader.LoopOffset; - } - else - { - Stop(); - break; - } - } - } while (--samplesPerBuffer > 0); - } - } - } - internal abstract class PSGChannel : Channel - { - protected enum GBPan : byte - { - Left, - Center, - Right - } - - private byte _processStep; - private EnvelopeState _nextState; - private byte _peakVelocity; - private byte _sustainVelocity; - protected GBPan _panpot = GBPan.Center; - - public PSGChannel(Mixer mixer) : base(mixer) { } - protected void Init(Track owner, Note note, ADSR env, int instPan) - { - State = EnvelopeState.Initializing; - if (Owner != null) - { - Owner.Channels.Remove(this); - } - Owner = owner; - Owner.Channels.Add(this); - Note = note; - _adsr.A = (byte)(env.A & 0x7); - _adsr.D = (byte)(env.D & 0x7); - _adsr.S = (byte)(env.S & 0xF); - _adsr.R = (byte)(env.R & 0x7); - _instPan = instPan; - } - - public override void Release() - { - if (State < EnvelopeState.Releasing) - { - if (_adsr.R == 0) - { - _velocity = 0; - Stop(); - } - else if (_velocity == 0) - { - Stop(); - } - else - { - _nextState = EnvelopeState.Releasing; - } - } - } - public override bool TickNote() - { - if (State < EnvelopeState.Releasing) - { - if (Note.Duration > 0) - { - Note.Duration--; - if (Note.Duration == 0) - { - if (_velocity == 0) - { - Stop(); - } - else - { - State = EnvelopeState.Releasing; - } - return false; - } - return true; - } - else - { - return true; - } - } - else - { - return false; - } - } - - public override ChannelVolume GetVolume() - { - const float max = 0x20; - return new ChannelVolume - { - LeftVol = _panpot == GBPan.Right ? 0 : _velocity / max, - RightVol = _panpot == GBPan.Left ? 0 : _velocity / max - }; - } - public override void SetVolume(byte vol, sbyte pan) - { - int combinedPan = pan + _instPan; - if (combinedPan > 63) - { - combinedPan = 63; - } - else if (combinedPan < -64) - { - combinedPan = -64; - } - if (State < EnvelopeState.Releasing) - { - _panpot = combinedPan < -21 ? GBPan.Left : combinedPan > 20 ? GBPan.Right : GBPan.Center; - _peakVelocity = (byte)((Note.Velocity * vol) >> 10); - _sustainVelocity = (byte)(((_peakVelocity * _adsr.S) + 0xF) >> 4); // TODO - if (State == EnvelopeState.Playing) - { - _velocity = _sustainVelocity; - } - } - } - - protected void StepEnvelope() - { - void dec() - { - _processStep = 0; - if (_velocity - 1 <= _sustainVelocity) - { - _velocity = _sustainVelocity; - _nextState = EnvelopeState.Playing; - } - else if (_velocity != 0) - { - _velocity--; - } - } - void sus() - { - _processStep = 0; - } - void rel() - { - if (_adsr.R == 0) - { - _velocity = 0; - Stop(); - } - else - { - _processStep = 0; - if (_velocity - 1 <= 0) - { - _nextState = EnvelopeState.Dying; - _velocity = 0; - } - else - { - _velocity--; - } - } - } - - switch (State) - { - case EnvelopeState.Initializing: - { - _nextState = EnvelopeState.Rising; - _processStep = 0; - if ((_adsr.A | _adsr.D) == 0 || (_sustainVelocity == 0 && _peakVelocity == 0)) - { - State = EnvelopeState.Playing; - _velocity = _sustainVelocity; - return; - } - else if (_adsr.A == 0 && _adsr.S < 0xF) - { - State = EnvelopeState.Decaying; - int next = _peakVelocity - 1; - if (next < 0) - { - next = 0; - } - _velocity = (byte)next; - if (_velocity < _sustainVelocity) - { - _velocity = _sustainVelocity; - } - return; - } - else if (_adsr.A == 0) - { - State = EnvelopeState.Playing; - _velocity = _sustainVelocity; - return; - } - else - { - State = EnvelopeState.Rising; - _velocity = 1; - return; - } - } - case EnvelopeState.Rising: - { - if (++_processStep >= _adsr.A) - { - if (_nextState == EnvelopeState.Decaying) - { - State = EnvelopeState.Decaying; - dec(); return; - } - if (_nextState == EnvelopeState.Playing) - { - State = EnvelopeState.Playing; - sus(); return; - } - if (_nextState == EnvelopeState.Releasing) - { - State = EnvelopeState.Releasing; - rel(); return; - } - _processStep = 0; - if (++_velocity >= _peakVelocity) - { - if (_adsr.D == 0) - { - _nextState = EnvelopeState.Playing; - } - else if (_peakVelocity == _sustainVelocity) - { - _nextState = EnvelopeState.Playing; - _velocity = _peakVelocity; - } - else - { - _velocity = _peakVelocity; - _nextState = EnvelopeState.Decaying; - } - } - } - break; - } - case EnvelopeState.Decaying: - { - if (++_processStep >= _adsr.D) - { - if (_nextState == EnvelopeState.Playing) - { - State = EnvelopeState.Playing; - sus(); return; - } - if (_nextState == EnvelopeState.Releasing) - { - State = EnvelopeState.Releasing; - rel(); return; - } - dec(); - } - break; - } - case EnvelopeState.Playing: - { - if (++_processStep >= 1) - { - if (_nextState == EnvelopeState.Releasing) - { - State = EnvelopeState.Releasing; - rel(); return; - } - sus(); - } - break; - } - case EnvelopeState.Releasing: - { - if (++_processStep >= _adsr.R) - { - if (_nextState == EnvelopeState.Dying) - { - Stop(); - return; - } - rel(); - } - break; - } - } - } - } - internal class SquareChannel : PSGChannel - { - private float[] _pat; - - public SquareChannel(Mixer mixer) : base(mixer) { } - public void Init(Track owner, Note note, ADSR env, int instPan, SquarePattern pattern) - { - Init(owner, note, env, instPan); - switch (pattern) - { - default: _pat = Utils.SquareD12; break; - case SquarePattern.D25: _pat = Utils.SquareD25; break; - case SquarePattern.D50: _pat = Utils.SquareD50; break; - case SquarePattern.D75: _pat = Utils.SquareD75; break; - } - } - - public override void SetPitch(int pitch) - { - _frequency = 3520 * (float)Math.Pow(2, ((Note.Key - 69) / 12f) + (pitch / 768f)); - } - - public override void Process(float[] buffer) - { - StepEnvelope(); - if (State == EnvelopeState.Dead) - { - return; - } - - ChannelVolume vol = GetVolume(); - float interStep = _frequency * _mixer.SampleRateReciprocal; - - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = _pat[_pos]; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos = (_pos + posDelta) & 0x7; - } while (--samplesPerBuffer > 0); - } - } - internal class PCM4Channel : PSGChannel - { - private float[] _sample; - - public PCM4Channel(Mixer mixer) : base(mixer) { } - public void Init(Track owner, Note note, ADSR env, int instPan, int sampleOffset) - { - Init(owner, note, env, instPan); - _sample = Utils.PCM4ToFloat(sampleOffset); - } - - public override void SetPitch(int pitch) - { - _frequency = 7040 * (float)Math.Pow(2, ((Note.Key - 69) / 12f) + (pitch / 768f)); - } - - public override void Process(float[] buffer) - { - StepEnvelope(); - if (State == EnvelopeState.Dead) - { - return; - } - - ChannelVolume vol = GetVolume(); - float interStep = _frequency * _mixer.SampleRateReciprocal; - - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = _sample[_pos]; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos = (_pos + posDelta) & 0x1F; - } while (--samplesPerBuffer > 0); - } - } - internal class NoiseChannel : PSGChannel - { - private BitArray _pat; - - public NoiseChannel(Mixer mixer) : base(mixer) { } - public void Init(Track owner, Note note, ADSR env, int instPan, NoisePattern pattern) - { - Init(owner, note, env, instPan); - _pat = pattern == NoisePattern.Fine ? Utils.NoiseFine : Utils.NoiseRough; - } - - public override void SetPitch(int pitch) - { - int key = Note.Key + (int)Math.Round(pitch / 64f); - if (key <= 20) - { - key = 0; - } - else - { - key -= 21; - if (key > 59) - { - key = 59; - } - } - byte v = Utils.NoiseFrequencyTable[key]; - // The following emulates 0x0400007C - SOUND4CNT_H - int r = v & 7; // Bits 0-2 - int s = v >> 4; // Bits 4-7 - _frequency = 524288f / (r == 0 ? 0.5f : r) / (float)Math.Pow(2, s + 1); - } - - public override void Process(float[] buffer) - { - StepEnvelope(); - if (State == EnvelopeState.Dead) - { - return; - } - - ChannelVolume vol = GetVolume(); - float interStep = _frequency * _mixer.SampleRateReciprocal; - - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = _pat[_pos & (_pat.Length - 1)] ? 0.5f : -0.5f; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos = (_pos + posDelta) & (_pat.Length - 1); - } while (--samplesPerBuffer > 0); - } - } -} \ No newline at end of file diff --git a/VG Music Studio/Core/GBA/MP2K/Commands.cs b/VG Music Studio/Core/GBA/MP2K/Commands.cs deleted file mode 100644 index 776d013f..00000000 --- a/VG Music Studio/Core/GBA/MP2K/Commands.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System.Drawing; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal class CallCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Call"; - public string Arguments => $"0x{Offset:X7}"; - - public int Offset { get; set; } - } - internal class EndOfTieCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "End Of Tie"; - public string Arguments => Key == -1 ? "All Ties" : Util.Utils.GetNoteName(Key); - - public int Key { get; set; } - } - internal class FinishCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Finish"; - public string Arguments => Prev ? "Resume previous track" : "End track"; - - public bool Prev { get; set; } - } - internal class JumpCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Jump"; - public string Arguments => $"0x{Offset:X7}"; - - public int Offset { get; set; } - } - internal class LFODelayCommand : ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Delay"; - public string Arguments => Delay.ToString(); - - public byte Delay { get; set; } - } - internal class LFODepthCommand : ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Depth"; - public string Arguments => Depth.ToString(); - - public byte Depth { get; set; } - } - internal class LFOSpeedCommand : ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Speed"; - public string Arguments => Speed.ToString(); - - public byte Speed { get; set; } - } - internal class LFOTypeCommand : ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Type"; - public string Arguments => Type.ToString(); - - public LFOType Type { get; set; } - } - internal class LibraryCommand : ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Library Call"; - public string Arguments => $"{Command}, {Argument}"; - - public byte Command { get; set; } - public byte Argument { get; set; } - } - internal class MemoryAccessCommand : ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Memory Access"; - public string Arguments => $"{Operator}, {Address}, {Data}"; - - public byte Operator { get; set; } - public byte Address { get; set; } - public byte Data { get; set; } - } - internal class NoteCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Note"; - public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Velocity} {Duration}"; - - public byte Key { get; set; } - public byte Velocity { get; set; } - public int Duration { get; set; } - } - internal class PanpotCommand : ICommand - { - public Color Color => Color.GreenYellow; - public string Label => "Panpot"; - public string Arguments => Panpot.ToString(); - - public sbyte Panpot { get; set; } - } - internal class PitchBendCommand : ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Pitch Bend"; - public string Arguments => Bend.ToString(); - - public sbyte Bend { get; set; } - } - internal class PitchBendRangeCommand : ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Pitch Bend Range"; - public string Arguments => Range.ToString(); - - public byte Range { get; set; } - } - internal class PriorityCommand : ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Priority"; - public string Arguments => Priority.ToString(); - - public byte Priority { get; set; } - } - internal class RepeatCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Repeat"; - public string Arguments => $"{Times}, 0x{Offset:X7}"; - - public byte Times { get; set; } - public int Offset { get; set; } - } - internal class RestCommand : ICommand - { - public Color Color => Color.PaleVioletRed; - public string Label => "Rest"; - public string Arguments => Rest.ToString(); - - public byte Rest { get; set; } - } - internal class ReturnCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Return"; - public string Arguments => string.Empty; - } - internal class TempoCommand : ICommand - { - public Color Color => Color.DeepSkyBlue; - public string Label => "Tempo"; - public string Arguments => Tempo.ToString(); - - public ushort Tempo { get; set; } - } - internal class TransposeCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Transpose"; - public string Arguments => Transpose.ToString(); - - public sbyte Transpose { get; set; } - } - internal class TuneCommand : ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Fine Tune"; - public string Arguments => Tune.ToString(); - - public sbyte Tune { get; set; } - } - internal class VoiceCommand : ICommand - { - public Color Color => Color.DarkSalmon; - public string Label => "Voice"; - public string Arguments => Voice.ToString(); - - public byte Voice { get; set; } - } - internal class VolumeCommand : ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Volume"; - public string Arguments => Volume.ToString(); - - public byte Volume { get; set; } - } -} diff --git a/VG Music Studio/Core/GBA/MP2K/Config.cs b/VG Music Studio/Core/GBA/MP2K/Config.cs deleted file mode 100644 index 9a0ea239..00000000 --- a/VG Music Studio/Core/GBA/MP2K/Config.cs +++ /dev/null @@ -1,242 +0,0 @@ -using Kermalis.EndianBinaryIO; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using YamlDotNet.RepresentationModel; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal class Config : Core.Config - { - public readonly byte[] ROM; - public readonly EndianBinaryReader Reader; - public readonly string GameCode; - public readonly byte Version; - - public readonly string Name; - public readonly int[] SongTableOffsets; - public readonly long[] SongTableSizes; - public readonly int SampleRate; - public readonly ReverbType ReverbType; - public readonly byte Reverb; - public readonly byte Volume; - public readonly bool HasGoldenSunSynths; - public readonly bool HasPokemonCompression; - - public Config(byte[] rom) - { - const string configFile = "MP2K.yaml"; - using (StreamReader fileStream = File.OpenText(Util.Utils.CombineWithBaseDirectory(configFile))) - { - string gcv = string.Empty; - try - { - ROM = rom; - Reader = new EndianBinaryReader(new MemoryStream(rom)); - GameCode = Reader.ReadString(4, false, 0xAC); - Version = Reader.ReadByte(0xBC); - gcv = $"{GameCode}_{Version:X2}"; - var yaml = new YamlStream(); - yaml.Load(fileStream); - - var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; - YamlMappingNode game; - try - { - game = (YamlMappingNode)mapping.Children.GetValue(gcv); - } - catch (BetterKeyNotFoundException) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KMissingGameCode, gcv))); - } - - YamlNode nameNode = null, - songTableOffsetsNode = null, - songTableSizesNode = null, - sampleRateNode = null, - reverbTypeNode = null, - reverbNode = null, - volumeNode = null, - hasGoldenSunSynthsNode = null, - hasPokemonCompression = null; - void Load(YamlMappingNode gameToLoad) - { - if (gameToLoad.Children.TryGetValue("Copy", out YamlNode node)) - { - YamlMappingNode copyGame; - try - { - copyGame = (YamlMappingNode)mapping.Children.GetValue(node); - } - catch (BetterKeyNotFoundException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KCopyInvalidGameCode, ex.Key))); - } - Load(copyGame); - } - if (gameToLoad.Children.TryGetValue(nameof(Name), out node)) - { - nameNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SongTableOffsets), out node)) - { - songTableOffsetsNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SongTableSizes), out node)) - { - songTableSizesNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SampleRate), out node)) - { - sampleRateNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(ReverbType), out node)) - { - reverbTypeNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(Reverb), out node)) - { - reverbNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(Volume), out node)) - { - volumeNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(HasGoldenSunSynths), out node)) - { - hasGoldenSunSynthsNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(HasPokemonCompression), out node)) - { - hasPokemonCompression = node; - } - if (gameToLoad.Children.TryGetValue(nameof(Playlists), out node)) - { - var playlists = (YamlMappingNode)node; - foreach (KeyValuePair kvp in playlists) - { - string name = kvp.Key.ToString(); - var songs = new List(); - foreach (KeyValuePair song in (YamlMappingNode)kvp.Value) - { - long songIndex = Util.Utils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Playlists)), song.Key.ToString(), 0, long.MaxValue); - if (songs.Any(s => s.Index == songIndex)) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongRepeated, name, songIndex))); - } - songs.Add(new Song(songIndex, song.Value.ToString())); - } - Playlists.Add(new Playlist(name, songs)); - } - } - } - - Load(game); - - if (nameNode == null) - { - throw new BetterKeyNotFoundException(nameof(Name), null); - } - Name = nameNode.ToString(); - if (songTableOffsetsNode == null) - { - throw new BetterKeyNotFoundException(nameof(SongTableOffsets), null); - } - string[] songTables = songTableOffsetsNode.ToString().SplitSpace(StringSplitOptions.RemoveEmptyEntries); - int numSongTables = songTables.Length; - if (numSongTables == 0) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyNoEntries, nameof(SongTableOffsets)))); - } - if (songTableSizesNode == null) - { - throw new BetterKeyNotFoundException(nameof(SongTableSizes), null); - } - string[] sizes = songTableSizesNode.ToString().SplitSpace(StringSplitOptions.RemoveEmptyEntries); - if (sizes.Length != numSongTables) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongTableCounts, nameof(SongTableSizes), nameof(SongTableOffsets)))); - } - SongTableOffsets = new int[numSongTables]; - SongTableSizes = new long[numSongTables]; - int maxOffset = rom.Length - 1; - for (int i = 0; i < numSongTables; i++) - { - SongTableSizes[i] = Util.Utils.ParseValue(nameof(SongTableSizes), sizes[i], 1, maxOffset); - SongTableOffsets[i] = (int)Util.Utils.ParseValue(nameof(SongTableOffsets), songTables[i], 0, maxOffset); - } - if (sampleRateNode == null) - { - throw new BetterKeyNotFoundException(nameof(SampleRate), null); - } - SampleRate = (int)Util.Utils.ParseValue(nameof(SampleRate), sampleRateNode.ToString(), 0, Utils.FrequencyTable.Length - 1); - if (reverbTypeNode == null) - { - throw new BetterKeyNotFoundException(nameof(ReverbType), null); - } - ReverbType = Util.Utils.ParseEnum(nameof(ReverbType), reverbTypeNode.ToString()); - if (reverbNode == null) - { - throw new BetterKeyNotFoundException(nameof(Reverb), null); - } - Reverb = (byte)Util.Utils.ParseValue(nameof(Reverb), reverbNode.ToString(), byte.MinValue, byte.MaxValue); - if (volumeNode == null) - { - throw new BetterKeyNotFoundException(nameof(Volume), null); - } - Volume = (byte)Util.Utils.ParseValue(nameof(Volume), volumeNode.ToString(), 0, 15); - if (hasGoldenSunSynthsNode == null) - { - throw new BetterKeyNotFoundException(nameof(HasGoldenSunSynths), null); - } - HasGoldenSunSynths = Util.Utils.ParseBoolean(nameof(HasGoldenSunSynths), hasGoldenSunSynthsNode.ToString()); - if (hasPokemonCompression == null) - { - throw new BetterKeyNotFoundException(nameof(HasPokemonCompression), null); - } - HasPokemonCompression = Util.Utils.ParseBoolean(nameof(HasPokemonCompression), hasPokemonCompression.ToString()); - - // The complete playlist - if (!Playlists.Any(p => p.Name == "Music")) - { - Playlists.Insert(0, new Playlist(Strings.PlaylistMusic, Playlists.SelectMany(p => p.Songs).Distinct().OrderBy(s => s.Index))); - } - } - catch (BetterKeyNotFoundException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyMissing, ex.Key))); - } - catch (InvalidValueException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + ex.Message)); - } - catch (YamlDotNet.Core.YamlException ex) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + ex.Message)); - } - } - } - - public override string GetGameName() - { - return Name; - } - public override string GetSongName(long index) - { - Song s = GetFirstSong(index); - if (s != null) - { - return s.Name; - } - return index.ToString(); - } - - public override void Dispose() - { - Reader.Dispose(); - } - } -} diff --git a/VG Music Studio/Core/GBA/MP2K/Enums.cs b/VG Music Studio/Core/GBA/MP2K/Enums.cs deleted file mode 100644 index f22ac7e9..00000000 --- a/VG Music Studio/Core/GBA/MP2K/Enums.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal enum EnvelopeState : byte - { - Initializing, - Rising, - Decaying, - Playing, - Releasing, - Dying, - Dead - } - internal enum ReverbType : byte - { - None, - Normal, - Camelot1, - Camelot2, - MGAT - } - - internal enum GoldenSunPSGType : byte - { - Square, - Saw, - Triangle - } - internal enum LFOType : byte - { - Pitch, - Volume, - Panpot - } - internal enum SquarePattern : byte - { - D12, - D25, - D50, - D75 - } - internal enum NoisePattern : byte - { - Fine, - Rough - } - internal enum VoiceType : byte - { - PCM8, - Square1, - Square2, - PCM4, - Noise, - Invalid5, - Invalid6, - Invalid7 - } - [Flags] - internal enum VoiceFlags : byte - { - // These are flags that apply to the types - Fixed = 0x08, // PCM8 - OffWithNoise = 0x08, // Square1, Square2, PCM4, Noise - Reversed = 0x10, // PCM8 - Compressed = 0x20, // PCM8 (Only in Pokémon main series games) - - // These are flags that cancel out every other bit after them if set so they should only be checked with equality - KeySplit = 0x40, - Drum = 0x80 - } -} diff --git a/VG Music Studio/Core/GBA/MP2K/Mixer.cs b/VG Music Studio/Core/GBA/MP2K/Mixer.cs deleted file mode 100644 index 738859f6..00000000 --- a/VG Music Studio/Core/GBA/MP2K/Mixer.cs +++ /dev/null @@ -1,275 +0,0 @@ -using NAudio.Wave; -using System; -using System.Linq; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal class Mixer : Core.Mixer - { - public readonly int SampleRate; - public readonly int SamplesPerBuffer; - public readonly float SampleRateReciprocal; - private readonly float _samplesReciprocal; - public readonly float PCM8MasterVolume; - private bool _isFading; - private long _fadeMicroFramesLeft; - private float _fadePos; - private float _fadeStepPerMicroframe; - - public readonly Config Config; - private readonly WaveBuffer _audio; - private readonly float[][] _trackBuffers; - private readonly PCM8Channel[] _pcm8Channels; - private readonly SquareChannel _sq1; - private readonly SquareChannel _sq2; - private readonly PCM4Channel _pcm4; - private readonly NoiseChannel _noise; - private readonly PSGChannel[] _psgChannels; - private readonly BufferedWaveProvider _buffer; - - public Mixer(Config config) - { - Config = config; - (SampleRate, SamplesPerBuffer) = Utils.FrequencyTable[config.SampleRate]; - SampleRateReciprocal = 1f / SampleRate; - _samplesReciprocal = 1f / SamplesPerBuffer; - PCM8MasterVolume = config.Volume / 15f; - - _pcm8Channels = new PCM8Channel[24]; - for (int i = 0; i < _pcm8Channels.Length; i++) - { - _pcm8Channels[i] = new PCM8Channel(this); - } - _psgChannels = new PSGChannel[] { _sq1 = new SquareChannel(this), _sq2 = new SquareChannel(this), _pcm4 = new PCM4Channel(this), _noise = new NoiseChannel(this) }; - - int amt = SamplesPerBuffer * 2; - _audio = new WaveBuffer(amt * sizeof(float)) { FloatBufferCount = amt }; - _trackBuffers = new float[0x10][]; - for (int i = 0; i < _trackBuffers.Length; i++) - { - _trackBuffers[i] = new float[amt]; - } - _buffer = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(SampleRate, 2)) - { - DiscardOnBufferOverflow = true, - BufferLength = SamplesPerBuffer * 64 - }; - Init(_buffer); - } - public override void Dispose() - { - base.Dispose(); - CloseWaveWriter(); - } - - public PCM8Channel AllocPCM8Channel(Track owner, ADSR env, Note note, byte vol, sbyte pan, int instPan, int pitch, bool bFixed, bool bCompressed, int sampleOffset) - { - PCM8Channel nChn = null; - IOrderedEnumerable byOwner = _pcm8Channels.OrderByDescending(c => c.Owner == null ? 0xFF : c.Owner.Index); - foreach (PCM8Channel i in byOwner) // Find free - { - if (i.State == EnvelopeState.Dead || i.Owner == null) - { - nChn = i; - break; - } - } - if (nChn == null) // Find releasing - { - foreach (PCM8Channel i in byOwner) - { - if (i.State == EnvelopeState.Releasing) - { - nChn = i; - break; - } - } - } - if (nChn == null) // Find prioritized - { - foreach (PCM8Channel i in byOwner) - { - if (owner.Priority > i.Owner.Priority) - { - nChn = i; - break; - } - } - } - if (nChn == null) // None available - { - PCM8Channel lowest = byOwner.First(); // Kill lowest track's instrument if the track is lower than this one - if (lowest.Owner.Index >= owner.Index) - { - nChn = lowest; - } - } - if (nChn != null) // Could still be null from the above if - { - nChn.Init(owner, note, env, sampleOffset, vol, pan, instPan, pitch, bFixed, bCompressed); - } - return nChn; - } - public PSGChannel AllocPSGChannel(Track owner, ADSR env, Note note, byte vol, sbyte pan, int instPan, int pitch, VoiceType type, object arg) - { - PSGChannel nChn; - switch (type) - { - case VoiceType.Square1: - { - nChn = _sq1; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) - { - return null; - } - _sq1.Init(owner, note, env, instPan, (SquarePattern)arg); - break; - } - case VoiceType.Square2: - { - nChn = _sq2; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) - { - return null; - } - _sq2.Init(owner, note, env, instPan, (SquarePattern)arg); - break; - } - case VoiceType.PCM4: - { - nChn = _pcm4; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) - { - return null; - } - _pcm4.Init(owner, note, env, instPan, (int)arg); - break; - } - case VoiceType.Noise: - { - nChn = _noise; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) - { - return null; - } - _noise.Init(owner, note, env, instPan, (NoisePattern)arg); - break; - } - default: return null; - } - nChn.SetVolume(vol, pan); - nChn.SetPitch(pitch); - return nChn; - } - - public void BeginFadeIn() - { - _fadePos = 0f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * GBA.Utils.AGB_FPS); - _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; - _isFading = true; - } - public void BeginFadeOut() - { - _fadePos = 1f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * GBA.Utils.AGB_FPS); - _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; - _isFading = true; - } - public bool IsFading() - { - return _isFading; - } - public bool IsFadeDone() - { - return _isFading && _fadeMicroFramesLeft == 0; - } - public void ResetFade() - { - _isFading = false; - _fadeMicroFramesLeft = 0; - } - - private WaveFileWriter _waveWriter; - public void CreateWaveWriter(string fileName) - { - _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); - } - public void CloseWaveWriter() - { - _waveWriter?.Dispose(); - } - public void Process(bool output, bool recording) - { - for (int i = 0; i < _trackBuffers.Length; i++) - { - float[] buf = _trackBuffers[i]; - Array.Clear(buf, 0, buf.Length); - } - _audio.Clear(); - - for (int i = 0; i < _pcm8Channels.Length; i++) - { - PCM8Channel c = _pcm8Channels[i]; - if (c.Owner != null) - { - c.Process(_trackBuffers[c.Owner.Index]); - } - } - - for (int i = 0; i < _psgChannels.Length; i++) - { - PSGChannel c = _psgChannels[i]; - if (c.Owner != null) - { - c.Process(_trackBuffers[c.Owner.Index]); - } - } - - float masterStep; - float masterLevel; - if (_isFading && _fadeMicroFramesLeft == 0) - { - masterStep = 0; - masterLevel = 0; - } - else - { - float fromMaster = 1f; - float toMaster = 1f; - if (_fadeMicroFramesLeft > 0) - { - const float scale = 10f / 6f; - fromMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadePos += _fadeStepPerMicroframe; - toMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadeMicroFramesLeft--; - } - masterStep = (toMaster - fromMaster) * _samplesReciprocal; - masterLevel = fromMaster; - } - for (int i = 0; i < _trackBuffers.Length; i++) - { - if (!Mutes[i]) - { - float level = masterLevel; - float[] buf = _trackBuffers[i]; - for (int j = 0; j < SamplesPerBuffer; j++) - { - _audio.FloatBuffer[j * 2] += buf[j * 2] * level; - _audio.FloatBuffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; - level += masterStep; - } - } - } - if (output) - { - _buffer.AddSamples(_audio.ByteBuffer, 0, _audio.ByteBufferCount); - } - if (recording) - { - _waveWriter.Write(_audio.ByteBuffer, 0, _audio.ByteBufferCount); - } - } - } -} diff --git a/VG Music Studio/Core/GBA/MP2K/Player.cs b/VG Music Studio/Core/GBA/MP2K/Player.cs deleted file mode 100644 index b0708425..00000000 --- a/VG Music Studio/Core/GBA/MP2K/Player.cs +++ /dev/null @@ -1,1510 +0,0 @@ -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using Sanford.Multimedia.Midi; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal class Player : IPlayer - { - public class MIDISaveArgs - { - public bool SaveCommandsBeforeTranspose; - public bool ReverseVolume; - public List<(int AbsoluteTick, (byte Numerator, byte Denominator))> TimeSignatures; - } - - private readonly Mixer _mixer; - private readonly Config _config; - private readonly TimeBarrier _time; - private Thread _thread; - private int _voiceTableOffset = -1; - private Track[] _tracks; - private ushort _tempo; - private int _tempoStack; - private long _elapsedLoops; - - public List[] Events { get; private set; } - public long MaxTicks { get; private set; } - public long ElapsedTicks { get; private set; } - public bool ShouldFadeOut { get; set; } - public long NumLoops { get; set; } - private int _longestTrack; - - public PlayerState State { get; private set; } - public event SongEndedEvent SongEnded; - - public Player(Mixer mixer, Config config) - { - _mixer = mixer; - _config = config; - - _time = new TimeBarrier(GBA.Utils.AGB_FPS); - } - private void CreateThread() - { - _thread = new Thread(Tick) { Name = "MP2K Player Tick" }; - _thread.Start(); - } - private void WaitThread() - { - if (_thread != null && (_thread.ThreadState == System.Threading.ThreadState.Running || _thread.ThreadState == System.Threading.ThreadState.WaitSleepJoin)) - { - _thread.Join(); - } - } - - private void InitEmulation() - { - _tempo = 150; - _tempoStack = 0; - _elapsedLoops = 0; - ElapsedTicks = 0; - _mixer.ResetFade(); - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - _tracks[trackIndex].Init(); - } - } - private void SetTicks() - { - MaxTicks = 0; - bool u = false; - for (int trackIndex = 0; trackIndex < Events.Length; trackIndex++) - { - Events[trackIndex] = Events[trackIndex].OrderBy(e => e.Offset).ToList(); - List evs = Events[trackIndex]; - Track track = _tracks[trackIndex]; - track.Init(); - ElapsedTicks = 0; - while (true) - { - SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); - if (track.CallStackDepth == 0 && e.Ticks.Count > 0) - { - break; - } - else - { - e.Ticks.Add(ElapsedTicks); - ExecuteNext(track, ref u); - if (track.Stopped) - { - break; - } - else - { - ElapsedTicks += track.Rest; - track.Rest = 0; - } - } - } - if (ElapsedTicks > MaxTicks) - { - _longestTrack = trackIndex; - MaxTicks = ElapsedTicks; - } - track.StopAllChannels(); - } - } - public void LoadSong(long index) - { - if (_tracks != null) - { - for (int i = 0; i < _tracks.Length; i++) - { - _tracks[i].StopAllChannels(); - } - _tracks = null; - } - Events = null; - SongEntry entry = _config.Reader.ReadObject(_config.SongTableOffsets[0] + (index * 8)); - SongHeader header = _config.Reader.ReadObject(entry.HeaderOffset - GBA.Utils.CartridgeOffset); - int oldVoiceTableOffset = _voiceTableOffset; - _voiceTableOffset = header.VoiceTableOffset - GBA.Utils.CartridgeOffset; - if (oldVoiceTableOffset != _voiceTableOffset) - { - _voiceTypeCache = new string[byte.MaxValue + 1]; - } - _tracks = new Track[header.NumTracks]; - Events = new List[header.NumTracks]; - for (byte trackIndex = 0; trackIndex < header.NumTracks; trackIndex++) - { - int trackStart = header.TrackOffsets[trackIndex] - GBA.Utils.CartridgeOffset; - _tracks[trackIndex] = new Track(trackIndex, trackStart); - Events[trackIndex] = new List(); - bool EventExists(long offset) - { - return Events[trackIndex].Any(e => e.Offset == offset); - } - - byte runCmd = 0, prevKey = 0, prevVelocity = 0x7F; - int callStackDepth = 0; - AddEvents(trackStart); - void AddEvents(long startOffset) - { - _config.Reader.BaseStream.Position = startOffset; - bool cont = true; - while (cont) - { - long offset = _config.Reader.BaseStream.Position; - void AddEvent(ICommand command) - { - Events[trackIndex].Add(new SongEvent(offset, command)); - } - void EmulateNote(byte key, byte velocity, byte addedDuration) - { - prevKey = key; - prevVelocity = velocity; - if (!EventExists(offset)) - { - AddEvent(new NoteCommand - { - Key = key, - Velocity = velocity, - Duration = runCmd == 0xCF ? -1 : (Utils.RestTable[runCmd - 0xCF] + addedDuration) - }); - } - } - - byte cmd = _config.Reader.ReadByte(); - if (cmd >= 0xBD) // Commands that work within running status - { - runCmd = cmd; - } - - #region TIE & Notes - - if (runCmd >= 0xCF && cmd <= 0x7F) // Within running status - { - byte velocity, addedDuration; - byte[] peek = _config.Reader.PeekBytes(2); - if (peek[0] > 0x7F) - { - velocity = prevVelocity; - addedDuration = 0; - } - else if (peek[1] > 3) - { - velocity = _config.Reader.ReadByte(); - addedDuration = 0; - } - else - { - velocity = _config.Reader.ReadByte(); - addedDuration = _config.Reader.ReadByte(); - } - EmulateNote(cmd, velocity, addedDuration); - } - else if (cmd >= 0xCF) - { - byte key, velocity, addedDuration; - byte[] peek = _config.Reader.PeekBytes(3); - if (peek[0] > 0x7F) - { - key = prevKey; - velocity = prevVelocity; - addedDuration = 0; - } - else if (peek[1] > 0x7F) - { - key = _config.Reader.ReadByte(); - velocity = prevVelocity; - addedDuration = 0; - } - // TIE (0xCF) cannot have an added duration so it needs to stop here - else if (cmd == 0xCF || peek[2] > 3) - { - key = _config.Reader.ReadByte(); - velocity = _config.Reader.ReadByte(); - addedDuration = 0; - } - else - { - key = _config.Reader.ReadByte(); - velocity = _config.Reader.ReadByte(); - addedDuration = _config.Reader.ReadByte(); - } - EmulateNote(key, velocity, addedDuration); - } - - #endregion - - #region Rests - - else if (cmd >= 0x80 && cmd <= 0xB0) - { - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = Utils.RestTable[cmd - 0x80] }); - } - } - - #endregion - - #region Commands - - else if (runCmd < 0xCF && cmd <= 0x7F) - { - switch (runCmd) - { - case 0xBD: - { - if (!EventExists(offset)) - { - AddEvent(new VoiceCommand { Voice = cmd }); - } - break; - } - case 0xBE: - { - if (!EventExists(offset)) - { - AddEvent(new VolumeCommand { Volume = cmd }); - } - break; - } - case 0xBF: - { - if (!EventExists(offset)) - { - AddEvent(new PanpotCommand { Panpot = (sbyte)(cmd - 0x40) }); - } - break; - } - case 0xC0: - { - if (!EventExists(offset)) - { - AddEvent(new PitchBendCommand { Bend = (sbyte)(cmd - 0x40) }); - } - break; - } - case 0xC1: - { - if (!EventExists(offset)) - { - AddEvent(new PitchBendRangeCommand { Range = cmd }); - } - break; - } - case 0xC2: - { - if (!EventExists(offset)) - { - AddEvent(new LFOSpeedCommand { Speed = cmd }); - } - break; - } - case 0xC3: - { - if (!EventExists(offset)) - { - AddEvent(new LFODelayCommand { Delay = cmd }); - } - break; - } - case 0xC4: - { - if (!EventExists(offset)) - { - AddEvent(new LFODepthCommand { Depth = cmd }); - } - break; - } - case 0xC5: - { - if (!EventExists(offset)) - { - AddEvent(new LFOTypeCommand { Type = (LFOType)cmd }); - } - break; - } - case 0xC8: - { - if (!EventExists(offset)) - { - AddEvent(new TuneCommand { Tune = (sbyte)(cmd - 0x40) }); - } - break; - } - case 0xCD: - { - byte arg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new LibraryCommand { Command = cmd, Argument = arg }); - } - break; - } - case 0xCE: - { - prevKey = cmd; - if (!EventExists(offset)) - { - AddEvent(new EndOfTieCommand { Key = cmd }); - } - break; - } - default: throw new Exception(string.Format(Strings.ErrorMP2KInvalidRunningStatusCommand, trackIndex, offset, runCmd)); - } - } - else if (cmd > 0xB0 && cmd < 0xCF) - { - switch (cmd) - { - case 0xB1: - case 0xB6: - { - if (!EventExists(offset)) - { - AddEvent(new FinishCommand { Prev = cmd == 0xB6 }); - } - cont = false; - break; - } - case 0xB2: - { - int jumpOffset = _config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; - if (!EventExists(offset)) - { - AddEvent(new JumpCommand { Offset = jumpOffset }); - if (!EventExists(jumpOffset)) - { - AddEvents(jumpOffset); - } - } - cont = false; - break; - } - case 0xB3: - { - int callOffset = _config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; - if (!EventExists(offset)) - { - AddEvent(new CallCommand { Offset = callOffset }); - } - if (callStackDepth < 3) - { - long backup = _config.Reader.BaseStream.Position; - callStackDepth++; - AddEvents(callOffset); - _config.Reader.BaseStream.Position = backup; - } - else - { - throw new Exception(string.Format(Strings.ErrorMP2KSDATNestedCalls, trackIndex)); - } - break; - } - case 0xB4: - { - if (!EventExists(offset)) - { - AddEvent(new ReturnCommand()); - } - if (callStackDepth != 0) - { - cont = false; - callStackDepth--; - } - break; - } - /*case 0xB5: // TODO: Logic so this isn't an infinite loop - { - byte times = config.Reader.ReadByte(); - int repeatOffset = config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; - if (!EventExists(offset)) - { - AddEvent(new RepeatCommand { Times = times, Offset = repeatOffset }); - } - break; - }*/ - case 0xB9: - { - byte op = _config.Reader.ReadByte(); - byte address = _config.Reader.ReadByte(); - byte data = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new MemoryAccessCommand { Operator = op, Address = address, Data = data }); - } - break; - } - case 0xBA: - { - byte priority = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new PriorityCommand { Priority = priority }); - } - break; - } - case 0xBB: - { - byte tempoArg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new TempoCommand { Tempo = (ushort)(tempoArg * 2) }); - } - break; - } - case 0xBC: - { - sbyte transpose = _config.Reader.ReadSByte(); - if (!EventExists(offset)) - { - AddEvent(new TransposeCommand { Transpose = transpose }); - } - break; - } - // Commands that work within running status: - case 0xBD: - { - byte voice = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new VoiceCommand { Voice = voice }); - } - break; - } - case 0xBE: - { - byte volume = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new VolumeCommand { Volume = volume }); - } - break; - } - case 0xBF: - { - byte panArg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); - } - break; - } - case 0xC0: - { - byte bendArg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new PitchBendCommand { Bend = (sbyte)(bendArg - 0x40) }); - } - break; - } - case 0xC1: - { - byte range = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new PitchBendRangeCommand { Range = range }); - } - break; - } - case 0xC2: - { - byte speed = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new LFOSpeedCommand { Speed = speed }); - } - break; - } - case 0xC3: - { - byte delay = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new LFODelayCommand { Delay = delay }); - } - break; - } - case 0xC4: - { - byte depth = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new LFODepthCommand { Depth = depth }); - } - break; - } - case 0xC5: - { - byte type = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new LFOTypeCommand { Type = (LFOType)type }); - } - break; - } - case 0xC8: - { - byte tuneArg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new TuneCommand { Tune = (sbyte)(tuneArg - 0x40) }); - } - break; - } - case 0xCD: - { - byte command = _config.Reader.ReadByte(); - byte arg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new LibraryCommand { Command = command, Argument = arg }); - } - break; - } - case 0xCE: - { - int key = _config.Reader.PeekByte() <= 0x7F ? (prevKey = _config.Reader.ReadByte()) : -1; - if (!EventExists(offset)) - { - AddEvent(new EndOfTieCommand { Key = key }); - } - break; - } - default: throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, trackIndex, offset, cmd)); - } - } - - #endregion - } - } - } - SetTicks(); - } - public void SetCurrentPosition(long ticks) - { - if (Events == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - if (State == PlayerState.Playing) - { - Pause(); - } - InitEmulation(); - bool u = false; - while (true) - { - if (ElapsedTicks == ticks) - { - goto finish; - } - else - { - while (_tempoStack >= 150) - { - _tempoStack -= 150; - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - Track track = _tracks[trackIndex]; - if (!track.Stopped) - { - track.Tick(); - while (track.Rest == 0 && !track.Stopped) - { - ExecuteNext(track, ref u); - } - } - } - ElapsedTicks++; - if (ElapsedTicks == ticks) - { - goto finish; - } - } - _tempoStack += _tempo; - } - } - finish: - for (int i = 0; i < _tracks.Length; i++) - { - _tracks[i].StopAllChannels(); - } - Pause(); - } - } - // TODO: Don't use events, read from rom - public void SaveAsMIDI(string fileName, MIDISaveArgs args) - { - // TODO: FINE vs PREV - // TODO: https://github.com/Kermalis/VGMusicStudio/issues/36 - // TODO: Nested calls - // TODO: REPT - byte baseVolume = 0x7F; - if (args.ReverseVolume) - { - baseVolume = Events.SelectMany(e => e).Where(e => e.Command is VolumeCommand).Select(e => ((VolumeCommand)e.Command).Volume).Max(); - Debug.WriteLine($"Reversing volume back from {baseVolume}."); - } - - using (var midi = new Sequence(24) { Format = 1 }) - { - var metaTrack = new Sanford.Multimedia.Midi.Track(); - midi.Add(metaTrack); - var ts = new TimeSignatureBuilder(); - foreach ((int AbsoluteTick, (byte Numerator, byte Denominator)) e in args.TimeSignatures) - { - ts.Numerator = e.Item2.Numerator; - ts.Denominator = e.Item2.Denominator; - ts.ClocksPerMetronomeClick = 24; - ts.ThirtySecondNotesPerQuarterNote = 8; - ts.Build(); - metaTrack.Insert(e.AbsoluteTick, ts.Result); - } - - for (int trackIndex = 0; trackIndex < Events.Length; trackIndex++) - { - var track = new Sanford.Multimedia.Midi.Track(); - midi.Add(track); - - bool foundTranspose = false; - int endOfPattern = 0; - long startOfPatternTicks = 0, endOfPatternTicks = 0; - sbyte transpose = 0; - var playing = new List(); - for (int i = 0; i < Events[trackIndex].Count; i++) - { - SongEvent e = Events[trackIndex][i]; - int ticks = (int)(e.Ticks[0] + (endOfPatternTicks - startOfPatternTicks)); - - // Preliminary check for saving events before transpose - switch (e.Command) - { - case TransposeCommand keysh: foundTranspose = true; break; - default: // If we should not save before transpose then skip this event - { - if (!args.SaveCommandsBeforeTranspose && !foundTranspose) - { - continue; - } - break; - } - } - // Now do the event magic... - switch (e.Command) - { - case CallCommand patt: - { - int callCmd = Events[trackIndex].FindIndex(c => c.Offset == patt.Offset); - endOfPattern = i; - endOfPatternTicks = e.Ticks[0]; - i = callCmd - 1; // -1 for incoming ++ - startOfPatternTicks = Events[trackIndex][callCmd].Ticks[0]; - break; - } - case EndOfTieCommand eot: - { - NoteCommand nc = eot.Key == -1 ? playing.LastOrDefault() : playing.LastOrDefault(no => no.Key == eot.Key); - if (nc != null) - { - int key = nc.Key + transpose; - if (key < 0) - { - key = 0; - } - else if (key > 0x7F) - { - key = 0x7F; - } - track.Insert(ticks, new ChannelMessage(ChannelCommand.NoteOff, trackIndex, key)); - playing.Remove(nc); - } - break; - } - case FinishCommand _: - { - // If the track is not only the finish command, place the finish command at the correct tick - if (track.Count > 1) - { - track.EndOfTrackOffset = (int)(e.Ticks[0] - track.GetMidiEvent(track.Count - 2).AbsoluteTicks); - } - goto endOfTrack; - } - case JumpCommand goTo: - { - if (trackIndex == 0) - { - int jumpCmd = Events[trackIndex].FindIndex(c => c.Offset == goTo.Offset); - metaTrack.Insert((int)Events[trackIndex][jumpCmd].Ticks[0], new MetaMessage(MetaType.Marker, new byte[] { (byte)'[' })); - metaTrack.Insert(ticks, new MetaMessage(MetaType.Marker, new byte[] { (byte)']' })); - } - break; - } - case LFODelayCommand lfodl: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 26, lfodl.Delay)); - break; - } - case LFODepthCommand mod: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, (int)ControllerType.ModulationWheel, mod.Depth)); - break; - } - case LFOSpeedCommand lfos: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 21, lfos.Speed)); - break; - } - case LFOTypeCommand modt: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 22, (byte)modt.Type)); - break; - } - case LibraryCommand xcmd: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 30, xcmd.Command)); - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 29, xcmd.Argument)); - break; - } - case MemoryAccessCommand memacc: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 13, memacc.Operator)); - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 14, memacc.Address)); - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 12, memacc.Data)); - break; - } - case NoteCommand note: - { - int key = note.Key + transpose; - if (key < 0) - { - key = 0; - } - else if (key > 0x7F) - { - key = 0x7F; - } - track.Insert(ticks, new ChannelMessage(ChannelCommand.NoteOn, trackIndex, key, note.Velocity)); - if (note.Duration != -1) - { - track.Insert(ticks + note.Duration, new ChannelMessage(ChannelCommand.NoteOff, trackIndex, key)); - } - else - { - playing.Add(note); - } - break; - } - case PanpotCommand pan: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, (int)ControllerType.Pan, pan.Panpot + 0x40)); - break; - } - case PitchBendCommand bend: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.PitchWheel, trackIndex, 0, bend.Bend + 0x40)); - break; - } - case PitchBendRangeCommand bendr: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 20, bendr.Range)); - break; - } - case PriorityCommand prio: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, (int)ControllerType.VolumeFine, prio.Priority)); - break; - } - case ReturnCommand _: - { - if (endOfPattern != 0) - { - i = endOfPattern; - endOfPattern = 0; - startOfPatternTicks = endOfPatternTicks = 0; - } - break; - } - case TempoCommand tempo: - { - var change = new TempoChangeBuilder { Tempo = 60000000 / tempo.Tempo }; - change.Build(); - metaTrack.Insert(ticks, change.Result); - break; - } - case TransposeCommand keysh: - { - transpose = keysh.Transpose; - break; - } - case TuneCommand tune: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 24, tune.Tune + 0x40)); - break; - } - case VoiceCommand voice: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.ProgramChange, trackIndex, voice.Voice)); - break; - } - case VolumeCommand vol: - { - double d = baseVolume / (double)0x7F; - int volume = (int)(vol.Volume / d); - // If there are rounding errors, fix them (happens if baseVolume is not 127 and baseVolume is not vol.Volume) - if (volume * baseVolume / 0x7F == vol.Volume - 1) - { - volume++; - } - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, (int)ControllerType.Volume, volume)); - break; - } - } - } - endOfTrack:; - } - midi.Save(fileName); - } - } - public void Play() - { - if (Events == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - Stop(); - InitEmulation(); - State = PlayerState.Playing; - CreateThread(); - } - } - public void Pause() - { - if (State == PlayerState.Playing) - { - State = PlayerState.Paused; - WaitThread(); - } - else if (State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.Playing; - CreateThread(); - } - } - public void Stop() - { - if (State == PlayerState.Playing || State == PlayerState.Paused) - { - State = PlayerState.Stopped; - WaitThread(); - } - } - public void Record(string fileName) - { - _mixer.CreateWaveWriter(fileName); - InitEmulation(); - State = PlayerState.Recording; - CreateThread(); - WaitThread(); - _mixer.CloseWaveWriter(); - } - public void Dispose() - { - if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.ShutDown; - WaitThread(); - } - } - private string[] _voiceTypeCache; - public void GetSongState(UI.SongInfoControl.SongInfo info) - { - info.Tempo = _tempo; - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - Track track = _tracks[trackIndex]; - UI.SongInfoControl.SongInfo.Track tin = info.Tracks[trackIndex]; - tin.Position = track.DataOffset; - tin.Rest = track.Rest; - tin.Voice = track.Voice; - tin.LFO = track.LFODepth; - if (_voiceTypeCache[track.Voice] == null) - { - byte t = _config.ROM[_voiceTableOffset + (track.Voice * 0xC)]; - if (t == (byte)VoiceFlags.KeySplit) - { - _voiceTypeCache[track.Voice] = "Key Split"; - } - else if (t == (byte)VoiceFlags.Drum) - { - _voiceTypeCache[track.Voice] = "Drum"; - } - else - { - switch ((VoiceType)(t & 0x7)) - { - case VoiceType.PCM8: _voiceTypeCache[track.Voice] = "PCM8"; break; - case VoiceType.Square1: _voiceTypeCache[track.Voice] = "Square 1"; break; - case VoiceType.Square2: _voiceTypeCache[track.Voice] = "Square 2"; break; - case VoiceType.PCM4: _voiceTypeCache[track.Voice] = "PCM4"; break; - case VoiceType.Noise: _voiceTypeCache[track.Voice] = "Noise"; break; - case VoiceType.Invalid5: _voiceTypeCache[track.Voice] = "Invalid 5"; break; - case VoiceType.Invalid6: _voiceTypeCache[track.Voice] = "Invalid 6"; break; - case VoiceType.Invalid7: _voiceTypeCache[track.Voice] = "Invalid 7"; break; - } - } - } - tin.Type = _voiceTypeCache[track.Voice]; - tin.Volume = track.GetVolume(); - tin.PitchBend = track.GetPitch(); - tin.Panpot = track.GetPanpot(); - - Channel[] channels = track.Channels.ToArray(); - if (channels.Length == 0) - { - tin.Keys[0] = byte.MaxValue; - tin.LeftVolume = 0f; - tin.RightVolume = 0f; - } - else - { - int numKeys = 0; - float left = 0f; - float right = 0f; - for (int j = 0; j < channels.Length; j++) - { - Channel c = channels[j]; - if (c.State < EnvelopeState.Releasing) - { - tin.Keys[numKeys++] = c.Note.OriginalKey; - } - ChannelVolume vol = c.GetVolume(); - if (vol.LeftVol > left) - { - left = vol.LeftVol; - } - if (vol.RightVol > right) - { - right = vol.RightVol; - } - } - tin.Keys[numKeys] = byte.MaxValue; // There's no way for numKeys to be after the last index in the array - tin.LeftVolume = left; - tin.RightVolume = right; - } - } - } - - // TODO: Don't use config.Reader (Or make ReadObjectCached(offset)) - private void PlayNote(Track track, byte key, byte velocity, byte addedDuration) - { - int k = key + track.Transpose; - if (k < 0) - { - k = 0; - } - else if (k > 0x7F) - { - k = 0x7F; - } - key = (byte)k; - track.PrevKey = key; - track.PrevVelocity = velocity; - if (track.Ready) - { - bool fromDrum = false; - int offset = _voiceTableOffset + (track.Voice * 12); - while (true) - { - VoiceEntry v = _config.Reader.ReadObject(offset); - if (v.Type == (int)VoiceFlags.KeySplit) - { - fromDrum = false; // In case there is a multi within a drum - byte inst = _config.Reader.ReadByte(v.Int8 - GBA.Utils.CartridgeOffset + key); - offset = v.Int4 - GBA.Utils.CartridgeOffset + (inst * 12); - } - else if (v.Type == (int)VoiceFlags.Drum) - { - fromDrum = true; - offset = v.Int4 - GBA.Utils.CartridgeOffset + (key * 12); - } - else - { - var note = new Note - { - Duration = track.RunCmd == 0xCF ? -1 : (Utils.RestTable[track.RunCmd - 0xCF] + addedDuration), - Velocity = velocity, - OriginalKey = key, - Key = fromDrum ? v.RootKey : key - }; - var type = (VoiceType)(v.Type & 0x7); - int instPan = v.Pan; - instPan = (instPan & 0x80) != 0 ? instPan - 0xC0 : 0; - switch (type) - { - case VoiceType.PCM8: - { - bool bFixed = (v.Type & (int)VoiceFlags.Fixed) != 0; - bool bCompressed = _config.HasPokemonCompression && ((v.Type & (int)VoiceFlags.Compressed) != 0); - _mixer.AllocPCM8Channel(track, v.ADSR, note, - track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), - bFixed, bCompressed, v.Int4 - GBA.Utils.CartridgeOffset); - return; - } - case VoiceType.Square1: - case VoiceType.Square2: - { - _mixer.AllocPSGChannel(track, v.ADSR, note, - track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), - type, (SquarePattern)v.Int4); - return; - } - case VoiceType.PCM4: - { - _mixer.AllocPSGChannel(track, v.ADSR, note, - track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), - type, v.Int4 - GBA.Utils.CartridgeOffset); - return; - } - case VoiceType.Noise: - { - _mixer.AllocPSGChannel(track, v.ADSR, note, - track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), - type, (NoisePattern)v.Int4); - return; - } - } - } - } - } - } - private void ExecuteNext(Track track, ref bool update) - { - byte cmd = _config.ROM[track.DataOffset++]; - if (cmd >= 0xBD) // Commands that work within running status - { - track.RunCmd = cmd; - } - if (track.RunCmd >= 0xCF && cmd <= 0x7F) // Within running status - { - byte peek0 = _config.ROM[track.DataOffset]; - byte peek1 = _config.ROM[track.DataOffset + 1]; - byte velocity, addedDuration; - if (peek0 > 0x7F) - { - velocity = track.PrevVelocity; - addedDuration = 0; - } - else if (peek1 > 3) - { - track.DataOffset++; - velocity = peek0; - addedDuration = 0; - } - else - { - track.DataOffset += 2; - velocity = peek0; - addedDuration = peek1; - } - PlayNote(track, cmd, velocity, addedDuration); - } - else if (cmd >= 0xCF) - { - byte peek0 = _config.ROM[track.DataOffset]; - byte peek1 = _config.ROM[track.DataOffset + 1]; - byte peek2 = _config.ROM[track.DataOffset + 2]; - byte key, velocity, addedDuration; - if (peek0 > 0x7F) - { - key = track.PrevKey; - velocity = track.PrevVelocity; - addedDuration = 0; - } - else if (peek1 > 0x7F) - { - track.DataOffset++; - key = peek0; - velocity = track.PrevVelocity; - addedDuration = 0; - } - else if (cmd == 0xCF || peek2 > 3) - { - track.DataOffset += 2; - key = peek0; - velocity = peek1; - addedDuration = 0; - } - else - { - track.DataOffset += 3; - key = peek0; - velocity = peek1; - addedDuration = peek2; - } - PlayNote(track, key, velocity, addedDuration); - } - else if (cmd >= 0x80 && cmd <= 0xB0) - { - track.Rest = Utils.RestTable[cmd - 0x80]; - } - else if (track.RunCmd < 0xCF && cmd <= 0x7F) - { - switch (track.RunCmd) - { - case 0xBD: - { - track.Voice = cmd; - //track.Ready = true; // This is unnecessary because if we're in running status of a voice command, then Ready was already set - break; - } - case 0xBE: - { - track.Volume = cmd; - update = true; - break; - } - case 0xBF: - { - track.Panpot = (sbyte)(cmd - 0x40); - update = true; - break; - } - case 0xC0: - { - track.PitchBend = (sbyte)(cmd - 0x40); - update = true; - break; - } - case 0xC1: - { - track.PitchBendRange = cmd; - update = true; - break; - } - case 0xC2: - { - track.LFOSpeed = cmd; - track.LFOPhase = 0; - track.LFODelayCount = 0; - update = true; - break; - } - case 0xC3: - { - track.LFODelay = cmd; - track.LFOPhase = 0; - track.LFODelayCount = 0; - update = true; - break; - } - case 0xC4: - { - track.LFODepth = cmd; - update = true; - break; - } - case 0xC5: - { - track.LFOType = (LFOType)cmd; - update = true; - break; - } - case 0xC8: - { - track.Tune = (sbyte)(cmd - 0x40); - update = true; - break; - } - case 0xCD: - { - track.DataOffset++; - break; - } - case 0xCE: - { - track.PrevKey = cmd; - int k = cmd + track.Transpose; - if (k < 0) - { - k = 0; - } - else if (k > 0x7F) - { - k = 0x7F; - } - track.ReleaseChannels(k); - break; - } - default: throw new Exception(string.Format(Strings.ErrorMP2KInvalidRunningStatusCommand, track.Index, track.DataOffset, track.RunCmd)); - } - } - else if (cmd > 0xB0 && cmd < 0xCF) - { - switch (cmd) - { - case 0xB1: - case 0xB6: - { - track.Stopped = true; - //track.ReleaseAllTieingChannels(); // Necessary? - break; - } - case 0xB2: - { - track.DataOffset = (_config.ROM[track.DataOffset++] | (_config.ROM[track.DataOffset++] << 8) | (_config.ROM[track.DataOffset++] << 16) | (_config.ROM[track.DataOffset++] << 24)) - GBA.Utils.CartridgeOffset; - break; - } - case 0xB3: - { - if (track.CallStackDepth < 3) - { - int callOffset = (_config.ROM[track.DataOffset++] | (_config.ROM[track.DataOffset++] << 8) | (_config.ROM[track.DataOffset++] << 16) | (_config.ROM[track.DataOffset++] << 24)) - GBA.Utils.CartridgeOffset; - track.CallStack[track.CallStackDepth] = track.DataOffset; - track.CallStackDepth++; - track.DataOffset = callOffset; - } - else - { - throw new Exception(string.Format(Strings.ErrorMP2KSDATNestedCalls, track.Index)); - } - break; - } - case 0xB4: - { - if (track.CallStackDepth != 0) - { - track.CallStackDepth--; - track.DataOffset = track.CallStack[track.CallStackDepth]; - } - break; - } - /*case 0xB5: // TODO: Logic so this isn't an infinite loop - { - byte times = config.Reader.ReadByte(); - int repeatOffset = config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; - if (!EventExists(offset)) - { - AddEvent(new RepeatCommand { Times = times, Offset = repeatOffset }); - } - break; - }*/ - case 0xB9: - { - track.DataOffset += 3; - break; - } - case 0xBA: - { - track.Priority = _config.ROM[track.DataOffset++]; - break; - } - case 0xBB: - { - _tempo = (ushort)(_config.ROM[track.DataOffset++] * 2); - break; - } - case 0xBC: - { - track.Transpose = (sbyte)_config.ROM[track.DataOffset++]; - break; - } - // Commands that work within running status: - case 0xBD: - { - track.Voice = _config.ROM[track.DataOffset++]; - track.Ready = true; - break; - } - case 0xBE: - { - track.Volume = _config.ROM[track.DataOffset++]; - update = true; - break; - } - case 0xBF: - { - track.Panpot = (sbyte)(_config.ROM[track.DataOffset++] - 0x40); - update = true; - break; - } - case 0xC0: - { - track.PitchBend = (sbyte)(_config.ROM[track.DataOffset++] - 0x40); - update = true; - break; - } - case 0xC1: - { - track.PitchBendRange = _config.ROM[track.DataOffset++]; - update = true; - break; - } - case 0xC2: - { - track.LFOSpeed = _config.ROM[track.DataOffset++]; - track.LFOPhase = 0; - track.LFODelayCount = 0; - update = true; - break; - } - case 0xC3: - { - track.LFODelay = _config.ROM[track.DataOffset++]; - track.LFOPhase = 0; - track.LFODelayCount = 0; - update = true; - break; - } - case 0xC4: - { - track.LFODepth = _config.ROM[track.DataOffset++]; - update = true; - break; - } - case 0xC5: - { - track.LFOType = (LFOType)_config.ROM[track.DataOffset++]; - update = true; - break; - } - case 0xC8: - { - track.Tune = (sbyte)(_config.ROM[track.DataOffset++] - 0x40); - update = true; - break; - } - case 0xCD: - { - track.DataOffset += 2; - break; - } - case 0xCE: - { - byte peek = _config.ROM[track.DataOffset]; - if (peek > 0x7F) - { - track.ReleaseChannels(track.PrevKey); - } - else - { - track.DataOffset++; - track.PrevKey = peek; - int k = peek + track.Transpose; - if (k < 0) - { - k = 0; - } - else if (k > 0x7F) - { - k = 0x7F; - } - track.ReleaseChannels(k); - } - break; - } - default: throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, track.Index, track.DataOffset, cmd)); - } - } - } - - private void Tick() - { - _time.Start(); - while (true) - { - PlayerState state = State; - bool playing = state == PlayerState.Playing; - bool recording = state == PlayerState.Recording; - if (!playing && !recording) - { - goto stop; - } - - void MixerProcess() - { - _mixer.Process(playing, recording); - } - - while (_tempoStack >= 150) - { - _tempoStack -= 150; - bool allDone = true; - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - Track track = _tracks[trackIndex]; - track.Tick(); - bool update = false; - while (track.Rest == 0 && !track.Stopped) - { - ExecuteNext(track, ref update); - } - if (trackIndex == _longestTrack) - { - if (ElapsedTicks == MaxTicks) - { - if (!track.Stopped) - { - List evs = Events[trackIndex]; - for (int i = 0; i < evs.Count; i++) - { - SongEvent ev = evs[i]; - if (ev.Offset == track.DataOffset) - { - ElapsedTicks = ev.Ticks[0] - track.Rest; - break; - } - } - _elapsedLoops++; - if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) - { - _mixer.BeginFadeOut(); - } - } - } - else - { - ElapsedTicks++; - } - } - if (!track.Stopped) - { - allDone = false; - } - if (track.Channels.Count > 0) - { - allDone = false; - if (update || track.LFODepth > 0) - { - track.UpdateChannels(); - } - } - } - if (_mixer.IsFadeDone()) - { - allDone = true; - } - if (allDone) - { - MixerProcess(); - State = PlayerState.Stopped; - SongEnded?.Invoke(); - } - } - _tempoStack += _tempo; - MixerProcess(); - if (playing) - { - _time.Wait(); - } - } - stop: - _time.Stop(); - } - } -} diff --git a/VG Music Studio/Core/GBA/MP2K/Structs.cs b/VG Music Studio/Core/GBA/MP2K/Structs.cs deleted file mode 100644 index 2da7d2c4..00000000 --- a/VG Music Studio/Core/GBA/MP2K/Structs.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal class SongEntry - { - public int HeaderOffset { get; set; } - public short Player { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown { get; set; } - } - internal class SongHeader - { - public byte NumTracks { get; set; } - public byte NumBlocks { get; set; } - public byte Priority { get; set; } - public byte Reverb { get; set; } - public int VoiceTableOffset { get; set; } - [BinaryArrayVariableLength(nameof(NumTracks))] - public int[] TrackOffsets { get; set; } - } - internal class VoiceEntry - { - public byte Type { get; set; } // 0 - public byte RootKey { get; set; } // 1 - public byte Unknown { get; set; } // 2 - public byte Pan { get; set; } // 3 - /// SquarePattern for Square1/Square2, NoisePattern for Noise, Address for PCM8/PCM4/KeySplit/Drum - public int Int4 { get; set; } // 4 - /// ADSR for PCM8/Square1/Square2/PCM4/Noise, KeysAddress for KeySplit - public ADSR ADSR { get; set; } // 8 - [BinaryIgnore] - public int Int8 => (ADSR.R << 24) | (ADSR.S << 16) | (ADSR.D << 8) | (ADSR.A); - } - internal struct ADSR // Used as a struct in GBChannel - { - public byte A { get; set; } - public byte D { get; set; } - public byte S { get; set; } - public byte R { get; set; } - } - internal class GoldenSunPSG - { - /// Always 0x80 - public byte Unknown { get; set; } - public GoldenSunPSGType Type { get; set; } - public byte InitialCycle { get; set; } - public byte CycleSpeed { get; set; } - public byte CycleAmplitude { get; set; } - public byte MinimumCycle { get; set; } - } - internal class SampleHeader - { - /// 0x40000000 if True - public int DoesLoop { get; set; } - /// Right shift 10 for value - public int SampleRate { get; set; } - public int LoopOffset { get; set; } - public int Length { get; set; } - } - - internal struct ChannelVolume - { - public float LeftVol, RightVol; - } - internal struct Note - { - public byte Key, OriginalKey; - public byte Velocity; - /// -1 if forever - public int Duration; - } -} diff --git a/VG Music Studio/Core/GBA/MP2K/Track.cs b/VG Music Studio/Core/GBA/MP2K/Track.cs deleted file mode 100644 index 1288008a..00000000 --- a/VG Music Studio/Core/GBA/MP2K/Track.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal class Track - { - public readonly byte Index; - private readonly int _startOffset; - public byte Voice; - public byte PitchBendRange; - public byte Priority; - public byte Volume; - public byte Rest; - public byte LFOPhase; - public byte LFODelayCount; - public byte LFOSpeed; - public byte LFODelay; - public byte LFODepth; - public LFOType LFOType; - public sbyte PitchBend; - public sbyte Tune; - public sbyte Panpot; - public sbyte Transpose; - public bool Ready; - public bool Stopped; - public int DataOffset; - public int[] CallStack = new int[3]; - public byte CallStackDepth; - public byte RunCmd; - public byte PrevKey; - public byte PrevVelocity; - - public readonly List Channels = new List(); - - public int GetPitch() - { - int lfo = LFOType == LFOType.Pitch ? (Utils.Tri(LFOPhase) * LFODepth) >> 8 : 0; - return (PitchBend * PitchBendRange) + Tune + lfo; - } - public byte GetVolume() - { - int lfo = LFOType == LFOType.Volume ? (Utils.Tri(LFOPhase) * LFODepth * 3 * Volume) >> 19 : 0; - int v = Volume + lfo; - if (v < 0) - { - v = 0; - } - else if (v > 0x7F) - { - v = 0x7F; - } - return (byte)v; - } - public sbyte GetPanpot() - { - int lfo = LFOType == LFOType.Panpot ? (Utils.Tri(LFOPhase) * LFODepth * 3) >> 12 : 0; - int p = Panpot + lfo; - if (p < -0x40) - { - p = -0x40; - } - else if (p > 0x3F) - { - p = 0x3F; - } - return (sbyte)p; - } - - public Track(byte i, int startOffset) - { - Index = i; - _startOffset = startOffset; - } - public void Init() - { - Voice = 0; - Priority = 0; - Rest = 0; - LFODelay = 0; - LFODelayCount = 0; - LFOPhase = 0; - LFODepth = 0; - CallStackDepth = 0; - PitchBend = 0; - Tune = 0; - Panpot = 0; - Transpose = 0; - DataOffset = _startOffset; - RunCmd = 0; - PrevKey = 0; - PrevVelocity = 0x7F; - PitchBendRange = 2; - LFOType = LFOType.Pitch; - Ready = false; - Stopped = false; - LFOSpeed = 22; - Volume = 100; - StopAllChannels(); - } - public void Tick() - { - if (Rest != 0) - { - Rest--; - } - if (LFODepth > 0) - { - LFOPhase += LFOSpeed; - } - else - { - LFOPhase = 0; - } - int active = 0; - Channel[] chans = Channels.ToArray(); - for (int i = 0; i < chans.Length; i++) - { - if (chans[i].TickNote()) - { - active++; - } - } - if (active != 0) - { - if (LFODelayCount > 0) - { - LFODelayCount--; - LFOPhase = 0; - } - } - else - { - LFODelayCount = LFODelay; - } - if ((LFODelay == LFODelayCount && LFODelay != 0) || LFOSpeed == 0) - { - LFOPhase = 0; - } - } - - public void ReleaseChannels(int key) - { - Channel[] chans = Channels.ToArray(); - for (int i = 0; i < chans.Length; i++) - { - Channel c = chans[i]; - if (c.Note.OriginalKey == key && c.Note.Duration == -1) - { - c.Release(); - } - } - } - public void StopAllChannels() - { - Channel[] chans = Channels.ToArray(); - for (int i = 0; i < chans.Length; i++) - { - chans[i].Stop(); - } - } - public void UpdateChannels() - { - byte vol = GetVolume(); - sbyte pan = GetPanpot(); - int pitch = GetPitch(); - for (int i = 0; i < Channels.Count; i++) - { - Channel c = Channels[i]; - c.SetVolume(vol, pan); - c.SetPitch(pitch); - } - } - } -} diff --git a/VG Music Studio/Core/GBA/MP2K/Utils.cs b/VG Music Studio/Core/GBA/MP2K/Utils.cs deleted file mode 100644 index fc10fbba..00000000 --- a/VG Music Studio/Core/GBA/MP2K/Utils.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System.Collections; -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal static class Utils - { - public static readonly byte[] RestTable = new byte[49] - { - 00, 01, 02, 03, 04, 05, 06, 07, - 08, 09, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, - 24, 28, 30, 32, 36, 40, 42, 44, - 48, 52, 54, 56, 60, 64, 66, 68, - 72, 76, 78, 80, 84, 88, 90, 92, - 96 - }; - public static readonly (int sampleRate, int samplesPerBuffer)[] FrequencyTable = new (int, int)[12] - { - (05734, 096), // 59.72916666666667 - (07884, 132), // 59.72727272727273 - (10512, 176), // 59.72727272727273 - (13379, 224), // 59.72767857142857 - (15768, 264), // 59.72727272727273 - (18157, 304), // 59.72697368421053 - (21024, 352), // 59.72727272727273 - (26758, 448), // 59.72767857142857 - (31536, 528), // 59.72727272727273 - (36314, 608), // 59.72697368421053 - (40137, 672), // 59.72767857142857 - (42048, 704) // 59.72727272727273 - }; - - // Squares - public static readonly float[] SquareD12 = new float[8] { 0.875f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f }; - public static readonly float[] SquareD25 = new float[8] { 0.750f, 0.750f, -0.250f, -0.250f, -0.250f, -0.250f, -0.250f, -0.250f }; - public static readonly float[] SquareD50 = new float[8] { 0.500f, 0.500f, 0.500f, 0.500f, -0.500f, -0.500f, -0.500f, -0.500f }; - public static readonly float[] SquareD75 = new float[8] { 0.250f, 0.250f, 0.250f, 0.250f, 0.250f, 0.250f, -0.750f, -0.750f }; - - // Noises - public static readonly BitArray NoiseFine; - public static readonly BitArray NoiseRough; - public static readonly byte[] NoiseFrequencyTable = new byte[60] - { - 0xD7, 0xD6, 0xD5, 0xD4, - 0xC7, 0xC6, 0xC5, 0xC4, - 0xB7, 0xB6, 0xB5, 0xB4, - 0xA7, 0xA6, 0xA5, 0xA4, - 0x97, 0x96, 0x95, 0x94, - 0x87, 0x86, 0x85, 0x84, - 0x77, 0x76, 0x75, 0x74, - 0x67, 0x66, 0x65, 0x64, - 0x57, 0x56, 0x55, 0x54, - 0x47, 0x46, 0x45, 0x44, - 0x37, 0x36, 0x35, 0x34, - 0x27, 0x26, 0x25, 0x24, - 0x17, 0x16, 0x15, 0x14, - 0x07, 0x06, 0x05, 0x04, - 0x03, 0x02, 0x01, 0x00 - }; - - // PCM4 - public static float[] PCM4ToFloat(int sampleOffset) - { - var config = (Config)Engine.Instance.Config; - float[] sample = new float[0x20]; - float sum = 0; - for (int i = 0; i < 0x10; i++) - { - byte b = config.ROM[sampleOffset + i]; - float first = (b >> 4) / 16f; - float second = (b & 0xF) / 16f; - sum += sample[i * 2] = first; - sum += sample[(i * 2) + 1] = second; - } - float dcCorrection = sum / 0x20; - for (int i = 0; i < 0x20; i++) - { - sample[i] -= dcCorrection; - } - return sample; - } - - // Pokémon Only - private static readonly sbyte[] _compressionLookup = new sbyte[16] - { - 0, 1, 4, 9, 16, 25, 36, 49, -64, -49, -36, -25, -16, -9, -4, -1 - }; - public static sbyte[] Decompress(int sampleOffset, int sampleLength) - { - var config = (Config)Engine.Instance.Config; - var samples = new List(); - sbyte compressionLevel = 0; - int compressionByte = 0, compressionIdx = 0; - - for (int i = 0; true; i++) - { - byte b = config.ROM[sampleOffset + i]; - if (compressionByte == 0) - { - compressionByte = 0x20; - compressionLevel = (sbyte)b; - samples.Add(compressionLevel); - if (++compressionIdx >= sampleLength) - { - break; - } - } - else - { - if (compressionByte < 0x20) - { - compressionLevel += _compressionLookup[b >> 4]; - samples.Add(compressionLevel); - if (++compressionIdx >= sampleLength) - { - break; - } - } - compressionByte--; - compressionLevel += _compressionLookup[b & 0xF]; - samples.Add(compressionLevel); - if (++compressionIdx >= sampleLength) - { - break; - } - } - } - - return samples.ToArray(); - } - - static Utils() - { - NoiseFine = new BitArray(0x8000); - int reg = 0x4000; - for (int i = 0; i < NoiseFine.Length; i++) - { - if ((reg & 1) == 1) - { - reg >>= 1; - reg ^= 0x6000; - NoiseFine[i] = true; - } - else - { - reg >>= 1; - NoiseFine[i] = false; - } - } - NoiseRough = new BitArray(0x80); - reg = 0x40; - for (int i = 0; i < NoiseRough.Length; i++) - { - if ((reg & 1) == 1) - { - reg >>= 1; - reg ^= 0x60; - NoiseRough[i] = true; - } - else - { - reg >>= 1; - NoiseRough[i] = false; - } - } - } - public static int Tri(int index) - { - index = (index - 64) & 0xFF; - return (index < 128) ? (index * 12) - 768 : 2304 - (index * 12); - } - } -} diff --git a/VG Music Studio/Core/GBA/Utils.cs b/VG Music Studio/Core/GBA/Utils.cs deleted file mode 100644 index d5858818..00000000 --- a/VG Music Studio/Core/GBA/Utils.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.GBA -{ - internal static class Utils - { - public const double AGB_FPS = 59.7275; - public const int SystemClock = 16777216; // 16.777216 MHz (16*1024*1024 Hz) - - public const int CartridgeOffset = 0x08000000; - public const int CartridgeCapacity = 0x02000000; - - public static readonly string[] PSGTypes = new string[4] { "Square 1", "Square 2", "PCM4", "Noise" }; - } -} diff --git a/VG Music Studio/Core/GlobalConfig.cs b/VG Music Studio/Core/GlobalConfig.cs deleted file mode 100644 index 4b3b57f7..00000000 --- a/VG Music Studio/Core/GlobalConfig.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.IO; -using YamlDotNet.RepresentationModel; - -namespace Kermalis.VGMusicStudio.Core -{ - internal enum PlaylistMode : byte - { - Random, - Sequential - } - - internal sealed class GlobalConfig - { - public static GlobalConfig Instance { get; private set; } - - public readonly bool TaskbarProgress; - public readonly ushort RefreshRate; - public readonly bool CenterIndicators; - public readonly bool PanpotIndicators; - public readonly PlaylistMode PlaylistMode; - public readonly long PlaylistSongLoops; - public readonly long PlaylistFadeOutMilliseconds; - public readonly sbyte MiddleCOctave; - public readonly HSLColor[] Colors; - - private GlobalConfig() - { - const string configFile = "Config.yaml"; - using (StreamReader fileStream = File.OpenText(Utils.CombineWithBaseDirectory(configFile))) - { - try - { - var yaml = new YamlStream(); - yaml.Load(fileStream); - - var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; - TaskbarProgress = mapping.GetValidBoolean(nameof(TaskbarProgress)); - RefreshRate = (ushort)mapping.GetValidValue(nameof(RefreshRate), 1, 1000); - CenterIndicators = mapping.GetValidBoolean(nameof(CenterIndicators)); - PanpotIndicators = mapping.GetValidBoolean(nameof(PanpotIndicators)); - PlaylistMode = mapping.GetValidEnum(nameof(PlaylistMode)); - PlaylistSongLoops = mapping.GetValidValue(nameof(PlaylistSongLoops), 0, long.MaxValue); - PlaylistFadeOutMilliseconds = mapping.GetValidValue(nameof(PlaylistFadeOutMilliseconds), 0, long.MaxValue); - MiddleCOctave = (sbyte)mapping.GetValidValue(nameof(MiddleCOctave), sbyte.MinValue, sbyte.MaxValue); - - var cmap = (YamlMappingNode)mapping.Children[nameof(Colors)]; - Colors = new HSLColor[256]; - foreach (KeyValuePair c in cmap) - { - int i = (int)Utils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Colors)), c.Key.ToString(), 0, 127); - if (Colors[i] != null) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigColorRepeated, i))); - } - double h = 0, s = 0, l = 0; - foreach (KeyValuePair v in ((YamlMappingNode)c.Value).Children) - { - string key = v.Key.ToString(); - string valueName = string.Format(Strings.ConfigKeySubkey, string.Format("{0} {1}", nameof(Colors), i)); - if (key == "H") - { - h = Utils.ParseValue(valueName, v.Value.ToString(), 0, 240); - } - else if (key == "S") - { - s = Utils.ParseValue(valueName, v.Value.ToString(), 0, 240); - } - else if (key == "L") - { - l = Utils.ParseValue(valueName, v.Value.ToString(), 0, 240); - } - else - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigColorInvalidKey, i))); - } - } - var co = new HSLColor(h, s, l); - Colors[i] = co; - Colors[i + 128] = co; - } - for (int i = 0; i < Colors.Length; i++) - { - if (Colors[i] == null) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigColorMissing, i))); - } - } - } - catch (BetterKeyNotFoundException ex) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyMissing, ex.Key))); - } - catch (Exception ex) when (ex is InvalidValueException || ex is YamlDotNet.Core.YamlException) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + ex.Message)); - } - } - } - - public static void Init() - { - Instance = new GlobalConfig(); - } - } -} diff --git a/VG Music Studio/Core/Mixer.cs b/VG Music Studio/Core/Mixer.cs deleted file mode 100644 index e0a242f6..00000000 --- a/VG Music Studio/Core/Mixer.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Kermalis.VGMusicStudio.UI; -using NAudio.CoreAudioApi; -using NAudio.CoreAudioApi.Interfaces; -using NAudio.Wave; -using System; - -namespace Kermalis.VGMusicStudio.Core -{ - internal abstract class Mixer : IAudioSessionEventsHandler, IDisposable - { - public readonly bool[] Mutes = new bool[SongInfoControl.SongInfo.MaxTracks]; - private IWavePlayer _out; - private AudioSessionControl _appVolume; - - protected void Init(IWaveProvider waveProvider) - { - _out = new WasapiOut(); - _out.Init(waveProvider); - using (var en = new MMDeviceEnumerator()) - { - SessionCollection sessions = en.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia).AudioSessionManager.Sessions; - int id = System.Diagnostics.Process.GetCurrentProcess().Id; - for (int i = 0; i < sessions.Count; i++) - { - AudioSessionControl session = sessions[i]; - if (session.GetProcessID == id) - { - _appVolume = session; - _appVolume.RegisterEventClient(this); - break; - } - } - } - _out.Play(); - } - - private bool _volChange = true; - public void OnVolumeChanged(float volume, bool isMuted) - { - if (_volChange) - { - MainForm.Instance.SetVolumeBarValue(volume); - } - _volChange = true; - } - public void OnDisplayNameChanged(string displayName) - { - throw new NotImplementedException(); - } - public void OnIconPathChanged(string iconPath) - { - throw new NotImplementedException(); - } - public void OnChannelVolumeChanged(uint channelCount, IntPtr newVolumes, uint channelIndex) - { - throw new NotImplementedException(); - } - public void OnGroupingParamChanged(ref Guid groupingId) - { - throw new NotImplementedException(); - } - // Fires on @out.Play() and @out.Stop() - public void OnStateChanged(AudioSessionState state) - { - if (state == AudioSessionState.AudioSessionStateActive) - { - OnVolumeChanged(_appVolume.SimpleAudioVolume.Volume, _appVolume.SimpleAudioVolume.Mute); - } - } - public void OnSessionDisconnected(AudioSessionDisconnectReason disconnectReason) - { - throw new NotImplementedException(); - } - public void SetVolume(float volume) - { - _volChange = false; - _appVolume.SimpleAudioVolume.Volume = volume; - } - - public virtual void Dispose() - { - _out.Stop(); - _out.Dispose(); - _appVolume.Dispose(); - } - } -} diff --git a/VG Music Studio/Core/NDS/DSE/Channel.cs b/VG Music Studio/Core/NDS/DSE/Channel.cs deleted file mode 100644 index 2ff239db..00000000 --- a/VG Music Studio/Core/NDS/DSE/Channel.cs +++ /dev/null @@ -1,368 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class Channel - { - public readonly byte Index; - - public Track Owner; - public EnvelopeState State; - public byte RootKey; - public byte Key; - public byte NoteVelocity; - public sbyte Panpot; // Not necessary - public ushort BaseTimer; - public ushort Timer; - public uint NoteLength; - public byte Volume; - - private int _pos; - private short _prevLeft; - private short _prevRight; - - private int _envelopeTimeLeft; - private int _volumeIncrement; - private int _velocity; // From 0-0x3FFFFFFF ((128 << 23) - 1) - private byte _targetVolume; - - private byte _attackVolume; - private byte _attack; - private byte _decay; - private byte _sustain; - private byte _hold; - private byte _decay2; - private byte _release; - - // PCM8, PCM16, ADPCM - private SWD.SampleBlock _sample; - // PCM8, PCM16 - private int _dataOffset; - // ADPCM - private ADPCMDecoder _adpcmDecoder; - private short _adpcmLoopLastSample; - private short _adpcmLoopStepIndex; - - public Channel(byte i) - { - Index = i; - } - - public bool StartPCM(SWD localswd, SWD masterswd, byte voice, int key, uint noteLength) - { - SWD.IProgramInfo programInfo = localswd.Programs.ProgramInfos[voice]; - if (programInfo != null) - { - for (int i = 0; i < programInfo.SplitEntries.Length; i++) - { - SWD.ISplitEntry split = programInfo.SplitEntries[i]; - if (key >= split.LowKey && key <= split.HighKey) - { - _sample = masterswd.Samples[split.SampleId]; - Key = (byte)key; - RootKey = split.SampleRootKey; - BaseTimer = (ushort)(NDS.Utils.ARM7_CLOCK / _sample.WavInfo.SampleRate); - if (_sample.WavInfo.SampleFormat == SampleFormat.ADPCM) - { - _adpcmDecoder = new ADPCMDecoder(_sample.Data); - } - //attackVolume = sample.WavInfo.AttackVolume == 0 ? split.AttackVolume : sample.WavInfo.AttackVolume; - //attack = sample.WavInfo.Attack == 0 ? split.Attack : sample.WavInfo.Attack; - //decay = sample.WavInfo.Decay == 0 ? split.Decay : sample.WavInfo.Decay; - //sustain = sample.WavInfo.Sustain == 0 ? split.Sustain : sample.WavInfo.Sustain; - //hold = sample.WavInfo.Hold == 0 ? split.Hold : sample.WavInfo.Hold; - //decay2 = sample.WavInfo.Decay2 == 0 ? split.Decay2 : sample.WavInfo.Decay2; - //release = sample.WavInfo.Release == 0 ? split.Release : sample.WavInfo.Release; - //attackVolume = split.AttackVolume == 0 ? sample.WavInfo.AttackVolume : split.AttackVolume; - //attack = split.Attack == 0 ? sample.WavInfo.Attack : split.Attack; - //decay = split.Decay == 0 ? sample.WavInfo.Decay : split.Decay; - //sustain = split.Sustain == 0 ? sample.WavInfo.Sustain : split.Sustain; - //hold = split.Hold == 0 ? sample.WavInfo.Hold : split.Hold; - //decay2 = split.Decay2 == 0 ? sample.WavInfo.Decay2 : split.Decay2; - //release = split.Release == 0 ? sample.WavInfo.Release : split.Release; - _attackVolume = split.AttackVolume == 0 ? _sample.WavInfo.AttackVolume == 0 ? (byte)0x7F : _sample.WavInfo.AttackVolume : split.AttackVolume; - _attack = split.Attack == 0 ? _sample.WavInfo.Attack == 0 ? (byte)0x7F : _sample.WavInfo.Attack : split.Attack; - _decay = split.Decay == 0 ? _sample.WavInfo.Decay == 0 ? (byte)0x7F : _sample.WavInfo.Decay : split.Decay; - _sustain = split.Sustain == 0 ? _sample.WavInfo.Sustain == 0 ? (byte)0x7F : _sample.WavInfo.Sustain : split.Sustain; - _hold = split.Hold == 0 ? _sample.WavInfo.Hold == 0 ? (byte)0x7F : _sample.WavInfo.Hold : split.Hold; - _decay2 = split.Decay2 == 0 ? _sample.WavInfo.Decay2 == 0 ? (byte)0x7F : _sample.WavInfo.Decay2 : split.Decay2; - _release = split.Release == 0 ? _sample.WavInfo.Release == 0 ? (byte)0x7F : _sample.WavInfo.Release : split.Release; - DetermineEnvelopeStartingPoint(); - _pos = 0; - _prevLeft = _prevRight = 0; - NoteLength = noteLength; - return true; - } - } - } - return false; - } - - public void Stop() - { - if (Owner != null) - { - Owner.Channels.Remove(this); - } - Owner = null; - Volume = 0; - } - - private bool CMDB1___sub_2074CA0() - { - bool b = true; - bool ge = _sample.WavInfo.EnvMult >= 0x7F; - bool ee = _sample.WavInfo.EnvMult == 0x7F; - if (_sample.WavInfo.EnvMult > 0x7F) - { - ge = _attackVolume >= 0x7F; - ee = _attackVolume == 0x7F; - } - if (!ee & ge - && _attack > 0x7F - && _decay > 0x7F - && _sustain > 0x7F - && _hold > 0x7F - && _decay2 > 0x7F - && _release > 0x7F) - { - b = false; - } - return b; - } - private void DetermineEnvelopeStartingPoint() - { - State = EnvelopeState.Two; // This isn't actually placed in this func - bool atLeastOneThingIsValid = CMDB1___sub_2074CA0(); // Neither is this - if (atLeastOneThingIsValid) - { - if (_attack != 0) - { - _velocity = _attackVolume << 23; - State = EnvelopeState.Hold; - UpdateEnvelopePlan(0x7F, _attack); - } - else - { - _velocity = 0x7F << 23; - if (_hold != 0) - { - UpdateEnvelopePlan(0x7F, _hold); - State = EnvelopeState.Decay; - } - else if (_decay != 0) - { - UpdateEnvelopePlan(_sustain, _decay); - State = EnvelopeState.Decay2; - } - else - { - UpdateEnvelopePlan(0, _release); - State = EnvelopeState.Six; - } - } - // Unk1E = 1 - } - else if (State != EnvelopeState.One) // What should it be? - { - State = EnvelopeState.Zero; - _velocity = 0x7F << 23; - } - } - public void SetEnvelopePhase7_2074ED8() - { - if (State != EnvelopeState.Zero) - { - UpdateEnvelopePlan(0, _release); - State = EnvelopeState.Seven; - } - } - public int StepEnvelope() - { - if (State > EnvelopeState.Two) - { - if (_envelopeTimeLeft != 0) - { - _envelopeTimeLeft--; - _velocity += _volumeIncrement; - if (_velocity < 0) - { - _velocity = 0; - } - else if (_velocity > 0x3FFFFFFF) - { - _velocity = 0x3FFFFFFF; - } - } - else - { - _velocity = _targetVolume << 23; - switch (State) - { - default: return _velocity >> 23; // case 8 - case EnvelopeState.Hold: - { - if (_hold == 0) - { - goto LABEL_6; - } - else - { - UpdateEnvelopePlan(0x7F, _hold); - State = EnvelopeState.Decay; - } - break; - } - case EnvelopeState.Decay: - LABEL_6: - { - if (_decay == 0) - { - _velocity = _sustain << 23; - goto LABEL_9; - } - else - { - UpdateEnvelopePlan(_sustain, _decay); - State = EnvelopeState.Decay2; - } - break; - } - case EnvelopeState.Decay2: - LABEL_9: - { - if (_decay2 == 0) - { - goto LABEL_11; - } - else - { - UpdateEnvelopePlan(0, _decay2); - State = EnvelopeState.Six; - } - break; - } - case EnvelopeState.Six: - LABEL_11: - { - UpdateEnvelopePlan(0, 0); - State = EnvelopeState.Two; - break; - } - case EnvelopeState.Seven: - { - State = EnvelopeState.Eight; - _velocity = 0; - _envelopeTimeLeft = 0; - break; - } - } - } - } - return _velocity >> 23; - } - private void UpdateEnvelopePlan(byte targetVolume, int envelopeParam) - { - if (envelopeParam == 0x7F) - { - _volumeIncrement = 0; - _envelopeTimeLeft = int.MaxValue; - } - else - { - _targetVolume = targetVolume; - _envelopeTimeLeft = _sample.WavInfo.EnvMult == 0 - ? Utils.Duration32[envelopeParam] * 1000 / 10000 - : Utils.Duration16[envelopeParam] * _sample.WavInfo.EnvMult * 1000 / 10000; - _volumeIncrement = _envelopeTimeLeft == 0 ? 0 : ((targetVolume << 23) - _velocity) / _envelopeTimeLeft; - } - } - - public void Process(out short left, out short right) - { - if (Timer != 0) - { - int numSamples = (_pos + 0x100) / Timer; - _pos = (_pos + 0x100) % Timer; - // prevLeft and prevRight are stored because numSamples can be 0. - for (int i = 0; i < numSamples; i++) - { - short samp; - switch (_sample.WavInfo.SampleFormat) - { - case SampleFormat.PCM8: - { - // If hit end - if (_dataOffset >= _sample.Data.Length) - { - if (_sample.WavInfo.Loop) - { - _dataOffset = (int)(_sample.WavInfo.LoopStart * 4); - } - else - { - left = right = _prevLeft = _prevRight = 0; - Stop(); - return; - } - } - samp = (short)((sbyte)_sample.Data[_dataOffset++] << 8); - break; - } - case SampleFormat.PCM16: - { - // If hit end - if (_dataOffset >= _sample.Data.Length) - { - if (_sample.WavInfo.Loop) - { - _dataOffset = (int)(_sample.WavInfo.LoopStart * 4); - } - else - { - left = right = _prevLeft = _prevRight = 0; - Stop(); - return; - } - } - samp = (short)(_sample.Data[_dataOffset++] | (_sample.Data[_dataOffset++] << 8)); - break; - } - case SampleFormat.ADPCM: - { - // If just looped - if (_adpcmDecoder.DataOffset == _sample.WavInfo.LoopStart * 4 && !_adpcmDecoder.OnSecondNibble) - { - _adpcmLoopLastSample = _adpcmDecoder.LastSample; - _adpcmLoopStepIndex = _adpcmDecoder.StepIndex; - } - // If hit end - if (_adpcmDecoder.DataOffset >= _sample.Data.Length && !_adpcmDecoder.OnSecondNibble) - { - if (_sample.WavInfo.Loop) - { - _adpcmDecoder.DataOffset = (int)(_sample.WavInfo.LoopStart * 4); - _adpcmDecoder.StepIndex = _adpcmLoopStepIndex; - _adpcmDecoder.LastSample = _adpcmLoopLastSample; - _adpcmDecoder.OnSecondNibble = false; - } - else - { - left = right = _prevLeft = _prevRight = 0; - Stop(); - return; - } - } - samp = _adpcmDecoder.GetSample(); - break; - } - default: samp = 0; break; - } - samp = (short)(samp * Volume / 0x7F); - _prevLeft = (short)(samp * (-Panpot + 0x40) / 0x80); - _prevRight = (short)(samp * (Panpot + 0x40) / 0x80); - } - } - left = _prevLeft; - right = _prevRight; - } - } -} diff --git a/VG Music Studio/Core/NDS/DSE/Commands.cs b/VG Music Studio/Core/NDS/DSE/Commands.cs deleted file mode 100644 index a7d8eaa3..00000000 --- a/VG Music Studio/Core/NDS/DSE/Commands.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System.Drawing; -using System.Linq; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class ExpressionCommand : ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Expression"; - public string Arguments => Expression.ToString(); - - public byte Expression { get; set; } - } - internal class FinishCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Finish"; - public string Arguments => string.Empty; - } - internal class InvalidCommand : ICommand - { - public Color Color => Color.MediumVioletRed; - public string Label => $"Invalid 0x{Command:X}"; - public string Arguments => string.Empty; - - public byte Command { get; set; } - } - internal class LoopStartCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Loop Start"; - public string Arguments => $"0x{Offset:X}"; - - public long Offset { get; set; } - } - internal class NoteCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Note"; - public string Arguments => $"{Util.Utils.GetPianoKeyName(Key)} {OctaveChange} {Velocity} {Duration}"; - - public byte Key { get; set; } - public sbyte OctaveChange { get; set; } - public byte Velocity { get; set; } - public uint Duration { get; set; } - } - internal class OctaveAddCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Add To Octave"; - public string Arguments => OctaveChange.ToString(); - - public sbyte OctaveChange { get; set; } - } - internal class OctaveSetCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Set Octave"; - public string Arguments => Octave.ToString(); - - public byte Octave { get; set; } - } - internal class PanpotCommand : ICommand - { - public Color Color => Color.GreenYellow; - public string Label => "Panpot"; - public string Arguments => Panpot.ToString(); - - public sbyte Panpot { get; set; } - } - internal class PitchBendCommand : ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Pitch Bend"; - public string Arguments => $"{(sbyte)Bend}, {(sbyte)(Bend >> 8)}"; - - public ushort Bend { get; set; } - } - internal class RestCommand : ICommand - { - public Color Color => Color.PaleVioletRed; - public string Label => "Rest"; - public string Arguments => Rest.ToString(); - - public uint Rest { get; set; } - } - internal class SkipBytesCommand : ICommand - { - public Color Color => Color.MediumVioletRed; - public string Label => $"Skip 0x{Command:X}"; - public string Arguments => string.Join(", ", SkippedBytes.Select(b => $"0x{b:X}")); - - public byte Command { get; set; } - public byte[] SkippedBytes { get; set; } - } - internal class TempoCommand : ICommand - { - public Color Color => Color.DeepSkyBlue; - public string Label => $"Tempo {Command - 0xA3}"; // The two possible tempo commands are 0xA4 and 0xA5 - public string Arguments => Tempo.ToString(); - - public byte Command { get; set; } - public byte Tempo { get; set; } - } - internal class UnknownCommand : ICommand - { - public Color Color => Color.MediumVioletRed; - public string Label => $"Unknown 0x{Command:X}"; - public string Arguments => string.Join(", ", Args.Select(b => $"0x{b:X}")); - - public byte Command { get; set; } - public byte[] Args { get; set; } - } - internal class VoiceCommand : ICommand - { - public Color Color => Color.DarkSalmon; - public string Label => "Voice"; - public string Arguments => Voice.ToString(); - - public byte Voice { get; set; } - } - internal class VolumeCommand : ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Volume"; - public string Arguments => Volume.ToString(); - - public byte Volume { get; set; } - } -} diff --git a/VG Music Studio/Core/NDS/DSE/Config.cs b/VG Music Studio/Core/NDS/DSE/Config.cs deleted file mode 100644 index 25f856ca..00000000 --- a/VG Music Studio/Core/NDS/DSE/Config.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Kermalis.EndianBinaryIO; -using Kermalis.VGMusicStudio.Properties; -using System; -using System.IO; -using System.Linq; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class Config : Core.Config - { - public readonly string BGMPath; - public readonly string[] BGMFiles; - - public Config(string bgmPath) - { - BGMPath = bgmPath; - BGMFiles = Directory.GetFiles(bgmPath, "bgm*.smd", SearchOption.TopDirectoryOnly); - if (BGMFiles.Length == 0) - { - throw new Exception(Strings.ErrorDSENoSequences); - } - var songs = new Song[BGMFiles.Length]; - for (int i = 0; i < BGMFiles.Length; i++) - { - using (var reader = new EndianBinaryReader(File.OpenRead(BGMFiles[i]))) - { - SMD.Header header = reader.ReadObject(); - songs[i] = new Song(i, $"{Path.GetFileNameWithoutExtension(BGMFiles[i])} - {new string(header.Label.TakeWhile(c => c != '\0').ToArray())}"); - } - } - Playlists.Add(new Playlist(Strings.PlaylistMusic, songs)); - } - - public override string GetGameName() - { - return "DSE"; - } - public override string GetSongName(long index) - { - return index < 0 || index >= BGMFiles.Length - ? index.ToString() - : '\"' + BGMFiles[index] + '\"'; - } - } -} diff --git a/VG Music Studio/Core/NDS/DSE/Enums.cs b/VG Music Studio/Core/NDS/DSE/Enums.cs deleted file mode 100644 index 6cec60df..00000000 --- a/VG Music Studio/Core/NDS/DSE/Enums.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal enum EnvelopeState : byte - { - Zero = 0, - One = 1, - Two = 2, - Hold = 3, - Decay = 4, - Decay2 = 5, - Six = 6, - Seven = 7, - Eight = 8 - } - - internal enum SampleFormat : ushort - { - PCM8 = 0x000, - PCM16 = 0x100, - ADPCM = 0x200 - } -} diff --git a/VG Music Studio/Core/NDS/DSE/Mixer.cs b/VG Music Studio/Core/NDS/DSE/Mixer.cs deleted file mode 100644 index 29c8de54..00000000 --- a/VG Music Studio/Core/NDS/DSE/Mixer.cs +++ /dev/null @@ -1,220 +0,0 @@ -using NAudio.Wave; -using System; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class Mixer : Core.Mixer - { - private const int _numChannels = 0x20; - private readonly float _samplesReciprocal; - private readonly int _samplesPerBuffer; - private bool _isFading; - private long _fadeMicroFramesLeft; - private float _fadePos; - private float _fadeStepPerMicroframe; - - private readonly Channel[] _channels; - private readonly BufferedWaveProvider _buffer; - - public Mixer() - { - // The sampling frequency of the mixer is 1.04876 MHz with an amplitude resolution of 24 bits, but the sampling frequency after mixing with PWM modulation is 32.768 kHz with an amplitude resolution of 10 bits. - // - gbatek - // I'm not using either of those because the samples per buffer leads to an overflow eventually - const int sampleRate = 65456; - _samplesPerBuffer = 341; // TODO - _samplesReciprocal = 1f / _samplesPerBuffer; - - _channels = new Channel[_numChannels]; - for (byte i = 0; i < _numChannels; i++) - { - _channels[i] = new Channel(i); - } - - _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) - { - DiscardOnBufferOverflow = true, - BufferLength = _samplesPerBuffer * 64 - }; - Init(_buffer); - } - public override void Dispose() - { - base.Dispose(); - CloseWaveWriter(); - } - - public Channel AllocateChannel() - { - int GetScore(Channel c) - { - // Free channels should be used before releasing channels - return c.Owner == null ? -2 : Utils.IsStateRemovable(c.State) ? -1 : 0; - } - Channel nChan = null; - for (int i = 0; i < _numChannels; i++) - { - Channel c = _channels[i]; - if (nChan != null) - { - int nScore = GetScore(nChan); - int cScore = GetScore(c); - if (cScore <= nScore && (cScore < nScore || c.Volume <= nChan.Volume)) - { - nChan = c; - } - } - else - { - nChan = c; - } - } - return nChan != null && 0 >= GetScore(nChan) ? nChan : null; - } - - public void ChannelTick() - { - for (int i = 0; i < _numChannels; i++) - { - Channel chan = _channels[i]; - if (chan.Owner != null) - { - chan.Volume = (byte)chan.StepEnvelope(); - if (chan.NoteLength == 0 && !Utils.IsStateRemovable(chan.State)) - { - chan.SetEnvelopePhase7_2074ED8(); - } - int vol = SDAT.Utils.SustainTable[chan.NoteVelocity] + SDAT.Utils.SustainTable[chan.Volume] + SDAT.Utils.SustainTable[chan.Owner.Volume] + SDAT.Utils.SustainTable[chan.Owner.Expression]; - //int pitch = ((chan.Key - chan.BaseKey) << 6) + chan.SweepMain() + chan.Owner.GetPitch(); // "<< 6" is "* 0x40" - int pitch = (chan.Key - chan.RootKey) << 6; // "<< 6" is "* 0x40" - if (Utils.IsStateRemovable(chan.State) && vol <= -92544) - { - chan.Stop(); - } - else - { - chan.Volume = SDAT.Utils.GetChannelVolume(vol); - chan.Panpot = chan.Owner.Panpot; - chan.Timer = SDAT.Utils.GetChannelTimer(chan.BaseTimer, pitch); - } - } - } - } - - public void BeginFadeIn() - { - _fadePos = 0f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); - _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; - _isFading = true; - } - public void BeginFadeOut() - { - _fadePos = 1f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); - _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; - _isFading = true; - } - public bool IsFading() - { - return _isFading; - } - public bool IsFadeDone() - { - return _isFading && _fadeMicroFramesLeft == 0; - } - public void ResetFade() - { - _isFading = false; - _fadeMicroFramesLeft = 0; - } - - private WaveFileWriter _waveWriter; - public void CreateWaveWriter(string fileName) - { - _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); - } - public void CloseWaveWriter() - { - _waveWriter?.Dispose(); - } - public void Process(bool output, bool recording) - { - float masterStep; - float masterLevel; - if (_isFading && _fadeMicroFramesLeft == 0) - { - masterStep = 0; - masterLevel = 0; - } - else - { - float fromMaster = 1f; - float toMaster = 1f; - if (_fadeMicroFramesLeft > 0) - { - const float scale = 10f / 6f; - fromMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadePos += _fadeStepPerMicroframe; - toMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadeMicroFramesLeft--; - } - masterStep = (toMaster - fromMaster) * _samplesReciprocal; - masterLevel = fromMaster; - } - byte[] b = new byte[4]; - for (int i = 0; i < _samplesPerBuffer; i++) - { - int left = 0, - right = 0; - for (int j = 0; j < _numChannels; j++) - { - Channel chan = _channels[j]; - if (chan.Owner != null) - { - bool muted = Mutes[chan.Owner.Index]; // Get mute first because chan.Process() can call chan.Stop() which sets chan.Owner to null - chan.Process(out short channelLeft, out short channelRight); - if (!muted) - { - left += channelLeft; - right += channelRight; - } - } - } - float f = left * masterLevel; - if (f < short.MinValue) - { - f = short.MinValue; - } - else if (f > short.MaxValue) - { - f = short.MaxValue; - } - left = (int)f; - b[0] = (byte)left; - b[1] = (byte)(left >> 8); - f = right * masterLevel; - if (f < short.MinValue) - { - f = short.MinValue; - } - else if (f > short.MaxValue) - { - f = short.MaxValue; - } - right = (int)f; - b[2] = (byte)right; - b[3] = (byte)(right >> 8); - masterLevel += masterStep; - if (output) - { - _buffer.AddSamples(b, 0, 4); - } - if (recording) - { - _waveWriter.Write(b, 0, 4); - } - } - } - } -} diff --git a/VG Music Studio/Core/NDS/DSE/Player.cs b/VG Music Studio/Core/NDS/DSE/Player.cs deleted file mode 100644 index 4fa2b81e..00000000 --- a/VG Music Studio/Core/NDS/DSE/Player.cs +++ /dev/null @@ -1,1040 +0,0 @@ -using Kermalis.EndianBinaryIO; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class Player : IPlayer - { - private readonly Mixer _mixer; - private readonly Config _config; - private readonly TimeBarrier _time; - private Thread _thread; - private readonly SWD _masterSWD; - private SWD _localSWD; - private byte[] _smdFile; - private Track[] _tracks; - private byte _tempo; - private int _tempoStack; - private long _elapsedLoops; - - public List[] Events { get; private set; } - public long MaxTicks { get; private set; } - public long ElapsedTicks { get; private set; } - public bool ShouldFadeOut { get; set; } - public long NumLoops { get; set; } - private int _longestTrack; - - public PlayerState State { get; private set; } - public event SongEndedEvent SongEnded; - - public Player(Mixer mixer, Config config) - { - _mixer = mixer; - _config = config; - _masterSWD = new SWD(Path.Combine(config.BGMPath, "bgm.swd")); - - _time = new TimeBarrier(192); - } - private void CreateThread() - { - _thread = new Thread(Tick) { Name = "DSE Player Tick" }; - _thread.Start(); - } - private void WaitThread() - { - if (_thread != null && (_thread.ThreadState == ThreadState.Running || _thread.ThreadState == ThreadState.WaitSleepJoin)) - { - _thread.Join(); - } - } - - private void InitEmulation() - { - _tempo = 120; - _tempoStack = 0; - _elapsedLoops = 0; - ElapsedTicks = 0; - _mixer.ResetFade(); - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - _tracks[trackIndex].Init(); - } - } - private void SetTicks() - { - MaxTicks = 0; - for (int trackIndex = 0; trackIndex < Events.Length; trackIndex++) - { - Events[trackIndex] = Events[trackIndex].OrderBy(e => e.Offset).ToList(); - List evs = Events[trackIndex]; - Track track = _tracks[trackIndex]; - track.Init(); - ElapsedTicks = 0; - while (true) - { - SongEvent e = evs.Single(ev => ev.Offset == track.CurOffset); - if (e.Ticks.Count > 0) - { - break; - } - else - { - e.Ticks.Add(ElapsedTicks); - ExecuteNext(track); - if (track.Stopped) - { - break; - } - else - { - ElapsedTicks += track.Rest; - track.Rest = 0; - } - } - } - if (ElapsedTicks > MaxTicks) - { - _longestTrack = trackIndex; - MaxTicks = ElapsedTicks; - } - track.StopAllChannels(); - } - } - public void LoadSong(long index) - { - if (_tracks != null) - { - for (int i = 0; i < _tracks.Length; i++) - { - _tracks[i].StopAllChannels(); - } - _tracks = null; - } - string bgm = _config.BGMFiles[index]; - _localSWD = new SWD(Path.ChangeExtension(bgm, "swd")); - _smdFile = File.ReadAllBytes(bgm); - using (var reader = new EndianBinaryReader(new MemoryStream(_smdFile))) - { - SMD.Header header = reader.ReadObject(); - SMD.ISongChunk songChunk; - switch (header.Version) - { - case 0x402: - { - songChunk = reader.ReadObject(); - break; - } - case 0x415: - { - songChunk = reader.ReadObject(); - break; - } - default: throw new Exception(string.Format(Strings.ErrorDSEInvalidHeaderVersion, header.Version)); - } - _tracks = new Track[songChunk.NumTracks]; - Events = new List[songChunk.NumTracks]; - for (byte trackIndex = 0; trackIndex < songChunk.NumTracks; trackIndex++) - { - Events[trackIndex] = new List(); - bool EventExists(long offset) - { - return Events[trackIndex].Any(e => e.Offset == offset); - } - - long chunkStart = reader.BaseStream.Position; - reader.BaseStream.Position += 0x14; // Skip header - _tracks[trackIndex] = new Track(trackIndex, reader.BaseStream.Position); - - uint lastNoteDuration = 0, lastRest = 0; - bool cont = true; - while (cont) - { - long offset = reader.BaseStream.Position; - void AddEvent(ICommand command) - { - Events[trackIndex].Add(new SongEvent(offset, command)); - } - byte cmd = reader.ReadByte(); - if (cmd <= 0x7F) - { - byte arg = reader.ReadByte(); - int numParams = (arg & 0xC0) >> 6; - int oct = ((arg & 0x30) >> 4) - 2; - int k = arg & 0xF; - if (k < 12) - { - uint duration; - if (numParams == 0) - { - duration = lastNoteDuration; - } - else // Big Endian reading of 8, 16, or 24 bits - { - duration = 0; - for (int b = 0; b < numParams; b++) - { - duration = (duration << 8) | reader.ReadByte(); - } - lastNoteDuration = duration; - } - if (!EventExists(offset)) - { - AddEvent(new NoteCommand { Key = (byte)k, OctaveChange = (sbyte)oct, Velocity = cmd, Duration = duration }); - } - } - else - { - throw new Exception(string.Format(Strings.ErrorDSEInvalidKey, trackIndex, offset, k)); - } - } - else if (cmd >= 0x80 && cmd <= 0x8F) - { - lastRest = Utils.FixedRests[cmd - 0x80]; - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - } - else // 0x90-0xFF - { - // TODO: 0x95 - a rest that may or may not repeat depending on some condition within channels - // TODO: 0x9E - may or may not jump somewhere else depending on an unknown structure - switch (cmd) - { - case 0x90: - { - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - break; - } - case 0x91: - { - lastRest = (uint)(lastRest + reader.ReadSByte()); - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - break; - } - case 0x92: - { - lastRest = reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - break; - } - case 0x93: - { - lastRest = reader.ReadUInt16(); - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - break; - } - case 0x94: - { - lastRest = (uint)(reader.ReadByte() | (reader.ReadByte() << 8) | (reader.ReadByte() << 16)); - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - break; - } - case 0x96: - case 0x97: - case 0x9A: - case 0x9B: - case 0x9F: - case 0xA2: - case 0xA3: - case 0xA6: - case 0xA7: - case 0xAD: - case 0xAE: - case 0xB7: - case 0xB8: - case 0xB9: - case 0xBA: - case 0xBB: - case 0xBD: - case 0xC1: - case 0xC2: - case 0xC4: - case 0xC5: - case 0xC6: - case 0xC7: - case 0xC8: - case 0xC9: - case 0xCA: - case 0xCC: - case 0xCD: - case 0xCE: - case 0xCF: - case 0xD9: - case 0xDA: - case 0xDE: - case 0xE6: - case 0xEB: - case 0xEE: - case 0xF4: - case 0xF5: - case 0xF7: - case 0xF9: - case 0xFA: - case 0xFB: - case 0xFC: - case 0xFD: - case 0xFE: - case 0xFF: - { - if (!EventExists(offset)) - { - AddEvent(new InvalidCommand { Command = cmd }); - } - break; - } - case 0x98: - { - if (!EventExists(offset)) - { - AddEvent(new FinishCommand()); - } - cont = false; - break; - } - case 0x99: - { - if (!EventExists(offset)) - { - AddEvent(new LoopStartCommand { Offset = reader.BaseStream.Position }); - } - break; - } - case 0xA0: - { - byte octave = reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new OctaveSetCommand { Octave = octave }); - } - break; - } - case 0xA1: - { - sbyte change = reader.ReadSByte(); - if (!EventExists(offset)) - { - AddEvent(new OctaveAddCommand { OctaveChange = change }); - } - break; - } - case 0xA4: - case 0xA5: // The code for these two is identical - { - byte tempoArg = reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new TempoCommand { Command = cmd, Tempo = tempoArg }); - } - break; - } - case 0xAB: - { - byte[] bytes = reader.ReadBytes(1); - if (!EventExists(offset)) - { - AddEvent(new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); - } - break; - } - case 0xAC: - { - byte voice = reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new VoiceCommand { Voice = voice }); - } - break; - } - case 0xCB: - case 0xF8: - { - byte[] bytes = reader.ReadBytes(2); - if (!EventExists(offset)) - { - AddEvent(new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); - } - break; - } - case 0xD7: - { - ushort bend = reader.ReadUInt16(); - if (!EventExists(offset)) - { - AddEvent(new PitchBendCommand { Bend = bend }); - } - break; - } - case 0xE0: - { - byte volume = reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new VolumeCommand { Volume = volume }); - } - break; - } - case 0xE3: - { - byte expression = reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new ExpressionCommand { Expression = expression }); - } - break; - } - case 0xE8: - { - byte panArg = reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); - } - break; - } - case 0x9D: - case 0xB0: - case 0xC0: - { - if (!EventExists(offset)) - { - AddEvent(new UnknownCommand { Command = cmd, Args = Array.Empty() }); - } - break; - } - case 0x9C: - case 0xA9: - case 0xAA: - case 0xB1: - case 0xB2: - case 0xB3: - case 0xB5: - case 0xB6: - case 0xBC: - case 0xBE: - case 0xBF: - case 0xC3: - case 0xD0: - case 0xD1: - case 0xD2: - case 0xDB: - case 0xDF: - case 0xE1: - case 0xE7: - case 0xE9: - case 0xEF: - case 0xF6: - { - byte[] args = reader.ReadBytes(1); - if (!EventExists(offset)) - { - AddEvent(new UnknownCommand { Command = cmd, Args = args }); - } - break; - } - case 0xA8: - case 0xB4: - case 0xD3: - case 0xD5: - case 0xD6: - case 0xD8: - case 0xF2: - { - byte[] args = reader.ReadBytes(2); - if (!EventExists(offset)) - { - AddEvent(new UnknownCommand { Command = cmd, Args = args }); - } - break; - } - case 0xAF: - case 0xD4: - case 0xE2: - case 0xEA: - case 0xF3: - { - byte[] args = reader.ReadBytes(3); - if (!EventExists(offset)) - { - AddEvent(new UnknownCommand { Command = cmd, Args = args }); - } - break; - } - case 0xDD: - case 0xE5: - case 0xED: - case 0xF1: - { - byte[] args = reader.ReadBytes(4); - if (!EventExists(offset)) - { - AddEvent(new UnknownCommand { Command = cmd, Args = args }); - } - break; - } - case 0xDC: - case 0xE4: - case 0xEC: - case 0xF0: - { - byte[] args = reader.ReadBytes(5); - if (!EventExists(offset)) - { - AddEvent(new UnknownCommand { Command = cmd, Args = args }); - } - break; - } - default: throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, trackIndex, offset, cmd)); - } - } - } - uint chunkLength = reader.ReadUInt32(chunkStart + 0xC); - reader.BaseStream.Position += chunkLength; - // Align 4 - while (reader.BaseStream.Position % 4 != 0) - { - reader.BaseStream.Position++; - } - } - SetTicks(); - } - } - public void SetCurrentPosition(long ticks) - { - if (_tracks == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - if (State == PlayerState.Playing) - { - Pause(); - } - InitEmulation(); - while (true) - { - if (ElapsedTicks == ticks) - { - goto finish; - } - else - { - while (_tempoStack >= 240) - { - _tempoStack -= 240; - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - Track track = _tracks[trackIndex]; - if (!track.Stopped) - { - track.Tick(); - while (track.Rest == 0 && !track.Stopped) - { - ExecuteNext(track); - } - } - } - ElapsedTicks++; - if (ElapsedTicks == ticks) - { - goto finish; - } - } - _tempoStack += _tempo; - } - } - finish: - for (int i = 0; i < _tracks.Length; i++) - { - _tracks[i].StopAllChannels(); - } - Pause(); - } - } - public void Play() - { - if (_tracks == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - Stop(); - InitEmulation(); - State = PlayerState.Playing; - CreateThread(); - } - } - public void Pause() - { - if (State == PlayerState.Playing) - { - State = PlayerState.Paused; - WaitThread(); - } - else if (State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.Playing; - CreateThread(); - } - } - public void Stop() - { - if (State == PlayerState.Playing || State == PlayerState.Paused) - { - State = PlayerState.Stopped; - WaitThread(); - } - } - public void Record(string fileName) - { - _mixer.CreateWaveWriter(fileName); - InitEmulation(); - State = PlayerState.Recording; - CreateThread(); - WaitThread(); - _mixer.CloseWaveWriter(); - } - public void Dispose() - { - if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.ShutDown; - WaitThread(); - } - } - public void GetSongState(UI.SongInfoControl.SongInfo info) - { - info.Tempo = _tempo; - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - Track track = _tracks[trackIndex]; - UI.SongInfoControl.SongInfo.Track tin = info.Tracks[trackIndex]; - tin.Position = track.CurOffset; - tin.Rest = track.Rest; - tin.Voice = track.Voice; - tin.Type = "PCM"; - tin.Volume = track.Volume; - tin.PitchBend = track.PitchBend; - tin.Extra = track.Octave; - tin.Panpot = track.Panpot; - - Channel[] channels = track.Channels.ToArray(); - if (channels.Length == 0) - { - tin.Keys[0] = byte.MaxValue; - tin.LeftVolume = 0f; - tin.RightVolume = 0f; - //tin.Type = string.Empty; - } - else - { - int numKeys = 0; - float left = 0f; - float right = 0f; - for (int j = 0; j < channels.Length; j++) - { - Channel c = channels[j]; - if (!Utils.IsStateRemovable(c.State)) - { - tin.Keys[numKeys++] = c.Key; - } - float a = (float)(-c.Panpot + 0x40) / 0x80 * c.Volume / 0x7F; - if (a > left) - { - left = a; - } - a = (float)(c.Panpot + 0x40) / 0x80 * c.Volume / 0x7F; - if (a > right) - { - right = a; - } - } - tin.Keys[numKeys] = byte.MaxValue; // There's no way for numKeys to be after the last index in the array - tin.LeftVolume = left; - tin.RightVolume = right; - //tin.Type = string.Join(", ", channels.Select(c => c.State.ToString())); - } - } - } - - private void ExecuteNext(Track track) - { - byte cmd = _smdFile[track.CurOffset++]; - if (cmd <= 0x7F) - { - byte arg = _smdFile[track.CurOffset++]; - int numParams = (arg & 0xC0) >> 6; - int oct = ((arg & 0x30) >> 4) - 2; - int k = arg & 0xF; - if (k < 12) - { - uint duration; - if (numParams == 0) - { - duration = track.LastNoteDuration; - } - else - { - duration = 0; - for (int b = 0; b < numParams; b++) - { - duration = (duration << 8) | _smdFile[track.CurOffset++]; - } - track.LastNoteDuration = duration; - } - Channel channel = _mixer.AllocateChannel(); - channel.Stop(); - track.Octave = (byte)(track.Octave + oct); - if (channel.StartPCM(_localSWD, _masterSWD, track.Voice, k + (12 * track.Octave), duration)) - { - channel.NoteVelocity = cmd; - channel.Owner = track; - track.Channels.Add(channel); - } - } - else - { - throw new Exception(string.Format(Strings.ErrorDSEInvalidKey, track.Index, track.CurOffset, k)); - } - } - else if (cmd >= 0x80 && cmd <= 0x8F) - { - track.LastRest = Utils.FixedRests[cmd - 0x80]; - track.Rest = track.LastRest; - } - else // 0x90-0xFF - { - // TODO: 0x95, 0x9E - switch (cmd) - { - case 0x90: - { - track.Rest = track.LastRest; - break; - } - case 0x91: - { - track.LastRest = (uint)(track.LastRest + (sbyte)_smdFile[track.CurOffset++]); - track.Rest = track.LastRest; - break; - } - case 0x92: - { - track.LastRest = _smdFile[track.CurOffset++]; - track.Rest = track.LastRest; - break; - } - case 0x93: - { - track.LastRest = (uint)(_smdFile[track.CurOffset++] | (_smdFile[track.CurOffset++] << 8)); - track.Rest = track.LastRest; - break; - } - case 0x94: - { - track.LastRest = (uint)(_smdFile[track.CurOffset++] | (_smdFile[track.CurOffset++] << 8) | (_smdFile[track.CurOffset++] << 16)); - track.Rest = track.LastRest; - break; - } - case 0x96: - case 0x97: - case 0x9A: - case 0x9B: - case 0x9F: - case 0xA2: - case 0xA3: - case 0xA6: - case 0xA7: - case 0xAD: - case 0xAE: - case 0xB7: - case 0xB8: - case 0xB9: - case 0xBA: - case 0xBB: - case 0xBD: - case 0xC1: - case 0xC2: - case 0xC4: - case 0xC5: - case 0xC6: - case 0xC7: - case 0xC8: - case 0xC9: - case 0xCA: - case 0xCC: - case 0xCD: - case 0xCE: - case 0xCF: - case 0xD9: - case 0xDA: - case 0xDE: - case 0xE6: - case 0xEB: - case 0xEE: - case 0xF4: - case 0xF5: - case 0xF7: - case 0xF9: - case 0xFA: - case 0xFB: - case 0xFC: - case 0xFD: - case 0xFE: - case 0xFF: - { - track.Stopped = true; - break; - } - case 0x98: - { - if (track.LoopOffset == -1) - { - track.Stopped = true; - } - else - { - track.CurOffset = track.LoopOffset; - } - break; - } - case 0x99: - { - track.LoopOffset = track.CurOffset; - break; - } - case 0xA0: - { - track.Octave = _smdFile[track.CurOffset++]; - break; - } - case 0xA1: - { - track.Octave = (byte)(track.Octave + (sbyte)_smdFile[track.CurOffset++]); - break; - } - case 0xA4: - case 0xA5: - { - _tempo = _smdFile[track.CurOffset++]; - break; - } - case 0xAB: - { - track.CurOffset++; - break; - } - case 0xAC: - { - track.Voice = _smdFile[track.CurOffset++]; - break; - } - case 0xCB: - case 0xF8: - { - track.CurOffset += 2; - break; - } - case 0xD7: - { - track.PitchBend = (ushort)(_smdFile[track.CurOffset++] | (_smdFile[track.CurOffset++] << 8)); - break; - } - case 0xE0: - { - track.Volume = _smdFile[track.CurOffset++]; - break; - } - case 0xE3: - { - track.Expression = _smdFile[track.CurOffset++]; - break; - } - case 0xE8: - { - track.Panpot = (sbyte)(_smdFile[track.CurOffset++] - 0x40); - break; - } - case 0x9D: - case 0xB0: - case 0xC0: - { - break; - } - case 0x9C: - case 0xA9: - case 0xAA: - case 0xB1: - case 0xB2: - case 0xB3: - case 0xB5: - case 0xB6: - case 0xBC: - case 0xBE: - case 0xBF: - case 0xC3: - case 0xD0: - case 0xD1: - case 0xD2: - case 0xDB: - case 0xDF: - case 0xE1: - case 0xE7: - case 0xE9: - case 0xEF: - case 0xF6: - { - track.CurOffset++; - break; - } - case 0xA8: - case 0xB4: - case 0xD3: - case 0xD5: - case 0xD6: - case 0xD8: - case 0xF2: - { - track.CurOffset += 2; - break; - } - case 0xAF: - case 0xD4: - case 0xE2: - case 0xEA: - case 0xF3: - { - track.CurOffset += 3; - break; - } - case 0xDD: - case 0xE5: - case 0xED: - case 0xF1: - { - track.CurOffset += 4; - break; - } - case 0xDC: - case 0xE4: - case 0xEC: - case 0xF0: - { - track.CurOffset += 5; - break; - } - default: throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, track.Index, track.CurOffset, cmd)); - } - } - } - - private void Tick() - { - _time.Start(); - while (true) - { - PlayerState state = State; - bool playing = state == PlayerState.Playing; - bool recording = state == PlayerState.Recording; - if (!playing && !recording) - { - goto stop; - } - - void MixerProcess() - { - _mixer.ChannelTick(); - _mixer.Process(playing, recording); - } - - while (_tempoStack >= 240) - { - _tempoStack -= 240; - bool allDone = true; - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - Track track = _tracks[trackIndex]; - track.Tick(); - while (track.Rest == 0 && !track.Stopped) - { - ExecuteNext(track); - } - if (trackIndex == _longestTrack) - { - if (ElapsedTicks == MaxTicks) - { - if (!track.Stopped) - { - List evs = Events[trackIndex]; - for (int i = 0; i < evs.Count; i++) - { - SongEvent ev = evs[i]; - if (ev.Offset == track.CurOffset) - { - ElapsedTicks = ev.Ticks[0] - track.Rest; - break; - } - } - _elapsedLoops++; - if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) - { - _mixer.BeginFadeOut(); - } - } - } - else - { - ElapsedTicks++; - } - } - if (!track.Stopped || track.Channels.Count != 0) - { - allDone = false; - } - } - if (_mixer.IsFadeDone()) - { - allDone = true; - } - if (allDone) - { - MixerProcess(); - State = PlayerState.Stopped; - SongEnded?.Invoke(); - } - } - _tempoStack += _tempo; - MixerProcess(); - if (playing) - { - _time.Wait(); - } - } - stop: - _time.Stop(); - } - } -} diff --git a/VG Music Studio/Core/NDS/DSE/SMD.cs b/VG Music Studio/Core/NDS/DSE/SMD.cs deleted file mode 100644 index 706cb06c..00000000 --- a/VG Music Studio/Core/NDS/DSE/SMD.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class SMD - { - public class Header - { - [BinaryStringFixedLength(4)] - public string Type { get; set; } // "smdb" or "smdl" - [BinaryArrayFixedLength(4)] - public byte[] Unknown1 { get; set; } - public uint Length { get; set; } - public ushort Version { get; set; } - [BinaryArrayFixedLength(10)] - public byte[] Unknown2 { get; set; } - public ushort Year { get; set; } - public byte Month { get; set; } - public byte Day { get; set; } - public byte Hour { get; set; } - public byte Minute { get; set; } - public byte Second { get; set; } - public byte Centisecond { get; set; } - [BinaryStringFixedLength(16)] - public string Label { get; set; } - [BinaryArrayFixedLength(16)] - public byte[] Unknown3 { get; set; } - } - - public interface ISongChunk - { - byte NumTracks { get; } - } - public class SongChunk_V402 : ISongChunk - { - [BinaryStringFixedLength(4)] - public string Type { get; set; } - [BinaryArrayFixedLength(16)] - public byte[] Unknown1 { get; set; } - public byte NumTracks { get; set; } - public byte NumChannels { get; set; } - [BinaryArrayFixedLength(4)] - public byte[] Unknown2 { get; set; } - public sbyte MasterVolume { get; set; } - public sbyte MasterPanpot { get; set; } - [BinaryArrayFixedLength(4)] - public byte[] Unknown3 { get; set; } - } - public class SongChunk_V415 : ISongChunk - { - [BinaryStringFixedLength(4)] - public string Type { get; set; } - [BinaryArrayFixedLength(18)] - public byte[] Unknown1 { get; set; } - public byte NumTracks { get; set; } - public byte NumChannels { get; set; } - [BinaryArrayFixedLength(40)] - public byte[] Unknown2 { get; set; } - } - } -} diff --git a/VG Music Studio/Core/NDS/DSE/SWD.cs b/VG Music Studio/Core/NDS/DSE/SWD.cs deleted file mode 100644 index 4e9f984a..00000000 --- a/VG Music Studio/Core/NDS/DSE/SWD.cs +++ /dev/null @@ -1,471 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class SWD - { - public interface IHeader - { - - } - private class Header_V402 : IHeader - { - [BinaryArrayFixedLength(10)] - public byte[] Unknown1 { get; set; } - public ushort Year { get; set; } - public byte Month { get; set; } - public byte Day { get; set; } - public byte Hour { get; set; } - public byte Minute { get; set; } - public byte Second { get; set; } - public byte Centisecond { get; set; } - [BinaryStringFixedLength(16)] - public string Label { get; set; } - [BinaryArrayFixedLength(22)] - public byte[] Unknown2 { get; set; } - public byte NumWAVISlots { get; set; } - public byte NumPRGISlots { get; set; } - public byte NumKeyGroups { get; set; } - [BinaryArrayFixedLength(7)] - public byte[] Padding { get; set; } - } - private class Header_V415 : IHeader - { - [BinaryArrayFixedLength(10)] - public byte[] Unknown1 { get; set; } - public ushort Year { get; set; } - public byte Month { get; set; } - public byte Day { get; set; } - public byte Hour { get; set; } - public byte Minute { get; set; } - public byte Second { get; set; } - public byte Centisecond { get; set; } - [BinaryStringFixedLength(16)] - public string Label { get; set; } - [BinaryArrayFixedLength(16)] - public byte[] Unknown2 { get; set; } - public uint PCMDLength { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown3 { get; set; } - public ushort NumWAVISlots { get; set; } - public ushort NumPRGISlots { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown4 { get; set; } - public uint WAVILength { get; set; } - } - - public interface ISplitEntry - { - byte LowKey { get; } - byte HighKey { get; } - int SampleId { get; } - byte SampleRootKey { get; } - sbyte SampleTranspose { get; } - byte AttackVolume { get; set; } - byte Attack { get; set; } - byte Decay { get; set; } - byte Sustain { get; set; } - byte Hold { get; set; } - byte Decay2 { get; set; } - byte Release { get; set; } - } - public class SplitEntry_V402 : ISplitEntry - { - public ushort Id { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } - public byte LowKey { get; set; } - public byte HighKey { get; set; } - public byte LowKey2 { get; set; } - public byte HighKey2 { get; set; } - public byte LowVelocity { get; set; } - public byte HighVelocity { get; set; } - public byte LowVelocity2 { get; set; } - public byte HighVelocity2 { get; set; } - [BinaryArrayFixedLength(5)] - public byte[] Unknown2 { get; set; } - public byte SampleId { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown3 { get; set; } - public byte SampleRootKey { get; set; } - public sbyte SampleTranspose { get; set; } - public byte SampleVolume { get; set; } - public sbyte SamplePanpot { get; set; } - public byte KeyGroupId { get; set; } - [BinaryArrayFixedLength(15)] - public byte[] Unknown4 { get; set; } - public byte AttackVolume { get; set; } - public byte Attack { get; set; } - public byte Decay { get; set; } - public byte Sustain { get; set; } - public byte Hold { get; set; } - public byte Decay2 { get; set; } - public byte Release { get; set; } - public byte Unknown5 { get; set; } - - int ISplitEntry.SampleId => SampleId; - } - public class SplitEntry_V415 : ISplitEntry - { - public ushort Id { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } - public byte LowKey { get; set; } - public byte HighKey { get; set; } - public byte LowKey2 { get; set; } - public byte HighKey2 { get; set; } - public byte LowVelocity { get; set; } - public byte HighVelocity { get; set; } - public byte LowVelocity2 { get; set; } - public byte HighVelocity2 { get; set; } - [BinaryArrayFixedLength(6)] - public byte[] Unknown2 { get; set; } - public ushort SampleId { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown3 { get; set; } - public byte SampleRootKey { get; set; } - public sbyte SampleTranspose { get; set; } - public byte SampleVolume { get; set; } - public sbyte SamplePanpot { get; set; } - public byte KeyGroupId { get; set; } - [BinaryArrayFixedLength(13)] - public byte[] Unknown4 { get; set; } - public byte AttackVolume { get; set; } - public byte Attack { get; set; } - public byte Decay { get; set; } - public byte Sustain { get; set; } - public byte Hold { get; set; } - public byte Decay2 { get; set; } - public byte Release { get; set; } - public byte Unknown5 { get; set; } - - int ISplitEntry.SampleId => SampleId; - } - - public interface IProgramInfo - { - ISplitEntry[] SplitEntries { get; } - } - public class ProgramInfo_V402 : IProgramInfo - { - public byte Id { get; set; } - public byte NumSplits { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } - public byte Volume { get; set; } - public byte Panpot { get; set; } - [BinaryArrayFixedLength(5)] - public byte[] Unknown2 { get; set; } - public byte NumLFOs { get; set; } - [BinaryArrayFixedLength(4)] - public byte[] Unknown3 { get; set; } - [BinaryArrayFixedLength(16)] - public KeyGroup[] KeyGroups { get; set; } - [BinaryArrayVariableLength(nameof(NumLFOs))] - public LFOInfo LFOInfos { get; set; } - [BinaryArrayVariableLength(nameof(NumSplits))] - public SplitEntry_V402[] SplitEntries { get; set; } - - [BinaryIgnore] - ISplitEntry[] IProgramInfo.SplitEntries => SplitEntries; - } - public class ProgramInfo_V415 : IProgramInfo - { - public ushort Id { get; set; } - public ushort NumSplits { get; set; } - public byte Volume { get; set; } - public byte Panpot { get; set; } - [BinaryArrayFixedLength(5)] - public byte[] Unknown1 { get; set; } - public byte NumLFOs { get; set; } - [BinaryArrayFixedLength(4)] - public byte[] Unknown2 { get; set; } - [BinaryArrayVariableLength(nameof(NumLFOs))] - public LFOInfo[] LFOInfos { get; set; } - [BinaryArrayFixedLength(16)] - public byte[] Unknown3 { get; set; } - [BinaryArrayVariableLength(nameof(NumSplits))] - public SplitEntry_V415[] SplitEntries { get; set; } - - [BinaryIgnore] - ISplitEntry[] IProgramInfo.SplitEntries => SplitEntries; - } - - public interface IWavInfo - { - byte RootKey { get; } - sbyte Transpose { get; } - SampleFormat SampleFormat { get; } - bool Loop { get; } - uint SampleRate { get; } - uint SampleOffset { get; } - uint LoopStart { get; } - uint LoopEnd { get; } - byte EnvMult { get; } - byte AttackVolume { get; } - byte Attack { get; } - byte Decay { get; } - byte Sustain { get; } - byte Hold { get; } - byte Decay2 { get; } - byte Release { get; } - } - public class WavInfo_V402 : IWavInfo - { - public byte Unknown1 { get; set; } - public byte Id { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown2 { get; set; } - public byte RootKey { get; set; } - public sbyte Transpose { get; set; } - public byte Volume { get; set; } - public sbyte Panpot { get; set; } - public SampleFormat SampleFormat { get; set; } - [BinaryArrayFixedLength(7)] - public byte[] Unknown3 { get; set; } - public bool Loop { get; set; } - public uint SampleRate { get; set; } - public uint SampleOffset { get; set; } - public uint LoopStart { get; set; } - public uint LoopEnd { get; set; } - [BinaryArrayFixedLength(16)] - public byte[] Unknown4 { get; set; } - public byte EnvOn { get; set; } - public byte EnvMult { get; set; } - [BinaryArrayFixedLength(6)] - public byte[] Unknown5 { get; set; } - public byte AttackVolume { get; set; } - public byte Attack { get; set; } - public byte Decay { get; set; } - public byte Sustain { get; set; } - public byte Hold { get; set; } - public byte Decay2 { get; set; } - public byte Release { get; set; } - public byte Unknown6 { get; set; } - } - public class WavInfo_V415 : IWavInfo - { - [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } - public ushort Id { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown2 { get; set; } - public byte RootKey { get; set; } - public sbyte Transpose { get; set; } - public byte Volume { get; set; } - public sbyte Panpot { get; set; } - [BinaryArrayFixedLength(6)] - public byte[] Unknown3 { get; set; } - public ushort Version { get; set; } - public SampleFormat SampleFormat { get; set; } - public byte Unknown4 { get; set; } - public bool Loop { get; set; } - public byte Unknown5 { get; set; } - public byte SamplesPer32Bits { get; set; } - public byte Unknown6 { get; set; } - public byte BitDepth { get; set; } - [BinaryArrayFixedLength(6)] - public byte[] Unknown7 { get; set; } - public uint SampleRate { get; set; } - public uint SampleOffset { get; set; } - public uint LoopStart { get; set; } - public uint LoopEnd { get; set; } - public byte EnvOn { get; set; } - public byte EnvMult { get; set; } - [BinaryArrayFixedLength(6)] - public byte[] Unknown8 { get; set; } - public byte AttackVolume { get; set; } - public byte Attack { get; set; } - public byte Decay { get; set; } - public byte Sustain { get; set; } - public byte Hold { get; set; } - public byte Decay2 { get; set; } - public byte Release { get; set; } - public byte Unknown9 { get; set; } - } - - public class SampleBlock - { - public IWavInfo WavInfo; - public byte[] Data; - } - public class ProgramBank - { - public IProgramInfo[] ProgramInfos; - public KeyGroup[] KeyGroups; - } - public class KeyGroup - { - public ushort Id { get; set; } - public byte Poly { get; set; } - public byte Priority { get; set; } - public byte Low { get; set; } - public byte High { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown { get; set; } - } - public class LFOInfo - { - [BinaryArrayFixedLength(16)] - public byte[] Unknown { get; set; } - } - - public string Type; // "swdb" or "swdl" - public byte[] Unknown; - public uint Length; - public ushort Version; - public IHeader Header; - - public ProgramBank Programs; - public SampleBlock[] Samples; - - public SWD(string path) - { - using (var reader = new EndianBinaryReader(new MemoryStream(File.ReadAllBytes(path)))) - { - Type = reader.ReadString(4, false); - Unknown = reader.ReadBytes(4); - Length = reader.ReadUInt32(); - Version = reader.ReadUInt16(); - switch (Version) - { - case 0x402: - { - Header_V402 header = reader.ReadObject(); - Header = header; - Programs = ReadPrograms(reader, header.NumPRGISlots); - Samples = ReadSamples(reader, header.NumWAVISlots); - break; - } - case 0x415: - { - Header_V415 header = reader.ReadObject(); - Header = header; - Programs = ReadPrograms(reader, header.NumPRGISlots); - if (header.PCMDLength != 0 && (header.PCMDLength & 0xFFFF0000) != 0xAAAA0000) - { - Samples = ReadSamples(reader, header.NumWAVISlots); - } - break; - } - default: throw new InvalidDataException(); - } - } - } - - private static long FindChunk(EndianBinaryReader reader, string chunk) - { - long pos = -1; - long oldPosition = reader.BaseStream.Position; - reader.BaseStream.Position = 0; - while (reader.BaseStream.Position < reader.BaseStream.Length) - { - string str = reader.ReadString(4, false); - if (str == chunk) - { - pos = reader.BaseStream.Position - 4; - break; - } - switch (str) - { - case "swdb": - case "swdl": - { - reader.BaseStream.Position += 0x4C; - break; - } - default: - { - reader.BaseStream.Position += 0x8; - uint length = reader.ReadUInt32(); - reader.BaseStream.Position += length; - // Align 4 - while (reader.BaseStream.Position % 4 != 0) - { - reader.BaseStream.Position++; - } - break; - } - } - } - reader.BaseStream.Position = oldPosition; - return pos; - } - - private static SampleBlock[] ReadSamples(EndianBinaryReader reader, int numWAVISlots) where T : IWavInfo, new() - { - long waviChunkOffset = FindChunk(reader, "wavi"); - long pcmdChunkOffset = FindChunk(reader, "pcmd"); - if (waviChunkOffset == -1 || pcmdChunkOffset == -1) - { - throw new InvalidDataException(); - } - else - { - waviChunkOffset += 0x10; - pcmdChunkOffset += 0x10; - var samples = new SampleBlock[numWAVISlots]; - for (int i = 0; i < numWAVISlots; i++) - { - ushort offset = reader.ReadUInt16(waviChunkOffset + (2 * i)); - if (offset != 0) - { - T wavInfo = reader.ReadObject(offset + waviChunkOffset); - samples[i] = new SampleBlock - { - WavInfo = wavInfo, - Data = reader.ReadBytes((int)((wavInfo.LoopStart + wavInfo.LoopEnd) * 4), pcmdChunkOffset + wavInfo.SampleOffset) - }; - } - } - return samples; - } - } - private static ProgramBank ReadPrograms(EndianBinaryReader reader, int numPRGISlots) where T : IProgramInfo, new() - { - long chunkOffset = FindChunk(reader, "prgi"); - if (chunkOffset == -1) - { - return null; - } - else - { - chunkOffset += 0x10; - var programInfos = new IProgramInfo[numPRGISlots]; - for (int i = 0; i < programInfos.Length; i++) - { - ushort offset = reader.ReadUInt16(chunkOffset + (2 * i)); - if (offset != 0) - { - programInfos[i] = reader.ReadObject(offset + chunkOffset); - } - } - return new ProgramBank - { - ProgramInfos = programInfos, - KeyGroups = ReadKeyGroups(reader) - }; - } - } - private static KeyGroup[] ReadKeyGroups(EndianBinaryReader reader) - { - long chunkOffset = FindChunk(reader, "kgrp"); - if (chunkOffset == -1) - { - return Array.Empty(); - } - else - { - uint chunkLength = reader.ReadUInt32(chunkOffset + 0xC); - var keyGroups = new KeyGroup[chunkLength / 8]; // 8 is the size of a KeyGroup - for (int i = 0; i < keyGroups.Length; i++) - { - keyGroups[i] = reader.ReadObject(); - } - return keyGroups; - } - } - } -} diff --git a/VG Music Studio/Core/NDS/DSE/Track.cs b/VG Music Studio/Core/NDS/DSE/Track.cs deleted file mode 100644 index 9a330cab..00000000 --- a/VG Music Studio/Core/NDS/DSE/Track.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class Track - { - public readonly byte Index; - private readonly long _startOffset; - public byte Octave; - public byte Voice; - public byte Expression; - public byte Volume; - public sbyte Panpot; - public uint Rest; - public ushort PitchBend; - public long CurOffset; - public long LoopOffset; - public bool Stopped; - public uint LastNoteDuration; - public uint LastRest; - - public readonly List Channels = new List(0x10); - - public Track(byte i, long startOffset) - { - Index = i; - _startOffset = startOffset; - } - - public void Init() - { - Expression = 0; - Voice = 0; - Volume = 0; - Octave = 4; - Panpot = 0; - Rest = 0; - PitchBend = 0; - CurOffset = _startOffset; - LoopOffset = -1; - Stopped = false; - LastNoteDuration = 0; - LastRest = 0; - StopAllChannels(); - } - - public void Tick() - { - if (Rest > 0) - { - Rest--; - } - for (int i = 0; i < Channels.Count; i++) - { - Channel c = Channels[i]; - if (c.NoteLength > 0) - { - c.NoteLength--; - } - } - } - - public void StopAllChannels() - { - Channel[] chans = Channels.ToArray(); - for (int i = 0; i < chans.Length; i++) - { - chans[i].Stop(); - } - } - } -} diff --git a/VG Music Studio/Core/NDS/DSE/Utils.cs b/VG Music Studio/Core/NDS/DSE/Utils.cs deleted file mode 100644 index 1f16b1a5..00000000 --- a/VG Music Studio/Core/NDS/DSE/Utils.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal static class Utils - { - public static short[] Duration16 = new short[128] - { - 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, - 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, - 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, - 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F, - 0x0020, 0x0023, 0x0028, 0x002D, 0x0033, 0x0039, 0x0040, 0x0048, - 0x0050, 0x0058, 0x0062, 0x006D, 0x0078, 0x0083, 0x0090, 0x009E, - 0x00AC, 0x00BC, 0x00CC, 0x00DE, 0x00F0, 0x0104, 0x0119, 0x012F, - 0x0147, 0x0160, 0x017A, 0x0196, 0x01B3, 0x01D2, 0x01F2, 0x0214, - 0x0238, 0x025E, 0x0285, 0x02AE, 0x02D9, 0x0307, 0x0336, 0x0367, - 0x039B, 0x03D1, 0x0406, 0x0442, 0x047E, 0x04C4, 0x0500, 0x0546, - 0x058C, 0x0622, 0x0672, 0x06CC, 0x071C, 0x0776, 0x07DA, 0x0834, - 0x0898, 0x0906, 0x096A, 0x09D8, 0x0A50, 0x0ABE, 0x0B40, 0x0BB8, - 0x0C3A, 0x0CBC, 0x0D48, 0x0DDE, 0x0E6A, 0x0F00, 0x0FA0, 0x1040, - 0x10EA, 0x1194, 0x123E, 0x12F2, 0x13B0, 0x146E, 0x1536, 0x15FE, - 0x16D0, 0x17A2, 0x187E, 0x195A, 0x1A40, 0x1B30, 0x1C20, 0x1D1A, - 0x1E1E, 0x1F22, 0x2030, 0x2148, 0x2260, 0x2382, 0x2710, 0x7FFF - }; - public static int[] Duration32 = new int[128] - { - 0x00000000, 0x00000004, 0x00000007, 0x0000000A, 0x0000000F, 0x00000015, 0x0000001C, 0x00000024, - 0x0000002E, 0x0000003A, 0x00000048, 0x00000057, 0x00000068, 0x0000007B, 0x00000091, 0x000000A8, - 0x00000185, 0x000001BE, 0x000001FC, 0x0000023F, 0x00000288, 0x000002D6, 0x0000032A, 0x00000385, - 0x000003E5, 0x0000044C, 0x000004BA, 0x0000052E, 0x000005A9, 0x0000062C, 0x000006B5, 0x00000746, - 0x00000BCF, 0x00000CC0, 0x00000DBD, 0x00000EC6, 0x00000FDC, 0x000010FF, 0x0000122F, 0x0000136C, - 0x000014B6, 0x0000160F, 0x00001775, 0x000018EA, 0x00001A6D, 0x00001BFF, 0x00001DA0, 0x00001F51, - 0x00002C16, 0x00002E80, 0x00003100, 0x00003395, 0x00003641, 0x00003902, 0x00003BDB, 0x00003ECA, - 0x000041D0, 0x000044EE, 0x00004824, 0x00004B73, 0x00004ED9, 0x00005259, 0x000055F2, 0x000059A4, - 0x000074CC, 0x000079AB, 0x00007EAC, 0x000083CE, 0x00008911, 0x00008E77, 0x000093FF, 0x000099AA, - 0x00009F78, 0x0000A56A, 0x0000AB80, 0x0000B1BB, 0x0000B81A, 0x0000BE9E, 0x0000C547, 0x0000CC17, - 0x0000FD42, 0x000105CB, 0x00010E82, 0x00011768, 0x0001207E, 0x000129C4, 0x0001333B, 0x00013CE2, - 0x000146BB, 0x000150C5, 0x00015B02, 0x00016572, 0x00017015, 0x00017AEB, 0x000185F5, 0x00019133, - 0x0001E16D, 0x0001EF07, 0x0001FCE0, 0x00020AF7, 0x0002194F, 0x000227E6, 0x000236BE, 0x000245D7, - 0x00025532, 0x000264CF, 0x000274AE, 0x000284D0, 0x00029536, 0x0002A5E0, 0x0002B6CE, 0x0002C802, - 0x000341B0, 0x000355F8, 0x00036A90, 0x00037F79, 0x000394B4, 0x0003AA41, 0x0003C021, 0x0003D654, - 0x0003ECDA, 0x000403B5, 0x00041AE5, 0x0004326A, 0x00044A45, 0x00046277, 0x00047B00, 0x7FFFFFFF - }; - public static readonly byte[] FixedRests = new byte[0x10] - { - 96, 72, 64, 48, 36, 32, 24, 18, 16, 12, 9, 8, 6, 4, 3, 2 - }; - - public static bool IsStateRemovable(EnvelopeState state) - { - return state == EnvelopeState.Two || state >= EnvelopeState.Seven; - } - } -} diff --git a/VG Music Studio/Core/NDS/SDAT/Channel.cs b/VG Music Studio/Core/NDS/SDAT/Channel.cs deleted file mode 100644 index 95b89010..00000000 --- a/VG Music Studio/Core/NDS/SDAT/Channel.cs +++ /dev/null @@ -1,387 +0,0 @@ -using System; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class Channel - { - public readonly byte Index; - - public Track Owner; - public InstrumentType Type; - public EnvelopeState State; - public bool AutoSweep; - public byte BaseKey; - public byte Key; - public byte NoteVelocity; - public sbyte StartingPan; - public sbyte Pan; - public int SweepCounter; - public int SweepLength; - public short SweepPitch; - public int Velocity; // The SEQ Player treats 0 as the 100% amplitude value and -92544 (-723*128) as the 0% amplitude value. The starting ampltitude is 0% (-92544). - public byte Volume; // From 0x00-0x7F (Calculated from Utils) - public ushort BaseTimer; - public ushort Timer; - public int NoteDuration; - - private byte _attack; - private int _sustain; - private ushort _decay; - private ushort _release; - public byte LFORange; - public byte LFOSpeed; - public byte LFODepth; - public ushort LFODelay; - public ushort LFOPhase; - public int LFOParam; - public ushort LFODelayCount; - public LFOType LFOType; - public byte Priority; - - private int _pos; - private short _prevLeft; - private short _prevRight; - - // PCM8, PCM16, ADPCM - private SWAR.SWAV _swav; - // PCM8, PCM16 - private int _dataOffset; - // ADPCM - private ADPCMDecoder _adpcmDecoder; - private short _adpcmLoopLastSample; - private short _adpcmLoopStepIndex; - // PSG - private byte _psgDuty; - private int _psgCounter; - // Noise - private ushort _noiseCounter; - - public Channel(byte i) - { - Index = i; - } - - public void StartPCM(SWAR.SWAV swav, int noteDuration) - { - Type = InstrumentType.PCM; - _dataOffset = 0; - _swav = swav; - if (swav.Format == SWAVFormat.ADPCM) - { - _adpcmDecoder = new ADPCMDecoder(swav.Samples); - } - BaseTimer = swav.Timer; - Start(noteDuration); - } - public void StartPSG(byte duty, int noteDuration) - { - Type = InstrumentType.PSG; - _psgCounter = 0; - _psgDuty = duty; - BaseTimer = 8006; - Start(noteDuration); - } - public void StartNoise(int noteLength) - { - Type = InstrumentType.Noise; - _noiseCounter = 0x7FFF; - BaseTimer = 8006; - Start(noteLength); - } - - private void Start(int noteDuration) - { - State = EnvelopeState.Attack; - Velocity = -92544; - _pos = 0; - _prevLeft = _prevRight = 0; - NoteDuration = noteDuration; - } - - public void Stop() - { - if (Owner != null) - { - Owner.Channels.Remove(this); - } - Owner = null; - Volume = 0; - Priority = 0; - } - - public int SweepMain() - { - if (SweepPitch != 0 && SweepCounter < SweepLength) - { - int sweep = (int)(Math.BigMul(SweepPitch, SweepLength - SweepCounter) / SweepLength); - if (AutoSweep) - { - SweepCounter++; - } - return sweep; - } - else - { - return 0; - } - } - public void LFOTick() - { - if (LFODelayCount > 0) - { - LFODelayCount--; - LFOPhase = 0; - } - else - { - int param = LFORange * Utils.Sin(LFOPhase >> 8) * LFODepth; - if (LFOType == LFOType.Volume) - { - param = (param * 60) >> 14; - } - else - { - param >>= 8; - } - LFOParam = param; - int counter = LFOPhase + (LFOSpeed << 6); // "<< 6" is "* 0x40" - while (counter >= 0x8000) - { - counter -= 0x8000; - } - LFOPhase = (ushort)counter; - } - } - - public void SetAttack(int a) - { - _attack = Utils.AttackTable[a]; - } - public void SetDecay(int d) - { - _decay = Utils.DecayTable[d]; - } - public void SetSustain(byte s) - { - _sustain = Utils.SustainTable[s]; - } - public void SetRelease(int r) - { - _release = Utils.DecayTable[r]; - } - public void StepEnvelope() - { - switch (State) - { - case EnvelopeState.Attack: - { - Velocity = _attack * Velocity / 0xFF; - if (Velocity == 0) - { - State = EnvelopeState.Decay; - } - break; - } - case EnvelopeState.Decay: - { - Velocity -= _decay; - if (Velocity <= _sustain) - { - State = EnvelopeState.Sustain; - Velocity = _sustain; - } - break; - } - case EnvelopeState.Release: - { - Velocity -= _release; - if (Velocity < -92544) - { - Velocity = -92544; - } - break; - } - } - } - - /// EmulateProcess doesn't care about samples that loop; it only cares about ones that force the track to wait for them to end - public void EmulateProcess() - { - if (Timer != 0) - { - int numSamples = (_pos + 0x100) / Timer; - _pos = (_pos + 0x100) % Timer; - for (int i = 0; i < numSamples; i++) - { - if (Type == InstrumentType.PCM && !_swav.DoesLoop) - { - switch (_swav.Format) - { - case SWAVFormat.PCM8: - { - if (_dataOffset >= _swav.Samples.Length) - { - Stop(); - } - else - { - _dataOffset++; - } - return; - } - case SWAVFormat.PCM16: - { - if (_dataOffset >= _swav.Samples.Length) - { - Stop(); - } - else - { - _dataOffset += 2; - } - return; - } - case SWAVFormat.ADPCM: - { - if (_adpcmDecoder.DataOffset >= _swav.Samples.Length && !_adpcmDecoder.OnSecondNibble) - { - Stop(); - } - else - { - // This is a faster emulation of adpcmDecoder.GetSample() without caring about the sample - if (_adpcmDecoder.OnSecondNibble) - { - _adpcmDecoder.DataOffset++; - } - _adpcmDecoder.OnSecondNibble = !_adpcmDecoder.OnSecondNibble; - } - return; - } - } - } - } - } - } - public void Process(out short left, out short right) - { - if (Timer != 0) - { - int numSamples = (_pos + 0x100) / Timer; - _pos = (_pos + 0x100) % Timer; - // prevLeft and prevRight are stored because numSamples can be 0. - for (int i = 0; i < numSamples; i++) - { - short samp; - switch (Type) - { - case InstrumentType.PCM: - { - switch (_swav.Format) - { - case SWAVFormat.PCM8: - { - // If hit end - if (_dataOffset >= _swav.Samples.Length) - { - if (_swav.DoesLoop) - { - _dataOffset = _swav.LoopOffset * 4; - } - else - { - left = right = _prevLeft = _prevRight = 0; - Stop(); - return; - } - } - samp = (short)((sbyte)_swav.Samples[_dataOffset++] << 8); - break; - } - case SWAVFormat.PCM16: - { - // If hit end - if (_dataOffset >= _swav.Samples.Length) - { - if (_swav.DoesLoop) - { - _dataOffset = _swav.LoopOffset * 4; - } - else - { - left = right = _prevLeft = _prevRight = 0; - Stop(); - return; - } - } - samp = (short)(_swav.Samples[_dataOffset++] | (_swav.Samples[_dataOffset++] << 8)); - break; - } - case SWAVFormat.ADPCM: - { - // If just looped - if (_swav.DoesLoop && _adpcmDecoder.DataOffset == _swav.LoopOffset * 4 && !_adpcmDecoder.OnSecondNibble) - { - _adpcmLoopLastSample = _adpcmDecoder.LastSample; - _adpcmLoopStepIndex = _adpcmDecoder.StepIndex; - } - // If hit end - if (_adpcmDecoder.DataOffset >= _swav.Samples.Length && !_adpcmDecoder.OnSecondNibble) - { - if (_swav.DoesLoop) - { - _adpcmDecoder.DataOffset = _swav.LoopOffset * 4; - _adpcmDecoder.StepIndex = _adpcmLoopStepIndex; - _adpcmDecoder.LastSample = _adpcmLoopLastSample; - _adpcmDecoder.OnSecondNibble = false; - } - else - { - left = right = _prevLeft = _prevRight = 0; - Stop(); - return; - } - } - samp = _adpcmDecoder.GetSample(); - break; - } - default: samp = 0; break; - } - break; - } - case InstrumentType.PSG: - { - samp = _psgCounter <= _psgDuty ? short.MinValue : short.MaxValue; - _psgCounter++; - if (_psgCounter >= 8) - { - _psgCounter = 0; - } - break; - } - case InstrumentType.Noise: - { - if ((_noiseCounter & 1) != 0) - { - _noiseCounter = (ushort)((_noiseCounter >> 1) ^ 0x6000); - samp = -0x7FFF; - } - else - { - _noiseCounter = (ushort)(_noiseCounter >> 1); - samp = 0x7FFF; - } - break; - } - default: samp = 0; break; - } - samp = (short)(samp * Volume / 0x7F); - _prevLeft = (short)(samp * (-Pan + 0x40) / 0x80); - _prevRight = (short)(samp * (Pan + 0x40) / 0x80); - } - } - left = _prevLeft; - right = _prevRight; - } - } -} diff --git a/VG Music Studio/Core/NDS/SDAT/Commands.cs b/VG Music Studio/Core/NDS/SDAT/Commands.cs deleted file mode 100644 index 7a83fe13..00000000 --- a/VG Music Studio/Core/NDS/SDAT/Commands.cs +++ /dev/null @@ -1,439 +0,0 @@ -using System; -using System.Drawing; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal abstract class SDATCommand - { - public bool RandMod { get; set; } - public bool VarMod { get; set; } - - protected string GetValues(int value, string ifNot) - { - return RandMod ? $"[{(short)value}, {(short)(value >> 16)}]" - : VarMod ? $"[{(byte)value}]" - : ifNot; - } - } - - internal class AllocTracksCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Alloc Tracks"; - public string Arguments => $"{Convert.ToString(Tracks, 2).PadLeft(16, '0')}b"; - - public ushort Tracks { get; set; } - } - internal class CallCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Call"; - public string Arguments => $"0x{Offset:X4}"; - - public int Offset { get; set; } - } - internal class FinishCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Finish"; - public string Arguments => string.Empty; - } - internal class ForceAttackCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "Force Attack"; - public string Arguments => GetValues(Attack, Attack.ToString()); - - public int Attack { get; set; } - } - internal class ForceDecayCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "Force Decay"; - public string Arguments => GetValues(Decay, Decay.ToString()); - - public int Decay { get; set; } - } - internal class ForceReleaseCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "Force Release"; - public string Arguments => GetValues(Release, Release.ToString()); - - public int Release { get; set; } - } - internal class ForceSustainCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "Force Sustain"; - public string Arguments => GetValues(Sustain, Sustain.ToString()); - - public int Sustain { get; set; } - } - internal class JumpCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Jump"; - public string Arguments => $"0x{Offset:X4}"; - - public int Offset { get; set; } - } - internal class LFODelayCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Delay"; - public string Arguments => GetValues(Delay, Delay.ToString()); - - public int Delay { get; set; } - } - internal class LFODepthCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Depth"; - public string Arguments => GetValues(Depth, Depth.ToString()); - - public int Depth { get; set; } - } - internal class LFORangeCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Range"; - public string Arguments => GetValues(Range, Range.ToString()); - - public int Range { get; set; } - } - internal class LFOSpeedCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Speed"; - public string Arguments => GetValues(Speed, Speed.ToString()); - - public int Speed { get; set; } - } - internal class LFOTypeCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Type"; - public string Arguments => GetValues(Type, Type.ToString()); - - public int Type { get; set; } - } - internal class LoopEndCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Loop End"; - public string Arguments => string.Empty; - } - internal class LoopStartCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Loop Start"; - public string Arguments => GetValues(NumLoops, NumLoops.ToString()); - - public int NumLoops { get; set; } - } - internal class ModIfCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "If Modifier"; - public string Arguments => string.Empty; - } - internal class ModRandCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Rand Modifier"; - public string Arguments => string.Empty; - } - internal class ModVarCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Modifier"; - public string Arguments => string.Empty; - } - internal class MonophonyCommand : SDATCommand, ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Monophony Toggle"; - public string Arguments => GetValues(Mono, (Mono == 1).ToString()); - - public int Mono { get; set; } - } - internal class NoteComand : SDATCommand, ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Note"; - public string Arguments => $"{Util.Utils.GetNoteName(Key)}, {Velocity}, {GetValues(Duration, Duration.ToString())}"; - - public byte Key { get; set; } - public byte Velocity { get; set; } - public int Duration { get; set; } - } - internal class OpenTrackCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Open Track"; - public string Arguments => $"{Track}, 0x{Offset:X4}"; - - public int Track { get; set; } - public int Offset { get; set; } - } - internal class PanpotCommand : SDATCommand, ICommand - { - public Color Color => Color.GreenYellow; - public string Label => "Panpot"; - public string Arguments => GetValues(Panpot, Panpot.ToString()); - - public int Panpot { get; set; } - } - internal class PitchBendCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Pitch Bend"; - public string Arguments => GetValues(Bend, Bend.ToString()); - - public int Bend { get; set; } - } - internal class PitchBendRangeCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Pitch Bend Range"; - public string Arguments => GetValues(Range, Range.ToString()); - - public int Range { get; set; } - } - internal class PlayerVolumeCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Player Volume"; - public string Arguments => GetValues(Volume, Volume.ToString()); - - public int Volume { get; set; } - } - internal class PortamentoControlCommand : SDATCommand, ICommand - { - public Color Color => Color.HotPink; - public string Label => "Portamento Control"; - public string Arguments => GetValues(Portamento, Portamento.ToString()); - - public int Portamento { get; set; } - } - internal class PortamentoToggleCommand : SDATCommand, ICommand - { - public Color Color => Color.HotPink; - public string Label => "Portamento Toggle"; - public string Arguments => GetValues(Portamento, (Portamento == 1).ToString()); - - public int Portamento { get; set; } - } - internal class PortamentoTimeCommand : SDATCommand, ICommand - { - public Color Color => Color.HotPink; - public string Label => "Portamento Time"; - public string Arguments => GetValues(Time, Time.ToString()); - - public int Time { get; set; } - } - internal class PriorityCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Priority"; - public string Arguments => GetValues(Priority, Priority.ToString()); - - public int Priority { get; set; } - } - internal class RestCommand : SDATCommand, ICommand - { - public Color Color => Color.PaleVioletRed; - public string Label => "Rest"; - public string Arguments => GetValues(Rest, Rest.ToString()); - - public int Rest { get; set; } - } - internal class ReturnCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Return"; - public string Arguments => string.Empty; - } - internal class SweepPitchCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Sweep Pitch"; - public string Arguments => GetValues(Pitch, Pitch.ToString()); - - public int Pitch { get; set; } - } - internal class TempoCommand : SDATCommand, ICommand - { - public Color Color => Color.DeepSkyBlue; - public string Label => "Tempo"; - public string Arguments => GetValues(Tempo, Tempo.ToString()); - - public int Tempo { get; set; } - } - internal class TieCommand : SDATCommand, ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Tie"; - public string Arguments => GetValues(Tie, (Tie == 1).ToString()); - - public int Tie { get; set; } - } - internal class TrackExpressionCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Track Expression"; - public string Arguments => GetValues(Expression, Expression.ToString()); - - public int Expression { get; set; } - } - internal class TrackVolumeCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Track Volume"; - public string Arguments => GetValues(Volume, Volume.ToString()); - - public int Volume { get; set; } - } - internal class TransposeCommand : SDATCommand, ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Transpose"; - public string Arguments => GetValues(Transpose, Transpose.ToString()); - - public int Transpose { get; set; } - } - internal class VarAddCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Add"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarCmpEECommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var =="; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarCmpGECommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var >="; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarCmpGGCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var >"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarCmpLECommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var <="; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarCmpLLCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var <"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarCmpNECommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var !="; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarDivCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Div"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarMulCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Mul"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarPrintCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Print"; - public string Arguments => GetValues(Variable, Variable.ToString()); - - public int Variable { get; set; } - } - internal class VarRandCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Rand"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarSetCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Set"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarShiftCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Shift"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarSubCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Sub"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VoiceCommand : SDATCommand, ICommand - { - public Color Color => Color.DarkSalmon; - public string Label => "Voice"; - public string Arguments => GetValues(Voice, Voice.ToString()); - - public int Voice { get; set; } - } -} diff --git a/VG Music Studio/Core/NDS/SDAT/Config.cs b/VG Music Studio/Core/NDS/SDAT/Config.cs deleted file mode 100644 index f7f52813..00000000 --- a/VG Music Studio/Core/NDS/SDAT/Config.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Kermalis.VGMusicStudio.Properties; -using System; -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class Config : Core.Config - { - public readonly SDAT SDAT; - - public Config(SDAT sdat) - { - if (sdat.INFOBlock.SequenceInfos.NumEntries == 0) - { - throw new Exception(Strings.ErrorSDATNoSequences); - } - SDAT = sdat; - var songs = new List(sdat.INFOBlock.SequenceInfos.NumEntries); - for (int i = 0; i < sdat.INFOBlock.SequenceInfos.NumEntries; i++) - { - if (sdat.INFOBlock.SequenceInfos.Entries[i] != null) - { - songs.Add(new Song(i, sdat.SYMBBlock is null ? i.ToString() : sdat.SYMBBlock.SequenceSymbols.Entries[i])); - } - } - Playlists.Add(new Playlist(Strings.PlaylistMusic, songs)); - } - - public override string GetGameName() - { - return "SDAT"; - } - public override string GetSongName(long index) - { - return SDAT.SYMBBlock is null || index < 0 || index >= SDAT.SYMBBlock.SequenceSymbols.NumEntries - ? index.ToString() - : '\"' + SDAT.SYMBBlock.SequenceSymbols.Entries[index] + '\"'; - } - } -} diff --git a/VG Music Studio/Core/NDS/SDAT/Enums.cs b/VG Music Studio/Core/NDS/SDAT/Enums.cs deleted file mode 100644 index 9f4aa423..00000000 --- a/VG Music Studio/Core/NDS/SDAT/Enums.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal enum EnvelopeState : byte - { - Attack, - Decay, - Sustain, - Release - } - internal enum ArgType : byte - { - None, - Byte, - Short, - VarLen, - Rand, - PlayerVar - } - - internal enum LFOType : byte - { - Pitch, - Volume, - Panpot - } - internal enum InstrumentType : byte - { - PCM = 0x1, - PSG = 0x2, - Noise = 0x3, - Drum = 0x10, - KeySplit = 0x11 - } - internal enum SWAVFormat : byte - { - PCM8, - PCM16, - ADPCM - } -} diff --git a/VG Music Studio/Core/NDS/SDAT/FileHeader.cs b/VG Music Studio/Core/NDS/SDAT/FileHeader.cs deleted file mode 100644 index 50b863e5..00000000 --- a/VG Music Studio/Core/NDS/SDAT/FileHeader.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class FileHeader : IBinarySerializable - { - public string FileType; - public ushort Endianness; - public ushort Version; - public int FileSize; - public ushort HeaderSize; // 16 - public ushort NumBlocks; - - public void Read(EndianBinaryReader er) - { - FileType = er.ReadString(4, false); - er.Endianness = EndianBinaryIO.Endianness.BigEndian; - Endianness = er.ReadUInt16(); - er.Endianness = Endianness == 0xFFFE ? EndianBinaryIO.Endianness.LittleEndian : EndianBinaryIO.Endianness.BigEndian; - Version = er.ReadUInt16(); - FileSize = er.ReadInt32(); - HeaderSize = er.ReadUInt16(); - NumBlocks = er.ReadUInt16(); - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } -} diff --git a/VG Music Studio/Core/NDS/SDAT/Mixer.cs b/VG Music Studio/Core/NDS/SDAT/Mixer.cs deleted file mode 100644 index a42b4e43..00000000 --- a/VG Music Studio/Core/NDS/SDAT/Mixer.cs +++ /dev/null @@ -1,252 +0,0 @@ -using NAudio.Wave; -using System; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class Mixer : Core.Mixer - { - private readonly float _samplesReciprocal; - private readonly int _samplesPerBuffer; - private bool _isFading; - private long _fadeMicroFramesLeft; - private float _fadePos; - private float _fadeStepPerMicroframe; - - public Channel[] Channels; - private readonly BufferedWaveProvider _buffer; - - public Mixer() - { - // The sampling frequency of the mixer is 1.04876 MHz with an amplitude resolution of 24 bits, but the sampling frequency after mixing with PWM modulation is 32.768 kHz with an amplitude resolution of 10 bits. - // - gbatek - // I'm not using either of those because the samples per buffer leads to an overflow eventually - const int sampleRate = 65456; - _samplesPerBuffer = 341; // TODO - _samplesReciprocal = 1f / _samplesPerBuffer; - - Channels = new Channel[0x10]; - for (byte i = 0; i < 0x10; i++) - { - Channels[i] = new Channel(i); - } - - _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) - { - DiscardOnBufferOverflow = true, - BufferLength = _samplesPerBuffer * 64 - }; - Init(_buffer); - } - public override void Dispose() - { - base.Dispose(); - CloseWaveWriter(); - } - - private static readonly int[] _pcmChanOrder = new int[] { 4, 5, 6, 7, 2, 0, 3, 1, 8, 9, 10, 11, 14, 12, 15, 13 }; - private static readonly int[] _psgChanOrder = new int[] { 8, 9, 10, 11, 12, 13 }; - private static readonly int[] _noiseChanOrder = new int[] { 14, 15 }; - public Channel AllocateChannel(InstrumentType type, Track track) - { - int[] allowedChannels; - switch (type) - { - case InstrumentType.PCM: allowedChannels = _pcmChanOrder; break; - case InstrumentType.PSG: allowedChannels = _psgChanOrder; break; - case InstrumentType.Noise: allowedChannels = _noiseChanOrder; break; - default: return null; - } - Channel nChan = null; - for (int i = 0; i < allowedChannels.Length; i++) - { - Channel c = Channels[allowedChannels[i]]; - if (nChan != null && c.Priority >= nChan.Priority && (c.Priority != nChan.Priority || nChan.Volume <= c.Volume)) - { - continue; - } - nChan = c; - } - if (nChan == null || track.Priority < nChan.Priority) - { - return null; - } - return nChan; - } - - public void ChannelTick() - { - for (int i = 0; i < 0x10; i++) - { - Channel chan = Channels[i]; - if (chan.Owner != null) - { - chan.StepEnvelope(); - if (chan.NoteDuration == 0 && !chan.Owner.WaitingForNoteToFinishBeforeContinuingXD) - { - chan.Priority = 1; - chan.State = EnvelopeState.Release; - } - int vol = Utils.SustainTable[chan.NoteVelocity] + chan.Velocity + chan.Owner.GetVolume(); - int pitch = ((chan.Key - chan.BaseKey) << 6) + chan.SweepMain() + chan.Owner.GetPitch(); // "<< 6" is "* 0x40" - int pan = 0; - chan.LFOTick(); - switch (chan.LFOType) - { - case LFOType.Pitch: pitch += chan.LFOParam; break; - case LFOType.Volume: vol += chan.LFOParam; break; - case LFOType.Panpot: pan += chan.LFOParam; break; - } - if (chan.State == EnvelopeState.Release && vol <= -92544) - { - chan.Stop(); - } - else - { - chan.Volume = Utils.GetChannelVolume(vol); - chan.Timer = Utils.GetChannelTimer(chan.BaseTimer, pitch); - int p = chan.StartingPan + chan.Owner.GetPan() + pan; - if (p < -0x40) - { - p = -0x40; - } - else if (p > 0x3F) - { - p = 0x3F; - } - chan.Pan = (sbyte)p; - } - } - } - } - - public void BeginFadeIn() - { - _fadePos = 0f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); - _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; - _isFading = true; - } - public void BeginFadeOut() - { - _fadePos = 1f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); - _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; - _isFading = true; - } - public bool IsFading() - { - return _isFading; - } - public bool IsFadeDone() - { - return _isFading && _fadeMicroFramesLeft == 0; - } - public void ResetFade() - { - _isFading = false; - _fadeMicroFramesLeft = 0; - } - - private WaveFileWriter _waveWriter; - public void CreateWaveWriter(string fileName) - { - _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); - } - public void CloseWaveWriter() - { - _waveWriter?.Dispose(); - } - public void EmulateProcess() - { - for (int i = 0; i < _samplesPerBuffer; i++) - { - for (int j = 0; j < 0x10; j++) - { - Channel chan = Channels[j]; - if (chan.Owner != null) - { - chan.EmulateProcess(); - } - } - } - } - public void Process(bool output, bool recording) - { - float masterStep; - float masterLevel; - if (_isFading && _fadeMicroFramesLeft == 0) - { - masterStep = 0; - masterLevel = 0; - } - else - { - float fromMaster = 1f; - float toMaster = 1f; - if (_fadeMicroFramesLeft > 0) - { - const float scale = 10f / 6f; - fromMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadePos += _fadeStepPerMicroframe; - toMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadeMicroFramesLeft--; - } - masterStep = (toMaster - fromMaster) * _samplesReciprocal; - masterLevel = fromMaster; - } - byte[] b = new byte[4]; - for (int i = 0; i < _samplesPerBuffer; i++) - { - int left = 0, - right = 0; - for (int j = 0; j < 0x10; j++) - { - Channel chan = Channels[j]; - if (chan.Owner != null) - { - bool muted = Mutes[chan.Owner.Index]; // Get mute first because chan.Process() can call chan.Stop() which sets chan.Owner to null - chan.Process(out short channelLeft, out short channelRight); - if (!muted) - { - left += channelLeft; - right += channelRight; - } - } - } - float f = left * masterLevel; - if (f < short.MinValue) - { - f = short.MinValue; - } - else if (f > short.MaxValue) - { - f = short.MaxValue; - } - left = (int)f; - b[0] = (byte)left; - b[1] = (byte)(left >> 8); - f = right * masterLevel; - if (f < short.MinValue) - { - f = short.MinValue; - } - else if (f > short.MaxValue) - { - f = short.MaxValue; - } - right = (int)f; - b[2] = (byte)right; - b[3] = (byte)(right >> 8); - masterLevel += masterStep; - if (output) - { - _buffer.AddSamples(b, 0, 4); - } - if (recording) - { - _waveWriter.Write(b, 0, 4); - } - } - } - } -} diff --git a/VG Music Studio/Core/NDS/SDAT/Player.cs b/VG Music Studio/Core/NDS/SDAT/Player.cs deleted file mode 100644 index 2722786f..00000000 --- a/VG Music Studio/Core/NDS/SDAT/Player.cs +++ /dev/null @@ -1,1680 +0,0 @@ -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class Player : IPlayer - { - public readonly byte Priority = 0x40; - private readonly short[] _vars = new short[0x20]; // 16 player variables, then 16 global variables - private readonly Track[] _tracks = new Track[0x10]; - private readonly Mixer _mixer; - private readonly Config _config; - private readonly TimeBarrier _time; - private Thread _thread; - private int _randSeed; - private Random _rand; - private SDAT.INFO.SequenceInfo _seqInfo; - private SSEQ _sseq; - private SBNK _sbnk; - public byte Volume; - private ushort _tempo; - private int _tempoStack; - private long _elapsedLoops; - - public List[] Events { get; private set; } - public long MaxTicks { get; private set; } - public long ElapsedTicks { get; private set; } - public bool ShouldFadeOut { get; set; } - public long NumLoops { get; set; } - private int _longestTrack; - - public PlayerState State { get; private set; } - public event SongEndedEvent SongEnded; - - public Player(Mixer mixer, Config config) - { - for (byte i = 0; i < 0x10; i++) - { - _tracks[i] = new Track(i, this); - } - _mixer = mixer; - _config = config; - - _time = new TimeBarrier(192); - } - private void CreateThread() - { - _thread = new Thread(Tick) { Name = "SDAT Player Tick" }; - _thread.Start(); - } - private void WaitThread() - { - if (_thread != null && (_thread.ThreadState == ThreadState.Running || _thread.ThreadState == ThreadState.WaitSleepJoin)) - { - _thread.Join(); - } - } - - private void InitEmulation() - { - _tempo = 120; // Confirmed: default tempo is 120 (MKDS 75) - _tempoStack = 0; - _elapsedLoops = 0; - ElapsedTicks = 0; - _mixer.ResetFade(); - Volume = _seqInfo.Volume; - _rand = new Random(_randSeed); - for (int i = 0; i < 0x10; i++) - { - _tracks[i].Init(); - } - // Initialize player and global variables. Global variables should not have a global effect in this program. - for (int i = 0; i < 0x20; i++) - { - _vars[i] = i % 8 == 0 ? short.MaxValue : (short)0; - } - } - private void SetTicks() - { - // TODO: (NSMB 81) (Spirit Tracks 18) does not count all ticks because the songs keep jumping backwards while changing vars and then using ModIfCommand to change events - MaxTicks = 0; - for (int i = 0; i < 0x10; i++) - { - if (Events[i] != null) - { - Events[i] = Events[i].OrderBy(e => e.Offset).ToList(); - } - } - InitEmulation(); - bool[] done = new bool[0x10]; // We use this instead of track.Stopped just to be certain that emulating Monophony works as intended - while (_tracks.Any(t => t.Allocated && t.Enabled && !done[t.Index])) - { - while (_tempoStack >= 240) - { - _tempoStack -= 240; - for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) - { - Track track = _tracks[trackIndex]; - List evs = Events[trackIndex]; - if (track.Enabled && !track.Stopped) - { - track.Tick(); - while (track.Rest == 0 && !track.WaitingForNoteToFinishBeforeContinuingXD && !track.Stopped) - { - SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); - ExecuteNext(track); - if (!done[trackIndex]) - { - e.Ticks.Add(ElapsedTicks); - bool b; - if (track.Stopped) - { - b = true; - } - else - { - SongEvent newE = evs.Single(ev => ev.Offset == track.DataOffset); - b = (track.CallStackDepth == 0 && newE.Ticks.Count > 0) // If we already counted the tick of this event and we're not looping/calling - || (track.CallStackDepth != 0 && track.CallStackLoops.All(l => l == 0) && newE.Ticks.Count > 0); // If we have "LoopStart (0)" and already counted the tick of this event - } - if (b) - { - done[trackIndex] = true; - if (ElapsedTicks > MaxTicks) - { - _longestTrack = trackIndex; - MaxTicks = ElapsedTicks; - } - } - } - } - } - } - ElapsedTicks++; - } - _tempoStack += _tempo; - _mixer.ChannelTick(); - _mixer.EmulateProcess(); - } - for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) - { - _tracks[trackIndex].StopAllChannels(); - } - } - public void LoadSong(long index) - { - Stop(); - SDAT.INFO.SequenceInfo oldSeqInfo = _seqInfo; - _seqInfo = _config.SDAT.INFOBlock.SequenceInfos.Entries[index]; - if (_seqInfo == null) - { - _sseq = null; - _sbnk = null; - Events = null; - } - else - { - if (oldSeqInfo == null || _seqInfo.Bank != oldSeqInfo.Bank) - { - _voiceTypeCache = new string[byte.MaxValue + 1]; - } - _sseq = new SSEQ(_config.SDAT.FATBlock.Entries[_seqInfo.FileId].Data); - SDAT.INFO.BankInfo bankInfo = _config.SDAT.INFOBlock.BankInfos.Entries[_seqInfo.Bank]; - _sbnk = new SBNK(_config.SDAT.FATBlock.Entries[bankInfo.FileId].Data); - for (int i = 0; i < 4; i++) - { - if (bankInfo.SWARs[i] != 0xFFFF) - { - _sbnk.SWARs[i] = new SWAR(_config.SDAT.FATBlock.Entries[_config.SDAT.INFOBlock.WaveArchiveInfos.Entries[bankInfo.SWARs[i]].FileId].Data); - } - } - _randSeed = new Random().Next(); - - // RECURSION INCOMING - Events = new List[0x10]; - AddTrackEvents(0, 0); - void AddTrackEvents(int i, int trackStartOffset) - { - if (Events[i] == null) - { - Events[i] = new List(); - } - int callStackDepth = 0; - AddEvents(trackStartOffset); - bool EventExists(long offset) - { - return Events[i].Any(e => e.Offset == offset); - } - void AddEvents(int startOffset) - { - int dataOffset = startOffset; - int ReadArg(ArgType type) - { - switch (type) - { - case ArgType.Byte: - { - return _sseq.Data[dataOffset++]; - } - case ArgType.Short: - { - return _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8); - } - case ArgType.VarLen: - { - int read = 0, value = 0; - byte b; - do - { - b = _sseq.Data[dataOffset++]; - value = (value << 7) | (b & 0x7F); - read++; - } - while (read < 4 && (b & 0x80) != 0); - return value; - } - case ArgType.Rand: - { - // Combine min and max into one int - return _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16) | (_sseq.Data[dataOffset++] << 24); - } - case ArgType.PlayerVar: - { - // Return var index - return _sseq.Data[dataOffset++]; - } - default: throw new Exception(); - } - } - bool cont = true; - while (cont) - { - bool @if = false; - int offset = dataOffset; - ArgType argOverrideType = ArgType.None; - again: - byte cmd = _sseq.Data[dataOffset++]; - void AddEvent(T command) where T : SDATCommand, ICommand - { - command.RandMod = argOverrideType == ArgType.Rand; - command.VarMod = argOverrideType == ArgType.PlayerVar; - Events[i].Add(new SongEvent(offset, command)); - } - void Invalid() - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, i, offset, cmd)); - } - - if (cmd <= 0x7F) - { - byte velocity = _sseq.Data[dataOffset++]; - int duration = ReadArg(argOverrideType == ArgType.None ? ArgType.VarLen : argOverrideType); - if (!EventExists(offset)) - { - AddEvent(new NoteComand { Key = cmd, Velocity = velocity, Duration = duration }); - } - } - else - { - int cmdGroup = cmd & 0xF0; - if (cmdGroup == 0x80) - { - int arg = ReadArg(argOverrideType == ArgType.None ? ArgType.VarLen : argOverrideType); - switch (cmd) - { - case 0x80: - { - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = arg }); - } - break; - } - case 0x81: // RAND PROGRAM: [BW2 (2249)] - { - if (!EventExists(offset)) - { - AddEvent(new VoiceCommand { Voice = arg }); // TODO: Bank change - } - break; - } - default: Invalid(); break; - } - } - else if (cmdGroup == 0x90) - { - switch (cmd) - { - case 0x93: - { - int trackIndex = _sseq.Data[dataOffset++]; - int offset24bit = _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16); - if (!EventExists(offset)) - { - AddEvent(new OpenTrackCommand { Track = trackIndex, Offset = offset24bit }); - AddTrackEvents(trackIndex, offset24bit); - } - break; - } - case 0x94: - { - int offset24bit = _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16); - if (!EventExists(offset)) - { - AddEvent(new JumpCommand { Offset = offset24bit }); - if (!EventExists(offset24bit)) - { - AddEvents(offset24bit); - } - } - if (!@if) - { - cont = false; - } - break; - } - case 0x95: - { - int offset24bit = _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16); - if (!EventExists(offset)) - { - AddEvent(new CallCommand { Offset = offset24bit }); - } - if (callStackDepth < 3) - { - if (!EventExists(offset24bit)) - { - callStackDepth++; - AddEvents(offset24bit); - } - } - else - { - throw new Exception(string.Format(Strings.ErrorMP2KSDATNestedCalls, i)); - } - break; - } - default: Invalid(); break; - } - } - else if (cmdGroup == 0xA0) - { - switch (cmd) - { - case 0xA0: // [New Super Mario Bros (BGM_AMB_CHIKA)] [BW2 (1917, 1918)] - { - if (!EventExists(offset)) - { - AddEvent(new ModRandCommand()); - } - argOverrideType = ArgType.Rand; - offset++; - goto again; - } - case 0xA1: // [New Super Mario Bros (BGM_AMB_SABAKU)] - { - if (!EventExists(offset)) - { - AddEvent(new ModVarCommand()); - } - argOverrideType = ArgType.PlayerVar; - offset++; - goto again; - } - case 0xA2: // [Mario Kart DS (75)] [BW2 (1917, 1918)] - { - if (!EventExists(offset)) - { - AddEvent(new ModIfCommand()); - } - @if = true; - offset++; - goto again; - } - default: Invalid(); break; - } - } - else if (cmdGroup == 0xB0) - { - byte varIndex = _sseq.Data[dataOffset++]; - int arg = ReadArg(argOverrideType == ArgType.None ? ArgType.Short : argOverrideType); - switch (cmd) - { - case 0xB0: - { - if (!EventExists(offset)) - { - AddEvent(new VarSetCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB1: - { - if (!EventExists(offset)) - { - AddEvent(new VarAddCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB2: - { - if (!EventExists(offset)) - { - AddEvent(new VarSubCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB3: - { - if (!EventExists(offset)) - { - AddEvent(new VarMulCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB4: - { - if (!EventExists(offset)) - { - AddEvent(new VarDivCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB5: - { - if (!EventExists(offset)) - { - AddEvent(new VarShiftCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB6: // [Mario Kart DS (75)] - { - if (!EventExists(offset)) - { - AddEvent(new VarRandCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB8: - { - if (!EventExists(offset)) - { - AddEvent(new VarCmpEECommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB9: - { - if (!EventExists(offset)) - { - AddEvent(new VarCmpGECommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xBA: - { - if (!EventExists(offset)) - { - AddEvent(new VarCmpGGCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xBB: - { - if (!EventExists(offset)) - { - AddEvent(new VarCmpLECommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xBC: - { - if (!EventExists(offset)) - { - AddEvent(new VarCmpLLCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xBD: - { - if (!EventExists(offset)) - { - AddEvent(new VarCmpNECommand { Variable = varIndex, Argument = arg }); - } - break; - } - default: Invalid(); break; - } - } - else if (cmdGroup == 0xC0 || cmdGroup == 0xD0) - { - int arg = ReadArg(argOverrideType == ArgType.None ? ArgType.Byte : argOverrideType); - switch (cmd) - { - case 0xC0: - { - if (!EventExists(offset)) - { - AddEvent(new PanpotCommand { Panpot = arg }); - } - break; - } - case 0xC1: - { - if (!EventExists(offset)) - { - AddEvent(new TrackVolumeCommand { Volume = arg }); - } - break; - } - case 0xC2: - { - if (!EventExists(offset)) - { - AddEvent(new PlayerVolumeCommand { Volume = arg }); - } - break; - } - case 0xC3: - { - if (!EventExists(offset)) - { - AddEvent(new TransposeCommand { Transpose = arg }); - } - break; - } - case 0xC4: - { - if (!EventExists(offset)) - { - AddEvent(new PitchBendCommand { Bend = arg }); - } - break; - } - case 0xC5: - { - if (!EventExists(offset)) - { - AddEvent(new PitchBendRangeCommand { Range = arg }); - } - break; - } - case 0xC6: - { - if (!EventExists(offset)) - { - AddEvent(new PriorityCommand { Priority = arg }); - } - break; - } - case 0xC7: - { - if (!EventExists(offset)) - { - AddEvent(new MonophonyCommand { Mono = arg }); - } - break; - } - case 0xC8: - { - if (!EventExists(offset)) - { - AddEvent(new TieCommand { Tie = arg }); - } - break; - } - case 0xC9: - { - if (!EventExists(offset)) - { - AddEvent(new PortamentoControlCommand { Portamento = arg }); - } - break; - } - case 0xCA: - { - if (!EventExists(offset)) - { - AddEvent(new LFODepthCommand { Depth = arg }); - } - break; - } - case 0xCB: - { - if (!EventExists(offset)) - { - AddEvent(new LFOSpeedCommand { Speed = arg }); - } - break; - } - case 0xCC: - { - if (!EventExists(offset)) - { - AddEvent(new LFOTypeCommand { Type = arg }); - } - break; - } - case 0xCD: - { - if (!EventExists(offset)) - { - AddEvent(new LFORangeCommand { Range = arg }); - } - break; - } - case 0xCE: - { - if (!EventExists(offset)) - { - AddEvent(new PortamentoToggleCommand { Portamento = arg }); - } - break; - } - case 0xCF: - { - if (!EventExists(offset)) - { - AddEvent(new PortamentoTimeCommand { Time = arg }); - } - break; - } - case 0xD0: - { - if (!EventExists(offset)) - { - AddEvent(new ForceAttackCommand { Attack = arg }); - } - break; - } - case 0xD1: - { - if (!EventExists(offset)) - { - AddEvent(new ForceDecayCommand { Decay = arg }); - } - break; - } - case 0xD2: - { - if (!EventExists(offset)) - { - AddEvent(new ForceSustainCommand { Sustain = arg }); - } - break; - } - case 0xD3: - { - if (!EventExists(offset)) - { - AddEvent(new ForceReleaseCommand { Release = arg }); - } - break; - } - case 0xD4: - { - if (!EventExists(offset)) - { - AddEvent(new LoopStartCommand { NumLoops = arg }); - } - break; - } - case 0xD5: - { - if (!EventExists(offset)) - { - AddEvent(new TrackExpressionCommand { Expression = arg }); - } - break; - } - case 0xD6: - { - if (!EventExists(offset)) - { - AddEvent(new VarPrintCommand { Variable = arg }); - } - break; - } - default: Invalid(); break; - } - } - else if (cmdGroup == 0xE0) - { - int arg = ReadArg(argOverrideType == ArgType.None ? ArgType.Short : argOverrideType); - switch (cmd) - { - case 0xE0: - { - if (!EventExists(offset)) - { - AddEvent(new LFODelayCommand { Delay = arg }); - } - break; - } - case 0xE1: - { - if (!EventExists(offset)) - { - AddEvent(new TempoCommand { Tempo = arg }); - } - break; - } - case 0xE3: - { - if (!EventExists(offset)) - { - AddEvent(new SweepPitchCommand { Pitch = arg }); - } - break; - } - default: Invalid(); break; - } - } - else // if (cmdGroup == 0xF0) - { - switch (cmd) - { - case 0xFC: // [HGSS(1353)] - { - if (!EventExists(offset)) - { - AddEvent(new LoopEndCommand()); - } - break; - } - case 0xFD: - { - if (!EventExists(offset)) - { - AddEvent(new ReturnCommand()); - } - if (!@if && callStackDepth != 0) - { - cont = false; - callStackDepth--; - } - break; - } - case 0xFE: - { - ushort bits = (ushort)ReadArg(ArgType.Short); - if (!EventExists(offset)) - { - AddEvent(new AllocTracksCommand { Tracks = bits }); - } - break; - } - case 0xFF: - { - if (!EventExists(offset)) - { - AddEvent(new FinishCommand()); - } - if (!@if) - { - cont = false; - } - break; - } - default: Invalid(); break; - } - } - } - } - } - } - SetTicks(); - } - } - public void SetCurrentPosition(long ticks) - { - if (_seqInfo == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - if (State == PlayerState.Playing) - { - Pause(); - } - InitEmulation(); - while (true) - { - if (ElapsedTicks == ticks) - { - goto finish; - } - else - { - while (_tempoStack >= 240) - { - _tempoStack -= 240; - for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) - { - Track track = _tracks[trackIndex]; - if (track.Enabled && !track.Stopped) - { - track.Tick(); - while (track.Rest == 0 && !track.WaitingForNoteToFinishBeforeContinuingXD && !track.Stopped) - { - ExecuteNext(track); - } - } - } - ElapsedTicks++; - if (ElapsedTicks == ticks) - { - goto finish; - } - } - _tempoStack += _tempo; - _mixer.ChannelTick(); - _mixer.EmulateProcess(); - } - } - finish: - for (int i = 0; i < 0x10; i++) - { - _tracks[i].StopAllChannels(); - } - Pause(); - } - } - public void Play() - { - if (_seqInfo == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - Stop(); - InitEmulation(); - State = PlayerState.Playing; - CreateThread(); - } - } - public void Pause() - { - if (State == PlayerState.Playing) - { - State = PlayerState.Paused; - WaitThread(); - } - else if (State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.Playing; - CreateThread(); - } - } - public void Stop() - { - if (State == PlayerState.Playing || State == PlayerState.Paused) - { - State = PlayerState.Stopped; - WaitThread(); - } - } - public void Record(string fileName) - { - _mixer.CreateWaveWriter(fileName); - InitEmulation(); - State = PlayerState.Recording; - CreateThread(); - WaitThread(); - _mixer.CloseWaveWriter(); - } - public void Dispose() - { - if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.ShutDown; - WaitThread(); - } - } - private string[] _voiceTypeCache; - public void GetSongState(UI.SongInfoControl.SongInfo info) - { - info.Tempo = _tempo; - for (int i = 0; i < 0x10; i++) - { - Track track = _tracks[i]; - if (track.Enabled) - { - UI.SongInfoControl.SongInfo.Track tin = info.Tracks[i]; - tin.Position = track.DataOffset; - tin.Rest = track.Rest; - tin.Voice = track.Voice; - tin.LFO = track.LFODepth * track.LFORange; - if (_voiceTypeCache[track.Voice] == null) - { - if (_sbnk.NumInstruments <= track.Voice) - { - _voiceTypeCache[track.Voice] = "Empty"; - } - else - { - InstrumentType t = _sbnk.Instruments[track.Voice].Type; - switch (t) - { - case InstrumentType.PCM: _voiceTypeCache[track.Voice] = "PCM"; break; - case InstrumentType.PSG: _voiceTypeCache[track.Voice] = "PSG"; break; - case InstrumentType.Noise: _voiceTypeCache[track.Voice] = "Noise"; break; - case InstrumentType.Drum: _voiceTypeCache[track.Voice] = "Drum"; break; - case InstrumentType.KeySplit: _voiceTypeCache[track.Voice] = "Key Split"; break; - default: _voiceTypeCache[track.Voice] = string.Format("Invalid {0}", (byte)t); break; - } - } - } - tin.Type = _voiceTypeCache[track.Voice]; - tin.Volume = track.Volume; - tin.PitchBend = track.GetPitch(); - tin.Extra = track.Portamento ? track.PortamentoTime : (byte)0; - tin.Panpot = track.GetPan(); - - Channel[] channels = track.Channels.ToArray(); - if (channels.Length == 0) - { - tin.Keys[0] = byte.MaxValue; - tin.LeftVolume = 0f; - tin.RightVolume = 0f; - } - else - { - int numKeys = 0; - float left = 0f; - float right = 0f; - for (int j = 0; j < channels.Length; j++) - { - Channel c = channels[j]; - if (c.State != EnvelopeState.Release) - { - tin.Keys[numKeys++] = c.Key; - } - float a = (float)(-c.Pan + 0x40) / 0x80 * c.Volume / 0x7F; - if (a > left) - { - left = a; - } - a = (float)(c.Pan + 0x40) / 0x80 * c.Volume / 0x7F; - if (a > right) - { - right = a; - } - } - tin.Keys[numKeys] = byte.MaxValue; // There's no way for numKeys to be after the last index in the array - tin.LeftVolume = left; - tin.RightVolume = right; - } - } - } - } - - public void PlayNote(Track track, byte key, byte velocity, int duration) - { - Channel channel = null; - if (track.Tie && track.Channels.Count != 0) - { - channel = track.Channels.Last(); - channel.Key = key; - channel.NoteVelocity = velocity; - } - else - { - SBNK.InstrumentData inst = _sbnk.GetInstrumentData(track.Voice, key); - if (inst != null) - { - InstrumentType type = inst.Type; - channel = _mixer.AllocateChannel(type, track); - if (channel != null) - { - if (track.Tie) - { - duration = -1; - } - SBNK.InstrumentData.DataParam param = inst.Param; - byte release = param.Release; - if (release == 0xFF) - { - duration = -1; - release = 0; - } - bool started = false; - switch (type) - { - case InstrumentType.PCM: - { - ushort[] info = param.Info; - SWAR.SWAV swav = _sbnk.GetSWAV(info[1], info[0]); - if (swav != null) - { - channel.StartPCM(swav, duration); - started = true; - } - break; - } - case InstrumentType.PSG: - { - channel.StartPSG((byte)param.Info[0], duration); - started = true; - break; - } - case InstrumentType.Noise: - { - channel.StartNoise(duration); - started = true; - break; - } - } - channel.Stop(); - if (started) - { - channel.Key = key; - byte baseKey = param.BaseKey; - channel.BaseKey = type != InstrumentType.PCM && baseKey == 0x7F ? (byte)60 : baseKey; - channel.NoteVelocity = velocity; - channel.SetAttack(param.Attack); - channel.SetDecay(param.Decay); - channel.SetSustain(param.Sustain); - channel.SetRelease(release); - channel.StartingPan = (sbyte)(param.Pan - 0x40); - channel.Owner = track; - channel.Priority = track.Priority; - track.Channels.Add(channel); - } - else - { - return; - } - } - } - } - if (channel != null) - { - if (track.Attack != 0xFF) - { - channel.SetAttack(track.Attack); - } - if (track.Decay != 0xFF) - { - channel.SetDecay(track.Decay); - } - if (track.Sustain != 0xFF) - { - channel.SetSustain(track.Sustain); - } - if (track.Release != 0xFF) - { - channel.SetRelease(track.Release); - } - channel.SweepPitch = track.SweepPitch; - if (track.Portamento) - { - channel.SweepPitch += (short)((track.PortamentoKey - key) << 6); // "<< 6" is "* 0x40" - } - if (track.PortamentoTime != 0) - { - channel.SweepLength = (track.PortamentoTime * track.PortamentoTime * Math.Abs(channel.SweepPitch)) >> 11; // ">> 11" is "/ 0x800" - channel.AutoSweep = true; - } - else - { - channel.SweepLength = duration; - channel.AutoSweep = false; - } - channel.SweepCounter = 0; - } - } - private void ExecuteNext(Track track) - { - int ReadArg(ArgType type) - { - if (track.ArgOverrideType != ArgType.None) - { - type = track.ArgOverrideType; - } - switch (type) - { - case ArgType.Byte: - { - return _sseq.Data[track.DataOffset++]; - } - case ArgType.Short: - { - return _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8); - } - case ArgType.VarLen: - { - int read = 0, value = 0; - byte b; - do - { - b = _sseq.Data[track.DataOffset++]; - value = (value << 7) | (b & 0x7F); - read++; - } - while (read < 4 && (b & 0x80) != 0); - return value; - } - case ArgType.Rand: - { - short min = (short)(_sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8)); - short max = (short)(_sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8)); - return _rand.Next(min, max + 1); - } - case ArgType.PlayerVar: - { - byte varIndex = _sseq.Data[track.DataOffset++]; - return _vars[varIndex]; - } - default: throw new Exception(); - } - } - - bool resetOverride = true; - bool resetCmdWork = true; - byte cmd = _sseq.Data[track.DataOffset++]; - if (cmd < 0x80) // Notes - { - byte velocity = _sseq.Data[track.DataOffset++]; - int duration = ReadArg(ArgType.VarLen); - if (track.DoCommandWork) - { - int k = cmd + track.Transpose; - if (k < 0) - { - k = 0; - } - else if (k > 0x7F) - { - k = 0x7F; - } - byte key = (byte)k; - PlayNote(track, key, velocity, duration); - track.PortamentoKey = key; - if (track.Mono) - { - track.Rest = duration; - if (duration == 0) - { - track.WaitingForNoteToFinishBeforeContinuingXD = true; - } - } - } - } - else - { - int cmdGroup = cmd & 0xF0; - switch (cmdGroup) - { - case 0x80: - { - int arg = ReadArg(ArgType.VarLen); - if (track.DoCommandWork) - { - switch (cmd) - { - case 0x80: // Rest - { - track.Rest = arg; - break; - } - case 0x81: // Program Change - { - if (arg <= byte.MaxValue) - { - track.Voice = (byte)arg; - } - break; - } - } - } - break; - } - case 0x90: - { - switch (cmd) - { - case 0x93: // Open Track - { - int index = _sseq.Data[track.DataOffset++]; - int offset24bit = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8) | (_sseq.Data[track.DataOffset++] << 16); - if (track.DoCommandWork && track.Index == 0) - { - Track other = _tracks[index]; - if (other.Allocated && !other.Enabled) - { - other.Enabled = true; - other.DataOffset = offset24bit; - } - } - break; - } - case 0x94: // Jump - { - int offset24bit = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8) | (_sseq.Data[track.DataOffset++] << 16); - if (track.DoCommandWork) - { - track.DataOffset = offset24bit; - } - break; - } - case 0x95: // Call - { - int offset24bit = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8) | (_sseq.Data[track.DataOffset++] << 16); - if (track.DoCommandWork && track.CallStackDepth < 3) - { - track.CallStack[track.CallStackDepth] = track.DataOffset; - track.CallStackLoops[track.CallStackDepth] = byte.MaxValue; // This is only necessary for SetTicks() to deal with LoopStart (0) - track.CallStackDepth++; - track.DataOffset = offset24bit; - } - break; - } - } - break; - } - case 0xA0: - { - if (track.DoCommandWork) - { - switch (cmd) - { - case 0xA0: // Rand Mod - { - track.ArgOverrideType = ArgType.Rand; - resetOverride = false; - break; - } - case 0xA1: // Var Mod - { - track.ArgOverrideType = ArgType.PlayerVar; - resetOverride = false; - break; - } - case 0xA2: // If Mod - { - track.DoCommandWork = track.VariableFlag; - resetCmdWork = false; - break; - } - } - } - break; - } - case 0xB0: - { - byte varIndex = _sseq.Data[track.DataOffset++]; - short mathArg = (short)ReadArg(ArgType.Short); - if (track.DoCommandWork) - { - switch (cmd) - { - case 0xB0: // VarSet - { - _vars[varIndex] = mathArg; - break; - } - case 0xB1: // VarAdd - { - _vars[varIndex] += mathArg; - break; - } - case 0xB2: // VarSub - { - _vars[varIndex] -= mathArg; - break; - } - case 0xB3: // VarMul - { - _vars[varIndex] *= mathArg; - break; - } - case 0xB4: // VarDiv - { - if (mathArg != 0) - { - _vars[varIndex] /= mathArg; - } - break; - } - case 0xB5: // VarShift - { - _vars[varIndex] = mathArg < 0 ? (short)(_vars[varIndex] >> -mathArg) : (short)(_vars[varIndex] << mathArg); - break; - } - case 0xB6: // VarRand - { - bool negate = false; - if (mathArg < 0) - { - negate = true; - mathArg = (short)-mathArg; - } - short val = (short)_rand.Next(mathArg + 1); - if (negate) - { - val = (short)-val; - } - _vars[varIndex] = val; - break; - } - case 0xB8: // VarCmpEE - { - track.VariableFlag = _vars[varIndex] == mathArg; - break; - } - case 0xB9: // VarCmpGE - { - track.VariableFlag = _vars[varIndex] >= mathArg; - break; - } - case 0xBA: // VarCmpGG - { - track.VariableFlag = _vars[varIndex] > mathArg; - break; - } - case 0xBB: // VarCmpLE - { - track.VariableFlag = _vars[varIndex] <= mathArg; - break; - } - case 0xBC: // VarCmpLL - { - track.VariableFlag = _vars[varIndex] < mathArg; - break; - } - case 0xBD: // VarCmpNE - { - track.VariableFlag = _vars[varIndex] != mathArg; - break; - } - } - } - break; - } - case 0xC0: - case 0xD0: - { - int cmdArg = ReadArg(ArgType.Byte); - if (track.DoCommandWork) - { - switch (cmd) - { - case 0xC0: // Panpot - { - track.Panpot = (sbyte)(cmdArg - 0x40); - break; - } - case 0xC1: // Track Volume - { - track.Volume = (byte)cmdArg; - break; - } - case 0xC2: // Player Volume - { - Volume = (byte)cmdArg; - break; - } - case 0xC3: // Transpose - { - track.Transpose = (sbyte)cmdArg; - break; - } - case 0xC4: // Pitch Bend - { - track.PitchBend = (sbyte)cmdArg; - break; - } - case 0xC5: // Pitch Bend Range - { - track.PitchBendRange = (byte)cmdArg; - break; - } - case 0xC6: // Priority - { - track.Priority = (byte)(Priority + (byte)cmdArg); - break; - } - case 0xC7: // Mono - { - track.Mono = cmdArg == 1; - break; - } - case 0xC8: // Tie - { - track.Tie = cmdArg == 1; - track.StopAllChannels(); - break; - } - case 0xC9: // Portamento Control - { - int k = cmdArg + track.Transpose; - if (k < 0) - { - k = 0; - } - else if (k > 0x7F) - { - k = 0x7F; - } - track.PortamentoKey = (byte)k; - track.Portamento = true; - break; - } - case 0xCA: // LFO Depth - { - track.LFODepth = (byte)cmdArg; - break; - } - case 0xCB: // LFO Speed - { - track.LFOSpeed = (byte)cmdArg; - break; - } - case 0xCC: // LFO Type - { - track.LFOType = (LFOType)cmdArg; - break; - } - case 0xCD: // LFO Range - { - track.LFORange = (byte)cmdArg; - break; - } - case 0xCE: // Portamento Toggle - { - track.Portamento = cmdArg == 1; - break; - } - case 0xCF: // Portamento Time - { - track.PortamentoTime = (byte)cmdArg; - break; - } - case 0xD0: // Forced Attack - { - track.Attack = (byte)cmdArg; - break; - } - case 0xD1: // Forced Decay - { - track.Decay = (byte)cmdArg; - break; - } - case 0xD2: // Forced Sustain - { - track.Sustain = (byte)cmdArg; - break; - } - case 0xD3: // Forced Release - { - track.Release = (byte)cmdArg; - break; - } - case 0xD4: // Loop Start - { - if (track.CallStackDepth < 3) - { - track.CallStack[track.CallStackDepth] = track.DataOffset; - track.CallStackLoops[track.CallStackDepth] = (byte)cmdArg; - track.CallStackDepth++; - } - break; - } - case 0xD5: // Track Expression - { - track.Expression = (byte)cmdArg; - break; - } - } - } - break; - } - case 0xE0: - { - int cmdArg = ReadArg(ArgType.Short); - if (track.DoCommandWork) - { - switch (cmd) - { - case 0xE0: // LFO Delay - { - track.LFODelay = (ushort)cmdArg; - break; - } - case 0xE1: // Tempo - { - _tempo = (ushort)cmdArg; - break; - } - case 0xE3: // Sweep Pitch - { - track.SweepPitch = (short)cmdArg; - break; - } - } - } - break; - } - case 0xF0: - { - if (track.DoCommandWork) - { - switch (cmd) - { - case 0xFC: // Loop End - { - if (track.CallStackDepth != 0) - { - byte count = track.CallStackLoops[track.CallStackDepth - 1]; - if (count != 0) - { - count--; - track.CallStackLoops[track.CallStackDepth - 1] = count; - if (count == 0) - { - track.CallStackDepth--; - break; - } - } - track.DataOffset = track.CallStack[track.CallStackDepth - 1]; - } - break; - } - case 0xFD: // Return - { - if (track.CallStackDepth != 0) - { - track.CallStackDepth--; - track.DataOffset = track.CallStack[track.CallStackDepth]; - track.CallStackLoops[track.CallStackDepth] = 0; // This is only necessary for SetTicks() to deal with LoopStart (0) - } - break; - } - case 0xFE: // Alloc Tracks - { - // Must be in the beginning of the first track to work - if (track.Index == 0 && track.DataOffset == 1) // == 1 because we read cmd already - { - // Track 1 enabled = bit 1 set, Track 4 enabled = bit 4 set, etc - int trackBits = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8); - for (int i = 0; i < 0x10; i++) - { - if ((trackBits & (1 << i)) != 0) - { - _tracks[i].Allocated = true; - } - } - } - break; - } - case 0xFF: // Finish - { - track.Stopped = true; - break; - } - } - } - break; - } - } - } - if (resetOverride) - { - track.ArgOverrideType = ArgType.None; - } - if (resetCmdWork) - { - track.DoCommandWork = true; - } - } - - private void Tick() - { - _time.Start(); - while (true) - { - PlayerState state = State; - bool playing = state == PlayerState.Playing; - bool recording = state == PlayerState.Recording; - if (!playing && !recording) - { - goto stop; - } - - void MixerProcess() - { - for (int i = 0; i < 0x10; i++) - { - Track track = _tracks[i]; - if (track.Enabled) - { - track.UpdateChannels(); - } - } - _mixer.ChannelTick(); - _mixer.Process(playing, recording); - } - - while (_tempoStack >= 240) - { - _tempoStack -= 240; - bool allDone = true; - for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) - { - Track track = _tracks[trackIndex]; - if (!track.Enabled) - { - continue; - } - track.Tick(); - while (track.Rest == 0 && !track.WaitingForNoteToFinishBeforeContinuingXD && !track.Stopped) - { - ExecuteNext(track); - } - if (trackIndex == _longestTrack) - { - if (ElapsedTicks == MaxTicks) - { - if (!track.Stopped) - { - List evs = Events[trackIndex]; - for (int i = 0; i < evs.Count; i++) - { - SongEvent ev = evs[i]; - if (ev.Offset == track.DataOffset) - { - //ElapsedTicks = ev.Ticks[0] - track.Rest; - ElapsedTicks = ev.Ticks.Count == 0 ? 0 : ev.Ticks[0] - track.Rest; // Prevent crashes with songs that don't load all ticks yet (See SetTicks()) - break; - } - } - _elapsedLoops++; - if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) - { - _mixer.BeginFadeOut(); - } - } - } - else - { - ElapsedTicks++; - } - } - if (!track.Stopped || track.Channels.Count != 0) - { - allDone = false; - } - } - if (_mixer.IsFadeDone()) - { - allDone = true; - } - if (allDone) - { - MixerProcess(); - State = PlayerState.Stopped; - SongEnded?.Invoke(); - goto stop; - } - } - _tempoStack += _tempo; - MixerProcess(); - if (playing) - { - _time.Wait(); - } - } - stop: - _time.Stop(); - } - } -} diff --git a/VG Music Studio/Core/NDS/SDAT/SBNK.cs b/VG Music Studio/Core/NDS/SDAT/SBNK.cs deleted file mode 100644 index c0b016d8..00000000 --- a/VG Music Studio/Core/NDS/SDAT/SBNK.cs +++ /dev/null @@ -1,184 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class SBNK - { - public class InstrumentData - { - public class DataParam - { - [BinaryArrayFixedLength(2)] - public ushort[] Info { get; set; } - public byte BaseKey { get; set; } - public byte Attack { get; set; } - public byte Decay { get; set; } - public byte Sustain { get; set; } - public byte Release { get; set; } - public byte Pan { get; set; } - } - - public InstrumentType Type { get; set; } - public byte Padding { get; set; } - public DataParam Param { get; set; } - } - public class Instrument : IBinarySerializable - { - public class DefaultData - { - public InstrumentData.DataParam Param { get; set; } - } - public class DrumSetData : IBinarySerializable - { - public byte MinNote; - public byte MaxNote; - public InstrumentData[] SubInstruments; - - public void Read(EndianBinaryReader er) - { - MinNote = er.ReadByte(); - MaxNote = er.ReadByte(); - SubInstruments = new InstrumentData[MaxNote - MinNote + 1]; - for (int i = 0; i < SubInstruments.Length; i++) - { - SubInstruments[i] = er.ReadObject(); - } - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } - public class KeySplitData : IBinarySerializable - { - public byte[] KeyRegions; - public InstrumentData[] SubInstruments; - - public void Read(EndianBinaryReader er) - { - KeyRegions = er.ReadBytes(8); - int numSubInstruments = 0; - for (int i = 0; i < 8; i++) - { - if (KeyRegions[i] == 0) - { - break; - } - numSubInstruments++; - } - SubInstruments = new InstrumentData[numSubInstruments]; - for (int i = 0; i < numSubInstruments; i++) - { - SubInstruments[i] = er.ReadObject(); - } - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } - - public InstrumentType Type; - public ushort DataOffset; - public byte Padding; - - public object Data; - - public void Read(EndianBinaryReader er) - { - Type = er.ReadEnum(); - DataOffset = er.ReadUInt16(); - Padding = er.ReadByte(); - - long p = er.BaseStream.Position; - switch (Type) - { - case InstrumentType.PCM: - case InstrumentType.PSG: - case InstrumentType.Noise: Data = er.ReadObject(DataOffset); break; - case InstrumentType.Drum: Data = er.ReadObject(DataOffset); break; - case InstrumentType.KeySplit: Data = er.ReadObject(DataOffset); break; - default: break; - } - er.BaseStream.Position = p; - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } - - public FileHeader FileHeader { get; set; } // "SBNK" - [BinaryStringFixedLength(4)] - public string BlockType { get; set; } // "DATA" - public int BlockSize { get; set; } - [BinaryArrayFixedLength(32)] - public byte[] Padding { get; set; } - public int NumInstruments { get; set; } - [BinaryArrayVariableLength(nameof(NumInstruments))] - public Instrument[] Instruments { get; set; } - - [BinaryIgnore] - public SWAR[] SWARs { get; } = new SWAR[4]; - - public SBNK(byte[] bytes) - { - using (var er = new EndianBinaryReader(new MemoryStream(bytes))) - { - er.ReadIntoObject(this); - } - } - - public InstrumentData GetInstrumentData(int voice, int key) - { - if (voice >= NumInstruments) - { - return null; - } - else - { - switch (Instruments[voice].Type) - { - case InstrumentType.PCM: - case InstrumentType.PSG: - case InstrumentType.Noise: - { - var d = (Instrument.DefaultData)Instruments[voice].Data; - // TODO: Better way? - return new InstrumentData - { - Type = Instruments[voice].Type, - Param = d.Param - }; - } - case InstrumentType.Drum: - { - var d = (Instrument.DrumSetData)Instruments[voice].Data; - return key < d.MinNote || key > d.MaxNote ? null : d.SubInstruments[key - d.MinNote]; - } - case InstrumentType.KeySplit: - { - var d = (Instrument.KeySplitData)Instruments[voice].Data; - for (int i = 0; i < 8; i++) - { - if (key <= d.KeyRegions[i]) - { - return d.SubInstruments[i]; - } - } - return null; - } - default: return null; - } - } - } - - public SWAR.SWAV GetSWAV(int swarIndex, int swavIndex) - { - SWAR swar = SWARs[swarIndex]; - return swar == null || swavIndex >= swar.NumWaves ? null : swar.Waves[swavIndex]; - } - } -} diff --git a/VG Music Studio/Core/NDS/SDAT/SDAT.cs b/VG Music Studio/Core/NDS/SDAT/SDAT.cs deleted file mode 100644 index 0f4c4c7f..00000000 --- a/VG Music Studio/Core/NDS/SDAT/SDAT.cs +++ /dev/null @@ -1,234 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class SDAT - { - public class SYMB : IBinarySerializable - { - public class Record - { - public int NumEntries; - public int[] EntryOffsets; - - public string[] Entries; - - public Record(EndianBinaryReader er, long baseOffset) - { - NumEntries = er.ReadInt32(); - EntryOffsets = er.ReadInt32s(NumEntries); - - long p = er.BaseStream.Position; - Entries = new string[NumEntries]; - for (int i = 0; i < NumEntries; i++) - { - if (EntryOffsets[i] != 0) - { - Entries[i] = er.ReadStringNullTerminated(baseOffset + EntryOffsets[i]); - } - } - er.BaseStream.Position = p; - } - } - - public string BlockType; // "SYMB" - public int BlockSize; - public int[] RecordOffsets; - public byte[] Padding; - - public Record SequenceSymbols; - //SequenceArchiveSymbols; - public Record BankSymbols; - public Record WaveArchiveSymbols; - //PlayerSymbols; - //GroupSymbols; - //StreamPlayerSymbols; - //StreamSymbols; - - public void Read(EndianBinaryReader er) - { - long baseOffset = er.BaseStream.Position; - BlockType = er.ReadString(4, false); - BlockSize = er.ReadInt32(); - RecordOffsets = er.ReadInt32s(8); - Padding = er.ReadBytes(24); - er.BaseStream.Position = baseOffset + RecordOffsets[0]; - SequenceSymbols = new Record(er, baseOffset); - er.BaseStream.Position = baseOffset + RecordOffsets[2]; - BankSymbols = new Record(er, baseOffset); - er.BaseStream.Position = baseOffset + RecordOffsets[3]; - WaveArchiveSymbols = new Record(er, baseOffset); - er.BaseStream.Position = baseOffset + BlockSize; - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } - - public class INFO : IBinarySerializable - { - public class Record where T : new() - { - public int NumEntries; - public int[] EntryOffsets; - - public T[] Entries; - - public Record(EndianBinaryReader er, long baseOffset) - { - NumEntries = er.ReadInt32(); - EntryOffsets = er.ReadInt32s(NumEntries); - - long p = er.BaseStream.Position; - Entries = new T[NumEntries]; - for (int i = 0; i < NumEntries; i++) - { - if (EntryOffsets[i] != 0) - { - Entries[i] = er.ReadObject(baseOffset + EntryOffsets[i]); - } - } - er.BaseStream.Position = p; - } - } - - public class SequenceInfo - { - public ushort FileId { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } - public ushort Bank { get; set; } - public byte Volume { get; set; } - public byte ChannelPriority { get; set; } - public byte PlayerPriority { get; set; } - public byte PlayerNum { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown2 { get; set; } - } - public class BankInfo - { - public ushort FileId { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown { get; set; } - [BinaryArrayFixedLength(4)] - public ushort[] SWARs { get; set; } - } - public class WaveArchiveInfo - { - public ushort FileId { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown { get; set; } - } - - public string BlockType; // "INFO" - public int BlockSize; - public int[] InfoOffsets; - - public Record SequenceInfos; - //SequenceArchiveInfos; - public Record BankInfos; - public Record WaveArchiveInfos; - //PlayerInfos; - //GroupInfos; - //StreamPlayerInfos; - //StreamInfos; - - public void Read(EndianBinaryReader er) - { - long baseOffset = er.BaseStream.Position; - BlockType = er.ReadString(4, false); - BlockSize = er.ReadInt32(); - InfoOffsets = er.ReadInt32s(8); - er.ReadBytes(24); - er.BaseStream.Position = baseOffset + InfoOffsets[0]; - SequenceInfos = new Record(er, baseOffset); - er.BaseStream.Position = baseOffset + InfoOffsets[2]; - BankInfos = new Record(er, baseOffset); - er.BaseStream.Position = baseOffset + InfoOffsets[3]; - WaveArchiveInfos = new Record(er, baseOffset); - er.BaseStream.Position = baseOffset; - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } - - public class FAT - { - public class FATEntry : IBinarySerializable - { - public int DataOffset; - public int DataLength; - public byte[] Padding; - - public byte[] Data; - - public void Read(EndianBinaryReader er) - { - DataOffset = er.ReadInt32(); - DataLength = er.ReadInt32(); - Padding = er.ReadBytes(8); - - long p = er.BaseStream.Position; - Data = er.ReadBytes(DataLength, DataOffset); - er.BaseStream.Position = p; - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } - - [BinaryStringFixedLength(4)] - public string BlockType { get; set; } // "FAT " - public int BlockSize { get; set; } - public int NumEntries { get; set; } - [BinaryArrayVariableLength(nameof(NumEntries))] - public FATEntry[] Entries { get; set; } - } - - public FileHeader FileHeader; // "SDAT" - public int SYMBOffset; - public int SYMBLength; - public int INFOOffset; - public int INFOLength; - public int FATOffset; - public int FATLength; - public int FILEOffset; - public int FILELength; - public byte[] Padding; - - public SYMB SYMBBlock; - public INFO INFOBlock; - public FAT FATBlock; - //FILEBlock - - public SDAT(byte[] bytes) - { - using (var er = new EndianBinaryReader(new MemoryStream(bytes))) - { - FileHeader = er.ReadObject(); - SYMBOffset = er.ReadInt32(); - SYMBLength = er.ReadInt32(); - INFOOffset = er.ReadInt32(); - INFOLength = er.ReadInt32(); - FATOffset = er.ReadInt32(); - FATLength = er.ReadInt32(); - FILEOffset = er.ReadInt32(); - FILELength = er.ReadInt32(); - Padding = er.ReadBytes(16); - - if (SYMBOffset != 0 && SYMBLength != 0) - { - SYMBBlock = er.ReadObject(SYMBOffset); - } - INFOBlock = er.ReadObject(INFOOffset); - FATBlock = er.ReadObject(FATOffset); - } - } - } -} diff --git a/VG Music Studio/Core/NDS/SDAT/SSEQ.cs b/VG Music Studio/Core/NDS/SDAT/SSEQ.cs deleted file mode 100644 index 3d97b1e1..00000000 --- a/VG Music Studio/Core/NDS/SDAT/SSEQ.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System.IO; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class SSEQ - { - public FileHeader FileHeader; // "SSEQ" - public string BlockType; // "DATA" - public int BlockSize; - public int DataOffset; - - public byte[] Data; - - public SSEQ(byte[] bytes) - { - using (var er = new EndianBinaryReader(new MemoryStream(bytes))) - { - FileHeader = er.ReadObject(); - BlockType = er.ReadString(4, false); - BlockSize = er.ReadInt32(); - DataOffset = er.ReadInt32(); - - Data = er.ReadBytes(FileHeader.FileSize - DataOffset, DataOffset); - } - } - } -} diff --git a/VG Music Studio/Core/NDS/SDAT/SWAR.cs b/VG Music Studio/Core/NDS/SDAT/SWAR.cs deleted file mode 100644 index c7a982c8..00000000 --- a/VG Music Studio/Core/NDS/SDAT/SWAR.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class SWAR - { - public class SWAV : IBinarySerializable - { - public SWAVFormat Format; - public bool DoesLoop; - public ushort SampleRate; - public ushort Timer; // (NDSUtils.ARM7_CLOCK / SampleRate) - public ushort LoopOffset; - public int Length; - - public byte[] Samples; - - public void Read(EndianBinaryReader er) - { - Format = er.ReadEnum(); - DoesLoop = er.ReadBoolean(); - SampleRate = er.ReadUInt16(); - Timer = er.ReadUInt16(); - LoopOffset = er.ReadUInt16(); - Length = er.ReadInt32(); - - Samples = er.ReadBytes((LoopOffset * 4) + (Length * 4)); - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } - - public FileHeader FileHeader; // "SWAR" - public string BlockType; // "DATA" - public int BlockSize; - public byte[] Padding; - public int NumWaves; - public int[] WaveOffsets; - - public SWAV[] Waves; - - public SWAR(byte[] bytes) - { - using (var er = new EndianBinaryReader(new MemoryStream(bytes))) - { - FileHeader = er.ReadObject(); - BlockType = er.ReadString(4, false); - BlockSize = er.ReadInt32(); - Padding = er.ReadBytes(32); - NumWaves = er.ReadInt32(); - WaveOffsets = er.ReadInt32s(NumWaves); - - Waves = new SWAV[NumWaves]; - for (int i = 0; i < NumWaves; i++) - { - Waves[i] = er.ReadObject(WaveOffsets[i]); - } - } - } - } -} diff --git a/VG Music Studio/Core/NDS/SDAT/Track.cs b/VG Music Studio/Core/NDS/SDAT/Track.cs deleted file mode 100644 index 75802b42..00000000 --- a/VG Music Studio/Core/NDS/SDAT/Track.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class Track - { - public readonly byte Index; - private readonly Player _player; - - public bool Allocated; - public bool Enabled; - public bool Stopped; - public bool Tie; - public bool Mono; - public bool Portamento; - public bool WaitingForNoteToFinishBeforeContinuingXD; // Is this necessary? - public byte Voice; - public byte Priority; - public byte Volume; - public byte Expression; - public byte PitchBendRange; - public byte LFORange; - public byte LFOSpeed; - public byte LFODepth; - public ushort LFODelay; - public ushort LFOPhase; - public int LFOParam; - public ushort LFODelayCount; - public LFOType LFOType; - public sbyte PitchBend; - public sbyte Panpot; - public sbyte Transpose; - public byte Attack; - public byte Decay; - public byte Sustain; - public byte Release; - public byte PortamentoKey; - public byte PortamentoTime; - public short SweepPitch; - public int Rest; - public int[] CallStack = new int[3]; - public byte[] CallStackLoops = new byte[3]; - public byte CallStackDepth; - public int DataOffset; - public bool VariableFlag; // Set by variable commands (0xB0 - 0xBD) - public bool DoCommandWork; - public ArgType ArgOverrideType; - - public readonly List Channels = new List(0x10); - - public int GetPitch() - { - //int lfo = LFOType == LFOType.Pitch ? LFOParam : 0; - int lfo = 0; - return (PitchBend * PitchBendRange / 2) + lfo; - } - public int GetVolume() - { - //int lfo = LFOType == LFOType.Volume ? LFOParam : 0; - int lfo = 0; - return Utils.SustainTable[_player.Volume] + Utils.SustainTable[Volume] + Utils.SustainTable[Expression] + lfo; - } - public sbyte GetPan() - { - //int lfo = LFOType == LFOType.Panpot ? LFOParam : 0; - int lfo = 0; - int p = Panpot + lfo; - if (p < -0x40) - { - p = -0x40; - } - else if (p > 0x3F) - { - p = 0x3F; - } - return (sbyte)p; - } - - public Track(byte i, Player player) - { - Index = i; - _player = player; - } - public void Init() - { - Stopped = Tie = WaitingForNoteToFinishBeforeContinuingXD = Portamento = false; - Allocated = Enabled = Index == 0; - DataOffset = 0; - ArgOverrideType = ArgType.None; - Mono = VariableFlag = DoCommandWork = true; - CallStackDepth = 0; - Voice = LFODepth = 0; - PitchBend = Panpot = Transpose = 0; - LFOPhase = LFODelay = LFODelayCount = 0; - LFORange = 1; - LFOSpeed = 0x10; - Priority = (byte)(_player.Priority + 0x40); - Volume = Expression = 0x7F; - Attack = Decay = Sustain = Release = 0xFF; - PitchBendRange = 2; - PortamentoKey = 60; - PortamentoTime = 0; - SweepPitch = 0; - LFOType = LFOType.Pitch; - Rest = 0; - StopAllChannels(); - } - public void LFOTick() - { - if (Channels.Count != 0) - { - if (LFODelayCount > 0) - { - LFODelayCount--; - LFOPhase = 0; - } - else - { - int param = LFORange * Utils.Sin(LFOPhase >> 8) * LFODepth; - if (LFOType == LFOType.Volume) - { - param = (param * 60) >> 14; - } - else - { - param >>= 8; - } - LFOParam = param; - int counter = LFOPhase + (LFOSpeed << 6); // "<< 6" is "* 0x40" - while (counter >= 0x8000) - { - counter -= 0x8000; - } - LFOPhase = (ushort)counter; - } - } - else - { - LFOPhase = 0; - LFOParam = 0; - LFODelayCount = LFODelay; - } - } - public void Tick() - { - if (Rest > 0) - { - Rest--; - } - if (Channels.Count != 0) - { - // TickNotes: - for (int i = 0; i < Channels.Count; i++) - { - Channel c = Channels[i]; - if (c.NoteDuration > 0) - { - c.NoteDuration--; - } - if (!c.AutoSweep && c.SweepCounter < c.SweepLength) - { - c.SweepCounter++; - } - } - } - else - { - WaitingForNoteToFinishBeforeContinuingXD = false; - } - } - public void UpdateChannels() - { - for (int i = 0; i < Channels.Count; i++) - { - Channel c = Channels[i]; - c.LFOType = LFOType; - c.LFOSpeed = LFOSpeed; - c.LFODepth = LFODepth; - c.LFORange = LFORange; - c.LFODelay = LFODelay; - } - } - - public void StopAllChannels() - { - Channel[] chans = Channels.ToArray(); - for (int i = 0; i < chans.Length; i++) - { - chans[i].Stop(); - } - } - } -} diff --git a/VG Music Studio/Core/NDS/SDAT/Utils.cs b/VG Music Studio/Core/NDS/SDAT/Utils.cs deleted file mode 100644 index 289f6012..00000000 --- a/VG Music Studio/Core/NDS/SDAT/Utils.cs +++ /dev/null @@ -1,345 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal static class Utils - { - public static readonly byte[] AttackTable = new byte[128] - { - 255, 254, 253, 252, 251, 250, 249, 248, - 247, 246, 245, 244, 243, 242, 241, 240, - 239, 238, 237, 236, 235, 234, 233, 232, - 231, 230, 229, 228, 227, 226, 225, 224, - 223, 222, 221, 220, 219, 218, 217, 216, - 215, 214, 213, 212, 211, 210, 209, 208, - 207, 206, 205, 204, 203, 202, 201, 200, - 199, 198, 197, 196, 195, 194, 193, 192, - 191, 190, 189, 188, 187, 186, 185, 184, - 183, 182, 181, 180, 179, 178, 177, 176, - 175, 174, 173, 172, 171, 170, 169, 168, - 167, 166, 165, 164, 163, 162, 161, 160, - 159, 158, 157, 156, 155, 154, 153, 152, - 151, 150, 149, 148, 147, 143, 137, 132, - 127, 123, 116, 109, 100, 92, 84, 73, - 63, 51, 38, 26, 14, 5, 1, 0 - }; - public static readonly ushort[] DecayTable = new ushort[128] - { - 1, 3, 5, 7, 9, 11, 13, 15, - 17, 19, 21, 23, 25, 27, 29, 31, - 33, 35, 37, 39, 41, 43, 45, 47, - 49, 51, 53, 55, 57, 59, 61, 63, - 65, 67, 69, 71, 73, 75, 77, 79, - 81, 83, 85, 87, 89, 91, 93, 95, - 97, 99, 101, 102, 104, 105, 107, 108, - 110, 111, 113, 115, 116, 118, 120, 122, - 124, 126, 128, 130, 132, 135, 137, 140, - 142, 145, 148, 151, 154, 157, 160, 163, - 167, 171, 175, 179, 183, 187, 192, 197, - 202, 208, 213, 219, 226, 233, 240, 248, - 256, 265, 274, 284, 295, 307, 320, 334, - 349, 366, 384, 404, 427, 452, 480, 512, - 549, 591, 640, 698, 768, 853, 960, 1097, - 1280, 1536, 1920, 2560, 3840, 7680, 15360, 65535 - }; - public static readonly int[] SustainTable = new int[128] - { - -92544, -92416, -92288, -83328, -76928, -71936, -67840, -64384, - -61440, -58880, -56576, -54400, -52480, -50688, -49024, -47488, - -46080, -44672, -43392, -42240, -41088, -40064, -39040, -38016, - -36992, -36096, -35328, -34432, -33664, -32896, -32128, -31360, - -30592, -29952, -29312, -28672, -28032, -27392, -26880, -26240, - -25728, -25088, -24576, -24064, -23552, -23040, -22528, -22144, - -21632, -21120, -20736, -20224, -19840, -19456, -19072, -18560, - -18176, -17792, -17408, -17024, -16640, -16256, -16000, -15616, - -15232, -14848, -14592, -14208, -13952, -13568, -13184, -12928, - -12672, -12288, -12032, -11648, -11392, -11136, -10880, -10496, - -10240, -9984, -9728, -9472, -9216, -8960, -8704, -8448, - -8192, -7936, -7680, -7424, -7168, -6912, -6656, -6400, - -6272, -6016, -5760, -5504, -5376, -5120, -4864, -4608, - -4480, -4224, -3968, -3840, -3584, -3456, -3200, -2944, - -2816, -2560, -2432, -2176, -2048, -1792, -1664, -1408, - -1280, -1024, -896, -768, -512, -384, -128, 0 - }; - - private static readonly sbyte[] _sinTable = new sbyte[33] - { - 000, 006, 012, 019, 025, 031, 037, 043, - 049, 054, 060, 065, 071, 076, 081, 085, - 090, 094, 098, 102, 106, 109, 112, 115, - 117, 120, 122, 123, 125, 126, 126, 127, - 127 - }; - public static int Sin(int index) - { - if (index < 0x20) - { - return _sinTable[index]; - } - else if (index < 0x40) - { - return _sinTable[0x20 - (index - 0x20)]; - } - else if (index < 0x60) - { - return -_sinTable[index - 0x40]; - } - else // < 0x80 - { - return -_sinTable[0x20 - (index - 0x60)]; - } - } - - private static readonly ushort[] _pitchTable = new ushort[768] - { - 0, 59, 118, 178, 237, 296, 356, 415, - 475, 535, 594, 654, 714, 773, 833, 893, - 953, 1013, 1073, 1134, 1194, 1254, 1314, 1375, - 1435, 1496, 1556, 1617, 1677, 1738, 1799, 1859, - 1920, 1981, 2042, 2103, 2164, 2225, 2287, 2348, - 2409, 2471, 2532, 2593, 2655, 2716, 2778, 2840, - 2902, 2963, 3025, 3087, 3149, 3211, 3273, 3335, - 3397, 3460, 3522, 3584, 3647, 3709, 3772, 3834, - 3897, 3960, 4022, 4085, 4148, 4211, 4274, 4337, - 4400, 4463, 4526, 4590, 4653, 4716, 4780, 4843, - 4907, 4971, 5034, 5098, 5162, 5226, 5289, 5353, - 5417, 5481, 5546, 5610, 5674, 5738, 5803, 5867, - 5932, 5996, 6061, 6125, 6190, 6255, 6320, 6384, - 6449, 6514, 6579, 6645, 6710, 6775, 6840, 6906, - 6971, 7037, 7102, 7168, 7233, 7299, 7365, 7431, - 7496, 7562, 7628, 7694, 7761, 7827, 7893, 7959, - 8026, 8092, 8159, 8225, 8292, 8358, 8425, 8492, - 8559, 8626, 8693, 8760, 8827, 8894, 8961, 9028, - 9096, 9163, 9230, 9298, 9366, 9433, 9501, 9569, - 9636, 9704, 9772, 9840, 9908, 9976, 10045, 10113, - 10181, 10250, 10318, 10386, 10455, 10524, 10592, 10661, - 10730, 10799, 10868, 10937, 11006, 11075, 11144, 11213, - 11283, 11352, 11421, 11491, 11560, 11630, 11700, 11769, - 11839, 11909, 11979, 12049, 12119, 12189, 12259, 12330, - 12400, 12470, 12541, 12611, 12682, 12752, 12823, 12894, - 12965, 13036, 13106, 13177, 13249, 13320, 13391, 13462, - 13533, 13605, 13676, 13748, 13819, 13891, 13963, 14035, - 14106, 14178, 14250, 14322, 14394, 14467, 14539, 14611, - 14684, 14756, 14829, 14901, 14974, 15046, 15119, 15192, - 15265, 15338, 15411, 15484, 15557, 15630, 15704, 15777, - 15850, 15924, 15997, 16071, 16145, 16218, 16292, 16366, - 16440, 16514, 16588, 16662, 16737, 16811, 16885, 16960, - 17034, 17109, 17183, 17258, 17333, 17408, 17483, 17557, - 17633, 17708, 17783, 17858, 17933, 18009, 18084, 18160, - 18235, 18311, 18387, 18462, 18538, 18614, 18690, 18766, - 18842, 18918, 18995, 19071, 19147, 19224, 19300, 19377, - 19454, 19530, 19607, 19684, 19761, 19838, 19915, 19992, - 20070, 20147, 20224, 20302, 20379, 20457, 20534, 20612, - 20690, 20768, 20846, 20924, 21002, 21080, 21158, 21236, - 21315, 21393, 21472, 21550, 21629, 21708, 21786, 21865, - 21944, 22023, 22102, 22181, 22260, 22340, 22419, 22498, - 22578, 22658, 22737, 22817, 22897, 22977, 23056, 23136, - 23216, 23297, 23377, 23457, 23537, 23618, 23698, 23779, - 23860, 23940, 24021, 24102, 24183, 24264, 24345, 24426, - 24507, 24589, 24670, 24752, 24833, 24915, 24996, 25078, - 25160, 25242, 25324, 25406, 25488, 25570, 25652, 25735, - 25817, 25900, 25982, 26065, 26148, 26230, 26313, 26396, - 26479, 26562, 26645, 26729, 26812, 26895, 26979, 27062, - 27146, 27230, 27313, 27397, 27481, 27565, 27649, 27733, - 27818, 27902, 27986, 28071, 28155, 28240, 28324, 28409, - 28494, 28579, 28664, 28749, 28834, 28919, 29005, 29090, - 29175, 29261, 29346, 29432, 29518, 29604, 29690, 29776, - 29862, 29948, 30034, 30120, 30207, 30293, 30380, 30466, - 30553, 30640, 30727, 30814, 30900, 30988, 31075, 31162, - 31249, 31337, 31424, 31512, 31599, 31687, 31775, 31863, - 31951, 32039, 32127, 32215, 32303, 32392, 32480, 32568, - 32657, 32746, 32834, 32923, 33012, 33101, 33190, 33279, - 33369, 33458, 33547, 33637, 33726, 33816, 33906, 33995, - 34085, 34175, 34265, 34355, 34446, 34536, 34626, 34717, - 34807, 34898, 34988, 35079, 35170, 35261, 35352, 35443, - 35534, 35626, 35717, 35808, 35900, 35991, 36083, 36175, - 36267, 36359, 36451, 36543, 36635, 36727, 36820, 36912, - 37004, 37097, 37190, 37282, 37375, 37468, 37561, 37654, - 37747, 37841, 37934, 38028, 38121, 38215, 38308, 38402, - 38496, 38590, 38684, 38778, 38872, 38966, 39061, 39155, - 39250, 39344, 39439, 39534, 39629, 39724, 39819, 39914, - 40009, 40104, 40200, 40295, 40391, 40486, 40582, 40678, - 40774, 40870, 40966, 41062, 41158, 41255, 41351, 41448, - 41544, 41641, 41738, 41835, 41932, 42029, 42126, 42223, - 42320, 42418, 42515, 42613, 42710, 42808, 42906, 43004, - 43102, 43200, 43298, 43396, 43495, 43593, 43692, 43790, - 43889, 43988, 44087, 44186, 44285, 44384, 44483, 44583, - 44682, 44781, 44881, 44981, 45081, 45180, 45280, 45381, - 45481, 45581, 45681, 45782, 45882, 45983, 46083, 46184, - 46285, 46386, 46487, 46588, 46690, 46791, 46892, 46994, - 47095, 47197, 47299, 47401, 47503, 47605, 47707, 47809, - 47912, 48014, 48117, 48219, 48322, 48425, 48528, 48631, - 48734, 48837, 48940, 49044, 49147, 49251, 49354, 49458, - 49562, 49666, 49770, 49874, 49978, 50082, 50187, 50291, - 50396, 50500, 50605, 50710, 50815, 50920, 51025, 51131, - 51236, 51341, 51447, 51552, 51658, 51764, 51870, 51976, - 52082, 52188, 52295, 52401, 52507, 52614, 52721, 52827, - 52934, 53041, 53148, 53256, 53363, 53470, 53578, 53685, - 53793, 53901, 54008, 54116, 54224, 54333, 54441, 54549, - 54658, 54766, 54875, 54983, 55092, 55201, 55310, 55419, - 55529, 55638, 55747, 55857, 55966, 56076, 56186, 56296, - 56406, 56516, 56626, 56736, 56847, 56957, 57068, 57179, - 57289, 57400, 57511, 57622, 57734, 57845, 57956, 58068, - 58179, 58291, 58403, 58515, 58627, 58739, 58851, 58964, - 59076, 59189, 59301, 59414, 59527, 59640, 59753, 59866, - 59979, 60092, 60206, 60319, 60433, 60547, 60661, 60774, - 60889, 61003, 61117, 61231, 61346, 61460, 61575, 61690, - 61805, 61920, 62035, 62150, 62265, 62381, 62496, 62612, - 62727, 62843, 62959, 63075, 63191, 63308, 63424, 63540, - 63657, 63774, 63890, 64007, 64124, 64241, 64358, 64476, - 64593, 64711, 64828, 64946, 65064, 65182, 65300, 65418 - }; - private static readonly byte[] _volumeTable = new byte[724] - { - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 4, 4, - 4, 4, 4, 4, 4, 4, 4, 4, - 4, 4, 4, 4, 4, 4, 4, 4, - 4, 5, 5, 5, 5, 5, 5, 5, - 5, 5, 5, 5, 5, 5, 5, 5, - 5, 6, 6, 6, 6, 6, 6, 6, - 6, 6, 6, 6, 6, 6, 6, 7, - 7, 7, 7, 7, 7, 7, 7, 7, - 7, 7, 7, 8, 8, 8, 8, 8, - 8, 8, 8, 8, 9, 9, 9, 9, - 9, 9, 9, 9, 9, 10, 10, 10, - 10, 10, 10, 10, 10, 11, 11, 11, - 11, 11, 11, 11, 11, 12, 12, 12, - 12, 12, 12, 12, 13, 13, 13, 13, - 13, 13, 13, 14, 14, 14, 14, 14, - 14, 15, 15, 15, 15, 15, 16, 16, - 16, 16, 16, 16, 17, 17, 17, 17, - 17, 18, 18, 18, 18, 19, 19, 19, - 19, 19, 20, 20, 20, 20, 21, 21, - 21, 21, 22, 22, 22, 22, 23, 23, - 23, 23, 24, 24, 24, 25, 25, 25, - 25, 26, 26, 26, 27, 27, 27, 28, - 28, 28, 29, 29, 29, 30, 30, 30, - 31, 31, 31, 32, 32, 33, 33, 33, - 34, 34, 35, 35, 35, 36, 36, 37, - 37, 38, 38, 38, 39, 39, 40, 40, - 41, 41, 42, 42, 43, 43, 44, 44, - 45, 45, 46, 46, 47, 47, 48, 48, - 49, 50, 50, 51, 51, 52, 52, 53, - 54, 54, 55, 56, 56, 57, 58, 58, - 59, 60, 60, 61, 62, 62, 63, 64, - 65, 66, 66, 67, 68, 69, 70, 70, - 71, 72, 73, 74, 75, 75, 76, 77, - 78, 79, 80, 81, 82, 83, 84, 85, - 86, 87, 88, 89, 90, 91, 92, 93, - 94, 95, 96, 97, 98, 99, 101, 102, - 103, 104, 105, 106, 108, 109, 110, 111, - 113, 114, 115, 117, 118, 119, 121, 122, - 124, 125, 126, 127 - }; - - public static ushort GetChannelTimer(ushort baseTimer, int pitch) - { - int shift = 0; - pitch = -pitch; - - while (pitch < 0) - { - shift--; - pitch += 0x300; - } - - while (pitch >= 0x300) - { - shift++; - pitch -= 0x300; - } - - ulong timer = (_pitchTable[pitch] + 0x10000uL) * baseTimer; - shift -= 16; - if (shift <= 0) - { - timer >>= -shift; - } - else if (shift < 32) - { - if ((timer & (ulong.MaxValue << (32 - shift))) != 0) - { - return ushort.MaxValue; - } - timer <<= shift; - } - else - { - return ushort.MaxValue; - } - - if (timer < 0x10) - { - return 0x10; - } - if (timer > ushort.MaxValue) - { - timer = ushort.MaxValue; - } - return (ushort)timer; - } - public static byte GetChannelVolume(int vol) - { - int a = vol / 0x80; - if (a < -723) - { - a = -723; - } - else if (a > 0) - { - a = 0; - } - return _volumeTable[a + 723]; - } - } -} diff --git a/VG Music Studio/Core/NDS/Utils.cs b/VG Music Studio/Core/NDS/Utils.cs deleted file mode 100644 index ea2388b6..00000000 --- a/VG Music Studio/Core/NDS/Utils.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.NDS -{ - internal static class Utils - { - public const int ARM7_CLOCK = 16756991; // (33.513982 MHz / 2) == 16.756991 MHz == 16,756,991 Hz - } -} diff --git a/VG Music Studio/Core/Player.cs b/VG Music Studio/Core/Player.cs deleted file mode 100644 index 7a6e26f2..00000000 --- a/VG Music Studio/Core/Player.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Core -{ - internal enum PlayerState : byte - { - Stopped = 0, - Playing, - Paused, - Recording, - ShutDown - } - - internal delegate void SongEndedEvent(); - - internal interface IPlayer : IDisposable - { - List[] Events { get; } - long MaxTicks { get; } - long ElapsedTicks { get; } - bool ShouldFadeOut { get; set; } - long NumLoops { get; set; } - - PlayerState State { get; } - event SongEndedEvent SongEnded; - - void LoadSong(long index); - void SetCurrentPosition(long ticks); - void Play(); - void Pause(); - void Stop(); - void Record(string fileName); - void GetSongState(UI.SongInfoControl.SongInfo info); - } -} diff --git a/VG Music Studio/Core/SongEvent.cs b/VG Music Studio/Core/SongEvent.cs deleted file mode 100644 index 7e518a3b..00000000 --- a/VG Music Studio/Core/SongEvent.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using System.Drawing; - -namespace Kermalis.VGMusicStudio.Core -{ - internal interface ICommand - { - Color Color { get; } - string Label { get; } - string Arguments { get; } - } - internal class SongEvent - { - public long Offset { get; } - public List Ticks { get; } = new List(); - public ICommand Command { get; } - - public SongEvent(long offset, ICommand command) - { - Offset = offset; - Command = command; - } - } -} diff --git a/VG Music Studio/Core/VGMSDebug.cs b/VG Music Studio/Core/VGMSDebug.cs deleted file mode 100644 index 91bc0919..00000000 --- a/VG Music Studio/Core/VGMSDebug.cs +++ /dev/null @@ -1,112 +0,0 @@ -using Kermalis.EndianBinaryIO; -using Sanford.Multimedia.Midi; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Kermalis.VGMusicStudio.Core -{ -#if DEBUG - internal static class VGMSDebug - { - public static void MIDIVolumeMerger(string f1, string f2) - { - var midi1 = new Sequence(f1); - var midi2 = new Sequence(f2); - var baby = new Sequence(midi1.Division); - - for (int i = 0; i < midi1.Count; i++) - { - Track midi1Track = midi1[i]; - Track midi2Track = midi2[i]; - var babyTrack = new Track(); - baby.Add(babyTrack); - - for (int j = 0; j < midi1Track.Count; j++) - { - MidiEvent e1 = midi1Track.GetMidiEvent(j); - if (e1.MidiMessage is ChannelMessage cm1 && cm1.Command == ChannelCommand.Controller && cm1.Data1 == (int)ControllerType.Volume) - { - MidiEvent e2 = midi2Track.GetMidiEvent(j); - var cm2 = (ChannelMessage)e2.MidiMessage; - babyTrack.Insert(e1.AbsoluteTicks, new ChannelMessage(ChannelCommand.Controller, cm1.MidiChannel, (int)ControllerType.Volume, Math.Max(cm1.Data2, cm2.Data2))); - } - else - { - babyTrack.Insert(e1.AbsoluteTicks, e1.MidiMessage); - } - } - } - - baby.Save(f1); - baby.Save(f2); - } - - public static void EventScan(List songs, bool showIndexes) - { - Console.WriteLine($"{nameof(EventScan)} started."); - var scans = new Dictionary>(); - foreach (Config.Song song in songs) - { - try - { - Engine.Instance.Player.LoadSong(song.Index); - } - catch (Exception ex) - { - Console.WriteLine("Exception loading {0} - {1}", showIndexes ? $"song {song.Index}" : $"\"{song.Name}\"", ex.Message); - continue; - } - if (Engine.Instance.Player.Events != null) - { - foreach (string cmd in Engine.Instance.Player.Events.Where(ev => ev != null).SelectMany(ev => ev).Select(ev => ev.Command.Label).Distinct()) - { - if (scans.ContainsKey(cmd)) - { - scans[cmd].Add(song); - } - else - { - scans.Add(cmd, new List() { song }); - } - } - } - } - foreach (KeyValuePair> kvp in scans.OrderBy(k => k.Key)) - { - Console.WriteLine("{0} ({1})", kvp.Key, showIndexes ? string.Join(", ", kvp.Value.Select(s => s.Index)) : string.Join(", ", kvp.Value.Select(s => s.Name))); - } - Console.WriteLine($"{nameof(EventScan)} ended."); - } - - public static void GBAGameCodeScan(string path) - { - Console.WriteLine($"{nameof(GBAGameCodeScan)} started."); - var scans = new List(); - foreach (string file in Directory.GetFiles(path, "*.gba", SearchOption.AllDirectories)) - { - try - { - using (var reader = new EndianBinaryReader(File.OpenRead(file))) - { - string gameCode = reader.ReadString(3, false, 0xAC); - char regionCode = reader.ReadChar(0xAF); - byte version = reader.ReadByte(0xBC); - scans.Add(string.Format("Code: {0}\tRegion: {1}\tVersion: {2}\tFile: {3}", gameCode, regionCode, version, file)); - } - } - catch (Exception ex) - { - Console.WriteLine("Exception loading \"{0}\" - {1}", file, ex.Message); - } - } - foreach (string s in scans.OrderBy(s => s)) - { - Console.WriteLine(s); - } - Console.WriteLine($"{nameof(GBAGameCodeScan)} ended."); - } - } -#endif -} diff --git a/VG Music Studio/Dependencies/DLS2.dll b/VG Music Studio/Dependencies/DLS2.dll deleted file mode 100644 index 1d3bc0c8..00000000 Binary files a/VG Music Studio/Dependencies/DLS2.dll and /dev/null differ diff --git a/VG Music Studio/Dependencies/Sanford.Multimedia.Midi.dll b/VG Music Studio/Dependencies/Sanford.Multimedia.Midi.dll deleted file mode 100644 index 4e31c6b0..00000000 Binary files a/VG Music Studio/Dependencies/Sanford.Multimedia.Midi.dll and /dev/null differ diff --git a/VG Music Studio/Dependencies/SoundFont2.dll b/VG Music Studio/Dependencies/SoundFont2.dll deleted file mode 100644 index f3f80083..00000000 Binary files a/VG Music Studio/Dependencies/SoundFont2.dll and /dev/null differ diff --git a/VG Music Studio/Program.cs b/VG Music Studio/Program.cs deleted file mode 100644 index e2e25293..00000000 --- a/VG Music Studio/Program.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.UI; -using System; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio -{ - internal static class Program - { - [STAThread] - private static void Main() - { -#if DEBUG - //Debug.GBAGameCodeScan(@"C:\Users\Kermalis\Documents\Emulation\GBA\Games"); -#endif - try - { - GlobalConfig.Init(); - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorGlobalConfig); - return; - } - Application.EnableVisualStyles(); - Application.Run(MainForm.Instance); - } - } -} diff --git a/VG Music Studio/Properties/AssemblyInfo.cs b/VG Music Studio/Properties/AssemblyInfo.cs deleted file mode 100644 index 90411a11..00000000 --- a/VG Music Studio/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Resources; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("VG Music Studio")] -[assembly: AssemblyDescription("Listen to the music from popular video game formats.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Kermalis")] -[assembly: AssemblyProduct("VG Music Studio")] -[assembly: AssemblyCopyright("Copyright © Kermalis 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("97c8acf8-66a3-4321-91d6-3e94eaca577f")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.0.0.2")] -[assembly: AssemblyFileVersion("0.0.0.2")] -[assembly: NeutralResourcesLanguage("en-US")] - diff --git a/VG Music Studio/Properties/Settings.Designer.cs b/VG Music Studio/Properties/Settings.Designer.cs deleted file mode 100644 index 875ce0f2..00000000 --- a/VG Music Studio/Properties/Settings.Designer.cs +++ /dev/null @@ -1,26 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Kermalis.VGMusicStudio.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.7.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} diff --git a/VG Music Studio/Properties/Settings.settings b/VG Music Studio/Properties/Settings.settings deleted file mode 100644 index 39645652..00000000 --- a/VG Music Studio/Properties/Settings.settings +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/VG Music Studio/UI/ColorSlider.cs b/VG Music Studio/UI/ColorSlider.cs deleted file mode 100644 index b23df9ce..00000000 --- a/VG Music Studio/UI/ColorSlider.cs +++ /dev/null @@ -1,485 +0,0 @@ -#region License - -/* Copyright (c) 2017 Fabrice Lacharme - * This code is inspired from Michal Brylka - * https://www.codeproject.com/Articles/17395/Owner-drawn-trackbar-slider - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#endregion - - -using System; -using System.ComponentModel; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory(""), ToolboxBitmap(typeof(TrackBar))] - internal class ColorSlider : Control - { - private const int thumbSize = 14; - private Rectangle thumbRect; - - private long _value = 0L; - public long Value - { - get => _value; - set - { - if (value >= _minimum && value <= _maximum) - { - _value = value; - ValueChanged?.Invoke(this, new EventArgs()); - Invalidate(); - } - else - { - throw new ArgumentOutOfRangeException(nameof(Value), $"{nameof(Value)} must be between {nameof(Minimum)} and {nameof(Maximum)}."); - } - } - } - private long _minimum = 0L; - public long Minimum - { - get => _minimum; - set - { - if (value <= _maximum) - { - _minimum = value; - if (_value < _minimum) - { - _value = _minimum; - ValueChanged?.Invoke(this, new EventArgs()); - } - Invalidate(); - } - else - { - throw new ArgumentOutOfRangeException(nameof(Minimum), $"{nameof(Minimum)} cannot be higher than {nameof(Maximum)}."); - } - } - } - private long _maximum = 10L; - public long Maximum - { - get => _maximum; - set - { - if (value >= _minimum) - { - _maximum = value; - if (_value > _maximum) - { - _value = _maximum; - ValueChanged?.Invoke(this, new EventArgs()); - } - Invalidate(); - } - else - { - throw new ArgumentOutOfRangeException(nameof(Maximum), $"{nameof(Maximum)} cannot be lower than {nameof(Minimum)}."); - } - } - } - private long _smallChange = 1L; - public long SmallChange - { - get => _smallChange; - set - { - if (value >= 0) - { - _smallChange = value; - } - else - { - throw new ArgumentOutOfRangeException(nameof(SmallChange), $"{nameof(SmallChange)} must be greater than or equal to 0."); - } - } - } - private long _largeChange = 5L; - public long LargeChange - { - get => _largeChange; - set - { - if (value >= 0) - { - _largeChange = value; - } - else - { - throw new ArgumentOutOfRangeException(nameof(LargeChange), $"{nameof(LargeChange)} must be greater than or equal to 0."); - } - } - } - private bool _acceptKeys = true; - public bool AcceptKeys - { - get => _acceptKeys; - set - { - _acceptKeys = value; - SetStyle(ControlStyles.Selectable, value); - } - } - - public event EventHandler ValueChanged; - - private readonly Color _thumbOuterColor = Color.White; - private readonly Color _thumbInnerColor = Color.White; - private readonly Color _thumbPenColor = Color.FromArgb(125, 125, 125); - private readonly Color _barInnerColor = Theme.BackColorMouseOver; - private readonly Color _elapsedPenColorTop = Theme.ForeColor; - private readonly Color _elapsedPenColorBottom = Theme.ForeColor; - private readonly Color _barPenColorTop = Color.FromArgb(85, 90, 104); - private readonly Color _barPenColorBottom = Color.FromArgb(117, 124, 140); - private readonly Color _elapsedInnerColor = Theme.BorderColor; - private readonly Color _tickColor = Color.White; - - protected override void Dispose(bool disposing) - { - if (disposing) - { - pen.Dispose(); - } - base.Dispose(disposing); - } - public ColorSlider() - { - SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | - ControlStyles.ResizeRedraw | ControlStyles.Selectable | - ControlStyles.SupportsTransparentBackColor | ControlStyles.UserMouse | - ControlStyles.UserPaint, true); - Size = new Size(200, 48); - } - - protected override void OnPaint(PaintEventArgs e) - { - if (!Enabled) - { - Color[] c = DesaturateColors(_thumbOuterColor, _thumbInnerColor, _thumbPenColor, - _barInnerColor, - _elapsedPenColorTop, _elapsedPenColorBottom, - _barPenColorTop, _barPenColorBottom, - _elapsedInnerColor); - Draw(e, - c[0], c[1], c[2], - c[3], - c[4], c[5], - c[6], c[7], - c[8]); - } - else - { - if (mouseInRegion) - { - Color[] c = LightenColors(_thumbOuterColor, _thumbInnerColor, _thumbPenColor, - _barInnerColor, - _elapsedPenColorTop, _elapsedPenColorBottom, - _barPenColorTop, _barPenColorBottom, - _elapsedInnerColor); - Draw(e, - c[0], c[1], c[2], - c[3], - c[4], c[5], - c[6], c[7], - c[8]); - } - else - { - Draw(e, - _thumbOuterColor, _thumbInnerColor, _thumbPenColor, - _barInnerColor, - _elapsedPenColorTop, _elapsedPenColorBottom, - _barPenColorTop, _barPenColorBottom, - _elapsedInnerColor); - } - } - } - private readonly Pen pen = new Pen(Color.Transparent); - private void Draw(PaintEventArgs e, - Color thumbOuterColorPaint, Color thumbInnerColorPaint, Color thumbPenColorPaint, - Color barInnerColorPaint, - Color elapsedTopPenColorPaint, Color elapsedBottomPenColorPaint, - Color barTopPenColorPaint, Color barBottomPenColorPaint, - Color elapsedInnerColorPaint) - { - if (Focused) - { - ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Color.FromArgb(50, elapsedTopPenColorPaint), ButtonBorderStyle.Dashed); - } - - long a = _maximum - _minimum; - long x = a == 0 ? 0 : (_value - _minimum) * (ClientRectangle.Width - thumbSize) / a; - thumbRect = new Rectangle((int)x, ClientRectangle.Y + (ClientRectangle.Height / 2) - (thumbSize / 2), thumbSize, thumbSize); - Rectangle barRect = ClientRectangle; - barRect.Inflate(-1, -barRect.Height / 3); - Rectangle elapsedRect = barRect; - elapsedRect.Width = thumbRect.Left + (thumbSize / 2); - - pen.Color = barInnerColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + (barRect.Height / 2)); - pen.Color = elapsedInnerColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y + (barRect.Height / 2)); - pen.Color = elapsedTopPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y - 1 + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y - 1 + (barRect.Height / 2)); - pen.Color = elapsedBottomPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y + 1 + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y + 1 + (barRect.Height / 2)); - pen.Color = barTopPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X + elapsedRect.Width, barRect.Y - 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y - 1 + (barRect.Height / 2)); - pen.Color = barBottomPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X + elapsedRect.Width, barRect.Y + 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + 1 + (barRect.Height / 2)); - pen.Color = barTopPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y - 1 + (barRect.Height / 2), barRect.X, barRect.Y + (barRect.Height / 2) + 1); - pen.Color = barBottomPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X + barRect.Width, barRect.Y - 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + 1 + (barRect.Height / 2)); - - e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; - Color newthumbOuterColorPaint = thumbOuterColorPaint, - newthumbInnerColorPaint = thumbInnerColorPaint; - if (busyMouse) - { - newthumbOuterColorPaint = Color.FromArgb(175, thumbOuterColorPaint); - newthumbInnerColorPaint = Color.FromArgb(175, thumbInnerColorPaint); - } - using (GraphicsPath thumbPath = CreateRoundRectPath(thumbRect, thumbSize)) - { - using (var lgbThumb = new LinearGradientBrush(thumbRect, newthumbOuterColorPaint, newthumbInnerColorPaint, LinearGradientMode.Vertical) { WrapMode = WrapMode.TileFlipXY }) - { - e.Graphics.FillPath(lgbThumb, thumbPath); - } - Color newThumbPenColor = thumbPenColorPaint; - if (busyMouse || mouseInThumbRegion) - { - newThumbPenColor = ControlPaint.Dark(newThumbPenColor); - } - pen.Color = newThumbPenColor; - e.Graphics.DrawPath(pen, thumbPath); - } - - const int numTicks = 1 + (10 * (5 + 1)); - int interval = 0; - int start = thumbRect.Width / 2; - int w = barRect.Width - thumbRect.Width; - int idx = 0; - pen.Color = _tickColor; - for (int i = 0; i <= 10; i++) - { - e.Graphics.DrawLine(pen, start + barRect.X + interval, ClientRectangle.Y + ClientRectangle.Height, start + barRect.X + interval, ClientRectangle.Y + ClientRectangle.Height - 5); - if (i < 10) - { - for (int j = 0; j <= 5; j++) - { - idx++; - interval = idx * w / (numTicks - 1); - } - } - } - } - - private bool mouseInRegion = false; - private bool mouseInThumbRegion = false; - private bool busyMouse = false; - private void SetValueFromPoint(Point p) - { - int x = p.X; - int margin = thumbSize / 2; - x -= margin; - _value = (long)((x * ((_maximum - _minimum) / (ClientSize.Width - (2f * margin)))) + _minimum); - if (_value < _minimum) - { - _value = _minimum; - } - else if (_value > _maximum) - { - _value = _maximum; - } - ValueChanged?.Invoke(this, new EventArgs()); - } - protected override void OnEnabledChanged(EventArgs e) - { - base.OnEnabledChanged(e); - Invalidate(); - } - protected override void OnMouseEnter(EventArgs e) - { - base.OnMouseEnter(e); - mouseInRegion = true; - Invalidate(); - } - protected override void OnMouseLeave(EventArgs e) - { - base.OnMouseLeave(e); - mouseInRegion = false; - mouseInThumbRegion = false; - Invalidate(); - } - protected override void OnMouseDown(MouseEventArgs e) - { - base.OnMouseDown(e); - mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); - busyMouse = (MouseButtons & MouseButtons.Left) != MouseButtons.None; - if (busyMouse) - { - SetValueFromPoint(e.Location); - } - Invalidate(); - } - protected override void OnMouseMove(MouseEventArgs e) - { - base.OnMouseMove(e); - mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); - if (busyMouse) - { - SetValueFromPoint(e.Location); - } - Invalidate(); - } - protected override void OnMouseUp(MouseEventArgs e) - { - base.OnMouseUp(e); - mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); - bool old = busyMouse; - busyMouse = old && e.Button == MouseButtons.Left ? false : old; - Invalidate(); - } - protected override void OnGotFocus(EventArgs e) - { - base.OnGotFocus(e); - Invalidate(); - } - protected override void OnLostFocus(EventArgs e) - { - base.OnLostFocus(e); - Invalidate(); - } - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - if (_acceptKeys && !busyMouse) - { - switch (e.KeyCode) - { - case Keys.Down: - case Keys.Left: - { - long newVal = _value - _smallChange; - if (newVal < _minimum) - { - newVal = _minimum; - } - Value = newVal; - break; - } - case Keys.Up: - case Keys.Right: - { - long newVal = _value + _smallChange; - if (newVal > _maximum) - { - newVal = _maximum; - } - Value = newVal; - break; - } - case Keys.Home: - { - Value = _minimum; - break; - } - case Keys.End: - { - Value = _maximum; - break; - } - case Keys.PageDown: - { - long newVal = _value - _largeChange; - if (newVal < _minimum) - { - newVal = _minimum; - } - Value = newVal; - break; - } - case Keys.PageUp: - { - long newVal = _value + _largeChange; - if (newVal > _maximum) - { - newVal = _maximum; - } - Value = newVal; - break; - } - } - } - } - protected override bool ProcessDialogKey(Keys keyData) - { - return !_acceptKeys || keyData == Keys.Tab || ModifierKeys == Keys.Shift ? base.ProcessDialogKey(keyData) : false; - } - - private static GraphicsPath CreateRoundRectPath(Rectangle rect, int size) - { - var gp = new GraphicsPath(); - gp.AddLine(rect.Left + (size / 2), rect.Top, rect.Right - (size / 2), rect.Top); - gp.AddArc(rect.Right - size, rect.Top, size, size, 270, 90); - - gp.AddLine(rect.Right, rect.Top + (size / 2), rect.Right, rect.Bottom - (size / 2)); - gp.AddArc(rect.Right - size, rect.Bottom - size, size, size, 0, 90); - - gp.AddLine(rect.Right - (size / 2), rect.Bottom, rect.Left + (size / 2), rect.Bottom); - gp.AddArc(rect.Left, rect.Bottom - size, size, size, 90, 90); - - gp.AddLine(rect.Left, rect.Bottom - (size / 2), rect.Left, rect.Top + (size / 2)); - gp.AddArc(rect.Left, rect.Top, size, size, 180, 90); - return gp; - } - private static Color[] DesaturateColors(params Color[] colors) - { - var ret = new Color[colors.Length]; - for (int i = 0; i < colors.Length; i++) - { - int gray = (int)((colors[i].R * 0.3) + (colors[i].G * 0.6) + (colors[i].B * 0.1)); - ret[i] = Color.FromArgb((-0x010101 * (255 - gray)) - 1); - } - return ret; - } - private static Color[] LightenColors(params Color[] colors) - { - var ret = new Color[colors.Length]; - for (int i = 0; i < colors.Length; i++) - { - ret[i] = ControlPaint.Light(colors[i]); - } - return ret; - } - private static bool IsPointInRect(Point p, Rectangle rect) - { - return p.X > rect.Left & p.X < rect.Right & p.Y > rect.Top & p.Y < rect.Bottom; - } - } -} diff --git a/VG Music Studio/UI/FlexibleMessageBox.cs b/VG Music Studio/UI/FlexibleMessageBox.cs deleted file mode 100644 index 30d29ad1..00000000 --- a/VG Music Studio/UI/FlexibleMessageBox.cs +++ /dev/null @@ -1,697 +0,0 @@ -using System; -using System.ComponentModel; -using System.Diagnostics; -using System.Drawing; -using System.Globalization; -using System.Linq; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - /* FlexibleMessageBox – A flexible replacement for the .NET MessageBox - * - * Author: Jörg Reichert (public@jreichert.de) - * Contributors: Thanks to: David Hall, Roink - * Version: 1.3 - * Published at: http://www.codeproject.com/Articles/601900/FlexibleMessageBox - * - ************************************************************************************************************ - * Features: - * - It can be simply used instead of MessageBox since all important static "Show"-Functions are supported - * - It is small, only one source file, which could be added easily to each solution - * - It can be resized and the content is correctly word-wrapped - * - It tries to auto-size the width to show the longest text row - * - It never exceeds the current desktop working area - * - It displays a vertical scrollbar when needed - * - It does support hyperlinks in text - * - * Because the interface is identical to MessageBox, you can add this single source file to your project - * and use the FlexibleMessageBox almost everywhere you use a standard MessageBox. - * The goal was NOT to produce as many features as possible but to provide a simple replacement to fit my - * own needs. Feel free to add additional features on your own, but please left my credits in this class. - * - ************************************************************************************************************ - * Usage examples: - * - * FlexibleMessageBox.Show("Just a text"); - * - * FlexibleMessageBox.Show("A text", - * "A caption"); - * - * FlexibleMessageBox.Show("Some text with a link: www.google.com", - * "Some caption", - * MessageBoxButtons.AbortRetryIgnore, - * MessageBoxIcon.Information, - * MessageBoxDefaultButton.Button2); - * - * var dialogResult = FlexibleMessageBox.Show("Do you know the answer to life the universe and everything?", - * "One short question", - * MessageBoxButtons.YesNo); - * - ************************************************************************************************************ - * THE SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS", WITHOUT WARRANTY - * OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL THE AUTHOR BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF THIS - * SOFTWARE. - * - ************************************************************************************************************ - * History: - * Version 1.3 - 19.Dezember 2014 - * - Added refactoring function GetButtonText() - * - Used CurrentUICulture instead of InstalledUICulture - * - Added more button localizations. Supported languages are now: ENGLISH, GERMAN, SPANISH, ITALIAN - * - Added standard MessageBox handling for "copy to clipboard" with + and + - * - Tab handling is now corrected (only tabbing over the visible buttons) - * - Added standard MessageBox handling for ALT-Keyboard shortcuts - * - SetDialogSizes: Refactored completely: Corrected sizing and added caption driven sizing - * - * Version 1.2 - 10.August 2013 - * - Do not ShowInTaskbar anymore (original MessageBox is also hidden in taskbar) - * - Added handling for Escape-Button - * - Adapted top right close button (red X) to behave like MessageBox (but hidden instead of deactivated) - * - * Version 1.1 - 14.June 2013 - * - Some Refactoring - * - Added internal form class - * - Added missing code comments, etc. - * - * Version 1.0 - 15.April 2013 - * - Initial Version - */ - - internal class FlexibleMessageBox - { - #region Public statics - - /// - /// Defines the maximum width for all FlexibleMessageBox instances in percent of the working area. - /// - /// Allowed values are 0.2 - 1.0 where: - /// 0.2 means: The FlexibleMessageBox can be at most half as wide as the working area. - /// 1.0 means: The FlexibleMessageBox can be as wide as the working area. - /// - /// Default is: 70% of the working area width. - /// - public static double MAX_WIDTH_FACTOR = 0.7; - - /// - /// Defines the maximum height for all FlexibleMessageBox instances in percent of the working area. - /// - /// Allowed values are 0.2 - 1.0 where: - /// 0.2 means: The FlexibleMessageBox can be at most half as high as the working area. - /// 1.0 means: The FlexibleMessageBox can be as high as the working area. - /// - /// Default is: 90% of the working area height. - /// - public static double MAX_HEIGHT_FACTOR = 0.9; - - /// - /// Defines the font for all FlexibleMessageBox instances. - /// - /// Default is: Theme.Font - /// - public static Font FONT = Theme.Font; - - #endregion - - #region Public show functions - - public static DialogResult Show(string text) - { - return FlexibleMessageBoxForm.Show(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(IWin32Window owner, string text) - { - return FlexibleMessageBoxForm.Show(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(string text, string caption) - { - return FlexibleMessageBoxForm.Show(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(Exception ex, string caption) - { - return FlexibleMessageBoxForm.Show(null, string.Format("Error Details:{1}{1}{0}{1}{2}", ex.Message, Environment.NewLine, ex.StackTrace), caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(IWin32Window owner, string text, string caption) - { - return FlexibleMessageBoxForm.Show(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(string text, string caption, MessageBoxButtons buttons) - { - return FlexibleMessageBoxForm.Show(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons) - { - return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) - { - return FlexibleMessageBoxForm.Show(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) - { - return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) - { - return FlexibleMessageBoxForm.Show(null, text, caption, buttons, icon, defaultButton); - } - public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) - { - return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, icon, defaultButton); - } - - #endregion - - #region Internal form class - - class FlexibleMessageBoxForm : ThemedForm - { - IContainer components = null; - - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - void InitializeComponent() - { - components = new Container(); - button1 = new ThemedButton(); - richTextBoxMessage = new ThemedRichTextBox(); - FlexibleMessageBoxFormBindingSource = new BindingSource(components); - panel1 = new ThemedPanel(); - pictureBoxForIcon = new PictureBox(); - button2 = new ThemedButton(); - button3 = new ThemedButton(); - ((ISupportInitialize)(FlexibleMessageBoxFormBindingSource)).BeginInit(); - panel1.SuspendLayout(); - ((ISupportInitialize)(pictureBoxForIcon)).BeginInit(); - SuspendLayout(); - // - // button1 - // - button1.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; - button1.AutoSize = true; - button1.DialogResult = DialogResult.OK; - button1.Location = new Point(11, 67); - button1.MinimumSize = new Size(0, 24); - button1.Name = "button1"; - button1.Size = new Size(75, 24); - button1.TabIndex = 2; - button1.Text = "OK"; - button1.UseVisualStyleBackColor = true; - button1.Visible = false; - // - // richTextBoxMessage - // - richTextBoxMessage.Anchor = (((AnchorStyles.Top | AnchorStyles.Bottom) - | AnchorStyles.Left) - | AnchorStyles.Right); - richTextBoxMessage.BorderStyle = BorderStyle.None; - richTextBoxMessage.DataBindings.Add(new Binding("Text", FlexibleMessageBoxFormBindingSource, "MessageText", true, DataSourceUpdateMode.OnPropertyChanged)); - richTextBoxMessage.Font = new Font(Theme.Font.FontFamily, 9); - richTextBoxMessage.Location = new Point(50, 26); - richTextBoxMessage.Margin = new Padding(0); - richTextBoxMessage.Name = "richTextBoxMessage"; - richTextBoxMessage.ReadOnly = true; - richTextBoxMessage.ScrollBars = RichTextBoxScrollBars.Vertical; - richTextBoxMessage.Size = new Size(200, 20); - richTextBoxMessage.TabIndex = 0; - richTextBoxMessage.TabStop = false; - richTextBoxMessage.Text = ""; - richTextBoxMessage.LinkClicked += new LinkClickedEventHandler(LinkClicked); - // - // panel1 - // - panel1.Anchor = (((AnchorStyles.Top | AnchorStyles.Bottom) - | AnchorStyles.Left) - | AnchorStyles.Right); - panel1.Controls.Add(pictureBoxForIcon); - panel1.Controls.Add(richTextBoxMessage); - panel1.Location = new Point(-3, -4); - panel1.Name = "panel1"; - panel1.Size = new Size(268, 59); - panel1.TabIndex = 1; - // - // pictureBoxForIcon - // - pictureBoxForIcon.BackColor = Color.Transparent; - pictureBoxForIcon.Location = new Point(15, 19); - pictureBoxForIcon.Name = "pictureBoxForIcon"; - pictureBoxForIcon.Size = new Size(32, 32); - pictureBoxForIcon.TabIndex = 8; - pictureBoxForIcon.TabStop = false; - // - // button2 - // - button2.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right); - button2.DialogResult = DialogResult.OK; - button2.Location = new Point(92, 67); - button2.MinimumSize = new Size(0, 24); - button2.Name = "button2"; - button2.Size = new Size(75, 24); - button2.TabIndex = 3; - button2.Text = "OK"; - button2.UseVisualStyleBackColor = true; - button2.Visible = false; - // - // button3 - // - button3.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right); - button3.AutoSize = true; - button3.DialogResult = DialogResult.OK; - button3.Location = new Point(173, 67); - button3.MinimumSize = new Size(0, 24); - button3.Name = "button3"; - button3.Size = new Size(75, 24); - button3.TabIndex = 0; - button3.Text = "OK"; - button3.UseVisualStyleBackColor = true; - button3.Visible = false; - // - // FlexibleMessageBoxForm - // - AutoScaleDimensions = new SizeF(6F, 13F); - AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(260, 102); - Controls.Add(button3); - Controls.Add(button2); - Controls.Add(panel1); - Controls.Add(button1); - DataBindings.Add(new Binding("Text", FlexibleMessageBoxFormBindingSource, "CaptionText", true)); - Icon = Properties.Resources.Icon; - MaximizeBox = false; - MinimizeBox = false; - MinimumSize = new Size(276, 140); - Name = "FlexibleMessageBoxForm"; - SizeGripStyle = SizeGripStyle.Show; - StartPosition = FormStartPosition.CenterParent; - Text = ""; - Shown += new EventHandler(FlexibleMessageBoxForm_Shown); - ((ISupportInitialize)(FlexibleMessageBoxFormBindingSource)).EndInit(); - panel1.ResumeLayout(false); - ((ISupportInitialize)(pictureBoxForIcon)).EndInit(); - ResumeLayout(false); - PerformLayout(); - } - - ThemedButton button1, button2, button3; - private BindingSource FlexibleMessageBoxFormBindingSource; - ThemedRichTextBox richTextBoxMessage; - ThemedPanel panel1; - private PictureBox pictureBoxForIcon; - - #region Private constants - - //These separators are used for the "copy to clipboard" standard operation, triggered by Ctrl + C (behavior and clipboard format is like in a standard MessageBox) - static readonly String STANDARD_MESSAGEBOX_SEPARATOR_LINES = "---------------------------\n"; - static readonly String STANDARD_MESSAGEBOX_SEPARATOR_SPACES = " "; - - //These are the possible buttons (in a standard MessageBox) - private enum ButtonID { OK = 0, CANCEL, YES, NO, ABORT, RETRY, IGNORE }; - - //These are the buttons texts for different languages. - //If you want to add a new language, add it here and in the GetButtonText-Function - private enum TwoLetterISOLanguageID { en, de, es, it }; - static readonly String[] BUTTON_TEXTS_ENGLISH_EN = { "OK", "Cancel", "&Yes", "&No", "&Abort", "&Retry", "&Ignore" }; //Note: This is also the fallback language - static readonly String[] BUTTON_TEXTS_GERMAN_DE = { "OK", "Abbrechen", "&Ja", "&Nein", "&Abbrechen", "&Wiederholen", "&Ignorieren" }; - static readonly String[] BUTTON_TEXTS_SPANISH_ES = { "Aceptar", "Cancelar", "&Sí", "&No", "&Abortar", "&Reintentar", "&Ignorar" }; - static readonly String[] BUTTON_TEXTS_ITALIAN_IT = { "OK", "Annulla", "&Sì", "&No", "&Interrompi", "&Riprova", "&Ignora" }; - - #endregion - - #region Private members - - MessageBoxDefaultButton defaultButton; - int visibleButtonsCount; - readonly TwoLetterISOLanguageID languageID = TwoLetterISOLanguageID.en; - - #endregion - - #region Private constructor - - private FlexibleMessageBoxForm() - { - InitializeComponent(); - - //Try to evaluate the language. If this fails, the fallback language English will be used - Enum.TryParse(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, out languageID); - - KeyPreview = true; - KeyUp += FlexibleMessageBoxForm_KeyUp; - } - - #endregion - - #region Private helper functions - - static string[] GetStringRows(string message) - { - if (string.IsNullOrEmpty(message)) - { - return null; - } - - var messageRows = message.Split(new char[] { '\n' }, StringSplitOptions.None); - return messageRows; - } - - string GetButtonText(ButtonID buttonID) - { - var buttonTextArrayIndex = Convert.ToInt32(buttonID); - - switch (languageID) - { - case TwoLetterISOLanguageID.de: return BUTTON_TEXTS_GERMAN_DE[buttonTextArrayIndex]; - case TwoLetterISOLanguageID.es: return BUTTON_TEXTS_SPANISH_ES[buttonTextArrayIndex]; - case TwoLetterISOLanguageID.it: return BUTTON_TEXTS_ITALIAN_IT[buttonTextArrayIndex]; - - default: return BUTTON_TEXTS_ENGLISH_EN[buttonTextArrayIndex]; - } - } - - static double GetCorrectedWorkingAreaFactor(double workingAreaFactor) - { - const double MIN_FACTOR = 0.2; - const double MAX_FACTOR = 1.0; - - if (workingAreaFactor < MIN_FACTOR) - { - return MIN_FACTOR; - } - - if (workingAreaFactor > MAX_FACTOR) - { - return MAX_FACTOR; - } - - return workingAreaFactor; - } - - static void SetDialogStartPosition(FlexibleMessageBoxForm flexibleMessageBoxForm, IWin32Window owner) - { - //If no owner given: Center on current screen - if (owner == null) - { - var screen = Screen.FromPoint(Cursor.Position); - flexibleMessageBoxForm.StartPosition = FormStartPosition.Manual; - flexibleMessageBoxForm.Left = screen.Bounds.Left + screen.Bounds.Width / 2 - flexibleMessageBoxForm.Width / 2; - flexibleMessageBoxForm.Top = screen.Bounds.Top + screen.Bounds.Height / 2 - flexibleMessageBoxForm.Height / 2; - } - } - - static void SetDialogSizes(FlexibleMessageBoxForm flexibleMessageBoxForm, string text, string caption) - { - //First set the bounds for the maximum dialog size - flexibleMessageBoxForm.MaximumSize = new Size(Convert.ToInt32(SystemInformation.WorkingArea.Width * FlexibleMessageBoxForm.GetCorrectedWorkingAreaFactor(MAX_WIDTH_FACTOR)), - Convert.ToInt32(SystemInformation.WorkingArea.Height * FlexibleMessageBoxForm.GetCorrectedWorkingAreaFactor(MAX_HEIGHT_FACTOR))); - - //Get rows. Exit if there are no rows to render... - var stringRows = GetStringRows(text); - if (stringRows == null) - { - return; - } - - //Calculate whole text height - var textHeight = TextRenderer.MeasureText(text, FONT).Height; - - //Calculate width for longest text line - const int SCROLLBAR_WIDTH_OFFSET = 15; - var longestTextRowWidth = stringRows.Max(textForRow => TextRenderer.MeasureText(textForRow, FONT).Width); - var captionWidth = TextRenderer.MeasureText(caption, SystemFonts.CaptionFont).Width; - var textWidth = Math.Max(longestTextRowWidth + SCROLLBAR_WIDTH_OFFSET, captionWidth); - - //Calculate margins - var marginWidth = flexibleMessageBoxForm.Width - flexibleMessageBoxForm.richTextBoxMessage.Width; - var marginHeight = flexibleMessageBoxForm.Height - flexibleMessageBoxForm.richTextBoxMessage.Height; - - //Set calculated dialog size (if the calculated values exceed the maximums, they were cut by windows forms automatically) - flexibleMessageBoxForm.Size = new Size(textWidth + marginWidth, - textHeight + marginHeight); - } - - static void SetDialogIcon(FlexibleMessageBoxForm flexibleMessageBoxForm, MessageBoxIcon icon) - { - switch (icon) - { - case MessageBoxIcon.Information: - flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Information.ToBitmap(); - break; - case MessageBoxIcon.Warning: - flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Warning.ToBitmap(); - break; - case MessageBoxIcon.Error: - flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Error.ToBitmap(); - break; - case MessageBoxIcon.Question: - flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Question.ToBitmap(); - break; - default: - //When no icon is used: Correct placement and width of rich text box. - flexibleMessageBoxForm.pictureBoxForIcon.Visible = false; - flexibleMessageBoxForm.richTextBoxMessage.Left -= flexibleMessageBoxForm.pictureBoxForIcon.Width; - flexibleMessageBoxForm.richTextBoxMessage.Width += flexibleMessageBoxForm.pictureBoxForIcon.Width; - break; - } - } - - static void SetDialogButtons(FlexibleMessageBoxForm flexibleMessageBoxForm, MessageBoxButtons buttons, MessageBoxDefaultButton defaultButton) - { - //Set the buttons visibilities and texts - switch (buttons) - { - case MessageBoxButtons.AbortRetryIgnore: - flexibleMessageBoxForm.visibleButtonsCount = 3; - - flexibleMessageBoxForm.button1.Visible = true; - flexibleMessageBoxForm.button1.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.ABORT); - flexibleMessageBoxForm.button1.DialogResult = DialogResult.Abort; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.RETRY); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.Retry; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.IGNORE); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.Ignore; - - flexibleMessageBoxForm.ControlBox = false; - break; - - case MessageBoxButtons.OKCancel: - flexibleMessageBoxForm.visibleButtonsCount = 2; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.OK); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.OK; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; - - flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; - break; - - case MessageBoxButtons.RetryCancel: - flexibleMessageBoxForm.visibleButtonsCount = 2; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.RETRY); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.Retry; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; - - flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; - break; - - case MessageBoxButtons.YesNo: - flexibleMessageBoxForm.visibleButtonsCount = 2; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.YES); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.Yes; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.NO); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.No; - - flexibleMessageBoxForm.ControlBox = false; - break; - - case MessageBoxButtons.YesNoCancel: - flexibleMessageBoxForm.visibleButtonsCount = 3; - - flexibleMessageBoxForm.button1.Visible = true; - flexibleMessageBoxForm.button1.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.YES); - flexibleMessageBoxForm.button1.DialogResult = DialogResult.Yes; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.NO); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.No; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; - - flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; - break; - - case MessageBoxButtons.OK: - default: - flexibleMessageBoxForm.visibleButtonsCount = 1; - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.OK); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.OK; - - flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; - break; - } - - //Set default button (used in FlexibleMessageBoxForm_Shown) - flexibleMessageBoxForm.defaultButton = defaultButton; - } - - #endregion - - #region Private event handlers - - void FlexibleMessageBoxForm_Shown(object sender, EventArgs e) - { - int buttonIndexToFocus = 1; - Button buttonToFocus; - - //Set the default button... - switch (defaultButton) - { - case MessageBoxDefaultButton.Button1: - default: - buttonIndexToFocus = 1; - break; - case MessageBoxDefaultButton.Button2: - buttonIndexToFocus = 2; - break; - case MessageBoxDefaultButton.Button3: - buttonIndexToFocus = 3; - break; - } - - if (buttonIndexToFocus > visibleButtonsCount) - { - buttonIndexToFocus = visibleButtonsCount; - } - - if (buttonIndexToFocus == 3) - { - buttonToFocus = button3; - } - else if (buttonIndexToFocus == 2) - { - buttonToFocus = button2; - } - else - { - buttonToFocus = button1; - } - - buttonToFocus.Focus(); - } - - void LinkClicked(object sender, LinkClickedEventArgs e) - { - try - { - Cursor.Current = Cursors.WaitCursor; - Process.Start(e.LinkText); - } - catch (Exception) - { - //Let the caller of FlexibleMessageBoxForm decide what to do with this exception... - throw; - } - finally - { - Cursor.Current = Cursors.Default; - } - } - - void FlexibleMessageBoxForm_KeyUp(object sender, KeyEventArgs e) - { - //Handle standard key strikes for clipboard copy: "Ctrl + C" and "Ctrl + Insert" - if (e.Control && (e.KeyCode == Keys.C || e.KeyCode == Keys.Insert)) - { - var buttonsTextLine = (button1.Visible ? button1.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty) - + (button2.Visible ? button2.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty) - + (button3.Visible ? button3.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty); - - //Build same clipboard text like the standard .Net MessageBox - var textForClipboard = STANDARD_MESSAGEBOX_SEPARATOR_LINES - + Text + Environment.NewLine - + STANDARD_MESSAGEBOX_SEPARATOR_LINES - + richTextBoxMessage.Text + Environment.NewLine - + STANDARD_MESSAGEBOX_SEPARATOR_LINES - + buttonsTextLine.Replace("&", string.Empty) + Environment.NewLine - + STANDARD_MESSAGEBOX_SEPARATOR_LINES; - - //Set text in clipboard - Clipboard.SetText(textForClipboard); - } - } - - #endregion - - #region Properties (only used for binding) - - public string CaptionText { get; set; } - public string MessageText { get; set; } - - #endregion - - #region Public show function - - public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) - { - //Create a new instance of the FlexibleMessageBox form - var flexibleMessageBoxForm = new FlexibleMessageBoxForm - { - ShowInTaskbar = false, - - //Bind the caption and the message text - CaptionText = caption, - MessageText = text - }; - flexibleMessageBoxForm.FlexibleMessageBoxFormBindingSource.DataSource = flexibleMessageBoxForm; - - //Set the buttons visibilities and texts. Also set a default button. - SetDialogButtons(flexibleMessageBoxForm, buttons, defaultButton); - - //Set the dialogs icon. When no icon is used: Correct placement and width of rich text box. - SetDialogIcon(flexibleMessageBoxForm, icon); - - //Set the font for all controls - flexibleMessageBoxForm.Font = FONT; - flexibleMessageBoxForm.richTextBoxMessage.Font = FONT; - - //Calculate the dialogs start size (Try to auto-size width to show longest text row). Also set the maximum dialog size. - SetDialogSizes(flexibleMessageBoxForm, text, caption); - - //Set the dialogs start position when given. Otherwise center the dialog on the current screen. - SetDialogStartPosition(flexibleMessageBoxForm, owner); - - //Show the dialog - return flexibleMessageBoxForm.ShowDialog(owner); - } - - #endregion - } //class FlexibleMessageBoxForm - - #endregion - } -} diff --git a/VG Music Studio/UI/ImageComboBox.cs b/VG Music Studio/UI/ImageComboBox.cs deleted file mode 100644 index c928af92..00000000 --- a/VG Music Studio/UI/ImageComboBox.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Drawing; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - internal class ImageComboBox : ComboBox - { - private const int _imgSize = 15; - private bool _open = false; - - public ImageComboBox() - { - DrawMode = DrawMode.OwnerDrawFixed; - DropDownStyle = ComboBoxStyle.DropDown; - } - - protected override void OnDrawItem(DrawItemEventArgs e) - { - e.DrawBackground(); - e.DrawFocusRectangle(); - - if (e.Index >= 0) - { - ImageComboBoxItem item = Items[e.Index] as ImageComboBoxItem ?? throw new InvalidCastException($"Item was not of type \"{nameof(ImageComboBoxItem)}\""); - int indent = _open ? item.IndentLevel : 0; - e.Graphics.DrawImage(item.Image, e.Bounds.Left + (indent * _imgSize), e.Bounds.Top, _imgSize, _imgSize); - e.Graphics.DrawString(item.ToString(), e.Font, new SolidBrush(e.ForeColor), e.Bounds.Left + (indent * _imgSize) + _imgSize, e.Bounds.Top); - } - - base.OnDrawItem(e); - } - protected override void OnDropDown(EventArgs e) - { - _open = true; - base.OnDropDown(e); - } - protected override void OnDropDownClosed(EventArgs e) - { - _open = false; - base.OnDropDownClosed(e); - } - } - internal class ImageComboBoxItem - { - public object Item { get; } - public Image Image { get; } - public int IndentLevel { get; } - - public ImageComboBoxItem(object item, Image image, int indentLevel) - { - Item = item; - Image = image; - IndentLevel = indentLevel; - } - - public override string ToString() - { - return Item.ToString(); - } - } -} diff --git a/VG Music Studio/UI/MainForm.cs b/VG Music Studio/UI/MainForm.cs deleted file mode 100644 index 761394fa..00000000 --- a/VG Music Studio/UI/MainForm.cs +++ /dev/null @@ -1,836 +0,0 @@ -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using Microsoft.WindowsAPICodePack.Dialogs; -using Microsoft.WindowsAPICodePack.Taskbar; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory("")] - internal class MainForm : ThemedForm - { - private const int _intendedWidth = 675; - private const int _intendedHeight = 675 + 1 + 125 + 24; - - public static MainForm Instance { get; } = new MainForm(); - - public readonly bool[] PianoTracks = new bool[SongInfoControl.SongInfo.MaxTracks]; - - private bool _playlistPlaying; - private Config.Playlist _curPlaylist; - private long _curSong = -1; - private readonly List _playedSongs = new List(); - private readonly List _remainingSongs = new List(); - - private TrackViewer _trackViewer; - - #region Controls - - private readonly MenuStrip _mainMenu; - private readonly ToolStripMenuItem _fileItem, _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem, - _dataItem, _trackViewerItem, _exportDLSItem, _exportMIDIItem, _exportSF2Item, _exportWAVItem, - _playlistItem, _endPlaylistItem; - private readonly Timer _timer; - private readonly ThemedNumeric _songNumerical; - private readonly ThemedButton _playButton, _pauseButton, _stopButton; - private readonly SplitContainer _splitContainer; - private readonly PianoControl _piano; - private readonly ColorSlider _volumeBar, _positionBar; - private readonly SongInfoControl _songInfo; - private readonly ImageComboBox _songsComboBox; - private readonly ThumbnailToolBarButton _prevTButton, _toggleTButton, _nextTButton; - - #endregion - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _timer.Dispose(); - } - base.Dispose(disposing); - } - private MainForm() - { - for (int i = 0; i < PianoTracks.Length; i++) - { - PianoTracks[i] = true; - } - - // File Menu - _openDSEItem = new ToolStripMenuItem { Text = Strings.MenuOpenDSE }; - _openDSEItem.Click += OpenDSE; - _openAlphaDreamItem = new ToolStripMenuItem { Text = Strings.MenuOpenAlphaDream }; - _openAlphaDreamItem.Click += OpenAlphaDream; - _openMP2KItem = new ToolStripMenuItem { Text = Strings.MenuOpenMP2K }; - _openMP2KItem.Click += OpenMP2K; - _openSDATItem = new ToolStripMenuItem { Text = Strings.MenuOpenSDAT }; - _openSDATItem.Click += OpenSDAT; - _fileItem = new ToolStripMenuItem { Text = Strings.MenuFile }; - _fileItem.DropDownItems.AddRange(new ToolStripItem[] { _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem }); - - // Data Menu - _trackViewerItem = new ToolStripMenuItem { ShortcutKeys = Keys.Control | Keys.T, Text = Strings.TrackViewerTitle }; - _trackViewerItem.Click += OpenTrackViewer; - _exportDLSItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveDLS }; - _exportDLSItem.Click += ExportDLS; - _exportMIDIItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveMIDI }; - _exportMIDIItem.Click += ExportMIDI; - _exportSF2Item = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveSF2 }; - _exportSF2Item.Click += ExportSF2; - _exportWAVItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveWAV }; - _exportWAVItem.Click += ExportWAV; - _dataItem = new ToolStripMenuItem { Text = Strings.MenuData }; - _dataItem.DropDownItems.AddRange(new ToolStripItem[] { _trackViewerItem, _exportDLSItem, _exportMIDIItem, _exportSF2Item, _exportWAVItem }); - - // Playlist Menu - _endPlaylistItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuEndPlaylist }; - _endPlaylistItem.Click += EndCurrentPlaylist; - _playlistItem = new ToolStripMenuItem { Text = Strings.MenuPlaylist }; - _playlistItem.DropDownItems.AddRange(new ToolStripItem[] { _endPlaylistItem }); - - // Main Menu - _mainMenu = new MenuStrip { Size = new Size(_intendedWidth, 24) }; - _mainMenu.Items.AddRange(new ToolStripItem[] { _fileItem, _dataItem, _playlistItem }); - - // Buttons - _playButton = new ThemedButton { Enabled = false, ForeColor = Color.MediumSpringGreen, Text = Strings.PlayerPlay }; - _playButton.Click += (o, e) => Play(); - _pauseButton = new ThemedButton { Enabled = false, ForeColor = Color.DeepSkyBlue, Text = Strings.PlayerPause }; - _pauseButton.Click += (o, e) => Pause(); - _stopButton = new ThemedButton { Enabled = false, ForeColor = Color.MediumVioletRed, Text = Strings.PlayerStop }; - _stopButton.Click += (o, e) => Stop(); - - // Numerical - _songNumerical = new ThemedNumeric { Enabled = false, Minimum = 0, Visible = false }; - _songNumerical.ValueChanged += SongNumerical_ValueChanged; - - // Timer - _timer = new Timer(); - _timer.Tick += UpdateUI; - - // Piano - _piano = new PianoControl(); - - // Volume bar - _volumeBar = new ColorSlider { Enabled = false, LargeChange = 20, Maximum = 100, SmallChange = 5 }; - _volumeBar.ValueChanged += VolumeBar_ValueChanged; - - // Position bar - _positionBar = new ColorSlider { AcceptKeys = false, Enabled = false, Maximum = 0 }; - _positionBar.MouseUp += PositionBar_MouseUp; - _positionBar.MouseDown += PositionBar_MouseDown; - - // Playlist box - _songsComboBox = new ImageComboBox { Enabled = false }; - _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; - - // Track info - _songInfo = new SongInfoControl { Dock = DockStyle.Fill }; - - // Split container - _splitContainer = new SplitContainer { BackColor = Theme.TitleBar, Dock = DockStyle.Fill, IsSplitterFixed = true, Orientation = Orientation.Horizontal, SplitterWidth = 1 }; - _splitContainer.Panel1.Controls.AddRange(new Control[] { _playButton, _pauseButton, _stopButton, _songNumerical, _songsComboBox, _piano, _volumeBar, _positionBar }); - _splitContainer.Panel2.Controls.Add(_songInfo); - - // MainForm - ClientSize = new Size(_intendedWidth, _intendedHeight); - Controls.AddRange(new Control[] { _splitContainer, _mainMenu }); - MainMenuStrip = _mainMenu; - MinimumSize = new Size(_intendedWidth + (Width - _intendedWidth), _intendedHeight + (Height - _intendedHeight)); // Borders - Resize += OnResize; - Text = Utils.ProgramName; - - // Taskbar Buttons - if (TaskbarManager.IsPlatformSupported) - { - _prevTButton = new ThumbnailToolBarButton(Resources.IconPrevious, Strings.PlayerPreviousSong); - _prevTButton.Click += PlayPreviousSong; - _toggleTButton = new ThumbnailToolBarButton(Resources.IconPlay, Strings.PlayerPlay); - _toggleTButton.Click += TogglePlayback; - _nextTButton = new ThumbnailToolBarButton(Resources.IconNext, Strings.PlayerNextSong); - _nextTButton.Click += PlayNextSong; - _prevTButton.Enabled = _toggleTButton.Enabled = _nextTButton.Enabled = false; - TaskbarManager.Instance.ThumbnailToolBars.AddButtons(Handle, _prevTButton, _toggleTButton, _nextTButton); - } - - OnResize(null, null); - } - - private void VolumeBar_ValueChanged(object sender, EventArgs e) - { - Engine.Instance.Mixer.SetVolume(_volumeBar.Value / (float)_volumeBar.Maximum); - } - public void SetVolumeBarValue(float volume) - { - _volumeBar.ValueChanged -= VolumeBar_ValueChanged; - _volumeBar.Value = (int)(volume * _volumeBar.Maximum); - _volumeBar.ValueChanged += VolumeBar_ValueChanged; - } - private bool _positionBarFree = true; - private void PositionBar_MouseUp(object sender, MouseEventArgs e) - { - if (e.Button == MouseButtons.Left) - { - Engine.Instance.Player.SetCurrentPosition(_positionBar.Value); - _positionBarFree = true; - LetUIKnowPlayerIsPlaying(); - } - } - private void PositionBar_MouseDown(object sender, MouseEventArgs e) - { - if (e.Button == MouseButtons.Left) - { - _positionBarFree = false; - } - } - - private bool _autoplay = false; - private void SongNumerical_ValueChanged(object sender, EventArgs e) - { - _songsComboBox.SelectedIndexChanged -= SongsComboBox_SelectedIndexChanged; - - long index = (long)_songNumerical.Value; - Stop(); - Text = Utils.ProgramName; - _songsComboBox.SelectedIndex = 0; - _songInfo.DeleteData(); - bool success; - try - { - Engine.Instance.Player.LoadSong(index); - success = true; - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, string.Format(Strings.ErrorLoadSong, Engine.Instance.Config.GetSongName(index))); - success = false; - } - - _trackViewer?.UpdateTracks(); - if (success) - { - Config config = Engine.Instance.Config; - List songs = config.Playlists[0].Songs; // Complete "Music" playlist is present in all configs at index 0 - Config.Song song = songs.SingleOrDefault(s => s.Index == index); - if (song != null) - { - Text = $"{Utils.ProgramName} - {song.Name}"; - _songsComboBox.SelectedIndex = songs.IndexOf(song) + 1; // + 1 because the "Music" playlist is first in the combobox - } - _positionBar.Maximum = Engine.Instance.Player.MaxTicks; - _positionBar.LargeChange = _positionBar.Maximum / 10; - _positionBar.SmallChange = _positionBar.LargeChange / 4; - _songInfo.SetNumTracks(Engine.Instance.Player.Events.Length); - if (_autoplay) - { - Play(); - } - } - else - { - _songInfo.SetNumTracks(0); - } - int numTracks = (Engine.Instance.Player.Events?.Length).GetValueOrDefault(); - _positionBar.Enabled = _exportWAVItem.Enabled = success && numTracks > 0; - _exportMIDIItem.Enabled = success && Engine.Instance.Type == Engine.EngineType.GBA_MP2K && numTracks > 0; - _exportDLSItem.Enabled = _exportSF2Item.Enabled = success && Engine.Instance.Type == Engine.EngineType.GBA_AlphaDream; - - _autoplay = true; - _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; - } - private void SongsComboBox_SelectedIndexChanged(object sender, EventArgs e) - { - var item = (ImageComboBoxItem)_songsComboBox.SelectedItem; - if (item.Item is Config.Song song) - { - SetAndLoadSong(song.Index); - } - else if (item.Item is Config.Playlist playlist) - { - if (playlist.Songs.Count > 0 - && FlexibleMessageBox.Show(string.Format(Strings.PlayPlaylistBody, Environment.NewLine + playlist), Strings.MenuPlaylist, MessageBoxButtons.YesNo) == DialogResult.Yes) - { - ResetPlaylistStuff(false); - _curPlaylist = playlist; - Engine.Instance.Player.ShouldFadeOut = _playlistPlaying = true; - Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; - _endPlaylistItem.Enabled = true; - SetAndLoadNextPlaylistSong(); - } - } - } - private void SetAndLoadSong(long index) - { - _curSong = index; - if (_songNumerical.Value == index) - { - SongNumerical_ValueChanged(null, null); - } - else - { - _songNumerical.Value = index; - } - } - private void SetAndLoadNextPlaylistSong() - { - if (_remainingSongs.Count == 0) - { - _remainingSongs.AddRange(_curPlaylist.Songs.Select(s => s.Index)); - if (GlobalConfig.Instance.PlaylistMode == PlaylistMode.Random) - { - _remainingSongs.Shuffle(); - } - } - long nextSong = _remainingSongs[0]; - _remainingSongs.RemoveAt(0); - SetAndLoadSong(nextSong); - } - private void ResetPlaylistStuff(bool enableds) - { - if (Engine.Instance != null) - { - Engine.Instance.Player.ShouldFadeOut = false; - } - _playlistPlaying = false; - _curPlaylist = null; - _curSong = -1; - _remainingSongs.Clear(); - _playedSongs.Clear(); - _endPlaylistItem.Enabled = false; - _songNumerical.Enabled = _songsComboBox.Enabled = enableds; - } - private void EndCurrentPlaylist(object sender, EventArgs e) - { - if (FlexibleMessageBox.Show(Strings.EndPlaylistBody, Strings.MenuPlaylist, MessageBoxButtons.YesNo) == DialogResult.Yes) - { - ResetPlaylistStuff(true); - } - } - - private void OpenDSE(object sender, EventArgs e) - { - var d = new CommonOpenFileDialog - { - Title = Strings.MenuOpenDSE, - IsFolderPicker = true - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - DisposeEngine(); - bool success; - try - { - new Engine(Engine.EngineType.NDS_DSE, d.FileName); - success = true; - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorOpenDSE); - success = false; - } - if (success) - { - var config = (Core.NDS.DSE.Config)Engine.Instance.Config; - FinishLoading(config.BGMFiles.Length); - _songNumerical.Visible = false; - _exportDLSItem.Visible = false; - _exportMIDIItem.Visible = false; - _exportSF2Item.Visible = false; - } - } - } - private void OpenAlphaDream(object sender, EventArgs e) - { - var d = new CommonOpenFileDialog - { - Title = Strings.MenuOpenAlphaDream, - Filters = { new CommonFileDialogFilter(Strings.FilterOpenGBA, ".gba") } - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - DisposeEngine(); - bool success; - try - { - new Engine(Engine.EngineType.GBA_AlphaDream, File.ReadAllBytes(d.FileName)); - success = true; - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorOpenAlphaDream); - success = false; - } - if (success) - { - var config = (Core.GBA.AlphaDream.Config)Engine.Instance.Config; - FinishLoading(config.SongTableSizes[0]); - _songNumerical.Visible = true; - _exportDLSItem.Visible = true; - _exportMIDIItem.Visible = false; - _exportSF2Item.Visible = true; - } - } - } - private void OpenMP2K(object sender, EventArgs e) - { - var d = new CommonOpenFileDialog - { - Title = Strings.MenuOpenMP2K, - Filters = { new CommonFileDialogFilter(Strings.FilterOpenGBA, ".gba") } - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - DisposeEngine(); - bool success; - try - { - new Engine(Engine.EngineType.GBA_MP2K, File.ReadAllBytes(d.FileName)); - success = true; - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorOpenMP2K); - success = false; - } - if (success) - { - var config = (Core.GBA.MP2K.Config)Engine.Instance.Config; - FinishLoading(config.SongTableSizes[0]); - _songNumerical.Visible = true; - _exportDLSItem.Visible = false; - _exportMIDIItem.Visible = true; - _exportSF2Item.Visible = false; - } - } - } - private void OpenSDAT(object sender, EventArgs e) - { - var d = new CommonOpenFileDialog - { - Title = Strings.MenuOpenSDAT, - Filters = { new CommonFileDialogFilter(Strings.FilterOpenSDAT, ".sdat") } - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - DisposeEngine(); - bool success; - try - { - new Engine(Engine.EngineType.NDS_SDAT, new Core.NDS.SDAT.SDAT(File.ReadAllBytes(d.FileName))); - success = true; - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorOpenSDAT); - success = false; - } - if (success) - { - var config = (Core.NDS.SDAT.Config)Engine.Instance.Config; - FinishLoading(config.SDAT.INFOBlock.SequenceInfos.NumEntries); - _songNumerical.Visible = true; - _exportDLSItem.Visible = false; - _exportMIDIItem.Visible = false; - _exportSF2Item.Visible = false; - } - } - } - - private void ExportDLS(object sender, EventArgs e) - { - var d = new CommonSaveFileDialog - { - DefaultFileName = Engine.Instance.Config.GetGameName(), - DefaultExtension = ".dls", - EnsureValidNames = true, - Title = Strings.MenuSaveDLS, - Filters = { new CommonFileDialogFilter(Strings.FilterSaveDLS, ".dls") } - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - try - { - Core.GBA.AlphaDream.SoundFontSaver_DLS.Save((Core.GBA.AlphaDream.Config)Engine.Instance.Config, d.FileName); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveDLS, d.FileName), Text); - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorSaveDLS); - } - } - } - private void ExportMIDI(object sender, EventArgs e) - { - var d = new CommonSaveFileDialog - { - DefaultFileName = Engine.Instance.Config.GetSongName((long)_songNumerical.Value), - DefaultExtension = ".mid", - EnsureValidNames = true, - Title = Strings.MenuSaveMIDI, - Filters = { new CommonFileDialogFilter(Strings.FilterSaveMIDI, ".mid;.midi") } - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - var p = (Core.GBA.MP2K.Player)Engine.Instance.Player; - var args = new Core.GBA.MP2K.Player.MIDISaveArgs - { - SaveCommandsBeforeTranspose = true, - ReverseVolume = false, - TimeSignatures = new List<(int AbsoluteTick, (byte Numerator, byte Denominator))> - { - (0, (4, 4)) - } - }; - try - { - p.SaveAsMIDI(d.FileName, args); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveMIDI, d.FileName), Text); - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorSaveMIDI); - } - } - } - private void ExportSF2(object sender, EventArgs e) - { - var d = new CommonSaveFileDialog - { - DefaultFileName = Engine.Instance.Config.GetGameName(), - DefaultExtension = ".sf2", - EnsureValidNames = true, - Title = Strings.MenuSaveSF2, - Filters = { new CommonFileDialogFilter(Strings.FilterSaveSF2, ".sf2") } - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - try - { - Core.GBA.AlphaDream.SoundFontSaver_SF2.Save((Core.GBA.AlphaDream.Config)Engine.Instance.Config, d.FileName); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveSF2, d.FileName), Text); - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorSaveSF2); - } - } - } - private void ExportWAV(object sender, EventArgs e) - { - var d = new CommonSaveFileDialog - { - DefaultFileName = Engine.Instance.Config.GetSongName((long)_songNumerical.Value), - DefaultExtension = ".wav", - EnsureValidNames = true, - Title = Strings.MenuSaveWAV, - Filters = { new CommonFileDialogFilter(Strings.FilterSaveWAV, ".wav") } - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - Stop(); - bool oldFade = Engine.Instance.Player.ShouldFadeOut; - long oldLoops = Engine.Instance.Player.NumLoops; - Engine.Instance.Player.ShouldFadeOut = true; - Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; - try - { - Engine.Instance.Player.Record(d.FileName); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveWAV, d.FileName), Text); - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorSaveWAV); - } - Engine.Instance.Player.ShouldFadeOut = oldFade; - Engine.Instance.Player.NumLoops = oldLoops; - _stopUI = false; - } - } - - public void LetUIKnowPlayerIsPlaying() - { - if (!_timer.Enabled) - { - _pauseButton.Enabled = _stopButton.Enabled = true; - _pauseButton.Text = Strings.PlayerPause; - _timer.Interval = (int)(1_000d / GlobalConfig.Instance.RefreshRate); - _timer.Start(); - UpdateTaskbarState(); - UpdateTaskbarButtons(); - } - } - private void Play() - { - Engine.Instance.Player.Play(); - LetUIKnowPlayerIsPlaying(); - } - private void Pause() - { - Engine.Instance.Player.Pause(); - if (Engine.Instance.Player.State == PlayerState.Paused) - { - _pauseButton.Text = Strings.PlayerUnpause; - _timer.Stop(); - } - else - { - _pauseButton.Text = Strings.PlayerPause; - _timer.Start(); - } - UpdateTaskbarState(); - UpdateTaskbarButtons(); - } - private void Stop() - { - Engine.Instance.Player.Stop(); - _pauseButton.Enabled = _stopButton.Enabled = false; - _pauseButton.Text = Strings.PlayerPause; - _timer.Stop(); - _songInfo.DeleteData(); - _piano.UpdateKeys(_songInfo.Info, PianoTracks); - UpdatePositionIndicators(0L); - UpdateTaskbarState(); - UpdateTaskbarButtons(); - } - private void TogglePlayback(object sender, EventArgs e) - { - if (Engine.Instance.Player.State == PlayerState.Stopped) - { - Play(); - } - else if (Engine.Instance.Player.State == PlayerState.Paused || Engine.Instance.Player.State == PlayerState.Playing) - { - Pause(); - } - } - private void PlayPreviousSong(object sender, EventArgs e) - { - long prevSong; - if (_playlistPlaying) - { - int index = _playedSongs.Count - 1; - prevSong = _playedSongs[index]; - _playedSongs.RemoveAt(index); - _remainingSongs.Insert(0, _curSong); - } - else - { - prevSong = (long)_songNumerical.Value - 1; - } - SetAndLoadSong(prevSong); - } - private void PlayNextSong(object sender, EventArgs e) - { - if (_playlistPlaying) - { - _playedSongs.Add(_curSong); - SetAndLoadNextPlaylistSong(); - } - else - { - SetAndLoadSong((long)_songNumerical.Value + 1); - } - } - - private void FinishLoading(long numSongs) - { - Engine.Instance.Player.SongEnded += SongEnded; - foreach (Config.Playlist playlist in Engine.Instance.Config.Playlists) - { - _songsComboBox.Items.Add(new ImageComboBoxItem(playlist, Resources.IconPlaylist, 0)); - _songsComboBox.Items.AddRange(playlist.Songs.Select(s => new ImageComboBoxItem(s, Resources.IconSong, 1)).ToArray()); - } - _songNumerical.Maximum = numSongs - 1; -#if DEBUG - //VGMSDebug.EventScan(Engine.Instance.Config.Playlists[0].Songs, numericalVisible); -#endif - _autoplay = false; - SetAndLoadSong(Engine.Instance.Config.Playlists[0].Songs.Count == 0 ? 0 : Engine.Instance.Config.Playlists[0].Songs[0].Index); - _songsComboBox.Enabled = _songNumerical.Enabled = _playButton.Enabled = _volumeBar.Enabled = true; - UpdateTaskbarButtons(); - } - private void DisposeEngine() - { - if (Engine.Instance != null) - { - Stop(); - Engine.Instance.Dispose(); - } - _trackViewer?.UpdateTracks(); - _prevTButton.Enabled = _toggleTButton.Enabled = _nextTButton.Enabled = _songsComboBox.Enabled = _songNumerical.Enabled = _playButton.Enabled = _volumeBar.Enabled = _positionBar.Enabled = false; - Text = Utils.ProgramName; - _songInfo.SetNumTracks(0); - _songInfo.ResetMutes(); - ResetPlaylistStuff(false); - UpdatePositionIndicators(0L); - UpdateTaskbarState(); - _songsComboBox.SelectedIndexChanged -= SongsComboBox_SelectedIndexChanged; - _songNumerical.ValueChanged -= SongNumerical_ValueChanged; - _songNumerical.Visible = false; - _songNumerical.Value = _songNumerical.Maximum = 0; - _songsComboBox.SelectedItem = null; - _songsComboBox.Items.Clear(); - _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; - _songNumerical.ValueChanged += SongNumerical_ValueChanged; - } - private bool _stopUI = false; - private void UpdateUI(object sender, EventArgs e) - { - if (_stopUI) - { - _stopUI = false; - if (_playlistPlaying) - { - _playedSongs.Add(_curSong); - SetAndLoadNextPlaylistSong(); - } - else - { - Stop(); - } - } - else - { - if (WindowState != FormWindowState.Minimized) - { - SongInfoControl.SongInfo info = _songInfo.Info; - Engine.Instance.Player.GetSongState(info); - _piano.UpdateKeys(info, PianoTracks); - _songInfo.Invalidate(); - } - UpdatePositionIndicators(Engine.Instance.Player.ElapsedTicks); - } - } - private void SongEnded() - { - _stopUI = true; - } - private void UpdatePositionIndicators(long ticks) - { - if (_positionBarFree) - { - _positionBar.Value = ticks; - } - if (GlobalConfig.Instance.TaskbarProgress && TaskbarManager.IsPlatformSupported) - { - TaskbarManager.Instance.SetProgressValue((int)ticks, (int)_positionBar.Maximum); - } - } - private void UpdateTaskbarState() - { - if (GlobalConfig.Instance.TaskbarProgress && TaskbarManager.IsPlatformSupported) - { - TaskbarProgressBarState state; - switch (Engine.Instance?.Player.State) - { - case PlayerState.Playing: state = TaskbarProgressBarState.Normal; break; - case PlayerState.Paused: state = TaskbarProgressBarState.Paused; break; - default: state = TaskbarProgressBarState.NoProgress; break; - } - TaskbarManager.Instance.SetProgressState(state); - } - } - private void UpdateTaskbarButtons() - { - if (TaskbarManager.IsPlatformSupported) - { - if (_playlistPlaying) - { - _prevTButton.Enabled = _playedSongs.Count > 0; - _nextTButton.Enabled = true; - } - else - { - _prevTButton.Enabled = _curSong > 0; - _nextTButton.Enabled = _curSong < _songNumerical.Maximum; - } - switch (Engine.Instance.Player.State) - { - case PlayerState.Stopped: _toggleTButton.Icon = Resources.IconPlay; _toggleTButton.Tooltip = Strings.PlayerPlay; break; - case PlayerState.Playing: _toggleTButton.Icon = Resources.IconPause; _toggleTButton.Tooltip = Strings.PlayerPause; break; - case PlayerState.Paused: _toggleTButton.Icon = Resources.IconPlay; _toggleTButton.Tooltip = Strings.PlayerUnpause; break; - } - _toggleTButton.Enabled = true; - } - } - - private void OpenTrackViewer(object sender, EventArgs e) - { - if (_trackViewer != null) - { - _trackViewer.Focus(); - return; - } - _trackViewer = new TrackViewer { Owner = this }; - _trackViewer.FormClosed += (o, s) => _trackViewer = null; - _trackViewer.Show(); - } - - protected override void OnFormClosing(FormClosingEventArgs e) - { - DisposeEngine(); - base.OnFormClosing(e); - } - private void OnResize(object sender, EventArgs e) - { - if (WindowState != FormWindowState.Minimized) - { - _splitContainer.SplitterDistance = (int)(ClientSize.Height / 5.5) - 25; // -25 for menustrip (24) and itself (1) - - int w1 = (int)(_splitContainer.Panel1.Width / 2.35); - int h1 = (int)(_splitContainer.Panel1.Height / 5.0); - - int xoff = _splitContainer.Panel1.Width / 83; - int yoff = _splitContainer.Panel1.Height / 25; - int a, b, c, d; - - // Buttons - a = (w1 / 3) - xoff; - b = (xoff / 2) + 1; - _playButton.Location = new Point(xoff + b, yoff); - _pauseButton.Location = new Point((xoff * 2) + a + b, yoff); - _stopButton.Location = new Point((xoff * 3) + (a * 2) + b, yoff); - _playButton.Size = _pauseButton.Size = _stopButton.Size = new Size(a, h1); - c = yoff + ((h1 - 21) / 2); - _songNumerical.Location = new Point((xoff * 4) + (a * 3) + b, c); - _songNumerical.Size = new Size((int)(a / 1.175), 21); - // Song combobox - d = _splitContainer.Panel1.Width - w1 - xoff; - _songsComboBox.Location = new Point(d, c); - _songsComboBox.Size = new Size(w1, 21); - - // Volume bar - c = (int)(_splitContainer.Panel1.Height / 3.5); - _volumeBar.Location = new Point(xoff, c); - _volumeBar.Size = new Size(w1, h1); - // Position bar - _positionBar.Location = new Point(d, c); - _positionBar.Size = new Size(w1, h1); - - // Piano - _piano.Size = new Size(_splitContainer.Panel1.Width, (int)(_splitContainer.Panel1.Height / 2.5)); // Force it to initialize piano keys again - _piano.Location = new Point((_splitContainer.Panel1.Width - (_piano.WhiteKeyWidth * PianoControl.WhiteKeyCount)) / 2, _splitContainer.Panel1.Height - _piano.Height - 1); - } - } - protected override bool ProcessCmdKey(ref Message msg, Keys keyData) - { - if (keyData == Keys.Space && _playButton.Enabled && !_songsComboBox.Focused) - { - TogglePlayback(null, null); - return true; - } - else - { - return base.ProcessCmdKey(ref msg, keyData); - } - } - } -} diff --git a/VG Music Studio/UI/PianoControl.cs b/VG Music Studio/UI/PianoControl.cs deleted file mode 100644 index 102a4b0a..00000000 --- a/VG Music Studio/UI/PianoControl.cs +++ /dev/null @@ -1,183 +0,0 @@ -#region License - -/* Copyright (c) 2006 Leslie Sanford - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#endregion - -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Util; -using System; -using System.ComponentModel; -using System.Drawing; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory("")] - internal class PianoControl : Control - { - private enum KeyType : byte - { - Black, - White - } - private static readonly KeyType[] KeyTypeTable = new KeyType[12] - { - KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White, KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White - }; - private const double blackKeyScale = 2.0 / 3.0; - - public class PianoKey : Control - { - public bool Dirty; - public bool Pressed; - - public readonly SolidBrush OnBrush = new SolidBrush(Color.Transparent); - private readonly SolidBrush _offBrush; - - public PianoKey(byte k) - { - SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); - SetStyle(ControlStyles.Selectable, false); - _offBrush = new SolidBrush(new HSLColor(160.0, 0.0, KeyTypeTable[k % 12] == KeyType.White ? k / 12 % 2 == 0 ? 240.0 : 120.0 : 0.0)); - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - OnBrush.Dispose(); - _offBrush.Dispose(); - } - base.Dispose(disposing); - } - protected override void OnPaint(PaintEventArgs e) - { - e.Graphics.FillRectangle(Pressed ? OnBrush : _offBrush, 1, 1, Width - 2, Height - 2); - e.Graphics.DrawRectangle(Pens.Black, 0, 0, Width - 1, Height - 1); - base.OnPaint(e); - } - } - - private readonly PianoKey[] _keys = new PianoKey[0x80]; - public const int WhiteKeyCount = 75; - public int WhiteKeyWidth; - - public PianoControl() - { - SetStyle(ControlStyles.Selectable, false); - for (byte k = 0; k <= 0x7F; k++) - { - var key = new PianoKey(k); - _keys[k] = key; - if (KeyTypeTable[k % 12] == KeyType.Black) - { - key.BringToFront(); - } - Controls.Add(key); - } - SetKeySizes(); - } - private void SetKeySizes() - { - WhiteKeyWidth = Width / WhiteKeyCount; - int blackKeyWidth = (int)(WhiteKeyWidth * blackKeyScale); - int blackKeyHeight = (int)(Height * blackKeyScale); - int offset = WhiteKeyWidth - (blackKeyWidth / 2); - int w = 0; - for (int k = 0; k <= 0x7F; k++) - { - PianoKey key = _keys[k]; - if (KeyTypeTable[k % 12] == KeyType.White) - { - key.Height = Height; - key.Width = WhiteKeyWidth; - key.Location = new Point(w * WhiteKeyWidth, 0); - w++; - } - else - { - key.Height = blackKeyHeight; - key.Width = blackKeyWidth; - key.Location = new Point(offset + ((w - 1) * WhiteKeyWidth)); - key.BringToFront(); - } - } - } - - public void UpdateKeys(SongInfoControl.SongInfo info, bool[] enabledTracks) - { - for (int k = 0; k <= 0x7F; k++) - { - PianoKey key = _keys[k]; - key.Dirty = key.Pressed; - key.Pressed = false; - } - for (int i = SongInfoControl.SongInfo.MaxTracks - 1; i >= 0; i--) - { - if (enabledTracks[i]) - { - SongInfoControl.SongInfo.Track tin = info.Tracks[i]; - for (int nk = 0; nk < SongInfoControl.SongInfo.MaxKeys; nk++) - { - byte k = tin.Keys[nk]; - if (k == byte.MaxValue) - { - break; - } - else - { - PianoKey key = _keys[k]; - key.OnBrush.Color = GlobalConfig.Instance.Colors[tin.Voice]; - key.Pressed = key.Dirty = true; - } - } - } - } - for (int k = 0; k <= 0x7F; k++) - { - PianoKey key = _keys[k]; - if (key.Dirty) - { - key.Invalidate(); - } - } - } - - protected override void OnResize(EventArgs e) - { - SetKeySizes(); - base.OnResize(e); - } - protected override void Dispose(bool disposing) - { - if (disposing) - { - for (int k = 0; k < 0x80; k++) - { - _keys[k].Dispose(); - } - } - base.Dispose(disposing); - } - } -} diff --git a/VG Music Studio/UI/SongInfoControl.cs b/VG Music Studio/UI/SongInfoControl.cs deleted file mode 100644 index ce91a71d..00000000 --- a/VG Music Studio/UI/SongInfoControl.cs +++ /dev/null @@ -1,354 +0,0 @@ -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.ComponentModel; -using System.Drawing; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory("")] - internal class SongInfoControl : Control - { - public class SongInfo - { - public class Track - { - public long Position; - public byte Voice; - public byte Volume; - public int LFO; - public long Rest; - public sbyte Panpot; - public float LeftVolume; - public float RightVolume; - public int PitchBend; - public byte Extra; - public string Type; - public byte[] Keys = new byte[MaxKeys]; - - public int PreviousKeysTime; - public string PreviousKeys; - - public Track() - { - for (int i = 0; i < MaxKeys; i++) - { - Keys[i] = byte.MaxValue; - } - } - } - public const int MaxKeys = 32 + 1; // DSE is currently set to use 32 channels - public const int MaxTracks = 18; // PMD2 has a few songs with 18 tracks - - public ushort Tempo; - public Track[] Tracks = new Track[MaxTracks]; - - public SongInfo() - { - for (int i = 0; i < MaxTracks; i++) - { - Tracks[i] = new Track(); - } - } - } - - private const int _checkboxSize = 15; - - private readonly CheckBox[] _mutes; - private readonly CheckBox[] _pianos; - private readonly SolidBrush _solidBrush = new SolidBrush(Theme.PlayerColor); - private readonly Pen _pen = new Pen(Color.Transparent); - - public SongInfo Info; - private int _numTracksToDraw; - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _solidBrush.Dispose(); - _pen.Dispose(); - } - base.Dispose(disposing); - } - public SongInfoControl() - { - SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); - SetStyle(ControlStyles.Selectable, false); - Font = new Font("Segoe UI", 10.5f, FontStyle.Regular, GraphicsUnit.Point); - Size = new Size(675, 675); - - _pianos = new CheckBox[SongInfo.MaxTracks + 1]; - _mutes = new CheckBox[SongInfo.MaxTracks + 1]; - for (int i = 0; i < SongInfo.MaxTracks + 1; i++) - { - _pianos[i] = new CheckBox - { - BackColor = Color.Transparent, - Checked = true, - Size = new Size(_checkboxSize, _checkboxSize), - TabStop = false - }; - _pianos[i].CheckStateChanged += TogglePiano; - _mutes[i] = new CheckBox - { - BackColor = Color.Transparent, - Checked = true, - Size = new Size(_checkboxSize, _checkboxSize), - TabStop = false - }; - _mutes[i].CheckStateChanged += ToggleMute; - } - Controls.AddRange(_pianos); - Controls.AddRange(_mutes); - - SetNumTracks(0); - DeleteData(); - } - - private void TogglePiano(object sender, EventArgs e) - { - var check = (CheckBox)sender; - CheckBox master = _pianos[SongInfo.MaxTracks]; - if (check == master) - { - bool b = check.CheckState != CheckState.Unchecked; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - _pianos[i].Checked = b; - } - } - else - { - int numChecked = 0; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - if (_pianos[i] == check) - { - MainForm.Instance.PianoTracks[i] = _pianos[i].Checked; - } - if (_pianos[i].Checked) - { - numChecked++; - } - } - master.CheckStateChanged -= TogglePiano; - master.CheckState = numChecked == SongInfo.MaxTracks ? CheckState.Checked : (numChecked == 0 ? CheckState.Unchecked : CheckState.Indeterminate); - master.CheckStateChanged += TogglePiano; - } - } - private void ToggleMute(object sender, EventArgs e) - { - var check = (CheckBox)sender; - CheckBox master = _mutes[SongInfo.MaxTracks]; - if (check == master) - { - bool b = check.CheckState != CheckState.Unchecked; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - _mutes[i].Checked = b; - } - } - else - { - int numChecked = 0; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - if (_mutes[i] == check) - { - Engine.Instance.Mixer.Mutes[i] = !check.Checked; - } - if (_mutes[i].Checked) - { - numChecked++; - } - } - master.CheckStateChanged -= ToggleMute; - master.CheckState = numChecked == SongInfo.MaxTracks ? CheckState.Checked : (numChecked == 0 ? CheckState.Unchecked : CheckState.Indeterminate); - master.CheckStateChanged += ToggleMute; - } - } - - public void DeleteData() - { - Info = new SongInfo(); - Invalidate(); - } - public void SetNumTracks(int num) - { - _numTracksToDraw = num; - _pianos[SongInfo.MaxTracks].Enabled = _mutes[SongInfo.MaxTracks].Enabled = num > 0; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - _pianos[i].Visible = _mutes[i].Visible = i < num; - } - OnResize(EventArgs.Empty); - } - public void ResetMutes() - { - for (int i = 0; i < SongInfo.MaxTracks + 1; i++) - { - CheckBox mute = _mutes[i]; - mute.CheckStateChanged -= ToggleMute; - mute.CheckState = CheckState.Checked; - mute.CheckStateChanged += ToggleMute; - } - } - - private float _infoHeight, _infoY, _positionX, _keysX, _delayX, _typeEndX, _typeX, _voicesX, _row2ElementAdditionX, _yMargin, _trackHeight, _row2Offset, _tempoX; - private int _barHeight, _barStartX, _barWidth, _bwd, _barRightBoundX, _barCenterX; - protected override void OnResize(EventArgs e) - { - _infoHeight = Height / 30f; - _infoY = _infoHeight - (TextRenderer.MeasureText("A", Font).Height * 1.125f); - _positionX = (_checkboxSize * 2) + 2; - int fWidth = Width - (int)_positionX; // Width between checkboxes' edges and the window edge - _keysX = _positionX + (fWidth / 4.4f); - _delayX = _positionX + (fWidth / 7.5f); - _typeEndX = _positionX + fWidth - (fWidth / 100f); - _typeX = _typeEndX - TextRenderer.MeasureText(Strings.PlayerType, Font).Width; - _voicesX = _positionX + (fWidth / 25f); - _row2ElementAdditionX = fWidth / 15f; - - _yMargin = Height / 200f; - _trackHeight = (Height - _yMargin) / ((_numTracksToDraw < 16 ? 16 : _numTracksToDraw) * 1.04f); - _row2Offset = _trackHeight / 2.5f; - _barHeight = (int)(Height / 30.3f); - _barStartX = (int)(_positionX + (fWidth / 2.35f)); - _barWidth = (int)(fWidth / 2.95f); - _bwd = _barWidth % 2; // Add/Subtract by 1 if the bar width is odd - _barRightBoundX = _barStartX + _barWidth - _bwd; - _barCenterX = _barStartX + (_barWidth / 2); - - _tempoX = _barCenterX - (TextRenderer.MeasureText(string.Format("{0} - 999", Strings.PlayerTempo), Font).Width / 2); - - if (_mutes != null) - { - int x1 = 3; - int x2 = _checkboxSize + 4; - int y = (int)_infoY + 3; - _mutes[SongInfo.MaxTracks].Location = new Point(x1, y); - _pianos[SongInfo.MaxTracks].Location = new Point(x2, y); - for (int i = 0; i < _numTracksToDraw; i++) - { - float r1y = _infoHeight + _yMargin + (i * _trackHeight); - y = (int)r1y + 4; - _mutes[i].Location = new Point(x1, y); - _pianos[i].Location = new Point(x2, y); - } - } - - base.OnResize(e); - } - protected override void OnPaint(PaintEventArgs e) - { - _solidBrush.Color = Theme.PlayerColor; - e.Graphics.FillRectangle(_solidBrush, e.ClipRectangle); - - e.Graphics.DrawString(Strings.PlayerPosition, Font, Brushes.Lime, _positionX, _infoY); - e.Graphics.DrawString(Strings.PlayerRest, Font, Brushes.Crimson, _delayX, _infoY); - e.Graphics.DrawString(Strings.PlayerNotes, Font, Brushes.Turquoise, _keysX, _infoY); - e.Graphics.DrawString("L", Font, Brushes.GreenYellow, _barStartX - 5, _infoY); - e.Graphics.DrawString(string.Format("{0} - ", Strings.PlayerTempo) + Info.Tempo, Font, Brushes.Cyan, _tempoX, _infoY); - e.Graphics.DrawString("R", Font, Brushes.GreenYellow, _barRightBoundX - 5, _infoY); - e.Graphics.DrawString(Strings.PlayerType, Font, Brushes.DeepPink, _typeX, _infoY); - e.Graphics.DrawLine(Pens.Gold, 0, _infoHeight, Width, _infoHeight); - - for (int i = 0; i < _numTracksToDraw; i++) - { - SongInfo.Track track = Info.Tracks[i]; - float r1y = _infoHeight + _yMargin + (i * _trackHeight); // Row 1 y - e.Graphics.DrawString(string.Format("0x{0:X}", track.Position), Font, Brushes.Lime, _positionX, r1y); - e.Graphics.DrawString(track.Rest.ToString(), Font, Brushes.Crimson, _delayX, r1y); - - float r2y = r1y + _row2Offset; // Row 2 y - e.Graphics.DrawString(track.Panpot.ToString(), Font, Brushes.OrangeRed, _voicesX + _row2ElementAdditionX, r2y); - e.Graphics.DrawString(track.Volume.ToString(), Font, Brushes.LightSeaGreen, _voicesX + (_row2ElementAdditionX * 2), r2y); - e.Graphics.DrawString(track.LFO.ToString(), Font, Brushes.SkyBlue, _voicesX + (_row2ElementAdditionX * 3), r2y); - e.Graphics.DrawString(track.PitchBend.ToString(), Font, Brushes.Purple, _voicesX + (_row2ElementAdditionX * 4), r2y); - e.Graphics.DrawString(track.Extra.ToString(), Font, Brushes.HotPink, _voicesX + (_row2ElementAdditionX * 5), r2y); - - int by = (int)(r1y + _yMargin); // Bar y - int byh = by + _barHeight; - e.Graphics.DrawString(track.Type, Font, Brushes.DeepPink, _typeEndX - e.Graphics.MeasureString(track.Type, Font).Width, by + (_row2Offset / (Font.Size / 2.5f))); - e.Graphics.DrawLine(Pens.GreenYellow, _barStartX, by, _barStartX, byh); // Left bar bound line - e.Graphics.DrawLine(Pens.GreenYellow, _barRightBoundX, by, _barRightBoundX, byh); // Right bar bound line - if (GlobalConfig.Instance.PanpotIndicators) - { - int pax = (int)(_barStartX + (_barWidth / 2) + (_barWidth / 2 * (track.Panpot / (float)0x40))); // Pan line x - e.Graphics.DrawLine(Pens.OrangeRed, pax, by, pax, byh); // Pan line - } - - { - Color color = GlobalConfig.Instance.Colors[track.Voice]; - _solidBrush.Color = color; - _pen.Color = color; - e.Graphics.DrawString(track.Voice.ToString(), Font, _solidBrush, _voicesX, r2y); - var rect = new Rectangle((int)(_barStartX + (_barWidth / 2) - (track.LeftVolume * _barWidth / 2)) + _bwd, - by, - (int)((track.LeftVolume + track.RightVolume) * _barWidth / 2), - _barHeight); - if (!rect.IsEmpty) - { - float velocity = (track.LeftVolume + track.RightVolume) * 2f; - if (velocity > 1f) - { - velocity = 1f; - } - _solidBrush.Color = Color.FromArgb((byte)(velocity * byte.MaxValue), color); - e.Graphics.FillRectangle(_solidBrush, rect); - e.Graphics.DrawRectangle(_pen, rect); - } - if (GlobalConfig.Instance.CenterIndicators) - { - e.Graphics.DrawLine(_pen, _barCenterX, by, _barCenterX, byh); // Center line - } - } - { - string keysString; - if (track.Keys[0] == byte.MaxValue) - { - if (track.PreviousKeysTime != 0) - { - track.PreviousKeysTime--; - keysString = track.PreviousKeys; - } - else - { - keysString = string.Empty; - } - } - else - { - keysString = string.Empty; - for (int nk = 0; nk < SongInfo.MaxKeys; nk++) - { - byte k = track.Keys[nk]; - if (k == byte.MaxValue) - { - break; - } - else - { - if (nk != 0) - { - keysString += ' '; - } - keysString += Utils.GetNoteName(k); - } - } - track.PreviousKeysTime = GlobalConfig.Instance.RefreshRate << 2; - track.PreviousKeys = keysString; - } - if (keysString != string.Empty) - { - e.Graphics.DrawString(keysString, Font, Brushes.Turquoise, _keysX, r1y); - } - } - } - base.OnPaint(e); - } - } -} diff --git a/VG Music Studio/UI/Theme.cs b/VG Music Studio/UI/Theme.cs deleted file mode 100644 index 0973e2a7..00000000 --- a/VG Music Studio/UI/Theme.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Drawing; -using System.Runtime.InteropServices; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - internal static class Theme - { - public static readonly Font Font = new Font("Segoe UI", 8f, FontStyle.Bold); - public static readonly Color - BackColor = Color.FromArgb(33, 33, 39), - BackColorDisabled = Color.FromArgb(35, 42, 47), - BackColorMouseOver = Color.FromArgb(32, 37, 47), - BorderColor = Color.FromArgb(25, 120, 186), - BorderColorDisabled = Color.FromArgb(47, 55, 60), - ForeColor = Color.FromArgb(94, 159, 230), - PlayerColor = Color.FromArgb(8, 8, 8), - SelectionColor = Color.FromArgb(7, 51, 141), - TitleBar = Color.FromArgb(16, 40, 63); - - public static HSLColor DrainColor(Color c) - { - var drained = new HSLColor(c); - drained.Saturation /= 2.5; - return drained; - } - } - - internal class ThemedButton : Button - { - public ThemedButton() : base() - { - FlatAppearance.MouseOverBackColor = Theme.BackColorMouseOver; - FlatStyle = FlatStyle.Flat; - Font = Theme.Font; - ForeColor = Theme.ForeColor; - } - protected override void OnEnabledChanged(EventArgs e) - { - base.OnEnabledChanged(e); - BackColor = Enabled ? Theme.BackColor : Theme.BackColorDisabled; - FlatAppearance.BorderColor = Enabled ? Theme.BorderColor : Theme.BorderColorDisabled; - } - protected override void OnPaint(PaintEventArgs e) - { - base.OnPaint(e); - if (!Enabled) - { - TextRenderer.DrawText(e.Graphics, Text, Font, ClientRectangle, Theme.DrainColor(ForeColor), BackColor); - } - } - protected override bool ShowFocusCues => false; - } - internal class ThemedLabel : Label - { - public ThemedLabel() : base() - { - Font = Theme.Font; - ForeColor = Theme.ForeColor; - } - } - internal class ThemedForm : Form - { - public ThemedForm() : base() - { - BackColor = Theme.BackColor; - Icon = Resources.Icon; - } - } - internal class ThemedPanel : Panel - { - public ThemedPanel() : base() - { - SetStyle(ControlStyles.UserPaint | ControlStyles.ResizeRedraw | ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); - } - protected override void OnPaint(PaintEventArgs e) - { - base.OnPaint(e); - using (var b = new SolidBrush(BackColor)) - { - e.Graphics.FillRectangle(b, e.ClipRectangle); - } - using (var b = new SolidBrush(Theme.BorderColor)) - using (var p = new Pen(b, 2)) - { - e.Graphics.DrawRectangle(p, e.ClipRectangle); - } - } - private const int WM_PAINT = 0xF; - protected override void WndProc(ref Message m) - { - if (m.Msg == WM_PAINT) - { - Invalidate(); - } - base.WndProc(ref m); - } - } - internal class ThemedTextBox : TextBox - { - public ThemedTextBox() : base() - { - BackColor = Theme.BackColor; - Font = Theme.Font; - ForeColor = Theme.ForeColor; - } - [DllImport("user32.dll")] - private static extern IntPtr GetWindowDC(IntPtr hWnd); - [DllImport("user32.dll")] - private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC); - [DllImport("user32.dll")] - private static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprc, IntPtr hrgn, uint flags); - private const int WM_NCPAINT = 0x85; - private const uint RDW_INVALIDATE = 0x1; - private const uint RDW_IUPDATENOW = 0x100; - private const uint RDW_FRAME = 0x400; - protected override void WndProc(ref Message m) - { - base.WndProc(ref m); - if (m.Msg == WM_NCPAINT && BorderStyle == BorderStyle.Fixed3D) - { - IntPtr hdc = GetWindowDC(Handle); - using (var g = Graphics.FromHdcInternal(hdc)) - using (var p = new Pen(Theme.BorderColor)) - { - g.DrawRectangle(p, new Rectangle(0, 0, Width - 1, Height - 1)); - } - ReleaseDC(Handle, hdc); - } - } - protected override void OnSizeChanged(EventArgs e) - { - base.OnSizeChanged(e); - RedrawWindow(Handle, IntPtr.Zero, IntPtr.Zero, RDW_FRAME | RDW_IUPDATENOW | RDW_INVALIDATE); - } - } - internal class ThemedRichTextBox : RichTextBox - { - public ThemedRichTextBox() : base() - { - BackColor = Theme.BackColor; - Font = Theme.Font; - ForeColor = Theme.ForeColor; - SelectionColor = Theme.SelectionColor; - } - } - internal class ThemedNumeric : NumericUpDown - { - public ThemedNumeric() : base() - { - BackColor = Theme.BackColor; - Font = new Font(Theme.Font.FontFamily, 7.5f, Theme.Font.Style); - ForeColor = Theme.ForeColor; - TextAlign = HorizontalAlignment.Center; - } - protected override void OnPaint(PaintEventArgs e) - { - base.OnPaint(e); - ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Enabled ? Theme.BorderColor : Theme.BorderColorDisabled, ButtonBorderStyle.Solid); - } - } -} diff --git a/VG Music Studio/UI/TrackViewer.cs b/VG Music Studio/UI/TrackViewer.cs deleted file mode 100644 index 7aa83b97..00000000 --- a/VG Music Studio/UI/TrackViewer.cs +++ /dev/null @@ -1,113 +0,0 @@ -using BrightIdeasSoftware; -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.Linq; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory("")] - internal class TrackViewer : ThemedForm - { - private List _events; - private readonly ObjectListView _listView; - private readonly ComboBox _tracksBox; - - public TrackViewer() - { - int w = (600 / 2) - 12 - 6, h = 400 - 12 - 11; - _listView = new ObjectListView - { - FullRowSelect = true, - HeaderStyle = ColumnHeaderStyle.Nonclickable, - HideSelection = false, - Location = new Point(12, 12), - MultiSelect = false, - RowFormatter = RowColorer, - ShowGroups = false, - Size = new Size(w, h), - UseFiltering = true, - UseFilterIndicator = true - }; - OLVColumn c1, c2, c3, c4; - c1 = new OLVColumn(Strings.TrackViewerEvent, "Command.Label"); - c2 = new OLVColumn(Strings.TrackViewerArguments, "Command.Arguments") { UseFiltering = false }; - c3 = new OLVColumn(Strings.TrackViewerOffset, "Offset") { AspectToStringFormat = "0x{0:X}", UseFiltering = false }; - c4 = new OLVColumn(Strings.TrackViewerTicks, "Ticks") { AspectGetter = (o) => string.Join(", ", ((SongEvent)o).Ticks), UseFiltering = false }; - c1.Width = c2.Width = c3.Width = 72; - c4.Width = 47; - c1.Hideable = c2.Hideable = c3.Hideable = c4.Hideable = false; - c1.TextAlign = c2.TextAlign = c3.TextAlign = c4.TextAlign = HorizontalAlignment.Center; - _listView.AllColumns.AddRange(new OLVColumn[] { c1, c2, c3, c4 }); - _listView.RebuildColumns(); - _listView.ItemActivate += ListView_ItemActivate; - - var panel1 = new ThemedPanel { Location = new Point(306, 12), Size = new Size(w, h) }; - _tracksBox = new ComboBox - { - Enabled = false, - Location = new Point(4, 4), - Size = new Size(100, 21) - }; - _tracksBox.SelectedIndexChanged += TracksBox_SelectedIndexChanged; - panel1.Controls.AddRange(new Control[] { _tracksBox }); - - ClientSize = new Size(600, 400); - Controls.AddRange(new Control[] { _listView, panel1 }); - FormBorderStyle = FormBorderStyle.FixedDialog; - MaximizeBox = false; - Text = $"{Utils.ProgramName} ― {Strings.TrackViewerTitle}"; - - UpdateTracks(); - } - - private void ListView_ItemActivate(object sender, EventArgs e) - { - List list = ((SongEvent)_listView.SelectedItem.RowObject).Ticks; - if (list.Count > 0) - { - Engine.Instance?.Player.SetCurrentPosition(list[0]); - MainForm.Instance.LetUIKnowPlayerIsPlaying(); - } - } - - private void RowColorer(OLVListItem item) - { - item.BackColor = ((SongEvent)item.RowObject).Command.Color; - } - - private void TracksBox_SelectedIndexChanged(object sender, EventArgs e) - { - int i = _tracksBox.SelectedIndex; - if (i != -1) - { - _events = Engine.Instance.Player.Events[i]; - _listView.SetObjects(_events); - } - else - { - _listView.Items.Clear(); - } - } - public void UpdateTracks() - { - int numTracks = (Engine.Instance?.Player.Events?.Length).GetValueOrDefault(); - bool tracks = numTracks > 0; - _tracksBox.Enabled = tracks; - if (tracks) - { - // Track 0, Track 1, ... - _tracksBox.DataSource = Enumerable.Range(0, numTracks).Select(i => string.Format(Strings.TrackViewerTrackX, i)).ToList(); - } - else - { - _tracksBox.DataSource = null; - } - } - } -} \ No newline at end of file diff --git a/VG Music Studio/UI/ValueTextBox.cs b/VG Music Studio/UI/ValueTextBox.cs deleted file mode 100644 index a83e3516..00000000 --- a/VG Music Studio/UI/ValueTextBox.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Kermalis.VGMusicStudio.Util; -using System; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - internal class ValueTextBox : ThemedTextBox - { - private bool _hex = false; - public bool Hexadecimal - { - get => _hex; - set - { - _hex = value; - OnTextChanged(EventArgs.Empty); - SelectionStart = Text.Length; - } - } - private long _max = long.MaxValue; - public long Maximum - { - get => _max; - set - { - _max = value; - OnTextChanged(EventArgs.Empty); - } - } - private long _min = 0; - public long Minimum - { - get => _min; - set - { - _min = value; - OnTextChanged(EventArgs.Empty); - } - } - public long Value - { - get - { - if (TextLength > 0) - { - if (Utils.TryParseValue(Text, _min, _max, out long l)) - { - return l; - } - } - return _min; - } - set - { - int i = SelectionStart; - Text = Hexadecimal ? ("0x" + value.ToString("X")) : value.ToString(); - SelectionStart = i; - OnValueChanged(EventArgs.Empty); - } - } - - protected override void WndProc(ref Message m) - { - const int WM_NOTIFY = 0x0282; - if (m.Msg == WM_NOTIFY && m.WParam == new IntPtr(0xB)) - { - if (Hexadecimal && SelectionStart < 2) - { - SelectionStart = 2; - } - } - base.WndProc(ref m); - } - protected override void OnKeyPress(KeyPressEventArgs e) - { - e.Handled = true; // Don't pay attention to this event unless: - - if ((char.IsControl(e.KeyChar) && !(Hexadecimal && SelectionStart <= 2 && SelectionLength == 0 && e.KeyChar == (char)Keys.Back)) || // Backspace isn't used on the "0x" prefix - char.IsDigit(e.KeyChar) || // It is a digit - (e.KeyChar >= 'a' && e.KeyChar <= 'f') || // It is a letter that shows in hex - (e.KeyChar >= 'A' && e.KeyChar <= 'F')) - { - e.Handled = false; - } - base.OnKeyPress(e); - } - protected override void OnTextChanged(EventArgs e) - { - base.OnTextChanged(e); - Value = Value; - } - - private EventHandler _onValueChanged = null; - public event EventHandler ValueChanged - { - add => _onValueChanged += value; - remove => _onValueChanged -= value; - } - protected virtual void OnValueChanged(EventArgs e) - { - _onValueChanged?.Invoke(this, e); - } - } -} diff --git a/VG Music Studio/Util/BetterExceptions.cs b/VG Music Studio/Util/BetterExceptions.cs deleted file mode 100644 index a40a6697..00000000 --- a/VG Music Studio/Util/BetterExceptions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Util -{ - internal class InvalidValueException : Exception - { - public object Value { get; } - - public InvalidValueException(object value, string message) : base(message) - { - Value = value; - } - } - internal class BetterKeyNotFoundException : KeyNotFoundException - { - public object Key { get; } - - public BetterKeyNotFoundException(object key, Exception innerException) : base($"\"{key}\" was not present in the dictionary.", innerException) - { - Key = key; - } - } -} diff --git a/VG Music Studio/Util/HSLColor.cs b/VG Music Studio/Util/HSLColor.cs deleted file mode 100644 index 6dbda597..00000000 --- a/VG Music Studio/Util/HSLColor.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System.Drawing; - -namespace Kermalis.VGMusicStudio.Util -{ - // https://richnewman.wordpress.com/about/code-listings-and-diagrams/hslcolor-class/ - class HSLColor - { - // Private data members below are on scale 0-1 - // They are scaled for use externally based on scale - private double hue = 1.0; - private double saturation = 1.0; - private double luminosity = 1.0; - - private const double scale = 240.0; - - public double Hue - { - get { return hue * scale; } - set { hue = CheckRange(value / scale); } - } - public double Saturation - { - get { return saturation * scale; } - set { saturation = CheckRange(value / scale); } - } - public double Luminosity - { - get { return luminosity * scale; } - set { luminosity = CheckRange(value / scale); } - } - - private double CheckRange(double value) - { - if (value < 0.0) - { - value = 0.0; - } - else if (value > 1.0) - { - value = 1.0; - } - return value; - } - - public override string ToString() - { - return string.Format("H: {0:#0.##} S: {1:#0.##} L: {2:#0.##}", Hue, Saturation, Luminosity); - } - - public string ToRGBString() - { - Color color = this; - return string.Format("R: {0:#0.##} G: {1:#0.##} B: {2:#0.##}", color.R, color.G, color.B); - } - - #region Casts to/from System.Drawing.Color - public static implicit operator Color(HSLColor hslColor) - { - double r = 0, g = 0, b = 0; - if (hslColor.luminosity != 0) - { - if (hslColor.saturation == 0) - { - r = g = b = hslColor.luminosity; - } - else - { - double temp2 = GetTemp2(hslColor); - double temp1 = 2.0 * hslColor.luminosity - temp2; - - r = GetColorComponent(temp1, temp2, hslColor.hue + 1.0 / 3.0); - g = GetColorComponent(temp1, temp2, hslColor.hue); - b = GetColorComponent(temp1, temp2, hslColor.hue - 1.0 / 3.0); - } - } - return Color.FromArgb((int)(255 * r), (int)(255 * g), (int)(255 * b)); - } - - private static double GetColorComponent(double temp1, double temp2, double temp3) - { - temp3 = MoveIntoRange(temp3); - if (temp3 < 1.0 / 6.0) - { - return temp1 + (temp2 - temp1) * 6.0 * temp3; - } - else if (temp3 < 0.5) - { - return temp2; - } - else if (temp3 < 2.0 / 3.0) - { - return temp1 + ((temp2 - temp1) * ((2.0 / 3.0) - temp3) * 6.0); - } - else - { - return temp1; - } - } - private static double MoveIntoRange(double temp3) - { - if (temp3 < 0.0) - { - temp3 += 1.0; - } - else if (temp3 > 1.0) - { - temp3 -= 1.0; - } - return temp3; - } - private static double GetTemp2(HSLColor hslColor) - { - double temp2; - if (hslColor.luminosity < 0.5) //<=?? - { - temp2 = hslColor.luminosity * (1.0 + hslColor.saturation); - } - else - { - temp2 = hslColor.luminosity + hslColor.saturation - (hslColor.luminosity * hslColor.saturation); - } - return temp2; - } - - public static implicit operator HSLColor(Color color) - { - HSLColor hslColor = new HSLColor - { - hue = color.GetHue() / 360.0, // We store hue as 0-1 as opposed to 0-360 - luminosity = color.GetBrightness(), - saturation = color.GetSaturation() - }; - return hslColor; - } - #endregion - - public void SetRGB(int red, int green, int blue) - { - HSLColor hslColor = Color.FromArgb(red, green, blue); - hue = hslColor.hue; - saturation = hslColor.saturation; - luminosity = hslColor.luminosity; - } - - public HSLColor() { } - public HSLColor(Color color) - { - SetRGB(color.R, color.G, color.B); - } - public HSLColor(int red, int green, int blue) - { - SetRGB(red, green, blue); - } - public HSLColor(double hue, double saturation, double luminosity) - { - Hue = hue; - Saturation = saturation; - Luminosity = luminosity; - } - } -} diff --git a/VG Music Studio/Util/SampleUtils.cs b/VG Music Studio/Util/SampleUtils.cs deleted file mode 100644 index 85ecb43e..00000000 --- a/VG Music Studio/Util/SampleUtils.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Kermalis.VGMusicStudio.Util -{ - internal static class SampleUtils - { - public static short[] PCMU8ToPCM16(byte[] data, int index, int length) - { - short[] ret = new short[length]; - for (int i = 0; i < length; i++) - { - byte b = data[index + i]; - ret[i] = (short)((b - 0x80) << 8); - } - return ret; - } - } -} diff --git a/VG Music Studio/Util/TimeBarrier.cs b/VG Music Studio/Util/TimeBarrier.cs deleted file mode 100644 index c253c0b5..00000000 --- a/VG Music Studio/Util/TimeBarrier.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Diagnostics; -using System.Threading; - -namespace Kermalis.VGMusicStudio.Util -{ - // Credit to ipatix - internal class TimeBarrier - { - private readonly Stopwatch _sw; - private readonly double _timerInterval; - private readonly double _waitInterval; - private double _lastTimeStamp; - private bool _started; - - public TimeBarrier(double framesPerSecond) - { - _waitInterval = 1.0 / framesPerSecond; - _started = false; - _sw = new Stopwatch(); - _timerInterval = 1.0 / Stopwatch.Frequency; - } - - public void Wait() - { - if (!_started) - { - return; - } - double totalElapsed = _sw.ElapsedTicks * _timerInterval; - double desiredTimeStamp = _lastTimeStamp + _waitInterval; - double timeToWait = desiredTimeStamp - totalElapsed; - if (timeToWait > 0) - { - Thread.Sleep((int)(timeToWait * 1000)); - } - _lastTimeStamp = desiredTimeStamp; - } - - public void Start() - { - if (_started) - { - return; - } - _started = true; - _lastTimeStamp = 0; - _sw.Restart(); - } - - public void Stop() - { - if (!_started) - { - return; - } - _started = false; - _sw.Stop(); - } - } -} diff --git a/VG Music Studio/Util/Utils.cs b/VG Music Studio/Util/Utils.cs deleted file mode 100644 index a7315069..00000000 --- a/VG Music Studio/Util/Utils.cs +++ /dev/null @@ -1,153 +0,0 @@ -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Properties; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using YamlDotNet.RepresentationModel; - -namespace Kermalis.VGMusicStudio.Util -{ - internal static class Utils - { - public const string ProgramName = "VG Music Studio"; - - private static readonly Random _rng = new Random(); - private static readonly string[] _notes = Strings.Notes.Split(';'); - private static readonly char[] _spaceArray = new char[1] { ' ' }; - - public static bool TryParseValue(string value, long minValue, long maxValue, out long outValue) - { - try - { - outValue = ParseValue(string.Empty, value, minValue, maxValue); - return true; - } - catch - { - outValue = default; - return false; - } - } - /// - public static long ParseValue(string valueName, string value, long minValue, long maxValue) - { - string GetMessage() - { - return string.Format(Strings.ErrorValueParseRanged, valueName, minValue, maxValue); - } - - var provider = new CultureInfo("en-US"); - if (value.StartsWith("0x") && long.TryParse(value.Substring(2), NumberStyles.HexNumber, provider, out long hexp)) - { - if (hexp < minValue || hexp > maxValue) - { - throw new InvalidValueException(hexp, GetMessage()); - } - return hexp; - } - else if (long.TryParse(value, NumberStyles.Integer, provider, out long dec)) - { - if (dec < minValue || dec > maxValue) - { - throw new InvalidValueException(dec, GetMessage()); - } - return dec; - } - else if (long.TryParse(value, NumberStyles.HexNumber, provider, out long hex)) - { - if (hex < minValue || hex > maxValue) - { - throw new InvalidValueException(hex, GetMessage()); - } - return hex; - } - throw new InvalidValueException(value, string.Format(Strings.ErrorValueParse, valueName)); - } - /// - public static bool ParseBoolean(string valueName, string value) - { - if (!bool.TryParse(value, out bool result)) - { - throw new InvalidValueException(value, string.Format(Strings.ErrorBoolParse, valueName)); - } - return result; - } - /// - public static TEnum ParseEnum(string valueName, string value) where TEnum : struct - { - if (!Enum.TryParse(value, out TEnum result)) - { - throw new InvalidValueException(value, string.Format(Strings.ErrorConfigKeyInvalid, valueName)); - } - return result; - } - /// - public static TValue GetValue(this IDictionary dictionary, TKey key) - { - try - { - return dictionary[key]; - } - catch (KeyNotFoundException ex) - { - throw new BetterKeyNotFoundException(key, ex.InnerException); - } - } - /// - /// - public static long GetValidValue(this YamlMappingNode yamlNode, string key, long minRange, long maxRange) - { - return ParseValue(key, yamlNode.Children.GetValue(key).ToString(), minRange, maxRange); - } - /// - /// - public static bool GetValidBoolean(this YamlMappingNode yamlNode, string key) - { - return ParseBoolean(key, yamlNode.Children.GetValue(key).ToString()); - } - /// - /// - public static TEnum GetValidEnum(this YamlMappingNode yamlNode, string key) where TEnum : struct - { - return ParseEnum(key, yamlNode.Children.GetValue(key).ToString()); - } - public static string[] SplitSpace(this string str, StringSplitOptions options) - { - return str.Split(_spaceArray, options); - } - - public static string Print(this IEnumerable source, bool parenthesis = true) - { - string str = parenthesis ? "( " : ""; - str += string.Join(", ", source); - str += parenthesis ? " )" : ""; - return str; - } - /// Fisher-Yates Shuffle - public static void Shuffle(this IList source) - { - for (int a = 0; a < source.Count - 1; a++) - { - int b = _rng.Next(a, source.Count); - T value = source[a]; - source[a] = source[b]; - source[b] = value; - } - } - - public static string GetPianoKeyName(int key) - { - return _notes[key]; - } - public static string GetNoteName(int key) - { - return _notes[key % 12] + ((key / 12) + (GlobalConfig.Instance.MiddleCOctave - 5)); - } - - public static string CombineWithBaseDirectory(string path) - { - return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path); - } - } -} diff --git a/VG Music Studio/VG Music Studio.csproj b/VG Music Studio/VG Music Studio.csproj deleted file mode 100644 index de7ee467..00000000 --- a/VG Music Studio/VG Music Studio.csproj +++ /dev/null @@ -1,252 +0,0 @@ - - - - - Debug - AnyCPU - {97C8ACF8-66A3-4321-91D6-3E94EACA577F} - WinExe - Kermalis.VGMusicStudio - VG Music Studio - v4.8 - 512 - true - - false - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - true - - - AnyCPU - true - full - false - ..\Build\ - DEBUG;TRACE - prompt - 4 - Off - false - - - AnyCPU - pdbonly - true - ..\Build\ - TRACE - prompt - 4 - false - On - - - Properties\Icon.ico - - - Kermalis.VGMusicStudio.Program - - - - Dependencies\DLS2.dll - - - ..\packages\EndianBinaryIO.1.1.2\lib\netstandard2.0\EndianBinaryIO.dll - - - ..\packages\Microsoft.WindowsAPICodePack-Core.1.1.0.2\lib\Microsoft.WindowsAPICodePack.dll - - - ..\packages\Microsoft.WindowsAPICodePack-Shell.1.1.0.0\lib\Microsoft.WindowsAPICodePack.Shell.dll - - - ..\packages\Microsoft.WindowsAPICodePack-Shell.1.1.0.0\lib\Microsoft.WindowsAPICodePack.ShellExtensions.dll - - - ..\packages\NAudio.Core.2.0.0\lib\netstandard2.0\NAudio.Core.dll - - - ..\packages\NAudio.Wasapi.2.0.0\lib\netstandard2.0\NAudio.Wasapi.dll - - - ..\packages\ObjectListView.Official.2.9.1\lib\net20\ObjectListView.dll - - - - - False - Dependencies\Sanford.Multimedia.Midi.dll - - - False - Dependencies\SoundFont2.dll - - - - - - - - - - - - - ..\packages\YamlDotNet.11.2.1\lib\net45\YamlDotNet.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - Strings.resx - - - - - Component - - - - - - Component - - - - - - - - - - - - Always - - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - True - - - Always - - - Always - - - Always - - - Always - - - - - ResXFileCodeGenerator - Strings.Designer.cs - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - - - - - - - - False - Microsoft .NET Framework 4.7.1 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 - false - - - - \ No newline at end of file diff --git a/VG Music Studio/midi2agb.exe b/VG Music Studio/midi2agb.exe deleted file mode 100644 index e5dc76ad..00000000 Binary files a/VG Music Studio/midi2agb.exe and /dev/null differ diff --git a/VG Music Studio/packages.config b/VG Music Studio/packages.config deleted file mode 100644 index cc9c7d08..00000000 --- a/VG Music Studio/packages.config +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file