From 2e5506c625378c5a16a0cad6b5a276459eb70b5b Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Sun, 4 Sep 2022 01:10:08 -0400 Subject: [PATCH 01/34] NO ERRORS - .net 6 first commit Still way more to do, and crashes to fix etc --- VG Music Studio - Core/ADPCMDecoder.cs | 90 + .../AlphaDream.yaml | 0 VG Music Studio - Core/Assembler.cs | 368 ++++ VG Music Studio - Core/Config.cs | 98 + .../Config.yaml | 0 VG Music Studio - Core/Dependencies/DLS2.dll | Bin 0 -> 39424 bytes .../Dependencies/SoundFont2.dll | Bin 0 -> 34304 bytes VG Music Studio - Core/Engine.cs | 20 + .../GBA/AlphaDream/AlphaDreamChannel.cs | 241 +++ .../GBA/AlphaDream/AlphaDreamConfig.cs | 224 +++ .../GBA/AlphaDream/AlphaDreamEngine.cs | 33 + .../GBA/AlphaDream/AlphaDreamException.cs | 17 + .../GBA/AlphaDream/AlphaDreamMixer.cs | 135 ++ .../GBA/AlphaDream/AlphaDreamPlayer.cs | 714 +++++++ .../AlphaDreamSoundFontSaver_DLS.cs | 184 ++ .../AlphaDreamSoundFontSaver_SF2.cs | 99 + .../GBA/AlphaDream/Commands.cs | 113 ++ .../GBA/AlphaDream/Enums.cs | 16 + .../GBA/AlphaDream/Structs.cs | 40 + .../GBA/AlphaDream/Track.cs | 6 +- VG Music Studio - Core/GBA/GBAUtils.cs | 12 + VG Music Studio - Core/GBA/MP2K/Channel.cs | 787 ++++++++ VG Music Studio - Core/GBA/MP2K/Commands.cs | 193 ++ VG Music Studio - Core/GBA/MP2K/Enums.cs | 76 + VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs | 248 +++ VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs | 33 + .../GBA/MP2K/MP2KExceptions.cs | 41 + .../GBA/MP2K/MP2KLoadedSong.cs | 535 ++++++ .../GBA/MP2K/MP2KLoadedSong_MIDI.cs | 253 +++ VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs | 273 +++ VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs | 786 ++++++++ VG Music Studio - Core/GBA/MP2K/Structs.cs | 80 + .../GBA/MP2K/Track.cs | 6 +- VG Music Studio - Core/GBA/MP2K/Utils.cs | 175 ++ .../MP2K.yaml | 0 .../MPlayDef.s | 0 VG Music Studio - Core/Mixer.cs | 93 + VG Music Studio - Core/NDS/DSE/Channel.cs | 368 ++++ VG Music Studio - Core/NDS/DSE/Commands.cs | 130 ++ VG Music Studio - Core/NDS/DSE/DSEConfig.cs | 45 + VG Music Studio - Core/NDS/DSE/DSEEngine.cs | 26 + .../NDS/DSE/DSEExceptions.cs | 51 + VG Music Studio - Core/NDS/DSE/DSEMixer.cs | 221 +++ VG Music Studio - Core/NDS/DSE/DSEPlayer.cs | 1046 ++++++++++ .../NDS/DSE/Enums.cs | 0 VG Music Studio - Core/NDS/DSE/SMD.cs | 61 + VG Music Studio - Core/NDS/DSE/SWD.cs | 477 +++++ VG Music Studio - Core/NDS/DSE/Track.cs | 71 + .../NDS/DSE/Utils.cs | 0 VG Music Studio - Core/NDS/SDAT/Channel.cs | 391 ++++ VG Music Studio - Core/NDS/SDAT/Commands.cs | 439 +++++ .../NDS/SDAT/Enums.cs | 0 VG Music Studio - Core/NDS/SDAT/FileHeader.cs | 25 + VG Music Studio - Core/NDS/SDAT/SBNK.cs | 195 ++ VG Music Studio - Core/NDS/SDAT/SDAT.cs | 251 +++ VG Music Studio - Core/NDS/SDAT/SDATConfig.cs | 40 + VG Music Studio - Core/NDS/SDAT/SDATEngine.cs | 26 + .../NDS/SDAT/SDATExceptions.cs | 27 + VG Music Studio - Core/NDS/SDAT/SDATMixer.cs | 252 +++ VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs | 1694 +++++++++++++++++ VG Music Studio - Core/NDS/SDAT/SDATUtils.cs | 342 ++++ VG Music Studio - Core/NDS/SDAT/SSEQ.cs | 31 + VG Music Studio - Core/NDS/SDAT/SWAR.cs | 65 + VG Music Studio - Core/NDS/SDAT/Track.cs | 196 ++ VG Music Studio - Core/NDS/Utils.cs | 7 + VG Music Studio - Core/Player.cs | 38 + .../Properties/Strings.Designer.cs | 162 +- .../Properties/Strings.es.resx | 0 .../Properties/Strings.it.resx | 0 .../Properties/Strings.resx | 0 VG Music Studio - Core/SongEvent.cs | 24 + VG Music Studio - Core/SongState.cs | 71 + .../Util/BetterExceptions.cs | 25 + VG Music Studio - Core/Util/ConfigUtils.cs | 126 ++ VG Music Studio - Core/Util/GlobalConfig.cs | 110 ++ VG Music Studio - Core/Util/HSLColor.cs | 152 ++ VG Music Studio - Core/Util/SampleUtils.cs | 19 + VG Music Studio - Core/Util/TimeBarrier.cs | 60 + .../VG Music Studio - Core.csproj | 47 + VG Music Studio - MIDI/Chunks/MIDIChunk.cs | 22 + .../Chunks/MIDIHeaderChunk.cs | 79 + .../Chunks/MIDITrackChunk.cs | 246 +++ .../Chunks/MIDIUnsupportedChunk.cs | 30 + .../Events/ChannelPressureMessage.cs | 48 + .../Events/ControllerMessage.cs | 141 ++ .../Events/EscapeMessage.cs | 39 + VG Music Studio - MIDI/Events/MIDIEvent.cs | 18 + VG Music Studio - MIDI/Events/MIDIMessage.cs | 10 + VG Music Studio - MIDI/Events/MetaMessage.cs | 122 ++ .../Events/NoteOffMessage.cs | 61 + .../Events/NoteOnMessage.cs | 61 + .../Events/PitchBendMessage.cs | 61 + .../Events/PolyphonicPressureMessage.cs | 53 + .../Events/ProgramChangeMessage.cs | 181 ++ .../Events/SysExContinuationMessage.cs | 47 + VG Music Studio - MIDI/Events/SysExMessage.cs | 47 + VG Music Studio - MIDI/MIDIFile.cs | 173 ++ VG Music Studio - MIDI/MIDINote.cs | 134 ++ VG Music Studio - MIDI/TimeDivisionValue.cs | 63 + .../VG Music Studio - MIDI.csproj | 15 + VG Music Studio - WinForms/MainForm.cs | 862 +++++++++ VG Music Studio - WinForms/PianoControl.cs | 205 ++ VG Music Studio - WinForms/Program.cs | 30 + .../Properties/Icon.ico | Bin .../Properties/Icon16.png | Bin .../Properties/Icon24.png | Bin .../Properties/Icon32.png | Bin .../Properties/Icon48.png | Bin .../Properties/Icon528.png | Bin .../Properties/Next.ico | Bin .../Properties/Next.png | Bin .../Properties/Pause.ico | Bin .../Properties/Pause.png | Bin .../Properties/Play.ico | Bin .../Properties/Play.png | Bin .../Properties/Playlist.png | Bin .../Properties/Previous.ico | Bin .../Properties/Previous.png | Bin .../Properties/Resources.Designer.cs | 4 +- .../Properties/Resources.resx | 0 .../Properties/Song.png | Bin VG Music Studio - WinForms/SongInfoControl.cs | 367 ++++ VG Music Studio - WinForms/Theme.cs | 163 ++ VG Music Studio - WinForms/TrackViewer.cs | 112 ++ .../Util/ColorSlider.cs | 484 +++++ .../Util/FlexibleMessageBox.cs | 696 +++++++ .../Util/ImageComboBox.cs | 61 + VG Music Studio - WinForms/Util/VGMSDebug.cs | 122 ++ .../Util/WinFormsUtils.cs | 39 + .../VG Music Studio - WinForms.csproj | 27 + VG Music Studio - WinForms/ValueTextBox.cs | 104 + VG Music Studio.sln | 26 +- VG Music Studio/Core/ADPCMDecoder.cs | 90 - VG Music Studio/Core/Assembler.cs | 351 ---- VG Music Studio/Core/Config.cs | 90 - VG Music Studio/Core/Engine.cs | 92 - .../Core/GBA/AlphaDream/Channel.cs | 237 --- .../Core/GBA/AlphaDream/Commands.cs | 113 -- VG Music Studio/Core/GBA/AlphaDream/Config.cs | 220 --- VG Music Studio/Core/GBA/AlphaDream/Enums.cs | 16 - VG Music Studio/Core/GBA/AlphaDream/Mixer.cs | 137 -- VG Music Studio/Core/GBA/AlphaDream/Player.cs | 696 ------- .../Core/GBA/AlphaDream/SoundFontSaver_DLS.cs | 181 -- .../Core/GBA/AlphaDream/SoundFontSaver_SF2.cs | 97 - .../Core/GBA/AlphaDream/Structs.cs | 33 - VG Music Studio/Core/GBA/MP2K/Channel.cs | 777 -------- VG Music Studio/Core/GBA/MP2K/Commands.cs | 193 -- VG Music Studio/Core/GBA/MP2K/Config.cs | 242 --- VG Music Studio/Core/GBA/MP2K/Enums.cs | 72 - VG Music Studio/Core/GBA/MP2K/Mixer.cs | 275 --- VG Music Studio/Core/GBA/MP2K/Player.cs | 1510 --------------- VG Music Studio/Core/GBA/MP2K/Structs.cs | 73 - VG Music Studio/Core/GBA/MP2K/Utils.cs | 174 -- VG Music Studio/Core/GBA/Utils.cs | 13 - VG Music Studio/Core/GlobalConfig.cs | 109 -- VG Music Studio/Core/Mixer.cs | 87 - VG Music Studio/Core/NDS/DSE/Channel.cs | 368 ---- VG Music Studio/Core/NDS/DSE/Commands.cs | 130 -- VG Music Studio/Core/NDS/DSE/Config.cs | 45 - VG Music Studio/Core/NDS/DSE/Mixer.cs | 220 --- VG Music Studio/Core/NDS/DSE/Player.cs | 1040 ---------- VG Music Studio/Core/NDS/DSE/SMD.cs | 61 - VG Music Studio/Core/NDS/DSE/SWD.cs | 471 ----- VG Music Studio/Core/NDS/DSE/Track.cs | 72 - VG Music Studio/Core/NDS/SDAT/Channel.cs | 387 ---- VG Music Studio/Core/NDS/SDAT/Commands.cs | 439 ----- VG Music Studio/Core/NDS/SDAT/Config.cs | 40 - VG Music Studio/Core/NDS/SDAT/FileHeader.cs | 31 - VG Music Studio/Core/NDS/SDAT/Mixer.cs | 252 --- VG Music Studio/Core/NDS/SDAT/Player.cs | 1680 ---------------- VG Music Studio/Core/NDS/SDAT/SBNK.cs | 184 -- VG Music Studio/Core/NDS/SDAT/SDAT.cs | 234 --- VG Music Studio/Core/NDS/SDAT/SSEQ.cs | 28 - VG Music Studio/Core/NDS/SDAT/SWAR.cs | 65 - VG Music Studio/Core/NDS/SDAT/Track.cs | 193 -- VG Music Studio/Core/NDS/SDAT/Utils.cs | 345 ---- VG Music Studio/Core/NDS/Utils.cs | 7 - VG Music Studio/Core/Player.cs | 36 - VG Music Studio/Core/SongEvent.cs | 24 - VG Music Studio/Core/VGMSDebug.cs | 112 -- VG Music Studio/Dependencies/DLS2.dll | Bin 36352 -> 0 bytes .../Dependencies/Sanford.Multimedia.Midi.dll | Bin 180224 -> 0 bytes VG Music Studio/Dependencies/SoundFont2.dll | Bin 31744 -> 0 bytes VG Music Studio/Program.cs | 30 - VG Music Studio/Properties/AssemblyInfo.cs | 39 - .../Properties/Settings.Designer.cs | 26 - VG Music Studio/Properties/Settings.settings | 7 - VG Music Studio/UI/ColorSlider.cs | 485 ----- VG Music Studio/UI/FlexibleMessageBox.cs | 697 ------- VG Music Studio/UI/ImageComboBox.cs | 62 - VG Music Studio/UI/MainForm.cs | 836 -------- VG Music Studio/UI/PianoControl.cs | 183 -- VG Music Studio/UI/SongInfoControl.cs | 354 ---- VG Music Studio/UI/Theme.cs | 165 -- VG Music Studio/UI/TrackViewer.cs | 113 -- VG Music Studio/UI/ValueTextBox.cs | 104 - VG Music Studio/Util/BetterExceptions.cs | 24 - VG Music Studio/Util/HSLColor.cs | 161 -- VG Music Studio/Util/SampleUtils.cs | 16 - VG Music Studio/Util/TimeBarrier.cs | 60 - VG Music Studio/Util/Utils.cs | 153 -- VG Music Studio/VG Music Studio.csproj | 252 --- VG Music Studio/midi2agb.exe | Bin 3404655 -> 0 bytes VG Music Studio/packages.config | 10 - 204 files changed, 18157 insertions(+), 16215 deletions(-) create mode 100644 VG Music Studio - Core/ADPCMDecoder.cs rename {VG Music Studio => VG Music Studio - Core}/AlphaDream.yaml (100%) create mode 100644 VG Music Studio - Core/Assembler.cs create mode 100644 VG Music Studio - Core/Config.cs rename {VG Music Studio => VG Music Studio - Core}/Config.yaml (100%) create mode 100644 VG Music Studio - Core/Dependencies/DLS2.dll create mode 100644 VG Music Studio - Core/Dependencies/SoundFont2.dll create mode 100644 VG Music Studio - Core/Engine.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamChannel.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamConfig.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamException.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_DLS.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_SF2.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/Commands.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/Enums.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/Structs.cs rename {VG Music Studio/Core => VG Music Studio - Core}/GBA/AlphaDream/Track.cs (91%) create mode 100644 VG Music Studio - Core/GBA/GBAUtils.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/Channel.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/Commands.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/Enums.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KExceptions.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/Structs.cs rename {VG Music Studio/Core => VG Music Studio - Core}/GBA/MP2K/Track.cs (97%) create mode 100644 VG Music Studio - Core/GBA/MP2K/Utils.cs rename {VG Music Studio => VG Music Studio - Core}/MP2K.yaml (100%) rename {VG Music Studio => VG Music Studio - Core}/MPlayDef.s (100%) create mode 100644 VG Music Studio - Core/Mixer.cs create mode 100644 VG Music Studio - Core/NDS/DSE/Channel.cs create mode 100644 VG Music Studio - Core/NDS/DSE/Commands.cs create mode 100644 VG Music Studio - Core/NDS/DSE/DSEConfig.cs create mode 100644 VG Music Studio - Core/NDS/DSE/DSEEngine.cs create mode 100644 VG Music Studio - Core/NDS/DSE/DSEExceptions.cs create mode 100644 VG Music Studio - Core/NDS/DSE/DSEMixer.cs create mode 100644 VG Music Studio - Core/NDS/DSE/DSEPlayer.cs rename {VG Music Studio/Core => VG Music Studio - Core}/NDS/DSE/Enums.cs (100%) create mode 100644 VG Music Studio - Core/NDS/DSE/SMD.cs create mode 100644 VG Music Studio - Core/NDS/DSE/SWD.cs create mode 100644 VG Music Studio - Core/NDS/DSE/Track.cs rename {VG Music Studio/Core => VG Music Studio - Core}/NDS/DSE/Utils.cs (100%) create mode 100644 VG Music Studio - Core/NDS/SDAT/Channel.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/Commands.cs rename {VG Music Studio/Core => VG Music Studio - Core}/NDS/SDAT/Enums.cs (100%) create mode 100644 VG Music Studio - Core/NDS/SDAT/FileHeader.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/SBNK.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/SDAT.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/SDATConfig.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/SDATEngine.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/SDATExceptions.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/SDATMixer.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/SDATUtils.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/SSEQ.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/SWAR.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/Track.cs create mode 100644 VG Music Studio - Core/NDS/Utils.cs create mode 100644 VG Music Studio - Core/Player.cs rename {VG Music Studio => VG Music Studio - Core}/Properties/Strings.Designer.cs (84%) rename {VG Music Studio => VG Music Studio - Core}/Properties/Strings.es.resx (100%) rename {VG Music Studio => VG Music Studio - Core}/Properties/Strings.it.resx (100%) rename {VG Music Studio => VG Music Studio - Core}/Properties/Strings.resx (100%) create mode 100644 VG Music Studio - Core/SongEvent.cs create mode 100644 VG Music Studio - Core/SongState.cs create mode 100644 VG Music Studio - Core/Util/BetterExceptions.cs create mode 100644 VG Music Studio - Core/Util/ConfigUtils.cs create mode 100644 VG Music Studio - Core/Util/GlobalConfig.cs create mode 100644 VG Music Studio - Core/Util/HSLColor.cs create mode 100644 VG Music Studio - Core/Util/SampleUtils.cs create mode 100644 VG Music Studio - Core/Util/TimeBarrier.cs create mode 100644 VG Music Studio - Core/VG Music Studio - Core.csproj create mode 100644 VG Music Studio - MIDI/Chunks/MIDIChunk.cs create mode 100644 VG Music Studio - MIDI/Chunks/MIDIHeaderChunk.cs create mode 100644 VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs create mode 100644 VG Music Studio - MIDI/Chunks/MIDIUnsupportedChunk.cs create mode 100644 VG Music Studio - MIDI/Events/ChannelPressureMessage.cs create mode 100644 VG Music Studio - MIDI/Events/ControllerMessage.cs create mode 100644 VG Music Studio - MIDI/Events/EscapeMessage.cs create mode 100644 VG Music Studio - MIDI/Events/MIDIEvent.cs create mode 100644 VG Music Studio - MIDI/Events/MIDIMessage.cs create mode 100644 VG Music Studio - MIDI/Events/MetaMessage.cs create mode 100644 VG Music Studio - MIDI/Events/NoteOffMessage.cs create mode 100644 VG Music Studio - MIDI/Events/NoteOnMessage.cs create mode 100644 VG Music Studio - MIDI/Events/PitchBendMessage.cs create mode 100644 VG Music Studio - MIDI/Events/PolyphonicPressureMessage.cs create mode 100644 VG Music Studio - MIDI/Events/ProgramChangeMessage.cs create mode 100644 VG Music Studio - MIDI/Events/SysExContinuationMessage.cs create mode 100644 VG Music Studio - MIDI/Events/SysExMessage.cs create mode 100644 VG Music Studio - MIDI/MIDIFile.cs create mode 100644 VG Music Studio - MIDI/MIDINote.cs create mode 100644 VG Music Studio - MIDI/TimeDivisionValue.cs create mode 100644 VG Music Studio - MIDI/VG Music Studio - MIDI.csproj create mode 100644 VG Music Studio - WinForms/MainForm.cs create mode 100644 VG Music Studio - WinForms/PianoControl.cs create mode 100644 VG Music Studio - WinForms/Program.cs rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Icon.ico (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Icon16.png (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Icon24.png (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Icon32.png (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Icon48.png (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Icon528.png (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Next.ico (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Next.png (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Pause.ico (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Pause.png (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Play.ico (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Play.png (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Playlist.png (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Previous.ico (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Previous.png (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Resources.Designer.cs (97%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Resources.resx (100%) rename {VG Music Studio => VG Music Studio - WinForms}/Properties/Song.png (100%) create mode 100644 VG Music Studio - WinForms/SongInfoControl.cs create mode 100644 VG Music Studio - WinForms/Theme.cs create mode 100644 VG Music Studio - WinForms/TrackViewer.cs create mode 100644 VG Music Studio - WinForms/Util/ColorSlider.cs create mode 100644 VG Music Studio - WinForms/Util/FlexibleMessageBox.cs create mode 100644 VG Music Studio - WinForms/Util/ImageComboBox.cs create mode 100644 VG Music Studio - WinForms/Util/VGMSDebug.cs create mode 100644 VG Music Studio - WinForms/Util/WinFormsUtils.cs create mode 100644 VG Music Studio - WinForms/VG Music Studio - WinForms.csproj create mode 100644 VG Music Studio - WinForms/ValueTextBox.cs delete mode 100644 VG Music Studio/Core/ADPCMDecoder.cs delete mode 100644 VG Music Studio/Core/Assembler.cs delete mode 100644 VG Music Studio/Core/Config.cs delete mode 100644 VG Music Studio/Core/Engine.cs delete mode 100644 VG Music Studio/Core/GBA/AlphaDream/Channel.cs delete mode 100644 VG Music Studio/Core/GBA/AlphaDream/Commands.cs delete mode 100644 VG Music Studio/Core/GBA/AlphaDream/Config.cs delete mode 100644 VG Music Studio/Core/GBA/AlphaDream/Enums.cs delete mode 100644 VG Music Studio/Core/GBA/AlphaDream/Mixer.cs delete mode 100644 VG Music Studio/Core/GBA/AlphaDream/Player.cs delete mode 100644 VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs delete mode 100644 VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs delete mode 100644 VG Music Studio/Core/GBA/AlphaDream/Structs.cs delete mode 100644 VG Music Studio/Core/GBA/MP2K/Channel.cs delete mode 100644 VG Music Studio/Core/GBA/MP2K/Commands.cs delete mode 100644 VG Music Studio/Core/GBA/MP2K/Config.cs delete mode 100644 VG Music Studio/Core/GBA/MP2K/Enums.cs delete mode 100644 VG Music Studio/Core/GBA/MP2K/Mixer.cs delete mode 100644 VG Music Studio/Core/GBA/MP2K/Player.cs delete mode 100644 VG Music Studio/Core/GBA/MP2K/Structs.cs delete mode 100644 VG Music Studio/Core/GBA/MP2K/Utils.cs delete mode 100644 VG Music Studio/Core/GBA/Utils.cs delete mode 100644 VG Music Studio/Core/GlobalConfig.cs delete mode 100644 VG Music Studio/Core/Mixer.cs delete mode 100644 VG Music Studio/Core/NDS/DSE/Channel.cs delete mode 100644 VG Music Studio/Core/NDS/DSE/Commands.cs delete mode 100644 VG Music Studio/Core/NDS/DSE/Config.cs delete mode 100644 VG Music Studio/Core/NDS/DSE/Mixer.cs delete mode 100644 VG Music Studio/Core/NDS/DSE/Player.cs delete mode 100644 VG Music Studio/Core/NDS/DSE/SMD.cs delete mode 100644 VG Music Studio/Core/NDS/DSE/SWD.cs delete mode 100644 VG Music Studio/Core/NDS/DSE/Track.cs delete mode 100644 VG Music Studio/Core/NDS/SDAT/Channel.cs delete mode 100644 VG Music Studio/Core/NDS/SDAT/Commands.cs delete mode 100644 VG Music Studio/Core/NDS/SDAT/Config.cs delete mode 100644 VG Music Studio/Core/NDS/SDAT/FileHeader.cs delete mode 100644 VG Music Studio/Core/NDS/SDAT/Mixer.cs delete mode 100644 VG Music Studio/Core/NDS/SDAT/Player.cs delete mode 100644 VG Music Studio/Core/NDS/SDAT/SBNK.cs delete mode 100644 VG Music Studio/Core/NDS/SDAT/SDAT.cs delete mode 100644 VG Music Studio/Core/NDS/SDAT/SSEQ.cs delete mode 100644 VG Music Studio/Core/NDS/SDAT/SWAR.cs delete mode 100644 VG Music Studio/Core/NDS/SDAT/Track.cs delete mode 100644 VG Music Studio/Core/NDS/SDAT/Utils.cs delete mode 100644 VG Music Studio/Core/NDS/Utils.cs delete mode 100644 VG Music Studio/Core/Player.cs delete mode 100644 VG Music Studio/Core/SongEvent.cs delete mode 100644 VG Music Studio/Core/VGMSDebug.cs delete mode 100644 VG Music Studio/Dependencies/DLS2.dll delete mode 100644 VG Music Studio/Dependencies/Sanford.Multimedia.Midi.dll delete mode 100644 VG Music Studio/Dependencies/SoundFont2.dll delete mode 100644 VG Music Studio/Program.cs delete mode 100644 VG Music Studio/Properties/AssemblyInfo.cs delete mode 100644 VG Music Studio/Properties/Settings.Designer.cs delete mode 100644 VG Music Studio/Properties/Settings.settings delete mode 100644 VG Music Studio/UI/ColorSlider.cs delete mode 100644 VG Music Studio/UI/FlexibleMessageBox.cs delete mode 100644 VG Music Studio/UI/ImageComboBox.cs delete mode 100644 VG Music Studio/UI/MainForm.cs delete mode 100644 VG Music Studio/UI/PianoControl.cs delete mode 100644 VG Music Studio/UI/SongInfoControl.cs delete mode 100644 VG Music Studio/UI/Theme.cs delete mode 100644 VG Music Studio/UI/TrackViewer.cs delete mode 100644 VG Music Studio/UI/ValueTextBox.cs delete mode 100644 VG Music Studio/Util/BetterExceptions.cs delete mode 100644 VG Music Studio/Util/HSLColor.cs delete mode 100644 VG Music Studio/Util/SampleUtils.cs delete mode 100644 VG Music Studio/Util/TimeBarrier.cs delete mode 100644 VG Music Studio/Util/Utils.cs delete mode 100644 VG Music Studio/VG Music Studio.csproj delete mode 100644 VG Music Studio/midi2agb.exe delete mode 100644 VG Music Studio/packages.config diff --git a/VG Music Studio - Core/ADPCMDecoder.cs b/VG Music Studio - Core/ADPCMDecoder.cs new file mode 100644 index 0000000..f2ee92c --- /dev/null +++ b/VG Music Studio - Core/ADPCMDecoder.cs @@ -0,0 +1,90 @@ +namespace Kermalis.VGMusicStudio.Core; + +internal sealed 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; + } + + // TODO: Span? + 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/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 0000000..5274414 --- /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 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}\""; + + 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); + + Debug.WriteLine(Read(fileName)); + 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); // b will contain {0x04, 0x28, 0x00, 0x00} [0x2804] (SEQ_STUFF+4 + baseOffset) + // Copy the new pointer to binary offset 0x1DF4 + // TODO: UPDATE THESE OLD COMMENTS LOL + } + 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 = 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 == 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++) + { + _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)); + } + + 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 0000000..f3c34b3 --- /dev/null +++ b/VG Music Studio - Core/Config.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; + +namespace Kermalis.VGMusicStudio.Core; + +public abstract class Config : IDisposable +{ + public sealed 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 sealed 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 = Thread.CurrentThread.CurrentUICulture; + if (cul.TwoLetterISOLanguageName == "it") // Italian + { + // PlaylistName - (1 Canzone) + // PlaylistName - (2 Canzoni) + return $"{Name} - ({songCount} {(songCount == 1 ? "Canzone" : "Canzoni")})"; + } + if (cul.TwoLetterISOLanguageName == "es") // Spanish + { + // PlaylistName - (1 Canción) + // PlaylistName - (2 Canciones) + return $"{Name} - ({songCount} {(songCount == 1 ? "Canción" : "Canciones")})"; + } + // Fallback to en-US + // PlaylistName - (1 Song) + // PlaylistName - (2 Songs) + return $"{Name} - ({songCount} {(songCount == 1 ? "Song" : "Songs")})"; + } + } + + public readonly List Playlists; + + protected Config() + { + 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/Config.yaml b/VG Music Studio - Core/Config.yaml similarity index 100% rename from VG Music Studio/Config.yaml rename to VG Music Studio - Core/Config.yaml diff --git a/VG Music Studio - Core/Dependencies/DLS2.dll b/VG Music Studio - Core/Dependencies/DLS2.dll new file mode 100644 index 0000000000000000000000000000000000000000..12414d5c6fff2c08810fbe1b47e4ad64e863543e GIT binary patch literal 39424 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 8); + _rightVol = (byte)((vol * (pan + 0x80)) >> 8); + } + public abstract void SetPitch(int pitch); + + public abstract void Process(float[] buffer); +} +internal class PCMChannel : AlphaDreamChannel +{ + private SampleHeader _sampleHeader; + private int _sampleOffset; + private bool _bFixed; + + public PCMChannel(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 = MemoryMarshal.Read(_mixer.Config.ROM.AsSpan(sampleOffset)); + _sampleOffset = sampleOffset + 0x10; + _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); + } +} +internal class SquareChannel : AlphaDreamChannel +{ + private float[] _pat; + + public SquareChannel(AlphaDreamMixer mixer) : base(mixer) + { + // + } + public void Init(byte key, ADSR env, byte vol, sbyte pan, int pitch) + { + _pat = MP2K.Utils.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); + } +} 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 0000000..4858291 --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamConfig.cs @@ -0,0 +1,224 @@ +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.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) + { + long songIndex = ConfigUtils.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, 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 == 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))); + } + } + 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(long index) + { + Song? s = GetFirstSong(index); + return s is not null ? s.Name : 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 0000000..ae021f7 --- /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.CartridgeCapacity) + { + throw new InvalidDataException($"The ROM is too large. Maximum size is 0x{GBAUtils.CartridgeCapacity: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/AlphaDreamException.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamException.cs new file mode 100644 index 0000000..3fd92e5 --- /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/AlphaDreamMixer.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs new file mode 100644 index 0000000..05c4afd --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs @@ -0,0 +1,135 @@ +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; + + 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; + } + + private WaveFileWriter? _waveWriter; + public void CreateWaveWriter(string fileName) + { + _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); + } + public void CloseWaveWriter() + { + _waveWriter!.Dispose(); + _waveWriter = null; + } + internal 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 : 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++) + { + Track track = tracks[i]; + if (!track.Enabled || 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 0000000..6d8f062 --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs @@ -0,0 +1,714 @@ +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +public sealed class AlphaDreamPlayer : IPlayer, ILoadedSong +{ + internal const int NUM_TRACKS = 12; // 8 PCM, 4 PSG + + private readonly Track[] _tracks = new Track[NUM_TRACKS]; + private readonly AlphaDreamMixer _mixer; + private readonly AlphaDreamConfig _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 ILoadedSong LoadedSong => this; + public bool ShouldFadeOut { get; set; } + public long NumLoops { get; set; } + private int _longestTrack; + + public PlayerState State { get; private set; } + public event Action? SongEnded; + + internal AlphaDreamPlayer(AlphaDreamConfig config, AlphaDreamMixer mixer) + { + _config = config; + _mixer = mixer; + + for (byte i = 0; i < NUM_TRACKS; i++) + { + _tracks[i] = new Track(i, mixer); + } + + _time = new TimeBarrier(GBAUtils.AGB_FPS); + } + private void CreateThread() + { + _thread = new Thread(Tick) { Name = "AlphaDream Player Tick" }; + _thread.Start(); + } + private void WaitThread() + { + if (_thread is not null && (_thread.ThreadState is ThreadState.Running or 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 < NUM_TRACKS; i++) + { + _tracks[i].Init(); + } + } + private void SetTicks() + { + MaxTicks = 0; + bool u = false; + for (int trackIndex = 0; trackIndex < NUM_TRACKS; 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) + { + _config.Reader.Stream.Position = _config.SongTableOffsets[0] + (index * 4); + int songOffset = _config.Reader.ReadInt32(); + if (songOffset == 0) + { + Events = null; + return; + } + + Events = new List[NUM_TRACKS]; + songOffset -= GBAUtils.CartridgeOffset; + _config.Reader.Stream.Position = songOffset; + ushort trackBits = _config.Reader.ReadUInt16(); + for (byte i = 0, usedTracks = 0; i < NUM_TRACKS; i++) + { + Track track = _tracks[i]; + if ((trackBits & (1 << i)) == 0) + { + track.Enabled = false; + track.StartOffset = 0; + continue; + } + + track.Enabled = true; + Events[i] = new List(); + bool EventExists(long offset) + { + return Events[i].Any(e => e.Offset == offset); + } + + _config.Reader.Stream.Position = songOffset + 2 + (2 * usedTracks++); + AddEvents(track.StartOffset = songOffset + _config.Reader.ReadInt16()); + void AddEvents(int startOffset) + { + _config.Reader.Stream.Position = startOffset; + bool cont = true; + while (cont) + { + long offset = _config.Reader.Stream.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 { Note = (byte)(keyArg - 0x80), Volume = volume, Duration = duration }); + } + break; + } + case AudioEngineVersion.MLSS: + { + byte duration = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new FreeNoteMLSSCommand { Note = (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.Stream.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 >= 0xE0) + { + throw new AlphaDreamInvalidCMDException(i, (int)offset, cmd); + } + + byte key = _config.Reader.ReadByte(); + switch (_config.AudioEngineVersion) + { + case AudioEngineVersion.Hamtaro: + { + byte volume = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new NoteHamtaroCommand { Note = key, Volume = volume, Duration = cmd }); + } + break; + } + case AudioEngineVersion.MLSS: + { + if (!EventExists(offset)) + { + AddEvent(new NoteMLSSCommand { Note = key, Duration = cmd }); + } + break; + } + } + 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 < NUM_TRACKS; 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 < NUM_TRACKS; i++) + { + _tracks[i].NoteDuration = 0; + } + Pause(); + } + } + public void Play() + { + if (State is PlayerState.ShutDown or PlayerState.Recording) + { + return; + } + + if (Events is null) + { + SongEnded?.Invoke(); + return; + } + + Stop(); + InitEmulation(); + State = PlayerState.Playing; + CreateThread(); + } + public void Pause() + { + 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(); + } + } + public void Record(string fileName) + { + _mixer.CreateWaveWriter(fileName); + InitEmulation(); + State = PlayerState.Recording; + CreateThread(); + WaitThread(); + _mixer.CloseWaveWriter(); + } + public void Dispose() + { + if (State != PlayerState.ShutDown) + { + State = PlayerState.ShutDown; + WaitThread(); + } + } + public void UpdateSongState(SongState info) + { + info.Tempo = _tempo; + for (int i = 0; i < NUM_TRACKS; i++) + { + Track track = _tracks[i]; + if (!track.Enabled) + { + continue; + } + + SongState.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 bool TryGetVoiceEntry(byte voice, byte key, out VoiceEntry e) + { + int vto = _config.VoiceTableOffset; + byte[] rom = _config.ROM; + short voiceOffset = BinaryPrimitives.ReadInt16LittleEndian(rom.AsSpan(vto + (voice * 2))); + short nextVoiceOffset = BinaryPrimitives.ReadInt16LittleEndian(rom.AsSpan(vto + ((voice + 1) * 2))); + if (voiceOffset == nextVoiceOffset) + { + e = default; + return false; + } + + int pos = vto + voiceOffset; // Prevent object creation in the last iteration + ref VoiceEntry refE = ref MemoryMarshal.AsRef(rom.AsSpan(pos)); + while (refE.MinKey > key || refE.MaxKey < key) + { + pos += 8; + if (pos == nextVoiceOffset) + { + e = default; + return false; + } + refE = ref MemoryMarshal.AsRef(rom.AsSpan(pos)); + } + e = refE; + return true; + } + private void PlayNote(Track track, byte key, byte duration) + { + if (!TryGetVoiceEntry(track.Voice, key, out VoiceEntry entry)) + { + return; + } + + track.NoteDuration = duration; + if (track.Index >= 8) + { + // TODO: "Sample" byte in VoiceEntry + var sqr = (SquareChannel)track.Channel; + sqr.Init(key, new ADSR { A = 0xFF, D = 0x00, S = 0xFF, R = 0x00 }, track.Volume, track.Panpot, track.GetPitch()); + } + else + { + int sto = _config.SampleTableOffset; + byte[] rom = _config.ROM; + int sampleOffset = BinaryPrimitives.ReadInt32LittleEndian(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 = (PCMChannel)track.Channel; + pcm.Init(key, new ADSR { A = 0xFF, D = 0x00, S = 0xFF, R = 0x00 }, sto + sampleOffset, entry.IsFixedFrequency == 0x80); + pcm.SetVolume(track.Volume, track.Panpot); + pcm.SetPitch(track.GetPitch()); + } + } + private void ExecuteNext(Track track, ref bool update) + { + byte[] rom = _config.ROM; + byte cmd = rom[track.DataOffset++]; + switch (cmd) + { + case 0x00: // Free Note + { + byte note = (byte)(rom[track.DataOffset++] - 0x80); + if (_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 (_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 + BinaryPrimitives.ReadInt16LittleEndian(rom.AsSpan(track.DataOffset, 2)); + break; + } + case 0xF9: // Track Tempo + { + _tempo = rom[track.DataOffset++]; + break; + } + case 0xFF: // Finish + { + track.Stopped = true; + break; + } + default: throw new AlphaDreamInvalidCMDException(track.Index, track.DataOffset - 1, cmd); + } + + 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) + { + break; + } + + while (_tempoStack >= 75) + { + _tempoStack -= 75; + bool allDone = true; + for (int trackIndex = 0; trackIndex < NUM_TRACKS; 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) + { + // TODO: lock state + _mixer.Process(_tracks, playing, recording); + _time.Stop(); + State = PlayerState.Stopped; + SongEnded?.Invoke(); + return; + } + } + _tempoStack += _tempo; + _mixer.Process(_tracks, playing, recording); + if (playing) + { + _time.Wait(); + } + } + _time.Stop(); + } +} 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 0000000..f9f351f --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_DLS.cs @@ -0,0 +1,184 @@ +using Kermalis.DLS2; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; + +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; + ref SampleHeader sh = ref MemoryMarshal.AsRef(config.ROM.AsSpan(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 == 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, 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(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 VoiceEntry entry = ref MemoryMarshal.AsRef(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 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 == 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 0000000..6ed9a2c --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_SF2.cs @@ -0,0 +1,99 @@ +using Kermalis.SoundFont2; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +public static class AlphaDreamSoundFontSaver_SF2 +{ + public static void Save(AlphaDreamConfig 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(AlphaDreamConfig config, InfoListChunk chunk) + { + chunk.Bank = config.Name; + //chunk.Copyright = config.Creator; + chunk.Tools = ConfigUtils.PROGRAM_NAME + " by Kermalis"; + } + + private static Dictionary AddSamples(AlphaDreamConfig config, SF2 sf2) + { + 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; + } + + ofs += config.SampleTableOffset; + ref SampleHeader sh = ref MemoryMarshal.AsRef(config.ROM.AsSpan(ofs)); + + short[] pcm16 = SampleUtils.PCMU8ToPCM16(config.ROM.AsSpan(ofs + 0x10, sh.Length)); + 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(AlphaDreamConfig config, SF2 sf2, Dictionary sampleDict) + { + for (ushort 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; + } + + 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 VoiceEntry entry = ref MemoryMarshal.AsRef(config.ROM.AsSpan(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 == VoiceEntry.FIXED_FREQ_TRUE) + { + 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 == 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/Commands.cs b/VG Music Studio - Core/GBA/AlphaDream/Commands.cs new file mode 100644 index 0000000..faaee21 --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/Commands.cs @@ -0,0 +1,113 @@ +using Kermalis.VGMusicStudio.Core.Util; +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 => $"{ConfigUtils.GetKeyName(Note)} {Volume} {Duration}"; + + public byte Note { 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 => $"{ConfigUtils.GetKeyName(Note)} {Duration}"; + + public byte Note { 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 => $"{ConfigUtils.GetKeyName(Note)} {Volume} {Duration}"; + + public byte Note { 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 => $"{ConfigUtils.GetKeyName(Note)} {Duration}"; + + public byte Note { 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/Enums.cs b/VG Music Studio - Core/GBA/AlphaDream/Enums.cs new file mode 100644 index 0000000..bca47c4 --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/Enums.cs @@ -0,0 +1,16 @@ +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/Structs.cs b/VG Music Studio - Core/GBA/AlphaDream/Structs.cs new file mode 100644 index 0000000..dcd8832 --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/Structs.cs @@ -0,0 +1,40 @@ +using System.Runtime.InteropServices; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 16)] +internal struct SampleHeader +{ + 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; +} +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 8)] +internal struct VoiceEntry +{ + public const byte FIXED_FREQ_TRUE = 0x80; + + public byte MinKey; + public byte MaxKey; + public byte Sample; + /// 0x80 if True + public byte IsFixedFrequency; + public byte Unknown1; + public byte Unknown2; + public byte Unknown3; + public byte Unknown4; +} + +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 similarity index 91% rename from VG Music Studio/Core/GBA/AlphaDream/Track.cs rename to VG Music Studio - Core/GBA/AlphaDream/Track.cs index 296a284..d9401fc 100644 --- a/VG Music Studio/Core/GBA/AlphaDream/Track.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/Track.cs @@ -4,7 +4,7 @@ internal class Track { public readonly byte Index; public readonly string Type; - public readonly Channel Channel; + public readonly AlphaDreamChannel Channel; public byte Voice; public byte PitchBendRange; @@ -24,12 +24,12 @@ public int GetPitch() return PitchBend * (PitchBendRange / 2); } - public Track(byte i, Mixer mixer) + public Track(byte i, AlphaDreamMixer mixer) { Index = i; if (i >= 8) { - Type = Utils.PSGTypes[i & 3]; + Type = GBAUtils.PSGTypes[i & 3]; Channel = new SquareChannel(mixer); // TODO: PSG Channels 3 and 4 } else diff --git a/VG Music Studio - Core/GBA/GBAUtils.cs b/VG Music Studio - Core/GBA/GBAUtils.cs new file mode 100644 index 0000000..5106acf --- /dev/null +++ b/VG Music Studio - Core/GBA/GBAUtils.cs @@ -0,0 +1,12 @@ +namespace Kermalis.VGMusicStudio.Core.GBA; + +internal static class GBAUtils +{ + public const double AGB_FPS = 59.7275; + public const int SystemClock = 16_777_216; // 16.777216 MHz (16*1024*1024 Hz) + + public const int CartridgeOffset = 0x08_000_000; + public const int CartridgeCapacity = 0x02_000_000; + + public static readonly string[] PSGTypes = new string[4] { "Square 1", "Square 2", "PCM4", "Noise" }; +} diff --git a/VG Music Studio - Core/GBA/MP2K/Channel.cs b/VG Music Studio - Core/GBA/MP2K/Channel.cs new file mode 100644 index 0000000..f25482d --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/Channel.cs @@ -0,0 +1,787 @@ +using System; +using System.Collections; +using System.Runtime.InteropServices; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal abstract class Channel +{ + public EnvelopeState State = EnvelopeState.Dead; + public Track? Owner; + protected readonly MP2KMixer _mixer; + + public NoteInfo 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(MP2KMixer 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(MP2KMixer mixer) : base(mixer) { } + public void Init(Track 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 = MemoryMarshal.Read(rom.AsSpan(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 = MemoryMarshal.Read(rom.AsSpan(_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) * 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 + { + 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(MP2KMixer mixer) : base(mixer) { } + protected void Init(Track owner, NoteInfo 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(MP2KMixer mixer) : base(mixer) + { + // + } + public void Init(Track owner, NoteInfo 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 = 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); + } +} +internal class PCM4Channel : PSGChannel +{ + private float[] _sample; + + public PCM4Channel(MP2KMixer mixer) : base(mixer) + { + // + } + public void Init(Track owner, NoteInfo note, ADSR env, int instPan, int sampleOffset) + { + Init(owner, note, env, instPan); + _sample = Utils.PCM4ToFloat(sampleOffset); + } + + 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); + } +} +internal class NoiseChannel : PSGChannel +{ + private BitArray _pat; + + public NoiseChannel(MP2KMixer mixer) : base(mixer) + { + // + } + public void Init(Track owner, NoteInfo 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.Note + (int)MathF.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 = 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/Commands.cs b/VG Music Studio - Core/GBA/MP2K/Commands.cs new file mode 100644 index 0000000..5778eec --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/Commands.cs @@ -0,0 +1,193 @@ +using Kermalis.VGMusicStudio.Core.Util; +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 => Note == -1 ? "All Ties" : ConfigUtils.GetKeyName(Note); + + public int Note { 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 => $"{ConfigUtils.GetKeyName(Note)} {Velocity} {Duration}"; + + public byte Note { 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/Enums.cs b/VG Music Studio - Core/GBA/MP2K/Enums.cs new file mode 100644 index 0000000..cb169c6 --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/Enums.cs @@ -0,0 +1,76 @@ +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/MP2KConfig.cs b/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs new file mode 100644 index 0000000..c583a1e --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs @@ -0,0 +1,248 @@ +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 EndianBinaryReader Reader; // TODO: Need? + 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))) + { + 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, + 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) + { + long songIndex = ConfigUtils.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, 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, Utils.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 + 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, 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(long index) + { + Song? s = GetFirstSong(index); + return s is not null ? s.Name : index.ToString(); + } + + public override void Dispose() + { + Reader.Stream.Dispose(); + } +} 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 0000000..43d5264 --- /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.CartridgeCapacity) + { + throw new InvalidDataException($"The ROM is too large. Maximum size is 0x{GBAUtils.CartridgeCapacity: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/MP2KExceptions.cs b/VG Music Studio - Core/GBA/MP2K/MP2KExceptions.cs new file mode 100644 index 0000000..f43e2da --- /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 0000000..1c8da7c --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong.cs @@ -0,0 +1,535 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal sealed partial class MP2KLoadedSong : ILoadedSong +{ + public List[] Events { get; private set; } + public long MaxTicks { get; private set; } + public long ElapsedTicks { get; internal set; } + internal int LongestTrack; + + private readonly MP2KPlayer _player; + public readonly int VoiceTableOffset; + internal readonly Track[] Tracks; + + public MP2KLoadedSong(long index, MP2KPlayer player, MP2KConfig cfg, int? oldVoiceTableOffset, string?[] voiceTypeCache) + { + _player = player; + + ref SongEntry entry = ref MemoryMarshal.AsRef(cfg.ROM.AsSpan(cfg.SongTableOffsets[0] + ((int)index * 8))); + cfg.Reader.Stream.Position = entry.HeaderOffset - GBA.GBAUtils.CartridgeOffset; + SongHeader header = cfg.Reader.ReadObject(); // TODO: Can I RefStruct this? If not, should still ditch reader and use pointer + VoiceTableOffset = header.VoiceTableOffset - GBA.GBAUtils.CartridgeOffset; + if (oldVoiceTableOffset != VoiceTableOffset) + { + Array.Clear(voiceTypeCache); + } + + Tracks = new Track[header.NumTracks]; + Events = new List[header.NumTracks]; + for (byte trackIndex = 0; trackIndex < header.NumTracks; trackIndex++) + { + int trackStart = header.TrackOffsets[trackIndex] - GBA.GBAUtils.CartridgeOffset; + Tracks[trackIndex] = new Track(trackIndex, trackStart); + Events[trackIndex] = new List(); + + byte runCmd = 0, prevKey = 0, prevVelocity = 0x7F; + int callStackDepth = 0; + AddEvents(trackStart, cfg, trackIndex, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); + } + } + + private static void AddEvent(List trackEvents, long offset, ICommand command) + { + trackEvents.Add(new SongEvent(offset, command)); + } + private static bool EventExists(List trackEvents, long offset) + { + return trackEvents.Any(e => e.Offset == offset); + } + private static void EmulateNote(List trackEvents, long offset, byte key, byte velocity, byte addedDuration, + ref byte runCmd, ref byte prevKey, ref byte prevVelocity) + { + prevKey = key; + prevVelocity = velocity; + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new NoteCommand + { + Note = key, + Velocity = velocity, + Duration = runCmd == 0xCF ? -1 : (Utils.RestTable[runCmd - 0xCF] + addedDuration), + }); + } + } + private void AddEvents(long startOffset, MP2KConfig cfg, byte trackIndex, + ref byte runCmd, ref byte prevKey, ref byte prevVelocity, ref int callStackDepth) + { + cfg.Reader.Stream.Position = startOffset; + List trackEvents = Events[trackIndex]; + + Span peek = stackalloc byte[3]; + bool cont = true; + while (cont) + { + long offset = cfg.Reader.Stream.Position; + + byte cmd = cfg.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; + cfg.Reader.PeekBytes(peek.Slice(0, 2)); + if (peek[0] > 0x7F) + { + velocity = prevVelocity; + addedDuration = 0; + } + else if (peek[1] > 3) + { + velocity = cfg.Reader.ReadByte(); + addedDuration = 0; + } + else + { + velocity = cfg.Reader.ReadByte(); + addedDuration = cfg.Reader.ReadByte(); + } + EmulateNote(trackEvents, offset, cmd, velocity, addedDuration, ref runCmd, ref prevKey, ref prevVelocity); + } + else if (cmd >= 0xCF) + { + byte key, velocity, addedDuration; + cfg.Reader.PeekBytes(peek); + if (peek[0] > 0x7F) + { + key = prevKey; + velocity = prevVelocity; + addedDuration = 0; + } + else if (peek[1] > 0x7F) + { + key = cfg.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 = cfg.Reader.ReadByte(); + velocity = cfg.Reader.ReadByte(); + addedDuration = 0; + } + else + { + key = cfg.Reader.ReadByte(); + velocity = cfg.Reader.ReadByte(); + addedDuration = cfg.Reader.ReadByte(); + } + EmulateNote(trackEvents, offset, key, velocity, addedDuration, ref runCmd, ref prevKey, ref prevVelocity); + } + + #endregion + + #region Rests + + else if (cmd >= 0x80 && cmd <= 0xB0) + { + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new RestCommand { Rest = Utils.RestTable[cmd - 0x80] }); + } + } + + #endregion + + #region Commands + + else if (runCmd < 0xCF && cmd <= 0x7F) + { + switch (runCmd) + { + case 0xBD: + { + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new VoiceCommand { Voice = cmd }); + } + break; + } + case 0xBE: + { + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new VolumeCommand { Volume = cmd }); + } + break; + } + case 0xBF: + { + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new PanpotCommand { Panpot = (sbyte)(cmd - 0x40) }); + } + break; + } + case 0xC0: + { + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new PitchBendCommand { Bend = (sbyte)(cmd - 0x40) }); + } + break; + } + case 0xC1: + { + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new PitchBendRangeCommand { Range = cmd }); + } + break; + } + case 0xC2: + { + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new LFOSpeedCommand { Speed = cmd }); + } + break; + } + case 0xC3: + { + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new LFODelayCommand { Delay = cmd }); + } + break; + } + case 0xC4: + { + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new LFODepthCommand { Depth = cmd }); + } + break; + } + case 0xC5: + { + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new LFOTypeCommand { Type = (LFOType)cmd }); + } + break; + } + case 0xC8: + { + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new TuneCommand { Tune = (sbyte)(cmd - 0x40) }); + } + break; + } + case 0xCD: + { + byte arg = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new LibraryCommand { Command = cmd, Argument = arg }); + } + break; + } + case 0xCE: + { + prevKey = cmd; + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new EndOfTieCommand { Note = cmd }); + } + break; + } + default: throw new MP2KInvalidRunningStatusCMDException(trackIndex, (int)offset, runCmd); + } + } + else if (cmd > 0xB0 && cmd < 0xCF) + { + switch (cmd) + { + case 0xB1: + case 0xB6: + { + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new FinishCommand { Prev = cmd == 0xB6 }); + } + cont = false; + break; + } + case 0xB2: + { + int jumpOffset = cfg.Reader.ReadInt32() - GBA.GBAUtils.CartridgeOffset; + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new JumpCommand { Offset = jumpOffset }); + if (!EventExists(trackEvents, jumpOffset)) + { + AddEvents(jumpOffset, cfg, trackIndex, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); + } + } + cont = false; + break; + } + case 0xB3: + { + int callOffset = cfg.Reader.ReadInt32() - GBA.GBAUtils.CartridgeOffset; + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new CallCommand { Offset = callOffset }); + } + if (callStackDepth < 3) + { + long backup = cfg.Reader.Stream.Position; + callStackDepth++; + AddEvents(callOffset, cfg, trackIndex, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); + cfg.Reader.Stream.Position = backup; + } + else + { + throw new MP2KTooManyNestedCallsException(trackIndex); + } + break; + } + case 0xB4: + { + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, 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 = cfg.Reader.ReadByte(); + byte address = cfg.Reader.ReadByte(); + byte data = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new MemoryAccessCommand { Operator = op, Address = address, Data = data }); + } + break; + } + case 0xBA: + { + byte priority = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new PriorityCommand { Priority = priority }); + } + break; + } + case 0xBB: + { + byte tempoArg = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new TempoCommand { Tempo = (ushort)(tempoArg * 2) }); + } + break; + } + case 0xBC: + { + sbyte transpose = cfg.Reader.ReadSByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new TransposeCommand { Transpose = transpose }); + } + break; + } + // Commands that work within running status: + case 0xBD: + { + byte voice = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new VoiceCommand { Voice = voice }); + } + break; + } + case 0xBE: + { + byte volume = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new VolumeCommand { Volume = volume }); + } + break; + } + case 0xBF: + { + byte panArg = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); + } + break; + } + case 0xC0: + { + byte bendArg = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new PitchBendCommand { Bend = (sbyte)(bendArg - 0x40) }); + } + break; + } + case 0xC1: + { + byte range = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new PitchBendRangeCommand { Range = range }); + } + break; + } + case 0xC2: + { + byte speed = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new LFOSpeedCommand { Speed = speed }); + } + break; + } + case 0xC3: + { + byte delay = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new LFODelayCommand { Delay = delay }); + } + break; + } + case 0xC4: + { + byte depth = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new LFODepthCommand { Depth = depth }); + } + break; + } + case 0xC5: + { + byte type = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new LFOTypeCommand { Type = (LFOType)type }); + } + break; + } + case 0xC8: + { + byte tuneArg = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new TuneCommand { Tune = (sbyte)(tuneArg - 0x40) }); + } + break; + } + case 0xCD: + { + byte command = cfg.Reader.ReadByte(); + byte arg = cfg.Reader.ReadByte(); + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new LibraryCommand { Command = command, Argument = arg }); + } + break; + } + case 0xCE: + { + int key = cfg.Reader.PeekByte() <= 0x7F ? (prevKey = cfg.Reader.ReadByte()) : -1; + if (!EventExists(trackEvents, offset)) + { + AddEvent(trackEvents, offset, new EndOfTieCommand { Note = key }); + } + break; + } + default: throw new MP2KInvalidCMDException(trackIndex, (int)offset, cmd); + } + } + + #endregion + } + } + + internal 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; + } + + e.Ticks.Add(ElapsedTicks); + _player.ExecuteNext(track, ref u); + if (track.Stopped) + { + break; + } + + ElapsedTicks += track.Rest; + track.Rest = 0; + } + if (ElapsedTicks > MaxTicks) + { + LongestTrack = trackIndex; + MaxTicks = ElapsedTicks; + } + track.StopAllChannels(); + } + } + + public void Dispose() + { + 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 0000000..4a9cd2d --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs @@ -0,0 +1,253 @@ +using Kermalis.VGMusicStudio.MIDI; +using System; +using System.Collections.Generic; +using System.Diagnostics; +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 List<(int AbsoluteTick, (byte Numerator, byte Denominator))> 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); + MIDITrackChunk metaTrack = midi.CreateTrack(); + + foreach ((int AbsoluteTick, (byte Numerator, byte Denominator)) e in args.TimeSignatures) + { + metaTrack.Insert(e.AbsoluteTick, MetaMessage.CreateTimeSignatureMessage(e.Item2.Numerator, e.Item2.Denominator)); + } + + for (byte trackIndex = 0; trackIndex < Events.Length; trackIndex++) + { + MIDITrackChunk track = midi.CreateTrack(); + + bool foundTranspose = false; + int endOfPattern = 0; + long startOfPatternTicks = 0; + long endOfPatternTicks = 0; + sbyte transpose = 0; + 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.Insert(ticks, new NoteOnMessage(trackIndex, (MIDINote)key, 0)); + playing.Remove(nc); + } + break; + } + case FinishCommand _: + { + track.Insert(ticks, new MetaMessage(MetaMessageType.EndOfTrack, Array.Empty())); + goto endOfTrack; + } + case JumpCommand c: + { + if (trackIndex == 0) + { + int jumpCmd = trackEvents.FindIndex(ev => ev.Offset == c.Offset); + metaTrack.Insert((int)trackEvents[jumpCmd].Ticks[0], new MetaMessage(MetaMessageType.Marker, new byte[] { (byte)'[' })); + metaTrack.Insert(ticks, new MetaMessage(MetaMessageType.Marker, new byte[] { (byte)']' })); + } + break; + } + case LFODelayCommand c: + { + track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)26, c.Delay)); + break; + } + case LFODepthCommand c: + { + track.Insert(ticks, new ControllerMessage(trackIndex, ControllerType.ModulationWheel, c.Depth)); + break; + } + case LFOSpeedCommand c: + { + track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)21, c.Speed)); + break; + } + case LFOTypeCommand c: + { + track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)22, (byte)c.Type)); + break; + } + case LibraryCommand c: + { + track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)30, c.Command)); + track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)29, c.Argument)); + break; + } + case MemoryAccessCommand c: + { + track.Insert(ticks, new ControllerMessage(trackIndex, ControllerType.EffectControl2, c.Operator)); + track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)14, c.Address)); + track.Insert(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.Insert(ticks, new NoteOnMessage(trackIndex, (MIDINote)note, c.Velocity)); + if (c.Duration != -1) + { + track.Insert(ticks + c.Duration, new NoteOnMessage(trackIndex, (MIDINote)note, 0)); + } + else + { + playing.Add(c); + } + break; + } + case PanpotCommand c: + { + track.Insert(ticks, new ControllerMessage(trackIndex, ControllerType.Pan, (byte)(c.Panpot + 0x40))); + break; + } + case PitchBendCommand c: + { + track.Insert(ticks, new PitchBendMessage(trackIndex, 0, (byte)(c.Bend + 0x40))); + break; + } + case PitchBendRangeCommand c: + { + track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)20, c.Range)); + break; + } + case PriorityCommand c: + { + track.Insert(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.Insert(ticks, MetaMessage.CreateTempoMessage(c.Tempo)); + break; + } + case TransposeCommand c: + { + transpose = c.Transpose; + break; + } + case TuneCommand c: + { + track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)24, (byte)(c.Tune + 0x40))); + break; + } + case VoiceCommand c: + { + track.Insert(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.Insert(ticks, new ControllerMessage(trackIndex, ControllerType.ChannelVolume, (byte)volume)); + break; + } + } + } + endOfTrack: + ; + } + + metaTrack.Insert(metaTrack.NumTicks, new MetaMessage(MetaMessageType.EndOfTrack, Array.Empty())); + + midi.Save(fileName); + } +} 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 0000000..b52245c --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs @@ -0,0 +1,273 @@ +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 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; + + internal MP2KMixer(MP2KConfig 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[4] { _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); + } + + internal PCM8Channel AllocPCM8Channel(Track owner, ADSR env, NoteInfo 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; + } + internal PSGChannel AllocPSGChannel(Track owner, ADSR env, NoteInfo 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; + } + + internal void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * GBA.GBAUtils.AGB_FPS); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * GBA.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; + } + + private WaveFileWriter? _waveWriter; + public void CreateWaveWriter(string fileName) + { + _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); + } + public void CloseWaveWriter() + { + _waveWriter!.Dispose(); + _waveWriter = null; + } + 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++) + { + 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 : 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 0000000..7bbd6d1 --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs @@ -0,0 +1,786 @@ +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +public sealed partial class MP2KPlayer : IPlayer +{ + private readonly MP2KMixer _mixer; + private readonly MP2KConfig _config; + private readonly TimeBarrier _time; + private Thread? _thread; + private ushort _tempo; + private int _tempoStack; + private long _elapsedLoops; + + private MP2KLoadedSong? _loadedSong; + public ILoadedSong? LoadedSong => _loadedSong; + public bool ShouldFadeOut { get; set; } + public long NumLoops { get; set; } + + public PlayerState State { get; private set; } + public event Action? SongEnded; + + private readonly string?[] _voiceTypeCache; + + internal MP2KPlayer(MP2KConfig config, MP2KMixer mixer) + { + _config = config; + _mixer = mixer; + + _voiceTypeCache = new string[256]; + + _time = new TimeBarrier(GBA.GBAUtils.AGB_FPS); + } + private void CreateThread() + { + _thread = new Thread(Tick) { Name = "MP2K Player Tick" }; + _thread.Start(); + } + private void WaitThread() + { + if (_thread is not null && (_thread.ThreadState is ThreadState.Running or ThreadState.WaitSleepJoin)) + { + _thread.Join(); + } + } + + private void InitEmulation() + { + _tempo = 150; + _tempoStack = 0; + _elapsedLoops = 0; + _loadedSong!.ElapsedTicks = 0; + _mixer.ResetFade(); + for (int trackIndex = 0; trackIndex < _loadedSong.Tracks.Length; trackIndex++) + { + _loadedSong.Tracks[trackIndex].Init(); + } + } + public void LoadSong(long index) + { + int? oldVoiceTableOffset = _loadedSong?.VoiceTableOffset; + if (_loadedSong is not null) + { + _loadedSong.Dispose(); + _loadedSong = null; + } + + // If there's an exception, this will remain null + _loadedSong = new MP2KLoadedSong(index, this, _config, oldVoiceTableOffset, _voiceTypeCache); + _loadedSong.SetTicks(); + if (_loadedSong.Events.Length == 0) + { + _loadedSong.Dispose(); + _loadedSong = null; + } + } + public void SetCurrentPosition(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) + { + Pause(); + } + InitEmulation(); + MP2KLoadedSong s = _loadedSong; + bool u = false; + while (ticks != s.ElapsedTicks) + { + while (_tempoStack >= 150) + { + _tempoStack -= 150; + for (int trackIndex = 0; trackIndex < s.Tracks.Length; trackIndex++) + { + Track track = s.Tracks[trackIndex]; + if (!track.Stopped) + { + track.Tick(); + while (track.Rest == 0 && !track.Stopped) + { + ExecuteNext(track, ref u); + } + } + } + s.ElapsedTicks++; + if (s.ElapsedTicks == ticks) + { + break; + } + } + _tempoStack += _tempo; + } + + for (int i = 0; i < s.Tracks.Length; i++) + { + s.Tracks[i].StopAllChannels(); + } + Pause(); + } + public void Play() + { + if (_loadedSong is null) + { + SongEnded?.Invoke(); + return; + } + + if (State is not PlayerState.ShutDown) + { + Stop(); + InitEmulation(); + State = PlayerState.Playing; + CreateThread(); + } + } + public void Pause() + { + 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(); + } + } + public void Record(string fileName) + { + _mixer.CreateWaveWriter(fileName); + + InitEmulation(); + State = PlayerState.Recording; + CreateThread(); + WaitThread(); + + _mixer.CloseWaveWriter(); + } + + public void SaveAsMIDI(string fileName, MIDISaveArgs args) + { + _loadedSong!.SaveAsMIDI(fileName, args); + } + public void UpdateSongState(SongState info) + { + info.Tempo = _tempo; + for (int trackIndex = 0; trackIndex < _loadedSong!.Tracks.Length; trackIndex++) + { + Track track = _loadedSong.Tracks[trackIndex]; + SongState.Track tin = info.Tracks[trackIndex]; + tin.Position = track.DataOffset; + tin.Rest = track.Rest; + tin.Voice = track.Voice; + tin.LFO = track.LFODepth; + ref string? voiceType = ref _voiceTypeCache[track.Voice]; + if (voiceType is null) + { + byte t = _config.ROM[_loadedSong.VoiceTableOffset + (track.Voice * 12)]; + if (t == (byte)VoiceFlags.KeySplit) + { + voiceType = "Key Split"; + } + else if (t == (byte)VoiceFlags.Drum) + { + voiceType = "Drum"; + } + else + { + switch ((VoiceType)(t & 0x7)) // Disregard the other flags + { + case VoiceType.PCM8: voiceType = "PCM8"; break; + case VoiceType.Square1: voiceType = "Square 1"; break; + case VoiceType.Square2: voiceType = "Square 2"; break; + case VoiceType.PCM4: voiceType = "PCM4"; break; + case VoiceType.Noise: voiceType = "Noise"; break; + case VoiceType.Invalid5: voiceType = "Invalid 5"; break; + case VoiceType.Invalid6: voiceType = "Invalid 6"; break; + default: voiceType = "Invalid 7"; break; // VoiceType.Invalid7 + } + } + } + tin.Type = voiceType; + 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.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; + } + } + } + + private void PlayNote(Track 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; + if (!track.Ready) + { + return; // Tracks do not play unless they have had a voice change event + } + + bool fromDrum = false; + int offset = _loadedSong!.VoiceTableOffset + (track.Voice * 12); + while (true) + { + ref VoiceEntry v = ref MemoryMarshal.AsRef(_config.ROM.AsSpan(offset)); + if (v.Type == (int)VoiceFlags.KeySplit) + { + fromDrum = false; // In case there is a multi within a drum + byte inst = _config.ROM[v.Int8 - GBAUtils.CartridgeOffset + note]; + offset = v.Int4 - GBAUtils.CartridgeOffset + (inst * 12); + } + else if (v.Type == (int)VoiceFlags.Drum) + { + fromDrum = true; + offset = v.Int4 - GBAUtils.CartridgeOffset + (note * 12); + } + else + { + var ni = new NoteInfo + { + Duration = track.RunCmd == 0xCF ? -1 : (Utils.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 = _config.HasPokemonCompression && ((v.Type & (int)VoiceFlags.Compressed) != 0); + _mixer.AllocPCM8Channel(track, v.ADSR, ni, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + bFixed, bCompressed, v.Int4 - GBAUtils.CartridgeOffset); + return; + } + case VoiceType.Square1: + case VoiceType.Square2: + { + _mixer.AllocPSGChannel(track, v.ADSR, ni, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + type, (SquarePattern)v.Int4); + return; + } + case VoiceType.PCM4: + { + _mixer.AllocPSGChannel(track, v.ADSR, ni, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + type, v.Int4 - GBAUtils.CartridgeOffset); + return; + } + case VoiceType.Noise: + { + _mixer.AllocPSGChannel(track, v.ADSR, ni, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + type, (NoisePattern)v.Int4); + return; + } + } + return; // Prevent infinite loop with invalid instruments + } + } + } + internal void ExecuteNext(Track track, ref bool update) + { + byte[] rom = _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; + } + PlayNote(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; + } + 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.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)) - GBA.GBAUtils.CartridgeOffset; + 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)) - GBA.GBAUtils.CartridgeOffset; + 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: + { + _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); + } + } + } + + private void Tick() + { + MP2KLoadedSong s = _loadedSong!; + _time.Start(); + while (true) + { + PlayerState state = State; + bool playing = state == PlayerState.Playing; + bool recording = state == PlayerState.Recording; + if (!playing && !recording) + { + break; + } + + while (_tempoStack >= 150) + { + _tempoStack -= 150; + bool allDone = true; + for (int trackIndex = 0; trackIndex < s.Tracks.Length; trackIndex++) + { + Track track = s.Tracks[trackIndex]; + track.Tick(); + bool update = false; + while (track.Rest == 0 && !track.Stopped) + { + ExecuteNext(track, ref update); + } + if (trackIndex == s.LongestTrack) + { + if (s.ElapsedTicks == s.MaxTicks) + { + if (!track.Stopped) + { + List evs = s.Events[trackIndex]; + for (int i = 0; i < evs.Count; i++) + { + SongEvent ev = evs[i]; + if (ev.Offset == track.DataOffset) + { + s.ElapsedTicks = ev.Ticks[0] - track.Rest; + break; + } + } + _elapsedLoops++; + if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) + { + _mixer.BeginFadeOut(); + } + } + } + else + { + s.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) + { + // TODO: lock state + _mixer.Process(playing, recording); + _time.Stop(); + State = PlayerState.Stopped; + SongEnded?.Invoke(); + return; + } + } + _tempoStack += _tempo; + _mixer.Process(playing, recording); + if (playing) + { + _time.Wait(); + } + } + _time.Stop(); + } + + public void Dispose() + { + if (State is not PlayerState.ShutDown) + { + State = PlayerState.ShutDown; + WaitThread(); + } + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/Structs.cs b/VG Music Studio - Core/GBA/MP2K/Structs.cs new file mode 100644 index 0000000..fe06998 --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/Structs.cs @@ -0,0 +1,80 @@ +using Kermalis.EndianBinaryIO; +using System.Runtime.InteropServices; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 8)] +internal struct SongEntry +{ + public int HeaderOffset; + public short Player; + public byte Unknown1; + public byte Unknown2; +} +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; } +} +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 12)] +internal struct VoiceEntry +{ + public byte Type; // 0 + public byte RootNote; // 1 + public byte Unknown; // 2 + public byte Pan; // 3 + /// SquarePattern for Square1/Square2, NoisePattern for Noise, Address for PCM8/PCM4/KeySplit/Drum + public int Int4; // 4 + /// ADSR for PCM8/Square1/Square2/PCM4/Noise, KeysAddress for KeySplit + public ADSR ADSR; // 8 + + public int Int8 => (ADSR.R << 24) | (ADSR.S << 16) | (ADSR.D << 8) | (ADSR.A); +} +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 4)] +internal struct ADSR +{ + public byte A; + public byte D; + public byte S; + public byte R; +} +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 6)] +internal struct GoldenSunPSG +{ + /// Always 0x80 + public byte Unknown; + public GoldenSunPSGType Type; + public byte InitialCycle; + public byte CycleSpeed; + public byte CycleAmplitude; + public byte MinimumCycle; +} +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 16)] +internal struct SampleHeader +{ + 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; +} + +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/Track.cs b/VG Music Studio - Core/GBA/MP2K/Track.cs similarity index 97% rename from VG Music Studio/Core/GBA/MP2K/Track.cs rename to VG Music Studio - Core/GBA/MP2K/Track.cs index 1288008..7b047ff 100644 --- a/VG Music Studio/Core/GBA/MP2K/Track.cs +++ b/VG Music Studio - Core/GBA/MP2K/Track.cs @@ -27,7 +27,7 @@ internal class Track public int[] CallStack = new int[3]; public byte CallStackDepth; public byte RunCmd; - public byte PrevKey; + public byte PrevNote; public byte PrevVelocity; public readonly List Channels = new List(); @@ -87,7 +87,7 @@ public void Init() Transpose = 0; DataOffset = _startOffset; RunCmd = 0; - PrevKey = 0; + PrevNote = 0; PrevVelocity = 0x7F; PitchBendRange = 2; LFOType = LFOType.Pitch; @@ -144,7 +144,7 @@ public void ReleaseChannels(int key) for (int i = 0; i < chans.Length; i++) { Channel c = chans[i]; - if (c.Note.OriginalKey == key && c.Note.Duration == -1) + if (c.Note.OriginalNote == key && c.Note.Duration == -1) { c.Release(); } diff --git a/VG Music Studio - Core/GBA/MP2K/Utils.cs b/VG Music Studio - Core/GBA/MP2K/Utils.cs new file mode 100644 index 0000000..223bb43 --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/Utils.cs @@ -0,0 +1,175 @@ +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 + // TODO: Do runtime instead of make arrays + public static float[] PCM4ToFloat(int sampleOffset) + { + var config = (MP2KConfig)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 = (MP2KConfig)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(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/MP2K.yaml b/VG Music Studio - Core/MP2K.yaml similarity index 100% rename from VG Music Studio/MP2K.yaml rename to VG Music Studio - Core/MP2K.yaml 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 0000000..fb6b818 --- /dev/null +++ b/VG Music Studio - Core/Mixer.cs @@ -0,0 +1,93 @@ +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? MixerVolumeChanged; + + public readonly bool[] Mutes; + private IWavePlayer _out; + private AudioSessionControl _appVolume; + + private bool _shouldSendVolUpdateEvent = true; + + protected Mixer() + { + Mutes = new bool[SongState.MAX_TRACKS]; + } + + 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 OnVolumeChanged(float volume, bool isMuted) + { + if (_shouldSendVolUpdateEvent) + { + MixerVolumeChanged?.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() + { + _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 new file mode 100644 index 0000000..e97da44 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/Channel.cs @@ -0,0 +1,368 @@ +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 is not 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 new file mode 100644 index 0000000..cc8ac62 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/Commands.cs @@ -0,0 +1,130 @@ +using Kermalis.VGMusicStudio.Core.Util; +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 => $"{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 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/DSEConfig.cs b/VG Music Studio - Core/NDS/DSE/DSEConfig.cs new file mode 100644 index 0000000..69edd6c --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSEConfig.cs @@ -0,0 +1,45 @@ +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Properties; +using System.IO; +using System.Linq; + +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); + } + + var songs = new Song[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(); + 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/DSEEngine.cs b/VG Music Studio - Core/NDS/DSE/DSEEngine.cs new file mode 100644 index 0000000..a7a933e --- /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/DSEExceptions.cs b/VG Music Studio - Core/NDS/DSE/DSEExceptions.cs new file mode 100644 index 0000000..82c22e9 --- /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/DSEMixer.cs b/VG Music Studio - Core/NDS/DSE/DSEMixer.cs new file mode 100644 index 0000000..cbcf840 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSEMixer.cs @@ -0,0 +1,221 @@ +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 Channel[] _channels; + private readonly BufferedWaveProvider _buffer; + + 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 Channel[NUM_CHANNELS]; + for (byte i = 0; i < NUM_CHANNELS; i++) + { + _channels[i] = new Channel(i); + } + + _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + { + DiscardOnBufferOverflow = true, + BufferLength = _samplesPerBuffer * 64, + }; + Init(_buffer); + } + + internal Channel? AllocateChannel() + { + static int GetScore(Channel c) + { + // Free channels should be used before releasing channels + return c.Owner is null ? -2 : Utils.IsStateRemovable(c.State) ? -1 : 0; + } + Channel? nChan = null; + for (int i = 0; i < NUM_CHANNELS; i++) + { + Channel 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++) + { + Channel chan = _channels[i]; + if (chan.Owner is null) + { + continue; + } + + chan.Volume = (byte)chan.StepEnvelope(); + if (chan.NoteLength == 0 && !Utils.IsStateRemovable(chan.State)) + { + chan.SetEnvelopePhase7_2074ED8(); + } + int vol = SDAT.SDATUtils.SustainTable[chan.NoteVelocity] + SDAT.SDATUtils.SustainTable[chan.Volume] + SDAT.SDATUtils.SustainTable[chan.Owner.Volume] + SDAT.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 (Utils.IsStateRemovable(chan.State) && vol <= -92544) + { + chan.Stop(); + } + else + { + chan.Volume = SDAT.SDATUtils.GetChannelVolume(vol); + chan.Panpot = chan.Owner.Panpot; + chan.Timer = SDAT.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 WaveFileWriter? _waveWriter; + public void CreateWaveWriter(string fileName) + { + _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); + } + public void CloseWaveWriter() + { + _waveWriter!.Dispose(); + _waveWriter = null; + } + 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++) + { + Channel 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 0000000..3eb9b59 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs @@ -0,0 +1,1046 @@ +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +public sealed class DSEPlayer : IPlayer, ILoadedSong +{ + private readonly DSEMixer _mixer; + private readonly DSEConfig _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 ILoadedSong LoadedSong => this; + public bool ShouldFadeOut { get; set; } + public long NumLoops { get; set; } + private int _longestTrack; + + public PlayerState State { get; private set; } + public event Action? SongEnded; + + public DSEPlayer(DSEConfig config, DSEMixer mixer) + { + _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 is not null && (_thread.ThreadState is ThreadState.Running or 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; + } + + 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(); + } + } + 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 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 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 = r.Stream.Position; + r.Stream.Position += 0x14; // Skip header + _tracks[trackIndex] = new Track(trackIndex, (int)r.Stream.Position); + + uint lastNoteDuration = 0, lastRest = 0; + bool cont = true; + while (cont) + { + long offset = r.Stream.Position; + void AddEvent(ICommand command) + { + Events[trackIndex].Add(new SongEvent(offset, command)); + } + 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)offset, 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(offset)) + { + AddEvent(new NoteCommand { Note = (byte)n, OctaveChange = (sbyte)oct, Velocity = cmd, Duration = duration }); + } + } + 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 + r.ReadSByte()); + if (!EventExists(offset)) + { + AddEvent(new RestCommand { Rest = lastRest }); + } + break; + } + case 0x92: + { + lastRest = r.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new RestCommand { Rest = lastRest }); + } + break; + } + case 0x93: + { + lastRest = r.ReadUInt16(); + if (!EventExists(offset)) + { + AddEvent(new RestCommand { Rest = lastRest }); + } + break; + } + case 0x94: + { + lastRest = (uint)(r.ReadByte() | (r.ReadByte() << 8) | (r.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 = r.Stream.Position }); + } + break; + } + case 0xA0: + { + byte octave = r.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new OctaveSetCommand { Octave = octave }); + } + break; + } + case 0xA1: + { + sbyte change = r.ReadSByte(); + if (!EventExists(offset)) + { + AddEvent(new OctaveAddCommand { OctaveChange = change }); + } + break; + } + case 0xA4: + case 0xA5: // The code for these two is identical + { + byte tempoArg = r.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new TempoCommand { Command = cmd, Tempo = tempoArg }); + } + break; + } + case 0xAB: + { + byte[] bytes = new byte[1]; + r.ReadBytes(bytes); + if (!EventExists(offset)) + { + AddEvent(new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); + } + break; + } + case 0xAC: + { + byte voice = r.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new VoiceCommand { Voice = voice }); + } + break; + } + case 0xCB: + case 0xF8: + { + byte[] bytes = new byte[2]; + r.ReadBytes(bytes); + if (!EventExists(offset)) + { + AddEvent(new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); + } + break; + } + case 0xD7: + { + ushort bend = r.ReadUInt16(); + if (!EventExists(offset)) + { + AddEvent(new PitchBendCommand { Bend = bend }); + } + break; + } + case 0xE0: + { + byte volume = r.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new VolumeCommand { Volume = volume }); + } + break; + } + case 0xE3: + { + byte expression = r.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new ExpressionCommand { Expression = expression }); + } + break; + } + case 0xE8: + { + byte panArg = r.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 = new byte[1]; + r.ReadBytes(args); + 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 = new byte[2]; + r.ReadBytes(args); + if (!EventExists(offset)) + { + AddEvent(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(offset)) + { + AddEvent(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(offset)) + { + AddEvent(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(offset)) + { + AddEvent(new UnknownCommand { Command = cmd, Args = args }); + } + break; + } + default: throw new DSEInvalidCMDException(trackIndex, (int)offset, cmd); + } + } + } + r.Stream.Position = chunkStart + 0xC; + uint chunkLength = r.ReadUInt32(); + r.Stream.Position += chunkLength; + // Align 4 + while (r.Stream.Position % 4 != 0) + { + r.Stream.Position++; + } + } + SetTicks(); + } + } + public void SetCurrentPosition(long ticks) + { + if (_tracks is null) + { + SongEnded?.Invoke(); + return; + } + + if (State is PlayerState.Playing or PlayerState.Paused or PlayerState.Stopped) + { + if (State == PlayerState.Playing) + { + Pause(); + } + InitEmulation(); + while (true) + { + if (ElapsedTicks == ticks) + { + goto finish; + } + + 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(); + return; + } + if (State is PlayerState.Playing or PlayerState.Paused or PlayerState.Stopped) + { + Stop(); + InitEmulation(); + State = PlayerState.Playing; + CreateThread(); + } + } + public void Pause() + { + if (State == PlayerState.Playing) + { + State = PlayerState.Paused; + WaitThread(); + } + else if (State is PlayerState.Paused or PlayerState.Stopped) + { + State = PlayerState.Playing; + CreateThread(); + } + } + public void Stop() + { + if (State is PlayerState.Playing or 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 is PlayerState.Playing or PlayerState.Paused or PlayerState.Stopped) + { + State = PlayerState.ShutDown; + WaitThread(); + } + } + public void UpdateSongState(SongState info) + { + info.Tempo = _tempo; + for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) + { + Track track = _tracks[trackIndex]; + SongState.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 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; + } + Channel? channel = _mixer.AllocateChannel(); + if (channel is null) + { + throw new Exception("Not enough channels"); + } + + channel.Stop(); + track.Octave = (byte)(track.Octave + oct); + if (channel.StartPCM(_localSWD, _masterSWD, track.Voice, n + (12 * track.Octave), duration)) + { + channel.NoteVelocity = cmd; + channel.Owner = track; + track.Channels.Add(channel); + } + } + 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 DSEInvalidCMDException(track.Index, track.CurOffset - 1, cmd); + } + } + } + + private void Tick() + { + _time.Start(); + while (true) + { + PlayerState state = State; + bool playing = state == PlayerState.Playing; + bool recording = state == PlayerState.Recording; + if (!playing && !recording) + { + break; + } + + 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) + { + // TODO: lock state + _mixer.ChannelTick(); + _mixer.Process(playing, recording); + _time.Stop(); + State = PlayerState.Stopped; + SongEnded?.Invoke(); + return; + } + } + _tempoStack += _tempo; + _mixer.ChannelTick(); + _mixer.Process(playing, recording); + if (playing) + { + _time.Wait(); + } + } + _time.Stop(); + } +} diff --git a/VG Music Studio/Core/NDS/DSE/Enums.cs b/VG Music Studio - Core/NDS/DSE/Enums.cs similarity index 100% rename from VG Music Studio/Core/NDS/DSE/Enums.cs rename to VG Music Studio - Core/NDS/DSE/Enums.cs 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 0000000..5383c4d --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/SMD.cs @@ -0,0 +1,61 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE +{ + internal sealed class SMD + { + public sealed 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 sealed 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 sealed 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 new file mode 100644 index 0000000..17c08ef --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/SWD.cs @@ -0,0 +1,477 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +internal sealed 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 stream = new MemoryStream(File.ReadAllBytes(path))) + { + var r = new EndianBinaryReader(stream, ascii: true); + Type = r.ReadString_Count(4); + Unknown = new byte[4]; + r.ReadBytes(Unknown); + Length = r.ReadUInt32(); + Version = r.ReadUInt16(); + 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: + { + r.Stream.Position += 0x8; + uint length = r.ReadUInt32(); + r.Stream.Position += length; + // Align 4 + while (r.Stream.Position % 4 != 0) + { + r.Stream.Position++; + } + 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(); + } + else + { + 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/DSE/Track.cs b/VG Music Studio - Core/NDS/DSE/Track.cs new file mode 100644 index 0000000..ba0a7c6 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/Track.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +internal sealed class Track +{ + 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 Track(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++) + { + 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 similarity index 100% rename from VG Music Studio/Core/NDS/DSE/Utils.cs rename to VG Music Studio - Core/NDS/DSE/Utils.cs diff --git a/VG Music Studio - Core/NDS/SDAT/Channel.cs b/VG Music Studio - Core/NDS/SDAT/Channel.cs new file mode 100644 index 0000000..742a839 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/Channel.cs @@ -0,0 +1,391 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal sealed class Channel + { + public readonly byte Index; + + public Track? 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; + 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 is not null) + { + 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) + { + 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/Commands.cs b/VG Music Studio - Core/NDS/SDAT/Commands.cs new file mode 100644 index 0000000..e7fe9e1 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/Commands.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/Enums.cs b/VG Music Studio - Core/NDS/SDAT/Enums.cs similarity index 100% rename from VG Music Studio/Core/NDS/SDAT/Enums.cs rename to VG Music Studio - Core/NDS/SDAT/Enums.cs diff --git a/VG Music Studio - Core/NDS/SDAT/FileHeader.cs b/VG Music Studio - Core/NDS/SDAT/FileHeader.cs new file mode 100644 index 0000000..638b4fd --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/FileHeader.cs @@ -0,0 +1,25 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +public sealed class FileHeader +{ + public string FileType; + public ushort FileEndianness; + public ushort Version; + public int FileSize; + public ushort HeaderSize; // 16 + public ushort NumBlocks; + + public FileHeader(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/SBNK.cs b/VG Music Studio - Core/NDS/SDAT/SBNK.cs new file mode 100644 index 0000000..2acd3a4 --- /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 FileHeader 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 FileHeader(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 0000000..85e4ff3 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDAT.cs @@ -0,0 +1,251 @@ +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; } + } + 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; } + } + 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 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 stream = new MemoryStream(bytes)) + { + var er = new EndianBinaryReader(stream, ascii: true); + FileHeader = new FileHeader(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/SDATConfig.cs b/VG Music Studio - Core/NDS/SDAT/SDATConfig.cs new file mode 100644 index 0000000..c35a95a --- /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(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/SDATEngine.cs b/VG Music Studio - Core/NDS/SDAT/SDATEngine.cs new file mode 100644 index 0000000..7611c7f --- /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/SDATExceptions.cs b/VG Music Studio - Core/NDS/SDAT/SDATExceptions.cs new file mode 100644 index 0000000..e79c0d4 --- /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/SDATMixer.cs b/VG Music Studio - Core/NDS/SDAT/SDATMixer.cs new file mode 100644 index 0000000..9cbf17b --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATMixer.cs @@ -0,0 +1,252 @@ +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 Channel[] Channels; + private readonly BufferedWaveProvider _buffer; + + 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 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); + } + + 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 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 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++) + { + Channel 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; + } + + private WaveFileWriter? _waveWriter; + public void CreateWaveWriter(string fileName) + { + _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); + } + public void CloseWaveWriter() + { + _waveWriter!.Dispose(); + _waveWriter = null; + } + internal void EmulateProcess() + { + for (int i = 0; i < _samplesPerBuffer; i++) + { + for (int j = 0; j < 0x10; j++) + { + Channel 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++) + { + Channel 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 0000000..3699cc9 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs @@ -0,0 +1,1694 @@ +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +public sealed class SDATPlayer : IPlayer, ILoadedSong +{ + internal 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 SDATMixer _mixer; + private readonly SDATConfig _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; + internal 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 ILoadedSong LoadedSong => this; + public bool ShouldFadeOut { get; set; } + public long NumLoops { get; set; } + private int _longestTrack; + + public PlayerState State { get; private set; } + public event Action? SongEnded; + + internal SDATPlayer(SDATConfig config, SDATMixer mixer) + { + _config = config; + _mixer = mixer; + + for (byte i = 0; i < 0x10; i++) + { + _tracks[i] = new Track(i, this); + } + + _time = new TimeBarrier(192); + } + private void CreateThread() + { + _thread = new Thread(Tick) { Name = "SDAT Player Tick" }; + _thread.Start(); + } + private void WaitThread() + { + if (_thread is not null && (_thread.ThreadState is ThreadState.Running or 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 + // Should evaluate all branches if possible + 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; + return; + } + + if (oldSeqInfo == null || _seqInfo.Bank != oldSeqInfo.Bank) + { + Array.Clear(_voiceTypeCache); + } + _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(byte 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 SDATInvalidCMDException(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 { Note = 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: + { + byte 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 SDATTooManyNestedCallsException(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 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) + { + Pause(); + } + InitEmulation(); + while (true) + { + if (ElapsedTicks == ticks) + { + goto finish; + } + + 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(); + return; + } + if (State is PlayerState.Playing or PlayerState.Paused or 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 is PlayerState.Playing or PlayerState.Paused or PlayerState.Stopped) + { + State = PlayerState.ShutDown; + WaitThread(); + } + } + private readonly string?[] _voiceTypeCache = new string?[256]; + public void UpdateSongState(SongState info) + { + info.Tempo = _tempo; + for (int i = 0; i < 0x10; i++) + { + Track track = _tracks[i]; + if (!track.Enabled) + { + continue; + } + + SongState.Track tin = info.Tracks[i]; + tin.Position = track.DataOffset; + tin.Rest = track.Rest; + tin.Voice = track.Voice; + tin.LFO = track.LFODepth * track.LFORange; + ref string? cache = ref _voiceTypeCache[track.Voice]; + if (cache is null) + { + if (_sbnk.NumInstruments <= track.Voice) + { + cache = "Empty"; + } + else + { + InstrumentType t = _sbnk.Instruments[track.Voice].Type; + switch (t) + { + case InstrumentType.PCM: cache = "PCM"; break; + case InstrumentType.PSG: cache = "PSG"; break; + case InstrumentType.Noise: cache = "Noise"; break; + case InstrumentType.Drum: cache = "Drum"; break; + case InstrumentType.KeySplit: cache = "Key Split"; break; + default: cache = "Invalid {0}" + (byte)t; break; + } + } + } + tin.Type = cache; + 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.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; + } + } + } + + private void TryStartChannel(SBNK.InstrumentData inst, Track track, byte note, byte velocity, int duration, out Channel? channel) + { + InstrumentType type = inst.Type; + channel = _mixer.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); + } + internal void PlayNote(Track track, byte note, byte velocity, int duration) + { + Channel? 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.PortamentoKey - 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; + } + 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) + { + break; + } + + 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) + { + // TODO: lock state + MixerProcess(); + _time.Stop(); + State = PlayerState.Stopped; + SongEnded?.Invoke(); + return; + } + } + _tempoStack += _tempo; + MixerProcess(); + if (playing) + { + _time.Wait(); + } + } + _time.Stop(); + } +} 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 0000000..675d657 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATUtils.cs @@ -0,0 +1,342 @@ +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +internal static class SDATUtils +{ + 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]; + } + if (index < 0x40) + { + return _sinTable[0x20 - (index - 0x20)]; + } + if (index < 0x60) + { + return -_sinTable[index - 0x40]; + } + // < 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/SDAT/SSEQ.cs b/VG Music Studio - Core/NDS/SDAT/SSEQ.cs new file mode 100644 index 0000000..ec3859c --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SSEQ.cs @@ -0,0 +1,31 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal sealed 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 stream = new MemoryStream(bytes)) + { + var er = new EndianBinaryReader(stream, ascii: true); + FileHeader = new FileHeader(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 0000000..2af20c3 --- /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; // (NDSUtils.ARM7_CLOCK / SampleRate) + 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 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 stream = new MemoryStream(bytes)) + { + var er = new EndianBinaryReader(stream, ascii: true); + FileHeader = new FileHeader(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/NDS/SDAT/Track.cs b/VG Music Studio - Core/NDS/SDAT/Track.cs new file mode 100644 index 0000000..43a1435 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/Track.cs @@ -0,0 +1,196 @@ +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal sealed class Track + { + 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 PortamentoKey; + 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 Track(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; + PortamentoKey = 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++) + { + 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(); + } + } + + 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; + } + } +} diff --git a/VG Music Studio - Core/NDS/Utils.cs b/VG Music Studio - Core/NDS/Utils.cs new file mode 100644 index 0000000..601e832 --- /dev/null +++ b/VG Music Studio - Core/NDS/Utils.cs @@ -0,0 +1,7 @@ +namespace Kermalis.VGMusicStudio.Core.NDS +{ + internal static class Utils + { + 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/Player.cs b/VG Music Studio - Core/Player.cs new file mode 100644 index 0000000..adb8fc8 --- /dev/null +++ b/VG Music Studio - Core/Player.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core; + +public enum PlayerState : byte +{ + Stopped, + Playing, + Paused, + Recording, + ShutDown, +} + +public interface ILoadedSong +{ + List[] Events { get; } + long MaxTicks { get; } + long ElapsedTicks { get; } +} + +public interface IPlayer : IDisposable +{ + ILoadedSong? LoadedSong { get; } + bool ShouldFadeOut { get; set; } + long NumLoops { get; set; } + + PlayerState State { get; } + event Action? SongEnded; + + void LoadSong(long index); + void SetCurrentPosition(long ticks); + void Play(); + void Pause(); + void Stop(); + void Record(string fileName); + void UpdateSongState(SongState info); +} diff --git a/VG Music Studio/Properties/Strings.Designer.cs b/VG Music Studio - Core/Properties/Strings.Designer.cs similarity index 84% rename from VG Music Studio/Properties/Strings.Designer.cs rename to VG Music Studio - Core/Properties/Strings.Designer.cs index 7153b37..8a481f3 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; @@ -22,7 +22,7 @@ namespace Kermalis.VGMusicStudio.Properties { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.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,7 +135,7 @@ 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); } @@ -144,7 +144,7 @@ internal static string ErrorBoolParse { /// /// Looks up a localized string similar to Color {0} has an invalid key.. /// - internal static string ErrorConfigColorInvalidKey { + public static string ErrorConfigColorInvalidKey { get { return ResourceManager.GetString("ErrorConfigColorInvalidKey", resourceCulture); } @@ -153,7 +153,7 @@ internal static string ErrorConfigColorInvalidKey { /// /// 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 +162,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 +171,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 +180,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 +189,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 +198,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 +207,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 +216,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 +225,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 +234,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 +243,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 +252,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 +261,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 +270,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 +279,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 +288,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 +297,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 +306,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 +315,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 +324,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 +333,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 +342,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 +351,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 +360,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 +369,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 +378,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 +387,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 +396,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 +405,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 +414,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 +423,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 +432,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 +441,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 +450,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 +459,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 +468,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 +477,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 +486,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 +495,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 +504,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 +513,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,7 +522,7 @@ 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); } @@ -531,7 +531,7 @@ internal static string MenuSaveWAV { /// /// Looks up a localized string similar to C;C#;D;D#;E;F;F#;G;G#;A;A#;B. /// - internal static string Notes { + public static string Notes { get { return ResourceManager.GetString("Notes", resourceCulture); } @@ -540,7 +540,7 @@ internal static string Notes { /// /// 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 +549,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 +558,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 +567,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 +576,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 +585,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 +594,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 +603,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 +612,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 +621,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 +630,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 +639,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,7 +648,7 @@ 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); } @@ -657,7 +657,7 @@ internal static string PlayPlaylistBody { /// /// 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 +666,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 +675,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 +684,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 +693,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 +702,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 +711,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 +720,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 +729,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 +738,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 100% rename from VG Music Studio/Properties/Strings.es.resx rename to VG Music Studio - Core/Properties/Strings.es.resx diff --git a/VG Music Studio/Properties/Strings.it.resx b/VG Music Studio - Core/Properties/Strings.it.resx similarity index 100% rename from VG Music Studio/Properties/Strings.it.resx rename to VG Music Studio - Core/Properties/Strings.it.resx diff --git a/VG Music Studio/Properties/Strings.resx b/VG Music Studio - Core/Properties/Strings.resx similarity index 100% rename from VG Music Studio/Properties/Strings.resx rename to VG Music Studio - Core/Properties/Strings.resx diff --git a/VG Music Studio - Core/SongEvent.cs b/VG Music Studio - Core/SongEvent.cs new file mode 100644 index 0000000..6d70322 --- /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 0000000..8987c14 --- /dev/null +++ b/VG Music Studio - Core/SongState.cs @@ -0,0 +1,71 @@ +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; + } + } + + 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 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 0000000..e0d7054 --- /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 0000000..ff25770 --- /dev/null +++ b/VG Music Studio - Core/Util/ConfigUtils.cs @@ -0,0 +1,126 @@ +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 readonly string[] _notes = Strings.Notes.Split(';'); + + 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.AsSpan(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 : 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) + { + 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 string CombineWithBaseDirectory(string path) + { + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path); + } + + public static string GetNoteName(int note) + { + return _notes[note]; + } + // TODO: Cache results? + public static string GetKeyName(int note) + { + return _notes[note % 12] + ((note / 12) + (GlobalConfig.Instance.MiddleCOctave - 5)); + } +} diff --git a/VG Music Studio - Core/Util/GlobalConfig.cs b/VG Music Studio - Core/Util/GlobalConfig.cs new file mode 100644 index 0000000..106f805 --- /dev/null +++ b/VG Music Studio - Core/Util/GlobalConfig.cs @@ -0,0 +1,110 @@ +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))); + } + long 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 = ConfigUtils.ParseValue(valueName, v.Value.ToString(), 0, 240); + } + else if (key == "S") + { + s = ConfigUtils.ParseValue(valueName, v.Value.ToString(), 0, 240); + } + else if (key == "L") + { + l = ConfigUtils.ParseValue(valueName, v.Value.ToString(), 0, 240); + } + else + { + throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigColorInvalidKey, i))); + } + } + var co = HSLColor.ToColor((int)h, (byte)s, (byte)l); + 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 0000000..ab371dd --- /dev/null +++ b/VG Music Studio - Core/Util/HSLColor.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.Util; + +public readonly struct HSLColor +{ + public readonly int H; + public readonly byte S; + public readonly byte L; + + public HSLColor(int h, byte s, byte l) + { + H = h; + S = s; + L = l; + } + public HSLColor(in Color c) + { + double modifiedR, modifiedG, modifiedB, min, max, delta, h, s, l; + + modifiedR = c.R / 255.0; + modifiedG = c.G / 255.0; + modifiedB = c.B / 255.0; + + min = new List(3) { modifiedR, modifiedG, modifiedB }.Min(); + max = new List(3) { modifiedR, modifiedG, modifiedB }.Max(); + delta = max - min; + l = (min + max) / 2; + + if (delta == 0) + { + h = 0; + s = 0; + } + else + { + s = (l <= 0.5) ? (delta / (min + max)) : (delta / (2 - max - min)); + + if (modifiedR == max) + { + h = (modifiedG - modifiedB) / 6 / delta; + } + else if (modifiedG == max) + { + h = (1.0 / 3) + ((modifiedB - modifiedR) / 6 / delta); + } + else + { + h = (2.0 / 3) + ((modifiedR - modifiedG) / 6 / delta); + } + + h = (h < 0) ? ++h : h; + h = (h > 1) ? --h : h; + } + + H = (int)Math.Round(h * 360); + S = (byte)Math.Round(s * 100); + L = (byte)Math.Round(l * 100); + } + + public Color ToColor() + { + return ToColor(H, S, L); + } + // https://github.com/iamartyom/ColorHelper/blob/master/ColorHelper/Converter/ColorConverter.cs + public static Color ToColor(int h, byte s, byte l) + { + double modifiedH, modifiedS, modifiedL, + r = 1, g = 1, b = 1, + q, p; + + modifiedH = h / 360.0; + modifiedS = s / 100.0; + modifiedL = l / 100.0; + + q = (modifiedL < 0.5) ? modifiedL * (1 + modifiedS) : modifiedL + modifiedS - modifiedL * modifiedS; + p = 2 * modifiedL - q; + + if (modifiedL == 0) // If the lightness value is 0 it will always be black + { + r = 0; + g = 0; + b = 0; + } + else if (modifiedS != 0) + { + r = GetHue(p, q, modifiedH + 1.0 / 3); + g = GetHue(p, q, modifiedH); + b = GetHue(p, q, modifiedH - 1.0 / 3); + } + + return Color.FromArgb(255, (byte)Math.Round(r * 255), (byte)Math.Round(g * 255), (byte)Math.Round(b * 255)); + } + private static double GetHue(double p, double q, double t) + { + double value = p; + + if (t < 0) + { + t++; + } + else if (t > 1) + { + t--; + } + + if (t < 1.0 / 6) + { + value = p + (q - p) * 6 * t; + } + else if (t < 1.0 / 2) + { + value = q; + } + else if (t < 2.0 / 3) + { + value = p + (q - p) * (2.0 / 3 - t) * 6; + } + + return value; + } + + public override bool Equals(object? obj) + { + if (obj is HSLColor other) + { + return H == other.H && S == other.S && L == other.L; + } + return false; + } + public override int GetHashCode() + { + return HashCode.Combine(H, S, L); + } + + public override string ToString() + { + return $"{H}° {S}% {L}%"; + } + + 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/SampleUtils.cs b/VG Music Studio - Core/Util/SampleUtils.cs new file mode 100644 index 0000000..057499e --- /dev/null +++ b/VG Music Studio - Core/Util/SampleUtils.cs @@ -0,0 +1,19 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.Util +{ + internal static class SampleUtils + { + // TODO: Span output? + public static short[] PCMU8ToPCM16(ReadOnlySpan data) + { + short[] ret = new short[data.Length]; + for (int i = 0; i < data.Length; i++) + { + byte b = data[i]; + ret[i] = (short)((b - 0x80) << 8); + } + return ret; + } + } +} diff --git a/VG Music Studio - Core/Util/TimeBarrier.cs b/VG Music Studio - Core/Util/TimeBarrier.cs new file mode 100644 index 0000000..fab0ae5 --- /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 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 * 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 0000000..e5af0e5 --- /dev/null +++ b/VG Music Studio - Core/VG Music Studio - Core.csproj @@ -0,0 +1,47 @@ + + + + net6.0 + Library + latest + Kermalis.VGMusicStudio.Core + enable + true + CA1069 + + + + + + + + + + + + + + + Dependencies\DLS2.dll + + + Dependencies\SoundFont2.dll + + + + + + Always + + + Always + + + Always + + + Always + + + + diff --git a/VG Music Studio - MIDI/Chunks/MIDIChunk.cs b/VG Music Studio - MIDI/Chunks/MIDIChunk.cs new file mode 100644 index 0000000..175d966 --- /dev/null +++ b/VG Music Studio - MIDI/Chunks/MIDIChunk.cs @@ -0,0 +1,22 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.VGMusicStudio.MIDI; + +public abstract class MIDIChunk +{ + protected static long GetEndOffset(EndianBinaryReader r, uint size) + { + return r.Stream.Position + size; + } + protected static void EatRemainingBytes(EndianBinaryReader r, long endOffset, string chunkName, uint size) + { + if (r.Stream.Position > endOffset) + { + throw new InvalidDataException($"Chunk was too short ({chunkName} = {size})"); + } + r.Stream.Position = endOffset; + } + + internal abstract void Write(EndianBinaryWriter w); +} diff --git a/VG Music Studio - MIDI/Chunks/MIDIHeaderChunk.cs b/VG Music Studio - MIDI/Chunks/MIDIHeaderChunk.cs new file mode 100644 index 0000000..846fdcd --- /dev/null +++ b/VG Music Studio - MIDI/Chunks/MIDIHeaderChunk.cs @@ -0,0 +1,79 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Diagnostics; +using System.IO; + +namespace Kermalis.VGMusicStudio.MIDI; + +// Section 2.1 +public sealed class MIDIHeaderChunk : MIDIChunk +{ + internal const string EXPECTED_NAME = "MThd"; + + public MIDIFormat Format { get; } + public ushort NumTracks { get; internal set; } + public TimeDivisionValue TimeDivision { get; } + + internal MIDIHeaderChunk(MIDIFormat format, TimeDivisionValue timeDivision) + { + if (format > MIDIFormat.Format2) + { + throw new ArgumentOutOfRangeException(nameof(format), format, null); + } + if (!timeDivision.Validate()) + { + throw new ArgumentOutOfRangeException(nameof(timeDivision), timeDivision, null); + } + + Format = format; + TimeDivision = timeDivision; + } + internal MIDIHeaderChunk(uint size, EndianBinaryReader r) + { + if (size < 6) + { + throw new InvalidDataException($"Invalid MIDI header length ({size})"); + } + + long endOffset = GetEndOffset(r, size); + + Format = r.ReadEnum(); + NumTracks = r.ReadUInt16(); + TimeDivision = new TimeDivisionValue(r.ReadUInt16()); + + if (Format > MIDIFormat.Format2) + { + // Section 2.2 states that unknown formats should be supported + Debug.WriteLine($"Unknown MIDI format ({Format}), so behavior is unknown"); + } + if (NumTracks == 0) + { + throw new InvalidDataException("MIDI has no tracks"); + } + if (Format == MIDIFormat.Format0 && NumTracks != 1) + { + throw new InvalidDataException($"MIDI format 0 must have 1 track, but this MIDI has {NumTracks}"); + } + if (!TimeDivision.Validate()) + { + throw new InvalidDataException($"Invalid MIDI time division ({TimeDivision})"); + } + + if (size > 6) + { + // Section 2.2 states that the length should be honored + Debug.WriteLine($"MIDI Header was longer than 6 bytes ({size}), so the extra data is being ignored"); + EatRemainingBytes(r, endOffset, EXPECTED_NAME, size); + } + } + + internal override void Write(EndianBinaryWriter w) + { + w.WriteChars_Count(EXPECTED_NAME, 4); + w.WriteUInt32(6); + + w.WriteEnum(Format); + w.WriteUInt16(NumTracks); + w.WriteUInt16(TimeDivision.RawValue); + } +} diff --git a/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs b/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs new file mode 100644 index 0000000..8e21ee2 --- /dev/null +++ b/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs @@ -0,0 +1,246 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.MIDI; + +public sealed class MIDITrackChunk : MIDIChunk +{ + internal const string EXPECTED_NAME = "MTrk"; + + public MIDIEvent? First { get; private set; } + public MIDIEvent? Last { get; private set; } + + /// Includes the end of track event + public int NumEvents { get; private set; } + public int NumTicks => Last is null ? 0 : Last.Ticks; + + internal MIDITrackChunk() + { + // + } + internal MIDITrackChunk(uint size, EndianBinaryReader r) + { + long endOffset = GetEndOffset(r, size); + + int ticks = 0; + byte runningStatus = 0; + bool foundEnd = false; + bool sysexContinue = false; + while (r.Stream.Position < endOffset) + { + if (foundEnd) + { + throw new InvalidDataException("Events found after the EndOfTrack MetaMessage"); + } + + ticks += MIDIFile.ReadVariableLength(r); + + // Get command + byte cmd = r.ReadByte(); + if (sysexContinue && cmd != 0xF7) + { + throw new InvalidDataException($"SysExContinuationMessage was missing at 0x{r.Stream.Position - 1:X}"); + } + if (cmd < 0x80) + { + cmd = runningStatus; + r.Stream.Position--; + } + + // Check which message it is + if (cmd >= 0x80 && cmd <= 0xEF) + { + runningStatus = cmd; + byte channel = (byte)(cmd & 0xF); + switch (cmd & ~0xF) + { + case 0x80: Insert(ticks, new NoteOnMessage(r, channel)); break; + case 0x90: Insert(ticks, new NoteOffMessage(r, channel)); break; + case 0xA0: Insert(ticks, new PolyphonicPressureMessage(r, channel)); break; + case 0xB0: Insert(ticks, new ControllerMessage(r, channel)); break; + case 0xC0: Insert(ticks, new ProgramChangeMessage(r, channel)); break; + case 0xD0: Insert(ticks, new ChannelPressureMessage(r, channel)); break; + case 0xE0: Insert(ticks, new PitchBendMessage(r, channel)); break; + } + } + else if (cmd == 0xF0) + { + runningStatus = 0; + var msg = new SysExMessage(r); + if (!msg.IsComplete) + { + sysexContinue = true; + } + } + else if (cmd == 0xF7) + { + runningStatus = 0; + if (sysexContinue) + { + var msg = new SysExContinuationMessage(r); + if (msg.IsFinished) + { + sysexContinue = false; + } + } + else + { + Insert(ticks, new EscapeMessage(r)); + } + } + else if (cmd == 0xFF) + { + var msg = new MetaMessage(r); + if (msg.Type == MetaMessageType.EndOfTrack) + { + foundEnd = true; + } + Insert(ticks, msg); + } + else + { + throw new InvalidDataException($"Unknown MIDI command found at 0x{r.Stream.Position - 1:X} (0x{cmd:X})"); + } + } + + if (!foundEnd) + { + throw new InvalidDataException("Could not find EndOfTrack MetaMessage"); + } + if (r.Stream.Position > endOffset) + { + throw new InvalidDataException("Expected to read a certain amount of events, but the data was read incorrectly..."); + } + } + + public void Insert(int ticks, MIDIMessage msg) + { + if (ticks < 0) + { + throw new ArgumentOutOfRangeException(nameof(ticks), ticks, null); + } + + var e = new MIDIEvent(ticks, msg); + + if (NumEvents == 0) + { + First = e; + Last = e; + } + else if (ticks < First!.Ticks) + { + e.Next = First; + First.Prev = e; + First = e; + } + else if (ticks >= Last!.Ticks) + { + e.Prev = Last; + Last.Next = e; + Last = e; + } + else // Somewhere between + { + MIDIEvent next = First; + + while (next.Ticks <= ticks) + { + next = next.Next!; + } + + MIDIEvent prev = next.Prev!; + + e.Next = next; + e.Prev = prev; + prev.Next = e; + next.Prev = e; + } + + NumEvents++; + } + + internal override void Write(EndianBinaryWriter w) + { + w.WriteChars_Count(EXPECTED_NAME, 4); + + long sizeOffset = w.Stream.Position; + w.WriteUInt32(0); // We will update the size later + + byte runningStatus = 0; + bool foundEnd = false; + bool sysexContinue = false; + for (MIDIEvent? e = First; e is not null; e = e.Next) + { + if (foundEnd) + { + throw new InvalidDataException("Events found after the EndOfTrack MetaMessage"); + } + + MIDIFile.WriteVariableLength(w, e.DeltaTicks); + + MIDIMessage msg = e.Message; + byte cmd = msg.GetCMDByte(); + if (sysexContinue && cmd != 0xF7) + { + throw new InvalidDataException("SysExContinuationMessage was missing"); + } + + if (cmd >= 0x80 && cmd <= 0xEF) + { + if (runningStatus != cmd) + { + runningStatus = cmd; + w.WriteByte(cmd); + } + } + else if (cmd == 0xF0) + { + runningStatus = 0; + var sysex = (SysExMessage)msg; + if (!sysex.IsComplete) + { + sysexContinue = true; + } + w.WriteByte(0xF0); + } + else if (cmd == 0xF7) + { + runningStatus = 0; + if (sysexContinue) + { + var sysex = (SysExContinuationMessage)msg; + if (sysex.IsFinished) + { + sysexContinue = false; + } + } + w.WriteByte(0xF0); + } + else if (cmd == 0xFF) + { + var meta = (MetaMessage)msg; + if (meta.Type == MetaMessageType.EndOfTrack) + { + foundEnd = true; + } + w.WriteByte(0xFF); + } + else + { + throw new InvalidDataException($"Unknown MIDI command 0x{cmd:X}"); + } + + msg.Write(w); + } + if (!foundEnd) + { + throw new InvalidDataException("You must insert an EndOfTrack MetaMessage"); + } + + // Update size now + uint size = (uint)(w.Stream.Position - sizeOffset + 4); + w.Stream.Position = sizeOffset; + w.WriteUInt32(size); + } +} diff --git a/VG Music Studio - MIDI/Chunks/MIDIUnsupportedChunk.cs b/VG Music Studio - MIDI/Chunks/MIDIUnsupportedChunk.cs new file mode 100644 index 0000000..942411e --- /dev/null +++ b/VG Music Studio - MIDI/Chunks/MIDIUnsupportedChunk.cs @@ -0,0 +1,30 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.VGMusicStudio.MIDI; + +public sealed class MIDIUnsupportedChunk : MIDIChunk +{ + /// Length 4 + public string ChunkName { get; } + public byte[] Data { get; } + + public MIDIUnsupportedChunk(string chunkName, byte[] data) + { + ChunkName = chunkName; + Data = data; + } + internal MIDIUnsupportedChunk(string chunkName, uint size, EndianBinaryReader r) + { + ChunkName = chunkName; + Data = new byte[size]; + r.ReadBytes(Data); + } + + internal override void Write(EndianBinaryWriter w) + { + w.WriteChars_Count(ChunkName, 4); + w.WriteUInt32((uint)Data.Length); + + w.WriteBytes(Data); + } +} diff --git a/VG Music Studio - MIDI/Events/ChannelPressureMessage.cs b/VG Music Studio - MIDI/Events/ChannelPressureMessage.cs new file mode 100644 index 0000000..f3836fc --- /dev/null +++ b/VG Music Studio - MIDI/Events/ChannelPressureMessage.cs @@ -0,0 +1,48 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.MIDI; + +public sealed class ChannelPressureMessage : MIDIMessage +{ + public byte Channel { get; } + + public byte Pressure { get; } + + internal ChannelPressureMessage(EndianBinaryReader r, byte channel) + { + Channel = channel; + + Pressure = r.ReadByte(); + if (Pressure > 127) + { + throw new InvalidDataException($"Invalid {nameof(ChannelPressureMessage)} pressure at 0x{r.Stream.Position - 1:X} ({Pressure})"); + } + } + + public ChannelPressureMessage(byte channel, byte pressure) + { + if (channel > 15) + { + throw new ArgumentOutOfRangeException(nameof(channel), channel, null); + } + if (pressure > 127) + { + throw new ArgumentOutOfRangeException(nameof(pressure), pressure, null); + } + + Channel = channel; + Pressure = pressure; + } + + internal override byte GetCMDByte() + { + return (byte)(0xD0 + Channel); + } + + internal override void Write(EndianBinaryWriter w) + { + w.WriteByte(Pressure); + } +} diff --git a/VG Music Studio - MIDI/Events/ControllerMessage.cs b/VG Music Studio - MIDI/Events/ControllerMessage.cs new file mode 100644 index 0000000..b0e3c6f --- /dev/null +++ b/VG Music Studio - MIDI/Events/ControllerMessage.cs @@ -0,0 +1,141 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.MIDI; + +public sealed class ControllerMessage : MIDIMessage +{ + public byte Channel { get; } + + public ControllerType Controller { get; } + public byte Value { get; } + + internal ControllerMessage(EndianBinaryReader r, byte channel) + { + Channel = channel; + + Controller = r.ReadEnum(); + if (Controller >= ControllerType.MAX) + { + throw new InvalidDataException($"Invalid {nameof(ControllerMessage)} controller at 0x{r.Stream.Position - 1:X} ({Controller})"); + } + + Value = r.ReadByte(); + if (Value > 127) + { + throw new InvalidDataException($"Invalid {nameof(ControllerMessage)} value at 0x{r.Stream.Position - 1:X} ({Value})"); + } + } + + public ControllerMessage(byte channel, ControllerType controller, byte value) + { + if (channel > 15) + { + throw new ArgumentOutOfRangeException(nameof(channel), channel, null); + } + if (controller >= ControllerType.MAX) + { + throw new ArgumentOutOfRangeException(nameof(controller), controller, null); + } + if (value > 127) + { + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + + Channel = channel; + Controller = controller; + Value = value; + } + + internal override byte GetCMDByte() + { + return (byte)(0xB0 + Channel); + } + + internal override void Write(EndianBinaryWriter w) + { + w.WriteEnum(Controller); + w.WriteByte(Value); + } +} + +public enum ControllerType : byte +{ + // MSB + BankSelect, + ModulationWheel, + BreathController, + FootController = 4, + PortamentoTime, + DataEntry, + ChannelVolume, + Balance, + Pan = 10, + ExpressionController, + EffectControl1, + EffectControl2, + GeneralPurposeController1 = 16, + GeneralPurposeController2, + GeneralPurposeController3, + GeneralPurposeController4, + // LSB + BankSelectLSB = 32, + ModulationWheelLSB, + BreathControllerLSB, + FootControllerLSB = 36, + PortamentoTimeLSB, + DataEntryLSB, + ChannelVolumeLSB, + BalanceLSB, + PanLSB = 42, + ExpressionControllerLSB, + EffectControl1LSB, + EffectControl2LSB, + GeneralPurposeController1LSB = 48, + GeneralPurposeController2LSB, + GeneralPurposeController3LSB, + GeneralPurposeController4LSB, + SustainToggle = 64, + PortamentoToggle, + SostenutoToggle, + SoftPedalToggle, + LegatoToggle, + Hold2Toggle, + SoundController1, + SoundController2, + SoundController3, + SoundController4, + SoundController5, + SoundController6, + SoundController7, + SoundController8, + SoundController9, + SoundController10, + GeneralPurposeController5, + GeneralPurposeController6, + GeneralPurposeController7, + GeneralPurposeController8, + PortamentoControl, + HighResolutionVelocityPrefix = 88, + Effects1Depth = 91, + Effects2Depth, + Effects3Depth, + Effects4Depth, + Effects5Depth, + DataIncrement, + DataDecrement, + NonRegisteredParameterNumberLSB, + NonRegisteredParameterNumberMSB, + RegisteredParameterNumberLSB, + RegisteredParameterNumberMSB, + AllSoundOff = 120, + ResetAllControllers, + LocalControlToggle, + AllNotesOff, + OmniModeOff, + OmniModeOn, + MonoModeOn, + PolyModeOn, + MAX, +} diff --git a/VG Music Studio - MIDI/Events/EscapeMessage.cs b/VG Music Studio - MIDI/Events/EscapeMessage.cs new file mode 100644 index 0000000..0913029 --- /dev/null +++ b/VG Music Studio - MIDI/Events/EscapeMessage.cs @@ -0,0 +1,39 @@ +using Kermalis.EndianBinaryIO; +using System; + +namespace Kermalis.VGMusicStudio.MIDI; + +public sealed class EscapeMessage : MIDIMessage +{ + public byte[] Data { get; } + + internal EscapeMessage(EndianBinaryReader r) + { + int len = MIDIFile.ReadVariableLength(r); + if (len == 0) + { + Data = Array.Empty(); + } + else + { + Data = new byte[len]; + r.ReadBytes(Data); + } + } + + public EscapeMessage(byte[] data) + { + Data = data; + } + + internal override byte GetCMDByte() + { + return 0xF7; + } + + internal override void Write(EndianBinaryWriter w) + { + MIDIFile.WriteVariableLength(w, Data.Length); + w.WriteBytes(Data); + } +} diff --git a/VG Music Studio - MIDI/Events/MIDIEvent.cs b/VG Music Studio - MIDI/Events/MIDIEvent.cs new file mode 100644 index 0000000..0886750 --- /dev/null +++ b/VG Music Studio - MIDI/Events/MIDIEvent.cs @@ -0,0 +1,18 @@ +namespace Kermalis.VGMusicStudio.MIDI; + +public sealed class MIDIEvent +{ + public int Ticks { get; internal set; } + public int DeltaTicks => Prev is null ? Ticks : Ticks - Prev.Ticks; + + public MIDIMessage Message { get; set; } + + public MIDIEvent? Prev { get; internal set; } + public MIDIEvent? Next { get; internal set; } + + internal MIDIEvent(int ticks, MIDIMessage msg) + { + Ticks = ticks; + Message = msg; + } +} \ No newline at end of file diff --git a/VG Music Studio - MIDI/Events/MIDIMessage.cs b/VG Music Studio - MIDI/Events/MIDIMessage.cs new file mode 100644 index 0000000..2358769 --- /dev/null +++ b/VG Music Studio - MIDI/Events/MIDIMessage.cs @@ -0,0 +1,10 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.VGMusicStudio.MIDI; + +public abstract class MIDIMessage +{ + internal abstract byte GetCMDByte(); + + internal abstract void Write(EndianBinaryWriter w); +} diff --git a/VG Music Studio - MIDI/Events/MetaMessage.cs b/VG Music Studio - MIDI/Events/MetaMessage.cs new file mode 100644 index 0000000..1c17f0d --- /dev/null +++ b/VG Music Studio - MIDI/Events/MetaMessage.cs @@ -0,0 +1,122 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.MIDI; + +public sealed class MetaMessage : MIDIMessage +{ + public MetaMessageType Type { get; } + public byte[] Data { get; } + + internal MetaMessage(EndianBinaryReader r) + { + Type = r.ReadEnum(); + if (Type >= MetaMessageType.MAX) + { + throw new InvalidDataException($"Invalid {nameof(MetaMessage)} type at 0x{r.Stream.Position - 1:X} ({Type})"); + } + + int len = MIDIFile.ReadVariableLength(r); + if (len == 0) + { + Data = Array.Empty(); + } + else + { + Data = new byte[len]; + r.ReadBytes(Data); + } + } + + public MetaMessage(MetaMessageType type, byte[] data) + { + if (type >= MetaMessageType.MAX) + { + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + + Type = type; + Data = data; + } + + public static MetaMessage CreateTempoMessage(int tempo) + { + if (tempo <= 0) + { + throw new ArgumentOutOfRangeException(nameof(tempo), tempo, null); + } + + tempo = 60_000_000 / tempo; + byte[] data = new byte[3]; + for (int i = 0; i < 3; i++) + { + data[2 - i] = (byte)(tempo >> (i * 8)); + } + return new MetaMessage(MetaMessageType.Tempo, data); + } + + public static MetaMessage CreateTimeSignatureMessage(byte numerator, byte denominator, byte clocksPerMetronomeClick = 24, byte num32ndNotesPerQuarterNote = 8) + { + if (numerator == 0) + { + throw new ArgumentOutOfRangeException(nameof(numerator), numerator, null); + } + if (denominator < 2 || denominator > 32) + { + throw new ArgumentOutOfRangeException(nameof(denominator), denominator, null); + } + if ((denominator & (denominator - 1)) != 0) + { + throw new ArgumentException("Denominator must be a power of 2", nameof(denominator)); + } + if (clocksPerMetronomeClick == 0) + { + throw new ArgumentOutOfRangeException(nameof(clocksPerMetronomeClick), clocksPerMetronomeClick, null); + } + if (num32ndNotesPerQuarterNote == 0) + { + throw new ArgumentOutOfRangeException(nameof(num32ndNotesPerQuarterNote), num32ndNotesPerQuarterNote, null); + } + + byte[] data = new byte[4]; + data[0] = numerator; + data[1] = (byte)Math.Log(denominator, 2); + data[2] = clocksPerMetronomeClick; + data[3] = num32ndNotesPerQuarterNote; + return new MetaMessage(MetaMessageType.TimeSignature, data); + } + + internal override byte GetCMDByte() + { + return 0xFF; + } + + internal override void Write(EndianBinaryWriter w) + { + w.WriteEnum(Type); + MIDIFile.WriteVariableLength(w, Data.Length); + w.WriteBytes(Data); + } +} + +public enum MetaMessageType : byte +{ + SequenceNumber, + Text, + Copyright, + TrackName, + InstrumentName, + Lyric, + Marker, + CuePoint, + ProgramName, + DeviceName, + EndOfTrack = 0x2F, + Tempo = 0x51, + SMPTEOffset = 0x54, + TimeSignature = 0x58, + KeySignature, + ProprietaryEvent = 0x7F, + MAX, +} \ No newline at end of file diff --git a/VG Music Studio - MIDI/Events/NoteOffMessage.cs b/VG Music Studio - MIDI/Events/NoteOffMessage.cs new file mode 100644 index 0000000..15adc01 --- /dev/null +++ b/VG Music Studio - MIDI/Events/NoteOffMessage.cs @@ -0,0 +1,61 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.MIDI; + +public sealed class NoteOffMessage : MIDIMessage +{ + public byte Channel { get; } + + public MIDINote Note { get; } + public byte Velocity { get; } + + internal NoteOffMessage(EndianBinaryReader r, byte channel) + { + Channel = channel; + + Note = r.ReadEnum(); + if (Note >= MIDINote.MAX) + { + throw new InvalidDataException($"Invalid {nameof(NoteOffMessage)} note at 0x{r.Stream.Position - 1:X} ({Note})"); + } + + Velocity = r.ReadByte(); + if (Velocity > 127) + { + throw new InvalidDataException($"Invalid {nameof(NoteOffMessage)} velocity at 0x{r.Stream.Position - 1:X} ({Velocity})"); + } + } + + public NoteOffMessage(byte channel, MIDINote note, byte velocity) + { + if (channel > 15) + { + throw new ArgumentOutOfRangeException(nameof(channel), channel, null); + } + if (note >= MIDINote.MAX) + { + throw new ArgumentOutOfRangeException(nameof(note), note, null); + } + if (velocity > 127) + { + throw new ArgumentOutOfRangeException(nameof(velocity), velocity, null); + } + + Channel = channel; + Note = note; + Velocity = velocity; + } + + internal override byte GetCMDByte() + { + return (byte)(0x90 + Channel); + } + + internal override void Write(EndianBinaryWriter w) + { + w.WriteEnum(Note); + w.WriteByte(Velocity); + } +} diff --git a/VG Music Studio - MIDI/Events/NoteOnMessage.cs b/VG Music Studio - MIDI/Events/NoteOnMessage.cs new file mode 100644 index 0000000..629cfed --- /dev/null +++ b/VG Music Studio - MIDI/Events/NoteOnMessage.cs @@ -0,0 +1,61 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.MIDI; + +public sealed class NoteOnMessage : MIDIMessage +{ + public byte Channel { get; } + + public MIDINote Note { get; } + public byte Velocity { get; } + + internal NoteOnMessage(EndianBinaryReader r, byte channel) + { + Channel = channel; + + Note = r.ReadEnum(); + if (Note >= MIDINote.MAX) + { + throw new InvalidDataException($"Invalid {nameof(NoteOnMessage)} note at 0x{r.Stream.Position - 1:X} ({Note})"); + } + + Velocity = r.ReadByte(); + if (Velocity > 127) + { + throw new InvalidDataException($"Invalid {nameof(NoteOnMessage)} velocity at 0x{r.Stream.Position - 1:X} ({Velocity})"); + } + } + + public NoteOnMessage(byte channel, MIDINote note, byte velocity) + { + if (channel > 15) + { + throw new ArgumentOutOfRangeException(nameof(channel), channel, null); + } + if (note >= MIDINote.MAX) + { + throw new ArgumentOutOfRangeException(nameof(note), note, null); + } + if (velocity > 127) + { + throw new ArgumentOutOfRangeException(nameof(velocity), velocity, null); + } + + Channel = channel; + Note = note; + Velocity = velocity; + } + + internal override byte GetCMDByte() + { + return (byte)(0x80 + Channel); + } + + internal override void Write(EndianBinaryWriter w) + { + w.WriteEnum(Note); + w.WriteByte(Velocity); + } +} diff --git a/VG Music Studio - MIDI/Events/PitchBendMessage.cs b/VG Music Studio - MIDI/Events/PitchBendMessage.cs new file mode 100644 index 0000000..0509578 --- /dev/null +++ b/VG Music Studio - MIDI/Events/PitchBendMessage.cs @@ -0,0 +1,61 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.MIDI; + +public sealed class PitchBendMessage : MIDIMessage +{ + public byte Channel { get; } + + public byte LSB { get; } + public byte MSB { get; } + + internal PitchBendMessage(EndianBinaryReader r, byte channel) + { + Channel = channel; + + LSB = r.ReadByte(); + if (LSB > 127) + { + throw new InvalidDataException($"Invalid {nameof(PitchBendMessage)} LSB value at 0x{r.Stream.Position - 1:X} ({LSB})"); + } + + MSB = r.ReadByte(); + if (MSB > 127) + { + throw new InvalidDataException($"Invalid {nameof(PitchBendMessage)} MSB value at 0x{r.Stream.Position - 1:X} ({MSB})"); + } + } + + public PitchBendMessage(byte channel, byte lsb, byte msb) + { + if (channel > 15) + { + throw new ArgumentOutOfRangeException(nameof(channel), channel, null); + } + if (lsb > 127) + { + throw new ArgumentOutOfRangeException(nameof(lsb), lsb, null); + } + if (msb > 127) + { + throw new ArgumentOutOfRangeException(nameof(msb), msb, null); + } + + Channel = channel; + LSB = lsb; + MSB = msb; + } + + internal override byte GetCMDByte() + { + return (byte)(0xE0 + Channel); + } + + internal override void Write(EndianBinaryWriter w) + { + w.WriteByte(LSB); + w.WriteByte(MSB); + } +} diff --git a/VG Music Studio - MIDI/Events/PolyphonicPressureMessage.cs b/VG Music Studio - MIDI/Events/PolyphonicPressureMessage.cs new file mode 100644 index 0000000..ead1ecc --- /dev/null +++ b/VG Music Studio - MIDI/Events/PolyphonicPressureMessage.cs @@ -0,0 +1,53 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.MIDI; + +public sealed class PolyphonicPressureMessage : MIDIMessage +{ + public byte Channel { get; } + + public MIDINote Note { get; } + public byte Pressure { get; } + + internal PolyphonicPressureMessage(EndianBinaryReader r, byte channel) + { + Channel = channel; + + Note = r.ReadEnum(); + + Pressure = r.ReadByte(); + if (Pressure > 127) + { + throw new InvalidDataException($"Invalid PolyphonicPressureMessage pressure at 0x{r.Stream.Position - 1:X} ({Pressure})"); + } + } + + public PolyphonicPressureMessage(byte channel, MIDINote note, byte pressure) + { + if (channel > 15) + { + throw new ArgumentOutOfRangeException(nameof(channel), channel, null); + } + if (pressure > 127) + { + throw new ArgumentOutOfRangeException(nameof(pressure), pressure, null); + } + + Channel = channel; + Note = note; + Pressure = pressure; + } + + internal override byte GetCMDByte() + { + return (byte)(0xA0 + Channel); + } + + internal override void Write(EndianBinaryWriter w) + { + w.WriteEnum(Note); + w.WriteByte(Pressure); + } +} diff --git a/VG Music Studio - MIDI/Events/ProgramChangeMessage.cs b/VG Music Studio - MIDI/Events/ProgramChangeMessage.cs new file mode 100644 index 0000000..e8e8829 --- /dev/null +++ b/VG Music Studio - MIDI/Events/ProgramChangeMessage.cs @@ -0,0 +1,181 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.MIDI; + +public sealed class ProgramChangeMessage : MIDIMessage +{ + public byte Channel { get; } + + public MIDIProgram Program { get; } + + internal ProgramChangeMessage(EndianBinaryReader r, byte channel) + { + Channel = channel; + + Program = r.ReadEnum(); + if (Program >= MIDIProgram.MAX) + { + throw new InvalidDataException($"Invalid {nameof(ProgramChangeMessage)} program at 0x{r.Stream.Position - 1:X} ({Program})"); + } + } + + public ProgramChangeMessage(byte channel, MIDIProgram program) + { + if (channel > 15) + { + throw new ArgumentOutOfRangeException(nameof(channel), channel, null); + } + if (program >= MIDIProgram.MAX) + { + throw new ArgumentOutOfRangeException(nameof(program), program, null); + } + + Channel = channel; + Program = program; + } + + internal override byte GetCMDByte() + { + return (byte)(0xC0 + Channel); + } + + internal override void Write(EndianBinaryWriter w) + { + w.WriteEnum(Program); + } +} + +public enum MIDIProgram : byte +{ + AcousticGrandPiano, + BrightAcousticPiano, + ElectricGrandPiano, + HonkyTonkPiano, + ElectricPiano1, + ElectricPiano2, + Harpsichord, + Clavinet, + Celesta, + Glockenspiel, + MusicBox, + Vibraphone, + Marimba, + Xylophone, + TubularBells, + Dulcimer, + DrawbarOrgan, + PercussiveOrgan, + RockOrgan, + ChurchOrgan, + ReedOrgan, + Accordion, + Harmonica, + TangoAccordion, + AcousticGuitarNylon, + AcousticGuitarSteel, + ElectricGuitarJazz, + ElectricGuitarClean, + ElectricGuitarMuted, + OverdrivenGuitar, + DistortionGuitar, + GuitarHarmonics, + AcousticBass, + ElectricBassFinger, + ElectricBassPick, + FretlessBass, + SlapBass1, + SlapBass2, + SynthBass1, + SynthBass2, + Violin, + Viola, + Cello, + Contrabass, + TremoloStrings, + PizzicatoStrings, + OrchestralHarp, + Timpani, + StringEnsemble1, + StringEnsemble2, + SynthStrings1, + SynthStrings2, + ChoirAahs, + VoiceOohs, + SynthVoice, + OrchestraHit, + Trumpet, + Trombone, + Tuba, + MutedTrumpet, + FrenchHorn, + BrassSection, + SynthBrass1, + SynthBrass2, + SopranoSax, + AltoSax, + TenorSax, + BaritoneSax, + Oboe, + EnglishHorn, + Bassoon, + Clarinet, + Piccolo, + Flute, + Recorder, + PanFlute, + BlownBottle, + Shakuhachi, + Whistle, + Ocarina, + Lead1Square, + Lead2Sawtooth, + Lead3Calliope, + Lead4Chiff, + Lead5Charang, + Lead6Voice, + Lead7Fifths, + Lead8BassAndLead, + Pad1NewAge, + Pad2Warm, + Pad3Polysynth, + Pad4Choir, + Pad5Bowed, + Pad6Metallic, + Pad7Halo, + Pad8Sweep, + Fx1Rain, + Fx2Soundtrack, + Fx3Crystal, + Fx4Atmosphere, + Fx5Brightness, + Fx6Goblins, + Fx7Echoes, + Fx8SciFi, + Sitar, + Banjo, + Shamisen, + Koto, + Kalimba, + BagPipe, + Fiddle, + Shanai, + TinkleBell, + Agogo, + SteelDrums, + Woodblock, + TaikoDrum, + MelodicTom, + SynthDrum, + ReverseCymbal, + GuitarFretNoise, + BreathNoise, + Seashore, + BirdTweet, + TelephoneRing, + Helicopter, + Applause, + Gunshot, + MAX, +} \ No newline at end of file diff --git a/VG Music Studio - MIDI/Events/SysExContinuationMessage.cs b/VG Music Studio - MIDI/Events/SysExContinuationMessage.cs new file mode 100644 index 0000000..a55a906 --- /dev/null +++ b/VG Music Studio - MIDI/Events/SysExContinuationMessage.cs @@ -0,0 +1,47 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.MIDI; + +public sealed class SysExContinuationMessage : MIDIMessage +{ + public byte[] Data { get; } + + public bool IsFinished => Data[Data.Length - 1] == 0xF7; + + internal SysExContinuationMessage(EndianBinaryReader r) + { + long offset = r.Stream.Position; + + int len = MIDIFile.ReadVariableLength(r); + if (len == 0) + { + throw new InvalidDataException($"SysEx continuation message at 0x{offset:X} was empty"); + } + + Data = new byte[len]; + r.ReadBytes(Data); + } + + public SysExContinuationMessage(byte[] data) + { + if (data.Length == 0) + { + throw new ArgumentException("SysEx continuation message must not be empty"); + } + + Data = data; + } + + internal override byte GetCMDByte() + { + return 0xF7; + } + + internal override void Write(EndianBinaryWriter w) + { + MIDIFile.WriteVariableLength(w, Data.Length); + w.WriteBytes(Data); + } +} diff --git a/VG Music Studio - MIDI/Events/SysExMessage.cs b/VG Music Studio - MIDI/Events/SysExMessage.cs new file mode 100644 index 0000000..4364c3c --- /dev/null +++ b/VG Music Studio - MIDI/Events/SysExMessage.cs @@ -0,0 +1,47 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.MIDI; + +public sealed class SysExMessage : MIDIMessage +{ + public byte[] Data { get; } + + public bool IsComplete => Data[Data.Length - 1] == 0xF7; + + internal SysExMessage(EndianBinaryReader r) + { + long offset = r.Stream.Position; + + int len = MIDIFile.ReadVariableLength(r); + if (len == 0) + { + throw new InvalidDataException($"SysEx message at 0x{offset:X} was empty"); + } + + Data = new byte[len]; + r.ReadBytes(Data); + } + + public SysExMessage(byte[] data) + { + if (data.Length == 0) + { + throw new ArgumentException("SysEx message must not be empty"); + } + + Data = data; + } + + internal override byte GetCMDByte() + { + return 0xF0; + } + + internal override void Write(EndianBinaryWriter w) + { + MIDIFile.WriteVariableLength(w, Data.Length); + w.WriteBytes(Data); + } +} diff --git a/VG Music Studio - MIDI/MIDIFile.cs b/VG Music Studio - MIDI/MIDIFile.cs new file mode 100644 index 0000000..fb2b858 --- /dev/null +++ b/VG Music Studio - MIDI/MIDIFile.cs @@ -0,0 +1,173 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Kermalis.VGMusicStudio.MIDI; + +// Section 2.1 +public enum MIDIFormat : ushort +{ + /// Contains a single multi-channel track + Format0, + /// Contains one or more simultaneous tracks + Format1, + /// Contains one or more independent single-track patterns + Format2, +} + +// https://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html +// http://www.somascape.org/midi/tech/mfile.html +public sealed class MIDIFile +{ + private readonly List _nonHeaderChunks; // Not really important to expose this at the moment + + public MIDIHeaderChunk HeaderChunk { get; } + + private readonly List _tracks; + + public MIDITrackChunk this[int index] + { + get => _tracks[index]; + set => _tracks[index] = value; + } + + public MIDIFile(MIDIFormat format, TimeDivisionValue timeDivision, int tracksInitialCapacity) + { + if (format == MIDIFormat.Format0 && tracksInitialCapacity != 1) + { + throw new ArgumentException("Format 0 must have 1 track", nameof(tracksInitialCapacity)); + } + + HeaderChunk = new MIDIHeaderChunk(format, timeDivision); + _nonHeaderChunks = new List(tracksInitialCapacity); + _tracks = new List(tracksInitialCapacity); + } + public MIDIFile(Stream stream) + { + var r = new EndianBinaryReader(stream, endianness: Endianness.BigEndian, ascii: true); + string chunkName = r.ReadString_Count(4); + if (chunkName != MIDIHeaderChunk.EXPECTED_NAME) + { + throw new InvalidDataException("MIDI header was not at the start of the file"); + } + + HeaderChunk = (MIDIHeaderChunk)ReadChunk(r, alreadyReadName: chunkName); + _nonHeaderChunks = new List(HeaderChunk.NumTracks); + _tracks = new List(HeaderChunk.NumTracks); + + while (stream.Position < stream.Length) + { + MIDIChunk c = ReadChunk(r); + _nonHeaderChunks.Add(c); + if (c is MIDITrackChunk tc) + { + _tracks.Add(tc); + } + } + + if (_tracks.Count != HeaderChunk.NumTracks) + { + throw new InvalidDataException($"Unexpected track count: (Expected {HeaderChunk.NumTracks} but found {_tracks.Count}"); + } + } + + private static MIDIChunk ReadChunk(EndianBinaryReader r, string? alreadyReadName = null) + { + string chunkName = alreadyReadName ?? r.ReadString_Count(4); + uint chunkSize = r.ReadUInt32(); + switch (chunkName) + { + case MIDIHeaderChunk.EXPECTED_NAME: return new MIDIHeaderChunk(chunkSize, r); + case MIDITrackChunk.EXPECTED_NAME: return new MIDITrackChunk(chunkSize, r); + default: return new MIDIUnsupportedChunk(chunkName, chunkSize, r); + } + } + + internal static int ReadVariableLength(EndianBinaryReader r) + { + int value = r.ReadByte(); + + if ((value & 0x80) != 0) + { + value &= 0x7F; + + byte c; + do + { + c = r.ReadByte(); + value = (value << 7) + (c & 0x7F); + } while ((c & 0x80) != 0); + } + + return value; + } + internal static void WriteVariableLength(EndianBinaryWriter w, int value) + { + int buffer = value & 0x7F; + while ((value >>= 7) > 0) + { + buffer <<= 8; + buffer |= 0x80; + buffer += value & 0x7F; + } + + while (true) + { + w.WriteByte((byte)buffer); + if ((buffer & 0x80) == 0) + { + break; + } + buffer >>= 8; + } + } + internal static int GetVariableLengthNumBytes(int value) + { + int buffer = value & 0x7F; + while ((value >>= 7) > 0) + { + buffer <<= 8; + buffer |= 0x80; + buffer += value & 0x7F; + } + + int numBytes = 0; + while (true) + { + numBytes++; + if ((buffer & 0x80) == 0) + { + break; + } + buffer >>= 8; + } + + return numBytes; + } + + public MIDITrackChunk CreateTrack() + { + var tc = new MIDITrackChunk(); + _nonHeaderChunks.Add(tc); + _tracks.Add(tc); + HeaderChunk.NumTracks++; + + return tc; + } + + public void Save(string fileName) + { + using (FileStream stream = File.Create(fileName)) + { + var w = new EndianBinaryWriter(stream, endianness: Endianness.BigEndian, ascii: true); + + HeaderChunk.Write(w); + + foreach (MIDIChunk c in _nonHeaderChunks) + { + c.Write(w); + } + } + } +} diff --git a/VG Music Studio - MIDI/MIDINote.cs b/VG Music Studio - MIDI/MIDINote.cs new file mode 100644 index 0000000..e7de7bc --- /dev/null +++ b/VG Music Studio - MIDI/MIDINote.cs @@ -0,0 +1,134 @@ +namespace Kermalis.VGMusicStudio.MIDI; + +public enum MIDINote : byte +{ + C_M1, + Db_M1, + D_M1, + Eb_M1, + E_M1, + F_M1, + Gb_M1, + G_M1, + Ab_M1, + A_M1, + Bb_M1, + B_M1, + C_0, + Db_0, + D_0, + Eb_0, + E_0, + F_0, + Gb_0, + G_0, + Ab_0, + A_0, + Bb_0, + B_0, + C_1, + Db_1, + D_1, + Eb_1, + E_1, + F_1, + Gb_1, + G_1, + Ab_1, + A_1, + Bb_1, + B_1, + C_2, + Db_2, + D_2, + Eb_2, + E_2, + F_2, + Gb_2, + G_2, + Ab_2, + A_2, + Bb_2, + B_2, + C_3, + Db_3, + D_3, + Eb_3, + E_3, + F_3, + Gb_3, + G_3, + Ab_3, + A_3, + Bb_3, + B_3, + C_4, + Db_4, + D_4, + Eb_4, + E_4, + F_4, + Gb_4, + G_4, + Ab_4, + A_4, + Bb_4, + B_4, + C_5, + Db_5, + D_5, + Eb_5, + E_5, + F_5, + Gb_5, + G_5, + Ab_5, + A_5, + Bb_5, + B_5, + C_6, + Db_6, + D_6, + Eb_6, + E_6, + F_6, + Gb_6, + G_6, + Ab_6, + A_6, + Bb_6, + B_6, + C_7, + Db_7, + D_7, + Eb_7, + E_7, + F_7, + Gb_7, + G_7, + Ab_7, + A_7, + Bb_7, + B_7, + C_8, + Db_8, + D_8, + Eb_8, + E_8, + F_8, + Gb_8, + G_8, + Ab_8, + A_8, + Bb_8, + B_8, + C_9, + Db_9, + D_9, + Eb_9, + E_9, + F_9, + Gb_9, + G_9, + MAX, +} diff --git a/VG Music Studio - MIDI/TimeDivisionValue.cs b/VG Music Studio - MIDI/TimeDivisionValue.cs new file mode 100644 index 0000000..87f783e --- /dev/null +++ b/VG Music Studio - MIDI/TimeDivisionValue.cs @@ -0,0 +1,63 @@ +namespace Kermalis.VGMusicStudio.MIDI; + +public enum DivisionType : byte +{ + PPQN, + SMPTE, +} + +public enum SMPTEFormat : byte +{ + Smpte24 = 24, + Smpte25 = 25, + Smpte30Drop = 29, + Smpte30 = 30, +} + +// Section 2.1 +public readonly struct TimeDivisionValue +{ + public const int PPQN_MIN_DIVISION = 24; + + public readonly ushort RawValue; + + public DivisionType Type => (DivisionType)(RawValue >> 15); + + public ushort PPQN_TicksPerQuarterNote => RawValue; // Type bit is already 0 + + public SMPTEFormat SMPTE_Format => (SMPTEFormat)(-(sbyte)(RawValue >> 8)); // Upper 8 bits, negated + public byte SMPTE_TicksPerFrame => (byte)RawValue; // Lower 8 bits + + public TimeDivisionValue(ushort rawValue) + { + RawValue = rawValue; + } + + public static TimeDivisionValue CreatePPQN(ushort ticksPerQuarterNote) + { + return new TimeDivisionValue(ticksPerQuarterNote); + } + public static TimeDivisionValue CreateSMPTE(SMPTEFormat format, byte ticksPerFrame) + { + ushort rawValue = (ushort)((-(sbyte)format) << 8); + rawValue |= ticksPerFrame; + + return new TimeDivisionValue(rawValue); + } + + public bool Validate() + { + if (Type == DivisionType.PPQN) + { + return PPQN_TicksPerQuarterNote >= PPQN_MIN_DIVISION; + } + + // SMPTE + return SMPTE_Format is SMPTEFormat.Smpte24 or SMPTEFormat.Smpte25 or SMPTEFormat.Smpte30Drop or SMPTEFormat.Smpte30; + } + + public override string ToString() + { + return string.Format("0x{0:X4}", RawValue); + } +} diff --git a/VG Music Studio - MIDI/VG Music Studio - MIDI.csproj b/VG Music Studio - MIDI/VG Music Studio - MIDI.csproj new file mode 100644 index 0000000..ef1e119 --- /dev/null +++ b/VG Music Studio - MIDI/VG Music Studio - MIDI.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + Library + latest + Kermalis.VGMusicStudio.MIDI + enable + + + + + + + diff --git a/VG Music Studio - WinForms/MainForm.cs b/VG Music Studio - WinForms/MainForm.cs new file mode 100644 index 0000000..eb1b18b --- /dev/null +++ b/VG Music Studio - WinForms/MainForm.cs @@ -0,0 +1,862 @@ +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.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.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 bool _playlistPlaying; + private Config.Playlist _curPlaylist; + private long _curSong = -1; + private readonly List _playedSongs; + private readonly List _remainingSongs; + + private TrackViewer? _trackViewer; + + private bool _stopUI = 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 ThumbnailToolBarButton _prevTButton, _toggleTButton, _nextTButton; + + #endregion + + private MainForm() + { + _playedSongs = new List(); + _remainingSongs = new List(); + + PianoTracks = new bool[SongState.MAX_TRACKS]; + for (int i = 0; i < SongState.MAX_TRACKS; i++) + { + PianoTracks[i] = true; + } + + Mixer.MixerVolumeChanged += SetVolumeBarValue; + + // 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 += (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(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) + { + _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 = ConfigUtils.PROGRAM_NAME; + _songsComboBox.SelectedIndex = 0; + _songInfo.Reset(); + bool success; + try + { + Engine.Instance!.Player.LoadSong(index); + success = Engine.Instance.Player.LoadedSong is not null; // TODO: Make sure loadedsong is null when there are no tracks (for each engine, only mp2k guarantees it rn) + } + 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 is not null) + { + Text = $"{ConfigUtils.PROGRAM_NAME} ― {song.Name}"; // TODO: Make this a func + _songsComboBox.SelectedIndex = songs.IndexOf(song) + 1; // + 1 because the "Music" playlist is first in the combobox + } + _positionBar.Maximum = Engine.Instance!.Player.LoadedSong!.MaxTicks; + _positionBar.LargeChange = _positionBar.Maximum / 10; + _positionBar.SmallChange = _positionBar.LargeChange / 4; + _songInfo.SetNumTracks(Engine.Instance.Player.LoadedSong.Events.Length); + if (_autoplay) + { + Play(); + } + } + else + { + _songInfo.SetNumTracks(0); + } + _positionBar.Enabled = _exportWAVItem.Enabled = success; + _exportMIDIItem.Enabled = success && MP2KEngine.MP2KInstance is not null; + _exportDLSItem.Enabled = _exportSF2Item.Enabled = success && AlphaDreamEngine.AlphaDreamInstance is not null; + + _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) + { + return; + } + + DisposeEngine(); + try + { + _ = new DSEEngine(d.FileName); + } + 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) + { + var d = new CommonOpenFileDialog + { + Title = Strings.MenuOpenAlphaDream, + Filters = { new CommonFileDialogFilter(Strings.FilterOpenGBA, ".gba") } + }; + if (d.ShowDialog() != CommonFileDialogResult.Ok) + { + return; + } + + DisposeEngine(); + try + { + _ = new AlphaDreamEngine(File.ReadAllBytes(d.FileName)); + } + 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) + { + var d = new CommonOpenFileDialog + { + Title = Strings.MenuOpenMP2K, + Filters = { new CommonFileDialogFilter(Strings.FilterOpenGBA, ".gba") } + }; + if (d.ShowDialog() != CommonFileDialogResult.Ok) + { + return; + } + + DisposeEngine(); + try + { + _ = new MP2KEngine(File.ReadAllBytes(d.FileName)); + } + 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) + { + var d = new CommonOpenFileDialog + { + Title = Strings.MenuOpenSDAT, + Filters = { new CommonFileDialogFilter(Strings.FilterOpenSDAT, ".sdat") }, + }; + if (d.ShowDialog() != CommonFileDialogResult.Ok) + { + return; + } + + DisposeEngine(); + try + { + _ = new SDATEngine(new SDAT(File.ReadAllBytes(d.FileName))); + } + 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; + + var d = new CommonSaveFileDialog + { + DefaultFileName = cfg.GetGameName(), + DefaultExtension = ".dls", + EnsureValidNames = true, + Title = Strings.MenuSaveDLS, + Filters = { new CommonFileDialogFilter(Strings.FilterSaveDLS, ".dls") }, + }; + if (d.ShowDialog() != CommonFileDialogResult.Ok) + { + return; + } + + try + { + AlphaDreamSoundFontSaver_DLS.Save(cfg, 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) + { + return; + } + + MP2KPlayer p = MP2KEngine.MP2KInstance!.Player; + var args = new 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) + { + AlphaDreamConfig cfg = AlphaDreamEngine.AlphaDreamInstance!.Config; + + var d = new CommonSaveFileDialog + { + DefaultFileName = cfg.GetGameName(), + DefaultExtension = ".sf2", + EnsureValidNames = true, + Title = Strings.MenuSaveSF2, + Filters = { new CommonFileDialogFilter(Strings.FilterSaveSF2, ".sf2") } + }; + if (d.ShowDialog() != CommonFileDialogResult.Ok) + { + return; + } + + try + { + AlphaDreamSoundFontSaver_SF2.Save(cfg, 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) + { + return; + } + + Stop(); + + IPlayer player = Engine.Instance.Player; + bool oldFade = player.ShouldFadeOut; + long oldLoops = player.NumLoops; + player.ShouldFadeOut = true; + player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; + + try + { + player.Record(d.FileName); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveWAV, d.FileName), Text); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveWAV); + } + + player.ShouldFadeOut = oldFade; + player.NumLoops = oldLoops; + _stopUI = false; + } + + public void LetUIKnowPlayerIsPlaying() + { + if (_timer.Enabled) + { + return; + } + + _pauseButton.Enabled = _stopButton.Enabled = true; + _pauseButton.Text = Strings.PlayerPause; + _timer.Interval = (int)(1_000.0 / 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.Reset(); + _piano.UpdateKeys(_songInfo.Info.Tracks, PianoTracks); + UpdatePositionIndicators(0L); + UpdateTaskbarState(); + UpdateTaskbarButtons(); + } + private void TogglePlayback(object? sender, EventArgs? e) + { + switch (Engine.Instance!.Player.State) + { + case PlayerState.Stopped: Play(); break; + case PlayerState.Paused: + case PlayerState.Playing: Pause(); break; + } + } + 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 is not 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 = ConfigUtils.PROGRAM_NAME; + _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 void UpdateUI(object? sender, EventArgs? e) + { + if (_stopUI) + { + _stopUI = false; + if (_playlistPlaying) + { + _playedSongs.Add(_curSong); + SetAndLoadNextPlaylistSong(); + } + else + { + Stop(); + } + } + else + { + if (WindowState != FormWindowState.Minimized) + { + SongState info = _songInfo.Info; + Engine.Instance!.Player.UpdateSongState(info); + _piano.UpdateKeys(info.Tracks, PianoTracks); + _songInfo.Invalidate(); + } + UpdatePositionIndicators(Engine.Instance!.Player.LoadedSong!.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 static void UpdateTaskbarState() + { + 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); + } + private void UpdateTaskbarButtons() + { + if (!TaskbarManager.IsPlatformSupported) + { + return; + } + + 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 is not 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) + { + 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(null, null); + return true; + } + return base.ProcessCmdKey(ref msg, keyData); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _timer.Dispose(); + } + base.Dispose(disposing); + } +} diff --git a/VG Music Studio - WinForms/PianoControl.cs b/VG Music Studio - WinForms/PianoControl.cs new file mode 100644 index 0000000..e7947a3 --- /dev/null +++ b/VG Music Studio - WinForms/PianoControl.cs @@ -0,0 +1,205 @@ +#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 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 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, + }; + + public sealed class PianoKey : Control + { + public bool PrevPressed; + public bool Pressed; + + public readonly SolidBrush OnBrush; + private readonly SolidBrush _offBrush; + + public PianoKey(byte k) + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + SetStyle(ControlStyles.Selectable, false); + + OnBrush = new(Color.Transparent); + byte l; + if (KeyTypeTable[k % 12] == KeyType.White) + { + if (k / 12 % 2 == 0) + { + l = 240; + } + else + { + l = 120; + } + } + else + { + l = 0; + } + _offBrush = new SolidBrush(HSLColor.ToColor(160, 0, l)); + } + + 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; + 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.PrevPressed = key.Pressed; + key.Pressed = 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.Pressed = true; + } + } + for (int k = 0; k <= 0x7F; k++) + { + PianoKey key = _keys[k]; + if (key.Pressed != key.PrevPressed) + { + 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/Program.cs b/VG Music Studio - WinForms/Program.cs new file mode 100644 index 0000000..c207096 --- /dev/null +++ b/VG Music Studio - WinForms/Program.cs @@ -0,0 +1,30 @@ +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.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 1aa1551..e9c8355 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 0000000..9b2c9f2 --- /dev/null +++ b/VG Music Studio - WinForms/SongInfoControl.cs @@ -0,0 +1,367 @@ +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) * 2f; + if (velocity > 1f) + { + velocity = 1f; + } + _solidBrush.Color = Color.FromArgb((int)WinFormsUtils.Lerp(velocity, 20f, 255f), 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/Theme.cs b/VG Music Studio - WinForms/Theme.cs new file mode 100644 index 0000000..0652802 --- /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.H, (byte)(hsl.S / 2.5), hsl.L); + } +} + +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 0000000..80949f3 --- /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.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) + { + _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 0000000..fa6171e --- /dev/null +++ b/VG Music Studio - WinForms/Util/ColorSlider.cs @@ -0,0 +1,484 @@ +#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 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, EventArgs.Empty); + 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(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) && 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 0000000..ed3a13c --- /dev/null +++ b/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs @@ -0,0 +1,696 @@ +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 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; + } + + 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 == 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 == 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 = 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)) + { + 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 0000000..45bca24 --- /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 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 - WinForms/Util/VGMSDebug.cs b/VG Music Studio - WinForms/Util/VGMSDebug.cs new file mode 100644 index 0000000..1c7bd11 --- /dev/null +++ b/VG Music Studio - WinForms/Util/VGMSDebug.cs @@ -0,0 +1,122 @@ +#if DEBUG +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +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>(); + 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.LoadedSong is null) + { + continue; + } + + foreach (string cmd in Engine.Instance.Player.LoadedSong.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."); + 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 reader = new EndianBinaryReader(stream, ascii: true); + stream.Position = 0xAC; + string gameCode = reader.ReadString_Count(3); + stream.Position = 0xAF; + char regionCode = reader.ReadChar(); + stream.Position = 0xBC; + byte version = reader.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."); + } +} +#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 0000000..0c64c71 --- /dev/null +++ b/VG Music Studio - WinForms/Util/WinFormsUtils.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +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)); + } +} 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 0000000..e960f0d --- /dev/null +++ b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj @@ -0,0 +1,27 @@ + + + + net6.0-windows + WinExe + latest + Kermalis.VGMusicStudio.WinForms + enable + true + true + ..\Build + + Kermalis + Kermalis + VGMusicStudio + VGMusicStudio + VGMusicStudio + 0.3.0 + + + + + + + + + \ 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 0000000..8a69ae5 --- /dev/null +++ b/VG Music Studio - WinForms/ValueTextBox.cs @@ -0,0 +1,104 @@ +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms; + +internal sealed 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 (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 EventHandler _onValueChanged = null; + public event EventHandler ValueChanged + { + add => _onValueChanged += value; + remove => _onValueChanged -= value; + } + private void OnValueChanged(EventArgs e) + { + _onValueChanged?.Invoke(this, e); + } +} diff --git a/VG Music Studio.sln b/VG Music Studio.sln index 660de87..0f1fbff 100644 --- a/VG Music Studio.sln +++ b/VG Music Studio.sln @@ -1,9 +1,13 @@  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 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VG Music Studio - MIDI", "VG Music Studio - MIDI\VG Music Studio - MIDI.csproj", "{6756ED81-71F6-457D-AD23-9C03B6C934E4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +15,18 @@ 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 + {6756ED81-71F6-457D-AD23-9C03B6C934E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6756ED81-71F6-457D-AD23-9C03B6C934E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6756ED81-71F6-457D-AD23-9C03B6C934E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6756ED81-71F6-457D-AD23-9C03B6C934E4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/VG Music Studio/Core/ADPCMDecoder.cs b/VG Music Studio/Core/ADPCMDecoder.cs deleted file mode 100644 index 9c8a4e9..0000000 --- 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 8da670d..0000000 --- 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 81bfbf6..0000000 --- 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 57200db..0000000 --- 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 4eba3ac..0000000 --- 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 50991f7..0000000 --- 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 c168930..0000000 --- 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 7c1a9e1..0000000 --- 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 de98004..0000000 --- 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 d075b8c..0000000 --- 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 222ee3f..0000000 --- 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 8fda0b9..0000000 --- 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 ac3b2bb..0000000 --- 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/MP2K/Channel.cs b/VG Music Studio/Core/GBA/MP2K/Channel.cs deleted file mode 100644 index faa606d..0000000 --- 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 776d013..0000000 --- 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 9a0ea23..0000000 --- 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 f22ac7e..0000000 --- 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 738859f..0000000 --- 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 b070842..0000000 --- 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 2da7d2c..0000000 --- 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/Utils.cs b/VG Music Studio/Core/GBA/MP2K/Utils.cs deleted file mode 100644 index fc10fbb..0000000 --- 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 d585881..0000000 --- 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 4b3b57f..0000000 --- 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 e0a242f..0000000 --- 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 2ff239d..0000000 --- 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 a7d8eaa..0000000 --- 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 25f856c..0000000 --- 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/Mixer.cs b/VG Music Studio/Core/NDS/DSE/Mixer.cs deleted file mode 100644 index 29c8de5..0000000 --- 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 4fa2b81..0000000 --- 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 706cb06..0000000 --- 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 4e9f984..0000000 --- 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 9a330ca..0000000 --- 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/SDAT/Channel.cs b/VG Music Studio/Core/NDS/SDAT/Channel.cs deleted file mode 100644 index 95b8901..0000000 --- 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 7a83fe1..0000000 --- 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 f7f5281..0000000 --- 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/FileHeader.cs b/VG Music Studio/Core/NDS/SDAT/FileHeader.cs deleted file mode 100644 index 50b863e..0000000 --- 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 a42b4e4..0000000 --- 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 2722786..0000000 --- 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 c0b016d..0000000 --- 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 0f4c4c7..0000000 --- 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 3d97b1e..0000000 --- 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 c7a982c..0000000 --- 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 75802b4..0000000 --- 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 289f601..0000000 --- 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 ea2388b..0000000 --- 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 7a6e26f..0000000 --- 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 7e518a3..0000000 --- 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 91bc091..0000000 --- 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 1d3bc0c868d173339396a66bf1f3204055a04c67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36352 zcmeHwcYIW3_V>AWW^N{vLK^9iK?TMHVk#sCF&&~b2_RN7Bm+c3Cd^ELh-k2|sHnJh z#T5kGieg(UtFYF!yY{tVgWY9a%NBKY;r*WTJX1pAzJA`%`^WqFyuX3@-t#@rdD?mU zoja51CvPMh5!vxRc#!B}T=`Wi_}`%)!N~=WCes7)FO7QGnD)}BIZHZ&?w){ualpIG z-R|w~_J`aHeeOVSx4X03-8i$=z0BX?D^5ys9ifw+)kHMSu+hA-Q*I0AwucULCm1C} z>!7iecJ?-0-FWZ9nU3h@j_}joL0?Q~n>lYRW2pA>EmL-Gw(h;?UO~ zqD1IE+e(ywXxM+_ZX#z)xD$M83@;A(R)xS88H6k2sdWI3XKqdT)0o1hn(MB#0DrptYw)s;*{Aq0NL1e3Ye&M

D}<*8v!HSnC0eE@ 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 AZU6uP 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 4e31c6b0b097ebdb81eaa3532dccbaa0a6a92cdf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180224 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 diff --git a/VG Music Studio/Program.cs b/VG Music Studio/Program.cs deleted file mode 100644 index e2e2529..0000000 --- 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 90411a1..0000000 --- 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 875ce0f..0000000 --- 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 3964565..0000000 --- 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 b23df9c..0000000 --- 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 30d29ad..0000000 --- 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 c928af9..0000000 --- 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 761394f..0000000 --- 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 102a4b0..0000000 --- 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 ce91a71..0000000 --- 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 0973e2a..0000000 --- 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 7aa83b9..0000000 --- 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 a83e351..0000000 --- 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 a40a669..0000000 --- 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 6dbda59..0000000 --- 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 85ecb43..0000000 --- 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 c253c0b..0000000 --- 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 a731506..0000000 --- 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 de7ee46..0000000 --- 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 e5dc76adb27526e8e384996ed5b3b9c5944fee16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3404655 zcmcG%3wRVo);HdhbiyPIbkK-Vqr`VdC4xvK3PZvg2!pr~jY1R^M0W*I(M6acQMm+X z2Bx-cjJx2n`@t@(uD4a!RS3vM5)v|isNrf55fIdF2Q>(*At3qxPF43@!bEue{$HLa zJ>6BO&aF zdeTGFa;8lE{ex5Qe>CTJ_doX7?>#xcoscut`&iCHkL3)#byUuyzkgstpDta}ohIs1 zw14&r|`pY6miM0Jjsr zr_O*Yx;9@FT>3|hs%a(>NqRq@}2X1b>x8_aQ-->kzsml+pw7 z@W5Bor}*~Kgo5j;2KlHvUbmEJ{&6_ZWKK7S(x6)oH}y<>*X9X9rVyS3$S2)-+QStM zx%N62VUo1r$ZN*`Z}4By9xh|ZwfT7*?wUgU6!nm#U*mVfWjxKg6iI-){(AnH{dfFS z0&Xci;y31x!`;xIrwpKLPL7acX?wW7L#`drm&3hG;`9aF4fy>>dpOW_3I2v(g8G^Q zxNQ73C4kH8dmS)HQ~rcY)vr*$_&xqVloa_R{;UVzk!*FSGe?zFY0z_$R8xM-APqm$ z?i-YXA0@CAfT5vIZkZw3Nrs{aZ%C4SEjGnJ90nc$&{s&+mS0IY^J;wc*&Af3%6uLK z`Zb=L2R%>N{0tC-z*&v=;~1oNr0V@Fq;KFUP~)ivHOgpfT-AoZ@GDw1o??07#ZFz% zdGUTg_^7>^#q>kS;3bVmP@Mgst-XrM54x0#Rfe<}4oE!xKLVBKE{NpMCjgntZE|4z z3Q1L_4y-iA#1l*TA>aWRQPaBwZ6+6AoE8*KNKWxaqPVHe*c_k7mHaC+?+xT>o+~Aj z|1$Q)DBqFHW%egWke0F`1nGh6g_?p%xUJE5#6kBNMUp<8RC`3$`wp~8%QB=95|f?! zw0Sm3pLGLLUSE1Ep&FuI`sv%ZN-ty*0FCBrHw+GJ_{H1 z0*)a=-*%%-@*Qs0Z!Q3($&^EQ?aL(4BV@~DxE z$S2L2`Tr+AzwL_SD5Xue^xt;HVLVqw{!kxxph7Z8*Eu63eJBw=B9jUI2T09iNOvfo zXS{IyHO{_#KA=h3&b)w~HIRt8X5tSV*#3e9(E>ksx>258h`&l2DYflh=IF-je;^19 zCc0Y%`asQUP?bCvP>EX!8}$2%z{WNNwtLc0fXcRQ$4%c%SjWXMVQoUoJ@<~QgP0*S z`w`O8=Lbvg-S@}AQg}|dOGH+>fxNyWS<5(MH5?mfsBsy|1uN=I1jh?zHrG{EsiiWg z_Pb+L-u5aoW3{hsw^Dr>+v7X9-)9W2JaN%@?=_hCIB;h8*Bxo^3(ry(3|A#R44q$ zS;~L23N~AxT^F&xWko1HXgDsOt|&`1{&HhF!3h4AjivktV)LJf%b!8{{@DDx;_??z z{^HntS8P5RSEib!PLW;5JpCqCsmV~X6SYhIEVTo>{f{Gf>u-Ec)b{F|(oqrK?!G$N z^_ll0UwtNbgZJFa^{G@B*^xv~+@Fq`b0iDe()`Yz^fT6(L6#%o9;$VyekbV?c1jA@ z6=;WjqNe*c$l9E?)2C1Wxb5;yS5)H9^YyEhm9V?Mtd@;M`C63BK}p{y$+@4f(M~%l zu4q@9G^d`(-BV}v54Ad~JSn`%dcN}x_8~;hQD->&s$-q`fi`bOV6TUKStZ%I}-Zpro1n&g_r6cu8y{MrMgov2}v(h34Pr$N*iovAEHv2jq4Rm z=77O;YRmmm+}1?eI!cDAUyG&%Qe5BY6UmIr?qne}h9B4tSd;QzN*Y?5zsA#V9od`bdu|l*?y^9knm#1m9$kqAZ}t?IU#Z<> zS4YY01nZKi*g&-_uIbC-5uDj_^(7lUX5iR4Su;B60Y{u*jyXDM?sVxZ5y<^Z4F5|s7E`S zjJnKr+V<;NeUjikwOefsopL!rP51?E0au36jOs-_9iV#J+S=Bphnmu?_^j+s@+jHrHBPu3bx;njuAe+NNBo^y zE~`CUpLs4b<2UYac{|h@}8`Tm?b<4S%YFm=158pw?_d~-_XOVCQ!$ z0ZF*z(_MecxPk7jpN-3Pbk~nAc_UQ$KU(1m5*@9qC#!>D3_pe1=?%B7&&uL9;0L8Y zfs$Hm&)W{0$_8lG{>jd)ra=!393eGznS|>;sC)ifHfi5j=NNdkNqbTzBbAyip6)~H zB;3omAG7CM=m`n;>F=Qh?34D)a1K*e4s{OOH`+O7xnm;WhHArXdnP-F?jP$Mf?s6g zX24iy-bA>=o=^tyOMbx_qYzh};`@o%9PN_TY?nG!c1@F&r@uy$R&H0vqFeSjv%>vE z5nr8+tI^e@%neZ~Sn+CS7Q1#j`?nOR79T9V0_y zJl>x?AYB6fkhJrC2tP*FGF|G$EGe*8@!tkK^PzI(!=) zuF0tMKo@$bpTW&kv^R(xW>NnSsd}o(g1NV-%HHp8G8aS)s`xJj(aIU9aZgvJ8kIh< zI8E{QM?47ygHN`B>(I0)SEDlbLul4g;61YIZYW?4l42Ej?U@Q(M5Piduza?qJkDP) zi$Bl1XDIL5{v9aKlNGDDrW-hzh}UAYXRL#^lxorl6dHuJ82>^Fx@w-FRF6+XVn2fD zjhpKXjdEiYcz_i6hYKP$e+iVgzc{_A%M{bT-+Uw1zC9h;cS;52m^S`NuXDEX(XjE+ z&isjJVzBXU;Lfa{Xrp&+P<$1gqE%$t_yi=yR#Ab~ADvlM1F3Z4s#4}%N|~mub8XDX zjM%!N{Deq+7!72cMo~M}wUFv=wSZxJgB8Cc6}IC zd%ILQPkmH&O_IxpsS@&fiw41&T7AAL0m}7O(>+}PTv-9G{11XF^={b*XJ%93%~+FY zgnId_Y?lgeNM?k9G#%m;nVH62r(iS4qjaDCK%s4V;Td-PV17q-e15DQX9zGj0AGXf zMaIdKz;0@%U?6Fy5}{!S+SxE|wssRK2Q`w#^@J#Vsyy`}(hya#sikgbSJJ}s5-KT6 z@eiTeIY%WGSW3!|5n~{)F*qk$z|2XLqBq-Pq700zAC$7 zf}Ppdw)1lcy3S|GG%9)kl^F4LnNH&-Jad<7hL`f6&|mbhSXBbYsmYPcHS;G?f6~^) z6h#T_Y?uEcME!MrO$lyQS`6@b_-qZU2pM4F$%2t#u> zY9*h}Ir0oDKji)tSNt5HA7WHMJ;tVG)t zMNbNIvAbkMIR4+0-ox90 z#a~NFi1>CZPh)-o4)j279fKh!7r2)yb8`^7X^-16-{@Yd@c(5_2^qbLyKo%9pYprBy?gkjIYl|{>5@H=&vpQbxg}~=& zqFRd3%x>z)^~v<-*k>T9l{H$lc=I%AZTpDRqB#>Um?2u03V0xzmu8ucA2YvF!cKW{z7#y{6&YC*pl&z+q>@ah0Qv46YWLQ#Y2)7*3o>f9`6rj91 z?nPrO#kTHGVP$+qr8GBP(k2||gHPZ?n?#K*R)@7PcMvkeiBVJj0oxHFv;JgfzB z2TM=EihS+`mKg=P8Ik?Ws{tjS+wTB?%U$XDBX1O@g$FJ}=DXB7^gGDQ@%G|gY+Oy! zxv7yz#tbx5+;^e-DdN5>-9I4iX=Tb7BkpPT$rvf_Nu>!`?*Z1qGjU#e9JPRez9=~4 zlrcz@qA6@6UEI@(Y6O=%v$V6?zq85`;#A>GI1G{t^pY4hnua_Li zt!0?L;dVUlO_B{$Bnm8#x*Br|vo}mZ-+VFF2Ta>D-Ugs$O?+zRP-?`QULf)}(bP=Y zGsJ+qasQ)1bR>3kBw~g}^eQ!|E_1GCT#cBX**C6n&$s)=ZPN2ErBUj>vKpGk3H{9m zN4R)jSrED=YuPuhM!gBbRYj})&{pW~rj~u~pitkQnQtm9-EY$Ljr;XIkG{Tt+-rLt zrKHzLqv06#`80sjV&8K$XrMJGs-M}Q0qn4@G-GKD20k+VJSCz4T1(!4Q_MI$6#{@~ zXkt%FWR&jS1Zho8?1>D~A^U)AOkhG>IKxJCS6r;JAj-!H{Qdj?8-ZJ?DVdIu$1?rE zF2yST9_0AS{OY(M94T8z7OkeUk1+&f-UX_Bk%rF6U4`LgTF@N~r(&4|S?J2e*t9|M z6%hjYBI=&rrdu;&;%@c>*;IsWsVD+{-9M4UlFdDW)z=QUZS>Ozy^R zcr?~fTXHu-C6e(N)K3)T1QZ?z1$fH}YGWmb$>WhnN6GU^{^8V#u#%w9$k5w}WB20Q z0zaT>Lwaw@dxV`G4Gup=3V|AgHcrQA!)APgKA(thM$?ID)R{!^cy}yA|9C|@aWIk+ z-apjV#^W$!7b0!_Q)C6UqZa@jt8-TTI}t)^@-9k4KX0t0u(Rw_R)-ZyJJ{7<`W3<{ zG~CN>u~`%8(e-%5n{$p#CQG@LLdIT~Qj8G_90r%1NI;L=9f{o09KFarZr;_NOnPW9 zg)G{r9<^VmX!#2{JNZt%f~fD;GW#-)w6?8BD1wunsQvB|wvdqc4)(C^sXc*`LCsNs zr`!`yr21-eeBa8hMz(@zGmYqax`-IZSAz`iF%jvioNd<3gV|`Dy93=qD6rM!F%(#3 z-Zcd3>8>HLk$ysfwGoJ#2tbuzDy$wntZc{U^s&XHsOh)r@&6!@P3Tiof~Gw zHidX1X`xn6sXdUy0?Pq}HAqR?aexKh!h=DJ3fBB=CXHEo*(|peNTxb5=Wd5mo)XRb zDyAHd9*#*?IlqqHhi22LCt3;{Lr>&i4m}j6Dt?L{3Om7K;K+FV5NH7#+B$5WYjd@T ze!b#y1j1Z0AD6(Ut|RG06Am=uR>XZ66_*0N%7N7Er?vb5xRTfk0#S=)7uWl=RK|a` z0MTgRS19}xU^p~qz!Gx+iP_)4fvPeK1>HRAd`1m_>(}K4z z$TkG1#%wG=HK_{uR&e>6l|VLlFnZu(uFpge^&v#_EkvG`k&G~9c&!ZYWt;+;1}Ko# z2nylF<|+~A6pRg^V!yVEc{U5M_1_@*U08?28jiiB__1s!tzSTC6ikS;TM#a+UltE{ z1&XYHIX(@>AgzBRzU*jJ^ZM14MqQ@BcRs3oeJ!O?SR~R`QQAlG`0hdW`dyTkVZjCJ zNWg_uSH4z7nfp5gQ&_8<+2pIq&`KIHe@M&DY-Xb_9|WKqyLM_+_zXUeuqU!y@ z62%+(tZU)C(dk|wOCuzhTgJX7s?<(B(xp8ql!jpA;Qj-DD|hZ|+LJN^v*dd=i-ZgJ z?b&lFK?pq>SA(T}d%{Wkn!;aM49d#f7RUew*JYuGeKjeXmVq0K&2{vk-+h4UjBW+9 z859V{GX1Pzx?z-Ki969Ml0i(ZMwo1M(8w2n98yl*6&x ze65|8xu?Mu^t7Zev0UTOw4S)J^t6N?^y~JMp6K=*^>iB+>(qrDQG@Ff(eYdZW}y<+ zJS$}tDdjIeZoj5koe&Mp4i!S`#x!+^pAJ{&tA8Xv!qGiujJF7R#fz93? z#vqg-f6Z#QozSx9tLJ0vOY>;CNN1a{U*=01Zsj|%Rf49nCh(CxHAf8A;+CJ+T*6}u zvL<~$?m1m`3H6X{9a)~aH0f@{_JA=m_+k6O(X|nd3Z9UsUTRE(@NxVUJSi)_`;p4I z(N&Ji_$Rm2CMud;e^v*V#!r>-Yj>l>7(vyq@_6rSkw_ohZhpu+#g z_<~tz*s$>x!^won7Zl;fvhfx4px^Q-**M*vqm4g`#X1%eMO@9IyS(}0h%e~6m)lx{ zGQSLl>}$JGng4I7Wc`iMcGMf_526Gk?Z&!+EOG;_zRzbHM^V;_H$>Z@SjD&vlVq5; zY0TZEO_R0KHe&)(W8|%aHIwhV&ytr0x|Y1_=s~|1{HX!DJx6(GUM+kd6|sEZT^B{< zz3daq_f0@k^qrm!-!~hwCh~n#QS1!gcL9Vp+E6wsFAvPpxjRq-;}G*I)la1>)zj=T z3cQkVeUAo}2_>Ta2t9HI3MUFRl0$9hN{i5tV_oULv(ga(#s-4M5rodZwi-n%fpXW^ zKy0HpqUMGa@(99%{sAqBtgC{QNgi|xZY*uDpa;Eq4{4ij&r#b-E_qOnsLu7h@Sx!f z;XzN%W=-&*q*~!Yr-KjEgXYi#d}M#8%45uA?vd1WZ-Mo+0mg+i!yIcle}RQrmXo5) zJqB+FS!P!GYEre5pe4o3pneUBKtp)y*3a5g-rAE}i#uqUN*^#UB1!$&$CjxKY&n~$ z+=OChn99XuDmziu>dnX@qb?lETw2`*|K3-8HC?olr569*rTU{y#Q)NBv`V4Lw78=9ph+Tu}k1qhzcT%DVKlC4v5M}y;|1Hz`?U>Fn>d=|<_@h0) zZz3ZBzq@F1B*O2%QH!zsUPb)=l@d*UhsE*Ry+eK}kVv59Gx>FP#IKT=-z(zy{hsRI z3c;Y?@=Wl%FUBaIM}nzebqNg$;F>N&Uq2zz$-_^vbritF^!6f*U`zPW@q00r6#f5T z)+okYW)$Fh0m#L| ze1AS4@-@S0SoKkjgou1JtTN?$;QvSYp56-Pe_p<)|4DrdMLW9iDr!gaP2e*#BGyp+ zStLqK{jEF?@+G%-isxf~D9%gLoN8P`AqCa({l(g5!0_R5q+2e8e%d9ap%1Y9EviXE zqb7e_Lt#nDD$8HZT&1tuLH=shIr^*p|0DeED$C!#*Dd02ecLU6yXE_{`P+|CjJ1lM z6E%A|Y*f`K7KGJOr~19szosWEYBy%1z|#Y)5uIX+>36_kj7K7A%=nH9)6EIGsWZd1 z{o7LcG}NC9uv@iG^k0#whrQ%(e?lJ<-Ufv@fZI;vP@+*JMr32~qjz4FMrE-;I?9Ph9(l)mzZjn zpzbp;kxE=b7jkaKM|9zIX}ZlI4`QK8B<58nD%iCW;8+^b6nTFm*2jHECNHD8O0ieg zh4t;VioXP?s?-ZGq)eedgWD3DDk9zVwN`p<8GQEdQ^X zOv65m=XPSRb99I7*l)}sIo3PPbiTeZ)JpesgDty;$>lF8)x$8Qza_lH0_4jWmqY+B z5`fCQCm@Qla#%W6QZ^Zr@n99;6M?a2V$%F+2Ir8my1B$NFCf=Sui7Y^{Ko>%`o| z8CF16i9Q*3ArVYy$U#f4#4H(chbfg9246c9ReAzc4RNThPl#$fP(@{}KSS1)?oAWf z;PefdnT&x2~gF=p^_4! zy8291SqV^Wh(ooaI+1|;)6Wp_@&u@A<4`>eRH}PtawnJtcE~#ViWhfeZneFf2xEJkjgYiO4`ky1|b%q*~K0A^g zGN#3(KNv}`H|~i^zcrG+(I}2d?;lBTFnZxWI{u)SA!e>s0?RQbx7PO25;09>BoSZ< zR+aSf8awaDKkA2d;IdVGcMqnm6n_{VFtWSnMrCd)%r)eG3!dXf)%_MNfuLDElm@GJ zFDH$;S7Dh-Em@VjExBYlD|^M0qPkyaHdomy)xF%~ffr!gXtRX(b-ad>uG(lHK71@z zg=C-mb=g(2%6o{s$R=#;VP&u9HmYUIwV_FNwd{4m?S3VkjBJ&f41bHt?4v=6mAtOM zWk>awy`q-9?%Kv0y}Q5*)>FxStg;r)9wm?x;)azRP)pvzo*+#QVn>h>Kr9H+h#`YQ zfH*xQAvsnLxp+Q95BCH2x#&Uh&m*DTJFPATDuH;=%FYB@*(tPwO)Vrj07(@)*`@@# zNkz5)jC!8ybXN7;LLdp%GXwTxsfVK905%`Ir38LJKOS-~r&=^@hyvhg$)sQv)6il{ zjqfmMH9>bE+XDRz_u@59b+Bx{`6u4T`I-0amSXupU;NVl?fZ6r_>ndlB!1s+dJ7sr z;`i;$^(9PG`&U6)ZMcjbME*B$D_)_~rl4Qx4QG+L3YJ9SP*!dW9_e-VOm)^N?XgpM z&m;qjZnw$>zmY@3PLa~4J&x>b7RF*~?hapT@~mFIZ?I&K{uTB`05Ek{CdW_tZc0g) zT>D_(BZ1k*TP?YQKzU#qngYLFkY()|BcN$75+;Vgx__?DE1+% zgY>SE&LU79>Ujk3v-Jy4fIWsN7?sounF8F=iEH3a5?uO1vTL7bpv&Vd^5m;sF%dpQ zb`6%NHVCFvg0M%#P$%`wcj(V0DO9Da`&_U*0v?F8QjpaY2j*CenkEYdXrMk|`p)Lv zK?bPh&t=Ww3aT$|3Tju@y}j0NH;WN<@6#w%#E#|a=m3whL>*G5{<%Y{St_>i!0e4wi=AUB8-Hj4NLwWW*?gk3Ym&=x4Nd!?K8%;LT)iQekej(dDB;}x+ zM%Y8x7t_MyHQIJHuYBKv}oCcz4*pF!Jc(q4QucHrSE2c$7qf&I}yp_Qhv6Ae;Z zaCkC1+E8S>tDz_N!Ep!#Hd6$T68(bOotL^e2{ndZU39 zp>3jpRG@uM0bfO(J7_4>FXNTV9(oT#2sL$zQPUVpO)}J^RF8D_uzHB#@KlPn5;fep ztwLR=aau@xe}wVHM+nCHl@4@M9c4_haOV-t$4ul8C8ub-IpyE7r?@~0FgK@Vkv=*;9$fQ z7A1f-=WP~z0wdKnaa7BBH@V#yVOCkW8KwaMrMjl}Kt@_^+K2i?(2wasNwro?3!brC zM45LR1z!-N4|Y|Q=Yk9(O-a9o7qp=glhG|vR}fQ_@5cdeChvLMC(=m;C`!=^ak$|R z;-%kg8g1$af5f35h|4teX~}Ga%toMoGooU~%(rXq3~il`RYDw<0vlGx*|lkoP_pv_ zZ0{3MQA05%!lucH0`cw!E#Aly09m0EDR%w&DOm7n{kh{w`XfNa;U`k9HbSPONm{|8 z1LsMAdoWKM@nro#J&nXOtZ6CEpLy6wEjHy~!)b$9t6BL2s&|jxJR3iSPQ53B7R%po z^W?oT?RB@aA9FwQwWTQY)`Cc0jq9ifK|(javn#JAoIbJ2cM?vdo98Kvpho$(KXFm* zflinW^wrwZ8nFoA`q2BC=!@&XXf|-@|AJOY{N4qaxkfBpr@sp;M2das_#tA^1g|zy zJI+H_50r0MB_B6S+9M@tktSUK!-grIN(*SD33OPTAOQ4t6VTtS{N85%mPr0Uk5BCGI`@YiA83`a%$9itx%UhryBg`8KTcF)}(c5rv#nQrrv7D+sAp^T^|A72_@$!em}L4$rzJ1u#YLwM70&kt|nm0 z@{Cn`Au0RE33O};9TepttE5wu@{YqRv`XdUF}M%+HoH9mjg$lhB(p#t7fpA#W$$*p zD4pXVir~+Q7ZO*_z~C2S(TQw;Ir>9fV%5rX$3@F<6k09g@0d|eq^}F9U+49)59UnO zJpt#GSa@zW@ia!-$(_Ixt`2Xlnpj2JjMzaB6S62m@tO`O{*zh-r}&6T@mMx2z{ICi z-)6^ToANA8T(U+yrukYgRG$4O9`TUwYb{Wor6W%8(3wA|;-QPL^>@m%v+#g3vN8*8 z-W;WRu*27qHM>6_B-Y_Q9(ulT_EqL{VUkx7Y1y;S7iopb-t8jk(pj4=B@8_#l=(nJ zwA*RMH9_0i0+X{!bv>h0HcEvz3DKuevw^Z(E+N+-+yV&OeKnm5yO#527Ngt^-wO`> z5f13BoUF3y?O4fFv{D-@Tg$rBe%LLTKlNRMm+O@3 zY+G<#z3-3`99N@MkE^deDf_~9-^otOyk`)xv^kfoy%Fz6y2{q#kRQCMgqN&sHoSE* zCJ7O%Ylq_d$V^OvGH97xKiG^Ar@ExZmztz{GeQ!rzttYH(Y~6dFcBC4NXI{Lr2W38%>A)!l8ir-skdAH>TvmPu%+1E-G(}^bH_G+-1{l;TlDJA;A1l7U zfQV8(!)B?{cWAc$ClppovMpr>X)&`kI3|rDzWjVP=@}a`p;K=rR2fR8XvWeZr*U&T z8rLVFacw*e3E{`j(V#%qqG9#_oYp?zj+JLKH?Vkl>b{MWhws(?8F?rOv}l0_%#K}# zGbgwZFhtAd9wn?Zvq>G?=4gEz+JjlCGc$u-p&FXSlDiv~pT-H3KsN>rERpofufQvCJwH~Cc?Qyag1(EoC!zl?nIg*XQjyHM8h-9&KG z)g|@HysznfkrQxcCzN?NqgAyz*PR0Gyc**hbbY>=H8v%11z@yd8%srxb+M(|=_m>F z2ub-63oSG_#tu_4Rz^x?B`Q^9m1^Xr6tmPElv3SmvHy);!J64 zfz8^oGY1V)KW%U=xwF->o!SU{DER?k!n&PSoWY8*S)nm{rsg{+2{T3evZVq8?NBgZ z2YiT-G#TkXLw0&R*?H~BF71$PAxeh-(2+Qo#pAuUJ>H%j;ysM_(G1$&)Dh}B0K{l& zr`}Fe*&U*0gqpUsb%c6SJnARfqn0~F-G@*=0o1T1yuJJ?niIDrtr(qe;l(Vqa>m6x z5T?#vSy;mw^h+%4q2yJBcMqzUwFHapn6xNP(+g>rYm-reu{&_eO5j#RFQ$FU7YE(- zK+RIMVw@fU)MT|IFHX{c0(WCCPBslVSW=_E{S}2vW%Z)(f-UPH7aY@>Hi)*un&D!C z!_$pJAlQ*&ZaomE^ZS62woyi`b4G`Xxd0`N`_Lsoh!iDIOhU*PE7kVe6RB|h+6WsW zrfSJ|;6@hW65;r`i)~!Iw1& zZN5508c|H_%euq{aIY))P2#ea*8^voC=gQ>d?D% zg!;~S)RWqyZlSX<6MCgf3H2m_I*vq5WwJa54XOf${P#`R;r>HpDf$qzk zl?q{23V)$us$wF6s2B(8Y$XH;h0#cg9qdC}LtrzThOqcyqzBsAGzZ?g1Wk&GCm-g{ zuM2ey9VqUJeQ}EW1C@xi>M+n zcFWfJQ}sc!@y<5 zS2~UGxfR;=BRD?D_&@ORWS%+HsF2a1Pav$(kl_YA(oDjeoiUTJi#3D~gcY>2TdjiSF$F6lP2R50rhuEC2LYxK zh3AO^nV@kGU1$YFe42qgAW61nzo?q?VQ0;fD0#@hz3VtC^6igB3YtjHW9-SGHqWb zCQ}YFWJBaGg$+&k40uVJ@B&MKe-LI!H2x}?NOea^M??7I}?s=R2&ptqZ z3h3DVC}KWt&JhQC5T=nc(X{)}Wb{AXoF*+R-7*3%NbHm}jZR}xuOE%)P=NBYzrdY1 zQ>?+gOJV^IP;@-xZi14K^{MzdAbt+h4_lr>P#kM0(2QG3O&)$6eAAtN>zx?1X~&U4 zTktu`$)8vhD{-fHQ94OlfFJ~$tC3TD&O($Ex`E zqJ6|rzJ3f*&W2z>qp!r_pLD3gMW}F53^?rw7O2l8PKgBl>RoUshsIIj`!$+22KpnG zx+n3&Sxk~kD48(uqU|ue&ksLwVDc{bMR#Bhj07+T43wI8>&4?}^O4Rv!hGWfn$+R- zf{sZhvs_07^es3PLJfGhDE3z{LJh+8y@DY6H~1AA(!V^BijAX+|1H9?#Dij*HW^ov zYx)pqTy{>v4_iWsdT=KXcvQ|>0cQ&b%Ya!#;G*2>60oG-2a8n$RK$9T>ax91>yl?U zet#nTl&JRves$qU+^QTosw=mVKG-yO6elvorfCGYdNfgK{es}=*`l)Q6Qc4WQZ#Kb zu4hO=iTZ{e38^G0bF(fWQHCUm(yJ#Cl?9v%6||__&Z(^BRF)^ALW%m=#8i@%xyuEW zWKAQ$)ejJrKAZ|YTU3thAz5}JMboz8O0ram8Y$;BLW%kh+Y`!yR}Gs46*L6`%-gQ_ z;w0$7BEdL`BS^t(dbplJf)e#VB_@Hl#}8rV7V5$Q3j_!fo9NK206kbFuH+2!tqW;0Qgo^5<%&mh1fE1lH zjR05EoT=W1v$xa$JzG@9fr_wubs@E`+zq}~yE6AKUU-bX_7K*I(%2rwf4N9WOM8E! z?_`oPZzoML`xoKL-&ilF$LisZ+Jh+2Mez^g&!t_|6;5DH+cq(G!L$%pT~=N(;kPPhCp8wgu59)Klp111dL)OtX-v znVR5BHFT~FhNftsR7|*myD*6Z2ZU!!ix+-Ob@c#JG;J)d?Ra5JC{e%s9BKZUYlmo3 zG2s-xz(k4~pl6Hb3%iNtNTg`mP+ZTTNs0O!ADx?=BO^4a*y^cJnEp7YNzWF|YdOt) zq-a`iT+g6MiMs0?Y2F;sEfrh+2+{lr^CD`1o-LYd@R1@>N1|Rtt%-%Sp&UcDjeVw# zv0+n+>l0;eHczwG%4)yLpu{`5^^UbA!Jyfa98|J4CYds>HZzjxHJLybG8ngtv}4)| zvW2vhodx(hvXX`X6@?9D<0o$~-=AWxh%MNU1GMTiZe)BwJJy*=1Hz%s42|0$?i?C7 zN*?j;L{r4tc%TrN6n_P}PBr#i!~ecxz4RFP8j*|2;iyNWZ&O;3{G7%2qmVUmkYC`q z7z;9+PM=;Dg?v5^vN{Hmzj9d~g`6A*IWGp1zxG)Yg&YzGIVJ`&pQ!&T3YirL*)K*S z{)Xs@wCCFY; z$jUg#i((+3AxK*k@{TyjuSq^rqD=((@mmos^^SvF69dVQuYMy6nG^?E5d*oCaQdQ< zO^AJ?^-c>T7WJNFb$3M}-;9IIje$HuIIoRB`X7i;M@MI;cYnDr!5%uzQB->{DSVL_ zwFtT?6=dNW6 zZ)~D3%|HquB<}1Sc*v`n$O7cdBJ1O-M(O&5C-JGaC7;3%YuZj++j;tiz;%Qw6c|cB z_-hFCqdRf-hu(iPd2wPWzIWv*BF^Bw>ntcm^sYoh^sXmAp|bRB^#JdFL_J+S=Vk4g zyfA)1iC(wq+-R;rl?j@JOZ2YuI8A!CXu3Jg*E!7>6VaqZ{nm4&c^6j01WhU?de`$g zO?tLyZu^krT!0i!dj{8bI%i8LQQsRnH#yg$OA$1wnCM;qzK6=vvqf_pXvX)hR7awB zy^Uiiv{B#s74KVn@f42@NlCR|@}9K~t(aC_&*)jV^Ni6p>RI#lvOp&AGHw&fN^Wv# zAE*7;d0h^YwG~8^t)uQ&D=gCFb!1W+?{XP$CgGmeeh1MQRE^50 zC4!5T%!8i-9{k{I3Pj$VrhM%K%=_T*5DJILBt3|(X*5prR&Qcti?sNypqRJ1634L$ ze@63b!$}Q=Sv0>)2&i&VW7t8(mBrSL24+3H4wG+=X5!}v3{;H#F@vEF&h%~KAA@}S zCVo_^(F;Iv8Do%PT=pFbVZApJEj$AB z@HEqr|93?ou82U~U_qQBi2i`c+YDMp5f(%8HXAPd=trRj{RmyYqRZ!W*+-Wqy6m9K zhjiIYmwLLar^{-(tf0$VxU`=N-}<VeV91-uATGcM8Ld)xnWY?&QV(`e zYX3H{T?b>+{|P_EJ~OU_U53BWUj_#KA9Ptlmqoa=XLI=!q`_)?b3qh-ew^_Y9ynmk z9HUBr?X*yC79MLBei^^UYgS>HFht>VvA39jLruV01jy&8+hM!{fD!po8PpCMX}*JN z?Lmy<6KQRKBZstOaK=mUUz7BX-^1wln>-OZjC+U#=c^i-7P+rC3v#bV$hBY~nuy#K zA}3nCvT`rZFNbE#Hv1eEZXiFqrh~%$OkUB|abEea$<9^8Yd_LWpBRSGnM4>)0R&Z7 zCi3xt!oc-#2?h<_$<2^-=uINPO^*p+FGEDmg}N4BNDe{B-#yEUS|cQL+Aq`th5BI?tQoasKorGl@7m~Q=;Uanq zEaDtKm1=tY(&J+Cc^OSkGyetT9iy&ZbOkXE|DJwO4xDcCQMB&5@EZK@wPe{J5{jb^ zjg-17;>sO34j75tsqgtPv3$3I)Ap^W3uW(q}{QY}TlKdbomoq=Hp^) zL2l%IGV}XWd^LuBH0Q`z=!9ux8<;^RXCymgkJG0^;m=0qufU*_Y&l){P6Ey{4)d*)d*|EN+q4j8P#vN zyaP^nO?~jQx&Qvz+~<%hP9#>>QG)^}tWhw9GgHLJAylq1O~#;Zj(Cfwj`0I06+gC8 zU{8;+9O}f^5~fh?afgOaUeLKQnC+kyR1&$Ofkp+#Ni5!G#VJ()R|N=wg+>TCOt!~A z$3d^wwTC}UDC6MQaCjmGcq?8Www@wr0=81}k%G5~Tznpa{pk6PiiIu$z(+G=XWMyd zGQ=lCIY?8M=42J0*18(sdqp0tw`Ri0#iQN#9tiPMoz=jnz!uAJCLd#|0kUr5SDPNUi@@rR$FUi1swwgEInleM-H?_AMJQd z5|6>51CvE{s|)$}D0nlEG!;6<-ayxHF0%Kz8sc60Ok|av)T|Dw>S&ccdBo4JuP@;i zK}YjZBh;k<7+04uR&QKgGKbX73+Sm z5n8dPFY1BE0XX%YOs6~bO-B<65;IlMnU07TQw2*s38o4j8IMXYK5oh^G`2Q{WQqwA zZ^$M4ehZ1zWX7zeCo(h3+K2HJvK%WVA1Tp!1pNxwEjEM1HIZ(-kcBk)45UZb5Sdt{ z`;kJi>YZQ0r7pjcDm89kKO5C$JOeI7Ll=6DgBS-0RK7vSaMH1?RM9bV-qI5YCQ;?r z8NU}lck>^Ckd!`vo}q+M;DsNAs*XC&gxc!Z+6zc=;g_vg=x-1T0iE)u70`Y0x`>4a zp$nn#g^cVkb%|r4Zgr$HA%vcZ$#>&J;+n^H8{uBUhw&OTcaHj(&15n4$Tl7oY}1EJ zEH1LZ1zGPHBFYDlS9U=1ZyTa<+~*LGNjTbgSc5S*8A}63-OA6 zP6vk>P#~>(?fR(I{Du3apoa5gn0cy=dWQq=+-vS55pRz-H%a{qYEa8%%_??k?-BkT zakW}@gyaK=p6P*E53{ScILM2Q5+@bfYRND1eTQgjd-NXD0pvaseOJ>_bha|K= z+~l{&qU!qO5Lz9!haRy^{bppS$#^vB$WKNRU;isRmLFMM z{)6r1M@EeNc=|>1qa5V_>$%Fm?kw^@elGHpk;Kbi-Ld@0;_@G8FF!J3nplaa*BzoKLLk;UcT-(G%X#K@1QUnD=uLH_5@RsJ<+ksm7nXZL?( zB=Pbu?^u3harr-MFF!J3wgm-GdniiTNci1!GP4!M_}(JPk4KJ&vcfpd(~rc{wg+)~^Me##HgIwIlI2Jg zBdyebBD6vuN9J)*KV*bIrti6*WBdbn)V~_7e>&PnD?5VtPiP_%0-D35=c<3+1iC$Z zRyDi_k>U+t#=ko>fcud}qVB;5-s2j;aAZUcU?84^0nmQ0v&xS#oQQvkaH*|9`jKK^ zd@7nMUudAc;EPuLyx>_6omKFS6Wa^!d$U8qCn1XjKY*?5af089jHuv)@e~!@ivPv= zE{M5va1LfE;_ob2VK%+c$B%d!?Ho-9XVa%X`DvV`$wQr^eJx3f|Bpyg@qnXMN|pIo zd!(mQ6-TcCQBJMi1CXWoK1`+$NS(;b&aGv2wI}e=z*%`X5&Jsi75dc2WVOHRF?rg> z2F~1&s#wFe(^}y>s^WS`R-W!kWSi-GD(tgfIP;S=i}P9O^wMk$$3@G*nidYZ%~EmJ z3?Dzx;ZgpJ=s@+M&M`PaiWqX*cH>AkJIeK>_a}~WKwbX{!!Qnp(-S{20Y>*CAPhtR zT#9=qL%VI(w*%BVGBi402$<18pDpAajYBdDVhPN5jU{ODZL zS~lH6X-)bPO!MQ!F7t?1v^Vp?@c$DdQEGp8vX9_^cGLN|V?n7etdEZ9@u1#jz$$yX z8gUM6BaCbc4!9L2Hv>-|WqvnI4iluvqw~k`GzALii?ThjNu*Fz$`oE51GH=(@_XlQ zU>n6pjIP5+j5?!2JON34LRP!tBSx^ohsosVBZeLgM`zPli=MuRk~YB#zrrC#`9PB2 z3y4rZg+gRH9cc=Z=)l=RERYSw2S4bvK#i6j3T8NGD9_Rfym-|sFb+H`)qZCso(Tcl z173&Hh8Ad%j{TZ}l5}EG1%Am|2QZx754GrY+<2rUo!>j!ncqPfPi`|xQ$-Y^G-~0` z+;qqCT0p7?eD)T)@vzojIB1b;s8EyV(dQH)A3msf4Xj#xrwWZUO?^mKm4b)lspo~q zaqSeU>V>X{<>{liVvew;USLwFV}Nl4^vwQ=m_P;1P@efMM8^b8pf^?2SZBUL6McBK ziM|*382?`2q-X+c93K$$!phLH$sYQ`M1M+F=F?7=3t9H##ZzKCC=_#BII#a*!Efa=x$<6X6K_Tjvi!DZLNET+hR7ITZh{J@>IMn+QLMl}76AtwrigORa zAk;9@783O}9Eseb@ffI?)gPACTkHL_xqtCz_{|{H(euIyHR=_0JpUpzd)7eh+xMO9 z3(M+-CW8s2-tscAd~3t;?nWQTC zfPia=JT(+@li&#@UqmHg0E+)A5cho}hc`s`d!jC>$>i$d40+fKpYsO)RPUz6Y>dMH zOzY_jI?>i+R?o<&(e&ouGKa~yxB}oCsp>-b9y)kZEyHn3tPBl5-qd&flZ5x~B&1|8 zI+nqyoxr9yMoM?}Lpz_u8&2#D#i2T}% z9J_ZH-*pDu(YC70e}~9{{|JQ}soBJw-!0CcN0;8nK7bE7LR}WU8Pf>=Z^K7?3lQUI zjsS6o-nO@v-+A^}9F>c+=JmnL(1;B>OjN%Cw-hFgMh9r85WwttC|-21mRvI|qb`TX zEWFpbyd@eiMeI#r0Rdnj@KVNM%iz|ntk&49W_=*C!cSZECC(%4jaH(%u=Yz)Wh?K_ z*{<1LjBg>vwx1L~)*mD-n};yajGX-}Cf2bo@!bn}le&gAc=p=rTy>uDILoa_7A=$w zKjo@Z{PeCOs@FkZ$SOL82Z`Z$IoVa`H8h0#dl15IQb%@`YxN{;=v;gsm;P{Gn$q4T zj{gIpHv(xys4^CTSZ_LY{`b^L9P?a_xXC)dOzqUZi@;0-UyuCWTiyr0pv@}R2Fp@HcIx{K8%0yt2tpw;3fR*2S}ghpRcuqOb~4@ zv;4+3=fax1Mo8$H^3^Gsd3y_{WX-%p9h0H;p6+YFkp-Dr_P+vqv3K};VpZM`I24jKb2&*Yq&vV;Mqs=5%feya&t0cIpW*uoJuLgg_bfcUe61IGQ$rHH zW=`hO;8}we_Y7a_#ohxU=~ASIbqhWuT}IDM)-!aqjkG1JQye&ttFrZWPnXKpTiGTL z4%3$Jxwno|t_JtXt8)lFs-?*ic$!1MhSxp8nf)~&lUDa25YD)kc{?t`u*ouagb8+=DKMY5^PG?n|Y# z@Ylx#JPowxfES_ zFd(&ej~Z|w38zUQL0W@bfI54S;Zq8E9}Fkj7TS^H3(!!>h6AFj0TO<}G<}mF;a}Cz}XcMR90DT>AM0F8=i1H3rM}|Az^Q*$g zJipS49r{%O_tn@cy2Gq12afl4CA6a)_te2&Yr@}SsH95r z0CW^1oI3hIw2q2ve8oLcg_#8-vSwbcj>^#TZ-x%oh)gZ>ZG6ciEkrLSNo)u<&%mq* z4}K#2!FRx1YiQL|uY&PW@beA9H>gkHJZU+%#nmwT_8*GvvL^S(=|7&7$^lN#6TZ`F zv+_}$cG^-y)!KI~x-?AsEuL?Ki}hwH)i>D-`zQ-)3&$%9HrTg$?#s=D8WY!9PNOR@v zv|GHdsl|PP*jLj%w;9-3ao?GLKNIPEfiQ%le!(nGa>1+K5+BpyK)T1FOsaO+J$-Mf zfxdt-#-Ud9t}Gbhy)MST)Zuh1AQF!_X z+TH6bPhM1hfhfA!)iix$6klEq#1KO8C@yciI@q4K*LT3KEUZKHZuee*kE(S8Ejmt2 zFD}HX1GLX%jR!PV6-^o;g>h6`)__kuH~VU`2gwhOkfweYel=o$u*W82Dk*-j=C8l$F=!`g-QoYDJcMR|a|~hgQg~ z>9$O*a1AUegO$q&<{hC<`~Z@2J9~nLM{tyi|7k*kik^~Td%Q0WG~YF=hdMYTia=gP z0YgaQCGp9DDR%WXw8$Z1$3G&zP3(O=gq|M_$4{SaRY&C%jB+X(?HExtw>x&JcX;d z+k<-w0wBE49KQrJkcnPrka`~CvcPt=$X;;`T6kvoDuRcH$wrdzYa8@*p>M#oo^OO< zoA!=_wU5;us|!_T0S%F%+?eqI%9l}lV>{4i`U*yrcjcW~DO#xLzi?uIv%Z_VI&Yfj z-I~ZKc^`>mLU}&pJfZ{s;NNIb12ZA$^_}=Kqk6Ah8`{Z+1~L?L<|x#~ z^g^b*!{lTN9&%tuV*LXXA?q3TbSVlDdIbj(7pq1`Fd zQ*C>Jz7?%QR{2iY@hA^UomeWn*B%av#J7Fck$i8abJm^u!Ndnuzbe32!t~ zo9w&_bCLXIh01Yow!NMDS3#VDh2MYUm+Wsg%Bjzjn$e5paQ(55VSZ>-j3);jzjieR z=kxBmY)-4_u18)fd7ISdsl%plJg+S{(s@b4T;6p@dTi_>0`a!zd5=Ap_t>k6O@ysD z01ySjf93Ri^&`2ru;L-kKjNX0Fjx)Kkq85>0@6Yhl?G5_&GybVo~w*Si3iHiS6 zWQvYinb#+ke3gC}VHQHcjf(%TP!>1en-zao=o=j5u;P3xh%#39L1~ou9N{wRoj#kE zijyRe=&Qk3JVru|;29VO;^VseXzuE%{7`W|38PdKM>&!u8IszTP)ra=}Pv9HRKQLnh-Qh!+vO#2Uy9o@wp-HEY zRvQYEt@=aS=Btpq(X1cRcHkT6TM7JuV2i*BV9JQQjp2h;5OTc_=80{qSaN`8KnOYf zv~5J+cY#zo_LB1FTK1vxM|}HZz$OfDg%SXVL)~rO-_p{~ACN}zlBwImM-~6eD7$_x zuyQ19H_!xr2QU!cigN&rdvR~_NBnk14)2+%Ss|h6dt%NQ3|6a@`L839FBom(H#mFJ z3qgqTqxfC_$2c;6I+K~WfmPB`1%;0(fnfl{$tSkLa*w_6ekE`No>z|-q`IIdfHP~- zU&L7g^oZZ-(#R!p%%ZM|~ zQPV+R%rH%S_4R_5O^Ed{V50ps$?;D>qaIWV*ng+^50C*5ga9{jmC=HRJ^`fQ<>-5w zJ=e2%k$(gD_0Pnnd2asyxO*4)sEV_JdpB8N!O#;lYS1WAqY_0Wik3*!a0#G*qFh8p zrPWfc+EUmBFMx|@S2#UgrB$rHm8z|_^wp~Mf`|f1fCLnki`6JzkXk**1qCS(f|Bq5 z%$&2kArV3S-tYJOXw9B8XD-h?^UQObXJ(e%-Qv9*Xpf}|v_xG03wsMdaEJ!yrmX^~ zg;w-q4dhr!Sne(dB+ZW+oUTk23W``dKe~iMg^VFC2?}`wcA^xjOZ|mznbkR(GDmRe zfx_&&NAMt+rMM9H3;$f zM$N(tG{pa_D>y_d;5_XHr(Dp6SIAv>md-It=h%vCYc}6sWLJFZgRJ?%?8r=LJ-Hg3 zZOmD#&Xw>~%KS&b7CL!P9_o4EY(mw_u7!PcE5}MJ3M*&pgf5bxa9geu0ve9%fkRG` z8hlr5S8(z{rgIHS*vw9)2UxMDWPO$%qnom;AQcvx$MPJ!MS>j-M4@(*7U1rF$I{a*XRP& z=`J8AZ9gdDq$YJotm*m)Ds{w2oJSr#4bSAMYzK)L=q+0Lv6NLqA(kYy+{sa*VB!l3 z@V0=hk(!{D_I?7Xsl^^WX#2H??+rk({4eCB1j|3w!z8T3_~TTIj1bB{`wPYx7b6`L-aK(QX=zsoeNWz1SA1N}Ws zpic`;pw~aeQhJ@4S>IkM0|P^DHE+O}L%(dSgnB55CgPkH727smoHTPv_Gt{-Xw`lmTqP?tAMD-GzUo zN$DCk_$vH=#wLmkzh{QEg*v1m z8J8NL|GYpQYJ2SX+46lmMWIt)e!fi&N`7*~4OPMTik#zhbwxswE#+rq+Fr7=pr!0I z63V^O;2bRd^2DHCAwZTEB1V0vj**^a%hG(^)c|d0<|pzAR_>1U`tOUIINC2$^$EIu zi|}iq6i-HC zjnGDe6J^X@*-DB$;|gbOLa)=XrSReU!b41DnnZ`2&QlpMM2z@D07L8EPzYb#A*z~^ zww?f~a#V*!;&>A+?Qa+RURBn?Ch#YCo#Bn(2R0s$*A3nuK)=&29hr}KHgA)Fi6UyxswAUd z&`N^>)#(YKsry>u!yb$V-NpRu{8&?P%E98;9GvIA4V`$|GVTO@ADeG4S0FN!>OI1U z-@?3|enG5hROIwn)8*{v=bfR%^(4n@J1@#aFX{k+U_HRah&OAxnnqAVI4p zlN4*Z&`2x=xm1!w4SJCx`lxQ_wNDhTmq%Wv_AO=2&L;YVkAOLZnS8|f+41#}sj=My zjQBd{Kyt`xd_B90rgU1+oEy$x=a?IU$srHLcH#XQ!FM))C5G@D+dayN*Qy7)(^16E zZgk%bDh0tH2cE@t1N?UNFf7^?uh*f;esWb27jFiw+W7jYrSxx%Lixv;AzhU`rhAqg zCkBW$v9r%N5{Hs3zJx|$f&+{D48)fu@iX>SMOL0EGhkpb6qFqEg1D*h_FX`mZ~(2l zZ*gwmyi7q=%@x)Pkt0l7ro?B`CKz$P5*}8gkWFLrya`XhnjVXJ;{jol`c13YsBy5f$BO_fmP2b_05r+><(rlI5?ayX5%2wIUu8omXkx`B6_CS z%fP(kz&z*AYFcn}oWSrkhvcbkcB2c*s{4#CoYu`A=sTrHdroP*fCK4r~OasdA-Md7^!xzl1=Qc@N-J%iP=jDE1Zic7scWOhr*! zoTI5&VapuYGgs`L#AJAqtkDP`v_}{B8O`tvlV=&8dt|naW_WrhFMWXtW8C==or9N- z<;T-Uwm1)ThEdR85!u>m5*-C(KFyhdr~|u4M6QP(AXi0Gmr9Fdua#Cjf|xJQ7ZzC+ zGkk(L!abHWIsd0$(A@B0#Yk&Q^ibzd^n;bf2dsJqRC)bqrYr5ITyK1JL+*bVcWocm zySFh+Y568C%b7{ivn^19vmTbIXw9u6-)!B1EzYAbRPrWHCQ*pF9`N0}sg(v7Fe#Ov z-)eLR2qy5Zj27?8)!ak|5@dvA(2e8?d~G%GM4pnIUObp1BUKq@WCQLFfy^K5F5cWX zX1y$ec8ij?Fy4C4B8Ms=&;OAkLY}|lLCpimGr_a_6aLKB{gnAYU%wD8%(MHTNR;Ya zh)O8efhw4y)&Tm-L>^9S+`=#oSATyV@pBe29S(x$_Q0Ff!x|KVhZ~6#`9|&3*MquU zx!Di<*7wg<*w^7et_q;mvt~0IAI-fr)|cT|qQ|mYdvpa1ORYByX0_}BV`ep-H|DQw zE=j%&HiQRaC^k56d?bKb*eeeST19FOv>Nc=@aFtphPA!0mOJDlB>IslBr38^tW!L} zH`@J+`}8lFcC9$h3ufTG>ml^jzgQ-hUc|W}h2snS+FbTNSbV=S1c$jtQ#wmtUMALLoqPlTqMlFW zyY9J>952r!imYCD&~rfmy>}%l*yo3%STIJEI{iu0xXu*@+~Az95{&qNVM)TBwk|wb z5Q6;wP_;*0so%{``sH)Up21y-&ibUz3TW;%00MmJ{NjSj6;4nFSb*!o5WP>GAycc~dx*9XDM5mCYA!n6vKtZFgDJMjcHT|LyvNfK$<*JDn!d@N8xFX5i_r;d$_V!KniNVg-*+KXQLfCie+C_b)TK zkCUpSL9|~PVpf6UKLt9|>6sjrBg>iYTwbvUUjaPPCoW(r_2k3nx{uR(+P4w6QfaNB zhoxJs{;cIa1zYQi)I8J+k(v^+E(*uarfOM0Dcj-7p?a}t!qyA)Sey$Z;7$urqZA9R z#``N_4Nfz%MB5>!X!xIePx8B~!IqEexlAWnRnJSK`ZrRa=pP!%tlr!_kz308_YAuu zh5veEbZy%Ohh-PMRu>GpM`jf~x$T1Dv`M$vT^H==Zlnu-i(evb+W>Vv$?1Z_b-_+5 zMA0uO3Tzt%|D0X0!f$bcZn12KqQsxdTWjbvcb6uS6pV2c@FB1?+THsDY=+z*oD&Hz$1bo z;~;`!B&JBL&SG)oDXa4&K2hBp??_2^tFC+v(o-ae)MUgrliBQDXe1``)wOwRA!>W! zqY+&O0gLJ)^8>CDz@F5dltO|v_+bl&(@hEv$%~br z9}yF=&>{njnw(njkyb=7hx@jm0Y`4)2Q5f17mMV!Uf|_TCZY9~D`(B5tpCU5n-fC-5 zZ=x?}5Fcq@)Y}_huGFeA5}m1T;TbYojm|L*z%hx7p|$0$_2)XOhSj~)pF9Xiy}PN< z+E%*8S|K5{E7mjyS98S5N1^23AN~ar!;-(fyG9P1K2nh12Ok)berbE2vsFvF^}Z|^ z-C$o@(C5;^X&>8ze8cBb&+u7SA1D)$u4CQXgC(smfsWMU5-HnFPqvvq73tgPJStdM zHCi>(W=oxVbyDhlS|;5sRxpfC&b}~yyOJ@Bf5fWDnNN>xORnWq9;iu$4m5HJL37G?NJ1)B8O6ZA)Yq;p7Sy| z6|%(>^SM1dOl*LQHfWxGV)P^Xl8||8en-~YFA0^7=t&$B?FUjT^}z1s|0z_PRR2jW#4@by;QKM&{cj%8HvICas_p;;zlEWv81uUAxqoIzQHn9 zZ6Q&V8@<#I4k`YCz zhbXHkO6W%@+c*^~`bA64Wku}5QURszUtw9E{ny}F!W)+R86$5?MlViEt+!cl6~<$E z%yF0jChHItiIUotA9t0j{#m-jZROAGAOuZofW~(MRO3 z&_9HudM;$$2Bz0xx(&^5K2uHH+}rcl_Y_R)cqO9Wdc2s}Y{Xv>4ipVpRpB(ks|4Y{ zy_*jpu52?ncI9{^;_1Y2I>vjrqZf{*g%)10rk6HoHHkVp*AoYQ)U(1#TTFS}F6C&K^$*uuqXJ4puSp zpbVVR0fBuK^Qdo*E6VF)$Rv<@X0dSa)Z>eV>{E{}M*Rz<=4$B5DniCgIfzHwQ7eG1 z6#&!DZ`F9@Ec$*rjdq}}tVaLJIOs+f>qcMDjXtd#{kuR$KW`_S_O=!%?tVd}tZWA} zwhWSb6j{I=KEqO$RH@Y8;M~1dsG=Y{eo~=AA}5|@F;>PtT1yD$yJ>pCjT<|AAn}{k zI-YZcD7u@%crl57zAVCiG3CK4Xa1o@mZsvaWURo zi;MZ*>Ts=*_yYw*yS-Zqere~Jjm5^yCwVa&iwsi&i_AvuM16!5m>9@EPV$6}ncEpu zv$2C=-YJ=#AkiFCI@p&8|9f9`Ff~!ne|s`^adP;_W>aYLHnVYPEQ##9x_g=`;L>_KqNA&nEfFV7T7Z+F~pjtvJM&eIx8Wj04-_z#2)H_<4kFt=a zHGCmA(7S523?YV~zs#jV=WtR6B8zm?jWPe`VDi!>v8J*q2)4Dv)W4@D%X4Dm=x!Sc zxu-Dokl2Ii@>-<%byPCv`d6G`%)B-|=N=u0s|1Cllo6Idpu1#6R1I~&h@ZoAa-x_^ zV$dW`<+W^c^5$mCG3Ng$j+`u#l(n&4CDE?#96ICq1C053hs9S$^7sI~)CS9&rfx#j z>T?6Zz-*2wN)Ej#VBe6R9LnxdUd`n}6pb4S2w!A^2T^u+xPKNLH8{0+&@#H_AsStC z-{4!FdplY1mjUNQX?i$2&c%PmN{bkE3e%IAOqt^6>*81G;+49Eaca?nrvAn}(xbxq zzKqIQI$t6ARAnc$sEk!`$<#$>+Qv8D`m0rC!RI?`gt(1aO?-f;mA_Za`SjE*BrE1w zJ8hL?{Yq_1Ifa&Q&X# z5BRoN^x4$?i%*lE-?0%UhsnL151xX#gmey$#M~O1kw>GF7YFqofK5FREFzA@S92d( zD=ycLQZVxt>$8HHxg?vtPc-7~5Cwu!G+hbhgh$18{nCi9;W@So2kRalU`rE{7Y0)u z(7)nqBiEv8vXqBX9sL}K&THP5gv-lOofhnD; z_ONuZL|z*6&(DeNx;%0pvixt!(Lw3wIdn;Vx{zpnS5!I`|1dmcUyv_`j07BL&EDN8 zd~CB??bh;n#-`s=8qSOLWyI$R@}4f_Fa1nhDsMsSHM4OT*PgM&i+c9nH7L7%!)EW^ zD5&!N{E%#l^8)(rA^NuE@bQC63*!4XJHDSi;3NV@j2l z`T%qXYcZ0zk0pA(73&Z6ri>H+4`f&7V{7qhu;kpI1|oGF>%{C(!2_dEGOOUA8G9PY zGwU8fhY|k|yT{H^h+~)3I@^>Hn*~AEnUbh>(#Qil0|7DIe2-eHHdVOw0s%trk}68g zO%K@O$WH*_(0zk&3weAH{^th+;bJyCvBX`DN+rzU`5zNDOa#7>xa1g6I2`?um{WOV ze?UF^1EL!lQ$c-lkhsoE)clcyUKHh7&lb}|X9)9*g9}QGyFTjbkL{v-P^kACX#6hE z!td7yXW{oBl=1L8DiB@!-4XntrmDps{gjqZ5k*hO7ql_HyUF4scYe2Y``>eX75yx( zk&d@TRBvQ;dKpZk4WcSMFrq4isC1*xOQWTpz22Ih7iBel#t%kJy6O9=d&zUs{_)?? ze)&!x*Ux6g`mpHR-+ibb@w$DhwEYKZyL2DpooWAt?7sg1?O!eJU%&tD-$ra(df2~L z`+e2X=4w|7Tqo(B(^AhIEpp7!?0q?$z01ivn)mG4vvch+tB;TIFY?*nW3fOW+P=og zH|ukVQoy_j)R-UUmU8^a9jF}|tz|n>-C9kW3=%IRqt}U%X9hTQ!)J*HOuY|z!}yBK z*pq$xxK-0L_H4+Sul=%d_gK1k<9GNZE%M2><*9&$ouQV!Y#cp-qCPU6}|fvPOeOI=wkRyb>FikaCebm zS`NzDJc!_sZFUC^svMw(cqqs5=oGzwg8~C|g04*uOMEiE-|UKg;i|Z{3&`cHOSr!i zJ(ui}X0EOj}XZZnGLR zk=*92L9J|2q6V`iU4p#Rbk1q+qdU}KXSFs*%2tI%4VWn(pA{_3*z+|ii!rn0SQa%z zgj;WekT21WPzNlPg-J1^SqX48HH>%-6Os##q}==t2=IH9h6E#0{=7CMn(Ru;R+Gxs z%4$SW>cR2mEuDyKzP%X8hJ|L_5~^ka-H4BZYkMf#z}kYIq~2Dz_~$H?$-18!#JbQS z-UKQhZ*n0b`#afsvN(s1?32HQv&Q$Owtq&ad_-6D__XO(n$ZR-GExAenI>GY? zwVGcUR(l`zbDGw^m%O+*#IXuzV+OtsGP7AxB~b$!v~0TR?$elNM8iVPc$T$V$e#O+{7P9=3g`AGO+Gc_9xuGQkBqO$)*vi89 zDiz;TU$s48pD!GCC`{~4VWQ+jOYN+x7tYqK6Je85q5BUajqR7@QPp!}$$)g-wNz82 zv$5jJPE-D?Bx+q57ubC?f;$%KQf8d#bd`MzGiyk&Rw&BGSJxfuusGEj#o0dC{H&|l zlr!aGy@WJCPL7&+4%5i)uI5k8#!zx-L8WzLL8X0N$b#(qgV47>Bc;_SSPkIO`2gNduS+!Ky8lLExh%^PMaJod8$!W)b5AgGocIzc zTWDYOUyc@%g&p&Yd{v&$tZ~U_jaX2g9H!zU8=UFqfVRn0w}Yj}Vk7ZyQR&YQiMf2E zNJv@ptusG)XkH~NsmG9@sz@RWzqFKP`}KKLaHhZ`*6`Uz;zoJKp6K1jNQ{%`^Fu96 z;UX4F#1xjzabkQtOZG?hZ%Oc*akjK3c%N-3Ax}F>%1^|ul=YII%jPyY#t8cA zNAE7>J)>{x$`&7ij!?O$^Oj}ag81yrlB5!{(NPVD2>{CtnE zE6I(NZM1*Dvc7!Ef|$xX;s#{s0QvgVN}}?dyMLHGf~HAGY~BzNycD z%xbjx2X6ER>RvKKS}va5dVJINZ8W>t?t_6P2N`-g{rp#F>S6aOvI0xaU#H;3$=GTCKuBR$!_S$|8<_fyH=#&~>( z`TmmZWV0`Z<=CB!#6v7enY-E<@i$R#&0WVC@#~Q*<-<^T$MIn9Dnaq$lc!egQL0Y* zyg1Q&vNfSLv@FqnP{BQ|?H8p~H5E+@6dcZ~4@y8K*NH31Z^X}MrJ5Aa`|6%8uW7UL zG>W<>N|MSgfT-O9S}7w29;F57W!+q^VToj`q|<#ZcDi&#w5NyBcHR3~8bOWpI+v8Bge2)X zw^n4zhaywH-JcaCKBA5BJz(Fk&(EKewpZ=HJi8a&=Z9?nacTeA{crzmqBwrY_8*e= z|9Ah}|HTj2{s88en)Q|~k3y)LWa|~-dge$mJRy>A_hyGoCzd$c8}SA_xr&lxIkGPX zd+a4yH>fb;V^DTlkY{Ih*=3Vwg-@OpK6wVy<mfC4n!*xMU1w|K z;mKcoY!VWBk@l>F!t?A1sX@c%<6n|Psn;DT5Z_^NtS(ADv)IP&Wer)y^Zrc zzl#x{4@dDP@aVlTbgJ&*sobkzo;wic!)m=kwkY;*;_Gl%v3E@k8-bVzUpuXrJf!KLW0O=nR|Mrs40<{<9(@WPS2OT1?7P zkF(`@z0f23k7S)Ez69@;{j4mk$@;l{n01dl3r?N*aU`lw?qsrOjo{CD&DcAT28RwO zwu3i!7+*4Sq8~N5XJ;oZeobcd8vZ!P^HC2->d0?IN2Dq}6zkJ|0XuO$$-Vrpkg?2G1ip~XoFs6?0JDaTBXhc@_Xr6Y5CeoAj)Y0q99+#xQv`D(2q!zrT6lRmnTYPVhdqazITiMw07rfnJEz0%VyWEIxDtD+V=j&IU z1cV$(QoNfW#zI@oOZajHE~egW*X!-g)M1uR}@f zR;#6`MvnK`ucfF!jYc?&Q75AkMs6(IAr7k4QS3LW*=v2?_h&=~d88oAQ17JK#Q_t; zUp!iUmr$haQBFJ0e-0*1Q@CqTVH-zWXz^nByEFV~MBn53i)I~d*oQOrRFiifyyJi& zegr=mj?9p(|Fwzt*xBC^?-390$7lHc|M&R+^nD>wpB~>s<8t3@k+f2hYFZ@Cm!v0K zB>hE_{@5aEwj|AxBo(n?z3tBgQG^)XnmiGn1`m63Sq1H|RgG8Ahn(B4iS9^cQLHm_AXx{mO|5x*6 zBBo}W^X13y5lq$WUW1#|zL|qM!(uodOX7a<_+9INC>~FY-Ywza&hkAI&vgGntvWs- zi?c_f$-(|wZ$Yk}p`zaGH13>4eyfp_kxRF5PSNyNVog^^mve5&)zKv!?K3L+V)B={ zv5*_fS_t=AnVf0P`&B!;m*GY3u2bbg%5_~7)oXUiEGkIuESrtoS8DO<33=P)Lq4 zO1l;Q1#K6(kCU|cmo&L#qCnP${7yT_j>-n+d&zHYtUn;?<)6f0Ecs@9q~%3h%H{qH zK?^)Q_H8mRbmo3eV-axa@QYkFx@gM$mhg*r8`9(X!-QYBYef5s;2_aUIp>F2-Vv_U zV14PFhD8eNmF1tG(6GAI;R;MYcLZ;XZx!@U74+|)f3g=mu?_X@fED~lV9LcyPJr5U zyd%30dUAlL#cnb77jgy;+yei+Ua07X8?{O+GU@Z5q}15E_E&`^&$`VJa*9LGqSd)ga!-i9h; z?&)A8y2#jG9JV{r%|u8(-x}t)zb2GTv$29>mqhFAhvFWnU~6PY8m=fa66;CT>uN*j z9}?~UN046QXbiQ$Q#(BupRaXl4HWN#3Rr1y=Chy6ekrYeHtP?OPGHc}JkySnptuyX zyL;ze-L24WfC`)KY#@u!)J$BAj0i4_OkB)4G_0!@$A}jm!&)U6Y0=15TFsVTzBk3a z0!Xd|$}{d_abtk8nOs>=znsb1td`M0QJ5a1V_W5@xB>2yA!fhmG+f%&0+@Iwedj1Tv zow+2pvsDt&qStE@4^UBd1ke|0_M1J;eMgOZjR`c(NGIc z<==`=G0q?R*`DIJFnjQql_q%n^s+wrM!bUvYTh`zh=?7tI-D|QTlM_*8Qy5bKgxln z)C9|4kEU=qm-KnI7sZX_U2sw zGEdAYOMT@mdW#rO@=GWRm)4$@*b%Lh_j=)SvR47DPkmVXTqAGLk1)r6?m}(8RJ@{g zF%>3O-csT|P0W)YuOW=hg|V@gz(}ZQ{5N=D#p^#_M1Hl5P$%~g>Ja&RkE&+vEl0Vh zv#>T>%F9{)Mm&cWRry`)IAb5@f7mP`>7ViHwkI!JI8i#g`zv=#+S6WBj*++v;O&dT zW;s4~E-4S#U6TDiX5Z-KcKG_Elkn^t@sngL;1)(wj;P{hQmLt!3tEd(dD{8U=#a%8 z{$E|apnC9sITpYk-dJDRDEP713_koT>$I&3vtyR@S$US*neASS3Ws2KJQ6| zoAVjEml--QkjgdLqQ=&xv1a3G(M#p z5dD||qQ=kuB>VQre`uu^N}D+DY9ZJ z)NMa2nwD(+UB~{mK8La5`?ZXaH|C2;w8Q1E81eBWb0N~F<*!Bm?Ow*qem7f}>noZP z)TbFXZ4#m>2VE%P;$A9&&)>=qTQPfg7JKTyz1&`MtVU+hw>Gv{p|N7IqBbhm%tQE1 z-9NAcPtH*teitV)q>tYo`-+#Z!MRNQKG}NH5zG{!DweU*8>TW^DRJNoIwRYc+U2b0 zz``C{Zyt9abylhGvYqM}<+YVSThA0&zyfYI*Y)x3{#Ss(Cc_kS9{|~UPPvaA2m-X( zy4Kwws`!Co?Ph6dLTJg|$jle%y%8b@%FNDmv$^E?f}hcd!rQqTZ{4M)Eh`b+Dz%SJJy=rT zB{f@oaTB^XR@sQWQR_=Lx{vRby<7lMLz|sFsxe0Y*g%52x&FYg zcEo{bsdQ?A=$beJ;D{9n@!psO{-VYr;YzZtlFK_@5o}g*O@r`({hbC>|KS0GbHM@T zzZ|Lj?eMhboJ&P=q%DVE67!-7_7|F9H~)^&d%kXe>h>6A*;@CW1A=|6`x7Yk+hOQXO~7gWy}Ey zjux55{T-BWzg|iv9i@E-`nuakc61y-FJ2+PwN(U;^7?h9PJ-e^S&{n*0H{d41SP#e zliTT#n-ja6a{8{<{|K<>>=(8(W9Lzb`y-)`Z-uvYxe3zm?@;dRT6ChX4O1=+_Dt@X z@9`|h3%x06>Fv_iTi=5y2|)imEoNrr<<7E0$bR>M{jwoj_x44hol!yxM&cweW-WJ* z<0k{GcZzE(@#cFEVEpdn#_xu*clL`#v(8yZ7-_e%a`=COMaul_-?dcX1rIZVFLjwwef z9xg;%9-=LrcCB8qgigt=$;#JiE-SpE#riDom{i=d}HQ6=ndx-Ohwhw z9CLAh9#bW)*Vkw48Oaylz4>R#wc*}u%eS+)`>Dmc$=tNPo2^)n6vcXix3vt1q^}c+ z2C2HIxSS4A>j4(9T|Ec$sKp8kJ$x)w=rbh5KP?LO;?t#f$6_zHu|C!(-M>g(x^F+g za#V%1*_OY!U%EjKd4rbIjb{AC2aZsWY~cIu>OEQh1l|AcG`XZq0NU4r+%gKLf%!fd z$ixd|v!hQ%_fpR0Y-grF$hzV9k$V|ICus3~0>?e8G0m><^?g>jc$9SdQt5Q}Z(k3@ zrGPd3gY@2`xiQjmTjM{D5B`LbLwVDU{t)A@$NMMJ_6cp%`wdymul?!=YTlpEda7mf>#2QyXz`6eCR2RLuXupd={YX*-Jj<-EzF+hy3T!)^j1V-8o%F* zq`mXQG=3)te!B~PmwWL@O?uPmj6JssdJ9hW2l(*}$2>Q>MvEaZ~Z}Ubt(gy(Ir8n4X7m+}{2I&q;bNo#e_P{Mqpj>cnJyo}fP%iq*C? z1fMNvJp|s|T|-~f8V>wB?ZCK!_A*`HSI1ka4AOgd3A4n9cRx)6f3@RTFq3Ken|2}Z zEvz`}Roa!abiZBG*vrKvr@eoBp;jGrKT0F%fDq(1eaCX|fmC%rDYtv#e;-*=fJ1d$ zWRmr&J*yD<;oxl!rb}I8-zRT)kT0|QvqL;O)E*K_Y_@~Z&fYnIedN5pvp5OQY%Hzk zY(l8OUaX>u?~}igQnT@-NEJI~BR_$vxXs-ej}Bz1EcTIHy*k(QZXLCAC4*Tbw}q2! z2}+mFws!`#KXr@q0gPp@3{R?c2Gvaco$@UIkc!STST)R+f{>-N+$oAdxz(_+u{U7)hR9$aV`?WNp;JIcaa1| z$SJ=tL`=;3lQdTDM$Ykg)s=FT#+{1PTVLcV#F~APtEX9| z=5Z-)9~8VJZrZyTl=RM zU^8)uCo+`Io-RSzbFH3hdFCw3S`8oM2CSTI`=PalP;j{x4InjqKnI8lHIacHV#fA3 zk?78B%m_0Tzf`kemvj5u#?D&BMIY6GN|TG!`d>C;?)rZXG42YcS_`1}1+dS#Cg}NT zz3ts7C2QjJg2lVYN!AKq9>^KRTu>r}30k9H_$SQC{^V{*2`vRYDt{F!gF$N}H!T)? z4a|tkbQQZlTR%%51@o>H|wHoWJ$rU-_(sY57(ozKU`lDIAt{K2*1Tu}qIYd(%W>=enik zsnz5ojQtV9NV8)p5?l6*m1BMt)}@b3^IFsV9lQi=_N#H<6jdn*+;u!<^wFmDy|4_} zFZYHZZc;(^Xrx8zN&5xbUZP1Z4f%G_v{xZ@sm`~$RaziSH^fBiM+_iZs_ zD-M@D(?NAm9SnovMfa!5HmpXo=qL`Z_q-gda#>XHevQg-hyZeQJL1}Lxy{SI4 zxofLW75FL`m$$@C(xTE*n#ry-?PVOsT5Y7v3rT8X&dPqOmd5J5r>KoYt<|=epmKhE zT7$CWfR|FMNo#GT+?x}^-_99pg9IFbOabg$Mp}Q%U;}2l@#|PqcYW1;i)Lh=r<9!r@GK&@GdPo_pf?t0sAlG9Ukt@{{-zN5ZZb0F>c ztoB@}b-#i13oSm7mi(T{{a?Cg$t7S;?zfkz(bFt1i=2^m^R%X!$UoGOryU$U;j8NY zdO{Fx4zo7LK0a3u3|rjx760mOpNYJbW}}(SRR_#QAItccN|k@<>mc`am|yp_-sTuk z>n%LjTjXHsS#;I>y1jSBeK!4weU$C<>7McQ_{7-O9J=<2Y<#q!$OojxEUU+-MSq@T z8fbHNc4|?xU&yy_HI<0Yae$`8k-`wgA9aBC%b6Y{_2nl)5*=gZ<0I|m=0tt}m)veK zwxklBg5@7r+L898uzg#|Jhu;u4skLonC)DU7%P_Fx9d&$rB>OCP@oQsy~KzMeHwQ? z$zQp8;3cZzCb5Lvj?n_Q?+Y*WajW=KcUhe9M#@pQFbyrqm$b!CAh?%2%^&A*QGe8+ z@6%Qn4K7WTo~`c5bUGy5woH&_j zRgNFhs^z|0ElfSA`LEY|=Qz>5(Abp1^w!>r*zvTQ#$%SH3|B9()BvG6DW0*n{xm&I zw3;ZeZYlg{cE!->tJyBGV0y%?t&48LUAK zmOf+CDn^315^K(IYPOO?`CD=Iti(uu+5bXR!HdKy1gsC1^c=H{#cjWX+M=AFaF+D=AoXZqCJ$Y_`>lt-eh^I8j}nkRF13 zB9lf_D!E8dQ?{~ft+^-1Q)Y}gwN|59+uxZs0g``Sp+}7j2BdQg)duSUtRbiG67SYkM-$x&}w@6=a4Ok;t%sz(FK=N~4uzUpa7sIPiXeymTes(E%o z#=pL*T2kt(mg%1b`sZc&v8tX(R&A)STA|a#&LQJ!{qv^&Y0y9K>7Nbq<2?RrdQkPI zJ&dv+ON#N-iezZHjF-l{HEe@ZN9-rV6M9XO**}`+4at)#-5RyZdPjz1p(35RV*%Bw zYOt$TSc!4c%Y;r@fs13)qco>dt*RAv)x*|T_NXW7;_am0;<62)1%$z3^Qd|Ds0Cc? zD)%KNfA(L+#-Qtt+)>i0Y)8fU+-rkWDBBUctrN?va-}zEbJ*F$H5VM9?jA-;?6%Gseu#z+kPbI5qlX#qa4- zP<@S3@lk#AdaHUEFM(IP zaxp)f2)KtRjOTH|@;$=S)U+{|cINDQAD0*Xky178Gkmrcx~#lK zA%QBih`;KewZ;l962_*>yLU?7bQ4eWMwmoDpaIi4$;)$+6CSd*a$)k60<9JM_>xPM zCx6Ld@t?c11yTNaHKK2dzC#c**5m;6o}rkDdF%-|9x*Z)Tk)vG`bhiwTsp(HL!_wH zs70vu-zSkZ3%9&nDMxj)=K$D|xZbbQo_JTQ(~jJ9Cusj)zT)94S_B#31&v&ct1BOE zJ4%~Bu_{WOt$S34!F_Tj^%cL@R3?G;LpcLssbsNsI&q$ahui>I^>8IfDh(QL*TU4_ zJ^vsKXbgGj;9#qJYL)!r*4dJ()!(I#;sHV$aLw%0v0l-^rhE;c+Wa?M2^r z8_80i%lBsY3m(l)d6Gq3mf-Ce`g4?MDUKXj%cUk=tIZ92%w>68cBApNK=7E_Jlp>_ zO361o#yi^O1%?E2h$n`3JBQ*7vVOLVmaRHx!`BS|fIt_I$(+xgk7ntM=MwG8l_5Xzl5sHZ7S@wo-09tSjw++wHh{;cvc>(-up}F z;fwg)@Rxx6{%o1g{B-%)?y>qd?bz{H8k@yN;wfppnu}x9PS42c$*E@t zt<@+do_+y=)m74&#_0C=%4io9yIFD^hk?wR0vzDQ+4u2<&0i=B|$V!l>k>!CBpC z_e$DT;Bq#T>i(PNuG`UBC^M(JV~u+b)T5CuvI*eblPRHli)C^ZpY>2lynS#D@zpH` z>sTXxKjmbw#LvJuooKz19wqlfh?Dy->Vn5+(q1L)5r;0>fgPd2mE1l}8|BkhO^n)A ztLg7AttOS3>H_Yc$Xo}pRyELX;aVcX@PhwolBo zeUVg`GnPKM0|YeahpOv32v#$-U%3KLSIC~WN|;9xwTtrYO$KvCP*J}dqP!X)@TgJR zvBp~MhJnL++wChqYur=FM50{X!`mu>@xcejShZ&c0>y#ha~~Wtw`}#_<=Ke4^lbzi zhyN@Dy`C3kTo`{YtKLx;jO5bpj^)LXcAaB<+EEvil$@QlOs#-ChU762jND|d2*Uwp zYzi>Bw#kQqB zyPEw-A&f=261&0r#?NkEtx}TgMBwg-L**+(;llE^alhaR0fbATebk0OUJx*MH^U#- zNAj8r=BoEpF#2Eg$DYIb2Any|_aIw{wJBSl%Hd|h{z1DSA-Py4+rpKA>G}G|a{vUV z?Ep+te|?smbe`x`a+?$sDPr(Nk-vmI% zXo9O8wTee`QwUU_!hVTdi;7Oz1+<$&$dV^`Vt_=zbD5q9*5=jr6=4<)|K-=xS7!!7 zE_NF|f}@Q$6+E(Og(KM&-@&uPR9zstF7&Mh)05YH&xf9NpuYM;&G-My@ zqhc+$p68S=jdn_1$hM7lBBd;s^^>(%@k=Yz4b^RXwPol{ZfdzeR72`sgv74 zOQ&zmPEs}q{tyS zB$Lks0+j*#H}cn*KO&r*DNkk-iohK5m`%l8W~3fCD*RalQg8GV6`E|nFmPjK@@`wjQPE-rnqE&Lwu~I zJ0)vym&z;%cW#r+9{`c2$I`92;dE_cOF5TIhDBtM(WdufWGJ^|IH@^t@$hp3*e#eb zc@qzv;mWhwF>s3fst;3Ar8OjsxeT^Dp8siksR+921qZ4SDnj4y4LYSU#Fh1dG3$PL z+F{I!$?tmF4dc0g!&)BgQod&Lu$^03p07!DR{WxT-Q;d%o7r)>f@(t6XDoJg*x6vN z4eWdusim#_MJaQ8x9H{N>n4WFR`y}aOj)C}+0-lJXJeopq_{!Z+MQcDb#9CLt+O0! zyS%6Vw)35xjJmlluyehObEbKxsAeG^npGq2NS+oH11OQ^O{LDde_J){KC|48@2>mQ z+D3&qH|T75UCMITeInmFzGb~@ZP^)N9YXzO# z4onT!8~iR6JXpgzGn-Lck_a)V}UDoo_-GKkjy3N+EbsIW{K6`2Bj22kwbp9uvz3%%hqSVTMavRlA{H58Gcs&`oz4l^?hj8m{|cx($KokJFd8A%-E|~1Dmcj^vzGD>N-IVkk^>1#;7pwt9|kIX zO~yV0T?iMN|DHrELa+?kLKd_r*{6C^X8d3vlnpqWzEGd-T(r6rhZ@v)^0t{K>T!6Y zxcy1+CGs}JDCPr0f%Bx4YHW^lwc6A50>%`96T0FmH$iJ!e^9UOdDZ+(>$AlI(e=7X z8tf!@YZoc~nqRkp4?g`X`R>$_V#(fVRj|7*2ub1$>F>QXXeGD5mhMuG<)is2$Pje&^s@1@edZ;)D;1oQhV^|Z@mF2KQ;#qwW;5g=0xa?BtlVP! zv(wJiTneucm!(e%c}VH&D9ucgLBIKU8d4^vG$_OAHJIn+5ryX7TEMbavq6A?-m5+j zKmCy6CBi;H;3P*-!aFQoD>8d`9 zc6)Gre{EAt;<{kb<~i?=@-bcB!;++D=v%&z)89=B3js)mw0viL!hnC09%N|n*mANi)ngOdE4urX|nd@k?61Fsu(V!&WP^@Dv9(kVoiC*%=dZI zmmV(=y_ogpuAKb}FDWFj>Z1vf%(bp>pn4SI&|C~Uw^Y}?&>z>GV|Y+k&!P6F|Ye~ z$CP_353?!19_?YdUmv~_Ji*-Ln=ZdCUVXD`Iqo2`kDqP4Anm(nOIe{W=bq2#Z%PSk zc{l6DR_keg`jy=+d1)xSCn`TmcAonqN_+gSqB&AzZkSL}9659HVHK5;V=IP74yx!G zJ=pFyMuq`PiuC+u^D=@`;Bb5zecb3)k$>Iv~c-qYt(9M*sA)O zBFp8UVWQ*)?7rjdKRn8VRrMwlI=Ao8^4dwiuGfn_)@lxxOI{VU)|ekv$97dlj*jh$ zMXreLLK&S)nOM_l?2Tq`E>~G^VpM6z+;|>(+f#pdn$N*nOI2MST|N0|yh^h5_UdtV zOf~neb>*8ipLNTo=f#@3P}7t{fy?@!en7oqAuj6>q7PdixYzJx zeZT_TdfruAqw!j$+gci5D{ZZ^#$4z2Cab_dYE^RD$Hx4p7YNF>IDz~%jdOS8F)F;lG_hBygBBeT#wPqzb(S~s8& zWbUSztW$xW`T-3Z#~st9()?~;#oxuU@QSn1D&W4tdwO6zdGHP8-Kpt8{DvF~M{SL- z!jA%9EF0R_RFoq_+uI!|Y13Aq)^TLZDRj$TB2&kjw&KN7`K{-)?`@AM(z4W~atm}V z>NDwfOy#eWOe?Gp@Sa)=B*onXndshq1H^y=`QIuXX>uw)*bwZE958O>eb4V$* zqSiWxxu?q%Lyg83$WT_KtipX{r@}u{p`Ht^k(8bUtv_k<-PO&Qxs$G#8!L_Y=R7F6 zyKckIhK@_!57J1H^HsiK{$$4L8}f+E$*2B^HZXPF^w67lnL+R{`zi~3`y`NWsnDw? zuB#CiA@Zmd{Atu20DtI7ok_(XWTIPzZZ{HSbY}0@Xtqm_<~QYMI9IXa_pQUZkMYRn zXEEvm-ks|+9POt4sa~C@)7E%{dSm9Nus207;x5Uim)CiBezOt3Vl_Q=oX>HzNbt!a z=7ufqGRQQepVYPcFRrdoUArCnXS@E{sehXE&u;y*OaCe>Y!Q9nWblcRrf z^-oCue60(7B|l5?lVtxJb?J*efl-G*qsMI;y6y4${@0NdsP8{qe(e6NU>osC@D%Iq zzKZoGZnMo|df5{1Be;c4I%{V&zY=a*JPa*XCcZ>2T3?Qoz^W%6{C(Mvo|&$wG+ zWo&B6X+i5L5rWoRMtmo*GyZM!FY5(a;H8trbu(Y&38##lN_ndhk4cv!7(h5#t@oL3 zsiP_`i3~=K8(A^JNEGq8Zl)|(0#$oHqs`qwklcD>epNtr+^FhO|MuZuAalicqCz(5 zY@&LkUY>2tzfw762?DR#4q=TfsYnhBT3GzJ5S(9fw2QSIFe|K?^Q3@nrC7}`mV4rU zEr5>Y04w~2tZG!0BuD0kEXR3;YK-|arT&U$BYrK}%nco6k#;zUGVZJ-p@Jhz`^sTlWC!a>}Eu@2+rg`ch|wtw%9-YqM!2o*{Sa z?2a4?EDyH^f%+l!lyi!an4_L@Nwd{cC?8j{dTK}76rSp5J}dz9U;RuG$-?+pS)D0D zSDxz!N#HC$VT1*_g+9betsgW%r&sdg54|h9BNV@VDUVcFg2J`D^+qT0Q&Q;j0ug-5 z?(yWmj2IzF*BJ>BN_M`v^dM%GOhfArFH2SRBn#qweW7jo1+!4Dj8!n%;|vM1WeY|kmQ&g>krZgSAt_R{K^|RBBYulqj3hXwZVaCs@QQOw1)z$NY8P$$q5WH zeoGbk^&Rf2D5m$7QwgGP=Uk65_)@+!QA-mq39?)X=RhUj!B6bgudb7brubztlnmgR zpoDQ-rly3E_>_*uZp~d_B;FTH^zCiWUPcvdjZ^&~rN`C}lC$sqk+Rm+4^sQ@>Ie0b zf0KB7}Miw8Od3XlEa(KrE9E<3X`*hdB|fpIZNi7 zJmx27J?uU9OwJMwNm6?!X9;#$Bj$ZIDT6A@s;!HnIPe8IxfLybiC0> z9L9p2PXxsFU+OwJbkkHJmYRbP4ZVkEAn#B^06g_Qx*z8 zx`#x2z!N=YER(i1&Ep@?r8}nTy!nBfFZD_mJ-(nbl0`MsA=9r-Ob;^*+|)Z*~XYLEJa?2}X3AG5_7 z#(_G?AvxOk+hTp;e1)us{r9kzx)OgpoB3KGriYKO*78K#AI)8EpTk5c+EIB;rQHjn z&SmDumMG-_21Hf%S=JY7-6d0{=yR>+x7O!G7fb8k{hP%DlNOcuFGPR2^BMT1n0t2@ zO7)EIj=nS=deKO%;7hvkCdkANxF0h<_&b((_;ybxne}k#pK8prPXwMe+nds7US;IR zN=>qGb)#6R{c@-0$mp-k!jDW&&uDrJ^H(=o7KoJk2^);n$85Xk&d1w zU71Qp2hq`jnMUF@NG-O&!*Un-nnP&q7w#w;)A)0StCHJENSEwINfBD zcI72)?ip%>q-EilIKH$$*Sh$h@A2oBY8K8r7Ns2taWxI-c#+O zCy>s#YYORh?;BAkz9c)tvY2GK%U7)OjKW>#p2fSShuG8(lD|7cN#s#aeqvM0 zB}HP9++y{va#Zr@UcevoQ!12IeHE9xZsX2(JHF*gl$L6rd3+g-?)*T3f39#&p;z%S z&SyfjG5?k;`}TA9U@7*ND%OEVZ@ennM+Q`MH4=}J#POHZRnyq5tgf`q-3ZH;XkAD_ zlr7o7XWYqNWR$E(xFT#MCQ9RHS?k;d z#e4coIrlU&Tm#rK&30yKlts@8gLx_9MraNk7iT^v_;Lo)%S^M3{2^;4=2$|OnPcZD zQ&39Vm4l<#>t0c5k4U&Ku6u>m*@Kiy_c#hrjon_Yj(?bzN3DSUmolZ)hWTVccL5pc z=6uY)%GgVaQBFR>d2$_yRrF8eg}Lp$FH=5&MHbWz=yqe!3O$5qGV8+T?t{@_M8JT+ z*aES96k3ghFr69W#_{R)8Ovm-l+5=5^!3;Mgl~ZC`=oKMc)yfMMy{@ zlkLtDd`FPPFYH5$B-RjNAPd|cX<-hLX-EHSDv2s8Vovm*Am=1V*h+DIyri0(d=Ky_ zdMw(KJc?c`|E&IO+j2ynST_uvu}r{HBOE(N$S4r~Qs9~%G z60gfksCf<1T#-t%F&sUMdrA8WB|#-;GBV!dFE-PU5##LJ2A8eGwN(0c`Sg4uwyig6 zKFwhb{jLRY-oT7y(mv$0)HzmQOs`KW`vr{5*dzom7dl;+@6O=S>jrL_Lf|U^&U{XD z8KG{3>%{kLfC0DpZJ8pnyhRTi_@RfVrwfkYh!vJkL)ir-XoB9V4qCO5Uq~0n(Wjo6 z38RI0^rOv@wz$Y@C1S5#^gOevossytaHwp+4&o3Okk&)mI3f!;iC1A|RDi<>=@Drs z`|Uu6n%sJQw3UC)S}yCcJ?wttWElQV{>sR-*sT)+ktqZRcf%Av$7A0{LM$(XS@{&?nyli=y~=9`Eu&St@PX%V;2n8ar_EIF*gY?As#}> zVM++!vHIn!b=S;z+vftURc=ph|CKd0_*L?j5KFD4yGmaV*3`H;bYt6NM&idpU7C!E zC$GAuGFoVl%4cs$H#WH_o|Kjkv1uS?qn`|K&f77N88k3I(mpnQLLhpO2W_;#7Qb8G z$yI}M`(5Yfcjl~0PfvY5qU>j3FALUHzS-2}_LH@V^*iR{LM2S2U2P^?WNO>Vp3j%YET3#}ZfK=}uz=1|{1$EM$* zJO)l8b0i+7r|F;>lsNMtYRKbYl8V4xR(wHOb?S4iS!n*I>mQS@Prr{$zh64Kta^kJ zels>z3iW=PytgC<;S%;glJ{+oBm2aM!BiSYa;5Bm8c;IS+Ta9TEeB1K&6i7x$&?eh zg)7nyjqS>b6xR>9M=5?p0GLe&M~<_9`(~V+v0cGgLk}4nU2n(VjPY)fO=pR|PgCMi zvigU4l6f-T>OY1@@h|asIlx~>GzuwdqV*5WSf*fZ5Jsk-m6J0IAFcfLYCZpw+~%CNH$jYuJ8+heIK(&J4O%dhNVtKnv6B@ zgsH)Pev808M_{P$pGTts1+DH6v{zb34BVuA-Lq|vId{+&vLpJKE~9(z6p7TE`(Nou zt*y?(p+hQ^saDd~SYr=mqs*q{(9R)y)Rr_9_Dun6m7rPg7rP`m*CG3G(3)Iy9njJA z2QxOwV1iBO51gB2SLJE==E!#}iD=jw$X1+uYhXScKCYpQ9F%Ti<nX#04O_4k3GT9P7*@%X6ybW=gt#Uj|$J&G*;69*H2FiJ9n|3k~$Vruypjk z2;%5T=AL$ulk_xTuzvxuq+jNKtfYNiXV%Me8AdfcSuI&D2>Z* z@9|sNY6;URXl7^GE!WzI^Wm^$oB_>wR^UQ;^<+@A`?`u&mfmXf!GMJ2f`M8yR( zS7?2(=5sDcG50h_PLb`VBL0;=D$rYmxb9N(<}>m*E<-~`?lZX&>5&oxsxYyB`l&j9 z!9&nE*+;R-Qux<*-auT#+O#H&KlnfCY zN7$pPta-WE5cz*8B)?D({DftZq2g@q5NpbboIbfrtSL8=A8QINkmZeNmu*BF5Q8nX zAi|j662vubStj08X}4MYUrtd`hPJ1FQ~&WgKJ56(VdBCHpk#>%%O@f%Pbxcv$|{CO z*?X?#bH9*Zfr<~*`%CsCKJQKKcKH8YeBQQN=FE4G&(qsb@DlViUKqqVGUIEUTw{I% z^Ilf$RE%&h>&xWea8lGT7PlK5iw}^Y+>2er2UU(mvM+VfYUg_Sj(N?bEL3bj7=Wqw zfy`#EjOj@9;ieK;j&ni{eyaHwvLhom$f~$LCp$L1Fhn+ z{5cx{Wz%i!=H|f7+fiSPI9o=1dkA$%eb|}2s*|Ee|DC7@5^&ixT@pPib*?|3V_dOx zuUd3CM2a?D5;-o`G(75XH7yI^MEjzR_bMTH&iOqIoP|Uw|305IbhBe;6FyAyMq(Hf zOfSRs=gF+ianIm4*)PZC#0Z?PKEDv_r9xc^89zh#=5iM9z%6x%PFLXl7RfJb9^NJy zwZb$hR9aX5IyY7E2@(BI@T|^*5vF6r*OF??7a!4QR-^N%hz_bXUO=MPj-RoyvBUTo zO~}pZ-Y^kPUl0k!hUKfP3>e)W$sw?q63Wh#vW-rVAG0xcA<#EZ>5|NYIdCL9AP0z1 z_$&ws^su%jY9oe;)*CFZ3QW$mssfyBmdk3TY1a4Q{Ns=B&{(rE zx8v&ZR#mifJ|hN{R%4>`Ej}U`ahD`)#axddD=Aw+`1zetmTF` zp!`tegyapO(tq{25i}mjit%xxWHecI#1}g!(kk|^SP6w}9{3|}Y-9rXWpP+xk*ga8e#1*1|7=-swP@8f-E2y%>D7kVe zPhQ-GMw_RPz^?`%ek*a|4^9pYojfq)T*B}e^BdJ{8cpK>R+zjn3{ZrkG&-gv^$(rQ zrq%8$=N58qaeC;SMKYOG&MnSmB*9>d$~iXB*KU&Iez+j#DS-ZERR>wI4Y_NwV`kw`j#8B5)DWkK#DlZSELgqW5 z=ff-v5WRw}G#! zy7K<-O)hYOThB$Kq&1b;#+FFb#A4-2)bJ9Z3Y8WRtYB$JGg_IJk$V9vB*A;5IlUgl zsqHw^&Wt|Vjx)70&eQ_FVZuuSs1(GPATNSaJ%=lTAP@qQ=lk2|+z_<$JfHvn!H>D; z?6ddUYp=cb+H0@9_S&$fLI9SMHk{uQ1xNMeHFxZdU?=n5TTpSFaaw9CCTvjxZP7}b zh=j~?g&5#yR@s)1vWuRMI^!e5#z)+<)$N=nmqW=K+fBb`xt5Ijnl*0-n8S6em4u%P zMQB~XPDztAHch&to|CbH@*}LwPxy=vAz#d=ujM~gC;J7-M{DEsO`Z4; z1{kOVsI9z+dUl)q$vA2TR!K&wzNS8+X8$;vC|ZF)g_9Bs;pm@H3}}&;cH5&+9qW$O z#yk7Hv^z73t*RsH`?2Os+t7R`jZ&SX{5~tSGBv!~>)Qj_zbYynwI!+!As#@Fy%ZoN z>L5G0B+7)&ELTbvT=%{LD=#^rQdUNuP4=$bMHI8&9>%z)R8e>{W2n7GRO-pNhi81r zv}WM;$%-R}i5ABJ^g;?BbY2FM?r>;w%YO=4UsmMPoyR z_cAUf$t;wd?5Nr=iHFoRllo)U3)3Rfg*k*yG zxyIeY;ts^}ti{)%?yE}cVXk?5F2Ad$`-ip13M%W$vQ;vLWZ5Q&30rF7n2yL%V=89p5#x zZKyU>9rf;F)Vt4tTir7DK8TpmUO?7vo7-Z?#C)+m#)xfv1pO`(@wC1(s2f6>uV`5G~3xnJrg@LRqRvz)$`C@9NcRn?rmOoSI?kLnU zB0=Mag4~%MO7-=R0kZ6m-t65+2l)C2+09{Cvh`Kq>QuquHB}o#0%2(4{hXoSv3MvJ z=SjUz^)YP(ElpzX9qg4+uv7*4Acxti4L5sVQe8drF(@w%9m=swek$7izHS>6FYr$# z%H9i{x+T{mpy7x-ku$#n^69RBjJG^Vt~OMHy!o!zzTM}#eoj971^Mg?Z^{$=b6vk0 z5c#e@jlVnWz+dctDa8NB{x{G*+y8#z|GEDOjPTk1&lfZOf8H7WFMHfr6Xl}E(gGzy zfn+JQ;i$XmkhE-bzP+R*-+%u`xQ2W_`ti*2lv5BQl{|q9-a>AP|2_LS5AvP{gg2vK{0Q3E zzw#LN$dBKFjo}~sambj*CxGWYK$=gXG3Es7D=ex!d05zMR!U+T4@vi%Y_gsfdb#xJ z@!*|c>JTI4&yyMcE6uVSn9@o9HR3qA^TAoAynA<2L9hJ5`p1wRxl+SSHx`~5n!Us{ z6wkZg^kSMDO=Aqx{7L#LrZIS)<<7PH+-A|e5?Y74Z3>frYeV-Oe#ODpn-EGE4B7WBZX9E5}2-4Ic!az{E zJo)>)NNRe(K#bV^WF;LRk$!14y-T85I~ikEi|kqZ(5#o`AeGeb{3@=)eUT)_Yl%Ic z!^b$lnzh8U1|7dDD|3z5g~ONrdVnJ$t~(yAnGM_Uu$9>H$C;IpV@|$*t0A*tTkad5 zNqT+&B(Dx~R2_AT^LZ8y&9lwtq~HtxDL8~z5!X!-9)A zA{=e(?a%gHsD*7x)m4YydsQ7uX!VoPB*r%$LXF$!lnrNvu%<^oSu4=3orkWe(T*wy z4p1E`^f0`2UtGW6B%x9dBZ7pha!)_k-N5EzsL`*ZT6J8N`$wPf#-1BD8UlNbV~U*E z)48)aQPEy=l0gwlw9fxBhb+3flR30_o9GhwG{N*e`_BXnSbV+~x?|$)WB;HHLv)EC zkHiM{NOXS@V)aFg)hTftmun=soY*hKyv`Jb+K7U1#9jG2NX5p|3Ac{e4cP_p+;3&C zdrNcHLWw%!CmnlJe$VbN%6{M!AA3@0d*S`u@z-4}H zKKgcq5aqM`{P(Gydf$}DSm|$rLB8O30SW|ZRz1Le z3@}w#2lsIu>6-MOAy(6I0A|O;try3p;KS<=5DZ@Odf=V6b8xwCX{xQ?eoB*KR+EY)z$+Y|PeO{Y;Vj;WT{kvIyC0ycLz2hKKw~XLNWBJk}4V;#yY#{TZt{ocj{{7%?0n9qc(;Fk*iUy)7& zdf4|rXZl5yf$t%2wD=e@{}LOYO4@7mX|_I16HMq0eVoxn+3&`k*fQ4JzUTp)ZbZ-7 zQcIE_GCu8ipvLxW30OA zDOri2cv^pwKD6Y+9RP4I{8w6Esay@eHG`}&ncMG&Zg-l*8Pha4%YIG93`sor;Q+608n89f)afAgg5cglN91r)j;lv{DiU( zm$m5U1(V4h4B00_+GzR#cwE85hH_UgyLE+tLay1)cer%`f{mx)zV?fTD99Z zr}pU2L50rkVf!glr?q@?ID=RfcQtfEf&f_S@DN|0`>GRr$ys#WW+aZb+5&2=iaR6`{PunUO`nL?ccf2P z4R1RWe;;z@T(>#5Ij@h2AT%uG-3&VLpCs2AB)`w`88hH7Ft*p_sn~;ilg1$S=y3H6 z4*8zVj`Kn*b0fb_ZI}%@AD~mimf&QyIhhjyJVw1BnCc5vz2--svfe$am3JCD)!u?s zte_hztW|vpS;7djGT(?mjhRO8r~!XLlAO@*2>o)hc=tQ$&RWaFSEe5;2IBV4{}VU_ z>N7-(viWRwy4fHgzOz^$Ix8z68cm9N0)kQ$EC1lAyV{!t)xselZIDq-ANIYlVcs$ej&gOJi{{@#i zHx|(L*-oS}$Y8&L#^H<|wb@#AUAObMo71QI&)-9qnGk0-{5x~l)Uw-E&4W2mzIw>F zX~cuOdF0y+c$=_yxx!@_C6i?pQipYb8?yK$X))-Ro+OO3_pNSba$bIL~SeAqZ<*I&9Lc4Wp9j$xvg=d-9v<=NF5h~U%b<#%?VAr8O7dN2v5JXToN zgLn9k6OdUMYO|I;islMD))(hl4?aW^6kAW#*+^4}s-#3NW~5YYIBdPJ@wqGbQm*Z$ z2|F_;TbRLg1?P))#9n=r?2g}ceiLPdmMw5DztQ{z;R|EBD&Q}POdA#22j_NRq?hj0 zj0_>>w|KZmKfsN_NZ%0}7*Or1$?Tv#LnoV!d5P9s|Epr0_CANXgaKzQkDW*0@(+y! zdQ)txQsINx!1J;;YGsCCRA%7*C3(bPw(cLrcgoce#kD02IhsyDj$mK_vqm?w<-uJS z_C>(_P2K>%*aSv!MfoB7&2U@#6y$#S3p7X@yvcI|3hs!l6XgQ>q~AYiHKkc+1z(}d zN@&2^fVyJ5y&#G(ySbabV$jf6-ba9g#z5gSevZw7g3193I|;v@+oQbSHAT0n!TSWUDgWlh`QHLHMB{lTVjl~38XFL+i;3bpk&B?2e7T(Lr7mlWXcYL zS9BiReBr4JJHS*6-^t{js|87>Oe+IVVHQ*eRk__IJS!Z-Wb}<9GDmo>v>|Qkd{b*> z)6shEjB;oa%t(J#C3tV@Ht7{$ZPNefr;9l&{T4*rK^M^Gh4HZ6ZY^C0H}I_@`~4V! z2@K-Z;)aO(D+o!?&D9_sLM{f*gM*d(8G|nYPdgHa$vJ`GUeHqaIou9gZgT?xb|JtZ z#!SH^sga*j{wHY$KcSsd%W8JcWPGW3R1 z^L);3!5CKHEMZ1Yh%X+TUR`LtIB{9TdU5K{Bk7Naqz?^AY+QUH1?D6kF#N(^yEu^w z(oY%I(<*NS)w}D?qc*0&7y0G;1c{#?FxLgmF7(ITNc_E1*YbTP{e7hxx#J6SHEvm# zZENYc=vJP$t=_hKr}xh8f41&h2Ay|yylr0Y|5sj^zjt;}oc&?0R-#38wR@dkrD2@!=#*^JnGKSt8;?2tnJ z_W%Q=nK00yYQhbH%xh{KU_|Me@)HHh$WPHVFF(EKwYLrbO(@biDFxgz_6aaB(qO7Y zwl8Inu_uLlU-;LpK=UP!kq)K)vl$je{&UhsBt&_ZqzEakM|an-O)QLB&RvijFMf?4 z^imyi&U}fsqmbeiVjd9Q=W7uA55N)qXQR%j zA+4Q#oTB^f&cs{u0_!X5!|ufD1&3q;%)CQ*nI20Jb_#D335FhSmTt&S*4kOsNw!}f zUwr;wlff$6-93UlVz>$3*G%UI>mk&cM4+ws zEGn{DnLlFvl4d?607NT>*OzDK7HN_6h-`Eu;LDz!_=sVcCgtB(ij5%MHKf5ycZKWE zcao135pNGy)C*9|U*4KaY_u=19yDPOnVQj0g*Oe8!AS~8pl>~F+T}jP!OWnkUJCGr z{@eO!5=)=pW~Ln*(z}@?)D?}uVGVpDZ2eu!HtBaWq!aAybGt^%J~P=(hltxYYX zm*llpWX$g~u{ugWOiC6lYTHxhaXV{W9+i)y_uCme8YH_s{?6u*BHd^X;$LotpP4Ub zU>E-8wvRN;n%Q)IIWD&|!z(KRG0ewKU;;!zt){npGsD&!$>?YN?c5ej&hro=9hb~h z12lVY@TckfZXua2X#{eAzrVbjkhVSWg6_&MApIr_S~DiM%$kYG;T2km7V_T+gj?twku~#p?IxsneIkHpD?Ry=@ZA%ClI!G$fN3a z45>VedC&6#G3SUvMxOfYW7+PP_J)?!-h1H188Xy9fU_2a=v+wP z-KC9hzqkKKyO4dQR|KNYX=_<&U@iMC<3^47F(Onrf7MTCrf8vpuzR2Ro{BQ2Oae7& zXao}{5-vYVUS;!orA&R^c|37y^#PUFt$0Y@K>@RSZ;7@Kn7(|DDJryNLnssvA@ets zZ}jF7p-M{1bp^1El#`&A3(06b_-hqCJqKBtED8RCt*KF@sqWckb1Sv7mJ(h;s)mDm z4fLg*OP8y^{4Ub;mG3&T4lnmd0;mS#toI0V)KFn=o87a zS4z9sx_e*&JLFc=06w*ki;D}}$C)i$Au1}+2GwQ0_{|ZOoUxU{(hTUgWG_TKsdp9T z$NK;&$Z(p-re4!gM!WYW31L)Q=C0(oV8VmaJO-l@6%PW4OS?!@|JF{03! z(Jc%9hZy)8K6K{%J`~=c+e?9|Sqf;RLh1?uL37rzo23d-mp&eEIE%!oPNdO^j3&#} zz}^J$Yd~77wF)n-;0iv0)hVW7zHO7TYOaK(rl)-S9$;x>t~m8Rf#H~J&XjsDj-sbx zQj1Rawl{+^hO?}U#7Xc;l!K-<9CI4pV*@bfD}rG)xroIGx+bN?pI=m~#GeGXqPNgV zdevc1F7iG`PsKU&7~+5p`k+`*!1*Ujo+~~renP}5x`%LIFXk`lRSN#>T%w! zw6Rw$-b5T9XnR0{6AYsNC^$z$WmVgoUZY>otBr7p>6g+)_NUH5_ytGV{QiQq z{AVFBV@=!ikQju$X-z=e2T|6e7;ChLQk4Cka9JI(cWr&&T8@RL|3Q27NxNgKi~Xcv!EW-zfH=dt z@5>+oS~DO;S&zO-=VYg!WEVPd+@5@L-a#%OJ?#v$dY;bH>YNjYLr$V(U(Rb8ODA># zr~1&!SclBW4eEiHSZ!qzd`i0!x$7nm1OaXR<83CsF1mS)fFsfR^FFu>A zMQ?MTPkKKm(L?Dy7FT)qZ^b)(Qpi(SeCdmXLvd0kgWM3J2>)|Vv{Y?}umyH!V^`^d zSin#`&;Nw&NUQ073hR6N-FF(hX7~YK8y6OqTKD}8Ulc&|HhzI!p&4iycsyImpJ9j# z=r`1#KHRU4KWUFY>6zdIr_x>J#O*thx(jl0@}BrFEd}a1I&`w#*SfbFfF8$|yWPZS zq<})^^~T^UD4@I-f}?fcxx5I8LHw$R+KuE$udO!3GaJt82l~KMXj-aHe%-mfz*<#9 zgq+l1Hi>)W3n7KARkcjY$+Oa(<@gpZ!sx~(Z9hSa&?W=z^nm8-Io$}@{O=0l!pgs% zEEFEb?3KC@u4T;8~NjG z5QI>7rLQ)KXXAE}NlRTH)FC0_USaB%v9ct0x!Ldj*l_sP_ngQ-Va^VvcXz6puQnO7 ze>s50*$*z%PW~~!jI1#dFCE+!=Yx1`Z0PzBaAF3Iy?V*M+4#c}HmR$y^*~{`Ye`or z<8Z-J_D#dB_7{{p_vo^`e>{F5<>`AO{TnW=YH^Q=?{HiTno`uH#@QwQ&pFBSed;d< zT{+I*?2QN8!2X#h=*0UBfAq@f$8(uBy=sGeR;0kxK*S01dpPEAsK^t%#+jiFkg@ho z=oKe6B0nTJ8t6sVe|8+ZwJaWfJA80ZmrbG^Ti&uyRdplEOB%RDj9fMS4{>KSZmT1w zL>HIxAcd>MPTpID7~saN(ed=!ir^F+_E8|YCe0vruZ)yGObWm;}Dce=}0;wgc_5UhdPWL=mY+I zI+0;In`z@#oB*@4;)z|0W41X!h3q?EuX7^b7&%@$YQwF6k*%uGH}%gp6_AX(guo+- zhC4OdXBz6nnz;-P52VEAd0zy9>K=a(bs`3^2)4Gv&Uj_6bRtjlQ|i>>4fzO9Wr_7t zl;-`E@sF@(*~_2o?I($)I|D>3>Bgb9$1p1vEPhM$Yo9RVlUZ?n_L^gVb+T%IxO33H zLUNr$=Wt4qTj9o?v=3D6XQnJ=E-Pj-Em^P$;Kg=d>KUp)L}m1B#R8iuhO;+zIVsiI zn@+M1{&iY0oR#YCg7Q}UVywJB!Ng@J+2`ql>X152XAK9OydsC=(WJ+8a35v9{XV@* z;2SCYl{RZF z?BTQ-O1VcxPqx?>u}rmxYZ(@$tg%D53a1L=wpmTJ$U@G>_0(eU!Pxt_&m3)bzvS+A zl?&y5?pNMll?QE1UKk+2hPZ@kAfW5-H7I!9G%C-|hbTss?aA|Ti_k7-z|)V5T0sr} z-QGuU`iJA5!kh*ABD_5rnordstrLB+(<1Ek%+>B1$F`#E5e~}JH{62ynrLe{W*6aC z=2r=Ca_KoRISj%4yi^M@r{obs&tBR}2Zt|e1t54LAvQU9% zA(Df7hl0`2sus@M^PasNWeyz*ZO%h|8gnu``lR%y1$#KKLn&cz7bg4hT*iGq052sl zi9I(?SG)f%aw;V_!VesZ0}%ad%*pw$o%z0CcMf^uO@TB9{~M=3mKcEJxV&a>jF)dt zRpBZu3Z~Z-IYk4ViDwn%zHnN<+B$Dg+KZ(3MY2_i%aaE&1*{L_!PZUS{*9{pj5Vt6GM|9TKA4pW+U$gP6T(=QU%t2bTl0Q!5!;SWY zlM9|l_f@X}Ke2xPL^~%zqAR?lAGAiYqkt(GfFxeN0_zn42bA7KhQt%EqO31?OQnYi z5z3}r7A+^8Iemy;fUlRXxHf`j9mkTfo*NO9JOUgoV#=fu+6yIJ%JO}FTK{@U1h#Ch zD+nJdT})?!SeQW=Kwp=aLu!LUvQI0P;q(d)&pDx%O0cz1rzB< z^ZI1(>D+q2nQ$(bEOasKcCsNLzfg`))Gg!tx6|%GvLtUXI&lvi`&Di;eb@GM3oX!a zQ@6XgVsZ=Qi}l~gd&HU2nRs*F`0SKV%;l4$%?YC#dljEpq zg!90~uVVVJ&wBO>N)p3gPEOVKAcU`>miUjeiT+e?)I4hY)7Aq``dS4 z=kSJX;6R4CTsYK7H&$$8q%e*8=ke&>5xI9x^SdbEJf!)PrxC614rVu+;}`um=AJYB z4!oA`+<)}QFT(9T)`Kh$MB!lTqGl&-H?yqRb)YQo#YfkMx3#23Z<9mX3hHm%d$fl^3es=8n zZ0>hbJ3B5ix!>^dp}jTRipsXlM1TJ>`0!rx{}G>1`g8b1Nctb}IWxDx$K($1F{@AL zUW3(3XV$9Mm=wBL>p3lRv}|ggT6T#ck(;>NC}ojLNmW1h=iOdi)q;=cK-q?vSl=Yc znOb%!FpQauU!D1vE*FuKK{0whPL@mfjk>q|RQdqHa_9mfaJmP5&ED`E<%CSAlJc+} zY&JY#yGpU0y9C7`P4LIlg?W-7D^pf8lwmG06eD>gR#B?fVVO7+cRrpMDtFW!0iZ;O zy?VjfZISZ;sW3?S`OkhkCr1Oh6)M>v5JhPos%3Te~8U`Ke<^}1li<4v1UDqbBr9{`w z>&*K~KO1k%kB_6+IID`GpKcW|O`n)$@3b;MfJ4$J@RXjx1Km`@CC75s9%|G|G%Lm_ zn#xDRl4T_b+uN;GZ&T7)ARBMKs3Sx%vGg|ymsuHEI?^X*;cqQ{~(96N@x5UK&+ zKcHX1*JbX}6fKqKSBcu~@>WhDA-*&nCG@0}uid4&v(5b9Zrnfr%|IL6*fom-id50q z)sWgtUz>=x*7E7$bbC0r75eMhMST&vPVcHc3O_k1;bPXC9*f= z22tNor+R2pcd|T@v)$yJrlWZOr(B^nNr-BT#2|XD5N+qE%v%iKQaHRU{cb7OKvmC5 z{-C-cImTQEmHLPg6~x@BTQ(A@lf1!N^?hOJ{Svj|#Y@1EU-D-grZw zU4jRlUKXhFJ}6=JIMrKAuja2I%BI{9GUn~uFo9DsLf5V&PS~GXnd^ZLfff_LPug-t zlE%m-(h%qZ-pbc;)@bWz31nreDF~Wp`9#|1y=mItyS;z*+nd{@j_=Wrg80XcS6vG4 za#2mxKYLA;MFhQRMb$f~g-5ZqYKyag3n^rZ&B*1s@%Xh?Wj^63cz{L0Qb_}p$6Ji0WoA8%o(75B~@r;ST&8_B%ww08($-K+T zj@{OB?vD1O8*x9#@@ZjvO=H)^skpDd>OK^Ls9l^qQq_`qE%^r8W|0(_a3~k^+uK;* z9ZJ4Pq@P2a*^P<3Eh#q-(ONaVTMarP^cROOOwNXzi(K8>XhUc#qm`Pqe3ClWI}S-3 z8@fYQ#vs2g_x>^*w(xraXuw~M$1k-qE$Z-9t!ejC(_C<|niN>k8?KDA3RDHSw4fXz zuP(NC>N1{ZjM{3gnifv4!)4!Ul1}U?BoL=Oj&*pQdRksv06X3cg;3F1ArLPIrc7SM z{X(RlSKW|8Q%m*NJsEt-?a`n2oIAr_6*aG%MO(b5fy0LFZ}?extrB$0*UFpq^b4Mx zS;XtBEzJGe7pp-$bD}tAN5=|BN1TW;^=N`3vZ97?!Cl4__qFWx5svN?>%qA@9NrS{ zvhRu5!<*Y9vJNwa#Qn&(In4)$@`V8XPf^F|S5)rBmlc=(BBJl0oo>cil%HwjE zZi%z9 zSIbkLf0j?c9f2%X=FYz6yOsGN566Z)7mO{0i%q<*6EE!oQ+u3SVnl1sa%t?_Zj~8MsaLrtxNo-hDf_mkov{U0(94-c z>Sa@pb__#Kl3fVqw?zixGt%?a^WuQ_!Uu)gbwUkS=GT5Ktn%DaZQ$4k9dI{^*3t(hiN8F%jvzS~#nascC5SJ;`e` zEzb0?^X+hRKTh~jW%WJ88R1uxGPdeXh_DVlxAkk#`Prwa7SRhSbH<;?Ui^xEjQgd{ zW~{Y*d>FW2BSZD#WdG`lR1K2 zi5v!=^xfo7F&ZtuslXXkK)&RMjVJC&-raa&acUY?wA70xlxR&jM>ylJK{hoHJHOi0 z&Hh!bgmN~u8L^{baVWhh?0$gBjSVV0wHe6*PL0=AunCN?Mv3;gk*_CWDb98Cm;+zK+z_+n)5~0;KUoD#694wE&ahEBT|f->W|&*Q6GPrnP)* zY#@UAYs?d0(evQ}wubt?cs^J=2kJE zl*0Gj8}8`MZO^iu39Cr}Z$tpY{nCNpelZwwMhxbwKkrHCs^5VKgPpIIO(Q-l+duxJ z`(@0E^pe^=Q`Ob3_uhkG*79N=n`b@ly}?`K;@3mO!-b^P6`WjX(%!UJ!5@T0RKw&k zpxD!1vH#@#kW6Q+M}2)ntA0XJ&x#VkkELGpSi2_90-dC^i zL3R?MlcC0?0A?Q+v;1I0uD#x@PTI$r-Ob!;vwr&^p4@BGg4^~=ySeE0-jsSUMe8Q* zHsdm4cjYYn33`$1{4>eb>r?8__N3h!S52k|v$yR943Pz)8d~(Ez3!iJLds6s>(!BR zMtg&JAq0>L+IAKipHcRF{g$|g*&CU8orT>_iPn&z`Y&e3Mbihu*+_9PfQr?K?(WnF ze1PvteF$EdbRBxAvI;T1@fB%}cT@s`^zxA8sJC)Q5aMALyOl zF!f!b5}GjmC_DCTJMwjc10__Q8S@{j)M{sJ=(pyI z5bAYyY`nFipS^f5!BF3wKcp_*iSDOM9eKh|aJG4BD7XrRkw?N9y<4X-xrb`$K|F9a z~iAJ3TKqV7u+@K&pVx9#{$ygFyxsYLBwD=p&; zisC6^j;a~1X~w(Ql#bWglWE;xlRQYtR+D^a1N}PP%Ez#XtfqI;bbEbyCzlskOLwU) z$Dt9lxR88;P-lPVRPu*)=@Bz!#F}m>bdU_Gce1q|RY&XWDuXwT_9t#P0_r$j!lm3M z7U_EiTP@?8*s7CXS|*9$2bi)C`rR=zX*$Jfy1Qpse!ik`u`p*aI)@Xl5x?5so172_k~KJTA_kgeTIrQW58Q|=De z-xZVItxF%jEIEL`Q1kEeX^9CyQ}lPp5)rkmjKUlknrHr&C;OoHBYt`tKxKDa@}Zom zFt)~F1964>V_3nstPuPw`Ms(Z=XwrGjOoi|IWY2=zD$~RS4S8sJZ_=e(YMD&s+lvw zsomMJWpU0gcE;xywU3EvhknUgffs4n&tW~=>)k!_SA1FHz6nLr96&EjR=Mwzv}D9U ziX;cSZ}5^HQILGzeVM2B(0Rbi{fE2CycFr>G55dB3u7^qdl-A}Uuw@U1z$lTY0mfm z|21d9xo4st@bBl_==L{_IXn59JGck4UTqEv_SlOQ&qN-z*$lp?{`{Wi7imhU3myx&`7Rn8d?5BGLViIoSc-!K_ucl)P3sIsbgC%+_>c3M*bzeW2+k*8Izd-Dz_!H zCEZb2-igsNa(*KNbW9^+=9*_`LNz2_KYBD*srm2!0_^wX?*aDQ#d*x$p+HXF79# z!$EH>xJ;C_Vk8v7zBz$aROVVe_w{)lQBZ-t$=W*eh#Jd@!SMWf2!#q}p1&tWvVyKL zTdm)OC`SI_@Uw`BIM#jV2oxKQ;AX~M)9k_;onho5z-2@)s+nQo5sp%A>g@J^b?7 zte|ah?7=O-)utCmKVX~gjprS_H?@#brq*y>i0_+txzH!kw6^J}S+Z6n8?t9% zA-h|g`4G8@;MbkJi6-1?>&EI^b5?d>cpVm-aBJd#J#hXtAf`BIn6AqJCRa=Ss^LTV zjoF>$F27MXV&r2(YvR*+1K_5yZT}wUHGjQBO4a_GZPx_YWWECJp)Ta{&J#`M zBNl~{y6T$bRtD|uU}b!$gZSv#v%>4ZZ4Pmf&YJ(IK?g@qGAcMn)L`l};x#s1_+uk< z8aTuRbVGl`4kHXJ_-s%N24}FrRo^?4yaRau3>&;f5^OL5Hn=RH##pLX@VYz?)Q}!w zJX|U?K9DG0JK|>ar|0&H7dH0D4E!yCG=qjrZ9?CN+NqK{Zk<{1HwcZ%&0#YTXb##lF) zjXIEUt7r!S?O%NnUZX+P0q4uhUlrT_~S*)K?hB+U0PfM zdhNNh!|jcWBA0}c*9s+$X3VdFmN5n~cSV6tFUHgQLoxQf+w=TS1wG@>{*-#X@AAq1 z)ELX)N+=CVHJ_n2uN7h^G3do^@$@sv?%w#huKpugSdhpnk!RJOX-ZQ-9zG*RmqVA^ zW)_yi$i*Z@-B&5^v#7SqE08GttyN>o%0b`vUdbKthtQ2vR@Pv3zy**kGpsDRe%Z** zM`eeA3Yb zKudXeH9IG7@?#&4ZcSYPh%Y79FZfc`YepVeD~5lk&K_={y(MTMUUaQ};@Ea)cs#tN z>NRV{qVI5V@1h$|K_a%;4k*j%`ttGnsnjPTZs3pV zelnF6lYeL|i@W*}rWYDK(;qcFP0nKMyskheznX4hF+o5F)`{Fd@+R#LyxsViJka>8 z&Uc|>-3uF9%ZFsPH|wN8<}iiu9~tqs)pP+C7aVE6p!L*MvGWKNhGu2J#0J}M5bbMU z_=vW^?TyxQJ2)+t`8m>1!4E&8p0 zT{-K;b@-&h=F;y=F|rSv3G_?lbN_ugK@tQ(vb{2K*n03*dg3KD-g=;)&n7wvTQqV8 zp$}iACrwTg*r8w-+4&9G?bYRPC*HKTT1&gh(A4ex2wTlpXb65Nx^%b{4`H*H@hZy- zcJMqiE!+FH?q-%Q)HHpd=)McNO)W?oVA1 zmA^a7tly_XtBXYg2-F&Rj?7F{bYpSV2Jcm77#p4f`Nk8Ill>b{%(a@jR8J1}kvTgy<=#BJmleJj9WSQ`m3n(veCp}8@lk2`E2woC;X z@a3gxxoL4P3YZ4+UaE#NcdU{w<&5j*dTNeV>ZElOlvE3~Dhn}5s%mvo6LD2zT*m~h zbW$v%4Sft-1cxVu!n5T9Bj>ZcEw*H%KR8bNU-L2F-!yKfCZ*?pNSb{muQTj^ZtG-< zo1l*-f`lM`3qED=db)N<9V8ge^=z9?DNsLoQ$W{mFG!Z!a8;6e-335;{&Dj9;CgXV zkN(sQ4ZV%D&^}n+foln~Vjjd}n|j7|IUL2OGEX<+g67oh^jNx!_TLX;=`I5K%ry^e zBiHbdhj3s6NE||%J`W7Ct(6L(8z@~mL~Mo?|(OdYq8QxB~N_aw2?wiDXu_ISO5 z_F{QkRzywQ7pckUW5#AlVrTt1&y|9My`}s(jV;;QWlvqH`Ex48XFNZMPsj$lulEC} zLAQXj8C%|evsLKaRX~)H2D`gLl0rvY#yHpOFn^ihe&2h4*K=^O7#RuE=i9fjh{2Gg1tq|+VS zg~=NX$?g~JUY6Ejc+RfmX$B1{vMWzB25GY;^T*K_-U!%&V>Xx37K89GKEegFq1$Z6 z;eV64G`lcvgGispdU!=18W~%yzydV#p7^5T^!r1SSL2Sp^5ja9m3f57c&is*tn$!K zhR=|Iw5Sk=N;sQQr0XqL+% zpa-={Iu`k}AEb*-)t)Y=mP@8$h>W7TtEO)X7Fr%x2fn6+?TB7+tX_<^g2XWAm|X1 ze4Xi=PhdS69WDeopQbVZ%r(p=GQ76~q>wa?l%^offy(6HX(s*>bdd>C7r3 zkbun*$Vl3${<*}BZ{NxeFAJE%HkJh0NjTx{Oy;@v3)+Edq#?bxJwYPo&>H=}bKbmj=5a`8{WJq`afGvmiIB zJ%2}{8O3R{|H}0qM&C}f&)<#tJv-T)xN6tYo`XW|*yeC2XL|Sq2msc`lP~2<$^%n? zU7W>#{+auref^f;>643`lAk5so;Q?;Z1~P^NDj=Mi@ejtw5b@XPr9Wvv30@0o7X7J zN_hRvYy8E;wCT?!mcBPaX2cR&rHkH%JCmXi`S_4y`?hqIZzMFAwR9!4He_Lcw=+lSwr`+rcZbG4`w8ejMo1Y#;SB~>93#gDXEaFt``eMWF)2c(_SUhOw@gF zExdU+wF?L}khL=072axpRDLSFEAeUl*Nr?kg{PcRcwePivVB*Dccpfv^QT!1eZjwy zYHOUCSN4BN=JMQspvV{pQS)65m92C#)`=0+IaMrUvu>O6GI4z{%+nch@G&MfSe9(M z%A!o!+{^qtMw2Cve=cuaeApPd?pL(#?o$ZRVU!>QEz^$Cg}U1wY`V=Xid0{)vg?_zHNGH+F=Ny8BIm~L>i z>Kk@z&-@q35{JJvlGtkB0q?RsGbN{SFg6}Zw9OBK<3aaOqHVz;s?9TgfIqddZiIMo z9i*;NO^MBm%bnrr#Nm10%Z@wg+;|clY*22T-)tQm6boOO*m~Dth*q!or*UWa&lM-< zmF#);=B+Nee5+|?Vqi!D9bj)b(W4Emm-NEB(cJYEJ?TPbLldITwFkm3W<=` zh_!-9Rpt(3YsJ<=7^XEB4|jz3*<0C{K51TvxoM9&)w*B7K1vtp*r1exQt%okz$N!3 zwlSm!0}QCviq-5#g+8`sFWW?tPw&p%dBm=U;KKs^g*%c=j>{m*L*B~~t?vgoqgGT`S@-`N zDBvRM{3zUA)>H;&r}5$XgJJ2l5uec+U#m*_{H*jU`>n&jP0h`6xLO%jD#_$pRnj|$ zGrG|F`&N4sl(znkYm!H-$H}Xr%xsqAo?!pm6rPwbyzHv$!pUKc$8R@$olIA?G#;PB zGC$m6?t>)%KKqT{IFZ%)QGto1VPzx+^Cg@}jZuI;GoMI4mT^rdXNvIgU0C)h*3Grt z5}%h>91++yhMAv``3#Qtp)ixxxC&e=mWObJpIqJ09%$Ujr2C)b#KeYDTox5eASGiy zv&mHbB|&)QeOV0aCtE9~mVK$tzHz8Mt}J)fY5L+LG8BIet4T35vg0baWP(!j0o*>$ zmJgUuX#VE zrAjmFbgp#u*ha*EBfwIs9FaBN)rC?^_FYqh!uBnZ2F48JFWozo5IjfQOHw8W#!IA8+EWM;+{2{=JRr9reK0HHV`v@xV?R% zh~UgY4S=*DEPU1fnlsHt{W&iEIZrb|2^ysH8lTa_n>X@{Gc|$HAGnvt_AycL*SVb% zzZU3P0>4P7FD}3bv4VtRDHGfucFHz!lvy8fpq5ORIrzHU2k=g|R*mmwi(26U62~~e z#2M5uUqturu{*CIez3v5U`K<0dj=B z?u#_TzT6FWh7YFxoV&DcS-Bga$BkZHWQ6P5bR(?i)`131W)ILf_eY%s^WbcUJlJW)k zAlAnJm!$t(T6NS~UUGT*y?6TOnoF*foBpZ7)Q8UW!t&MbcEI+ibm*^lM2@ZF08c1+ zu1XKfgUYS+`Qg~Q8rURRtdFHVAAi-epQ38n!}wXA9{AoR@5~=kaYkGACY=4)gy1BK zecS8qPJDQA*OW`ucA&+ABPUvQ8^&eU``(`ob1^sfgAMY`b;$i+tYi}Ja9{RquVV?u z-%z^GGP70eZz1p@_W}A(v9xKRNsold`(C zb4Yr3zwqYPPK8FpI6$Z#f)}03RC7?zp@*yl+#4IYhrTO z(LNuuT^UXdSScq&KYv$2vdz5a4$wY>&rkv?r@KB)UT&?JP{a;}z0ECyZLqxcF;WXA z))t*>nSD^imx=9u@!W&f zitCGVNAmh`X9e+wIxkK2vp0L+hnC(y%4pM5-<0h4%Iq!HiqS*uw{Q098C5~S0H1&r zH)&t?rWi+Y*^^$e*DscL zP)q)V%~z20ur5msyLK}(rCqy&BPin{oSxz)d1e+}`cT_pH4Wssy>^GFakTdJ#$<7} zb}dWs=7&gMa@*?-VhGL|;3&)~qiK6}ylRcTxqpY1Sw|5&)nYH+O=yZqJ94+MUS7Og z(xbM61D7^l2`uj+aa0REe$}xdd(sXX<(|#ZO1D%t_xm5Rxm##}YO>$#-vRXGR2H!{ zxM|X@|6r{eh3c5Rh^Z;v*?{vhT!ltvFYciB3tPdI7@T#3+s1cSCOT6`Fi8y>xp;Fb*4D3* z{n>4J#pT4)g+0e?IzO0K4e3 zu=5g0C<)VVnhJ*H{s~e;Fc=-)Sh^Re5ZI@#AuYYXgtL9WI=VP*abnj)s!`jvV! zc=`=5Xif@sX!@d+JF0I5XFKCn?hl9)~d2vT6BR z2qrrAHp9Y`Z(P1V+iN?-W8URLk@?HauWxF8EumB4M3z96*3yf>hS*d)-4aFbY~e#J zc~y2g7kCx0C$sczK38qXo$dSQcQ*LDgo$((ld{bjdZMnv3acy8)$ zc)&iPPRfpFd)8Vi{|LL&Eg}nbUEo~_rk_WO%1hQd-nD+%94K|sl_2r-Va~fUm$Gk!v?|VW;dm=^@dt*or z)B%4!^zUzVZt8yauj}gSIM>0@;A}KTgBqrF^qyi8O6(5JMV{7Lx-hk%e4E)ENEW}S zQpQ+F%a84E?J6kWXm3gF&;GbHv6J~}rw0GDg2d*A8Nu+TTZ-)AW#;^#-Gz4ReoH3c zai#XSnB?E;^zPCn8#N-*>uc>BBZ#^QhmjdSj#9GN{uo{=aoaQ@24~XCHzRO!PX_$w zuinS9tJc)A@Gf|f<(-y(a`jMP&|-eI`B%ifz?EWTJEGO#aI!@B)mlvzRr4D&i*WZj z4hCU!s<7LZ9M>Ja$qIWlIfgQgxjG$AW8CFcplghJ?PFq$*wD&;{K=(}$9~>z?tAp7 zvSxd-8v9>we68lCl=^=`SgTaG|7Q`NcpBlI469r$ujC(4&fbe2A-E6r^V=eMns1L9 zWhT;|2Av%==(#iU2j&w36KXU!D~79;(U$T5OS2G+A@}lwAd9;y{m`v`urj+zPIrI0 z=+5kzSfah*R#rlc&F4u~JO1%<1aK!JcTCJ`mgW+4ngS8HZ?JztOg{WI^6NiA|12rq zxlE*}L6h(CUsfHdYXtu);A<$3iS-8j6wIuzPn<3a388{-KKG8mG3h&F;oL@l7@k4j z4nCH50@0hyu=V#d^ZfHTVWe}vS(cq55TQIA5TJNjk()e;PRoy zuu+FKtnTU%Tb(CRNMvoPlv$;@g15l(5oGB@Pze(2WJS1?y6^rtJByPb0D&gVGm-R(v#k5f{8t;n zHT4?&9Aa7Hn-?c!3obfQyd97b5&|EDSOnnydp-pfn>dpD4*DkJ((qRO0I-Rv14hTJ zr%pv$4@8*o*~zgo|L%8ol%*AcPOPQR@t$tIa=^s0^C=!}MhN#mVyd;WL;s3Dg#a@Be2QNfd*$P2CFzsZYX3 z>5ij8H#~cm7?Li-rd7VJbx(oyo7G!)A8jp7B7nJ)(=Q5H!&?q0mZ>=9An>gxR)@na z&sr4pi~WA{)_s1l&8$I062N_XqQ}5lp=GEUhJWtd}TfD6b*l zMa0lPrC5nxLt31kmBebJ6iL^%k$fp}q1r-~-Y3#oYNOsJ80IwnjoLI7luQU6c9?Ar zQs?W>6K=Rr?%hA$I{-mGFf61Z5VT z-gA9OGzMPxN~MQMufxn34SNH@)3nihmM7Dxh$bi&w=z{^Gndye_Er`k0Ohk9HFEBk zpy8bp5bDhJ{+srnq-#yvjSZ;tCoH{KX5j)!W7F?;kz#x`Ib`0FZYgwPuS%QHs=hYL z!k;7Z>DiG}$RUpYF$9in{_3chGqa%BUhiJ*YI&p8vn&O*2rCvTtSQel6&xEOF5_G> zhhh6~u`(4J+RIu0E$0yRdj#}=8=I%?ZLZy&8{9ml+xq>ftq1&3PAFpuPlejxnBki6 zg$JF1dL|9a=+< zVZGfEthZW>-BeJ7r#!7$i*E$5f!)xEc4Z%h6$0Q>zJgZvP(`q zN^;|$BUaNZByen!MTQyD{4(*Mgg}@oa8llvt=;hwo!6b{m^X>zyG&^Ba6&wHNl-_7 zjTy;S(>~z%8E5U7H-?aGLX#e4DR`+hgas#DRP^*GCR zcL_PtC%Q3o2F5|Zd;xc7YEEzdK%Gn<%<5?BZmS7bHWxcH3hIBRwq%A2&sjU}S^{Jp z1${CvUxh3IE?{iPa}@Rsruj9=eAR&{jAhrd<>YO%;N#@GX? zW&2-0>lu-ZxzsD|wVTPoFj)z57n!JjwHXul0fX4lTN22E*AVcFd z7p3+Q$jq)?X4gLH)IRC<=fV%BJ7U^Cs0wC1DTmtQnz31v*{KIO0B-fjHyB^$;e+7& zP4Kl=bxLh=k6^q+3E9K?C-p$%@r$|5ybtnpy1AR1(~`z0)S7lH)1Ce2A2dO*3Y4Mt zNqg3l?$_U=Usdl&z#5xvr?sjDITdk#Z2%_+8&mE-q~Glq=1Po0A~#x1@8kJscUE77 zuNZjShqI|=K|`AcL-Rl%ne?y|an7dU!J+wI!KyFu8Czu21_VArs@bk>v@*W|h*Q+x z{>?%a9-R;9b0_&Bx`~kxvr|vmo#8HG6t)!9*%MABRud$9@v_{Pdgv3-`zb^32Q}35 z^ln-NT*WS`eaihMLk*JGK42}q11FGd?NjZwPpkLZYoE~%^N&62sazlIJ%)4vwGBmn zh8+V^zfT6!QPBD@=1hfOBdUd1rT>BpPUBPi6l}zfG5hP>2coa@bB6wVs93oC`yPfp z!CzJr0E6+A?qoQht?8ro0s9w8aL&$pN*q~48m7JGrvJ^}yFf=(U622H3@|`+21#wK zvBWyINsTqp*k&YXzywkR8UN(G_hfoV=mowPS#QI=; z6*Umfr_&fvcQidne8rs5(J_J3?KOnJ7B&tBpAWTUMV~EhKG0{MEG#guN%_bpj=JOfjWQGY;m1CQAGF}Y53k`3_s9>6yXA+VAMQaRcvcg2j4FO8y+K^pa>Y~s z+W7e&6z{~viYe7+r1@W>L%#i*mSFe;wn<=)o#$!h7&|}U>mtK}LH^kbXJ;C-7ecJa z^;jZJb%)PqI+6ZI_p1KdTFtCnXllWniDi&Y)FmUB&2*G>jWZIw-uppEPN2nCYah$p}s!UP*N zG;|g#8CG7|S3P%q7PW=l!$ym8^j}lBW~Ehy1nEd8tZ!8=WEo^|O#T>u^>BWGxCe&I zXWaxs%*u9WjCJt+RjO6^X8nK*(BEVpB!S+x;`|`;4DUV5uab~9k>xzoQ)6tl_0L^0 zbWcU0fE-W5rPxb0n|bQ<`60WC8;0=ERym))X`!*JT=ZESEMpU0*3s&XjVcr+awxkj zFNBFgv$@{6VJxtLZo0Y>QBSkR?}pel*^wED6|x$#;1Q1Cev52oWlpFv$7SZoQL@4C zkjz3YJ(qM1Bk|?fU~lD2 z)xONEQpW*CK(n$59%ztx$GKxyi9uj&ihQ15q1p3UAj*C_nv%jC-C1Kj60r zW(~W{=N>c1HiG~3$P{_2={cFofRHgOGEvpLYO$hb!><8-mmt7x)8i%^YZ0^u!LsaZ z5>hoOTRldOAa@PfY!v`v!H?P$tPx6x5?F1iwRM-y`?G{91Jy}{z0~Fci7ys!bqk;- zwYtCtZj8>Bz*)hiNg3a=8Ra}O#)v)*eDDLJ6Ln1yU5ITuvH`T7`hS9CP4(L(_t7A? z2`JH8is*nMgoZd#pJj4RdcO{pfyCMfa0GY16i7uzqsGNv8yE9*$>Pme1KKPQvD=G* zt&9+iu=&h+V}$)(%$&#(S=kxe6gmt$%GT}hmF-He{heo-Sdtl6m<`T>;_wV%9|cA3 zP_#%k2hZU?LL(hNv5^%FVS@2|)%vi-75N1%G3cNJ#dsXe&?6N{$RK|kj$~VG%=9d; zN{JL0tXNVNl*y*$C8Wkv~B>qfJ4-MY=BfHi3g9Uq)v1ORdi&I^7kNaYi|b5EQO_ z=pQslo!Z5ztcx6^E>Ns^V2L#nE){!vspc%m%s?m2!<>=R^>6{+;3vB&9Bdmvk8wmH zRjImZAY8hEC>@;G%r&N!jgeh~#>#cbj8q80=p&t!Piy$(;8q|@7{EWITgrb{{1F?! z|Hb25YETncg{S^{uCr5MvKiNJ|4KRG~p_;ta9&lvR zxhwh!Vz2lbn(s{94@;>Pc7+BuH@U=ohU72#~#Djg=2e$Lwv8b-!e|WCa(b$-dMaR&=;$1hDh?RT#U@M7DS4vu`?&JC3S4 zx1Rr^w7~&TmQyzFJZ&q#cJ|rL-yKx3^E8X6S6e)dgMr9szwlDf$5u-(wdUvy`&0&Q z!nnn=qQZBJT$xU=5w5_P%0=lpq_=PtMnvvdzw0#&M&@HT2vtF@ICN?H*hGHl$e9%L z)fVh>y(IS@3w*Il&A&&ZM!S@t09qkkS~s@xqFi1SY+%!GM7Fb)c^aP-tC!CZxbbeD z)Ai!+7&2E#X6`7KnE`x)zl-+PzlbK6J(yoh)HfHCwE8_p?NKP{S@27-7xg>Y?=)T@ z_tYo6#Xg%xebREKz9bbGC*T4r-YX@*?1FQ$sN>M-yr1RyS@XH=EOk}muj9k~k@*x} z=fez(sF;~v-N&MUtVR_{SFU18BJ@vp;Ix&(Sr^DcHvb9dQwVzpA2zHjNzYn@5@(3C8S^r;? zzVl`ww#_nm!ZLF0RUZ76$7x)k6 z*nm0#7F&a*w-l@u{S_HbuC{u`rV970_kyPUou?f_3>B0Ud{YF26rRFjf`PqCHCBZ; zmBn~nYO@(<(E)@Kg~Dn-g`0>1m2*1nsM#|bf{LYLF?jBtF1)5cfYF>5YsfCwha#~O zX=;;v)U0y%dY-AS;Zay->uFx$F6imVbm*6wlk$n%OyzlYX9~Gei|(*mRJ>Z=0i+mqx4MVmx5~;TVRWP2K#`utZG4p5`WSO%80U2lYp(rNHTv!$r4H0 zdVU-|HlJULx`hLGZ`Yc0Dy$sMl6&Z*eYM!qks|bH31AcnfM6;Q#V(Qx6=x1q6!a{( z26GS9Fka5Q?$8RVoZl6!>u5$K^PEpp5n+1foat|$lfmgj^eX=g$uWzxjNLF5jI&kb zB36v2eleacmmjKI*2TE5x*L;J2QV<&tc{RBlOxai!hA)ZJawZ+MZrFlqpax80_ z5Q{Kov_q&j&w|;ogm?=E#O_#tsihUJh)mw?{X<8Ya@ayoi+4~ckDJ>(PX+Guh7Jt5 zfE%iStvvLui=)EnA5h0EV;}uFL*QV=+&Z%fnB-QES()^Q9%oahi{FZ|4hj4`^Seqo zc5ea~=K+%qM}}v?8JcND6*jyHC{hCifL2OGTy!qY0Qx_e4h~#Wv9+A`DYM{FT_%S~ zN!}|x5Xll_(oy^z+f4V@E2G7;U@+{3G?b_Fr%Z=Er@=9+hfoW()yv{*WFG`}02J2b z8_H%6tP7Ki_~ITecm=?0p-mlu?AL%Zf&ZnEsaT)8WN6Kknu-N9sbw|#BTZemuA%-G z94_{la`y|?L|zwLliw(#99zbJ+?Tk|VvrLPAwANUC)`9jkW)aPXkd&qpsK8hOy;Zd zM^$}%ER&A~YoK0vg_7tzXP{MkBV{v)!L}jAQ$g)O|{eBRaP}^XfY^xpCJUjFHBVN&&nh#oU=jpst~f%of--grh#BR(MSq7I9gBe2AdL-Hg%K%qqaht`D3 z=T|9onB}P#XA-ReiDN>|xSeLb2t59S#`@F`^pmL?$?-RG7zx*)wvkmGfVS?FFRe5? zOPbBo%F$*0R90eIWVw$w*g5pOLTp&8QE(;)Q}Y~c*eY+;R;q+dP(Z7~0+3$hGcP)%n`W$U z(}F^nvED(@+H~vd&?Ll>f-5_RqT+9FrMwJMWyMd^ikQju8xRwjoUiYchuL0sgZ&8( z;M9qXH2Klf{ri|2qvu>10*=|q5WccA@D4(y%nf{b!G-MpM2k;rPzHy%jrA2`%{(2* z@*fVk**#{B;~RO=>^5=0(smL97;KOwDfc^ zu5c7{5fs%UFX7o6Yc3ea$bEpgH8N&aL@v_Cpy+2oYve5OZA-W)DcRwH*^ic>+euwl!N@V#>&K*Y5!6)i+Rx%D%ZXpYF%3X3>t<=rK57V`>Xi zjLSYNosdvl#ug%Ng|F)jd;kMfqdzbrB`9!VsdIEbOx(xPhgJL>$YXiR;DKkt!o^u6KnPBR3ADd;cp8)cjquv z2M4m}u8dG91Nk9cQ|xnlRwS2R>G5SP(H4tD=>Hw@%HVDfhzipnlcuX9=Znz{4n?C} z%A6n1=rqrQJ9#Fg zhXS4j4+0Npst7d~t9H49N;97X)-1~7IYti80^+%8c2J~y z*b-SqZym~mSzg-mY8;CEm2HsV5r|l$X17|J{|P_fBMwbPJ=kgdkS=mYATKC{B)jk-k9DC;upS~a%= zl-%10AKre3GuqNlspszR3;WokY-AQ>({;*)Da8k1+q)$`QDi=UV&ZWvKODM5zQqrM z7z>d15&*yDpRS19xPsCc%yOGD$Rm6A+VjTONhHi1BrY#>c<_utYELoKy=>pB_WC!@ z=rCZ(DYF5V^W-qPi?f(oaohVLnQoT5?Ig61IB|rMkb1`Jy~)gfP!~t z7^UHv1oP_~zM4~kjU%{QC2*#_|BUD-1hZr4;X_7nAKMi&jKQIGUgMB#<1g-vKCUy` z?Sm+n>XKP?j0?rNAezl@)k?G?b+o0=Gmjyg#7UmS!PDaAp*MIvceiQ=znLh7YRh^a zieH5uENa=E5iaS+mhS-GH{qqsvqP=f>HCO<$D2~&1y*C(4}I``?4tGjF3~3|AwWKi zCDL4Ir{ZME?`%o+`dpjKuy~UzZw`LvSSE!A%eH)2-`GO@!yV|xe5xl>f_eG{OyizS z2r^PHX?Cd9<B7MvF%~U78^dtiX_Uv4x@f+^}T<)$k2{9y_ z-=){OKa38nJj;Rs;l0eOjur3FSE|cTu2o;po!_z;bPPfF!yU%+)1RfOQUELpXRk2X zp%T0-JmQ$YjW8+SJI2+V=gI+(n0;_!a7&z%{%Q%N^ZccceI>1_sq_3LcDmS3AF|U$ zc6z^^-e;!^?X<~G=i6zcoz~lFot@6I)0uV}u+wRFI@M06*y+u7I>}Ba*y(sX9cQQ2 zb~?sRtL(JGPRs4I%uY+~w8Tz}?6lBM^X=4Ur+IdoYo|GOnr)|EJI%6Fx1GA|G|f(r z5fokUe8^7s+38+8-EF5^?X=xapS9BscG_&GtL$`{ozAz@Mmw#y(>gnyWv4UkG+?LG z>~yM~PO;OQ?R1izPO#JQb~?^ZtL=1*omSatg`Jk$X_=jt+G&ZM7TIZ`o#xxA&rb8~ zG}lgZ>@?d>y>^;qr*1oSsWkHEzItLNuKzfT5pYK`<-Np1PmY+=dH!O%{6i|0Fky(S zwQTuQ97N2rr%6yM7<|gObgx|WJtLixTfSG-nNxaX;o1jRF8T@{dr+eL6){c}v8A0;8OAk$eY@OnI7D6=sG8SraS_AW6@6Ol z+LdB&?enyZa7SNp);1-T_7mF^{Vt&pDJf@+N>ETm#Ew1PnQvhv6JKYmwf65%brQVj zz3|8^X>1ukiIo)|pT$0L)pa7v$)#Z&X4-1~jIRV^$}}c&ek@m?prFw0S?)Az0GmI# z!e;vEBn*yEmL#pfG8)Qyoep+~3fz^kD_!{mx1PT%Q(qbXK&{7i3yh7mZeqf@#B^bJ zB?iP=d4wA7?es~g@^GHIe(MY8viDi-P}2R6bS%>(S@}xkRf$285@RVP;48Au$-klZ z{1RU#%&3F?yfrNi|A~Qm2G3RE;hWND~^JV>fzc ztWiZD=n}qC+2{YlFru=mLQ>q0u>Zqj$y{r9ByI9DO{IdBYe!ywt4mF_PCk<70d`@QJ>8aR>bJ%p=|6 zWgRst5T9}PO#<;rehXgSEL2T)hYxFASA4|UyUA|Px|W9&ynMxJ^!k)WN7{|vq8oLX zg){{}!LF@W`1x}|GJbx=Z^6&n3wp%Q zhPB<}N3}Q9ZqK@yhZOu=mH+r>hv}gRB!?bJ+Kka{Z@Y9#qJ>#cwargRW zq2R})l5X%*PmKyc151+ebMz98pWm^`Uem9OeUsB$duQA2S!eK&f*+65=+9^*QLgT^ z8~uWAv}gQ$$lOv4KT98W@bgbz>q0V}qb^3_>YjYFFRn$lIi5Xy%0eFk`enmja4$0< zc_Fz1CGuyc*VL1C0YqLn)WdxPoq=x|+d{ARt2>+#xO&Ey>JDcHzS#0c=E3cChh4$a z%<3#-lUeP?0mwDf9_%&DDTC--)vwroIBw6#YI!5$;P&WqYQDj`hxXDp^ZP0Fn^0=Y z61vNMZ$|5N#tX(4Zi6w|8I4hzYi4v-yNNy}QDSqMyOFt` zW+Ke9{Htt34(+~u%kEH1zy59YJ7&uH`P}qZjG0;C5qv*GN=vcE>p#K7r#e)%J@m%y zTi%cYuc!j#NN+Rdc=5@UKVwhh;WOnLqg;_0xLdN`FBP|yvxAELDZiuNI=&36ow!%H z1oSuS;FiqsS&o0jeH~BZW66S?!Lef-#`X1^XGUsp51wrfe=&Sx z9?b7Dwsp401m=o#H)2N+JahO?1Ap)<5YUF(Nx;_&B15@l)i6{R`wfM^)`Z^h)E&w4 z+_g*b!{$!ky^AZjC)cX z?JaNikE+n5O_bjO`>l8CQ~WnVmb;zu`|F!$Mhq)5igJ^iJS(LA$YuP{^11(CXqafE zfEU6~p|(uvLFnyc=8*6WuEd_G{H#r6MNXH}`X89+rb5e2Vjd7q`}vY9W?cctZJsz4YXA6{MpPd}z+0_Qq&G@o%k{DFgF zJXe2vbei?>iVu;v9989_mdTi_$ zhr2CZzC`|gbKe+#g^q6>vgw#gKkzT#*Z1^Y>t}Sc`3W`gv^hB2JZ)bF+eJt13;%GR ziYb??=j-9i-0*}xY5r}V`ZmyMB8aS_zRn}T%1@8{lZPh%T4U4nEP^JDaD{)wO##FQ z9T3hP;NRt`e}wPGs1B3MLtG99iyZap$dBNQ)~y$biYLpDLPDz_+-A$G^W6tNAw@I4 zg_o^ypr|cv(sRuIFdJ0&%@Jxg=;nLa+D(oB1lxKUq-|6O+0%>*rH?9lqI4^hP;>jt z;V!_lzuZK-nG9CO02P~v0c_)HRRl{M&8?m}-#CO{h&*is`dGWq1=g$J(FB6(cvsmD zc0CQx(0aJs8#?5^wbJ~AR7pt7(C&T4T0!Q1zM2DwZSINeSoP2MAwC{$x>g>?Z1)mZI(GFrZZ&?%eslz2|B3f@@MFb z#LcJDxUuz-*(r!AFFWGKZWi)Kx-p#ev?ax#<{?IK1l{0iVS(t<0*uke%GE@-CoP^od>}pRf8S1K_t0$;H%Mp%6Pyw;HzSjQ-OB$G*=X*VLKL#>%cBr;Aiipe6| z;6Q^&z4|RY$?s?R9qT*~&iGIYr&$-xit(1BcjpI5yd~w@(j{l44>?sePWdrX{=J_h zm;Xn19?6O(}nScjYB#q`@g)^Ef99(~P%Rh6q13i_WjMYOhMOqn*@b zT~OoR|9fn7qanbz@E!BWjB>OtegAmp<8p%naWmA3P}uKm;jh>B|DO0jo)vv38~r@X zM`kpR$_QWieN0JxS1lO);7u^Bs%x#t{GJ+g|M=4wpX>GR!n$FuL092=G&IvjYajWM zZdD_JG6s#rY~_jo!aEQByo@enQKO5U<^rk2=S^R$oHI26nPr9EJl<(ux|2mDDI#0c z)#gdHZw2|RVp$rxvh1O zlCx;7`QC=uOcT?qiz(Yju7`#0Vf?I!FgfR)=r{&;)H->-F9LjGcv zzb6x6tHt;oTGZR>U z7nV?(Y~;zgiOS;=K>2(#ozf*t$arD=q%AZ2&XE_Q|9DoLX3W6?hzyuUwy(1q#Agxl zkchCMH~NN-^bHOK9Iuh7w%N*D(i(R{b22~l0&RMchC((Sm(X%4k)$ht{;1(`+@M(KatRQAv0J20j%h<{Yb?UZE23$Qym2D5vLaf+$&)~s0t;@*TEX(aOc_32< zox6d+?;-tZ9UQ*=?gJ{5iC8UqtP^!aEF1>RGDK|31n1ArE>ey7)({U1-+bxs(3$+ z_qn9iW;qH)O;Q1I=;-m_lD3g50&DblDt>4#22zedXF#vLPbUKQbK@599tS|pUu_gy zuaI6=(83scT24=^{I3SiGe>KX#sE$gpi}@7k#F;2&2xUQ%qMevR((P{xq()4tQ3wG zye1J)@GBA~JIDg4$T@R5T>0RKv|O8cXJK_hK(O8L?LBSB2)3 ztZ=}RqCN@!50ge@LQ#%S6LBuk$NZ$}%1A%Nhg|b?GCh$5r;Sv4+p3kaIj>HIIwB`h|9X$omon(s;Qcy1tP%)z7q{wBczO z4tQcmG|PL=sYn(5hr5U`9(va;*OSUAYhL=yugP{bu@z)UE3wluNok_g*v_=(8|k6l za(GJav!R2i(iH^4YCz-4jz%T%$}6?~LAyN}(WY=cE43QKUA!!%55`q)p301?vgVwz zI3w_Fz7`%Rce8!?8`Q=&F+GKrEH4xY9d-xK4IOm{a*bSH<2b1Ovo@QnJPl`yF|?eQ z)HbE9+$H8qVpWUN1HZQ$;ylVBq%(RiL9^+RFH*`cDTdz)zV2NkJJpPKVVNw2P(7mA zl}5xxx8-xHxx{y$V2Q0U2P(Z@=A(xbg}`gp`xf%rU+0}EbB zs;8kGSovBAR%hI`LoR7i?Mdr*bc$fQG z4!NV{a!zNxG+f~uG#I?`%89cQQN{F}7F{|N?Hl(-M9NjL9T@S>vUD@4EyC1$bd>WhFE zW@_Xx{ChYY^7EF<#`rGt)b9}h-pB|JVy|r-bsuMWS{Q`t$)S-f&Kn4h z!HJ+%`O*P2;WA&7{1i#H5hrR)q$^jnb&Z?0#}6Fm8^;E_0*kqSUU-)d|cm4Xmq5~I>(BLKJ?cWD+AZUpajB=Dx7Pc;!Aa zd)gC0W;f>!shbVd3K1mp+wIcFS{&Bkc~xjL~3g_OY+ z3=4gAykd&1aVY)DoUeZR=PTTK>X-9Qa3RNpjeUy5Dquf!j^xl>e7v{6@$+eU}(3i7EAfp;O)Ns=aqAJn0W$I4ivDyH$yaR8* z*nUsLg`g=?L|KjB&{2kt18dU*BV(PDAw>)cUjm=}TUX{wW)jvqgxF#_zN)SvVZ7by zbj3>>Y;sl~0X%Xo7*(NRo|J(%*1vWv0g6kGB67qajnp4i>k~kJ8BB3m+GpSl&a^ia z@;>rIvg-8**@Qm^84wN^*P_xzG(~!4Ow}Erat; z6Fb5NFfaqHm{_skHH&wuMlOG|TjH&ow{?KaXlGZt)>sp%6aKd;mqMn7dcLVpt7zza zLH}y`6IlW(Vbi;Bq{op-n3)ZK6(xY%|Vl-Y?v7o#7T zmOJL(Vla&EM(n^)m4V$E;O=df_4c}y8CbtLxHd9I#_yH)U7fTaQ^&sz|H!Sbv?2bj zQQS71fe&UG&SIh*ZG4v>bAAjwfe{jyr{Pch2G3a%WUX~?mO48t8x+yoVYdIx9QtmXn+`lPj zY)b|8Q(a3+p|k8lZQV5X_L45W!|QAFzsyWD>$e+T#ZxcyS>^rV%KC9h_{RE%n+ThS z_&}y_x5IYK&O&2cLI~8!0ml-bHH`P^YBrZ&%xeGrF=^2ahsvgg1ebB;tmOJv?quCI zVSR9A8dE(9zppa~>0`Z!nSXojKyyS#*foMXihkaF(;8P=Q;PpR@de=1-hpgE&=WD% zC&3LSdnzpAMr!|!IdYh~)EwWz(y@!<9XU@*-*EJ7c8K}!1g>UIr=<2v#`EFAv!i{? zzPu4B@T_peSB3tS!2yx2(Z6W@j>%9GP3WK4I^yo9nbjR281aj# zDr=H}AYkXl5qO>1U!h&y4Pjo>AptFwOYYe7u!5XKX|Lsoyo{HuSG>ml@X13bT$2%a z$!L%ME5UniO2ywf&z~gzm}kdAyaGH{cW`?{$@CH=y~KDSTzCd{Tc9g`2G3OZIm7HH z)933m{Np=nU+IE%)~9>M4`%_p;K#Eg4=ovC?6>cq2RkFUf1WTA8-n}ow&T*R+f)3h z3-?Fa32(9|d5m5TW5k+2lS%a}SuO)UC2(O^wWCt-YY92z{Nnp?&V_h>6FbGC^H)?K zx3i>{*Zb830y%)_Gr!v5-&%WZ!CL?R+7fhQE+H@gi94S(L*|M3BO=qmdJOti)Me6q zfU!Ti!Lve6hvrQ-5Hlx-7*o_CvyiTK`a`$INw=on9uqyqvu1j{FY+RVe}3@vN%2o1 z-DZ}|oFe)&7vL)V|DN7wwU*05Sz}al3h;yfrCY6nm;6kv)$V49sE|bvrpmZ1FkChs zER~0=G9XQk5i!$B*coVCm|uk2tbI1RkVd99$`v*LiX9d=!Z zptuak*Kn(9{gnb4us3lu)Zs}g;*JFK3}cYiqad!lBY`BT^y%`uyYg@(Q}rK(zTvi$ zqOUWab9jj{-T&H*9>?>7qiQ%W4VR3Mo(H{UC}-AEt@khekwVJTp?4X=y?9qldd9-xalVQm`#Zp3j|C-v*08PG;CF>Yc9$vDxY!T0&ON&B!NrIQ^ zP`(KG`?tjf+;moYHO{*5ix?0I`Yb+wFBV#nIjsZXv1Y6%7y?^jW)P+yBkOdb$i4zi z6I3PeKu0-7E4&?Hj}Vf#gG@@Ulk9=GzRNRj?J(VhzPPaiWye*rR6K~Oo^{z$vEW7Q zDOES#pMl-n6suTlTwSeWPYzPh%5)zZ^tIZr6&y95^RKPF7Q~ebP|McQgRMqu?HM#9 zD-Xuj0!Qsg(}BIxlFBWbwlXd3nknUSb(_ySoylTVF#534rf{wx`^^Mx`sp7hYg5sm zq90_m)&ymOPddD{b`m;%OKr9PRpW!&Pl;Akw!6TD`<7sjY{z@sW1Fe@Vb^eg5+1NZ zx7fm>41S6%`z%&>8phuT{#R%GCEA*(@e^VE@okAPs`XWG0)Ps*sS3F3V8MLzGPAZr zL75GdXR<0}V@?k{hQRlZz=b6(Odv>m`v(d(>?1E6kUTzZNvd?lpQBIp1n8!j0P4_J zFj&OehnTr#3WJ`zWQG}~TWVe|AbwE$L!gev|P0z<}^Lkaq8rI_gnzq`>FK#an$<&SMv3Avk^*pk!h?-c*iok_R0IL;*yrPv!a zEhjl16~ik<=!8=9YmSeZ-NoC1zP5jfJuL$Z_ViliT&{L~+MaQ-t_1{i1~J1Jl7g>A zA5SD-uj5a5_G-+&UydQyt3Nq7$)CJz!l3lP%ea&8CN-amuk)KfHola1!>Zv}jnJN+WCS)uhYV0%`(zQCxUIIe^S)rhcOT&mpQYqqmh7;}u0;q7g_ z?hap`)$pyf6xni{k(2^B0HrDs z4u~5nwmv2*v=}@I{Ds^8it?9MyCl?@!s5Gt!5H)p&0{ihsy1R)PaKE6;jdSFrw4v> z?47tjS(=+3+@;;g54yOMsr<${^N)`|=|eG!PwfprKK{+v-&@fRc) zl`o39okqY;7LKHUS8$ijVR6?xj-B4^!Mbkjcg8>EK+0ADukAn@`!CDQFb)O=5cbeG zTAG{TIFL3z7}8>UGNdo!+vJ&59sZ5e-RbQR58bHTM>Hkcx$evOS*}|(3!3Sf$@ct5 z^B=vPwpL9ouk8zgK>Lqa-`i`)7Hq+!DnO3U2A-qofdb(&&>3?df3PaieZ*}5ixew(HFW@C0#215nW0hA5!G&UikQ9%a_ci3x8CZwgVZ|4aW6FY?y6Y^QM-y)4IR2`_;eo<_p>SAa&>g0v;K$o z_{&7^@s$LHx|_S^Y=2BoNzPJ}N`-n-@auRtM{B==EB1bE85^BGk>HB41<&#Sn6`xa z`RVQG2L>URHIKf4;Z3$P#@gUE0*9`NzNGEBqsIIAi;qSli5^QLRHK1P>o)nD1Yc~z zQvAjJwbx0*7)8ooe4qKtr-R-EAQf7GOD0xrNAcaiE7-Q%u(7dH0k<)_)#;B^$*n_9 zrTB}xdII#p?*XVo-;?zhOYj%f^oBD%ke!x(Bz;4opXiSPt;ubBlpCWjC;N+$-u%Tx z3|rq#_7@YsJL-^r+2Wwsa8h7B0;vMnT zVHm^aZ&skqAF=z`^s*_ffc;dNTtaO2HD|aAT3e21`h$CdkC?$d7}+i6#Y2mX;LhN$ zjj_AAk(fY>6L*>u_YmK&c4nt9ywrCU=ef8?$5lGoM;trMz$0zNGTS z^F|b@@)uM7lHSX!@hTs^TLeX~&7ON?_s;;OAUw`DMCN7KRBD!Iqspv%%4ED!E8Ogf zcBMJ`jvZ{9>5a^usrQM(!n3YFOqeba0M(9E>6R+XQ>v7r(x1Fx-k$?RKh|>0^1`)h z-?3Aze0LAalkaZ6dzP0EO3I+hl*9?5eiy}mC-*mZ?vWy?D!IQt0q%E0Pocl6pWriR z?vY7d>(7rTpQux+5cF-&9X9}mziyrDzD5~qG zz8>TiO) zwRf#wG2fOiC3pTK*01F)?N{;se{^}9l+xs>%A2>F`g)+Z>hkU{CU@CxANlS@-ge*D zg&!63+>=t-$G~6yu;%-zG$wEUc_T{I_;eZL)6m|>C&9mOSbI|Wk25|d>l4Pe=3jT| zUykVG{mW%N?>{=eP2zI2#46+Rwoic2@>Z0jzSa2W9+vh1-<7CgW9m}$ zwOO7p-X-^!@y#n$CF{?8>2Kz#^fy7?%DdK|C-wjC?khk)r@~0$kMGtoApCVT1v1H_a#Qya_FNLyw#mOiWdI>+}4VxrD^dfK5 z;W#I{bn?xwLEjiuo=%x%#!d?M~?bN0+zezb1D{;al|4Ny^)_ zZtClS-m1&x-Q+)^yfy!(3qLC6dF-NO=J|;H827ZL_wd^_ZiwaLWFU9TU7KZ8C-~w? z-@WoZyXWs!d>=iAX!PdjYJ;PXqsJLl?64beRIyAl!KlhMubpI6=k(Zqj?|yqWBv1F zt3O%hsbgZaGVgwQK|}KvsfR3c-b3oaYtB;#R%mhF67?p>oVQdxR5BcW*G|NMEVKRiV{VlSZK?d={C9y8~9W&K9$8FZ@zvXbqcnKC^UHw-zGo$CL zTKR01kc~rMb}2Qfa+v(!yo?`Nq@|=ngA&K#lw$XiNA`X><4n#jk39wRrPngBe~u4Vu8Q-`NVsZicZF===Xseyw@DnQ&n$K8j3YPIOxjkASJ$ zh$~Q@6)st+LQD=|iqBrX0AW+$daDF9aWIxgVqydc#A&qHf!f=2bi&qhFB!5`hH^Q% z!Y_4NxUD=-ezlhS_4eXVs52@33Pe|KBgZeLo|ZMPo)>sA4(Qo8+9ek66f z?or+C4KKyJUGw1LBBJ4rE?o}=3iTRzD^1y zX)Q0}e=+|r=Km%9FX8{C{Qn~Vzr_D59qgDLU9t1Yi@L>59W=5A{!L?NIzN)I^G(qm zFvY`MqL4 z5wMfsXWo`%{OnG_&%Db|fS+*?*vkG>s=Q>kr9ImNo{)(%+PYH`-7-%b%%gZHm1j!f z8xF{qU&epV$d_O1@X*`o;3Gw`Y$@y(M`h5}Y7;>MM;GxU2}hrXsRT!pH#<10d3;X_ ze!e&Cqu{6GkKN#B=YI)5D+q6$il4_W=oUY@(AD~*h!Xhm@FNL7$3d9jXXGXaKdJjC zWjTy%uVVgsWJhdki4sZ~gc!S9v0AY&WmkdNn_g0}H^u%eBE^-I(;hDCoL}NqtSgL% z`1qLRfBlP;@~r0(DJ-~CPAQg)VNuy3%%*l*2pk*`6)-CC!m?~2UDoG*Rw?JrC?lizupYv1>R+q@s2fMhlU4qZC5nN(TFsLrNo*hTGSAx%)4hhYu zGi?@R9)7_=rs%7afo}!9bl|%V$aVtaV6$d7kfqC&zb@+&w+RPyFwXV>-@Y4w@3o5V z;JYyqzK@CjUH7Hn|6`V)m|Fg%@Y4ntIr!cKUMnO5ToDF za3cL$;WH7$aJB?9c3D^a!KPwQP%0{()h(3wJg1;c!C#Ke|0cKZNQ@On5OU1QY}h*+ z2_Z=m3vWy;nis^;nh18B?H-uXGuvg0SSWy}jOY%nZ#*ji_XMvx|H@e?fm3!$0sIN! zopg5`-tO7p@*yfmw_>c|Rbsqycz5t#tl-uBUBtGv5@7Obyk?ncHquaRf>4*``X4PA zv5lCv$&yeWVyGHAC?Mrj6>OMSrraSzf!*dm>*bU`cJS5_i%lh>s=|&l`&tmfQAFcn9uk~Jk;1uEC_4WVV z`u{bt_xf);MflG-0sN<0|Ak%Q&t=NmW*L_i)_N;ueyaVj3EwAn{hq`hwWr$u6Y$5W z)_>uI-s$&)Q-ptFSNta7=Tz(e!A-rxf2#8T)Cu_SRO^541o$~s`Ty_){Bx@HpLPQH zPgOpLodEt*t$%D+{!fzsM;MZ^b9$8j*`As{hbxzcCq6rOuPnHjEF{ksBR9{iB(7gJ+okixG~koto#c9!SLz6) zM3-u-+#{dbDrHSgCKd1rw9}HH_b-JmMypx5)OZC4c!*h(XJjxVqMTu(aN(?X5K-=k zngq|o6Xc7hp5-hapEbv}hig_DH4hmz%hFp6y)8XFcA3@uUF_1aKAdW^W|>*(3$?h1 z);wgU1%B+`K+M&#L}N6+>^8;xC7;8%_5_6nNhQ#e+`td2HgLgKnl(OS1Tzk+j1f5b4^ckMbLB&c=Xt`F%cM1m z|4)Nh&C8f=kN(Q>w2qgErMbswRTB_&e6uX!&HP8)Mt1124tj+4&N^vgtdmalK#Y8w z80$V7F~;s;zyFWJm21OeCE6ujZzll{&q_DVLF}ci+@R=?B`@ANDmM29`W>$Bn0rKS z!i8{w2b?Q2g6Eml$HJpKjjCe}n{U61dd;!Rq~PlZWjWpzA&NsM2a?qFx7Ej@+nw2) zmP?rYf`{TmzqIP;^Ny>fQaKMYE-Vp$iARfN zd$SqvO@+PH>^ZFN<69ZO%WV9KXPd?QJad9CfTt_s0Q*nyP2)jA2TvjOCYnGSQ4yRv z;D|-w48G4a28Py+<3LGXn(Uw6F_osr_-5@OuA+XM1fDwY|Gx|0!#AD^d=7su4bOJ; z#l$Sp5?WuduoPDo_zoaPREbg;Ju8YzaUNv*O3aR24-(n5UbzUd_pGRVXmat6{%cVm zU6e;zOnGc~l!x%Er~X+8D9WRzJv?@mp2!)y46Pw^Ij9hs&Y8AQB-E!_S5!zE8pNo) zAF1}Q2?YIP@8@e^EV5s-lIYE>b&K|>MgZ;?^+dq1PSGJ_SD9m_42xGN$&0@G3Z-+CT55Idq9c?L~PEcSpGQHX)v6Tz{;2)X^V+6usY3Ax{=*6{6=b z>Asr7y!1(1g*fzC2Yp0;d~D)$Q5;=~v)CriQsf%qEVGHT4&vk`Y4)zfX<|CNH*GR| zL1wZxnLjh8O+>zzh&CC!B=GC*waJ}In=BFGzcQ$`i5v>=PMb`>2yFsOX>B5q%3Scj z9!Hz-R%;V!Ev8NGc&V#4QSslWa5|t`)C1~dmZM5Y$Ji>w$M-Qx5D*A(r9{OJCaXll zj;E{L>Y02Nw|ZKtNLIZHe(!Yd)KTX`tA<2cm58!?hl1+D;hO@DKRiA_jt=k$#}_(t5!y%^n^wh zSl2u|h{lYXb+Wdw$*gQPYt}hS<~eFiPhM1bXoVI?TOMubbB(dh#{1@MuQ`#KFWm)r z_2&a*+LJiSE)DDI9UM?9???bxtSv&cwlm#IxusWVSdBx(&{zCo0}WW^aV6SxlCWFL z4zofE(^{+Z%4KERmS>(bGo%^v zKUV*@U^kh0N~u4Ff4LsY<{v1K!a<6C)F5GA>q2`t6aMcmfQwcL7iE1kE?U$*7oB;M zl=#};i;Evx^G#$7Y@Bw{4evFzr(|0N81#^LMHxPtciXs*l zxoGZbYU=L$hNSPw@pwWlg^|T4_}-ZC9V0HOeRFtx@3_E_3NH)IDfx3!etVOCtNpa1 zaq_~|>O$1-%T>wx{2o`UbXi~dKZj=r@u7QxXXT4rkCo6P))xrZ_hktpTf&kzYTnCZ zspuf=0Dj8!dpT*2XkXG?jyC34FL9a$8$j}w83TBd&s7EoH4Ec&?}Bgfcf1FNXJwc_ zl1-&~xHqz0m`4>khO`j%ET4)#yV6@XGd5xHm~7Ei+1 zYCX-i_4?*utBU)}N%f?E>b!TJc@3v--5lvJp`lW2rLxc)vI$K>WaI{ia;En@0x5}o zOs{i{53Ijar@z&6&l-aM-`mW$`Zd8ll+6jrjl>0qzAifwB!9snVo1+e&9LO=l7Tc* z*L{uHh-E%&Q>`2h5B@ehDwAe%qQ4NpWIac1aW0vAq_b1G$YO2DCJ5OKQ7$=2)q=nq~P*6|G&+!l!7F#Z!^cB0HJZ(m%+Z1HhnI(@q?9wzSJ7EOXPB}D)_t_z8vcp925FTyr}UnXqNuF?EVWr1V4fg!4LT8 zw!JLfp35o_*5n@M7(N6uz2n21jE}51J__YnSOPwr_%jmEi|b%K^?!h?iZ_SmxH`En zXP~@!K;ATmej;y5+lh<`AymK!;YW}?ba>Wu?f_5YqE2q-e#U;oS?Dw_Q7UduD#H1W z&T5~8h1}HESNsKa*Fp4WG5Ipn8^`%PI*(k#;t4*X6D*0%iusyEQ~_={BOfRTL*%XQ}I* z&Hm~(2BuEDxF7wCcT5i54mK*y(^bprUGrpxGf(MSFobK4YEHn5R#^p06bb3hz`g?-rZJFA-s5qD@g953-GjvEq8krY}ynFev;=RCNV|de@ba# zf}OC+YFGu_hLnMx>!(l?eMPF`F0T))Z+tl^z*+n)GX}*wU=Ai??-&L1`_@-3l97b?2>{Clo4Q}S6o@T0 zf9k75p127Ge#&2@_304 z0sf+^9ORKISQ5tnkHa5c4U?z-k6rj9aGEdzL>&^xbr)t4`I}38#z^ z6})A{FrxeKGQXQ8!<1}+!b?B6Uhxr^Fpr`6!h(IY_*vba(>d{ zzk}wcH-)~|*~W`y2KZ}^wd8gxx=X>qeJT~!lWlcB_tEAAo$QjOntBJ~!uY>Ig5ujRF8 z#i!uS@RO?MZF9MTnwtCgbA8j7C?Qjme=%Ih1LL=BzJw#i@DuyU9=~%9HGc0EpbT@&Y{sZIr({MK3II(Rh@M`!`OXgdc94hoQybMygT!jd*aAcr{JU16aTaFS|I#7iM z$TyBygFmOVtEWCnUVoeCjt@wO9f!MXhmk+mSTnh0jhg})TynQ!b7;NGs5!=mLSb8TW@&)VMO!$tX8m*uNL;#=Xe zN9oKVHAn<5Fh3cdCsL+M&sZG3L+To)1g^jT<)=g|O#g?1RP+Y;bsLxQg!dF5j6{t| z69+lkhexwWBku(i-!sDN^6y0U_5Ex_PP8X`@b8jb37D*VFYgKzK9Ke%iO8ShKjv!x zQLN!Slm$JJJ*A>l^JO?DEhP0SpW>|xRLv*$DNfYiC7(&|FZx=*bsOH5Vxh)2j%)Na z700)xt2ZiFg8oYV9+OvPLiHrCtllQbt4zitubYkrM%uzUS*UV|JMUg4qX_F;q*_=n zPZie5h!yvnB#7$Lo(mIPZKs% zHvX>-S6Zwgx{Yks|KuZUOF#V$)1Z3(R!%<@CbzgpwaCc_-D2!|A(!=ChJ5`xw!|IG zk_{6&em)I;V&$T*aA#fcGqSRNYo{?yR@QfgE8i5@=519AVbSN5eUz{iwu4}1MqBtQ zd)YkbmPwPn4;=3d4U_t~4C7evZ1Xx*<8-OqGF7Fwz%1Ul`G)U&NSC~E&D7o zn&!VG=3w)V`yN<+QSrK3jZH#zVH_b*=kUcOicZ>FBhow#1~3BlQ|C$(3i1Y(p;zD=&cT%E0{^4mQt+SA zsy@>NfkAZvJUJ}}zbxxoMU54{i@3w|qV&L~gnV?hNZ^o_Fgm6XBsfg$vW$nh5!o%k z(0h?&p@f}{EOd^F01nDUU6MMe#*nfC;gFG3&CZP zfB&s=XPd1~xuD|(&x(IJ_OoZjV6z|7DFj*<8dbZ|`k00<;34~8t$jVQoa$h_es$Y~ z|J~`I3spN!!(hKo4ju7&>h~chwHNd3^3;pJPiD& z2zimKb1an?#qwgHe({jJ$deaGumQEG+58xuy8zuMB+0SU+@ zwCMqRd}Os-CgEM46`8_vfB%}+@RJ7y#VGcF7% zbEI&!E*v-;1u0R(Wxfpkn0{KqZ(Dc9RFIq}4(2?l3`}mQc(9P@4ww_alhjBLS?OyP zc`yX~*q@B_s1*mPh6QJ}#n^aWT;X)1ayNFb-2G}_1ItdAnO?IW_i$J4^wd8l968D* z+w1c!}1n!Tz*qk%tHOYSm`H`kN=*s?|7jNjB z_TVa}hh^}}F)+VJlV7ag+b_7Di)=i1{g$RwU5wT5TNO9TvaQ?f>j%KExxzm|f8+`< z*Sr0%c;v1YI5b*Bvt5uZ;jDxfehmbzuF_#iEyTK_8#y%Xn-&?MRrMD~qN;!IW*6s~e(@Bwlh`p?JBd?&dR#^cm8p(Q&T}B) zc?!>tgxUTi!c_(t+tN3%Widgd+CBB7g^O=+$(8265l$G}j!0!W`0hruUbLBZ|4tN+XM zz#kVLnUYBq9?WQScDpe8uakl9zT@5`|H#c`7VF#-}_ZFQb~+ce}oFsc$rY9+VySUD#dAwcybDNvpu>jWGKf zYkYi%(qxfyslfbnmpO--h-2!U!-!6nE7hDs<=ZlLtz)6JyE%tA|FbRBEr8L6$fJAa z&lRAqI#PxW>)NRVo@x%9Zl-}pTU$+TntN2@GkNMO;GVfh?^Dkf2%CF!gL?j4A9>!Y zp2w-@-RgO+dY0HWp85@{ti%EG)UQI?<{n+C^8A4r(78t?9+IbiyL!G~J#%cI_s^>5 z@AQ-Bo$7g^dfua+TlBN4^J?9$dM;z>d+yOylBeOPXh_5|8*nYxx0J3>w3dMI*_#>v zWYk8|jOkv){HvjnuJpFd+~+~Z*2p2yr1YoD=aus7zL^kjXXhK_S!`7wTJdwjG1n} zs&22f-rJPeX@WF(8vX{pG`!=a<3I7LQ7PbawNlEuT6VH;jYLu5&iLcN5Nv!{eUhc8~cf8Z3|4$=my8mOk>Hjs~)crq-zObce z_1BZ7=n}n~GkB2q2}%=|_v&R@~fr*F%z@ zO)oxNM`P4y9oRmTSohSf%>S7riC4_O3#Fs`e}6a3{|(L>!u;7oUQy=+&!L?hu(x{@p)w{D{n^Bm#C7%G|s+-a+FJ?Jq_o?3htGH#}BLijuTLypP0~J z6&UBiANScH5{td2Acy(@DNg3UyK>N$V)0XpJKB+9)EgjUw{e zD57YMB8t{1qG*jGiq@!WV>-M%5t@ZPOZtWnx^Q|wh8<1m`b}LW{opi3X;-}G#XGFc z6C^^`WMh1`(Z;q00))ZYbVfxH>yB%?2{L%9@xvV69tkY5_9yk+^W*=*`cj0(3X~BE z@}9})tGk}tP1l!vS9QJrAw_h>pWANg!k;VS{COB0sOr*6g( zxYa|y%0R(6%*X2G9AQgQzp7Sx>Z;F z*8!^H?a|D%=-Y87>~=h^ z2{E8+^xLZ$BO@bu>$p$OdK~`KiR{NSFh07oAE$^dX4{YB)w2ab=N_G=p0)irQ$1_@ zu}(c}`*DnV*7jqSde-*iB=xNA$EoUB+m92}v$h`t>RH>5v(&S;AM4e#wjal-XKg=L zt7mOL-Ym~a_T#$oNT6*U4_zIJ27}^!clmcIn1`4A4{p{QpJ!@tx}vIz4`~ z)9Ehu;~P0Kig&dit@f^?O0hPcySE<$w3^WW|Dyf)PEA7peLeU8;_NQ{kK2!R-SmIc zIBh>A*pGAmAA9EmXH`}8{TaC8sH11pQOBHgtWgK00z;dTK}VUZV}g#tC>A6Y%|k_n zaxYLaiO#*s=`cN$QBqN1`u32Il_miy;_yenKNSip3MnbsJzji5EhJ3l{r>ho=iYON z0YvNRdEZAqpW&Q)&OUpuz4qE`t-bbIYpbouEc(%Z^)1>pxh3 zd|^}s4-Mf$K%K5q#=EYKfs*N0L4arMaEyBB<${Cl20AIYhK>wD^tr|82%{c%8^ zKJ)tHVA1FQbp28NM1NcdYm_ed<|S5tocy(%{+Jn!{5reXk5(Vfqn!SDp?%or{b-20>yM||w~0Fn^v9#@(_P5OLj5t^ z89Rm$n~r8S1B*#Alc;z-jHg+QvMDzy1>h7XGlZjzi{+G0Zmj*Xed`#VZdpt;1Qz>; zX)M?jOI+t&Q|2A*CKnK)GFdJN7(?CPW`5Y9ec9xv1H-<2Vmd~w6>hvO*zU=_GGei` z@;3X6R1cFid_d@fBj+zD^7Zy4WTF7M4rgbY<$88 z0vp`d*tVs2i32nLHjQz8HM6!E=*)R{lS+m~&I8(?^L+F@UR?#>@6UO6*8fE@YgNGk zlbr_vOC53c-lTGEZWfy%=D^T{h0!f%cXt<5v@@DuNt~pFz@Ajylr}~1T6b~9%U3>4vgf$uA+l; z_^I9<_?1Ho@iXRxca9%>KxK2jy~^F&8`Yz|t;g?Qd%|Do(0OVjz)#_MX3_rdfg~QU zFYNN?dxB6fUOInhK;9ub9Fwq2&-Qvj@uz#V_cs`w%q2Mz4C~UhXMMe(@bDh>{cLab zS$Om3lSS)iyf`py^MN_~=!Lj%A5w^{`ksyLWByovF|U+1a_5KbKfk!E;QM#SFS-uR z^|u#(vEZ!2{tibsD;TVO^f!v%J@AW5domGf}Bx=jRlR zRq~U`{PY41S!tfd=l1XS(R~xI)p0dmtphx=AE?y{CGMc~EBI4M<;5HC(M%a_w(oE_ z5UR1da_ak0WD}#)UC9J7(5qrJymCNJxl??Gyr9ii0ybZE?$0{Il2c!~LAxV$7Fr2T z$5Lc7CsQ-+R(c=J{wnl;YblyJvV_g#1;+m$Yf9`AmIEta>}UMd+-=#VQF^reYO`>* zm-7*~>Am4s`+FW~kMWUyb7XFOYIYx=S%Z5UpMK}R)A8xiJ_jf^=i9H^z5UW&+rKgX zPTRNqY@XT-@K<=gViZ2fpI%7zs~_zRC9gO30LhLj{9v!=FZOJ&mmxT)*Y?_bZ?9*4 zy`c17GW6{}u|eKftS2Gm?O4C}k*7xRtDmz#3`Zl_%4$F!KA za8;UcU(|Z)I=$b0((OLyR&lDAw20p3+^?uSm~;2Eb}|cd%Mt6o8^b5m&cWHZ1%&gj zadU3|<%2gEGSNrP(9WZwk9Wa8=M{Ya?)c~0V{-Ge7h>GXme%fr*?L|NV*H2l&$YCf zAI|;dpF25>YfoeO5;ea){+W6%H-5eF&#DS=wio_6Q1Aai{IfxIS^3t>D?U# z!+LKoieK^nUQqhgUh8`um)HW8B?tdL`Df=*`{18f(PDnWy#xMvBZrsmY0h0k<@?V+ zV;ghxuYiA!BmoT$3`HJ!x$2&sTGIE@Slv43maZ_O5D8ybObBFX5dh!pRhLt|O4h#Q z0DLuNiEFIJ1Dq#P3^T`2A-D}gDJ^F0|6#n)l`+qTlhu-qJoK!BbE3`ezS79=IzA?J6N1Cz7Q&Ybsj7_-j*HdHc)7 z?(gc>PC3e5s}w?;y<^;0J4Sb(RoW-99u=5mO0U>Ys1N*ws51cdz~33C?Kl1&6rRFx znkFw%-2ev9!TeiCn1?jv$)|h$L2jeLANeGNt)cRfFk>@}7QxnG?W>E2kc>3>oT*Pb z^bX^cYNFmO?)3lu;!lY>V`|8M0Uez9*fq-wa^eN`5eY&d#JMp-Q>w4&rx7smFEuVo z`9aV2%ej~HeF6GpFN)INU;FPj{w^PBDGkCP2XaV+UCE=Ow3m~=jH{u)3v%?wO6bYm zXz&%_z&T@yrNL*Z$}|$JONi+UjtN#Q%~^@vh!taZDnzCW4epYf#hrEOMQ@shr-!)d3bu@_d7N30KXbz=C?8m66RR< z?Boxpw|o8FrxT~4@7dW4JwhW)rhyvqU(yQLD}^TOo=ef!K^bT@gci%XxvN&;Yw!G7QQ}`(*q=*o$06%8B2Pjc{d7*D;JG?oMavPOORc^yAFplMLBEMMfOTt_Vx# zCnI;j86&~U5$@rZk3v5Y>eBkB;OFkYcmEzt%U(C`#$NOOGVC^En|p-<$4)Ut7t+=Dr$IJw{~h>$F(Jp(GaiB*xZU&*O3Np z>Iub+&8Dwk^}B4MXmgbKR}rHHh0C8KJGR~LHx*!3`q)H*pbg`9+St&9;B`WC&c{s4>xV|gTbB6zd_wf z612_xM6B-hseRlHe1DRmvp)PE^Ry3}`IfEs&~WF{>NCuha#bCu6%l|@BFBzzU zQIA4(7EtAf1bQP{DheR!vH)?Dt@kO=)H#_BrfS)i~H+CEM4eN0oi3s2*_LuN3kA3OKh4VkE zZ%j3cG0IxVfSJdEAlEF(OrkFM$!VG~5DY64Qo)E)^50{AhSRcOi{&yfhgaflav}?oe zqYqYNgd@y*(1GUJ0_8qvGU0F zm;1Dm{%4&_I=t?r5^pea$iKDOrz69+ul3^(E8ii0nMCPslbDxfz@G*Sg<4?u zj=-B&83dLY<*QHK*x1ykb{m$xuNP%}h} z_YUy|J)+8>4jjH)r$Z8K@Pj$*N2PV=K8R;uJYXEvvz^_YHooT$`j~fZ|lSNzwaI5lWq>N zbA0kyUNKYG{Y~+`*4J(UE&1@$i)fiOR%4Rtgc|liO&pRviuw(c3z@?Kzz9+V|oQ4 z&XFbK*NEL z!YzP#KrVZf(f`QS{8F$~R3?~4+2TlF*)h5pG}e#iQA0;lACl-*kn`&yyHs4=r*6&E zyPQQWq?BX**IIYQuH>I6S5gU)sV)h z=U=%V{Ls~y2G~&EgJYjV9#7+q#vs;hvL35X30INe0o>j4aC-ZHu^4!|j%BP+pX?_&^xwpe!`bM z^7ht;73ISsx~kLQ~2u{c`%BnX{2a z;{aRzI*OV#Vr2awQ?J&C&rkhS|2NJ0@G^dz_2K15n)TtTsYjq9vd+mb8BjH~INciX zhFY%bNk?p&rs>iLOlN)@c81a`vUeEWAG+0gaA$;pcCFdtdhjyC#LqKpE$ulR5nT_O zCDeaOfIuC}R40>S>^uQ5)B=DKVHbv-u3i5v$vzTYfkdG=3n=m=^)}ak6~Lsl|GTaK z3~VnM*p4ZH?6xHaHIL>Ng28$!vi@7KClGJpEQ$cc$oTE|`tL~Af7a?`7OiIeXI6Jv ztpUArI?Aso0C^jS7&6ws2}iSB`9t&Tzmp_xe>}5T8PN<9kDEr6I8wyIWB((Cc+DrpX8lpkB44JTe+0Nq; zmz;a2#T_kOVNAjXp5$W^%8JNe zl(V;()j;-v96vI#2HBtMuwQcUc7-{3ze5!Q_0XpLQDlY;EIyUL-r87&mH@ARJ52hI zcE%zElh0>{oBkN(Gvg584V>r~q%%`yvk;A?q4{*`wg?VSYb!;)rh~C_z^@x$lBp4J z@s-(f@JVv6A|>Y<7W4X=q&eW*i?8%Xlo-@W8Z#&gVa5_(w|eSe%wa+U<~{9qyHZzh0@``$pg&LwSaippntuwJ}Wo zCa+kR*S9XdAW`d$Bx_lhbK`~72CFSh+x3i?FI5|079FZ8%e=lm<_McME4wn_M;7cq z9@jGv@am$S3`i=t;Prt4yyY zp9~V4Dxjw%LLcU9@`dv~8*gQu&eDUIbsb1#OYxezb;(z?_ZKOReMin=6`rhqIRF@zO0lHlQ<%0UnywRetNN0-e9I9&4M+I2IKl*F zkX+R#!Vx}bNK+hPNl%Wj;np*^?insX+YTtzmDU zAKXd;w>|R%qNLau?}a0@x>t&^=5d;13)}EzhZ}ff*9xIR<*@6!arQmOZR1$+#m-GX z$1}vTiA+oY!y}IDxTJW{6RmFfH230|(svA>)+f&*SiqPW4`Jg38L)`P9xURk6$cgt z?d))FI)pZgyEGMU<$1_5RFP#FSpW)H#yLUe#JeLb<5hh*)#URl5khAjWX8U3?Bc|D zH+-a-a_ZxnbL}C)Q9d%5ciDBB2gF6X$bB$Md}Wj5cL6`yHuT-{lSP)F^btRq z`tJFOCxNF#r~y~PUhS@l$TIdl3pkeHS2cX4BQ$Vmg-L0ztR9qctAki-0~R(o!oSMJ zziK|o27vI5MhG5*)qKdb#s2d%hr>M7d! z{|xrV22y^03P1OXqV|>Sn8{9D9@+mX@VzyY;p6?R{Om_-mG0%TD&+Q2&pZ&{F{;nr zk+c6SJZ9$QCHb`9bh!ZE`H`^$w72nc4;N>+v?N-?KWMzF&fWWXnfGshyauYeUdL-B z!(~LG8Lfd{AHz2bPvsp=6n9=GvZ)mLw=eu*FWXmq<%u{)MMIA6edz$2dzKdy5VDE;Ykp-bOsm;Jb4E ztOfq$QF@R+a4;k&5xEei&y!zjcmLuSkH}5x=&<*!FF<*;zGHi>@7ulCXXnF=csL&x zHS{(gIwS8--fceUn~>$$c#o5Fzcqg1FgBdZ&WO#!hV4DxFQU(!bF-o|%1cEP)?QW*!p9-#N1+4hz_(_cCUnZ& znD(xT^e}TYF1Cub)7u8K?-1D8OIb9rD$^Qo0#_{z$8%LafN@;QGSkVk)4sKebm{97 z6We!^axHOuy4!sg=gqge6DO*CdkKDs^?~ABsdk2(Upl$Jv#=ujWaprb-UTH?E-0OR zK2D45eB+8%yCSis;DdfFa0j@r+GAkRfccgD!JzHzSI?snv!Y7=FuTaCuQ(h-h5trj zmCN24PK@CC_VCf~nFv10(3tEpW+Ha+ec)pvt`-09;bV-qq&k)i~&#@Ds&E62Dy%|Z)$3*W@D%dy`MD;YAZbaIokuq@lf z`U?6x-_YZPJpFA*+}*R196#u28gGu#2=!&JFD4OlJ~78CroU*!3jM1EmLezBszq-= zqE7lV{K|Hszl)T!Hyet?4qD?Ks~&^Yda$4=)(Dn#Fp-YI)>LZ{)`UfY#kGX%Ko`Yi zWI6U1vy$PsQS-0od3Eoy{)(RUYyK=Hc_HH_rZO685i=PDR8{;Q!?l|0q=2!s;at`} zy?UZ{WGWNKaZEKoVoY*(Rr`MxU;omvf9HtART(m(4thmoBeeOy<-Z($@e5R{_-~O= zbkGy#`|55d^&~}VmpiHd;!1NB_+U*pgSzg@tlM=l9|qD$4P0AnO0TEaL(*@+E3yOy#%G4NDX-%p)bSUrY={72A%1b z)F#K*a{|pqZmDEkM|>_v2p5ZyS|_O*d)~F7f2V!BZ5W&c*l}0)Ku-Ealo~zLu@Y57 z2Xfd|a*)C~POCrFNhJjeo8U83hYN}RLJDg?#LkBrA0cmkL%9Ro(rU&wcB+M}QU^C5594hT?#}j-ShQn8+RRZ@7#vrU$)w9$V?MI_yJx;4=@@`dgNtZx5q zTh3D64d3LK|7q&d{J4Irhz7oMmoBrbZ|7n->fGvida|H;zAj6u@8r@k!Lru@_|=jz z*)`|Q49+yJLjtio3FwWKGSYcM6CAY3n=qWqaBo60mu7FmC@!PC38T4;_9l$sGRB)Q zj>|Z2!T>Jgy$J)kOz#v2TC;=c0S#*bDov+tbuD;W-FPg+P7B2(x2q_V7kxZI}lh^qCLBWih}cpLT(+pkcauPINhS!Ybo>C9Y%#} zS?jiJaWOe1*Jr*#(sw3PoUQis@zlI56I-@9mRZ+lS2xy7uVqXBm0;`0_RZVU54$Uz z)KN+Y-B#M2I2Fg(k*nfcir{j)IEntO_!g`^@fwI-hd8w8j75WsYIgN3hgb@u6NiYG zW|w>Cl?*u#u5mE+ISU64>f-xE7Z7!fD9NPpwVTNqFL5-r1V{b4V>b=j?zL31Jza&$ z#JL|hjiWS?cp+9xxt7g|BNbAStV&dRv0ph$OKL}KNj~YtH@So71mPCRs^k`T?1uKX z4aMHrO{?RZgh%JbkHHQi`0MK>D<9-hkr%tseGVLDE@hT`V=FmwM*J?e)Qh$2@mV~s zi*K3Imlp~~Yuj+m3o`${?j^URA4cj^N!V|l#UmbjZb^H5Q}OCJI}Xg+_@=sTPWvOw zLcY@{QzD=Io_kQmJ^=(5FGUt@2hVNmkrFk$a$l1;(K#%9JE+?oyV*-_aNo=x12z(u za=<3ys_u0!c9(nL$Mqz~dwWA?2tvP;(&OnJ#oI;JSr&WMP} zIBe)p4%Dby<*vqAZtU8ix8`^t)2x%42MC&FL%9xm0$cu|hoxMnzQFI=$DQ=GTp5^7H=J&bg9cy~ z`){hIWv!(4+1xIrsUp9VGOD}AoZ4Y4ywyBo=2!)aso|st@kP+8DtHV1fGJVzXSh+@ z0eH!@W$f{_g3muYsGSq0x|1IXzW1k!DEhdM2m0A6C;caCDfn;>4;TKtwRfYv`Sa}E7;nB2v=V&tC1CktyfuPLp6h zBQKcG;_VjR<^;1rRw$TrFJsvi;rhzRc|Ky~^X= z^sOb;2D;(ZgF8k~1a+CZhaoGoO0DIXfvkSv-?=LD@GBf`1#0M+&^6H74`!LMkn}i9 ztB56LG~k>ch1|VaMsw54NsNS9k60~^0;f};mTZ}KP#r#t1D*DH+<7#B$gKfMqB_a) zlpSrW7g{C9i1jKXHV2&GEFA&2c-&c-I$El=)+y4~>rYBv<1CzbLUG4PVj)u>);rhN z+dH_ckB3y~q;{zECHA;VcU^nt-1=JQru7uU5vRL4J-g+iqTG7c!sn#NFry9B>C3qD zui(EZK9qt*-AH%cL25Ygu7$%cjzXc3)Jq08%?6>}}OcAXY zg7<$pD13h>B@rrjQ$qSdzi{#4q_^=V1rQSeZssh59CwYs#TE?oHJ!7#5FCaGdKm9o<@pTW@{nl*bZ9lu+)*+{@DYHd@WDdPYw8I znvksS)pl&DHcBgVsMs>y$Ha6afP9!^ zSv|SSWG(RjtOCzwmobR7Vw4RfotpOykoX#$SGZ($%Z^adAub@hLa-o_X~8a0#9YK5 z_-Po63=Fm-Xd9XDBO>>$&9z_qcN+>q-<*f&a*L++Z@ zRUP7ud+lKVKdXZ^K?gMp%VRF6S8TuI;h& zs+`m`R!iZ#HJ`?zjp%3ChXzD_5OiEC7=K+Yv~mfbZ5EA4OIgaQ!d92-R~f&|_{j0I znO*YiRp$J8V|NRYazeDi#yoPYWH4Rm5}E`^bj%9~BOHE1Iw+i=FVpwGqF4&a;82Jo zM|~Ck*DbaTF?LGs@oBTyImh0O@;W8-csAPWoM-RGc%5P?Aw)*A=32AYEZQ68H8*n^?KO|$GRA8j&1Ia|Jci47uVEZu zPk>Af*uf%OVtkKaSNM(MMaCWy(%9|SMCgyRLcg+;61&Zv3KRfCZ}S{vKmIt2_2Sk1h53OAzYn(wTi{WgJ0zE7?9h96iGj=d(fk7_K$KOcH()8&-i*} zk^7L7`aEZaISbGKxchSL^2Etui3cJj#@iB2D&eHtRZpbDM212JIcA|0G<3KZX5spj zE9bRA$iBP2^JO~`d^#8qf!a0r%CXk4oq)iikPHcipht^R-l7~hb92y6A>W;gO+2Kr zhBE(9JIu{A5W5+i?>vrU*SkKRcv0>ImlN8igB0%Px`sgTK# z1v5Q&yBUA<_Kr@wKk|1_;{~R*=5qg9Tfng3=J8M^OlSfZn9xKnFri6YU_uElFrjH& z#(T}vxxj5_&=>CDwkdlDx1DM4;I^~u9o)8)JHG?0g|yf_fX`@ZAQze%%!Q_ExzJQS z7n*9|0>5kY57BT1_PAmUxAox~lv^jgbOfB^yLA`XP z8bwv2U(o%YG#JW6RJsBl0RQviWOQ#;t{_la^j99i|3Y0u9k0_eQTp zg(ww$3kbua2T;_~6HDx#{hM$5H;?c1&)Py3<@)zTu5Yip<<+)t%l#32WH2%43IDsV zbB_e*_OI7h&Et*D!dcoC`I5~c+Ld3$a{p5l-SgX{cnhx>Ii23IH3iXs2&lT7+4_n3 zUuTGUd1n5l&Laq3vP$eOu!oKf?V)4Wh&d)7%d{=Y8ABh67(-XR@|Fx8QA=oigOhSG zUCI>dq`Dz$*{=>quQAK4T|8{$H7l&Nq4=>S81TAr5r0$blIt`H0^64lgkH-Utf^+oh~d|i6G-C7#-#OnBZzAAFtFm-O` znBw;M`eMAN>z^uIGC!aEVCW@~QV-PSzceva|Uf0l2#3U^MbZtic#{r?J+}3<}iHBmW!#Y2Vc+InOq8 zz>s0FaODh8~>kQhOApQ^k3;FH@jok!-tL}{))YTE$x5W z=5iAGF0ZY!n3$F=IeF^!ZC}NrIp)P~V#;LB_&u(?=l{KK?3UyoIl9~%yWUKCkNTi= zwh0|+*dx}2*yq4=P;C~MtK(|~kpDiF$gN68@H!ZvRq=J*{s;8d?wQnSJ*1Xo3|b6&iZFeN1)B7Gr=AiJb9ECnA8>+D;jdPfih+}(TJ~tRg&zcGw()cVvG9~ z4OtTE(2W$ho%&V_Lq*Af{%D5N*f(ldIO!w#QD@6yvKIZ=(q54!(}#GV{L?!SI7qt& z!%F_BhuZ~3;{BMo03qNpn|V>Y&Plz>mDlyo!eiJXZ*&&UJ<=F#)~4WUZ}4))n7XX$IISVf#s%Cra<5h_du!_{mAdsI*k@h#o9MKZg zKj6JKjj(T_bpsCkB`kE}ZmoP5eD8 z)i&3_PVaU@JJvnIm+d_L3gfOmI_Wxq9P~YVmQVbUPF z2;hv!v-YbvMlw;IDR+&ls8fE+FwQPWP&;z7fm^%(=cJ#Z`s|!Qeh2mY)DOu1f$E^~ z7;)M+K@mI~^aKN*cJv~k8xXL%sGmk4f`*_zVeb&{%Bt(%l(yld{}a^9i1$}bOkU1R zSwdMM0!cs5(>yVN<7Ovyf&t}PM#e(dW&Wj%jt~82mEQPn*UXt1n@b{mVr_`K1p0 zH9Tgu%>A-sqeg=~seIg41bF0DCaQV%shnTxc~r`^fom*NWL8M>J$ne95b~S}bSNcG z>P8wA$v*%rhTQG@=Xo5|EmQXN>Sm^oUknq<^W!Le9Ii33({uX`yZE@uNwx9JP$P7B zuHIXB9z#z{sL2rXpzZ!`(s8F4&hbHuG{ZTTVj->-7{j51VkvmdWhh$C(qnLJD}06@ z+$i-`RqL}sbS6?nTeUquGuyWt67LT>shPOCh|w<~L^~Cn_Sc0RGbcm(7dBp(QXfUa zQFKI85O0~V>sEm`>m_F)gvPLb#SE){UBdW&**{KGG=Y7$xnxK)yV4u!v$OCBzg5#j zD9t&QIO(Z|V)#zJW;BLJAH3&#BhT_i(?#X}{I9Xw203 z6mD?RrIhjCqAE_=#8WBrX>19AGDca@<+r(o>6kj7_o%>%uXC4K$L|%sHYJ1hou$g$ zCsU2kTI9y6H6-2Z(6qsJlRrp;Zmy+drI3KWmDvm$r`#w!2`hMeyS^L)anG%uXK%}x z`t>|2<=P;BL+Sn1mahGgG;ilAiT}LIs;=K@S;q`|nCeO-^bT}gGBahSv6;C3c>p5) zFyLQ9EjB`YEuczVqV(0ECm6ma%-MWk?gl-~8l$iJ5h3kkpaP=_K-!g<5=sJl!m8@VB7NT+!lCuRs$no z0My0eKO_9FfHdo1?6fbVyxfBB#Q`GNh=0yJ3VFiXd7UspkyDU}x#yG%4h3rHINZ2E|}OS+IFu%d-le8Fa{@SkYM1 zL5uag<+jYsdAb+qO;&%&%|T5p1wZ2Wj0(AxJ3LFxlyuS=e)-4H_fAxE!62VhdBF|z z3C54A*jB$D4(g{v+T+ozs{n2s&b||nhZ7knR zQ39R@IH{Kym^|@-IERsIM)&HsCs{=P7uLVx0SSs$_D|Iqp>d)8;;AMTp|cH$orCjLQ_v1k0ll%GM| zCjOzC3v`-B9S|L|lAlD|UiG^66IJ8wh>Qd%{Uzekcf{I{RpzKJ5IL>$c{+;z4@DN9+6uKIFUqw(&8`xR}7o|DVB! z`y1ut8UseAAqQs&+tZC`l^h+&l})x0%Z zc?y9Lf90LsUh;V>**6e`mhyBsZAmniWL8PyZLMj=TJ#XiaySobiHX31n|?Ssh9_*B zb=+<3{dWq&Sw^-XFt098eB4mm8=rx~sKUEb$1^DowP<4fT?I;m^deLSM{D z-%o|yR`zkyG9k6UGM*yiT;|qvg4^KE?U9HGdVM`pz=+$jH<9PCy9RnQ+txVdP=FLXLlu(o^;q5@0|-^*Q>f&(6zh1FR?AKjJ~D_~3YTpy~I& zl>ujas%Hel;ZMBP&5qrO640#h1$T8vyv)TAIIUUgI$j=$V>)|}SCBjV2QFn|jHq%~ zu}k#KpeNW4Aw|v8?A&BOhzmFo2e@jNR?AykJCCh(tMCUmTQ+rLO%THOokbHrgAK*ZX!;f3FeJXE)oo#A zq4i?0v#71B@GI$Y;n=2z>BWe$R`x)^#xa(0x0ORvTA+(l%fLXbtWL&m*4Eah?BQlS zNiOWgeq8sg+i&vFj={HhBP#JHj9u@I*hGTs64NfVZZX@#iNl;l@lwO5Qexn(bT7G7 zyO+;ivx0mg>}WbSZKNUhS)nc6ARfX?SY`-l#WIkA=T(1%U^r z2li|mX|YSy+pXKA?VGTskT;&HUv1phJ@2G98BRkF$5!-i-|&RA(Oun1q!dj{!ZL zE~Cqm>TO(L+6~B$&3ZD|IETYWWp&!hlLGnizPX|NsL%YCdw5_yThdUR{>BBoJ<2+g-S&RWMck7cNG)J;qI; zaI^Adfh}U>+ub?&_E1#5E#ISj`%(6%dy#M0=ex7%cPZbl7{uXd?pxPytruTPRgdEW z@rJoPpH6n-NBHSRFAPGHt>Z)1m5JzF-U|6yviL7s#oJEf0Czii~XzlgvE^D3!;5&;xX7~ou<_Kp| z%2a9v!wFO+hLbvvn|4bevEZxrhl=S(T0D&>>KNaj8|hNOXF0Ab7a1i(e~ts8>_Rjt z(|J;cfzy8zL6g_lc&%~PKVNYI47dhYX2B5rj_Tky%jGNWNavkt`?C1#HHb|>gI zF!**@isy}X1TxjKTrY^91|IOy>VDXPYiMIj}S^1*!W(- z0~i1UAb2ItuGX;0F>!Wv%-jv;uEgBca#w2Z>bWa3cMaT?>#m_O=oc8YD~S}5{LH%m z_<|l*h5fKStfY{d4SJ})w|7+vwf8;vwf8;vwfY!o$adxm+h+rm+7k!TVj5nyT6iBN9JPfhKS}jBkbkG}G$|7h9Fm47gLIx33L&cyt6UPP8B5bV%WW6qV zgz0AvVIO9LGPExRa77vd@ZMZ_$I*!h6;rC5IyItI)FJK>Q1c#!^z~N+VGZTw?LQ`f zjpd+ty~uY=dV2_=la_lGcv7`_&)!cR3UY&)^OlQm@8@<>s&B^vOIwCP{0{!a-qb2C z?pvq_CEd*f~ zKEX3Iy%+Ex6GIel7rc$x5WG41(rS|t%AFepF9E9ejy?I;Gw}O|J?*;f0ekg1@Pg@X zCVC!w`kBMtlk$izq{k;MJ&G*By!&-3#_bUtU%DXji?q4N|r0lMxe{j@S(*6=t#!|p!BN6Imd))@NlYpWRrHBl3mxiwnGy732P zh>#Tq3#Ub*mjJ3gFd7q`8WoTGM$P({?8JB##0(EG0-l;A85L})6;o77MB|7eNE2$P zAwp{rDI!{vur|=_k>hG8;v?J;>!!{WU}NNT;*>-wmokY?Wve>~1aFING^8Vl58g+S?N z4ybAYRXEL|<>)+9tkmA5AA=Hd#R=Tg5416Dtn7gWZZ)nv3a;+bm$S`oNCamqd71++ zOud2AvS#At;~2X_fy*2o$mwgo&5yY5#AJYwg*4{y)!h=u!MCcjrdzIdZjgZ(S zZH`4F+WxX|_lUMlPU=sLVX!ZvoK^DNJDP=0D+#`w)Dbc4gIMIZ09Fy{xCmupbFCb^ z7QRC*k2$F;`6S%J!I1xyllna$dnU-s>^@R839QoFd1^x4^1)t1@j2Iq87>*l?B&kF zQ!oL;se@W6o1NE4IeU9#7jHV z^rMp>ij>n*yogODH@PKyM4*qg$Jd&8xu>UGLLRRmUaq6G9}!JM22K7$-#GhL$*1JC zTS+c4?>Mh=4gq_ub%Xmlsm*ks&A#?OlV5B@-P2xdF-f9964&CXcMoY>%ib83k{(3` z&0fiIA=CqbY4`n-_V{CcWHtN`nY9!m=%i%A4A3Z_N)x`O&9~5=WHwct_VcMP+`l<2 zy;0jLPFmKn?1jMwkjD1IJkZ!C2b=Am(4FD^`H%T(sJ=;G z*|cp3bKBm{F6NfRSKGV$Bq_3GNWVm4$oa9vS9*_SZ0!v96L;~k_7lV3gZXW?!oZA8 z6Bz{Axy$@TGR_#g=a*!k)}3a9|B9Zg&E&Tr(|v?94$=;s;@xab%5h@a^S=Sj1YV&2 zvpl8PN-mN8xAv{m;eg`Yu;{s(W7>S1YqPuLT)*pE5e{d%uTpfL%5ZF^d{c(=%^ZH) z&3%fD$~0Y1Y}{NM8}|^!rKH)&xWvbBJp2sy^6E3iIovgg205cHf>CKhXE@aJMcu2V z+=mKulG=Wz9@lGokFcjtx;ujCFO%NcY5xk*XTg9b|5KH2;(@KyeFJG%OD#oqKfy3Y zoacE;V|2xL79E+hezUWYei#bsnB1psfejN&9Zy|7J{Z|2|3w@x;EQc6ROtcwGM4*t z3()ucGH+?;yGuM426F7a*oLe$->En#AHec`*4=PCz043a^1&#u(8we}WH)D?xek%B zEQc=j$SV&0$(ywVBUI{QgVYIp*X*RPfaSI-)#@ce=@-~=;WDuU6L>4FoY_jL;b{A}v@R3D;SIYqjaKA*``v8aG0M#LHLosk0=TIB_o@IO)?M4ed9I#1!?R9-P_j?lPk# z029|rgc%ZcX48@M_C!^_qj&O2EnYh8hcVa1F5gFmcBq5Tc=r;e*pp$aA#MXus(26u z+)4dfa0Ik}4EJKM7nFogWNQz1$psSmhs0CGg)IUY+#q;zSn7 z{=M8Ae~X|C3at~sA{T*Di`rp4T?_)4sB&xzMsN_(JJ!x(z)kI7q#2I%cd65T!&jEY zyuIr{1!>m=c)3A=40tc|sM>#?d!6?RNx@w5e)@+QPy*Ga%UQSs1>wXuw6EI98^iPb zrN-nBf6ss4?f;C2W?EgW8o!XAVH52P`?CK%__M~^HtC-unq`!wC#;B)fwPF#Oz^I{ z?`mVNN<0d|9SJ_FvKAO8ZFYvC$uQDjo(LLxI6u_C#JG1c6rxZ6VSvcvCy1ZB){L)l z0dvxyqC{G`>kQL$QlH_srH^1k_LDpaN$t`IWyoolRCc`eYxxg-!cdqA0W%RahTBCL zT}p9Z4o8Qd33XSA--z#M?$_(-aNQ9vU3aleeg?sA zgwKh(%rNEq9|VYfkfG3wLoO-6^LNZQGxrScD|W|y;6vu5Qlco(n*FTZ-wYyk)dt>*CMn?N!6AsIW=06 z1DnDLV&^n5$qI6%6Y3pTWp#NO~xM-Mfl??Gu-k6g|;v?$y_UMQ+i}g+JcW;`Q<3kEi>i!T9_)pZ3RxzlZ>QYn;4mXh2t{Ht?;Ss zPJEGiRf&mf8iR}EhSnfPq&k-m-$H5*+SBZ7tp2$12FTBsAkytWs3)MH8SX-6rh?v?i)Q9*%tt zR6Rh9&&2gb27g}zKTreTwlNrZxf{dWsJ@;t|3W|kmzwaPaz*-B|C0g=&^OzHU=4!X zZ2Z|az%-zWMn{QW6P6Gz_Ak`5HTuA0xHFm$u|z*I=cLRxu|#)9V~I`+N`$dQ>1>DO zJ@G`(5tK`$nqem(swTS-MxqCj%I3l9yuIXI#%GlEjOTin_5IW1&8&SW$nYPp2v$|` zd6{od?pJhgQyF_k@!6Tnhakeq`%>XGOyoSXj=_t)eoX%O949rLfH5yVr(+Tsx~kmd zEF8I0qf5;*uc3xt--&;NgMwGbWlO}W(?x}g`C!JhvZBNhoRl0WS(zKYtuvNl9Fodu zp25Q)ThXfc2EJOGxr!ouN~zUxO(m*>FUDu+c6D5{gC{eKIwo;E+F&<1chGi@PIVT> zztO&}cxK!+`d_?r#`FUg#GIS1iNR48HXg8m6tc4}zbNMwbpa7yO6Gw-_tVB^7pR9)Cx7@lG=h^8C>HCpYnBApez@-AR3q2Z7)4 z++6*aaAyD{VLIVZvt_@JSK^I_qdUeKP3VbLn$Yrf?`K|dWbE1wQn-`Yq@}d()kL|A z4_%;%`0+JhaOT?G^1^%`sbu(VdnK3 zz}j4zGQi1s@?$&&AGXOUDq_979|aM%xm&rb8=C?*rj7N*Q<^jr0;2&lVjx|}9Kz~~ zJzu;lPi5e}mi12R^R!;K*172uT=@dN9HX>mLuTl=%)w=XEpa+o;b+tiOq^iveZWj5f9mwh=3pAVWudhK@Ifbnc$bY#ckMBfb~{qR|p> zn3n1>tb}^~frfr$b4XgUim8eO7Ogo$CJi8x4(+Og^N$Iipk(dW%*^*&rE>-L$u)*P zGz;y}q(28zf}y*QeufFo&X-;|b~aPh%+m9irI)H?P|c|zS~)?^EeXv6;A3y%$nSMv*UFm6cmit9Bx;|=jl)j69{k-zw4f#a!_r2o|B#j zm2m{~`yki!&4MhX<}^Rdd>v$yt@sD?!0@90&;w``t^)|7Xs61@mbzc>koq~k6q6>6n!|qzT;YW_&jJBQcwLK;h_a1x^vU_ zDNZ`?dE`8sSzf_=w{88ZMhp@bbti@ZoWa=ahMNcEfo3P-I&Os$%T7{9N%=6$HqH9q z<|llRn{NMH{>$N$HBxX+$p-+*bdYEacc*-V6@TJas)Yb$f3`(vP=k!!zJ-_mYKrCR z3yU@aZCIN>SJP#I^#={A3f||dnn0Pw8ch+W{{?}=nH$Q-p(M`@kh8AO^{Vh<4-h%qeZTrxg6@*?$Zo}~~3x@fw{CvIPM%`mcHW~7+ z<3|8*z-t7U#+plx*SO@ef9so;&&1dIwE&>}esi3K=ap$Wa{(>6Z`OX|2q*m&pb-Zl zuRC`#g^lOxp-7Bf($UgYw-(~S2gVRZmxx0C)$VJ9wi}9A%z592B8;gf0Dci5g|q5u zO4G1+S$S>jeNOr;Gg%AqE%(y`M#AodMsSm%`ZADie!isXvXlQp`~Q8YM@9Z9A;jpr z`SUxR^y55T9SzHQC_tjk=CzG#LUVpRJ^vqP97Jo62lkNj#mHwDJ7 zOUz{IBUbr@e}{|+R*U-p&#iztP}FGo(9c1jNo!}sOFO@m)9)6p>i6gx&dfeucg-eF zdNIueG6mJQ*#DIrpK{vWH!+GD?GB!sdNin_sY?3Db%IaInNi2C=2vzR^BXEV1Z1eq5m@u0LK%d$7GihrZX|4-bdwY&KBu-R%gdqSky znxNSi_$aLLl%CBRR&UUDEe+3TTYwh-XNG4++rr|-6o%jEg};$!q5R9$c9Y&~oe=W- z)Hxt=2nt;LncM_J{E~Q5umsuB%$uu zBbyW>c}>83Yy2mK6F{yp@tj&>`im&;uJwORy&dOuqk!Bvo7?QxJiIUGVK7qIzd%6d zI{piM(9o;`7)&>!TGsu%W~O})WTAmKG7y<>e;1?Aiq#DyZQY2(AZafiD-+B7Qx#J$r|>3chFW5Fh>j!QOFOy1?GCp-nEzBS_KwdU^bfapj7#;jcU;U9**jMK zVt0GTXNc4Z?Hw<9*4}Y3&y2kzKGS~K!G{I*j-3C=aCwQb>sIN>XKWNci}(!jyMo`1 z{El&7F7L?(ZgL5jcqzZ1?vXg6RJE_H`Q-AdqJK*L_}BJYzqQwNaM~0pZHcKFvC>Nh zUMiVw&2&26#YVFGTIva}rQ98>&2RPqtD=r0>T8R3rpu|o8?iFzY#F909_j!WdYCvf z^9KT`yySX3csSOD)OYFa$uqUxxWQVdeszOJOKuV2g1@S$wKH@7rk-CV9`Qz0vq@tt zckbZo%;L|9R*qAN2E8%FjwR^H2#%WGT7B%)}N^^ z^k>q(>D%>Z`qh`sDt~YGy?>|3+p_PS?KRZy%b)32&|B!ww4wQ;!2igYnyr75V)uew zi65$LE|=oyyzfvSZV5+HhQlPcJ)M(orwZ$wq+NG8CruZb;YzeU$k(Z@rAd70ENUZD zBOW=agV|;db};V9*FKmy6>A1Ju1Vhw_e+fvQp7)LE9bfyjc(uey2FV}!D77!|D@Ny z9=;8yZ_Yny8d=Sq^dlJ884RyMgPg84{wINdk^%$5{lCzodimN_+ZFCX}l?s z^H2IUm4yCDm-8y`pVY)J>z{OocB|qTyPVWd0Edy#;ej1y;7m@)v+!Ed=3p>JS5io4 zYkZp$gEm?ZC7IfH_fSgWGZT6!rLV)~f}OdDhf=}#{f@o|O<%2&cK>CEeQJ_l^3-@L zU2NLIQz_nyr_!fM*<~$vr&v#=WM8v+GNq5ZwuhV2cPDDD?7!LW%cJb_&zSAb&ABPD zR|tfGo6^CM<6hj9u02j>I}V-ai-?obKqq~*wlH!|O5biV)7k&D_KAXh-C=y=Eb3!J zNE4RMiKH{G9p(fWyUn|eH~HE2fXd-icqB*3Sx>Dhbht2+hs&nw>7oMrsZPBKw=e&| z0Q3lI^gqCR>&WyP)yjXvY`fy6qfNhg*^BGO&U0>@P6I)On&5d@&gPeKe5hz068l8* zig16>X)otZkH*Z#q?5WEY7MBP2sRQ>$NRY@Brqh*pHave0Uzb)Iq3Fq;0e2( ztO~37jbVfIMjn*%-_hWaW6m2f+dI6M!EsVLSU0HhN^!nmLtFk~J+S4^rf43Y3$$@= zeVCopF@|Xd>2h(G<)@v{X^)R^DYDhxaIDH{579uF)@KDC45j7)JRmm=KU1R-FDB!O zWx}G1iXNnNH*wLYhP*lX{t*7#4RkiSw+T;cb=yG1YT-azrx7o$FCJyBmU4)RdTBkr zLq0gxRcmH^o@6?HT4#fpz)$ORKG?ILmXkUQ=t6j=^n%Zy;6TvYnfE{2M422$@1Sx^ z17e2<(k5VR-PeFaenX~R%uaC#h zX|22V2)W51G+Lm0Om`9>M`Qki7B#VDO&zR5q z@Sl3XY$c$|uJz~f0raMgYbey83~b^D@~8SlnNDd`_gd?|R_>$~Kx5shZsD2Ssea70 zr#scvV8Fw<3U?}bpY-BRb)Mi3koP4GZ%E)b7Ix@GW~A8_Cx%8IQUyeOQP9yPr-+UV z@T;@wtV7TJW(W*_hH>F3M+DsNvi`28a@P52GE-eoK0sNK zC!HTX05_s5bk|%hpY+4=aEZDRmB>*e=-S9A>uRHdU|O{EO~CzUaBF=tKFwXe?GH!W z{(I1NQ?+K{kBut`?GKQ*>$mvjUn>q^`eJn@JdvKm7XweEr@7!hu$D^!?I`ONAOxeT zYPVC7h2&}OB#{a1_!;nL|EIaR`cuth>&xUX5MHRS#y?J2Fg1TN?0~xqy@xuK zv&Z!IF0di1z!ZXyLR;5P!XWkRGdwf4t}Bx~z({uM4=|GTa3MPMYOV63E<|rmvZW`h z^fF(5cX$wO42@WOv=_X7Hui#1Vf+*3{(rW;;D%4_(_U~DMc`fg_6OQud%?>8Z|nt2 zNP6=&?FE1Ma%d6gEuLvb?(KW_g8v?S!Jv=5ZF@oMn0IC`IJ)sM+mf!|3&tKAqTu?FTlL}Kg3?paiwK|@7W8`V0#Nlo5*H} z0$>04+6%t+;=j&bu#0e=od3?h$X@Vq`u{)1Uhn`2F~GcMFL=*hAWP&w!Cugx$d`z{ zVAH?YUhwkrUhD;b{NrBj1?PV@Z!dUcg0&Z%&NE{#=>Iuu0l1n(p*`&dzaQ7rUhpVS zWL+4h(!1LW{{Qf&aer&wX}lIrE-dlJmMZT{%MzJc-uw{#$AZGfXUmd~5u`U=Z>&8^ z!D4-Dtg8)qCU7@V(x6v)F4r342O{4g>sOO#%>0;)(Kv#{yWEzQ?7pq^+Sa$SO}(}9 zpcQFy%Cc)g19fYxf6ZlAg{~j=IOi&pUnO(eb6C;ti|ps+U2E#AGefVIMPxA!H}bA& zS?e9-6%{{?yNd}^8;)vi8{!9@m2M3Z5Sgr0HCQ=J~APiG9|wUMYo#n)xBo>c0ka#wwaN> z$>x=#-^RfIt6ZtV8UQ**Kjil%CoxjrD7lloqZ;!LU={6h0IMi)0JHUNoT#jM)MF~$ zTU{H4Avu)o!Qso)XDg~Fjc9F=XA<&t0bof0ajAi6Otn0cl*K&hWX_bGd~10CU*;wZ z%K4w6sn1;P__lAIC@LKRfj?%_5mk^V$cvW;3Ad*1iWT9sgeF4u8TVA^WYywkra|WS zpEPI2j;a2Lk_M0M%1nPVRq1cVz)iLaZWtrHr&9Tgko|T_qE9X-$epk=>!+L4PRD1*#8~Uao``R>I zSxx3M(k4}W$dC{-gyx)u=gaG?LOu^arWD7m25x6g`ltHMY#oDBm)ZHfOa1@D2$Fj* zjW3wdwiq*iDKuW@jaVWo0b>E-gkE%wnM35>U~&jC0=X1H;sBXRpA_wOc`Y2e3);=I zdQcra``llG0|7Jt1{~S@O#TY@)r?CL%?OmKPsk+Rau<9@G||#kyiW6-8P~m*lcLN~ ztCr1^A7riNE!#f?Wxg-7dYa$Emh z+OnO*I}&SY|Fa;sR!8hijoL>^5#Tl_0@oLu3-PfHK zU*e4%P`BK<>0qi-X)yq#z@Ml}{6AjC#!y44yZrhW?xF{7%WQUH`?f6EzLJf<(+$2h z6hEyk^@vbI2*u73#Kjk=T{5`E+S_KAbtD&X;vT8Z>sE^#QqIj?CoX3ibk1z~365{> zn*KZ7)ichHO+Cq3*pC!qA?7!FcysSY}qIW*|m*!66|O{t^(nE+SiwWUCHsk?)G?{urbNtqT-d&VLd z!py}VF{@ek%^7XCF&b;}_!u$QD>kenJbkfl1uvYmlHNyzR}q3we1FGi8pv*A6usVzcOmv()U~q(06G3OgATFl-WhrCw%JEr)`U z72ZYV>c70_qWxz~d`>OR`CrUv`w3NaI;q<s|+N4;T#%_M{)GYMH$1d~u;R{}QK-Tg1J4;Ae9Ut8g?V&^UN|FBcVG-s!Yg~3je zf3G&JGVS~tzR`ZZU<01?g?bb7|H@-Xz0OWIs5tDz>^C-0Q=l+pz6-|=NuDWJx6Qdh zS;|E62}|;S!MyC3P#Z(tNG+MqUXGN?@B00V@&(LMXXpG_JY5q>`2vtacm`)a_$&{P zXO{Z%oN$w~{cCYk*o|_q5V@F%GnC z3-!xi?bl>k+amq)`T<7ZZGnC{_r#9#@+5Poa0~TI5uUv*&@X>wOtwV7yt}{Y_~)!& z4&BSt+KGPIcbJ}RvVJ*&=5JWPT=$(0`lXvT+hp|1|Iz*|qhAVc>qNgauG(b%QnFuX z`ehsOpVKdsrA*N;uhZ#_ei`#-u70`Y_>O%13+_n2Y;##gzwB26F zvrSX@1?{U^Rc=u^mc2(Uwdxp`Ad*3QHP5o+)x9MY5EyS`$KAK-62Qs+Fx(qowso zoVq~zC?6imUICBkk7a2V%U}Wnd9f#Ok;Qt01*shu>%&@)w~KYH)IV^ke$A@9`B-sv zI%}o=Ylzr!rOy7hg8-jx^Jy*1>!0$9Sbux+yBZ)?y1wFN&7%pF*Y+d_eo3cEEabGhk=UlHS;H!df2sYQs<~8E0h391; zRifjP{W`m3pQ9yvb#%$@O}U3^?6Q3ZGtjMBZ{}ug%&b}G80kOehv*53BG^D{){4tr zuU3=iu=d#A>e$(IH?m%Rbo{n?ZDXpds`-XR>NR|9=U!)v)WYMI?^k?PtJBAG z><3p+Vg}#6=_LnhjT*HfK<3bfu#MWJ9I25umD&;v#=CCf&o^!gxHQ`oFn6R?apmdK zHD1DZA)0q^*ME*5wZlrxdYY9qQ>=eD#k8Kjp6BuAZTz}$J&kz6f|2Mes(6XzB5oDN zr)ZjMh9NJtzMokSx4w!oNJgiR{NePQ_}MG^_%E{}4A$e8x|%l3h`{clY0$b4Fkhbh zVFOGMy^Gg%>+#C-^{~7DN2~$}fK#Zlb#Ho9%u$6d=id0#s9l!kDSDPeh_f>9Deqo1 zdbSq~YE7~WjCYHb zK3V27qp3E(=|*xNiqS~E<6j;bzjlDOMaHo8kIwso2}3O+rS}ny@Kl~ zY&uq2xqH!BdT-dPJfpP(#jOs;EjkWO&%OP*XIz1M9c_Ws_@5eVW(vJrg?0!dZXRw~ zzA_#7U1oDfs;z(I%faT$wkM;IZ1eToFLnt6jhD8*{`CKjej9q$R@QI%#zt1SiTW+R zYNPsXmTlqxfqrYI=B=UM)|!BCYckj}{dUs^^xH+6wwtHlUL!bmll9wxk!HqkSik*_ z5rivi}l;ngFDl2jokel^jpsnRw{13ejEC2^JVAy?fSFzFk8P3p+`CTP0=3z zH~MXx=qf&<-#%>I4E=VL8T-$s-%^;49I)#`zr9bm`9}5IP#asmx%%y+=QH~44X(S= zZ|A&Z_1kIUR;S;dk6HaTl6ywKeS4_YQj<9iAW)#cRiPd9+rN+MsNYs|NBZr!$d_lC zFSGSq{MG(!MJvp!5J!wrfZC;<-i;_qPo_U7+!{`TSIslwL8Yun`29>-!- zS!jB2)tjqPsUGpB{!98qA__nVLHWHYU#VFrr;lI;e+4%bl5hZjd!}A!-RX5o?WseY z`kF!&BG6xj76ygtY&Nw_XY(Dvsb>aw7N?WX3NBV06mQF?c?I*-rPgV!+vr5=>z5p* zpWY2=o+g%5uI2Lr)`d-lr=78+Fw$1({Z8m1EJph_Opm7j2t5{Z!o-H@0Xcd@iXuj! zKg2p9urM%=>U3&N>$gZ*9&hU1Z_R1ytl75Pm_axle?uFMX+cWe?ZZ{CRH4ab;Rb-a zJrB~L5i(P{QmG%ZNtpTGe$|n_w>8{%LSDQipW}Zx|jQFI9(l8du7oW4l`cMnDp`D*E2c;uPi#ni`QzL-mklmW>*xAX)88(IKKYVil1{< zQV({Br?xY_g;cVG^~qn#Ql|8y)Xo!*DXl0vWy0{(8gqsk-YtB=1_~Fn_95{#=>#h0 zz+Yp`39@5MCV3j73{2xv-%NcAo;Q&WeaIh_d965I`VOZGwLPI*regbfTT^cT2GxE^ zbfK4HDEJiGe-^2QhBKrbU)yiZW9z+{`8507esA?_VI6jjpmpi#MdO%J{p+qQIuIe# z$Gc%M-8dIEFIZf0RNHvx?;OroZ=7=>Xc+2SNo$E>T9$)S7;+qDSja%Rgza8?{aRgZzp0jC$%#)4M2-69fkJ49)o4 zH^pjYj{~&`Sp4B$jNJ9yTRLE*} z_H#HRVmR}veeHDj+Bm)TS6-{VVqNX|>xk2%Bl*wg|E=vCDHOpK{bDI#u zUSiB)nAV~D8+NH|sW_SDz#3!L)g6;RaMb!#*|HSzAXAPJvB!K&@X@c-0RK4tiNtrV zTsl~h|9PoV>uO`mnul-mr959pF4^&-y4K!m;OM4ss4A6_Ew#qXt)2w27?bnYe``i9 z`h??*cMLzO*vY;4;mvk)7!_FW5Fq$D@bRa*iFfu=fWJHDDb0SEODxRcyp$J;g+i;6MhR9X~nYR{2 z-@3uQ)j}M7lHkV&i{+vxKRiu+Yrod4C9Bm5|CGI(WGvv>SU*DkoAq0GoiEBpw zG-Syi^t~!8-c@5$J9u5c=dW(ZPWqkH=$5+fLrCc7P0{J$E^O}MV&(@DqP(T;0NEE; z;;CY~4C{|L+OGH^E0B&-4dYWi$9TzkjFXcnf#-|YO{xV%(b9?E#*5~}&!H4;tM;b+ zlh0g@wwisz+_{)eW ztNud~bw71>R`eRks*!DR8-{<5{mb@Cg?`M@)bF94DM)GRnXO~w_e~w97uVMKO%~DIu}c0GkS=Fs?5QsgNj-& z`HS0XRufz3CA;&`?bCgOG;8+6rc|?+xYBq?)2X+;`XR?~sNZ$51rSQr)7k;MRE;+G?L+Q=00`hI`5#${Cib>!WIgC&oaXl+o~prx0e*>B%^=-P!|psHBVyHzR8<3h;smc}wrNzYTv`DE3vCvx#7-1%Gf}cE&m%Ewb8Jz*0-w*I?F7Qk za;}^M`(lT#OtURR!!H~*!T=LajuvbD?vZUxe!q9f##K9d7Drs}TwV2Ge#MR)ovQff zeJRb-VNb47>lGKjzuqN@Y@XW4&K7sVyiVP-ubyMah=h00?D$k{dIY~fX7K}D0we6} zCB8>LaGt_5Lt%^OC_|CK!g+7jR!(Fba#Ha}5CH~D7`wm!zB%d{x z5Jdll1gWlCBqr2}!sgiEsx|J|;H+4J%MX(9GO+|M!rvs`ba>wqDXF6cjeNyG`L<*I z)Y{Y|(2<=t>5{P!lC3^C3%Q1j=W$&!_INLGA$?GCvr~eH$5rf5GIq3=e3A$B=ekDC z81K5p1&`E}GtrTEd8sFvRb=&1oe(fg#5E4LlS`CT$E8TKkg;HjOj5Va-u;WuH<@a* zzC1gcimGj#)#os2o_{aoa_f!Mn{}gBR)4UAH~DW=S2ya`>JPT{CLcwy@zx=Oww<`5 zZsAW=m>d`_o^!1gCD#2R?1Bdjy_+Z7aD-Vx)_w`H~rA2Lf zhWs72@5xB}2oYFlXot8Xksp)~1!%G@(!L#d;@Y=5XrHT3U6Br6YENmC4Du$=q?Ja5 zVV?_$H>FY39r`DKqI{UOm9r!h_JFrFpH>t4TZ3pALG%K_=_UtZe=`Bsl$PD;O%dTq zLO9E@SWT}1=GE1_+B|$fJ|_#RxwJ_MqnwaZpI)MSzBw+lYDWB>{CIm0!%|+Nh6iG^ zbiAF-0*eb;yyR2#QuEUd*Ho$u2I%K{)Y;rT!L%!seii3xa4Lo}Z)(aP6u&Mcx8mSJ z|C0|8n))flkng|F&rVwVWhmrtOHZ(Wgv>F)5a03C2PKOuZg&klnFoXl#Xlqv7ygt&NO!-ByLs0(i;>P@%vPG7e8F6&ptu)BJ;AX+^ zxg$aw3E(A39n|hW%0KF4_oBYcrq)+`Sx3q&Dz*7ZXDC|8zhCLqm|xXZ3-T(qdH-1@ zx9;t~LbHRhe#uP6LK2Dl==WfLDEe?>n@AWB|(JNa6q$fL;_cMygP% z<-_9lBJiM@?LQ6@nDKg*cf#=sg|**K4>TTzDiQV;am3>6vTFA)gm5iVH3eMVJRITA-U-{ppK>c@M zZBWTVvK_MzU|}A>#Q$!1FSw&Kf&&&1BRywIR@3qp8b%sSd6LOMEB`BcIWwaW1k9+v z@Pd7HHzi%`gtq22B`9=#FMieyhyN9^afe-{wM8Im0Cu+%%u+3aSFg z(`mOCb}1&=9ZIgn0GbY`i*DaxuiK!_2QNyn0|4 zc(o+jl*B6*3q=@G1lwG^dbBB}rugyr3;7&!(tZ`7sx#m^1OXKQ*L$H;6u4?WM!@S3 zdL;1LkG}@I_T?e~uR7n_K&$x@EP&VAvgydfeoJ!D>z^eGofNvgEA;vV@FNRb6;i%Y zu!&dneFnY$NRmE-UN78jgXs0|X^URp?E<|jsjh{j2XJN4Yd?ctYuK#35W7T=M) z;4|RY`$WRo`-wss{JKOv>>R)TS&!`dAwKMYUq9p(hc{j@gb(oRv3ih;Uk_7}(35Wh z{3_WV;MYQ0Xhwc(;nzimcAqPLt=I^Doz@Y*PTK_hIz!~_0>7R>g_sQfFY)VZsNe{G zUF+0$1da%b82q{&J^9@6>yLNp62JD{TKKiRbNssRe+0kw?_u%lv<~?7ju5}rMDXjo zn}lBrZr&99y30)&z!KuuQJvz~nk@YKT<7@plh24>FaBKc>*L_WF7fO4V0VjY1N=J5 zO@Lq_l!IRfP+ovv_teWB@$2t+VWEM#+j5nQUu!JPy_$!cgkK+FyM^%Uu;Cr?YsB6< z`NB@{>xS*E-EPRR!~e0p^-`9mA(Z~Y-nyCg)_F*j4cJ=`s{dcHx9+jSM)7KD$^V|c zwa4zcAT@)Bzp%G{VQ&qT%@_7oR!c_u{mV1 ze!nh*3PQa4g}pT!um1PhTle%VUj0q!2Jq^WXLo{EH*9Y`xi-TNn`Lj!#0PY+x5^&5 zMfTQ~UqqU+745AJN=kk6`vegv`5kt@Fg#!cM#8|#I=_RJHDX~sfgZ`idL)01h4m0F z0t@RtTzhj?m6px+e%XwnZoI6lI0bNSFY)F)Qu>%oJGW*93J}2&1Vu!1Y68KE>@F1_~IvVnQeUSX1l4Tv=1= zA?Dq_bG7~&pCWrx5GtTp0^`5>55^08_tQ_>an2{Q;{^hef3q`^L+=iT*FF)F|5JqQ z%<#Ijqv3TZUs;ly?{$PHXEa_p@qX6&t#Ka;(6J~Ya)x2lU>*jsPWqYc|z-{qA~?X5@YL9V@Z zKNXp6ZwSKuSG>SY@lC<6w@=ETgb=@WZg0(w7wE#?y8iR9w{nvCXNwoupuP2+9DD1Dn`3V+ z+7f%~g3YwIp4Q#s*Hwpf#IKQff!)V~U$3z7tqbBW9}{mM>?Mljo^LN$sJK=yaS6Zb z%1m@?uOhP0wEns}H@ZT|G}lnf>9hE! zgtyK%;jP_Lm9smIZS73~)m1m*G?*sfwKR4o|6WzG6Tej=zxlVCGvwLgnW{|RTpOE4 z`eALXC08jipnK+xYI^d_~wIW{kk4P)u! zi%H;p%T)5|c=baS-?>A{_XtWH1R6EOf+E$TNNl!ZJH3UCwdJ>PWg_WkuCqw(OkU`> zhRDv;Vq!b}?Mka=d6T{exSF^_Y2{7cq!YLV?SOfjW7G3ftGt;@y_swC8&hw-e_lHE z6kD`fYDZ7Yf4;W-&YSx!-eXni^Ameq+`V-9#O}Ng70ag=l+Vj=-ubC`Y(@}%_9d0+ z^!=?gwo`yN0UR3%U#K|1fV<=Hu$iCA)g0%=M#Rw;&OKJd&TW0@OZlLN4#ow4{zZk>@WagR)56e;y^A+ z%BOqR>@Cpg4~Tz2oEm}bc>|%|lo>SQ;vEJ;y(zb;Jq9i|$L8m=`E8{)v)P;Z{HD=w zzWM?Q=SN5=U!?dU=*(?Upi}xi36Uf8^Ad`wvjF)ZuGAjV6X*nNg@066%_Y2crfKdB zA?G{Ir31QHXdHyCu+4joI#mb^*$#?hll1B^NS-c4UMkXF|!bI4Xr42K$5Hsr< zW^UxVH_F(PJG@K`an-y_EMFz7-0s_ParYsr*c@L0|01}}N3n4t<5o|~Vl zTHqg{%ES0Jh|KWoV6fy};s+P6@C?_Y*L)}eZ}bvDEC)?m&z;EkVLai#%v6sAkv+wY zsgN0JYa4e$0f;RBX1F-3sAIPsw-PW+PJ+vVRCnlGbg*f{aInK<#& zDNW4lB~LO}uyEpe`Y%C5Jc*Y$i4T|31pXb&WnQ&k%8RKrbz@dme@OiA5D;(Om|Lqq zB>wjUo}2hzZ;E!7#Qh`vef4-kWV-FbgXnadlR?bC(|=Oy0}BDE1B3b`{J3d=K+VHw_}-dtzm2Sa*y=XNG0c)tGuWo6&h-)p@K|h|j(@6z-vw-+SgrY+5u(WqKgO@rqs~^z$C}o`JkF;46Op&#cq7dl zXr>fwp>LeHnjMmZc7_yyGlF=zPFk-6vi*|)iki>Gj|ajiMms|OSWq0%qY_W(~S)mG>6yLf$n{91^|e?zsI@%55li{RxEl&6*&PeP-Y{AvcP z&)1myPXHdX;&(qV#4&o{+ARNm$X{1Mg&=;{MB;}}6h50BxI3LYqV7COWk%+!$`?(0 zBcZ#+zJ=nCMeAD#cF6Nr;SCSyQP5g)DfpTU8Fql45v4rquaxy4{n08uccmSJ=dv;=nznaUo6d6+IL!4RjFXZEdAxni7hNNHYsRA(S( zK&2TIf1o(ELx^tcdNQKG>}P%bllDk_ZX~|pF6LYg-X0=!8{+Ld_?3mX_o)oW{a(|@ zzB@{jn2@qJvheo#768*|i??TG@b-s#w8?mT4~j(;v5U%WRnUeq@b--rnWlJ&n{9PV zfry6t?lV-~47_~+1z0p~vV^p^-;4+mLz2c_iClZ?aF1`U|Cm9W#^|KVX3#Z*r$GZ2 z@Z1YO+|Nw?6;#UteRqa{Zzw9j0KPvV@k`3@^sbr4MF9A&BAEXy-C`hGBDQQO)lm_@skDR z_gl<7-JDnY47URr%hs~aW&L^CfZ0ntuC^kP*pJK=tQ!D6h5uGp-3Q>4alAG*U4ZZF z=CVt`*RTnI@9Xbj&F)|vMQl}9-3yru?U0M+7GASQ?M|m}v)3=$Ef%a_2B_ze^~;^X z`sEEWF3QsE@j~|cg#qb732s2zRe)K)bUh%qWDkhvO_~c=4vgL6O`gR?WH8*^S)fAQ zYT|f!q*L8{I0F>{;K2e0!Oj-F;Ol9W6l%;h?9Ah-tE5@@BUlh8pV9+Y<@G#qs48tr z^B+QXh4`rWS^sEFg!kNjwodVOIPp5*ZL&Knur+vldVse%_sHy)J61Fn-oC|~Vt1vLO?Rpz z7jJ)8G|I&z25&z`l?HF$mxZ_Q>lAOVV`|2+c{qY!AHmz?@#uWcYh@mt^HIEgZ!X@J z3bHd~Q}Fg38qqAgJvk~pz}v2==W$yH=zWCf|0yj()=MsXi8k{-;JXJz3ddLYbOZSN zwSR>88>4Szyg8sJf{(SP4mc$R{yu}&I)pr5bHm>)9B(JaWJPge?CK~U4odNoX+8syS_-55qp1Q| zk(Xy1GLRxI#=qC!StBgK4))L*_imm!KIEz+{J!8d3%~#IWh}53%xBTv6mbzBMi8q7 z^J^h_nCZq|1o-`*%BB?NgZTG%;HagXPsTzl{=zZw^@GV9&iq87#@CY(q6L+?G?oTF z=hamjn>l+|&%PjJjAdT3H|9w#srURLr{C3&J;qzO#-x9kU$MitMq(K!`zDo$q`DkgZNtA4kJql<{7jp0vwIMZVWr9rS zFDf6Dmz>7iVGakLwFGAB6=)VCYL-#Na^9S2;Am{64ArD?Sdp1O@v4>BQyaZ0r_wY7 zuP;|)%U5{U{EUlu>j1XkvY(#o(#jTZlJ?VkGlBIkm&23F<CvRd+G%4wGLv+Gs$(1377%&7+)m1CPH3xZPY#s-jwvCS&%}fMF$;f@#=ZiRC zM)-qhQQgVbz&VS1bthW`r?P6q+lEwswC%(%#aoXQk0o#6`BcU|&p#Qd7TBX@4_{okiB)xZ2a9i_R( zzf0{gcro5O*1w$Q1nq~dWxIY;BfXGAHC_}}zVTc$A)M4O3y|>rT=X@U%KVGfMf8uf zcnAHorvQR%Ji&8`c`tbaUj+Jpg4%1^mq7svaS5JF#o6byFxV=ef3|6*e>74$)W1ks zDGmR6X?5BoDzuzys37SX5rnD7NC~+^iRM{7++*h1U%BA5<4c~YxB~XgOFir*ZR@B5EtvDA6py{q`h&)2A{8d*>Y-+42ThTgg zOi<}|L8Vx)5#HaYR<&vox4rrk3dIubBIyL=xnw#me-XIH@6^M-eB>cLtE=z^ZS($G zV~X@+8s$)dO@qyxJ){RB61Ybn#gOiWU!58F;dLe)pO1?yc47`XLc%ih7D_h&o z3J2&>FFDyEjJX7-(#jBF+~g3(j0nQG9uaI1hAu>$;g~`MtQO1OcXzbh#0BoVrLh)R zz#s%OGZI&I1~PW!ZO*S?M~~TikI&t0zhCe^ny`t}kt3*Mt(h*dN0sjCT`&hj$2eNp zA*2C*G+-l5y_Vk$aU1ZhG&JFJeskFBS;oPo403u zV+Gv;eqF^Jk$ko_aIBxjLn{m(YRdF43lFtZfsxb72O8p`rT`B$tg?7WnMI7*(BYv5 zZ<6wlG3K)wb2s1`>w>ZsAt3rXZvMK|_Dh3=HE@4I27BVpx*uQ?*%uW`zMXN24(vnz$GO zg?PqGru4*_e|{`YoZ+8v)h!=bmKTPT%7l-?;@8sh058c|9G3st@lqp2OUmq3Zbk5R z`W@mGg^-5$qr63UMMmHZUU?ai51Tu7^AA)*1H6}uN1o=fjS}b_uk1nJ{2pq7dv5W{ zuCyY+BkU~=^jkmUn??lus%OW*!xoS)T6?R-M#0Y80}+QnH^_>tC>9JEi|92ZqCe z9omuIO+TMP5DLpX;+f?Y=PHqwvU6H6BWt`SB{d4ntQtwB<*F21a70k4#WOchs~YwO zw{!5!qtZb6{$(J6PVvl55j>M*OzWZ=^U?3R-pLs97Am ze#4K3m-~PtFP}Z9dUenr%P1iniq7jV-sIoE6j}v%yxepZuY8 zgLoqxyZ?y&NGE;=E? zd&wi5{p7MS;)0C*BtRG(+GVh?JTxDoHI4m57lNwYkBZn&eh@7;K?+lcp~La9_LCz! zv!BfGo~NY#W+QC}AI%tGvHS}HXbF{jM zdTKr$-38J(?;?vdIIbGNZ9Fw*Kdtf9T((YU9v|eW<0Y-nMxL6r5R=9Dd{%tyr|liO zCwz~WjsMm9Yh>cu1b>bECB{CX{mh|#BJ|hv#;GX-u2Lsv>=OZ*Z|80fnau{;Y|o9B zTmhMn7x6mt+*I-)Vx004_wbb^^Dp#$=($l!TI;BJfh$NSYsC}hGG`C%23$7pR-X;cS3L=R`>Dh#Y#I+9iEE#to#&F}>4`HcT&Dz60gh$gPLqCG;f zt(vvh9B1&yQY{l_cjCVx?lG{q$gX1uTwng#{Wmf}tNxv+Un2DewI{TPe2?aY_KmV8 zxgo9pW{I=>J%t%PfFT1hcXZ~zxkD{*&#gVAlrI8%M`!+Ko%wG%vv-8fn|~b)^l1v3KlMyczgoZz^#9o1R=6|4lb8pk4?6&1hK`++FLx8OWW$ ze{(3;8n%DXm5%sBW*hNMj`yg8mFjnTFUx;(S=Sbm5&~DX-o-Fg$G-P03r7ThMEp0+ zUqz1K*UAbIY4Q?(Ku3c>+?Gm3(Pr{-4;>kf3?lg_dPXNd<}Q91GaXK{2ZY>BICt5~ zZw)0#!C2AZyOhTHN}N5s?ZK!)OXB3tK%r zfY@D({uooJjaG@*C^c2t6p0jRMjuvJ-Hml@mQ*K}ef~8&B4x$n_H6zQcSN?ZBXX8b z<1#xUX9qhXfA+8}Df*Z5jAx3w_*eT{En}I+zu|@{J$?e;VS2f}GnPJ>M z@2eK=+1n2dQc^yH^Fp}b00gsVZ)dJcs}^{ZEJx zb*ywTms`$C9ry^&Qn1q@D$}vjKe_Idm3FXPl5s+vq&E$O*7qHpP2M&plC*o*T={Xl#f#MgL9 zg@(I+PTEn(q7#4Adj?-<*z;_V;Vay*{wQ1gDmoG1KeNv-DF3tjqZ%k$!t$D-R;ARd zjRt?!mNf`(eFs8#3j@6M18YKetm_}Dh6Q*v7jOAI_Ft6J>Kt$BJWJnG3$pOmV!jCQ z)LnXN+Y|bqUX9?bfAc(?N|NQxvAgq3Orx7;p*)EApYA=%;<2xz=)>nWOmsYI+1972 zoV!2mp^!eJ+PEQxYD_b!kcdVxCL@Bqn0`dvXjFr#sBP7}OyG@|_&FmEn;K`80?y1k zioY2b#@}d_;&uU>@SC(eR*UofQ`JsdaKpiYlKYs*OlLk~<@IY@oNJOHh^XU;qSpGm zTLiLL{wi(SjJ5QLwbaBGSDa@TM+qHzqm7%1yL7?E8!D_RYiZpltg|Ml6ZFRUEGJW` z8Z~5aMs*)k=qreMJn?C1DcgMi*WeB7uhL0XA^kIW;2S}ya!qN&vW-!FS>dxGNIOR$ z%^V{YY~kD2Od}&x?O2+{RMbgz!Tz0sHprgCLWV+bDf$*aRXWfAJC|PlNo@F}Gv>}C z=1zIqM*cR^H(vXRSuAgU(O9Vt6IP*4|+s>Pfdz{&&~T@!j!R_%1C2Qg|TT_m2uW^Fg9V9lq=H?F^GRe77Kj@Al|` z@489eI()~q2>H7M1J=)vR*-ndYDWvrpaUnd1^Djj9DFx{m%G}5*J5|zEo;3uE&MhE z%BIQ)+OhZza$f*%l9r1)7J&pVrPC4k#$ic9GA8B1y{HQV-*kdeN%<`An&n&sJMflp zU0Ox##%Ovboy*=YbA*V2Z=G)VYS9k7bk2q^&a-60SK6EGae;m+C|_jp+iZj1KGt2Q zbU&f4PL*y^Hh%LGI+@NY-4FO4zGg2;5DvQscLMnzn=RElmOG?oL-k_85*Atte}QBM zQ>=cwJw*aCT?T?^dsamG&fZU-iqPm&3*JR>3zOM@H_Pq6$;Lmgn(8h7Im<{xhks^; z_=mV!nw%KQ(`@|1SfC<;vG|5c+Q?W;&mN0iyh*pv=7F*4-sInKfxL^1g~LFvs#R_{ zuAz=jhvNYrMC@AuoW9KHz2NJWloW;2giRRj(C{rQYh>|aKb;PP3gs=oFNn%ti*FDh z5>AV$wQktTL1b6Z*%x)gZXHB&g%s5|%)K+26%6Jc#$!u=88p*$$V6zp&TSC)>@f8F zU>WSo%t?aso@N2=YfrO)doG;DdPIQRM4vCr0Pg!K&vJm`Ypbi6aEVt$zaaWNXVcnm zdCx#J&Nd_`MtAem9MMKT%M3)T9Ab%halarSw{o9oa8TPGj=zIYw@aRiQu;LrCSi1Z zsj;7FM^eUqmNo-zj)m;S2snhO)A5}BjQN7!F9e8`kr|($IbM=egAv*WA0l7U{#W=t zerntw>pzHg3Gr9iY~i)NpkW5D%{kI&5+@7&I6V*F&&6xcQLw*`G6Ou;!9F%ncbxc zM32J02x^J^hz7{UWA_@yv7+IRX6i)o*g}KHY|b0r*)Se^Pc7Vrz{q>;E^@qDUEgn0C zN=24?X^NJIvMfgMm=qUH|L4Z=*zG~7H8J#e29Om85hif}N4d%WQeD-8HvJjev^lnt|Cc&(KV>STRy(?{SO2W6 zeiKP{O3mgNKc}@c{(;_U=*kH6d^U&(wAR1*HjtxU{6ngW%F-6cuG`STRHIWr7r{Fv zXJEsafnX~+<8)R>XMCS6FTBfSS(*Bf>C$&e>M63HsbQo0vT5Qx3T|D1+J<7ZzV&B) zyDOuF4e4I}jbM)0%}LCUYrpwvd&!7nS#BgIsFo4Oq&}=+z9bc2+i zcA(ga=-tWlb>D;@XS?KaXD-1>Wp`kKa=>ylh*S#mf&;huS!9UGmwA z-XdXTw52Z`YNlm`+n004aeMr$5pMrCCK{S93-gbJ>7dIU1CV$1$HVR=i}6a+PZ*v6 zS1wZPMJ_no{{uQ3imfatqsOw&*2b0zD|Z(^E=)U$jkE0#XD(%T3jg^F48(vxq1)0P zDB|Qu@J@uLMVW69JNf<}9o}d-NT2^k0LtidwZ!oF2<;Cv(-J9gGkjUHnBu(a(_VeL zZg}27V&WB;(o^f{sM;=khd(D5-{4eg_rJr_0Kas=H&R@FscTPW`1C5;u=@wZ5uF0}sR`gpFBeYj3wX+2jn8UBr}G;6>K`spUNtk|6{ zI%b&)#p3P=n;rC~F}%LSC>eZpYN62z%>**jJ--fZYJ{SHj!3}#D1XsT``Tez5hMkT z{e#2{h=!BRmeQ~CD_oCgB2rLol}LrCMnDYW{Q3Tt)5~AU8( zQDN(^7cnD5&7#xJV0Z$1Qbi}FR8UGqrj!awsi2gKqQdr7ks>t0IqmCt^+zvkQTt9> z`nj~P_thT_n7u{xTk*NHukh-Rs<)_pCq4DKw6D+AAFbS?_I=~|&!v5Rul}g-16wk_ z7q|W=+E+GKGh`66@T}Ski%v&Sj$xvnQhQ0y)cf(5ABe9gNL|sh?#iP6nxm-;$wkN1 zy^{HNWzo523SY_OyRv8;_X=9QM@`IO@6R zy}>>6NxOrxsI#J6;`)3Ft3am=Lc_LGL2p`fp%dX}QAsZ&PEMh6muGr@qpfo|9pEz_ zpBrDF?@iuS_*GZs08liUUWgZLfiSnFZnG# zh6-oN%`Fdh9D=o1oF&5O)|Y4=w=N`5Q(!jLW_-R%_5W91E}htyti@a?gz6UlR$p0k za2^D`w7FYe#SQ}}_VyB&sn8el^V5DG3JCCH0O}@k55%JCpx(Kp|GL($PhFFY)szQH zO_0zMoT`(uDAkVbI`k{R(s@-Df=KE1Hyl%hkt4FS%g1~VjvDY$1jAo&A*6;_mg@r_aY0EHczDeO=$c^PD+u=7=z zfsg)=xD)8h&_8~deeEp0)*#CJd#IW#i!QC%zIuO6h_jem=f<0k3+AU?4^dx#a$1<- z@JH~88KsKtN+iMBh(FDQ*q>np~J z*jrPxtXB$XfTZe``ha2N*o#6VF3Nfu2@(?)>7HUj-|(sx8r&)C+1h&;ivpI}vA%Vc zFYS@J&7sePwkUnp57{*Oi~-q4=`+~=-a`5;*?x2Aa~*FR`m`L=@I^em4FF=E zDjJWx#&(dxCiIPHYZBdQuoAqjb%$^_$L>Oz6VO{V0}tQ=g|{fsfTQ@xn7T$@k=0U+ z&9OCze$11n5VjHKg5#|Q)6D8Sc}Gz-v%Tan;DgTWyBj35lG?mE%B?30`W*Hp&35`X zUbUbHJ6yae+X~=)NUZKbTnyxpzB(P)7k_0yDT!Jp?aQ^!>r1VqzwfUsZS-zT=daS) zaXTy(IMA7U0STT1wOSGwH9j6=T=3{Bw5c~IZ@fWqNNL&X^jt-}20k2bt#!XtX_Xw@ zKmZ8f=mL6zoo$v&BzyE!h5x4`2RCGyOIUajG6sjFP%O;Y&OFsFs)p9itpfpRB z{kXE{_Kl=np-(=v;ztikl2-KR)*MbP_HBQ4^^5u(lB|W&{Nf2M8P~OgcF-33#jb;VL(hso9e3B zVI&M8PMphQaYX3{6}?hD!3=370l(&Vxk;&+J^N|)B=5tHI+Y`L!ei*Mn}5lY4m!qZ zeMm>eJ3-M)y~F}qt5wIj;o}NT){7?C*=bK7VGg;xU(EX>7+YpnzHiU#b4FwZ>v(f| zTLJJcL>#XpX^b6w#7g*+5RwtlsBXlq{2+)#`Sj{WEYgpOJ>#z&8UK*T?SIe%m=yN` zG;;R!(FMl93`Zy1cm6`R3eM8%_c9@H?EDh(JXJy z0P`U6O$Km$sW<=@pt{qd z)fENR?a3R(?*;EXD%K1pu$IEi-d@FNPGQ8#tYc+tr;-OD`xZ(7`oF{ejTG@85s9!* zhef>|EvmUgQ9n|BUnWKFLD=2d&32!g|;<_lajp>&h< zQlyN(8+~;Rz>hTBA3*&<14->>?AN1b#NWw}e?a7Fx&Yq?a=@K6@y7SR%cB<=j-Wg#$OeEo;+W47b8|&XR592Z`OT5FR+tmm`K|`EfcYXPn^w{vb8_4 z+o$cC<^*m_*2`y+YWAO{$_*t zXB2{XoRwkz!lcbteEs?eeE=Eg(mekLxg!j}>1UoT{|!p5uKJ@A*gT8K3XP z5nEHbq{5t$@vrM_7UI(uecWFBxIczkL&X%(@rM#{o%GQ?ceu?=s*zNoWp4z}M$YdW z(MqagcYa^mp5MobEbMm%W^km4mr#mlN;P=4+++-7DgEZo;Q-&^B`YnST_n!7c(w{1 z0$max8CjRMcy>K_wni%Q=iu4J6iWxxLgCqytS~?$gJ(bW5<0)xNSzFxP4b16ICE9e z_|#~FXKBs%qpe9)Sb&uJG#vih&CqWuo^3J+tj6RVbO{HizhBuYuss^i7O=eoT;|cd z4!|}U8Afv10)=z{Z!v%P}bM@ z>E!O6qE80W8gpj`(oR6sYyi@}JitO)!RHXt-kyQ9--aTV9*1#dA?<7fkZQU}1t=@2 zk$?8H*@pTVczb8450nu-3*U;Eu#m;K+5;8Ew|55kw(MYwZx{1I4!*?zW=UT*on(Kj zO!nhh`T31IJmpm0j*|LL<@M~E^#3w|#gYD$Y|^i#Vn=#Kdpm%eLSQ;ryNt`~s@uW3 zOUz9d@g2&?O#`@po!S+^{nSg|hPWtxDD}LX5~)YB04~SRWeE8aKPu*pwsBar>c7T|B?<3 zAnqFo^AO?|L$Vx*yOIJ8#GNNy10IOr+qJ69;?2DgmsZsuVi0c7hQHI~5HG$-ZBe|) zCXoQ-l~DIW2^e3=1gRtdweLy~*g8sRfgMsXZN(_VD-}P>KxcFOw(I*6xFLkjH_-?K zoe!ljVbOi4$mzoj9=$!<)c2xI1(zs3$W&PQE8EN$X{PkQ@aQTU))9{?xFZLTO0YRR zS|N7L!lS`~8%lo~9Jp~jWZ57d#W>cUp+LS=tE0ed(;c^XP=0blf#)3_Q3z2KvLy&! zZ1)e~`zU_ZMx_8EOjL(7{@M8TyCO{40y&n_@eso9L*+6K*f9!?BU%o3fUnLtvfGYM zwN-rEXzSZyXrrnZpud}AcK{{e?wL->=d}JATH(J$>!%7l-b7)VLKu=IRULS|3R2ng zD7rdt1Nd0=^AJ8J=rHH4*~1o$b3o&1!Q4*5Xqk8_U$Is>_;K;&7C-Jwt3!DjK&Smc zUpe^kR}@=aRTIIF6Eva*qVCRBh#$>q5~hCjcRJCw8T|MQ`=Y~-bs>7VZ-)$eaDc^2 z%oi}rLNvES>Kd?zF+mw9b6gbvhVs!Y5CZC4BY+vw;{gAwVixn;;z>Hwboe(;8jv^RD9(59j9!LNU*-E8%=iIq z&-H(d&bDtJv35)Bn=fo1j_7}ZeeKxBrBWt1#T-zPv zWnkY74cLXBL3BgRmWl5j?sc3t?!; z8Jmr4yQwDYUbNW0CCv#+HopStp(t&%JR^Ct{Rl zJ@*5-vS!Y5W4Y`*S6nhaRghVp_Tv@S%l7?WTI+PieIKsJbL^Y9m>xL$rrhLF`)26B zuk3B{?Q&j-+BXB@ACBoEhxjI0XqKUKFIgy@!RGs26TdcUW3EyIGs}v1X@MjDMJg?} z8v9^!3JY*&-<3OGA*#;qIp197fM*{#4PmKYv1t@a}@__|yi@ zW3za7k^J~X9)dM9r!(&Jb~wC5YhKJNNFX!qtT(?5Z$3e0iSHS(wZNM*gy__jc=KDs`>NFACAVM_l2aU@0Ko_)=vUJ>HP4 z!NzAc)|TI5{MW3KuIE5oa8%-8Ug)>R#Al`!6QSvES6Vg8o3uM)qLgPhd6Rb3@RT*N zHfZL&dXx6do4Gc>G4W;Q!bR;bog#<^iDcO*{?5T}jU+ zZVguz)m8PI5cPV~MY%XRSidnliGE#0?2SY+csWM=YdF6Mo8{138w5KJX1&HRD~k~^ zD@Q^SBQTC2*e}!7>MEvV;>Y0&CQ4u&VX&8r4zUJ$g-5oXK-eV(#ET!Qby6jBXEyUV zKUKNFA4&tzUyjZ0GNdfDBIi|lE#2=Ygr84d$*2oLPv~tk84E#A=oxPVJ?lK@Adj)d ztK}^I>hp94;BDw@7JoBb*!Hhk;NU5agl%VKQ|q>qUHoxIF2gv(hHXEPiE%lf((Hs+ z3d-s#SU7Q&p~H8sxvYKFzA_5HU$DE$rS_Kyo_>vM!yP6YZx()EK?&jB zfTW30a5N%e6;E8u@NdLliWz>`4y42HXt8?gRMM3>Uh+tuxfO#BOfWT8vU9M!KA`)n z6cetiF%M9t0&0~OW3+9r!Qb!lWWEHce-J~G$sluZ=JSff+^1{2gpT7jtsn~uAK5SW zrZJ&EdmXgt7@yGA1V5C`P`KGBJD|`|0>8UnBPVZNW?P9VhhZfft?cI-;9v|O&Tl*{>zkX=QD}#pl(_T zfJk}vCgj^tyeo=oS$T+$FNR#f_}URaR3tOe15a}m!N<#}-QqzN-@T|W?A5w6NQ3_Q zu_!)Xgm%sIM+hAo{1}YaPgMf(5Yq9v`R!J#W9EHZ{dYla(cfQ+)KB~?k?TkX*XeBg zu*FM@6vnDCc#XhG7voA;`y;}A+SL*fq0KRY4uejFR{S#rRl_82w?onF{JVW=YS5|c zRi246z+!FbW#bn#pIrN;=V!AAXMz7NZ`Z^gaCKWk&E!SXWx`NbJLZ?k_9Sy3l%=^) zmiDXo)~i1nFy(&;ZD9A=N(46>6Cm)$KPkMYnAbV5lcHJc{Y;eW5?=>dNYpCIK~b*a zeE5>=D3&d2%4Yi~sY4w?89wB&92tTy#$wYM@eI}W<--V34gLMcnMaX$)`-1#!h@aT z-{XZh*B3ba`wxCaPV-E+evLC(-Dp1GCC69@yij7!LSVNkA~8ogp&Vp83<&%t5O}sA z@FhUt6ZB1tX=nplP5dQ44FrCN^C2b1vLW!Ud|~JJJXIt$$DGv^XV>aAOv|uAi8n0- zPQ632d-zWn0&&M4tz7(@vp?%FgGz;NnO>^sX0||g*9B--Fjw1SybF|}cN!fq__q2^ zDiOY&;U%@-L?XdrwY#8PN33?Ina>ulJ>pGyfUd?rz#jZ87XwG%>D4{1JN^KJ*UF%v z@Y*zQ(lH{Y0j!;YKrVF_p&BG1gwHcd$ z*ILJSjn|A=PVQtmL4#r8GF!kb{gGSQ>kG3U2-X)Th*+`A`r^)DeR23Uc6}j~+R6Gt z7A!YhpQwn94A(6=!}SuJG%$9HH~Bd(B4cGiL1LZ?b;I?0h~33-tuUa_4cC1lbTC|i z4P)?6eiuQRfg|@sKlm9RWFQIY+Z-fGoK5W+y{^F0pe;W%D;kHe_NFkS*KgvDVEkFg z{2W;t;&*<3Rg)^%zhd$AI~+$Z-D&W3ZDm7b0t;dX`^*Hf|3WPY01L7IpehZ-UQHW? z_Pqp=U+#qvVvnU~7$M1UFMMq>+#|oH7*|AED-EfKf6{;qzFto$VL<9`sz!Uc_E#f^ z0j{;*>{_NgDU0oXv+K-{MQ*>D%UWD4AR~Z1!vO3z`3bPOed-wN>Y;+oOeQucyeV&@1rz7tBtv1hn((~h2Cj@EMBg}lg$_I{ks zgz~~m+|6fBykBCtiiu({vI(91lY!8*&){dNJv)i6<-S2=G^7C%qp$65;q|oHKScxA z2|1EVFk-l*;zmh^5&Zpt!QYj<1Socs#S-Q?2pQn- zS>g`6ItbyLfV5$_;d~sts3eevjeP%I$(xf4Q}6hV=4NC^+VY@i6Yb3ed%&h?7CL_b z6h6Zr%|O}vx+5e%!!OnHLu1Sqxps<_>}Jv6@2&flnge+bu|w)RsJ zxSrJ;L(p)+!*|$waNa zXv#m}%{kL1R@jao`tqKo-4Ko|Z3^VKi1K&fe4dv$M^J%py*Z>%NYfC8VLsVD7ZlP? z$`z=Nt&F6Tz6=dBerlLAfu}7IlX8=nwBZHwC}$bw(G@Cx4ri=jRJO*^CdtpZ&S#yo z%A2xE5>ojRbb{=_vL>(YE!`nsLS>6L>0L>fnam-VFQHla5}quGEsJvyg)E#e@ua_Q@>-+r^!~@j z41WVp9Za@q5=7&-xM^stGzm|dU1aI}g0f}#N_;FDo84&+U(BYaF0|H1JPJ*4vd2Ye z{CDbbG(JxB36Bt&=Hy>>)qK_?&+`k-rS-^abD6uFtSMZG#C=wr1+`^8ZNeolsqDm= zq)SW!n;+qIP1Xo+16v*0=7ePXt7-ehOHDeGF|?ibFgQ2Z7YtxM6Pxf|>Xk`_3})37 zv7nVX+KUb{aq|6T60w9K1Zeaf&FuhKZ_c*J!~(B0R}W8eJUk= zusmJzWK{Z?-S$YI$nYQ!1-+KWR_<>8m=wxLwnWN6h9Hk}>no3$iRfR9rK)9R(P-R_oXl~-h%zbKg}@-VE9qyUWboztjFwRvKs}#*9JdndtMaJEDP}r zqr%!f&o4vwT6^VOZLeC(s)plM&x2C7gikd{^xyt^G%Qc89TjQqg_&GU=YdwO~XozYCtc21R- zQ&TpR`@aWP^1Z9NCN|%{f;(O0N8XzrNdqC*-(xc#EBCPwlRs}zyPG!tu>z`*{{nj> z_#|R)+>YaPI>9Gzpau;-`4#{G8s*@Vdmax5tDaPzyro{BuV~c>KDjBtCn;V~--J*8 zW(0}DC(ZoOmpd(r@cD_+!V`B0Kk%)=CoOig<}g*#;1hF97{R1j_++lZC$+JgfQ4oo ze5!Jm+X=tmeH3UDNuDBrWto?JlTsP!dbD@)6RML)%t9;;$d>@G#A&j@D}Uxh;EsWJ^}Lb|dWJ_1-;3}D$6l@bWmDP04W)1!c;FYiT^X`!@|H5*CoF!E2( z1bVQ{jlS5;kP1AD4CYwTSyrSj;MY}Pefh7k$s|9{#uiN(`H_t;?y-8XBfe+~@Wnmo zSd@06Bfc0|+2Bq2nHo22u zp-x%l7@~}wAQP@m5QfeW<@5<5jFINDBf{9gsh|n!k4TUJUmU_D2VaD|nZXxF@_~hA zoS8_|9lm&swpz!l;vaY+#25RiQz5>9#jUDfj$KD%<$(2jN~A{c#U(rl@x_z;$iWxq zsC^(V*>9AKKcS4JVA>X^^{#z1Uq^IEqyQJQLG8P5SWsK8hE=r))V3>{cuoe?&ZRWV zO*74vKeC^N9CFPUoX1;RIG?T+*sGcST~euK_~lrFqx zDTI*pim(Urv~zrOv{4=*zUlFh#W&yJw#7Gx(9-~qY#iUT2;Uf77~-3|A947`@T&iF z5o)9O=7*;K0N>1}4btyf^*_N)0L~l(aMA(5nFA%EodY@yMn(Rg+gbz+}+KCsUmXzVk& z{-d21M+d9g*=Jg~8@11b_~f#PeMSjuL;sQ1qr!bZLH>p?{oZt>@WgMT_L*?Btp6xZ zxjFu$KWWW76}yDl!<*|q(h6Np66-#CMa5?sW13vNmi$M=*T2IG%f#1zg|-fi;Xm4$ z3$uUs$eQ{1-ELf+ILmed?IrLg zkLO3m{^NGzzQY&Rza;lsu-?zuN0dZ2@Gl+BwV|dlpjBL<$$zbaQXBnWxQeZRA=`hX zl!DHJa|M^y!lL;6(0?TRTsVX$Xv{(f)zN%m{6_~+k)-lPG$IphB=2MRm9v`FVzl4hxq!)7$RWl<%0w|F=kN{a3O!Isec+M>#KvMl*U~^85@!*p0$6^DFc>Z6i}b7xtK^ z`NZGfjF+{h)a&I){LGit8}mEx=P2o)`d3{Q`kTh%a5|YnY!NcexcZ9x->XgLd-1^< zIuPKQ(?!~-J*I7LgfEgi(n05wF?$Un_L$3Qqw_cIM`PSr1?|i6H^rxI2A+97Vvm_e zM;JciZ_0Rf@Hg$G&ePM^=}drUvi(SxP($Don#gq*KBY&=*e+d!Pw94? zd7|3Jd{+G^4cEh-5Vq_1munCmoa&l&-bfSsu>b~{c=i{BPcmcE(t1KeI5u;=f{b|65%KWqb;0@fN7A2>1P*v$>Vtk!+8+DehqR}KTZ5P2aM>qin!}e@pp@jgYoA? zYDo+m1A@x9P3LF0{ z?XsMmv&(Wms!5v+=NFVW*^QP9j7KY>GF^Dc6X?h;l(^!5LplCqVU>*cMjOzq5>K8@ zzhSEp+>tQDXs9p%F{2q3BC;&G)BM4)wbI(s&7m%CC~9JJ-DpT?c*$``Z{DU}#Fp9k<(;I5IfB?k(aZ?ETrI0C>UonUW(S)Ogv?VEZF}(!10nMi>;I6` z*^s$0wbGl}?9F`MZfD&%p*v6+y0eiZO#ixbE|JCD{sWpHY&~RC07n!OxT8G=#c79* zj^T$+hUO6aj_$ET=@2+wPO(ZZ!HRgweq?Ss_gKdboEz|xkytANlLGx{oX8d#1XnLv-wQ^Z854I9~xxl^zmb1pAqpO5&@9+Q4u$bmwm%@J)U$^u`ebE_pm zk0y;YLEgNj;zd|y=d%6*+-2oq^3JmKcP zb22#Do|q!ckb!0Y;;2dJ?q9_1ET`1h5fB-C65u7p;u!yp$+es~k(x7bY|1QCmSap4 z_Z{$Wr+7@~di#B#mhu}*5arn}%Y_2~o0k^~6(lxR1n6CA0F*aG7T+wl(( z)pR^gM*~{Dq(qU3zatbQhzj~EHH7~tRVZY|@UylBng;r9G{(j!oB84YgV!SQ%IzA9 z2;TZ>h`(&u?*w$ZeJ<9^-H*5}#YMm0R@4mf*7>x$wh|R;o45xrhyJ=O{r=Ngu6y!@ zwL&%E8e59uBH!&)dNd#j<6IlD2k%{IpdMV(@w5{Q6==os_^kA;fF0@enj;{Cm; znfFCdq>8@-KfU@B6?5XBY$gM-LF-IZa`{~O%jOc5Y}{iAYHnzAVG~2aTX#cI`ON0} z#<$48okV9fYjgmRVR94D=G6}-e)Ioh?``0tDz5(V-LOK?)EgzWsYYGxfN;N6L z2FL;xVpAoGN|d%VrIof+b`>kp!0u|U*R5&AzNxKMA8Y&AR$EO_DFzY~R7wM)f}lo4 z-D}i>QbbVm|9;QRy?Zw=5Y+bh{rf5U$=;W_GiT16bLPxBXU?Ep^oMxZ9$wCo1TbC| z;C+CB60F}L$!Vw_h8R2rY|!*fPGbnCKM%kSpxV8CldIZwT=+g}TEhv|F2)yqkfDR1 zRf(UGcAS5n_#uydsvQ!dKmG2HP*~FXT`mKMw|kZPhCPR40lw@x zJQo+^EAKUum3+cIhfv?1!Zp>mdyJ*Dktp@8-#v$&+;g}ha}(PU#?l}}$aw7Lyyi2M zC3=0yt%q8TTP|vxvHb zdVoFvv;z##rJ^oj9Jn`>>jBRIorWs3p3QT#pl5TNKIcPR+7M8#yvF?xF@d`GuKt$b z6rlAtmydeuZ+{f6OzUqdU7Ob5)PBHbU{~pHzm}32`dgliL~s3#afX0ZVm>72hZA6Z zDSO}qSU-gu#{iZS)7$-;05a$hY*XViJx&M?JhGT|FkR6%IjV#CIDRwU^=x zvClQ2dPo1`e!gFG_D8c-H+eL>8}IhW_PAfh5~{wCiq`C!2hi-2)JD>p-S9h=W_KAL zH*&wA3N%;p%hBf~9tQNAp?{?Sm`YR;j}U9Fz3na-tTDzK9QzL9P0Pdn>}}scJ@yb_ z)79^YR{J$PcG|y-I6(VO_>;<)f&GBZk;PiwqqS4iy(zp1FBf&MV3}v8y7;%Qy7wB& z5T^w|qetE27Ew3k=}br6!vHTQS%DJd=uaN>ZSNF#6@S^kk+B1O%2+mq820FYOtYZw zl_GU6G!Avo9jtxzzeum|)A8M^{%g^;`ksB5g>HHD!N>5@l_wecz{PiYXeRM_pg<{v zuR=$0#>dqMuRxa^dcv&jv*k!PulDCl?o0b~g%;G zPa>31tf%E6!J!vcKAjSa%foF~a$Se{o>}))zuwmpBbvWW;2D(Vz;fZjNtu%s) z9@iv#T(hIceILy@8XB($(CLo3-qq>eLAWF4Evj-EJ@cw^(#s&KbF{gPc&HgXCA?)m z^ET}IAp}S}#s*9RsQIY%PCYhehY^33lRuUpY;7%Y4{_Y7&cE_l#%`BF*W8KM)Nmg+ z5?{iIN}Ab38AgoAdIF-X6*hZp*uMNKYz(t1gGN&e+Z%$-yJ#I1_dfOJAxsr&&U;7^ zy&MUgdK&;{nmssyxMq)+Apn>K=tr8d*dHuAoYpT}iW7=qWZ?8)Tl%YI06Ow1T-=5W zBXPa}9^38@;;i8Kbld?@=!4(H+tm5DBW|`b8^ET{Y8>Lj2{BmP!hrhYJ6vua3N47E ze-;|8EuC_y5eM7*%vQ9yz-X%LTz--?vmJbkirM`DDn*TaX}^!h&U#3pkJ0JGe{u@) zvk{93XIK2DoK6?PcEU4*q)&;H1p0%+PhjX0PoO0WhuxQB0*oAv$#1PTU;N}Z(xU3v z=h?A(JON!l4mcXZPdM5KJFm5m9yBq4_CaHyJ4-dAoJad$6t-3mk7jK(+pNii=G*mk zPxzvzvy0JULdGRU9Stf#DD(u_3*LlP9pK=T?oTWI&H1>9#jw;vIhtNFjxavGl{0sA zxI678r|-<%GS&cEP3w-J3S<|_=5_~E7Jw0nL$V$x88U48Lp&bCT~k5ivBO@*l|rxQ z05T#AXaayS*|XKalcoR8nZea}fd*RSvMH_gUi-%*1*DBr5T7jB9$W3x`H7m3(*7@w zr;AaV=k&b4DZ0vL?6n$yXJ|yqq!%$`UxZP04jj=VE66^ zd!bL7Di$k;09T&E==MJ`RrF;LI{wVv4uhO1wP_m5mr?anTA`}b!$L>kiDscgl3<}a zvJi&*EW~93o3wpn@0hUAA=oTsG@o^8ZRsk+mt!cvPYVDwt`zo!W&VlI34NHmLH9;t zH07+YtWfq{zY;PM-tkMuO^3ghk3mKDCEp8Nay^n^$U))y@cKDPhX(>%6?n$ zo1u>^#w?=jGjtvbe1sW1`zfROBJ5IxeA+3)d=d8T;^qFQjq{Z~6&D|A z{7(V?k?M_S3?mah;KmJYhnPW1H>(kF(EqJlTymX_G?2+^l51Wl^=tSY6}iTLOE}Rw zCq}cwVV>uyVe~^WiKEew77bxXG(G1JittOg2V*H$S|U z?E!-i+@dgO7lU&KP`2({D){5ne$6yVf-4!T)4CL|&WXKIXnoeI2u-tQhRV=#KCUao z-8VBd4)=MuFUNfV*SWYZ!EZKx$C}7%b8wIhp!V6ay_*A_gvp&p4pJ;uBpKyIe zpKyJl-u0ae1cqclN;n|ag?UT%)>njm(J9vK&@|#+#s>Ju>v350=i_+=#zKZ@Him09 zhHG{xH+B3vm%n6hF#J*Qw{+dV1-{%Rdl!5(_@4f^z*n$jZ_7u5@Al{aE${`G?9H9>QO5VB z*Zvjooks>K2m8zcBhCbm&bF?~fu`B@Kx|F6IVC4q0VxZ{qKlrKTwA;aOiiIX7rzv? zv$TM%0#m0xctU_g$s(f}f2VM0kN(N6I_&srRcVBRUi)YyY+^8w;~-QpaiE6gKMcs3WU<1&!CadeWh1fd zESnptv4+QIWq+kIz@nos!NRyIXidw9Zen({SA<6T6C2FeRdB$vickpoM4Ws`MicIX zo6JX%ZUz+qKXVXra4A8Yt4zsI)%kcHmodLV6Sc9$q4_W|vpw@ESzDN|GfzVtu#{dN zgbM^0JY!-Ze(M*9M*4t{MSwIieEjA4Mx5I?uszh@VjqtxfXg|kRW}j&gHt`s0Rqi( zA{ro#c0r0B{>so$+0KT@55}XvH?~SAqqPk)L(rQX ze8%sk-t|;*i~XD^PilRKW&zX>s93Y*NSv6XKUgqF6Bsc!ico+Y+G)@r#hJtGE$9-~ zZ{5J5nQ9=^H=-$fJSumfq<-w;;|{*SxCu@;cubR_v74Ceeg*Ovq^D#^B?8!Bwbgnw9u3eHhB)%+LwY9))t6a2>rD<2#SzE9BXImowb< zx%im=gvFsn4a4eBAj!`m-^AKxfE{bdkK(Uql_A7L8mKO0Uxlv(+mWHAm*+<=Z`4@A zk7k(eCiVUZypJB1*oEj4G|@iHF98XSyotB85A#LVs&8Of@pUhC`zKu(B?|D&&?x&p zc99^YA0f02+1?`8sFwX0-wRPlX>YE5E!t(`gxVSA4y&dBT4@lCbcDOs96H^XI#H~Y z9#@|#W zvPzn;?u3KM=r&Z8jSBfOY#3equ;zaPvWk8bA)p+TWF_~On;oh9^#gMc?n_ojhr|xa zvTC}Nl?3Jzm0Ks8n>LateIBV?koXB^%hXR`Wq_|tc~8>Cs7!Yyr|AkJIn@3E$Xt}J zGNDS3M-Dy*S@_ld<$d65kHvNl3|~id4}heKX~Yqt?#(jPqw}vC|xPm_6ha(#V0>u z_@3*hKB|jc!j)l^OQ1H6vqn&oLQMHr;}LbPJUqz3b%;XMSP9*W0y1?t68DbYZVrjH z<;wz35see?@*4B`K>*XjoI|^GI1e^=2xw?qCgj;hY^F3jVlAVnV}%h|n`$ z;h%zMrY3)Mo4Fl|;|Z~@GtF>I?4|9o-Q~tjtC(Nsrk3MYCAL7QHB@fAdMHk;$a)u{ zQGd^CZb4nuszX?n z|4Cg*E|lC8kfkK{;`Z38EL4Qc*zX(Ba~(|!7cqHa0C4!pC}@I0r{(b+MA%FU#qWs5;(dSWaSL5af7Yy z`cluveyE2>a470|lGL-&Lp>^9Fg?C64*y8wdja`Cjqj6se7VoY8Q%(A>+vncrHt)sId@dzV67U8~$rYCiB{O@WqKJ2K1^t#ZiWaM-|yQ$+AwIQxU1t?tyPn2P0s-@q>|ah zY3#)_le58nl`CQ|Z;!nPv9mh%o`@YXw7DK8UWQ1v08yQ>4LyfcZ!}Eyo>uxl;ukO9 z-4xs4d-qvLrL{sblPO8HEXo7=DUy!U@hb zt{6d5#T(4s4bRFtA)nCNGckag|E8P}TkVh8{szC($W=EdCp+?K{xBj4quP>;07IM| z&;^K?+}NwGbc#q%QwyRP(`U^c_AHWzTEBVZ({WOj|8ZSOE>R9YBvIa~X3?gZbOnHj< zkjogpR1)D-A;PE6hl!y&H@RRG=0JxIqid~8b5e&4?Jb)yKQ8A4kSUwm(z0o7@++M8 z@<^K;4B#v|sgNRS-A^c@+SHs^$T=^1ks_==kW4x0VsbNXcvaoGgtW$);QA9$@1w91 zQJongDqq4B)ADH|qMr9Z|Fo$gPR~CNEu(yDOrT~MAzjNa3|Rr`;|d#Y8$5%51lLmO7|<c>jFp>QG~a|5diS}Qdu7Ju zB7H#s7MTXdFQSC8atmZdcCuy@)iT&1Q%}fpKG!PGF;|N`Uh+}^LMk<62!{veyD_n@ z?MwFX8DzQ<|9~%ligTWY&)S~D_Aq76%zrMmAhT+6k|Wq|%wl^8U4Y&-#=n}n79a$EfZc%kVd@*$nPJsHj^7!AJ07AY-X9cYgr+jy@Po!(!cjv!+voO~X(z z1o}IWf8D=bhNo@?hMxL{vT0P_BT5?jn3-wrW|&ik5Uv8$MUGySUHXD?!*v2+dTLk> zj_#n!+M<^jNF_r@Z2HXZ12|$GYV5f@ffL%ewb*xC{ z4Ktc1h8nW!ub@`F8*Jc0@}tYZeaWc~Qi}Z8CAqTtZZ)*1TB+)b(aXIUXhAmmsMZQ> zZOr!(@CP93I+4NrMAVh~#ceY982MxrqL%RGV6b!D@rQ|pbH$H;<_`RihLKyV{IQl} zm2zxW-fPuQ*xuukGF#Ww?4@F*Iiy-J`LXRUqBTdJiMQVVl3-kumfQ#M_OoCK8_C^G;90R(w5 z24Li=01Wj#y*R1@^_{MyzX}j>;jKV_&o&9bb2|7#WN?3<7r=Za?dN;j-!h--l&)g{ z*g3X6=L99bARRy=>RM0&ASA-USQ%jy1fmSeFM^kMER;3|CgebgE1`nG{&9Z5k|*VR z69d8C1fiG#A>x0bmPKQfE{jH?c$HAGU~EIWupWm2+J~3Ee!uN^z4eK;?>AP`COA`U zg5`j?!YZe0mi`f1pYkTO035PmG-x@&AgYV0Z*M0U7!^SS)|azGV}Y)5T*F0h1|4*j z^h@2}{@TmWXfOBTf!e#`?E`2}#g8Clg%DXoq&rTKk)kgKA&HrDET0ghvjUCPJQB z;7#2valv%VnsWN+4Jos-L;8<{7LSq3@Z>8UUi5G(TDS8qdOxb~e;JLjVh> zV^)8Ux_ot8{IO`6wJ2znV+wAwCS{j)EH10Bd&bu>No=FhKSBUmL(!NRA9iT7=J}B z*;wUNuh5Ocs=MSR+T3D$f{~L6jgep^qBWH2ZuN515-@k*TLsPdFlMr9ytn7!k1D>C z^B0P@^?}?-Ok!1aEquM4ukGXUz!979XNaJoprXLStdt8#e6sp*1eOwl*`S57#OK{Y zu!xK*TJ0qzsS@>N2rt$*1NC7*`xC&T=gh)cLa{f3nT`hypz^m2N|>k0Q$iW6M@w_Z zUo*Jx=P1e=;QI=P_&J&65m%ZCN_5{`{1n1lD>9;g`~Z!FXYlpS61fPO(@%W^`#+L- zRcNWN0|b#BWKpay5?nX0-<{zC>0_V9nyf$jblWs#pGJTn>^o3q#s5)`kxmWAPNa|O z|0pD6H2!eP$@G8Rz69Dt>+(D_0fvDQ4c4G!-XtTz_+;e-IR@&Tc7n{prFMc;3UG9S zT!1UrMzl{S$QP(x_Ui<}F-*#!n^kM3Js@`_D^7FRfA~9Of4JxeX`WXHY zF*Z)Tx?ci|&Q?y5`_Pg@NyZI3)m>j|>d)eMh)Yeqog$5y9zkBHXI01IBbbjL;p>v+2sA!}c zSGA3q^l=NGWspZNf7u}XBV(}INX{x${*gKCq4JMhm;;M@&6?CDI4$L)c}P~FGI~fJ z#BYBdk}5E1Hs(i6*J%$)A?|WgKdl34tzKS;FGqLMQIgD=4C6m;l{H%>;@OBLYQucc zisL5vI_XW^B;WT+uKl}7N>NE~H;Lj$I!nIn8Bc6|Q2qh%a@t|?V~f5jE;<0zVbJs%JEMW5M&^q*WaQTtB{mH%W8D8W?zlP7S3K&C9i{JLNN z3Fpnc4F5?LHML`|6p=={gYN6jnb8Xs*=729xt+#KX8UxaWV<4*j}s+h274cwX$B$D z+5kVI+R`)F&c&2y@S->~7&4tE`ucg5A4Q3@MhT>Q@$6y$bzc~3$C<3WwG|B%!fZ^SLC~t7~)T9R{oT-kogYu zA%?L8Mn8$S+3WYFHgaTFkKSWGV^#as_l$lv`f2|jv)TtG5o<1nPbz*FnS@m1VIV_4{b|1w%&&m8;ef%sP% z&gg4p-G=2Rh_A1I<&ucU%36-$)mGL!Y$2>Z{*^;eQR*}kgCi!^{rgu=tar`qH!y#P z_*a%YR@REc9kuQhG-%bd!P4p%7t6DdPxP{IVM8~|hdotAX*26R(X2%Q{%b{D8y%l?~w2OtFm1N#jq;L#DX*3eDV8!ev-X7&+srZtE>JB9LG@1{< z$#U8(kHvK~<{p>6Q)8|bQrNGz<#U*2w43cm?*U)Ey)CB#CNaIzpGVW{hHLikZCS^8 z9~Mv~b2rDEBME`~WghJ#1eW&iboq&ZmtGd{)6vSA zu0J6VM6kZ{7xp6z{Ll3ExAqnWZwNU$!r*)0-(*@CJoK^%gTcFBM$hyJgX2(H>V%kw z2=?KAITk0)IqnxpnU~@GyeD-sK*?~wtXt|Ij4XB3yaf4_i_9$P8~AC_icKR{+DLQ z|I%{#{`@cXVmC~?R6Y^^i)tVK7vG}&wZGt#^uILk$Ny3eaQgSZ4F5>}m%i;~`d=Cc zY_IfR;eP?~WcXi};b?_V$p3<>GyE?PDzX8Y{!9EX&2U?I{V&gAuLvA}2jGAC`6-y? z|K-ai|h!7{Mv{AWnbTKSClot_Z3*z zAUBt4f1m!BHQ4<%SpUml>dW7U|E2mo^Ka*#^Wa`+QI9OUZaT8GlwoaO75nfI4&H5k?70_T==#;A-+68`{zD(n&~R)zsN&1hvED^bX@8h2>iv(CIuH%{3iZh)Cm5lE*?}Fka$#f1#Z?))L^}*FOK5h%?>PJh%*{P#=TO>i(m^Vg{~W74dqUTh zn9PR~&}`p@JzXAn{8m-=l0EYNpyC$v0|6pvPHZ~(4Wgl*4Tz?XwG|LYmftGRov@wN z1qijbF4lSu;myZ~;`#j*g7}GaK$EXRX3&K0nSvqRb9Ij|``}~wCy$Tqi}#HWc|Ul3 z`056Sk9AMOX@5ZY5PTLQEcjTk1*0>@VmR?Xh9z~|Oky`%kF_G90m@5c@PfgYFJS%U z{gmhUgPhpkZ0zMmdg@TCmj?JgXTo;R`;nRN^*3s?LH}|;(&Y&Szw|TLIy-mCo?KQn z8XMz|E?+tAW!9g!WKVhD`UP2&KZ*MDm+V>4w|+tH z%Q(NxJ9q~KfB*E**sDtRd0+H!+mocmLD7$Zyr7@*iXOIK?a)JWzx3d%Pro14haUKi zGd`q;=JHYQedc(99)fJDA9}d)s~-cO4!waMs`}P1^so>|A$XB>K;z}q4|=HYTfZQ8 z#7C=N;eSCKNC_0TwDyt5DnCgX0FVSJm?P**s6vF>EbP&kt#_x^-Uh1`DaFQ3}|5dl$?d5ji&tWry7yJaBBAWsW}U$FnX9LlC+exM%H9}FqBIb6R}kx z6u{0R+(mCL-j(Qy+=fQP{T98oyIJOM>;+?C>;en1T=zW=Vd-D!Uw>x|40othA%u;o zV=9ti*`+iGNv4T~nOHBqhiQ%uMWvH)Lux?df%#&HhnX4=)9{;}ntOBP z0vC}}umyEm>in-D6$=1ILY0=1&gd|HD>x}KH<^1hRvdc^u`@Ym;c#HX>?nS-`>%-E z+pKyXeM@p;6mRLiTP4T%4f=H6x#l`_harm`;JJFkRW-T-AwiqQYykuEvT@n&o}NW#;%=T$-*Zl0ukhDK@{IT2$E+V|ClKeB4zFz_|E*-AP%8RB zE5?Y-M>I2#bT)oN_+6kG@m6dyXTBWn2P}vjRoudx08?TQkj-7)F8$051$lPJk7184-XPzHkbLJU6e(G4#NWa%n#&)Lq%B6Q3wJ!^O)~7j zco%2pRqT|6lgwDT#%St9(E?*-JeRx0kz`YLjVG%*CARtwe4L#52VZi2Rj0Y#_|v1P zbDnX-^?2;xUJAZ9lJ&SYw?Fi)+S=N8UpKd}+I{S*Jz3_{quUc(BA>A)lhTdY9u$hT z_|3`Lk6@YG69aU90u>^m0jLE1GfrV{7Z%pAN8l;eM-W*-l~cxujl>sx36vHiM~RU* zoL@S@b=OEnCfXzbv))lB5b zu5muIAVh)_#=bJi$1JtLW!Z^FJW5bzgbEtshY^`}6HU1ACoA?Yw0-Oj?nQd#gfO;& z6BT3y3?R;CtKgBM?mwgjg7%7ml8U!*x58nlLsCN80oLAx?>X2Jb5AVWv+gStq&3V$ zh0N7RRzw)?7^G0eSF$=n>p^~OKSm3$D}c;ZMq)3@Jj78z3ZF5|P`#N&I2-IS3%$ro z7kUt2_1HM`B|R2PvBjjvW_m>Ohq;dX?c$-^P_1=K=uZ43=gl{Zu%8oq+3<_}Gx1+S zHczF~a2#Gkepyx1fU6R#Y6gDFtSUy+lv`CZ@l#<{&BD(#t7{5o@Ka?~72#*5 zRW%krv#hFd_?c~0F&cA@RYgHI*QzSF$}4cixo$qB<2d{VHDtIfpun`2g>C@^fP`mP zklOO_n3d+>)iU)c7a#qcmyYy|hZ=bct@`(fmqpG&Q#}Fd#+m9fh|wt+;<5PjZkDae zPpV3i*SjB2Ko}*o+jvVv67c9c)c3Kr#d|Qw5}wVn8t+wCv#rMa)zuuUk-78ncCOX9LS4b$+pXR44 zRFy4hy9Zxt8K&1)6lGo=p7gNf4e5S7!~wEagk&UO087rA50yRj3+`W&Siq9vEvf0) zUJts5_BnbvPPKw55+AYqV-@{hFVjxye(VhcxOem2qp7=ahowaZSKp3TdfwGa&?;1_>kM$?d zL57C-=lRaGscLQG+c4mrfeW7zXNJPEuNd(pe$6LisflGJ#$toUZg0 zfeVLaftLi@#0BKai}6SLYcze=hko9MT2d{OAhBPkS}6Ju)+zx=7Mro&ZafTp6k3^4 z@ux^Ww4&j9>4Uk(o`7l*0B$s)gAVksGaLJCyzqWHayS%7sHH2>Zo@Lw%})EYUZt+i zX2fiCevF`)Puk+y*T9YdutrnLXi{ItH$+EShho0@a&XCe)U~j(Ei)4Dvj((tGHTZC z)bBaiNF0m%jPZ_teuz&Ff^N88L9@pG5&GQ-eke0BJ?#MI0|3@1+LyTsL|$h15Hru( zHB#CAqxc%UP{K81dA5>|xwE_%dW?7i>IpUV#PKL=4*}57mkEUjF!Zt@ z3)SLRNs$UHM_i~VEC?&!=7G`(a}X6!_LY)9Py8<>F58jWBGh3RdUti)$N+A6Dof6!FJ>W#Rvj7Y0)V*VuIR1y~;kwO$ z4@-@a<}inr@l+vcpEUgFPA>wIiZL}T8XX*uKbC2o$w;hbih0N_CavdjBo zTsxik%7hPk>aQxm45N;&MY5eUkver%WRz3lBu|OCkUuCfjU|l4*IAFJ!}F!XE}ecx z(O*BzYuZ3e9$q`_%dy%A`=0p^MVK}^s0o1W%14S2fI_DojefM?LNZVlq^dZd!*XO8D88}>FwzImaU zkO_?gH!2_rl11z@U{i7DJJ!X<*a{k}(~WiiSnDH1g}44*z3r#|SWCd(LTXj?-0S-s z4WAaT{eGWq-#p;=9CyIujPC&7U+Dc_`p1r;e-Yh3`;X3i;vq4`Q<-+d3U}y15Drgy z02svlU`zZ-Aq=q7G6hWHO=J+pkq{XF8ICQ{Xc!9A=%Ip1(Y^o$I7r|%5s8=Ne9&X3 zIQVLP;>f2Wzz6v>7C(?rTGKJ`Zn@#_VTE&%O1O=mhF07Q!gBj$+S**;MIgE9n(iZOay38h^~y z_9G|^Td4gu#t9jYJZ0eU0>DzL`|bf>*{`Ak47el=+L1a4Tb!}_wyy$V;OQ7bz9aRI zGu6}U@Ki6u9Q#38+J5j}gKIx{)MG!G^pMyODzh;8*m<&)A_xV?qe)h6ghJ?FlN+~ewwpw4Z%9i8YpJz$}$3|wsV!#D| z-PY2lt-!tJFdnIeU;pKQ9Ut9f)ooK}$7q;%%C-Z`6S$^1G;M!?O8zvtQ|KFT*bBY- zryRAl5Q-Afrq1_Etfs<8;|0%W(awVx^(tMTBJ)2eLIjJw&`2<~rG{WBk4i)i>*{Te zaj<%uIsmG}dv^8jq?9g_;08|J05-n&*v+`dTB7^MVhTVv?au?8*oQE9vM+U+1^2=Lq25KeF_Zem)VN^bnR`5R z%)AFU(&?O87dJDIy8zp*qFWs*$LMz<8+Y9%bDe*+*PwmSX!cR2fZ z%KN2t+oG?)B4pKVQX9thV%kymjo2IWA>ghj5!^4!cC2Y{!yT41CDuJ+7|`=oBc3WN z9v+NKeIGlraY3O^vq0&G9r(+|;tTkz<>B?AdfbrB4#yd0u@z!*fT{mw&^)rS9SsK@ z~t0uBqLRk&q@tV}zRD?k1oUe@m&2?_=?lD{y8o3f&s~4xQbB$B%j(cp~3|@Q?p5zQB;u=@IOS zL~tBmf|ZoM8#zpO(elyIeFA6+4_FZtkT`x~ZS%D$661;Txf-Cz4RRmdZhqJum-hA1 zbYI~4XHCEWrXW@jy|%kh-iNcthvAjjjlNNO5I68h8u827W*T^&yaf=X?cF_*GZmUz ztaI`e9;m@8hFW-uV&}wT9jZLuz^jpkzgUkY-V|yu65L^Ci^D=gSfLOK(!_rk-~v=^xRxy|4SDK(MpGcWOLzfA=VS6mP)rC;a3}MN=I-HQA@u z#)2P!V;y3hk7Gc%>NXO{T_modcfdG=wIQxZrIjR-c#iBuasVR78gXP(2UgEz@_rDN z@+-j+aWc+-tOykv@m~p)xGW1tvSIB=ti*%llq~1_AISHYWkI>d_sdQZqTC+ez&eb0 z0BjA2Vd+QJ+?SXT}&GAn@#dn`z#9`%FT z@nohZ7<)Bj3`s(qJPEpBK!YsDe<&ET%JX;Ce~A7Cjl|bb*ly$(h_tqykM`h!IE9Me zw0S0Qy1Fu8D)FYLLQTm?K;L8I04&EoQIED#Gsj~6Mp3e|1&JLQk=Ju}E;JBh&|Ld>n3_9bB?UypTV{0TuuNwLh=l5}t;*u#f#XpLs`o zH_#<&0Y>~ER9_uQ0MYKV>!1$e~k#rXAl&cA{EcxX1_<>h~% zxY0CiQhZB;SO>3`dwd6UoWaqZkR<-m4g^I2F&^27mEb~^@h*$-%Q%k+ei`qw0Keqj zLK8e2mC$2`y{zYA{zX*GfRBlZUFPEmed+0mOyQ>z<}t%e>A4B=n6wgtSeXD2zC;kt zuonRrY5fK9AK{#Zi;Sl1?mweE6y+(5moZ|u3-FAQG50c(WK-nUSevJAwSE!WDoQ;C z!vp{*dXR*aL0~*X)?B4YOcB^Bo7sc4z5q&qq0HfbF?>QcgaBjln^cfS;!51|pTmXP z9e4q*9Q_-)koY-+bub!1wXxsh(sAN%IKm4c5F!}i#ZY_d0jKs){sPaH;&e&@k!a5FCZ^IV`9I_Mrh10Sg9=jL{2U?<3_J7?Gz0XrDTY<#%fF8qp91lk8K3eo>qD&ZCt4rU+hx|L<4u1*P<)$?e;Nw`!Emmb zI{xXO*vjUFa_ztcMwT;kg%F6!3`QQOp^`*FK9@Ky{Faj_9uS8Yb%r9mE(CFu|!{u0r7EzQeQqg8}QaP%d!3OI5${jMMYi8p%% z*NivgfnlStlAMTP#Kc-Keg*^0p2l1H85yaT*z!i0mJWIzVsDaY=a_}HV3Q(LRTOJWN0`Ar!v!Xyw<1jr?zBbb5lqR{O!Ol1a)49( zn1~mg31j<*uhOEX7HZNcqiGGE9f#ufwfs(k))p=_noCsQZpOn$04_qefOl+?n9USBQeNGg9o-4xA}4-B ztQ*>6e~Y-xb^ONk2y7Td4Ds?uA+sgKs{)8~L%a~iMJE%&xVSumNtaG0m~<(-VbZ0O z2_{|6-7x9W>WeV0vG|#%tiFiKpddk12Ccq`%FrQRRPPo;gjJ9ypyG;~2xE&!j#CMw z=ne5I$M=rK8y~sO3`c;aUACvgDo^(rY|;3p%v5i*7>kQb72{q3NyfO>a44#ohBxt`JdCaQzzi-HoV8O^-lbFVhr5#W+}=Z^vEyAe(Q<<`o%(E95%ObFf#yb z$(_9nxE1DfJGEXZ6e!DLHFlq6szvC zvP{_J*7sELY_{6ccB^`u*yT!VB<|~%Mss#;>5k|IE4xQR zV|#G=rG%+gcL}`>v!XEST_ZVlkoV?8BJH)C5d2lg;IBq2@GC2DEgx>cLpS=1*1#?m z{q-(v%||(Ak_)t$n3ye*OSLel()x=~w2b(#yMPhKf3>G>!By|M)QHnwi*I0zuIcjA zT-iOsiD&8dTY+DfK5GtLI3YRiXN&?HDp6otkvUIAfuYfD(t6}jBp1pNK9AGTR15H< zD`|;-_R`kaUL@1+GOlGnrui(KWp^L_|R0apS=jxba{SHy&(}8xMw$26Z#&5)bg0B#BHO`31_}(K`!NxuYjm*V|Do9L@&0QA4-Z}_y0xF#dI`;Z| z1fFEsQPxnZLWIQsfze#4U6FF^TG_}5G9%S-o)F+ZLvUzvzY3*e*Gsz+%5?_V2aU5! z48DorRsc-U{$hWhZoVqPKP}FuwMcqU?sT(+%$4kBXODYtR*6~KDMv=P= zH*E!RK{Tm^0Za_Xq7{LQAN7XhKED<>Y{6baL9FaCmNGDv02lxQ{bX6lhayx_mW6o8 z3$>JGA*RQpqOvTMBUg;C%$2KhTv2(2pIZ0JRZHU7DFL~sfzRk6H5%!M>R|y2v02?i z_C3~;k2~^oKb>rcms`dEbYZ{&0R^nv)wtZcy}c0_arHsn5HMe6cALQ0mH&93%7cbd zcYy=LZsf39rGIH?sbD#y0i4pv1^ii9KKq5MzmSJs z@B#Wf)FsfP4)TVkrU6j;jkk%?LV!-ib+(2Ha~GMDTu1_(2&&wjs+LLuAzWd(aO+JZ#`Wd%i}A?#})> z!i3#c1t@Zo0kPb}ABQj0WaISAu7z>Af1J|?Lz<9Crfz^ox8^>u?qCAc5o^gFm?U18 z^f0~Rj6C3R(saS=KwtI^o{;Q5zyoVJB<+ZpeS$aL1@GtlQf)5rfOm@*yu$s0$9NhpT^gJWkX(7%i@iC~g1w2=e-}D`kH8@{ z;Vl5j5f8`_7vx5uT+RPhzw>V{zVwI~kKiHcxe{kE^Y2kEICD_xTn{*NU2yyaN6o*E zKC&~<(I*1jSV$Jd83{dS!*7@kmqPqrexhblkC3E1azaYG(jle2q!Q|KCs;LC4Zuv_ z7hVscJay}t8a7WCW;$H}C^9W$0{zYiy)MI=3_w~+ynD}V8-e0LFg|0 zszWg;sF*J-ykyng8Yy(jyoie3GGl-fw~Uea9qYL*V?}5r?^(&9^Gil`G$RkVQ_&M9GQ4p=oL?v%sh~@#JUTmc_ zVVACt<=6X|zqRb5exFt^=&~L_?atXXgVN{@7#7re*N7e_j@Vyl5#fCWN9 ze%ec5-VR8dvX}48@<9tF^FcgOa6zx-5{w*6i1901Gk|t($q@Oel11m3-INwA1e&6` zG>8JC=j%-x+?j#L0TZ(D7`Hde3DPpP^()<*6?p0*doQGItXEi*^a_`ILz+UtM6s`?7dLk_TByc5t|NZ9yGCC{fdKwdx^SbV&~U5VCV10 zQ>W|pY=Vb~<`W@a(k4E~UhRoDwrA5`y-DoV-hkuv$zJyA@K$ZGJ{>XPVz4&i7h+ja z+=CQfyOMc`kj*w*&FZzV_W z!^R(jU=Q4kzbJrxzPNxryq{>~w7*~5LsdxgU!-2;;EA6uO=dGPlbH#;@I*&D_UIqU z8z2opHiw^9w%}=aNV6;Ls|kR)2*wXaEOW_=<14z%=(g^|9eZ=QQ(~8&xoo->-KJxg zlQ~1NEqHwK!WV}ilD0YeG}M4F&QslF9c2~#+zPL)Egd%`avluvv43#8b?Gx!V7WO& zVwjgBhWUZIpX9V7IKD9I>qco-`kaIo_Rm+*JtTeJFU`$^nk zSiCXJ-Ro)_9eeft{Ep^n#wv&LkvO~rPA3-HXsX`UeS&A>x&tuPvS8^X}7 z=%(eMCZEm7FwzqUJW)vFLwM_cg0dCQ(%u?-W446_45ZxkBtWPJ2!|D^xTj&97XJskr4xG5n!3LJPp26X{2ep!19;h6#0UEkhvVKRO5;=X}Dz zRJ6Om!m1?JHe1Z7AxTm?J;W#?*KaV0MCWf}-JV?k@E@IdN%kPAEiQeVGM#^>60`O5r{B4NMTBeFxZ^ZwG>%^Pl!;TzL zyg`lf>y)diQUuVk%cIcV0^lG>*dnoxELR`Zl!RK9#f*@sAn`xrfxQ)fFqC>B6eMhb zTDAsY*#|UZ7#VboDoYhjMuG#EHxh5MdLdCG{ySVdWcp8BgW|?X2NK_c{6Jj24y|Un z{z%Sj1#eT*pg#EfP{CsnLk3uh`E6|L2+7nGz+eV(n_jO5^oRH+;K&W~ooMW_u#t5oEumFqCh*|K5=!A)b(<|r}_`7+HU zXCh<+uV>hQr|{J9Rb`K_LbPVBz;|$M>^;Q#9|tSe~dHwLg4mE>@NmC zWIyR%F}H|%CYymA=}tLpAFb+am>D`rB2pQEa+1ASe^F>(Lh|uRwl{N?eFL7W`AFi2 zF|hTh1QAdSA)XH@7s9+W57=V-FpXJ75Gk;`%?=%3Yeqsr9e;Yo*&~UAydoGRg5w7&D5H2>q#)Jf10}int(@Q zof-Nf0R)r`Jm#68I#j4cQ(p8|1S7o}`K@X|s5JT;HNp!+Uh0g*yG`ML74Or%+M)lj z52K~xhdElo?zL&<9#<;=v5N3k@xLq7C`CexHU8hL68!5JV22x({R(iGn7bJREWQSu zxQOm;*O-qe%wvG&o3=!Ha>7!kRz`o$)fnf{6Z@Q_NDgl7;Ug~!)=3$7%&5;f_?=^5 z29hD4oBD%Kq9_?&csnlpcU0pOwiaufp@%~T0ZGybt2+!4XBfCu$i>mej5qxMU3{;X zUn5{tM1+G;k(eF<|G<3^LEjaj(>PF?I<7nm>f`LtA|rkj1g80@i4%WYP;#tI`%7bT zvLCtUMES%{H9RqzuEb%>%O=QHq21hfON&JPo^WJ%{T}Qvx(av6E3--$vr1NBG+l^< zCf6S=A8g_Qe)*KlD_5C&(M$*;1Gy-#n*pGPnd>Mk8D6ZiGLHr2n0Dd?R)cKy1jx=dp=-QbqKy=JxA=IV?<2H%ZZaTb=WT6TRH6 zAx4729f4iHYly4HR(Ic&S-?^Srs@L5%1)c1iB7z~vGNIYE@V~uyqQ=Ejl@mryI~D+ zg~`1(ccnIBfVp`kgtv$nm2KzP(!!^ZV0dodf>NGcBVc4_&j`g;WB;!%#4Bx&1zz-4 zqH&lDsY@j8LY0WhKg}cyFJ<_7p!-Qx?q` z!zFT?4=MmJH-?nsci#uTdqUb(dV9R4RVp@BKh#Rqns(0QqWLCL zchygaU_>gTM^jB_25=H25=kKD<5?6inkv=yh$4!{TjU8oN2fo}Jtilio-WoiKQtD| z57grtn{QPawB_bnLu2iMWMj{VAAb05=W(l-yi|{W5&PZM_4qJS zV|MyF(d~lR^Fv0z6^C9Q{j_Z9iU2i5P)r!2QQ988Tn_#L6e?7wVYSYy2&lP7;$#7g znpHj^{JecWhww7rs@HR4?OEM7JM?ARso_{(L$Od>S<3S#sEtyeLU|0K+A75z`T=MF zyD`$TC#8F-mZ)C&!o5UFz94)}$(N99(l`a(apen8tK`cB@RZqZG z@geolVo*u*5bM0W@#lg4rl2T}DZ}xR=6QKad;c6BAvF@&8H*DjJaeTu!gOP6Mn-?{ z3Lx%vNf$Jlr%TMFK32ReY_3)d%7;)aIXOT{2U0#202ZmtP#NEPk0du%E)JcF-WNe| z6oQEWnsu0ILynzdolVsw&-@#(uz(w(SOi*viVxv#Np+ZwGgXF6#gPT3&E9$B*xyi+ zymg9^IF!Saf&@ARQAKzv66kd$fy}+dJvL*JFy5eqQ~b&}Bk@B-to0cXCskZ>u>A*& zsXILmo*yx1mubLD@XI-#>rpuXs1HAuVX&Ysisf3xYJn|jgvQP!x)!kNVNgo)JDij9 zLFK8L)4+>hAn-w|VJcP;h;>I#GgsYlM07xph%Uy&KRc8UWd-sBn#AnTiQq_D!C|-= zs{|2PO9!Ws9}5X`>R82(CX$-vVecg;J6W{mgX7@Z<5=OHK6C!j=ogANT!#rrN0ph| z6}Hj#le&G;0AVZxzY&Nc`!WmyMj+qc-KJ5z{Fd{3omY^XksucnPOQtHeuVq=DbEvY z;)3)`)VXV7-a*Dq=_I@LS44f0gK=b3q*c}j$o`1~1M;jNEJogiawf-#9EPMf6B;H* z+9yZ-v9`PmFKR_01>eD8id_1zDR{`t%#Zc69k(G8z{gr^=NaV9^!v7Vg9vy27TzC=F$ZP%9+?=}NAb>Ii zpqSeoIH1?TcZn*bc+X3zS3a%bQ1YjMEikp`;?Ov>@;#&JZESx&l_~}%-$P<;1)?e9 zWCUbeU2n|`F8M%qZXRduE?E;9-*CMYMKN=YQ_z72>dyvqGj$+!R87xQ5WnWC?njlp zGIw|XCUu?z4fE~iW%OUh^Pt#n|7Tl{TZ1<;UTJjo89!^{CruBRksg{IdJuBRx4yg> zhK&MYCyuxFOq(Q%%Rm(Q7q+IB<`KpF8(+jep)yX_%qemxWFGbYNk~gnm|Rf8DK5OGJ^T=k{#}6}`{7o3A!z%i@IzL3O-Fbuj}56`lXwCzbw)oei+tqI&Sbo6TYuK{GcznpwucKhqH!@ zcOiV-3b)iRK4@FOxR$C5*MDG=gSH|6;0z=Z;yn%G^WJ{HCs~(E-D_E^`Ce)k?&U@j zDLhNDhfn~kHy7c#E~f!Z*pJ?ei>Id z0G8<>o`mfQ2KNohX)^{dn8e`EcxvvAy@TNW7Gz+E?H*#>@EtPT79`WcuNiJ}yK~JK zAP7YsVQyte|wVEQd)KdQXvhZ<&fiu3sVpOL0T(f%PXD!F=ZU}eQ*LC`g_@{?s=~ee2;lyGK@LA;{GrSgqE9QARd#=P& zHNt=4d!_<1;>XKFqWFFEvb5%YBk@}_z$811cKR=yK=Rh6dyx590F#>hTOv6dJFAWr zgW_Rm8pXi=G^V12FIBz;O<{u>05TFcvNbq87VX5j0+Z_N9)_9zOv2j>7A-y~oWpqLLRI37iTA zJmo6~_bYgS1-#dq3Z`3|ZvnzHCf+h8M6GR`T(=*H*4%R(Bf%!Z|}JU{Um&T%v1 zzhs-Duc@I*fd=$Y?ZG7-u8J8+Mdlu)Um9Lo(`j6LD8A!G9D8wkkmNn_A|Y@{awBep z-0UyoO0Va1FQWn;nAXA8At)ld&H$G#69iAX8J}QWms|*nf1ks6>!iwV)~Caq;yJLy ziwyyJt=EE%vc>O>ak~8L>Lk+h(2FtQ71pOWk~NzGSs-Y#_@bT3i~Zwg?KZCYAB-2N z3!2&wZ8p_~@x`jF)2iCpkdur1WYx|kdlYMzpf60+LC|nShK7D9pGx8F|YIhC}UG*mZxD8fT zdp-Vuj2viqbaX~esUy+6EZ3b9lhlX+I_0KK3JvY${W{3k zu@_dZDLMZzXov@gAI3^@fi(pci9_In^Rub$AkogueuGbds$;R zO~z`_p{F@qvUD2>4Y&~h?sFtBZLhD!IR}$+83H&JUzS{OIF1cK&&a+;q6bhj>A^5O zQNqC)@CJ|srdbZS`I@}MfMc-D9mTt3z_+FcJbpOf?Vv4x;r9ngt95Izwk!!8X(VtE84K$C0S@+GK_NvgnpR-@fuu~#XFn@*g^hT>5cjs} z>2KBhh!wn{3av{n5}YWus^@G)AwCrAP&vND@@qe^TOm!oKAW@{&eb&J2$aO1NEkPw zWX5>os34)Ax;123-4(w8#|bqt4&w(n0ksS28dmf?8*#?z9hP@)^6&-x^}Wz|KZ~e7 zeHU1^ufQMiCJ4>bb*|yHA3(`;J~Sio6k5`~6{@u#!vi;8nvuXM%g(nCqv;+=;+U?Q z=RLaYggH2`zAPWMrep9G#?&S-Vjp+;&o8j#0xv$*-Zs#ir@g-javb!!Sot$b*=z7e z$wN%<)FNOFIwhsG2(vZv1B&Qbg^4ZEV|Z01Te5u^lj1E>5i~i!y^}GW>f2qfrN8|e zznB)}G`o%grMMHlGSPy&?v+MU-NSYfF3k>mB!0l$$KZ!*T0J1Js@GU&7nDGukj*~O zKPr(7IcSFOPaXFZf{=;`Im*stSrT=qBr6L4qbgU(a_?$VMnj~_wg^>wfM zDS^O()qvXmA)cxJBOi)L0Bmm7x&1i381WQfqF!*#3mUkW(M79nEt2sesU+$UMvFxC z*Ow~Z5vFm4Z33TaVfzbQ$_&<2*BSk-`smW6Gy2hMG~qxa>Qo-ha_lx;YoE#*6nsYt zd^z?PSranh-Q^H+_jPVHFK7blHkD6boLF!A!8u9Z{gwK}Y z8GbQa<(vdOan^ze;V))2{Av~FZ%@dlFzSJEgo^V~0abnb9JIqptb8Zp&FmLR95q`o zyPMS;Db>EQ4qryMQmSFHtKOJaMB|qeLT}mASyoD+cv?keR8d!QnpSBehbF^YsQnB_ zX{mN=-*<2RP2LfJNjL6Acyjk9@9GTxBl8s!V+jQdAlFRge{03d1u z;&OM^OLyP}$hITgLi_^$kGEc$<5@42ypp+ID#U%?_0r*ZI>36VdHsL%dTE*pG5+s* z2^9D*TQ80H<$l*oxx%;yv|g$q*9@>;S~X;_>!r&PL=BGn?|SL~@%7Tw>W{EqI&&)p z>wnivnfmj8@OtTdcq|!O{NMEw?4?RuG-CfZ?e z$HP1r5H}9@3_;|PhXGvYF&sm}S@1B^YmP*Z!59xV4LT>)t998YRe$yX^*igm8Qh>! zxzWO@<8?V!O}>r>`)(oJ@ZruRTvOF~@hu(UZFFu~l^dmJ;UGI|;t1o#s(-A6Uc4|< zNMuJ&N&SJ_uPV189LsD+va0iU!h}*3vvZUeCVB-Hv*vbvNN5yFt; zHXOM&8Ge~-<>x?;82d~xxUSxoriC0`_CA!YA`sc}y>7&W!e6o)`W3cR71Y<|gGv<+ zi5wNIq&#Abh*Mb0j#Q=+vU6FGlp4M}3pn2foYMz_zQ80O!Vf4EyGuf; z(l?NP%Cqo~9+e4xDjUJA-S=sG7HYva|NQF!{q)!m&`&A*7|KdNg9xN{=)>bbeQ=c5 z9&qKKfZeK4asiIW2=w-!($>6h|LHw2n{xpA*aL3Fi$U1~&alc$`ub1fAQ-RzbnA5G zKNWMGxX|Jqezc&>!*9a=j2%$)ds?vXXd~58xMaF~y<6=S6k^K!03P>td|rvV==fZS zUytMSvU>PCuf%sAdZu|cjvz|}qt_t$7`NTL3v0Y?6K8)A61w7!!&3nlgDu|9s0sUO zFJI?9aOwbm`(C@x?c?jbD#O=Fn`Bjpd)zfY3ZlAP=^LMyY9!?%}L2Kq+=oKunD)$pz+&AHYx}%RfaVoxW zJfWrd#^VWHTVJ=dUbz1z;nH!<*qjr zIW5tw>kYk{JTSoO{$)56=~ao~1CK9stdZb}!AaK_dNX1nK_GI}Fi|`GN$MeO*!Ce| zU<$w>`s3__%u|T?P&zWwtD%AW(&u{^pY^~cdZHtFqDMQ`Z}QC zG`Fg0C&Oub=39GvAQ|E#z=N*ScKz;t-L_|;Cb9u0%~r=!KA_tcMpoBt3q*SGb0y#O z!Vk}Y1AfxpX~T=5R{)$lT5_nieIGt>_oITUKt}ZF#UBB?xWoT@+&DOa+XMrvQQqWM zXkUXet~}51a6Jz<3i!3Q^y0&n$2B^~nmU&L)<;3^X@Bcy(Kp9Gs>gS#x3SR_Iqv@4 z#2cvAKAkKHdT`uz8_4DKZ^FNNMXtGrk@pXylG@Vq^Nhq}_(Zu_|A=RFvHl6anJ!lD z$<{8`QIE?@$ccemthH#qw{P%lf~n!Z50$yd`dKFY72qjx$&qvIspLf0|Kfbl2~>X{ zxrnsQeQ9*r3FX>2<44$7E`d}oc=eg`PkMZUIDKSD`^!K-&=)9PZVA5M{g!t+>|Yx_beT<0lV#KMIv%suGJA$p?lit7-aK!@lNcqaaK zhc=Xc&<&_Wx<*fiDzlbhDw_Up^o||(W@zcd>G1)4MO7kp4=xB`2jY$~q8A&9lU&bf zMAft~;t_NhU5>v!9p71t3hnXyl?E4{=Mu^V=!ldmao@@ex%9j2H^)^bumtz=rQND) z?XTe;4cN!&VvOAAZjcYU|3~>t4?>Ohbs7Frac$O$Ym-5D_7W6f1eZM(7a2~=totBS z`d_dw?d{-v`d-Q(xVboz3*0egiMS68ra`4sN}){XcZQw7BCxalLdIdX=$W`X(OyKe}EzxNdOk zCCU#sdUSB>rLT|&23Rls<<-HhmoE8}FqpeuI;#Bu)=TT|`|o<`zw4#UAlD;b{7CDi z8(}WMjDs0ypfK0}u9uv}*}r|gv|9aL-B`my;_0sO&{~xcHeg^FP$FG+*+yR*~$o0})EW5w;(qXsC z3?gpYUhAb@w+w2%^pD%5hQY0u4hKwnz4Y3=0oO}!+%Bcu_0lKh-~7MzpPGMX#+7h^ zRhdukUri3(r=3m0hesUu|P*JpHLHuDuf8 zQYVI)GV7#bj&L7;YHfi$rau*1leSrh`~PktwFca(^jX5@*mHJKZv<+`f-Sw>NV&$QveO#zjgDkH4^TH&EMsR3LQU?$Gx@X=vM$>GNX>{x@RCk zp8$gV7y}6MGXe-yeOu;-Dx@B%bYFFCSqxn2ZT$TOBuIS#2o;qfS3`-!~bwN@p*3_-}*rtr9n+f+l%B-oz7+UYMy$%E`(3T!ssv z{>{%_eV@(Is4fcMQ63BY?ZrxC*;J&3T%C9r zTpbQ)-#>&uP#@eK5yQT6FnS3_hf!4A0}vpab2C!`b@unPXGz>Z+6>@i&wYBnN5d620E+{#&_{om zenaEFir2h__9ehkhc zpa_J)Wc$YOKY)}Wa$iMHmaBMIW(LyNz0V4Kv!levGS8#aZEW6yf@1;Sw9}w0%xZz~JuTGOSNg%&K_|0Dr<)pa@XW z#{x2MOZIPJr?;qSAmL)vpiNp5#{LYLH=H?cGm@!7p+83FT00cJnVnIm zr&d2Mzz$ExPql>`oZ<0!Ns!#uNvmg6-jtITRB2>&TPDSJ4qEF2E#hb2IBfiEPsRTW ze&%@@Ncp|;vnL-TnlCvHKg;|RbG3+{ZLN&*vyWetQpx=6b&ya>ewNK8p=0v1YkseA z@}&9MN@21m%g?go_}OgfOiTIMTwxsFg`a(mn0wzlKbwN}(eKL7?gU=8fS=8V@H{#{ zdqz#|56920g`G`-lElwm{Od9JnOk+s#?KaknH-0oZHUfwJU=^xm1zLGMf~hNl{Y0n z`=?4HKXcy_<7X-C72N9cz1SLkj5UmmEJB+d0E9{crvR5ilES0^Y2Qp2QC9q_&7N~^XQc<`83Yd8j)wBZ? zTyharopHr4Al?z>K+F|fBR#0^WFJoJ_obaMsFyynk+U`k#>!jzAShS$dh1Z;NZ4@D zcwjA1Eu;zFT$Gu&1+doY)-`Lhmj}*0zS1aqJuH|d7?N@K1#GSduKfVdQ1;70?;~4K zJMEWWu`_dETrOnXgmdu~>I;C;a=`416$5_^?k9*dV6JaVJE@O%@tjQR#f7ixgFIT{ z%laS~bT6TFbc@G_#!hovEL8hh(B$YtfX$1m!t~KE(~8kYzr;}*la~TFVcXfuzNX6N z)uDL|zB=z9BDT)IlARRWMl@%X7N^1MYMg)+?3*hAZQkicU2@_@7~<%K7=6^f8PtPm z)E-?UnI$h%F?A>eqhYK8*7Lp)^Jex!c+t}zXw(k&Y=(*BE&m1@z4Lu2oiK0+-jM4$ zR^ZM>8>WG?&}QJ&(ZE@*3>=`b7&w(MaGJC_7&zYfzXFJrkyFXt8R|aA;q}+_`RMpjYLwGh~pF94h$! zvtdg_KIQ(|m}#{K=Laz&(@YAi!Ig{f zG43Wt$C*iYp=V+yk?6eT{qSi?DEXx89o-oHIvIu+*CH0r*TLA@>7=?ZI{)Pp@> zxE+gEFyWHU@IpRkFU)~|aEZ6P@j_>J{1;& zb)TV$I^-Zd6-Yr|;rZ;bis!5Bo6ms~>ayO&gRV8pfXtMA;|9XPx`HCX6bZ2GGX4mr z6Aqvrq`)i=--&V1o(iz@V(9FGO$w;5l6j&MsO^<2+)Yre2gy<~c$KzKbU&t;Jde=i z0SHLUv~+K$_<=_i597^uDHg1~P{Ie{$drpGN>Sqo`_%ZYl*vdDKkvImqB?vK_4(QD zq2uw({6-tUJb%uA1HbH-gQ@wR`Q=$)WG&>E%ix3{7d#%nyz2F2e)$Vkwq^XXT#YlC zUv@taJvcVMJOKW0I)qM+UvBBlQ7QxKK0P#nUvk+G7tWKh_k^s%+~L;RYEN{{ zr)Om~Zn=&9>lwY7SN&#|_tpCg^_yL#*IewsM9+8iTv^)tVl6Y*`&gB;p7O(()n2Ze zc&w7uV}WI#c|RI19+V#}`6m1N&7ar;I_K&J_20S#m*1K*)~BRzXFqC2exGQ8b9$-! zX=&?VbhLTErJ`-en^w7A4Vgi0YJ6$XLuK{4`-o?0~JpK#3i@%0~!u=k+9ol&M zEzD2Tyd#u@fsTL!*H3EH_hjuJkJjY>5~1S;Ddn>>?x()hW95AiZ(ogG; zi9e_>7a;EHYw%xEUKuzG*X5o84dLbcp&{IY3tOZ3g25)kA3s`C0Qj!}{5Ry)Xdef^ z>b~ws+$_MA8-XntJmS>qIO2}w`vU#uHOq+4j*S?)n?wCNSsU!}{~LoCtRQ|h#?S>2 zw_vk?!0}`Lc(pfy(|xH4oWH0MwqShsznw6?-p~N!@XI#7CG4v;STq5^@v3x2u5jfw ztZTx}(Ee;du=QSbU$>}T$OACA!jHaz)1iygCTp<^!B43CP2aJ+_dAwYu|f5ES^%=Io~XGdXaE4kEZ+}zpaSQ#FQQ+O8g;YoVY)^e6k1t@yyh?)%A z>7h@}?Hgk7!!F(WH~MOd#|j0|*xOBOrWVnZsk?0RpM&5FAZ1rcjFe$vl9bn@vzRz- zmn?y1nD*6V4OdymendwvAdX)E9FSg(9iy#<4=e*6Ara?bZWZ-`L4Xw*!=f#Bj<(zd zJ?|U(eK?I^!f2 zHTpKs=%mc;Q?=DbDGv*!;|lnYYnoibPTLyPL5IS_B^c# z>Oj>&T-zOT71iLC532zWXM`AWmF2Jj3QAv3vs+rSa0^5k? zN4>(zCxmIZTHUhx6YB4&vG=ESu)o_G2`|_s&rhxF3z#qZftN&juj}t-52JbTO@S8u zw%W7C&-sPGP`NZ5{~6WaV~5&EVF3Lxx{^$@Y-jXg0wt`RZ#7*poXQDdD9%xcwvwK2 z2;Jh|6-;+VR;gZ-oDd*@8%uJ4ux`D-i+D>kPIqdc!k?oGgdd?41e5O)JScLoYpB2@ z*kYpj=L^9`J47X6Cm=4!2%YjSF7R055N@9)|K~kdTH>%H;u4Mlb2hU|hk&kGn7tef zGk4x05_It`z6~?43Ax()YG$OtAvv&9+t84Vc?TR&4`*KJ`(Va-^FD3%;siN=$g5vx zIBQhFnWwzT@+`oCoR0;fK%QSdx8qq&5nGMZ z*v+Y}wYmd@aamRNI!cvs$}7gB(32btvChE7a`hpNaEUeecFj!FcKKEnpHFnvwnp2? z#Cj`SBj~KQ#p(%!sbDL|)eh{t&Xqfg^Zw0S(RnBA_%9K@6M59l244$aKv4}oosU|< z3jvP|_wl$4U?CcIt7a9ei^;2rkXDEf1Q|3$cP=Gn;(lv5DK~iMW4*`W{G?Aw)65yZ z6*Gz;v9JN?Gt{Pcgo3XRXcUn(AaI^{@o;Em?s*5P!UaahB$L}=>Te=esN7)Miv9tj z>=;=`mYiAS+f@7~sX1?j@sL%wb5rD5eaK<0x_srF-}zQdFN>^)@}zK1d!TW$=6WFV z1iMGw8B{Ve28oVd}6qh;qmxXe7C}->lGe7 z2`>VPmq!&il*}St0HCZGD8p2J-nWgWb8Iwj>>r)1T^#wh8pNxSw4w?bCfi~{SyxyE^nT@L95N%yzYo^Z}DXS ziGMr{smY3{w0i$KNGOv6#$%2=<0J3H^bJJO*~}sr0q*Z=rlmy=g!n{8m%jrm@S?ZF z^*amUg;(dfwY13dmVJ;|v+9^?@rz;o29t zN}ql>G^beueD=+K8w3iDmTf$ZmnzqL{OAg|hY5G?$lZ%c8fkZX`|1$2+mN`-}r zQUH1)bzoLG1VYB5j$VrufWq72=>wd}yBIB@4ELcboyM6U2p8`)(IPwMm@xZ`GOlh; zh&?>Rp8#et(vvr{e#Y2SAXV+KJ=%2;qDo|riZu~)&Y%Vh|0OFbiMv%AaT2V)Yg<%3 z9@InXweCxUY!T6G$w{lei5N2E(Qw=`Z*@g2rDy;uY!C`WOB~vVWbyOvu&1WVdiy|d z_956>?)%m||4C>8ODL-Hny@=^{#<whA#jXManCI?2CRDo%BloCyHLm-8 z)x~S|{?Lr()^+OuE-R6oVxAUUl4V?YnsiX_14PLRxy}rYy2KIcepSfR)%m{fz3Jz` z0pKlr4x_+L77|03vfXON*EHjLZC$rvxNK|Hx`R+qy=4zEE3Pwo1kb$9&TpOXNewSZ z4gPBMUwHsv8g~0X@Zf9-4+6>(ex_KzGX~C|zrsZkD9-B( zgg$sIUu%tD{sXXqZK&^Xv6U`LvnorLfzK@CCDb8o10NC>n*Iz`OGlLRE`}MvDxXJ{ z!2bbc5HI{4PKa5U21ry+0)Mn-jnafKRxLQ#?H zrj&^F{{-6q5!)wx&^>pwdv4WExPYmm&oJU_SLDml+w0>SNj&&L&@)=drI@wX>Xx9V zbm|29vkrK+-~{qN$NcCDLG5y`7vNITCXc0K z55!FkJ>di(nF^SIvVWj%(EwCpPp$9LW7iiF2FG}2XX^0K6}r9i9|g3u>TaXmz7L9r z&;vZLhO|UI{WnPR4bPZyC6@gl#>nSrHBiRl7yw!-M<;;*a7*w4Ns%}3*nxj*A3)W1 z_4gHK6xS%B^A_sRC%NJBb8gcnZFH{oZSg+zIUs@`of|^gxA-PyOdqdL%E(*2?oi$a z=OH%1)&%p|SOvGebkqxRIf=*FfV=HNp7FTL232qy%mb7Nz)?`4i@85QAR&9N1VD^U z_C+ml8k^uZ3cVuds|J`5K6%TQ42g*>_^4UTjoVm`Wd2ttq%;= zM;;E{(5wxWfHRE%9b5mZmCF(S0w1RSX^ty=5sDH%-o@?USG4zUDN?AxjXWG#gpOc$ zj|h~pF@eJF88v@Pr-0$l#zik-A%1A1R;x5l|0zbl;c%|NTmN;B*wvVVaEhOHwd45?LVi+fZ_rRkv`8%s9DZ8=|qp zWV+Oc*k_8Q9?*n(p-BX9hkKc{Ppx$iwAMY)TC*W~`(ol5V!9VV)C2t|B8G1XV=5rL z2sX+Qdo#1`yq!ao=6XnT`F#d7`R_$D!7n|&Ez_^W_KmgLI@IE#4zFQyZvxiSCE$)J z`yD8~deFLos!#mi5~5j`D4De;Ku} z-!!pd{Nc!I_K&cj`V2QJ^tdjTT;sSvqYe%3mb>s&Y6KBErK9Dczgi*3}%E-KHg`1XDEvoJH`y3NWRrO#;83!0q!yZLjA@PCxA zZsyNIqX04VQG+uA)fg|5n5xlu5f%kI1!p1ZEJA9B#(TWwU!waWGzWMhtrw_YvCK3x z)W1=YT{(4hEQifIM6ZjtT;q6`_cfJVP+|G{xsSpw`~=mHLdf88{!M&KM`NMP0=$mn zD-_F+tH6J+D34?>n9pyXf#NsG5qU9YUq(y1`lJ8U-J+!j4)6r-1l(ZYaBUzw0aHbL zyg)pJ+Jp2bJQ(YkvhU2i`OHiFK!ruZso=CiRcOO)bY(XXU>x7!G-RYMD&2(gf_X69 zDn0xq*oto1i=FkG4~M2VYxfEJ`N(8Hcc+2 z{mWTNu6SZicz{)qNTcR{He?JKMl4@cezXz;eiBHt()CUid`SAV1nLM9puMomu^Xt!3eu5|Kn(Q#@`ps}iNs?EkUy z-?H7-`4;3q)jn7>dQ$YV@tdR5OFakho#^EuI|yvh{Ql|XD;$^q-=>!ej_&yM(*K3; ziC(hI_Krg@>;BkcdinQ9Hk6B-pO(^#|3g*7_dqXyF{^4Rz2wN~}K*IUKsBSsRBnj5%kJ-PgR)jDOI!ha8SXL|=mf)f*>5BIc6v z_fem5FjgP>Lq;P0_)^>pe;j}O>0iOVgKD7Gv*s${p(LGhA%uSp?bbHKQFzgp5oK9} zOsNAeoj5#V1yp`7+?@2Wi1!l(J)&d6ynt%uU`0me$Lwnc{d}k)>b_9%!_7SiT$p`p zD^&*YVwexI0l}7+t;XzES+Bb!VjtgairC}E*Xi@P@r8MRMy&fWe?>ddVBkO+=j=0{ z8VsRkT+)c{n2UC5K>;evfWTQ$OVsWTsNP&8rUoPWSZuY8YYndPwv5uMrSE2=xgL6J zu1h=hX^l?q2y?DQi;+8Eiw&B}dEKo+_;#9(?*jSW%I`M#)(1_~M^0mwG1IhyY2HNz ze($0&{)$2VijjCL2;eJFF(^vhYdW5=R!H+nu61jE6#erRpXy6!F-JogA&cPMT@-D)Vq#Jk( z)J5B?kPg%h7PY7@ed1;~OA9<@@^DYX!#YHb=^4I5t3_h==P-2d!LZnP$MY<+R{1kR z_j#z6WpnhqjhB85)7TnmDSM;sKE{w!j#hwXj=2s2>RAa~VY4wv46UxfJqo};`V>5n zfL8B7Qru8fzWC8-b>>h|;5l@YbzXn&p=LEnRzr1>trm*Q_%J-7(`=Ml4gC5624~W` z@zk^w1GE^yZeuo*k3Gm5`w*9*vmD~&oTCjfw%*}aJ1+GxE^P%^PPW!gzoAi|gRPxy z^dT;7s0%B=4>YFV)#&^L$E>ivQFSXYaI8?)0S#>XAGppzA3d$E&_Y0+wTuOMjrLcJ z1zn8})D14-Oje8y)aA>Z^5|0m4@l`_a92k<)|j<)`O4HX(|Itb0J{g&!iHMmG5u$P zY$~qEAoz1(Iq5gLHBSd~2DvGsZ?1EzG5lTe^LcGI9FjK*bZ+3obAENm~+i#+_xA2YOx?eOhFo^ft0F=*M& zy8bfzc0p!V3;zC|@;G5{h+gQ3GWdwa;7^+r&)~0_8qeS_#`lRZcra#gckuW^Vend$ z+I3;|$7JsC*J_^J(C~j*#-uSKhg=r%EB@v1-=e4E9xFna{P|=3kutn$yKoY18d#z} zvqa8-W>CU26P_OGJ@A%gg=X&nM`f+%FPY6FdXSYC?tnEVo=IyXi1;fyfGV9^+w_;~ zDc0UC+=A{)E0giTqxkJh?o|t%Wt@W}LV9}oxW?enOy|CW45z=R@dS=p8ZJoy-8er8 z-hiY58O}f_W9NUMDV5{0n9uP-47nuxP_r?G1%8Y_NLXVgWFmneiqT^td@je4gdYe# zxcQ}J@cBD%0rVXQpQVr3;PdUCWcb|QgyHUxghcqP1=IvSEAT@2oZboW8Nvbu_yhR7 zY$h}!e*LlG(;k}~P_}sD|!(A>3iSYS57>dB>MZ6F` zU!Mi|bY+3_@dxmE*i6`ngk!^JPxcQ6pYAP%&pkg&hR*{J+u-x|u4MT5_F=d$NJ1if z9syPfe9G}c_`I71__$dh9e)6ysb<1LBpe$)>v8tu2g0A+mcr-8yOQBEUbn&L>2H$Z za~4>LQ6>qA@VN(ACGhzPUI?GRoeB6Dpef^DB&-@^n3?bx5{?a@*UtXI;DdA9E#p6z zPDzH(HNUjMXZFry_#6O<7j#4m^pBRor|nOZ;d8cTgU_U|li{-!Bw`GdghcpU3ak?NT!0tC=k7BApO;wRk0h)b z!(%3lLc+1(GabjIejxh7odzv~&(5DD!{^|98+`hGl?zP9zyVPk=-W zmn0;@XD6U0@cA4s1j8jA0G}x=@M97d^dB=J6A8zL&*gG<=LdmLc1z*&cbK?|@L7t` zSyBD)+i)^`?gxn&J0u|yK5GFrfzJxO5I(232YiOGKmiF0`j45=*zS1nX|Im|{}B3f zPD|nQ2<+oT_&ix=gU?4sGJI|U?HJ1?ArU@*|I&odi+CY?zQ&$kqbm!XPl|*7VF4THm}DZaVcr!72~a7yai|3|9y3T?l{vdqoFfQ z?ULo1wvN?d?1da<+^%j;z%{3rVe`djta_JsQ6-%PXdEN(rPWWrkz_6R3QUN@Q$3WEnvV7Ucvge^1(w$KFQGt4mdcUMLpgB|u9>M>0$j{{pO zX8~|df zi}Cvf%~U~&fZG@$r%J%zz5p`3kFIzZ)!?>M_+)_zvs#zvgWY`wd%VF;DBxX`)n`(J z_Ces1@ZZ>JOz&V!ulJ!{QZ9>8KNoEvUQB90q`0ywZ`m72(FbMr8I{toXI8NorB zPHZ~Dm7l?UmlM@vv-|6MYwbXD5$6;4XR-)(C6&0GXH^EbyKv14BJ=9)aKol)FaH59 zE7`Xc4XWgJ7zcLX$G}FtRr<6>YyQ3Eb*Rkvm=la9^Ig~`(GPDi$B?z32>1l&S94J> zpqupK7TEC;#q>VBScL;MO9;mBRQ@Ji#+9t$Q(|8}+&x?4UDV(gBri`xO5_cNXx?&G zX^cqGO@9|-H5v6_H=q*!D>og~7}s22 ztYKTTY}|_tY*c|}<#uuSFH$r<3^abT+jp~P=Jno1X~AkI-8?v30?!*UqasZ&Ah1pi zF7FD0fxAiu9#5TT!i8f}CoX7)M+;H8gd`UQAB@diWK~p5jrkV_8Ry36WvmDF82uz6 zo(*?4F8Vy(5%vRU7<4DR6B<8m18Dq#WrQAUj5IUh1|(R}I1W5+Q&%A3A3K>87j027zSCMfOQGnnL zbQP#ZmY5KF&*&uu?5rr;c=ZcZTiXhuhQz93lYxKZ%XMzuVW}Gt&)59bDd94OwEp?#7WnFnSDCABmv`SEbQUeAWaR zEuJ7d&6ubXR+tH= zBf$pksQkqGgu2ld7rM#iZrHbri{5YxIqq>HeUiob2lnVA9o1a}STgG=LMA-E*d>}XD1xZN6c*oyc0lwaRER(fV;*g_=AKYX2Jp_MAx740h~G~B?vg1dT<~BPE>2- z%I#Bf@ItgU;8iP5&>n$Io6t|1sNu>A(}0rDa6{*Mg+K#O4~09Pq733GbrXFE9Rr}j z8d~93T)i&bg+)7CWl>Q4Y8yadSF-$<8>e$vC5Z_D#GSyO@LdX1#^IfScs3n?*apfo zHk15njO)yV`A9ISFv{O?>RV~9ap>~&D5A^$1az4%_b~w|`yGCoGDH{sh7z!!BHWnZ z>+hOzI`2mB?^Y54G9B79Vcc8Q6$ zk(f!F@Ut0u#$4VlgDdX0c^}z?=@xeEEn9_GuEf#6JH<5cybme09t*xMY}D%#y-x_R zpc?>*_<{R4S9Ca$$SkXHrz%F?#xt?Ps59G>3E3LbEJAHluk4rje{m9Gx=9(GxH zAHqezFc%PVLe(>C$vm8zZJvt@?{n7qZ>}pO0+v=lOI^yB$fL@@R0hCr_!E`Yz-8<( zq;XiJvD~)!{0N~^&9F>D**`1U{imW{wlaAE=kmiyRrEX&6kPOev}@+?Oq;ZBrU%`_ zu*2<8Vu5MjMs^x^;8aAcYV-H-htSDs4$kpJmYa6t*~a`WnD;_Q`jp1taFBi>mgTNC z(m>V5-6};+k4M=;jxh%Kpc&x-YVL>d3v<7~fw^A|8Zc@}2sOrKW23~ z`_5*}0k_TE?k!7aoC4@yF!Qf7SK>+Aq;ZO*Vh#m((CmGLd#M4V>tN&U#w)x8+1_On zbMFM}06t7@Rf6LYP=LG-tw3P{1{r`5=ks7ecYUUqM8B=-Gcfl~nEU6h^6mFNLd%ZP zx&OiXhI?@AL_3(b2bUOZ@m0@B)2ex5@zLc>q=s1Q^ljCq0Wst-QH@iZ!L!_bFos}T zjDf%4z>9Uim-J!zFxmIR^WXDOj@Ve0v&#P)wq7wR_x!p+f|bkANNM;qWQg0p-W`(G zjrjujyW%w$DzVFWxRy~6b$o&ry82x>+`#Sl+0G5l%@{CXGR-x!Jw_Pj)d;rw3LRRQ zA+~8D0*Ck^rqdlVa<5p9GRD@#{{uGsze(YLQ!qLlt#7We4dg{07X8tzG&kInsB5+m z=6M>-6y_^D8?PoRpJTkW38+kvwoWf;v@q6xjnNA{-FPNdV&FVu{`)9|!O96v;B7I! ziO}zSd$8F!2qZTCLrktQZZ#ABgan1q$Fx`9!30q3i@n+z9NARam3@L(IppCn;|$_1 zW}X>#1}+Taz8e+5;6s8*7G>J2Hlp)!wrEdd5c~WONr-3Kos8dahD-3*7|XBm!a;s; z7=t{61v1ni?>7^wkU)NI?tkEY5;){v>x9tcRPM%9;?m*rUt*AY|7naN!SQNU#!I+6 zq->ksN}cbOy!-l}p5ZMkW7blF#$OS#upIRMU<)RMWf4IOaXzr536&$eN z;8W35&jTB4TRUxuN*A4>AW)!wxv#p|9)P*L+zZvKX zI!0oK*r#o7#tuBJpI$FG$4r)Pn_jwu^6{kf2fwSms8!MWGDAJ=^p`97d@EOE^oNa;9> za*1cSa7(sWwNPJjP@W~|w1LPfaYf#V%5&i);U<(a`JE|G2~xtcJL|KF)q#YYD#X*c zQQ`Q_x9a?;^#jXW^I;&(sw1pRDFp8Ggn2X`EWOBb>kh%U@!;AN^XBhp?A-vlu?ccx zJSLWMf|z2GFbtfgvG*hXrj zVs*y5iDI>v@eT0S7%vI&t23RAf$zZ~;KF|PCKMt;;ZF!w?&mNX;`g&}x)S@@v8B5%{c-FL zw>GoOX5QL@+1S5c3Zmw}*dB9pCwqnZ9SPAr?FH_aS;;hWC;R=E10M_gv7PKw2B7Eh zJK0NQC%bHD$2Rs-v(Acw+2$U0Yh5O~U)?PmU6-NTKs;meUJC(0A7jUa?sJPC$^G1i zdZ95K@h*_TV52%ld9)$bMYrkOMk5fZQ)4o>9|I4J4H$$FLZf+RfcvtgS7`J?_N#BM zv38HuFB3hLzsugg3Fx2MId+X5ObzdrcFx*qe0f|4W!t#bz4>U9-McKQd+K~P&-&$n z-LsyTy7xjD?opAe^XaVIBp~NNw|+v7$&f|wI+l+ zqPUt7;7RCu>^H@J>L#K-wpFuN=0@sUuTbRkP%WC{aVDHsRk>8(tKgC&B8IO-MyU{x zb4l~yEF+*cTV5)xf+_lo>ZeED|4e||4!B%%T|9s<#`lRh?2o#eiHKNbhbNAmEl zcw)H?LYVQu^b$_*Ra4R(V8;W*(vxL;|Hbi3j0=Hg#5ovmap!?iEmoa!>FLC1CNE$O zvRZs3SlKG_<^r{@_RlEO{SY&2S$i&Qpmt#c^*Hcbqec=ert886Y8N(8|E(VK`57?Q z7w|&H`qfTw+>2P?Jp4g|+<%UQ4M+gI!oOb^cb+Qz*Y)w=Z*GYHUcE8?d)ubCZ{cq; z32Hy~Cl-i}=MgrU8t?2+9R441zxsB8Jh-rZzq%j$(UbP8t9|O*QI*=Zp15BfacFjc zhZ0xd4c?CRNl(lf4wf9ZHREjHr(%qp1i9hRxbR2#&2pLFwIT*`YtQ=Us{p|wNr-ou zcQUT21IRT%wkKW)^83FA$QQD}EBJ$iEHmMDB!pMT!z;XPa~%A_k@w@ij}Fff2&Qbo zv*c*-^p-KI$;rW#0j0A0IZB(GXXgQ`SaMv(5A1cBtX-}Nn0wg4Ea&I?HP3Z8=J-_p zH~1m>Pb){XrS1NP_^EibU(!_IxB@QMLh;D6#)8{yv$CNkaaOK2ZdwT-FO>v~5Y$P6 zbBz}`SB2ZdW0i{bTYPeEK8j#&vRJGG{$OrqnF-60Kn57r*3!+_%zq zah*;M{fIoFvCF6scfPB1_AZD2Oy#H?21jN8%s&0IX53Mc73|NAB#nAT`hZ5n{>;&8 z{AU~Kx2HSGDg*8Gq4fKo7F~A4dRr5ZDso+f{)gc>aA+6I7)#RVi7UH!&~7dIu~A-hW}N zM!Ip86yzN2iJs;-&B#HHa0eQPf09ZJFN-WBn4|xcy^f0>gvrU!(1y0w_+wtYnAy-f zUd+_sJ4wv&^dv+%j#5&ZkHk4j9HPWg%7TEkNhvBHs98G+>hmm62eM47AZAVMyhdOP z(Mn%kltT=zO2oyU!jFwhB_SRcI~iHP(8!wrnHMjF;Mg#*@Yi7AMhJh9un+KK!nH_< zf<|6q^pEx2OkBu^B}n+JM`Jm6p@vw?gB!KL5^eZf+UQE>mVNnHVmv)YlNpW$dVUFG zt&ZNVAyiRdiw5q>3vy5cmt}b$PG$xL6ei9-_!}C*BE%s|*0>K9-qE`m8ZFnF;40Ar3$>`1G~bX9@mQ zvZmNR1`4dUN_?jj>#ZFBG1gn(Od9+apkAB=v8}guez~KEXb1(j2@LSA;93&`ygXQQRM_F$LrIQS;@kh;IYrmLnX_W(~OofZ) zJ1(tWwG5+U_ON3UXAgOmIIw+xlVD*aC;n%w7C&8JS&M2wz*m+Gl?J17=Ju|3d;(&UFS7Y)^^ZE~`k#!eA3X*!^FDPy%vley}^gf~#AW4ps&0wf#P=-jKn zSqhrxZF#+cPo)nA;Di1(e!(7JKljiUx}k>XXVw*N#E*2{@>)Tt*1#C=V}%=0Gmc<` zb6|TZJ*LegaAGU7Y<&vydKrc%T4yBEICUp;Z0h+LK$Z=4NAz;iSHY}u2I};dEksYy zUTI0T!+*VZS$;o8XKbh#>epM<76cd}z`0jesNu*-n9_?>C+t;6=YRM{a({NbGPs2AhH((PxMk6gp%8YMDVQ|W9xYoG*jYQ(U!1y}| z(pZUH)S$K-H(@GBykkEV4a)JRXiyq6H7*ejfNfU$tyRpxX~xG(!2v4iro9uHz!u-d z4{;ilZN4#UYRUmF!zJqr;E9;^xkW97dxcMJ&vhzQ}KL3kT%s=?Lu@bq=`Nvwf z%>O59{(&)I-WoGCE=oE7z>4tOGXKcL`Nt1OoBxlj`TuBAgW&>uJv!B29i{q5qZ7YI zKNIu51mBL2uo;{*Ke}EYy@n)T3B>vc@`J<<$`}2yzIg=B0cEA6`bC$ep7_oqQ&0T$ zv8lmsS4Hym%68QY>xJZpS|qsnK1}FWFk7Ux@08KFIk1zU z1}cQBIO+Ey6ZJp*U`@J>-c9~~wk#PaN^co|-?;5=cWkoNOgG@-Q){M$xAST}SP2b? zJ)MC?1-JrI&h(l~QqOd+n^VuUjlV-BkMj2hi@!I-^Y@Jjlg%AS$x}R7bs{#!xWvzr z@5396CD(GQ8z9Y6O?8aFH`u3oBlx@bCfihx_-FD|uLU_8;Tx@~zIVTx>f&uO)j2>8 zO!Z%wDN{;Q{kQY?BR~Mf-^-8*Q~fo5u%=q{s^s~;SYjxg$b6rKJjV&z)GYnOZmH*c zAV8JMePNq#<-hnoV zhDoKrjRL6t*XH}DsuM?>?=9Dy#mXPW3IXT)ckj!5 z=VOjB-z%A^my}MN@6O1f=KBR?!hE03B6bg0jJ_vQ>mIFa+U;6ly*7G{ajk@K5L)+^ zl|xP?X>voNlKgojTPj>@NODtohXfZ64_{FsLX*eTcWnJIm3o^duNN*y$oiv+^-nHJ zjSPbUp;X9V)8}9;EC0)h(f{Qs`Tbbcj~G5+bI(dze*Ad9 zbvImWlR|7q1olGK#dZ)BxHhshb|;jyqicN{4ch};-JEhRMjF5WLHX4SxD5hJ6!vP<2l8!Q1YuCi#9Q%M}*Mv3Po6!=7d!QjUT=P6P4(^M83HPMoFfz8= zVcSG2Z8bN?a{j&XODj{lB+Y;7EfQ)`#o8Jn`YP-Movqnh8mRU}^jBTyI)1E3(tZS6 z8Y@I)a{sZ_ZTPEY#Xh_jokY4dpV%L9nABiyj3{gmSb!4mei7i3jSA(3mRxjF8|!w7Lc^AtjBV?gOK`=kXrNfnoPjGM?hO@w1DyevCDK_r zuvH0$PtSnS1^g0=?`Nwc*J?*}8+B9M3z#m^BNLWAJq4*n#hJEcPeg;Olep%Hd6r`R zQ`KzCPi1YF@fmte6!`MpgU!*EyAbdpexc%0<5aL_!!HSz2VQNb$TDvIGx)Q)HZ=n8 zkv|#R{tt3*#vgF&>-f`eY|vY*u1uH(rY*1hIOMlzmF4dz8?3r zw=mxPwb@XCPb}X2vV?f^Y|ZK%`i=eCM~gS#4x6#8c=KHtsS)w!duWDe(|vQj%M6Lt zc>Zs2gE1n`zlC}O+@wT_Xp0ABsH6%Bw!7U5wcELNU}vlvnz?Q8v*MkYr!RbaaRPZ@ zZtuLk7UNU2<~C;VASEKt*M+8d{MUd)&Y0IMedvc;HA8xVX_ek`B>@+vR{Z)#uqRS&>d#i7_Aa%E)9#4;)8gmmL7{~B^3UFYOL-4m2V$_!E1~xI z`0{KDKUyFkJvOfz3}%Cd1IO9s3bZhEEOi*qaN`8VRLe1S#F+YVKpmde0i*KT&Lp8kCS-@!7@S|{@L)&I{(71 zNqqUUB|?h?&0Zi&u29;XSYU%rA$SMg0Nt|mdW>-zWHEPEBBD#lnWC_}jfrZRHece& z<39cL{6<@ALO;+)j8mSXo|Q75yfCGf#gl*M_fkuX4EDvhr6P{&MsS%w zg3s?HLQDuxBB|Q{olHFXEL$Z~9u$10YPwuP*=NQ0{SxF=UBD)LoJ*&+W_c*oCHAN0ko@P$MzR(1fRL-vi^rNo~S(0n)D9-9hOY#SUa#IWpwiXZ|eT&z`R3bK*ghc zHKCEG&%7#>>F{lsk)~}3jXY=GK?io%%T@%L+#h$~C1zt-{It|Te7Setlivm!Lmb*yW(a8e{ zl`@|kZf-|5*4B3T9VB>xFx3;28JGBlfyQMFv$jp(= zYPyo*wP%|hb5Br_Ey>B9Kmr*l)KZL7&;e*=+>Udka<5!eTd?|{$?5MFEr;^fS#fCL zc__v!%RT5LahVKGZLgN`!?VrOz{}jc>VVg>SL{4u7YD4$97u>ut56qkdz;T=c z=>i}O;beb<{jeq+t%t>r+nM}$4Fg)F2jO;5@Kp1gi7CsWv2$gGJ)VY&{}{WV4VA{^ zDFb+8k3S?;A>x%28{wVGJLucsozL@gDrWgK--;O$L4LRKYt$y8G}1>fZh7DWY#`uu zTdn$01qzIbNln=p6?*F;nOLqPPX$B(2RAwwd5<<$wQp1LUm&Qp-FYhvpH;DQQ{<0` z(4tl2c;J)171QTOHY~94uB5Tn)!Pwy5}=w%dE5)zwg@BZ{=jU^MGqXp{aGW<-(Cx- zVWsVxJ|#^vrT1Hnjkr1s$*>(<5MjY@o&F2ZEfrW2ex$qSgFmFJkx2Y_Yiqc7QTvLF z$d0H!iT;(Zocq^ke7=PKEsw!>6#6p@FoyOXjdw5ckF9w3IA5F?@BUJ`;LC+F67Sym z9{UO<;*zKui@sJpgua?k?-7H#x=%*p-7oT1xFa7bel79YV)5>+{!RfryE-D3s5lz! zo`AJb@ylWM3yTT0y*vi|%Qzqfd_f~EjG=rMvrmQV&3!TQn#F(e}*G*1_1wz*)9M{30jcf;ARcuIrLrU6&vkr_lHomOv5j)SoTIjA%CpU!|3IY zNLyR3h_NA4{`SCFQ^&g(b;OEyFCWK_ci&C6E3`D;{oj5Cm2@DE=YGd{_oe^mkoqX zK${P(;^_uAiWhVj!IdItVE2S7B10L(yQd5ZuLH)I^4li=u-+Hltcx2Pr7dy4d5i|E z7E)>*EjB${F#Fe)qK7DH_LKdkoeCkVfyF!|>yE(FqzJGvdXf0{D{woGRTQR|2}RgI zN{}mYr$(m!TKt;v?Pr-0Ul`y1=0@N9-ud@qbZ=7>q7qHsrb7 z_wMv_CEWbu=sqHj7E@qbbzAn9@5>q2<<%kPu(xa;v*FrL#vj#oId?k0&1-^z?=1^4 zq0-NbX@&afd+`uZTf#R{KLHbb095>}ut5e^Mfz8zl71P|N`9O8HC@@ljOeP?aIpe%@GrAyFiLJ<iG4SfA{$HpUh6#9x%s~ zhb`xLN-TB3HvlyJ3HXDZx!@iCnYDmvhH!c+#b6XUs21}|ehiJ1D9-rx#9pe*PofgR z0h8s&SBNt7zoc&ew&eEB{PF$dc1yEbneSu8ugBm-5~W++64Z=#>X`j`5119rsa}qm z|CyuakMDnRET4B7CxW0$H6b_3nuom$F?k{J>VNxS+*HswAb`n4KpDzjf!Lwku*a)s zw0bA+gXRT{nJ~G0YrXS-1qf-Y^7ix@jr+b4uYUEs8q$sr@#=q#B!rxwag|<}fh(9X z<+Qh4SnMQy8&{*7m~yNWMFSThTD{tzGTIaQr?U#d?h&tEbp5w*KVvAnhx34MtM?&Z zS`^B@+*!jQ^*=^RURB8D%fsEtY{qN?2xdIS81>j4{{g5}cQ~t^wIVa9BbMVLtBw9L z;ne653K=^eP%-M2;)}k_beQ&2sPGFYcf8{c{AqLpyN@vuSa%qZ_M;oD;ZfGqae#$u zq{@+uuwA1y5w#6ZKWb*)G7tE_3OKCKUV}3ph5A^DS$`A6rT5XD;NPsdcLi3xKE(&R zAw$$whtHt|f_#O%LGLSC*&(+SY4mxGJseqr&U!47JJcpbG9h5Vkoc2Pg6&-T@HIH7 z5%wq_5hB+A5pzOv?DLv!_1f~!Q)v@DI+idDS&B>8i($RA<- zQoRi$XNw#HMhbI4xO%@}l86ZV@UFCYEcF}m{P=kF%T6R-{i$5Iw8g8pxAX2LHeOCT zljd3)ul^Olm?pf6S1-!?NyMu^>mj9+M}F9N^>)X|=I++8F+s_Fj&x(Z`b{(M!pZ;3gq*wc+Po~`8csm398v2- zMU)G_!hk7+&UAx$q4ozV$p7! zy8!Co$4~%9`6eP<{YIebDCfcSS-IoDIO?b`PKhdoP+`%yv2dmsd$xre`QlD(CJHx|GdKWH5b`Kka zVdzJp?R?dCHoBP=Zi9?$MLG!Ml9`a-=wxOJldHt|Su|e#g>Wo}<}_<1a4&iv{eY}T zN@p2w1c6@|V|WFAT!!k&Tc#b3yia_$><4%eF@rte<_Q%`$oi|u`?eZiK?I1mf&3y| z;lYtN2?#96ejZd)E{uvZd*Owc3WjiprBD-TNbo02YpflJy|uOiY(2IgrOrY7Jmxo5 zsmfm|YXEdRLjQn!?KHBrr~HSSom&7gnoWwE6ROZ?j3++B*pCv9+q%!f)EHDj6q1Tu zxz1`LsWFx{q|Ub-0u?KPsn4OH_OG0^&n4;#jh6t9Y22kR7f2#kQ0bsFAdUP z-UE}`3*^>Y&*1$myYlMcCP~wpaQ|w9Z+G#9AwRCdSQUc}(7_Mzj5h~ok6y1%F1uFQ z1k69X3X3rP6;x4j&|a?SfvMZm{kh#!wD+YMe%)?)q-d`|wI`#?!gO?Hi$|HC#r6pB zvAN-!a_gcjwcAWAEVY#t<@osZ%hnsCK2cMg9FLpx8}s;^{eir*0I5W#O{P6zRwdZd zg8T>g}yVJ|kj}>Mfhh93U_F6( zJGHMe{CjA-HWG~TBU)-v!|)+f>cAm~S%~c(weEYO8i z0nulsH_bHlnQX52bs*3YxET6OTB1G^y~N10UnQFTvGkc}0qKQ0f5O1=lRO#%#Jh4c zp%oeB4?qWNqmOhW_Bz!hhlUW`V>~cxu6N;5zc-=OI((~eXS%>rUyWSuphnM_;@ik zV7yXn|iLdc6{Tw=l~b$zOsGsU7OGh+whi8 zX-4>>|55hp00i)Q;1pzWHedP3FYoG)-Yk0HNYxi<(k+$0H}cC(3)sII&41;x++R&| zHrukbW46Bd6B{1@#&ur94}js_fx+W+c#ZK`A1IF!Qp25}HAFo2vwn{>sDcMdF$8PB z9H7CmL(!r0+!&6!mJ+ytLO2U8X|Tt)pl*)+*%E9$oxsv{tY_YAf+F$}T3ZAPAvA<@)U3r_FF_oAEgW%GSV;XR%g< z+h*aa&x0;JD|ZMko^oovg9RS8HYDc!*ZV|Pnf3wtg0e%(E44Ijq}$(4PeXU}GxpCQ zuNtDceuwdCPF&<>t+%0f;d6;H4`$UBU|Sl7jqA$8U%<98t~&#u1F-fJgb!vKbLZe$ zV;G!o%;t9dtZL0QUYLowzfGmro9P+P9c;Gvkz5bj9`m70bzZP- z4Qat8=}) zt-f;51!T=tUtQH#cRsv}Jn0p~(t6`o;$Tyoz%KR?Qchb9jxrGG#y&r|OTLg-o#W+? z!AGji%|dng(sXTphNCvW9sajRMxm~{eARqD_DrMQ{2cYjRS(sCzBFH(-vb4E;{WCN ze+B;c!v8Dr|0?{y8vhF%xQab|2sk^sXQiAFfDy_2jq99E05qJ3zpd186ANx2zyWayzOe(Q20U%pAL2PyeEk`!esjZFtQqD0LuJ$3k68KybSG@_J(Ps7UKJ`7oXUk#x1e@^L+fEJO=kYBl`ZUe}VvxnXdtU-Y zD*r^Lniu_gH$=uMyr7Nu@n~Bo+#3Ep>*WOe+HByoxCZ_^=yec=6{Xj={;TM<`}-$F zuelj&I-hzn9%16~9r(!l6X+@Kg6h=ty27if@0U=00=|P@Q`Fxgdi}B4!nP;k1`ywW z(ChKxlagLtX7#h;t54{EOW>1|USDjh;B!tw^$GR23_dC8wZLrQ&Bx>5lR&TM+;cQ~ zCEQZcYvgp3UO(dtIR5y!24eI&XUU~t3QE4dLAf+p8_-^4?quzT%y>y#Hy~>%FR>l|y2Ld27`A(<_gv~`fQ_?IkTd*e4B{$GRteei#T2;46r+M=3@ zdP&09+X45OU`!={pNJEXrhFOYQM0k;f8wS)0iS@ctpAwwd`(>SrhNHN8&&^ zj-Jch3ZV}FI&QiX@Co$H`cIsmFEFcrI-&Z6`a#br>TeM}*C5Ib#Y3O?7Lw@s`0zd^*LqkU-C~Cm)TT3Aa@AJe2j4o(Gr> zEX6Ugn9vb=rgOnt7KCXItMlanRWa)$%CmBad16e3X_#A{WYA8QzA;TLs?6$lJqsyH(y6;O(dK_88u# z$s4zd`sM8@yp_n?vv`{&Z!h3YS}c7DZ>93v%Xll3w^#92A#V%u_OQGy!P^3Pdkb%m z$y+7f9><#~DzL8T6{yE=#`ZgHR>n&o5k7AG&e>6q4D~occYvv+)WHiNmGxGSzUq;$ z9s|{5h=AC>M=_mrKNn}UtMXLdcj*o z^zGs3+k)uZW6`(Aqi;_|-=2-Wy%2qSDf;$u^zGH?+rsGElIYu8(YH$Tt+sSI-{>cK zAM)6upd|iPpTysd$DL{7JC@=ZAjSD2&~dAY-DC26nqmNFhwStz^_z=$-El3rJ{vX+ z+eqN?#uaEpll{*wt=70?6hmAE&edk%R#)t+rX!`D)vCJG#XL{a7k#tXkE_!wd2x)mVP&NC4Ovb+W?*nCXGI&ul-e+W;0{zX_{& z{n>CQR=;0erWrpU?ak_cX|G=K`1yG8ynn0cb7ME;9m0fEZ+Lc8#%DFDDrWj{6-EuOy0$IGOAaQmmz47H7l#ykIaqFEFRA%4+enm;9neTj zKAt{2nI9ijK2{F1^W%#rq~^z`p~^UZypAn`gI$jflV$Cmn8c6&FdqE)E(+XQWBsiu z`EjbT3_g$mu$1!gX0!d3Wj25%$;TA)VKgOFpHe>VK_@u8pV_N#`FvnW&^8~lhbGU* zQRQO~WRA(lCvQnT9}gf`+0kqPp#E$TA1QsCAa1HfHU$j4Okv3P$1 zz*5S`DvlTc^Pzo9TqVlKr^g*bKC)?JDB7~i#|bErLO$lAF-x8e!MoLEhxWzS z*Cus+8_}#)UlZP~`s6;bP~m>_+PxfH(`KNX&=_U45NN*&0mWj=gQl+VpY2Llx#Sl% zSg=kV1?!xw0++W1D_yK8e9@+;!maKvQ+NDPwYAzqLr&zvmW+5M@nN%}<-jJ^pI}d4 zGBzc=a}gFo^&&K+Q5|6joB_rCTGSR~z_(a@i8GcwdlMtpUdYY1)0;J1r?7tRMu{wk zKD~{z6X;VUn%_E0QDp9SMt|hKLT>EN;doJK%5o``w3jK?e`aRRwr62hunQ&qFagF@ z<=D1zwKG0Ox3bKwAGq(v_fcTy zJb1Wa+3lHe3T6lAe~`i00a0qtY3=vOtH!aGgK0B^aC*6M21s`PJdQmD8m7ZLm|Vn0HZ6?D5!DR_WCkLqgl5?J7N3 zg>hc19Mxk+bdK9s%~EG z&w=go7bE&(t4C+H)%M7+8ogyNs)A`K*qQ}vk-)s@(k~_HzI4DVJ^1fR^H;z8ie|iv z{JE{GzGz>y-Bb1NR?a3T0CGy{Cs!X9fDA`AFiiBj0B_Q)_o3c+jr_`neCB-Xz4dXf zyhG~_1==k`mgWiCF5VO;=Zk}%I=zdp4!9;))w;cl_l5rM=-S*o@7rdedhr#wmSE<) zxMOHW!|J^gzv^%w9~eSp79i*664@L~Wj{!V(fv{ z&oqi1#U)r5evE{p`xmtav@}^8bVR#C+KVjcSs8ga)@AVL_t4qRjLJkFo$m_F(z}Aa z`8N4SVsl!zR#i=Da0y*$?Uy^KZ82~eOLMU4h(E_yH~m_@W9SB#)>gyLS6_k4-x-DS z>ZMR%y)s_^mJR{hY=6#4rmBgrVZ;vQcD}3u_bdKcfqaFGr zS7?AMuSP3$s0`Wq2p6uM$km#RepnpDP1#!~$VfUkMCe$ zH6UmLZ2>@oOAOLxzM2FU_znq{q4^eA0uKl4(zQO3auk>D3)uNV3YU+`3$#9Qsj>40 zh~7-MA%rO};qhn(4JVyOxFB&NV0aZId5)61)B6S^&glF}{yn&58a~P9@@?MVR{Ltb zzo|r)vg*KRkjT&{coM;qXW%FfN965j`QR7M$TPMHr0&p5bO-dND|*sBFcH3_UeqM4 z_CvQcN6VxAu22>}Z?an7<^3PC^&M!vYjAllFda;UDUTD|xAcSGC&3rq)&I-hn}A1A zW&Ok5q=i;nJ4n=^5u!v5!f2vViJ&HQIsuYEAPcC$VMfTHL}ls5AiJGt%CeD}(Q$X? zo!4=gaUBQ26$1%rK!kv(ATFq=)ix?9LIMu?e&^h(>Z)F{pp5_Td0rk(SNFYjm$Th- z&OP^B%Hk5bis6e#XWc9io6r-|`TQQn4Zer6i~%S1@b>XF*%8eh90G7_`iInlR%oSG zdSHmP4RTT%@NV;NLtTNSsP!EO=5c%(cjkewwzN8HIiJOQp;3aLTCKC^I#=fQ@Yfpz zb@}Q~Pk5c>_<+-e&<*C9sTF%@Cr(uvffXQ*>!Z~s5T3&rez`tP8| zuB0_#_VL-@y-q@Y_Np+ghvBEPgAV^NqX;BYZk0sClw?EhOsc{DIh?xleR?I50Q(LN zy{)%wyFc|V1Whz}ddj^K*hyIwK>j#7hi=K903K7NH%VbzaU~%`O7MKp?6O%=58yT* zv7@uToHFGRRK;mQ!%%pD+&_gFU%hA!?cI1!4vF^R^1WyA zwOeMR!z5OHUI8VyBayYkg0|8b$W|~Lh>nMtPeW`7!1k zkG_ya4h{75qM1G($6kz}4@@hSXatttRsr}u261Ow-zF+^4;D&LZ$A36PK(74N$^a+6sX$KXzpcr>rLVVLLji|uzk~U$y zd-5Qt_#iGb2$8{8*oFa&dOKH^rJaL?-?Fy~Sqc|8g(MiZ!wrHt!Se*vm$Y0kB&XJLaUVsW7Y^`A`qgkKK=_9}*+N<&`tEY~tv=H~Kemq(VD zkN1zYb4-1b)Ejzai9P#m&jnInd4gTK#ty9l7j{ZW=)|aK#{Mp?tH$%oH4b??w2vft zs$H7nkY*%GH{#8tP?H!aTF2mQXdB$d$A#^|2hUW4-D&*(TwLFmjPn(g&#;Glt8OXr2ILy{8+24ZCq{iW{|(emoX1AHbX=KE=Wx3*^2K5}0T2_@6hg5gq&fUQ zR^MuS_BzkWXqX^=kDFWrDIysn=7an%>q`A|Fej0uEki!hKg*uI&oc@N(!~<)wY1f3 zm22!c&jhcSG{Wu~B=wQz;)IIml>9fTfH8xZ_^s3iLjiggjzX2#rAwsmLfde_2S?h# zU&?|@2|_m9YL0I>^btu9*NbukJ(Q1s6`DL^q!2_GfhCE6TQyjT`~7=yoy!`keq%Ie#OQ|-*f4^=AWTaDP!n$^8e%+{4-P%yc8u& zfSB)eczz(}aAH88uLMf?M#+Fe5=bZu&(Fs5n~D0J&2I|k5;?z5KE-HuD2D*G@H7Jn zdp?2lB`Zw|C>iup!iZh?zC$U+7x?>?H*eOa&f|@oui<+p&mROzQ%Me3xnYpt4(?Yp zto%q{0FwQ~b&zno+8^Sdi1#1DE8=Zh7-e!Sdx<V-hBX?i7#W< z|E7!e5w=D6>`yskF!;;EgBK;i<50p~i1UJQN*3>?G#XxqJRkQ!a40|)u32{Q?{oyj z-Up<|bX|1vaIefM*e8unnm=finpTfT}2t$8Y&r6=f z{@L}yKdaS$1cOrRpQ+Zb7xfM6!+XSUhVKtRi+1nQM-N|w`(x^HBw83p)6fXkOIW*GZqVX5aQMp&W!s72aIxz*TR1)-Rr)JIT%Mv~(3xDrxsMH1e*wn@P z0V_4Aw4*u=_jKcUs3iC~N|-*(<2mxBO0O+l8_!kRcsi*cuu=u(R&_jgU2T95l?2Pp zrZ2B_Cx_)UtfatZw|+z5?EjD)hbP&&9G z9tB2)--pmpc$6t%Xx#m!Hlp&&(T@jrN^0Xd`bKa!ooPIs;A6;y|HB6nSSA0<3b|)dRm>c)>rg}z0r@~qy z7pKYPDe`471+a~s45wZaog0VXr_D`=TrASnA;TcDeB!!psO{|)Wc8>jk>>wIMQW`) z#qZr6aPOuhJaV?$DEfe`*V`nkgHjgK(QY&!Zg;$c#)#DEsnUD!WEIO3cl!(XQ`JLi z`o6pMv|dx>S)A5uE~oXn6)LlT;%Ah&5VfH>y!8ladE3ZlDsKsvpjdXpyO4}#?efG7 zRB1bZH`8K+X0T!I-q3hE zU>=a&=>+q4W%5;?FBi8+t%`v|Ut5BWkgPq=t;k_JN! zenkX72A(`I381TM_v8`qdYgeqkSpItlQP1OGG#hI$~J;wdx;5t^LLA8RJsSv0}W~$ zDuIfoK+P|Knz-<091n~FHdt6M(hLNQ8q871@HEM!<39^VsQM1rm2-`G#0dLV*W@fjs73@O>h0$BC!aX-=X%9)^=wU z2g#a6JCH>2=ys_PqIlu|kU5JjJaMmd{-204*RdGh7^*Xf(`_)-{{kaM8L$-*SZ2|B zJo!r&A}I;e;~#J}l#w2RsENt4H06hW`T*Z4yC^%Q`r9{8(6u=CCOaui}reK;fW$|qv2T-?wECI#rHV^W@ zU}g;T5fT=>CNn&x@6fYz)py_q>V}ZSjmq96d14|95i)yH(xL$hi0p;hs`TE1 zff6BM8rf?EEuDn!h?eTEQw8KtrKh5cKu;@w0zHxNh(%8;k0m`ld1|Nh^!8>WJ+X*m zdOD1pEKESne+tvn1~WY|^CJ)i*!3rk6@&$K2(+dA=3;?><2pQh50yVy)q;r&UKTX_IKfwo@U6ir+ILQY|#t%295v_%dI;a}lfzXDdz zThQi4B3+hu2m%uQCA57mjkS5QDalg82cLoxAfmE2v0wDJ^nvECMYH4@68k;8JBJl0 z*M@h!0Zz3MYG79g|1tHqw8Q?kZ4>>ie~bJ3jy}-eS4dfWl>LS0cY$esgG}?g1|6XT z@|-Tfqj^T}h#76tW)x>WvD(+>)3#A>zhC#-f4W0>wW|1e2<^imy@oyjuUC-k_$cAE zuEYNJs`wcw`WvVAH|ps73(qeW{IvPK{H6}SkMP(H{DkKdt9@-g*M!?&(7pC|GrVvj z&6{qbuYo|vzaN6U0y+i_QqiHt6R8@T;FtFVb+u^J6~UjDO~jwE+y8#PX#dR%y5GKC z<^S;b`=6O(FVtK*T-<_U<_E34@OWdlcQwXrMxUv@R1}-|E2V?^ zf2P9s&4JyYf3iCNAsw_oV1a<|w4-QWgGYG$vEk7lbLapb78IM{8y;`$_HOMMz9l+* zktHu+E#uik$>m@J?IOr?Oq4JHl>E>m0usML#$pyM<{^J5Xjr|#PvDQ+ujyj@I~I!e ztzB;4-og0$na1B=rQ~-W#+Ehhx0uKNiIJTDMRf$cI~afL_D{teI%|I)C`w&_x&24G z2%l>n6Y%-uyzarjp^NQ5v{mL3Ir9k3Ig_?Z30A^Y;Hm zbp(BC@{b{esI?3{Ns*aj9#Pv)!l`U)_&aa2NG!X8vS>>eGUGv zbp)TO1plua1pGfwJsS9L>!|%7nf}%G>$=sx2A{a&4FM_$*@jr?!dy`yUPbZ7m(h|D<<;-;>n#kLykL*KW)|wz;+RAMNet`I^kx z87MaDv)!0)?EVrzjP!n(`rG#bVvr2>*YckN-^ou4{8pcF6z~=FrTmosw|y+|yQl9_ z^e^z+jrqrh_aww@cYxnq6dUo|jrqpzZx?8~gZ@^d*ofbhE$qG3vvs8i#!V9+gFEKm zGDMn_V7({#DBvsbTk=!-Z~8>w*Ksuc3;cFt{;}a*@=S;L%|x*gzulN`?EYGIb%@`8 zq1cGu+Mfd7JnSnHexE+=sPX&KPw9U-g-4_R!AH}-z;8F^9~<5|&vl63vr%lsZ#U)} zyT67{JH+q*pqS2+rOSMInjX&UJ&{k2sPU1ed=Ki`l7$~>$4K6)5z)tQY4OU2_+h@^ z4sGMS3?enK!`O$mv4a0hO0b0YoAiSY>XIBW9@B-lj1I>m_CNjZ zkNm~EuxU;);`6cHohX8fD1OF#w?poQy>vcaiYo;;{6#LOC?~sI zP8;cni*?8@1ft@4G|zD$%JvotE7_rBpasNN&@Oh#exL*QYfyxcx~E=h^PXmm$M$Av zJHqmn9QD!e9cMWIv+!*-T>AnX3MP=K=b^L+ku!{SU!LUF-d_b^Nr}Q&Jt=I+n;q7vbe%4nQ=*Lv@97>pe4qVY8{WRwxVn!z- zL+H$fxYUBF2t4`IsEE5TwCtglRa$a@mMAVqqos9D6D_@ruQ6%qFDRfjDVb+yrlnex z5iP0o`Ek(h*#P{hkjGz9M5kF^MAMQH+d>U*C)ffo8j58%7`WQL`*myU=vnqvFL zp@_uTh9QRVf-b_ZN5;kUO`gLcYUNq8m^LT5}G#Ygo-e|R~b zMv*A8hc)W{ApN`<=l9a|y&Zn{_atXNLedu!2J7|u2K|8L0qHGj5H;jGzjW4Em83H+ zJ=Bi*K*h_7I~S-~(Ualze79K7cZGF?NiaZ3?W9;J*9mJxUP4li6=+B>s+}f z4s8a+4SfMu61yK_urG~0$yMr^#Tlex3*WxT+Ba?=R%WDQ<#`m0{?nocz|oh+KBw+&5G3fgef$!gQn z_xtmC)b~a-?LovRMjcR^ktBjEW5k!^Y-x%#D^+jSJ5AF9V1{Y@?yUPO^@TI0xAPt5 zTb=i&BQ!`2+Wz7u=KCPRBHMLqW3s7sdARmVYVF}vJC^@|;7O5=pdd8vl(EFk7It^;{Q$$C7XQ>^%v? zEm6eNq+HcPcxYma!$dzVny&chao`{jESZ4qB}oX|54Za~%3`%& zIA(5Fkj;dMc6GA3mzxy^l7VuxvF^2DJX1J(H>+O8z~OBuBZe=!2mt>yv`YiH25&xE zt;4%Y(is+y=XJobz9&GIahP@~jVC74HfMLnw2FzdG^Wkj@2|0f>le2l;p;(h^d6D; zS3|$Rksl?M=7>`)+l-w;S%nWkNg9M6%Ud#G)B@2-RZ`yRM^IC85z{j3k<511Q99^o zj)t(f&ARxA=rkFfS`Uya_FO`|3I6Q@o?zxy?k=@TD$6(`xl)XMc|AI=R33EJJsZj6 zaF=L2-UAE>+6#U`Kk50NbZ+PK=elgj4)gEr3a?qg^O%1P_Cz=riSu6Q!CF6~)cV(R z{SNs3fe3yt!VAiew0!Gf{MtImCrcKOP?>(+;6v@o+o%|NCtQ_ojP`t@%CoysbsU@O z8hx7OK@9qgBN0sWX=L8-t?`(5;6@Ykh8Go`dGErzMxnqK7*l4GMwxE_lNx0MPCo+4q;dBtrcQCTh9!nZmbc9mB$*O-T6c(k zB1sZGz_QgkdBzeJAxg&am}bNQgnLkd7bjxSdNF5Z@CcDdrnmMj`*OxbeNrObK^$}sMXtM)`NaG1w6*`g~1JKB51Y1(OM zIxN$TOhN^dZHg3VwPAsl`CB2-X5c5?UeWXsXI*XNt51YqJ@6ax>hJNB zCIOvwiz456EBr>2`o=5thNkljH)%-7bg9X}KdH6p*5t4R49pw0{|2bdiKOr?7R_7nHyz78}V#~c~{xQnFgFh05$5M!X73fU%$#~Z&`;6`W zIc~e}G@IHDGmkELE-|&Mi8rDB7sPFMzPa5n=jiS3JZu~<4tE+9xNlJw8xH51+YPgg z-mZ_^{TX>CLeLIC`y^-|Iaerbp^sR~{MB*fPCVzAkgpFMj3-y#T5XbpVgAwO${@TOPrhCqw_UHf-7xd$?Y@1$ z*sfW=eu=W!a_}s3yJ61J+nsA_*DPP}joa?p1``~@Y@@e3fZIKG^0n~?Azz24c0s@SH;)5gNnqA}KXs|OefY=)BGGRP+puv*e8?C`cTE~Q_^5Bl|ra{aV|xO{6T@52C|P zFHXqX(vYY?cfnv|Kw7OVq;890lvA>ZJpgT9HjMJ!Qemo8nr1M{uQ7G2slJijwxCvc zx_!`fyy^C$esyO+de|9&6dgiti|PIwh!nUMdU2LPFTPaOi%IV_>%~LDdU1Ar;T83z zLp8ma{CT=+{3$hV^ydu<>z}8M7ot1apsmKxZ%K1jE#~{S3oHM6dP9phm}zSNV(6o4 z?JBi)fBd9t!&#=<_k`OXqSl^+pA70R>9?Zj&UWhl*#&yKA4Vewn+;kx;$br`c(2^q|4BgPTG|}+7 z@l$zLxuBJRVNSzvCpsbJ{77F=>hY)$`Vjfg@$qhGn~@sh`9nIS$MD`r$MhKC05}I@ zioKppNFy2{2^(*VK5eXJ1dQLZ2P1Cl^t-$7nri{5t-0S0)i{ zYR={V2iKG4lJ2f2d0V@)o}8xE?%H~yY!Pk$9P7zFw9Lo$H}}3+T~8*Q1}?n;6|tUF z(nq}Yqz~TsW!94^p?K@bBQFvwHFdC_w0%Vq5Bj^dzZorN&Fe`kUWg}VD;v4HPS=xK zQ>UG*CwEch`0I(XhS;|A^(4{MXBXBJX9p9VMV)qGJ$WQZ(ES9-y!SlGo{=lDL2udmx#NOa+9w|ZgSl} zdtD$`?AYffvXzs|YZO%x@n=%5BlNss2~^LQjGRCxdy;Wk$H}tGKFCFPU~rOm{)W`( z6OhRb`IcnYWPI3VS0zW!i8R#h<5Ch_l?Wdi0-6>6{B-{< zL;ScW19M46Ek8n{M39vKzBIaRmctkZr%|{RVvZ?3sMUBHNjc;3W45j_ZOxk;Syh2_#*N? zV)EI0o%d;(3UR3e>@OrFL$HhppOl*YuJnON_?^{~d!w>YeXH5qESInIeQNcr!fgEB zUBDXBDr1wL#)Zcugqq{SA(tZDQ;l`r4F)FoYlG|IIZwu$#T(I?=tSusb5uZpV>7b~qY z@iAF^Or;NEpzw7SOQ~gVFVS)W=v_MxmPg@ao6#1Uw@R)Y?X~e0{u$}0B<=I<$xt>S zLZ-Gso|l&AtXqx7Wn8!=?ISCHIY>f{(ovKLiHMhbYZ7f+UkL*t7wVFyJfi%Lwo9joN69Z>FNwR^TNIU7z@C(7)_r zh`j!p4w2vp{Ja#6P;Nf_4SoXiSd|wP|CCFYtdxM%2g{{4|Fje&%EByhuLFdYG}caG zN~I1GS7RMCSi`0g4<<&X_;pL~xrRh>dx^qS8V9AA#{K54V7*|F; zY^VGTsUkxz&5~Rha(Ad5UFB_TsjPpmh*>66~pAx9DrQT&ygx}WzSzj>{-$vED@R2{NzG9SIT2${ECFd7O6-AP(NX{>jOG~7R z63JD9ie;!(R$oz8?<(^(4bv_v(OHKd7GfV4~jR97J2tIU=fA%Ke*k% z8vOov|D1HXOoD)!s(ggobZT)+qNfJIj+MZ|hjfS7UMo_7;u@VmZU&JIttU#}kDwWd zUa3tUp3^)WgghLCJX{d+a6!n!1tAZ&NyF{ZaEClROBy~z8a_<99O)9l&wc2gOYcdz zAcQHrk;mVZ$^_bo+oeDX-rmXuAW7U$mVkGZXBCK$zW6~&`n-pQ@9>fu6+5265h+Uh z$3o30IFZBkLzp2gZxH4gvIm@tkd{4v?qR{@nzghWLf;2JhgX^*S(Q%_vxDKBEX|q# zE)iFjR4rXJccjHx%lYyOc^RCA7fJpnfvr5#Me$vL3Ir)F6=iBMQ~_r#S^v0U=^3G4 z;V1835hY)mp|LOOiYpz&r^ddJvLfBQk%h%ArvcXl)Fn0+1H|}7VSL1@!Rv@$#rGk2 z1jqMp<@>`nKn%r^F<@D6sw3Eo-X-{F9ta`^)UaPo+6?P2b1TCt3}uak5cIo-0Y^|@ zK=0O8tnZoaAp3!9yjMVA$LVphwH4fe5ebo~3PTolLAoH0L=CA&@6oD=)|E51z=8do z^7cGlI2fI);UTdpnL}7OBL$(iPu&4;Uyx%GG`-#dR~`Y@*JLBIf}{fqzLWD%*s$O$ z`jlp&^Sp4mk)y=@inv9I6e+nH*926i<$12C+X<;AkF+hoT{+I$)6stBf$Vki_#Amm z33RLG0;pI}t_q5z@iu9ULmrBz72YHN5%y1{J?o!UY-K6lazD-y69LUY+O&VskxMmJSMB-yF!HBa) zk}ctpTJiT55;ESYv}(GN2euH0dry=L^E49TYyV!hz+_t3Lt34^Z{d})D^ci*4%}5_ z#}$cKQ;DB!)+5T!KM>v%anLL)b8B{BzHMNuv<9mK?mniQuy98dU3!O6(QiBGw_y60 z;rkE|xH5WsFQAnY7dhb$r&5Gu$IxD&*5F=9+7BqqLuCwAuRnW}RGK7}Ci=@xXqQTp zL)(SJ4NUFe8x5@)3eO3kul~h_{^Y`La*gS}Bskqfa6YZ5Yj6 zrTk+lP`3r@+1aaoZS9^da$zMJ$ielYq<~3^K?Rcvr%HvBB!bv)_znXsln?c#dQMROrZ>s~^8K=d%ulhJ^*_aTCbw8i<{W|&>p z)zaRBUxK2Wt(%EAJQv{t<|{0obQ$Z>4tfE{gNr1r9oO$AVz)hHZSMUZ>I_BG1Bsld zN>N$~NS#uq+sU$_{OGj|G(0*K4|rGT&?RCPWs;ke(FqarU`_`Jh7kyY!%2F$Y}usy zmVk_!UBdJR*_6_~0SPWr^RVUy`(YSFIzm0kGgCX=hJ#BzREo!VQmJSEXcFgXcqHdR zx{{em>z`-jq6KgfjK|OEz;+tum4Wea4nyJsnim=X_fx`WoAL-0H!-mc98aYy0m23=_wI-Cg_n8u|oo~{`C!jS2_&%qJff*w7QiqGNkDc!C?-G zK$Wunur>wFzVSRj_Ju1cd(Eurz9a3PL}_0=oGh>|aP%AIUDCi7DT$6H5(N4(QP_zI zCSr6X(VQY0&!CjVlxk=iSEJWaTpR+e)mgz(Ag9u7Zl{*xs}1d-Z&*C!YJ!1-_en5% z)?jGTclC)rSCSRY(BcX3u>OVCpc~QJ!xkgdBj=sd(VwZEAYEju`lI%uQ7~+(X9|d( zj6xfOiXnQ@w*)Aj=`2tt^)2lU-b_oGe*Usk)v$p2(&MqIjm(B1Er(*fU7~i{=M^LZ zeH_|27C$=?h_iMN@JcEYZgQOf6)QUzPO3R&PG22GD+tUZe`VPdLl8#l!5$m%>K9eBAFID_ukisBW@lRHu%i%9Ag6P1S z>3}8ZcNL1!1XJ;v&S~O-^a(6&@|=u=0a_zGWBoU#!<-8$r~Hb=LoWHNW;uLc+6K+C z<0j@DHeTu)YJP<)CI+J38Fcm(>k?}nsaRB6eNB#s4z^uqZ5H>`uc8U!H}%k~GIBS|LFE#-}ZpJLF;M}od$n@}cNfn@+t;a2sq^e4Fw-)kZJUUljJJ@`TG zPpp5iO`J;#F!G2g7})3dPUs_}5x9H!PR1SV4IkGG-35%r;X|wynivF{ zcrg*U(&dpnYl0uc!o@z-a9~ygcF4~FN`Y-U1nY&(`j)?NGP*@^61!UqC$v{2O3maa zDYVhl>dpnuU~$URrVd=2O)Y#i^W?g(y@@#JSvom*8kN!+-*>bnRKn#5@JcJv8rWNS z56?Ktq2~JnQ0|=#-9Z550D}+kdIt~Cmp&jXLAn)If+l~-_`V+XwfAYsrpB{cu#19+ z%=OY*7_}qN5j-Yz*dDVOth}=s#dwgWRZ6oa1$*oCBz=`hR!KH~AjT(0HR`4NJ=XT% zKhO_tK#(dR>_%nnYRm!cKv!RW9>z#(Eb9qmf}?vv*h5rjS|wErE<+QH2JJp1OzH`m z)bd;4u(hRS(DO1p(<1hbwR5yO>jq$`c(a<`yajJ6!D@qifqsPhoReSxq|(y=%!fo} z>4AJ!J)tTDdOCLCKFs`79(;Z}G}yuQ`GaLwM!=O$zcsrCTLP{OQJg^vWqy_n+Q=V5 z+G0LbOb}m=D9$0lB)kVwK74+z5#)TXC)b~ABpD0!`S9GiMiJB3=a0f;VSRp~>?#Vl zibM+#-X#H7iTDB^o~w*h!20|$>;*{q69CnKi+$Hpex>Z1q_5SM+(MBstPzmP{uwFk14{{5m7RckKcS3JF?)atX?KvqXy(tey z(_0<}sngr(Hw$`uZhsWLF=yiPV`447e-$gG(hD#93sc1V$_@a8WZ%B^v=?sL-BRyI zu&eK1q|kfmN?@_pxvvSkhAGzt??)Rf6}UgGe*2fA{sU3aT7GeL_#n1J|$5{^G8uC5>$OnYC?4VBj^hz577>r2_7OaXOe_jWaN{9JrS1;x8j9kw}Mq2F1&CDuv2!jKx0;}oy( zO$yaF#VdGmJ%ji`2)?6;S%5yK$1EIWyu#~n?^~fN>h2LOUID-FAlt^36RZT67~>Q6 z;rIVud_tee5%CFkOfkeKc=1KVC!C3=|9*Ue8k0Z)38jcbh>S^)ORLGw&##s$szrPP zK4QivlyH248j~QyjwvmZD#|1mViQUy;GGHe6%*=7*Y0+Ff=L9%4Nx%0CtP&~(W@ao z;jNhQ2@@KS`ZooUr$?G6e=HLg#1CG_=6Q<%BPQ(=DjWeJu9YQxFtP^|cbM;Dso6ZR~@D;%FNyf4k2;u9*t6>x)KjW)+8@UrNIw}sY&==cOK zu273r5ROOqv<1sgWITd%WriYHt?f}Id5?&V{)h3R zcE9dX#w!@5p{J@F0Sj*QjldKDA8tSL1IjU?7rvpHsnH81M<{xM7E8Q9I||T;IC^2< zwUArY*O>Jo&8x4*E6~n4#ZqOd@d_4zU0SQ!9?seYz=xz@Q?iC*7G`QO3tL({k6LgW zq86|x%TWtk4zb(SX8lGPz$8F%3&UVqtVP@c9I&+Wuf;8>`(YHlfH6{uh+g;#Q8VHF ztM0@vJb>3aieDIYM8q!)nGlAVw*L=j8$jD}{6b>a;unIq0(qwRg-P+_7pf7$a4h2& zXzTgsjbFIwl!*9+2S7wWXZ*rRB7R}ZkxnBR79zc*0n!~uFlhTXn&qvM`FFB{-Wb6W-l>8 zCUl>{8t3?7L%f1FQH6I{{}!b_i$qTxwm^|q6tn<500Vzjx1dcLK|LUZc!dcufkWF-VO{m8;}x8>{Q+1tW`P`2 z(i-e06Tm4tScNxYKPXKg!~lmWjBa+pEl6PsWq#V-rg-9D2C8d7B>jQn6K>2nxR&D+ zN)d1_ZGyox)4}ZTlCO6Ry58>l+y=kimc!-Hd=(vX zgaYDyv6HX-y+mJ-ssB}6yr+m_6}%^7BUz1P5b;P9d*ZBH4jcf7g2%t2F9=syohnVhK7>llTXL6&*|ay&C@rZzIPm%)@vM@d}6@sP1aK!er;Y zWKdAN0t0*6BvZTsPES;GyuyD_3GoUy=<@W7;HB!~%x)HcAW;3(6EmAB>L0B4VK~EPvyJ*$}(H z1Q_(uHb1#lH3L%JU(fz4?XOSb{q_rn~E5!!H|5JST27-485g*1%A*ag(+Tp$4InGLgn!x>%~ z4ey2?k3sPaSJ6b(_O*D1$v9_0&8hJW*P(S|Ji~ER%<&A9q*;@L`_O`_&x?45z8EpD zm*|z_4f4>d=J|#;i~xsT?NVEiLNCblH-tER`WMWFLLBbFyBwcjr+2#?pKvw0#+w|U zu&Ed((#RrRy0HFZj8AwBk!cb8#YMkde8NgsQ57OIO)&|w3qfyXK%zy;hZD~=0TLVm z3KW{qotT7b)x9m{lcyx+vJ_sDItWmJd^W@ul1goW=$?H3#NW66{FtET^>H{hb{0SZ5z-bNL5LT}l>{pILw;*BxrZ5hs2 zMAF+6zcJ9;ZI}|#n{!(fy=n0Y#9DfM!td}h$0rcID%tclXmRX6I*3o;_qF(h^YzZP z_=Fz{BjOXdz80TwaFRAyt^QiAzJVVM@d;n723s8a_=HJai{CQH%6Rb!hM=+d0y?Mb zVpfk&ApEVy(fedMFD;D~53%8|^l4O;8acspX2OL1SmH=-Vt;Mq0N|1CSV0(o-GU;D zR^Q=U$I+XEO@(5=G^%|Z7{;dMzt+U-;mdP6=wXAYP&<#80%a~khF2KFvC7l;TSLCJ z4p5zS6Xaze5h_T2HN~Rrqn;1;$9ETh=Q#K;@m7V0?pB13k~@)1i{O7SC(X3sPaVF>3r)Of1p5LZ zI=a3pCqw`+bhbWsI*aRDodtU~d`X4a@AuECp)LQxveyp#C;b%c{`x3V-hzwH@5K_) zhGQ84pF+RjU$gIP`Uv>;(g*$peEY=%o?6u>tj5T=;QLW4aMf%?OStF_X$WvMqI|cq z`eZ=Ow;L4*kZlCWXZrzgxWlO#6l9Z!PY(>APVYRlF(?9>+O5hl z%F6%}Mz4*{RkNS!sO+>)c^Jih%9dt}357Q08iE0mrSDU2C2+f`D!@j6ooijuQKbx$YLp=!F8W96M#Xt${Cxm8QCVecPr2D zfbxtahDIQ(bCm+2wV^MtV;Kcs*?Fwtd(BxL!uJXkcL?9MZ5n)?s2TxZA*y<=GD0}T z0O4T*xf*Fg9vT7MFS67K;NE}?0r=&O8i2=a?-amM@EruMJht@Pc4mk0eILag!uO%K zHTXU!TGG-%8i8TMGzb_^zo`LZ_Y{0k=b`*UQyxL~?;y+(F#LK;BGbp`05=R@1O3(u#MJAtRn6}kt{%Ug(cba;-S zCfp`?3Nd!|%SMQ%86fHf3JeQ{1BVTT|HlN+F_E?6!t?Wh2G6)g%S|SDww`7J4q|5Gaoab|+O%u8c5B+Yf&#t7KN2f^av*m<>qNQLtP|z_NIH=k z$Nf8ShQBaNX)mSE^nlAj2N#r;mugE@9xa3K2B=ST4`JVBd`z!!+b-2E&jaRAx6U(QbxGn8_F zRPInaJ;U5-2r2MmO4s~EA)#~$Y*%HG1L9T{hrT9T#0eOEKVGK5g?vRipW^jP6?1&4=Qym=8EBKE72dM}CS{TJ=_dfl0(FsvV(+S982+l>J1~ zaR9VJgrC$kMA>IMggzA8K6}kn*r9#4`ki<^+-7Pd!hpHfRM;i^?EC~XfFFR|HUjuA zd=CRyx6f9>DqETFsgf%2Kig`vVWrKk&!1giIy;_)b{MnLsnIZhc_cEv99QAiC} zcFjvcgGLLjKSE;tYvW$o3nzx8Xs9R`u6l)r83?Q^A;|T<8 zp?%0pZ;~`D_dwr`(dzIMTxK;`#J9Gzb@mI)$)7xZp7g-g9xn@mRxqyii!;!FvdbJB0ULUuf_? z$)x%;8i6wo6CvOndR+rf##fyKCqiHDw!Jp&M=bwFdu>`0QAA98ZDk1=YqM2jtx6i} zsn4DLvl(pX?8a<=euM3)*GvXm1iU)6*D47d9ocI`xkC5!;oEqsx_!~gcgvgX3%1wZ zGP0o2UMo1&#AI>pwf>Ry;wsv8uWCg1DrCbkv@aa2+jU_7hTwd~THY=D_x#U@baZ$w zqb6e7Ya5LaHQH-GU|0sR6~|uN99b(aJTHAkgXc8x{xO8-Yw*@~NS}{zh3>)gy-!T= zoPZ__!0EtVyXs|a+Mdt5HEmtUYvv5uYpfgOPG=hwNtMl&RPZKm4kP3BIAqSODfy|(=T;A*hfM$pEEVY_DtzK6{Tw%44n zC1}e>H7BsqGh3Jxx;YVHu{{A628~5oY+-plOmd|8i$9}GK%}lKFZ3r3*}iE0XBK1} zObEDl)6cY3^^v@!7LU?i1h-9>=iBAkHGC8|5${q=9A_13N5m2W*OUG8chiOmT2+5Z zY9E(D*-G5I5rBmI-1m#1H<*wd|J4T$PwS2okx$o_rmTX?S#XA4NqKl6eiO-5`zjy$ z0UFwp@)ago54aMoo=oMMKO(55i3`Zsc;7b)MZ*)V-ZPbF??sOGI;2?r8y?87@b!yr z?MgbmFaUp0aF1HB4FyD0@!|7_e#Z9T_De;gkIbO!o;c zh=k7)ur0%<4i5yMx0VAwc6uQJe*m9qwO~04I)=~8vyK^ja7w>x@L6zDG<>c--vFP- zVw%49BxgwJSD6~o7k2ZE3P1;A$=z0gRE)u;?s3p^<37(N%D zdCcIG)>Zgibz(Gp&N$ZqpNUd5e0G3ElsqnogwKVbDu&Owcp&)P{4c;~8NKko#8{1r zLoFynLC5ep<&0wnpQNtBr{4+D@Hs>g6`Fj$vOXFl%DM>J<&2S16!F zgU^5JqTw?KETZh@f=Kvm0@N5jYwmL?Khqg}UO4TT!3XA9 z*Wk0NXEb~sPBp;i!`f*0Tm#-wnz$emKF{8%!sl;zAozU!G~km?FAN}#gZ`ryY(qiE z^z(;Pj~RSYx(c7ikBf%S?Gz-U(a-C?X!vA9geXsNK_q;ZfU1~&>hM7DdFv^_$4)OK zkl=y-qZTYjLC5f!nRLwHliF4IEPx|BQvO^^F&-Lx9=jtNK7AoVlv*x`gwHLYDu&M# zJP>@I`6u9`fTxtth_Rsms0H_-fZ)S^@nnR!A-15%Iu91ZWqcm!1egi9oF0R7u592$ zwstws9MRVsRdCcs-4{~+ihUaBQ%{Y`{)k3GN6@ z>!*Bkion6Vw2LgxTH1-gmKjD;*$W9%?b$BJZNt^`L7`zHK7VLgTG{+!Uo%ca=O3XH z<)!waNO|cagU7kDhi^>}#4*reBF~LzVW`V7Yn5}Qg?^B4wK?z4OFOG-D0=pnTX8(S zwkYi^?=zee=e`D{OSU#3aKEf>{et5%w^m7~;Q&JpBAevX$>eEMO`bR5B?nX}YpF;< z;jp0PmvA)25?YP>gE$3idTq_CTWvV@1QfyKl;x`g%_WQ=bP%3~8m4y397@G_58lky)8PIISZ_1BE z|BcENwcr^P5SsOW(fGbF4!&)`H>GM)`ZAu8t(ns_DX$>tMsH5DzcvHo&XsP5igh+s zztr*VwO2{*cNGgLhAq}FEuLQ)5e+oWELP9AiWIhl6R!)$JQ$TXVe^f$#5|9YkU=Vd z$@dSLah|K<{ACsAo`$NS?sq+}U=Md_Me?k1RT5K3MUt<;W)-9}^K}(v+~pg-g*I#7 z2H!3lPDPp#zZL#zl~)b@%cl{4P!wqUIJ#7dTlpU`-ntT?@g=g>L(5fc_Ep|oK=?Oe zGfOD|Qz|cT0i1XUEE)+!=fWcHr#uSe2mga3AwcRjs5ytJQeUDk`PF!k;mGz$2w+XGnAnNA6@~aZ9d5;lPGf5(G?}i#|u$XYGV)}spv-8~wJ z@}P2L7NRT`ic70*22J6a8GI)6YdGF?@J}NOymc#e1BuX~=br;Ym`4 zZO7~jRd)1#BKLvZPXX+ZsR1$G8+uTPTn$d$fpuh*h}SX4w9qDfjt|a_oa3cR5{-9Z zWP$f=Wf~~Mx2~O2mSZlJ_)3#};(sy8kHII(`@}4bic2k!QJ_-;`;&OeX@)5mAJk`j zr9)7}{DT(n@zQpA;*aYQvyS$Kx1FR1(8Ut0jeo!10l5<&otlVf5m*i1;s005!p8? zrE0+=C@{`i79z= zB0%iTQ!+qKXhh{#6&vp;eIu)4{k@-9q)|Bnd=Rwr{|C@sjNg=bqTqA2;8!RxK-=iA ztfpvd_cH80OYWz!T_d@lm)%K{d%0{?gXDJohp6OQxX=B?2w1RbRLJ-}UM0kU#d8t> zQnTOs@qvP*<2)yaJ+_I+*CdxOmkVlS?=rdgc?8|d<;&zDl6Sl8eOfBsB`9?{<>Euz zN#q;$7q64NAK*Mb-4aOYpWDYL$?lB<-0KiSMvL-ajAYD5G+qV;V06ZQbxQv5wg@nL zvXpDUc}f!(L{jpz0CjMvpya>enP&Rcp8?x*>77(!*+xZD3$~y@(?9tBL@SOjAY#`9 zy%ZwX;`bt;HycxBP2@cRJ#s@I7!K$e{N319r1Sg3YvkfYne!F;i?;(zi3BG7CJ$Qo z6J_@Y1KeO_%7NvDJ`v_~2GE185dd<875vv`M+0aGSXKF7E?~TBdLQJszXkxBAON%& z&jg_Lj{`t9dZ!%(3;<143!X;-{NuFc;@gu0;Fxm6s0o?<#k!tGAvKk~RLDR_J4xOm8bE1DrD>?Zmf>{9+f3YIdegZ=SpAJ&&@yU7yK6#&k$cLb#05k}mbMK+pY4=`= zQVut!)5^cR07cJ)>HB@7q5kAU&%Idr6=tM77+C;7)PjD3Hwr-1;F$o?@E8Db7@VgZ zAogoieytY#9R(^E0vuBJT5vxK7$Um8CgXBN-(jn>_P4;K zVevEAZOYZhF&rrI*E*n^#j&&Ybvz1f-UG8O-=3Y1J46Pd6|DNWo2of4t-nRr=LkAT z&6@q@g2LLZo&q`FA>U$`)<7#rmbMT4YT&vphqBi!=z&yAhpf&elyA$QS8lOx$)*a3 zRv2S*-nk6Fsu~41weZEoNO#h{(0SkL)D5010|i8ci|WhL`oByMAsGYhZ}ASoAW6IC z1-J*{qJ2LzX4saq(VUMP{O4&dR&H^msUbINo*iE3nQNHAdI30N@4?yIagMuC1LRg zNt|v$=Cfo0UnIRHqdW-~`PnFmISVS2Dz7SBr~D-N{Pn7YGrszzHyX{XaAxr>6sP>7kIZt?WeXa+#^JW&OLP4T`6 zL-0y^-P4oq1;QaeCK0FaP8v4y?tr3OL%#d|7M-g7(u@7uP zPM$_-t>1PN=}q}|>-*p^n(kc}=@gZQqTur&9+klO!W_bm1Ts%XZc0o`qc-TPDz#M` zUZ9*SI^~@=J(}|V3Ms6d#08O*cW?$%Ub8S$gY*rQm-!$lubSTa9sYpwTGWDM6lh8j z3*&h9DKe??GR-`BK18X>KFu?mPN#-VC#+N2eGx|a%RGPLK7nJlI!XQ#JFe0t?%-Ra zCGIkCjM9e-A|>vB0FRI#bK8#xn)I*-FzMUqg_*=>jml27U;qj*X-(dW^QDz(Sj^*o%$U^OUcsc@>y8e;a#oT~a|GBvSXS4E^7p7%@aKf6!&pM}jn|+U|NyRQQlxdMrPpEa2IbO;syypD0q(36%Vkt zSZa#W&XFhDVB4hm3$47eZN`@rEHbp%oCD3Fc8j?0Y3RJP$xi=PYOXA8vb=znn}P33 zZ%c;;Zb0@;Xe4iAy?LA0n-8c~W!fZPV~_03klE? zg8zDY#sxVT8h8N%niQN!tRVCP(Emi_7lVU*a@~Oi$D>i?DXNm}bvwQ1`8Ggz7J}kL zTxAWTHfN6zWjN)*S4zRXycBM>9r&u=d+1Q*0Np1{vmX2ksYc#Ei_u`{DbhsStUIt0 zWp2gF61tP}Tc`0-^s%-S{WC#ZiY}q0sICEvQNVk?wLM{?%{nh_lJ!t_1&*lL(bDW( zuzk=^%Yb&@5KV#1OoSTo*NiDp$3(|Um?a1o(K@A{xPvM0SrOPG`ds+45^KE zD`~yTW8m?7Ac{%B&Ge(Xp!Eztt3YN5AEqol9t^~jmaeXG8Orb*A?e+ggsJ;|#jrQr zus2RqUcUwMd>h1e+B6~0tA7u9{&)PUoFeMe+V_pFb)Bgkz6s5UF#(1&psO&5(1W2R z%5=Pjrgor)gr(tZG)4BRu*Xud=K_l?Y=ewV@_qUrLhFrR#j-&f+h`F-xEE}YbgXJ* zvI-kyuxf*pRSlhf#5>xj^uS#QoC)q?gPEn}N7};bFPkD3bOgP?P>JL(+bf|Rn*8nU zTzLS7Vf#2TPo(D1lloF=of^4RW-0S;2BehVQ2|Gy>455|Yz6#+(|~Azq#4fyl9QJL zx+CeGJp4hyGit#P_o^y_Zm$_+gTWv7Olue_h3HdN5ON)Za_vN`o*Z1l{42T7P`85) zw-)t5B-{d6q7Gn6J?`E2Q{kYfSb0nQZ;{Mf?!2y@X;%NPp7OOZS^gd4X6z@jt;XJ6KI7J6;L+-x?I2yEw z9`P^0duQ~-Cz}JldV0pcfbSmhK!@^Ds39^q$Vt!(*GBtY9uYS$LBqW4b9Mp-^7(8t z5sIru#6G&_A!$2K0xeRTs&_3AIy7zM@SYfqpW@w=fWbZ(r+bt_jGX%T-Y@henw)=z zo!6Zd#4Wx?PTL z>2yM+7#72x^^M5!g*L?)UwCLV%vuesu|OSTaN5!6mfgky^5F?>c&Dnvd;K1jF0=u1 zI>bb*YpHPXrpY~UJo{Q3NKZ=XP1eL%>~4|+WafV9O@Z4%ZxU{+35UG5aouN8s8z#S z>4Y3mD=<;-nyup1*OUmn!Yj^0;QJ-0cVo{6sYlBVVE+SpL6^M8sJP}3NEw$}X=_=%KnI?DEZpsYIEwJ2uCHOe*dSp`f=l3QQLo(TtXwQs$N)e) z`29#FBjUwknxSNVFJ$k$w0;)PcwBm4ie#+)*)s~n2w|L;P($c#PUwN1aL;h|uCT7} zxxl;`=n(_}&sbBMxV%@K7e}+$6iOq{BW8p92Ve{G2JI1FL(kBwOU3r;zjdKo4a{o$ zVKxLD+YoT9K;9~)T%ZYE8v>4P2spm-2CVmghG5OY15w622@-cDz3>A5px{ik;06?c zB7)O!^iGf({|(Z4bmMm!=lzY}A2FW~4L*f)wuW-7Z!EmAe@-wi{#XOCvUQ)jw0&&x z{D$JYcQNE4gN^as9>i})#dnLi?p@$@OKtxCaWLM*XKz@8ZK%Pf0 zT#Y{{Sf&>2y{oM~_=p)^!KZMn)&RfYtJ_WA0*h&oM8kr$9r7azR+S>kS*%JOkcSE2~e}$yF4vsRE*gb zg$k_qRZ=g!9{P)tik%`ID>qypiIvNhk3q}IC@#<#K}6o4tz=C?b@#5|c%h)3i?4*6 zo9_f}meFhfOMHO%SGAxF1%!s+b!PYnr|mG|BRKbc(|4i&B86qDJkib+zgJnt?seOu z`Fky%H0(K$64w(^2uVZ7COdt110hw?85G$wHmS;YxZU$>*}Y#*Oc=iZK+Z|2QlsY# zRh=KkP+8uNsU3|3T{_{o~?XS2`HMjFjAG9eoKJ!7w=X6f_V~9lk0Fw2=os` zguYZpp9UnHb_ryqwnLHRRk1fA$WIhPwzme4E94djn}=<02yEcAG4=tA103}`Yh zJAd41UUq!=-s!SaiUaUC{a!H}%X_TbJ|pAD2>Rz}(Dx=|4bXSI8P-d1AFx>F$wjBr z>dNaPX*E;11VTW0oD0mfdZto85m0y)5L}1{0%*VoD(yip{D;W1QK?c3o<)HXS`quN zN%BzIGAxEX#P;iQ$=xIu?3cVRNyV>8>SoEmxla;bcmRQv(< zQ6)%Uy7w0?&^D?aA~z|KBnY-qJ$(N#ZKFo+p*~{-3~v#7KZxM$#4sm6bZsPbvXoOX zPh}AoMA}(5fs}$H1RGzEX9CcZcK|@2KpZO{;12-gRtx+nFagLQKiNQ~3`nFp@E+hP zVNhUqRK&AH?~l^_dGf;KkK7-9HEQxTAVt#(W7r>k^qT0&C(}fHTwq>Y`YAVqV1gB5 z^1s0|P5vK?G5Ou#9OYyD!Q@A%1xryN1P zOGG1HK^43l&nHL8=W9iShHARpSU5fsOv9AJplqdz3(Vs50;L2b6TCqHDi6;zS3(v6 ztac#)_6Bi9BV_Uleo*?OQfP+}Ty*~g7B3rl?JJRQCV?)P1b@I4GhJ$d^h07TIPtyd z%4dh*bE1=P*a(uy@>vB zlcv73deW(PsSzuTlJ*2I(47W%BB@_ucSFHTyLz4~v*Pb}5J8Ol0oZ zW$`S4EThO9dF3MTJng;g!{?%fv*Dh;A5YGe1s-YsHo1Ho_F!l;+9KaxL$P7=Tk6{7 ztKfuk$nI^}x5nCGBPu{7DcMf$7FbQ~<8clu3A^Oc>uA0hDV?j<^`K_xK?~shW+;v} z>p|IJ!436(425~m;K+ki3<(=xa-S-xz(aIhFxx8Mju~OpK^(kohi@hVpBvtZkk$wR zeUi3@ggtPT5-g1r+2<-#z)8v?)FMr2m+~jTkht#Td8`SoEN4w9hbleAZ<`mj6O{W= zBX|gxe`vTQUv_8(3Y8-wu~)O`Nq1f>P@!pjn4br+hAQKvWwi7 z2IK)>Hr`MZn2J;#AKk{#GGU4|)@^`AEPB71Fa?d3?JS51xsZ_MF(KTyaZD4zq1e2s@P4V8eeZTKMqUmwD0$N2hi;yUFCh%b$+yc)V*T?7Qw z;S!S&J_%ROT5?G;nvj=FAPA_EPbW_L(7le(QnwzhN^OKGpyxwKX@MdfT1jSRibPS{ z*4QY@#C}{X6b%c55?kqA!q4aq@N>K9-q@&8K|4DdKRr|ujh|C10QwBn68Omn^awwz zY8XGi14glm?FEl0*YVqt_;I5~@C4$gH&F@r$)Z=x{G>9|3B2(DZ2Bz7VP<$3h`N5-CH#=O9FKqA zheH~aX>3|qK|3cJe%eMy@N-YdATqeKbK-v zQA$ut;HL^OB>a3di}7<8a032Gq)K!7?MVDgK@EYQ5EuveDWg}y`~!WV5*|>5?F%lF zuy&MhRQm1LapkPN3j!@_C9=X^5GxVI*3e3XqfJ{8Nv5wya`{Hq4&Y31ufxUhq%qKf z1obWnGA|KPgS1afiXVwU+1*5!=f+u^CT@&I9H|fY12nNDU`|*}!JMM_(IdWd&`O>GE%64EYECP?n;WD*v!m!18aVkbl4#$v>*}D!&~i z|4<{?ljR>Ok^G}qI>q1h-~BT9!-4A9y2WGI{E1{d!rwap&3_4hXNVyf2}3zRyDb|2 zvPVSY??teS(t=t7e_voPg7A0!bjDw=QH;N(ROxU5y&Z|a&w*8eziFrh{B6SzVf^{$ z*vL#vfzJi`EMNMU*sRXLwk+kdbk*SI0G9{+g{|t_u~R#b9>aU62oaCDiR!ZDQ482S zu6mZ7oznq%k+t$qWUuE>H||fXsXnn!&6}9qMZN2GfJs%pX-nERSR|ZWfe})>*WYUP zo~ZVRe}+xppAJ1Q_J8%^^L%)JI&vN{Cy_uF(q?Qa zB+8YTBs)mx_7il)d)b?wGH)te)o6z7h4|j-rb2}wt1f~C_%yFhVSq1I%b#!mIye+p zrW)pN^ml6ePw9KqRRjA^v+8zwZuPsr#nOxf`{X>z>sf>8(^9RpBLD-NrDyP_vwRu_ zqV79UWWrhwbg`h1*qPe%acn_XS^~tBk{_ zi3B#>)TV7f^;K>LJ(xG3E>wb$&`KE>XdY3q{hh2_l?O4c?iE~&=g^bNt5kah{y=E= zr=R|4fZo!j&J_Rz0V8+^12#Aa+p2~d!B??V7`_9aJHRg=VxU&B0W0DXv?{?tPz|&% z_zb=Z70mek#E9}o%;j+O5r+|;tPbCODicv#L|wML*%-sYj}AEQf_#2 zq!V9ZWZGH(D*EG((z?}3zsz+vHcXLvRa@cO`y);)EIu?Hp^x5=1HJ|v<)dex8u&zT4~GT?<|&7%ecoLz4o!p4U0deFWwtpIy4F^54tL*m$3BIWKiU% z)YEgB8q)(&ArF;ypk1n}?7B^zxMv`Ka*D3r)PGFUhSrLR7tKG6;|`BH|GM~a!IWFP zUO!?&B&K})^a&UshsWTooej+|fZbBCM6Ch%cx3+dyAP_%Tq?w387=x4wt9Qb6|Wqj zKuPe6*!Pv10e~r3MG1nB{Z3ywM7tz^16FDR!qV3uNZ|oAp>`|eg;-M&D}jJb(I3Dw zgFB+|Nkd0JViBN@veb@PFGyBE7%Z-13FPr%+_;PDxvwa$;_AAt z2Zv`uAOpf`1mqGG72RhT)F24qko>+?-On{g2&li;Ki+&kFnvAMRn^s9)!o(I{S%7M z1lq5VN+Gc@*VoTnv3L;PF)hA8EjkvMQm;%tR=RQaX7;=RP8TmG;+w#}s#s8+NsK=yi+Q3k<>o-;*2j-q$hq;W}o0@9T81RA&YM8vM{!jcC+; z@<-n^C=~B~bq-DYOZ0v6KC-{%dtW`CcivA3qDO`F9}`5-RNS_g(|z=r(ysnC-nUs#?7FMQDll zzV;$Se`SnLAkQ7VihcXg-GYge*lPC8p|OA-`sxb54CQtD1d#1Uy-z;$_2u71S5VTkvamD`X#?L{~pJrC^rAb;J~op zUopx=^G^&iw_R)H;5il!J`JwhI5;8V{j z>oLoU&A;{&b^dij8Iuz{Nz>)zK_EzUUOHB%b1l67Te>>(oYC+8V+he}B)l45n`0&7R4@MKb?}E{Vhmr5b z19kmUcAxDPE#S$oXaP^B^@Xg<$5>4aojztLjkiRRcp?k*#e5=bJ5kUS+(j>Aq;R|@ zIZ=BSN9Ndd2F%4HSFbajSn)+MV+7!Lu3j7zPkz6^tkG;W92bg^-}fW)%~z`;h#WRr zZ`?5Y6dE_YS@ii+pf~i-nDoBJtVhEOdaJ0Eywu#JSM=EB*k5C_AnG}c@4zJ+#kTS8 zqsZL2^p3w#r}rrOz%QfsFqR`?wC}2Mw)WkGQWngyZErl=7SR^#M(MbXvExQ9g05xw z!?TXeokTiPKKyi75#~nyJ8Mn%9LH~Z5O1l`!%thW^Bw5#vkmN05oj-=!_l;JQ&xl} zE81VL4~BAGbbZGmnpEFkL$>;EMSZr)sB)orX!$AB9d!!uyk;vCt>Fb!>lPw+#$dz>95<9~N3)W=cNmKQuJQ zx5vN7$gbCDx5uBe0gY>q&$JaorH!)3`#x@OfaRpIEDi7i;${OF_IShB649xO7#4Xu zFjZ#;ehq$T8)5xm{(7SKot_jn0dZHi$F&8~_PF)^-KCH(df(|CHpTHW5t*@Uk+ZRh zNVWfa{0}>zmVWRT-wwCFz*`exkze^J_{O#@2^+ufGzlsorbW(nxOm@*&=K!D>D8Ap zJ`pDQA~wmVE!9o(+}UK3(+IKna+*CK98M-V(I07&OMB9MZ+ALmf!@D~ zV2$LO$st;cK0vI(pT!P0*(0l)MofyZ$cKX|xag}tqKPf?F2uN)7Wq1i%BFU* z+LNqUSyZl`|Q|I67HhY0Ba$QpDfFe=4c&JY47Mx3Ai`?49SW$FC;}qV4h5 zrV}QHecFZTjAf6ngJBn^|J&Lu+8!ShnIG2%Tyw47v`5jSewlrG+dpE`yNWq)(3?l4 z;@IO`;bd)6Z0)ZBaLF?Id+g6!-yNA7m)=hX>-4tx#q{1!=2~?7Zp0P7mi8TjQWne` zu*b6v-0nv2w6q185xV(G=To^W=T0UCDIfNkg*gw|$M=4&+sFK{Pv3Q9k597=>{1bE z?f!z!otm!YU2sBsJi5L!WPMHT^=(~C_PAwi;D=awIAMGI1$1(06P>^w=jV!+q5}|B zi$Akbbq{L!RqXK|f5Px+vB#HUJ=1CqK8Cny4zfMoxc7N17P)ftcqu6@Y6O7X#=OmN5qM z3^|04jYrRJ{&Cmf$6Ulk4P-QW$Pm)nfif}~s|NBYFUI%4Js#m-0+;Z8+6c%8Ohn;+ zAt{v{Fx?->KGaFJ+b5cVa`v(k1z>|ej6pCoShgl zNthBKItYl8qCPZ$%wJKTlemr7HsvuCY5#g?9)hj%ecE)$*Kb0czHEF0)_tQ0)Hh!C zX+4qMFp+GF=p;@8xc&Zh#M|hw!eJ#7`*9CK{)Y3-1&49aLwg1bk?OF?!Gg)eLXZG(smgG;dJKcp-f-Mr zFbsDV@QoN{!|x5|FsE_Zoc`={k}?!;(>IlCaJX>?tC1kqY5*s>?->`R&OnLv9q1+h749Ip2UxCid80^P%P<@x2kEfc?W1&9 zFjw>|2YuzQ#0B$~4@2kRL#c@5&k!`Sn;dHBx|b2jbj?Lk3U1|8i#+r{CF1Z#OT=lJ zd4yZ-1Lqd~QisURPr!`79-$BLZ*(ugLV$>%lW>;8hw6Fj8Y(V3#{|vMh zYmndmHBO!7eHYkSejg?{R+it2xWV$kAm@qqW379z3WzeeVDMY(gL{b^vEDNs^J&Ry zvEGvzsw<9O@0o1be_a6ysuo*{!NjhlJ`U4X>Mk0#@K^1p3;m#xHTuC{tc_TB_E^?8 z@+KDojN?ZPgShlr8(s?j_;=z{8hCh}C;t(!h<0Alwzi$uT_`1XUU!R7F|@0t?!-D) z$&)=;C@y2n0(o?Vv0AGfKebJ{ik8 zqPvNW);^cTo8(M`_*qyGu_$pvWG-0%f)y)KSyzO8fJoLw$yZPGjo(PVk}x%kEni#u>HMPg5|eT^m2_Fi1!hE0sE(v}L%olO(u|Mdfa|p1Vw`*6;1!hB}j)JLl*~8Ro zyVZg0hfy>+n8P-cGGwxsHcf}nyVu3WO;5#u9T@FIqq2kKU=KC@b|tx~_Ff?nBnyUC zXGS%hrjG)!Td=u{9n&_Xf#r^A8_f&Ktir7Ke=-`VUz+=+xS|Rd-)>jvwrm;zv^NOj z=t=v`tAT*IDDx20r7z0J-piY$C6`)d(Yi$OmaRpwC}R`Bry_HstRF2qS8uP6&@I5$ z@TVq`y1jTJ`Zi-)6NA3L5ra+oUZGOPB8*O-=xxtt#iMW=xMIp&B!zE9=0;OEJV&SS zyZ#XrHkQ5vnCir!?_Hc^wb1t=N?9m*9#4^Q?dYp&c$q`CC{ z&n~nwk*PKP53Nk3VOl5lKbO;b%VeRUW*Zqk*dQd$G~X#XGr+Z<6dtI z{QCgk1{|v@sBdKG8q{IR3Wdb_3M*+J?i*-|mywPA&SR2_rI2N!BFA1!OVnatU-8T9 z2P{>@UB95=LVA5~n~BlFvcBRDAa1d6>tKbO=GMT*m|GauT44-C+eYkhlHP|SW=#8b zj_%kHW#3+gwS2&eZQuUEmT94#g7NATedr8JjduHXwb0_S#<=$F-)zNDU;O!m#lF3D zhMnPCNs(9>9z@(^xMANmxW8$&aQlKR$p?det;yg<*|+P34kp5N`!?l{X#3XcUr6#p z`L<2qt?q@Btrl+a1K^N<7l4pRv4#6AM(UUrF4gK4 zJDG$C?PRivOWT_C7fei}5hBmml}%hiO*e68;hL3rUWK$81=CRlv1HqU6=Nrp>jKuq7hOyK4|t$kAQqy4<^iIFEw%@U zPcbdrB&aWQ8Q*HJRYL$Ry!f2QGl&aWi$kU9~+Mxtl@X7Y6f?2p*|S~OPYS6h@ZWkt9O zN)Z?X+=!%R21&ii2`YjlIZ}xqXYI|{TGI*25FhS7 zi)--SIU4vj-rzl_d!)hJSbJ$RVH|DmUOWc%TkJumE0(>R55v*y%T{~$h~37Duy=Px z=0;nCS6`+JHRh+zmg6|Dc|pZz8L;z;tf6F&mWlTChNns6dcD&I2jDE{P^_P+!p}DD9p#AMxzEQ zKL1jETmn9ahFbCItac=<1V-;oh>6i*d|_gQ7lCz*CX`EjicxnYKKq`J!skQ4CwvZO zS@5y*$A-@(vGK8UC>B27pzcU~Zi>d|3W?8z`1n}bYw`G)+_7U5yS;|Rz~|$?NAc$s z)Ij{%(%HhF2HNY4*chF-z1E=aNdDwT<8zM0=PD~c4Yb#+agE?mtoG^_1D}6C7sa24 z0H64??2kHsXnupqzk_;_b~j~XNIT1A3~3@KGz>XB5XpFf9!Gx|frE`P`{!a53MNjD z?4LKFyxALKxA%D99?9P?mqgL?3Ly-7zP&`J2l(U-vf^Xw4`;;2r~qG>7{%i6C#X9T zpTW`ioFnnM&WewnKQ?@3-O>mS#loi`20oR~M)Bt%z$gAxUu@xz9Un{oti8Dr*u-wH zH&J&af3l+SIgT0#pUbTHG|*n7V`FsU_UaG=pJ$$l;?G?YpXW2<^2gR*AKugm4#jG( zM^JYpe_BT4^Eu!Xf7%%M=>3y=k@h}iqkrD#u=G#L3H=>8vVU^?5j{R^cvgk%oI%IV zui%a{RH#tAr1eOJlIzr(FNO-lVZp?w0Em?>Yg1C5QOdNXsk-F+OzN*U0VI;XEToy` zgNal-h*N^v0;+Pm_A32A?|1A3lZ{RxKTQcB|12a(vq$%LAl2WQaqpN_!MF=@h=RhW z8&84P#E9<~m-zn#5Rv%bgEX_W75^YrxLvEIAK*XnB8mSOlmPsPBf-G`MErjckC^>p z{&z#ok@)``fMes|OyJ*H;=eRQ;@@84|0og+{LP^~9{<<=t??AZ;eRo3jl_Q+z{bYE zFR^U9Hkp3Ff32g$e+(sn|67qD@gG9(zX%n_@`n?kr-`pGr5|*ri;iPXAy?}#KZq{B zo$*LfwvP-QM^FNe5C288{D)EHZT+z>R)2ipDd0u@aRv298mz7T(XKq>_QwvWIFg^w z0I-#xRJ-Vphp5W!S{gngem-`g4xCqY3R{(l1yk@(+)wAlP#P8Dv~cF_;;AJIYb|07BO{(X@U z-Cr18^vCVYP$BskvRQPFMl-(qQg>uybD1II6nM8nB}jCF3*DllnZ+Zz4s2! zK!b^!(f4@o!hZXY*bm{&7cPIik>!I90;v7*T`c;6#N~$#5=BnvOyn>HxSwP#?kDN2 zl6&6(xc7}pFT_nc*Cx$9l5_2EOhKWQvayy+5rAMQULz_ic$h2QnBxX&T#wV5MQiv>;s|NXC#}5Y^Q!x zg9r{Q?{#YTvdE@Uqeci|Rt4=UnSVr&l^>DiMZSeUx$d61o~dr{vVC{;K+~EbtxmW7EuNIUfD!nVZ6|PEBoHmgLV(c6gVqBF8{^KdeI?(N(}Z zns9Srf^WBX*+yj>ZW?lrtlx$Eya2DW6`n)QN$<@yH|5W&*y459*U&S;E%98ccUco$ z2c!&vU#S~!Vyt#^G2UDQ9JEKz!JetjWvRjLD%^Nf)N$^Sw9z1@r2+gj0ld8atA^oo zF^db4veqsj;%mGph{n&Znc9k;EzWf#lmU)1hSvJCjQYEqh-1B0lI+`qRE%Twy9|FG zh(74vi1%RCA)fFH<$@2ZM4T$ibB7z=PiFHL35S*He6&ao>%VdZ$0n?O>eE z{aI_~cdL1xjJ3FL2X8Lwe81YL`4)Hs z%lo>q0EgSek7WEP9xx}7;E^gb9l=!XgKrPl!@y5<3~@WQWFMMU?f3-ueJR@oM^?!B zhQHbo80IJ|NI;3_lutr9c2JKpEG>(y(h~k8>tJ|ZH2$+@Dj#NjYVcdV1}|Wz0DpR^ zA*X9^lG);(gtWMf6lGIm%nro}?I2@%FAI>y%}J2qv1F>4(#UJ0*hxZlCC5IAAS z9gX>#zsliX>^&uI5{XVOPF=*&5C2}cDb;fa$z(a7dt?rg+hTu}!4LRpV{?>hq(PY8 zPm36D275XUc9(ASHC1Y=(4MMeh>DS|X715f2~?AjRbZTj2(HI6D4F|Hsd(ThFg=w9 zzTu0oGA$0RxqDA0UZi5jRcuqgjduco2t1_>F``>(;Z7wu9R39Ft?twEQH%!EovDVY z!5oULpYYA`^sb=Mp{cTVBzm_M-SVjM6`ecRDaX4WMfJsJn4F|h5A8QXGV>@g)3?Vy z-f{gF7SebebdVQ~zcLk0*8YlAWw53e?NtWvRf}qs!L{&IRR$kWi|UlYb-2Sy8GH;X z+!^YE3;02i_*Om;2oS+s-j)yM^4@namp8t3Cvfq$U#O$_l=*SW^}mk$%TKZXrQ%br zALCEpYPxDZn7f;OiBup*4Y#D$u!*>!{LkQ3=w_zEj9<%f10D!VYb-xubdJN<5UZ#= zy&AB;YHo+j{2uMET0-pqr~Or7S{!U&Ag)^&6#sqOUwi&!vA=G7*kXSbAtLOrgXKtG z-DvyE!Vk+(#}XXHAKm_19&LY>PeMDMjQur1Ny5|AWX{&G1xEK-TkNm$J1`!7OE)^6 z$o}G*BkV7F5?mNy%VB^uiDQ7Ny8R^znfBM>TR{xlUkp6j{)z*i2Taodi(`L@TF=9C z+5$BOb`h+ko_U_BnRq!|H*2Il#MpErtAw#RC(;_DRMQ%(uVHvQU{8dfs^1uX#vGBL zPuYHEzPk;3i{Z1qC8&+Ew-_K9Tum4=L*K@;w#HWUS5HL=%{_{`0{uE37hW!M4jD1~>B9rW~*8h!Tk4avIHZl|g_s}Yi8}Oj` zPR1VFjIU$aV-5O$45i(;iQxaq`+v-jUf-NsN=jdM%?KtY?bg@Fw;~7Yn+f=crYqBu zOqnMA4&O$y(BU6U<6V4zmBm9&8Q)>RAaXtS%u4qq|LzYlfD3BRqp#84ZNHAw-g^0Uj0d;(_Xx&qZ@v8EX8HG`$`8w` zFkbl1#Kb=6C`2>hR5OeOT;nYEsC9Sk_pgOxSuwW87DEQit!j)L*sE@d9Mks;o?pr4 z=_+%5i#%0}cN?tb^>L>*e=ZbW0Zx7?;K%i-g@z)m)qak26AJH9cN!+P=%;$*CdVF> zp|Uq4L7vaVAc*(MQ56VMI~)7e7RoS1l0+%Ro3;NArhy?2QJZZ zSm;pzUD9(#G(96Rh!XlsT1a}@N_zf`1kiIb?b9AL{U+`6?Q9D@D~k+zt^l1TL%4lZ zbULDEQ*%jAKa`<_RY*9+_UVfnf7SMBW2GlF$e`ygq({*6Hl&fzPt@so0A(oQ$J2-& z7B%>9k??4LqO$11!||ws+|m{~pdED%O-y=POp6QTALf_w1(sKl5 zC}9c`%>GBnX@jUN`rlT1uv@s41RazU>V_QI|2W=3k7JhV#_wJCu zC`Yf?+7oHO|12y1loN6xN8-=%pB|8L9lxZ{8pi*(%GdE2V$Z9w)&6_nNQMzl-U=U(&zbLO|~x?+NMuF&6A-jQ>X?%Lg3_@_B2QJoAs?`KFC#iSR%z^}Osk;eAse+wwm4NYAu$?BKIQ>s|8KC&c zq5QmyTCkJu7|7$p{P&?g9F@x#=N+(Ls}9_Zi2{A7z2rGkkF93^UJu!+z82nP&HS39 z*G%tUE6Vb4pjypI?w!NcZgEhxyJWRHjB1mq+TBBBwY#|56iQ2=YFFVgX|v`Osu`EA zP|aCXGmdQGN`{o)levn~T*YpxVnd+dQ=^Jh0ykH$VmDX84fZ!6MOADzt4LD^ZqTdv zum!4ULsj%~_uj=-&{N6a7Is3@%kt9IKAzsSoW9mb?@*T40k%6;`8lqmQmUV+!Fw}- zAs1j<7o&BcC>>b{IW_P2BlXX0;LLugM9*)l=l?Jc)fZqN*rQB$SLcnx?tL;nYng}T zwB5!_Hr~=lsqTVZmXyD9%1%ql>qya66E2N8768*VmC@$#_-V!U_^(@kAsq3 zFw0DoN=5=GwG#4OoWIkak4s|o{BN@P2_2IF`8aj3poVHui+2XDkq`U$S_Ot}NBg4< zwO8<2k%};?cnwWmTJOXCW_Z7FGYp-A8eBKT%`}{&oAqcRqFU>nuo=V)*lO`E6Z)dS zupJV55ugM3wc$B-Y{DPnZLd6xAvBh#$Ls1txL2e;{BM3n++H-RK9qwz!mkzZ3zvme zwRpE#TS;Kpr?R%MdT?!nL%0{aUP1|P#Q2MK=AgreQ9#amgSj5!Np)@#Qj#fHx-poW zLV;jzD*a$q9L!ClP<3v40&Lmp+ztqiONiWk!g}^^3}1|&#`{g5Z8SzU9?sc$+qx##_Aq8GpPDUlTpvHbYJE#@n9FPCVWgaka}D8gJkD=w~|3NjC@Q9O#Yi zdZm(=;$4sgR2>y0j{c84l~Eo$nm=BXE|Qe5H0r8FkXl8ff%pF`1;73)ZEU{R1}g!ZmX_5 zq=xRIqeXhc+p?F{sbF&_U<7jry5 z4-Kx5$6G$MjmJTb8sqT_L^VH#N9`ID6MdLumY3k21ZW|BsC?lKMq_YjD3a}%hj6Gj zA_-$KE@I#@_@Nq%!HeVS#VoAAFev`?h&iJ|?~x;pYWIPP~(cU{T+kxbQaM3k$q=IcmUr#f10zBnv!M+TXAESf=sT3Gi%${r!C-%Lg3< zP-|a&oc*u63pB`wROEzQ$YF)0x7X4rc?d!uP%*_$d@W1H?~}_!%mGMu?wL;%BV*87F=wiXX6+5S=W3eBx)C_`zxn^5)K= zAN&R9mWTjCWkzVB5h^!Ai;U1?M(8OcwActeXM~m-p_h%&t48PzBecQ@y<>zbWT<-X zY6=-W>i^n@+U)n+K4N?bI;j6>A3+{zpbf4yMkSFGdJ{QvK29Y=f7j!B`>y!E{s*xS z&i%dizyE6=8kT;4?W6ZUY9DRTc+vm*uCeq#$_aHtj_iLN@1V!^@7MD8*9b$Yne}_h z33Wpbe;>VnRJX;L$?6N*Se$V(yX57R3kG2-O#8Sy0n56N>yEXL>(T9PdjBW({%!Rw zWUF4rUWanD69m?F_yeVcLS;t4$Z>8$b8Yheo@>!t7O#Np%fP=*qoKmwB{=reYw zqxW+yl+u2Zok-f(eHwGaxJ26bb(@<+VudKimhRU##~|?vG5I94yMP5I3?{LBupn{m zcap>_QHIJcM}j2rl-e&i@K_B4F}eLK9;(%&g3 zREivC3Qo;ayjYJ%w}0l=6ZYTx@HO|}q|bky{@ZkH4Cb^%EqePSKY}^8$o?DNqqjfG zaQh>naqVxRGRX8-*w@*{{_BKvPA(88nDvnWHvwKa)DGuqeZ?EW+-Y`>4V z|HdKmB-`(n8)J}om)U<`M}7o}6%b&es8Ew6zK=4L&>aa@| zDaY)uK?ldjqa;I!ZvU|-?!ULv5osO#UtZ}s*^ z8E$_hND@z}{a(=fZyeIxHkXw~wBP(2Vi1>W_TMeYk04Ihzju5iiTes=h`1Y&@GG_7 z``mxy5P6dAx8(X5B#t)w?|$S*kf`h5vfYxzdXym&??ZwlQSZNmq@jPeuC(-D%CYKS zjyIrx7oE8O8v1wIuhf6%V^Jxp|626#7UV}T$1nBo9bfD1k22i;NN8O9TPTg)e}(>y zL*&V}|L_08-ua(*-t#=ZqZxiW(ncLKyOeHTyDi{p z=TN&?2QPd|I#Sxz5`2XTzM~0#I)N7(nd!Z2xmBhCJhyLQXYaCUS!DxX@GdL76dopB z$_kcxmleEF*8kG7yv|BpSzZ>ROUv@QsC_#2-o+oc(?6cv6<>A3dm@(sNOz^Stbez% zA(xfqb*1mROW_(w!gc0mayjYWgLgptwV*bo9nMN^b|3OLS&i+_>byLP|tmWuk4X!9~Hc{JH1-s-zxu;3V zrd*eFBG_m1?7DY1RdMh)!z3uRDR{I)KV(=~*P$mWP_; zek^C6iOeW}nAHSbeEd@z%8&4eS!U*cf_&P~U4o=wK?N9!9a`S~2o~^mh&ppyV31`8 zb~7kKLu*grLwLk2B@7q84q+%*t@}$1{x?Ne=+QnLeMgUW;b?^(O-EGwVFhgrTWeQm z?lh|PmfVNE)4;G#WYy;~O-rv0;p+1Ga1a}*?Wvz*yu9_(y4X~N|E12{Z2+X5=B*Og z<}-;wONXd4cbWNf1H-n+{5M4Y;Lz=;OyU@RO%JTIV;x=@{>Gjnxfm>1O{@*Q0yc*h z;Xh*AZLc{JLUfB#_|?#H%9xJ-bd9I)KfH`lKiVIsI=3a9ajJ7$>Dz$piIYNgNY@-P zWOCC)Lb~|rD1I{Or#iPY{$=6crTEtc|GMH|H~hN{|GMK}p1IdWIQ`!65%#X(@AKU& zhNcu86!~-R&x5}Y@zB@H;_ve>l(uXRFXpJRIs7n3({%}*CqUEQ^MY&SYPIff9S8m( zJNz6w{46{C^Gx^`n((9b`H}m9wAAP2PsG;e2~Sdbv_7B1GpU))WimT86b@~6cK4-y> z*5@qOG4(mi_3u)j-|THbpBEsrA${I~^W*DtmjHh9`h3F~rarI1^X?XXK9}BIH}&}x zjvD&>7LFSFd?=z?dnk3g9&AXT4@T8c!4`c!i26CLKW;i{eeM*%;_CC=r*mb$g+71$ zAC1uG@BGg6`R@gv2z`F`3k~RV!r7+Jzjz)JYWnZiaMaM}l^iwn`D=(`Qce1N`BgFX zc@+HFcKDO+@Ncrizt)65+=L&k&mWuuq@_Mj_#YbYmnMq&+wz=N@HfST#nXE-XUsM- zl97>2GxXZO(HEDC44$EPR`NR2487A;9e|i!cy@Ii-jhdnJ zY+S({2kY?SM2K|VP4zD52pWW|@Cb>BMZMEqmC53mM!yXf^`66gIV;w5ou}q!>vdV& zlGD8l-Z1LgOWxIROSa72G40dq`h2}nS8DGht_xcs(Q|j6r~ms|NteYXIn%pfKadl2 z&5?Ck6n~~!*A%KtpUBs66M0O>bGjQ7d3{2Ti8@W>yVFFzf3deTHbyKKniWqCblBHvR^jN~Mw#~q=p-I?2V-3LUIVIJ{Y%}qX5f%3jVX+`I6EOon#D^MjbMD>_m6gl+ z_xLj5FRh_EneqzO7?m+@6LrJq-l;AmU%<}b#cPCM1aOgZFYkxC=%JqrxDw;!g0LtM7SOQ3;u>&T< zxb<;N27L;?A8d@t!O(W(grBhIg`P8uhv*zpxGHo3enM?2yea%<=q~!|gYc)}?fTT6 zfA#tDhM4+Z9;2c2G^vn%Oxn}vhiCED**a$NIFysiQ+Bv@x1vaADv3yP^qjp1=Iz<| zcRBv`#J_9JIsESpp9p<_@zZhi{k53C6Ax|r{s$bvQ1eps8Tu}crs>g-IoeT=u0mAm z``{H(`aUN7KiT2WvBST|4*zBo{;ekbXnlX=b|5YF{p4rj==)uq5kudP<&0SRzT)Wy z^!+DoqxJp1zcr-q&);+s`o1$`6-VDc{^SYt{X~gKY<+*jYC{L7^nR5qim&f4*6WI= z?~gszkiP%E%BU-~_Z+S(zP_(~;spA>T-Fs^-_N7EbbX(*m>TaC^?hgl_~i8cf!iR3 z|EKR+-*eCTb@hGmoG5+&<8hG`Q{NZJ@)mu6sZ3&h|MzDNWg59IA6wranBB0x9|vr7 zeXl)93f@?^fBbQZ8v6c0jvD%YI!6tCKM7H-J1PApCW`U&{TP8%gZjRwC?oa#KLOdU ziG{xJ29#OfXVTaKeebLKed+t1zcYRRd%-6{-}ipB0ew$6+w^_?Um=;MzOUt|q3^%s zsG;vSA}aO$*2`k*`zZKN+u@hn;m@_hpJKwlFA{#xQML>F_hXPw!Z}9u)8oJz{VX@8KaQ)B!nce=>ZxdRpl^-;he=v`4aL4tpxFOxR9~SQ+6m<-LZP>7u@6R&7#~r`o*#j&?;7)dipue{e<9pl( z#rzo5P2k(tiZAn{8*)s1QJ(qHUY0-0TAumgiY&jD`0>Y|1pNOlvhag)LT4h!2LAw+ zfBJMWcr3S;r<_nJa%|-p|37sV@E2IiGycWMv6W~1$H?--t>qd2g2?jaUjm=I4+!`d zTFX;TXc%%BKJLTEb^n@+OW-d2{g%7jslbZoF!8gbtypT0i}w>e>L}cXIUqe<%}?)` zz1~|=jk!ejT5k#1gyRSqpI07lqWnnv&>wObZzTNaEqM`{YOnN+wT^A@TX1I_!kY(I zmevQ7U!QeOR)uVTk6+E4r)2sS&pgOkcCh%nKDS)6Kynu7N>5ch}p*^`OR>>rCmAi!Dq zRg;WO47$E_qq3#=1yF-mIXrl#OyAyO5av`Qt1Jqnk#{Ld;h|ph` z=Ii6HOY)XHj#$>Z(oc(T$@s9?i;IJ~0_i{CIO_NS@5>Z|v>e>okuS4Dqe>3f(|s7F z2fd}A0YK@ezBP*GIF53K-uEsue%jT}(prVC5x>Wo`}XXp%o6Y`_1Eo-(C3quM1pd+*bWeI;=O?WYq2yyMD zv?JuHzz8#%A5pzThqtspVI|6U(#ubeEdR7wp5B#^y=92H4WcsXPMtHLn^%`^|s<)nuNeBQB?F*dkZ1`9HBKRW6E1TWQ=5P6lBiF zGRN|Uwn`?M``nPZD_G|6TrXrUtG@JL@r@Zri%-*~t~`)moB)Y~6oI;Ye4e9A+@*Na zaY8x7bX4g<-$&l%fXsr4ngjVal2jN)m!qhZU%i}L5znNsywUABe?b{T-tITslcbZH zIuq|HcRE}j{B^hOO);bOZ`V7uFQY4C^ee3XrCs&_Oc#o9($Izl$>JdD)BYWl}=>8j|E*U zyc8VsF7Jc;#|w^!=A(e*590Fn-!)C%Cpiw?#_dXD3H`0u^^I+O9*&VDN%;UW>CRdg zZ1N5U`(U3We21T8`oRk~k5racDE*xHJ#Rp|r?jH1pT~Dc;DK`k{mxeUq+l(m&)NLJ zygyKd`1AkwSjOSGl?jMqjNAWG;H`5J3_N%?{;%$nnh=~vA^Ho>`vXyfzsPT@=bcSC zWAIiL;~aj7@h5zf8DCH1@J;&vjc<|-UtwPv{vrQaXjleJj`hA_;=O{k#QU+#HfOpg z!RNKunkC{x+AUI$FFwZq!>`M#A40d``}Zb=TTdv*uvM0S&gr<)eZt)OISIad6xe@f z;k`nTIi%M~=qX zc%?d+SO~in$s++ChJ?21P4;oM+}BL-h@8`?#CGw4mPrxtCtZzbC`nL^@7Dnh@ih>q z2GWpV(C^>d5tOFDcar)pSJ?5K!}Tk2`gjvC!loXYgz;m2<7I)M?xPm-s=l# zi0p+-Ej3VAFN+m#Me9wtA`EhKJ+&834AILFP1RZY-NivyvbR5V zB+#CO5M6Mr4BWkv!b-u;B`NsTnjcO`c!oF*W|dZWOUJlCfcQ`y;#^q)o+-n2X?f^m z>ablw=REp4=vs*X1>`b>=f*q$*6M(^`>i~?t3%Ujva0(Hc132E&NeqQ@bSS$dlxGPFHxQK; zohazaG+=Z@i0%f(a8=rdp-`Rl?nM&YY^grF2s}yWol8&(ULWD^6<(l|I zs%bUXv{`$#9Z|d)XbL!wNytIhB82EUQtdyGrX^1y#)wlhgh8?2#YG9$QxeuV0c+)C zlm$Qn0Dq$>sq`UThzk{@18z&EA)vhpe`Bk-6muG zEIsI&V^l$<=(LK`t%T}0x1GHXJTeYSxH#3ZTh{S}r~}~XdnuCn2GAUYCQ6a~7f8XV zh!kB6c1m^(BOp>FH?v66cR|-s15Ob_kFiKz&rm2whq7Bj@fxU5X{o#IZ=W|@*N3bfrLO~86XfKT&bxH z+pOIwa@2u4F+az}n_~DV*r^Sr45ap&%Y7YZF|=Gx+O2pNumn=lC7iU4lBkI(sU0Wn zQb&3yl^R8JBT4D%H+S-D<@g$U@<@F>B8+gTA2dZdV$7e=5wfB0O+cJD>);Jn5$CoH z(MG+9&*A*Tw3W~0eHYW>4rLp09x_7zJ*vZ_!Nw7K*ogPy zcuylfk>lTj6jA>uj!!n?3psx5FlLBVJmhP`&l5(?M=#^xPu0pY6wvZy+S?*6mD2tw z(_Rp1<0!42O#6#Sn@nlVWZG29+7~iyxJaY^p{22S*uW~@mM2m6vXXB%quKD#7pDCqC&WHA&fbhDOl77Mb{Yn~4G zeOf@V;(i?3ztVNCFkQbhGwSn@FHnM*?N#sUxcoX$1f27WNUv zYk@EH=w_{d6#USf2xgGh#FSGo!wUZgI2~F?#iuPy5gK%}7CzI0Ke1C&7(QN5Cf){F ze;OPZh(t63#tMrD-K;$y1tcFp!~lki9(7QpLQB_qOAkSxAW?@RH0Wk+Y!sB^vjK(8 zQ417Zi9Tb8A~fh`t*sRbS##P7K*6-WAPKw?0wd0cHKjJVW3M4Nn96!fdy=k`Z7fvk zW-XX(;SmeucmdKBNYeVM z%K}u_q-~>su9zk0$F;`EMi1h{;X-881U?^im};1mWEYuZXe6B-x)xp0I+7jN#j1f1 zdPdihTk(NkF{56mPQ3`rslY6xWyK+uH}Kv;(YNXsZ7 z{{oOGp(G_rLirU*yA(=7g>)C7tZx;G%54NCQYdvby3h`Vbts^eTA{E|p3elu5y}Z+ zt56nS#4Rw=xsnvBmJL`+FGHOrK}SJeL#?0?CP0579NuKiS8DwrDRhsq{w3n=EOGjl z*80T!PQaQR5y$4NEq`G|oDQ^Ezki>U8L#h(`bI^>n_A*j|Er|TWO<5@jEILxnOX9u zBMwi+DlO0s9RjPirPCoEn5O^y_quxAV)-@RyhZBWGWEZd`ce2l;q~Ef^@+Ir#e41_ zi5Hx)qOFcf&-X5Kt#I^DD5>z4d`zPpH#7#0)H?8<45gWrdU{0}OHB!#2Yibn;r2ZL z)SgT_QlDMrEg45*y~?dNhisL!K}4I&Ich&=@eZvRVP%av+*A2=lYpnK>cG%b;zlW~ zeKu2@!ix~Esk8k@qWLJQl*^=82SJx0T(rTn>+0#VyV00Pqv2Zn4ipCy2K)zh7RI`q zoBQy@Zp2?8%jn#*=ONdbPV{Mh4AAs7iGp5}&!fYBWEQrsWy#y1&k#-<-!DPCv&W(O z5IGH&%k@PSoFUL%B{IV-$c$xV#!6)7NMy!IWPSu#+*idcco$#Q>wGA!1AKpPnvW#} zu5uP%?7zzC@c&$|WO+*tQWm0F>y)nl2<<{lZLWN#UEl>ft3od$Uh9YLIou;iqOZEP z^_DD026i0RaHcS;hkL^BC{;R*9cbKz{JguQuHFw=&UBwkdleC_-3&)U0P8Xkwlr*V zppZ?5^!*q}PU)xKk{i*4%6eKU*ZzD4;8f$uO&l4{uJ$gt7Fj}?z_Wakqyief(LL2X zXQ0=sYBOq%6*lR@JA(=DvUGLO`9097^#Ei*ETPMc{cGInKpMYF+90N#%~{3U)u~Pu zCC@-K&~?7rltBMP2oyMhTt2nfMl2{OYovJ~Rvrjm0q{9P&2{oK&Ul~O;VpR#>B=Q) zGhIw@)|o&RIRpK`EQeC)^p@b_T37^bL7LLcH$3Yg%=vJdfCMngV5hGKl7h}2ASwax zlxI(M-`iRByc+2BCZ(W#_Q)aKw~n77to2sr5(dRYC^L=oENbJkwWW1YU*4ibph=R))enY z|4+#V04z%;-P@Y`bI`ef(M5q(#Xkan+Gog5Pto!SY=1X#9u9N~>Kz{5A52w8JpxH8 z#};As2l!xAdTKVHf$2&2wgmn`XF0<|_Q%K`fbA1xE7P4yo~Lvj9+CBVz(YPc{>;6o z0aa1kRbhaoMhj3H4@rX4-jcVGBL-RAO%d))_h*VcaiAtCTE2gGmor1gN-g z4B;sy%(?kOD;ZPB7q_x{MrlQ0FeIlO$$E1)38}FP`Cc{(g#VGXG4u~$l2sAH^%D~G zXnaNNc?FH4y?8&)yG)iTPa%a=!O&vlv?ra5VhgC)JSujFELKb@lPRUOOu2S>xXX$8ojq2LEnHsLR|pHP}L3f{;52y3!QX!vw&l%-+CS!oUn;u?2g zaD8^oeeKojoT_sk)-TV52&O@l(rKMsIXrp{Ye5W1kJ0*~v#kI)oRpIRO-;my7bqW@ z?FRr(e-%B(rX1F0qwVNjQmo4re=C;X^>uSy?F}2wEqopCLuAT#IZ2kVkCLM})V^+c zy}rE8w>Lnxhj+z&%>v0RDGCl*_w_*vr@G;5QF>7M(KjS(V>m^QPySqtA5+~V{#)+N zz{sW!a;lz(A=;jDUr6o7OUnm++iI-^@7p>TO{TQi`^RB#aU#a#B=mGgE%=+N4JSlsGrI|r)!|fJO}-S{+HQZf6Ccrr zqIS`EY^oejDSp*5M4_7{^WYYueGCHcfgD06#={2G${#?uN;`}`4O!B1|Araxu(zhs zI?^|yfjZ+f1Ws9B0(2F}ii*>r)~&e1WrosLRCxL)yfqD>3FQ&_9?A;3-oXF#7j&)A zLwQNhan@2j!S%8cdesO~nK{iMj|YG-o{e6)7!fipFyygm!BEAT@q{H@@~yAYUGSpE zb`*1Rao7%IuY6ob8Sa&Hhzz-=`Og|O1l|(l!ZM?r3O^?cC-KICGf^)4>8sNTNZ=E6<|Ag3>f)LfL z9b98Un^AP8Pb9RzJH#}C9#;w4QRmVeghBFR6u|-`BFSH)aD`cTk)?3CD7=&lQ)D3( z{);FabUg+33B(>lX!B;|-Rc#MK?$g41in!kE(FkSAP_?Twg{oay;^$KF(?$Gr z6+hj?&t>ALyZFhYpP=gu3eVxf6iLRuk+$ssL&i$C zQy1MFgy>@HpsNre!P)^bv~n6jaTOUELygb~BQ(kgjWt5!jL<|QG|31}HbOolG|dRj zFhX;TP>B&LGeQfEP`MFWWP~0wLQff?#YX5kBec{Ay=;VDH9~I~p%q5x9V1kshtN1I zw$Ah%Bn4fo^$gcqBecN?)fl18MrfN6+G&J#8KK=qNHaovjZm!-I$(tAjLY9-?p!7p4e5 z`!+%pURjHIudaB8zH1r5WyV;1rgf5qD^4>DFZhB`lUhn^ljL=g?Xt4}USn@nVP;t~AL06j2D;J?Kk(F9HLR6zRc$Ecl$_H|j2=Np_B`LLq z7Clua^we`G5_D0d=xY=X*BZ_El%?=vqVUUHm?G~`;rB%0plc}@D2RU!p;}UEf5y5W znGlo!Oh(~LD7BXn(-w%&;9z!ef^zys3~N-77`3uS;4PT88aV=L1wwq}&9xRGk-I^L zs$De{nxJ&sjNhPZn^9t?5!z*hb{iqh2<~E( zyM|Jz+BHo4Tqk~pi=PqV=X&vTgZLRKex}e*(6!6p3lUVoeA$fGs8n1y|o!jnbe5-v=UOww(~zGKMXEJioUS%mod ze^*C2_JOl9v~nRqadkE_x*DM#MyQt&$}vLuMySvT4KPAQMrf!J8exP+8KJR8Xq*w6 zXoMyip~*(bXN0C1p&3SKju9#`LS;s1p%E%KLW_*hV@BvHBeYl#nYt|vNkP|hdWLJM z5qjAOy=sKsFhVPg&^tz`!U(N4LTioC1|w8sgf<(YZANIP5!xj~)vnzX(*AgwMYnNF zJJa`KtkbrH4f*S85vk5Xqb76yD5*DZ)>B&JcxHCWu^;K+TOs zL_=N4jB)r(8)QNrV1ZmHzFo+WDKg?K6h6-^Jk(OSNE9yP!W7A*!awc7@)5m%NY-}2 zE}A3-3w9@9u)?G_SfGtXC|IzU^h~gzmIkq4!2#~GZ|}gG#Z=t1=v>J&sF8;c*RLci zJ?NZIV34Qvr8F{9DGlFJS{F(q!vSdoozmJ<8mTO#kxfKtEhvo!N2F2ply(S@t*xYi z0cnH_rF~5gu#NPrr1nBGX~9|1!n@#1;d` zpF%t|1B(f)={QNDq}!0B-928eLg}kL<>g`>0Y2JT;q{GH2fMM9GAQd{I89$q=9ON# zK6???YvjW0g;fc_}JtLG{iM=~#!MwU=*^>!zin@knOp&nhbP=q*^w z`!kpmqTLQkDW#M-ltPS?t9#9~_f%{Y?b9yA=8a_Gikt^+{?YJ1lq@Muex@@-2#9nd z??TB>QoEB6uzzG% z^hkUFPl4yWYb^0Q#`z!N&q1&C!aMRtkM`6jkkjrH(ARhy25*0st}DJ>?VS%MVCgc! z5g1s5#jqt5*Z%Yqmeg>?^iAR7 zLM-|!`!pPzqM;D3KT54QhJ(gW8OIxErL>+Y7t>|^v_&utY~X#O#L#2}Rq?v&Q2xgE zA`sz?wh|B=Oo+F))#g^LHS&HXFvKUQhz>9~wa!6>Kt9-*vcN`!{4( zsj`#w&(Q!haD(>u+r-vWjZziPxV=fKJrxJW zF$sj?k_Xt`R2;=;pbaQn$}@4oENzS@K~gjc+J~D^Gc}cx*pvNyy(Ldl`gK@Dl?`0Y z`Do-b0ZB>JJJ02!cb1bYcG=CMJrsN@a1q~Z^lhN(Wd-_3R860(;ZJ;T>CN#I_!Pf6 z7NDc+5p$<5`Y% zQ#^CwWmwR5D0H zBIHBJ#-w19E*EpIXTGFDF79$#WP%Bi`<)nwlHZLG4Kk+!ssCZo7-EVBQ!M!7qf^|e zLJ#UbNI3*0p*qKdf2U(NDG5BQDi5_lqS3p=`8m`v9;Ae#|-%f#`O6{a{gcoxV-ftL$!R#U@ZN1&$R42NXB0L8Vf89IWWknu@PA@CM zfg*@WQug}dS0Fn$)Aq1hYcHcrg#38#CJF&)rVV%@#t-G@1sG+54@C0M-}Kv1AgiLN)I@n z378$>`Jnb7z=_E7CC3tr?5%J*Ax@%%avbzu7L(~`DDGfW6iZM?JJmv`GQz1`<5Wkx z)k3#2!mV6`{Y8(z!jrwF_?0eRg-ix z`VwSw%23oyZV??xE{1{)MHq2=u|P6DrEnnc4qex(aulXx}@$O#cIZPu8L#kP&2KCnsK73b;V64C;U&8 zRh?Sm_}h^tWQtdU!;Xy@C1KChU+JAsJq@I$P>0p&r=bH>Zyti?43fV=3uTo$3BC-~ z86T*3COD3us*C60|0BD$S&5ltLdIkRhFaopxqe$3bt$Uln}Q`pV; z_}4heInQTfad%Xf{k;+@{!;Ct_oPHZpl2d%7BDhZnNHpjq4q$O5T+?ZXfR>j*GZ#z zn2tu%5-oWqBzQ~uQkIjHCQTDk_$oIsti{&P#5jw7J_~IxM?o9EF*D5ZodfKft8FB~ z7`5$Ow=w*4AYf(qZp00Si}{S?_c*w-34XU$)&S?^j87at`&y`H$a)4QWY>tv0Ia<& zroRg7bDjGc#QjRa{oC4LKIfJBE0I z(mF82sSI~2doq5^*yKNwIJ;>$v)uoklU%5V9Sat4DV(GphPw)YmeGN?7lFEP@h6}v zUzN@(a0zV_$%TKToZfrf393Wck+Dj2oYGVbdthg!@}HFmB~*Ud1gaLQD_MIPA3={K zLx-RxNn12?e2kpbKtCrHQ1&E3pZi*)Og_3%zIs})fT&4Onv=`nc7%|XqO6kb!|i!d z5KB-fl7X&7?YRZLX(BlqrcjT{-X9py(oMj9bcaB)@?*@8HWh0S2QIK9sKU+>FKtk_TFIv2pB$L2OrOkcalVF*p@z;9!c~t< zh~TU%&{@fX_&nnirHc6(cKW}^peYT%Hp&?oA5G?BDXrf%ERI`1jy!rhs~d*#BGI4p zfdw70b!4+7;Y;e_RZjm84muCc2E(c(RyE3YM{V{RUkVe@L)cJCHby5Bpfw->CPYR} zAk&XPsuEHlPxoOG!x*bWe2BRSpqfhYx?cb~X?IqNxs&hct*my4pn@>CDCz8{U$i@fF%J4Yd?0wQ{~p z+RH83)tuM`?G*&OPzJuR89}9vkb-F#7_fj?HdOh5HJ;Am%jGwM#X}LxpgDR`<#$a2 zGhTFNZ&Qx=F0+i+vFh8#%~Paor>`rqV1U|YEO2Ij?mM4cjV=?( z;YPR~;~n(Zc0+%qgd5afhx+k&Mf%H_2UQ^r{E0OWf~Gg*QJ)W4Mojpg1;zA7@M4P<_`x;)nSR7w=md{m| z1ly6XR2_2xq35xrjz-EZMlMlY;hck|3 zZ}HBbiyXCe##W{7Pz@cZAYcCM<4Wz+CQ6+n=$-c%;<%lr^e_0u*vq%oWPL~UWYuTX zcuQdzCOCee3$5T=o!~f>b+8i_p1h^N1u?WE)7KMIMtqa3bYiD`V_`8#&QE^sgvmwr z_fs!We$My_*BjkMao=`k+Yd}VHXdNjX|1xceq0H<^BsFKYKm{m_#Tyf2cW)}sj#)^ ze|sFRcYvSZ9kTN2b1nj5*nlkE$V-BMFUmNf!RdxA?N$10s*@1#maZngAtU@QvH*cw zK?|S=&PRmj>FYlXcK~ufq++ymL%7~W#SjS{fT6=Eh$^W0V77=tSBpa82CsKfkmqY? zYpyM*;9vv{4uYw*p`v8&5X>?ZMPn1jms|zmSrc&fP+Q{^2EI!UjUv;^ncjiKV$6Th z!X$q-My~6QJ(F|gz^2Rl??zr)6#Po^^3`RHmY3xy8b@B9LRLI^nM0p7EH5{rNF(Is z3MzIYdHI7V)EIgBwp(m@IiA;`y!`Q-Sn~2XfW?xR?BB)s@@b!6NnT#-+Gu%MfTD5a zWfHRD$x9J^*08)>j3SMYmouo?iR2~Jg$p%CUY1eO7~{)_xedxo{a3N%Jw8{+jad*Tvbg&OCb|xr&IxBs;(tT&iB9-TZ!9J-X zmPTcQebPm&qloeHsCStsv1~w-9?;JIx)#N?esRH-vK|9rKFVoLK}S$OHBWx|%BrB) zZwO+)0e{swN%+?i|5~9StGZy0s6I!`PIJqe~fPZI5- zUXAmVJ{S8I`Bx|7?zGXNmp%hyr~#g(03@ibt%Bc2f4vT58U2-xjQoE3D;@tZJ5rVB3yfXfA&-2#B$qZHEY+N_zkc$m06#DSe1yECzs7*ysQx-t)({?VBfyS7 z`s<1L6SKc2OYjTr;3M#F41Z$w*Ee?v{#+aZJ_3H@_!G0g4v{sihhvl88Sxv@UoXBq zUVkON#pSPb=2B6O?|*c1;`XlX4OC zF0`%Erw1uqq2v0bQ7Tj{Nnfx}M-gKk7wm&`PFO6O;C;`NI6)cEr(&9%NWP>_mwL^0#p z2i|YM1<#rAc2lu;>=>=Y|?@b1JOXg-`kGaYIoc?vAUk}TGH zv-f!CK^@b!SB_&9))H~IcPE7L-~(3trrv{9+KaP~`!cXfnCz%_)X^m+CF}74in_7< z=_H&UJD(mSc!Tf%Ev@&R>#s*4tR&IeVj))Z$-{saSoxNfsVI!i%R8La&W^O5E?!;0 z=9AcObF8I`?^^&kU9*o*>CCVeSYgo;sRdp;f`>G{{*As&AWVC)$;w*CVMi^a4CgX| zGQL#c1^3N)^8JMD<6i%rU{|7#-5LrVb=lQZZs1z8thHKdd=cM%d6KQNWcZ=aEl%Qpigsjr(7)(^xpAAIhY_nXQoPq4y#TiilhIcp%mB2{b=S77{Jq zWeH`DP!&?%!sP=Ap*)1B{!W007a4qgX`P*K#3#b*GHM+kIX;9>w72956sId~7T^n1 z^*wapzzxN`Fve-E%~J&Q8EupTJTaYam89 z;kd)RbtN(`=ev*)KDXgN;=OMa9E>*l(jXVUEM*@HvJ=l|?4t(MR;RHzr9ni8Ktz)u zqI9LXTlZ&I_LY(MZrpsjRb3yC6*T0G_9O*vC6PYDbwlA2t2IdbQ_6?X>9ptE8GE2z zGmiR?dg1t(dS3^%CEhN8*Fh2XAbWr=xLpIICcr2SdtJDfpZJaX=zs|?2vL0=SzlA? z6UudNfD$c^yt?+Ftxfn1;&~IbW%wna@@v<>@Ek#Jtoo-lqW;qQV)8Ghi_r?Ng*15J zCT#NLTs)Y^JQ)F=To#ijR(|XwjvT=qj?@CNF!~lZ!Gj~q%>F5O)5h@FkK~U9zZFA) z-=qyEgCE2w3cni~!0)PGf}dvu@Y}Hdm*V#p=6><`_dIUCKV|-L`(~aLevtVn{xT)`Dpyz$l1rP8x@N zm7N(i81Tveu=uq7`uRAF_#PhOkRLkK?l8T|Q|8b|Q`i?L^ZoK)`2Bsq@cZu3--AEd z4%jF7$#qU~RN?#^b` ziy+SbP=^Q~iA0O|(Ro$1Wgx#vy{rcEh*KMCE2X7S@nZj(9N{`=SNbkN+3V@dN)NS1 zV4_>S$DRFy?`|ArN>Yp5%3!xL*-eK9u6L3X-U!1h1PJNxO{gh>A~%jj-J#x}q@wg~ z9#Z>cq^kEMtM|K=i5|E_{Xh2JJU*)Gj2oXx1{h(=1c^0jl&DdOL=1|S5tM|GfNTa? zRnS^7MWu=`L!#_X0$i_y)U9r{SZ!TuU0PXGih*E;O$-=;um}j&I}U0Tgn&qX-{&m% z-nnxp8IZnz{NC5kM>6-`bDsS<%X6M{jxsSF9WrH`;@Yq@jkBtR=4e#uvSo(tXh(;mb=x79(6P@fa7%Kevfh!xF?Q9 zKuvW}UAm!)3YMp1c@1o;*xvwl)QNOlqX*oReSv4}EFc!gdqB8{_$Ho_XbXP90;nW2 zyq_?z9R;dcd9VjFy`)0EyDc3aFwNPCEU4O3E- z2H)DUbI2p%Tj|-_N>rwH9e17Z5hC12l^O3KSqa` zoqu`8ktRxioY`d~(L3xNEYWCeqQ5>yAZ9OEVGxY?idqwr8Sx`!HUix)V=F0s!lxVjsH*qh~hj>57{)XfFchP`E$+cH;3t$hU|hgCRxIDtHfoa?jKDq zQo33yt!4V#N|>ZB>VLR?b>wjj=b)p~^()&0xC5)l8Ywq5Qf3RG#^(o|(;IP%Ua*V>r?B(d(19QK*&Y|+ZhLyUdUVlvo)O#(g!;l zM`#${GTiW%8k6`gL4GpRzD^&J)&hU0Tj^u`33+zbso?&?Ml56{$Y%syn-A$CWn2m5 z_#@^^X=n*p+oK>0Ev*9vIU`-ZPs=W*?ve!bJsGvcq5Xy(>H{{JnP7#0_!Ocr^Vu66 zn}Le`B72eJkFp%Jcn0Mz%D3}SBZ$9lZPR`e&50zrGqyD(d6K@UA*EJvl1n`s=n|>x zK8Zqf2np0xZbBl*H^Z~L149oCB6>k|d9o3Wq7jXNG+wBn+ap>VU8?`ms!t$Ad{qhH*T7(Wns3~ z?tn2rA-}*tf@PCN`n9L|8V2ab7##4Duix~mxcc1Qa#f~*e3)8)usulmk+!Af`~(f) zK<(_lIiKZJE1EA_mZEI+{ZK}svrL#AO@#V_+kBYVAd$ruc`zbO4pOk1)%=xKhduT9 zk7|kWw@--wCK?^}hcxq*&v8dEb`#J>$Z24I8ra{AeH{(H!(hMBw+)dvhy{tjEXzW{ z%nWv_1%Z}z4*VPh*uSs@s|+4qVU_{$Pb0ouCVD^6qk;Uc9eVj6Ftq5>5fY{WSYPF1 z=%B%;Dc^#K8jL(I!CTS+4t#?2T|@>A;)a>D@oQzX1{|7+KDUbBcUWl<;wQ%>@ek6i54YJ$>pS*eU{4_VG+>wbPMy6!Oc<#d zvf=jy>``XToc@o*f2#QPG{bKeuzQ!&>G#y=&|i?4vl3l4{z7=J$axq$>7=nAlD<GuDTfnRPDlpE`Rx`bA=Sw~xV1Vn6lxA8ZEy z{142$6aMD?a|T8`RLb~I?ti9Tdp*z)<{=`Web@>E>icM?^R}KCI8LFytUG#ZnnSSR zHQl#G4U|jUu}Am=-yel;iuy5hVAgJG@B1cy$VB)DjYuAlc1Ih{hNZOK+4lp+qd)kz zmQj05gU}v8(jCb1s?Vt-cMGvpFL|gc2Vy(HCQD?A?K|^3*aOq>$t5-etY8`w*L;j^}Ld38u>~@f|HI zVvJB{3pD6(wMTcy2s54T(?MZ`lTE`?GspAa!;qU8vFR%&;hVHatGvC-NxVr3d>{?K z5{SafP#DI9Us4`JnSK$oZ4KJ;lv>pnLNN8Xx8ffd9jkY_u~j%H*yvT(m0jlhXi^TA z)X&CoU9@I89l^-T*HDT^F4H^E?lhr%3K(F0CJ$<(Xa$rugR&3kln9Ks5qqZed#67a zL*CK8&3yY;F90AbPMUjt?EC|^zKs&m9ULLeM+#izd=zsCYHT`P z27366q?!#6LsJodN>fvL6f)b_80y}^A5f=4L1kUBc73{YH7E*DVnCL6aR=-?OjRah zc_0t-z%-UnK?~OM{D>?CXA#tcdShY*d)9sj0_gIRv}iS4oPvWii6J>@pE$u;YV;pi z7X6T|m(0~|h}#d%QA>PB{8vCice&wAzK^^Wm1H?X-M$}`b(1<{NuxcMGt@H!W4(({ zkD-w7N*D1l*h?-@(;mBAvY0^x1gjKo-)H_b_#AYZsh#O%Mnc=$auqDhyQ2%G{#lDh z^-(v2ll>IWknl4lle{4%)Xb)HMal>IYNwp*+yuivvvN~;cBqe2=|FW|KmqfE+-M6Y zUL0sIUn~Vlv%*e*n@JzG=^qgA+FydnXU5w`E3eYfiyaoiMJC(tUZl&u*<-q3hd7$? z5@myPb?xggLjSj>#UA+Qu2Bm8kAawgf#hTU&-=Y8&L{Wb$J_Dsa*xs^lt{EB)ZNT&LKx zNqHZs(a%i&>zG&Mh}=QSVEG}y%M2p=Fnea}agp{+S0pjObVFg(qj{*YxX>~`_AcK9 zhG~jSBV2bR9dym>V9bw4j0z2P^C=c>GR81t0#=S{Lo<*fL&mvf$|Z0<4>=$H^2ITB zUj<$`6!fhFDP@J+LW#`t~b*kiY}7+W%xjLy3#=|E?ugeMawN% zMZp16SV&0|D>F6NQzU>$`&G+5Sdvjkr_;G#4|uTBqTGd=>Csgj{-}|FKMdg04DekD zJOSoNtl-J_^?W0~R>aqh_*xNPH;P{|qJTWDe_%zGHu$IG0vu?i3C5a*wy-T3LBF3H zo3P!Q;&(M9JTg7)n$M~6^6)4$iy?B#`M;oEgkX4$4p3~ig4<}=B9Hd-PEk1%5hSVs z>~aANNtEW3GdVmW533uvIf+8jDGVqM`5=hZ@CKKr?h+7r0NYX?MOhUTh96M*sn~eI z$mc;D@J|$zGAZCE;DEgs)vJz83WH2MO4u59m*EvS2dlo5TXyX)j?v zgNUM;L%>%38J@}Y$JCd}ZNUjx)WGX7@53e!rVELk*#qz=DF5LJmcLJ`$$&4~3sCZZ zf&b1uV&lILGDV)G|6VIAONme;^*e`Jb{@!xe)r%C$n2U|~p|4zZ)8NCf_ z-hV&%=PB^t)@FRo`|rS5d@cCzKczHG`mp!kz1UuGs{Qw^Ehpx`O($6XEh#q1U3`A@ zevGRzj??p_t@+dIE#DKY_in&?@0L@)-n*Qwm^8EA`{j?g?hrkP?Rsw-GRRrT*|grv z^V4Ocd@F}Sr-ygs%5w1&bLN=-|xSV5;TKn-gKF!j*ajit-0 zPUJb$*?HoHWqtd{gN;VMLv5KU-{HUy!ag~(7;Ce9hc{l1aX*%SL>;v`Uty`NYLl;! zR6ao&07!&AV-YqpzPvuE@nsZ?L1A{AbaF#k;=qrnrKk917o^ATN=T%cC_-tUaH ze?DK!r8z1VHZaK@6#5fJb-j5O+j5$pufw@)N3cTxV`}UVaI5Wn8@&~)Xm-f=2lS+$ zSo8^{55L!R=GYofQ}OVpw9IUA>Aw9x7W zYk)2^}s0k+@CSgNX6Z*_qa-x^zn!<8ao6~KjbP98w3vd zuk;9RHgl^DIg!+d`L zkq0RKL!PTcu5#yEEEEoBsAtf;%7%}QfDpNo2GIN??GT_C1kG<~)d{TrHj5S_(;{?Q zqMQ#9bCYhSI?z`JFaR5NYtZC`50uiGz)*YA^FTo7fQrKDxGu6(X%qPnuZ;2k`*cuJ zl{Co^Q`ThdP zi3}%Zo5w0(#k?>xG91DZNI&Dd23{V9IrtL`dAk7kt}c+V9Cy<5FWN8ArmTSN7dVe% zK;67c4l%RF9(qK_Wg?s^mrYVj>^20=-3~eCwM6{vujO zmOnxF+0u$D-w$wLBX$I|!Av5v>_WObe{Q_x} zuf5}WYz#KG!DTIU=w@cwxloO*gE7ZdK1N3^ z7=qbQdnNMANv=+RCSo#eFv24KAz&8WriMPU>rajF%@jDcj^l*t|K%6fzwH;+?-XAj z@@gi({Ji-5a%yjRe)%7dIgnqj9*-HYP=CzVJ1M^$hn2z6DXoH{!l8#*&%DC7!8><1 zz~E?QmS4UF`Q;Jb39$Tf??ZF&wF`QYoLYZZ9GZ72Eo(qCq1?Md!$&wm69NeMJ%%y> z%ok7)AAIO3SZ+HJ>Z4TiPG4y&7SV|{jcHUP zRcWpCj8?FCD!XXPhe~kDNqT7-X+Y;gsC1zVqbbmsDsoh*zouT4`7bJV+eF&~Dc4-D zK7k|iQgswq1LoA$;bPWCqPdHlfx=w>A@qJR)nG=%S>5A}1~@@Y72ZMK>_Q|a33ei$ zOM8{xGa*J6(-^`0pEX}vb>gZ;o*@rDsuSd!Z{65P4f&(-IpOd=3T~oFV`~CD-+bdy z(Ckt~t(&a1%o8G6Ur+tSz|kRVcCyxV>G|r-;ENz9IwmvTf-lK@^uO}dYMO)5lnKF8 zQzlSnlNp>wu^ZDrWdH#3eeDrVjH4~p$!0xR@N{A^w!S&Z%@Zn}zi{*3c z@ro>R;Nu-=h6K;l;|j^>7x#0Je(e&{zmX~PHtPP5N!_14Bed2s{m)8}dOkYSuPI0Z z`=uTXJv{!nv?GU2`W5X7*+imza_Rs@)3h8_V{+2Xp?79wCU`9WIzfKBfOoGIp1J7} z@Tj$9*#gM@mg^xK8xy5KKO(d9$p#Qfg&K%=bP=xkCiw=QHss5ArvlVpqA<^*CxJn5 zU7WlrUzH%^{KWX30Vpx@i{sZWf4y-J`lsgd*Z(Bb^lkFj&0Np4G>(uk*%y~CB-(8L z`V|rh1wRwX^;|246UtxzMCRd(4kUr{mYSMqnNLEALgxH+(dd4SM)zyh=w2GiEQSa8 z!YqNk2S!18V4@fF9+SX62qa*#s~gP&CZov*Ua}904_FNgl2M z0W77Ry_f&+_9-U1@JsUB$5MX#k_7qf`KLO+{Vj#&i%v`@PF@mN%c}fe+!=yxsYMvxA`_kg*% zuKlB-Uz>bLvL`!n8@Z7dTbf9?5=eR+aFK*hHGi}}Ad&qxEHL=viI|kv)g4;xLADgi zyc+p4bi)Ol1s0_o&e`H?gu^uI4nrj^OsdTwoC$=n=7e~FO};vtkGavX!d@e5(eD|R zPPKe?ShD%e&2jCcTs|~owAlNX^(SoalYlAK=}h)+Mtc*`>I|Q1e)l(%oxgj&VGpOm z-e(amlf9dn^aB?$Df#USdZiU1zoi4hs1xX+V4l2$^M@O?57%EW;6KG zfUjl$x;rN4T;?mrwjC zq&sMQ)Ezs>O#?`?P|pJ`iCX&7?uFUC&59#arn=E`PQj-wmB@K4#Q?k#c?%Dd(3X&9X}1ce7Y33m7&bn? z{N?%PM3Sd}pdTpC@-8{EROtZJlL_@ibH+K=gJ>p%1$3Ky^GUQuFY?W)5V3}W=;F0k zrh_e!Z_d_F66Kp;*<8N)udt@U^39i=j?l1)K%Sh8@E7PyD&PE--ONZ@$~SMA#>jq> zud&mL2A8*p2ZR02gQ--NM&r@f+(+zXJzsy@c}5m$aodTZNvNGJkH*bVn`&`8A(^z9csP{CSWB zchKjr&Ey9aCzT(h=bv}mRI21oWXo{FC#Hz`C>FYywM7>T{}%bf*~nMM<}fvt4lAkg z=O8~CYg4#r9IH|VF0AHVsfU9}Qv5c1;vaKbzAEL1h_QA{le`nysnipDi(m!_WlEaZnGv8 zT|+GK{Ls!wKF3NBC7=17#?yMa>{auwng&k6=$?Z3OoK9R%P`4+g95Doj2 zC|@#^PPaX}v^Qi;ky2>nuPV-NRn+vyE{S6%fFiK-EQna7pWjH?6cXT120%*HiY>3+ z$kWfMFj7c}fOek?(pmVJ^1*Ln`R8ZB68W|joDZUAQ2zPKva4`IW9Bf%H-(zG7+4d! zPl6F~S9#tQQqk%BAboa!4U#0}nnuGz4$AjGDSUo2K4h>a!Z*T%Zx-;iq>oGq*U$$< zi2QR3suQOq7RXK;#xm7mYDO+7DLh_(x@&TKpzQ;R^2xpN`Qd<%PBDhxVW^4~EMuAvWm|9zZgs-If_y&*n7yjg#=$tQ2N zd?yQYXi=>Bw-YwA!Ck`q^FfyHC!2qMa_hTqvK5bJ^3Q)2Zf1S=?N7y&m~2MmA;+%Q z4&m@k>$Up#=9Yj*aloQlOb5>EQ|r&tOeos|-537lD}`c7_3unE_W9@Nt6fL{5(M+n zFr9LkMI*{{YfSkKG5P0vWfD1ORxan~PB9I(p??@R>!{87=da4DHu(uj*{dEJp%j`_m_7lXg5zn z1g6j(PCdA3pyR{C=yf6tVm%jJFA65irwG*G&HkfHg{+=|7XxpHF{4|I%ZE{>ugZ zui%AFe@pV;HZghX6Ux65v|8zRCZvB939Me5MSnnl<4=PA`{)Iv@fBX^^b6~0M*bNm zz(E4}e*_R#`U~*RUjBa~g!Nh#{Q>ivE3($XwK|jq$$bK?}xnSgZ6R-Uc{mIPM z9>h_=w1mvZnKtEfP!I)hyIN!jehR^eKQ`r$+^3g6FRpxyKQ`F;D=%)N7m()`yhy?x7(n== z`vf>hU=ND`!b*R)g!IoPg!S6n^au2Jmh}IHUV#2H&Gei6(N_Kg{(wrqRQ^8#h?V~P z@y=fUVM1B2HP9c>KfY1Oe;2&~{iE>W1mr(&Lh>9WkpES{Y^DDlfVHQ;4W~a((EsXT zL4PMf|DW(8-d~uxz#rETxw!v~{=gr<#)++07j&+Whpeo;LyU)X5B{dgr$a+L1ve|Tr?G2GoJVx{&1pd?x)b zj4z~r`a}9Zq>GEF& z*VH~TexdJrzy-G{R2%v}ilc&U^)1W4#^_-B-hiJ8^)1UkV3dC$t~}&NO{%TFXEFKp zx9U5#$+g#aDQ z9G{5ZOE|qH^au2=K443)Ro{$X=sNGZbxmgV1LbTEB?j-LtjEz2uL z`8VUrv-_*`JsWf(Z(pdva$&BRk!UwSplSeJ?>Opu^`S6#{0{hH42KW=J&+!{^PpI@ zG4|`>(%n#CpDtQ-uN2>$@_WQWi}qnvh2+7uC$S;&2D~?*us-(#TmVCD{v9~Kn7!DB zGW6m`yuf%^`BHGx3#f3zu$rfjhfSPeSS+VmfjX@W7!xv87sHg-AcyZSD5|sJ=kKK^ ze$F!ZIga-R07iZkPDSKD{=ML*t>EV&yZ}FlZA*@F*@ej_F610V>gO$Nocf>WXY-vV zey(TRvzUIm#`ANI;HSDl@N*5y5QG21i<8vPma|W>ep;LPc>vHU2^%!tiuV@%41~?o zrvu*!e(pvYdhrEbAf7J<&*TwWCn^gU?uV0zg$swB^d-^?Uznqc=O1Ac>^WY);}iQ| z6KXO2&LjWRhrTBN)7#<`^S?tlzexALXAFBCGA>RYQ-%NS{Z`21W|Sd$Y{HA?^=slR z*8eU6#&Bu^rs_U4GnJhGU2NuOjbX1d@ZO@|e+YZ+DEWC9Wr&{?&QF~GnYgg}-)++W z60qWqVdVtc$B|gx)Ae)2u-7;7-onpzVXuYX2>rZ|GQ`gncyW^Ynalhy0aGWVpE$9uanVBg#TJnK(KfRz8f&u(B$n| zoTElfuy`NJ5Q|^og<$cd^!L@YQ>?$M$J*#`Hn0fh=HR=9xld2KH=8x~&x0>R=-_-{}k?!|Wti@RaK#8LTP!Qyu)Loa@h7lK9Uzhq?ETU3F@KghGw=O6SXqWf#u z$(}1wl0k^qKV973g8$A%ZKnSTWlfe-p44J5NbMe~U-hFn7D*zpdk$ll|{=y9INn3+5iii&Lb(UCe(IFnNOd`*gSs zi!%)Wy$~X>=uh^)OLqwtFGm^D-z#__Sd{)tMC$!-n?)x7r7!0Gmp$wL9It=6sJ#XM z)%)Ltr^tU753>=DssC*a;aP<9lIVY*`&{ZDWk@&=<3)1%H*sq3zr6oVz~qVQ|3(`Y zP5tkM5P^loD$)Ng-6{2tGEDz?kH=+zNw-zr>k^Xr9n}Ep^)L%zC7ES%{9r$ix(IHrD+%8zW4`qnO zukga+zeJ?=iRgd(y<_rU`Vv_r``fUSJ?rD?VTdmo%kP-YXG^8ip=uNkA5a3Mlyl4u za2_ctr0dx#S+aum_6?#WPw8QExMfruBzG4iw~TL_gwi6ud|A?jZ;-&ZY8w}kX?=_3 zw}7D|RIOgSfJ$PpayCwvw_Wd$<=3D*3Q&H*xAhyRg4AF{j;1%*Ct#m|KLkmXM^%6E+~kJKyVHR9OTa`oKW_87YL zap(3M=s--mkB4HOHGR_Xe|FcJJ{RR;8P&VklR9ru%Ej!ine0G=dhU8u>Q<%i=i}c8 zub`t14r{%0G0kM$S!&%yEaV5|J>`F+t2-Wvj5wk^Eo`7Mb7 zDEPfa0Q__l7~79wSxzH zvVkWZJTC^Iu%~tcV2}em0e<(yn5W=(SrO;AyDfc4cd?;QwikBrCJ1=@J~?rI^J4%C ze$NpAALjrq{_`^|z!)5u{fFqb^PhEnh5xLiALu!+5tMQVLWhs-l6)0WnvLA+KqXrG zF5?xC0KIk_p@5f_jQ@r1V%@cXdj z!aBbd6%2kGu8ZT>vxo6}h+u%u* zTGI@_I}-5wtmMMVuc%<~>yRa-exX^9KwTkF8}dzr?@|e+MSKgcwa6Fvo~PQ7Y`Vl} z#19A|iR*eQQm;*-KWOale!(naX-?QbBFmqJf?|Gj9ex_NNxy8$M?jV+-#)&)?oW*g z3dt~9};|*?JL#SdILPM@Ed~# z^auD7xkw@w&u7SIiN7QP{Fc5;{8V52uqVgA#-vqy_wm zzDxY{UF$9I|Hh1;zC@EXDYvnKxOOXP3(BKT*| zPs(Rme?O~#A)Cd~vH%4Iv_}m%(#pb4Dj9haCD|+N-@GfEPx5kcJ7<0y71#23=hvY6 zY&g4g2O*1aXAvpvDmy5{wi0av_j@gVm zaB7t?wJu!w>pL2V5?mF9aCyK?-;e&Oz>2i=OscEhgOid$E8Vi1p?nl=^&Z7Th??(=trHK#)^2-X+gR7mD1!+n_dSHywSW)HA3>RiG>Q?233i+{N*a9r} zhlUGT{3QB?cm|5UYGg0k6@SKbc7a;R;1#ARh3WpcF>}he35qT50iIpied+f#_^sVZ zBa*M_A|?L{t$(%AZHe=wZ_0ok=$^~7lnr!piXU50aj+$JbT;8c{&R5ON<lQssVN!`<5VK1)iG_a_>@HAvIEJH+#l)?-p4F;X1 zl+d}E*-D9A#V{ajq80Z?xGw~ z`+`P5TMB2Ogz)bnVwj~vTXLMgz%kw8;1`{N7a74-fiqupcNpjhl{kY{F69jR&F>Fc zN&rAbD|&@*CQ=v-&VM$}bf8U(V&Wb0;}U@aPYPtJe6Q8_z{=FX3FHdrfimTL(59^- zqUoF)3qbha1LS|~NJly`haHe356kf=1-N<*wwVUIf`zJu8A7H?L55l|9y;*&8u<;l zlW^j&S~xy5EF0(Hd-XhCc0-i(;N(7;O6~z=+5uOv#u+^B4y+AL!^Ot;PY8{kpd2CPIDaF|hPpgIltwmG zPZsujJSayUOny!r=vGBe7pSV$^+w-iR0CwoUpUAbFkVO{K*~A;9(g`8IzeZVtjNdZc_47 zlcjtTU8$R^p3fM%9(R^!BGM5qIMx?4CbIjy(|_gx*Mn@^4qAdOpua|2iWKZ%*a;rd z3Pf>6yEalkhMW7pKHnJpgx=F|oWj0woEt)Zz!U0!aQ2a92w4Ztz!>LNg3PU0w13in z0~f0$$3l~ul!?bGH_fcWVrVBKap&PW_J<3&ecv=bxMIRB z5#Lp&L6=1y*WyTHjLpR6X`)Z8_A>JLK@ZyKqcD$pGLLWZOZT~s@J1H6)^I>@xNtlX z0$6AYZi##(ahle1(!eo{z*tXN*bQ*y`j}YHdE*MagRy4+hxZZj^^NiJZ_V{ZxN3r^ zzi(Nh`Ud_ibA5q7SJZzwVSNMt?mQFy0{?!33Hry0`lk5=@lQ7L3>B%@R?;7M;`=yx z*ieB@`MXdM1!#SFK7Mk$mH*n5_Zj7HjVo`GXRcKrX=Z+eJTd`+_<7=kIC+ZthCFI7 zM_H*)A&(sd6ZMBDtZ(RJfw{iG|GcQ5p0K`wzr4F6Mkmc)vM(E=>eo9U7zc%n+GsFK@HHHf_o&U0<6HaK`%DL+r_ZeQhRA zJ{C<$`2W=UT7PIk23jew-NO3XBVD;Yx!&XYjElb$Jp1(l+x4|x0^Ykz?cia3zy=uZAqyj4q1oL}4ZwV!f1 zzmqvYOaHR-5wkPFMZ|cT(`~mt@DGAV>uY}%fX^0`3jdP+((L+LoCp1~^|g619`sn8 z2f-l3`r5VN_y6qr+JuDuW4pdKs0&xuszrAChdmKId;hUrUpsawhv&D22mi5wC;i8E zeeE9t;DsDuoc|=|*LHnvi~v{n?g{hz$Di2{Id*;DBj8oo!fT#i+x4{%kb^)S+B=R4UgPs4}L-5FdekTBL`#0yD`ws?SY<+Ek$!zS_*Ajh~<3swcHFX9qbeV_e z=~RZD^d<5ozA%L1`7`+}@h@!wf1>XaKYiC`3jAee{PZPqr^JW#HhLZ*pC$g?E#Ob| zUE-(jTGJ(h|G6gq=}Y8Gd=dP!=g;J`#DA~_{E5Cx{PbO$De#w>@za;cof2Qz$(~2Z zXIbC(ZVQw+*oNpWXk5vuw$7#uhmo2ti#>zh@6;)F{Q%w3>eGbrHS3 zdlY{?XBPFj9{#0(zAPAtFLNsJTBg|HyxZbwcxJgBvsux=2VquIzFk*LpaUO>poq_z?}IbJ zqgLk+93;SSsABkGM*5p$p#PR~6hrLOhwy7?#6DD4HY!DRS_xp#iS4AD z?%7QUC=IToC+KYvAaL;7&Ghrmg9@_HSd16fE+5f~2m_Gr{O4gRr&McK;!X5Rc3xsZ zIcyC4DnrZtUJ+&B9G5OQRFLMxD~f;lqmu8k3v@51_!or;#0{<9ipx<{MX;^W?R}Jf zBfO3{Ri-*4qq520L4+T@m4CyhU_Xyj^!;Ex81-0V?M*h3!kxo`P#tOaF-ILS}pb< z0zPX-z(KL_LJz+&7B`*Jb*jb8Rx0*`QSaiB{#jG$ko4tvVqvr53Gm?1J)}xVGkhT< zRf$lr>>6l=2g`P6<5xYMy?*geyg)tJO92e+q6pb#8rNMRi)O+o9_1qn1laVm9m21i~f)S^65g~Y_DWHX>W+QlpuwP^WoJ_`>)Tz^O zqkP5R0B_bzv`D?FHHx736ZgfcGuZb^`Mt!uc+|CUm6~D{I}AV#q@~CR1`_tK9GORr z*PZRrc(w5^#&0!0<672k{SC}!0giH{sTPc~kI@AXR}WVBefp*sem=IavNHOxR)WEe z_8iW)Fx-iqm(@;mF25z`VEs)wZ;Vk6{>BY3O?w9pMFGOpO!QDny=eE0W{@>i;X>9Q zgoCp{4gD7mQinnRk+A=afEH*MV>}TvXarz#vW2f7Zeqsn){5|oBRXL;!BuV}yk@5V z`{*yHeUCPJbnEof7)x_<>}ueFCV|${e0y8voFj$Rd1US`9nd^WWq% z*QY*y_$R!@qM?%U+}jvGJbVLxJ!ckl zu4VEsZ2a){5PV_70NI+ml-1@O)G!00!P7e2O(7ij!o9WN}wc!9ArQm{2_3Puc5=8c>(uP~0S6B;k{7D_Y8 zB++tW&pv9wa9}4o5Yfw0GegnL zdftpB)1z;<>(MVpvqR0#hQFF)A>@{#f4|cncXDXWJ-lgr(!n0)3G&mhI}G#+k-va(b-z9$fuI)O5m770{%qbC4Ty@-6rsl zGUKN&ks^sN>}1bZ$!CfG=@#%O`Y!R)cWoE8g^;~$@tgRkFOiS%Mexs_7s+Q~FK#q% zWH0|=HJh;)lz(xDlP}~g#LY%DKbQ5bn-`ty|NhaazBzrJ*VvN{-b z9{y^@*Qp`bW6r9a6)>{JecQW~jq_3iUnpA6R+>gvquI&|?j8_q%=gp1+F45UB1b-$ zXmp-c72Mqr8s0jc=l3Y9S43O@8QkryT;pBrN&%Q?bcqrb?JIpxW#8M-_r9%NDDZXk zDaJhh;rS$)Ah|CM5W6GSg0A)L<-b)$;tJ?Rsu!-a0|dB{yDh|mzaIQ?Xa#}8O@P=6 z5JjoDBF86VXZV)WRw^}t#bARHVGIWI1%p0($tP^FEDmawj{ST}17{AbIlXTjcdhu& zr5ru&2Qcwv^q*8o6u{gNl%PcFitkcIiG5L(@fWX;D`WJLV$sKvzC+x~#!@9Uu!j_~ zkwrF|Q^}^_7Y)Ic=@hRK`bdF3oVAen5?3(+(xQu?67e1Sz<1KelD@@m`W(>}l29MF zTlA3Ex7?dixCQc=5f_oZ*E7BK`xEXV+36;a75+<+c z>ryAgA*05m6DqPdYFzFI`+IRKoX2R6+5CHME8`{3yZ?snT=#ev(~ax?>|9uAXE;_K zNbDZ`);VuXb{lZ(UEHZ#6YM24tZCNK>18=ejd%H{IU9VNXPk!%HhX1zgQwHL4|i!H zQ^&hF^EzH|$`;>hZ-s|GQABtxCU-0Gc|a4ghJ3a21~vu0%V|L9Z)GZ+5Gh*UIDVcwWD zC(!gv$Ntn%aT9=gmp6bNH!`Q3-#Z(CBJfJ^J7=(|$v=`oC&;1kP6POi(XoK_d>j*e zpM?J%!Dp}mG1T`eN8okD#aTPEAo!6pvKj*?LLb3Eu}RUQ6?n*sq|xvT{zLNbc8D~v z1nnBy=UiR-U*qcbY1t{H5xUFFySTUVlXv-c+-JEKcYkl2(JeHJYa^6?4zz$kq&LXo zb*KP(cscLlUcX`0eA{MzLfW`U*9OEKT$$os^5F4cb&9VRBi-YHZ$m>F8CiBNtPPqD3Y_J4{UurY6~(i}pMhjRVWb!la(86fJsRZR2gY z+XwN)ur?FFLZitbwf%oUM~6$-NoZKMLr7{9K2r4(+Qum|4W6P3eUagZ_rRJ@<*vXx z{Lb{@dFKd+|Fl^>9R5B#w?j*oDjA%REK6g^^RGuxZT?ZHn#*r6; z3UP;N_0Cqcz+As(=bl}=chwtmev_VWy05J(7{K=fBp zc&sBDfkr_mQu0Rs2`PEi_pvzvZ(nWY^UkeOKDXhQkk9%FR{8u3AFcA4LKQ6Xne@(K zT|V_y+owDH9Z5okkdVi)A1Nz*7c4-0W)Yo3R6#_kAVi92`(?XhMYR3023ryFf`Dxn z5ig9FCJ|lr6hyS}UU^@#t$hACR?4S0ehK-!agS9#kKvD6F zpKuXbu9V7YVmuQooa1A-aQ=I@A)NYFt73&y-)gn3aDV{W_A^bQc^UCH_Oi(&nzNsT zXewc#%*WVZFRs6$5g>o*UEHb)4_H%dz0^Tj&NQHOWz#!t78bRTvzVbj-w*a&*|Y8( z;Ls{MA8KM1g67!?`p1PS5;VgPYF%?%882}k!RUc>AAuOSGLgBM^V2X++i^={6TgqC zMybZ3*Ox;L`B)&KYg;MO*+aN=cLgrp3FqbkKUQ$~C=_N&uS);}{Tx~0f9`_1(Kw>X zzyzE=zJ!mYZlVIrkh^QYrwYyBSpwBlgCojgb?7S8P1msHtlH!BDSMwao1cq$z<~Z>W)8kMhj^;%)p09?eoltoN67 z38c-MNy{ud(8FNe!9Q(2Bo=is6IUu7vZ{hv@a2??vyJ&e{o92Dp)N>fw)85UZn^6KD$XpP)V{Yqj0sBUHU z8V(>heSKEoJ+6xGsp^n)3~wv1AmL4~?J_-$R>8a#FMxSk?P7ut)J4z2cu1G`cTC;^ zA|OuQwESq2cLb^ASaQO6+9cu17tlvWdQ<&=)36<;sLoq~Z5ohv-K=sfMUC<<&cAjZ z-YzbxtLcX|@_v3sZln=WF4;e;#j+~VDy#a;Rk2bvuUJ&Qny?M7(>^+Y&akLX)fb!s zzpp8(%ZffuA7738*p7XCM>`Mh0w>5KRD&e`L-A6nA;)C#%;R|UE}4pWcvvY~A3d*h0mg8_ zsuX1#He)D_rQ{RW!ZE!SXA`u6(k1Ivr#fQ2l1^5ljv(wHNIgCE=7Us4*%nx*>lgbv z>V=!WtwDL9bp$KzNG&<24q#@xn)ERsp%-CC0%{E=4$Y2uJHSzsFugX*TX{awhf)D5 zHSwTYoL*Vwt$cyrj;-r57XCo-+glmGGdRC>CVqd=`Sn(ui$a(L`>#UlAetb?Z=Jz! zK=3>1pz=d>4R$s#J}R)8q6w#cfE&Jl6 zZyx-LNsTu-JXireBx-OV0te(Z*c~IPs{w;FL|-9(NIo(gv=Tmw2YV}CC6Y}`QWS_) z!YD6-!_vMYn_#gJE;jw}rgsMxkfJXe?KzJh=f%P*7O;3AS?VY<1?`9OLu4)fXcZtp zIAFdO9>RGF!%K>OKUPc#juj9i9G!qVO_8stc2W=-rf9sz3{`G7pVESKp@QM5#Rf79- z(erjfukKbcK^DDx9`#D!0}4ivrol_|#WP_hB?m>_49qn> zqJBEn*IHBkG{%@oUPBpu`!;j>X6-6w@~V4=%WE$hRYspj)9!?N^IdSzC#x{$iV+Zt zr-uRugZs0Y1eILIex3}H(t;{3 zrE;PIW0|p_B2;vM06ezpSW`)K8zy0Re+Eb*K!1MN5@2WaKq!)!35*z`ft|_!a~rlr zi7Dh~&hWfvIjSGf>~r8tAz1>R58zgKnX&NBJxzyKdz{02Lcsg04sX1G*Pz6}qbQ-a z{Or73^r}twDUoZKG zR|)>%NR0njdeeyBOa@Q8>L3(2f(FU75S@hx9#u=@nqJ~~*p0J{KSt?(wq!u!U6S8@g?vqADtCdG_vDIA0G&*33r zO@r&{bg&r7K$%{rox#9r%h#YOt5GqK*vA3{Gz{~EijY<&>@Ada9EMJ5W`?0(f`Jgp zX_g@17$+c{x^F8iak2+nbhJA`Oz>LT8|nz0Lp#|XY*NZ;CmZ}~FL^`yiepVTMi&Lw zvMs8|qWeHb(LNoCA5^e%V^FSyJZ3K$E{yt~xpWw5bU%EX{uA+3)z1Qk6x!~t4srX2 zcxoI@?;M;XjPa9A6EWD9)~mRgv_dEJ|INE#$f}26^!Pr+xQbn zp+-pIx>Cg%T#=#-PKO9kq~{RNn7cUQ#w>#|#7;Qb1HKb(i)W!=Kz zxu56o`snc70^S0G2XBQivS7&Rp%Mz9WQ59_d|RfZC|i(n(-d$=?^eF=Q6D%HQQAxd zk6U3^HA2+gJ76^9Yn-%KY3Q*Pc{$!EtC8^7zhZl!FA81jh_>bHNj)h^n_kI5o3JC+`a@&#z@8zi!*v{_W_y9LCmo`K`Y>hTn6si9_-`o%35E_`Q)F zn8`2WRxZDV!Fc&ii-kA%5gp#bc^uxW0$z#^Z}nIXufzlo14vR@U;rW&3mhp9N2~Mj z&kD`8Vz*1%GU{+s&ZG| zi?pl_0u@YX;yer(-dSHHE3cV9#K{Z&16(jP_^aUlK^WDlW7FWQE+?>%@ppZQO#)_3 z|H=`_dEbS%R7OM!+TqxRbljg*E9bs)nynUj)WIH%aD~(u;dd0`+?U1Gn4BuRl%iU| zfw5A2$>hb}Io|*?x$t0*a;6JXv>3G(KQ!>a`}&PjV8O}!&>JE*2-e=Z1R$_iWp<^ii` zp~oAf_#fvJ(c8+wC>*&I|FF9B<^ntf*SaES)LEnbMSkufL-(^*!AHHJsv zkHMo9g%12i^I}I68K4M41&1KoSB&sshE|EbrrV+DXXv!P9s$zOCS5TNlkPv@y|#52 zuO?!6GU^!cXv{87=$g-Ro`q6FQ~2KiM7gL9?fxweDD)r}9!pf9sv|K65X0SmdS~;ii#UfVV*S zC&jTtqntfU8sP)pIRnYWxd)ml7q94(s3c)O7dTB zEihsqT=Um>VL0j|zy>p&eH@vey_v7#+z0NW~;&R$YqfC`QK`4suR>`gtixk8{p40wbN1(KBZ< zUeWx^&Of{8{&}(PpOYUje3KGCBVF+i_|x6pnYxjz3R!?iI4M*8XCkRE`Mh;f36^hC#DobO>U%URhi%tJ6yG86z zplfMsT1e|3Uv|bZ$kh*ee>F4q_cpu8)3{YwS^?H3tWZba=bs^V8+3!(bAYF@-icF|EBk`8^ zFGOV^Z7J&j`+*aB_Fq%lGq8;5G0>evBbj~;%+Tr!`kVUXYMw{}{O$ zZv>x8O$7ERojNiUyvp8i26iF|AH7a<4(2{8hGW6&(Hmm7p}&(>2Hz5<6|If_2ygq0 zw&tKyJQq2R+Gu>%sxgw}Hp-TLWNpOoUqji*cF?0;PCi<#JuNEl_GAB(&G`C+{PQO) zm6-h9a;hL+NYu*6kVXldOtX8__Xxx}+xMp}GR($o* zXXceg2N!acOE+Yz*s_Nsij=T-&V2w*WJv$}JxHKu|D~lpr?2Mzmu>WLGr8;0Sd+Fx zMy|v^3z)yrD;kTEv(YeA?SBl5W{8V0_P@4!OuO0_ShvbeCi=TP_L z%hl^$YWg1$=DLW63jO=So*d>D6#a7A$Tt(3-4tMyocl9{g&VjOBUYS7I@dh!5c^LQj>*OYk~JQ zn(0`6#}mANZ%a~s4HkIM#=)CF@a{+!-Z2Zj0depq61om0ykEJ)SJQ zi57Sd#=+Z9@CuWKH_Za?(l~foq>lreljL{e3$greL&p)T-)w^SQnK(CSl~Sz2X6tv z8$^vg&Un{?L1n;F} z;jOp8dpHi>R)RMyS$M96vG#mT9K0O_FEv?sX%={27h3tfMO2n3#{G0=_yNvysz#_HnBNT$qlyW+O`4IpJIc5Wek0 zy!%h1^)Su36;q=9J=wCa9swZugNXi@K1l-ptLC{0;LkV1e{l=oKVk!)&wrAEA2as> z{H|vB1q46W2L5oOfA5AQ=+`XtZzwRyXU}H9Uw=)!d`1)eU^4Jy=31ctUNijJ1b>1J z{4oUo!erpb%)J1=y&3-b1m9x=|F;Cc`r{%g2;J@LEm(MtYe@8O# zW9DXnKg10Gc7i|120ojwr6vPEX08VK-}W}q?^b>-*BBtK6^d|{Pn+ymrpsf z|776D%=G~OUNijJ1b>1J{AtYolYt*I_XGU)X87k5e2)$MS|CLy1gxgg-bd96u4 zD>ecC8&}86r-IpkGVo*OhJZiB4F7h5Kgb3?n=hs&13zZ22>9RjGSTlO`1`Mlr+){{ z&t6)SgnZJ>b2Gr7Z-)OO7HPCcY~Zu`X<;((W9FuS-_;DifZ*raz~9H_zpIm=zr;fS z2J|L+|F{Q<6x#ak@$zBw;b1cGW9Gu3|6Viv*#v)r4SY5~zAzd1F>___We-pilE}s>j0R9_S#>*$0*?%(dW9H6)Kg10Gc7i|120oilrzQhG zW-bl*-=a6s>30(R{a3`(&*s-J)g&RGn7K9J&o{$=aRcB#Vgvtal22hW@MGrMfZx>& zzkuN9+Q5IF;P3q?3HoE^-hjUWy@@WLJy^og)_04S&q9J9Oa^|;TpaN4HN&4x@F&>7 zf0^K4m<;@wxjEpsH^V=l;CpP~v-x`UswCtSGgk-vH_@Bu@>zjI4(*M+c=@pT`yI)^ zkD0pz{tz?#+X?<48~95}KB>vTkD1E@{s3YnAic}4>7~Po!}3$fxng6e=_i6VhMo%EqW83ekZ}- zpBqpA4rc#VNysNAwgC9^&G27b4fv1Pz~9a6KN7=VG{Jm z#2x^D19}r(K6}D|zrIVXe0csHKPrdrv&*6^hDXmq>;xNHcv39Rf3CvXG47K`rguyN zH`}~^_up7%03~qKHFA37f&Z|Df4K#Izvke#J#qM-oE=O5S06S*|LZR|M?dyQVvV>v zf-r7m6FBB!ucX*7Ne7#-eUb`;F7`>rm(N6bc@PRKe^c}2CP<&?Qk8~^=h(K3EEcFkj(CeO6j^MmL-X#eHxtmxKs2z0a#dGb@$sg4{!bo z`8-*`BTjmbZqdG6C9+}=4+Hhte1EAM=|dz>BmpS!ma6}dnaIp@Zojg;*iXvKLUdGq zflYZ^d;jW8i#;}H?*m?M&K`L_4_O(~WTv~Z&{gqIaEd3QXfqH|%3*Kpm?+xeo%=cX`KP zO$v@A(#!itn<5 zr++Q}Tnf(O96H0~pz8n6{8QR9ZTR5sImO{Wo3d)M={QyD6|fVgTF>v}`{&Ov@1L*X z+vcNdwcA$20lO|oI;yQaD=OFVV8-2XAHg8A{YrW>=>!C9-Uy_Hrqc3)(o;=A z7lPxxuz?1fTwdP$BjF(cf9%-U^gbM8W|v_uUv1erL=24t>a|BZ#1nJY`5>m45u?=j zws2aWI-k?hK86<3w=F$HObulb5@EO_c7-77yC1Pps~95j2$8fVk(_3wLhWc^&J>uR z7|5j{@#PZ#fixt^+G`0|u-ygF+j-FfH9|p3Jq}ru)`q`KvB+e{g|9@UmV%vSjJPy2 zaTB2B7=6X)Xhs&7%b@{MM+SYO)%o&w}j$!vuzbP-~|OIF&coMTs(`q z6Ur*5z195>R&FZ4SX?W5osx>YQNRYvmrK}<v_@y13dY9wavSneIwDS1#QM*dU$D zcMYSft5~M*i+)B!k3d~=ruM>r(B=~;fIwSGdn?=M`%XSVqeFWu0T73=0&Ye(wqMX& zu>n|n;@KNoMfh>Tvv+Z=Iwj+fYh;%HMgSa+Ay7$WQw=^cLNQnk_wC5C>ism^DiL>F zSMxohgTO%lgJOT^*H1EcGMB>k2qdlJqU%A~X_zCX=X{oP1lRKTBFI~h1dm`#$s*7Z z>Vv~WeJiHmsNB1qhg|n&`F~fsfcBCECOUs``m={S596kzG-a6E8>I5y#VNGwtMUqr z4cL!D{E)5sB(zgCqKD4+2z?#wVY5I#T6N{3T}1lwXWs~PPu zpTh-6U;>x3MgBmQ-^C0D@)-nSfi@f#F^~40Q2IODZ+?N?x;FVTC;@Jh*G zo1LXGmKEqt4DhUb_Iv{h!GhB9dS>LCpY@NC&iH`5c)oipU;FB8p(ths=GjOm?UpQj zLnkoI6PXA&m{`1*3D3if#94?LNiTfXUXk^ob!4|22ek#R)>M?njYyTPiC}1+YupiO zgJ;a@aD%v(fjwzTgK{)-7Z5~B@sIXpC*Y{oh6q$o^^qPX$JcON%LrJqlhP!VePL+5(8J z6;*I>Chrm--oXbWp7Hjd?kmv7|zYdRvWUC z>3gw19s78MG4kWtMEi61A9?#@90Nx;mVJ*dg8Y>x4_ep-TgEP+>3qMQsqvk@+B^Rl zYJ11-)0^JYmgr6Id3#yYTeM2cUW%5Nf#B`#G_EUGLnA<0h1TZtt9pz^9H_?;Ek2w%5){LH7~IL&YF? zx6U{93TLuj0rhAH-CQ&ZOec}M@Qdw7l`=BQ@&f&-0QkSvg+n7lh1mR9tF~r4AAck| zMq*Qct#|HU@SBd<3>K|-&P&gra>XvSbxq$4Z0YakhRA5A@W&`c%`FsA>wTyPHFJl= z`Q{im(191~;MKmt>nC?({lvwb2b}ongb6@@25ox(@ETfM)<0ms{q;5s^DX!|>6{v9 z4-N0}mx%(VCM_1N#WKj6cFVVpTxFJAJ|x$MOU|%UzLy>q5IZEQ*#_SrXESmEUPE60 zAA4^C*i^CokI&qjY14(Ktbzg!P@rXPS%k_Wn~1noL`6lRK!GZoZ3?KUKpG%~APPlL zP>{vtsqBad2ozcnd{#gyI|#xJ1qDGUi}wFHbMH-)Qq=dpzTfZtzJCiz&TMC9&dfP; z=FFCN7p(Fa4W6XYK(Cvu6TNg(Rxv|RGT*`LD5l=)j4|U$ZAB$e*=U@=5u>dZkI|rT zuOZvpuF_?@k-YGW!qwm;c~t)xcouT;nJT(mT;3LP@hoJh)~jsPnU%Hnh>LSvbhT=4 zl*yNbFYPm=0iN0ho@ao^)V|F5mXrr{9^Ow{2!nBV{;@E4%6o$_`27QdkyGd}pDr43 z?k`X@?6ggFl@bjLEikK+HfiyaLhinBDynM8mge zCVBrqsDr*Af5pAS^FU<$H%a>-4)#eEX&0h+E#*lZbdutrfo=5@+~E4m8sK^yy`M(% zK&ntx4M72U@Ccn~hhn3j)CH;Qzko`1l>*=h_#e+ab_M^Fyh(a#r)2)hlKEvRO9WU} zA9Ot;&i&9+4YnySJF(66rl0f2=!|Xh-_7`+js&7VgZXKyPp2lmkMlOYaKJL(Hen5l zLWdN=K=)#fj2M`p$;$)wfLY79Jb>tj8hIX-7neaKEQAM%1P~q~p)Lw;oDbD6L2L8U z&Wsk4ap>TlUMHxGL6!2$f)Bx4ru_hEG8^k1BuH@R@?b1+p?+Au9q{_@HIPdwQzp;r z*M3P(|3I&ROyrke=8;ESMSE9%ZWpB-^N~6Tb_DC(=^IWN`tNnKx!xsr86=lt;Cm$T zYmMEtWzl6zfQDLqVnvbw`?R*p!2eg>=Mswxa|<`iOLd>GpfmiY)?oPSmBH|X3{0g? zCTCXA?nhkG9$zGG??v3c0}0RtTNp^j07(hA-@xx=UooXASiZ@8<6!V{DZ4O%BGIo8 z|L$$_Id6=Kwy9a@zJWt{SDX|jYYh6+=Ld8?-KnK4)%lc4EvHT;NA{DE9%pcIYTP(h z?bPG|b$@;m(0f-QQ$zpPkL6Nt1LxX4-3u!*<_!>$c6i~*Sq!qcOZ@?~N#ZWB=s7|2&J_=Izx-Rqi$Uj# z9o_Xa#Tm~*JWz>ooOntPbQ@V(*Koa8CLVCUv)RZL(D38D6XCflmkV2?S(IqSa zk@OD#Frm^<;1;^%cyCN!@_b)|!9hJlc*QpYusN~G^DWXtKp-Vs~hj*ggtXxia&Wn#&U(Y7G2Cvy*-Jbsl5 z#tf%$|Og7+rW6TrP(WQHeXrzw%!xve5Jm*P+-ni+8WLAkbeDE6wO)&y3J|J z4KtNfQv+uzbbESSVXl9|5=NCOHDPgqeYA6t6o57fny~B#B@44rnQs74kFSQxXgw*x zgT!<>+8QH(Apldl|5fX^f9d+ro$uYSK4i>Kyrlxp9cstHB9|(jLKG zFT>@skZdk1ERH7Ek zY_Xi|qML0b{p8g*pQJ%q)NFHR@oV%!wlpdF$%Oo_0!w%+9Na>=>GxmhgEia}P-^FT%3RJu0xM5%q+$I>@omR&p68;X z;qOjgHL5G5L##W;_cUM(Gu<*^iGS>U^%b0pZ$PWhc+PR~v`8o@^>GP4zmVXucSH8NDjU{hemR4Gy$CUeafLsVI1W z^HDepf6{Ux(vW*oBll6r-B1GlkiQzv{!{!l!!5f$2w@JIe*Qw6$zhlHjQR>lf)>*) z15a^X{iiGSN&UwnS3WM7!&~p!I;Sly&1r>;OL5v0yZtoLIqk}OI;YXi5!5yBL)SDo z?YWz9TD)KX`vZPU_V+}Sx$gbxT8XTnt{lWxwyXoFs>jb<2ho~3b8P?w{{GkbYiKF{ zdIn8w@K-;{UqrYh$z`^z`w^(~7gke>eeLn(uR$O$xqiGB$V(X%Z#A`&p)Z4fUMs~< zGo{<$C_e|C@^cVM5##HKZt-956jyNy{w zJ$z;<_ejkC^=cX6rQEAi%@XM0Rs1#3-CZ++!xxuw51bbqKB1Jm$``@mx0iN*9UMNh zlzZ|1;P8+G!S0dAg58TtxxXt64zG76*uD5tu=|qVE#C*VdcyKLV&gV8c698?v{|?P z3hmmH+#$EaJuJ6Fcb3-y78uC-4w#qGPgVMHV+%^M)twyaNp9gzZtYHP>)c{}veb4u z<2~O1{JogAt<7JD-v@3AT2FiS=b#?$d$N=p)3rbj@WI#~oIcnMeO=xM>pGArKBtkG zMzjB&&~J=O!vYmtmg4-v?;-mwh@}01ubaOGF$&yhaG_tn49ehTnE_UgqRnlo zroBMBHbdri3b}m?l^zSS?k&2GmWbkMjj()(-3k7`$54J0Dwzrur(@mhXyAPT53J%n zVwjIm&QABykMq=ZtfFm7-sDp*`~x*}=hOYaYh=ntL+M6C_vXx8?Kb4*$Bz4#i9Xn0 z=pEhH-8NR+dX6aL-t34kl|}D>Ec%*Rcsup&ljAPZE{7QCztq;{1fdVS4;ps(;+wzs zQ3oaolneWv%r;1o@lSjwk7rv;zjtu~q7?Dz`fx{Vw004s@I=9S?0q@k6FqJd`*zQ| zal6-jxNME{8XF~GF&kOQwJY-uck9HBLH9vFaKg16mZs6SxSu}Z9Bko9R~^Fr=uvlT zPglCjK1X(%_#K5uwPTx6wUYkiXhB_R3%&-SBXo@kUFVCQXU};uG|!6%u{>e-bS@wy6@Y|(!x2Rz!=r~? z>X5*_y^b{Oggl@?QL#lS4Rw~BFYWnP_^4y@d7;sjumtUh zabCd%y6*0kWQ>8+aLZffdCwEk?y7~Sr9Gp2G?2Nigvbs=g{!pf*hgv>M_=!&d=Zb_ zCFPH-pjIxDP=?i)L45%4&tZb#n|RhQWOi$4bTTP~;cW zW83Y)Q+#aAHhgC)IkJDw&xeD$%97tMkz|X|Yb*675R;T$?cf>7=sAwcl!HIT$9jq| z?cx{Euy8+Q9^?_4Lfg&(Hb1~5_^8rN2Y%umk&N7GQcIF&W zjvE&FnOaa|>*y6-Dpwsq)-;-Q$m zrt5XTg+_wOKxsTF;?9xSaCJ6V_K7-p5Cn}oUdMYzyJFQ@Pm)C6FDmJacQH(I$O{xZJRMRGrVf2|Gr4!%}GUy{e*M;c|RgpmRKxQP~ccCIgpO9hbpI0z!c6YcC^ z;o!-HvU#$m&XC?G5~xyjsZ#iD47zBeffE9I)S=5yR4P1dwzU3hwzsAt56S@y6r0z1 z9w>%K#q93R`S6HyciyAb#!`>FbAk5E1dv)~4I6fLcRq-Y-1(6Q?#}CMP43l6O2FIf zdd@4ZAm`uK1=l8MJi-MaXtRq)^ft3^)Za50J6CVB{t(M3BIyHNJhX2D3Rn>?G6dDI ze_K*dYG`od(2;cG@F2Fo?}txYQp`L`n{L+~QHdL!Te@J&vfjV&VnZXCR(i))f*o1f zzR7ft-5~T3dr$mE9ywGkod*P)%8Bm2%NtHo~^Mx{reE-)ey&fWOl>`m4sT~ zIFE%muY@?Vo!<-RRg1&px|BH)p}n!OvIhO~%}Kj$uIErE-!ObZa2e}2&M~pQf3syh z22aY^Qw2qfbe~6!dy*gl@q5FEHpvwCuse^2V6D?x6k>D9HCB+q7lV`mP5Q`VYe>Qn z*~U26#ZDuNc&5b8B6`e;or52crwe}Qw;TOBATP#XWXASR@T)HBu{pO1*N1!TDY9e8 zMnoqX*96vVW@*v z4sA}l!{>!7;kyti$ahqADLiq$2J>ecdwSwE9z7W2a4+rDCG@ErIwLZ~Gef4*S`@*n zOKe{+Y9R?#g6+y}7>~xpcH`IwdziK99Xr_OYzVA)FycPECpjc`C?0s$w4W~n!#?+B zjJ9ws1U@`c54iO`&01vv!U4pz)m0{zACpqohLP5K65J@mfz=rj09aQD*OqtAKP>5YMwcHdoujc@c+#^{S{ ztp4e|5;H2=`F#gRglzcTM=uwIZFTQYyoBC;#I_m=AjiHhvrFe@9T;THh zojVN~VHqA4p`grs?sHNSUjuc43VrJ^+SYZ+k>Is7Cu;gCgXEA_O*vj_rnY~I!VlQo zRuEYmgjt4Be{6_rie*mNLy93EiQdlR92e@sUDCJcwbcNsj|wA{6PCd@@qlkNeoFY= z=rK}kC19!@B(z?+pbM=a&u8Fiw3z$ui#{6bsmZ6H$$bw}mjELWnY_d=jj%rsSe@s% zF9bDoLwI)o8G{PmEzm~+Q$qzmUq+?b$j{6(Y) zCYFW2?D!hR=VitJj2^|#6ZGAC2Pn|?$=?Epw`mvrnz}ydm3E`PAXFUsfED}|i(eW; z?L??rgNZ$ZkNeU581X3{#kW)Z`3ILjh}E7Bl;-RpBMn}fmH*JN7Uzj`#m`;WE$I)9 zJ5xlW1cQy+)tCB7M~mAv7=V^?3gr6@eedVmP(P3N#}fH^ioxA3JAT~RvG}%4{RYXs zE*vf?{v1reysORubh~;ZuyEuw@MqFoN@bfmsSfgys6As^Tilo=?mZ&=Q{?6t#@kuxLhS8N@H%CLsQw6P!mQtWXN`Zf|h&hDU!`_E!XF3F`6)kfyxZ9A9n zcq+~E$mlTA%fz()RP3nY#&(zNUMQKCjAD2s$Vt-B{qxub*J+7MchS2qqL4l;AVKVjUVyVUPLf^8 zmNfxO<73;kgCC^r-54QdpQXNFN2z7?F*~dP3SMY%uTBXqdi!t1$?)loRG&4%m-b0H zu+$lgbRdB2u3GqI>Sq>heBLj`^IIU6r)2`Z^ITad2O~&EJoiA-!UBIgP`bZV*evQxHRHRT{YQozC$nkvQ0nQd<#VUCq$h}wV_lDfFgW^a7MxE2B-Dr!a ziZ@cG$YXmTfB83_Bsa$i?1|_o7K%u6C(nl=hjzOUoECtes092lrJ|Erg0XK!3Scjs z=gf^vT1C8Mb8SLD_IRbJ{O&H;5M%+h9RyaU7L<)CYnrz4A0K}BArYq)dQP5XKokWv zCrsYz9X1_T_8X_H%(dvLJT0nu zMp}~6fqxlAPy#yR!CaF#i#(DeJlVuC-s)9VS&XF3j+XJ8omW+Fi;6Ed#QHqhat(7A z=7bd;@<|HWk_!~bS?8ZlvsHxw==wqrjae1gf|XhdCZM8TGs7p zDTUqBMV^;0db?g+K(3@~Wnr&cg5A>|exG^{1hWuPu$njDTt3ndokN-4-TkT2y3 zmL&BU2K%rTG!|R2v(!;BDKA68QyZ)0s5D6?!HFWWW_5chrl*_5?Y#)V9sL;z*&Z)31lk7nE{isNHtj{AzO&Jiq!&bX8JlYq$ZSZWRny8S=&OICplxqf&=}wS zD3-22_=}r_;*L-H-QsSXb+h7<;n5ghBTy$})MWs5jj0`pzoa~!rnanS(NB!|vF8>X zZ}#gqoe_{Ld1pYboD;l~c%U%Rwz_Lr+fZ8;?dokCVau9DznJ$d1rcQWK#hgo()F}> zlir0TRj7zY+=&~FK_zpg=`Tm(;ikW14e|&d+wOT36A%B$MmSP{E`}!+p8n554`VJUGh$iA8KVDVx+K+Y-DMUYTYcfDEDp zmO1l|f|fH-xI-xQ5Ij}vJ4ER8^-{u@+chlIwpyZ^B+(Xr?AMP9T8^cZViJAB>_s@X zeRH|Epd3|~T(=TN4=_r4>0~%~<&b2b;B?cF?vSM7;q8?2=%YwUkMTM4B9qn=quJ!7 zOdFN|OR<)|2w(Rv!&n@PR`#^CQO!%zqN($^2==rfb;=# z(^1vE8Ald%A#%dRllI!OMj;g*mUeYR9HV`4lS;TpadS^63m#{6Znio%S(Elcyvg~wM@Dr@x^M)> z@AYI;aX0;>dhaW;HuOqOQp(RwS5&=}9|$;sMXw--r6 z@JbT%4pD6g z!X;gE364k0Drvh&Q%u@#oA4oM^z81)JlT~fJ8$)9PaC(?>^x6`1k|D2bLDj5ntT7I zs}|}NgNd?w@9R}8P!k(oA6I4Tx#v=&<6T2fRijfC#TUq4t#=k%)=XrB2Mmw63v5{h z$kExlqHW>_@bp9%$7XMV_A8GV;FUz23eU?t>bxRs6P8fK_0$WPqmf6&u?(q2(DD&> z7T7#(bG(nyras#Q7sbS3DA_eLPkLm}_R4LDxiCPvUF$#_y&2uEjS{93yIorVdu6E9 zh9Xj~7iaBy=%Gyev&mL$%#Ktm6TPXTN^XZpt|QLvP!3Q}s&Z8t^CbfsDuC?S4W&5K z`xvN)Q5$6g;ctb+&}E|!XUU4!f}+evn%r7U#+Z*@CQjVtyG%jUEb{SM{Q%{0_~~4O zjX|=O2xnkB7fwteg!o1u!F3KXxXK;KD3}zw!YQ8K+mkK68CVN$BiY!N)d|HQz4Aet z)S~}>XqQ%~*oGJq+kSPKG;)mhOnV=Dv;cR6Ke#>+ku zQ*2qU1JGJnNNIQP)>~Ms+#eUGoOl1G?cDNfap6vs?p}TFFz0<^?(h>fJLd`-u(@v{ z%tTtcUJH4DP{@lY(r#T@#yv_dspM{UWI;vNy6=N=f%GdNtpKE2wb{SWtqpYOrxCfs z$c@A_f1t#22C*5MGZL3W;y@FpEsJ&lk;*3<YyO=!qaumHRC z{75L#Qhy?=_B`cC*ePIdKtNvvkY7|Iy=cjS5#w}ES)x`(#I{Dn`s9|}LtWTYj%qDO zS!oE+tt;lF`8qnIr$)(iI3B{t6l6F^xru_;6C*yG7wN6Ud4W5-5nNh-kdv&>R4ppx z3r60BZyL2hpbU6DprBK!4a};}2?)qU0L|%0AVT9Li3%49NmPV}S@bT*WYPp=m0VN? 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

f1`!)jAJx~X$qi@S+k7GtUQalm|%g*}JG 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

2ekX#8kUbTncR{%;Awr|2-1 z_)*%UK`DQtm+SbvG01ZR!l)GU}^Xr~7E74j+Sf6gve! zz#+MskOF`VBWfx$AEPsu&qR2&PG&p`O7g`zS$J}dPTmdG1Q1b9-K&#tgaQjC?#2(` z{%{9omsfSlU!d=bzKby8^Pr)Ley-!0$CQ^SB>W%N&I7)xBK`Yk?o9$j5H6?zyJBw; zs$Hb05flUqtR=Ld6afJNT~=g@&B7zk~?4k=IHpGr))m*q3Q9(cwrMNcU@9&v& zPHsYAcYXi&^O?ENJoB4po~h?dJ;mLQ@6MfUMW-@+Nz_Yd$X0`QkCVD;jI_P^yDfuj z(2=Nbz6sQMvx}wUWB<2rS5i3iF3aR;r&-Jv_;{_yxVr(2Hh&R?p*-3F~E zdRJ()5TDRHDnv8yz0+By-1*Gx^eJ~(;h!=pH3J`5H;saR%F}%=hJMAu%g8_2&m-@u>2MCjaMylagd-}Xf zOa*!4BdPouxb74K_XCh>?CEBgnTEzX7#4AeMx9ppq>lBd54_}iCcrw)qvVZTQ>ed3 zS%~4Do+juxPycHY^fjJ7_zDZwhCkvk)PwjCt!P+jfuHx_Bd>>k&3lVaeCGYB_qL2# z4o$E!@;$Z2Lnn}A1R86zhl&QZkFvPoJ(df14>Ex>{hxdYquc_wmnMCW^tdHzVJASt z|5YuoyXPC`JkOsp)*9waCQLzfVy-ha)}xv+ADOz!qmJZ4#MCXGFAUJVr=ZXb?_DbT zDNnDzgkaj00A{)PZ~I_|`NY%1?=zLdm`BZSbsPZZTO! z{!c!HQAU9~PKB}CRLh*EDx?FU;kRNUH`Utng@M)fX+)mny?gu<`gHGY{NB)Q^kQB+ zaYxR493wyGr8aSQj;&vh9K@=7Vv0F(K6Um2e`gMlQhiU7rzl%7o+L~ieI(MaxrWQ$ z`Yn|lwXbC50>d!}m&po}1=rT`FqhrDQasM}dp(S0PH&5x?^!S)8eWV;(vJk0H z3#atCaaj7=IV}C9^p$C73w^w&pGRQo6c4@W4a>+q;6NS6jhndS4FH;kI zn8$q>d4tloFjMz>pULl7_}%?q>T8oNfb`@KxR|NMz$w3m;9#Atm$0Q?!dhO{Qmdfh zU)KpV>swDhdoH9R}cL(gYOmg?;J z!k~xEH=O6azyAjP26?*WYUt4(Dn3%5`OfLDgdxYUZyB=HPAtt`JWcb9npMgf&ns49 zZSG!h!l^?2cj4<*nkr2AslsG9RhZ+a3UkA$!aSQQtmUT)Yx}9fI)17!-=+$;v#G+m zeyXtE_C=?jd3=|mj=}aE3BQmzX2^%$p|y9@173N{e=&hCs!CIOJv(ZW9$QdDEqGc_;wbIn= zGpD}2?MBEt3SN00*_IeTM|?_rs+|ookAx;}eb6tZuk=8-sIXdTEPehIs95;ugMTS~ zniSSQnj1L$YYQNCwfEWUX9ItvBNbCT>th|kc*xT|AAo+^(@%3kLz)Qs1}x$@SKc3a z?;oZ^r#-EaF;IDfS|^{4-bZIT@VRRStUA*Tb-_Kx5(@A&&*0M(ALywXNK zru{A84>1D3{QX&QXL;PU&qMd~wD@@6B-Usz^38eBS9m({q6v{VvUp~c5rUkyv?V{EYgXl)_%m(th8@cu?EyPNlJpjvT`=Qkm~)CHd3 zV~xcvC_>EV3VcYH?lI&~bq86s_;T1Brv*ZN^U0qt+)f_)@KQ=VXnk*tzn;iUE%L0B z*O{jKmfK%fT&XgAh-={{pM9`!t@eMp9w$EpWqja5m_TOsO$MnwW?0}k&;V}u)s&la z7CuLKpHqJ`)`DM1Zx>jgI=~_&Vj<>MT>L2ve-CIyGN=ZFaWF;Rei>HCzZ4(+Btqc`sy_sYiK~f+)aEa>#>?0p8u;%Xevk)Dy~CKA z_QB5@`z|xBVDw4R)c(*2HM%7TmY9-!GPmx5?1d**?r-Rr2#aT{z#o z2fqgEQZGi{NE3^{j$|6~TbO$RKsR4+Ijq0bY#%0Mjla_-o*q4&DzV5%Q2%~o$p@u^ zo6WVdJAhRl@OK|ZJ^&goH$Zp4`26&@<`R>5ZG67RL%MuI9d|pZd~JX*OMwrD!`}(x zdG?*?>7G|Y_wckfi>9=N6pXfCrnEB?TKgGO+Uf|cuU)6~NpF(An0JnpzLE|9E_Ijs z96Eeq_!~cyVwml{hm%>LmWF?CTGojPZo|XiKKHnXAA>IQ^zu2-KYEeklR|sram+s< z>sxu(^Yru2K{tU$ocMUJ6YCJ~z2+t8!@aloNV16`&13=IU z_TCr127QJ17N7X)y#ebE&uRh62$b^)WW-%U?<#i-K-hOz1|?c$fAzf!bA*+Rw8zd7WzEX)V^Ij`hK` z2{Wb5q-2F?LTisI0<||Vr5%I#h{i`7H1W}2N?QF@xOM8mQRm_GEZjiiYf^@oo>bOla-DSf^PTA%I#pg#Z| z+8I-SDSbHTGfiu2Vw_`mC2VgVqK)gks0%yG<=aOCdrNS0`-^g|=auA1EIL_VZ9;}k{OGXq8?9`gOe+CZi6kK(EPB5O|*njYU&6_u0Zfa&ljPPm^+bK&O1sc$ayRmfNP;trF;hY-h4z=c;g7fHG_WPom z2HTL*zeXwZk4L2+7%)Ld1Bj#SC2GY?)#omqU(QCnzKiM43F?t4wF zLHFuN)4i}Jr1B%mhGE+Pn1pdP09v+b-aK1K>#MiaCktb8I@Ews$rvsylZV*mvPBM5 zVoc66V638SHGjRtsD0ftqCI`zN$uvxcP<}L{)*Aw5h=UtA6ro_*{ zs`r^&@2oHEqz|9l<#e5b8|j?({O`l|vyNVU^``{9{J(!fjEtWU2Xpp#{P@eNcdJ|1 z*9TRcZ3r4Z9cpf8gSrfARhiNIos;C&aaVj|m#*!P@50vxF78`g#Iu5{ecRo6GFPF~ zDNfCKE;lH3vOB>0jSerdyjI$^hH(gvv^xc=9n?~HdrmC_y>v97ekaVy#rHvYXX3le zy)-}1eAy%&BrkKHJ;fy-E(0^|KE@sjJMG?vqUi_0a<_I~UT_LgeuT8-h$Y4}!PYaIK(p$4gM=GAGs!&I4ErSMuSE+k`vqe!$*O0Lha2 z7Xjfs85MfV}!0p+fZ^ z1Ybq{a||W*heLVw1@h`owdQA&zaie)>ND9As=r+lX+HPg0bfP^rug~F(-z9BFOXNiA2PpjQa>-LU6s?)PATXr zfj$MFfZZ|${RB|;&p)nqkO8t%)&KHMmVzemE(Nf{-Je%0SdDLR42J}6NxLU;Gwh_I z4|ydo_NkksqW>L+yjHk67i4+;BkdaFmXChR-1Q&S3U;~c;Q72bq(wOfhP=vLeo2-WQ8vLXAN{^{gGj+cTq$-HaoM$ExykW6 zwv*4Tq0jx5KE1uqOQFw8l|Jpfk8(@WmQ?!e>3wz}S@Ky^=`(z1;+f};4SmAc%bUOr zg73HyP1BFOdh4$IfqaquQoTqHE#~4un-UyxG1m{_ZXUS4^jO~$AV%^HO7&{v5hT90 zEso&)PQ(l7Ba27%8&KRjP;{e!xv5$+FLk4^H^D1`M-imm)i4%X_J2TKiM^ElOJT@s znVa=oUXUa?%o7E5A9we?sM@?7X4-v<{S~0JTLiThs?7E5UkR^wSH_LSopD33SGq_{ znY(3|EYxKgHwSmx&BP9JB<0u*T=`0*Es3<=`5D(8cbWS!Kd+K|ZpH=M<&msEu_G)y zLej2Y-MpX?VwSlN56KI5M9kh^_YYo>?m34lu3zxl4^Y~zfjYusJ*7@1T)JO~JLCFb zuXK?Z?txsH8w;(6?%(20yDzc3n3Q`;bAvsJl>LjQ`)u50?n$J?Ty%dGcd476s12Agh`Q2y7VHyYl(}hC@0k5S#x25~cK^gKCgzNL25O~?5M}P=T}dNi zbV8@@Ga2_LZh9i@v|EBbf>5PyyN!IJBt$iPnaFmBk#ViC)9wIl!B)A$fILMl&;FIV zLss*x?n+J5?kE@&Egzky{GT4Z8mc48zghpwetc5)iQ z*e+L9aw1hY_98j6aW}<>*9NFgM%@i1^hy^Y%iKIF_32s3S%mu>Lzh8~GU_v^JE2P5 zcq)2?w&dIn!^0ng^6>kh1fS>5sgj&AmC0eQLAzd3YWY97-5}X>lTd@d7qi-ZL_rQS z>V6(#x|S(eV;zK6CbVVGHr^DnQ`RWO{m#}T8O+U=jJ;j+iTyAWRD z^0e!N`$^=Mxz;>BBA*?uPP>k{-^6E`oAGj9a3x{RAPk+$6|X3sFJPwKd)P|>rQHIk zaud|~{7SeQ=wRF#SB$;VMPka_u-&pyG|-!Ir`?sRdj?UFOc1 zP^*$l108_7%;nO%-3X|o zEz)zlR>GzFKX7Ng{mzXvYSKIxqf|8$3XytWlZj@8T|VCvM2A zEPx(Hn6rmB`@rRWjdT@3vi zRDYwMgSr~3)NQ8DMQBS=!%aLt40HgLhwlm{_&m3wN{X`D`HxN)zwB^zLal8y#y9Qb zt`?QmwADCi+R-fxmA30;#N%&!^o++|OH5*Fe2zn4tr* zFfXUwCAj62cDF;VFzRZk%}~5C^YDr!TJ8m8tcULe<>C2Ig0FIIiM;aB6}=#}2=r#> zQ`cvHl)6svx+!&|eCisawNIUVF33)uynO1q;+37cgK+R-LF$e-j8Ye4!qmyfr>>V# zO5G?ZpSlPgr_RHbIuBRs1YhOOBJwR$R~Yn$?^Ac&t5NDo;Eh-6N_^@jA;G6kK6huQ zPF_BB!|=ipm)$fRK6Nh`MyZQ2Vd~`LQ}=;UO5G|bpSlPgr_RHbIuBRs1YhOG5c!s= zYaWb;?^Czif+%$p;muL%`uWuTjs%}N`7Fv#oxFVNZpI5ooVp$AM5#L%s>~^MF(yo% ze0=IU7^T#m2jx>2q2ttfxKiigN}b@V++9RonYwjur{8me#33Z;XpC>l7vIL(*90sc z&`N5RY5c;LLrd6%KSbC1bGJcJbo`-HhZfpn-=a9UgdkUAGEQPMt_^lj?yfvKH<$vM zlrfRM98c@!3uQME4EppWWof+{!gy7_Dq{ zAY_HB^Dn~Zz7~epYK_8-3U(|}cmRV1I5q-keJ6_Gh^z=iM-d342pU=hJAFa;0a17@ z4MhQpIn;Dy77bSP9wwnGW|jKO zDz`+{upYK84&3W(Bf^l8rsK8P)WaD^*+prK}y=M+$(? ze#-^j1ebMw3@_w}i{N8}x+P~olkBrWCPss}aSK$Rpl)%IP-~n-)KpH&ahH5a&@zHr=b$?al=W)d3JKsa<^+7D7uu-x50jRhEy6#(%iv43151rV+tEhldkrMo?9YdkD zGV23c7YFAP+bB%NDejEhEN#}f|Fd}PsX{y-5=XjvJQc1zn*oxExBZM&pP<<^_s+xK zF;tE=cx&C;wk--);ZABtLpcGm=|Swz(^I6WzB{kOf(xAH3L6zC>)^e>F*zAD>TM44 zcj-Ii?-_Uu!T2h0L1ia!(+$3POK?q9pjz{$FuUqm`4I4DFc~Mw3@ou#Ez_T>-hZuX z`4udy);iu9Rm)X+_?5d3^OM0^{FAbN!Fau{B@58F?M5`NwJ0~(9pioeRoNxnw9z+q z1*=Kp>Ww{8xhKL(Fc~M&8JEJA%2Q3{Gt#JhBP>%{C&D6?2P(R9_vPGV@BscvS+g)+ zu|ZO~+TR4op7 z!GWNYvf5$1BGIE&o#xGIGwa-x_T*z%5a-9J_wmtOD2+eged(K)WLQSF@m~`E>n*HG zvyUFOA0d7zCgY?^#`VG`8IzB6K{;en)^8Y}jM>tu+#NY98En58Us+8s-gm=8=KJXS zAUFi3lQlT@UG-qpf*QY7t7WesSnv8Z&v+Xt{j)p`piKY z{Q5p+@QDP<*ksb(v^XE8?rCnmZ+;S3r3|XKyF%aYc6*mEWnnT-GBa)|wkq-2R*C<% z4BfipZIxK3j-yJvSkaIW+s`CKh5J(A8zC(rdTERk(hF{!5IIOlpI=GHROsh0J|T(Q zqc9|-DGvHi39OQkEt+GPlJ5bo$7Gy@X57QrN=dGzcmmV;W`<}z&<7|&X(@oPUn-jqb_(@s6W4xw4XoC_y zG$`oqX0g4LM4j838xTJLDr;wqhjylk6ZI`$qv=s_t)goeovO!(V8>=FeUF>_T_QMD z!DaP|ga1SAx!kS8cZhstjfs6Pl5d4el_C6LxRwoBEqrix3m@WJc!gW1=kl@$Uej~= z)$Hf;0{>htcQ<~U2r3Acl=VBtr_fZSETlx64=sg-fzCjWKAw!(1I`$^EWyKrdMB&l zZgBNZ&abOF%nY`U%CkU{I0Fu8YJ{z_%MAjTyQ_JwzX_R?^%2HLZqM~{m-lKi_(s06evEzp z;U9y~O89Och1s6f9}UjQ>W_w)16_7p0sJiB=u#MHd#7bkJ2+$HiZe+4fn`v4#X|=5 zg~94qVj$2o%p^FGE`Rtn>N&+}QdQNr87t=`cM&dKWN zhM0qnuD7O0x&rSX3sOf{Wwuk&{<|)(V+*D~?*l#)!^0e#IO=Kk;92PT7_Y2m59mw< zLvpnlgVDiq_}^nPPQl2sbG{{2do2k|Qm1mPB=g~nkt;4qwJj^j5;at1d}n36f>yZ2 zl5(n-GCWYYDB}-+NyayLAsO_6Ov)O9@!Hxe8Y8jq2!116E4L`PdXx>sO7NQq{(yqZ zniU6c=}YD^1^)_e_1=DPPF8O}#2hHQOz<5(36rc{&X(jq5HLorILYe#Ey=a5@{;6T zm1G62a2>u!%JE*x@Ic|BB<}(foqy%M^K{6htYVB$a`@gkUBSo0wIoNumE@i7v*071 zNd~toxUBo*;5+yvuU7E);aZZjo>GHzvYt{y%z-YSJeLTTOPH+Hv9Pn1+H!Z`3yI)Q z`O2#EX{2da?|apWF4z@jQdXDPH|q(v(CG=Mw>o{Yi#-FwM)WzqB&hp_)3+t`UQxb8 zBa==mQF;^-EX}%NFU+!%&CmdvtO79G0}2Bj;kJ^w3(gq1;*!zh(MsmcI|)bTp4CM>vn76X*=w@i~FET~e z-k3v+&ln!)RFTy*amVQ7N~qyT(yux?JjflRLu?yd?wI_;W~$Dub2G{lL81d8&h~N& z109b+V2eNpWPBP=33G6GuS)Y98vV2?wE;B;C1RySj70Bdttit zW!th|9EwgaKAlOmU@;>9RihdiClxd96m0d{wP=bdK^cUo;^%x3wnr%+bz#c{E_Y2@ zarqDaNm(s1K23*H+(K&Bb7S+$S2Z4Nk>moFXBshpi^7hrzL~FYBFoQuxmN3R!&x z{z+N)W2$Dg@-9(G-YLH+nKjhomj##gF($6ZK3rDC9jA&*n!6uDnwJCJfT=`P$!L+x~O%CwVjq@9?NcAoay2@`2&(#oCtGbh*;VM$s0V|;1d z$kPDbyS+zT7e{gF5SPWJ_x>m@F{8LV?c)+Aic2)ry4iNQ!8M9Y*6ndz#gp+}=LYF@ z_d&QX#y)!8tx|&(h}x6=;%i;Nm++TkGEM>zR!55(>!5$d_#|oRg9<2^L{wiCi-v4l zUC0<69LG+e6EGPk$z)H#`()2#I95?~9Bzw>B4(65p597DDR=v>&IuktSW?zA7_WB| z4M{89XNoIW7RIHMc@~#m4x^$FGm6X8J}zORq7Y3{+`e~i&z-|6{${7q6oz zx`MDC^8x_jt3=5#XaF5Q0&pJyYX~}FxPd|(( zl`;ii=kg6ca#N>s-B#b#kI#Nrj~td?-m%`TW5FkNLc)eJ74FA!WFLWSt8>0EpVmwN zj#jc%k={EqOahRUbr#0!w&pI&hdU|=f33oQhHm;Ypl>hvy*;gR^K^bwY7;qk*iVF* zp%6cXA@a-h23qdwaCGqnJd(0r!}yq{pJy@E-;TPn9L_39$a^~})l&f${<>tvv z+*KTz3`XIflyw8fYu|20q*uY9gx>Xm_L>_6(*%|EaSYW^MoD0FG9xc*bTT7v;|qS* zIisZ+c{c$=$USViSs@{^*2O|J%0k^gA0W4Ce-pn8e4hN~0Si`sTr$`NzLV7s5`wk24%MEqz@;D!V1)h{OHVzjJ>dI!X2_6K`l)XyKS=GikgLQMeW}J&eX?y{w8!AzA$O5<$d7T-o#P++uWcrrfGAUv{G0ZS$@9lDmFy_d38%s6SCaSgG_-uF2UpZo~gH@8kVr2UN1 zxHsV^zl#fJZd^D%+bNvNZ1*L#{Q!crAV^$u!h&6rT~(G?RZ$N9#CZ64;Adid#@$FI zs!~dl=5z0V&x%gA2lPQCJch|QMMboCDcaYdKgLumIK>|ooC5vjsI4rs2AWk0wRJci z&nl(UzRwNL!^HYzoK(!Xcd=>Zxj%8^4*p45<1jwIw1`R>UcHEnpA0uD!>1lL{;nI~ zm%-oauw!u6up^}HAv(klJAP%f(r|3}C@b8Ev>jF=O^+fUQ1e&$y(@)yI_m99HWA`5 zg-~xVRKAv7DTH>bnC(6WpHnbCEsnR+{CavdUE>snyx;oTUbx~~xgYRaLg>sy#z`tC zOR&keNB_tPbaEmoOD9}>T$))|UJYlM{17XQ`BSjS$Xy>n;h%un%SPn@s9t|h>w)HJ zz*qeOxZSEScWW9p4bB=ig|yFIKWxg%-F42f8k6XXwDU1uORcr21q}=u)y}z%5g0k6 z_*5%L6}Gg=(}AP>mm-R~MZ*ndeL86y*S#Pg#W4GCb}wvlq$@e{3S?5&zc5vEL~}IC zjA&2`c$}H>l~~8Kg69xsq5f93ja;soP&qeyqbo2gR_QtA?@?V z5A&+z%^hs#NDM~Wl^CzD7FpuF*$02}M)B30HfKv5Exihym^|X(FCmG5evb9$Om(>X4;j@5~I~S!AK~g7)!fvqC)s@FO z_rCVb=tNPsIEePlRO$H?&L^hoR?DcEqfw=eRwn_z36rsNO2m++*b?`_c@*Q7QVe90 zVE>I=n*zxI{tlCIk`?^u&g1gqx?K~SaMfKCN!B>HitY;X-^5XM$ES7Iry;c`hSR}{ zvBIrTO?nv4R!78xvqr=ry_G&}ox72(KZ)m&_CChvix!3AnzRT0WRl{mxhBOOk8fnb zT0VS>$v6q+_!~CW=;9wZu?{;a>u8LRN?lx3jow&U*~q?m+XY<(m-V+8KFW{N`A}_; z90@nBMz7y!S##5`WX)@MSk}}ufFGPSfDdV(HGTkJrIG!E^{2!~NLz{V`fBkg&YFh! zlQoL3=B&{ri&67x{9|sgFDBz8G2_0%CSO+Vl^YxmJ1OgQjF0C)CJCZ^IdRX*<`uk| z2+kK=)`%E>jh|EK_$tlodbn}EtnyP4$L`IPsDjCQISv-ZqE5euVo_AIl`Hv6yaVjh zDp1m@u%vbCk0GCA&EVEq$H`7i#z_+Ow%KnK&UtXGaB73hPbFXu#Jn=fxN-@4Eu6igg z{n|Nu;7>D9d^I=uzr4Lyn-8t8V5@P*pK^oKFc~LRGVXnB`ik!#%?bL$PRbgO@d|2_ zf7DkTz!`w(QC#Op7u+tmtoviQZBb;Dvlo4+P8r61#YSJVkFG<8eu0NgJ8023nj8{@s~08P}_Kb8~d++I@F0hp?Ks<&w+J*6zYO@ICpZGp9~0wtRjcALG; zU(W65$inDg4spDQ$vCNi_K!Am!D7gytTK$(ehImw#nItG-pOvJmeMQS*nD!LaD8?Y z*y=P{kL+o($YD*uI}mr{c8KeNxN0+F``jJPjIDS+Cl~-ADeE$f*X{Uwd@&9U8Xb|m za1Oaz?&dei4Q>@w){`;xQzp@(%F?WH<%6XGb8ZU+mzCZEt};7HaD}1F9^x;t8rW}D zp!v%+N4JKTt^ZJP7lz{$-pUI%B{d4W&1#ta zGOb5n&PL9IU@}hfD2(@c`_WAbCbS z@i@4!imOHBBG3}$$5aAYN9gB5Z7@2?(>C>JG~k8pQAWq3n-((Xc1m~UuJ#w7a<{+$ zI@CkjG60sln$wQ5U%fGa=>q6*YYd38-{z~CTV0$J=&Wl})?pZ*=1Bw>QfYpM4>aaK zj79dB@s~IT*hy8OWWU00GyCJiu7%An9N2%L-}7cCl12wN6ZlRHqhM_G-lacz`xJEC z?=>D|9&ahrSA-s9dao*(-YcH&9ZaTs7t-o&PcEc>2{S!wzIRgAe6P`fck4iIv=CUC zt_j^J(**`FQPO$q*vX4@DkF@;!d;;wBt#}n-R@7$mnCgY@N z#@&b={OSfAlMFh;F2AjP(39@fO_l4j-|1s)-?^r>FXAla?^GVP+D@=JYbDVbd=8QLSJn z!lm86@#+h;%w5SnB?8epg2Z%JW&-Yn8Ty@pAL*7~;x|Puzp09Re4;mkGbS}-5vhz6@0lwXR6Kj|4W5 z1vi=kLL3i2?Swd8iht|2o7_HF%1r??fY@yT+t~ds__}4D)={;h?GV zuhdYFR-)1_LZl7xj8yl(Qm2KWoxyGgqu^%hgcg0?G-;PXl{#&xivfc>${eFiBa>s{}^$$!)pKO}?^DjeY<5pmh&*J0^oc zFngy@pjxPVkn5Ht7dlw?pyi#_q;_J$HmvQRrzNH43<<*iVE;VdAJ9M8GSdNhT4thu z&`aIOKBG&b%jgRZ&eMXB*!_obxx9T1uHcZol3!I}wGK5E73LgBF|^EU>~y)ugD|-( z0(%4zrfuQrIjGG}d-(+aLh#A8J`4mO4KwWqVtcsu^eqQIJ|WnI#NTn(tuN{}sDq4} z1~m$5v(w20iPU{0?Ck5aS;BsVIScQ!)7gZtp_V(HJ9-FUV*&q0)6sFGdYrjSyEafq zK#fnxbFJ%udqScc_At0>-MLVkozDG9Kyxr0JUZH9`X1(uCS*O-^H9s3j{8QyW)BdP z&iHyzTd36rKNw2zZ{1tS+^tLPpg98l%^lRORn9FtA^7z&UIwUoX&{9_y!ROU&vDJ= ztesFzR1e+_y}m%kTK6Kkv(~+h?b|_vr6>4?GKWh6)eg4f%C)ppJ7{H;+QLvM-yR}( z+#Wn!?Li=}sDZiyieIUFZ4mbZ5dmol3KkN9be88IEkqb6c9 z)IW{74(daLf0LIN^aQR3SI`gmMi^xVs#hy7*kDuuRfidal`ev=bvwgb>-NVkby_Zr zd>Vg&I{jhnN&r$0B_ZtEgDQ2}fEYpT&H`;S^w4vlJhU6sD%TseD(@>$Xa8G=+Cd$P zNL^aNJ+=N#RZhF@$(U9aS7WGNP=pogcnWWoOF?xusxj0VP*kgD_vDro#E)0KY?U{g zCX{w>!5l(xrpKVTmJIuH+{>Bh;RHYgA4b2Xp2tIff=Y8$KEqqnVH0j2NWEMXsPUSx zms6m$Q>l8mN>y-vo1GS>M-t5g(yDDV@U%Uo}&$)04tp2-F6@>#l>h*4>3&>a-LZ z`80l>3bp~p9j5Y1C~_40F-x8HXGTye*f&mlF)f;_6L&+pjJtuqdF~MOQT4L+oxHU+ zoA3C2>u#jox4bV1PD9E0P7mb<*5+Lyo|3G3b%A*%_~ge(RkuRezLFgdrAo#_8LINp z1x&k_264|F*JarkFYZD{xtqsDD6c{#WqppZ7w=vVM31%mVd)KfY|yxWZ@9dp7u4q! zyk1UTa9DBC&{2VQar-q&eaTV5{oZAT;>OaJi^E{$=QFxEieR>$xg}Qnn1hy&7`oiO zu{H=!1)P-C3*%qqKh&eJ+%=jV1ViL2YfS9hM*A?zUEPgAFa@TQwH#ynvgOF$m#uZu zNr}c&YCVI8-=Lr;-cYVX4;tTFOZ(By9WZ``QzvJipz*X?`Q<9BAKZv%c`WSc z5e{C6`g}l*0-`u=lASUA!GfOb@engR<5|_f8h{?*U zHBP{)dbB94+)hwz*R225v5w|2Mh6wd@Czp6qyS^EL2Y=is6|iS5aX4xcbGL!u&N_j zZb9c#$n%Gm^k=vJfB{^}{ufsDEmy`8+2t6{hhwXr*FBeg>SF!i`a|xUQ#)}Vu17Im ztE&ZE)OFfsDsKK9?Sa71# zVFsY>e+~k5W7d~F6hjqx7~gEGy5Y&`r{hXZEq=z=;&Qj~nIL$Hut{03$3mK@7IT#K zG+OOG^iDRxt{nh2vn?XtE$AIKv)$c!hfV8wN3>gTby=0&g0-y2V+&VBPS6%QDXTrk zC%60k7X2#qHFv7_U^F%PM?PiZBb z&Dw`xG&SQ?HeBBcHYrQ@aoYA9?eq58o3+_toPyIrj^|IuPIX)7rx*{^vC}H+xDrNM zqa|lsp#2HnzcK!*PjsCiUAu6FsTEt5Z4mZ+ks@)i$vZAK?+A-cn&sQ|4Qwmmn$zSM zM8*R4+064?c;yuPA>PihlmI2elJ@}(Gk02?xwePQ^0VB%5F~;*fReIa!T1Cp#XKt+ zc-eOBgpjW+UB+q|s7Y84%bG(SXVbAiIDK$e$5M4w>y8)?)Unhm87N`u0^_r(mW8be za5To>^hy?PPRr6zS@YE6)cKH`7w<@JHm?^JP?c@@mcLcHCV9sNAl(b zEMHkmFqUYYBlWPX#qn{rqrbv^BMtZ6K1_6#?FFW@0>`yd-f^P6BTTfU<>%jL`8cZp zk=+m(3$R2BA16ADy&E4}OpXQ`CweJ@;zW1En`C|ZQ6jh&P*T=pj8DzSKDv)C;DPom zT3^=`G`6nqYYn))J?0t$VHyvCC zZz6_^eXvQv26l6XQZV}%W{Z(I*)^PW%T>!DeQnp9vql;!5?%~1|F5Df_-$$aQ zN%WH7X}^>tn#3HxC*Yk@WJk5#3xl)IkPL}-FI>JC@DkwFb}uMZ)pKE{rPY^6DsosZ zcn7f;H$~i}?AZMV0oF_{pUU04tV7KJkd*Z@#@e}7w0z6RT96tSu5H!b2cdNuZlR;L z9GH$Ge62w(@30rZ$PwxoZc(^=U&I}ZILjdq*Io*AG(@?Z_;^lmB7mf<^Dw5P>ZaE* zTHPKjIJ)&5l~W=Zans>*>0kxV1l-m>U!X~ z-uhZrufHPk=U6;Va@%RU!g#=8gmYaKq4iApL>R_C*T=rj^?D>HNZd@2nQ_d@$0|LO zByO{3`v2QuU|m|n9rA)E7?w1MjV|qHF5ycY0H-C!XNj(iqe~OacWHk)UVlxT2By~` z>{d+1DK7Hx@(xU{zXKibFUZ>5V^^@fn#?+~wZI${^Qa^Jyj2!pI+RRPo@qUUOLY!u2p28EVF{}O>J>(3uyjqzE1feY)5#MPL? zg`G~Wp`O%Sq3lN0q8{3(M7{>ajhPbC6NSCPY> z(%ylPB?lqo2MN)JQoTGh)oa~&>ZPL7hSHLIyq-g$#gDdjHamo%yY8G7G};U*G5$%G z-&=GPV)mhgXrmC?3~D}hK9+4w-FO8*2cPgkH_hF7SJsntINm6`(`le?u0+j^QT90O z2`YXZS`eC6SE|VU1bH6gYsN1~R@yX~^B6QON-|Y5`V>W5w2FgI;4Q~woPuZEquA8B zm5q7xUk9DTM=iWS?JleGd{r>SZT-#vLVvqK8$=v~;n=qqK3KxLz&Wdma5NO`KZvx+ z%4@;t>oYWHZ1U`I7KW4hNQb>|Xs92(Z+Kd7aP|6&23(LLgUbPj#W0tlq%Q``;1e5U zWw6Z$taDe@LFLT^*n#Tas#+c6+V3hWh^n7OxbuPY8m;K{;9aj7QTn>y+Ai z=EGeuNTqW(Uo5Hfq2InXlTAII&j0LG25<=N&z0ywoqvT#Z>Otx^=$@AliuW`;`OvI z-tZ0o$8n_aL%?wgk0O8^mcl%|;}q_WgN(m+5wDZW2=X(=YdMK9Rf<;MJ<>w>a7U0V!s~LZJ3Oc5OV!N^7A3c zq^vm@FLV@{jc@xCgWx@wPF8Y{uuQZyZObz8yW4!3h~7$>Jo+$YvJ0YA2Sx;)Zdm#d zQLS^QUQ1pc1pi2km#&(Ue}{Ilt#Jym&1~#fxqWmv*neO$PV)Gg5jIu0aWgvD`=RXt zleJTBQn*v@-TCY|dJ@2!7_Y+6nNbyJ+{cZ?fhrQAoX$GA*sAJIxxcVtqqh+M_$=Jk zM!C+cf%n8@ob=4N7qKa)XO2w4=%YRC>WE_);Kx) z1qEXh+6SY>yuKv^hK(rEUb;&Y+7^fCR!1iTt;8o~t*#>4qy+^-vtUS< zPS#F)MJh!{wK-n?U5<7K0X_ck+~7bENm;F8Xt>Mg+B3N43g-B)P6(FJ?A%njX6+xY zV#?uHF)KIyyvj)WnuKmHXEWfWM8UVY6G8bU&Gmk-&nny7r!=!s@CVx{xZdv)Ty47q z|LKpRZ?r9iyK^Swio|9VID$v~FMG4NNkBFaZZaun;Jw+YSOl4K2y5z_a(jScTOG`_ zy8?S8Aog%UU1O9=>Jg~%2?dytxF7c`xaYZsr6<^CNG*0JFYce`7sM~_ZA4CDbdKNh zG7?o-ir0eJA%w7{{9oBUp!Kg05X5JOpjNubbITR{$R}RGFSB5kEBG66mpZNBM^M`$ zq7{6_kak+Y&u5!mw1(dbMdCI5a8r)8G12_^~j*wBVhg4rgX; zxzj~l5zw|F>C&w(cwL=16pB!L@h9v&cP|392g&l=btHGkCq5#AweDk!RQCV~^NS*# zUaQqU+BBgP|9L;XhyrB7cEroJMp(<8Ze)wl&E96;gRVh~k-OjkkG;ZfE{l-4WBsLK z{(>=7SZ2%S4-zdg1dSi4bv@ChZJB!(T9?{tcI>?)8-}#xEH6I##AcS*>!-B)6}No& zmAV55w*Vz~{83y~&eEWcY z5SVwbbx+`4>s~T@q1o?Yk59A%z1DpJz1*G0X3vGFHWpsuDe~qb81EtEibQW}>TV=q z#=cTl{1&`Tu%qNXJhUrxE*`!nXjf<&?&rqI-W3|`+gU+2;i+Are?&oQ-W7UW`h@Dc zLM;!M+4iTO(9?DRZOol+S*ZTDJ39I8PYmvGm)h2+7#eMT^03OSPnTIRZGG~a*`fgP zwkHMf+nzi$+~B#)dXodtlgZ+s;HTUodRgD2++iYpj}ig4>`@}0xJQ|5!76){_i_6k zC4$C1$~UHydX!pCDtnaXD6>V6@~vwGTl+yTLBXx`lYZ~6Z4F%I6r10F)*8xh4LTXh zZw=}WRds7ng*#hs3*R7~w}rxB@w`Ap3%mG6&%Lq1u4~9LDByd|0NLbZL%%Fh3s#L`wT?Deb+KHBJ%RJBHtEzwMWe;{V~c!}vX;Q(umj z74p^v0ZskUtx(RdG;7D(uLxZ|GxwpS(!^v9uY|){J>WJ}D zqK_N#b&J0pqebd9PI37Hta|4`xYH)uCs6g;0OrQI?1rOtHxODMXU#Z8#0f`iRo6`} z(DzuAvh?L#AAR(ZlHoz!*^hIpfrt)2_`X*7E)BiCSi?H(AVZ z-NtCYd*#|hlX4dvPDpyl;dU9kDmkl0OX965jJlV&a0AQLis*Jlq?Ii(ZA5Q`7B*KS zEnKoa(87c(TL*;>TiH4|blAe?Aujw1%GV_8VI(JIJ&mbMIr-9ne0f8@vP!G?hI#Qw zcyI9H?GnL{Ldx3nut<(}&-iYo?hU!Ce(`e%BLf^7Bd>QC@EyClx8`oZhv;UpOQ}6! zld=Y2;_R8|bXmUFEo-mR7-hkAiQF+8d0je;x0oQfHqp9aBA6Tn*0z{9Z?h(>mbh?Q zt9M)Sb{%lbTQTFwu%^n}?U-GP^H$z*-g-xvx84!vt?jQVcjq+WS=fx^$ZCS|`cGqW zs#@;de^??oL`YfZ#Yk#SW%G(JhC@m0>Ibu5WSj}PHZh;uD0XTdDxiHY zaS3G2nk});i5{)};grBdz^w$V*NHAZwGx=gp1ZgN|z&O zh2*5HdoeyW(dUwA%TfJ3kd0Qeg_N~CMrvnFTmqBmr{vhO1gr<+;=+!kHKouTaAgNk zK_LQV9geZmP-fWfV{yj_9;%eJp=gX!Vz)wnk0iSgaXf!12`P82n{w<0Dkv6zUcV0s3V(6r-a!ggJ;(Dp)5$ZomEfI(Z z@+13d{+F-?EuyTyj_EGtq3$b6-l@4Su0I2zT2;9jrY-&Kun`yb84{nqEb68$Ej>m> z`@-nV)zxQx0*sGX4I-K`7^a31nQ?WX=@5SB5|D==<21$9F>5c+5~rM@IyUBm=*ggE zSRKW*4#5a@2;aVz)gg!*S4Zy%tD|>>)zP*#F;De&B4~uO)`NBI8-DdFez2}{J+;HBE$A+B2<>pH zGO;XSdJhQqI@!L0LGUiZ@Lb2H0}eAK5Sh&m@&O|KDF%eMAa!O_8c1p5S& zagxdz-DxE*_yzK#+@hc_lL>CBTsNE?W{-9R`H4Aw+hBi2b}u_!A5?!};)COP-MJOF z-V|^P%3WO1JXT>BN&A7P-8yfiD+up#}62MioKaO4F+{gH{sQQQ2i?secNbM>>BXBcV^Ey{DSxHP~#H0Q%nGLqe`CzkX?HDdkI}`w!Yp^X0V2XQYRB}+a2Ot>Yaax% z-!iA2ZhxSvK95`f%EYmUtEiU1OuIL+&j6WrFF}noY8KQrP^C`y@cW^Iv+DaKg zQ;TSuXTk8$lcBiC0(-gBeNi#&VM=cb45sGIz8rgeLXk~KJPf_YB91ny<>ez#h#=qbrx}c2P5sYE$jjSzG(_zCH7}qy4a{` zM%?TY6K~5Bn>8yh7=#ZqghAe)X}1XX-;thn^Pz5nV&<}s$1pe#@_M;Di74&H!<>$H z#*Knn=_Eve35gqU&&5Y+Sp8UDFajT)-pxCkH2eu8?P@>HcjbZ1b64Q`W_UC)hg$-& zew&^4(-qL;Hdi^0Nc?IMzXCI35zmGSeA{gU>Lj5?Cl(!}e5h=@o51?E>nXL}eD>e@ zwi^Zfk8L*w*4%a#d3M{4;Wf0~-H3gBLLq$H?Sk94-F76yx7|l971i5rNBpjL?h^TIwe3cTZM5AOvbDB*DzRj@-9e;3{4H~Xn&br~0E*z$ z|9fKUajGxVVO{|$?IuIrVbmn3N1;lcu8oR@w_?F7lp`sPlDh@K2MjFD*s<)Tv1C2IC`Bp##*9Mzw*GGT9=|g;#fv zs3l)bn)W13seOpKvL!bK;9If)z9l=-9JS<#n_Yh5ZCQS5$v5GX-ICiN-M8c-DBqHW z30rb+%UreObMTH@as==#c@#cTOTLq!HMHbOK<2rJ@vN~Wms)>38}DO~RR15OU;VM~ zi+P=fF13C*g3u3VtRK#}%08I;JLpc5>2BpFjQW$!aWxV*yZ26Iwum173w6!6C3bEvZ8I>Z;GIov zg!hv1Rza0RaXp81kjbuj!Tx(G=|z9C`id~k#MQkiH(|ogfmwn#>bIy<6ck*Zoh{TTx_luOcJ@JTzdmiRhgyR??)cAxv%bd33{{~dAhIJoLyYF$&G}L;i zQm1PIBY#`-?hiu%)`p>8GU!mKc`jMIsB+0WF-EFLnG=a^C%hW$hcb61F6yX?G8tyZ zO~OVF_IoDrLa60X{y`N5YS^5{v8|WP7+CXzN)cy2sA6~x530o8DhGw|532UK{e!AG zN$?MO#^V{`_#TKLAQ7sDE$b&=Zsv4F@we9L(5IkfMU)hky~--u^WN%s37IST4X` z=_2p7PQyMwDI5*?vWI&UQKmA~goK8BkHiI}kiLK_I!UGK1#|)aTPq;>`2xD$ z!umIfh?`wN;?-P0!y})q7EpxPMghf;nhHq6y%EF`6_AE|FOz;h+>3nT;hwzWvQsOZ zNZ1oqcGD8claxhjh0G&htMl?LaS5T)PAzejiPms03b>7ySZc$)7+6zFT`S}7LXJLJTN8IcJ7cW7k zZ8^SwBJ$a4fk%jK6nG4&slYYtn@ucHfos@z66p_r%iI|==sE%H52yaD#CKMz*TLkr z7qdrTm$`F!qC~KIcd?fXH+S}6uXGW=%;^ACn_<#B7>5ZugEFbMh_o;``zWe! zwEP+0x=P}cyo^2z<8}h2-6K%X8g(DkBB+%vf|fa*{Mtl76?5HZ(yphHW9Si3q&9{g$d8(z(z*u13|#}L%$LgtE2hT%cSKzWe9 z#1(`30Mh5Rl#c|x#Ery zd|NuU1!QuIC09bq-EB|i1g}CRWi7{4l`HNT!M7#%W^# zU(IBnUyU4EF-ds`LYlM3J9)YZVQpNeQsd>CtR3k~?EY-vOOGPXDN&p{JzD)dY9{+U zYUBvx3{RUb{30iK0vSnJFJk;!!O<*eu!_*Idc@c!hNe?E2{)+ zx`L8Yh|w$S1i@DdpB;30&KMu`m4`Sg67WFQsunU|3;8F`gx@&i_YE4{+Ro!Q?M;#E zc^}ukxX824v@Y>?K3BNgjzu$#7;M2QdWxI+#_|8SIV=Rbwlo-jJV|3a=};& zH1e#0&tZVU58-A?FB|MXYOnM1OjdK`hpE|MPYi-QBZvv7#XI4PKMQ?XfM zZE?N}`oNx(WbS~D%1xDP`g!3A2swNssa$A1abIn_w}saBP7AHmU`$HXZ|`mfoo`L% zPj?MdA(#Ixo-R4hsa0x~)Gwg=(;z|p-o{^ygm5aQkIR}$>1&SN_KT)cI_DGx4``01 z*j>~^ucPW8&cZvJ(p1Xz#?w^F>rmqp3N#_1sg!lN{ZxvCg;Ob~m@rMH)YH3jshi!l zcF-Mvdt1DflMf%j*b~2uTL{I^PtN}h%pxh&+uel>|IU@Rcdo-F$xpsq0BWUBecZmM zOKo+4!%do>e37T0d=c~j0!EWBQw&u(`4aiZlP?1J$(JV#>L*`DAtpX=ki3;h^<#wa zAg2`Z)BH~Tf$q#Rq|I2E3oP(mP-!<2dl~Lhr>-}GbI>5|)M~48YFWffrzuGt73Q!1LUs6?nC&&@#VPv;CKN^@Pp&cXDS8N zXG7k=e`|wd`PH^kn_^-8;5g!D501sNve{zs`@P7gN~!tF2YiV|h-xL4z4#qNwl+Ba zfLQ2hZSh-!;}=MO`12+C4S?n<$xGaiJ)@HJQyw3H^-~@dMrq0;bzoGQQNaILnlZ5E z(p2Qxr5VF(D9xjZeSAV8d})rw?Mssnk%p!DzNMmiY5p7kt(B(yd}*FodD zi1=kT)e*U>PIb&DPCwPrlE}84>WI1VR7b?gp6WP)uoPpES7oYWC9>zaBK)>K)o~iW zHq~(*ov)@kej|{d>L@r!lwL6phFa+&sGsUMgMcdLXsY8_Lu;zzG@~@taUqnS>WI+s zRELLas>8!I)gkyicP@groa%Vl=@=fb0DAhf(Q30jY2M+bqMe?5UEG4RqbJP-#IWxs ze$q_Cz1*HO5j=jPnsB38DNz*>0soJ{lqX3pVt=S6DlUkDZXOX3O5xiGSxOQNC0JYh6 zUY~&g3YuSS|^N}EO)vu$6G<9cRJd6kxf|+7$fm!NuzzWm zp(aHrasPX~00Zq_(29RJ;{@gFIv6CC{ ztTvDKJuI8j9dpwtax0!kTQtRTze2&9(+^Yzvw1X40^2;=-uP7sx;*D*AN1ubm@hjC z?`R$^pOc1^lF}lQF&&+K^EpbhU^6>aGGh z0K_djo@lQ;ZmL}CFn$t1VTRMtSGp-6^p@dIn>G2_9p6hGgzgiHyqc`%&EFBx+sU70w#uv5?zsU;ZJ zrjWo+jwR2l7}mL&y8UP>fTuA&r*->LmB~DXmuia7 zCgk3O@d<{>8EllVg3zSekMK8CBG$S06i>bWdBJWNyX(L5Ah06LzgYULaS9a0#nD_+ z9)F*{Ga-9shvwp6oEcYu{r|{&4>+reZ0&p3KHW4Rf`}q!M$7?evX~eJBngb*D2`*( zKob-iY?`E_fO*tW5FzTHvmrb5AsM0#{PH5rYOrQS~jQMO+ae zQX_wJY^D3)B)Z4JD3jM5*JK@gTGb&sB^IO+oqV`@c2bHP=}CgeSc^Tm3P-zxMl^P0;7?o~^ zE;+%2gc9o7;0uUOo`6++T5(mgAPm*YL>Q`L z2t$56-fhQR4gQLNCsfz)gwXiX>L3sxOG4gBu|~V?1-sAtkhPk~m9Ar15R8zdyz6mI z3V6J%6`$kwHMZ-K z!F5*B+F>Gi7k)xs5|=J1w%p${5qu5N$=jICUF&)_XWOC9IW>Ym;C#Ba+^LcEIw=tk zHRB{?3lQD75M|hi0fo#9x?~|?{0`U0q=44$#H9Oitp4z6gU$0X_DRAo;?l;_?j-g< zY@$K8bsAP9I2o68GE1%ZT#yjzi^SaxphpZNl#n+T=S}WINRqSF&PAqECU_&EguJJ4 zUh+ahY9`=Jv`l2+OteZE&IG8ZWEQ@LR5DJ}@KB|`l|FqelU%As>U`hDDMDdO3>rGD zXsgH)dk*I|1RV1M(*9}{ns1#&@}jf~N;~*k8nj4+3+XKCpG%nQ7a)gM6P$zdr50lmb&}&$D4M?VXsp%#iP#?B)lSe+l`t9IccEjpWHPkm?BYm z_r{{C(haUXuuwb9eCryUb=)8WHXu<2V?@{=_H~T{{zyJ(?>?fLWE`=tYdnYYywnqQ zyN{$i+Q0DPDYSP*Fzvw#7qmhkA+JD^+Nr1(O=M+G2AYVE_+fRed+}9f6~&~!6zA(9 zn!5rZGGU^hl}f!moAjiCrc)GM)f3IGc{Cm}IZ9`%g-tLlsWEdr!>B%S_~gCCVdlfm&_7XcT_)`;HOXPXMEG- zf9`Kn5Hu>`Lqi-p?PQd6x8q}6uQimqCG_q%Z(O?!;}V^SV4PBBSHbpKQXOu<9q4)p z2Ue!}$Klf|=jY@G&l60@dmZPkIY@R-rnq#Ss=q1@8gxwjV-uaikFHhSFzAKE zvY$E!1L$4c48>-i$NOz)Fn*^;R|tK^=F3%Ky_=l}l&Lq(2~Z~WwZ;s01Ci9hmzfsX z`v~oR3(w{&bn|l+hXY)b(c1R;9^x^u%Uy0$E;Y5#SI~yUq3a2)bWuXCZbohaw56`{ z1+S0^s1Cnu!dh;Sevus$*!+N`FE%!iVI=~9-uN#zK7*I>#fBhwcKf+=P?Mg&-V8U_ zj1u}z-KLUzgV4lK%nCQQwio?_vg07kMa0{g)U|}l+`X&#C>B-X+qW1`L6AVXdz;Y7 zCi)Vg`EFmt;%_l@hot_NI@NVsSdEF*o8^X#QGp(ZSZG>aByGAF>FNYH*76M)ZM zCe)pfgjTv^(8Nh3R0yf(WlQ)I*FKBRxDbT5vJF=|!DZIQy5(`6M2b}>0qb4}VpZ72 zLd?2=8-9yC2POP1bwXA;outLY>dkgHjDiSGcFjGLrE7c_nf7H1Nya68zN^ zC!K0i_%pPRkK$SXIPCXxz|@&xhd0 z^4}F?+ZNSzCMVq)#_Wavv75cX^LmLlcqcRYMQEI|!Jin4J}G>Qa65{qpVpM(YP1be ziXkU#mO^x&qKBpU)KW|*#T801H0d(+i{_;ZO!5IJ6y+mV) zV*0&)1@UtCA^xMLWg1Uf{JLbSqX`cZuW%2fbQ_i5>vi{DnY*2rB$Bn~ITu2iYs>qv z0u^;oQD+WM@+1fqAd+N|XC>XM#LL~&_)Ez->9o1G(nSd?oIuZ4p+>vbbspwm6mcfy z_~mXf{yI{XxgEaa>;9A!o8U>e4+O?b{Bn0NzQk6!b%;b8;OMizGAG2fA5xaqo5!pC zJcOKPs@Hjh8e2%vYb^<_bdhL<(@lvRW81htnD|L1eLSHvEYzIP7(!+4Mrt>bwvGDx zAb9a-33>5H2uXaE%iWk^EBCj7R+IREF3rBd6^NT3W;86Zd-!rDGTY0BrFK;al`-wU z`nkauBuvN)&yU754Zrc2_DUVzpC8?>`C+g>61~X#RvXh4d*`C;G3_O`g0IBJ>@iI+ zU-6js^KnMASpcB;tpQ&VCBY;_7d^7rfe~xGWYTq+_Q`(@oh-^5P}5C-C9E3P4p8&^W99u zsu|K$)$?HiY@;FV=nGUqYasSBEk6<}B~<3l!mLQphBTq7d7&!_Nob{e0Zr;FX~)oR zwuHKIFrtUA(b#pRpl4n4Wp2!}A#ElEu`1lM@(dP3+FIh`_`ju2fOa2{+bIwj(ne3v zOS&9l(!Fc`E9UCmq4;I)9}CkwDmCfuhQNU#;z!#4QcgX6*CWix!d7I z&-6pu4Hv2+_J@{qd*GApat5!BWV;{PYDoLVhP3tg%U$ksp2x_-koLJHJ^&iOuiyg% zL^|;%h55vPvghSf<^7WCn1}3b10_Old9MY3Nx;`X|X;_;}yxh&kKbbr$+;QJ>tvi{uATteXrx34jy5cmZGOX!F)H0{LJ|n6< z(;azH{BV3KD(rdO?=GJtJJQ4PIppJq;|ncE4abpq)!{gz`{6i>We>+wOpAu&eq_4& za2!*!hU17@WjOASifV@Am^>?UIG#<;(Qq6|_~CdEYPK{SKTaw?97j^s4#yH(<%&_X z`EYzVz0TN7OOJ+7VWV_R#!?N^KsuVQ!Fi>NxPD+hKf_85%>3S9z6NAJ7)KIjp$21S zH0Em%p6@PBS!X!oF_@lg6BkTEV(yQfgHJKb%}}h#<*D-#ig=_+zhD+J@0k>Z;}AEyTVd8Vn8|qQ zEG)}-n$f}{h8Z!_DU$k%@@A(*z}GEx(+NC7)`yTaQ;xGQQIGHh#Hm)%FA=)OLPFMj zHV%D9Xr+r1P6`*pJ)))NFc&ks6`dJ@rW~D2;f)a6!%j^(UV%Rufp{5x7QBpQwBSTP zN9JB+m~w0|9RdNp%;c^iG%*yj!s&ipiAKxlLBt<6sZoT=+>UFQ#-;c+=V%N;0!*w3 zy=0wdk`%sED0sv7D9@k2{nL1b!~i9|AYUU zA`p##vxr~oD?pV{X6vC22&iASZbS>Bdyw#pq8BWrMbQU@R=P;k_r#q^=zC%&qb!EL zFljB0Hd;svq}`dY_#Qcuwr)fVBrmQ-k{8!PN#gTeFBEV7jq*CTJ7BkoVYCHXg~V{N zYAyaHjyskNDBYJ0O7{oHa>T#P^a>As90oH;HSvee!4DMO%~0&zt5Us!A{jWbzH7K+ zCJ@$`XI;ZHNYqY)#K!%Dz(wQ!p@~IO{evQL|DcoxZ~5|$)ceg&7@~Z2z0?W!^csxP z9G?DS+7)^mPJ%d)oXcGwLgOqX=+nt@=oLaMU6gQASP1tQXiME$Z+eBP{$h+NJQrg9 zHe}(q-EJboewqEnk?=D53*o)aLuQ=sFAn;b{oztGjx5r8qHp`4?nVIiT$gv#7i zw2w&IdXrlrc=1OGdGUJ*NqoL*i{k2flRgWWoxDgJvEC#GiB?<6c=_WlB?AumWrIWZ zT}rwWV6)@Ir9k|X-pR|qI@O&hvKc_Mx)bY4Y9Z#k5<`aG&aQ-C4&RskmykN!{=QH$ za@#KZJtlRft9f8p%#Fh7Y7B`-D9ejX^HhUusmPu6J zmuXQ7P4`yWALUg~WgueUDWhI~Lexawj-*2&- z8-?h88)>1R>VDxj%j|wz!>ii;BGcJ-zjvEa>VD5MIp6(?S>devMKqeOsr$Xsq%ykS z6hH2MCE&Z?nI@X*ei5st`_)7G0M>4pPxpK5wJK1fDSRkvY1nQifQF0_`o`)zo1)cqb}A$7mU5aKV@{gNQn{hol8 z+vt8%!rShCBf%|nzbSTB_Zuatru*%Q#dNCAoi`v)t*=G`v>nceS9 zUjeFwRQHQOR`>fD62AL=+d}GoR}iB6O^K$uUt*cv@AoFH?stb{qVD$~LaFXIB^`Ia zUR>R;7gzTyak^hbtLuLI0u`S)kv5XueW@*Fy!_khzOvBQFYd*+6c?hvzWWs7x_zsr zu08c-@AVll>=v$_LZ5%=#W0vc%7nZbIN$RLeV$*5M}bWr&Vjlhi<9*|gA(}<0J@JAfbYWQfs^(C~|x{jM*aT)opb`sl`PWy`CBvSUk0TqfLeC3)^o;p3l z0%D~&uT}8wSvD)lw)CgJ1ko4v_M!V@Bzqdi?_>Ck41XTtf+YkK^6E`ZeWD`JcAv5E z-TgPhb3m2rOO1hMW|SZ^B@RmlwiWSnnY#XBy4|bAl_HutK_qg!WR!AWcK!JmU8$ z13TtrgMRVTnFy(A`PnX*ODZRC9nNf3Bom-1w)Hd2Gv8k1qA2 zduNyn>fcx+Xb;wB1N)6jv9qS9j8?=WO8G^nj6Rxf)EY!nn6(W~YRiVlpK_>ml#nu> z^eCb2NeQ{92iYIOpurmR7qoi#jTwmh(5n^_5XX8835cT(;I}JXl+c55G=}EEI7DX( zqCKgPOrgLyS_m)KPjXI!FM)VZsuR47NsbVU#v;=Q8TO>Ex=BU}W~12To+Sh_0!$CH zA<+Yr9cxTsoK7>TG|WcCw@HpLP9@-BHXblh53?}=HPuXV^f+d~(s;-uC+B7vGYR5z zmUb2)uBpc_bJtU=k*G~=gb7w*VjmJpy8kHdKK_>ZG#aBlsqcsW{)q|qvp2CmAl>WJ|p9OaILZ5mTOsH%r@j0H7~sfygc(nah{D94Ca z9hBoCSe#Df@eTSy^Y{jHDT1HIJYXfLif`~JeEl+1-Bk_ZsorZWKFQMhQ?(H_^Hi;9 zs?W?*wf?Gxrz6f5(JCT|DyP0P(JEq*E#VtHLoU%=OlTF?`lpCRJV`-0euBV0#Q0(9 z$!U6$!Z%=-0skIkIbu%eDMB97F%qwea}d!h4Cf$9ST;<)lMCh)&~s2IjD2I3Mfd&gDoVSgJTG- zbdjjXImjhpH0>0mV^5P7&Ov_*3FqKKLLTQJlD285ARWE9a1Ok<(NnweN4Y)q#5$ze<Q*xPsg;5mSeBFLQ@lKrMN|(`q!?$o6l2s37=vKwkPfGy3x1OMYu!VX zHyjQ9YFxTLBt?DPZ`HBdDWQL0)q~V+{#=+&SADj2_>2>|y$A#=(fBPc>6CfWO~A*T zH~)z_jc%h|;x2Xtr{)`kqC(9B72BIQTcza`EQ!plR;8my4lC-?wO9J&+xDAeciZyK zb;|;OyDeCEWIk&P25>|U4yHD^2U8y+{BN8$Yl=5ZNqueQ7xa-~1zIU}>3(*zj=$AH z9jVxJ7Uy4Zz}$HYYTtnc`B09*c?*v47Dz1hyb3xO6%O=Aq-DTXYe*OVcC_{5K0U~5lED<+jR_jU#h90)`{_ZHL0eNQL{ z;(YFJ_^w1^)s+;Gyv`Rwoa*-Ol4U$wPw zH80ENOVWOgOFF5hD|vRf3wF7Ku3Vmw-E7^Wh==U+ImhdkBVc%RzAaDxtF8wZY1q4! zgxSNM5aWCv+Zy&ffF=h--_8mCMXH3nr*Yo6n|!a*Z&3~d0n|@o&ICA8ZeP(~-ha)*{xx~wkQZXGnLYuiy?qa7y|I=Q5T*0X(AVZg&NL>x;TrIwY#E3zOiKQ2>s@* zhfG{d+e$iaHX8{ze(@qN%-a2E%<2QB0GDC#YA3P(He;-PFC>S>xTKQ->_NTK)@cvw z6Jp=vyg9-Q&8j6O`EN62f^tV z=P)nQMTC2-&lB74SH%&*iSF@jp73NAIKj9OQJQX3n1=1~eV$-q{2RN+k9qHq^LNh9 z*%l5P6zuVfQP#V@^^x6V>Sfc}m(gyU$|hMLRG1B>y32!x5c8vZzSsqQ;ZNM5OHjyM z(#=rp^m|jCh$8+4PwyjK$GkX&``Ofe#2ocM5ZG8~>Mlpx_bjQ)6Pp>T=X&_5?SrNf zFxXOe2n*KB$yH0Qdd*w~Y1#3ISmqA9FU_iR0N3PnfRJ?U@Ur5YekVhF^Eg<8_R$l>x8rixQv8Zet%*Bml8~aj!Dpi zj=h9dEjzP8$NIwepkw0urRVcznqPWG)Xb%)Xw@%0UynFhOHU6vHq1=!){r01Qc_ix zpB{8fVjgrX7Rv%16W{;McMqXRsIbpC_or~Qzd44F#q`lwj@{W(>3Teo6P!aOCghF5 z`S-A|y5x0k`KdJi+B_iZAUALPohC+&zy8CCbmNaBYW(#dNu(P;5v8%lUw>XA-T3Q2 zk$8*TGa7&Wr;XwpfBpHUUN&8HIgMX!scPfD!5aTscfh|GZq%wy!TI9+F>|;%*$+2T zvF+jJH}}RZ*z?U8m0H_IaBFP@GwJ1cB&Yb6%08Tb2mJfMz(0p|lkSmA$lLY)s0`P! zWQtZBhCQ^Fn^>8N9D&5KI2*$oj>}qMeeGd#q{MfkTb`Hr(bO-aq7_#2qXvv9E-e`` zY#6Xo!;40ZDjZz2rRCP6OmT-LXT}r-at@ zs-((|h18$jY^E4!T@UKjDv8Pq{uya~p2rqFYP;~D(zP4O{l^F-am1!m*stIXeO-{wosIuG*MEv`Ci zxa|cKu=}V>uzNqGj=*^v@AEb)sjtVaEjfR?xmuM?CyzUDNhcF9=kg0N=U;@M#(8r( zdUGU}daPSqe{Rc|^kQ_EhiU|S;*w68k?|Afk@2BWTH$=g&3(oatF|7tu^8Q!G11EO zHuT?(OFCtbDKC%4l*b4!zP4gUzG zJI>oO&)Xs~Z_5^kgKcJPbNbs*bT=;Pln)(H?wRx??-8zwHMQPlU`?%e2EO+o^cvs+GQ;(q|R4C8Y0f148Sk|KFyX-7a(&9j^P;}#bcy+D&aXeZ%hQR2PoaU zu8Xf;rP+#ZYL4N*SwZkIa$n&1zR>)c_`w(M87gJ>J6-rQ(fSWZ843L^s{~59m0YSo z-`NlIJkRYYuV`9TXLZkyq^U(K+siC^1--~&w1b>c}Ub6FqZQ@kCRB5fd;kQY80 z6>n*3iZq^=3yes;V(nqZV|-wn$IT&aK1EucGu2O#4*Dy}&d*9_W4tC+2BkS%=`&|n z%W}@}Nq_z$WBOFs8m)CDO^Uu>d+0hMl^W}(H1YCpD67w)+oD0xfwp4S0gq@`Ma0y# zD%UGq`v{J0QUkj+41*s?nUI(BSX2W77~}!(wma`%1E>dQak5?uCGxLD_O40>_(BQ; z(BAPv#E0OLPI}3s5!YmdV+mh`^Uce0U+pBeeVVQDK85)6xTKRd_B*cI)xNoThuBJ- z*Dd72Ec=yY`*Nvf^Ww;Pd=P`1YIB`hmqKa0D-J-$0AebaMp zkmx%1kRIiSh*X^e?2w5!=_BNu4>0sHU&&MEsb4BIV|G$Ax4!JmXxwX?g0#W z6_lmbH$%pNapUsy^P79@tWl*ULyHGj2cN}+Vu1aS!Jyx=;Ow)5chT`3jo-eF;B+B>Xk=%RNx z17P!dg=-JMBg3W_xnLbB6Y_q>`3!}!$t!mErMzN0JjDn8xHKp0O;Doh-h?guT#y34 zv@xIvt;29hr##57_r>IQKHHK*968rVJ*c-l(q>FK!;KJt(pE=3ze-ry2=kpPi zXI8nC-{v)BNem^^obR%*7 zN5ceXl-3&(cKvywf%vU+r}abQ%hIS%8hl!TmDS(XDZ+EE!>3ChB9M^x3(l8Xz@Mr2 z_s{nJYD=X!Nc7ENJ3;AY(w_I2Pda(W;;cPaiyZoH26Cu{d7xAN&Oq0lWQ91N<=@Y$ zQqj^%MQ?eYJHXF#ry@5EmvmBy@?E6-KEjXU(#mNq65EDKYZ{*+w+5GVQbTdCqq2f$ zD6YKSaOrB&J4emjnd%VrgTkSbl-Fr9$zessgG+}5iHlR>hoQKy#O00565rss%@V-l zlw@xdUo1&^)3PLG!pI;oA=f{fjp%$plJcI2CDlh!JNfK$g|9$e9dnL@BOL<^>05_O zi|hB`#%crvtY>3fkh?MZLb0Jy88X^S$2MKNhed1?rrmv_|*19 zdb?tn33=mj>Dq>SHnnoKxbkkQ%6%IR_lqmfl_#Ad-qtGh9l*YqDq0nl(^VvHS zL464`-B72~j-d5kLa$Y80(igsE)Z#f%~sKycOg6o=gY8uN@|u{OdBi=_I%12NbPDT zJ-)YXQ&KdjsHCWPKvBWilERTAi%PmgKy$W%uLbcp;XnkDhDtX*41*;E z6Y>Ok=k*JRPfZ+T_&SMIAnqYLnqLrwf0vL~7w1KfMig$F1H#}B;>tTJ<_i2MR&+d= zM>PKqjp)fcBl6c+tT(Sz#w6qFP#XMW^0>&Qh5h+{itY5Ry#>8XTaGLoTGFK(_wBar zUs%FNd!tG_3Imj1xvGFI_vU-P0j2ry^V=251$k0f z2CW^mGS@GUX<3TiXhB}N8wKHHSmo{lLjAH5u5njFTjOrRFLQ6L$W14GjHKT}7)64l zTS=(gmEm7*xs3QQEoxuKUIfACax8R=S1ogb z`7VO|s_J7A*Mkev`$2Rpb7Lu(SS!UE!q>jHLwEreS6mVLz(Q9NlK4{Bih@X-CK~M4 z&wWqT0ek~7={n<6$WLnURR9vIx7`My@H+@!n3^4t*kqxAP=gax#gXV5w+FN}?jZa! z*Z0%hSV^NL)YL!-2a6+tRC6_}_&`-r`l77B1;z308%&stc4pxU^MtXcGJHvc)F0 z&yvB*I0i_3`mG0<@_a}zA@5V1x5uVDYuzt!!*8JO{GUutx~s~9*;aDjX|?og@DYPp zg%=eLuWl9Iu|qS)n4akUGY-gX5>o@eQb#^IrjmYITE|+~?_|yYp;X|~<*CkU_07Pf zFAOta(iepWlg?O4^6f8=eMCSnyTvK8ZN9Pdpf4X=6m%Ies-Wk{qT=Br1{MW>eyv6@ z6vukQ3i68i@8WB2RdccpienL&=OlJHuC@X^zq7!=eTHd3PN z+^|Az2N2#e6n75~#m%S9lC8H8*(|i~(WODn!%fE591+{yL3w`?^dZ3!1zoxrE_PLL z?PMmnb`(inU7v!^9`&{h2BRY(uLS1{Ba|QBo6|pK-gqU{r*TcT2&z5RWq@k?h>Qg_ z`+7H9npQihwkO-#Us1FfUUt65+FyG@u~~t?=$>!h`xG#QveMy8>Z2F&qT^zNIafmV zoD(wtcJm*^r-Y4PqJ#qw9*6T~KcMOkqjdQ`A_m;nTW|~HSva{>?*)o?a2}EA!?&-pj zjm`wEbR*fRw<8+|GkZ)O*%+SK0e0%hMzKYBfj`^mg1!q_ouGdp7UnPcK!Ziwx0(S zXKec`QsQp~EKVqbb!RmWgWX7$khdSs+pbwJ+z<8*gI40oYhRT+IwuTziz`pCH(BMr z>8&sj?m|M|-(v3DvYp+h#bI!*xbp6axx$=?AMaQIe88@l4Dj}UkeCHV5hzDYC>5Le zRyIhfYWp~*A6JI;&RlL#(f1eC=$aCzhcnN#QesmpI zxZrSz6Txda1RLB8#XiB=ckzl;g=(9Fwb!7#GdvIObZuxC*`6r`wncE4I{}Pcox+x> z*KY}u9$)NBxJf5`v2w!~`-Qm27>lIs5eQ?=f9vXE{`Mp4_sPndOF7tr1V(> zdKQX1(1uz9W!9$NS%ym*KVsHZyw&bMT_H@Zadovd z!9*J89~QWUEV~dn|5E)7a0}mDhwEMKQxvJizCMxXNw?k|uHV|ebw9JGs2|P-6pabO zjqVM2)~-dbuN-(3YsV9Qh4AlHismiTrI_^I=A~%#yOiR0v$lEK+9TiJ+}ei!tE|mW z8(UtDvF?ABvCW&Mt-bPt&8v2o|5euJXIR^I$>!G9{$FKnn{?H#sK(g5|5;;44lNxp zq<;~8;O5_?rwzUKL#x$q&gZ)FFzVAL{101NGGcH^;qa~6+2$j&^Qixuc9s?mA2|Xm zOA9kT?@w3dN8ZG?=l%Xwd0jYQSmE%I#|#`XnmuQ-{LmGg(aLCD2J)dKOIr@(+P`2- z858TSCj|W%R@@B57_!C?`PnhxeCRHK0*1-YZrF%)H0NhGWN12CbG{3@oe)e6TLu*dAsk=WAw?Q=VEC=`c;aUR4AO3eN?o00AfO zXq-)i6sd|3WOJaYT~p)lC7S+7o)eFQ$YcmXoBoWwD?JrE0U`^S?jjE*F2hFz~)Bz6cP(BttvV#*IFjAqwDOu5692{DDgPn~d%c5+>U zAv}?KMW47Ah+|B-VHQ1x?8H#4HBRV6Yux+zlfqB%H^JNFgoC5x6P0|r3zy2~Bt#|6 z3<(G4_zt282WK#$iJ=mC44lh}dkh>Y<8vNi;4HNY5C+aP*le5YdZXRp-%Pp~1LpyF zRWNYcBQsY9&2W>=AYtIVXL23`M@)}_BT-Hb#uzxYI!ap_11I9g7&sE}7&r%+sK>zR zj+z()Co$!8tK3wH{sd2#TL6MH*Q`;AjwUgDJ@J0cMg%b|zR(M5_)g=N&9w=^_aamJ@c0z;a3`0`qPFoIkP>R0Yc!1Aj|kIpXP!tFgG$ z()tsN5jFF~qG+nm&B1amj5t{*7CDN)tKL?fR*WR7oL0;P%ZWv{1eP7iRcvumJ`LY!E(G7f#rz5 zIap3ijlpsvN+wv&eWYboR0S+&Iu_1%Pr`eNtM2U_rUS?}_2Gzu)|K`WrZ za$d8Lz;c!nTInKD50>*13H7+^JY4Psmb1>J1(uV;b=u`lU^$Hmd9a*FItI(};sVR@ z;sVQ&_-3NrF2f=C``oykOiU_#zJTw3V@!}zIlzK#Eg zUN|o&_>V;8EssTYx;EZn9zHkcSFGtv`LTP{(80xprK3wYi@Bv4+tr_9f1jY|gx`P0 z#+l)1F1U>h67n9$`3exoP+S3DfgPj?RJ+BX3`u6KYxN;Nl$8Y~c&fGzMeF1)3lS^|D`0PU$8DA#j3v^e)KHFDNJ~ z)+Y}ILkD#^eN+iQ#x&y>9O`#e;c&i#I6*(JbR_dJg~LV{b>!%7$JTse#5XMbycRsL z0?-#+Tq5wau^eP?6{FIf@m5aoI{bvZ6*ymFVPnK?;LU}!0U$p(x%|CE&HF48wZqs< zKp)Nt8bVCS`$Npt*=Cxp>&B*fU#RwjfS@XDQ#}JuYIx$qrMWzz4<`kBQsETqVubxgBm-zRny6o$Ga<7DD{({XbwwUmDIA00EMN03iedoUMXi95(OTC%^lD0RNU+sa=Y7n4!c2y6x*%?qf0M+wz z)ZvoO%-}=ol`>5QIq+B=uBpY>eu~PI%N-w}7 zd*lYubdHL+&ULwq?|dp@`^wtsnH2rYJE7NH>FFReT~pxY3(p~|Hqfni5}=m#aECz| zdluj120yqO4HLEfHtdgXBmH52c)h|d!Ra=gSL~#GL(6zYv~>eTb&> zLVL04yg(QRpivW(dj0NNr3soqW3DEtM`7UVQsN$gA(FIt-7ySaUQ8GaN!JPA<1Y+G zVJ7}U(uK@1JobWkekOAqnQ`SeW7A3(Q8Q;UqE$bW84+=|IFpGawlb56MK+(wu;dVa z&m^*`#DaQ7`KAB3G+b6g3(G3nwp$8+$i ztU2Z)(^dw}a5-PfAgwu8nVes9i0RiH63tw5?9FG}nQM-SAFnwi;MW{So2XxNJdK(y ztvQ~9r&hw6Vt=znCF)6HN~~tN5PoV+vfTJ;m;FuoO@zv=T}~wK+h!ylw@oja#s`t8 zZ=Y|Xc?a^z#s_)N_>a>bBYq9qmv5gD#kbF8PDlPk>qUiS&a};VtyeyR*;Iv_?dGge zJKx%RB@*7|dLy?u! zf9lV#SDGZc*ez_VS8n`DPcpq;DMHq-S1u;x*DH~D)%8k5udww>6w6+(cr9A5h`;%I zC8oyfm57qLUO9xco3B^qV&Qyu6ud33S30Ir+66&cue@Ld)OsapA+1-wA+*v(qJF({ zG70s#qxDLSZdNg@Qg*kH)+?S)N(K5AO;~zBWNP%zqMYDzf(d!o;nFiVxc_=RC%8*od2?f~ zkj+rOXLqJ!E1}wSOlO^{LTV1Td2T!%vq^8I`~Dv;_(@vj)u@c})ZCgp`@-EFVnW{j z+1$16rcQiY(*kPen4^_WdV&>P9V$`QuZ^1QjxHX0?&zX|k)YEYK z{hJZRvfqs9rbTatt5e;Z5mVzgBcf!!8F{4L{LOd}-SgdU@Tz|^gznZDu2%BgFkEgB zY|oiNLd1<(vpgW~2XH(f?gk6#ovG7Z?@T1=-76;9kbh?) z>G+))ZbEuzF1C=~nL7#1cZZ`merKX<%r$F2oZad03LfNCoPPn-sFHhvr$$s(v z9@U$0NvE87oj%aJdd4@rPG7Qa+#HBX&`*B9eUG*^(?C>l#8B9ds+k6&+T*C21{YQ7 zjyb~x!_l0OcO5QW$kb|fLqgm6TC9x=>TF2trgw3zTLc(Ect7&b<81M#MVC*Ufjh91 zUudm%QoS9^+28y=ZVh*URj;6@e$}eCQ$g>JnXBFtvR1u!uBX$(#RX3~)kV@hiq8Xk zt(XV4!?)Y>z&_1{&5I)Tz*c0!=B>y}J+Q00x?mqPC*%olF8#phB5&*B0^!XiO2d(0cMU9RwV}lSR}JCqGQi!wWHbnuG=%x2)9)u*1{WK~ z#Qc#wyxFj;EQ4=^v^mB^48t|>T#_ZTl4WB|ysh+^7!zeVJv?T_WMNEffE_J^Q}jj0 zL$9|Xy$n7<@*o3uVK1RM^kZ=zB}qsY9Rss|j5JmEx)e1Y&AIB!s?H%LkSqud%m({|dXuU`-Oo-5LDNvHhCyVh~!eLR$| zIG^`UK5vPon$Z@~n6h$CLoZ!R(i?GHSBp_xXTltj0e)R z!W#+Sj`JA{%A5jA%x7G6|EcN*WrVS(=eC?wD{)CDrNQ;7U&99t8&O!A{bTr`DnEvw z^dr06^4R9RoL=N}{~LG@D4(Z(0QX`4>$k#s!SB`&+DbOZISTHpsOWHUFsF6 zZeqVJXs@k~VN0-dcG*F9`Bd zTtok=bT>bh!=<));D6%n6LSsvtkS*wbWV^juDoMnu9hJ*pD(o+Q3}=eBC5M;WO(LP zBgN5GBSAOE(1KQZ*Tq^>SB>n-H2f}c<;|_iy`R#&Ag;VOVy=~Dt#kWOiyuS%I_BsU zM^-aaqO9j$4TP6k-2N^vI1HC`Qph7J(8!GL|jYaN49LRt8 zJh|DN5T8OYA#VoGt3B*G>bKI(2S@86apnCx<{HXYrJJyx-~Ysww=-+<_< zFY-E9rw7kzFAi(ibGjLLE88=Abz#Pt9y(rLm()*qije=mILCG*$rAF~;k=_+h`u-gk=q~w$yxX%xsr9CBvWA%p^`)3| zGX`WmQYFf&|EdUEdY73a;9W?ngoZB3s&%nqdLhU40E z{NRRg^{#-R5S)VZ>N5Jtv%}lzG(G&2fkPxN2nA8H-ov9@7#cnb59>bl$%|33`SHdK z;46KzlDNBpbo!^3xTk>~VXSnmF=GYk6Y@4@*`k^U&go=-+HtMhgFTP1PEKBsk4u}J z5_{QDpM1BOc-doXu6ELBb+g42qNVxy1BP%AKXo>KKu5Yg4^`ExTM9R2BKb|lC7p6l zx`yUA!RNVr!0#;K9fTjirLE$*D9LZldra^xsei^Los`qQ=dhz#FPEB<*AnM5%Rs^E zSxb+_H`s)aknTnh>Q7QSdBd{ONpwJvc)sSzr|8LCL24rQY%5(kPi=zI$-5~|=Ua)1 zPD`XjzuW`-7m3PyEEY|H-(1ALVfZ4{@|cs+)c#=@WK@^Z{nqL-RI%XT8eBGxOFHES zzA-*e=;*6>b2>opi}RJHjpVF%QpvL3p{mOjn#X{1)Hc$;qu)fDTX8^^mSCG*I zgrC5rGoow~`xWZ#js1=^8*xb|l|0D%rn+FKJZf3q!MJo5{+V;Wa%W6H^rur?aI{3_ z^^QfgWf_b9!fqa-CB0oRNTTvCj72rPr9}5knCLop#CTrz7ZBMZR@S?jSXog-vzq$v z8djFpTOUkM&~aQVU0Mny)@L|kQi*;%pwR+dwjm34PHgxG6+kp!7U~SApbZF)u2Z$TBx;|Z()~P z8`|Y$2?`BKg8Hp1ajmzjR3Acrvyd?Bt|he6MWPk%t3eqFKPY#N2S@34A>`B5CX}6S zzFSRZEjsH{J8nQ2MXvhW#C(Yll3*>+dw+I={ix$|_X)&1%>tp5y-sLiD1kMulK4{h z@<}xGop01dt`B*2Yp}t5ar6Np({)(EMPXBT zI^ns(GU-_-FBpQVGAGQch?i&@mODXJiB;i*(%772`gtSq7}y%b|7Mm5jB%>T6jSrD z%)iUsc;e!eyQzfkvCw6No+ni1*6fv*wqG5By(964guHldLK0u)+L3t-p)uc`K=h~7 zhmpQqYh6J<`p}v?*rE&8gr{Wp{DW<@6icOv6voL!&Z_IA26C1E%D z6pm;pxPQOeL7=(9EVnyv%S|EEhY`hN1_gCnD?`4D5~-Na1%tY6luI}zXKE;5)F@hW zYIrC*>b8*~;gmX8hIQNCOqvpMsM}6?yvHkXOW1cugpLs?XEeeT}pjR(2@H&j_ z!!NeX?0UB<=yi7Qqrhn5!^hr*V~PrjN=k}HoY<00wBDTxg3`Jy_Hebv;|U#06?Z9N z7AsftUwsjRX`xK0d;ITi1SfgEBNtaMYB3`SkzFU=!n*B$-|RZHWW5mzl?CPuN zS*=tR^84m(Go58*`#FvuV8|et9;(>CZ$9>7bfEoknjPgq@Qv%v@LTr;J%_+Lg_xE_C&0u$LQKK zH&{l>yNJ0NZ|$1uF_hp4>o6o2uL{SgzjzHrc33^D=TK+!8+wjw=sEV>8OxMUknTei z9ZUsg_aSFSeaH>FYhu`d`x+jd$lLr^UxaORBL5XD zc&(3VkQbbU^W)AuLfhy?esg}l8~G1|9b!C%&i|em}h$=j;jPz z&gr2F{yS%69mzi|6BRYxk?46w9my5E1eekVH5%$ZjV8^4x^2}k!znv0cPjo`-W=b# ze2=bjGx$w=?bpNcoy$+D5S@z((t1#olF*Dv?*ow}J;zdB74jWKYBmA2nEHKfeg>aT{G> z$>32zSe|z)8|SKfdfnr`PL zrUW&txXfHgIx`$Nv?vJQ$(y@N``}2kq0Vlx4exAj!@Fj~X{cXnHat!`v*G=`A28nA zadB+NLm76wgXk9QnD3G>E23qU%QwsVQwB2WtwX+1)CssT3#&PBBx<#XcTf?y>LraSafU*q2ws~&<-5I*V|Ky!kk{jF>ju>6a zb!h_M7+O4$PI!Rgh%Hcbbb!^)eIOhd?ZMyqlS(sE--w`AncfT#5svzR`};e zdF(aTXKodC&Wivw3QJ4Q0mCovDE7#;H8~Xg%pmNV=3BfQ^EM`^;qj(ov?lz0OvKW7 z0{vLzeAW{!>qQPg{8CDEQQl1;Ql%}q zvWg{Fr7ZbR+LCE$OU@}8AB4B&ZCL5hADMMq-fQy%;`{?etwJ5#Uo0<(JlgKp>X83;9%p5s21V1rCooRff!3Q zF~P7gWd%8|#&I(&K5Nz6hw&VLB3xt7Ofz{l68j%B&PLXYFyZgGq?17yH>@_s-3;Z< z*tp)*A;w9}8~6V&6kV&Q`}fKV{)J0A)eUgejWz^9qciiu7a%Ud`R2B&DjurRSf#ZE z5T4VFLx<(J&cN+06r6h(HgbrMd&=1G=QW(Hv z?OREwHv-CexU{W2R*C)3T4>u)Kap*?k1Ui`ROppoQlSeI;Vuvl!FeM`RIN~@$u_rn zeny2B^&e3%q-dD%%)1Y5*75hOS*u{%3VIwHp1~!ZjHPzxt)g~cC%gogHk{g(*#AXy zt*Qs@n->hnC7n#va^?C@*|-nwjs0ApTJOh# zQFkQ5M%S5%vVF2$pYN=pA9ZBHpMc6$-m?w1#QhvQQYs)4Gi zvxKUe9fmcV?VT~S9BoLo5a|J_PT_fmg6ATQ{QOFGpIBT?7K8HrAV(jVt*=>lI% z63eco{8kwQ=Wp+&wyQ5c%8&@EzX4$@efhR~Y~NtI9GDmM!X=%`PCXp|DD_YPda@N%5bwzbbzV%gcIs-Kngz@q;` zTiu@Om&~uL`*ay+ACwo&!6lu_MH&Bi2W9*>l$UV6j2ma!C@8V)GH$tQo7)_qI-}BA z?|X1wa3C(}l$|CnL#8ot5$Ow8Z#rGVV=nAKtS8P}as(lUndbbLZAo?Iw@&#IM6bf} zv4_&Gb+cwszG;N-!g;M9`0`0CTkDp~mtXBwAnCIGgs$)qWvi@IwrBZbuD(cLxY?|3 z!v!vEOzbe6x1tsy%2t>Eu%fz_X$3kJ{+YO>Q*sKl$88ko9KvI8UezpLAc<{nftqbL zX-gO8RrI`xOFCtv!u;!I9+XI5xjSy*LG=q)6WM_C2D~r1)vk>HvJGGr6i*ASIQt(; z9pE_1qcm&XCiV%!c2Ej&URx`Q^SR$!;e@Ddn0&5Xl_)|^QbA7fGM-f(mlaNedOeOW zAe?0RNinY9G$Ntm7I6#p+umwPEya?%ozHVqgB-+&O&NxWMmmeVeO2 zuG>Q{DvNf)zRTSY*}}kuiJaUTc{OX*-l5Kpb?fa^f9D3f?Ama*M*Quy*FO6+=5K%g z4nFwMLl5V#8GkKXwr<^)zhn4o-~NOXI`G$-zwX_8^ytN3AO221{fslt2v2gE!^2B-h+T(6(FflpD-8SH!7Pqmyx73KhZA&$HXb`TfF;RP0v%*hfQcj+cw4G)73gRaS>l)RF zBx~YjY=cO9i1k(1Y!+!Bu6{$pbKN_C$qiJ7=iI6M#@-K#GMDY1G9g7dJ*EJ<>~5#A zoC75xZw$^in71L*l*X$mT?%z_HfObyD9y#b$+d@Uy8v^cK8^$a1D{P~UG`(u^KNd? zuzhd^MRzmi9HEV5YbWnoJ7L>3wYmLnZtBPLd)!m5|L8$M_+GBW6H_WLh<3Z*&(&^s z{Xgjg8{IFFkK )*$>S_X0%Aom*HV=mwihdSDMVL4CXeVOZ`5OV>{<{!YS-8&UN}sU_SbNwQow* z0(-v0i;7DNh7B#|>CVe-->7|XAI7*DHS*)?n3t}OLBrWFo|mqUsPvCp>F>enR0Tbm zs-R`#H~Mz@?_9N~9jt2dTz4!xrL;J-oqaHRyoY#$WtvLSqZX_+&8U*3nxqm$}dcJqU_s{n^ zLVVH1qm*DH_O^Mx?M+b6cexbJcU^a{_W6#nTy(>U1Ui$LTyATo7CajwA7|ljLdB;} zx1B^nHP0<~Z>Bh#T<;Mzf7@*UN2eho`vu)Q}-C5w+XFuvk*9(0(kD)n4372 zufW_r(l&*=zc9zX7o)6qgfBu(#ngBM*A4~zWOEQkPJ+ikl=WN(xM-YAg*wwRK4Diw=6S#l{R{7Z^;U`j0C@sU~QZr`bmGZGWnED^qZI2 zn44GzFl+Q`?M_p>GgKe9iA!T$-rv~v?gY)ge2T#33xoPE=*W(?!274dj9AtkjiiIs zt-|5Wxvp7rR-AOa{%4L`a`g+#?68o={dbM>3nMmJz|36~+3G7T>&u)%Lujt0L&XdLte z`iNZkX+d~<&NHua`eI6-)XzwF=jdmojgFtY#u!FD-^y9av7~H$4A8&*2NoWz$AXdr z4{to0vwba^7nB}y9OM7OoOvynWrpu2f}l2`Cb(Ml@R&cnPnl}(NtmK4oD$22(m4{V zeKa8k$rJdmz6srJy8cZ3%t(lYO#gzjiI0wGwI-?>?+FWc0hteCLLQl#qDu&W>F%o) z1XmGCO{2hXqb+c_P!ofNRzm%nRXA{9!Pvr~@v~cKw#Mb=H^Nb+jbpri|19*mH8r7& zuM0}kH4>8!w7PjKC%;uxHwU)lYe!tI!|*Wk4eYAjhA?%baH?)jhB7b`t9=?F>gFu| zt8YTSZc2!ci-b~ja~bg|5zW`ly;e86I$$o0ggi1fWwQzU`k6y0g}42dwS&)X%EgbK zc2PkPzLlfdU#iyL&KW_ITVNeh_;!wN7SJ2gl8O!&=R6LNA3lTdT}!jb*{_K21cdt0%0D zZs5>CgRH$WO3tyz`L#)mLn5c0B#Mj17HBBd#R~}i%YbR_Q70)!$-Lv z?>-1!U@@o=Dz{5DZYRFnNN;n_)|hjx61r?@hjY`R7NDMD@eW~z6r(UZaJZyPm@6(_zL^fy?9lP;BGXp#Asoc zCCKjp6Zr*Bx%(1-IPr<0B-XfE@YlG8_>;mW`1irv#COYHOQSkyneN)QQGrf^xEyx5 z>qE$$uF$cBq%Iwr7%I`Eun^86$Sifbrbi}ehygWu7gMGidX9meLNNO${AQ$!5zPJs zF9X3WK-*i$be2Ig+yc|l0OEKvV{by!2~7+oP~m#j_2PAv<^AREFT@`-jY9~PxdXn+ zHFe?}idkC-5-4{)2)$sUClFfY%E(nHX8QAGSqg=|RoG?A;<9`|68LHeE7Sb3ZKIa3 z+Ek`n0`mz(>-N>x3~CKgqPR8q1nJhm^^Axqfo~CO$R(FNqZZK{j&BizETk4Oo)CYV z++EE%S4)C^s=nE-^eD}YzJ>_ArTI(DUx>fVE#5ILY^Y@WQfEoGv-!E^e~-vAH);*z zP)h60LM6KwLekAJe=`2WP#Laqj}m6)^>K#yg-Q20aX>clnT6qBjkP|-r0^T!uM%JG zK3bd`>~wJZ6dGA?vwX*QlyV~+Dq|v%eGp)DF(9w0#Dg=e;PRHauW1~|A}~&cpXbWm zo-JzxjgTmJ^$4{flytd-CWaEJaF0;N4yi2bJe+jTSecjLm%D}dok&sUuA+HFQhXzy zbhkp_*8%);cQ3xgR=J)iiocD=o?qN#GWqUk^@F8aNI#KtK96dt(@E0Y!~AO!KAF># z?kVYXh!j}uN7Aes2a?qW2x@M+ji(`LRQB-coZBspZr>?$uhYCDR-K|m(k+8f<|=7K zF}Y2cbhYfsZ-N|aY!zvICg+0YCmgqFK=>7;s*q!uRC>-V&!xCPp?aFgx{LP<9tzueu8|1Q)r*MSm5Y3wB` zh2RrhLn!H{;&V|bz7qJCC`u5$M4!X)FVRmH(n}<1{^q+kk8B?dyS#>JJo}}uS(5#Keh87{?U%49nj+j;o~`nGB#yC+K83$Xji!FksneJ1RW4g zsmqz#O)YADGv0=i>5-N>oe@8VH}GIIhxao?=`?uVzsTNS+^bah=q}pDuup+L&l$lS z?mn9p@5ZV2KOpAkx)p%@`nj$`zqp!wyWX0F(R_MUj^@)@vqx}lY@{5Dl8w%8EgCRl zbg?ZM*BB=W=NH!tRjoS5qx4rcN}qnFSQBCG1xr)lAY7aC!ad5v#$nB5Q~OaHP1zh$ zTok@@i0^p+daM-MRAGZn754nA6}T7Od7a}ls@v5?yXETYqNebcI^9=RczEhX>5%&x z8u(ZnVwuzRlM&0BwXTt5h$zV*t63ihft$1O`CH-c=27c*v*oC==c6XD!by|J-)EBN zy9Ft7EJ<+=KBB!HSNO(Ew!WZi?o?V_e@tdNK5}>vcF!F{Ziy*ROxWb^Yg;o=n#T2b z*xhr_Bw4w88lvP=Wt<7$I)h%h55mtZW5lXb#+Y198Hb@wmxjEDs+8sJ^?y#P+1FH= zSGnN60C4T2&u@gqI8Qzp6{eA zyP&_gSv0@tC#q?-q~i(;ay8kVP9dIlpRyYe7UpWL^%R8lesi*Q|&3lHBQN=WH!^b(Fj3@uwqARcrjc2PvzJ@wsoBRC@f4SesSGKS@(Z zm2qC~w`R90g@rx0miJ_9NaX%2A6xLutDc%}apO7z?)Hjul@8sM=?PosZ zh?P;UiQy8{*Vxi)jLGA1Y6=`K$$8V#Yn(~x-VL6;i67-8Fg;Uu#LCdU)Xi>RGuWRr zJ4@e3IVW0ipM!X=7430Ceh8 zbvP@WOZ6Z7qAG3+^WBPU)4H2!n{1dL$>8_Ij#SzIq3k^1vnsN`|IB@o5SoCZf)%iL ztcz`pU14>xqwb0dDmFx#1<{1HUP06S?gTAO{$tu^Fo0Ac@q%A6Y#l(xXKC()3b5797s-GjXs-j$0wg{O*(4ndyS_$3^ z)#X^zu1CMhP1EM9ti1i+idv6;Jd@~b*BI+tb!`Yb5I3?cQ;#N~A-sjJ zXTi$UFA1m*1+M&-2TlBJfW}5?tSEl~jeLwt;MQ2fwwzEWL-8sSsy6HiwCI+1mAK`8 z(k9!F08&Z4NZOUZRh8aF}xsBF|t7wFTicLLRDXId7$$ z`-+?)&iQdGx9Z~}r**5^$*KzcRxx~6L|cB;h9lsVoKn`hjxze3I`i_~$=J?RoaEcG zdK=8E4(TY~1V$g#>&CL449%GhAh19=&kgU7W6Te`8*-hFZExI#gq0nqTE7HpEvUM1 z%vMF5Jt0*S;GoN(1ZoT`qloEJ1fL*mD-%4jLq#rzC$IAWJ%j*fUG=Z>jAaK8-$sB~ z^Fl+RgvG(bi?|oZf3;x=4QevRAE(OmJtk`eR2!55sy})y;dKYc*9OduJP$4IKn8rp zV~%!J-UD3?;khYARtNR}9};jBtorZ<6u;#`N6~)7jQCVWQOTWUZ&(g(|2}6egDZsI zP}drz!)PCcstsLvsQMQV%=V6@wP(HMcHrwgw9b~+JIZ?XRjKG?*Z0xT5SC12?k;6p z&a}P8S@Xl0%r#^za;BPl37N6lYN|W2H%jeYWq&fY+G*;0Gci)QZfBD9I=QkMa>=Z+ zAt396&Jl*EqixS5gBkJgs-bM(elkvH*tS1jRbdjwjf72p`nJXo5U>TTc{W>;u_pUu z$)Dl)WXUN;X|hD<#t@M{X>v9yE2Wa7i!^C6*3go;(EQlP3{v8w_aj#NnDi zakwT?1aAt}C@xN-c=Eb$U1i^fk@p+YQ+x7en06&^9oVYKwrjJJCzdBKW2MRa5RNBr zu~AB%(2XG?J$b!QDd&vI%MX&AlGn{BC2w0OPhLc4$#b}p=Wr!Y@TRaSii^ox5I#s7 zjYCOS+VFO5wMm>+MqS+o>Ed<^BQTH`0(#WwqL)9=eX5~HI(<+LbhXplje)+x>2F2f z#LEq){iQ}NBIlZg&JO{lfTsw&s1)3fC4}ZSa;B|s#gm=48;cl?#XM|Jo*$qom zG>^?%10AyLt5Q}!piOR5{(Snn-lntbZF)Pt(ek%qd`C2R)X>4l@#4zgtkLh5EncH4 zH>Z7$S6qDt=@r+tWqQR`oy;W8bjBlZGamP6GaeQB-n24*Z++6^7v!EeUXQ4Die@e; zvzHS4P(Vw==rc|p#A~r~3mR7p;tj~jyxyxQ(<_wPG@Li-Z46j8*kw;kVU-4}ve042 z@rJ3b!7i}?9fZm_qwE#lguIl6@czBMftFd&>1TlHU;#Uxpa)A^pVH9+_@Yb3j~a@h zD_O`R7BX!3$(*Cv$wK}`$eUsCsMAm9+pR7(8B?7o<>_0LRV--36Ez<0T$an-M27hM zOxLo1unXO?P(^5u{@LW^YGu2T`_G0U$DPL8%Yim$v3i;2ERu=)Ee(7^$BCr5q`SpF z2fcG_8so|}(+9}a%hvs~DuAXjXsLCG>Y8g+Iujdb=)$Zl`OuAzCiQ_N$sd7U2dGwlTAicqE z>)@yJkkZtGNsvAI&!_MG^}jNi$P0pHLD?|uU}djOl=Ld=NA@lcIt2H~wX1UdVJGXC z&7lAap#!?kw|rR+`yR`ee?wI#g4HD5;Xj?_%U=*5BJrQYHHfF93(uEP@Fs}&Yk1Xo z#e9jZX8H1=bPde%#fDibTIF#8UR$yv*|2QogA|W+`4o0!mFSuibU4xiWHfp~=2~Rv zk_lXd>2t!pf2_>?kgBX$Jzs6?83dLMQH^*4YIUf2;SC;kmH?zO=epir2=lpj%x6%- zs9*mMM0O^P&g`2L^Z>Oq6~ut|%;=U6P!Z7b0je(OtghTTRqX0$_OsPJ0rf%0aMgte z9w;&^R%YZhlYmXJg_jCYbHcI5IezbIhgnkU z!oTk0*$NJAV6Nh5YV9%t3Ze3TwgobX>eE!@dLNe6r_IZbFn1m7t<4rCp6VgJt^FB; zyqD(R@c=8$?aCUkt+X^FYlYIBij0-!c4fyM5=(QcsZ?n`nX05T4>Yz)vlh0O=4>c0 z&02t^rRf-zrZ6ha$3V1FnlnMv0Bp|7Zo#E{%1r!9<@deiiC}wTIn^@tJ|D zzc!`6W7${i{`*6C3$ICbvQA~WmDwujs*-g>%Vv=9(lTo>FN^@njLl{7Juj~Yb0aYa zAl)L|4b>PTv?jTY z`vqA>D}0#D{W+|Qie`;*=)seUc)x~2j91L)$STR`IpIz!cO^1%6=ZHPlV%WDb&5*+ z38;BNTj)hl*Rl0Md*!jXjjK?ahCc)qJYVJ>L4IL)5Ob+;`w&=Ci1x%Dn})mKwN;#> z%YMu3^oBmNn%Sv_{vl+nX&7C0-Vw4u4gKj@;0^tAsY;rLlZ~yWVH|948m@=(rr||^ zrA>ol)HDbq%by0(O4IO4Du~g~62pp3Lj<(mG|WWY8v4;?e;Lrcnt5X%0a?>96?;mX z1_9Ehp${6oX?WH^-ZVsjHx2Jwa=dAnjc6;4{g(zI2j8d6%6nGj9#^59UN-1RsXzQf z=3+n;!v0WQPL(ygKsAO4tw{!PzdoF_5kSQZuba zY`)dA2x?x>gi;h5BW!}2&T`>j!w#tNUd6{@kyTQ4#wBNiSKaIfcwrchxs>p!1h!T; zYhb?#?;&Nw;IdPWQZ|%*RFS(53ku<9*bkb;Uqdy92(3xFfcYmJ&jy8WEE|6gFPe=d zZQ+;jj&=KgYGHq5mDuktx)7PXO!_tKY9=Y`);O)Xi$+lMtP@Hu`iEYg4F@B?F!aD& zvSCvKTg!$5?91U``%EEAwp}C)3_fF$Fo7c{VIHUa_T$tQE51L1fZw*vm{(HoRqq6kDnYYMu>3DI0!5MMtP3kzW|b zU@qCvL||*#a2f2r>!sPSf7#i8Q8sj>t6YM%LdZd_e7XwZd&`Chtx48^vjH5>28GY` zY?w{7&9Y%IJSw$HwhZ;9wK=j@$c9sqNwcA!nWSu}HbaWp5JAneK`3Rz&J+^uZPUzH8d5cm+<3gIEB4=wh|P%PmBR+GF2-AJfs zfy8@D{O9n#C9}jKwuCptv!D&COS2%dR>*?wkx8@QKr=^K&}V3Khlrr&Ss;|M;BHiC zLS_*13&Tg4OBUQHTLxwiCt07h6%O$lytffZjxHN_jIv-lfrHS-t~yX-EcP3r8bgHE zB+qky6&%k7g;#qvd}c{4X&x8Ad&;w+r>R$xnoMc2d5oauNfJs?sCit3n%S=AFw~Uh zW@MG*=D1|f4ZJ3t0(fC~3vdfwUs%DdtF0jdb&^Oj@O4GSYH?f}tBH~dg4n2ask0|Qphdfo*`X4VB)VT*Ucb@sd^K-vX+_j`x|y!(wz?|x6UWDwJGdp&#{Pw*Qf znshDu*38z6GViw{tJ#Y(y?PsrOdHwSwXE~8N@~gbHy^v!(XTfGNvEuma92qVM5gD!b7>AN53_kO zyCs6%h$y+L>|=}Q3j(P%s6xU_ANlfECD zOLdjZ0Y#221mh(##*1@Ok(0i?>X~wCik$R)l!~Hcg<2n5q2}+NI+LLGDvVXqg=Fo9 zDetKJdjInC8dbRl@|e&0enG3hgIODdzk`YF>>bSZu;M$Ix}YO4m!g5OqcjwPPQE+< zNS(dGiL4GAq;GJx8D|U| zc-+vz`Vxvy0NI_2?OBt*i|ye5s8g^f4lBML&&iQ|j&S}OND zCyoaoyB4z9#PKD0O}?K%3euo-`geR@v&5HLcsikcXh_AMQQw`x$#1ky!atrY(;)xdW zpoLfl>hrRZ>E|BTTcX~CS)C}w&-sU+H|DkU{}+N;*GllRF?v?l8n2u3nlK)h9J^1I z?GwK837$o;ZpmxXc#GlGg>lJCDSsQ|+pOp0yk zx*D;ji~gqOHA8SBymXe}K1&tv1;9NUI2)B!{%_|WtYWA8iHEW^`o9_N?whVFVeuC45$?s|$7Bo>xi_J@KP{6`SDzbV`wiq9Q*d3?;R6}cw>Rv^~t*e-^vA?!-H^s!8P9iSdy$DPS8G^`CU`7r;y*>(znb*APRs6|E{3{^H#s`rCh4{BcMLQnAp z!WtoWT~-J?aKD3LHihbER1c_A46SSx+VcKh&(*PpQLava;_*7U_F$pALj2n zUESRG1Qx;qu6Kf)8*U>tZTE{gI*4I$Ay08lw7SWcYClTP# z8q3i$xeDqz+7ZR2Il2Un=V-@KX5{>^6S^<`Q&p}CBRieIzEc+CU%2)Fg7*@>FpT1U zVHn4?Cb^pH9`KfjHxG2#VUoQy^gda}>*RWgmQp&o{tOFig1XTZOUO^If0BDYxn727 zP{4N|9ZZ=<%df*G=?}B^?i%O;rb8#!x8WTVA521R z0#%&|;HTFMuEmM^?&GVKsqu#V0qPj2x^U3H%T1*)_U*@D0vvQM)Chy?1be}o!aq@` zfrEbj1Td43$+Bm1XvorZ31Ijt{MqoIa{iI0#L}Mx-%FqEsQ@)t$wz2b@(xqUvrdYu zm-;7YEJuIzqv>X&P8xkx(JHfb?)>M#{>U9)c&vAts^HjWe(rqbS#q3_AC%{QBMDtd!pi*;OF_$X=M$QXcJMp)<1Cw1l}icN zx#7w0c^?f={vomwE(Cl>W7*cw=_WsDOg>1#5()Un*$HLk$ z5MF$1QRZ(eEU|S#M`(|Rs}>+Tcm5xiK%G1P7F2!E8QP5@!u+75WB+bvx zZZJumo&6h>pPe0%_JvLdYDppV;_6VHn=NQl7>B~*3Je_iv|V`Gp|PEfBPXBa_Z})o zzK_6_&Q|{42Gz@`iBLNjbrDoQqsBm;0_EM%A5ml7(7(JJ8fzGJL)RFkZfLrp)eQ+< z+6}#I7e+Xu>RyR?OEM;b=9 z4K+%(U1@09CUmK7lMN%=9);pJCyZjyG7H$6+$EpqN1ma4n@ZrThP@T)Go!A8T4dCP zP%E9Q0#m2`PPAw(MmOrUd%`LNnY4pZGU+fVfASN&)TAMXkx65q_$?1*g^JvKV4XKM zlLn4bOM?n40*6?s=Y4C4l$Zg0D^l zvDe-mej%vWJ{-z>?TE>GZ3n5>KGoprwFPeqPol1QuRVj139@H$xWVjEnl1oL2CDF9 z!k_B=PQzpAUkBeyzuro}!Ad?tvyyk1N}kT0tC#w0G?w<-_nD3AwVzkCvDf|(n6>qH zg-=KPuUhp zUZ25B+H1c&QYBgJwflkfUi%m*@3kYmWv?B>z1NO+&3kRvqF!746?^S0H0!lvNNKOV zDY9!R@0<17H)COKsDk(3d+j6OHtV$?v;?Zxehn)2+7afx_R$EbsGIlNpP8h3?H{4M z*N(^*y|zQuYYW;G2BC0;Uc1}=Rk;~Ba?+6hji%mv?aK3&|LPgnGDAj&!YFk^4;WfKqtK<@(DQ~-H}ozPKku~%0c((LU*vBb9c`O$ zSlRZAQL?SW1+rJR^)yPhRYAFJBU;#Yh+$;g5Tj( zf^A>sw>>M`Hp8&8?E|A^Tcc63ZK+YRZLR8P+w(1K+X{|%-+LJ)+Xfk0wh3Ko+i1hc zwo9S-dA|J$tcPrC%D6@bY}*6MY$= zA&9euSe);5j;w!?z{5=2<4_kv@m)VuA>7XOKJMwZ5m*@B;hxFgYl=!b{E7E5{~@40 zEVAo7uB?#eS`@4ew-V?3+of|pXW2w}htNAUTc4W=aFdMsF8@c2O5e6F2-h?BqF((_ z0&V&{?80iy57W?7nuTu0m`m8|W}(LtI)p*r?H0;mg4x>s0caYX$~@Lg=7Ch^y;fyj zNM#zE$*f9+RqV@}h4v*>v9EQm>~9(Sv8l{9#?_`yO5OFTO#fD89!O?>Vcy`u6P|DF6N~0=rfh?ETw3 z!{{40p|zy!7V%A;m1nV-GQc`oI=Rpg{^a~7nWuZkRdn^PNBqe@C!`i{rnLHp(gA?ds^@1N`E{w51>N4;hFb#EFIaWBpv!#f3k%d!dxf6Eft*;|&)V8!<&{+4AK z8t`su@V6{`0`a#jk(Iq=5i3elG#d6d|B&Ez-&o3Ds+@T8X?)+$^!dK|aKQz7eNtnu zPqKZGwChu!xY-ee?QqmL|JvBN!%-zvPyX6=&YcUKe~7PB?oQFIN0cyMLXXOZw$~vW*uUKnIQ(bey)z>YJBQE%U&VJ|O?t`7!Bv zsI~ZOTge3rJ|oI@U7<<3=rrk@60&=Qwc8Ggr0>kXE(Y?X|C#&4A}CGzAnu38u$D=` z5=LGZA~t0;(4O@1P$c~Xl0Lwao+kFllEj{nC3ZkwW@w|PH!7)OgY7+;Z=I$sC0hHk zLi)(AQKyqvCs+);A0l)8+?-r;Vj63@iP{i*p(G)_L^|1CB27+Cq`I*LbmZ{U_yUAY zUhO51KvoDGY@+WXM&&P2iQ8svwq=x{WAYax>p!ycaDD!w%A3~g^aB&VQW$xTPSCbZ zpuS7OVKu7XeXM?KpBBDia5mrHCMOQp#%KMDG(VSst*=19IT*uz0$tFxqB0H&~_F-rhFG zjO>u#?!suqFGgvTySI0b8-Ix6H#vFD6xx zZJF%QH%GjS$4*X0{!OscRWY|tR(p%ApPY=hFsrOPB)t?iF8N|x|2lBp3x&6}<6Y2VJDS`CC>o7R|5?;0QSjx%`r;He@Yzc-(# zPa>cYuHh;`ZTJD{8&}fEfNeKN{W8`@2+0VvD{sywrRQ9nnY3G?9c-3|LBCbxZrO&n zO0bhZC1+!FA&e&QXLPcL3992IivFP8D|3?o&IZ{1NH%>MOn}A~eV10`zTKI1M$)if za_B`$R1Jak;bOa9Zr2H1{i$XNU@diK>CNtd{VBQ^k@yo=l~H=CIn*dU)tqFMo@&m8 zns2kYQqv7JUDLVmD-ENkn#o4#spe5Af8xsR()_9BRm141=2Iwsd^L0G~rQ@TX9%%4|X8jH13(X?Hmst&gqhXpp>e(96^(5N- zqnIopvh1Zz|N(NcLj|40a|0!c+ zvjgkIkx7nA4j-@7Z&=RBAD1etyF+bnR41szpq7W9p61OPBHfsb${*Br_$3nQ@eI5B z8E_S-JB(@z^#W9F*le$=+;#Lob&={}T?y;Oyxv%%N6Ts8K_f3gxFWoPHoi{6N%@2Yh!y~A}by~A}bz2Hq@ z43TH^gUiFfB^9~96Es8FbVBl1Ds+9gfxtqzmFq>wu;c}*KHSZ9j=co@2Lh{-02;!+ z2zWzyo7tu5ooPnd)RR$a2tPJT4dK^N-VjR7C&U{Y!j}!BhS1S!2!-~B@G;PNBJG;4 zMOkWSHH4k7AXo(`h0Pj5FR5+uiwXdg)XiqDN@_iXyrd#KD=D$0?qbv(P5(zl>|Zu& z=2&#F$0{zo=~bEApPE#I-4O}*@@;5Sy2;B=O?J5=dim+F-OH~wN?!hmiO9<((gzW| ze8A<=%ilMky! zEek^sSPA1xy?h@OO^~A2y!;`=suQu@%QJ8*US7mbNsd9Od->F6UcM{g(-c{AFMr$Y zl$S4p;^$s2Ei*xr6dmAM$?+;ZQt_J@5z5GVd9f%6$gRYETJ`zP< zcOpBhJ7T$)--=|ZmtSUf$jfgxjJ!OU|%O^~A2 zys`U4^G4Y2jTyKVZ!BV`BzvILy>Uh}Z|q6~6>T_>1N`3A$DDQKnrh(## zeeO$!QJ?#fQR;J>puEq07IbN!`ya!o&+T-rikt$J!dCj+SMZCD0H_8X@mOzhdLiT` z71>!yi6wQ{pl*dex1n1ux5f?f`_oQ5gUNxj&CUBDlKTC*7H~EJ?)Mcu5&eP?$QQ3w z>>6v|0NefkDWl}~pO}dJULxISU-0`mX0XQE|1qHae*NntEWh6isy6)l|Htn;p}V*1 zcE2BD+T{0RjgsF_G)jJdFBD^K1g^vQQommVMZ=`1HNXD|F~-`k-S0DSD}Gw zsr&sW&HTO#;S&^DbHD%D?3CYkyIy6M`n|MF1x=Q(dtLPV9YJ_6e56tG`*Wb!a1i+K z_xn>po8O!Hb`MBe!nF8{n<%59-EFd5b~0W?5w2fOifm?C&B6dnL8Kv&#i_f<*OdqK)Abgr4YwqTk5si264?ywr?p<1Df+ou=Zi;UH zreW2+e`A!~yu;1PzjuNEemDOVw7L1XWVR`noA*YMSDwhuDvwz1=AR>3>gM~v@nPyw zhLM|RnAFY1aW@}kl-ztgl)HIEXKwCrxw*sT=7Kkc1w@{?xevCtqTX2dH;%a@rQ%jo ztig6h0O=cS=4t}-OB2@w=5H2- zCNLu~PGEMvRY}sTY@sz_qY2EXzA~U1xK5KKtOjl?sM_$C-_gMB5T9T6NB1z- z?G4-^rcDjp5Tn$;K_xPwqMO^POm`W5C3UqGxn5!)NM4BScs zSHw<92BFj&xbYV#<7Ktx4Eqs2Tah(y;O3c~8pq^sGqg8w($WZ;EdR=~T+c7-gYa?8 zUPh^b8w^z&CIbKc4crZ&t%18ZnSPrJM@_?c6nO&|*;&&dmN#&>AX(bL-EMYh9PUjVG5CF4V=%Z9Tyw8i<5;}O1?Mc zHlvWTvy_OX{CWsgEA;aXt8yUdWKF_b!#Ba&Iro6v9~sHRP9`K*{5^U=+uNfDY-W@^ zpdXZbfJC~{EaCxEX-y0E1b>DBUfOqlNQg5s~nUHqzV@$DXe`IB;5Q`r#GrO{8{BDhC{rlqU ztg26ho%ZoJSirjkc>SNY2StZaa$R!8lvw{4!S)`Y%N?Rr|F?$n`Y(}bh^YSG5$pe9 z22}kYYn1B$ZBTT+zoY(dAM5|q=$`4iz5c&o+Eo8PGD`LTYok>Eb5oS)1_X}5_|p3S z0*WeE{V(-@E5xc3vAzCh;8yB?5j!P$AEjRZpSmDtKkosaC46H=*1Z1rCmJ8(4uj(7 z^x|Lu)Z{XYQ8>wiRN_21#D{|;CE7rZH~OypVp z_tEDL1PxF&-IQE=SM$;5vB;FpVEO2Ce*~%#VDwpyfR8@UHoG)~b-7U*eNHw?qtEA! z(&+PDC?9=F&1BTX(Wj#|`kZfQjXr;d^3mrppz{=Md~4M1F6D?upKC+$s|`}vZ1m}` z&-)QP!_zm<%+=_#w~1@?xi^%*K99gS`W#>wjXs5rqt7!@*OR;CmgIUGfE7od5s-~O zPer>t(1*=>Vm&}WsQ6)>lfjE+CAe*<=Uuh(aXEaeYUOgLoNUj4r^jGF%plU9Fmy!tOSO4Wb0yJPi_=&brXT-D#0mY(T&%K9M`#_p2kVcHe%j0!gT#i z7;oFTlA)^udlEo90q*8g1n!STG2Z|}@$=5-4Y4PQ>=kCj+RDg|HsiU6D+eWh|ja$tg&6*9T&%PcTC?G>hyk)na+7!(ORFJkHJ>r1e}l z-ojjE7^OPHq^TCiQ+=mVO7*i)p6ZCsQtfc1+TlvI;7y?|k(Z=e3rsfLE`4FUWzs)O z$=jw}DT(YXC1S-~c!6B7vfnaUI<-#sdo(mD3<$}YN3l3K<2;LOlC-N*8_=;s4|72i0l8`QH>gq?4e3x<# zEOPd8PDh$GeP%GE$VoSi`XuFyD{}g{Oj~mLq`f)%&p0KE5c%K%zv5GfgHyz-D9^GL9I&SW#5HuRb9 zpUE(_F^4b~83ywvDt)pk%wYr^Z^dvT)LBsT!_ta28K~=(h0vM7$<>Bg14?MqrE@m& z=-ODiHbIw8)_Bcuky4ZT3%+>rtZX6c-L@=s5is^@r+eE2R=SY&9$U!DHbg^MQG0Jv zO)MMVuSK$hwHmcC>9L8n9NO=>P**{WO^H@Q?*o7bB@`{5u5I*fA?iaBny%aRZJ6xj zgMq{JN^qLV4nfujo2TqfmY^efBw6^d96rNQP*qcqt39Lfiq zQu7pQ;$YL!8f-dRgH1>4%e^x}eU9&E1XcN<`Z>i?lN7i=m;2@6>4C1K+d8>im#6aInSGxU$XDE4=ZCMm z7Xj@qXWX%dc*_W#A0~D!;hk6r&vO4XY760EsCS|GRVTgCU6cHaIGYGs7=D9V9?nAj z3#7ZhVtzh<1?et?$pmiukThHebr4i_B0x>@AomiVA08p@DA?{*KCR>4W!{96414hY zP+r7L$4R(%%!yFK=(jv zA4YOjqnH1ZHFbM<8FzV+700U)V81zG6N>sqB1pKWbho047s4(Co`Vf_VKb(RBilYn z+KT|O=<}dt;!)o-(B7jeSB;6uW64-F)Q1ZRtPf{#Erip#UWG_)SQFTm_cZ&ckgch$ zDdo7z{?fKDQ44mHv^|o1ZuT5b;DczWP83}Fb@V1c?Lng1*U>LnY&*hh@f=YbS*Jeq+A8F!_{fs&G2gKlxeOVO)Od0!h^6EdLWB&x(9lQO*qU6}? z8YRcx&L}zd{!s4NQnMH}(XkyZ$9A+F+tG6DiJ&{LTXgKdm_|AFnNa-PvF`?!=MLK% zv8{z;FaLK%?g0=RqbR9OUVTi6KUtCO#`Qwexee5#Mr{DK9I7Flb9Yg2jXf*t!>a@o z!qZ$`=s~Ex9#O*o4mAO)I#Db&Nj>-f;@%xwvG*r@jU{bfe^jxzbnFOd&9O6H>e!o3 zQ-pHtJ)rniCqpdGZ;A6Lf)<8lQ0~}siFv$Y&Q+d3y4|twG7WO<7on;X0o<`A?v5P= zqhq^_99uH(*cm2uY{$s4h0$+$*!dv*79%Fh2o8j>%pXl)eK^Ig#|mJM9bx<5*P3Hr zv4@xO8Wj{o3&(zv2oj>BbStUg?$}>rgFAL)XO1mabH`pEcXdw-xSI(BcctT=_Yf@8~pS~zwiI`K=5Jo?53)aSn>K@h>JYkvNdRq`-t+xe-?a^9W;9YcIBDjeNl9@>l zd}2jU?(~>^>u6xV!;=M@CT%Yz)o9}k18UnAXsAvUTipxYsHfXfJne6 z?0vr3*k+9-MQEl}>sQLu$4yO2Ej z4HJ?lFM?tddSY=;-r|Yq$x@Pf^5kb*^W+F<&66`;>dB{});;+`D1HprElzpzB!Wsk z`4nQ_4=$d(GwF6ut}_ktF@};odlP4P`PkziOdGaiy?6DcjJy~i#K~41J#fFh5mp>(4^5pJN z-X~XsF7@Qi4WmA}3W}e5@|D2MlRr-O!M0XB`A!h#$#uzivtFKj2s+%8BRlhCv6_4G z-Dvnl>90#poYCBqPc*CK$>WWZCqD?~o*V^Rc(M!0lRq~hd2)DKg{@fJlXv6Z`(!Ce zJ^7S>w&uwZ(3&S_ywsD=N3DDE4N&~5lg_eePPcA3j?#T;HgFLzY zGZGiTJz3)J$x$$RvdhSmCF7o)VNy?aj67Kw{oIqE!iXvv5i0vy=C|(25!SjVpSZ_= z_vEjMU;q&$h2$}dprt2oi#@gBH(o?PF;ljoTypMy&Ggilsx%KqvXkN z8YNF|fO1ckn#HJzp6qCOvZLk6Lc1qF3cA#jzlY;}a+_zZpxu*a05eaXn{=(kJaL02hKh`OikllSe?gCr7~+ zp6o*MW zM-f!&$#ukB1s6|#hnU@yA2SW|A$lZDaG zJ$VjB43H6_va4l&>z*88t$XrTyZ(1iZnq=8M+C{2$@vyROHW>cJxqWB`yHMv*m!B1 zo6JF*dveEG<&=U;J$VD5ok?f&KDigz+OR#m75Zd3P%}@SV^j945ZnPh$vsKcD~eLn z_S>VT5HxMy7iykO+ecuWwm;r5nzk2O8LpiWMxk!JynRk`zNynr2wwxJx1A7t7lbi@ zCIc$j1;L+XcgJw^wr`TBkmfWr`c3z=$ima=gC--Jd)b~yPFBu+licuHb1&QU`RHYT zGD=={F_e2*6l~#TE+j8|)`a9`KSEU}ip9Nb+kYz6dWMyf*@W{9d&{fMz04=7BcQpL z`9yWbOTBD3YTe5&gW^XYZE?!WrVv!>Wv3H!=k?9YwkO^0Wphn~ylgoX4Fdr8GKssF zMZxH0E+a3KjC)yzNxjT5@-kucb1xf)5gW^hP7Zd<@&EjrZGfTX&e^=Uwwr*?zcE@fY&091z|({RjX*WLOCl7 z`X^^8@$$%GgVIM1rT2Cp5M? zc(WV17_S)zfFI`gzrLw5`-VVPrf@BU_qgu;f})uzDAb&A;dB?;hA6uql#G8}LbnlE zAFkzE7Zx2~0{Af0hsr@>E+d%H0eiD3%C3@rJoGc^ZwQ$HgDEb5dHOboCKWa%{ z7ts8w%hYW0tC1?xfcrDt^DJ-JCtPggSS2UHH2@gDs$pYR^oQyu%Y(k%$H_g zfch1RHr5i>jr&e7N@zo`a|lwP89^E!6SxEP>5;7i2%SkH@heoA$hd-s^ABYdZb3 z=#8M+9&hSZN$$Jw@OfpqyjqAY1|B*r*ONWR{v8Gm9GP471nobTdmtG({M3OboiK9X zpdrT%9mU&T%h4b$%&s_!77t&3` z5afCNAGWgBs6Wz7`59@ERkDz-Hhctr6AqeBH)xqX(RaD2EYDZ6nnR)K#;Fq$ZHL8n zaRUdPK8%r3&{kpj%ZcNwa9oaBB;&sd$LEp;BXeJeKV4tWNk}2N4bW8ne1Eeuo+2~9 z36f-gvp&b2bo{^(XAC{bHZ^;Vz_|j{@palL7yRJp2CTk|%<;%8MJ^JsBRjGH!4; z?W(vH;h;BT(TI}C&!F`>Y1QS+7Fvg(_Mv8KAO0O`Wx^0%D4U=~aww7fsz`KMcXrGd znf_H6u2?3AtGSWn$+_gtRB}<+62W9k1a{1X&H_h)NQ*X{Q-K_R+;A(8BM3W&KURlv zC2>cS1!qF$bvcheFVnvh40jX$T`jII_cr6ER-I+31^V+9<+*1B)#bg`pnr+b*MTk& zRF}ogpt(iiM!3mxLRWt`T5~OC%m`Pmj2CPf_PWecCpl8LgHm_GXcBkeAp^G=dCuOs zoe~VKCfvbKf@ad!3aPfA*pM5!(`#dbox5-U9sc&uqh~)O4-jn6)m8mse zjcXm4Lg)Zh@rwFleYYj_RM7d?!d?b1?)S77AYVPXD-;dMt1OP34t^!>@-xFZ;Mp09 zz(e5IhJV6(B<_$_HtLge>2e!v_o~<6%upmcT}(?sN0@5uV0E!k+QsT-sOm&8UlN)L zeauS1AyGypG}|0KzW6h8vju=+`2xVM2x|Bh7k};n z#}@z|VHhm{%rNN!KyiEl;ORzb0pP_@z5pCYQwgme}75t z4BATaxhB)C*8CClcu7WXmH}dUN$!ncC1%Ewe8tR=Pkw3`m1KrVOHv#!$wfw~B-^|m zOERLfl61IA(%~ve!JEPXMBbt#&mhQr^11cNNUxG>m~Q!GP7*h+F_X_?iA+fzhZq61XU_+*Z9+O%NflrsjWyOzIv39Q~k__=+GQ9!br ziu2_;W~si!*b{Cc=u3=2P_-d{Rp0;nOAPnJw!qdwdGhkTburPGpukHna<)zmh z!Icnw z^j@BqCFI|xTcsDdSwh58=_xB!dcPv%rS~6|-Z^Mf>8(ORq~-IR`{6|?Ck_33T57Xt zCx850rRMuUy-|(&$~U3|zJyexn$kD!K%;kxTf=n+Jjf_H;F(bD-~{^jJKz$~=74XU zf&pG{uSbs;VdQ2h7t4#V?CRzY_<)(ABK)UeRD>BOEkbd;2tPDRMYs^ki!h?IB6PTl z(BUdV!J9${B5zTITM}eN_{QB?LUM1$ghXzZ5V2H*%3BrTa|n46UUweB2cbT&EaDxt(Fs+!n`kyV@w__RUb9+Yz1Rw!@X%4p(jq z-V|OU@+`N#Pq>xTkGF(uS0Ah7gBFW@N^g{^Ib+N$LHOHH@>gwAip zl8oFelf?2&Dj-GW*!JEQ- zBF{2uK~M&%UUj7|c)fa1^{UqduU9)*u+^*n&Fj^cDI%>`+rrh+Ywz)xc^{9zTX2`$ z{5|4@u!yiRX7vK7tIX<(r#XKo#pMrV;%_|y9Mm03Ab+Y9$TDj|*;}SfPnGw;W=aoM z_Ec$uz1r|TVMBMwSz5~T*G>Mjw`{(9aHWA#D^F#ZxrMB}`{udP%BRuGxye}hCKQL4 zU`oVgRys&l3glJ_lv+8$w8_f(u-Rb|R!b|dC#-1Yd97Ocx#i_CWbf)ROMHmuR@#rSS9f=oTnr`XVrNzjhiClY24eSUv4J-7nAmG^GouQDcxLh z{T!241^+8D-sVPbR>8&c3jS>@v-BIb5f-J|-0_A{1P1sBIF`0Ylif&BIe!=KlIwr`qWQeX>lT;hJ${7pc@LqFW#GZQ z$1Vmh&U>_fM@DMLj-F6y*>4JdCGMEQI1{|jd;9@@ZRi6lEBkcbqt50%s^D~SA)og+ z!c=SC;}oMb?=cptF+}J*+rc~D<%Tl9hBEZWQN zmMzcEm)$DgYl+g!H*&M`70b)_Lj*g@__TcQGmBNe&l^VNn_<%O701i>1EZ`@h4S)^ z=&XDluJU!b^{K#{!sn%}{DROxao2JF=LaN#H1mep(PUF{uKZt9dH8xjdor( zfTjE_=S)dCbBdh1ouejDGQSi#wc^lhUhkZ=VqB$AJ(~=psW_4e*QJ~ZluK3)t1yR~ zo2{z1fSli*Y0L$o@=_f8#PZyM;3UsZ^<%a}zA1Esa~K%TH@FY@7O1=@Q?_bY7iFuM zvWGKe??Or0twm+mxU!Gnm@@BJFJO+vMjT5Q0rie`LzH=Q{zs_VFab=lGswD@^8ELf zzUATa3oseb*;~;dR)*sf&ZA3o^GNapto*18$2^j}Vbp{%%fe(hmwlt}_rE2qCi#u) z;l#8slvgXX6IVYcOiE`;>CegejcR6p6u#5g0c$xy3Mhw`r3TTFI$wtjK+b zy534oF5l;Csq0PPLjaZstshwmdq3Fh__BeqwSMFk)1>typFsJ#K>?M-xPHVjT0bI; zs+ADgTkX(CF=&gxXp@$ISqw_gGAQ4qB?4o5y1b_}D?LIhdyf3FBG(0V z>vBhW_8|A_!*~Mg!}(l&i@*rTHfdQOJ*Atp2vCLwHfbqCgKyIE2Lt&gEfL_GwEP*4 z`XFr)*d5W;CEEOqljDKZhD~6`1tK|yk>>Qu2l@QBWX>PRd3?S%xNQln!ewfbEh+p_ z$a~?7XNA8cdCY86W{rpKg?}rQ7k&iB%zD@`$}FL!PKEzx)agY*@}FdTV)Md(0l*w9 z{G(xO6hz_oGPVl;Q`4lv{|3qnKLTUnFE@+|f93ZT8z0G_s#)PTg+CEwNnQ|!l6rmd ze-yE#ZRzIzUH^?KXJnC6cCEFNufkc0m&i@w)pMPa**d{$7Hxse)qJ1)$IRAYRU2T) zyl@U5ZAEt1a|>ZS_u|xrz3(b=I^=nibSMEXG`W8XYiuDr!o4{9Ew>oIQe&t({6jKy zq4H5N?hItU#TeN!MsXS}#>mNHbRor$v)Y z$A{3bQEJj5zxB6Dr4~Fs4Ad7qz69k99wRU=c&syw7CZ{AV$wI%&!Wy&4RpwlFm+n8 zxZZ~<41Gf#+1UbNvEtIjIiWq^S~TR{pvO(aIt1>JX^3n)R#FQe_b<`ljrmM8RBla@ zI_8_uP}-PBR!PIWAhbEnD{7t$ODpOSRn(?%3}Y@;N6O-v1*}Lpy>G6E6nD*U2bK~b z@ED8#Ehtb|%x80QP<@12HGc;MS|9EskWYZQjsV*bHkwofbvTbTKYI~yIV_gBKnXP0 zPS}(t^?#ivJV|ukaeDU`IV*IWpPQj&L;L3XV?w714Vj(KSp>3kHWV=&`X|oq0eHP) zm^ktW#c(-cw5xVKlWSdgrb`jkr7>Ux0rlZqyUyiW7wQ+YOd?g(r2GO;hb+0@E=(9;h#{2@DHvVeWXX7C|;y$lCR(%1-C9F4Qv`f z{8~2CMNkAd2&h<JUa9P&XX+2v!UNs=2vRVU)Rmt-UGf~0#JiQQ9T3%cpu;2O_hsq2(&Uw{yP@V<90IBt=y1|- z!zo=)8BW&ChH4BEvoI)a(YiiJ%KbYGaqGUwq<%B0$d6L^)B21G_eJa}WyiziMj z+i^V*4fBGIxgU%ztC56`Co!Dmex9vL%|f$d?qf*lV$@TR?Tl>m1+S-}yEdE)FOD_J ze2f879`G>9tbyBsyABUfQELgfzzn*KBxS70oD0W&A>7OLM)-Vm0wrW)h^Tqt0FrYN z>NHH~@Q*?m!2SJT>cenL?g?C9gnghELN$a-XmlcGtM^Ilodh`OAt-_7+o7lt zRDK?P8GW~NR9hlPje#nQICHGBJWu33F^I}i__29q>G)}^EIpv+S!Idv=9Q%}#PE4m zVd7oKW))^z)1nIVB9X3GVY1My!o-l$3NsVg6)Vg>=&lW)!29nNrXFr5B{f!Y77zSmF7$2N-NEDL#xufYLqI@Tqv(N5uH^WhpXZ^ zT$M*~M$#zGChxqy)CP6O9ahi!?7Q`Btg-g)^yia#d-2|H8v5MJQ0E7f%&`YS@m2?w zteyXk&eiGv6HuW} z_4yFSSrOQk!Q+YCuMUUt8kEpdH!asDcO&ZhySlGX$96*m`lD=QRcsmR!_SXuXfel3 zxCwnGk`|gaMfeLaWZLPob0?CvM~VI5^xa9nT~^^Aju@ z4GxDQ=!3(^&IX5K#layH6coqS(yqky%0Eo$vLu`f1JF@C5PflnL&FU?IFu*drFfEU1^Dh@qh$ zJHG(jp1V#HE6mi@X~Ec+$*xP`*M-$+t|H3DR!0y}ANtyLSFUwo0As5RqOsM@1k{Hs z?0Ny$#t_*x$vxcHVL)Bjj=@n3@1n8QsRS%CE02Ln3d)d!pn5?SRTQb3k&{2}~fAhlsQ%;xPJ9Wiak!GyO% zMa8oE@H+R6A%-jrA8_xJH40PZujre3h2pi}{Lt+{Y7K~Km}NZ@_g@ohA#4t{6sjR? zwsH|;J<`4etn#_#F_hr*Z43|`LSe_h;Ch`=fAhA)ii#_^&lV%!+a=a^0+QH?-+Mx-&_M| zj?CWaKPpbS$0(pAmePpq>>=!Oi^#XaVpX{%XbIZzv$gu5jn;gVwFu5OS*y2A)_4G$s`{$U)$YJfJ^(|b zZ?0C)y?^6v!q~{>Y6mV=OvOEzeBYG_X#UNb@4J%m(r?z*nWM7T9!xtx@#D)-i&LAc z9Y|2zTuu9$y+j(T;IgmT;}&yqUo$!x09PX5TOdW~{u2tex7pnmkM=iv%Hr`&o!&y> zu~PVaekU@`KZ1M1p!x`|3932~z(0bMJ^m3~6pSCixr{!7lZ<}^mtoS6;2fim;Dpi7 zH(Gm>`5Spv^NrT(Y@4)SF|Y@`WD7Q7YxyZzmwMInF2nn#pOP`1pT2}COGY51ihMT4 z4-S=NV<*OSHOX3Bw?=MZ*ihrV(3`7Q9E9 zRISfIw3m_}lAA1*dRFN&SHcTm?Z@+pUecD0DrkAU%ZOnz-zRToG8>u9;$||9MVUqt zqVYcQ@PdcjR6M(y%+Ih|jYkZd9sEAoE|WRLWD@4aiZT|@f^aTVZ@&#M&;1Fo8!bj{ zxF`)TNeH8)pSR-^HQsx^Agg{|sZ2(7-@Y<;G7&U}$f-#-AZ$8eHObacO-3CEwbeWg z_g_InZ#1;aSAzHp;l~p`KWtatCMVv;RFLGbLO6!|QE(XeKwW0kzEHP8)rOBjuC|LW z5TTv1dHkg9a`es(kAvEKSl`_GvUA`(;wgl;347FZybLu9%2x>pS4E8ZtHbdCOE(Mu z+TdCx(EbZFHH0mnVINHJ1B9QzDD7ARHit*M5A|oG`a&HKRh%)IKD9838S(330d*;&3HI@TPD_X+k~^6IsYJgP>um z-#=7TDj_cuc7Vn5B-BMv%fqYBRTyr9YT49PMpg*;1%a~-_$gH70@W`87KX*#FArmf zxX?5S9aVOK2|YpJAwYTTf+~btxQ^kz5GFv~3dP2vGz+U254-Q)QxD~};ad#-W(Rk# z{HT}DOXt}a^YYnTEih!nBW`ZrCMW1jx3xa}gMdPq%JrY9cNY_&QGn0GE7(=)Vqcgw za7s=KL&ofyT8Z05vWA^ zz702ls<)Uu)+?opT2o8c3rg*Oei5yPju@D1diL z?k?LKM(&bfQg;!@-KC#Va+kqS?k*9Xxr@W)E)JKw2+qqJB5&dMg9)+*?1!7$Dyd4y zS*BZlAGujV#PWm;MX=QGuY}_Xxx+9@NQOxhB915INu#U*gYtw#be0f@DIbr&Fe7r~}e<9T*X&WBwz$a-T1}qG(jnxSFvGdzB3!TM%P4W@fdFWgi8lV=2 zuetgn4asY^A{m_Dtevt(i!{z{lutg#ij2s%ak7?MTmvgE)@Tf!N6?93PEGO?8t;c! zlT@%EOPEiLNvfOT9FiYvacckcj!jfFw#FoUVb~1(vd|y;+4kz(cjdkZ zfN@D~WZ&G4AXm2V{)GRL@Hs)p608Gnu&V1>`MKz2IKM>GD#c?7MktOsVfN0Ixp81u zb!;0{)rW-y(mZm#T&SiXZQJ@gz!7BaUEB_l(IfJc5GaIe2)rFATbV;WVbmn3Sy0u9 zpf$_aAbY^(WK6Yv|X`fw4HGHkZRaizs^ zE7w)NQ5<($9FJHW`@PLJHJG+oaokdVsl{|CsyP+%FG*OWNdKAcl>>C6mf$SqwK2xXnW3@gKr3AV+GFFOk`oz(I{>b{@@OLOSj*Uu7lb#F+%%0=zz)1O;mN8?=(3COB$FXBFtf z1lET;?0Oy7Lb#0UBgi#`?!$@#9c~Eh_C-K_*o|u;Y|m9NMjq&E2ybjw0$y0g-XjFm zhwr!+!dG02;F_fK=B%bbczL+K(#4)aNr$rXZ92*H#|hl`Tk}e&(NOclysvmShxG#L z;Wug2RO(NJ&JU;7mGF4@y@LD4O#T9>H=!EBJ#QB=t2~H(vk72+ovVJ!!gPvzPeVnj8`HQe7| zsd=eQiI}G9mk{uTnYa+D5E{5H=e{Ujq!xx%DYo?25bnU~k0mi1lN!9Z5T+4u$aks% zeV*hfsgz}J{Prb4toh;ZCzP-z*N2(h7s7K~$G~SD29%IYVxo6mcxElfZ$Z3zJ^Tvy zH^XNj4#oCET&Hti7ux?+ZjlOU1D$mUaOieWg|IPKL7T!5Y|BP#lD&;4XBEo71G33& zRM}|+@D_*b%SiZd`TyhYJpiLB)<4jhT_9lvHA+)3Bs2>}5|R**CKx3mMFJ{{t|0`H z5R#BWu_1^Ztk_Udxn8@1*sy@TcQ4n9Vng97iWNKW_xt9w-90I865szlboNZne0}Pf z`DR96j0M28z#T!Alk~WLZ29@en!ue*S^%hacnJ}Q zB=p;x#p6A^dBH|}43{LPe)^jC*r10E1VKvBL<(R{l2V@$U|7<-dWB$K&-bz2{r4)Cl0dKpjUXYzA$d?tquD z(Z_hVUH=K#U3&LtTB#EN_Y2{26WX_#@u;Xd$ zgaPIVybm0ikRzBuz>oVu;7`W;?Rr1J@6yi#b_HP1B2ygU`KoAjrMy+wNj0CBHTJ{aEw7koQ68T@qRcGB3s zCT5C6wG1BGdkhQ4lSup$C<1P!!UO!YYsX-)jsYEXuiB@VDC>{Wiz{v@dz-*J=g_qw7t)CD}{)x2mN?gFMec_?K z055Q`6MhMR<^UA7>vQn;dR+sG8L9TMOdC%(ChN~fi(t)Tr<3;wQUp!~WLx|dC`iDi zFv#@bAhubupGI5?BVLOxObE0L_JC=HNXy_IDJGg242?(a!M~66DAceNOgZdEf;kEA ziZYY%vKU0y>)uE@5v7qcT)~=+kOJ5KLh5e4X!@zJ@YR6(NGi%_r&(XQrzf=ONSm;t zY(_FPoiCcs0{nJ8AHP^0#qUnR_qc0>;4p0thrqifrU*HfcPPWoE`;c240%loOT9$GKjq*p;)k4n?DSRvTW(|klvcg zUYl^aq_egE9qFdUU)C0o7@ZuHpr!)y2~xTi#pf-&ScSAr`Z>fh_GIiK#GaSf9f);J z;yG7D*seF@@6~}xyvho;5tZwuG{4G)`D~aY1q@E!m*#i| z4E_=ZUoAHbGVwl2ua7|rQfFM*00!I80WA)MFE~<_!(0ywlQm${cQ0zO0`F0^b;0DchHDp zaU>x4N_LEH!MS8c+oNsBmchOj1loO1d$l*4$nya0Q)pb8RS^FpeH?WE0Xf+Ve+9a= zS`ONq^(?%EX!(l`i3e?lZoxW$8L&mK0tB1S&uxg7!GHJn%jE}J0eXW4y;m2f!CmeP zeLtb!v$$v2&@T5DhWk$z#4)iDpMx%F^gYXHFh3Teoq{s4x(Z0fIWdU_-PF zj`WrKCQAK^Melz$v@3OY7^(ZkLaZ}1DlCWzu@F)&QLZhB z%32|ww;-0sLVRxI_}GGYFczY;pGf+vRX5&^h3H`*vM)5c{1yxGxq+BxL3BxuwTFRt z(1I8i3$ffl{A59#91AhpKx8a6ax9L8=wr04vLJ4bg-A3IYb}W9Vj(6N2yIp7t+5al zrn;wC5TULy+HNoqLoJ9tu@I{aM1ch{E*4^wfjG^AsECD7#vb1Qf~9(;4bd{V&bNJj zpQZXkbPt4n#f84b7y3Oy|7t=1=0cz53;iyk+oSa%?E}z}O9R+4ILR0K9YV9MVzYLn z4ei#ie53mktJ^4zg|NEA_Eyt)Q7nXXhg@J!g^(J!Uhj?0$)^D} zT`GHy!ZV^TRP z;&}T%NH$~ct%w2g5{#+e2jqE#ydmM2?u5J>FaHwAe<9XwZ;b)YJ6MCV0lVhmtMf;^ zFhKu9j6m2B04m$H#;_YM`uIH@Z?WMKu~~@i(qE+*B5MHGu}w=Q-2TGLGmyMVCt{So z2CMs1f zU9Xehr{wn){K9EOQr|`VW6fhAzX#V}@D3+YVoruHKxA>?uro1zg>G+YHyViVJGX-Y zx9HhO;K$f1BuI&rkFnG7_HlXR{TNd{r6If$iToJ*!bb7?AX~2oFFI0v=lNy>clozV z9|wz(ND90HI_}=G9A@0CFU8AEdNF?cL9$J{1~J+n9+rp|yE5k$^uAMK+_V)?008XT8;cbG_AobG?<| z+w}Wj?o34*O*)-pY4S+ITaG5z32|y-Cp(%j$!O9Im|jiRBE@L(oWM{M2gcHbDMph` z5~C(RB4#wPp&d;OI5jcg)P&&M^g&=ArAd*k$zuuE9bo54LI*of>|{q1CK*jCqcqtM zDMphtfuSZ2jHL-vj3!4&jGBZIGn&}YjwS}2niz0uLhx<+6t5sMq-LF(pHJlNV=nKBpcc>k^!fY z3^J3wPJgrvy1F{6AH-m?=9Jd)7=AY1uS zA`g|flO5%mWE<`h;Oo?i2^tPIhpa#FmQ_@t?lfN-YPH*Bi|oTO=*49s#%v z%;y~gxH~O4PU_=zdraLiYhRVu9!JZT!Q(hJyG0MQAij--m~S9Xupp9p$F%*+4aB(? z#DG|cH3s5F3u1CC#5)FJjRi3;7GihPM89Z3ToVg%fPr|=f_O9*Vu*oALJLNFd>9LH zqZu8g0)pMou2_gsrY}shXr%UuvB!7=!KR;FM#VxD7>G&>qA(Vs$Ut0RL0l9IaS0%p z^NG4YGVnG4euXK`$8123&ZS_u$9dtb`L%}ZQx@5eY-o3!vd%#KV$o~U*HSN7bF&Y< z_YL%3R@>3fhIZ+FY#>Ghg7QtZA%ZnaeduX!Qrn`ZS@h1Zp+oyGgYAZ; z+-V%_RHt!Zl4(`X0HU{5JsT;eRb3%4Y*if?t5s!+X;s%qjIHX6h?%a(hIU$21I|{} zfU{L4_%?kGm^-biaftg3rjusw$BB+2n}jx7NITV0gh?DPP07IZ=RiG=T_M=0Z4gy zv%U;}-z#ar|3n{|jeD%l$M+Qe3S4qW8l90&!r$Mthue97E5};~FF-?K)vs#PW|UY! zyfO4v<#&!k*rr7Un$J~t^Vtdz^$lmmK6wYkMgx!X-S*`%zs zQl58HZZ;_!tdvjOly^2As|CS@-xEPxZtcdmu1nvX?&qAAG6R6-@IxUmzDb`!# z4@|n2gh$LVbgy&h^6)}YSUUV|dO^*RYHC3Oq&)+%gT^vO|F7fV%(O*!L5=10EpH0G zX}|FL8@y(t^_Bg)W$+yyezi@rQqq7p(grclp5iiTzHs5ov%KVmw>BVt!rMWW)Cut& zdOg0o1|k;Z4=bj*#p0z>xUhVuPljtikhQVpihqtbHc&&|o1IA&59TOM%Y9fHGbvnGGcSdc!X>j){62=Sd>yAV3E zsq4NE#_2)Wy>Ixin#weF*eolWF3~4+GNewqATSBAPw3$wlyaf`0U|>e0hJp_`i3VK zR-_rD{?y4B)sdq6U)CFo*7=&&vuIr)e^i9S1;~Jl^dDz~+uFd4;?lyh{Mm)!dBxR5 zBW^QFd+*orJAMod)%$T7tfcpR6pn>^k;#idArNYx+1^2C|p%oQ3!LDSB7Vl z*YLiMiM-whH+dkh0(qZIXbd2_4u;;{ax1GlFbR!rw1zY7+*z@bvQPuy8$sz>5QyfN*zkKRhdu zml;r=kxW?xG_!Yx5Lct)3XC*+XO<(y?47w?V%$6P7-H-7DnQn@cZR!g9!2^-;1xLH zT5vPeUU#T{?oj(xsJ$6W4f~b)Nna$pQOt&%%>e5{qPbyM#%#!Gvkyh&hMca5?a~X- z8xw8~)WC+E&ESr`(|C!#mYZ+3AngUF*_&_Dg#b6-jFA{O-<*WlnE`^C%{LB`wfV;2 zaPy79;pQ6`XTAOvOq|U(XfNa+uAb;LLN?zlf%NjpV|kO<8*nh{U9X1#$TQuv&hmip zPB#S-QB1zm(@w{oQ*3MH&Z%!Em^-KPRniA@%1wNl6<7D8z|9X!NjmW4b|X{{B+}On z!?55gQ@#$&L0nJ3aR&G&_sjHV(U=26e5y-f-3nld>v5k=`h5I-tr%+|V)r11f2i;* zy-6hcK%k~7e`X=hL(;2si|jMeN=?S=9lCO1OI3|nkUt9%OanC)F{HkW)XSL4pX(!2 zK_}x>(BbPHdgLj%4~vledESA%88M_@jMVp;%AYNfso(1DNDcgmx2c0{{xkl~7UE)z zH`_h0+fQX{SyL58tqxBs%&)dzqRk{R-g8t?Q8BxCdU1MMb>Zxa^6>1!>ijTY;Ci6K zvTEEAR8pdj1^E($&6>>M+A6=h;f;baa)lU^cj%euXwFA0$e-m1#wc?TBcxnH#@9Ra zxwwz)KE#6j*??dmuSJZIazz(| z1P6%;pe^w{+NNbj>zSTYqK1LQ7z9i_1AMdAh*Ox`PeX1oVTb+`8CQZ>kUupDMwp3+ zL72$^3@kx>os)8gn;-j#Fi&WDinQBD|K|i|PDOEbL6K^=ul^E$LQvB52%4TP0MiP~ zrXy>rSQMjj2bWy^44gd zSc%lYXv9x-QYxXtPE)3SXOY@zRQR15e6K4i%V$>R&sOdJ;NMVzB)uwRSrD};P^2CL z2KO^;)?^GPT=_joeoOERp;P9!RNU7PM_|?(%Y$p_BIn2*(DCsk+j!zg9^&asw>M&OHqGo$^N1u^)s;A52fKK z1YCFJm)CIP0y+8RHQg))$1H+r2)aYpAew<#kUwJ(jA^6ftkRUts z^S~-WEXbc41cNmZF{JJT!1I{OpUWdt!F?!OPrlxvPegPrA^G#71Gxe*i~GYi_oFE@ zL3Zdhz}kvfkUu{l818Q%hSVScTMa`hf07V9so&~Ew8#NoEI!IfnT3YJD%ZDoGU*oK z{IwghAE+=Xb6KRmzGNOd-NqqhCU zqQjFbUv*NRTH#2wl2V~q5X~z|*7Ta$vlm1)1nN&fg)q*L4?jo964*m_kTm-obIGN5MA-oLo z=M5)y@=4b4r*fw1aNgS_vO{NLSJ7vL1vQaM>}|LxOjw_|Ul)Z#Ot89vWjj1{<>udO{D9Fb2yI zgH;yJD6A|jD`3ksVfJjb73set;Cf>D{kQzyi(d#ca3zk9Jq*I|rx$_|`%rvIJfTP6 zleg2i`q^v2XNSJ&dZh*tlt0|7U_i$L6mo9_P=MPMW;rP&!uf|&RHs+;xu5}IiD!(C z$q)>mi%Zk{hbL4P<14(fuqeN(2%oN{#RUr>`wX=Tz;tMUZ@c_*KLiAMYK2mCYY6g( zn<|VTnHD=~g`pI%s|I$=*C{)Cv{T>~+>=b~kfjdvgw_*!;nL`s&H86HiE z%F3q~n$nksoF53pVF;Tw$utm=MWzBs^IXIodd#IbSRF5d{8@!y^yrKYLlnF~>A@|= z;`<&dp9ate2+E&V{XmO?0{R{HrsDJPJAkk~$yeF8uU}^7o~BuGd_sOmnD&P|F#q|QjY>ikUz&F7!4M2hHHoZu~exNrt+u8Nu9#fZ}q}C z$cbfmdn`&Rk*oDr)lTPml;<*So`Sv!)1}^;P(V(z*DQ4fY4X z6Z#>{V+4n_=LF0X`k;{fF|jS&Az)%c&2XW5c%gc- zEig}z3KULfe5P4I_0gZ{<#+~A6xRj1VA14~4kHw*6ClR`igyTY(%E?bBxVLa(<2cB z>}PtML|Uv4ge6k?i7v-pu=|0`S%=o|+yi86Q^Y$gj^OuGee5T0I=64m!{2a>#xKvH z8w{8yfjt2oOzTgU@bPtAT|xLkM)zlv@wFmzz^*Uytq<;-b2iQ zzk(RScWLIz3t&(L2Eji1B;l|QFYgzWI}m$IV%H(I1F`jbFUWlh!d)=5rbC5oE? z4owDBccf!3N(B$npUL41`dD-P!tR~HgljcC>){PBrN2$T~ALkozaT39oV2V2j6c&f>RryfyUJ-TMv zRGEy#z*HrkW6Dh8aCEAP38m#_GgIWJrmU)XW?A9%l;SchG_**F_&W$jn696*7jD}? zXE1+$TAJE-^GE32TA=SLF5t{@aoNlv1x5LleNq5Ccrc^k>dO4$>MD6X%zB?+T3TMf z#Srs0Wti#|9Q`aPwY>EpkPFVjji0L3$%POz0fnGK1uOVQB{DfUTe|gumJDg>UhcUC z`0PpZ@XamA$;-!2PFi~SP_hjd6;Cg$9F>tXpeya8c#E+}7x(0cM!0Tm*j(DF;r z%I$WB7r~4r8prgo3=HhPw^_A5b0OQ(HaGQwPRHJs8c@1zi>vS^IJY+vb`R%;X{FL| zL4HMkL2>m0oYJ<(p1}o$NZb=vF^SV^W~ksP`4FA&EW;K~caegp5<#_jo2JX#KsHHy zvsW1xcY-I+L0m7SnWyJh=U0^0;APcr5K>HU@-&PZG_$%$1*?hKVsFZq*e*PZ*()r= z)e9;xdKr_@BcR%}>jq8}A0pkP*YVq?Zwg*aevz!(yuCl-$841NJ!yy^cb~+kBqKhh z7w~nP>-r&n!p-2K+wACp_|zW7Al9ZFO@Hvj`x(bVCnoT!+>cuooVh1@x%QSGD!4GN zQZmrON9j;Eg5I47tryw_L&)25+3!#MSxcUH?@WQwx$83UK=vY!vlu7?t(0afe3Hj9_yr>O z!Z@xEf>D6^>(qzf4e=?G<{^SyI1kh>CkM%A$px}OpXHSMf5pd$7IVpf6%H3xe?_ul zh8I_v+N^?i#A!OJWK%)ToP?$&i~&?3f-xM~6jWlN3e|LVT-vbY8VWfRv2w-Ce+uMz zYJjoaRjekC`KA|Fp@9LnwQ+Gv>koDo4JZV!GX)UuhDqUF+i_ttcALhUs(k%;|7JpGs|jZh>y`nS$XAbj6&UM@62#sMmQ%YFFUMW zccf{7fl`P+%1ATeACd+cY@|6ftWJb)hOrHYf+7A`(oAkDY4V^v7L1;EI7zYMkHu+9 zQ*okV1H&o}9)g~-D;1=RVCV@N<1sq$0zP--rMr)L$#Xg|{c?Du+C zRGP~87_#ZaqFPlUP4@!$yc^3)C6C}Y&1vy*zz6DFrMqKFz%vxb4tTTeq%PF5;KF>9VkHuy-SezcoMrJvT<=%*!Ly0wuqF)2? z&m0C$=j7F6Q`}s}v+dAna7G$}U`(}+jfW9`XWWD_Ta;B?R$N`2Us`+$T!Bi9tEw?u zWEpK0n5}gf4l6mnD^^|;3?N09=qb^L6cc5ia~yX!-94*YNVXu zojOy&zL>s^W4I;LTKY9Gc+X~H=C)G1aa@uaAJzOk<^r<)amcItV2X7hf@>vZsE>`C zI_jXIeIDpu?$DMV*3dpKZrT>*PeFCNLs`aZhH^>Vl%*z!Nrq!e{zwE*Z7{@djGK5Z zwZWKaHF!PZ@MC3AsR{j&!FR{UFLPSq%;GZiavU}4wpgs#8$q#~f?&i={ckEP{91hC zW>wlIK7fX(0M)sq%y}+lD8Cb@#4hPBB>ghTzUd0RFnA0*jV8a z{dW-7Sok!eC%?qv*N?>i{$xiM)?Y~(a|3!JNKPYJXC~XsCmIHaG?IbRYe9Q`1bvlU zv>#+YG(OU41FDL!W;lzR)@Bq}BV%=Ykn4?WRu$)w4+1G=OKbR-PXx<>L9U!q=>l{{h91Y^uf*sX}^1e3lT(ACq!l ze643AxJ6ra!>*g)>NsUi&#$(HJ`ZFsCuy3(2rZ|}CMc>M)FyZ5jaa^`)M zVRKBQ*%*zcgF|@)7mLBn#xcNXL$r7Xv^P5Romn4?$(+V9sk6)tLmkgWFa<)&T=E8# z!Qj&Pm=3C_f%mqwRv1ZL2g3tuw;zt;Vr)>rDFoxW5R&Z85DehOqgx z(QHhaz8W0<6~RRrv;ELG26mnK1+)_}3NTuD>WpZyN6WanExUl#S!XK2^>l}`X{VRb z{j5ivOpSYHPCYAz$Uscwh44Yqjd%=lIf~0VMo}yRa`Aern9gq-PA3h|DeM|)a8##6(0LEibn&rHLb?M z`1l=+;3{j$(C43!A#YIFi18pqcrHSW5Dl-AgBS({hr}mKc13xG8GGQPzZ2%(k`Y8v zmD&ldWpHwQbTLaIPV!i$_Ov<+K>Q+`f!hFtOw%L$W;G2zyYcx7+(HN2dFK=&;phrr z>AIxRyxlfvI+&i}!`RahiN4o21y9oyy#sb%*TT}$@PS5ivs z(eL?2a%^BtMJ@4^vriz{&yH}?JjsuzoPF?*$pv>M>nm@58up(cxDAQaV2^W{sdL}` zld_qzvv;~u2O#)~W|f^*330XRPD{}6oV3f>Z4l@wAEBh6dCJ)Ee@wCo1$l7ylI|&I z0JbC{L|e@D%QBgH1z3K5OLpus9!tc zIzooz3xck+R-Y_;)`Cmp6FMy;k`Ow0tGXVOh11~>jJ4d(z^$zw8C39+IQi2(-yK{y z9qvYO{>F#9+uxGAnch>u3M5B zr1kw^@gmvCT7fh&j)_(BLVVc=SI4JRM%BFhikyl3G}QZME7cRhD8o{vy2<`J>~@G# zkxaqIL_AHSPX+NsWMC%vXJY;)_;j3npnQ$#{LarwBUIfGUVwozV<>DsdXXoO;VM-l=U{v(v;=l0G8N4)J zZt0SXrTMX$cpI3la~OIuQ5ddj8it-c>@*z5{UZ41p)g(DbWBYaE(O2qVtJ}mVfR$i zu(NaUS27x5E9l9==kQe;d?Q|F13mSy49EIL)x~mfT%VSZ0znWH)ww$Fm)8xC7m2Fsw&HI{|!WHU?-I$WvY2+_KE*nQF@>?~WZ0I&ON z;b)re#5a55w3gT6nu^3pES}x)n2gHFnL8E8K-e}sBfqq&(45FpIvOK>gzi^%^Ece&aWI^}j48sU zX(jcVHA>Wuxj5$oK}7X4r-&-$F1Rs1)74h))aBs&FPpbd`7%YTiP*X}P`e!oXJ>@k zbXFWb#o>SQUOH?60f4udy*&ekzX?Y|g9;4VkZKwou04rJ*vn%>GC%B{;Y%&}5+lNvmDZhmQv zRROvjX$E>}17nbKryVs7Cq5R;orYPlj^KR>!j%ZEXMv^a_)`klkFOWK)WUuZm>V7J z1H+Rl#L@boaM*ZU!ya2;k6P`3qd^MA4Bs5pNHYkfzhi|o+X~5GeI>$m2(2p{Y6YUx z3dG)s@Vjjji&lzsA>S9j+@uJ91#A5+I%|gI`~E(lYQOO$sa0A;KcJJ3>=ot# zK~f6}D~C8IyYbKm6uaabRsbzs&L2y@X}~y2r&NaEOj}T07>lq(zY2^@gK>_wQ6kyM zsdCfHYoI-uENp{*+8lD0yeNRUU}1{`Ys-PuEE3&=dAJ92NEBvvEar^T^89LHxzK}H z6NNaEh-t-T6}ZmFKHowH9J=e2UcMs{-8K9Oh_oVANIMPJc~s)GLR^UCIU>U%QSdn- zWfY)#)qYU4(p_inJ^_#CQC{9JyJb4rG=R$Hy5b&6i?B%%0jjJt0^m4xfIKdERUS)wV5py+dhrN(TslJ@%g>U>6*tM_%8%r6b>J*QuIwd`8#Cl_ zQ?5K#mCNH_m&@bkb@I67EqUC!3y(pmEp#>*4oYpMlb7(Bs0YZiwf=KI{vFas$=_{s zi9Flt*W|goK1-f^=-cGEr{0d|;P5f&s$2WOykpRV;6k->o^3M3Ia3Gcnns_g^S6qP zIRdpoC@I7EpGxd&oDv5>Fw{LGP;PyWAdRb<)mcW)T)IQ#^B0F};~(m3j{Sd~=jW?G zMg-K*$rzJ<*GYn-C@i=H>$^JO%v-{Z35$EZkE=>8f5_Ea0`}jXlY1B}{bwAn+ zlrp1er44Y=KpMjs97X&K>P7HajAh!M&HN-0eu3sspN`82RQJ3FkrE#O!NAD#NKl(# zQxsyA=q7lediO~Uq9&&eS~#G$0<>;)=<(=8tVFBs$AA`m2ykAT#~M&8N=xgsI?$nR zUx3cnq}2}Es$pWSsSOfJ27S8MaTxa^h_x*IlNyST(QSYD6O;hI#-=%s5KOrItPR%IK)IX zLu_tCiHT4`>r&9U&Y>lOjW{Es0ITjeE1Nf~)Kdh^ktm;l*4H*Y^CdEjE+XB}Y!DIK zBZN8F2eoh@f~hli9tunKa6?vp~FQbU5QGc-TjDP@Cwht{Bu-gVKpulo#!%F-A9Fwhjrns7?sih zo98GN$q*^4c4alCrKp+VYPIjH-5|?hR104}Gs@KkfXbP3F!se+lL|wJs)J|og-V)^ zw3anJ@bS}Q;**FU%y^HM?~n47C3wnO@<; z%t@m%0cd^c?^$}%RIKE3xGyJ}Mt)*p-fhncQ%usm*fQwL!3lj9R_=r%GkuDath@KZ z;On{np;2{y(99>k*MiObX!%l0ydbm$TzR;3K~W8M-t+7LTqBU(bHtEyFof!PVF^^q z?zwD|{B_Z6d0c#vJT6%)k4xW_$7MU@ary4&5@C6oJg&%-$Cah>xatgfTz$JduKABV zR{SK7YY#Y&c-I{+j~l1SjvgnP2(vF12=+w925K$(m*v)H7AnAcK1LwD5Xac>4=p zFV9iv=CF086SnO=4OPUX@Z9c%uQ;ifG#3+k?Jz-Lrj8T7=CVrCQoPLN#!+Yvz z2auRV5{!5?5VuEM_u?zR?bQhROB6A$@*y!sa=n*71v}g2Oj1&K&!HfCuq>%AIM%~c z%KG_}N=x^GA#aNnu~YJ))qbEXWZYtzQ45tcP_dy{Kom=EA&E#)>{2+#NmdEjwGr_^ zQ4+g&uC;|R5*<}oK0_r8@h36>iRb~zo}EBgU`P1F+5n<8F${gxVg}C69s+gH3-qcu z3MGR>@bVcmRIjR|Swi_IX3=q)cdu&J)5Ku|x}l#rRkAyS*tx|yW&k-8m3=ls>*B9r zOapgzG~9fpF9ysC8!jYV#@bz^nRiDwRhU0pq&Oc0Fzm2770jIvm5HO@28m51Q$cnk zEFqUgFwB&X!liHZdrU zvEA@DiNQra560|b8{8%D8q6_2r%JgAfj^$!Q z3+HHZy9Yd=^m_n~yE_^7)5Dhbz^0qCSQdCuh^aK!A$=7>>+`@z84#&77Kkhx=%)bj zstsk%R2+X|KTwk!76@uS1ir9g%trtkeZ0s%F!M(0a$wf*%lH_0G9L#`>(61;K?S2O z=cDjAdCaJg$IJ`lQM5`P#gECOi)y`X!ZJ5Z?y-PK(0-4aR8`h1= zHO-imJ`*pvR@i%v<~U{oFI4GWt+|dnfme+5o~OB_Ibq)v#820|80T#$z0cCD31cot zj`hC5YM`?mu9hsh=K7dy>wO_y^!sDzFdkfAKnNWG1wvJbS%mKXdxR#8nJcvRDe}?^ z%_PCjt5Gt0KU@#NwEoizBZNbrtZ;X+B^m{_vy3z{olYV8e53 z$hilCpsV3LA+XO8rW^DrC10yt`Rt3n78rr z{n!qN1jlvQ0s`ug;87hmBGR+(aDJ-ig>%cR!sBrS(rjENs#>sX>rlQLRQTPF)nuh5 zg3(`qk!i(css$r>Ch}QcQ?2%71YKcSfl9%od$<@RF`N^UVd@F(RZId7Dpb36?OHfF z8e=wSpNW6-E$JYk3_p!<*Bmz62sg95eEKw8N+FU?%a^zai%~G&i@Y;%kt_dbVY7#9 zBo-r<{XSYM=Y5n=d4+|uupp~ss-we`ms&#McupfwK~1GG9rX{`+^YTtJCLMG^AEDY7*RZYe9F_u91meR~7t+M+~+=B9$-7oJH_Uip6$+Swp zV}6#G$0l9Pms9%4{*OHJzmdnZ*4GfdpocuB50^*b z3Gyg9O&-Np;&GU1>tI}t+ofHot+7Mrnp}=0JnKYg3q^H2k#)D+jAEvgL9++&^DIK^ z8y^cQ8P2r63o+YM$w53*x`=WaO$Gi3gx`FSVjYTd=I*D|yh>CO)&D`pZ6$CncSDp& zsBo3AP}JTtsYJ0+f?DcR@N)q|>-Eq3Dd8ZVB+IYGMv2uxc-#jmR*B@^zdoJjhygTG?Eh448JF&!D|86&Cxo%8KQ7L2Kc() z99$@k(E33T53MH$L&)l0d=}pR6s|+0<@$@}EIhmnc)bDWFFGXN zqC5th9%smaXnBAwD-DADI}lq{+RBi>l9hq9vHr;3rGSCJ8|mPZoZ@$SnMyPB)G?eh zn97bCQVodcT8b5osm}+sl@3LA{h=GsteMa8;TQCSNsjZEE4yle=~Z$jWTA&aIg8<8 zSbPoge>f~yK7@rb)K937dT}jeM^&M>L@QKCC0Q4QGE({FA(=V$%Ppx;?w8OeH3wcy{)|nlHw$QUZB+9SI(B{)+@4Ts zO8pVDOghU=v6_ksuGfIT6E+dEHJ0y{MG4oVrIq{B$~LsZ=|T;YY8J&n4QBTcO!i5^ zBgIJCSbBw&RsNJACDXA(+afs`YgL^n8u`6_fTDhZ~m7GQ{#w!mM$45r#S{~geG+9cG-s-p6Fv-8Uq0Q)q5>_H9) zNJt*^hj~o?t0Z;+ya!VnW0l^Q69@7)ZU&vD(hvF*BM*Ayq63gso?g>9J(4|OVv7|R zJ`9-LT^=V4lgHGld}Itf6Aw7y!6(bP6$6)v!V69Oqwop7$FYM?!OXr5V$$S$o%7P} z_gRF8;X%0hHR^@h!_;~%A;xku%UtwaGHJplm#shq7p{Kc4p-X?EK4qD;^NmS^RPBb zXXeV|wP#(7e}&|SK+#&%>7o-UzBP`rd~qVu;BnI=%GT$fQ%G7d z@-0a03dz4h@*_eXA5k9ao&$)G)zLso>i?VBHpVaCT#JmNFQY8tO7VT6+-nW~6_OvZ z2K{p*tB&m@Sr$%n>g(Qtgj2B;VS{4f*mzQwMJ+gc3au}OiV zOlJxvoL@OJY?j{#0jl*CXsj>;Nd5Xp0IAIWBqMInHC2UG)-A>8FAKq9BpW#AZGXHu z>{CGZ3g_W0di-_xYxwH~4h1t0HzKedauIL`8~`z(LF>v zR8rg1Fr6T$xS5w#Drx&0Kwuf*8&8K_cK7S_$##l!Qwdv2o`^}79d!BssVz+I)I{@l5{jBY&9Yqm;rcO z7V|a(ZkZ*c1_G%98!_=T+G{SyxkP@b`aivAM-Y?KVB!I}#r*75kd{jg5Ux1*gZpriQ1+zNU(o#lEJFx5vJwl263Grk3x-yiTW<-^9MAmMxKq zk;CBXE_P}e;!c46<0f&s0=Fcz0=T_uvt|QgV zMsFVZI=^5}4R+*Uo;jL`xB~|xUjv%-F?k*VT?H?Yf~soOvi_Ax8BSv8Q^&3v>r=rV z7?}+$zT0mscYUsM&N2Z@*{*$Im8N$MXVvKqD?CV2W6CF*4bV;LWfeA2CS$yQkbaK*sSmbY*i{zzc|8B zG|Mveug5l0n25F9tgvE+%s&VGPet}LWG%Om{Qt%}Fr;GBPAVZyVJ_Xvbhs+O6G(~VRawGIWRoALb1x3Ld^=NIuA*OK-RsiX}&%KjTC5* zvrx{xZbwd>fLSwCI9syAD#r7xY9@^1$aivXFD!oGO@7IC$moh2lD}m!o!0a^#58&P zrSt)%Enuv1pA3c!kbn?>jf z@`>>3X&fIjA^VF>-}VS%#wwYPWbB)$#yzm;e6zd-dVlmMvw4HU|DE3MdD8`LHw}Zp| z4i~fWM>6NlCSYK$c5br_S&v{8`H?z|IKwSYD)d!jS($oQ4IT?^KBn%;cA?N8jioQ^ zdpNxHZnPKr1F-nQW)l*14dsq>Ue~C;LDs(wHk-&w8ZpCaXydhs=9NHRBQL?e z9t2~#>D&+&I<)b;Je$RC2K74}4&Cf+m7&q((x?K4Jzla!)6SyZFU4vWg0Wi)uh&F5 zIHu8jGKEr(=>w|3;0&9I=>z1@=FrKF=3_6>^Ze%b!D5TUhILgHN`pDG(QMFDqMsp? z;ppCvL@>4gVKSM)IqOhSqnV(MF-)YHxeZM2i)17RcIpAQ>#-QI5(E4;ZO2z%i5YzvG*A5KV4Pht zkuTK5OW;V^HjnWZ6W237iSfilVzuQqVN{KDuPwNA8DB%gVLvy}^S}$2X7TKbQW-Sm zEXRi&LhGMc0E_^~|8oMMnBdFpB?$0eTZoX}=13C$=Oh_VLGrG^lo^6pBa|qDl%h_w zE&PWBK@MSK4o>_DPtUK;hp#wpg4hayesF|pN1=*_L#Tg9BpLV=p?Hc>tnZBjIJ$2; z_WUfIvvm#D+S-nJ3g2r<3jsE9pMLoJ=H-{8pdUa-`?h7Q07=&(t>f1;=A7n}oZI@@ zsZOdeOLqBaOwdE5z-R|!nJ^Qf_5QS=k)#Ha7)KpXyVFEJ)7xSQYH64*RFt7tut$04G;T?5{?NAR~y z|3iP&G3(g5X6W9frS5PYKC%!*dDB{V+@mM>-r8)+;}9<#zXJb+0yyF0R& z*UmQy3_kk!IM_aPe&WQ1eMM5YiODc@Fy$ zs?ZDN&!lOB4?vVB9C5@hG8pFQ*PUTadKu&87M7b)6VI{!L&M;=k9!8ocx~TiS;T{7 z0gsDee>~U^N_H&v+Cq28;nm=s2g&C&X^i4tHm&-BQ&~4skZ@Kjb ziF0#O+ig78Nz2BgwqI>IlJOlq@c!!)A%SjF&Puoawr3gVw!OCB(CRwrpc}wM#wR;C zKG8{s;Ki_c9I)(?QsJCH)f7W->HqY7VDT_Q>xpcXx(YMvjDV)*W||`1^G5hKAqdCN zNiN%t<}0tM+47!9G=chh@VwjMOIsV}$)dANFuDGRP4aOA3zK=K)d=gZH=)-@Fq(@5 zg;3us#)#&z0i-}Lq;CZCI~@T^X?mmm(apnF`aa!Z721CUqjr_cb#n7?6=khVzYKg= zIlQT{*V-pFj{sEL;=k9wlh9$~uX*^R@W$@+JkD0#-!Pug2j7bCe+03IGF;WR_OKgEns;U*&mmHhz>e|41=rUlKRtj$$aExrSX-62@2KHU?l&TbB_Sp|01N{RXe zJim1Kb}Nv&Gq*VyXOK3GENqXv6O+IQmb&k;!Cu%LTn837MOp0pCYZkCGA`mg>Uqt< zI1}HcqOY(WeHXsM5G;Lpd_vocn}e-=?gINFy#XAbakm5@k2-@7jXvU?PkzCZ9r8~CAPSPai-dgVy>d=yCJ}Rwh$_H8b91OH%oRGq_qNK)aGly5>N;#i zWc3J*A3(>;{ekO3;Ua!m;HDd2&6p-hh7B3{VEv)1baaF2b+O=c13xG(H*`2 zo#1+}%~=g!)(4&8tfudiFb{{$usWx{t?wKcVb(AQ{uQ|OMLwCwK7`G*2*x)24lskH zRLa_RXEPY+Zwo7#D8l{*K||{zgq7Sh!X~!(KVaF~=YT|m#~}SHks6rEATf8B{|g$F zxP_zSVR-!_cnSxHmR1S5gzd+v%^-3nPI|*ZGYu}yrX+WK1k+3iuCOXqM7;9|KVm}jPrsD^XY}8 z*Mj9bo2^P7*cBZ2Y}xd+H7y%!&p7(06es!>SDRxOti>@E82A`&4L~ruyX83fzx*8B zw;9Aqr+d9u9tCx=nD}x%;W7!>+HMt-71L7uautjE*D&uiW?rxO3OLKhK)2YGENgO1} zStNW1T>we0aHNsxjnpn_kYQYN$iVw?qURU*$sI^~9DaQWl8-9&bGFM%S)QBv8&teB zBqEg8_*to1{hPs6tR&cix`-yNq&WcBV(CtM9 z_Im=}mk6dzzl{Q-hQHnwSz=C7Z_)`Twl)pxs_bGcJyqaI$oGmPBMY%5<26lR`TxF* zJV%Ez1&W@;u2%$ON~voi>wY`=?R!^qWGY)KIjG8Nskp+45taWXw75T2wRs9tKL|!f zuKI_@7s8%K#)tl2mk}4`sx^@BiAWh=8VeaySbX4*j$>=&gTdT#9ecgxLA{2l-LGnn z)|jrk(GujkduF*=7FBBD(>V1LLH5b=53^6!m|V8Vj_?xk+oCTjm~+dTv<9Z7dpXC5 zt_ME@Uta_foqxoL?l__K;F;O7Vy`Su2UYWOW^>zvyZg@15`SmohJEt&|9Ac2 z;yLrXiQWGG|6)XU{8)*@u&b(@eiu*ur*=f%eOq0L86Mt~$%8kZeaSs|gYl$GpLY-5 z7^#GceMi${!9F^0aiR{|t=UxVYc+|_Ym`6-_7>marJBJQkuB(TvA!PayS1Xx#-TK}MeUP&*z91b6)B1FzJ^4ZTB4MI7W%3jyPPx57rW zc1$Hv&B*u$cwO5J{Z-=r zK^7#R{ex$}f|IZjT6d>?>)1c|Hz}Js`57pGV^a?uuO#m?%8z*!WftkC+{8G)vO!kQ>2J zuVY7B-NdEgI1zLTNK33y$5yp^X_=CIH)yT1>4i?B`gQC$YORK2qL?i#C?wA8b^L3Eu%Sn8S4-*-D$18DN7lsDn}hahsLG2hoxU!j=_k-@sKjb?Fk;fa0Sz)p~bk)lD#Z%N5)8J<%a_Mlj}(two8o zZ@H?6e#~Afj=(BGeX+xVJnP*{RSz5gO;YcJ#}UI9JaVahNL^uHrkt{eEP|T z_oZ)tvR(EgF?X;niAKcZZRm#w+eAFrY>gvFXRi{a(MfauLp|9i9QQtF0x65=DYNwb{Ex4kMr8-jrB$l``jT&wGCC@Z9@&B zn#Ro`Bz^FIaYqV*s2vj03n8$Ve$;-Tdg+5Qo1ybTb-6?N99qG5qffo`%%M#0g4AaY zxn(Z7s2x1@k`wVP?fbs1)KCPYJ!SMN96Udj~IB76>rUg;2~v|hqddv59_?7wV` zv8ToDM{sKKj;%(UZ{6}if0d}sG<9<`!}J^wn_v^HeV0t#q|Ejg6outbUtbM!|FTJj zq&WC)f2o(I6bB*s70CVUkdz|fyRW5QlC|2`h@ST@T%Qq4DWQ4&H>}jn$KD6RO3m-6 z+d=VRhpJQa{CA=>jCAdK=#KAU{TsofgYOoTx*0i}IBr23q-TNN9Ghk>dq)~bZ!PIP z3R+K+-nFdVzMDeoCTDHD5aANv$FdHBC~Ogn-tEyKY9iV(pfr`#L^R)R9CZ_O_es1& zRc`^UyBz5Zz3BZJ4JEjN1b2eipRRy@8!zgnXYXwg@p3-ETn~aVUaIeoiiVLB;g*Bc z3X(I{^W7>@H#v6?gk;AS5c|p@Sj!%Wx+&V58-!lwCY*nXU`u&fuL@Rj)%U|&w|byq{YFQj7t1>1~ibM)#7&k2(w-YF4YjnI{q&Ib(8k-tGezx9h)BG-Y|(|+`#{pA}#a0Lmr{uoE!A&9b88S-_6ubZHshx>GpEsrGa z=iy#AVdFwBy5A34kCUEjOJCRZh7c6pTYiEMUj&ctzCP)76Lh@3MaU_j7bZ>lK=OAs zZy-I9@pjN!ZOaIE?o!ufh?*G(tk0A>6v320(<=J8 zU^kfVcp2j^1Ieo$vbRcO>*rftH`xfM>5oC~3!9{`({$a$8t!167RL7+y`gb(hYTrC z_#EfoBN!ub;9&X>UvKCpmO|_)QhWjlzKfJX4D0LW+{9AU{(&K)owfzHJ|cJwBwtHL zv?%}OCKe5j$#y;kzfh_Zf=3MZ^U8SrMmZ+eElYzvj;#`7O3V3>V0nZT{vO9o%bQ0d zN?s&>2_qnQjKC7>?@C;^47QIjXLZ6LS>ll8XSlzQaNQJL=ik+!^?*&!-}$$0Qnh;Z zqFD>H-3n(f1Y-oZ5cqoa*3G95cik#*I3t3Kue)yD^j+syj~^~|coURAaj1(Qu4$kB z{BRq}qk&sO(1=SLXw6v!PTYpO9}$c_S9L4+utinbob`pR_5N(^2$2Fy0o)ymm*?Sf9{(#PN6bq~nU^ z=XoNQVhRw%S;bVGNXJgFjP&8Ghhvm;rprpd05-2hvT_!>#1T_`I+Fgxf>d0Nm_Dmw zQGS3v5Di>6i^r$Yhg%cN6XPD}!*W>t9)1y*zuLZioxaCf0axLAmutq8W@CbU0uL_i zJx5=S9SF8>-*adY8{ylB>utRkWA(?%>jmJn6Cv~w8{1WgHN=fCmJmJmNNe;CFRjp( zB-j9tJrIm?&g&n>J+100ha8Dp^40H}NBL&w&kxHn8JN1ljYL%K15*1R2V4#C!UK)J z;cy2I%(S83*&J+#JLYHS%l1cB)_F}7xH-Och{LlE27qVXbs#97bu0!ZlUN8;=lgqs zWStj1fsDtAuINbo?a;M+=yr#24t4S2{tNu_$;-nmwDnv3E$idd#_LiSS)A!icOM|e zJvjpikn#s0BeIJmiXU%qrWhHjM2I zM*$<9;UYwgX@VkBEeE(8esLY(Zut=oaH6&40LK@$103VF103UZIlx(0Vf%QxmH(hr z4MOYbRI?6Gx7tXU0{$RKJZX~&Rk*Z#UD|4+HJ)`vLzgxPB2nlxAyvEYnlGssCpD~; z9|w{J4%zcaw$}6JeThn*udpdtS#G(yO_=Vux~)R#OS%_emaE$zC=<4;8?!R$RixG7 z>Spt<#iwmK1n>S6Dh@*HYpF$DK5g|7V?I@Y&|DI`RfyH$yk--N@lvBk4}rvbhfEz_ zY8Dl#k6#1fdm@PmL^cY9fn6R! z!BU`hCoqeQl>e1Lz1>Ey*W1g&jCSi{YxO0Fd~Xw~_atqbP_+LRI_dssFGnC4laO@1 z-di>`e>bf~Ahy&Y=(=gKUvj&lq;;svwwqQ&JKm61{_P1JHQldhN<3%?Jf4K`c(7{Y z!Ad!fTpZUdN}!Y4{OY)7QCr(J>jX4Gwrkc5i5u6f0VRBIT(e#%XWY1EO@;)vYu0-b z_qt}yA(Q7YaA^DOAndr)mYWtEFr9P}68+q?EW4Vn_5}dFhS2(DmQr7TEEnFCP5lSp z6MnOBLhrbUQI1zGA}2Q3XJi5Ea0ipp8+2a}s|fTWi^6t`83gc>iE^ZJ$$$hZ zr+yUZ&p9OiNAZ0q~ zyYrHTW4x3&ufiJ~hXF6w#uZ~&N99Hx((58i76|Ssz&qQ)m2B{JM{;p%v49cfMUdL) zkfQ8pXb(nhYuStuHDED_%X(mO=l9U0{B6=dOA?mxmV;V%>d1k-%@ANCrFCd`Hb|oa7bYru$jt&*X%Yk%_gP7{;U1Q;S+-OAF*MPRs zMfG)~u}~d<89_|INqB7$EH!=oWo%jHLwl}bEz!e)ljGv~I>uOd?nk(a zFczxE)nyG39wid{xe#UWcXf%zoHz#6d^-^Tj6e>VYVLQ4rA5KVbtOY}@5sQNH3=y$k1JZj$ zboKT142i%Lu@i$@?Tz3O+t&re!gPE+gl!HG#}HWtU;e%x(TKwJ3?MDBxhh|$4hzrr z-VlBd0Oe7a9}rdB=)?V@@PyrV;QZjS^K;X%(JX%q&OhlMEwnla!BWrHA0rCyE#gfD z&NSjlSx?^34Pr(ek!PVrcvzsKUJ0z5Y>rkP74)bh?qbo$1A8MdH#^w-vL8(LbEB}Z zBm5=yY^hZT1f#3pA#)a1gB>G^F!G~Y1a|I8S6d0{i){`ew)jRtU$=<(n866b>O-*E z9?43q;p_VlAFJ9%Kf?dSR$9$QFx631HNzmk1KQ%_4?hOmurGtt$C2FJk({p|gT=C; z4htqHjcKzJv>Jurs^;&m5El!&W{3e-g2O!#T%^GGdnH(`Z1)3xB7FqhZ4N$%-Tv+e zwv^5xV;vK<+7H201K&f&ES%aL3W`)sRtv-`T7mm za`_z81(|k$fNE{$o45y}Nf{$e|%m=f%4nyXu*Ajj{05(_u#r}Ii z>oJF(41jzW`)w+>- zQDP5kf*(O$hc!J64lg=fWEP?J#r3$ja23648?6pTFuDjAXBE9t&apOCVn{eVv#cgu zFn|8w!QpUOdFAYUoEwN6$$59`kcbJplmZ^_6SZO+0fD6%oKCQNQRKOAwhk;lUe>ww z9&KsL02ai?p|8lDVE&vG#<8t=)#?8ad*1;cS8@Em?^)Qs!xVu5LySAd1_PGdjcr_z zv0SjRO-K?DL`lek`O{j2!wb{@?HH zt9S42-kqe_`G0=?_?_ev>-P$%h!T$qTXNHb*Ed_Q+NK1%7;^T@7`Ckd6-`$b2bgL#W6S& zMB=YjDoA=8LmiJxVy^t5W3HMU3Aa*$bU%tUBr|RC_WGu7=ulf7;orf{t^t*47WU5d zDZ~R~jqzseCfL5VEuqYoZ4GrMuAzV3r$Api_&}>KWzmwSJC#zKGI~^Y9jYoZ#lt0a z$lH_h)}bpqQzq$1!dP z)(3G($Kj8hj?)jK%b9Adtvzd$nWnaSmg4T&wbRBZ8_8OQYJm@_7o01xi_P70;09(kbX-T>1MGll&Eux?Vn~L*DWnTxn3nV>( zD@*uX4erRDJr3Dq=IcC0uqvQ3%Ld^mEuXeMsIQ?Kamje;_79gtQM5z4&05%MP9VRn zS$bG~wyLJaYa?KM5i>zg#>Vbos9IcEYD}iJj!B^GD&`T3!el}UNWvsXT~tlTv8HST z=0J4WS_rPV7d&LX3KBoG$#h#n$aI>+A=ANB)NU(2%6GC2^w^I2qhh)h<~c?VcG#y4 zZ9KH2p`OE)rHrI<&Y-RaS(`lOX@d>^?LV!qIcqE>9q6SKT4A*j2-Qx-mAB_f)KtD1eCr6@-}h47uaF4)s0s3Z?|;Fp7&T3{ z`wcinRWi-cvdh(bDES4h$Z)!*@5Z+%fvIT%U}ix8*m!+U&{>5`imUw?BN=so9%2H~ z+AD~O!)QXmRuy$0d#6C)6s8M<;P7USX`GW25cG>$Kiwz4g>QNw``uj&sm$&n2nm}Bw%`}iud!FxX05Vt^g?fTn= z`v{AC&e}CsKJ&Dtz0-X|qB+r?2-?P2Qp5W%?|CtRYUx-Z-_`ICWt9;f-!i~ZJNVyK z(MejLjrHHJ&0c1ZSe3vDSvp~NE5O+vSL6XIsotLqf`f3jrDj6Hsc**K@o+sK1~Bym z^%!;SbA%_OD8%hSHqs zy?hwJPS#Z(?Lw}dzr3mj26ag4=htB$bV;b{H_%W6a7F$itPt>1y9zvv3oH&i7U0jY zQA9s-@l5YRJeb(L2^=3dcs_IS%;+jS3psz9JIGK6;}VWVzjo1Zj6@T3zEuEZT)eqC zRZpiP29;&8cH9~hi*;lYnV38otzG~LuRCP)Iu;_F#)n$|2u7?*D?hrEw9qr(1mV?D zy`Jxzs&+7piS6*k!c*5Jb$0R0BZny>_Y#q`;SR2p>(MGi-+^1Ta{n$N4c4+Ob>yb@ z3(yS@!!`69$Tefv)*`TmD+%_9)i$hA9LSBKFT+mb;}v(nz?1VWRb?7FVr*<_M_Y3u zmc;)3C8+{#mtDEo7`Hp35Co)p%#&6=LL2x|#7lD_=%Ldl1pCy}_%>SeGCY#r6?Ee~K$oMexl6zP_u#YsI#7n4x001cg>?x*99G z5D)spkAUN82aktctzCsj6x-0<47E2dU7e|9Tgwm_|CKj(2^yVIiRw_W&Ig_=99&W* zjca39;gTv5)#pI7#c*G>=;F$(j<>1GC%Xh4dl2DiJ!v)r+iC}+^rKc!4=qWHHFXJr zUM;pgw8#Klt`=htiDqlD)bC*x#i_<10mnh>dF`0duw(pd1;xC=JX-wW(^N%e`>K6ccNL)06|aTk`6@)*W7xAmZ57FC@~B8*f9&xu_i;3hkJEH{+=aAoP@J| zDk|@PVl<`p~2oZt(#N;5?wY z+D0Wh*P0dz(%*J1QZ&|=z}0(it7={j{;msA$%(VefF;m+r2CCN!5sM_CAI>2$#r6RI2CNQnR}ZrN^E^JBjd4|m znvN^-L$rPLX-cz=NG-O$zd znBJko6A$&bLsydWkvDXO30)4N0gZHMsN5zy~7d*fB+K z!yM|wjUKwwjvMh_E~);ZPR_AoAaUTmy(ZWJDKB7dGitT?JNPO*kVDKm)m+6hTT5%S zhe*4`T(8)_&1tlQ@Kp$A&Pf0#5QfxM-)f>=JdfNTaf-u*gY0l9?*&99{ruHV4J+O_ z4`}R|CN*I|X`g6X#ZK`QlYn$yH3k2ED^yRiHPHZd#3(Cbsb}?m!U1r0?+dtR-0-$k zvK1-E-heDQG3l6blfVfcnKajbKb1zl1xL&;uOnU%C zvq8E&nNe4WL=XrvEPPoD!`nKc2X9B9tJ4qYK1>@r#Yreea!%nFc zJ%x1{v1nolFt#9=H-&R$>AP|}NV(dH9U(NjB#{`E&sQ8U*--6_U01$P!FD$GfNB%Z zzShSaYD!jjU9DoQivKSk^6JsrPws2?;43E0L|+wMdF5<=)e3wGzKSmRVmc(VBDNG+ z%&Bxdy*5_Y(TG%r!db-s$NYw5OmvwHmy~o%e!4DQ0JEq*{f@bL7E+wJ;YYtyWq*$j zxM5Q1ll65_We%cIBBr7ZeP{! ztc&n!Z&UK8`klfbhGZu7;}PvhenkRdwonvJhD52+E(h{t33v34-xJ&p{(Ckn;p$o8kNDfJex+C`lj->_B&3F4EG?*tO1jW{EXIv?r)2UC{iZO=+WQkfm_wGU?} zdbqpMimhfmqgrp36Y-AD`)d|Ab`2a%=#1%=9 zz`+m0E{pUn5nuzMjUcpTfYjnbTSaL7N`%(d4rt#XvxByV!5ZP9Gkct)+E=Hn7MiEb^@cRL$oT4_YK%?CtjkPU!# z4I#2?SP)+zku@a}*^;!0d<)<{$|78bCnb?-_XY_;OuN0Q8B}Z2jAvEcxvy@fk6+1r}bXIuAQYbyLwWL zoFC}h>cy_U6%VGPZ~4p7x2Jmbt$t$Z+qNa>U4`{+*~1zWHgpn^vCVvBlH-b;M%ENo z%bmuDbzaIi8Q9j@7^5ra71!RK!)q&fVluu0;)(^R?ggxF5hdR_yfPMCMx+@aTTtoE%xM0zZv!9f>OqpL#4`l9Av0*xV%*^IEHo-F^5{I@{K@x z7ZH1^TzIJM9A+7jMN{pHQELP)PnBgvmJzIyT*-}Bbn*CUMT_nM6?ULRrosWHKv!di z8PU}swFz=O#&X*P`6p8q0xIJPySK%Y=`b^3i`-B#9Rz?7E1O13$&K%Gsd;N6(^20f zTtBt<@TD%HcXNas-i@@V-n%~X-eJg%ZcL^#?P}L2M9LPAhu3`9CpnX-*qPYXuKjgB z)mZ-O#AKdUPm*R2l6r7lQsiJm^~DwW4Hb*j*21@I7vozR0rKJ$XrQK+jUu2 zV$=R5pnDw%-UYgUIVg8V+p4wni|v~3dX!x5J#xCq- zeP0E(w;YUn9RZBHo(R9ju2*&?Mmq}$OrC$Jp$^5Rt4I=*h)|f$JI(5mYS)=vNgeaC z$H$WX?4_RQWjo@Mf!(dzaf);H@Zs;=$4?r7O9#T8T%Zl6uXjbFHi>k1p&+MORSip4IzGbPd~6yN(VN zAEkQME8|jkl4y^8ZYLgYjR}*{kLWBK>49O(Ux1QVW}z-P%Rphy6a@8iDEM*~>cTUHrW&#KMWVJuXW0>#r`BTa zOV6t$bpe{l*KO%}^WV@0Z!4hMIzu+R%hefOY-!tC=WPdbs;AhKxtR5+#oak}h}mSO zddfhN`*K&4N&YPFx!ys%q5zfp-5+?~6(W(ch-8gcd&jSan2ciSj{0^ci8woq>C9ii z==Z>}af4B+j`~p33R)%wQ&vOpgfQf_@QGPgN{;IHeTnIfnHa8`I@&i*YS)R$0W(>O zY{P}4qx0mt=n&!T7&FC()>SDnKb&x`8!FWK0L+=oZosy+PIGND{g?F^W9)a(x_BtO z^o85P*_h$-n;1&nW`ALOHO%;2;xvDbV(!1iru(Ia}PG; zRLqPCAD^#7*p(Ye(*5AZU->I4AJWZ>^UI&nDuIJNz@EU{UBrz%5nV)c?P@}#FppqB z_CYkdRFiQJH`dEvQTdRl?H;kE>Exq^SDXy^cIGG-PG@J1zFJ8u7ulqlSYvZ4-p);ye8ULH9I%t| z6u`{UdqVAO{188^jiDqzLh@kIlhBY!ZaFMO3P|PzFGDJ%AEKp?3Lz!+0}h>)z{w~Y z>*1sE-Ae2q8C%k(8nI=mU}p`j8pN@bw5cW0o~+Np2w-!JV*#NjE27Kk#AhMGN;%Fg zG{JZTg{+ECGVZr&@PY`ecwS1pbq_|Xt3IvKt@@t$?yWl6B2{ONSas^9n5d+>c>N0Q zs60i_RXj5mY%ST4YKg_`>l2xbXI(^m8CbpLp5|W5422C3J)pALvE@E`Q)ucX&ubP& z=%uO}Uf30Qe12p#)Lfp@)+V5fxJnvxO|HbG&Ev?G-X)ovnMK7G@>SPfQ4_kFapOHy z^O*bL*V;0n=la;F#HB}+Yluo2jN(N5bO z#D1kDT3DC8-cxq!mDCS8WlJR!$ zX6SvFyOgXvi83xVla!HZ8#O$bZ_m0RL8V?-M-|g}{nV)91d}5lh;|qeyEi`V-jc(rzEj2oj(v>(PZndCA7 zVx?@4leKzSt~-{ycw==x(z$D}4QsQd_{owe_r8)3HJ2ztR~9VplaR`?Snc8xYP*n| ziQz;rsgkpjkk!$IE@WE4U7wrkEt^u8NKcCt?#wiSy{%Dx&Y&b$ba@NSpnaUvLO9T1 z4)%L%v08|ha8x{@{P4C=vC3yRPbaHiyvdMHIrIxtUEXSu?o!>~ql98{OAYyKIm|8n zzo)4!d5R{)#5c~z#XP|A52Ir>OR=TL=opvwVy{9D>Z4QGmn?#WNZFTcWURKYb;(}h_Gi%By8TnOwfE4Sy-q)_7QJkHmK~fh>fFb_Fr1x z?7v&z+|b_G7Sm2iF5k$W#GzveE5@~9BEnr=$*w5fL0*X}X?ViiK3l@LQl#8vC(^}g zLJ8S}Uz_SHR77mM>)Ai}p>`2X?KejB45<{WK~+Y3x(dAH{RLJ*3);*2_iqE+IRREb zT8Rztx#!e(@g>gbncnUo%0=$-kt2&%G^3!?#@}PCS_W-9D~qE1Z=-+ZmE+OH+$QLM z&I%!G=WSFJO=xvVJq4zWJ(2cwNwgTcB-bgAjM-h%+3;JN<^5?6>k&xC1}6x`(D}7i{I9Y;K0?@{s`^ zK8mT#l6Dw4`Z>&aug3{ff~+weDeM@m>(;i**o}FTJwRiymmzu)=Pwg@%rGeYK&V^V zCc8bmiYv;^nX&yaR^4+K&8bs0!-@?E(^MoTDkZV96J9mxsCA$(NXgK_L*;uzyQODj z_75gqwa*VPhblwjS#b-ay`A9)W@7(omKhzWYA#@ zWed?*3Xv;|zQvNW9CcU|TY}|5Tn6;3)gEKI7`i1=LKy1N`5)SCQP9rzc@DBtX?)Luoi$$X zMiHv>duK$SxXzhFPNU=(b;(%HjaIMw10zqC3kGJ#zeSRnH7?!|6sTQZibDjfc03~V zgVfkg5g6=CEtg2+DpPHJI$9!)EN~u4;YN7Hr^#Dc{*l4q{Ua;HKhm1$tYwjRK5zL) z9%l{chn248`bSpBW^pM86Th?Dr$ZYv#&E)~gmdTelv5 zhUmU3#qg{g(Rm>5#3fj(_auH`NF3YAq;#_MCq!sP@7P4$=X*C4ktfZ(g>_!W1N?u>6$6}q#X%pVbVXa7;74o!Y zas8L;&@qa;*QBrf4=~#+*qh9Mmc`;R>n*{iiQx*iYui)Av-Gg-SKd^DYt0yn2eR^y ztDDH#KhbfF>FGMz{OI|fQ76mUFU{a88U9qqA<&ORnpjIBz0AtODaMISr(?1(r`j?& ze7cryz4}y3o8*NCnQD_KXHw~Qyb~>KfuBJj?cqRNBAvuGq^XWetMr zJzpkjM}Hf7hl%H8_|V0e!?6v9zK78K_1nSsAkC8XA+5xxSq-$Acas0T1|r{$4ORn5 zyRdMqsN2*{d zwL0n|F#V$;R%bB4bn!OVeSjVz-?-&|{`{Jf(rcopieaMjBeSP!+I0uhFFR{e&s&*c z=-t#->Q;j_Jn9=4%RIJC@o3tSIG99^afoumdHL{}YOoY<_Z5crKP?g8!P!!>Nx z>H40+*xaFvwNi8gL^8mWmDT6Ns|@A_%#~2T$Kf^ia?9bxwG86$+7EZ)va&&s{<4{i zxCome944d_%-4T_D2K0xXgMK|TF&97n+aHuuX%k`;-a#0rVqHv!v&q~I2SfG z8Vf5DlgD5$_p&oGx53e+=m`^Z!G+Jy{5Q#?!Oo0KSwbmX$jZ;`uj37V?8n4SDu^O=oC`cK17SDrURv3Qj)$C(dwrcU9!|Ir|7jme?25P)-kx3KnVx3H& zrlwQ7T;o*>w2ry-7)Ljyv0^XP7?!d=`$+R4ht(eIvX#ET)QfktPI1TDt+4ri%742j*o zJZ~x$XN?UKz7O1`hkEc@Y`czhUwv@8q<`OoTiNN~?7|?$`$HZ(GN)@4m*AX!owriXY4FJ%u$W4ps;)z! zPS4|6KTaINcbDUiH>zpHL&S~chC@CFLWxn*d+v0^?A4q;eU0?UlC$R>5@o>86Dsj2 z6jrN9e5aU&7ZhNraU()l2LwT)w(E6+XH3w{cqZj#27gId#nRu{ygZp{muav?XV0Uh zFC#K*Kj9fk49AVLGwiJsm$(M2YFtB-C(k4>k=9x1Tx5g)g{P5kOfb}SpjD=lJXtKp}qAQoXu{1SMCj4k{x%%c}~G1QZ!iqmawq#t!Z3iHSzv>>Mn zQ5?)FEHiipL@m6@VwKQyoy96g^Luy&Yd2tI0Af+^7&Zyv8i3Q0dDijB3&8oX&KaMQ z2-z>o^SK1}z_3n}4L4h@$lf>9ahe>-JSNv^^50TS1?RUujutVuVUpx`t`}bz6v)y8 zVN}7sV)m$$bUjnj8)dm&z!%pmd6%Qp!sD63#^}veLo`iRb6}bBr zhzlp&PHlXuD17^ctww-8D{Dk5MFURam{mjr-Uz+?lUqG~M~_^>mi!v9>-keijdn4k zf)y`D_shVGXKu3;uW+D#ASUr1O&Kh~!RihMR9NM;Xrt8tw5nX?bvCdUS6;z1g>RwG zX&}_HSn~w91{}Sk%_=IAqi)a9JC2PhdWXNXp&6ief|!s`cYtJ1Qpop#H#;dL7E7nv zVG&`f6~{gVScG4YongFESzHcNB=q5V%^(CgW*gG>vMR7i0v=GdN$uo>pA-Kv zZ^3p4nA>pE1A00WcY^o(;N(#L7`hNA-$;1L8ND5Es9u95#`4CZ(sARCjgmS~6pZS>9no76ev`Kr; z6BdSdQX5_oY$ljDdqMfK_m95|L0N~R>P4Geg)gFC3gs7MX&MofO`RJUMl|U3@TNNz z8;jv$@P=%IT~GnOD)^o9FRf*^jVIlSTpx0uYpo|-7B^hiTt(nzSJ|_HE zRbi-}AvRH#!u}s1LWT_|IPC z_}Sk_s|>f+T8c}P(ROK-g*#z_=(fuDF~UT7kBNfKsn!3>ty1e`a$B!>%|#<79b;5aiFMz8Am2pX)wB5>Nc9j(hcuz4?%H=2{JiFn!?m^b0@ z;A=o_6m7nWf0te>9tD$~p_uc0SJ#V%&sq@VtKNUrGTl^|*)xLI7;TanVZd$)V;@5s za*dH_wtQ)#PNrW_rb$j>U!LTVLMeM##%!hRy@Jr-gCNS?@1Vz?@b)_thzS)g?ISne zFgtlAc%qG+&hjtA#f;mcbj{zf8u)RGQrRPRv{34aoo_<9DFqjx-}WtoAoFh+TqaRT)J&P34uyA2W?fJ02+a`3$NDe#<0B4WuODT1Dx zvRHWR^hJZRIO=;AxVb}tM9E^xG$_=C>=(;D=5F;c<0q&j%{+#(+&oH&bIAu2jl6-! zg8kc9rJ~ghj#idav#{aKeV(?uk<`}qWJIg|3tIa7a;3{i8Y^1!E@L7`+q`?W70=2D zl#=YdyvVEcGdVF)-WGqVh>8D$xr=4xVie&Vs#{(bMo>i=5X2r2q7G)^8kfwUg{AQH{-_f>7jLXn_9WCK@lso}f%|mTcR#&w+l9 zl)e=Q?k+?Ar3cY%Wh-W=LAQf7;?P`uphTX|hBYlu>#THcJ6W(`Em|hK-=Q;-dZb$l zjobagPPIgO!mu{@?}bDe&_gce2xTXhaPC-13qQ&>1!dnvsCKISy?8ZBVS1+-)K9L?IPh&x|t>BJs+*rbjd5*fd=s#NeOu~N4o z3zbWl)#tL)_KVqZfr*hEucZIES?+J5hLY&dJUBPe-$|FWa@&2$+n}ZAnRL%hv~L&8 z70;8_u9&#K^aYE-Zq7Qt!6dd9^aKb(?(&^HO)Beh?ga|bBivp~cGvQ+%Lzmflxv*_ ziR52nK&HG5_>~veJKU#yuFWG)E_V^SV?bI2GN>M2D}GEjZ#uixU?R&Owri9x4#v8-7deDSFOR znxj9kr!2SE@T;#;5#cYpgm~&@uRBRN=Qq_&y}v7sH{XJvvNVT3mt1s+n@Ya>Z?biW z7$L3>;ZILT2w$~O?GGa?TOE~E$5K2?l5(w>RK4R%9{F&Ss@Jbg(cmCco~vmnvlmwD(*YUjF6Aq92MPglKF+8 zW)n|V#v->V1tl{id*!u~PBa-5)|~!dYrFN)AN~$)=LFB3vy%gwOxnnw?ZuW!b;z;T zo3?SJcll&vv(M$aEi(Q0f!!v6^;x~eZiLMygX@2b^LzEW;SXM+4`8@(*mJ-lW}hoN z60x@SY}G4^+;h)XD>sjp+n|Iv69C?uIf)+Xkk>40+1)y6bvn`Bk#3E(CtBK4>3Djr ztvJKzMwmqX+Bie29+L-5C-)SPnN^=ZT2#wMDAXzpWn|Cqt&NpQe%Qo$pA`NV%6F2h5)Uh#y6;pp4G4=ax&|;yi zSe6t0FQODMUC%;Z{{CB9FXX2jel?5o2SIu38x~V!$iB=+IhZm$hp@#3*#c*)1)OJ( z{LBU}2a#OYR5Duv(03nWu=v2Dod=wr`KHAfIqmd0q1|z`Y(n)>>O{0`FKmHz8l}u* zB|}0n2jaCqL!r}csJxjLVXg82$$trCSOGV^(@V;6;x~2sqh5gwEAqW4`}asub_B9Bw;1-}FZE?y}SD2KE2Wo2+M@q%0J; z2PvoChTJiAj$$RJ-#MXV|K4MM(?mjfS!bl8&-qEQ;+XDP!@kGd@NJs`d187RK6jG9 zirzTRTD4dQA9QtWvAtv0}9W9d<@! zvhC9MEs6AnmSseixhHQ&8}CNKhF6OW$xvx6WqtSv;K6 z-$$LVw?s!~1TxbmWgQ!jgV5rw4Y68?TXhu@({_=mcGTcfqg%X*LK4I>{xdA+WPz)Bj;fn1n9v0XusaNpH%c52Zy& zo=pO!#lmi+7-o~eeUMG@H&VHX~dy{L-frpf``zQygs9<=4H=6bDCk`ZvP%rpBKC8OXJjXTiO&Wm3I=T|ASl z=UJc$hFnOXW%u)|wco7HKthRqJ9nT{GFb45hF)Se);Qy&J1dWnv3as3p~q zQ19N|Lm7ws+fa?TA|K;d^p#67GG5z)V8DK3q4)ZJ0b1sTBW41|WT9u3ItnSe#@zt= z00H0p6L`80J;7HU)%SzYwcLMf%_?OGi+X^2TjLU9BkZDT=ysr!%5~T>Y)JI2qm-&S zW>2Mrms9TL*Au4hq12hXDmCTT{H7`is>B&<=$YKM7j@;tmJ<>^w-rkgVHP#)KbCY- zOSv;L(OJI9m|ye*JRl*pekrfH?yfMUX*591k7?B$Z9n z4mC3F+%LdbI^_97`oDakLv7_*n3)K3-4A=tTa?WK20FZj!FPpgE)xS@63<~NEX!ly z!wU*tST;R)F)jxCc`==foP1pRrNs;W5MIa^^5UtVf)_GDaX)0LGPfnZ#`Qgk3w_I1 z*%`**LG5y=DWH)Jw$&^5^-!+?g@~^>!g(jkkGyj~pN`LCGJD1aYRcksJ29i}z=YV* z&Ww%r^fg~uZ1CCBLiJ_PJz0jeUL@iASc53eO`R-go#snKrs+KJYY%ZQDIU${BOu7> z)+EXWZN7rA>>pV+(vf8Zhw5M3?AuzYtnJBqg_(=t^ zW?z>iW2fS71}$6P!^wb-7{t%uz9vk@jN+F>N?dNo;WzGN@qczD&jTM6X3!XJA9ZMK zzmExqIvIHR>`Kc%OEtnIO2k`=8EBVPm}-z%4}Hht3cv4l#HuyPGu4_nW}o2Y632SsRoNd7cBu&wncX`V4dl}JJGV}Y__Pr zufyyP%P(!zu?3Bn0rG7dtQd8XJHP~WlHupO>ie@25X>4|zF};vfDoLHOE~9}5H7kE9%bkdQ0gGx>c4!m>u(|g>+ zq_^SjThP-onPv2Ddh^@%?rM?sNm@ivJWwl4wBP6K zt6FAXn1KlZ<9QJCJ6w@BKu7d(&bbz(8!YH&13DO51pEZ+eROt~H-_}WLLgj`{+x2z z9Tdc=?RL?#$|3-DuOyp{R=`F|v>PUW5m1K`CHX#R<_oL3Bvdpi;k`{zO@#*CJ-l&e4Y9_A8^NfN36`}9u z$wt=Xqxgie%n67+#grL{Z;EMb6*LoZ`Q)FHWH(RHyd==K5YSY?lNGuI=y4i@gVL~F z06k8ZkVJdlB9u8Rd$l7#rvJxp9&?xgj%<>{*j4X2+rPLqd)DF^T!JQPo)T>h6XOHHGYfJh0qfZ(ns`>QPOlVA{S7S#-#5}X5 zL+1e$GPT@&)fn!7$fkimqDQ|6d!(;3Q6YgQ$V|3Q(ayCev~w-lTATDb*FHn4`M#A_{UnOTJ5he9y?BE*X3qAl@ zC2+jTGW;SR>L59QQYQ-8!J39dbD}+wwQy6f9zr~&=62+>#A5AD>D202bE0(_Ry-!s z=~PgA1$vF7=zjtT}@Iz9cfGyXW@J0WxzIqB0Y28h4?*!5 zuy_(o&0>IJZvDxGbRcMx4|Tfn1GK};hA{A|Nn%?%)fQjIgKsS=V|v|!6_PsLScom4 zW-coao@DlA!Ap_H%R|9qeSP#v&2_}>bmMTuy=G!k8X^w3k$ zhuQcllRG14aod5A-^XW}ZG7485`x#dZ}rp^_OqcbmmJ25$S3Btdzxy^8eornX3CbD zX8TVdbrIEr`)(TTl=XTaf*g)8y{IA7F2!P&bzaayCt1Hk!X8*lV6A|L!Pq&>0&k$d%j*VVXza`OO0#27Oi z<1?14!?65q|@ovB<97#_qN@;QckKXbjsODImCXGY=NL z!@3hMHj`m}uL90nY`k)|x*9x)+#cC&$Xi@N8lpySJ$(LzqcWB5L%s=}q4S{xPON8&QE%TO%Uz6qE& z+Sn}-$L}1@aQOrUI~M7nb>7C7OwA7 zWvbn9>B?*D;YZ_g(Q`N!k-HqIZz6h6V|lcw8=e4#-mZ zOMv=%qW4rj{D530dV55sCO7(F)Ej_H>o{?EbMW(w$Rsq^6WZ9Einn8t=oI+^56_H- zivec$m*IbzvCQ>=E*B4IGiLEKR><9Y%wA_SJI>IVCqdc^fNw~3V6nTv|D+uF6N|ua zPPHx@NEfyS3GNkQ0ev+89@A#Dy@m2ed%pZ|P`HtWgHP#@kaZ$$Dj~O5!~OaSJs48z+tL)`elgAB;4Y9azb*Y%EP zTCQtmEhuqav%_Y`47-7hipif&4~#!DAI^?H*2{PLtt)0QiNcOQGHMG>NJ*TOdhe%a=cMyvcb3xbbN=j zP*~KoUQBK{&L7pvN>Jz=0TUV$NX<{Jt>sRZHF83Nu@}_ZL|h|d2e&DzRm~WxH8pJt z{G=7DyOE+v&R%t-Y3|}hJmXN4wtw3~ET&P$A3)suUILG<#l(tt<6J4>p*JeOWxb7z z50$h|1N^F8JUrL|((uRplwxV;1W~(~dRp>GR$xH(Pjg*ou(ZF~{wm${UzsiA=?rGJ zLgZ_6jr_1ewWrDtD^z>A{IJ}_>-i(Ro48Yw?JjJA6d}_=ChbLaKfS zie7N33hkEe4x;F?uA?96=o(Ws;?klO-euiAL?tCb+4uwmso54$r3RL}fiiJ*3%kw- zRTtZomEh{`9-=IE4LiV8`{0r`EIB0I4Wx;STzcZ8AnT_jjXg2C1l^zn7kM{OChl`- z$;0QEssWc~nx-tg`@B0yqidZR%EmuI*4HG>!?e(@^=_a{-0s4(^|hwD9G91A;oa`t zK^k51!nBbX`N!h&GA+DIz8grh-Sr$9ji*7`b2fE4fKKy7-1RSCjp=_SD-mGmgRz31s@HKv&Up`% zmyj6HJru#L!wO_$yp>lUhL+1&;97?T&8YC0}0Ug0gfZlWrw{sR4H{1OzsYExCJY1bX3 ziESqgTd~MgD{)y23!PEeO;lM7GY*2!7MJdiI<=#O9EWbu6=K~KpLAa?rs}nEEdKw!+lU{>=$f z{}NxrhN-jngt3l2$rpdd!WnKB{C$uR{VmXhjz8zc;}0z3j0tS#vg;uL&mD;LfhnQa z=Hx@tr4i%sNIDS;&Wof=V!ZjFcRF}m5O9I0Y*4>oV0AQlg%w!+`LkAFl^a#*z$zs% zJFprIsbcclZw1Cixmm9r8@)!p>)7Z!@?EdZ{1?B6S(}-s_B!bK4yy7YuE>`F7b@tP z9~^9XbVOt7;ij5{OF%{=?+|W?Xl5R`I!yU(z`M$ZSwfgH4^U715jo2%QGeqffc_OR zYzu;;XX945_+wEFk`{eDQ5uZA0q?lMtd;eycSHE$csG9U4OxBc4O#uQ zw2)OW_dH$;ePnpkpaH7#Ce9G3-4tp1)X>g1`*vGK=rnZF0}WcBy98wT!`uzi(=Rt3zu%b@Lquu zu_e_B=w8Mn!1Sz*t<>3mDJW$YFBPmh8vCE)vMLyQ7GEMZtAf`8(?%Ox$)T50aLTM( zpj$(fo@$1l{g#B2vs$U<>%jM3RyD)VYD>Xs&sM2Ljl(du9ED3OIaxoz zAx37aU;c^#2~u}Z&VuwQIv^1+Zgu78S7es#Q_sZ!F&iwcs{I=%D|AKL2?MU$@zJ{G2W7p+8so2<&f`@WH32f1dchO73 z?npt9{#mcykz)G!zeHr^@|^;q2Gh> zS<}#|A6e5-9yg$;p_GE`X=pZ#iphH~51fERY%XV=0$mHV2ZDFv_?&&_yVDtz9}gT|k7GRyNh>8RUy7`kGz5gc)R0jXPOv zQXhlm#(*Z+@VFxLK}0kPpy8&9UQqLz)K`SH9B?`uSUU==5Ocz&nyl%c5cWL)dYZt< z;OJX`AAb7Rgx>Gn9H0BtggD;oU?pM;M_H@QJQ~ zN~Z$g*#s)A4L@ZH2AWi3r2YW-rVX^znNa{_nE(m7)h#fDa9QMro&W^{wa7ga057zG zmORf104>uQk%Tt@_I-k;Bt#zsb4r-b1VENqi*RB}tEmpaC7iGzho7|sf#&2yn(tD8 zy@8-TM2DW71VENKh!Fh|pnj1>boe<)5NJ*{gy{KDR)^y95*>c35dhkrQBX4&8v*$q z8@i5vQ6tKX;-=3yqaYE*l(K)bkf0+0!m8#8t6bGg!wI=TRw_78`F2z@PgQg_390dJ z5;D2OO+o?~q;>O?ZAgN|6`Ay2NxC`cU_(HJa_06PyR5|ee_R4JD(94!pqztXi*`N= zgc_>=_c+2nh!hVFubqRyih5o_u+IVHi!RvE>NyZ9_4AP+&lsA9iGWM9H@tq%12rk< zLmF!V@iZH1sR}v>v}kA{_}2jYI~#NfNyK@eS_JQr!M5=cW&%dp0($od!8@d zE7+JnvQ>y3wt~>k8jBOxYvf4A(Xra4u>+UHs;XkKL~9&-L&TDewTESRAjxR#^O;V> zGVzu~tv;W*2KxeHgX4}w4Nq>y$s6r!+Y*=^C}U@6^kg1cB)J7cQ-FY_<81FozWc^ppNqNN(qJ4Ey{nv#21L}d%jJfO8< zFf9y%d0q_j*H{?1iAGd52y>csukLNtNBynZlI)&VE!d=0Ij2d(e5i>nnG;i7^RQ-Z zk%sp0$~OfrGBLWrEtwfpoF|Ms#vCs$K{)jwb<=b zvC;a0U^hQ}><+QSb6S=Rg()HnJtG0Fvq{zEBgcj9MG(ZYLMQozvXuBw2x&!2Q~j?$ z-l~r;Bouw}b+j};d@UV%g-VyUcXy<7hF}oXCK+`12^NEV8J^LOx4%9J3yI6Z{N6pUEuMCY4*~)PC3t5*sSQ4v!#}*4|bP@f8Up!Wt-X7397TSbNJd8VwB-5L^g z9m~|9iH#>=9zAIC){9V^zT7}R)~;$^X5G%gc2zBPvHFg54E}R9dp#?EuF-pTFB7Bc zD!lg)%C0k3cBI-BZpxf%osmwo*uN^&d4_%ykI%--EZy52->h$b>M7pa{2I^!O^WMZ zEN;DuQcp|yZD(I#PH497^6(2A{{<#bz_0=6U%d&yhH!z|3$L#?uqFK(9CSSUeS-wo z(=6f#8+PH6qjJnLJs> zrmwjtm*r|L<{@9#r#-nTH5f7?y zS!W})n1Z)%Fpmerd5%qno}-a2IYip(&qpY# zWB$3sXh_sIbD}(g^Rl+~^hkB7ei3h2@1OHcDRw^onC7+=!K(2PLbF1?kEzD@^NiKd zviMSyFz!ODVor=Bcj_@8k2NPV?dpIL7l4|}jEY#St|Qru%_N;i>_@|6$y8lOqZ%~l zSEJ9s(d{u1qUQXX&59rO7zXD2hTXEFSByJMt2r<6QSV-{#^(4kHRrc{)(5|m*iCTG z%lx%1ih)o9o%0HB^hME*MyP5v=Xdb9E7QrRXQL zhdiTxN>lgMMoo6h8~CoH)uQ}K)@Y{URncX&edOI2`L16L>H-9cZ6h%h)$T5p{D5!r zgRm9~oAHJK%c$CI1lr)UY-sXLRG*EW1g$qLroSwN&*tlFLYwLGls3kkSLB5{`vsKD z*bIAG7sH=L=C6@sU&IDBsq_TczR`NX5Qym)BzBeTmikwm^CK4v4$*Ug zP})JhNlk1)Cy7G4aOEPQeKg;fs=5`BVg3Rc<`+7T`Hh@lWdw-WdY8&L)Zu^2db~o_ zUZR=6#|{(h(irK^6(ivxUuj!Y$tX`FVZ9g$f5`XJn#OdCs=ZVyW3KZea=G1}f}CnY z`x|_PJ)2~-BwC~yszr}X=Oc#|PHDcxW*BF(3W;D8c!`;8s&<0Bb#C6bG6{5tJ>{i) zoR_4_*v)P$=trgBJ!caoy4mjkKappD^FKppYD%lxvxVoq&$BB-;F{-cb!oK8h4Ora z^W1HBSk@@NL7vZXp1b8?qNv)7OV)0Q@Hy(DUdFiVgEw^TvCs&Ah!jtgy zOCnsB*s#M-7DI_A5BnzBF6Z6lV78i&|&}Fms3(c6kBKB6iz7Rss24xtQ(J|K5*gGfR&j zOW4yn%aYRi_YB&2e2jz0;~9;=#=-BoIoqZ}-_y7caA@(~onup^Z)!Sf9lE@C=i1ci zo0_J14vpTs^BqbXZ9*5jggW<@ICS!sCUdDvrhD^Xhf3blL>}T2>E1llp^^7AiHEr) zx;GCO3K21AZHLuJZZ8*j?(gPz&Ik8nF*c9&$|`~Gxv{3t1-iMJ^adCwIjF1~Cld{1 zMy7o{WD(zGV#`AoAyPabuac+w;XaRStmOHJxX zyBCECPKR_Xv$mxUUYYqTdB0CD-b=LBuXXOHd4JnpEjVB<3t@&&`pT0mr0smtPoB7l zJ9x8yueMYs*`8d5P#l~wYW^zT9n`Befm7_oN7ZRE>WLA3Za^R4DarNyWm2s4%KOk*C*8cV|jbeUO3Xv`|@$TI|_IG zFCWj_dl3dm1)DRV_XOVCo3Fa}PUO9P)D;HwHMSg&V^%SLJ!^0@nn^T~*sPtnxZp=JAG=7~uaBx%9n)PYyYtmsm_aJ4}fkvvrgt~&>J@UzYOeZhko1Um`5 zJ`0aD7dgy{^OQW=V6>st*N$D-Bl|f>f=`o1)ts$$``a+(iJrD^BTqiDpJ-w>pdg$PVE$jsEibV# z!L5~O7jM7_W4L`+njP@)Xq7zMT>vifJ=f$~_F2ZR=q>VQns18iSPg%XL8pSaA-0tU zUC$e!R?Vpt7vj}+380mUAz1SrU3EV3vSyVdW#1KKZ#ZNTnKPBMDf*tgWL}C#zUL0f z56cT9_<}>ShJqohdKtzfKEwBE{61EH=kct#b)){nQ4F%QrGXa?#tT;&)RY-~yVl_6 zHgwn19Zu!PHy^{GS$QI!;QLc^@%>zU2fPG?5`Ui8e_qvp>%qq_XG z<%0bA74qa|LVE>IsA}P{q>ftfD0S@>MiM8JHsdsBwO}0?T6r0&ELOkusI4ik7M#yV zqx_HRn&Yi2)PnEw(EqpDd@wO(l;7UGv$p5H0wHn%e!8LsJqW_UJxms{NAAOMZsH3$WjJS!9BA5H>tw+oS zxA4(0|D%pJbZfO>BOmSOf3(J;{!Tvorso;mHNtqc;HPBnarU$5U(hP3p7Z!;ym1L1 zi?)y_l#c~JCy(xS9zaH>tdcgqy-ohi$1lhNe?b`FdFEbip8MLQ55G?7j z^{f;`7YslPS8Tl~or27@s4cdh)f8`SO*DI6?YZ@$B((9&1e9klwZ%ibD%Hn4=Hjbh zn#y|h#HvIq#!d5B*?e#(ZYX7P+wOT2EW)csyX)GK2e@4E0D$vA?`60m+zCVTMHf90 zMe~;AF#=Dc)UQ~Q5oKNSdV}GNm`%y4@8blb5Ae!A?bp-|2I{=x2`eOGF+O^7JAC0G@2us7X|ZRf{3s8RU{fgr1^0$;B|d-5T*X(l>9SG-e!D(&_?4Md%;I$ zjyB25TW$pR_yy->8q2Uq(41tl&3fM|uX!p}H6ek}1*YDvKs&7LcLT)|Ne-~ebKfB_`atTv01upBVj$Lr+5T5BDiGHwU7A36{zJ<)gGb-<1d0d`>k zR0Hg)#HA}LQ2#}!cS)}>K4qe!)0hDq%hNl4goZMX1l)SUUcu_$Z6qL9<^&R22;xLc zFzwF93L4irAXFvcZZzIShwWz$lJ>6nPIh~b((Qex@iGUey-XT@eQ%XQUe=G#tj#3W z1U3E+H^DE3E3%r6f2wYL;w)C&YWssx_GqX4InwqlZVvG^QV;<_O<$*sTLA4x4n(Kz zqpX@5Ah35U4m-E~0ouG7OMh|c$~pp|L0eMru3rjr2+tz~cc9^nI>2rs{BM&FgGB~L z`eh&oUXBRd0swb9K<=Rr0wZ`j14m|42j*es9z`Hljms(mWklu~Upi1#CF$L0)If>% zg^r>QS$Zpk>u`KH%H$iiGU00;c^ES$lUUgS_h}-XX@hsR5f(~IqCFlYUmXzHlW;|@ zrhMI|<*O`smcac4z+P|w-Xj7>c;hL z6*a=3Jf&}QlojnHzkGPbscKx-Lp=!I4d_20hBWLA+hQUDk0j<4bq2NfQSvjpbo2ym zHial{N!ch6KLM8@kDkur-<8^#dl*IV!|-WzN>X}Y_^?}TG`1MbsiTtK_WA9e-gX;S zZJ2$`VR*Usorc;2zqS%p9=TNtc`#N_T#;Byr+mD<0}Fk!IOkOVJy7r|sd$3j?s?tq zh`$t27B41lgaw34!+J@296_0;w<{DSY zKZ(RiIa^oX37GdeaPKw#f@z2GdveBh4iqMMJiIscO)CKIAA~|NfEkn7tZy+!F327_?mL;Zt1fX{uU|$f}0M^v}(A#6Hi;}jy3IDMF!JVdBfJ@UR zl1jcr{U5gN_<~<%3nR%Rv|pp4jGKY-cB1X^ku9`v*(TCRBZf?U%Bna9Ws$m6jiFRI zr6CHW-RN}}k|1$;Ff=4uakem23UPc^5)SDXp7J)97GTKuv9u>~r9lb*HM^&;}HX1h!p=atP=`*YD^Yoc2YQn$i zKC{k1R2nRNM}DHy+(t`S|BBvejHhDPzqY*7IFYjXuDsdjPUAhg74)yIZWV0a{xl)e z($&zjb^f0~^t&W{8;DNy8S&-MWJynA zUpfY$Zgjxq>q~CevU}Ad{$5oGU7kKQ2K+Go4b)!~{Ya-zA+!!J5_(lanXmgrr688E zVp=Lyq2y zTnAhpax)1y%o>#W=Taf-f;Qv^Kwa*KEUZS4|4XTmbwL~QBY^s30A#pWue zC7_TYSBrrI!N#=*I{0W@nm^Xy`=oF1EhHuG*qb5{c1Mi86vd6k^LumbeMok4^pz_2>~p!<U8?Nq%6ngS`8@Q?!b_q&JPRFvmpUBU$U zu*2eY0ArPnhXCji2bh*d^H(8Q>B`}!EKb>g9J2ZwjeGXtOzjbo!(+bZ$RTEGh;yYH zwJ$zvcHxuA@%i7=^S9K4IR64y?!0*#O#P9ZaAA`K@H}3(ejSOGx(7fl3 zjm8%k>-(7Z3-9;*zQg-a% zEEs$Ie+c70E)C}*VHClb5BeBYh@j;d_tOAziUUTp0ZmZ56W`(Q#Cf6R>BlFcqQ-9k z{#By5l0<3?P_yy-{=SwMx{SLlVr&0lXc7$$NgBYh(U>rZ4YpAl>_0zn8thIZ$qCbU z?e^kpUPezssE)y8r0X>%;eGYHKpgqL4ESy&&Rf`M_vuC}8J5*@e*vHm9bi9@mLn|l z4qwX^C5_Ejg{scLr3uSvzFYjwR~TfTHXIF7jmv@KTH<-kZ9|K7%Dmg(j76|iQFmHp z?e_eq16oxQ-$vs#%xnjl*GPM|KJ01Fw`i~5VlV=QL71<3+jD$FVpXyp`=7vc%-5KS zADXHSSL8W1<}11}O9N!Jg^7ZP^S~u#gp-ZEeq^etxO6?F z?Io%^KuEZu6cQGeN5S2|{-~dVzmf@-pZ;pU^c7%39??_atZUIDg06XVyYM0CrKQIlH*VF{2dp&RxHQhae~P}>pI0McY$ww`xV%eE2w&d<|zuXmq zolo%oH06DM&z>!nOgp zZ}j8$3ftL!VarQF9!Yx-!eIOh;J+Y>{XCL(o?p`P0?sF92SAWCK%^D|3rr+fhl9*x zM9gk`GF!|}wZ)9Dd6`<>5>Kzdq$}TWdOr|7N@VjWWQS@Y3lGE+vCjcvizjR-M~R3L zhuC;VkAMmK6T+e|Q@lO~W)qE}xtF8MPV|dcp6cfjt|j29u>`;m zCxV1WxK8#9S01qWMC%3zoT?;_jm80Dt+k41ef5)U(OPYb7GLv3f;!BF!F)u#?LRY?>ZjTLaW4KnLQ zs8&9kEmSLPq2g;^p&FlTMJ^@+GFVrZPiHkfjnD_K$fcC58?|JW2FMbxdjaKP2hv6n zF9I?fd<|TfsIp~j^fRWKj!W0WBV$5BV<{w*DQNcs`{RBJyu;%Pzn~RhLmp}C0p1!r zq4jsg75T15+LC^0D*!s5$Srq3Vwo-QAcQ0$cZi7GYd^~txtJ|-e9bFz!=(ZJQUs20%)@-GU67=*h!Q9$ zQ``olQL1q1+UMxCxL@4zGa-+(twIYKrvv`E#PM5?v>oY}w)~Lu3EOWSh^mrkHW~*a z-G7idSA=ceFS3Pgi7jk=%_nU2sWuFo%bN15w*!8Tre9BOG(Szo`q6RJO2f^&EOVE?oRSJ28Ys_dnM->S$bp>Xa zJHBA5!MGy(pgz%kS*db>SpwGpNGS)}OhOyL>X_BuHV;AEa;z@+E&x5|023*aH7gq8 z*is=P|M^#lf8o+~B|zJoI@Ko?#puf;AJ?KKjYhz4CXQNGUT57URx%9<`S3n5DT+m$ z`DjKKhG&&^H=Y9MpE=-lya%E2_O;5KSQ2!J|4T`Lmh|ZUFWOYAN>*i~agxM+b`n86 z;5QJ&`S@UZ^jv254>e|hB8VPe^N3#c#5OJ%tV4hl$}N+LFAH3DaX!#pL6irBQPDU) zAb6#Mvc&FHzpdk^@kispU%%)J3_656$asEDFi zK&l}2SfYX`RRvoJ5Tp}2ii)C$T@eefpnwHM>>V`;7lasvNVOqVv0%gciUohqGjsOj z+?yL-^!vX5&p)4>y|cSBv$M0ad-lxP-GgmnpU{K(I29Me3neUnPPK!u#k6W)Ip1EQ zzlcIhSn#bsA@H}2vB;3UrK1jBF8)l5g4Z2YQU-R%C**nD6>3Ie+Dqdf zhI;8F?upsU*5W;`XraQ|wDYCQ4|WCbs<)o_KE8i}D{}%YHERmZ^`Eo!H9HmjW(GWO zC(nkgOe8C8qq{9AmfLx<75sh+cm~SSwRNud9+}j8caMJ+y|^6hBE3F{czL)o?_m`V z8piuwouvM|du3=pwr^GVYgJzW*98IR=VV7?u&(>}wWB@RI73I(U?kR#?4PhB*5Xn5 z_8{8P4tC(S>jE>vstkW~c%+&$C1Qq5sdYOaWmg7tH443n1<#=YJ!J;;_ZazR^A@<-$gefCZRdd>dgs9+eYefHfnZ z(MAs05ZFk!^Z7F27h-01(@{rZObv)1ZWVr{(LZ)zSH?0QrN2xGN@*;s%~<|6FW+8% z1*7Q4j@d_QFXR>Z;%`aD)x4hMf>B<;EZJs~2ATbn4QJOCufleYgXHoGN-JWCn<@D@ zy1*k)q8IrFl?d)at^Y$Yx_q~y37iczSdUGRxU7gAnnDGV7x{Mg_0;O)oKDs;^83FbdgZb4U9emm2;EtS#vgWH(&QnrK8!gepBJa1( zzpf}nqd9xe+JyDrxQypXZ*VYJ_xH4N4DFmOVc@;Msvp^D(}fIt!{3y%vXQ?Cwyy<} znvrk(dy*y_y1a(Zzp`B8aO$MuCY^x{ua41I+e`T%N4VHi5`Lt?aUOGC%mftBa+ z!AyK2O1_z;?}>)YI*_gTH~F~xKbWiz7&TGDraX~FyDgC!bbJdIeB&~Sc45%9lMH$n zm94^%_kgrA5hYD#X-8EUa%tE=cELB*fZqY1l>sj^;J0?K_F==_15JGZE>nOR?mJu$ z{LXI5)v7gbM`H`HscGQopnDAj%i(0JHSPQ^AH-l8)yf~Hls9dL$ z)u(8%<339<*rJfZDhz`qeprNdYOwwL@5*9ZeF{$iuFPX;vAJfke~*!GvJ=4NhJaHi zHCZx>JhS^UnawT%myH3ZUD#}mWSiYZeXB6q#ZbH)y*1(;hbyyth0(s4WE{I-oNBe# zg3*+KnOW^iyQzS%*_MIF8uHP037PGyyDM3GyFCbobQmtHtl8~?-ITAg;SMIJ3JJY9 z75b>uIGh^~X3}sE{2Yc$z>|}h9CLdGeV8SbM(|Y*&(X9*vR);JsvP z@1sdAUYWj%cD@qKq&`H+?9Vp#;!NnMM~muVIG*$ki`B9sho6s4J-ejapl$=Y@k?xN zl8!0M?bc_I$D?p&qaZ!YBW48CK))7q>$R`%E-WASbIP|5AJU?it6RyUq( zOXBWz1a@m4$}Xpd7#-uMrsJbrnttKS=v@fn)+))~VFrF`?&Z4EA?tR1ipHUBnKwgy zJfscq(z^ijn@%`_uKx9ahZ}qd;Ryy`-cGZxx1S7B-(xu=ZUj8XVCBk_&%jB!^87TE zu=zL~=d#4o|D6MPrKKyp-eBePrNMVk1J9og|K@x3484i?iO0TF)|$PS!AwU9@30Ps zAqnv62LQLR?72p**=JZb4}Xhyv+Ofx=^=b8AJm_oZ)#DBFv<`Vo@j*3j^6?pNBS)R z1&ZR?a`@qiXmK9$hI&se1=e#0KR~$1;6~?Zc2oPA`;L~l<1>H`eT)Del6V|koCml8=4oAVo(Kk*r)$TT z8a$6MzorLHL%#!y4F+FMc$>jF{WSY*`+4C9A-3*m8o(HQB9!!P?Hea}8xsZRaC zwa~Je1S}>2VQWVa-fTI8&^P1v8sXv|fFH7S;sEuY10cssmOZo~;Do`vgTy(uLVW(j zKeY6b#MufMr4b)L;|>;LF$XbaSp6GCP~<%fz7kmRh6cAiOm(11t&jw2!Vu>;ff9%20s@8J1FvHFz3PCmFoBCE(i)-hp(GWjEC_6zg&1pjg5v_AMg6 zZpiPo0bFX?Ob4N6e`MKI359|Z(*NLKdy=JJ$~Nn0>6_S==Ucks^s)4-Il2t9^d~Xy#ILaQ5q$yQVClp` zk&iT0DQ>wIIkOF`RU1uW4iK6gqOtr*Ku8$EETdr#5N^FMgSF)kYIx=J3?}+NbM^q$ zzpcQw$3l0}@04ctiK^O1Peal^mh|TFfDZ-?CQQfAkkgUW){;2t#7_Z?qI8JmX_+W! zX)bVj89ciq-~t~X=|EMB8*kaaG5dOhTXzF|r=c<(rSUczf7p_~?y1z8^fHpbiLZj< ziwthkL&fJ$e63Ls29}OxUmyvhjl2-qKN;NUQo!DJtsT>m-MJnZ?ujJ8r!@e4pus$K zF3w}Y9P z0xZJ7;)y57;ZK~0Yyjrg>^Qe+1B)=Qc*X;A_!H-`0f4!2HqK47z#n|EN*H;jQiN)T>JqnCdIkS2{3nt#JO1rID|pLJxR#nPn>&F z0CQn~oSQp; z`93(#m-)aV3@k1hKn{Q6e8mlz@0#L#i3lvhz~W0wXx4u9fwWdP=TtGfN=IEGL{0;ujY*%-3K2A;rC5=a`yAagtt8~ z;W4hj<&W-K#@V^Y;Bz1aIOur@-TOE`df4a=eLD4NnVZ|z-VJ&+&&C7buCZHHVDH%p zPp+@<71Oc$VLzVP+xlpH5uOM3VCw-E?&5PE`~*wEWd$QU49sndkDv}5mg{wEh(q!4 zH~!VXNt{1E*SiG7fBp5>L=`hsFqo$ltlwzmxxxK&@$DIYcr~wJeBtn6SiIhY& zJE5bF!o+MMyb*86WMqY&z3q(I(_6xozX3VD^G@}SgU!lJ#*W&7S7kzmz4jNcSZVWt z(d8sKS5AuT5oAx+?4N&*ddwbR*-_2T_$BH+jO-H0-B^(wZ5}C-o=9NULOE-LWkpeR z^ugwlocbzOGPpU@g?9@B!dBc2xyV)IX;B* zT9GtFoH<3(9?GRuNAPQw$y%QQRCijMPdav^EbP~ED{u!3=U`9t`%BAk5I;s5mA``pu7 z)1P)so_E>rI06S(LMTf@zC-AhOSLArbs;&*U^Xt3usvkQSu?mv;Pc&bC~HzC=MO?v zw*g00#Yy}MiDOkwc7f2vu|oDbBorBeVFNjQBv~nW=VGB{_Au#K05d3)J7E>*dmL8(kCogiFE`g$a@#y_85n+uD|iJl25`3$+q zl>Dz@s9HX724_OGx&riwybc{a>hXxY&Ib5;AE(;&_3k37cZ;l&9GvEseWmp0pj|E< zFA{GwSVtL54CJVR!5z7m-s5?9F_*!-AomYnZ{xjM1^a!e+Fg4$wEHSB-mQd{@72ny zL#*XO7S!(OR;}~SFTls0afng=sDgj4>(BVql!i0a=WU{?lq z@(b8C9)WkEY{V6^qM_$yuG1%92zP5I!LEJ?$6zm9Cft2y$n)-psv9HqxFDxF)JtC6#i9twFGS3)RHLXJYnE?EP^LYcHJgx)qFNe2h|$X%CwoSm&) z%Fht*d*nwM{DI5rIfV5rlZ(6H_HYUAD{AcQ?P}}7STb*MO4`q=^|KHncYLK zYbV}n!H|!~P2IXFVu!10X{kfpsuD{RcFO>b6eAAdxc z>i2OMNw%@YBf;(}uv6X9agxfUSeWWFaS>%wtR@VZdW`~;_mm0lgQ3B_Bw|*pdbbEQ z)Yr={iM03%AAQl=Vs;`zuk>nJC7<98YWAW?Av~$-&QmWEIhV<|EF>O*D%l&TG}C-$ zUdDPLnC3HkYXqbC{5dfGMnG?m%gm)04BlvJb(Jg3`pHnO-yPWhk4N+H2%}>P%ziew zKZPqHlq4ZXBBa@0HIQci&}h_XEt1si`SQe>{S6cj4}9uxBSx8w9RuXLQNQ|phZ0o( zV;}_8X9U%sd^~0j2Lk;_TvmO~95A$!2WnjLfb?IVG>rA8ql{U&5<(Fm{STD>TfiwJ zBYmoBa0L7w@rYPNjmAZMt7Q(@tr`b*CuW4%C3mn8yYAr*<|4|jd&Flq;&QONHHlsF zP!eMINO&l5k!0z1UIBJbgI!>$Hf}eZuC$Q<(FwrFsuq?%xj-8ebb>T4T%e0+ffh-= zQJ2ZwE5W-Nc&C(}y|*jFM~O4CG2KUx=J5*8_T2JentA* z96i6obdbK zA6O$@u(mCGE)&;CP0kN}_+h3$rj1uFS5Ni4TGde)gF|syVf5m&Q;hMlddrJ6{WAF+ zuOOl)0>$Kwzv<7Ls$YZju}D{iaA$ScjIi_&GRiOcHTsm{U^1+8F>%mB z=U)$5CX>TR9GJ$B2CFTE?qxf)Xqpt}20J1|VFdTG9eOW=-DjL5-gNMYGI$u5nW9d| z%H><+zRX+(i6HkDo5a+f3<0(yHR!hFJ|16R3E$p^*zK)M77_i(Of}h79^cpW?tuCF zo=C}OOf9g`tJ{%xinUU=V^$BABSrH2M5kMcTrLNmDc%x*Q3fk; znIt-=E0@=i`w4Ryd>iEUHf4QC<_Bc?$_m{K2WNzChC?)ho8c8FpvujFehMxthBw2I zQ);=8+yC&kczSDiSDrx`FXKuGg@IrnV@*h83E&mDOfWub;x0`z&P)CakC$V;!?L4Z zb6g2wS!kT|ra*sJAte}(Y)D|YOP&8c5`I8;5$%lrgG#wf9ToBJ1h*)I$8lLzaxh3* zf5|*H+O<&o+0Q6hRs%)NoaO0g#dr6?X!X``MV)^mgltiBw}c!Q7Scsj$aGJ$7?5uQ z@2=n-h+(DUCP|r~qDP2vFEDhhyiMSS5c;tOvTP8R;{Gb7%M6rRB{TqbI6#hmC^ zn-je+A9u$ejsb%5r{R{goCjN!^M{Nn=)spvv6HGjg0zqH))vdDc4(cb z_Yf$VFxZQoq6zT>Euqkhbx_^|{e+PPqrF&1P1=d1b@KStp7&MVsFIKHVi#!EL5;-w zSY88XKG1@qTeVh*0DU3Q-}KQ3da<5b+DM=;mUj+~dcC2Dn$Sn=!fHkG6B4E(t4O{8 zlmN6yTI^kclzn$XdLf>WP6!w4#L_XynjA?;J?;Ym2?+_|SYgSLlSR^EbPN2m?H(iifl97;KkdP31=oSm-mqF&)V0(#{*%je>aTyzAAmZPn z0>R&6d9`2E>yGL|EmT{PtVKe`L9FgOfa(GAyp3`vP`UydvqzxgVXEU6K=@c1#seJ> zG#zi0IUs@m**nB1fu0Lh&)q>%qI!M~D6xqscyzUd>e+OBGY~eZjvYd>j!RU>E-yvL zC92~fGg-$as$-X#qT>?Pagdp;;}X?zkeRIG&B9ukexFv2q2Bx}R{c_WAHd>b!>XMN zWtff=z%w1MB}Iww;bs~zw-a+XTH*;Le4|)=q|u(~ZIM?C&^3Xk_L);{mNMLe#QNyE zU=64xJF#K50A>vcf(}&{V4f9V_O8G@4VW15*sZ<-Wq>V^^DQ}d8$ zRTxh`Fs5ozZ#%H|B33S{@C_2~L|#HR1A=r2mmFhyK6FSiGhk?Z znkXNoy@T04&pXHylfPW>i7W#Bhq%HNym92*#N}SzC&B_g$Mq8~IHfgQh7k8LnTC5h zhri(V(3pNq_dq|;t3H1qZZDj74+G5fEZz%1dg!(>9W$435QR?43`b(91es z?uF3M#D4cc6E^~-Vq&C|HEmD&Y60nl;p0-je)v`Y1{R*Od>O6SJd^bOPdue_rwlNsPGEx}ur&nNrJ4;Pf4N|^yM zZVihe+U!ciW+&h``zc{Bwu@-9TV$2JDhoU^Ka%&;VKIV-3*Xyt=1gGxL5!;W6@x?m z3LjRD4aAP}lI@JB5o(2_@bHCw2IuUz)TtaV@HM;WxlW!iWP529;i5S|gT&QQ-?xs15O z^G+isgL4B+ju7lVB`B#(-e!5XBJ)UUk|K+~0lFGVDQ|#Ad-2cQYrvOhfvhNrtnmkU z(%|c&fqO#=Y5@f6GW?h0rMOc?-vfc4aiM!_z8Mq5=(osVJ^Og^ynW4|I4d9NC++9k z50}?!kENP?f7j_u^n0uitNr8>2VX?W>x-{?)j3lYTP}y39Pzq>U6jGaxJ+-Kn%dv( zgVLUKrExv5mPWUWD~*Y+G=r@);#8Ez7)HPr7vTgLmd4=iptO3lsb%soU%!8d)Ng|v z`b3hPH6;3Pct_MLle>S^>x3&IEEN*GJq;3Egp~fcOcQ*OjV?qq>q7ruyp|s8Z9;jU zL9Qylh$xQGr`z6ZBl>J5x|Cxb_nK}TFx$L`h(e`68< zLTYeBw8H3>3A%aks)nh-3r34*VZlr68V`ZpJ7A}ilnW<0o2lO0U^*ELc5{>2q}WP| zOPYBev9jLcyr4|#W@8+x-_ZI=KQDTiFqfTX6eHJP>joG^3P1spLmmdRjv&$EiD4#N zy|=+uJYiS-o22MR3Kp+9k6H1ZA3@G!{@}H-U+Q;I1$AwC`O=|XC4DaUt-X=z(l7QgH17n zO)(f0bw>D2; zz($qy5tw|EKA0L{K6y;p>AD$hrU+vVs^bV39ie>kNlF2gpEZ z(|_#LOq&y`=jAj%LVd+f_aj@e{3e#Ghg+?XlQ8rLk@?^mJ0p{9SI7-$k=U7vwn|>c zy)f26b0AIuKAJ1%9+Xy`Zce}yXH`AFbahtM_ao}8+Se~oomCC|2sISj&qr(57?J%m zeNWr@d*Qri6dq-8FQrKGw6Xd&W>n>A)447t4$q(#dmRk=Tk$ldx||6>wOf_H@Xwej zM{D6CT8o`A)4SHfMYI-w`8isPzhZtZ+U_0kjz!^71{YHb)NnQkcaW-WUFJu$ z!UKE)pruz;=+%D3w9ttf;rOoKiO(A~@CtYL9OAwqG~G(QftJ z@EiN7Mo2F%LT^Fr=Nf#p_Z#Bp8l>s6gYEEZ4LV(Ic}wiK8uz0dTV^yMe@K5Xwo_A9 znS6gKdk#rAEr3h|4y{rq?rHLG3D4^*UVjL55z3VPLbO6AL`b7DWxpJ)kSY7sXmgYk zk}3NQO);6Wi|h?DWa_m)R$<*lnWz;$iLX(+%eh}WU6~yCIyws-w-wHH*VY{8;gnbu za!M?s<#K}9&#S(K@}P+G-T((_0uGgy1dv0%mpz^hK)8<&gQ|KllyyF-0voGnH*M<% ze!FQm*-u+%m3nz%4Qo7tqIIPej_>ti2U-)9Nv#7=+i_%?Qrj2&A!g!jo_90yqYUQY zvSE>r5tI*$5B|_c@-i7*f?9n>R(utteIUiVU@v_9HgpjkMEqAl%jCsdF*E%cOllsM zrl^%#)N)}dHcM?eX4t?HV+y#Rq(kZpT|EgP7f*vn_bce1KW512ynY2k3Pxj%CD*0u zo8F`Iygn#sIIe`S*o6Fr5L4t;SQQz$6X~zuIwJ49R(cWufY$nyV_I-)B=cKg=VNq} z|28ncHDeRM!(Df|khO~1cJ!FSAq5>ecKLf^Le37|TjzB@I38B~txnIM_5u!S!El`~T;NOI+@qV~R^D(jaOSmaA)fW0$rMU2xO*^0< zzn>Rh&04LM`06aJ5NgCyP5G_zdazD;Tx8mz)1a2Qf4_3AS-Fm=UG7+|+v$Z`yE;ci zy~A)NOUhJ}G$E8aA;S?;xP(js#EO1@1S)zm5YG>jTg}dPHN&nZRtuG3A-*N$orMb9 zB3#y`igwbiN2jUSDzG;n99|8Vl@J!6kPZkT(6}ZLXa(S}91@?BO(3_B+*Cb4e48P= zv>I-e+cAwfr#C2FdEKFV8<;w5#_M(@c;ZrxQi6m~sDylk5Mn+L-+yEr5N;16nV4Tg zS85U1JMD`|R_XKxFH7bAR)v6S&{U<9@fR?SHcgU|5+&qXgq(SM{owN>MwuL)z-ZGF zpx(vYiYDwV=#+&-B6ACMoET;DGLbKF$gDl9L*!$8WZec$j4~NB7DM73fI5@b2}H$m zqrK5{GV`MSki9~#JPMKC8Ufw*JQPyB8R+`BcdhPDYY!gv7m%4b5e%jjqY4W8dG%l8 zcb(iOr@f7fLD7vAe2~!` zN)KKgmLO*~bLgYvb403FbTAWruZItFxUN&#M~AH*OTlfOlT5j#+!p5_i>FLhIrUMf zMJY&+JgU;85Si!$If3BO&?3CyWv8t8We(~!@-49I1a{hhhVDNK`4~r+0b@qEz^Z+t z-NU|77tx!RMNr^IvMX{0nqSo~(07M6cpDfS!x)_N!FTy`hL3R@G2+erhNYaYVGU+| ztM?`xJgw?hf0C5R1Mh(P7*ey56{|jCa7A4LS4ek&Q3iu>;f7%=n5}Og51$Czq|nQ_ z(idg=GFAtum3_D77tpyl=kImug_ceS5jBzex^z0Lp1E8uUk;XEXzBivxf+G4g9fYAa&}6Rd0&_aIvo&f7H!!Y*P=M94RZ8&7D(q2?DqM{$ZN+pR z)WIPSst;T5W*t*J@Oxt)M&veddlZ*h4wv=$jTg%pakhHkS)SD^)dP>)!atjD#(YSu)OpNO}(k=7l%w!t@FJ0t?>#SS3+0@-gP5H)$=cmcs-FEWiT3- zX)0rz8<;ct7T}~Ir>XPGv89#h4DJfhdjTB^`Qt5UYCci}O}$AKE|YmP&>>!9MnJ-C zITalO6(gNh?0?{BRFtz8U6pHR>u5UFNDZvQDEx4*(bzGEEblpBcr0q)Ots*|2?>rS6e zWFYid32uJ@LGQ3!Ozhqa>MNx(d7cQ3+Jz8!-{PCW?`gy3@)o*w8{|hBbjD?Mc3qow z9>K27{*3}lS?BA4=GVDbQk{DRuNnrU&K3hv!&MOCIS^@`L+=`RgCi zcUNEA5Opp?fC3L+4Sth>rsC7K)bY(J0@3NY3BA2o#B}GmdH5Lh-26ojZ-Ya!b#lVx z&rh8}>ipD2eCs3{#5im%nEX2|hCkJtLXrGAyuv`<=RF`F+uoO1J0PdkVVOg37?Nae z(LiOsh8(U2hvYYENivgf(4#C8mic|@`@VRM=H+-ju)a%X9(57qX^KUye4Geg9#lN{ zeBHQEBsONK(9pyp@hB_5%k{>aq2>!kg4Sgv_;6y8aF12INPw+Q{^r_9gs&PFrG z#$>iUo@>UYA#onK7Re2OiWIsW&?kTv3)d(+lI$;Vd_g`Eh@OxhfD&>h!o|X!Q32ZH zSRnZw60pWi!?g(4i_6|1e;~dWd<2Vy+df@-_9Sp`gN2euBdbW702-{&zJRU)RKcPG z+92m5cbYPzPp=PZhnDcNFMtepBcVv>#VY~ALg5k~BE*1@we#-dC?OZ@tQVKMRZ0I|=y&3HVziT$nTnJnActB58{FDixE zA{)2E>MYVB*t)j5hrw`BFc(6M1ZiE8UW9_V=%Gj&BWw(mQklfE{SzQEq&?8wUy1Og z&aKGrg4U0X%`B1~NN5MLBKaE7XoWTcx*t$NRskxKB?v!4e2c!*dDa&@7R_vU0`NXT1=W45K?dW2hKzt5tILG8&Wk*{H932BZ*Q!I&9vCRX; zE>@c7{S45A@m+FO1wNNwvbh(60`J?rShdVQR9rTBLYv&0n+Vl`khOEL=38^SRBLY6 ztV@9#Zq|l}Q%h`CYf|R>O}Z4!tVvrbk~OJAuqN$*9M`1fAWhSx7XzhKHn7~}Cfy>f zH%C2Y(ClK>GxtA{1?D1gIN4hMW|vtHZQ$R65bt063s zUx6?SxLAIP?&wg_#S?P1_Sz8|_CsjhJ3yHWl*--PWn1?yQO@k%EBew^iPyCL8-1zztV&mdajX?9BHo_yU+&1)mR9&>>g_-wsvqT#%-z z;Cij#r7SnKg6rhhNuD?2tl)+=YYFebay~z(Zul^{1H_%w>b1i zwzFu5U=4mXa-2oa0cn~Bp9+*xd5&eLHaJMRB%ez&7BinWtSfQDYI9K3djw<{9{@#~ z5!x5faSGK0)L)^$K)4pr7U6p3tzfYq7{;1po_;DVgbR>s9HrwV?-JhAey3@cdGKk} zVd`Ci2-b0?g5K%ar6zZr>c9=_@<5amzDqFQ*W=eJ4E5Mrk?bzv5KNC9DJIO8H~!in zO{2$4fKnl zY}89Ak~N+~u*Un)uNfQfERd#Yysv>$Dm__lYU6Pu;=|A6=)Rv=i_H0FuvwNPu}Bso z{0o#t@&ce{&8b_SwBXRQAH)o)MFt5D)^Klxi-p_O1C;ZDvJnZG=xMkF;d*h|8{})m zbCferL2>EX=i&slawPOXR*@tCO;hMaKolg*Vja>xRTerKLtWsL@Ujm<$rW?&c6bl# zPsV^yQ(LO;8I;SPjQOeIp==@2v>htVy_X?}=8(11=H5NP-G;Ku0NGGR933)y$#4{8 ztvDIkZYXmI)`|}x$92IEK)NT{C(pf~1xl%Wfy|Uv1SIPvOHbkz? zFPfkZjZlY_HoO=J)`mo|+tHdcE85VdhTD*7>2F8PkrVEP!&NxWJthINHYASU6Wd^3 zU~PC0vRxZG1Z%^mkmK609Z1vk#Dzd9l`~j&a!&-I{%w|DukpOk<(igg!)x{Nd^Ko3 zm%E!GVUi{+Z)H0s@o9lpt-RQE`Urj=7EW+qCBHY&+KXMUi5ro)O1d8m(&tU z0v?5y*CO%8%=0?1Ue6=($u?}AxqwOlZ5ASIkYdE&M|_iTpO7Q0zO+aV!2Qi=;($oA zN%y`6IL{SHaczgg&8fuUU$L%-8UrEDq7XM_{@IbOxEG2lk{SpjM^lvF4vBhg0c{qd zZII)TGY;`$`E5YdBl!TNV};QlT89P$u?HU638@WelkkHfMP zuYZwlgZoko<<>fBt8#9ZYqZ=BD0g2ZZIxbtiiJD89z^NWS$awI5*40Xwh}T|!}}3# z(nnN>mUCz2M^x&8jqwmzy-HTa(Jf$QjnNb3S!0X^v{{H?jX_DQF&v?545MLVkj5G# z!0{WyaM&2c;oo9?$~)A5ipyRU#i0wEd1vLVn~Giq`8XQIZp|EVHszzD9|U!gD*8o0 zi&WVTvHC5TIYG^aLxmd-H#G)0zS7@OK2-WACQq@P(g-^uQRWI%IW{Bns|#4>OGx}( zaUTbCb~~0n1JDeGZU=M_JcUJaEub3|8VzWRLYDzL9mlC`7P8zbi4YSsufOPSHn0fG&oM6e;W9%}AxgDy2ZgfcDt z5IPh&;UV-z6_Z0~DIgm{iQ^BUV^zhRiGGA^H-tI_8$y3TjvGS9fHchzTJ1Ovp_5s5 z+99-9c>Ka6%%6m&y)g5N&aB{#NGy^G2IFHCF)fS zQ07Wyx5H*(LDrlnfOm;DXF%dSiWeixn)3>U*qrwRGIGa+p2Sk5+MRGAYLTWHw*tYR z%>cogal^S4%}6q)hMSRT>6`H_9& z>8CU<+om*y%9&FdmmW-Mu1A(lX&zRHQyL1gUL2}TXeV2 zACRVL$eutcl_o4ZT|?SPTZm_mfo#WfGY{(SM%uxkw2^kALiC?J49G@XM-q&-M6W#B z259L=TbFI!vrM_G|HKz47;V2)5$Hc@bRxxK_dFU9ye2+s1!SXb59C@moed0a-DHRV z1Rz-J62V$`a<_`sb*bUjWm@{yeH=OA*1cWDWb4iWWUWgazjZ%FLDsr2BHOjDL$KCe zh8)+r&w?~f>uv-}sVrpK=~@?am^m=-50T#zgJ7(4=K3D2;0H)d$U7PqBP;%`5CfZYdQpL&7H_`t$7+q)3jzhhppM2WvA_mTXZK(A@Z@c*^Bkb^e}Z!NNYSs zCFD>I>mw|c+knD@f|3q!&zSng#Oh3mMaF7QO=;;K0Z72Q#QJ9T)9E-L0Se3GU`|v_6XEP`SW>n2%z=i5bQDNdM%UB zq%Pe(1{ob6gGjeXpGgB8|1rpL_!vYS`?II9d!E2kDF&I?)Xc)3Y&Jd`&jynJXbcF? zqd@Q+&~1BZXb!ktF5A;bU*aaU&D3#qAVkXR(6 z5Jpa+DE$E)4rsFwZG+53{6fU-(U|0SlKi(Cb$XGI&Gj#2haZg{QgTz-qj8Xz+Ekn6 zaV_s9lve_VTV)BLV&PFzuc6e1ELCEKC^aEG4J9F$YS;~-JrX;#oOxA05-$R)O=N|g zPg(&s!cLTDjj;cztT_>^5h#f@f+KW|U^Hw5(%2($fa5oU;jj^i!#{f@Zs%TXR*$7V z5`PZ)_61_>p3LjfKBnN)L2dnkko56{wprweH;< zMsD(1lAV&1+v&Xnl>EFL# zWZsbcQP^g6Zx$B(2^Cp6&;P0-C>^EKhD(}E>5Po^xPaq$d)h0Ig_3YnPiO#q4&(@`FHNX}FCpwa)^UP7oa98uUP9Pu_-78ipHNtP7M92?zm(Y(4!tu`fI0NW z1KKP^Gl$+x#BJ0fc~7Lnp|>2eTBllv?C_}NkkXA>L7v}{YM#MD*^v$bgul&FsO8e3 zcLI{a4!wOJ%kgex=_S$TDtvl}o$)1fgOpGH*v&>1HEW0wtJ@1S42Wi(){I~K98#Obws@K|d>X6eJbdZ2Edm%qJ1&0pZ@{5YqLgAg}TqAVi(-}M07oNhA0Wn-XWoc*vEk}cm5=WqMgg*(al<>!ye&kbTHFF{V->H7@dzZp^>QPQk|=DZicy< zJzZgL#kpH#bVhV)PaEknrp9z?R_;>*yNhB=H=Ch#b@MT=##FC5=ygJtQ3I1R@UYpL zF~Pd@erI9*Kgy0W_ym`&(GDGzf1*1oKj&l2+RlQ_RLt7kG{~Q<9p&$E*BMXq@OJm+ zHMn8j_V?rP&(@m94{qqW1;^2h%euClUn=JkaBK-Ja3^i; z6Y*%)Q3iSrX42UjT={G@37-(cRCl%pVHDJ9JeI+4XQf799?7j$q0{SiJ84OHJLy8V zo%H+-5HQ}~7clc?cNSj#Lh=CPnj3*|J`gIVwki8fKd;!VR$pOO(e|5u;Y&AT6I6az zxRf>W$w8i1|GZ-hJnUIoe+b|UcnCc=vGEgM^)J=(v7hFG3$TUh1<-z=^w=WL9Ni+% zLk~YNr2-ZHbcwPl@>b}SufvCI*mjkk*_5(G0Nmc zA}?{sEXX0xBJwiXrzceQ8Q^TZ&{S#oc`j8MzcP_VW&C6pw`i!!^y?GLg(duVRn`sP zhw7H1wA&!!?6hLy6iP^tknI}&iI95Wr!(RtD3f7Zpois33#Em=pV8?tBodilDvJ{X z-A=t2F z+f2}cZl`GIQxMp^Y8xU(G{v^o44?ML{_x3gneZQ-Yi>P`I^mA5HR|kr}$9MeY80bDEYYueEn)~MM;Cp|d z(*)`cGwAuqZr|4Df2Sanp*8D^;V%_Ueg6~4k9Rb8q59DmCLcxn!T=n zpgo)2U{rYS$q{egp{Ou}mbkRSyeZm{>9+cE7du&jf9tQo|2m+mr6=uw)`5V;!9c)l z+y8`tz#`g)_4QK&S^AMS9QO4MSU#<#`^R_cE_ULxs@Tq7s-J*I>Ag_N=o+B<3Yirs z1Ui^yIw+SnpTrU?AVnGcfXixg7kBhwU->Pb7m;CDD1ggy+_5`2L-z?i-gmi>%(jUB zv;EcnSvXyAD2_J_1x>(}5EhFwQW0{rzTsO=neIgxU4W$Euu%J4+Z_hbl{t4fQ`;R2QqCqcw*IpED}<)6c*P_UIvgI!+C}{VJXs zx=eauVOJy<>0NML<__%vka}nj<{czgz5SOC<{9f1gX5C41;cNPbO^Y#83bGdcq1-r zQXaPhaY$&*_}@5H2Rn|BE5s%=TnS-$DEqp@P<9ujT!hQY=7BgUn`piX{jEcAP|9qO zK87nH?%PRK{8H*z z?*~xrz?Bd(#n^ZAMGZ{8)RSg$;cA+-lm2L?I(W@3_BNTkj7U;SXBYP1}z*W$wJ5rmMZ(=5pI zDd6vLnWU5lR!g-1z7#n0Y^>K~G{yj2_|P|{z_T#kL`3gKiZ%jg2z+ee=km_NB!Mu$ z4oBF19gaABmM*xEH-=6>fc_0fe~M4TXKB&^;Ta-ijqwdfH!<^3m0bK&1O_wFoit>y zT1&cW_X z%qX%tu_u?wpBJIT8O(8FXHj+yklplhWUps7C)K*I+~N7vyQpGkcO4(r>bZ}zowXet z>W&Snv>-e!T_883O1_~l5Ut-bx%e{l@dH890+*Q%=XqIY^28l{1QV&SpQ=BCi6hbV z>({vL7XWu8E_}>V!y^!)E4_Gz=iLra7%ain_@c&kT(K2*!+mVB5zub6ckRrB74C`~G3Y9nr6&Kadp_#E$Jx`uZ8HlRkx^)_dQAQVty#Qi|*Fg`7t| zjSe3u#qa%Ro#}a}fI=9I$E6~33SC)brmhhanQ#>%H^NJ-=$&B2elXHI_SO?<)63-K z1EK#}NPh;Gap$=MSyv)}{z6!Kz>)ZwG)JuVz{n|h^w9nI>_mtYH8K_Z@!4jz2Wfl|2tE`xJ-)`YS*B0$ z^ey2qulBGAA6=e6{yOP$rRR0K7Keez#hCro9;sP-VG{khoO(DCMnw()?p;X0rG z@Kso;i%Jb0*w3qdtroNsnOlS>K5RloxLB$7q{z(6>HZ#q#2#nUJ>MJ9Wq{U;L)swY zkv0)&i-gBqyL7tfpGMs19s)$VMZyor1vqfg-;cQA@H=qC;a{=*@*(y(LXd@!w)S`46c1DUsK&BJ z#ClGITfq%0=QTbA;jbo8^9rImjUmu0NWdqM5E{V)fN+c^LM*$kA3|mmeS!o-dmj)M z7b3*#H*K2l0M|T+O~bE#I>Fd9ZDNk`1&AS)rCBV4SO7@*CC+^LqhUR?GDp*3Ei+-Z$29jp8*TVt4v z7I{0!O68Dnm0eZw&Lo3_kN#~D`qiFwg__Yy&LcYz-^bY#ml$wnQrunyo=JU8^2VH$YEsx+X16m(rT)1|;cC*GOo(XMm(s za>DKCMwZAJ9bvfQF#K06^w};#TiwXEnh=?C4O_H55=Vj=_FMv*p-@9Wt5qf*Jn2ZV zzPLzkLjuw9#vBmdm?JFFkN5;=7hsR*2pvrNX?Ov`&B8)A$kl-VQqKG&aX@k|NVXsW zs~0tV3!x+V4sg@Xf|q|AAUns=OCY$e%yKn$792y>p_Rkxq#19Ma7-`Om(PI?^`3i` z-&Pw z?~2r&M9ZL0_cLUNq3@BF(UobR0y+T@_J4tBtL#L4i#+zdF~Vfbt95&|U#}&j*N|8M zR5VQ-O;Zc6HnsVxMGrmwY*wIHroDt?z97JM3NS4)PX(BZ#PM3#vw-GkVMJIYeEQmk zcH57|JW(WjBVMAI2LoCK2w&90M8V-SosC-^k8Crf&49Kl`dUClFO?s_u1Dx5O}QW# z-R$`gEXF=_U*!DjX`~B~i2D@69m-`Epn7-_TO@Y@ItGx9Z;r4|ON`7Fw`CF~DvfUe zvK!wFEj+$a2piunKpA#h4#@oR&8Skxw*bo@-wZ2td~;aA_(n7v->iBZ->iBZ-vadX zyV1uR94jb3Go=W+8u@x#l5mGuMFJpShB{^2{~B z@@KAwl{#~ESlSMpxf)vP%r!tuKXWxHIai>RHggR~(#;iI#W=Q11WBnp1N{#ObqarJ zMTS2$O1i;VUzq^)x2O7NMJ7#2?kn@bvPjrh$`oQ>`O+lAT)+{g^c4~%D)p5B+4U7e z3-=WYVSOdDhwCc=ncr88Dz&c!SbkqItkk~Zu!6oqH0vu?J@yr=9{Wmwp1!Y`wCpRC z*7{07lD@AP3H!XKT zdT}U=gf5}mI1+-O*7Nw>EaGE*4c3RFb+=sUz;U+LIk_~{K0<=l^eI$p(WnWzpU#U2`fIe58kBdS$ zSoT%DIO_eWI6IKD_j$Mp@$V}+zLt1%al7mtcdi4*=1lip2lT2{xDJj2U6rl_qQZ6X znR6XjJQngCoHa7Vb8r>-trKq)&ZC{0;yL&nSxKIQg7b;n6SyDTmgG5LIV+L5MR@vR z&K`m1;JKUFgpJxqy#uhOrAYPy)DqBoaY!4a8Pd)|+9J6Jx^wCD926sNbk74K9sY_X z=OvuNgfa>t2%G|E-@>-M4T(i^4Z{9f(kMVP6zUIXDWDReC)E+^(E2?R41FsgL;nyE z(Xo~o{5Qy-i2scECgJ(50Xe$ue8i1<79gXZ28h&f4(uPo<`9dq)3D=!3_Al5u}h?R zb_KR}CN+mzN^L`JK$3o_H4+ZBdt(_*sVs(at8x

3Z9pC|N|1sYFY!d&^p1{rp@r`VmDUt2BQu2x?u%6%^b{Y&u+-4_^DCOzP2vVQE z0%SMh8d`Y7r2sbKPDB}-)M*fq`6I4TrH;4(mOtVeR_ciBu!0fS&?=k;RyB^c0c!fu z)+FUHCu-jm@Uz4 zqF@jw3RXRuZh)TNbWK{CE~Pco4M@_Pu947mUjj*~%nrAsdxYMl)8JrGv}apQh`ezp zTa-?N@3k6q8Z^LrGmN!>x&bN?It?62;4~n5Wv4-amfmT=Ld|J#wQ{D@ARtNaG%yl6 z4W0nW7jlwc+q)q6uABx__DnPWIxAPlf{nP=m!AUHz-hp~{IlwSefhA9s15ez?trW> zJM^G06TNa@4$#u~WtVMzd7^S>Uv}wL^ktWAeR)2}{J(WF05g*w(^H%VUjie^X;9;0 zCWM>@ha$n822(M8x&}<(G|0V+mO-aMFJy-yIt|8Y8l48u0NN}h3OfzR$afkXrHtq_ z_yDLd*b!)&s+9N#LO21Aq!AB1jJhz~+f0J8DT z5vDu{ktk7Vd<&4>_-1I~@r^>*`1Uqf*!UKZ`Qw{WrH*d_mOs83R_gfXu!8Z8Xg0oC z^*Fv+^*Fu-=;_Bdla}KfrM2-bAW1*I841U?Z6GO?pfX1fxNwStW`Bhuhr zHde*V_7JS?#U^1OovILLwtWEE%+?VGGh0LF%+}C3vn4v*WZDj#*-k>-X0`#jKeIJz z&TNgEGh0&I%r?OGXSRmTnXO@SW=m|F**a`(7tU;nUU_C4prxPLn$(=xPEyVsZvv9^ zGg~9!%=RIWlu8}6$KP`ztV45r!RDA1DZMYb@05e3IT5~Ah<&Ff)@obdafB&-heVb8 zPJrzCj-iG74u!D3(+p);-wDY4zGGCWeJ8;3`;K9y_8o^6^c|vE-?8ek?^yNNcLMbE zeaEC_-=VbDcLI|1eaA@HcRGQjRJP&1mZq(C_qq% za4I_y99G~&Fto4};b$-kIT4D`Gp|6>Y|d2ZL|Bi6C&9c(Rst#kv`M(q#v##(&;@bB zIUNvj`1ign{7JlPp3U*zLQH0EzaM@9uDszI{fZqb?vr4R-NqjO69Myd|L5yhQgul}JFH8IJ*hji;a4z!e(Y)eH0;G7_}sob`b-wT z@^(8CiuE9f8zJm~>p=N*R^h+cXF{hzY!U9uegqh`QJvT`S@#12`%01cEEr&h4rnnT zI1{)z=o4VpAZBsa8)^(Q+#?#!=XYj4!OYEv(+-nxmpC~J)Ku(94EoVzg* zyYY`eYCx*hxkaunR3c305Dyb0Sv1%)mlE z2DNe78k+;S`sRHp{6)Ok*u)3#7mRJ#%&@VgnATFYq;+U3-?U0KAtRp&YB{B9IY%ME zx0G_l$VY-%${dum@EWvkS=K|4#Lj6Pi)`Czd^;f80Zif*VDiY6Sb5f4z{J}*B$~C% zC6@7953CQ!Z)Mgt#Y(n3Sj!KskH>z?1C`!W24ST28b6=shIu>KKOLRbYX<$5LuEi(uyGjUZ^)Rv~z z|EXe^k-`3l8NK?yHG!X$i@nJ0|95r8TF>sM<>BAUeGqTta3zG5Ovq>ruR+KU#@&uz z`gf(acsBs6C0;n%(lEwmKJEK=xdErbzgrAruF}6t)bP?U#%uRUS{hb=P>O^1T;#8l z6-XZXSc-%9Rb(YOc-!?Q?%{*cogPkd@UozXkhw*;1FUY{z`^^jtj>TEaw@_S`O>Sv z(s|dBSe9gH2LMV)U4$hvFI1#@eYa^z>or_~&`J4kz?2d*&`|klpT!*s=f)R-q*SIs znN@jxRp%C%m$xC1ds6dLBJVv!Nh`d(H-TH&%RAEq!h1DGl;SQkf>bYWfb6Ekh8CU@ zQvjP1KZ`PKN*s{+Q(~h^oe~FF{*>6TQm4cYE0_`+T7{R_s-~_oUmw~$m6wK@q?`^@ zQr}hPNYYP-jfB(TH$hS=$EIr@Hz5c)g|w9x3%6CC0FuDV`y&$G2d5(W4A3_UeF!Ml zpUwt;U)G^(b@zwkjOYoe0|;|sgp1V@1GKr4kW@=tFD`q7&>9;mcUq%MPiu`~GFoJ7 zkd;c0aFtzE(|CC|VPN&Wyq{v+GB0l~SR{FQzXjHI41@6U-uY;<$zB3n^YR)*FYgUV zvnM%6m}0g>O;qxN1jx>84J~Z86vE857%a?e12W%ijVje_11#Tc4J*}b9adnrM6)M3 zgZLz8)uZVK=;@#2Oj?>Qr8UzHNYb0Gk*F60B&AXqZpYp7^8SFfig9?E5P4`WTa;ej zeJ*1)=;b|GA$obo0WvSIBMH2`M6c}S4bakid0D7=c^_8J^zsHI>Ak#0LND*TAo)US z`?Vbp!FT25y#UXLmBw3V<)IF3efcVI4ZOVU%MAul8|=%S6k=b#8j$s6haU80qF3(A z0b2UL?6R#dKdao?mtA@lec5H3m-l0k`G5Fi9Bltc%zToThBX+-c}Qq!SbI$fd3n!A zf_ZuKVHsW6i|>jweVmp-FYnFB4ny?vKCWr>@@@jOSx6N2@{*D7<=s;m(aW1T2(p&S zDQKFiyu1@ZS4aV-Mc!08dU^K;U6Ihsn+pi9j)7oa-UeV5_VV^sOnQ080x~bJ!wI~+ zhD0y#O^Qx0FVRcoV6dyg%R3c<-AKXii;Q@J<6DK7_W|WXFYoIL(Nnb!kd1GSFvU|v zqC}nhiBpl!N8_Y(@O6tk~(aX!3>l`%JY&O=6$oBbctkFoc znd^DrX*1VR3UTIoA0V5#I>KP)YUrG~zOLw;xo!l6R}I<@oViAaxS4A}?$2CFU3umj zVEHpw!%CgGIxKAm&Rh*Gb>l#EAqfo$$h0WSejRNs6y;3x0z&^!aBl~zCxnPeI-D4eZ|nieT70; zUwH~Ftgi%QeqS-F)V>m6`F+K(Qu~U-3i=Astgl%0*jKE2>?;9!`o3b)vae8D>nj0C z`o3Z$>?_MbQYx>dZ>wE-br+zqs`TpC7|MrDhhkpcA`pbVx($F+*{kcY0i7`4P}9io^X30S=ai z=~f2C;i`lMfb_3i{)FqG7`IDd@E0yUhJumsF%;ZV_MOz8F7G45&Y!S@Eo$E{+#R;c z?yWP0+rL)X+tUX$g3WL*qwF($ok+iXU|~UGD+^{#Yz4o`2hHk2IS@@e=b{h(mn`^M{y;D3?b$F|Ay1@ zE5N}<(Z=pD4c%Kyx&0l|6T+uzOwdy`vh*aCDUy-+RXqIMfuHyf;8rdl5PTMc4}*5N zO!6JbWW`5$d$o~G*!;IbZfN2$GHWQ>20UsMdS09a%j9@~k*)yy2lOW@bQHGXO@UTYlX7`{CK~)X)R;jNTvlU#v7I|H3%y!TkG|Iq#loQ}PxtAE z`j$!VZ)kvgU~2=|QBVF^9sW+!yX~kE-D#?@o$j$s4xZw9cYrcDTEw@gRl(66*j2eo z4+KF$hy0F$wy_}Y`6bVwL_J7n#Err6B1b5VzZd>UKcd@zoBENJO|cW9ndmViwj_zpKPQ4d9kPp#MQYSN$_l^{)UAGFtL>NdHU?^luSg z{}%D}zZGbx)axrf?<#PPGPoO;34hKuv%HWFkKE>Yj}eo>hXE!J8whmx-s-A!xCJ>p zU1b`&UYyi8VdhWNYL6GG1X!*6$alV%y>1ioZ706Lkn`t@@WprFMj2e7L^#}tnj6>w zgMdjLIvrvhEBigp{#lLb$^hVd-g`38j*4J(6fPs;Q4#3Ja2(osGz3@pne#&fQz) zd2R4`!xah%A^(JYju5KdK-Eq|x*80Z0taJs>d;(Im>nEmVG%W$@WEi^GVkA>$H%HD z1HMbO>OSOGH!zekd2lC3i*VsK8daY{=I)d4gdypHe*u~MD*^%iq3a4eFu_hn(jyqk zWH6DrGQG;P2L^}E9`IqUOXZ^#%H;>th%4En3})a;t`P^_xoBfHCTeGcZn@i2c2ayhRwbP7+(cg&N$HQ7ml-LCyT$+?A^|0yElCW`@3-Az#GsfWCKE2 zTHhZ~$pK(~BxnQX$72G3jDc_%L@yxIltUJ--~eF8R##z2vfnDa`T}m0!ELxqMQ*TT zi@(0SQj1@EmgmhQ6@!(yRQ`15EJPPpoU>rC$&VMj88{(=JQ#e3%SyhBl~^VZ6`2O~MkU@|TfgNIuXbJV~q3UKO`o?lTeryk~c(}~RB*(7AUPe!+7u)g4XRUPq` z5mkfNLLJl!;B0t>e2js*)`iu0xPr|K5n+F|!KcVXSCD%fbC=0|m*6$u9HcG?a>gKs zioj85vFNo#!vUYjMH_`qB1!kgH|<>}{3^G#zRGRQSGm?f+vIr-U&lsQTnV8V37L-& zO}&C0U?$Ql7NEE_0jA`b?8%q9rsN>ft}))oNA~17vR!sG16*TIp7Vd3v9O}@&%;9SYE{sJM{3=`B1PE zvFsuWN(Kp8N2F!4ge)5%{a9T8ktG=@OFdqzD`J=8!Xe}uHb;nBpB)$P3V=}t&*9SB zDc>g>|EhZ{af05;i0fKs4#PV!vSh$rR>tWl-!O)GnI}eH20jdS6Hm3=s~Dw@XpBeS zZJ3)>Iy!R@_AdlSXEFk*oBZW@hk-20pfxTlf`>^W_X3O~xy)tICCH@!P-64Ju-QDM zjtg@3_C?d)dl1o<3+eu|%?_wobKyTN`<)c03oQl*9^ai1mX?r#2q9hVJ3Q|bfKdkD z;xgF>@$^Pail=_dlclC-KrziqDjhl45J{Df9Q-6 z+>~*Tkxpoe+r&gzZTc4 z6oPeceC4lp!I!cUag>Q&dH@Q5v{{(7L7h-b5Z@r5BIgr@z6Vr)xO;~~Hsi@=hQt@K z9KORLi!j7@IKx3=+xs5_gs%aBDEtnGj4;{Z_WsqVYeKj8<3-U{Jkwo{yy?Kl;jTMW z57qFvvKnj>vIE(0Mg9hi&|99&@BsjmkoPP_oGDMJh@KV*B4!kp zD7sz~2F&6AzE$0QW_Hiw{oU{R_TdcOU2j!)&2-J%Gu<5VfA>{GHKOA{G{2m?-zd>_{pHMEh>5IehAZ)K@-4Dqd72Msq8K!#y zv%BUmX^MBvKSIi8n7m?*yXI8N)|z1oV1L)#VDY==Y=qr4Phalung=y^*W9#f-Zc*> zch}rd-7t9(eE4B@cehpt+l*s&eqyp^PrOduDPl3uK8|I z!8hTt=W}z#_JIK`gy_T>;qW(4AEbT+(1=qTFCd`#AoXdmkkN;!hii&gpm!m)MhLSz z=1;m_;~%De9&)>5p3#S?Uqjv=sP@Qk*W94IYo5^ustK)-=V371Ial79btG=I^8B_) zs>vdgkPP<+j1#sxwI2#`cF%0YIOIX3pTy73`@HtY37uLTbCl$68sm0qaXUwjIucn9 z%v$`iLK7S{owgZ9O)Od>BaxJapcQU*#Lvus4xwI^lyogAj{b39nY+t-RXu1DwnRRL z=26t-=A@vx8&Z(Ifb)4bO1ZfLKfPhc+tOA6hYm~SQrr-3u&7XPF)We3^)Lvg4mS_u zXHX7_NMyLY-(Fu?O&5xA|FtnOOQh#7*uzxg=B=Ri1X4s^%O!*0k8z%MH_kd=R93zIE32Um|qdJ=wSlASNH zZ>gsH_DI{ceFk7spsrbh*8zSTzpT(IcI$tw%{^LgQ95dfpVM9}BiO#3P_h?(x%R;> zVgF65)cY;~_rfnLw1olB{4%_H5Xmm}+eJ;lDKeG0RZHA}hZ(!0Z37;@t#vt0B|5M5 zG=%l_c6ON8;(hWkul2z+yzbl2YccbWL0mv&Rj>1$ z)enV71D=s9km{f*-ns7`Qj?LY60f*Y#z8tCWow0V2rS!?@>ZnCVWFWp?Jfm zzG~}b=`vqn5 z<^64OIR+QDh6@eb@Bb$C>NVr`XGqHA(>>DBdyv9YP2JDHeAuo9+D>IPK%00TBfNi-P+cJk}{6HFS zSV2Kn2I#gJ+cMd6uNdTgydnj8oVu{pzcQI#FCDE!Ei7nwN=9Bl{(uraibSjx&M2n0 zY!~5;wb9nIr8Rm~RxUzeM$W+YWMDIL0#X+tRVMtlfrmG`3!gqg9`8@awn}_?ne;m# zhQd)9k9Oso2is+oIA&y>JpozD(|MT3UK>Rvts%tg>Xi%tIcZ@WK(_0uAQV zCH23_%67wH@4gLLF>?)N7jJEb9uSs>DOf zgkOc&9f~~M9CFP-{u@P?A(a}!UGzLsJ0XR`hGu%S4!h1ofx&wrW$=rTBD_NOgW)jr zS|rb5_BGw|EjJ+H#~E}6QG;4%5IGX)8et|s_GO8MI)7^O`!H6t#n$1OI9p zJ)$RY=0<;jcYbqL%ud^baf{_3G~)*@M+hftPRNCm^;l%W$(ornfnO_}4=jS|L$ObA za^7(%kHRx3EQT0YnMjS$6sH6yCQ?=66<5j|kUp#iH-2gc8*(S({cmV&M(TZL%I@TR z1}co=C+9g(jh&o14>|;x=5}(Pfku5UoV`pm5B^CWJ}hD*T8Ls{Ax9} z9a81e=AfKh&&?fCKtM+NBlUwqyCGF6qflX5&5iK zhb@+VV|%vl7mrve_o3`sl+Bk7IL9r33$5Wo!}i18rQU8^aN7jiGT9s3MUWo`InV5E zRCbig8mDry)D>CT8~LoXz_whP{*?-P4bFlS`=J1)v7IlE;d+rqd5{|1Hs>9xm7xqT zAI287UWNav0N9m)30aw{!rQT(FaN=nBW2{82EFQQi|hYR#70cZ2nU0p1+{xiDVt}RXM;~Iduij zHBr0hbihF*AHB~#a5W0EGDNq1u*J^DT$G2AKu*!QsbjIN!uQQhy-fI)b)Rd5gTEg6 zjC_RcM$}m*D{=iw`BA8h1o9p7Wx@|Rgi;X5G02w*zhmG_c}V%m0pqT4Gye9AYa^pRpW3zL-ccfY&?-ila+PoJ^1Mm5gIPaYV-P*i& zDr{HEd64Djy~hv2g#e`5Paa3VeHjI1N{6Ai6hsPVvqC1!3R$p9ywn)5J4_e@Vz2_X zL;uz-na^0yBfxrC19bo)WIT)~u&tEKV1P_#tL%qj$H)H4g)_CIT{HD=$QTrK*5gou z)K!||?CfNus>DMvW<|Fc8yc69KaiiH=w`z(=+#txq*fqRE-enrp_QHwMuEZGA!YDm zkRrT7ZgP#*rY@4>Uk5$E_<^J6u*M{ia%_ptLS3Ts0~g)l+J4t261@w(G&g_X7nWbx zMVn2m@SXAvstaP%l$P;xDVDaXE=wM&{`%|eBLmEisdq1>DlpdERq zeX+bg5EohJqO@O-6vc1I>jxj+6zbs`@^9uW+t)emaZu;zWtgbM4>#;|+ZkIl;ENw| zsr~`dU*Tt7`1K-K9&_tLyMIJQy@svx{?QryUeiv?F#m~PR%kIRtFevnS&l1k6AK@V ztB?CvXG5&_uNuL!yMI-z>kOAjIgZT$$kW_Bf}buJ;6bQ$Y(!^`-8@&yrjYrbf6j&C z22g>0nQXCq$6I30!_;E@lI~%F$5k(zd#r2I?%9%#KY`owBYH-YS(%3|_Tl~f^u_il zKL9_ggrB}h+?$|U@(3P`FHv;v+PCk3?)r>z?>2fzr7Li~@xy!|wlKacjSoY5EPgrT zxUoRk-Wd?kM_2iLsSElbN=qpo`=^s16|JrLQJEVT<>}b)i+TM_VuOKO?F?7l|T65A{;7N z`AE0xvBd(n?r^uk8L4)VUxJ_2%tc2Dm#Dspx^iLIr{BQ7`V{#+(0BkpTzq1S6VG1g z`m>Qtb2AS=tLiCKg+6e34;;u3QEE;ee~B(U!uN`B8~l4}akDLk3U#jpdM~yTNYkkF z6EM?W&`NW22!2+nE}~f^t9C)99H970b=k}!kp?IqhjQ&qTot4plBadwA!AF0CNpgx zY{CjLEC#vJ=E$|dBgms zO5#Y->L#PGy9IV@m^com+JP1{-u12LzKC5UYEpD=w9k=*nQNVn{2Z-7!mP;?>2Voa?=#GHKx}G0laf?;l(QHaquF~yzY~jGw=<+*4nC50r{BrvS(#tT>+L}^s z+SHW3gPEv~lycL(rnCWchf>PT)j_JeW{DiWO%zS0l$$4lln2{pDblBf{IWvh@U~}DiCUo`&CNjk!YWYPDK@&|7knDDEjTT= z$gFKajw>3p)66^a{Jwy-FBog%sCL^(bPG*$^M*HFb8wK)bZivYPKR_gO<#kXZYyb; zn;-GB#&SQ~in#QRY1gm6!fk{t)&R;)Q~YvJxc~48sc271xj86E-RiZsPsI8cO1bF} zq+FSW6Y@hNaSprzrFDLnyI=gf-2Lsl+)JeXC8_9MTIJ@EU{%+|Es<(SU!s(oiXi2> zIIe2BygMenSiDH)UWxkat#$SD6sv1~{=j<7rJ|q9?U#N7$nYgwX5e%7Bdq_2TAG_Z z@UuO_J6e$L3h9xQa&ux*iZ5#MygweLdv!kRxORBkioKnQi{oy&s{mDT9A2+&d~ux5 z9JU)I?V1pVYMyvBib`1xH;;#E^xJhyWZol~F`$&2MKz^gVS(8SO1b$gNV&iamJd&- zqF*WHrf{8iPo;}x9HgnHC_OkK$q__iF&Wi0*jgQ8C!_&A?QxcTrzCrpWgt|B;fEK) zVvB(NKic5;^=3o!m7I;mQq5Msj*|2zaewl)+Rb;q5ufi|Z1bIO3XT5v{^%-6OhMOq zR&K=wf;bIhm1V1gJGUD!xbqnUi~IK*(4#mShKAmRAErmtk!sy;#1@BW1I`SOK$zxc zJ$`xD%H=yV#Ma8SD_|yhX21trhhR_r0h8OThgp7xkAq?B;j!zqM)q0fH-)uhkNi*9 zY3(}Tp{}#qpN_TT?at|Z`q`aZwd<xPRAfeTzHy>DjL1;6Yq(g%RnX;?9G+_A738 z_K}^>KB99Q?9ZWH`wl5?bM~Q~+Z={PVj~6?V<$$l(d2Br*;%0{_%;>_vDDI zndW8{e&!ZW(U5)!>35WJ^G8kTskh>O4{j8vx!D0f)78atOXNvN_otMbBWp^pJPog^ zqm-NTgH%_jEs>e0r=m+KZFdhW9f~u_e<sueiv;9hu|n?%RXb(!cIW|cB!USATKa}2uj^L7b2$p{@l6SX;4?Tg@@$Qb;^&HFaOO?CvFyPV> zC!8Y0`CA|j?z}4p9!Nr40jvmr~aGzfN zi+i--({uZ_=+nRC*hm_7=~5J3gdv$seY6@4Iw#^PjJ?i_#^Apnr7I9D_!f6#ew1#= zvHTyynd^s-`AI{rh&UMeY8e4b!*PSD73}CuCOI|wNhYH73}2Gs``=O}1%>I?k801C zg*^u8Sm<0?8&Nc=y@Kl%rb{~?Pw;a9-vaQY7#KIUUP1Ali5pu>q!f2*UIr}9%{%zn z5%b3cmwJc$J2l5|k&4z6%FTuV<+2ZSn|YYvPk#<6H(TRphTJ-q&9P);7fQL=x2E*_ zv_$`=l$%q6R6}jCZ2l{b@dYRyosjU>G|M_)XiZ+v#>=$nejnr}U-9_k3ATs!2 z-d4BWbvqDSICD=o1PPL9ZkFR`&J1JcTq3t%M|@2wH@^g_?uf;*0@75y|6r_(pXpJO zk3G;(swX(2&);%h57@m7KgbWTS3BOIZ+Hu}r@dsG( zX-zI|dQ)ihzxU_=3+G+-1$qR2cpHy8vO>3?V~b@WHfFz?|c3*}R{=SxWfN=Q>PxZ$hfu&pY zXptP!kmE!o^)$)pS9h8`wHFHi=Jdvs9M5l27k_>m_f`D-#t)h1&Tn6!DRzEi`Bo#6 z=ePZkua@m#iRZV*u#-E#wL<9uzNFUqtsSI1zjdmOsC|Ar8(^N_2FJiSzg>jl-`BD`rsB9t0B6n5MWTP9*=cT$#?OvMjA^zqrdcd+Zi|J2=Kyj&esxbkOo~1C z*}yV(C>u=nJivPL^FX@}?RyVx)8?Gc7#7-X$pyf!j8}*`IApVOiEb~`?Qm={?%3fJ zbgLhcZn?p=kR2)4zn1EEmeafI9fqa))nb=%oo$bl>ujv?f9q_m;6itE;Rw2b8!y$uu{M@w3*)=4f{C%>NOfs|e-hr+m@t+r`B+WAbnLO3{+fX&4gQRx z(X`6VjlrsWtY#WU>QgD@hHKq(eo{BW_=i$%79^#MWg&+i@1gX|Ao2Ybqt!Fz4;yVz zFMlsrUt`(ho+f2 zs>0>ZVSW1dDe2R-FCKy%)VXh;!6kS+);jmy(0c;EtgynY9Ie}S*kUIV;fkDC2}+rs$akvPC`=*P2T?T3;!_~k6}{*z7Pu+s_Blwy5z4JdTtzE0k$xUOYZt9x zcM#@v^$zY*6nuyu)&OCP#-6kTu78nCbF&dY(|5PyM}SmQ1wW>v?eNR(_eA@s^gr!# zoBxF2-uU6uD%fJa>@JXP`m)prmM#rIi z6n^H%L6E&AJ^PhJf+rBbuy@P5^0ssx)N61gse&>0-~(~IyyknVBPl4LCeHT4vFQY) zf0e=56Mt51l7@ChD1&F+Ee)A1sx2T@=kKEF=TT$xx>sGxzcmR?^lT&+z-VQYvn2;P92 z1|EhRaeQ0*-~t}qv_W=sh^`m+!O%K^>GbXl54Z+!Cu1P%pIN{Ex)53S>YrO7hPh)>}trir0go`0@bWMgTkzE=54-k z?u~%W33&K;HO-l}elU%5BvKfV&V*Ap0E9c(xOa(VJji2Z0k-@|IcKqM$<>cVOeo0c z)YH?jlF_LsHkA!x%r&^ANS7ADm-BIOGa5i;0ALj*n(f8u{juXY-9%BDbi|Ht49$9M z#PhI<{D*3v?;rR=F-?ZYFfZdfQOHw*_lqJGjbv&cq?#jz<#~ta(7Lc^PZSvZ7^Do| z3Ms-XWII&O)kymHle9p#51PMF4=)dROO)iUdWHe_g*2XyPywp+;i3rB+_-1O+)k+i zx1M$o;%Xn5|4fGO3F*)2hww^}^HK1-4DCBKl3%nSHL=HB;ecZ%%rqaZz|H^UQRKM6 z#t<1DPuvF8wZdg0g!0Ow!3b&n8$nzHf@R0p&H<=Q__B}y8e^~h$mlCTC}+px5}3I# zQ-m5DjFglApqcAYh!c()#VQVDE!Pi4dITO%#OnPmDa?H& z(mur(AbcdT_NVx~*goII)|Klk!+m)jFLw3f_4>_k-D_@BE?m3P@sT23o{T<~(d8*K z)Nj5;rxQfma=(_^Q;DxYVoxQ$rzt*_NOYBWq&=C~1$wwM5I*zznWFib*IzZY5y!vD zD7Js!LqQD=t>Mbgy&9ZPCmNied?ma>dIK856*0jJj%7Ibn8;>W?WW|dJzy#$8KjOzDl4;)LO;g#Eaa=iE3KCgQC2Q|L4q%9aB4wTrl9~! zk+IFlwb&M`LB1;?X`oV?qrLprz(PBpC;?!BEC1#S=dS++z;Hy1{Fc7>FWNg7g~Qce zTcqyQ1|Ee}MmVWG3wblziWOQi#0c=^PISyjClGHj4MP>RdYY>%Mr};t~!0!SVjOLDK zry;sZJkmN%8<->y_{UkBDVm*TUrn*ov_Z-`jYo%0V{mpFgR|2RULmK!aJ@dLy;YhgM|>ar`bN#tuWNwSzX_+Fw)j zi|8uxNb_qPOl}=JY{w~@ex0K!`qd99^UI@yUk0aN2B%+ySIBJ%zp6DX$DCal)d$Zq zkT5gV7pyQ4upB`TMN7VrW)G*L)_|2xFN*%+QJ%_N`^`N;PXR%XK_!l$zf?XhMS&ye zZyiCOvsNd%o`MJuMr_F}zx$ETMmDigjyGl+2WjxWWe1b=gd zvv-SOw%#b1{W@Kcrr85g_!AHr;S^r|F(kd^0P#%}u`>u3WWt7>{3zCw-?!-&un}IS zK>KSTYeD-P(iaE&En(H{AEovOJml?jkD6gUb3$3qoZqGYRArtyFHmKkId4KLe&&1< zd7L?&QsT_%%JQB$=c_@UIg(Hk7v$z)gI5BKWYPWXU;9J z))5U95@$|lB=4DXC#YnEXU?25&YV!$^qCWYj%?Vs>7j-G)Y1yJ+?f;N#F>*Kcjok> zQaf{=qz&Miv$Licf<)uY>5%TsiCp~5IY`kA!Eu^m2$mw{&YTV%LeSt0L4z{{3CEce z(A=4Gl?+9mcgRoQ%=`76n&qiwIAl6j@TZoA5Sd>=e0RD zkTUl?8t#2A93>f??irl!5ndtpCETmfu;iJuQf`5y=E-u}qp7GDJT8UockY0x%>ApO zKUvPj$#Nm=x|8KvJvV-b0(Y`pSL131W(KnRuT3}Dl4m4d&1CeB=-$_G$M6p6g-E$8 z8o=EZ%~tgY&t1`61CYC-@t`sBD;mnf6%7zmP%ExzPOHzO$19pUwLV_e%+(aHY(7P5 zjSy;AHVv;0S2i#)hb9UOKS4|E%H{xw?aHP*Qfq_&cV*+@?#jlHyt26lNOxu9FyYFE zpv098vAD7UE_Y>f5WM1p=)AI_WGNfvuWT}UWiuUW_EFInG{q~NB}myvMZFTPax!|A zV{l&S7@Su-gyW;4x+C~e(NB@LkBSC$yp)AkJEqMm9@FMk54Bh8l}|ulj7#%>dNi+o z49zPbYuf&}=<#TMNei!nXgq!;G*8}*!}47T`Bw9zqHMLjem+>v|N41zl&_!P>^gRv z+@GD6JL5_Lt1EeBZ3Eiv0kin-4pICnXg^3SzE9E=?-Il$dt=JNxu%6 zT?I|iHjvAoLMqPX706eKSBi7FFU!m2AF4redD1|o?2PtqlFRo2pc*|<)~CB~#Rihg z|D*QEB?@&$Az0;90_dGhdXK=b_aJol$h1`&EZx;CQrl9Fcu=8}?yF)(=VWfa$J_1!bE zAO^6~^NJ!~j>uHzMm8yM6B?hO)tEH+SO#NG;=}Yox2B>dH-yp2G98Q*p}_8~&W4qi zC&0?GbT62weQ$L*pf(aAlsyIvaA%|BNaO*4Y$OsuW8xzb%Kb>>HJE9O8Zi=i^dB@+ z^WN%gfbnt|Kx_n3r7f8BJl;J4RXv>)7GBzz;mCWde?V*_kXARc-+7RIsmSO2JM0Y2 zQOKD<+n0(wCX7M|N{m8?t(5mreQp$TC_M7_S3iUV&wJ!XA$8qf^}ssruX?fFU%ec) z+Wl2uUibagfT;8Ssz+?%{%U~a8&y<^7u)?+w$ARa`tmyNuX=G^_gDYI&f`T>7+GAW ze)Iln`h3^r+}UOdBzCrWMpHc75RLbrIHa9zHb;(6fcslz6^iEB=0i>KY_kC=JKK14 zINKPUXB&g_Y(sd3G=$+CNmNQR(dzJMGCFh=%(SU{E{Z<8HVT}su$!k^Fr3&{BpTR{GxiUimtL4dnU0z>KxC2#hZ z1RfKDk)T8{HU+j)^SXY*g}+;1H&nC+K@k} zLSlh=ucjE7L|2JNT3{Xzy?9_g zrDz6bR#ObjkCC##^ym=w;B%?yK*Us+ZbcCmz#uT0 z`x8HV&Cj+7%&%arT7CzDe18sPI*3rd-x_Q@$d}~!zK8Ms9*~mn@2`!h&G!cZ%nF|O zV6EUpl)mdrYE{qyQjQV+6GI>nI-Ror+k~8X?3^MH94<92-!zBUK9v$8P6OMYABbQ_)AN zNPwM+0^FU749Qc`FF@Me36BYF#|7&kil2UpAhFZW1)AdNhv+KtNIU&}2EF*{r>~-U`ngI|JpD{T%1%EX z9Zo+6=jq4bJpB+}A>Y7o{Pgn^^0}ZK3@^Tr%yrDMQi}%jo1pPFGEd;*y5e?MJ#JtBZOGQ-lmOY#8UNVsumU=@=qVJPeW`G zyG|7eu!s$C7qNz9#HMfa5$iD_VhKt_EU}gHF{;i*>|r<>8mBi`LyxqYWB@N}g2`Hed*dJke@{1&_!Ztu{~ykbKeVTgHI`TV?}=ppt)}KxgWT z(k(FrY>wR*qV#ZBwLwBdKRzwBTRf*gVo};%Q;brgtHdKMN}E709;HJS%_zN5Q;gCF zk+LZD=n$m_XOtS8QA&7)>;S{@DBTVDT$FNZbcvk%BA$Ol%m_FA@Ux|WS0XsyvE@q7 zYOwLESh_wKkv(=!D!LKSqH~L)Ua%rlnfq>6AK4FDWV;dnldAtbT4cd`2DE-Y4q8v9 zS3;$Buzm%o1uLO0SR1#<7pwu0H(0wvy~XhwtUGBEu1AN!H8=y;;0#>CE98D2XbgSruTmaGJ{h=Q$Z;>HqOEr;h-N`%LCD;&J$w+p zmI%Tb8iYr~x&`4pXmv#Ql^N;oP^le+C4gEG66%8RPSj9)j2i%XgYaFbk3mgf%;~#R zy=D-yrZqx{1>t?#NHQi>OQ~8|*z;dL2s02{5PqwQ1XvITxC=rPfNZo3(r%RfMMrx!sYUv|&Ux+PIyCAhj2(U;Ea2KhDWTcJ((#@4SlOa+GN<=EL zm@5Y^7paN4a!8u9QOO%)rBJiE^5-sGJ|ujoN&A|7`82X z*CFrb%DuX~QD)k_QD)klE2p-bEB9!3!_3gUVP0LRSX7^&DMmHX zRpOBr)jgpXpDRC4(TwUonqpLsMarVuqeE01oKbCXMm6CT(jSI%Q9V~z1^@nFQFI^< zrj(}NLD<=R_#Ijg=ffXIDn1{cMShJ?!REr}>55=)B=21Kduohx;W_0>*%582a~_;8 zmD(AAwrtq+^v`-es77H%_+qHfU=du-w_3FXs*9m++=r)uSt!iPblu*GZASQ(s=u_^ z1g@71^7AD9a!y&BeF6%yau~K5*$dl$+{vTG?e49! z#;`&6r+e?q{mIAhc84wNE+<2rm?fvkx=Rq1+9gzLZ2)J<&(;*X3(-~Lk=9+>!(@Dx zyr-hsT}En(-DNUT)?GY0bQgoOyBM6^h42bF8-{b;#a4z5N14u&-=98tKkwe7YMrym zBWt?%Dn#a95IOhWhQtjtHAVM`#%R_VG54-QF7DnRil%%2zyoPE(A)qcM`v*c$&WPSya^Q#9W=GQ1q(J!K_#3RkG z@h};mC7+;Z`ZZlsJZ!HbWqx^d@XO%z%i#2j@Cvyj;TLXjBf>dLei;0U&5{oVtR|%& zHZK+R1g!J{PwCA4^Ds~8ExyA;a;U^n`ebGB-%#Kv{f-)x4l~O}AQmR4w>l_E=?72Y zULvJ;N6Jw;;EvL_JRnKww*cfQ-Gj!&C!Hw|lnz8>lzwU}9!gUB{aPO>y;4)8^ly*? zGY3jH!kv`OjIc9ibXj?xWHN;fnq-I}J9?$CkK3C~069+scdt<|J-w%Sp;SIJN5rb0?@ zcn`Zx?$1&BgEc69H^3aFLlmdKSNUtcA`P)4(TWzxj3bFQ#6Bph^83qw<6^z z-JwIU8=S#za0WZ!pmacUCoxCqaK8f%rreo+7-46W{(#m)N`DclIHkXd{2HM`g3_Il zyp+CLjgiuG%Aj<#Wz&=nKyNne?(`daKFCq}_psU413l)mM?Y&L-jN_Wb6 zDSaoX7^UZwLFrK0G^GPj$_Cw$KI#z94Xt3yQ98s4N~g$Cx)+tYMeN!DUBs>_b{C>S z=?>{A9l1E?4N^3_%Q#K3yObj3DBYn$cQH7-i^17l2nVGDn(Ho((ov?Aen)!yL%n;? zsdc4vh!gI;29cw5FLLfxL1NMHqo(K{(V%pPbd-)<+`TQPQjhNKq$#?0I8u(%9XhyY zaJpx3x<@!D9nhS6pmgNPDBlIYPEoT;>5wHT{bGn5rF)U{Yak@%*NvK@Uqpk_9nw)c za&bzZs%ZN4w5I6SYe+dtcj(}k!ReR5=@;RkbU<@{fzlD2r1UMo%yCKwtR|&T&!(ay z0V^%_l+N6SV?3pw1WI3yN*tx%qzu0KeSy+%u0iQA^Yd`T!cFN_MY#K;IK0BIsh<7=@7*!{dh<$*e}u)gPmwlx1fNQDII_t*s$Bu)6lT2m3II3<+Z&u1?6PTcMr<|A4 z_lAm5dQKUX4wX$)Iso(8pqtZMAMH8s7}#=@4sn9>C~}nUMWsgRXK4dS>6dAW-GyjS zx9rkDBYn$cQH7-i^17l2nVGDn(Ho((ov?AeslV(;NCp7 zu9OaO!oBw(a+K~x&b`kev1mv=z!cph8kFvkj?$5fySI&^>E7O&qI)MGF23grF6&=l->s-N9kVV{JIhn^Xm>x(J!Jw=?>{A z9l1EA&rme|dPP(8>pi3#r8{)+%i#3O;Pi`dP&%MFzd-2-PEz^`_!Xygz-m%@ssgV= zJQkFGzo&HO%G-EK?+Z%*43#)aAEON35MS|2HWm8MAPn@?9}2Gl71 zK;+%)H(;U*O%xX1_-~pZrFVeXDE(ShB*0O+hdW9)Bq{wNARVPUOrUgv5|mCXC>^*u zC>@gFY*docE1+hS{-LHw>E9sbDBURqN;fzu-Qc8j!tuTX-4VP>-~6FK>0aGYx@nWr zO`DWXZAa-I?I_*Qq;x}*(yeJq=?)zzo$x%A?qT^U-C9jbXR95hdzJi@ZYreoJ)si+ zb64pIm;|LC1(>6Bh~kufIwThCy)?yOCmNLQkdD%ki&Od)ie|8n*A#>O5u_ZYJ9G$k zgEQC-&R{1Tln!Vv*d3+A{RudjGB(}*c=ldW`b%05Dg7;^;*`D~`87g?1f@G8c`5y8 zHAYI$DTC6{mQ7PS05jOI>(ZB_VR1^|^5Gn%12x}JsT1m^^xdGbMhHw$x>L?e>4!nZ zC_SePN{7m(DII{-Y|z!|HE57=Lp#`Vln!x%^C)tZ?nR|W>6d5&Na@2g#qL5hDBU3) zr6U*Tyqgrw?sC7T*j?rzZV0Rzu_{-HV)i8zHf1*!mHs=pNCabcb}5j$GWmT@+3C4$~CfI|C_4=?)#-GdSHd zINc*0ln!XlJy1IGWRxjjFgtAB)T~lEWC=l2nVGDn)3^kj^HGvKMlX)lnz);O21%XDq00t z=`>I2%-wmKr}W!F>EELgN9jYA!FPT%Q2MYMlnygT3_~moO<&iJ!I^vR{Cp@GrH@0( zQ99s`(yu*<@Z59fvjB3G?m=VXlumh|bRfE*R#5tawmg)i^m46_l>UXLNa;;x@T5Sf zQToxyJ4%O%F*H$F*y&`NAf0aGYx@nWr zO`DWXZAa-I?I_*Qq;x}*(yeJq=?)zzo$x%A?qT^U-C9jbXR95hdzJi@ZYreogP{`t zbCiyNNl^O9fH_KsC{F1YLSn%_SW^skqCx2n=_nn!IHg~&Xa@V;nqsg&iFQpgE zq}!zQoH8gKZP_%X1MnIfHZpx88WyMYX0V9h)46n@RvYSlMdhDMKL8q738T%9&80i# zyp(>tHk)(lIb~2fR5nfN07RtpVd>fJJvW>STaMBpPH-MYj?%rT)F}N@Z2&3#8cngg z5DiLqNJr_�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¥fR4fj< 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�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%0A*0FWCEks&L8vIIK9qxGY;uP@r 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%0DTBguUP` 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%0D0Amg^sr zL&y%;+=+s)jz(G8_^-?f07B!?I&{&fgS^ntg&WJZEfy^S`u?EuGe@~ zFTUgbe|Of4+H1t0xV>hKAK|=6BHj0G~llG>;(dLf-ERzHWfh+pNN{2e-ETvAg0kJ{_N zF7AV?q0uyn+G}=-_Hs0Wv!Q{<_gR3X9&Xda)3oWyXw!IcGW$stPBGl3E{FW{MT51w z+w|-&aGU--uuWGS!)^K-!yYo6+Vp>yahu+5c0h2bP5U98u1%MzHl_SyUeu;fUdC6A2WdIs9G`?U0J>T;+}r^fQiORY{UVKAz4ak5(I7hku zp?Llh`a8X=;9a{rp5H%-DD<7Ep+mM?^-#<&%OK`wH*p@@ z3hU*Xk@MIJ74sb}2>80=>*kA?A2R~+Q|3m(P}5w;zOR5&bY_y{AYTL0Es%O3IcMvf z%YdlasUvkWkY2(0K9Ihz>r#&a>8jC@7lFj1I`SqEH8*xN|ARPlbk4q?L-Yz{Jdmyi zol^>=yHQ6XK(gk#h#O8VK*|Mj36Ls*TnD6AAj^UD3FL=B1_bgrkU@d`0Z1sOm;Mou zEP)&}67~pW0+1?!oCYK=kn>2YS+8k~q>_kR(?vjfTlCUzC8<^&Sp{TJAkPBniR+wK zf%GYa^M)H^6!SSphC)?NtMPx#yg@5jdGUHsMU>??*?B#reU}=Pe?z?F1@JKc1 zmbcJjuO8~7IBUJXp1`>UukBba$}LUza&Gx^c_@SllkcM3l9NGhDO9jBak+51`@i|4M55}bmT5V=IO{fAU*SSbl(9gpg0y!MW7-3Hiajw$!lmh7$$TxrtD1`Hd8$T5D z^y4v?I`!rMaNbbfaDJhc@4ElzyrH~0ghmc|ao*S%aihuottfL3pYq1te^Pm)Dip#C zY`%-;B>4rLH?~uN8W-h_iobE*_z>n#H8_+v$}`9to37_M$+BUF5&)Dpit!;kH*dU* z#EORTU6ePToyd9Pb>Ch0uQVs=%OG!zY3IDrhq@mubWLca4d1c(t5GjXs6#_Zq~I$2 zRUr922B*Q`&@O_ID!9PlV0r4}R2rP62ImYvCvO1uin8)ODXL^EqBnkZGQv$4tLGpt zhvGQ~DH$Fd&*O0iCV^vZ($E6xDJjZ|v?tmUt+6`AQKh>GY5qCs$G_girRM;0OCROk zO#X>`%J3>z+Wkc*@{7C{QSZJc*c-(D4mdeC=sDp&@}WQ;1~RA+&Izt&szv@`*wGb5 zvxV7JY~d`l#stBkzdT8rzaMNNm*sM(tmoh^=`qst0`v^@>b3YiklqJ$qj~=EDLjNfn)*c#RmgDscazG59`PTl6pi(CIjgfNHLK3qdKRYkRR*F*MU_1 zgb}WL-wz3K>W(EA7A@Mv(J~E~7ppSxIrkO;c1G$U-L%9K7q?eeHMbWQ&X%iz>ctjX z7;enhdmI(>t`yZ`1@y`mkHogCvdo#VTULFjKZCMdPU^BkwOlVtSv85)`M6(&%^4>SQ`F1lP+9W3 zsqCdTv|#A*^m<$l>A{fX#2;&L|Gx-⁣ty*3CLcMS?3unlA*0+FM?CLu&AsNei7@ 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

BPu^?ZkMNDJj=x$4ExAGQ4HN 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 - - - - - - - - - \ No newline at end of file From a20fae575e3792b2a748523899b25d89877da63f Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Mon, 5 Sep 2022 15:16:33 -0400 Subject: [PATCH 02/34] Why was this in the wrong project BRO --- VG Music Studio - Core/VG Music Studio - Core.csproj | 2 -- VG Music Studio - WinForms/VG Music Studio - WinForms.csproj | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VG Music Studio - Core/VG Music Studio - Core.csproj b/VG Music Studio - Core/VG Music Studio - Core.csproj index e5af0e5..a60696a 100644 --- a/VG Music Studio - Core/VG Music Studio - Core.csproj +++ b/VG Music Studio - Core/VG Music Studio - Core.csproj @@ -12,8 +12,6 @@ - - diff --git a/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj index e960f0d..7c85a3b 100644 --- a/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj +++ b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj @@ -7,7 +7,7 @@ Kermalis.VGMusicStudio.WinForms enable true - true + true ..\Build Kermalis @@ -20,6 +20,8 @@ + + From e3f0e1482a84aa2fefbcaf86334d2d22bec16456 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Mon, 5 Sep 2022 15:22:11 -0400 Subject: [PATCH 03/34] Middle C should be C5 since MIDI starts at C-1 --- VG Music Studio - Core/Config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VG Music Studio - Core/Config.yaml b/VG Music Studio - Core/Config.yaml index d4bdf7b..bf4d636 100644 --- a/VG Music Studio - Core/Config.yaml +++ b/VG Music Studio - Core/Config.yaml @@ -5,7 +5,7 @@ PanpotIndicators: False # True or False 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 +MiddleCOctave: 5 # 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} From 2cc129374f1572c620855d73fee697dc065abc93 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Mon, 5 Sep 2022 16:15:00 -0400 Subject: [PATCH 04/34] Ignore me I'm dumb --- VG Music Studio - Core/Config.yaml | 4 ++-- VG Music Studio - MIDI/MIDINote.cs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/VG Music Studio - Core/Config.yaml b/VG Music Studio - Core/Config.yaml index bf4d636..67278b6 100644 --- a/VG Music Studio - Core/Config.yaml +++ b/VG Music Studio - Core/Config.yaml @@ -5,8 +5,8 @@ PanpotIndicators: False # True or False 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: 5 # Octave >= --128 and Octave <= 127 # The octave that holds middle C. Used in the visual and track viewer -Colors: +MiddleCOctave: 4 # Octave >= --128 and Octave <= 127 # The octave that holds middle C. Used in the visual and track viewer +Colors: # Each color's H must be >= 0 and <= 239, S must be >= 0 and <= 240, L must be >= 0 and <= 240 0: {H: 185, S: 240, L: 180} 1: {H: 183, S: 240, L: 170} 2: {H: 180, S: 240, L: 157} diff --git a/VG Music Studio - MIDI/MIDINote.cs b/VG Music Studio - MIDI/MIDINote.cs index e7de7bc..f680f33 100644 --- a/VG Music Studio - MIDI/MIDINote.cs +++ b/VG Music Studio - MIDI/MIDINote.cs @@ -62,6 +62,7 @@ public enum MIDINote : byte A_3, Bb_3, B_3, + ///

Middle C C_4, Db_4, D_4, From 2c3b1c34d57df1dde23e66640b383c9463bc1e77 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Mon, 5 Sep 2022 16:49:03 -0400 Subject: [PATCH 05/34] Fix HSL colors --- VG Music Studio - Core/Util/GlobalConfig.cs | 4 +- VG Music Studio - Core/Util/HSLColor.cs | 159 +++++++++----------- VG Music Studio - WinForms/PianoControl.cs | 8 +- VG Music Studio - WinForms/Theme.cs | 2 +- 4 files changed, 82 insertions(+), 91 deletions(-) diff --git a/VG Music Studio - Core/Util/GlobalConfig.cs b/VG Music Studio - Core/Util/GlobalConfig.cs index 106f805..a462b3e 100644 --- a/VG Music Studio - Core/Util/GlobalConfig.cs +++ b/VG Music Studio - Core/Util/GlobalConfig.cs @@ -65,7 +65,7 @@ private GlobalConfig() string valueName = string.Format(Strings.ConfigKeySubkey, string.Format("{0} {1}", nameof(Colors), i)); if (key == "H") { - h = ConfigUtils.ParseValue(valueName, v.Value.ToString(), 0, 240); + h = ConfigUtils.ParseValue(valueName, v.Value.ToString(), 0, 239); } else if (key == "S") { @@ -80,7 +80,7 @@ private GlobalConfig() throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigColorInvalidKey, i))); } } - var co = HSLColor.ToColor((int)h, (byte)s, (byte)l); + var co = HSLColor.ToColor(h / 240.0, s / 240.0, l / 240.0); // h is / 240 even though the max is 239 because H should never be 1.0 Colors[i] = co; Colors[i + 128] = co; } diff --git a/VG Music Studio - Core/Util/HSLColor.cs b/VG Music Studio - Core/Util/HSLColor.cs index ab371dd..d7b6ebe 100644 --- a/VG Music Studio - Core/Util/HSLColor.cs +++ b/VG Music Studio - Core/Util/HSLColor.cs @@ -1,144 +1,135 @@ using System; -using System.Collections.Generic; using System.Drawing; -using System.Linq; 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 public readonly struct HSLColor { - public readonly int H; - public readonly byte S; - public readonly byte L; - - public HSLColor(int h, byte s, byte l) + /// [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) { - H = h; - S = s; - L = l; + Hue = h; + Saturation = s; + Lightness = l; } public HSLColor(in Color c) { - double modifiedR, modifiedG, modifiedB, min, max, delta, h, s, l; + double nR = c.R / 255.0; + double nG = c.G / 255.0; + double nB = c.B / 255.0; - modifiedR = c.R / 255.0; - modifiedG = c.G / 255.0; - modifiedB = 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; - min = new List(3) { modifiedR, modifiedG, modifiedB }.Min(); - max = new List(3) { modifiedR, modifiedG, modifiedB }.Max(); - delta = max - min; - l = (min + max) / 2; + Lightness = (min + max) * 0.5; if (delta == 0) { - h = 0; - s = 0; + Hue = 0; } - else + else if (max == nR) { - s = (l <= 0.5) ? (delta / (min + max)) : (delta / (2 - max - min)); - - if (modifiedR == max) - { - h = (modifiedG - modifiedB) / 6 / delta; - } - else if (modifiedG == max) - { - h = (1.0 / 3) + ((modifiedB - modifiedR) / 6 / delta); - } - else - { - h = (2.0 / 3) + ((modifiedR - modifiedG) / 6 / delta); - } - - h = (h < 0) ? ++h : h; - h = (h > 1) ? --h : h; + 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; } - H = (int)Math.Round(h * 360); - S = (byte)Math.Round(s * 100); - L = (byte)Math.Round(l * 100); + if (delta == 0) + { + Saturation = 0; + } + else + { + Saturation = delta / (1 - Math.Abs((2 * Lightness) - 1)); + } } public Color ToColor() { - return ToColor(H, S, L); + return ToColor(Hue, Saturation, Lightness); } - // https://github.com/iamartyom/ColorHelper/blob/master/ColorHelper/Converter/ColorConverter.cs - public static Color ToColor(int h, byte s, byte l) + public static Color ToColor(double h, double s, double l) { - double modifiedH, modifiedS, modifiedL, - r = 1, g = 1, b = 1, - q, p; - - modifiedH = h / 360.0; - modifiedS = s / 100.0; - modifiedL = l / 100.0; + h *= 360; - q = (modifiedL < 0.5) ? modifiedL * (1 + modifiedS) : modifiedL + modifiedS - modifiedL * modifiedS; - p = 2 * modifiedL - q; + 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 (modifiedL == 0) // If the lightness value is 0 it will always be black + double r; + double g; + double b; + if (h < 60) { - r = 0; - g = 0; + r = c; + g = x; b = 0; } - else if (modifiedS != 0) - { - r = GetHue(p, q, modifiedH + 1.0 / 3); - g = GetHue(p, q, modifiedH); - b = GetHue(p, q, modifiedH - 1.0 / 3); - } - - return Color.FromArgb(255, (byte)Math.Round(r * 255), (byte)Math.Round(g * 255), (byte)Math.Round(b * 255)); - } - private static double GetHue(double p, double q, double t) - { - double value = p; - - if (t < 0) + else if (h < 120) { - t++; + r = x; + g = c; + b = 0; } - else if (t > 1) + else if (h < 180) { - t--; + r = 0; + g = c; + b = x; } - - if (t < 1.0 / 6) + else if (h < 240) { - value = p + (q - p) * 6 * t; + r = 0; + g = x; + b = c; } - else if (t < 1.0 / 2) + else if (h < 300) { - value = q; + r = x; + g = 0; + b = c; } - else if (t < 2.0 / 3) + else // h < 360 { - value = p + (q - p) * (2.0 / 3 - t) * 6; + r = c; + g = 0; + b = x; } - return value; + return Color.FromArgb((int)((r + m) * 255), (int)((g + m) * 255), (int)((b + m) * 255)); } public override bool Equals(object? obj) { if (obj is HSLColor other) { - return H == other.H && S == other.S && L == other.L; + return Hue == other.Hue && Saturation == other.Saturation && Lightness == other.Lightness; } return false; } public override int GetHashCode() { - return HashCode.Combine(H, S, L); + return HashCode.Combine(Hue, Saturation, Lightness); } public override string ToString() { - return $"{H}° {S}% {L}%"; + return $"{Hue * 360}° {Saturation:P} {Lightness:P}"; } public static bool operator ==(HSLColor left, HSLColor right) diff --git a/VG Music Studio - WinForms/PianoControl.cs b/VG Music Studio - WinForms/PianoControl.cs index e7947a3..3629331 100644 --- a/VG Music Studio - WinForms/PianoControl.cs +++ b/VG Music Studio - WinForms/PianoControl.cs @@ -64,23 +64,23 @@ public PianoKey(byte k) SetStyle(ControlStyles.Selectable, false); OnBrush = new(Color.Transparent); - byte l; + double l; if (KeyTypeTable[k % 12] == KeyType.White) { if (k / 12 % 2 == 0) { - l = 240; + l = 1.0; } else { - l = 120; + l = 0.5; } } else { l = 0; } - _offBrush = new SolidBrush(HSLColor.ToColor(160, 0, l)); + _offBrush = new SolidBrush(HSLColor.ToColor(240 / 360.0, 0, l)); } protected override void Dispose(bool disposing) diff --git a/VG Music Studio - WinForms/Theme.cs b/VG Music Studio - WinForms/Theme.cs index 0652802..c8803e6 100644 --- a/VG Music Studio - WinForms/Theme.cs +++ b/VG Music Studio - WinForms/Theme.cs @@ -24,7 +24,7 @@ public static readonly Color public static Color DrainColor(Color c) { var hsl = new HSLColor(c); - return HSLColor.ToColor(hsl.H, (byte)(hsl.S / 2.5), hsl.L); + return HSLColor.ToColor(hsl.Hue, hsl.Saturation / 2.5, hsl.Lightness); } } From 5bc23a07f4b4b73bb7fb93a555ec42c125ebbd9a Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Mon, 5 Sep 2022 17:41:21 -0400 Subject: [PATCH 06/34] Use RGB colors in config --- VG Music Studio - Core/Config.yaml | 258 +++++++++--------- .../Properties/Strings.Designer.cs | 11 +- .../Properties/Strings.es.resx | 3 - .../Properties/Strings.it.resx | 3 - .../Properties/Strings.resx | 4 - VG Music Studio - Core/Util/ConfigUtils.cs | 8 +- VG Music Studio - Core/Util/GlobalConfig.cs | 26 +- VG Music Studio - Core/Util/HSLColor.cs | 15 +- .../VG Music Studio - Core.csproj | 23 +- VG Music Studio - WinForms/PianoControl.cs | 12 +- VG Music Studio - WinForms/SongInfoControl.cs | 15 +- 11 files changed, 183 insertions(+), 195 deletions(-) diff --git a/VG Music Studio - Core/Config.yaml b/VG Music Studio - Core/Config.yaml index 67278b6..c7c809c 100644 --- a/VG Music Studio - Core/Config.yaml +++ b/VG Music Studio - Core/Config.yaml @@ -6,132 +6,132 @@ PlaylistMode: "Random" # "Random" or "Sequential" 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's H must be >= 0 and <= 239, S must be >= 0 and <= 240, L must be >= 0 and <= 240 - 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 +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/Properties/Strings.Designer.cs b/VG Music Studio - Core/Properties/Strings.Designer.cs index 8a481f3..84ff46b 100644 --- a/VG Music Studio - Core/Properties/Strings.Designer.cs +++ b/VG Music Studio - Core/Properties/Strings.Designer.cs @@ -19,7 +19,7 @@ namespace Kermalis.VGMusicStudio.Core.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()] public class Strings { @@ -141,15 +141,6 @@ public static string ErrorBoolParse { } } - /// - /// Looks up a localized string similar to Color {0} has an invalid key.. - /// - public static string ErrorConfigColorInvalidKey { - get { - return ResourceManager.GetString("ErrorConfigColorInvalidKey", resourceCulture); - } - } - /// /// Looks up a localized string similar to Color {0} is not defined.. /// diff --git a/VG Music Studio - Core/Properties/Strings.es.resx b/VG Music Studio - Core/Properties/Strings.es.resx index d98bb26..18b745d 100644 --- a/VG Music Studio - Core/Properties/Strings.es.resx +++ b/VG Music Studio - Core/Properties/Strings.es.resx @@ -243,9 +243,6 @@ "{0}" debe ser Verdadero o Falso. - - El color {0} tiene una clave inválida. - El color {0} no está definido. diff --git a/VG Music Studio - Core/Properties/Strings.it.resx b/VG Music Studio - Core/Properties/Strings.it.resx index 935f17e..47e55a0 100644 --- a/VG Music Studio - Core/Properties/Strings.it.resx +++ b/VG Music Studio - Core/Properties/Strings.it.resx @@ -243,9 +243,6 @@ "{0}" deve essere Vero o Falso. - - Il colore {0} non ha una chiave valida. - Il colore {0} non è definito. diff --git a/VG Music Studio - Core/Properties/Strings.resx b/VG Music Studio - Core/Properties/Strings.resx index 8e4ae4a..267757e 100644 --- a/VG Music Studio - Core/Properties/Strings.resx +++ b/VG Music Studio - Core/Properties/Strings.resx @@ -249,10 +249,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. diff --git a/VG Music Studio - Core/Util/ConfigUtils.cs b/VG Music Studio - Core/Util/ConfigUtils.cs index ff25770..add6d84 100644 --- a/VG Music Studio - Core/Util/ConfigUtils.cs +++ b/VG Music Studio - Core/Util/ConfigUtils.cs @@ -11,6 +11,7 @@ public static class ConfigUtils { public const string PROGRAM_NAME = "VG Music Studio"; private static readonly string[] _notes = Strings.Notes.Split(';'); + private static readonly CultureInfo _enUS = new("en-US"); public static bool TryParseValue(string value, long minValue, long maxValue, out long outValue) { @@ -33,8 +34,7 @@ string GetMessage() return string.Format(Strings.ErrorValueParseRanged, valueName, minValue, maxValue); } - var provider = new CultureInfo("en-US"); - if (value.StartsWith("0x") && long.TryParse(value.AsSpan(2), NumberStyles.HexNumber, provider, out long hexp)) + if (value.StartsWith("0x") && long.TryParse(value.AsSpan(2), NumberStyles.HexNumber, _enUS, out long hexp)) { if (hexp < minValue || hexp > maxValue) { @@ -42,7 +42,7 @@ string GetMessage() } return hexp; } - else if (long.TryParse(value, NumberStyles.Integer, provider, out long dec)) + else if (long.TryParse(value, NumberStyles.Integer, _enUS, out long dec)) { if (dec < minValue || dec > maxValue) { @@ -50,7 +50,7 @@ string GetMessage() } return dec; } - else if (long.TryParse(value, NumberStyles.HexNumber, provider, out long hex)) + else if (long.TryParse(value, NumberStyles.HexNumber, _enUS, out long hex)) { if (hex < minValue || hex > maxValue) { diff --git a/VG Music Studio - Core/Util/GlobalConfig.cs b/VG Music Studio - Core/Util/GlobalConfig.cs index a462b3e..87ee284 100644 --- a/VG Music Studio - Core/Util/GlobalConfig.cs +++ b/VG Music Studio - Core/Util/GlobalConfig.cs @@ -58,29 +58,9 @@ private GlobalConfig() { throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigColorRepeated, i))); } - long 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 = ConfigUtils.ParseValue(valueName, v.Value.ToString(), 0, 239); - } - else if (key == "S") - { - s = ConfigUtils.ParseValue(valueName, v.Value.ToString(), 0, 240); - } - else if (key == "L") - { - l = ConfigUtils.ParseValue(valueName, v.Value.ToString(), 0, 240); - } - else - { - throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigColorInvalidKey, i))); - } - } - var co = HSLColor.ToColor(h / 240.0, s / 240.0, l / 240.0); // h is / 240 even though the max is 239 because H should never be 1.0 + + 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; } diff --git a/VG Music Studio - Core/Util/HSLColor.cs b/VG Music Studio - Core/Util/HSLColor.cs index d7b6ebe..b2b7ee7 100644 --- a/VG Music Studio - Core/Util/HSLColor.cs +++ b/VG Music Studio - Core/Util/HSLColor.cs @@ -5,6 +5,7 @@ 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) @@ -63,7 +64,7 @@ public Color ToColor() { return ToColor(Hue, Saturation, Lightness); } - public static Color ToColor(double h, double s, double l) + public static void ToRGB(double h, double s, double l, out double r, out double g, out double b) { h *= 360; @@ -71,9 +72,6 @@ public static Color ToColor(double h, double s, double l) double x = c * (1 - Math.Abs((h / 60 % 2) - 1)); double m = l - (c * 0.5); - double r; - double g; - double b; if (h < 60) { r = c; @@ -111,7 +109,14 @@ public static Color ToColor(double h, double s, double l) b = x; } - return Color.FromArgb((int)((r + m) * 255), (int)((g + m) * 255), (int)((b + m) * 255)); + 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) diff --git a/VG Music Studio - Core/VG Music Studio - Core.csproj b/VG Music Studio - Core/VG Music Studio - Core.csproj index a60696a..d03f414 100644 --- a/VG Music Studio - Core/VG Music Studio - Core.csproj +++ b/VG Music Studio - Core/VG Music Studio - Core.csproj @@ -16,14 +16,11 @@ - - - Dependencies\DLS2.dll - Dependencies\SoundFont2.dll + Dependencies\SoundFont2.dll @@ -42,4 +39,22 @@ + + + True + True + Strings.resx + + + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + Strings.Designer.cs + + + diff --git a/VG Music Studio - WinForms/PianoControl.cs b/VG Music Studio - WinForms/PianoControl.cs index 3629331..81075ae 100644 --- a/VG Music Studio - WinForms/PianoControl.cs +++ b/VG Music Studio - WinForms/PianoControl.cs @@ -38,7 +38,7 @@ internal sealed class PianoControl : Control private enum KeyType : byte { Black, - White + White, } private const double BLACK_KEY_SCALE = 2.0 / 3; @@ -64,23 +64,23 @@ public PianoKey(byte k) SetStyle(ControlStyles.Selectable, false); OnBrush = new(Color.Transparent); - double l; + byte c; if (KeyTypeTable[k % 12] == KeyType.White) { if (k / 12 % 2 == 0) { - l = 1.0; + c = 255; } else { - l = 0.5; + c = 127; } } else { - l = 0; + c = 0; } - _offBrush = new SolidBrush(HSLColor.ToColor(240 / 360.0, 0, l)); + _offBrush = new SolidBrush(Color.FromArgb(c, c, c)); } protected override void Dispose(bool disposing) diff --git a/VG Music Studio - WinForms/SongInfoControl.cs b/VG Music Studio - WinForms/SongInfoControl.cs index 9b2c9f2..1fda3dd 100644 --- a/VG Music Studio - WinForms/SongInfoControl.cs +++ b/VG Music Studio - WinForms/SongInfoControl.cs @@ -289,12 +289,19 @@ private void DrawVerticalBars(Graphics g, SongState.Track track, int vBarY1, int _barHeight); if (rect.Width > 0) { - float velocity = (track.LeftVolume + track.RightVolume) * 2f; - if (velocity > 1f) + float velocity = track.LeftVolume + track.RightVolume; + int alpha; + if (velocity >= 2f) { - velocity = 1f; + alpha = 255; } - _solidBrush.Color = Color.FromArgb((int)WinFormsUtils.Lerp(velocity, 20f, 255f), color); + else + { + const int DELTA = 100; + alpha = (int)WinFormsUtils.Lerp(velocity, 0f, DELTA); + alpha += 255 - DELTA; + } + _solidBrush.Color = Color.FromArgb(alpha, color); g.FillRectangle(_solidBrush, rect); g.DrawRectangle(_pen, rect); //_solidBrush.Color = color; From a75c611077c36fa0d9d8d1a31bd841b3521e7d28 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Mon, 5 Sep 2022 22:11:16 -0400 Subject: [PATCH 07/34] Fix MIDI tracks overwriting each other --- VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs b/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs index 8e21ee2..57f25b5 100644 --- a/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs +++ b/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs @@ -239,8 +239,11 @@ internal override void Write(EndianBinaryWriter w) } // Update size now - uint size = (uint)(w.Stream.Position - sizeOffset + 4); + long endOffset = w.Stream.Position; + uint size = (uint)(endOffset - sizeOffset + 4); w.Stream.Position = sizeOffset; w.WriteUInt32(size); + + w.Stream.Position = endOffset; // Go back to the end } } From 0729be9997f1551146ef3cec88a415faf972f398 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Mon, 5 Sep 2022 22:18:22 -0400 Subject: [PATCH 08/34] Fix MTrk size --- VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs b/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs index 57f25b5..61fc670 100644 --- a/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs +++ b/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs @@ -240,7 +240,7 @@ internal override void Write(EndianBinaryWriter w) // Update size now long endOffset = w.Stream.Position; - uint size = (uint)(endOffset - sizeOffset + 4); + uint size = (uint)(endOffset - sizeOffset - 4); w.Stream.Position = sizeOffset; w.WriteUInt32(size); From 36ab8af1a783cc4763e8d31b81af46b680824f1b Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Mon, 5 Sep 2022 23:23:20 -0400 Subject: [PATCH 09/34] MIDIs are good now --- VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs | 7 +++++-- VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs | 4 ++-- VG Music Studio - MIDI/Events/NoteOffMessage.cs | 2 +- VG Music Studio - MIDI/Events/NoteOnMessage.cs | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs index 4a9cd2d..4cf0d37 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs @@ -51,6 +51,7 @@ public void SaveAsMIDI(string fileName, MIDISaveArgs args) 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++) @@ -103,13 +104,14 @@ public void SaveAsMIDI(string fileName, MIDISaveArgs args) key = 0x7F; } track.Insert(ticks, new NoteOnMessage(trackIndex, (MIDINote)key, 0)); + //track.Insert(ticks, new NoteOffMessage(trackIndex, (MIDINote)key, 0)); playing.Remove(nc); } break; } case FinishCommand _: { - track.Insert(ticks, new MetaMessage(MetaMessageType.EndOfTrack, Array.Empty())); + endTicks = ticks; goto endOfTrack; } case JumpCommand c: @@ -170,6 +172,7 @@ public void SaveAsMIDI(string fileName, MIDISaveArgs args) if (c.Duration != -1) { track.Insert(ticks + c.Duration, new NoteOnMessage(trackIndex, (MIDINote)note, 0)); + //track.Insert(ticks + c.Duration, new NoteOffMessage(trackIndex, (MIDINote)note, 0)); } else { @@ -243,7 +246,7 @@ public void SaveAsMIDI(string fileName, MIDISaveArgs args) } } endOfTrack: - ; + track.Insert(endTicks ?? track.NumTicks, new MetaMessage(MetaMessageType.EndOfTrack, Array.Empty())); } metaTrack.Insert(metaTrack.NumTicks, new MetaMessage(MetaMessageType.EndOfTrack, Array.Empty())); diff --git a/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs b/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs index 61fc670..63eb0b5 100644 --- a/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs +++ b/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs @@ -55,8 +55,8 @@ internal MIDITrackChunk(uint size, EndianBinaryReader r) byte channel = (byte)(cmd & 0xF); switch (cmd & ~0xF) { - case 0x80: Insert(ticks, new NoteOnMessage(r, channel)); break; - case 0x90: Insert(ticks, new NoteOffMessage(r, channel)); break; + case 0x80: Insert(ticks, new NoteOffMessage(r, channel)); break; + case 0x90: Insert(ticks, new NoteOnMessage(r, channel)); break; case 0xA0: Insert(ticks, new PolyphonicPressureMessage(r, channel)); break; case 0xB0: Insert(ticks, new ControllerMessage(r, channel)); break; case 0xC0: Insert(ticks, new ProgramChangeMessage(r, channel)); break; diff --git a/VG Music Studio - MIDI/Events/NoteOffMessage.cs b/VG Music Studio - MIDI/Events/NoteOffMessage.cs index 15adc01..4408b89 100644 --- a/VG Music Studio - MIDI/Events/NoteOffMessage.cs +++ b/VG Music Studio - MIDI/Events/NoteOffMessage.cs @@ -50,7 +50,7 @@ public NoteOffMessage(byte channel, MIDINote note, byte velocity) internal override byte GetCMDByte() { - return (byte)(0x90 + Channel); + return (byte)(0x80 + Channel); } internal override void Write(EndianBinaryWriter w) diff --git a/VG Music Studio - MIDI/Events/NoteOnMessage.cs b/VG Music Studio - MIDI/Events/NoteOnMessage.cs index 629cfed..2ad63be 100644 --- a/VG Music Studio - MIDI/Events/NoteOnMessage.cs +++ b/VG Music Studio - MIDI/Events/NoteOnMessage.cs @@ -50,7 +50,7 @@ public NoteOnMessage(byte channel, MIDINote note, byte velocity) internal override byte GetCMDByte() { - return (byte)(0x80 + Channel); + return (byte)(0x90 + Channel); } internal override void Write(EndianBinaryWriter w) From 618a405f966a5a835208acd25b068d02bb472502 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Mon, 5 Sep 2022 23:29:40 -0400 Subject: [PATCH 10/34] Fix program icon --- .../VG Music Studio - WinForms.csproj | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj index 7c85a3b..1e811e9 100644 --- a/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj +++ b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj @@ -12,10 +12,12 @@ Kermalis Kermalis - VGMusicStudio - VGMusicStudio - VGMusicStudio + VG Music Studio + VG Music Studio + VG Music Studio 0.3.0 + Properties\Icon.ico + False From 3c2d1d7d1f7634ee2651d211b81b5b27df09c17c Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Wed, 7 Sep 2022 16:42:44 -0400 Subject: [PATCH 11/34] Prevent this from getting optimized out --- VG Music Studio - Core/Assembler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/VG Music Studio - Core/Assembler.cs b/VG Music Studio - Core/Assembler.cs index 5274414..8f64824 100644 --- a/VG Music Studio - Core/Assembler.cs +++ b/VG Music Studio - Core/Assembler.cs @@ -51,7 +51,8 @@ public Assembler(string fileName, int baseOffset, Endianness endianness, Diction _stream = new MemoryStream(); _writer = new EndianBinaryWriter(_stream, endianness: endianness); - Debug.WriteLine(Read(fileName)); + string status = Read(fileName); + Debug.WriteLine(status); SetBaseOffset(baseOffset); } From 568f5db3ed99e7f46ecf785fbc9b4cc8739d7eae Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Thu, 8 Sep 2022 13:58:43 -0400 Subject: [PATCH 12/34] Update VG Music Studio - Core.csproj --- VG Music Studio - Core/VG Music Studio - Core.csproj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/VG Music Studio - Core/VG Music Studio - Core.csproj b/VG Music Studio - Core/VG Music Studio - Core.csproj index d03f414..f82177d 100644 --- a/VG Music Studio - Core/VG Music Studio - Core.csproj +++ b/VG Music Studio - Core/VG Music Studio - Core.csproj @@ -45,12 +45,6 @@ True Strings.resx - - PublicResXFileCodeGenerator - - - PublicResXFileCodeGenerator - PublicResXFileCodeGenerator Strings.Designer.cs From f7157b2910a58acd9ab0fb29ecc716a5bdc41d14 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Thu, 8 Sep 2022 14:00:51 -0400 Subject: [PATCH 13/34] Update ValueTextBox.cs --- VG Music Studio - WinForms/ValueTextBox.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/VG Music Studio - WinForms/ValueTextBox.cs b/VG Music Studio - WinForms/ValueTextBox.cs index 8a69ae5..f05190f 100644 --- a/VG Music Studio - WinForms/ValueTextBox.cs +++ b/VG Music Studio - WinForms/ValueTextBox.cs @@ -6,6 +6,8 @@ namespace Kermalis.VGMusicStudio.WinForms; internal sealed class ValueTextBox : ThemedTextBox { + public event EventHandler? ValueChanged; + private bool _hex = false; public bool Hexadecimal { @@ -91,14 +93,8 @@ protected override void OnTextChanged(EventArgs e) Value = old; } - private EventHandler _onValueChanged = null; - public event EventHandler ValueChanged - { - add => _onValueChanged += value; - remove => _onValueChanged -= value; - } private void OnValueChanged(EventArgs e) { - _onValueChanged?.Invoke(this, e); + ValueChanged?.Invoke(this, e); } } From 5b633cc461df397c3d872daa2354ecaec29bfcfd Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Thu, 8 Sep 2022 14:04:48 -0400 Subject: [PATCH 14/34] Update TrackViewer.cs --- VG Music Studio - WinForms/TrackViewer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VG Music Studio - WinForms/TrackViewer.cs b/VG Music Studio - WinForms/TrackViewer.cs index 80949f3..81520cc 100644 --- a/VG Music Studio - WinForms/TrackViewer.cs +++ b/VG Music Studio - WinForms/TrackViewer.cs @@ -72,7 +72,7 @@ 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]); + Engine.Instance!.Player.SetSongPosition(list[0]); MainForm.Instance.LetUIKnowPlayerIsPlaying(); } } From a6d2ea9f8c002321ce8ffa51ec5ffd2fa2514dc4 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Thu, 8 Sep 2022 14:05:02 -0400 Subject: [PATCH 15/34] Update TimeBarrier.cs --- VG Music Studio - Core/Util/TimeBarrier.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VG Music Studio - Core/Util/TimeBarrier.cs b/VG Music Studio - Core/Util/TimeBarrier.cs index fab0ae5..5379357 100644 --- a/VG Music Studio - Core/Util/TimeBarrier.cs +++ b/VG Music Studio - Core/Util/TimeBarrier.cs @@ -13,9 +13,9 @@ internal sealed class TimeBarrier private double _lastTimeStamp; private bool _started; - public TimeBarrier(double framesPerSecond) + public TimeBarrier(double ticksPerSecond) { - _waitInterval = 1.0 / framesPerSecond; + _waitInterval = 1.0 / ticksPerSecond; _started = false; _sw = new Stopwatch(); _timerInterval = 1.0 / Stopwatch.Frequency; From eead4edf758118ca95076d02a3c0fb40d4553e67 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Thu, 8 Sep 2022 14:07:51 -0400 Subject: [PATCH 16/34] Cache key strings --- VG Music Studio - Core/Util/ConfigUtils.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/VG Music Studio - Core/Util/ConfigUtils.cs b/VG Music Studio - Core/Util/ConfigUtils.cs index add6d84..faeca3d 100644 --- a/VG Music Studio - Core/Util/ConfigUtils.cs +++ b/VG Music Studio - Core/Util/ConfigUtils.cs @@ -10,8 +10,9 @@ namespace Kermalis.VGMusicStudio.Core.Util; public static class ConfigUtils { public const string PROGRAM_NAME = "VG Music Studio"; - private static readonly string[] _notes = Strings.Notes.Split(';'); + private static readonly string[] _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) { @@ -118,9 +119,13 @@ public static string GetNoteName(int note) { return _notes[note]; } - // TODO: Cache results? public static string GetKeyName(int note) { - return _notes[note % 12] + ((note / 12) + (GlobalConfig.Instance.MiddleCOctave - 5)); + if (!_keyCache.TryGetValue(note, out string? str)) + { + str = _notes[note % 12] + ((note / 12) + (GlobalConfig.Instance.MiddleCOctave - 5)); + _keyCache.Add(note, str); + } + return str; } } From 840332e4e752fda8833ffc8129559b917ddaf2a6 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Thu, 8 Sep 2022 14:18:57 -0400 Subject: [PATCH 17/34] Update translations + add Russian/French --- README.md | 2 + .../Properties/Strings.Designer.cs | 18 +- .../Properties/Strings.es.resx | 16 +- .../Properties/Strings.fr.resx | 345 ++++++++++++++++++ .../Properties/Strings.it.resx | 6 +- .../Properties/Strings.resx | 6 +- .../Properties/Strings.ru.resx | 345 ++++++++++++++++++ 7 files changed, 715 insertions(+), 23 deletions(-) create mode 100644 VG Music Studio - Core/Properties/Strings.fr.resx create mode 100644 VG Music Studio - Core/Properties/Strings.ru.resx diff --git a/README.md b/README.md index 90f4177..83a0af9 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/Properties/Strings.Designer.cs b/VG Music Studio - Core/Properties/Strings.Designer.cs index 84ff46b..eea96fb 100644 --- a/VG Music Studio - Core/Properties/Strings.Designer.cs +++ b/VG Music Studio - Core/Properties/Strings.Designer.cs @@ -519,15 +519,6 @@ public static string MenuSaveWAV { } } - /// - /// Looks up a localized string similar to C;C#;D;D#;E;F;F#;G;G#;A;A#;B. - /// - public static string Notes { - get { - return ResourceManager.GetString("Notes", resourceCulture); - } - } - /// /// Looks up a localized string similar to Next Song. /// @@ -645,6 +636,15 @@ public static string PlayPlaylistBody { } } + /// + /// 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}.. /// diff --git a/VG Music Studio - Core/Properties/Strings.es.resx b/VG Music Studio - Core/Properties/Strings.es.resx index 18b745d..c524dfd 100644 --- a/VG Music Studio - Core/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}. @@ -271,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. @@ -342,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 0000000..0befad5 --- /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 - Core/Properties/Strings.it.resx b/VG Music Studio - Core/Properties/Strings.it.resx index 47e55a0..6da605e 100644 --- a/VG Music Studio - Core/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 @@ -342,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 - Core/Properties/Strings.resx b/VG Music Studio - Core/Properties/Strings.resx index 267757e..916279d 100644 --- a/VG Music Studio - Core/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 @@ -369,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 0000000..b35e074 --- /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 From 4a9524fcd64a2b3fd1c57f1ed46356c693454122 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Sun, 11 Sep 2022 19:36:36 -0400 Subject: [PATCH 18/34] Update Assembler.cs --- VG Music Studio - Core/Assembler.cs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/VG Music Studio - Core/Assembler.cs b/VG Music Studio - Core/Assembler.cs index 8f64824..b23b512 100644 --- a/VG Music Studio - Core/Assembler.cs +++ b/VG Music Studio - Core/Assembler.cs @@ -4,17 +4,18 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using static Kermalis.EndianBinaryIO.EndianBinaryPrimitives; namespace Kermalis.VGMusicStudio.Core; internal sealed class Assembler : IDisposable { - private class Pair + private sealed class Pair // Must be a class { public bool Global; public int Offset; } - private class Pointer + private struct Pointer { public string Label; public int BinaryOffset; @@ -65,13 +66,11 @@ public void SetBaseOffset(int baseOffset) // 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 oldPointer = 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); // b will contain {0x04, 0x28, 0x00, 0x00} [0x2804] (SEQ_STUFF+4 + baseOffset) - // Copy the new pointer to binary offset 0x1DF4 - // TODO: UPDATE THESE OLD COMMENTS LOL + _writer.WriteInt32(baseOffset + labelOffset); // Copy the new pointer to binary offset 0x1DF4 } BaseOffset = baseOffset; } @@ -116,7 +115,7 @@ private string Read(string fileName) } bool readingCMD = false; // If it's reading the command - string cmd = null; + string? cmd = null; var args = new List(); string str = string.Empty; foreach (char c in line) @@ -125,7 +124,7 @@ private string Read(string fileName) { break; } - else if (c == '.' && cmd == null) + if (c == '.' && cmd is null) { readingCMD = true; } @@ -157,7 +156,7 @@ private string Read(string fileName) str += c; } } - if (cmd == null) + if (cmd is null) { continue; // Commented line } @@ -192,11 +191,12 @@ private string Read(string fileName) } case "global": { - if (!_labels.ContainsKey(args[0])) + if (!_labels.TryGetValue(args[0], out Pair? pair)) { - _labels.Add(args[0], new Pair()); + pair = new Pair(); + _labels.Add(args[0], pair); } - _labels[args[0]].Global = true; + pair.Global = true; break; } case "align": @@ -285,7 +285,7 @@ private int ParseInt(string value) { return def; } - if (_labels.TryGetValue(value, out Pair pair)) + if (_labels.TryGetValue(value, out Pair? pair)) { _lPointers.Add(new Pointer { Label = value, BinaryOffset = BinaryLength }); return pair.Offset; @@ -359,7 +359,7 @@ private int ParseInt(string value) return ret; } - throw new ArgumentOutOfRangeException(nameof(value)); + throw new ArgumentOutOfRangeException(nameof(value), value, null); } public void Dispose() From aaa7d02cabcdfa1666e21e713e42263efa5eecb1 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Sun, 11 Sep 2022 20:03:27 -0400 Subject: [PATCH 19/34] Fat commit --- VG Music Studio - Core/ADPCMDecoder.cs | 1 + VG Music Studio - Core/Config.cs | 58 +- VG Music Studio - Core/Engine.cs | 2 +- .../GBA/AlphaDream/AlphaDreamChannel.cs | 18 +- .../{Commands.cs => AlphaDreamCommands.cs} | 26 +- .../GBA/AlphaDream/AlphaDreamConfig.cs | 18 +- .../GBA/AlphaDream/AlphaDreamEngine.cs | 4 +- .../GBA/AlphaDream/AlphaDreamEnums.cs | 15 + .../GBA/AlphaDream/AlphaDreamLoadedSong.cs | 41 + .../AlphaDream/AlphaDreamLoadedSong_Events.cs | 266 +++ .../AlphaDreamLoadedSong_Runtime.cs | 160 ++ .../GBA/AlphaDream/AlphaDreamMixer.cs | 18 +- .../GBA/AlphaDream/AlphaDreamPlayer.cs | 733 +------ .../AlphaDreamSoundFontSaver_DLS.cs | 11 +- .../AlphaDreamSoundFontSaver_SF2.cs | 48 +- .../GBA/AlphaDream/AlphaDreamStructs.cs | 67 + .../GBA/AlphaDream/AlphaDreamTrack.cs | 92 + .../GBA/AlphaDream/Enums.cs | 16 - .../GBA/AlphaDream/Structs.cs | 40 - .../GBA/AlphaDream/Track.cs | 69 - VG Music Studio - Core/GBA/GBAUtils.cs | 6 +- VG Music Studio - Core/GBA/MP2K/Enums.cs | 76 - .../GBA/MP2K/{Channel.cs => MP2KChannel.cs} | 348 ++-- .../GBA/MP2K/{Commands.cs => MP2KCommands.cs} | 46 +- VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs | 18 +- VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs | 4 +- VG Music Studio - Core/GBA/MP2K/MP2KEnums.cs | 75 + .../GBA/MP2K/MP2KLoadedSong.cs | 527 +---- .../GBA/MP2K/MP2KLoadedSong_Events.cs | 542 ++++++ .../GBA/MP2K/MP2KLoadedSong_MIDI.cs | 9 +- .../GBA/MP2K/MP2KLoadedSong_Runtime.cs | 458 +++++ VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs | 84 +- VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs | 797 +------- .../GBA/MP2K/MP2KStructs.cs | 187 ++ VG Music Studio - Core/GBA/MP2K/MP2KTrack.cs | 224 +++ VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs | 172 ++ VG Music Studio - Core/GBA/MP2K/Structs.cs | 80 - VG Music Studio - Core/GBA/MP2K/Track.cs | 174 -- VG Music Studio - Core/GBA/MP2K/Utils.cs | 175 -- VG Music Studio - Core/Mixer.cs | 17 +- VG Music Studio - Core/NDS/DSE/Channel.cs | 368 ---- VG Music Studio - Core/NDS/DSE/DSEChannel.cs | 375 ++++ .../NDS/DSE/{Commands.cs => DSECommands.cs} | 30 +- VG Music Studio - Core/NDS/DSE/DSEConfig.cs | 11 +- VG Music Studio - Core/NDS/DSE/DSEEnums.cs | 21 + .../NDS/DSE/DSELoadedSong.cs | 62 + .../NDS/DSE/DSELoadedSong_Events.cs | 461 +++++ .../NDS/DSE/DSELoadedSong_Runtime.cs | 288 +++ VG Music Studio - Core/NDS/DSE/DSEMixer.cs | 45 +- VG Music Studio - Core/NDS/DSE/DSEPlayer.cs | 1063 +--------- VG Music Studio - Core/NDS/DSE/DSETrack.cs | 120 ++ VG Music Studio - Core/NDS/DSE/DSEUtils.cs | 52 + VG Music Studio - Core/NDS/DSE/Enums.cs | 22 - VG Music Studio - Core/NDS/DSE/SMD.cs | 107 +- VG Music Studio - Core/NDS/DSE/SWD.cs | 91 +- VG Music Studio - Core/NDS/DSE/Track.cs | 71 - VG Music Studio - Core/NDS/DSE/Utils.cs | 53 - VG Music Studio - Core/NDS/NDSUtils.cs | 6 + VG Music Studio - Core/NDS/SDAT/Channel.cs | 391 ---- VG Music Studio - Core/NDS/SDAT/Enums.cs | 40 - VG Music Studio - Core/NDS/SDAT/SBNK.cs | 4 +- VG Music Studio - Core/NDS/SDAT/SDAT.cs | 61 +- .../NDS/SDAT/SDATChannel.cs | 394 ++++ .../NDS/SDAT/{Commands.cs => SDATCommands.cs} | 0 VG Music Studio - Core/NDS/SDAT/SDATConfig.cs | 2 +- VG Music Studio - Core/NDS/SDAT/SDATEnums.cs | 39 + .../SDAT/{FileHeader.cs => SDATFileHeader.cs} | 4 +- .../NDS/SDAT/SDATLoadedSong.cs | 38 + .../NDS/SDAT/SDATLoadedSong_Events.cs | 746 +++++++ .../NDS/SDAT/SDATLoadedSong_Runtime.cs | 787 ++++++++ VG Music Studio - Core/NDS/SDAT/SDATMixer.cs | 30 +- VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs | 1713 +---------------- VG Music Studio - Core/NDS/SDAT/SDATTrack.cs | 248 +++ VG Music Studio - Core/NDS/SDAT/SSEQ.cs | 39 +- VG Music Studio - Core/NDS/SDAT/SWAR.cs | 94 +- VG Music Studio - Core/NDS/SDAT/Track.cs | 196 -- VG Music Studio - Core/NDS/Utils.cs | 7 - VG Music Studio - Core/Player.cs | 193 +- VG Music Studio - Core/SongState.cs | 101 +- VG Music Studio - Core/Util/ConfigUtils.cs | 22 + VG Music Studio - Core/Util/DataUtils.cs | 14 + VG Music Studio - Core/Util/LanguageUtils.cs | 30 + VG Music Studio - Core/Util/SampleUtils.cs | 18 +- VG Music Studio - WinForms/MainForm.cs | 586 +++--- VG Music Studio - WinForms/PlayingPlaylist.cs | 49 + VG Music Studio - WinForms/Program.cs | 5 + .../TaskbarPlayerButtons.cs | 81 + .../Util/ColorSlider.cs | 76 +- .../Util/FlexibleMessageBox.cs | 35 +- .../Util/ImageComboBox.cs | 4 +- VG Music Studio - WinForms/Util/VGMSDebug.cs | 24 +- .../Util/WinFormsUtils.cs | 37 + 92 files changed, 7750 insertions(+), 7326 deletions(-) rename VG Music Studio - Core/GBA/AlphaDream/{Commands.cs => AlphaDreamCommands.cs} (77%) create mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEnums.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong_Events.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong_Runtime.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamStructs.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamTrack.cs delete mode 100644 VG Music Studio - Core/GBA/AlphaDream/Enums.cs delete mode 100644 VG Music Studio - Core/GBA/AlphaDream/Structs.cs delete mode 100644 VG Music Studio - Core/GBA/AlphaDream/Track.cs delete mode 100644 VG Music Studio - Core/GBA/MP2K/Enums.cs rename VG Music Studio - Core/GBA/MP2K/{Channel.cs => MP2KChannel.cs} (65%) rename VG Music Studio - Core/GBA/MP2K/{Commands.cs => MP2KCommands.cs} (79%) create mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KEnums.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Events.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Runtime.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KStructs.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KTrack.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs delete mode 100644 VG Music Studio - Core/GBA/MP2K/Structs.cs delete mode 100644 VG Music Studio - Core/GBA/MP2K/Track.cs delete mode 100644 VG Music Studio - Core/GBA/MP2K/Utils.cs delete mode 100644 VG Music Studio - Core/NDS/DSE/Channel.cs create mode 100644 VG Music Studio - Core/NDS/DSE/DSEChannel.cs rename VG Music Studio - Core/NDS/DSE/{Commands.cs => DSECommands.cs} (80%) create mode 100644 VG Music Studio - Core/NDS/DSE/DSEEnums.cs create mode 100644 VG Music Studio - Core/NDS/DSE/DSELoadedSong.cs create mode 100644 VG Music Studio - Core/NDS/DSE/DSELoadedSong_Events.cs create mode 100644 VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs create mode 100644 VG Music Studio - Core/NDS/DSE/DSETrack.cs create mode 100644 VG Music Studio - Core/NDS/DSE/DSEUtils.cs delete mode 100644 VG Music Studio - Core/NDS/DSE/Enums.cs delete mode 100644 VG Music Studio - Core/NDS/DSE/Track.cs delete mode 100644 VG Music Studio - Core/NDS/DSE/Utils.cs create mode 100644 VG Music Studio - Core/NDS/NDSUtils.cs delete mode 100644 VG Music Studio - Core/NDS/SDAT/Channel.cs delete mode 100644 VG Music Studio - Core/NDS/SDAT/Enums.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/SDATChannel.cs rename VG Music Studio - Core/NDS/SDAT/{Commands.cs => SDATCommands.cs} (100%) create mode 100644 VG Music Studio - Core/NDS/SDAT/SDATEnums.cs rename VG Music Studio - Core/NDS/SDAT/{FileHeader.cs => SDATFileHeader.cs} (87%) create mode 100644 VG Music Studio - Core/NDS/SDAT/SDATLoadedSong.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs create mode 100644 VG Music Studio - Core/NDS/SDAT/SDATTrack.cs delete mode 100644 VG Music Studio - Core/NDS/SDAT/Track.cs delete mode 100644 VG Music Studio - Core/NDS/Utils.cs create mode 100644 VG Music Studio - Core/Util/DataUtils.cs create mode 100644 VG Music Studio - Core/Util/LanguageUtils.cs create mode 100644 VG Music Studio - WinForms/PlayingPlaylist.cs create mode 100644 VG Music Studio - WinForms/TaskbarPlayerButtons.cs diff --git a/VG Music Studio - Core/ADPCMDecoder.cs b/VG Music Studio - Core/ADPCMDecoder.cs index f2ee92c..cb2a5d8 100644 --- a/VG Music Studio - Core/ADPCMDecoder.cs +++ b/VG Music Studio - Core/ADPCMDecoder.cs @@ -1,5 +1,6 @@ namespace Kermalis.VGMusicStudio.Core; +// TODO: Struct or something to prevent allocations internal sealed class ADPCMDecoder { private static readonly short[] _indexTable = new short[8] diff --git a/VG Music Studio - Core/Config.cs b/VG Music Studio - Core/Config.cs index f3c34b3..2f26ad5 100644 --- a/VG Music Studio - Core/Config.cs +++ b/VG Music Studio - Core/Config.cs @@ -1,24 +1,32 @@ -using System; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using System; using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; namespace Kermalis.VGMusicStudio.Core; public abstract class Config : IDisposable { - public sealed class Song + public readonly struct Song { - public long Index; - public string Name; + public readonly int Index; + public readonly string Name; - public Song(long index, 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; @@ -37,32 +45,16 @@ public sealed class Playlist public string Name; public List Songs; - public Playlist(string name, IEnumerable songs) + public Playlist(string name, List songs) { Name = name; - Songs = songs.ToList(); + Songs = songs; } public override string ToString() { - int songCount = Songs.Count; - CultureInfo cul = Thread.CurrentThread.CurrentUICulture; - if (cul.TwoLetterISOLanguageName == "it") // Italian - { - // PlaylistName - (1 Canzone) - // PlaylistName - (2 Canzoni) - return $"{Name} - ({songCount} {(songCount == 1 ? "Canzone" : "Canzoni")})"; - } - if (cul.TwoLetterISOLanguageName == "es") // Spanish - { - // PlaylistName - (1 Canción) - // PlaylistName - (2 Canciones) - return $"{Name} - ({songCount} {(songCount == 1 ? "Canción" : "Canciones")})"; - } - // Fallback to en-US - // PlaylistName - (1 Song) - // PlaylistName - (2 Songs) - return $"{Name} - ({songCount} {(songCount == 1 ? "Song" : "Songs")})"; + int num = Songs.Count; + return string.Format("{0} - ({1:N0} {2})", Name, num, LanguageUtils.HandlePlural(num, Strings.Song_s_)); } } @@ -73,7 +65,7 @@ protected Config() Playlists = new List(); } - public Song? GetFirstSong(long index) + public bool TryGetFirstSong(int index, out Song song) { foreach (Playlist p in Playlists) { @@ -81,15 +73,17 @@ protected Config() { if (s.Index == index) { - return s; + song = s; + return true; } } } - return null; + song = default; + return false; } public abstract string GetGameName(); - public abstract string GetSongName(long index); + public abstract string GetSongName(int index); public virtual void Dispose() { diff --git a/VG Music Studio - Core/Engine.cs b/VG Music Studio - Core/Engine.cs index e58922a..a37f0e0 100644 --- a/VG Music Studio - Core/Engine.cs +++ b/VG Music Studio - Core/Engine.cs @@ -8,7 +8,7 @@ public abstract class Engine : IDisposable public abstract Config Config { get; } public abstract Mixer Mixer { get; } - public abstract IPlayer Player { get; } + public abstract Player Player { get; } public virtual void Dispose() { diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamChannel.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamChannel.cs index f799d45..7b4fa85 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamChannel.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamChannel.cs @@ -1,5 +1,5 @@ -using System; -using System.Runtime.InteropServices; +using Kermalis.VGMusicStudio.Core.GBA.MP2K; +using System; namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; @@ -42,13 +42,13 @@ public void SetVolume(byte vol, sbyte pan) public abstract void Process(float[] buffer); } -internal class PCMChannel : AlphaDreamChannel +internal sealed class AlphaDreamPCMChannel : AlphaDreamChannel { private SampleHeader _sampleHeader; private int _sampleOffset; private bool _bFixed; - public PCMChannel(AlphaDreamMixer mixer) : base(mixer) + public AlphaDreamPCMChannel(AlphaDreamMixer mixer) : base(mixer) { // } @@ -60,8 +60,7 @@ public void Init(byte key, ADSR adsr, int sampleOffset, bool bFixed) Key = key; _adsr = adsr; - _sampleHeader = MemoryMarshal.Read(_mixer.Config.ROM.AsSpan(sampleOffset)); - _sampleOffset = sampleOffset + 0x10; + _sampleHeader = new SampleHeader(_mixer.Config.ROM, sampleOffset, out _sampleOffset); _bFixed = bFixed; Stopped = false; } @@ -149,17 +148,18 @@ public override void Process(float[] buffer) } while (--samplesPerBuffer > 0); } } -internal class SquareChannel : AlphaDreamChannel +internal sealed class AlphaDreamSquareChannel : AlphaDreamChannel { private float[] _pat; - public SquareChannel(AlphaDreamMixer mixer) : base(mixer) + public AlphaDreamSquareChannel(AlphaDreamMixer mixer) + : base(mixer) { // } public void Init(byte key, ADSR env, byte vol, sbyte pan, int pitch) { - _pat = MP2K.Utils.SquareD50; // TODO: Which square pattern? + _pat = MP2KUtils.SquareD50; // TODO: Which square pattern? Key = key; _adsr = env; SetVolume(vol, pan); diff --git a/VG Music Studio - Core/GBA/AlphaDream/Commands.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamCommands.cs similarity index 77% rename from VG Music Studio - Core/GBA/AlphaDream/Commands.cs rename to VG Music Studio - Core/GBA/AlphaDream/AlphaDreamCommands.cs index faaee21..b27f327 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/Commands.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamCommands.cs @@ -3,13 +3,13 @@ namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; -internal class FinishCommand : ICommand +internal sealed 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 +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"; @@ -19,7 +19,7 @@ internal class FreeNoteHamtaroCommand : ICommand // TODO: When optimization come public byte Volume { get; set; } public byte Duration { get; set; } } -internal class FreeNoteMLSSCommand : ICommand +internal sealed class FreeNoteMLSSCommand : ICommand { public Color Color => Color.SkyBlue; public string Label => "Free Note"; @@ -28,7 +28,7 @@ internal class FreeNoteMLSSCommand : ICommand public byte Note { get; set; } public byte Duration { get; set; } } -internal class JumpCommand : ICommand +internal sealed class JumpCommand : ICommand { public Color Color => Color.MediumSpringGreen; public string Label => "Jump"; @@ -36,7 +36,7 @@ internal class JumpCommand : ICommand public int Offset { get; set; } } -internal class NoteHamtaroCommand : ICommand +internal sealed class NoteHamtaroCommand : ICommand { public Color Color => Color.SkyBlue; public string Label => "Note"; @@ -46,7 +46,7 @@ internal class NoteHamtaroCommand : ICommand public byte Volume { get; set; } public byte Duration { get; set; } } -internal class NoteMLSSCommand : ICommand +internal sealed class NoteMLSSCommand : ICommand { public Color Color => Color.SkyBlue; public string Label => "Note"; @@ -55,7 +55,7 @@ internal class NoteMLSSCommand : ICommand public byte Note { get; set; } public byte Duration { get; set; } } -internal class PanpotCommand : ICommand +internal sealed class PanpotCommand : ICommand { public Color Color => Color.GreenYellow; public string Label => "Panpot"; @@ -63,7 +63,7 @@ internal class PanpotCommand : ICommand public sbyte Panpot { get; set; } } -internal class PitchBendCommand : ICommand +internal sealed class PitchBendCommand : ICommand { public Color Color => Color.MediumPurple; public string Label => "Pitch Bend"; @@ -71,7 +71,7 @@ internal class PitchBendCommand : ICommand public sbyte Bend { get; set; } } -internal class PitchBendRangeCommand : ICommand +internal sealed class PitchBendRangeCommand : ICommand { public Color Color => Color.MediumPurple; public string Label => "Pitch Bend Range"; @@ -79,7 +79,7 @@ internal class PitchBendRangeCommand : ICommand public byte Range { get; set; } } -internal class RestCommand : ICommand +internal sealed class RestCommand : ICommand { public Color Color => Color.PaleVioletRed; public string Label => "Rest"; @@ -87,7 +87,7 @@ internal class RestCommand : ICommand public byte Rest { get; set; } } -internal class TrackTempoCommand : ICommand +internal sealed class TrackTempoCommand : ICommand { public Color Color => Color.DeepSkyBlue; public string Label => "Track Tempo"; @@ -95,7 +95,7 @@ internal class TrackTempoCommand : ICommand public byte Tempo { get; set; } } -internal class VoiceCommand : ICommand +internal sealed class VoiceCommand : ICommand { public Color Color => Color.DarkSalmon; public string Label => "Voice"; @@ -103,7 +103,7 @@ internal class VoiceCommand : ICommand public byte Voice { get; set; } } -internal class VolumeCommand : ICommand +internal sealed class VolumeCommand : ICommand { public Color Color => Color.SteelBlue; public string Label => "Volume"; diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamConfig.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamConfig.cs index 4858291..750ae76 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamConfig.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamConfig.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using YamlDotNet.Core; using YamlDotNet.RepresentationModel; namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; @@ -113,7 +114,7 @@ void Load(YamlMappingNode gameToLoad) var songs = new List(); foreach (KeyValuePair song in (YamlMappingNode)kvp.Value) { - long songIndex = ConfigUtils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Playlists)), song.Key.ToString(), 0, long.MaxValue); + 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))); @@ -139,7 +140,7 @@ void Load(YamlMappingNode gameToLoad) } AudioEngineVersion = ConfigUtils.ParseEnum(nameof(AudioEngineVersion), audioEngineVersionNode.ToString()); - if (songTableOffsetsNode == null) + if (songTableOffsetsNode is null) { throw new BetterKeyNotFoundException(nameof(SongTableOffsets), null); } @@ -189,7 +190,7 @@ void Load(YamlMappingNode gameToLoad) // 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))); + Playlists.Insert(0, new Playlist(Strings.PlaylistMusic, Playlists.SelectMany(p => p.Songs).Distinct().OrderBy(s => s.Index).ToList())); } } catch (BetterKeyNotFoundException ex) @@ -200,7 +201,7 @@ void Load(YamlMappingNode gameToLoad) { throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + ex.Message)); } - catch (YamlDotNet.Core.YamlException ex) + catch (YamlException ex) { throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + ex.Message)); } @@ -211,10 +212,13 @@ public override string GetGameName() { return Name; } - public override string GetSongName(long index) + public override string GetSongName(int index) { - Song? s = GetFirstSong(index); - return s is not null ? s.Name : index.ToString(); + if (TryGetFirstSong(index, out Song s)) + { + return s.Name; + } + return index.ToString(); } public override void Dispose() diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs index ae021f7..fdee70e 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs @@ -12,9 +12,9 @@ public sealed class AlphaDreamEngine : Engine public AlphaDreamEngine(byte[] rom) { - if (rom.Length > GBAUtils.CartridgeCapacity) + if (rom.Length > GBAUtils.CARTRIDGE_CAPACITY) { - throw new InvalidDataException($"The ROM is too large. Maximum size is 0x{GBAUtils.CartridgeCapacity:X7} bytes."); + throw new InvalidDataException($"The ROM is too large. Maximum size is 0x{GBAUtils.CARTRIDGE_CAPACITY:X7} bytes."); } Config = new AlphaDreamConfig(rom); 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 0000000..3698f7f --- /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/AlphaDreamLoadedSong.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong.cs new file mode 100644 index 0000000..f81833a --- /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 0000000..66ae341 --- /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 0000000..5b71b59 --- /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 index 05c4afd..1cc823c 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs @@ -19,6 +19,8 @@ public sealed class AlphaDreamMixer : Mixer 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; @@ -69,17 +71,7 @@ internal void ResetFade() _fadeMicroFramesLeft = 0; } - private WaveFileWriter? _waveWriter; - public void CreateWaveWriter(string fileName) - { - _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); - } - public void CloseWaveWriter() - { - _waveWriter!.Dispose(); - _waveWriter = null; - } - internal void Process(Track[] tracks, bool output, bool recording) + internal void Process(AlphaDreamTrack[] tracks, bool output, bool recording) { _audio.Clear(); float masterStep; @@ -106,8 +98,8 @@ internal void Process(Track[] tracks, bool output, bool recording) } for (int i = 0; i < AlphaDreamPlayer.NUM_TRACKS; i++) { - Track track = tracks[i]; - if (!track.Enabled || track.NoteDuration == 0 || track.Channel.Stopped || Mutes[i]) + AlphaDreamTrack track = tracks[i]; + if (!track.IsEnabled || track.NoteDuration == 0 || track.Channel.Stopped || Mutes[i]) { continue; } diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs index 6d8f062..e202a38 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs @@ -1,714 +1,167 @@ -using Kermalis.VGMusicStudio.Core.Util; -using System; -using System.Buffers.Binary; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Threading; +using System; +using static System.Buffers.Binary.BinaryPrimitives; namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; -public sealed class AlphaDreamPlayer : IPlayer, ILoadedSong +public sealed class AlphaDreamPlayer : Player { internal const int NUM_TRACKS = 12; // 8 PCM, 4 PSG - private readonly Track[] _tracks = new Track[NUM_TRACKS]; + protected override string Name => "AlphaDream Player"; + + internal readonly AlphaDreamTrack[] Tracks; + internal readonly AlphaDreamConfig Config; private readonly AlphaDreamMixer _mixer; - private readonly AlphaDreamConfig _config; - private readonly TimeBarrier _time; - private Thread? _thread; - private byte _tempo; - private int _tempoStack; - private long _elapsedLoops; + private AlphaDreamLoadedSong? _loadedSong; - public List[] Events { get; private set; } - public long MaxTicks { get; private set; } - public long ElapsedTicks { get; private set; } - public ILoadedSong LoadedSong => this; - public bool ShouldFadeOut { get; set; } - public long NumLoops { get; set; } - private int _longestTrack; + internal byte Tempo; + internal int TempoStack; + private long _elapsedLoops; - public PlayerState State { get; private set; } - public event Action? SongEnded; + public override ILoadedSong? LoadedSong => _loadedSong; + protected override Mixer Mixer => _mixer; internal AlphaDreamPlayer(AlphaDreamConfig config, AlphaDreamMixer mixer) + : base(GBAUtils.AGB_FPS) { - _config = config; + Config = config; _mixer = mixer; + Tracks = new AlphaDreamTrack[NUM_TRACKS]; for (byte i = 0; i < NUM_TRACKS; i++) { - _tracks[i] = new Track(i, mixer); + Tracks[i] = new AlphaDreamTrack(i, mixer); } - - _time = new TimeBarrier(GBAUtils.AGB_FPS); } - private void CreateThread() + + public override void LoadSong(int index) { - _thread = new Thread(Tick) { Name = "AlphaDream Player Tick" }; - _thread.Start(); + 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(); } - private void WaitThread() + public override void UpdateSongState(SongState info) { - if (_thread is not null && (_thread.ThreadState is ThreadState.Running or ThreadState.WaitSleepJoin)) + info.Tempo = Tempo; + for (int i = 0; i < NUM_TRACKS; i++) { - _thread.Join(); + AlphaDreamTrack track = Tracks[i]; + if (track.IsEnabled) + { + track.UpdateSongState(info.Tracks[i]); + } } } - - private void InitEmulation() + 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; + 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(); + Tracks[i].Init(); } } - private void SetTicks() + protected override void SetCurTick(long ticks) { - MaxTicks = 0; - bool u = false; - for (int trackIndex = 0; trackIndex < NUM_TRACKS; 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; - } + _loadedSong!.SetCurTick(ticks); } - public void LoadSong(long index) + protected override void OnStopped() { - _config.Reader.Stream.Position = _config.SongTableOffsets[0] + (index * 4); - int songOffset = _config.Reader.ReadInt32(); - if (songOffset == 0) - { - Events = null; - return; - } - - Events = new List[NUM_TRACKS]; - songOffset -= GBAUtils.CartridgeOffset; - _config.Reader.Stream.Position = songOffset; - ushort trackBits = _config.Reader.ReadUInt16(); - for (byte i = 0, usedTracks = 0; i < NUM_TRACKS; i++) - { - Track track = _tracks[i]; - if ((trackBits & (1 << i)) == 0) - { - track.Enabled = false; - track.StartOffset = 0; - continue; - } - - track.Enabled = true; - Events[i] = new List(); - bool EventExists(long offset) - { - return Events[i].Any(e => e.Offset == offset); - } - - _config.Reader.Stream.Position = songOffset + 2 + (2 * usedTracks++); - AddEvents(track.StartOffset = songOffset + _config.Reader.ReadInt16()); - void AddEvents(int startOffset) - { - _config.Reader.Stream.Position = startOffset; - bool cont = true; - while (cont) - { - long offset = _config.Reader.Stream.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 { Note = (byte)(keyArg - 0x80), Volume = volume, Duration = duration }); - } - break; - } - case AudioEngineVersion.MLSS: - { - byte duration = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new FreeNoteMLSSCommand { Note = (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.Stream.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 >= 0xE0) - { - throw new AlphaDreamInvalidCMDException(i, (int)offset, cmd); - } - - byte key = _config.Reader.ReadByte(); - switch (_config.AudioEngineVersion) - { - case AudioEngineVersion.Hamtaro: - { - byte volume = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new NoteHamtaroCommand { Note = key, Volume = volume, Duration = cmd }); - } - break; - } - case AudioEngineVersion.MLSS: - { - if (!EventExists(offset)) - { - AddEvent(new NoteMLSSCommand { Note = key, Duration = cmd }); - } - break; - } - } - break; - } - } - } - } - } - SetTicks(); + // } - public void SetCurrentPosition(long ticks) + + protected override bool Tick(bool playing, bool recording) { - if (Events == null) + bool allDone = false; // TODO: Individual track tempo + while (!allDone && TempoStack >= 75) { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - if (State == PlayerState.Playing) - { - Pause(); - } - InitEmulation(); - bool u = false; - while (true) + TempoStack -= 75; + allDone = true; + for (int i = 0; i < NUM_TRACKS; i++) { - if (ElapsedTicks == ticks) - { - goto finish; - } - - while (_tempoStack >= 75) + AlphaDreamTrack track = Tracks[i]; + if (track.IsEnabled) { - _tempoStack -= 75; - for (int trackIndex = 0; trackIndex < NUM_TRACKS; 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; - } + TickTrack(track, ref allDone); } - _tempoStack += _tempo; } - finish: - for (int i = 0; i < NUM_TRACKS; i++) + if (_mixer.IsFadeDone()) { - _tracks[i].NoteDuration = 0; + allDone = true; } - Pause(); - } - } - public void Play() - { - if (State is PlayerState.ShutDown or PlayerState.Recording) - { - return; } - - if (Events is null) + if (!allDone) { - SongEnded?.Invoke(); - return; + TempoStack += Tempo; } - - Stop(); - InitEmulation(); - State = PlayerState.Playing; - CreateThread(); + _mixer.Process(Tracks, playing, recording); + return allDone; } - public void Pause() + private void TickTrack(AlphaDreamTrack track, ref bool allDone) { - switch (State) + byte prevDuration = track.NoteDuration; + track.Tick(); + bool update = false; + while (track.Rest == 0 && !track.Stopped) { - case PlayerState.Playing: - { - State = PlayerState.Paused; - WaitThread(); - break; - } - case PlayerState.Paused: - case PlayerState.Stopped: - { - State = PlayerState.Playing; - CreateThread(); - break; - } + _loadedSong!.ExecuteNext(track, ref update); } - } - public void Stop() - { - if (State is PlayerState.Playing or PlayerState.Paused) + if (track.Index == _loadedSong!.LongestTrack) { - State = PlayerState.Stopped; - WaitThread(); + HandleTicksAndLoop(_loadedSong, track); } - } - public void Record(string fileName) - { - _mixer.CreateWaveWriter(fileName); - InitEmulation(); - State = PlayerState.Recording; - CreateThread(); - WaitThread(); - _mixer.CloseWaveWriter(); - } - public void Dispose() - { - if (State != PlayerState.ShutDown) + if (prevDuration == 1 && track.NoteDuration == 0) // Note was not renewed { - State = PlayerState.ShutDown; - WaitThread(); + track.Channel.State = EnvelopeState.Release; } - } - public void UpdateSongState(SongState info) - { - info.Tempo = _tempo; - for (int i = 0; i < NUM_TRACKS; i++) + if (track.NoteDuration != 0) // A note is playing { - Track track = _tracks[i]; - if (!track.Enabled) - { - continue; - } - - SongState.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) + allDone = false; + if (update) { - tin.Keys[0] = track.Channel.Key; - ChannelVolume vol = track.Channel.GetVolume(); - tin.LeftVolume = vol.LeftVol; - tin.RightVolume = vol.RightVol; + track.Channel.SetVolume(track.Volume, track.Panpot); + track.Channel.SetPitch(track.GetPitch()); } - else - { - tin.Keys[0] = byte.MaxValue; - tin.LeftVolume = 0f; - tin.RightVolume = 0f; - } - } - } - - private bool TryGetVoiceEntry(byte voice, byte key, out VoiceEntry e) - { - int vto = _config.VoiceTableOffset; - byte[] rom = _config.ROM; - short voiceOffset = BinaryPrimitives.ReadInt16LittleEndian(rom.AsSpan(vto + (voice * 2))); - short nextVoiceOffset = BinaryPrimitives.ReadInt16LittleEndian(rom.AsSpan(vto + ((voice + 1) * 2))); - if (voiceOffset == nextVoiceOffset) - { - e = default; - return false; } - - int pos = vto + voiceOffset; // Prevent object creation in the last iteration - ref VoiceEntry refE = ref MemoryMarshal.AsRef(rom.AsSpan(pos)); - while (refE.MinKey > key || refE.MaxKey < key) + if (!track.Stopped) { - pos += 8; - if (pos == nextVoiceOffset) - { - e = default; - return false; - } - refE = ref MemoryMarshal.AsRef(rom.AsSpan(pos)); + allDone = false; } - e = refE; - return true; } - private void PlayNote(Track track, byte key, byte duration) + private void HandleTicksAndLoop(AlphaDreamLoadedSong s, AlphaDreamTrack track) { - if (!TryGetVoiceEntry(track.Voice, key, out VoiceEntry entry)) + if (ElapsedTicks != s.MaxTicks) { + ElapsedTicks++; return; } - track.NoteDuration = duration; - if (track.Index >= 8) + // Track reached the detected end, update loops/ticks accordingly + if (track.Stopped) { - // TODO: "Sample" byte in VoiceEntry - var sqr = (SquareChannel)track.Channel; - sqr.Init(key, new ADSR { A = 0xFF, D = 0x00, S = 0xFF, R = 0x00 }, track.Volume, track.Panpot, track.GetPitch()); - } - else - { - int sto = _config.SampleTableOffset; - byte[] rom = _config.ROM; - int sampleOffset = BinaryPrimitives.ReadInt32LittleEndian(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 = (PCMChannel)track.Channel; - pcm.Init(key, new ADSR { A = 0xFF, D = 0x00, S = 0xFF, R = 0x00 }, sto + sampleOffset, entry.IsFixedFrequency == 0x80); - pcm.SetVolume(track.Volume, track.Panpot); - pcm.SetPitch(track.GetPitch()); - } - } - private void ExecuteNext(Track track, ref bool update) - { - byte[] rom = _config.ROM; - byte cmd = rom[track.DataOffset++]; - switch (cmd) - { - case 0x00: // Free Note - { - byte note = (byte)(rom[track.DataOffset++] - 0x80); - if (_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 (_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 + BinaryPrimitives.ReadInt16LittleEndian(rom.AsSpan(track.DataOffset, 2)); - break; - } - case 0xF9: // Track Tempo - { - _tempo = rom[track.DataOffset++]; - break; - } - case 0xFF: // Finish - { - track.Stopped = true; - break; - } - default: throw new AlphaDreamInvalidCMDException(track.Index, track.DataOffset - 1, cmd); + return; } - track.PrevCommand = cmd; - } - - private void Tick() - { - _time.Start(); - while (true) + _elapsedLoops++; + UpdateElapsedTicksAfterLoop(s.Events[track.Index]!, track.DataOffset, track.Rest); + if (ShouldFadeOut && _elapsedLoops > NumLoops && !_mixer.IsFading()) { - PlayerState state = State; - bool playing = state == PlayerState.Playing; - bool recording = state == PlayerState.Recording; - if (!playing && !recording) - { - break; - } - - while (_tempoStack >= 75) - { - _tempoStack -= 75; - bool allDone = true; - for (int trackIndex = 0; trackIndex < NUM_TRACKS; 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) - { - // TODO: lock state - _mixer.Process(_tracks, playing, recording); - _time.Stop(); - State = PlayerState.Stopped; - SongEnded?.Invoke(); - return; - } - } - _tempoStack += _tempo; - _mixer.Process(_tracks, playing, recording); - if (playing) - { - _time.Wait(); - } + _mixer.BeginFadeOut(); } - _time.Stop(); } } diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_DLS.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_DLS.cs index f9f351f..606f9b5 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_DLS.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_DLS.cs @@ -4,7 +4,6 @@ using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; -using System.Runtime.InteropServices; namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; @@ -55,7 +54,7 @@ private static void AddInfo(AlphaDreamConfig config, DLS dls) } ofs += config.SampleTableOffset; - ref SampleHeader sh = ref MemoryMarshal.AsRef(config.ROM.AsSpan(ofs)); + var sh = new SampleHeader(config.ROM, ofs, out int sampleOffset); // Create format chunk var fmt = new FormatChunk(WaveFormat.PCM); @@ -81,7 +80,7 @@ private static void AddInfo(AlphaDreamConfig config, DLS dls) } // Get PCM sample byte[] pcm = new byte[sh.Length]; - Array.Copy(config.ROM, ofs + 0x10, pcm, 0, sh.Length); + Array.Copy(config.ROM, sampleOffset, pcm, 0, sh.Length); // Add int dlsIndex = waves.Count; @@ -127,7 +126,7 @@ private static void AddInstruments(AlphaDreamConfig config, DLS dls, Dictionary< lins.Add(ins); for (int e = 0; e < numEntries; e++) { - ref VoiceEntry entry = ref MemoryMarshal.AsRef(config.ROM.AsSpan(config.VoiceTableOffset + off + (e * 8))); + ref readonly var entry = ref VoiceEntry.Get(config.ROM.AsSpan(config.VoiceTableOffset + off + (e * 8))); // Sample if (entry.Sample >= config.SampleTableSize) { @@ -140,7 +139,7 @@ private static void AddInstruments(AlphaDreamConfig config, DLS dls, Dictionary< continue; } - void Add(ushort low, ushort high, ushort baseKey) + void Add(ushort low, ushort high, ushort baseNote) { var rgnh = new RegionHeaderChunk(); rgnh.KeyRange.Low = low; @@ -150,7 +149,7 @@ void Add(ushort low, ushort high, ushort baseKey) rgnh, new WaveSampleChunk { - UnityNote = baseKey, + UnityNote = baseNote, Options = WaveSampleOptions.NoTruncation | WaveSampleOptions.NoCompression, Loop = value.Item1.Loop, }, diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_SF2.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_SF2.cs index 6ed9a2c..30c0529 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_SF2.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamSoundFontSaver_SF2.cs @@ -1,57 +1,63 @@ using Kermalis.SoundFont2; using Kermalis.VGMusicStudio.Core.Util; using System; -using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; -using System.Runtime.InteropServices; +using static System.Buffers.Binary.BinaryPrimitives; namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; public static class AlphaDreamSoundFontSaver_SF2 { - public static void Save(AlphaDreamConfig config, string path) + 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(config, sf2.InfoChunk); - Dictionary sampleDict = AddSamples(config, sf2); - AddInstruments(config, sf2, sampleDict); + AddInfo(romName, sf2.InfoChunk); + Dictionary sampleDict = AddSamples(rom, sampleTableOffset, sampleTableSize, sf2); + AddInstruments(rom, voiceTableOffset, sampleTableSize, sf2, sampleDict); sf2.Save(path); } - private static void AddInfo(AlphaDreamConfig config, InfoListChunk chunk) + private static void AddInfo(string romName, InfoListChunk chunk) { - chunk.Bank = config.Name; + chunk.Bank = romName; //chunk.Copyright = config.Creator; chunk.Tools = ConfigUtils.PROGRAM_NAME + " by Kermalis"; } - private static Dictionary AddSamples(AlphaDreamConfig config, SF2 sf2) + private static Dictionary AddSamples(byte[] rom, int sampleTableOffset, int sampleTableSize, SF2 sf2) { - var sampleDict = new Dictionary((int)config.SampleTableSize); - for (int i = 0; i < config.SampleTableSize; i++) + var sampleDict = new Dictionary(sampleTableSize); + for (int i = 0; i < sampleTableSize; i++) { - int ofs = BinaryPrimitives.ReadInt32LittleEndian(config.ROM.AsSpan(config.SampleTableOffset + (i * 4))); + int ofs = ReadInt32LittleEndian(rom.AsSpan(sampleTableOffset + (i * 4))); if (ofs == 0) { continue; } - ofs += config.SampleTableOffset; - ref SampleHeader sh = ref MemoryMarshal.AsRef(config.ROM.AsSpan(ofs)); + ofs += sampleTableOffset; + var sh = new SampleHeader(rom, ofs, out int sampleOffset); - short[] pcm16 = SampleUtils.PCMU8ToPCM16(config.ROM.AsSpan(ofs + 0x10, sh.Length)); + 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(AlphaDreamConfig config, SF2 sf2, Dictionary sampleDict) + private static void AddInstruments(byte[] rom, int voiceTableOffset, int sampleTableSize, SF2 sf2, Dictionary sampleDict) { for (ushort 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))); + 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) { @@ -64,10 +70,10 @@ private static void AddInstruments(AlphaDreamConfig config, SF2 sf2, Dictionary< sf2.AddPresetGenerator(SF2Generator.Instrument, new SF2GeneratorAmount { Amount = (short)sf2.AddInstrument(name) }); for (int e = 0; e < numEntries; e++) { - ref VoiceEntry entry = ref MemoryMarshal.AsRef(config.ROM.AsSpan(config.VoiceTableOffset + off + (e * 8))); + ref readonly var entry = ref VoiceEntry.Get(rom.AsSpan(voiceTableOffset + off + (e * 8))); sf2.AddInstrumentBag(); // Key range - if (!(entry.MinKey == 0 && entry.MaxKey == 0x7F)) + if (entry.MinKey != 0 || entry.MaxKey != 0x7F) { sf2.AddInstrumentGenerator(SF2Generator.KeyRange, new SF2GeneratorAmount { LowByte = entry.MinKey, HighByte = entry.MaxKey }); } @@ -77,7 +83,7 @@ private static void AddInstruments(AlphaDreamConfig config, SF2 sf2, Dictionary< sf2.AddInstrumentGenerator(SF2Generator.ScaleTuning, new SF2GeneratorAmount { Amount = 0 }); } // Sample - if (entry.Sample < config.SampleTableSize) + if (entry.Sample < sampleTableSize) { if (!sampleDict.TryGetValue(entry.Sample, out (SampleHeader, int) value)) { 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 0000000..720c72e --- /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 0000000..ded6a34 --- /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/Enums.cs b/VG Music Studio - Core/GBA/AlphaDream/Enums.cs deleted file mode 100644 index bca47c4..0000000 --- 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/Structs.cs b/VG Music Studio - Core/GBA/AlphaDream/Structs.cs deleted file mode 100644 index dcd8832..0000000 --- a/VG Music Studio - Core/GBA/AlphaDream/Structs.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; - -[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 16)] -internal struct SampleHeader -{ - 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; -} -[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 8)] -internal struct VoiceEntry -{ - public const byte FIXED_FREQ_TRUE = 0x80; - - public byte MinKey; - public byte MaxKey; - public byte Sample; - /// 0x80 if True - public byte IsFixedFrequency; - public byte Unknown1; - public byte Unknown2; - public byte Unknown3; - public byte Unknown4; -} - -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 d9401fc..0000000 --- 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 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 Enabled; - public bool Stopped; - public int StartOffset; - public int DataOffset; - public byte PrevCommand; - - public int GetPitch() - { - return PitchBend * (PitchBendRange / 2); - } - - public Track(byte i, AlphaDreamMixer mixer) - { - Index = i; - if (i >= 8) - { - Type = GBAUtils.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/GBAUtils.cs b/VG Music Studio - Core/GBA/GBAUtils.cs index 5106acf..c59b491 100644 --- a/VG Music Studio - Core/GBA/GBAUtils.cs +++ b/VG Music Studio - Core/GBA/GBAUtils.cs @@ -3,10 +3,10 @@ internal static class GBAUtils { public const double AGB_FPS = 59.7275; - public const int SystemClock = 16_777_216; // 16.777216 MHz (16*1024*1024 Hz) + public const int SYSTEM_CLOCK = 16_777_216; // 16.777216 MHz (16*1024*1024 Hz) - public const int CartridgeOffset = 0x08_000_000; - public const int CartridgeCapacity = 0x02_000_000; + public const int CARTRIDGE_OFFSET = 0x08_000_000; + public const int CARTRIDGE_CAPACITY = 0x02_000_000; public static readonly string[] PSGTypes = new string[4] { "Square 1", "Square 2", "PCM4", "Noise" }; } 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 cb169c6..0000000 --- a/VG Music Studio - Core/GBA/MP2K/Enums.cs +++ /dev/null @@ -1,76 +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 - /// 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/Channel.cs b/VG Music Studio - Core/GBA/MP2K/MP2KChannel.cs similarity index 65% rename from VG Music Studio - Core/GBA/MP2K/Channel.cs rename to VG Music Studio - Core/GBA/MP2K/MP2KChannel.cs index f25482d..9f32498 100644 --- a/VG Music Studio - Core/GBA/MP2K/Channel.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KChannel.cs @@ -1,16 +1,15 @@ using System; using System.Collections; -using System.Runtime.InteropServices; namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; -internal abstract class Channel +internal abstract class MP2KChannel { public EnvelopeState State = EnvelopeState.Dead; - public Track? Owner; + public MP2KTrack? Owner; protected readonly MP2KMixer _mixer; - public NoteInfo Note; // Must be a struct & field + public NoteInfo Note; protected ADSR _adsr; protected int _instPan; @@ -19,7 +18,7 @@ internal abstract class Channel protected float _interPos; protected float _frequency; - protected Channel(MP2KMixer mixer) + protected MP2KChannel(MP2KMixer mixer) { _mixer = mixer; } @@ -40,39 +39,33 @@ public virtual void Release() // Returns whether the note is active or not public virtual bool TickNote() { - if (State < EnvelopeState.Releasing) + if (State >= EnvelopeState.Releasing) { - if (Note.Duration > 0) - { - Note.Duration--; - if (Note.Duration == 0) - { - State = EnvelopeState.Releasing; - return false; - } - return true; - } - else - { - return true; - } + return false; } - else + + if (Note.Duration > 0) { - return false; + Note.Duration--; + if (Note.Duration == 0) + { + State = EnvelopeState.Releasing; + return false; + } } + return true; } public void Stop() { State = EnvelopeState.Dead; - if (Owner != null) + if (Owner is not null) { Owner.Channels.Remove(this); } Owner = null; } } -internal class PCM8Channel : Channel +internal sealed class MP2KPCM8Channel : MP2KChannel { private SampleHeader _sampleHeader; private int _sampleOffset; @@ -84,11 +77,16 @@ internal class PCM8Channel : Channel private byte _rightVol; private sbyte[]? _decompressedSample; - public PCM8Channel(MP2KMixer mixer) : base(mixer) { } - public void Init(Track owner, NoteInfo note, ADSR adsr, int sampleOffset, byte vol, sbyte pan, int instPan, int pitch, bool bFixed, bool bCompressed) + 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; + _pos = 0; + _interPos = 0; if (Owner is not null) { Owner.Channels.Remove(this); @@ -99,15 +97,14 @@ public void Init(Track owner, NoteInfo note, ADSR adsr, int sampleOffset, byte v _adsr = adsr; _instPan = instPan; byte[] rom = _mixer.Config.ROM; - _sampleHeader = MemoryMarshal.Read(rom.AsSpan(sampleOffset)); - _sampleOffset = sampleOffset + 0x10; + _sampleHeader = SampleHeader.Get(rom, sampleOffset, out _sampleOffset); _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; + _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 = MemoryMarshal.Read(rom.AsSpan(_sampleOffset)); + _gsPSG = GoldenSunPSG.Get(rom.AsSpan(_sampleOffset)); } SetVolume(vol, pan); SetPitch(pitch); @@ -115,11 +112,11 @@ public void Init(Track owner, NoteInfo note, ADSR adsr, int sampleOffset, byte v public override ChannelVolume GetVolume() { - const float max = 0x10000; + const float MAX = 0x10_000; return new ChannelVolume { - LeftVol = _leftVol * _velocity / max * _mixer.PCM8MasterVolume, - RightVol = _rightVol * _velocity / max * _mixer.PCM8MasterVolume + LeftVol = _leftVol * _velocity / MAX * _mixer.PCM8MasterVolume, + RightVol = _rightVol * _velocity / MAX * _mixer.PCM8MasterVolume }; } public override void SetVolume(byte vol, sbyte pan) @@ -222,134 +219,149 @@ public override void Process(float[] buffer) 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) + 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: { - 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 { - _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) { - float samp = _interPos < threshold ? 0.5f : -0.5f; - samp += 0.5f - threshold; - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; + _interPos--; + } + } while (--samplesPerBuffer > 0); + break; + } + case GoldenSunPSGType.Saw: + { + const int FIX = 0x70; - _interPos += interStep; - if (_interPos >= 1) - { - _interPos--; - } - } while (--samplesPerBuffer > 0); - break; - } - case GoldenSunPSGType.Saw: + int bufPos = 0; + int samplesPerBuffer = _mixer.SamplesPerBuffer; + do { - const int fix = 0x70; - - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do + _interPos += interStep; + if (_interPos >= 1) { - _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); + _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; + float samp = _pos / (float)0x100; - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - } while (--samplesPerBuffer > 0); - break; - } - case GoldenSunPSGType.Triangle: + 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 { - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do + _interPos += interStep; + if (_interPos >= 1) { - _interPos += interStep; - if (_interPos >= 1) - { - _interPos--; - } - float samp = _interPos < 0.5f ? (_interPos * 4) - 1 : 3 - (_interPos * 4); + _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; - } + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + } while (--samplesPerBuffer > 0); + break; } } - else if (_bCompressed) + } + private void Process_Compressed(float[] buffer, ChannelVolume vol, float interStep) + { + int bufPos = 0; + int samplesPerBuffer = _mixer.SamplesPerBuffer; + do { - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = _decompressedSample![_pos] / (float)0x80; + float samp = _decompressedSample![_pos] / (float)0x80; - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; + 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 + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos += posDelta; + if (_pos >= _decompressedSample.Length) { - float samp = (sbyte)_mixer.Config.ROM[_pos + _sampleOffset] / (float)0x80; + 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; + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos += posDelta; - if (_pos >= _sampleHeader.Length) + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos += posDelta; + if (_pos >= _sampleHeader.Length) + { + if (_sampleHeader.DoesLoop != SampleHeader.LOOP_TRUE) { - if (_sampleHeader.DoesLoop == 0x40000000) - { - _pos = _sampleHeader.LoopOffset; - } - else - { - Stop(); - break; - } + Stop(); + return; } - } while (--samplesPerBuffer > 0); - } + + _pos = _sampleHeader.LoopOffset; + } + } while (--samplesPerBuffer > 0); } } -internal abstract class PSGChannel : Channel +internal abstract class MP2KPSGChannel : MP2KChannel { protected enum GBPan : byte { Left, Center, - Right + Right, } private byte _processStep; @@ -358,11 +370,15 @@ protected enum GBPan : byte private byte _sustainVelocity; protected GBPan _panpot = GBPan.Center; - public PSGChannel(MP2KMixer mixer) : base(mixer) { } - protected void Init(Track owner, NoteInfo note, ADSR env, int instPan) + public MP2KPSGChannel(MP2KMixer mixer) + : base(mixer) + { + // + } + protected void Init(MP2KTrack owner, NoteInfo note, ADSR env, int instPan) { State = EnvelopeState.Initializing; - if (Owner != null) + if (Owner is not null) { Owner.Channels.Remove(this); } @@ -627,24 +643,25 @@ void rel() } } } -internal class SquareChannel : PSGChannel +internal sealed class MP2KSquareChannel : MP2KPSGChannel { - private float[] _pat; + private float[]? _pat; - public SquareChannel(MP2KMixer mixer) : base(mixer) + public MP2KSquareChannel(MP2KMixer mixer) + : base(mixer) { // } - public void Init(Track owner, NoteInfo note, ADSR env, int instPan, SquarePattern pattern) + public void Init(MP2KTrack owner, NoteInfo note, ADSR env, int instPan, SquarePattern pattern) { Init(owner, note, env, instPan); - switch (pattern) + _pat = pattern switch { - 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; - } + SquarePattern.D12 => MP2KUtils.SquareD12, + SquarePattern.D25 => MP2KUtils.SquareD25, + SquarePattern.D50 => MP2KUtils.SquareD50, + _ => MP2KUtils.SquareD75, + }; } public override void SetPitch(int pitch) @@ -663,10 +680,11 @@ public override void Process(float[] buffer) ChannelVolume vol = GetVolume(); float interStep = _frequency * _mixer.SampleRateReciprocal; - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + int bufPos = 0; + int samplesPerBuffer = _mixer.SamplesPerBuffer; do { - float samp = _pat[_pos]; + float samp = _pat![_pos]; buffer[bufPos++] += samp * vol.LeftVol; buffer[bufPos++] += samp * vol.RightVol; @@ -678,18 +696,19 @@ public override void Process(float[] buffer) } while (--samplesPerBuffer > 0); } } -internal class PCM4Channel : PSGChannel +internal sealed class MP2KPCM4Channel : MP2KPSGChannel { - private float[] _sample; + private readonly float[] _sample; - public PCM4Channel(MP2KMixer mixer) : base(mixer) + public MP2KPCM4Channel(MP2KMixer mixer) + : base(mixer) { - // + _sample = new float[0x20]; } - public void Init(Track owner, NoteInfo note, ADSR env, int instPan, int sampleOffset) + public void Init(MP2KTrack owner, NoteInfo note, ADSR env, int instPan, int sampleOffset) { Init(owner, note, env, instPan); - _sample = Utils.PCM4ToFloat(sampleOffset); + MP2KUtils.PCM4ToFloat(_mixer.Config.ROM.AsSpan(sampleOffset), _sample); } public override void SetPitch(int pitch) @@ -708,7 +727,8 @@ public override void Process(float[] buffer) ChannelVolume vol = GetVolume(); float interStep = _frequency * _mixer.SampleRateReciprocal; - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + int bufPos = 0; + int samplesPerBuffer = _mixer.SamplesPerBuffer; do { float samp = _sample[_pos]; @@ -723,18 +743,19 @@ public override void Process(float[] buffer) } while (--samplesPerBuffer > 0); } } -internal class NoiseChannel : PSGChannel +internal sealed class MP2KNoiseChannel : MP2KPSGChannel { private BitArray _pat; - public NoiseChannel(MP2KMixer mixer) : base(mixer) + public MP2KNoiseChannel(MP2KMixer mixer) + : base(mixer) { // } - public void Init(Track owner, NoteInfo note, ADSR env, int instPan, NoisePattern pattern) + public void Init(MP2KTrack owner, NoteInfo note, ADSR env, int instPan, NoisePattern pattern) { Init(owner, note, env, instPan); - _pat = pattern == NoisePattern.Fine ? Utils.NoiseFine : Utils.NoiseRough; + _pat = pattern == NoisePattern.Fine ? MP2KUtils.NoiseFine : MP2KUtils.NoiseRough; } public override void SetPitch(int pitch) @@ -752,7 +773,7 @@ public override void SetPitch(int pitch) key = 59; } } - byte v = Utils.NoiseFrequencyTable[key]; + 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 @@ -770,7 +791,8 @@ public override void Process(float[] buffer) ChannelVolume vol = GetVolume(); float interStep = _frequency * _mixer.SampleRateReciprocal; - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + int bufPos = 0; + int samplesPerBuffer = _mixer.SamplesPerBuffer; do { float samp = _pat[_pos & (_pat.Length - 1)] ? 0.5f : -0.5f; diff --git a/VG Music Studio - Core/GBA/MP2K/Commands.cs b/VG Music Studio - Core/GBA/MP2K/MP2KCommands.cs similarity index 79% rename from VG Music Studio - Core/GBA/MP2K/Commands.cs rename to VG Music Studio - Core/GBA/MP2K/MP2KCommands.cs index 5778eec..4959a5b 100644 --- a/VG Music Studio - Core/GBA/MP2K/Commands.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KCommands.cs @@ -3,7 +3,7 @@ namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; -internal class CallCommand : ICommand +internal sealed class CallCommand : ICommand { public Color Color => Color.MediumSpringGreen; public string Label => "Call"; @@ -11,7 +11,7 @@ internal class CallCommand : ICommand public int Offset { get; set; } } -internal class EndOfTieCommand : ICommand +internal sealed class EndOfTieCommand : ICommand { public Color Color => Color.SkyBlue; public string Label => "End Of Tie"; @@ -19,7 +19,7 @@ internal class EndOfTieCommand : ICommand public int Note { get; set; } } -internal class FinishCommand : ICommand +internal sealed class FinishCommand : ICommand { public Color Color => Color.MediumSpringGreen; public string Label => "Finish"; @@ -27,7 +27,7 @@ internal class FinishCommand : ICommand public bool Prev { get; set; } } -internal class JumpCommand : ICommand +internal sealed class JumpCommand : ICommand { public Color Color => Color.MediumSpringGreen; public string Label => "Jump"; @@ -35,7 +35,7 @@ internal class JumpCommand : ICommand public int Offset { get; set; } } -internal class LFODelayCommand : ICommand +internal sealed class LFODelayCommand : ICommand { public Color Color => Color.LightSteelBlue; public string Label => "LFO Delay"; @@ -43,7 +43,7 @@ internal class LFODelayCommand : ICommand public byte Delay { get; set; } } -internal class LFODepthCommand : ICommand +internal sealed class LFODepthCommand : ICommand { public Color Color => Color.LightSteelBlue; public string Label => "LFO Depth"; @@ -51,7 +51,7 @@ internal class LFODepthCommand : ICommand public byte Depth { get; set; } } -internal class LFOSpeedCommand : ICommand +internal sealed class LFOSpeedCommand : ICommand { public Color Color => Color.LightSteelBlue; public string Label => "LFO Speed"; @@ -59,7 +59,7 @@ internal class LFOSpeedCommand : ICommand public byte Speed { get; set; } } -internal class LFOTypeCommand : ICommand +internal sealed class LFOTypeCommand : ICommand { public Color Color => Color.LightSteelBlue; public string Label => "LFO Type"; @@ -67,7 +67,7 @@ internal class LFOTypeCommand : ICommand public LFOType Type { get; set; } } -internal class LibraryCommand : ICommand +internal sealed class LibraryCommand : ICommand { public Color Color => Color.SteelBlue; public string Label => "Library Call"; @@ -76,7 +76,7 @@ internal class LibraryCommand : ICommand public byte Command { get; set; } public byte Argument { get; set; } } -internal class MemoryAccessCommand : ICommand +internal sealed class MemoryAccessCommand : ICommand { public Color Color => Color.SteelBlue; public string Label => "Memory Access"; @@ -86,7 +86,7 @@ internal class MemoryAccessCommand : ICommand public byte Address { get; set; } public byte Data { get; set; } } -internal class NoteCommand : ICommand +internal sealed class NoteCommand : ICommand { public Color Color => Color.SkyBlue; public string Label => "Note"; @@ -96,7 +96,7 @@ internal class NoteCommand : ICommand public byte Velocity { get; set; } public int Duration { get; set; } } -internal class PanpotCommand : ICommand +internal sealed class PanpotCommand : ICommand { public Color Color => Color.GreenYellow; public string Label => "Panpot"; @@ -104,7 +104,7 @@ internal class PanpotCommand : ICommand public sbyte Panpot { get; set; } } -internal class PitchBendCommand : ICommand +internal sealed class PitchBendCommand : ICommand { public Color Color => Color.MediumPurple; public string Label => "Pitch Bend"; @@ -112,7 +112,7 @@ internal class PitchBendCommand : ICommand public sbyte Bend { get; set; } } -internal class PitchBendRangeCommand : ICommand +internal sealed class PitchBendRangeCommand : ICommand { public Color Color => Color.MediumPurple; public string Label => "Pitch Bend Range"; @@ -120,7 +120,7 @@ internal class PitchBendRangeCommand : ICommand public byte Range { get; set; } } -internal class PriorityCommand : ICommand +internal sealed class PriorityCommand : ICommand { public Color Color => Color.SteelBlue; public string Label => "Priority"; @@ -128,7 +128,7 @@ internal class PriorityCommand : ICommand public byte Priority { get; set; } } -internal class RepeatCommand : ICommand +internal sealed class RepeatCommand : ICommand { public Color Color => Color.MediumSpringGreen; public string Label => "Repeat"; @@ -137,7 +137,7 @@ internal class RepeatCommand : ICommand public byte Times { get; set; } public int Offset { get; set; } } -internal class RestCommand : ICommand +internal sealed class RestCommand : ICommand { public Color Color => Color.PaleVioletRed; public string Label => "Rest"; @@ -145,13 +145,13 @@ internal class RestCommand : ICommand public byte Rest { get; set; } } -internal class ReturnCommand : ICommand +internal sealed class ReturnCommand : ICommand { public Color Color => Color.MediumSpringGreen; public string Label => "Return"; public string Arguments => string.Empty; } -internal class TempoCommand : ICommand +internal sealed class TempoCommand : ICommand { public Color Color => Color.DeepSkyBlue; public string Label => "Tempo"; @@ -159,7 +159,7 @@ internal class TempoCommand : ICommand public ushort Tempo { get; set; } } -internal class TransposeCommand : ICommand +internal sealed class TransposeCommand : ICommand { public Color Color => Color.SkyBlue; public string Label => "Transpose"; @@ -167,7 +167,7 @@ internal class TransposeCommand : ICommand public sbyte Transpose { get; set; } } -internal class TuneCommand : ICommand +internal sealed class TuneCommand : ICommand { public Color Color => Color.MediumPurple; public string Label => "Fine Tune"; @@ -175,7 +175,7 @@ internal class TuneCommand : ICommand public sbyte Tune { get; set; } } -internal class VoiceCommand : ICommand +internal sealed class VoiceCommand : ICommand { public Color Color => Color.DarkSalmon; public string Label => "Voice"; @@ -183,7 +183,7 @@ internal class VoiceCommand : ICommand public byte Voice { get; set; } } -internal class VolumeCommand : ICommand +internal sealed class VolumeCommand : ICommand { public Color Color => Color.SteelBlue; public string Label => "Volume"; diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs b/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs index c583a1e..f7ea0b3 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs @@ -125,7 +125,7 @@ void Load(YamlMappingNode gameToLoad) var songs = new List(); foreach (KeyValuePair song in (YamlMappingNode)kvp.Value) { - long songIndex = ConfigUtils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Playlists)), song.Key.ToString(), 0, long.MaxValue); + 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))); @@ -178,7 +178,7 @@ void Load(YamlMappingNode gameToLoad) { throw new BetterKeyNotFoundException(nameof(SampleRate), null); } - SampleRate = (int)ConfigUtils.ParseValue(nameof(SampleRate), sampleRateNode.ToString(), 0, Utils.FrequencyTable.Length - 1); + SampleRate = (int)ConfigUtils.ParseValue(nameof(SampleRate), sampleRateNode.ToString(), 0, MP2KUtils.FrequencyTable.Length - 1); if (reverbTypeNode is null) { @@ -211,10 +211,7 @@ void Load(YamlMappingNode gameToLoad) HasPokemonCompression = ConfigUtils.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))); - } + ConfigUtils.TryCreateMasterPlaylist(Playlists); } catch (BetterKeyNotFoundException ex) { @@ -235,10 +232,13 @@ public override string GetGameName() { return Name; } - public override string GetSongName(long index) + public override string GetSongName(int index) { - Song? s = GetFirstSong(index); - return s is not null ? s.Name : index.ToString(); + if (TryGetFirstSong(index, out Song s)) + { + return s.Name; + } + return index.ToString(); } public override void Dispose() diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs b/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs index 43d5264..43c40ba 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs @@ -12,9 +12,9 @@ public sealed class MP2KEngine : Engine public MP2KEngine(byte[] rom) { - if (rom.Length > GBAUtils.CartridgeCapacity) + if (rom.Length > GBAUtils.CARTRIDGE_CAPACITY) { - throw new InvalidDataException($"The ROM is too large. Maximum size is 0x{GBAUtils.CartridgeCapacity:X7} bytes."); + throw new InvalidDataException($"The ROM is too large. Maximum size is 0x{GBAUtils.CARTRIDGE_CAPACITY:X7} bytes."); } Config = new MP2KConfig(rom); 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 0000000..86029a6 --- /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/MP2KLoadedSong.cs b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong.cs index 1c8da7c..b336371 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong.cs @@ -1,535 +1,46 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; internal sealed partial class MP2KLoadedSong : ILoadedSong { - public List[] Events { get; private set; } + public List[] Events { get; } public long MaxTicks { get; private set; } - public long ElapsedTicks { get; internal set; } - internal int LongestTrack; + public int LongestTrack; private readonly MP2KPlayer _player; - public readonly int VoiceTableOffset; - internal readonly Track[] Tracks; + private readonly int _voiceTableOffset; + public readonly MP2KTrack[] Tracks; - public MP2KLoadedSong(long index, MP2KPlayer player, MP2KConfig cfg, int? oldVoiceTableOffset, string?[] voiceTypeCache) + public MP2KLoadedSong(MP2KPlayer player, int index) { _player = player; - ref SongEntry entry = ref MemoryMarshal.AsRef(cfg.ROM.AsSpan(cfg.SongTableOffsets[0] + ((int)index * 8))); - cfg.Reader.Stream.Position = entry.HeaderOffset - GBA.GBAUtils.CartridgeOffset; - SongHeader header = cfg.Reader.ReadObject(); // TODO: Can I RefStruct this? If not, should still ditch reader and use pointer - VoiceTableOffset = header.VoiceTableOffset - GBA.GBAUtils.CartridgeOffset; - if (oldVoiceTableOffset != VoiceTableOffset) - { - Array.Clear(voiceTypeCache); - } + MP2KConfig cfg = player.Config; + var entry = SongEntry.Get(cfg.ROM, cfg.SongTableOffsets[0], index); + int headerOffset = entry.HeaderOffset - GBAUtils.CARTRIDGE_OFFSET; - Tracks = new Track[header.NumTracks]; + 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 = header.TrackOffsets[trackIndex] - GBA.GBAUtils.CartridgeOffset; - Tracks[trackIndex] = new Track(trackIndex, trackStart); - Events[trackIndex] = new List(); - - byte runCmd = 0, prevKey = 0, prevVelocity = 0x7F; - int callStackDepth = 0; - AddEvents(trackStart, cfg, trackIndex, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); - } - } - - private static void AddEvent(List trackEvents, long offset, ICommand command) - { - trackEvents.Add(new SongEvent(offset, command)); - } - private static bool EventExists(List trackEvents, long offset) - { - return trackEvents.Any(e => e.Offset == offset); - } - private static void EmulateNote(List trackEvents, long offset, byte key, byte velocity, byte addedDuration, - ref byte runCmd, ref byte prevKey, ref byte prevVelocity) - { - prevKey = key; - prevVelocity = velocity; - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new NoteCommand - { - Note = key, - Velocity = velocity, - Duration = runCmd == 0xCF ? -1 : (Utils.RestTable[runCmd - 0xCF] + addedDuration), - }); - } - } - private void AddEvents(long startOffset, MP2KConfig cfg, byte trackIndex, - ref byte runCmd, ref byte prevKey, ref byte prevVelocity, ref int callStackDepth) - { - cfg.Reader.Stream.Position = startOffset; - List trackEvents = Events[trackIndex]; - - Span peek = stackalloc byte[3]; - bool cont = true; - while (cont) - { - long offset = cfg.Reader.Stream.Position; - - byte cmd = cfg.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; - cfg.Reader.PeekBytes(peek.Slice(0, 2)); - if (peek[0] > 0x7F) - { - velocity = prevVelocity; - addedDuration = 0; - } - else if (peek[1] > 3) - { - velocity = cfg.Reader.ReadByte(); - addedDuration = 0; - } - else - { - velocity = cfg.Reader.ReadByte(); - addedDuration = cfg.Reader.ReadByte(); - } - EmulateNote(trackEvents, offset, cmd, velocity, addedDuration, ref runCmd, ref prevKey, ref prevVelocity); - } - else if (cmd >= 0xCF) - { - byte key, velocity, addedDuration; - cfg.Reader.PeekBytes(peek); - if (peek[0] > 0x7F) - { - key = prevKey; - velocity = prevVelocity; - addedDuration = 0; - } - else if (peek[1] > 0x7F) - { - key = cfg.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 = cfg.Reader.ReadByte(); - velocity = cfg.Reader.ReadByte(); - addedDuration = 0; - } - else - { - key = cfg.Reader.ReadByte(); - velocity = cfg.Reader.ReadByte(); - addedDuration = cfg.Reader.ReadByte(); - } - EmulateNote(trackEvents, offset, key, velocity, addedDuration, ref runCmd, ref prevKey, ref prevVelocity); - } - - #endregion - - #region Rests - - else if (cmd >= 0x80 && cmd <= 0xB0) - { - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new RestCommand { Rest = Utils.RestTable[cmd - 0x80] }); - } - } - - #endregion - - #region Commands - - else if (runCmd < 0xCF && cmd <= 0x7F) - { - switch (runCmd) - { - case 0xBD: - { - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new VoiceCommand { Voice = cmd }); - } - break; - } - case 0xBE: - { - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new VolumeCommand { Volume = cmd }); - } - break; - } - case 0xBF: - { - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new PanpotCommand { Panpot = (sbyte)(cmd - 0x40) }); - } - break; - } - case 0xC0: - { - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new PitchBendCommand { Bend = (sbyte)(cmd - 0x40) }); - } - break; - } - case 0xC1: - { - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new PitchBendRangeCommand { Range = cmd }); - } - break; - } - case 0xC2: - { - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new LFOSpeedCommand { Speed = cmd }); - } - break; - } - case 0xC3: - { - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new LFODelayCommand { Delay = cmd }); - } - break; - } - case 0xC4: - { - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new LFODepthCommand { Depth = cmd }); - } - break; - } - case 0xC5: - { - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new LFOTypeCommand { Type = (LFOType)cmd }); - } - break; - } - case 0xC8: - { - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new TuneCommand { Tune = (sbyte)(cmd - 0x40) }); - } - break; - } - case 0xCD: - { - byte arg = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new LibraryCommand { Command = cmd, Argument = arg }); - } - break; - } - case 0xCE: - { - prevKey = cmd; - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new EndOfTieCommand { Note = cmd }); - } - break; - } - default: throw new MP2KInvalidRunningStatusCMDException(trackIndex, (int)offset, runCmd); - } - } - else if (cmd > 0xB0 && cmd < 0xCF) - { - switch (cmd) - { - case 0xB1: - case 0xB6: - { - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new FinishCommand { Prev = cmd == 0xB6 }); - } - cont = false; - break; - } - case 0xB2: - { - int jumpOffset = cfg.Reader.ReadInt32() - GBA.GBAUtils.CartridgeOffset; - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new JumpCommand { Offset = jumpOffset }); - if (!EventExists(trackEvents, jumpOffset)) - { - AddEvents(jumpOffset, cfg, trackIndex, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); - } - } - cont = false; - break; - } - case 0xB3: - { - int callOffset = cfg.Reader.ReadInt32() - GBA.GBAUtils.CartridgeOffset; - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new CallCommand { Offset = callOffset }); - } - if (callStackDepth < 3) - { - long backup = cfg.Reader.Stream.Position; - callStackDepth++; - AddEvents(callOffset, cfg, trackIndex, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); - cfg.Reader.Stream.Position = backup; - } - else - { - throw new MP2KTooManyNestedCallsException(trackIndex); - } - break; - } - case 0xB4: - { - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, 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 = cfg.Reader.ReadByte(); - byte address = cfg.Reader.ReadByte(); - byte data = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new MemoryAccessCommand { Operator = op, Address = address, Data = data }); - } - break; - } - case 0xBA: - { - byte priority = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new PriorityCommand { Priority = priority }); - } - break; - } - case 0xBB: - { - byte tempoArg = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new TempoCommand { Tempo = (ushort)(tempoArg * 2) }); - } - break; - } - case 0xBC: - { - sbyte transpose = cfg.Reader.ReadSByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new TransposeCommand { Transpose = transpose }); - } - break; - } - // Commands that work within running status: - case 0xBD: - { - byte voice = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new VoiceCommand { Voice = voice }); - } - break; - } - case 0xBE: - { - byte volume = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new VolumeCommand { Volume = volume }); - } - break; - } - case 0xBF: - { - byte panArg = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); - } - break; - } - case 0xC0: - { - byte bendArg = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new PitchBendCommand { Bend = (sbyte)(bendArg - 0x40) }); - } - break; - } - case 0xC1: - { - byte range = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new PitchBendRangeCommand { Range = range }); - } - break; - } - case 0xC2: - { - byte speed = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new LFOSpeedCommand { Speed = speed }); - } - break; - } - case 0xC3: - { - byte delay = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new LFODelayCommand { Delay = delay }); - } - break; - } - case 0xC4: - { - byte depth = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new LFODepthCommand { Depth = depth }); - } - break; - } - case 0xC5: - { - byte type = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new LFOTypeCommand { Type = (LFOType)type }); - } - break; - } - case 0xC8: - { - byte tuneArg = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new TuneCommand { Tune = (sbyte)(tuneArg - 0x40) }); - } - break; - } - case 0xCD: - { - byte command = cfg.Reader.ReadByte(); - byte arg = cfg.Reader.ReadByte(); - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new LibraryCommand { Command = command, Argument = arg }); - } - break; - } - case 0xCE: - { - int key = cfg.Reader.PeekByte() <= 0x7F ? (prevKey = cfg.Reader.ReadByte()) : -1; - if (!EventExists(trackEvents, offset)) - { - AddEvent(trackEvents, offset, new EndOfTieCommand { Note = key }); - } - break; - } - default: throw new MP2KInvalidCMDException(trackIndex, (int)offset, cmd); - } - } - - #endregion - } - } - - internal 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; - } + int trackStart = SongHeader.GetTrackOffset(cfg.ROM, tracksOffset, trackIndex) - GBAUtils.CARTRIDGE_OFFSET; + Tracks[trackIndex] = new MP2KTrack(trackIndex, trackStart); - e.Ticks.Add(ElapsedTicks); - _player.ExecuteNext(track, ref u); - if (track.Stopped) - { - break; - } - - ElapsedTicks += track.Rest; - track.Rest = 0; - } - if (ElapsedTicks > MaxTicks) - { - LongestTrack = trackIndex; - MaxTicks = ElapsedTicks; - } - track.StopAllChannels(); + AddTrackEvents(trackIndex, trackStart); } } - public void Dispose() + public void CheckVoiceTypeCache(ref int? old, string?[] voiceTypeCache) { - for (int i = 0; i < Tracks.Length; i++) + if (old != _voiceTableOffset) { - Tracks[i].StopAllChannels(); + 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 0000000..2bfd97f --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Events.cs @@ -0,0 +1,542 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections.Generic; +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) + { + EndianBinaryReader r = _player.Config.Reader; + 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 >= 0x80 && cmd <= 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 > 0xB0 && cmd < 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 index 4cf0d37..12c8439 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs @@ -10,7 +10,14 @@ public sealed class MIDISaveArgs { public bool SaveCommandsBeforeTranspose; // TODO: I forgor why I would want this public bool ReverseVolume; - public List<(int AbsoluteTick, (byte Numerator, byte Denominator))> TimeSignatures; + 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 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 0000000..b3c038c --- /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 index b52245c..8997f70 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs @@ -20,28 +20,30 @@ public sealed class MP2KMixer : Mixer internal readonly MP2KConfig 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 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) = Utils.FrequencyTable[config.SampleRate]; + (SampleRate, SamplesPerBuffer) = MP2KUtils.FrequencyTable[config.SampleRate]; SampleRateReciprocal = 1f / SampleRate; _samplesReciprocal = 1f / SamplesPerBuffer; PCM8MasterVolume = config.Volume / 15f; - _pcm8Channels = new PCM8Channel[24]; + _pcm8Channels = new MP2KPCM8Channel[24]; for (int i = 0; i < _pcm8Channels.Length; i++) { - _pcm8Channels[i] = new PCM8Channel(this); + _pcm8Channels[i] = new MP2KPCM8Channel(this); } - _psgChannels = new PSGChannel[4] { _sq1 = new SquareChannel(this), _sq2 = new SquareChannel(this), _pcm4 = new PCM4Channel(this), _noise = new NoiseChannel(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 }; @@ -58,21 +60,21 @@ internal MP2KMixer(MP2KConfig config) Init(_buffer); } - internal PCM8Channel AllocPCM8Channel(Track owner, ADSR env, NoteInfo note, byte vol, sbyte pan, int instPan, int pitch, bool bFixed, bool bCompressed, int sampleOffset) + internal MP2KPCM8Channel? AllocPCM8Channel(MP2KTrack owner, ADSR env, NoteInfo 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 + 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 == null) + if (i.State == EnvelopeState.Dead || i.Owner is null) { nChn = i; break; } } - if (nChn == null) // Find releasing + if (nChn is null) // Find releasing { - foreach (PCM8Channel i in byOwner) + foreach (MP2KPCM8Channel i in byOwner) { if (i.State == EnvelopeState.Releasing) { @@ -81,40 +83,40 @@ internal PCM8Channel AllocPCM8Channel(Track owner, ADSR env, NoteInfo note, byte } } } - if (nChn == null) // Find prioritized + if (nChn is null) // Find prioritized { - foreach (PCM8Channel i in byOwner) + foreach (MP2KPCM8Channel i in byOwner) { - if (owner.Priority > i.Owner.Priority) + if (owner.Priority > i.Owner!.Priority) { nChn = i; break; } } } - if (nChn == null) // None available + if (nChn is 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) + 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 != null) // Could still be null from the above if + 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 PSGChannel AllocPSGChannel(Track owner, ADSR env, NoteInfo note, byte vol, sbyte pan, int instPan, int pitch, VoiceType type, object arg) + internal MP2KPSGChannel? AllocPSGChannel(MP2KTrack owner, ADSR env, NoteInfo note, byte vol, sbyte pan, int instPan, int pitch, VoiceType type, object arg) { - PSGChannel nChn; + MP2KPSGChannel nChn; switch (type) { case VoiceType.Square1: { nChn = _sq1; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) { return null; } @@ -124,7 +126,7 @@ internal PSGChannel AllocPSGChannel(Track owner, ADSR env, NoteInfo note, byte v case VoiceType.Square2: { nChn = _sq2; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) { return null; } @@ -134,7 +136,7 @@ internal PSGChannel AllocPSGChannel(Track owner, ADSR env, NoteInfo note, byte v case VoiceType.PCM4: { nChn = _pcm4; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) { return null; } @@ -144,7 +146,7 @@ internal PSGChannel AllocPSGChannel(Track owner, ADSR env, NoteInfo note, byte v case VoiceType.Noise: { nChn = _noise; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) { return null; } @@ -161,14 +163,14 @@ internal PSGChannel AllocPSGChannel(Track owner, ADSR env, NoteInfo note, byte v internal void BeginFadeIn() { _fadePos = 0f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * GBA.GBAUtils.AGB_FPS); + _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 * GBA.GBAUtils.AGB_FPS); + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * GBAUtils.AGB_FPS); _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; _isFading = true; } @@ -186,16 +188,6 @@ internal void ResetFade() _fadeMicroFramesLeft = 0; } - private WaveFileWriter? _waveWriter; - public void CreateWaveWriter(string fileName) - { - _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); - } - public void CloseWaveWriter() - { - _waveWriter!.Dispose(); - _waveWriter = null; - } internal void Process(bool output, bool recording) { for (int i = 0; i < _trackBuffers.Length; i++) @@ -207,8 +199,8 @@ internal void Process(bool output, bool recording) for (int i = 0; i < _pcm8Channels.Length; i++) { - PCM8Channel c = _pcm8Channels[i]; - if (c.Owner != null) + MP2KPCM8Channel c = _pcm8Channels[i]; + if (c.Owner is not null) { c.Process(_trackBuffers[c.Owner.Index]); } @@ -216,8 +208,8 @@ internal void Process(bool output, bool recording) for (int i = 0; i < _psgChannels.Length; i++) { - PSGChannel c = _psgChannels[i]; - if (c.Owner != null) + MP2KPSGChannel c = _psgChannels[i]; + if (c.Owner is not null) { c.Process(_trackBuffers[c.Owner.Index]); } diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs b/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs index 7bbd6d1..7ebe510 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs @@ -1,786 +1,155 @@ -using Kermalis.VGMusicStudio.Core.Util; -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Threading; +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; - -public sealed partial class MP2KPlayer : IPlayer +public sealed partial class MP2KPlayer : Player { - private readonly MP2KMixer _mixer; - private readonly MP2KConfig _config; - private readonly TimeBarrier _time; - private Thread? _thread; - private ushort _tempo; - private int _tempoStack; - private long _elapsedLoops; + protected override string Name => "MP2K Player"; + private readonly string?[] _voiceTypeCache; + internal readonly MP2KConfig Config; + internal readonly MP2KMixer MMixer; private MP2KLoadedSong? _loadedSong; - public ILoadedSong? LoadedSong => _loadedSong; - public bool ShouldFadeOut { get; set; } - public long NumLoops { get; set; } - public PlayerState State { get; private set; } - public event Action? SongEnded; + internal ushort Tempo; + internal int TempoStack; + private long _elapsedLoops; + + private int? _prevVoiceTableOffset; - private readonly string?[] _voiceTypeCache; + public override ILoadedSong? LoadedSong => _loadedSong; + protected override Mixer Mixer => MMixer; internal MP2KPlayer(MP2KConfig config, MP2KMixer mixer) + : base(GBAUtils.AGB_FPS) { - _config = config; - _mixer = mixer; + Config = config; + MMixer = mixer; _voiceTypeCache = new string[256]; - - _time = new TimeBarrier(GBA.GBAUtils.AGB_FPS); - } - private void CreateThread() - { - _thread = new Thread(Tick) { Name = "MP2K Player Tick" }; - _thread.Start(); - } - private void WaitThread() - { - if (_thread is not null && (_thread.ThreadState is ThreadState.Running or ThreadState.WaitSleepJoin)) - { - _thread.Join(); - } } - private void InitEmulation() + public override void LoadSong(int index) { - _tempo = 150; - _tempoStack = 0; - _elapsedLoops = 0; - _loadedSong!.ElapsedTicks = 0; - _mixer.ResetFade(); - for (int trackIndex = 0; trackIndex < _loadedSong.Tracks.Length; trackIndex++) - { - _loadedSong.Tracks[trackIndex].Init(); - } - } - public void LoadSong(long index) - { - int? oldVoiceTableOffset = _loadedSong?.VoiceTableOffset; if (_loadedSong is not null) { - _loadedSong.Dispose(); _loadedSong = null; } // If there's an exception, this will remain null - _loadedSong = new MP2KLoadedSong(index, this, _config, oldVoiceTableOffset, _voiceTypeCache); - _loadedSong.SetTicks(); + _loadedSong = new MP2KLoadedSong(this, index); if (_loadedSong.Events.Length == 0) { - _loadedSong.Dispose(); _loadedSong = null; - } - } - public void SetCurrentPosition(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) - { - Pause(); - } - InitEmulation(); - MP2KLoadedSong s = _loadedSong; - bool u = false; - while (ticks != s.ElapsedTicks) - { - while (_tempoStack >= 150) - { - _tempoStack -= 150; - for (int trackIndex = 0; trackIndex < s.Tracks.Length; trackIndex++) - { - Track track = s.Tracks[trackIndex]; - if (!track.Stopped) - { - track.Tick(); - while (track.Rest == 0 && !track.Stopped) - { - ExecuteNext(track, ref u); - } - } - } - s.ElapsedTicks++; - if (s.ElapsedTicks == ticks) - { - break; - } - } - _tempoStack += _tempo; - } - - for (int i = 0; i < s.Tracks.Length; i++) - { - s.Tracks[i].StopAllChannels(); - } - Pause(); + _loadedSong.CheckVoiceTypeCache(ref _prevVoiceTableOffset, _voiceTypeCache); + _loadedSong.SetTicks(); } - public void Play() + public override void UpdateSongState(SongState info) { - if (_loadedSong is null) - { - SongEnded?.Invoke(); - return; - } - - if (State is not PlayerState.ShutDown) - { - Stop(); - InitEmulation(); - State = PlayerState.Playing; - CreateThread(); - } + info.Tempo = Tempo; + _loadedSong!.UpdateSongState(info, _voiceTypeCache); } - public void Pause() + internal override void InitEmulation() { - switch (State) + Tempo = 150; + TempoStack = 0; + _elapsedLoops = 0; + ElapsedTicks = 0; + MMixer.ResetFade(); + MP2KTrack[] tracks = _loadedSong!.Tracks; + for (int i = 0; i < tracks.Length; i++) { - case PlayerState.Playing: - { - State = PlayerState.Paused; - WaitThread(); - break; - } - case PlayerState.Paused: - case PlayerState.Stopped: - { - State = PlayerState.Playing; - CreateThread(); - break; - } + tracks[i].Init(); } } - public void Stop() + protected override void SetCurTick(long ticks) { - if (State is PlayerState.Playing or PlayerState.Paused) - { - State = PlayerState.Stopped; - WaitThread(); - } + _loadedSong!.SetCurTick(ticks); } - public void Record(string fileName) + protected override void OnStopped() { - _mixer.CreateWaveWriter(fileName); - - InitEmulation(); - State = PlayerState.Recording; - CreateThread(); - WaitThread(); - - _mixer.CloseWaveWriter(); + MP2KTrack[] tracks = _loadedSong!.Tracks; + for (int i = 0; i < tracks.Length; i++) + { + tracks[i].StopAllChannels(); + } } - public void SaveAsMIDI(string fileName, MIDISaveArgs args) - { - _loadedSong!.SaveAsMIDI(fileName, args); - } - public void UpdateSongState(SongState info) + protected override bool Tick(bool playing, bool recording) { - info.Tempo = _tempo; - for (int trackIndex = 0; trackIndex < _loadedSong!.Tracks.Length; trackIndex++) - { - Track track = _loadedSong.Tracks[trackIndex]; - SongState.Track tin = info.Tracks[trackIndex]; - tin.Position = track.DataOffset; - tin.Rest = track.Rest; - tin.Voice = track.Voice; - tin.LFO = track.LFODepth; - ref string? voiceType = ref _voiceTypeCache[track.Voice]; - if (voiceType is null) - { - byte t = _config.ROM[_loadedSong.VoiceTableOffset + (track.Voice * 12)]; - if (t == (byte)VoiceFlags.KeySplit) - { - voiceType = "Key Split"; - } - else if (t == (byte)VoiceFlags.Drum) - { - voiceType = "Drum"; - } - else - { - switch ((VoiceType)(t & 0x7)) // Disregard the other flags - { - case VoiceType.PCM8: voiceType = "PCM8"; break; - case VoiceType.Square1: voiceType = "Square 1"; break; - case VoiceType.Square2: voiceType = "Square 2"; break; - case VoiceType.PCM4: voiceType = "PCM4"; break; - case VoiceType.Noise: voiceType = "Noise"; break; - case VoiceType.Invalid5: voiceType = "Invalid 5"; break; - case VoiceType.Invalid6: voiceType = "Invalid 6"; break; - default: voiceType = "Invalid 7"; break; // VoiceType.Invalid7 - } - } - } - tin.Type = voiceType; - tin.Volume = track.GetVolume(); - tin.PitchBend = track.GetPitch(); - tin.Panpot = track.GetPanpot(); + MP2KLoadedSong s = _loadedSong!; - Channel[] channels = track.Channels.ToArray(); - if (channels.Length == 0) + bool allDone = false; + while (!allDone && TempoStack >= 150) + { + TempoStack -= 150; + allDone = true; + for (int i = 0; i < s.Tracks.Length; i++) { - tin.Keys[0] = byte.MaxValue; - tin.LeftVolume = 0f; - tin.RightVolume = 0f; + TickTrack(s, s.Tracks[i], ref allDone); } - else + if (MMixer.IsFadeDone()) { - 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.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; + allDone = true; } } + if (!allDone) + { + TempoStack += Tempo; + } + MMixer.Process(playing, recording); + return allDone; } - - private void PlayNote(Track track, byte note, byte velocity, byte addedDuration) + private void TickTrack(MP2KLoadedSong s, MP2KTrack track, ref bool allDone) { - int n = note + track.Transpose; - if (n < 0) + track.Tick(); + bool update = false; + while (track.Rest == 0 && !track.Stopped) { - n = 0; + s.ExecuteNext(track, ref update); } - else if (n > 0x7F) + if (track.Index == s.LongestTrack) { - n = 0x7F; + HandleTicksAndLoop(s, track); } - note = (byte)n; - track.PrevNote = note; - track.PrevVelocity = velocity; - if (!track.Ready) + if (!track.Stopped) { - return; // Tracks do not play unless they have had a voice change event + allDone = false; } - - bool fromDrum = false; - int offset = _loadedSong!.VoiceTableOffset + (track.Voice * 12); - while (true) + if (track.Channels.Count > 0) { - ref VoiceEntry v = ref MemoryMarshal.AsRef(_config.ROM.AsSpan(offset)); - if (v.Type == (int)VoiceFlags.KeySplit) - { - fromDrum = false; // In case there is a multi within a drum - byte inst = _config.ROM[v.Int8 - GBAUtils.CartridgeOffset + note]; - offset = v.Int4 - GBAUtils.CartridgeOffset + (inst * 12); - } - else if (v.Type == (int)VoiceFlags.Drum) + allDone = false; + if (update || track.LFODepth > 0) { - fromDrum = true; - offset = v.Int4 - GBAUtils.CartridgeOffset + (note * 12); - } - else - { - var ni = new NoteInfo - { - Duration = track.RunCmd == 0xCF ? -1 : (Utils.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 = _config.HasPokemonCompression && ((v.Type & (int)VoiceFlags.Compressed) != 0); - _mixer.AllocPCM8Channel(track, v.ADSR, ni, - track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), - bFixed, bCompressed, v.Int4 - GBAUtils.CartridgeOffset); - return; - } - case VoiceType.Square1: - case VoiceType.Square2: - { - _mixer.AllocPSGChannel(track, v.ADSR, ni, - track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), - type, (SquarePattern)v.Int4); - return; - } - case VoiceType.PCM4: - { - _mixer.AllocPSGChannel(track, v.ADSR, ni, - track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), - type, v.Int4 - GBAUtils.CartridgeOffset); - return; - } - case VoiceType.Noise: - { - _mixer.AllocPSGChannel(track, v.ADSR, ni, - track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), - type, (NoisePattern)v.Int4); - return; - } - } - return; // Prevent infinite loop with invalid instruments + track.UpdateChannels(); } } } - internal void ExecuteNext(Track track, ref bool update) + private void HandleTicksAndLoop(MP2KLoadedSong s, MP2KTrack track) { - byte[] rom = _config.ROM; - byte cmd = rom[track.DataOffset++]; - if (cmd >= 0xBD) // Commands that work within running status + if (ElapsedTicks != s.MaxTicks) { - track.RunCmd = cmd; + ElapsedTicks++; + return; } - if (track.RunCmd >= 0xCF && cmd <= 0x7F) // Within running status + // Track reached the detected end, update loops/ticks accordingly + if (track.Stopped) { - 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; - } - PlayNote(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; - } - 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.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)) - GBA.GBAUtils.CartridgeOffset; - 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)) - GBA.GBAUtils.CartridgeOffset; - 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: - { - _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); - } + return; } - } - private void Tick() - { - MP2KLoadedSong s = _loadedSong!; - _time.Start(); - while (true) + _elapsedLoops++; + UpdateElapsedTicksAfterLoop(s.Events[track.Index], track.DataOffset, track.Rest); + if (ShouldFadeOut && _elapsedLoops > NumLoops && !MMixer.IsFading()) { - PlayerState state = State; - bool playing = state == PlayerState.Playing; - bool recording = state == PlayerState.Recording; - if (!playing && !recording) - { - break; - } - - while (_tempoStack >= 150) - { - _tempoStack -= 150; - bool allDone = true; - for (int trackIndex = 0; trackIndex < s.Tracks.Length; trackIndex++) - { - Track track = s.Tracks[trackIndex]; - track.Tick(); - bool update = false; - while (track.Rest == 0 && !track.Stopped) - { - ExecuteNext(track, ref update); - } - if (trackIndex == s.LongestTrack) - { - if (s.ElapsedTicks == s.MaxTicks) - { - if (!track.Stopped) - { - List evs = s.Events[trackIndex]; - for (int i = 0; i < evs.Count; i++) - { - SongEvent ev = evs[i]; - if (ev.Offset == track.DataOffset) - { - s.ElapsedTicks = ev.Ticks[0] - track.Rest; - break; - } - } - _elapsedLoops++; - if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) - { - _mixer.BeginFadeOut(); - } - } - } - else - { - s.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) - { - // TODO: lock state - _mixer.Process(playing, recording); - _time.Stop(); - State = PlayerState.Stopped; - SongEnded?.Invoke(); - return; - } - } - _tempoStack += _tempo; - _mixer.Process(playing, recording); - if (playing) - { - _time.Wait(); - } + MMixer.BeginFadeOut(); } - _time.Stop(); } - public void Dispose() + public void SaveAsMIDI(string fileName, MIDISaveArgs args) { - if (State is not PlayerState.ShutDown) - { - State = PlayerState.ShutDown; - WaitThread(); - } + _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 0000000..5b33542 --- /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 0000000..e612465 --- /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 0000000..f754d87 --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal static class MP2KUtils +{ + 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 + /// 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; + } + } + + // 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, + }; + // TODO: Do runtime + 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; + } + } + compressionByte--; + compressionLevel += _compressionLookup[b & 0xF]; + samples.Add(compressionLevel); + if (++compressionIdx >= sampleLength) + { + break; + } + } + } + + return samples.ToArray(); + } + + 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/Structs.cs b/VG Music Studio - Core/GBA/MP2K/Structs.cs deleted file mode 100644 index fe06998..0000000 --- a/VG Music Studio - Core/GBA/MP2K/Structs.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System.Runtime.InteropServices; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; - -[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 8)] -internal struct SongEntry -{ - public int HeaderOffset; - public short Player; - public byte Unknown1; - public byte Unknown2; -} -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; } -} -[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 12)] -internal struct VoiceEntry -{ - public byte Type; // 0 - public byte RootNote; // 1 - public byte Unknown; // 2 - public byte Pan; // 3 - /// SquarePattern for Square1/Square2, NoisePattern for Noise, Address for PCM8/PCM4/KeySplit/Drum - public int Int4; // 4 - /// ADSR for PCM8/Square1/Square2/PCM4/Noise, KeysAddress for KeySplit - public ADSR ADSR; // 8 - - public int Int8 => (ADSR.R << 24) | (ADSR.S << 16) | (ADSR.D << 8) | (ADSR.A); -} -[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 4)] -internal struct ADSR -{ - public byte A; - public byte D; - public byte S; - public byte R; -} -[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 6)] -internal struct GoldenSunPSG -{ - /// Always 0x80 - public byte Unknown; - public GoldenSunPSGType Type; - public byte InitialCycle; - public byte CycleSpeed; - public byte CycleAmplitude; - public byte MinimumCycle; -} -[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 16)] -internal struct SampleHeader -{ - 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; -} - -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/Track.cs b/VG Music Studio - Core/GBA/MP2K/Track.cs deleted file mode 100644 index 7b047ff..0000000 --- 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 PrevNote; - 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; - 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; - 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.OriginalNote == 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 223bb43..0000000 --- a/VG Music Studio - Core/GBA/MP2K/Utils.cs +++ /dev/null @@ -1,175 +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 - // TODO: Do runtime instead of make arrays - public static float[] PCM4ToFloat(int sampleOffset) - { - var config = (MP2KConfig)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 = (MP2KConfig)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(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/Mixer.cs b/VG Music Studio - Core/Mixer.cs index fb6b818..4bb8efe 100644 --- a/VG Music Studio - Core/Mixer.cs +++ b/VG Music Studio - Core/Mixer.cs @@ -7,7 +7,7 @@ namespace Kermalis.VGMusicStudio.Core; public abstract class Mixer : IAudioSessionEventsHandler, IDisposable { - public static event Action? MixerVolumeChanged; + public static event Action? VolumeChanged; public readonly bool[] Mutes; private IWavePlayer _out; @@ -15,6 +15,9 @@ public abstract class Mixer : IAudioSessionEventsHandler, IDisposable private bool _shouldSendVolUpdateEvent = true; + protected WaveFileWriter? _waveWriter; + protected abstract WaveFormat WaveFormat { get; } + protected Mixer() { Mutes = new bool[SongState.MAX_TRACKS]; @@ -42,11 +45,21 @@ protected void Init(IWaveProvider waveProvider) _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) { - MixerVolumeChanged?.Invoke(volume); + VolumeChanged?.Invoke(volume); } _shouldSendVolUpdateEvent = true; } 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 e97da44..0000000 --- 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 is not 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/DSEChannel.cs b/VG Music Studio - Core/NDS/DSE/DSEChannel.cs new file mode 100644 index 0000000..4b1b83f --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSEChannel.cs @@ -0,0 +1,375 @@ +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) + { + 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 = 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 is not 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 + ? 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/Commands.cs b/VG Music Studio - Core/NDS/DSE/DSECommands.cs similarity index 80% rename from VG Music Studio - Core/NDS/DSE/Commands.cs rename to VG Music Studio - Core/NDS/DSE/DSECommands.cs index cc8ac62..e07ac3b 100644 --- a/VG Music Studio - Core/NDS/DSE/Commands.cs +++ b/VG Music Studio - Core/NDS/DSE/DSECommands.cs @@ -4,7 +4,7 @@ namespace Kermalis.VGMusicStudio.Core.NDS.DSE; -internal class ExpressionCommand : ICommand +internal sealed class ExpressionCommand : ICommand { public Color Color => Color.SteelBlue; public string Label => "Expression"; @@ -12,13 +12,13 @@ internal class ExpressionCommand : ICommand public byte Expression { get; set; } } -internal class FinishCommand : ICommand +internal sealed class FinishCommand : ICommand { public Color Color => Color.MediumSpringGreen; public string Label => "Finish"; public string Arguments => string.Empty; } -internal class InvalidCommand : ICommand +internal sealed class InvalidCommand : ICommand { public Color Color => Color.MediumVioletRed; public string Label => $"Invalid 0x{Command:X}"; @@ -26,7 +26,7 @@ internal class InvalidCommand : ICommand public byte Command { get; set; } } -internal class LoopStartCommand : ICommand +internal sealed class LoopStartCommand : ICommand { public Color Color => Color.MediumSpringGreen; public string Label => "Loop Start"; @@ -34,7 +34,7 @@ internal class LoopStartCommand : ICommand public long Offset { get; set; } } -internal class NoteCommand : ICommand +internal sealed class NoteCommand : ICommand { public Color Color => Color.SkyBlue; public string Label => "Note"; @@ -45,7 +45,7 @@ internal class NoteCommand : ICommand public byte Velocity { get; set; } public uint Duration { get; set; } } -internal class OctaveAddCommand : ICommand +internal sealed class OctaveAddCommand : ICommand { public Color Color => Color.SkyBlue; public string Label => "Add To Octave"; @@ -53,7 +53,7 @@ internal class OctaveAddCommand : ICommand public sbyte OctaveChange { get; set; } } -internal class OctaveSetCommand : ICommand +internal sealed class OctaveSetCommand : ICommand { public Color Color => Color.SkyBlue; public string Label => "Set Octave"; @@ -61,7 +61,7 @@ internal class OctaveSetCommand : ICommand public byte Octave { get; set; } } -internal class PanpotCommand : ICommand +internal sealed class PanpotCommand : ICommand { public Color Color => Color.GreenYellow; public string Label => "Panpot"; @@ -69,7 +69,7 @@ internal class PanpotCommand : ICommand public sbyte Panpot { get; set; } } -internal class PitchBendCommand : ICommand +internal sealed class PitchBendCommand : ICommand { public Color Color => Color.MediumPurple; public string Label => "Pitch Bend"; @@ -77,7 +77,7 @@ internal class PitchBendCommand : ICommand public ushort Bend { get; set; } } -internal class RestCommand : ICommand +internal sealed class RestCommand : ICommand { public Color Color => Color.PaleVioletRed; public string Label => "Rest"; @@ -85,7 +85,7 @@ internal class RestCommand : ICommand public uint Rest { get; set; } } -internal class SkipBytesCommand : ICommand +internal sealed class SkipBytesCommand : ICommand { public Color Color => Color.MediumVioletRed; public string Label => $"Skip 0x{Command:X}"; @@ -94,7 +94,7 @@ internal class SkipBytesCommand : ICommand public byte Command { get; set; } public byte[] SkippedBytes { get; set; } } -internal class TempoCommand : ICommand +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 @@ -103,7 +103,7 @@ internal class TempoCommand : ICommand public byte Command { get; set; } public byte Tempo { get; set; } } -internal class UnknownCommand : ICommand +internal sealed class UnknownCommand : ICommand { public Color Color => Color.MediumVioletRed; public string Label => $"Unknown 0x{Command:X}"; @@ -112,7 +112,7 @@ internal class UnknownCommand : ICommand public byte Command { get; set; } public byte[] Args { get; set; } } -internal class VoiceCommand : ICommand +internal sealed class VoiceCommand : ICommand { public Color Color => Color.DarkSalmon; public string Label => "Voice"; @@ -120,7 +120,7 @@ internal class VoiceCommand : ICommand public byte Voice { get; set; } } -internal class VolumeCommand : ICommand +internal sealed class VolumeCommand : ICommand { public Color Color => Color.SteelBlue; public string Label => "Volume"; diff --git a/VG Music Studio - Core/NDS/DSE/DSEConfig.cs b/VG Music Studio - Core/NDS/DSE/DSEConfig.cs index 69edd6c..6a68eed 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEConfig.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEConfig.cs @@ -1,7 +1,7 @@ using Kermalis.EndianBinaryIO; using Kermalis.VGMusicStudio.Core.Properties; +using System.Collections.Generic; using System.IO; -using System.Linq; namespace Kermalis.VGMusicStudio.Core.NDS.DSE; @@ -19,14 +19,17 @@ internal DSEConfig(string bgmPath) throw new DSENoSequencesException(bgmPath); } - var songs = new Song[BGMFiles.Length]; + // 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(); - songs[i] = new Song(i, $"{Path.GetFileNameWithoutExtension(BGMFiles[i])} - {new string(header.Label.TakeWhile(c => c != '\0').ToArray())}"); + 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)); @@ -36,7 +39,7 @@ public override string GetGameName() { return "DSE"; } - public override string GetSongName(long index) + public override string GetSongName(int index) { return index < 0 || index >= BGMFiles.Length ? index.ToString() 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 0000000..911c5f0 --- /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/DSELoadedSong.cs b/VG Music Studio - Core/NDS/DSE/DSELoadedSong.cs new file mode 100644 index 0000000..8f7a943 --- /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(4); + } + } + } +} 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 0000000..b37dde9 --- /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 0000000..e07c497 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs @@ -0,0 +1,288 @@ +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(); + if (channel is null) + { + 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 >= 0x80 && cmd <= 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 index cbcf840..89f6c52 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEMixer.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEMixer.cs @@ -1,4 +1,5 @@ -using Kermalis.VGMusicStudio.Core.Util; +using Kermalis.VGMusicStudio.Core.NDS.SDAT; +using Kermalis.VGMusicStudio.Core.Util; using NAudio.Wave; using System; @@ -15,9 +16,11 @@ public sealed class DSEMixer : Mixer private float _fadePos; private float _fadeStepPerMicroframe; - private readonly Channel[] _channels; + 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. @@ -27,10 +30,10 @@ public DSEMixer() _samplesPerBuffer = 341; // TODO _samplesReciprocal = 1f / _samplesPerBuffer; - _channels = new Channel[NUM_CHANNELS]; + _channels = new DSEChannel[NUM_CHANNELS]; for (byte i = 0; i < NUM_CHANNELS; i++) { - _channels[i] = new Channel(i); + _channels[i] = new DSEChannel(i); } _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) @@ -41,17 +44,17 @@ public DSEMixer() Init(_buffer); } - internal Channel? AllocateChannel() + internal DSEChannel? AllocateChannel() { - static int GetScore(Channel c) + static int GetScore(DSEChannel c) { // Free channels should be used before releasing channels - return c.Owner is null ? -2 : Utils.IsStateRemovable(c.State) ? -1 : 0; + return c.Owner is null ? -2 : DSEUtils.IsStateRemovable(c.State) ? -1 : 0; } - Channel? nChan = null; + DSEChannel? nChan = null; for (int i = 0; i < NUM_CHANNELS; i++) { - Channel c = _channels[i]; + DSEChannel c = _channels[i]; if (nChan is null) { nChan = c; @@ -73,29 +76,29 @@ internal void ChannelTick() { for (int i = 0; i < NUM_CHANNELS; i++) { - Channel chan = _channels[i]; + DSEChannel chan = _channels[i]; if (chan.Owner is null) { continue; } chan.Volume = (byte)chan.StepEnvelope(); - if (chan.NoteLength == 0 && !Utils.IsStateRemovable(chan.State)) + if (chan.NoteLength == 0 && !DSEUtils.IsStateRemovable(chan.State)) { chan.SetEnvelopePhase7_2074ED8(); } - int vol = SDAT.SDATUtils.SustainTable[chan.NoteVelocity] + SDAT.SDATUtils.SustainTable[chan.Volume] + SDAT.SDATUtils.SustainTable[chan.Owner.Volume] + SDAT.SDATUtils.SustainTable[chan.Owner.Expression]; + 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 (Utils.IsStateRemovable(chan.State) && vol <= -92544) + if (DSEUtils.IsStateRemovable(chan.State) && vol <= -92544) { chan.Stop(); } else { - chan.Volume = SDAT.SDATUtils.GetChannelVolume(vol); + chan.Volume = SDATUtils.GetChannelVolume(vol); chan.Panpot = chan.Owner.Panpot; - chan.Timer = SDAT.SDATUtils.GetChannelTimer(chan.BaseTimer, pitch); + chan.Timer = SDATUtils.GetChannelTimer(chan.BaseTimer, pitch); } } } @@ -128,16 +131,6 @@ internal void ResetFade() _fadeMicroFramesLeft = 0; } - private WaveFileWriter? _waveWriter; - public void CreateWaveWriter(string fileName) - { - _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); - } - public void CloseWaveWriter() - { - _waveWriter!.Dispose(); - _waveWriter = null; - } private readonly byte[] _b = new byte[4]; internal void Process(bool output, bool recording) { @@ -169,7 +162,7 @@ internal void Process(bool output, bool recording) right = 0; for (int j = 0; j < NUM_CHANNELS; j++) { - Channel chan = _channels[j]; + DSEChannel chan = _channels[j]; if (chan.Owner is null) { continue; diff --git a/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs b/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs index 3eb9b59..bfdcda2 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs @@ -1,1046 +1,135 @@ -using Kermalis.EndianBinaryIO; -using Kermalis.VGMusicStudio.Core.Util; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; +using System.IO; namespace Kermalis.VGMusicStudio.Core.NDS.DSE; -public sealed class DSEPlayer : IPlayer, ILoadedSong +public sealed class DSEPlayer : Player { - private readonly DSEMixer _mixer; + protected override string Name => "DSE Player"; + private readonly DSEConfig _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; + internal readonly DSEMixer DMixer; + internal readonly SWD MasterSWD; + private DSELoadedSong? _loadedSong; - public List[] Events { get; private set; } - public long MaxTicks { get; private set; } - public long ElapsedTicks { get; private set; } - public ILoadedSong LoadedSong => this; - public bool ShouldFadeOut { get; set; } - public long NumLoops { get; set; } - private int _longestTrack; + internal byte Tempo; + internal int TempoStack; + private long _elapsedLoops; - public PlayerState State { get; private set; } - public event Action? SongEnded; + public override ILoadedSong? LoadedSong => _loadedSong; + protected override Mixer Mixer => DMixer; public DSEPlayer(DSEConfig config, DSEMixer mixer) + : base(192) { - _mixer = mixer; + DMixer = mixer; _config = config; - _masterSWD = new SWD(Path.Combine(config.BGMPath, "bgm.swd")); - _time = new TimeBarrier(192); + MasterSWD = new SWD(Path.Combine(config.BGMPath, "bgm.swd")); } - private void CreateThread() - { - _thread = new Thread(Tick) { Name = "DSE Player Tick" }; - _thread.Start(); - } - private void WaitThread() + + public override void LoadSong(int index) { - if (_thread is not null && (_thread.ThreadState is ThreadState.Running or ThreadState.WaitSleepJoin)) + if (_loadedSong is not null) { - _thread.Join(); + _loadedSong = null; } - } - private void InitEmulation() + // If there's an exception, this will remain null + _loadedSong = new DSELoadedSong(this, _config.BGMFiles[index]); + _loadedSong.SetTicks(); + } + public override void UpdateSongState(SongState info) { - _tempo = 120; - _tempoStack = 0; + info.Tempo = Tempo; + _loadedSong!.UpdateSongState(info); + } + internal override void InitEmulation() + { + Tempo = 120; + TempoStack = 0; _elapsedLoops = 0; ElapsedTicks = 0; - _mixer.ResetFade(); - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) + DMixer.ResetFade(); + DSETrack[] tracks = _loadedSong!.Tracks; + for (int i = 0; i < tracks.Length; i++) { - _tracks[trackIndex].Init(); + tracks[i].Init(); } } - private void SetTicks() + protected override void SetCurTick(long ticks) { - 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; - } - - 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(); - } + _loadedSong!.SetCurTick(ticks); } - public void LoadSong(long index) + protected override void OnStopped() { - 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 stream = new MemoryStream(_smdFile)) + DSETrack[] tracks = _loadedSong!.Tracks; + for (int i = 0; i < tracks.Length; i++) { - 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 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 = r.Stream.Position; - r.Stream.Position += 0x14; // Skip header - _tracks[trackIndex] = new Track(trackIndex, (int)r.Stream.Position); - - uint lastNoteDuration = 0, lastRest = 0; - bool cont = true; - while (cont) - { - long offset = r.Stream.Position; - void AddEvent(ICommand command) - { - Events[trackIndex].Add(new SongEvent(offset, command)); - } - 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)offset, 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(offset)) - { - AddEvent(new NoteCommand { Note = (byte)n, OctaveChange = (sbyte)oct, Velocity = cmd, Duration = duration }); - } - } - 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 + r.ReadSByte()); - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - break; - } - case 0x92: - { - lastRest = r.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - break; - } - case 0x93: - { - lastRest = r.ReadUInt16(); - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - break; - } - case 0x94: - { - lastRest = (uint)(r.ReadByte() | (r.ReadByte() << 8) | (r.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 = r.Stream.Position }); - } - break; - } - case 0xA0: - { - byte octave = r.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new OctaveSetCommand { Octave = octave }); - } - break; - } - case 0xA1: - { - sbyte change = r.ReadSByte(); - if (!EventExists(offset)) - { - AddEvent(new OctaveAddCommand { OctaveChange = change }); - } - break; - } - case 0xA4: - case 0xA5: // The code for these two is identical - { - byte tempoArg = r.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new TempoCommand { Command = cmd, Tempo = tempoArg }); - } - break; - } - case 0xAB: - { - byte[] bytes = new byte[1]; - r.ReadBytes(bytes); - if (!EventExists(offset)) - { - AddEvent(new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); - } - break; - } - case 0xAC: - { - byte voice = r.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new VoiceCommand { Voice = voice }); - } - break; - } - case 0xCB: - case 0xF8: - { - byte[] bytes = new byte[2]; - r.ReadBytes(bytes); - if (!EventExists(offset)) - { - AddEvent(new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); - } - break; - } - case 0xD7: - { - ushort bend = r.ReadUInt16(); - if (!EventExists(offset)) - { - AddEvent(new PitchBendCommand { Bend = bend }); - } - break; - } - case 0xE0: - { - byte volume = r.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new VolumeCommand { Volume = volume }); - } - break; - } - case 0xE3: - { - byte expression = r.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new ExpressionCommand { Expression = expression }); - } - break; - } - case 0xE8: - { - byte panArg = r.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 = new byte[1]; - r.ReadBytes(args); - 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 = new byte[2]; - r.ReadBytes(args); - if (!EventExists(offset)) - { - AddEvent(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(offset)) - { - AddEvent(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(offset)) - { - AddEvent(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(offset)) - { - AddEvent(new UnknownCommand { Command = cmd, Args = args }); - } - break; - } - default: throw new DSEInvalidCMDException(trackIndex, (int)offset, cmd); - } - } - } - r.Stream.Position = chunkStart + 0xC; - uint chunkLength = r.ReadUInt32(); - r.Stream.Position += chunkLength; - // Align 4 - while (r.Stream.Position % 4 != 0) - { - r.Stream.Position++; - } - } - SetTicks(); + tracks[i].StopAllChannels(); } } - public void SetCurrentPosition(long ticks) + + protected override bool Tick(bool playing, bool recording) { - if (_tracks is null) - { - SongEnded?.Invoke(); - return; - } + DSELoadedSong s = _loadedSong!; - if (State is PlayerState.Playing or PlayerState.Paused or PlayerState.Stopped) + bool allDone = false; + while (!allDone && TempoStack >= 240) { - if (State == PlayerState.Playing) - { - Pause(); - } - InitEmulation(); - while (true) + TempoStack -= 240; + allDone = true; + for (int i = 0; i < s.Tracks.Length; i++) { - if (ElapsedTicks == ticks) - { - goto finish; - } - - 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; + TickTrack(s, s.Tracks[i], ref allDone); } - finish: - for (int i = 0; i < _tracks.Length; i++) + if (DMixer.IsFadeDone()) { - _tracks[i].StopAllChannels(); + allDone = true; } - Pause(); } - } - public void Play() - { - if (_tracks == null) + if (!allDone) { - SongEnded?.Invoke(); - return; - } - if (State is PlayerState.Playing or PlayerState.Paused or PlayerState.Stopped) - { - Stop(); - InitEmulation(); - State = PlayerState.Playing; - CreateThread(); + TempoStack += Tempo; } + DMixer.ChannelTick(); + DMixer.Process(playing, recording); + return allDone; } - public void Pause() + private void TickTrack(DSELoadedSong s, DSETrack track, ref bool allDone) { - if (State == PlayerState.Playing) + track.Tick(); + while (track.Rest == 0 && !track.Stopped) { - State = PlayerState.Paused; - WaitThread(); + s.ExecuteNext(track); } - else if (State is PlayerState.Paused or PlayerState.Stopped) + if (track.Index == s.LongestTrack) { - State = PlayerState.Playing; - CreateThread(); + HandleTicksAndLoop(s, track); } - } - public void Stop() - { - if (State is PlayerState.Playing or PlayerState.Paused) + if (!track.Stopped || track.Channels.Count != 0) { - State = PlayerState.Stopped; - WaitThread(); + allDone = false; } } - public void Record(string fileName) - { - _mixer.CreateWaveWriter(fileName); - InitEmulation(); - State = PlayerState.Recording; - CreateThread(); - WaitThread(); - _mixer.CloseWaveWriter(); - } - public void Dispose() + private void HandleTicksAndLoop(DSELoadedSong s, DSETrack track) { - if (State is PlayerState.Playing or PlayerState.Paused or PlayerState.Stopped) + if (ElapsedTicks != s.MaxTicks) { - State = PlayerState.ShutDown; - WaitThread(); - } - } - public void UpdateSongState(SongState info) - { - info.Tempo = _tempo; - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - Track track = _tracks[trackIndex]; - SongState.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())); - } + ElapsedTicks++; + return; } - } - private void ExecuteNext(Track track) - { - byte cmd = _smdFile[track.CurOffset++]; - if (cmd <= 0x7F) + // Track reached the detected end, update loops/ticks accordingly + if (track.Stopped) { - 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; - } - Channel? channel = _mixer.AllocateChannel(); - if (channel is null) - { - throw new Exception("Not enough channels"); - } - - channel.Stop(); - track.Octave = (byte)(track.Octave + oct); - if (channel.StartPCM(_localSWD, _masterSWD, track.Voice, n + (12 * track.Octave), duration)) - { - channel.NoteVelocity = cmd; - channel.Owner = track; - track.Channels.Add(channel); - } - } - 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 DSEInvalidCMDException(track.Index, track.CurOffset - 1, cmd); - } + return; } - } - private void Tick() - { - _time.Start(); - while (true) + _elapsedLoops++; + UpdateElapsedTicksAfterLoop(s.Events[track.Index], track.CurOffset, track.Rest); + if (ShouldFadeOut && _elapsedLoops > NumLoops && !DMixer.IsFading()) { - PlayerState state = State; - bool playing = state == PlayerState.Playing; - bool recording = state == PlayerState.Recording; - if (!playing && !recording) - { - break; - } - - 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) - { - // TODO: lock state - _mixer.ChannelTick(); - _mixer.Process(playing, recording); - _time.Stop(); - State = PlayerState.Stopped; - SongEnded?.Invoke(); - return; - } - } - _tempoStack += _tempo; - _mixer.ChannelTick(); - _mixer.Process(playing, recording); - if (playing) - { - _time.Wait(); - } + DMixer.BeginFadeOut(); } - _time.Stop(); } } 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 0000000..a15e380 --- /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 0000000..349c009 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSEUtils.cs @@ -0,0 +1,52 @@ +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +internal static class DSEUtils +{ + 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/DSE/Enums.cs b/VG Music Studio - Core/NDS/DSE/Enums.cs deleted file mode 100644 index 6cec60d..0000000 --- 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/SMD.cs b/VG Music Studio - Core/NDS/DSE/SMD.cs index 5383c4d..33cd44f 100644 --- a/VG Music Studio - Core/NDS/DSE/SMD.cs +++ b/VG Music Studio - Core/NDS/DSE/SMD.cs @@ -1,61 +1,60 @@ using Kermalis.EndianBinaryIO; -namespace Kermalis.VGMusicStudio.Core.NDS.DSE +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +internal sealed class SMD { - internal sealed class SMD + public sealed class Header // Size 0x40 { - public sealed 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; } - } + [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 sealed 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 sealed 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; } - } + public interface ISongChunk + { + byte NumTracks { get; } + } + public sealed class SongChunk_V402 : ISongChunk // Size 0x20 + { + [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 sealed class SongChunk_V415 : ISongChunk // Size 0x40 + { + [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 index 17c08ef..0b042fb 100644 --- a/VG Music Studio - Core/NDS/DSE/SWD.cs +++ b/VG Music Studio - Core/NDS/DSE/SWD.cs @@ -1,5 +1,7 @@ using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Util; using System; +using System.Diagnostics; using System.IO; namespace Kermalis.VGMusicStudio.Core.NDS.DSE; @@ -8,11 +10,11 @@ internal sealed class SWD { public interface IHeader { - + // } - private class Header_V402 : IHeader + private sealed class Header_V402 : IHeader // Size 0x40 { - [BinaryArrayFixedLength(10)] + [BinaryArrayFixedLength(8)] public byte[] Unknown1 { get; set; } public ushort Year { get; set; } public byte Month { get; set; } @@ -31,9 +33,9 @@ private class Header_V402 : IHeader [BinaryArrayFixedLength(7)] public byte[] Padding { get; set; } } - private class Header_V415 : IHeader + private sealed class Header_V415 : IHeader // Size 0x40 { - [BinaryArrayFixedLength(10)] + [BinaryArrayFixedLength(8)] public byte[] Unknown1 { get; set; } public ushort Year { get; set; } public byte Month { get; set; } @@ -71,7 +73,7 @@ public interface ISplitEntry byte Decay2 { get; set; } byte Release { get; set; } } - public class SplitEntry_V402 : ISplitEntry + public sealed class SplitEntry_V402 : ISplitEntry // Size 0x30 { public ushort Id { get; set; } [BinaryArrayFixedLength(2)] @@ -105,9 +107,10 @@ public class SplitEntry_V402 : ISplitEntry public byte Release { get; set; } public byte Unknown5 { get; set; } + [BinaryIgnore] int ISplitEntry.SampleId => SampleId; } - public class SplitEntry_V415 : ISplitEntry + public sealed class SplitEntry_V415 : ISplitEntry // 0x30 { public ushort Id { get; set; } [BinaryArrayFixedLength(2)] @@ -141,6 +144,7 @@ public class SplitEntry_V415 : ISplitEntry public byte Release { get; set; } public byte Unknown5 { get; set; } + [BinaryIgnore] int ISplitEntry.SampleId => SampleId; } @@ -148,7 +152,7 @@ public interface IProgramInfo { ISplitEntry[] SplitEntries { get; } } - public class ProgramInfo_V402 : IProgramInfo + public sealed class ProgramInfo_V402 : IProgramInfo { public byte Id { get; set; } public byte NumSplits { get; set; } @@ -171,7 +175,7 @@ public class ProgramInfo_V402 : IProgramInfo [BinaryIgnore] ISplitEntry[] IProgramInfo.SplitEntries => SplitEntries; } - public class ProgramInfo_V415 : IProgramInfo + public sealed class ProgramInfo_V415 : IProgramInfo { public ushort Id { get; set; } public ushort NumSplits { get; set; } @@ -195,7 +199,7 @@ public class ProgramInfo_V415 : IProgramInfo public interface IWavInfo { - byte RootKey { get; } + byte RootNote { get; } sbyte Transpose { get; } SampleFormat SampleFormat { get; } bool Loop { get; } @@ -212,13 +216,13 @@ public interface IWavInfo byte Decay2 { get; } byte Release { get; } } - public class WavInfo_V402 : IWavInfo + 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; } - public byte RootKey { get; set; } + public byte RootNote { get; set; } public sbyte Transpose { get; set; } public byte Volume { get; set; } public sbyte Panpot { get; set; } @@ -245,14 +249,14 @@ public class WavInfo_V402 : IWavInfo public byte Release { get; set; } public byte Unknown6 { get; set; } } - public class WavInfo_V415 : IWavInfo + public sealed class WavInfo_V415 : IWavInfo // 0x40 { [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 byte RootNote { get; set; } public sbyte Transpose { get; set; } public byte Volume { get; set; } public sbyte Panpot { get; set; } @@ -293,30 +297,38 @@ public class SampleBlock } public class ProgramBank { - public IProgramInfo[] ProgramInfos; + public IProgramInfo?[] ProgramInfos; public KeyGroup[] KeyGroups; } - public class KeyGroup + public class KeyGroup // Size 0x8 { 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 byte LowNote { get; set; } + public byte HighNote { get; set; } + public ushort Unknown { get; set; } } - public class LFOInfo + public sealed class LFOInfo { - [BinaryArrayFixedLength(16)] - public byte[] Unknown { get; set; } + 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[] Unknown; + public byte[] Unknown1; public uint Length; public ushort Version; public IHeader Header; + public byte[] Unknown2; public ProgramBank Programs; public SampleBlock[] Samples; @@ -327,10 +339,12 @@ public SWD(string path) { var r = new EndianBinaryReader(stream, ascii: true); Type = r.ReadString_Count(4); - Unknown = new byte[4]; - r.ReadBytes(Unknown); + Unknown1 = new byte[4]; + r.ReadBytes(Unknown1); Length = r.ReadUInt32(); Version = r.ReadUInt16(); + Unknown2 = new byte[2]; + r.ReadBytes(Unknown2); switch (Version) { case 0x402: @@ -380,14 +394,11 @@ private static long FindChunk(EndianBinaryReader r, string chunk) } default: { + Debug.WriteLine($"Ignoring {str} chunk"); r.Stream.Position += 0x8; uint length = r.ReadUInt32(); r.Stream.Position += length; - // Align 4 - while (r.Stream.Position % 4 != 0) - { - r.Stream.Position++; - } + r.Stream.Align(4); break; } } @@ -438,7 +449,7 @@ private static long FindChunk(EndianBinaryReader r, string chunk) } chunkOffset += 0x10; - var programInfos = new IProgramInfo[numPRGISlots]; + var programInfos = new IProgramInfo?[numPRGISlots]; for (int i = 0; i < programInfos.Length; i++) { r.Stream.Position = chunkOffset + (2 * i); @@ -462,16 +473,14 @@ private static KeyGroup[] ReadKeyGroups(EndianBinaryReader r) { return Array.Empty(); } - else + + 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++) { - 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; + keyGroups[i] = r.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 ba0a7c6..0000000 --- a/VG Music Studio - Core/NDS/DSE/Track.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE; - -internal sealed class Track -{ - 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 Track(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++) - { - 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 1f16b1a..0000000 --- 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/NDSUtils.cs b/VG Music Studio - Core/NDS/NDSUtils.cs new file mode 100644 index 0000000..2bfd9b5 --- /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/Channel.cs b/VG Music Studio - Core/NDS/SDAT/Channel.cs deleted file mode 100644 index 742a839..0000000 --- a/VG Music Studio - Core/NDS/SDAT/Channel.cs +++ /dev/null @@ -1,391 +0,0 @@ -using System; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal sealed class Channel - { - public readonly byte Index; - - public Track? 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; - 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 is not null) - { - 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) - { - 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/Enums.cs b/VG Music Studio - Core/NDS/SDAT/Enums.cs deleted file mode 100644 index 9f4aa42..0000000 --- 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/SBNK.cs b/VG Music Studio - Core/NDS/SDAT/SBNK.cs index 2acd3a4..953f065 100644 --- a/VG Music Studio - Core/NDS/SDAT/SBNK.cs +++ b/VG Music Studio - Core/NDS/SDAT/SBNK.cs @@ -119,7 +119,7 @@ public Instrument(EndianBinaryReader er) } } - public FileHeader FileHeader; // "SBNK" + public SDATFileHeader FileHeader; // "SBNK" public string BlockType; // "DATA" public int BlockSize; public byte[] Padding; @@ -133,7 +133,7 @@ public SBNK(byte[] bytes) using (var stream = new MemoryStream(bytes)) { var er = new EndianBinaryReader(stream, ascii: true); - FileHeader = new FileHeader(er); + FileHeader = new SDATFileHeader(er); BlockType = er.ReadString_Count(4); BlockSize = er.ReadInt32(); Padding = new byte[32]; diff --git a/VG Music Studio - Core/NDS/SDAT/SDAT.cs b/VG Music Studio - Core/NDS/SDAT/SDAT.cs index 85e4ff3..df2f861 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDAT.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDAT.cs @@ -111,6 +111,24 @@ public sealed class SequenceInfo 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 { @@ -206,7 +224,7 @@ public FAT(EndianBinaryReader er) } } - public FileHeader FileHeader; // "SDAT" + public SDATFileHeader FileHeader; // "SDAT" public int SYMBOffset; public int SYMBLength; public int INFOOffset; @@ -222,30 +240,27 @@ public FAT(EndianBinaryReader er) public FAT FATBlock; //FILEBlock - public SDAT(byte[] bytes) + public SDAT(Stream stream) { - using (var stream = new MemoryStream(bytes)) + 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) { - var er = new EndianBinaryReader(stream, ascii: true); - FileHeader = new FileHeader(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); + 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 0000000..87fdf6d --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATChannel.cs @@ -0,0 +1,394 @@ +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 = 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; // 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() + { + if (Owner is not null) + { + 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/Commands.cs b/VG Music Studio - Core/NDS/SDAT/SDATCommands.cs similarity index 100% rename from VG Music Studio - Core/NDS/SDAT/Commands.cs rename to VG Music Studio - Core/NDS/SDAT/SDATCommands.cs diff --git a/VG Music Studio - Core/NDS/SDAT/SDATConfig.cs b/VG Music Studio - Core/NDS/SDAT/SDATConfig.cs index c35a95a..ce0c21c 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATConfig.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATConfig.cs @@ -31,7 +31,7 @@ public override string GetGameName() { return "SDAT"; } - public override string GetSongName(long index) + public override string GetSongName(int index) { return SDAT.SYMBBlock is null || index < 0 || index >= SDAT.SYMBBlock.SequenceSymbols.NumEntries ? index.ToString() 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 0000000..62f10b9 --- /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/FileHeader.cs b/VG Music Studio - Core/NDS/SDAT/SDATFileHeader.cs similarity index 87% rename from VG Music Studio - Core/NDS/SDAT/FileHeader.cs rename to VG Music Studio - Core/NDS/SDAT/SDATFileHeader.cs index 638b4fd..dc742dc 100644 --- a/VG Music Studio - Core/NDS/SDAT/FileHeader.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATFileHeader.cs @@ -2,7 +2,7 @@ namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; -public sealed class FileHeader +public sealed class SDATFileHeader { public string FileType; public ushort FileEndianness; @@ -11,7 +11,7 @@ public sealed class FileHeader public ushort HeaderSize; // 16 public ushort NumBlocks; - public FileHeader(EndianBinaryReader er) + public SDATFileHeader(EndianBinaryReader er) { FileType = er.ReadString_Count(4); er.Endianness = Endianness.BigEndian; 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 0000000..ff00ed2 --- /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 0000000..66118b1 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs @@ -0,0 +1,746 @@ +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]; + if (evs is not null) + { + 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 0000000..7f00ca7 --- /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 {0}" + (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 index 9cbf17b..e516e15 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATMixer.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATMixer.cs @@ -13,9 +13,11 @@ public sealed class SDATMixer : Mixer private float _fadePos; private float _fadeStepPerMicroframe; - internal Channel[] Channels; + 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. @@ -25,10 +27,10 @@ internal SDATMixer() _samplesPerBuffer = 341; // TODO _samplesReciprocal = 1f / _samplesPerBuffer; - Channels = new Channel[0x10]; + Channels = new SDATChannel[0x10]; for (byte i = 0; i < 0x10; i++) { - Channels[i] = new Channel(i); + Channels[i] = new SDATChannel(i); } _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) @@ -42,7 +44,7 @@ internal SDATMixer() 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 Channel? AllocateChannel(InstrumentType type, Track track) + internal SDATChannel? AllocateChannel(InstrumentType type, SDATTrack track) { int[] allowedChannels; switch (type) @@ -52,10 +54,10 @@ internal SDATMixer() case InstrumentType.Noise: allowedChannels = _noiseChanOrder; break; default: return null; } - Channel? nChan = null; + SDATChannel? nChan = null; for (int i = 0; i < allowedChannels.Length; i++) { - Channel c = Channels[allowedChannels[i]]; + SDATChannel c = Channels[allowedChannels[i]]; if (nChan is not null && c.Priority >= nChan.Priority && (c.Priority != nChan.Priority || nChan.Volume <= c.Volume)) { continue; @@ -73,7 +75,7 @@ internal void ChannelTick() { for (int i = 0; i < 0x10; i++) { - Channel chan = Channels[i]; + SDATChannel chan = Channels[i]; if (chan.Owner is null) { continue; @@ -145,23 +147,13 @@ internal void ResetFade() _fadeMicroFramesLeft = 0; } - private WaveFileWriter? _waveWriter; - public void CreateWaveWriter(string fileName) - { - _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); - } - public void CloseWaveWriter() - { - _waveWriter!.Dispose(); - _waveWriter = null; - } internal void EmulateProcess() { for (int i = 0; i < _samplesPerBuffer; i++) { for (int j = 0; j < 0x10; j++) { - Channel chan = Channels[j]; + SDATChannel chan = Channels[j]; if (chan.Owner is not null) { chan.EmulateProcess(); @@ -200,7 +192,7 @@ internal void Process(bool output, bool recording) right = 0; for (int j = 0; j < 0x10; j++) { - Channel chan = Channels[j]; + SDATChannel chan = Channels[j]; if (chan.Owner is null) { continue; diff --git a/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs b/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs index 3699cc9..97fb6ae 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs @@ -1,1694 +1,195 @@ -using Kermalis.VGMusicStudio.Core.Util; using System; using System.Collections.Generic; -using System.Linq; -using System.Threading; namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; -public sealed class SDATPlayer : IPlayer, ILoadedSong +public sealed class SDATPlayer : Player { + protected override string Name => "SDAT Player"; + internal 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 SDATMixer _mixer; - private readonly SDATConfig _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; + 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; - private ushort _tempo; - private int _tempoStack; + internal ushort Tempo; + internal int TempoStack; private long _elapsedLoops; - public List[] Events { get; private set; } - public long MaxTicks { get; private set; } - public long ElapsedTicks { get; private set; } - public ILoadedSong LoadedSong => this; - public bool ShouldFadeOut { get; set; } - public long NumLoops { get; set; } - private int _longestTrack; + private ushort? _prevBank; - public PlayerState State { get; private set; } - public event Action? SongEnded; + public override ILoadedSong? LoadedSong => _loadedSong; + protected override Mixer Mixer => SMixer; internal SDATPlayer(SDATConfig config, SDATMixer mixer) + : base(192) { - _config = config; - _mixer = mixer; + Config = config; + SMixer = mixer; for (byte i = 0; i < 0x10; i++) { - _tracks[i] = new Track(i, this); - } - - _time = new TimeBarrier(192); - } - private void CreateThread() - { - _thread = new Thread(Tick) { Name = "SDAT Player Tick" }; - _thread.Start(); - } - private void WaitThread() - { - if (_thread is not null && (_thread.ThreadState is ThreadState.Running or ThreadState.WaitSleepJoin)) - { - _thread.Join(); + Tracks[i] = new SDATTrack(i, this); } } - private void InitEmulation() + public override void LoadSong(int index) { - _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++) + if (_loadedSong is not null) { - _vars[i] = i % 8 == 0 ? short.MaxValue : (short)0; + _loadedSong = null; } - } - 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 - // Should evaluate all branches if possible - 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) + + SDAT.INFO.SequenceInfo? seqInfo = Config.SDAT.INFOBlock.SequenceInfos.Entries[index]; + if (seqInfo is null) { - _sseq = null; - _sbnk = null; - Events = null; return; } - if (oldSeqInfo == null || _seqInfo.Bank != oldSeqInfo.Bank) - { - Array.Clear(_voiceTypeCache); - } - _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(); + // If there's an exception, this will remain null + _loadedSong = new SDATLoadedSong(this, seqInfo); + _loadedSong.SetTicks(); - // RECURSION INCOMING - Events = new List[0x10]; - AddTrackEvents(0, 0); - void AddTrackEvents(byte i, int trackStartOffset) + ushort? old = _prevBank; + ushort nu = _loadedSong.SEQInfo.Bank; + if (old != nu) { - 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 SDATInvalidCMDException(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 { Note = 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: - { - byte 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 SDATTooManyNestedCallsException(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; - } - } - } - } - } + _prevBank = nu; + Array.Clear(_voiceTypeCache); } - SetTicks(); } - - public void SetCurrentPosition(long ticks) + public override void UpdateSongState(SongState info) { - if (_seqInfo 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) - { - Pause(); - } - InitEmulation(); - while (true) + info.Tempo = Tempo; + for (int i = 0; i < 0x10; i++) { - if (ElapsedTicks == ticks) + SDATTrack track = Tracks[i]; + if (track.Enabled) { - goto finish; + track.UpdateSongState(info.Tracks[i], _loadedSong!, _voiceTypeCache); } - - 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() + internal override void InitEmulation() { - if (_seqInfo == null) - { - SongEnded?.Invoke(); - return; - } - if (State is PlayerState.Playing or PlayerState.Paused or 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) + 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++) { - State = PlayerState.Playing; - CreateThread(); + Tracks[i].Init(); } - } - public void Stop() - { - if (State == PlayerState.Playing || State == PlayerState.Paused) + // Initialize player and global variables. Global variables should not have a global effect in this program. + for (int i = 0; i < 0x20; i++) { - State = PlayerState.Stopped; - WaitThread(); + Vars[i] = i % 8 == 0 ? short.MaxValue : (short)0; } } - public void Record(string fileName) + protected override void SetCurTick(long ticks) { - _mixer.CreateWaveWriter(fileName); - InitEmulation(); - State = PlayerState.Recording; - CreateThread(); - WaitThread(); - _mixer.CloseWaveWriter(); + _loadedSong!.SetCurTick(ticks); } - public void Dispose() + protected override void OnStopped() { - if (State is PlayerState.Playing or PlayerState.Paused or PlayerState.Stopped) + for (int i = 0; i < 0x10; i++) { - State = PlayerState.ShutDown; - WaitThread(); + Tracks[i].StopAllChannels(); } } - private readonly string?[] _voiceTypeCache = new string?[256]; - public void UpdateSongState(SongState info) + + protected override bool Tick(bool playing, bool recording) { - info.Tempo = _tempo; - for (int i = 0; i < 0x10; i++) + bool allDone = false; + while (!allDone && TempoStack >= 240) { - Track track = _tracks[i]; - if (!track.Enabled) - { - continue; - } - - SongState.Track tin = info.Tracks[i]; - tin.Position = track.DataOffset; - tin.Rest = track.Rest; - tin.Voice = track.Voice; - tin.LFO = track.LFODepth * track.LFORange; - ref string? cache = ref _voiceTypeCache[track.Voice]; - if (cache is null) + TempoStack -= 240; + allDone = true; + for (int i = 0; i < 0x10; i++) { - if (_sbnk.NumInstruments <= track.Voice) - { - cache = "Empty"; - } - else - { - InstrumentType t = _sbnk.Instruments[track.Voice].Type; - switch (t) - { - case InstrumentType.PCM: cache = "PCM"; break; - case InstrumentType.PSG: cache = "PSG"; break; - case InstrumentType.Noise: cache = "Noise"; break; - case InstrumentType.Drum: cache = "Drum"; break; - case InstrumentType.KeySplit: cache = "Key Split"; break; - default: cache = "Invalid {0}" + (byte)t; break; - } - } + TickTrack(i, ref allDone); } - tin.Type = cache; - 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 + if (SMixer.IsFadeDone()) { - 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.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; + allDone = true; } } - } - - private void TryStartChannel(SBNK.InstrumentData inst, Track track, byte note, byte velocity, int duration, out Channel? channel) - { - InstrumentType type = inst.Type; - channel = _mixer.AllocateChannel(type, track); - if (channel is null) - { - return; - } - - if (track.Tie) + if (!allDone) { - duration = -1; + TempoStack += Tempo; } - SBNK.InstrumentData.DataParam param = inst.Param; - byte release = param.Release; - if (release == 0xFF) - { - duration = -1; - release = 0; - } - bool started = false; - switch (type) + for (int i = 0; i < 0x10; i++) { - 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: + SDATTrack track = Tracks[i]; + if (track.Enabled) { - channel.StartPSG((byte)param.Info[0], duration); - started = true; - break; - } - case InstrumentType.Noise: - { - channel.StartNoise(duration); - started = true; - break; + track.UpdateChannels(); } } - 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); + SMixer.ChannelTick(); + SMixer.Process(playing, recording); + return allDone; } - internal void PlayNote(Track track, byte note, byte velocity, int duration) + private void TickTrack(int trackIndex, ref bool allDone) { - Channel? channel = null; - if (track.Tie && track.Channels.Count != 0) + SDATTrack track = Tracks[trackIndex]; + if (!track.Enabled) { - 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; - } + 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) + track.Tick(); + SDATLoadedSong s = _loadedSong!; + while (track.Rest == 0 && !track.WaitingForNoteToFinishBeforeContinuingXD && !track.Stopped) { - channel.SweepPitch += (short)((track.PortamentoKey - note) << 6); // "<< 6" is "* 0x40" + s.ExecuteNext(track); } - if (track.PortamentoTime != 0) + if (trackIndex == s.LongestTrack) { - channel.SweepLength = (track.PortamentoTime * track.PortamentoTime * Math.Abs(channel.SweepPitch)) >> 11; // ">> 11" is "/ 0x800" - channel.AutoSweep = true; + HandleTicksAndLoop(s, track); } - else + if (!track.Stopped || track.Channels.Count != 0) { - channel.SweepLength = duration; - channel.AutoSweep = false; + allDone = false; } - channel.SweepCounter = 0; } - private void ExecuteNext(Track track) + private void HandleTicksAndLoop(SDATLoadedSong s, SDATTrack track) { - int ReadArg(ArgType type) + if (ElapsedTicks != s.MaxTicks) { - 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(); - } + ElapsedTicks++; + return; } - 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 + // Track reached the detected end, update loops/ticks accordingly + if (track.Stopped) { - 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; + return; } - } - private void Tick() - { - _time.Start(); - while (true) + _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++) { - PlayerState state = State; - bool playing = state == PlayerState.Playing; - bool recording = state == PlayerState.Recording; - if (!playing && !recording) + 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; } - - 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) - { - // TODO: lock state - MixerProcess(); - _time.Stop(); - State = PlayerState.Stopped; - SongEnded?.Invoke(); - return; - } - } - _tempoStack += _tempo; - MixerProcess(); - if (playing) - { - _time.Wait(); - } } - _time.Stop(); + 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 0000000..87be5b5 --- /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/SSEQ.cs b/VG Music Studio - Core/NDS/SDAT/SSEQ.cs index ec3859c..22068c7 100644 --- a/VG Music Studio - Core/NDS/SDAT/SSEQ.cs +++ b/VG Music Studio - Core/NDS/SDAT/SSEQ.cs @@ -1,31 +1,30 @@ using Kermalis.EndianBinaryIO; using System.IO; -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +internal sealed class SSEQ { - internal sealed class SSEQ - { - public FileHeader FileHeader; // "SSEQ" - public string BlockType; // "DATA" - public int BlockSize; - public int DataOffset; + public SDATFileHeader FileHeader; // "SSEQ" + public string BlockType; // "DATA" + public int BlockSize; + public int DataOffset; - public byte[] Data; + public byte[] Data; - public SSEQ(byte[] bytes) + public SSEQ(byte[] bytes) + { + using (var stream = new MemoryStream(bytes)) { - using (var stream = new MemoryStream(bytes)) - { - var er = new EndianBinaryReader(stream, ascii: true); - FileHeader = new FileHeader(er); - BlockType = er.ReadString_Count(4); - BlockSize = er.ReadInt32(); - DataOffset = er.ReadInt32(); + 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); - } + 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 index 2af20c3..5a5e64d 100644 --- a/VG Music Studio - Core/NDS/SDAT/SWAR.cs +++ b/VG Music Studio - Core/NDS/SDAT/SWAR.cs @@ -1,64 +1,64 @@ using Kermalis.EndianBinaryIO; using System.IO; -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +internal sealed class SWAR { - internal sealed class SWAR + public sealed class SWAV { - public sealed class SWAV - { - public SWAVFormat Format; - public bool DoesLoop; - public ushort SampleRate; - public ushort Timer; // (NDSUtils.ARM7_CLOCK / SampleRate) - public ushort LoopOffset; - public int Length; + public SWAVFormat Format; + public bool DoesLoop; + public ushort SampleRate; + /// / + public ushort Timer; + public ushort LoopOffset; + public int Length; - public byte[] Samples; + 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(); + 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); - } + Samples = new byte[(LoopOffset * 4) + (Length * 4)]; + er.ReadBytes(Samples); } + } - public FileHeader FileHeader; // "SWAR" - public string BlockType; // "DATA" - public int BlockSize; - public byte[] Padding; - public int NumWaves; - public int[] WaveOffsets; + public SDATFileHeader FileHeader; // "SWAR" + public string BlockType; // "DATA" + public int BlockSize; + public byte[] Padding; + public int NumWaves; + public int[] WaveOffsets; - public SWAV[] Waves; + public SWAV[] Waves; - public SWAR(byte[] bytes) + public SWAR(byte[] bytes) + { + using (var stream = new MemoryStream(bytes)) { - using (var stream = new MemoryStream(bytes)) - { - var er = new EndianBinaryReader(stream, ascii: true); - FileHeader = new FileHeader(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); + 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); - } + 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/NDS/SDAT/Track.cs b/VG Music Studio - Core/NDS/SDAT/Track.cs deleted file mode 100644 index 43a1435..0000000 --- a/VG Music Studio - Core/NDS/SDAT/Track.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal sealed class Track - { - 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 PortamentoKey; - 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 Track(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; - PortamentoKey = 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++) - { - 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(); - } - } - - 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; - } - } -} diff --git a/VG Music Studio - Core/NDS/Utils.cs b/VG Music Studio - Core/NDS/Utils.cs deleted file mode 100644 index 601e832..0000000 --- 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 = 16_756_991; // (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 index adb8fc8..453af4b 100644 --- a/VG Music Studio - Core/Player.cs +++ b/VG Music Studio - Core/Player.cs @@ -1,5 +1,8 @@ -using System; +using Kermalis.VGMusicStudio.Core.Util; +using System; using System.Collections.Generic; +using System.IO; +using System.Threading; namespace Kermalis.VGMusicStudio.Core; @@ -14,25 +17,179 @@ public enum PlayerState : byte public interface ILoadedSong { - List[] Events { get; } + List?[] Events { get; } long MaxTicks { get; } - long ElapsedTicks { get; } } -public interface IPlayer : IDisposable +public abstract class Player : IDisposable { - ILoadedSong? LoadedSong { get; } - bool ShouldFadeOut { get; set; } - long NumLoops { get; set; } - - PlayerState State { get; } - event Action? SongEnded; - - void LoadSong(long index); - void SetCurrentPosition(long ticks); - void Play(); - void Pause(); - void Stop(); - void Record(string fileName); - void UpdateSongState(SongState info); + 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() + { + if (State != PlayerState.ShutDown) + { + State = PlayerState.ShutDown; + WaitThread(); + } + SongEnded = null; + } } diff --git a/VG Music Studio - Core/SongState.cs b/VG Music Studio - Core/SongState.cs index 8987c14..c3035a7 100644 --- a/VG Music Studio - Core/SongState.cs +++ b/VG Music Studio - Core/SongState.cs @@ -1,71 +1,70 @@ -namespace Kermalis.VGMusicStudio.Core +namespace Kermalis.VGMusicStudio.Core; + +public sealed class SongState { - public sealed class SongState + public sealed class Track { - 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 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 int PreviousKeysTime; // TODO: Fix + public string PreviousKeys; - public Track() + public Track() + { + Keys = new byte[MAX_KEYS]; + for (int i = 0; i < MAX_KEYS; i++) { - Keys = new byte[MAX_KEYS]; - for (int i = 0; i < MAX_KEYS; i++) - { - Keys[i] = byte.MaxValue; - } + Keys[i] = byte.MaxValue; } + } - public void Reset() + 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++) { - 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; - } + 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 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 Track[] Tracks; + public ushort Tempo; + public Track[] Tracks; - public SongState() + public SongState() + { + Tracks = new Track[MAX_TRACKS]; + for (int i = 0; i < MAX_TRACKS; i++) { - Tracks = new Track[MAX_TRACKS]; - for (int i = 0; i < MAX_TRACKS; i++) - { - Tracks[i] = new Track(); - } + Tracks[i] = new Track(); } + } - public void Reset() + public void Reset() + { + Tempo = 0; + for (int i = 0; i < MAX_TRACKS; i++) { - Tempo = 0; - for (int i = 0; i < MAX_TRACKS; i++) - { - Tracks[i].Reset(); - } + Tracks[i].Reset(); } } } diff --git a/VG Music Studio - Core/Util/ConfigUtils.cs b/VG Music Studio - Core/Util/ConfigUtils.cs index faeca3d..af490fa 100644 --- a/VG Music Studio - Core/Util/ConfigUtils.cs +++ b/VG Music Studio - Core/Util/ConfigUtils.cs @@ -110,6 +110,28 @@ public static TEnum GetValidEnum(this YamlMappingNode yamlNode, string ke 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); diff --git a/VG Music Studio - Core/Util/DataUtils.cs b/VG Music Studio - Core/Util/DataUtils.cs new file mode 100644 index 0000000..fe16180 --- /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/LanguageUtils.cs b/VG Music Studio - Core/Util/LanguageUtils.cs new file mode 100644 index 0000000..ad1bc50 --- /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 index 057499e..cbce3fb 100644 --- a/VG Music Studio - Core/Util/SampleUtils.cs +++ b/VG Music Studio - Core/Util/SampleUtils.cs @@ -1,19 +1,15 @@ using System; -namespace Kermalis.VGMusicStudio.Core.Util +namespace Kermalis.VGMusicStudio.Core.Util; + +internal static class SampleUtils { - internal static class SampleUtils + public static void PCMU8ToPCM16(ReadOnlySpan src, Span dest) { - // TODO: Span output? - public static short[] PCMU8ToPCM16(ReadOnlySpan data) + for (int i = 0; i < src.Length; i++) { - short[] ret = new short[data.Length]; - for (int i = 0; i < data.Length; i++) - { - byte b = data[i]; - ret[i] = (short)((b - 0x80) << 8); - } - return ret; + byte b = src[i]; + dest[i] = (short)((b - 0x80) << 8); } } } diff --git a/VG Music Studio - WinForms/MainForm.cs b/VG Music Studio - WinForms/MainForm.cs index eb1b18b..8820c08 100644 --- a/VG Music Studio - WinForms/MainForm.cs +++ b/VG Music Studio - WinForms/MainForm.cs @@ -7,7 +7,6 @@ using Kermalis.VGMusicStudio.Core.Util; using Kermalis.VGMusicStudio.WinForms.Properties; using Kermalis.VGMusicStudio.WinForms.Util; -using Microsoft.WindowsAPICodePack.Dialogs; using Microsoft.WindowsAPICodePack.Taskbar; using System; using System.Collections.Generic; @@ -29,15 +28,14 @@ internal sealed class MainForm : ThemedForm public readonly bool[] PianoTracks; - private bool _playlistPlaying; - private Config.Playlist _curPlaylist; - private long _curSong = -1; - private readonly List _playedSongs; - private readonly List _remainingSongs; + private PlayingPlaylist? _playlist; + private int _curSong = -1; private TrackViewer? _trackViewer; - private bool _stopUI = false; + private bool _songEnded = false; + private bool _positionBarFree = true; + private bool _autoplay = false; #region Controls @@ -53,22 +51,19 @@ internal sealed class MainForm : ThemedForm private readonly ColorSlider _volumeBar, _positionBar; private readonly SongInfoControl _songInfo; private readonly ImageComboBox _songsComboBox; - private readonly ThumbnailToolBarButton _prevTButton, _toggleTButton, _nextTButton; + private readonly TaskbarPlayerButtons? _taskbar; #endregion private MainForm() { - _playedSongs = new List(); - _remainingSongs = new List(); - PianoTracks = new bool[SongState.MAX_TRACKS]; for (int i = 0; i < SongState.MAX_TRACKS; i++) { PianoTracks[i] = true; } - Mixer.MixerVolumeChanged += SetVolumeBarValue; + Mixer.VolumeChanged += Mixer_VolumeChanged; // File Menu _openDSEItem = new ToolStripMenuItem { Text = Strings.MenuOpenDSE }; @@ -108,11 +103,11 @@ private MainForm() // Buttons _playButton = new ThemedButton { Enabled = false, ForeColor = Color.MediumSpringGreen, Text = Strings.PlayerPlay }; - _playButton.Click += (o, e) => Play(); + _playButton.Click += PlayButton_Click; _pauseButton = new ThemedButton { Enabled = false, ForeColor = Color.DeepSkyBlue, Text = Strings.PlayerPause }; - _pauseButton.Click += (o, e) => Pause(); + _pauseButton.Click += PauseButton_Click; _stopButton = new ThemedButton { Enabled = false, ForeColor = Color.MediumVioletRed, Text = Strings.PlayerStop }; - _stopButton.Click += (o, e) => Stop(); + _stopButton.Click += StopButton_Click; // Numerical _songNumerical = new ThemedNumeric { Enabled = false, Minimum = 0, Visible = false }; @@ -120,7 +115,7 @@ private MainForm() // Timer _timer = new Timer(); - _timer.Tick += UpdateUI; + _timer.Tick += Timer_Tick; // Piano _piano = new PianoControl(); @@ -157,162 +152,109 @@ private MainForm() // 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); + _taskbar = new TaskbarPlayerButtons(Handle); } - 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; - } + OnResize(null, EventArgs.Empty); } - private bool _autoplay = false; - private void SongNumerical_ValueChanged(object? sender, EventArgs? e) + private void SongNumerical_ValueChanged(object? sender, EventArgs e) { _songsComboBox.SelectedIndexChanged -= SongsComboBox_SelectedIndexChanged; - long index = (long)_songNumerical.Value; + int index = (int)_songNumerical.Value; Stop(); Text = ConfigUtils.PROGRAM_NAME; _songsComboBox.SelectedIndex = 0; _songInfo.Reset(); - bool success; + + Player player = Engine.Instance!.Player; + Config cfg = Engine.Instance.Config; try { - Engine.Instance!.Player.LoadSong(index); - success = Engine.Instance.Player.LoadedSong is not null; // TODO: Make sure loadedsong is null when there are no tracks (for each engine, only mp2k guarantees it rn) + player.LoadSong(index); } catch (Exception ex) { - FlexibleMessageBox.Show(ex, string.Format(Strings.ErrorLoadSong, Engine.Instance!.Config.GetSongName(index))); - success = false; + FlexibleMessageBox.Show(ex, string.Format(Strings.ErrorLoadSong, cfg.GetSongName(index))); } _trackViewer?.UpdateTracks(); - if (success) + ILoadedSong? loadedSong = player.LoadedSong; // LoadedSong is still null when there are no tracks + if (loadedSong is not null) { - 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 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} ― {song.Name}"; // TODO: Make this a func - _songsComboBox.SelectedIndex = songs.IndexOf(song) + 1; // + 1 because the "Music" playlist is first in the combobox + 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 = Engine.Instance!.Player.LoadedSong!.MaxTicks; + _positionBar.Maximum = loadedSong.MaxTicks; _positionBar.LargeChange = _positionBar.Maximum / 10; _positionBar.SmallChange = _positionBar.LargeChange / 4; - _songInfo.SetNumTracks(Engine.Instance.Player.LoadedSong.Events.Length); + _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; } - _positionBar.Enabled = _exportWAVItem.Enabled = success; - _exportMIDIItem.Enabled = success && MP2KEngine.MP2KInstance is not null; - _exportDLSItem.Enabled = _exportSF2Item.Enabled = success && AlphaDreamEngine.AlphaDreamInstance is not null; _autoplay = true; _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; } - private void SongsComboBox_SelectedIndexChanged(object? sender, EventArgs? e) + private void SongsComboBox_SelectedIndexChanged(object? sender, EventArgs e) { var item = (ImageComboBoxItem)_songsComboBox.SelectedItem; - if (item.Item is Config.Song song) + switch (item.Item) { - 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) + case Config.Song song: { - ResetPlaylistStuff(false); - _curPlaylist = playlist; - Engine.Instance.Player.ShouldFadeOut = _playlistPlaying = true; - Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; - _endPlaylistItem.Enabled = true; - SetAndLoadNextPlaylistSong(); + SetAndLoadSong(song.Index); + break; } - } - } - 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) + case Config.Playlist playlist: { - _remainingSongs.Shuffle(); + 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; } } - long nextSong = _remainingSongs[0]; - _remainingSongs.RemoveAt(0); - SetAndLoadSong(nextSong); } - private void ResetPlaylistStuff(bool enableds) + private void ResetPlaylistStuff(bool numericalAndComboboxEnabled) { - if (Engine.Instance != null) + if (Engine.Instance is not null) { Engine.Instance.Player.ShouldFadeOut = false; } - _playlistPlaying = false; - _curPlaylist = null; _curSong = -1; - _remainingSongs.Clear(); - _playedSongs.Clear(); + _playlist = null; _endPlaylistItem.Enabled = false; - _songNumerical.Enabled = _songsComboBox.Enabled = enableds; + _songNumerical.Enabled = numericalAndComboboxEnabled; + _songsComboBox.Enabled = numericalAndComboboxEnabled; } - private void EndCurrentPlaylist(object? sender, EventArgs? e) + private void EndCurrentPlaylist(object? sender, EventArgs e) { if (FlexibleMessageBox.Show(Strings.EndPlaylistBody, Strings.MenuPlaylist, MessageBoxButtons.YesNo) == DialogResult.Yes) { @@ -320,14 +262,14 @@ private void EndCurrentPlaylist(object? sender, EventArgs? e) } } - private void OpenDSE(object? sender, EventArgs? e) + private void OpenDSE(object? sender, EventArgs e) { - var d = new CommonOpenFileDialog + var d = new FolderBrowserDialog { - Title = Strings.MenuOpenDSE, - IsFolderPicker = true, + Description = Strings.MenuOpenDSE, + UseDescriptionForTitle = true, }; - if (d.ShowDialog() != CommonFileDialogResult.Ok) + if (d.ShowDialog() != DialogResult.OK) { return; } @@ -335,7 +277,7 @@ private void OpenDSE(object? sender, EventArgs? e) DisposeEngine(); try { - _ = new DSEEngine(d.FileName); + _ = new DSEEngine(d.SelectedPath); } catch (Exception ex) { @@ -350,14 +292,10 @@ private void OpenDSE(object? sender, EventArgs? e) _exportMIDIItem.Visible = false; _exportSF2Item.Visible = false; } - private void OpenAlphaDream(object? sender, EventArgs? e) + 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) + string? inFile = WinFormsUtils.CreateLoadDialog(".gba", Strings.MenuOpenAlphaDream, Strings.FilterOpenGBA + " (*.gba)|*.gba"); + if (inFile is null) { return; } @@ -365,7 +303,7 @@ private void OpenAlphaDream(object? sender, EventArgs? e) DisposeEngine(); try { - _ = new AlphaDreamEngine(File.ReadAllBytes(d.FileName)); + _ = new AlphaDreamEngine(File.ReadAllBytes(inFile)); } catch (Exception ex) { @@ -380,14 +318,10 @@ private void OpenAlphaDream(object? sender, EventArgs? e) _exportMIDIItem.Visible = false; _exportSF2Item.Visible = true; } - private void OpenMP2K(object? sender, EventArgs? e) + 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) + string? inFile = WinFormsUtils.CreateLoadDialog(".gba", Strings.MenuOpenMP2K, Strings.FilterOpenGBA + " (*.gba)|*.gba"); + if (inFile is null) { return; } @@ -395,7 +329,7 @@ private void OpenMP2K(object? sender, EventArgs? e) DisposeEngine(); try { - _ = new MP2KEngine(File.ReadAllBytes(d.FileName)); + _ = new MP2KEngine(File.ReadAllBytes(inFile)); } catch (Exception ex) { @@ -410,14 +344,10 @@ private void OpenMP2K(object? sender, EventArgs? e) _exportMIDIItem.Visible = true; _exportSF2Item.Visible = false; } - private void OpenSDAT(object? sender, EventArgs? e) + 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) + string? inFile = WinFormsUtils.CreateLoadDialog(".sdat", Strings.MenuOpenSDAT, Strings.FilterOpenSDAT + " (*.sdat)|*.sdat"); + if (inFile is null) { return; } @@ -425,7 +355,10 @@ private void OpenSDAT(object? sender, EventArgs? e) DisposeEngine(); try { - _ = new SDATEngine(new SDAT(File.ReadAllBytes(d.FileName))); + using (FileStream stream = File.OpenRead(inFile)) + { + _ = new SDATEngine(new SDAT(stream)); + } } catch (Exception ex) { @@ -441,114 +374,81 @@ private void OpenSDAT(object? sender, EventArgs? e) _exportSF2Item.Visible = false; } - private void ExportDLS(object? sender, EventArgs? e) + private void ExportDLS(object? sender, EventArgs e) { AlphaDreamConfig cfg = AlphaDreamEngine.AlphaDreamInstance!.Config; - - var d = new CommonSaveFileDialog - { - DefaultFileName = cfg.GetGameName(), - DefaultExtension = ".dls", - EnsureValidNames = true, - Title = Strings.MenuSaveDLS, - Filters = { new CommonFileDialogFilter(Strings.FilterSaveDLS, ".dls") }, - }; - if (d.ShowDialog() != CommonFileDialogResult.Ok) + string? outFile = WinFormsUtils.CreateSaveDialog(cfg.GetGameName(), ".dls", Strings.MenuSaveDLS, Strings.FilterSaveDLS + " (*.dls)|*.dls"); + if (outFile is null) { return; } try { - AlphaDreamSoundFontSaver_DLS.Save(cfg, d.FileName); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveDLS, d.FileName), Text); + 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) + 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) + 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 + var args = new MIDISaveArgs(true, false, new (int AbsoluteTick, (byte Numerator, byte Denominator))[] { - SaveCommandsBeforeTranspose = true, - ReverseVolume = false, - TimeSignatures = new List<(int AbsoluteTick, (byte Numerator, byte Denominator))> - { - (0, (4, 4)), - }, - }; + (0, (4, 4)), + }); try { - p.SaveAsMIDI(d.FileName, args); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveMIDI, d.FileName), Text); + 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) + private void ExportSF2(object? sender, EventArgs e) { AlphaDreamConfig cfg = AlphaDreamEngine.AlphaDreamInstance!.Config; - - var d = new CommonSaveFileDialog - { - DefaultFileName = cfg.GetGameName(), - DefaultExtension = ".sf2", - EnsureValidNames = true, - Title = Strings.MenuSaveSF2, - Filters = { new CommonFileDialogFilter(Strings.FilterSaveSF2, ".sf2") } - }; - if (d.ShowDialog() != CommonFileDialogResult.Ok) + string? outFile = WinFormsUtils.CreateSaveDialog(cfg.GetGameName(), ".sf2", Strings.MenuSaveSF2, Strings.FilterSaveSF2 + " (*.sf2)|*.sf2"); + if (outFile is null) { return; } try { - AlphaDreamSoundFontSaver_SF2.Save(cfg, d.FileName); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveSF2, d.FileName), Text); + 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) + 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) + 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(); - IPlayer player = Engine.Instance.Player; + Player player = Engine.Instance.Player; bool oldFade = player.ShouldFadeOut; long oldLoops = player.NumLoops; player.ShouldFadeOut = true; @@ -556,8 +456,8 @@ private void ExportWAV(object? sender, EventArgs? e) try { - player.Record(d.FileName); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveWAV, d.FileName), Text); + player.Record(outFile); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveWAV, outFile), Text); } catch (Exception ex) { @@ -566,23 +466,9 @@ private void ExportWAV(object? sender, EventArgs? e) player.ShouldFadeOut = oldFade; player.NumLoops = oldLoops; - _stopUI = false; + _songEnded = false; // Don't make UI do anything about the song ended event } - public void LetUIKnowPlayerIsPlaying() - { - if (_timer.Enabled) - { - return; - } - - _pauseButton.Enabled = _stopButton.Enabled = true; - _pauseButton.Text = Strings.PlayerPause; - _timer.Interval = (int)(1_000.0 / GlobalConfig.Instance.RefreshRate); - _timer.Start(); - UpdateTaskbarState(); - UpdateTaskbarButtons(); - } private void Play() { Engine.Instance!.Player.Play(); @@ -590,7 +476,7 @@ private void Play() } private void Pause() { - Engine.Instance!.Player.Pause(); + Engine.Instance!.Player.TogglePlaying(); if (Engine.Instance.Player.State == PlayerState.Paused) { _pauseButton.Text = Strings.PlayerUnpause; @@ -601,62 +487,26 @@ private void Pause() _pauseButton.Text = Strings.PlayerPause; _timer.Start(); } - UpdateTaskbarState(); + TaskbarPlayerButtons.UpdateState(); UpdateTaskbarButtons(); } private void Stop() { Engine.Instance!.Player.Stop(); - _pauseButton.Enabled = _stopButton.Enabled = false; + _pauseButton.Enabled = false; + _stopButton.Enabled = false; _pauseButton.Text = Strings.PlayerPause; _timer.Stop(); _songInfo.Reset(); _piano.UpdateKeys(_songInfo.Info.Tracks, PianoTracks); UpdatePositionIndicators(0L); - UpdateTaskbarState(); + TaskbarPlayerButtons.UpdateState(); UpdateTaskbarButtons(); } - private void TogglePlayback(object? sender, EventArgs? e) - { - switch (Engine.Instance!.Player.State) - { - case PlayerState.Stopped: Play(); break; - case PlayerState.Paused: - case PlayerState.Playing: Pause(); break; - } - } - 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; + Engine.Instance!.Player.SongEnded += Player_SongEnded; foreach (Config.Playlist playlist in Engine.Instance.Config.Playlists) { _songsComboBox.Items.Add(new ImageComboBoxItem(playlist, Resources.IconPlaylist, 0)); @@ -679,54 +529,30 @@ private void DisposeEngine() Engine.Instance.Dispose(); } - _trackViewer?.UpdateTracks(); - _prevTButton.Enabled = _toggleTButton.Enabled = _nextTButton.Enabled = _songsComboBox.Enabled = _songNumerical.Enabled = _playButton.Enabled = _volumeBar.Enabled = _positionBar.Enabled = false; 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); - UpdateTaskbarState(); + TaskbarPlayerButtons.UpdateState(); + _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 void UpdateUI(object? sender, EventArgs? e) - { - if (_stopUI) - { - _stopUI = false; - if (_playlistPlaying) - { - _playedSongs.Add(_curSong); - SetAndLoadNextPlaylistSong(); - } - else - { - Stop(); - } - } - else - { - if (WindowState != FormWindowState.Minimized) - { - SongState info = _songInfo.Info; - Engine.Instance!.Player.UpdateSongState(info); - _piano.UpdateKeys(info.Tracks, PianoTracks); - _songInfo.Invalidate(); - } - UpdatePositionIndicators(Engine.Instance!.Player.LoadedSong!.ElapsedTicks); - } - } - private void SongEnded() - { - _stopUI = true; - } private void UpdatePositionIndicators(long ticks) { if (_positionBarFree) @@ -738,59 +564,81 @@ private void UpdatePositionIndicators(long ticks) TaskbarManager.Instance.SetProgressValue((int)ticks, (int)_positionBar.Maximum); } } - private static void UpdateTaskbarState() + private void UpdateTaskbarButtons() + { + _taskbar?.UpdateButtons(_playlist, _curSong, (int)_songNumerical.Maximum); + } + + private void OpenTrackViewer(object? sender, EventArgs e) { - if (!GlobalConfig.Instance.TaskbarProgress || !TaskbarManager.IsPlatformSupported) + if (_trackViewer is not null) { + _trackViewer.Focus(); return; } - TaskbarProgressBarState state; - switch (Engine.Instance?.Player.State) + _trackViewer = new TrackViewer { Owner = this }; + _trackViewer.FormClosed += TrackViewer_FormClosed; + _trackViewer.Show(); + } + + public void TogglePlayback() + { + switch (Engine.Instance!.Player.State) { - case PlayerState.Playing: state = TaskbarProgressBarState.Normal; break; - case PlayerState.Paused: state = TaskbarProgressBarState.Paused; break; - default: state = TaskbarProgressBarState.NoProgress; break; + case PlayerState.Stopped: Play(); break; + case PlayerState.Paused: + case PlayerState.Playing: Pause(); break; } - TaskbarManager.Instance.SetProgressState(state); } - private void UpdateTaskbarButtons() + public void PlayPreviousSong() { - if (!TaskbarManager.IsPlatformSupported) + if (_playlist is not null) { - return; + _playlist.UndoThenSetAndLoadPrevSong(_curSong); } - - if (_playlistPlaying) + else { - _prevTButton.Enabled = _playedSongs.Count > 0; - _nextTButton.Enabled = true; + SetAndLoadSong((int)_songNumerical.Value - 1); } - else + } + public void PlayNextSong() + { + if (_playlist is not null) { - _prevTButton.Enabled = _curSong > 0; - _nextTButton.Enabled = _curSong < _songNumerical.Maximum; + _playlist.AdvanceThenSetAndLoadNextSong(_curSong); } - switch (Engine.Instance.Player.State) + else { - 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; + SetAndLoadSong((int)_songNumerical.Value + 1); } - _toggleTButton.Enabled = true; } - - private void OpenTrackViewer(object? sender, EventArgs? e) + public void LetUIKnowPlayerIsPlaying() { - if (_trackViewer is not null) + if (_timer.Enabled) { - _trackViewer.Focus(); return; } - _trackViewer = new TrackViewer { Owner = this }; - _trackViewer.FormClosed += (o, s) => _trackViewer = null; - _trackViewer.Show(); + _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) @@ -798,7 +646,7 @@ protected override void OnFormClosing(FormClosingEventArgs e) DisposeEngine(); base.OnFormClosing(e); } - private void OnResize(object? sender, EventArgs? e) + private void OnResize(object? sender, EventArgs e) { if (WindowState == FormWindowState.Minimized) { @@ -845,7 +693,7 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { if (keyData == Keys.Space && _playButton.Enabled && !_songsComboBox.Focused) { - TogglePlayback(null, null); + TogglePlayback(); return true; } return base.ProcessCmdKey(ref msg, keyData); @@ -859,4 +707,78 @@ protected override void Dispose(bool disposing) } 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/PlayingPlaylist.cs b/VG Music Studio - WinForms/PlayingPlaylist.cs new file mode 100644 index 0000000..100f88f --- /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 index c207096..24f9e89 100644 --- a/VG Music Studio - WinForms/Program.cs +++ b/VG Music Studio - WinForms/Program.cs @@ -12,6 +12,11 @@ internal static class Program 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 diff --git a/VG Music Studio - WinForms/TaskbarPlayerButtons.cs b/VG Music Studio - WinForms/TaskbarPlayerButtons.cs new file mode 100644 index 0000000..d6185e4 --- /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/Util/ColorSlider.cs b/VG Music Studio - WinForms/Util/ColorSlider.cs index fa6171e..fba1516 100644 --- a/VG Music Studio - WinForms/Util/ColorSlider.cs +++ b/VG Music Studio - WinForms/Util/ColorSlider.cs @@ -25,7 +25,6 @@ #endregion - using System; using System.ComponentModel; using System.Drawing; @@ -35,9 +34,9 @@ namespace Kermalis.VGMusicStudio.WinForms.Util; [DesignerCategory(""), ToolboxBitmap(typeof(TrackBar))] -internal class ColorSlider : Control +internal sealed class ColorSlider : Control { - private const int thumbSize = 14; + private const int THUMB_SIZE = 14; private Rectangle thumbRect; private long _value = 0L; @@ -46,16 +45,13 @@ public long Value get => _value; set { - if (value >= _minimum && value <= _maximum) - { - _value = value; - ValueChanged?.Invoke(this, EventArgs.Empty); - Invalidate(); - } - else + 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; @@ -64,20 +60,17 @@ public long Minimum get => _minimum; set { - if (value <= _maximum) + if (value > _maximum) { - _minimum = value; - if (_value < _minimum) - { - _value = _minimum; - ValueChanged?.Invoke(this, new EventArgs()); - } - Invalidate(); + throw new ArgumentOutOfRangeException(nameof(Minimum), $"{nameof(Minimum)} cannot be higher than {nameof(Maximum)}."); } - else + _minimum = value; + if (_value < _minimum) { - throw new ArgumentOutOfRangeException(nameof(Minimum), $"{nameof(Minimum)} cannot be higher than {nameof(Maximum)}."); + _value = _minimum; + ValueChanged?.Invoke(this, new EventArgs()); } + Invalidate(); } } private long _maximum = 10L; @@ -86,20 +79,17 @@ public long Maximum get => _maximum; set { - if (value >= _minimum) + if (value < _minimum) { - _maximum = value; - if (_value > _maximum) - { - _value = _maximum; - ValueChanged?.Invoke(this, new EventArgs()); - } - Invalidate(); + throw new ArgumentOutOfRangeException(nameof(Maximum), $"{nameof(Maximum)} cannot be lower than {nameof(Minimum)}."); } - else + _maximum = value; + if (_value > _maximum) { - throw new ArgumentOutOfRangeException(nameof(Maximum), $"{nameof(Maximum)} cannot be lower than {nameof(Minimum)}."); + _value = _maximum; + ValueChanged?.Invoke(this, new EventArgs()); } + Invalidate(); } } private long _smallChange = 1L; @@ -108,14 +98,11 @@ public long SmallChange get => _smallChange; set { - if (value >= 0) - { - _smallChange = value; - } - else + if (value < 0) { throw new ArgumentOutOfRangeException(nameof(SmallChange), $"{nameof(SmallChange)} must be greater than or equal to 0."); } + _smallChange = value; } } private long _largeChange = 5L; @@ -124,14 +111,11 @@ public long LargeChange get => _largeChange; set { - if (value >= 0) - { - _largeChange = value; - } - else + if (value < 0) { throw new ArgumentOutOfRangeException(nameof(LargeChange), $"{nameof(LargeChange)} must be greater than or equal to 0."); } + _largeChange = value; } } private bool _acceptKeys = true; @@ -145,7 +129,7 @@ public bool AcceptKeys } } - public event EventHandler ValueChanged; + public event EventHandler? ValueChanged; private readonly Color _thumbOuterColor = Color.White; private readonly Color _thumbInnerColor = Color.White; @@ -232,12 +216,12 @@ private void Draw(PaintEventArgs e, } 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); + 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 + thumbSize / 2; + 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); @@ -264,7 +248,7 @@ private void Draw(PaintEventArgs e, newthumbOuterColorPaint = Color.FromArgb(175, thumbOuterColorPaint); newthumbInnerColorPaint = Color.FromArgb(175, thumbInnerColorPaint); } - using (GraphicsPath thumbPath = CreateRoundRectPath(thumbRect, thumbSize)) + using (GraphicsPath thumbPath = CreateRoundRectPath(thumbRect, THUMB_SIZE)) { using (var lgbThumb = new LinearGradientBrush(thumbRect, newthumbOuterColorPaint, newthumbInnerColorPaint, LinearGradientMode.Vertical) { WrapMode = WrapMode.TileFlipXY }) { @@ -305,7 +289,7 @@ private void Draw(PaintEventArgs e, private void SetValueFromPoint(Point p) { int x = p.X; - int margin = thumbSize / 2; + int margin = THUMB_SIZE / 2; x -= margin; _value = (long)(x * ((_maximum - _minimum) / (ClientSize.Width - 2f * margin)) + _minimum); if (_value < _minimum) diff --git a/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs b/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs index ed3a13c..5a073ff 100644 --- a/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs +++ b/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs @@ -1,4 +1,5 @@ -using System; +using Kermalis.VGMusicStudio.WinForms.Properties; +using System; using System.ComponentModel; using System.Diagnostics; using System.Drawing; @@ -80,7 +81,7 @@ namespace Kermalis.VGMusicStudio.WinForms.Util; * - Initial Version */ -internal class FlexibleMessageBox +internal sealed class FlexibleMessageBox { #region Public statics @@ -166,13 +167,13 @@ public static DialogResult Show(IWin32Window owner, string text, string caption, #region Internal form class - class FlexibleMessageBoxForm : ThemedForm + private sealed class FlexibleMessageBoxForm : ThemedForm { - IContainer components = null; + IContainer components; protected override void Dispose(bool disposing) { - if (disposing && components != null) + if (disposing && components is not null) { components.Dispose(); } @@ -284,7 +285,7 @@ void InitializeComponent() Controls.Add(panel1); Controls.Add(button1); DataBindings.Add(new Binding("Text", FlexibleMessageBoxFormBindingSource, "CaptionText", true)); - Icon = Properties.Resources.Icon; + Icon = Resources.Icon; MaximizeBox = false; MinimizeBox = false; MinimumSize = new Size(276, 140); @@ -350,7 +351,7 @@ private FlexibleMessageBoxForm() #region Private helper functions - static string[] GetStringRows(string message) + static string[]? GetStringRows(string message) { if (string.IsNullOrEmpty(message)) { @@ -393,10 +394,10 @@ static double GetCorrectedWorkingAreaFactor(double workingAreaFactor) return workingAreaFactor; } - static void SetDialogStartPosition(FlexibleMessageBoxForm flexibleMessageBoxForm, IWin32Window owner) + static void SetDialogStartPosition(FlexibleMessageBoxForm flexibleMessageBoxForm, IWin32Window? owner) { - //If no owner given: Center on current screen - if (owner == null) + // If no owner given: Center on current screen + if (owner is null) { var screen = Screen.FromPoint(Cursor.Position); flexibleMessageBoxForm.StartPosition = FormStartPosition.Manual; @@ -412,8 +413,8 @@ static void SetDialogSizes(FlexibleMessageBoxForm flexibleMessageBoxForm, string 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 == null) + string[]? stringRows = GetStringRows(text); + if (stringRows is null) { return; } @@ -563,7 +564,7 @@ static void SetDialogButtons(FlexibleMessageBoxForm flexibleMessageBoxForm, Mess #region Private event handlers - void FlexibleMessageBoxForm_Shown(object sender, EventArgs e) + void FlexibleMessageBoxForm_Shown(object? sender, EventArgs e) { int buttonIndexToFocus = 1; Button buttonToFocus; @@ -604,7 +605,7 @@ void FlexibleMessageBoxForm_Shown(object sender, EventArgs e) buttonToFocus.Focus(); } - void LinkClicked(object sender, LinkClickedEventArgs e) + void LinkClicked(object? sender, LinkClickedEventArgs e) { try { @@ -613,7 +614,7 @@ void LinkClicked(object sender, LinkClickedEventArgs e) } catch (Exception) { - //Let the caller of FlexibleMessageBoxForm decide what to do with this exception... + // Let the caller of FlexibleMessageBoxForm decide what to do with this exception... throw; } finally @@ -622,7 +623,7 @@ void LinkClicked(object sender, LinkClickedEventArgs e) } } - void FlexibleMessageBoxForm_KeyUp(object sender, KeyEventArgs e) + 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)) @@ -656,7 +657,7 @@ void FlexibleMessageBoxForm_KeyUp(object sender, KeyEventArgs e) #region Public show function - public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) + 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 diff --git a/VG Music Studio - WinForms/Util/ImageComboBox.cs b/VG Music Studio - WinForms/Util/ImageComboBox.cs index 45bca24..48826e3 100644 --- a/VG Music Studio - WinForms/Util/ImageComboBox.cs +++ b/VG Music Studio - WinForms/Util/ImageComboBox.cs @@ -4,7 +4,7 @@ namespace Kermalis.VGMusicStudio.WinForms.Util; -internal class ImageComboBox : ComboBox +internal sealed class ImageComboBox : ComboBox { private const int _imgSize = 15; private bool _open = false; @@ -41,7 +41,7 @@ protected override void OnDropDownClosed(EventArgs e) base.OnDropDownClosed(e); } } -internal class ImageComboBoxItem +internal sealed class ImageComboBoxItem { public object Item { get; } public Image Image { get; } diff --git a/VG Music Studio - WinForms/Util/VGMSDebug.cs b/VG Music Studio - WinForms/Util/VGMSDebug.cs index 1c7bd11..ab222a0 100644 --- a/VG Music Studio - WinForms/Util/VGMSDebug.cs +++ b/VG Music Studio - WinForms/Util/VGMSDebug.cs @@ -3,8 +3,10 @@ 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; @@ -48,11 +50,12 @@ 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 { - Engine.Instance.Player.LoadSong(song.Index); + player.LoadSong(song.Index); } catch (Exception ex) { @@ -60,21 +63,19 @@ public static void EventScan(List songs, bool showIndexes) continue; } - if (Engine.Instance.Player.LoadedSong is null) + if (player.LoadedSong is null) { continue; } - foreach (string cmd in Engine.Instance.Player.LoadedSong.Events.Where(ev => ev != null).SelectMany(ev => ev).Select(ev => ev.Command.Label).Distinct()) + foreach (string cmd in player.LoadedSong.Events.Where(ev => ev is not null).SelectMany(ev => ev).Select(ev => ev.Command.Label).Distinct()) { - if (scans.ContainsKey(cmd)) + if (!scans.TryGetValue(cmd, out List? list)) { - scans[cmd].Add(song); - } - else - { - scans.Add(cmd, new List { song }); + list = new List(); + scans.Add(cmd, list); } + list.Add(song); } } foreach (KeyValuePair> kvp in scans.OrderBy(k => k.Key)) @@ -118,5 +119,10 @@ public static void GBAGameCodeScan(string path) } 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 index 0c64c71..33a0766 100644 --- a/VG Music Studio - WinForms/Util/WinFormsUtils.cs +++ b/VG Music Studio - WinForms/Util/WinFormsUtils.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Windows.Forms; namespace Kermalis.VGMusicStudio.WinForms.Util; @@ -36,4 +37,40 @@ 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; + } } From 8ef1e7ed2ce040945e9bdf4dbafd7cd10eb10359 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Sun, 11 Sep 2022 20:18:58 -0400 Subject: [PATCH 20/34] Fix crash --- VG Music Studio - WinForms/SongInfoControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VG Music Studio - WinForms/SongInfoControl.cs b/VG Music Studio - WinForms/SongInfoControl.cs index 1fda3dd..b2bc0b7 100644 --- a/VG Music Studio - WinForms/SongInfoControl.cs +++ b/VG Music Studio - WinForms/SongInfoControl.cs @@ -297,8 +297,8 @@ private void DrawVerticalBars(Graphics g, SongState.Track track, int vBarY1, int } else { - const int DELTA = 100; - alpha = (int)WinFormsUtils.Lerp(velocity, 0f, DELTA); + const int DELTA = 125; + alpha = (int)WinFormsUtils.Lerp(velocity * 0.5f, 0f, DELTA); alpha += 255 - DELTA; } _solidBrush.Color = Color.FromArgb(alpha, color); From 0ed511d7e759f8b57cd13d9afb485234f45fb550 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Tue, 13 Sep 2022 09:03:55 -0400 Subject: [PATCH 21/34] [DSE] Align chunks to 16 bytes --- VG Music Studio - Core/NDS/DSE/DSELoadedSong.cs | 2 +- VG Music Studio - Core/NDS/DSE/SWD.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VG Music Studio - Core/NDS/DSE/DSELoadedSong.cs b/VG Music Studio - Core/NDS/DSE/DSELoadedSong.cs index 8f7a943..2cee106 100644 --- a/VG Music Studio - Core/NDS/DSE/DSELoadedSong.cs +++ b/VG Music Studio - Core/NDS/DSE/DSELoadedSong.cs @@ -55,7 +55,7 @@ public DSELoadedSong(DSEPlayer player, string bgm) r.Stream.Position = chunkStart + 0xC; uint chunkLength = r.ReadUInt32(); r.Stream.Position += chunkLength; - r.Stream.Align(4); + r.Stream.Align(16); } } } diff --git a/VG Music Studio - Core/NDS/DSE/SWD.cs b/VG Music Studio - Core/NDS/DSE/SWD.cs index 0b042fb..227f105 100644 --- a/VG Music Studio - Core/NDS/DSE/SWD.cs +++ b/VG Music Studio - Core/NDS/DSE/SWD.cs @@ -398,7 +398,7 @@ private static long FindChunk(EndianBinaryReader r, string chunk) r.Stream.Position += 0x8; uint length = r.ReadUInt32(); r.Stream.Position += length; - r.Stream.Align(4); + r.Stream.Align(16); break; } } From 4f98708f708cac4870814d2f8da21bc74ef351ff Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Wed, 14 Sep 2022 15:21:40 -0400 Subject: [PATCH 22/34] [MP2K] Add F-Zero games Closes #106 --- VG Music Studio - Core/MP2K.yaml | 42 ++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/VG Music Studio - Core/MP2K.yaml b/VG Music Studio - Core/MP2K.yaml index 1347ea8..94b9104 100644 --- a/VG Music Studio - Core/MP2K.yaml +++ b/VG Music Studio - Core/MP2K.yaml @@ -223,6 +223,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" @@ -979,6 +993,34 @@ BE8P_00: Name: "Fire Emblem: The Sacred Stones (Europe)" SongTableOffsets: 0x42FFB0 Copy: "BE8E_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 From e52aac40cfe6f00a958144e930fcec19133060f6 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Thu, 20 Oct 2022 19:19:53 -0400 Subject: [PATCH 23/34] [MP2K] Add remaining Final Fantasy games Closes #107 --- VG Music Studio - Core/MP2K.yaml | 75 ++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/VG Music Studio - Core/MP2K.yaml b/VG Music Studio - Core/MP2K.yaml index 94b9104..37be503 100644 --- a/VG Music Studio - Core/MP2K.yaml +++ b/VG Music Studio - Core/MP2K.yaml @@ -993,6 +993,24 @@ 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 @@ -1560,6 +1578,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 From 7cde095b41a36c697b983865ce35d9925f726f45 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Thu, 20 Oct 2022 19:20:03 -0400 Subject: [PATCH 24/34] Fix file copying in new VGMS version --- .../VG Music Studio - Core.csproj | 15 --------------- .../VG Music Studio - WinForms.csproj | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/VG Music Studio - Core/VG Music Studio - Core.csproj b/VG Music Studio - Core/VG Music Studio - Core.csproj index f82177d..95fb9f8 100644 --- a/VG Music Studio - Core/VG Music Studio - Core.csproj +++ b/VG Music Studio - Core/VG Music Studio - Core.csproj @@ -24,21 +24,6 @@ - - - Always - - - Always - - - Always - - - Always - - - True diff --git a/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj index 1e811e9..5c7daf3 100644 --- a/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj +++ b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj @@ -28,4 +28,19 @@ + + + Always + + + Always + + + Always + + + Always + + + \ No newline at end of file From 9e909504bdf4c5e1be42e802bf20143738df893d Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Sun, 26 Mar 2023 16:31:15 -0400 Subject: [PATCH 25/34] [MP2K] Yoshi Topsy-Turvy: all regions --- VG Music Studio - Core/MP2K.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/VG Music Studio - Core/MP2K.yaml b/VG Music Studio - Core/MP2K.yaml index 37be503..8c23198 100644 --- a/VG Music Studio - Core/MP2K.yaml +++ b/VG Music Studio - Core/MP2K.yaml @@ -1653,6 +1653,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 From df646b7a77f9c8c461f2d901f1482cf3bbabdc31 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Fri, 7 Apr 2023 16:15:56 -0400 Subject: [PATCH 26/34] [MP2K] Kirby & The Amazing Mirror --- VG Music Studio - Core/MP2K.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/VG Music Studio - Core/MP2K.yaml b/VG Music Studio - Core/MP2K.yaml index 8c23198..7235295 100644 --- a/VG Music Studio - Core/MP2K.yaml +++ b/VG Music Studio - Core/MP2K.yaml @@ -975,6 +975,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 From d6341c43ab07a8220e636c6a08c30c04498f3b8c Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Fri, 7 Apr 2023 16:16:44 -0400 Subject: [PATCH 27/34] Use KMIDI --- VG Music Studio - Core/Assembler.cs | 3 +- .../Dependencies/KMIDI.deps.json | 41 +++ VG Music Studio - Core/Dependencies/KMIDI.dll | Bin 0 -> 46592 bytes VG Music Studio - Core/Dependencies/KMIDI.xml | 68 +++++ .../GBA/MP2K/MP2KLoadedSong_MIDI.cs | 68 ++--- .../NDS/DSE/DSELoadedSong_Runtime.cs | 2 +- VG Music Studio - Core/NDS/DSE/SWD.cs | 3 +- .../VG Music Studio - Core.csproj | 8 +- VG Music Studio - MIDI/Chunks/MIDIChunk.cs | 22 -- .../Chunks/MIDIHeaderChunk.cs | 79 ------ .../Chunks/MIDITrackChunk.cs | 249 ------------------ .../Chunks/MIDIUnsupportedChunk.cs | 30 --- .../Events/ChannelPressureMessage.cs | 48 ---- .../Events/ControllerMessage.cs | 141 ---------- .../Events/EscapeMessage.cs | 39 --- VG Music Studio - MIDI/Events/MIDIEvent.cs | 18 -- VG Music Studio - MIDI/Events/MIDIMessage.cs | 10 - VG Music Studio - MIDI/Events/MetaMessage.cs | 122 --------- .../Events/NoteOffMessage.cs | 61 ----- .../Events/NoteOnMessage.cs | 61 ----- .../Events/PitchBendMessage.cs | 61 ----- .../Events/PolyphonicPressureMessage.cs | 53 ---- .../Events/ProgramChangeMessage.cs | 181 ------------- .../Events/SysExContinuationMessage.cs | 47 ---- VG Music Studio - MIDI/Events/SysExMessage.cs | 47 ---- VG Music Studio - MIDI/MIDIFile.cs | 173 ------------ VG Music Studio - MIDI/MIDINote.cs | 135 ---------- VG Music Studio - MIDI/TimeDivisionValue.cs | 63 ----- .../VG Music Studio - MIDI.csproj | 15 -- .../VG Music Studio - WinForms.csproj | 4 +- VG Music Studio.sln | 6 - 31 files changed, 157 insertions(+), 1701 deletions(-) create mode 100644 VG Music Studio - Core/Dependencies/KMIDI.deps.json create mode 100644 VG Music Studio - Core/Dependencies/KMIDI.dll create mode 100644 VG Music Studio - Core/Dependencies/KMIDI.xml delete mode 100644 VG Music Studio - MIDI/Chunks/MIDIChunk.cs delete mode 100644 VG Music Studio - MIDI/Chunks/MIDIHeaderChunk.cs delete mode 100644 VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs delete mode 100644 VG Music Studio - MIDI/Chunks/MIDIUnsupportedChunk.cs delete mode 100644 VG Music Studio - MIDI/Events/ChannelPressureMessage.cs delete mode 100644 VG Music Studio - MIDI/Events/ControllerMessage.cs delete mode 100644 VG Music Studio - MIDI/Events/EscapeMessage.cs delete mode 100644 VG Music Studio - MIDI/Events/MIDIEvent.cs delete mode 100644 VG Music Studio - MIDI/Events/MIDIMessage.cs delete mode 100644 VG Music Studio - MIDI/Events/MetaMessage.cs delete mode 100644 VG Music Studio - MIDI/Events/NoteOffMessage.cs delete mode 100644 VG Music Studio - MIDI/Events/NoteOnMessage.cs delete mode 100644 VG Music Studio - MIDI/Events/PitchBendMessage.cs delete mode 100644 VG Music Studio - MIDI/Events/PolyphonicPressureMessage.cs delete mode 100644 VG Music Studio - MIDI/Events/ProgramChangeMessage.cs delete mode 100644 VG Music Studio - MIDI/Events/SysExContinuationMessage.cs delete mode 100644 VG Music Studio - MIDI/Events/SysExMessage.cs delete mode 100644 VG Music Studio - MIDI/MIDIFile.cs delete mode 100644 VG Music Studio - MIDI/MIDINote.cs delete mode 100644 VG Music Studio - MIDI/TimeDivisionValue.cs delete mode 100644 VG Music Studio - MIDI/VG Music Studio - MIDI.csproj diff --git a/VG Music Studio - Core/Assembler.cs b/VG Music Studio - Core/Assembler.cs index b23b512..f822018 100644 --- a/VG Music Studio - Core/Assembler.cs +++ b/VG Music Studio - Core/Assembler.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Globalization; using System.IO; -using static Kermalis.EndianBinaryIO.EndianBinaryPrimitives; namespace Kermalis.VGMusicStudio.Core; @@ -66,7 +65,7 @@ public void SetBaseOffset(int baseOffset) // There is a pointer (p) to SEQ_STUFF at the binary offset 0x1DFC _stream.Position = p.BinaryOffset; _stream.Read(span); - int oldPointer = ReadInt32(span, Endianness); // If there was a pointer to "SEQ_STUFF+4", the pointer would be 0x1504, at binary offset 0x1DFC + 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; 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 0000000..7feb759 --- /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 0000000000000000000000000000000000000000..634b3cea9728e210c11e605cec8e977b38f29daa GIT binary patch literal 46592 zcmeFaeSB2awLZSiIdjfTCa*Jj~aYHJlLt=ihwwzkz)uW;2?Z?#vgz13D*sO{Ch-Dlc`3)?Rz>wbx#I?X_RdOw#u>Z!@Cy?u$mKs?qH5A_GSLeXd}8R!fL;)BsZUo_CLrX|oH>ke1v=6a{uqSrPO ztC?6$q^>y_v!K%TGWf0-t{j>bQ=J z^8fU^on#h%Iw5xr6DNpr*bt}vXp{rqgFhmwJuUBj7y%+rMz#m(6&dO3WO!#1=?Cut zAW!;g>y4EB1c*ASh;6l^0pln@ER|7T%}9xNyF(u_`<&`Yncg1G4=#;wCp%Qa)MlVO~e=JCwT z$9TAA5EahSsR)Xox{vhOB*>3d0E}pxx00+Gg|iHbi~eKsPw8H(Y90Btig9JZ0BZQP zID`xYC$oxv%@v%Yi1^G5t{Fs`N@U09WpIP=SZ`uF;3}np5j%$~&p?mQ6=IUU%VdS| z4ySb?!x$C<3^y}a&lc#3Zu#_JmiV?&CQ24kd;uCW4tOyPMTsCcldlKAf-)tiv}|`3 z3M@wf*8ynk6o7ngP4x21J+r%d zMt5CC5Z$o6x+{hOs#I+Tp{NDjbF-@ISax+v_UAHW>ryO;4#S>${D&D?(m)WS%RP3* zDnSI{ycX%fY}M;Rm(Nv|L-D_(mThcQp2pP z;z5{&waDn1-EQJ8G$=OIgSj9J-F~-Y`a;WRRV`uJY3qG-lx(gM#5hO~Gdw1Q7wxj8yu=Bwx+1Q?hCiXj9TXk0ObxIzyi{8?4G>OHmr z3Ap`?bDSU|cDCr_@C6J@YVJD|w~{%XNtn z!~$=%wHmQG7~||bb9q&+JMb2I;mqtDPgOJdjS9aVzKyuYl^Sy+ejz6&v+wzh4B4eu zZi;ts78=E@G&20!9T;KpE{QFZ$XuSS>P?pgg>8e%DzK9(I|nNLnd|6wuz@)ic;L}w zy4Sla^4p&+bh|5FYcI_5Ww8MQhmkdF8i)}P)f+1bW(Sci3v&B8L|unKL~_B0C{qDioWJMbD5f_Hd*-l`y(CF_mEr%K_EE8D3?La-{f|GP2w%%cFcObMee{I8056ft9cniee6woN}r4{9Xt%4#4#_ zd%-tci7TM1R}Yrp4Q7M;my&J;3z6jaVt%LEe}_y2TR&0~3r@MP3ZF-o&AMDlOSToe z%&saTsP>Sm%HhPW%A>;z?Kxau_^OJ~R!j+Vny$PQ%QLrIXjW#E&x~yrnkf!ShD;I; z6ElAe)`vLLvOtLxOdO?-&$8wEOq&C#+~q=#?(>wk1xrE8`exE`D2y-(yTu=5<7ci& zu^LQ$nYdn~M6FIL)Y&>(l{3lX1g&(uGklf`c)>~B4nZyH2J|aZ%k<30J!}@tXbyKj zK)WhZ8tE3OX#2s{@B>77c(D5>ah?Q zvF*0JR;&YW*?F!r1wv>t-gGoMb8LbeOd;2Dw}ej_)m(D9*X!;W_9bYQYTpK=CM<88 zYRFZx-b!LSt#hDHO;`@~&RmfRZi}e>jz~~JS_1w8mH?H-T;*gXVO5=jfttLC0wRbf zubwHXGUg80Qhlbj#WmF ze}P-N&`!vn!_HA2%)5Ht2yzepfu!%8T~Zm;94$$CbZ+Govf_`UV(cQ3pA|!t^Tsab z1Z9aE=Vq}J!#ig_XGt!mD&{BeJ92nefp=_7_*f-z368;M#?+p{h%JGz@!~<)JLXaR z$7s&5BNR>?ECeSGk?}oe1Crv)Sx+)eod?7U*0F+10W{<19JK<>tl~E8eWfaQLtkop zDb?z^T7y&*JuySmdq_J}kKRJsqMiNVui3m1g;7IvVVO7d@fI1QL-F5dWRBXJm0<76 zCMxB3-n$^I+4kv#2>7RKn9E@HB5-?$xtyH;tcq*pAXiE8|AFv0j!XliwNu5T5aJQ8*$6$HopPBy9OLZw@%)ayy67-;SLf)|xNu>N3mmj^m@DW(#9_2p1bQ#C-ng8l$T- zzN1vbD1Qe6U7~_L%BI~S@}5MVImQE|!!FfVbJ(#>rXRqM(4U!5%Jzde`vLbfhMvm# zjWxENf^~fgHB9y)KC6{GPQgY?jp7G&3U1aJ-}oq}VDqQMjdlvwEtq#EPhxYQr4skS zY3VGUG(kK^&6DNmBbq?WQ@*3lAzu5YB_9^cXCLA!R2jg=OGPzK!+MCAnNdylDLQO9 zW)36Td;=ww-(l^39_lC1GOmvd5zT(A@fu{$Szc0QuHL|7#H~QY*YV~ggJWajYSc)L zRVpSfa%b47jT5OTXbW|spiDPAb5s;1goTNmT{EBOetW4aWy@N zl`mt`VM{WJ;!&s(-v_`Rlk*S2{Hb{+oL~#s*=i(m+{M*dZjW;aQM{*eg|~ zJcaEfp5jrS!fA{l*61;wqSo9WqAM_1+Lq}+piTz=1q}?bk#jN-^LVXdITKWnEA3q{ z@F?%1$`0gO=`A4cKh28q!raJ@MH^qfla%veu`WT_@%tB}7A%cJZe0*;QlT42@9H0Hg2 zYegVK!#R$IB{!~j>aJrMqT9BKSi!GphTtG3Aa;#aWfY$Ulj)LkfV1sk3(!YCWhB<1 zl*$XPtjn;&DW?x*H!4$<-Z-hVLy;vnRupu0w7Syj87qU>o_j^V-P{2g5AM5+=>)q` z7|JR&Zner>w_4-OTdnbVu3IZwGb|@NJRVzE4xxal(i_)fE67yYjW{MKavVsTJKnLe zOV%EoKr#M^nyUYMrT@HVa%Mu=9Jf&s5ZA!*7%St=C}L!;j7IRQtidS_Qv>_^dyy}N z5(qKgt6a#CqYfqJ*-kxEOx1Qdv=yS=@j=NH8t9xr+xsj*?BC0M+#g( zS?3Dm8V3+gHBb){HCC%JyUoAQa&_&ih3i(R?)S#HsvoiH*=a zH_NM+RD0FoOa?`48=!I7ekuuGiXtWJ{Zz`2mAr@soA8FweEr$=JNQtCCD zU=KLdB!^pC#&;@}!48PABhWZ6FJA1YmTH3Yc4Nms0dpa9o#r#EtkeI>(iWjY3{ES1 zDXyKdXmc`i(dIomq*AGjWwzUkYpHpKR@H1g#Ht8XSM%HX)s>YOSrx7G@~nz=)l-?( zS{0pC?Dajiuvrx_`PhMaW)KDeyOG&N-r9>)wuJ{mSjZrumnB#dk$rB7^}%kb zQK(9K{8@glKii+fs}RaRi1HJzhtJ{(sLvx|>I>6LeOWeD=JVRrIA69+jrZj!Di{{Y z4?q=kE!Ust&-WJuF=~+8OwASAQrhCY?Gg>{F+ z79pY*`-*X`8()k(7vD1$$IB`AT?0iu-2uTttW)bXLWxD$;|9 zqS#t;N-iQdRU*Yau2kVv5&{f-L_kRq0t`G7Qw$-t*bj7ZMHfVj!_BLU;}+jYsyH|q z@8fu0#RsI@+>mry=P+m4Rzc&LkA|mk8)$2){XUJ~S>74XvRoS5YDE3YVXmRG(2Q-h zI?L{3lN|+&Ju0rmgWAY&XFJVEX&P0C(O}j zSvG%4T*iR{y0UJ3mwo!J2%GyXmADU1OJ_ODPG{GFE1hG%pUdy!n`iVTF7EgeSJ8tJ zv=Lx?%0@x?(u+{(jK0K$&*V$osvhh|Zu(wCgq+EjxY++^z9b!X4NG6mVP&1szI0H0 zX)Bb`ScMKkZB02dAa8_!2(+A-;5;{XT== zZD0B<-VMY#=bi+2pIqFz7~?8>Fo`zoi%MLKz0F=UKIim^#Kkflz!Adj>A^u;2=-`> zkSS*n!o@O$Nc=SD@-cck_iMIV*vrp=u+ba+^v&8F4kI@mk3`6suHw1a zC{Nd-$=wBHeKfOHa(!AEd}>L$(NF1TEv$dalpl0B zxh2QQqlc3%krFq0WLTZxc(oNd%gEr6>zaYR@5fM7;SGJIn{Ko}kE6!QAU>Z#PHIia zu)J5@-e$Yi<%4{1ry%vIicufj$+)gHMqSq$@%yY*O(l@2JF~KaxW$BO6MCvMDy3MgF^Ni0Y&GcP7Hv6myt(%c~?fy@WBP)qN@u`BVO?N*=1#7-4A zd^}~=v*_taUZq>^;O8JZ=|6R+T;p{IZ3tjpyZW|wk!7i5dSD)UF zVSDPuNAYmqI`b!b@Gz?9crs+EM;QE|Wi|QT6*KKn;*Q?~W$X~)&M0qxTGJi>OGdu) zSjU|aOJ<<|Y*$D z4~_Am%2JRBiA?5Lpr)p9sbFd9m--k8NYqR8ID5!8oWK@<=CApE;?raN*yE zAw~ggQN1(&M+00ZDjSoLClv`B=jDRGn-HmXjG zMhqKJ1lb9=Cuv2QfQ%PA>~`?+Q8_l76+;6smR(&b=}#-^jjkHNOz) zBtD{Y>85t>3L3TZFiq$j(MdkA<1NiryGRr>@PuS7pBCD^l}pJzUM@rA@dX0jF*Ioh zJgJzAIhk#hcV?k@n>Q;k0=e9jJS{S)(xOs4@Oc1Hzzz3S_Gm*HL|DX^;5X4HGNh?u zvLD4`&Y4r@(Y+4<5Myx7msY-dk%-paxfSwFm%GL@_%pm`M;W;^ngd+_^E za7M-JSG)R+pL)&Yr zXI0OgHFx#`=9m-#J~|ic&NcW#Lk-c$G|(-{cwe*!J;qlS5ey^f)6@+uG!c7EkTiAq zhNcF*S0UZ&MS5u@hNp7!gQ=@sTJeSKEb!@nw7LA?nOk3s7uFbp<>@iJ--VYCFSs;) z1iZKKj{eH`1?S-XLcDl=z~qtoY*P*k*#F|^6?}e^e=PcFVWQlksYQ(I{2k@_^eJrF z@C?--E4Ij6v}2q_3kwUYk44{@kSO=k{E19ows(wU zX@4Ha<&PG#KA$dTdctJJlhXRjQ<=^l&wY8Ze4fWk_g8Q)wiIuk3dvID%c$k2OQr7@ zySSuL%w_5guKNPyv)%F8V*H3-!QpfKTPIzVN`=uEPO&Ex&`YqvMW8W$NNtl8zF9`Q z;YmL%dCA>`9E&vM6_vi^Etz5b>}>-tx%=Q9=SXUA0jKuqi*Vw7QOX{GVIUBM;q*S<;yQa59yIdz|;P!*|T+C#9+8|BxV=g_j^ z8-bnU4w)?@x4A!KPNwYa&zc(ji}yCrd%Z_&zD5hOzXbkN!PmTpko!;;m%kzFdegGj zd2TZ|jzs9Q!0%~{-`77Y{2M`^tqDY;Z*x?X48CUjir$p&+76AZeUzSm;>yG%Z!-9_po)Y}E$+dh!zbW)op_d3=F8H5$Tt*WO{eq7O=S{)y3U-KYAI;@6a1UtCTZm%!E)lX^iJv5ce7zykl6fo!7mFod%l?(!Nn84gIZlv8J`X? zekh-DzuciUlGZ>O1R7;;~Bpyt^B%x z>DL5*CX$=c7W?P-!1sC{0{+FtxJU3^gXw*OS%OtMbN)t*yWNbh3q4Kvnv}Ul@UMcq zM9%L8n<1auVxRxr)Z@Uv2sV3;r}#(4KL-9)f;R}>C%8^y{(7D9tJ#e40OPG5#&4D~ z{(LIqc?FCi;nxUVQ^B0c6B*wvWBi%m8-lM0J|$Q^j&q|@_dKD?rS;oHeq}2~|67Fqtk6@Wm7UUJMB4hQl-DM4-2%b? z7Jfpc&5^ourS8XtJ|OgDDOoA_Z(^UHi+x4}17e3hX=Rsi7KqF%rS-3goUeZQDRgZyms~0M zuxLI{df^odzb2e+vH2TPW`m65dda;yIwflghI_bJdSf(j>k8+2p9QD9;y+UJ_ZoT% z{k=nQkKo;c`vtET{JP*L1wSL06#Ts4kl@9FmkZVlb_y;RTq)QixJIx=aK2!h;6lNN z1b;1fT<{ga#{{1c{G;Ggg3k+nTks{p2L%7DaleXn#y5l>FZ5eN|6Q;`IPVFaE%Ylw z7YO|ap?@UwUj+YM@GZfg2>wj)9l>7+zAE^L;GYD4D7emGO=5x@1e1cB1TPmH5)283 z1)Bt~6l@lJT<|{ypA`Iq;M0Q73BD%yyx@z1#{^FbJ}S7{#ri}9*9&eF+$i`V!R>-q z2zClC6YLSZMDQWOUkDx-JR$g);1hzs6?{tY8NqK0{)gZLf>kE#bBW+A!8L+4g0+IJ zf{O%~2u>4h6buUP75tjue!*`E9uPbzc)#Ew!DbJ)o98)%ed9Alw_!fE`gm@BWy&vs ze+3_V#nNA!7S#yWP5Lc3&(QCIhR$W2@^66V@)n(5=2!X~=Gp+ZtF*7I5vL)IHlsdpje!>JVKeKM8e6V6dd9ij&UhNaP?cItRJt^i5>{J@O*3N1G+oy_UI(w>J_ zN@|~HVPKr)p%y#!;Yp9;OnaH6_IM6X`aM$Jc3#7{Nmdp`R34;EL+Z&?YNnM<4^HJ4 zen8w_E_F;(sgD*P#A#^TbWZK_%$!x$d3-~XJQ_ZmKQSwKhayyKH@(|q)xN=g5G^thzYqrYhR@@soT92m)AM@!(dU8suHG$s5btmU>9|H8dnVj0^>CWfW>vn3Nr_&11 zJ1T|tdXT!PnoDx)QELiCB&F;!osQZmmU#~4oX;iyGwUa%0i589B(=};WC5opN{Xdy zx6UCPA@IZgGHeCuaY;R*cN7J*8T5*sV$08tuB6Y+O1Dr+cSvfV=h+E+@J)iR z*r{tErIPNGlxnY%I%YfV?YCyq(~?r{&7wcsDQ<5T6xqVwrR4eo3i5;NnMqevr#nsv*BH_IWlH++@|zcss@I z)zCCasrG7UU>;Xwn|;ojM-3Nn>L$7-_YP}5#U-_mx^wTgYH7cnSCjX3YXLoN=S|D| zuCX zr^LOH_&ej=qqlPd?lyYT&U-Pp(tR0CTgZ9Jx|h>JJH^($oFbCCk-k?v-+cu=FDYf~ zcKQlmhQZ5k!>nqjZ%Jy8=PrCUx}6@h^S+6^4mx3{+&LHFEdQ#VD$H4m)a!PtA9FfH zf0NW6&vwk|5b5~V7PnA`yiW2;YLDj< z()aBYx3`s^mz2^hLf@!EHG39E=mANo_9ArLPH}q?dQwuVy$J1G%Jo!xQF>KUs=aOG z=F4-8AGf!Srb$Y*7pHe6rTP~qvq7YIuCfAJoO10{y_-{Il2Yx(>6pqx|1NbWsj5-6 z!1mlhEA14ww}W~mrP|v`b<4P=d^*Oot?;+PjAOSESp!h6W|I&oi&=3imZMA}Q6vHT0^=LknH*5vpn8da8vF zQ;VJA7Cubfl2R?~A@?PsnMA2Qly9dhvZ`qh;rD6bhg&#k?V-n29#Zd`d&#qs^HdAh zQH7o27OtZPNvRh0(bJMrad00!Z>QRQ)tJ3+N=mhWdA^EevKPE-euSQulxkr=y<(@h zh5eM%%q3L|AElcmwa+uH^h)99&cN(`yjQmG;L0eX8i*SnFPDH(BJPaSJGwU2&L zvfup)I%=o>R&s;;AiZX%o||;YeIp_KNXg$!y50RrDzj66!|eYg9k5g0io20_mz{d6 zXvqC3DqE-O{e9{|+*9sVDa7mh+=u9(oqEvsDDrN$Q!@e&An%KI>dyRtc8I>FQs~j| zQSyFC-9!P;zqmh5Pur<`+>g0$qBm7Z+KlnLnO3al)|HiRrel)2iGDKv$L^cyMLYFm z@lV{hP+JR^yor8Q{DS+l^q`%3xb%ekR(jP=?JxSV`}363$|Y~4JBohmzKt%F)J=5% zxZk^Pryfc1_-S+p?UmF?ZJZBx-SnuPYMEM%)YBVS3icry=lwxaC$nCj{1^98dPAkO z{Q=JVA3JYk%Ed^zHgdg_+K!2DxxYv)cB-s=1AU1ulay-xOSIF@Nn_N>56+p&vxYi-#>1K@n%go)>7!cwRVkNY)aiu+rj`8Db4&2y zAH(ixx{PMGlF2y=IZo^Q-8y9Mce7;0@14q=Ua{M+N}2u@o-1i|iS%@zk2#FG%~>g< zunD;aeF-+fH(${^lRkrA{HM{%axS^4NS9oG;_(#toXagPl)CK`3knSSVo5=Pi;jr= zg;J}1BG>w23D-K}3}1=9X+P(XSyP9&@@ zFDTIIVi}R=MJt61#P=q_+ci=pe=a`of{cZd_OR4aHsoF?YwNZjD(}s-_SNEh0kmt- zE}2Ivw-quofA~C`nfBxJQiXVp!)rWV6Y!ddS2+^Wsf~Tr}ypG_N zhu7!vx&^;^izB5EXhBXRo>!g+ETGwf3k2%~n*`ShZW0U$_5q7&8_=SU0w>S|z{&K2 zaC};0`MG$uRD~G0K;zh#L$^=45cHjhtqZi<5!H%le)&qO)8-?JHKDwPUT~Rd!rvsE zA(3_q<(0+oTt$?|PJWYp(>JMfB9C{sQ<5?YF?~W!C{Obp4(_sudOd6(zS~U&gIW)9w}> z?xti>pLQIQ3+On#m%mGU0`v%w^?5?{VZ2Uq51{1j*@y6ijZd0f{*bm?`+D(hS{1z# zxJ$c*?kT?yr>R#0k3jx+%Ksg?Ie{0oTeL0H{-_<1zTB<7lwG9Xt)1_htUsW=?w$wy zN%=zHCyVR!lk~*+X5g;UbwD1|lTjeEq}28@tR zUmQ5s*sWb0s1}?jxX@T6THOLn7`M>tQ+9*jHfqpt#=r>_HFq;CLE(6@lUp>G3UrSAg&NDlyC zqwfRXq=$fS)5E}b=s1wHM}RKvF`!5LAuvaK0$8B^2w0>&1uWD49XL^Y1{lzO0-UD( z6d2Tg2CULv0M62W0j$wp0@iB30xr74t%o6QoV1xEs;0o;(;41BRz_r>R zfUVk}fNk2Jft$70fgRc#z;5lYz+UYwU_|>Ha6tPzFsZ!@+^M|>yjs&tjD=oXoTjPMv);OrMH4f=Ff^)O}Dd4U8r-6rc_V%OtdC0v> zuLRzs&jjA9pAWoGXK%S*-vNA3pAF72eJ=1(eID>}eF*#~^ z8;mu;6~;Q?Dx(Fs*4O}SH6DPpHskxi&BjB(4&z~9w{aZUYdivs7>@x5j2{A%#uLDu z#*ctk8&3g8jDH92HJ$p_@>bXeA@^E-!XcCU|T27ZH! zW5Nm-$AncbjtOgB91~hy921^%Z9~cDT?yceu3_K_7tey1`L!f_Gdz3&`YI;4*m@cubxJIrx?XdSUV`C^ByZr_B5maH9EXV8HwgaGJ@p zAZYR|s4{sL%rbcv)R=?Ftu=Q57nwW@mY6&X8cd!AE6gGASDCwjYfYX7t>!JD+sw}a zH=Caac9=X1y3N}`_nJq55%UhXL9g}APSv(6|)-ynl^%G!@^;2Mh^)q0R^#ZWW`UP;J^%5{({R%kEItdI~ zJPWEUo&~cko&`15%iz~qJPQ_CJPVdszXhkkdIh+`;#siD;#siP;#tsY@hoVwUI%Wr z-T-!3e+711ZvlI)zX2l_&w>GqXF<~9S+LXMS#Y(*vtY#HS+LjQS+L*YS#ZGOS#Z$e zS#ZeWS#Yz(v*1>XXTf2MXTed6XTe<-&w_g_o(1<>JPYo#{*D&!x84OlXuStKX7Ma| z)Z$t2xW%*JNsDK}(-zNy=d5hgL(f}#Og{2wLmYhQRH_Cr%O0pl%N-~!ouGp z{5`_o13r1L6aEq5AE90JSk@gv-yvo0K$&yBUz9S(g@0ViJc4gYY{+^<_>T#mlw9nG zA>n1oeOdUw71T68${X4gTJ1GJzh#<&lkrKqwONz#nYqWZrXqI(kaMRCE|J_i8@WuQ z@H>PPvXS{=;g1Msw~frdPWXp~bHqmGe^L043I0~l)TLd)CA!#CXMO60(;=KL;dBXS zL^ykdvqw0Gg>yIP4Oz#8enfH~k=zr)c^RD5-j{_l)sX%g-0l*g>uqFChtOR%GG|2S zJvK7uu+Vqd$ed$BKVljw5OZTLh zXNh2k;E3R1!DCY9gesZC5;}6EUBSbG9l630JS^CeCmg}Uf*tw75j-r|Q6L<_B|fGf z_4zSU0YB4J$at*q<8)cp3E=fvdyCj^M~iR^;Qfuzu41M~ibboEmr;JCg?M zKyZm*hv10dVZmd9fif<+M6g3}MDVcS@hPmy@hR-XCxt#Kv^JIb+EnIG7J9PKbwbw( z9TGYu^lqVd3w=cBBSIe+`nb?1r}DU*6wXQEXw#&XY0`?&lZCDmx=!ej&>^9B3%y(D zBSIe$`nb@?g+3|tNujmrB7eHb7kaYLbwbw(9TGYu^lqVd3w=cBBSIe+`nb?1g+3{? zc8x4cspGWri{PFbTtdl~AYMHaUmN`d+J}$TwpOC+WzDR$@w?9p- zMyuD>X;JO7+Vk2g+8dgo=jt={<$6>f(LbvHRex8<=d_Km@k!%$CnTfeZIU{0VC>)`lG1*W^<%PS_>5H!1~Z9KMe>o+jYm zB;4HKGfcR-nS|%*75KhS04vKBss!hJ+&9g}_0`3J0^Fw^n#A9BSut)LaN*PnU{LVQ zifN!9o5a|g9|V5VQw984>1^QB1&oT$#^>sZUMgZvW|`XyE=+MA7dhV%{7Bx#;9n)J zTq>yKyy|NJ|H5f$$)|HJc5`lzU}mjMeHN5ml4^Hq)|wREMuVZA&f%7n4I#zdl?FBa` zQ-K#@#B_W|^jyf?2-N9P+z#rvF{uP+6Hv!DZ1L%Kto<`VZ-yidzpLeZ&|Mg79XBa+ zfnmtju^!I{MrZ-BAMNN^%lY&ENxB#~NOizn_?D868C?gztVcfBt}H3izM;X>k1-cg1`|pIL4(#(!+NUfc_h z<~6%KxUc5#cm~jZ0Mv(1DfK~4S3DcTIknWJ6_B2 zx?a+!K%dgS5Bg!e+CjI_a-$uu<#@GFGw5c}{Q8i=?b4#g=wN?19!iE=|3ET4cNW#P&!VMx)#KHGS0i4_@LG;nT_<2E zLjyx2!*YgYv3P$d$(5UDqc)(Pp@E^1VHv}62FL(H2oO?$5Ceo9U|l=c=jvRWD|210 zI-3?PPC$1SAoZoX)RxM#siij*A3)x+NGOT7CcFEck#NUsS|1K|uZc#6S_VQdR7%(L}f(l^}Y{mT*_nwi1#Nq~d*DYx_dc7)p;ZAuSI_VeQD;!T3Nd z5za6!wG2g*z4lkJP?1whLx}{)NNB*Ogc%pEvL<)9zrB4~Uo;en45=P$Xo@E1&Y?@= zeaWy**R{`q#o*d_HQ?2V*D}18dC+OWi7k!(HzRgA7=Av(N(YLwi z+gw_dl+MlN&Y=_oI)^e0=-gbpbEv_cLvpmbD0CKeQIzMxoEn0)FQ|bQHPE64TGT*` z8fZ}iEoz_z@;U)%961bV9DD}lj3~&BBRRTp6go@eC@PK5L$mYH>^wA!Ph;WLh}SZ_ zmg9wHk;#B&k;Q;!=h@AoAUBKT=w?yqEX~f73BoQl`+~H4fv#)609srCEiQl-7eI>( zpv48y0(qSPXn`CCXaPQh>O2av7D$fP0)@_^1vfVz&CN%1^U>UVG&dj3%|~Wb12BoAvwA^6go?D5ZPXf=4#PgEt;!EbG2x$7R}XibI4>sbI4*qbG2jT zgL@YFEO!CqE`Zzxkh=hK7i5IxbVNp}PA#-Y;|R`(@T}W=}ZT&hJY@kx;ZN%;efwJQ?Z_N0YJEzJB4< z_lBa;aAadFf<=mpNC!j7zF73q-f%b~wL(#TTOEt85BDHOhvVVywITdv(=f=@gZ-W1 zIOi{oheOHUbiid^l?n&)XbZbkPLmQ_WLdj4gmc=1Xa7UW) z{(cH@erilI$a-2C9$K@do)Y10gJGBfU$jk?YoNxqwT<zW&>o_i2(XZUbmxsB0oEOSe-_{4|6Z?0>j?V-qExV@cLqT&9| z5H*M5^4>a##EMY7KNjul3em>CSR_Pix{wK|swuG%MnH4$lEquw+m~X0h3{D}>kCJ~giD;x5}X@dwRxN^#_Mf&*0hK(6_yh zC*tY3>8W{o9;UZ!TN9^ewf1#kBYT?JQKNA?eBumQYFtmRvMQ8Fp3Ys?7e5nwHU3`k z>4jnU?Wc1$^4|jPOAeiuvxOHYOfK8u5FNT=NMZU6B>K8~WASe6iTb+2#EyEw#u$^b zds3twQZtw6_6^ZQXiJ#AbN1@+j=G*OCVWGa?O7^@DKA4~h-Ws<)6JpR_9eS|m%{&3 z3^krS-aTQ5*_h}G4Wv?QW09eOUY_!f=Q-tUFB!|`9XPu~Dx#&zOYl2m7S|aS%xvXV zygXA5JNud~TO8R6j+n<0&BZeeiV@z@x6{c~b5DoA!aX5uF;wY{J%=L>%UpCYRe*I} z)`xa3#UD*i;vK*JVn=joEXi~z5=U=`xXT=dsUf@_b5Tf%OG5FIn~K!b#V2gx+wo9n zSUefW9w(ZVJ!g-Oj z-CXUc?D(>(FN*H*aZxDN7coNU)_APHGluMqeVy?TPvtPR4Mk##TGJVWJwiQe`vz1j znp@ASa||gK!fOy^gb+!~k|GoqdwI_c|H4Q|d`Tu*3`e@zE!ze|aX7)wn!03vEHTiF zy&koMLkWaL)uaq~@5E#b@J;iO7CyPdS}RVy;gMvH_7 zQKuo?IoQ(^jxUYJc8GhnB(Xum_F{H>I~)!qTcIgYAL}28U^l94EPWa&s}n98L3`R(NJ_499urae8J` zbc@_E$V5DADR}FazMes7H%drDES$iotq)^go*{dEcuNZw;^feJ9Mlr&HkGMG#`BsF?Mur_Z zSV1V&*%!f%C0%W8D7q}d-qVT0343TWmPjP~)!XuLvc9=t=@2;Fq!1Emgkletc;M`n zH{v|Np0X+&?Me2kEMQ+ZCexYnGDC+dy*{)<5pjpu&U;u&3?;(O3XYf&+Rh5~$MCi^ z7K?;K(U!ieP>P{05|P^htfsW4KiY>gC+`-RFVm1|4o5Zhy?r3@*PU^v&?BsQ2(NK0 zVAP7Ui^RzF*zr?iv@6!lgdJ6ElUCb4#W}4pb+1OSm`$yU_3(j6O(`6tlD!f!lx?^i zj;}cTgmGM9Yl(qJ8%YJ6F+6Mm=kmTR!Pjk~^_-T_jwFt}z4m26Vs=A#Af1{sDm8ah zs%BJbo=Pp-S=-Xpx2%uvT%sX)+dA0Ga~5tv7&AB+K5!iEUb973Nd)vcV(}zjL8net zd>C7c1&+F~O_pU2F)JR6#`?qc*!^y$OJlL_PR^5%Dod;wK~dOCD1ujTY<~v_25`R+ zwwY@1(*;D4bTZ?FBSz0mFIqg8kB?oE!R~M-e|>m6B1yP@2g-?L9f`pD(XEm2(lE&MM!{Zir7D~+9O@@*NB9l?CwD1gVSB?O z#T=xGs;Z`-dZuMV|86!IC~+?^WZrrR_S_XAfjZ(d6w)s zj?D(#xkcSoq~bt$i+y{cY#K}8P6E>T2)5QqG({1?`nnsiRyXeK!o=hfxVDJxuygFa z_2>eKR4tCjr>oV)dt{TjW-z&Ci|n*d&&%M9a=+Es_C>MVOC(}=$4)8Q)w?1V$52Fj zaLJ@-UWHm?RM!*hiJ@%t$`}v-{1u@{jPG-Rn9*G^yf0OYmED9qE=dn6VraKwrKUid zwIUYXibkSaRS7w3(TBqE7_E#YV^~Vo;`$|y*s?^TrFWj z@{u|fdvWu_jufXBzRsaFn9kjC_)t_i8DRs69SGasW{f_$aYRd9hw*9`h9r4dvUIR7 z!m0Jx;>DS8E{@omxLv{?k}t}(;!RyK;fg(s2Sm844tFaND_{}q#1K}tga#wYvlrks z)t=8>e(bg98SgytEG6^8le$u8Pih#9AdW#$e<(V*1*a0+9zp+%p2~6_rxjtu$-FKIux`#(ub z2mKEqGDER3?N@iP7QL~-xP6Rp?l}`qM;rP=J<%A4@)XP212}u^7F^il?3hr?eGi<1 zqg?`VXnrHEd_XxHsDz52yn{+mV+dQ!egrC1P#oCC&dM01wYY^vY}t+)&aRL*EeUFk zV;2;$Hy;Vi&a1A%{cJMDka4UF^(IK}me#~T@#bg#vc4@z(2Ew=w6`a5G(JODtLxE& zIviq$63B0sLkquK1QxD}D1NJ!*Mr#MdzH^NsG^BTgDWGJ2F3VaYo;AM0?BP`C6 zkxmbHg6eU_mds?d@X0Tlv7RTWJvxT5A=wv!$p$+IBcV92q3AU)%DimxNL{cz)*0!G zqOUM2Zd`WaCV`I@DdS`~JpMR54=F-D_Lb=gn@BDC92+>y;uw@jNJ2`5I+?E~U=%rM zcAko*l1lB&)sZM63kwXSELU!RD*1iC`O*-oIcJXV|^H74x(yB#VUyqpkgT> z+Y&Zo`Ct_HNlBSFeDyDLMhMnMuKk6jDCY5A4iN(hlF2NHniaT$WYQ(1%@ZC@QlObyJbV=x{tMK`w%6jU> z=f)>NaJIxJ z!W!_t9j^plF-WBaO~CCa6#=IkwVUx-vqD>TsG6B=OWC>SQ58xgP&PoFkkkvwtOsi!5Pbr6FCBR_JyR3aZ_eZ1$5ERFsRw1B(PSUD8*&CXKb0#Hc&0UpPd}r24Jhv)|I}mOl!De zltrD<#8Fo)tL)5Lgv6c~h;BpB3l}e-Ve^%lcI1-WqcFa+scga;@OX#7RXt%__lb1o z^nhZEhf$8Qn(+$VtoR5eQ2^_#L|*)a(I8E%hQTBGR}oJZ@X0 zhb;epC3EDCMi_7a;=3S#O~ZoN(b!Nt0a)<>a#?XUE*q42{dU(_i5S%2p-6XqtZVU5 zrH98E{B#>EwF`3M(k|-~!H1ozek}djAxkA%FYmu|rgU*9>qbS#*DUaY(Du0#?&BF zwKG~S8JOTE`zje|b*kfCag10*f&+H7N4mMWp9MG5om`#6|>*4k7i1tY_-ft ziLORS>_QEmccbNJ&XUoKPTG4otZ{E<$+E(uf{)LR`iH(uIC~pZ%(YiRinF9ipetof zc!rPGh_YHSGOQ`(vahE6e!f~*RD@s;X0Og%Oh@al7ARP{v>Ihkk2;i;8vh) z6=XT6QaM|OHN;UB6px*X6CBxS9!G*M85N#;YLvQDW!GW!LdaDidNsySY87&v=@)4% znIjaH^0t5jFiVU|i^8vKI3quE=Bu&eh&q7!J4CJ%Kj#pg-vuv<*)hcF1CI*(l4=o8 zX;DVaEt;pan~M*obBuDdcKUyoKA8!`Ycg4T!q>ELboyUdhJ9uiJZ~89K~S@W>O{Z# z`44)Ge0&8|=INR)$P?i99!{&TY9uYD#HD9R#y^Ob;@__rmRL6HKTiMlr4DY?x-j?)^uZM ztX;eGbUu#?6*{e4X7?IVp?oYa-7n-8FG-CkZ>4Sd<&IbJHf76c(ZR_j-$o3OvS}as zFoaDOd#ubNAMXrCh4VO|Z%`nRvVx;*B#lzT?vRjOkzKY&jo1E?<{ z4|vXaDUWs5IcH1F$0)TnII-Hv;q5o;PmR2f;=>59l$m>JM>pO>tHVH20$=bm zK$f}Nq>IS-pmI;wL+XZ%Bz{pkpMG$4Zhxk(yeHdC1lz;U5>|Vtn9Jj zl!GcrO8}Ws%RdQffyOEs|Juaj+;-HICm-4PCE@ROtRvLSdPVb4*GltIwKyJQwZb$oM4$C{;S+9Wc( zS{AbMSO>2Q`MMP_w7iLVdG0JP3WK5hM)vFGEDhhvba^!&{){HnMIATlx=nCK_IpSx z_6=%AUKV5v-M6y?p4fEcY(h{fm+9Jl2U`9r=i>YCvqMjmjsH*X{PAm)u{@tIcv-NO2l}5wzU&x8;zb-=pd{%GdNOHi}iWG~4pH+eMuZHDFBhA(yMzHPp39+EfTHj}h0LEpBl zY_}T%_=kn)xw*L>w~IOcHox1FK6%PjhrzNifccsSzE^-wX9a8vc)S5!*}}t2o10sp z33n2A(UVu88C*uCJbB6|3Q5&NKYX>mTF(@BX&w{qJd;&)ete63OH`lrJp3g@*igMq zoQS`ImX}vPQ4BlLO|HDWi4!Nn)Oq51d)b@~Znw+pfiW?nx>f8S#>~idd;HCLaI7-6 za&EwdxwISh^o_i(!!PnY5D39AB}UQXHuJLb^895O{4AaYZp-Zg>d4j$sQ{x@>>Ife zV(~Aa!vyFR(uD+_h3{pf@SH59T({xhkSd<%cKJj0^!1nJnQoph7<7y?-V7)=i%nDI ztIG4bJ@648D-W7y|G=d2psOAe1(D-M9}Ws0^4xoI`?bEGdN1@mxvTKKr)Ivi&f{D= z3BiYG9bXOA&`W$h6mz{y8>f|PlQ6CDD{eGPgILY0WotQFu2!J=G{07;1+>Z96m6O| zT{}lRR|{%0v`Vc?o2kvxW@~e_x!Mw~PFsrahBs)9+A?jqwnA&tF40zMtF&fqwYElE zi*Jpu*IKkztxda3+pJx#U7@vW9a>21)Vj28Ev)U-hP11+UE0;!uy&1ht%l#erDbV( z@VWD~P1@Vq-?abH{;s{Fy{nzl-qVV-Vy#3g)y8WRw24}UHdQ-MtH!s}=V=#c^R-%S zfp(#`P+O#3q%GDi);4GxwM(@RXwe8vt z7k*tLxi~l*7UqJ5|Cp9^QJOyTfUVw0oXzuUG#kcnT8~mFU6Q$7xau6ms z+T}q4C;j-fI(RcpZmEWIOkWLHXq;QXtVFJ_7O{mif18^#3fyi&T=3P(R4K@l9H~-Z z;_LnXwgL$>{eOvzD=qX2iP48cL9 zlmU^%{I!=%gL zvASS<^YXkCS&!6OEyDZ(XX*9_TtN)G5hS+>Vq`y(@Vp>qeh}+&5F-h~aU9*|pn z^dgPQs#iC**5lL7_*_Vp{ZXQew$HD|HlQ`HDD{C?{%CR}G{m1B_i&8>O5~zB=K~b@ zoje+Tt2uvO8GjcN@@M1ELgJs|OG1(AOJev{fRwvFj3?B1G8G@%5RXjmO(q8t7oLB9 zPhYZku(P@=)_=Yu{CvBTii-+s69nJW}Odmdi4MtW|6rUQ$N8P&# z#a7|hzJ7pWEchQ4L&s4JX6C}I6g0yx4Yq#<@4Na*COH#NrRdEc;q_JKq<`=WWJmq1 zZ)s?G;F5=<$Np5(^yNU=TU(aB`70J&f8pj0_S z(#)Tf>rdd*P2sS7%uv;OZ#txm?bcX<|9|?QwgCUF7UI7jJ~Fzh%sm{cPv>*F6?nCs zz~6XfC$xX=)oVa)#1%_B5Z@~W)r8;8u^MSU04@W~KM%S7d`h`Bdm#U)_ls46TzJnA zE8GStr4EREHNl5yKF0B}mw%>7Eq>6z2fYOH`TEsY`Hqoo$``H;z%*{4hT zBUI7QYv*4Lj*+kdj)DC9Rh%mbZes{NQn!pOekJt5=i0#G%T^pZ@l%7}w2a@UZfnbz zy?l+&ZSxgRTDO%rY^&$Ugtl<*O4Z>jvN&p`%g)9lu34!Uw}HQTDwg0YkvQ~X4bqyW zWmn@8haXZ9T_Al~h4MX8ntL^X{;*~}I4UPm=1l!rByFv+%drL34XPRqPW3}3*LC`n zZnH{kp)SI@abd|<9%(Hz>-@ig!T=O*sA|ze0uc@jz|KA>%p8H?1RAK%1;!X?C{F>H c22n!~p$8O(BN!Zj+~>RlIF&sLMqUU20A2wi`2YX_ literal 0 HcmV?d00001 diff --git a/VG Music Studio - Core/Dependencies/KMIDI.xml b/VG Music Studio - Core/Dependencies/KMIDI.xml new file mode 100644 index 0000000..4e6b5f2 --- /dev/null +++ b/VG Music Studio - Core/Dependencies/KMIDI.xml @@ -0,0 +1,68 @@ + + + + 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 + + + Middle C + + + diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs index 12c8439..1d6c4f0 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs @@ -1,7 +1,8 @@ -using Kermalis.VGMusicStudio.MIDI; +using Kermalis.MIDI; using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; @@ -42,16 +43,18 @@ public void SaveAsMIDI(string fileName, MIDISaveArgs args) } var midi = new MIDIFile(MIDIFormat.Format1, TimeDivisionValue.CreatePPQN(24), Events.Length + 1); - MIDITrackChunk metaTrack = midi.CreateTrack(); + var metaTrack = new MIDITrackChunk(); + midi.AddChunk(metaTrack); foreach ((int AbsoluteTick, (byte Numerator, byte Denominator)) e in args.TimeSignatures) { - metaTrack.Insert(e.AbsoluteTick, MetaMessage.CreateTimeSignatureMessage(e.Item2.Numerator, e.Item2.Denominator)); + metaTrack.InsertMessage(e.AbsoluteTick, MetaMessage.CreateTimeSignatureMessage(e.Item2.Numerator, e.Item2.Denominator)); } for (byte trackIndex = 0; trackIndex < Events.Length; trackIndex++) { - MIDITrackChunk track = midi.CreateTrack(); + var track = new MIDITrackChunk(); + midi.AddChunk(track); bool foundTranspose = false; int endOfPattern = 0; @@ -110,8 +113,8 @@ public void SaveAsMIDI(string fileName, MIDISaveArgs args) { key = 0x7F; } - track.Insert(ticks, new NoteOnMessage(trackIndex, (MIDINote)key, 0)); - //track.Insert(ticks, new NoteOffMessage(trackIndex, (MIDINote)key, 0)); + track.InsertMessage(ticks, new NoteOnMessage(trackIndex, (MIDINote)key, 0)); + //track.InsertMessage(ticks, new NoteOffMessage(trackIndex, (MIDINote)key, 0)); playing.Remove(nc); } break; @@ -126,42 +129,42 @@ public void SaveAsMIDI(string fileName, MIDISaveArgs args) if (trackIndex == 0) { int jumpCmd = trackEvents.FindIndex(ev => ev.Offset == c.Offset); - metaTrack.Insert((int)trackEvents[jumpCmd].Ticks[0], new MetaMessage(MetaMessageType.Marker, new byte[] { (byte)'[' })); - metaTrack.Insert(ticks, new MetaMessage(MetaMessageType.Marker, new byte[] { (byte)']' })); + metaTrack.InsertMessage((int)trackEvents[jumpCmd].Ticks[0], MetaMessage.CreateTextMessage(MetaMessageType.Marker, "[")); + metaTrack.InsertMessage(ticks, MetaMessage.CreateTextMessage(MetaMessageType.Marker, "]")); } break; } case LFODelayCommand c: { - track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)26, c.Delay)); + track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)26, c.Delay)); break; } case LFODepthCommand c: { - track.Insert(ticks, new ControllerMessage(trackIndex, ControllerType.ModulationWheel, c.Depth)); + track.InsertMessage(ticks, new ControllerMessage(trackIndex, ControllerType.ModulationWheel, c.Depth)); break; } case LFOSpeedCommand c: { - track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)21, c.Speed)); + track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)21, c.Speed)); break; } case LFOTypeCommand c: { - track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)22, (byte)c.Type)); + track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)22, (byte)c.Type)); break; } case LibraryCommand c: { - track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)30, c.Command)); - track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)29, c.Argument)); + 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.Insert(ticks, new ControllerMessage(trackIndex, ControllerType.EffectControl2, c.Operator)); - track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)14, c.Address)); - track.Insert(ticks, new ControllerMessage(trackIndex, ControllerType.EffectControl1, c.Data)); + 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: @@ -175,11 +178,11 @@ public void SaveAsMIDI(string fileName, MIDISaveArgs args) { note = 0x7F; } - track.Insert(ticks, new NoteOnMessage(trackIndex, (MIDINote)note, c.Velocity)); + track.InsertMessage(ticks, new NoteOnMessage(trackIndex, (MIDINote)note, c.Velocity)); if (c.Duration != -1) { - track.Insert(ticks + c.Duration, new NoteOnMessage(trackIndex, (MIDINote)note, 0)); - //track.Insert(ticks + c.Duration, new NoteOffMessage(trackIndex, (MIDINote)note, 0)); + track.InsertMessage(ticks + c.Duration, new NoteOnMessage(trackIndex, (MIDINote)note, 0)); + //track.InsertMessage(ticks + c.Duration, new NoteOffMessage(trackIndex, (MIDINote)note, 0)); } else { @@ -189,22 +192,22 @@ public void SaveAsMIDI(string fileName, MIDISaveArgs args) } case PanpotCommand c: { - track.Insert(ticks, new ControllerMessage(trackIndex, ControllerType.Pan, (byte)(c.Panpot + 0x40))); + track.InsertMessage(ticks, new ControllerMessage(trackIndex, ControllerType.Pan, (byte)(c.Panpot + 0x40))); break; } case PitchBendCommand c: { - track.Insert(ticks, new PitchBendMessage(trackIndex, 0, (byte)(c.Bend + 0x40))); + track.InsertMessage(ticks, new PitchBendMessage(trackIndex, 0, (byte)(c.Bend + 0x40))); break; } case PitchBendRangeCommand c: { - track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)20, c.Range)); + track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)20, c.Range)); break; } case PriorityCommand c: { - track.Insert(ticks, new ControllerMessage(trackIndex, ControllerType.ChannelVolumeLSB, c.Priority)); + track.InsertMessage(ticks, new ControllerMessage(trackIndex, ControllerType.ChannelVolumeLSB, c.Priority)); break; } case ReturnCommand _: @@ -220,7 +223,7 @@ public void SaveAsMIDI(string fileName, MIDISaveArgs args) } case TempoCommand c: { - metaTrack.Insert(ticks, MetaMessage.CreateTempoMessage(c.Tempo)); + metaTrack.InsertMessage(ticks, MetaMessage.CreateTempoMessage(c.Tempo)); break; } case TransposeCommand c: @@ -230,12 +233,12 @@ public void SaveAsMIDI(string fileName, MIDISaveArgs args) } case TuneCommand c: { - track.Insert(ticks, new ControllerMessage(trackIndex, (ControllerType)24, (byte)(c.Tune + 0x40))); + track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)24, (byte)(c.Tune + 0x40))); break; } case VoiceCommand c: { - track.Insert(ticks, new ProgramChangeMessage(trackIndex, (MIDIProgram)c.Voice)); + track.InsertMessage(ticks, new ProgramChangeMessage(trackIndex, (MIDIProgram)c.Voice)); break; } case VolumeCommand c: @@ -247,17 +250,20 @@ public void SaveAsMIDI(string fileName, MIDISaveArgs args) { volume++; } - track.Insert(ticks, new ControllerMessage(trackIndex, ControllerType.ChannelVolume, (byte)volume)); + track.InsertMessage(ticks, new ControllerMessage(trackIndex, ControllerType.ChannelVolume, (byte)volume)); break; } } } endOfTrack: - track.Insert(endTicks ?? track.NumTicks, new MetaMessage(MetaMessageType.EndOfTrack, Array.Empty())); + track.InsertMessage(endTicks ?? track.NumTicks, new MetaMessage(MetaMessageType.EndOfTrack, Array.Empty())); } - metaTrack.Insert(metaTrack.NumTicks, new MetaMessage(MetaMessageType.EndOfTrack, Array.Empty())); + metaTrack.InsertMessage(metaTrack.NumTicks, new MetaMessage(MetaMessageType.EndOfTrack, Array.Empty())); - midi.Save(fileName); + using (FileStream fs = File.Create(fileName)) + { + midi.Save(fs); + } } } diff --git a/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs b/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs index e07c497..df7aeda 100644 --- a/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs +++ b/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs @@ -55,7 +55,7 @@ public void ExecuteNext(DSETrack track) track.Channels.Add(channel); } } - else if (cmd >= 0x80 && cmd <= 0x8F) + else if (cmd is >= 0x80 and <= 0x8F) { track.LastRest = DSEUtils.FixedRests[cmd - 0x80]; track.Rest = track.LastRest; diff --git a/VG Music Studio - Core/NDS/DSE/SWD.cs b/VG Music Studio - Core/NDS/DSE/SWD.cs index 227f105..4f6ab41 100644 --- a/VG Music Studio - Core/NDS/DSE/SWD.cs +++ b/VG Music Studio - Core/NDS/DSE/SWD.cs @@ -407,7 +407,8 @@ private static long FindChunk(EndianBinaryReader r, string chunk) return pos; } - private static SampleBlock[] ReadSamples(EndianBinaryReader r, int numWAVISlots) where T : IWavInfo, new() + private static SampleBlock[] ReadSamples(EndianBinaryReader r, int numWAVISlots) + where T : IWavInfo, new() { long waviChunkOffset = FindChunk(r, "wavi"); long pcmdChunkOffset = FindChunk(r, "pcmd"); diff --git a/VG Music Studio - Core/VG Music Studio - Core.csproj b/VG Music Studio - Core/VG Music Studio - Core.csproj index 95fb9f8..1d8bb4e 100644 --- a/VG Music Studio - Core/VG Music Studio - Core.csproj +++ b/VG Music Studio - Core/VG Music Studio - Core.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 Library latest Kermalis.VGMusicStudio.Core @@ -11,14 +11,16 @@ - + - Dependencies\DLS2.dll + + Dependencies\KMIDI.dll + Dependencies\SoundFont2.dll diff --git a/VG Music Studio - MIDI/Chunks/MIDIChunk.cs b/VG Music Studio - MIDI/Chunks/MIDIChunk.cs deleted file mode 100644 index 175d966..0000000 --- a/VG Music Studio - MIDI/Chunks/MIDIChunk.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System.IO; - -namespace Kermalis.VGMusicStudio.MIDI; - -public abstract class MIDIChunk -{ - protected static long GetEndOffset(EndianBinaryReader r, uint size) - { - return r.Stream.Position + size; - } - protected static void EatRemainingBytes(EndianBinaryReader r, long endOffset, string chunkName, uint size) - { - if (r.Stream.Position > endOffset) - { - throw new InvalidDataException($"Chunk was too short ({chunkName} = {size})"); - } - r.Stream.Position = endOffset; - } - - internal abstract void Write(EndianBinaryWriter w); -} diff --git a/VG Music Studio - MIDI/Chunks/MIDIHeaderChunk.cs b/VG Music Studio - MIDI/Chunks/MIDIHeaderChunk.cs deleted file mode 100644 index 846fdcd..0000000 --- a/VG Music Studio - MIDI/Chunks/MIDIHeaderChunk.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.Diagnostics; -using System.IO; - -namespace Kermalis.VGMusicStudio.MIDI; - -// Section 2.1 -public sealed class MIDIHeaderChunk : MIDIChunk -{ - internal const string EXPECTED_NAME = "MThd"; - - public MIDIFormat Format { get; } - public ushort NumTracks { get; internal set; } - public TimeDivisionValue TimeDivision { get; } - - internal MIDIHeaderChunk(MIDIFormat format, TimeDivisionValue timeDivision) - { - if (format > MIDIFormat.Format2) - { - throw new ArgumentOutOfRangeException(nameof(format), format, null); - } - if (!timeDivision.Validate()) - { - throw new ArgumentOutOfRangeException(nameof(timeDivision), timeDivision, null); - } - - Format = format; - TimeDivision = timeDivision; - } - internal MIDIHeaderChunk(uint size, EndianBinaryReader r) - { - if (size < 6) - { - throw new InvalidDataException($"Invalid MIDI header length ({size})"); - } - - long endOffset = GetEndOffset(r, size); - - Format = r.ReadEnum(); - NumTracks = r.ReadUInt16(); - TimeDivision = new TimeDivisionValue(r.ReadUInt16()); - - if (Format > MIDIFormat.Format2) - { - // Section 2.2 states that unknown formats should be supported - Debug.WriteLine($"Unknown MIDI format ({Format}), so behavior is unknown"); - } - if (NumTracks == 0) - { - throw new InvalidDataException("MIDI has no tracks"); - } - if (Format == MIDIFormat.Format0 && NumTracks != 1) - { - throw new InvalidDataException($"MIDI format 0 must have 1 track, but this MIDI has {NumTracks}"); - } - if (!TimeDivision.Validate()) - { - throw new InvalidDataException($"Invalid MIDI time division ({TimeDivision})"); - } - - if (size > 6) - { - // Section 2.2 states that the length should be honored - Debug.WriteLine($"MIDI Header was longer than 6 bytes ({size}), so the extra data is being ignored"); - EatRemainingBytes(r, endOffset, EXPECTED_NAME, size); - } - } - - internal override void Write(EndianBinaryWriter w) - { - w.WriteChars_Count(EXPECTED_NAME, 4); - w.WriteUInt32(6); - - w.WriteEnum(Format); - w.WriteUInt16(NumTracks); - w.WriteUInt16(TimeDivision.RawValue); - } -} diff --git a/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs b/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs deleted file mode 100644 index 63eb0b5..0000000 --- a/VG Music Studio - MIDI/Chunks/MIDITrackChunk.cs +++ /dev/null @@ -1,249 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.MIDI; - -public sealed class MIDITrackChunk : MIDIChunk -{ - internal const string EXPECTED_NAME = "MTrk"; - - public MIDIEvent? First { get; private set; } - public MIDIEvent? Last { get; private set; } - - /// Includes the end of track event - public int NumEvents { get; private set; } - public int NumTicks => Last is null ? 0 : Last.Ticks; - - internal MIDITrackChunk() - { - // - } - internal MIDITrackChunk(uint size, EndianBinaryReader r) - { - long endOffset = GetEndOffset(r, size); - - int ticks = 0; - byte runningStatus = 0; - bool foundEnd = false; - bool sysexContinue = false; - while (r.Stream.Position < endOffset) - { - if (foundEnd) - { - throw new InvalidDataException("Events found after the EndOfTrack MetaMessage"); - } - - ticks += MIDIFile.ReadVariableLength(r); - - // Get command - byte cmd = r.ReadByte(); - if (sysexContinue && cmd != 0xF7) - { - throw new InvalidDataException($"SysExContinuationMessage was missing at 0x{r.Stream.Position - 1:X}"); - } - if (cmd < 0x80) - { - cmd = runningStatus; - r.Stream.Position--; - } - - // Check which message it is - if (cmd >= 0x80 && cmd <= 0xEF) - { - runningStatus = cmd; - byte channel = (byte)(cmd & 0xF); - switch (cmd & ~0xF) - { - case 0x80: Insert(ticks, new NoteOffMessage(r, channel)); break; - case 0x90: Insert(ticks, new NoteOnMessage(r, channel)); break; - case 0xA0: Insert(ticks, new PolyphonicPressureMessage(r, channel)); break; - case 0xB0: Insert(ticks, new ControllerMessage(r, channel)); break; - case 0xC0: Insert(ticks, new ProgramChangeMessage(r, channel)); break; - case 0xD0: Insert(ticks, new ChannelPressureMessage(r, channel)); break; - case 0xE0: Insert(ticks, new PitchBendMessage(r, channel)); break; - } - } - else if (cmd == 0xF0) - { - runningStatus = 0; - var msg = new SysExMessage(r); - if (!msg.IsComplete) - { - sysexContinue = true; - } - } - else if (cmd == 0xF7) - { - runningStatus = 0; - if (sysexContinue) - { - var msg = new SysExContinuationMessage(r); - if (msg.IsFinished) - { - sysexContinue = false; - } - } - else - { - Insert(ticks, new EscapeMessage(r)); - } - } - else if (cmd == 0xFF) - { - var msg = new MetaMessage(r); - if (msg.Type == MetaMessageType.EndOfTrack) - { - foundEnd = true; - } - Insert(ticks, msg); - } - else - { - throw new InvalidDataException($"Unknown MIDI command found at 0x{r.Stream.Position - 1:X} (0x{cmd:X})"); - } - } - - if (!foundEnd) - { - throw new InvalidDataException("Could not find EndOfTrack MetaMessage"); - } - if (r.Stream.Position > endOffset) - { - throw new InvalidDataException("Expected to read a certain amount of events, but the data was read incorrectly..."); - } - } - - public void Insert(int ticks, MIDIMessage msg) - { - if (ticks < 0) - { - throw new ArgumentOutOfRangeException(nameof(ticks), ticks, null); - } - - var e = new MIDIEvent(ticks, msg); - - if (NumEvents == 0) - { - First = e; - Last = e; - } - else if (ticks < First!.Ticks) - { - e.Next = First; - First.Prev = e; - First = e; - } - else if (ticks >= Last!.Ticks) - { - e.Prev = Last; - Last.Next = e; - Last = e; - } - else // Somewhere between - { - MIDIEvent next = First; - - while (next.Ticks <= ticks) - { - next = next.Next!; - } - - MIDIEvent prev = next.Prev!; - - e.Next = next; - e.Prev = prev; - prev.Next = e; - next.Prev = e; - } - - NumEvents++; - } - - internal override void Write(EndianBinaryWriter w) - { - w.WriteChars_Count(EXPECTED_NAME, 4); - - long sizeOffset = w.Stream.Position; - w.WriteUInt32(0); // We will update the size later - - byte runningStatus = 0; - bool foundEnd = false; - bool sysexContinue = false; - for (MIDIEvent? e = First; e is not null; e = e.Next) - { - if (foundEnd) - { - throw new InvalidDataException("Events found after the EndOfTrack MetaMessage"); - } - - MIDIFile.WriteVariableLength(w, e.DeltaTicks); - - MIDIMessage msg = e.Message; - byte cmd = msg.GetCMDByte(); - if (sysexContinue && cmd != 0xF7) - { - throw new InvalidDataException("SysExContinuationMessage was missing"); - } - - if (cmd >= 0x80 && cmd <= 0xEF) - { - if (runningStatus != cmd) - { - runningStatus = cmd; - w.WriteByte(cmd); - } - } - else if (cmd == 0xF0) - { - runningStatus = 0; - var sysex = (SysExMessage)msg; - if (!sysex.IsComplete) - { - sysexContinue = true; - } - w.WriteByte(0xF0); - } - else if (cmd == 0xF7) - { - runningStatus = 0; - if (sysexContinue) - { - var sysex = (SysExContinuationMessage)msg; - if (sysex.IsFinished) - { - sysexContinue = false; - } - } - w.WriteByte(0xF0); - } - else if (cmd == 0xFF) - { - var meta = (MetaMessage)msg; - if (meta.Type == MetaMessageType.EndOfTrack) - { - foundEnd = true; - } - w.WriteByte(0xFF); - } - else - { - throw new InvalidDataException($"Unknown MIDI command 0x{cmd:X}"); - } - - msg.Write(w); - } - if (!foundEnd) - { - throw new InvalidDataException("You must insert an EndOfTrack MetaMessage"); - } - - // Update size now - long endOffset = w.Stream.Position; - uint size = (uint)(endOffset - sizeOffset - 4); - w.Stream.Position = sizeOffset; - w.WriteUInt32(size); - - w.Stream.Position = endOffset; // Go back to the end - } -} diff --git a/VG Music Studio - MIDI/Chunks/MIDIUnsupportedChunk.cs b/VG Music Studio - MIDI/Chunks/MIDIUnsupportedChunk.cs deleted file mode 100644 index 942411e..0000000 --- a/VG Music Studio - MIDI/Chunks/MIDIUnsupportedChunk.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.VGMusicStudio.MIDI; - -public sealed class MIDIUnsupportedChunk : MIDIChunk -{ - /// Length 4 - public string ChunkName { get; } - public byte[] Data { get; } - - public MIDIUnsupportedChunk(string chunkName, byte[] data) - { - ChunkName = chunkName; - Data = data; - } - internal MIDIUnsupportedChunk(string chunkName, uint size, EndianBinaryReader r) - { - ChunkName = chunkName; - Data = new byte[size]; - r.ReadBytes(Data); - } - - internal override void Write(EndianBinaryWriter w) - { - w.WriteChars_Count(ChunkName, 4); - w.WriteUInt32((uint)Data.Length); - - w.WriteBytes(Data); - } -} diff --git a/VG Music Studio - MIDI/Events/ChannelPressureMessage.cs b/VG Music Studio - MIDI/Events/ChannelPressureMessage.cs deleted file mode 100644 index f3836fc..0000000 --- a/VG Music Studio - MIDI/Events/ChannelPressureMessage.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.MIDI; - -public sealed class ChannelPressureMessage : MIDIMessage -{ - public byte Channel { get; } - - public byte Pressure { get; } - - internal ChannelPressureMessage(EndianBinaryReader r, byte channel) - { - Channel = channel; - - Pressure = r.ReadByte(); - if (Pressure > 127) - { - throw new InvalidDataException($"Invalid {nameof(ChannelPressureMessage)} pressure at 0x{r.Stream.Position - 1:X} ({Pressure})"); - } - } - - public ChannelPressureMessage(byte channel, byte pressure) - { - if (channel > 15) - { - throw new ArgumentOutOfRangeException(nameof(channel), channel, null); - } - if (pressure > 127) - { - throw new ArgumentOutOfRangeException(nameof(pressure), pressure, null); - } - - Channel = channel; - Pressure = pressure; - } - - internal override byte GetCMDByte() - { - return (byte)(0xD0 + Channel); - } - - internal override void Write(EndianBinaryWriter w) - { - w.WriteByte(Pressure); - } -} diff --git a/VG Music Studio - MIDI/Events/ControllerMessage.cs b/VG Music Studio - MIDI/Events/ControllerMessage.cs deleted file mode 100644 index b0e3c6f..0000000 --- a/VG Music Studio - MIDI/Events/ControllerMessage.cs +++ /dev/null @@ -1,141 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.MIDI; - -public sealed class ControllerMessage : MIDIMessage -{ - public byte Channel { get; } - - public ControllerType Controller { get; } - public byte Value { get; } - - internal ControllerMessage(EndianBinaryReader r, byte channel) - { - Channel = channel; - - Controller = r.ReadEnum(); - if (Controller >= ControllerType.MAX) - { - throw new InvalidDataException($"Invalid {nameof(ControllerMessage)} controller at 0x{r.Stream.Position - 1:X} ({Controller})"); - } - - Value = r.ReadByte(); - if (Value > 127) - { - throw new InvalidDataException($"Invalid {nameof(ControllerMessage)} value at 0x{r.Stream.Position - 1:X} ({Value})"); - } - } - - public ControllerMessage(byte channel, ControllerType controller, byte value) - { - if (channel > 15) - { - throw new ArgumentOutOfRangeException(nameof(channel), channel, null); - } - if (controller >= ControllerType.MAX) - { - throw new ArgumentOutOfRangeException(nameof(controller), controller, null); - } - if (value > 127) - { - throw new ArgumentOutOfRangeException(nameof(value), value, null); - } - - Channel = channel; - Controller = controller; - Value = value; - } - - internal override byte GetCMDByte() - { - return (byte)(0xB0 + Channel); - } - - internal override void Write(EndianBinaryWriter w) - { - w.WriteEnum(Controller); - w.WriteByte(Value); - } -} - -public enum ControllerType : byte -{ - // MSB - BankSelect, - ModulationWheel, - BreathController, - FootController = 4, - PortamentoTime, - DataEntry, - ChannelVolume, - Balance, - Pan = 10, - ExpressionController, - EffectControl1, - EffectControl2, - GeneralPurposeController1 = 16, - GeneralPurposeController2, - GeneralPurposeController3, - GeneralPurposeController4, - // LSB - BankSelectLSB = 32, - ModulationWheelLSB, - BreathControllerLSB, - FootControllerLSB = 36, - PortamentoTimeLSB, - DataEntryLSB, - ChannelVolumeLSB, - BalanceLSB, - PanLSB = 42, - ExpressionControllerLSB, - EffectControl1LSB, - EffectControl2LSB, - GeneralPurposeController1LSB = 48, - GeneralPurposeController2LSB, - GeneralPurposeController3LSB, - GeneralPurposeController4LSB, - SustainToggle = 64, - PortamentoToggle, - SostenutoToggle, - SoftPedalToggle, - LegatoToggle, - Hold2Toggle, - SoundController1, - SoundController2, - SoundController3, - SoundController4, - SoundController5, - SoundController6, - SoundController7, - SoundController8, - SoundController9, - SoundController10, - GeneralPurposeController5, - GeneralPurposeController6, - GeneralPurposeController7, - GeneralPurposeController8, - PortamentoControl, - HighResolutionVelocityPrefix = 88, - Effects1Depth = 91, - Effects2Depth, - Effects3Depth, - Effects4Depth, - Effects5Depth, - DataIncrement, - DataDecrement, - NonRegisteredParameterNumberLSB, - NonRegisteredParameterNumberMSB, - RegisteredParameterNumberLSB, - RegisteredParameterNumberMSB, - AllSoundOff = 120, - ResetAllControllers, - LocalControlToggle, - AllNotesOff, - OmniModeOff, - OmniModeOn, - MonoModeOn, - PolyModeOn, - MAX, -} diff --git a/VG Music Studio - MIDI/Events/EscapeMessage.cs b/VG Music Studio - MIDI/Events/EscapeMessage.cs deleted file mode 100644 index 0913029..0000000 --- a/VG Music Studio - MIDI/Events/EscapeMessage.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; - -namespace Kermalis.VGMusicStudio.MIDI; - -public sealed class EscapeMessage : MIDIMessage -{ - public byte[] Data { get; } - - internal EscapeMessage(EndianBinaryReader r) - { - int len = MIDIFile.ReadVariableLength(r); - if (len == 0) - { - Data = Array.Empty(); - } - else - { - Data = new byte[len]; - r.ReadBytes(Data); - } - } - - public EscapeMessage(byte[] data) - { - Data = data; - } - - internal override byte GetCMDByte() - { - return 0xF7; - } - - internal override void Write(EndianBinaryWriter w) - { - MIDIFile.WriteVariableLength(w, Data.Length); - w.WriteBytes(Data); - } -} diff --git a/VG Music Studio - MIDI/Events/MIDIEvent.cs b/VG Music Studio - MIDI/Events/MIDIEvent.cs deleted file mode 100644 index 0886750..0000000 --- a/VG Music Studio - MIDI/Events/MIDIEvent.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Kermalis.VGMusicStudio.MIDI; - -public sealed class MIDIEvent -{ - public int Ticks { get; internal set; } - public int DeltaTicks => Prev is null ? Ticks : Ticks - Prev.Ticks; - - public MIDIMessage Message { get; set; } - - public MIDIEvent? Prev { get; internal set; } - public MIDIEvent? Next { get; internal set; } - - internal MIDIEvent(int ticks, MIDIMessage msg) - { - Ticks = ticks; - Message = msg; - } -} \ No newline at end of file diff --git a/VG Music Studio - MIDI/Events/MIDIMessage.cs b/VG Music Studio - MIDI/Events/MIDIMessage.cs deleted file mode 100644 index 2358769..0000000 --- a/VG Music Studio - MIDI/Events/MIDIMessage.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.VGMusicStudio.MIDI; - -public abstract class MIDIMessage -{ - internal abstract byte GetCMDByte(); - - internal abstract void Write(EndianBinaryWriter w); -} diff --git a/VG Music Studio - MIDI/Events/MetaMessage.cs b/VG Music Studio - MIDI/Events/MetaMessage.cs deleted file mode 100644 index 1c17f0d..0000000 --- a/VG Music Studio - MIDI/Events/MetaMessage.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.MIDI; - -public sealed class MetaMessage : MIDIMessage -{ - public MetaMessageType Type { get; } - public byte[] Data { get; } - - internal MetaMessage(EndianBinaryReader r) - { - Type = r.ReadEnum(); - if (Type >= MetaMessageType.MAX) - { - throw new InvalidDataException($"Invalid {nameof(MetaMessage)} type at 0x{r.Stream.Position - 1:X} ({Type})"); - } - - int len = MIDIFile.ReadVariableLength(r); - if (len == 0) - { - Data = Array.Empty(); - } - else - { - Data = new byte[len]; - r.ReadBytes(Data); - } - } - - public MetaMessage(MetaMessageType type, byte[] data) - { - if (type >= MetaMessageType.MAX) - { - throw new ArgumentOutOfRangeException(nameof(type), type, null); - } - - Type = type; - Data = data; - } - - public static MetaMessage CreateTempoMessage(int tempo) - { - if (tempo <= 0) - { - throw new ArgumentOutOfRangeException(nameof(tempo), tempo, null); - } - - tempo = 60_000_000 / tempo; - byte[] data = new byte[3]; - for (int i = 0; i < 3; i++) - { - data[2 - i] = (byte)(tempo >> (i * 8)); - } - return new MetaMessage(MetaMessageType.Tempo, data); - } - - public static MetaMessage CreateTimeSignatureMessage(byte numerator, byte denominator, byte clocksPerMetronomeClick = 24, byte num32ndNotesPerQuarterNote = 8) - { - if (numerator == 0) - { - throw new ArgumentOutOfRangeException(nameof(numerator), numerator, null); - } - if (denominator < 2 || denominator > 32) - { - throw new ArgumentOutOfRangeException(nameof(denominator), denominator, null); - } - if ((denominator & (denominator - 1)) != 0) - { - throw new ArgumentException("Denominator must be a power of 2", nameof(denominator)); - } - if (clocksPerMetronomeClick == 0) - { - throw new ArgumentOutOfRangeException(nameof(clocksPerMetronomeClick), clocksPerMetronomeClick, null); - } - if (num32ndNotesPerQuarterNote == 0) - { - throw new ArgumentOutOfRangeException(nameof(num32ndNotesPerQuarterNote), num32ndNotesPerQuarterNote, null); - } - - byte[] data = new byte[4]; - data[0] = numerator; - data[1] = (byte)Math.Log(denominator, 2); - data[2] = clocksPerMetronomeClick; - data[3] = num32ndNotesPerQuarterNote; - return new MetaMessage(MetaMessageType.TimeSignature, data); - } - - internal override byte GetCMDByte() - { - return 0xFF; - } - - internal override void Write(EndianBinaryWriter w) - { - w.WriteEnum(Type); - MIDIFile.WriteVariableLength(w, Data.Length); - w.WriteBytes(Data); - } -} - -public enum MetaMessageType : byte -{ - SequenceNumber, - Text, - Copyright, - TrackName, - InstrumentName, - Lyric, - Marker, - CuePoint, - ProgramName, - DeviceName, - EndOfTrack = 0x2F, - Tempo = 0x51, - SMPTEOffset = 0x54, - TimeSignature = 0x58, - KeySignature, - ProprietaryEvent = 0x7F, - MAX, -} \ No newline at end of file diff --git a/VG Music Studio - MIDI/Events/NoteOffMessage.cs b/VG Music Studio - MIDI/Events/NoteOffMessage.cs deleted file mode 100644 index 4408b89..0000000 --- a/VG Music Studio - MIDI/Events/NoteOffMessage.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.MIDI; - -public sealed class NoteOffMessage : MIDIMessage -{ - public byte Channel { get; } - - public MIDINote Note { get; } - public byte Velocity { get; } - - internal NoteOffMessage(EndianBinaryReader r, byte channel) - { - Channel = channel; - - Note = r.ReadEnum(); - if (Note >= MIDINote.MAX) - { - throw new InvalidDataException($"Invalid {nameof(NoteOffMessage)} note at 0x{r.Stream.Position - 1:X} ({Note})"); - } - - Velocity = r.ReadByte(); - if (Velocity > 127) - { - throw new InvalidDataException($"Invalid {nameof(NoteOffMessage)} velocity at 0x{r.Stream.Position - 1:X} ({Velocity})"); - } - } - - public NoteOffMessage(byte channel, MIDINote note, byte velocity) - { - if (channel > 15) - { - throw new ArgumentOutOfRangeException(nameof(channel), channel, null); - } - if (note >= MIDINote.MAX) - { - throw new ArgumentOutOfRangeException(nameof(note), note, null); - } - if (velocity > 127) - { - throw new ArgumentOutOfRangeException(nameof(velocity), velocity, null); - } - - Channel = channel; - Note = note; - Velocity = velocity; - } - - internal override byte GetCMDByte() - { - return (byte)(0x80 + Channel); - } - - internal override void Write(EndianBinaryWriter w) - { - w.WriteEnum(Note); - w.WriteByte(Velocity); - } -} diff --git a/VG Music Studio - MIDI/Events/NoteOnMessage.cs b/VG Music Studio - MIDI/Events/NoteOnMessage.cs deleted file mode 100644 index 2ad63be..0000000 --- a/VG Music Studio - MIDI/Events/NoteOnMessage.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.MIDI; - -public sealed class NoteOnMessage : MIDIMessage -{ - public byte Channel { get; } - - public MIDINote Note { get; } - public byte Velocity { get; } - - internal NoteOnMessage(EndianBinaryReader r, byte channel) - { - Channel = channel; - - Note = r.ReadEnum(); - if (Note >= MIDINote.MAX) - { - throw new InvalidDataException($"Invalid {nameof(NoteOnMessage)} note at 0x{r.Stream.Position - 1:X} ({Note})"); - } - - Velocity = r.ReadByte(); - if (Velocity > 127) - { - throw new InvalidDataException($"Invalid {nameof(NoteOnMessage)} velocity at 0x{r.Stream.Position - 1:X} ({Velocity})"); - } - } - - public NoteOnMessage(byte channel, MIDINote note, byte velocity) - { - if (channel > 15) - { - throw new ArgumentOutOfRangeException(nameof(channel), channel, null); - } - if (note >= MIDINote.MAX) - { - throw new ArgumentOutOfRangeException(nameof(note), note, null); - } - if (velocity > 127) - { - throw new ArgumentOutOfRangeException(nameof(velocity), velocity, null); - } - - Channel = channel; - Note = note; - Velocity = velocity; - } - - internal override byte GetCMDByte() - { - return (byte)(0x90 + Channel); - } - - internal override void Write(EndianBinaryWriter w) - { - w.WriteEnum(Note); - w.WriteByte(Velocity); - } -} diff --git a/VG Music Studio - MIDI/Events/PitchBendMessage.cs b/VG Music Studio - MIDI/Events/PitchBendMessage.cs deleted file mode 100644 index 0509578..0000000 --- a/VG Music Studio - MIDI/Events/PitchBendMessage.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.MIDI; - -public sealed class PitchBendMessage : MIDIMessage -{ - public byte Channel { get; } - - public byte LSB { get; } - public byte MSB { get; } - - internal PitchBendMessage(EndianBinaryReader r, byte channel) - { - Channel = channel; - - LSB = r.ReadByte(); - if (LSB > 127) - { - throw new InvalidDataException($"Invalid {nameof(PitchBendMessage)} LSB value at 0x{r.Stream.Position - 1:X} ({LSB})"); - } - - MSB = r.ReadByte(); - if (MSB > 127) - { - throw new InvalidDataException($"Invalid {nameof(PitchBendMessage)} MSB value at 0x{r.Stream.Position - 1:X} ({MSB})"); - } - } - - public PitchBendMessage(byte channel, byte lsb, byte msb) - { - if (channel > 15) - { - throw new ArgumentOutOfRangeException(nameof(channel), channel, null); - } - if (lsb > 127) - { - throw new ArgumentOutOfRangeException(nameof(lsb), lsb, null); - } - if (msb > 127) - { - throw new ArgumentOutOfRangeException(nameof(msb), msb, null); - } - - Channel = channel; - LSB = lsb; - MSB = msb; - } - - internal override byte GetCMDByte() - { - return (byte)(0xE0 + Channel); - } - - internal override void Write(EndianBinaryWriter w) - { - w.WriteByte(LSB); - w.WriteByte(MSB); - } -} diff --git a/VG Music Studio - MIDI/Events/PolyphonicPressureMessage.cs b/VG Music Studio - MIDI/Events/PolyphonicPressureMessage.cs deleted file mode 100644 index ead1ecc..0000000 --- a/VG Music Studio - MIDI/Events/PolyphonicPressureMessage.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.MIDI; - -public sealed class PolyphonicPressureMessage : MIDIMessage -{ - public byte Channel { get; } - - public MIDINote Note { get; } - public byte Pressure { get; } - - internal PolyphonicPressureMessage(EndianBinaryReader r, byte channel) - { - Channel = channel; - - Note = r.ReadEnum(); - - Pressure = r.ReadByte(); - if (Pressure > 127) - { - throw new InvalidDataException($"Invalid PolyphonicPressureMessage pressure at 0x{r.Stream.Position - 1:X} ({Pressure})"); - } - } - - public PolyphonicPressureMessage(byte channel, MIDINote note, byte pressure) - { - if (channel > 15) - { - throw new ArgumentOutOfRangeException(nameof(channel), channel, null); - } - if (pressure > 127) - { - throw new ArgumentOutOfRangeException(nameof(pressure), pressure, null); - } - - Channel = channel; - Note = note; - Pressure = pressure; - } - - internal override byte GetCMDByte() - { - return (byte)(0xA0 + Channel); - } - - internal override void Write(EndianBinaryWriter w) - { - w.WriteEnum(Note); - w.WriteByte(Pressure); - } -} diff --git a/VG Music Studio - MIDI/Events/ProgramChangeMessage.cs b/VG Music Studio - MIDI/Events/ProgramChangeMessage.cs deleted file mode 100644 index e8e8829..0000000 --- a/VG Music Studio - MIDI/Events/ProgramChangeMessage.cs +++ /dev/null @@ -1,181 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.MIDI; - -public sealed class ProgramChangeMessage : MIDIMessage -{ - public byte Channel { get; } - - public MIDIProgram Program { get; } - - internal ProgramChangeMessage(EndianBinaryReader r, byte channel) - { - Channel = channel; - - Program = r.ReadEnum(); - if (Program >= MIDIProgram.MAX) - { - throw new InvalidDataException($"Invalid {nameof(ProgramChangeMessage)} program at 0x{r.Stream.Position - 1:X} ({Program})"); - } - } - - public ProgramChangeMessage(byte channel, MIDIProgram program) - { - if (channel > 15) - { - throw new ArgumentOutOfRangeException(nameof(channel), channel, null); - } - if (program >= MIDIProgram.MAX) - { - throw new ArgumentOutOfRangeException(nameof(program), program, null); - } - - Channel = channel; - Program = program; - } - - internal override byte GetCMDByte() - { - return (byte)(0xC0 + Channel); - } - - internal override void Write(EndianBinaryWriter w) - { - w.WriteEnum(Program); - } -} - -public enum MIDIProgram : byte -{ - AcousticGrandPiano, - BrightAcousticPiano, - ElectricGrandPiano, - HonkyTonkPiano, - ElectricPiano1, - ElectricPiano2, - Harpsichord, - Clavinet, - Celesta, - Glockenspiel, - MusicBox, - Vibraphone, - Marimba, - Xylophone, - TubularBells, - Dulcimer, - DrawbarOrgan, - PercussiveOrgan, - RockOrgan, - ChurchOrgan, - ReedOrgan, - Accordion, - Harmonica, - TangoAccordion, - AcousticGuitarNylon, - AcousticGuitarSteel, - ElectricGuitarJazz, - ElectricGuitarClean, - ElectricGuitarMuted, - OverdrivenGuitar, - DistortionGuitar, - GuitarHarmonics, - AcousticBass, - ElectricBassFinger, - ElectricBassPick, - FretlessBass, - SlapBass1, - SlapBass2, - SynthBass1, - SynthBass2, - Violin, - Viola, - Cello, - Contrabass, - TremoloStrings, - PizzicatoStrings, - OrchestralHarp, - Timpani, - StringEnsemble1, - StringEnsemble2, - SynthStrings1, - SynthStrings2, - ChoirAahs, - VoiceOohs, - SynthVoice, - OrchestraHit, - Trumpet, - Trombone, - Tuba, - MutedTrumpet, - FrenchHorn, - BrassSection, - SynthBrass1, - SynthBrass2, - SopranoSax, - AltoSax, - TenorSax, - BaritoneSax, - Oboe, - EnglishHorn, - Bassoon, - Clarinet, - Piccolo, - Flute, - Recorder, - PanFlute, - BlownBottle, - Shakuhachi, - Whistle, - Ocarina, - Lead1Square, - Lead2Sawtooth, - Lead3Calliope, - Lead4Chiff, - Lead5Charang, - Lead6Voice, - Lead7Fifths, - Lead8BassAndLead, - Pad1NewAge, - Pad2Warm, - Pad3Polysynth, - Pad4Choir, - Pad5Bowed, - Pad6Metallic, - Pad7Halo, - Pad8Sweep, - Fx1Rain, - Fx2Soundtrack, - Fx3Crystal, - Fx4Atmosphere, - Fx5Brightness, - Fx6Goblins, - Fx7Echoes, - Fx8SciFi, - Sitar, - Banjo, - Shamisen, - Koto, - Kalimba, - BagPipe, - Fiddle, - Shanai, - TinkleBell, - Agogo, - SteelDrums, - Woodblock, - TaikoDrum, - MelodicTom, - SynthDrum, - ReverseCymbal, - GuitarFretNoise, - BreathNoise, - Seashore, - BirdTweet, - TelephoneRing, - Helicopter, - Applause, - Gunshot, - MAX, -} \ No newline at end of file diff --git a/VG Music Studio - MIDI/Events/SysExContinuationMessage.cs b/VG Music Studio - MIDI/Events/SysExContinuationMessage.cs deleted file mode 100644 index a55a906..0000000 --- a/VG Music Studio - MIDI/Events/SysExContinuationMessage.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.MIDI; - -public sealed class SysExContinuationMessage : MIDIMessage -{ - public byte[] Data { get; } - - public bool IsFinished => Data[Data.Length - 1] == 0xF7; - - internal SysExContinuationMessage(EndianBinaryReader r) - { - long offset = r.Stream.Position; - - int len = MIDIFile.ReadVariableLength(r); - if (len == 0) - { - throw new InvalidDataException($"SysEx continuation message at 0x{offset:X} was empty"); - } - - Data = new byte[len]; - r.ReadBytes(Data); - } - - public SysExContinuationMessage(byte[] data) - { - if (data.Length == 0) - { - throw new ArgumentException("SysEx continuation message must not be empty"); - } - - Data = data; - } - - internal override byte GetCMDByte() - { - return 0xF7; - } - - internal override void Write(EndianBinaryWriter w) - { - MIDIFile.WriteVariableLength(w, Data.Length); - w.WriteBytes(Data); - } -} diff --git a/VG Music Studio - MIDI/Events/SysExMessage.cs b/VG Music Studio - MIDI/Events/SysExMessage.cs deleted file mode 100644 index 4364c3c..0000000 --- a/VG Music Studio - MIDI/Events/SysExMessage.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.MIDI; - -public sealed class SysExMessage : MIDIMessage -{ - public byte[] Data { get; } - - public bool IsComplete => Data[Data.Length - 1] == 0xF7; - - internal SysExMessage(EndianBinaryReader r) - { - long offset = r.Stream.Position; - - int len = MIDIFile.ReadVariableLength(r); - if (len == 0) - { - throw new InvalidDataException($"SysEx message at 0x{offset:X} was empty"); - } - - Data = new byte[len]; - r.ReadBytes(Data); - } - - public SysExMessage(byte[] data) - { - if (data.Length == 0) - { - throw new ArgumentException("SysEx message must not be empty"); - } - - Data = data; - } - - internal override byte GetCMDByte() - { - return 0xF0; - } - - internal override void Write(EndianBinaryWriter w) - { - MIDIFile.WriteVariableLength(w, Data.Length); - w.WriteBytes(Data); - } -} diff --git a/VG Music Studio - MIDI/MIDIFile.cs b/VG Music Studio - MIDI/MIDIFile.cs deleted file mode 100644 index fb2b858..0000000 --- a/VG Music Studio - MIDI/MIDIFile.cs +++ /dev/null @@ -1,173 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.Collections.Generic; -using System.IO; - -namespace Kermalis.VGMusicStudio.MIDI; - -// Section 2.1 -public enum MIDIFormat : ushort -{ - /// Contains a single multi-channel track - Format0, - /// Contains one or more simultaneous tracks - Format1, - /// Contains one or more independent single-track patterns - Format2, -} - -// https://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html -// http://www.somascape.org/midi/tech/mfile.html -public sealed class MIDIFile -{ - private readonly List _nonHeaderChunks; // Not really important to expose this at the moment - - public MIDIHeaderChunk HeaderChunk { get; } - - private readonly List _tracks; - - public MIDITrackChunk this[int index] - { - get => _tracks[index]; - set => _tracks[index] = value; - } - - public MIDIFile(MIDIFormat format, TimeDivisionValue timeDivision, int tracksInitialCapacity) - { - if (format == MIDIFormat.Format0 && tracksInitialCapacity != 1) - { - throw new ArgumentException("Format 0 must have 1 track", nameof(tracksInitialCapacity)); - } - - HeaderChunk = new MIDIHeaderChunk(format, timeDivision); - _nonHeaderChunks = new List(tracksInitialCapacity); - _tracks = new List(tracksInitialCapacity); - } - public MIDIFile(Stream stream) - { - var r = new EndianBinaryReader(stream, endianness: Endianness.BigEndian, ascii: true); - string chunkName = r.ReadString_Count(4); - if (chunkName != MIDIHeaderChunk.EXPECTED_NAME) - { - throw new InvalidDataException("MIDI header was not at the start of the file"); - } - - HeaderChunk = (MIDIHeaderChunk)ReadChunk(r, alreadyReadName: chunkName); - _nonHeaderChunks = new List(HeaderChunk.NumTracks); - _tracks = new List(HeaderChunk.NumTracks); - - while (stream.Position < stream.Length) - { - MIDIChunk c = ReadChunk(r); - _nonHeaderChunks.Add(c); - if (c is MIDITrackChunk tc) - { - _tracks.Add(tc); - } - } - - if (_tracks.Count != HeaderChunk.NumTracks) - { - throw new InvalidDataException($"Unexpected track count: (Expected {HeaderChunk.NumTracks} but found {_tracks.Count}"); - } - } - - private static MIDIChunk ReadChunk(EndianBinaryReader r, string? alreadyReadName = null) - { - string chunkName = alreadyReadName ?? r.ReadString_Count(4); - uint chunkSize = r.ReadUInt32(); - switch (chunkName) - { - case MIDIHeaderChunk.EXPECTED_NAME: return new MIDIHeaderChunk(chunkSize, r); - case MIDITrackChunk.EXPECTED_NAME: return new MIDITrackChunk(chunkSize, r); - default: return new MIDIUnsupportedChunk(chunkName, chunkSize, r); - } - } - - internal static int ReadVariableLength(EndianBinaryReader r) - { - int value = r.ReadByte(); - - if ((value & 0x80) != 0) - { - value &= 0x7F; - - byte c; - do - { - c = r.ReadByte(); - value = (value << 7) + (c & 0x7F); - } while ((c & 0x80) != 0); - } - - return value; - } - internal static void WriteVariableLength(EndianBinaryWriter w, int value) - { - int buffer = value & 0x7F; - while ((value >>= 7) > 0) - { - buffer <<= 8; - buffer |= 0x80; - buffer += value & 0x7F; - } - - while (true) - { - w.WriteByte((byte)buffer); - if ((buffer & 0x80) == 0) - { - break; - } - buffer >>= 8; - } - } - internal static int GetVariableLengthNumBytes(int value) - { - int buffer = value & 0x7F; - while ((value >>= 7) > 0) - { - buffer <<= 8; - buffer |= 0x80; - buffer += value & 0x7F; - } - - int numBytes = 0; - while (true) - { - numBytes++; - if ((buffer & 0x80) == 0) - { - break; - } - buffer >>= 8; - } - - return numBytes; - } - - public MIDITrackChunk CreateTrack() - { - var tc = new MIDITrackChunk(); - _nonHeaderChunks.Add(tc); - _tracks.Add(tc); - HeaderChunk.NumTracks++; - - return tc; - } - - public void Save(string fileName) - { - using (FileStream stream = File.Create(fileName)) - { - var w = new EndianBinaryWriter(stream, endianness: Endianness.BigEndian, ascii: true); - - HeaderChunk.Write(w); - - foreach (MIDIChunk c in _nonHeaderChunks) - { - c.Write(w); - } - } - } -} diff --git a/VG Music Studio - MIDI/MIDINote.cs b/VG Music Studio - MIDI/MIDINote.cs deleted file mode 100644 index f680f33..0000000 --- a/VG Music Studio - MIDI/MIDINote.cs +++ /dev/null @@ -1,135 +0,0 @@ -namespace Kermalis.VGMusicStudio.MIDI; - -public enum MIDINote : byte -{ - C_M1, - Db_M1, - D_M1, - Eb_M1, - E_M1, - F_M1, - Gb_M1, - G_M1, - Ab_M1, - A_M1, - Bb_M1, - B_M1, - C_0, - Db_0, - D_0, - Eb_0, - E_0, - F_0, - Gb_0, - G_0, - Ab_0, - A_0, - Bb_0, - B_0, - C_1, - Db_1, - D_1, - Eb_1, - E_1, - F_1, - Gb_1, - G_1, - Ab_1, - A_1, - Bb_1, - B_1, - C_2, - Db_2, - D_2, - Eb_2, - E_2, - F_2, - Gb_2, - G_2, - Ab_2, - A_2, - Bb_2, - B_2, - C_3, - Db_3, - D_3, - Eb_3, - E_3, - F_3, - Gb_3, - G_3, - Ab_3, - A_3, - Bb_3, - B_3, - /// Middle C - C_4, - Db_4, - D_4, - Eb_4, - E_4, - F_4, - Gb_4, - G_4, - Ab_4, - A_4, - Bb_4, - B_4, - C_5, - Db_5, - D_5, - Eb_5, - E_5, - F_5, - Gb_5, - G_5, - Ab_5, - A_5, - Bb_5, - B_5, - C_6, - Db_6, - D_6, - Eb_6, - E_6, - F_6, - Gb_6, - G_6, - Ab_6, - A_6, - Bb_6, - B_6, - C_7, - Db_7, - D_7, - Eb_7, - E_7, - F_7, - Gb_7, - G_7, - Ab_7, - A_7, - Bb_7, - B_7, - C_8, - Db_8, - D_8, - Eb_8, - E_8, - F_8, - Gb_8, - G_8, - Ab_8, - A_8, - Bb_8, - B_8, - C_9, - Db_9, - D_9, - Eb_9, - E_9, - F_9, - Gb_9, - G_9, - MAX, -} diff --git a/VG Music Studio - MIDI/TimeDivisionValue.cs b/VG Music Studio - MIDI/TimeDivisionValue.cs deleted file mode 100644 index 87f783e..0000000 --- a/VG Music Studio - MIDI/TimeDivisionValue.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace Kermalis.VGMusicStudio.MIDI; - -public enum DivisionType : byte -{ - PPQN, - SMPTE, -} - -public enum SMPTEFormat : byte -{ - Smpte24 = 24, - Smpte25 = 25, - Smpte30Drop = 29, - Smpte30 = 30, -} - -// Section 2.1 -public readonly struct TimeDivisionValue -{ - public const int PPQN_MIN_DIVISION = 24; - - public readonly ushort RawValue; - - public DivisionType Type => (DivisionType)(RawValue >> 15); - - public ushort PPQN_TicksPerQuarterNote => RawValue; // Type bit is already 0 - - public SMPTEFormat SMPTE_Format => (SMPTEFormat)(-(sbyte)(RawValue >> 8)); // Upper 8 bits, negated - public byte SMPTE_TicksPerFrame => (byte)RawValue; // Lower 8 bits - - public TimeDivisionValue(ushort rawValue) - { - RawValue = rawValue; - } - - public static TimeDivisionValue CreatePPQN(ushort ticksPerQuarterNote) - { - return new TimeDivisionValue(ticksPerQuarterNote); - } - public static TimeDivisionValue CreateSMPTE(SMPTEFormat format, byte ticksPerFrame) - { - ushort rawValue = (ushort)((-(sbyte)format) << 8); - rawValue |= ticksPerFrame; - - return new TimeDivisionValue(rawValue); - } - - public bool Validate() - { - if (Type == DivisionType.PPQN) - { - return PPQN_TicksPerQuarterNote >= PPQN_MIN_DIVISION; - } - - // SMPTE - return SMPTE_Format is SMPTEFormat.Smpte24 or SMPTEFormat.Smpte25 or SMPTEFormat.Smpte30Drop or SMPTEFormat.Smpte30; - } - - public override string ToString() - { - return string.Format("0x{0:X4}", RawValue); - } -} diff --git a/VG Music Studio - MIDI/VG Music Studio - MIDI.csproj b/VG Music Studio - MIDI/VG Music Studio - MIDI.csproj deleted file mode 100644 index ef1e119..0000000 --- a/VG Music Studio - MIDI/VG Music Studio - MIDI.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net6.0 - Library - latest - Kermalis.VGMusicStudio.MIDI - enable - - - - - - - diff --git a/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj index 5c7daf3..07947c0 100644 --- a/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj +++ b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj @@ -1,7 +1,7 @@  - net6.0-windows + net7.0-windows WinExe latest Kermalis.VGMusicStudio.WinForms @@ -21,7 +21,7 @@ - + diff --git a/VG Music Studio.sln b/VG Music Studio.sln index 0f1fbff..31bb2a2 100644 --- a/VG Music Studio.sln +++ b/VG Music Studio.sln @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VG Music Studio - WinForms" 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 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VG Music Studio - MIDI", "VG Music Studio - MIDI\VG Music Studio - MIDI.csproj", "{6756ED81-71F6-457D-AD23-9C03B6C934E4}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -23,10 +21,6 @@ Global {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 - {6756ED81-71F6-457D-AD23-9C03B6C934E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6756ED81-71F6-457D-AD23-9C03B6C934E4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6756ED81-71F6-457D-AD23-9C03B6C934E4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6756ED81-71F6-457D-AD23-9C03B6C934E4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 04495885d44bcc75c4e02575929d58447f9aa025 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Sun, 14 May 2023 14:10:33 -0400 Subject: [PATCH 28/34] Some changes I did a while ago idk --- VG Music Studio - Core/Dependencies/KMIDI.dll | Bin 46592 -> 49664 bytes VG Music Studio - Core/Dependencies/KMIDI.xml | 13 +- .../GBA/AlphaDream/AlphaDreamChannel.cs | 241 ------ .../AlphaDream/Channels/AlphaDreamChannel.cs | 41 + .../Channels/AlphaDreamPCMChannel.cs | 110 +++ .../Channels/AlphaDreamSquareChannel.cs | 96 +++ .../GBA/MP2K/Channels/MP2KChannel.cs | 62 ++ .../GBA/MP2K/Channels/MP2KNoiseChannel.cs | 69 ++ .../GBA/MP2K/Channels/MP2KPCM4Channel.cs | 51 ++ .../GBA/MP2K/Channels/MP2KPCM8Channel.cs | 294 +++++++ .../GBA/MP2K/Channels/MP2KPSGChannel.cs | 282 ++++++ .../GBA/MP2K/Channels/MP2KSquareChannel.cs | 57 ++ .../GBA/MP2K/MP2KChannel.cs | 809 ------------------ VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs | 17 +- .../GBA/MP2K/MP2KLoadedSong_Events.cs | 630 +++++++------- VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs | 66 +- .../GBA/MP2K/MP2KUtils_PkmnCompress.cs | 58 ++ VG Music Studio - Core/NDS/DSE/SWD.cs | 7 +- 18 files changed, 1466 insertions(+), 1437 deletions(-) delete mode 100644 VG Music Studio - Core/GBA/AlphaDream/AlphaDreamChannel.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamChannel.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamPCMChannel.cs create mode 100644 VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamSquareChannel.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/Channels/MP2KChannel.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/Channels/MP2KNoiseChannel.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM4Channel.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM8Channel.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/Channels/MP2KPSGChannel.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/Channels/MP2KSquareChannel.cs delete mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KChannel.cs create mode 100644 VG Music Studio - Core/GBA/MP2K/MP2KUtils_PkmnCompress.cs diff --git a/VG Music Studio - Core/Dependencies/KMIDI.dll b/VG Music Studio - Core/Dependencies/KMIDI.dll index 634b3cea9728e210c11e605cec8e977b38f29daa..372b9e3267f3ac7910431a340807fa7f02cdc29b 100644 GIT binary patch literal 49664 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 z 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%0Dqg@DjKi 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 diff --git a/VG Music Studio - Core/Dependencies/KMIDI.xml b/VG Music Studio - Core/Dependencies/KMIDI.xml index 4e6b5f2..2358835 100644 --- a/VG Music Studio - Core/Dependencies/KMIDI.xml +++ b/VG Music Studio - Core/Dependencies/KMIDI.xml @@ -8,6 +8,9 @@ Includes the end of track event + + + If there are other events at , will be inserted after them. @@ -58,8 +61,14 @@ 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 + + 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/GBA/AlphaDream/AlphaDreamChannel.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamChannel.cs deleted file mode 100644 index 7b4fa85..0000000 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamChannel.cs +++ /dev/null @@ -1,241 +0,0 @@ -using Kermalis.VGMusicStudio.Core.GBA.MP2K; -using System; - -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); -} -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); - } -} -internal sealed class AlphaDreamSquareChannel : AlphaDreamChannel -{ - private float[] _pat; - - public AlphaDreamSquareChannel(AlphaDreamMixer mixer) - : base(mixer) - { - // - } - 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); - } -} 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 0000000..471fdb4 --- /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 0000000..455dc77 --- /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 0000000..a627bf0 --- /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/MP2K/Channels/MP2KChannel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KChannel.cs new file mode 100644 index 0000000..3d35241 --- /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 0000000..4389352 --- /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 0000000..90ba63b --- /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 0000000..552e230 --- /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 0000000..bc795df --- /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 0000000..4e655ff --- /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/MP2KChannel.cs b/VG Music Studio - Core/GBA/MP2K/MP2KChannel.cs deleted file mode 100644 index 9f32498..0000000 --- a/VG Music Studio - Core/GBA/MP2K/MP2KChannel.cs +++ /dev/null @@ -1,809 +0,0 @@ -using System; -using System.Collections; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; - -internal abstract class MP2KChannel -{ - public EnvelopeState State = EnvelopeState.Dead; - 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; - } - - 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; - if (Owner is not null) - { - Owner.Channels.Remove(this); - } - Owner = null; - } -} -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); - } -} -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; - if (Owner is not 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 sealed class MP2KSquareChannel : MP2KPSGChannel -{ - private float[]? _pat; - - public MP2KSquareChannel(MP2KMixer mixer) - : base(mixer) - { - // - } - 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); - } -} -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); - } -} -internal sealed class MP2KNoiseChannel : MP2KPSGChannel -{ - private BitArray _pat; - - public MP2KNoiseChannel(MP2KMixer mixer) - : base(mixer) - { - // - } - 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/MP2KConfig.cs b/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs index f7ea0b3..97eb540 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs @@ -14,7 +14,6 @@ public sealed class MP2KConfig : Config private const string CONFIG_FILE = "MP2K.yaml"; internal readonly byte[] ROM; - internal readonly EndianBinaryReader Reader; // TODO: Need? internal readonly string GameCode; internal readonly byte Version; @@ -31,16 +30,17 @@ public sealed class MP2KConfig : Config 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; - Reader = new EndianBinaryReader(new MemoryStream(rom), ascii: true); - Reader.Stream.Position = 0xAC; - GameCode = Reader.ReadString_Count(4); - Reader.Stream.Position = 0xBC; - Version = Reader.ReadByte(); + 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); @@ -240,9 +240,4 @@ public override string GetSongName(int index) } return index.ToString(); } - - public override void Dispose() - { - Reader.Stream.Dispose(); - } } diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Events.cs b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Events.cs index 2bfd97f..ab62db7 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Events.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Events.cs @@ -1,6 +1,7 @@ using Kermalis.EndianBinaryIO; using System; using System.Collections.Generic; +using System.IO; using System.Linq; namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; @@ -44,259 +45,261 @@ private void AddTrackEvents(byte trackIndex, long trackStart) } private void AddEvents(byte trackIndex, long startOffset, ref byte runCmd, ref byte prevKey, ref byte prevVelocity, ref int callStackDepth) { - EndianBinaryReader r = _player.Config.Reader; - r.Stream.Position = startOffset; - - Span peek = stackalloc byte[3]; - bool cont = true; - while (cont) + using (var ms = new MemoryStream(_player.Config.ROM)) { - long offset = r.Stream.Position; + var r = new EndianBinaryReader(ms, ascii: true); + r.Stream.Position = startOffset; - byte cmd = r.ReadByte(); - if (cmd >= 0xBD) // Commands that work within running status + Span peek = stackalloc byte[3]; + bool cont = true; + while (cont) { - runCmd = cmd; - } + long offset = r.Stream.Position; - #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) + byte cmd = r.ReadByte(); + if (cmd >= 0xBD) // Commands that work within running status { - key = r.ReadByte(); - velocity = prevVelocity; - addedDuration = 0; + runCmd = cmd; } - // TIE (0xCF) cannot have an added duration so it needs to stop here - else if (cmd == 0xCF || peek[2] > 3) + + #region TIE & Notes + + if (runCmd >= 0xCF && cmd <= 0x7F) // Within running status { - key = r.ReadByte(); - velocity = r.ReadByte(); - addedDuration = 0; + 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 + else if (cmd >= 0xCF) { - key = r.ReadByte(); - velocity = r.ReadByte(); - addedDuration = r.ReadByte(); + 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); } - EmulateNote(trackIndex, offset, key, velocity, addedDuration, ref runCmd, ref prevKey, ref prevVelocity); - } - #endregion + #endregion - #region Rests + #region Rests - else if (cmd >= 0x80 && cmd <= 0xB0) - { - if (!EventExists(trackIndex, offset)) + else if (cmd is >= 0x80 and <= 0xB0) { - AddEvent(trackIndex, offset, new RestCommand { Rest = MP2KUtils.RestTable[cmd - 0x80] }); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new RestCommand { Rest = MP2KUtils.RestTable[cmd - 0x80] }); + } } - } - #endregion + #endregion - #region Commands + #region Commands - else if (runCmd < 0xCF && cmd <= 0x7F) - { - switch (runCmd) + else if (runCmd < 0xCF && cmd <= 0x7F) { - case 0xBD: + switch (runCmd) { - if (!EventExists(trackIndex, offset)) + case 0xBD: { - AddEvent(trackIndex, offset, new VoiceCommand { Voice = cmd }); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new VoiceCommand { Voice = cmd }); + } + break; } - break; - } - case 0xBE: - { - if (!EventExists(trackIndex, offset)) + case 0xBE: { - AddEvent(trackIndex, offset, new VolumeCommand { Volume = cmd }); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new VolumeCommand { Volume = cmd }); + } + break; } - break; - } - case 0xBF: - { - if (!EventExists(trackIndex, offset)) + case 0xBF: { - AddEvent(trackIndex, offset, new PanpotCommand { Panpot = (sbyte)(cmd - 0x40) }); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new PanpotCommand { Panpot = (sbyte)(cmd - 0x40) }); + } + break; } - break; - } - case 0xC0: - { - if (!EventExists(trackIndex, offset)) + case 0xC0: { - AddEvent(trackIndex, offset, new PitchBendCommand { Bend = (sbyte)(cmd - 0x40) }); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new PitchBendCommand { Bend = (sbyte)(cmd - 0x40) }); + } + break; } - break; - } - case 0xC1: - { - if (!EventExists(trackIndex, offset)) + case 0xC1: { - AddEvent(trackIndex, offset, new PitchBendRangeCommand { Range = cmd }); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new PitchBendRangeCommand { Range = cmd }); + } + break; } - break; - } - case 0xC2: - { - if (!EventExists(trackIndex, offset)) + case 0xC2: { - AddEvent(trackIndex, offset, new LFOSpeedCommand { Speed = cmd }); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFOSpeedCommand { Speed = cmd }); + } + break; } - break; - } - case 0xC3: - { - if (!EventExists(trackIndex, offset)) + case 0xC3: { - AddEvent(trackIndex, offset, new LFODelayCommand { Delay = cmd }); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFODelayCommand { Delay = cmd }); + } + break; } - break; - } - case 0xC4: - { - if (!EventExists(trackIndex, offset)) + case 0xC4: { - AddEvent(trackIndex, offset, new LFODepthCommand { Depth = cmd }); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFODepthCommand { Depth = cmd }); + } + break; } - break; - } - case 0xC5: - { - if (!EventExists(trackIndex, offset)) + case 0xC5: { - AddEvent(trackIndex, offset, new LFOTypeCommand { Type = (LFOType)cmd }); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFOTypeCommand { Type = (LFOType)cmd }); + } + break; } - break; - } - case 0xC8: - { - if (!EventExists(trackIndex, offset)) + case 0xC8: { - AddEvent(trackIndex, offset, new TuneCommand { Tune = (sbyte)(cmd - 0x40) }); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new TuneCommand { Tune = (sbyte)(cmd - 0x40) }); + } + break; } - break; - } - case 0xCD: - { - byte arg = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + case 0xCD: { - AddEvent(trackIndex, offset, new LibraryCommand { Command = cmd, Argument = arg }); + byte arg = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LibraryCommand { Command = cmd, Argument = arg }); + } + break; } - break; - } - case 0xCE: - { - prevKey = cmd; - if (!EventExists(trackIndex, offset)) + case 0xCE: { - AddEvent(trackIndex, offset, new EndOfTieCommand { Note = cmd }); + prevKey = cmd; + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new EndOfTieCommand { Note = cmd }); + } + break; } - break; + default: throw new MP2KInvalidRunningStatusCMDException(trackIndex, (int)offset, runCmd); } - default: throw new MP2KInvalidRunningStatusCMDException(trackIndex, (int)offset, runCmd); } - } - else if (cmd > 0xB0 && cmd < 0xCF) - { - switch (cmd) + else if (cmd is > 0xB0 and < 0xCF) { - case 0xB1: - case 0xB6: + switch (cmd) { - if (!EventExists(trackIndex, offset)) + case 0xB1: + case 0xB6: { - 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)) + if (!EventExists(trackIndex, offset)) { - AddEvents(trackIndex, jumpOffset, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); + AddEvent(trackIndex, offset, new FinishCommand { Prev = cmd == 0xB6 }); } + cont = false; + break; } - 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 + case 0xB2: { - throw new MP2KTooManyNestedCallsException(trackIndex); + 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; } - break; - } - case 0xB4: - { - if (!EventExists(trackIndex, offset)) + case 0xB3: { - AddEvent(trackIndex, offset, new ReturnCommand()); + 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; } - if (callStackDepth != 0) + case 0xB4: { - cont = false; - callStackDepth--; + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new ReturnCommand()); + } + if (callStackDepth != 0) + { + cont = false; + callStackDepth--; + } + break; } - break; - } - /*case 0xB5: // TODO: Logic so this isn't an infinite loop + /*case 0xB5: // TODO: Logic so this isn't an infinite loop { byte times = config.Reader.ReadByte(); int repeatOffset = config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; @@ -306,159 +309,160 @@ private void AddEvents(byte trackIndex, long startOffset, ref byte runCmd, ref b } break; }*/ - case 0xB9: - { - byte op = r.ReadByte(); - byte address = r.ReadByte(); - byte data = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + case 0xB9: { - AddEvent(trackIndex, offset, new MemoryAccessCommand { Operator = op, Address = address, Data = data }); + 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; } - break; - } - case 0xBA: - { - byte priority = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + case 0xBA: { - AddEvent(trackIndex, offset, new PriorityCommand { Priority = priority }); + byte priority = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new PriorityCommand { Priority = priority }); + } + break; } - break; - } - case 0xBB: - { - byte tempoArg = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + case 0xBB: { - AddEvent(trackIndex, offset, new TempoCommand { Tempo = (ushort)(tempoArg * 2) }); + byte tempoArg = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new TempoCommand { Tempo = (ushort)(tempoArg * 2) }); + } + break; } - break; - } - case 0xBC: - { - sbyte transpose = r.ReadSByte(); - if (!EventExists(trackIndex, offset)) + case 0xBC: { - AddEvent(trackIndex, offset, new TransposeCommand { Transpose = transpose }); + sbyte transpose = r.ReadSByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new TransposeCommand { Transpose = transpose }); + } + break; } - break; - } - // Commands that work within running status: - case 0xBD: - { - byte voice = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + // Commands that work within running status: + case 0xBD: { - AddEvent(trackIndex, offset, new VoiceCommand { Voice = voice }); + byte voice = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new VoiceCommand { Voice = voice }); + } + break; } - break; - } - case 0xBE: - { - byte volume = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + case 0xBE: { - AddEvent(trackIndex, offset, new VolumeCommand { Volume = volume }); + byte volume = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new VolumeCommand { Volume = volume }); + } + break; } - break; - } - case 0xBF: - { - byte panArg = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + case 0xBF: { - AddEvent(trackIndex, offset, new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); + byte panArg = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); + } + break; } - break; - } - case 0xC0: - { - byte bendArg = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + case 0xC0: { - AddEvent(trackIndex, offset, new PitchBendCommand { Bend = (sbyte)(bendArg - 0x40) }); + byte bendArg = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new PitchBendCommand { Bend = (sbyte)(bendArg - 0x40) }); + } + break; } - break; - } - case 0xC1: - { - byte range = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + case 0xC1: { - AddEvent(trackIndex, offset, new PitchBendRangeCommand { Range = range }); + byte range = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new PitchBendRangeCommand { Range = range }); + } + break; } - break; - } - case 0xC2: - { - byte speed = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + case 0xC2: { - AddEvent(trackIndex, offset, new LFOSpeedCommand { Speed = speed }); + byte speed = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFOSpeedCommand { Speed = speed }); + } + break; } - break; - } - case 0xC3: - { - byte delay = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + case 0xC3: { - AddEvent(trackIndex, offset, new LFODelayCommand { Delay = delay }); + byte delay = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFODelayCommand { Delay = delay }); + } + break; } - break; - } - case 0xC4: - { - byte depth = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + case 0xC4: { - AddEvent(trackIndex, offset, new LFODepthCommand { Depth = depth }); + byte depth = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFODepthCommand { Depth = depth }); + } + break; } - break; - } - case 0xC5: - { - byte type = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + case 0xC5: { - AddEvent(trackIndex, offset, new LFOTypeCommand { Type = (LFOType)type }); + byte type = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LFOTypeCommand { Type = (LFOType)type }); + } + break; } - break; - } - case 0xC8: - { - byte tuneArg = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + case 0xC8: { - AddEvent(trackIndex, offset, new TuneCommand { Tune = (sbyte)(tuneArg - 0x40) }); + byte tuneArg = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new TuneCommand { Tune = (sbyte)(tuneArg - 0x40) }); + } + break; } - break; - } - case 0xCD: - { - byte command = r.ReadByte(); - byte arg = r.ReadByte(); - if (!EventExists(trackIndex, offset)) + case 0xCD: { - AddEvent(trackIndex, offset, new LibraryCommand { Command = command, Argument = arg }); + byte command = r.ReadByte(); + byte arg = r.ReadByte(); + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new LibraryCommand { Command = command, Argument = arg }); + } + break; } - break; - } - case 0xCE: - { - int key = r.PeekByte() <= 0x7F ? (prevKey = r.ReadByte()) : -1; - if (!EventExists(trackIndex, offset)) + case 0xCE: { - AddEvent(trackIndex, offset, new EndOfTieCommand { Note = key }); + int key = r.PeekByte() <= 0x7F ? (prevKey = r.ReadByte()) : -1; + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new EndOfTieCommand { Note = key }); + } + break; } - break; + default: throw new MP2KInvalidCMDException(trackIndex, (int)offset, cmd); } - default: throw new MP2KInvalidCMDException(trackIndex, (int)offset, cmd); } - } - #endregion + #endregion + } } } diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs b/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs index f754d87..09dd5e6 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs @@ -1,12 +1,11 @@ using System; using System.Collections; -using System.Collections.Generic; namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; -internal static class MP2KUtils +internal static partial class MP2KUtils { - public static readonly byte[] RestTable = new byte[49] + public static ReadOnlySpan RestTable => new byte[49] { 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, 15, @@ -16,7 +15,7 @@ internal static class MP2KUtils 72, 76, 78, 80, 84, 88, 90, 92, 96, }; - public static readonly (int sampleRate, int samplesPerBuffer)[] FrequencyTable = new (int, int)[12] + public static ReadOnlySpan<(int sampleRate, int samplesPerBuffer)> FrequencyTable => new (int, int)[12] { (05734, 096), // 59.72916666666667 (07884, 132), // 59.72727272727273 @@ -33,15 +32,15 @@ internal static class MP2KUtils }; // 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, }; + 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] + public static ReadOnlySpan NoiseFrequencyTable => new byte[60] { 0xD7, 0xD6, 0xD5, 0xD4, 0xC7, 0xC6, 0xC5, 0xC4, @@ -80,55 +79,6 @@ public static void PCM4ToFloat(ReadOnlySpan src, Span dest) } } - // 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, - }; - // TODO: Do runtime - 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; - } - } - compressionByte--; - compressionLevel += _compressionLookup[b & 0xF]; - samples.Add(compressionLevel); - if (++compressionIdx >= sampleLength) - { - break; - } - } - } - - return samples.ToArray(); - } - static MP2KUtils() { NoiseFine = new BitArray(0x8_000); 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 0000000..ae25958 --- /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 - Core/NDS/DSE/SWD.cs b/VG Music Studio - Core/NDS/DSE/SWD.cs index 4f6ab41..d8771fa 100644 --- a/VG Music Studio - Core/NDS/DSE/SWD.cs +++ b/VG Music Studio - Core/NDS/DSE/SWD.cs @@ -330,12 +330,12 @@ public sealed class LFOInfo public IHeader Header; public byte[] Unknown2; - public ProgramBank Programs; + public ProgramBank? Programs; public SampleBlock[] Samples; public SWD(string path) { - using (var stream = new MemoryStream(File.ReadAllBytes(path))) + using (FileStream stream = File.OpenRead(path)) { var r = new EndianBinaryReader(stream, ascii: true); Type = r.ReadString_Count(4); @@ -441,7 +441,8 @@ private static SampleBlock[] ReadSamples(EndianBinaryReader r, int numWAVISlo return samples; } } - private static ProgramBank? ReadPrograms(EndianBinaryReader r, int numPRGISlots) where T : IProgramInfo, new() + private static ProgramBank? ReadPrograms(EndianBinaryReader r, int numPRGISlots) + where T : IProgramInfo, new() { long chunkOffset = FindChunk(r, "prgi"); if (chunkOffset == -1) From 016c3e4140bfae96193f279396e36a1d027f44ae Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Sun, 14 May 2023 15:08:45 -0400 Subject: [PATCH 29/34] Address all warnings --- VG Music Studio - Core/Mixer.cs | 3 + VG Music Studio - Core/NDS/DSE/DSEChannel.cs | 11 ++- VG Music Studio - Core/NDS/DSE/DSECommands.cs | 4 +- .../NDS/DSE/DSELoadedSong_Runtime.cs | 7 +- VG Music Studio - Core/NDS/DSE/SMD.cs | 24 +++--- VG Music Studio - Core/NDS/DSE/SWD.cs | 86 ++++++++++--------- VG Music Studio - Core/NDS/SDAT/SDAT.cs | 2 +- .../NDS/SDAT/SDATLoadedSong_Events.cs | 14 ++- VG Music Studio - Core/SongState.cs | 7 +- VG Music Studio - Core/Util/ConfigUtils.cs | 8 +- VG Music Studio - WinForms/SongInfoControl.cs | 12 +-- .../Util/FlexibleMessageBox.cs | 21 +++-- .../Util/ImageComboBox.cs | 8 +- VG Music Studio - WinForms/Util/VGMSDebug.cs | 18 ++-- .../VG Music Studio - WinForms.csproj | 15 +++- 15 files changed, 135 insertions(+), 105 deletions(-) diff --git a/VG Music Studio - Core/Mixer.cs b/VG Music Studio - Core/Mixer.cs index 4bb8efe..7b8d4c5 100644 --- a/VG Music Studio - Core/Mixer.cs +++ b/VG Music Studio - Core/Mixer.cs @@ -21,6 +21,8 @@ public abstract class Mixer : IAudioSessionEventsHandler, IDisposable protected Mixer() { Mutes = new bool[SongState.MAX_TRACKS]; + _out = null!; + _appVolume = null!; } protected void Init(IWaveProvider waveProvider) @@ -99,6 +101,7 @@ public void SetVolume(float 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 index 4b1b83f..f66e0e7 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEChannel.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEChannel.cs @@ -43,12 +43,14 @@ internal sealed class DSEChannel public DSEChannel(byte i) { + _sample = null!; + _adpcmDecoder = null!; Index = i; } public bool StartPCM(SWD localswd, SWD masterswd, byte voice, int key, uint noteLength) { - SWD.IProgramInfo? programInfo = localswd.Programs.ProgramInfos[voice]; + SWD.IProgramInfo? programInfo = localswd.Programs?.ProgramInfos[voice]; if (programInfo is null) { return false; @@ -62,7 +64,7 @@ public bool StartPCM(SWD localswd, SWD masterswd, byte voice, int key, uint note continue; } - _sample = masterswd.Samples[split.SampleId]; + _sample = masterswd.Samples![split.SampleId]; Key = (byte)key; RootKey = split.SampleRootKey; BaseTimer = (ushort)(NDSUtils.ARM7_CLOCK / _sample.WavInfo.SampleRate); @@ -102,10 +104,7 @@ public bool StartPCM(SWD localswd, SWD masterswd, byte voice, int key, uint note public void Stop() { - if (Owner is not null) - { - Owner.Channels.Remove(this); - } + Owner?.Channels.Remove(this); Owner = null; Volume = 0; } diff --git a/VG Music Studio - Core/NDS/DSE/DSECommands.cs b/VG Music Studio - Core/NDS/DSE/DSECommands.cs index e07ac3b..76e8e5b 100644 --- a/VG Music Studio - Core/NDS/DSE/DSECommands.cs +++ b/VG Music Studio - Core/NDS/DSE/DSECommands.cs @@ -92,7 +92,7 @@ internal sealed class SkipBytesCommand : ICommand public string Arguments => string.Join(", ", SkippedBytes.Select(b => $"0x{b:X}")); public byte Command { get; set; } - public byte[] SkippedBytes { get; set; } + public byte[] SkippedBytes { get; set; } = null!; } internal sealed class TempoCommand : ICommand { @@ -110,7 +110,7 @@ internal sealed class UnknownCommand : ICommand public string Arguments => string.Join(", ", Args.Select(b => $"0x{b:X}")); public byte Command { get; set; } - public byte[] Args { get; set; } + public byte[] Args { get; set; } = null!; } internal sealed class VoiceCommand : ICommand { diff --git a/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs b/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs index df7aeda..90e30e4 100644 --- a/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs +++ b/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs @@ -40,11 +40,8 @@ public void ExecuteNext(DSETrack track) } track.LastNoteDuration = duration; } - DSEChannel? channel = _player.DMixer.AllocateChannel(); - if (channel is null) - { - throw new Exception("Not enough channels"); - } + DSEChannel channel = _player.DMixer.AllocateChannel() + ?? throw new Exception("Not enough channels"); channel.Stop(); track.Octave = (byte)(track.Octave + oct); diff --git a/VG Music Studio - Core/NDS/DSE/SMD.cs b/VG Music Studio - Core/NDS/DSE/SMD.cs index 33cd44f..e9a9083 100644 --- a/VG Music Studio - Core/NDS/DSE/SMD.cs +++ b/VG Music Studio - Core/NDS/DSE/SMD.cs @@ -7,13 +7,13 @@ internal sealed class SMD public sealed class Header // Size 0x40 { [BinaryStringFixedLength(4)] - public string Type { get; set; } // "smdb" or "smdl" + public string Type { get; set; } = null!; // "smdb" or "smdl" [BinaryArrayFixedLength(4)] - public byte[] Unknown1 { get; set; } + public byte[] Unknown1 { get; set; } = null!; public uint Length { get; set; } public ushort Version { get; set; } [BinaryArrayFixedLength(10)] - public byte[] Unknown2 { get; set; } + public byte[] Unknown2 { get; set; } = null!; public ushort Year { get; set; } public byte Month { get; set; } public byte Day { get; set; } @@ -22,9 +22,9 @@ public sealed class Header // Size 0x40 public byte Second { get; set; } public byte Centisecond { get; set; } [BinaryStringFixedLength(16)] - public string Label { get; set; } + public string Label { get; set; } = null!; [BinaryArrayFixedLength(16)] - public byte[] Unknown3 { get; set; } + public byte[] Unknown3 { get; set; } = null!; } public interface ISongChunk @@ -34,27 +34,27 @@ public interface ISongChunk public sealed class SongChunk_V402 : ISongChunk // Size 0x20 { [BinaryStringFixedLength(4)] - public string Type { get; set; } + public string Type { get; set; } = null!; [BinaryArrayFixedLength(16)] - public byte[] Unknown1 { get; set; } + public byte[] Unknown1 { get; set; } = null!; public byte NumTracks { get; set; } public byte NumChannels { get; set; } [BinaryArrayFixedLength(4)] - public byte[] Unknown2 { get; set; } + public byte[] Unknown2 { get; set; } = null!; public sbyte MasterVolume { get; set; } public sbyte MasterPanpot { get; set; } [BinaryArrayFixedLength(4)] - public byte[] Unknown3 { get; set; } + public byte[] Unknown3 { get; set; } = null!; } public sealed class SongChunk_V415 : ISongChunk // Size 0x40 { [BinaryStringFixedLength(4)] - public string Type { get; set; } + public string Type { get; set; } = null!; [BinaryArrayFixedLength(18)] - public byte[] Unknown1 { get; set; } + public byte[] Unknown1 { get; set; } = null!; public byte NumTracks { get; set; } public byte NumChannels { get; set; } [BinaryArrayFixedLength(40)] - public byte[] Unknown2 { get; set; } + 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 index d8771fa..fa022f5 100644 --- a/VG Music Studio - Core/NDS/DSE/SWD.cs +++ b/VG Music Studio - Core/NDS/DSE/SWD.cs @@ -15,7 +15,7 @@ public interface IHeader private sealed class Header_V402 : IHeader // Size 0x40 { [BinaryArrayFixedLength(8)] - public byte[] Unknown1 { get; set; } + public byte[] Unknown1 { get; set; } = null!; public ushort Year { get; set; } public byte Month { get; set; } public byte Day { get; set; } @@ -24,19 +24,19 @@ private sealed class Header_V402 : IHeader // Size 0x40 public byte Second { get; set; } public byte Centisecond { get; set; } [BinaryStringFixedLength(16)] - public string Label { get; set; } + public string Label { get; set; } = null!; [BinaryArrayFixedLength(22)] - public byte[] Unknown2 { get; set; } + 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; } + public byte[] Padding { get; set; } = null!; } private sealed class Header_V415 : IHeader // Size 0x40 { [BinaryArrayFixedLength(8)] - public byte[] Unknown1 { get; set; } + public byte[] Unknown1 { get; set; } = null!; public ushort Year { get; set; } public byte Month { get; set; } public byte Day { get; set; } @@ -45,16 +45,16 @@ private sealed class Header_V415 : IHeader // Size 0x40 public byte Second { get; set; } public byte Centisecond { get; set; } [BinaryStringFixedLength(16)] - public string Label { get; set; } + public string Label { get; set; } = null!; [BinaryArrayFixedLength(16)] - public byte[] Unknown2 { get; set; } + public byte[] Unknown2 { get; set; } = null!; public uint PCMDLength { get; set; } [BinaryArrayFixedLength(2)] - public byte[] Unknown3 { get; set; } + public byte[] Unknown3 { get; set; } = null!; public ushort NumWAVISlots { get; set; } public ushort NumPRGISlots { get; set; } [BinaryArrayFixedLength(2)] - public byte[] Unknown4 { get; set; } + public byte[] Unknown4 { get; set; } = null!; public uint WAVILength { get; set; } } @@ -77,7 +77,7 @@ public sealed class SplitEntry_V402 : ISplitEntry // Size 0x30 { public ushort Id { get; set; } [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } + public byte[] Unknown1 { get; set; } = null!; public byte LowKey { get; set; } public byte HighKey { get; set; } public byte LowKey2 { get; set; } @@ -87,17 +87,17 @@ public sealed class SplitEntry_V402 : ISplitEntry // Size 0x30 public byte LowVelocity2 { get; set; } public byte HighVelocity2 { get; set; } [BinaryArrayFixedLength(5)] - public byte[] Unknown2 { get; set; } + public byte[] Unknown2 { get; set; } = null!; public byte SampleId { get; set; } [BinaryArrayFixedLength(2)] - public byte[] Unknown3 { get; set; } + 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; } + public byte[] Unknown4 { get; set; } = null!; public byte AttackVolume { get; set; } public byte Attack { get; set; } public byte Decay { get; set; } @@ -114,7 +114,7 @@ public sealed class SplitEntry_V415 : ISplitEntry // 0x30 { public ushort Id { get; set; } [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } + public byte[] Unknown1 { get; set; } = null!; public byte LowKey { get; set; } public byte HighKey { get; set; } public byte LowKey2 { get; set; } @@ -124,17 +124,17 @@ public sealed class SplitEntry_V415 : ISplitEntry // 0x30 public byte LowVelocity2 { get; set; } public byte HighVelocity2 { get; set; } [BinaryArrayFixedLength(6)] - public byte[] Unknown2 { get; set; } + public byte[] Unknown2 { get; set; } = null!; public ushort SampleId { get; set; } [BinaryArrayFixedLength(2)] - public byte[] Unknown3 { get; set; } + 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; } + public byte[] Unknown4 { get; set; } = null!; public byte AttackVolume { get; set; } public byte Attack { get; set; } public byte Decay { get; set; } @@ -157,20 +157,20 @@ public sealed class ProgramInfo_V402 : IProgramInfo public byte Id { get; set; } public byte NumSplits { get; set; } [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } + public byte[] Unknown1 { get; set; } = null!; public byte Volume { get; set; } public byte Panpot { get; set; } [BinaryArrayFixedLength(5)] - public byte[] Unknown2 { get; set; } + public byte[] Unknown2 { get; set; } = null!; public byte NumLFOs { get; set; } [BinaryArrayFixedLength(4)] - public byte[] Unknown3 { get; set; } + public byte[] Unknown3 { get; set; } = null!; [BinaryArrayFixedLength(16)] - public KeyGroup[] KeyGroups { get; set; } + public KeyGroup[] KeyGroups { get; set; } = null!; [BinaryArrayVariableLength(nameof(NumLFOs))] - public LFOInfo LFOInfos { get; set; } + public LFOInfo LFOInfos { get; set; } = null!; [BinaryArrayVariableLength(nameof(NumSplits))] - public SplitEntry_V402[] SplitEntries { get; set; } + public SplitEntry_V402[] SplitEntries { get; set; } = null!; [BinaryIgnore] ISplitEntry[] IProgramInfo.SplitEntries => SplitEntries; @@ -182,16 +182,16 @@ public sealed class ProgramInfo_V415 : IProgramInfo public byte Volume { get; set; } public byte Panpot { get; set; } [BinaryArrayFixedLength(5)] - public byte[] Unknown1 { get; set; } + public byte[] Unknown1 { get; set; } = null!; public byte NumLFOs { get; set; } [BinaryArrayFixedLength(4)] - public byte[] Unknown2 { get; set; } + public byte[] Unknown2 { get; set; } = null!; [BinaryArrayVariableLength(nameof(NumLFOs))] - public LFOInfo[] LFOInfos { get; set; } + public LFOInfo[] LFOInfos { get; set; } = null!; [BinaryArrayFixedLength(16)] - public byte[] Unknown3 { get; set; } + public byte[] Unknown3 { get; set; } = null!; [BinaryArrayVariableLength(nameof(NumSplits))] - public SplitEntry_V415[] SplitEntries { get; set; } + public SplitEntry_V415[] SplitEntries { get; set; } = null!; [BinaryIgnore] ISplitEntry[] IProgramInfo.SplitEntries => SplitEntries; @@ -221,25 +221,25 @@ 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; } + 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; } + 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; } + public byte[] Unknown4 { get; set; } = null!; public byte EnvOn { get; set; } public byte EnvMult { get; set; } [BinaryArrayFixedLength(6)] - public byte[] Unknown5 { get; set; } + public byte[] Unknown5 { get; set; } = null!; public byte AttackVolume { get; set; } public byte Attack { get; set; } public byte Decay { get; set; } @@ -252,16 +252,16 @@ public sealed class WavInfo_V402 : IWavInfo // Size 0x40 public sealed class WavInfo_V415 : IWavInfo // 0x40 { [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } + public byte[] Unknown1 { get; set; } = null!; public ushort Id { get; set; } [BinaryArrayFixedLength(2)] - public byte[] Unknown2 { get; set; } + 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; } + public byte[] Unknown3 { get; set; } = null!; public ushort Version { get; set; } public SampleFormat SampleFormat { get; set; } public byte Unknown4 { get; set; } @@ -271,7 +271,7 @@ public sealed class WavInfo_V415 : IWavInfo // 0x40 public byte Unknown6 { get; set; } public byte BitDepth { get; set; } [BinaryArrayFixedLength(6)] - public byte[] Unknown7 { get; set; } + public byte[] Unknown7 { get; set; } = null!; public uint SampleRate { get; set; } public uint SampleOffset { get; set; } public uint LoopStart { get; set; } @@ -279,7 +279,7 @@ public sealed class WavInfo_V415 : IWavInfo // 0x40 public byte EnvOn { get; set; } public byte EnvMult { get; set; } [BinaryArrayFixedLength(6)] - public byte[] Unknown8 { get; set; } + public byte[] Unknown8 { get; set; } = null!; public byte AttackVolume { get; set; } public byte Attack { get; set; } public byte Decay { get; set; } @@ -292,13 +292,13 @@ public sealed class WavInfo_V415 : IWavInfo // 0x40 public class SampleBlock { - public IWavInfo WavInfo; - public byte[] Data; + public IWavInfo WavInfo = null!; + public byte[] Data = null!; } public class ProgramBank { - public IProgramInfo?[] ProgramInfos; - public KeyGroup[] KeyGroups; + public IProgramInfo?[] ProgramInfos = null!; + public KeyGroup[] KeyGroups = null!; } public class KeyGroup // Size 0x8 { @@ -331,13 +331,14 @@ public sealed class LFOInfo public byte[] Unknown2; public ProgramBank? Programs; - public SampleBlock[] Samples; + 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); @@ -345,6 +346,7 @@ public SWD(string path) Version = r.ReadUInt16(); Unknown2 = new byte[2]; r.ReadBytes(Unknown2); + switch (Version) { case 0x402: diff --git a/VG Music Studio - Core/NDS/SDAT/SDAT.cs b/VG Music Studio - Core/NDS/SDAT/SDAT.cs index df2f861..2b31cf4 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDAT.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDAT.cs @@ -136,7 +136,7 @@ public sealed class BankInfo public byte Unknown1 { get; set; } public byte Unknown2 { get; set; } [BinaryArrayFixedLength(4)] - public ushort[] SWARs { get; set; } + public ushort[] SWARs { get; set; } = null!; } public sealed class WaveArchiveInfo { diff --git a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs index 66118b1..6d69009 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs @@ -7,15 +7,16 @@ 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 + 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)); + Events[trackIndex]!.Add(new SongEvent(cmdOffset, command)); } private bool EventExists(byte trackIndex, long cmdOffset) { - return Events[trackIndex].Exists(e => e.Offset == cmdOffset); + return Events[trackIndex]!.Exists(e => e.Offset == cmdOffset); } private int ReadArg(ref int dataOffset, ArgType type) @@ -63,7 +64,7 @@ private int ReadArg(ref int dataOffset, ArgType type) private void AddTrackEvents(byte trackIndex, int trackStartOffset) { - ref List trackEvents = ref Events[trackIndex]; + ref List? trackEvents = ref Events[trackIndex]; trackEvents ??= new List(); int callStackDepth = 0; @@ -637,10 +638,7 @@ public void SetTicks() for (int i = 0; i < 0x10; i++) { ref List? evs = ref Events[i]; - if (evs is not null) - { - evs.Sort((e1, e2) => e1.Offset.CompareTo(e2.Offset)); - } + evs?.Sort((e1, e2) => e1.Offset.CompareTo(e2.Offset)); } _player.InitEmulation(); diff --git a/VG Music Studio - Core/SongState.cs b/VG Music Studio - Core/SongState.cs index c3035a7..02d5e92 100644 --- a/VG Music Studio - Core/SongState.cs +++ b/VG Music Studio - Core/SongState.cs @@ -27,6 +27,9 @@ public Track() { Keys[i] = byte.MaxValue; } + + Type = null!; + PreviousKeys = null!; } public void Reset() @@ -36,7 +39,7 @@ public void Reset() LFO = PitchBend = PreviousKeysTime = 0; Panpot = 0; LeftVolume = RightVolume = 0f; - Type = PreviousKeys = null; + Type = PreviousKeys = null!; for (int i = 0; i < MAX_KEYS; i++) { Keys[i] = byte.MaxValue; @@ -48,7 +51,7 @@ public void Reset() public const int MAX_TRACKS = 18; // PMD2 has a few songs with 18 tracks public ushort Tempo; - public Track[] Tracks; + public readonly Track[] Tracks; public SongState() { diff --git a/VG Music Studio - Core/Util/ConfigUtils.cs b/VG Music Studio - Core/Util/ConfigUtils.cs index af490fa..a51db6e 100644 --- a/VG Music Studio - Core/Util/ConfigUtils.cs +++ b/VG Music Studio - Core/Util/ConfigUtils.cs @@ -10,7 +10,7 @@ namespace Kermalis.VGMusicStudio.Core.Util; public static class ConfigUtils { public const string PROGRAM_NAME = "VG Music Studio"; - private static readonly string[] _notes = new string[12] { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" }; + 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); @@ -81,6 +81,7 @@ public static TEnum ParseEnum(string valueName, string value) where TEnum } /// public static TValue GetValue(this IDictionary dictionary, TKey key) + where TKey : notnull { try { @@ -139,13 +140,14 @@ public static string CombineWithBaseDirectory(string path) public static string GetNoteName(int note) { - return _notes[note]; + return Notes[note]; } public static string GetKeyName(int note) { if (!_keyCache.TryGetValue(note, out string? str)) { - str = _notes[note % 12] + ((note / 12) + (GlobalConfig.Instance.MiddleCOctave - 5)); + // {C} + {5} = "C5" + str = Notes[note % 12] + ((note / 12) + (GlobalConfig.Instance.MiddleCOctave - 5)); _keyCache.Add(note, str); } return str; diff --git a/VG Music Studio - WinForms/SongInfoControl.cs b/VG Music Studio - WinForms/SongInfoControl.cs index b2bc0b7..36cfe49 100644 --- a/VG Music Studio - WinForms/SongInfoControl.cs +++ b/VG Music Studio - WinForms/SongInfoControl.cs @@ -69,7 +69,7 @@ public SongInfoControl() private void TogglePiano(object? sender, EventArgs e) { - var check = (CheckBox)sender; + var check = (CheckBox)sender!; CheckBox master = _pianos[SongState.MAX_TRACKS]; if (check == master) { @@ -100,7 +100,7 @@ private void TogglePiano(object? sender, EventArgs e) } private void ToggleMute(object? sender, EventArgs e) { - var check = (CheckBox)sender; + var check = (CheckBox)sender!; CheckBox master = _mutes[SongState.MAX_TRACKS]; if (check == master) { @@ -117,7 +117,7 @@ private void ToggleMute(object? sender, EventArgs e) { if (_mutes[i] == check) { - Engine.Instance.Mixer.Mutes[i] = !check.Checked; + Engine.Instance!.Mixer.Mutes[i] = !check.Checked; } if (_mutes[i].Checked) { @@ -284,9 +284,9 @@ private void DrawVerticalBars(Graphics g, SongState.Track track, int vBarY1, int // 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); + vBarY1, + (int)((track.LeftVolume + track.RightVolume) * _barWidth / 2), + _barHeight); if (rect.Width > 0) { float velocity = track.LeftVolume + track.RightVolume; diff --git a/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs b/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs index 5a073ff..d9cfebf 100644 --- a/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs +++ b/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs @@ -341,10 +341,21 @@ 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); + _ = Enum.TryParse(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, out languageID); KeyPreview = true; KeyUp += FlexibleMessageBoxForm_KeyUp; + + components = null!; + button1 = null!; + button2 = null!; + button3 = null!; + FlexibleMessageBoxFormBindingSource = null!; + richTextBoxMessage = null!; + panel1 = null!; + pictureBoxForIcon = null!; + CaptionText = null!; + MessageText = null!; } #endregion @@ -401,8 +412,8 @@ static void SetDialogStartPosition(FlexibleMessageBoxForm flexibleMessageBoxForm { 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; + flexibleMessageBoxForm.Left = screen.Bounds.Left + (screen.Bounds.Width / 2) - (flexibleMessageBoxForm.Width / 2); + flexibleMessageBoxForm.Top = screen.Bounds.Top + (screen.Bounds.Height / 2) - (flexibleMessageBoxForm.Height / 2); } } @@ -566,7 +577,7 @@ static void SetDialogButtons(FlexibleMessageBoxForm flexibleMessageBoxForm, Mess void FlexibleMessageBoxForm_Shown(object? sender, EventArgs e) { - int buttonIndexToFocus = 1; + int buttonIndexToFocus; Button buttonToFocus; //Set the default button... @@ -610,7 +621,7 @@ void LinkClicked(object? sender, LinkClickedEventArgs e) try { Cursor.Current = Cursors.WaitCursor; - Process.Start(e.LinkText); + Process.Start(e.LinkText!); } catch (Exception) { diff --git a/VG Music Studio - WinForms/Util/ImageComboBox.cs b/VG Music Studio - WinForms/Util/ImageComboBox.cs index 48826e3..bfcb4ff 100644 --- a/VG Music Studio - WinForms/Util/ImageComboBox.cs +++ b/VG Music Studio - WinForms/Util/ImageComboBox.cs @@ -6,7 +6,7 @@ namespace Kermalis.VGMusicStudio.WinForms.Util; internal sealed class ImageComboBox : ComboBox { - private const int _imgSize = 15; + private const int IMG_SIZE = 15; private bool _open = false; public ImageComboBox() @@ -24,8 +24,8 @@ protected override void OnDrawItem(DrawItemEventArgs e) { 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); + 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); @@ -54,7 +54,7 @@ public ImageComboBoxItem(object item, Image image, int indentLevel) IndentLevel = indentLevel; } - public override string ToString() + 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 index ab222a0..9b3c7e8 100644 --- a/VG Music Studio - WinForms/Util/VGMSDebug.cs +++ b/VG Music Studio - WinForms/Util/VGMSDebug.cs @@ -49,6 +49,7 @@ internal static class VGMSDebug 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) @@ -68,7 +69,7 @@ public static void EventScan(List songs, bool showIndexes) continue; } - foreach (string cmd in player.LoadedSong.Events.Where(ev => ev is not null).SelectMany(ev => ev).Select(ev => ev.Command.Label).Distinct()) + 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)) { @@ -78,6 +79,7 @@ public static void EventScan(List songs, bool showIndexes) 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))); @@ -88,6 +90,7 @@ public static void EventScan(List songs, bool showIndexes) 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++) { @@ -96,14 +99,17 @@ public static void GBAGameCodeScan(string path) { using (FileStream stream = File.OpenRead(file)) { - var reader = new EndianBinaryReader(stream, ascii: true); + var r = new EndianBinaryReader(stream, ascii: true); + stream.Position = 0xAC; - string gameCode = reader.ReadString_Count(3); + string gameCode = r.ReadString_Count(3); stream.Position = 0xAF; - char regionCode = reader.ReadChar(); + char regionCode = r.ReadChar(); stream.Position = 0xBC; - byte version = reader.ReadByte(); - files[i] = string.Format("Code: {0}\tRegion: {1}\tVersion: {2}\tFile: {3}", gameCode, regionCode, version, file); + byte version = r.ReadByte(); + + files[i] = string.Format("Code: {0}\tRegion: {1}\tVersion: {2}\tFile: {3}", + gameCode, regionCode, version, file); } } catch (Exception ex) diff --git a/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj index 07947c0..cd3552b 100644 --- a/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj +++ b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj @@ -22,9 +22,18 @@ - - - + + + + NU1701 + + + NU1701 + + + NU1701 + + From cc44ec3af4ad8c9e4fc6b8a082bfef25a91cbe1e Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Sun, 14 May 2023 16:07:19 -0400 Subject: [PATCH 30/34] MORE SPAN --- VG Music Studio - Core/ADPCMDecoder.cs | 27 ++++++++++-------- VG Music Studio - Core/GBA/GBAUtils.cs | 6 ++-- VG Music Studio - Core/NDS/DSE/DSEChannel.cs | 3 +- VG Music Studio - Core/NDS/DSE/DSEUtils.cs | 12 ++++---- .../NDS/SDAT/SDATChannel.cs | 15 ++++------ VG Music Studio - Core/NDS/SDAT/SDATUtils.cs | 28 ++++++++++--------- 6 files changed, 48 insertions(+), 43 deletions(-) diff --git a/VG Music Studio - Core/ADPCMDecoder.cs b/VG Music Studio - Core/ADPCMDecoder.cs index cb2a5d8..894ea76 100644 --- a/VG Music Studio - Core/ADPCMDecoder.cs +++ b/VG Music Studio - Core/ADPCMDecoder.cs @@ -1,13 +1,14 @@ -namespace Kermalis.VGMusicStudio.Core; +using System; -// TODO: Struct or something to prevent allocations -internal sealed class ADPCMDecoder +namespace Kermalis.VGMusicStudio.Core; + +internal struct ADPCMDecoder { - private static readonly short[] _indexTable = new short[8] + private static ReadOnlySpan IndexTable => new short[8] { -1, -1, -1, -1, 2, 4, 6, 8, }; - private static readonly short[] _stepTable = new short[89] + private static ReadOnlySpan StepTable => new short[89] { 00007, 00008, 00009, 00010, 00011, 00012, 00013, 00014, 00016, 00017, 00019, 00021, 00023, 00025, 00028, 00031, @@ -23,24 +24,26 @@ internal sealed class ADPCMDecoder 32767, }; - private readonly byte[] _data; + private byte[] _data; public short LastSample; public short StepIndex; public int DataOffset; public bool OnSecondNibble; - public ADPCMDecoder(byte[] data) + public void Init(byte[] data) { + _data = data; LastSample = (short)(data[0] | (data[1] << 8)); StepIndex = (short)((data[2] | (data[3] << 8)) & 0x7F); DataOffset = 4; - _data = data; + OnSecondNibble = false; } - // TODO: Span? public static short[] ADPCMToPCM16(byte[] data) { - var decoder = new ADPCMDecoder(data); + var decoder = new ADPCMDecoder(); + decoder.Init(data); + short[] buffer = new short[(data.Length - 4) * 2]; for (int i = 0; i < buffer.Length; i++) { @@ -52,7 +55,7 @@ public static short[] ADPCMToPCM16(byte[] data) public short GetSample() { int val = (_data[DataOffset] >> (OnSecondNibble ? 4 : 0)) & 0xF; - short step = _stepTable[StepIndex]; + short step = StepTable[StepIndex]; int diff = (step / 8) + (step / 4 * (val & 1)) + @@ -70,7 +73,7 @@ public short GetSample() } LastSample = (short)a; - a = StepIndex + _indexTable[val & 7]; + a = StepIndex + IndexTable[val & 7]; if (a < 0) { a = 0; diff --git a/VG Music Studio - Core/GBA/GBAUtils.cs b/VG Music Studio - Core/GBA/GBAUtils.cs index c59b491..ca4ecf1 100644 --- a/VG Music Studio - Core/GBA/GBAUtils.cs +++ b/VG Music Studio - Core/GBA/GBAUtils.cs @@ -1,4 +1,6 @@ -namespace Kermalis.VGMusicStudio.Core.GBA; +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA; internal static class GBAUtils { @@ -8,5 +10,5 @@ internal static class GBAUtils public const int CARTRIDGE_OFFSET = 0x08_000_000; public const int CARTRIDGE_CAPACITY = 0x02_000_000; - public static readonly string[] PSGTypes = new string[4] { "Square 1", "Square 2", "PCM4", "Noise" }; + public static ReadOnlySpan PSGTypes => new string[4] { "Square 1", "Square 2", "PCM4", "Noise" }; } diff --git a/VG Music Studio - Core/NDS/DSE/DSEChannel.cs b/VG Music Studio - Core/NDS/DSE/DSEChannel.cs index f66e0e7..11670ce 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEChannel.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEChannel.cs @@ -44,7 +44,6 @@ internal sealed class DSEChannel public DSEChannel(byte i) { _sample = null!; - _adpcmDecoder = null!; Index = i; } @@ -70,7 +69,7 @@ public bool StartPCM(SWD localswd, SWD masterswd, byte voice, int key, uint note BaseTimer = (ushort)(NDSUtils.ARM7_CLOCK / _sample.WavInfo.SampleRate); if (_sample.WavInfo.SampleFormat == SampleFormat.ADPCM) { - _adpcmDecoder = new ADPCMDecoder(_sample.Data); + _adpcmDecoder.Init(_sample.Data); } //attackVolume = sample.WavInfo.AttackVolume == 0 ? split.AttackVolume : sample.WavInfo.AttackVolume; //attack = sample.WavInfo.Attack == 0 ? split.Attack : sample.WavInfo.Attack; diff --git a/VG Music Studio - Core/NDS/DSE/DSEUtils.cs b/VG Music Studio - Core/NDS/DSE/DSEUtils.cs index 349c009..8264b31 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEUtils.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEUtils.cs @@ -1,8 +1,10 @@ -namespace Kermalis.VGMusicStudio.Core.NDS.DSE; +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; internal static class DSEUtils { - public static short[] Duration16 = new short[128] + public static ReadOnlySpan Duration16 => new short[128] { 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, @@ -21,7 +23,7 @@ internal static class DSEUtils 0x16D0, 0x17A2, 0x187E, 0x195A, 0x1A40, 0x1B30, 0x1C20, 0x1D1A, 0x1E1E, 0x1F22, 0x2030, 0x2148, 0x2260, 0x2382, 0x2710, 0x7FFF, }; - public static int[] Duration32 = new int[128] + public static ReadOnlySpan Duration32 => new int[128] { 0x00000000, 0x00000004, 0x00000007, 0x0000000A, 0x0000000F, 0x00000015, 0x0000001C, 0x00000024, 0x0000002E, 0x0000003A, 0x00000048, 0x00000057, 0x00000068, 0x0000007B, 0x00000091, 0x000000A8, @@ -40,13 +42,13 @@ internal static class DSEUtils 0x000341B0, 0x000355F8, 0x00036A90, 0x00037F79, 0x000394B4, 0x0003AA41, 0x0003C021, 0x0003D654, 0x0003ECDA, 0x000403B5, 0x00041AE5, 0x0004326A, 0x00044A45, 0x00046277, 0x00047B00, 0x7FFFFFFF, }; - public static readonly byte[] FixedRests = new byte[0x10] + 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 == EnvelopeState.Two || state >= EnvelopeState.Seven; + return state is EnvelopeState.Two or >= EnvelopeState.Seven; } } diff --git a/VG Music Studio - Core/NDS/SDAT/SDATChannel.cs b/VG Music Studio - Core/NDS/SDAT/SDATChannel.cs index 87fdf6d..a925176 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATChannel.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATChannel.cs @@ -49,7 +49,7 @@ internal sealed class SDATChannel // PCM8, PCM16 private int _dataOffset; // ADPCM - private ADPCMDecoder? _adpcmDecoder; + private ADPCMDecoder _adpcmDecoder; private short _adpcmLoopLastSample; private short _adpcmLoopStepIndex; // PSG @@ -70,7 +70,7 @@ public void StartPCM(SWAR.SWAV swav, int noteDuration) _swav = swav; if (swav.Format == SWAVFormat.ADPCM) { - _adpcmDecoder = new ADPCMDecoder(swav.Samples); + _adpcmDecoder.Init(swav.Samples); } BaseTimer = swav.Timer; Start(noteDuration); @@ -102,10 +102,7 @@ private void Start(int noteDuration) public void Stop() { - if (Owner is not null) - { - Owner.Channels.Remove(this); - } + Owner?.Channels.Remove(this); Owner = null; Volume = 0; Priority = 0; @@ -249,7 +246,7 @@ public void EmulateProcess() } case SWAVFormat.ADPCM: { - if (_adpcmDecoder!.DataOffset >= _swav.Samples.Length && !_adpcmDecoder.OnSecondNibble) + if (_adpcmDecoder.DataOffset >= _swav.Samples.Length && !_adpcmDecoder.OnSecondNibble) { Stop(); } @@ -329,13 +326,13 @@ public void Process(out short left, out short right) case SWAVFormat.ADPCM: { // If just looped - if (_swav.DoesLoop && _adpcmDecoder!.DataOffset == _swav.LoopOffset * 4 && !_adpcmDecoder.OnSecondNibble) + 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 (_adpcmDecoder.DataOffset >= _swav.Samples.Length && !_adpcmDecoder.OnSecondNibble) { if (_swav.DoesLoop) { diff --git a/VG Music Studio - Core/NDS/SDAT/SDATUtils.cs b/VG Music Studio - Core/NDS/SDAT/SDATUtils.cs index 675d657..e86d328 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATUtils.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATUtils.cs @@ -1,8 +1,10 @@ -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; internal static class SDATUtils { - public static readonly byte[] AttackTable = new byte[128] + public static ReadOnlySpan AttackTable => new byte[128] { 255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240, @@ -21,7 +23,7 @@ internal static class SDATUtils 127, 123, 116, 109, 100, 92, 84, 73, 63, 51, 38, 26, 14, 5, 1, 0, }; - public static readonly ushort[] DecayTable = new ushort[128] + public static ReadOnlySpan DecayTable => new ushort[128] { 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, @@ -40,7 +42,7 @@ internal static class SDATUtils 549, 591, 640, 698, 768, 853, 960, 1097, 1280, 1536, 1920, 2560, 3840, 7680, 15360, 65535, }; - public static readonly int[] SustainTable = new int[128] + public static ReadOnlySpan SustainTable => new int[128] { -92544, -92416, -92288, -83328, -76928, -71936, -67840, -64384, -61440, -58880, -56576, -54400, -52480, -50688, -49024, -47488, @@ -60,7 +62,7 @@ internal static class SDATUtils -1280, -1024, -896, -768, -512, -384, -128, 0, }; - private static readonly sbyte[] _sinTable = new sbyte[33] + private static ReadOnlySpan SinTable => new sbyte[33] { 000, 006, 012, 019, 025, 031, 037, 043, 049, 054, 060, 065, 071, 076, 081, 085, @@ -72,21 +74,21 @@ public static int Sin(int index) { if (index < 0x20) { - return _sinTable[index]; + return SinTable[index]; } if (index < 0x40) { - return _sinTable[0x20 - (index - 0x20)]; + return SinTable[0x20 - (index - 0x20)]; } if (index < 0x60) { - return -_sinTable[index - 0x40]; + return -SinTable[index - 0x40]; } // < 0x80 - return -_sinTable[0x20 - (index - 0x60)]; + return -SinTable[0x20 - (index - 0x60)]; } - private static readonly ushort[] _pitchTable = new ushort[768] + private static ReadOnlySpan PitchTable => new ushort[768] { 0, 59, 118, 178, 237, 296, 356, 415, 475, 535, 594, 654, 714, 773, 833, 893, @@ -185,7 +187,7 @@ public static int Sin(int index) 63657, 63774, 63890, 64007, 64124, 64241, 64358, 64476, 64593, 64711, 64828, 64946, 65064, 65182, 65300, 65418, }; - private static readonly byte[] _volumeTable = new byte[724] + private static ReadOnlySpan VolumeTable => new byte[724] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -297,7 +299,7 @@ public static ushort GetChannelTimer(ushort baseTimer, int pitch) pitch -= 0x300; } - ulong timer = (_pitchTable[pitch] + 0x10000uL) * baseTimer; + ulong timer = (PitchTable[pitch] + 0x10000uL) * baseTimer; shift -= 16; if (shift <= 0) { @@ -337,6 +339,6 @@ public static byte GetChannelVolume(int vol) { a = 0; } - return _volumeTable[a + 723]; + return VolumeTable[a + 723]; } } From 66ab16ab1efbe51bf82a245a8fc778ef6310fc14 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Sun, 14 May 2023 16:08:04 -0400 Subject: [PATCH 31/34] C key names on the piano --- VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs | 2 +- VG Music Studio - Core/NDS/DSE/SWD.cs | 3 + VG Music Studio - Core/Player.cs | 1 + VG Music Studio - Core/Util/ConfigUtils.cs | 14 +-- VG Music Studio - WinForms/PianoControl.cs | 64 ++------------ .../PianoControl_PianoKey.cs | 86 +++++++++++++++++++ 6 files changed, 107 insertions(+), 63 deletions(-) create mode 100644 VG Music Studio - WinForms/PianoControl_PianoKey.cs diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs b/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs index 09dd5e6..6b3b0f6 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs @@ -31,7 +31,7 @@ internal static partial class MP2KUtils (42048, 704), // 59.72727272727273 }; - // Squares + // 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, }; diff --git a/VG Music Studio - Core/NDS/DSE/SWD.cs b/VG Music Studio - Core/NDS/DSE/SWD.cs index fa022f5..90c28ad 100644 --- a/VG Music Studio - Core/NDS/DSE/SWD.cs +++ b/VG Music Studio - Core/NDS/DSE/SWD.cs @@ -378,6 +378,7 @@ 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); @@ -386,6 +387,7 @@ private static long FindChunk(EndianBinaryReader r, string chunk) pos = r.Stream.Position - 4; break; } + switch (str) { case "swdb": @@ -405,6 +407,7 @@ private static long FindChunk(EndianBinaryReader r, string chunk) } } } + r.Stream.Position = oldPosition; return pos; } diff --git a/VG Music Studio - Core/Player.cs b/VG Music Studio - Core/Player.cs index 453af4b..33bd9b0 100644 --- a/VG Music Studio - Core/Player.cs +++ b/VG Music Studio - Core/Player.cs @@ -185,6 +185,7 @@ private void TimerTick() public void Dispose() { + GC.SuppressFinalize(this); if (State != PlayerState.ShutDown) { State = PlayerState.ShutDown; diff --git a/VG Music Studio - Core/Util/ConfigUtils.cs b/VG Music Studio - Core/Util/ConfigUtils.cs index a51db6e..5238c51 100644 --- a/VG Music Studio - Core/Util/ConfigUtils.cs +++ b/VG Music Studio - Core/Util/ConfigUtils.cs @@ -71,7 +71,8 @@ public static bool ParseBoolean(string valueName, string value) return result; } /// - public static TEnum ParseEnum(string valueName, string value) where TEnum : unmanaged + public static TEnum ParseEnum(string valueName, string value) + where TEnum : unmanaged { if (!Enum.TryParse(value, out TEnum result)) { @@ -106,7 +107,8 @@ public static bool GetValidBoolean(this YamlMappingNode yamlNode, string key) } /// /// - public static TEnum GetValidEnum(this YamlMappingNode yamlNode, string key) where TEnum : unmanaged + public static TEnum GetValidEnum(this YamlMappingNode yamlNode, string key) + where TEnum : unmanaged { return ParseEnum(key, yamlNode.Children.GetValue(key).ToString()); } @@ -142,13 +144,13 @@ public static string GetNoteName(int note) { return Notes[note]; } - public static string GetKeyName(int note) + public static string GetKeyName(int midiNote) { - if (!_keyCache.TryGetValue(note, out string? str)) + if (!_keyCache.TryGetValue(midiNote, out string? str)) { // {C} + {5} = "C5" - str = Notes[note % 12] + ((note / 12) + (GlobalConfig.Instance.MiddleCOctave - 5)); - _keyCache.Add(note, str); + str = Notes[midiNote % 12] + ((midiNote / 12) + (GlobalConfig.Instance.MiddleCOctave - 5)); + _keyCache.Add(midiNote, str); } return str; } diff --git a/VG Music Studio - WinForms/PianoControl.cs b/VG Music Studio - WinForms/PianoControl.cs index 81075ae..9c87c2d 100644 --- a/VG Music Studio - WinForms/PianoControl.cs +++ b/VG Music Studio - WinForms/PianoControl.cs @@ -33,7 +33,7 @@ namespace Kermalis.VGMusicStudio.WinForms; [DesignerCategory("")] -internal sealed class PianoControl : Control +internal sealed partial class PianoControl : Control { private enum KeyType : byte { @@ -44,62 +44,14 @@ private enum KeyType : byte private const double BLACK_KEY_SCALE = 2.0 / 3; public const int WHITE_KEY_COUNT = 75; - private static readonly KeyType[] KeyTypeTable = new KeyType[12] + 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, }; - public sealed class PianoKey : Control - { - public bool PrevPressed; - public bool Pressed; - - public readonly SolidBrush OnBrush; - private readonly SolidBrush _offBrush; - - 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)); - } - - 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; public int WhiteKeyWidth; @@ -152,8 +104,8 @@ public void UpdateKeys(SongState.Track[] tracks, bool[] enabledTracks) for (int k = 0; k <= 0x7F; k++) { PianoKey key = _keys[k]; - key.PrevPressed = key.Pressed; - key.Pressed = false; + key.PrevIsHeld = key.IsHeld; + key.IsHeld = false; } for (int i = SongState.MAX_TRACKS - 1; i >= 0; i--) { @@ -173,13 +125,13 @@ public void UpdateKeys(SongState.Track[] tracks, bool[] enabledTracks) PianoKey key = _keys[k]; key.OnBrush.Color = GlobalConfig.Instance.Colors[track.Voice]; - key.Pressed = true; + key.IsHeld = true; } } for (int k = 0; k <= 0x7F; k++) { PianoKey key = _keys[k]; - if (key.Pressed != key.PrevPressed) + if (key.IsHeld != key.PrevIsHeld) { key.Invalidate(); } diff --git a/VG Music Studio - WinForms/PianoControl_PianoKey.cs b/VG Music Studio - WinForms/PianoControl_PianoKey.cs new file mode 100644 index 0000000..5fb41f3 --- /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); + } + } +} From 95dafe0c5966752d60992929faa453a10d06c407 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Tue, 6 Jun 2023 20:04:55 -0400 Subject: [PATCH 32/34] OOOOOOOOOOOOOOOOOPS --- VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs index 7f00ca7..c9a87d0 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs @@ -27,7 +27,7 @@ public void UpdateInstrumentCache(byte voice, out string str) case InstrumentType.Noise: str = "Noise"; break; case InstrumentType.Drum: str = "Drum"; break; case InstrumentType.KeySplit: str = "Key Split"; break; - default: str = "Invalid {0}" + (byte)t; break; + default: str = "Invalid " + (byte)t; break; } } } From a556ebac8f53cca0551fec40f124dd445c537986 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Tue, 27 Jun 2023 18:20:38 -0400 Subject: [PATCH 33/34] Fix crash --- .../Util/FlexibleMessageBox.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs b/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs index d9cfebf..05c12e9 100644 --- a/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs +++ b/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs @@ -338,14 +338,6 @@ private enum TwoLetterISOLanguageID { en, de, es, it }; 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; - components = null!; button1 = null!; button2 = null!; @@ -356,6 +348,14 @@ private FlexibleMessageBoxForm() 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 From b0d14fb4aac46b5ac2328e3d635702150f109b48 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Mon, 5 Feb 2024 05:52:28 -0500 Subject: [PATCH 34/34] [MP2K] Sonic Advance 2 --- VG Music Studio - Core/MP2K.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/VG Music Studio - Core/MP2K.yaml b/VG Music Studio - Core/MP2K.yaml index 7235295..d73128c 100644 --- a/VG Music Studio - Core/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

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