diff --git a/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj b/(Deprecated) VG Music Studio - WinForms/(Deprecated) VG Music Studio - WinForms.csproj old mode 100644 new mode 100755 similarity index 86% rename from VG Music Studio - WinForms/VG Music Studio - WinForms.csproj rename to (Deprecated) VG Music Studio - WinForms/(Deprecated) VG Music Studio - WinForms.csproj index cd3552b0..d9aeb315 --- a/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj +++ b/(Deprecated) VG Music Studio - WinForms/(Deprecated) VG Music Studio - WinForms.csproj @@ -1,14 +1,15 @@  - net7.0-windows + net10.0-windows + true WinExe latest Kermalis.VGMusicStudio.WinForms enable true true - ..\Build + ..\Build\WinForms Kermalis Kermalis @@ -16,12 +17,12 @@ VG Music Studio VG Music Studio 0.3.0 - Properties\Icon.ico + ..\Icons\Icon.ico False - + diff --git a/(Deprecated) VG Music Studio - WinForms/AssemblerDialog.cs b/(Deprecated) VG Music Studio - WinForms/AssemblerDialog.cs new file mode 100755 index 00000000..1a5fad9f --- /dev/null +++ b/(Deprecated) VG Music Studio - WinForms/AssemblerDialog.cs @@ -0,0 +1,131 @@ +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.GBA; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using Kermalis.VGMusicStudio.WinForms.Util; +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 class AssemblerDialog : ThemedForm +{ + private Assembler? _assembler; + private readonly LoadedSong _song = (LoadedSong)Engine.Instance!.Player.LoadedSong!; + private readonly ThemedButton _previewButton; + private readonly ValueTextBox _offsetValueBox; + private readonly ThemedLabel _sizeLabel; + private readonly ThemedTextBox _headerLabelTextBox; + private readonly DataGridView _addedDefsGrid; + + public AssemblerDialog() + { + var openButton = new ThemedButton + { + Location = new Point(150, 0), + Text = Strings.AssemblerOpenFile + }; + openButton.Click += OpenASM; + _previewButton = new ThemedButton + { + Enabled = false, + Location = new Point(150, 50), + Size = new Size(120, 23), + Text = Strings.AssemblerPreviewSong + }; + _previewButton.Click += PreviewSong; + _sizeLabel = new ThemedLabel + { + Location = new Point(0, 100), + Size = new Size(150, 23) + }; + _offsetValueBox = new ValueTextBox + { + Hexadecimal = true, + Maximum = Engine.Instance!.Config.ROM!.LongLength - 1 + }; + _headerLabelTextBox = new ThemedTextBox { Location = new Point(0, 50), Size = new Size(150, 22) }; + _addedDefsGrid = new DataGridView + { + ColumnCount = 2, + Location = new Point(0, 150), + MultiSelect = false + }; + _addedDefsGrid.Columns[0].Name = Strings.AssemblerDefinition; + _addedDefsGrid.Columns[1].Name = Strings.AssemblerValue; + _addedDefsGrid.Columns[1].DefaultCellStyle.NullValue = "0"; + _addedDefsGrid.Rows.Add(["voicegroup000", $"0x{Engine.Instance!.Player.LoadedSong!.Bank.Offset + GBAUtils.CARTRIDGE_OFFSET:X7}"]); + _addedDefsGrid.CellValueChanged += AddedDefsGrid_CellValueChanged; + + Controls.AddRange(new Control[] { openButton, _previewButton, _sizeLabel, _offsetValueBox, _headerLabelTextBox, _addedDefsGrid }); + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + Size = new Size(600, 400); + Text = $"VG Music Studio ― {Strings.AssemblerTitle}"; + } + + void AddedDefsGrid_CellValueChanged(object? sender, DataGridViewCellEventArgs e) + { + DataGridViewCell cell = _addedDefsGrid.Rows[e.RowIndex].Cells[e.ColumnIndex]; + if (cell.Value == null) + { + return; + } + + if (e.ColumnIndex == 0) + { + if (char.IsDigit(cell.Value.ToString()![0])) + { + FlexibleMessageBox.Show(Strings.AssemblerErrorDefinitionDigit, Strings.ErrorTitleAssembler, MessageBoxButtons.OK, MessageBoxIcon.Error); + cell.Value = cell.Value.ToString()!.Substring(1); + } + } + else + { + if (!ConfigUtils.TryParseValue(cell.Value.ToString()!, 0, long.MaxValue, out long val)) + { + FlexibleMessageBox.Show(string.Format(Strings.AssemblerErrorInvalidValue, cell.Value), Strings.ErrorTitleAssembler, MessageBoxButtons.OK, MessageBoxIcon.Error); + cell.Value = null; + } + } + } + void PreviewSong(object? sender, EventArgs e) + { + ((MainForm)Owner!).PreviewSong(_song, Path.GetFileName(_assembler!.FileName)); + } + void OpenASM(object? sender, EventArgs e) + { + var d = new OpenFileDialog { Title = Strings.TitleOpenASM, Filter = $"{Strings.FilterOpenASM}|*.s" }; + if (d.ShowDialog() != DialogResult.OK) + { + return; + } + + try + { + var s = new Dictionary(); + foreach (DataGridViewRow r in _addedDefsGrid.Rows.Cast()) + { + if (r.Cells[0].Value == null || r.Cells[1].Value == null) + { + continue; + } + s.Add(r.Cells[0].Value.ToString()!, (int)ConfigUtils.ParseValue(nameof(r.Index), r.Cells[1].Value.ToString()!, 0, long.MaxValue)); + } + _song.OpenASM(_assembler = new Assembler(d.FileName, (int)(GBAUtils.CARTRIDGE_OFFSET + _offsetValueBox.Value), EndianBinaryIO.Endianness.LittleEndian, s), + _headerLabelTextBox.Text = Assembler.FixLabel(Path.GetFileNameWithoutExtension(d.FileName))); + _sizeLabel.Text = string.Format(Strings.AssemblerSizeInBytes, _assembler.BinaryLength); + _previewButton.Enabled = true; + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex.Message, Strings.ErrorTitleAssembler, MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } +} diff --git a/(Deprecated) VG Music Studio - WinForms/MIDIConverterDialog.cs b/(Deprecated) VG Music Studio - WinForms/MIDIConverterDialog.cs new file mode 100755 index 00000000..4b87b804 --- /dev/null +++ b/(Deprecated) VG Music Studio - WinForms/MIDIConverterDialog.cs @@ -0,0 +1,104 @@ +using Kermalis.MIDI; +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.GBA; +using Kermalis.VGMusicStudio.Core.GBA.AlphaDream; +using Kermalis.VGMusicStudio.Core.GBA.MP2K; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.WinForms.Properties; +using Kermalis.VGMusicStudio.WinForms.Util; +using PlatinumLucario.MIDI.GBA.MP2K; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms; + +[DesignerCategory("")] +class MIDIConverterDialog : ThemedForm +{ + private readonly LoadedSong _song = (LoadedSong)Engine.Instance!.Player.LoadedSong!; + private string? _midiFilePath; + private readonly ThemedButton _previewButton; + private readonly ValueTextBox _offsetValueBox; + + public MIDIConverterDialog() + { + var openButton = new ThemedButton + { + Location = new Point(150, 0), + Text = Strings.MIDIConverterOpenFile + }; + openButton.Click += OpenMIDI; + _previewButton = new ThemedButton + { + Enabled = false, + Location = new Point(150, 50), + Size = new Size(120, 23), + Text = Strings.MIDIConverterPreviewSong + }; + _previewButton.Click += PreviewSong; + _offsetValueBox = new ValueTextBox + { + Hexadecimal = true, + Maximum = Engine.Instance!.Config.ROM!.Length - 1, + Value = Engine.Instance.Player.LoadedSong!.Bank.Offset + }; + + Controls.AddRange([openButton, _previewButton, _offsetValueBox]); + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + Size = new Size(600, 400); + Text = $"VG Music Studio ― {Strings.MIDIConverterTitle}"; + } + + private void PreviewSong(object? sender, EventArgs e) + { + ((MainForm)Owner!).PreviewSong(_song, Path.GetFileName(_midiFilePath)!); + } + private void OpenMIDI(object? sender, EventArgs e) + { + string? inFile = WinFormsUtils.CreateLoadDialog(".mid", Strings.MenuOpenMIDI, Strings.FilterOpenMIDI + " (*.mid;*.midi)|*.mid;*.midi" ); + if (inFile is null) + { + return; + } + + try + { + _midiFilePath = inFile; + switch (Engine.Instance) + { + case MP2KEngine: + { + MP2KConverter converter = new(new MIDIFile(new FileStream(inFile, FileMode.Open))); + string asmFilePath = Path.ChangeExtension(Path.GetFullPath(_midiFilePath), "s"); + string asmFileName = Path.GetFileName(asmFilePath); + converter.SaveAsASM(asmFilePath); + // var process = new Process + // { + // StartInfo = new ProcessStartInfo + // { + // FileName = "midi2agb.exe", + // Arguments = string.Format("\"{0}\" \"{1}\"", _midiFilePath, "temp.s") + // } + // }; + // process.Start(); + // process.WaitForExit(); + var asm = new Assembler(asmFileName, GBAUtils.CARTRIDGE_CAPACITY, EndianBinaryIO.Endianness.LittleEndian, new Dictionary { { "voicegroup000", (int)(GBAUtils.CARTRIDGE_CAPACITY + _offsetValueBox.Value) } }); + File.Delete(asmFileName); + _song.OpenASM(asm, Path.GetFileNameWithoutExtension(asmFilePath)); + break; + } + } + _previewButton.Enabled = true; + } + catch (Exception ex) + { + FlexibleMessageBox.Show(string.Format(Strings.MIDIConverterError, Environment.NewLine + ex.Message), Strings.MIDIConverterTitleError, MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } +} diff --git a/VG Music Studio - WinForms/MainForm.cs b/(Deprecated) VG Music Studio - WinForms/MainForm.cs old mode 100644 new mode 100755 similarity index 82% rename from VG Music Studio - WinForms/MainForm.cs rename to (Deprecated) VG Music Studio - WinForms/MainForm.cs index 8820c082..c00b0d3c --- a/VG Music Studio - WinForms/MainForm.cs +++ b/(Deprecated) VG Music Studio - WinForms/MainForm.cs @@ -31,7 +31,10 @@ internal sealed class MainForm : ThemedForm private PlayingPlaylist? _playlist; private int _curSong = -1; + private AssemblerDialog? _assemblerDialog; + private MIDIConverterDialog? _midiConverterDialog; private TrackViewer? _trackViewer; + private SoundBankEditor? _soundBankEditor; private bool _songEnded = false; private bool _positionBarFree = true; @@ -41,7 +44,7 @@ internal sealed class MainForm : ThemedForm private readonly MenuStrip _mainMenu; private readonly ToolStripMenuItem _fileItem, _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem, - _dataItem, _trackViewerItem, _exportDLSItem, _exportMIDIItem, _exportSF2Item, _exportWAVItem, + _dataItem, _trackViewerItem, _soundBankEditorItem, _importASMItem, _exportASMItem, _importMIDIItem, _exportMIDIItem, _exportDLSItem, _exportSF2Item, _exportWAVItem, _playlistItem, _endPlaylistItem; private readonly Timer _timer; private readonly ThemedNumeric _songNumerical; @@ -57,6 +60,7 @@ internal sealed class MainForm : ThemedForm private MainForm() { + Mixer.PlaybackBackend = Mixer.AudioBackend.NAudio; PianoTracks = new bool[SongState.MAX_TRACKS]; for (int i = 0; i < SongState.MAX_TRACKS; i++) { @@ -78,18 +82,26 @@ private MainForm() _fileItem.DropDownItems.AddRange(new ToolStripItem[] { _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem }); // Data Menu - _trackViewerItem = new ToolStripMenuItem { ShortcutKeys = Keys.Control | Keys.T, Text = Strings.TrackViewerTitle }; + _trackViewerItem = new ToolStripMenuItem { ShortcutKeys = Keys.Control | Keys.T, Text = Strings.TrackEditorTitle }; _trackViewerItem.Click += OpenTrackViewer; - _exportDLSItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveDLS }; - _exportDLSItem.Click += ExportDLS; + _soundBankEditorItem = new ToolStripMenuItem { Text = Strings.SoundBankEditorTitle, Enabled = false, ShortcutKeys = Keys.Control | Keys.V }; + _soundBankEditorItem.Click += OpenSoundBankEditor; + _importASMItem = new ToolStripMenuItem { Text = Strings.MenuOpenASM, Enabled = false, ShortcutKeys = Keys.Control | Keys.Shift | Keys.M }; + _importASMItem.Click += OpenAssembler; + _exportASMItem = new ToolStripMenuItem { Text = Strings.MenuSaveASM, Enabled = false }; + _exportASMItem.Click += ExportASM; + _importMIDIItem = new ToolStripMenuItem { Text = Strings.MenuOpenMIDI, Enabled = false, ShortcutKeys = Keys.Control | Keys.M }; + _importMIDIItem.Click += OpenMIDIConverter; _exportMIDIItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveMIDI }; _exportMIDIItem.Click += ExportMIDI; + _exportDLSItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveDLS }; + _exportDLSItem.Click += ExportDLS; _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 }); + _dataItem.DropDownItems.AddRange(new ToolStripItem[] { _trackViewerItem, _soundBankEditorItem, _importMIDIItem, _exportMIDIItem, _importASMItem, _exportASMItem, _exportDLSItem, _exportSF2Item, _exportWAVItem }); // Playlist Menu _endPlaylistItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuEndPlaylist }; @@ -183,12 +195,12 @@ private void SongNumerical_ValueChanged(object? sender, EventArgs e) ILoadedSong? loadedSong = player.LoadedSong; // LoadedSong is still null when there are no tracks if (loadedSong is not null) { - List songs = cfg.Playlists[0].Songs; // Complete "Music" playlist is present in all configs at index 0 + List songs = cfg.Playlists[0].Songs; // Complete "All Songs" playlist is present in all configs at index 0 int songIndex = songs.FindIndex(s => s.Index == index); if (songIndex != -1) { Text = $"{ConfigUtils.PROGRAM_NAME} ― {songs[songIndex].Name}"; // TODO: Make this a func - _songsComboBox.SelectedIndex = songIndex + 1; // + 1 because the "Music" playlist is first in the combobox + _songsComboBox.SelectedIndex = songIndex + 1; // + 1 because the "All Songs" playlist is first in the combobox } _positionBar.Maximum = loadedSong.MaxTicks; _positionBar.LargeChange = _positionBar.Maximum / 10; @@ -200,8 +212,8 @@ private void SongNumerical_ValueChanged(object? sender, EventArgs e) } _positionBar.Enabled = true; _exportWAVItem.Enabled = true; - _exportMIDIItem.Enabled = MP2KEngine.MP2KInstance is not null; - _exportDLSItem.Enabled = _exportSF2Item.Enabled = AlphaDreamEngine.AlphaDreamInstance is not null; + _soundBankEditorItem.Enabled = _importMIDIItem.Enabled = _exportMIDIItem.Enabled = _importASMItem.Enabled = _exportASMItem.Enabled = MP2KEngine.MP2KInstance is not null; + _exportDLSItem.Enabled = _exportSF2Item.Enabled = AlphaDreamEngine.AlphaDreamInstance is not null; } else { @@ -218,30 +230,38 @@ private void SongNumerical_ValueChanged(object? sender, EventArgs e) } private void SongsComboBox_SelectedIndexChanged(object? sender, EventArgs e) { - var item = (ImageComboBoxItem)_songsComboBox.SelectedItem; + var item = (ImageComboBoxItem)_songsComboBox.SelectedItem!; switch (item.Item) { case Config.Song song: - { - SetAndLoadSong(song.Index); - break; - } + { + SetAndLoadSong(song.Index); + break; + } case Config.Playlist playlist: - { - if (playlist.Songs.Count > 0 - && FlexibleMessageBox.Show(string.Format(Strings.PlayPlaylistBody, Environment.NewLine + playlist), Strings.MenuPlaylist, MessageBoxButtons.YesNo) == DialogResult.Yes) { - ResetPlaylistStuff(false); - Engine.Instance!.Player.ShouldFadeOut = true; - Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; - _endPlaylistItem.Enabled = true; - _playlist = new PlayingPlaylist(playlist); - _playlist.SetAndLoadNextSong(); + 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; } - break; - } } } + public void PreviewSong(LoadedSong song, string caption) + { + Text = $"{ConfigUtils.PROGRAM_NAME} - {caption}"; + Stop(); + Engine.Instance!.Player.LoadSong(song); + _trackViewer?.UpdateTracks(); + _soundBankEditor?.UpdateTable(); + } private void ResetPlaylistStuff(bool numericalAndComboboxEnabled) { if (Engine.Instance is not null) @@ -264,9 +284,14 @@ private void EndCurrentPlaylist(object? sender, EventArgs e) private void OpenDSE(object? sender, EventArgs e) { + var f = WinFormsUtils.CreateLoadDialog(".swd", Strings.MenuOpenSWD, Strings.FilterOpenSWD + " (*.swd)|*.swd"); + if (f is null) + { + return; + } var d = new FolderBrowserDialog { - Description = Strings.MenuOpenDSE, + Description = Strings.MenuOpenSMD, UseDescriptionForTitle = true, }; if (d.ShowDialog() != DialogResult.OK) @@ -277,7 +302,7 @@ private void OpenDSE(object? sender, EventArgs e) DisposeEngine(); try { - _ = new DSEEngine(d.SelectedPath); + _ = new DSEEngine(f.ToString(), d.SelectedPath); } catch (Exception ex) { @@ -286,7 +311,7 @@ private void OpenDSE(object? sender, EventArgs e) } DSEConfig config = DSEEngine.DSEInstance!.Config; - FinishLoading(config.BGMFiles.Length); + FinishLoading(config.SMDFiles.Length); _songNumerical.Visible = false; _exportDLSItem.Visible = false; _exportMIDIItem.Visible = false; @@ -340,8 +365,11 @@ private void OpenMP2K(object? sender, EventArgs e) MP2KConfig config = MP2KEngine.MP2KInstance!.Config; FinishLoading(config.SongTableSizes[0]); _songNumerical.Visible = true; - _exportDLSItem.Visible = false; - _exportMIDIItem.Visible = true; + _importMIDIItem.Visible = true; + _exportMIDIItem.Visible = true; + _importASMItem.Visible = true; + _exportASMItem.Visible = true; + _exportDLSItem.Visible = false; _exportSF2Item.Visible = false; } private void OpenSDAT(object? sender, EventArgs e) @@ -393,6 +421,49 @@ private void ExportDLS(object? sender, EventArgs e) FlexibleMessageBox.Show(ex, Strings.ErrorSaveDLS); } } + private void OpenAssembler(object? sender, EventArgs e) + { + if (_assemblerDialog != null) + { + _assemblerDialog.Focus(); + return; + } + _assemblerDialog = new AssemblerDialog { Owner = this }; + _assemblerDialog.FormClosed += (o, s) => _assemblerDialog = null!; + _assemblerDialog.Show(); + } + private void ExportASM(object? sender, EventArgs e) + { + string songName = Engine.Instance!.Config.GetSongName((int)_songNumerical.Value); + string? outFile = WinFormsUtils.CreateSaveDialog(songName, ".s", Strings.TitleSaveASM, Strings.FilterSaveASM + " (*.s)|*.s"); + if (outFile is null) + { + return; + } + + MP2KPlayer p = MP2KEngine.MP2KInstance!.Player; + var args = new ASMSaveArgs(true); + try + { + p.SaveAsASM(outFile, args); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveASM, outFile), Text); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex.Message, Strings.ErrorSaveASM); + } + } + private void OpenMIDIConverter(object? sender, EventArgs e) + { + if (_midiConverterDialog != null) + { + _midiConverterDialog.Focus(); + return; + } + _midiConverterDialog = new MIDIConverterDialog { Owner = this }; + _midiConverterDialog.FormClosed += (o, s) => _midiConverterDialog = null!; + _midiConverterDialog.Show(); + } private void ExportMIDI(object? sender, EventArgs e) { string songName = Engine.Instance!.Config.GetSongName((int)_songNumerical.Value); @@ -519,6 +590,7 @@ private void FinishLoading(long numSongs) _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; + _volumeBar.Value = _volumeBar.Maximum; UpdateTaskbarButtons(); } private void DisposeEngine() @@ -582,6 +654,18 @@ private void OpenTrackViewer(object? sender, EventArgs e) _trackViewer.Show(); } + private void OpenSoundBankEditor(object? sender, EventArgs e) + { + if (_soundBankEditor != null) + { + _soundBankEditor.Focus(); + return; + } + _soundBankEditor = new SoundBankEditor { Owner = this }; + _soundBankEditor.FormClosed += (o, s) => _soundBankEditor = null; + _soundBankEditor.Show(); + } + public void TogglePlayback() { switch (Engine.Instance!.Player.State) @@ -623,7 +707,7 @@ public void LetUIKnowPlayerIsPlaying() _pauseButton.Enabled = true; _stopButton.Enabled = true; _pauseButton.Text = Strings.PlayerPause; - _timer.Interval = (int)(1_000.0 / GlobalConfig.Instance.RefreshRate); + _timer.Interval = 50; _timer.Start(); TaskbarPlayerButtons.UpdateState(); UpdateTaskbarButtons(); @@ -727,9 +811,8 @@ private void Timer_Tick(object? sender, EventArgs e) Player player = Engine.Instance!.Player; if (WindowState != FormWindowState.Minimized) { - SongState info = _songInfo.Info; - player.UpdateSongState(info); - _piano.UpdateKeys(info.Tracks, PianoTracks); + player.Info = _songInfo.Info; + _piano.UpdateKeys(player.Info.Tracks, PianoTracks); _songInfo.Invalidate(); } UpdatePositionIndicators(player.ElapsedTicks); diff --git a/VG Music Studio - WinForms/PianoControl.cs b/(Deprecated) VG Music Studio - WinForms/PianoControl.cs similarity index 100% rename from VG Music Studio - WinForms/PianoControl.cs rename to (Deprecated) VG Music Studio - WinForms/PianoControl.cs diff --git a/VG Music Studio - WinForms/PianoControl_PianoKey.cs b/(Deprecated) VG Music Studio - WinForms/PianoControl_PianoKey.cs similarity index 100% rename from VG Music Studio - WinForms/PianoControl_PianoKey.cs rename to (Deprecated) VG Music Studio - WinForms/PianoControl_PianoKey.cs diff --git a/VG Music Studio - WinForms/PlayingPlaylist.cs b/(Deprecated) VG Music Studio - WinForms/PlayingPlaylist.cs old mode 100644 new mode 100755 similarity index 96% rename from VG Music Studio - WinForms/PlayingPlaylist.cs rename to (Deprecated) VG Music Studio - WinForms/PlayingPlaylist.cs index 100f88f2..27197538 --- a/VG Music Studio - WinForms/PlayingPlaylist.cs +++ b/(Deprecated) VG Music Studio - WinForms/PlayingPlaylist.cs @@ -1,6 +1,5 @@ using Kermalis.VGMusicStudio.Core; using Kermalis.VGMusicStudio.Core.Util; -using Kermalis.VGMusicStudio.WinForms.Util; using System.Collections.Generic; using System.Linq; diff --git a/VG Music Studio - WinForms/Program.cs b/(Deprecated) VG Music Studio - WinForms/Program.cs similarity index 100% rename from VG Music Studio - WinForms/Program.cs rename to (Deprecated) VG Music Studio - WinForms/Program.cs diff --git a/VG Music Studio - WinForms/Properties/Resources.Designer.cs b/(Deprecated) VG Music Studio - WinForms/Properties/Resources.Designer.cs similarity index 100% rename from VG Music Studio - WinForms/Properties/Resources.Designer.cs rename to (Deprecated) VG Music Studio - WinForms/Properties/Resources.Designer.cs diff --git a/VG Music Studio - WinForms/Properties/Resources.resx b/(Deprecated) VG Music Studio - WinForms/Properties/Resources.resx similarity index 88% rename from VG Music Studio - WinForms/Properties/Resources.resx rename to (Deprecated) VG Music Studio - WinForms/Properties/Resources.resx index ad871cdc..c65e7a94 100644 --- a/VG Music Studio - WinForms/Properties/Resources.resx +++ b/(Deprecated) VG Music Studio - WinForms/Properties/Resources.resx @@ -119,24 +119,24 @@ - ..\Properties\Icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\..\Icons\Icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - Next.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\..\Icons\Next.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - Pause.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\..\Icons\Pause.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - Play.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\..\Icons\Play.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - ..\Properties\Playlist.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\..\Icons\Playlist.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - Previous.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\..\Icons\Previous.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - ..\Properties\Song.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\..\Icons\Song.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a \ No newline at end of file diff --git a/VG Music Studio - WinForms/SongInfoControl.cs b/(Deprecated) VG Music Studio - WinForms/SongInfoControl.cs similarity index 98% rename from VG Music Studio - WinForms/SongInfoControl.cs rename to (Deprecated) VG Music Studio - WinForms/SongInfoControl.cs index 36cfe499..06304529 100644 --- a/VG Music Studio - WinForms/SongInfoControl.cs +++ b/(Deprecated) VG Music Studio - WinForms/SongInfoControl.cs @@ -1,7 +1,6 @@ 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; @@ -298,7 +297,7 @@ private void DrawVerticalBars(Graphics g, SongState.Track track, int vBarY1, int else { const int DELTA = 125; - alpha = (int)WinFormsUtils.Lerp(velocity * 0.5f, 0f, DELTA); + alpha = (int)GUIUtils.Lerp(velocity * 0.5f, 0f, DELTA); alpha += 255 - DELTA; } _solidBrush.Color = Color.FromArgb(alpha, color); @@ -351,7 +350,7 @@ private void DrawHeldKeys(Graphics g, SongState.Track track, float row1Y) } keys = _keysCache.ToString(); - track.PreviousKeysTime = GlobalConfig.Instance.RefreshRate << 2; + track.PreviousKeysTime = 120; track.PreviousKeys = keys; } if (keys.Length != 0) diff --git a/(Deprecated) VG Music Studio - WinForms/SoundBankEditor.cs b/(Deprecated) VG Music Studio - WinForms/SoundBankEditor.cs new file mode 100755 index 00000000..0ba32b8b --- /dev/null +++ b/(Deprecated) VG Music Studio - WinForms/SoundBankEditor.cs @@ -0,0 +1,269 @@ +using BrightIdeasSoftware; +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.GBA.MP2K; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms; + +[DesignerCategory("")] +class SoundBankEditor : ThemedForm +{ + private readonly ObjectListView _voicesListView, _subVoicesListView; + private readonly ThemedPanel _voicePanel; + private readonly ThemedLabel _bytesLabel, _addressLabel, + _voiceALabel, _voiceDLabel, _voiceSLabel, _voiceRLabel; + private readonly ValueTextBox _addressValue, + _voiceAValue, _voiceDValue, _voiceSValue, _voiceRValue; + private SoundBank? _table; + private int _voiceIndex; // The voice entry being edited + + public SoundBankEditor() + { + int w = (600 / 2) - 12 - 6, h = 400 - 12 - 11; + // Main SoundBank view + _voicesListView = new ObjectListView + { + FullRowSelect = true, + HeaderStyle = ColumnHeaderStyle.Nonclickable, + HideSelection = false, + Location = new Point(12, 12), + MultiSelect = false, + ShowGroups = false, + Size = new Size(w, h) + }; + _voicesListView.FormatRow += FormatRow; + OLVColumn c1, c2, c3; + c1 = new OLVColumn("#", ""); + c2 = new OLVColumn(Strings.PlayerType, "ToString"); + c3 = new OLVColumn(Strings.TrackEditorOffset, "GetOffset") { AspectToStringFormat = "0x{0:X7}" }; + c1.Width = 45; + c2.Width = c3.Width = 108; + c1.Hideable = c2.Hideable = c3.Hideable = false; + c1.TextAlign = c2.TextAlign = c3.TextAlign = HorizontalAlignment.Center; + _voicesListView.AllColumns.AddRange([c1, c2, c3]); + _voicesListView.RebuildColumns(); + _voicesListView.SelectedIndexChanged += MainIndexChanged; + + int h2 = (h / 2) - 5; + // View of the selected voice's sub-voices + _subVoicesListView = new ObjectListView + { + FullRowSelect = true, + HeaderStyle = ColumnHeaderStyle.Nonclickable, + HideSelection = false, + Location = new Point(306, 12), + MultiSelect = false, + ShowGroups = false, + Size = new Size(w, h2) + }; + _subVoicesListView.FormatRow += FormatRow; + c1 = new OLVColumn("#", ""); + c2 = new OLVColumn(Strings.PlayerType, "ToString"); + c3 = new OLVColumn(Strings.TrackEditorOffset, "GetOffset") { AspectToStringFormat = "0x{0:X7}" }; + c1.Width = 45; + c2.Width = c3.Width = 108; + c1.Hideable = c2.Hideable = c3.Hideable = false; + c1.TextAlign = c2.TextAlign = c3.TextAlign = HorizontalAlignment.Center; + _subVoicesListView.AllColumns.AddRange([c1, c2, c3]); + _subVoicesListView.RebuildColumns(); + _subVoicesListView.SelectedIndexChanged += SubIndexChanged; + + // Panel to edit a voice + _voicePanel = new ThemedPanel { Location = new Point(306, 206), Size = new Size(w, h2) }; + + // Panel controls + _bytesLabel = new ThemedLabel { Location = new Point(2, 2) }; + _addressLabel = new ThemedLabel { Location = new Point(2, 130), Text = $"{Strings.SoundBankEditorAddress}:" }; + _voiceALabel = new ThemedLabel { Location = new Point(0 * w / 4 + 2, 160), Text = "A:" }; + _voiceDLabel = new ThemedLabel { Location = new Point(1 * w / 4 + 2, 160), Text = "D:" }; + _voiceSLabel = new ThemedLabel { Location = new Point(2 * w / 4 + 2, 160), Text = "S:" }; + _voiceRLabel = new ThemedLabel { Location = new Point(3 * w / 4 + 2, 160), Text = "R:" }; + _bytesLabel.AutoSize = _addressLabel.AutoSize = + _voiceALabel.AutoSize = _voiceDLabel.AutoSize = _voiceSLabel.AutoSize = _voiceRLabel.AutoSize = true; + + _addressValue = new ValueTextBox { Location = new Point(w / 5, 127), Size = new Size(78, 24) }; + _voiceAValue = new ValueTextBox { Location = new Point(0 * w / 4 + 20, 157) }; + _voiceDValue = new ValueTextBox { Location = new Point(1 * w / 4 + 20, 157) }; + _voiceSValue = new ValueTextBox { Location = new Point(2 * w / 4 + 20, 157) }; + _voiceRValue = new ValueTextBox { Location = new Point(3 * w / 4 + 20, 157) }; + _voiceAValue.Size = _voiceDValue.Size = _voiceSValue.Size = _voiceRValue.Size = new Size(44, 22); + _voiceAValue.ValueChanged += ArgumentChanged; _voiceDValue.ValueChanged += ArgumentChanged; _voiceSValue.ValueChanged += ArgumentChanged; _voiceRValue.ValueChanged += ArgumentChanged; + + _voicePanel.Controls.AddRange([ _bytesLabel, _addressLabel, _addressValue, + _voiceALabel, _voiceDLabel, _voiceSLabel, _voiceRLabel, + _voiceAValue, _voiceDValue, _voiceSValue, _voiceRValue ]); + + ClientSize = new Size(600, 400); + Controls.AddRange([_voicesListView, _subVoicesListView, _voicePanel]); + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + + UpdateTable(); + } + + private void FormatRow(object? sender, FormatRowEventArgs e) + { + // Auto-number + e.Item.Text = e.RowIndex.ToString(); + // Auto-color + if (e.ListView == _voicesListView) + { + HSLColor color = new(GlobalConfig.Instance.Colors[(byte)e.RowIndex]); + e.Item.BackColor = GlobalConfig.Instance.Colors[(byte)e.RowIndex]; + if (color.Lightness <= 0.6 && (color.R <= 0.7 && color.G <= 0.7)) + { + e.Item.ForeColor = Color.White; + } + } + } + + // Sets the subVoicesListView objects if the selected voice has sub-voices + // Also enables editing of the selected voice + private void MainIndexChanged(object? sender, EventArgs e) + { + if (_voicesListView.SelectedIndices.Count != 1) + { + return; + } + + var elementSelected = _table!.ElementAt(_voicesListView.SelectedIndex); + _subVoicesListView.SetObjects(elementSelected.GetSubVoices()); + SetVoice(_voicesListView.SelectedIndex); + } + private void SubIndexChanged(object? sender, EventArgs e) + { + if (_subVoicesListView.SelectedIndices.Count != 1) + { + return; + } + + SetVoice(_subVoicesListView.SelectedIndex); + } + public void UpdateTable() + { + _voicesListView.SetObjects(_table = Engine.Instance!.Player.LoadedSong!.Bank); + _subVoicesListView.ClearObjects(); + Text = $"{ConfigUtils.PROGRAM_NAME} ― {Strings.SoundBankEditorTitle} (0x{_table.Offset:X7})"; + _voicesListView.SelectedIndex = 0; + } + + // Places voice info into the panel + private void SetVoice(int index) + { + _subVoicesListView.SelectedIndexChanged -= SubIndexChanged; + _addressValue.ValueChanged -= ArgumentChanged; + _voiceAValue.ValueChanged -= ArgumentChanged; _voiceDValue.ValueChanged -= ArgumentChanged; _voiceSValue.ValueChanged -= ArgumentChanged; _voiceRValue.ValueChanged -= ArgumentChanged; + _addressValue.Visible = _addressLabel.Visible = + _voiceAValue.Visible = _voiceDValue.Visible = _voiceSValue.Visible = _voiceRValue.Visible = + _voiceALabel.Visible = _voiceDLabel.Visible = _voiceSLabel.Visible = _voiceRLabel.Visible = false; + + IVoiceInfo voice = _table!.ElementAt(index); + IVoiceInfo[] subs = [.. voice.GetSubVoices()]; + + if (Engine.Instance is MP2KEngine && _table.HasADSR(index)) + { + _bytesLabel.Text = _table.GetBytesToString(index); + + #region Addresses (Direct, Key Split, Drum, Wave) + + if (_table.IsValidVoiceAddress(index)) + { + _addressValue.Hexadecimal = true; + _addressValue.Maximum = Engine.Instance!.Config.ROM!.Length - 1; + _addressValue.Visible = _addressLabel.Visible = true; + _addressValue.Value = _table.GetVoiceAddress(index); + } + + #endregion + + #region ADSR (everything except Key Split, Drum and invalids) + + if (_table.IsValidADSR(index)) + { + bool isPCM8 = !_table.IsPSGInstrument(index); + _voiceAValue.Hexadecimal = _voiceDValue.Hexadecimal = _voiceSValue.Hexadecimal = _voiceRValue.Hexadecimal = false; + _voiceAValue.Maximum = _voiceDValue.Maximum = _voiceRValue.Maximum = isPCM8 ? byte.MaxValue : 0x7; + _voiceSValue.Maximum = isPCM8 ? byte.MaxValue : 0xF; + _voiceAValue.Minimum = _voiceDValue.Minimum = _voiceSValue.Minimum = _voiceRValue.Minimum = byte.MinValue; + _voiceAValue.Visible = _voiceDValue.Visible = _voiceSValue.Visible = _voiceRValue.Visible = + _voiceALabel.Visible = _voiceDLabel.Visible = _voiceSLabel.Visible = _voiceRLabel.Visible = true; + _table.GetADSRValues(index, out byte a, out byte d, out byte s, out byte r); + _voiceAValue.Value = a; + _voiceDValue.Value = d; + _voiceSValue.Value = s; + _voiceRValue.Value = r; + } + + #endregion + } + //else if (voice.Voice is MLSSVoice mlss) + //{ + // if (mlss.Entries.Length > 0) + // { + // if (subIndex == -1) + // { + // subIndex = 0; + // } + // MLSSVoiceEntry mlssEntry = mlss.Entries[subIndex]; + // entry = mlssEntry; + + // bytesLabel.Text = mlssEntry.GetBytesToString(); + + // #region Last 4 values are probably ADSR + + // voiceAValue.Hexadecimal = voiceDValue.Hexadecimal = voiceSValue.Hexadecimal = voiceRValue.Hexadecimal = true; + // voiceAValue.Maximum = voiceDValue.Maximum = voiceSValue.Maximum = voiceRValue.Maximum = byte.MaxValue; + // voiceAValue.Minimum = voiceDValue.Minimum = voiceSValue.Minimum = voiceRValue.Minimum = byte.MinValue; + // voiceAValue.Visible = voiceDValue.Visible = voiceSValue.Visible = voiceRValue.Visible = + // voiceALabel.Visible = voiceDLabel.Visible = voiceSLabel.Visible = voiceRLabel.Visible = true; + + // voiceAValue.Value = mlssEntry.Unknown1; + // voiceDValue.Value = mlssEntry.Unknown2; + // voiceSValue.Value = mlssEntry.Unknown3; + // voiceRValue.Value = mlssEntry.Unknown4; + + // #endregion + // } + //} + + _subVoicesListView.SelectedIndex = 0; + _subVoicesListView.SelectedIndexChanged += SubIndexChanged; + _addressValue.ValueChanged += ArgumentChanged; + _voiceAValue.ValueChanged += ArgumentChanged; _voiceDValue.ValueChanged += ArgumentChanged; _voiceSValue.ValueChanged += ArgumentChanged; _voiceRValue.ValueChanged += ArgumentChanged; + } + + private void ArgumentChanged(object? sender, EventArgs e) + { + if (_table!.HasADSR(_voiceIndex!)) + { + if (_addressValue.Visible) + { + _table.SetAddressPointer(_voiceIndex!, (int)_addressValue.Value); + } + if (_voiceAValue.Visible) + { + byte a = (byte)_voiceAValue.Value; + byte d = (byte)_voiceDValue.Value; + byte s = (byte)_voiceSValue.Value; + byte r = (byte)_voiceRValue.Value; + _table.SetADSRValues(_voiceIndex!, in a, in d, in s, in r); + } + + _bytesLabel.Text = _table.GetBytesToString(_voiceIndex!); + } + //else if (entry is MLSSVoiceEntry mlssEntry) + //{ + // mlssEntry.Unknown1 = (byte)voiceAValue.Value; + // mlssEntry.Unknown2 = (byte)voiceDValue.Value; + // mlssEntry.Unknown3 = (byte)voiceSValue.Value; + // mlssEntry.Unknown4 = (byte)voiceRValue.Value; + + // bytesLabel.Text = mlssEntry.GetBytesToString(); + //} + } +} diff --git a/VG Music Studio - WinForms/TaskbarPlayerButtons.cs b/(Deprecated) VG Music Studio - WinForms/TaskbarPlayerButtons.cs similarity index 100% rename from VG Music Studio - WinForms/TaskbarPlayerButtons.cs rename to (Deprecated) VG Music Studio - WinForms/TaskbarPlayerButtons.cs diff --git a/VG Music Studio - WinForms/Theme.cs b/(Deprecated) VG Music Studio - WinForms/Theme.cs similarity index 100% rename from VG Music Studio - WinForms/Theme.cs rename to (Deprecated) VG Music Studio - WinForms/Theme.cs diff --git a/(Deprecated) VG Music Studio - WinForms/TrackViewer.cs b/(Deprecated) VG Music Studio - WinForms/TrackViewer.cs new file mode 100755 index 00000000..ea3bd5e1 --- /dev/null +++ b/(Deprecated) VG Music Studio - WinForms/TrackViewer.cs @@ -0,0 +1,286 @@ +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.Reflection; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms; + +[DesignerCategory("")] +internal sealed class TrackViewer : ThemedForm +{ + private int _currentTrack = 0; + private List? _events; + + private readonly ObjectListView _listView; + private readonly ThemedLabel[] _argLabels = new ThemedLabel[3]; + private readonly ThemedNumeric[] _argNumerics = new ThemedNumeric[3]; + + private readonly ComboBox _tracksBox, _commandsBox; + private readonly ThemedButton _trackAddEventButton, _trackRemoveEventButton; + + 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.TrackEditorEvent, "Command.Label"); + c2 = new OLVColumn(Strings.TrackEditorArguments, "Command.Arguments") { UseFiltering = false }; + c3 = new OLVColumn(Strings.TrackEditorOffset, "Offset") { AspectToStringFormat = "0x{0:X}", UseFiltering = false }; + c4 = new OLVColumn(Strings.TrackEditorTicks, "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([c1, c2, c3, c4]); + _listView.RebuildColumns(); + _listView.SelectedIndexChanged += SelectedIndexChanged; + _listView.ItemActivate += ListView_ItemActivate; + + int h2 = (H / 3) - 4; + var panel1 = new ThemedPanel { Location = new Point(306, 12), Size = new Size(W, h2) }; + var panel2 = new ThemedPanel { Location = new Point(306, 140), Size = new Size(W, h2) }; + var panel3 = new ThemedPanel { Location = new Point(306, 268), Size = new Size(W, h2) }; + + // Track controls + _tracksBox = new ComboBox + { + Enabled = false, + Location = new Point(4, 4), + Size = new Size(100, 21) + }; + _tracksBox.SelectedIndexChanged += TracksBox_SelectedIndexChanged; + _trackAddEventButton = new ThemedButton + { + Location = new Point(13, 30 + 25 + 5), + Size = new Size(100, 25), + Text = Strings.TrackEditorAddEvent + }; + _trackAddEventButton.Click += AddEvent; + _commandsBox = new ComboBox + { + Location = new Point(115, 30 + 25 + 5 + 2), + Size = new Size(100, 21) + }; + _trackRemoveEventButton = new ThemedButton + { + Location = new Point(13, 30 + 25 + 5 + 25 + 5), + Text = Strings.TrackEditorRemoveEvent + }; + _trackRemoveEventButton.Click += RemoveEvent; + _tracksBox.Enabled = _trackAddEventButton.Enabled = _trackRemoveEventButton.Enabled = _commandsBox.Enabled = false; + _trackAddEventButton.Size = _trackRemoveEventButton.Size = new Size(95, 25); + panel1.Controls.AddRange([_tracksBox, _trackAddEventButton, _commandsBox, _trackRemoveEventButton]); + + // Arguments Info + for (int i = 0; i < 3; i++) + { + int y = 17 + (33 * i); + _argLabels[i] = new ThemedLabel + { + AutoSize = true, + Location = new Point(52, y + 3), + Text = string.Format(Strings.TrackEditorArgX, i + 1), + Visible = false, + }; + _argNumerics[i] = new ThemedNumeric + { + Location = new Point(W - 152, y), + Maximum = int.MaxValue, + Minimum = int.MinValue, + Size = new Size(100, 25), + Visible = false + }; + _argNumerics[i].ValueChanged += ArgumentChanged; + panel2.Controls.AddRange([_argLabels[i], _argNumerics[i]]); + } + + // Global controls + ClientSize = new Size(600, 400); + Controls.AddRange([_listView, panel1, panel2, panel3]); + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + Text = $"{ConfigUtils.PROGRAM_NAME} ― {Strings.TrackEditorTitle}"; + + UpdateTracks(); + } + + private void ListView_ItemActivate(object? sender, EventArgs e) + { + List list = ((SongEvent)_listView.SelectedItem.RowObject).Ticks; + if (list.Count > 0) + { + Engine.Instance!.Player.SetSongPosition(list[0]); + MainForm.Instance.LetUIKnowPlayerIsPlaying(); + } + } + + private void AddEvent(object? sender, EventArgs e) + { + var cmd = (ICommand)Activator.CreateInstance(Engine.Instance!.GetCommands()[_commandsBox.SelectedIndex].GetType())!; + var ev = new SongEvent(int.MaxValue, cmd); + int index = _listView.SelectedIndex + 1; + Engine.Instance.Player.LoadedSong!.InsertEvent(ev, _currentTrack, index); + Engine.Instance.Player.RefreshSong(); + LoadTrack(_currentTrack); + SelectItem(index); + } + private void RemoveEvent(object? sender, EventArgs e) + { + if (_listView.SelectedIndex == -1) + { + return; + } + Engine.Instance!.Player.LoadedSong!.RemoveEvent(_currentTrack, _listView.SelectedIndex); + Engine.Instance!.Player.RefreshSong(); + LoadTrack(_currentTrack); + } + + 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]); + _currentTrack = i; + _events = Engine.Instance!.Player.LoadedSong!.Events[i]!; + _listView.SetObjects(_events); + SelectedIndexChanged(null, null!); + } + else + { + _listView.Items.Clear(); + } + } + private void SelectItem(int index) + { + _listView.Items[index].Selected = true; + _listView.Select(); + _listView.EnsureVisible(index); + } + + private void ArgumentChanged(object? sender, EventArgs e) + { + for (int i = 0; i < 3; i++) + { + if (sender == _argNumerics[i]) + { + SongEvent se = _events![_listView.SelectedIndices[0]]; + object value = _argNumerics[i].Value; + MemberInfo m = se.Command.GetType().GetMember(_argLabels[i].Text)[0]; + if (m is FieldInfo f) + { + f.SetValue(se.Command, Convert.ChangeType(value, f.FieldType)); + } + else if (m is PropertyInfo p) + { + p.SetValue(se.Command, Convert.ChangeType(value, p.PropertyType)); + } + + Engine.Instance!.Player.RefreshSong(); + + Control control = ActiveControl!; + int index = _listView.SelectedIndex; + LoadTrack(_currentTrack); + SelectItem(index); + control.Select(); + + return; + } + } + } + private void SelectedIndexChanged(object? sender, EventArgs e) + { + if (_listView.SelectedIndices.Count != 1) + { + _argLabels[0].Visible = _argLabels[1].Visible = _argLabels[2].Visible = + _argNumerics[0].Visible = _argNumerics[1].Visible = _argNumerics[2].Visible = false; + } + else + { + var se = (SongEvent)_listView.SelectedObject; + MemberInfo[] ignore = typeof(ICommand).GetMembers(); + MemberInfo[] mi = se.Command == null ? [] : se.Command.GetType().GetMembers().Where(m => !ignore.Any(a => m.Name == a.Name) && (m is FieldInfo || m is PropertyInfo)).ToArray(); + for (int i = 0; i < 3; i++) + { + _argLabels[i].Visible = _argNumerics[i].Visible = i < mi.Length; + if (_argNumerics[i].Visible) + { + _argLabels[i].Text = mi[i].Name; + + _argNumerics[i].ValueChanged -= ArgumentChanged; + + dynamic m = mi[i]; + + _argNumerics[i].Hexadecimal = Engine.Instance!.Player.LoadedSong!.CallOrJumpCommand(se); + + TypeInfo valueType; + if (mi[i].MemberType == MemberTypes.Field) + { + valueType = m.FieldType; + } + else + { + valueType = m.PropertyType; + } + _argNumerics[i].Maximum = valueType.DeclaredFields.Single(f => f.Name == "MaxValue").GetValue(m); + _argNumerics[i].Minimum = valueType.DeclaredFields.Single(f => f.Name == "MinValue").GetValue(m); + + object value = m.GetValue(se.Command); + _argNumerics[i].Value = (decimal)Convert.ChangeType(value, TypeCode.Decimal); + + _argNumerics[i].ValueChanged += ArgumentChanged; + } + } + } + _argLabels[0].Parent!.Refresh(); + } + private void LoadTrack(int track) + { + _currentTrack = track; + _events = Engine.Instance!.Player.LoadedSong!.Events[track]!; + _listView.SetObjects(_events); + SelectedIndexChanged(null, null!); + } + public void UpdateTracks() + { + int numTracks = Engine.Instance?.Player.LoadedSong?.Events.Length ?? 0; + bool tracks = numTracks > 0; + _tracksBox.Enabled = _trackAddEventButton.Enabled = _trackRemoveEventButton.Enabled = _commandsBox.Enabled = tracks; + if (tracks) + { + // Track 0, Track 1, ... + _tracksBox.DataSource = Enumerable.Range(0, numTracks).Select(i => string.Format(Strings.TrackEditorTrackX, i)).ToList(); + _commandsBox.DataSource = Engine.Instance!.GetCommands().Select(c => c.Label).ToList(); + } + else + { + _tracksBox.DataSource = null; + } + } +} diff --git a/VG Music Studio - WinForms/Util/ColorSlider.cs b/(Deprecated) VG Music Studio - WinForms/Util/ColorSlider.cs similarity index 100% rename from VG Music Studio - WinForms/Util/ColorSlider.cs rename to (Deprecated) VG Music Studio - WinForms/Util/ColorSlider.cs diff --git a/VG Music Studio - WinForms/Util/FlexibleMessageBox.cs b/(Deprecated) VG Music Studio - WinForms/Util/FlexibleMessageBox.cs similarity index 100% rename from VG Music Studio - WinForms/Util/FlexibleMessageBox.cs rename to (Deprecated) VG Music Studio - WinForms/Util/FlexibleMessageBox.cs diff --git a/VG Music Studio - WinForms/Util/ImageComboBox.cs b/(Deprecated) VG Music Studio - WinForms/Util/ImageComboBox.cs similarity index 100% rename from VG Music Studio - WinForms/Util/ImageComboBox.cs rename to (Deprecated) VG Music Studio - WinForms/Util/ImageComboBox.cs diff --git a/VG Music Studio - WinForms/Util/VGMSDebug.cs b/(Deprecated) VG Music Studio - WinForms/Util/VGMSDebug.cs similarity index 100% rename from VG Music Studio - WinForms/Util/VGMSDebug.cs rename to (Deprecated) VG Music Studio - WinForms/Util/VGMSDebug.cs diff --git a/(Deprecated) VG Music Studio - WinForms/Util/WinFormsUtils.cs b/(Deprecated) VG Music Studio - WinForms/Util/WinFormsUtils.cs new file mode 100644 index 00000000..9b79cc4e --- /dev/null +++ b/(Deprecated) VG Music Studio - WinForms/Util/WinFormsUtils.cs @@ -0,0 +1,140 @@ +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.WinForms.Util; + +internal class WinFormsUtils : DialogUtils +{ + private static void Convert(string filterName, Span fileExtensions) + { + string extensions; + if (fileExtensions == null) fileExtensions = new string[1]; + if (fileExtensions.Length > 1) + { + extensions = $"|"; + foreach (string ext in fileExtensions) + { + extensions += $"*.{ext}"; + if (ext != fileExtensions[fileExtensions.Length]) + { + extensions += $";"; + } + } + } + else + { + if (filterName.Contains('|')) + { + var filters = filterName.Split('|'); + fileExtensions[0] = filters[1]; + } + extensions = fileExtensions[0]; + if (extensions.StartsWith('.')) + { + if (extensions.Contains(';')) + { + var ext = extensions.Split(';'); + fileExtensions[0] = ext[0]; + } + } + else if (extensions.StartsWith('*')) + { + var modifiedExt = extensions.Trim('*'); + if (modifiedExt.Contains(';')) + { + var ext = modifiedExt.Split(';'); + fileExtensions[0] = ext[0]; + } + else + { + fileExtensions[0] = modifiedExt; + } + } + else + { + if (extensions.Contains(';')) + { + var ext = extensions.Split(';'); + fileExtensions[0] = $".{ext[0]}"; + } + else + { + fileExtensions[0] = extensions; + } + } + } + } + + public static string CreateLoadDialog(string title, object parent = null!) => + new WinFormsUtils().CreateLoadDialog(title, "", "", false, false, parent); + public static string CreateLoadDialog(string extension, string title, string filter, object parent = null!) => + new WinFormsUtils().CreateLoadDialog(title, filter, [extension], true, true, parent); + public static string CreateLoadDialog(Span extensions, string title, string filter, object parent = null!) => + new WinFormsUtils().CreateLoadDialog(title, filter, extensions, true, true, parent); + public override string CreateLoadDialog(string title, string filterName = "", string fileExtension = "", bool isFile = false, bool allowAllFiles = false, object? parent = null) => + CreateLoadDialog(title, filterName, [fileExtension], isFile, allowAllFiles); + public override string CreateLoadDialog(string title, string filterName, Span fileExtensions, bool isFile = false, bool allowAllFiles = false, object? parent = null) + { + if (isFile) + { + Convert(filterName, fileExtensions); + var allFiles = ""; + if (allowAllFiles) allFiles = $"|{Strings.FilterAllFiles}|*.*"; + var d = new OpenFileDialog + { + DefaultExt = fileExtensions[0], + ValidateNames = true, + CheckFileExists = true, + CheckPathExists = true, + Title = title, + Filter = $"{filterName}{allFiles}", + }; + if (d.ShowDialog() == DialogResult.OK) + { + return d.FileName; + } + } + else + { + var d = new FolderBrowserDialog + { + Description = Strings.MenuOpenDSE, + UseDescriptionForTitle = true, + }; + if (d.ShowDialog() == DialogResult.OK) + { + return d.SelectedPath; + } + } + return null!; + } + public static string CreateSaveDialog(string fileName, string extension, string title, string filter, object parent = null!) => + new WinFormsUtils().CreateSaveDialog(fileName, title, filter, [extension], false, false, parent); + public static string CreateSaveDialog(string fileName, Span extensions, string title, string filter, object parent = null!) => + new WinFormsUtils().CreateSaveDialog(fileName, title, filter, extensions, false, false, parent); + public override string CreateSaveDialog(string fileName, string title, string filterName, string fileExtension = "", bool isFile = false, bool allowAllFiles = false, object? parent = null) => + CreateSaveDialog(fileName, title, filterName, [fileExtension], false); + public override string CreateSaveDialog(string fileName, string title, string filterName, Span fileExtensions, bool isFile = false, bool allowAllFiles = false, object? parent = null) + { + Convert(filterName, fileExtensions); + var allFiles = ""; + if (allowAllFiles) allFiles = $"|{Strings.FilterAllFiles}|*.*"; + var d = new SaveFileDialog + { + FileName = fileName, + DefaultExt = fileExtensions[0], + AddExtension = true, + ValidateNames = true, + CheckPathExists = true, + Title = title, + Filter = $"{filterName}{allFiles}", + }; + if (d.ShowDialog() == DialogResult.OK) + { + return d.FileName; + } + return null!; + } +} diff --git a/VG Music Studio - WinForms/ValueTextBox.cs b/(Deprecated) VG Music Studio - WinForms/ValueTextBox.cs similarity index 100% rename from VG Music Studio - WinForms/ValueTextBox.cs rename to (Deprecated) VG Music Studio - WinForms/ValueTextBox.cs diff --git a/.gitignore b/.gitignore index 80921d31..4370a8e6 100644 --- a/.gitignore +++ b/.gitignore @@ -259,4 +259,40 @@ paket-files/ # Python Tools for Visual Studio (PTVS) __pycache__/ -*.pyc \ No newline at end of file +*.pyc +/VG Music Studio - GTK4/share/ +/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamChannel.cs +/VG Music Studio - Core/GBA/AlphaDream/Commands.cs +/VG Music Studio - Core/GBA/AlphaDream/Enums.cs +/VG Music Studio - Core/GBA/AlphaDream/Structs.cs +/VG Music Studio - Core/GBA/AlphaDream/Track.cs +/VG Music Studio - Core/GBA/MP2K/Channel.cs +/VG Music Studio - Core/GBA/MP2K/Commands.cs +/VG Music Studio - Core/GBA/MP2K/Enums.cs +/VG Music Studio - Core/GBA/MP2K/Structs.cs +/VG Music Studio - Core/GBA/MP2K/Track.cs +/VG Music Studio - Core/GBA/MP2K/Utils.cs +/VG Music Studio - Core/NDS/DSE/Channel.cs +/VG Music Studio - Core/NDS/DSE/Commands.cs +/VG Music Studio - Core/NDS/DSE/Enums.cs +/VG Music Studio - Core/NDS/DSE/Track.cs +/VG Music Studio - Core/NDS/DSE/Utils.cs +/VG Music Studio - Core/NDS/SDAT/Channel.cs +/VG Music Studio - Core/NDS/SDAT/Commands.cs +/VG Music Studio - Core/NDS/SDAT/Enums.cs +/VG Music Studio - Core/NDS/SDAT/FileHeader.cs +/VG Music Studio - Core/NDS/SDAT/Track.cs +/VG Music Studio - Core/NDS/Utils.cs +/VG Music Studio - MIDI +/.vscode +/ObjectListView + +# Native libraries copied from VCPKG while building Native NuGet packages +*.a +*.so +*.lib +*.dll +*.dylib +/CreateNugets/**/*.csproj +/CreateNugets/Adwaita/lib +/CreateNugets/PortAudio/lib diff --git a/CreateNugets/Adwaita/README.md b/CreateNugets/Adwaita/README.md new file mode 100644 index 00000000..d017c410 --- /dev/null +++ b/CreateNugets/Adwaita/README.md @@ -0,0 +1,22 @@ +Binary Nuget packages for Adwaita. + +Source code repository for Adwaita: +https://gitlab.gnome.org/GNOME/libadwaita/ + +# Adwaita + +Building blocks for modern GNOME applications. + +## License + +Libadwaita is licensed under the LGPL-2.1+. + +## Documentation + +The documentation can be found online +[here](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/). + +## Getting in Touch + +Matrix room: [#libadwaita:gnome.org](https://matrix.to/#/#libadwaita:gnome.org) + diff --git a/CreateNugets/Build.bat b/CreateNugets/Build.bat new file mode 100644 index 00000000..b0bcca25 --- /dev/null +++ b/CreateNugets/Build.bat @@ -0,0 +1,22 @@ +:: +:: Fetches the paths to the libraries and generates the csproj +:: +:: C:/msys64/usr/bin/bash.exe -lc "ldd /mingw64/bin/libadwaita-1-0.dll | grep '\/mingw.*\.dll' -o" > output.txt +:: +call setup-vcpkg.bat +if exist ./Adwaita/org.adwaita.native.win-x64.csproj ( + del ./Adwaita/org.adwaita.native.win-x64.csproj +) +if exist ./PortAudio/org.portaudio.runtime.win-x64.csproj ( + del ./PortAudio/org.portaudio.runtime.win-x64.csproj +) +python update-libs.py +python generate-csproj.py +pushd Adwaita +dotnet pack -c Release +::del org.adwaita.native.win-x64.csproj +popd +pushd PortAudio +dotnet pack -c Release +::del org.portaudio.runtime.win-x64.csproj +popd diff --git a/CreateNugets/Build.sh b/CreateNugets/Build.sh new file mode 100755 index 00000000..fa069b68 --- /dev/null +++ b/CreateNugets/Build.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# +# Fetches the paths to the libraries and generates the csproj +# +source ./setup-vcpkg.sh +if [[ -z "./Adwaita/org.adwaita.native.linux-x64.csproj" ]]; then + rm ./Adwaita/org.adwaita.native.linux-x64.csproj +fi +python3 update-libs.py +python3 generate-csproj.py +pushd PortAudio +dotnet pack -c Release +#rm org.portaudio.runtime.linux-x64.csproj +popd diff --git a/CreateNugets/PortAudio/README.md b/CreateNugets/PortAudio/README.md new file mode 100755 index 00000000..f86c440f --- /dev/null +++ b/CreateNugets/PortAudio/README.md @@ -0,0 +1,65 @@ +# PortAudio - portable audio I/O library + +PortAudio is a portable audio I/O library designed for cross-platform +support of audio. It uses either a callback mechanism to request audio +processing, or blocking read/write calls to buffer data between the +native audio subsystem and the client. Audio can be processed in various +formats, including 32 bit floating point, and will be converted to the +native format internally. + +## Documentation: + +* Documentation is available at http://www.portaudio.com/docs/ +* Or at `/doc/html/index.html` after running Doxygen. +* Also see `src/common/portaudio.h` for the API spec. +* And see the `examples/` and `test/` directories for many examples of usage. (We suggest `examples/paex_saw.c` for an example.) + +For information on compiling programs with PortAudio, please see the +tutorial at: + + http://portaudio.com/docs/v19-doxydocs/tutorial_start.html + +We have an active mailing list for user and developer discussions. +Please feel free to join. See http://www.portaudio.com for details. + +## Important Files and Folders: + + include/portaudio.h = header file for PortAudio API. Specifies API. + src/common/ = platform independent code, host independent + code for all implementations. + src/os = os specific (but host api neutral) code + src/hostapi = implementations for different host apis + + +### Host API Implementations: + + src/hostapi/alsa = Advanced Linux Sound Architecture (ALSA) + src/hostapi/asihpi = AudioScience HPI + src/hostapi/asio = ASIO for Windows and Macintosh + src/hostapi/audioio = /dev/audio (Solaris/NetBSD Audio) + src/hostapi/coreaudio = Macintosh Core Audio for OS X + src/hostapi/dsound = Windows Direct Sound + src/hostapi/jack = JACK Audio Connection Kit + src/hostapi/oss = Unix Open Sound System (OSS) + src/hostapi/pulseaudio = Sound system for POSIX OSes + src/hostapi/sndio = Small audio and MIDI framework (sndio) + src/hostapi/wasapi = Windows Vista WASAPI + src/hostapi/wdmks = Windows WDM Kernel Streaming + src/hostapi/wmme = Windows MultiMedia Extensions (MME) + + +### Test Programs: + + test/pa_fuzz.c = guitar fuzz box + test/pa_devs.c = print a list of available devices + test/pa_minlat.c = determine minimum latency for your machine + test/paqa_devs.c = self test that opens all devices + test/paqa_errs.c = test error detection and reporting + test/patest_clip.c = hear a sine wave clipped and unclipped + test/patest_dither.c = hear effects of dithering (extremely subtle) + test/patest_pink.c = fun with pink noise + test/patest_record.c = record and playback some audio + test/patest_maxsines.c = how many sine waves can we play? Tests Pa_GetCPULoad(). + test/patest_sine.c = output a sine wave in a simple PA app + test/patest_sync.c = test synchronization of audio and video + test/patest_wire.c = pass input to output, wire simulator \ No newline at end of file diff --git a/CreateNugets/generate-csproj.py b/CreateNugets/generate-csproj.py new file mode 100644 index 00000000..6f5b14b7 --- /dev/null +++ b/CreateNugets/generate-csproj.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python3 + +import os +import sys +import platform +import argparse +import re +import shutil +import subprocess +from pathlib import Path + + +# To assign the correct platform moniker on the platform it's built on +def get_platform_and_architecture(): + os_platform = "" + architecture = "" + + # Operating System + if sys.platform == "win32": + os_platform = "win" + elif sys.platform == "darwin": + os_platform = "osx" + elif sys.platform == "linux": + os_platform = sys.platform + else: + print("The OS type could not be determined.") + exit() + + # CPU Architecture + if (platform.machine() == 'x86_64') or (platform.machine() == 'amd64') or (platform.machine() == 'AMD64'): + architecture = "x64" + elif (platform.machine() == 'arm64') or (platform.machine() == 'ARM64') or (platform.machine() == 'aarch64') or (platform.machine() == 'Aarch64') or (platform.machine() == 'AARCH64'): + architecture = "arm64" + else: + print("The CPU architecture type could not be determined.") + exit() + + return os_platform + "-" + architecture + + +def create_adwaita_csproj(mingw_folder, dotnet_rid, lib_paths, version): + with open("./Adwaita/org.adwaita.native." + dotnet_rid + ".csproj", "w") as f: + csproj_strings = [ + "", "\n", + " ", "\n", + " LGPL-2.1-or-later", "\n", + " README.md", "\n", + " Library", "\n", + " netstandard2.0;netcoreapp3.1;net6.0;net8.0", "\n", + " NU5128", "\n", + " " + dotnet_rid + "", "\n", + " Adwaita.Native", "\n", + " " + version + "", "\n", + "\n", + " https://github.com/PlatinumLucario/VGMusicStudio/tree/new-gui-experimental/CreateNugets", "\n", + " https://gitlab.gnome.org/GNOME/libadwaita/", "\n", + " adwaita libadwaita gtk glib gio native runtime", "\n", + "\n", + " ", "\n", + " ", "\n", + " Building blocks for modern GNOME applications.", "\n", + " Source code repository: https://gitlab.gnome.org/GNOME/libadwaita/", "\n", + " ", "\n", + " false", "\n", + "\n", + " ", "\n", + " Adwaita " + dotnet_rid + " v" + version + "", "\n", + " org.adwaita.native." + dotnet_rid + "", "\n", + "\n", + " ", "\n", + " false", "\n", + " false", "\n", + " false", "\n", + " ", "\n", + "\n", + " ", "\n", + " ", "\n", + " ", "\n", + "\n", + " ", "\n", + " ", "\n" + ] + prev_path = "" # To prevent any duplicate paths, this string variable is made + for path in lib_paths: + if path == prev_path: + continue # That way, if the path is identical to the previous, it'll continue to the next one + # Append the path to the csproj strings + csproj_strings.append( + " " + "\n" + ) + prev_path = path + csproj_strings.append( + " \n" + + " " + "\n" + " " + "\n" + " " + "\n" + " \n" + + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " \n" + + " " + "\n" + " " + "\n" + + " " + "\n" + + " " + "\n" + + " " + "\n" + ) + csproj_strings.append( + " " + "\n" + + " \n" + + " " + "\n" + + " " + "\n" + + " runtimes/" + dotnet_rid + "/native/%(Filename)%(Extension)" + "\n" + + " true" + "\n" + + " PreserveNewest" + "\n" + + " " + "\n" + " " + "\n" + + "\n" + + "" + "\n" + ) + writable_strings = ' '.join(csproj_strings) + f.write(writable_strings) + +def create_portaudio_csproj(dotnet_rid, pa_libs, version): + if dotnet_rid.startswith('win'): + os_platform = "Windows" + elif dotnet_rid.startswith('osx'): + os_platform = "macOS" + elif dotnet_rid.startswith('linux'): + os_platform = "Linux" + if dotnet_rid.endswith('x64'): + cpu = "x64" + elif dotnet_rid.endswith('arm64'): + cpu = "ARM64" + with open("./PortAudio/org.portaudio.runtime." + dotnet_rid + ".csproj", "w") as f: + csproj_strings = [ + "", "\n", + " ", "\n", + " Apache-2.0", "\n", + " README.md", "\n", + " Library", "\n", + " netstandard2.0;netcoreapp3.1;net6.0;net8.0", "\n", + " NU5128", "\n", + " " + dotnet_rid + "", "\n", + " PortAudio.Native", "\n", + " " + version + "", "\n", + "\n", + " https://gitlab.com/PlatinumLucario/Bassoon", "\n", + " https://github.com/PortAudio/portaudio", "\n", + " native library audio music playback cross platform PortAudio port audio", "\n", + "\n", + " ", "\n", + " .NET native " + os_platform + " " + cpu + " wrapper for PortAudio.", "\n", + "\n", + " This Nuget is useful for projects that rely on the native PortAudio library.", "\n", + "\n", + " Also note that https://www.nuget.org/packages/PortAudioSharp uses this Nuget, and is helpful if you need to use PortAudio C# bindings.", "\n", + " ", "\n", + " false", "\n", + "\n", + " ", "\n", + " PortAudio " + dotnet_rid + " v" + version + "", "\n", + " org.portaudio.runtime." + dotnet_rid + "", "\n", + "\n", + " ", "\n", + " false", "\n", + " false", "\n", + " false", "\n", + " ", "\n", + "\n", + " ", "\n", + " ", "\n", + " ", "\n", + "\n", + " ", "\n", + ] + for lib in pa_libs: + csproj_strings.append( + " ' + "\n" + ) + csproj_strings.append( + " " + "\n" + + " " + "\n" + + " " + "\n" + + " " + "\n" + + " " + "\n" + + " runtimes/" + dotnet_rid + "/native/%(Filename)%(Extension)" + "\n" + + " true" + "\n" + + " PreserveNewest" + "\n" + + " " + "\n" + + " " + "\n" + + "" + "\n" + ) + writable_strings = ' '.join(csproj_strings) + f.write(writable_strings) + + +# Windows-specific functions +def windows(dotnet_rid): + lib_paths = [] + mingw_folder = "" + pa_version = "" + adw_version = "" + + # Checks the Environment Variable + if os.getenv("MinGWFolder") == None: + mingw_folder = "C:/msys64/mingw64" + else: + mingw_folder = os.getenv("MinGWFolder") + + # Gets the output from the MSYS2 bash terminal + bash_output_libadw = subprocess.run("C:/msys64/usr/bin/bash.exe -lc '"'ldd /mingw64/bin/libadwaita-1-0.dll | grep ''\\/mingw.*\\.dll'' -o'"'", stdout=subprocess.PIPE, text=True) + bash_output_libadw_ver = subprocess.run("C:/msys64/usr/bin/bash.exe -lc '"'pacman -Qi mingw-w64-x86_64-libadwaita | grep ''\\1.*\\'' -o'"'", stdout=subprocess.PIPE, text=True) + + # Splits the lines, appends them to a new list, then adds the main library to the end of the list + if bash_output_libadw.returncode == 0: + lib_paths = bash_output_libadw.stdout.splitlines() + lib_paths.append("/mingw64/bin/libadwaita-1-0.dll") + adw_version = bash_output_libadw_ver.stdout + else: + print("Error: MSYS2 or libadwaita cannot be found.\nPlease install MSYS2, then run 'pacman -S libadwaita' in MSYS2 to install libadwaita.\n") + exit() + + # Ensure we have access to the VCPKG executable, should be first argument + vcpkg_dir = os.path.abspath(os.environ.get('VCPKG_DIR')) + if not vcpkg_dir: + print('Error, need environment variable VCPKG_DIR to point to directory where `vcpkg` executable is') + sys.exit(1) + elif not os.path.exists(vcpkg_dir): + print('Error, not able to find %s' % vcpkg_dir) + sys.exit(1) + + # Make sure the executable is correctly defined, based on the OS + vcpkg_exe = os.path.join(vcpkg_dir, 'vcpkg.exe') + cmd_output = subprocess.run(vcpkg_exe + " " + "list").stdout.splitlines() + for line in cmd_output: + if line is line.startswith("portaudio"): + split_line = line.split() + for word in split_line: + if word is word.startswith(19): + pa_version = word + + pa_libs = ["portaudio.dll"] + + # Creates the csprojs with the params + create_adwaita_csproj(mingw_folder, dotnet_rid, lib_paths, adw_version) + create_portaudio_csproj(dotnet_rid, pa_libs, pa_version) + +# Linux-specific functions +def linux(dotnet_rid): + pa_version = "" + + # Ensure we have access to the VCPKG executable, should be first argument + vcpkg_dir = os.path.abspath(os.environ.get('VCPKG_DIR')) + if not vcpkg_dir: + print('Error, need environment variable VCPKG_DIR to point to directory where `vcpkg` executable is') + sys.exit(1) + elif not os.path.exists(vcpkg_dir): + print('Error, not able to find %s' % vcpkg_dir) + sys.exit(1) + + # Make sure the executable is correctly defined, based on the OS + vcpkg_exe = os.path.join(vcpkg_dir, 'vcpkg') + cmd_output = subprocess.run([vcpkg_exe, 'list'], stdout=subprocess.PIPE, text=True).stdout.splitlines() + print(cmd_output) + for line in cmd_output: + print(line) + if line.startswith("portaudio"): + print("THIS IS THE LINE WE NEED: " + line) + split_line = line.split() + for word in split_line: + if word.startswith("19"): + pa_version = word.replace("#", ".") + ".1" + print("PortAudio Version: " + pa_version) + + pa_libs = [ + "libportaudio.a", + "libportaudio.so", + "libjack.a", + "libjack.so" + ] + + # Creates the csprojs with the params + create_portaudio_csproj(dotnet_rid, pa_libs, pa_version) + + +# The main function +def main(): + + # Appends platform moniker to variable + dotnet_rid = get_platform_and_architecture() + + if dotnet_rid.startswith("win"): + windows(dotnet_rid) + elif dotnet_rid.startswith("linux"): + linux(dotnet_rid) + + +if __name__ == "__main__": + main() diff --git a/CreateNugets/setup-vcpkg.bat b/CreateNugets/setup-vcpkg.bat new file mode 100644 index 00000000..7ec2417c --- /dev/null +++ b/CreateNugets/setup-vcpkg.bat @@ -0,0 +1,16 @@ +:: This will set up the VCPKG environment into +:: C:\src\vcpkg on Windows +:: +:: It's very useful if you want to save time setting up the dependencies +if not exist %VCPKG% ( + if not exist C:\src\ ( + mkdir C:\src + pushd C:\src + if not exist .\vcpkg\ ( + git clone https://github.com/microsoft/vcpkg.git + call .\vcpkg\bootstrap-vcpkg.bat + ) + popd + ) + set VCPKG_DIR=C:\src\vcpkg +) diff --git a/CreateNugets/setup-vcpkg.command b/CreateNugets/setup-vcpkg.command new file mode 100644 index 00000000..ee142c4d --- /dev/null +++ b/CreateNugets/setup-vcpkg.command @@ -0,0 +1,16 @@ +#!/usr/bin/env zsh +# +# This will set up the VCPKG environment into +# '/User/$USER/source/vcpkg' on macOS +# +# It's very useful if you want to save time setting up the dependencies +if [[ -z "$VCPKG_DIR" ]]; then + mkdir ~/source + pushd ~/source + if [[ -z "./vcpkg" ]]; then + git clone https://github.com/microsoft/vcpkg.git + ./vcpkg/bootstrap-vcpkg.sh + fi + popd + export VCPKG_DIR=~/source/vcpkg +fi \ No newline at end of file diff --git a/CreateNugets/setup-vcpkg.sh b/CreateNugets/setup-vcpkg.sh new file mode 100755 index 00000000..e77c1fe1 --- /dev/null +++ b/CreateNugets/setup-vcpkg.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# +# This will set up the VCPKG environment into +# '/home/$USER/source/vcpkg' on Linux +# +# It's very useful if you want to save time setting up the dependencies +if [[ -z "$VCPKG_DIR" ]]; then + echo "VCPKG directory not found. Creating folder 'source'..." + mkdir ~/source + echo "Done." + pushd ~/source + if [[ ! -d "./vcpkg" ]]; then + echo "Attempting to clone VCPKG repository..." + git clone https://github.com/microsoft/vcpkg.git + pushd ./vcpkg + source ./bootstrap-vcpkg.sh + popd + echo "Done." + fi + popd + export VCPKG_DIR=~/source/vcpkg + echo "Set VCPKG_DIR variable to $VCPKG_DIR" +fi diff --git a/CreateNugets/update-libs.py b/CreateNugets/update-libs.py new file mode 100644 index 00000000..4029cc6b --- /dev/null +++ b/CreateNugets/update-libs.py @@ -0,0 +1,289 @@ +#!/usr/bin/python3 + +import os, sys, platform +import pathlib +import subprocess +import shutil + +from pathlib import Path + +# Some OS flags +is_windows = (sys.platform == 'win32') or (sys.platform == 'msys') +is_linux = sys.platform == 'linux' +is_macos = sys.platform == 'darwin' + +deps = [ + 'portaudio', +] +vcpkg_exe = '' +libs_portaudio = [] + +# To assign the correct platform moniker on the platform it's built on +def get_platform_and_architecture(): + os_platform = "" + architecture = "" + + # Operating System + if sys.platform == "win32": + os_platform = "win" + elif sys.platform == "darwin": + os_platform = "osx" + elif sys.platform == "linux": + os_platform = sys.platform + else: + print("The OS type could not be determined.") + exit() + + # CPU Architecture + if (platform.machine() == 'x86_64') or (platform.machine() == 'amd64') or (platform.machine() == 'AMD64'): + architecture = "x64" + elif (platform.machine() == 'arm64') or (platform.machine() == 'ARM64') or (platform.machine() == 'aarch64') or (platform.machine() == 'Aarch64') or (platform.machine() == 'AARCH64'): + architecture = "arm64" + else: + print("The CPU architecture type could not be determined.") + exit() + + return os_platform + "-" + architecture + +# Get platform and architecture variable, and path to copy to +dotnet_rid = get_platform_and_architecture() +lib_path = './PortAudio/lib/' + dotnet_rid + +# Ensure we have access to the VCPKG executable, should be first argument +vcpkg_dir = os.path.abspath(os.environ.get('VCPKG_DIR')) +if not vcpkg_dir: + print('Error, need environment variable VCPKG_DIR to point to directory where `vcpkg` executable is') + sys.exit(1) +elif not os.path.exists(vcpkg_dir): + print('Error, not able to find %s' % vcpkg_dir) + sys.exit(1) + +# Make sure the executable is correctly defined, based on the OS +if is_windows: + vcpkg_exe = os.path.join(vcpkg_dir, 'vcpkg.exe') +else: + vcpkg_exe = os.path.join(vcpkg_dir, 'vcpkg') + +# Pull the latest commits from VCPKG repo, then upgrade all libraries +proc = subprocess.Popen(['git', 'pull'], cwd=vcpkg_dir) +proc.wait() +proc = subprocess.Popen([vcpkg_exe, 'upgrade', '--no-dry-run']) +proc.wait() + +def BuildWindowsDeps(deps): + # Build the x86-64 version of the Windows dependencies + deps = ['%s:x64-windows' % x for x in deps] + lib_src_dir = 'installed/x64-windows/bin/' + libs_portaudio = [ + 'portaudio.dll', + ] + + # Upgrade MinGW deps + subprocess.run("C:/msys64/usr/bin/bash.exe -lc '"'pacman-key --refresh-keys'"'", stdout=subprocess.PIPE, text=True) + subprocess.run("C:/msys64/usr/bin/bash.exe -lc '"'pacman -Syuu --noconfirm'"'", stdout=subprocess.PIPE, text=True) + subprocess.run("C:/msys64/usr/bin/bash.exe -lc '"'pacman -Syuu --noconfirm mingw-w64-x86_64-libadwaita'"'", stdout=subprocess.PIPE, text=True) + + # First make sure the lib directory is there + os.makedirs(pathlib.Path(lib_path), exist_ok=True) + + # Install the deps + proc = subprocess.Popen([vcpkg_exe, 'install', *deps, '--overlay-triplets=dynamic-triplets']) + proc.wait() + print("All x86-64 Windows libraries built successfully!") + + # Now get the dlls that we really want + for lib in libs_portaudio: + src = os.path.join(vcpkg_dir, lib_src_dir, lib) + shutil.copy(src, lib_path) + print("All x86-64 Windows libraries copied successfully!") + + # Clear deps list and re-add needed deps + # Required to prevent the following error: + # :1:23: error: expected eof + # on expression: portaudio:x64-windows:arm64-windows + # ^ + deps.clear() + deps = [ + 'portaudio', + ] + + # Build the ARM64 version of the Windows dependencies + deps = ['%s:arm64-windows' % x for x in deps] + lib_src_dir = 'installed/arm64-windows/bin/' + libs_portaudio = [ + 'portaudio.dll', + ] + + # First make sure the lib directory is there + os.makedirs(pathlib.Path(lib_path), exist_ok=True) + + # Install the deps + proc = subprocess.Popen([vcpkg_exe, 'install', *deps, '--overlay-triplets=dynamic-triplets']) + proc.wait() + print("All ARM64 Windows libraries built successfully!") + + # Now get the dlls that we really want + for lib in libs_portaudio: + src = os.path.join(vcpkg_dir, lib_src_dir, lib) + shutil.copy(src, lib_path) + print("All ARM64 Windows libraries copied successfully!") + + # Clear deps list and re-add needed deps + # Required to prevent the following error: + # :1:23: error: expected eof + # on expression: portaudio:arm64-windows:x64-linux + # ^ + deps.clear() + deps = [ + 'portaudio', + ] + +def BuildLinuxDeps(deps): + # Build the x86-64 version of the Linux dependencies + lib_src_dir_static = 'installed/x64-linux/lib/' + lib_src_dir_dynamic = 'installed/x64-linux-dynamic/lib/' + libs_portaudio = [ + 'libportaudio.a', + 'libportaudio.so' + ] + libs_jack = [ + 'libjack.a', + 'libjack.so' + ] + + # First make sure the lib directory is there + os.makedirs(pathlib.Path(lib_path), exist_ok=True) + + # Install the deps + for dep in deps: + proc = subprocess.Popen([vcpkg_exe, 'install', dep, '--overlay-triplets=dynamic-triplets']) + proc.wait() + proc = subprocess.Popen([vcpkg_exe, 'install', dep + ":x64-linux-dynamic", '--overlay-triplets=dynamic-triplets']) + proc.wait() + print("All x86-64 Linux libraries built successfully!") + + # Now get the dlls that we really want + for lib in deps: + src_portaudio_static = os.path.join(vcpkg_dir, lib_src_dir_static, libs_portaudio[0]) + src_portaudio_dynamic = os.path.join(vcpkg_dir, lib_src_dir_dynamic, libs_portaudio[1]) + src_jack_static = os.path.join(vcpkg_dir, lib_src_dir_static, libs_jack[0]) + src_jack_dynamic = os.path.join(vcpkg_dir, lib_src_dir_dynamic, libs_jack[1]) + try: + shutil.copy(src_portaudio_static, lib_path) + shutil.copy(src_portaudio_dynamic, lib_path) + shutil.copy(src_jack_static, lib_path) + shutil.copy(src_jack_dynamic, lib_path) + except: + print("Unable to copy library: File not found.") + print("All x86-64 Linux libraries copied successfully!") + + # Clear deps list and re-add needed deps + # Required to prevent the following error: + # :1:23: error: expected eof + # on expression: portaudio:x64-linux:arm64-linux + # ^ + deps.clear() + deps = [ + 'portaudio:arm64-linux', + ] + + # Build the ARM64 version of the Linux dependencies + # Note: VCPKG doesn't support building dynamic libs yet, + # so we have to use the static ones instead. + lib_src_dir_static = 'installed/arm64-linux/lib/' + libs_portaudio = [ + 'libportaudio.a', + ] + + # First make sure the lib directory is there + os.makedirs(pathlib.Path(lib_path), exist_ok=True) + + # Install the deps + proc = subprocess.Popen([vcpkg_exe, 'install', *deps, '--overlay-triplets=dynamic-triplets']) + proc.wait() + print("All ARM64 Linux libraries built successfully!") + + # Now get the dlls that we really want + for lib in libs_portaudio: + src_portaudio_static = os.path.join(vcpkg_dir, lib_src_dir_static, lib) + try: + shutil.copy(src_portaudio_static, lib_path) + except: + print("Unable to copy library: File not found.") + print("All ARM64 Linux libraries copied successfully!") + + # Clear deps list and re-add needed deps + # Required to prevent the following error: + # :1:23: error: expected eof + # on expression: portaudio:arm64-linux:x64-osx-dynamic + # ^ + deps.clear() + deps = [ + 'portaudio', + ] + +def BuildMacOSDeps(deps): + # Build the x86-64 version of the macOS dependencies + deps[0] = "portaudio:x64-osx-dynamic" + + lib_src_dir = 'installed/x64-osx-dynamic/lib/' + libs_portaudio = [ + 'libportaudio.dylib', + ] + + # First make sure the lib directory is there + os.makedirs(pathlib.Path(lib_path), exist_ok=True) + + # Install the deps + proc = subprocess.Popen([vcpkg_exe, 'install', *deps, '--overlay-triplets=dynamic-triplets']) + proc.wait() + print("All x86-64 macOS libraries built successfully!") + + # Now get the dlls that we really want + for lib in libs_portaudio: + src = os.path.join(vcpkg_dir, lib_src_dir, lib) + shutil.copy(src, lib_path) + print("All x86-64 macOS libraries copied successfully!") + + # Clear deps list and re-add needed deps + # Required to prevent the following error: + # :1:23: error: expected eof + # on expression: portaudio:x64-os:arm64-osx-dynamic + # ^ + deps.clear() + deps = [ + 'portaudio:arm64-osx-dynamic', + ] + + # Build the ARM64 version of the macOS dependencies + lib_src_dir = 'installed/arm64-osx-dynamic/lib/' + libs_portaudio = [ + 'libportaudio.dylib', + ] + + # First make sure the lib directory is there + os.makedirs(pathlib.Path(lib_path), exist_ok=True) + + # Install the deps + proc = subprocess.Popen([vcpkg_exe, 'install', *deps, '--overlay-triplets=dynamic-triplets']) + proc.wait() + print("All ARM64 macOS libraries built successfully!") + + # Now get the dlls that we really want + for lib in libs_portaudio: + src = os.path.join(vcpkg_dir, lib_src_dir, lib) + shutil.copy(src, lib_path) + print("All ARM64 macOS libraries copied successfully!") + +def Main(): + # Currently, I don't know how to compile libraries of every OS within one OS + if is_windows: + BuildWindowsDeps(deps) + elif is_linux: + BuildLinuxDeps(deps) + elif is_macos: + BuildMacOSDeps(deps) + print("All neccessary tasks completed!") + +# Runs the main function +Main() diff --git a/VG Music Studio - WinForms/Properties/Icon.ico b/Icons/Icon.ico similarity index 100% rename from VG Music Studio - WinForms/Properties/Icon.ico rename to Icons/Icon.ico diff --git a/VG Music Studio - WinForms/Properties/Icon16.png b/Icons/Icon16.png similarity index 100% rename from VG Music Studio - WinForms/Properties/Icon16.png rename to Icons/Icon16.png diff --git a/VG Music Studio - WinForms/Properties/Icon24.png b/Icons/Icon24.png similarity index 100% rename from VG Music Studio - WinForms/Properties/Icon24.png rename to Icons/Icon24.png diff --git a/VG Music Studio - WinForms/Properties/Icon32.png b/Icons/Icon32.png similarity index 100% rename from VG Music Studio - WinForms/Properties/Icon32.png rename to Icons/Icon32.png diff --git a/VG Music Studio - WinForms/Properties/Icon48.png b/Icons/Icon48.png similarity index 100% rename from VG Music Studio - WinForms/Properties/Icon48.png rename to Icons/Icon48.png diff --git a/VG Music Studio - WinForms/Properties/Icon528.png b/Icons/Icon528.png similarity index 100% rename from VG Music Studio - WinForms/Properties/Icon528.png rename to Icons/Icon528.png diff --git a/VG Music Studio - WinForms/Properties/Next.ico b/Icons/Next.ico similarity index 100% rename from VG Music Studio - WinForms/Properties/Next.ico rename to Icons/Next.ico diff --git a/VG Music Studio - WinForms/Properties/Next.png b/Icons/Next.png similarity index 100% rename from VG Music Studio - WinForms/Properties/Next.png rename to Icons/Next.png diff --git a/VG Music Studio - WinForms/Properties/Pause.ico b/Icons/Pause.ico similarity index 100% rename from VG Music Studio - WinForms/Properties/Pause.ico rename to Icons/Pause.ico diff --git a/VG Music Studio - WinForms/Properties/Pause.png b/Icons/Pause.png similarity index 100% rename from VG Music Studio - WinForms/Properties/Pause.png rename to Icons/Pause.png diff --git a/VG Music Studio - WinForms/Properties/Play.ico b/Icons/Play.ico similarity index 100% rename from VG Music Studio - WinForms/Properties/Play.ico rename to Icons/Play.ico diff --git a/VG Music Studio - WinForms/Properties/Play.png b/Icons/Play.png similarity index 100% rename from VG Music Studio - WinForms/Properties/Play.png rename to Icons/Play.png diff --git a/VG Music Studio - WinForms/Properties/Playlist.png b/Icons/Playlist.png similarity index 100% rename from VG Music Studio - WinForms/Properties/Playlist.png rename to Icons/Playlist.png diff --git a/VG Music Studio - WinForms/Properties/Previous.ico b/Icons/Previous.ico similarity index 100% rename from VG Music Studio - WinForms/Properties/Previous.ico rename to Icons/Previous.ico diff --git a/VG Music Studio - WinForms/Properties/Previous.png b/Icons/Previous.png similarity index 100% rename from VG Music Studio - WinForms/Properties/Previous.png rename to Icons/Previous.png diff --git a/VG Music Studio - WinForms/Properties/Song.png b/Icons/Song.png similarity index 100% rename from VG Music Studio - WinForms/Properties/Song.png rename to Icons/Song.png diff --git a/Icons/vgms-play-playlist-symbolic.svg b/Icons/vgms-play-playlist-symbolic.svg new file mode 100644 index 00000000..3aff2eae --- /dev/null +++ b/Icons/vgms-play-playlist-symbolic.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Icons/vgms-playlist-symbolic.svg b/Icons/vgms-playlist-symbolic.svg new file mode 100644 index 00000000..7c4ebbe1 --- /dev/null +++ b/Icons/vgms-playlist-symbolic.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Icons/vgms-song-symbolic.svg b/Icons/vgms-song-symbolic.svg new file mode 100644 index 00000000..26bcef3a --- /dev/null +++ b/Icons/vgms-song-symbolic.svg @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/README.md b/README.md index 83a0af92..29515f43 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Releases](https://img.shields.io/github/downloads/Kermalis/VGMusicStudio/total.svg)](https://github.com/Kermalis/VGMusicStudio/releases/latest) [![License](https://img.shields.io/badge/License-LGPLv3-blue.svg)](LICENSE.md) -VG Music Studio is a music player and visualizer for the most common GBA music format (MP2K), AlphaDream's GBA music format, the most common NDS music format (SDAT), and a more rare NDS/WII music format (DSE) [found in PMD2 among others]. +VG Music Studio is a cross-platform music player and visualizer for the most common GBA music format (MP2K), AlphaDream's GBA music format, the most common NDS music format (SDAT), and a less common PS1/PS2/NDS/Wii music format (DSE) [found in PMD2 among others] (developed by Hidenori Suzuki and used internally at Square as 'Square Digital Sound Elements' and then licensed as 'Procyon Studio Digital Sound Elements' after moving to Procyon Studio). [![VG Music Studio Preview](https://i.imgur.com/hWJGG83.png)](https://www.youtube.com/watch?v=s1BZ7cRbtBU "VG Music Studio Preview") @@ -18,8 +18,6 @@ If you want to talk or would like a game added to our configs, join our [Discord * MIDI saving - UI with saving options, such as remapping * MIDI saving - Make errors more clear * Voice table viewer - Tooltips which provide a huge chunk of information -* Detachable piano -* Tempo numerical (it fits) * Help dialog that explains the commands and config for each engine ### AlphaDream Engine @@ -33,8 +31,8 @@ If you want to talk or would like a game added to our configs, join our [Discord * ADSR * Pitch bend * LFO -* Ability to load SMDB and SWDB (Big Endian as opposed to SMDL and SWDL for Little Endian) -* Some more unknown commands +* Support for SMDS and WDS (for the earlier PlayStation version, used in Xenogears). The chunks in this version have no labels in them. +* Support for SMDM and SWDM (for the earlier PlayStation 2 version, used in Xenosaga Episode I). The chunks in this version have no labels in them. ### MP2K Engine * Add Golden Sun 2 reverb effect @@ -50,13 +48,112 @@ If you want to talk or would like a game added to our configs, join our [Discord ### SDAT Engine * Find proper formulas for LFO +---- +## Building +### Windows +VG Music Studio already includes the native library Nuget package for libadwaita. So that it can build and run without any issues and without needing to install MSYS2. + +But if for any reason you need to update the libadwaita Nuget, follow the steps below: +1. Download and install MSYS2 from [the official website](https://www.msys2.org/), and ensure it is installed in the default directory (``C:\``). If you install it in a different location, it will need to be set via the ``Path`` environment variable. +2. After installation, run the following commands in the MSYS2 terminal: ``pacman -Syuu`` to reload the database and update all the packages. +** If any errors occur saying that the signatures are of `unknown trust` during the update process and can't update, run ``pacman-key --refresh-keys`` to refresh the keys + +If libawaita hasn't been installed in MSYS2: +2.1 (Optional) Run each of the following commands to install the required packages: +``pacman -S mingw-w64-x86_64-gtk4`` +``pacman -S mingw-w64-x86_64-libadwaita`` +``pacman -S mingw-w64-x86_64-gtksourceview5`` + +3. Now run ``Build.bat`` inside the CreateNugets folder, it should update the .csproj for the Nuget and build the Nuget. + +### macOS +#### Intel (x86-64) +Even though it will build without any issues, since VG Music Studio runs on GTK4 bindings via Gir.Core, it requires some C libraries to be installed or placed within the same directory as the macOS executable. + +Otherwise it will complain upon launch with the following System.TypeInitializationException error: +``DllNotFoundException: Unable to load DLL 'libgtk-4-1.dylib' or one of its dependencies: The specified module could not be found. (0x8007007E)`` + +To avoid this error while debugging VG Music Studio, you will need to do the following: +1. Download and install [Homebrew](https://brew.sh/) with the following macOS terminal command: +``/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`` +This will ensure Homebrew is installed in the default directory, which is ``/usr/local``. +2. After installation, run the following command from the macOS terminal to update all packages: ``brew update`` +3. Run each of the following commands to install the required packages: +``brew install gtk4`` +``brew install libadwaita`` +``brew install gtksourceview5`` + +#### Apple Silicon (AArch64) +Currently unknown if this will work on Apple Silicon, since it's a completely different CPU architecture, it may need some ARM-specific APIs to build or function correctly. + +If you have figured out a way to get it to run under Apple Silicon, please let us know! + +### Linux +Most Linux distributions should be able to build this without anything extra to download and install. + +However, if you get the following System.TypeInitializationException error upon launching VG Music Studio during debugging: +``DllNotFoundException: Unable to load DLL 'libgtk-4-1.so.0' or one of its dependencies: The specified module could not be found. (0x8007007E)`` +Then it means that either ``gtk4``, ``libadwaita`` or ``gtksourceview5`` is missing from your current installation of your Linux distribution. Often occurs if a non-GTK based desktop environment is installed by default, or the Linux distribution has been installed without a GUI. + +To install them, run the following commands: +#### Debian (or Debian based distributions, such as Ubuntu, elementary OS, Pop!_OS, Zorin OS, Kali Linux etc.) +First, update the current packages with ``sudo apt update && sudo apt upgrade`` and install any updates, then run: +``sudo apt install libgtk-4-1`` +``sudo apt install libadwaita-1`` +``sudo apt install libgtksourceview-5`` + +##### Vanilla OS (Debian based distribution) +Debian based distribution, Vanilla OS, uses the Distrobox based package management system called 'apx' instead of apt (apx as in 'apex', not to be confused with Microsoft Windows's UWP appx packages). +But it is still a Debian based distribution, nonetheless. And fortunately, it comes pre-installed with GNOME, which means you don't need to install any libraries! + +You will, however, still need to install the .NET SDK and .NET Runtime using apx, and cannot be used with 'sudo'. + +Instead, run any commands to install packages like this: +``apx install [package-name]`` + +#### Arch Linux (or Arch Linux based distributions, such as Manjaro, Garuda Linux, EndeavourOS, SteamOS etc.) +First, update the current packages with ``sudo pacman -Syy && sudo pacman -Syuu`` and install any updates, then run: +``sudo pacman -S gtk4`` +``sudo pacman -S libadwaita`` +``sudo pacman -S gtksourceview5`` + +#### Fedora (or other Red Hat based distributions, such as Red Hat Enterprise Linux, AlmaLinux, Rocky Linux etc.) +First, update the current packages with ``sudo dnf check-update && sudo dnf update`` and install any updates, then run: +``sudo dnf install gtk4`` +``sudo dnf install libadwaita`` +``sudo dnf install gtksourceview5`` + +#### openSUSE (or other SUSE Linux based distributions, such as SUSE Linux Enterprise, GeckoLinux etc.) +First, update the current packages with ``sudo zypper up`` and install any updates, then run: +``sudo zypper in libgtk-4-1`` +``sudo zypper in libadwaita-1-0`` +``sudo zypper in libgtksourceview-5-0`` + +#### Alpine Linux (or Alpine Linux based distributions, such as postmarketOS etc.) +First, update the current packages with ``apk -U upgrade`` to their latest versions, then run: +``apk add gtk4.0`` +``apk add libadwaita`` +``apk add gtksourceview5`` + +Please note that VG Music Studio may not be able to build on other CPU architectures (such as AArch64, ppc64le, s390x etc.), since it hasn't been developed to support those architectures yet. Same thing applies for postmarketOS. + +#### Void Linux +First, update the current packages with ``sudo xbps-install -Su`` to their latest versions, then run: +``sudo xbps-install gtk4`` +``sudo xbps-install libadwaita`` +``sudo xbps-install gtksourceview5`` + +### FreeBSD +Currently, .NET has not been ported to FreeBSD or similar operating systems. As a result, it cannot run natively under FreeBSD and may require a Linux to FreeBSD compatibility layer to run it. + ---- ## Special Thanks To: ### General * Stich991 - Italian translation * tuku473 - Design suggestions, colors, Spanish translation -* Lachesis - French translation -* Delusional Moonlight - Russian translation +* J. Ritchie Carroll (from Grid Protection Alliance) - Int24 and UInt24 classes and functions +* Benjamin Summerton (define-private-public) - PortAudio bindings for C# +* LSXPrime - Developing [the SoundFlow library](https://github.com/LSXPrime/SoundFlow) and helping out with using QueueDataProvider correctly ### AlphaDream Engine * irdkwia - Finding games that used the engine @@ -68,7 +165,7 @@ If you want to talk or would like a game added to our configs, join our [Discord ### MP2K Engine * Bregalad - Extensive documentation -* Ipatix - Engine research, help, [(and his MP2K music player)](https://github.com/ipatix/agbplay) from which some of my code is based on +* Ipatix - Engine research, help, [(his MP2K music player)](https://github.com/ipatix/agbplay) from which some of my code is based on, and the [LowLatencyRingbuffer](https://github.com/ipatix/agbplay/blob/agbplay_v2/src/agbplay/LowLatencyRingbuffer.cpp) which helps PortAudio to process audio with low latency * mimi - Told me about a hidden feature of the engine * SomeShrug - Engine research and helped me understand more about the engine parameters @@ -77,12 +174,20 @@ If you want to talk or would like a game added to our configs, join our [Discord ---- ## VG Music Studio Uses: -* [DLS2](https://github.com/Kermalis/DLS2) +### Core * [EndianBinaryIO](https://github.com/Kermalis/EndianBinaryIO) -* [NAudio](https://github.com/naudio/NAudio) -* [ObjectListView](http://objectlistview.sourceforge.net) -* [My fork of Sanford.Multimedia.Midi](https://github.com/Kermalis/Sanford.Multimedia.Midi) +* [KMIDI](https://github.com/Kermalis/KMIDI) +* [DLS2](https://github.com/Kermalis/DLS2) * [SoundFont2](https://github.com/Kermalis/SoundFont2) +* [PortAudio bindings](https://github.com/PlatinumLucario/VGMusicStudio/tree/new-gui-experimental/VG%20Music%20Studio%20-%20Core/PortAudio) [from Benjamin Summerton's Bassoon Project](https://gitlab.com/define-private-public/Bassoon) * [YamlDotNet](https://github.com/aaubry/YamlDotNet/wiki) +### New GUI (Cross platform): +* [GTK4](https://gtk.org) +* [Adwaita](https://gitlab.gnome.org/GNOME/libadwaita) +* [Gir.Core](https://github.com/gircore/gir.core) + +### Old Legacy GUI (Windows only) +* [NAudio](https://github.com/naudio/NAudio) +* [ObjectListView](http://objectlistview.sourceforge.net) [Discord]: https://discord.gg/mBQXCTs \ No newline at end of file diff --git a/VG Music Studio - Core/AlphaDream.yaml b/VG Music Studio - Core/AlphaDream.yaml index c6709d4d..478c7d98 100644 --- a/VG Music Studio - Core/AlphaDream.yaml +++ b/VG Music Studio - Core/AlphaDream.yaml @@ -8,59 +8,61 @@ A88E_00: SampleTableSize: 236 Remap: "MLSS" Playlists: - Music: + Mario & Luigi':' Superstar Saga + Bowser's Minions: + 41: "A New Adventure Begins" + 30: "We're Off Again!" + 32: "Touch of Evil" + 28: "Prince Peasley's Theme" + 33: "Fawful Music" + 34: "Bowser's Road" + 29: "Cackletta, the Fiercest Foe" + 39: "A Journey Full of Laughs" + 40: "Going Home" + 25: "Mario is Everyone's Star" + 23: "Peach's Castle" + 8: "Stardust Fields Area 64" + 9: "Hoohoo Mountaintop" + 12: "The Kingdom Called Beanbean" + 31: "Beanish People" + 15: "Castle of Beans" + 13: "Chucklehuck Woods" + 35: "Danger Abounds!" + 17: "Woohoo Hooniversity" + 20: "Another Sky for Toads" + 14: "Sea... Sea... Sea..." + 18: "Don't Dwell on Danger" + 21: "Sweet Surfin'" + 19: "Hold the Corny Jokes, Please!" + 24: "Decisive Battleground" + 22: "Climbing" + 10: "Let's Go!" + 16: "We Can't Lose!" + 36: "The Marvelous Duo" + 37: "Fawful and Cackletta" + 38: "Time to Settle This!" + 44: "Showdown with Cackletta!" + 11: "Win & Dance" + 4: "Jump! (Ground Theme)" + 3: "To Challenge!" + 5: "It's My Turn" + Other Music: + 27: "Prince Peasley's Theme (Brief Arrangement)" + 42: "Chucklehuck Woods (Light Arrangement)" + 50: "Win & Dance (No Intro Arrangement)" + 26: "Got an Item!" + 48: "Professor E. Gadd's Theme" + 49: "Ghostly Encounter" + Unused: 1: "1" 2: "2" - 3: "Mini Game" - 4: "Border Jump" - 5: "Star 'Stache Smash" 6: "6" 7: "7" - 8: "Stardust Fields" - 9: "Hoohoo Mountain" - 10: "Battle" - 11: "Victory" - 12: "Beanbean Fields" - 13: "Chucklehuck Woods" - 14: "Seabed" - 15: "Beanbean Castle" - 16: "Boss Battle" - 17: "Woohoo Hooniversity" - 18: "Teehee Valley" - 19: "Joke's End" - 20: "Little Fungitown" - 21: "Gwarhar Lagoon" - 22: "Underground" - 23: "Toad Town Square" - 24: "Bowser's Castle" - 25: "Warp Pipe" - 26: "Special Item" - 27: "Royal Welcome" - 28: "Prince Peasley's Theme" - 29: "Cackletta's Theme" - 30: "File Select" - 31: "Hoohoo Village" - 32: "Devastation" - 33: "Panic!" - 34: "Koopa Cruiser" - 35: "Danger!" - 36: "Popple Battle" - 37: "Cackletta Battle" - 38: "Bowletta Battle" - 39: "Ending" - 40: "Credits" - 41: "Title" - 42: "Chateau de Chucklehuck" 43: "43" - 44: "Final Cackletta Battle" 45: "45" 46: "46" 47: "47" - 48: "Professor E Gadd" - 49: "Ghostly Encounter" - 50: "Bean Time!" A88J_00: - Name: "Mario & Luigi - Superstar Saga (Japan)" + Name: "Mario & Luigi RPG (Japan)" SongTableOffsets: 0x205060 SongTableSizes: 418 VoiceTableOffset: 0x2056E8 diff --git a/VG Music Studio - Core/Assembler.cs b/VG Music Studio - Core/Assembler.cs index f8220184..79747544 100644 --- a/VG Music Studio - Core/Assembler.cs +++ b/VG Music Studio - Core/Assembler.cs @@ -7,7 +7,7 @@ namespace Kermalis.VGMusicStudio.Core; -internal sealed class Assembler : IDisposable +public sealed class Assembler : IDisposable { private sealed class Pair // Must be a class { diff --git a/VG Music Studio - Core/Codec/CodecEnums.cs b/VG Music Studio - Core/Codec/CodecEnums.cs new file mode 100644 index 00000000..1e916a80 --- /dev/null +++ b/VG Music Studio - Core/Codec/CodecEnums.cs @@ -0,0 +1,180 @@ +namespace Kermalis.VGMusicStudio.Core.Codec; + +/* This code has been copied directly from vgmstream.h in VGMStream's repository * + * and modified into C# code to work with VGMS. Link to its repository can be * + * found here: https://github.com/vgmstream/vgmstream */ +public enum CodecType +{ + codec_SILENCE, /* generates silence */ + + /* PCM */ + codec_PCM16LE, /* little endian 16-bit PCM */ + codec_PCM16BE, /* big endian 16-bit PCM */ + codec_PCM16_int, /* 16-bit PCM with sample-level interleave (for blocks) */ + + codec_PCM8, /* 8-bit PCM */ + codec_PCM8_int, /* 8-bit PCM with sample-level interleave (for blocks) */ + codec_PCM8_U, /* 8-bit PCM, unsigned (0x80 = 0) */ + codec_PCM8_U_int, /* 8-bit PCM, unsigned (0x80 = 0) with sample-level interleave (for blocks) */ + codec_PCM8_SB, /* 8-bit PCM, sign bit (others are 2's complement) */ + codec_PCM4, /* 4-bit PCM, signed */ + codec_PCM4_U, /* 4-bit PCM, unsigned */ + + codec_ULAW, /* 8-bit u-Law (non-linear PCM) */ + codec_ULAW_int, /* 8-bit u-Law (non-linear PCM) with sample-level interleave (for blocks) */ + codec_ALAW, /* 8-bit a-Law (non-linear PCM) */ + + codec_PCMFLOAT, /* 32-bit float PCM */ + codec_PCM24LE, /* little endian 24-bit PCM */ + codec_PCM24BE, /* big endian 24-bit PCM */ + + /* ADPCM */ + codec_CRI_ADX, /* CRI ADX */ + codec_CRI_ADX_fixed, /* CRI ADX, encoding type 2 with fixed coefficients */ + codec_CRI_ADX_exp, /* CRI ADX, encoding type 4 with exponential scale */ + codec_CRI_ADX_enc_8, /* CRI ADX, type 8 encryption (God Hand) */ + codec_CRI_ADX_enc_9, /* CRI ADX, type 9 encryption (PSO2) */ + + codec_NGC_DSP, /* Nintendo DSP ADPCM */ + codec_NGC_DSP_subint, /* Nintendo DSP ADPCM with frame subinterframe */ + codec_NGC_DTK, /* Nintendo DTK ADPCM (hardware disc), also called TRK or ADP */ + codec_NGC_AFC, /* Nintendo AFC ADPCM */ + codec_VADPCM, /* Silicon Graphics VADPCM */ + + codec_G721, /* CCITT G.721 */ + + codec_XA, /* CD-ROM XA 4-bit */ + codec_XA8, /* CD-ROM XA 8-bit */ + codec_XA_EA, /* EA's Saturn XA (not to be confused with EA-XA) */ + codec_PSX, /* Sony PS ADPCM (VAG) */ + codec_PSX_badflags, /* Sony PS ADPCM with custom flag byte */ + codec_PSX_cfg, /* Sony PS ADPCM with configurable frame size (int math) */ + codec_PSX_pivotal, /* Sony PS ADPCM with configurable frame size (float math) */ + codec_HEVAG, /* Sony PSVita ADPCM */ + + codec_EA_XA, /* Electronic Arts EA-XA ADPCM v1 (stereo) aka "EA ADPCM" */ + codec_EA_XA_int, /* Electronic Arts EA-XA ADPCM v1 (mono/interleave) */ + codec_EA_XA_V2, /* Electronic Arts EA-XA ADPCM v2 */ + codec_MAXIS_XA, /* Maxis EA-XA ADPCM */ + codec_EA_XAS_V0, /* Electronic Arts EA-XAS ADPCM v0 */ + codec_EA_XAS_V1, /* Electronic Arts EA-XAS ADPCM v1 */ + + codec_IMA, /* IMA ADPCM (stereo or mono, low nibble first) */ + codec_IMA_int, /* IMA ADPCM (mono/interleave, low nibble first) */ + codec_DVI_IMA, /* DVI IMA ADPCM (stereo or mono, high nibble first) */ + codec_DVI_IMA_int, /* DVI IMA ADPCM (mono/interleave, high nibble first) */ + codec_NW_IMA, + codec_SNDS_IMA, /* Heavy Iron Studios .snds IMA ADPCM */ + codec_QD_IMA, + codec_WV6_IMA, /* Gorilla Systems WV6 4-bit IMA ADPCM */ + codec_HV_IMA, /* High Voltage 4-bit IMA ADPCM */ + codec_FFTA2_IMA, /* Final Fantasy Tactics A2 4-bit IMA ADPCM */ + codec_BLITZ_IMA, /* Blitz Games 4-bit IMA ADPCM */ + + codec_MS_IMA, /* Microsoft IMA ADPCM */ + codec_MS_IMA_mono, /* Microsoft IMA ADPCM (mono/interleave) */ + codec_XBOX_IMA, /* XBOX IMA ADPCM */ + codec_XBOX_IMA_mch, /* XBOX IMA ADPCM (multichannel) */ + codec_XBOX_IMA_int, /* XBOX IMA ADPCM (mono/interleave) */ + codec_NDS_IMA, /* IMA ADPCM w/ NDS layout */ + codec_DAT4_IMA, /* Eurocom 'DAT4' IMA ADPCM */ + codec_RAD_IMA, /* Radical IMA ADPCM */ + codec_RAD_IMA_mono, /* Radical IMA ADPCM (mono/interleave) */ + codec_APPLE_IMA4, /* Apple Quicktime IMA4 */ + codec_FSB_IMA, /* FMOD's FSB multichannel IMA ADPCM */ + codec_WWISE_IMA, /* Audiokinetic Wwise IMA ADPCM */ + codec_REF_IMA, /* Reflections IMA ADPCM */ + codec_AWC_IMA, /* Rockstar AWC IMA ADPCM */ + codec_UBI_IMA, /* Ubisoft IMA ADPCM */ + codec_UBI_SCE_IMA, /* Ubisoft SCE IMA ADPCM */ + codec_H4M_IMA, /* H4M IMA ADPCM (stereo or mono, high nibble first) */ + codec_MTF_IMA, /* Capcom MT Framework IMA ADPCM */ + codec_CD_IMA, /* Crystal Dynamics IMA ADPCM */ + + codec_MSADPCM, /* Microsoft ADPCM (stereo/mono) */ + codec_MSADPCM_int, /* Microsoft ADPCM (mono) */ + codec_MSADPCM_ck, /* Microsoft ADPCM (Cricket Audio variation) */ + codec_WS, /* Westwood Studios VBR ADPCM */ + + codec_AICA, /* Yamaha AICA ADPCM (stereo) */ + codec_AICA_int, /* Yamaha AICA ADPCM (mono/interleave) */ + codec_CP_YM, /* Capcom's Yamaha ADPCM (stereo/mono) */ + codec_ASKA, /* Aska ADPCM */ + codec_NXAP, /* NXAP ADPCM */ + + codec_TGC, /* Tiger Game.com 4-bit ADPCM */ + + codec_PSX_DSE_SQUARESOFT, /* SquareSoft Digital Sound Elements 16-bit PCM (For PSX) */ + codec_PS2_DSE_PROCYON, /* Procyon Studio Digital Sound Elements ADPCM (PS2 Version, encoded with VAG-ADPCM) */ + codec_NDS_DSE_PROCYON, /* Procyon Studio Digital Sound Elements ADPCM (NDS Version, encoded with IMA-ADPCM) */ + codec_WII_DSE_PROCYON, /* Procyon Studio Digital Sound Elements ADPCM (Wii Version, encoded with DSP-ADPCM) */ + codec_L5_555, /* Level-5 0x555 ADPCM */ + codec_LSF, /* lsf ADPCM (Fastlane Street Racing iPhone)*/ + codec_MTAF, /* Konami MTAF ADPCM */ + codec_MTA2, /* Konami MTA2 ADPCM */ + codec_MC3, /* Paradigm MC3 3-bit ADPCM */ + codec_FADPCM, /* FMOD FADPCM 4-bit ADPCM */ + codec_ASF, /* Argonaut ASF 4-bit ADPCM */ + codec_DSA, /* Ocean DSA 4-bit ADPCM */ + codec_XMD, /* Konami XMD 4-bit ADPCM */ + codec_TANTALUS, /* Tantalus 4-bit ADPCM */ + codec_PCFX, /* PC-FX 4-bit ADPCM */ + codec_OKI16, /* OKI 4-bit ADPCM with 16-bit output and modified expand */ + codec_OKI4S, /* OKI 4-bit ADPCM with 16-bit output and cuadruple step */ + codec_PTADPCM, /* Platinum 4-bit ADPCM */ + codec_IMUSE, /* LucasArts iMUSE Variable ADPCM */ + codec_COMPRESSWAVE, /* CompressWave Huffman ADPCM */ + + /* others */ + codec_SDX2, /* SDX2 2:1 Squareroot-Delta-Exact compression DPCM */ + codec_SDX2_int, /* SDX2 2:1 Squareroot-Delta-Exact compression with sample-level interleave */ + codec_CBD2, /* CBD2 2:1 Cuberoot-Delta-Exact compression DPCM */ + codec_CBD2_int, /* CBD2 2:1 Cuberoot-Delta-Exact compression, with sample-level interleave */ + codec_SASSC, /* Activision EXAKT SASSC 8-bit DPCM */ + codec_DERF, /* DERF 8-bit DPCM */ + codec_WADY, /* WADY 8-bit DPCM */ + codec_NWA, /* VisualArt's NWA DPCM */ + codec_ACM, /* InterPlay ACM */ + codec_CIRCUS_ADPCM, /* Circus 8-bit ADPCM */ + codec_UBI_ADPCM, /* Ubisoft 4/6-bit ADPCM */ + + codec_EA_MT, /* Electronic Arts MicroTalk (linear-predictive speech codec) */ + codec_CIRCUS_VQ, /* Circus VQ */ + codec_RELIC, /* Relic Codec (DCT-based) */ + codec_CRI_HCA, /* CRI High Compression Audio (MDCT-based) */ + codec_TAC, /* tri-Ace Codec (MDCT-based) */ + codec_ICE_RANGE, /* Inti Creates "range" codec */ + codec_ICE_DCT, /* Inti Creates "DCT" codec */ + + + codec_OGG_VORBIS, /* Xiph Vorbis with Ogg layer (MDCT-based) */ + codec_VORBIS_custom, /* Xiph Vorbis with custom layer (MDCT-based) */ + + + codec_MPEG_custom, /* MPEG audio with custom features (MDCT-based) */ + codec_MPEG_ealayer3, /* EALayer3, custom MPEG frames */ + codec_MPEG_layer1, /* MP1 MPEG audio (MDCT-based) */ + codec_MPEG_layer2, /* MP2 MPEG audio (MDCT-based) */ + codec_MPEG_layer3, /* MP3 MPEG audio (MDCT-based) */ + + + codec_G7221C, /* ITU G.722.1 annex C (Polycom Siren 14) */ + + + codec_G719, /* ITU G.719 annex B (Polycom Siren 22) */ + + + codec_MP4_AAC, /* AAC (MDCT-based) */ + + + codec_ATRAC9, /* Sony ATRAC9 (MDCT-based) */ + + + codec_CELT_FSB, /* Custom Xiph CELT (MDCT-based) */ + + + codec_SPEEX, /* Custom Speex (CELP-based) */ + + + codec_FFmpeg, /* Formats handled by FFmpeg (ATRAC3, XMA, AC3, etc) */ +} \ No newline at end of file diff --git a/VG Music Studio - Core/Codec/DSPADPCM.cs b/VG Music Studio - Core/Codec/DSPADPCM.cs new file mode 100644 index 00000000..1196fbde --- /dev/null +++ b/VG Music Studio - Core/Codec/DSPADPCM.cs @@ -0,0 +1,1026 @@ +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Formats; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace Kermalis.VGMusicStudio.Core.Codec; + +internal struct DSPADPCM +{ + public static short Min = short.MinValue; + public static short Max = short.MaxValue; + + public static double[] Tvec = new double[3]; + + public DSPADPCMInfo[] Info; + private readonly ushort NumChannels; + private ushort Channel; + public byte[] Data; + public short[]? DataOutput; + + private int DataOffset; + private int SamplePos; + public int Scale; + public byte ByteValue; + public int FrameOffset; + public int[]? Coef1; + public int[]? Coef2; + public short[]? Hist1; + public short[]? Hist2; + private int Nibble; + + public static class DSPADPCMConstants + { + public const int BytesPerFrame = 8; + public const int SamplesPerFrame = 14; + public const int NibblesPerFrame = 16; + } + + public DSPADPCM(EndianBinaryReader r, ushort? numChannels) + { + _ = numChannels == null ? NumChannels = 1 : NumChannels = (ushort)numChannels; // The number of waveform channels + Info = new DSPADPCMInfo[NumChannels]; + for (int i = 0; i < NumChannels; i++) + { + Info[i] = new DSPADPCMInfo(r); // First, the 96 byte-long DSP-ADPCM header table is read and each variable is assigned with a value + } + Data = new byte[DataUtils.RoundUp((int)Info[0].NumAdpcmNibbles / 2, 16) * NumChannels]; // Next, this allocates the full size of the data, based on NumAdpcmNibbles divided by 2 and rounded up to the 16th byte + DataOutput = new short[Info[0].NumSamples * NumChannels]; // This will allocate the size of the DataOutput array based on the value in NumSamples + + r.ReadBytes(Data); // This reads the compressed sample data based on the size allocated + r.Stream.Align(16); // This will align the EndianBinaryReader stream offset to the 16th byte, since all sample data ends at every 16th byte + return; + } + + #region DSP-ADPCM Info + public interface IDSPADPCMInfo + { + public short[] Coef { get; } + public ushort Gain { get; } + public ushort PredScale { get; } + public short Yn1 { get; } + public short Yn2 { get; } + + public ushort LoopPredScale { get; } + public short LoopYn1 { get; } + public short LoopYn2 { get; } + } + + public class DSPADPCMInfo : IDSPADPCMInfo + { + public uint NumSamples { get; set; } + public uint NumAdpcmNibbles { get; set; } + public uint SampleRate { get; set; } + public ushort LoopFlag { get; set; } + public ushort Format { get; set; } + public uint Sa { get; set; } + public uint Ea { get; set; } + public uint Ca { get; set; } + public short[] Coef { get; set; } + public ushort Gain { get; set; } + public ushort PredScale { get; set; } + public short Yn1 { get; set; } + public short Yn2 { get; set; } + + public ushort LoopPredScale { get; set; } + public short LoopYn1 { get; set; } + public short LoopYn2 { get; set; } + public ushort[] Padding { get; set; } + + public DSPADPCMInfo(EndianBinaryReader r) + { + NumSamples = r.ReadUInt32(); + + NumAdpcmNibbles = r.ReadUInt32(); + + SampleRate = r.ReadUInt32(); + + LoopFlag = r.ReadUInt16(); + + Format = r.ReadUInt16(); + + Sa = r.ReadUInt32(); + + Ea = r.ReadUInt32(); + + Ca = r.ReadUInt32(); + + Coef = new short[16]; + r.ReadInt16s(Coef); + + Gain = r.ReadUInt16(); + + PredScale = r.ReadUInt16(); + + Yn1 = r.ReadInt16(); + + Yn2 = r.ReadInt16(); + + LoopPredScale = r.ReadUInt16(); + + LoopYn1 = r.ReadInt16(); + + LoopYn2 = r.ReadInt16(); + + Padding = new ushort[11]; + r.ReadUInt16s(Padding); + } + + public byte[] ToBytes() + { + _ = new byte[96]; + var numSamples = new byte[4]; + var numAdpcmNibbles = new byte[4]; + var sampleRate = new byte[4]; + var loopFlag = new byte[2]; + var format = new byte[2]; + var sa = new byte[4]; + var ea = new byte[4]; + var ca = new byte[4]; + var coef = new byte[32]; + var gain = new byte[2]; + var predScale = new byte[2]; + var yn1 = new byte[2]; + var yn2 = new byte[2]; + var loopPredScale = new byte[2]; + var loopYn1 = new byte[2]; + var loopYn2 = new byte[2]; + var padding = new byte[22]; + + BinaryPrimitives.WriteUInt32BigEndian(numSamples, NumSamples); + BinaryPrimitives.WriteUInt32BigEndian(numAdpcmNibbles, NumAdpcmNibbles); + BinaryPrimitives.WriteUInt32BigEndian(sampleRate, SampleRate); + BinaryPrimitives.WriteUInt16BigEndian(loopFlag, LoopFlag); + BinaryPrimitives.WriteUInt16BigEndian(format, Format); + BinaryPrimitives.WriteUInt32BigEndian(sa, Sa); + BinaryPrimitives.WriteUInt32BigEndian(ea, Ea); + BinaryPrimitives.WriteUInt32BigEndian(ca, Ca); + int index = 0; + for (int i = 0; i < 16; i++) + { + coef[index++] = (byte)(Coef[i] >> 8); + coef[index++] = (byte)(Coef[i] & 0xff); + } + BinaryPrimitives.WriteUInt16BigEndian(gain, Gain); + BinaryPrimitives.WriteUInt16BigEndian(predScale, PredScale); + BinaryPrimitives.WriteInt16BigEndian(yn1, Yn1); + BinaryPrimitives.WriteInt16BigEndian(yn2, Yn2); + BinaryPrimitives.WriteUInt16BigEndian(loopPredScale, LoopPredScale); + BinaryPrimitives.WriteInt16BigEndian(loopYn1, LoopYn1); + BinaryPrimitives.WriteInt16BigEndian(loopYn2, LoopYn2); + + // A collection expression is used for combining all byte arrays into one, instead of using the Concat() function + byte[]? bytes = [.. numSamples, .. numAdpcmNibbles, .. sampleRate, + .. loopFlag, .. format, .. sa, .. ea, .. ca, .. coef, .. gain, + .. predScale, .. yn1, .. yn2, .. loopPredScale, .. loopYn1, .. loopYn2, .. padding]; + + return bytes; + } + } + #endregion + + #region DSP-ADPCM Convert + + public static int NibblesToSamples(int nibbles) + { + var fullFrames = nibbles / 16; + var remainder = nibbles % 16; + + return remainder > 0 ? (fullFrames * 14) + remainder - 2 : fullFrames * 14; + } + + public static int BytesToSamples(int bytes, int channels) + { + return channels <= 0 ? 0 : (bytes / channels) / (8 * 14); + } + + public readonly byte[] InfoToBytes() + { + var info = new DSPADPCMInfo[NumChannels]; + var infoData = new byte[96 * NumChannels]; + for (int i = 0; i < NumChannels; i++) + { + Array.Copy(info[i].ToBytes(), infoData, 96 * (i + 1)); + } + return infoData; + } + + public readonly byte[] DataOutputToBytes() + { + int index = 0; + var data = new byte[DataOutput!.Length * 2]; + for (int i = 0; i < DataOutput.Length; i++) + { + data[index++] = (byte)(DataOutput[i] >> 8); + data[index++] = (byte)DataOutput[i]; + } + return data; + } + + public readonly byte[] ConvertToWav() + { + return new Wave().CreateIeeeFloatWave( + Info[0].SampleRate, NumChannels, + isLooped: Info[0].LoopFlag == 0 || Info[0].LoopFlag == 1, + loopStart: (uint)NibblesToSamples((int)Info[0].Sa), + loopEnd: (uint)NibblesToSamples((int)Info[0].Ea)) + .WriteBytes(DataOutput).ToArray(); + } + + #endregion + + #region DSP-ADPCM Encode + + public static void Encode(Span src, Span dst, DSPADPCMInfo cxt, uint samples) + { + Span coefs = cxt.Coef; + CorrelateCoefs(src, samples, coefs); + + int frameCount = (int)((samples / DSPADPCMConstants.SamplesPerFrame) + (samples % DSPADPCMConstants.SamplesPerFrame)); + + Span pcm = src; + Span adpcm = dst; + Span pcmFrame = new short[DSPADPCMConstants.SamplesPerFrame + 2]; + Span adpcmFrame = new byte[DSPADPCMConstants.BytesPerFrame]; + + short srcIndex = 0; + short dstIndex = 0; + + for (int i = 0; i < frameCount; ++i, pcm[srcIndex] += DSPADPCMConstants.SamplesPerFrame, adpcm[srcIndex] += DSPADPCMConstants.BytesPerFrame) + { + coefs = new short[2 + 0]; + + DSPEncodeFrame(pcmFrame, DSPADPCMConstants.SamplesPerFrame, adpcmFrame, coefs); + + pcmFrame[0] = pcmFrame[14]; + pcmFrame[1] = pcmFrame[15]; + } + + cxt.Gain = 0; + cxt.PredScale = dst[dstIndex++]; + cxt.Yn1 = 0; + cxt.Yn2 = 0; + } + + public static void InnerProductMerge(Span vecOut, Span pcmBuf) + { + pcmBuf = new short[14].AsSpan(); + vecOut = Tvec; + + for (int i = 0; i <= 2; i++) + { + vecOut[i] = 0.0f; + for (int x = 0; x < 14; x++) + vecOut[i] -= pcmBuf[x - i] * pcmBuf[x]; + + } + } + + public static void OuterProductMerge(Span mtxOut, Span pcmBuf) + { + pcmBuf = new short[14].AsSpan(); + mtxOut[3] = Tvec[3]; + + for (int x = 1; x <= 2; x++) + for (int y = 1; y <= 2; y++) + { + mtxOut[x] = 0.0; + mtxOut[y] = 0.0; + for (int z = 0; z < 14; z++) + mtxOut[x + y] += pcmBuf[z - x] * pcmBuf[z - y]; + } + } + + public static bool AnalyzeRanges(Span mtx, Span vecIdxsOut) + { + mtx[3] = Tvec[3]; + Span recips = new double[3].AsSpan(); + double val, tmp, min, max; + + /* Get greatest distance from zero */ + for (int x = 1; x <= 2; x++) + { + val = Math.Max(Math.Abs(mtx[x] + mtx[1]), Math.Abs(mtx[x] + mtx[2])); + if (val < double.Epsilon) + return true; + + recips[x] = 1.0 / val; + } + + int maxIndex = 0; + for (int i = 1; i <= 2; i++) + { + for (int x = 1; x < i; x++) + { + tmp = mtx[x] + mtx[i]; + for (int y = 1; y < x; y++) + tmp -= (mtx[x] + mtx[y]) * (mtx[y] + mtx[i]); + mtx[x + i] = tmp; + } + + val = 0.0; + for (int x = i; x <= 2; x++) + { + tmp = mtx[x] + mtx[i]; + for (int y = 1; y < i; y++) + tmp -= (mtx[x] + mtx[y]) * (mtx[y] + mtx[i]); + + mtx[x + i] = tmp; + tmp = Math.Abs(tmp) * recips[x]; + if (tmp >= val) + { + val = tmp; + maxIndex = x; + } + } + + if (maxIndex != i) + { + for (int y = 1; y <= 2; y++) + { + tmp = mtx[maxIndex] + mtx[y]; + mtx[maxIndex + y] = mtx[i] + mtx[y]; + mtx[i + y] = tmp; + } + recips[maxIndex] = recips[i]; + } + + vecIdxsOut[i] = maxIndex; + + if (mtx[i] + mtx[i] == 0.0) + return true; + + if (i != 2) + { + tmp = 1.0 / mtx[i] + mtx[i]; + for (int x = i + 1; x <= 2; x++) + mtx[x + i] *= tmp; + } + } + + /* Get range */ + min = 1.0e10; + max = 0.0; + for (int i = 1; i <= 2; i++) + { + tmp = Math.Abs(mtx[i] + mtx[i]); + if (tmp < min) + min = tmp; + if (tmp > max) + max = tmp; + } + + if (min / max < 1.0e-10) + return true; + + return false; + } + + public static void BidirectionalFilter(Span mtx, Span vecIdxs, Span vecOut) + { + mtx[3] = Tvec[3]; + vecOut = Tvec; + double tmp; + + for (int i = 1, x = 0; i <= 2; i++) + { + int index = vecIdxs[i]; + tmp = vecOut[index]; + vecOut[index] = vecOut[i]; + if (x != 0) + for (int y = x; y <= i - 1; y++) + tmp -= vecOut[y] * mtx[i] + mtx[y]; + else if (tmp != 0.0) + x = i; + vecOut[i] = tmp; + } + + for (int i = 2; i > 0; i--) + { + tmp = vecOut[i]; + for (int y = i + 1; y <= 2; y++) + tmp -= vecOut[y] * mtx[i] + mtx[y]; + vecOut[i] = tmp / mtx[i] + mtx[i]; + } + + vecOut[0] = 1.0; + } + + public static bool QuadraticMerge(Span inOutVec) + { + inOutVec = Tvec; + + double v0, v1, v2 = inOutVec[2]; + double tmp = 1.0 - (v2 * v2); + + if (tmp == 0.0) + return true; + + v0 = (inOutVec[0] - (v2 * v2)) / tmp; + v1 = (inOutVec[1] - (inOutVec[1] * v2)) / tmp; + + inOutVec[0] = v0; + inOutVec[1] = v1; + + return Math.Abs(v1) > 1.0; + } + + public static void FinishRecord(Span vIn, Span vOut) + { + vIn = Tvec; + vOut = Tvec; + for (int z = 1; z <= 2; z++) + { + if (vIn[z] >= 1.0) + vIn[z] = 0.9999999999; + + else if (vIn[z] <= -1.0) + vIn[z] = -0.9999999999; + } + vOut[0] = 1.0; + vOut[1] = (vIn[2] * vIn[1]) + vIn[1]; + vOut[2] = vIn[2]; + } + + public static void MatrixFilter(Span src, Span dst) + { + src = Tvec; + dst = Tvec; + double[] mtx = new double[3]; + Tvec = mtx; + + mtx[2 + 0] = 1.0; + for (int i = 1; i <= 2; i++) + mtx[2 + i] = -src[i]; + + for (int i = 2; i > 0; i--) + { + double val = 1.0 - ((mtx[i] + mtx[i]) * (mtx[i] + mtx[i])); + for (int y = 1; y <= i; y++) + mtx[i - 1 + y] = (((mtx[i] + mtx[i]) * (mtx[i] + mtx[y])) + mtx[i] + mtx[y]) / val; + } + + dst[0] = 1.0; + for (int i = 1; i <= 2; i++) + { + dst[i] = 0.0; + for (int y = 1; y <= i; y++) + dst[i] += (mtx[i] + mtx[y]) * dst[i - y]; + } + } + + public static void MergeFinishRecord(Span src, Span dst) + { + src = Tvec; + dst = Tvec; + int dstIndex = 0; + Span tmp = new double[dstIndex].AsSpan(); + double val = src[0]; + + dst[0] = 1.0; + for (int i = 1; i <= 2; i++) + { + double v2 = 0.0; + for (int y = 1; y < i; y++) + v2 += dst[y] * src[i - y]; + + if (val > 0.0) + dst[i] = -(v2 + src[i]) / val; + else + dst[i] = 0.0; + + tmp[i] = dst[i]; + + for (int y = 1; y < i; y++) + dst[y] += dst[i] * dst[i - y]; + + val *= 1.0 - (dst[i] * dst[i]); + } + + FinishRecord(tmp, dst); + } + + public static double ContrastVectors(Span source1, Span source2) + { + source1 = Tvec; + source2 = Tvec; + double val = (source2[2] * source2[1] + -source2[1]) / (1.0 - source2[2] * source2[2]); + double val1 = (source1[0] * source1[0]) + (source1[1] * source1[1]) + (source1[2] * source1[2]); + double val2 = (source1[0] * source1[1]) + (source1[1] * source1[2]); + double val3 = source1[0] * source1[2]; + return val1 + (2.0 * val * val2) + (2.0 * (-source2[1] * val + -source2[2]) * val3); + } + + public static void FilterRecords(Span vecBest, int exp, Span records, int recordCount) + { + vecBest[8] = Tvec[8]; + records = Tvec; + Span bufferList = new double[8].AsSpan(); + bufferList[8] = Tvec[8]; + + Span buffer1 = new int[8].AsSpan(); + Span buffer2 = Tvec; + + int index; + double value, tempVal = 0; + + for (int x = 0; x < 2; x++) + { + for (int y = 0; y < exp; y++) + { + buffer1[y] = 0; + for (int i = 0; i <= 2; i++) + bufferList[y + i] = 0.0; + } + for (int z = 0; z < recordCount; z++) + { + index = 0; + value = 1.0e30; + for (int i = 0; i < exp; i++) + { + vecBest = new double[i].AsSpan(); + records = new double[z].AsSpan(); + tempVal = ContrastVectors(vecBest, records); + if (tempVal < value) + { + value = tempVal; + index = i; + } + } + buffer1[index]++; + MatrixFilter(records, buffer2); + for (int i = 0; i <= 2; i++) + bufferList[index + i] += buffer2[i]; + } + + for (int i = 0; i < exp; i++) + if (buffer1[i] > 0) + for (int y = 0; y <= 2; y++) + bufferList[i + y] /= buffer1[i]; + + for (int i = 0; i < exp; i++) + bufferList = new double[i]; + MergeFinishRecord(bufferList, vecBest); + } + } + + public static void CorrelateCoefs(Span source, uint samples, Span coefsOut) + { + int numFrames = (int)((samples + 13) / 14); + int frameSamples; + + Span blockBuffer = new short[0x3800].AsSpan(); + Span pcmHistBuffer = new short[2 + 14].AsSpan(); + + Span vec1 = Tvec; + Span vec2 = Tvec; + + Span mtx = Tvec; + mtx[3] = Tvec[3]; + Span vecIdxs = new int[3].AsSpan(); + + Span records = new double[numFrames * 2].AsSpan(); + records = Tvec; + int recordCount = 0; + + Span vecBest = new double[8].AsSpan(); + vecBest[8] = Tvec[8]; + + int sourceIndex = 0; + + /* Iterate though 1024-block frames */ + for (int x = (int)samples; x > 0;) + { + if (x > 0x3800) /* Full 1024-block frame */ + { + frameSamples = 0x3800; + x -= 0x3800; + } + else /* Partial frame */ + { + /* Zero lingering block samples */ + frameSamples = x; + for (int z = 0; z < 14 && z + frameSamples < 0x3800; z++) + blockBuffer[frameSamples + z] = 0; + x = 0; + } + + /* Copy (potentially non-frame-aligned PCM samples into aligned buffer) */ + source[sourceIndex] += (short)frameSamples; + + + for (int i = 0; i < frameSamples;) + { + for (int z = 0; z < 14; z++) + pcmHistBuffer[0 + z] = pcmHistBuffer[1 + z]; + for (int z = 0; z < 14; z++) + pcmHistBuffer[1 + z] = blockBuffer[i++]; + + pcmHistBuffer = new short[1].AsSpan(); + + InnerProductMerge(vec1, pcmHistBuffer); + if (Math.Abs(vec1[0]) > 10.0) + { + OuterProductMerge(mtx, pcmHistBuffer); + if (!AnalyzeRanges(mtx, vecIdxs)) + { + BidirectionalFilter(mtx, vecIdxs, vec1); + if (!QuadraticMerge(vec1)) + { + records = new double[recordCount].AsSpan(); + FinishRecord(vec1, records); + recordCount++; + } + } + } + } + } + + vec1[0] = 1.0; + vec1[1] = 0.0; + vec1[2] = 0.0; + + for (int z = 0; z < recordCount; z++) + { + records = new double[z].AsSpan(); + vecBest = new double[0].AsSpan(); + MatrixFilter(records, vecBest); + for (int y = 1; y <= 2; y++) + vec1[y] += vecBest[0] + vecBest[y]; + } + for (int y = 1; y <= 2; y++) + vec1[y] /= recordCount; + + MergeFinishRecord(vec1, vecBest); + + + int exp = 1; + for (int w = 0; w < 3;) + { + vec2[0] = 0.0; + vec2[1] = -1.0; + vec2[2] = 0.0; + for (int i = 0; i < exp; i++) + for (int y = 0; y <= 2; y++) + vecBest[exp + i + y] = (0.01 * vec2[y]) + vecBest[i] + vecBest[y]; + ++w; + exp = 1 << w; + FilterRecords(vecBest, exp, records, recordCount); + } + + /* Write output */ + for (int z = 0; z < 8; z++) + { + double d; + d = -vecBest[z] + vecBest[1] * 2048.0; + if (d > 0.0) + coefsOut[z * 2] = (d > 32767.0) ? (short)32767 : (short)Math.Round(d); + else + coefsOut[z * 2] = (d < -32768.0) ? (short)-32768 : (short)Math.Round(d); + + d = -vecBest[z] + vecBest[2] * 2048.0; + if (d > 0.0) + coefsOut[z * 2 + 1] = (d > 32767.0) ? (short)32767 : (short)Math.Round(d); + else + coefsOut[z * 2 + 1] = (d < -32768.0) ? (short)-32768 : (short)Math.Round(d); + } + } + + /* Make sure source includes the yn values (16 samples total) */ + public static void DSPEncodeFrame(Span pcmInOut, int sampleCount, Span adpcmOut, Span coefsIn) + { + pcmInOut = new short[16].AsSpan(); + adpcmOut = new byte[8].AsSpan(); + coefsIn = new short[8].AsSpan(); + coefsIn = new short[2].AsSpan(); + + Span inSamples = new int[8].AsSpan(); + inSamples = new int[16].AsSpan(); + Span outSamples = new int[8].AsSpan(); + outSamples = new int[14].AsSpan(); + + int bestIndex = 0; + + Span scale = new int[8].AsSpan(); + Span distAccum = new double[8].AsSpan(); + + /* Iterate through each coef set, finding the set with the smallest error */ + for (int i = 0; i < 8; i++) + { + int v1, v2, v3; + int distance, index; + + /* Set yn values */ + inSamples[i + 0] = pcmInOut[0]; + inSamples[i + 1] = pcmInOut[1]; + + /* Round and clamp samples for this coef set */ + distance = 0; + for (int s = 0; s < sampleCount; s++) + { + /* Multiply previous samples by coefs */ + inSamples[i + (s + 2)] = v1 = ((pcmInOut[s] * (coefsIn[i] + coefsIn[1])) + (pcmInOut[s + 1] * (coefsIn[i] + coefsIn[0]))) / 2048; + /* Subtract from current sample */ + v2 = pcmInOut[s + 2] - v1; + /* Clamp */ + v3 = (v2 >= 32767) ? 32767 : (v2 <= -32768) ? -32768 : v2; + /* Compare distance */ + if (Math.Abs(v3) > Math.Abs(distance)) + distance = v3; + } + + /* Set initial scale */ + for (scale[i] = 0; (scale[i] <= 12) && ((distance > 7) || (distance < -8)); scale[i]++, distance /= 2) + { + } + scale[i] = (scale[i] <= 1) ? -1 : scale[i] - 2; + + do + { + scale[i]++; + distAccum[i] = 0; + index = 0; + + for (int s = 0; s < sampleCount; s++) + { + /* Multiply previous */ + v1 = (((inSamples[i] + inSamples[s]) * (coefsIn[i] + coefsIn[1])) + ((inSamples[i] + inSamples[s + 1]) * (coefsIn[i] + coefsIn[0]))); + /* Evaluate from real sample */ + v2 = (pcmInOut[s + 2] << 11) - v1; + /* Round to nearest sample */ + v3 = (v2 > 0) ? (int)((double)v2 / (1 << scale[i]) / 2048 + 0.4999999f) : (int)((double)v2 / (1 << scale[i]) / 2048 - 0.4999999f); + + /* Clamp sample and set index */ + if (v3 < -8) + { + if (index < (v3 = -8 - v3)) + index = v3; + v3 = -8; + } + else if (v3 > 7) + { + if (index < (v3 -= 7)) + index = v3; + v3 = 7; + } + + /* Store result */ + outSamples[i + s] = v3; + + /* Round and expand */ + v1 = (v1 + ((v3 * (1 << scale[i])) << 11) + 1024) >> 11; + /* Clamp and store */ + inSamples[i + (s + 2)] = v2 = (v1 >= 32767) ? 32767 : (v1 <= -32768) ? -32768 : v1; + /* Accumulate distance */ + v3 = pcmInOut[s + 2] - v2; + distAccum[i] += v3 * (double)v3; + } + + for (int x = index + 8; x > 256; x >>= 1) + if (++scale[i] >= 12) + scale[i] = 11; + } while ((scale[i] < 12) && (index > 1)); + } + + double min = double.MaxValue; + for (int i = 0; i < 8; i++) + { + if (distAccum[i] < min) + { + min = distAccum[i]; + bestIndex = i; + } + } + + /* Write converted samples */ + for (int s = 0; s < sampleCount; s++) + pcmInOut[s + 2] = (short)(inSamples[bestIndex] + inSamples[s + 2]); + + /* Write ps */ + adpcmOut[0] = (byte)((bestIndex << 4) | (scale[bestIndex] & 0xF)); + + /* Zero remaining samples */ + for (int s = sampleCount; s < 14; s++) + outSamples[bestIndex + s] = 0; + + /* Write output samples */ + for (int y = 0; y < 7; y++) + { + adpcmOut[y + 1] = (byte)((outSamples[bestIndex] + outSamples[y * 2] << 4) | (outSamples[bestIndex] + outSamples[y * 2 + 1] & 0xF)); + } + } + + public static void EncodeFrame(Span src, Span dst, Span coefs, byte one) + { + coefs = new short[0 + 2]; + DSPEncodeFrame(src, 14, dst, coefs); + } + + #endregion + + #region DSP-ADPCM Decode + + #region Method 1 + + private static readonly sbyte[] NibbleToSbyte = [0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1]; + + public static int DivideByRoundUp(int dividend, int divisor) + { + return (dividend + divisor - 1) / divisor; + } + + private static sbyte GetHighNibble(byte value) + { + return NibbleToSbyte[(value >> 4) & 0xF]; + } + + private static sbyte GetLowNibble(byte value) + { + return NibbleToSbyte[value & 0xF]; + } + + public static short Clamp16(int value) + { + if (value > Max) + return Max; + else if (value < Min) + return Min; + else return (short)value; + } + + #region Current code + public void Init(byte[] data, DSPADPCMInfo[] info) + { + Info = info; + Data = data; + DataOffset = 0; + SamplePos = 0; + FrameOffset = 0; + + Hist1 = new short[NumChannels]; + Hist2 = new short[NumChannels]; + Coef1 = new int[NumChannels]; + Coef2 = new int[NumChannels]; + } + + public void Decode() + { + Init(Data, Info); // Sets up the field variables + + // Each DSP-ADPCM frame is 8 bytes long: 1 byte for header, 7 bytes for sample data + // This loop reads every 8 bytes and decodes them until the samplePos reaches NumSamples + while (SamplePos < Info[0].NumSamples) + { + // This function will decode one frame at a time + DecodeFrame(); + + // The dataOffset is incremented by 8, and samplePos is incremented by 14 to prepare for the next frame + DataOffset += DSPADPCMConstants.BytesPerFrame; + SamplePos += DSPADPCMConstants.SamplesPerFrame; + } + + } + + public void DecodeFrame() + { + // It will decode 1 single DSP frame of size 0x08 (src) into 14 samples in a PCM buffer (dst) + for (int i = 0; i < NumChannels; i++) + { + Hist1![i] = Info[i].Yn1; + Hist2![i] = Info[i].Yn2; + } + + // Parsing the frame's header byte + Scale = 1 << ((Data[DataOffset]) & 0xf); + int coefIndex = ((Data[DataOffset] >> 4) & 0xf) * 2; + + // Parsing the coefficient pairs, based on the nibble's value + for (int i = 0; i < NumChannels; i++) + { + Coef1![i] = Info[i].Coef[coefIndex + 0]; + Coef2![i] = Info[i].Coef[coefIndex + 1]; + } + + // This loop decodes the frame's nibbles, each of which are 4-bits long (half a byte in length) + for (FrameOffset = 0; FrameOffset < DSPADPCMConstants.SamplesPerFrame * NumChannels; FrameOffset += NumChannels) + { + // This ensures multi-channel DSP-ADPCM data is decoded as well + for (Channel = 0; Channel < NumChannels; Channel++) + { + // Stores the value of the entire byte based on the frame's offset + ByteValue = Data[DataOffset + 0x01 + FrameOffset / 2]; + + // This function decodes one nibble within a frame into a sample + short sample = GetSample(); + + // The DSP-ADPCM frame may have bytes that go beyond the DataOutput length, if this happens, this will safely finish the DecodeFrame function's task as is + if ((SamplePos + FrameOffset) * (Channel + 1) >= DataOutput!.Length) { return; } + + // The PCM16 sample is stored into the array entry, based on the sample offset and frame offset, multiplied by which wave channel is being used + DataOutput[(SamplePos + FrameOffset) * (Channel + 1)] = sample; + + // History values are stored, hist1 is copied into hist2 and the PCM16 sample is copied into hist1, before moving onto the next byte in the frame + Hist2![Channel] = Hist1![Channel]; + Hist1[Channel] = sample; + } + } + + // After the frame is decoded, the values in hist1 and hist2 are copied into Yn1 and Yn2 to prepare for the next frame + for (int i = 0; i < NumChannels; i++) + { + Info[i].Yn1 = Hist1![i]; + Info[i].Yn2 = Hist2![i]; + } + } + + public short GetSample() + { + Nibble = (FrameOffset & 1) != 0 ? // This conditional operator will store the value of the nibble + GetLowNibble(ByteValue) : // If the byte is not 0, it will obtain the least significant nibble (4-bits) + GetHighNibble(ByteValue); // Otherwise, if the byte is 0, it will obtain the most significant nibble (4-bits) + int largerVal = (Nibble * Scale) << 11; // The nibble's value is multiplied by scale's value, then 11 bits are shifted left, making the value larger + int newVal = (largerVal + 1024 + (Coef1![Channel] * Hist1![Channel]) + (Coef2![Channel] * Hist2![Channel])) >> 11; // Coefficients are multiplied by the value stored in hist1 and hist2 respectively, then the values are added together to make a new value + short sample = Clamp16(newVal); // The new value is then clamped into a 16-bit value, which makes a PCM16 sample + + return sample; + } + + #endregion + + + #endregion + + + #endregion + + #region DSP-ADPCM Math + public static uint GetBytesForADPCMBuffer(uint samples) + { + uint frames = samples / DSPADPCMConstants.SamplesPerFrame; + if ((samples % DSPADPCMConstants.SamplesPerFrame) == frames) + frames++; + + return frames * DSPADPCMConstants.BytesPerFrame; + } + + public static uint GetBytesForADPCMSamples(uint samples) + { + uint extraBytes = 0; + uint frames = samples / DSPADPCMConstants.SamplesPerFrame; + uint extraSamples = (samples % DSPADPCMConstants.SamplesPerFrame); + + if (extraSamples == frames) + { + extraBytes = (extraSamples / 2) + (extraSamples % 2) + 1; + } + + return DSPADPCMConstants.BytesPerFrame * frames + extraBytes; + } + + public static uint GetBytesForPCMBuffer(uint samples) + { + uint frames = samples / DSPADPCMConstants.SamplesPerFrame; + if ((samples % DSPADPCMConstants.SamplesPerFrame) == frames) + frames++; + + return frames * DSPADPCMConstants.SamplesPerFrame * sizeof(int); + } + + public static uint GetBytesForPCMSamples(uint samples) + { + return samples * sizeof(int); + } + + public static uint GetNibbleAddress(uint samples) + { + int frames = (int)(samples / DSPADPCMConstants.SamplesPerFrame); + int extraSamples = (int)(samples % DSPADPCMConstants.SamplesPerFrame); + + return (uint)(DSPADPCMConstants.NibblesPerFrame * frames + extraSamples + 2); + } + + public static uint GetNibblesForNSamples(uint samples) + { + uint frames = samples / DSPADPCMConstants.SamplesPerFrame; + uint extraSamples = (samples % DSPADPCMConstants.SamplesPerFrame); + uint extraNibbles = extraSamples == 0 ? 0 : extraSamples + 2; + + return DSPADPCMConstants.NibblesPerFrame * frames + extraNibbles; + } + + public static uint GetSampleForADPCMNibble(uint nibble) + { + uint frames = nibble / DSPADPCMConstants.NibblesPerFrame; + uint extraNibbles = (nibble % DSPADPCMConstants.NibblesPerFrame); + uint samples = DSPADPCMConstants.SamplesPerFrame * frames; + + return samples + extraNibbles - 2; + } + #endregion +} diff --git a/VG Music Studio - Core/ADPCMDecoder.cs b/VG Music Studio - Core/Codec/IMAADPCM.cs similarity index 88% rename from VG Music Studio - Core/ADPCMDecoder.cs rename to VG Music Studio - Core/Codec/IMAADPCM.cs index 894ea768..ed1ada26 100644 --- a/VG Music Studio - Core/ADPCMDecoder.cs +++ b/VG Music Studio - Core/Codec/IMAADPCM.cs @@ -1,9 +1,10 @@ using System; -namespace Kermalis.VGMusicStudio.Core; +namespace Kermalis.VGMusicStudio.Core.Codec; -internal struct ADPCMDecoder +internal struct IMAADPCM { + // TODO: Add encoding functionality and PCM16 to IMA-ADPCM conversion private static ReadOnlySpan IndexTable => new short[8] { -1, -1, -1, -1, 2, 4, 6, 8, @@ -39,9 +40,9 @@ public void Init(byte[] data) OnSecondNibble = false; } - public static short[] ADPCMToPCM16(byte[] data) + public static short[] IMAADPCMToPCM16(byte[] data) { - var decoder = new ADPCMDecoder(); + var decoder = new IMAADPCM(); decoder.Init(data); short[] buffer = new short[(data.Length - 4) * 2]; @@ -54,6 +55,10 @@ public static short[] ADPCMToPCM16(byte[] data) public short GetSample() { + if (DataOffset >= _data.Length) + { + return 0; + } int val = (_data[DataOffset] >> (OnSecondNibble ? 4 : 0)) & 0xF; short step = StepTable[StepIndex]; int diff = diff --git a/VG Music Studio - Core/Codec/LayoutEnums.cs b/VG Music Studio - Core/Codec/LayoutEnums.cs new file mode 100644 index 00000000..f2419480 --- /dev/null +++ b/VG Music Studio - Core/Codec/LayoutEnums.cs @@ -0,0 +1,59 @@ +namespace Kermalis.VGMusicStudio.Core.Codec; + +/* This code has been copied directly from vgmstream.h in VGMStream's repository * + * and modified into C# code to work with VGMS. Link to its repository can be * + * found here: https://github.com/vgmstream/vgmstream */ +public enum LayoutType +{ + /* generic */ + layout_none, /* straight data */ + + /* interleave */ + layout_interleave, /* equal interleave throughout the stream */ + + /* headered blocks */ + layout_blocked_ast, + layout_blocked_halpst, + layout_blocked_xa, + layout_blocked_ea_schl, + layout_blocked_ea_1snh, + layout_blocked_caf, + layout_blocked_wsi, + layout_blocked_str_snds, + layout_blocked_ws_aud, + layout_blocked_matx, + layout_blocked_dec, + layout_blocked_xvas, + layout_blocked_vs, + layout_blocked_mul, + layout_blocked_gsb, + layout_blocked_thp, + layout_blocked_filp, + layout_blocked_ea_swvr, + layout_blocked_adm, + layout_blocked_bdsp, + layout_blocked_mxch, + layout_blocked_ivaud, /* GTA IV .ivaud blocks */ + layout_blocked_ps2_iab, + layout_blocked_vs_str, + layout_blocked_rws, + layout_blocked_hwas, + layout_blocked_ea_sns, /* newest Electronic Arts blocks, found in SNS/SNU/SPS/etc formats */ + layout_blocked_awc, /* Rockstar AWC */ + layout_blocked_vgs, /* Guitar Hero II (PS2) */ + layout_blocked_xwav, + layout_blocked_xvag_subsong, /* XVAG subsongs [God of War III (PS4)] */ + layout_blocked_ea_wve_au00, /* EA WVE au00 blocks */ + layout_blocked_ea_wve_ad10, /* EA WVE Ad10 blocks */ + layout_blocked_sthd, /* Dream Factory STHD */ + layout_blocked_h4m, /* H4M video */ + layout_blocked_xa_aiff, /* XA in AIFF files [Crusader: No Remorse (SAT), Road Rash (3DO)] */ + layout_blocked_vs_square, + layout_blocked_vid1, + layout_blocked_ubi_sce, + layout_blocked_tt_ad, + + /* otherwise odd */ + layout_segmented, /* song divided in segments (song sections) */ + layout_layered, /* song divided in layers (song channels) */ +} \ No newline at end of file diff --git a/VG Music Studio - Core/Codec/MetaEnums.cs b/VG Music Studio - Core/Codec/MetaEnums.cs new file mode 100644 index 00000000..e4cb65a6 --- /dev/null +++ b/VG Music Studio - Core/Codec/MetaEnums.cs @@ -0,0 +1,479 @@ +namespace Kermalis.VGMusicStudio.Core.Codec; + +/* This code has been copied directly from vgmstream.h in VGMStream's repository * + * and modified into C# code to work with VGMS. Link to its repository can be * + * found here: https://github.com/vgmstream/vgmstream */ +public enum MetaType +{ + meta_SILENCE, + + meta_DSP_STD, /* Nintendo standard GC ADPCM (DSP) header */ + meta_DSP_CSTR, /* Star Fox Assault "Cstr" */ + meta_DSP_RS03, /* Retro: Metroid Prime 2 "RS03" */ + meta_DSP_STM, /* Paper Mario 2 STM */ + meta_AGSC, /* Retro: Metroid Prime 2 title */ + meta_CSMP, /* Retro: Metroid Prime 3 (Wii), Donkey Kong Country Returns (Wii) */ + meta_RFRM, /* Retro: Donkey Kong Country Tropical Freeze (Wii U) */ + meta_DSP_MPDSP, /* Monopoly Party single header stereo */ + meta_DSP_JETTERS, /* Bomberman Jetters .dsp */ + meta_DSP_MSS, /* Free Radical GC games */ + meta_DSP_GCM, /* some of Traveller's Tales games */ + meta_DSP_STR, /* Conan .str files */ + meta_DSP_SADB, /* Procyon Studio Digtial Sound Elements DSP-ADPCM (Wii) .sad */ + meta_DSP_WSI, /* .wsi */ + meta_IDSP_TT, /* Traveller's Tales games */ + meta_DSP_WII_MUS, /* .mus */ + meta_DSP_WII_WSD, /* Phantom Brave (Wii) */ + meta_WII_NDP, /* Vertigo (Wii) */ + meta_DSP_YGO, /* Konami: Yu-Gi-Oh! The Falsebound Kingdom (NGC), Hikaru no Go 3 (NGC) */ + + meta_STRM, /* Nintendo/HAL Labs Nitro Soundmaker STRM */ + meta_RSTM, /* Nintendo/HAL Labs NW4R Soundmaker RSTM (Revolution Stream, similar to STRM) */ + meta_AFC, /* AFC */ + meta_AST, /* AST */ + meta_RWSD, /* Nintendo/HAL Labs NW4R Soundmaker single-stream RWSD */ + meta_RWAR, /* Nintendo/HAL Labs NW4R Soundmaker single-stream RWAR */ + meta_RWAV, /* Nintendo/HAL Labs NW4R Soundmaker contents of RWAR */ + meta_CWAV, /* Nintendo/HAL Labs NW4C Soundmaker contents of CWAR */ + meta_FWAV, /* Nintendo/HAL Labs NW4F Soundmaker contents of FWAR */ + meta_THP, /* THP movie files */ + meta_SWAV, + meta_NDS_RRDS, /* Ridge Racer DS */ + meta_WII_BNS, /* Wii BNS Banner Sound (similar to RSTM) */ + meta_WIIU_BTSND, /* Wii U Boot Sound */ + + meta_ADX_03, /* CRI ADX "type 03" */ + meta_ADX_04, /* CRI ADX "type 04" */ + meta_ADX_05, /* CRI ADX "type 05" */ + meta_AIX, /* CRI AIX */ + meta_AAX, /* CRI AAX */ + meta_UTF_DSP, /* CRI ADPCM_WII, like AAX with DSP */ + + meta_DTK, + meta_RSF, + meta_HALPST, /* HAL Labs HALPST */ + meta_GCSW, /* GCSW (PCM) */ + meta_CAF, /* tri-Crescendo CAF */ + meta_MYSPD, /* U-Sing .myspd */ + meta_HIS, /* Her Ineractive .his */ + meta_BNSF, /* Bandai Namco Sound Format */ + + meta_XA, /* CD-ROM XA */ + meta_ADS, + meta_NPS, + meta_RXWS, + meta_RAW_INT, + meta_EXST, + meta_SVAG_KCET, + meta_PS_HEADERLESS, /* headerless PS-ADPCM */ + meta_MIB_MIH, + meta_PS2_MIC, /* KOEI MIC File */ + meta_PS2_VAGi, /* VAGi Interleaved File */ + meta_PS2_VAGp, /* VAGp Mono File */ + meta_PS2_pGAV, /* VAGp with Little Endian Header */ + meta_PS2_VAGp_AAAP, /* Acclaim Austin Audio VAG header */ + meta_SEB, + meta_STR_WAV, /* Blitz Games STR+WAV files */ + meta_ILD, + meta_PS2_PNB, /* PsychoNauts Bgm File */ + meta_VPK, /* VPK Audio File */ + meta_PS2_BMDX, /* Beatmania thing */ + meta_PS2_IVB, /* Langrisser 3 IVB */ + meta_PS2_SND, /* some Might & Magics SSND header */ + meta_SVS, /* Square SVS */ + meta_XSS, /* Dino Crisis 3 */ + meta_SL3, /* Test Drive Unlimited */ + meta_HGC1, /* Knights of the Temple 2 */ + meta_AUS, /* Various Capcom games */ + meta_RWS, /* RenderWare games (only when using RW Audio middleware) */ + meta_FSB1, /* FMOD Sample Bank, version 1 */ + meta_FSB2, /* FMOD Sample Bank, version 2 */ + meta_FSB3, /* FMOD Sample Bank, version 3.0/3.1 */ + meta_FSB4, /* FMOD Sample Bank, version 4 */ + meta_FSB5, /* FMOD Sample Bank, version 5 */ + meta_RWX, /* Air Force Delta Storm (XBOX) */ + meta_XWB, /* Microsoft XACT framework (Xbox, X360, Windows) */ + meta_PS2_XA30, /* Driver - Parallel Lines (PS2) */ + meta_MUSC, /* Krome PS2 games */ + meta_MUSX, + meta_LEG, /* Legaia 2 [no header_id] */ + meta_FILP, /* Resident Evil - Dead Aim */ + meta_IKM, + meta_STER, + meta_BG00, /* Ibara, Mushihimesama */ + meta_PS2_RSTM, /* Midnight Club 3 */ + meta_PS2_KCES, /* Dance Dance Revolution */ + meta_HXD, + meta_VSV, + meta_SCD_PCM, /* Lunar - Eternal Blue */ + meta_PS2_PCM, /* Konami KCEJ East: Ephemeral Fantasia, Yu-Gi-Oh! The Duelists of the Roses, 7 Blades */ + meta_PS2_RKV, /* Legacy of Kain - Blood Omen 2 (PS2) */ + meta_PS2_VAS, /* Pro Baseball Spirits 5 */ + meta_PS2_ENTH, /* Enthusia */ + meta_SDT, /* Baldur's Gate - Dark Alliance */ + meta_NGC_TYDSP, /* Ty - The Tasmanian Tiger */ + meta_DC_STR, /* SEGA Stream Asset Builder */ + meta_DC_STR_V2, /* variant of SEGA Stream Asset Builder */ + meta_NGC_BH2PCM, /* Bio Hazard 2 */ + meta_SAP, + meta_DC_IDVI, /* Eldorado Gate */ + meta_KRAW, /* Geometry Wars - Galaxies */ + meta_PS2_OMU, /* PS2 Int file with Header */ + meta_PS2_XA2, /* XG3 Extreme-G Racing */ + meta_NUB, + meta_IDSP_NL, /* Mario Strikers Charged (Wii) */ + meta_IDSP_IE, /* Defencer (GC) */ + meta_SPT_SPD, /* Various (SPT+SPT DSP) */ + meta_ISH_ISD, /* Various (ISH+ISD DSP) */ + meta_GSP_GSB, /* Tecmo games (Super Swing Golf 1 & 2, Quamtum Theory) */ + meta_YDSP, /* WWE Day of Reckoning */ + meta_FFCC_STR, /* Final Fantasy: Crystal Chronicles */ + meta_UBI_JADE, /* Beyond Good & Evil, Rayman Raving Rabbids */ + meta_GCA, /* Metal Slug Anthology */ + meta_NGC_SSM, /* Golden Gashbell Full Power */ + meta_PS2_JOE, /* Wall-E / Pixar games */ + meta_NGC_YMF, /* WWE WrestleMania X8 */ + meta_SADL, + meta_PS2_CCC, /* Tokyo Xtreme Racer DRIFT 2 */ + meta_FAG, /* Jackie Chan - Stuntmaster */ + meta_PS2_MIHB, /* Merged MIH+MIB */ + meta_NGC_PDT, /* Mario Party 6 */ + meta_DC_ASD, /* Miss Moonligh */ + meta_NAOMI_SPSD, /* Guilty Gear X */ + meta_RSD, + meta_PS2_ASS, /* ASS */ + meta_SEG, /* Eragon */ + meta_NDS_STRM_FFTA2, /* Final Fantasy Tactics A2 */ + meta_KNON, + meta_ZWDSP, /* Zack and Wiki */ + meta_VGS, /* Guitar Hero Encore - Rocks the 80s */ + meta_DCS_WAV, + meta_SMP, + meta_WII_SNG, /* Excite Trucks */ + meta_MUL, + meta_SAT_BAKA, /* Crypt Killer */ + meta_VSF, + meta_PS2_VSF_TTA, /* Tiny Toon Adventures: Defenders of the Universe */ + meta_ADS_MIDWAY, + meta_PS2_SPS, /* Ape Escape 2 */ + meta_PS2_XA2_RRP, /* RC Revenge Pro */ + meta_NGC_DSP_KONAMI, /* Konami DSP header, found in various games */ + meta_UBI_CKD, /* Ubisoft CKD RIFF header (Rayman Origins Wii) */ + meta_RAW_WAVM, + meta_WVS, + meta_XBOX_MATX, /* XBOX MATX */ + meta_XMU, + meta_XVAS, + meta_EA_SCHL, /* Electronic Arts SCHl with variable header */ + meta_EA_SCHL_fixed, /* Electronic Arts SCHl with fixed header */ + meta_EA_BNK, /* Electronic Arts BNK */ + meta_EA_1SNH, /* Electronic Arts 1SNh/EACS */ + meta_EA_EACS, + meta_RAW_PCM, + meta_GENH, /* generic header */ + meta_AIFC, /* Audio Interchange File Format AIFF-C */ + meta_AIFF, /* Audio Interchange File Format */ + meta_STR_SNDS, /* .str with SNDS blocks and SHDR header */ + meta_WS_AUD, /* Westwood Studios .aud */ + meta_WS_AUD_old, /* Westwood Studios .aud, old style */ + meta_RIFF_WAVE, /* RIFF, for WAVs */ + meta_RIFF_WAVE_POS, /* .wav + .pos for looping (Ys Complete PC) */ + meta_RIFF_WAVE_labl, /* RIFF w/ loop Markers in LIST-adtl-labl */ + meta_RIFF_WAVE_smpl, /* RIFF w/ loop data in smpl chunk */ + meta_RIFF_WAVE_wsmp, /* RIFF w/ loop data in wsmp chunk */ + meta_RIFF_WAVE_MWV, /* .mwv RIFF w/ loop data in ctrl chunk pflt */ + meta_RIFX_WAVE, /* RIFX, for big-endian WAVs */ + meta_RIFX_WAVE_smpl, /* RIFX w/ loop data in smpl chunk */ + meta_XNB, /* XNA Game Studio 4.0 */ + meta_PC_MXST, /* Lego Island MxSt */ + meta_SAB, /* Worms 4 Mayhem SAB+SOB file */ + meta_NWA, /* Visual Art's NWA */ + meta_NWA_NWAINFOINI, /* Visual Art's NWA w/ NWAINFO.INI for looping */ + meta_NWA_GAMEEXEINI, /* Visual Art's NWA w/ Gameexe.ini for looping */ + meta_SAT_DVI, /* Konami KCE Nagoya DVI (SAT games) */ + meta_DC_KCEY, /* Konami KCE Yokohama KCEYCOMP (DC games) */ + meta_ACM, /* InterPlay ACM header */ + meta_MUS_ACM, /* MUS playlist of InterPlay ACM files */ + meta_DEC, /* Falcom PC games (Xanadu Next, Gurumin) */ + meta_VS, /* Men in Black .vs */ + meta_FFXI_BGW, /* FFXI (PC) BGW */ + meta_FFXI_SPW, /* FFXI (PC) SPW */ + meta_STS, + meta_PS2_P2BT, /* Pop'n'Music 7 Audio File */ + meta_PS2_GBTS, /* Pop'n'Music 9 Audio File */ + meta_NGC_DSP_IADP, /* Gamecube Interleave DSP */ + meta_PS2_TK5, /* Tekken 5 Stream Files */ + meta_PS2_MCG, /* Gunvari MCG Files (was name .GCM on disk) */ + meta_ZSD, /* Dragon Booster ZSD */ + meta_REDSPARK, /* "RedSpark" RSD (MadWorld) */ + meta_IVAUD, /* .ivaud GTA IV */ + meta_NDS_HWAS, /* Spider-Man 3, Tony Hawk's Downhill Jam, possibly more... */ + meta_NGC_LPS, /* Rave Master (Groove Adventure Rave)(GC) */ + meta_NAOMI_ADPCM, /* NAOMI/NAOMI2 ARcade games */ + meta_SD9, /* beatmaniaIIDX16 - EMPRESS (Arcade) */ + meta_2DX9, /* beatmaniaIIDX16 - EMPRESS (Arcade) */ + meta_PS2_VGV, /* Rune: Viking Warlord */ + meta_GCUB, + meta_MAXIS_XA, /* Sim City 3000 (PC) */ + meta_NGC_SCK_DSP, /* Scorpion King (NGC) */ + meta_CAFF, /* iPhone .caf */ + meta_EXAKT_SC, /* Activision EXAKT .SC (PS2) */ + meta_WII_WAS, /* DiRT 2 (WII) */ + meta_PONA_3DO, /* Policenauts (3DO) */ + meta_PONA_PSX, /* Policenauts (PSX) */ + meta_XBOX_HLWAV, /* Half Life 2 (XBOX) */ + meta_AST_MV, + meta_AST_MMV, + meta_DMSG, /* Nightcaster II - Equinox (XBOX) */ + meta_NGC_DSP_AAAP, /* Turok: Evolution (NGC), Vexx (NGC) */ + meta_PS2_WB, /* Shooting Love. ~TRIZEAL~ */ + meta_S14, /* raw Siren 14, 24kbit mono */ + meta_SSS, /* raw Siren 14, 48kbit stereo */ + meta_PS2_GCM, /* NamCollection */ + meta_PS2_SMPL, /* Homura */ + meta_PS2_MSA, /* Psyvariar -Complete Edition- */ + meta_PS2_VOI, /* RAW Danger (Zettaizetsumei Toshi 2 - Itetsuita Kiokutachi) [PS2] */ + meta_P3D, /* Prototype P3D */ + meta_PS2_TK1, /* Tekken (NamCollection) */ + meta_NGC_RKV, /* Legacy of Kain - Blood Omen 2 (GC) */ + meta_DSP_DDSP, /* Various (2 dsp files stuck together */ + meta_NGC_DSP_MPDS, /* Big Air Freestyle, Terminator 3 */ + meta_DSP_STR_IG, /* Micro Machines, Superman Superman: Shadow of Apokolis */ + meta_EA_SWVR, /* Future Cop L.A.P.D., Freekstyle */ + meta_PS2_B1S, /* 7 Wonders of the ancient world */ + meta_PS2_WAD, /* The golden Compass */ + meta_DSP_XIII, /* XIII, possibly more (Ubisoft header???) */ + meta_DSP_CABELAS, /* Cabelas games */ + meta_PS2_ADM, /* Dragon Quest V (PS2) */ + meta_LPCM_SHADE, + meta_DSP_BDSP, /* Ah! My Goddess */ + meta_PS2_VMS, /* Autobahn Raser - Police Madness */ + meta_XAU, /* XPEC Entertainment (Beat Down (PS2 Xbox), Spectral Force Chronicle (PS2)) */ + meta_GH3_BAR, /* Guitar Hero III Mobile .bar */ + meta_FFW, /* Freedom Fighters [NGC] */ + meta_DSP_DSPW, /* Sengoku Basara 3 [WII] */ + meta_PS2_JSTM, /* Tantei Jinguji Saburo - Kind of Blue (PS2) */ + meta_SQEX_SCD, /* Square-Enix SCD */ + meta_NGC_NST_DSP, /* Animaniacs [NGC] */ + meta_BAF, /* Bizarre Creations (Blur, James Bond) */ + meta_XVAG, /* Ratchet & Clank Future: Quest for Booty (PS3) */ + meta_CPS, + meta_MSF, + meta_PS3_PAST, /* Bakugan Battle Brawlers (PS3) */ + meta_SGXD, /* Sony: Folklore, Genji, Tokyo Jungle (PS3), Brave Story, Kurohyo (PSP) */ + meta_WII_RAS, /* Donkey Kong Country Returns (Wii) */ + meta_SPM, + meta_VGS_PS, + meta_PS2_IAB, /* Ueki no Housoku - Taosu ze Robert Juudan!! (PS2) */ + meta_VS_STR, /* The Bouncer */ + meta_LSF_N1NJ4N, /* .lsf n1nj4n Fastlane Street Racing (iPhone) */ + meta_XWAV, + meta_RAW_SNDS, + meta_PS2_WMUS, /* The Warriors (PS2) */ + meta_HYPERSCAN_KVAG, /* Hyperscan KVAG/BVG */ + meta_IOS_PSND, /* Crash Bandicoot Nitro Kart 2 (iOS) */ + meta_BOS_ADP, + meta_QD_ADP, + meta_EB_SFX, /* Excitebots .sfx */ + meta_EB_SF0, /* Excitebots .sf0 */ + meta_MTAF, + meta_PS2_VAG1, /* Metal Gear Solid 3 VAG1 */ + meta_PS2_VAG2, /* Metal Gear Solid 3 VAG2 */ + meta_ALP, + meta_WPD, /* Shuffle! (PC) */ + meta_MN_STR, /* Mini Ninjas (PC/PS3/WII) */ + meta_MSS, /* Guerilla: ShellShock Nam '67 (PS2/Xbox), Killzone (PS2) */ + meta_PS2_HSF, /* Lowrider (PS2) */ + meta_IVAG, + meta_PS2_2PFS, /* Konami: Mahoromatic: Moetto - KiraKira Maid-San, GANTZ (PS2) */ + meta_PS2_VBK, /* Disney's Stitch - Experiment 626 */ + meta_OTM, /* Otomedius (Arcade) */ + meta_CSTM, /* Nintendo/HAL Labs NW4C Soundmaker CSTM (Century Stream) */ + meta_FSTM, /* Nintendo/HAL Labs NW4F Soundmaker FSTM (caFe? Stream) */ + meta_IDSP_NAMCO, + meta_KT_WIIBGM, /* Koei Tecmo WiiBGM */ + meta_KTSS, /* Koei Tecmo Nintendo Stream (KNS) */ + meta_MCA, /* Capcom MCA "MADP" */ + meta_XB3D_ADX, /* Xenoblade Chronicles 3D ADX */ + meta_HCA, /* CRI HCA */ + meta_SVAG_SNK, + meta_PS2_VDS_VDM, /* Graffiti Kingdom */ + meta_FFMPEG, + meta_FFMPEG_faulty, + meta_CXS, + meta_AKB, + meta_PASX, + meta_XMA_RIFF, + meta_ASTB, + meta_WWISE_RIFF, /* Audiokinetic Wwise RIFF/RIFX */ + meta_UBI_RAKI, /* Ubisoft RAKI header (Rayman Legends, Just Dance 2017) */ + meta_SXD, /* Sony SXD (Gravity Rush, Freedom Wars PSV) */ + meta_OGL, /* Shin'en Wii/WiiU (Jett Rocket (Wii), FAST Racing NEO (WiiU)) */ + meta_MC3, /* Paradigm games (T3 PS2, MX Rider PS2, MI: Operation Surma PS2) */ + meta_GHS, + meta_AAC_TRIACE, + meta_MTA2, + meta_NGC_ULW, /* Burnout 1 (GC only) */ + meta_XA_XA30, + meta_XA_04SW, + meta_TXTH, /* generic text header */ + meta_SK_AUD, /* Silicon Knights .AUD (Eternal Darkness GC) */ + meta_AHX, + meta_STM, /* Angel Studios/Rockstar San Diego Games */ + meta_BINK, /* RAD Game Tools BINK audio/video */ + meta_EA_SNU, /* Electronic Arts SNU (Dead Space) */ + meta_AWC, /* Rockstar AWC (GTA5, RDR) */ + meta_OPUS, /* Nintendo Opus [Lego City Undercover (Switch)] */ + meta_RAW_AL, + meta_PC_AST, /* Dead Rising (PC) */ + meta_NAAC, /* Namco AAC (3DS) */ + meta_UBI_SB, /* Ubisoft banks */ + meta_EZW, /* EZ2DJ (Arcade) EZWAV */ + meta_VXN, /* Gameloft mobile games */ + meta_EA_SNR_SNS, /* Electronic Arts SNR+SNS (Burnout Paradise) */ + meta_EA_SPS, /* Electronic Arts SPS (Burnout Crash) */ + meta_VID1, + meta_PC_FLX, /* Ultima IX PC */ + meta_MOGG, /* Harmonix Music Systems MOGG Vorbis */ + meta_OGG_VORBIS, /* Ogg Vorbis */ + meta_OGG_SLI, /* Ogg Vorbis file w/ companion .sli for looping */ + meta_OPUS_SLI, /* Ogg Opus file w/ companion .sli for looping */ + meta_OGG_SFL, /* Ogg Vorbis file w/ .sfl (RIFF SFPL) for looping */ + meta_OGG_KOVS, /* Ogg Vorbis with header and encryption (Koei Tecmo Games) */ + meta_OGG_encrypted, /* Ogg Vorbis with encryption */ + meta_KMA9, /* Koei Tecmo [Nobunaga no Yabou - Souzou (Vita)] */ + meta_XWC, /* Starbreeze games */ + meta_SQEX_SAB, /* Square-Enix newest middleware (sound) */ + meta_SQEX_MAB, /* Square-Enix newest middleware (music) */ + meta_WAF, /* KID WAF [Ever 17 (PC)] */ + meta_WAVE, /* EngineBlack games [Mighty Switch Force! (3DS)] */ + meta_WAVE_segmented, /* EngineBlack games, segmented [Shantae and the Pirate's Curse (PC)] */ + meta_SMV, /* Cho Aniki Zero (PSP) */ + meta_NXAP, /* Nex Entertainment games [Time Crisis 4 (PS3), Time Crisis Razing Storm (PS3)] */ + meta_EA_WVE_AU00, /* Electronic Arts PS movies [Future Cop - L.A.P.D. (PS), Supercross 2000 (PS)] */ + meta_EA_WVE_AD10, /* Electronic Arts PS movies [Wing Commander 3/4 (PS)] */ + meta_STHD, /* STHD .stx [Kakuto Chojin (Xbox)] */ + meta_MP4, /* MP4/AAC */ + meta_PCM_SRE, /* .PCM+SRE [Viewtiful Joe (PS2)] */ + meta_DSP_MCADPCM, /* Skyrim (Switch) */ + meta_UBI_LYN, /* Ubisoft LyN engine [The Adventures of Tintin (multi)] */ + meta_MSB_MSH, /* sfx companion of MIH+MIB */ + meta_TXTP, /* generic text playlist */ + meta_SMC_SMH, /* Wangan Midnight (System 246) */ + meta_PPST, /* PPST [Parappa the Rapper (PSP)] */ + meta_SPS_N1, + meta_UBI_BAO, /* Ubisoft BAO */ + meta_DSP_SWITCH_AUDIO, /* Gal Gun 2 (Switch) */ + meta_H4M, /* Hudson HVQM4 video [Resident Evil 0 (GC), Tales of Symphonia (GC)] */ + meta_ASF, /* Argonaut ASF [Croc 2 (PC)] */ + meta_XMD, /* Konami XMD [Silent Hill 4 (Xbox), Castlevania: Curse of Darkness (Xbox)] */ + meta_CKS, /* Cricket Audio stream [Part Time UFO (Android), Mega Man 1-6 (Android)] */ + meta_CKB, /* Cricket Audio bank [Fire Emblem Heroes (Android), Mega Man 1-6 (Android)] */ + meta_WV6, /* Gorilla Systems PC games */ + meta_WAVEBATCH, /* Firebrand Games */ + meta_HD3_BD3, /* Sony PS3 bank */ + meta_BNK_SONY, /* Sony Scream Tool bank */ + meta_SSCF, + meta_DSP_VAG, /* Penny-Punching Princess (Switch) sfx */ + meta_DSP_ITL, /* Charinko Hero (GC) */ + meta_A2M, /* Scooby-Doo! Unmasked (PS2) */ + meta_AHV, /* Headhunter (PS2) */ + meta_MSV, + meta_SDF, + meta_SVG, /* Hunter - The Reckoning - Wayward (PS2) */ + meta_VIS, /* AirForce Delta Strike (PS2) */ + meta_VAI, /* Ratatouille (GC) */ + meta_AIF_ASOBO, /* Ratatouille (PC) */ + meta_AO, /* Cloudphobia (PC) */ + meta_APC, /* MegaRace 3 (PC) */ + meta_WV2, /* Slave Zero (PC) */ + meta_XAU_KONAMI, /* Yu-Gi-Oh - The Dawn of Destiny (Xbox) */ + meta_DERF, /* Stupid Invaders (PC) */ + meta_SADF, + meta_UTK, + meta_NXA, + meta_ADPCM_CAPCOM, + meta_UE4OPUS, + meta_XWMA, + meta_VA3, /* DDR Supernova 2 AC */ + meta_XOPUS, + meta_VS_SQUARE, + meta_NWAV, + meta_XPCM, + meta_MSF_TAMASOFT, + meta_XPS_DAT, + meta_ZSND, + meta_DSP_ADPY, + meta_DSP_ADPX, + meta_OGG_OPUS, + meta_IMC, + meta_GIN, + meta_DSF, + meta_208, + meta_DSP_DS2, + meta_MUS_VC, + meta_STRM_ABYLIGHT, + meta_MSF_KONAMI, + meta_XWMA_KONAMI, + meta_9TAV, + meta_BWAV, + meta_RAD, + meta_SMACKER, + meta_MZRT, + meta_XAVS, + meta_PSF, + meta_DSP_ITL_i, + meta_IMA, + meta_XWV_VALVE, + meta_UBI_HX, + meta_BMP_KONAMI, + meta_ISB, + meta_XSSB, + meta_XMA_UE3, + meta_FWSE, + meta_FDA, + meta_TGC, + meta_KWB, + meta_LRMD, + meta_WWISE_FX, + meta_DIVA, + meta_IMUSE, + meta_KTSR, + meta_KAT, + meta_PCM_SUCCESS, + meta_ADP_KONAMI, + meta_SDRH, + meta_WADY, + meta_DSP_SQEX, + meta_DSP_WIIVOICE, + meta_SBK, + meta_DSP_WIIADPCM, + meta_DSP_CWAC, + meta_COMPRESSWAVE, + meta_KTAC, + meta_MJB_MJH, + meta_BSNF, + meta_TAC, + meta_IDSP_TOSE, + meta_DSP_KWA, + meta_OGV_3RDEYE, + meta_PIFF_TPCM, + meta_WXD_WXH, + meta_BNK_RELIC, + meta_XSH_XSD_XSS, + meta_PSB, + meta_LOPU_FB, + meta_LPCM_FB, + meta_WBK, + meta_WBK_NSLB, + meta_DSP_APEX, + meta_MPEG, + meta_SSPF, + meta_S3V, + meta_ESF, + meta_ADM3, + meta_TT_AD, + meta_SNDZ, + meta_VAB, + meta_BIGRP, +} \ No newline at end of file diff --git a/VG Music Studio - Core/Config.cs b/VG Music Studio - Core/Config.cs old mode 100644 new mode 100755 index 2f26ad57..532b8059 --- a/VG Music Studio - Core/Config.cs +++ b/VG Music Studio - Core/Config.cs @@ -7,86 +7,109 @@ namespace Kermalis.VGMusicStudio.Core; public abstract class Config : IDisposable { - public readonly struct Song - { - public readonly int Index; - public readonly string Name; + public virtual byte[]? ROM { get; } + public int[]? SongTableOffset { get; internal set; } + public virtual HSLColor[]? Colors { get; protected set; } - public Song(int index, string name) - { - Index = index; - Name = name; - } + public readonly struct Song + { + public readonly int Index; + public readonly string 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 Song(int 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 static bool operator ==(Song left, Song right) + { + return left.Equals(right); + } + public static bool operator !=(Song left, Song right) + { + return !(left == right); + } - public Playlist(string name, List songs) - { - Name = name; - Songs = songs; - } + 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 InternalSongName + { + public string Name; + public List Songs; - public override string ToString() - { - int num = Songs.Count; - return string.Format("{0} - ({1:N0} {2})", Name, num, LanguageUtils.HandlePlural(num, Strings.Song_s_)); - } - } + public InternalSongName(string name, List songs) + { + Name = name; + Songs = songs; + } - public readonly List Playlists; + public override string ToString() + { + int num = Songs.Count; + return string.Format("{0} - ({1:N0} {2})", num, LanguageUtils.HandlePlural(num, Strings.Song_s_)); + } + } + public sealed class Playlist + { + public string Name; + public List Songs; - protected Config() - { - Playlists = new List(); - } + public Playlist(string name, List songs) + { + Name = name; + Songs = songs; + } - public bool TryGetFirstSong(int index, out Song song) - { - foreach (Playlist p in Playlists) - { - foreach (Song s in p.Songs) - { - if (s.Index == index) - { - song = s; - return true; - } - } - } - song = default; - return false; - } + public override string ToString() + { + int num = Songs.Count; + return string.Format("{0} - ({1:N0} {2})", Name, num, LanguageUtils.HandlePlural(num, Strings.Song_s_)); + } + } - public abstract string GetGameName(); - public abstract string GetSongName(int index); + public readonly List InternalSongNames; + public readonly List Playlists; - public virtual void Dispose() - { - // - } + protected Config() + { + InternalSongNames = new List(); + Playlists = new List(); + } + + public bool TryGetFirstSong(int index, out Song song) + { + foreach (Playlist p in Playlists) + { + foreach (Song s in p.Songs) + { + if (s.Index == index) + { + song = s; + return true; + } + } + } + song = default; + return false; + } + + public abstract string GetGameName(); + public abstract string GetSongName(int index); + + public virtual void Dispose() + { + // + } } diff --git a/VG Music Studio - Core/Config.yaml b/VG Music Studio - Core/Config.yaml index c7c809c9..abc12163 100644 --- a/VG Music Studio - Core/Config.yaml +++ b/VG Music Studio - Core/Config.yaml @@ -1,5 +1,4 @@ TaskbarProgress: True # True or False # Whether the taskbar will show the song progress -RefreshRate: 30 # RefreshRate >= 1 and RefreshRate <= 1000 # How many times a second the visual updates CenterIndicators: False # True or False # Whether lines should be drawn for the center of panpot in the visual PanpotIndicators: False # True or False # Whether lines should be drawn for the track's panpot in the visual PlaylistMode: "Random" # "Random" or "Sequential" # The way the playlist will behave diff --git a/VG Music Studio - Core/Dependencies/KMIDI.deps.json b/VG Music Studio - Core/Dependencies/KMIDI.deps.json deleted file mode 100644 index 7feb759b..00000000 --- a/VG Music Studio - Core/Dependencies/KMIDI.deps.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "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 deleted file mode 100644 index 372b9e32..00000000 Binary files a/VG Music Studio - Core/Dependencies/KMIDI.dll and /dev/null differ diff --git a/VG Music Studio - Core/Dependencies/KMIDI.xml b/VG Music Studio - Core/Dependencies/KMIDI.xml deleted file mode 100644 index 23588353..00000000 --- a/VG Music Studio - Core/Dependencies/KMIDI.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - KMIDI - - - - Includes the end of track event - - - - - - If there are other events at , will be inserted after them. - - - Length 4 - - - Contains a single multi-channel track - - - Contains one or more simultaneous tracks - - - Contains one or more independent single-track patterns - - - Used with - - - Used with - - - Reserved for ASCII treatment - - - Reserved for ASCII treatment - - - Reserved for ASCII treatment - - - Reserved for ASCII treatment - - - Reserved for ASCII treatment - - - Reserved for ASCII treatment - - - Not optional - - - Used with - - - Used with - - - Used with - - - How many ticks are between this event and the previous one. If this is the first event in the track, then it is equal to - - - Returns a value in the range [-8_192, 8_191] - - - - - - Middle C - - - diff --git a/VG Music Studio - Core/Engine.cs b/VG Music Studio - Core/Engine.cs old mode 100644 new mode 100755 index a37f0e03..b08e0a8c --- a/VG Music Studio - Core/Engine.cs +++ b/VG Music Studio - Core/Engine.cs @@ -4,17 +4,23 @@ namespace Kermalis.VGMusicStudio.Core; public abstract class Engine : IDisposable { - public static Engine? Instance { get; protected set; } + public static Engine? Instance { get; protected set; } - public abstract Config Config { get; } - public abstract Mixer Mixer { get; } - public abstract Player Player { get; } + public abstract Config Config { get; } + public abstract Mixer Mixer { get; } + public abstract Player Player { get; } - public virtual void Dispose() - { - Config.Dispose(); - Mixer.Dispose(); - Player.Dispose(); - Instance = null; - } + public abstract bool IsFileSystemFormat { get; } + + public virtual ICommand[] GetCommands() { return new ICommand[1]; } + + public abstract void Reload(); + public virtual void Dispose() + { + Config.Dispose(); + Mixer.Dispose(); + Player.Dispose(); + Instance = null; + GC.SuppressFinalize(this); + } } diff --git a/VG Music Studio - Core/Formats/Enumerations/WaveEncodingEnums.cs b/VG Music Studio - Core/Formats/Enumerations/WaveEncodingEnums.cs new file mode 100644 index 00000000..7cace6b9 --- /dev/null +++ b/VG Music Studio - Core/Formats/Enumerations/WaveEncodingEnums.cs @@ -0,0 +1,455 @@ +#region Original License Info +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Copyright 2020 Mark Heath + * + * 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 + +// From NAudio.Wave, modified by Platinum Lucario for use in VG Music Studio + +namespace Kermalis.VGMusicStudio.Core.Formats; + +/// +/// Summary description for WaveFormatEncoding. +/// +public enum WaveEncoding : ushort +{ + /// WAVE_FORMAT_UNKNOWN, Microsoft Corporation + Unknown = 0x0000, + + /// WAVE_FORMAT_PCM Microsoft Corporation + Pcm = 0x0001, + + /// WAVE_FORMAT_PCM4 Microsoft Corporation + Pcm4 = Pcm, + + /// WAVE_FORMAT_PCM8 Microsoft Corporation + Pcm8 = Pcm, + + /// WAVE_FORMAT_PCM16 Microsoft Corporation + Pcm16 = Pcm, + + /// WAVE_FORMAT_ADPCM Microsoft Corporation + Adpcm = 0x0002, + + /// WAVE_FORMAT_IEEE_FLOAT Microsoft Corporation + IeeeFloat = 0x0003, + + /// WAVE_FORMAT_VSELP Compaq Computer Corp. + Vselp = 0x0004, + + /// WAVE_FORMAT_IBM_CVSD IBM Corporation + IbmCvsd = 0x0005, + + /// WAVE_FORMAT_ALAW Microsoft Corporation + ALaw = 0x0006, + + /// WAVE_FORMAT_MULAW Microsoft Corporation + MuLaw = 0x0007, + + /// WAVE_FORMAT_DTS Microsoft Corporation + Dts = 0x0008, + + /// WAVE_FORMAT_DRM Microsoft Corporation + Drm = 0x0009, + + /// WAVE_FORMAT_WMAVOICE9 + WmaVoice9 = 0x000A, + + /// WAVE_FORMAT_OKI_ADPCM OKI + OkiAdpcm = 0x0010, + + /// WAVE_FORMAT_DVI_ADPCM Intel Corporation + DviAdpcm = 0x0011, + + /// WAVE_FORMAT_IMA_ADPCM Intel Corporation + ImaAdpcm = DviAdpcm, + + /// WAVE_FORMAT_MEDIASPACE_ADPCM Videologic + MediaspaceAdpcm = 0x0012, + + /// WAVE_FORMAT_SIERRA_ADPCM Sierra Semiconductor Corp + SierraAdpcm = 0x0013, + + /// WAVE_FORMAT_G723_ADPCM Antex Electronics Corporation + G723Adpcm = 0x0014, + + /// WAVE_FORMAT_DIGISTD DSP Solutions, Inc. + DigiStd = 0x0015, + + /// WAVE_FORMAT_DIGIFIX DSP Solutions, Inc. + DigiFix = 0x0016, + + /// WAVE_FORMAT_DIALOGIC_OKI_ADPCM Dialogic Corporation + DialogicOkiAdpcm = 0x0017, + + /// WAVE_FORMAT_MEDIAVISION_ADPCM Media Vision, Inc. + MediaVisionAdpcm = 0x0018, + + /// WAVE_FORMAT_CU_CODEC Hewlett-Packard Company + CUCodec = 0x0019, + + /// WAVE_FORMAT_YAMAHA_ADPCM Yamaha Corporation of America + YamahaAdpcm = 0x0020, + + /// WAVE_FORMAT_SONARC Speech Compression + SonarC = 0x0021, + + /// WAVE_FORMAT_DSPGROUP_TRUESPEECH DSP Group, Inc + DspGroupTrueSpeech = 0x0022, + + /// WAVE_FORMAT_ECHOSC1 Echo Speech Corporation + EchoSpeechCorporation1 = 0x0023, + + /// WAVE_FORMAT_AUDIOFILE_AF36, Virtual Music, Inc. + AudioFileAf36 = 0x0024, + + /// WAVE_FORMAT_APTX Audio Processing Technology + Aptx = 0x0025, + + /// WAVE_FORMAT_AUDIOFILE_AF10, Virtual Music, Inc. + AudioFileAf10 = 0x0026, + + /// WAVE_FORMAT_PROSODY_1612, Aculab plc + Prosody1612 = 0x0027, + + /// WAVE_FORMAT_LRC, Merging Technologies S.A. + Lrc = 0x0028, + + /// WAVE_FORMAT_DOLBY_AC2, Dolby Laboratories + DolbyAc2 = 0x0030, + + /// WAVE_FORMAT_GSM610, Microsoft Corporation + Gsm610 = 0x0031, + + /// WAVE_FORMAT_MSNAUDIO, Microsoft Corporation + MsnAudio = 0x0032, + + /// WAVE_FORMAT_ANTEX_ADPCME, Antex Electronics Corporation + AntexAdpcme = 0x0033, + + /// WAVE_FORMAT_CONTROL_RES_VQLPC, Control Resources Limited + ControlResVqlpc = 0x0034, + + /// WAVE_FORMAT_DIGIREAL, DSP Solutions, Inc. + DigiReal = 0x0035, + + /// WAVE_FORMAT_DIGIADPCM, DSP Solutions, Inc. + DigiAdpcm = 0x0036, + + /// WAVE_FORMAT_CONTROL_RES_CR10, Control Resources Limited + ControlResCr10 = 0x0037, + + /// + WAVE_FORMAT_NMS_VBXADPCM = 0x0038, // Natural MicroSystems + /// + WAVE_FORMAT_CS_IMAADPCM = 0x0039, // Crystal Semiconductor IMA ADPCM + /// + WAVE_FORMAT_ECHOSC3 = 0x003A, // Echo Speech Corporation + /// + WAVE_FORMAT_ROCKWELL_ADPCM = 0x003B, // Rockwell International + /// + WAVE_FORMAT_ROCKWELL_DIGITALK = 0x003C, // Rockwell International + /// + WAVE_FORMAT_XEBEC = 0x003D, // Xebec Multimedia Solutions Limited + /// + WAVE_FORMAT_G721_ADPCM = 0x0040, // Antex Electronics Corporation + /// + WAVE_FORMAT_G728_CELP = 0x0041, // Antex Electronics Corporation + /// + WAVE_FORMAT_MSG723 = 0x0042, // Microsoft Corporation + /// WAVE_FORMAT_MPEG, Microsoft Corporation + Mpeg = 0x0050, + + /// + WAVE_FORMAT_RT24 = 0x0052, // InSoft, Inc. + /// + WAVE_FORMAT_PAC = 0x0053, // InSoft, Inc. + /// WAVE_FORMAT_MPEGLAYER3, ISO/MPEG Layer3 Format Tag + MpegLayer3 = 0x0055, + + /// + WAVE_FORMAT_LUCENT_G723 = 0x0059, // Lucent Technologies + /// + WAVE_FORMAT_CIRRUS = 0x0060, // Cirrus Logic + /// + WAVE_FORMAT_ESPCM = 0x0061, // ESS Technology + /// + WAVE_FORMAT_VOXWARE = 0x0062, // Voxware Inc + /// + WAVE_FORMAT_CANOPUS_ATRAC = 0x0063, // Canopus, co., Ltd. + /// + WAVE_FORMAT_G726_ADPCM = 0x0064, // APICOM + /// + WAVE_FORMAT_G722_ADPCM = 0x0065, // APICOM + /// + WAVE_FORMAT_DSAT_DISPLAY = 0x0067, // Microsoft Corporation + /// + WAVE_FORMAT_VOXWARE_BYTE_ALIGNED = 0x0069, // Voxware Inc + /// + WAVE_FORMAT_VOXWARE_AC8 = 0x0070, // Voxware Inc + /// + WAVE_FORMAT_VOXWARE_AC10 = 0x0071, // Voxware Inc + /// + WAVE_FORMAT_VOXWARE_AC16 = 0x0072, // Voxware Inc + /// + WAVE_FORMAT_VOXWARE_AC20 = 0x0073, // Voxware Inc + /// + WAVE_FORMAT_VOXWARE_RT24 = 0x0074, // Voxware Inc + /// + WAVE_FORMAT_VOXWARE_RT29 = 0x0075, // Voxware Inc + /// + WAVE_FORMAT_VOXWARE_RT29HW = 0x0076, // Voxware Inc + /// + WAVE_FORMAT_VOXWARE_VR12 = 0x0077, // Voxware Inc + /// + WAVE_FORMAT_VOXWARE_VR18 = 0x0078, // Voxware Inc + /// + WAVE_FORMAT_VOXWARE_TQ40 = 0x0079, // Voxware Inc + /// + WAVE_FORMAT_SOFTSOUND = 0x0080, // Softsound, Ltd. + /// + WAVE_FORMAT_VOXWARE_TQ60 = 0x0081, // Voxware Inc + /// + WAVE_FORMAT_MSRT24 = 0x0082, // Microsoft Corporation + /// + WAVE_FORMAT_G729A = 0x0083, // AT&T Labs, Inc. + /// + WAVE_FORMAT_MVI_MVI2 = 0x0084, // Motion Pixels + /// + WAVE_FORMAT_DF_G726 = 0x0085, // DataFusion Systems (Pty) (Ltd) + /// + WAVE_FORMAT_DF_GSM610 = 0x0086, // DataFusion Systems (Pty) (Ltd) + /// + WAVE_FORMAT_ISIAUDIO = 0x0088, // Iterated Systems, Inc. + /// + WAVE_FORMAT_ONLIVE = 0x0089, // OnLive! Technologies, Inc. + /// + WAVE_FORMAT_SBC24 = 0x0091, // Siemens Business Communications Sys + /// + WAVE_FORMAT_DOLBY_AC3_SPDIF = 0x0092, // Sonic Foundry + /// + WAVE_FORMAT_MEDIASONIC_G723 = 0x0093, // MediaSonic + /// + WAVE_FORMAT_PROSODY_8KBPS = 0x0094, // Aculab plc + /// + WAVE_FORMAT_ZYXEL_ADPCM = 0x0097, // ZyXEL Communications, Inc. + /// + WAVE_FORMAT_PHILIPS_LPCBB = 0x0098, // Philips Speech Processing + /// + WAVE_FORMAT_PACKED = 0x0099, // Studer Professional Audio AG + /// + WAVE_FORMAT_MALDEN_PHONYTALK = 0x00A0, // Malden Electronics Ltd. + /// WAVE_FORMAT_GSM + Gsm = 0x00A1, + + /// WAVE_FORMAT_G729 + G729 = 0x00A2, + + /// WAVE_FORMAT_G723 + G723 = 0x00A3, + + /// WAVE_FORMAT_ACELP + Acelp = 0x00A4, + + /// + /// WAVE_FORMAT_RAW_AAC1 + /// + RawAac = 0x00FF, + /// + WAVE_FORMAT_RHETOREX_ADPCM = 0x0100, // Rhetorex Inc. + /// + WAVE_FORMAT_IRAT = 0x0101, // BeCubed Software Inc. + /// + WAVE_FORMAT_VIVO_G723 = 0x0111, // Vivo Software + /// + WAVE_FORMAT_VIVO_SIREN = 0x0112, // Vivo Software + /// + WAVE_FORMAT_DIGITAL_G723 = 0x0123, // Digital Equipment Corporation + /// + WAVE_FORMAT_SANYO_LD_ADPCM = 0x0125, // Sanyo Electric Co., Ltd. + /// + WAVE_FORMAT_SIPROLAB_ACEPLNET = 0x0130, // Sipro Lab Telecom Inc. + /// + WAVE_FORMAT_SIPROLAB_ACELP4800 = 0x0131, // Sipro Lab Telecom Inc. + /// + WAVE_FORMAT_SIPROLAB_ACELP8V3 = 0x0132, // Sipro Lab Telecom Inc. + /// + WAVE_FORMAT_SIPROLAB_G729 = 0x0133, // Sipro Lab Telecom Inc. + /// + WAVE_FORMAT_SIPROLAB_G729A = 0x0134, // Sipro Lab Telecom Inc. + /// + WAVE_FORMAT_SIPROLAB_KELVIN = 0x0135, // Sipro Lab Telecom Inc. + /// + WAVE_FORMAT_G726ADPCM = 0x0140, // Dictaphone Corporation + /// + WAVE_FORMAT_QUALCOMM_PUREVOICE = 0x0150, // Qualcomm, Inc. + /// + WAVE_FORMAT_QUALCOMM_HALFRATE = 0x0151, // Qualcomm, Inc. + /// + WAVE_FORMAT_TUBGSM = 0x0155, // Ring Zero Systems, Inc. + /// + WAVE_FORMAT_MSAUDIO1 = 0x0160, // Microsoft Corporation + /// + /// Windows Media Audio, WAVE_FORMAT_WMAUDIO2, Microsoft Corporation + /// + WindowsMediaAudio = 0x0161, + + /// + /// Windows Media Audio Professional WAVE_FORMAT_WMAUDIO3, Microsoft Corporation + /// + WindowsMediaAudioProfessional = 0x0162, + + /// + /// Windows Media Audio Lossless, WAVE_FORMAT_WMAUDIO_LOSSLESS + /// + WindowsMediaAudioLosseless = 0x0163, + + /// + /// Windows Media Audio Professional over SPDIF WAVE_FORMAT_WMASPDIF (0x0164) + /// + WindowsMediaAudioSpdif = 0x0164, + + /// + WAVE_FORMAT_UNISYS_NAP_ADPCM = 0x0170, // Unisys Corp. + /// + WAVE_FORMAT_UNISYS_NAP_ULAW = 0x0171, // Unisys Corp. + /// + WAVE_FORMAT_UNISYS_NAP_ALAW = 0x0172, // Unisys Corp. + /// + WAVE_FORMAT_UNISYS_NAP_16K = 0x0173, // Unisys Corp. + /// + WAVE_FORMAT_CREATIVE_ADPCM = 0x0200, // Creative Labs, Inc + /// + WAVE_FORMAT_CREATIVE_FASTSPEECH8 = 0x0202, // Creative Labs, Inc + /// + WAVE_FORMAT_CREATIVE_FASTSPEECH10 = 0x0203, // Creative Labs, Inc + /// + WAVE_FORMAT_UHER_ADPCM = 0x0210, // UHER informatic GmbH + /// + WAVE_FORMAT_QUARTERDECK = 0x0220, // Quarterdeck Corporation + /// + WAVE_FORMAT_ILINK_VC = 0x0230, // I-link Worldwide + /// + WAVE_FORMAT_RAW_SPORT = 0x0240, // Aureal Semiconductor + /// + WAVE_FORMAT_ESST_AC3 = 0x0241, // ESS Technology, Inc. + /// + WAVE_FORMAT_IPI_HSX = 0x0250, // Interactive Products, Inc. + /// + WAVE_FORMAT_IPI_RPELP = 0x0251, // Interactive Products, Inc. + /// + WAVE_FORMAT_CS2 = 0x0260, // Consistent Software + /// + WAVE_FORMAT_SONY_SCX = 0x0270, // Sony Corp. + /// + WAVE_FORMAT_FM_TOWNS_SND = 0x0300, // Fujitsu Corp. + /// + WAVE_FORMAT_BTV_DIGITAL = 0x0400, // Brooktree Corporation + /// + WAVE_FORMAT_QDESIGN_MUSIC = 0x0450, // QDesign Corporation + /// + WAVE_FORMAT_VME_VMPCM = 0x0680, // AT&T Labs, Inc. + /// + WAVE_FORMAT_TPC = 0x0681, // AT&T Labs, Inc. + /// + WAVE_FORMAT_OLIGSM = 0x1000, // Ing C. Olivetti & C., S.p.A. + /// + WAVE_FORMAT_OLIADPCM = 0x1001, // Ing C. Olivetti & C., S.p.A. + /// + WAVE_FORMAT_OLICELP = 0x1002, // Ing C. Olivetti & C., S.p.A. + /// + WAVE_FORMAT_OLISBC = 0x1003, // Ing C. Olivetti & C., S.p.A. + /// + WAVE_FORMAT_OLIOPR = 0x1004, // Ing C. Olivetti & C., S.p.A. + /// + WAVE_FORMAT_LH_CODEC = 0x1100, // Lernout & Hauspie + /// + WAVE_FORMAT_NORRIS = 0x1400, // Norris Communications, Inc. + /// + WAVE_FORMAT_SOUNDSPACE_MUSICOMPRESS = 0x1500, // AT&T Labs, Inc. + + /// + /// Advanced Audio Coding (AAC) audio in Audio Data Transport Stream (ADTS) format. + /// The format block is a WAVEFORMATEX structure with wFormatTag equal to WAVE_FORMAT_MPEG_ADTS_AAC. + /// + /// + /// The WAVEFORMATEX structure specifies the core AAC-LC sample rate and number of channels, + /// prior to applying spectral band replication (SBR) or parametric stereo (PS) tools, if present. + /// No additional data is required after the WAVEFORMATEX structure. + /// + /// http://msdn.microsoft.com/en-us/library/dd317599%28VS.85%29.aspx + MPEG_ADTS_AAC = 0x1600, + + /// + /// Source wmCodec.h + MPEG_RAW_AAC = 0x1601, + + /// + /// MPEG-4 audio transport stream with a synchronization layer (LOAS) and a multiplex layer (LATM). + /// The format block is a WAVEFORMATEX structure with wFormatTag equal to WAVE_FORMAT_MPEG_LOAS. + /// + /// + /// The WAVEFORMATEX structure specifies the core AAC-LC sample rate and number of channels, + /// prior to applying spectral SBR or PS tools, if present. + /// No additional data is required after the WAVEFORMATEX structure. + /// + /// http://msdn.microsoft.com/en-us/library/dd317599%28VS.85%29.aspx + MPEG_LOAS = 0x1602, + + /// NOKIA_MPEG_ADTS_AAC + /// Source wmCodec.h + NOKIA_MPEG_ADTS_AAC = 0x1608, + + /// NOKIA_MPEG_RAW_AAC + /// Source wmCodec.h + NOKIA_MPEG_RAW_AAC = 0x1609, + + /// VODAFONE_MPEG_ADTS_AAC + /// Source wmCodec.h + VODAFONE_MPEG_ADTS_AAC = 0x160A, + + /// VODAFONE_MPEG_RAW_AAC + /// Source wmCodec.h + VODAFONE_MPEG_RAW_AAC = 0x160B, + + /// + /// High-Efficiency Advanced Audio Coding (HE-AAC) stream. + /// The format block is an HEAACWAVEFORMAT structure. + /// + /// http://msdn.microsoft.com/en-us/library/dd317599%28VS.85%29.aspx + MPEG_HEAAC = 0x1610, + + /// WAVE_FORMAT_DVM + WAVE_FORMAT_DVM = 0x2000, // FAST Multimedia AG + + // others - not from MS headers + /// WAVE_FORMAT_VORBIS1 "Og" Original stream compatible + Vorbis1 = 0x674f, + + /// WAVE_FORMAT_VORBIS2 "Pg" Have independent header + Vorbis2 = 0x6750, + + /// WAVE_FORMAT_VORBIS3 "Qg" Have no codebook header + Vorbis3 = 0x6751, + + /// WAVE_FORMAT_VORBIS1P "og" Original stream compatible + Vorbis1P = 0x676f, + + /// WAVE_FORMAT_VORBIS2P "pg" Have independent headere + Vorbis2P = 0x6770, + + /// WAVE_FORMAT_VORBIS3P "qg" Have no codebook header + Vorbis3P = 0x6771, + + /// WAVE_FORMAT_EXTENSIBLE + Extensible = 0xFFFE, // Microsoft + /// + WAVE_FORMAT_DEVELOPMENT = 0xFFFF, +} \ No newline at end of file diff --git a/VG Music Studio - Core/Formats/Wave.cs b/VG Music Studio - Core/Formats/Wave.cs new file mode 100644 index 00000000..94e564ae --- /dev/null +++ b/VG Music Studio - Core/Formats/Wave.cs @@ -0,0 +1,548 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace Kermalis.VGMusicStudio.Core.Formats; + +// Some code has been based on NAudio's BufferedWaveProvider and CircularBuffer +// Sources: +// https://github.com/naudio/NAudio/blob/master/NAudio.Core/Wave/WaveProviders/BufferedWaveProvider.cs +// https://github.com/naudio/NAudio/blob/master/NAudio.Core/Utils/CircularBuffer.cs +// NAudio License (MIT) - https://github.com/naudio/NAudio/blob/master/license.txt + + +public enum BufferState +{ + Idle, + Reading, + Writing +} + +public class Wave +{ + public string? FileName; + public ushort Channels; + public uint SampleRate; + public ushort BitsPerSample; + public ushort ExtraSize; + public ushort BlockAlign; + public uint AverageBytesPerSecond; + public bool IsLooped = false; + public uint LoopStart; + public uint LoopEnd; + + public BufferState BufferState { get; protected set; } + + public bool DiscardOnBufferOverflow { get; set; } + public int BufferLength; + + public byte[]? Buffer; + public int ReadPosition { get; private set; } + public int WritePosition { get; private set; } + private int ByteCount; + private readonly object? LockObject = new(); + + private long DataChunkSize; + private long DataChunkLength; + private long DataChunkPosition; + private Stream? InStream; + private Stream? OutStream; + private EndianBinaryReader? Reader; + private EndianBinaryWriter? Writer; + + public long Position + { + get + { + return InStream!.Position - DataChunkPosition; + } + set + { + lock (LockObject!) + { + value = Math.Min(value, DataChunkLength); + // To keep it in sync + value -= (value % BlockAlign); + InStream!.Position = value + DataChunkPosition; + } + } + } + + public int BufferedBytes + { + get + { + if (this != null) + { + return Count; + } + + return 0; + } + } + + public int Count + { + get + { + lock (LockObject!) + { + return ByteCount; + } + } + } + + public void CreateStream() + { + InStream = new MemoryStream(); + OutStream = new MemoryStream(); + } + + public void CreateInStream() + { + InStream = new MemoryStream(); + } + + public void CreateOutStream() + { + OutStream = new MemoryStream(); + } + + public void CreateFileStream(string fileName) + { + OutStream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.Read); + Writer = new EndianBinaryWriter(OutStream, ascii: true); + } + + public Wave CreateFormat(uint sampleRate, ushort channels, ushort blockAlign, uint averageBytesPerSecond, ushort bitsPerSample, bool isLooped = false, uint loopStart = 0, uint loopEnd = 0) + { + Channels = channels; + SampleRate = sampleRate; + AverageBytesPerSecond = averageBytesPerSecond; + BlockAlign = blockAlign; + BitsPerSample = bitsPerSample; + ExtraSize = 0; + if (isLooped) + { + IsLooped = isLooped; + LoopStart = loopStart; + LoopEnd = loopEnd; + } + return this; + } + public Wave CreateIeeeFloatWave(uint sampleRate, ushort channels, ushort bits = 32, bool isLooped = false, uint loopStart = 0, uint loopEnd = 0) => CreateFormat(sampleRate, channels, (ushort)(channels * (bits / 8)), sampleRate * (ushort)(channels * (bits / 8)), bits, isLooped, loopStart, loopEnd); + + public void AddSamples(Span buffer, int offset, int count) + { + if (Engine.Instance!.Player.State == PlayerState.Playing) + { + BufferState = BufferState.Writing; + Buffer ??= new byte[BufferLength]; + + if (WriteBuffer(buffer, offset, count) < count && !DiscardOnBufferOverflow) + { + throw new InvalidOperationException("The buffer is full and cannot be written to."); + } + BufferState = BufferState.Idle; + } + else + { + Buffer ??= new byte[BufferLength]; + Array.Clear(Buffer); + } + } + + public int ReadBuffer(Span data, int offset, int count) + { + lock (LockObject!) + { + if (count > ByteCount) + { + count = ByteCount; + } + + int num = 0; + int readToEnd = Math.Min(Buffer!.Length - ReadPosition, count); + var src = new Span(Buffer, ReadPosition, readToEnd); + var dst = data.Slice(offset, readToEnd); + src.CopyTo(dst); + num += readToEnd; + ReadPosition += readToEnd; + ReadPosition %= Buffer.Length; + if (num < count) + { + src = new Span(Buffer, ReadPosition, count - num); + dst = data.Slice(offset + num, count - num); + src.CopyTo(dst); + ReadPosition += count - num; + num = count; + } + + ByteCount -= num; + return num; + } + } + + public int WriteBuffer(Span data, int offset, int count) + { + lock (LockObject!) + { + int bytesWritten = 0; + ByteCount = Buffer!.Length % count; + if (ByteCount >= Buffer!.Length) + { + ByteCount = 0; + } + if (count > Buffer!.Length - ByteCount) + { + count = Buffer.Length - ByteCount; + } + + int writeToEnd = Math.Min(Buffer.Length - WritePosition, count); + var src = data.Slice(offset, writeToEnd); + var dst = new Span(Buffer, WritePosition, writeToEnd); + src.CopyTo(dst); + WritePosition += writeToEnd; + WritePosition %= Buffer.Length; + bytesWritten += writeToEnd; + if (bytesWritten < count) + { + Debug.Assert(WritePosition == 0); + src = data.Slice(offset + bytesWritten, count - bytesWritten); + dst = new Span(Buffer, WritePosition, count - bytesWritten); + src.CopyTo(dst); + WritePosition += count - bytesWritten; + bytesWritten = count; + } + + ByteCount += bytesWritten; + return bytesWritten; + } + } + + public void ResetBuffer() + { + lock (LockObject!) + { + Array.Clear(Buffer!); + ByteCount = 0; + ReadPosition = 0; + WritePosition = 0; + } + } + + public int Read(Span array, int offset, int count) + { + if (count % BlockAlign != 0) + { + throw new ArgumentException( + $"Must read complete blocks: requested {count}, block align is {BlockAlign}"); + } + lock (LockObject!) + { + // sometimes there is more junk at the end of the file past the data chunk + if (Position + count > DataChunkLength) + { + count = (int)(DataChunkLength - Position); + } + return InStream!.Read(array.ToArray(), offset, count); + } + } + + public void Write(Span data, int offset, int count) + { + if (OutStream!.Length + count > uint.MaxValue) + { + throw new ArgumentException("WAV file too large", nameof(count)); + } + + OutStream.Write(data.ToArray(), offset, count); + DataChunkSize += count; + } + + public Span WriteBytes(Span data, WaveEncoding encoding = WaveEncoding.Pcm16) + { + var convertedData = new byte[data.Length * 2]; + int index = 0; + for (int i = 0; i < data.Length; i++) + { + convertedData[index++] = (byte)(data[i] & 0xff); + convertedData[index++] = (byte)(data[i] >> 8); + convertedData[index++] = (byte)(data[i] >> 16); + convertedData[index++] = (byte)(data[i] >> 24); + convertedData[index++] = (byte)(data[i] >> 32); + convertedData[index++] = (byte)(data[i] >> 40); + convertedData[index++] = (byte)(data[i] >> 48); + convertedData[index++] = (byte)(data[i] >> 56); + } + + return WriteBytes(convertedData, encoding); + } + + public Span WriteBytes(Span data, WaveEncoding encoding = WaveEncoding.Pcm16) + { + var convertedData = new byte[data.Length * 2]; + int index = 0; + for (int i = 0; i < data.Length; i++) + { + convertedData[index++] = (byte)(data[i] & 0xff); + convertedData[index++] = (byte)(data[i] >> 8); + convertedData[index++] = (byte)(data[i] >> 16); + convertedData[index++] = (byte)(data[i] >> 24); + } + + return WriteBytes(convertedData, encoding); + } + + public Span WriteBytes(Span data, WaveEncoding encoding = WaveEncoding.Pcm16) + { + var convertedData = new byte[data.Length * 2]; + int index = 0; + for (int i = 0; i < data.Length; i++) + { + convertedData[index++] = (byte)(data[i] & 0xff); + convertedData[index++] = (byte)(data[i] >> 8); + } + + return WriteBytes(convertedData, encoding); + } + + public Span WriteBytes(Span data, WaveEncoding encoding = WaveEncoding.Pcm16) + { + // Creating the RIFF Wave header + string fileID = "RIFF"; + uint fileSize = (uint)(data.Length + 44); // File size must match the size of the samples and header size + string waveID = "WAVE"; + string formatID = "fmt "; + uint formatLength = 16; // Always a length 16 + ushort formatType = (ushort)encoding; // 1 is PCM16, 2 is ADPCM, etc. + // Number of channels is already manually defined + uint sampleRate = SampleRate; // Sample Rate is read directly from the Info context + ushort bitsPerSample = 16; // bitsPerSample must be written to AFTER numNibbles + uint numNibbles = sampleRate * bitsPerSample * Channels / 8; // numNibbles must be written BEFORE bitsPerSample is written + ushort bitRate = (ushort)(bitsPerSample * Channels / 8); + string dataID = "data"; + uint dataSize = (uint)data.Length; + + byte[] samplerChunk; + + if (IsLooped) + { + string samplerID = "smpl"; + uint samplerSize = 0; + + uint manufacturer = 0; + uint product = 0; + uint samplePeriod = 0; + uint midiUnityNote = 0; + uint midiPitchFraction = 0; + uint smpteFormat = 0; + uint smpteOffset = 0; + uint numSampleLoops = 1; + uint samplerDataSize = 0; + + samplerSize += 36; + + if (numSampleLoops > 0) + { + var loopID = new uint[numSampleLoops]; + var loopType = new uint[numSampleLoops]; + var loopStart = new uint[numSampleLoops]; + var loopEnd = new uint[numSampleLoops]; + var loopFraction = new uint[numSampleLoops]; + var loopNumPlayback = new uint[numSampleLoops]; + + var loopHeaderSize = 0; + for (int i = 0; i < numSampleLoops; i++) + { + loopID[i] = 0; + loopType[i] = 0; + loopStart[i] = LoopStart; + loopEnd[i] = LoopEnd; + loopFraction[i] = 0; + loopNumPlayback[i] = 0; + + loopHeaderSize += 24; + samplerSize += 24; + } + var loopHeader = new byte[loopHeaderSize]; + var lw = new EndianBinaryWriter(new MemoryStream(loopHeader)) + { + ASCII = true + }; + for (int i = 0; i < numSampleLoops; i++) + { + lw.WriteUInt32(loopID[i]); + lw.WriteUInt32(loopType[i]); + lw.WriteUInt32(loopStart[i]); + lw.WriteUInt32(loopEnd[i]); + lw.WriteUInt32(loopFraction[i]); + lw.WriteUInt32(loopNumPlayback[i]); + } + samplerChunk = new byte[samplerSize + loopHeaderSize]; + + var sw = new EndianBinaryWriter(new MemoryStream(samplerChunk)) + { + ASCII = true + }; + sw.WriteChars(samplerID); + sw.WriteUInt32(samplerSize); + sw.WriteUInt32(manufacturer); + sw.WriteUInt32(product); + sw.WriteUInt32(samplePeriod); + sw.WriteUInt32(midiUnityNote); + sw.WriteUInt32(midiPitchFraction); + sw.WriteUInt32(smpteFormat); + sw.WriteUInt32(smpteOffset); + sw.WriteUInt32(numSampleLoops); + sw.WriteUInt32(samplerDataSize); + sw.WriteBytes(loopHeader); + + fileSize += (uint)samplerChunk.Length; + + var waveData = new byte[fileSize]; + var w = new EndianBinaryWriter(new MemoryStream(waveData)) + { + ASCII = true + }; + w.WriteChars(fileID); + w.WriteUInt32(fileSize); + w.WriteChars(waveID); + w.WriteChars(formatID); + w.WriteUInt32(formatLength); + w.WriteUInt16(formatType); + w.WriteUInt16(Channels); + w.WriteUInt32(sampleRate); + w.WriteUInt32(numNibbles); + w.WriteUInt16(bitRate); + w.WriteUInt16(bitsPerSample); + w.WriteChars(dataID); + w.WriteUInt32(dataSize); + w.WriteBytes(data); + w.WriteBytes(samplerChunk); + + return waveData; + } + else + { + samplerChunk = new byte[samplerSize + 8]; + + var sw = new EndianBinaryWriter(new MemoryStream(samplerChunk)) + { + ASCII = true + }; + sw.WriteChars(samplerID); + sw.WriteUInt32(samplerSize); + sw.WriteUInt32(manufacturer); + sw.WriteUInt32(product); + sw.WriteUInt32(samplePeriod); + sw.WriteUInt32(midiUnityNote); + sw.WriteUInt32(midiPitchFraction); + sw.WriteUInt32(smpteFormat); + sw.WriteUInt32(smpteOffset); + sw.WriteUInt32(numSampleLoops); + sw.WriteUInt32(samplerDataSize); + + fileSize += (uint)samplerChunk.Length; + + var waveData = new byte[fileSize]; + var w = new EndianBinaryWriter(new MemoryStream(waveData)) + { + ASCII = true + }; + w.WriteChars(fileID); + w.WriteUInt32(fileSize); + w.WriteChars(waveID); + w.WriteChars(formatID); + w.WriteUInt32(formatLength); + w.WriteUInt16(formatType); + w.WriteUInt16(Channels); + w.WriteUInt32(sampleRate); + w.WriteUInt32(numNibbles); + w.WriteUInt16(bitRate); + w.WriteUInt16(bitsPerSample); + w.WriteChars(dataID); + w.WriteUInt32(dataSize); + w.WriteBytes(data); + w.WriteBytes(samplerChunk); + + return waveData; + } + } + else + { + var waveData = new byte[fileSize]; + var w = new EndianBinaryWriter(new MemoryStream(waveData)) + { + ASCII = true + }; + w.WriteChars(fileID); + w.WriteUInt32(fileSize); + w.WriteChars(waveID); + w.WriteChars(formatID); + w.WriteUInt32(formatLength); + w.WriteUInt16(formatType); + w.WriteUInt16(Channels); + w.WriteUInt32(sampleRate); + w.WriteUInt32(numNibbles); + w.WriteUInt16(bitRate); + w.WriteUInt16(bitsPerSample); + w.WriteChars(dataID); + w.WriteUInt32(dataSize); + w.WriteBytes(data); + + return waveData; + } + } + protected virtual void WriteHeader(EndianBinaryWriter writer) + { + // Make sure the stream is at position 0 before writing the header + Writer!.Stream.Position = 0; + Writer.ASCII = true; + + // Creating the RIFF Wave headers + writer.WriteChars("RIFF"); + writer.WriteUInt32((uint)(DataChunkSize + 44)); + writer.WriteChars("WAVE"); + writer.WriteChars("fmt "); + writer.WriteUInt32((uint)(18 + ExtraSize)); + writer.WriteInt16((short)WaveEncoding.Pcm16); + writer.WriteInt16((short)Channels); + writer.WriteUInt32(SampleRate); + writer.WriteUInt32(AverageBytesPerSecond); + writer.WriteInt16((short)BlockAlign); + writer.WriteInt16((short)BitsPerSample); + writer.WriteUInt16(ExtraSize); + writer.WriteChars("data"); + writer.WriteUInt32((uint)DataChunkSize); + } + + internal void Dispose(bool disposing) + { + if (disposing && OutStream != null) + { + try + { + WriteHeader(Writer!); + } + finally + { + Writer = null!; + OutStream.Dispose(); + OutStream = null; + } + } + if (disposing && InStream != null) + { + InStream.Dispose(); + InStream = null; + } + } + + ~Wave() + { + Dispose(disposing: false); + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamConfig.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamConfig.cs index 750ae76b..4eacd6b8 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamConfig.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamConfig.cs @@ -14,7 +14,7 @@ public sealed class AlphaDreamConfig : Config { private const string CONFIG_FILE = "AlphaDream.yaml"; - internal readonly byte[] ROM; + public override byte[] ROM { get; } internal readonly EndianBinaryReader Reader; // TODO: Need? internal readonly string GameCode; internal readonly byte Version; diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs index fdee70e4..d67e51a3 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs @@ -10,6 +10,8 @@ public sealed class AlphaDreamEngine : Engine public override AlphaDreamMixer Mixer { get; } public override AlphaDreamPlayer Player { get; } + public override bool IsFileSystemFormat { get; } = false; + public AlphaDreamEngine(byte[] rom) { if (rom.Length > GBAUtils.CARTRIDGE_CAPACITY) @@ -25,6 +27,12 @@ public AlphaDreamEngine(byte[] rom) Instance = this; } + public override void Reload() + { + var config = Config; + Dispose(); + _ = new AlphaDreamEngine(config.ROM); + } public override void Dispose() { base.Dispose(); diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong.cs old mode 100644 new mode 100755 index f81833ae..6f357a10 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamLoadedSong.cs @@ -5,37 +5,40 @@ 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); - } - } + public List?[] Events { get; } + public long MaxTicks { get; private set; } + + public SoundBank Bank { get; } + + 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/AlphaDreamMixer.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs index 1cc823c5..1c4452be 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs @@ -1,6 +1,9 @@ using Kermalis.VGMusicStudio.Core.Util; -using NAudio.Wave; +using Kermalis.VGMusicStudio.Core.Formats; using System; +using NAudio.Wave; +using SoundFlow.Structs; +using SoundFlow.Enums; namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; @@ -8,18 +11,36 @@ public sealed class AlphaDreamMixer : Mixer { public readonly float SampleRateReciprocal; private readonly float _samplesReciprocal; - public readonly int SamplesPerBuffer; + internal override int SamplesPerBuffer { get; } 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; + private readonly AudioBackend AlphaDreamPlaybackBackend; + + #region PortAudio Fields + // PortAudio Fields + private readonly Audio? _audioPortAudio; + private readonly Wave? _bufferPortAudio; + #endregion + + #region MiniAudio Fields + // MiniAudio Fields + private readonly float[]? _bufferMiniAudio; + private readonly AudioFormat _formatSoundFlow; + protected override AudioFormat SoundFlowFormat => _formatSoundFlow; + #endregion - protected override WaveFormat WaveFormat => _buffer.WaveFormat; + #region NAudio Fields + // NAudio Fields + private readonly WaveBuffer? _audioNAudio; + private readonly BufferedWaveProvider? _bufferNAudio; + #endregion + + protected override WaveFormat WaveFormat => _bufferNAudio!.WaveFormat; internal AlphaDreamMixer(AlphaDreamConfig config) { @@ -30,17 +51,54 @@ internal AlphaDreamMixer(AlphaDreamConfig config) _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 + AlphaDreamPlaybackBackend = PlaybackBackend; + switch (PlaybackBackend) { - DiscardOnBufferOverflow = true, - BufferLength = SamplesPerBuffer * 64 - }; - Init(_buffer); + case AudioBackend.PortAudio: + { + _audioPortAudio = new Audio(amt * sizeof(float)) { Float32BufferCount = amt }; + _bufferPortAudio = new Wave() + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64 + }; + _bufferPortAudio.CreateIeeeFloatWave(sampleRate, 2); // TODO + + Init(waveData: _bufferPortAudio); + break; + } + case AudioBackend.MiniAudio: + { + _bufferMiniAudio = new float[amt]; + _formatSoundFlow = new AudioFormat + { + Channels = 2, + SampleRate = sampleRate, + Format = SampleFormat.F32 + }; + Init(); + break; + } + case AudioBackend.NAudio: + { + _audioNAudio = new WaveBuffer(amt * sizeof(float)) { FloatBufferCount = amt }; + for (int i = 0; i < AlphaDreamPlayer.NUM_TRACKS; i++) + { + _trackBuffers[i] = new float[amt]; + } + _bufferNAudio = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, 2)) // TODO + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64 + }; + Init(waveProvider: _bufferNAudio); + break; + } + } } internal void BeginFadeIn() @@ -73,7 +131,19 @@ internal void ResetFade() internal void Process(AlphaDreamTrack[] tracks, bool output, bool recording) { - _audio.Clear(); + switch (AlphaDreamPlaybackBackend) + { + case AudioBackend.PortAudio: + { + _audioPortAudio!.Clear(); + break; + } + case AudioBackend.NAudio: + { + _audioNAudio!.Clear(); + break; + } + } float masterStep; float masterLevel; if (_isFading && _fadeMicroFramesLeft == 0) @@ -110,18 +180,71 @@ internal void Process(AlphaDreamTrack[] tracks, bool output, bool recording) 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; + switch (AlphaDreamPlaybackBackend) + { + case AudioBackend.PortAudio: + { + _audioPortAudio!.Float32Buffer![j * 2] += buf[j * 2] * level; + _audioPortAudio.Float32Buffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; + break; + } + case AudioBackend.MiniAudio: + { + _bufferMiniAudio![j * 2] += buf[j * 2] * level; + _bufferMiniAudio[(j * 2) + 1] += buf[(j * 2) + 1] * level; + break; + } + case AudioBackend.NAudio: + { + _audioNAudio!.FloatBuffer![j * 2] += buf[j * 2] * level; + _audioNAudio.FloatBuffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; + break; + } + } level += masterStep; } } if (output) { - _buffer.AddSamples(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + switch (AlphaDreamPlaybackBackend) + { + case AudioBackend.PortAudio: + { + _bufferPortAudio!.AddSamples(_audioPortAudio!.ByteBuffer, 0, _audioPortAudio.ByteBufferCount); + break; + } + case AudioBackend.MiniAudio: + { + DataProvider!.AddSamples(_bufferMiniAudio); + break; + } + case AudioBackend.NAudio: + { + _bufferNAudio!.AddSamples(_audioNAudio!.ByteBuffer, 0, _audioNAudio.ByteBufferCount); + break; + } + } } if (recording) { - _waveWriter!.Write(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + switch (AlphaDreamPlaybackBackend) + { + case AudioBackend.PortAudio: + { + _waveWriterPortAudio!.Write(_audioPortAudio!.ByteBuffer, 0, _audioPortAudio.ByteBufferCount); + break; + } + case AudioBackend.MiniAudio: + { + _soundFlowEncoder!.Encode(_bufferMiniAudio); + break; + } + case AudioBackend.NAudio: + { + _waveWriterNAudio!.Write(_audioNAudio!.ByteBuffer, 0, _audioNAudio.ByteBufferCount); + break; + } + } } } } diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs index e202a387..c53fde41 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs @@ -14,7 +14,7 @@ public sealed class AlphaDreamPlayer : Player private readonly AlphaDreamMixer _mixer; private AlphaDreamLoadedSong? _loadedSong; - internal byte Tempo; + public override ushort Tempo { get; set; } internal int TempoStack; private long _elapsedLoops; diff --git a/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamPCMChannel.cs b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamPCMChannel.cs index 455dc77e..c08008f8 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamPCMChannel.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamPCMChannel.cs @@ -35,43 +35,43 @@ 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; + int nextVel = _velocity + _adsr.A; + if (nextVel >= 0xFF) + { + State = EnvelopeState.Decay; + _velocity = 0xFF; + } + else + { + _velocity = (byte)nextVel; + } + break; } - break; - } case EnvelopeState.Decay: - { - int nextVel = (_velocity * _adsr.D) >> 8; - if (nextVel <= _adsr.S) { - State = EnvelopeState.Sustain; - _velocity = _adsr.S; - } - else - { - _velocity = (byte)nextVel; + int nextVel = (_velocity * _adsr.D) >> 8; + if (nextVel <= _adsr.S) + { + State = EnvelopeState.Sustain; + _velocity = _adsr.S; + } + else + { + _velocity = (byte)nextVel; + } + break; } - break; - } case EnvelopeState.Release: - { - int next = (_velocity * _adsr.R) >> 8; - if (next < 0) { - next = 0; + int next = (_velocity * _adsr.R) >> 8; + if (next < 0) + { + next = 0; + } + _velocity = (byte)next; + break; } - _velocity = (byte)next; - break; - } } } diff --git a/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamSquareChannel.cs b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamSquareChannel.cs index a627bf07..cc825709 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamSquareChannel.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamSquareChannel.cs @@ -32,43 +32,43 @@ private void StepEnvelope() switch (State) { case EnvelopeState.Attack: - { - int next = _velocity + _adsr.A; - if (next >= 0xF) { - State = EnvelopeState.Decay; - _velocity = 0xF; + int next = _velocity + _adsr.A; + if (next >= 0xF) + { + State = EnvelopeState.Decay; + _velocity = 0xF; + } + else + { + _velocity = (byte)next; + } + break; } - 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; + int next = (_velocity * _adsr.D) >> 3; + if (next <= _adsr.S) + { + State = EnvelopeState.Sustain; + _velocity = _adsr.S; + } + else + { + _velocity = (byte)next; + } + break; } - break; - } case EnvelopeState.Release: - { - int next = (_velocity * _adsr.R) >> 3; - if (next < 0) { - next = 0; + int next = (_velocity * _adsr.R) >> 3; + if (next < 0) + { + next = 0; + } + _velocity = (byte)next; + break; } - _velocity = (byte)next; - break; - } } } @@ -77,9 +77,11 @@ public override void Process(float[] buffer) StepEnvelope(); ChannelVolume vol = GetVolume(); - float interStep = _frequency * _mixer.SampleRateReciprocal; - - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + float interStep; + int bufPos = 0; + int samplesPerBuffer; + interStep = _frequency * _mixer.SampleRateReciprocal; + samplesPerBuffer = _mixer.SamplesPerBuffer; do { float samp = _pat[_pos]; diff --git a/VG Music Studio - Core/GBA/GBAUtils.cs b/VG Music Studio - Core/GBA/GBAUtils.cs old mode 100644 new mode 100755 index ca4ecf1d..bbe7b98b --- a/VG Music Studio - Core/GBA/GBAUtils.cs +++ b/VG Music Studio - Core/GBA/GBAUtils.cs @@ -2,13 +2,31 @@ namespace Kermalis.VGMusicStudio.Core.GBA; -internal static class GBAUtils +public static class GBAUtils { - public const double AGB_FPS = 59.7275; - public const int SYSTEM_CLOCK = 16_777_216; // 16.777216 MHz (16*1024*1024 Hz) + public const double AGB_FPS = 59.7275; + public const int SYSTEM_CLOCK = 16_777_216; // 16.777216 MHz (16*1024*1024 Hz) - public const int CARTRIDGE_OFFSET = 0x08_000_000; - public const int CARTRIDGE_CAPACITY = 0x02_000_000; + public const int CARTRIDGE_OFFSET = 0x08_000_000; + public const int CARTRIDGE_CAPACITY = 0x02_000_000; - public static ReadOnlySpan PSGTypes => new string[4] { "Square 1", "Square 2", "PCM4", "Noise" }; + public static ReadOnlySpan PSGTypes => new string[4] { "Square 1", "Square 2", "PCM4", "Noise" }; + public static bool IsValidRomOffset(int offset) + { + return + (offset >= 0 && offset < Math.Min(CARTRIDGE_CAPACITY, Engine.Instance!.Config.ROM!.Length)) // 0 <= Offset < min(0x2000000, ROM.Length) + || (offset >= CARTRIDGE_OFFSET && offset < Math.Min(CARTRIDGE_CAPACITY + CARTRIDGE_OFFSET, Engine.Instance!.Config.ROM!.Length + CARTRIDGE_OFFSET)); // 0x8000000 <= Offset < min(0xA000000, ROM.Length + 0x8000000) + } + public static int SanitizeOffset(int offset) + { + if (!IsValidRomOffset(offset)) + { + throw new ArgumentOutOfRangeException($"Offset 0x{offset:X} was invalid."); + } + if (offset >= CARTRIDGE_OFFSET) + { + return offset - CARTRIDGE_OFFSET; + } + return offset; + } } diff --git a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KChannel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KChannel.cs index 3d352412..0b966857 100644 --- a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KChannel.cs +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KChannel.cs @@ -1,10 +1,12 @@ -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; internal abstract class MP2KChannel { public EnvelopeState State; public MP2KTrack? Owner; - protected readonly MP2KMixer _mixer; + protected readonly MP2KMixer? _mixer; public NoteInfo Note; protected ADSR _adsr; @@ -32,7 +34,7 @@ public virtual void Release() } } - public abstract void Process(float[] buffer); + public abstract void Process(Span buffer); /// Returns whether the note is active or not public virtual bool TickNote() diff --git a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KNoiseChannel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KNoiseChannel.cs index 43893524..e667eb0f 100644 --- a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KNoiseChannel.cs +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KNoiseChannel.cs @@ -40,7 +40,7 @@ public override void SetPitch(int pitch) _frequency = 524_288f / (r == 0 ? 0.5f : r) / MathF.Pow(2, s + 1); } - public override void Process(float[] buffer) + public override void Process(Span buffer) { StepEnvelope(); if (State == EnvelopeState.Dead) @@ -49,10 +49,12 @@ public override void Process(float[] buffer) } ChannelVolume vol = GetVolume(); - float interStep = _frequency * _mixer.SampleRateReciprocal; + float interStep; int bufPos = 0; - int samplesPerBuffer = _mixer.SamplesPerBuffer; + int samplesPerBuffer; + interStep = _frequency * _mixer!.SampleRateReciprocal; + samplesPerBuffer = _mixer!.SamplesPerBuffer; do { float samp = _pat[_pos & (_pat.Length - 1)] ? 0.5f : -0.5f; diff --git a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM4Channel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM4Channel.cs index 90ba63ba..fb87cf6e 100644 --- a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM4Channel.cs +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM4Channel.cs @@ -14,7 +14,7 @@ public MP2KPCM4Channel(MP2KMixer mixer) 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); + MP2KUtils.PCM4ToFloat(_mixer!.Config.ROM.AsSpan(sampleOffset), _sample); } public override void SetPitch(int pitch) @@ -22,7 +22,7 @@ public override void SetPitch(int pitch) _frequency = 7_040 * MathF.Pow(2, ((Note.Note - 69) / 12f) + (pitch / 768f)); } - public override void Process(float[] buffer) + public override void Process(Span buffer) { StepEnvelope(); if (State == EnvelopeState.Dead) @@ -31,10 +31,12 @@ public override void Process(float[] buffer) } ChannelVolume vol = GetVolume(); - float interStep = _frequency * _mixer.SampleRateReciprocal; + float interStep; int bufPos = 0; - int samplesPerBuffer = _mixer.SamplesPerBuffer; + int samplesPerBuffer; + interStep = _frequency * _mixer!.SampleRateReciprocal; + samplesPerBuffer = _mixer!.SamplesPerBuffer; do { float samp = _sample[_pos]; diff --git a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM8Channel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM8Channel.cs index 552e230b..822b89ed 100644 --- a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM8Channel.cs +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM8Channel.cs @@ -24,21 +24,21 @@ public void Init(MP2KTrack owner, NoteInfo note, ADSR adsr, int sampleOffset, by State = EnvelopeState.Initializing; _pos = 0; _interPos = 0; - if (Owner is not null) - { - Owner.Channels.Remove(this); - } + Owner?.Channels.Remove(this); Owner = owner; Owner.Channels.Add(this); Note = note; _adsr = adsr; _instPan = instPan; - byte[] rom = _mixer.Config.ROM; + byte[] rom; + 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; + _bGoldenSun = _mixer!.Config.HasGoldenSunSynths && _sampleHeader.Length == 0 && _sampleHeader.DoesLoop == SampleHeader.LOOP_TRUE && _sampleHeader.LoopOffset == 0; + if (_bGoldenSun) { _gsPSG = GoldenSunPSG.Get(rom.AsSpan(_sampleOffset)); @@ -52,8 +52,8 @@ public override ChannelVolume GetVolume() 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) @@ -85,66 +85,66 @@ 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; + _velocity = _adsr.A; + State = EnvelopeState.Rising; + break; } - else + case EnvelopeState.Rising: { - _velocity = (byte)nextVel; + int nextVel = _velocity + _adsr.A; + if (nextVel >= 0xFF) + { + State = EnvelopeState.Decaying; + _velocity = 0xFF; + } + else + { + _velocity = (byte)nextVel; + } + break; } - break; - } case EnvelopeState.Decaying: - { - int nextVel = (_velocity * _adsr.D) >> 8; - if (nextVel <= _adsr.S) { - State = EnvelopeState.Playing; - _velocity = _adsr.S; + int nextVel = (_velocity * _adsr.D) >> 8; + if (nextVel <= _adsr.S) + { + State = EnvelopeState.Playing; + _velocity = _adsr.S; + } + else + { + _velocity = (byte)nextVel; + } + break; } - else + case EnvelopeState.Playing: { - _velocity = (byte)nextVel; + break; } - break; - } - case EnvelopeState.Playing: - { - break; - } case EnvelopeState.Releasing: - { - int nextVel = (_velocity * _adsr.R) >> 8; - if (nextVel <= 0) { - State = EnvelopeState.Dying; - _velocity = 0; + int nextVel = (_velocity * _adsr.R) >> 8; + if (nextVel <= 0) + { + State = EnvelopeState.Dying; + _velocity = 0; + } + else + { + _velocity = (byte)nextVel; + } + break; } - else + case EnvelopeState.Dying: { - _velocity = (byte)nextVel; + Stop(); + break; } - break; - } - case EnvelopeState.Dying: - { - Stop(); - break; - } } } - public override void Process(float[] buffer) + public override void Process(Span buffer) { StepEnvelope(); if (State == EnvelopeState.Dead) @@ -153,7 +153,9 @@ public override void Process(float[] buffer) } ChannelVolume vol = GetVolume(); - float interStep = _bFixed && !_bGoldenSun ? _mixer.SampleRate * _mixer.SampleRateReciprocal : _frequency * _mixer.SampleRateReciprocal; + float interStep; + 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); @@ -164,88 +166,96 @@ public override void Process(float[] buffer) } else { - Process_Standard(buffer, vol, interStep, _mixer.Config.ROM); + Process_Standard(buffer, vol, interStep, _mixer!.Config.ROM); } } - private void Process_GS(float[] buffer, ChannelVolume vol, float interStep) + private void Process_GS(Span 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; + _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; - _interPos += interStep; - if (_interPos >= 1) + int bufPos = 0; + int samplesPerBuffer; + samplesPerBuffer = _mixer!.SamplesPerBuffer; + + do { - _interPos--; - } - } while (--samplesPerBuffer > 0); - break; - } - case GoldenSunPSGType.Saw: - { - const int FIX = 0x70; + float samp = _interPos < threshold ? 0.5f : -0.5f; + samp += 0.5f - threshold; + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; - int bufPos = 0; - int samplesPerBuffer = _mixer.SamplesPerBuffer; - do + _interPos += interStep; + if (_interPos >= 1) + { + _interPos--; + } + } while (--samplesPerBuffer > 0); + break; + } + case GoldenSunPSGType.Saw: { - _interPos += interStep; - if (_interPos >= 1) + const int FIX = 0x70; + + int bufPos = 0; + int samplesPerBuffer; + samplesPerBuffer = _mixer!.SamplesPerBuffer; + + do { - _interPos--; - } - int var1 = (int)(_interPos * 0x100) - FIX; - int var2 = (int)(_interPos * 0x10000) << 17; - int var3 = var1 - (var2 >> 27); - _pos = var3 + (_pos >> 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); - float samp = _pos / (float)0x100; + float samp = _pos / (float)0x100; - 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; + } case GoldenSunPSGType.Triangle: - { - int bufPos = 0; - int samplesPerBuffer = _mixer.SamplesPerBuffer; - do { - _interPos += interStep; - if (_interPos >= 1) + int bufPos = 0; + int samplesPerBuffer; + samplesPerBuffer = _mixer!.SamplesPerBuffer; + + do { - _interPos--; - } - float samp = _interPos < 0.5f ? (_interPos * 4) - 1 : 3 - (_interPos * 4); + _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; - } + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + } while (--samplesPerBuffer > 0); + break; + } } } - private void Process_Compressed(float[] buffer, ChannelVolume vol, float interStep) + private void Process_Compressed(Span buffer, ChannelVolume vol, float interStep) { int bufPos = 0; - int samplesPerBuffer = _mixer.SamplesPerBuffer; + int samplesPerBuffer; + samplesPerBuffer = _mixer!.SamplesPerBuffer; + do { float samp = _decompressedSample![_pos] / (float)0x80; @@ -264,10 +274,12 @@ private void Process_Compressed(float[] buffer, ChannelVolume vol, float interSt } } while (--samplesPerBuffer > 0); } - private void Process_Standard(float[] buffer, ChannelVolume vol, float interStep, byte[] rom) + private void Process_Standard(Span buffer, ChannelVolume vol, float interStep, byte[] rom) { int bufPos = 0; - int samplesPerBuffer = _mixer.SamplesPerBuffer; + int samplesPerBuffer; + samplesPerBuffer = _mixer!.SamplesPerBuffer; + do { float samp = (sbyte)rom[_pos + _sampleOffset] / (float)0x80; diff --git a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPSGChannel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPSGChannel.cs index bc795dff..3f3b576a 100644 --- a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPSGChannel.cs +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPSGChannel.cs @@ -2,7 +2,7 @@ internal abstract class MP2KPSGChannel : MP2KChannel { - protected enum GBPan : byte + protected enum PSGPan : byte { Left, Center, @@ -13,7 +13,7 @@ protected enum GBPan : byte private EnvelopeState _nextState; private byte _peakVelocity; private byte _sustainVelocity; - protected GBPan _panpot = GBPan.Center; + protected PSGPan _panpot = PSGPan.Center; public MP2KPSGChannel(MP2KMixer mixer) : base(mixer) @@ -80,13 +80,18 @@ public override bool TickNote() return true; } + public byte GetPseudoEchoLevel() + { + return (byte)(((_peakVelocity * Note.PseudoEchoVolume) + 0xFF) >> 8); + } + 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 + LeftVol = _panpot == PSGPan.Right ? 0 : _velocity / MAX, + RightVol = _panpot == PSGPan.Left ? 0 : _velocity / MAX }; } public override void SetVolume(byte vol, sbyte pan) @@ -102,7 +107,7 @@ public override void SetVolume(byte vol, sbyte pan) } if (State < EnvelopeState.Releasing) { - _panpot = combinedPan < -21 ? GBPan.Left : combinedPan > 20 ? GBPan.Right : GBPan.Center; + _panpot = combinedPan < -21 ? PSGPan.Left : combinedPan > 20 ? PSGPan.Right : PSGPan.Center; _peakVelocity = (byte)((Note.Velocity * vol) >> 10); _sustainVelocity = (byte)(((_peakVelocity * _adsr.S) + 0xF) >> 4); // TODO if (State == EnvelopeState.Playing) @@ -141,6 +146,10 @@ void rel() else { _processStep = 0; + if (GetPseudoEchoLevel() != 0 && Note.PseudoEchoLength != 0) + { + _nextState = EnvelopeState.PseudoEcho; + } if (_velocity - 1 <= 0) { _nextState = EnvelopeState.Dying; @@ -156,127 +165,142 @@ void rel() 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) + _nextState = EnvelopeState.Rising; + _processStep = 0; + if ((_adsr.A | _adsr.D) == 0 || (_sustainVelocity == 0 && _peakVelocity == 0)) { + State = EnvelopeState.Playing; _velocity = _sustainVelocity; + return; } - 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) + else if (_adsr.A == 0 && _adsr.S < 0xF) { State = EnvelopeState.Decaying; - dec(); return; + int next = _peakVelocity - 1; + if (next < 0) + { + next = 0; + } + _velocity = (byte)next; + if (_velocity < _sustainVelocity) + { + _velocity = _sustainVelocity; + } + else if (GetPseudoEchoLevel() != 0) + { + _velocity = GetPseudoEchoLevel(); + } + return; } - if (_nextState == EnvelopeState.Playing) + else if (_adsr.A == 0) { State = EnvelopeState.Playing; - sus(); return; + _velocity = _sustainVelocity; + return; } - if (_nextState == EnvelopeState.Releasing) + else { - State = EnvelopeState.Releasing; - rel(); return; + State = EnvelopeState.Rising; + _velocity = 1; + return; } - _processStep = 0; - if (++_velocity >= _peakVelocity) + } + case EnvelopeState.PseudoEcho: + { + if (--Note.PseudoEchoLength == 0) { - if (_adsr.D == 0) + _nextState = EnvelopeState.Dying; + _processStep = 4 - 1; + return; + } + + return; + } + case EnvelopeState.Rising: + { + if (++_processStep >= _adsr.A) + { + if (_nextState == EnvelopeState.Decaying) { - _nextState = EnvelopeState.Playing; + State = EnvelopeState.Decaying; + dec(); return; } - else if (_peakVelocity == _sustainVelocity) + if (_nextState == EnvelopeState.Playing) { - _nextState = EnvelopeState.Playing; - _velocity = _peakVelocity; + State = EnvelopeState.Playing; + sus(); return; } - else + if (_nextState == EnvelopeState.Releasing) { - _velocity = _peakVelocity; - _nextState = EnvelopeState.Decaying; + 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; } - break; - } case EnvelopeState.Decaying: - { - if (++_processStep >= _adsr.D) { - if (_nextState == EnvelopeState.Playing) + if (++_processStep >= _adsr.D) { - State = EnvelopeState.Playing; - sus(); return; - } - if (_nextState == EnvelopeState.Releasing) - { - State = EnvelopeState.Releasing; - rel(); return; + if (_nextState == EnvelopeState.Playing) + { + State = EnvelopeState.Playing; + sus(); return; + } + if (_nextState == EnvelopeState.Releasing) + { + State = EnvelopeState.Releasing; + rel(); return; + } + dec(); } - dec(); + break; } - break; - } case EnvelopeState.Playing: - { - if (++_processStep >= 1) { - if (_nextState == EnvelopeState.Releasing) + if (++_processStep >= 1) { - State = EnvelopeState.Releasing; - rel(); return; + if (_nextState == EnvelopeState.Releasing) + { + State = EnvelopeState.Releasing; + rel(); return; + } + sus(); } - sus(); + break; } - break; - } case EnvelopeState.Releasing: - { - if (++_processStep >= _adsr.R) { - if (_nextState == EnvelopeState.Dying) + if (++_processStep >= _adsr.R) { - Stop(); - return; + if (_nextState == EnvelopeState.Dying) + { + Stop(); + return; + } + rel(); } - rel(); + break; } - 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 index 4e655ffe..177672ac 100644 --- a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KSquareChannel.cs +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KSquareChannel.cs @@ -28,7 +28,7 @@ public override void SetPitch(int pitch) _frequency = 3_520 * MathF.Pow(2, ((Note.Note - 69) / 12f) + (pitch / 768f)); } - public override void Process(float[] buffer) + public override void Process(Span buffer) { StepEnvelope(); if (State == EnvelopeState.Dead) @@ -37,13 +37,15 @@ public override void Process(float[] buffer) } ChannelVolume vol = GetVolume(); - float interStep = _frequency * _mixer.SampleRateReciprocal; + float interStep; int bufPos = 0; - int samplesPerBuffer = _mixer.SamplesPerBuffer; + int samplesPerBuffer; + interStep = _frequency * _mixer!.SampleRateReciprocal; + samplesPerBuffer = _mixer!.SamplesPerBuffer; do { - float samp = _pat[_pos]; + float samp = _pat[_pos] * 1.6f; buffer[bufPos++] += samp * vol.LeftVol; buffer[bufPos++] += samp * vol.RightVol; diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KCommands.cs b/VG Music Studio - Core/GBA/MP2K/MP2KCommands.cs old mode 100644 new mode 100755 index 4959a5be..814289ff --- a/VG Music Studio - Core/GBA/MP2K/MP2KCommands.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KCommands.cs @@ -26,6 +26,7 @@ internal sealed class FinishCommand : ICommand public string Arguments => Prev ? "Resume previous track" : "End track"; public bool Prev { get; set; } + public byte Type { get => (byte)(Prev ? 0xB6 : 0xB1); set => Prev = value == 0xB6; } } internal sealed class JumpCommand : ICommand { @@ -71,9 +72,9 @@ internal sealed class LibraryCommand : ICommand { public Color Color => Color.SteelBlue; public string Label => "Library Call"; - public string Arguments => $"{Command}, {Argument}"; + public string Arguments => $"{LibraryCommandType}, {Argument}"; - public byte Command { get; set; } + public LibraryCommandTypes LibraryCommandType { get; set; } public byte Argument { get; set; } } internal sealed class MemoryAccessCommand : ICommand @@ -82,7 +83,7 @@ internal sealed class MemoryAccessCommand : ICommand public string Label => "Memory Access"; public string Arguments => $"{Operator}, {Address}, {Data}"; - public byte Operator { get; set; } + public MemoryOperatorType Operator { get; set; } public byte Address { get; set; } public byte Data { get; set; } } diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs b/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs old mode 100644 new mode 100755 index 97eb5406..badeef33 --- a/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KConfig.cs @@ -6,238 +6,258 @@ using System.IO; using System.Linq; using YamlDotNet.RepresentationModel; +using static Kermalis.VGMusicStudio.Core.GBA.MP2K.MP2KConfig; namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; public sealed class MP2KConfig : Config { - private const string CONFIG_FILE = "MP2K.yaml"; - - internal readonly byte[] ROM; - internal readonly string GameCode; - internal readonly byte Version; - - internal readonly string Name; - internal readonly int[] SongTableOffsets; - public readonly long[] SongTableSizes; - internal readonly int SampleRate; - internal readonly ReverbType ReverbType; - internal readonly byte Reverb; - internal readonly byte Volume; - internal readonly bool HasGoldenSunSynths; - internal readonly bool HasPokemonCompression; - - internal MP2KConfig(byte[] rom) - { - using (StreamReader fileStream = File.OpenText(ConfigUtils.CombineWithBaseDirectory(CONFIG_FILE))) - using (var ms = new MemoryStream(rom)) - { - string gcv = string.Empty; - try - { - ROM = rom; - var r = new EndianBinaryReader(ms, ascii: true); - r.Stream.Position = 0xAC; - GameCode = r.ReadString_Count(4); - r.Stream.Position = 0xBC; - Version = r.ReadByte(); - gcv = $"{GameCode}_{Version:X2}"; - var yaml = new YamlStream(); - yaml.Load(fileStream); - - var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; - YamlMappingNode game; - try - { - game = (YamlMappingNode)mapping.Children.GetValue(gcv); - } - catch (BetterKeyNotFoundException) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KMissingGameCode, gcv))); - } - - YamlNode? nameNode = null, - songTableOffsetsNode = null, - songTableSizesNode = null, - sampleRateNode = null, - reverbTypeNode = null, - reverbNode = null, - volumeNode = null, - hasGoldenSunSynthsNode = null, - hasPokemonCompression = null; - void Load(YamlMappingNode gameToLoad) - { - if (gameToLoad.Children.TryGetValue("Copy", out YamlNode? node)) - { - YamlMappingNode copyGame; - try - { - copyGame = (YamlMappingNode)mapping.Children.GetValue(node); - } - catch (BetterKeyNotFoundException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KCopyInvalidGameCode, ex.Key))); - } - Load(copyGame); - } - if (gameToLoad.Children.TryGetValue(nameof(Name), out node)) - { - nameNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SongTableOffsets), out node)) - { - songTableOffsetsNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SongTableSizes), out node)) - { - songTableSizesNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SampleRate), out node)) - { - sampleRateNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(ReverbType), out node)) - { - reverbTypeNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(Reverb), out node)) - { - reverbNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(Volume), out node)) - { - volumeNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(HasGoldenSunSynths), out node)) - { - hasGoldenSunSynthsNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(HasPokemonCompression), out node)) - { - hasPokemonCompression = node; - } - if (gameToLoad.Children.TryGetValue(nameof(Playlists), out node)) - { - var playlists = (YamlMappingNode)node; - foreach (KeyValuePair kvp in playlists) - { - string name = kvp.Key.ToString(); - var songs = new List(); - foreach (KeyValuePair song in (YamlMappingNode)kvp.Value) - { - int songIndex = (int)ConfigUtils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Playlists)), song.Key.ToString(), 0, int.MaxValue); - if (songs.Any(s => s.Index == songIndex)) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongRepeated, name, songIndex))); - } - songs.Add(new Song(songIndex, song.Value.ToString())); - } - Playlists.Add(new Playlist(name, songs)); - } - } - } - - Load(game); - - if (nameNode is null) - { - throw new BetterKeyNotFoundException(nameof(Name), null); - } - Name = nameNode.ToString(); - - if (songTableOffsetsNode is null) - { - throw new BetterKeyNotFoundException(nameof(SongTableOffsets), null); - } - string[] songTables = songTableOffsetsNode.ToString().Split(' ', options: StringSplitOptions.RemoveEmptyEntries); - int numSongTables = songTables.Length; - if (numSongTables == 0) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigKeyNoEntries, nameof(SongTableOffsets)))); - } - - if (songTableSizesNode is null) - { - throw new BetterKeyNotFoundException(nameof(SongTableSizes), null); - } - string[] sizes = songTableSizesNode.ToString().Split(' ', options: StringSplitOptions.RemoveEmptyEntries); - if (sizes.Length != numSongTables) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongTableCounts, nameof(SongTableSizes), nameof(SongTableOffsets)))); - } - SongTableOffsets = new int[numSongTables]; - SongTableSizes = new long[numSongTables]; - int maxOffset = rom.Length - 1; - for (int i = 0; i < numSongTables; i++) - { - SongTableSizes[i] = ConfigUtils.ParseValue(nameof(SongTableSizes), sizes[i], 1, maxOffset); - SongTableOffsets[i] = (int)ConfigUtils.ParseValue(nameof(SongTableOffsets), songTables[i], 0, maxOffset); - } - - if (sampleRateNode is null) - { - throw new BetterKeyNotFoundException(nameof(SampleRate), null); - } - SampleRate = (int)ConfigUtils.ParseValue(nameof(SampleRate), sampleRateNode.ToString(), 0, MP2KUtils.FrequencyTable.Length - 1); - - if (reverbTypeNode is null) - { - throw new BetterKeyNotFoundException(nameof(ReverbType), null); - } - ReverbType = ConfigUtils.ParseEnum(nameof(ReverbType), reverbTypeNode.ToString()); - - if (reverbNode is null) - { - throw new BetterKeyNotFoundException(nameof(Reverb), null); - } - Reverb = (byte)ConfigUtils.ParseValue(nameof(Reverb), reverbNode.ToString(), byte.MinValue, byte.MaxValue); - - if (volumeNode is null) - { - throw new BetterKeyNotFoundException(nameof(Volume), null); - } - Volume = (byte)ConfigUtils.ParseValue(nameof(Volume), volumeNode.ToString(), 0, 15); - - if (hasGoldenSunSynthsNode is null) - { - throw new BetterKeyNotFoundException(nameof(HasGoldenSunSynths), null); - } - HasGoldenSunSynths = ConfigUtils.ParseBoolean(nameof(HasGoldenSunSynths), hasGoldenSunSynthsNode.ToString()); - - if (hasPokemonCompression is null) - { - throw new BetterKeyNotFoundException(nameof(HasPokemonCompression), null); - } - HasPokemonCompression = ConfigUtils.ParseBoolean(nameof(HasPokemonCompression), hasPokemonCompression.ToString()); - - // The complete playlist - ConfigUtils.TryCreateMasterPlaylist(Playlists); - } - catch (BetterKeyNotFoundException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigKeyMissing, ex.Key))); - } - catch (InvalidValueException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + ex.Message)); - } - catch (YamlDotNet.Core.YamlException ex) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + ex.Message)); - } - } - } - - public override string GetGameName() - { - return Name; - } - public override string GetSongName(int index) - { - if (TryGetFirstSong(index, out Song s)) - { - return s.Name; - } - return index.ToString(); - } + private const string CONFIG_FILE = "MP2K.yaml"; + + public override byte[] ROM { get; } + 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; + public override HSLColor[]? Colors { get; protected set; } + + internal MP2KConfig(byte[] rom, bool mainPlaylistFirst) + { + using (StreamReader fileStream = File.OpenText(ConfigUtils.CombineWithBaseDirectory(CONFIG_FILE))) + using (var ms = new MemoryStream(rom)) + { + string gcv = string.Empty; + try + { + ROM = rom; + var r = new EndianBinaryReader(ms, ascii: true); + r.Stream.Position = 0xAC; + GameCode = r.ReadString_Count(4); + r.Stream.Position = 0xBC; + Version = r.ReadByte(); + gcv = $"{GameCode}_{Version:X2}"; + var yaml = new YamlStream(); + yaml.Load(fileStream); + + var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; + YamlMappingNode game; + try + { + game = (YamlMappingNode)mapping.Children.GetValue(gcv); + } + catch (BetterKeyNotFoundException) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KMissingGameCode, gcv))); + } + + YamlNode? nameNode = null, + songTableOffsetsNode = null, + songTableSizesNode = null, + sampleRateNode = null, + reverbTypeNode = null, + reverbNode = null, + volumeNode = null, + hasGoldenSunSynthsNode = null, + hasPokemonCompression = null; + void Load(YamlMappingNode gameToLoad) + { + if (gameToLoad.Children.TryGetValue("Copy", out YamlNode? node)) + { + YamlMappingNode copyGame; + try + { + copyGame = (YamlMappingNode)mapping.Children.GetValue(node); + } + catch (BetterKeyNotFoundException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KCopyInvalidGameCode, ex.Key))); + } + Load(copyGame); + } + if (gameToLoad.Children.TryGetValue(nameof(Name), out node)) + { + nameNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SongTableOffsets), out node)) + { + songTableOffsetsNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SongTableSizes), out node)) + { + songTableSizesNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SampleRate), out node)) + { + sampleRateNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(ReverbType), out node)) + { + reverbTypeNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(Reverb), out node)) + { + reverbNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(Volume), out node)) + { + volumeNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(HasGoldenSunSynths), out node)) + { + hasGoldenSunSynthsNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(HasPokemonCompression), out node)) + { + hasPokemonCompression = node; + } + if (gameToLoad.Children.TryGetValue(nameof(InternalSongNames), out node)) + { + var internalNames = (YamlMappingNode)node; + var songs = new List(); + foreach (KeyValuePair song in internalNames) + { + string name = song.Key.ToString(); + int songIndex = (int)ConfigUtils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(InternalSongNames)), song.Key.ToString(), 0, int.MaxValue); + if (songs.Any(s => s.Index == songIndex)) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongRepeated, name, songIndex))); + } + songs.Add(new Song(songIndex, song.Value.ToString())); + InternalSongNames.Add(new InternalSongName(name, songs)); + } + } + if (gameToLoad.Children.TryGetValue(nameof(Playlists), out node)) + { + var playlists = (YamlMappingNode)node; + foreach (KeyValuePair kvp in playlists) + { + string name = kvp.Key.ToString(); + var songs = new List(); + foreach (KeyValuePair song in (YamlMappingNode)kvp.Value) + { + int songIndex = (int)ConfigUtils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Playlists)), song.Key.ToString(), 0, int.MaxValue); + if (songs.Any(s => s.Index == songIndex)) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongRepeated, name, songIndex))); + } + songs.Add(new Song(songIndex, song.Value.ToString())); + } + Playlists.Add(new Playlist(name, songs)); + } + } + } + + Load(game); + + if (nameNode is null) + { + throw new BetterKeyNotFoundException(nameof(Name), null); + } + Name = nameNode.ToString(); + + if (songTableOffsetsNode is null) + { + throw new BetterKeyNotFoundException(nameof(SongTableOffsets), null); + } + string[] songTables = songTableOffsetsNode.ToString().Split(' ', options: StringSplitOptions.RemoveEmptyEntries); + int numSongTables = songTables.Length; + if (numSongTables == 0) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigKeyNoEntries, nameof(SongTableOffsets)))); + } + + if (songTableSizesNode is null) + { + throw new BetterKeyNotFoundException(nameof(SongTableSizes), null); + } + string[] sizes = songTableSizesNode.ToString().Split(' ', options: StringSplitOptions.RemoveEmptyEntries); + if (sizes.Length != numSongTables) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongTableCounts, nameof(SongTableSizes), nameof(SongTableOffsets)))); + } + SongTableOffsets = new int[numSongTables]; + SongTableSizes = new long[numSongTables]; + int maxOffset = rom.Length - 1; + for (int i = 0; i < numSongTables; i++) + { + SongTableSizes[i] = ConfigUtils.ParseValue(nameof(SongTableSizes), sizes[i], 1, maxOffset); + SongTableOffsets[i] = (int)ConfigUtils.ParseValue(nameof(SongTableOffsets), songTables[i], 0, maxOffset); + } + SongTableOffset = SongTableOffsets; + + if (sampleRateNode is null) + { + throw new BetterKeyNotFoundException(nameof(SampleRate), null); + } + SampleRate = (int)ConfigUtils.ParseValue(nameof(SampleRate), sampleRateNode.ToString(), 0, MP2KUtils.FrequencyTable.Length - 1); + + if (reverbTypeNode is null) + { + throw new BetterKeyNotFoundException(nameof(ReverbType), null); + } + ReverbType = ConfigUtils.ParseEnum(nameof(ReverbType), reverbTypeNode.ToString()); + + if (reverbNode is null) + { + throw new BetterKeyNotFoundException(nameof(Reverb), null); + } + Reverb = (byte)ConfigUtils.ParseValue(nameof(Reverb), reverbNode.ToString(), byte.MinValue, byte.MaxValue); + + if (volumeNode is null) + { + throw new BetterKeyNotFoundException(nameof(Volume), null); + } + Volume = (byte)ConfigUtils.ParseValue(nameof(Volume), volumeNode.ToString(), 0, 15); + + if (hasGoldenSunSynthsNode is null) + { + throw new BetterKeyNotFoundException(nameof(HasGoldenSunSynths), null); + } + HasGoldenSunSynths = ConfigUtils.ParseBoolean(nameof(HasGoldenSunSynths), hasGoldenSunSynthsNode.ToString()); + + if (hasPokemonCompression is null) + { + throw new BetterKeyNotFoundException(nameof(HasPokemonCompression), null); + } + HasPokemonCompression = ConfigUtils.ParseBoolean(nameof(HasPokemonCompression), hasPokemonCompression.ToString()); + + // The complete playlist + if (mainPlaylistFirst) ConfigUtils.TryCreateMasterPlaylist.CreateFirst(Playlists); + else ConfigUtils.TryCreateMasterPlaylist.CreateLast(Playlists); + } + catch (BetterKeyNotFoundException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + string.Format(Strings.ErrorConfigKeyMissing, ex.Key))); + } + catch (InvalidValueException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, CONFIG_FILE, Environment.NewLine + ex.Message)); + } + catch (YamlDotNet.Core.YamlException ex) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, CONFIG_FILE, Environment.NewLine + ex.Message)); + } + } + } + + public override string GetGameName() + { + return Name; + } + public override string GetSongName(int index) + { + if (TryGetFirstSong(index, out Song s)) + { + return s.Name; + } + return index.ToString(); + } } diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs b/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs old mode 100644 new mode 100755 index 43c40baa..4acc1886 --- a/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs @@ -1,33 +1,67 @@ -using System.IO; +using System; +using System.Collections.Generic; +using System.IO; namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; public sealed class MP2KEngine : Engine { - public static MP2KEngine? MP2KInstance { get; private set; } - - public override MP2KConfig Config { get; } - public override MP2KMixer Mixer { get; } - public override MP2KPlayer Player { get; } - - public MP2KEngine(byte[] rom) - { - if (rom.Length > GBAUtils.CARTRIDGE_CAPACITY) - { - throw new InvalidDataException($"The ROM is too large. Maximum size is 0x{GBAUtils.CARTRIDGE_CAPACITY:X7} bytes."); - } - - Config = new MP2KConfig(rom); - Mixer = new MP2KMixer(Config); - Player = new MP2KPlayer(Config, Mixer); - - MP2KInstance = this; - Instance = this; - } - - public override void Dispose() - { - base.Dispose(); - MP2KInstance = null; - } + public static MP2KEngine? MP2KInstance { get; private set; } + + public override MP2KConfig Config { get; } + public override MP2KMixer Mixer { get; } + public override MP2KPlayer Player { get; } + + public override bool IsFileSystemFormat { get; } = false; + + private ICommand[]? _allowedCommands; + + public MP2KEngine(byte[] rom, bool mainPlaylistFirst = true) + { + if (rom.Length > GBAUtils.CARTRIDGE_CAPACITY) + { + throw new InvalidDataException($"The ROM is too large. Maximum size is 0x{GBAUtils.CARTRIDGE_CAPACITY:X7} bytes."); + } + + Config = new MP2KConfig(rom, mainPlaylistFirst); + Mixer = new MP2KMixer(Config); + Player = new MP2KPlayer(Config, Mixer); + + MP2KInstance = this; + Instance = this; + } + + public override ICommand[] GetCommands() + { + var types = new List(); + types.AddRange([ + typeof(TempoCommand), typeof(RestCommand), typeof(NoteCommand), typeof(EndOfTieCommand), + typeof(VoiceCommand), typeof(VolumeCommand), typeof(PanpotCommand), typeof(PitchBendCommand), + typeof(TuneCommand), typeof(PitchBendRangeCommand), typeof(LFOSpeedCommand), typeof(LFODelayCommand), + typeof(LFODepthCommand), typeof(LFOTypeCommand), typeof(PriorityCommand), typeof(TransposeCommand), + typeof(JumpCommand), typeof(CallCommand), typeof(ReturnCommand), typeof(FinishCommand), + typeof(RepeatCommand), typeof(MemoryAccessCommand), typeof(LibraryCommand) + ]); + + _allowedCommands = new ICommand[types.Count]; + int i = 0; + foreach (Type type in types) + { + _allowedCommands[i++] = (ICommand)Activator.CreateInstance(type)!; + } + + return _allowedCommands; + } + + public override void Reload() + { + var config = Config; + Dispose(); + _ = new MP2KEngine(config.ROM, false); + } + public override void Dispose() + { + base.Dispose(); + MP2KInstance = null; + } } diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KEnums.cs b/VG Music Studio - Core/GBA/MP2K/MP2KEnums.cs old mode 100644 new mode 100755 index 86029a67..8b606507 --- a/VG Music Studio - Core/GBA/MP2K/MP2KEnums.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KEnums.cs @@ -8,6 +8,7 @@ internal enum EnvelopeState : byte Rising, Decaying, Playing, + PseudoEcho, Releasing, Dying, Dead, @@ -45,26 +46,39 @@ internal enum NoisePattern : byte Fine, Rough, } +/// +/// These flags determines the voice type used, whenever it be either PCM8 data or a PSG format +/// internal enum VoiceType : byte { + /// Raw PCM8 data PCM8, + /// PSG Square Wave Type 1 Square1, + /// PSG Square Wave Type 2 Square2, + /// PSG PCM4 Wave Data PCM4, + /// PSG Noise Wave Noise, + /// Unused Invalid5, + /// Unused Invalid6, + /// Unused Invalid7, } +/// +/// These are flags that apply to the voice types +/// [Flags] internal enum VoiceFlags : byte { - // These are flags that apply to the types - /// PCM8 + /// PCM8 only Fixed = 0x08, - /// Square1, Square2, PCM4, Noise - OffWithNoise = 0x08, - /// PCM8 + /// Only used with PSG types: Square1, Square2, PCM4, Noise + OffWithNoise = 0x09, + /// PCM8 only Reversed = 0x10, /// PCM8 (Only in Pokémon main series games) Compressed = 0x20, @@ -73,3 +87,12 @@ internal enum VoiceFlags : byte KeySplit = 0x40, Drum = 0x80, } +internal enum LibraryCommandTypes : byte +{ + xIECV = 8, + xIECL = 9, +} +internal enum MemoryOperatorType : byte +{ + mem_set = 0, +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KInterfaces.cs b/VG Music Studio - Core/GBA/MP2K/MP2KInterfaces.cs new file mode 100755 index 00000000..04540e3d --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KInterfaces.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal interface IVoice : IVoiceInfo +{ + VoiceEntry? VoiceEntry { get; } + sbyte RootNote { get; } +} + +// Used in the Voice Group Editor +internal interface ISample : IOffset +{ + MP2KSample Sample { get; } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong.cs b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong.cs old mode 100644 new mode 100755 index b336371b..8dd85db4 --- a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong.cs @@ -3,44 +3,48 @@ namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; -internal sealed partial class MP2KLoadedSong : ILoadedSong +internal sealed partial class MP2KLoadedSong : LoadedSong { - public List[] Events { get; } - public long MaxTicks { get; private set; } - public int LongestTrack; - - private readonly MP2KPlayer _player; - private readonly int _voiceTableOffset; - public readonly MP2KTrack[] Tracks; - - public MP2KLoadedSong(MP2KPlayer player, int index) - { - _player = player; - - MP2KConfig cfg = player.Config; - var entry = SongEntry.Get(cfg.ROM, cfg.SongTableOffsets[0], index); - int headerOffset = entry.HeaderOffset - GBAUtils.CARTRIDGE_OFFSET; - - var header = SongHeader.Get(cfg.ROM, headerOffset, out int tracksOffset); - _voiceTableOffset = header.VoiceTableOffset - GBAUtils.CARTRIDGE_OFFSET; - - Tracks = new MP2KTrack[header.NumTracks]; - Events = new List[header.NumTracks]; - for (byte trackIndex = 0; trackIndex < header.NumTracks; trackIndex++) - { - int trackStart = SongHeader.GetTrackOffset(cfg.ROM, tracksOffset, trackIndex) - GBAUtils.CARTRIDGE_OFFSET; - Tracks[trackIndex] = new MP2KTrack(trackIndex, trackStart); - - AddTrackEvents(trackIndex, trackStart); - } - } - - public void CheckVoiceTypeCache(ref int? old, string?[] voiceTypeCache) - { - if (old != _voiceTableOffset) - { - old = _voiceTableOffset; - Array.Clear(voiceTypeCache); - } - } + public override List[] Events { get; } + public override long MaxTicks { get; protected set; } + public override int HeaderOffset { get; protected set; } + public override SoundBank Bank { get; protected set; } + public int LongestTrack; + + private readonly MP2KPlayer _player; + public readonly SongHeader Header; + private readonly int _soundBankOffset; + public readonly MP2KTrack[] Tracks; + + public MP2KLoadedSong(MP2KPlayer player, int index) + { + _player = player; + + MP2KConfig cfg = player.Config; + var entry = SongEntry.Get(cfg.ROM, cfg.SongTableOffsets[0], index); + HeaderOffset = entry.HeaderOffset - GBAUtils.CARTRIDGE_OFFSET; + + Header = SongHeader.Get(cfg.ROM, HeaderOffset, out int tracksOffset); + _soundBankOffset = Header.SoundBankOffset - GBAUtils.CARTRIDGE_OFFSET; + Bank = MP2KSoundBank.LoadTable(_soundBankOffset); + + Tracks = new MP2KTrack[Header.NumTracks]; + Events = new List[Header.NumTracks]; + for (byte trackIndex = 0; trackIndex < Header.NumTracks; trackIndex++) + { + int trackStart = SongHeader.GetTrackOffset(cfg.ROM, tracksOffset, trackIndex) - GBAUtils.CARTRIDGE_OFFSET; + Tracks[trackIndex] = new MP2KTrack(trackIndex, trackStart); + + AddTrackEvents(trackIndex, trackStart); + } + } + + public void CheckVoiceTypeCache(ref int? old, string?[] voiceTypeCache) + { + if (old != _soundBankOffset) + { + old = _soundBankOffset; + Array.Clear(voiceTypeCache); + } + } } diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_ASM.cs b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_ASM.cs new file mode 100755 index 00000000..8dcf0918 --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_ASM.cs @@ -0,0 +1,932 @@ +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal sealed class WholeNoteMark : ICommand +{ + public Color Color => Color.Azure; + public string Label => "Time Signature"; + public string Arguments => $"N/A"; +} + +internal sealed class CallFlag : ICommand +{ + public Color Color => Color.Azure; + public string Label => "Call Flag"; + public string Arguments => $"N/A"; +} + +internal sealed class JumpFlag : ICommand +{ + public Color Color => Color.Azure; + public string Label => "Jump Flag"; + public string Arguments => $"N/A"; +} + +internal sealed class RepeatFlag : ICommand +{ + public Color Color => Color.Azure; + public string Label => "Repeat Flag"; + public string Arguments => $"N/A"; +} + +public sealed class ASMSaveArgs +{ + public bool MergeRemainingDuration; + public string? VoicegroupLabel; + + public ASMSaveArgs(bool mergeRemainingDuration, string voicegroupLabel = "") + { + MergeRemainingDuration = mergeRemainingDuration; + VoicegroupLabel = voicegroupLabel; + } +} + +internal sealed partial class MP2KLoadedSong +{ + public override void OpenASM(Assembler assembler, string headerLabel) + { + HeaderOffset = assembler.BaseOffset; + } + public override void SaveAsASM(string fileName, ASMSaveArgs args) + { + using (var file = new StreamWriter(fileName)) + { + string label = Assembler.FixLabel(Path.GetFileNameWithoutExtension(fileName)); + if (args.VoicegroupLabel == "") + { + args.VoicegroupLabel = label.Replace("mus_", ""); + } + string prevParam1 = ""; + string prevParam2 = ""; + string prevParam3 = ""; + string prevParam4 = ""; + ICommand prevCommand = null!; + ICommand nextCommand = null!; + int prevRest = 0; + SongHeader header = ((MP2KLoadedSong)MP2KEngine.MP2KInstance!.Player.LoadedSong!).Header; + byte baseVolume = Events.SelectMany(e => e).Where(e => e.Command is VolumeCommand).Select(e => ((VolumeCommand)e.Command).Volume).Max(); + byte reverb = (byte)(header.Reverb << 1); // Reverb value begins as too high, so it needs to be left shifted to delete the 8th bit + reverb = (byte)(reverb >> 1); // Right shifted to shift the bits back to their original location + file.WriteLine("\t.include \"MPlayDef.s\""); + file.WriteLine(); + file.WriteLine($"\t.equ\t{label}_grp, voicegroup_{args.VoicegroupLabel}"); + file.WriteLine($"\t.equ\t{label}_pri, {header.Priority}"); + file.WriteLine($"\t.equ\t{label}_rev, reverb_set+{reverb}"); + file.WriteLine($"\t.equ\t{label}_mvl, {baseVolume}"); + file.WriteLine($"\t.equ\t{label}_key, 0"); + file.WriteLine($"\t.equ\t{label}_tbs, 1"); + file.WriteLine($"\t.equ\t{label}_exg, 1"); + file.WriteLine($"\t.equ\t{label}_cmp, 1"); + file.WriteLine(); + file.WriteLine("\t.section .rodata"); + file.WriteLine($"\t.global\t{label}"); + file.WriteLine("\t.align\t2"); + + for (int trackIndex = 0; trackIndex < Events.Length; trackIndex++) + { + int num = trackIndex + 1; + file.WriteLine(); + file.WriteLine($"@**************** Track {num} (Midi-Chn.{num}) ****************@"); + file.WriteLine(); + file.WriteLine($"{label}_{num}:"); + + //IEnumerable offsets = Events[i].Where(e => e.Command is CallCommand || e.Command is JumpCommand || e.Command is RepeatCommand) + // .Select(e => (int)((dynamic)e).Command.Offset).Distinct(); // Get all offsets we need labels for + IEnumerable evts = Events[trackIndex].Where(e => e.Command is CallCommand || e.Command is JumpCommand || e.Command is RepeatCommand); + int jumps = 1; + int repeats = 1; + var labelsGOTO = new Dictionary(); + var labelsPATT = new Dictionary(); + var labelsREPT = new Dictionary(); + foreach (SongEvent e in evts) + { + switch (e.Command) + { + //case CallCommand c: + // { + // if (!labels.ContainsKey(c.Offset)) + // { + // labels.Add(c.Offset, $"{label}_{num}_B{jumps++}"); + // } + // break; + // } + case JumpCommand j: + { + if (!labelsGOTO.ContainsKey(j.Offset)) + { + labelsGOTO.Add(j.Offset, $"{label}_{num}_B{jumps++}"); + } + break; + } + case RepeatCommand r: + { + if (!labelsREPT.ContainsKey(r.Offset)) + { + labelsREPT.Add(r.Offset, $"{label}_{num}_L{repeats++}"); + } + break; + } + } + } + //foreach (int o in offsets) + //{ + // labels.Add(o, $"{label}_{num}_{jumps++:D3}"); + //} + int ticks = 0; + bool wholeNoteMarkDisplayed = false; // So that we can have conditional statements to prevent time signature duplicates + bool wholeNoteMarkPattDisplayed = false; // This is for conditional statements so that note commands will write note type and velocity + bool wholeNoteMarkRestBegin = false; // When the song begins with a rest command + bool wholeNoteMarkNoteBegin = false; // When the song begins with a note command + int wholeNoteNum = 0; // The index number for a regular Time Signature or Pattern + int wholeNoteGotoNum = 0; // The index number for a Goto Time Signature or Pattern + List trackEvents = Events[trackIndex]; + List trackEventsEX = []; + int ticksEX = 0; + // bool callTimeSignatureEnabled = false; + // bool callTimeSignatureDisplayed = false; + // bool jumpTimeSignatureDisplayed = false; + // bool returnTimeSignatureDisplayed = false; + for (int i = trackEvents.Count - 1; i > -1; i--) + { + SongEvent e = trackEvents[i]; + int eOffset = (int)e.Offset; + switch (e.Command) + { + case RestCommand c: + { + byte amt = (byte)MP2KUtils.RestTable.BinarySearch(c.Rest); + int rem = c.Rest - amt; + if (rem is not 0) + { + amt += (byte)rem; + } + if (((ticksEX % 96 == 0) || ((ticksEX % 96) + amt) > 96) && !wholeNoteMarkDisplayed) + { + trackEventsEX.Insert(0, new SongEvent(0, new WholeNoteMark())); + } + else + { + wholeNoteMarkDisplayed = false; + } + trackEventsEX.Insert(0, e); + var ex = 0; + if (((ticksEX % 96) + amt) > 96) + { + ex = 96 - ((ticksEX + amt) % 96); + } + ticksEX += amt + ex; // TODO: Separate by 96 ticks + // returnTimeSignatureDisplayed = false; + // jumpTimeSignatureDisplayed = false; + // callTimeSignatureDisplayed = false; + break; + } + case EndOfTieCommand c: + { + if ((ticksEX % 96) == 0 && !wholeNoteMarkDisplayed) + { + trackEventsEX.Insert(0, new SongEvent(0, new WholeNoteMark())); + wholeNoteMarkDisplayed = true; + } + trackEventsEX.Insert(0, e); + break; + } + case CallCommand c: + { + if (!wholeNoteMarkDisplayed) + { + trackEventsEX.Insert(0, new SongEvent(0, new WholeNoteMark())); + } + trackEventsEX.Insert(0, e); + trackEventsEX.Insert(0, new SongEvent(0, new WholeNoteMark())); + wholeNoteMarkDisplayed = true; + if ((ticksEX % 96) != 0) + { + ticksEX += 96 - (ticksEX % 96); + } + // callTimeSignatureEnabled = true; + break; + } + case JumpCommand c: + { + if ((ticksEX % 96) == 0 && !wholeNoteMarkDisplayed) + { + trackEventsEX.Insert(0, new SongEvent(0, new WholeNoteMark())); + wholeNoteMarkDisplayed = true; + } + trackEventsEX.Insert(0, e); + break; + } + case ReturnCommand c: + { + if ((ticksEX % 96) == 0 && !wholeNoteMarkDisplayed) + { + trackEventsEX.Insert(0, new SongEvent(0, new WholeNoteMark())); + wholeNoteMarkDisplayed = true; + } + trackEventsEX.Insert(0, e); + break; + } + default: + { + trackEventsEX.Insert(0, e); + break; + } + } + foreach (SongEvent ev in evts) + { + if (ev.Command is RepeatCommand r && r.Offset == eOffset) + { + trackEventsEX.Insert(0, new SongEvent(eOffset, new RepeatFlag())); + break; + } + } + foreach (SongEvent ev in evts) + { + if (ev.Command is CallCommand c && c.Offset == eOffset) + { + trackEventsEX.Insert(0, new SongEvent(eOffset, new CallFlag())); + wholeNoteMarkDisplayed = true; + if ((ticksEX % 96) != 0) + { + ticksEX += 96 - (ticksEX % 96); + } + break; + } + } + foreach (SongEvent ev in evts) + { + if (ev.Command is JumpCommand j && j.Offset == eOffset) + { + if (!wholeNoteMarkDisplayed) + { + trackEventsEX.Insert(0, new SongEvent(0, new WholeNoteMark())); + wholeNoteMarkDisplayed = true; + } + trackEventsEX.Insert(0, new SongEvent(eOffset, new JumpFlag())); + break; + } + } + } + wholeNoteMarkDisplayed = false; + for (int i = 0; i < trackEventsEX.Count; i++) + { + SongEvent e = trackEventsEX[i]; + // prevCommand = i > 0 ? trackEvents[i - 1].Command : null!; // If the trackEvents index is more than 0, it'll apply the prevCommand, otherwise it's null + // nextCommand = i < trackEventsEX.Count - 1 ? trackEventsEX[i + 1].Command : null!; // If the trackEvents index is less than the number of trackEvents, it'll apply the nextCommand, otherwise it's null + int eOffset = (int)e.Offset; + // string prevLabel = ""; + // foreach (SongEvent ev in evts) + // { + // if (ev.Command is JumpCommand j && j.Offset == e.Offset) + // { + // if (prevLabel != "GOTO") + // { + // file.WriteLine($"{labelsGOTO[eOffset]}:"); + // file.WriteLine($"@ {timeSignatureNum:D3} ----------------------------------------"); + // timeSignatureDisplayed = true; + // timeSignatureGotoNum = timeSignatureNum; + // prevParam1 = ""; + // prevParam2 = ""; + // prevParam3 = ""; + // prevLabel = "GOTO"; + // } + // break; + // } + // } + // foreach (SongEvent ev in evts) + // { + // if (ev.Command is RepeatCommand r && r.Offset == e.Offset) + // { + // if (prevLabel != "REPT") + // { + // file.WriteLine($"{labelsREPT[eOffset]}:"); + // file.WriteLine($"@ {timeSignatureNum++:D3} ----------------------------------------"); + // timeSignatureDisplayed = true; + // prevParam1 = ""; + // prevParam2 = ""; + // prevParam3 = ""; + // prevLabel = "REPT"; + // } + // break; + // } + // } + // foreach (SongEvent ev in evts) + // { + // if (ev.Command is CallCommand c && c.Offset == e.Offset) + // { + // if (prevLabel != "PATT") + // { + // if (!timeSignatureDisplayed) + // { + // file.WriteLine($"@ {timeSignatureNum:D3} ----------------------------------------"); + // timeSignatureDisplayed = true; + // timeSignaturePattDisplayed = true; + // labelsPATT.Remove(eOffset); + // labelsPATT.Add(eOffset, $"{label}_{num}_{timeSignatureNum++:D3}"); + // } + // else + // { + // labelsPATT.Remove(eOffset); + // labelsPATT.Add(eOffset, $"{label}_{num}_{timeSignatureNum++:D3}"); + // } + // file.WriteLine($"{labelsPATT[eOffset]}:"); + // prevParam1 = ""; + // prevParam2 = ""; + // prevLabel = "PATT"; + // } + // continue; + // } + // } + var rmd96 = ticks % 96; + var tdiv96 = ticks / 96; + // if (!timeSignatureDisplayed && (ticks % 96 == 0) && (ticks / 96 != 0) && (e.Command is not JumpCommand && e.Command is not RepeatCommand && e.Command is not ReturnCommand && e.Command is not EndOfTieCommand)) + // { + // file.WriteLine($"@ {timeSignatureNum++:D3} ----------------------------------------"); + // timeSignatureDisplayed = true; + // } + //if (offsets.Contains(eOffset)) + //{ + // file.WriteLine($"{labels[eOffset]}:"); + // prevParam1 = ""; + // prevParam2 = ""; + // prevParam3 = ""; + //} + switch (e.Command) + { + case null: + continue; + case TempoCommand c: + file.WriteLine($"\t.byte\tTEMPO , {c.Tempo}*{label}_tbs/2"); + prevParam1 = "TEMPO"; + prevParam2 = $"{c.Tempo}*{label}_tbs/2"; + prevParam3 = ""; + prevParam4 = ""; + prevCommand = c; + break; + case RestCommand c: + { + byte amt = (byte)MP2KUtils.RestTable.BinarySearch(c.Rest); + int rem = c.Rest - amt; + if (rem is not 0) + { + amt += (byte)rem; + } + var t = ticks % 96; + var ta = t + amt; + ticks += amt; // TODO: Separate by 96 ticks + if (!wholeNoteMarkRestBegin && !wholeNoteMarkNoteBegin && (!wholeNoteMarkDisplayed || !wholeNoteMarkPattDisplayed)) + { + // file.WriteLine($"@ {timeSignatureNum++:D3} ----------------------------------------"); + // timeSignatureDisplayed = true; + wholeNoteMarkRestBegin = true; + var r = 96 - (ticks % 96); + ticks += r; + } + if (ta > 96) + { + ticks += 96 - (ticks % 96); + } + file.WriteLine($"\t.byte\tW{amt:D2}"); + //if (rem != 0) + //{ + // file.WriteLine($"\t.byte\tW{rem:D2}"); + //} + if (prevCommand is not RestCommand || amt == 96) + { + wholeNoteMarkDisplayed = false; + } + if (wholeNoteGotoNum == wholeNoteNum) + { + wholeNoteNum++; + } + prevRest = c.Rest; + prevCommand = c; + break; + } + case NoteCommand c: + { + // Hide base note, velocity and duration + byte baseDur = c.Duration == -1 ? (byte)0 : (byte)MP2KUtils.RestTable.BinarySearch((byte)c.Duration); + int rem = c.Duration - baseDur; + if (rem is not 0) + { + baseDur = (byte)(baseDur + rem); + rem = c.Duration - baseDur; + } + string name = c.Duration == -1 ? "TIE" : $"N{baseDur:D2}"; + string not = ConfigUtils.GetKeyNameASM(c.Note); + string vel = $"v{c.Velocity:D3}"; + + var n = "\t.byte\t\t"; + if (name != prevParam1 || prevCommand is NoteCommand) + { + if (name != prevParam1 || prevCommand is not NoteCommand || wholeNoteMarkPattDisplayed) + { + n += $"{name} "; + } + if (not != prevParam2 || vel != prevParam3 || wholeNoteMarkPattDisplayed) + { + if (n == "\t.byte\t\t") + { + n += $"{name} "; + } + n += $", {not} "; + prevParam2 = not; + } + } + else + { + if (not != prevParam2 || vel != prevParam3) + { + n += $" {not} "; + prevParam2 = not; + } + } + if (name == "TIE" && prevParam1 == "EOT" && !wholeNoteMarkDisplayed) + { + file.WriteLine($"@ {wholeNoteNum++:D3} ----------------------------------------"); + wholeNoteMarkDisplayed = true; + } + prevParam1 = name; + if (c.Duration != -1 && rem != 0) + { + if ($"gtp{rem}" != prevParam4) + { + n += vel != prevParam3 ? $", {vel}, gtp{rem}" : $" , gtp{rem}"; + prevParam3 = vel; + prevParam4 = $"gtp{rem}"; + } + else + { + n += vel != prevParam3 ? $", {vel}" : ""; + prevParam3 = vel; + prevParam4 = ""; + } + } + else + { + if (vel != prevParam3 || wholeNoteMarkPattDisplayed) + { + if (n == "\t.byte\t\t") + { + n += $"{name} "; + } + n += $", {vel}"; + } + else + { + n += ""; + } + prevParam3 = vel; + prevParam4 = ""; + } + + if (n == "\t.byte\t\t") + { + n += $"{name} "; + } + file.WriteLine(n); + prevCommand = c; + wholeNoteMarkPattDisplayed = false; + if (!wholeNoteMarkRestBegin && !wholeNoteMarkNoteBegin) + { + wholeNoteMarkNoteBegin = true; + } + + break; + } + + case EndOfTieCommand c: + { + if (c.Note == -1) + { + file.WriteLine("\t.byte\t\tEOT "); + wholeNoteMarkDisplayed = false; + prevParam1 = "EOT"; + } + else + { + file.WriteLine($"\t.byte\t\tEOT , {ConfigUtils.GetKeyNameASM(c.Note)}"); + wholeNoteMarkDisplayed = true; + prevParam1 = "EOT"; + prevParam2 = $"{ConfigUtils.GetKeyNameASM(c.Note)}"; + } + prevCommand = c; + break; + } + case VoiceCommand c: + { + file.WriteLine($"\t.byte\t\tVOICE , {c.Voice}"); + prevParam1 = "VOICE"; + prevCommand = c; + break; + } + case VolumeCommand c: + { + var v = $"\t.byte\t\t"; + v += "VOL" != prevParam1 ? "VOL ," : " "; + double d = baseVolume / (double)0x7F; + int vol = (int)(c.Volume / d); + // If there are rounding errors, fix them (happens if baseVolume is not 127 and baseVolume is not vol.Volume) + if (vol * baseVolume / 0x7F == c.Volume - 1) + { + vol++; + } + v += $" {vol}*{label}_mvl/mxv"; + prevParam1 = "VOL"; + prevCommand = c; + file.WriteLine(v); + break; + } + case PanpotCommand c: + { + var pan = $"\t.byte\t\t"; + if (prevParam1 != "PAN") + { + pan += "PAN "; + pan += $", {ConfigUtils.CenterValueString(c.Panpot)}"; + } + else + { + if (prevParam2 != $"{ConfigUtils.CenterValueString(c.Panpot)}") + { + pan += " "; + pan += $" {ConfigUtils.CenterValueString(c.Panpot)}"; + } + } + if (pan == $"\t.byte\t\t") + { + pan += $"PAN , {ConfigUtils.CenterValueString(c.Panpot)}"; + } + prevParam1 = "PAN"; + prevCommand = c; + file.WriteLine(pan); + break; + } + case PitchBendCommand c: + { + var bend = $"\t.byte\t\t"; + if (prevParam1 != "BEND") + { + bend += "BEND "; + bend += $", {ConfigUtils.CenterValueString(c.Bend)}"; + } + else + { + if (prevParam2 != $"{ConfigUtils.CenterValueString(c.Bend)}") + { + bend += " "; + bend += $" {ConfigUtils.CenterValueString(c.Bend)}"; + } + } + if (bend == $"\t.byte\t\t") + { + bend += $"BEND , {ConfigUtils.CenterValueString(c.Bend)}"; + } + prevParam1 = "BEND"; + prevCommand = c; + file.WriteLine(bend); + break; + } + case TuneCommand c: + { + var tune = $"\t.byte\t\t"; + if (prevParam1 != "TUNE") + { + tune += "TUNE "; + tune += $", {ConfigUtils.CenterValueString(c.Tune)}"; + } + else + { + if (prevParam2 != $"{ConfigUtils.CenterValueString(c.Tune)}") + { + tune += " "; + tune += $" {ConfigUtils.CenterValueString(c.Tune)}"; + } + } + if (tune == $"\t.byte\t\t") + { + tune += $"TUNE , {ConfigUtils.CenterValueString(c.Tune)}"; + } + prevParam1 = "TUNE"; + prevCommand = c; + file.WriteLine(tune); + break; + } + case PitchBendRangeCommand c: + { + var bendr = $"\t.byte\t\t"; + if (prevParam1 != "BENDR") + { + bendr += "BENDR "; + bendr += $", {c.Range}"; + } + else + { + if (prevParam2 != $"{c.Range}") + { + bendr += " "; + bendr += $" {c.Range}"; + } + } + if (bendr == $"\t.byte\t\t") + { + bendr += $"BENDR , {c.Range}"; + } + prevParam1 = "BENDR"; + prevCommand = c; + file.WriteLine(bendr); + break; + } + case LFOSpeedCommand c: + { + var lfos = $"\t.byte\t\t"; + if (prevParam1 != "LFOS") + { + lfos += "LFOS "; + lfos += $", {c.Speed}"; + } + else + { + if (prevParam2 != $"{c.Speed}") + { + lfos += " "; + lfos += $" {c.Speed}"; + } + } + if (lfos == $"\t.byte\t\t") + { + lfos += $"LFOS , {c.Speed}"; + } + prevParam1 = "LFOS"; + prevCommand = c; + file.WriteLine(lfos); + break; + } + case LFODelayCommand c: + { + var lfodl = $"\t.byte\t\t"; + if (prevParam1 != "LFODL") + { + lfodl += "LFODL "; + lfodl += $", {c.Delay}"; + } + else + { + if (prevParam2 != $"{c.Delay}") + { + lfodl += " "; + lfodl += $" {c.Delay}"; + } + } + if (lfodl == $"\t.byte\t\t") + { + lfodl += $"LFODL , {c.Delay}"; + } + prevParam1 = "LFODL"; + prevCommand = c; + file.WriteLine(lfodl); + break; + } + case LFODepthCommand c: + { + var mod = $"\t.byte\t\t"; + if (prevParam1 != "MOD") + { + mod += "MOD "; + mod += $", {c.Depth}"; + } + else + { + if (prevParam2 != $"{c.Depth}") + { + mod += " "; + mod += $" {c.Depth}"; + } + } + if (mod == $"\t.byte\t\t") + { + mod += $"MOD , {c.Depth}"; + } + prevParam1 = "MOD"; + prevCommand = c; + file.WriteLine(mod); + break; + } + case LFOTypeCommand c: + { + var modt = $"\t.byte\t\t"; + if (prevParam1 != "MODT") + { + modt += "MODT "; + modt += $", {c.Type}"; + } + else + { + if (prevParam2 != $"{c.Type}") + { + modt += " "; + modt += $" {c.Type}"; + } + } + if (modt == $"\t.byte\t\t") + { + modt += $"MODT , {c.Type}"; + } + prevParam1 = "MODT"; + prevCommand = c; + file.WriteLine(modt); + break; + } + case PriorityCommand c: + { + file.WriteLine($"\t.byte\tPRIO , {c.Priority}"); + prevParam1 = "PRIO"; + prevCommand = c; + break; + } + case TransposeCommand c: + { + file.WriteLine($"\t.byte\tKEYSH , {label}_key+{c.Transpose}"); + file.WriteLine($"@ {wholeNoteNum++:D3} ----------------------------------------"); + wholeNoteMarkDisplayed = true; + prevParam1 = "KEYSH"; + prevParam2 = $"{label}_key+{c.Transpose}"; + prevParam3 = ""; + prevParam4 = ""; + prevCommand = c; + break; + } + case JumpCommand c: + { + file.WriteLine("\t.byte\tGOTO"); + file.WriteLine($"\t .word\t{labelsGOTO[c.Offset]}"); + if (labelsGOTO.TryGetValue(eOffset, out string? value)) + { + file.WriteLine($"{value}:"); + } + else + { + file.WriteLine($"{label}_{num}_B{jumps++}:"); + } + // file.WriteLine($"@ {timeSignatureNum++:D3} ----------------------------------------"); + // timeSignatureDisplayed = true; + prevParam1 = "GOTO"; + prevParam2 = ""; + prevParam3 = ""; + prevParam4 = ""; + prevCommand = c; + break; + } + case RepeatCommand c: + { + file.WriteLine($"\t.byte\t\tREPT , {c.Times}"); + file.WriteLine($"\t .word\t{labelsREPT[c.Offset]}"); + file.WriteLine($"@ {wholeNoteNum++:D3} ----------------------------------------"); + wholeNoteMarkDisplayed = true; + prevParam1 = "REPT"; + prevParam2 = $"{c.Times}"; + prevParam3 = ""; + prevParam4 = ""; + prevCommand = c; + break; + } + case FinishCommand c: + { + if (c.Type == 0xB1) + { + file.WriteLine("\t.byte\tFINE"); + } + else + { + file.WriteLine("\t.byte\t0xB6\t@PREV"); + } + prevCommand = c; + break; + } + case CallCommand c: + { + file.WriteLine("\t.byte\tPATT"); + file.WriteLine($"\t .word\t{labelsPATT[c.Offset]}"); + //file.WriteLine($"@ {separatorNum++:D3} ----------------------------------------"); + //displayed = true; + wholeNoteMarkDisplayed = false; + if (wholeNoteGotoNum == wholeNoteNum) + { + wholeNoteNum++; + } + prevParam1 = "PATT"; + prevParam2 = ""; + prevParam3 = ""; + prevCommand = c; + break; + } + case ReturnCommand c: + { + file.WriteLine("\t.byte\tPEND"); + wholeNoteMarkDisplayed = false; + prevCommand = c; + break; + } + case MemoryAccessCommand c: + { + file.WriteLine($"\t.byte\tMEMACC, {c.Operator}, 0x{c.Address:X2}, {c.Data}"); + prevParam1 = "MEMACC"; + prevParam2 = $"{c.Operator,4}"; + prevParam3 = $"{c.Address,4}"; + prevParam4 = $"{c.Data}"; + prevCommand = c; + break; + } + case LibraryCommand c: + { + var xcmd = $"\t.byte\t\t"; + if (prevParam1 != "XCMD") + { + xcmd += "XCMD ,"; + xcmd += $" {c.LibraryCommandType} , {c.Argument}"; + } + else + { + xcmd += " "; + xcmd += $" {c.LibraryCommandType} , {c.Argument}"; + } + file.WriteLine(xcmd); + prevParam1 = "XCMD"; + prevParam2 = $"{c.LibraryCommandType}"; + prevParam3 = $"{c.Argument}"; + prevCommand = c; + break; + } + case WholeNoteMark t: + { + file.WriteLine($"@ {wholeNoteNum++:D3} ----------------------------------------"); + wholeNoteMarkDisplayed = true; + break; + } + case JumpFlag j: + { + file.WriteLine($"{labelsGOTO[eOffset]}:"); + // file.WriteLine($"@ {timeSignatureNum:D3} ----------------------------------------"); + wholeNoteMarkDisplayed = true; + wholeNoteGotoNum = wholeNoteNum; + prevParam1 = ""; + prevParam2 = ""; + prevParam3 = ""; + prevParam4 = ""; + break; + } + case RepeatFlag r: + { + file.WriteLine($"{labelsREPT[eOffset]}:"); + file.WriteLine($"@ {wholeNoteNum++:D3} ----------------------------------------"); + wholeNoteMarkDisplayed = true; + break; + } + case CallFlag c: + { + file.WriteLine($"@ {wholeNoteNum:D3} ----------------------------------------"); + wholeNoteMarkDisplayed = true; + wholeNoteMarkPattDisplayed = true; + labelsPATT.Remove(eOffset); + labelsPATT.Add(eOffset, $"{label}_{num}_{wholeNoteNum++:D3}"); + file.WriteLine($"{labelsPATT[eOffset]}:"); + prevParam1 = ""; + prevParam2 = ""; + prevParam3 = ""; + prevParam4 = ""; + break; + } + } + // prevLabel = ""; + } + } + + file.WriteLine(); + file.WriteLine("@******************************************************@"); + file.WriteLine("\t.align\t2"); + file.WriteLine(); + file.WriteLine($"{label}:"); + file.WriteLine($"\t.byte\t{Tracks.Length}\t@ NumTrks"); + file.WriteLine($"\t.byte\t{(this is MP2KLoadedSong mp2kSequence ? mp2kSequence.Header.NumBlocks : 0)}\t@ NumBlks"); + file.WriteLine($"\t.byte\t{label}_pri\t@ Priority"); + file.WriteLine($"\t.byte\t{label}_rev\t@ Reverb."); + file.WriteLine(); + file.WriteLine($"\t.word\t{label}_grp"); + file.WriteLine(); + for (int i = 0; i < Tracks.Length; i++) + { + file.WriteLine($"\t.word\t{label}_{i + 1}"); + } + + file.WriteLine(); + file.WriteLine("\t.end"); + } + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Events.cs b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Events.cs old mode 100644 new mode 100755 index ab62db76..8b7c470b --- a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Events.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Events.cs @@ -8,539 +8,589 @@ 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 AddEvent(byte trackIndex, long cmdOffset, ICommand command) + { + Events[trackIndex].Add(new SongEvent(cmdOffset, command)); + } + public override void InsertEvent(SongEvent e, int trackIndex, int insertIndex) + { + Events[trackIndex]!.Insert(insertIndex, e); + CalculateTicks(trackIndex); + } + public override void ChangeEvent(SongEvent ev, decimal vArgsVal1, byte vArgsVal2, bool changed) + { + if (ev.Command is VoiceCommand voice && voice.Voice == vArgsVal1) + { + voice.Voice = vArgsVal2; + changed = true; + } + } + public override void RemoveEvent(int trackIndex, int eventIndex) + { + Events[trackIndex].RemoveAt(eventIndex); + CalculateTicks(trackIndex); + } + public override bool CallOrJumpCommand(SongEvent e) + { + return e.Command is CallCommand || e.Command is JumpCommand; + } + 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; - } + 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), - }); - } + AddEvent(trackIndex, cmdOffset, new NoteCommand + { + Note = key, + Velocity = velocity, + Duration = runCmd == 0xCF ? -1 : (MP2KUtils.RestTable[runCmd - 0xCF] + addedDuration), + }); + } - private void AddTrackEvents(byte trackIndex, long trackStart) - { - Events[trackIndex] = new List(); - byte runCmd = 0; - byte prevKey = 0; - byte prevVelocity = 0x7F; - int callStackDepth = 0; - AddEvents(trackIndex, trackStart, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); - } - private void AddEvents(byte trackIndex, long startOffset, ref byte runCmd, ref byte prevKey, ref byte prevVelocity, ref int callStackDepth) - { - using (var ms = new MemoryStream(_player.Config.ROM)) - { - var r = new EndianBinaryReader(ms, ascii: true); - r.Stream.Position = startOffset; + private void AddTrackEvents(byte trackIndex, long trackStart) + { + Events[trackIndex] = new List(); + byte runCmd = 0; + byte prevKey = 0; + byte prevVelocity = 0x7F; + int callStackDepth = 0; + AddEvents(trackIndex, trackStart, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); + } + private void AddEvents(byte trackIndex, long startOffset, ref byte runCmd, ref byte prevKey, ref byte prevVelocity, ref int callStackDepth) + { + using (var ms = new MemoryStream(_player.Config.ROM)) + { + var r = new EndianBinaryReader(ms, ascii: true); + r.Stream.Position = startOffset; - Span peek = stackalloc byte[3]; - bool cont = true; - while (cont) - { - long offset = r.Stream.Position; + 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; - } + byte cmd = r.ReadByte(); + if (cmd >= 0xBD) // Commands that work within running status + { + runCmd = cmd; + } - #region TIE & Notes + #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); - } + 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 + #endregion - #region Rests + #region Rests - else if (cmd is >= 0x80 and <= 0xB0) - { - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new RestCommand { Rest = MP2KUtils.RestTable[cmd - 0x80] }); - } - } + else if (cmd is >= 0x80 and <= 0xB0) + { + 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) - { - case 0xBD: - { - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new VoiceCommand { Voice = cmd }); - } - break; - } - case 0xBE: - { - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new VolumeCommand { Volume = cmd }); - } - break; - } - case 0xBF: - { - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new PanpotCommand { Panpot = (sbyte)(cmd - 0x40) }); - } - break; - } - case 0xC0: - { - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new PitchBendCommand { Bend = (sbyte)(cmd - 0x40) }); - } - break; - } - case 0xC1: - { - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new PitchBendRangeCommand { Range = cmd }); - } - break; - } - case 0xC2: - { - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new LFOSpeedCommand { Speed = cmd }); - } - break; - } - case 0xC3: - { - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new LFODelayCommand { Delay = cmd }); - } - break; - } - case 0xC4: - { - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new LFODepthCommand { Depth = cmd }); - } - break; - } - case 0xC5: - { - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new LFOTypeCommand { Type = (LFOType)cmd }); - } - break; - } - case 0xC8: - { - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new TuneCommand { Tune = (sbyte)(cmd - 0x40) }); - } - break; - } - case 0xCD: - { - byte arg = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new LibraryCommand { Command = cmd, Argument = arg }); - } - break; - } - case 0xCE: - { - prevKey = cmd; - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new EndOfTieCommand { Note = cmd }); - } - break; - } - default: throw new MP2KInvalidRunningStatusCMDException(trackIndex, (int)offset, runCmd); - } - } - else if (cmd is > 0xB0 and < 0xCF) - { - switch (cmd) - { - case 0xB1: - case 0xB6: - { - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new FinishCommand { Prev = cmd == 0xB6 }); - } - cont = false; - break; - } - case 0xB2: - { - int jumpOffset = r.ReadInt32() - GBAUtils.CARTRIDGE_OFFSET; - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new JumpCommand { Offset = jumpOffset }); - if (!EventExists(trackIndex, jumpOffset)) - { - AddEvents(trackIndex, jumpOffset, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); - } - } - cont = false; - break; - } - case 0xB3: - { - int callOffset = r.ReadInt32() - GBAUtils.CARTRIDGE_OFFSET; - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new CallCommand { Offset = callOffset }); - } - if (callStackDepth < 3) - { - long backup = r.Stream.Position; - callStackDepth++; - AddEvents(trackIndex, callOffset, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); - r.Stream.Position = backup; - } - else - { - throw new MP2KTooManyNestedCallsException(trackIndex); - } - break; - } - case 0xB4: - { - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new ReturnCommand()); - } - if (callStackDepth != 0) - { - cont = false; - callStackDepth--; - } - break; - } - /*case 0xB5: // TODO: Logic so this isn't an infinite loop - { - byte times = config.Reader.ReadByte(); - int repeatOffset = config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; - if (!EventExists(offset, trackEvents)) - { - AddEvent(new RepeatCommand { Times = times, Offset = repeatOffset }); - } - break; - }*/ - case 0xB9: - { - byte op = r.ReadByte(); - byte address = r.ReadByte(); - byte data = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new MemoryAccessCommand { Operator = op, Address = address, Data = data }); - } - break; - } - case 0xBA: - { - byte priority = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new PriorityCommand { Priority = priority }); - } - break; - } - case 0xBB: - { - byte tempoArg = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new TempoCommand { Tempo = (ushort)(tempoArg * 2) }); - } - break; - } - case 0xBC: - { - sbyte transpose = r.ReadSByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new TransposeCommand { Transpose = transpose }); - } - break; - } - // Commands that work within running status: - case 0xBD: - { - byte voice = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new VoiceCommand { Voice = voice }); - } - break; - } - case 0xBE: - { - byte volume = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new VolumeCommand { Volume = volume }); - } - break; - } - case 0xBF: - { - byte panArg = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); - } - break; - } - case 0xC0: - { - byte bendArg = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new PitchBendCommand { Bend = (sbyte)(bendArg - 0x40) }); - } - break; - } - case 0xC1: - { - byte range = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new PitchBendRangeCommand { Range = range }); - } - break; - } - case 0xC2: - { - byte speed = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new LFOSpeedCommand { Speed = speed }); - } - break; - } - case 0xC3: - { - byte delay = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new LFODelayCommand { Delay = delay }); - } - break; - } - case 0xC4: - { - byte depth = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new LFODepthCommand { Depth = depth }); - } - break; - } - case 0xC5: - { - byte type = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new LFOTypeCommand { Type = (LFOType)type }); - } - break; - } - case 0xC8: - { - byte tuneArg = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new TuneCommand { Tune = (sbyte)(tuneArg - 0x40) }); - } - break; - } - case 0xCD: - { - byte command = r.ReadByte(); - byte arg = r.ReadByte(); - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new LibraryCommand { Command = command, Argument = arg }); - } - break; - } - case 0xCE: - { - int key = r.PeekByte() <= 0x7F ? (prevKey = r.ReadByte()) : -1; - if (!EventExists(trackIndex, offset)) - { - AddEvent(trackIndex, offset, new EndOfTieCommand { Note = key }); - } - break; - } - default: throw new MP2KInvalidCMDException(trackIndex, (int)offset, cmd); - } - } + 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 { LibraryCommandType = (LibraryCommandTypes)cmd, Argument = arg }); + } + break; + } + case 0xCE: + { + prevKey = cmd; + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new EndOfTieCommand { Note = cmd }); + } + break; + } + default: throw new MP2KInvalidRunningStatusCMDException(trackIndex, (int)offset, runCmd); + } + } + else if (cmd is > 0xB0 and < 0xCF) + { + switch (cmd) + { + case 0xB1: + case 0xB6: + { + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new FinishCommand { Prev = cmd == 0xB6 }); + } + cont = false; + break; + } + case 0xB2: + { + int jumpOffset = r.ReadInt32() - GBAUtils.CARTRIDGE_OFFSET; + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, new JumpCommand { Offset = jumpOffset }); + if (!EventExists(trackIndex, jumpOffset)) + { + AddEvents(trackIndex, jumpOffset, ref runCmd, ref prevKey, ref prevVelocity, ref callStackDepth); + } + } + 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 = r.ReadByte(); + int repeatOffset = r.ReadInt32() - GBAUtils.CARTRIDGE_OFFSET; + if (!EventExists(trackIndex, offset)) + { + AddEvent(trackIndex, offset, 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 = (MemoryOperatorType)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 { LibraryCommandType = (LibraryCommandTypes)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 - } - } - } + #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)); + public void CalculateTicks(int trackIndex) + { + List track = Events[trackIndex]; - MP2KTrack track = Tracks[trackIndex]; - track.Init(); + int length = 0, endOfPattern = 0; + for (int i = 0; i < track.Count; i++) + { + SongEvent e = track[i]; + if (endOfPattern == 0) + { + e.Ticks.Add(length); + } + if (e.Command is RestCommand rest) + { + length += rest.Rest; + } + else if (e.Command is CallCommand call) + { + int jumpCmd = track.FindIndex(c => c.Offset == call.Offset); + endOfPattern = i; + i = jumpCmd - 1; + } + else if (e.Command is ReturnCommand && endOfPattern != 0) + { + i = endOfPattern; + endOfPattern = 0; + } + } + } + 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)); - _player.ElapsedTicks = 0; - while (true) - { - SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); - if (track.CallStackDepth == 0 && e.Ticks.Count > 0) - { - break; - } + MP2KTrack track = Tracks[trackIndex]; + track.Init(); - e.Ticks.Add(_player.ElapsedTicks); - ExecuteNext(track, ref u); - if (track.Stopped) - { - break; - } + _player.ElapsedTicks = 0; + while (true) + { + SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); + if (track.CallStackDepth == 0 && e.Ticks.Count > 0) + { + 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(); - } - } + 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 old mode 100644 new mode 100755 index 1d6c4f0f..f0fd2357 --- a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_MIDI.cs @@ -156,13 +156,13 @@ public void SaveAsMIDI(string fileName, MIDISaveArgs args) } case LibraryCommand c: { - track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)30, c.Command)); + track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)30, (byte)c.LibraryCommandType)); track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)29, c.Argument)); break; } case MemoryAccessCommand c: { - track.InsertMessage(ticks, new ControllerMessage(trackIndex, ControllerType.EffectControl2, c.Operator)); + track.InsertMessage(ticks, new ControllerMessage(trackIndex, ControllerType.EffectControl2, (byte)c.Operator)); track.InsertMessage(ticks, new ControllerMessage(trackIndex, (ControllerType)14, c.Address)); track.InsertMessage(ticks, new ControllerMessage(trackIndex, ControllerType.EffectControl1, c.Data)); break; diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Runtime.cs b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Runtime.cs old mode 100644 new mode 100755 index b3c038c5..b1dc65fd --- a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Runtime.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Runtime.cs @@ -27,7 +27,7 @@ private void TryPlayNote(MP2KTrack track, byte note, byte velocity, byte addedDu private void PlayNote(byte[] rom, MP2KTrack track, byte note, byte velocity, byte addedDuration) { bool fromDrum = false; - int offset = _voiceTableOffset + (track.Voice * 12); + int offset = _soundBankOffset + (track.Voice * 12); while (true) { var v = new VoiceEntry(rom.AsSpan(offset)); @@ -50,6 +50,8 @@ private void PlayNote(byte[] rom, MP2KTrack track, byte note, byte velocity, byt Velocity = velocity, OriginalNote = note, Note = fromDrum ? v.RootNote : note, + PseudoEchoVolume = track.PseudoEchoVolume, + PseudoEchoLength = track.PseudoEchoLength, }; var type = (VoiceType)(v.Type & 0x7); int instPan = v.Pan; @@ -57,36 +59,36 @@ private void PlayNote(byte[] rom, MP2KTrack track, byte note, byte velocity, byt 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; - } + { + 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; - } + { + _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; - } + { + _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; - } + { + _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 } @@ -169,89 +171,89 @@ public void ExecuteNext(MP2KTrack track, ref bool update) 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; - } + { + 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; - } + { + track.Volume = cmd; + update = true; + break; + } case 0xBF: - { - track.Panpot = (sbyte)(cmd - 0x40); - update = true; - break; - } + { + track.Panpot = (sbyte)(cmd - 0x40); + update = true; + break; + } case 0xC0: - { - track.PitchBend = (sbyte)(cmd - 0x40); - update = true; - break; - } + { + track.PitchBend = (sbyte)(cmd - 0x40); + update = true; + break; + } case 0xC1: - { - track.PitchBendRange = cmd; - update = true; - break; - } + { + track.PitchBendRange = cmd; + update = true; + break; + } case 0xC2: - { - track.LFOSpeed = cmd; - track.LFOPhase = 0; - track.LFODelayCount = 0; - update = true; - break; - } + { + 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; - } + { + track.LFODelay = cmd; + track.LFOPhase = 0; + track.LFODelayCount = 0; + update = true; + break; + } case 0xC4: - { - track.LFODepth = cmd; - update = true; - break; - } + { + track.LFODepth = cmd; + update = true; + break; + } case 0xC5: - { - track.LFOType = (LFOType)cmd; - update = true; - break; - } + { + track.LFOType = (LFOType)cmd; + update = true; + break; + } case 0xC8: - { - track.Tune = (sbyte)(cmd - 0x40); - update = true; - break; - } + { + 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; + track.DataOffset++; + break; } - else if (k > 0x7F) + case 0xCE: { - k = 0x7F; + track.PrevNote = cmd; + int k = cmd + track.Transpose; + if (k < 0) + { + k = 0; + } + else if (k > 0x7F) + { + k = 0x7F; + } + track.ReleaseChannels(k); + break; } - track.ReleaseChannels(k); - break; - } default: throw new MP2KInvalidRunningStatusCMDException(track.Index, track.DataOffset - 1, track.RunCmd); } } @@ -261,162 +263,198 @@ public void ExecuteNext(MP2KTrack track, ref bool update) { case 0xB1: case 0xB6: - { - track.Stopped = true; - //track.ReleaseAllTieingChannels(); // Necessary? - break; - } + { + 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); + 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; - } + 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]; + if (track.CallStackDepth != 0) + { + track.CallStackDepth--; + track.DataOffset = track.CallStack[track.CallStackDepth]; + } + break; } - 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)) + case 0xB5: // TODO: Logic so this isn't an infinite loop { - AddEvent(new RepeatCommand { Times = times, Offset = repeatOffset }); + if (track.RepeatActivated is false) + { + if (track.CallStackDepth >= 3) + { + throw new MP2KTooManyNestedCallsException(track.Index); + } + + track.RepeatTimes = rom[track.DataOffset++]; + track.RepeatOffset = (rom[track.DataOffset++] | (rom[track.DataOffset++] << 8) | (rom[track.DataOffset++] << 16) | (rom[track.DataOffset++] << 24)) - GBAUtils.CARTRIDGE_OFFSET; + track.RepeatActivated = true; + track.CallStack[track.CallStackDepth] = track.DataOffset; + track.CallStackDepth++; + track.DataOffset = track.RepeatOffset; + } + if (track.RepeatTimes > 0) + { + track.RepeatTimes--; + track.DataOffset = track.RepeatOffset; + break; + } + else + { + track.RepeatTimes = 0; + track.RepeatOffset = 0; + track.RepeatActivated = false; + track.CallStackDepth--; + track.DataOffset = track.CallStack[track.CallStackDepth]; + } + break; } - break; - }*/ case 0xB9: - { - track.DataOffset += 3; - break; - } + { + track.MemSet = rom[track.DataOffset++]; + track.MemAddress = rom[track.DataOffset++]; + track.MemData = rom[track.DataOffset++]; + break; + } case 0xBA: - { - track.Priority = rom[track.DataOffset++]; - break; - } + { + track.Priority = rom[track.DataOffset++]; + break; + } case 0xBB: - { - _player.Tempo = (ushort)(rom[track.DataOffset++] * 2); - break; - } + { + _player.Tempo = (ushort)(rom[track.DataOffset++] * 2); + break; + } case 0xBC: - { - track.Transpose = (sbyte)rom[track.DataOffset++]; - break; - } + { + track.Transpose = (sbyte)rom[track.DataOffset++]; + break; + } // Commands that work within running status: case 0xBD: - { - track.Voice = rom[track.DataOffset++]; - track.Ready = true; - break; - } + { + track.Voice = rom[track.DataOffset++]; + track.Ready = true; + break; + } case 0xBE: - { - track.Volume = rom[track.DataOffset++]; - update = true; - break; - } + { + track.Volume = rom[track.DataOffset++]; + update = true; + break; + } case 0xBF: - { - track.Panpot = (sbyte)(rom[track.DataOffset++] - 0x40); - update = true; - break; - } + { + track.Panpot = (sbyte)(rom[track.DataOffset++] - 0x40); + update = true; + break; + } case 0xC0: - { - track.PitchBend = (sbyte)(rom[track.DataOffset++] - 0x40); - update = true; - break; - } + { + track.PitchBend = (sbyte)(rom[track.DataOffset++] - 0x40); + update = true; + break; + } case 0xC1: - { - track.PitchBendRange = rom[track.DataOffset++]; - update = true; - break; - } + { + track.PitchBendRange = rom[track.DataOffset++]; + update = true; + break; + } case 0xC2: - { - track.LFOSpeed = rom[track.DataOffset++]; - track.LFOPhase = 0; - track.LFODelayCount = 0; - update = true; - break; - } + { + 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; - } + { + track.LFODelay = rom[track.DataOffset++]; + track.LFOPhase = 0; + track.LFODelayCount = 0; + update = true; + break; + } case 0xC4: - { - track.LFODepth = rom[track.DataOffset++]; - update = true; - break; - } + { + track.LFODepth = rom[track.DataOffset++]; + update = true; + break; + } case 0xC5: - { - track.LFOType = (LFOType)rom[track.DataOffset++]; - update = true; - break; - } + { + track.LFOType = (LFOType)rom[track.DataOffset++]; + update = true; + break; + } case 0xC8: - { - track.Tune = (sbyte)(rom[track.DataOffset++] - 0x40); - update = true; - break; - } + { + 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); + switch (rom[track.DataOffset++]) + { + case 0x08: + { + track.PseudoEchoVolume = rom[track.DataOffset++]; + break; + } + case 0x09: + { + track.PseudoEchoLength = rom[track.DataOffset++]; + break; + } + } + break; } - else + case 0xCE: { - track.DataOffset++; - track.PrevNote = peek; - int k = peek + track.Transpose; - if (k < 0) + byte peek = rom[track.DataOffset]; + if (peek > 0x7F) { - k = 0; + track.ReleaseChannels(track.PrevNote); } - else if (k > 0x7F) + else { - k = 0x7F; + track.DataOffset++; + track.PrevNote = peek; + int k = peek + track.Transpose; + if (k < 0) + { + k = 0; + } + else if (k > 0x7F) + { + k = 0x7F; + } + track.ReleaseChannels(k); } - track.ReleaseChannels(k); + break; } - break; - } default: throw new MP2KInvalidCMDException(track.Index, track.DataOffset - 1, cmd); } } @@ -424,7 +462,7 @@ public void ExecuteNext(MP2KTrack track, ref bool update) public void UpdateInstrumentCache(byte voice, out string str) { - byte t = _player.Config.ROM[_voiceTableOffset + (voice * 12)]; + byte t = _player.Config.ROM[_soundBankOffset + (voice * 12)]; if (t == (byte)VoiceFlags.KeySplit) { str = "Key Split"; diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs b/VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs old mode 100644 new mode 100755 index 8997f706..5dfb75ea --- a/VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs @@ -1,265 +1,430 @@ -using Kermalis.VGMusicStudio.Core.Util; -using NAudio.Wave; -using System; +using System; +using System.IO; using System.Linq; +using Kermalis.VGMusicStudio.Core.Formats; +using Kermalis.VGMusicStudio.Core.Util; +using NAudio.Wave; +using SoundFlow.Enums; +using SoundFlow.Structs; 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 int SampleRate; + internal override int SamplesPerBuffer { get; } + internal readonly float SampleRateReciprocal; + private readonly float _samplesReciprocal; + internal MP2KReverb[] Reverbs; + internal readonly float PCM8MasterVolume; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; + + internal readonly MP2KConfig Config; + private readonly AudioBackend MP2KPlaybackBackend; + + #region PortAudio Fields + // PortAudio Fields + private readonly Audio? _audioPortAudio; + private readonly Wave? _bufferPortAudio; + #endregion + + #region MiniAudio Fields + // MiniAudio Fields + private readonly float[]? _bufferMiniAudio; + private readonly AudioFormat _formatSoundFlow; + protected override AudioFormat SoundFlowFormat => _formatSoundFlow; + #endregion + + #region NAudio Fields + // NAudio Fields + private readonly WaveBuffer? _audioNAudio; + private readonly BufferedWaveProvider? _bufferNAudio; + + protected override WaveFormat WaveFormat => _bufferNAudio!.WaveFormat; + #endregion + + private readonly float[][] _trackBuffers; + private readonly MP2KPCM8Channel[] _pcm8Channels; + private readonly MP2KSquareChannel _sq1; + private readonly MP2KSquareChannel _sq2; + private readonly MP2KPCM4Channel _pcm4; + private readonly MP2KNoiseChannel _noise; + private readonly MP2KPSGChannel[] _psgChannels; + + internal MP2KMixer(MP2KConfig config) + { + Config = config; + (SampleRate, SamplesPerBuffer) = MP2KUtils.FrequencyTable[config.SampleRate]; + SampleRateReciprocal = 1f / SampleRate; + _samplesReciprocal = 1f / SamplesPerBuffer; + PCM8MasterVolume = config.Volume / 15f; - internal readonly MP2KConfig Config; - private readonly WaveBuffer _audio; - private readonly float[][] _trackBuffers; - private readonly MP2KPCM8Channel[] _pcm8Channels; - private readonly MP2KSquareChannel _sq1; - private readonly MP2KSquareChannel _sq2; - private readonly MP2KPCM4Channel _pcm4; - private readonly MP2KNoiseChannel _noise; - private readonly MP2KPSGChannel[] _psgChannels; - private readonly BufferedWaveProvider _buffer; + _pcm8Channels = new MP2KPCM8Channel[24]; + for (int i = 0; i < _pcm8Channels.Length; i++) + { + _pcm8Channels[i] = new MP2KPCM8Channel(this); + } + _psgChannels = [_sq1 = new MP2KSquareChannel(this), _sq2 = new MP2KSquareChannel(this), _pcm4 = new MP2KPCM4Channel(this), _noise = new MP2KNoiseChannel(this)]; - protected override WaveFormat WaveFormat => _buffer.WaveFormat; + int amt = SamplesPerBuffer * 2; + _trackBuffers = new float[0x10][]; + Reverbs = new MP2KReverb[0x10]; + for (int i = 0; i < _trackBuffers.Length; i++) + { + _trackBuffers[i] = new float[amt]; + } + MP2KPlaybackBackend = PlaybackBackend; + switch (PlaybackBackend) + { + case AudioBackend.PortAudio: + { + _audioPortAudio = new Audio(amt * sizeof(float)) { Float32BufferCount = amt }; + _bufferPortAudio = new Wave() + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64, + }; + _bufferPortAudio.CreateIeeeFloatWave((uint)SampleRate, 2); - internal MP2KMixer(MP2KConfig config) - { - Config = config; - (SampleRate, SamplesPerBuffer) = MP2KUtils.FrequencyTable[config.SampleRate]; - SampleRateReciprocal = 1f / SampleRate; - _samplesReciprocal = 1f / SamplesPerBuffer; - PCM8MasterVolume = config.Volume / 15f; + Init(waveData: _bufferPortAudio); + break; + } + case AudioBackend.MiniAudio: + { + _bufferMiniAudio = new float[amt]; + _formatSoundFlow = new AudioFormat + { + Channels = 2, + SampleRate = SampleRate, + Format = SampleFormat.F32 + }; + Init(); + break; + } + case AudioBackend.NAudio: + { + _audioNAudio = new WaveBuffer(amt * sizeof(float)) { FloatBufferCount = amt }; + _bufferNAudio = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(SampleRate, 2)) + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64, + }; + Init(waveProvider: _bufferNAudio); + break; + } + } + } - _pcm8Channels = new MP2KPCM8Channel[24]; - for (int i = 0; i < _pcm8Channels.Length; i++) - { - _pcm8Channels[i] = new MP2KPCM8Channel(this); - } - _psgChannels = new MP2KPSGChannel[4] { _sq1 = new MP2KSquareChannel(this), _sq2 = new MP2KSquareChannel(this), _pcm4 = new MP2KPCM4Channel(this), _noise = new MP2KNoiseChannel(this), }; + internal MP2KPCM8Channel? AllocPCM8Channel(MP2KTrack owner, ADSR env, NoteInfo note, byte vol, sbyte pan, int instPan, int pitch, bool bFixed, bool bCompressed, int sampleOffset) + { + MP2KPCM8Channel? nChn = null; + IOrderedEnumerable byOwner = _pcm8Channels.OrderByDescending(c => c.Owner is null ? 0xFF : c.Owner.Index); + foreach (MP2KPCM8Channel i in byOwner) // Find free + { + if (i.State == EnvelopeState.Dead || i.Owner is null) + { + nChn = i; + break; + } + } + if (nChn is null) // Find releasing + { + foreach (MP2KPCM8Channel i in byOwner) + { + if (i.State == EnvelopeState.Releasing) + { + nChn = i; + break; + } + } + } + if (nChn is null) // Find prioritized + { + foreach (MP2KPCM8Channel i in byOwner) + { + if (owner.Priority > i.Owner!.Priority) + { + nChn = i; + break; + } + } + } + if (nChn is null) // None available + { + MP2KPCM8Channel lowest = byOwner.First(); // Kill lowest track's instrument if the track is lower than this one + if (lowest.Owner!.Index >= owner.Index) + { + nChn = lowest; + } + } + if (nChn is not null) // Could still be null from the above if + { + nChn.Init(owner, note, env, sampleOffset, vol, pan, instPan, pitch, bFixed, bCompressed); + } + return nChn; + } + internal MP2KPSGChannel? AllocPSGChannel(MP2KTrack owner, ADSR env, NoteInfo note, byte vol, sbyte pan, int instPan, int pitch, VoiceType type, object arg) + { + MP2KPSGChannel nChn; + switch (type) + { + case VoiceType.Square1: + { + nChn = _sq1; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) + { + return null; + } + _sq1.Init(owner, note, env, instPan, (SquarePattern)arg); + break; + } + case VoiceType.Square2: + { + nChn = _sq2; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) + { + return null; + } + _sq2.Init(owner, note, env, instPan, (SquarePattern)arg); + break; + } + case VoiceType.PCM4: + { + nChn = _pcm4; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) + { + return null; + } + _pcm4.Init(owner, note, env, instPan, (int)arg); + break; + } + case VoiceType.Noise: + { + nChn = _noise; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) + { + return null; + } + _noise.Init(owner, note, env, instPan, (NoisePattern)arg); + break; + } + default: return null; + } + nChn.SetVolume(vol, pan); + nChn.SetPitch(pitch); + return nChn; + } - 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 void SetReverb(MP2KTrack track) + { + byte reverb = (byte)(Config.Reverb >= 0x80 ? Config.Reverb & 0x7F : 0 & 0x7F); + if (track.Reverb >> 7 is 1) + { + reverb = (byte)(track.Reverb >> 1); + } + float engineFrequency = SampleRate / SamplesPerBuffer; + for (int i = 0; i < Reverbs.Length; i++) + { + byte numBuffers = (byte)(0x630 / (engineFrequency / GBAUtils.AGB_FPS)); + switch (Config.ReverbType) + { + default: Reverbs[i] = new MP2KReverb(this, reverb, numBuffers); break; + case ReverbType.Camelot1: Reverbs[i] = new MP2KReverbCamelot1(this, reverb, numBuffers); break; + case ReverbType.Camelot2: Reverbs[i] = new MP2KReverbCamelot2(this, reverb, numBuffers, 53 / 128f, -8 / 128f); break; + case ReverbType.MGAT: Reverbs[i] = new MP2KReverbCamelot2(this, reverb, numBuffers, 32 / 128f, -6 / 128f); break; + case ReverbType.None: Reverbs[i] = null!; break; + } + } + } - internal MP2KPCM8Channel? AllocPCM8Channel(MP2KTrack owner, ADSR env, NoteInfo note, byte vol, sbyte pan, int instPan, int pitch, bool bFixed, bool bCompressed, int sampleOffset) - { - MP2KPCM8Channel? nChn = null; - IOrderedEnumerable byOwner = _pcm8Channels.OrderByDescending(c => c.Owner is null ? 0xFF : c.Owner.Index); - foreach (MP2KPCM8Channel i in byOwner) // Find free - { - if (i.State == EnvelopeState.Dead || i.Owner is null) - { - nChn = i; - break; - } - } - if (nChn is null) // Find releasing - { - foreach (MP2KPCM8Channel i in byOwner) - { - if (i.State == EnvelopeState.Releasing) - { - nChn = i; - break; - } - } - } - if (nChn is null) // Find prioritized - { - foreach (MP2KPCM8Channel i in byOwner) - { - if (owner.Priority > i.Owner!.Priority) - { - nChn = i; - break; - } - } - } - if (nChn is null) // None available - { - MP2KPCM8Channel lowest = byOwner.First(); // Kill lowest track's instrument if the track is lower than this one - if (lowest.Owner!.Index >= owner.Index) - { - nChn = lowest; - } - } - if (nChn is not null) // Could still be null from the above if - { - nChn.Init(owner, note, env, sampleOffset, vol, pan, instPan, pitch, bFixed, bCompressed); - } - return nChn; - } - internal MP2KPSGChannel? AllocPSGChannel(MP2KTrack owner, ADSR env, NoteInfo note, byte vol, sbyte pan, int instPan, int pitch, VoiceType type, object arg) - { - MP2KPSGChannel nChn; - switch (type) - { - case VoiceType.Square1: - { - nChn = _sq1; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) - { - return null; - } - _sq1.Init(owner, note, env, instPan, (SquarePattern)arg); - break; - } - case VoiceType.Square2: - { - nChn = _sq2; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) - { - return null; - } - _sq2.Init(owner, note, env, instPan, (SquarePattern)arg); - break; - } - case VoiceType.PCM4: - { - nChn = _pcm4; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) - { - return null; - } - _pcm4.Init(owner, note, env, instPan, (int)arg); - break; - } - case VoiceType.Noise: - { - nChn = _noise; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) - { - return null; - } - _noise.Init(owner, note, env, instPan, (NoisePattern)arg); - break; - } - default: return null; - } - nChn.SetVolume(vol, pan); - nChn.SetPitch(pitch); - return nChn; - } + internal void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * GBAUtils.AGB_FPS); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * GBAUtils.AGB_FPS); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal bool IsFading() + { + return _isFading; + } + internal bool IsFadeDone() + { + return _isFading && _fadeMicroFramesLeft == 0; + } + internal void ResetFade() + { + _isFading = false; + _fadeMicroFramesLeft = 0; + } - internal void BeginFadeIn() - { - _fadePos = 0f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * GBAUtils.AGB_FPS); - _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; - _isFading = true; - } - internal void BeginFadeOut() - { - _fadePos = 1f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * GBAUtils.AGB_FPS); - _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; - _isFading = true; - } - internal bool IsFading() - { - return _isFading; - } - internal bool IsFadeDone() - { - return _isFading && _fadeMicroFramesLeft == 0; - } - internal void ResetFade() - { - _isFading = false; - _fadeMicroFramesLeft = 0; - } + internal void Process(bool output, bool recording) + { + for (int i = 0; i < _trackBuffers.Length; i++) + { + Span buf = _trackBuffers[i]; + buf.Clear(); + } + switch (MP2KPlaybackBackend) + { + case AudioBackend.PortAudio: + { + _audioPortAudio!.Clear(); + break; + } + case AudioBackend.MiniAudio: + { + Array.Clear(_bufferMiniAudio!); + break; + } + case AudioBackend.NAudio: + { + _audioNAudio!.Clear(); + break; + } + } - internal void Process(bool output, bool recording) - { - for (int i = 0; i < _trackBuffers.Length; i++) - { - float[] buf = _trackBuffers[i]; - Array.Clear(buf, 0, buf.Length); - } - _audio.Clear(); + for (int i = 0; i < _pcm8Channels.Length; i++) + { + MP2KPCM8Channel c = _pcm8Channels[i]; + if (c.Owner is not null) + { + var index = c.Owner.Index; + c.Process(_trackBuffers[index]); + Reverbs[index].Process(_trackBuffers[index], SamplesPerBuffer); + //if (c.Owner is not null) + //{ + // Reverbs[i].Process(_trackBuffers[c.Owner.Index], SamplesPerBuffer); + //} + } + } - for (int i = 0; i < _pcm8Channels.Length; i++) - { - MP2KPCM8Channel c = _pcm8Channels[i]; - if (c.Owner is not null) - { - c.Process(_trackBuffers[c.Owner.Index]); - } - } + for (int i = 0; i < _psgChannels.Length; i++) + { + MP2KPSGChannel c = _psgChannels[i]; + if (c.Owner is not null) + { + c.Process(_trackBuffers[c.Owner.Index]); + //if (c.Owner is not null) + //{ + // Reverbs[i].Process(_trackBuffers[c.Owner.Index], SamplesPerBuffer); + //} + } + } - for (int i = 0; i < _psgChannels.Length; i++) - { - MP2KPSGChannel c = _psgChannels[i]; - if (c.Owner is not null) - { - c.Process(_trackBuffers[c.Owner.Index]); - } - } + //for (int i = 0; i < _trackBuffers.Length; i++) + //{ + // Reverbs[i].Process(_trackBuffers[i], SamplesPerBuffer); + //} - 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 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); - } - } + float level = masterLevel; + Span buf = _trackBuffers[i]; + for (int j = 0; j < SamplesPerBuffer; j++) + { + switch (MP2KPlaybackBackend) + { + case AudioBackend.PortAudio: + { + _audioPortAudio!.Float32Buffer![j * 2] += buf[j * 2] * level; + _audioPortAudio.Float32Buffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; + break; + } + case AudioBackend.MiniAudio: + { + _bufferMiniAudio![j * 2] += buf[j * 2] * level; + _bufferMiniAudio[(j * 2) + 1] += buf[(j * 2) + 1] * level; + break; + } + case AudioBackend.NAudio: + { + _audioNAudio!.FloatBuffer![j * 2] += buf[j * 2] * level; + _audioNAudio.FloatBuffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; + break; + } + } + level += masterStep; + } + } + if (output) + { + switch (MP2KPlaybackBackend) + { + case AudioBackend.PortAudio: + { + _bufferPortAudio!.AddSamples(_audioPortAudio!.ByteBuffer, 0, _audioPortAudio.ByteBufferCount); + break; + } + case AudioBackend.MiniAudio: + { + DataProvider!.AddSamples(_bufferMiniAudio); // Thank you LSXPrime for pointing out that it just needs AddSamples and nothing else in here + break; + } + case AudioBackend.NAudio: + { + _bufferNAudio!.AddSamples(_audioNAudio!.ByteBuffer, 0, _audioNAudio.ByteBufferCount); + break; + } + } + } + if (recording) + { + switch (MP2KPlaybackBackend) + { + case AudioBackend.PortAudio: + { + _waveWriterPortAudio!.Write(_audioPortAudio!.ByteBuffer, 0, _audioPortAudio.ByteBufferCount); + break; + } + case AudioBackend.MiniAudio: + { + _soundFlowEncoder!.Encode(_bufferMiniAudio); // Again, thank you LSXPrime for showing how to encode + break; + } + case AudioBackend.NAudio: + { + _waveWriterNAudio!.Write(_audioNAudio!.ByteBuffer, 0, _audioNAudio.ByteBufferCount); + break; + } + } + } + } } diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs b/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs old mode 100644 new mode 100755 index 7ebe510e..5dbff82c --- a/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs @@ -1,155 +1,200 @@ -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; public sealed partial class MP2KPlayer : Player { - protected override string Name => "MP2K Player"; - - private readonly string?[] _voiceTypeCache; - internal readonly MP2KConfig Config; - internal readonly MP2KMixer MMixer; - private MP2KLoadedSong? _loadedSong; - - internal ushort Tempo; - internal int TempoStack; - private long _elapsedLoops; - - private int? _prevVoiceTableOffset; - - public override ILoadedSong? LoadedSong => _loadedSong; - protected override Mixer Mixer => MMixer; - - internal MP2KPlayer(MP2KConfig config, MP2KMixer mixer) - : base(GBAUtils.AGB_FPS) - { - Config = config; - MMixer = mixer; - - _voiceTypeCache = new string[256]; - } - - public override void LoadSong(int index) - { - if (_loadedSong is not null) - { - _loadedSong = null; - } - - // If there's an exception, this will remain null - _loadedSong = new MP2KLoadedSong(this, index); - if (_loadedSong.Events.Length == 0) - { - _loadedSong = null; - return; - } - - _loadedSong.CheckVoiceTypeCache(ref _prevVoiceTableOffset, _voiceTypeCache); - _loadedSong.SetTicks(); - } - public override void UpdateSongState(SongState info) - { - info.Tempo = Tempo; - _loadedSong!.UpdateSongState(info, _voiceTypeCache); - } - internal override void InitEmulation() - { - Tempo = 150; - TempoStack = 0; - _elapsedLoops = 0; - ElapsedTicks = 0; - MMixer.ResetFade(); - MP2KTrack[] tracks = _loadedSong!.Tracks; - for (int i = 0; i < tracks.Length; i++) - { - tracks[i].Init(); - } - } - protected override void SetCurTick(long ticks) - { - _loadedSong!.SetCurTick(ticks); - } - protected override void OnStopped() - { - MP2KTrack[] tracks = _loadedSong!.Tracks; - for (int i = 0; i < tracks.Length; i++) - { - tracks[i].StopAllChannels(); - } - } - - protected override bool Tick(bool playing, bool recording) - { - MP2KLoadedSong s = _loadedSong!; - - bool allDone = false; - while (!allDone && TempoStack >= 150) - { - TempoStack -= 150; - allDone = true; - for (int i = 0; i < s.Tracks.Length; i++) - { - TickTrack(s, s.Tracks[i], ref allDone); - } - if (MMixer.IsFadeDone()) - { - allDone = true; - } - } - if (!allDone) - { - TempoStack += Tempo; - } - MMixer.Process(playing, recording); - return allDone; - } - private void TickTrack(MP2KLoadedSong s, MP2KTrack track, ref bool allDone) - { - track.Tick(); - bool update = false; - while (track.Rest == 0 && !track.Stopped) - { - s.ExecuteNext(track, ref update); - } - if (track.Index == s.LongestTrack) - { - HandleTicksAndLoop(s, track); - } - if (!track.Stopped) - { - allDone = false; - } - if (track.Channels.Count > 0) - { - allDone = false; - if (update || track.LFODepth > 0) - { - track.UpdateChannels(); - } - } - } - private void HandleTicksAndLoop(MP2KLoadedSong s, MP2KTrack track) - { - if (ElapsedTicks != s.MaxTicks) - { - ElapsedTicks++; - return; - } - - // Track reached the detected end, update loops/ticks accordingly - if (track.Stopped) - { - return; - } - - _elapsedLoops++; - UpdateElapsedTicksAfterLoop(s.Events[track.Index], track.DataOffset, track.Rest); - if (ShouldFadeOut && _elapsedLoops > NumLoops && !MMixer.IsFading()) - { - MMixer.BeginFadeOut(); - } - } - - public void SaveAsMIDI(string fileName, MIDISaveArgs args) - { - _loadedSong!.SaveAsMIDI(fileName, args); - } + protected override string Name => "MP2K Player"; + + private readonly string?[] _voiceTypeCache; + internal readonly MP2KConfig Config; + internal readonly MP2KMixer MMixer; + private MP2KLoadedSong? _loadedSong; + + public override ushort Tempo { get; set; } + internal int TempoStack; + private long _elapsedLoops; + + private int? _prevVoiceTableOffset; + private int _longestTrack; + + public override ILoadedSong? LoadedSong => _loadedSong; + protected override Mixer Mixer => MMixer; + + internal MP2KPlayer(MP2KConfig config, MP2KMixer mixer) + : base(GBAUtils.AGB_FPS) + { + Config = config; + MMixer = mixer; + + _voiceTypeCache = new string[256]; + } + + public override void LoadSong(int index) + { + if (_loadedSong is not null) + { + _loadedSong = null; + } + + // If there's an exception, this will remain null + _loadedSong = new MP2KLoadedSong(this, index); + if (_loadedSong.Events.Length == 0) + { + _loadedSong = null; + return; + } + + _loadedSong.CheckVoiceTypeCache(ref _prevVoiceTableOffset, _voiceTypeCache); + _loadedSong.SetTicks(); + } + public override void LoadSong(LoadedSong song) + { + // If there's an exception, this will remain null + _loadedSong = (MP2KLoadedSong)song; + if (_loadedSong.Events.Length == 0) + { + _loadedSong = null; + return; + } + + _loadedSong.CheckVoiceTypeCache(ref _prevVoiceTableOffset, _voiceTypeCache); + _loadedSong.SetTicks(); + } + public override void RefreshSong() + { + if (State == PlayerState.Stopped || State == PlayerState.ShutDown) + { + return; + } + DetermineLongestTrack(); + SetSongPosition(ElapsedTicks); + } + private void DetermineLongestTrack() + { + for (int i = 0; i < _loadedSong!.Tracks.Length; i++) + { + if (LoadedSong!.Events[i]!.Last().Ticks[0] == _loadedSong.MaxTicks - 1) + { + _longestTrack = i; + break; + } + } + } + public override void UpdateSongState(SongState info) + { + info.Tempo = Tempo; + _loadedSong!.UpdateSongState(info, _voiceTypeCache); + } + internal override void InitEmulation() + { + Tempo = 150; + TempoStack = 0; + _elapsedLoops = 0; + ElapsedTicks = 0; + MMixer.ResetFade(); + MP2KTrack[] tracks = _loadedSong!.Tracks; + for (int i = 0; i < tracks.Length; i++) + { + tracks[i].Init(); + } + } + protected override void SetCurTick(long ticks) + { + _loadedSong!.SetCurTick(ticks); + } + protected override void OnStopped() + { + MP2KTrack[] tracks = _loadedSong!.Tracks; + for (int i = 0; i < tracks.Length; i++) + { + tracks[i].StopAllChannels(); + } + } + + protected override bool Tick(bool playing, bool recording) + { + MP2KLoadedSong s = _loadedSong!; + + bool allDone = false; + while (!allDone && TempoStack >= 150) + { + TempoStack -= 150; + allDone = true; + for (int i = 0; i < s.Tracks.Length; i++) + { + TickTrack(s, s.Tracks[i], ref allDone); + } + if (MMixer.IsFadeDone()) + { + allDone = true; + } + } + if (!allDone) + { + TempoStack += Tempo; + } + for (int i = 0; i < s.Tracks.Length; i++) + { + MMixer.SetReverb(s.Tracks[i]); + } + MMixer.Process(playing, recording); + return allDone; + } + private void TickTrack(MP2KLoadedSong s, MP2KTrack track, ref bool allDone) + { + track.Tick(); + bool update = false; + while (track.Rest == 0 && !track.Stopped) + { + s.ExecuteNext(track, ref update); + } + if (track.Index == s.LongestTrack) + { + HandleTicksAndLoop(s, track); + } + if (!track.Stopped) + { + allDone = false; + } + if (track.Channels.Count > 0) + { + allDone = false; + if (update || track.LFODepth > 0) + { + track.UpdateChannels(); + } + } + } + private void HandleTicksAndLoop(MP2KLoadedSong s, MP2KTrack track) + { + if (ElapsedTicks != s.MaxTicks) + { + ElapsedTicks++; + return; + } + + // Track reached the detected end, update loops/ticks accordingly + if (track.Stopped) + { + return; + } + + _elapsedLoops++; + UpdateElapsedTicksAfterLoop(s.Events[track.Index], track.DataOffset, track.Rest); + if (ShouldFadeOut && _elapsedLoops > NumLoops && !MMixer.IsFading()) + { + MMixer.BeginFadeOut(); + } + } + + public void SaveAsMIDI(string fileName, MIDISaveArgs args) + { + _loadedSong!.SaveAsMIDI(fileName, args); + } + + public void SaveAsASM(string fileName, ASMSaveArgs args) + { + _loadedSong!.SaveAsASM(fileName, args); + } } diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KReverb.cs b/VG Music Studio - Core/GBA/MP2K/MP2KReverb.cs new file mode 100644 index 00000000..b40494ea --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KReverb.cs @@ -0,0 +1,183 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +// All of this was written in C++ by ipatix; I just converted it +internal class MP2KReverb +{ + protected readonly float[] _reverbBuffer; + protected readonly float _intensity; + protected readonly byte _numBuffers; + protected readonly int _bufferLen; + protected int _bufferPos1, _bufferPos2; + + public MP2KReverb(MP2KMixer mixer, byte intensity, byte numBuffers) + { + _bufferLen = mixer.SamplesPerBuffer; + _bufferPos2 = _bufferLen; + _intensity = intensity / (float)0x80; + _numBuffers = numBuffers; + _reverbBuffer = new float[_bufferLen * 2 * numBuffers]; + } + + public void Process(Span buffer, int samplesPerBuffer) + { + int index = 0; + while (samplesPerBuffer > 0) + { + int left = Process(buffer, samplesPerBuffer, ref index); + index += (samplesPerBuffer - left) * 2; + samplesPerBuffer = left; + } + } + + protected virtual void Reset() + { + Array.Fill(_reverbBuffer, 0f, 0, _reverbBuffer.Length); + } + + protected virtual int Process(Span buffer, int samplesPerBuffer, ref int index) + { + int rSamplesPerBuffer = _reverbBuffer.Length / 2; + + int count = Math.Min( + Math.Min(rSamplesPerBuffer - _bufferPos2, rSamplesPerBuffer - _bufferPos1), + samplesPerBuffer + ); + bool reset1 = rSamplesPerBuffer - _bufferPos1 == count, + reset2 = rSamplesPerBuffer - _bufferPos2 == count; + for (int i = 0; i < count; i++) + { + float rev = (_reverbBuffer[_bufferPos1 * 2] * 2 + + _reverbBuffer[_bufferPos1 * 2 + 1] * 2 + + _reverbBuffer[_bufferPos2 * 2] * 2 + + _reverbBuffer[_bufferPos2 * 2 + 1] * 2) * _intensity * (1.0f / 4.0f); + + _reverbBuffer[_bufferPos1 * 2] = buffer[index++] += rev; + _reverbBuffer[_bufferPos1 * 2 + 1] = buffer[index++] += rev; + _bufferPos1++; _bufferPos2++; + } + if (reset1) + { + _bufferPos1 = 0; + } + if (reset2) + { + _bufferPos2 = 0; + } + return samplesPerBuffer - count; + } +} + +internal class MP2KReverbCamelot1 : MP2KReverb +{ + readonly float[] _cBuffer; + public MP2KReverbCamelot1(MP2KMixer mixer, byte intensity, byte numBuffers) : base(mixer, intensity, numBuffers) + { + _bufferPos2 = 0; + _cBuffer = new float[_bufferLen * 2]; + } + + protected override int Process(Span buffer, int samplesPerBuffer, ref int index) + { + int rSamplesPerBuffer = _reverbBuffer.Length / 2; + int cSamplesPerBuffer = _cBuffer.Length / 2; + int count = Math.Min( + Math.Min(rSamplesPerBuffer - _bufferPos1, cSamplesPerBuffer - _bufferPos2), + samplesPerBuffer + ); + bool reset1 = count == rSamplesPerBuffer - _bufferPos1, + resetC = count == cSamplesPerBuffer - _bufferPos2; + for (int i = 0; i < count; i++) + { + float mixL = buffer[index] + _cBuffer[_bufferPos2 * 2]; + float mixR = buffer[index + 1] + _cBuffer[_bufferPos2 * 2 + 1]; + + float lA = _reverbBuffer[_bufferPos1 * 2]; + float rA = _reverbBuffer[_bufferPos1 * 2 + 1]; + + buffer[index] = _reverbBuffer[_bufferPos1 * 2] = mixL; + buffer[index + 1] = _reverbBuffer[_bufferPos1 * 2 + 1] = mixR; + + float lRMix = mixL / 4f + rA / 4f; + float rRMix = mixR / 4f + lA / 4f; + + _cBuffer[_bufferPos2 * 2] = lRMix; + _cBuffer[_bufferPos2 * 2 + 1] = rRMix; + + index += 2; + _bufferPos1++; _bufferPos2++; + } + + if (reset1) + { + _bufferPos1 = 0; + } + if (resetC) + { + _bufferPos2 = 0; + } + return samplesPerBuffer - count; + } +} + +internal class MP2KReverbCamelot2 : MP2KReverb +{ + readonly float[] _cBuffer; int _cPos; + readonly float _primary, _secondary; + internal MP2KReverbCamelot2(MP2KMixer mixer, byte intensity, byte numBuffers, float primary, float secondary) : base(mixer, intensity, numBuffers) + { + _cBuffer = new float[_bufferLen * 2]; + _bufferPos2 = _reverbBuffer.Length / 2 - (_cBuffer.Length / 2 / 3); + _primary = primary; _secondary = secondary; + } + + protected override int Process(Span buffer, int samplesPerBuffer, ref int index) + { + int rSamplesPerBuffer = _reverbBuffer.Length / 2; + int count = Math.Min( + Math.Min(rSamplesPerBuffer - _bufferPos1, rSamplesPerBuffer - _bufferPos2), + Math.Min(samplesPerBuffer, _cBuffer.Length / 2 - _cPos) + ); + bool reset = rSamplesPerBuffer - _bufferPos1 == count, + reset2 = rSamplesPerBuffer - _bufferPos2 == count, + resetC = _cBuffer.Length / 2 - _cPos == count; + + for (int i = 0; i < count; i++) + { + float mixL = buffer[index] + _cBuffer[_cPos * 2]; + float mixR = buffer[index + 1] + _cBuffer[_cPos * 2 + 1]; + + float lA = _reverbBuffer[_bufferPos1 * 2]; + float rA = _reverbBuffer[_bufferPos1 * 2 + 1]; + + buffer[index] = _reverbBuffer[_bufferPos1 * 2] = mixL; + buffer[index + 1] = _reverbBuffer[_bufferPos1 * 2 + 1] = mixR; + + float lRMix = lA * _primary + rA * _secondary; + float rRMix = rA * _primary + lA * _secondary; + + float lB = _reverbBuffer[_bufferPos2 * 2 + 1] / 4f; + float rB = mixR / 4f; + + _cBuffer[_cPos * 2] = lRMix + lB; + _cBuffer[_cPos * 2 + 1] = rRMix + rB; + + index += 2; + _bufferPos1++; _bufferPos2++; _cPos++; + } + if (reset) + { + _bufferPos1 = 0; + } + if (reset2) + { + _bufferPos2 = 0; + } + if (resetC) + { + _cPos = 0; + } + return samplesPerBuffer - count; + } +} \ No newline at end of file diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KSample.cs b/VG Music Studio - Core/GBA/MP2K/MP2KSample.cs new file mode 100755 index 00000000..f75e010a --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KSample.cs @@ -0,0 +1,22 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal class MP2KSample +{ + public SampleHeader Header; + public byte[] PCMData; + + public MP2KSample(ReadOnlySpan src, bool isPCM4 = false) + { + if (isPCM4) + { + src[..16].CopyTo(PCMData = new byte[16]); + } + else + { + Header = new SampleHeader(src); + PCMData = src.Slice(16, Header.Length - 1).ToArray(); + } + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KSoundBank.cs b/VG Music Studio - Core/GBA/MP2K/MP2KSoundBank.cs new file mode 100755 index 00000000..1165b13e --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KSoundBank.cs @@ -0,0 +1,235 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +internal class MP2KSoundBank : SoundBank +{ + public override int Offset { get; set; } + public int Length { get; private set; } + protected readonly IVoice[] Voices; + private static readonly Dictionary _cache = new Dictionary(); + public static void ClearCache() => _cache.Clear(); + + public MP2KSoundBank(int tableOffset, bool isSubVoiceGroup = false) + { + Offset = tableOffset; + if (isSubVoiceGroup) + { + Voices = new IVoice[Length = 128]; + } + else + { + List v = []; + int i = 0; + while (true) + { + int off = Offset + (i++ * 0xC); + if (!GBAUtils.IsValidRomOffset(off)) + { + break; + } + WrappedVoice voice = new(new(Engine.Instance!.Config.ROM!.AsSpan(off)), isSubVoiceGroup); + if (!voice.IsValidVoiceEntry) + { + Debug.WriteLine("Reached the end of the voicegroup table."); + break; + } + v.Add(voice); + Length = v.Count; + } + Voices = new IVoice[Length]; + Voices = [.. v]; + } + } + + public WrappedVoice this[int i] + { + get => (WrappedVoice)Voices[i]; + protected set => Voices[i] = value; + } + public override IEnumerator GetEnumerator() => ((IEnumerable)Voices).GetEnumerator(); + + public Tuple[] GetKeys(int offset) + { + Span keys = stackalloc byte[128]; + Engine.Instance!.Config.ROM!.AsSpan(offset, 128).CopyTo(keys); + var loading = new List>(); // Key, min, max + int prev = -1; + for (int i = 0; i < 128; i++) + { + byte a = keys[i]; + byte bi = (byte)i; + if (prev == a) + { + loading[loading.Count - 1] = new(loading[loading.Count - 1].Item1, loading[loading.Count - 1].Item2, bi); + } + else + { + prev = a; + loading.Add(new Tuple(a, bi, bi)); + } + } + return [.. loading]; + } + + public static T LoadTable(int tableOffset, bool shouldCache = false, bool isSubVoiceGroup = false) where T : MP2KSoundBank + { + if (_cache.ContainsKey(tableOffset)) + { + return (T)_cache[tableOffset]; + } + else + { + T vTable = (T)new MP2KSoundBank(tableOffset, isSubVoiceGroup); + if (shouldCache) + { + _cache.Add(tableOffset, vTable); + } + vTable.Load(); + return vTable; + } + } + protected override void Load() + { + Span vBytes = stackalloc byte[12]; + for (int i = 0; i < Length; i++) + { + int off = Offset + (i * 0xC); + if (!GBAUtils.IsValidRomOffset(off)) + { + break; + } + vBytes = Engine.Instance!.Config.ROM!.AsSpan(off); + VoiceEntry voice = new(vBytes); + Voices[i] = new WrappedVoice(voice) + { + Offset = off + }; + } + } + public override SoundBank LoadFromAddress(int tableOffset) + { + return LoadTable(tableOffset); + } + public override object GetSubVoiceEntry(IVoiceInfo voiceInfo) + { + return ((IVoice)voiceInfo).VoiceEntry!; + } + public override string GetBytesToString(int voiceIndex) + { + if (Voices[voiceIndex].VoiceEntry is VoiceEntry entry) + { + return entry.GetBytesToString(); + } + else + { + return Voices[voiceIndex].VoiceEntry!.Value.GetBytesToString(); + } + } + public override bool IsTableAddress(int voiceIndex) + { + if (Voices[voiceIndex].VoiceEntry!.Value.Type is (byte)VoiceFlags.KeySplit || Voices[voiceIndex].VoiceEntry!.Value.Type is (byte)VoiceFlags.Drum) + { + return true; + } + else + { + return false; + } + } + public override bool HasADSR(int voiceIndex) + { + switch (Voices[voiceIndex].VoiceEntry!.Value.Type) + { + case (byte)VoiceFlags.Drum: + { + return false; + } + case (byte)VoiceFlags.KeySplit: + { + return false; + } + default: + { + return true; + } + } + } + public override bool IsValidVoiceAddress(int voiceIndex) + { + var flags = (VoiceFlags)Voices[voiceIndex].VoiceEntry!.Value.Type; + var type = (VoiceType)(Voices[voiceIndex].VoiceEntry!.Value.Type & 0x7); + return type == VoiceType.PCM8 || type == VoiceType.PCM4 || flags == VoiceFlags.KeySplit || flags == VoiceFlags.Drum; + } + public override int GetVoiceAddress(int voiceIndex) + { + return Voices[voiceIndex].VoiceEntry!.Value.Int4 - GBAUtils.CARTRIDGE_OFFSET; + } + + public override bool IsValidADSR(int voiceIndex) + { + var flags = Voices[voiceIndex].VoiceEntry!.Value.Type; + return flags != (byte)VoiceFlags.KeySplit && flags != (byte)VoiceFlags.Drum && !Voices[voiceIndex].VoiceEntry!.Value.IsInvalid(); + } + public override bool IsPSGInstrument(int voiceIndex) + { + return Voices[voiceIndex].VoiceEntry!.Value.IsPSGInstrument(); + } + public override void GetADSRValues(int voiceIndex, out byte attackValue, out byte decayValue, out byte sustainValue, out byte releaseValue) + { + attackValue = Voices[voiceIndex].VoiceEntry!.Value.ADSR.A; + decayValue = Voices[voiceIndex].VoiceEntry!.Value.ADSR.D; + sustainValue = Voices[voiceIndex].VoiceEntry!.Value.ADSR.S; + releaseValue = Voices[voiceIndex].VoiceEntry!.Value.ADSR.R; + } + public override void SetADSRValues(int voiceIndex, in byte attackValue, in byte decayValue, in byte sustainValue, in byte releaseValue) + { + var adsr = Voices[voiceIndex].VoiceEntry!.Value.ADSR; + adsr.A = attackValue; + adsr.D = decayValue; + adsr.S = sustainValue; + adsr.R = releaseValue; + } + public override void SetAddressPointer(int voiceIndex, int address) + { + Voices[voiceIndex].VoiceEntry!.Value.SetAddressPointer(address); + // Engine.Instance!.Config.ROM.AsSpan(address + GBAUtils.CARTRIDGE_CAPACITY); + } + public WrappedVoice GetVoiceFromNote(byte voice, sbyte note, out bool fromDrum) + { + fromDrum = false; + + IVoice sv = (WrappedVoice)Voices[voice]; + Read: + VoiceEntry v = (VoiceEntry)sv.VoiceEntry!; + switch (v.Type) + { + case (int)VoiceFlags.KeySplit: + { + fromDrum = false; // In case there is a multi within a drum + var keySplit = (WrappedVoice)sv; + byte inst = Engine.Instance!.Config.ROM!.AsSpan(v.Int4 - GBAUtils.CARTRIDGE_OFFSET + note)[0]; + sv = keySplit.Table![inst]; + goto Read; + } + case (int)VoiceFlags.Drum: + { + fromDrum = true; + var drum = (WrappedVoice)sv; + sv = drum.Table![note]; + goto Read; + } + default: return (WrappedVoice)sv; + } + } + + public override IVoiceInfo ElementAt(int selectedIndex) + { + return this.ElementAt(selectedIndex); + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KStructs.cs b/VG Music Studio - Core/GBA/MP2K/MP2KStructs.cs old mode 100644 new mode 100755 index 5b33542c..b358eac4 --- a/VG Music Studio - Core/GBA/MP2K/MP2KStructs.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KStructs.cs @@ -1,4 +1,10 @@ -using System; +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; using System.Runtime.InteropServices; using static System.Buffers.Binary.BinaryPrimitives; @@ -7,181 +13,529 @@ 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))); - } + 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))); - } + public const int SIZE = 8; + + public readonly byte NumTracks; + public readonly byte NumBlocks; + public readonly byte Priority; + public readonly byte Reverb; + public readonly int SoundBankOffset; + // 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]; + SoundBankOffset = 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))); + } +} + +internal struct WrappedVoice : IVoice +{ + public VoiceEntry? VoiceEntry { get; private set; } + + public int Offset { get; set; } + public readonly string? Name { get; } + public sbyte RootNote { get; } + public byte Sweep { get; } + public bool IsValidVoiceEntry { get; } + + public readonly MP2KSoundBank? Table; + public readonly Tuple[]? Keys; + public readonly MP2KSample? Sample; + + internal WrappedVoice(VoiceEntry voice, bool isSubVoiceGroup = false) + { + switch (voice.Type) + { + case (byte)VoiceType.PCM8: + { + VoiceEntry = voice; + if (voice.Int4 - GBAUtils.CARTRIDGE_OFFSET >= 0) + { + Sample = new MP2KSample(Engine.Instance!.Config.ROM!.AsSpan(voice.Int4 - GBAUtils.CARTRIDGE_OFFSET)); + Name = voice.Name; + RootNote = (sbyte)voice.RootNote; + IsValidVoiceEntry = IsValidADSR(); + } + else + { + IsValidVoiceEntry = false; + } + break; + } + case (byte)VoiceType.Square1: + { + VoiceEntry = voice; + Name = voice.Name; + RootNote = (sbyte)voice.RootNote; + Sweep = voice.Pan; + IsValidVoiceEntry = IsValidADSR(); + break; + } + case (byte)VoiceType.Square2: + { + VoiceEntry = voice; + Name = voice.Name; + RootNote = (sbyte)voice.RootNote; + IsValidVoiceEntry = IsValidADSR(); + break; + } + case (byte)VoiceType.PCM4: + { + VoiceEntry = voice; + if (voice.Int4 - GBAUtils.CARTRIDGE_OFFSET >= 0) + { + Sample = new MP2KSample(Engine.Instance!.Config.ROM!.AsSpan(voice.Int4 - GBAUtils.CARTRIDGE_OFFSET), true); + Name = voice.Name; + RootNote = (sbyte)voice.RootNote; + IsValidVoiceEntry = IsValidADSR(); + } + else + { + IsValidVoiceEntry = false; + } + break; + } + case (byte)VoiceType.Noise: + { + VoiceEntry = voice; + Name = voice.Name; + RootNote = (sbyte)voice.RootNote; + IsValidVoiceEntry = IsValidADSR(); + break; + } + + case (byte)VoiceType.Invalid5: + case (byte)VoiceType.Invalid6: + case (byte)VoiceType.Invalid7: + default: + { + Debug.WriteLine($"Invalid instrument type detected. If this was reading from a while loop, it was likely due to reaching the end of the voicegroup table."); + IsValidVoiceEntry = false; + break; + } + + case (byte)VoiceFlags.Fixed: + { + VoiceEntry = voice; + if (voice.Int4 - GBAUtils.CARTRIDGE_OFFSET >= 0) + { + Sample = new MP2KSample(Engine.Instance!.Config.ROM!.AsSpan(voice.Int4 - GBAUtils.CARTRIDGE_OFFSET)); + Name = voice.Name; + RootNote = (sbyte)voice.RootNote; + IsValidVoiceEntry = IsValidADSR(); + } + else + { + IsValidVoiceEntry = false; + } + break; + } + case (byte)VoiceFlags.OffWithNoise: + { + VoiceEntry = voice; + Name = voice.Name; + RootNote = (sbyte)voice.RootNote; + IsValidVoiceEntry = IsValidADSR(); + break; + } + case 0xA: + { + goto case (byte)VoiceType.Square2; + } + case 0xB: + { + goto case (byte)VoiceType.PCM4; + } + case 0xC: + { + goto case (byte)VoiceType.Noise; + } + case (byte)VoiceFlags.Reversed: + case (byte)VoiceFlags.Compressed: + { + goto case (byte)VoiceType.PCM8; + } + case (byte)VoiceFlags.KeySplit: + { + VoiceEntry = voice; + RootNote = (sbyte)voice.RootNote; + if (!isSubVoiceGroup) + { + try + { + Table = MP2KSoundBank.LoadTable(voice.Int4 - GBAUtils.CARTRIDGE_OFFSET, true, true); + Keys = Table.GetKeys(voice.Int8 - GBAUtils.CARTRIDGE_OFFSET); + } + catch + { + Table = null; + Keys = null; + } + Name = $"Key Split ({Keys!.Select(k => k.Item1).Distinct().Count()})"; + } + else + { + Name = $"Key Split"; + } + IsValidVoiceEntry = IsValidTableOffset(); + break; + } + case (byte)VoiceFlags.Drum: + { + VoiceEntry = voice; + Name = voice.Name; + RootNote = (sbyte)voice.RootNote; + if (!isSubVoiceGroup) + { + Table = MP2KSoundBank.LoadTable(voice.Int4 - GBAUtils.CARTRIDGE_OFFSET, true, true); + } + IsValidVoiceEntry = IsValidTableOffset(); + break; + } + } + } + + private readonly bool IsValidVoiceType(VoiceEntry voice) + { + switch (voice.Type) + { + case (byte)VoiceType.PCM8: + case (byte)VoiceType.Square1: + case (byte)VoiceType.Square2: + case (byte)VoiceType.PCM4: + case (byte)VoiceType.Noise: + { + return true; + } + + case (byte)VoiceType.Invalid5: + case (byte)VoiceType.Invalid6: + case (byte)VoiceType.Invalid7: + default: + { + return false; + } + + case (byte)VoiceFlags.Fixed: + case (byte)VoiceFlags.OffWithNoise: + case 0xA: + case 0xB: + case 0xC: + case (byte)VoiceFlags.Reversed: + case (byte)VoiceFlags.Compressed: + case (byte)VoiceFlags.KeySplit: + case (byte)VoiceFlags.Drum: + { + return true; + } + } + } + private readonly bool IsValidTableOffset() + { + var offset = VoiceEntry!.Value.Int4 - GBAUtils.CARTRIDGE_OFFSET; + if (GBAUtils.IsValidRomOffset(offset)) + { + VoiceEntry[] vTable = new VoiceEntry[128]; + for (int i = 0; i < vTable.Length; i++) + { + vTable[i] = new VoiceEntry(Engine.Instance!.Config.ROM!.AsSpan(offset + (i * 12))); + if (!IsValidVoiceType(vTable[i])) + { + return false; + } + } + return true; + } + else + { + return false; + } + } + private readonly bool IsValidADSR() + { + switch (VoiceEntry!.Value.Type) + { + case (byte)VoiceType.PCM8: + { + return true; + } + case (byte)VoiceType.Square1: + case (byte)VoiceType.Square2: + case (byte)VoiceType.PCM4: + case (byte)VoiceType.Noise: + { + return (VoiceEntry!.Value.ADSR.A <= 0x7) && (VoiceEntry.Value.ADSR.D <= 0x7) && (VoiceEntry.Value.ADSR.S <= 0xF) && (VoiceEntry.Value.ADSR.R <= 0x7); + } + + case (byte)VoiceType.Invalid5: + case (byte)VoiceType.Invalid6: + case (byte)VoiceType.Invalid7: + default: + { + return false; + } + + case (byte)VoiceFlags.Fixed: + { + goto case (byte)VoiceType.PCM8; + } + case (byte)VoiceFlags.OffWithNoise: + { + goto case (byte)VoiceType.Square1; + } + case 0xA: + { + goto case (byte)VoiceType.Square2; + } + case 0xB: + { + goto case (byte)VoiceType.PCM4; + } + case 0xC: + { + goto case (byte)VoiceType.Noise; + } + case (byte)VoiceFlags.Reversed: + case (byte)VoiceFlags.Compressed: + { + goto case (byte)VoiceType.PCM8; + } + } + } + public IEnumerable GetSubVoices() => Enumerable.Empty(); + + public override readonly string ToString() => Name!; } + [StructLayout(LayoutKind.Sequential, Pack = 4, Size = SIZE)] -internal readonly struct VoiceEntry +internal 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)); - } - } + public const int SIZE = 12; + + public byte Type; // 0 + public byte RootNote; // 1 + /// Hardware microseconds for Square1, Square2 and Noise PSG types only, while all other types set it to 0 because it's skipped + public byte TimeLength; // 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 readonly int Int8 => (ADSR.R << 24) | (ADSR.S << 16) | (ADSR.D << 8) | (ADSR.A); + + public readonly string? Name + { + get + { + var name = ""; + if (Type == (int)VoiceFlags.KeySplit) + { + name = "Key Split"; + } + else if (Type == (int)VoiceFlags.Drum) + { + name = "Drum"; + } + else + { + switch ((VoiceType)(Type & 0x7)) + { + case VoiceType.PCM8: name = IsGoldenSunPSG() ? $"GS {GoldenSunPSG.Get(Engine.Instance!.Config.ROM!.AsSpan(Int8 /*- GBAUtils.CARTRIDGE_OFFSET*/ + 0x10)).Type}" : "PCM8"; break; + case VoiceType.Square1: name = "Square 1"; break; + case VoiceType.Square2: name = "Square 2"; break; + case VoiceType.PCM4: name = "PCM4"; break; + case VoiceType.Noise: name = "Noise"; break; + case VoiceType.Invalid5: name = "Invalid 5"; break; + case VoiceType.Invalid6: name = "Invalid 6"; break; + case VoiceType.Invalid7: name = "Invalid 7"; break; + } + } + return name; + } + } + + public VoiceEntry(ReadOnlySpan src) + { + if (BitConverter.IsLittleEndian) + { + this = MemoryMarshal.AsRef(src); + } + else + { + Type = src[0]; + RootNote = src[1]; + TimeLength = src[2]; + Pan = src[3]; + Int4 = ReadInt32LittleEndian(src.Slice(4)); + ADSR = ADSR.Get(src.Slice(8)); + } + } + + public void SetAddressPointer(int address) + { + Int4 = address; + } + public readonly bool IsPSGInstrument() + { + if (Type == (int)VoiceFlags.KeySplit || Type == (int)VoiceFlags.Drum) + { + return false; + } + VoiceType vType = (VoiceType)(Type & 0x7); + return vType >= VoiceType.Square1 && vType <= VoiceType.Noise; + } + public readonly bool IsGoldenSunPSG() + { + if (!MP2KEngine.MP2KInstance!.Config.HasGoldenSunSynths || (Type & 0x7) != (int)VoiceType.PCM8 + || Type == (int)VoiceFlags.KeySplit || Type == (int)VoiceFlags.Drum) + { + return false; + } + var gSample = new SampleHeader(Engine.Instance!.Config.ROM!.AsSpan(Int8 - GBAUtils.CARTRIDGE_OFFSET)); + return gSample.DoesLoop == 0 && gSample.LoopOffset == 0 && gSample.Length == 0; + } + public bool IsInvalid() + { + return (Type & 0x7) >= (int)VoiceType.Invalid5; + } + public string GetBytesToString() + { + return $"{Type:X2} {RootNote:X2} {TimeLength:X2} {Pan:X2} " + + $"{(byte)(Int8):X2} {(byte)(Int8 >> 8):X2} {(byte)(Int8 >> 16):X2} {(byte)(Int8 >> 24):X2} " + + $"{ADSR.A:X2} {ADSR.D:X2} {ADSR.S:X2} {ADSR.R:X2}"; + } } [StructLayout(LayoutKind.Sequential, Pack = 4, Size = SIZE)] -internal struct ADSR +public struct ADSR { - public const int SIZE = 4; + public const int SIZE = 4; - public byte A; - public byte D; - public byte S; - public byte R; + 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); - } + 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); - } + 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)); - } + 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; + public float LeftVol, RightVol; } internal struct NoteInfo { - public byte Note, OriginalNote; - public byte Velocity; - /// -1 if forever - public int Duration; + public byte Note, OriginalNote; + public byte Velocity; + /// -1 if forever + public int Duration; + public byte PseudoEchoVolume, PseudoEchoLength; } diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KTrack.cs b/VG Music Studio - Core/GBA/MP2K/MP2KTrack.cs index e612465d..a4fee3ee 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KTrack.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KTrack.cs @@ -26,11 +26,19 @@ internal sealed class MP2KTrack public int DataOffset; public int[] CallStack = new int[3]; public byte CallStackDepth; + public bool RepeatActivated = false; + public byte RepeatTimes; + public int RepeatOffset; public byte RunCmd; public byte PrevNote; public byte PrevVelocity; - - public readonly List Channels = new(); + internal byte PseudoEchoVolume; + internal byte PseudoEchoLength; + internal byte MemSet; + internal byte MemAddress; + internal byte MemData; + public byte Reverb; + public readonly List Channels = []; public int GetPitch() { @@ -186,6 +194,7 @@ public void UpdateSongState(SongState.Track tin, MP2KLoadedSong loadedSong, stri tin.Volume = GetVolume(); tin.PitchBend = GetPitch(); tin.Panpot = GetPanpot(); + tin.Reverb = Reverb = loadedSong.Header.Reverb; MP2KChannel[] channels = Channels.ToArray(); if (channels.Length == 0) @@ -202,18 +211,21 @@ public void UpdateSongState(SongState.Track tin, MP2KLoadedSong loadedSong, stri 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) + if (c is not null) { - right = vol.RightVol; + 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 diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs b/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs old mode 100644 new mode 100755 index 6b3b0f6f..4571786f --- a/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KUtils.cs @@ -5,118 +5,124 @@ namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; internal static partial class MP2KUtils { - public static ReadOnlySpan RestTable => new byte[49] - { - 00, 01, 02, 03, 04, 05, 06, 07, - 08, 09, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, - 24, 28, 30, 32, 36, 40, 42, 44, - 48, 52, 54, 56, 60, 64, 66, 68, - 72, 76, 78, 80, 84, 88, 90, 92, - 96, - }; - public static ReadOnlySpan<(int sampleRate, int samplesPerBuffer)> FrequencyTable => new (int, int)[12] - { - (05734, 096), // 59.72916666666667 - (07884, 132), // 59.72727272727273 - (10512, 176), // 59.72727272727273 - (13379, 224), // 59.72767857142857 - (15768, 264), // 59.72727272727273 - (18157, 304), // 59.72697368421053 - (21024, 352), // 59.72727272727273 - (26758, 448), // 59.72767857142857 - (31536, 528), // 59.72727272727273 - (36314, 608), // 59.72697368421053 - (40137, 672), // 59.72767857142857 - (42048, 704), // 59.72727272727273 - }; + public static ReadOnlySpan RestTable => + [ + 00, 01, 02, 03, 04, 05, 06, 07, + 08, 09, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 28, 30, 32, 36, 40, 42, 44, + 48, 52, 54, 56, 60, 64, 66, 68, + 72, 76, 78, 80, 84, 88, 90, 92, + 96, + ]; + public static ReadOnlySpan VolumeTable => + [ + 14, 16, 17, 19, 21, 22, 24, 27, 29, 32, 34, 36, 37, 39, 42, 44, 46, 47, 49, 51, 52, 54, 56, 57, 59, 62, 64, 66, 67, 69, 72, 77, 79, 80, 82, 84, 85, 87, + 94, 96, 97, 104, 105, 106, 109, 113, 114, 116, 119, 120, 127, + ]; + public static ReadOnlySpan<(int sampleRate, int samplesPerBuffer)> FrequencyTable => new (int, int)[12] + { + (05734, 096), // 59.72916666666667 + (07884, 132), // 59.72727272727273 + (10512, 176), // 59.72727272727273 + (13379, 224), // 59.72767857142857 + (15768, 264), // 59.72727272727273 + (18157, 304), // 59.72697368421053 + (21024, 352), // 59.72727272727273 + (26758, 448), // 59.72767857142857 + (31536, 528), // 59.72727272727273 + (36314, 608), // 59.72697368421053 + (40137, 672), // 59.72767857142857 + (42048, 704), // 59.72727272727273 + }; - // Squares (Use arrays since they are stored as references in MP2KSquareChannel) - public static readonly float[] SquareD12 = new float[8] { 0.875f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f, }; - public static readonly float[] SquareD25 = new float[8] { 0.750f, 0.750f, -0.250f, -0.250f, -0.250f, -0.250f, -0.250f, -0.250f, }; - public static readonly float[] SquareD50 = new float[8] { 0.500f, 0.500f, 0.500f, 0.500f, -0.500f, -0.500f, -0.500f, -0.500f, }; - public static readonly float[] SquareD75 = new float[8] { 0.250f, 0.250f, 0.250f, 0.250f, 0.250f, 0.250f, -0.750f, -0.750f, }; + // Squares (Use arrays since they are stored as references in MP2KSquareChannel) + public static readonly float[] SquareD12 = [0.875f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f,]; + public static readonly float[] SquareD25 = [0.750f, 0.750f, -0.250f, -0.250f, -0.250f, -0.250f, -0.250f, -0.250f,]; + public static readonly float[] SquareD50 = [0.500f, 0.500f, 0.500f, 0.500f, -0.500f, -0.500f, -0.500f, -0.500f,]; + public static readonly float[] SquareD75 = [0.250f, 0.250f, 0.250f, 0.250f, 0.250f, 0.250f, -0.750f, -0.750f,]; - // Noises - public static readonly BitArray NoiseFine; - public static readonly BitArray NoiseRough; - public static ReadOnlySpan NoiseFrequencyTable => new byte[60] - { - 0xD7, 0xD6, 0xD5, 0xD4, - 0xC7, 0xC6, 0xC5, 0xC4, - 0xB7, 0xB6, 0xB5, 0xB4, - 0xA7, 0xA6, 0xA5, 0xA4, - 0x97, 0x96, 0x95, 0x94, - 0x87, 0x86, 0x85, 0x84, - 0x77, 0x76, 0x75, 0x74, - 0x67, 0x66, 0x65, 0x64, - 0x57, 0x56, 0x55, 0x54, - 0x47, 0x46, 0x45, 0x44, - 0x37, 0x36, 0x35, 0x34, - 0x27, 0x26, 0x25, 0x24, - 0x17, 0x16, 0x15, 0x14, - 0x07, 0x06, 0x05, 0x04, - 0x03, 0x02, 0x01, 0x00, - }; + // Noises + public static readonly BitArray NoiseFine; + public static readonly BitArray NoiseRough; + public static ReadOnlySpan NoiseFrequencyTable => + [ + 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; - } - } + // PCM4 + /// 4-bit PCM Wave to Float conversion + /// The dest param must be 0x20 bytes in length + public static void PCM4ToFloat(ReadOnlySpan src, Span dest) + { + float sum = 0; + for (int i = 0; i < 0x10; i++) + { + byte b = src[i]; + float first = (b >> 4) / 16f; + float second = (b & 0xF) / 16f; + sum += dest[i * 2] = first; + sum += dest[(i * 2) + 1] = second; + } + float dcCorrection = sum / 0x20; + for (int i = 0; i < 0x20; i++) + { + dest[i] -= dcCorrection; + } + } - static MP2KUtils() - { - NoiseFine = new BitArray(0x8_000); - int reg = 0x4_000; - for (int i = 0; i < NoiseFine.Length; i++) - { - if ((reg & 1) == 1) - { - reg >>= 1; - reg ^= 0x6_000; - NoiseFine[i] = true; - } - else - { - reg >>= 1; - NoiseFine[i] = false; - } - } - NoiseRough = new BitArray(0x80); - reg = 0x40; - for (int i = 0; i < NoiseRough.Length; i++) - { - if ((reg & 1) == 1) - { - reg >>= 1; - reg ^= 0x60; - NoiseRough[i] = true; - } - else - { - reg >>= 1; - NoiseRough[i] = false; - } - } - } - public static int Tri(int index) - { - index = (index - 64) & 0xFF; - return (index < 128) ? (index * 12) - 768 : 2_304 - (index * 12); - } + 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/Interfaces.cs b/VG Music Studio - Core/Interfaces.cs new file mode 100755 index 00000000..51c38bb1 --- /dev/null +++ b/VG Music Studio - Core/Interfaces.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core; + +// Used everywhere +public interface IOffset +{ + int Offset { get; set; } +} + +// Used for song events +public interface ICommand +{ + Color Color { get; } + string Label { get; } + string Arguments { get; } +} + +// Used for loading songs +public interface ILoadedSong +{ + List?[] Events { get; } + long MaxTicks { get; } + SoundBank Bank { get; } + + virtual bool CallOrJumpCommand(SongEvent se) { return false; } + virtual void ChangeEvent(SongEvent ev, decimal vArgsVal1, byte vArgsVal2, bool changed) { } + virtual void InsertEvent(SongEvent e, int trackIndex, int insertIndex) { } + virtual void RemoveEvent(int trackIndex, int eventIndex) { } +} + +// Used in the SoundBankEditor. GetName() is also used for the UI +public interface IVoiceInfo : IOffset +{ + string? Name { get; } + IEnumerable GetSubVoices() => Enumerable.Empty(); +} \ No newline at end of file diff --git a/VG Music Studio - Core/LoadedSong.cs b/VG Music Studio - Core/LoadedSong.cs new file mode 100755 index 00000000..3118415a --- /dev/null +++ b/VG Music Studio - Core/LoadedSong.cs @@ -0,0 +1,24 @@ +using Kermalis.VGMusicStudio.Core.GBA.MP2K; +using System; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core; + +public abstract class LoadedSong : IDisposable, ILoadedSong +{ + public abstract List[] Events { get; } + public abstract long MaxTicks { get; protected set; } + public virtual int HeaderOffset { get; protected set; } + public abstract SoundBank Bank { get; protected set; } + + public virtual void InsertEvent(SongEvent e, int trackIndex, int insertIndex) { } + public virtual void ChangeEvent(SongEvent ev, decimal vArgsVal1, byte vArgsVal2, bool changed) { } + public virtual void RemoveEvent(int trackIndex, int eventIndex) { } + public virtual bool CallOrJumpCommand(SongEvent e) { return false; } + public virtual void OpenASM(Assembler assembler, string headerLabel) { } + public virtual void SaveAsASM(string fileName, ASMSaveArgs args) { } + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/VG Music Studio - Core/LowLatencyRingbuffer.cs b/VG Music Studio - Core/LowLatencyRingbuffer.cs new file mode 100644 index 00000000..e5649db0 --- /dev/null +++ b/VG Music Studio - Core/LowLatencyRingbuffer.cs @@ -0,0 +1,226 @@ +// Based on ipatix's implementation from agbplay_v2 branch. +// Original sources: +// https://github.com/ipatix/agbplay/blob/agbplay_v2/src/agbplay/LowLatencyRingbuffer.cpp +// https://github.com/ipatix/agbplay/blob/agbplay_v2/src/agbplay/LowLatencyRingbuffer.hpp +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +namespace Kermalis.VGMusicStudio.Core; + +internal class LowLatencyRingbuffer +{ + internal struct Sample + { + internal float left; + internal float right; + } + + private readonly object? lockObj = new(); + private readonly object? cv = new(); + private List? buffer; + + // Free variables + private int freePos = 0; + private int freeCount = 0; + + // Data variables + private int dataPos = 0; + private int dataCount = 0; + + // Atomic variable for System.Threading.Interlocked + private int lastTake = 0; + + // Last put value + private int lastPut = 0; + + // Number of buffers (beginning from 1) + private int numBuffers = 1; + + // Made by cwills on StackOverflow, found here: + // https://stackoverflow.com/questions/8546/is-there-a-try-to-lock-skip-if-timed-out-operation-in-c/23728163#23728163 + internal class TryLock : IDisposable + { + private object? locked; + + internal bool OwnsLock { get; private set; } + + internal TryLock(object obj) + { + if (Monitor.TryEnter(obj)) + { + OwnsLock = true; + locked = obj; + } + } + + public void Dispose() + { + if (OwnsLock) + { + Monitor.Exit(locked!); + locked = null; + OwnsLock = false; + } + } + } + + public LowLatencyRingbuffer() + { + Reset(); + } + + public void Reset() + { + lock (lockObj!) + { + dataCount = 0; + dataPos = 0; + if (buffer is not null) + { + freeCount = buffer.Count; + } + freePos = 0; + } + } + + public void SetNumBuffers(int numBuffers) + { + lock (lockObj!) + { + if (numBuffers is 0) + numBuffers = 1; + this.numBuffers = numBuffers; + } + } + + public void Put(Span inBuffer) + { + lastPut = inBuffer.Length; + + lock (lockObj!) + { + int bufferedNumBuffers = numBuffers; + int bufferedLastTake = lastTake; + int requiredBufferSize = bufferedNumBuffers * bufferedLastTake + lastPut; + + if (buffer!.Count < requiredBufferSize) + { + IncreaseBufferSize(requiredBufferSize); + } + + while (dataCount > bufferedNumBuffers * bufferedLastTake) + { + Monitor.Wait(cv!); + } + + while (inBuffer.Length > 0) + { + int elementsPut = PutSome(inBuffer); + inBuffer = inBuffer.Slice(elementsPut); + } + } + } + + public void Take(Span outBuffer) + { + lastTake = outBuffer.Length; + + using (var tl = new TryLock(lockObj!)) + { + if (!tl.OwnsLock || outBuffer.Length > dataCount) + { + outBuffer.Fill(new Sample{left = 0.0f, right = 0.0f}); + //Array.Fill(outBuffer.ToArray(), new Sample { left = 0.0f, right = 0.0f }, 0, outBuffer.Length); + return; + } + + while (outBuffer.Length > 0) + { + int elementsTaken = TakeSome(outBuffer); + outBuffer = outBuffer.Slice(elementsTaken); + } + + Monitor.Pulse(cv!); + } + } + + private int PutSome(Span inBuffer) + { + Debug.Assert(inBuffer.Length <= freeCount); + bool wrap = inBuffer.Length >= (buffer!.Count - freePos); + + int putCount; + int newFreePos; + if (wrap) + { + putCount = buffer.Count - freePos; + newFreePos = 0; + } + else + { + putCount = buffer.Count; + newFreePos = freePos + inBuffer.Length; + } + + Array.Copy(inBuffer.ToArray(), 0, buffer.ToArray(), 0 + freePos, putCount); + + freePos = newFreePos; + Debug.Assert(freeCount >= putCount); + freeCount -= putCount; + dataCount += putCount; + return putCount; + } + + private int TakeSome(Span outBuffer) + { + Debug.Assert(outBuffer.Length <= dataCount); + bool wrap = outBuffer.Length >= (buffer!.Count - dataPos); + + int takeCount; + int newDataPos; + if (wrap) + { + takeCount = buffer.Count - dataPos; + newDataPos = 0; + } + else + { + takeCount = outBuffer.Length; + newDataPos = dataPos + outBuffer.Length; + } + + Array.Copy(buffer.ToArray(), 0 + dataPos, outBuffer.ToArray(), 0, takeCount); + + dataPos = newDataPos; + freeCount += takeCount; + Debug.Assert(dataCount >= takeCount); + dataCount -= takeCount; + return takeCount; + } + + private void IncreaseBufferSize(int requiredBufferSize) + { + List backupBuffer = new(new Sample[dataCount]); + int beforeWraparoundSize = Math.Min(dataCount, buffer.Count - dataPos); + Span beforeWraparound = new Span(new Sample[dataCount], 0 + dataPos, beforeWraparoundSize); + int afterWraparoundSize = dataCount - beforeWraparoundSize; + Span afterWraparound = new Span(new Sample[dataCount], 0, afterWraparoundSize); + Array.Copy(buffer.ToArray(), 0 + dataPos, backupBuffer.ToArray(), 0, beforeWraparoundSize); + Array.Copy(buffer.ToArray(), 0, backupBuffer.ToArray(), 0 + beforeWraparoundSize, afterWraparoundSize); + Debug.Assert(beforeWraparoundSize + afterWraparoundSize == dataCount); + Debug.Assert(dataCount <= requiredBufferSize); + + buffer.EnsureCapacity(requiredBufferSize); + //buffer.CopyTo([.. backupBuffer]); + Array.Copy(backupBuffer.ToArray(), 0, buffer.ToArray(), 0, dataCount); + Array.Fill(buffer.ToArray(), new Sample { left = 0.0f, right = 0.0f }, 0 + dataCount, buffer.Count); + + dataPos = 0; + freeCount = buffer.Count - dataCount; + freePos = dataCount; + } +} \ No newline at end of file diff --git a/VG Music Studio - Core/MP2K.yaml b/VG Music Studio - Core/MP2K.yaml index 7235295e..09b080bf 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 @@ -110,7 +128,7 @@ A7KP_00: SongTableOffsets: 0x843B50 Copy: "A7KE_00" A88E_00: - Name: "Mario & Luigi - Superstar Saga (USA)" + Name: "Mario Bros. (Mario & Luigi: Superstar Saga) (USA)" SongTableOffsets: 0xFB5DA4 SongTableSizes: 323 SampleRate: 2 @@ -120,11 +138,11 @@ A88E_00: HasGoldenSunSynths: False HasPokemonCompression: False A88J_00: - Name: "Mario & Luigi - Superstar Saga (Japan)" + Name: "Mario Bros. (Mario & Luigi RPG) (Japan)" SongTableOffsets: 0xFB5E00 Copy: "A88E_00" A88P_00: - Name: "Mario & Luigi - Superstar Saga (Europe)" + Name: "Mario Bros. (Mario & Luigi: Superstar Saga) (Europe)" Copy: "A88E_00" AE7E_00: Name: "Fire Emblem: The Blazing Blade (USA)" @@ -376,7 +394,7 @@ AMKE_00: 44: "SNES Vanilla Lake" 45: "SNES Ghost Valley" 46: "SNES Rainbow Road" - 47: "SNES Bowser's Castle" + 47: "SNES Bowser Castle" 48: "SNES Koopa Beach" 51: "Night Opening" 52: "Title Screen" @@ -798,16 +816,17 @@ AXVE_00: 455: "Ending Theme" 456: "The End" Other Music: - 350: "Unused - TETSUJI" - 351: "Unused - Route 38" 352: "Victory! (Wild Pokémon) (No Intro)" - 356: "Unused - Pokémon Center (2)" - 357: "Unused - Viridian City" - 358: "Unused - Battle! (Entei/Raikou/Suicune)" 393: "Pokémon Contest! (Multiplayer - Player 1)" 394: "Pokémon Contest! (Multiplayer - Player 2)" 395: "Pokémon Contest! (Multiplayer - Player 3)" 396: "Pokémon Contest! (Multiplayer - Player 4)" + Unused Early Prototype Music: + 350: "Unused - TETSUJI" + 351: "Unused - Route 38" + 356: "Unused - Pokémon Communication Center" + 357: "Unused - Pewter City" + 358: "Unused - Battle! (Entei/Raikou/Suicune)" 467: "Unused - Team Rocket Appears!" AXVE_01: SongTableOffsets: 0x4554A0 @@ -837,7 +856,7 @@ AXVS_00: AXVS_01: Copy: "AXVS_00" AZLE_00: - Name: "The Legend of Zelda: A Link to the Past and Four Swords (USA)" + Name: "The Legend of Zelda: Four Swords (The Legend of Zelda: A Link to the Past and Four Swords) (USA)" SongTableOffsets: 0x3C3BBC SongTableSizes: 545 SampleRate: 3 @@ -847,11 +866,11 @@ AZLE_00: HasGoldenSunSynths: False HasPokemonCompression: False AZLJ_00: - Name: "The Legend of Zelda: A Link to the Past and Four Swords (Japan)" + Name: "The Legend of Zelda: Four Swords (The Legend of Zelda: A Link to the Past and Four Swords) (Japan)" SongTableOffsets: 0x3E7A54 Copy: "AZLE_00" AZLP_00: - Name: "The Legend of Zelda: A Link to the Past and Four Swords (Europe)" + Name: "The Legend of Zelda: Four Swords (The Legend of Zelda: A Link to the Past and Four Swords) (Europe)" SongTableOffsets: 0x43F8AC Copy: "AZLE_00" B24E_00: @@ -1163,8 +1182,8 @@ BPEE_00: 353: "Victory! (Wild Pokémon)" 354: "Victory! (Gym Leader)" 355: "Victory! (Wallace)" - 356: "Unused - Pokémon Center (2)" - 357: "Unused - Viridian City" + 356: "Unused - Pokémon Communications Center" + 357: "Unused - Pewter City" 358: "Unused - Battle! (Entei/Raikou/Suicune)" 359: "Route 101" 360: "Route 110" @@ -1341,7 +1360,7 @@ BPEE_00: 531: "Fanfare: Pokémon Caught" 532: "Pokémon Printer (FRLG)" 533: "Game Freak Logo (FRLG)" - 534: "Fanfare: Pokémon Caught (No Intro) (FRLG)" + 534: "Victory! (Wild Pokémon) (No Intro) (FRLG)" 535: "Game Tutorial (1) (FRLG)" 536: "Game Tutorial (2) (FRLG)" 537: "Game Tutorial (3) (FRLG)" @@ -1479,6 +1498,354 @@ BPRE_00: Volume: 12 HasGoldenSunSynths: False HasPokemonCompression: True + InternalSongNames: + 0: "MUS_DUMMY" + 1: "SE_KAIFUKU" + 2: "SE_PC_LOGIN" + 3: "SE_PC_OFF" + 4: "SE_PC_ON" + 5: "SE_SELECT" + 6: "SE_WIN_OPEN" + 7: "SE_WALL_HIT" + 8: "SE_DOOR" + 9: "SE_KAIDAN" + 10: "SE_DANSA" + 11: "SE_JITENSYA" + 12: "SE_KOUKA_L" + 13: "SE_KOUKA_M" + 14: "SE_KOUKA_H" + 15: "SE_BOWA2" + 16: "SE_POKE_DEAD" + 17: "SE_NIGERU" + 18: "SE_JIDO_DOA" + 19: "SE_NAMINORI" + 20: "SE_BAN" + 21: "SE_PIN" + 22: "SE_BOO" + 23: "SE_BOWA" + 24: "SE_JYUNI" + 25: "SE_SEIKAI" + 26: "SE_HAZURE" + 27: "SE_EXP" + 28: "SE_JITE_PYOKO" + 29: "SE_MU_PACHI" + 30: "SE_TK_KASYA" + 31: "SE_FU_ZAKU" + 32: "SE_FU_ZAKU2" + 33: "SE_FU_ZUZUZU" + 34: "SE_RU_GASHIN" + 35: "SE_RU_GASYAN" + 36: "SE_RU_BARI" + 37: "SE_RU_HYUU" + 38: "SE_KI_GASYAN" + 39: "SE_TK_WARPIN" + 40: "SE_TK_WARPOUT" + 41: "SE_TU_SAA" + 42: "SE_HI_TURUN" + 43: "SE_TRACK_MOVE" + 44: "SE_TRACK_STOP" + 45: "SE_TRACK_HAIKI" + 46: "SE_TRACK_DOOR" + 47: "SE_MOTER" + 48: "SE_SAVE" + 49: "SE_KON" + 50: "SE_KON2" + 51: "SE_KON3" + 52: "SE_KON4" + 53: "SE_SUIKOMU" + 54: "SE_NAGERU" + 55: "SE_TOY_C" + 56: "SE_TOY_D" + 57: "SE_TOY_E" + 58: "SE_TOY_F" + 59: "SE_TOY_G" + 60: "SE_TOY_A" + 61: "SE_TOY_B" + 62: "SE_TOY_C1" + 63: "SE_MIZU" + 64: "SE_HASHI" + 65: "SE_DAUGI" + 66: "SE_PINPON" + 67: "SE_FUUSEN1" + 68: "SE_FUUSEN2" + 69: "SE_FUUSEN3" + 70: "SE_TOY_KABE" + 71: "SE_TOY_DANGO" + 72: "SE_DOKU" + 73: "SE_ESUKA" + 74: "SE_T_AME" + 75: "SE_T_AME_E" + 76: "SE_T_OOAME" + 77: "SE_T_OOAME_E" + 78: "SE_T_KOAME" + 79: "SE_T_KOAME_E" + 80: "SE_T_KAMI" + 81: "SE_T_KAMI2" + 82: "SE_ELEBETA" + 83: "SE_HINSI" + 84: "SE_EXPMAX" + 85: "SE_TAMAKORO" + 86: "SE_TAMAKORO_E" + 87: "SE_BASABASA" + 88: "SE_REGI" + 89: "SE_C_GAJI" + 90: "SE_C_MAKU_U" + 91: "SE_C_MAKU_D" + 92: "SE_C_PASI" + 93: "SE_C_SYU" + 94: "SE_C_PIKON" + 95: "SE_REAPOKE" + 96: "SE_OP_BASYU" + 97: "SE_BT_START" + 98: "SE_DENDOU" + 99: "SE_JIHANKI" + 100: "SE_TAMA" + 101: "SE_Z_SCROLL" + 102: "SE_Z_PAGE" + 103: "SE_PN_ON" + 104: "SE_PN_OFF" + 105: "SE_Z_SEARCH" + 106: "SE_TAMAGO" + 107: "SE_TB_START" + 108: "SE_TB_KON" + 109: "SE_TB_KARA" + 110: "SE_BIDORO" + 111: "SE_W085" + 112: "SE_W085B" + 113: "SE_W231" + 114: "SE_W171" + 115: "SE_W233" + 116: "SE_W233B" + 117: "SE_W145" + 118: "SE_W145B" + 119: "SE_W145C" + 120: "SE_W240" + 121: "SE_W015" + 122: "SE_W081" + 123: "SE_W081B" + 124: "SE_W088" + 125: "SE_W016" + 126: "SE_W016B" + 127: "SE_W003" + 128: "SE_W104" + 129: "SE_W013" + 130: "SE_W196" + 131: "SE_W086" + 132: "SE_W004" + 133: "SE_W025" + 134: "SE_W025B" + 135: "SE_W152" + 136: "SE_W026" + 137: "SE_W172" + 138: "SE_W172B" + 139: "SE_W053" + 140: "SE_W007" + 141: "SE_W092" + 142: "SE_W221" + 143: "SE_W221B" + 144: "SE_W052" + 145: "SE_W036" + 146: "SE_W059" + 147: "SE_W059B" + 148: "SE_W010" + 149: "SE_W011" + 150: "SE_W017" + 151: "SE_W019" + 152: "SE_W028" + 153: "SE_W013B" + 154: "SE_W044" + 155: "SE_W029" + 156: "SE_W057" + 157: "SE_W056" + 158: "SE_W250" + 159: "SE_W030" + 160: "SE_W039" + 161: "SE_W054" + 162: "SE_W077" + 163: "SE_W020" + 164: "SE_W082" + 165: "SE_W047" + 166: "SE_W195" + 167: "SE_W006" + 168: "SE_W091" + 169: "SE_W146" + 170: "SE_W120" + 171: "SE_W153" + 172: "SE_W071B" + 173: "SE_W071" + 174: "SE_W103" + 175: "SE_W062" + 176: "SE_W062B" + 177: "SE_W048" + 178: "SE_W187" + 179: "SE_W118" + 180: "SE_W155" + 181: "SE_W122" + 182: "SE_W060" + 183: "SE_W185" + 184: "SE_W014" + 185: "SE_W043" + 186: "SE_W207" + 187: "SE_W207B" + 188: "SE_W215" + 189: "SE_W109" + 190: "SE_W173" + 191: "SE_W280" + 192: "SE_W202" + 193: "SE_W060B" + 194: "SE_W076" + 195: "SE_W080" + 196: "SE_W100" + 197: "SE_W107" + 198: "SE_W166" + 199: "SE_W129" + 200: "SE_W115" + 201: "SE_W112" + 202: "SE_W197" + 203: "SE_W199" + 204: "SE_W236" + 205: "SE_W204" + 206: "SE_W268" + 207: "SE_W070" + 208: "SE_W063" + 209: "SE_W127" + 210: "SE_W179" + 211: "SE_W151" + 212: "SE_W201" + 213: "SE_W161" + 214: "SE_W161B" + 215: "SE_W227" + 216: "SE_W227B" + 217: "SE_W226" + 218: "SE_W208" + 219: "SE_W213" + 220: "SE_W213B" + 221: "SE_W234" + 222: "SE_W260" + 223: "SE_W328" + 224: "SE_W320" + 225: "SE_W255" + 226: "SE_W291" + 227: "SE_W089" + 228: "SE_W239" + 229: "SE_W230" + 230: "SE_W281" + 231: "SE_W327" + 232: "SE_W287" + 233: "SE_W257" + 234: "SE_W253" + 235: "SE_W258" + 236: "SE_W322" + 237: "SE_W298" + 238: "SE_W287B" + 239: "SE_W114" + 240: "SE_W063B" + 241: "MUS_W_DOOR" + 242: "SE_CARD1" + 243: "SE_CARD2" + 244: "SE_CARD3" + 245: "SE_BAG1" + 246: "SE_BAG2" + 247: "SE_GETTING" + 248: "SE_SHOP" + 249: "SE_KITEKI" + 250: "SE_HELP_OP" + 251: "SE_HELP_CL" + 252: "SE_HELP_NG" + 253: "SE_DEOMOV" + 254: "SE_EXCELLENT" + 255: "SE_NAWAMISS" + 256: "MUS_ME_ASA" + 257: "MUS_FANFA1" + 258: "MUS_FANFA4" + 259: "MUS_FANFA5" + 260: "MUS_ME_BACHI" + 261: "MUS_ME_WAZA" + 262: "MUS_ME_KINOMI" + 263: "MUS_ME_SHINKA" + 264: "MUS_SHINKA" + 265: "MUS_BATTLE32" + 266: "MUS_BATTLE20" + 267: "MUS_P_SCHOOL" + 268: "MUS_ME_B_BIG" + 269: "MUS_ME_B_SMALL" + 270: "MUS_ME_WASURE" + 271: "MUS_ME_ZANNEN" + 272: "MUS_ANNAI" + 273: "MUS_SLOT" + 274: "MUS_AJITO" + 275: "MUS_GYM" + 276: "MUS_PURIN" + 277: "MUS_DEMO" + 278: "MUS_TITLE" + 279: "MUS_GUREN" + 280: "MUS_SHION" + 281: "MUS_KAIHUKU" + 282: "MUS_CYCLING" + 283: "MUS_ROCKET" + 284: "MUS_SHOUJO" + 285: "MUS_SHOUNEN" + 286: "MUS_DENDOU" + 287: "MUS_T_MORI" + 288: "MUS_OTSUKIMI" + 289: "MUS_POKEYASHI" + 290: "MUS_ENDING" + 291: "MUS_LOAD01" + 292: "MUS_OPENING" + 293: "MUS_LOAD02" + 294: "MUS_LOAD03" + 295: "MUS_CHAMP_R" + 296: "MUS_VS_GYM" + 297: "MUS_VS_TORE" + 298: "MUS_VS_YASEI" + 299: "MUS_VS_LAST" + 300: "MUS_MASARA" + 301: "MUS_KENKYU" + 302: "MUS_OHKIDO" + 303: "MUS_POKECEN" + 304: "MUS_SANTOAN" + 305: "MUS_NAMINORI" + 306: "MUS_P_TOWER" + 307: "MUS_SHIRUHU" + 308: "MUS_HANADA" + 309: "MUS_TAMAMUSI" + 310: "MUS_WIN_TRE" + 311: "MUS_WIN_YASEI" + 312: "MUS_WIN_GYM" + 313: "MUS_KUCHIBA" + 314: "MUS_NIBI" + 315: "MUS_RIVAL1" + 316: "MUS_RIVAL2" + 317: "MUS_FAN2" + 318: "MUS_FAN5" + 319: "MUS_FAN6" + 320: "MUS_ME_PHOTO" + 321: "MUS_TITLEROG" + 322: "MUS_GET_YASEI" + 323: "MUS_SOUSA" + 324: "MUS_SEKAIKAN" + 325: "MUS_SEIBETU" + 326: "MUS_JUMP" + 327: "MUS_UNION" + 328: "MUS_NETWORK" + 329: "MUS_OKURIMONO" + 330: "MUS_KINOMIKUI" + 331: "MUS_NANADUNGEON" + 332: "MUS_OSHIE_TV" + 333: "MUS_NANASHIMA" + 334: "MUS_NANAISEKI" + 335: "MUS_NANA123" + 336: "MUS_NANA45" + 337: "MUS_NANA67" + 338: "MUS_POKEFUE" + 339: "MUS_VS_DEO" + 340: "MUS_VS_MYU2" + 341: "MUS_VS_DEN" + 342: "MUS_EXEYE" + 343: "MUS_DEOEYE" + 344: "MUS_T_TOWER" + 345: "MUS_SLOWMASARA" + 346: "MUS_TVNOIZE" Playlists: Disc 1: 321: "Game Freak Logo" @@ -1568,7 +1935,7 @@ BPRE_00: 267: "Unused - Trainers' School (RS)" 281: "Unused - Pokémon Healed (2)" 316: "A Rival Appears (No Intro)" - 322: "Fanfare: Pokémon Caught (No Intro)" + 322: "Victory! (Wild Pokémon) (No Intro)" 331: "Mt. Ember" 332: "Teachy TV Lesson" 334: "Tanoby Chambers" @@ -1686,7 +2053,7 @@ KYGJ_00: SongTableOffsets: 0x4A79D8 Copy: "KYGE_00" KYGP_00: - Name: "Yoshi Topsy-Turvy (Europe)" + Name: "Yoshi's Universal Gravitation (Europe)" SongTableOffsets: 0x619658 Copy: "KYGE_00" U32E_00: diff --git a/VG Music Studio - Core/Mixer.cs b/VG Music Studio - Core/Mixer.cs index 7b8d4c55..402c923d 100644 --- a/VG Music Studio - Core/Mixer.cs +++ b/VG Music Studio - Core/Mixer.cs @@ -1,109 +1,507 @@ -using NAudio.CoreAudioApi; -using NAudio.CoreAudioApi.Interfaces; -using NAudio.Wave; +using PortAudio; using System; +using System.Runtime.InteropServices; +using System.Linq; +using System.IO; +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Formats; +using Stream = PortAudio.Stream; +using NAudio.Wave; +using NAudio.CoreAudioApi; +using NAudio.CoreAudioApi.Interfaces; +using SoundFlow.Abstracts; +using SoundFlow.Backends.MiniAudio; +using SoundFlow.Components; +using SoundFlow.Enums; +using SoundFlow.Providers; +using SoundFlow.Structs; +using SoundFlow.Abstracts.Devices; +using SoundFlow.Interfaces; namespace Kermalis.VGMusicStudio.Core; public abstract class Mixer : IAudioSessionEventsHandler, IDisposable { - public static event Action? VolumeChanged; - - public readonly bool[] Mutes; - private IWavePlayer _out; - private AudioSessionControl _appVolume; - - private bool _shouldSendVolUpdateEvent = true; - - protected WaveFileWriter? _waveWriter; - protected abstract WaveFormat WaveFormat { get; } - - protected Mixer() - { - Mutes = new bool[SongState.MAX_TRACKS]; - _out = null!; - _appVolume = null!; - } - - protected void Init(IWaveProvider waveProvider) - { - _out = new WasapiOut(); - _out.Init(waveProvider); - using (var en = new MMDeviceEnumerator()) - { - SessionCollection sessions = en.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia).AudioSessionManager.Sessions; - int id = Environment.ProcessId; - for (int i = 0; i < sessions.Count; i++) - { - AudioSessionControl session = sessions[i]; - if (session.GetProcessID == id) - { - _appVolume = session; - _appVolume.RegisterEventClient(this); - break; - } - } - } - _out.Play(); - } - - public void CreateWaveWriter(string fileName) - { - _waveWriter = new WaveFileWriter(fileName, WaveFormat); - } - public void CloseWaveWriter() - { - _waveWriter!.Dispose(); - _waveWriter = null; - } - - public void OnVolumeChanged(float volume, bool isMuted) - { - if (_shouldSendVolUpdateEvent) - { - VolumeChanged?.Invoke(volume); - } - _shouldSendVolUpdateEvent = true; - } - public void OnDisplayNameChanged(string displayName) - { - throw new NotImplementedException(); - } - public void OnIconPathChanged(string iconPath) - { - throw new NotImplementedException(); - } - public void OnChannelVolumeChanged(uint channelCount, IntPtr newVolumes, uint channelIndex) - { - throw new NotImplementedException(); - } - public void OnGroupingParamChanged(ref Guid groupingId) - { - throw new NotImplementedException(); - } - // Fires on @out.Play() and @out.Stop() - public void OnStateChanged(AudioSessionState state) - { - if (state == AudioSessionState.AudioSessionStateActive) - { - OnVolumeChanged(_appVolume.SimpleAudioVolume.Volume, _appVolume.SimpleAudioVolume.Mute); - } - } - public void OnSessionDisconnected(AudioSessionDisconnectReason disconnectReason) - { - throw new NotImplementedException(); - } - public void SetVolume(float volume) - { - _shouldSendVolUpdateEvent = false; - _appVolume.SimpleAudioVolume.Volume = volume; - } - - public virtual void Dispose() - { - GC.SuppressFinalize(this); - _out.Stop(); - _out.Dispose(); - _appVolume.Dispose(); - } + public readonly bool[] Mutes; + + #region MiniAudio Fields + // MiniAudio Fields + private AudioEngine? _soundFlowEngine; + private AudioPlaybackDevice? _playbackDevice; + internal SoundPlayer? MiniAudioPlayer; + internal QueueDataProvider? DataProvider; + private System.IO.Stream? _soundFlowFileStream; + protected ISoundEncoder? _soundFlowEncoder; + protected abstract AudioFormat SoundFlowFormat { get; } // New virtual field! Thank you, LSXPrime! + #endregion + + #region PortAudio Fields + // PortAudio Fields + public Wave? WaveData; + internal abstract int SamplesPerBuffer { get; } + + public readonly object CountLock = new(); + + protected Wave? _waveWriterPortAudio; + + internal PortAudioPlayer? PortAudioPlayer; + #endregion + + #region NAudio Fields + // NAudio Fields + public static event Action? VolumeChanged; + private WasapiOut? _out; + private AudioSessionControl? _appVolume; + + private bool _shouldSendVolUpdateEvent = true; + + protected WaveFileWriter? _waveWriterNAudio; + protected abstract WaveFormat? WaveFormat { get; } + #endregion + + // Audio Backend + public static AudioBackend PlaybackBackend { get; set; } + + public enum AudioBackend + { + PortAudio, + MiniAudio, + NAudio + } + + protected Mixer() + { + Mutes = new bool[SongState.MAX_TRACKS]; + if (PlaybackBackend is AudioBackend.NAudio) + { + _out = null!; + _appVolume = null!; + } + } + + protected void Init(Wave waveData = null!, PortAudio.SampleFormat sampleFormat = PortAudio.SampleFormat.Float32, + IWaveProvider waveProvider = null!) + { + switch (PlaybackBackend) + { + case AudioBackend.PortAudio: + { + // First, check if the instance contains something + if (WaveData is null || PortAudioPlayer is null) + { + WaveData = waveData; + PortAudioPlayer = new PortAudioPlayer(sampleFormat, SamplesPerBuffer, WaveData); + } + PortAudioPlayer.Play(); + break; + } + case AudioBackend.MiniAudio: + { + _soundFlowEngine = new MiniAudioEngine(); + // LSXPrime's notes: Let SoundFlow pick the default device by passing null + _playbackDevice = _soundFlowEngine.InitializePlaybackDevice(null, SoundFlowFormat); // I have to pass the deviceInfo as null, thank you LSXPrime for showing me + // LSXPrime's notes: Provide a capacity to the queue to prevent unbound memory growth + DataProvider = new QueueDataProvider(SoundFlowFormat, SamplesPerBuffer * 64, QueueFullBehavior.Block); // Apparently I needed to add buffer length as well. Thank you, LSXPrime for pointing that out + // _soundFlowDecoder = _soundFlowEngine.CreateDecoder(new MemoryStream(), SoundFlowFormat); + MiniAudioPlayer = new SoundPlayer(_soundFlowEngine, SoundFlowFormat, DataProvider); + _playbackDevice.MasterMixer.AddComponent(MiniAudioPlayer); + _playbackDevice.Start(); + // LSXPrime's notes: Start from the player once; it will pull from the queue automatically + MiniAudioPlayer.Play(); // LSXPrime said that I only need to start the player once and it will pull from the queue automatically + break; + } + case AudioBackend.NAudio: + { + _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(); + break; + } + } + } + + public float Volume + { + get => GetVolume(); + set => SetVolume(value); + } + + public float GetVolume() + { + return PlaybackBackend switch + { + AudioBackend.PortAudio => PortAudioPlayer!.Volume, + AudioBackend.MiniAudio => _playbackDevice!.MasterMixer.Volume, + AudioBackend.NAudio => _appVolume!.SimpleAudioVolume.Volume, + _ => float.NaN, + }; + } + + public void SetVolume(float volume) + { + switch (PlaybackBackend) + { + case AudioBackend.PortAudio: + { + PortAudioPlayer!.Volume = Math.Clamp(volume, 0, 1); + break; + } + case AudioBackend.MiniAudio: + { + _playbackDevice!.MasterMixer.Volume = volume; + break; + } + case AudioBackend.NAudio: + { + _shouldSendVolUpdateEvent = false; + _appVolume!.SimpleAudioVolume.Volume = volume; + break; + } + } + } + + #region NAudio Functions + public void OnVolumeChanged(float volume, bool isMuted) + { + if (_shouldSendVolUpdateEvent) + { + VolumeChanged?.Invoke(volume); + } + _shouldSendVolUpdateEvent = true; + } + public void OnDisplayNameChanged(string displayName) + { + throw new NotImplementedException(); + } + public void OnIconPathChanged(string iconPath) + { + throw new NotImplementedException(); + } + public void OnChannelVolumeChanged(uint channelCount, IntPtr newVolumes, uint channelIndex) + { + throw new NotImplementedException(); + } + public void OnGroupingParamChanged(ref Guid groupingId) + { + throw new NotImplementedException(); + } + // Fires on @out.Play() and @out.Stop() + public void OnStateChanged(AudioSessionState state) + { + if (state == AudioSessionState.AudioSessionStateActive) + { + OnVolumeChanged(_appVolume!.SimpleAudioVolume.Volume, _appVolume.SimpleAudioVolume.Mute); + } + } + public void OnSessionDisconnected(AudioSessionDisconnectReason disconnectReason) + { + throw new NotImplementedException(); + } + #endregion + + public void CreateWaveWriter(string fileName) + { + switch (PlaybackBackend) + { + case AudioBackend.PortAudio: + { + _waveWriterPortAudio = WaveData; + _waveWriterPortAudio!.CreateFileStream(fileName); + break; + } + case AudioBackend.MiniAudio: + { + if (_soundFlowEngine is null) + { + throw new InvalidOperationException("SoundFlow engine or format is not initialized for recording."); + } + + _soundFlowFileStream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None); + + _soundFlowEncoder = _soundFlowEngine.CreateEncoder(_soundFlowFileStream, EncodingFormat.Wav, SoundFlowFormat); + break; + } + case AudioBackend.NAudio: + { + _waveWriterNAudio = new WaveFileWriter(fileName, WaveFormat); + break; + } + } + } + public void CloseWaveWriter() + { + switch (PlaybackBackend) + { + case AudioBackend.PortAudio: + { + _waveWriterPortAudio!.Dispose(true); + _waveWriterPortAudio = null; + break; + } + case AudioBackend.MiniAudio: // Thank you LSXPrime for adding this in + { + _soundFlowEncoder?.Dispose(); + _soundFlowFileStream?.Dispose(); + _soundFlowEncoder = null; + _soundFlowFileStream = null; + break; + } + case AudioBackend.NAudio: + { + _waveWriterNAudio!.Dispose(); + _waveWriterNAudio = null; + break; + } + } + } + + public virtual void Dispose() + { + switch (PlaybackBackend) + { + case AudioBackend.PortAudio: + { + if (PortAudioPlayer is not null) + { + PortAudioPlayer.Stop(); + PortAudioPlayer.Dispose(); + } + break; + } + case AudioBackend.MiniAudio: + { + if (MiniAudioPlayer is not null && _playbackDevice is not null) + { + MiniAudioPlayer.Stop(); + _playbackDevice.Stop(); + _playbackDevice.Dispose(); + _soundFlowEngine?.Dispose(); + DataProvider?.Dispose(); + } + break; + } + case AudioBackend.NAudio: + { + if (_out is not null && _appVolume is not null) + { + _out.Stop(); + _out.Dispose(); + _appVolume.Dispose(); + } + break; + } + } + GC.SuppressFinalize(this); + } + + public interface IAudio + { + Span ByteBuffer { get; } + Span Int16Buffer { get; } + Span Int32Buffer { get; } + Span Int64Buffer { get; } + Span Int128Buffer { get; } + Span Float16Buffer { get; } + Span Float32Buffer { get; } + Span Float64Buffer { get; } + Span Float128Buffer { get; } + } + + [StructLayout(LayoutKind.Explicit, Pack = 2)] + public class Audio : IAudio + { + [FieldOffset(0)] + public int NumberOfBytes; + [FieldOffset(8)] + public byte[]? ByteBuffer; + [FieldOffset(8)] + public short[]? Int16Buffer; + [FieldOffset(8)] + public int[]? Int32Buffer; + [FieldOffset(8)] + public long[]? Int64Buffer; + [FieldOffset(8)] + public Int128[]? Int128Buffer; + [FieldOffset(8)] + public Half[]? Float16Buffer; + [FieldOffset(8)] + public float[]? Float32Buffer; + [FieldOffset(8)] + public double[]? Float64Buffer; + [FieldOffset(8)] + public decimal[]? Float128Buffer; + + Span IAudio.ByteBuffer => ByteBuffer!; + Span IAudio.Int16Buffer => Int16Buffer!; + Span IAudio.Int32Buffer => Int32Buffer!; + Span IAudio.Int64Buffer => Int64Buffer!; + Span IAudio.Int128Buffer => Int128Buffer!; + Span IAudio.Float16Buffer => Float16Buffer!; + Span IAudio.Float32Buffer => Float32Buffer!; + Span IAudio.Float64Buffer => Float64Buffer!; + Span IAudio.Float128Buffer => Float128Buffer!; + + public int ByteBufferCount + { + get + { + return NumberOfBytes; + } + set + { + NumberOfBytes = CheckValidityCount("ByteBufferCount", value, 1); + } + } + + public int Int16BufferCount + { + get + { + return NumberOfBytes / 2; + } + set + { + NumberOfBytes = CheckValidityCount("Int16BufferCount", value, 2); + } + } + + public int Int32BufferCount + { + get + { + return NumberOfBytes / 4; + } + set + { + NumberOfBytes = CheckValidityCount("Int32BufferCount", value, 4); + } + } + + public int Int64BufferCount + { + get + { + return NumberOfBytes / 8; + } + set + { + NumberOfBytes = CheckValidityCount("Int64BufferCount", value, 8); + } + } + + public int Int128BufferCount + { + get + { + return NumberOfBytes / 16; + } + set + { + NumberOfBytes = CheckValidityCount("Int128BufferCount", value, 16); + } + } + + public int Float16BufferCount + { + get + { + return NumberOfBytes / 2; + } + set + { + NumberOfBytes = CheckValidityCount("Float16BufferCount", value, 2); + } + } + + public int Float32BufferCount + { + get + { + return NumberOfBytes / 4; + } + set + { + NumberOfBytes = CheckValidityCount("Float32BufferCount", value, 4); + } + } + + public int Float64BufferCount + { + get + { + return NumberOfBytes / 8; + } + set + { + NumberOfBytes = CheckValidityCount("Float64BufferCount", value, 8); + } + } + + public int Float128BufferCount + { + get + { + return NumberOfBytes / 16; + } + set + { + NumberOfBytes = CheckValidityCount("Float128BufferCount", value, 16); + } + } + + public Audio(int sizeToAllocateInBytes) + { + var sizeInBytes = sizeToAllocateInBytes; + int aligned32Bits = sizeInBytes % 4; + sizeToAllocateInBytes = (aligned32Bits == 0) ? sizeInBytes : (sizeInBytes + 4 - aligned32Bits); + ByteBuffer = new byte[sizeToAllocateInBytes]; + NumberOfBytes = 0; + } + + public static implicit operator byte[](Audio waveBuffer) + { + return waveBuffer.ByteBuffer!; + } + + private int CheckValidityCount(string argName, int value, int sizeOfValue) + { + int num = value * sizeOfValue; + if (num % 4 != 0) + { + throw new ArgumentOutOfRangeException(argName, $"{argName} cannot set a count ({num}) that is not 4 bytes aligned "); + } + + if (value < 0 || value > ByteBuffer!.Length / sizeOfValue) + { + throw new ArgumentOutOfRangeException(argName, $"{argName} cannot set a count that exceeds max count of {ByteBuffer!.Length / sizeOfValue}."); + } + + return num; + } + + public void Clear() + { + Array.Clear(ByteBuffer!, 0, ByteBuffer!.Length); + } + + public void Copy(Array destinationArray) + { + Array.Copy(ByteBuffer!, destinationArray, NumberOfBytes); + } + } } diff --git a/VG Music Studio - Core/NDS/DSE/DSEChannel.cs b/VG Music Studio - Core/NDS/DSE/DSEChannel.cs index 11670ce0..65300162 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEChannel.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEChannel.cs @@ -1,12 +1,18 @@ -namespace Kermalis.VGMusicStudio.Core.NDS.DSE; +using Kermalis.VGMusicStudio.Core.Codec; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Wii; +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; internal sealed class DSEChannel { public readonly byte Index; public DSETrack? Owner; + public string? SWDType; public EnvelopeState State; - public byte RootKey; + public sbyte RootKey; public byte Key; public byte NoteVelocity; public sbyte Panpot; // Not necessary @@ -14,6 +20,8 @@ internal sealed class DSEChannel public ushort Timer; public uint NoteLength; public byte Volume; + internal int SweepCounter; + public static readonly float Root12Of2 = MathF.Pow(2, 1f / 12); private int _pos; private short _prevLeft; @@ -25,21 +33,26 @@ internal sealed class DSEChannel private byte _targetVolume; private byte _attackVolume; - private byte _attack; + private byte _attackTime; private byte _decay; private byte _sustain; private byte _hold; - private byte _decay2; + private byte _fade; private byte _release; - // PCM8, PCM16, ADPCM - private SWD.SampleBlock _sample; + // PCM8, PCM16, IMA-ADPCM, DSP-ADPCM + private SWD.SampleBlock? _sample; // PCM8, PCM16 private int _dataOffset; - // ADPCM - private ADPCMDecoder _adpcmDecoder; + // IMA-ADPCM + private IMAADPCM _adpcmDecoder; private short _adpcmLoopLastSample; private short _adpcmLoopStepIndex; + // DSP-ADPCM + private DSPADPCM _dspADPCM; + // PSG + private byte _psgDuty; + private int _psgCounter; public DSEChannel(byte i) { @@ -47,9 +60,36 @@ public DSEChannel(byte i) Index = i; } - public bool StartPCM(SWD localswd, SWD masterswd, byte voice, int key, uint noteLength) + public bool StartChannel(SWD localswd, SWD masterswd, DSETrack track, int key, uint noteLength) { - SWD.IProgramInfo? programInfo = localswd.Programs?.ProgramInfos[voice]; + Owner = track; + if (localswd == null) { SWDType = masterswd.Type; } + else { SWDType = localswd.Type; } + + SWD.IProgramInfo? programInfo = null; // Declaring Program Info Interface here, to ensure VGMS compiles + if (localswd == null) + { + // Failsafe to check if SWD.ProgramBank contains an instance, if it doesn't, it will be skipped + // This is especially important for initializing a main SWD before the local SWDs + // accompaning the SMDs with the same names are loaded in. + if (track.Voice > masterswd.Programs!.ProgramInfos!.Length) + { + throw new IndexOutOfRangeException(string.Format(Strings.ErrorDSEVoiceIndexOutOfRange, track.Voice, masterswd.Programs!.ProgramInfos!.Length)); + } + if (masterswd.Programs != null) + { + programInfo = masterswd.Programs!.ProgramInfos![track.Voice]; + } + } + else if (track.Voice > localswd.Programs!.ProgramInfos!.Length) + { + programInfo = masterswd.Programs!.ProgramInfos![track.Voice]; + } + else + { + programInfo = localswd.Programs!.ProgramInfos![track.Voice]; + } + if (programInfo is null) { return false; @@ -66,65 +106,96 @@ public bool StartPCM(SWD localswd, SWD masterswd, byte voice, int key, uint note _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) + if (_sample != null) { - _adpcmDecoder.Init(_sample.Data); + switch (SWDType) // Configures the base timer based on the specific console's sound framework and sample rate + { + case "wds ": throw new NotImplementedException("The base timer for the WDS type is not yet implemented."); // PlayStation + case "swdm": throw new NotImplementedException("The base timer for the SWDM type is not yet implemented."); // PlayStation 2 + case "swdl": BaseTimer = (ushort)(NDSUtils.ARM7_CLOCK / _sample.WavInfo!.SampleRate); break; // Nintendo DS // Time Base algorithm is the ARM7 CPU clock rate divided by SampleRate + case "swdb": BaseTimer = (ushort)(WiiUtils.Macronix_DSP_Clock / _sample.WavInfo!.SampleRate); break; // Wii // The AX Time Base algorithm is the DSP clock rate divided by SampleRate + } + if (_sample.WavInfo!.SampleFormat == SampleFormat.ADPCM) + { + _adpcmDecoder.Init(_sample.Data!); + } + if (masterswd.Type == "swdb") + { + _dspADPCM.Init(_sample.DSPADPCM.Data, _sample.DSPADPCM.Info); + } + //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 ? _sample.WavInfo.Volume : _sample.WavInfo.AttackVolume : split.AttackVolume; + _attackTime = split.AttackTime == 0 ? _sample.WavInfo.AttackTime == 0 ? _sample.WavInfo.Volume : _sample.WavInfo.AttackTime : split.AttackTime; + _decay = split.Decay == 0 ? _sample.WavInfo.Decay == 0 ? _sample.WavInfo.Volume : _sample.WavInfo.Decay : split.Decay; + _sustain = split.Sustain == 0 ? _sample.WavInfo.Sustain == 0 ? _sample.WavInfo.Volume : _sample.WavInfo.Sustain : split.Sustain; + _hold = split.Hold == 0 ? _sample.WavInfo.Hold == 0 ? _sample.WavInfo.Volume : _sample.WavInfo.Hold : split.Hold; + _fade = split.Fade == 0 ? _sample.WavInfo.Fade == 0 ? _sample.WavInfo.Volume : _sample.WavInfo.Fade : split.Fade; + _release = split.Release == 0 ? _sample.WavInfo.Release == 0 ? _sample.WavInfo.Volume : _sample.WavInfo.Release : split.Release; + + DetermineEnvelopeStartingPoint(); + _pos = 0; + _prevLeft = _prevRight = 0; + NoteLength = noteLength; + return true; } - //attackVolume = sample.WavInfo.AttackVolume == 0 ? split.AttackVolume : sample.WavInfo.AttackVolume; - //attack = sample.WavInfo.Attack == 0 ? split.Attack : sample.WavInfo.Attack; - //decay = sample.WavInfo.Decay == 0 ? split.Decay : sample.WavInfo.Decay; - //sustain = sample.WavInfo.Sustain == 0 ? split.Sustain : sample.WavInfo.Sustain; - //hold = sample.WavInfo.Hold == 0 ? split.Hold : sample.WavInfo.Hold; - //decay2 = sample.WavInfo.Decay2 == 0 ? split.Decay2 : sample.WavInfo.Decay2; - //release = sample.WavInfo.Release == 0 ? split.Release : sample.WavInfo.Release; - //attackVolume = split.AttackVolume == 0 ? sample.WavInfo.AttackVolume : split.AttackVolume; - //attack = split.Attack == 0 ? sample.WavInfo.Attack : split.Attack; - //decay = split.Decay == 0 ? sample.WavInfo.Decay : split.Decay; - //sustain = split.Sustain == 0 ? sample.WavInfo.Sustain : split.Sustain; - //hold = split.Hold == 0 ? sample.WavInfo.Hold : split.Hold; - //decay2 = split.Decay2 == 0 ? sample.WavInfo.Decay2 : split.Decay2; - //release = split.Release == 0 ? sample.WavInfo.Release : split.Release; - _attackVolume = split.AttackVolume == 0 ? _sample.WavInfo.AttackVolume == 0 ? (byte)0x7F : _sample.WavInfo.AttackVolume : split.AttackVolume; - _attack = split.Attack == 0 ? _sample.WavInfo.Attack == 0 ? (byte)0x7F : _sample.WavInfo.Attack : split.Attack; - _decay = split.Decay == 0 ? _sample.WavInfo.Decay == 0 ? (byte)0x7F : _sample.WavInfo.Decay : split.Decay; - _sustain = split.Sustain == 0 ? _sample.WavInfo.Sustain == 0 ? (byte)0x7F : _sample.WavInfo.Sustain : split.Sustain; - _hold = split.Hold == 0 ? _sample.WavInfo.Hold == 0 ? (byte)0x7F : _sample.WavInfo.Hold : split.Hold; - _decay2 = split.Decay2 == 0 ? _sample.WavInfo.Decay2 == 0 ? (byte)0x7F : _sample.WavInfo.Decay2 : split.Decay2; - _release = split.Release == 0 ? _sample.WavInfo.Release == 0 ? (byte)0x7F : _sample.WavInfo.Release : split.Release; - DetermineEnvelopeStartingPoint(); - _pos = 0; - _prevLeft = _prevRight = 0; - NoteLength = noteLength; - return true; } return false; } public void Stop() { - Owner?.Channels.Remove(this); + if (Owner is not null) + { + Owner.Channels.Remove(this); + } Owner = null; Volume = 0; } - private bool CMDB1___sub_2074CA0() + public int SweepMain() + { + if (Owner!.SweepPitch == 0 || SweepCounter >= Owner.SweepRate) + { + return 0; + } + + int sweep = (int)(Math.BigMul(Owner.SweepPitch, Owner.SweepRate - SweepCounter) / Owner.SweepRate); + SweepCounter++; + return sweep; + } + + // CMDB1___sub_2074CA0 + private bool IsValidEnvelope() { bool b = true; - bool ge = _sample.WavInfo.EnvMult >= 0x7F; - bool ee = _sample.WavInfo.EnvMult == 0x7F; - if (_sample.WavInfo.EnvMult > 0x7F) + bool ge = _sample!.WavInfo!.EnvMult >= _sample.WavInfo.Volume; + bool ee = _sample.WavInfo.EnvMult == _sample.WavInfo.Volume; + if (_sample.WavInfo.EnvMult > _sample.WavInfo.Volume) { - ge = _attackVolume >= 0x7F; - ee = _attackVolume == 0x7F; + ge = _attackVolume >= _sample.WavInfo.Volume; + ee = _attackVolume == _sample.WavInfo.Volume; } if (!ee & ge - && _attack > 0x7F - && _decay > 0x7F - && _sustain > 0x7F - && _hold > 0x7F - && _decay2 > 0x7F - && _release > 0x7F) + && _attackTime > _sample.WavInfo.Volume + && _decay > _sample.WavInfo.Volume + && _sustain > _sample.WavInfo.Volume + && _hold > _sample.WavInfo.Volume + && _fade > _sample.WavInfo.Volume + && _release > _sample.WavInfo.Volume) { b = false; } @@ -132,54 +203,61 @@ private bool CMDB1___sub_2074CA0() } private void DetermineEnvelopeStartingPoint() { - State = EnvelopeState.Two; // This isn't actually placed in this func - bool atLeastOneThingIsValid = CMDB1___sub_2074CA0(); // Neither is this + State = EnvelopeState.Attack; // This isn't actually placed in this func + bool atLeastOneThingIsValid = IsValidEnvelope(); // Neither is this if (atLeastOneThingIsValid) { - if (_attack != 0) + if (_fade != 0) + { + UpdateEnvelopePlan(0, _fade); + State = EnvelopeState.Attack; + } + if (_attackTime != 0) { _velocity = _attackVolume << 23; State = EnvelopeState.Hold; - UpdateEnvelopePlan(0x7F, _attack); + UpdateEnvelopePlan(_sample!.WavInfo!.Volume, _attackTime); } else { - _velocity = 0x7F << 23; + _velocity = _sample!.WavInfo!.Volume << 23; if (_hold != 0) { - UpdateEnvelopePlan(0x7F, _hold); + UpdateEnvelopePlan(_sample.WavInfo.Volume, _hold); State = EnvelopeState.Decay; } else if (_decay != 0) { UpdateEnvelopePlan(_sustain, _decay); - State = EnvelopeState.Decay2; + State = EnvelopeState.Fade; } else { UpdateEnvelopePlan(0, _release); - State = EnvelopeState.Six; + State = EnvelopeState.Sustain; } } // Unk1E = 1 } - else if (State != EnvelopeState.One) // What should it be? + else if (State != EnvelopeState.PlayNote) // Need to Initialize before it starts the PlayNote state { - State = EnvelopeState.Zero; - _velocity = 0x7F << 23; + State = EnvelopeState.Initialize; + _velocity = _sample!.WavInfo!.Volume << 23; } } - public void SetEnvelopePhase7_2074ED8() + + // SetEnvelopePhase7_2074ED8 + public void SetEnvelopeRelease() { - if (State != EnvelopeState.Zero) + if (State != EnvelopeState.Initialize) { UpdateEnvelopePlan(0, _release); - State = EnvelopeState.Seven; + State = EnvelopeState.End; } } public int StepEnvelope() { - if (State > EnvelopeState.Two) + if (State > EnvelopeState.Attack) { if (_envelopeTimeLeft != 0) { @@ -201,18 +279,18 @@ public int StepEnvelope() { default: return _velocity >> 23; // case 8 case EnvelopeState.Hold: - { - if (_hold == 0) { - goto LABEL_6; - } - else - { - UpdateEnvelopePlan(0x7F, _hold); - State = EnvelopeState.Decay; + if (_hold == 0) + { + goto LABEL_6; + } + else + { + UpdateEnvelopePlan(_sample!.WavInfo!.Volume, _hold); + State = EnvelopeState.Decay; + } + break; } - break; - } case EnvelopeState.Decay: LABEL_6: { @@ -224,38 +302,38 @@ public int StepEnvelope() else { UpdateEnvelopePlan(_sustain, _decay); - State = EnvelopeState.Decay2; + State = EnvelopeState.Fade; } break; } - case EnvelopeState.Decay2: + case EnvelopeState.Fade: LABEL_9: { - if (_decay2 == 0) + if (_fade == 0) { goto LABEL_11; } else { - UpdateEnvelopePlan(0, _decay2); - State = EnvelopeState.Six; + UpdateEnvelopePlan(0, _fade); + State = EnvelopeState.Sustain; } break; } - case EnvelopeState.Six: + case EnvelopeState.Sustain: LABEL_11: { UpdateEnvelopePlan(0, 0); - State = EnvelopeState.Two; + State = EnvelopeState.Attack; + break; + } + case EnvelopeState.End: + { + State = EnvelopeState.Release; + _velocity = 0; + _envelopeTimeLeft = 0; break; } - case EnvelopeState.Seven: - { - State = EnvelopeState.Eight; - _velocity = 0; - _envelopeTimeLeft = 0; - break; - } } } } @@ -263,7 +341,7 @@ public int StepEnvelope() } private void UpdateEnvelopePlan(byte targetVolume, int envelopeParam) { - if (envelopeParam == 0x7F) + if (envelopeParam == _sample!.WavInfo!.Volume) { _volumeIncrement = 0; _envelopeTimeLeft = int.MaxValue; @@ -271,9 +349,9 @@ private void UpdateEnvelopePlan(byte targetVolume, int envelopeParam) else { _targetVolume = targetVolume; - _envelopeTimeLeft = _sample.WavInfo.EnvMult == 0 + _envelopeTimeLeft = (int)(_sample!.WavInfo!.EnvMult == 0 ? DSEUtils.Duration32[envelopeParam] * 1_000 / 10_000 - : DSEUtils.Duration16[envelopeParam] * _sample.WavInfo.EnvMult * 1_000 / 10_000; + : DSEUtils.Duration16[envelopeParam] * (_sample.WavInfo.EnvMult * .2)); _volumeIncrement = _envelopeTimeLeft == 0 ? 0 : ((targetVolume << 23) - _velocity) / _envelopeTimeLeft; } } @@ -292,80 +370,121 @@ public void Process(out short left, out short right) // prevLeft and prevRight are stored because numSamples can be 0. for (int i = 0; i < numSamples; i++) { - short samp; - switch (_sample.WavInfo.SampleFormat) + switch (SWDType) { - case SampleFormat.PCM8: - { - // If hit end - if (_dataOffset >= _sample.Data.Length) + case "wds ": + case "swdm": + case "swdl": { - if (_sample.WavInfo.Loop) - { - _dataOffset = (int)(_sample.WavInfo.LoopStart * 4); - } - else + short samp; + switch (_sample!.WavInfo!.SampleFormat) { - left = right = _prevLeft = _prevRight = 0; - Stop(); - return; + case SampleFormat.PCM8: + { + // If hit end + if (_dataOffset >= _sample.Data!.Length) + { + if (_sample.WavInfo.Loop) + { + _dataOffset = (int)(_sample.WavInfo.LoopStart * 4); // DS counts LoopStart 32-bits (4 bytes) at a time, so LoopStart needs to be bigger + } + 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; + } + case SampleFormat.PSG: + { + samp = _psgCounter <= _psgDuty ? short.MinValue : short.MaxValue; + _psgCounter++; + if (_psgCounter >= 8) + { + _psgCounter = 0; + } + break; + } + default: samp = 0; break; } + samp = (short)(samp * Volume / _sample.WavInfo.Volume); + _prevLeft = (short)(samp * (-Panpot + 0x40) / 0x80); + _prevRight = (short)(samp * (Panpot + 0x40) / 0x80); + break; } - 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) + case "swdb": { - if (_sample.WavInfo.Loop) + // If hit end + if (_dataOffset >= DSPADPCM.NibblesToSamples((int)_sample!.WavInfo!.LoopEnd)) // Wii DSE always reads the LoopEnd address (in nibbles) when looping is enabled for a SWD entry, instead of reading until the end of the sample data { - _adpcmDecoder.DataOffset = (int)(_sample.WavInfo.LoopStart * 4); - _adpcmDecoder.StepIndex = _adpcmLoopStepIndex; - _adpcmDecoder.LastSample = _adpcmLoopLastSample; - _adpcmDecoder.OnSecondNibble = false; - } - else - { - left = right = _prevLeft = _prevRight = 0; - Stop(); - return; + if (_sample.WavInfo!.Loop) + { + _dataOffset = DSPADPCM.NibblesToSamples((int)_sample.WavInfo.LoopStart); // Wii values for LoopStart offset are counted in nibbles (4-bits or half a byte) at a time, but because DataOutput is using a 16-bit array, LoopStart value needs to be converted to a 16-bit PCM sample offset + } + else + { + left = right = _prevLeft = _prevRight = 0; + Stop(); + return; + } } + short samp = _sample.DSPADPCM!.DataOutput![_dataOffset++]; // Since DataOutput is already a 16-bit array, only one array entry is needed per loop, no bitshifting needed either + samp = (short)(samp * Volume / _sample.WavInfo.Volume); + _prevLeft = (short)(samp * (-Panpot + 0x40) / 0x80); + _prevRight = (short)(samp * (Panpot + 0x40) / 0x80); + break; } - samp = _adpcmDecoder.GetSample(); - break; - } - default: samp = 0; break; } - samp = (short)(samp * Volume / 0x7F); - _prevLeft = (short)(samp * (-Panpot + 0x40) / 0x80); - _prevRight = (short)(samp * (Panpot + 0x40) / 0x80); } left = _prevLeft; right = _prevRight; diff --git a/VG Music Studio - Core/NDS/DSE/DSECommands.cs b/VG Music Studio - Core/NDS/DSE/DSECommands.cs index 76e8e5bd..08bab88c 100644 --- a/VG Music Studio - Core/NDS/DSE/DSECommands.cs +++ b/VG Music Studio - Core/NDS/DSE/DSECommands.cs @@ -77,6 +77,14 @@ internal sealed class PitchBendCommand : ICommand public ushort Bend { get; set; } } +internal sealed class SetPitchBendRangeCommand : ICommand +{ + public Color Color => Color.MediumPurple; + public string Label => "Set Pitch Bend Range"; + public string Arguments => $"{PitchBendRange}"; + + public byte PitchBendRange { get; set; } +} internal sealed class RestCommand : ICommand { public Color Color => Color.PaleVioletRed; @@ -85,6 +93,14 @@ internal sealed class RestCommand : ICommand public uint Rest { get; set; } } +internal sealed class CheckIntervalCommand : ICommand +{ + public Color Color => Color.DarkViolet; + public string Label => "Check Interval"; + public string Arguments => Interval.ToString(); + + public uint Interval { get; set; } +} internal sealed class SkipBytesCommand : ICommand { public Color Color => Color.MediumVioletRed; @@ -103,6 +119,390 @@ internal sealed class TempoCommand : ICommand public byte Command { get; set; } public byte Tempo { get; set; } } +internal sealed class DalSegnoAlCodaCommand : ICommand +{ + public Color Color => Color.CornflowerBlue; + public string Label => $"Dal Segno Al Coda: Repeat {Repeats} Time(s)"; + public string Arguments => Repeats.ToString(); + + public byte Command { get; set; } + public byte Repeats { get; set; } +} +internal sealed class DalSegnoAlFineCommand : ICommand +{ + public Color Color => Color.CornflowerBlue; + public string Label => $"Dal Segno Al Fine"; + public string Arguments => string.Empty; + + public byte Command { get; set; } +} +internal sealed class ToCodaCommand : ICommand +{ + public Color Color => Color.Aquamarine; + public string Label => $"To Coda"; + public string Arguments => string.Empty; + + public byte Command { get; set; } +} +internal sealed class SetSWDAndBankCommand : ICommand +{ + public Color Color => Color.BlanchedAlmond; + public string Label => $"Set SWD and Bank"; + public string Arguments => $"{WaveID}, {BankID}"; + + public byte WaveID { get; set; } + public byte BankID { get; set; } +} +internal sealed class SetBankHiCommand : ICommand +{ + public Color Color => Color.Beige; + public string Label => $"Set Bank Hi"; + public string Arguments => $"{BankHiID}"; + + public byte BankHiID { get; set; } +} +internal sealed class SetBankLoCommand : ICommand +{ + public Color Color => Color.Beige; + public string Label => $"Set Bank Lo"; + public string Arguments => $"{BankLoID}"; + + public byte BankLoID { get; set; } +} +internal sealed class SweepSongVolumeCommand : ICommand +{ + public Color Color => Color.DarkGreen; + public string Label => $"Sweep Song Volume"; + public string Arguments => $"{SweepRate}, {SweepPitch}"; + + public ushort SweepRate { get; set; } + public byte SweepPitch { get; set; } +} +internal sealed class DisableEnvelopeCommand : ICommand +{ + public Color Color => Color.Aquamarine; + public string Label => $"Disable Envelope"; + public string Arguments => string.Empty; + + public byte Command { get; set; } +} +internal sealed class SetEnvelopeAttackVolumeCommand : ICommand +{ + public Color Color => Color.Aquamarine; + public string Label => $"Set Envelope: Attack Volume"; + public string Arguments => $"{AttackVolume}"; + + public byte AttackVolume { get; set; } +} +internal sealed class SetEnvelopeAttackTimeCommand : ICommand +{ + public Color Color => Color.Aquamarine; + public string Label => $"Set Envelope: Attack Time"; + public string Arguments => $"{AttackTime}"; + + public byte AttackTime { get; set; } +} +internal sealed class SetEnvelopeHoldCommand : ICommand +{ + public Color Color => Color.Aquamarine; + public string Label => $"Set Envelope: Hold"; + public string Arguments => $"{Hold}"; + + public byte Hold { get; set; } +} +internal sealed class SetEnvelopeDecaySustainCommand : ICommand +{ + public Color Color => Color.Aquamarine; + public string Label => $"Set Envelopes: Decay and Sustain"; + public string Arguments => $"{Decay}, {Sustain}"; + + public byte Decay { get; set; } + public byte Sustain { get; set; } +} +internal sealed class SetEnvelopeFadeCommand : ICommand +{ + public Color Color => Color.Aquamarine; + public string Label => $"Set Envelope: Fade"; + public string Arguments => $"{Fade}"; + + public byte Fade { get; set; } +} +internal sealed class SetEnvelopeReleaseCommand : ICommand +{ + public Color Color => Color.Aquamarine; + public string Label => $"Set Envelope: Release"; + public string Arguments => $"{Release}"; + + public byte Release { get; set; } +} +internal sealed class SetNoteVolumeCommand : ICommand +{ + public Color Color => Color.Purple; + public string Label => $"Set Note Volume"; + public string Arguments => $"{NoteVolume}"; + + public byte NoteVolume { get; set; } +} +internal sealed class SetChannelPanpotCommand : ICommand +{ + public Color Color => Color.Bisque; + public string Label => $"Set Channel Panpot"; + public string Arguments => $"{ChannelPanpot}"; + + public byte ChannelPanpot { get; set; } +} +internal sealed class FlagBeginCommand : ICommand +{ + public Color Color => Color.DarkGoldenrod; + public string Label => $"Flag Begin with value {FlagValue}"; + public string Arguments => $"{FlagValue}"; + + public byte FlagValue { get; set; } +} +internal sealed class FlagEndCommand : ICommand +{ + public Color Color => Color.DarkGoldenrod; + public string Label => $"Flag End"; + public string Arguments => string.Empty; + + public byte Command { get; set; } +} +internal sealed class SetChannelVolumeCommand : ICommand +{ + public Color Color => Color.Tomato; + public string Label => $"Set Channel Volume"; + public string Arguments => $"{ChannelVolume}"; + + public byte ChannelVolume { get; set; } +} +internal sealed class SetFineTuneCommand : ICommand +{ + public Color Color => Color.Coral; + public string Label => $"Set Fine Tune"; + public string Arguments => $"{FineTune}"; + + public byte FineTune { get; set; } +} +internal sealed class AddToFineTuneCommand : ICommand +{ + public Color Color => Color.Coral; + public string Label => $"Add To Fine Tune"; + public string Arguments => $"{FineTuneAddValue}"; + + public byte FineTuneAddValue { get; set; } +} +internal sealed class SetCoarseTuneCommand : ICommand +{ + public Color Color => Color.DarkBlue; + public string Label => $"Set Coarse Tune"; + public string Arguments => $"{CoarseTune}"; + + public byte CoarseTune { get; set; } +} +internal sealed class AddToCoarseTuneCommand : ICommand +{ + public Color Color => Color.DarkBlue; + public string Label => $"Add To Coarse Tune"; + public string Arguments => $"{CoarseTuneAddValue}"; + + public ushort CoarseTuneAddValue { get; set; } +} +internal sealed class SweepTuneCommand : ICommand +{ + public Color Color => Color.Crimson; + public string Label => $"Sweep Tune"; + public string Arguments => $"{SweepTuneRate}, {SweepTuneTarget}"; + + public ushort SweepTuneRate { get; set; } + public byte SweepTuneTarget { get; set; } +} +internal sealed class SetRandomNoteRangeCommand : ICommand +{ + public Color Color => Color.Beige; + public string Label => $"Set Random Note Range: {RandomNoteRangeMin}-{RandomNoteRangeMax}"; + public string Arguments => $"{RandomNoteRangeMin}, {RandomNoteRangeMax}"; + + public byte RandomNoteRangeMin { get; set; } + public byte RandomNoteRangeMax { get; set; } +} +internal sealed class SetDetuneRangeCommand : ICommand +{ + public Color Color => Color.Honeydew; + public string Label => $"Set Detune Range"; + public string Arguments => $"{DetuneRange}"; + + public ushort DetuneRange { get; set; } +} +internal sealed class SetParamCommand : ICommand +{ + public Color Color => Color.DimGray; + public string Label => $"Set Parameter And Value"; + public string Arguments => $"{ParamValue}, {ParamTarget}"; + + public byte ParamValue { get; set; } + public byte ParamTarget { get; set; } +} +internal sealed class ReplaceLFO1AsPitchCommand : ICommand +{ + public Color Color => Color.YellowGreen; + public string Label => $"Replace LFO1 As Pitch"; + public string Arguments => $"{Rate}, {Depth}, {WaveformType}"; + + public ushort Rate { get; set; } + public ushort Depth { get; set; } + public WaveformType WaveformType { get; set; } +} +internal sealed class SetLFO1DelayFade : ICommand +{ + public Color Color => Color.YellowGreen; + public string Label => $"Set LFO1 To Delay Fade"; + public string Arguments => $"{Delay}, {FadeTime}"; + + public ushort Delay { get; set; } + public ushort FadeTime { get; set; } +} +internal sealed class SetLFO1ToPitchEnabledCommand : ICommand +{ + public Color Color => Color.YellowGreen; + public string Label => $"Set LFO1 To Pitch Enabled"; + public string Arguments => $"{PitchEnabled}"; + + public bool PitchEnabled { get; set; } +} +internal sealed class ReplaceLFO2AsVolumeCommand : ICommand +{ + public Color Color => Color.YellowGreen; + public string Label => $"Replace LFO1 As Volume"; + public string Arguments => $"{Rate}, {Depth}, {WaveformType}"; + + public ushort Rate { get; set; } + public ushort Depth { get; set; } + public WaveformType WaveformType { get; set; } +} +internal sealed class SetLFO2DelayFade : ICommand +{ + public Color Color => Color.YellowGreen; + public string Label => $"Set LFO2 To Delay Fade"; + public string Arguments => $"{Delay}, {FadeTime}"; + + public ushort Delay { get; set; } + public ushort FadeTime { get; set; } +} +internal sealed class SetLFO2ToVolumeEnabledCommand : ICommand +{ + public Color Color => Color.YellowGreen; + public string Label => $"Set LFO2 To Volume Enabled"; + public string Arguments => $"{VolumeEnabled}"; + + public bool VolumeEnabled { get; set; } +} +internal sealed class ReplaceLFO3AsPanpotCommand : ICommand +{ + public Color Color => Color.YellowGreen; + public string Label => $"Replace LFO3 As Panpot"; + public string Arguments => $"{Rate}, {Depth}, {WaveformType}"; + + public ushort Rate { get; set; } + public ushort Depth { get; set; } + public WaveformType WaveformType { get; set; } +} +internal sealed class SetLFO3DelayFade : ICommand +{ + public Color Color => Color.YellowGreen; + public string Label => $"Set LFO3 To Delay Fade"; + public string Arguments => $"{Delay}, {FadeTime}"; + + public ushort Delay { get; set; } + public ushort FadeTime { get; set; } +} +internal sealed class SetLFO3ToPanpotEnabledCommand : ICommand +{ + public Color Color => Color.YellowGreen; + public string Label => $"Set LFO2 To Volume Enabled"; + public string Arguments => $"{PanpotEnabled}"; + + public bool PanpotEnabled { get; set; } +} +internal sealed class ReplaceLFOCommand : ICommand +{ + public Color Color => Color.DarkSalmon; + public string Label => $"Replace LFO"; + public string Arguments => $"{Rate}, {Depth}, {WaveformType}"; + + public ushort Rate { get; set; } + public ushort Depth { get; set; } + public WaveformType WaveformType { get; set; } +} +internal sealed class SetLFODelayFadeCommand : ICommand +{ + public Color Color => Color.DarkSalmon; + public string Label => $"Set LFO To Delay Fade"; + public string Arguments => $"{Delay}, {FadeTime}"; + + public ushort Delay { get; set; } + public ushort FadeTime { get; set; } +} +internal sealed class SetLFOParamCommand : ICommand +{ + public Color Color => Color.AliceBlue; + public string Label => $"Set LFO Parameters"; + public string Arguments => $"{ParamType}, {WaveformType}"; + + public ParameterType ParamType { get; set; } + public WaveformType WaveformType { get; set; } +} +internal sealed class SetLFORouteCommand : ICommand +{ + public Color Color => Color.Lavender; + public string Label => $"Set LFO Route"; + public string Arguments => $"{Target}, {Enabled}, {TargetID}"; + + public byte Target { get; set; } + public bool Enabled { get; set; } + public byte TargetID { get; set; } +} +internal sealed class AddToTrackVolCommand : ICommand +{ + public Color Color => Color.Chocolate; + public string Label => $"Add To Track Volume"; + public string Arguments => $"{TrackVolAdd}"; + + public byte TrackVolAdd { get; set; } +} +internal sealed class SweepTrackVolCommand : ICommand +{ + public Color Color => Color.Aquamarine; + public string Label => $"Sweep Track Volume"; + public string Arguments => $"{SweepRate}, {SweepVolume}"; + + public ushort SweepRate { get; set; } + public byte SweepVolume { get; set; } +} +internal sealed class AddToPanpotCommand : ICommand +{ + public Color Color => Color.BurlyWood; + public string Label => $"Add To Panpot"; + public string Arguments => $"{PanpotAdd}"; + + public sbyte PanpotAdd { get; set; } +} +internal sealed class SweepPanpotCommand : ICommand +{ + public Color Color => Color.ForestGreen; + public string Label => $"Sweep Panpot"; + public string Arguments => $"{SweepRate}, {PanpotTarget}"; + + public ushort SweepRate { get; set; } + public byte PanpotTarget { get; set; } +} +internal sealed class ScenarioSyncCommand : ICommand +{ + public Color Color => Color.Pink; + public string Label => $"Scenario Sync #{Checkpoint}"; + public string Arguments => $"{Checkpoint}"; + + public byte Checkpoint { get; set; } +} internal sealed class UnknownCommand : ICommand { public Color Color => Color.MediumVioletRed; diff --git a/VG Music Studio - Core/NDS/DSE/DSEConfig.cs b/VG Music Studio - Core/NDS/DSE/DSEConfig.cs index 6a68eed5..9019be5c 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEConfig.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEConfig.cs @@ -1,5 +1,6 @@ using Kermalis.EndianBinaryIO; using Kermalis.VGMusicStudio.Core.Properties; +using System; using System.Collections.Generic; using System.IO; @@ -7,32 +8,59 @@ namespace Kermalis.VGMusicStudio.Core.NDS.DSE; public sealed class DSEConfig : Config { - public readonly string BGMPath; - public readonly string[] BGMFiles; + public readonly string MainSWDFile; + public readonly string SMDPath; + public readonly string[] SMDFiles; + internal SMD.Header? Header; - internal DSEConfig(string bgmPath) + internal DSEConfig(string mainSWDFile, string smdPath, bool useNewUI) { - BGMPath = bgmPath; - BGMFiles = Directory.GetFiles(bgmPath, "bgm*.smd", SearchOption.TopDirectoryOnly); - if (BGMFiles.Length == 0) + MainSWDFile = mainSWDFile; + SMDPath = smdPath; + SMDFiles = Directory.GetFiles(smdPath, "*.smd", SearchOption.TopDirectoryOnly); + Array.Sort(SMDFiles); + if (SMDFiles.Length == 0) { - throw new DSENoSequencesException(bgmPath); + throw new DSENoSequencesException(smdPath); } - // TODO: Big endian files - var songs = new List(BGMFiles.Length); - for (int i = 0; i < BGMFiles.Length; i++) + // TODO: Big endian for SMDS (PlayStation 1), and mixed endian for SMDM (PlayStation 2) + var songs = new List(SMDFiles.Length); + for (int i = 0; i < SMDFiles.Length; i++) { - using (FileStream stream = File.OpenRead(BGMFiles[i])) + using FileStream stream = File.OpenRead(SMDFiles[i]); + // This will read the SMD header to determine if the majority of the file is little endian or big endian + var r = new EndianBinaryReader(stream, ascii: true); + Header = new SMD.Header(r); + if (Header.Type == "smdl") { - var r = new EndianBinaryReader(stream, ascii: true); - SMD.Header header = r.ReadObject(); - char[] chars = header.Label.ToCharArray(); + char[] chars = Header.Label.ToCharArray(); EndianBinaryPrimitives.TrimNullTerminators(ref chars); - songs.Add(new Song(i, $"{Path.GetFileNameWithoutExtension(BGMFiles[i])} - {new string(chars)}")); + if (useNewUI) + { + songs.Add(new Song(i, $"{new string(chars)}")); + } + else + { + songs.Add(new Song(i, $"{Path.GetFileNameWithoutExtension(SMDFiles[i])} - {new string(chars)}")); + } + } + else if (Header.Type == "smdb") + { + r.Endianness = Endianness.BigEndian; + char[] chars = Header.Label.ToCharArray(); + EndianBinaryPrimitives.TrimNullTerminators(ref chars); + if (useNewUI) + { + songs.Add(new Song(i, $"{new string(chars)}")); + } + else + { + songs.Add(new Song(i, $"{Path.GetFileNameWithoutExtension(SMDFiles[i])} - {new string(chars)}")); + } } } - Playlists.Add(new Playlist(Strings.PlaylistMusic, songs)); + InternalSongNames.Add(new InternalSongName(Strings.InternalSongName, songs)); } public override string GetGameName() @@ -41,8 +69,14 @@ public override string GetGameName() } public override string GetSongName(int index) { - return index < 0 || index >= BGMFiles.Length + return index < 0 || index >= SMDFiles.Length + ? index.ToString() + : SMDFiles[index].Split("/")[^1].Remove(SMDFiles[index].Split("/")[^1].LastIndexOf('.')); + } + public string GetSongPath(int index) + { + return index < 0 || index >= SMDFiles.Length ? index.ToString() - : '\"' + BGMFiles[index] + '\"'; + : '\"' + SMDFiles[index] + '\"'; } } diff --git a/VG Music Studio - Core/NDS/DSE/DSEEngine.cs b/VG Music Studio - Core/NDS/DSE/DSEEngine.cs index a7a933ed..30d497cc 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEEngine.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEEngine.cs @@ -1,4 +1,6 @@ -namespace Kermalis.VGMusicStudio.Core.NDS.DSE; +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; public sealed class DSEEngine : Engine { @@ -8,9 +10,11 @@ public sealed class DSEEngine : Engine public override DSEMixer Mixer { get; } public override DSEPlayer Player { get; } - public DSEEngine(string bgmPath) + public override bool IsFileSystemFormat { get; } = true; + + public DSEEngine(string mainSWDFile, string smdPath, bool useNewUI = false) { - Config = new DSEConfig(bgmPath); + Config = new DSEConfig(mainSWDFile, smdPath, useNewUI); Mixer = new DSEMixer(); Player = new DSEPlayer(Config, Mixer); @@ -18,6 +22,12 @@ public DSEEngine(string bgmPath) Instance = this; } + public override void Reload() + { + var config = Config; + Dispose(); + _ = new DSEEngine(config.MainSWDFile, config.SMDPath, true); + } public override void Dispose() { base.Dispose(); diff --git a/VG Music Studio - Core/NDS/DSE/DSEEnums.cs b/VG Music Studio - Core/NDS/DSE/DSEEnums.cs index 911c5f0e..aac9338b 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEEnums.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEEnums.cs @@ -2,20 +2,66 @@ internal enum EnvelopeState : byte { - Zero = 0, - One = 1, - Two = 2, + Initialize = 0, + PlayNote = 1, + Attack = 2, Hold = 3, Decay = 4, - Decay2 = 5, - Six = 6, - Seven = 7, - Eight = 8, + Fade = 5, + Sustain = 6, + End = 7, + Release = 8, +} + +internal enum ModulationType : byte +{ + None = 0, + Pitch = 1, + Volume = 2, + Panpot = 3, + FineTune = 4, + CoarseTune = 5 +} + +internal enum WaveformType : byte +{ + None = 0, + Square = 1, + Triangle = 2, + Sine = 3, + Pulse = 4, + Sawtooth = 5, + Noise = 6, + Random = 7 +} + +internal enum ParameterType : byte +{ + LFOTarget = 1, + Enabled = 2, + Route = 3, + SelectWaveform = 4, + LFORate = 5, + LFODepth = 6, + LFODelay = 7, + LFODelayFine = 8, + LFODelayCoarse = 9, + LFOFade = 10 +} + +internal enum LFOType : byte +{ + Disconnected = 0, + Pitch = 1, + Volume = 2, + Panpot = 3, + Filter = 4 } internal enum SampleFormat : ushort { - PCM8 = 0x000, - PCM16 = 0x100, - ADPCM = 0x200, + PCM8 = 0, + PCM16 = 1, + ADPCM = 2, + PSG = 3 } diff --git a/VG Music Studio - Core/NDS/DSE/DSEExceptions.cs b/VG Music Studio - Core/NDS/DSE/DSEExceptions.cs index 82c22e9c..b7a46413 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEExceptions.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEExceptions.cs @@ -4,11 +4,11 @@ namespace Kermalis.VGMusicStudio.Core.NDS.DSE; public sealed class DSENoSequencesException : Exception { - public string BGMPath { get; } + public string SMDPath { get; } internal DSENoSequencesException(string bgmPath) { - BGMPath = bgmPath; + SMDPath = bgmPath; } } @@ -22,6 +22,18 @@ internal DSEInvalidHeaderVersionException(ushort version) } } +public sealed class DSEArrayIndexAndHeaderIDMismatchException : Exception +{ + public int ArrayIndex { get; } + public ushort HeaderID { get; } + + internal DSEArrayIndexAndHeaderIDMismatchException(int arrayIndex, ushort headerID) + { + ArrayIndex = arrayIndex; + HeaderID = headerID; + } +} + public sealed class DSEInvalidNoteException : Exception { public byte TrackIndex { get; } diff --git a/VG Music Studio - Core/NDS/DSE/DSELoadedSong.cs b/VG Music Studio - Core/NDS/DSE/DSELoadedSong.cs old mode 100644 new mode 100755 index 2cee1061..5f0445e2 --- a/VG Music Studio - Core/NDS/DSE/DSELoadedSong.cs +++ b/VG Music Studio - Core/NDS/DSE/DSELoadedSong.cs @@ -12,51 +12,45 @@ internal sealed partial class DSELoadedSong : ILoadedSong public int LongestTrack; private readonly DSEPlayer _player; - private readonly SWD LocalSWD; + private readonly SWD? LocalSWD; private readonly byte[] SMDFile; + public SMD.SongChunk SongChunk; + public SMD.Header Header; public readonly DSETrack[] Tracks; + public SoundBank Bank { get; } public DSELoadedSong(DSEPlayer player, string bgm) { _player = player; + //StringComparison comparison = StringComparison.CurrentCultureIgnoreCase; + + // Check if a local SWD is accompaning a SMD + if (new FileInfo(Path.ChangeExtension(bgm, "swd")).Exists) + { + LocalSWD = new SWD(Path.ChangeExtension(bgm, "swd")); // If it exists, this will be loaded as the local SWD + } - LocalSWD = new SWD(Path.ChangeExtension(bgm, "swd")); SMDFile = File.ReadAllBytes(bgm); - using (var stream = new MemoryStream(SMDFile)) + using var stream = new MemoryStream(SMDFile); + var r = new EndianBinaryReader(stream, ascii: true); + Header = new SMD.Header(r); + if (Header.Version != 0x415) { throw new DSEInvalidHeaderVersionException(Header.Version); } + SongChunk = new SMD.SongChunk(r); + + Tracks = new DSETrack[SongChunk.NumTracks]; + Events = new List[SongChunk.NumTracks]; + for (byte trackIndex = 0; trackIndex < SongChunk.NumTracks; trackIndex++) { - var r = new EndianBinaryReader(stream, ascii: true); - SMD.Header header = r.ReadObject(); - SMD.ISongChunk songChunk; - switch (header.Version) - { - case 0x402: - { - songChunk = r.ReadObject(); - break; - } - case 0x415: - { - songChunk = r.ReadObject(); - break; - } - default: throw new DSEInvalidHeaderVersionException(header.Version); - } - - Tracks = new DSETrack[songChunk.NumTracks]; - Events = new List[songChunk.NumTracks]; - for (byte trackIndex = 0; trackIndex < songChunk.NumTracks; trackIndex++) - { - long chunkStart = r.Stream.Position; - r.Stream.Position += 0x14; // Skip header - Tracks[trackIndex] = new DSETrack(trackIndex, (int)r.Stream.Position); - - AddTrackEvents(trackIndex, r); - - r.Stream.Position = chunkStart + 0xC; - uint chunkLength = r.ReadUInt32(); - r.Stream.Position += chunkLength; - r.Stream.Align(16); - } + 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 index b37dde91..6c8cf1a0 100644 --- a/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Events.cs +++ b/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Events.cs @@ -1,4 +1,6 @@ using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Util; +using Kermalis.VGMusicStudio.Core.Util.EndianBinaryExtras; using System; using System.Collections.Generic; using System.Linq; @@ -108,274 +110,953 @@ private void AddTrackEvents(byte trackIndex, EndianBinaryReader r) } case 0x94: { - lastRest = (uint)(r.ReadByte() | (r.ReadByte() << 8) | (r.ReadByte() << 16)); + lastRest = new EndianBinaryReaderExtras(r).ReadUInt24(); 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: + case 0x95: { + uint intervals = r.ReadByte(); if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + AddEvent(trackIndex, cmdOffset, new CheckIntervalCommand { Interval = intervals }); } break; } - case 0x98: + case 0x96: { if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new FinishCommand()); + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); } - cont = false; break; } - case 0x99: + case 0x97: { if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new LoopStartCommand { Offset = r.Stream.Position }); + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); } break; } - case 0xA0: + case 0x98: { - byte octave = r.ReadByte(); if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new OctaveSetCommand { Octave = octave }); + AddEvent(trackIndex, cmdOffset, new FinishCommand()); + r.Stream.Align(4); } + cont = false; break; } + case 0x99: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LoopStartCommand { Offset = r.Stream.Position }); + } + break; + } + case 0x9A: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0x9B: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0x9C: + { + byte repeats = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new DalSegnoAlCodaCommand { Command = cmd, Repeats = repeats }); + } + break; + } + case 0x9D: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new DalSegnoAlFineCommand { Command = cmd }); + } + break; + } + case 0x9E: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ToCodaCommand { Command = cmd }); + Tracks[trackIndex].ToCodaCommand = new SongEvent(cmdOffset, new ToCodaCommand { Command = cmd }); + } + break; + } + case 0x9F: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + 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 }); + sbyte change = r.ReadSByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new OctaveAddCommand { OctaveChange = change }); + } + break; + } + case 0xA2: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xA3: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; } - break; - } case 0xA4: + { + byte tempoArg = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new TempoCommand { Command = cmd, Tempo = tempoArg }); + } + break; + } 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 }); + byte tempoArg = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new TempoCommand { Command = cmd, Tempo = tempoArg }); + } + break; } - break; - } - case 0xAB: - { - byte[] bytes = new byte[1]; - r.ReadBytes(bytes); - if (!EventExists(trackIndex, cmdOffset)) + case 0xA6: { - AddEvent(trackIndex, cmdOffset, new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; } - break; - } - case 0xAC: - { - byte voice = r.ReadByte(); - if (!EventExists(trackIndex, cmdOffset)) + case 0xA7: { - AddEvent(trackIndex, cmdOffset, new VoiceCommand { Voice = voice }); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; } - break; - } - case 0xCB: - case 0xF8: - { - byte[] bytes = new byte[2]; - r.ReadBytes(bytes); - if (!EventExists(trackIndex, cmdOffset)) + case 0xA8: { - AddEvent(trackIndex, cmdOffset, new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); + byte waveID = r.ReadByte(); + byte bankID = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetSWDAndBankCommand { WaveID = waveID, BankID = bankID }); + } + break; } - break; - } - case 0xD7: - { - ushort bend = r.ReadUInt16(); - if (!EventExists(trackIndex, cmdOffset)) + case 0xA9: { - AddEvent(trackIndex, cmdOffset, new PitchBendCommand { Bend = bend }); + byte bankHiID = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetBankHiCommand { BankHiID = bankHiID }); + } + break; } - break; - } - case 0xE0: - { - byte volume = r.ReadByte(); - if (!EventExists(trackIndex, cmdOffset)) + case 0xAA: { - AddEvent(trackIndex, cmdOffset, new VolumeCommand { Volume = volume }); + byte bankLoID = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetBankLoCommand { BankLoID = bankLoID }); + } + break; } - break; - } - case 0xE3: - { - byte expression = r.ReadByte(); - if (!EventExists(trackIndex, cmdOffset)) + case 0xAB: { - AddEvent(trackIndex, cmdOffset, new ExpressionCommand { Expression = expression }); + byte[] bytes = new byte[1]; + r.ReadBytes(bytes); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); + } + break; } - break; - } - case 0xE8: - { - byte panArg = r.ReadByte(); - if (!EventExists(trackIndex, cmdOffset)) + case 0xAC: { - AddEvent(trackIndex, cmdOffset, new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); + byte voice = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VoiceCommand { Voice = voice }); + } + break; + } + case 0xAD: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xAE: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xAF: + { + ushort sweepRate = r.ReadUInt16(); + byte sweepPitch = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SweepSongVolumeCommand { SweepRate = sweepRate, SweepPitch = sweepPitch }); + } + break; } - break; - } - case 0x9D: case 0xB0: - case 0xC0: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new UnknownCommand { Command = cmd, Args = Array.Empty() }); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new DisableEnvelopeCommand { Command = cmd }); + } + break; } - break; - } - case 0x9C: - case 0xA9: - case 0xAA: case 0xB1: + { + byte attackVolume = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetEnvelopeAttackVolumeCommand { AttackVolume = attackVolume }); + } + break; + } case 0xB2: + { + byte attackTime = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetEnvelopeAttackTimeCommand { AttackTime = attackTime }); + } + break; + } case 0xB3: + { + byte hold = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetEnvelopeHoldCommand { Hold = hold }); + } + break; + } + case 0xB4: + { + byte decay = r.ReadByte(); + byte sustain = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetEnvelopeDecaySustainCommand { Decay = decay, Sustain = sustain }); + } + break; + } case 0xB5: + { + byte fade = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetEnvelopeFadeCommand { Fade = fade }); + } + break; + } case 0xB6: + { + byte release = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetEnvelopeReleaseCommand { Release = release }); + } + break; + } + case 0xB7: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xB8: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xB9: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xBA: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xBB: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } case 0xBC: + { + byte noteVolume = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetNoteVolumeCommand { NoteVolume = noteVolume }); + } + break; + } + case 0xBD: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } case 0xBE: + { + byte channelPanpot = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetChannelPanpotCommand { ChannelPanpot = channelPanpot }); + } + break; + } case 0xBF: + { + byte flagValue = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new FlagBeginCommand { FlagValue = flagValue }); + } + break; + } + case 0xC0: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new FlagEndCommand { Command = cmd }); + } + break; + } + case 0xC1: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xC2: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } case 0xC3: + { + byte channelVolume = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetChannelVolumeCommand { ChannelVolume = channelVolume }); + } + break; + } + case 0xC4: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xC5: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xC6: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xC7: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xC8: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xC9: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xCA: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xCB: + { + byte[] bytes = new byte[2]; + r.ReadBytes(bytes); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); + } + break; + } + case 0xCC: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xCD: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xCE: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xCF: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } case 0xD0: + { + byte fineTune = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetFineTuneCommand { FineTune = fineTune }); + } + break; + } case 0xD1: + { + byte fineTuneAddValue = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new AddToFineTuneCommand { FineTuneAddValue = fineTuneAddValue }); + } + break; + } 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 }); + byte coarseTune = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetCoarseTuneCommand { CoarseTune = coarseTune }); + } + break; } - break; - } - case 0xA8: - case 0xB4: case 0xD3: + { + ushort coarseTuneAddValue = r.ReadUInt16(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new AddToCoarseTuneCommand { CoarseTuneAddValue = coarseTuneAddValue }); + } + break; + } + case 0xD4: + { + ushort sweepTuneRate = r.ReadUInt16(); + byte sweepTuneTarget = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SweepTuneCommand { SweepTuneRate = sweepTuneRate, SweepTuneTarget = sweepTuneTarget }); + } + break; + } case 0xD5: + { + byte randomNoteRangeMin = r.ReadByte(); + byte randomNoteRangeMax = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetRandomNoteRangeCommand { RandomNoteRangeMin = randomNoteRangeMin, RandomNoteRangeMax = randomNoteRangeMax }); + } + break; + } case 0xD6: + { + ushort detuneRange = r.ReadUInt16(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetDetuneRangeCommand { DetuneRange = detuneRange }); + } + break; + } + case 0xD7: + { + ushort bend = r.ReadUInt16(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PitchBendCommand { Bend = bend }); + } + break; + } 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 }); + byte paramValue = r.ReadByte(); + byte paramTarget = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetParamCommand { ParamValue = paramValue, ParamTarget = paramTarget }); + } + break; } - break; - } - case 0xAF: - case 0xD4: - case 0xE2: - case 0xEA: - case 0xF3: - { - byte[] args = new byte[3]; - r.ReadBytes(args); - if (!EventExists(trackIndex, cmdOffset)) + case 0xD9: { - AddEvent(trackIndex, cmdOffset, new UnknownCommand { Command = cmd, Args = args }); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; } - break; - } - case 0xDD: - case 0xE5: - case 0xED: - case 0xF1: - { - byte[] args = new byte[4]; - r.ReadBytes(args); - if (!EventExists(trackIndex, cmdOffset)) + case 0xDA: { - AddEvent(trackIndex, cmdOffset, new UnknownCommand { Command = cmd, Args = args }); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xDB: + { + byte pitchBendRange = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetPitchBendRangeCommand { PitchBendRange = pitchBendRange }); + } + break; } - break; - } case 0xDC: + { + ushort rate = r.ReadUInt16(); + ushort depth = r.ReadUInt16(); + WaveformType waveformType = (WaveformType)r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ReplaceLFO1AsPitchCommand { Rate = rate, Depth = depth, WaveformType = waveformType }); + } + break; + } + case 0xDD: + { + ushort delay = r.ReadUInt16(); + ushort fadeTime = r.ReadUInt16(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetLFO1DelayFade { Delay = delay, FadeTime = fadeTime }); + } + break; + } + case 0xDE: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xDF: + { + bool pitchEnabled = r.ReadBoolean(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetLFO1ToPitchEnabledCommand { PitchEnabled = pitchEnabled }); + } + break; + } + case 0xE0: + { + byte volume = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VolumeCommand { Volume = volume }); + } + break; + } + case 0xE1: + { + byte trackAddVol = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new AddToTrackVolCommand { TrackVolAdd = trackAddVol }); + } + break; + } + case 0xE2: + { + ushort sweepRate = r.ReadUInt16(); + byte sweepVolume = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SweepTrackVolCommand { SweepRate = sweepRate, SweepVolume = sweepVolume }); + } + break; + } + case 0xE3: + { + byte expression = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ExpressionCommand { Expression = expression }); + } + break; + } case 0xE4: + { + ushort rate = r.ReadUInt16(); + ushort depth = r.ReadUInt16(); + WaveformType waveformType = (WaveformType)r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ReplaceLFO2AsVolumeCommand{ Rate = rate, Depth = depth, WaveformType = waveformType }); + } + break; + } + case 0xE5: + { + ushort delay = r.ReadUInt16(); + ushort fadeTime = r.ReadUInt16(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetLFO2DelayFade { Delay = delay, FadeTime = fadeTime }); + } + break; + } + case 0xE6: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xE7: + { + bool volumeEnabled = r.ReadBoolean(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetLFO2ToVolumeEnabledCommand { VolumeEnabled = volumeEnabled }); + } + break; + } + case 0xE8: + { + byte panArg = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); + } + break; + } + case 0xE9: + { + byte panArg = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new AddToPanpotCommand { PanpotAdd = (sbyte)(panArg - 0x40) }); + } + break; + } + case 0xEA: + { + ushort sweepRate = r.ReadUInt16(); + byte panpotTarget = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SweepPanpotCommand { SweepRate = sweepRate, PanpotTarget = panpotTarget }); + } + break; + } + case 0xEB: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } case 0xEC: + { + ushort rate = r.ReadUInt16(); + ushort depth = r.ReadUInt16(); + WaveformType waveformType = (WaveformType)r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ReplaceLFO3AsPanpotCommand { Rate = rate, Depth = depth, WaveformType = waveformType }); + } + break; + } + case 0xED: + { + ushort delay = r.ReadUInt16(); + ushort fadeTime = r.ReadUInt16(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetLFO3DelayFade { Delay = delay, FadeTime = fadeTime }); + } + break; + } + case 0xEE: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xEF: + { + bool panpotEnabled = r.ReadBoolean(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetLFO3ToPanpotEnabledCommand { PanpotEnabled = panpotEnabled }); + } + break; + } case 0xF0: - { - byte[] args = new byte[5]; - r.ReadBytes(args); - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new UnknownCommand { Command = cmd, Args = args }); + ushort rate = r.ReadUInt16(); + ushort depth = r.ReadUInt16(); + WaveformType waveformType = (WaveformType)r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ReplaceLFOCommand { Rate = rate, Depth = depth, WaveformType = waveformType }); + } + break; + } + case 0xF1: + { + ushort delay = r.ReadUInt16(); + ushort fadeTime = r.ReadUInt16(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetLFODelayFadeCommand { Delay = delay, FadeTime = fadeTime }); + } + break; + } + case 0xF2: + { + ParameterType paramType = (ParameterType)r.ReadByte(); + WaveformType waveformType = (WaveformType)r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetLFOParamCommand { ParamType = paramType, WaveformType = waveformType }); + } + break; + } + case 0xF3: + { + byte target = r.ReadByte(); + bool enabled = r.ReadBoolean(); + byte targetID = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SetLFORouteCommand { Target = target, Enabled = enabled, TargetID = targetID }); + } + break; + } + case 0xF4: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xF5: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xF6: + { + byte checkpoint = r.ReadByte(); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ScenarioSyncCommand { Checkpoint = checkpoint }); + } + break; + } + case 0xF7: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + 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 0xF9: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xFA: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xFB: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xFC: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xFD: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xFE: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; + } + case 0xFF: + { + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new InvalidCommand { Command = cmd }); + } + break; } - break; - } default: throw new DSEInvalidCMDException(trackIndex, (int)cmdOffset, cmd); } } @@ -429,26 +1110,58 @@ internal void SetCurTick(long ticks) goto finish; } - while (_player.TempoStack >= 240) + switch (Header.Type) { - _player.TempoStack -= 240; - for (int trackIndex = 0; trackIndex < Tracks.Length; trackIndex++) - { - DSETrack track = Tracks[trackIndex]; - if (!track.Stopped) + case "smdl": { - track.Tick(); - while (track.Rest == 0 && !track.Stopped) + while (_player.TempoStack >= 240) { - ExecuteNext(track); + _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; + } } + break; + } + case "smdb": + { + while (_player.TempoStack >= 120) + { + _player.TempoStack -= 120; + 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; + } + } + break; } - } - _player.ElapsedTicks++; - if (_player.ElapsedTicks == ticks) - { - goto finish; - } } _player.TempoStack += _player.Tempo; } diff --git a/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs b/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs index 90e30e4a..591733f2 100644 --- a/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs +++ b/VG Music Studio - Core/NDS/DSE/DSELoadedSong_Runtime.cs @@ -14,6 +14,14 @@ public void UpdateSongState(SongState info) public void ExecuteNext(DSETrack track) { + // Note: Within a SMD file, all event values + // after a command byte are all written in + // Little Endian byte order, regardless of + // whenever the DSE version is PlayStation 2, + // DS or Wii, even if the hardware CPU + // (eg. PowerPC) natively reads in a different + // byte order. + byte cmd = SMDFile[track.CurOffset++]; if (cmd <= 0x7F) { @@ -40,17 +48,17 @@ public void ExecuteNext(DSETrack track) } track.LastNoteDuration = duration; } - DSEChannel channel = _player.DMixer.AllocateChannel() + DSEChannel channel = _player.DMixer!.AllocateChannel() ?? throw new Exception("Not enough channels"); channel.Stop(); track.Octave = (byte)(track.Octave + oct); - if (channel.StartPCM(LocalSWD, _player.MasterSWD, track.Voice, n + (12 * track.Octave), duration)) + if (channel.StartChannel(LocalSWD!, _player.MainSWD, track, n + (12 * track.Octave), duration)) { channel.NoteVelocity = cmd; - channel.Owner = track; track.Channels.Add(channel); } + channel.SweepCounter = 0; } else if (cmd is >= 0x80 and <= 0x8F) { @@ -62,222 +70,798 @@ public void ExecuteNext(DSETrack track) // TODO: 0x95, 0x9E switch (cmd) { + // Pause processing events for duration of last + // pause(include Fixed duration pauses). Doesn't silence the track. case 0x90: - { - track.Rest = track.LastRest; - break; - } + { + track.Rest = track.LastRest; + break; + } + + // Pause for duration of last pause(include Fixed duration pauses) + a signed nb Ticks + // Doesn't silence the track.(seen in bgm0021) case 0x91: - { - track.LastRest = (uint)(track.LastRest + (sbyte)SMDFile[track.CurOffset++]); - track.Rest = track.LastRest; - break; - } + { + track.LastRest = (uint)(track.LastRest + (sbyte)SMDFile[track.CurOffset++]); + track.Rest = track.LastRest; + break; + } + + // Pause processing events for nb Ticks + // Doesn't silence the track. case 0x92: - { - track.LastRest = SMDFile[track.CurOffset++]; - track.Rest = track.LastRest; - break; - } + { + track.LastRest = SMDFile[track.CurOffset++]; + track.Rest = track.LastRest; + break; + } + + // Pause processing events for specified + // amount of ticks. (little endian) + // Doesn't silence the track. case 0x93: - { - track.LastRest = (uint)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); - track.Rest = track.LastRest; - break; - } + { + track.LastRest = (uint)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.Rest = track.LastRest; + break; + } + + // Pause processing events for specified + // amount of ticks. (little endian) + // Doesn't silence the track. case 0x94: - { - track.LastRest = (uint)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8) | (SMDFile[track.CurOffset++] << 16)); - track.Rest = track.LastRest; - break; - } + { + track.LastRest = (uint)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8) | (SMDFile[track.CurOffset++] << 16)); + track.Rest = track.LastRest; + break; + } + + // Pause until all notes are released! + // It re-checks at the rate specified as param in ticks! + // So it will always pause at least "tick_interval" ticks. + // (uint8) tick_interval + case 0x95: + { + for (track.TickInterval = SMDFile[track.CurOffset++]; track.TickInterval > 0; track.TickInterval--) + { + track.LastNoteDuration = track.TickInterval; + } + break; + } + + // Invalid (Disable track) case 0x96: + + // Invalid (Disable track) case 0x97: + { + track.Stopped = true; + break; + } + + // End of track terminator + Filler + // Can have a delta-time prefixed. + case 0x98: + { + if (track.LoopOffset == -1) + { + track.Stopped = true; + } + else + { + track.CurOffset = track.LoopOffset; + } + break; + } + + // Sets the looping point for the + // track. + case 0x99: + { + track.LoopOffset = track.CurOffset; + break; + } + + // Invalid (Disable track) case 0x9A: + + // Invalid (Disable track) case 0x9B: + { + track.Stopped = true; + break; + } + + // Repeat mark (Dal Segno Al Coda): (Seen in bgm0113.smd) + // Start of the segment to repeat, when the next event 0x9D is processed. (confirmed ?) + // (uint8) nb_repeats + case 0x9C: + { + track.DalSegnoCommands!.Add(new SongEvent(track.CurOffset + 1, new DalSegnoAlCodaCommand { Command = cmd, Repeats = SMDFile[track.CurOffset++] })); + break; + } + + // Repeat from mark (Dal Segno Al Fine): + // Repeats the segment starting at the previous event 0x9C, the amount of times specified in event 0x9C. (confirmed ?) + case 0x9D: + { + var segno = (DalSegnoAlCodaCommand)track.DalSegnoCommands![^1].Command; + if (segno.Repeats > 0) + { + segno.Repeats--; + track.CurOffset = (int)track.DalSegnoCommands[^1].Offset; + } + else + { + track.DalSegnoCommands.Remove(track.DalSegnoCommands[^1]); + if (track.ToCodaCommand is not null && track.DalSegnoCommands.Capacity is 0) + { + track.CurOffset = (int)track.ToCodaCommand!.Offset; + } + } + break; + } + + // After repeat mark (To Coda): + // After the segment has been repeated enough times, it will then jump to this from the last 0x9D event.(confirmed ?) + case 0x9E: + { + break; + } + + // Invalid (Disable track) case 0x9F: + { + track.Stopped = true; + break; + } + + // Reset the current pitch to the octave specified. + // ( Ex: To play C6, you'd set this to 6, for C4, + // you'd set it to 4, and so on! ) + case 0xA0: + { + track.Octave = SMDFile[track.CurOffset++]; + break; + } + + // Adds the value specified to the current track octave. + case 0xA1: + { + track.Octave = (byte)(track.Octave + (sbyte)SMDFile[track.CurOffset++]); + break; + } + + // Invalid (Disable track) case 0xA2: + + // Invalid (Disable track) case 0xA3: + { + track.Stopped = true; + break; + } + + // Sets the tempo in BPM. Only on Track#0 + case 0xA4: + { + _player.Tempo = SMDFile[track.CurOffset++]; + break; + } + + // Sets the tempo in BPM. Only on Track#0 ? + case 0xA5: + { + _player.Tempo = SMDFile[track.CurOffset++]; + break; + } + + // Invalid (Disable track) case 0xA6: + + // Invalid (Disable track) case 0xA7: + { + track.Stopped = true; + break; + } + + // SetSWDAndBank (Seen in ev_e09b.sed) + case 0xA8: + { + track.WaveIDIndex = SMDFile[track.CurOffset++]; + track.BankIDIndex = SMDFile[track.CurOffset++]; + break; + } + + // SetBankHi Sets the bank id high byte + // (Value from the file's header at offset 0xF) + case 0xA9: + { + track.BankHi = SMDFile[track.CurOffset++]; + break; + } + + // SetBankLo Sets the bank id low byte + // (Value from the file's header at offset 0xE) + case 0xAA: + { + track.BankLo = SMDFile[track.CurOffset++]; + break; + } + + // Skips processing the next byte. + case 0xAB: + { + track.CurOffset++; + break; + } + + // Set the current instrument/program. + // Program/Instrument IDs are from the associated SWD file. + case 0xAC: + { + track.Voice = SMDFile[track.CurOffset++]; + break; + } + + // Invalid (Disable track) case 0xAD: + + // Invalid (Disable track) case 0xAE: + { + track.Stopped = true; + break; + } + + // SweepSongVolume + case 0xAF: + { + track.SweepRate = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.SweepPitch = SMDFile[track.CurOffset++]; + break; + } + + // DisableEnvelope + case 0xB0: + { + track.AttackVolume = 0; + track.AttackTime = 0; + track.Hold = 0; + track.Decay = 0; + track.Sustain = 0; + track.Fade = 0; + track.Release = 0; + break; + } + + // SetEnvelopeAttackVolume + case 0xB1: + { + track.AttackVolume = SMDFile[track.CurOffset++]; + break; + } + + // SetEnvelopeAttackTime (Seen in ev_e09b.sed) + case 0xB2: + { + track.AttackTime = SMDFile[track.CurOffset++]; + break; + } + + // SetEnvelopeHold + case 0xB3: + { + track.Hold = SMDFile[track.CurOffset++]; + break; + } + + // SetEnvelopeDecaySustain (Seen in ev_e09b.sed) + // (setting either to 0xFF means it won't change it) + case 0xB4: + { + if (SMDFile[track.CurOffset] is not 0xFF) + { + track.Decay = SMDFile[track.CurOffset++]; + } + else + { + track.CurOffset++; + } + if (SMDFile[track.CurOffset] is not 0xFF) + { + track.Sustain = SMDFile[track.CurOffset++]; + } + else + { + track.CurOffset++; + } + break; + } + + // SetEnvelopeFade (Seen in bgm0100.smd) + case 0xB5: + { + track.Fade = SMDFile[track.CurOffset++]; + break; + } + + // SetEnvelopeRelease + case 0xB6: + { + track.Release = SMDFile[track.CurOffset++]; + break; + } + + // Invalid (Disable track) case 0xB7: + + // Invalid (Disable track) case 0xB8: + + // Invalid (Disable track) case 0xB9: + + // Invalid (Disable track) case 0xBA: + + // Invalid (Disable track) case 0xBB: + { + track.Stopped = true; + break; + } + + // SetNoteVolume (? Does he means velocity, aftertouch, or something else?) + case 0xBC: + { + track.NoteVolume = SMDFile[track.CurOffset++]; + break; + } + + // Invalid (Disable track) case 0xBD: + { + track.Stopped = true; + break; + } + + // SetChannelPanpot: Set the panpot at the CHANNEL level. + // (seen in last 3 tracks of bgm0048.smd) + case 0xBE: + { + track.ChannelPanpot = SMDFile[track.CurOffset++]; + break; + } + + // SetFlag0 (note by coda : sets or clears flag bit 0) + case 0xBF: + { + track.FlagEnded = false; + track.FlagValue = SMDFile[track.CurOffset++]; + break; + } + + // SetFlag1 (note by coda: sets flag bit 1) + case 0xC0: + { + track.FlagEnded = true; + break; + } + + // Invalid (Disable track) case 0xC1: + + // Invalid (Disable track) case 0xC2: + { + track.Stopped = true; + break; + } + + // SetChannelVolume: Set the volume at the CHANNEL level(used in inazuma eleven a lot) + case 0xC3: + { + track.ChannelVolume = SMDFile[track.CurOffset++]; + break; + } + + // Invalid (Disable track) case 0xC4: + + // Invalid (Disable track) case 0xC5: + + // Invalid (Disable track) case 0xC6: + + // Invalid (Disable track) case 0xC7: + + // Invalid (Disable track) case 0xC8: + + // Invalid (Disable track) case 0xC9: + + // Invalid (Disable track) case 0xCA: + { + track.Stopped = true; + break; + } + + // Skip Next 2 bytes. Seen in Professor Layton and the Last Specter, BGM_10.SMD + case 0xCB: + { + track.CurOffset += 2; + break; + } + + // Invalid (Disable track) case 0xCC: + + // Invalid (Disable track) case 0xCD: + + // Invalid (Disable track) case 0xCE: + + // Invalid (Disable track) case 0xCF: + { + track.Stopped = true; + break; + } + + // SetFineTune (Seen in bgm0100.smd) + case 0xD0: + { + track.FineTune = SMDFile[track.CurOffset++]; + break; + } + + // AddToFineTune (Seen in bgm0113.smd) + case 0xD1: + { + track.FineTuneAdd = SMDFile[track.CurOffset++]; + break; + } + + // SetCoarseTune (Seen in bgm0116.smd) + case 0xD2: + { + track.CoarseTune = SMDFile[track.CurOffset++]; + break; + } + + // AddToCoarseTune (Seen in bgm0116.smd) (That seems wrong because setcoarsetune param is smaller than this one) + case 0xD3: + { + track.CoarseTuneAdd = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + break; + } + + // SweepTune (Seen in ev_e09b.sed) + case 0xD4: + { + track.SweepTuneRate = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.SweepTuneTarget = SMDFile[track.CurOffset++]; + break; + } + + // SetRandomNoteRange + case 0xD5: + { + track.RandomNoteRangeMin = SMDFile[track.CurOffset++]; + track.RandomNoteRangeMax = SMDFile[track.CurOffset++]; + break; + } + + // SetDetuneRange: random detune (Seen in bgm0101.smd) + case 0xD6: + { + track.DetuneRange = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + break; + } + + // Pitch bend. Works the same as the MIDI event 0xE0 Pitch Wheel. + // The uint16 is stored in big endian order. + // 500 == 1 semitone. Negative val, means increase pitch, positive the opposite. + case 0xD7: + { + track.PitchBend = (ushort)((SMDFile[track.CurOffset++] << 8) | SMDFile[track.CurOffset++]); + break; + } + + // SetParam (note by coda: modifies an unused parameter) + case 0xD8: + { + track.CurOffset += 2; + break; + } + + // Invalid (Disable track) case 0xD9: + + // Invalid (Disable track) case 0xDA: + { + track.Stopped = true; + break; + } + + // SetPitchBendRange: In semitones (Used in bgm0000, and bgm0134 (Value usually range between 0,2,4,7,12,24)) + case 0xDB: + { + track.PitchBendRange = SMDFile[track.CurOffset++]; + break; + } + + // ReplaceLFO1AsPitch (Seen in ev_e09b.sed) + case 0xDC: + { + track.LFO1Rate = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.LFO1Depth = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.LFO1WaveformType = (WaveformType)SMDFile[track.CurOffset++]; + break; + } + + // SetLFO1DelayFade (Seen in ev_e09b.sed) + case 0xDD: + { + track.LFO1Delay = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.LFO1FadeTime = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + break; + } + + // Invalid (Disable track) case 0xDE: + { + track.Stopped = true; + break; + } + + // SetLFO1ToPitchEnabled: (Used in bgm0000) + // If true, turns on LFO1 and connects it to pitch. If false, disables LFO1 + case 0xDF: + { + track.LFO1PitchEnabled = SMDFile[track.CurOffset] is 0 || SMDFile[track.CurOffset] is 1; + track.CurOffset++; + break; + } + + // Track volume, similar to GM CC#7. + case 0xE0: + { + track.Volume = SMDFile[track.CurOffset++]; + break; + } + + // AddToTrackVol + case 0xE1: + { + track.VolumeAdd = SMDFile[track.CurOffset++]; + break; + } + + // SweepTrackVol (Seen in ev_e09b.sed) + case 0xE2: + { + track.SweepRate = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.SweepVolume = SMDFile[track.CurOffset++]; + break; + } + + // Similar to the GM CC#11. + // Its a secondary volume control + // there to avoid messing with the + // dynamic range of instruments + // when changing track volume! + case 0xE3: + { + track.Expression = SMDFile[track.CurOffset++]; + break; + } + + // ReplaceLFO2AsVolume + case 0xE4: + { + track.LFO2Rate = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.LFO2Depth = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.LFO2WaveformType = (WaveformType)SMDFile[track.CurOffset++]; + break; + } + + // SetLFO2DelayFade + case 0xE5: + { + track.LFO2Delay = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.LFO2FadeTime = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + break; + } + + // Invalid (Disable track) case 0xE6: + { + track.Stopped = true; + break; + } + + // SetLFO2ToVolumeEnabled: + // If true, turns on LFO2 and connects it to volume. If false, disables LFO2. + case 0xE7: + { + track.LFO2VolumeEnabled = SMDFile[track.CurOffset] is 0 || SMDFile[track.CurOffset] is 1; + track.CurOffset++; + break; + } + + // SetPanpot + // 0x00 == Full Left + // 0x40 == Middle + // 0x80 == Full Right + case 0xE8: + { + track.Panpot = (sbyte)(SMDFile[track.CurOffset++] - 0x40); + break; + } + + // AddToPan + case 0xE9: + { + track.PanpotAdd = SMDFile[track.CurOffset++]; + break; + } + + // SweepPanpot (Seen in bgm0100.smd) + case 0xEA: + { + track.SweepRate = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.PanpotTarget = SMDFile[track.CurOffset++]; + break; + } + + // Invalid (Disable track) case 0xEB: + { + track.Stopped = true; + break; + } + + // ReplaceLFO3AsPanpot (Seen in ev_e09b.sed) + case 0xEC: + { + track.LFO3Rate = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.LFO3Depth = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.LFO3WaveformType = (WaveformType)SMDFile[track.CurOffset++]; + break; + } + + // SetLFO3DelayFade (Seen in ev_e09b.sed) + case 0xED: + { + track.LFO3Delay = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.LFO3FadeTime = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + break; + } + + // Invalid (Disable track) case 0xEE: + { + track.Stopped = true; + break; + } + + // SetLFO3ToPanpotEnabled: (Seen in ev_e09b.sed) + // If true, turns on LFO3 and connects it to pan. If false, disables LFO3. + case 0xEF: + { + track.LFO3PanpotEnabled = SMDFile[track.CurOffset] is 0 || SMDFile[track.CurOffset] is 1; + track.CurOffset++; + break; + } + + // ReplaceLFO: (Seen in ev_e09b.sed) + // Rate is in Hertz. + // LFO delay and LFO fade time are set to 0. + // *For waveform ids see "LFO Waveforms IDs" below + case 0xF0: + { + track.LFORate = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.LFODepth = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.LFOWaveformType = (WaveformType)SMDFile[track.CurOffset++]; + break; + } + + // SetLFODelayFade: (Seen in ev_e09b.sed) + case 0xF1: + { + track.LFODelay = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + track.LFOFadeTime = (ushort)(SMDFile[track.CurOffset++] | (SMDFile[track.CurOffset++] << 8)); + break; + } + + // SetLFOParam: (Seen in ev_e09b.sed) + // + // *For parameter ids see "LFO Parameter IDs" below + // *For waveform ids see "LFO Waveforms IDs" below + case 0xF2: + { + track.LFOParamType = (ParameterType)SMDFile[track.CurOffset++]; + track.LFOParamWaveformType = (WaveformType)SMDFile[track.CurOffset++]; + break; + } + + // SetLFORoute (Seen in ev_e09b.sed) + // + // *For target ids see "LFO Target IDs" below + case 0xF3: + { + track.LFOTarget = SMDFile[track.CurOffset++]; + track.LFOEnabled = SMDFile[track.CurOffset] is 0 || SMDFile[track.CurOffset] is 1; + track.CurOffset++; + track.LFOType = (LFOType)SMDFile[track.CurOffset++]; + break; + } + + // Invalid (Disable track) case 0xF4: + + // Invalid (Disable track) case 0xF5: + { + track.Stopped = true; + break; + } + + // ScenarioSync(Seen in bgm0001.smd) param value is usually between 0x0-0xf Seems to be used to sync music and script engine scene! + case 0xF6: + { + track.CurOffset++; + break; + } + + // Invalid (Disable track) case 0xF7: + { + track.Stopped = true; + break; + } + + // Skips processing next 2 bytes. + case 0xF8: + { + track.CurOffset += 2; + break; + } + + // Invalid (Disable track) case 0xF9: + + // Invalid (Disable track) case 0xFA: + + // Invalid (Disable track) case 0xFB: + + // Invalid (Disable track) case 0xFC: + + // Invalid (Disable track) case 0xFD: + + // Invalid (Disable track) case 0xFE: + + // Invalid (Disable track) case 0xFF: - { - track.Stopped = true; - break; - } - case 0x98: - { - if (track.LoopOffset == -1) { track.Stopped = true; + break; } - 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 89f6c529..facf0e42 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEMixer.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEMixer.cs @@ -1,6 +1,9 @@ -using Kermalis.VGMusicStudio.Core.NDS.SDAT; +using Kermalis.VGMusicStudio.Core.Formats; +using Kermalis.VGMusicStudio.Core.NDS.SDAT; using Kermalis.VGMusicStudio.Core.Util; using NAudio.Wave; +using SoundFlow.Structs; +using SoundFlow.Enums; using System; namespace Kermalis.VGMusicStudio.Core.NDS.DSE; @@ -10,16 +13,31 @@ public sealed class DSEMixer : Mixer private const int NUM_CHANNELS = 0x20; // Actual value unknown for now private readonly float _samplesReciprocal; - private readonly int _samplesPerBuffer; + internal override int SamplesPerBuffer { get; } private bool _isFading; private long _fadeMicroFramesLeft; private float _fadePos; private float _fadeStepPerMicroframe; private readonly DSEChannel[] _channels; - private readonly BufferedWaveProvider _buffer; + private readonly AudioBackend DSEPlaybackBackend; - protected override WaveFormat WaveFormat => _buffer.WaveFormat; + #region PortAudio Fields + // PortAudio Fields + private readonly Wave? _bufferPortAudio; + #endregion + + #region MiniAudio Fields + // MiniAudio Fields + private readonly AudioFormat _formatSoundFlow; + protected override AudioFormat SoundFlowFormat => _formatSoundFlow; + #endregion + + #region NAudio Fields + // NAudio Fields + private readonly BufferedWaveProvider? _bufferNAudio; + protected override WaveFormat? WaveFormat => _bufferNAudio!.WaveFormat; + #endregion public DSEMixer() { @@ -27,8 +45,8 @@ public DSEMixer() // - 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; + SamplesPerBuffer = 341; // TODO + _samplesReciprocal = 1f / SamplesPerBuffer; _channels = new DSEChannel[NUM_CHANNELS]; for (byte i = 0; i < NUM_CHANNELS; i++) @@ -36,12 +54,42 @@ public DSEMixer() _channels[i] = new DSEChannel(i); } - _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + DSEPlaybackBackend = PlaybackBackend; + switch (PlaybackBackend) { - DiscardOnBufferOverflow = true, - BufferLength = _samplesPerBuffer * 64, - }; - Init(_buffer); + case AudioBackend.PortAudio: + { + _bufferPortAudio = new Wave() + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64, + }; + _bufferPortAudio.CreateIeeeFloatWave(sampleRate, 2, 16); + Init(waveData: _bufferPortAudio, PortAudio.SampleFormat.Int16); + break; + } + case AudioBackend.MiniAudio: + { + _formatSoundFlow = new AudioFormat + { + Channels = 2, + SampleRate = sampleRate, + Format = SoundFlow.Enums.SampleFormat.F32 + }; + Init(); + break; + } + case AudioBackend.NAudio: + { + _bufferNAudio = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64, + }; + Init(waveProvider: _bufferNAudio); + break; + } + } } internal DSEChannel? AllocateChannel() @@ -85,20 +133,26 @@ internal void ChannelTick() chan.Volume = (byte)chan.StepEnvelope(); if (chan.NoteLength == 0 && !DSEUtils.IsStateRemovable(chan.State)) { - chan.SetEnvelopePhase7_2074ED8(); + chan.SetEnvelopeRelease(); } - int vol = SDATUtils.SustainTable[chan.NoteVelocity] + SDATUtils.SustainTable[chan.Volume] + SDATUtils.SustainTable[chan.Owner.Volume] + SDATUtils.SustainTable[chan.Owner.Expression]; + int vol = DSEUtils.SustainTable[chan.NoteVelocity] + DSEUtils.SustainTable[chan.Volume] + DSEUtils.SustainTable[chan.Owner.Volume] + DSEUtils.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" + int pitch = ((chan.Key - chan.RootKey) << 6) + chan.SweepMain() + chan.Owner.GetPitch(); // "<< 6" is "* 0x40" + // switch (chan.Owner.LFOType) + // { + // case LFOType.Pitch: pitch += chan.LFOParam; break; + // case LFOType.Volume: vol += chan.LFOParam; break; + // case LFOType.Panpot: pan += chan.LFOParam; break; + // } if (DSEUtils.IsStateRemovable(chan.State) && vol <= -92544) { chan.Stop(); } else { - chan.Volume = SDATUtils.GetChannelVolume(vol); + chan.Volume = DSEUtils.GetChannelVolume(vol); chan.Panpot = chan.Owner.Panpot; - chan.Timer = SDATUtils.GetChannelTimer(chan.BaseTimer, pitch); + chan.Timer = DSEUtils.GetChannelTimer(chan.BaseTimer, pitch); } } } @@ -132,6 +186,7 @@ internal void ResetFade() } private readonly byte[] _b = new byte[4]; + private readonly float[] _f = new float[2]; internal void Process(bool output, bool recording) { float masterStep; @@ -156,7 +211,7 @@ internal void Process(bool output, bool recording) masterStep = (toMaster - fromMaster) * _samplesReciprocal; masterLevel = fromMaster; } - for (int i = 0; i < _samplesPerBuffer; i++) + for (int i = 0; i < SamplesPerBuffer; i++) { int left = 0, right = 0; @@ -188,6 +243,7 @@ internal void Process(bool output, bool recording) left = (int)f; _b[0] = (byte)left; _b[1] = (byte)(left >> 8); + _f[0] = left / (float)short.MaxValue; f = right * masterLevel; if (f < short.MinValue) { @@ -200,14 +256,49 @@ internal void Process(bool output, bool recording) right = (int)f; _b[2] = (byte)right; _b[3] = (byte)(right >> 8); + _f[1] = right / (float)short.MaxValue; masterLevel += masterStep; if (output) { - _buffer.AddSamples(_b, 0, 4); + switch (DSEPlaybackBackend) + { + case AudioBackend.PortAudio: + { + _bufferPortAudio!.AddSamples(_b, 0, 4); + break; + } + case AudioBackend.MiniAudio: + { + DataProvider!.AddSamples(_f); + break; + } + case AudioBackend.NAudio: + { + _bufferNAudio!.AddSamples(_b, 0, 4); + break; + } + } } if (recording) { - _waveWriter!.Write(_b, 0, 4); + switch (DSEPlaybackBackend) + { + case AudioBackend.PortAudio: + { + _waveWriterPortAudio!.Write(_b, 0, 4); + break; + } + case AudioBackend.MiniAudio: + { + _soundFlowEncoder!.Encode(_f); + break; + } + case AudioBackend.NAudio: + { + _waveWriterNAudio!.Write(_b, 0, 4); + break; + } + } } } } diff --git a/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs b/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs index bfdcda23..34a3edec 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; namespace Kermalis.VGMusicStudio.Core.NDS.DSE; @@ -7,16 +8,16 @@ public sealed class DSEPlayer : Player protected override string Name => "DSE Player"; private readonly DSEConfig _config; - internal readonly DSEMixer DMixer; - internal readonly SWD MasterSWD; + internal readonly DSEMixer? DMixer; + internal readonly SWD MainSWD; private DSELoadedSong? _loadedSong; - internal byte Tempo; + public override ushort Tempo { get; set; } internal int TempoStack; private long _elapsedLoops; public override ILoadedSong? LoadedSong => _loadedSong; - protected override Mixer Mixer => DMixer; + protected override Mixer Mixer => DMixer!; public DSEPlayer(DSEConfig config, DSEMixer mixer) : base(192) @@ -24,7 +25,7 @@ public DSEPlayer(DSEConfig config, DSEMixer mixer) DMixer = mixer; _config = config; - MasterSWD = new SWD(Path.Combine(config.BGMPath, "bgm.swd")); + MainSWD = new SWD(config.MainSWDFile); } public override void LoadSong(int index) @@ -35,7 +36,7 @@ public override void LoadSong(int index) } // If there's an exception, this will remain null - _loadedSong = new DSELoadedSong(this, _config.BGMFiles[index]); + _loadedSong = new DSELoadedSong(this, _config.SMDFiles[index]); _loadedSong.SetTicks(); } public override void UpdateSongState(SongState info) @@ -49,7 +50,7 @@ internal override void InitEmulation() TempoStack = 0; _elapsedLoops = 0; ElapsedTicks = 0; - DMixer.ResetFade(); + DMixer!.ResetFade(); DSETrack[] tracks = _loadedSong!.Tracks; for (int i = 0; i < tracks.Length; i++) { @@ -74,24 +75,48 @@ protected override bool Tick(bool playing, bool recording) DSELoadedSong s = _loadedSong!; bool allDone = false; - while (!allDone && TempoStack >= 240) + switch (_config.Header!.Type) { - TempoStack -= 240; - allDone = true; - for (int i = 0; i < s.Tracks.Length; i++) - { - TickTrack(s, s.Tracks[i], ref allDone); - } - if (DMixer.IsFadeDone()) - { - allDone = true; - } + case "smdl": + { + while (!allDone && TempoStack >= 240) + { + TempoStack -= 240; + allDone = true; + for (int i = 0; i < s.Tracks.Length; i++) + { + TickTrack(s, s.Tracks[i], ref allDone); + } + if (DMixer!.IsFadeDone()) + { + allDone = true; + } + } + break; + } + case "smdb": + { + while (!allDone && TempoStack >= 120) // Wii tempo is 120 by default + { + TempoStack -= 120; + allDone = true; + for (int i = 0; i < s.Tracks.Length; i++) + { + TickTrack(s, s.Tracks[i], ref allDone); + } + if (DMixer!.IsFadeDone()) + { + allDone = true; + } + } + break; + } } if (!allDone) { TempoStack += Tempo; } - DMixer.ChannelTick(); + DMixer!.ChannelTick(); DMixer.Process(playing, recording); return allDone; } @@ -127,7 +152,7 @@ private void HandleTicksAndLoop(DSELoadedSong s, DSETrack track) _elapsedLoops++; UpdateElapsedTicksAfterLoop(s.Events[track.Index], track.CurOffset, track.Rest); - if (ShouldFadeOut && _elapsedLoops > NumLoops && !DMixer.IsFading()) + if (ShouldFadeOut && _elapsedLoops > NumLoops && !DMixer!.IsFading()) { DMixer.BeginFadeOut(); } diff --git a/VG Music Studio - Core/NDS/DSE/DSETrack.cs b/VG Music Studio - Core/NDS/DSE/DSETrack.cs index a15e380a..516b7f89 100644 --- a/VG Music Studio - Core/NDS/DSE/DSETrack.cs +++ b/VG Music Studio - Core/NDS/DSE/DSETrack.cs @@ -2,10 +2,10 @@ namespace Kermalis.VGMusicStudio.Core.NDS.DSE; -internal sealed class DSETrack +internal sealed class DSETrack(byte i, int startOffset) { - public readonly byte Index; - private readonly int _startOffset; + public readonly byte Index = i; + private readonly int _startOffset = startOffset; public byte Octave; public byte Voice; public byte Expression; @@ -13,19 +13,75 @@ internal sealed class DSETrack public sbyte Panpot; public uint Rest; public ushort PitchBend; + public byte PitchBendRange; + public byte FineTune; + public byte FineTuneAdd; + public byte CoarseTune; + public ushort CoarseTuneAdd; + public ushort SweepTuneRate; + public byte SweepTuneTarget; + public byte RandomNoteRangeMin; + public byte RandomNoteRangeMax; + public ushort DetuneRange; + public byte NoteVolume; + public byte BankHi; + public byte BankLo; + public bool FlagEnded; + public byte FlagValue; // Unsure as to what value it's referring to + public byte ChannelPanpot; + public byte ChannelVolume; + public ushort LFORate; + public ushort LFODepth; + public WaveformType LFOWaveformType; + public ushort LFODelay; + public ushort LFOFadeTime; + internal ushort LFO1Rate; + internal ushort LFO1Depth; + internal WaveformType LFO1WaveformType; + internal ushort LFO1Delay; + internal ushort LFO1FadeTime; + internal ushort LFO2Rate; + internal ushort LFO2Depth; + internal WaveformType LFO2WaveformType; + internal ushort LFO2Delay; + internal ushort LFO2FadeTime; + internal ushort LFO3Rate; + internal ushort LFO3Depth; + internal WaveformType LFO3WaveformType; + internal ushort LFO3Delay; + internal ushort LFO3FadeTime; + public ParameterType LFOParamType; + public WaveformType LFOParamWaveformType; + public byte LFOTarget; + public bool LFOEnabled; + public LFOType LFOType; + public bool LFO1PitchEnabled; + public bool LFO2VolumeEnabled; + public bool LFO3PanpotEnabled; + public byte VolumeAdd; + public byte PanpotAdd; 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 uint TickInterval; + public ushort SweepRate; + public byte SweepPitch; + public byte SweepVolume; + public byte PanpotTarget; + public int WaveIDIndex; + public int BankIDIndex; + public List? DalSegnoCommands; + public SongEvent? ToCodaCommand; + public byte AttackVolume; + public byte AttackTime; + public byte Hold; + public byte Decay; + public byte Sustain; + public byte Fade; + public byte Release; + public readonly List Channels = new(0x10); public void Init() { @@ -36,14 +92,39 @@ public void Init() Panpot = 0; Rest = 0; PitchBend = 0; + NoteVolume = 0; + FlagEnded = true; + FlagValue = 0; + ChannelPanpot = 0; + ChannelVolume = 0; CurOffset = _startOffset; LoopOffset = -1; Stopped = false; LastNoteDuration = 0; LastRest = 0; + TickInterval = 0; + SweepRate = 0; + SweepPitch = 0; + SweepTuneRate = 0; + WaveIDIndex = -1; + BankIDIndex = -1; + DalSegnoCommands = []; + + AttackVolume = 0; + AttackTime = 0; + Hold = 0; + Decay = 0; + Sustain = 0; + Fade = 0; + Release = 0; StopAllChannels(); } - + public int GetPitch() + { + //int lfo = LFOType == LFOType.Pitch ? LFOParam : 0; + int lfo = 0; + return (PitchBend * PitchBendRange / 2) + lfo; + } public void Tick() { if (Rest > 0) @@ -76,7 +157,7 @@ public void UpdateSongState(SongState.Track tin) tin.Voice = Voice; tin.Type = "PCM"; tin.Volume = Volume; - tin.PitchBend = PitchBend; + tin.PitchBend = GetPitch(); tin.Extra = Octave; tin.Panpot = Panpot; @@ -96,6 +177,7 @@ public void UpdateSongState(SongState.Track tin) for (int j = 0; j < channels.Length; j++) { DSEChannel c = channels[j]; + c ??= new DSEChannel((byte)j); // Failsafe in the rare event that the c variable becomes null if (!DSEUtils.IsStateRemovable(c.State)) { tin.Keys[numKeys++] = c.Key; diff --git a/VG Music Studio - Core/NDS/DSE/DSEUtils.cs b/VG Music Studio - Core/NDS/DSE/DSEUtils.cs index 8264b315..c42a94cb 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEUtils.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEUtils.cs @@ -1,12 +1,15 @@ -using System; +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Diagnostics; namespace Kermalis.VGMusicStudio.Core.NDS.DSE; internal static class DSEUtils { - public static ReadOnlySpan Duration16 => new short[128] - { - 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, + public static ReadOnlySpan Duration16 => + [ + 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, @@ -22,10 +25,10 @@ internal static class DSEUtils 0x10EA, 0x1194, 0x123E, 0x12F2, 0x13B0, 0x146E, 0x1536, 0x15FE, 0x16D0, 0x17A2, 0x187E, 0x195A, 0x1A40, 0x1B30, 0x1C20, 0x1D1A, 0x1E1E, 0x1F22, 0x2030, 0x2148, 0x2260, 0x2382, 0x2710, 0x7FFF, - }; - public static ReadOnlySpan Duration32 => new int[128] - { - 0x00000000, 0x00000004, 0x00000007, 0x0000000A, 0x0000000F, 0x00000015, 0x0000001C, 0x00000024, + ]; + public static ReadOnlySpan Duration32 => + [ + 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, @@ -41,14 +44,395 @@ internal static class DSEUtils 0x00025532, 0x000264CF, 0x000274AE, 0x000284D0, 0x00029536, 0x0002A5E0, 0x0002B6CE, 0x0002C802, 0x000341B0, 0x000355F8, 0x00036A90, 0x00037F79, 0x000394B4, 0x0003AA41, 0x0003C021, 0x0003D654, 0x0003ECDA, 0x000403B5, 0x00041AE5, 0x0004326A, 0x00044A45, 0x00046277, 0x00047B00, 0x7FFFFFFF, - }; - public static ReadOnlySpan FixedRests => new byte[0x10] + ]; + + public static ReadOnlySpan AttackVolumeTable => + [ + 255, 254, 253, 252, 251, 250, 249, 248, + 247, 246, 245, 244, 243, 242, 241, 240, + 239, 238, 237, 236, 235, 234, 233, 232, + 231, 230, 229, 228, 227, 226, 225, 224, + 223, 222, 221, 220, 219, 218, 217, 216, + 215, 214, 213, 212, 211, 210, 209, 208, + 207, 206, 205, 204, 203, 202, 201, 200, + 199, 198, 197, 196, 195, 194, 193, 192, + 191, 190, 189, 188, 187, 186, 185, 184, + 183, 182, 181, 180, 179, 178, 177, 176, + 175, 174, 173, 172, 171, 170, 169, 168, + 167, 166, 165, 164, 163, 162, 161, 160, + 159, 158, 157, 156, 155, 154, 153, 152, + 151, 150, 149, 148, 147, 143, 137, 132, + 127, 123, 116, 109, 100, 92, 84, 73, + 63, 51, 38, 26, 14, 5, 1, 0, + ]; + public static ReadOnlySpan DecayTable => + [ + 1, 3, 5, 7, 9, 11, 13, 15, + 17, 19, 21, 23, 25, 27, 29, 31, + 33, 35, 37, 39, 41, 43, 45, 47, + 49, 51, 53, 55, 57, 59, 61, 63, + 65, 67, 69, 71, 73, 75, 77, 79, + 81, 83, 85, 87, 89, 91, 93, 95, + 97, 99, 101, 102, 104, 105, 107, 108, + 110, 111, 113, 115, 116, 118, 120, 122, + 124, 126, 128, 130, 132, 135, 137, 140, + 142, 145, 148, 151, 154, 157, 160, 163, + 167, 171, 175, 179, 183, 187, 192, 197, + 202, 208, 213, 219, 226, 233, 240, 248, + 256, 265, 274, 284, 295, 307, 320, 334, + 349, 366, 384, 404, 427, 452, 480, 512, + 549, 591, 640, 698, 768, 853, 960, 1097, + 1280, 1536, 1920, 2560, 3840, 7680, 15360, 65535, + ]; + public static ReadOnlySpan SustainTable => + [ + -92544, -92416, -92288, -83328, -76928, -71936, -67840, -64384, + -61440, -58880, -56576, -54400, -52480, -50688, -49024, -47488, + -46080, -44672, -43392, -42240, -41088, -40064, -39040, -38016, + -36992, -36096, -35328, -34432, -33664, -32896, -32128, -31360, + -30592, -29952, -29312, -28672, -28032, -27392, -26880, -26240, + -25728, -25088, -24576, -24064, -23552, -23040, -22528, -22144, + -21632, -21120, -20736, -20224, -19840, -19456, -19072, -18560, + -18176, -17792, -17408, -17024, -16640, -16256, -16000, -15616, + -15232, -14848, -14592, -14208, -13952, -13568, -13184, -12928, + -12672, -12288, -12032, -11648, -11392, -11136, -10880, -10496, + -10240, -9984, -9728, -9472, -9216, -8960, -8704, -8448, + -8192, -7936, -7680, -7424, -7168, -6912, -6656, -6400, + -6272, -6016, -5760, -5504, -5376, -5120, -4864, -4608, + -4480, -4224, -3968, -3840, -3584, -3456, -3200, -2944, + -2816, -2560, -2432, -2176, -2048, -1792, -1664, -1408, + -1280, -1024, -896, -768, -512, -384, -128, 0, + ]; + + private static ReadOnlySpan SinTable => + [ + 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) { - 96, 72, 64, 48, 36, 32, 24, 18, 16, 12, 9, 8, 6, 4, 3, 2, - }; + if (index < 0x20) + { + return SinTable[index]; + } + if (index < 0x40) + { + return SinTable[0x20 - (index - 0x20)]; + } + if (index < 0x60) + { + return -SinTable[index - 0x40]; + } + // < 0x80 + return -SinTable[0x20 - (index - 0x60)]; + } + private static ReadOnlySpan PitchTable => + [ + 0, 59, 118, 178, 237, 296, 356, 415, + 475, 535, 594, 654, 714, 773, 833, 893, + 953, 1013, 1073, 1134, 1194, 1254, 1314, 1375, + 1435, 1496, 1556, 1617, 1677, 1738, 1799, 1859, + 1920, 1981, 2042, 2103, 2164, 2225, 2287, 2348, + 2409, 2471, 2532, 2593, 2655, 2716, 2778, 2840, + 2902, 2963, 3025, 3087, 3149, 3211, 3273, 3335, + 3397, 3460, 3522, 3584, 3647, 3709, 3772, 3834, + 3897, 3960, 4022, 4085, 4148, 4211, 4274, 4337, + 4400, 4463, 4526, 4590, 4653, 4716, 4780, 4843, + 4907, 4971, 5034, 5098, 5162, 5226, 5289, 5353, + 5417, 5481, 5546, 5610, 5674, 5738, 5803, 5867, + 5932, 5996, 6061, 6125, 6190, 6255, 6320, 6384, + 6449, 6514, 6579, 6645, 6710, 6775, 6840, 6906, + 6971, 7037, 7102, 7168, 7233, 7299, 7365, 7431, + 7496, 7562, 7628, 7694, 7761, 7827, 7893, 7959, + 8026, 8092, 8159, 8225, 8292, 8358, 8425, 8492, + 8559, 8626, 8693, 8760, 8827, 8894, 8961, 9028, + 9096, 9163, 9230, 9298, 9366, 9433, 9501, 9569, + 9636, 9704, 9772, 9840, 9908, 9976, 10045, 10113, + 10181, 10250, 10318, 10386, 10455, 10524, 10592, 10661, + 10730, 10799, 10868, 10937, 11006, 11075, 11144, 11213, + 11283, 11352, 11421, 11491, 11560, 11630, 11700, 11769, + 11839, 11909, 11979, 12049, 12119, 12189, 12259, 12330, + 12400, 12470, 12541, 12611, 12682, 12752, 12823, 12894, + 12965, 13036, 13106, 13177, 13249, 13320, 13391, 13462, + 13533, 13605, 13676, 13748, 13819, 13891, 13963, 14035, + 14106, 14178, 14250, 14322, 14394, 14467, 14539, 14611, + 14684, 14756, 14829, 14901, 14974, 15046, 15119, 15192, + 15265, 15338, 15411, 15484, 15557, 15630, 15704, 15777, + 15850, 15924, 15997, 16071, 16145, 16218, 16292, 16366, + 16440, 16514, 16588, 16662, 16737, 16811, 16885, 16960, + 17034, 17109, 17183, 17258, 17333, 17408, 17483, 17557, + 17633, 17708, 17783, 17858, 17933, 18009, 18084, 18160, + 18235, 18311, 18387, 18462, 18538, 18614, 18690, 18766, + 18842, 18918, 18995, 19071, 19147, 19224, 19300, 19377, + 19454, 19530, 19607, 19684, 19761, 19838, 19915, 19992, + 20070, 20147, 20224, 20302, 20379, 20457, 20534, 20612, + 20690, 20768, 20846, 20924, 21002, 21080, 21158, 21236, + 21315, 21393, 21472, 21550, 21629, 21708, 21786, 21865, + 21944, 22023, 22102, 22181, 22260, 22340, 22419, 22498, + 22578, 22658, 22737, 22817, 22897, 22977, 23056, 23136, + 23216, 23297, 23377, 23457, 23537, 23618, 23698, 23779, + 23860, 23940, 24021, 24102, 24183, 24264, 24345, 24426, + 24507, 24589, 24670, 24752, 24833, 24915, 24996, 25078, + 25160, 25242, 25324, 25406, 25488, 25570, 25652, 25735, + 25817, 25900, 25982, 26065, 26148, 26230, 26313, 26396, + 26479, 26562, 26645, 26729, 26812, 26895, 26979, 27062, + 27146, 27230, 27313, 27397, 27481, 27565, 27649, 27733, + 27818, 27902, 27986, 28071, 28155, 28240, 28324, 28409, + 28494, 28579, 28664, 28749, 28834, 28919, 29005, 29090, + 29175, 29261, 29346, 29432, 29518, 29604, 29690, 29776, + 29862, 29948, 30034, 30120, 30207, 30293, 30380, 30466, + 30553, 30640, 30727, 30814, 30900, 30988, 31075, 31162, + 31249, 31337, 31424, 31512, 31599, 31687, 31775, 31863, + 31951, 32039, 32127, 32215, 32303, 32392, 32480, 32568, + 32657, 32746, 32834, 32923, 33012, 33101, 33190, 33279, + 33369, 33458, 33547, 33637, 33726, 33816, 33906, 33995, + 34085, 34175, 34265, 34355, 34446, 34536, 34626, 34717, + 34807, 34898, 34988, 35079, 35170, 35261, 35352, 35443, + 35534, 35626, 35717, 35808, 35900, 35991, 36083, 36175, + 36267, 36359, 36451, 36543, 36635, 36727, 36820, 36912, + 37004, 37097, 37190, 37282, 37375, 37468, 37561, 37654, + 37747, 37841, 37934, 38028, 38121, 38215, 38308, 38402, + 38496, 38590, 38684, 38778, 38872, 38966, 39061, 39155, + 39250, 39344, 39439, 39534, 39629, 39724, 39819, 39914, + 40009, 40104, 40200, 40295, 40391, 40486, 40582, 40678, + 40774, 40870, 40966, 41062, 41158, 41255, 41351, 41448, + 41544, 41641, 41738, 41835, 41932, 42029, 42126, 42223, + 42320, 42418, 42515, 42613, 42710, 42808, 42906, 43004, + 43102, 43200, 43298, 43396, 43495, 43593, 43692, 43790, + 43889, 43988, 44087, 44186, 44285, 44384, 44483, 44583, + 44682, 44781, 44881, 44981, 45081, 45180, 45280, 45381, + 45481, 45581, 45681, 45782, 45882, 45983, 46083, 46184, + 46285, 46386, 46487, 46588, 46690, 46791, 46892, 46994, + 47095, 47197, 47299, 47401, 47503, 47605, 47707, 47809, + 47912, 48014, 48117, 48219, 48322, 48425, 48528, 48631, + 48734, 48837, 48940, 49044, 49147, 49251, 49354, 49458, + 49562, 49666, 49770, 49874, 49978, 50082, 50187, 50291, + 50396, 50500, 50605, 50710, 50815, 50920, 51025, 51131, + 51236, 51341, 51447, 51552, 51658, 51764, 51870, 51976, + 52082, 52188, 52295, 52401, 52507, 52614, 52721, 52827, + 52934, 53041, 53148, 53256, 53363, 53470, 53578, 53685, + 53793, 53901, 54008, 54116, 54224, 54333, 54441, 54549, + 54658, 54766, 54875, 54983, 55092, 55201, 55310, 55419, + 55529, 55638, 55747, 55857, 55966, 56076, 56186, 56296, + 56406, 56516, 56626, 56736, 56847, 56957, 57068, 57179, + 57289, 57400, 57511, 57622, 57734, 57845, 57956, 58068, + 58179, 58291, 58403, 58515, 58627, 58739, 58851, 58964, + 59076, 59189, 59301, 59414, 59527, 59640, 59753, 59866, + 59979, 60092, 60206, 60319, 60433, 60547, 60661, 60774, + 60889, 61003, 61117, 61231, 61346, 61460, 61575, 61690, + 61805, 61920, 62035, 62150, 62265, 62381, 62496, 62612, + 62727, 62843, 62959, 63075, 63191, 63308, 63424, 63540, + 63657, 63774, 63890, 64007, 64124, 64241, 64358, 64476, + 64593, 64711, 64828, 64946, 65064, 65182, 65300, 65418, + ]; + private static ReadOnlySpan VolumeTable => + [ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 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 ReadOnlySpan FixedRests => + [ + 96, 72, 64, 48, 36, 32, 24, 18, 16, 12, 9, 8, 6, 4, 3, 2, + ]; public static bool IsStateRemovable(EnvelopeState state) { - return state is EnvelopeState.Two or >= EnvelopeState.Seven; + return state is EnvelopeState.Attack or >= EnvelopeState.End; } + + + #region FindChunk + internal 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" or "swdl": + { + r.Stream.Position += 0x4C; + break; + } + case "smdb" or "smdl": + { + r.Stream.Position += 0x3C; + break; + } + default: + { + Debug.WriteLine($"Ignoring {str} chunk"); + r.Stream.Position += 0x8; + uint length = r.ReadUInt32(); + r.Stream.Position += length; + r.Stream.Align(16); + break; + } + } + } + r.Stream.Position = oldPosition; + return pos; + } + #endregion + + 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/DSE/SMD.cs b/VG Music Studio - Core/NDS/DSE/SMD.cs index e9a90839..510e7cab 100644 --- a/VG Music Studio - Core/NDS/DSE/SMD.cs +++ b/VG Music Studio - Core/NDS/DSE/SMD.cs @@ -1,19 +1,23 @@ using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Util; +using System.Collections.Generic; +using System.IO; namespace Kermalis.VGMusicStudio.Core.NDS.DSE; internal sealed class SMD { + + #region Header public sealed class Header // Size 0x40 { - [BinaryStringFixedLength(4)] - public string Type { get; set; } = null!; // "smdb" or "smdl" - [BinaryArrayFixedLength(4)] - public byte[] Unknown1 { get; set; } = null!; + public string Type { get; set; } // "smdb" or "smdl" + public byte[] Padding1 { get; set; } public uint Length { get; set; } public ushort Version { get; set; } - [BinaryArrayFixedLength(10)] - public byte[] Unknown2 { get; set; } = null!; + public byte BankLo { get; set; } + public byte BankHi { get; set; } + public byte[] Padding2 { get; set; } public ushort Year { get; set; } public byte Month { get; set; } public byte Day { get; set; } @@ -21,40 +25,93 @@ public sealed class Header // Size 0x40 public byte Minute { get; set; } public byte Second { get; set; } public byte Centisecond { get; set; } - [BinaryStringFixedLength(16)] - public string Label { get; set; } = null!; - [BinaryArrayFixedLength(16)] - public byte[] Unknown3 { get; set; } = null!; + public string Label { get; set; } + public byte[] Unknown { get; set; } + public byte[] HeaderEndPadding { get; set; } + + public Header(EndianBinaryReader r) + { + Type = r.ReadString_Count(4); + + if (Type == "smdb") { r.Endianness = Endianness.BigEndian; } + + Padding1 = new byte[4]; + r.ReadBytes(Padding1); + + Length = r.ReadUInt32(); + + Version = r.ReadUInt16(); + + BankLo = r.ReadByte(); + BankHi = r.ReadByte(); + + Padding2 = new byte[8]; + r.ReadBytes(Padding2); + + r.Endianness = Endianness.LittleEndian; + + Year = r.ReadUInt16(); + + Month = r.ReadByte(); + + Day = r.ReadByte(); + + Hour = r.ReadByte(); + + Minute = r.ReadByte(); + + Second = r.ReadByte(); + + Centisecond = r.ReadByte(); + + Label = r.ReadString_Count(16); + + Unknown = new byte[8]; + r.ReadBytes(Unknown); + + HeaderEndPadding = new byte[8]; + r.ReadBytes(HeaderEndPadding); + + if (Type == "smdb") { r.Endianness = Endianness.BigEndian; } + } } + #endregion + #region SongChunk public interface ISongChunk { byte NumTracks { get; } } - public sealed class SongChunk_V402 : ISongChunk // Size 0x20 + public sealed class SongChunk : ISongChunk // Size 0x40 { - [BinaryStringFixedLength(4)] - public string Type { get; set; } = null!; - [BinaryArrayFixedLength(16)] - public byte[] Unknown1 { get; set; } = null!; + public string Type { get; set; } + public byte[] Unknown1 { get; set; } + public ushort TicksPerQuarter { get; set; } + public byte[] Unknown2 { get; set; } public byte NumTracks { get; set; } - public byte NumChannels { get; set; } - [BinaryArrayFixedLength(4)] - public byte[] Unknown2 { get; set; } = null!; - public sbyte MasterVolume { get; set; } - public sbyte MasterPanpot { get; set; } - [BinaryArrayFixedLength(4)] - public byte[] Unknown3 { get; set; } = null!; - } - public sealed class SongChunk_V415 : ISongChunk // Size 0x40 - { - [BinaryStringFixedLength(4)] - public string Type { get; set; } = null!; - [BinaryArrayFixedLength(18)] - public byte[] Unknown1 { get; set; } = null!; - public byte NumTracks { get; set; } - public byte NumChannels { get; set; } - [BinaryArrayFixedLength(40)] - public byte[] Unknown2 { get; set; } = null!; + public byte Channel { get; set; } + public byte[] Unknown3 { get; set; } + + public SongChunk(EndianBinaryReader r) + { + Type = r.ReadString_Count(4); + + Unknown1 = new byte[14]; + r.ReadBytes(Unknown1); + + TicksPerQuarter = r.ReadUInt16(); + + Unknown2 = new byte[2]; + r.ReadBytes(Unknown2); + + NumTracks = r.ReadByte(); + + Channel = r.ReadByte(); + + Unknown3 = new byte[40]; + r.ReadBytes(Unknown3); + } } + #endregion + } diff --git a/VG Music Studio - Core/NDS/DSE/SWD.cs b/VG Music Studio - Core/NDS/DSE/SWD.cs index 90c28ad9..3f9b5cb8 100644 --- a/VG Music Studio - Core/NDS/DSE/SWD.cs +++ b/VG Music Studio - Core/NDS/DSE/SWD.cs @@ -1,42 +1,30 @@ using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Codec; using Kermalis.VGMusicStudio.Core.Util; using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; namespace Kermalis.VGMusicStudio.Core.NDS.DSE; internal sealed class SWD { + #region Header public interface IHeader { // } - private sealed class Header_V402 : IHeader // Size 0x40 + public class Header : IHeader // Size 0x40 { - [BinaryArrayFixedLength(8)] - public byte[] Unknown1 { get; set; } = null!; - public ushort Year { get; set; } - public byte Month { get; set; } - public byte Day { get; set; } - public byte Hour { get; set; } - public byte Minute { get; set; } - public byte Second { get; set; } - public byte Centisecond { get; set; } - [BinaryStringFixedLength(16)] - public string Label { get; set; } = null!; - [BinaryArrayFixedLength(22)] - public byte[] Unknown2 { get; set; } = null!; - public byte NumWAVISlots { get; set; } - public byte NumPRGISlots { get; set; } - public byte NumKeyGroups { get; set; } - [BinaryArrayFixedLength(7)] - public byte[] Padding { get; set; } = null!; - } - private sealed class Header_V415 : IHeader // Size 0x40 - { - [BinaryArrayFixedLength(8)] - public byte[] Unknown1 { get; set; } = null!; + public string Type { get; set; } + public byte[]? Unknown1 { get; set; } + public uint Length { get; set; } + public ushort Version { get; set; } + public byte BankID { get; set; } + public byte WaveID { get; set; } + public byte[]? Padding { get; set; } public ushort Year { get; set; } public byte Month { get; set; } public byte Day { get; set; } @@ -44,159 +32,477 @@ private sealed class Header_V415 : IHeader // Size 0x40 public byte Minute { get; set; } public byte Second { get; set; } public byte Centisecond { get; set; } - [BinaryStringFixedLength(16)] - public string Label { get; set; } = null!; - [BinaryArrayFixedLength(16)] - public byte[] Unknown2 { get; set; } = null!; + public string Label { get; set; } + public byte[]? Unknown3 { get; set; } public uint PCMDLength { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown3 { get; set; } = null!; + public byte[]? Unknown4 { get; set; } public ushort NumWAVISlots { get; set; } public ushort NumPRGISlots { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown4 { get; set; } = null!; + public byte NumKeyGroups { get; set; } + public byte[]? Unknown5 { get; set; } public uint WAVILength { get; set; } + public byte[]? HeaderEndPadding { get; set; } // TODO: Check if there's anything other than padding in this array + + public Header(EndianBinaryReader r) + { + // File type metadata - The file type, version, and size of the file + Type = r.ReadString_Count(4); + if (Type.StartsWith("swd") == false) // Failsafe, to check if the file is a valid SWD file + { + throw new InvalidDataException("Invalid Data Exception:\nThis file is not a Wave Data (.SWD) file, please make sure the file extension is correct before opening.\nCall Stack:"); + } + if (Type == "swdb") + { + r.Endianness = Endianness.BigEndian; + } + Unknown1 = new byte[4]; + r.ReadBytes(Unknown1); + Length = r.ReadUInt32(); + Version = r.ReadUInt16(); + BankID = r.ReadByte(); + WaveID = r.ReadByte(); + + // Timestamp metadata - The time the SWD was published + r.Endianness = Endianness.LittleEndian; // Timestamp is always Little Endian, regardless of version or type, so it must be set to Little Endian to be read + + Padding = new byte[8]; // Padding + r.ReadBytes(Padding); + Year = r.ReadUInt16(); // Year + Month = r.ReadByte(); // Month + Day = r.ReadByte(); // Day + Hour = r.ReadByte(); // Hour + Minute = r.ReadByte(); // Minute + Second = r.ReadByte(); // Second + Centisecond = r.ReadByte(); // Centisecond + if (Type == "swdb") { r.Endianness = Endianness.BigEndian; } // If type is swdb, restore back to Big Endian + + + // Info table + Label = r.ReadString_Count(16); + + switch (Version) // To ensure the version differences apply beyond this point + { + case 1026: + { + Unknown3 = new byte[22]; + r.ReadBytes(Unknown3); + + NumWAVISlots = r.ReadByte(); + + NumPRGISlots = r.ReadByte(); + + NumKeyGroups = r.ReadByte(); + + HeaderEndPadding = new byte[7]; + r.ReadBytes(HeaderEndPadding); + + break; + } + case 1045: + { + Unknown3 = new byte[16]; + r.ReadBytes(Unknown3); + + PCMDLength = r.ReadUInt32(); + + Unknown4 = new byte[2]; + r.ReadBytes(Unknown4); + + NumWAVISlots = r.ReadUInt16(); + + NumPRGISlots = r.ReadUInt16(); + + Unknown5 = new byte[2]; + r.ReadBytes(Unknown5); + + WAVILength = r.ReadUInt32(); + + break; + } + } + } } - public interface ISplitEntry + public class ChunkHeader : IHeader // Size 0x10 { - 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 string Name { get; set; } + public byte[] Padding { get; set; } + public ushort Version { get; set; } + public uint ChunkBegin { get; set; } + public uint ChunkEnd { get; set; } + + public ChunkHeader(EndianBinaryReader r, long chunkOffset, SWD swd) + { + long oldOffset = r.Stream.Position; + r.Stream.Position = chunkOffset; + + // Chunk Name + Name = r.ReadString_Count(4); + + // Padding + Padding = new byte[2]; + r.ReadBytes(Padding); + + // Version + Version = r.ReadUInt16(); + + // Chunk Begin + r.Endianness = Endianness.LittleEndian; // To ensure this is read in Little Endian in all versions and types + ChunkBegin = r.ReadUInt32(); + if (swd.Type == "swdb") { r.Endianness = Endianness.BigEndian; } // To revert back to Big Endian when the type is "swdb" + + // Chunk End + ChunkEnd = r.ReadUInt32(); + + r.Stream.Position = oldOffset; + } } - public sealed class SplitEntry_V402 : ISplitEntry // Size 0x30 - { - public ushort Id { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } = null!; - public byte LowKey { get; set; } - public byte HighKey { get; set; } - public byte LowKey2 { get; set; } - public byte HighKey2 { get; set; } - public byte LowVelocity { get; set; } - public byte HighVelocity { get; set; } - public byte LowVelocity2 { get; set; } - public byte HighVelocity2 { get; set; } - [BinaryArrayFixedLength(5)] - public byte[] Unknown2 { get; set; } = null!; - public byte SampleId { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown3 { get; set; } = null!; - public byte SampleRootKey { get; set; } - public sbyte SampleTranspose { get; set; } - public byte SampleVolume { get; set; } - public sbyte SamplePanpot { get; set; } - public byte KeyGroupId { get; set; } - [BinaryArrayFixedLength(15)] - public byte[] Unknown4 { get; set; } = null!; - public byte AttackVolume { get; set; } - public byte Attack { get; set; } - public byte Decay { get; set; } - public byte Sustain { get; set; } - public byte Hold { get; set; } - public byte Decay2 { get; set; } - public byte Release { get; set; } - public byte Unknown5 { get; set; } + #endregion - [BinaryIgnore] - int ISplitEntry.SampleId => SampleId; + #region SplitEntry + public interface ISplitEntry + { + sbyte LowKey { get; } + sbyte HighKey { get; } + ushort SampleId { get; } + sbyte SampleRootKey { get; } + sbyte SampleTranspose { get; } + byte EnvelopeVolume { get; } + byte EnvelopeMultiplier { get; } + byte AttackVolume { get; } + byte AttackTime { get; } + byte Decay { get; } + byte Sustain { get; } + byte Hold { get; } + byte Fade { get; } + byte Release { get; } } - public sealed class SplitEntry_V415 : ISplitEntry // 0x30 + public class SplitEntry : ISplitEntry // 0x30 { public ushort Id { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } = null!; - public byte LowKey { get; set; } - public byte HighKey { get; set; } - public byte LowKey2 { get; set; } - public byte HighKey2 { get; set; } - public byte LowVelocity { get; set; } - public byte HighVelocity { get; set; } - public byte LowVelocity2 { get; set; } - public byte HighVelocity2 { get; set; } - [BinaryArrayFixedLength(6)] - public byte[] Unknown2 { get; set; } = null!; + public byte BendRange { get; set; } + public bool Enabled { get; set; } + public sbyte LowKey { get; set; } + public sbyte HighKey { get; set; } + public sbyte LowKey2 { get; set; } + public sbyte HighKey2 { get; set; } + public sbyte LowVelocity { get; set; } + public sbyte HighVelocity { get; set; } + public sbyte LowVelocity2 { get; set; } + public sbyte HighVelocity2 { get; set; } + public byte[]? Padding { get; set; } + public byte[]? Padding2 { get; set; } + public byte[]? Unknown3 { get; set; } public ushort SampleId { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown3 { get; set; } = null!; - public byte SampleRootKey { get; set; } + public byte FineTune { get; set; } + public sbyte CoarseTune { get; set; } + public byte[] Unknown4 { get; set; } + public sbyte SampleRootKey { get; set; } public sbyte SampleTranspose { get; set; } - public byte SampleVolume { get; set; } + public sbyte SampleVolume { get; set; } public sbyte SamplePanpot { get; set; } public byte KeyGroupId { get; set; } - [BinaryArrayFixedLength(13)] - public byte[] Unknown4 { get; set; } = null!; + public byte KeyGroupFlag { get; set; } + public ushort UnusedValue { get; set; } + public byte EnvelopeVolume { get; set; } + public byte EnvelopeMultiplier { get; set; } + public byte[]? Unknown5 { get; set; } public byte AttackVolume { get; set; } - public byte Attack { get; set; } + public byte AttackTime { get; set; } public byte Decay { get; set; } public byte Sustain { get; set; } public byte Hold { get; set; } - public byte Decay2 { get; set; } + public byte Fade { get; set; } public byte Release { get; set; } - public byte Unknown5 { get; set; } + public byte Break { get; set; } + + ushort ISplitEntry.SampleId => SampleId; - [BinaryIgnore] - int ISplitEntry.SampleId => SampleId; + public SplitEntry(EndianBinaryReader r, SWD swd) + { + if (swd.Type == "swdl") + { + r.Endianness = Endianness.BigEndian; + Id = r.ReadUInt16(); // ID for the SplitEntry is always read in Big Endian format + r.Endianness = Endianness.LittleEndian; + } + else + { + Id = r.ReadUInt16(); + } + + BendRange = r.ReadByte(); + + Enabled = r.ReadBoolean(); + + LowKey = r.ReadSByte(); + + HighKey = r.ReadSByte(); + + LowKey2 = r.ReadSByte(); + + HighKey2 = r.ReadSByte(); + + LowVelocity = r.ReadSByte(); + + HighVelocity = r.ReadSByte(); + + LowVelocity2 = r.ReadSByte(); + + HighVelocity2 = r.ReadSByte(); + + switch (swd.Version) + { + case 1026: + { + Padding = new byte[5]; + r.ReadBytes(Padding); + + SampleId = r.ReadByte(); + + FineTune = r.ReadByte(); + + CoarseTune = r.ReadSByte(); + + SampleRootKey = r.ReadSByte(); + + SampleTranspose = r.ReadSByte(); + + SampleVolume = r.ReadSByte(); + + SamplePanpot = r.ReadSByte(); + + KeyGroupId = r.ReadByte(); + + KeyGroupFlag = r.ReadByte(); + + UnusedValue = r.ReadUInt16(); + + Padding2 = new byte[4]; + r.ReadBytes(Padding2); + + EnvelopeVolume = r.ReadByte(); + + EnvelopeMultiplier = r.ReadByte(); + + Unknown4 = new byte[6]; + r.ReadBytes(Unknown4); + + AttackVolume = r.ReadByte(); + + AttackTime = r.ReadByte(); + + Decay = r.ReadByte(); + + Sustain = r.ReadByte(); + + Hold = r.ReadByte(); + + Fade = r.ReadByte(); + + Release = r.ReadByte(); + + Break = r.ReadByte(); + + break; + } + case 1045: + { + Padding = new byte[6]; + r.ReadBytes(Padding); + + SampleId = r.ReadUInt16(); + + FineTune = r.ReadByte(); + + CoarseTune = r.ReadSByte(); + + SampleRootKey = r.ReadSByte(); + + SampleTranspose = r.ReadSByte(); + + SampleVolume = r.ReadSByte(); + + SamplePanpot = r.ReadSByte(); + + KeyGroupId = r.ReadByte(); + + KeyGroupFlag = r.ReadByte(); + + UnusedValue = r.ReadUInt16(); + + Padding2 = new byte[2]; + r.ReadBytes(Padding2); + + EnvelopeVolume = r.ReadByte(); + + EnvelopeMultiplier = r.ReadByte(); + + Unknown4 = new byte[6]; + r.ReadBytes(Unknown4); + + AttackVolume = r.ReadByte(); + + AttackTime = r.ReadByte(); + + Decay = r.ReadByte(); + + Sustain = r.ReadByte(); + + Hold = r.ReadByte(); + + Fade = r.ReadByte(); + + Release = r.ReadByte(); + + Break = r.ReadByte(); + + break; + } + + // In the event that there's a SWD version that hasn't been discovered yet + default: throw new NotImplementedException("This version of the SWD specification has not been implemented into VG Music Studio."); + } + } } + #endregion + #region ProgramInfo public interface IProgramInfo { ISplitEntry[] SplitEntries { get; } } - public sealed class ProgramInfo_V402 : IProgramInfo - { - public byte Id { get; set; } - public byte NumSplits { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } = null!; - public byte Volume { get; set; } - public byte Panpot { get; set; } - [BinaryArrayFixedLength(5)] - public byte[] Unknown2 { get; set; } = null!; - public byte NumLFOs { get; set; } - [BinaryArrayFixedLength(4)] - public byte[] Unknown3 { get; set; } = null!; - [BinaryArrayFixedLength(16)] - public KeyGroup[] KeyGroups { get; set; } = null!; - [BinaryArrayVariableLength(nameof(NumLFOs))] - public LFOInfo LFOInfos { get; set; } = null!; - [BinaryArrayVariableLength(nameof(NumSplits))] - public SplitEntry_V402[] SplitEntries { get; set; } = null!; - - [BinaryIgnore] - ISplitEntry[] IProgramInfo.SplitEntries => SplitEntries; - } - public sealed class ProgramInfo_V415 : IProgramInfo + public class ProgramInfo : IProgramInfo { public ushort Id { get; set; } public ushort NumSplits { get; set; } + public byte[]? Unknown1 { get; set; } public byte Volume { get; set; } public byte Panpot { get; set; } - [BinaryArrayFixedLength(5)] - public byte[] Unknown1 { get; set; } = null!; + public byte[] Unknown2 { get; set; } public byte NumLFOs { get; set; } - [BinaryArrayFixedLength(4)] - public byte[] Unknown2 { get; set; } = null!; - [BinaryArrayVariableLength(nameof(NumLFOs))] - public LFOInfo[] LFOInfos { get; set; } = null!; - [BinaryArrayFixedLength(16)] - public byte[] Unknown3 { get; set; } = null!; - [BinaryArrayVariableLength(nameof(NumSplits))] - public SplitEntry_V415[] SplitEntries { get; set; } = null!; - - [BinaryIgnore] + public byte[] HeaderPadding { get; set; } + public LFOInfo[] LFOInfos { get; set; } + public byte[]? LFOPadding { get; set; } + public KeyGroup[]? KeyGroups { get; set; } + public SplitEntry[] SplitEntries { get; set; } + ISplitEntry[] IProgramInfo.SplitEntries => SplitEntries; + + public ProgramInfo(EndianBinaryReader r, SWD swd) + { + switch (swd.Version) + { + case 1026: + { + Id = r.ReadByte(); + + if (swd.Type == "swdb") + { + r.Endianness = Endianness.LittleEndian; + NumSplits = r.ReadUInt16(); + r.Endianness = Endianness.BigEndian; + } + else + { + NumSplits = r.ReadUInt16(); + } + + Unknown1 = new byte[2]; + r.ReadBytes(Unknown1); + + Volume = r.ReadByte(); + + Panpot = r.ReadByte(); + + Unknown2 = new byte[5]; + r.ReadBytes(Unknown2); + + NumLFOs = r.ReadByte(); + + HeaderPadding = new byte[4]; + r.ReadBytes(HeaderPadding); + + KeyGroups = new KeyGroup[16]; + + LFOInfos = new LFOInfo[NumLFOs]; + for (int i = 0; i < NumLFOs; i++) + { + LFOInfos[i] = new LFOInfo(r); + } + + SplitEntries = new SplitEntry[NumSplits]; + for (int i = 0; i < NumSplits; i++) + { + SplitEntries[i] = new SplitEntry(r, swd); + if (SplitEntries[i].Id != i) + { + throw new DSEArrayIndexAndHeaderIDMismatchException(i, SplitEntries[i].Id); + } + } + + break; + } + + case 1045: + { + Id = r.ReadUInt16(); + + if (swd.Type == "swdb") + { + r.Endianness = Endianness.LittleEndian; + NumSplits = r.ReadUInt16(); // NumSplits is always read in Little Endian format + r.Endianness = Endianness.BigEndian; + } + else + { + NumSplits = r.ReadUInt16(); + } + + Volume = r.ReadByte(); + + Panpot = r.ReadByte(); + + Unknown2 = new byte[5]; + r.ReadBytes(Unknown2); + + NumLFOs = r.ReadByte(); + + HeaderPadding = new byte[4]; + r.ReadBytes(HeaderPadding); + + LFOInfos = new LFOInfo[NumLFOs]; + for (int i = 0; i < NumLFOs; i++) + { + LFOInfos[i] = new LFOInfo(r); + } + + LFOPadding = new byte[16]; + r.ReadBytes(LFOPadding); + + SplitEntries = new SplitEntry[NumSplits]; + for (int i = 0; i < NumSplits; i++) + { + SplitEntries[i] = new SplitEntry(r, swd); + if (SplitEntries[i].Id != i) + { + throw new DSEArrayIndexAndHeaderIDMismatchException(i, SplitEntries[i].Id); + } + } + + break; + } + + // In the event that there's a version that hasn't been discovered yet + default: throw new NotImplementedException("This Digital Sound Elements version has not been implemented into VG Music Studio."); + } + + } + } + #endregion + #region WavInfo public interface IWavInfo { byte RootNote { get; } @@ -209,59 +515,24 @@ public interface IWavInfo uint LoopEnd { get; } byte EnvMult { get; } byte AttackVolume { get; } - byte Attack { get; } + byte AttackTime { get; } byte Decay { get; } byte Sustain { get; } byte Hold { get; } - byte Decay2 { get; } + byte Fade { get; } byte Release { get; } } - public sealed class WavInfo_V402 : IWavInfo // Size 0x40 - { - public byte Unknown1 { get; set; } - public byte Id { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown2 { get; set; } = null!; - public byte RootNote { get; set; } - public sbyte Transpose { get; set; } - public byte Volume { get; set; } - public sbyte Panpot { get; set; } - public SampleFormat SampleFormat { get; set; } - [BinaryArrayFixedLength(7)] - public byte[] Unknown3 { get; set; } = null!; - public bool Loop { get; set; } - public uint SampleRate { get; set; } - public uint SampleOffset { get; set; } - public uint LoopStart { get; set; } - public uint LoopEnd { get; set; } - [BinaryArrayFixedLength(16)] - public byte[] Unknown4 { get; set; } = null!; - public byte EnvOn { get; set; } - public byte EnvMult { get; set; } - [BinaryArrayFixedLength(6)] - public byte[] Unknown5 { get; set; } = null!; - public byte AttackVolume { get; set; } - public byte Attack { get; set; } - public byte Decay { get; set; } - public byte Sustain { get; set; } - public byte Hold { get; set; } - public byte Decay2 { get; set; } - public byte Release { get; set; } - public byte Unknown6 { get; set; } - } - public sealed class WavInfo_V415 : IWavInfo // 0x40 + + public class WavInfo : IWavInfo // Size 0x40 { - [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } = null!; + public byte[] Entry { get; set; } public ushort Id { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown2 { get; set; } = null!; + public byte[] Unknown2 { get; set; } public byte RootNote { get; set; } public sbyte Transpose { get; set; } public byte Volume { get; set; } public sbyte Panpot { get; set; } - [BinaryArrayFixedLength(6)] - public byte[] Unknown3 { get; set; } = null!; + public byte[] Unknown3 { get; set; } public ushort Version { get; set; } public SampleFormat SampleFormat { get; set; } public byte Unknown4 { get; set; } @@ -270,35 +541,270 @@ public sealed class WavInfo_V415 : IWavInfo // 0x40 public byte SamplesPer32Bits { get; set; } public byte Unknown6 { get; set; } public byte BitDepth { get; set; } - [BinaryArrayFixedLength(6)] - public byte[] Unknown7 { get; set; } = null!; + 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; } = null!; + public byte[] Unknown8 { get; set; } public byte AttackVolume { get; set; } - public byte Attack { get; set; } + public byte AttackTime { get; set; } public byte Decay { get; set; } public byte Sustain { get; set; } public byte Hold { get; set; } - public byte Decay2 { get; set; } + public byte Fade { get; set; } public byte Release { get; set; } - public byte Unknown9 { get; set; } + public byte Break { get; set; } + + public WavInfo(EndianBinaryReader r, SWD swd) + { + // SWD version format check + switch (swd.Version) + { + + case 1026: + { + // The wave table Entry Variable + Entry = new byte[1]; // Specify a variable with a byte array before doing EndianBinaryReader.ReadBytes() + r.ReadBytes(Entry); // Reads the byte + + // Wave ID + Id = r.ReadByte(); // Reads the ID of the wave sample + + // Currently undocumented variable(s) + Unknown2 = new byte[2]; // Specify a variable with a byte array before doing EndianBinaryReader.ReadBytes() + r.ReadBytes(Unknown2); // Reads the bytes + + // Root Note + RootNote = r.ReadByte(); + + // Transpose + Transpose = r.ReadSByte(); + + // Volume + Volume = r.ReadByte(); + + // Panpot + Panpot = r.ReadSByte(); + + // Sample Format + if (swd.Type == "swdb") + { + r.Endianness = Endianness.LittleEndian; + SampleFormat = (SampleFormat)r.ReadUInt16(); + r.Endianness = Endianness.BigEndian; + } + else + { + r.Endianness = Endianness.BigEndian; + SampleFormat = (SampleFormat)r.ReadUInt16(); + r.Endianness = Endianness.LittleEndian; + } + + // Undocumented variable(s) + Unknown3 = new byte[7]; + r.ReadBytes(Unknown3); + + // Version + Version = r.ReadUInt16(); + + // Loop enable and disable + Loop = r.ReadBoolean(); + + // Sample Rate + SampleRate = r.ReadUInt32(); + + // Sample Offset + SampleOffset = r.ReadUInt32(); + + // Loop Start + LoopStart = r.ReadUInt32(); + + // Loop End + LoopEnd = r.ReadUInt32(); + + // Undocumented variable(s) + Unknown7 = new byte[16]; + r.ReadBytes(Unknown7); + + // Volume Envelop On + EnvOn = r.ReadByte(); + + // Volume Envelop Multiplier + EnvMult = r.ReadByte(); + + // Undocumented variable(s) + Unknown8 = new byte[6]; + r.ReadBytes(Unknown8); + + // Attack Volume + AttackVolume = r.ReadByte(); + + // Attack + AttackTime = r.ReadByte(); + + // Decay + Decay = r.ReadByte(); + + // Sustain + Sustain = r.ReadByte(); + + // Hold + Hold = r.ReadByte(); + + // Fade + Fade = r.ReadByte(); + + // Release + Release = r.ReadByte(); + + // The wave table Break Variable + Break = r.ReadByte(); + + break; + } + + case 1045: // Digital Sound Elements - SWD Specification 4.21 + { + // The wave table Entry Variable + Entry = new byte[2]; // Specify a variable with a byte array before doing EndianBinaryReader.ReadBytes() + r.ReadBytes(Entry); // Reads the bytes + + // Wave ID + r.Endianness = Endianness.LittleEndian; // Changes the reader to Little Endian + Id = r.ReadUInt16(); // Reads the ID of the wave sample as Little Endian + if (swd.Type == "swdb") // Checks if the str string value matches "swdb" + { + r.Endianness = Endianness.BigEndian; // Restores the reader back to Big Endian + } + + // Currently undocumented variable + Unknown2 = new byte[2]; // Same as the one before + r.ReadBytes(Unknown2); + + // Root Note + RootNote = r.ReadByte(); + + // Transpose + Transpose = r.ReadSByte(); + + // Volume + Volume = r.ReadByte(); + + // Panpot + Panpot = r.ReadSByte(); + + // Undocumented variable + Unknown3 = new byte[6]; // Same as before, except we need to read 6 bytes instead of 2 + r.ReadBytes(Unknown3); + + // Version + Version = r.ReadUInt16(); + + // Sample Format + if (swd.Type == "swdb") + { + r.Endianness = Endianness.LittleEndian; + SampleFormat = (SampleFormat)r.ReadUInt16(); + r.Endianness = Endianness.BigEndian; + } + else + { + r.Endianness = Endianness.BigEndian; + SampleFormat = (SampleFormat)r.ReadUInt16(); + r.Endianness = Endianness.LittleEndian; + } + + // Undocumented variable(s) + Unknown4 = r.ReadByte(); + + // Loop enable or disable + Loop = r.ReadBoolean(); + + // Undocumented variable(s) + Unknown5 = r.ReadByte(); + + // Samples per 32 bits + SamplesPer32Bits = r.ReadByte(); + + // Undocumented variable(s) + Unknown6 = r.ReadByte(); + + // Bit Depth + BitDepth = r.ReadByte(); + + // Undocumented variable(s) + Unknown7 = new byte[6]; // Once again, create a variable to specify 6 bytes and to read using it + r.ReadBytes(Unknown7); + + // Sample Rate + SampleRate = r.ReadUInt32(); + + // Sample Offset + SampleOffset = r.ReadUInt32(); + + // Loop Start + LoopStart = r.ReadUInt32(); + + // Loop End + LoopEnd = r.ReadUInt32(); + + // Volume Envelop On + EnvOn = r.ReadByte(); + + // Volume Envelop Multiplier + EnvMult = r.ReadByte(); + + // Undocumented variable(s) + Unknown8 = new byte[6]; // Same as before + r.ReadBytes(Unknown8); + + // Attack Volume + AttackVolume = r.ReadByte(); + + // Attack + AttackTime = r.ReadByte(); + + // Decay + Decay = r.ReadByte(); + + // Sustain + Sustain = r.ReadByte(); + + // Hold + Hold = r.ReadByte(); + + // Fade + Fade = r.ReadByte(); + + // Release + Release = r.ReadByte(); + + // The wave table Break Variable + Break = r.ReadByte(); + + break; + } + + // In the event that there's a version that hasn't been discovered yet + default: throw new NotImplementedException("This version of the SWD specification has not yet been implemented into VG Music Studio."); + } + } } + #endregion public class SampleBlock { - public IWavInfo WavInfo = null!; - public byte[] Data = null!; + public WavInfo? WavInfo; + public DSPADPCM DSPADPCM; + public byte[]? Data; } public class ProgramBank { - public IProgramInfo?[] ProgramInfos = null!; - public KeyGroup[] KeyGroups = null!; + public ProgramInfo[]? ProgramInfos; + public KeyGroup[]? KeyGroups; } public class KeyGroup // Size 0x8 { @@ -308,186 +814,220 @@ public class KeyGroup // Size 0x8 public byte LowNote { get; set; } public byte HighNote { get; set; } public ushort Unknown { get; set; } + + public KeyGroup(EndianBinaryReader r, SWD swd) + { + r.Endianness = Endianness.LittleEndian; + Id = r.ReadUInt16(); + if (swd.Type == "swdb") { r.Endianness = Endianness.BigEndian; } + + Poly = r.ReadByte(); + + Priority = r.ReadByte(); + + LowNote = r.ReadByte(); + + HighNote = r.ReadByte(); + + Unknown = r.ReadUInt16(); + } } - public sealed class LFOInfo + public class LFOInfo(EndianBinaryReader r) { - 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 byte Entry { get; set; } = r.ReadByte(); + public byte HasData { get; set; } = r.ReadByte(); + public ModulationType ModulationType { get; set; } = (ModulationType)r.ReadByte(); + public WaveformType WaveformType { get; set; } = (WaveformType)r.ReadByte(); + public ushort Rate { get; set; } = r.ReadUInt16(); + public ushort Unused { get; set; } = r.ReadUInt16(); + public ushort Depth { get; set; } = r.ReadUInt16(); + public ushort Delay { get; set; } = r.ReadUInt16(); + public short Fade { get; set; } = r.ReadInt16(); + public ushort Break { get; set; } = r.ReadUInt16(); } + public string FileName; + public Header? Info; public string Type; // "swdb" or "swdl" - public byte[] Unknown1; public uint Length; public ushort Version; - public IHeader Header; - public byte[] Unknown2; + + public long WaviChunkOffset, WaviDataOffset, + PrgiChunkOffset, PrgiDataOffset, + KgrpChunkOffset, KgrpDataOffset, + PcmdChunkOffset, PcmdDataOffset, + EodChunkOffset; + public ChunkHeader? WaviInfo, PrgiInfo, KgrpInfo, PcmdInfo, EodInfo; public ProgramBank? Programs; public SampleBlock[]? Samples; public SWD(string path) { - using (FileStream stream = File.OpenRead(path)) - { - var r = new EndianBinaryReader(stream, ascii: true); + FileName = new FileInfo(path).Name; + var stream = File.OpenRead(path); + var r = new EndianBinaryReader(stream, ascii: true); + Info = new Header(r); + Type = Info.Type; + Length = Info.Length; + Version = Info.Version; + Programs = ReadPrograms(r, Info.NumPRGISlots, this); - Type = r.ReadString_Count(4); - Unknown1 = new byte[4]; - r.ReadBytes(Unknown1); - Length = r.ReadUInt32(); - Version = r.ReadUInt16(); - Unknown2 = new byte[2]; - r.ReadBytes(Unknown2); - - switch (Version) - { - case 0x402: + switch (Version) + { + case 0x402: { - Header_V402 header = r.ReadObject(); - Header = header; - Programs = ReadPrograms(r, header.NumPRGISlots); - Samples = ReadSamples(r, header.NumWAVISlots); + Samples = ReadSamples(r, Info.NumWAVISlots, this); break; } - case 0x415: + case 0x415: { - Header_V415 header = r.ReadObject(); - Header = header; - Programs = ReadPrograms(r, header.NumPRGISlots); - if (header.PCMDLength != 0 && (header.PCMDLength & 0xFFFF0000) != 0xAAAA0000) + if (Info.PCMDLength != 0 && (Info.PCMDLength & 0xFFFF0000) != 0xAAAA0000) { - Samples = ReadSamples(r, header.NumWAVISlots); + Samples = ReadSamples(r, Info.NumWAVISlots, this); } break; } - default: throw new InvalidDataException(); - } + default: throw new InvalidDataException(); } + return; } - private static long FindChunk(EndianBinaryReader r, string chunk) + #region SampleBlock + private SampleBlock[] ReadSamples(EndianBinaryReader r, int numWAVISlots, SWD swd) { - long pos = -1; - long oldPosition = r.Stream.Position; - r.Stream.Position = 0; - - while (r.Stream.Position < r.Stream.Length) - { - string str = r.ReadString_Count(4); - if (str == chunk) - { - pos = r.Stream.Position - 4; - break; - } - - switch (str) - { - case "swdb": - case "swdl": - { - r.Stream.Position += 0x4C; - break; - } - default: - { - Debug.WriteLine($"Ignoring {str} chunk"); - r.Stream.Position += 0x8; - uint length = r.ReadUInt32(); - r.Stream.Position += length; - r.Stream.Align(16); - break; - } - } - } - - r.Stream.Position = oldPosition; - return pos; - } - - private static SampleBlock[] ReadSamples(EndianBinaryReader r, int numWAVISlots) - where T : IWavInfo, new() - { - long waviChunkOffset = FindChunk(r, "wavi"); - long pcmdChunkOffset = FindChunk(r, "pcmd"); + // These apply the chunk offsets that are found to both local and the field functions, chunk header constructors are available here incase they're needed + long waviChunkOffset = swd.WaviChunkOffset = DSEUtils.FindChunk(r, "wavi"); + long pcmdChunkOffset = swd.PcmdChunkOffset = DSEUtils.FindChunk(r, "pcmd"); + long eodChunkOffset = swd.EodChunkOffset = DSEUtils.FindChunk(r, "eod "); if (waviChunkOffset == -1 || pcmdChunkOffset == -1) { throw new InvalidDataException(); } else { - waviChunkOffset += 0x10; - pcmdChunkOffset += 0x10; + WaviInfo = new ChunkHeader(r, waviChunkOffset, swd); + long waviDataOffset = WaviDataOffset = waviChunkOffset + 0x10; + PcmdInfo = new ChunkHeader(r, pcmdChunkOffset, swd); + long pcmdDataOffset = PcmdDataOffset = pcmdChunkOffset + 0x10; + EodInfo = new ChunkHeader(r, eodChunkOffset, swd); var samples = new SampleBlock[numWAVISlots]; for (int i = 0; i < numWAVISlots; i++) { - r.Stream.Position = waviChunkOffset + (2 * i); + r.Stream.Position = waviDataOffset + (2 * i); ushort offset = r.ReadUInt16(); if (offset != 0) { - r.Stream.Position = offset + waviChunkOffset; - T wavInfo = r.ReadObject(); - samples[i] = new SampleBlock + r.Stream.Position = offset + waviDataOffset; + var wavInfo = new WavInfo(r, swd); + if (wavInfo.Id != i) + { + throw new DSEArrayIndexAndHeaderIDMismatchException(i, wavInfo.Id); + } + switch (Type) { - WavInfo = wavInfo, - Data = new byte[(int)((wavInfo.LoopStart + wavInfo.LoopEnd) * 4)], - }; - r.Stream.Position = pcmdChunkOffset + wavInfo.SampleOffset; - r.ReadBytes(samples[i].Data); + case "swdm": + { + throw new NotImplementedException("This Digital Sound Elements type has not yet been implemented."); + } + + case "swdl": + { + samples[i] = new SampleBlock + { + WavInfo = wavInfo, + Data = new byte[(int)((wavInfo.LoopStart + wavInfo.LoopEnd) * 4)], + }; + r.Stream.Position = pcmdDataOffset + wavInfo.SampleOffset; + r.ReadBytes(samples[i].Data); + + break; + } + + case "swdb": + { + samples[i] = new SampleBlock + { + WavInfo = wavInfo, // This is the only variable we can use for this initializer declarator, since the samples are DSP-ADPCM compressed + }; + r.Stream.Position = pcmdDataOffset + wavInfo.SampleOffset; // This sets the EndianBinaryReader stream position offset to the DSP-ADPCM header + + samples[i].DSPADPCM = new DSPADPCM(r, null); // Reads the entire DSP-ADPCM header and encoded data, also the SWD spec doesn't define number of channels + samples[i].DSPADPCM.Decode(); // Decodes all bytes into PCM16 data +#if DEBUG + // This is for dumping both the encoded and decoded samples, for ensuring that the decoder works correctly + new FileInfo("./ExtractedSamples/" + FileName + "/dsp/").Directory!.Create(); + File.WriteAllBytes("./ExtractedSamples/" + FileName + "/dsp/" + "sample" + i.ToString() + ".dsp", [.. samples[i].DSPADPCM.Info[0].ToBytes(), .. samples[i].DSPADPCM.Data]); + new FileInfo("./ExtractedSamples/" + FileName + "/wav/").Directory!.Create(); + File.WriteAllBytes("./ExtractedSamples/" + FileName + "/wav/" + "sample" + i.ToString() + ".wav", samples[i].DSPADPCM.ConvertToWav()); +#endif + break; + } + default: + { + throw new NotImplementedException("This Digital Sound Elements type has not yet been implemented."); + } + } } } return samples; } } - private static ProgramBank? ReadPrograms(EndianBinaryReader r, int numPRGISlots) - where T : IProgramInfo, new() + #endregion + + #region ProgramBank and KeyGroup + private static ProgramBank? ReadPrograms(EndianBinaryReader r, int numPRGISlots, SWD swd) { - long chunkOffset = FindChunk(r, "prgi"); + long chunkOffset = swd.PrgiChunkOffset = DSEUtils.FindChunk(r, "prgi"); if (chunkOffset == -1) { return null; } - chunkOffset += 0x10; - var programInfos = new IProgramInfo?[numPRGISlots]; + swd.PrgiInfo = new ChunkHeader(r, chunkOffset, swd); + long dataOffset = swd.PrgiDataOffset = chunkOffset + 0x10; + var programInfos = new ProgramInfo[numPRGISlots]; for (int i = 0; i < programInfos.Length; i++) { - r.Stream.Position = chunkOffset + (2 * i); + r.Stream.Position = dataOffset + (2 * i); ushort offset = r.ReadUInt16(); if (offset != 0) { - r.Stream.Position = offset + chunkOffset; - programInfos[i] = r.ReadObject(); + r.Stream.Position = offset + dataOffset; + programInfos[i] = new ProgramInfo(r, swd); + if (programInfos[i].Id != i) + { + throw new DSEArrayIndexAndHeaderIDMismatchException(i, programInfos[i].Id); + } } } return new ProgramBank { ProgramInfos = programInfos, - KeyGroups = ReadKeyGroups(r), + KeyGroups = ReadKeyGroups(r, swd), }; } - private static KeyGroup[] ReadKeyGroups(EndianBinaryReader r) + private static KeyGroup[] ReadKeyGroups(EndianBinaryReader r, SWD swd) { - long chunkOffset = FindChunk(r, "kgrp"); + long chunkOffset = swd.KgrpChunkOffset = DSEUtils.FindChunk(r, "kgrp"); if (chunkOffset == -1) { - return Array.Empty(); + return []; } - r.Stream.Position = chunkOffset + 0xC; - uint chunkLength = r.ReadUInt32(); - var keyGroups = new KeyGroup[chunkLength / 8]; // 8 is the size of a KeyGroup + ChunkHeader info = swd.KgrpInfo = new ChunkHeader(r, chunkOffset, swd); + swd.KgrpDataOffset = chunkOffset + 0x10; + r.Stream.Position = swd.KgrpDataOffset; + var keyGroups = new KeyGroup[info.ChunkEnd / 8]; // 8 is the size of a KeyGroup for (int i = 0; i < keyGroups.Length; i++) { - keyGroups[i] = r.ReadObject(); + keyGroups[i] = new KeyGroup(r, swd); + if (keyGroups[i].Id != i) + { + throw new DSEArrayIndexAndHeaderIDMismatchException(i, keyGroups[i].Id); + } } return keyGroups; } + #endregion } diff --git a/VG Music Studio - Core/NDS/NDSUtils.cs b/VG Music Studio - Core/NDS/NDSUtils.cs old mode 100644 new mode 100755 diff --git a/VG Music Studio - Core/NDS/SDAT/SBNK.cs b/VG Music Studio - Core/NDS/SDAT/SBNK.cs index 953f065d..ccc1f04d 100644 --- a/VG Music Studio - Core/NDS/SDAT/SBNK.cs +++ b/VG Music Studio - Core/NDS/SDAT/SBNK.cs @@ -1,4 +1,5 @@ using Kermalis.EndianBinaryIO; +using System; using System.IO; namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; @@ -128,9 +129,9 @@ public Instrument(EndianBinaryReader er) public SWAR[] SWARs { get; } - public SBNK(byte[] bytes) + public SBNK(Span bytes) { - using (var stream = new MemoryStream(bytes)) + using (var stream = new MemoryStream(bytes.ToArray())) { var er = new EndianBinaryReader(stream, ascii: true); FileHeader = new SDATFileHeader(er); diff --git a/VG Music Studio - Core/NDS/SDAT/SDATChannel.cs b/VG Music Studio - Core/NDS/SDAT/SDATChannel.cs index a9251764..b4228744 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATChannel.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATChannel.cs @@ -1,4 +1,5 @@ using System; +using Kermalis.VGMusicStudio.Core.Codec; namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; @@ -49,7 +50,7 @@ internal sealed class SDATChannel // PCM8, PCM16 private int _dataOffset; // ADPCM - private ADPCMDecoder _adpcmDecoder; + private IMAADPCM _adpcmDecoder; private short _adpcmLoopLastSample; private short _adpcmLoopStepIndex; // PSG diff --git a/VG Music Studio - Core/NDS/SDAT/SDATConfig.cs b/VG Music Studio - Core/NDS/SDAT/SDATConfig.cs index ce0c21c1..57dd777b 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATConfig.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATConfig.cs @@ -24,7 +24,7 @@ internal SDATConfig(SDAT sdat) songs.Add(new Song(i, sdat.SYMBBlock?.SequenceSymbols.Entries[i] ?? i.ToString())); } } - Playlists.Add(new Playlist(Strings.PlaylistMusic, songs)); + InternalSongNames.Add(new InternalSongName(Strings.InternalSongName, songs)); } public override string GetGameName() diff --git a/VG Music Studio - Core/NDS/SDAT/SDATEngine.cs b/VG Music Studio - Core/NDS/SDAT/SDATEngine.cs index 7611c7f6..e7bc0d6e 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATEngine.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATEngine.cs @@ -8,6 +8,8 @@ public sealed class SDATEngine : Engine public override SDATMixer Mixer { get; } public override SDATPlayer Player { get; } + public override bool IsFileSystemFormat { get; } = true; + public SDATEngine(SDAT sdat) { Config = new SDATConfig(sdat); @@ -18,6 +20,12 @@ public SDATEngine(SDAT sdat) Instance = this; } + public override void Reload() + { + var config = Config; + Dispose(); + _ = new SDATEngine(config.SDAT); + } public override void Dispose() { base.Dispose(); diff --git a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong.cs b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong.cs old mode 100644 new mode 100755 index ff00ed23..2715155c --- a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong.cs @@ -15,6 +15,7 @@ internal sealed partial class SDATLoadedSong : ILoadedSong public readonly SDAT.INFO.SequenceInfo SEQInfo; // TODO: Not public private readonly SSEQ _sseq; private readonly SBNK _sbnk; + public SoundBank Bank { get; } public SDATLoadedSong(SDATPlayer player, SDAT.INFO.SequenceInfo seqInfo) { diff --git a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs index 6d690097..c4f8fefe 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using static System.Buffers.Binary.BinaryPrimitives; @@ -16,7 +17,17 @@ private void AddEvent(byte trackIndex, long cmdOffset, T command, ArgType arg } private bool EventExists(byte trackIndex, long cmdOffset) { - return Events[trackIndex]!.Exists(e => e.Offset == cmdOffset); + if (Events is not null && Events[trackIndex] is not null) // A more readable and easier to understand way to find if the event exists, rather than using lambda + { + foreach (var e in Events[trackIndex]!) + { + if (e.Offset == cmdOffset) + { + return true; + } + } + } + return false; } private int ReadArg(ref int dataOffset, ArgType type) @@ -24,40 +35,40 @@ private int ReadArg(ref int dataOffset, ArgType type) switch (type) { case ArgType.Byte: - { - return _sseq.Data[dataOffset++]; - } + { + return _sseq.Data[dataOffset++]; + } case ArgType.Short: - { - short s = ReadInt16LittleEndian(_sseq.Data.AsSpan(dataOffset)); - dataOffset += 2; - return s; - } + { + 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++; + 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; } - 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; - } + { + // 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 - } + { + return _sseq.Data[dataOffset++]; // Return var index + } default: throw new Exception(); } } @@ -65,7 +76,7 @@ private int ReadArg(ref int dataOffset, ArgType type) private void AddTrackEvents(byte trackIndex, int trackStartOffset) { ref List? trackEvents = ref Events[trackIndex]; - trackEvents ??= new List(); + trackEvents ??= []; // The [] essentially is just a simplified "new List()" int callStackDepth = 0; AddEvents(trackIndex, trackStartOffset, ref callStackDepth); @@ -93,13 +104,13 @@ private void AddEvents(byte trackIndex, int startOffset, ref int callStackDepth) 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; + if (HandleCmdGroup0xA0(trackIndex, ref cmdOffset, cmd, ref argOverrideType, ref @if)) + { + goto again; + } + break; } - 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; @@ -125,21 +136,21 @@ private void HandleCmdGroup0x80(byte trackIndex, ref int dataOffset, int cmdOffs switch (cmd) { case 0x80: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new RestCommand { Rest = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new RestCommand { Rest = arg }, argOverrideType); + } + break; } - break; - } case 0x81: // RAND PROGRAM: [BW2 (2249)] - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VoiceCommand { Voice = arg }, argOverrideType); // TODO: Bank change + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VoiceCommand { Voice = arg }, argOverrideType); // TODO: Bank change + } + break; } - break; - } default: throw Invalid(trackIndex, cmdOffset, cmd); } } @@ -148,54 +159,54 @@ private void HandleCmdGroup0x90(byte trackIndex, ref int dataOffset, ref int cal 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); + 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; } - 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)) + 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) { - AddEvents(trackIndex, offset24bit, ref callStackDepth); + cont = false; } + break; } - 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)) + int offset24bit = _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16); + if (!EventExists(trackIndex, cmdOffset)) { - callStackDepth++; - AddEvents(trackIndex, offset24bit, ref callStackDepth); + 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; } - else - { - throw new SDATTooManyNestedCallsException(trackIndex); - } - break; - } default: throw Invalid(trackIndex, cmdOffset, cmd); } } @@ -204,35 +215,35 @@ private bool HandleCmdGroup0xA0(byte trackIndex, ref int cmdOffset, byte cmd, re switch (cmd) { case 0xA0: // [New Super Mario Bros (BGM_AMB_CHIKA)] [BW2 (1917, 1918)] - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new ModRandCommand(), argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ModRandCommand(), argOverrideType); + } + argOverrideType = ArgType.Rand; + cmdOffset++; + return true; } - 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); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ModVarCommand(), argOverrideType); + } + argOverrideType = ArgType.PlayerVar; + cmdOffset++; + return true; } - 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 (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ModIfCommand(), argOverrideType); + } + @if = true; + cmdOffset++; + return true; } - @if = true; - cmdOffset++; - return true; - } default: throw Invalid(trackIndex, cmdOffset, cmd); } } @@ -243,109 +254,109 @@ private void HandleCmdGroup0xB0(byte trackIndex, ref int dataOffset, int cmdOffs switch (cmd) { case 0xB0: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VarSetCommand { Variable = varIndex, Argument = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarSetCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; } - break; - } case 0xB1: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VarAddCommand { Variable = varIndex, Argument = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarAddCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; } - break; - } case 0xB2: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VarSubCommand { Variable = varIndex, Argument = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarSubCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; } - break; - } case 0xB3: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VarMulCommand { Variable = varIndex, Argument = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarMulCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; } - break; - } case 0xB4: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VarDivCommand { Variable = varIndex, Argument = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarDivCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; } - break; - } case 0xB5: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VarShiftCommand { Variable = varIndex, Argument = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarShiftCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; } - break; - } case 0xB6: // [Mario Kart DS (75)] - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VarRandCommand { Variable = varIndex, Argument = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarRandCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; } - break; - } case 0xB8: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VarCmpEECommand { Variable = varIndex, Argument = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarCmpEECommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; } - break; - } case 0xB9: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VarCmpGECommand { Variable = varIndex, Argument = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarCmpGECommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; } - break; - } case 0xBA: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VarCmpGGCommand { Variable = varIndex, Argument = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarCmpGGCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; } - break; - } case 0xBB: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VarCmpLECommand { Variable = varIndex, Argument = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarCmpLECommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; } - break; - } case 0xBC: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VarCmpLLCommand { Variable = varIndex, Argument = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarCmpLLCommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; } - break; - } case 0xBD: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VarCmpNECommand { Variable = varIndex, Argument = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarCmpNECommand { Variable = varIndex, Argument = arg }, argOverrideType); + } + break; } - break; - } default: throw Invalid(trackIndex, cmdOffset, cmd); } } @@ -355,133 +366,133 @@ private void HandleCmdGroup0xC0(byte trackIndex, ref int dataOffset, int cmdOffs switch (cmd) { case 0xC0: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new PanpotCommand { Panpot = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PanpotCommand { Panpot = arg }, argOverrideType); + } + break; } - break; - } case 0xC1: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new TrackVolumeCommand { Volume = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new TrackVolumeCommand { Volume = arg }, argOverrideType); + } + break; } - break; - } case 0xC2: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new PlayerVolumeCommand { Volume = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PlayerVolumeCommand { Volume = arg }, argOverrideType); + } + break; } - break; - } case 0xC3: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new TransposeCommand { Transpose = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new TransposeCommand { Transpose = arg }, argOverrideType); + } + break; } - break; - } case 0xC4: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new PitchBendCommand { Bend = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PitchBendCommand { Bend = arg }, argOverrideType); + } + break; } - break; - } case 0xC5: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new PitchBendRangeCommand { Range = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PitchBendRangeCommand { Range = arg }, argOverrideType); + } + break; } - break; - } case 0xC6: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new PriorityCommand { Priority = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PriorityCommand { Priority = arg }, argOverrideType); + } + break; } - break; - } case 0xC7: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new MonophonyCommand { Mono = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new MonophonyCommand { Mono = arg }, argOverrideType); + } + break; } - break; - } case 0xC8: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new TieCommand { Tie = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new TieCommand { Tie = arg }, argOverrideType); + } + break; } - break; - } case 0xC9: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new PortamentoControlCommand { Portamento = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PortamentoControlCommand { Portamento = arg }, argOverrideType); + } + break; } - break; - } case 0xCA: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new LFODepthCommand { Depth = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LFODepthCommand { Depth = arg }, argOverrideType); + } + break; } - break; - } case 0xCB: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new LFOSpeedCommand { Speed = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LFOSpeedCommand { Speed = arg }, argOverrideType); + } + break; } - break; - } case 0xCC: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new LFOTypeCommand { Type = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LFOTypeCommand { Type = arg }, argOverrideType); + } + break; } - break; - } case 0xCD: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new LFORangeCommand { Range = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LFORangeCommand { Range = arg }, argOverrideType); + } + break; } - break; - } case 0xCE: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new PortamentoToggleCommand { Portamento = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PortamentoToggleCommand { Portamento = arg }, argOverrideType); + } + break; } - break; - } case 0xCF: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new PortamentoTimeCommand { Time = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new PortamentoTimeCommand { Time = arg }, argOverrideType); + } + break; } - break; - } } } private void HandleCmdGroup0xD0(byte trackIndex, ref int dataOffset, int cmdOffset, byte cmd, ArgType argOverrideType) @@ -490,61 +501,61 @@ private void HandleCmdGroup0xD0(byte trackIndex, ref int dataOffset, int cmdOffs switch (cmd) { case 0xD0: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new ForceAttackCommand { Attack = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ForceAttackCommand { Attack = arg }, argOverrideType); + } + break; } - break; - } case 0xD1: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new ForceDecayCommand { Decay = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ForceDecayCommand { Decay = arg }, argOverrideType); + } + break; } - break; - } case 0xD2: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new ForceSustainCommand { Sustain = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ForceSustainCommand { Sustain = arg }, argOverrideType); + } + break; } - break; - } case 0xD3: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new ForceReleaseCommand { Release = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ForceReleaseCommand { Release = arg }, argOverrideType); + } + break; } - break; - } case 0xD4: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new LoopStartCommand { NumLoops = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LoopStartCommand { NumLoops = arg }, argOverrideType); + } + break; } - break; - } case 0xD5: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new TrackExpressionCommand { Expression = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new TrackExpressionCommand { Expression = arg }, argOverrideType); + } + break; } - break; - } case 0xD6: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new VarPrintCommand { Variable = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new VarPrintCommand { Variable = arg }, argOverrideType); + } + break; } - break; - } default: throw Invalid(trackIndex, cmdOffset, cmd); } } @@ -554,29 +565,29 @@ private void HandleCmdGroup0xE0(byte trackIndex, ref int dataOffset, int cmdOffs switch (cmd) { case 0xE0: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new LFODelayCommand { Delay = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LFODelayCommand { Delay = arg }, argOverrideType); + } + break; } - break; - } case 0xE1: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new TempoCommand { Tempo = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new TempoCommand { Tempo = arg }, argOverrideType); + } + break; } - break; - } case 0xE3: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new SweepPitchCommand { Pitch = arg }, argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new SweepPitchCommand { Pitch = arg }, argOverrideType); + } + break; } - break; - } default: throw Invalid(trackIndex, cmdOffset, cmd); } } @@ -585,51 +596,93 @@ private void HandleCmdGroup0xF0(byte trackIndex, ref int dataOffset, ref int cal switch (cmd) { case 0xFC: // [HGSS(1353)] - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new LoopEndCommand(), argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new LoopEndCommand(), argOverrideType); + } + break; } - break; - } case 0xFD: - { - if (!EventExists(trackIndex, cmdOffset)) - { - AddEvent(trackIndex, cmdOffset, new ReturnCommand(), argOverrideType); - } - if (!@if && callStackDepth != 0) { - cont = false; - callStackDepth--; + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new ReturnCommand(), argOverrideType); + } + if (!@if && callStackDepth != 0) + { + cont = false; + callStackDepth--; + } + break; } - break; - } case 0xFE: - { - ushort bits = (ushort)ReadArg(ref dataOffset, ArgType.Short); - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new AllocTracksCommand { Tracks = bits }, argOverrideType); + ushort bits = (ushort)ReadArg(ref dataOffset, ArgType.Short); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new AllocTracksCommand { Tracks = bits }, argOverrideType); + } + break; } - break; - } case 0xFF: - { - if (!EventExists(trackIndex, cmdOffset)) { - AddEvent(trackIndex, cmdOffset, new FinishCommand(), argOverrideType); + if (!EventExists(trackIndex, cmdOffset)) + { + AddEvent(trackIndex, cmdOffset, new FinishCommand(), argOverrideType); + } + if (!@if) + { + cont = false; + } + break; } - if (!@if) + default: throw Invalid(trackIndex, cmdOffset, cmd); + } + } + + private bool MatchTrack(Span done) + { + foreach (var t in _player.Tracks) + { + if (t.Allocated && t.Enabled && !done[t.Index]) + { + return true; + } + } + return false; + } + private static SongEvent GetSongEventAtOffset(List songEvents, int dataOffset) + { + SongEvent? foundEvent = null; + foreach (var ev in songEvents) + { + if (ev.Offset == dataOffset) + { + if (foundEvent is not null) { - cont = false; + throw new DuplicateNameException("DuplicateNameException:\nThis Song Event is a duplicate of an existing Song Event in the same list entry. A Sequence cannot have the same Song Event in the same list entry with identical values."); } - break; + foundEvent = ev; } - default: throw Invalid(trackIndex, cmdOffset, cmd); } + if (foundEvent is null) + { + throw new NullReferenceException("NullReferenceException:\nThere are no Song Events in this entry. Each Song Event entry in the Sequence must have at least 1 Song Event before it can be used."); + } + return foundEvent; + } + private static bool IsDoneCallingTracks(Span callStackLoops) + { + foreach (byte l in callStackLoops) + { + if (l != 0) + { + return false; + } + } + return true; } - 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 @@ -642,8 +695,8 @@ public void SetTicks() } _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])) + Span done = stackalloc bool[0x10]; // We use this instead of track.Stopped just to be certain that emulating Monophony works as intended + while (MatchTrack(done)) { while (_player.TempoStack >= 240) { @@ -660,7 +713,7 @@ public void SetTicks() track.Tick(); while (track.Rest == 0 && !track.WaitingForNoteToFinishBeforeContinuingXD && !track.Stopped) { - SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); + SongEvent e = GetSongEventAtOffset(evs, track.DataOffset); ExecuteNext(track); if (done[trackIndex]) { @@ -675,9 +728,9 @@ public void SetTicks() } else { - SongEvent newE = evs.Single(ev => ev.Offset == track.DataOffset); + SongEvent newE = GetSongEventAtOffset(evs, 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 + || (track.CallStackDepth != 0 && IsDoneCallingTracks(track.CallStackLoops) && newE.Ticks.Count > 0); // If we have "LoopStart (0)" and already counted the tick of this event } if (b) { @@ -693,7 +746,7 @@ public void SetTicks() _player.ElapsedTicks++; } _player.TempoStack += _player.Tempo; - _player.SMixer.ChannelTick(); + _player.SMixer!.ChannelTick(); _player.SMixer.EmulateProcess(); } for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) @@ -732,7 +785,7 @@ internal void SetCurTick(long ticks) } } _player.TempoStack += _player.Tempo; - _player.SMixer.ChannelTick(); + _player.SMixer!.ChannelTick(); _player.SMixer.EmulateProcess(); } finish: diff --git a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs index c9a87d02..d9623bdb 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs @@ -41,44 +41,44 @@ private int ReadArg(SDATTrack track, ArgType type) switch (type) { case ArgType.Byte: - { - return _sseq.Data[track.DataOffset++]; - } + { + return _sseq.Data[track.DataOffset++]; + } case ArgType.Short: - { - return _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8); - } + { + 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++; + 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; } - 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); - } + { + 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]; - } + { + 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); + channel = _player.SMixer!.AllocateChannel(type, track); if (channel is null) { return; @@ -99,28 +99,28 @@ private void TryStartChannel(SBNK.InstrumentData inst, SDATTrack track, byte not 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; + Span info = param.Info; + SWAR.SWAV? swav = _sbnk.GetSWAV(info[1], info[0]); + if (swav is not null) + { + channel.StartPCM(swav, duration); + started = true; + } + break; } - break; - } case InstrumentType.PSG: - { - channel.StartPSG((byte)param.Info[0], duration); - started = true; - break; - } + { + channel.StartPSG((byte)param.Info[0], duration); + started = true; + break; + } case InstrumentType.Noise: - { - channel.StartNoise(duration); - started = true; - break; - } + { + channel.StartNoise(duration); + started = true; + break; + } } channel.Stop(); if (!started) @@ -268,22 +268,22 @@ private void ExecuteCmdGroup0x80(SDATTrack track, byte cmd) switch (cmd) { case 0x80: // Rest - { - if (track.DoCommandWork) { - track.Rest = arg; + if (track.DoCommandWork) + { + track.Rest = arg; + } + break; } - break; - } case 0x81: // Program Change - { - if (track.DoCommandWork && arg <= byte.MaxValue) { - track.Voice = (byte)arg; + if (track.DoCommandWork && arg <= byte.MaxValue) + { + track.Voice = (byte)arg; + } + break; } - break; - } - throw Invalid(track.Index, track.DataOffset - 1, cmd); + throw Invalid(track.Index, track.DataOffset - 1, cmd); } } private void ExecuteCmdGroup0x90(SDATTrack track, byte cmd) @@ -291,41 +291,41 @@ 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) + 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) { - other.Enabled = true; - other.DataOffset = offset24bit; + SDATTrack other = _player.Tracks[index]; + if (other.Allocated && !other.Enabled) + { + other.Enabled = true; + other.DataOffset = offset24bit; + } } + break; } - 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; + int offset24bit = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8) | (_sseq.Data[track.DataOffset++] << 16); + if (track.DoCommandWork) + { + track.DataOffset = offset24bit; + } + break; } - 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; + 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; - } default: throw Invalid(track.Index, track.DataOffset - 1, cmd); } } @@ -334,32 +334,32 @@ private static void ExecuteCmdGroup0xA0(SDATTrack track, byte cmd, ref bool rese switch (cmd) { case 0xA0: // Rand Mod - { - if (track.DoCommandWork) { - track.ArgOverrideType = ArgType.Rand; - resetOverride = false; + if (track.DoCommandWork) + { + track.ArgOverrideType = ArgType.Rand; + resetOverride = false; + } + break; } - break; - } case 0xA1: // Var Mod - { - if (track.DoCommandWork) { - track.ArgOverrideType = ArgType.PlayerVar; - resetOverride = false; + if (track.DoCommandWork) + { + track.ArgOverrideType = ArgType.PlayerVar; + resetOverride = false; + } + break; } - break; - } case 0xA2: // If Mod - { - if (track.DoCommandWork) { - track.DoCommandWork = track.VariableFlag; - resetCmdWork = false; + if (track.DoCommandWork) + { + track.DoCommandWork = track.VariableFlag; + resetCmdWork = false; + } + break; } - break; - } default: throw Invalid(track.Index, track.DataOffset - 1, cmd); } } @@ -370,121 +370,121 @@ private void ExecuteCmdGroup0xB0(SDATTrack track, byte cmd) switch (cmd) { case 0xB0: // VarSet - { - if (track.DoCommandWork) { - _player.Vars[varIndex] = mathArg; + if (track.DoCommandWork) + { + _player.Vars[varIndex] = mathArg; + } + break; } - break; - } case 0xB1: // VarAdd - { - if (track.DoCommandWork) { - _player.Vars[varIndex] += mathArg; + if (track.DoCommandWork) + { + _player.Vars[varIndex] += mathArg; + } + break; } - break; - } case 0xB2: // VarSub - { - if (track.DoCommandWork) { - _player.Vars[varIndex] -= mathArg; + if (track.DoCommandWork) + { + _player.Vars[varIndex] -= mathArg; + } + break; } - break; - } case 0xB3: // VarMul - { - if (track.DoCommandWork) { - _player.Vars[varIndex] *= mathArg; + if (track.DoCommandWork) + { + _player.Vars[varIndex] *= mathArg; + } + break; } - break; - } case 0xB4: // VarDiv - { - if (track.DoCommandWork && mathArg != 0) { - _player.Vars[varIndex] /= mathArg; + if (track.DoCommandWork && mathArg != 0) + { + _player.Vars[varIndex] /= mathArg; + } + break; } - break; - } case 0xB5: // VarShift - { - if (track.DoCommandWork) { - ref short v = ref _player.Vars[varIndex]; - v = mathArg < 0 ? (short)(v >> -mathArg) : (short)(v << mathArg); + if (track.DoCommandWork) + { + ref short v = ref _player.Vars[varIndex]; + v = mathArg < 0 ? (short)(v >> -mathArg) : (short)(v << mathArg); + } + break; } - 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) + if (track.DoCommandWork) { - val = (short)-val; + 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; } - _player.Vars[varIndex] = val; + break; } - break; - } case 0xB8: // VarCmpEE - { - if (track.DoCommandWork) { - track.VariableFlag = _player.Vars[varIndex] == mathArg; + if (track.DoCommandWork) + { + track.VariableFlag = _player.Vars[varIndex] == mathArg; + } + break; } - break; - } case 0xB9: // VarCmpGE - { - if (track.DoCommandWork) { - track.VariableFlag = _player.Vars[varIndex] >= mathArg; + if (track.DoCommandWork) + { + track.VariableFlag = _player.Vars[varIndex] >= mathArg; + } + break; } - break; - } case 0xBA: // VarCmpGG - { - if (track.DoCommandWork) { - track.VariableFlag = _player.Vars[varIndex] > mathArg; + if (track.DoCommandWork) + { + track.VariableFlag = _player.Vars[varIndex] > mathArg; + } + break; } - break; - } case 0xBB: // VarCmpLE - { - if (track.DoCommandWork) { - track.VariableFlag = _player.Vars[varIndex] <= mathArg; + if (track.DoCommandWork) + { + track.VariableFlag = _player.Vars[varIndex] <= mathArg; + } + break; } - break; - } case 0xBC: // VarCmpLL - { - if (track.DoCommandWork) { - track.VariableFlag = _player.Vars[varIndex] < mathArg; + if (track.DoCommandWork) + { + track.VariableFlag = _player.Vars[varIndex] < mathArg; + } + break; } - break; - } case 0xBD: // VarCmpNE - { - if (track.DoCommandWork) { - track.VariableFlag = _player.Vars[varIndex] != mathArg; + if (track.DoCommandWork) + { + track.VariableFlag = _player.Vars[varIndex] != mathArg; + } + break; } - break; - } default: throw Invalid(track.Index, track.DataOffset - 1, cmd); } } @@ -494,144 +494,144 @@ private void ExecuteCmdGroup0xC0(SDATTrack track, byte cmd) switch (cmd) { case 0xC0: // Panpot - { - if (track.DoCommandWork) { - track.Panpot = (sbyte)(cmdArg - 0x40); + if (track.DoCommandWork) + { + track.Panpot = (sbyte)(cmdArg - 0x40); + } + break; } - break; - } case 0xC1: // Track Volume - { - if (track.DoCommandWork) { - track.Volume = (byte)cmdArg; + if (track.DoCommandWork) + { + track.Volume = (byte)cmdArg; + } + break; } - break; - } case 0xC2: // Player Volume - { - if (track.DoCommandWork) { - _player.Volume = (byte)cmdArg; + if (track.DoCommandWork) + { + _player.Volume = (byte)cmdArg; + } + break; } - break; - } case 0xC3: // Transpose - { - if (track.DoCommandWork) { - track.Transpose = (sbyte)cmdArg; + if (track.DoCommandWork) + { + track.Transpose = (sbyte)cmdArg; + } + break; } - break; - } case 0xC4: // Pitch Bend - { - if (track.DoCommandWork) { - track.PitchBend = (sbyte)cmdArg; + if (track.DoCommandWork) + { + track.PitchBend = (sbyte)cmdArg; + } + break; } - break; - } case 0xC5: // Pitch Bend Range - { - if (track.DoCommandWork) { - track.PitchBendRange = (byte)cmdArg; + if (track.DoCommandWork) + { + track.PitchBendRange = (byte)cmdArg; + } + break; } - break; - } case 0xC6: // Priority - { - if (track.DoCommandWork) { - track.Priority = (byte)(_player.Priority + (byte)cmdArg); + if (track.DoCommandWork) + { + track.Priority = (byte)(_player.Priority + (byte)cmdArg); + } + break; } - break; - } case 0xC7: // Mono - { - if (track.DoCommandWork) { - track.Mono = cmdArg == 1; + if (track.DoCommandWork) + { + track.Mono = cmdArg == 1; + } + break; } - break; - } case 0xC8: // Tie - { - if (track.DoCommandWork) { - track.Tie = cmdArg == 1; - track.StopAllChannels(); + if (track.DoCommandWork) + { + track.Tie = cmdArg == 1; + track.StopAllChannels(); + } + break; } - break; - } case 0xC9: // Portamento Control - { - if (track.DoCommandWork) { - int k = cmdArg + track.Transpose; - if (k < 0) - { - k = 0; - } - else if (k > 0x7F) + if (track.DoCommandWork) { - k = 0x7F; + int k = cmdArg + track.Transpose; + if (k < 0) + { + k = 0; + } + else if (k > 0x7F) + { + k = 0x7F; + } + track.PortamentoNote = (byte)k; + track.Portamento = true; } - track.PortamentoNote = (byte)k; - track.Portamento = true; + break; } - break; - } case 0xCA: // LFO Depth - { - if (track.DoCommandWork) { - track.LFODepth = (byte)cmdArg; + if (track.DoCommandWork) + { + track.LFODepth = (byte)cmdArg; + } + break; } - break; - } case 0xCB: // LFO Speed - { - if (track.DoCommandWork) { - track.LFOSpeed = (byte)cmdArg; + if (track.DoCommandWork) + { + track.LFOSpeed = (byte)cmdArg; + } + break; } - break; - } case 0xCC: // LFO Type - { - if (track.DoCommandWork) { - track.LFOType = (LFOType)cmdArg; + if (track.DoCommandWork) + { + track.LFOType = (LFOType)cmdArg; + } + break; } - break; - } case 0xCD: // LFO Range - { - if (track.DoCommandWork) { - track.LFORange = (byte)cmdArg; + if (track.DoCommandWork) + { + track.LFORange = (byte)cmdArg; + } + break; } - break; - } case 0xCE: // Portamento Toggle - { - if (track.DoCommandWork) { - track.Portamento = cmdArg == 1; + if (track.DoCommandWork) + { + track.Portamento = cmdArg == 1; + } + break; } - break; - } case 0xCF: // Portamento Time - { - if (track.DoCommandWork) { - track.PortamentoTime = (byte)cmdArg; + if (track.DoCommandWork) + { + track.PortamentoTime = (byte)cmdArg; + } + break; } - break; - } } } private void ExecuteCmdGroup0xD0(SDATTrack track, byte cmd) @@ -640,55 +640,55 @@ private void ExecuteCmdGroup0xD0(SDATTrack track, byte cmd) switch (cmd) { case 0xD0: // Forced Attack - { - if (track.DoCommandWork) { - track.Attack = (byte)cmdArg; + if (track.DoCommandWork) + { + track.Attack = (byte)cmdArg; + } + break; } - break; - } case 0xD1: // Forced Decay - { - if (track.DoCommandWork) { - track.Decay = (byte)cmdArg; + if (track.DoCommandWork) + { + track.Decay = (byte)cmdArg; + } + break; } - break; - } case 0xD2: // Forced Sustain - { - if (track.DoCommandWork) { - track.Sustain = (byte)cmdArg; + if (track.DoCommandWork) + { + track.Sustain = (byte)cmdArg; + } + break; } - break; - } case 0xD3: // Forced Release - { - if (track.DoCommandWork) { - track.Release = (byte)cmdArg; + if (track.DoCommandWork) + { + track.Release = (byte)cmdArg; + } + break; } - 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++; + if (track.DoCommandWork && track.CallStackDepth < 3) + { + track.CallStack[track.CallStackDepth] = track.DataOffset; + track.CallStackLoops[track.CallStackDepth] = (byte)cmdArg; + track.CallStackDepth++; + } + break; } - break; - } case 0xD5: // Track Expression - { - if (track.DoCommandWork) { - track.Expression = (byte)cmdArg; + if (track.DoCommandWork) + { + track.Expression = (byte)cmdArg; + } + break; } - break; - } default: throw Invalid(track.Index, track.DataOffset - 1, cmd); } } @@ -698,29 +698,29 @@ private void ExecuteCmdGroup0xE0(SDATTrack track, byte cmd) switch (cmd) { case 0xE0: // LFO Delay - { - if (track.DoCommandWork) { - track.LFODelay = (ushort)cmdArg; + if (track.DoCommandWork) + { + track.LFODelay = (ushort)cmdArg; + } + break; } - break; - } case 0xE1: // Tempo - { - if (track.DoCommandWork) { - _player.Tempo = (ushort)cmdArg; + if (track.DoCommandWork) + { + _player.Tempo = (ushort)cmdArg; + } + break; } - break; - } case 0xE3: // Sweep Pitch - { - if (track.DoCommandWork) { - track.SweepPitch = (short)cmdArg; + if (track.DoCommandWork) + { + track.SweepPitch = (short)cmdArg; + } + break; } - break; - } } } private void ExecuteCmdGroup0xF0(SDATTrack track, byte cmd) @@ -728,59 +728,59 @@ 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) + if (track.DoCommandWork && track.CallStackDepth != 0) { - count--; - track.CallStackLoops[track.CallStackDepth - 1] = count; - if (count == 0) + byte count = track.CallStackLoops[track.CallStackDepth - 1]; + if (count != 0) { - track.CallStackDepth--; - break; + count--; + track.CallStackLoops[track.CallStackDepth - 1] = count; + if (count == 0) + { + track.CallStackDepth--; + break; + } } + track.DataOffset = track.CallStack[track.CallStackDepth - 1]; } - track.DataOffset = track.CallStack[track.CallStackDepth - 1]; + break; } - 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) + 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; } - 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++) + // 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 { - if ((trackBits & (1 << i)) != 0) + // 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++) { - _player.Tracks[i].Allocated = true; + if ((trackBits & (1 << i)) != 0) + { + _player.Tracks[i].Allocated = true; + } } } + break; } - break; - } case 0xFF: // Finish - { - if (track.DoCommandWork) { - track.Stopped = true; + if (track.DoCommandWork) + { + track.Stopped = true; + } + break; } - 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 e516e150..83a766a1 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATMixer.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATMixer.cs @@ -1,5 +1,7 @@ -using Kermalis.VGMusicStudio.Core.Util; +using Kermalis.VGMusicStudio.Core.Formats; +using Kermalis.VGMusicStudio.Core.Util; using NAudio.Wave; +using SoundFlow.Structs; using System; namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; @@ -7,16 +9,30 @@ namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; public sealed class SDATMixer : Mixer { private readonly float _samplesReciprocal; - private readonly int _samplesPerBuffer; + internal override int SamplesPerBuffer { get; } private bool _isFading; private long _fadeMicroFramesLeft; private float _fadePos; private float _fadeStepPerMicroframe; internal SDATChannel[] Channels; - private readonly BufferedWaveProvider _buffer; + private readonly AudioBackend SDATPlaybackBackend; - protected override WaveFormat WaveFormat => _buffer.WaveFormat; + #region PortAudio Fields + // PortAudio Fields + private readonly Wave? _bufferPortAudio; + #endregion + + #region MiniAudio Fields + // MiniAudio Fields + private readonly AudioFormat _formatSoundFlow; + protected override AudioFormat SoundFlowFormat => _formatSoundFlow; + #endregion + + #region NAudio Fields + private readonly BufferedWaveProvider? _bufferNAudio; + protected override WaveFormat? WaveFormat => _bufferNAudio!.WaveFormat; + #endregion internal SDATMixer() { @@ -24,8 +40,8 @@ internal SDATMixer() // - 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; + SamplesPerBuffer = 341; // TODO + _samplesReciprocal = 1f / SamplesPerBuffer; Channels = new SDATChannel[0x10]; for (byte i = 0; i < 0x10; i++) @@ -33,20 +49,51 @@ internal SDATMixer() Channels[i] = new SDATChannel(i); } - _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + SDATPlaybackBackend = PlaybackBackend; + switch (PlaybackBackend) { - DiscardOnBufferOverflow = true, - BufferLength = _samplesPerBuffer * 64 - }; - Init(_buffer); + case AudioBackend.PortAudio: + { + _bufferPortAudio = new Wave() + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64 + }; + _bufferPortAudio.CreateIeeeFloatWave(sampleRate, 2, 16); + + Init(waveData: _bufferPortAudio, PortAudio.SampleFormat.Int16); + break; + } + case AudioBackend.MiniAudio: + { + _formatSoundFlow = new AudioFormat + { + Channels = 2, + SampleRate = sampleRate, + Format = SoundFlow.Enums.SampleFormat.F32 + }; + Init(); + break; + } + case AudioBackend.NAudio: + { + _bufferNAudio = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64 + }; + Init(waveProvider: _bufferNAudio); + break; + } + } } - 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 }; + private static readonly int[] _pcmChanOrder = [4, 5, 6, 7, 2, 0, 3, 1, 8, 9, 10, 11, 14, 12, 15, 13]; + private static readonly int[] _psgChanOrder = [8, 9, 10, 11, 12, 13]; + private static readonly int[] _noiseChanOrder = [14, 15]; internal SDATChannel? AllocateChannel(InstrumentType type, SDATTrack track) { - int[] allowedChannels; + Span allowedChannels; switch (type) { case InstrumentType.PCM: allowedChannels = _pcmChanOrder; break; @@ -149,7 +196,7 @@ internal void ResetFade() internal void EmulateProcess() { - for (int i = 0; i < _samplesPerBuffer; i++) + for (int i = 0; i < SamplesPerBuffer; i++) { for (int j = 0; j < 0x10; j++) { @@ -162,6 +209,7 @@ internal void EmulateProcess() } } private readonly byte[] _b = new byte[4]; + private readonly float[] _f = new float[2]; internal void Process(bool output, bool recording) { float masterStep; @@ -186,7 +234,7 @@ internal void Process(bool output, bool recording) masterStep = (toMaster - fromMaster) * _samplesReciprocal; masterLevel = fromMaster; } - for (int i = 0; i < _samplesPerBuffer; i++) + for (int i = 0; i < SamplesPerBuffer; i++) { int left = 0, right = 0; @@ -218,6 +266,7 @@ internal void Process(bool output, bool recording) left = (int)f; _b[0] = (byte)left; _b[1] = (byte)(left >> 8); + _f[0] = left / (float)short.MaxValue; f = right * masterLevel; if (f < short.MinValue) { @@ -230,14 +279,49 @@ internal void Process(bool output, bool recording) right = (int)f; _b[2] = (byte)right; _b[3] = (byte)(right >> 8); + _f[1] = right / (float)short.MaxValue; masterLevel += masterStep; if (output) { - _buffer.AddSamples(_b, 0, 4); + switch (SDATPlaybackBackend) + { + case AudioBackend.PortAudio: + { + _bufferPortAudio!.AddSamples(_b, 0, 4); + break; + } + case AudioBackend.MiniAudio: + { + DataProvider!.AddSamples(_f); + break; + } + case AudioBackend.NAudio: + { + _bufferNAudio!.AddSamples(_b, 0, 4); + break; + } + } } if (recording) { - _waveWriter!.Write(_b, 0, 4); + switch (SDATPlaybackBackend) + { + case AudioBackend.PortAudio: + { + _waveWriterPortAudio!.Write(_b, 0, 4); + break; + } + case AudioBackend.MiniAudio: + { + _soundFlowEncoder!.Encode(_f); + break; + } + case AudioBackend.NAudio: + { + _waveWriterNAudio!.Write(_b, 0, 4); + break; + } + } } } } diff --git a/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs b/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs index 97fb6ae7..4d7aef52 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs @@ -12,18 +12,18 @@ public sealed class SDATPlayer : Player internal readonly SDATTrack[] Tracks = new SDATTrack[0x10]; private readonly string?[] _voiceTypeCache = new string?[256]; internal readonly SDATConfig Config; - internal readonly SDATMixer SMixer; + internal readonly SDATMixer? SMixer; private SDATLoadedSong? _loadedSong; internal byte Volume; - internal ushort Tempo; + public override ushort Tempo { get; set; } internal int TempoStack; private long _elapsedLoops; private ushort? _prevBank; public override ILoadedSong? LoadedSong => _loadedSong; - protected override Mixer Mixer => SMixer; + protected override Mixer Mixer => SMixer!; internal SDATPlayer(SDATConfig config, SDATMixer mixer) : base(192) @@ -70,7 +70,7 @@ public override void UpdateSongState(SongState info) SDATTrack track = Tracks[i]; if (track.Enabled) { - track.UpdateSongState(info.Tracks[i], _loadedSong!, _voiceTypeCache); + track.UpdateSongState(info.Tracks[i], _loadedSong!, _voiceTypeCache!); } } } @@ -80,7 +80,7 @@ internal override void InitEmulation() TempoStack = 0; _elapsedLoops = 0; ElapsedTicks = 0; - SMixer.ResetFade(); + SMixer!.ResetFade(); _loadedSong!.InitEmulation(); for (int i = 0; i < 0x10; i++) { @@ -115,7 +115,7 @@ protected override bool Tick(bool playing, bool recording) { TickTrack(i, ref allDone); } - if (SMixer.IsFadeDone()) + if (SMixer!.IsFadeDone()) { allDone = true; } @@ -132,7 +132,7 @@ protected override bool Tick(bool playing, bool recording) track.UpdateChannels(); } } - SMixer.ChannelTick(); + SMixer!.ChannelTick(); SMixer.Process(playing, recording); return allDone; } @@ -187,7 +187,7 @@ private void HandleTicksAndLoop(SDATLoadedSong s, SDATTrack track) break; } } - if (ShouldFadeOut && _elapsedLoops > NumLoops && !SMixer.IsFading()) + 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 index 87be5b51..a2b861ee 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATTrack.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATTrack.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; @@ -158,7 +159,7 @@ public void UpdateChannels() public void StopAllChannels() { - SDATChannel[] chans = Channels.ToArray(); + Span chans = Channels.ToArray(); for (int i = 0; i < chans.Length; i++) { chans[i].Stop(); @@ -193,13 +194,13 @@ public sbyte GetPan() return (sbyte)p; } - public void UpdateSongState(SongState.Track tin, SDATLoadedSong loadedSong, string?[] voiceTypeCache) + public void UpdateSongState(SongState.Track tin, SDATLoadedSong loadedSong, Span voiceTypeCache) { tin.Position = DataOffset; tin.Rest = Rest; tin.Voice = Voice; tin.LFO = LFODepth * LFORange; - ref string? cache = ref voiceTypeCache[Voice]; + ref string cache = ref voiceTypeCache[Voice]; if (cache is null) { loadedSong.UpdateInstrumentCache(Voice, out cache); @@ -210,7 +211,7 @@ public void UpdateSongState(SongState.Track tin, SDATLoadedSong loadedSong, stri tin.Extra = Portamento ? PortamentoTime : (byte)0; tin.Panpot = GetPan(); - SDATChannel[] channels = Channels.ToArray(); + Span channels = [.. Channels]; if (channels.Length == 0) { tin.Keys[0] = byte.MaxValue; @@ -225,19 +226,22 @@ public void UpdateSongState(SongState.Track tin, SDATLoadedSong loadedSong, stri for (int j = 0; j < channels.Length; j++) { SDATChannel c = channels[j]; - if (c.State != EnvelopeState.Release) + if (c is not null) // Nullability check { - 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; + 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 diff --git a/VG Music Studio - Core/NDS/SDAT/SSEQ.cs b/VG Music Studio - Core/NDS/SDAT/SSEQ.cs index 22068c78..f0277733 100644 --- a/VG Music Studio - Core/NDS/SDAT/SSEQ.cs +++ b/VG Music Studio - Core/NDS/SDAT/SSEQ.cs @@ -1,4 +1,5 @@ using Kermalis.EndianBinaryIO; +using System; using System.IO; namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; @@ -12,19 +13,17 @@ internal sealed class SSEQ public byte[] Data; - public SSEQ(byte[] bytes) + public SSEQ(Span bytes) { - using (var stream = new MemoryStream(bytes)) - { - var er = new EndianBinaryReader(stream, ascii: true); - FileHeader = new SDATFileHeader(er); - BlockType = er.ReadString_Count(4); - BlockSize = er.ReadInt32(); - DataOffset = er.ReadInt32(); + using var stream = new MemoryStream(bytes.ToArray()); + 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 5a5e64de..1290092e 100644 --- a/VG Music Studio - Core/NDS/SDAT/SWAR.cs +++ b/VG Music Studio - Core/NDS/SDAT/SWAR.cs @@ -1,4 +1,5 @@ using Kermalis.EndianBinaryIO; +using System; using System.IO; namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; @@ -40,26 +41,24 @@ public SWAV(EndianBinaryReader er) public SWAV[] Waves; - public SWAR(byte[] bytes) + public SWAR(Span bytes) { - using (var stream = new MemoryStream(bytes)) - { - var er = new EndianBinaryReader(stream, ascii: true); - FileHeader = new SDATFileHeader(er); - BlockType = er.ReadString_Count(4); - BlockSize = er.ReadInt32(); - Padding = new byte[32]; - er.ReadBytes(Padding); - NumWaves = er.ReadInt32(); - WaveOffsets = new int[NumWaves]; - er.ReadInt32s(WaveOffsets); + using var stream = new MemoryStream(bytes.ToArray()); + 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/Player.cs b/VG Music Studio - Core/Player.cs old mode 100644 new mode 100755 index 33bd9b07..6208cc6f --- a/VG Music Studio - Core/Player.cs +++ b/VG Music Studio - Core/Player.cs @@ -1,8 +1,8 @@ -using Kermalis.VGMusicStudio.Core.Util; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading; +using Kermalis.VGMusicStudio.Core.Util; namespace Kermalis.VGMusicStudio.Core; @@ -15,34 +15,32 @@ public enum PlayerState : byte ShutDown, } -public interface ILoadedSong -{ - List?[] Events { get; } - long MaxTicks { get; } -} - -public abstract class Player : IDisposable +public abstract class Player(double ticksPerSecond) : IDisposable { protected abstract string Name { get; } protected abstract Mixer Mixer { get; } public abstract ILoadedSong? LoadedSong { get; } + public abstract ushort Tempo { get; set; } public bool ShouldFadeOut { get; set; } public long NumLoops { get; set; } + public SongState? Info { get; set; } public long ElapsedTicks { get; internal set; } public PlayerState State { get; protected set; } + public Exception? ErrorDetails { get; set; } + public event Action? SongEnded; - private readonly TimeBarrier _time; + private readonly BetterTimer _timer = new(ticksPerSecond); private Thread? _thread; - - protected Player(double ticksPerSecond) - { - _time = new TimeBarrier(ticksPerSecond); - } + private double? _deltaTimeElapsed; + public bool IsStreamStopped = true; + public bool IsPauseToggled = false; public abstract void LoadSong(int index); + public virtual void LoadSong(LoadedSong song) { } + public virtual void RefreshSong() { } public abstract void UpdateSongState(SongState info); internal abstract void InitEmulation(); protected abstract void SetCurTick(long ticks); @@ -86,7 +84,10 @@ public void Play() if (State is not PlayerState.ShutDown) { - Stop(); + if (State is not PlayerState.Stopped) + { + Stop(); + } InitEmulation(); State = PlayerState.Playing; CreateThread(); @@ -97,18 +98,17 @@ public void TogglePlaying() switch (State) { case PlayerState.Playing: - { - State = PlayerState.Paused; - WaitThread(); - break; - } + { + State = PlayerState.Paused; + break; + } case PlayerState.Paused: case PlayerState.Stopped: - { - State = PlayerState.Playing; - CreateThread(); - break; - } + { + State = PlayerState.Playing; + CreateThread(); + break; + } } } public void Stop() @@ -118,6 +118,7 @@ public void Stop() State = PlayerState.Stopped; WaitThread(); OnStopped(); + ElapsedTicks = 0L; } } public void Record(string fileName) @@ -144,43 +145,62 @@ public void SetSongPosition(long ticks) return; } + if (State is PlayerState.Stopped) + { + Play(); + } + if (State is PlayerState.Playing) { TogglePlaying(); } InitEmulation(); SetCurTick(ticks); - TogglePlaying(); + if (State is PlayerState.Paused && !IsPauseToggled || State is PlayerState.Stopped) + { + TogglePlaying(); + } } private void TimerTick() { - _time.Start(); + _deltaTimeElapsed = 0; + _timer.Start(); while (true) { - PlayerState state = State; - bool playing = state == PlayerState.Playing; - bool recording = state == PlayerState.Recording; + var state = State; + var playing = state == PlayerState.Playing; + var 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) + _deltaTimeElapsed += _timer.GetDeltaTime(); + while (_deltaTimeElapsed >= _timer.GetDeltaTick()) { - _time.Wait(); + _deltaTimeElapsed -= _timer.GetDeltaTick(); + try + { + bool allDone = Tick(playing, recording); + if (Info is not null) + { + UpdateSongState(Info); + } + if (allDone) + { + // TODO: lock state + _timer.Stop(); // TODO: Don't need timer if recording + SongEnded?.Invoke(); + return; + } + } + catch (Exception ex) + { + ErrorDetails = ex; + } } } - _time.Stop(); + _timer.Stop(); } public void Dispose() diff --git a/VG Music Studio - Core/PortAudio/Enumerations/ErrorCode.cs b/VG Music Studio - Core/PortAudio/Enumerations/ErrorCode.cs new file mode 100644 index 00000000..c3cb4861 --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Enumerations/ErrorCode.cs @@ -0,0 +1,44 @@ +// License: APL 2.0 +// Author: Benjamin N. Summerton + +namespace PortAudio +{ + /// + /// Error codes returned by PortAudio functions. + /// Note that with the exception of paNoError, all PaErrorCodes are negative. + /// + public enum ErrorCode + { + NoError = 0, + + NotInitialized = -10000, + UnanticipatedHostError, + InvalidChannelCount, + InvalidSampleRate, + InvalidDevice, + InvalidFlag, + SampleFormatNotSupported, + BadIODeviceCombination, + InsufficientMemory, + BufferTooBig, + BufferTooSmall, + NullCallback, + BadStreamPtr, + TimedOut, + InternalError, + DeviceUnavailable, + IncompatibleHostApiSpecificStreamInfo, + StreamIsStopped, + StreamIsNotStopped, + InputOverflowed, + OutputUnderflowed, + HostApiNotFound, + InvalidHostApi, + CanNotReadFromACallbackStream, + CanNotWriteToACallbackStream, + CanNotReadFromAnOutputOnlyStream, + CanNotWriteToAnInputOnlyStream, + IncompatibleStreamHostApi, + BadBufferPtr + } +} diff --git a/VG Music Studio - Core/PortAudio/Enumerations/HostApiTypeId.cs b/VG Music Studio - Core/PortAudio/Enumerations/HostApiTypeId.cs new file mode 100644 index 00000000..0262e7d5 --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Enumerations/HostApiTypeId.cs @@ -0,0 +1,34 @@ +namespace PortAudio; + +/// +/// Unchanging unique identifiers for each supported host API. This type +/// is used in the PaHostApiInfo structure. The values are guaranteed to be +/// unique and to never change, thus allowing code to be written that +/// conditionally uses host API specific extensions. +/// +/// New type ids will be allocated when support for a host API reaches +/// "public alpha" status, prior to that developers should use the +/// paInDevelopment type id. +/// +/// @see PaHostApiInfo +/// +public enum HostApiTypeId +{ + paInDevelopment=0, /* use while developing support for a new host API */ + paDirectSound=1, + paMME=2, + paASIO=3, + paSoundManager=4, + paCoreAudio=5, + paOSS=7, + paALSA=8, + paAL=9, + paBeOS=10, + paWDMKS=11, + paJACK=12, + paWASAPI=13, + paAudioScienceHPI=14, + paAudioIO=15, + paPulseAudio=16, + paSndio=17 +} \ No newline at end of file diff --git a/VG Music Studio - Core/PortAudio/Enumerations/SampleFormat.cs b/VG Music Studio - Core/PortAudio/Enumerations/SampleFormat.cs new file mode 100644 index 00000000..9780cae6 --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Enumerations/SampleFormat.cs @@ -0,0 +1,47 @@ +// License: APL 2.0 +// Author: Benjamin N. Summerton + +using System; + +namespace PortAudio +{ + /// + /// NOTE: this doesn't exist an as actual enum in the native library, but we can make it a bit safer in C# + /// + /// A type used to specify one or more sample formats. Each value indicates + /// a possible format for sound data passed to and from the stream callback, + /// Pa_ReadStream and Pa_WriteStream. + /// + /// The standard formats paFloat32, paInt16, paInt32, paInt24, paInt8 + /// and aUInt8 are usually implemented by all implementations. + /// + /// The floating point representation (paFloat32) uses +1.0 and -1.0 as the + /// maximum and minimum respectively. + /// + /// paUInt8 is an unsigned 8 bit format where 128 is considered "ground" + /// + /// The paNonInterleaved flag indicates that audio data is passed as an array + /// of pointers to separate buffers, one buffer for each channel. Usually, + /// when this flag is not used, audio data is passed as a single buffer with + /// all channels interleaved. + /// + /// @see Pa_OpenStream, Pa_OpenDefaultStream, PaDeviceInfo + /// @see paFloat32, paInt16, paInt32, paInt24, paInt8 + /// @see paUInt8, paCustomFormat, paNonInterleaved + /// + public enum SampleFormat : System.UInt32 + { + Float32 = 0x00000001, + Int32 = 0x00000002, + + /// Packed 24 bit format. + Int24 = 0x00000004, + + Int16 = 0x00000008, + Int8 = 0x00000010, + UInt8 = 0x00000020, + CustomFormat = 0x00010000, + + NonInterleaved = 0x80000000, + } +} diff --git a/VG Music Studio - Core/PortAudio/Enumerations/StreamCallbackFlags.cs b/VG Music Studio - Core/PortAudio/Enumerations/StreamCallbackFlags.cs new file mode 100644 index 00000000..ba41c144 --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Enumerations/StreamCallbackFlags.cs @@ -0,0 +1,50 @@ +// License: APL 2.0 +// Author: Benjamin N. Summerton + +using System; + +namespace PortAudio +{ + /// + /// NOTE: this doesn't exist an as actual enum in the native library, but we can make it a bit safer in C# + /// + /// Flag bit constants for the statusFlags to PaStreamCallback. + /// + public enum StreamCallbackFlags : System.UInt32 + { + /// + /// In a stream opened with paFramesPerBufferUnspecified, indicates that + /// input data is all silence (zeros) because no real data is available. In a + /// stream opened without paFramesPerBufferUnspecified, it indicates that one or + /// more zero samples have been inserted into the input buffer to compensate + /// for an input underflow. + /// + InputUnderflow = 0x00000001, + + /// + /// In a stream opened with paFramesPerBufferUnspecified, indicates that data + /// prior to the first sample of the input buffer was discarded due to an + /// overflow, possibly because the stream callback is using too much CPU time. + /// Otherwise indicates that data prior to one or more samples in the + /// input buffer was discarded. + /// + InputOverflow = 0x00000002, + + /// + /// Indicates that output data (or a gap) was inserted, possibly because the + /// stream callback is using too much CPU time. + /// + OutputUnderflow = 0x00000004, + + /// + /// Indicates that output data will be discarded because no room is available. + /// + OutputOverflow = 0x00000008, + + /// + /// Some of all of the output data will be used to prime the stream, input + /// data may be zero. + /// + PrimingOutput = 0x00000010 + } +} diff --git a/VG Music Studio - Core/PortAudio/Enumerations/StreamCallbackResult.cs b/VG Music Studio - Core/PortAudio/Enumerations/StreamCallbackResult.cs new file mode 100644 index 00000000..29b5a9ef --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Enumerations/StreamCallbackResult.cs @@ -0,0 +1,27 @@ +// License: APL 2.0 +// Author: Benjamin N. Summerton + +namespace PortAudio +{ + /// + /// Allowable return values for the PaStreamCallback. + /// @see PaStreamCallback + /// + public enum StreamCallbackResult + { + /// + /// Signal that the stream should continue invoking the callback and processing audio. + /// + Continue = 0, + + /// + /// Signal that the stream should stop invoking the callback and finish once all output samples have played. + /// + Complete = 1, + + /// + /// Signal that the stream should stop invoking the callback and finish as soon as possible. + /// + Abort = 2, + } +} diff --git a/VG Music Studio - Core/PortAudio/Enumerations/StreamFlags.cs b/VG Music Studio - Core/PortAudio/Enumerations/StreamFlags.cs new file mode 100644 index 00000000..2c43f01a --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Enumerations/StreamFlags.cs @@ -0,0 +1,57 @@ +// License: APL 2.0 +// Author: Benjamin N. Summerton + +using System; + +namespace PortAudio +{ + /// + /// NOTE: this doesn't exist an as actual enum in the native library, but we can make it a bit safer in C# + /// + /// Flags used to control the behavior of a stream. They are passed as + /// parameters to Pa_OpenStream or Pa_OpenDefaultStream. Multiple flags may be + /// ORed together. + /// + /// @see Pa_OpenStream, Pa_OpenDefaultStream + /// @see paNoFlag, paClipOff, paDitherOff, paNeverDropInput, + /// paPrimeOutputBuffersUsingStreamCallback, paPlatformSpecificFlags + /// + public enum StreamFlags : System.UInt32 + { + NoFlag = 0, + + /// + /// Disable default clipping of out of range samples. + /// + ClipOff = 0x00000001, + + /// + /// Disable default dithering. + /// + DitherOff = 0x00000002, + + /// + /// Flag requests that where possible a full duplex stream will not discard + /// overflowed input samples without calling the stream callback. This flag is + /// only valid for full duplex callback streams and only when used in combination + /// with the paFramesPerBufferUnspecified (0) framesPerBuffer parameter. Using + /// this flag incorrectly results in a paInvalidFlag error being returned from + /// Pa_OpenStream and Pa_OpenDefaultStream. + /// + /// @see paFramesPerBufferUnspecified + /// + NeverDropInput = 0x00000004, + + /// + /// Call the stream callback to fill initial output buffers, rather than the + /// default behavior of priming the buffers with zeros (silence). This flag has + /// no effect for input-only and blocking read/write streams. + /// + PrimeOutputBuffersUsingStreamCallback = 0x00000008, + + /// + /// A mask specifying the platform specific bits. + /// + PlatformSpecificFlags = 0xFFFF0000, + } +} diff --git a/VG Music Studio - Core/PortAudio/Native.cs b/VG Music Studio - Core/PortAudio/Native.cs new file mode 100644 index 00000000..2a01e344 --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Native.cs @@ -0,0 +1,186 @@ +using System; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace PortAudio.Native; + +internal static partial class Pa +{ + [LibraryImport("PortAudioLib")] + public static partial int Pa_GetVersion(); + + [LibraryImport("PortAudioLib")] + public static partial nint Pa_GetVersionInfo(); // Originally returns `const PaVersionInfo *` + + [LibraryImport("PortAudioLib")] + public static partial nint Pa_GetErrorText([MarshalAs(UnmanagedType.I4)] int errorCode); // Orignially returns `const char *` + + [LibraryImport("PortAudioLib")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int Pa_Initialize(); + + [LibraryImport("PortAudioLib")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int Pa_Terminate(); + + [LibraryImport("PortAudioLib")] + public static partial int Pa_GetHostApiCount(); + + [LibraryImport("PortAudioLib")] + public static partial int Pa_GetDefaultHostApi(); + + [LibraryImport("PortAudioLib")] + public static partial nint Pa_GetHostApiInfo(int hostApi); + + [LibraryImport("PortAudioLib")] + public static partial int Pa_HostApiTypeIdToHostApiIndex(HostApiTypeId type); + + [LibraryImport("PortAudioLib")] + public static partial int Pa_HostApiDeviceIndexToDeviceIndex(int hostApi, int hostApiDeviceIndex); + + [LibraryImport("PortAudioLib")] + public static partial nint Pa_GetLastHostErrorInfo(); + + [LibraryImport("PortAudioLib")] + public static partial int Pa_GetDeviceCount(); + + [LibraryImport("PortAudioLib")] + public static partial int Pa_GetDefaultInputDevice(); + + [LibraryImport("PortAudioLib")] + public static partial int Pa_GetDefaultOutputDevice(); + + [LibraryImport("PortAudioLib")] + public static partial nint Pa_GetDeviceInfo(int device); // Originally returns `const PaDeviceInfo *` + + [LibraryImport("PortAudioLib")] + public static partial void Pa_Sleep(int msec); +} + + +internal static partial class Stream +{ + [LibraryImport("PortAudioLib")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int Pa_OpenStream( + out nint stream, // `PaStream **` + nint inputParameters, // `const PaStreamParameters *` + nint outputParameters, // `const PaStreamParameters *` + double sampleRate, + uint framesPerBuffer, + StreamFlags streamFlags, + nint streamCallback, // `PaStreamCallback *` + nint userData // `void *` + ); + + [LibraryImport("PortAudioLib")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int Pa_OpenDefaultStream( + out nint stream, + int numInputChannels, + int numOutputChannels, + SampleFormat sampleFormat, + double sampleRate, + uint framesPerBuffer, + nint streamCallback, + nint userData + ); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [return: MarshalAs(UnmanagedType.I4)] + public delegate StreamCallbackResult Callback( + nint input, nint output, // Originally `const void *, void *` + uint frameCount, + ref StreamCallbackTimeInfo timeInfo, // Originally `const PaStreamCallbackTimeInfo*` + StreamCallbackFlags statusFlags, + nint userData // Orignially `void *` + ); + + [LibraryImport("PortAudioLib")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int Pa_CloseStream(nint stream); // `PaStream *` + + [LibraryImport("PortAudioLib")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int Pa_SetStreamFinishedCallback( + nint stream, // `PaStream *` + nint streamFinishedCallback // `PaStreamFinishedCallback *` + ); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void FinishedCallback( + nint userData // Originally `void *` + ); + + [LibraryImport("PortAudioLib")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int Pa_StartStream(nint stream); // `PaStream *` + + [LibraryImport("PortAudioLib")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int Pa_StopStream(nint stream); // `PaStream *` + + [LibraryImport("PortAudioLib")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int Pa_AbortStream(nint stream); // `PaStream *` + + [LibraryImport("PortAudioLib")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int Pa_IsStreamStopped(nint stream); // `PaStream *` + + [LibraryImport("PortAudioLib")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int Pa_IsStreamActive(nint stream); // `PaStream *` + + [LibraryImport("PortAudioLib")] + public static partial double Pa_GetStreamCpuLoad(nint stream); // `PaStream *` + + [LibraryImport("PortAudioLib")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int Pa_ReadStream( + nint stream, // `PaStream *` + nint buffer, // `void *` + ulong frames // `unsigned long` + ); + + [LibraryImport("PortAudioLib")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int Pa_WriteStream( + nint stream, // `PaStream *` + nint buffer, // `const void *` + ulong frames // `unsigned long` + ); +} + +internal static class Config +{ + internal static bool IsLoaded = false; + // Based on the code from the Nickvision Application template https://github.com/NickvisionApps/Application + // Code reference: https://github.com/NickvisionApps/Application/blob/28e3307b8242b2d335f8f65394a03afaf213363a/NickvisionApplication.GNOME/Program.cs#L50 + internal static void ImportLibrary() + { + try + { + NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), LibraryResolver); + } + catch + { + Debug.WriteLine("PortAudio Library is already loaded."); + return; + } + IsLoaded = true; + } + + // Code reference: https://github.com/NickvisionApps/Application/blob/28e3307b8242b2d335f8f65394a03afaf213363a/NickvisionApplication.GNOME/Program.cs#L136 + private static nint LibraryResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + string fileName; + fileName = libraryName switch + { + "PortAudioLib" => "portaudio", + _ => libraryName + }; + return NativeLibrary.Load(fileName, assembly, searchPath); + } +} diff --git a/VG Music Studio - Core/PortAudio/PortAudio.cs b/VG Music Studio - Core/PortAudio/PortAudio.cs new file mode 100644 index 00000000..4f485e8a --- /dev/null +++ b/VG Music Studio - Core/PortAudio/PortAudio.cs @@ -0,0 +1,462 @@ +// License: APL 2.0 +// Author: Benjamin N. Summerton + +using System; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace PortAudio; + +public static class Pa +{ + #region Constants + /// + /// A special int (DeviceIndex) value indicating that no device is available, + /// or should be used.
+ ///
+ /// See: + ///
+ public const int NoDevice = -1; + + /// + /// Can be passed as the framesPerBuffer parameter to Stream.Open() + /// or Stream.OpenDefault() to indicate that the stream callback will + /// accept buffers of any size. + /// + public const uint FramesPerBufferUnspecified = 0; + #endregion // Constants + + #region Properties + /// + /// Retrieve the release number of the currently running PortAudio build. + /// For example, for version "19.5.1" this will return 0x00130501.
+ ///
+ /// See: + ///
+ public static int Version + { + get => Native.Pa.Pa_GetVersion(); + } + + /// + /// Retrieve version information for the currently running PortAudio build.
+ ///
+ /// See:
+ ///
+ ///
+ ///
+ /// Available as of 19.5.0. + ///
+ /// + /// A pointer to an immutable PortAudio.VersionInfo structure. + /// + /// + /// This function can be called at any time. It does not require PortAudio + /// to be initialized. The structure pointed to is statically allocated. Do not + /// attempt to free it or modify it. + /// + public static VersionInfo VersionInfo + { + get => Marshal.PtrToStructure(Native.Pa.Pa_GetVersionInfo()); + } + + /// + /// Retrieve the number of available host APIs. Even if a host API is + /// available it may have no devices available.
+ ///
+ /// See: + ///
+ /// + /// A non-negative value indicating the number of available host APIs, + /// or an ErrorCode (which are always negative) if PortAudio is not initialized + /// or an error is encountered. + /// + public static int HostApiCount + { + get => Native.Pa.Pa_GetHostApiCount(); + } + + /// + /// Retrieve the index of the default host API. The default host API will be + /// the lowest common denominator host API on the current platform and is + /// unlikely to provide the best performance. + /// + /// + /// A non-negative value ranging from 0 to (Pa.GetHostApiCount()-1) + /// indicating the default host API index or, an ErrorCode (which are always + /// negative) if PortAudio is not initialized or an error is encountered. + /// + public static int DefaultHostApi + { + get => Native.Pa.Pa_GetDefaultHostApi(); + } + + /// + /// Return information about the last host error encountered. The error + /// information returned by Pa.GetLastHostErrorInfo() will never be modified + /// asynchronously by errors occurring in other PortAudio owned threads + /// (such as the thread that manages the stream callback.)
+ ///
+ /// This function is provided as a last resort, primarily to enhance debugging + /// by providing clients with access to all available error information. + ///
+ /// + /// A pointer to an immutable structure constraining information about + /// the host error. The values in this structure will only be valid if a + /// PortAudio function has previously returned the paUnanticipatedHostError + /// error code. + /// + public static HostErrorInfo LastHostErrorInfo + { + get => Marshal.PtrToStructure(Native.Pa.Pa_GetLastHostErrorInfo()); + } + + /// + /// Retrieve the number of available devices. The number of available devices + /// may be zero. + /// + /// + /// A non-negative value indicating the number of available devices + /// or, an ErrorCode (which are always negative) if PortAudio is not initialized + /// or an error is encountered. + /// + public static int DeviceCount + { + get => Native.Pa.Pa_GetDeviceCount(); + } + + /// + /// Retrieve the index of the default input device. The result can be + /// used in the inputDevice parameter to Stream.Open(). + /// + /// + /// The default input device index for the default host API, or NoDevice + /// if no default input device is available or an error was encountered. + /// + public static int DefaultInputDevice + { + get => Native.Pa.Pa_GetDefaultInputDevice(); + } + + /// + /// Retrieve the index of the default output device. The result can be + /// used in the outputDevice parameter to Stream.Open(). + /// + /// + /// The default output device index for the default host API, or NoDevice + /// if no default output device is available or an error was encountered. + /// + /// + /// On the PC, the user can specify a default device by + /// setting an environment variable. For example, to use device #1.
+ ///
+    /// set PA_RECOMMENDED_OUTPUT_DEVICE=1
+    /// 

+ /// The user should first determine the available device ids by using + /// the supplied application "pa_devs". + ///
+ public static int DefaultOutputDevice + { + get => Native.Pa.Pa_GetDefaultOutputDevice(); + } + #endregion + + #region Methods + /// + /// Generate a packed integer version number in the same format used + /// by Pa.GetVersion(). Use this to compare a specified version number with + /// the currently running version.
+ ///
+ /// + /// For example:
+ /// + /// if (Pa.GetVersion() < Pa.MakeVersionNumber(19,5,1)) { } + /// + ///

+ ///
+ /// See:
+ ///
+ ///
+ ///
+ /// + /// Available as of 19.5.0. + /// + ///
+ public static int MakeVersionNumber(int major, int minor, int subminor) + { + return ((major)&0xFF)<<16 | ((minor)&0xFF)<<8 | ((subminor)&0xFF); + } + + /// + /// Retrieve the release number of the currently running PortAudio build. + /// For example, for version "19.5.1" this will return 0x00130501.
+ ///
+ /// See: + ///
+ public static int GetVersion() => + Native.Pa.Pa_GetVersion(); + + /// + /// Retrieve version information for the currently running PortAudio build.
+ ///
+ /// See:
+ ///
+ ///
+ ///
+ /// Available as of 19.5.0. + ///
+ /// + /// A pointer to an immutable PortAudio.VersionInfo structure. + /// + /// + /// This function can be called at any time. It does not require PortAudio + /// to be initialized. The structure pointed to is statically allocated. Do not + /// attempt to free it or modify it. + /// + public static VersionInfo GetVersionInfo() => + Marshal.PtrToStructure(Native.Pa.Pa_GetVersionInfo()); + + /// + /// Translate the supplied PortAudio error code into a human readable + /// message. + /// + public static string GetErrorText(ErrorCode errorCode) => + Marshal.PtrToStringAnsi(Native.Pa.Pa_GetErrorText((int)errorCode))!; + + /// + /// Library initialization function - call this before using PortAudio. + /// This function initializes internal data structures and prepares underlying + /// host APIs for use. With the exception of Pa.GetVersion(), Pa.GetVersionText(), + /// and Pa.GetErrorText(), this function MUST be called before using any other + /// PortAudio API functions.
+ ///
+ /// If Pa.Initialize() is called multiple times, each successful + /// call must be matched with a corresponding call to Pa.Terminate(). + /// Pairs of calls to Pa.Initialize()/Pa.Terminate() may overlap, and are not + /// required to be fully nested.
+ ///
+ /// Note that if Pa.Initialize() returns an error code, Pa.Terminate() should + /// NOT be called.
+ ///
+ /// See: + ///
+ /// + /// NoError if successful, otherwise an error code indicating the cause + /// of failure. + /// + public static void Initialize() + { + if (!Native.Config.IsLoaded) + { + Native.Config.ImportLibrary(); + } + + ErrorCode ec = (ErrorCode)Native.Pa.Pa_Initialize(); + if (ec != ErrorCode.NoError) + throw new PortAudioException(ec, $"Error initializing PortAudio. Error code: {ec}"); + } + + /// + /// Library termination function - call this when finished using PortAudio. + /// This function deallocates all resources allocated by PortAudio since it was + /// initialized by a call to Pa.Initialize(). In cases wherePa.Initialize() has + /// been called multiple times, each call must be matched with a corresponding call + /// to Pa.Terminate(). The final matching call to Pa.Terminate() will automatically + /// close any PortAudio streams that are still open.
+ ///
+ /// Pa.Terminate() MUST be called before exiting a program which uses PortAudio. + /// Failure to do so may result in serious resource leaks, such as audio devices + /// not being available until the next reboot.
+ ///
+ /// See: + ///
+ /// + /// NoError if successful, otherwise an error code indicating the cause + /// of failure. + /// + public static void Terminate() + { + ErrorCode ec = (ErrorCode)Native.Pa.Pa_Terminate(); + if (ec != ErrorCode.NoError) + throw new PortAudioException(ec, $"Error terminating PortAudio. Error code: {ec}"); + } + + /// + /// Retrieve the number of available host APIs. Even if a host API is + /// available it may have no devices available.
+ ///
+ /// See: + ///
+ /// + /// A non-negative value indicating the number of available host APIs, + /// or an ErrorCode (which are always negative) if PortAudio is not initialized + /// or an error is encountered. + /// + public static int GetHostApiCount() => + Native.Pa.Pa_GetHostApiCount(); + + /// + /// Retrieve the index of the default host API. The default host API will be + /// the lowest common denominator host API on the current platform and is + /// unlikely to provide the best performance. + /// + /// + /// A non-negative value ranging from 0 to (Pa.GetHostApiCount()-1) + /// indicating the default host API index or, an ErrorCode (which are always + /// negative) if PortAudio is not initialized or an error is encountered. + /// + public static int GetDefaultHostApi() => + Native.Pa.Pa_GetDefaultHostApi(); + + /// + /// Retrieve a pointer to a structure containing information about a specific + /// host Api. + /// + /// + /// A valid host API index ranging from 0 to (Pa.GetHostApiCount()-1) + /// The returned structure is owned by the PortAudio implementation and must not + /// be manipulated or freed. The pointer is only guaranteed to be valid between + /// calls to Pa.Initialize() and Pa.Terminate(). + /// + /// + /// A pointer to an immutable HostApiInfo structure describing + /// a specific host API. If the hostApi parameter is out of range or an error + /// is encountered, the function returns null. + /// + public static HostApiInfo GetHostApiInfo(int hostApi) => + Marshal.PtrToStructure(Native.Pa.Pa_GetHostApiInfo(hostApi)); + + /// + /// Convert a static host API unique identifier, into a runtime + /// host API index.
+ ///
+ /// See: + ///
+ /// + /// A unique host API identifier belonging to the HostApiTypeId + /// enumeration. + /// + /// + /// A valid Paint ranging from 0 to (Pa.GetHostApiCount()-1) or, + /// an ErrorCode (which are always negative) if PortAudio is not initialized + /// or an error is encountered.
+ ///
+ /// The HostApiNotFound error code indicates that the host API specified by the + /// type parameter is not available. + ///
+ public static int HostApiTypeIdToHostApiIndex(HostApiTypeId type) => + Native.Pa.Pa_HostApiTypeIdToHostApiIndex(type); + + /// + /// Convert a host-API-specific device index to standard PortAudio device index. + /// This function may be used in conjunction with the deviceCount field of + /// HostApiInfo to enumerate all devices for the specified host API.
+ ///
+ /// See: + ///
+ /// + /// A valid host API index ranging from 0 to (Pa.GetHostApiCount()-1) + /// + /// + /// A valid per-host device index in the range + /// 0 to (GetHostApiInfo(hostApi).DeviceCount-1) + /// + /// + /// A non-negative int (DeviceIndex) ranging from 0 to (Pa.GetDeviceCount()-1) + /// or, an ErrorCode (which are always negative) if PortAudio is not initialized + /// or an error is encountered.
+ ///
+ /// A InvalidHostApi error code indicates that the host API index specified by + /// the hostApi parameter is out of range.
+ ///
+ /// A InvalidDevice error code indicates that the hostApiDeviceIndex parameter + /// is out of range. + ///
+ public static int HostApiDeviceIndexToDeviceIndex(int hostApi, int hostApiDeviceIndex) => + Native.Pa.Pa_HostApiDeviceIndexToDeviceIndex(hostApi, hostApiDeviceIndex); + + /// + /// Return information about the last host error encountered. The error + /// information returned by Pa.GetLastHostErrorInfo() will never be modified + /// asynchronously by errors occurring in other PortAudio owned threads + /// (such as the thread that manages the stream callback.)
+ ///
+ /// This function is provided as a last resort, primarily to enhance debugging + /// by providing clients with access to all available error information. + ///
+ /// + /// A pointer to an immutable structure constraining information about + /// the host error. The values in this structure will only be valid if a + /// PortAudio function has previously returned the paUnanticipatedHostError + /// error code. + /// + public static HostErrorInfo GetLastHostErrorInfo() => + Marshal.PtrToStructure(Native.Pa.Pa_GetLastHostErrorInfo()); + + /// + /// Retrieve the number of available devices. The number of available devices + /// may be zero. + /// + /// + /// A non-negative value indicating the number of available devices + /// or, an ErrorCode (which are always negative) if PortAudio is not initialized + /// or an error is encountered. + /// + public static int GetDeviceCount() => + Native.Pa.Pa_GetDeviceCount(); + + /// + /// Retrieve the index of the default input device. The result can be + /// used in the inputDevice parameter to Stream.Open(). + /// + /// + /// The default input device index for the default host API, or NoDevice + /// if no default input device is available or an error was encountered. + /// + public static int GetDefaultInputDevice() => + Native.Pa.Pa_GetDefaultInputDevice(); + + /// + /// Retrieve the index of the default output device. The result can be + /// used in the outputDevice parameter to Stream.Open(). + /// + /// + /// The default output device index for the default host API, or NoDevice + /// if no default output device is available or an error was encountered. + /// + /// + /// On the PC, the user can specify a default device by + /// setting an environment variable. For example, to use device #1.
+ ///
+    /// set PA_RECOMMENDED_OUTPUT_DEVICE=1
+    /// 

+ /// The user should first determine the available device ids by using + /// the supplied application "pa_devs". + ///
+ public static int GetDefaultOutputDevice() => + Native.Pa.Pa_GetDefaultOutputDevice(); + + /// + /// Retrieve a pointer to a PaDeviceInfo structure containing information + /// about the specified device.
+ ///
+ /// See:
+ ///
+ /// + ///
+ /// + /// A valid device index in the range 0 to (Pa.GetDeviceCount()-1) + /// + /// + /// A pointer to an immutable PaDeviceInfo structure. If the device + /// parameter is out of range the function returns null. + /// + /// + /// PortAudio manages the memory referenced by the returned pointer, + /// the client must not manipulate or free the memory. The pointer is only + /// guaranteed to be valid between calls to Pa.Initialize() and Pa.Terminate(). + /// + public static DeviceInfo GetDeviceInfo(int device) => + Marshal.PtrToStructure(Native.Pa.Pa_GetDeviceInfo(device)); + #endregion +} diff --git a/VG Music Studio - Core/PortAudio/PortAudioException.cs b/VG Music Studio - Core/PortAudio/PortAudioException.cs new file mode 100644 index 00000000..73664f92 --- /dev/null +++ b/VG Music Studio - Core/PortAudio/PortAudioException.cs @@ -0,0 +1,44 @@ +// License: APL 2.0 +// Author: Benjamin N. Summerton + +using System; + +namespace PortAudio +{ + public class PortAudioException : Exception + { + /// + /// Error code (from the native PortAudio library). Use `PortAudio.GetErrorText()` for some more details. + /// + public ErrorCode ErrorCode { get; private set; } + + /// + /// Creates a new PortAudio error. + /// + public PortAudioException(ErrorCode ec) : base() + { + this.ErrorCode = ec; + } + + /// + /// Creates a new PortAudio error with a message attached. + /// + /// Message to send + public PortAudioException(ErrorCode ec, string message) + : base(message) + { + this.ErrorCode = ec; + } + + /// + /// Creates a new PortAudio error with a message attached and an inner error. + /// + /// Message to send + /// The exception that occured inside of this one + public PortAudioException(ErrorCode ec, string message, Exception inner) + : base(message, inner) + { + this.ErrorCode = ec; + } + } +} diff --git a/VG Music Studio - Core/PortAudio/PortAudioPlayer.cs b/VG Music Studio - Core/PortAudio/PortAudioPlayer.cs new file mode 100644 index 00000000..a24a1d62 --- /dev/null +++ b/VG Music Studio - Core/PortAudio/PortAudioPlayer.cs @@ -0,0 +1,564 @@ +using System; +using System.Runtime.InteropServices; +using Kermalis.VGMusicStudio.Core.Formats; + +namespace PortAudio; + +public enum CallbackState +{ + Stop, + Play, + Pause +} + +public class PortAudioPlayer +{ + public static CallbackState? CallbackState { get; protected set; } + public float Volume = 1; + private bool _isDisposed = true; + private bool _isDisposing = false; + private static int _readPos = 0; + private readonly int _samplesPerBuffer; + private int? _prevBufferNum = 0; + private byte[]? _prevBuffer1; + private byte[]? _prevBuffer2; + private byte[]? _prevBuffer3; + private byte[]? _prevBuffer4; + private StreamParameters _oParams; + public StreamParameters DefaultOutputParams { get; private set; } + public HostApiInfo HostApiInfo { get; private set; } + private readonly Stream _stream; + + + internal PortAudioPlayer(SampleFormat sampleFormat, int samplesPerBuffer, Wave waveData) + { + _isDisposed = false; + + Pa.Initialize(); + + // Try setting up an output device + _oParams.Device = Pa.DefaultOutputDevice; + if (_oParams.Device == Pa.NoDevice) + { + throw new Exception("No default audio output device is available."); + } + + _oParams.Channels = 2; + _oParams.SampleFormat = sampleFormat; + _oParams.SuggestedLatency = Pa.GetDeviceInfo(_oParams.Device).defaultLowOutputLatency; + _oParams.HostApiSpecificStreamInfo = IntPtr.Zero; + + // Set it as the default + DefaultOutputParams = _oParams; + + _samplesPerBuffer = samplesPerBuffer; + + _stream = new Stream( + null, + _oParams, + waveData!.SampleRate, + (uint)samplesPerBuffer, + StreamFlags.NoFlag, + PlayCallback, + waveData + ); + + HostApiInfo = Pa.GetHostApiInfo(Pa.DefaultHostApi); + } + + public void Play() + { + CallbackState = PortAudio.CallbackState.Play; + _stream!.Start(); + } + + public void Stop() + { + CallbackState = PortAudio.CallbackState.Stop; + _stream!.Stop(); + } + + private static Span CastBytesToSBytes(Span byteMem) + { + return MemoryMarshal.Cast(byteMem); + } + + private static Span CastBytesToShorts(Span byteMem) + { + return MemoryMarshal.Cast(byteMem); + } + + private static Span CastBytesToInts(Span byteMem) + { + return MemoryMarshal.Cast(byteMem); + } + + private static Span CastBytesToInt24s(Span byteMem) + { + return MemoryMarshal.Cast(byteMem); + } + + private static Span CastBytesToFloats(Span byteMem) + { + return MemoryMarshal.Cast(byteMem); + } + + internal StreamCallbackResult PlayCallback( + nint input, nint output, + uint frameCount, + ref StreamCallbackTimeInfo timeInfo, + StreamCallbackFlags statusFlags, + nint userData + ) + { + // Marshal.AllocHGlobal() or any related functions cannot and must not be used + // in this callback, otherwise it will cause an OutOfMemoryException. + // + // The memory is already allocated by the output and userData params by + // the PortAudio bindings. + + if (_stream is null) + { + _readPos = 0; + return StreamCallbackResult.Abort; + } + + Wave d = _stream!.GetUserData(userData); + + if (d.Buffer is null) + { + _readPos = 0; + return StreamCallbackResult.Continue; + } + + if (!_stream.UDHandle.IsAllocated) + { + _readPos = 0; + return StreamCallbackResult.Abort; + } + + if (_prevBuffer1 is not null || _prevBuffer2 is not null || _prevBuffer3 is not null || _prevBuffer4 is not null) + { + if (d.Buffer.CompareTo(_prevBuffer1) is 0 && d.Buffer.CompareTo(_prevBuffer2) is 0 && d.Buffer.CompareTo(_prevBuffer3) is 0 && d.Buffer.CompareTo(_prevBuffer4) is 0) + { + CallbackState = PortAudio.CallbackState.Pause; + } + else + { + CallbackState = PortAudio.CallbackState.Play; + } + } + else + { + _prevBuffer1 = new byte[d.Buffer.Length]; + _prevBuffer2 = new byte[d.Buffer.Length]; + _prevBuffer3 = new byte[d.Buffer.Length]; + _prevBuffer4 = new byte[d.Buffer.Length]; + } + // RealignBufferPos(d); + + Option1(d, output, frameCount); + + while (d.BufferState is BufferState.Writing) + { + CallbackState = PortAudio.CallbackState.Pause; + var allocatedReadPos = _readPos + frameCount; + var allocatedWritePos = d.WritePosition + _samplesPerBuffer; + if (allocatedReadPos >= d.WritePosition && allocatedReadPos < allocatedWritePos) + { + if ((_readPos + _samplesPerBuffer) > d.BufferLength) + { + _readPos = d.WritePosition - _samplesPerBuffer; + } + else + { + _readPos = d.WritePosition + _samplesPerBuffer; + } + } + } + + CallbackState = PortAudio.CallbackState.Play; + + if (_prevBufferNum is 0) + { + d.Buffer.CopyTo(_prevBuffer1!, 0); + _prevBufferNum = 1; + } + else if (_prevBufferNum is 1) + { + d.Buffer.CopyTo(_prevBuffer2!, 0); + _prevBufferNum = 2; + } + else if (_prevBufferNum is 2) + { + d.Buffer.CopyTo(_prevBuffer3!, 0); + _prevBufferNum = 3; + } + else if (_prevBufferNum is 3) + { + d.Buffer.CopyTo(_prevBuffer4!, 0); + _prevBufferNum = 0; + } + + if (!_isDisposing) + { + // Continue if the mixer isn't being disposed + return StreamCallbackResult.Continue; + } + else + { + // Complete the callback if the mixer is being disposed + d.ResetBuffer(); + _readPos = 0; + _isDisposing = false; + return StreamCallbackResult.Complete; + } + } + + private void Option1(Wave d, nint output, uint frameCount) + { + + switch (_oParams.SampleFormat) + { + case SampleFormat.UInt8: + { + Span buffer; + unsafe + { + // Apply buffer value + buffer = new Span((byte*)output, (int)(frameCount * 2)); + } + + _readPos %= d.Buffer!.Length; + + // If we're reading data, play it back + if (CallbackState == PortAudio.CallbackState.Play) + { + for (int i = 0; i < buffer.Length; i++) + { + if (_readPos + i >= buffer.Length) + { + break; + } + buffer[i] = (byte)(d.Buffer[_readPos + i] * Volume); + } + } + else + { + buffer.Clear(); + } + + _readPos += buffer.Length; + + if (_readPos >= d.Buffer.Length) + { + _readPos = 0; + } + + if (_isDisposing) + { + buffer.Clear(); + } + + break; + } + case SampleFormat.Int8: + { + Span buffer; + Span waveBuffer = CastBytesToSBytes(d.Buffer); + unsafe + { + // Apply buffer value + buffer = new Span((sbyte*)output, (int)(frameCount * 2)); + } + + _readPos %= waveBuffer.Length; + + // If we're reading data, play it back + if (CallbackState == PortAudio.CallbackState.Play) + { + for (int i = 0; i < buffer.Length; i++) + { + if (_readPos + i >= waveBuffer.Length) + { + break; + } + buffer[i] = (sbyte)(waveBuffer[_readPos + i] * Volume); + } + } + else + { + buffer.Clear(); + } + + _readPos += buffer.Length; + + if (_readPos >= waveBuffer.Length) + { + _readPos = 0; + } + + if (_isDisposing) + { + buffer.Clear(); + } + + break; + } + case SampleFormat.Int16: + { + Span buffer; + Span waveBuffer = CastBytesToShorts(d.Buffer); + unsafe + { + // Apply buffer value + buffer = new Span((short*)output, (int)(frameCount * 2)); + } + + _readPos %= waveBuffer.Length; + + // If we're reading data, play it back + if (CallbackState == PortAudio.CallbackState.Play) + { + for (int i = 0; i < buffer.Length; i++) + { + if (_readPos + i >= waveBuffer.Length) + { + break; + } + buffer[i] = (short)(waveBuffer[_readPos + i] * Volume); + } + } + else + { + buffer.Clear(); + } + + _readPos += buffer.Length; + + if (_readPos >= waveBuffer.Length) + { + _readPos = 0; + } + + if (_isDisposing) + { + buffer.Clear(); + } + + break; + } + case SampleFormat.Int24: + { + Span buffer; + Span waveBuffer = CastBytesToInt24s(d.Buffer); + unsafe + { + // Apply buffer value + buffer = new Span((Int24*)output, (int)(frameCount * 2)); + } + + _readPos %= waveBuffer.Length; + + // If we're reading data, play it back + if (CallbackState == PortAudio.CallbackState.Play) + { + for (int i = 0; i < buffer.Length; i++) + { + if (_readPos + i >= waveBuffer.Length) + { + break; + } + buffer[i] = (Int24)(waveBuffer[_readPos + i] * Volume); + } + } + else + { + buffer.Clear(); + } + + _readPos += buffer.Length; + + if (_readPos >= waveBuffer.Length) + { + _readPos = 0; + } + + if (_isDisposing) + { + buffer.Clear(); + } + + break; + } + case SampleFormat.Int32: + { + Span buffer; + Span waveBuffer = CastBytesToInts(d.Buffer); + unsafe + { + // Apply buffer value + buffer = new Span((int*)output, (int)(frameCount * 2)); + } + + _readPos %= waveBuffer.Length; + + // If we're reading data, play it back + if (CallbackState == PortAudio.CallbackState.Play) + { + for (int i = 0; i < buffer.Length; i++) + { + if (_readPos + i >= waveBuffer.Length) + { + break; + } + buffer[i] = (int)(waveBuffer[_readPos + i] * Volume); + } + } + else + { + buffer.Clear(); + } + + _readPos += buffer.Length; + + if (_readPos >= waveBuffer.Length) + { + _readPos = 0; + } + + if (_isDisposing) + { + buffer.Clear(); + } + + break; + } + case SampleFormat.Float32: + { + Span buffer; + Span waveBuffer = CastBytesToFloats(d.Buffer); + unsafe + { + // Apply buffer value + buffer = new Span((float*)output, (int)(frameCount * 2)); + } + + _readPos %= waveBuffer.Length; + + // If we're reading data, play it back + if (CallbackState == PortAudio.CallbackState.Play) + { + for (int i = 0; i < buffer.Length; i++) + { + if (_readPos + i >= waveBuffer.Length) + { + break; + } + buffer[i] = (float)(waveBuffer[_readPos + i] * Volume); + } + } + else + { + buffer.Clear(); + } + + _readPos += buffer.Length; + + if (_readPos >= waveBuffer.Length) + { + _readPos = 0; + } + + if (_isDisposing) + { + buffer.Clear(); + } + + break; + } + } + } + + internal void Dispose() + { + if (!_isDisposed) + { + _isDisposing = true; + _stream.Dispose(); + Pa.Terminate(); + } + _isDisposed = true; + } + + // // Experimental realignment func to prevent reading from buffers being written to + // protected static void RealignBufferPos(Wave waveData) + // { + // var count = waveData.Count / 4; + // var writePos = waveData.WritePosition / 4; + + // if (writePos - count < 0) + // { + // if (ReadPos.Equals((writePos - count + (waveData.BufferLength / 4))..^(waveData.BufferLength / 4))) + // { + // if (ReadPos < writePos) + // { + // ReadPos -= count; + // if (ReadPos <= 0) + // { + // ReadPos += waveData.BufferLength / 4; + // } + // } + // else + // { + // ReadPos += count; + // if (ReadPos + count >= (waveData.BufferLength / 4)) + // { + // ReadPos -= waveData.BufferLength / 4; + // } + // } + // } + // else if (ReadPos.Equals(writePos..^(writePos + count))) + // { + // if (ReadPos < writePos) + // { + // ReadPos -= count; + // if (ReadPos <= 0) + // { + // ReadPos += waveData.BufferLength / 4; + // } + // } + // else + // { + // ReadPos += count; + // if (ReadPos + count >= (waveData.BufferLength / 4)) + // { + // ReadPos -= waveData.BufferLength / 4; + // } + // } + // } + // } + // if (writePos > count && writePos < (waveData.BufferLength / 4)) + // { + // if (ReadPos.Equals((writePos - count)..^(writePos + count))) + // { + // if (ReadPos < writePos) + // { + // ReadPos -= count; + // if (ReadPos <= 0) + // { + // ReadPos += waveData.BufferLength / 4; + // } + // } + // else + // { + // ReadPos += count; + // if (ReadPos + count >= (waveData.BufferLength / 4)) + // { + // ReadPos -= waveData.BufferLength / 4; + // } + // } + // } + // } + // } +} diff --git a/VG Music Studio - Core/PortAudio/Stream.cs b/VG Music Studio - Core/PortAudio/Stream.cs new file mode 100644 index 00000000..f583e620 --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Stream.cs @@ -0,0 +1,855 @@ +// License: APL 2.0 +// Author: Benjamin N. Summerton + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace PortAudio +{ + /// + /// A single PaStream can provide multiple channels of real-time + /// streaming audio input and output to a client application. A stream + /// provides access to audio hardware represented by one or more + /// PaDevices. Depending on the underlying Host API, it may be possible + /// to open multiple streams using the same device, however this behavior + /// is implementation defined. Portable applications should assume that + /// a PaDevice may be simultaneously used by at most one PaStream. + /// + /// Pointers to PaStream objects are passed between PortAudio functions that + /// operate on streams. + /// + /// @see Pa_OpenStream, Pa_OpenDefaultStream, Pa_OpenDefaultStream, Pa_CloseStream, + /// Pa_StartStream, Pa_StopStream, Pa_AbortStream, Pa_IsStreamActive, + /// Pa_GetStreamTime, Pa_GetStreamCpuLoad + /// + public class Stream : IDisposable + { + // Clean & manually managed data + private bool disposed = false; + private nint streamPtr = nint.Zero; // `Stream *` + private GCHandle userDataHandle; + + // Callback structures + private _NativeInterfacingCallback? streamCallback; + private _NativeInterfacingCallback? finishedCallback; + + /// + /// The input parameters for this stream, if any + /// + /// will be `null` if the user never supplied any + public StreamParameters? InputParameters { get; private set; } + + /// + /// The output parameters for this stream, if any + /// + /// will be `null` if the user never supplied any + public StreamParameters? OutputParameters { get; private set; } + + + #region Constructors & Cleanup + /// + /// Opens a stream for either input, output or both. + /// + /// @param stream The address of a PaStream pointer which will receive + /// a pointer to the newly opened stream. + /// + /// @param inputParameters A structure that describes the input parameters used by + /// the opened stream. See PaStreamParameters for a description of these parameters. + /// inputParameters must be NULL for output-only streams. + /// + /// @param outputParameters A structure that describes the output parameters used by + /// the opened stream. See PaStreamParameters for a description of these parameters. + /// outputParameters must be NULL for input-only streams. + /// + /// @param sampleRate The desired sampleRate. For full-duplex streams it is the + /// sample rate for both input and output + /// + /// @param framesPerBuffer The number of frames passed to the stream callback + /// function, or the preferred block granularity for a blocking read/write stream. + /// The special value paFramesPerBufferUnspecified (0) may be used to request that + /// the stream callback will receive an optimal (and possibly varying) number of + /// frames based on host requirements and the requested latency settings. + /// Note: With some host APIs, the use of non-zero framesPerBuffer for a callback + /// stream may introduce an additional layer of buffering which could introduce + /// additional latency. PortAudio guarantees that the additional latency + /// will be kept to the theoretical minimum however, it is strongly recommended + /// that a non-zero framesPerBuffer value only be used when your algorithm + /// requires a fixed number of frames per stream callback. + /// + /// @param streamFlags Flags which modify the behavior of the streaming process. + /// This parameter may contain a combination of flags ORed together. Some flags may + /// only be relevant to certain buffer formats. + /// + /// @param streamCallback A pointer to a client supplied function that is responsible + /// for processing and filling input and output buffers. If this parameter is NULL + /// the stream will be opened in 'blocking read/write' mode. In blocking mode, + /// the client can receive sample data using Pa_ReadStream and write sample data + /// using Pa_WriteStream, the number of samples that may be read or written + /// without blocking is returned by Pa_GetStreamReadAvailable and + /// Pa_GetStreamWriteAvailable respectively. + /// + /// @param userData A client supplied pointer which is passed to the stream callback + /// function. It could for example, contain a pointer to instance data necessary + /// for processing the audio buffers. This parameter is ignored if streamCallback + /// is NULL. + /// NOTE: userData will no longer be automatically GC'd normally by C#. The cleanup + /// of that will be handled by this class upon `Dipose()` or deletion. You (the + /// programmer), shouldn't have to worry about this. + /// + /// @return + /// Upon success Pa_OpenStream() returns paNoError and places a pointer to a + /// valid PaStream in the stream argument. The stream is inactive (stopped). + /// If a call to Pa_OpenStream() fails, a non-zero error code is returned (see + /// PaError for possible error codes) and the value of stream is invalid. + /// + /// @see PaStreamParameters, PaStreamCallback, Pa_ReadStream, Pa_WriteStream, + /// Pa_GetStreamReadAvailable, Pa_GetStreamWriteAvailable + /// + /// + /// + /// + /// + /// + /// + /// + public Stream( + StreamParameters? inputParameters, + StreamParameters? outputParameters, + double sampleRate, + uint framesPerBuffer, + StreamFlags streamFlags, + Callback callback, + object userData + ) + { + // Setup the steam's callback + streamCallback = new _NativeInterfacingCallback(callback); + + // Take control of the userdata object + userDataHandle = GCHandle.Alloc(userData); + + // Set the ins and the outs + InputParameters = inputParameters; + OutputParameters = outputParameters; + + // If the in/out params are set, then we need to make some P/Invoke friendly memory + nint inputParametersPtr = nint.Zero; + nint outputParametersPtr = nint.Zero; + if (inputParameters.HasValue) + { + inputParametersPtr = Marshal.AllocHGlobal(Marshal.SizeOf(inputParameters.Value)); + Marshal.StructureToPtr(inputParameters.Value, inputParametersPtr, false); + } + if (outputParameters.HasValue) + { + outputParametersPtr = Marshal.AllocHGlobal(Marshal.SizeOf(outputParameters.Value)); + Marshal.StructureToPtr(outputParameters.Value, outputParametersPtr, false); + } + + // Open the stream + ErrorCode ec = (ErrorCode)Native.Stream.Pa_OpenStream( + out streamPtr, + inputParametersPtr, + outputParametersPtr, + sampleRate, + framesPerBuffer, + streamFlags, + streamCallback.Ptr, + GCHandle.ToIntPtr(userDataHandle) + ); + if (ec != ErrorCode.NoError) + throw new PortAudioException(ec, "Error opening PortAudio Stream.\nError Code: " + ec.ToString()); + + // Cleanup the in/out params ptrs + if (inputParametersPtr != nint.Zero) + Marshal.FreeHGlobal(inputParametersPtr); + if (outputParametersPtr != nint.Zero) + Marshal.FreeHGlobal(outputParametersPtr); + } + + /// + /// A simplified version of Pa_OpenStream() that opens the default input + /// and/or output devices. + /// + /// @param stream The address of a PaStream pointer which will receive + /// a pointer to the newly opened stream. + /// + /// @param numInputChannels The number of channels of sound that will be supplied + /// to the stream callback or returned by Pa_ReadStream(). It can range from 1 to + /// the value of maxInputChannels in the PaDeviceInfo record for the default input + /// device. If 0 the stream is opened as an output-only stream. + /// + /// @param numOutputChannels The number of channels of sound to be delivered to the + /// stream callback or passed to Pa_WriteStream. It can range from 1 to the value + /// of maxOutputChannels in the PaDeviceInfo record for the default output device. + /// If 0 the stream is opened as an input-only stream. + /// + /// @param sampleFormat The sample format of both the input and output buffers + /// provided to the callback or passed to and from Pa_ReadStream() and Pa_WriteStream(). + /// sampleFormat may be any of the formats described by the PaSampleFormat + /// enumeration. + /// + /// @param sampleRate Same as Pa_OpenStream parameter of the same name. + /// @param framesPerBuffer Same as Pa_OpenStream parameter of the same name. + /// @param streamCallback Same as Pa_OpenStream parameter of the same name. + /// @param userData Same as Pa_OpenStream parameter of the same name. + /// + /// @return As for Pa_OpenStream + /// + /// @see Pa_OpenStream, PaStreamCallback + /// + /// + /// + /// + /// + /// + /// + /// + public Stream( + int numInputChannels, + int numOutputChannels, + SampleFormat sampleFormat, + double sampleRate, + uint framesPerBuffer, + Callback callback, + object userData + ) + { + // Setup the stream's callback + streamCallback = new _NativeInterfacingCallback(callback); + + // Take control of the userdata object + userDataHandle = GCHandle.Alloc(userData); + + // Open the stream + ErrorCode ec = (ErrorCode)Native.Stream.Pa_OpenDefaultStream( + out streamPtr, + numInputChannels, + numOutputChannels, + sampleFormat, + sampleRate, + framesPerBuffer, + streamCallback.Ptr, + GCHandle.ToIntPtr(userDataHandle) + ); + if (ec != ErrorCode.NoError) + throw new PortAudioException(ec, "Error opening PortAudio Stream.\nError Code: " + ec.ToString()); + } + ~Stream() + { + Dispose(false); + } + + /// + /// Cleanup resources (for the IDisposable interface) + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Does the actual disposing work + /// + protected virtual void Dispose(bool disposing) + { + if (disposed) + return; + + // Free Managed Resources + if (disposing) + { + } + + // Free Unmanaged resources + Close(); + userDataHandle.Free(); + streamCallback!.Free(); + if (finishedCallback != null) + finishedCallback.Free(); + + disposed = true; + } + #endregion // Constructors & Cleanup + + /// + /// Set a callback to be triggered when the stream is done. + /// + public void SetFinishedCallback(FinishedCallback fcb) + { + finishedCallback = new _NativeInterfacingCallback(fcb); + + // TODO what happens if a callback is already set? Find out and make the necessary adjustments + ErrorCode ec = (ErrorCode)Native.Stream.Pa_SetStreamFinishedCallback(streamPtr, finishedCallback.Ptr); + if (ec != ErrorCode.NoError) + throw new PortAudioException(ec, "Error setting finished callback for PortAudio Stream.\nError Code: " + ec.ToString()); + } + + #region Operations + /// + /// Closes an audio stream. If the audio stream is active it + /// discards any pending buffers as if Pa_AbortStream() had been called. + /// + public void Close() + { + // Did we already clean up? + if (streamPtr == nint.Zero) + return; + + ErrorCode ec = (ErrorCode)Native.Stream.Pa_CloseStream(streamPtr); + if (ec != ErrorCode.NoError) + throw new PortAudioException(ec, "Error closing PortAudio Stream.\nError Code: " + ec.ToString()); + + // Reset the handle, since we've cleaned up + streamPtr = nint.Zero; + } + + /// + /// Commences audio processing. + /// + public void Start() + { + ErrorCode ec = (ErrorCode)Native.Stream.Pa_StartStream(streamPtr); + if (ec != ErrorCode.NoError) + throw new PortAudioException(ec, "Error starting PortAudio Stream.\nError Code: " + ec.ToString()); + } + + /// + /// Terminates audio processing. It waits until all pending + /// audio buffers have been played before it returns. + /// + public void Stop() + { + ErrorCode ec = (ErrorCode)Native.Stream.Pa_StopStream(streamPtr); + if (ec != ErrorCode.NoError) + if (ec == ErrorCode.TimedOut) + throw new PortAudioException(ec, "Unable to stop PortAudio stream due to an active callback loop.\n" + + "A StreamCallbackResult must be set to 'Complete' or 'Abort' before a stream can be stopped.\n" + + "Error Code: " + ec.ToString()); + else + throw new PortAudioException(ec, "Error stopping PortAudio Stream.\nError Code: " + ec.ToString()); + } + + /// + /// Terminates audio processing immediately without waiting for pending + /// buffers to complete. + /// + public void Abort() + { + ErrorCode ec = (ErrorCode)Native.Stream.Pa_AbortStream(streamPtr); + if (ec != ErrorCode.NoError) + throw new PortAudioException(ec, "Error aborting PortAudio Stream.\nError Code: " + ec.ToString()); + } + + #region ReadInput + /// + /// Reads the audio input when the stream is opened and processing. + /// The stream must be started with `Pa_StartStream()` before using this. + /// + /// @param buffer The audio input buffer that will be read. + /// + /// @param frames The number of frames in the audio input buffer. + /// + public void ReadInput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_ReadStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error reading PortAudio Input Stream.\nError Code: " + ec.ToString()); + } + } + public void ReadInput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_ReadStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error reading PortAudio Input Stream.\nError Code: " + ec.ToString()); + } + } + public void ReadInput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_ReadStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error reading PortAudio Input Stream.\nError Code: " + ec.ToString()); + } + } + public void ReadInput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_ReadStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error reading PortAudio Input Stream.\nError Code: " + ec.ToString()); + } + } + public void ReadInput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_ReadStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error reading PortAudio Input Stream.\nError Code: " + ec.ToString()); + } + } + public void ReadInput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_ReadStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error reading PortAudio Input Stream.\nError Code: " + ec.ToString()); + } + } + public void ReadInput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_ReadStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error reading PortAudio Input Stream.\nError Code: " + ec.ToString()); + } + } + public void ReadInput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_ReadStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error reading PortAudio Input Stream.\nError Code: " + ec.ToString()); + } + } + public void ReadInput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_ReadStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error reading PortAudio Input Stream.\nError Code: " + ec.ToString()); + } + } + #endregion + + #region WriteOutput + /// + /// Writes the audio output when the stream is opened and processing. + /// The stream must be started with `Pa_StartStream()` before using this. + /// + /// @param buffer The audio output buffer that will be written. + /// + /// @param frames The number of frames in the audio output buffer. + /// + public void WriteOutput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_WriteStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error writing PortAudio Output Stream.\nError Code: " + ec.ToString()); + } + } + public void WriteOutput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_WriteStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error writing PortAudio Output Stream.\nError Code: " + ec.ToString()); + } + } + public void WriteOutput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_WriteStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error writing PortAudio Output Stream.\nError Code: " + ec.ToString()); + } + } + public void WriteOutput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_WriteStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error writing PortAudio Output Stream.\nError Code: " + ec.ToString()); + } + } + public void WriteOutput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_WriteStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error writing PortAudio Output Stream.\nError Code: " + ec.ToString()); + } + } + public void WriteOutput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_WriteStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error writing PortAudio Output Stream.\nError Code: " + ec.ToString()); + } + } + public void WriteOutput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_WriteStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error writing PortAudio Output Stream.\nError Code: " + ec.ToString()); + } + } + public void WriteOutput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_WriteStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error writing PortAudio Output Stream.\nError Code: " + ec.ToString()); + } + } + public void WriteOutput(Span buffer, ulong frames) + { + nint buffPtr; + unsafe + { + buffPtr = (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer)); + } + ErrorCode ec = (ErrorCode)Native.Stream.Pa_WriteStream(streamPtr, buffPtr, frames); + if (ec != ErrorCode.NoError) + { + throw new PortAudioException(ec, "Error writing PortAudio Output Stream.\nError Code: " + ec.ToString()); + } + } + #endregion + + #endregion // Operations + + #region Properties + /// + /// Determine whether the stream is stopped. + /// A stream is considered to be stopped prior to a successful call to + /// Pa_StartStream and after a successful call to Pa_StopStream or Pa_AbortStream. + /// If a stream callback returns a value other than paContinue the stream is NOT + /// considered to be stopped. + /// + /// @return Returns one (1) when the stream is stopped, zero (0) when + /// the stream is running or, a PaErrorCode (which are always negative) if + /// PortAudio is not initialized or an error is encountered. + /// + /// @see Pa_StopStream, Pa_AbortStream, Pa_IsStreamActive + /// + public bool IsStopped + { + get + { + ErrorCode ec = (ErrorCode)Native.Stream.Pa_IsStreamStopped(streamPtr); + + // Yes, No, or wat? + if ((int)ec == 1) + return true; + else if ((int)ec == 0) + return false; + else + throw new PortAudioException(ec, "Error checking if PortAudio Stream is stopped"); + } + } + + /// + /// Determine whether the stream is active. + /// A stream is active after a successful call to Pa_StartStream(), until it + /// becomes inactive either as a result of a call to Pa_StopStream() or + /// Pa_AbortStream(), or as a result of a return value other than paContinue from + /// the stream callback. In the latter case, the stream is considered inactive + /// after the last buffer has finished playing. + /// + /// @return Returns one (1) when the stream is active (ie playing or recording + /// audio), zero (0) when not playing or, a PaErrorCode (which are always negative) + /// if PortAudio is not initialized or an error is encountered. + /// + /// @see Pa_StopStream, Pa_AbortStream, Pa_IsStreamStopped + /// + public bool IsActive + { + get + { + ErrorCode ec = (ErrorCode)Native.Stream.Pa_IsStreamActive(streamPtr); + + // Yes, No, or wat? + if ((int)ec == 1) + return true; + else if ((int)ec == 0) + return false; + else + throw new PortAudioException(ec, "Error checking if PortAudio Stream is active"); + } + } + + /// + /// Retrieve CPU usage information for the specified stream. + /// The "CPU Load" is a fraction of total CPU time consumed by a callback stream's + /// audio processing routines including, but not limited to the client supplied + /// stream callback. This function does not work with blocking read/write streams. + /// + /// This function may be called from the stream callback function or the + /// application. + /// + /// @return + /// A floating point value, typically between 0.0 and 1.0, where 1.0 indicates + /// that the stream callback is consuming the maximum number of CPU cycles possible + /// to maintain real-time operation. A value of 0.5 would imply that PortAudio and + /// the stream callback was consuming roughly 50% of the available CPU time. The + /// return value may exceed 1.0. A value of 0.0 will always be returned for a + /// blocking read/write stream, or if an error occurs. + /// + public double CpuLoad + { + get => Native.Stream.Pa_GetStreamCpuLoad(streamPtr); + } + #endregion Properties + + #region Programmer Friendly Callbacks + /// + /// Functions of type PaStreamCallback are implemented by PortAudio clients. + /// They consume, process or generate audio in response to requests from an + /// active PortAudio stream. + /// + /// When a stream is running, PortAudio calls the stream callback periodically. + /// The callback function is responsible for processing buffers of audio samples + /// passed via the input and output parameters. + /// + /// The PortAudio stream callback runs at very high or real-time priority. + /// It is required to consistently meet its time deadlines. Do not allocate + /// memory, access the file system, call library functions or call other functions + /// from the stream callback that may block or take an unpredictable amount of + /// time to complete. + /// + /// In order for a stream to maintain glitch-free operation the callback + /// must consume and return audio data faster than it is recorded and/or + /// played. PortAudio anticipates that each callback invocation may execute for + /// a duration approaching the duration of frameCount audio frames at the stream + /// sample rate. It is reasonable to expect to be able to utilise 70% or more of + /// the available CPU time in the PortAudio callback. However, due to buffer size + /// adaption and other factors, not all host APIs are able to guarantee audio + /// stability under heavy CPU load with arbitrary fixed callback buffer sizes. + /// When high callback CPU utilisation is required the most robust behavior + /// can be achieved by using paFramesPerBufferUnspecified as the + /// Pa_OpenStream() framesPerBuffer parameter. + /// + /// @param input and @param output are either arrays of interleaved samples or; + /// if non-interleaved samples were requested using the paNonInterleaved sample + /// format flag, an array of buffer pointers, one non-interleaved buffer for + /// each channel. + /// + /// The format, packing and number of channels used by the buffers are + /// determined by parameters to Pa_OpenStream(). + /// + /// @param frameCount The number of sample frames to be processed by + /// the stream callback. + /// + /// @param timeInfo Timestamps indicating the ADC capture time of the first sample + /// in the input buffer, the DAC output time of the first sample in the output buffer + /// and the time the callback was invoked. + /// See PaStreamCallbackTimeInfo and Pa_GetStreamTime() + /// + /// @param statusFlags Flags indicating whether input and/or output buffers + /// have been inserted or will be dropped to overcome underflow or overflow + /// conditions. + /// + /// @param userData The value of a user supplied pointer passed to + /// Pa_OpenStream() intended for storing synthesis data etc. + /// NOTE: In the implementing callback, you can use the `GetUserData()` method to + /// retrive the actual object. + /// + /// @return + /// The stream callback should return one of the values in the + /// ::PaStreamCallbackResult enumeration. To ensure that the callback continues + /// to be called, it should return paContinue (0). Either paComplete or paAbort + /// can be returned to finish stream processing, after either of these values is + /// returned the callback will not be called again. If paAbort is returned the + /// stream will finish as soon as possible. If paComplete is returned, the stream + /// will continue until all buffers generated by the callback have been played. + /// This may be useful in applications such as soundfile players where a specific + /// duration of output is required. However, it is not necessary to utilize this + /// mechanism as Pa_StopStream(), Pa_AbortStream() or Pa_CloseStream() can also + /// be used to stop the stream. The callback must always fill the entire output + /// buffer irrespective of its return value. + /// + /// @see Pa_OpenStream, Pa_OpenDefaultStream + /// + /// @note With the exception of Pa_GetStreamCpuLoad() it is not permissible to call + /// PortAudio API functions from within the stream callback. + /// + public delegate StreamCallbackResult Callback( + nint input, nint output, // Originally `const void *, void *` + uint frameCount, + ref StreamCallbackTimeInfo timeInfo, // Originally `const PaStreamCallbackTimeInfo*` + StreamCallbackFlags statusFlags, + nint userDataPtr // Orignially `void *` + ); + + /// + /// Functions of type PaStreamFinishedCallback are implemented by PortAudio + /// clients. They can be registered with a stream using the Pa_SetStreamFinishedCallback + /// function. Once registered they are called when the stream becomes inactive + /// (ie once a call to Pa_StopStream() will not block). + /// A stream will become inactive after the stream callback returns non-zero, + /// or when Pa_StopStream or Pa_AbortStream is called. For a stream providing audio + /// output, if the stream callback returns paComplete, or Pa_StopStream() is called, + /// the stream finished callback will not be called until all generated sample data + /// has been played. + /// + /// @param userData The userData parameter supplied to Pa_OpenStream() + /// NOTE: In the implementing callback, you can use the `GetUserData()` method to + /// retrive the actual object. + /// + /// @see Pa_SetStreamFinishedCallback + /// + public delegate void FinishedCallback( + nint userDataPtr // Originally `void *` + ); + #endregion // Callbacks + + /// + /// This function will retrieve the `userData` of the stream from it's pointer. + /// + /// This is meant to be used by the callbacks for `Callback` and `FinishedCallback`, and + /// their `userDataPtr`. + /// + /// + /// The type of data that was put into the stream + /// + public UD GetUserData(nint userDataPtr) + { + UDHandle = GCHandle.FromIntPtr(userDataPtr); + return (UD)GCHandle.FromIntPtr(userDataPtr).Target!; + } + internal GCHandle UDHandle + { + get; private set; + } + + /// + /// This is an internal structure to aid with C# Callbacks that interface with P/Invoke functions. + /// + /// The constructor, the `Free()` method, and the `Ptr` property are all that you can use, and are + /// the most important parts. + /// + /// Callback + private class _NativeInterfacingCallback + where CB : Delegate + { + /// + /// The callback itself (needs to be a delegate) + /// + private CB callback; + + /// + /// GC Handle to the callback + /// + private GCHandle handle; + + /// + /// Get the pointer to where the function/delegate lives in memory + /// + public nint Ptr { get; private set; } = nint.Zero; + + /// + /// Setup the data structure. + /// + /// When done with it, don't forget to call the Free() method. + /// + /// + public _NativeInterfacingCallback(CB cb) + { + callback = cb ?? throw new ArgumentNullException(nameof(cb)); + handle = GCHandle.Alloc(cb); + Ptr = Marshal.GetFunctionPointerForDelegate(cb); + } + + /// + /// Manually clean up memory + /// + public void Free() + { + handle.Free(); + } + } + } +} diff --git a/VG Music Studio - Core/PortAudio/Structures/DeviceInfo.cs b/VG Music Studio - Core/PortAudio/Structures/DeviceInfo.cs new file mode 100644 index 00000000..bb9919d7 --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Structures/DeviceInfo.cs @@ -0,0 +1,59 @@ +// License: APL 2.0 +// Author: Benjamin N. Summerton + +using System; +using System.Text; +using System.Runtime.InteropServices; + +using HostApiIndex = System.Int32; +using Time = System.Double; + +namespace PortAudio +{ + /// + /// A structure providing information and capabilities of PortAudio devices. + /// Devices may support input, output or both input and output. + /// + [StructLayout(LayoutKind.Sequential)] + public struct DeviceInfo + { + public int structVersion; // this is struct version 2 + + [MarshalAs(UnmanagedType.LPStr)] + public string name; // Originally: `const char *` + + public HostApiIndex hostApi; // note this is a host API index, not a type id + + public int maxInputChannels; + public int maxOutputChannels; + + // Default latency values for interactive performance. + public Time defaultLowInputLatency; + public Time defaultLowOutputLatency; + + // Default latency values for robust non-interactive applications (eg. playing sound files). + public Time defaultHighInputLatency; + public Time defaultHighOutputLatency; + + public double defaultSampleRate; + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("DeviceInfo ["); + sb.AppendLine($" structVersion={structVersion}"); + sb.AppendLine($" name={name}"); + sb.AppendLine($" hostApi={hostApi}"); + sb.AppendLine($" maxInputChannels={maxInputChannels}"); + sb.AppendLine($" maxOutputChannels={maxOutputChannels}"); + sb.AppendLine($" defaultSampleRate={defaultSampleRate}"); + sb.AppendLine($" defaultLowInputLatency={defaultLowInputLatency}"); + sb.AppendLine($" defaultLowOutputLatency={defaultLowOutputLatency}"); + sb.AppendLine($" defaultHighInputLatency={defaultHighInputLatency}"); + sb.AppendLine($" defaultHighOutputLatency={defaultHighOutputLatency}"); + sb.AppendLine($" defaultHighSampleRate={defaultSampleRate}"); + sb.AppendLine("]"); + return sb.ToString(); + } + } +} diff --git a/VG Music Studio - Core/PortAudio/Structures/HostApiInfo.cs b/VG Music Studio - Core/PortAudio/Structures/HostApiInfo.cs new file mode 100644 index 00000000..9ed1c5a0 --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Structures/HostApiInfo.cs @@ -0,0 +1,50 @@ +using System.Runtime.InteropServices; + +using DeviceIndex = int; + +namespace PortAudio; + +/// +/// A structure containing information about a particular host API. +/// +[StructLayout(LayoutKind.Sequential)] +public struct HostApiInfo +{ + /// + /// this is struct version 1 + /// + public int StructVersion; + + /// + /// The well known unique identifier of this host API @see PaHostApiTypeId + /// + public HostApiTypeId Type; + + /// + /// A textual description of the host API for display on user interfaces. Encoded as UTF-8. + /// + [MarshalAs(UnmanagedType.LPStr)] + public string Name; + + /// + /// The number of devices belonging to this host API. This field may be + /// used in conjunction with Pa_HostApiDeviceIndexToDeviceIndex() to enumerate + /// all devices for this host API. + /// @see Pa_HostApiDeviceIndexToDeviceIndex + /// + public int DeviceCount; + + /// + /// The default input device for this host API. The value will be a + /// device index ranging from 0 to (Pa_GetDeviceCount()-1), or paNoDevice + /// if no default input device is available. + /// + public DeviceIndex DefaultInputDevice; + + /// + /// The default output device for this host API. The value will be a + /// device index ranging from 0 to (Pa_GetDeviceCount()-1), or paNoDevice + /// if no default output device is available. + /// + public DeviceIndex DefaultOutputDevice; +} \ No newline at end of file diff --git a/VG Music Studio - Core/PortAudio/Structures/HostErrorInfo.cs b/VG Music Studio - Core/PortAudio/Structures/HostErrorInfo.cs new file mode 100644 index 00000000..e21eefbd --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Structures/HostErrorInfo.cs @@ -0,0 +1,19 @@ +namespace PortAudio; + +public struct HostErrorInfo +{ + /// + /// The host API which returned the error code. + /// + public HostApiTypeId HostApiType; + + /// + /// The error code returned. + /// + public long ErrorCode; + + /// + /// A textual description of the error if available (encoded as UTF-8), otherwise a zero-length C string. + /// + public string ErrorText; +} \ No newline at end of file diff --git a/VG Music Studio - Core/PortAudio/Structures/StreamCallbackTimeInfo.cs b/VG Music Studio - Core/PortAudio/Structures/StreamCallbackTimeInfo.cs new file mode 100644 index 00000000..4ad4a41b --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Structures/StreamCallbackTimeInfo.cs @@ -0,0 +1,36 @@ +// License: APL 2.0 +// Author: Benjamin N. Summerton + +using System; +using System.Runtime.InteropServices; + +using Time = System.Double; + +namespace PortAudio +{ + /// + /// Timing information for the buffers passed to the stream callback. + /// + /// Time values are expressed in seconds and are synchronised with the time base used by Pa_GetStreamTime() for the associated stream. + /// + /// @see PaStreamCallback, Pa_GetStreamTime + /// + [StructLayout(LayoutKind.Sequential)] + public struct StreamCallbackTimeInfo + { + /// + /// The time when the first sample of the input buffer was captured at the ADC input + /// + public Time inputBufferAdcTime; + + /// + /// The time when the stream callback was invoked + /// + public Time currentTime; + + /// + /// The time when the first sample of the output buffer will output the DAC + /// + public Time outputBufferDacTime; + } +} diff --git a/VG Music Studio - Core/PortAudio/Structures/StreamInfo.cs b/VG Music Studio - Core/PortAudio/Structures/StreamInfo.cs new file mode 100644 index 00000000..6184925a --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Structures/StreamInfo.cs @@ -0,0 +1,39 @@ +using Time = double; + +namespace PortAudio; + +public struct StreamInfo +{ + /// + /// This is struct version 1 + /// + public int StructVersion; + + /// + /// The input latency of the stream in seconds. This value provides the most + /// accurate estimate of input latency available to the implementation. It may + /// differ significantly from the suggestedLatency value passed to Pa_OpenStream(). + /// The value of this field will be zero (0.) for output-only streams. + /// @see PaTime + /// + public Time InputLatency; + + /// + /// The output latency of the stream in seconds. This value provides the most + /// accurate estimate of output latency available to the implementation. It may + /// differ significantly from the suggestedLatency value passed to Pa_OpenStream(). + /// The value of this field will be zero (0.) for input-only streams. + /// @see PaTime + /// + public Time OutputLatency; + + /// + /// The sample rate of the stream in Hertz (samples per second). In cases + /// where the hardware sample rate is inaccurate and PortAudio is aware of it, + /// the value of this field may be different from the sampleRate parameter + /// passed to Pa_OpenStream(). If information about the actual hardware sample + /// rate is not available, this field will have the same value as the sampleRate + /// parameter passed to Pa_OpenStream(). + /// + public double SampleRate; +} \ No newline at end of file diff --git a/VG Music Studio - Core/PortAudio/Structures/StreamParameters.cs b/VG Music Studio - Core/PortAudio/Structures/StreamParameters.cs new file mode 100644 index 00000000..31d85781 --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Structures/StreamParameters.cs @@ -0,0 +1,78 @@ +// License: APL 2.0 +// Author: Benjamin N. Summerton + +using System; +using System.Text; +using System.Runtime.InteropServices; + +using DeviceIndex = System.Int32; +using Time = System.Double; + +namespace PortAudio +{ + /// + /// Parameters for one direction (input or output) of a stream. + /// + [StructLayout(LayoutKind.Sequential)] + public struct StreamParameters + { + /// + /// A valid device index in the range 0 to (Pa_GetDeviceCount()-1) + /// specifying the device to be used or the special constant + /// paUseHostApiSpecificDeviceSpecification which indicates that the actual + /// device(s) to use are specified in hostApiSpecificStreamInfo. + /// This field must not be set to paNoDevice. + /// + public DeviceIndex Device; + + /// + /// The number of channels of sound to be delivered to the + /// stream callback or accessed by Pa_ReadStream() or Pa_WriteStream(). + /// It can range from 1 to the value of maxInputChannels in the + /// PaDeviceInfo record for the device specified by the device parameter. + /// + public int Channels; + + /// + /// The sample format of the buffer provided to the stream callback, + /// a_ReadStream() or Pa_WriteStream(). It may be any of the formats described + /// by the PaSampleFormat enumeration. + /// + public SampleFormat SampleFormat; + + /// + /// The desired latency in seconds. Where practical, implementations should + /// configure their latency based on these parameters, otherwise they may + /// choose the closest viable latency instead. Unless the suggested latency + /// is greater than the absolute upper limit for the device implementations + /// should round the suggestedLatency up to the next practical value - ie to + /// provide an equal or higher latency than suggestedLatency wherever possible. + /// Actual latency values for an open stream may be retrieved using the + /// inputLatency and outputLatency fields of the PaStreamInfo structure + /// returned by Pa_GetStreamInfo(). + /// @see default*Latency in PaDeviceInfo, *Latency in PaStreamInfo + /// + public Time SuggestedLatency; + + /// + /// An optional pointer to a host api specific data structure + /// containing additional information for device setup and/or stream processing. + /// hostApiSpecificStreamInfo is never required for correct operation, + /// if not used it should be set to NULL. + /// + public IntPtr HostApiSpecificStreamInfo; // Originally `void *` + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("StreamParameters ["); + sb.AppendLine($" device={Device}"); + sb.AppendLine($" channelCount={Channels}"); + sb.AppendLine($" sampleFormat={SampleFormat}"); + sb.AppendLine($" suggestedLatency={SuggestedLatency}"); + sb.AppendLine($" hostApiSpecificStreamInfo?=[{HostApiSpecificStreamInfo != IntPtr.Zero}]"); + sb.AppendLine("]"); + return sb.ToString(); + } + } +} diff --git a/VG Music Studio - Core/PortAudio/Structures/VersionInfo.cs b/VG Music Studio - Core/PortAudio/Structures/VersionInfo.cs new file mode 100644 index 00000000..69efccd0 --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Structures/VersionInfo.cs @@ -0,0 +1,38 @@ +// License: APL 2.0 +// Author: Benjamin N. Summerton + +using System; +using System.Runtime.InteropServices; + +namespace PortAudio +{ + /// + /// A structure containing PortAudio API version information. + /// @see Pa_GetVersionInfo, paMakeVersionNumber + /// @version Available as of 19.5.0. + /// + [StructLayout(LayoutKind.Sequential)] + public struct VersionInfo + { + public int versionMajor; + public int versionMinor; + public int versionSubMinor; + + /// + /// This is currently the Git revision hash but may change in the future. + /// The versionControlRevision is updated by running a script before compiling the library. + /// If the update does not occur, this value may refer to an earlier revision. + /// + [MarshalAs(UnmanagedType.LPStr)] + public string versionControlRevision; // Orignally `const char *` + + /// + /// Version as a string, for example "PortAudio V19.5.0-devel, revision 1952M" + /// + [MarshalAs(UnmanagedType.LPStr)] + public string versionText; // Orignally `const char *` + + public override string ToString() => + $"VersionInfo: v{versionMajor}.{versionMinor}.{versionSubMinor}"; + } +} diff --git a/VG Music Studio - Core/Properties/Strings.Designer.cs b/VG Music Studio - Core/Properties/Strings.Designer.cs old mode 100644 new mode 100755 index eea96fb7..31299297 --- a/VG Music Studio - Core/Properties/Strings.Designer.cs +++ b/VG Music Studio - Core/Properties/Strings.Designer.cs @@ -60,6 +60,78 @@ internal Strings() { } } + /// + /// Looks up a localized string similar to Definition. + /// + public static string AssemblerDefinition { + get { + return ResourceManager.GetString("AssemblerDefinition", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Definitions cannot start with a digit.. + /// + public static string AssemblerErrorDefinitionDigit { + get { + return ResourceManager.GetString("AssemblerErrorDefinitionDigit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid value: {0}. + /// + public static string AssemblerErrorInvalidValue { + get { + return ResourceManager.GetString("AssemblerErrorInvalidValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open File. + /// + public static string AssemblerOpenFile { + get { + return ResourceManager.GetString("AssemblerOpenFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Preview Song. + /// + public static string AssemblerPreviewSong { + get { + return ResourceManager.GetString("AssemblerPreviewSong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Size in bytes: {0}. + /// + public static string AssemblerSizeInBytes { + get { + return ResourceManager.GetString("AssemblerSizeInBytes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ASM Assembler. + /// + public static string AssemblerTitle { + get { + return ResourceManager.GetString("AssemblerTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value. + /// + public static string AssemblerValue { + get { + return ResourceManager.GetString("AssemblerValue", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0} key. /// @@ -205,7 +277,7 @@ public static string ErrorDSEInvalidKey { } /// - /// Looks up a localized string similar to There are no "bgm(NNNN).smd" files.. + /// Looks up a localized string similar to There are no SMD files.. /// public static string ErrorDSENoSequences { get { @@ -213,6 +285,36 @@ public static string ErrorDSENoSequences { } } + /// + /// Looks up a localized string similar to There's not enough Program Infos (instruments) in the loaded SWD to load this song. + /// + /// Voice key index requested: {0} + /// Number of ProgramInfos (Voices): {1}. + /// + public static string ErrorDSEVoiceIndexOutOfRange { + get { + return ResourceManager.GetString("ErrorDSEVoiceIndexOutOfRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Converting MIDI files to this game engine is not supported at this time.. + /// + public static string ErrorEngineOpenMIDI { + get { + return ResourceManager.GetString("ErrorEngineOpenMIDI", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Exporting to ASM from this game engine is not supported at this time.. + /// + public static string ErrorEngineSaveASM { + get { + return ResourceManager.GetString("ErrorEngineSaveASM", resourceCulture); + } + } + /// /// Looks up a localized string similar to Error Loading Global Config. /// @@ -294,6 +396,15 @@ public static string ErrorParseConfig { } } + /// + /// Looks up a localized string similar to Error Exporting ASM. + /// + public static string ErrorSaveASM { + get { + return ResourceManager.GetString("ErrorSaveASM", resourceCulture); + } + } + /// /// Looks up a localized string similar to Error Exporting DLS. /// @@ -339,6 +450,15 @@ public static string ErrorSDATNoSequences { } } + /// + /// Looks up a localized string similar to Error Assembling File. + /// + public static string ErrorTitleAssembler { + get { + return ResourceManager.GetString("ErrorTitleAssembler", resourceCulture); + } + } + /// /// Looks up a localized string similar to "{0}" is not an integer value.. /// @@ -358,7 +478,25 @@ public static string ErrorValueParseRanged { } /// - /// Looks up a localized string similar to GBA Files. + /// Looks up a localized string similar to All files (*.*). + /// + public static string FilterAllFiles { + get { + return ResourceManager.GetString("FilterAllFiles", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ASM files. + /// + public static string FilterOpenASM { + get { + return ResourceManager.GetString("FilterOpenASM", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Game Boy Advance ROM (*.gba, *.srl). /// public static string FilterOpenGBA { get { @@ -367,7 +505,16 @@ public static string FilterOpenGBA { } /// - /// Looks up a localized string similar to SDAT Files. + /// Looks up a localized string similar to MIDI files. + /// + public static string FilterOpenMIDI { + get { + return ResourceManager.GetString("FilterOpenMIDI", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Nitro Soundmaker Sound Data (*.sdat). /// public static string FilterOpenSDAT { get { @@ -376,7 +523,25 @@ public static string FilterOpenSDAT { } /// - /// Looks up a localized string similar to DLS Files. + /// Looks up a localized string similar to Wave Data. + /// + public static string FilterOpenSWD { + get { + return ResourceManager.GetString("FilterOpenSWD", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ASM file. + /// + public static string FilterSaveASM { + get { + return ResourceManager.GetString("FilterSaveASM", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DLS Format (*.dls). /// public static string FilterSaveDLS { get { @@ -385,7 +550,7 @@ public static string FilterSaveDLS { } /// - /// Looks up a localized string similar to MIDI Files. + /// Looks up a localized string similar to MIDI Format (*.mid, *.midi). /// public static string FilterSaveMIDI { get { @@ -394,7 +559,7 @@ public static string FilterSaveMIDI { } /// - /// Looks up a localized string similar to SF2 Files. + /// Looks up a localized string similar to SoundFont2 Format (*.sf2). /// public static string FilterSaveSF2 { get { @@ -403,7 +568,7 @@ public static string FilterSaveSF2 { } /// - /// Looks up a localized string similar to WAV Files. + /// Looks up a localized string similar to RIFF Wave (*.wav). /// public static string FilterSaveWAV { get { @@ -411,6 +576,15 @@ public static string FilterSaveWAV { } } + /// + /// Looks up a localized string similar to Internal Song Name. + /// + public static string InternalSongName { + get { + return ResourceManager.GetString("InternalSongName", resourceCulture); + } + } + /// /// Looks up a localized string similar to Data. /// @@ -420,6 +594,15 @@ public static string MenuData { } } + /// + /// Looks up a localized string similar to Edit. + /// + public static string MenuEdit { + get { + return ResourceManager.GetString("MenuEdit", resourceCulture); + } + } + /// /// Looks up a localized string similar to End Current Playlist. /// @@ -448,7 +631,16 @@ public static string MenuOpenAlphaDream { } /// - /// Looks up a localized string similar to Open DSE Folder. + /// Looks up a localized string similar to Open ASM. + /// + public static string MenuOpenASM { + get { + return ResourceManager.GetString("MenuOpenASM", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open DSE Files. /// public static string MenuOpenDSE { get { @@ -456,6 +648,15 @@ public static string MenuOpenDSE { } } + /// + /// Looks up a localized string similar to Open MIDI. + /// + public static string MenuOpenMIDI { + get { + return ResourceManager.GetString("MenuOpenMIDI", resourceCulture); + } + } + /// /// Looks up a localized string similar to Open GBA ROM (MP2K). /// @@ -474,6 +675,24 @@ public static string MenuOpenSDAT { } } + /// + /// Looks up a localized string similar to Open Folder Containing SMD Files. + /// + public static string MenuOpenSMD { + get { + return ResourceManager.GetString("MenuOpenSMD", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open Main SWD File. + /// + public static string MenuOpenSWD { + get { + return ResourceManager.GetString("MenuOpenSWD", resourceCulture); + } + } + /// /// Looks up a localized string similar to Playlist. /// @@ -483,6 +702,33 @@ public static string MenuPlaylist { } } + /// + /// Looks up a localized string similar to Play Current Playlist. + /// + public static string MenuPlayPlaylist { + get { + return ResourceManager.GetString("MenuPlayPlaylist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Preferences. + /// + public static string MenuPreferences { + get { + return ResourceManager.GetString("MenuPreferences", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Export Song To ASM. + /// + public static string MenuSaveASM { + get { + return ResourceManager.GetString("MenuSaveASM", resourceCulture); + } + } + /// /// Looks up a localized string similar to Export VoiceTable as DLS. /// @@ -519,6 +765,51 @@ public static string MenuSaveWAV { } } + /// + /// Looks up a localized string similar to There was an error converting the MIDI file:{0}. + /// + public static string MIDIConverterError { + get { + return ResourceManager.GetString("MIDIConverterError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open MIDI. + /// + public static string MIDIConverterOpenFile { + get { + return ResourceManager.GetString("MIDIConverterOpenFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Preview Song. + /// + public static string MIDIConverterPreviewSong { + get { + return ResourceManager.GetString("MIDIConverterPreviewSong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MIDI Converter. + /// + public static string MIDIConverterTitle { + get { + return ResourceManager.GetString("MIDIConverterTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Converting MIDI. + /// + public static string MIDIConverterTitleError { + get { + return ResourceManager.GetString("MIDIConverterTitleError", resourceCulture); + } + } + /// /// Looks up a localized string similar to Next Song. /// @@ -573,6 +864,15 @@ public static string PlayerPreviousSong { } } + /// + /// Looks up a localized string similar to Record Song. + /// + public static string PlayerRecord { + get { + return ResourceManager.GetString("PlayerRecord", resourceCulture); + } + } + /// /// Looks up a localized string similar to Rest. /// @@ -619,7 +919,7 @@ public static string PlayerUnpause { } /// - /// Looks up a localized string similar to Music. + /// Looks up a localized string similar to All Songs. /// public static string PlaylistMusic { get { @@ -645,6 +945,51 @@ public static string Song_s_ { } } + /// + /// Looks up a localized string similar to Address. + /// + public static string SoundBankEditorAddress { + get { + return ResourceManager.GetString("SoundBankEditorAddress", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sound Bank Editor. + /// + public static string SoundBankEditorTitle { + get { + return ResourceManager.GetString("SoundBankEditorTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Voice Group Editor. + /// + public static string VoiceGroupEditorTitle { + get { + return ResourceManager.GetString("VoiceGroupEditorTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Voice Group / Sound Bank Editor. + /// + public static string MenuVoiceGroupSoundBankEditor { + get { + return ResourceManager.GetString("MenuVoiceGroupSoundBankEditor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ASM saved to {0}.. + /// + public static string SuccessSaveASM { + get { + return ResourceManager.GetString("SuccessSaveASM", resourceCulture); + } + } + /// /// Looks up a localized string similar to VoiceTable saved to {0}.. /// @@ -681,57 +1026,147 @@ public static string SuccessSaveWAV { } } + /// + /// Looks up a localized string similar to Open ASM. + /// + public static string TitleOpenASM { + get { + return ResourceManager.GetString("TitleOpenASM", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open MIDI. + /// + public static string TitleOpenMIDI { + get { + return ResourceManager.GetString("TitleOpenMIDI", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Export ASM File. + /// + public static string TitleSaveASM { + get { + return ResourceManager.GetString("TitleSaveASM", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add Event. + /// + public static string TrackEditorAddEvent { + get { + return ResourceManager.GetString("TrackEditorAddEvent", resourceCulture); + } + } + /// /// Looks up a localized string similar to Arguments. /// - public static string TrackViewerArguments { + public static string TrackEditorArguments { get { - return ResourceManager.GetString("TrackViewerArguments", resourceCulture); + return ResourceManager.GetString("TrackEditorArguments", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Arg. {0}. + /// + public static string TrackEditorArgX { + get { + return ResourceManager.GetString("TrackEditorArgX", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change Voices. + /// + public static string TrackEditorChangeVoices { + get { + return ResourceManager.GetString("TrackEditorChangeVoices", resourceCulture); } } /// /// Looks up a localized string similar to Event. /// - public static string TrackViewerEvent { + public static string TrackEditorEvent { + get { + return ResourceManager.GetString("TrackEditorEvent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to From. + /// + public static string TrackEditorFrom { get { - return ResourceManager.GetString("TrackViewerEvent", resourceCulture); + return ResourceManager.GetString("TrackEditorFrom", resourceCulture); } } /// /// Looks up a localized string similar to Offset. /// - public static string TrackViewerOffset { + public static string TrackEditorOffset { get { - return ResourceManager.GetString("TrackViewerOffset", resourceCulture); + return ResourceManager.GetString("TrackEditorOffset", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove Event. + /// + public static string TrackEditorRemoveEvent { + get { + return ResourceManager.GetString("TrackEditorRemoveEvent", resourceCulture); } } /// /// Looks up a localized string similar to Ticks. /// - public static string TrackViewerTicks { + public static string TrackEditorTicks { get { - return ResourceManager.GetString("TrackViewerTicks", resourceCulture); + return ResourceManager.GetString("TrackEditorTicks", resourceCulture); } } /// /// Looks up a localized string similar to Track Viewer. /// - public static string TrackViewerTitle { + public static string TrackEditorTitle { + get { + return ResourceManager.GetString("TrackEditorTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to To. + /// + public static string TrackEditorTo { get { - return ResourceManager.GetString("TrackViewerTitle", resourceCulture); + return ResourceManager.GetString("TrackEditorTo", resourceCulture); } } /// /// Looks up a localized string similar to Track {0}. /// - public static string TrackViewerTrackX { + public static string TrackEditorTrackX { + get { + return ResourceManager.GetString("TrackEditorTrackX", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Argument Editor. + /// + public static string TrackEditorArgEditor { get { - return ResourceManager.GetString("TrackViewerTrackX", resourceCulture); + return ResourceManager.GetString("TrackEditorArgEditor", resourceCulture); } } } diff --git a/VG Music Studio - Core/Properties/Strings.es.resx b/VG Music Studio - Core/Properties/Strings.es.resx old mode 100644 new mode 100755 index c524dfd2..c0164f05 --- a/VG Music Studio - Core/Properties/Strings.es.resx +++ b/VG Music Studio - Core/Properties/Strings.es.resx @@ -117,65 +117,38 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - ¿Quisiera detener la Lista de Repoducción actual? - Error al Cargar la Canción {0} - - Error al Abrir Carpeta DSE - Error al Abrir GBA ROM (AlphaDream) - - Error al Abrir GBA ROM (MP2K) - - - Error al Abrir Archivo SDAT - Error al Exportar MIDI Archivo GBA - - Archivo SDAT - Archivos MIDI Datos - - Detener Lista de Reproducción Actual - Archivo - - Abrir Carpeta DSE - - - Abrir GBA ROM (AlphaDream) - Abrir GBA ROM (MP2K) - - Abrir Archivo SDAT - - - Lista de Reproducción - Exportar Canción como MIDI - - Siguiente Canción + + Exportar Canción como ASM + + + Retraso Notas @@ -189,15 +162,12 @@ Posición - - Canción Anterior - - - Retraso - Detener + + Grabar Canción + Tempo @@ -207,31 +177,70 @@ Resumir - - Música + + MIDI guardado en {0}. ¿Quisiera reproducir la siguiente Lista de Reproducción? - - MIDI guardado en {0}. + + Siguiente Canción + + + Canción Anterior + + + ¿Quisiera detener la Lista de Repoducción actual? + + + Error al Abrir Carpeta DSE - + + Error al Abrir GBA ROM (MP2K) + + + Error al Abrir Archivo SDAT + + + Archivo SDAT + + + Reproducir Lista de Reproducción Actual + + + Detener Lista de Reproducción Actual + + + Abrir GBA ROM (AlphaDream) + + + Abrir Archivo SDAT + + + Lista de Reproducción + + + Nombre Interno de la Canción + + + Todas las Canciones + + Argumento - + Evento - + Offset - + Ticks - + Visor de Eventos - + Pista {0} @@ -265,7 +274,7 @@ Comando inválido en la pista {0} en 0x{1:X}: 0x{2:X} - No hay ningún archivo "bgm(NNNN).smd". + No hay ningún archivo SMD. Error al Cargar la Configuración Global @@ -342,4 +351,7 @@ canciones|0_0|canción|1_1|canciones|2_*| + + Abrir Carpeta DSE + \ 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 old mode 100644 new mode 100755 index 0befad50..5040f921 --- a/VG Music Studio - Core/Properties/Strings.fr.resx +++ b/VG Music Studio - Core/Properties/Strings.fr.resx @@ -144,6 +144,9 @@ Exporter le titre en MIDI + + Exporter le titre en ASM + Silence @@ -160,7 +163,10 @@ Position - Stop + Arrêt + + + Enregistrer une Chanson Tempo @@ -198,12 +204,12 @@ Fichiers SDAT + + Lecture la playlist en cours + Arrêter la playlist en cours - - Ouvrir dossier DSE - Ouvrir ROM GBA (AlphaDream) @@ -213,25 +219,28 @@ Playlist + + Nom de la Chanson Interne + - Musique + Toutes les Chansons - + Arguments - + Event - + Offset - + Ticks - + Visualiseur de pistes - + Piste {0} @@ -265,7 +274,7 @@ Commande invalide pour la piste {0} à l'adresse 0x{1:X}: 0x{2:X} - Il n'y a pas de fichiers "bgm(NNNN).smd". + Il n'y a pas de fichiers SMD. Echec du chargement de la configuration globale. @@ -342,4 +351,7 @@ titres|0_0|titre|1_1|titres|2_*| + + Ouvrir dossier DSE + \ 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 old mode 100644 new mode 100755 index 6da605e1..06727854 --- a/VG Music Studio - Core/Properties/Strings.it.resx +++ b/VG Music Studio - Core/Properties/Strings.it.resx @@ -1,6 +1,6 @@  - + + + + + + + + + + + + + + + + + + Dependencies\DLS2.dll - - Dependencies\KMIDI.dll - Dependencies\SoundFont2.dll diff --git a/VG Music Studio - Core/Wii/WiiUtils.cs b/VG Music Studio - Core/Wii/WiiUtils.cs new file mode 100644 index 00000000..9bc65932 --- /dev/null +++ b/VG Music Studio - Core/Wii/WiiUtils.cs @@ -0,0 +1,9 @@ +namespace Kermalis.VGMusicStudio.Core.Wii +{ + internal static class WiiUtils + { + // (48 * 48 / 9) = 256, (256 * 65536) = 16,777,216 Hz = 16.777216 MHz, (16777216 * 7.24) = 121,467,043.84 Hz = 121.46704384 MHz + // It's close enough to the 121.5MHz mentioned in the RVL DSP spec sheet + public const int Macronix_DSP_Clock = 16_777_216; + } +} diff --git a/VG Music Studio - GTK4/AssemblerDialog.cs b/VG Music Studio - GTK4/AssemblerDialog.cs new file mode 100644 index 00000000..9bc71dff --- /dev/null +++ b/VG Music Studio - GTK4/AssemblerDialog.cs @@ -0,0 +1,129 @@ +// using Kermalis.VGMusicStudio.Core; +// using Kermalis.VGMusicStudio.Core.Properties; +// using Kermalis.VGMusicStudio.Core.GBA.MP2K; +// using Kermalis.VGMusicStudio.GTK4.Util; +// using System; +// using System.Collections.Generic; +// using System.ComponentModel; +// using System.Drawing; +// using System.IO; +// using System.Linq; +// using Adw; + +// namespace Kermalis.VGMusicStudio.GTK4; + +// internal class AssemblerDialog : Window +// { +// private readonly Assembler Assembler; +// private ILoadedSong Song; +// private readonly Gtk.Button PreviewButton; +// private readonly ValueTextBox OffsetValueBox; +// private readonly Gtk.Label SizeLabel; +// private readonly TextBox HeaderLabelTextBox; +// private readonly DataGridView AddedDefsGrid; + +// public AssemblerDialog() +// { +// var openButton = new Gtk.Button +// { +// Location = new Point(150, 0), +// Text = Strings.AssemblerOpenFile +// }; +// openButton.Click += OpenASM; +// PreviewButton = new Gtk.Button +// { +// Enabled = false, +// Location = new Point(150, 50), +// Size = new Size(120, 23), +// Text = Strings.AssemblerPreviewSong +// }; +// PreviewButton.Click += PreviewSong; +// SizeLabel = new Label +// { +// Location = new Point(0, 100), +// Size = new Size(150, 23) +// }; +// OffsetValueBox = new ValueTextBox +// { +// Hexadecimal = true, +// Maximum = ROM.Capacity - 1 +// }; +// HeaderLabelTextBox = new TextBox { Location = new Point(0, 50), Size = new Size(150, 22) }; +// AddedDefsGrid = new DataGridView +// { +// ColumnCount = 2, +// Location = new Point(0, 150), +// MultiSelect = false +// }; +// AddedDefsGrid.Columns[0].Name = Strings.AssemblerDefinition; +// AddedDefsGrid.Columns[1].Name = Strings.AssemblerValue; +// AddedDefsGrid.Columns[1].DefaultCellStyle.NullValue = "0"; +// AddedDefsGrid.Rows.Add(new string[] { "voicegroup000", $"0x{SongPlayer.Instance.Song.VoiceTable.GetOffset() + ROM.Pak:X7}" }); +// AddedDefsGrid.CellValueChanged += AddedDefsGrid_CellValueChanged; + +// Controls.AddRange(new Control[] { openButton, PreviewButton, SizeLabel, OffsetValueBox, HeaderLabelTextBox, AddedDefsGrid }); +// FormBorderStyle = FormBorderStyle.FixedDialog; +// MaximizeBox = false; +// Size = new Size(600, 400); +// Text = $"GBA Music Studio ― {Strings.AssemblerTitle}"; +// } + +// void AddedDefsGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e) +// { +// DataGridViewCell cell = AddedDefsGrid.Rows[e.RowIndex].Cells[e.ColumnIndex]; +// if (cell.Value == null) +// { +// return; +// } + +// if (e.ColumnIndex == 0) +// { +// if (char.IsDigit(cell.Value.ToString()[0])) +// { +// FlexibleDialog.Show(Strings.AssemblerErrorDefinitionDigit, Strings.TitleError, MessageBoxButtons.OK, MessageBoxIcon.Error); +// cell.Value = cell.Value.ToString().Substring(1); +// } +// } +// else +// { +// if (!Utils.TryParseValue(cell.Value.ToString(), out long val)) +// { +// FlexibleDialog.Show(string.Format(Strings.AssemblerErrorInvalidValue, cell.Value), Strings.TitleError, MessageBoxButtons.OK, MessageBoxIcon.Error); +// cell.Value = null; +// } +// } +// } +// void PreviewSong(object sender, EventArgs e) +// { +// ((MainForm)Owner).PreviewSong(Song, Path.GetFileName(Assembler.FileName)); +// } +// void OpenASM(object sender, EventArgs e) +// { +// var d = new Gtk.FileDialog { Title = Strings.TitleOpenASM, Filter = $"{Strings.FilterOpenASM}|*.s" }; +// if (d.ShowDialog() != DialogResult.OK) +// { +// return; +// } + +// try +// { +// var s = new Dictionary(); +// foreach (DataGridViewRow r in AddedDefsGrid.Rows.Cast()) +// { +// if (r.Cells[0].Value == null || r.Cells[1].Value == null) +// { +// continue; +// } +// s.Add(r.Cells[0].Value.ToString(), (int)Utils.ParseValue(r.Cells[1].Value.ToString())); +// } +// Song = new MP2KASMSong(Assembler = new Assembler(d.FileName, (int)(ROM.Pak + OffsetValueBox.Value), s), +// HeaderLabelTextBox.Text = Assembler.FixLabel(Path.GetFileNameWithoutExtension(d.FileName))); +// SizeLabel.Text = string.Format(Strings.AssemblerSizeInBytes, Assembler.BinaryLength); +// PreviewButton.Enabled = true; +// } +// catch (Exception ex) +// { +// FlexibleDialog.Show(ex.Message, Strings.TitleAssemblerError, MessageBoxButtons.OK, MessageBoxIcon.Error); +// } +// } +// } diff --git a/VG Music Studio - GTK4/ExtraLibBindings/Gtk.cs b/VG Music Studio - GTK4/ExtraLibBindings/Gtk.cs new file mode 100644 index 00000000..ebd27415 --- /dev/null +++ b/VG Music Studio - GTK4/ExtraLibBindings/Gtk.cs @@ -0,0 +1,199 @@ +//using System; +//using System.IO; +//using System.Reflection; +//using System.Runtime.InteropServices; +//using Gtk.Internal; + +//namespace Gtk; + +//internal partial class AlertDialog : GObject.Object +//{ +// protected AlertDialog(IntPtr handle, bool ownedRef) : base(handle, ownedRef) +// { +// } + +// [DllImport("Gtk", EntryPoint = "gtk_alert_dialog_new")] +// private static extern nint InternalNew(string format); + +// private static IntPtr ObjPtr; + +// internal static AlertDialog New(string format) +// { +// ObjPtr = InternalNew(format); +// return new AlertDialog(ObjPtr, true); +// } +//} + +//internal partial class FileDialog : GObject.Object +//{ +// [DllImport("GObject", EntryPoint = "g_object_unref")] +// private static extern void InternalUnref(nint obj); + +// [DllImport("Gio", EntryPoint = "g_task_return_value")] +// private static extern void InternalReturnValue(nint task, nint result); + +// [DllImport("Gio", EntryPoint = "g_file_get_path")] +// private static extern nint InternalGetPath(nint file); + +// [DllImport("Gtk", EntryPoint = "gtk_css_provider_load_from_data")] +// private static extern void InternalLoadFromData(nint provider, string data, int length); + +// [DllImport("Gtk", EntryPoint = "gtk_file_dialog_new")] +// private static extern nint InternalNew(); + +// [DllImport("Gtk", EntryPoint = "gtk_file_dialog_get_initial_file")] +// private static extern nint InternalGetInitialFile(nint dialog); + +// [DllImport("Gtk", EntryPoint = "gtk_file_dialog_get_initial_folder")] +// private static extern nint InternalGetInitialFolder(nint dialog); + +// [DllImport("Gtk", EntryPoint = "gtk_file_dialog_get_initial_name")] +// private static extern string InternalGetInitialName(nint dialog); + +// [DllImport("Gtk", EntryPoint = "gtk_file_dialog_set_title")] +// private static extern void InternalSetTitle(nint dialog, string title); + +// [DllImport("Gtk", EntryPoint = "gtk_file_dialog_set_filters")] +// private static extern void InternalSetFilters(nint dialog, nint filters); + +// internal delegate void GAsyncReadyCallback(nint source, nint res, nint user_data); + +// [DllImport("Gtk", EntryPoint = "gtk_file_dialog_open")] +// private static extern void InternalOpen(nint dialog, nint parent, nint cancellable, GAsyncReadyCallback callback, nint user_data); + +// [DllImport("Gtk", EntryPoint = "gtk_file_dialog_open_finish")] +// private static extern nint InternalOpenFinish(nint dialog, nint result, nint error); + +// [DllImport("Gtk", EntryPoint = "gtk_file_dialog_save")] +// private static extern void InternalSave(nint dialog, nint parent, nint cancellable, GAsyncReadyCallback callback, nint user_data); + +// [DllImport("Gtk", EntryPoint = "gtk_file_dialog_save_finish")] +// private static extern nint InternalSaveFinish(nint dialog, nint result, nint error); + +// [DllImport("Gtk", EntryPoint = "gtk_file_dialog_select_folder")] +// private static extern void InternalSelectFolder(nint dialog, nint parent, nint cancellable, GAsyncReadyCallback callback, nint user_data); + +// [DllImport("Gtk", EntryPoint = "gtk_file_dialog_select_folder_finish")] +// private static extern nint InternalSelectFolderFinish(nint dialog, nint result, nint error); + + +// private static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); +// private static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); +// private static bool IsFreeBSD() => RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD); +// private static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + +// private static IntPtr ObjPtr; + +// // Based on the code from the Nickvision Application template https://github.com/NickvisionApps/Application +// // Code reference: https://github.com/NickvisionApps/Application/blob/28e3307b8242b2d335f8f65394a03afaf213363a/NickvisionApplication.GNOME/Program.cs#L50 +// private static void ImportNativeLibrary() => NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), LibraryImportResolver); + +// // Code reference: https://github.com/NickvisionApps/Application/blob/28e3307b8242b2d335f8f65394a03afaf213363a/NickvisionApplication.GNOME/Program.cs#L136 +// private static IntPtr LibraryImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) +// { +// string fileName; +// if (IsWindows()) +// { +// fileName = libraryName switch +// { +// "GObject" => "libgobject-2.0-0.dll", +// "Gio" => "libgio-2.0-0.dll", +// "Gtk" => "libgtk-4-1.dll", +// _ => libraryName +// }; +// } +// else if (IsMacOS()) +// { +// fileName = libraryName switch +// { +// "GObject" => "libgobject-2.0.0.dylib", +// "Gio" => "libgio-2.0.0.dylib", +// "Gtk" => "libgtk-4.1.dylib", +// _ => libraryName +// }; +// } +// else +// { +// fileName = libraryName switch +// { +// "GObject" => "libgobject-2.0.so.0", +// "Gio" => "libgio-2.0.so.0", +// "Gtk" => "libgtk-4.so.1", +// _ => libraryName +// }; +// } +// return NativeLibrary.Load(fileName, assembly, searchPath); +// } + +// private FileDialog(IntPtr handle, bool ownedRef) : base(handle, ownedRef) +// { +// } + +// // GtkFileDialog* gtk_file_dialog_new (void) +// internal static FileDialog New() +// { +// ImportNativeLibrary(); +// ObjPtr = InternalNew(); +// return new FileDialog(ObjPtr, true); +// } + +// // void gtk_file_dialog_open (GtkFileDialog* self, GtkWindow* parent, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer user_data) +// internal void Open(nint parent, nint cancellable, GAsyncReadyCallback callback, nint user_data) => InternalOpen(ObjPtr, parent, cancellable, callback, user_data); + +// // GFile* gtk_file_dialog_open_finish (GtkFileDialog* self, GAsyncResult* result, GError** error) +// internal nint OpenFinish(nint result, nint error) +// { +// return InternalOpenFinish(ObjPtr, result, error); +// } + +// // void gtk_file_dialog_save (GtkFileDialog* self, GtkWindow* parent, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer user_data) +// internal void Save(nint parent, nint cancellable, GAsyncReadyCallback callback, nint user_data) => InternalSave(ObjPtr, parent, cancellable, callback, user_data); + +// // GFile* gtk_file_dialog_save_finish (GtkFileDialog* self, GAsyncResult* result, GError** error) +// internal nint SaveFinish(nint result, nint error) +// { +// return InternalSaveFinish(ObjPtr, result, error); +// } + +// // void gtk_file_dialog_select_folder (GtkFileDialog* self, GtkWindow* parent, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer user_data) +// internal void SelectFolder(nint parent, nint cancellable, GAsyncReadyCallback callback, nint user_data) => InternalSelectFolder(ObjPtr, parent, cancellable, callback, user_data); + +// // GFile* gtk_file_dialog_select_folder_finish(GtkFileDialog* self, GAsyncResult* result, GError** error) +// internal nint SelectFolderFinish(nint result, nint error) +// { +// return InternalSelectFolderFinish(ObjPtr, result, error); +// } + +// // GFile* gtk_file_dialog_get_initial_file (GtkFileDialog* self) +// internal nint GetInitialFile() +// { +// return InternalGetInitialFile(ObjPtr); +// } + +// // GFile* gtk_file_dialog_get_initial_folder (GtkFileDialog* self) +// internal nint GetInitialFolder() +// { +// return InternalGetInitialFolder(ObjPtr); +// } + +// // const char* gtk_file_dialog_get_initial_name (GtkFileDialog* self) +// internal string GetInitialName() +// { +// return InternalGetInitialName(ObjPtr); +// } + +// // void gtk_file_dialog_set_title (GtkFileDialog* self, const char* title) +// internal void SetTitle(string title) => InternalSetTitle(ObjPtr, title); + +// // void gtk_file_dialog_set_filters (GtkFileDialog* self, GListModel* filters) +// internal void SetFilters(Gio.ListModel filters) => InternalSetFilters(ObjPtr, filters.Handle); + + + + + +// internal static nint GetPath(nint path) +// { +// return InternalGetPath(path); +// } +//} \ No newline at end of file diff --git a/VG Music Studio - GTK4/ExtraLibBindings/GtkInternal.cs b/VG Music Studio - GTK4/ExtraLibBindings/GtkInternal.cs new file mode 100644 index 00000000..125c4f73 --- /dev/null +++ b/VG Music Studio - GTK4/ExtraLibBindings/GtkInternal.cs @@ -0,0 +1,425 @@ +//using System; +//using System.Runtime.InteropServices; + +//namespace Gtk.Internal; + +//public partial class AlertDialog : GObject.Internal.Object +//{ +// protected AlertDialog(IntPtr handle, bool ownedRef) : base() +// { +// } + +// [DllImport("libgtk-4.so.1", EntryPoint = "gtk_alert_dialog_new")] +// private static extern nint linux_gtk_alert_dialog_new(string format); + +// [DllImport("libgtk-4.1.dylib", EntryPoint = "gtk_alert_dialog_new")] +// private static extern nint macos_gtk_alert_dialog_new(string format); + +// [DllImport("libgtk-4-1.dll", EntryPoint = "gtk_alert_dialog_new")] +// private static extern nint windows_gtk_alert_dialog_new(string format); + +// private static IntPtr ObjPtr; + +// public static AlertDialog New(string format) +// { +// if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +// { +// ObjPtr = linux_gtk_alert_dialog_new(format); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) +// { +// ObjPtr = macos_gtk_alert_dialog_new(format); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +// { +// ObjPtr = windows_gtk_alert_dialog_new(format); +// } +// return new AlertDialog(ObjPtr, true); +// } +//} + +//public partial class FileDialog : GObject.Internal.Object +//{ +// [DllImport("libgobject-2.0.so.0", EntryPoint = "g_object_unref")] +// private static extern void LinuxUnref(nint obj); + +// [DllImport("libgobject-2.0.0.dylib", EntryPoint = "g_object_unref")] +// private static extern void MacOSUnref(nint obj); + +// [DllImport("libgobject-2.0-0.dll", EntryPoint = "g_object_unref")] +// private static extern void WindowsUnref(nint obj); + +// [DllImport("libgio-2.0.so.0", EntryPoint = "g_task_return_value")] +// private static extern void LinuxReturnValue(nint task, nint result); + +// [DllImport("libgio-2.0.0.dylib", EntryPoint = "g_task_return_value")] +// private static extern void MacOSReturnValue(nint task, nint result); + +// [DllImport("libgio-2.0-0.dll", EntryPoint = "g_task_return_value")] +// private static extern void WindowsReturnValue(nint task, nint result); + +// [DllImport("libgio-2.0.so.0", EntryPoint = "g_file_get_path")] +// private static extern string LinuxGetPath(nint file); + +// [DllImport("libgio-2.0.0.dylib", EntryPoint = "g_file_get_path")] +// private static extern string MacOSGetPath(nint file); + +// [DllImport("libgio-2.0-0.dll", EntryPoint = "g_file_get_path")] +// private static extern string WindowsGetPath(nint file); + +// [DllImport("libgtk-4.so.1", EntryPoint = "gtk_css_provider_load_from_data")] +// private static extern void LinuxLoadFromData(nint provider, string data, int length); + +// [DllImport("libgtk-4.1.dylib", EntryPoint = "gtk_css_provider_load_from_data")] +// private static extern void MacOSLoadFromData(nint provider, string data, int length); + +// [DllImport("libgtk-4-1.dll", EntryPoint = "gtk_css_provider_load_from_data")] +// private static extern void WindowsLoadFromData(nint provider, string data, int length); + +// [DllImport("libgtk-4.so.1", EntryPoint = "gtk_file_dialog_new")] +// private static extern nint LinuxNew(); + +// [DllImport("libgtk-4.1.dylib", EntryPoint = "gtk_file_dialog_new")] +// private static extern nint MacOSNew(); + +// [DllImport("libgtk-4-1.dll", EntryPoint = "gtk_file_dialog_new")] +// private static extern nint WindowsNew(); + +// [DllImport("libgtk-4.so.1", EntryPoint = "gtk_file_dialog_get_initial_file")] +// private static extern nint LinuxGetInitialFile(nint dialog); + +// [DllImport("libgtk-4.1.dylib", EntryPoint = "gtk_file_dialog_get_initial_file")] +// private static extern nint MacOSGetInitialFile(nint dialog); + +// [DllImport("libgtk-4-1.dll", EntryPoint = "gtk_file_dialog_get_initial_file")] +// private static extern nint WindowsGetInitialFile(nint dialog); + +// [DllImport("libgtk-4.so.1", EntryPoint = "gtk_file_dialog_get_initial_folder")] +// private static extern nint LinuxGetInitialFolder(nint dialog); + +// [DllImport("libgtk-4.1.dylib", EntryPoint = "gtk_file_dialog_get_initial_folder")] +// private static extern nint MacOSGetInitialFolder(nint dialog); + +// [DllImport("libgtk-4-1.dll", EntryPoint = "gtk_file_dialog_get_initial_folder")] +// private static extern nint WindowsGetInitialFolder(nint dialog); + +// [DllImport("libgtk-4.so.1", EntryPoint = "gtk_file_dialog_get_initial_name")] +// private static extern string LinuxGetInitialName(nint dialog); + +// [DllImport("libgtk-4.1.dylib", EntryPoint = "gtk_file_dialog_get_initial_name")] +// private static extern string MacOSGetInitialName(nint dialog); + +// [DllImport("libgtk-4-1.dll", EntryPoint = "gtk_file_dialog_get_initial_name")] +// private static extern string WindowsGetInitialName(nint dialog); + +// [DllImport("libgtk-4.so.1", EntryPoint = "gtk_file_dialog_set_title")] +// private static extern void LinuxSetTitle(nint dialog, string title); + +// [DllImport("libgtk-4.1.dylib", EntryPoint = "gtk_file_dialog_set_title")] +// private static extern void MacOSSetTitle(nint dialog, string title); + +// [DllImport("libgtk-4-1.dll", EntryPoint = "gtk_file_dialog_set_title")] +// private static extern void WindowsSetTitle(nint dialog, string title); + +// [DllImport("libgtk-4.so.1", EntryPoint = "gtk_file_dialog_set_filters")] +// private static extern void LinuxSetFilters(nint dialog, Gio.Internal.ListModel filters); + +// [DllImport("libgtk-4.1.dylib", EntryPoint = "gtk_file_dialog_set_filters")] +// private static extern void MacOSSetFilters(nint dialog, Gio.Internal.ListModel filters); + +// [DllImport("libgtk-4-1.dll", EntryPoint = "gtk_file_dialog_set_filters")] +// private static extern void WindowsSetFilters(nint dialog, Gio.Internal.ListModel filters); + +// public delegate void GAsyncReadyCallback(nint source, nint res, nint user_data); + +// [DllImport("libgtk-4.so.1", EntryPoint = "gtk_file_dialog_open")] +// private static extern void LinuxOpen(nint dialog, Gtk.Internal.Window parent, Gio.Internal.Cancellable cancellable, Gio.Internal.AsyncReadyCallback callback, nint user_data); + +// [DllImport("libgtk-4.1.dylib", EntryPoint = "gtk_file_dialog_open")] +// private static extern void MacOSOpen(nint dialog, Gtk.Internal.Window parent, Gio.Internal.Cancellable cancellable, Gio.Internal.AsyncReadyCallback callback, nint user_data); + +// [DllImport("libgtk-4-1.dll", EntryPoint = "gtk_file_dialog_open")] +// private static extern void WindowsOpen(nint dialog, Gtk.Internal.Window parent, Gio.Internal.Cancellable cancellable, Gio.Internal.AsyncReadyCallback callback, nint user_data); + +// [DllImport("libgtk-4.so.1", EntryPoint = "gtk_file_dialog_open_finish")] +// private static extern nint LinuxOpenFinish(nint dialog, Gio.Internal.AsyncResult result, GLib.Internal.Error error); + +// [DllImport("libgtk-4.1.dylib", EntryPoint = "gtk_file_dialog_open_finish")] +// private static extern nint MacOSOpenFinish(nint dialog, Gio.Internal.AsyncResult result, GLib.Internal.Error error); + +// [DllImport("libgtk-4-1.dll", EntryPoint = "gtk_file_dialog_open_finish")] +// private static extern nint WindowsOpenFinish(nint dialog, Gio.Internal.AsyncResult result, GLib.Internal.Error error); + +// [DllImport("libgtk-4.so.1", EntryPoint = "gtk_file_dialog_save")] +// private static extern void LinuxSave(nint dialog, Gtk.Internal.Window parent, Gio.Internal.Cancellable cancellable, Gio.Internal.AsyncReadyCallback callback, nint user_data); + +// [DllImport("libgtk-4.1.dylib", EntryPoint = "gtk_file_dialog_save")] +// private static extern void MacOSSave(nint dialog, Gtk.Internal.Window parent, Gio.Internal.Cancellable cancellable, Gio.Internal.AsyncReadyCallback callback, nint user_data); + +// [DllImport("libgtk-4-1.dll", EntryPoint = "gtk_file_dialog_save")] +// private static extern void WindowsSave(nint dialog, Gtk.Internal.Window parent, Gio.Internal.Cancellable cancellable, Gio.Internal.AsyncReadyCallback callback, nint user_data); + +// [DllImport("libgtk-4.so.1", EntryPoint = "gtk_file_dialog_save_finish")] +// private static extern nint LinuxSaveFinish(nint dialog, Gio.Internal.AsyncResult result, GLib.Internal.Error error); + +// [DllImport("libgtk-4.1.dylib", EntryPoint = "gtk_file_dialog_save_finish")] +// private static extern nint MacOSSaveFinish(nint dialog, Gio.Internal.AsyncResult result, GLib.Internal.Error error); + +// [DllImport("libgtk-4-1.dll", EntryPoint = "gtk_file_dialog_save_finish")] +// private static extern nint WindowsSaveFinish(nint dialog, Gio.Internal.AsyncResult result, GLib.Internal.Error error); + +// [DllImport("libgtk-4.so.1", EntryPoint = "gtk_file_dialog_select_folder")] +// private static extern void LinuxSelectFolder(nint dialog, Gtk.Internal.Window parent, Gio.Internal.Cancellable cancellable, Gio.Internal.AsyncReadyCallback callback, nint user_data); + +// [DllImport("libgtk-4.1.dylib", EntryPoint = "gtk_file_dialog_select_folder")] +// private static extern void MacOSSelectFolder(nint dialog, Gtk.Internal.Window parent, Gio.Internal.Cancellable cancellable, Gio.Internal.AsyncReadyCallback callback, nint user_data); + +// [DllImport("libgtk-4-1.dll", EntryPoint = "gtk_file_dialog_select_folder")] +// private static extern void WindowsSelectFolder(nint dialog, Gtk.Internal.Window parent, Gio.Internal.Cancellable cancellable, Gio.Internal.AsyncReadyCallback callback, nint user_data); + +// [DllImport("libgtk-4.so.1", EntryPoint = "gtk_file_dialog_select_folder_finish")] +// private static extern nint LinuxSelectFolderFinish(nint dialog, Gio.Internal.AsyncResult result, GLib.Internal.Error error); + +// [DllImport("libgtk-4.1.dylib", EntryPoint = "gtk_file_dialog_select_folder_finish")] +// private static extern nint MacOSSelectFolderFinish(nint dialog, Gio.Internal.AsyncResult result, GLib.Internal.Error error); + +// [DllImport("libgtk-4-1.dll", EntryPoint = "gtk_file_dialog_select_folder_finish")] +// private static extern nint WindowsSelectFolderFinish(nint dialog, Gio.Internal.AsyncResult result, GLib.Internal.Error error); + +// private static IntPtr ObjPtr; +// private static IntPtr UserData; +// private GAsyncReadyCallback callbackHandle { get; set; } +// private static IntPtr FilePath; + +// private FileDialog(IntPtr handle, bool ownedRef) : base() +// { +// } + +// // void gtk_file_dialog_open (GtkFileDialog* self, GtkWindow* parent, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer user_data) +// public void Open(Gtk.Internal.Window parent, Gio.Internal.Cancellable cancellable, Gio.Internal.AsyncReadyCallback callback, int user_data) +// { +// if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +// { +// LinuxOpen(ObjPtr, parent, cancellable, callback, user_data); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) +// { +// MacOSOpen(ObjPtr, parent, cancellable, callback, user_data); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +// { +// WindowsOpen(ObjPtr, parent, cancellable, callback, user_data); +// } +// } + +// // GFile* gtk_file_dialog_open_finish (GtkFileDialog* self, GAsyncResult* result, GError** error) +// public Gio.Internal.File OpenFinish(Gio.Internal.AsyncResult result, GLib.Internal.Error error) +// { +// if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +// { +// LinuxOpenFinish(ObjPtr, result, error); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) +// { +// MacOSOpenFinish(ObjPtr, result, error); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +// { +// WindowsOpenFinish(ObjPtr, result, error); +// } +// return OpenFinish(result, error); +// } + +// // void gtk_file_dialog_save (GtkFileDialog* self, GtkWindow* parent, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer user_data) +// public void Save(Gtk.Internal.Window parent, Gio.Internal.Cancellable cancellable, Gio.Internal.AsyncReadyCallback callback, int user_data) +// { +// if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +// { +// LinuxSave(ObjPtr, parent, cancellable, callback, user_data); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) +// { +// MacOSSave(ObjPtr, parent, cancellable, callback, user_data); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +// { +// WindowsSave(ObjPtr, parent, cancellable, callback, user_data); +// } +// } + +// // GFile* gtk_file_dialog_save_finish (GtkFileDialog* self, GAsyncResult* result, GError** error) +// public Gio.Internal.File SaveFinish(Gio.Internal.AsyncResult result, GLib.Internal.Error error) +// { +// if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +// { +// LinuxSaveFinish(ObjPtr, result, error); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) +// { +// MacOSSaveFinish(ObjPtr, result, error); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +// { +// WindowsSaveFinish(ObjPtr, result, error); +// } +// return SaveFinish(result, error); +// } + +// // void gtk_file_dialog_select_folder (GtkFileDialog* self, GtkWindow* parent, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer user_data) +// public void SelectFolder(Gtk.Internal.Window parent, Gio.Internal.Cancellable cancellable, Gio.Internal.AsyncReadyCallback callback, int user_data) +// { +// // if (cancellable is null) +// // { +// // cancellable = Gio.Internal.Cancellable.New(); +// // cancellable.Handle.Equals(IntPtr.Zero); +// // cancellable.Cancel(); +// // UserData = IntPtr.Zero; +// // } + + +// // callback = (source, res) => +// // { +// // var data = new nint(); +// // callbackHandle.BeginInvoke(source.Handle, res.Handle, data, callback, callback); +// // }; +// if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +// { +// LinuxSelectFolder(ObjPtr, parent, cancellable, callback, user_data); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) +// { +// MacOSSelectFolder(ObjPtr, parent, cancellable, callback, UserData); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +// { +// WindowsSelectFolder(ObjPtr, parent, cancellable, callback, UserData); +// } +// } + +// // GFile* gtk_file_dialog_select_folder_finish(GtkFileDialog* self, GAsyncResult* result, GError** error) +// public Gio.Internal.File SelectFolderFinish(Gio.Internal.AsyncResult result, GLib.Internal.Error error) +// { +// if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +// { +// LinuxSelectFolderFinish(ObjPtr, result, error); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) +// { +// MacOSSelectFolderFinish(ObjPtr, result, error); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +// { +// WindowsSelectFolderFinish(ObjPtr, result, error); +// } +// return SelectFolderFinish(result, error); +// } + +// // GFile* gtk_file_dialog_get_initial_file (GtkFileDialog* self) +// public Gio.Internal.File GetInitialFile() +// { +// if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +// { +// LinuxGetInitialFile(ObjPtr); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) +// { +// MacOSGetInitialFile(ObjPtr); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +// { +// WindowsGetInitialFile(ObjPtr); +// } +// return GetInitialFile(); +// } + +// // GFile* gtk_file_dialog_get_initial_folder (GtkFileDialog* self) +// public Gio.Internal.File GetInitialFolder() +// { +// if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +// { +// LinuxGetInitialFolder(ObjPtr); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) +// { +// MacOSGetInitialFolder(ObjPtr); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +// { +// WindowsGetInitialFolder(ObjPtr); +// } +// return GetInitialFolder(); +// } + +// // const char* gtk_file_dialog_get_initial_name (GtkFileDialog* self) +// public string GetInitialName() +// { +// if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +// { +// return LinuxGetInitialName(ObjPtr); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) +// { +// return MacOSGetInitialName(ObjPtr); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +// { +// return WindowsGetInitialName(ObjPtr); +// } +// return GetInitialName(); +// } + +// // void gtk_file_dialog_set_title (GtkFileDialog* self, const char* title) +// public void SetTitle(string title) +// { +// if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +// { +// LinuxSetTitle(ObjPtr, title); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) +// { +// MacOSSetTitle(ObjPtr, title); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +// { +// WindowsSetTitle(ObjPtr, title); +// } +// } + +// // void gtk_file_dialog_set_filters (GtkFileDialog* self, GListModel* filters) +// public void SetFilters(Gio.Internal.ListModel filters) +// { +// if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +// { +// LinuxSetFilters(ObjPtr, filters); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) +// { +// MacOSSetFilters(ObjPtr, filters); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +// { +// WindowsSetFilters(ObjPtr, filters); +// } +// } + + + + + +// public string GetPath(nint path) +// { +// if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +// { +// return LinuxGetPath(path); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) +// { +// MacOSGetPath(FilePath); +// } +// else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +// { +// WindowsGetPath(FilePath); +// } +// return FilePath.ToString(); +// } +//} \ No newline at end of file diff --git a/VG Music Studio - GTK4/MIDIConverterDialog.cs b/VG Music Studio - GTK4/MIDIConverterDialog.cs new file mode 100644 index 00000000..3ec51949 --- /dev/null +++ b/VG Music Studio - GTK4/MIDIConverterDialog.cs @@ -0,0 +1,185 @@ +using System; +using System.IO; +using Kermalis.MIDI; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.GTK4.Util; +using PlatinumLucario.MIDI.GBA.MP2K; + +namespace Kermalis.VGMusicStudio.GTK4; + +internal class MIDIConverterDialog : Adw.Dialog +{ + private readonly Gtk.Label _midiFilePathLabel = Gtk.Label.New(""); + private readonly Gtk.Button _buttonSaveASM = Gtk.Button.NewWithLabel(Strings.TitleSaveASM); + + // MP2K param config + private readonly Gtk.Box _engineBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 5); + private readonly Gtk.Box _mp2kParamBox = Gtk.Box.New(Gtk.Orientation.Vertical, 5); + private readonly Gtk.Box _masterVolumeBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 5); + private Gtk.Label? _masterVolumeLabel; + private Gtk.SpinButton? _masterVolume; + private readonly Gtk.Box _voiceGroupBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 5); + private Gtk.Label? _voiceGroupLabel; + private Gtk.Entry? _voiceGroup; + private readonly Gtk.Box _priorityBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 5); + private Gtk.Label? _priorityLabel; + private Gtk.SpinButton? _priority; + private readonly Gtk.Box _reverbBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 5); + private Gtk.Label? _reverbLabel; + private Gtk.SpinButton? _reverb; + private readonly Gtk.Box _clocksPerBeatBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 5); + private Gtk.Label? _clocksPerBeatLabel; + private Gtk.SpinButton? _clocksPerBeat; + private readonly Gtk.Box _gateTimeBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 5); + private Gtk.Label? _gateTimeLabel; + private Gtk.CheckButton? _gateTime; + private readonly Gtk.Box _compressionBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 5); + private Gtk.Label? _compressionLabel; + private Gtk.CheckButton? _compression; + + private MP2KConverter? _converter; + + internal MIDIConverterDialog() + { + New(); + + Title = $"{MainWindow.GetProgramName()} — {Strings.MIDIConverterTitle}"; + + var mainBox = Gtk.Box.New(Gtk.Orientation.Vertical, 5); + + var header = Adw.HeaderBar.New(); + header.SetShowEndTitleButtons(true); + header.SetShowStartTitleButtons(true); + + var buttonOpenMIDI = Gtk.Button.NewWithLabel(Strings.TitleOpenMIDI); + buttonOpenMIDI.OnClicked += ButtonOpenMIDI_Clicked; + _buttonSaveASM.Sensitive = false; + + _engineBox.Append(_buttonSaveASM); + + mainBox.Append(header); + mainBox.Append(_midiFilePathLabel); + mainBox.Append(buttonOpenMIDI); + mainBox.Append(_engineBox); + + SetChild(mainBox); + } + + private void AddParamsMP2K() + { + _masterVolumeLabel = Gtk.Label.New("Master Volume"); + _masterVolume = Gtk.SpinButton.New(Gtk.Adjustment.New(127, 0, 128, 1, 1, 1), 1, 0); + _voiceGroupLabel = Gtk.Label.New("Voice Group Label"); + _voiceGroup = Gtk.Entry.New(); + _voiceGroup.Text_ = "_dummy"; + _priorityLabel = Gtk.Label.New("Priority"); + _priority = Gtk.SpinButton.New(Gtk.Adjustment.New(0, 0, 128, 1, 1, 1), 1, 0); + _reverbLabel = Gtk.Label.New("Reverb"); + _reverb = Gtk.SpinButton.New(Gtk.Adjustment.New(-1, -1, 128, 1, 1, 1), 1, 0); + _clocksPerBeatLabel = Gtk.Label.New("Clocks Per Beat"); + _clocksPerBeat = Gtk.SpinButton.New(Gtk.Adjustment.New(1, 1, 128, 1, 1, 1), 1, 0); + _gateTimeLabel = Gtk.Label.New("Use Exact Gate Time"); + _gateTime = Gtk.CheckButton.New(); + _gateTime.Active = false; + _compressionLabel = Gtk.Label.New("Use Compression"); + _compression = Gtk.CheckButton.New(); + _compression.Active = true; + + _masterVolumeBox.Append(_masterVolumeLabel); + _masterVolumeBox.Append(_masterVolume); + _voiceGroupBox.Append(_voiceGroupLabel); + _voiceGroupBox.Append(_voiceGroup); + _priorityBox.Append(_priorityLabel); + _priorityBox.Append(_priority); + _reverbBox.Append(_reverbLabel); + _reverbBox.Append(_reverb); + _clocksPerBeatBox.Append(_clocksPerBeatLabel); + _clocksPerBeatBox.Append(_clocksPerBeat); + _gateTimeBox.Append(_gateTimeLabel); + _gateTimeBox.Append(_gateTime); + _compressionBox.Append(_compressionLabel); + _compressionBox.Append(_compression); + + _mp2kParamBox.Append(_masterVolumeBox); + _mp2kParamBox.Append(_voiceGroupBox); + _mp2kParamBox.Append(_priorityBox); + _mp2kParamBox.Append(_reverbBox); + _mp2kParamBox.Append(_clocksPerBeatBox); + _mp2kParamBox.Append(_gateTimeBox); + _mp2kParamBox.Append(_compressionBox); + } + + private void ButtonOpenMIDI_Clicked(Gtk.Button sender, EventArgs args) + { + GTK4Utils.OnPathChanged += LoadFile; + GTK4Utils.CreateLoadDialog(["*.mid", "*.midi"], Strings.TitleOpenMIDI, Strings.FilterOpenMIDI); + + void LoadFile(string path) + { + GTK4Utils.OnPathChanged -= LoadFile; + if (path is null) + { + return; + } + + try + { + _buttonSaveASM.OnClicked -= ButtonSaveASM_Clicked; + _buttonSaveASM.OnClicked += ButtonSaveASM_Clicked; + _buttonSaveASM.Sensitive = true; + + _midiFilePathLabel.SetLabel(path); + AddParamsMP2K(); + _engineBox.Append(_mp2kParamBox); + } + catch (Exception ex) + { + FlexibleDialog.Show(ex, ex.Message); + } + } + } + + private void ButtonSaveASM_Clicked(Gtk.Button sender, EventArgs args) + { + GTK4Utils.CreateSaveDialog(Path.GetFileNameWithoutExtension(_midiFilePathLabel.Label_!), ["*.s"], Strings.TitleSaveASM, Strings.FilterSaveASM); + GTK4Utils.OnPathChanged += SaveFile; + + void SaveFile(string path) + { + GTK4Utils.OnPathChanged -= SaveFile; + if (path is null) + { + return; + } + + string fileName = Path.GetFileNameWithoutExtension(path); + bool success = false; + try + { + _converter = new(new MIDIFile(new FileStream(_midiFilePathLabel.Label_!, FileMode.Open)), fileName, (byte)_masterVolume!.Adjustment!.Value, _voiceGroup!.Text_!, (byte)_priority!.Adjustment!.Value, (byte)_reverb!.Adjustment!.Value, (byte)_clocksPerBeat!.Adjustment!.Value, _gateTime!.Active, _compression!.Active); + success = true; + } + catch (Exception ex) + { + // try + // { + // _converter = new(new MIDIFile(new FileStream(_midiFilePathLabel.Label_!, FileMode.Open), true), fileName, (byte)_masterVolume!.Adjustment!.Value, _voiceGroup!.Text_!, (byte)_priority!.Adjustment!.Value, (byte)_reverb!.Adjustment!.Value, (byte)_clocksPerBeat!.Adjustment!.Value, _gateTime!.Active, _compression!.Active); + // success = true; + // } + // catch (InvalidDataException idex) + // { + // FlexibleDialog.Show(idex, idex.Message); + // } + // if (success) + // { + // FlexibleDialog.Show("This MIDI file has the following non-critical issue:\n\n" + ex.Message, "This MIDI contains errors!", buttonsType: FlexibleDialog.ButtonsType.OK, icon: Gtk.MessageType.Warning); + // } + FlexibleDialog.Show("This MIDI file has the following issue:\n\n" + ex.Message, "This MIDI contains errors!", buttonsType: FlexibleDialog.ButtonsType.OK, icon: Gtk.MessageType.Warning); + } + if (success) + { + _converter!.SaveAsASM(path); + } + } + } +} diff --git a/VG Music Studio - GTK4/MainWindow.cs b/VG Music Studio - GTK4/MainWindow.cs new file mode 100644 index 00000000..b6237d4b --- /dev/null +++ b/VG Music Studio - GTK4/MainWindow.cs @@ -0,0 +1,2291 @@ +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.GTK4.Util; +using Adw; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Diagnostics; + +namespace Kermalis.VGMusicStudio.GTK4; + +internal sealed class MainWindow : Window +{ + private PlayingPlaylist? _playlist; + private int _curSong = -1; + private int _backupSong = -1; + + private bool _songEnded = false; + private bool _playlistChanged = false; + private bool _autoplay = false; + private bool _preventAutoplay = false; + + public static MainWindow? Instance { get; private set; } + + private readonly Gtk.WindowGroup _windowGroup; + + #region Widgets + + // The Windows + private Preferences? _preferences; + private MIDIConverterDialog? _midiConverterDialog; + private TrackEditor? _trackEditor; + private SoundBankEditor? _soundBankEditor; + private WidgetWindow? _playlistWindow, _seqAudioPianoWindow, _sequencedAudioTrackInfoWindow, _sequencedAudioListWindow; + + // Buttons + private readonly Gtk.Button _buttonPlay, _buttonStop, _buttonRecord; + private readonly Gtk.ToggleButton _buttonPause; + + // Spin Button for the numbered tracks + private readonly Gtk.SpinButton _sequenceNumberSpinButton; + + // Timer + private readonly GLib.Timer _timer; + + // Popover Menu Bar + private readonly Gtk.PopoverMenuBar _popoverMenuBar; + + // LibAdwaita Header Bar + private readonly HeaderBar _headerBar; + + // LibAdwaita Application + private readonly Application _app; + + // Menus + private readonly Gio.Menu _mainMenu, _fileMenu, _editMenu, _dataMenu, _playlistMenu, _widgetMenu, _playlistWidgetMenu, + _seqAudioPianoWidgetMenu, _seqAudioTrackInfoWidgetMenu, _seqAudioListWidgetMenu; + + // Menu Labels + private readonly Gtk.Label _fileLabel, _editLabel, _dataLabel, _playlistLabel, _widgetLabel; + + // Menu Items + private readonly Gio.MenuItem + _fileItem, + _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem, + _editItem, + _preferencesItem, + _dataItem, + _midiConverterDialogItem, _trackEditorItem, _soundBankEditorItem, _exportASMItem, _exportMIDIItem, _exportDLSItem, _exportSF2Item, _exportWAVItem, + _playlistItem, + _playPlaylistItem, _endPlaylistItem, + _widgetItem, + _playlistWidgetTiledItem, _playlistWidgetWindowedItem, _playlistWidgetHideItem, + _seqAudioPianoWidgetTiledItem, _seqAudioPianoWidgetWindowedItem, _seqAudioPianoWidgetHideItem, + _seqAudioTrackInfoWidgetTiledItem, _seqAudioTrackInfoWidgetWindowedItem, _seqAudioTrackInfoWidgetHideItem, + _seqAudioListWidgetTiledItem, _seqAudioListWidgetWindowedItem, _seqAudioListWidgetHideItem; + + // Menu Actions + private readonly Gio.SimpleAction + _openDSEAction, _openAlphaDreamAction, _openMP2KAction, _openSDATAction, + _preferencesAction, + _midiConverterDialogAction, _trackEditorAction, _soundBankEditorAction, _exportASMAction, _exportMIDIAction, _exportDLSAction, _exportSF2Action, _exportWAVAction, + _playPlaylistAction, _endPlaylistAction, + _playlistWidgetTiledAction, _playlistWidgetWindowedAction, _playlistWidgetHideAction, + _seqAudioPianoWidgetTiledAction, _seqAudioPianoWidgetWindowedAction, _seqAudioPianoWidgetHideAction, + _seqAudioTrackInfoWidgetTiledAction, _seqAudioTrackInfoWidgetWindowedAction, _seqAudioTrackInfoWidgetHideAction, + _seqAudioListWidgetTiledAction, _seqAudioListWidgetWindowedAction, _seqAudioListWidgetHideAction; + + // Boxes + private readonly Gtk.Box _mainBox, _configButtonBox, _configPlayerButtonBox, _configSpinButtonBox, _configBarBox, + _playlistBox, _pianoBox, _sequencedAudioTrackInfoBox, _sequencedAudioListBox; + + // One Scale controling volume and one Scale for the sequenced track + private readonly Gtk.Scale _volumeBar, _positionBar; + + // Mouse Click and Drag Gestures + private readonly Gtk.GestureClick _positionGestureClick, _sequenceNumberSpinButtonGestureClick; + private readonly Gtk.GestureDrag _positionGestureDrag; + + // Playlist + private readonly PlaylistSelector _playlistSelector; + + // Sequenced Audio Piano + private readonly SequencedAudio_Piano _piano; + + // Sequenced Audio List + private readonly SequencedAudio_List _sequencedAudioList; + + // Sequenced Audio Track Info + private readonly SequencedAudio_TrackInfo _sequencedAudioTrackInfo; + + #endregion + + public MainWindow(Application app) + { + // Main Window + SetDefaultSize(100, 100); // Sets the default size of the Window + Title = GetProgramName(); // Sets the title to the name of the program, which is "VG Music Studio" + _app = app; + _windowGroup = Gtk.WindowGroup.New(); + + // LibAdwaita Header Bar + _headerBar = Adw.HeaderBar.New(); + _headerBar.SetShowEndTitleButtons(true); + + // Main Menu + _mainMenu = Gio.Menu.New(); + + // Popover Menu Bar + _popoverMenuBar = Gtk.PopoverMenuBar.NewFromModel(_mainMenu); // This will ensure that the menu model is used inside of the PopoverMenuBar widget + _popoverMenuBar.MenuModel = _mainMenu; + _popoverMenuBar.MnemonicActivate(true); + + // File Menu + _fileMenu = Gio.Menu.New(); + + _fileLabel = Gtk.Label.NewWithMnemonic(Strings.MenuFile); + _fileLabel.GetMnemonicKeyval(); + _fileLabel.SetUseUnderline(true); + _fileItem = Gio.MenuItem.New(_fileLabel.GetLabel(), null); + _fileLabel.SetMnemonicWidget(_popoverMenuBar); + _popoverMenuBar.AddMnemonicLabel(_fileLabel); + _fileItem.SetSubmenu(_fileMenu); + + _openDSEItem = Gio.MenuItem.New(Strings.MenuOpenDSE, "app.openDSE"); + _openDSEAction = Gio.SimpleAction.New("openDSE", null); + _openDSEItem.SetActionAndTargetValue("app.openDSE", null); + _app.AddAction(_openDSEAction); + _openDSEAction.OnActivate += OpenDSE; + _fileMenu.AppendItem(_openDSEItem); + _openDSEItem.Unref(); + + _openSDATItem = Gio.MenuItem.New(Strings.MenuOpenSDAT, "app.openSDAT"); + _openSDATAction = Gio.SimpleAction.New("openSDAT", null); + _openSDATItem.SetActionAndTargetValue("app.openSDAT", null); + _app.AddAction(_openSDATAction); + _openSDATAction.OnActivate += OpenSDAT; + _fileMenu.AppendItem(_openSDATItem); + _openSDATItem.Unref(); + + _openAlphaDreamItem = Gio.MenuItem.New(Strings.MenuOpenAlphaDream, "app.openAlphaDream"); + _openAlphaDreamAction = Gio.SimpleAction.New("openAlphaDream", null); + _app.AddAction(_openAlphaDreamAction); + _openAlphaDreamAction.OnActivate += OpenAlphaDream; + _fileMenu.AppendItem(_openAlphaDreamItem); + _openAlphaDreamItem.Unref(); + + _openMP2KItem = Gio.MenuItem.New(Strings.MenuOpenMP2K, "app.openMP2K"); + _openMP2KAction = Gio.SimpleAction.New("openMP2K", null); + _app.AddAction(_openMP2KAction); + _openMP2KAction.OnActivate += OpenMP2K; + _fileMenu.AppendItem(_openMP2KItem); + _openMP2KItem.Unref(); + + _mainMenu.AppendItem(_fileItem); // Note: It must append the menu item variable (_fileItem), not the file menu variable (_fileMenu) itself + _fileItem.Unref(); + + // Edit Menu + _editMenu = Gio.Menu.New(); + + _editLabel = Gtk.Label.NewWithMnemonic(Strings.MenuEdit); + _editLabel.GetMnemonicKeyval(); + _editLabel.SetUseUnderline(true); + _editItem = Gio.MenuItem.New(_editLabel.GetLabel(), null); + _popoverMenuBar.AddMnemonicLabel(_editLabel); + _editItem.SetSubmenu(_editMenu); + + _preferencesItem = Gio.MenuItem.New(Strings.MenuPreferences, "app.preferences"); + _preferencesAction = Gio.SimpleAction.New("preferences", null); + _app.AddAction(_preferencesAction); + _preferencesAction.OnActivate += OpenPreferences; + _editMenu.AppendItem(_preferencesItem); + _preferencesItem.Unref(); + + _mainMenu.AppendItem(_editItem); + _editItem.Unref(); + + // Data Menu + _dataMenu = Gio.Menu.New(); + + _dataLabel = Gtk.Label.NewWithMnemonic(Strings.MenuData); + _dataLabel.GetMnemonicKeyval(); + _dataLabel.SetUseUnderline(true); + _dataItem = Gio.MenuItem.New(_dataLabel.GetLabel(), null); + _popoverMenuBar.AddMnemonicLabel(_dataLabel); + _dataItem.SetSubmenu(_dataMenu); + + _midiConverterDialogItem = Gio.MenuItem.New(Strings.MIDIConverterTitle, "app.midiConverterDialog"); + _midiConverterDialogAction = Gio.SimpleAction.New("midiConverterDialog", null); + _app.AddAction(_midiConverterDialogAction); + _midiConverterDialogAction.Enabled = true; + _midiConverterDialogAction.OnActivate += OpenMIDIConverterDialog; + _dataMenu.AppendItem(_midiConverterDialogItem); + _midiConverterDialogItem.Unref(); + + _trackEditorItem = Gio.MenuItem.New(Strings.TrackEditorTitle, "app.trackEditor"); + _trackEditorAction = Gio.SimpleAction.New("trackEditor", null); + _app.AddAction(_trackEditorAction); + _trackEditorAction.Enabled = false; + _trackEditorAction.OnActivate += OpenTrackEditor; + _dataMenu.AppendItem(_trackEditorItem); + _trackEditorItem.Unref(); + + _soundBankEditorItem = Gio.MenuItem.New(Strings.MenuVoiceGroupSoundBankEditor, "app.soundBankEditor"); + _soundBankEditorAction = Gio.SimpleAction.New("soundBankEditor", null); + _app.AddAction(_soundBankEditorAction); + _soundBankEditorAction.Enabled = false; + _soundBankEditorAction.OnActivate += OpenSoundBankEditor; + _dataMenu.AppendItem(_soundBankEditorItem); + _soundBankEditorItem.Unref(); + + _exportASMItem = Gio.MenuItem.New(Strings.MenuSaveASM, "app.exportASM"); + _exportASMAction = Gio.SimpleAction.New("exportASM", null); + _app.AddAction(_exportASMAction); + _exportASMAction.Enabled = false; + _exportASMAction.OnActivate += ExportASM; + _dataMenu.AppendItem(_exportASMItem); + _exportASMItem.Unref(); + + _exportMIDIItem = Gio.MenuItem.New(Strings.MenuSaveMIDI, "app.exportMIDI"); + _exportMIDIAction = Gio.SimpleAction.New("exportMIDI", null); + _app.AddAction(_exportMIDIAction); + _exportMIDIAction.Enabled = false; + _exportMIDIAction.OnActivate += ExportMIDI; + _dataMenu.AppendItem(_exportMIDIItem); + _exportMIDIItem.Unref(); + + _exportDLSItem = Gio.MenuItem.New(Strings.MenuSaveDLS, "app.exportDLS"); + _exportDLSAction = Gio.SimpleAction.New("exportDLS", null); + _app.AddAction(_exportDLSAction); + _exportDLSAction.Enabled = false; + _exportDLSAction.OnActivate += ExportDLS; + _dataMenu.AppendItem(_exportDLSItem); + _exportDLSItem.Unref(); + + _exportSF2Item = Gio.MenuItem.New(Strings.MenuSaveSF2, "app.exportSF2"); + _exportSF2Action = Gio.SimpleAction.New("exportSF2", null); + _app.AddAction(_exportSF2Action); + _exportSF2Action.Enabled = false; + _exportSF2Action.OnActivate += ExportSF2; + _dataMenu.AppendItem(_exportSF2Item); + _exportSF2Item.Unref(); + + _exportWAVItem = Gio.MenuItem.New(Strings.MenuSaveWAV, "app.exportWAV"); + _exportWAVAction = Gio.SimpleAction.New("exportWAV", null); + _app.AddAction(_exportWAVAction); + _exportWAVAction.Enabled = false; + _exportWAVAction.OnActivate += ExportWAV; + _dataMenu.AppendItem(_exportWAVItem); + _exportWAVItem.Unref(); + + _mainMenu.AppendItem(_dataItem); + _dataItem.Unref(); + + // Playlist Menu + _playlistMenu = Gio.Menu.New(); + + _playlistLabel = Gtk.Label.NewWithMnemonic(Strings.MenuPlaylist); + _playlistLabel.GetMnemonicKeyval(); + _playlistLabel.SetUseUnderline(true); + _playlistItem = Gio.MenuItem.New(_playlistLabel.GetLabel(), null); + _popoverMenuBar.AddMnemonicLabel(_playlistLabel); + _playlistItem.SetSubmenu(_playlistMenu); + + _playPlaylistItem = Gio.MenuItem.New(Strings.MenuPlayPlaylist, "app.playPlaylist"); + _playPlaylistAction = Gio.SimpleAction.New("playPlaylist", null); + _app.AddAction(_playPlaylistAction); + _playPlaylistAction.Enabled = false; + _playPlaylistAction.OnActivate += PlayCurrentPlaylist; + _playlistMenu.AppendItem(_playPlaylistItem); + _playPlaylistItem.Unref(); + + _endPlaylistItem = Gio.MenuItem.New(Strings.MenuEndPlaylist, "app.endPlaylist"); + _endPlaylistAction = Gio.SimpleAction.New("endPlaylist", null); + _app.AddAction(_endPlaylistAction); + _endPlaylistAction.Enabled = false; + _endPlaylistAction.OnActivate += EndCurrentPlaylist; + _playlistMenu.AppendItem(_endPlaylistItem); + _endPlaylistItem.Unref(); + + _mainMenu.AppendItem(_playlistItem); + _playlistItem.Unref(); + + // Widget Menu + _widgetMenu = Gio.Menu.New(); + + _widgetLabel = Gtk.Label.NewWithMnemonic("Widgets"); + _widgetLabel.GetMnemonicKeyval(); + _widgetLabel.SetUseUnderline(true); + _widgetItem = Gio.MenuItem.New(_widgetLabel.GetLabel(), null); + _popoverMenuBar.AddMnemonicLabel(_widgetLabel); + _widgetItem.SetSubmenu(_widgetMenu); + + _playlistWidgetMenu = Gio.Menu.New(); + + _playlistWidgetTiledItem = Gio.MenuItem.New("Tiled", "app.playlistWidgetTiled"); + _playlistWidgetTiledAction = Gio.SimpleAction.NewStateful("playlistWidgetTiled", null, GLib.Variant.NewBoolean(false)); + _app.AddAction(_playlistWidgetTiledAction); + _playlistWidgetTiledAction.Enabled = false; + _playlistWidgetTiledAction.OnActivate += Playlist_GetTiled; + _playlistWidgetMenu.AppendItem(_playlistWidgetTiledItem); + _playlistWidgetTiledItem.Unref(); + + _playlistWidgetWindowedItem = Gio.MenuItem.New("Windowed", "app.playlistWidgetWindowed"); + _playlistWidgetWindowedAction = Gio.SimpleAction.NewStateful("playlistWidgetWindowed", null, GLib.Variant.NewBoolean(false)); + _app.AddAction(_playlistWidgetWindowedAction); + _playlistWidgetWindowedAction.Enabled = false; + _playlistWidgetWindowedAction.OnActivate += Playlist_GetWindowed; + _playlistWidgetMenu.AppendItem(_playlistWidgetWindowedItem); + _playlistWidgetWindowedItem.Unref(); + + _playlistWidgetHideItem = Gio.MenuItem.New("Hide", "app.playlistWidgetHide"); + _playlistWidgetHideAction = Gio.SimpleAction.NewStateful("playlistWidgetHide", null, GLib.Variant.NewBoolean(true)); + _app.AddAction(_playlistWidgetHideAction); + _playlistWidgetHideAction.Enabled = false; + _playlistWidgetHideAction.OnActivate += Playlist_Hide; + _playlistWidgetMenu.AppendItem(_playlistWidgetHideItem); + _playlistWidgetHideItem.Unref(); + + _widgetMenu.AppendSubmenu("Playlist", _playlistWidgetMenu); + + _seqAudioPianoWidgetMenu = Gio.Menu.New(); + + _seqAudioPianoWidgetTiledItem = Gio.MenuItem.New("Tiled", "app.seqAudioPianoWidgetTiled"); + _seqAudioPianoWidgetTiledAction = Gio.SimpleAction.NewStateful("seqAudioPianoWidgetTiled", null, GLib.Variant.NewBoolean(false)); + _app.AddAction(_seqAudioPianoWidgetTiledAction); + _seqAudioPianoWidgetTiledAction.Enabled = false; + _seqAudioPianoWidgetTiledAction.OnActivate += SeqAudioPiano_GetTiled; + _seqAudioPianoWidgetMenu.AppendItem(_seqAudioPianoWidgetTiledItem); + _seqAudioPianoWidgetTiledItem.Unref(); + + _seqAudioPianoWidgetWindowedItem = Gio.MenuItem.New("Windowed", "app.seqAudioPianoWidgetWindowed"); + _seqAudioPianoWidgetWindowedAction = Gio.SimpleAction.NewStateful("seqAudioPianoWidgetWindowed", null, GLib.Variant.NewBoolean(false)); + _app.AddAction(_seqAudioPianoWidgetWindowedAction); + _seqAudioPianoWidgetWindowedAction.Enabled = false; + _seqAudioPianoWidgetWindowedAction.OnActivate += SeqAudioPiano_GetWindowed; + _seqAudioPianoWidgetMenu.AppendItem(_seqAudioPianoWidgetWindowedItem); + _seqAudioPianoWidgetWindowedItem.Unref(); + + _seqAudioPianoWidgetHideItem = Gio.MenuItem.New("Hide", "app.seqAudioPianoWidgetHide"); + _seqAudioPianoWidgetHideAction = Gio.SimpleAction.NewStateful("seqAudioPianoWidgetHide", null, GLib.Variant.NewBoolean(true)); + _app.AddAction(_seqAudioPianoWidgetHideAction); + _seqAudioPianoWidgetHideAction.Enabled = false; + _seqAudioPianoWidgetHideAction.OnActivate += SeqAudioPiano_Hide; + _seqAudioPianoWidgetMenu.AppendItem(_seqAudioPianoWidgetHideItem); + _seqAudioPianoWidgetHideItem.Unref(); + + _widgetMenu.AppendSubmenu("Piano", _seqAudioPianoWidgetMenu); + + _seqAudioTrackInfoWidgetMenu = Gio.Menu.New(); + + _seqAudioTrackInfoWidgetTiledItem = Gio.MenuItem.New("Tiled", "app.seqAudioTrackInfoWidgetTiled"); + _seqAudioTrackInfoWidgetTiledAction = Gio.SimpleAction.NewStateful("seqAudioTrackInfoWidgetTiled", null, GLib.Variant.NewBoolean(false)); + _app.AddAction(_seqAudioTrackInfoWidgetTiledAction); + _seqAudioTrackInfoWidgetTiledAction.Enabled = false; + _seqAudioTrackInfoWidgetTiledAction.OnActivate += SeqAudioTrackInfo_GetTiled; + _seqAudioTrackInfoWidgetMenu.AppendItem(_seqAudioTrackInfoWidgetTiledItem); + _seqAudioTrackInfoWidgetTiledItem.Unref(); + + _seqAudioTrackInfoWidgetWindowedItem = Gio.MenuItem.New("Windowed", "app.seqAudioTrackInfoWidgetWindowed"); + _seqAudioTrackInfoWidgetWindowedAction = Gio.SimpleAction.NewStateful("seqAudioTrackInfoWidgetWindowed", null, GLib.Variant.NewBoolean(false)); + _app.AddAction(_seqAudioTrackInfoWidgetWindowedAction); + _seqAudioTrackInfoWidgetWindowedAction.Enabled = false; + _seqAudioTrackInfoWidgetWindowedAction.OnActivate += SeqAudioTrackInfo_GetWindowed; + _seqAudioTrackInfoWidgetMenu.AppendItem(_seqAudioTrackInfoWidgetWindowedItem); + _seqAudioTrackInfoWidgetWindowedItem.Unref(); + + _seqAudioTrackInfoWidgetHideItem = Gio.MenuItem.New("Hide", "app.seqAudioTrackInfoWidgetHide"); + _seqAudioTrackInfoWidgetHideAction = Gio.SimpleAction.NewStateful("seqAudioTrackInfoWidgetHide", null, GLib.Variant.NewBoolean(true)); + _app.AddAction(_seqAudioTrackInfoWidgetHideAction); + _seqAudioTrackInfoWidgetHideAction.Enabled = false; + _seqAudioTrackInfoWidgetHideAction.OnActivate += SeqAudioTrackInfo_Hide; + _seqAudioTrackInfoWidgetMenu.AppendItem(_seqAudioTrackInfoWidgetHideItem); + _seqAudioTrackInfoWidgetHideItem.Unref(); + + _widgetMenu.AppendSubmenu("Sequenced Audio Track Info", _seqAudioTrackInfoWidgetMenu); + + _seqAudioListWidgetMenu = Gio.Menu.New(); + + _seqAudioListWidgetTiledItem = Gio.MenuItem.New("Tiled", "app.seqAudioListWidgetTiled"); + _seqAudioListWidgetTiledAction = Gio.SimpleAction.NewStateful("seqAudioListWidgetTiled", null, GLib.Variant.NewBoolean(true)); + _app.AddAction(_seqAudioListWidgetTiledAction); + _seqAudioListWidgetTiledAction.OnActivate += SeqAudioList_GetTiled; + _seqAudioListWidgetMenu.AppendItem(_seqAudioListWidgetTiledItem); + _seqAudioListWidgetTiledItem.Unref(); + + _seqAudioListWidgetWindowedItem = Gio.MenuItem.New("Windowed", "app.seqAudioListWidgetWindowed"); + _seqAudioListWidgetWindowedAction = Gio.SimpleAction.NewStateful("seqAudioListWidgetWindowed", null, GLib.Variant.NewBoolean(false)); + _app.AddAction(_seqAudioListWidgetWindowedAction); + _seqAudioListWidgetWindowedAction.OnActivate += SeqAudioList_GetWindowed; + _seqAudioListWidgetMenu.AppendItem(_seqAudioListWidgetWindowedItem); + _seqAudioListWidgetWindowedItem.Unref(); + + _seqAudioListWidgetHideItem = Gio.MenuItem.New("Hide", "app.seqAudioListWidgetHide"); + _seqAudioListWidgetHideAction = Gio.SimpleAction.NewStateful("seqAudioListWidgetHide", null, GLib.Variant.NewBoolean(false)); + _app.AddAction(_seqAudioListWidgetHideAction); + _seqAudioListWidgetHideAction.OnActivate += SeqAudioList_Hide; + _seqAudioListWidgetMenu.AppendItem(_seqAudioListWidgetHideItem); + _seqAudioListWidgetHideItem.Unref(); + + _widgetMenu.AppendSubmenu("Sequenced Audio List", _seqAudioListWidgetMenu); + + _mainMenu.AppendItem(_widgetItem); + _widgetItem.Unref(); + + // Buttons + _buttonPlay = new Gtk.Button() { Sensitive = false, TooltipText = Strings.PlayerPlay, IconName = "media-playback-start-symbolic" }; + _buttonPlay.OnClicked += ButtonPlay_Clicked; + _buttonPause = new Gtk.ToggleButton() { Sensitive = false, TooltipText = Strings.PlayerPause, IconName = "media-playback-pause-symbolic" }; + _buttonPause.OnClicked += ButtonPause_Clicked; + _buttonStop = new Gtk.Button() { Sensitive = false, TooltipText = Strings.PlayerStop, IconName = "media-playback-stop-symbolic" }; + _buttonStop.OnClicked += ButtonStop_Clicked; + + _buttonRecord = new Gtk.Button() { Sensitive = false, TooltipText = Strings.PlayerRecord, IconName = "media-record-symbolic" }; + _buttonRecord.OnClicked += ExportWAV; + + // Spin Button + _sequenceNumberSpinButton = Gtk.SpinButton.New(Gtk.Adjustment.New(0, 0, 10000, 1, 10, 0), 0, 0); + _sequenceNumberSpinButtonGestureClick = Gtk.GestureClick.New(); + _sequenceNumberSpinButton.AddController(_sequenceNumberSpinButtonGestureClick); + _sequenceNumberSpinButton.Sensitive = false; + _sequenceNumberSpinButton.SetNumeric(true); + _sequenceNumberSpinButton.Value = 0; + _sequenceNumberSpinButton.OnValueChanged += SequenceNumberSpinButton_ValueChanged; + _sequenceNumberSpinButton.OnChangeValue += SequenceNumberSpinButton_ChangeValue; + + // // Timer + _timer = GLib.Timer.New(); + + // Volume Bar + _volumeBar = Gtk.Scale.New(Gtk.Orientation.Horizontal, Gtk.Adjustment.New(0, 0, 100, 1, 10, 0)); + _volumeBar.OnValueChanged += VolumeBar_ValueChanged; + _volumeBar.Sensitive = false; + _volumeBar.ShowFillLevel = true; + _volumeBar.DrawValue = false; + _volumeBar.WidthRequest = 250; + + // Position Bar + _positionBar = Gtk.Scale.New(Gtk.Orientation.Horizontal, Gtk.Adjustment.New(0, 0, 100, 1, 10, 0)); // The Upper value property must contain a value of 1 or higher for the widget to show upon startup + _positionGestureClick = Gtk.GestureClick.New(); + _positionGestureDrag = Gtk.GestureDrag.New(); + _positionBar.AddController(_positionGestureClick); + _positionBar.AddController(_positionGestureDrag); + _positionBar.Sensitive = false; + _positionBar.Focusable = true; + _positionBar.ShowFillLevel = true; + _positionBar.DrawValue = false; + _positionBar.WidthRequest = 250; + _positionBar.RestrictToFillLevel = false; + _positionBar.OnChangeValue += PositionBar_ChangeValue; + _positionBar.OnMoveSlider += PositionBar_MoveSlider; + _positionBar.OnValueChanged += PositionBar_ValueChanged; + _positionGestureClick.OnStopped += PositionBar_MouseButtonRelease; + _positionGestureClick.OnCancel += PositionBar_MouseButtonRelease; + _positionGestureClick.OnPressed += PositionBar_MouseButtonPress; + _positionGestureClick.OnReleased += PositionBar_MouseButtonRelease; + _positionGestureClick.OnUnpairedRelease += PositionBar_MouseButtonRelease; + _positionGestureClick.OnBegin += PositionBar_MouseButtonOnBegin; + _positionGestureClick.OnEnd += PositionBar_MouseButtonOnEnd; + // _positionGestureDrag.OnDragBegin += PositionBar_MouseButtonOnBegin; + // _positionGestureDrag.OnDragEnd += PositionBar_MouseButtonOnEnd; + + // Playlist + _playlistSelector = new PlaylistSelector + { + PlaylistClick = Gtk.GestureClick.New() + }; + _playlistSelector.PlaylistClick.SetButton(1); + // _playlistSelector.PlaylistClick.OnPressed += PlaylistClick_LeftClick; + _playlistSelector.PlaylistSongClick = Gtk.GestureClick.New(); + _playlistSelector.PlaylistSongClick.SetButton(1); + // _playlistSelector.PlaylistSongClick.OnPressed += PlaylistSongClick_LeftClick; + _playlistSelector.PlaylistDropDown!.AddController(_playlistSelector.PlaylistClick); + _playlistSelector.PlaylistSongDropDown!.AddController(_playlistSelector.PlaylistSongClick); + _playlistSelector.ButtonPlayPlist!.OnClicked += ButtonPlayPlist_Clicked; + _playlistSelector.ButtonPlistStyle!.OnClicked += ButtonPlistStyle_Clicked; + _playlistSelector.ButtonPrevPlistSong!.OnClicked += PlayPreviousSong; + _playlistSelector.ButtonNextPlistSong!.OnClicked += PlayNextSong; + _playlistBox = Gtk.Box.New(Gtk.Orientation.Vertical, 0); + _playlistBox.SetVexpand(true); + + // Sequenced Audio Piano + _piano = new(); + _pianoBox = Gtk.Box.New(Gtk.Orientation.Vertical, 0); + _pianoBox.SetVexpand(false); + + // Sequenced Audio Track Info + _sequencedAudioTrackInfo = new(); + _sequencedAudioTrackInfoBox = Gtk.Box.New(Gtk.Orientation.Vertical, 0); + _sequencedAudioTrackInfoBox.SetVexpand(true); + + // Sequenced Audio List + _sequencedAudioList = new(); + _sequencedAudioList.Init(); + _sequencedAudioListBox = Gtk.Box.New(Gtk.Orientation.Vertical, 0); + _sequencedAudioListBox.SetVexpand(true); + _sequencedAudioListBox.Append(_sequencedAudioList); + + // Main display + _mainBox = Gtk.Box.New(Gtk.Orientation.Vertical, 4); + + _configButtonBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 2); + _configButtonBox.Halign = Gtk.Align.Center; + _configPlayerButtonBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 3); + _configPlayerButtonBox.Halign = Gtk.Align.Center; + _configSpinButtonBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 1); + _configSpinButtonBox.Halign = Gtk.Align.Center; + _configSpinButtonBox.WidthRequest = 100; + _configBarBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 2); + _configBarBox.Halign = Gtk.Align.Center; + + _configPlayerButtonBox.MarginStart = 40; + _configPlayerButtonBox.MarginEnd = 40; + _configButtonBox.Append(_configPlayerButtonBox); + _configSpinButtonBox.MarginStart = 100; + _configSpinButtonBox.MarginEnd = 100; + _configButtonBox.Append(_configSpinButtonBox); + + _configPlayerButtonBox.Append(_buttonPlay); + _configPlayerButtonBox.Append(_buttonPause); + _configPlayerButtonBox.Append(_buttonStop); + _configPlayerButtonBox.Append(_buttonRecord); + + if (_configSpinButtonBox.GetFirstChild() == null) + { + _sequenceNumberSpinButton.Hide(); + _configSpinButtonBox.Append(_sequenceNumberSpinButton); + } + + _volumeBar.MarginStart = 20; + _volumeBar.MarginEnd = 20; + _configBarBox.Append(_volumeBar); + _positionBar.MarginStart = 20; + _positionBar.MarginEnd = 20; + _configBarBox.Append(_positionBar); + + _mainBox.Append(_headerBar); + _mainBox.Append(_popoverMenuBar); + _mainBox.Append(_configButtonBox); + _mainBox.Append(_configBarBox); + _mainBox.Append(_playlistBox); + _mainBox.Append(_pianoBox); + _mainBox.Append(_sequencedAudioTrackInfoBox); + _mainBox.Append(_sequencedAudioListBox); + + SetContent(_mainBox); + + Instance = this; + + _windowGroup.AddWindow(this); + + // Ensures the entire application gets closed when the main window is closed + OnCloseRequest += MainWindow_CloseRequest; + } + + // Closes the entire application + private bool MainWindow_CloseRequest(Gtk.Window sender, EventArgs args) + { + DisposeEngine(); // Engine must be disposed first, otherwise the window will softlock when closing + Dispose(); + return false; + } + + #region Widget Display Toggle Methods + private void Playlist_GetTiled(object sender, EventArgs args) + { + if (_playlistWidgetTiledAction.GetState()!.GetBoolean() == true) + { + return; + } + + if (_playlistWindow is not null) + { + if (_playlistWindow.WidgetBox.GetFirstChild() is not null) + { + _playlistWindow.WidgetBox.Remove(_playlistSelector); + } + _playlistWindow.OnCloseRequest -= PlaylistWindow_CloseRequest; + _playlistWindow.Dispose(); + _playlistWindow.Close(); + if (_playlistWindow is not null) + { + _playlistWindow = null!; + } + } + + _playlistBox.Append(_playlistSelector); + + _playlistWidgetTiledAction.SetState(GLib.Variant.NewBoolean(true)); + _playlistWidgetWindowedAction.SetState(GLib.Variant.NewBoolean(false)); + _playlistWidgetHideAction.SetState(GLib.Variant.NewBoolean(false)); + } + + private void Playlist_GetWindowed(object sender, EventArgs args) + { + if (_playlistWidgetWindowedAction.GetState()!.GetBoolean() == true) + { + return; + } + + if (_playlistBox.GetFirstChild() is not null) + { + _playlistBox.Remove(_playlistSelector); + } + _playlistWindow ??= new WidgetWindow(_playlistSelector); + _playlistWindow.OnCloseRequest += PlaylistWindow_CloseRequest; + _playlistWindow.Present(); + + _playlistWidgetTiledAction.SetState(GLib.Variant.NewBoolean(false)); + _playlistWidgetWindowedAction.SetState(GLib.Variant.NewBoolean(true)); + _playlistWidgetHideAction.SetState(GLib.Variant.NewBoolean(false)); + } + + private void Playlist_Hide(object sender, EventArgs args) + { + if (_playlistWidgetHideAction.GetState()!.GetBoolean() == true) + { + return; + } + + if (_playlistWindow is not null) + { + if (_playlistWindow.WidgetBox.GetFirstChild() is not null) + { + _playlistWindow.WidgetBox.Remove(_playlistSelector); + } + _playlistWindow.OnCloseRequest -= PlaylistWindow_CloseRequest; + _playlistWindow.Dispose(); + _playlistWindow.Close(); + if (_playlistWindow is not null) + { + _playlistWindow = null!; + } + } + if (_playlistBox.GetFirstChild() is not null) + { + _playlistBox.Remove(_playlistSelector); + } + + _playlistWidgetTiledAction.SetState(GLib.Variant.NewBoolean(false)); + _playlistWidgetWindowedAction.SetState(GLib.Variant.NewBoolean(false)); + _playlistWidgetHideAction.SetState(GLib.Variant.NewBoolean(true)); + } + + private void SeqAudioPiano_GetTiled(object sender, EventArgs args) + { + if (_seqAudioPianoWidgetTiledAction.GetState()!.GetBoolean() == true) + { + return; + } + + if (_seqAudioPianoWindow is not null) + { + if (_seqAudioPianoWindow.WidgetBox.GetFirstChild() is not null) + { + _seqAudioPianoWindow.WidgetBox.Remove(_piano); + } + _seqAudioPianoWindow.OnCloseRequest -= SeqAudioPianoWindow_CloseRequest; + _seqAudioPianoWindow.Dispose(); + _seqAudioPianoWindow.Close(); + if (_seqAudioPianoWindow is not null) + { + _seqAudioPianoWindow = null!; + } + } + + _pianoBox.Append(_piano); + + _seqAudioPianoWidgetTiledAction.SetState(GLib.Variant.NewBoolean(true)); + _seqAudioPianoWidgetWindowedAction.SetState(GLib.Variant.NewBoolean(false)); + _seqAudioPianoWidgetHideAction.SetState(GLib.Variant.NewBoolean(false)); + } + + private void SeqAudioPiano_GetWindowed(object sender, EventArgs args) + { + if (_seqAudioPianoWidgetWindowedAction.GetState()!.GetBoolean() == true) + { + return; + } + + if (_pianoBox.GetFirstChild() is not null) + { + _pianoBox.Remove(_piano); + } + _seqAudioPianoWindow ??= new WidgetWindow(_piano); + _seqAudioPianoWindow.HeightRequest = 60; + _seqAudioPianoWindow.OnCloseRequest += SeqAudioPianoWindow_CloseRequest; + _seqAudioPianoWindow.Present(); + + _seqAudioPianoWidgetTiledAction.SetState(GLib.Variant.NewBoolean(false)); + _seqAudioPianoWidgetWindowedAction.SetState(GLib.Variant.NewBoolean(true)); + _seqAudioPianoWidgetHideAction.SetState(GLib.Variant.NewBoolean(false)); + } + + private void SeqAudioPiano_Hide(object sender, EventArgs args) + { + if (_seqAudioPianoWidgetHideAction.GetState()!.GetBoolean() == true) + { + return; + } + + if (_seqAudioPianoWindow is not null) + { + if (_seqAudioPianoWindow.WidgetBox.GetFirstChild() is not null) + { + _seqAudioPianoWindow.WidgetBox.Remove(_piano); + } + _seqAudioPianoWindow.OnCloseRequest -= SeqAudioPianoWindow_CloseRequest; + _seqAudioPianoWindow.Dispose(); + _seqAudioPianoWindow.Close(); + if (_seqAudioPianoWindow is not null) + { + _seqAudioPianoWindow = null!; + } + } + if (_pianoBox.GetFirstChild() is not null) + { + _pianoBox.Remove(_piano); + } + + _seqAudioPianoWidgetTiledAction.SetState(GLib.Variant.NewBoolean(false)); + _seqAudioPianoWidgetWindowedAction.SetState(GLib.Variant.NewBoolean(false)); + _seqAudioPianoWidgetHideAction.SetState(GLib.Variant.NewBoolean(true)); + } + + private void SeqAudioTrackInfo_GetTiled(object sender, EventArgs args) + { + if (_seqAudioTrackInfoWidgetTiledAction.GetState()!.GetBoolean() == true) + { + return; + } + + if (_sequencedAudioTrackInfoWindow is not null) + { + if (_sequencedAudioTrackInfoWindow.WidgetBox.GetFirstChild() is not null) + { + _sequencedAudioTrackInfoWindow.WidgetBox.Remove(_sequencedAudioTrackInfo); + } + _sequencedAudioTrackInfoWindow.OnCloseRequest -= SeqAudioTrackInfoWindow_CloseRequest; + _sequencedAudioTrackInfoWindow.Dispose(); + _sequencedAudioTrackInfoWindow.Close(); + if (_sequencedAudioTrackInfoWindow is not null) + { + _sequencedAudioTrackInfoWindow = null!; + } + } + + _sequencedAudioTrackInfoBox.Append(_sequencedAudioTrackInfo); + + _seqAudioTrackInfoWidgetTiledAction.SetState(GLib.Variant.NewBoolean(true)); + _seqAudioTrackInfoWidgetWindowedAction.SetState(GLib.Variant.NewBoolean(false)); + _seqAudioTrackInfoWidgetHideAction.SetState(GLib.Variant.NewBoolean(false)); + } + + private void SeqAudioTrackInfo_GetWindowed(object sender, EventArgs args) + { + if (_seqAudioTrackInfoWidgetWindowedAction.GetState()!.GetBoolean() == true) + { + return; + } + + if (_sequencedAudioTrackInfoBox.GetFirstChild() is not null) + { + _sequencedAudioTrackInfoBox.Remove(_sequencedAudioTrackInfo); + } + _sequencedAudioTrackInfoWindow ??= new WidgetWindow(_sequencedAudioTrackInfo); + _sequencedAudioTrackInfoWindow.OnCloseRequest += SeqAudioTrackInfoWindow_CloseRequest; + _sequencedAudioTrackInfoWindow.Present(); + + _seqAudioTrackInfoWidgetTiledAction.SetState(GLib.Variant.NewBoolean(false)); + _seqAudioTrackInfoWidgetWindowedAction.SetState(GLib.Variant.NewBoolean(true)); + _seqAudioTrackInfoWidgetHideAction.SetState(GLib.Variant.NewBoolean(false)); + } + + private void SeqAudioTrackInfo_Hide(object sender, EventArgs args) + { + if (_seqAudioTrackInfoWidgetHideAction.GetState()!.GetBoolean() == true) + { + return; + } + + if (_sequencedAudioTrackInfoWindow is not null) + { + if (_sequencedAudioTrackInfoWindow.WidgetBox.GetFirstChild() is not null) + { + _sequencedAudioTrackInfoWindow.WidgetBox.Remove(_sequencedAudioTrackInfo); + } + _sequencedAudioTrackInfoWindow.OnCloseRequest -= SeqAudioTrackInfoWindow_CloseRequest; + _sequencedAudioTrackInfoWindow.Dispose(); + _sequencedAudioTrackInfoWindow.Close(); + if (_sequencedAudioTrackInfoWindow is not null) + { + _sequencedAudioTrackInfoWindow = null!; + } + } + if (_sequencedAudioTrackInfoBox.GetFirstChild() is not null) + { + _sequencedAudioTrackInfoBox.Remove(_sequencedAudioTrackInfo); + } + + _seqAudioTrackInfoWidgetTiledAction.SetState(GLib.Variant.NewBoolean(false)); + _seqAudioTrackInfoWidgetWindowedAction.SetState(GLib.Variant.NewBoolean(false)); + _seqAudioTrackInfoWidgetHideAction.SetState(GLib.Variant.NewBoolean(true)); + } + + private void SeqAudioList_GetTiled(object sender, EventArgs args) + { + if (_seqAudioListWidgetTiledAction.GetState()!.GetBoolean() == true) + { + return; + } + + if (_sequencedAudioListWindow is not null) + { + if (_sequencedAudioListWindow.WidgetBox.GetFirstChild() is not null) + { + _sequencedAudioListWindow.WidgetBox.Remove(_sequencedAudioList); + } + _sequencedAudioListWindow.OnCloseRequest -= SeqAudioListWindow_CloseRequest; + _sequencedAudioListWindow.Dispose(); + _sequencedAudioListWindow.Close(); + if (_sequencedAudioListWindow is not null) + { + _sequencedAudioListWindow = null!; + } + } + + _sequencedAudioListBox.Append(_sequencedAudioList); + + _seqAudioListWidgetTiledAction.SetState(GLib.Variant.NewBoolean(true)); + _seqAudioListWidgetWindowedAction.SetState(GLib.Variant.NewBoolean(false)); + _seqAudioListWidgetHideAction.SetState(GLib.Variant.NewBoolean(false)); + } + + private void SeqAudioList_GetWindowed(object sender, EventArgs args) + { + if (_seqAudioListWidgetWindowedAction.GetState()!.GetBoolean() == true) + { + return; + } + + if (_sequencedAudioListBox.GetFirstChild() is not null) + { + _sequencedAudioListBox.Remove(_sequencedAudioList); + } + _sequencedAudioListWindow ??= new WidgetWindow(_sequencedAudioList); + _sequencedAudioListWindow.OnCloseRequest += SeqAudioListWindow_CloseRequest; + _sequencedAudioListWindow.Present(); + + _seqAudioListWidgetTiledAction.SetState(GLib.Variant.NewBoolean(false)); + _seqAudioListWidgetWindowedAction.SetState(GLib.Variant.NewBoolean(true)); + _seqAudioListWidgetHideAction.SetState(GLib.Variant.NewBoolean(false)); + } + + private void SeqAudioList_Hide(object sender, EventArgs args) + { + if (_seqAudioListWidgetHideAction.GetState()!.GetBoolean() == true) + { + return; + } + + if (_sequencedAudioListWindow is not null) + { + if (_sequencedAudioListWindow.WidgetBox.GetFirstChild() is not null) + { + _sequencedAudioListWindow.WidgetBox.Remove(_sequencedAudioList); + } + _sequencedAudioListWindow.OnCloseRequest -= SeqAudioListWindow_CloseRequest; + _sequencedAudioListWindow.Dispose(); + _sequencedAudioListWindow.Close(); + if (_sequencedAudioListWindow is not null) + { + _sequencedAudioListWindow = null!; + } + } + if (_sequencedAudioListBox.GetFirstChild() is not null) + { + _sequencedAudioListBox.Remove(_sequencedAudioList); + } + + _seqAudioListWidgetTiledAction.SetState(GLib.Variant.NewBoolean(false)); + _seqAudioListWidgetWindowedAction.SetState(GLib.Variant.NewBoolean(false)); + _seqAudioListWidgetHideAction.SetState(GLib.Variant.NewBoolean(true)); + } + + #region Widget Window Closure Methods + + private bool PlaylistWindow_CloseRequest(Gtk.Window sender, EventArgs args) + { + Playlist_GetTiled(sender, args); + return false; + } + + private bool SeqAudioPianoWindow_CloseRequest(Gtk.Window sender, EventArgs args) + { + SeqAudioPiano_GetTiled(sender, args); + return false; + } + + private bool SeqAudioTrackInfoWindow_CloseRequest(Gtk.Window sender, EventArgs args) + { + SeqAudioTrackInfo_GetTiled(sender, args); + return false; + } + + private bool SeqAudioListWindow_CloseRequest(Gtk.Window sender, EventArgs args) + { + SeqAudioList_GetTiled(sender, args); + return false; + } + #endregion + + #region Widget Menu Enable and Disable Methods + private void PlaylistWidgetAction_IsEnabled(bool enabled) + { + _playlistWidgetTiledAction.Enabled = enabled; + _playlistWidgetWindowedAction.Enabled = enabled; + _playlistWidgetHideAction.Enabled = enabled; + } + + private void PianoWidgetAction_IsEnabled(bool enabled) + { + _seqAudioPianoWidgetTiledAction.Enabled = enabled; + _seqAudioPianoWidgetWindowedAction.Enabled = enabled; + _seqAudioPianoWidgetHideAction.Enabled = enabled; + } + + private void SeqAudioTrackInfoWidgetAction_IsEnabled(bool enabled) + { + _seqAudioTrackInfoWidgetTiledAction.Enabled = enabled; + _seqAudioTrackInfoWidgetWindowedAction.Enabled = enabled; + _seqAudioTrackInfoWidgetHideAction.Enabled = enabled; + } + + private void SeqAudioListWidgetAction_IsEnabled(bool enabled) + { + _seqAudioListWidgetTiledAction.Enabled = enabled; + _seqAudioListWidgetWindowedAction.Enabled = enabled; + _seqAudioListWidgetHideAction.Enabled = enabled; + } + #endregion + + #region Widget Auto-Close when Unsupported, Re-Open when Supported and Already Selected + private void CheckWidgetPlaylist() + { + if ((_playlistWidgetTiledAction.Enabled = + _playlistWidgetWindowedAction.Enabled = + _playlistWidgetHideAction.Enabled) == true + ) + { + if (_playlistWidgetTiledAction.GetState()!.GetBoolean() == true) + { + + if (_playlistWindow is not null) + { + if (_playlistWindow.WidgetBox.GetFirstChild() is not null) + { + _playlistWindow.WidgetBox.Remove(_playlistSelector); + } + _playlistWindow.OnCloseRequest -= PlaylistWindow_CloseRequest; + _playlistWindow.Dispose(); + _playlistWindow.Close(); + if (_playlistWindow is not null) + { + _playlistWindow = null!; + } + } + + _playlistBox.Append(_playlistSelector); + } + else if (_playlistWidgetWindowedAction.GetState()!.GetBoolean() == true) + { + if (_playlistBox.GetFirstChild() is not null) + { + _playlistBox.Remove(_playlistSelector); + } + _playlistWindow ??= new WidgetWindow(_playlistSelector); + _playlistWindow.OnCloseRequest += PlaylistWindow_CloseRequest; + _playlistWindow.Present(); + + } + } + else + { + if (_playlistWindow is not null) + { + if (_playlistWindow.WidgetBox.GetFirstChild() is not null) + { + _playlistWindow.WidgetBox.Remove(_playlistSelector); + } + _playlistWindow.OnCloseRequest -= PlaylistWindow_CloseRequest; + _playlistWindow.Dispose(); + _playlistWindow.Close(); + if (_playlistWindow is not null) + { + _playlistWindow = null!; + } + } + if (_playlistBox.GetFirstChild() is not null) + { + _playlistBox.Remove(_playlistSelector); + } + } + } + #endregion + #endregion + + private void OpenPreferences(Gio.SimpleAction sender, Gio.SimpleAction.ActivateSignalArgs args) + { + _preferences = null; + _preferences = new Preferences(); + SetSensitive(false); + SetModal(false); + SetFocus(_preferences); + _preferences.SetModal(true); + _preferences.SetSensitive(true); + + _preferences.Present(); + } + + // When the value is changed on the volume scale + private void VolumeBar_ValueChanged(object sender, EventArgs e) + { + Engine.Instance!.Mixer!.SetVolume((float)(_volumeBar.Adjustment!.Value / _volumeBar.Adjustment.Upper)); + } + + // Sets the volume scale to the specified position + public void SetVolumeBar(float volume) + { + _volumeBar.Adjustment!.Value = (int)(volume * _volumeBar.Adjustment.Upper); + _volumeBar.OnValueChanged += VolumeBar_ValueChanged; + } + + private bool _positionBarFree = true; + private readonly bool _positionBarDebug = false; + + private void PositionBar_MouseButtonPress(object sender, EventArgs args) + { + if (_positionBarDebug) + { + Debug.WriteLine(sender.ToString() + " | " + args.ToString() + " | _positionBarFree: " + _positionBarFree.ToString()); + } + _positionBarFree = false; + } + private void PositionBar_MouseButtonRelease(object sender, EventArgs args) + { + // if (args == EventArgs.Empty) + // { + // return; + // } + + if (_positionBarDebug) + { + Debug.WriteLine(sender.ToString() + " | " + args.ToString() + " | _positionBarFree: " + _positionBarFree.ToString()); + } + + if (!_positionBarFree) + { + Engine.Instance!.Player.SetSongPosition((long)_positionBar.Adjustment!.Value); // Sets the value based on the position when mouse button is released + _positionBarFree = true; // Sets _positionBarFree to true when mouse button is released + if (Engine.Instance!.Player.State is PlayerState.Playing) + { + LetUIKnowPlayerIsPlaying(); // This method will run the void that tells the UI that the player is playing a track + } + else + { + return; + } + } + else + { + return; + } + + } + private void PositionBar_MouseButtonOnBegin(object sender, EventArgs args) + { + if (_positionBarDebug) + { + Debug.WriteLine(sender.ToString() + " | " + args.ToString() + " | _positionBarFree: " + _positionBarFree.ToString()); + } + _positionBarFree = false; + } + private void PositionBar_MouseButtonOnEnd(object sender, EventArgs args) + { + if (_positionBarDebug) + { + Debug.WriteLine(sender.ToString() + " | " + args.ToString() + " | _positionBarFree: " + _positionBarFree.ToString()); + } + _positionBarFree = true; + } + private bool PositionBar_ChangeValue(object sender, EventArgs args) + { + if (_positionBarDebug) + { + Debug.WriteLine(sender.ToString() + " | " + args.ToString() + " | _positionBarFree: " + _positionBarFree.ToString()); + } + return _positionBarFree = false; + } + private void PositionBar_MoveSlider(object sender, EventArgs args) + { + if (_positionBarDebug) + { + Debug.WriteLine(sender.ToString() + " | " + args.ToString() + " | _positionBarFree: " + _positionBarFree.ToString()); + } + UpdatePositionIndicators(Engine.Instance!.Player.ElapsedTicks); + _positionBarFree = false; + } + private void PositionBar_ValueChanged(object sender, EventArgs args) + { + if (Engine.Instance is not null) + { + UpdatePositionIndicators(Engine.Instance!.Player.ElapsedTicks); // Sets the value based on the position when mouse button is released + } + } + + private void SequenceNumberSpinButton_ValueChanged(object sender, EventArgs e) + { + _sequenceNumberSpinButton.OnValueChanged -= SequenceNumberSpinButton_ValueChanged; + _sequenceNumberSpinButton.OnChangeValue -= SequenceNumberSpinButton_ChangeValue; + int index = (int)_sequenceNumberSpinButton.Adjustment!.Value; + if (Engine.Instance is not null) + { + _sequencedAudioList.SelectRow(index); + _sequencedAudioList.ColumnView!.ScrollTo((uint)index, null, Gtk.ListScrollFlags.Select, Gtk.ScrollInfo.New()); + if (Engine.Instance!.Config.Playlists is not null) + { + PlaylistSongStringChanged(index); + } + if (!_preventAutoplay) + { + _autoplay = true; + } + else + { + _preventAutoplay = false; + } + LoadSong(index); + _autoplay = false; + } + _sequenceNumberSpinButton.OnChangeValue += SequenceNumberSpinButton_ChangeValue; + _sequenceNumberSpinButton.OnValueChanged += SequenceNumberSpinButton_ValueChanged; + } + + private void SequenceNumberSpinButton_ChangeValue(Gtk.SpinButton sender, Gtk.SpinButton.ChangeValueSignalArgs args) + { + _sequenceNumberSpinButton.OnValueChanged -= SequenceNumberSpinButton_ValueChanged; + _sequenceNumberSpinButton.OnChangeValue -= SequenceNumberSpinButton_ChangeValue; + int index = (int)_sequenceNumberSpinButton.Adjustment!.Value; + if (Engine.Instance is not null) + { + _sequencedAudioList.SelectRow(index); + _sequencedAudioList.ColumnView!.ScrollTo((uint)index, null, Gtk.ListScrollFlags.Select, Gtk.ScrollInfo.New()); + if (Engine.Instance!.Config.Playlists is not null) + { + PlaylistSongStringChanged(index); + } + _autoplay = true; + LoadSong(index); + _autoplay = false; + } + _sequenceNumberSpinButton.OnChangeValue += SequenceNumberSpinButton_ChangeValue; + _sequenceNumberSpinButton.OnValueChanged += SequenceNumberSpinButton_ValueChanged; + } + + internal static string GetProgramName() + { + return ConfigUtils.PROGRAM_NAME; + } + + private void SetSongToProgramTitle(List songs, int songIndex) + { + Title = $"{GetProgramName()} - {songs[songIndex].Name}"; + } + + // For SequencedAudio_List + internal void ChangeIndex(int index) + { + // First, check if the index is identical to current song index + // to prevent it from unexpectedly stopping + if (index != _curSong) + { + _sequencedAudioList.HasSelectedRow = true; + _preventAutoplay = true; + _sequenceNumberSpinButton.Value = index; + if (Engine.Instance!.Config.Playlists is not null) + { + PlaylistSongStringChanged(index); + } + _sequencedAudioList.HasSelectedRow = false; + } + } + + // For SequencedAudio_List + internal void CheckIndex(int index) + { + // First, check if the index is identical to current song index + // to prevent it from unexpectedly stopping + if (index != _curSong) + { + _preventAutoplay = true; + _sequenceNumberSpinButton.Value = index; + if (Engine.Instance is not null) + { + if (Engine.Instance!.Config.Playlists is not null) + { + PlaylistSongStringChanged(index); + } + } + } + } + + private void ResetPlaylistStuff(bool spinButtonAndPlaylistSelectEnabled) + { + if (Engine.Instance != null) + { + Engine.Instance.Player.ShouldFadeOut = false; + } + _curSong = -1; + _playlist = null; + _endPlaylistAction.Enabled = false; + _sequenceNumberSpinButton.Sensitive = + _playlistSelector.PlaylistDropDown!.Sensitive = + _playlistSelector.PlaylistSongBox!.Sensitive = + spinButtonAndPlaylistSelectEnabled; + } + private void ButtonPlayPlist_Clicked(object sender, EventArgs e) + { + _playlistSelector.ButtonPlayPlist!.OnClicked -= ButtonPlayPlist_Clicked; + if (_playlistSelector.ButtonPlayPlist!.Active) + { + PlayCurrentPlaylist(sender, e); + } + else + { + EndCurrentPlaylist(sender, e); + } + _playlistSelector.ButtonPlayPlist!.OnClicked += ButtonPlayPlist_Clicked; + } + private void ButtonPlistStyle_Clicked(object sender, EventArgs e) + { + if (GlobalConfig.Instance.PlaylistMode is PlaylistMode.Sequential) + { + GlobalConfig.Instance.PlaylistMode = PlaylistMode.Random; + _playlistSelector.ButtonPlistStyle!.SetIconName("media-playlist-shuffle-symbolic"); + } + else if (GlobalConfig.Instance.PlaylistMode is PlaylistMode.Random) + { + GlobalConfig.Instance.PlaylistMode = PlaylistMode.Sequential; + _playlistSelector.ButtonPlistStyle!.SetIconName("media-playlist-consecutive-symbolic"); + } + } + private void CheckPlaylistMode() + { + if (GlobalConfig.Instance.PlaylistMode is PlaylistMode.Sequential) + { + GlobalConfig.Instance.PlaylistMode = PlaylistMode.Sequential; + _playlistSelector.ButtonPlistStyle!.SetIconName("media-playlist-consecutive-symbolic"); + } + else if (GlobalConfig.Instance.PlaylistMode is PlaylistMode.Random) + { + GlobalConfig.Instance.PlaylistMode = PlaylistMode.Random; + _playlistSelector.ButtonPlistStyle!.SetIconName("media-playlist-shuffle-symbolic"); + } + } + private void PlayCurrentPlaylist(object sender, EventArgs e) + { + Config.Playlist playlist = _playlistSelector.GetPlaylist(); + FlexibleDialog.Show(string.Format(Strings.PlayPlaylistBody, Environment.NewLine + playlist), Strings.MenuPlaylist, FlexibleDialog.ButtonsType.YesNo); + FlexibleDialog.OnResponse += ResponseSelected; + void ResponseSelected(FlexibleDialog.ResponseSelected response) + { + FlexibleDialog.OnResponse -= ResponseSelected; + if (response == FlexibleDialog.ResponseSelected.Yes) + { + if (playlist.Songs.Count > 0) + { + _playPlaylistAction.OnActivate -= PlayCurrentPlaylist; + _backupSong = _curSong; + ResetPlaylistStuff(false); + Engine.Instance!.Player.ShouldFadeOut = true; + Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; + _playPlaylistAction.Enabled = false; + _endPlaylistAction.Enabled = true; + _preventAutoplay = false; + _autoplay = true; + _playlist = new PlayingPlaylist(playlist); + _playlist.SetAndLoadNextSong(); + _endPlaylistAction.OnActivate += EndCurrentPlaylist; + } + else + { + _playlistSelector.ButtonPlayPlist!.Active = false; + } + } + else + { + _playlistSelector.ButtonPlayPlist!.Active = false; + } + } + } + private void EndCurrentPlaylist(object sender, EventArgs e) + { + FlexibleDialog.Show(Strings.EndPlaylistBody, Strings.MenuPlaylist, FlexibleDialog.ButtonsType.YesNo); + FlexibleDialog.OnResponse += ResponseSelected; + void ResponseSelected(FlexibleDialog.ResponseSelected response) + { + FlexibleDialog.OnResponse -= ResponseSelected; + if (response == FlexibleDialog.ResponseSelected.Yes) + { + _endPlaylistAction.OnActivate -= EndCurrentPlaylist; + _autoplay = false; + _preventAutoplay = true; + ResetPlaylistStuff(true); + Stop(); + if (_sequenceNumberSpinButton.Value != _backupSong) + { + _sequenceNumberSpinButton.Value = _backupSong; + _backupSong = -1; + } + _playPlaylistAction.Enabled = true; + _playPlaylistAction.OnActivate += PlayCurrentPlaylist; + } + else + { + _playlistSelector.ButtonPlayPlist!.Active = true; + } + } + } + + private void OnPlaylistStringSelected(GObject.Object sender, NotifySignalArgs args) + { + var name = args.Pspec.GetName(); + if (_playlistSelector.PlaylistDropDown!.SelectedItem is not null) + { + if (args.Pspec.GetName() == "selected" || args.Pspec.GetName() == "root") + { + _playlistSelector.PlaylistDropDown.OnNotify -= OnPlaylistStringSelected; + _playlistSelector.PlaylistSongDropDown!.OnNotify -= OnPlaylistSongStringSelected; + _autoplay = false; // Must be set to false first + CheckPlaylistItem(); // Check the playlist item, to set the dropdown to it's first song in the playlist + _playlistSelector.PlaylistStringSelect(); // Selects the playlist item + _playlistChanged = true; // We set this, so that the autoplay doesn't get set while changing playlists + _sequencedAudioList.SelectRow(_playlistSelector.GetSongIndex(_playlistSelector.PlaylistDropDown.Selected)); + if (!_playlistChanged) + { + _sequencedAudioList.ColumnView!.ScrollTo((uint)_playlistSelector.GetSongIndex(_playlistSelector.PlaylistDropDown.Selected), null, Gtk.ListScrollFlags.Select, Gtk.ScrollInfo.New()); + } + if (_sequenceNumberSpinButton.Value != _playlistSelector.GetSongIndex(_playlistSelector.PlaylistDropDown.Selected)) + { + _preventAutoplay = true; + _sequenceNumberSpinButton.Value = _playlistSelector.GetSongIndex(_playlistSelector.PlaylistDropDown.Selected); + } + _playlistChanged = false; // Now we can set it back to false + _playlistSelector.PlaylistDropDown.OnNotify += OnPlaylistStringSelected; + _playlistSelector.PlaylistSongDropDown.OnNotify += OnPlaylistSongStringSelected; + } + } + } + + private void OnPlaylistSongStringSelected(GObject.Object sender, NotifySignalArgs args) + { + // Debug.WriteLine(args.Pspec.GetName()); + if (_playlistSelector.PlaylistSongDropDown!.SelectedItem is not null) + { + if (args.Pspec.GetName() == "selected") + { + _playlistSelector.PlaylistDropDown!.OnNotify -= OnPlaylistStringSelected; + _playlistSelector.PlaylistSongDropDown.OnNotify -= OnPlaylistSongStringSelected; + if (_playlistSelector.PlaylistDropDown.Selected != _playlistSelector.SelectedPlaylistIndex) + { + Stop(); + } + + if (_playlistSelector.PlaylistSongDropDown.Selected != _playlistSelector.SelectedSongIndex) + { + CheckPlaylistItem(); + var selectedItem = (PlaylistSelector)_playlistSelector.PlaylistSongDropDown.SelectedItem; + var selectedItemName = selectedItem.GetTitle(); + foreach (var song in _playlistSelector.Songs!) + { + if (song.Name.Equals(selectedItemName)) + { + _playlistSelector.SelectedSongIndex = _playlistSelector.PlaylistSongDropDown.Selected; + if (!_playlistChanged) + { + _autoplay = true; + } + + _sequencedAudioList.SelectRow(_playlistSelector.GetSongIndex(_playlistSelector.PlaylistDropDown.Selected)); + if (!_playlistChanged) + { + _sequencedAudioList.ColumnView!.ScrollTo((uint)_playlistSelector.GetSongIndex(_playlistSelector.PlaylistDropDown.Selected), null, Gtk.ListScrollFlags.Select, Gtk.ScrollInfo.New()); + } + _sequenceNumberSpinButton.Value = _playlistSelector.GetSongIndex(_playlistSelector.PlaylistDropDown.Selected); + _autoplay = false; + } + } + } + _playlistSelector.PlaylistDropDown.OnNotify += OnPlaylistStringSelected; + _playlistSelector.PlaylistSongDropDown.OnNotify += OnPlaylistSongStringSelected; + } + } + } + private void PlaylistSongStringChanged(int index) + { + if (_playlistSelector.PlaylistSongDropDown!.SelectedItem is not null) + { + foreach (var song in _playlistSelector.Songs!) + { + if (song.Index.Equals(index)) + { + _playlistSelector.PlaylistSongDropDown.SetSelected(_playlistSelector.GetPlaylistSongIndex(index)); + } + } + } + } + + public void SetSong(int index) + { + if (_sequenceNumberSpinButton.Value != index) + { + _sequenceNumberSpinButton.Value = index; + } + else + { + if (!_preventAutoplay) + { + _autoplay = true; + } + else + { + _preventAutoplay = false; + } + LoadSong(index); + } + } + + public void LoadSong(int index) + { + _curSong = index; + + Stop(); + Title = GetProgramName(); + bool success; + if (Engine.Instance == null) + { + return; // Prevents referencing a null Engine.Instance when the engine is being disposed, especially while main window is being closed + } + Player player = Engine.Instance!.Player; + Config cfg = Engine.Instance.Config; + try + { + if (Engine.Instance.Config.Playlists is not null && Engine.Instance.Config.Playlists.Count != 0) + { + List songs = cfg.Playlists![^1].Songs; // Complete "All Songs" playlist is present in all configs at the last index value + int songIndex = songs.FindIndex(s => s.Index == index); + if (songIndex != -1) + { + SetSongToProgramTitle(songs, songIndex); // Done! It's now a func + PlaylistSongStringChanged(index); + CheckPlaylistItem(); + } + } + else + { + List songs = cfg.InternalSongNames![0].Songs; + int songIndex = songs.FindIndex(s => s.Index == index); + if (songIndex != -1) + { + SetSongToProgramTitle(songs, songIndex); + } + } + player.LoadSong(index); + success = Engine.Instance.Player.LoadedSong is not null; // Done! Every Engine now disables playback and remains null when there's no tracks in the sequence + } + catch (Exception ex) + { + if (ex is IndexOutOfRangeException && Engine.Instance is DSEEngine) + { + FlexibleDialog.Show(ex, string.Format(Strings.ErrorLoadSong, Engine.Instance!.Config.GetSongName(index))); + } + else if (ex is DSEInvalidNoteException) + { + var dseEx = ex as DSEInvalidNoteException; + FlexibleDialog.Show($"Attempted to read a note that was out of range.\n\nTrack Index: {dseEx!.TrackIndex}\nCommand Offset: {string.Format("0x{0:X}", dseEx.Offset)}\nAttempted note value: {dseEx.Note} ({string.Format("0x{0:X}", dseEx.Note)})", "Unable to load song."); + } + else + { + FlexibleDialog.Show(ex, string.Format(Strings.ErrorLoadSong, Engine.Instance!.Config.GetSongName(index))); + } + success = false; + } + + ILoadedSong? loadedSong = player.LoadedSong; // LoadedSong is still null when there are no tracks + if (success) + { + if (_buttonPlay.Sensitive is false) + { + _buttonPlay.Sensitive = true; + } + _positionBar.Adjustment!.Upper = loadedSong!.MaxTicks; + _positionBar.SetRange(0, loadedSong.MaxTicks); + SequencedAudio_TrackInfo.SetNumTracks(loadedSong.Events.Length); + _sequencedAudioTrackInfo.AddTrackInfo(); + if (_autoplay) + { + Play(); + } + } + else + { + _buttonPlay.Sensitive = false; + SequencedAudio_TrackInfo.SetNumTracks(0); + } + _trackEditor?.UpdateTracks(); + _positionBar.Sensitive = _exportWAVAction.Enabled = success; + _exportMIDIAction.Enabled = success && MP2KEngine.MP2KInstance is not null; + _exportDLSAction.Enabled = _exportSF2Action.Enabled = success && AlphaDreamEngine.AlphaDreamInstance is not null; + } + + private void OpenDSE(Gio.SimpleAction sender, EventArgs e) + { + GTK4Utils.OnPathChanged += LoadSWD; + GTK4Utils.CreateLoadDialog(["*.swd"], Strings.MenuOpenSWD, Strings.FilterOpenSWD); + + void LoadSWD(string swdPath) + { + GTK4Utils.OnPathChanged -= LoadSWD; + if (swdPath is null) + { + return; + } + + GTK4Utils.OnPathChanged += LoadFiles; + GTK4Utils.CreateLoadDialog(Strings.MenuOpenSMD); + + void LoadFiles(string smdPath) + { + GTK4Utils.OnPathChanged -= LoadFiles; + if (smdPath is null) + { + return; + } + if (Engine.Instance is not null) + { + DisposeEngine(); + } + try + { + _ = new DSEEngine(swdPath, smdPath, true); + } + catch (Exception ex) + { + FlexibleDialog.Show(ex, Strings.ErrorOpenDSE); + return; + } + DSEConfig config = DSEEngine.DSEInstance!.Config; + _sequencedAudioList.ChangeColumns(); + FinishLoading(config.SMDFiles.Length); + _sequenceNumberSpinButton.Visible = false; + _sequenceNumberSpinButton.Hide(); + _buttonRecord.Sensitive = true; + _playlistSelector.PlaylistDropDown!.Sensitive = + _playlistSelector.ButtonPlayPlist!.Sensitive = + _playlistSelector.ButtonPlistStyle!.Sensitive = + _playlistSelector.ButtonPrevPlistSong!.Sensitive = + _playlistSelector.PlaylistSongDropDown!.Sensitive = + _playlistSelector.ButtonNextPlistSong!.Sensitive = false; + _midiConverterDialogAction.Enabled = false; + _trackEditorAction.Enabled = true; + _exportASMAction.Enabled = false; + _exportMIDIAction.Enabled = false; + _exportDLSAction.Enabled = false; + _exportSF2Action.Enabled = false; + _playPlaylistAction.Enabled = + _endPlaylistAction.Enabled = false; + PlaylistWidgetAction_IsEnabled(false); + PianoWidgetAction_IsEnabled(true); + SeqAudioTrackInfoWidgetAction_IsEnabled(true); + SeqAudioListWidgetAction_IsEnabled(true); + CheckWidgetPlaylist(); + } + } + } + private void OpenSDAT(Gio.SimpleAction sender, EventArgs e) + { + GTK4Utils.OnPathChanged += LoadFile; + GTK4Utils.CreateLoadDialog(["*.sdat"], Strings.MenuOpenSDAT, Strings.FilterOpenSDAT); + + void LoadFile(string path) + { + GTK4Utils.OnPathChanged -= LoadFile; + if (path is null) + { + return; + } + if (Engine.Instance is not null) + { + DisposeEngine(); + } + try + { + using FileStream stream = File.OpenRead(path); + _ = new SDATEngine(new SDAT(stream)); + } + catch (Exception ex) + { + FlexibleDialog.Show(ex, Strings.ErrorOpenSDAT); + return; + } + + SDATConfig config = SDATEngine.SDATInstance!.Config; + _sequencedAudioList.ChangeColumns(); + FinishLoading(config.SDAT.INFOBlock.SequenceInfos.NumEntries); + _sequenceNumberSpinButton.Visible = false; + _sequenceNumberSpinButton.Hide(); + _buttonRecord.Sensitive = true; + _playlistSelector.PlaylistDropDown!.Sensitive = + _playlistSelector.ButtonPlayPlist!.Sensitive = + _playlistSelector.ButtonPlistStyle!.Sensitive = + _playlistSelector.ButtonPrevPlistSong!.Sensitive = + _playlistSelector.PlaylistSongDropDown!.Sensitive = + _playlistSelector.ButtonNextPlistSong!.Sensitive = false; + _midiConverterDialogAction.Enabled = false; + _trackEditorAction.Enabled = true; + _exportASMAction.Enabled = false; + _exportMIDIAction.Enabled = false; + _exportDLSAction.Enabled = false; + _exportSF2Action.Enabled = false; + _playPlaylistAction.Enabled = + _endPlaylistAction.Enabled = false; + PlaylistWidgetAction_IsEnabled(false); + PianoWidgetAction_IsEnabled(true); + SeqAudioTrackInfoWidgetAction_IsEnabled(true); + SeqAudioListWidgetAction_IsEnabled(true); + CheckWidgetPlaylist(); + } + } + private void OpenAlphaDream(Gio.SimpleAction sender, EventArgs e) + { + GTK4Utils.OnPathChanged += LoadFile; + GTK4Utils.CreateLoadDialog(["*.gba", "*.srl"], Strings.MenuOpenAlphaDream, Strings.FilterOpenGBA); + + void LoadFile(string path) + { + GTK4Utils.OnPathChanged -= LoadFile; + if (path is null) + { + return; + } + if (Engine.Instance is not null) + { + DisposeEngine(); + } + try + { + _ = new AlphaDreamEngine(File.ReadAllBytes(path)); + } + catch (Exception ex) + { + FlexibleDialog.Show(ex, Strings.ErrorOpenAlphaDream); + return; + } + + AlphaDreamConfig config = AlphaDreamEngine.AlphaDreamInstance!.Config; + _sequencedAudioList.ChangeColumns(true); + FinishLoading(config.SongTableSizes[0]); + _sequenceNumberSpinButton.Visible = true; + _sequenceNumberSpinButton.Show(); + _buttonRecord.Sensitive = true; + _playlistSelector.PlaylistDropDown!.Sensitive = + _playlistSelector.ButtonPlayPlist!.Sensitive = + _playlistSelector.ButtonPlistStyle!.Sensitive = + _playlistSelector.ButtonPrevPlistSong!.Sensitive = + _playlistSelector.PlaylistSongDropDown!.Sensitive = + _playlistSelector.ButtonNextPlistSong!.Sensitive = true; + _midiConverterDialogAction.Enabled = false; + _trackEditorAction.Enabled = true; + _exportASMAction.Enabled = false; + _exportMIDIAction.Enabled = false; + _exportDLSAction.Enabled = true; + _exportSF2Action.Enabled = true; + _playPlaylistAction.Enabled = true; + _endPlaylistAction.Enabled = false; + PlaylistWidgetAction_IsEnabled(true); + PianoWidgetAction_IsEnabled(true); + SeqAudioTrackInfoWidgetAction_IsEnabled(true); + SeqAudioListWidgetAction_IsEnabled(true); + CheckWidgetPlaylist(); + } + } + + private void OpenMP2K(Gio.SimpleAction sender, EventArgs e) + { + GTK4Utils.OnPathChanged += LoadFile; + GTK4Utils.CreateLoadDialog(["*.gba", "*.srl"], Strings.MenuOpenMP2K, Strings.FilterOpenGBA); + + void LoadFile(string path) + { + GTK4Utils.OnPathChanged -= LoadFile; + if (path is null) + { + return; + } + if (Engine.Instance is not null) + { + DisposeEngine(); + } + + try + { + _ = new MP2KEngine(File.ReadAllBytes(path), false); + } + catch (Exception ex) + { + DisposeEngine(); + FlexibleDialog.Show(ex, Strings.ErrorOpenMP2K); + return; + } + + MP2KConfig config = MP2KEngine.MP2KInstance!.Config; + _sequencedAudioList.ChangeColumns(true); + FinishLoading(config.SongTableSizes[0]); + _sequenceNumberSpinButton.Visible = true; + _sequenceNumberSpinButton.Show(); + _buttonRecord.Sensitive = true; + _playlistSelector.PlaylistDropDown!.Sensitive = + _playlistSelector.ButtonPlayPlist!.Sensitive = + _playlistSelector.ButtonPlistStyle!.Sensitive = + _playlistSelector.ButtonPrevPlistSong!.Sensitive = + _playlistSelector.PlaylistSongDropDown!.Sensitive = + _playlistSelector.ButtonNextPlistSong!.Sensitive = true; + _midiConverterDialogAction.Enabled = true; + _trackEditorAction.Enabled = true; + _soundBankEditorAction.Enabled = true; + _exportASMAction.Enabled = true; + _exportMIDIAction.Enabled = true; + _exportDLSAction.Enabled = false; + _exportSF2Action.Enabled = false; + _playPlaylistAction.Enabled = true; + _endPlaylistAction.Enabled = false; + PlaylistWidgetAction_IsEnabled(true); + PianoWidgetAction_IsEnabled(true); + SeqAudioTrackInfoWidgetAction_IsEnabled(true); + SeqAudioListWidgetAction_IsEnabled(true); + CheckWidgetPlaylist(); + } + } + private void ExportASM(Gio.SimpleAction sender, EventArgs e) + { + GTK4Utils.CreateSaveDialog(Engine.Instance!.Config.GetSongName((int)_sequenceNumberSpinButton.Value), ["*.s"], Strings.MenuSaveASM, Strings.FilterSaveASM); + GTK4Utils.OnPathChanged += SaveFile; + + static void SaveFile(string path) + { + GTK4Utils.OnPathChanged -= SaveFile; + if (path is null) + { + return; + } + + MP2KPlayer p = MP2KEngine.MP2KInstance!.Player; + var args = new ASMSaveArgs(true); + try + { + p.SaveAsASM(path, args); + FlexibleDialog.Show(string.Format(Strings.SuccessSaveASM, path), Strings.SuccessSaveASM); + } + catch (Exception ex) + { + FlexibleDialog.Show(ex.Message, Strings.ErrorSaveASM); + } + } + } + private void ExportMIDI(Gio.SimpleAction sender, EventArgs e) + { + GTK4Utils.CreateSaveDialog(Engine.Instance!.Config.GetSongName((int)_sequenceNumberSpinButton.Value), ["*.mid", "*.midi"], Strings.MenuSaveMIDI, Strings.FilterSaveMIDI); + GTK4Utils.OnPathChanged += SaveFile; + + static void SaveFile(string path) + { + GTK4Utils.OnPathChanged -= SaveFile; + if (path is null) + { + return; + } + + MP2KPlayer p = MP2KEngine.MP2KInstance!.Player; + var args = new MIDISaveArgs(true, false, [(0, (4, 4))]); // timeSignatures collection contains: (int AbsoluteTick, (byte Numerator, byte Denominator)) + + try + { + p.SaveAsMIDI(path, args); + FlexibleDialog.Show(string.Format(Strings.SuccessSaveMIDI, path), Strings.SuccessSaveMIDI); + } + catch (Exception ex) + { + FlexibleDialog.Show(ex, Strings.ErrorSaveMIDI); + } + } + } + private void ExportDLS(Gio.SimpleAction sender, EventArgs e) + { + GTK4Utils.CreateSaveDialog(Engine.Instance!.Config.GetGameName(), ["*.dls"], Strings.MenuSaveDLS, Strings.FilterSaveDLS); + GTK4Utils.OnPathChanged += SaveFile; + + static void SaveFile(string path) + { + GTK4Utils.OnPathChanged -= SaveFile; + if (path is null) + { + return; + } + + AlphaDreamConfig cfg = AlphaDreamEngine.AlphaDreamInstance!.Config; + + try + { + AlphaDreamSoundFontSaver_DLS.Save(cfg, path); + FlexibleDialog.Show(string.Format(Strings.SuccessSaveDLS, path), Strings.SuccessSaveDLS); + } + catch (Exception ex) + { + FlexibleDialog.Show(ex, Strings.ErrorSaveDLS); + } + } + } + private void ExportSF2(Gio.SimpleAction sender, EventArgs e) + { + GTK4Utils.CreateSaveDialog(Engine.Instance!.Config.GetGameName(), ["*.sf2"], Strings.MenuSaveSF2, Strings.FilterSaveSF2); + GTK4Utils.OnPathChanged += SaveFile; + + static void SaveFile(string path) + { + GTK4Utils.OnPathChanged -= SaveFile; + if (path is null) + { + return; + } + + AlphaDreamConfig cfg = AlphaDreamEngine.AlphaDreamInstance!.Config; + + try + { + AlphaDreamSoundFontSaver_SF2.Save(path, cfg); + FlexibleDialog.Show(string.Format(Strings.SuccessSaveSF2, path), Strings.SuccessSaveSF2); + } + catch (Exception ex) + { + FlexibleDialog.Show(ex, Strings.ErrorSaveSF2); + } + } + } + private void ExportWAV(object sender, EventArgs e) + { + GTK4Utils.CreateSaveDialog(Engine.Instance!.Config.GetSongName((int)_sequenceNumberSpinButton.Value), ["*.wav"], Strings.MenuSaveWAV, Strings.FilterSaveWAV); + GTK4Utils.OnPathChanged += SaveFile; + + void SaveFile(string path) + { + GTK4Utils.OnPathChanged -= SaveFile; + if (path is null) + { + return; + } + + Stop(); + + Player player = Engine.Instance!.Player; + bool oldFade = player.ShouldFadeOut; + long oldLoops = player.NumLoops; + player.ShouldFadeOut = true; + player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; + + try + { + player.Record(path); + FlexibleDialog.Show(string.Format(Strings.SuccessSaveWAV, path), Strings.SuccessSaveWAV); + } + catch (Exception ex) + { + FlexibleDialog.Show(ex, Strings.ErrorSaveWAV); + } + + player.ShouldFadeOut = oldFade; + player.NumLoops = oldLoops; + } + } + + public void LetUIKnowPlayerIsPlaying() + { + // Prevents method from being used if timer is already active + if (_timer.IsActive()) + { + return; + } + + // _sequencedAudioTrackInfo.AddEntries(); + + // Configures the buttons when player is playing a sequenced track + _buttonPause.Sensitive = _buttonStop.Sensitive = true; // Setting the 'Sensitive' property to 'true' enables the buttons, allowing you to click on them + _buttonPause.TooltipText = Strings.PlayerPause; + + ConfigureTimer(); + } + + // Configures the timer, which triggers the CheckPlayback method at every interval depending on the GlobalConfig RefreshRate + private void ConfigureTimer() + { + var context = GLib.MainContext.GetThreadDefault(); // Grabs the default GLib MainContext thread + var source = GLib.Functions.TimeoutSourceNew(50); // Creates and configures the timeout interval + source.SetCallback(TimerCallback); // Sets the callback for the timer interval to be used on + var microsec = new CULong(source.Attach(context)); // Configures the microseconds based on attaching the GLib MainContext thread + // _timer.Elapsed(ref microsec); // Adds the pointer to the configured microseconds source + GLib.Internal.Timer.Elapsed(_timer.Handle, ref microsec); // GLib.Timer.Elapsed was removed in GirCore 0.6.3, so we're using this workaround instead + _timer.Start(); // Starts the timer + } + + private void Play() + { + _buttonPlay.OnClicked -= ButtonPlay_Clicked; + Engine.Instance!.Player.IsPauseToggled = _buttonPause.Active = false; + _sequencedAudioTrackInfo.ResetTempo(); + Engine.Instance.Player.Play(); + LetUIKnowPlayerIsPlaying(); + _buttonPlay.OnClicked += ButtonPlay_Clicked; + } + private void Pause() + { + _buttonPause.OnClicked -= ButtonPause_Clicked; + Engine.Instance!.Player.TogglePlaying(); + if (Engine.Instance.Player.State == PlayerState.Paused) + { + _buttonPause.Active = true; + _buttonPause.TooltipText = Strings.PlayerUnpause; + Engine.Instance.Player.IsPauseToggled = true; + _timer.Stop(); + } + else + { + _buttonPause.Active = false; + _buttonPause.TooltipText = Strings.PlayerPause; + Engine.Instance.Player.IsPauseToggled = false; + _timer.Start(); + } + _buttonPause.OnClicked += ButtonPause_Clicked; + } + private void Stop() + { + _buttonStop.OnClicked -= ButtonStop_Clicked; + if (Engine.Instance == null) + { + return; // This is here to ensure that it returns if the Engine.Instance is null while closing the main window + } + _timer.Stop(); + Engine.Instance!.Player.Stop(); + _sequencedAudioTrackInfo.Info!.Reset(); + _sequencedAudioTrackInfo.ResetTempo(); + Engine.Instance.Player.IsPauseToggled = _buttonPause.Active = false; + _buttonPause.Sensitive = _buttonStop.Sensitive = false; + _buttonPause.TooltipText = Strings.PlayerPause; + UpdatePositionIndicators(0L); + _buttonStop.OnClicked += ButtonStop_Clicked; + } + private void TogglePlayback() + { + switch (Engine.Instance!.Player.State) + { + case PlayerState.Stopped: Play(); break; + case PlayerState.Paused: + case PlayerState.Playing: Pause(); break; + } + } + private void ButtonPlay_Clicked(Gtk.Button sender, EventArgs args) + { + Play(); + } + + private void ButtonPause_Clicked(Gtk.Button sender, EventArgs args) + { + Pause(); + } + + private void ButtonStop_Clicked(Gtk.Button sender, EventArgs args) + { + Stop(); + } + + private void PlayPreviousSong(object? sender, EventArgs? e) + { + _playlistSelector.ButtonPrevPlistSong!.OnClicked -= PlayPreviousSong; + _playlistSelector.ButtonNextPlistSong!.OnClicked -= PlayNextSong; + if (_playlist is not null) + { + _playlist.UndoThenSetAndLoadPrevSong(_curSong); + } + else + { + _playlistSelector.PlaylistSongDropDown!.Selected -= 1; + _autoplay = true; + int index = _playlistSelector.Songs![(int)_playlistSelector.PlaylistSongDropDown.Selected].Index; + _sequencedAudioList.SelectRow(index); + if (!_playlistChanged) + { + _sequencedAudioList.ColumnView!.ScrollTo((uint)index, null, Gtk.ListScrollFlags.Select, Gtk.ScrollInfo.New()); + } + _sequenceNumberSpinButton.Value = index; + _autoplay = false; + CheckPlaylistItem(); + } + _playlistSelector.ButtonPrevPlistSong.OnClicked += PlayPreviousSong; + _playlistSelector.ButtonNextPlistSong.OnClicked += PlayNextSong; + } + private void PlayNextSong(object? sender, EventArgs? e) + { + _playlistSelector.ButtonPrevPlistSong!.OnClicked -= PlayPreviousSong; + _playlistSelector.ButtonNextPlistSong!.OnClicked -= PlayNextSong; + if (_playlist is not null) + { + _playlist.AdvanceThenSetAndLoadNextSong(_curSong); + } + else + { + _playlistSelector.PlaylistSongDropDown!.Selected += 1; + _autoplay = true; + int index = _playlistSelector.Songs![(int)_playlistSelector.PlaylistSongDropDown.Selected].Index; + _sequencedAudioList.SelectRow(index); + if (!_playlistChanged) + { + _sequencedAudioList.ColumnView!.ScrollTo((uint)index, null, Gtk.ListScrollFlags.Select, Gtk.ScrollInfo.New()); + } + _sequenceNumberSpinButton.Value = index; + _autoplay = false; + CheckPlaylistItem(); + } + _playlistSelector.ButtonPrevPlistSong.OnClicked += PlayPreviousSong; + _playlistSelector.ButtonNextPlistSong.OnClicked += PlayNextSong; + } + + private void CheckPlaylistItem() + { + // For the Previous Song button + if (_playlistSelector.PlaylistSongDropDown!.Selected is 0) + { + _playlistSelector.ButtonPrevPlistSong!.Sensitive = false; + } + else + { + _playlistSelector.ButtonPrevPlistSong!.Sensitive = true; + } + + // For the Next Song button + if (_playlistSelector.PlaylistSongDropDown.Selected == PlaylistSelector.GetNumSongs() - 1) + { + _playlistSelector.ButtonNextPlistSong!.Sensitive = false; + } + else + { + _playlistSelector.ButtonNextPlistSong!.Sensitive = true; + } + } + + private void FinishLoading(long numSongs) + { + Engine.Instance!.Player.SongEnded += SongEnded; + _sequencedAudioList.Show(); + var config = Engine.Instance.Config; + + // Ensures a GlobalConfig Instance is created if one doesn't exist + if (GlobalConfig.Instance == null) + { + GlobalConfig.Init(); // A new instance needs to be initialized before it can do anything + } + _sequencedAudioList.AddEntries(numSongs, config); + if (config.Playlists is not null) + { + _playlistSelector.AddPlaylistEntries(config.Playlists); + CheckPlaylistMode(); + } + _sequenceNumberSpinButton.Adjustment!.Upper = numSongs - 1; +#if DEBUG + // [Debug methods specific to this GUI will go in here] +#endif + _autoplay = false; + _sequenceNumberSpinButton.Sensitive = _buttonPlay.Sensitive = _volumeBar.Sensitive = true; + int index = 0; + if (Engine.Instance!.Config.InternalSongNames is not null && Engine.Instance!.Config.InternalSongNames.Capacity > 0) + { + index = Engine.Instance.Config.InternalSongNames[0].Songs.Count == 0 ? 0 : Engine.Instance.Config.InternalSongNames[0].Songs[0].Index; + } + if (Engine.Instance!.Config.Playlists is not null && Engine.Instance!.Config.Playlists.Capacity > 0) + { + index = Engine.Instance.Config.Playlists[^1].Songs.Count == 0 ? 0 : Engine.Instance.Config.Playlists[^1].Songs[0].Index; + PlaylistSongStringChanged(index); + } + _sequencedAudioList.SelectRow(index); + _sequencedAudioList.ColumnView!.ScrollTo((uint)index, null, Gtk.ListScrollFlags.Select, Gtk.ScrollInfo.New()); + _preventAutoplay = true; + if (_sequenceNumberSpinButton.Value != index) + { + _sequenceNumberSpinButton.Value = index; + } + else + { + SequenceNumberSpinButton_ValueChanged(null!, null!); + } + if (config.Playlists is not null) + { + _playlistSelector.PlaylistDropDown!.OnNotify += OnPlaylistStringSelected; + _playlistSelector.PlaylistSongDropDown!.OnNotify += OnPlaylistSongStringSelected; + } + _volumeBar.SetValue(100); + } + public void ReloadEngine() + { + if (Engine.Instance is not null) + { + Stop(); + Engine.Instance.Reload(); + ResetPlaylistStuff(true); + UpdatePositionIndicators(0L); + } + } + private void DisposeEngine() + { + if (Engine.Instance is not null) + { + Stop(); + Engine.Instance.Dispose(); + } + + //_trackEditor?.UpdateTracks(); + Name = GetProgramName(); + SequencedAudio_TrackInfo.SetNumTracks(0); + _sequencedAudioTrackInfo.ResetMutes(); + ResetPlaylistStuff(false); + UpdatePositionIndicators(0L); + _sequenceNumberSpinButton.Visible = false; + _sequenceNumberSpinButton.Value = _sequenceNumberSpinButton.Adjustment!.Upper = 0; + } + + private bool TimerCallback() + { + if (_songEnded) + { + _songEnded = false; + if (_playlist is not null) + { + _playlist.AdvanceThenSetAndLoadNextSong(_curSong); + } + else + { + Stop(); + } + } + if (Engine.Instance is not null) + { + if (_positionBarFree) + { + Player player = Engine.Instance!.Player; + player.Info = _sequencedAudioTrackInfo.Info!; + if (player.ErrorDetails is not null) + { + FlexibleDialog.Show(player.ErrorDetails, player.ErrorDetails.Message); + Stop(); + player.ErrorDetails = null; + } + _piano.UpdateKeys(player.Info.Tracks, _sequencedAudioTrackInfo.NumTracks!); + if (player.State is PlayerState.Stopped) + { + UpdatePositionIndicators(0L); + } + else + { + UpdatePositionIndicators(player.ElapsedTicks); + } + } + } + return true; + } + + private void SongEnded() + { + _songEnded = true; + } + + // This updates _positionBar to the value specified + private void UpdatePositionIndicators(long ticks) + { + if (_positionBarFree) + { + _positionBar.Adjustment!.SetValue(ticks); + } + } + + private void OpenMIDIConverterDialog(Gio.SimpleAction sender, Gio.SimpleAction.ActivateSignalArgs args) + { + _midiConverterDialog = new MIDIConverterDialog(); + _midiConverterDialog.Present(this); + + _midiConverterDialog.OnClosed += WindowClosed; + + void WindowClosed(Dialog sender, EventArgs args) + { + _midiConverterDialog!.Dispose(); + _midiConverterDialog = null!; + } + } + + private void OpenTrackEditor(Gio.SimpleAction sender, Gio.SimpleAction.ActivateSignalArgs args) + { + if (_trackEditor is not null) + { + _trackEditor.FocusVisible = true; + } + + _trackEditor = new TrackEditor(); + _windowGroup.AddWindow(_trackEditor); + if (Engine.Instance is not null) + { + _trackEditor.Init(); + _trackEditor.ReloadDropDownEntries(); + // _trackEditor.ReloadColumnEntries(); + } + _trackEditor.Present(); + + _trackEditor.OnCloseRequest += WindowClosed; + + bool WindowClosed(Gtk.Window sender, EventArgs args) + { + _windowGroup.RemoveWindow(_trackEditor); + _trackEditor!.Dispose(); + _trackEditor = null!; + return false; + } + } + + private void OpenSoundBankEditor(Gio.SimpleAction sender, Gio.SimpleAction.ActivateSignalArgs args) + { + if (_soundBankEditor is not null) + { + _soundBankEditor.FocusVisible = true; + } + + _soundBankEditor = new SoundBankEditor(); + _windowGroup.AddWindow(_soundBankEditor); + if (Engine.Instance is not null) + { + _soundBankEditor.Init(); + _soundBankEditor.LoadVoices(Engine.Instance.Player.LoadedSong!.Bank); + } + _soundBankEditor.Present(); + + _soundBankEditor.OnCloseRequest += WindowClosed; + + bool WindowClosed(Gtk.Window sender, EventArgs args) + { + _windowGroup.RemoveWindow(_soundBankEditor); + _soundBankEditor!.Dispose(); + _soundBankEditor = null!; + return false; + } + } +} diff --git a/VG Music Studio - GTK4/PlayingPlaylist.cs b/VG Music Studio - GTK4/PlayingPlaylist.cs new file mode 100644 index 00000000..817cec0b --- /dev/null +++ b/VG Music Studio - GTK4/PlayingPlaylist.cs @@ -0,0 +1,48 @@ +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.Util; +using System.Collections.Generic; +using System.Linq; + +namespace Kermalis.VGMusicStudio.GTK4; + +internal sealed class PlayingPlaylist +{ + public readonly List _playedSongs; + public readonly List _remainingSongs; + public readonly Config.Playlist _curPlaylist; + + public PlayingPlaylist(Config.Playlist play) + { + _playedSongs = []; + _remainingSongs = []; + _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); + MainWindow.Instance!.SetSong(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); + MainWindow.Instance!.SetSong(nextSong); + } +} diff --git a/VG Music Studio - GTK4/PlaylistSelector.cs b/VG Music Studio - GTK4/PlaylistSelector.cs new file mode 100644 index 00000000..8e38d575 --- /dev/null +++ b/VG Music Studio - GTK4/PlaylistSelector.cs @@ -0,0 +1,434 @@ +using System; +using System.Collections.Generic; +using Gtk; +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.Properties; + +namespace Kermalis.VGMusicStudio.GTK4; + +internal class PlaylistSelector : Box +{ + public DropDown? PlaylistDropDown { get; set; } + public DropDown? PlaylistSongDropDown { get; set; } + public GestureClick? PlaylistClick { get; set; } + public GestureClick? PlaylistSongClick { get; set; } + public ToggleButton? ButtonPlayPlist; + public Button? ButtonPlistStyle; + public Button? ButtonPrevPlistSong, ButtonNextPlistSong; + public uint SelectedPlaylistIndex, SelectedSongIndex = 0; + public static int PrevSelectedPlaylistIndex, PrevSelectedSongIndex = -1; + private readonly SignalListItemFactory? PlaylistFactory; + private readonly SignalListItemFactory? SongFactory; + private readonly static Gio.ListStore PlaylistModel = Gio.ListStore.New(GetGType()); + private readonly static Gio.ListStore PlaylistSongModel = Gio.ListStore.New(GetGType()); + private List? Playlists { get; set; } + internal List? Songs { get; set; } + private readonly Box? PlaylistBox, PlaylistSongBoxDropDown; + internal readonly Box? PlaylistSongBox; + private int Index { get; set; } + private string? Title { get; set; } + private string? ImageName { get; set; } + private PlaylistSelector? SelectedPlaylist { get; set; } + private PlaylistSelector? SelectedSong { get; set; } + private static Contents[]? ContentsPlaylist { get; set; } + private static Contents[]? ContentsSong { get; set; } + internal bool PlaylistIsSelected = false; + internal bool SongIsSelected = false; + + protected PlaylistSelector(string title, string image) + : base() + { + Title = title; + ImageName = image; + } + internal PlaylistSelector() + { + SetOrientation(Orientation.Vertical); + Spacing = 1; + Halign = Align.Center; + + IconTheme.GetForDisplay(Gdk.Display.GetDefault()!).AddResourcePath("/org/Kermalis/VGMusicStudio/GTK4/icons/scalable/actions"); + + ButtonPlayPlist = new ToggleButton() { Sensitive = false, TooltipText = "Play Playlist", IconName = "vgms-play-playlist-symbolic" }; + ButtonPlistStyle = new Button() { Sensitive = false, TooltipText = "Play Style" }; + + ButtonPrevPlistSong = new Button() { Sensitive = false, TooltipText = Strings.PlayerPreviousSong, IconName = "media-skip-backward-symbolic" }; + ButtonNextPlistSong = new Button() { Sensitive = false, TooltipText = Strings.PlayerNextSong, IconName = "media-skip-forward-symbolic" }; + + PlaylistDropDown = new DropDown + { + WidthRequest = 300, + Sensitive = false + }; + PlaylistDropDown.SetModel(PlaylistModel); + PlaylistSongDropDown = new DropDown + { + WidthRequest = 300, + Sensitive = false + }; + PlaylistSongDropDown.SetModel(PlaylistSongModel); + + PlaylistFactory = SignalListItemFactory.New(); + PlaylistFactory.OnSetup += PlaylistFactory_SetupPlaylists; + PlaylistFactory.OnBind += PlaylistFactory_BindPlaylists; + + PlaylistDropDown!.SetFactory(PlaylistFactory); + + SongFactory = SignalListItemFactory.New(); + SongFactory.OnSetup += SongFactory_SetupSongs; + SongFactory.OnBind += SongFactory_BindSongs; + + PlaylistSongDropDown!.SetFactory(SongFactory); + + PlaylistBox = New(Orientation.Horizontal, 4); + PlaylistBox.Halign = Align.Center; + PlaylistSongBox = New(Orientation.Horizontal, 4); + PlaylistSongBox.Halign = Align.Center; + + PlaylistBox.Append(ButtonPlayPlist); + PlaylistBox.Append(PlaylistDropDown); + PlaylistBox.Append(ButtonPlistStyle); + PlaylistSongBoxDropDown = New(Orientation.Horizontal, 1); + PlaylistSongBoxDropDown.Halign = Align.Center; + PlaylistSongBoxDropDown.Append(PlaylistSongDropDown); + PlaylistSongBox.MarginStart = 40; + PlaylistSongBox.MarginEnd = 40; + PlaylistSongBox.Append(ButtonPrevPlistSong); + PlaylistSongBox.Append(PlaylistSongBoxDropDown); + PlaylistSongBox.Append(ButtonNextPlistSong); + + Append(PlaylistBox); + Append(PlaylistSongBox); + } + + protected struct Contents() + { + internal Label Title = Label.New(""); + internal Image Image = Image.New(); + internal Image Checkmark = Image.NewFromIconName("object-select-symbolic"); + } + + private void PlaylistFactory_SetupPlaylists(SignalListItemFactory sender, SignalListItemFactory.SetupSignalArgs args) + { + if (args.Object is not ListItem item) + { + return; + } + if (Index >= ContentsPlaylist!.Length - 1) + { + Index = 0; + ContentsPlaylist[^1] = new(); + ContentsPlaylist[^1].Title.SetXalign(0); + ContentsPlaylist[^1].Title.SetMaxWidthChars(20); + ContentsPlaylist[^1].Title.SetEllipsize(Pango.EllipsizeMode.End); + + Box box = New(Orientation.Horizontal, 10); + + box.Append(ContentsPlaylist[^1].Image); + box.Append(ContentsPlaylist[^1].Title); + box.Append(ContentsPlaylist[^1].Checkmark); + + item.SetChild(box); + } + else + { + ContentsPlaylist[Index] = new(); + ContentsPlaylist[Index].Title.SetXalign(0); + ContentsPlaylist[Index].Title.SetMaxWidthChars(50); + ContentsPlaylist[Index].Title.SetEllipsize(Pango.EllipsizeMode.End); + + Box box = New(Orientation.Horizontal, 10); + + box.Append(ContentsPlaylist[Index].Image); + box.Append(ContentsPlaylist[Index].Title); + box.Append(ContentsPlaylist[Index].Checkmark); + + item.SetChild(box); + Index++; + } + } + + private void SongFactory_SetupSongs(SignalListItemFactory sender, SignalListItemFactory.SetupSignalArgs args) + { + if (args.Object is not ListItem item) + { + return; + } + if (Index >= ContentsSong!.Length - 1) + { + Index = 0; + ContentsSong[^1] = new(); + ContentsSong[^1].Title.SetXalign(0); + ContentsSong[^1].Title.SetMaxWidthChars(20); + ContentsSong[^1].Title.SetEllipsize(Pango.EllipsizeMode.End); + + Box box = New(Orientation.Horizontal, 10); + + box.Append(ContentsSong[^1].Image); + box.Append(ContentsSong[^1].Title); + box.Append(ContentsSong[^1].Checkmark); + + item.SetChild(box); + } + else + { + ContentsSong[Index] = new(); + ContentsSong[Index].Title.SetXalign(0); + ContentsSong[Index].Title.SetMaxWidthChars(50); + ContentsSong[Index].Title.SetEllipsize(Pango.EllipsizeMode.End); + + Box box = New(Orientation.Horizontal, 10); + + box.Append(ContentsSong[Index].Image); + box.Append(ContentsSong[Index].Title); + box.Append(ContentsSong[Index].Checkmark); + + item.SetChild(box); + Index++; + } + } + + private void PlaylistFactory_BindPlaylists(SignalListItemFactory sender, SignalListItemFactory.BindSignalArgs args) + { + if (args.Object is not ListItem item) + { + return; + } + if (item.Item is not PlaylistSelector holder) + { + return; + } + if (item.Child is not Box box) + { + return; + } + BindItems(ContentsPlaylist![(int)item.Position], holder, box); + } + + private void SongFactory_BindSongs(SignalListItemFactory sender, SignalListItemFactory.BindSignalArgs args) + { + if (args.Object is not ListItem item) + { + return; + } + if (item.Item is not PlaylistSelector holder) + { + return; + } + if (item.Child is not Box box) + { + return; + } + BindItems(ContentsSong![(int)item.Position], holder, box); + } + + private void BindItems(Contents contents, PlaylistSelector holder, Box box) + { + contents.Title = (Label)box.GetFirstChild()!.GetNextSibling()!; + contents.Title.SetLabel(holder.Title!); + contents.Image = (Image)box.GetFirstChild()!; + contents.Image.SetFromIconName(holder.ImageName); + contents.Checkmark = (Image)box.GetLastChild()!; + Popover popup = (Popover)box.GetAncestor(Popover.GetGType())!; + if (popup is not null) + { + if (popup.IsAncestor(PlaylistDropDown!)) + { + DropDown dropdown = (DropDown)popup.GetAncestor(DropDown.GetGType())!; + PlaylistSelector selectedItem = (PlaylistSelector)dropdown.SelectedItem!; + if (selectedItem.Title == contents.Title.Label_) + { + SelectedPlaylist = selectedItem; + contents.Checkmark.SetVisible(true); + dropdown.OnNotify += Playlist_Notify; + } + else + { + contents.Checkmark.SetVisible(false); + } + } + else if (popup.IsAncestor(PlaylistSongDropDown!)) + { + DropDown dropdown = (DropDown)popup.GetAncestor(DropDown.GetGType())!; + PlaylistSelector selectedItem = (PlaylistSelector)dropdown.SelectedItem!; + if (selectedItem.Title == contents.Title.Label_) + { + SelectedSong = selectedItem; + contents.Checkmark.SetVisible(true); + dropdown.OnNotify += Song_Notify; + } + else + { + contents.Checkmark.SetVisible(false); + } + } + } + else + { + contents.Checkmark.SetVisible(false); + } + } + + private void Playlist_Notify(GObject.Object sender, NotifySignalArgs args) + { + var name = args.Pspec.GetName(); + if (args.Pspec.GetName() == "selected") + { + var dropdown = (DropDown)sender; + + PlaylistIsSelected = true; + + if (ContentsPlaylist is not null) + { + if (dropdown.Selected >= ContentsPlaylist.Length) + { + return; + } + } + + if (ContentsPlaylist![PrevSelectedPlaylistIndex].Checkmark is null) + { + ContentsPlaylist![PrevSelectedPlaylistIndex] = new(); + } + if (ContentsPlaylist[dropdown.Selected].Checkmark is null) + { + ContentsPlaylist[dropdown.Selected] = new(); + } + + ContentsPlaylist![PrevSelectedPlaylistIndex].Checkmark.SetVisible(false); + ContentsPlaylist[dropdown.Selected].Checkmark.SetVisible(true); + + PrevSelectedPlaylistIndex = (int)dropdown.Selected; + } + } + + private void Song_Notify(GObject.Object sender, NotifySignalArgs args) + { + var name = args.Pspec.GetName(); + if (args.Pspec.GetName() == "selected") + { + var dropdown = (DropDown)sender; + + SongIsSelected = true; + + if (ContentsSong is not null) + { + if (dropdown.Selected >= ContentsSong.Length) + { + return; + } + } + + if (ContentsSong![PrevSelectedSongIndex].Checkmark is null) + { + ContentsSong![PrevSelectedSongIndex] = new(); + } + if (ContentsSong[dropdown.Selected].Checkmark is null) + { + ContentsSong[dropdown.Selected] = new(); + } + + ContentsSong![PrevSelectedSongIndex].Checkmark.SetVisible(false); + ContentsSong[dropdown.Selected].Checkmark.SetVisible(true); + + PrevSelectedSongIndex = (int)dropdown.Selected; + } + } + + internal void PlaylistStringSelect() + { + AddSongEntries(); + } + + internal Config.Playlist GetPlaylist() + { + Config.Playlist playlist = null!; + foreach (Config.Playlist plist in Playlists!) + { + PlaylistSelector selectedItem = (PlaylistSelector)PlaylistDropDown!.GetSelectedItem()!; + var selectedItemName = selectedItem.Title; + if (plist.Name == selectedItemName) + { + playlist = plist; + } + } + return playlist; + } + + internal string GetTitle() + { + return Title!; + } + + internal uint GetPlaylistSongIndex(int index) + { + var numItems = PlaylistSongModel!.GetNItems(); + var newIndex = PlaylistDropDown!.Selected; + for (int i = 0; i < numItems; i++) + { + if (Songs![i].Index.Equals(index)) + newIndex = (uint)i; + } + return newIndex; + } + + internal int GetSongIndex(uint index) + { + var strObj = (PlaylistSelector)PlaylistSongDropDown!.SelectedItem!; + var selectedItemName = strObj.Title; + var newIndex = (int)index; + foreach (var song in Songs!) + { + if (song.Name.Equals(selectedItemName)) + { + newIndex = song.Index; + } + } + return newIndex; + } + + internal static int GetNumSongs() + { + return (int)PlaylistSongModel!.NItems; + } + + internal void AddPlaylistEntries(List playlists) + { + Playlists = playlists; + if (PlaylistModel!.GetNItems() is not 0) + { + PlaylistModel.RemoveAll(); + } + ContentsPlaylist = new Contents[Playlists.Count + 1]; + int i = 0; + foreach (Config.Playlist plist in Playlists) + { + var data = new PlaylistSelector(plist.Name, "vgms-playlist-symbolic") + { + Index = i++ + }; + PlaylistModel.Append(data); + } + PrevSelectedPlaylistIndex = (int)(PlaylistDropDown!.Selected = SelectedPlaylistIndex = PlaylistDropDown.Selected = PlaylistModel.NItems - 1); // So that "All Songs" main playlist is selected + Index = 0; + } + internal void AddSongEntries() + { + if (PlaylistSongModel!.GetNItems() is not 0) + { + PlaylistSongModel.RemoveAll(); + } + Songs = Playlists![(int)PlaylistDropDown!.Selected].Songs; + ContentsSong = new Contents[Songs.Count + 1]; + for (int i = 0; i < Songs.Count; i++) + { + var data = new PlaylistSelector(Songs[i].Name, "vgms-song-symbolic") + { + Index = i + }; + PlaylistSongModel.Append(data); + } + PrevSelectedSongIndex = (int)(SelectedSongIndex = PlaylistSongDropDown!.Selected); + Index = 0; + } +} \ No newline at end of file diff --git a/VG Music Studio - GTK4/Preferences.cs b/VG Music Studio - GTK4/Preferences.cs new file mode 100644 index 00000000..0a84f504 --- /dev/null +++ b/VG Music Studio - GTK4/Preferences.cs @@ -0,0 +1,174 @@ +using System; +using System.Runtime.InteropServices; +using Adw; +using Kermalis.VGMusicStudio.Core; + +namespace Kermalis.VGMusicStudio.GTK4; + +internal class Preferences : Window +{ + private Mixer.AudioBackend PlaybackBackend; + + private Gtk.CheckButton RadioButtonPortAudio { get; set; } + private Gtk.CheckButton RadioButtonMiniAudio { get; set; } + private Gtk.CheckButton RadioButtonNAudio { get; set; } + + internal Preferences() + { + New(); + + Title = $"Preferences - {MainWindow.GetProgramName()}"; + FocusVisible = true; + FocusOnClick = true; + + var header = HeaderBar.New(); + + var labelAudioBackend = Gtk.Label.New("Audio Backend:"); + + RadioButtonPortAudio = Gtk.CheckButton.New(); + RadioButtonPortAudio.Label = "PortAudio"; + RadioButtonPortAudio.OnNotify += OnNotify_PortAudio; + RadioButtonMiniAudio = Gtk.CheckButton.New(); + RadioButtonMiniAudio.Label = "MiniAudio"; + RadioButtonMiniAudio.OnNotify += OnNotify_MiniAudio; + RadioButtonNAudio = Gtk.CheckButton.New(); + RadioButtonNAudio.Label = "NAudio (Legacy, Deprecated, Windows Only)"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + RadioButtonNAudio.Sensitive = true; + RadioButtonNAudio.OnNotify += OnNotify_NAudio; + } + else + { + RadioButtonNAudio.Sensitive = false; + } + RadioButtonPortAudio.SetGroup(RadioButtonMiniAudio); + RadioButtonPortAudio.SetGroup(RadioButtonNAudio); + + switch (Mixer.PlaybackBackend) + { + case Mixer.AudioBackend.PortAudio: + { + RadioButtonPortAudio.Active = true; + break; + } + case Mixer.AudioBackend.MiniAudio: + { + RadioButtonMiniAudio.Active = true; + break; + } + case Mixer.AudioBackend.NAudio: + { + RadioButtonNAudio.Active = true; + break; + } + } + + var buttonOK = Gtk.Button.New(); + buttonOK.Label = "OK"; + buttonOK.SetValign(Gtk.Align.End); + buttonOK.OnClicked += ButtonOK_Clicked; + var buttonApply = Gtk.Button.New(); + buttonApply.Label = "Accept"; + buttonApply.SetValign(Gtk.Align.End); + buttonApply.OnClicked += ButtonApply_Clicked; + var buttonCancel = Gtk.Button.New(); + buttonCancel.Label = "Cancel"; + buttonCancel.SetValign(Gtk.Align.End); + buttonCancel.OnClicked += ButtonCancel_Clicked; + + var buttonBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 15); + buttonBox.SetHalign(Gtk.Align.Center); + buttonBox.SetBaselinePosition(Gtk.BaselinePosition.Bottom); + buttonBox.SetVexpand(true); + buttonBox.SetMarginBottom(10); + buttonBox.Append(buttonOK); + buttonBox.Append(buttonApply); + buttonBox.Append(buttonCancel); + + var box = Gtk.Box.New(Gtk.Orientation.Vertical, 5); + box.SetVexpand(true); + box.Append(header); + box.Append(labelAudioBackend); + box.Append(RadioButtonPortAudio); + box.Append(RadioButtonMiniAudio); + box.Append(RadioButtonNAudio); + box.Append(buttonBox); + + SetContent(box); + + OnCloseRequest += Preferences_WindowClosed; + } + + private void ButtonOK_Clicked(Gtk.Button sender, EventArgs args) + { + Mixer.PlaybackBackend = PlaybackBackend; + MainWindow.Instance!.ReloadEngine(); + Preferences_WindowClosed(null!, null!); + Close(); + } + private void ButtonApply_Clicked(Gtk.Button sender, EventArgs args) + { + Mixer.PlaybackBackend = PlaybackBackend; + MainWindow.Instance!.ReloadEngine(); + } + private void ButtonCancel_Clicked(Gtk.Button sender, EventArgs args) + { + Preferences_WindowClosed(null!, null!); + Close(); + } + + private bool Preferences_WindowClosed(Gtk.Window sender, EventArgs args) + { + OnCloseRequest -= Preferences_WindowClosed; + RadioButtonPortAudio.OnNotify -= OnNotify_PortAudio; + RadioButtonMiniAudio.OnNotify -= OnNotify_MiniAudio; + RadioButtonNAudio.OnNotify -= OnNotify_NAudio; + MainWindow.Instance!.SetCanTarget(true); + MainWindow.Instance.SetSensitive(true); + Dispose(); + return false; + } + + private void OnNotify_PortAudio(object sender, EventArgs args) + { + if (args is NotifySignalArgs notifyArgs) + { + var name = notifyArgs.Pspec.GetName(); + if (name is "active" && RadioButtonPortAudio.Active is true) + { + PlaybackBackend = Mixer.AudioBackend.PortAudio; + RadioButtonMiniAudio.Active = false; + RadioButtonNAudio.Active = false; + } + } + } + + private void OnNotify_MiniAudio(object sender, EventArgs args) + { + if (args is NotifySignalArgs notifyArgs) + { + var name = notifyArgs.Pspec.GetName(); + if (name is "active" && RadioButtonMiniAudio.Active is true) + { + PlaybackBackend = Mixer.AudioBackend.MiniAudio; + RadioButtonPortAudio.Active = false; + RadioButtonNAudio.Active = false; + } + } + } + + private void OnNotify_NAudio(object sender, EventArgs args) + { + if (args is NotifySignalArgs notifyArgs) + { + var name = notifyArgs.Pspec.GetName(); + if (name is "active" && RadioButtonNAudio.Active is true) + { + PlaybackBackend = Mixer.AudioBackend.NAudio; + RadioButtonPortAudio.Active = false; + RadioButtonMiniAudio.Active = false; + } + } + } +} \ No newline at end of file diff --git a/VG Music Studio - GTK4/Program.cs b/VG Music Studio - GTK4/Program.cs new file mode 100644 index 00000000..c369a7de --- /dev/null +++ b/VG Music Studio - GTK4/Program.cs @@ -0,0 +1,80 @@ +using Adw; +using System; +using System.IO; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Kermalis.VGMusicStudio.GTK4 +{ + internal class Program + { + private static readonly Application _app = Application.New("org.Kermalis.VGMusicStudio.GTK4", Gio.ApplicationFlags.FlagsNone); + private static readonly OSPlatform Linux = OSPlatform.Linux; + private static readonly OSPlatform FreeBSD = OSPlatform.FreeBSD; + private static readonly OSPlatform Windows = OSPlatform.Windows; + + static void OnActivate(Gio.Application sender, EventArgs e) + { + + } + + [STAThread] + public static void Main(string[] args) + { + //if (!RuntimeInformation.IsOSPlatform(Windows)) + //{ + // Environment.SetEnvironmentVariable("Path", ".\\runtimes\\win-x64\\native", EnvironmentVariableTarget.Process); + //} + _app.Register(Gio.Cancellable.GetCurrent()); + + if (!RuntimeInformation.IsOSPlatform(Linux) | !RuntimeInformation.IsOSPlatform(FreeBSD)) + { + if (GLib.Functions.Getenv("GDK_BACKEND") is not "wayland") + { + GLib.Functions.Setenv("GSK_RENDERER", "cairo", false); + } + } + + _app.OnActivate += OnActivate; + + if (File.Exists(Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!) + "/org.Kermalis.VGMusicStudio.GTK4.gresource")) + { + //Load file from program directory, required for `dotnet run` + Gio.Functions.ResourcesRegister(Gio.Functions.ResourceLoad(Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!) + "/org.Kermalis.VGMusicStudio.GTK4.gresource")); + } + else + { + var prefixes = new List { + Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!), + Directory.GetParent(Directory.GetParent(Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!))!.FullName)!.FullName, + Directory.GetParent(Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!))!.FullName, + "/usr" + }; + foreach (var prefix in prefixes) + { + if (File.Exists(prefix + "/share/org.Kermalis.VGMusicStudio.GTK4/org.Kermalis.VGMusicStudio.GTK4.gresource")) + { + Gio.Functions.ResourcesRegister(Gio.Functions.ResourceLoad(Path.GetFullPath(prefix + "/share/org.Kermalis.VGMusicStudio.GTK4/org.Kermalis.VGMusicStudio.GTK4.gresource"))); + break; + } + } + } + + var argv = new string[args.Length + 1]; + argv[0] = "Kermalis.VGMusicStudio.GTK4"; + args.CopyTo(argv, 1); + + // Set an initial? + string initial = ""; + if (args.Length > 0) + initial = args[0].Trim(); + + // Add Main Window + var win = new MainWindow(_app); + _app.AddWindow(win); + win.Present(); + _app.Run(args); + } + } +} diff --git a/VG Music Studio - GTK4/Properties/org.Kermalis.VGMusicStudio.GTK4.gresource.xml b/VG Music Studio - GTK4/Properties/org.Kermalis.VGMusicStudio.GTK4.gresource.xml new file mode 100644 index 00000000..9bb9b38f --- /dev/null +++ b/VG Music Studio - GTK4/Properties/org.Kermalis.VGMusicStudio.GTK4.gresource.xml @@ -0,0 +1,8 @@ + + + + vgms-song-symbolic.svg + vgms-playlist-symbolic.svg + vgms-play-playlist-symbolic.svg + + \ No newline at end of file diff --git a/VG Music Studio - GTK4/Properties/vgms-play-playlist-symbolic.svg b/VG Music Studio - GTK4/Properties/vgms-play-playlist-symbolic.svg new file mode 100644 index 00000000..3aff2eae --- /dev/null +++ b/VG Music Studio - GTK4/Properties/vgms-play-playlist-symbolic.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + diff --git a/VG Music Studio - GTK4/Properties/vgms-playlist-symbolic.svg b/VG Music Studio - GTK4/Properties/vgms-playlist-symbolic.svg new file mode 100644 index 00000000..7c4ebbe1 --- /dev/null +++ b/VG Music Studio - GTK4/Properties/vgms-playlist-symbolic.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + diff --git a/VG Music Studio - GTK4/Properties/vgms-song-symbolic.svg b/VG Music Studio - GTK4/Properties/vgms-song-symbolic.svg new file mode 100644 index 00000000..26bcef3a --- /dev/null +++ b/VG Music Studio - GTK4/Properties/vgms-song-symbolic.svg @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/VG Music Studio - GTK4/SequencedAudio_List.cs b/VG Music Studio - GTK4/SequencedAudio_List.cs new file mode 100644 index 00000000..7c8f0514 --- /dev/null +++ b/VG Music Studio - GTK4/SequencedAudio_List.cs @@ -0,0 +1,486 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using Gtk; +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core; +using static Gtk.SignalListItemFactory; + +namespace Kermalis.VGMusicStudio.GTK4; + +internal class SequencedAudio_List : Viewport +{ + private GObject.Value? Id { get; set; } + private GObject.Value? InternalName { get; set; } + private GObject.Value? PlaylistName { get; set; } + private GObject.Value? SongTableOffset { get; set; } + private GObject.Value? SequenceOffset { get; set; } + + private bool IsSongTable = false; + internal bool IsInitialized = false; + public bool HasSelectedRow = false; + private string? _selectedRowName; + + private EndianBinaryReader? Reader { get; set; } + + private readonly Gio.ListStore Model = Gio.ListStore.New(GetGType()); + private SignalListItemFactory? SeqListItemFactory { get; set; } + private SingleSelection? SelectionModel { get; set; } + // private SortListModel? SortModel { get; set; } + private ColumnViewSorter? ColumnSorter { get; set; } + + private GestureClick? ColumnViewGestureClick { get; set; } + + internal ColumnView? ColumnView { get; set; } + + private ColumnViewColumn _columnName = null!; + private ColumnViewColumn _columnPlist = null!; + private ColumnViewColumn _columnSongTableOffset = null!; + private ColumnViewColumn _columnSequenceOffset = null!; + + public SequencedAudio_List[]? SoundData { get; set; } + + public SequencedAudio_List(int id, string name, string plistname, string songTableOffset, string seqOffset) + : base() + { + Id = new GObject.Value(id); + InternalName = new GObject.Value(name); + PlaylistName = new GObject.Value(plistname); + if (songTableOffset is not null) + { + SongTableOffset = new GObject.Value(songTableOffset); + SequenceOffset = new GObject.Value(seqOffset); + } + } + + public void AddEntries(long numSongs, Config config) + { + if (Model.GetNItems() is not 0) + { + Model.RemoveAll(); + } + + SoundData = new SequencedAudio_List[numSongs]; + var sNames = new string[numSongs]; + for (int i = 0; i < sNames.Length; i++) + { + sNames[i] = ""; + } + if (config.InternalSongNames is not null) + { + foreach (Config.InternalSongName sf in config.InternalSongNames) + { + foreach (Config.Song s in sf.Songs) + { + sNames[s.Index] = s.Name; + } + } + } + + var plistNames = new string[numSongs]; + for (int i = 0; i < plistNames.Length; i++) + { + plistNames[i] = ""; + } + if (config.Playlists is not null) + { + foreach (Config.Playlist p in config.Playlists) + { + foreach (Config.Song s in p.Songs) + { + plistNames[s.Index] = s.Name; + } + } + } + + var sTEntryOffsetString = new string[numSongs]; + var seqOffsetString = new string[numSongs]; + if (config.SongTableOffset is not null) + { + IsSongTable = true; + Reader ??= new EndianBinaryReader(new MemoryStream(config.ROM!)); + for (int i = 0, s = 0; i < SoundData.Length; i++) + { + sTEntryOffsetString[i] = string.Format("0x{0:X}", config.SongTableOffset[s] + (i * 8)); + Reader.Stream.Position = config.SongTableOffset[s] + (i * 8); + var seqOffset = (Reader.ReadUInt32() << 8) >> 8; // To remove the "08" modifier value from the offset (which that value is only for memory usage anyways) + seqOffsetString[i] = string.Format("0x{0:X}", seqOffset); + if (s < config.SongTableOffset.Length - 1) + { + s++; + } + } + } + else + { + IsSongTable = false; + } + for (int i = 0; i < SoundData!.Length; i++) + { + SoundData[i] = new SequencedAudio_List(i, sNames[i], plistNames[i], sTEntryOffsetString[i], seqOffsetString[i]); + } + + foreach (var data in SoundData!) + { + Model.Append(data); + } + } + + internal SequencedAudio_List() + { + var scrolledWindow = ScrolledWindow.New(); + scrolledWindow.SetSizeRequest(600, 200); + scrolledWindow.SetHexpand(true); + + SelectionModel = SingleSelection.New(Model); + SelectionModel.OnNotify += SelectionModel_Notified; + + ColumnViewGestureClick = GestureClick.New(); + ColumnViewGestureClick.Button = 1; + ColumnViewGestureClick.OnPressed += ColumnViewGestureClick_LeftClick; + + ColumnView = ColumnView.New(SelectionModel); + ColumnView.AddCssClass("data-table"); + ColumnView.AddController(ColumnViewGestureClick); + ColumnView.SetShowColumnSeparators(true); + ColumnView.SetShowRowSeparators(true); + ColumnView.SetReorderable(false); + ColumnView.SetHexpand(true); + + // ColumnSorter = (ColumnViewSorter)ColumnView.GetSorter()!; + // ColumnSorter.GetPrimarySortColumn(); + // SortModel = SortListModel.New(Model, ColumnSorter); + + scrolledWindow.SetChild(ColumnView); + + Child = scrolledWindow; + + SetVexpand(true); + SetHexpand(true); + } + + private void ColumnViewGestureClick_LeftClick(GestureClick sender, GestureClick.PressedSignalArgs args) + { + if (SelectionModel?.GetSelectedItem() is SequencedAudio_List list) + { + if (list.Id is not null) + { + if (IsInitialized) + { + MainWindow.Instance!.ChangeIndex(list.Id.GetInt()); + } + } + } + } + + private void SelectionModel_Notified(GObject.Object sender, NotifySignalArgs args) + { + _selectedRowName = args.Pspec.GetName(); + } + + internal void Init() + { + IsInitialized = false; + + // ID Column + SeqListItemFactory = SignalListItemFactory.New(); + SeqListItemFactory.OnSetup += OnSetupIDLabel; + SeqListItemFactory.OnBind += OnBindIDText; + + var idColumn = ColumnViewColumn.New("#", SeqListItemFactory); + idColumn.SetResizable(true); + // NewWithProperties(GetGType(), ["id", "internalName", "playlistName", "offset"], [Id, InternalName, PlaylistName, Offset]); + // var idExpression = Gtk.Internal.PropertyExpression.New(GetGType(), nint.Zero, GLib.Internal.NonNullableUtf8StringOwnedHandle.Create("Id")); + // var idSorter = NumericSorter.New(new PropertyExpression(idExpression)); + // idColumn.SetSorter(idSorter); + ColumnView!.AppendColumn(idColumn); + + // Internal Name Column + SeqListItemFactory = SignalListItemFactory.New(); + SeqListItemFactory.OnSetup += OnSetupNameLabel; + SeqListItemFactory.OnBind += OnBindNameText; + + _columnName = ColumnViewColumn.New("Internal Name", SeqListItemFactory); + _columnName.SetFixedWidth(160); + _columnName.SetExpand(true); + _columnName.SetResizable(true); + // nameColumn.SetSorter(ColumnSorter); + ColumnView.AppendColumn(_columnName); + + IsInitialized = true; + } + + internal void ChangeColumns(bool isSongTable = false) + { + IsSongTable = isSongTable; + if (IsSongTable) + { + _columnName.SetExpand(false); + + // Playlist Name Column + SeqListItemFactory = SignalListItemFactory.New(); + SeqListItemFactory.OnSetup += OnSetupPlistLabel; + SeqListItemFactory.OnBind += OnBindPlistText; + + _columnPlist = ColumnViewColumn.New("Playlist Name", SeqListItemFactory); + _columnPlist.SetFixedWidth(160); + _columnPlist.SetResizable(true); + // plistColumn.SetSorter(ColumnSorter); + ColumnView!.AppendColumn(_columnPlist); + + // Song Table Offset Column + SeqListItemFactory = SignalListItemFactory.New(); + SeqListItemFactory.OnSetup += OnSetupSongTableOffsetLabel; + SeqListItemFactory.OnBind += OnBindSongTableOffsetText; + + _columnSongTableOffset = ColumnViewColumn.New("Song Table Offset", SeqListItemFactory); + _columnSongTableOffset.SetFixedWidth(80); + _columnSongTableOffset.SetResizable(true); + // offsetColumn.SetSorter(ColumnSorter); + ColumnView.AppendColumn(_columnSongTableOffset); + + // Sequence Offset Column + SeqListItemFactory = SignalListItemFactory.New(); + SeqListItemFactory.OnSetup += OnSetupSeqOffsetLabel; + SeqListItemFactory.OnBind += OnBindSeqOffsetText; + + _columnSequenceOffset = ColumnViewColumn.New("Sequence Offset", SeqListItemFactory); + _columnSequenceOffset.SetFixedWidth(80); + _columnSequenceOffset.SetExpand(true); + _columnSequenceOffset.SetResizable(true); + // offsetColumn.SetSorter(ColumnSorter); + ColumnView.AppendColumn(_columnSequenceOffset); + } + else + { + if (_columnPlist is not null) + { + ColumnView!.RemoveColumn(_columnPlist); + } + if (_columnSongTableOffset is not null) + { + ColumnView!.RemoveColumn(_columnSongTableOffset); + } + if (_columnSequenceOffset is not null) + { + ColumnView!.RemoveColumn(_columnSequenceOffset); + } + _columnName.SetExpand(true); + } + ConfigureTimer(); + } + internal void SelectRow(int index) + { + HasSelectedRow = true; + SelectionModel?.SelectItem((uint)index, true); + HasSelectedRow = false; + } + + private void ConfigureTimer() + { + var timer = GLib.Timer.New(); // Creates a new timer variable + var context = GLib.MainContext.GetThreadDefault(); // Reads the main context default thread + var source = GLib.Functions.TimeoutSourceNew(50); // Creates and configures the timeout interval at 50 microseconds, so it updates upon selection + source.SetCallback(ListCallback); // Sets the callback for the timer interval to be used on + var microsec = new CULong(source.Attach(context)); // Configures the microseconds based on attaching the GLib MainContext thread + // timer.Elapsed(ref microsec); // Adds the pointer to the configured microseconds source + GLib.Internal.Timer.Elapsed(timer.Handle, ref microsec); // GLib.Timer.Elapsed was removed in GirCore 0.6.3, so we're using this workaround instead + timer.Start(); // Starts the timer + } + + private bool ListCallback() + { + if (SelectionModel?.GetSelectedItem() is SequencedAudio_List list) + { + if (list.Id is not null) + { + if (IsInitialized) + { + MainWindow.Instance!.CheckIndex(list.Id.GetInt()); + } + } + } + return true; + } + + private static void OnSetupIDLabel(SignalListItemFactory sender, SetupSignalArgs args) + { + if (args.Object is not ListItem listItem) + { + return; + } + + var label = Label.New(null); + label.SetEllipsize(Pango.EllipsizeMode.End); + label.Halign = Align.Center; + listItem.Child = label; + } + + private static void OnSetupNameLabel(SignalListItemFactory sender, SetupSignalArgs args) + { + if (args.Object is not ListItem listItem) + { + return; + } + + var label = Label.New(null); + label.SetEllipsize(Pango.EllipsizeMode.End); + label.Halign = Align.Start; + listItem.Child = label; + } + + private static void OnSetupPlistLabel(SignalListItemFactory sender, SetupSignalArgs args) + { + if (args.Object is not ListItem listItem) + { + return; + } + + var label = Label.New(null); + label.SetEllipsize(Pango.EllipsizeMode.End); + label.Halign = Align.Start; + listItem.Child = label; + } + + private static void OnSetupSongTableOffsetLabel(SignalListItemFactory sender, SetupSignalArgs args) + { + if (args.Object is not ListItem listItem) + { + return; + } + + var label = Label.New(null); + label.SetEllipsize(Pango.EllipsizeMode.End); + label.Halign = Align.Start; + listItem.Child = label; + } + + private static void OnSetupSeqOffsetLabel(SignalListItemFactory sender, SetupSignalArgs args) + { + if (args.Object is not ListItem listItem) + { + return; + } + + var label = Label.New(null); + label.SetEllipsize(Pango.EllipsizeMode.End); + label.Halign = Align.Start; + listItem.Child = label; + } + + private void OnBindIDText(SignalListItemFactory sender, BindSignalArgs args) + { + if (args.Object is not ListItem listItem) + { + return; + } + + if (listItem.Child is not Label label) + { + return; + } + + if (listItem.Item is not SequencedAudio_List userData) + { + return; + } + + if (userData.Id is not null) + { + label.SetText(userData.Id.GetInt().ToString()); + } + } + + private void OnBindNameText(SignalListItemFactory sender, BindSignalArgs args) + { + if (args.Object is not ListItem listItem) + { + return; + } + + if (listItem.Child is not Label label) + { + return; + } + + if (listItem.Item is not SequencedAudio_List userData) + { + return; + } + + if (userData.InternalName is not null && userData.InternalName.GetString != null) + { + label.SetText(userData.InternalName.GetString()!); + } + } + + private void OnBindPlistText(SignalListItemFactory sender, BindSignalArgs args) + { + if (args.Object is not ListItem listItem) + { + return; + } + + if (listItem.Child is not Label label) + { + return; + } + + if (listItem.Item is not SequencedAudio_List userData) + { + return; + } + + if (userData.PlaylistName is not null && userData.PlaylistName.GetString != null) + { + label.SetText(userData.PlaylistName.GetString()!); + } + } + + private void OnBindSongTableOffsetText(SignalListItemFactory sender, BindSignalArgs args) + { + if (args.Object is not ListItem listItem) + { + return; + } + + if (listItem.Child is not Label label) + { + return; + } + + if (listItem.Item is not SequencedAudio_List userData) + { + return; + } + + if (userData.SongTableOffset is not null && userData.SongTableOffset.GetString != null) + { + label.SetText(userData.SongTableOffset.GetString()!); + } + } + + private void OnBindSeqOffsetText(SignalListItemFactory sender, BindSignalArgs args) + { + if (args.Object is not ListItem listItem) + { + return; + } + + if (listItem.Child is not Label label) + { + return; + } + + if (listItem.Item is not SequencedAudio_List userData) + { + return; + } + + if (userData.SequenceOffset is not null && userData.SequenceOffset.GetString != null) + { + label.SetText(userData.SequenceOffset.GetString()!); + } + } +} diff --git a/VG Music Studio - GTK4/SequencedAudio_Piano.cs b/VG Music Studio - GTK4/SequencedAudio_Piano.cs new file mode 100644 index 00000000..8c414491 --- /dev/null +++ b/VG Music Studio - GTK4/SequencedAudio_Piano.cs @@ -0,0 +1,191 @@ +using Gtk; +using Cairo; +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.Util; +using System.Runtime.InteropServices; + +namespace Kermalis.VGMusicStudio.GTK4; + +internal class SequencedAudio_Piano : DrawingArea +{ + private SongState.Track[]? Tracks; + private bool[]? EnabledTracks; + private readonly HSLColor[] Colors; + private double R; + private double G; + private double B; + + internal SequencedAudio_Piano() + { + Colors = new HSLColor[SongState.MAX_TRACKS]; + HeightRequest = 60; + WidthRequest = 600; + SetHexpand(true); + SetVexpand(true); + ConfigureTimer(); + SetDrawFunc(DrawPiano); + } + private void ConfigureTimer() + { + var timer = GLib.Timer.New(); // Creates a new timer variable + var context = GLib.MainContext.GetThreadDefault(); // Reads the main context default thread + var source = GLib.Functions.TimeoutSourceNew(1); // Creates and configures the timeout interval at 1 microsecond, so it updates in real time + source.SetCallback(PianoTimerCallback); // Sets the callback for the timer interval to be used on + var microsec = new CULong(source.Attach(context)); // Configures the microseconds based on attaching the GLib MainContext thread + // timer.Elapsed(ref microsec); // Adds the pointer to the configured microseconds source + GLib.Internal.Timer.Elapsed(timer.Handle, ref microsec); // GLib.Timer.Elapsed was removed in GirCore 0.6.3, so we're using this workaround instead + timer.Start(); // Starts the timer + } + + private void DrawPiano(DrawingArea da, Context cr, int width, int height) + { + cr.Save(); // Save context before drawing + + cr.SetSourceRgba(0, 0, 0, 0.7); // Set piano background color to black with slight transparency + cr.Rectangle(0, 0, width, height); // Draw the rectangle for the background + cr.Fill(); // Fill the rectangle background with the applied colors + + DrawPianoKeys(cr); // Now we draw the piano keys + + cr.Restore(); + } + + private void DrawPianoKeys(Context cr) + { + if (EnabledTracks is not null && Tracks is not null) + { + InitPianoTracks(); + } + + // 75 keys total. 40 white keys consisting of 5 white key sets with 7 keys each + // and 1 white key set of 5, plus 35 darker white keys with 5 sets with 7 keys each. + bool isDarker = false; // Must be set to false, so that the lighter key group is drawn first + int k = 0; // And this key index starting at 0, important for highlighting keys when used + var kGrp = 0; // To count the key groups for adding the text to the white key groups + for (int i = 0; i < 75; kGrp++) // All 75 keys accounted for + { + for (int l = 0; l < 7; l++, i++) // With each key group consisting of 7 keys (except for the last one, which is 5 keys) + { + if (i >= 75) break; // So that if it's more or equal to 75, we'll break out of the loop + DrawPianoKeyWhite(cr, k, kGrp, i * 8, isDarker); // This will draw a white piano key + if ((k % 12) != 4 && (k % 12) != 11) k += 2; // So that way, every key that's not a 4th or 11th key within 12 keys, the key index (k) increments by 2 + else k += 1; // Otherwise if it's a 4th or 11th key in a remainder of 12, it'll increment by 1 + } + if (!isDarker) isDarker = true; // Then we change it to true, once the lighter key group is drawn + else isDarker = false; // Otherwise if the darker key group is drawn, we set this to false + } + + // 53 keys total, 21 gaps (adding up to 74). 11 sets of black keys in twos, 10 sets of black keys in threes, and 1 single black key at the end + kGrp = 2; // The first key group only has two keys, so the index is set to 2 + k = 1; // For the black keys, it needs to start at 1, since the first black key is piano key number 2 + for (int i = 0; i < 74; i++) // We include the gaps in there, so it's 74 total + { + for (int ki = 0; ki < kGrp; i++, ki++) // So that when each key has been made in the group, it will exit the loop and then repeat + { + DrawPianoKeyBlack(cr, k, i * 8); // This will draw a black piano key + if ((k % 12) != 3 && (k % 12) != 10) k += 2; // If it's not a 3rd or 10th key within a set of 12 keys, the key index (k) will increment by 2 + else k += 3; // Otherwise if it's a 3rd or 10th key in a remainder of 12, the key index increments by 3 + } + if (kGrp is 2 && i is not 72) kGrp++; // That way, if the key group is two and the index is not 72, kGrp will increment to 3 + else if (i is 72) kGrp = 1; // If the index is 72, the key group will be reduced to 1 + else kGrp--; // Otherwise if it's neither of the above, the key group will be decremented to 2 + } + } + + private void DrawText(Context cr, int keyIndex, int keyGroup, double pos, float areaWidth, float areaHeight) + { + float smallAdj = -0.5f; // Small workaround to ensure the text remains in center of key + if (keyGroup > 0) smallAdj = 0.5f; // If key group is more than 0, make it 0.5f + cr.SetSourceRgb(0, 0, 0); // Set the font color to black + cr.SelectFontFace("Sans", FontSlant.Normal, FontWeight.Normal); // We're using Sans as the font, with no slant or weight + cr.SetFontSize(areaWidth * 4); // Setting it to be large enough to fit in the keys + cr.MoveTo((pos * areaWidth) + areaWidth + smallAdj, areaHeight - 5); // Move the font to the bottom of the keys + cr.ShowText(ConfigUtils.GetKeyName(keyIndex)); // Set the text so it shows as the actual piano key note + } + private void DrawPianoKeyWhite(Context cr, int keyIndex, int keyGroup, double pos, bool isDarker) + { + var width = GetWidth() / 599f; // Piano key width + var height = (float)GetHeight(); + if (isDarker) R = G = B = 1f / 2f; // If it's the darker key group, make sure all RGB channels are half each + else R = G = B = 1; // Otherwise, set them to 1.0 (maximum value) + cr.Save(); // Save the context before we start drawing + CheckPianoTrack(keyIndex); // Check to see if the piano key is being pressed, and set it to that highlighted color + cr.SetSourceRgb(R, G, B); // Then apply the color values + cr.Rectangle(pos * width, 0, 7 * width, height); // Create the key as a rectangle, positioned right after each one + cr.Fill(); // Fill in the rectangle with the applied color values + if (keyIndex % 12 == 0) DrawText(cr, keyIndex, keyGroup, pos, width, height); + cr.Restore(); // This will restore the context, to prepare for the next drawing + } + + private void DrawPianoKeyBlack(Context cr, int keyIndex, double pos) + { + R = G = B = 0; // All black keys are set to 0.0 (minimum value) + cr.Save(); // Save the context before we start drawing + CheckPianoTrack(keyIndex); // Check to see if the piano key is being pressed, and set it to that highlighted color + cr.SetSourceRgb(R, G, B); // Then apply the color values + cr.Rectangle((pos + 5.1) * (GetWidth() / 599f), 0, 5 * (GetWidth() / 599f), GetHeight() / 1.5); // Create the key as a smaller rectangle + cr.Fill(); // Fill in the rectangle with the applied color values + cr.Restore(); // This will restore the context, to prepare for the next drawing + } + + private void InitPianoTracks() + { + 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; + } + + Colors[i] = new HSLColor(GlobalConfig.Instance.Colors[track.Voice]); + } + } + } + private void CheckPianoTrack(int keyIndex) + { + for (int ti = 0; ti < Colors.Length; ti++) + { + if (Colors[ti].R is not 0f && + Colors[ti].G is not 0f && + Colors[ti].B is not 0f) + { + for (int i = 0; i < Tracks![ti].Keys.Length; i++) + { + if (EnabledTracks![ti]) + { + if (Tracks[ti].Keys[i] != byte.MaxValue) + { + if (Tracks[ti].Keys[i] == keyIndex) + { + R = Colors[ti].R; + G = Colors[ti].G; + B = Colors[ti].B; + } + } + } + } + } + } + } + protected bool PianoTimerCallback() + { + // Redraws the piano on every interval + QueueDraw(); // This function redraws the piano graphics + return true; // Returns the boolean as true, so the callback can start again on next interval + } + + internal void UpdateKeys(SongState.Track[] tracks, bool[] enabledTracks) + { + Tracks = tracks; + EnabledTracks = enabledTracks; + } +} \ No newline at end of file diff --git a/VG Music Studio - GTK4/SequencedAudio_TrackInfo.cs b/VG Music Studio - GTK4/SequencedAudio_TrackInfo.cs new file mode 100644 index 00000000..1a596860 --- /dev/null +++ b/VG Music Studio - GTK4/SequencedAudio_TrackInfo.cs @@ -0,0 +1,666 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Cairo; +using Gtk; +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; + +namespace Kermalis.VGMusicStudio.GTK4; + +internal class SequencedAudio_TrackInfo : Box +{ + private readonly Label? _tempoLabel; + private ushort _baseTempo; + private readonly Label? _baseTempoLabel; + private readonly SpinButton _tempoSpinButton; + + private CheckButton? _trackToggleCheckButtonHeader; + private Label? _velocityHeader; + + private CheckButton[]? _trackToggleCheckButton; + private Label[]? _labelPosition; + private Label[]? _labelRest; + private Label[]? _labelVoice; + private Label[]? _labelNotes; + private Label[]? _labelPanpot; + private Label[]? _labelVolume; + private Label[]? _labelLFO; + private Label[]? _labelPitchBend; + private Label[]? _labelExtra; + private VelocityBar[]? _velocity; + private Label[]? _labelType; + + private readonly ListBox? _listBox; + private static readonly List _keysCache = new(128); + + public readonly bool[]? NumTracks; + public readonly SongState? Info; + internal static int NumTracksToDraw; + + internal SequencedAudio_TrackInfo[]? TrackInfo { get; set; } + + internal SequencedAudio_TrackInfo() + { + NumTracks = new bool[SongState.MAX_TRACKS]; + for (int i = 0; i < SongState.MAX_TRACKS; i++) + { + NumTracks[i] = true; + } + + Info = new SongState(); + + _tempoLabel = Label.New(string.Format("{0} - {1}", Strings.PlayerTempo, Info!.Tempo)); + _tempoSpinButton = SpinButton.New(Adjustment.New(0, 0, 10000, 1, 10, 0), 0, 0); + _tempoSpinButton.SetNumeric(true); + _tempoSpinButton.OnValueChanged += ChangeTempo; + _tempoSpinButton.OnChangeValue += ChangeTempo; + _baseTempo = Info.Tempo; + _baseTempoLabel = Label.New(string.Format("{0} + ", _baseTempo)); + var tempoBox = New(Orientation.Vertical, 4); + var tempoControlBox = New(Orientation.Horizontal, 4); + tempoControlBox.Append(_baseTempoLabel); + tempoControlBox.Append(_tempoSpinButton); + tempoControlBox.SetHalign(Align.Center); + tempoBox.Append(_tempoLabel); + tempoBox.Append(tempoControlBox); + var listHeader = CreateListHeader(); + var viewport = Viewport.New(Adjustment.New(0, double.MinValue, double.MaxValue, 1, 1, 1), Adjustment.New(0, double.MinValue, double.MaxValue, 1, 1, 1)); + var scrolledWindow = ScrolledWindow.New(); + scrolledWindow.SetSizeRequest(700, 150); + scrolledWindow.SetHexpand(true); + scrolledWindow.SetVexpand(true); + + _listBox = ListBox.New(); + _listBox.SetHexpand(true); + _listBox.SetSelectionMode(SelectionMode.None); + + scrolledWindow.SetChild(_listBox); + + viewport.Child = scrolledWindow; + + SetOrientation(Orientation.Vertical); + Append(tempoBox); + Append(listHeader); + Append(viewport); + SetVexpand(true); + SetHexpand(true); + SetNumTracks(0); + ConfigureTimer(); + } + + private void ChangeTempo(SpinButton sender, EventArgs args) + { + if (Engine.Instance is not null) + { + Engine.Instance.Player.Tempo = (ushort)(_baseTempo! + _tempoSpinButton.Value); + } + } + + internal void ResetTempo() + { + _tempoSpinButton.Value = 0; + } + + internal static void SetNumTracks(int num) => NumTracksToDraw = num; + + private void ConfigureTimer() + { + var timer = GLib.Timer.New(); // Creates a new timer variable + var context = GLib.MainContext.GetThreadDefault(); // Reads the main context default thread + var source = GLib.Functions.TimeoutSourceNew(1); // Creates and configures the timeout interval at 1 microsecond, so it updates in real time + source.SetCallback(TrackTimerCallback); // Sets the callback for the timer interval to be used on + var microsec = new CULong(source.Attach(context)); // Configures the microseconds based on attaching the GLib MainContext thread + // timer.Elapsed(ref microsec); // Adds the pointer to the configured microseconds source + GLib.Internal.Timer.Elapsed(timer.Handle, ref microsec); // GLib.Timer.Elapsed was removed in GirCore 0.6.3, so we're using this workaround instead + timer.Start(); // Starts the timer + } + + private bool TrackTimerCallback() + { + if (Engine.Instance is not null) + { + _tempoLabel!.SetLabel(string.Format("{0} - {1}", Strings.PlayerTempo, Engine.Instance!.Player.Tempo)); + } + if (_trackToggleCheckButton is not null && + _labelPosition is not null && + _labelRest is not null && + _labelVoice is not null && + _labelNotes is not null && + _labelPanpot is not null && + _labelVolume is not null && + _labelLFO is not null && + _labelPitchBend is not null && + _labelExtra is not null && + _velocity is not null && + _labelType is not null) + { + if (_labelPosition.Length == 0) + { + return true; + } + + for (int i = 0; i < NumTracksToDraw; i++) + { + if (_trackToggleCheckButton[i] is not null && + _labelPosition[i] is not null && + _labelRest[i] is not null && + _labelVoice[i] is not null && + _labelNotes[i] is not null && + _labelPanpot[i] is not null && + _labelVolume[i] is not null && + _labelLFO[i] is not null && + _labelPitchBend[i] is not null && + _labelExtra[i] is not null && + _velocity[i] is not null && + _labelType[i] is not null) + { + if (Engine.Instance!.Player.State is not PlayerState.Stopped) + { + ToggleTrack(i, _trackToggleCheckButton[i].Active); + _labelPosition[i].SetText(string.Format("0x{0:X}", Info!.Tracks[i].Position)); + _labelRest[i].SetText(Info.Tracks[i].Rest.ToString()); + _labelVoice[i].SetText(Info.Tracks[i].Voice.ToString()); + _labelNotes[i].SetText(GetNote(Info.Tracks[i])); + _labelPanpot[i].SetText(Info.Tracks[i].Panpot.ToString()); + _labelVolume[i].SetText(Info.Tracks[i].Volume.ToString()); + _labelLFO[i].SetText(Info.Tracks[i].LFO.ToString()); + _labelPitchBend[i].SetText(Info.Tracks[i].PitchBend.ToString()); + _labelExtra[i].SetText(Info.Tracks[i].Extra.ToString()); + _velocityHeader!.WidthRequest = GetWidth() / 4; + _velocity[i].WidthRequest = GetWidth() / 4; + _velocity[i].UpdateColor(Info.Tracks[i], _trackToggleCheckButton[i].Active); + _velocity[i].QueueDraw(); + if (Info.Tracks[i].Type is not null) + { + _labelType[i].SetText(Info.Tracks[i].Type); + } + } + else + { + ToggleTrack(i, _trackToggleCheckButton[i].Active); + _labelPosition[i].SetText(string.Format("0x{0:X}", 0)); + _labelRest[i].SetText(0.ToString()); + _labelVoice[i].SetText(0.ToString()); + _labelNotes[i].SetText(""); + _labelPanpot[i].SetText(0.ToString()); + _labelVolume[i].SetText(0.ToString()); + _labelLFO[i].SetText(0.ToString()); + _labelPitchBend[i].SetText(0.ToString()); + _labelExtra[i].SetText(Info!.Tracks[i].Extra.ToString()); + _velocityHeader!.WidthRequest = GetWidth() / 4; + _velocity[i].WidthRequest = GetWidth() / 4; + _velocity[i].UpdateColor(Info.Tracks[i], _trackToggleCheckButton[i].Active); + _velocity[i].QueueDraw(); + if (Info.Tracks[i].Type is not null) + { + _labelType[i].SetText(""); + } + } + } + } + } + return true; + } + + private class VelocityBar : DrawingArea + { + private SongState.Track? _track; + private HSLColor _color; + private HSLColor _overampColor; + internal VelocityBar() + { + _color = new HSLColor(); + _overampColor = new HSLColor(0, 1, 0.5); + SetHexpand(true); + SetVexpand(true); + SetDrawFunc(DrawVelocityBar); + } + + internal void UpdateColor(SongState.Track track, bool trackEnabled) + { + _track = track; + if (GlobalConfig.Instance is not null) // Nullability check + { + _color = new HSLColor(GlobalConfig.Instance.Colors[track.Voice]); + if (!trackEnabled) + { + _color = new HSLColor(_color.Hue, 0, _color.Lightness); + } + } + _overampColor = new HSLColor(0, 1, 0.5); + if (!trackEnabled) + { + _overampColor = new HSLColor(_overampColor.Hue, 0, 0.8); + } + } + + private void DrawVelocityBar(DrawingArea drawingArea, Context cr, int width, int height) + { + // cr.LineWidth = 3; + + DrawLineL(cr, width, height); + DrawLineR(cr, width, height); + + cr.Save(); + + cr.SetSourceRgb(0.5, 0.5, 0.5); + + cr.Rectangle(width / 2.0, 0, 5 * (width / 599f), height); + cr.Fill(); + cr.Restore(); + // DrawRounded(cr, (width / 2.0) - 20, 150); + + cr.Restore(); + } + + private void DrawLineL(Context cr, int width, int height) + { + DrawTrough(cr, height, width / 2, -(width / 3)); + if (_track is not null) + { + DrawVolumeLine(cr, height, width / 2, -(_track.LeftVolume * ((width / 3) + (width / 9)))); + } + DrawText(cr, height, (width / 2) - (width / 3) - 8, (width / 2) - (width / 3) - (width / 9) - 7.5, "-1.0", "L"); + DrawOverampLine(cr, height, (width / 2) - (width / 3), -(width / 9)); + } + + private void DrawLineR(Context cr, int width, int height) + { + DrawTrough(cr, height, (width / 2) + (5 * (width / 599f)), width / 3); + if (_track is not null) + { + DrawVolumeLine(cr, height, (width / 2) + (5 * (width / 599f)), _track.LeftVolume * ((width / 3) + (width / 9))); + } + DrawText(cr, height, (width / 2) + (5 * (width / 599f)) + (width / 3) - 8, width / 1.045, "+1.0", "R"); + DrawOverampLine(cr, height, (width / 2) + (5 * (width / 599f)) + (width / 3), width / 9); + } + + private void DrawTrough(Context cr, int height, float pos, double length) + { + cr.Save(); + cr.SetSourceRgba(0.5, 0.5, 0.5, 0.5); + cr.LineWidth = 5; + cr.MoveTo(pos, height / 2); + cr.LineTo(pos + length, height / 2); + cr.Stroke(); + cr.Restore(); + } + + private void DrawVolumeLine(Context cr, int height, float pos, float length) + { + cr.Save(); + cr.SetSourceRgb(_color.R, _color.G, _color.B); + cr.LineWidth = 5; + cr.MoveTo(pos, height / 2); + cr.LineTo(pos + length, height / 2); + cr.Stroke(); + cr.Restore(); + } + + private void DrawText(Context cr, double height, double posVolLabel, double posChLabel, string volumeLabel, string channelLabel) + { + cr.Save(); + cr.SetSourceRgb(0.5, 0.5, 0.5); + cr.SelectFontFace("Sans", FontSlant.Normal, FontWeight.Normal); + cr.SetFontSize(7.0); + cr.MoveTo(posVolLabel, height); + cr.ShowText(volumeLabel); + cr.Restore(); + + cr.Save(); + cr.SetSourceRgb(0.5, 0.5, 0.5); + cr.SelectFontFace("Sans", FontSlant.Normal, FontWeight.Normal); + cr.SetFontSize(8.0); + cr.MoveTo(posChLabel, height / 1.5); + cr.ShowText(channelLabel); + cr.Restore(); + } + + private void DrawOverampLine(Context cr, int height, float pos, int length) + { + cr.Save(); + cr.SetSourceRgba(_overampColor.R, _overampColor.G, _overampColor.B, 0.5); + cr.LineWidth = 5; + cr.MoveTo(pos, height / 2); + cr.LineTo(pos + length, height / 2); + cr.Stroke(); + cr.ClosePath(); + cr.Restore(); + } + } + + private static string GetNote(SongState.Track track) + { + string key = ""; + if (track.Keys[0] == byte.MaxValue) + { + if (track.PreviousKeysTime != 0) + { + track.PreviousKeysTime--; + key = track.PreviousKeys; + } + else + { + key = string.Empty; + } + } + else // Keys are held down + { + _keysCache.Clear(); + string noteName = ""; + for (int nk = 0; nk < SongState.MAX_KEYS; nk++) + { + byte k = track.Keys[nk]; + if (k == byte.MaxValue) + { + break; + } + + noteName = ConfigUtils.GetKeyName(k); + if (nk != 0) + { + _keysCache.Add(' ' + noteName); + } + else + { + _keysCache.Add(noteName); + } + } + foreach (var k in _keysCache) + { + if (k == noteName) + { + key = k; + } + } + + track.PreviousKeysTime = 120; + track.PreviousKeys = key; + } + return key; + } + private Box CreateListHeader() + { + var columns = New(Orientation.Horizontal, 4); + columns.SetHexpand(true); + + _trackToggleCheckButtonHeader = CheckButton.New(); + _trackToggleCheckButtonHeader.Active = true; + _trackToggleCheckButtonHeader.OnToggled += ToggleAllTracks; + _trackToggleCheckButtonHeader.SetMarginStart(2); // So that the starting margin is aligned with the check buttons in the list box + _trackToggleCheckButtonHeader.SetHalign(Align.Start); + columns.Append(_trackToggleCheckButtonHeader); + + var positionLabelHeader = Label.New(Strings.PlayerPosition); + positionLabelHeader.SetMaxWidthChars(1); + positionLabelHeader.SetEllipsize(Pango.EllipsizeMode.End); + positionLabelHeader.SetHalign(Align.Start); + positionLabelHeader.WidthRequest = 30; + positionLabelHeader.SetHexpand(true); + columns.Append(positionLabelHeader); + + var restLabelHeader = Label.New(Strings.PlayerRest); + restLabelHeader.SetMaxWidthChars(1); + restLabelHeader.SetEllipsize(Pango.EllipsizeMode.End); + restLabelHeader.WidthRequest = 30; + restLabelHeader.SetHexpand(true); + columns.Append(restLabelHeader); + + var voiceLabelHeader = Label.New("Voice"); + voiceLabelHeader.SetMaxWidthChars(1); + voiceLabelHeader.SetEllipsize(Pango.EllipsizeMode.End); + voiceLabelHeader.WidthRequest = 30; + voiceLabelHeader.SetHexpand(true); + columns.Append(voiceLabelHeader); + + var notesLabelHeader = Label.New(Strings.PlayerNotes); + notesLabelHeader.SetMaxWidthChars(1); + notesLabelHeader.SetEllipsize(Pango.EllipsizeMode.End); + notesLabelHeader.WidthRequest = 30; + notesLabelHeader.SetHexpand(true); + columns.Append(notesLabelHeader); + + var panpotLabelHeader = Label.New("Panpot"); + panpotLabelHeader.SetMaxWidthChars(1); + panpotLabelHeader.SetEllipsize(Pango.EllipsizeMode.End); + panpotLabelHeader.WidthRequest = 30; + panpotLabelHeader.SetHexpand(true); + columns.Append(panpotLabelHeader); + + var volumeLabelHeader = Label.New("Volume"); + volumeLabelHeader.SetMaxWidthChars(1); + volumeLabelHeader.SetEllipsize(Pango.EllipsizeMode.End); + volumeLabelHeader.WidthRequest = 30; + volumeLabelHeader.SetHexpand(true); + columns.Append(volumeLabelHeader); + + var lfoLabelHeader = Label.New("LFO"); + lfoLabelHeader.SetMaxWidthChars(1); + lfoLabelHeader.SetEllipsize(Pango.EllipsizeMode.End); + lfoLabelHeader.WidthRequest = 30; + lfoLabelHeader.SetHexpand(true); + columns.Append(lfoLabelHeader); + + var pitchBendLabelHeader = Label.New("Pitch Bend"); + pitchBendLabelHeader.SetMaxWidthChars(1); + pitchBendLabelHeader.SetEllipsize(Pango.EllipsizeMode.End); + pitchBendLabelHeader.WidthRequest = 30; + pitchBendLabelHeader.SetHexpand(true); + columns.Append(pitchBendLabelHeader); + + var extraLabelHeader = Label.New("Extra"); + extraLabelHeader.SetMaxWidthChars(1); + extraLabelHeader.SetEllipsize(Pango.EllipsizeMode.End); + extraLabelHeader.WidthRequest = 30; + extraLabelHeader.SetHexpand(true); + columns.Append(extraLabelHeader); + + _velocityHeader = Label.New(""); + _velocityHeader.SetMaxWidthChars(1); + _velocityHeader.WidthRequest = GetWidth() / 4; + _velocityHeader.SetHexpand(true); + columns.Append(_velocityHeader); + + var typeLabelHeader = Label.New(Strings.PlayerType); + typeLabelHeader.SetMaxWidthChars(1); + typeLabelHeader.SetEllipsize(Pango.EllipsizeMode.End); + typeLabelHeader.WidthRequest = 30; + typeLabelHeader.SetHexpand(true); + columns.Append(typeLabelHeader); + return columns; + } + + private void ToggleTrack(int index, bool active) + { + if (active) + { + Engine.Instance!.Mixer!.Mutes[index] = false; + NumTracks![index] = true; + } + else + { + Engine.Instance!.Mixer!.Mutes[index] = true; + NumTracks![index] = false; + } + + var numActive = 0; + for (int i = 0; i < NumTracksToDraw; i++) + { + if (NumTracks[i]) + { + numActive++; + } + } + if (numActive == NumTracksToDraw) + { + _trackToggleCheckButtonHeader!.Inconsistent = false; + _trackToggleCheckButtonHeader.Active = true; + } + else if (numActive < NumTracksToDraw && numActive is not 0) + { + _trackToggleCheckButtonHeader!.Inconsistent = true; + } + else + { + _trackToggleCheckButtonHeader!.Inconsistent = false; + _trackToggleCheckButtonHeader.Active = false; + } + } + + private void ToggleAllTracks(CheckButton sender, EventArgs args) + { + if (sender.Active) + { + for (int i = 0; i < NumTracksToDraw; i++) + { + Engine.Instance!.Mixer!.Mutes[i] = false; + NumTracks![i] = _trackToggleCheckButton![i].Active = true; + } + } + else + { + for (int i = 0; i < NumTracksToDraw; i++) + { + Engine.Instance!.Mixer!.Mutes[i] = true; + NumTracks![i] = _trackToggleCheckButton![i].Active = false; + } + } + } + + public void AddTrackInfo() + { + _listBox!.RemoveAll(); + for (int i = 0; i < NumTracks!.Length; i++) + { + if (i < NumTracksToDraw) + { + NumTracks[i] = true; + } + else + { + NumTracks[i] = false; + } + } + + _tempoSpinButton.Value = 0; + _baseTempo = Engine.Instance!.Player.Tempo; + _baseTempoLabel!.SetLabel(string.Format("{0} + ", _baseTempo)); + _tempoSpinButton.SetRange(-_baseTempo, short.MaxValue); + + _trackToggleCheckButton = new CheckButton[NumTracksToDraw]; + _labelPosition = new Label[NumTracksToDraw]; + _labelRest = new Label[NumTracksToDraw]; + _labelVoice = new Label[NumTracksToDraw]; + _labelNotes = new Label[NumTracksToDraw]; + _labelPanpot = new Label[NumTracksToDraw]; + _labelVolume = new Label[NumTracksToDraw]; + _labelLFO = new Label[NumTracksToDraw]; + _labelPitchBend = new Label[NumTracksToDraw]; + _labelExtra = new Label[NumTracksToDraw]; + _velocity = new VelocityBar[NumTracksToDraw]; + _labelType = new Label[NumTracksToDraw]; + for (int i = 0; i < NumTracksToDraw; i++) + { + var columns = New(Orientation.Horizontal, 4); + columns.SetHexpand(true); + + _trackToggleCheckButton[i] = CheckButton.New(); + _trackToggleCheckButton[i].Active = true; + _trackToggleCheckButton[i].SetHalign(Align.Start); + columns.Append(_trackToggleCheckButton[i]); + + _labelPosition[i] = Label.New(string.Format("0x{0:X}", Info!.Tracks[i].Position)); + _labelPosition[i].SetEllipsize(Pango.EllipsizeMode.Start); + _labelPosition[i].SetMaxWidthChars(1); + _labelPosition[i].SetHalign(Align.Start); + _labelPosition[i].WidthRequest = 30; + _labelPosition[i].SetHexpand(true); + columns.Append(_labelPosition[i]); + + _labelRest[i] = Label.New(Info.Tracks[i].Rest.ToString()); + _labelRest[i].SetEllipsize(Pango.EllipsizeMode.End); + _labelRest[i].SetMaxWidthChars(1); + _labelRest[i].WidthRequest = 30; + _labelRest[i].SetHexpand(true); + columns.Append(_labelRest[i]); + + _labelVoice[i] = Label.New(Info.Tracks[i].Voice.ToString()); + _labelVoice[i].SetEllipsize(Pango.EllipsizeMode.End); + _labelVoice[i].SetMaxWidthChars(1); + _labelVoice[i].WidthRequest = 30; + _labelVoice[i].SetHexpand(true); + columns.Append(_labelVoice[i]); + + _labelNotes[i] = Label.New(""); + _labelNotes[i].SetEllipsize(Pango.EllipsizeMode.End); + _labelNotes[i].SetMaxWidthChars(1); + _labelNotes[i].WidthRequest = 30; + _labelNotes[i].SetHexpand(true); + columns.Append(_labelNotes[i]); + + _labelPanpot[i] = Label.New(Info.Tracks[i].Panpot.ToString()); + _labelPanpot[i].SetEllipsize(Pango.EllipsizeMode.End); + _labelPanpot[i].SetMaxWidthChars(1); + _labelPanpot[i].WidthRequest = 30; + _labelPanpot[i].SetHexpand(true); + columns.Append(_labelPanpot[i]); + + _labelVolume[i] = Label.New(Info.Tracks[i].Volume.ToString()); + _labelVolume[i].SetEllipsize(Pango.EllipsizeMode.End); + _labelVolume[i].SetMaxWidthChars(1); + _labelVolume[i].WidthRequest = 30; + _labelVolume[i].SetHexpand(true); + columns.Append(_labelVolume[i]); + + _labelLFO[i] = Label.New(Info.Tracks[i].LFO.ToString()); + _labelLFO[i].SetEllipsize(Pango.EllipsizeMode.End); + _labelLFO[i].SetMaxWidthChars(1); + _labelLFO[i].WidthRequest = 30; + _labelLFO[i].SetHexpand(true); + columns.Append(_labelLFO[i]); + + _labelPitchBend[i] = Label.New(Info.Tracks[i].PitchBend.ToString()); + _labelPitchBend[i].SetEllipsize(Pango.EllipsizeMode.End); + _labelPitchBend[i].SetMaxWidthChars(1); + _labelPitchBend[i].WidthRequest = 30; + _labelPitchBend[i].SetHexpand(true); + columns.Append(_labelPitchBend[i]); + + _labelExtra[i] = Label.New(Info.Tracks[i].Extra.ToString()); + _labelExtra[i].SetEllipsize(Pango.EllipsizeMode.End); + _labelExtra[i].SetMaxWidthChars(1); + _labelExtra[i].WidthRequest = 30; + _labelExtra[i].SetHexpand(true); + columns.Append(_labelExtra[i]); + + _velocity[i] = new VelocityBar(); + _velocity[i].SetHalign(Align.Center); + _velocity[i].WidthRequest = GetWidth() / 4; + _velocity[i].SetHexpand(true); + columns.Append(_velocity[i]); + + _labelType[i] = Label.New(""); + _labelType[i].SetEllipsize(Pango.EllipsizeMode.Middle); + _labelType[i].SetMaxWidthChars(1); + _labelType[i].WidthRequest = 30; + _labelType[i].SetHexpand(true); + columns.Append(_labelType[i]); + + _listBox.Append(columns); + } + } + + internal void ResetMutes() + { + for (int i = 0; i < SongState.MAX_TRACKS; i++) + { + NumTracks![i] = true; + if (_trackToggleCheckButton is not null && i < _trackToggleCheckButton.Length) + { + _trackToggleCheckButton[i].Active = true; + } + } + } +} \ No newline at end of file diff --git a/VG Music Studio - GTK4/SoundBankEditor.cs b/VG Music Studio - GTK4/SoundBankEditor.cs new file mode 100644 index 00000000..601f2560 --- /dev/null +++ b/VG Music Studio - GTK4/SoundBankEditor.cs @@ -0,0 +1,453 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using Kermalis.VGMusicStudio.GTK4.Util; + +namespace Kermalis.VGMusicStudio.GTK4; + +internal sealed class SoundBankEditor : Adw.Window +{ + private IVoiceInfo? _voiceInfo; + + private SoundBank? _bank; + private readonly Gio.ListStore _voicesModel = Gio.ListStore.New(GetGType()); + private Gtk.SingleSelection? _singleSelectionModel; + private Gtk.ColumnView? _voicesColumnView; + internal List? VoicesData { get; set; } = []; + + private int _voicesClickSelectionIndex = 0; + + private readonly Gtk.Box? _voicesEditorBox; + private Gtk.Box[]? _voicesEditorParamBox; + private Gtk.SpinButton[]? _voiceParamValue; + private Gtk.Label[]? _voiceParamLabel; + private OffsetEntry? _voiceParamOffset; + private Gtk.Button? _buttonSubVoicesOffset; + + private SoundBankEditor(IVoiceInfo voiceInfo) + : base() + { + _voiceInfo = voiceInfo; + } + internal SoundBankEditor() + { + New(); + + _voicesEditorBox = Gtk.Box.New(Gtk.Orientation.Vertical, 6); + var viewportBox = Gtk.Box.New(Gtk.Orientation.Vertical, 6); + var contentBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 6); + var mainBox = Gtk.Box.New(Gtk.Orientation.Vertical, 6); + + var header = Adw.HeaderBar.New(); + header.SetShowEndTitleButtons(true); + header.SetShowStartTitleButtons(true); + + var viewport = Gtk.Viewport.New(Gtk.Adjustment.New(0, double.MinValue, double.MaxValue, 1, 1, 1), Gtk.Adjustment.New(0, double.MinValue, double.MaxValue, 1, 1, 1)); + var scrolledWindow = Gtk.ScrolledWindow.New(); + scrolledWindow.SetSizeRequest(300, 200); + scrolledWindow.SetHexpand(true); + scrolledWindow.SetVexpand(true); + + _singleSelectionModel = Gtk.SingleSelection.New(_voicesModel); + + _voicesColumnView = Gtk.ColumnView.New(_singleSelectionModel); + _voicesColumnView.AddCssClass("data-table"); + _voicesColumnView.SetShowColumnSeparators(true); + _voicesColumnView.SetShowRowSeparators(true); + _voicesColumnView.SetReorderable(false); + _voicesColumnView.SetHexpand(true); + + scrolledWindow.SetChild(_voicesColumnView); + + viewport.Child = scrolledWindow; + + var voicesFrame = Gtk.Frame.New("Voices"); + voicesFrame.Child = viewport; + voicesFrame.SetMarginStart(10); + voicesFrame.SetMarginEnd(10); + voicesFrame.SetMarginTop(10); + voicesFrame.SetMarginBottom(10); + + var voicesArgsFrame = Gtk.Frame.New("Voices"); + voicesArgsFrame.Child = _voicesEditorBox; + voicesArgsFrame.SetMarginStart(10); + voicesArgsFrame.SetMarginEnd(10); + voicesArgsFrame.SetMarginTop(10); + voicesArgsFrame.SetMarginBottom(10); + + viewportBox.Append(voicesFrame); + + contentBox.Append(viewportBox); + contentBox.Append(voicesArgsFrame); + + mainBox.Append(header); + mainBox.Append(contentBox); + + SetVexpand(true); + SetHexpand(true); + + SetContent(mainBox); + } + + internal void Init() + { + SetupColumns(); + ConfigureTimer(); + } + + private void SetupColumns() + { + // Rows + var listItemFactory = Gtk.SignalListItemFactory.New(); + listItemFactory.OnSetup += OnSetupRow; + listItemFactory.OnBind += OnBindRow; + + _voicesColumnView!.SetRowFactory(listItemFactory); + + // Index Column + listItemFactory = Gtk.SignalListItemFactory.New(); + listItemFactory.OnSetup += OnSetupLabel; + listItemFactory.OnBind += OnBindIndexText; + + var indexColumn = Gtk.ColumnViewColumn.New("#", listItemFactory); + indexColumn.SetFixedWidth(50); + indexColumn.SetResizable(true); + _voicesColumnView.AppendColumn(indexColumn); + + // Voice Type Column + listItemFactory = Gtk.SignalListItemFactory.New(); + listItemFactory.OnSetup += OnSetupLabel; + listItemFactory.OnBind += OnBindEventTypeText; + + var voiceTypeColumn = Gtk.ColumnViewColumn.New(Strings.PlayerType, listItemFactory); + voiceTypeColumn.SetFixedWidth(150); + voiceTypeColumn.SetResizable(true); + _voicesColumnView.AppendColumn(voiceTypeColumn); + + // Offset Column + listItemFactory = Gtk.SignalListItemFactory.New(); + listItemFactory.OnSetup += OnSetupLabel; + listItemFactory.OnBind += OnBindOffsetText; + + var offsetColumn = Gtk.ColumnViewColumn.New(Strings.TrackEditorOffset, listItemFactory); + offsetColumn.SetFixedWidth(150); + offsetColumn.SetExpand(true); + offsetColumn.SetResizable(true); + _voicesColumnView!.AppendColumn(offsetColumn); + } + + private void ConfigureTimer() + { + var timer = GLib.Timer.New(); // Creates a new timer variable + var context = GLib.MainContext.GetThreadDefault(); // Reads the main context default thread + var source = GLib.Functions.TimeoutSourceNew(50); // Creates and configures the timeout interval at 50 microseconds, so it updates upon selection + source.SetCallback(EventsCallback); // Sets the callback for the timer interval to be used on + var microsec = new CULong(source.Attach(context)); // Configures the microseconds based on attaching the GLib MainContext thread + GLib.Internal.Timer.Elapsed(timer.Handle, ref microsec); // GLib.Timer.Elapsed was removed in GirCore 0.6.3, so we're using this workaround instead + timer.Start(); // Starts the timer + } + + private bool EventsCallback() + { + if (_singleSelectionModel?.GetSelectedItem() is SoundBankEditor rowVoices && _voicesClickSelectionIndex != (int)_singleSelectionModel.Selected) + { + if (rowVoices._voiceInfo is not null) + { + _voicesClickSelectionIndex = (int)_singleSelectionModel.Selected; + UpdateParamBoxes(); + } + } + return true; + } + + private void ArgumentChanged(Gtk.SpinButton sender, EventArgs args) + { + if (_bank!.HasADSR((int)_singleSelectionModel.Selected!)) + { + if (_voiceParamOffset is not null) + { + _bank.SetAddressPointer((int)_singleSelectionModel.Selected!, (int)BinaryPrimitives.ReadInt32LittleEndian(Convert.FromHexString(_voiceParamOffset.Entry.Text_))); + } + if (_voiceParamValue is not null) + { + byte a = (byte)_voiceParamValue[0].Value; + byte d = (byte)_voiceParamValue[1].Value; + byte s = (byte)_voiceParamValue[2].Value; + byte r = (byte)_voiceParamValue[3].Value; + _bank.SetADSRValues((int)_singleSelectionModel.Selected!, in a, in d, in s, in r); + } + } + } + private void UpdateParamBoxes() + { + if (_voicesEditorParamBox is not null) + { + for (int i = 0; i < _voicesEditorParamBox.Length; i++) + { + if (_voicesEditorParamBox[i].GetFirstChild() is not null) + { + while (_voicesEditorParamBox[i].GetFirstChild() is not null) + { + _voicesEditorParamBox[i].Remove(_voicesEditorParamBox[i].GetFirstChild()!); + } + if (_voiceParamLabel is not null && _voiceParamLabel[i] is not null) + { + _voiceParamLabel[i].Dispose(); + _voiceParamLabel[i] = null!; + } + if (_voiceParamValue is not null && _voiceParamValue[i] is not null) + { + _voiceParamValue[i].OnValueChanged -= ArgumentChanged; + _voiceParamValue[i].Dispose(); + _voiceParamValue[i] = null!; + } + if (_voiceParamOffset is not null) + { + _voiceParamOffset.Dispose(); + _voiceParamOffset = null; + } + if (_buttonSubVoicesOffset is not null) + { + _buttonSubVoicesOffset.OnClicked -= OpenSoundBankEditor; + _buttonSubVoicesOffset.Dispose(); + _buttonSubVoicesOffset = null; + } + } + } + while (true) + { + if (_voicesEditorBox.GetFirstChild() is not null) + { + _voicesEditorBox!.Remove(_voicesEditorBox.GetFirstChild()); + } + else + { + break; + } + } + } + + #region Addresses (PCM8, Key Split, Drum, PCM4) + + if (_bank.IsValidVoiceAddress((int)_singleSelectionModel.Selected)) + { + _voicesEditorParamBox = new Gtk.Box[1]; + _voicesEditorParamBox[0] = Gtk.Box.New(Gtk.Orientation.Vertical, 3); + _voicesEditorParamBox[0].SetMarginStart(5); + _voicesEditorParamBox[0].SetMarginEnd(5); + _voicesEditorParamBox[0].SetMarginTop(5); + _voicesEditorParamBox[0].SetMarginBottom(5); + _voiceParamLabel = new Gtk.Label[1]; + if (_bank.IsTableAddress((int)_singleSelectionModel.Selected)) + { + _voiceParamLabel[0] = Gtk.Label.New("Table Offset"); + _buttonSubVoicesOffset = Gtk.Button.New(); + _buttonSubVoicesOffset.Label = "Open Voice Table"; + _buttonSubVoicesOffset.OnClicked += OpenSoundBankEditor; + } + else + { + _voiceParamLabel[0] = Gtk.Label.New("Sample Offset"); + } + _voiceParamOffset = new(_bank.GetVoiceAddress((int)_singleSelectionModel.Selected)); + _voicesEditorParamBox[0].Append(_voiceParamLabel[0]); + _voicesEditorParamBox[0].Append(_voiceParamOffset); + if (_bank.IsTableAddress((int)_singleSelectionModel.Selected)) + { + _voicesEditorParamBox[0].Append(_buttonSubVoicesOffset!); + } + _voicesEditorBox.Append(_voicesEditorParamBox[0]); + } + + #endregion + + #region ADSR (everything except Key Split, Drum and invalids) + + if (_bank.IsValidADSR((int)_singleSelectionModel.Selected)) + { + bool isPCM8 = !_bank.IsPSGInstrument((int)_singleSelectionModel.Selected); + _voiceParamLabel = new Gtk.Label[4]; + _voiceParamValue = new Gtk.SpinButton[4]; + _voicesEditorParamBox = new Gtk.Box[4]; + for (int i = 0; i < _voiceParamValue.Length; i++) + { + _voicesEditorParamBox[i] = Gtk.Box.New(Gtk.Orientation.Vertical, 3); + _voicesEditorParamBox[i].SetMarginStart(5); + _voicesEditorParamBox[i].SetMarginEnd(5); + _voicesEditorParamBox[i].SetMarginTop(5); + _voicesEditorParamBox[i].SetMarginBottom(5); + + _voiceParamValue[i] = Gtk.SpinButton.New(Gtk.Adjustment.New(0, 0, 100, 1, 1, 1), 1, 0); + _voiceParamValue[i].SetNumeric(true); + } + _voiceParamValue[0].Adjustment.Upper = _voiceParamValue[1].Adjustment.Upper = _voiceParamValue[3].Adjustment.Upper = isPCM8 ? byte.MaxValue : 0x7; + _voiceParamValue[2].Adjustment.Upper = isPCM8 ? byte.MaxValue : 0xF; + _voiceParamValue[0].Adjustment.Lower = _voiceParamValue[1].Adjustment.Lower = _voiceParamValue[2].Adjustment.Lower = _voiceParamValue[3].Adjustment.Lower = byte.MinValue; + _bank.GetADSRValues((int)_singleSelectionModel.Selected, out byte a, out byte d, out byte s, out byte r); + _voiceParamValue[0].Value = a; + _voiceParamValue[1].Value = d; + _voiceParamValue[2].Value = s; + _voiceParamValue[3].Value = r; + + _voiceParamLabel[0] = Gtk.Label.New("Attack"); + _voiceParamLabel[1] = Gtk.Label.New("Decay"); + _voiceParamLabel[2] = Gtk.Label.New("Sustain"); + _voiceParamLabel[3] = Gtk.Label.New("Release"); + + for (int i = 0; i < _voicesEditorParamBox.Length; i++) + { + _voicesEditorParamBox[i].Append(_voiceParamLabel[i]); + _voicesEditorParamBox[i].Append(_voiceParamValue[i]); + _voicesEditorBox.Append(_voicesEditorParamBox[i]); + } + for (int i = 0; i < _voiceParamValue.Length; i++) + { + _voiceParamValue[i].OnValueChanged += ArgumentChanged; + } + } + + #endregion + } + + private void OpenSoundBankEditor(Gtk.Button sender, EventArgs args) + { + var subBank = _bank!.LoadFromAddress(_bank.GetVoiceAddress((int)_singleSelectionModel.Selected)); + + var soundBankEditor = new SoundBankEditor(); + if (Engine.Instance is not null) + { + soundBankEditor.Init(); + soundBankEditor.LoadVoices(subBank); + } + soundBankEditor.Present(); + + soundBankEditor.OnCloseRequest += WindowClosed; + + bool WindowClosed(Gtk.Window sender, EventArgs args) + { + soundBankEditor!.Dispose(); + soundBankEditor = null!; + return false; + } + } + + public void LoadVoices(SoundBank soundBank) + { + _bank = soundBank; + if (Engine.Instance!.IsFileSystemFormat) + { + Title = $"{ConfigUtils.PROGRAM_NAME} ― {Strings.SoundBankEditorTitle}"; + } + else + { + Title = $"{ConfigUtils.PROGRAM_NAME} ― {Strings.VoiceGroupEditorTitle} (0x{_bank!.Offset:X7})"; + } + ReloadColumnEntries(_bank, _voicesModel, VoicesData); + UpdateParamBoxes(); + } + public void UpdateVoices() + { + // ReloadColumnEntries(Engine.Instance.Player.LoadedSong.Bank, _voicesModel, VoicesData); + // ReloadColumnEntries(Engine.Instance.Player.LoadedSong.Bank.ElementAt(_voicesClickSelectionIndex).GetSubVoices(), _subVoicesModel, SubVoicesData); + } + public void ReloadColumnEntries(IEnumerable? instance, Gio.ListStore? listStore, List? voicesData) + { + if (listStore!.GetNItems() is not 0) + { + listStore.RemoveAll(); + } + voicesData ??= []; + voicesData.Clear(); + + if (Engine.Instance is null) return; + if (Engine.Instance.Player.LoadedSong is null) return; + if (instance is null) return; + + foreach (var voice in instance) + { + voicesData.Add(new SoundBankEditor(voice)); + } + if (voicesData.Count is not 0) + { + foreach (var voice in voicesData) + { + listStore.Append(voice); + } + } + } + + private void OnSetupRow(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.SetupSignalArgs args) + { + + } + + private void OnBindRow(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args) + { + if (args.Object is Gtk.ColumnViewRow row) + { + + } + } + + private void OnSetupLabel(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.SetupSignalArgs args) + { + if (args.Object is not Gtk.ListItem listItem) + { + return; + } + + var label = Gtk.Label.New(null); + label.Halign = Gtk.Align.Center; + + listItem.Child = label; + } + + private void OnBindIndexText(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args) + { + if (args.Object is not Gtk.ListItem listItem) + { + return; + } + + if (listItem.Child is not Gtk.Label label) return; + + label.SetText($"{listItem.Position}"); + } + private void OnBindEventTypeText(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args) + { + if (args.Object is not Gtk.ListItem listItem) + { + return; + } + + if (listItem.Child is not Gtk.Label label) return; + if (listItem.Item is not SoundBankEditor userData) return; + if (userData._voiceInfo is null) return; + + var voiceClass = $"{userData._voiceInfo}".ToLower().Replace(' ', '-').Trim('('); + + label.GetParent()!.GetParent()!.SetName($"row-{voiceClass}"); + + label.SetText(userData._voiceInfo.ToString()!); + label.SetEllipsize(Pango.EllipsizeMode.End); + } + private void OnBindOffsetText(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args) + { + if (args.Object is not Gtk.ListItem listItem) + { + return; + } + + if (listItem.Child is not Gtk.Label label) return; + if (listItem.Item is not SoundBankEditor userData) return; + + label.SetText(string.Format("0x{0:X}", userData._voiceInfo!.Offset)); + label.SetEllipsize(Pango.EllipsizeMode.End); + } +} \ No newline at end of file diff --git a/VG Music Studio - GTK4/TrackEditor.cs b/VG Music Studio - GTK4/TrackEditor.cs new file mode 100644 index 00000000..72dfbf1e --- /dev/null +++ b/VG Music Studio - GTK4/TrackEditor.cs @@ -0,0 +1,588 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using Kermalis.VGMusicStudio.GTK4.Util; + +namespace Kermalis.VGMusicStudio.GTK4; + +internal sealed class TrackEditor : Adw.Window +{ + private event Action? OnArgsChanged; + private Gtk.CssProvider? _cssProvider; + private ICommand? _command; + private long _offset; + private long[]? _ticks; + + private readonly Gtk.Button _buttonEventAdd = Gtk.Button.New(); + private readonly Gtk.Button _buttonEventRemove = Gtk.Button.New(); + private readonly Gtk.Box? _paramEditorBox; + private Gtk.Box[]? _paramEditorParamBox; + private Gtk.SpinButton[]? _eventParamNum; + private Gtk.Label[]? _eventParamLabel; + private OffsetEntry? _eventParamOffset; + private readonly Gtk.StringList? _eventList; + private readonly Gtk.StringList? _trackList; + private readonly Gtk.DropDown? _trackDropDown; + private readonly Gtk.DropDown? _eventDropDown; + + private readonly Gio.ListStore _eventsModel = Gio.ListStore.New(GetGType()); + private readonly Gtk.SingleSelection? _selectionModel; + internal Gtk.ColumnView? EventsColumnView { get; set; } + internal List? EventsData { get; set; } = []; + + private int _clickSelectionIndex = 0; + + private TrackEditor(ICommand command, long offset, Span ticks) + : base() + { + _command = command; + _offset = offset; + _ticks = ticks.ToArray(); + } + internal TrackEditor() + { + New(); + + Title = $"{MainWindow.GetProgramName()} — {Strings.TrackEditorTitle}"; + + var header = Adw.HeaderBar.New(); + header.SetShowEndTitleButtons(true); + header.SetShowStartTitleButtons(true); + + _trackList = Gtk.StringList.New(null); + _trackDropDown = new Gtk.DropDown + { + WidthRequest = 100 + }; + _trackDropDown.SetModel(_trackList); + _trackDropDown.OnNotify += TrackSelected; + + _eventList = Gtk.StringList.New(null); + _eventDropDown = new Gtk.DropDown + { + WidthRequest = 50 + }; + _eventDropDown.SetModel(_eventList); + + _buttonEventAdd.SetLabel(Strings.TrackEditorAddEvent); + _buttonEventAdd.OnClicked += ButtonEventAdd_OnClicked; + _buttonEventRemove.SetLabel(Strings.TrackEditorRemoveEvent); + _buttonEventRemove.OnClicked += ButtonEventRemove_OnClicked; + + var viewport = Gtk.Viewport.New(Gtk.Adjustment.New(0, double.MinValue, double.MaxValue, 1, 1, 1), Gtk.Adjustment.New(0, double.MinValue, double.MaxValue, 1, 1, 1)); + var scrolledWindow = Gtk.ScrolledWindow.New(); + scrolledWindow.SetSizeRequest(700, 500); + scrolledWindow.SetHexpand(true); + scrolledWindow.SetVexpand(true); + + _selectionModel = Gtk.SingleSelection.New(_eventsModel); + + EventsColumnView = Gtk.ColumnView.New(_selectionModel); + EventsColumnView.AddCssClass("data-table"); + EventsColumnView.SetShowColumnSeparators(true); + EventsColumnView.SetShowRowSeparators(true); + EventsColumnView.SetReorderable(false); + EventsColumnView.SetHexpand(true); + + scrolledWindow.SetChild(EventsColumnView); + + viewport.Child = scrolledWindow; + + var paramFrame = Gtk.Frame.New(Strings.TrackEditorArgEditor); + paramFrame.Child = _paramEditorBox = Gtk.Box.New(Gtk.Orientation.Vertical, 6); + _paramEditorBox.SetMarginStart(6); + _paramEditorBox.SetMarginEnd(6); + _paramEditorBox.SetMarginTop(6); + _paramEditorBox.SetMarginBottom(6); + var eventEditorButtonBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 6); + var eventEditorBox = Gtk.Box.New(Gtk.Orientation.Vertical, 6); + var mainEditorBox = Gtk.Box.New(Gtk.Orientation.Vertical, 6); + var contentBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 6); + var mainBox = Gtk.Box.New(Gtk.Orientation.Vertical, 6); + + eventEditorButtonBox.Append(_buttonEventAdd); + eventEditorButtonBox.Append(_buttonEventRemove); + + eventEditorBox.Append(_eventDropDown); + eventEditorBox.Append(eventEditorButtonBox); + + mainEditorBox.Append(eventEditorBox); + mainEditorBox.Append(paramFrame); + + contentBox.Append(viewport); + contentBox.Append(mainEditorBox); + + mainBox.Append(header); + mainBox.Append(_trackDropDown); + mainBox.Append(contentBox); + + SetVexpand(true); + SetHexpand(true); + + SetContent(mainBox); + } + + internal void Init() + { + // Rows + var listItemFactory = Gtk.SignalListItemFactory.New(); + listItemFactory.OnSetup += OnSetupRow; + listItemFactory.OnBind += OnBindRow; + + EventsColumnView!.SetRowFactory(listItemFactory); + + // Event Column + listItemFactory = Gtk.SignalListItemFactory.New(); + listItemFactory.OnSetup += OnSetupLabel; + listItemFactory.OnBind += OnBindEventTypeText; + + var eventColumn = Gtk.ColumnViewColumn.New(Strings.TrackEditorEvent, listItemFactory); + eventColumn.SetFixedWidth(100); + eventColumn.SetExpand(true); + eventColumn.SetResizable(true); + EventsColumnView.AppendColumn(eventColumn); + + // Arguments Column + listItemFactory = Gtk.SignalListItemFactory.New(); + listItemFactory.OnSetup += OnSetupLabel; + listItemFactory.OnBind += OnBindArgumentsText; + listItemFactory.OnUnbind += OnUnbindArgumentsText; + + var argumentsColumn = Gtk.ColumnViewColumn.New(Strings.TrackEditorArguments, listItemFactory); + argumentsColumn.SetFixedWidth(100); + argumentsColumn.SetExpand(true); + argumentsColumn.SetResizable(true); + EventsColumnView.AppendColumn(argumentsColumn); + + // Offset Column + listItemFactory = Gtk.SignalListItemFactory.New(); + listItemFactory.OnSetup += OnSetupLabel; + listItemFactory.OnBind += OnBindOffsetText; + + var offsetColumn = Gtk.ColumnViewColumn.New(Strings.TrackEditorOffset, listItemFactory); + offsetColumn.SetFixedWidth(100); + offsetColumn.SetExpand(true); + offsetColumn.SetResizable(true); + EventsColumnView!.AppendColumn(offsetColumn); + + // Ticks Column + listItemFactory = Gtk.SignalListItemFactory.New(); + listItemFactory.OnSetup += OnSetupLabel; + listItemFactory.OnBind += OnBindTicksText; + + var ticksColumn = Gtk.ColumnViewColumn.New(Strings.TrackEditorTicks, listItemFactory); + ticksColumn.SetFixedWidth(100); + ticksColumn.SetExpand(true); + ticksColumn.SetResizable(true); + EventsColumnView.AppendColumn(ticksColumn); + + ConfigureTimer(); + } + + private void ConfigureTimer() + { + var timer = GLib.Timer.New(); // Creates a new timer variable + var context = GLib.MainContext.GetThreadDefault(); // Reads the main context default thread + var source = GLib.Functions.TimeoutSourceNew(50); // Creates and configures the timeout interval at 50 microseconds, so it updates upon selection + source.SetCallback(EventsCallback); // Sets the callback for the timer interval to be used on + var microsec = new CULong(source.Attach(context)); // Configures the microseconds based on attaching the GLib MainContext thread + GLib.Internal.Timer.Elapsed(timer.Handle, ref microsec); // GLib.Timer.Elapsed was removed in GirCore 0.6.3, so we're using this workaround instead + timer.Start(); // Starts the timer + } + + private bool EventsCallback() + { + if (_selectionModel?.GetSelectedItem() is TrackEditor row && _clickSelectionIndex != (int)_selectionModel.Selected) + { + if (row._command is not null) + { + UpdateParamBoxes(); + _clickSelectionIndex = (int)_selectionModel.Selected; + } + } + return true; + } + + private void ButtonEventAdd_OnClicked(Gtk.Button sender, EventArgs args) + { + var cmd = (ICommand)Activator.CreateInstance(Engine.Instance!.GetCommands()[_eventDropDown!.Selected].GetType())!; + var ev = new SongEvent(int.MaxValue, cmd); + int index = (int)(_selectionModel!.Selected + 1); + Engine.Instance.Player.LoadedSong!.InsertEvent(ev, (int)_trackDropDown!.Selected, index); + EventsData!.Insert(index, new TrackEditor(ev.Command, ev.Offset, ev.Ticks.ToArray())); + _eventsModel.Insert((uint)index, new TrackEditor(ev.Command, ev.Offset, ev.Ticks.ToArray())); + } + + private void ButtonEventRemove_OnClicked(Gtk.Button sender, EventArgs args) + { + if (_selectionModel!.Selected == 4294967295) + { + return; + } + Engine.Instance!.Player.LoadedSong!.RemoveEvent((int)_trackDropDown!.Selected, (int)_selectionModel!.Selected); + EventsData!.RemoveAt((int)_selectionModel!.Selected); + _eventsModel.Remove(_selectionModel.Selected); + } + + private void UpdateParamBoxes() + { + if (_paramEditorParamBox is not null) + { + for (int i = 0; i < _paramEditorParamBox.Length; i++) + { + if (_paramEditorParamBox[i].GetFirstChild() is not null) + { + if (_eventParamNum is not null && _eventParamNum[i] is not null) + { + _paramEditorParamBox[i].Remove(_eventParamNum![i]); + _eventParamNum[i] = null!; + } + if (_eventParamOffset is not null) + { + _paramEditorParamBox[i].Remove(_eventParamOffset); + _eventParamOffset.Dispose(); + _eventParamOffset = null; + } + _paramEditorParamBox[i].Remove(_eventParamLabel![i]); + } + _paramEditorBox!.Remove(_paramEditorParamBox[i]); + } + } + var trackIndex = 0; + var eventIndex = 0; + if (_trackDropDown!.Selected != 4294967295) + { + trackIndex = (int)_trackDropDown.Selected; + } + if (_selectionModel!.Selected != 4294967295) + { + eventIndex = (int)_selectionModel.Selected; + } + var se = Engine.Instance!.Player.LoadedSong!.Events[trackIndex]![eventIndex]!; + MemberInfo[] ignore = typeof(ICommand).GetMembers(); + MemberInfo[] mi = se.Command == null ? [] : se.Command.GetType().GetMembers().Where(m => !ignore.Any(a => m.Name == a.Name) && (m is FieldInfo || m is PropertyInfo)).ToArray(); + _paramEditorParamBox = new Gtk.Box[mi.Length]; + _eventParamLabel = new Gtk.Label[mi.Length]; + var isAnOffset = Engine.Instance!.Player.LoadedSong!.CallOrJumpCommand(se); + if (isAnOffset) + { + _eventParamOffset = new OffsetEntry + { + Halign = Gtk.Align.Center, + Spacing = 3 + }; + } + else + { + _eventParamNum = new Gtk.SpinButton[mi.Length]; + } + for (int i = 0; i < mi.Length; i++) + { + _paramEditorParamBox[i] = Gtk.Box.New(Gtk.Orientation.Vertical, 6); + _eventParamLabel[i] = Gtk.Label.New(mi[i].Name); + _paramEditorParamBox[i].Append(_eventParamLabel[i]); + if (_eventParamNum is not null && _eventParamNum[i] is not null) + { + _eventParamNum[i].OnValueChanged -= ArgumentChanged; + } + + TypeInfo valueType; + object value; + if (mi[i].MemberType == MemberTypes.Field) + { + valueType = (TypeInfo)((FieldInfo)mi[i]).FieldType; + value = ((FieldInfo)mi[i]).GetValue(se.Command)!; + } + else + { + valueType = (TypeInfo)((PropertyInfo)mi[i]).PropertyType; + value = ((PropertyInfo)mi[i]).GetValue(se.Command)!; + } + object lower = null!; + object upper = null!; + foreach (var val in valueType.DeclaredFields) + { + if (val.Name == "MinValue") + { + lower = val.GetValue(mi[i])!; + } + if (val.Name == "MaxValue") + { + upper = val.GetValue(mi[i])!; + } + } + + if (isAnOffset) + { + _eventParamOffset!.SetValue((long)Convert.ChangeType(value, TypeCode.Int64)); + _paramEditorParamBox[i].Append(_eventParamOffset); + } + else + { + value = (double)Convert.ChangeType(value, TypeCode.Double); + if (lower is not null && upper is not null) + { + lower = (double)Convert.ChangeType(lower, TypeCode.Double); + upper = (double)Convert.ChangeType(upper, TypeCode.Double); + } + else + { + lower = -100d; + upper = 100d; + } + _eventParamNum![i] = Gtk.SpinButton.New(Gtk.Adjustment.New((double)value, (double)lower, (double)upper, 1, 1, 1), 1, 0); + _eventParamNum[i].OnValueChanged += ArgumentChanged; + _eventParamNum[i].SetNumeric(true); + _paramEditorParamBox[i].Append(_eventParamNum[i]); + } + + _paramEditorBox!.Append(_paramEditorParamBox[i]); + } + } + + private void ArgumentChanged(Gtk.SpinButton sender, EventArgs args) + { + for (int i = 0; i < _paramEditorParamBox!.Length; i++) + { + if (sender == _eventParamNum![i]) + { + SongEvent se = Engine.Instance!.Player.LoadedSong!.Events[_trackDropDown!.Selected]![(int)_selectionModel!.Selected]; + object value = _eventParamNum[i].Value; + MemberInfo m = se.Command.GetType().GetMember(_eventParamLabel![i].Label_!)[0]; + if (m is FieldInfo f) + { + f.SetValue(se.Command, Convert.ChangeType(value, f.FieldType)); + } + else if (m is PropertyInfo p) + { + p.SetValue(se.Command, Convert.ChangeType(value, p.PropertyType)); + } + + ((TrackEditor)_eventsModel.GetObject(_selectionModel.Selected)!).OnArgsChanged!.Invoke(); + + return; + } + } + } + + public void ReloadDropDownEntries() + { + if (_trackList!.NItems is not 0) + { + _trackList.Splice(0, _trackList.NItems, null); + } + if (_eventList!.NItems is not 0) + { + _eventList.Splice(0, _eventList.NItems, null); + } + + for (int i = 0; i < Engine.Instance?.Player.LoadedSong?.Events.Length; i++) + { + _trackList.Append($"Track {i}"); + } + for (int i = 0; i < Engine.Instance?.GetCommands().Length; i++) + { + _eventList.Append(Engine.Instance?.GetCommands()[i].Label!); + } + } + + public void ReloadColumnEntries() + { + if (_eventsModel.GetNItems() is not 0) + { + _eventsModel.RemoveAll(); + EventsData = []; + } + + if (Engine.Instance is null) return; + if (Engine.Instance.Player.LoadedSong is null) return; + + string cssCode = ""; + List eventsUsed = []; + foreach (var trackEvent in Engine.Instance.Player.LoadedSong.Events[_trackDropDown!.Selected]!) + { + var hexCode = $"{trackEvent.Command.Color.R:X2}{trackEvent.Command.Color.G:X2}{trackEvent.Command.Color.B:X2}".ToLower(); + var hexCodeLit = $"{(byte)(trackEvent.Command.Color.R + ((255 - trackEvent.Command.Color.R) / 2f)):X2}{(byte)(trackEvent.Command.Color.G + ((255 - trackEvent.Command.Color.G) / 2f)):X2}{(byte)(trackEvent.Command.Color.B + ((255 - trackEvent.Command.Color.B) / 2f)):X2}".ToLower(); + + var eventClass = $"{trackEvent.Command.Label}command".ToLower().Replace(' ', '-'); + + var hslColor = new HSLColor(trackEvent.Command.Color); + var textColor = hslColor.Lightness <= 0.6 && hslColor.R <= 0.7 && hslColor.G <= 0.7 ? "white" : "black"; + if (!eventsUsed.Contains(eventClass)) + { + cssCode += $"columnview > listview > #row-{eventClass}" + " {" + + "background-image: none;" + + $"background-color: #{hexCode};" + + $"color: {textColor};" + + "} "; + cssCode += $"columnview > listview > #row-{eventClass}:hover" + " {" + + "background-image: none;" + + $"background-color: #{hexCodeLit};" + + $"color: inherit;" + + "} "; + // cssCode += $"columnview > listview > #row-{eventClass}.activatable:hover" + " {" + + // "background-image: none;" + + // $"background-color: #{hexCodeLit};" + + // $"color: white;" + + // "} "; + // cssCode += $"columnview > listview > #row-{eventClass}.{eventClass}:hover" + " {" + + // "background-image: none;" + + // $"background-color: #{hexCodeLit};" + + // $"color: inherit;" + + // "} "; + cssCode += $"columnview > listview > #row-{eventClass}:selected" + " {" + + $"color: inherit;" + + "} "; + eventsUsed.Add(eventClass); + } + } + _cssProvider ??= Gtk.CssProvider.New(); + _cssProvider.LoadFromString(cssCode); + Gtk.StyleContext.RemoveProviderForDisplay(EventsColumnView!.GetDisplay(), _cssProvider); + Gtk.StyleContext.AddProviderForDisplay(EventsColumnView!.GetDisplay(), _cssProvider, 0); + foreach (var trackEvent in Engine.Instance.Player.LoadedSong.Events[_trackDropDown.Selected]!) + { + var numTicks = new long[trackEvent.Ticks.Count]; + int t = 0; + foreach (var ticks in trackEvent.Ticks) + { + numTicks[t++] = ticks; + } + EventsData!.Add(new TrackEditor(trackEvent.Command, trackEvent.Offset, numTicks)); + } + + foreach (var data in EventsData!) + { + _eventsModel.Append(data); + } + UpdateParamBoxes(); + } + + internal void UpdateTracks() + { + ReloadDropDownEntries(); + ReloadColumnEntries(); + } + + private void TrackSelected(GObject.Object sender, NotifySignalArgs args) + { + if (_trackDropDown!.SelectedItem is not null) + { + ReloadColumnEntries(); + } + } + + private void OnSetupRow(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.SetupSignalArgs args) + { + + } + + private void OnBindRow(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args) + { + if (args.Object is Gtk.ColumnViewRow row) + { + + } + } + + private void OnSetupLabel(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.SetupSignalArgs args) + { + if (args.Object is not Gtk.ListItem listItem) + { + return; + } + + var label = Gtk.Label.New(null); + label.Halign = Gtk.Align.Center; + + listItem.Child = label; + } + + private void OnBindEventTypeText(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args) + { + if (args.Object is not Gtk.ListItem listItem) + { + return; + } + + if (listItem.Child is not Gtk.Label label) return; + if (listItem.Item is not TrackEditor userData) return; + if (userData._command is null) return; + + var eventClass = $"{userData._command.Label}command".ToLower().Replace(' ', '-'); + + label.GetParent()!.GetParent()!.SetName($"row-{eventClass}"); + + label.SetText(userData._command.Label); + } + private void OnBindArgumentsText(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args) + { + if (args.Object is not Gtk.ListItem listItem) + { + return; + } + + if (listItem.Child is not Gtk.Label label) return; + if (listItem.Item is not TrackEditor userData) return; + if (userData._command is null) return; + + label.SetText(userData._command.Arguments); + + userData.OnArgsChanged += ChangeArguments; + + void ChangeArguments() + { + label.SetText(userData._command.Arguments); + } + } + private void OnUnbindArgumentsText(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.UnbindSignalArgs args) + { + if (args.Object is not Gtk.ListItem listItem) + { + return; + } + + if (listItem.Child is not Gtk.Label label) return; + if (listItem.Item is not TrackEditor userData) return; + if (userData._command is null) return; + + userData.OnArgsChanged = null; + } + private void OnBindOffsetText(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args) + { + if (args.Object is not Gtk.ListItem listItem) + { + return; + } + + if (listItem.Child is not Gtk.Label label) return; + if (listItem.Item is not TrackEditor userData) return; + + label.SetText(string.Format("0x{0:X}", userData._offset)); + } + private void OnBindTicksText(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args) + { + if (args.Object is not Gtk.ListItem listItem) + { + return; + } + + if (listItem.Child is not Gtk.Label label) return; + if (listItem.Item is not TrackEditor userData) return; + if (userData._ticks is null) return; + + var array = userData._ticks; + var str = ""; + foreach (var val in array) + { + str = string.Join(", ", val); + } + + label.SetText(str); + } +} \ No newline at end of file diff --git a/VG Music Studio - GTK4/Util/FlexibleDialog.cs b/VG Music Studio - GTK4/Util/FlexibleDialog.cs new file mode 100644 index 00000000..42713dbc --- /dev/null +++ b/VG Music Studio - GTK4/Util/FlexibleDialog.cs @@ -0,0 +1,362 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using Adw; +using Kermalis.VGMusicStudio.Core; + +namespace Kermalis.VGMusicStudio.GTK4.Util; + + +internal sealed class FlexibleDialog +{ + public static ResponseSelected Response = ResponseSelected.None; + public static event Action? OnResponse; + private static event Action? ClickedResult; + private static ISOLanguageNameID LanguageID = ISOLanguageNameID.en; + + private static readonly Gdk.Clipboard Clipboard = Gdk.Display.GetDefault()!.GetClipboard(); + private static string? ExceptionDetails; + private static readonly string[] Button_Labels_English_EN = ["OK", "Cancel", "Yes", "No", "Copy", "Abort", "Terminate", "Retry", "Ignore"]; + private static readonly string[] Button_Labels_German_DE = ["OK", "Abbrechen", "Ja", "Nein", "Kopie", "Abbrechen", "Beenden", "Wiederholen", "Ignorieren"]; + private static readonly string[] Button_Labels_Spanish_ES = ["Aceptar", "Cancelar", "Sí", "No", "Copiar", "Abortar", "Terminar", "Reintentar", "Ignorar"]; + public static readonly string[] Button_Labels_French_FR = ["Approuvé", "Annuler", "Oui", "Non", "Copie", "Avorter", "Terminer", "Refaire", "Ignorer"]; + private static readonly string[] Button_Labels_Italian_IT = ["OK", "Annulla", "Sì", "No", "Copia", "Interrompi", "Terminare", "Riprova", "Ignora"]; + public static readonly string[] Button_Labels_Russian_RU = ["Есть", "Отмена", "Да", "Нет", "Копия", "Стоп", "Прекратить", "Переделать", "Игнорировать"]; + + private enum ButtonID + { + OK = 0, + Cancel, + Yes, + No, + Copy, + Abort, + Terminate, + Retry, + Ignore + }; + + public enum ButtonsType + { + AbortRetryIgnore, + OKCancel, + RetryCancel, + YesNo, + YesNoCancel, + TerminateCopyOK, + OK + } + + public enum ResponseSelected + { + None, + OK, + Cancel, + Abort, + Retry, + Ignore, + Yes, + No + } + + public enum DialogType + { + Exception, + AlertDialog + } + + public enum DefaultButton + { + First, + Second, + Third, + Fourth + } + + private enum ISOLanguageNameID + { + en, + de, + es, + fr, + it, + ru + } + + public static void Show(Exception ex, string heading) + { + Show(string.Empty, heading, ButtonsType.OK, Gtk.MessageType.Other, DefaultButton.Third, ex); + } + public static void Show(string text, string heading = "", ButtonsType buttonsType = ButtonsType.OK, Gtk.MessageType icon = Gtk.MessageType.Other, DefaultButton defaultButton = DefaultButton.First, Exception ex = null!, Window? parent = null) + { + Response = ResponseSelected.None; + parent ??= MainWindow.Instance!; + CreateDialog(parent, text, heading, buttonsType, icon, ex!); + } + + private static void CreateDialog(Window parent, string text, string heading, ButtonsType buttonsType, Gtk.MessageType icon, Exception ex) + { + _ = Enum.TryParse(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, out LanguageID); + Gtk.Widget dialog; + if (ex is not null) + { + dialog = Dialog.New(); + var d = dialog as Dialog; + AddContent(d!, heading, buttonsType, ex); + d!.Present(parent); + } + else + { + dialog = AlertDialog.New(heading, text); + var d = dialog as AlertDialog; + AddButtons(d!, DialogType.AlertDialog, buttonsType); + d!.Present(parent); + } + } + + private static void AddContent(Dialog dialog, string heading, ButtonsType buttonsType, Exception ex) + { + var mainBox = Gtk.Box.New(Gtk.Orientation.Vertical, 15); + mainBox.SetBaselinePosition(Gtk.BaselinePosition.Center); + mainBox.SetHalign(Gtk.Align.Center); + mainBox.SetMarginTop(20); + mainBox.SetMarginStart(20); + mainBox.SetMarginBottom(20); + mainBox.SetMarginEnd(20); + + var headerIconBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 30); + headerIconBox.SetHalign(Gtk.Align.Center); + var buttonBox = Gtk.Box.New(Gtk.Orientation.Horizontal, 20); + buttonBox.SetBaselinePosition(Gtk.BaselinePosition.Bottom); + buttonBox.SetHalign(Gtk.Align.Center); + + var image = Gtk.Image.NewFromIconName("dialog-warning-symbolic"); + image.SetPixelSize(48); + image.SetSizeRequest(48, 48); + + var header = Gtk.Label.New("An unhandled exception has occurred."); + header.SetMarkup($"{header.Label_}"); + header.SetHalign(Gtk.Align.Center); + + var scrolledWindow = Gtk.ScrolledWindow.New(); + + ExceptionDetails = $"{heading}\n\n-------------\n\n {string.Format("Error Details:{1}{1}{0}{1}{2}", ex.Message, Environment.NewLine, ex.StackTrace)}"; + + var textTag = Gtk.TextTag.New("Error"); + + var textTagTable = Gtk.TextTagTable.New(); + textTagTable.Add(textTag); + var textBuffer = Gtk.TextBuffer.New(textTagTable); + textBuffer.SetText(ExceptionDetails, ExceptionDetails.Length); + var textView = Gtk.TextView.NewWithBuffer(textBuffer); + textView.SetEditable(false); + + scrolledWindow.SetChild(textView); + scrolledWindow.SetSizeRequest(500, 180); + + AddButtons(dialog, DialogType.Exception, ButtonsType.TerminateCopyOK, buttonBox); + + headerIconBox.Append(image); + headerIconBox.Append(header); + mainBox.Append(headerIconBox); + mainBox.Append(scrolledWindow); + mainBox.Append(buttonBox); + + dialog.SetChild(mainBox); + } + + private static string GetButtonLabel(ButtonID buttonID) + { + int buttonLabelIndex = Convert.ToInt32(buttonID); + + return LanguageID switch + { + ISOLanguageNameID.de => Button_Labels_German_DE[buttonLabelIndex], + ISOLanguageNameID.es => Button_Labels_Spanish_ES[buttonLabelIndex], + ISOLanguageNameID.fr => Button_Labels_French_FR[buttonLabelIndex], + ISOLanguageNameID.it => Button_Labels_Italian_IT[buttonLabelIndex], + ISOLanguageNameID.ru => Button_Labels_Russian_RU[buttonLabelIndex], + _ => Button_Labels_English_EN[buttonLabelIndex], + }; + } + + private static void AddButtons(object dialog, DialogType dialogType, ButtonsType buttonsType = ButtonsType.OK, Gtk.Box box = null!) + { + switch (dialogType) + { + case DialogType.Exception: + { + var d = (Dialog)dialog; + + var buttonTerminate = Gtk.Button.New(); + buttonTerminate.SetLabel(GetButtonLabel(ButtonID.Terminate)); + buttonTerminate.SetName("_buttonTerminate"); + buttonTerminate.AddCssClass("destructive-action"); + buttonTerminate.OnClicked += ResponseClicked; + + var buttonCopy = Gtk.Button.New(); + buttonCopy.SetLabel(GetButtonLabel(ButtonID.Copy)); + buttonCopy.SetName("_buttonCopy"); + buttonCopy.OnClicked += ResponseClicked; + + var buttonOK = Gtk.Button.New(); + buttonOK.SetLabel(GetButtonLabel(ButtonID.OK)); + buttonOK.SetName("_buttonOK"); + buttonOK.SetReceivesDefault(true); + buttonOK.OnClicked += ResponseClicked; + + ClickedResult += OnClose; + + void OnClose() + { + ClickedResult -= OnClose; + d!.Close(); + } + + box.Append(buttonTerminate); + box.Append(buttonCopy); + box.Append(buttonOK); + + dialog = d; + break; + } + case DialogType.AlertDialog: + { + var d = (AlertDialog)dialog; + var id = $"_{ButtonID.OK}"; + switch (buttonsType) + { + case ButtonsType.AbortRetryIgnore: + { + d.AddResponse($"_{ButtonID.Abort}", GetButtonLabel(ButtonID.Abort)); + d.AddResponse($"_{ButtonID.Retry}", GetButtonLabel(ButtonID.Retry)); + d.AddResponse($"_{ButtonID.Ignore}", GetButtonLabel(ButtonID.Ignore)); + d.SetDefaultResponse($"_{ButtonID.Abort}"); + d.SetCloseResponse($"_{ButtonID.Abort}"); + d.OnResponse += ResponseClicked; + break; + } + case ButtonsType.OKCancel: + { + d.AddResponse($"_{ButtonID.OK}", GetButtonLabel(ButtonID.OK)); + d.AddResponse($"_{ButtonID.Cancel}", GetButtonLabel(ButtonID.Cancel)); + d.SetDefaultResponse($"_{ButtonID.OK}"); + d.SetCloseResponse($"_{ButtonID.Cancel}"); + d.OnResponse += ResponseClicked; + break; + } + case ButtonsType.RetryCancel: + { + d.AddResponse($"_{ButtonID.Retry}", GetButtonLabel(ButtonID.Retry)); + d.AddResponse($"_{ButtonID.Cancel}", GetButtonLabel(ButtonID.Cancel)); + d.SetDefaultResponse($"_{ButtonID.Retry}"); + d.SetCloseResponse($"_{ButtonID.Cancel}"); + d.OnResponse += ResponseClicked; + break; + } + case ButtonsType.YesNo: + { + d.AddResponse($"_{ButtonID.Yes}", GetButtonLabel(ButtonID.Yes)); + d.AddResponse($"_{ButtonID.No}", GetButtonLabel(ButtonID.No)); + d.SetDefaultResponse($"_{ButtonID.Yes}"); + d.SetCloseResponse($"_{ButtonID.No}"); + d.OnResponse += ResponseClicked; + break; + } + case ButtonsType.YesNoCancel: + { + d.AddResponse($"_{ButtonID.Yes}", GetButtonLabel(ButtonID.Yes)); + d.AddResponse($"_{ButtonID.No}", GetButtonLabel(ButtonID.No)); + d.AddResponse($"_{ButtonID.Cancel}", GetButtonLabel(ButtonID.Cancel)); + d.SetDefaultResponse($"_{ButtonID.Yes}"); + d.SetCloseResponse($"_{ButtonID.Cancel}"); + d.OnResponse += ResponseClicked; + break; + } + case ButtonsType.OK: + { + d.AddResponse($"_{ButtonID.OK}", GetButtonLabel(ButtonID.OK)); + d.SetDefaultResponse($"_{ButtonID.OK}"); + d.SetCloseResponse($"_{ButtonID.OK}"); + d.OnResponse += ResponseClicked; + break; + } + } + dialog = d; + break; + } + } + } + + private static void ResponseClicked(object sender, EventArgs args) + { + if (sender is Gtk.Button button) + { + switch (button.Name) + { + case "_buttonTerminate": + { + button.OnClicked -= ResponseClicked; + ClickedResult!.Invoke(); + Engine.Instance?.Dispose(); + MainWindow.Instance!.Close(); + break; + } + case "_buttonCopy": + { + Clipboard.SetText(ExceptionDetails!); + break; + } + case "_buttonOK": + { + button.OnClicked -= ResponseClicked; + Response = ResponseSelected.OK; + ClickedResult!.Invoke(); + break; + } + } + } + else if (sender is AlertDialog alertDialog) + { + alertDialog.OnResponse -= ResponseClicked; + if (args is AlertDialog.ResponseSignalArgs arg) + { + if (arg.Response == $"_{ButtonID.Abort}") + { + Response = ResponseSelected.Abort; + } + else if (arg.Response == $"_{ButtonID.Retry}") + { + Response = ResponseSelected.Retry; + } + else if (arg.Response == $"_{ButtonID.Ignore}") + { + Response = ResponseSelected.Ignore; + } + else if (arg.Response == $"_{ButtonID.OK}") + { + Response = ResponseSelected.OK; + } + else if (arg.Response == $"_{ButtonID.Cancel}") + { + Response = ResponseSelected.Cancel; + } + else if (arg.Response == $"_{ButtonID.Yes}") + { + Response = ResponseSelected.Yes; + } + else if (arg.Response == $"_{ButtonID.No}") + { + Response = ResponseSelected.No; + } + } + if (OnResponse is not null) + { + OnResponse!.Invoke(Response); + } + alertDialog.Close(); + } + } +} diff --git a/VG Music Studio - GTK4/Util/GTK4Utils.cs b/VG Music Studio - GTK4/Util/GTK4Utils.cs new file mode 100644 index 00000000..a27a8d32 --- /dev/null +++ b/VG Music Studio - GTK4/Util/GTK4Utils.cs @@ -0,0 +1,296 @@ +using Gtk; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using System; +using System.Runtime.InteropServices; + +namespace Kermalis.VGMusicStudio.GTK4.Util; + +internal class GTK4Utils : DialogUtils +{ + // Callback + private static Gio.Internal.AsyncReadyCallback? SaveCallback { get; set; } + private static Gio.Internal.AsyncReadyCallback? OpenCallback { get; set; } + private static Gio.Internal.AsyncReadyCallback? SelectFolderCallback { get; set; } + + + public static event Action? OnPathChanged; + + private static void Convert(string filterName, Span fileExtensions, FileFilter fileFilter) + { + if (fileExtensions.IsEmpty | filterName.Contains('|')) + { + for (int i = 0; i < filterName.Length; i++) + { + _ = new string[filterName.Split('|').Length]; + Span fn = filterName.Split('|'); + fileFilter.SetName(fn[0]); + if (fn[1].Contains(';')) + { + _ = new string[fn[1].Split(';').Length]; + Span fe = fn[1].Split(';'); + for (int k = 0; k < fe.Length; k++) + { + //fe[k] = fe[k].Trim('*', '.'); + fileFilter.AddPattern(fe[k]); + fileFilter.AddPattern(fe[k].ToLower()); + fileFilter.AddPattern(fe[k].ToLowerInvariant()); + fileFilter.AddPattern(fe[k].ToUpper()); + fileFilter.AddPattern(fe[k].ToUpperInvariant()); + } + } + else + { + fileFilter.AddPattern(fn[1]); + fileFilter.AddPattern(fn[1].ToLower()); + fileFilter.AddPattern(fn[1].ToLowerInvariant()); + fileFilter.AddPattern(fn[1].ToUpper()); + fileFilter.AddPattern(fn[1].ToUpperInvariant()); + } + } + } + else + { + fileFilter.SetName(filterName); + for (int i = 0; i < fileExtensions.Length; i++) + { + fileFilter.AddPattern(fileExtensions[i]); + fileFilter.AddPattern(fileExtensions[i].ToLower()); + fileFilter.AddPattern(fileExtensions[i].ToLowerInvariant()); + fileFilter.AddPattern(fileExtensions[i].ToUpper()); + fileFilter.AddPattern(fileExtensions[i].ToUpperInvariant()); + } + } + } + public static string CreateLoadDialog(string title, object parent = null!) => + new GTK4Utils().CreateLoadDialog(title, "", [""], false, false, parent); + public static string CreateLoadDialog(string extension, string title, string filter, object parent = null!) => + new GTK4Utils().CreateLoadDialog(title, filter, [extension], true, true, parent); + public static string CreateLoadDialog(Span extensions, string title, string filter, bool allowAllFiles = true, object parent = null!) => + new GTK4Utils().CreateLoadDialog(title, filter, extensions, true, allowAllFiles, parent); + public override string CreateLoadDialog(string title, string filterName = "", string fileExtension = "", bool isFile = false, bool allowAllFiles = false, object? parent = null) => + CreateLoadDialog(title, filterName, [fileExtension], isFile, allowAllFiles, parent!); + public override string CreateLoadDialog(string title, string filterName, Span fileExtensions, bool isFile, bool allowAllFiles, object? parent) + { + parent ??= MainWindow.Instance; + string? path = null; + if (isFile) + { + var ff = FileFilter.New(); + Convert(filterName, fileExtensions, ff); + + var filters = Gio.ListStore.New(FileFilter.GetGType()); + filters.Append(ff); + var allFiles = FileFilter.New(); + allFiles.SetName(Strings.FilterAllFiles); + allFiles.AddPattern("*.*"); + if (allowAllFiles) + { + filters.Append(allFiles); + } + + if (Functions.GetMinorVersion() <= 8) + { + var d = FileChooserNative.New( + title, + (Window)parent!, + FileChooserAction.Open, + "Open", + "Cancel"); + + + d.AddFilter(ff); + d.AddFilter(allFiles); + + d.OnResponse += OpenResponse; + d.Show(); + return null!; + + void OpenResponse(NativeDialog sender, NativeDialog.ResponseSignalArgs e) + { + if (e.ResponseId != (int)ResponseType.Accept) + { + d.Dispose(); + return; + } + path = d.GetFile()!.GetPath() ?? ""; + OnPathChanged!.Invoke(path!); + d.Dispose(); + } + } + else + { + var d = FileDialog.New(); + d.SetTitle(title); + d.SetFilters(filters); + GTK4Utils.OpenCallback += OpenCallback; + + // SelectFolder, Open and Save methods are currently missing from GirCore, but are available in the Gtk.Internal namespace, + // so we're using this until GirCore updates with the method bindings. See here: https://github.com/gircore/gir.core/issues/900 + var p = (Window)parent!; + Gtk.Internal.FileDialog.Open(d.Handle.DangerousGetHandle(), p.Handle.DangerousGetHandle(), nint.Zero, GTK4Utils.OpenCallback, nint.Zero); + //d.Open(Handle, IntPtr.Zero, _openCallback, IntPtr.Zero); + return path!; + + void OpenCallback(nint sourceObject, nint res, nint data) + { + GTK4Utils.OpenCallback -= OpenCallback; + var fileHandle = Gtk.Internal.FileDialog.OpenFinish(d.Handle.DangerousGetHandle(), res, out GLib.Internal.ErrorOwnedHandle errorHandle); + if (fileHandle != IntPtr.Zero) + { + path = Marshal.PtrToStringUTF8(Gio.Internal.File.GetPath(fileHandle).DangerousGetHandle()); + Gio.Internal.File.GetPath(fileHandle).Dispose(); + } + OnPathChanged!.Invoke(path!); + errorHandle.Close(); + filters.Dispose(); + allFiles.Dispose(); + ff.Dispose(); + d.Dispose(); + } + } + } + else + { + if (Functions.GetMinorVersion() <= 8) + { + // To allow the dialog to display in native windowing format, FileChooserNative is used instead of FileChooserDialog + var d = FileChooserNative.New( + title, // The title shown in the folder select dialog window + (Window)parent!, // The parent of the dialog window, is the MainWindow itself + FileChooserAction.SelectFolder, // To ensure it becomes a folder select dialog window, SelectFolder is used as the FileChooserAction + "Select Folder", // Followed by the accept + "Cancel"); // and cancel button names. + + d.SetModal(true); + + // Note: Blocking APIs were removed in GTK4, which means the code will proceed to run and return to the main loop, even when a dialog is displayed. + // Instead, it's handled by the OnResponse event function when it re-enters upon selection. + d.OnResponse += SelectFolderResponse; + d.Show(); + return path!; + + void SelectFolderResponse(NativeDialog sender, NativeDialog.ResponseSignalArgs e) + { + if (e.ResponseId != (int)ResponseType.Accept) // In GTK4, the 'Gtk.FileChooserNative.Action' property is used for determining the button selection on the dialog. The 'Gtk.Dialog.Run' method was removed in GTK4, due to it being a non-GUI function and going against GTK's main objectives. + { + d.Dispose(); + return; + } + path = d.GetCurrentFolder()!.GetPath() ?? ""; + d.GetData(path); + OnPathChanged!.Invoke(path!); + d.Dispose(); // Ensures disposal of the dialog when closed + } + } + else + { + var d = FileDialog.New(); + d.SetTitle(title); + + GTK4Utils.SelectFolderCallback += SelectFolderCallback; + var p = (Window)parent!; + Gtk.Internal.FileDialog.SelectFolder(d.Handle.DangerousGetHandle(), p.Handle.DangerousGetHandle(), nint.Zero, GTK4Utils.SelectFolderCallback, nint.Zero); + return path!; + + void SelectFolderCallback(nint sourceObject, nint res, nint data) + { + GTK4Utils.SelectFolderCallback -= SelectFolderCallback; + var folderHandle = Gtk.Internal.FileDialog.SelectFolderFinish(d.Handle.DangerousGetHandle(), res, out GLib.Internal.ErrorOwnedHandle errorHandle); + if (folderHandle != IntPtr.Zero) + { + path = Marshal.PtrToStringUTF8(Gio.Internal.File.GetPath(folderHandle).DangerousGetHandle()); + Gio.Internal.File.GetPath(folderHandle).Dispose(); + } + OnPathChanged!.Invoke(path!); + errorHandle.Close(); + d.Dispose(); + } + } + } + } + + public static string CreateSaveDialog(string fileName, string extension, string title, string filter, bool allowAllFiles = false, object parent = null!) => + new GTK4Utils().CreateSaveDialog(fileName, title, filter, [extension], true, allowAllFiles, parent); + public static string CreateSaveDialog(string fileName, Span extensions, string title, string filter, bool allowAllFiles = false, object parent = null!) => + new GTK4Utils().CreateSaveDialog(fileName, title, filter, extensions, true, allowAllFiles, parent); + public override string CreateSaveDialog(string fileName, string title, string filterName, string fileExtension = "", bool isFile = false, bool allowAllFiles = false, object? parent = null) => + CreateSaveDialog(fileName, title, filterName, [fileExtension], false); + public override string CreateSaveDialog(string fileName, string title, string filterName, Span fileExtensions, bool isFile = false, bool allowAllFiles = false, object? parent = null) + { + parent ??= MainWindow.Instance; + string? path = null; + + var ff = FileFilter.New(); + Convert(filterName, fileExtensions, ff); + + var filters = Gio.ListStore.New(FileFilter.GetGType()); + filters.Append(ff); + var allFiles = FileFilter.New(); + allFiles.SetName(Strings.FilterAllFiles); + allFiles.AddPattern("*.*"); + if (allowAllFiles) + { + filters.Append(allFiles); + } + + if (Functions.GetMinorVersion() <= 8) + { + var d = FileChooserNative.New( + title, + (Window)parent!, + FileChooserAction.Save, + "Save", + "Cancel"); + d.SetCurrentName(fileName); + d.AddFilter(ff); + + d.OnResponse += SaveResponse; + d.Show(); + return null!; + + void SaveResponse(NativeDialog sender, NativeDialog.ResponseSignalArgs e) + { + if (e.ResponseId != (int)ResponseType.Accept) + { + d.Dispose(); + return; + } + + path = d.GetFile()!.GetPath() ?? ""; + OnPathChanged!.Invoke(path!); + d.Dispose(); + } + } + else + { + var d = FileDialog.New(); + d.SetTitle(title); + d.SetInitialName(fileName + fileExtensions[0].Trim('*')); + d.SetFilters(filters); + GTK4Utils.SaveCallback += SaveCallback; + var p = (Window)parent!; + Gtk.Internal.FileDialog.Save(d.Handle.DangerousGetHandle(), p.Handle.DangerousGetHandle(), nint.Zero, GTK4Utils.SaveCallback, nint.Zero); + //d.Open(Handle, IntPtr.Zero, _openCallback, IntPtr.Zero); + return path!; + + void SaveCallback(nint sourceObject, nint res, nint data) + { + GTK4Utils.SaveCallback -= SaveCallback; + var errorHandle = new GLib.Internal.ErrorOwnedHandle(IntPtr.Zero); + var fileHandle = Gtk.Internal.FileDialog.SaveFinish(d.Handle.DangerousGetHandle(), res, out errorHandle); + if (fileHandle != IntPtr.Zero) + { + path = Marshal.PtrToStringUTF8(Gio.Internal.File.GetPath(fileHandle).DangerousGetHandle()); + Gio.Internal.File.GetPath(fileHandle).Dispose(); + } + OnPathChanged!.Invoke(path!); + errorHandle.Close(); + filters.Dispose(); + allFiles.Dispose(); + ff.Dispose(); + d.Dispose(); + } + } + } +} diff --git a/VG Music Studio - GTK4/Util/OffsetEntry.cs b/VG Music Studio - GTK4/Util/OffsetEntry.cs new file mode 100644 index 00000000..868dbbab --- /dev/null +++ b/VG Music Studio - GTK4/Util/OffsetEntry.cs @@ -0,0 +1,77 @@ + +using System; + +namespace Kermalis.VGMusicStudio.GTK4.Util; + +internal class OffsetEntry : Gtk.Box +{ + private Gtk.Label _hexLabel; + internal Gtk.Entry Entry; + internal OffsetEntry(int offset = 0) + { + New(Gtk.Orientation.Horizontal, 3); + _hexLabel = Gtk.Label.New("0x"); + Entry = Gtk.Entry.New(); + Entry.Text_ = $"{offset:X7}"; + Entry.OnChanged += Offset_OnChanged; + Append(_hexLabel); + Append(Entry); + } + + private void Offset_OnChanged(Gtk.Editable sender, EventArgs args) + { + Entry.OnChanged -= Offset_OnChanged; + var text = sender.GetText(); + for (int i = 0; i < text.Length; i++) + { + bool isValidHexValue = false; + if (char.IsDigit(text[i])) + { + isValidHexValue = true; + } + else + { + switch (text[i]) + { + case 'A': + case 'a': + isValidHexValue = true; + break; + case 'B': + case 'b': + isValidHexValue = true; + break; + case 'C': + case 'c': + isValidHexValue = true; + break; + case 'D': + case 'd': + isValidHexValue = true; + break; + case 'E': + case 'e': + isValidHexValue = true; + break; + case 'F': + case 'f': + isValidHexValue = true; + break; + } + } + + if (!isValidHexValue) + { + text.Remove(i, 1); + } + + Entry.Text_ = $"{text:X7}"; + } + Entry.OnChanged += Offset_OnChanged; + } + + internal void SetValue(long offset) + { + Entry.Text_ = $"{offset:X7}"; + } +} \ No newline at end of file diff --git a/VG Music Studio - GTK4/VG Music Studio - GTK4.csproj b/VG Music Studio - GTK4/VG Music Studio - GTK4.csproj new file mode 100644 index 00000000..1fd87836 --- /dev/null +++ b/VG Music Studio - GTK4/VG Music Studio - GTK4.csproj @@ -0,0 +1,40 @@ + + + + Exe + net10.0 + enable + ..\Build\GTK4 + ..\Icons\Icon.ico + true + + + + + + + + + + %(Filename)%(Extension) + + + + + + + + + + Always + %(Filename)%(Extension) + + + + + + + + + + diff --git a/VG Music Studio - GTK4/WidgetWindow.cs b/VG Music Studio - GTK4/WidgetWindow.cs new file mode 100644 index 00000000..bb513307 --- /dev/null +++ b/VG Music Studio - GTK4/WidgetWindow.cs @@ -0,0 +1,24 @@ +using Adw; + +namespace Kermalis.VGMusicStudio.GTK4; + +internal class WidgetWindow : Window +{ + internal Gtk.Box WidgetBox; + internal WidgetWindow(Gtk.Widget widget) + { + New(); + + Title = MainWindow.GetProgramName(); + + var header = HeaderBar.New(); + WidgetBox = Gtk.Box.New(Gtk.Orientation.Vertical, 0); + WidgetBox.Append(widget); + + var box = Gtk.Box.New(Gtk.Orientation.Vertical, 0); + box.Append(header); + box.Append(WidgetBox); + + SetContent(box); + } +} \ No newline at end of file diff --git a/VG Music Studio - WinForms/TrackViewer.cs b/VG Music Studio - WinForms/TrackViewer.cs deleted file mode 100644 index 81520cc4..00000000 --- a/VG Music Studio - WinForms/TrackViewer.cs +++ /dev/null @@ -1,112 +0,0 @@ -using BrightIdeasSoftware; -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Core.Properties; -using Kermalis.VGMusicStudio.Core.Util; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.Linq; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.WinForms; - -[DesignerCategory("")] -internal sealed class TrackViewer : ThemedForm -{ - private readonly ObjectListView _listView; - private readonly ComboBox _tracksBox; - - public TrackViewer() - { - const int W = (600 / 2) - 12 - 6; - const int H = 400 - 12 - 11; - - _listView = new ObjectListView - { - FullRowSelect = true, - HeaderStyle = ColumnHeaderStyle.Nonclickable, - HideSelection = false, - Location = new Point(12, 12), - MultiSelect = false, - RowFormatter = RowColorer, - ShowGroups = false, - Size = new Size(W, H), - UseFiltering = true, - UseFilterIndicator = true - }; - OLVColumn c1, c2, c3, c4; - c1 = new OLVColumn(Strings.TrackViewerEvent, "Command.Label"); - c2 = new OLVColumn(Strings.TrackViewerArguments, "Command.Arguments") { UseFiltering = false }; - c3 = new OLVColumn(Strings.TrackViewerOffset, "Offset") { AspectToStringFormat = "0x{0:X}", UseFiltering = false }; - c4 = new OLVColumn(Strings.TrackViewerTicks, "Ticks") { AspectGetter = (o) => string.Join(", ", ((SongEvent)o).Ticks), UseFiltering = false }; - c1.Width = c2.Width = c3.Width = 72; - c4.Width = 47; - c1.Hideable = c2.Hideable = c3.Hideable = c4.Hideable = false; - c1.TextAlign = c2.TextAlign = c3.TextAlign = c4.TextAlign = HorizontalAlignment.Center; - _listView.AllColumns.AddRange(new OLVColumn[] { c1, c2, c3, c4 }); - _listView.RebuildColumns(); - _listView.ItemActivate += ListView_ItemActivate; - - var panel1 = new ThemedPanel { Location = new Point(306, 12), Size = new Size(W, H) }; - _tracksBox = new ComboBox - { - Enabled = false, - Location = new Point(4, 4), - Size = new Size(100, 21) - }; - _tracksBox.SelectedIndexChanged += TracksBox_SelectedIndexChanged; - panel1.Controls.AddRange(new Control[] { _tracksBox }); - - ClientSize = new Size(600, 400); - Controls.AddRange(new Control[] { _listView, panel1 }); - FormBorderStyle = FormBorderStyle.FixedDialog; - MaximizeBox = false; - Text = $"{ConfigUtils.PROGRAM_NAME} ― {Strings.TrackViewerTitle}"; - - UpdateTracks(); - } - - private void ListView_ItemActivate(object? sender, EventArgs e) - { - List list = ((SongEvent)_listView.SelectedItem.RowObject).Ticks; - if (list.Count > 0) - { - Engine.Instance!.Player.SetSongPosition(list[0]); - MainForm.Instance.LetUIKnowPlayerIsPlaying(); - } - } - - private void RowColorer(OLVListItem item) - { - item.BackColor = ((SongEvent)item.RowObject).Command.Color; - } - - private void TracksBox_SelectedIndexChanged(object? sender, EventArgs? e) - { - int i = _tracksBox.SelectedIndex; - if (i != -1) - { - _listView.SetObjects(Engine.Instance!.Player.LoadedSong!.Events[i]); - } - else - { - _listView.Items.Clear(); - } - } - public void UpdateTracks() - { - int numTracks = Engine.Instance?.Player.LoadedSong?.Events.Length ?? 0; - bool tracks = numTracks > 0; - _tracksBox.Enabled = tracks; - if (tracks) - { - // Track 0, Track 1, ... - _tracksBox.DataSource = Enumerable.Range(0, numTracks).Select(i => string.Format(Strings.TrackViewerTrackX, i)).ToList(); - } - else - { - _tracksBox.DataSource = null; - } - } -} \ No newline at end of file diff --git a/VG Music Studio.sln b/VG Music Studio.sln index 31bb2a27..f1b04406 100644 --- a/VG Music Studio.sln +++ b/VG Music Studio.sln @@ -3,24 +3,72 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.3.32819.101 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VG Music Studio - WinForms", "VG Music Studio - WinForms\VG Music Studio - WinForms.csproj", "{646D3254-F214-4F33-991F-5D5DEB7219AA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "(Deprecated) VG Music Studio - WinForms", "(Deprecated) VG Music Studio - WinForms\(Deprecated) 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 - GTK4", "VG Music Studio - GTK4\VG Music Studio - GTK4.csproj", "{AB599ACD-26E0-4925-B91E-E25D41CB05E8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {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}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Debug|ARM64.Build.0 = Debug|Any CPU + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Debug|x64.ActiveCfg = Debug|Any CPU + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Debug|x64.Build.0 = Debug|Any CPU + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Debug|x86.ActiveCfg = Debug|Any CPU + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Debug|x86.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 + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Release|ARM64.ActiveCfg = Release|Any CPU + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Release|ARM64.Build.0 = Release|Any CPU + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Release|x64.ActiveCfg = Release|Any CPU + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Release|x64.Build.0 = Release|Any CPU + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Release|x86.ActiveCfg = Release|Any CPU + {646D3254-F214-4F33-991F-5D5DEB7219AA}.Release|x86.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}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Debug|ARM64.Build.0 = Debug|Any CPU + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Debug|x64.ActiveCfg = Debug|Any CPU + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Debug|x64.Build.0 = Debug|Any CPU + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Debug|x86.ActiveCfg = Debug|Any CPU + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Debug|x86.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 + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Release|ARM64.ActiveCfg = Release|Any CPU + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Release|ARM64.Build.0 = Release|Any CPU + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Release|x64.ActiveCfg = Release|Any CPU + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Release|x64.Build.0 = Release|Any CPU + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Release|x86.ActiveCfg = Release|Any CPU + {5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}.Release|x86.Build.0 = Release|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Debug|ARM64.Build.0 = Debug|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Debug|x64.Build.0 = Debug|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Debug|x86.ActiveCfg = Debug|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Debug|x86.Build.0 = Debug|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Release|Any CPU.Build.0 = Release|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Release|ARM64.ActiveCfg = Release|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Release|ARM64.Build.0 = Release|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Release|x64.ActiveCfg = Release|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Release|x64.Build.0 = Release|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Release|x86.ActiveCfg = Release|Any CPU + {AB599ACD-26E0-4925-B91E-E25D41CB05E8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE