diff --git a/.gitignore b/.gitignore index 80921d3..181a70e 100644 --- a/.gitignore +++ b/.gitignore @@ -259,4 +259,30 @@ 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 diff --git a/README.md b/README.md index 83a0af9..d214231 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,138 @@ 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 +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 Windows executable (.exe). + +Otherwise it will complain upon launch with the following System.TypeInitializationException error: +``DllNotFoundException: Unable to load DLL 'libgtk-4-1.dll' 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 MSYS2 from [the official website](https://www.msys2.org/), and ensure it is installed in the default directory: ``C:\``. +2. After installation, run the following commands in the MSYS2 terminal: ``pacman -Syy`` to reload the package database, then ``pacman -Syuu`` to update all the packages. +3. 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`` + +### 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`` + +##### ChimeraOS (Arch based distribution) +Note: Not to be confused with Chimera Linux, the Linux distribution made from scratch with a custom Linux kernel. This one is an Arch Linux based distribution. + +Arch Linux based distribution, ChimeraOS, comes pre-installed with the GNOME desktop environment. To access it, open the terminal and type ``chimera-session desktop``. + +But because it is missing the .NET SDK and .NET Runtime, and the root directory is read-only, you will need to run the following command: ``sudo frzr-unlock`` + +Then install any required packages like this example: ``sudo pacman -S [package-name]`` + +Note: Any installed packages installed in the root directory with the pacman utility will be undone when ChimeraOS is updated, due to the way [frzr](https://github.com/ChimeraOS/frzr) functions. Also, frzr may be what inspired Vanilla OS's [ABRoot](https://github.com/Vanilla-OS/ABRoot) utility. + +#### 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. + +#### Puppy Linux +Puppy Linux is an independent distribution that has many variants, each with packages from other Linux distributions. + +It's not possible to find the gtk4, libadwaita and gtksourceview5 libraries or their dependencies in the GUI package management tool, Puppy Package Manager. Because Puppy Linux is built to be a portable and lightweight distribution and to be compatible with older hardware. And because of this, it is only possible to find gtk+2 libraries and other legacy dependencies that it relies on. + +So therefore, VG Music Studio isn't supported on Puppy Linux. + +#### Chimera Linux +Note: Not to be confused with the Arch Linux based distribution named ChimeraOS. This one is completely different and written from scratch, and uses a modified Linux kernel. + +Chimera Linux already comes pre-installed with the GNOME desktop environment and uses the Alpine Package Kit. If you need to install any necessary packages, run the following command example: +``apk add [package-name]`` + +#### 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 +It may be possible to build VG Music Studio on FreeBSD (and FreeBSD based operating systems), however this section will need to be updated with better accuracy on how to build on this platform. + +If your operating system is FreeBSD, or is based on FreeBSD, the [portmaster](https://cgit.freebsd.org/ports/tree/ports-mgmt/portmaster/) utility will need to be installed before installing ``gtk40``, ``libadwaita`` and ``gtksourceview5``. A guide on how to do so can be found [here](https://docs.freebsd.org/en/books/handbook/ports/). + +Once installed and configured, run the following commands to install these ports: +``portmaster -PP gtk40`` +``portmaster -PP libadwaita`` +``portmaster -PP gtksourceview5`` + ---- ## Special Thanks To: ### General * Stich991 - Italian translation * tuku473 - Design suggestions, colors, Spanish translation -* Lachesis - French translation -* Delusional Moonlight - Russian translation ### AlphaDream Engine * irdkwia - Finding games that used the engine diff --git a/VG Music Studio - Core/Dependencies/KMIDI.deps.json b/VG Music Studio - Core/Dependencies/KMIDI.deps.json deleted file mode 100644 index 7feb759..0000000 --- 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 372b9e3..0000000 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 2358835..0000000 --- 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 index a37f0e0..29554e0 100644 --- a/VG Music Studio - Core/Engine.cs +++ b/VG Music Studio - Core/Engine.cs @@ -8,9 +8,11 @@ public abstract class Engine : IDisposable public abstract Config Config { get; } public abstract Mixer Mixer { get; } + public abstract Mixer_NAudio Mixer_NAudio { get; } public abstract Player Player { get; } + public abstract bool UseNewMixer { get; } - public virtual void Dispose() + public virtual void Dispose() { Config.Dispose(); Mixer.Dispose(); 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 0000000..7cace6b --- /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 0000000..5163f55 --- /dev/null +++ b/VG Music Studio - Core/Formats/Wave.cs @@ -0,0 +1,423 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.Formats; + +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 bool DiscardOnBufferOverflow; + public int BufferLength; + + public byte[]? Buffer; + private int ReadPosition; + private int WritePosition; + private int ByteCount; + private object? LockObject; + + private long DataChunkSize; + private long DataChunkLength; + private long DataChunkPosition; + private readonly Stream? InStream; + private readonly Stream? OutStream; + + 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 Wave() + { + InStream = new MemoryStream(); + OutStream = new MemoryStream(); + } + public Wave(string fileName) + { + InStream = new MemoryStream(); + OutStream = new MemoryStream(); + FileName = fileName; + } + + public Wave CreateFormat(uint sampleRate, ushort channels, ushort blockAlign, uint averageBytesPerSecond, ushort bitsPerSample) + { + Channels = channels; + SampleRate = sampleRate; + AverageBytesPerSecond = averageBytesPerSecond; + BlockAlign = blockAlign; + BitsPerSample = bitsPerSample; + ExtraSize = 0; + return new Wave(); + } + public Wave CreateIeeeFloatWave(uint sampleRate, ushort channels) => CreateFormat(sampleRate, channels, (ushort)(4 * channels), sampleRate * (ushort)(4 * channels), 32); + public Wave CreateIeeeFloatWave(uint sampleRate, ushort channels, ushort bits) => CreateFormat(sampleRate, channels, (ushort)(4 * channels), sampleRate * (ushort)(4 * channels), bits); + + public void AddSamples(Span buffer, int offset, int count) + { + if (Buffer == null) + { + Buffer = new byte[BufferLength]; + LockObject = new object(); + } + + if (WriteBuffer(buffer, offset, count) < count && !DiscardOnBufferOverflow) + { + throw new InvalidOperationException("The buffer is full and cannot be written to."); + } + } + + public int ReadBuffer(Span data, int offset, int count) + { + lock (LockObject!) + { + if (count > ByteCount) + { + count = ByteCount; + } + + int num = 0; + int num2 = Math.Min(Buffer!.Length - ReadPosition, count); + Array.Copy(Buffer, ReadPosition, data.ToArray(), offset, num2); + num += num2; + ReadPosition += num2; + ReadPosition %= Buffer.Length; + if (num < count) + { + Array.Copy(Buffer, ReadPosition, data.ToArray(), offset + num, count - num); + ReadPosition += count - num; + num = count; + } + + ByteCount -= num; + return num; + } + } + + public int WriteBuffer(Span data, int offset, int count) + { + lock (LockObject!) + { + int num = 0; + if (count > Buffer!.Length - ByteCount) + { + count = Buffer.Length - ByteCount; + } + + int num2 = Math.Min(Buffer.Length - WritePosition, count); + Array.Copy(data.ToArray(), offset, Buffer, WritePosition, num2); + WritePosition += num2; + WritePosition %= Buffer.Length; + num += num2; + if (num < count) + { + Array.Copy(data.ToArray(), offset + num, Buffer, WritePosition, count - num); + WritePosition += count - num; + num = count; + } + + ByteCount += num; + return num; + } + } + + 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) => WriteBytes(data, WaveEncoding.Pcm16); + + public Span WriteBytes(Span data, WaveEncoding encoding) + { + 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) => WriteBytes(data, WaveEncoding.Pcm16); + + public Span WriteBytes(Span data, WaveEncoding encoding) + { + 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) => WriteBytes(data, WaveEncoding.Pcm16); + + public Span WriteBytes(Span data, WaveEncoding encoding) + { + 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) => WriteBytes(data, WaveEncoding.Pcm16); + + public Span WriteBytes(Span data, WaveEncoding encoding) + { + // 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)); + 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 + 8]; + + var sw = new EndianBinaryWriter(new MemoryStream(samplerChunk)); + 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)); + 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)); + 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)); + 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)); + 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; + } + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs index fdee70e..a59e28e 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamEngine.cs @@ -8,7 +8,9 @@ public sealed class AlphaDreamEngine : Engine public override AlphaDreamConfig Config { get; } public override AlphaDreamMixer Mixer { get; } + public override AlphaDreamMixer_NAudio Mixer_NAudio { get; } public override AlphaDreamPlayer Player { get; } + public override bool UseNewMixer { get => false; } public AlphaDreamEngine(byte[] rom) { @@ -18,8 +20,16 @@ public AlphaDreamEngine(byte[] rom) } Config = new AlphaDreamConfig(rom); - Mixer = new AlphaDreamMixer(Config); - Player = new AlphaDreamPlayer(Config, Mixer); + if (Engine.Instance!.UseNewMixer) + { + Mixer = new AlphaDreamMixer(Config); + Player = new AlphaDreamPlayer(Config, Mixer); + } + else + { + Mixer_NAudio = new AlphaDreamMixer_NAudio(Config); + Player = new AlphaDreamPlayer(Config, Mixer_NAudio); + } AlphaDreamInstance = this; Instance = this; diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs index 1cc823c..bda9a0a 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer.cs @@ -1,5 +1,5 @@ using Kermalis.VGMusicStudio.Core.Util; -using NAudio.Wave; +using Kermalis.VGMusicStudio.Core.Formats; using System; namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; @@ -15,11 +15,9 @@ public sealed class AlphaDreamMixer : Mixer private float _fadeStepPerMicroframe; public readonly AlphaDreamConfig Config; - private readonly WaveBuffer _audio; + private readonly Audio _audio; private readonly float[][] _trackBuffers = new float[AlphaDreamPlayer.NUM_TRACKS][]; - private readonly BufferedWaveProvider _buffer; - - protected override WaveFormat WaveFormat => _buffer.WaveFormat; + private readonly Wave _buffer; internal AlphaDreamMixer(AlphaDreamConfig config) { @@ -30,16 +28,18 @@ internal AlphaDreamMixer(AlphaDreamConfig config) _samplesReciprocal = 1f / SamplesPerBuffer; int amt = SamplesPerBuffer * 2; - _audio = new WaveBuffer(amt * sizeof(float)) { FloatBufferCount = amt }; + _audio = new Audio(amt * sizeof(float)) { Float32BufferCount = amt }; for (int i = 0; i < AlphaDreamPlayer.NUM_TRACKS; i++) { _trackBuffers[i] = new float[amt]; } - _buffer = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, 2)) // TODO + _buffer = new Wave() { DiscardOnBufferOverflow = true, BufferLength = SamplesPerBuffer * 64 }; + _buffer.CreateIeeeFloatWave(sampleRate, 2); // TODO + Init(_buffer); } @@ -110,8 +110,8 @@ 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; + _audio.Float32Buffer![j * 2] += buf[j * 2] * level; + _audio.Float32Buffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; level += masterStep; } } diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer_NAudio.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer_NAudio.cs new file mode 100644 index 0000000..d98c64a --- /dev/null +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamMixer_NAudio.cs @@ -0,0 +1,127 @@ +using Kermalis.VGMusicStudio.Core.Util; +using NAudio.Wave; +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream; + +public sealed class AlphaDreamMixer_NAudio : Mixer_NAudio +{ + public readonly float SampleRateReciprocal; + private readonly float _samplesReciprocal; + public readonly int SamplesPerBuffer; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; + + public readonly AlphaDreamConfig Config; + private readonly WaveBuffer _audio; + private readonly float[][] _trackBuffers = new float[AlphaDreamPlayer.NUM_TRACKS][]; + private readonly BufferedWaveProvider _buffer; + + protected override WaveFormat WaveFormat => _buffer.WaveFormat; + + internal AlphaDreamMixer_NAudio(AlphaDreamConfig config) + { + Config = config; + const int sampleRate = 13_379; // TODO: Actual value unknown + SamplesPerBuffer = 224; // TODO + SampleRateReciprocal = 1f / sampleRate; + _samplesReciprocal = 1f / SamplesPerBuffer; + + int amt = SamplesPerBuffer * 2; + _audio = new WaveBuffer(amt * sizeof(float)) { FloatBufferCount = amt }; + for (int i = 0; i < AlphaDreamPlayer.NUM_TRACKS; i++) + { + _trackBuffers[i] = new float[amt]; + } + _buffer = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, 2)) // TODO + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64 + }; + Init(_buffer); + } + + internal void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * GBAUtils.AGB_FPS); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * GBAUtils.AGB_FPS); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal bool IsFading() + { + return _isFading; + } + internal bool IsFadeDone() + { + return _isFading && _fadeMicroFramesLeft == 0; + } + internal void ResetFade() + { + _isFading = false; + _fadeMicroFramesLeft = 0; + } + + internal void Process(AlphaDreamTrack[] tracks, bool output, bool recording) + { + _audio.Clear(); + float masterStep; + float masterLevel; + if (_isFading && _fadeMicroFramesLeft == 0) + { + masterStep = 0; + masterLevel = 0; + } + else + { + float fromMaster = 1f; + float toMaster = 1f; + if (_fadeMicroFramesLeft > 0) + { + const float scale = 10f / 6f; + fromMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadePos += _fadeStepPerMicroframe; + toMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadeMicroFramesLeft--; + } + masterStep = (toMaster - fromMaster) * _samplesReciprocal; + masterLevel = fromMaster; + } + for (int i = 0; i < AlphaDreamPlayer.NUM_TRACKS; i++) + { + AlphaDreamTrack track = tracks[i]; + if (!track.IsEnabled || track.NoteDuration == 0 || track.Channel.Stopped || Mutes[i]) + { + continue; + } + + float level = masterLevel; + float[] buf = _trackBuffers[i]; + Array.Clear(buf, 0, buf.Length); + track.Channel.Process(buf); + for (int j = 0; j < SamplesPerBuffer; j++) + { + _audio.FloatBuffer[j * 2] += buf[j * 2] * level; + _audio.FloatBuffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; + level += masterStep; + } + } + if (output) + { + _buffer.AddSamples(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + } + if (recording) + { + _waveWriter!.Write(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + } + } +} diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs index e202a38..124cda7 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamPlayer.cs @@ -12,6 +12,7 @@ public sealed class AlphaDreamPlayer : Player internal readonly AlphaDreamTrack[] Tracks; internal readonly AlphaDreamConfig Config; private readonly AlphaDreamMixer _mixer; + private readonly AlphaDreamMixer_NAudio _mixer_NAudio; private AlphaDreamLoadedSong? _loadedSong; internal byte Tempo; @@ -20,6 +21,7 @@ public sealed class AlphaDreamPlayer : Player public override ILoadedSong? LoadedSong => _loadedSong; protected override Mixer Mixer => _mixer; + protected override Mixer_NAudio Mixer_NAudio => _mixer_NAudio; internal AlphaDreamPlayer(AlphaDreamConfig config, AlphaDreamMixer mixer) : base(GBAUtils.AGB_FPS) @@ -34,6 +36,19 @@ internal AlphaDreamPlayer(AlphaDreamConfig config, AlphaDreamMixer mixer) } } + internal AlphaDreamPlayer(AlphaDreamConfig config, AlphaDreamMixer_NAudio mixer) + : base(GBAUtils.AGB_FPS) + { + Config = config; + _mixer_NAudio = mixer; + + Tracks = new AlphaDreamTrack[NUM_TRACKS]; + for (byte i = 0; i < NUM_TRACKS; i++) + { + Tracks[i] = new AlphaDreamTrack(i, mixer); + } + } + public override void LoadSong(int index) { if (_loadedSong is not null) @@ -70,7 +85,10 @@ internal override void InitEmulation() TempoStack = 0; _elapsedLoops = 0; ElapsedTicks = 0; - _mixer.ResetFade(); + if (Engine.Instance!.UseNewMixer) + _mixer.ResetFade(); + else + _mixer_NAudio.ResetFade(); for (int i = 0; i < NUM_TRACKS; i++) { Tracks[i].Init(); @@ -88,28 +106,56 @@ protected override void OnStopped() protected override bool Tick(bool playing, bool recording) { bool allDone = false; // TODO: Individual track tempo - while (!allDone && TempoStack >= 75) + if (Engine.Instance!.UseNewMixer) { - TempoStack -= 75; - allDone = true; - for (int i = 0; i < NUM_TRACKS; i++) + while (!allDone && TempoStack >= 75) { - AlphaDreamTrack track = Tracks[i]; - if (track.IsEnabled) + TempoStack -= 75; + allDone = true; + for (int i = 0; i < NUM_TRACKS; i++) { - TickTrack(track, ref allDone); + AlphaDreamTrack track = Tracks[i]; + if (track.IsEnabled) + { + TickTrack(track, ref allDone); + } + } + if (_mixer.IsFadeDone()) + { + allDone = true; } } - if (_mixer.IsFadeDone()) + if (!allDone) { - allDone = true; + TempoStack += Tempo; } + _mixer.Process(Tracks, playing, recording); } - if (!allDone) + else { - TempoStack += Tempo; + while (!allDone && TempoStack >= 75) + { + TempoStack -= 75; + allDone = true; + for (int i = 0; i < NUM_TRACKS; i++) + { + AlphaDreamTrack track = Tracks[i]; + if (track.IsEnabled) + { + TickTrack(track, ref allDone); + } + } + if (_mixer_NAudio.IsFadeDone()) + { + allDone = true; + } + } + if (!allDone) + { + TempoStack += Tempo; + } + _mixer_NAudio.Process(Tracks, playing, recording); } - _mixer.Process(Tracks, playing, recording); return allDone; } private void TickTrack(AlphaDreamTrack track, ref bool allDone) @@ -158,10 +204,21 @@ private void HandleTicksAndLoop(AlphaDreamLoadedSong s, AlphaDreamTrack track) } _elapsedLoops++; - UpdateElapsedTicksAfterLoop(s.Events[track.Index]!, track.DataOffset, track.Rest); - if (ShouldFadeOut && _elapsedLoops > NumLoops && !_mixer.IsFading()) + if (Engine.Instance!.UseNewMixer) { - _mixer.BeginFadeOut(); + UpdateElapsedTicksAfterLoop(s.Events[track.Index]!, track.DataOffset, track.Rest); + if (ShouldFadeOut && _elapsedLoops > NumLoops && !_mixer.IsFading()) + { + _mixer.BeginFadeOut(); + } + } + else + { + UpdateElapsedTicksAfterLoop(s.Events[track.Index]!, track.DataOffset, track.Rest); + if (ShouldFadeOut && _elapsedLoops > NumLoops && !_mixer_NAudio.IsFading()) + { + _mixer_NAudio.BeginFadeOut(); + } } } } diff --git a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamTrack.cs b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamTrack.cs index ded6a34..6dffcc4 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamTrack.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/AlphaDreamTrack.cs @@ -38,6 +38,20 @@ public AlphaDreamTrack(byte i, AlphaDreamMixer mixer) Channel = new AlphaDreamPCMChannel(mixer); } } + public AlphaDreamTrack(byte i, AlphaDreamMixer_NAudio mixer) + { + Index = i; + if (i >= 8) + { + Type = GBAUtils.PSGTypes[i & 3]; + Channel = new AlphaDreamSquareChannel(mixer); // TODO: PSG Channels 3 and 4 + } + else + { + Type = "PCM8"; + Channel = new AlphaDreamPCMChannel(mixer); + } + } // 0x819B040 public void Init() { diff --git a/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamChannel.cs b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamChannel.cs index 471fdb4..d566243 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamChannel.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamChannel.cs @@ -3,6 +3,7 @@ internal abstract class AlphaDreamChannel { protected readonly AlphaDreamMixer _mixer; + protected readonly AlphaDreamMixer_NAudio _mixer_NAudio; public EnvelopeState State; public byte Key; public bool Stopped; @@ -20,6 +21,10 @@ protected AlphaDreamChannel(AlphaDreamMixer mixer) { _mixer = mixer; } + protected AlphaDreamChannel(AlphaDreamMixer_NAudio mixer) + { + _mixer_NAudio = mixer; + } public ChannelVolume GetVolume() { diff --git a/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamPCMChannel.cs b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamPCMChannel.cs index 455dc77..81a740e 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamPCMChannel.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamPCMChannel.cs @@ -12,6 +12,10 @@ public AlphaDreamPCMChannel(AlphaDreamMixer mixer) : base(mixer) { // } + public AlphaDreamPCMChannel(AlphaDreamMixer_NAudio mixer) : base(mixer) + { + // + } public void Init(byte key, ADSR adsr, int sampleOffset, bool bFixed) { _velocity = adsr.A; @@ -20,7 +24,10 @@ public void Init(byte key, ADSR adsr, int sampleOffset, bool bFixed) Key = key; _adsr = adsr; - _sampleHeader = new SampleHeader(_mixer.Config.ROM, sampleOffset, out _sampleOffset); + if (Engine.Instance!.UseNewMixer) + _sampleHeader = new SampleHeader(_mixer.Config.ROM, sampleOffset, out _sampleOffset); + else + _sampleHeader = new SampleHeader(_mixer_NAudio.Config.ROM, sampleOffset, out _sampleOffset); _bFixed = bFixed; Stopped = false; } @@ -80,31 +87,63 @@ public override void Process(float[] buffer) StepEnvelope(); ChannelVolume vol = GetVolume(); - float interStep = (_bFixed ? _sampleHeader.SampleRate >> 10 : _frequency) * _mixer.SampleRateReciprocal; - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do + if (Engine.Instance!.UseNewMixer) { - float samp = (_mixer.Config.ROM[_pos + _sampleOffset] - 0x80) / (float)0x80; + float interStep = (_bFixed ? _sampleHeader.SampleRate >> 10 : _frequency) * _mixer.SampleRateReciprocal; + int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = (_mixer.Config.ROM[_pos + _sampleOffset] - 0x80) / (float)0x80; - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos += posDelta; - if (_pos >= _sampleHeader.Length) - { - if (_sampleHeader.DoesLoop == 0x40000000) + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos += posDelta; + if (_pos >= _sampleHeader.Length) { - _pos = _sampleHeader.LoopOffset; + if (_sampleHeader.DoesLoop == 0x40000000) + { + _pos = _sampleHeader.LoopOffset; + } + else + { + Stopped = true; + break; + } } - else + } while (--samplesPerBuffer > 0); + } + else + { + float interStep = (_bFixed ? _sampleHeader.SampleRate >> 10 : _frequency) * _mixer_NAudio.SampleRateReciprocal; + int bufPos = 0; int samplesPerBuffer = _mixer_NAudio.SamplesPerBuffer; + do + { + float samp = (_mixer_NAudio.Config.ROM[_pos + _sampleOffset] - 0x80) / (float)0x80; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos += posDelta; + if (_pos >= _sampleHeader.Length) { - Stopped = true; - break; + if (_sampleHeader.DoesLoop == 0x40000000) + { + _pos = _sampleHeader.LoopOffset; + } + else + { + Stopped = true; + break; + } } - } - } while (--samplesPerBuffer > 0); + } while (--samplesPerBuffer > 0); + } } } \ No newline at end of file diff --git a/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamSquareChannel.cs b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamSquareChannel.cs index a627bf0..9141b29 100644 --- a/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamSquareChannel.cs +++ b/VG Music Studio - Core/GBA/AlphaDream/Channels/AlphaDreamSquareChannel.cs @@ -12,6 +12,11 @@ public AlphaDreamSquareChannel(AlphaDreamMixer mixer) { _pat = null!; } + public AlphaDreamSquareChannel(AlphaDreamMixer_NAudio mixer) + : base(mixer) + { + _pat = null!; + } public void Init(byte key, ADSR env, byte vol, sbyte pan, int pitch) { _pat = MP2KUtils.SquareD50; // TODO: Which square pattern? @@ -77,9 +82,19 @@ 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; + if (Engine.Instance!.UseNewMixer) + { + interStep = _frequency * _mixer.SampleRateReciprocal; + samplesPerBuffer = _mixer.SamplesPerBuffer; + } + else + { + interStep = _frequency * _mixer_NAudio.SampleRateReciprocal; + samplesPerBuffer = _mixer_NAudio.SamplesPerBuffer; + } do { float samp = _pat[_pos]; diff --git a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KChannel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KChannel.cs index 3d35241..07b099c 100644 --- a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KChannel.cs +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KChannel.cs @@ -4,7 +4,8 @@ internal abstract class MP2KChannel { public EnvelopeState State; public MP2KTrack? Owner; - protected readonly MP2KMixer _mixer; + protected readonly MP2KMixer? _mixer; + protected readonly MP2KMixer_NAudio? _mixer_NAudio; public NoteInfo Note; protected ADSR _adsr; @@ -21,6 +22,12 @@ protected MP2KChannel(MP2KMixer mixer) State = EnvelopeState.Dead; } + protected MP2KChannel(MP2KMixer_NAudio mixer) + { + _mixer_NAudio = mixer; + State = EnvelopeState.Dead; + } + public abstract ChannelVolume GetVolume(); public abstract void SetVolume(byte vol, sbyte pan); public abstract void SetPitch(int pitch); diff --git a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KNoiseChannel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KNoiseChannel.cs index 4389352..8491f09 100644 --- a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KNoiseChannel.cs +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KNoiseChannel.cs @@ -12,6 +12,11 @@ public MP2KNoiseChannel(MP2KMixer mixer) { _pat = null!; } + public MP2KNoiseChannel(MP2KMixer_NAudio mixer) + : base(mixer) + { + _pat = null!; + } public void Init(MP2KTrack owner, NoteInfo note, ADSR env, int instPan, NoisePattern pattern) { Init(owner, note, env, instPan); @@ -49,10 +54,20 @@ 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; + if (Engine.Instance!.UseNewMixer) + { + interStep = _frequency * _mixer!.SampleRateReciprocal; + samplesPerBuffer = _mixer!.SamplesPerBuffer; + } + else + { + interStep = _frequency * _mixer_NAudio!.SampleRateReciprocal; + samplesPerBuffer = _mixer_NAudio!.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 90ba63b..73b50f1 100644 --- a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM4Channel.cs +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM4Channel.cs @@ -11,10 +11,18 @@ public MP2KPCM4Channel(MP2KMixer mixer) { _sample = new float[0x20]; } + public MP2KPCM4Channel(MP2KMixer_NAudio mixer) + : base(mixer) + { + _sample = new float[0x20]; + } public void Init(MP2KTrack owner, NoteInfo note, ADSR env, int instPan, int sampleOffset) { Init(owner, note, env, instPan); - MP2KUtils.PCM4ToFloat(_mixer.Config.ROM.AsSpan(sampleOffset), _sample); + if (Engine.Instance!.UseNewMixer) + MP2KUtils.PCM4ToFloat(_mixer!.Config.ROM.AsSpan(sampleOffset), _sample); + else + MP2KUtils.PCM4ToFloat(_mixer_NAudio!.Config.ROM.AsSpan(sampleOffset), _sample); } public override void SetPitch(int pitch) @@ -31,10 +39,20 @@ 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; + if (Engine.Instance!.UseNewMixer) + { + interStep = _frequency * _mixer!.SampleRateReciprocal; + samplesPerBuffer = _mixer!.SamplesPerBuffer; + } + else + { + interStep = _frequency * _mixer_NAudio!.SampleRateReciprocal; + samplesPerBuffer = _mixer_NAudio!.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 552e230..fedb7f3 100644 --- a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM8Channel.cs +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPCM8Channel.cs @@ -19,6 +19,11 @@ public MP2KPCM8Channel(MP2KMixer mixer) { // } + public MP2KPCM8Channel(MP2KMixer_NAudio mixer) + : base(mixer) + { + // + } public void Init(MP2KTrack owner, NoteInfo note, ADSR adsr, int sampleOffset, byte vol, sbyte pan, int instPan, int pitch, bool bFixed, bool bCompressed) { State = EnvelopeState.Initializing; @@ -33,12 +38,19 @@ public void Init(MP2KTrack owner, NoteInfo note, ADSR adsr, int sampleOffset, by Note = note; _adsr = adsr; _instPan = instPan; - byte[] rom = _mixer.Config.ROM; + byte[] rom; + if (Engine.Instance!.UseNewMixer) + rom = _mixer!.Config.ROM; + else + rom = _mixer_NAudio!.Config.ROM; _sampleHeader = SampleHeader.Get(rom, sampleOffset, out _sampleOffset); _bFixed = bFixed; _bCompressed = bCompressed; _decompressedSample = bCompressed ? MP2KUtils.Decompress(rom.AsSpan(_sampleOffset), _sampleHeader.Length) : null; - _bGoldenSun = _mixer.Config.HasGoldenSunSynths && _sampleHeader.Length == 0 && _sampleHeader.DoesLoop == SampleHeader.LOOP_TRUE && _sampleHeader.LoopOffset == 0; + if (Engine.Instance!.UseNewMixer) + _bGoldenSun = _mixer!.Config.HasGoldenSunSynths && _sampleHeader.Length == 0 && _sampleHeader.DoesLoop == SampleHeader.LOOP_TRUE && _sampleHeader.LoopOffset == 0; + else + _bGoldenSun = _mixer_NAudio!.Config.HasGoldenSunSynths && _sampleHeader.Length == 0 && _sampleHeader.DoesLoop == SampleHeader.LOOP_TRUE && _sampleHeader.LoopOffset == 0; if (_bGoldenSun) { _gsPSG = GoldenSunPSG.Get(rom.AsSpan(_sampleOffset)); @@ -50,11 +62,22 @@ public void Init(MP2KTrack owner, NoteInfo note, ADSR adsr, int sampleOffset, by public override ChannelVolume GetVolume() { const float MAX = 0x10_000; - return new ChannelVolume + if (Engine.Instance!.UseNewMixer) + { + return new ChannelVolume + { + LeftVol = _leftVol * _velocity / MAX * _mixer!.PCM8MasterVolume, + RightVol = _rightVol * _velocity / MAX * _mixer!.PCM8MasterVolume + }; + } + else { - LeftVol = _leftVol * _velocity / MAX * _mixer.PCM8MasterVolume, - RightVol = _rightVol * _velocity / MAX * _mixer.PCM8MasterVolume - }; + return new ChannelVolume + { + LeftVol = _leftVol * _velocity / MAX * _mixer_NAudio!.PCM8MasterVolume, + RightVol = _rightVol * _velocity / MAX * _mixer_NAudio!.PCM8MasterVolume + }; + } } public override void SetVolume(byte vol, sbyte pan) { @@ -153,7 +176,11 @@ public override void Process(float[] buffer) } ChannelVolume vol = GetVolume(); - float interStep = _bFixed && !_bGoldenSun ? _mixer.SampleRate * _mixer.SampleRateReciprocal : _frequency * _mixer.SampleRateReciprocal; + float interStep; + if (Engine.Instance!.UseNewMixer) + interStep = _bFixed && !_bGoldenSun ? _mixer!.SampleRate * _mixer!.SampleRateReciprocal : _frequency * _mixer!.SampleRateReciprocal; + else + interStep = _bFixed && !_bGoldenSun ? _mixer_NAudio!.SampleRate * _mixer_NAudio!.SampleRateReciprocal : _frequency * _mixer_NAudio!.SampleRateReciprocal; if (_bGoldenSun) // Most Golden Sun processing is thanks to ipatix { Process_GS(buffer, vol, interStep); @@ -164,7 +191,10 @@ public override void Process(float[] buffer) } else { - Process_Standard(buffer, vol, interStep, _mixer.Config.ROM); + if (Engine.Instance!.UseNewMixer) + Process_Standard(buffer, vol, interStep, _mixer!.Config.ROM); + else + Process_Standard(buffer, vol, interStep, _mixer_NAudio!.Config.ROM); } } private void Process_GS(float[] buffer, ChannelVolume vol, float interStep) @@ -173,79 +203,95 @@ private void Process_GS(float[] buffer, ChannelVolume vol, float interStep) 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; + if (Engine.Instance!.UseNewMixer) + samplesPerBuffer = _mixer!.SamplesPerBuffer; + else + samplesPerBuffer = _mixer_NAudio!.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; + if (Engine.Instance!.UseNewMixer) + samplesPerBuffer = _mixer!.SamplesPerBuffer; + else + samplesPerBuffer = _mixer_NAudio!.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; + if (Engine.Instance!.UseNewMixer) + samplesPerBuffer = _mixer!.SamplesPerBuffer; + else + samplesPerBuffer = _mixer_NAudio!.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) { int bufPos = 0; - int samplesPerBuffer = _mixer.SamplesPerBuffer; + int samplesPerBuffer; + if (Engine.Instance!.UseNewMixer) + samplesPerBuffer = _mixer!.SamplesPerBuffer; + else + samplesPerBuffer = _mixer_NAudio!.SamplesPerBuffer; do { float samp = _decompressedSample![_pos] / (float)0x80; @@ -267,7 +313,11 @@ private void Process_Compressed(float[] buffer, ChannelVolume vol, float interSt private void Process_Standard(float[] buffer, ChannelVolume vol, float interStep, byte[] rom) { int bufPos = 0; - int samplesPerBuffer = _mixer.SamplesPerBuffer; + int samplesPerBuffer; + if (Engine.Instance!.UseNewMixer) + samplesPerBuffer = _mixer!.SamplesPerBuffer; + else + samplesPerBuffer = _mixer_NAudio!.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 bc795df..939c174 100644 --- a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPSGChannel.cs +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KPSGChannel.cs @@ -20,6 +20,11 @@ public MP2KPSGChannel(MP2KMixer mixer) { // } + public MP2KPSGChannel(MP2KMixer_NAudio mixer) + : base(mixer) + { + // + } protected void Init(MP2KTrack owner, NoteInfo note, ADSR env, int instPan) { State = EnvelopeState.Initializing; diff --git a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KSquareChannel.cs b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KSquareChannel.cs index 4e655ff..74ea7e7 100644 --- a/VG Music Studio - Core/GBA/MP2K/Channels/MP2KSquareChannel.cs +++ b/VG Music Studio - Core/GBA/MP2K/Channels/MP2KSquareChannel.cs @@ -11,6 +11,11 @@ public MP2KSquareChannel(MP2KMixer mixer) { _pat = null!; } + public MP2KSquareChannel(MP2KMixer_NAudio mixer) + : base(mixer) + { + _pat = null!; + } public void Init(MP2KTrack owner, NoteInfo note, ADSR env, int instPan, SquarePattern pattern) { Init(owner, note, env, instPan); @@ -37,10 +42,20 @@ 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; + if (Engine.Instance!.UseNewMixer) + { + interStep = _frequency * _mixer!.SampleRateReciprocal; + samplesPerBuffer = _mixer!.SamplesPerBuffer; + } + else + { + interStep = _frequency * _mixer_NAudio!.SampleRateReciprocal; + samplesPerBuffer = _mixer_NAudio!.SamplesPerBuffer; + } do { float samp = _pat[_pos]; diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs b/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs index 43c40ba..0f8c5ab 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KEngine.cs @@ -4,30 +4,41 @@ namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; public sealed class MP2KEngine : Engine { - public static MP2KEngine? MP2KInstance { get; private set; } + public static MP2KEngine? MP2KInstance { get; private set; } - public override MP2KConfig Config { get; } - public override MP2KMixer Mixer { get; } - public override MP2KPlayer Player { get; } + public override MP2KConfig Config { get; } + public override MP2KMixer Mixer { get; } + public override MP2KMixer_NAudio Mixer_NAudio { get; } + public override MP2KPlayer Player { get; } + public override bool UseNewMixer { get => true; } - 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."); - } + 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); + Config = new MP2KConfig(rom); + if (UseNewMixer) + { + Mixer = new MP2KMixer(Config); + Player = new MP2KPlayer(Config, Mixer); + } + else + { + Mixer = new MP2KMixer(); + Mixer_NAudio = new MP2KMixer_NAudio(Config); + Player = new MP2KPlayer(Config, Mixer_NAudio); + } + + MP2KInstance = this; + Instance = this; + } - MP2KInstance = this; - Instance = this; - } - - public override void Dispose() - { - base.Dispose(); - MP2KInstance = null; - } + public override void Dispose() + { + base.Dispose(); + MP2KInstance = null; + } } diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Runtime.cs b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Runtime.cs index b3c038c..c11ee31 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Runtime.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KLoadedSong_Runtime.cs @@ -57,36 +57,72 @@ 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); + if (Engine.Instance!.UseNewMixer) + { + _player.MMixer.AllocPCM8Channel(track, v.ADSR, ni, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + bFixed, bCompressed, v.Int4 - GBAUtils.CARTRIDGE_OFFSET); + } + else + { + _player.MMixer_NAudio.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; - } + { + if (Engine.Instance!.UseNewMixer) + { + _player.MMixer.AllocPSGChannel(track, v.ADSR, ni, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + type, (SquarePattern)v.Int4); + } + else + { + _player.MMixer_NAudio.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; - } + { + if (Engine.Instance!.UseNewMixer) + { + _player.MMixer.AllocPSGChannel(track, v.ADSR, ni, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + type, v.Int4 - GBAUtils.CARTRIDGE_OFFSET); + } + else + { + _player.MMixer_NAudio.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; - } + { + if (Engine.Instance!.UseNewMixer) + { + _player.MMixer.AllocPSGChannel(track, v.ADSR, ni, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + type, (NoisePattern)v.Int4); + } + else + { + _player.MMixer_NAudio.AllocPSGChannel(track, v.ADSR, ni, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + type, (NoisePattern)v.Int4); + } + return; + } } return; // Prevent infinite loop with invalid instruments } diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs b/VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs index 8997f70..422d8b0 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KMixer.cs @@ -1,5 +1,5 @@ -using Kermalis.VGMusicStudio.Core.Util; -using NAudio.Wave; +using Kermalis.VGMusicStudio.Core.Formats; +using Kermalis.VGMusicStudio.Core.Util; using System; using System.Linq; @@ -7,259 +7,262 @@ 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 readonly int SamplesPerBuffer; + internal readonly float SampleRateReciprocal; + private readonly float _samplesReciprocal; + internal readonly float PCM8MasterVolume; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; - internal readonly MP2KConfig Config; - private readonly WaveBuffer _audio; - private readonly float[][] _trackBuffers; - private readonly MP2KPCM8Channel[] _pcm8Channels; - private readonly MP2KSquareChannel _sq1; - private readonly MP2KSquareChannel _sq2; - private readonly MP2KPCM4Channel _pcm4; - private readonly MP2KNoiseChannel _noise; - private readonly MP2KPSGChannel[] _psgChannels; - private readonly BufferedWaveProvider _buffer; + internal readonly MP2KConfig Config; + private readonly Audio _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 Wave _buffer; - protected override WaveFormat WaveFormat => _buffer.WaveFormat; + internal MP2KMixer() { } - internal MP2KMixer(MP2KConfig config) - { - Config = config; - (SampleRate, SamplesPerBuffer) = MP2KUtils.FrequencyTable[config.SampleRate]; - SampleRateReciprocal = 1f / SampleRate; - _samplesReciprocal = 1f / SamplesPerBuffer; - PCM8MasterVolume = config.Volume / 15f; + internal MP2KMixer(MP2KConfig config) + { + Config = config; + (SampleRate, SamplesPerBuffer) = MP2KUtils.FrequencyTable[config.SampleRate]; + SampleRateReciprocal = 1f / SampleRate; + _samplesReciprocal = 1f / SamplesPerBuffer; + PCM8MasterVolume = config.Volume / 15f; - _pcm8Channels = new MP2KPCM8Channel[24]; - for (int i = 0; i < _pcm8Channels.Length; i++) - { - _pcm8Channels[i] = new MP2KPCM8Channel(this); - } - _psgChannels = new MP2KPSGChannel[4] { _sq1 = new MP2KSquareChannel(this), _sq2 = new MP2KSquareChannel(this), _pcm4 = new MP2KPCM4Channel(this), _noise = new MP2KNoiseChannel(this), }; + _pcm8Channels = new MP2KPCM8Channel[24]; + for (int i = 0; i < _pcm8Channels.Length; i++) + { + _pcm8Channels[i] = new MP2KPCM8Channel(this); + } + _psgChannels = new MP2KPSGChannel[4] { _sq1 = new MP2KSquareChannel(this), _sq2 = new MP2KSquareChannel(this), _pcm4 = new MP2KPCM4Channel(this), _noise = new MP2KNoiseChannel(this), }; - int amt = SamplesPerBuffer * 2; - _audio = new WaveBuffer(amt * sizeof(float)) { FloatBufferCount = amt }; - _trackBuffers = new float[0x10][]; - for (int i = 0; i < _trackBuffers.Length; i++) - { - _trackBuffers[i] = new float[amt]; - } - _buffer = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(SampleRate, 2)) - { - DiscardOnBufferOverflow = true, - BufferLength = SamplesPerBuffer * 64, - }; - Init(_buffer); - } + int amt = SamplesPerBuffer * 2; + Instance = this; + _audio = new Audio(amt * sizeof(float)) { Float32BufferCount = amt }; + _trackBuffers = new float[0x10][]; + for (int i = 0; i < _trackBuffers.Length; i++) + { + _trackBuffers[i] = new float[amt]; + } + _buffer = new Wave() + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64, + }; + _buffer.CreateIeeeFloatWave((uint)SampleRate, 2); - 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; - } + Init(_buffer, _audio); + } - 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 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 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(); + 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; + } - for (int i = 0; i < _pcm8Channels.Length; i++) - { - MP2KPCM8Channel c = _pcm8Channels[i]; - if (c.Owner is not null) - { - c.Process(_trackBuffers[c.Owner.Index]); - } - } + 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 < _psgChannels.Length; i++) - { - MP2KPSGChannel c = _psgChannels[i]; - if (c.Owner is not null) - { - c.Process(_trackBuffers[c.Owner.Index]); - } - } + for (int i = 0; i < _pcm8Channels.Length; i++) + { + MP2KPCM8Channel c = _pcm8Channels[i]; + if (c.Owner is not null) + { + c.Process(_trackBuffers[c.Owner.Index]); + } + } - float masterStep; - float masterLevel; - if (_isFading && _fadeMicroFramesLeft == 0) - { - masterStep = 0; - masterLevel = 0; - } - else - { - float fromMaster = 1f; - float toMaster = 1f; - if (_fadeMicroFramesLeft > 0) - { - const float scale = 10f / 6f; - fromMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); - _fadePos += _fadeStepPerMicroframe; - toMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); - _fadeMicroFramesLeft--; - } - masterStep = (toMaster - fromMaster) * _samplesReciprocal; - masterLevel = fromMaster; - } - for (int i = 0; i < _trackBuffers.Length; i++) - { - if (Mutes[i]) - { - continue; - } + for (int i = 0; i < _psgChannels.Length; i++) + { + MP2KPSGChannel c = _psgChannels[i]; + if (c.Owner is not null) + { + c.Process(_trackBuffers[c.Owner.Index]); + } + } - float 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 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.Float32Buffer![j * 2] += buf[j * 2] * level; + _audio.Float32Buffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; + level += masterStep; + } + } + if (output) + { + _buffer.AddSamples(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + } + if (recording) + { + _waveWriter!.Write(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + } + } } diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KMixer_NAudio.cs b/VG Music Studio - Core/GBA/MP2K/MP2KMixer_NAudio.cs new file mode 100644 index 0000000..871c53f --- /dev/null +++ b/VG Music Studio - Core/GBA/MP2K/MP2KMixer_NAudio.cs @@ -0,0 +1,265 @@ +using Kermalis.VGMusicStudio.Core.Util; +using NAudio.Wave; +using System; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K; + +public sealed class MP2KMixer_NAudio : Mixer_NAudio +{ + internal readonly int SampleRate; + internal readonly int SamplesPerBuffer; + internal readonly float SampleRateReciprocal; + private readonly float _samplesReciprocal; + internal readonly float PCM8MasterVolume; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; + + internal readonly MP2KConfig Config; + private readonly WaveBuffer _audio; + private readonly float[][] _trackBuffers; + private readonly MP2KPCM8Channel[] _pcm8Channels; + private readonly MP2KSquareChannel _sq1; + private readonly MP2KSquareChannel _sq2; + private readonly MP2KPCM4Channel _pcm4; + private readonly MP2KNoiseChannel _noise; + private readonly MP2KPSGChannel[] _psgChannels; + private readonly BufferedWaveProvider _buffer; + + protected override WaveFormat WaveFormat => _buffer.WaveFormat; + + internal MP2KMixer_NAudio(MP2KConfig config) + { + Config = config; + (SampleRate, SamplesPerBuffer) = MP2KUtils.FrequencyTable[config.SampleRate]; + SampleRateReciprocal = 1f / SampleRate; + _samplesReciprocal = 1f / SamplesPerBuffer; + PCM8MasterVolume = config.Volume / 15f; + + _pcm8Channels = new MP2KPCM8Channel[24]; + for (int i = 0; i < _pcm8Channels.Length; i++) + { + _pcm8Channels[i] = new MP2KPCM8Channel(this); + } + _psgChannels = new MP2KPSGChannel[4] { _sq1 = new MP2KSquareChannel(this), _sq2 = new MP2KSquareChannel(this), _pcm4 = new MP2KPCM4Channel(this), _noise = new MP2KNoiseChannel(this), }; + + int amt = SamplesPerBuffer * 2; + _audio = new WaveBuffer(amt * sizeof(float)) { FloatBufferCount = amt }; + _trackBuffers = new float[0x10][]; + for (int i = 0; i < _trackBuffers.Length; i++) + { + _trackBuffers[i] = new float[amt]; + } + _buffer = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(SampleRate, 2)) + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64, + }; + Init(_buffer); + } + + internal MP2KPCM8Channel? AllocPCM8Channel(MP2KTrack owner, ADSR env, NoteInfo note, byte vol, sbyte pan, int instPan, int pitch, bool bFixed, bool bCompressed, int sampleOffset) + { + MP2KPCM8Channel? nChn = null; + IOrderedEnumerable byOwner = _pcm8Channels.OrderByDescending(c => c.Owner is null ? 0xFF : c.Owner.Index); + foreach (MP2KPCM8Channel i in byOwner) // Find free + { + if (i.State == EnvelopeState.Dead || i.Owner is null) + { + nChn = i; + break; + } + } + if (nChn is null) // Find releasing + { + foreach (MP2KPCM8Channel i in byOwner) + { + if (i.State == EnvelopeState.Releasing) + { + nChn = i; + break; + } + } + } + if (nChn is null) // Find prioritized + { + foreach (MP2KPCM8Channel i in byOwner) + { + if (owner.Priority > i.Owner!.Priority) + { + nChn = i; + break; + } + } + } + if (nChn is null) // None available + { + MP2KPCM8Channel lowest = byOwner.First(); // Kill lowest track's instrument if the track is lower than this one + if (lowest.Owner!.Index >= owner.Index) + { + nChn = lowest; + } + } + if (nChn is not null) // Could still be null from the above if + { + nChn.Init(owner, note, env, sampleOffset, vol, pan, instPan, pitch, bFixed, bCompressed); + } + return nChn; + } + internal MP2KPSGChannel? AllocPSGChannel(MP2KTrack owner, ADSR env, NoteInfo note, byte vol, sbyte pan, int instPan, int pitch, VoiceType type, object arg) + { + MP2KPSGChannel nChn; + switch (type) + { + case VoiceType.Square1: + { + nChn = _sq1; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) + { + return null; + } + _sq1.Init(owner, note, env, instPan, (SquarePattern)arg); + break; + } + case VoiceType.Square2: + { + nChn = _sq2; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) + { + return null; + } + _sq2.Init(owner, note, env, instPan, (SquarePattern)arg); + break; + } + case VoiceType.PCM4: + { + nChn = _pcm4; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) + { + return null; + } + _pcm4.Init(owner, note, env, instPan, (int)arg); + break; + } + case VoiceType.Noise: + { + nChn = _noise; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner!.Index < owner.Index) + { + return null; + } + _noise.Init(owner, note, env, instPan, (NoisePattern)arg); + break; + } + default: return null; + } + nChn.SetVolume(vol, pan); + nChn.SetPitch(pitch); + return nChn; + } + + internal void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * GBAUtils.AGB_FPS); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * GBAUtils.AGB_FPS); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal bool IsFading() + { + return _isFading; + } + internal bool IsFadeDone() + { + return _isFading && _fadeMicroFramesLeft == 0; + } + internal void ResetFade() + { + _isFading = false; + _fadeMicroFramesLeft = 0; + } + + internal void Process(bool output, bool recording) + { + for (int i = 0; i < _trackBuffers.Length; i++) + { + float[] buf = _trackBuffers[i]; + Array.Clear(buf, 0, buf.Length); + } + _audio.Clear(); + + for (int i = 0; i < _pcm8Channels.Length; i++) + { + MP2KPCM8Channel c = _pcm8Channels[i]; + if (c.Owner is not null) + { + c.Process(_trackBuffers[c.Owner.Index]); + } + } + + for (int i = 0; i < _psgChannels.Length; i++) + { + MP2KPSGChannel c = _psgChannels[i]; + if (c.Owner is not null) + { + c.Process(_trackBuffers[c.Owner.Index]); + } + } + + float masterStep; + float masterLevel; + if (_isFading && _fadeMicroFramesLeft == 0) + { + masterStep = 0; + masterLevel = 0; + } + else + { + float fromMaster = 1f; + float toMaster = 1f; + if (_fadeMicroFramesLeft > 0) + { + const float scale = 10f / 6f; + fromMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadePos += _fadeStepPerMicroframe; + toMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadeMicroFramesLeft--; + } + masterStep = (toMaster - fromMaster) * _samplesReciprocal; + masterLevel = fromMaster; + } + for (int i = 0; i < _trackBuffers.Length; i++) + { + if (Mutes[i]) + { + continue; + } + + float level = masterLevel; + float[] buf = _trackBuffers[i]; + for (int j = 0; j < SamplesPerBuffer; j++) + { + _audio.FloatBuffer[j * 2] += buf[j * 2] * level; + _audio.FloatBuffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; + level += masterStep; + } + } + if (output) + { + _buffer.AddSamples(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + } + if (recording) + { + _waveWriter!.Write(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + } + } +} diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs b/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs index 7ebe510..57e773b 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KPlayer.cs @@ -7,6 +7,7 @@ public sealed partial class MP2KPlayer : Player private readonly string?[] _voiceTypeCache; internal readonly MP2KConfig Config; internal readonly MP2KMixer MMixer; + internal readonly MP2KMixer_NAudio MMixer_NAudio; private MP2KLoadedSong? _loadedSong; internal ushort Tempo; @@ -17,7 +18,8 @@ public sealed partial class MP2KPlayer : Player public override ILoadedSong? LoadedSong => _loadedSong; protected override Mixer Mixer => MMixer; - + protected override Mixer_NAudio Mixer_NAudio => MMixer_NAudio; + internal MP2KPlayer(MP2KConfig config, MP2KMixer mixer) : base(GBAUtils.AGB_FPS) { @@ -26,6 +28,14 @@ internal MP2KPlayer(MP2KConfig config, MP2KMixer mixer) _voiceTypeCache = new string[256]; } + internal MP2KPlayer(MP2KConfig config, MP2KMixer_NAudio mixer) + : base(GBAUtils.AGB_FPS) + { + Config = config; + MMixer_NAudio = mixer; + + _voiceTypeCache = new string[256]; + } public override void LoadSong(int index) { @@ -56,7 +66,10 @@ internal override void InitEmulation() TempoStack = 0; _elapsedLoops = 0; ElapsedTicks = 0; - MMixer.ResetFade(); + if (Engine.Instance!.UseNewMixer) + MMixer.ResetFade(); + else + MMixer_NAudio.ResetFade(); MP2KTrack[] tracks = _loadedSong!.Tracks; for (int i = 0; i < tracks.Length; i++) { @@ -81,24 +94,48 @@ protected override bool Tick(bool playing, bool recording) MP2KLoadedSong s = _loadedSong!; bool allDone = false; - while (!allDone && TempoStack >= 150) + if (Engine.Instance!.UseNewMixer) { - TempoStack -= 150; - allDone = true; - for (int i = 0; i < s.Tracks.Length; i++) + while (!allDone && TempoStack >= 150) { - TickTrack(s, s.Tracks[i], ref allDone); + 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 (MMixer.IsFadeDone()) + if (!allDone) { - allDone = true; + TempoStack += Tempo; } + MMixer.Process(playing, recording); } - if (!allDone) + else { - TempoStack += Tempo; + 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_NAudio.IsFadeDone()) + { + allDone = true; + } + } + if (!allDone) + { + TempoStack += Tempo; + } + MMixer_NAudio.Process(playing, recording); } - MMixer.Process(playing, recording); return allDone; } private void TickTrack(MP2KLoadedSong s, MP2KTrack track, ref bool allDone) @@ -142,9 +179,19 @@ private void HandleTicksAndLoop(MP2KLoadedSong s, MP2KTrack track) _elapsedLoops++; UpdateElapsedTicksAfterLoop(s.Events[track.Index], track.DataOffset, track.Rest); - if (ShouldFadeOut && _elapsedLoops > NumLoops && !MMixer.IsFading()) + if (Engine.Instance!.UseNewMixer) + { + if (ShouldFadeOut && _elapsedLoops > NumLoops && !MMixer.IsFading()) + { + MMixer.BeginFadeOut(); + } + } + else { - MMixer.BeginFadeOut(); + if (ShouldFadeOut && _elapsedLoops > NumLoops && !MMixer_NAudio.IsFading()) + { + MMixer_NAudio.BeginFadeOut(); + } } } diff --git a/VG Music Studio - Core/GBA/MP2K/MP2KTrack.cs b/VG Music Studio - Core/GBA/MP2K/MP2KTrack.cs index e612465..0ec7376 100644 --- a/VG Music Studio - Core/GBA/MP2K/MP2KTrack.cs +++ b/VG Music Studio - Core/GBA/MP2K/MP2KTrack.cs @@ -202,18 +202,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) + if (c is not null) { - tin.Keys[numKeys++] = c.Note.OriginalNote; - } - ChannelVolume vol = c.GetVolume(); - if (vol.LeftVol > left) - { - left = vol.LeftVol; - } - if (vol.RightVol > right) - { - 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/LowLatencyRingbuffer.cs b/VG Music Studio - Core/LowLatencyRingbuffer.cs new file mode 100644 index 0000000..0fe2d65 --- /dev/null +++ b/VG Music Studio - Core/LowLatencyRingbuffer.cs @@ -0,0 +1,185 @@ +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 System.Threading.Mutex? mtx = new(); + private int? mtxOwner; + 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; + + public LowLatencyRingbuffer() + { + Reset(); + } + + public void Reset() + { + lock (mtx!) + { + dataCount = 0; + dataPos = 0; + freeCount = buffer!.Count; + freePos = 0; + } + } + + public void SetNumBuffers(int numBuffers) + { + lock (mtx!) + { + if (numBuffers is 0) + numBuffers = 1; + this.numBuffers = numBuffers; + } + } + + public void Put(Span inBuffer) + { + lastPut = inBuffer.Length; + + lock (mtx!) + { + int bufferedNumBuffers = numBuffers; + int bufferedLastTake = lastTake; + int requiredBufferSize = bufferedNumBuffers * bufferedLastTake + lastPut; + + if (buffer!.Count < 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); + 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 - 1); + + dataPos = 0; + freeCount = buffer.Count - dataCount; + freePos = dataCount; + } + + 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; + + lock (mtx!) + { + mtxOwner = Thread.CurrentThread.ManagedThreadId; + + if (mtxOwner == Thread.CurrentThread.ManagedThreadId || outBuffer.Length > dataCount) + { + Array.Fill(outBuffer.ToArray(), new Sample{left = 0.0f, right = 0.0f}, 0, outBuffer.Length - 1); + return; + } + + while (outBuffer.Length > 0) + { + int elementsTaken = TakeSome(outBuffer); + outBuffer = outBuffer.Slice(elementsTaken); + } + + Monitor.Exit(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; + } +} \ No newline at end of file diff --git a/VG Music Studio - Core/MP2K.yaml b/VG Music Studio - Core/MP2K.yaml index 7235295..d73128c 100644 --- a/VG Music Studio - Core/MP2K.yaml +++ b/VG Music Studio - Core/MP2K.yaml @@ -1,3 +1,21 @@ +A2NE_00: + Name: "Sonic Advance 2 (USA)" + SongTableOffsets: 0xAD4F4C + SongTableSizes: 507 + SampleRate: 2 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +A2NP_00: + Name: "Sonic Advance 2 (Europe)" + SongTableOffsets: 0xAD4F4C + Copy: "A2NE_00" +A2NJ_00: + Name: "Sonic Advance 2 (Japan)" + SongTableOffsets: 0xAD4B14 + Copy: "A2NE_00" A2UJ_00: Name: "Mother 1 + 2 (Japan)" SongTableOffsets: 0x10B530 diff --git a/VG Music Studio - Core/Mixer.cs b/VG Music Studio - Core/Mixer.cs index 7b8d4c5..b3ea375 100644 --- a/VG Music Studio - Core/Mixer.cs +++ b/VG Music Studio - Core/Mixer.cs @@ -1,109 +1,340 @@ -using NAudio.CoreAudioApi; -using NAudio.CoreAudioApi.Interfaces; -using NAudio.Wave; +using PortAudio; using System; +using System.Runtime.InteropServices; +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Core.Formats; +using Stream = PortAudio.Stream; namespace Kermalis.VGMusicStudio.Core; -public abstract class Mixer : IAudioSessionEventsHandler, IDisposable +public abstract class Mixer : 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 static event Action? VolumeChanged; + + public Wave? WaveData; + public EndianBinaryReader? Reader; + public float[] Buffer; + + public readonly bool[] Mutes; + public int SizeInBytes; + public uint FramesPerBuffer; + public int SizeToAllocateInBytes; + public long FinalFrameSize; + private float Vol = 1; + + public readonly object CountLock = new object(); + + protected Wave? _waveWriter; + + public StreamParameters OParams; + public StreamParameters DefaultOutputParams { get; private set; } + + public Stream? Stream; + private bool IsDisposed = false; + + public static Mixer? Instance { get; set; } + + protected Mixer() + { + Mutes = new bool[SongState.MAX_TRACKS]; + Buffer = null!; + } + + protected void Init(Wave waveData) => Init(waveData, new Audio(SizeToAllocateInBytes), SampleFormat.Float32); + protected void Init(Wave waveData, Audio audioData) => Init(waveData, audioData, SampleFormat.Float32); + protected void Init(Wave waveData, Audio audioData, SampleFormat sampleFormat) + { + // First, check if the instance contains something + if (WaveData == null) + { + IsDisposed = false; + + Pa.Initialize(); + WaveData = waveData; + + // 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.channelCount = 2; + OParams.sampleFormat = sampleFormat; + OParams.suggestedLatency = Pa.GetDeviceInfo(OParams.device).defaultLowOutputLatency; + OParams.hostApiSpecificStreamInfo = IntPtr.Zero; + + // Set it as a the default + DefaultOutputParams = OParams; + } + + Instance!.Stream = new Stream( + null, + OParams, + WaveData!.SampleRate, + FramesPerBuffer, + StreamFlags.ClipOff, + Player.PlayCallback, + audioData + ); + + FinalFrameSize = FramesPerBuffer * 2; + Buffer = new Span(audioData.Float32Buffer).ToArray(); + } + + private int ProcessFrame(Span output, Span buffer, int framesPerBuffer) + { + float counter = 0; + + counter += framesPerBuffer; + while (counter >= Instance!.FramesPerBuffer) + { + counter -= Instance.FramesPerBuffer; + } + + framesPerBuffer = (int)(Instance.FramesPerBuffer * 2); + float[] outBuffer = buffer.ToArray(); + + float[] outBuf = output.ToArray(); + for (int i = 0; i < framesPerBuffer; i++) + { + outBuf[i] = outBuffer[i]; + } + + return 1; + } + + public float Volume + { + get => Vol; + set => Vol = Math.Clamp(value, 0, 1); + } + + public float GetVolume() + { + return Vol; + } + + public void SetVolume(float volume) + { + if (!Engine.Instance!.UseNewMixer) + Engine.Instance.Mixer_NAudio.SetVolume(volume); + else + Vol = Math.Clamp(volume, 0, 1); + } + + public void CreateWaveWriter(string fileName) + { + _waveWriter = new Wave(fileName); + } + public void CloseWaveWriter() + { + + } + + public virtual void Dispose() + { + if (IsDisposed || Stream is null) return; + + Stream!.Dispose(); + //Reader!.Stream.Dispose(); + GC.SuppressFinalize(this); + + IsDisposed = true; + } + + 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) + { + Instance!.FramesPerBuffer = (uint)(sizeToAllocateInBytes / sizeof(float)) / 2; + Instance.SizeInBytes = sizeToAllocateInBytes; + int num = Instance.SizeInBytes % 4; + Instance.SizeToAllocateInBytes = (num == 0) ? Instance.SizeInBytes : (Instance.SizeInBytes + 4 - num); + ByteBuffer = new Span(new byte[Instance.SizeToAllocateInBytes]).ToArray(); + 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/Mixer_NAudio.cs b/VG Music Studio - Core/Mixer_NAudio.cs new file mode 100644 index 0000000..59e83e7 --- /dev/null +++ b/VG Music Studio - Core/Mixer_NAudio.cs @@ -0,0 +1,109 @@ +using NAudio.CoreAudioApi; +using NAudio.CoreAudioApi.Interfaces; +using NAudio.Wave; +using System; + +namespace Kermalis.VGMusicStudio.Core; + +public abstract class Mixer_NAudio : 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_NAudio() + { + Mutes = new bool[SongState.MAX_TRACKS]; + _out = null!; + _appVolume = null!; + } + + protected void Init(IWaveProvider waveProvider) + { + _out = new WasapiOut(); + _out.Init(waveProvider); + using (var en = new MMDeviceEnumerator()) + { + SessionCollection sessions = en.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia).AudioSessionManager.Sessions; + int id = Environment.ProcessId; + for (int i = 0; i < sessions.Count; i++) + { + AudioSessionControl session = sessions[i]; + if (session.GetProcessID == id) + { + _appVolume = session; + _appVolume.RegisterEventClient(this); + break; + } + } + } + _out.Play(); + } + + public void CreateWaveWriter(string fileName) + { + _waveWriter = new WaveFileWriter(fileName, WaveFormat); + } + public void CloseWaveWriter() + { + _waveWriter!.Dispose(); + _waveWriter = null; + } + + public void OnVolumeChanged(float volume, bool isMuted) + { + if (_shouldSendVolUpdateEvent) + { + VolumeChanged?.Invoke(volume); + } + _shouldSendVolUpdateEvent = true; + } + public void OnDisplayNameChanged(string displayName) + { + throw new NotImplementedException(); + } + public void OnIconPathChanged(string iconPath) + { + throw new NotImplementedException(); + } + public void OnChannelVolumeChanged(uint channelCount, IntPtr newVolumes, uint channelIndex) + { + throw new NotImplementedException(); + } + public void OnGroupingParamChanged(ref Guid groupingId) + { + throw new NotImplementedException(); + } + // Fires on @out.Play() and @out.Stop() + public void OnStateChanged(AudioSessionState state) + { + if (state == AudioSessionState.AudioSessionStateActive) + { + OnVolumeChanged(_appVolume.SimpleAudioVolume.Volume, _appVolume.SimpleAudioVolume.Mute); + } + } + public void OnSessionDisconnected(AudioSessionDisconnectReason disconnectReason) + { + throw new NotImplementedException(); + } + public void SetVolume(float volume) + { + _shouldSendVolUpdateEvent = false; + _appVolume.SimpleAudioVolume.Volume = volume; + } + + public virtual void Dispose() + { + GC.SuppressFinalize(this); + _out.Stop(); + _out.Dispose(); + _appVolume.Dispose(); + } +} diff --git a/VG Music Studio - Core/NDS/DSE/DSEEngine.cs b/VG Music Studio - Core/NDS/DSE/DSEEngine.cs index a7a933e..6b1ffdd 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEEngine.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEEngine.cs @@ -6,13 +6,23 @@ public sealed class DSEEngine : Engine public override DSEConfig Config { get; } public override DSEMixer Mixer { get; } + public override DSEMixer_NAudio Mixer_NAudio { get; } public override DSEPlayer Player { get; } + public override bool UseNewMixer { get => false; } public DSEEngine(string bgmPath) { Config = new DSEConfig(bgmPath); - Mixer = new DSEMixer(); - Player = new DSEPlayer(Config, Mixer); + if (Engine.Instance!.UseNewMixer) + { + Mixer = new DSEMixer(); + Player = new DSEPlayer(Config, Mixer); + } + else + { + Mixer_NAudio = new DSEMixer_NAudio(); + Player = new DSEPlayer(Config, Mixer_NAudio); + } DSEInstance = this; Instance = this; diff --git a/VG Music Studio - Core/NDS/DSE/DSEMixer.cs b/VG Music Studio - Core/NDS/DSE/DSEMixer.cs index 89f6c52..212f2a5 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEMixer.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEMixer.cs @@ -1,6 +1,6 @@ -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 System; namespace Kermalis.VGMusicStudio.Core.NDS.DSE; @@ -17,9 +17,7 @@ public sealed class DSEMixer : Mixer private float _fadeStepPerMicroframe; private readonly DSEChannel[] _channels; - private readonly BufferedWaveProvider _buffer; - - protected override WaveFormat WaveFormat => _buffer.WaveFormat; + private readonly Wave _buffer; public DSEMixer() { @@ -36,11 +34,12 @@ public DSEMixer() _channels[i] = new DSEChannel(i); } - _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + _buffer = new Wave() { DiscardOnBufferOverflow = true, BufferLength = _samplesPerBuffer * 64, }; + _buffer.CreateIeeeFloatWave(sampleRate, 2, 16); Init(_buffer); } diff --git a/VG Music Studio - Core/NDS/DSE/DSEMixer_NAudio.cs b/VG Music Studio - Core/NDS/DSE/DSEMixer_NAudio.cs new file mode 100644 index 0000000..dffaea7 --- /dev/null +++ b/VG Music Studio - Core/NDS/DSE/DSEMixer_NAudio.cs @@ -0,0 +1,214 @@ +using Kermalis.VGMusicStudio.Core.NDS.SDAT; +using Kermalis.VGMusicStudio.Core.Util; +using NAudio.Wave; +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE; + +public sealed class DSEMixer_NAudio : Mixer_NAudio +{ + private const int NUM_CHANNELS = 0x20; // Actual value unknown for now + + private readonly float _samplesReciprocal; + private readonly int _samplesPerBuffer; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; + + private readonly DSEChannel[] _channels; + private readonly BufferedWaveProvider _buffer; + + protected override WaveFormat WaveFormat => _buffer.WaveFormat; + + public DSEMixer_NAudio() + { + // The sampling frequency of the mixer is 1.04876 MHz with an amplitude resolution of 24 bits, but the sampling frequency after mixing with PWM modulation is 32.768 kHz with an amplitude resolution of 10 bits. + // - gbatek + // I'm not using either of those because the samples per buffer leads to an overflow eventually + const int sampleRate = 65_456; + _samplesPerBuffer = 341; // TODO + _samplesReciprocal = 1f / _samplesPerBuffer; + + _channels = new DSEChannel[NUM_CHANNELS]; + for (byte i = 0; i < NUM_CHANNELS; i++) + { + _channels[i] = new DSEChannel(i); + } + + _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + { + DiscardOnBufferOverflow = true, + BufferLength = _samplesPerBuffer * 64, + }; + Init(_buffer); + } + + internal DSEChannel? AllocateChannel() + { + static int GetScore(DSEChannel c) + { + // Free channels should be used before releasing channels + return c.Owner is null ? -2 : DSEUtils.IsStateRemovable(c.State) ? -1 : 0; + } + DSEChannel? nChan = null; + for (int i = 0; i < NUM_CHANNELS; i++) + { + DSEChannel c = _channels[i]; + if (nChan is null) + { + nChan = c; + } + else + { + int nScore = GetScore(nChan); + int cScore = GetScore(c); + if (cScore <= nScore && (cScore < nScore || c.Volume <= nChan.Volume)) + { + nChan = c; + } + } + } + return nChan is not null && 0 >= GetScore(nChan) ? nChan : null; + } + + internal void ChannelTick() + { + for (int i = 0; i < NUM_CHANNELS; i++) + { + DSEChannel chan = _channels[i]; + if (chan.Owner is null) + { + continue; + } + + chan.Volume = (byte)chan.StepEnvelope(); + if (chan.NoteLength == 0 && !DSEUtils.IsStateRemovable(chan.State)) + { + chan.SetEnvelopePhase7_2074ED8(); + } + int vol = SDATUtils.SustainTable[chan.NoteVelocity] + SDATUtils.SustainTable[chan.Volume] + SDATUtils.SustainTable[chan.Owner.Volume] + SDATUtils.SustainTable[chan.Owner.Expression]; + //int pitch = ((chan.Key - chan.BaseKey) << 6) + chan.SweepMain() + chan.Owner.GetPitch(); // "<< 6" is "* 0x40" + int pitch = (chan.Key - chan.RootKey) << 6; // "<< 6" is "* 0x40" + if (DSEUtils.IsStateRemovable(chan.State) && vol <= -92544) + { + chan.Stop(); + } + else + { + chan.Volume = SDATUtils.GetChannelVolume(vol); + chan.Panpot = chan.Owner.Panpot; + chan.Timer = SDATUtils.GetChannelTimer(chan.BaseTimer, pitch); + } + } + } + + internal void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal bool IsFading() + { + return _isFading; + } + internal bool IsFadeDone() + { + return _isFading && _fadeMicroFramesLeft == 0; + } + internal void ResetFade() + { + _isFading = false; + _fadeMicroFramesLeft = 0; + } + + private readonly byte[] _b = new byte[4]; + internal void Process(bool output, bool recording) + { + float masterStep; + float masterLevel; + if (_isFading && _fadeMicroFramesLeft == 0) + { + masterStep = 0; + masterLevel = 0; + } + else + { + float fromMaster = 1f; + float toMaster = 1f; + if (_fadeMicroFramesLeft > 0) + { + const float scale = 10f / 6f; + fromMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadePos += _fadeStepPerMicroframe; + toMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadeMicroFramesLeft--; + } + masterStep = (toMaster - fromMaster) * _samplesReciprocal; + masterLevel = fromMaster; + } + for (int i = 0; i < _samplesPerBuffer; i++) + { + int left = 0, + right = 0; + for (int j = 0; j < NUM_CHANNELS; j++) + { + DSEChannel chan = _channels[j]; + if (chan.Owner is null) + { + continue; + } + + bool muted = Mutes[chan.Owner.Index]; // Get mute first because chan.Process() can call chan.Stop() which sets chan.Owner to null + chan.Process(out short channelLeft, out short channelRight); + if (!muted) + { + left += channelLeft; + right += channelRight; + } + } + float f = left * masterLevel; + if (f < short.MinValue) + { + f = short.MinValue; + } + else if (f > short.MaxValue) + { + f = short.MaxValue; + } + left = (int)f; + _b[0] = (byte)left; + _b[1] = (byte)(left >> 8); + f = right * masterLevel; + if (f < short.MinValue) + { + f = short.MinValue; + } + else if (f > short.MaxValue) + { + f = short.MaxValue; + } + right = (int)f; + _b[2] = (byte)right; + _b[3] = (byte)(right >> 8); + masterLevel += masterStep; + if (output) + { + _buffer.AddSamples(_b, 0, 4); + } + if (recording) + { + _waveWriter!.Write(_b, 0, 4); + } + } + } +} diff --git a/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs b/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs index bfdcda2..3abde7c 100644 --- a/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs +++ b/VG Music Studio - Core/NDS/DSE/DSEPlayer.cs @@ -8,6 +8,7 @@ public sealed class DSEPlayer : Player private readonly DSEConfig _config; internal readonly DSEMixer DMixer; + internal readonly DSEMixer_NAudio DMixer_NAudio; internal readonly SWD MasterSWD; private DSELoadedSong? _loadedSong; @@ -17,6 +18,7 @@ public sealed class DSEPlayer : Player public override ILoadedSong? LoadedSong => _loadedSong; protected override Mixer Mixer => DMixer; + protected override Mixer_NAudio Mixer_NAudio => DMixer_NAudio; public DSEPlayer(DSEConfig config, DSEMixer mixer) : base(192) @@ -26,6 +28,14 @@ public DSEPlayer(DSEConfig config, DSEMixer mixer) MasterSWD = new SWD(Path.Combine(config.BGMPath, "bgm.swd")); } + public DSEPlayer(DSEConfig config, DSEMixer_NAudio mixer) + : base(192) + { + DMixer_NAudio = mixer; + _config = config; + + MasterSWD = new SWD(Path.Combine(config.BGMPath, "bgm.swd")); + } public override void LoadSong(int index) { @@ -49,7 +59,10 @@ internal override void InitEmulation() TempoStack = 0; _elapsedLoops = 0; ElapsedTicks = 0; - DMixer.ResetFade(); + if (Engine.Instance!.UseNewMixer) + DMixer.ResetFade(); + else + DMixer_NAudio.ResetFade(); DSETrack[] tracks = _loadedSong!.Tracks; for (int i = 0; i < tracks.Length; i++) { @@ -82,17 +95,35 @@ protected override bool Tick(bool playing, bool recording) { TickTrack(s, s.Tracks[i], ref allDone); } - if (DMixer.IsFadeDone()) + if (Engine.Instance!.UseNewMixer) + { + if (DMixer.IsFadeDone()) + { + allDone = true; + } + } + else { - allDone = true; + if (DMixer_NAudio.IsFadeDone()) + { + allDone = true; + } } } if (!allDone) { TempoStack += Tempo; } - DMixer.ChannelTick(); - DMixer.Process(playing, recording); + if (Engine.Instance!.UseNewMixer) + { + DMixer.ChannelTick(); + DMixer.Process(playing, recording); + } + else + { + DMixer_NAudio.ChannelTick(); + DMixer_NAudio.Process(playing, recording); + } return allDone; } private void TickTrack(DSELoadedSong s, DSETrack track, ref bool allDone) @@ -127,9 +158,19 @@ private void HandleTicksAndLoop(DSELoadedSong s, DSETrack track) _elapsedLoops++; UpdateElapsedTicksAfterLoop(s.Events[track.Index], track.CurOffset, track.Rest); - if (ShouldFadeOut && _elapsedLoops > NumLoops && !DMixer.IsFading()) + if (Engine.Instance!.UseNewMixer) { - DMixer.BeginFadeOut(); + if (ShouldFadeOut && _elapsedLoops > NumLoops && !DMixer.IsFading()) + { + DMixer.BeginFadeOut(); + } + } + else + { + if (ShouldFadeOut && _elapsedLoops > NumLoops && !DMixer_NAudio.IsFading()) + { + DMixer_NAudio.BeginFadeOut(); + } } } } diff --git a/VG Music Studio - Core/NDS/SDAT/SDATEngine.cs b/VG Music Studio - Core/NDS/SDAT/SDATEngine.cs index 7611c7f..faea0af 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATEngine.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATEngine.cs @@ -6,13 +6,23 @@ public sealed class SDATEngine : Engine public override SDATConfig Config { get; } public override SDATMixer Mixer { get; } + public override SDATMixer_NAudio Mixer_NAudio { get; } public override SDATPlayer Player { get; } + public override bool UseNewMixer { get => false; } public SDATEngine(SDAT sdat) { Config = new SDATConfig(sdat); - Mixer = new SDATMixer(); - Player = new SDATPlayer(Config, Mixer); + if (UseNewMixer) + { + Mixer = new SDATMixer(); + Player = new SDATPlayer(Config, Mixer); + } + else + { + Mixer_NAudio = new SDATMixer_NAudio(); + Player = new SDATPlayer(Config, Mixer_NAudio); + } SDATInstance = this; Instance = this; diff --git a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs index 6d69009..26f8cd8 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Events.cs @@ -693,8 +693,16 @@ public void SetTicks() _player.ElapsedTicks++; } _player.TempoStack += _player.Tempo; - _player.SMixer.ChannelTick(); - _player.SMixer.EmulateProcess(); + if (Engine.Instance!.UseNewMixer) + { + _player.SMixer.ChannelTick(); + _player.SMixer.EmulateProcess(); + } + else + { + _player.SMixer_NAudio.ChannelTick(); + _player.SMixer_NAudio.EmulateProcess(); + } } for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) { @@ -732,8 +740,16 @@ internal void SetCurTick(long ticks) } } _player.TempoStack += _player.Tempo; - _player.SMixer.ChannelTick(); - _player.SMixer.EmulateProcess(); + if (Engine.Instance!.UseNewMixer) + { + _player.SMixer.ChannelTick(); + _player.SMixer.EmulateProcess(); + } + else + { + _player.SMixer_NAudio.ChannelTick(); + _player.SMixer_NAudio.EmulateProcess(); + } } finish: for (int i = 0; i < 0x10; i++) diff --git a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs index c9a87d0..1f440be 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATLoadedSong_Runtime.cs @@ -78,7 +78,10 @@ private int ReadArg(SDATTrack track, ArgType type) private void TryStartChannel(SBNK.InstrumentData inst, SDATTrack track, byte note, byte velocity, int duration, out SDATChannel? channel) { InstrumentType type = inst.Type; - channel = _player.SMixer.AllocateChannel(type, track); + if (Engine.Instance!.UseNewMixer) + channel = _player.SMixer.AllocateChannel(type, track); + else + channel = _player.SMixer_NAudio.AllocateChannel(type, track); if (channel is null) { return; diff --git a/VG Music Studio - Core/NDS/SDAT/SDATMixer.cs b/VG Music Studio - Core/NDS/SDAT/SDATMixer.cs index e516e15..55e44d3 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATMixer.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATMixer.cs @@ -1,5 +1,5 @@ -using Kermalis.VGMusicStudio.Core.Util; -using NAudio.Wave; +using Kermalis.VGMusicStudio.Core.Formats; +using Kermalis.VGMusicStudio.Core.Util; using System; namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; @@ -14,9 +14,7 @@ public sealed class SDATMixer : Mixer private float _fadeStepPerMicroframe; internal SDATChannel[] Channels; - private readonly BufferedWaveProvider _buffer; - - protected override WaveFormat WaveFormat => _buffer.WaveFormat; + private readonly Wave _buffer; internal SDATMixer() { @@ -33,11 +31,12 @@ internal SDATMixer() Channels[i] = new SDATChannel(i); } - _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + _buffer = new Wave() { DiscardOnBufferOverflow = true, BufferLength = _samplesPerBuffer * 64 }; + _buffer.CreateIeeeFloatWave(sampleRate, 2, 16); Init(_buffer); } diff --git a/VG Music Studio - Core/NDS/SDAT/SDATMixer_NAudio.cs b/VG Music Studio - Core/NDS/SDAT/SDATMixer_NAudio.cs new file mode 100644 index 0000000..abf42c6 --- /dev/null +++ b/VG Music Studio - Core/NDS/SDAT/SDATMixer_NAudio.cs @@ -0,0 +1,244 @@ +using Kermalis.VGMusicStudio.Core.Util; +using NAudio.Wave; +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT; + +public sealed class SDATMixer_NAudio : Mixer_NAudio +{ + private readonly float _samplesReciprocal; + private readonly int _samplesPerBuffer; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; + + internal SDATChannel[] Channels; + private readonly BufferedWaveProvider _buffer; + + protected override WaveFormat WaveFormat => _buffer.WaveFormat; + + internal SDATMixer_NAudio() + { + // The sampling frequency of the mixer is 1.04876 MHz with an amplitude resolution of 24 bits, but the sampling frequency after mixing with PWM modulation is 32.768 kHz with an amplitude resolution of 10 bits. + // - gbatek + // I'm not using either of those because the samples per buffer leads to an overflow eventually + const int sampleRate = 65456; + _samplesPerBuffer = 341; // TODO + _samplesReciprocal = 1f / _samplesPerBuffer; + + Channels = new SDATChannel[0x10]; + for (byte i = 0; i < 0x10; i++) + { + Channels[i] = new SDATChannel(i); + } + + _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + { + DiscardOnBufferOverflow = true, + BufferLength = _samplesPerBuffer * 64 + }; + Init(_buffer); + } + + private static readonly int[] _pcmChanOrder = new int[] { 4, 5, 6, 7, 2, 0, 3, 1, 8, 9, 10, 11, 14, 12, 15, 13 }; + private static readonly int[] _psgChanOrder = new int[] { 8, 9, 10, 11, 12, 13 }; + private static readonly int[] _noiseChanOrder = new int[] { 14, 15 }; + internal SDATChannel? AllocateChannel(InstrumentType type, SDATTrack track) + { + int[] allowedChannels; + switch (type) + { + case InstrumentType.PCM: allowedChannels = _pcmChanOrder; break; + case InstrumentType.PSG: allowedChannels = _psgChanOrder; break; + case InstrumentType.Noise: allowedChannels = _noiseChanOrder; break; + default: return null; + } + SDATChannel? nChan = null; + for (int i = 0; i < allowedChannels.Length; i++) + { + SDATChannel c = Channels[allowedChannels[i]]; + if (nChan is not null && c.Priority >= nChan.Priority && (c.Priority != nChan.Priority || nChan.Volume <= c.Volume)) + { + continue; + } + nChan = c; + } + if (nChan is null || track.Priority < nChan.Priority) + { + return null; + } + return nChan; + } + + internal void ChannelTick() + { + for (int i = 0; i < 0x10; i++) + { + SDATChannel chan = Channels[i]; + if (chan.Owner is null) + { + continue; + } + + chan.StepEnvelope(); + if (chan.NoteDuration == 0 && !chan.Owner.WaitingForNoteToFinishBeforeContinuingXD) + { + chan.Priority = 1; + chan.State = EnvelopeState.Release; + } + int vol = SDATUtils.SustainTable[chan.NoteVelocity] + chan.Velocity + chan.Owner.GetVolume(); + int pitch = ((chan.Note - chan.BaseNote) << 6) + chan.SweepMain() + chan.Owner.GetPitch(); // "<< 6" is "* 0x40" + int pan = 0; + chan.LFOTick(); + switch (chan.LFOType) + { + case LFOType.Pitch: pitch += chan.LFOParam; break; + case LFOType.Volume: vol += chan.LFOParam; break; + case LFOType.Panpot: pan += chan.LFOParam; break; + } + if (chan.State == EnvelopeState.Release && vol <= -92544) + { + chan.Stop(); + } + else + { + chan.Volume = SDATUtils.GetChannelVolume(vol); + chan.Timer = SDATUtils.GetChannelTimer(chan.BaseTimer, pitch); + int p = chan.StartingPan + chan.Owner.GetPan() + pan; + if (p < -0x40) + { + p = -0x40; + } + else if (p > 0x3F) + { + p = 0x3F; + } + chan.Pan = (sbyte)p; + } + } + } + + internal void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * 192); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1_000.0 * 192); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + internal bool IsFading() + { + return _isFading; + } + internal bool IsFadeDone() + { + return _isFading && _fadeMicroFramesLeft == 0; + } + internal void ResetFade() + { + _isFading = false; + _fadeMicroFramesLeft = 0; + } + + internal void EmulateProcess() + { + for (int i = 0; i < _samplesPerBuffer; i++) + { + for (int j = 0; j < 0x10; j++) + { + SDATChannel chan = Channels[j]; + if (chan.Owner is not null) + { + chan.EmulateProcess(); + } + } + } + } + private readonly byte[] _b = new byte[4]; + internal void Process(bool output, bool recording) + { + float masterStep; + float masterLevel; + if (_isFading && _fadeMicroFramesLeft == 0) + { + masterStep = 0; + masterLevel = 0; + } + else + { + float fromMaster = 1f; + float toMaster = 1f; + if (_fadeMicroFramesLeft > 0) + { + const float scale = 10f / 6f; + fromMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadePos += _fadeStepPerMicroframe; + toMaster *= (_fadePos < 0f) ? 0f : MathF.Pow(_fadePos, scale); + _fadeMicroFramesLeft--; + } + masterStep = (toMaster - fromMaster) * _samplesReciprocal; + masterLevel = fromMaster; + } + for (int i = 0; i < _samplesPerBuffer; i++) + { + int left = 0, + right = 0; + for (int j = 0; j < 0x10; j++) + { + SDATChannel chan = Channels[j]; + if (chan.Owner is null) + { + continue; + } + + bool muted = Mutes[chan.Owner.Index]; // Get mute first because chan.Process() can call chan.Stop() which sets chan.Owner to null + chan.Process(out short channelLeft, out short channelRight); + if (!muted) + { + left += channelLeft; + right += channelRight; + } + } + float f = left * masterLevel; + if (f < short.MinValue) + { + f = short.MinValue; + } + else if (f > short.MaxValue) + { + f = short.MaxValue; + } + left = (int)f; + _b[0] = (byte)left; + _b[1] = (byte)(left >> 8); + f = right * masterLevel; + if (f < short.MinValue) + { + f = short.MinValue; + } + else if (f > short.MaxValue) + { + f = short.MaxValue; + } + right = (int)f; + _b[2] = (byte)right; + _b[3] = (byte)(right >> 8); + masterLevel += masterStep; + if (output) + { + _buffer.AddSamples(_b, 0, 4); + } + if (recording) + { + _waveWriter!.Write(_b, 0, 4); + } + } + } +} diff --git a/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs b/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs index 97fb6ae..40fc72f 100644 --- a/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs +++ b/VG Music Studio - Core/NDS/SDAT/SDATPlayer.cs @@ -13,6 +13,7 @@ public sealed class SDATPlayer : Player private readonly string?[] _voiceTypeCache = new string?[256]; internal readonly SDATConfig Config; internal readonly SDATMixer SMixer; + internal readonly SDATMixer_NAudio SMixer_NAudio; private SDATLoadedSong? _loadedSong; internal byte Volume; @@ -24,6 +25,7 @@ public sealed class SDATPlayer : Player public override ILoadedSong? LoadedSong => _loadedSong; protected override Mixer Mixer => SMixer; + protected override Mixer_NAudio Mixer_NAudio => SMixer_NAudio; internal SDATPlayer(SDATConfig config, SDATMixer mixer) : base(192) @@ -36,6 +38,17 @@ internal SDATPlayer(SDATConfig config, SDATMixer mixer) Tracks[i] = new SDATTrack(i, this); } } + internal SDATPlayer(SDATConfig config, SDATMixer_NAudio mixer) + : base(192) + { + Config = config; + SMixer_NAudio = mixer; + + for (byte i = 0; i < 0x10; i++) + { + Tracks[i] = new SDATTrack(i, this); + } + } public override void LoadSong(int index) { @@ -80,7 +93,10 @@ internal override void InitEmulation() TempoStack = 0; _elapsedLoops = 0; ElapsedTicks = 0; - SMixer.ResetFade(); + if (Engine.Instance!.UseNewMixer) + SMixer.ResetFade(); + else + SMixer_NAudio.ResetFade(); _loadedSong!.InitEmulation(); for (int i = 0; i < 0x10; i++) { @@ -107,34 +123,68 @@ protected override void OnStopped() protected override bool Tick(bool playing, bool recording) { bool allDone = false; - while (!allDone && TempoStack >= 240) + if (Engine.Instance!.UseNewMixer) { - TempoStack -= 240; - allDone = true; - for (int i = 0; i < 0x10; i++) + while (!allDone && TempoStack >= 240) { - TickTrack(i, ref allDone); + TempoStack -= 240; + allDone = true; + for (int i = 0; i < 0x10; i++) + { + TickTrack(i, ref allDone); + } + if (SMixer.IsFadeDone()) + { + allDone = true; + } } - if (SMixer.IsFadeDone()) + if (!allDone) { - allDone = true; + TempoStack += Tempo; } + for (int i = 0; i < 0x10; i++) + { + SDATTrack track = Tracks[i]; + if (track.Enabled) + { + track.UpdateChannels(); + } + } + SMixer.ChannelTick(); + SMixer.Process(playing, recording); + return allDone; } - if (!allDone) - { - TempoStack += Tempo; - } - for (int i = 0; i < 0x10; i++) + else { - SDATTrack track = Tracks[i]; - if (track.Enabled) + while (!allDone && TempoStack >= 240) + { + TempoStack -= 240; + allDone = true; + for (int i = 0; i < 0x10; i++) + { + TickTrack(i, ref allDone); + } + if (SMixer_NAudio.IsFadeDone()) + { + allDone = true; + } + } + if (!allDone) + { + TempoStack += Tempo; + } + for (int i = 0; i < 0x10; i++) { - track.UpdateChannels(); + SDATTrack track = Tracks[i]; + if (track.Enabled) + { + track.UpdateChannels(); + } } + SMixer_NAudio.ChannelTick(); + SMixer_NAudio.Process(playing, recording); + return allDone; } - SMixer.ChannelTick(); - SMixer.Process(playing, recording); - return allDone; } private void TickTrack(int trackIndex, ref bool allDone) { @@ -187,9 +237,19 @@ private void HandleTicksAndLoop(SDATLoadedSong s, SDATTrack track) break; } } - if (ShouldFadeOut && _elapsedLoops > NumLoops && !SMixer.IsFading()) + if (Engine.Instance!.UseNewMixer) + { + if (ShouldFadeOut && _elapsedLoops > NumLoops && !SMixer.IsFading()) + { + SMixer.BeginFadeOut(); + } + } + else { - SMixer.BeginFadeOut(); + if (ShouldFadeOut && _elapsedLoops > NumLoops && !SMixer_NAudio.IsFading()) + { + SMixer_NAudio.BeginFadeOut(); + } } } } diff --git a/VG Music Studio - Core/Player.cs b/VG Music Studio - Core/Player.cs index 33bd9b0..55fec7c 100644 --- a/VG Music Studio - Core/Player.cs +++ b/VG Music Studio - Core/Player.cs @@ -1,4 +1,5 @@ -using Kermalis.VGMusicStudio.Core.Util; +using PortAudio; +using Kermalis.VGMusicStudio.Core.Util; using System; using System.Collections.Generic; using System.IO; @@ -25,6 +26,7 @@ public abstract class Player : IDisposable { protected abstract string Name { get; } protected abstract Mixer Mixer { get; } + protected abstract Mixer_NAudio Mixer_NAudio { get; } public abstract ILoadedSong? LoadedSong { get; } public bool ShouldFadeOut { get; set; } @@ -36,6 +38,7 @@ public abstract class Player : IDisposable private readonly TimeBarrier _time; private Thread? _thread; + public bool IsStopped = true; protected Player(double ticksPerSecond) { @@ -49,10 +52,81 @@ protected Player(double ticksPerSecond) protected abstract void OnStopped(); protected abstract bool Tick(bool playing, bool recording); + + internal static 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 native PortAudio library itself. + + var player = Engine.Instance!.Player; + + Mixer.Audio d = Mixer.Instance!.Stream!.GetUserData(userData); + + if (!Mixer.Instance.Stream.UDHandle.IsAllocated) + { + return StreamCallbackResult.Abort; + } + + var frameSize = (int)Mixer.Instance.FinalFrameSize; + float[] frameBuffer = new float[frameSize]; + for (int i = 0; i < frameSize; i++) + frameBuffer[i] = d.Float32Buffer![i]; + + float[] newBuffer = new float[frameSize]; + for (int i = 0; i < frameSize; i++) + { + newBuffer[i] = Math.Clamp(frameBuffer[i], -1f, 1f); + } + // for (int i = 0; i < newBuffer.Length; i++) + // { + // System.Diagnostics.Debug.WriteLine("Buffer value: " + newBuffer[i].ToString()); + // } + + Span buffer; + unsafe + { + buffer = new((float*)output, frameSize); + } + + // Zero out the memory buffer output before applying buffer values + for (int i = 0; i < frameSize; i++) + buffer[i] = 0; + + // If we're reading data, play it back + if (player.State == PlayerState.Playing) + { + // Apply buffer value + for (int i = 0; i < frameSize; i++) + buffer[i] = newBuffer[i]; + + for (int i = 0; i < frameSize; i++) + buffer[i] *= Mixer.Instance.Volume; + } + + if (player.State is PlayerState.Playing or PlayerState.Paused) + { + // Continue if the song isn't finished + return StreamCallbackResult.Continue; + } + else + { + // Complete the callback when song is finished + return StreamCallbackResult.Complete; + } + } protected void CreateThread() { - _thread = new Thread(TimerTick) { Name = Name + " Tick" }; + _thread = new Thread(TimerLoop) { Name = Name + " Tick" }; _thread.Start(); } protected void WaitThread() @@ -89,7 +163,19 @@ public void Play() Stop(); InitEmulation(); State = PlayerState.Playing; - CreateThread(); + if (Engine.Instance!.UseNewMixer) + { + if (IsStopped) + { + IsStopped = false; + CreateThread(); + Mixer.Instance!.Stream!.Start(); + } + } + else + { + CreateThread(); + } } } public void TogglePlaying() @@ -97,18 +183,18 @@ public void TogglePlaying() switch (State) { case PlayerState.Playing: - { - State = PlayerState.Paused; - WaitThread(); - break; - } + { + State = PlayerState.Paused; + WaitThread(); + break; + } case PlayerState.Paused: case PlayerState.Stopped: - { - State = PlayerState.Playing; - CreateThread(); - break; - } + { + State = PlayerState.Playing; + CreateThread(); + break; + } } } public void Stop() @@ -118,6 +204,15 @@ public void Stop() State = PlayerState.Stopped; WaitThread(); OnStopped(); + if (Engine.Instance!.UseNewMixer) + { + if (!IsStopped) + { + IsStopped = true; + //_time.Stop(); + Mixer.Instance!.Stream!.Stop(); + } + } } } public void Record(string fileName) @@ -153,35 +248,42 @@ public void SetSongPosition(long ticks) TogglePlaying(); } - private void TimerTick() + private void TimerLoop() { _time.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) + if (TimerTick(_time, playing, recording) is true) { - // TODO: lock state - _time.Stop(); // TODO: Don't need timer if recording - State = PlayerState.Stopped; - SongEnded?.Invoke(); return; } - if (playing) - { - _time.Wait(); - } } _time.Stop(); } + private bool TimerTick(TimeBarrier time, bool playing, bool recording) + { + 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 allDone; + } + if (playing) + { + time.Wait(); + } + return false; + } 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 0000000..c3cb486 --- /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/SampleFormat.cs b/VG Music Studio - Core/PortAudio/Enumerations/SampleFormat.cs new file mode 100644 index 0000000..9780cae --- /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 0000000..ba41c14 --- /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 0000000..29b5a9e --- /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 0000000..2c43f01 --- /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/PortAudio.cs b/VG Music Studio - Core/PortAudio/PortAudio.cs new file mode 100644 index 0000000..199ef3d --- /dev/null +++ b/VG Music Studio - Core/PortAudio/PortAudio.cs @@ -0,0 +1,285 @@ +// License: APL 2.0 +// Author: Benjamin N. Summerton + +using System; +using System.Reflection; +using System.Runtime.InteropServices; + +using DeviceIndex = System.Int32; + +namespace PortAudio +{ + internal static partial class Native + { + public const string PortAudioDLL = "portaudio"; + + [DllImport(PortAudioDLL)] + public static extern int Pa_GetVersion(); + + [DllImport(PortAudioDLL)] + public static extern IntPtr Pa_GetVersionInfo(); // Originally returns `const PaVersionInfo *` + + [DllImport(PortAudioDLL)] + public static extern IntPtr Pa_GetErrorText([MarshalAs(UnmanagedType.I4)] ErrorCode errorCode); // Orignially returns `const char *` + + [DllImport(PortAudioDLL)] + [return: MarshalAs(UnmanagedType.I4)] + public static extern ErrorCode Pa_Initialize(); + + [DllImport(PortAudioDLL)] + [return: MarshalAs(UnmanagedType.I4)] + public static extern ErrorCode Pa_Terminate(); + + [DllImport(PortAudioDLL)] + public static extern DeviceIndex Pa_GetDefaultOutputDevice(); + + [DllImport(PortAudioDLL)] + public static extern DeviceIndex Pa_GetDefaultInputDevice(); + + [DllImport(PortAudioDLL)] + public static extern IntPtr Pa_GetDeviceInfo(DeviceIndex device); // Originally returns `const PaDeviceInfo *` + + [DllImport(PortAudioDLL)] + public static extern DeviceIndex Pa_GetDeviceCount(); + + [DllImport(PortAudioDLL)] + public static extern void Pa_Sleep(System.Int32 msec); + } + + public static class Pa + { + #region Constants + /// + /// A special PaDeviceIndex value indicating that no device is available, + /// or should be used. + /// + /// @see PaDeviceIndex + /// + public const DeviceIndex NoDevice = -1; + + /// + /// Can be passed as the framesPerBuffer parameter to Pa_OpenStream() + /// or Pa_OpenDefaultStream() to indicate that the stream callback will + /// accept buffers of any size. + /// + public const System.UInt32 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 paMakeVersionNumber + /// + /// + public static int Version + { + get => Native.Pa_GetVersion(); + } + + /// + /// Retrieve version information for the currently running PortAudio build. + /// @return A pointer to an immutable PaVersionInfo structure. + /// + /// @note 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. + /// + /// @see PaVersionInfo, paMakeVersionNumber + /// @version Available as of 19.5.0. + /// + public static VersionInfo VersionInfo + { + get => Marshal.PtrToStructure(Native.Pa_GetVersionInfo()); + } + + /// + /// Retrieve the index of the default output device. The result can be + /// used in the outputDevice parameter to Pa_OpenStream(). + /// + /// @return The default output device index for the default host API, or paNoDevice + /// if no default output device is available or an error was encountered. + /// + /// @note + /// 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 DeviceIndex DefaultOutputDevice + { + get => Native.Pa_GetDefaultOutputDevice(); + } + + /// + /// Retrieve the index of the default input device. The result can be + /// used in the inputDevice parameter to Pa_OpenStream(). + /// + /// @return The default input device index for the default host API, or paNoDevice + /// if no default input device is available or an error was encountered. + /// + public static DeviceIndex DefaultInputDevice + { + get => Native.Pa_GetDefaultInputDevice(); + } + + /// + /// Retrieve the number of available devices. The number of available devices + /// may be zero. + /// + /// @return A non-negative value indicating the number of available devices + /// or, a PaErrorCode (which are always negative) if PortAudio is not initialized + /// or an error is encountered. + /// + public static DeviceIndex DeviceCount + { + get => Native.Pa_GetDeviceCount(); + } + #endregion + + #region Methods + /// + /// Retrieve the release number of the currently running PortAudio build. + /// For example, for version "19.5.1" this will return 0x00130501. + /// + /// @see paMakeVersionNumber + /// + /// + public static int GetVersion() => + Native.Pa_GetVersion(); + + /// + /// Retrieve version information for the currently running PortAudio build. + /// @return A pointer to an immutable PaVersionInfo structure. + /// + /// @note 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. + /// + /// @see PaVersionInfo, paMakeVersionNumber + /// @version Available as of 19.5.0. + /// + public static VersionInfo GetVersionInfo() => + Marshal.PtrToStructure(Native.Pa_GetVersionInfo()); + + /// + /// Retrieve the index of the default output device. The result can be + /// used in the outputDevice parameter to Pa_OpenStream(). + /// + /// @return The default output device index for the default host API, or paNoDevice + /// if no default output device is available or an error was encountered. + /// + /// @note + /// 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 DeviceIndex GetDefaultOutputDevice() => + Native.Pa_GetDefaultOutputDevice(); + + /// + /// Retrieve the index of the default input device. The result can be + /// used in the inputDevice parameter to Pa_OpenStream(). + /// + /// @return The default input device index for the default host API, or paNoDevice + /// if no default input device is available or an error was encountered. + /// + public static DeviceIndex GetDefaultInputDevice() => + Native.Pa_GetDefaultInputDevice(); + + /// + /// Retrieve the number of available devices. The number of available devices + /// may be zero. + /// + /// @return A non-negative value indicating the number of available devices + /// or, a PaErrorCode (which are always negative) if PortAudio is not initialized + /// or an error is encountered. + /// + public static DeviceIndex GetDeviceCount() => + Native.Pa_GetDeviceCount(); + + /// + /// Retrieve a pointer to a PaDeviceInfo structure containing information + /// about the specified device. + /// @return A pointer to an immutable PaDeviceInfo structure. If the device + /// parameter is out of range the function returns NULL. + /// + /// @param device A valid device index in the range 0 to (Pa_GetDeviceCount()-1) + /// + /// @note 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(). + /// + /// @see PaDeviceInfo, PaDeviceIndex + /// + public static DeviceInfo GetDeviceInfo(DeviceIndex device) => + Marshal.PtrToStructure(Native.Pa_GetDeviceInfo(device)); + + /// + /// Translate the supplied PortAudio error code into a human readable + /// message. + /// + public static string GetErrorText(ErrorCode errorCode) => + Marshal.PtrToStringAnsi(Native.Pa_GetErrorText(errorCode)); + + /// + /// 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 where Pa_Initialise() 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. + /// + /// @return paNoError if successful, otherwise an error code indicating the cause + /// of failure. + /// + /// @see Pa_Initialize + /// + public static void Terminate() + { + ErrorCode ec = Native.Pa_Terminate(); + if (ec != ErrorCode.NoError) + throw new PortAudioException(ec, "Error terminating PortAudio"); + } + + /// + /// 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. + /// + /// @return paNoError if successful, otherwise an error code indicating the cause + /// of failure. + /// + /// @see Pa_Terminate + /// + public static void Initialize() + { + ErrorCode ec = Native.Pa_Initialize(); + if (ec != ErrorCode.NoError) + throw new PortAudioException(ec, "Error initializing PortAudio"); + } + #endregion + } +} diff --git a/VG Music Studio - Core/PortAudio/PortAudioException.cs b/VG Music Studio - Core/PortAudio/PortAudioException.cs new file mode 100644 index 0000000..73664f9 --- /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/Stream.cs b/VG Music Studio - Core/PortAudio/Stream.cs new file mode 100644 index 0000000..2dcf7e6 --- /dev/null +++ b/VG Music Studio - Core/PortAudio/Stream.cs @@ -0,0 +1,949 @@ +// License: APL 2.0 +// Author: Benjamin N. Summerton + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace PortAudio +{ + internal static partial class Native + { + [DllImport(PortAudioDLL)] + [return: MarshalAs(UnmanagedType.I4)] + public static extern ErrorCode Pa_OpenStream( + out IntPtr stream, // `PaStream **` + IntPtr inputParameters, // `const PaStreamParameters *` + IntPtr outputParameters, // `const PaStreamParameters *` + double sampleRate, + System.UInt32 framesPerBuffer, + StreamFlags streamFlags, + IntPtr streamCallback, // `PaStreamCallback *` + IntPtr userData // `void *` + ); + + [DllImport(PortAudioDLL)] + [return: MarshalAs(UnmanagedType.I4)] + public static extern ErrorCode Pa_OpenDefaultStream( + out IntPtr stream, + int numInputChannels, + int numOutputChannels, + SampleFormat sampleFormat, + double sampleRate, + System.UInt32 framesPerBuffer, + IntPtr streamCallback, + IntPtr userData + ); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [return: MarshalAs(UnmanagedType.I4)] + public delegate StreamCallbackResult Callback( + IntPtr input, IntPtr output, // Originally `const void *, void *` + System.UInt32 frameCount, + ref StreamCallbackTimeInfo timeInfo, // Originally `const PaStreamCallbackTimeInfo*` + StreamCallbackFlags statusFlags, + IntPtr userData // Orignially `void *` + ); + + [DllImport(PortAudioDLL)] + [return: MarshalAs(UnmanagedType.I4)] + public static extern ErrorCode Pa_CloseStream(IntPtr stream); // `PaStream *` + + [DllImport(PortAudioDLL)] + [return: MarshalAs(UnmanagedType.I4)] + public static extern ErrorCode Pa_SetStreamFinishedCallback( + IntPtr stream, // `PaStream *` + IntPtr streamFinishedCallback // `PaStreamFinishedCallback *` + ); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void FinishedCallback( + IntPtr userData // Originally `void *` + ); + + [DllImport(PortAudioDLL)] + [return: MarshalAs(UnmanagedType.I4)] + public static extern ErrorCode Pa_StartStream(IntPtr stream); // `PaStream *` + + [DllImport(PortAudioDLL)] + [return: MarshalAs(UnmanagedType.I4)] + public static extern ErrorCode Pa_StopStream(IntPtr stream); // `PaStream *` + + [DllImport(PortAudioDLL)] + [return: MarshalAs(UnmanagedType.I4)] + public static extern ErrorCode Pa_AbortStream(IntPtr stream); // `PaStream *` + + [DllImport(PortAudioDLL)] + [return: MarshalAs(UnmanagedType.I4)] + public static extern ErrorCode Pa_IsStreamStopped(IntPtr stream); // `PaStream *` + + [DllImport(PortAudioDLL)] + [return: MarshalAs(UnmanagedType.I4)] + public static extern ErrorCode Pa_IsStreamActive(IntPtr stream); // `PaStream *` + + [DllImport(PortAudioDLL)] + public static extern double Pa_GetStreamCpuLoad(IntPtr stream); // `PaStream *` + + [DllImport(PortAudioDLL)] + [return: MarshalAs(UnmanagedType.I4)] + public static extern ErrorCode Pa_ReadStream( + nint stream, // `PaStream *` + nint buffer, // `void *` + ulong frames // `unsigned long` + ); + + [DllImport(PortAudioDLL)] + [return: MarshalAs(UnmanagedType.I4)] + public static extern ErrorCode Pa_WriteStream( + nint stream, // `PaStream *` + nint buffer, // `const void *` + ulong frames // `unsigned long` + ); + } + + /// + /// 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 IntPtr streamPtr = IntPtr.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, + System.UInt32 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 + IntPtr inputParametersPtr = IntPtr.Zero; + IntPtr outputParametersPtr = IntPtr.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 = Native.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 != IntPtr.Zero) + Marshal.FreeHGlobal(inputParametersPtr); + if (outputParametersPtr != IntPtr.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( + System.Int32 numInputChannels, + System.Int32 numOutputChannels, + SampleFormat sampleFormat, + double sampleRate, + System.UInt32 framesPerBuffer, + Callback callback, + object userData + ) + { + // Setup the steam's callback + streamCallback = new _NativeInterfacingCallback(callback); + + // Take control of the userdata object + userDataHandle = GCHandle.Alloc(userData); + + // Open the stream + ErrorCode ec = Native.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 = Native.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 == IntPtr.Zero) + return; + + ErrorCode ec = Native.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 = IntPtr.Zero; + } + + /// + /// Commences audio processing. + /// + public void Start() + { + ErrorCode ec = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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 = Native.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.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( + IntPtr input, IntPtr output, // Originally `const void *, void *` + System.UInt32 frameCount, + ref StreamCallbackTimeInfo timeInfo, // Originally `const PaStreamCallbackTimeInfo*` + StreamCallbackFlags statusFlags, + IntPtr 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( + IntPtr 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 IntPtr Ptr { get; private set; } = IntPtr.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 0000000..bb9919d --- /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/StreamCallbackTimeInfo.cs b/VG Music Studio - Core/PortAudio/Structures/StreamCallbackTimeInfo.cs new file mode 100644 index 0000000..4ad4a41 --- /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/StreamParameters.cs b/VG Music Studio - Core/PortAudio/Structures/StreamParameters.cs new file mode 100644 index 0000000..7c172d3 --- /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 channelCount; + + /// + /// 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={channelCount}"); + 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 0000000..69efccd --- /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 index eea96fb..540d38e 100644 --- a/VG Music Studio - Core/Properties/Strings.Designer.cs +++ b/VG Music Studio - Core/Properties/Strings.Designer.cs @@ -358,7 +358,16 @@ 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 Game Boy Advance ROM (*.gba, *.srl). /// public static string FilterOpenGBA { get { @@ -367,7 +376,7 @@ public static string FilterOpenGBA { } /// - /// Looks up a localized string similar to SDAT Files. + /// Looks up a localized string similar to Nitro Soundmaker Sound Data (*.sdat). /// public static string FilterOpenSDAT { get { @@ -376,7 +385,7 @@ public static string FilterOpenSDAT { } /// - /// Looks up a localized string similar to DLS Files. + /// Looks up a localized string similar to DLS Format (*.dls). /// public static string FilterSaveDLS { get { @@ -385,7 +394,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 +403,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 +412,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 { diff --git a/VG Music Studio - Core/Properties/Strings.resx b/VG Music Studio - Core/Properties/Strings.resx index 916279d..fce9432 100644 --- a/VG Music Studio - Core/Properties/Strings.resx +++ b/VG Music Studio - Core/Properties/Strings.resx @@ -128,10 +128,10 @@ Error Exporting MIDI - GBA Files + Game Boy Advance ROM (*.gba, *.srl) - MIDI Files + MIDI Format (*.mid, *.midi) Data @@ -199,7 +199,7 @@ Error Loading SDAT File - SDAT Files + Nitro Soundmaker Sound Data (*.sdat) End Current Playlist @@ -331,7 +331,7 @@ Error Exporting WAV - WAV Files + RIFF Wave (*.wav) Export Song as WAV @@ -344,7 +344,7 @@ Error Exporting SF2 - SF2 Files + SoundFont2 Format (*.sf2) Export VoiceTable as SF2 @@ -357,7 +357,7 @@ Error Exporting DLS - DLS Files + DLS Format (*.dls) Export VoiceTable as DLS @@ -369,4 +369,7 @@ songs|0_0|song|1_1|songs|2_*| + + All files (*.*) + \ No newline at end of file diff --git a/VG Music Studio - Core/Util/ActionExtensions.cs b/VG Music Studio - Core/Util/ActionExtensions.cs new file mode 100644 index 0000000..c6c753c --- /dev/null +++ b/VG Music Studio - Core/Util/ActionExtensions.cs @@ -0,0 +1,186 @@ +//****************************************************************************************************** +// ActionExtensions.cs - Gbtc +// +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 02/02/2016 - Stephen C. Wills +// Generated original version of source code. +// 10/01/2019 - Stephen C. Wills +// Updated implementation of DelayAndExecute to use TPL instead of ThreadPool. +// +//****************************************************************************************************** + +using System.Threading.Tasks; +using System.Threading; + +namespace System; + +/// +/// Defines extension methods for actions. +/// +public static class ActionExtensions +{ + /// + /// Execute an action on the thread pool after a specified number of milliseconds. + /// + /// The action to be executed. + /// The amount of time to wait before execution, in milliseconds. + /// The token used to cancel execution. + /// The action to be performed if an exception is thrown from the action. + /// + /// End users should attach to the or + /// events to log exceptions if the is not defined. + /// + public static void DelayAndExecute(this Action action, int delay, CancellationToken cancellationToken, Action? exceptionAction = null) => + new Action(_ => action()).DelayAndExecute(delay, cancellationToken, exceptionAction); + + /// + /// Execute a cancellable action on the thread pool after a specified number of milliseconds. + /// + /// The action to be executed. + /// The amount of time to wait before execution, in milliseconds. + /// The token used to cancel execution. + /// The action to be performed if an exception is thrown from the action. + /// + /// End users should attach to the or + /// events to log exceptions if the is not defined. + /// + public static void DelayAndExecute(this Action action, int delay, CancellationToken cancellationToken, Action? exceptionAction = null) => + Task.Delay(delay, cancellationToken) + .ContinueWith(_ => action(cancellationToken), cancellationToken) + .ContinueWith(task => + { + // ReSharper disable once PossibleNullReferenceException + if (exceptionAction is null) + throw task.Exception ?? new Exception("Task failed without an exception."); + + exceptionAction(task.Exception ?? new Exception("Task failed without an exception.")); + }, + cancellationToken, + TaskContinuationOptions.OnlyOnFaulted, + TaskScheduler.Default); + + /// + /// Execute an action on the thread pool after a specified number of milliseconds. + /// + /// The action to be executed. + /// The amount of time to wait before execution, in milliseconds. + /// The action to be performed if an exception is thrown from the action. + /// + /// A function to call which will cancel the operation. + /// Cancel function returns true if is canceled in time, false if not. + /// + /// + /// End users should attach to the or + /// events to log exceptions if the is not defined. + /// + public static Func DelayAndExecute(this Action action, int delay, Action? exceptionAction = null) => + new Action(_ => action()).DelayAndExecute(delay, exceptionAction); + + /// + /// Execute a cancellable action on the thread pool after a specified number of milliseconds. + /// + /// The action to be executed. + /// The amount of time to wait before execution, in milliseconds. + /// The action to be performed if an exception is thrown from the action. + /// + /// A function to call which will cancel the operation. + /// Cancel function returns true if is canceled, false if not. + /// + /// + /// End users should attach to the or + /// events to log exceptions if the is not defined. + /// + public static Func DelayAndExecute(this Action action, int delay, Action? exceptionAction = null) + { + // All this state complexity ensures that the token source + // is not disposed until after the action finishes executing; + // otherwise, token.ThrowIfCancellationRequested() might unexpectedly + // throw an ObjectDisposedException if used in the action + const int NotCancelled = 0; + const int Cancelling = 1; + const int Cancelled = 2; + const int Disposing = 3; + + CancellationTokenSource tokenSource = new(); + CancellationToken token = tokenSource.Token; + int state = NotCancelled; + + bool cancelFunc() + { + // if (state == NotCancelled) + // state = Cancelling; + // else + // return false; + // + // tokenSource.Cancel(); + // + // if (state == Cancelling) + // state = Canceled; + // else if (state == Disposing) + // tokenSource.Dispose(); + // + // return true; + + int previousState = Interlocked.CompareExchange(ref state, Cancelling, NotCancelled); + + if (previousState != NotCancelled) + return false; + + tokenSource.Cancel(); + + previousState = Interlocked.CompareExchange(ref state, Cancelled, Cancelling); + + // If the state changed to Disposing while cancelFunc was cancelling, + // executeAction will prevent the race condition by not calling + // tokenSource.Dispose() so it must be called here instead + if (previousState == Disposing) + tokenSource.Dispose(); + + return true; + } + + Action executeAction = _ => + { + try + { + if (!token.IsCancellationRequested) + action(token); + } + finally + { + // int previousState = state; + // state = Disposing; + // + // if (previousState != Cancelling) + // tokenSource.Dispose(); + + int previousState = Interlocked.Exchange(ref state, Disposing); + + // The Cancelling state is the only state in which it is not + // safe to dispose on this thread because Cancelling means that + // cancelFunc is in the process of calling tokenSource.Cancel() + if (previousState != Cancelling) + tokenSource.Dispose(); + } + }; + + executeAction.DelayAndExecute(delay, token, exceptionAction); + + return cancelFunc; + } +} + diff --git a/VG Music Studio - Core/Util/ArrayExtensions.cs b/VG Music Studio - Core/Util/ArrayExtensions.cs new file mode 100644 index 0000000..5f82a05 --- /dev/null +++ b/VG Music Studio - Core/Util/ArrayExtensions.cs @@ -0,0 +1,1605 @@ + +//****************************************************************************************************** +// ArrayExtensions.cs - Gbtc +// +// Copyright © 2012, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://www.opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 09/19/2008 - J. Ritchie Carroll +// Generated original version of source code. +// 12/03/2008 - J. Ritchie Carroll +// Added "Combine" and "IndexOfSequence" overloaded extensions. +// 02/13/2009 - Josh L. Patterson +// Edited Code Comments. +// 09/14/2009 - Stephen C. Wills +// Added new header and license agreement. +// 12/31/2009 - Andrew K. Hill +// Modified the following methods per unit testing: +// BlockCopy(T[], int, int) +// Combine(T[], T[]) +// Combine(T[], int, int, T[], int, int) +// Combine(T[][]) +// IndexOfSequence(T[], T[]) +// IndexOfSequence(T[], T[], int) +// IndexOfSequence(T[], T[], int, int) +// 11/22/2011 - J. Ritchie Carroll +// Added common case array parameter validation extensions +// 12/14/2012 - Starlynn Danyelle Gilliam +// Modified Header. +// 11/02/2023 - AJ Stadlin +// Added Extensions: +// CountOfSequence(T[], T[]) +// CountOfSequence(T[], T[], int) +// CountOfSequence(T[], T[], int, int) +// +//****************************************************************************************************** + +//****************************************************************************************************** +// BlockAllocatedMemoryStream.cs - Gbtc +// +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://www.opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 06/14/2013 - J. Ritchie Carroll +// Adapted from the "MemoryTributary" class written by Sebastian Friston: +// Source Code: http://memorytributary.codeplex.com/ +// Article: http://www.codeproject.com/Articles/348590/A-replacement-for-MemoryStream +// 11/21/2016 - Steven E. Chisholm +// A complete refresh of BlockAllocatedMemoryStream and how it works. +// +//****************************************************************************************************** + +//****************************************************************************************************** +// BufferPool.cs - Gbtc +// +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://www.opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 11/17/2016 - Steven E. Chisholm +// Generated original version of source code. +// 12/26/2019 - J. Ritchie Carroll +// Simplified DynamicObjectPool as an internal resource renaming to BufferPool. +// +//****************************************************************************************************** + + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace System; + +public static class ArrayExtensions +{ + /// + /// Zero the given buffer in a way that will not be optimized away. + /// + /// Buffer to zero. + /// of array. + public static void Zero(this T[] buffer) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + // Zero buffer + for (int i = 0; i < buffer.Length; i++) + buffer[i] = default!; + } + + /// + /// Validates that the specified and are valid within the given . + /// + /// Array to validate. + /// 0-based start index into the . + /// Valid number of items within from . + /// is null. + /// + /// or is less than 0 -or- + /// and will exceed length. + /// + /// of array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidateParameters(this T[]? array, int startIndex, int length) + { + if (array is null || startIndex < 0 || length < 0 || startIndex + length > array.Length) + RaiseValidationError(array, startIndex, length); + } + + // This method will raise the actual error - this is needed since .NET will not inline anything that might throw an exception + [MethodImpl(MethodImplOptions.NoInlining)] + private static void RaiseValidationError(T[]? array, int startIndex, int length) + { + if (array is null) + throw new ArgumentNullException(nameof(array)); + + if (startIndex < 0) + throw new ArgumentOutOfRangeException(nameof(startIndex), "cannot be negative"); + + if (length < 0) + throw new ArgumentOutOfRangeException(nameof(length), "cannot be negative"); + + if (startIndex + length > array.Length) + throw new ArgumentOutOfRangeException(nameof(length), $"startIndex of {startIndex} and length of {length} will exceed array size of {array.Length}"); + } + + /// + /// Returns a copy of the specified portion of the array. + /// + /// Source array. + /// Offset into array. + /// Length of array to copy at offset. + /// An array of data copied from the specified portion of the source array. + /// + /// + /// Returned array will be extended as needed to make it the specified , but + /// it will never be less than the source array length - . + /// + /// + /// If an existing array of primitives is already available, using the directly + /// instead of this extension method may be optimal since this method always allocates a new return array. + /// Unlike , however, this function also works with non-primitive types. + /// + /// + /// + /// is outside the range of valid indexes for the source array -or- + /// is less than 0. + /// + /// of array. + public static T[] BlockCopy(this T[] array, int startIndex, int length) + { + if (array is null) + throw new ArgumentNullException(nameof(array)); + + if (startIndex < 0) + throw new ArgumentOutOfRangeException(nameof(startIndex), "cannot be negative"); + + if (length < 0) + throw new ArgumentOutOfRangeException(nameof(length), "cannot be negative"); + + if (startIndex >= array.Length) + throw new ArgumentOutOfRangeException(nameof(startIndex), "not a valid index into the array"); + + length = array.Length - startIndex < length ? array.Length - startIndex : length; + T[] copiedBytes = new T[length]; + + if (typeof(T).IsPrimitive) + Buffer.BlockCopy(array, startIndex, copiedBytes, 0, length); + else + Array.Copy(array, startIndex, copiedBytes, 0, length); + + return copiedBytes; + } + + /// + /// Combines arrays together into a single array. + /// + /// Source array. + /// Other array to combine to array. + /// Combined arrays. + /// + /// + /// Only use this function if you need a copy of the combined arrays, it will be optimal + /// to use the Linq function if you simply need to + /// iterate over the combined arrays. + /// + /// + /// This function can easily throw an out of memory exception if there is not enough + /// contiguous memory to create an array sized with the combined lengths. + /// + /// + /// of array. + public static T[] Combine(this T[] source, T[] other) + { + if (source is null) + throw new ArgumentNullException(nameof(source)); + + if (other is null) + throw new ArgumentNullException(nameof(other)); + + return source.Combine(0, source.Length, other, 0, other.Length); + } + + /// + /// Combines specified portions of arrays together into a single array. + /// + /// Source array. + /// Offset into array to begin copy. + /// Number of bytes to copy from array. + /// Other array to combine to array. + /// Offset into array to begin copy. + /// Number of bytes to copy from array. + /// Combined specified portions of both arrays. + /// + /// or is outside the range of valid indexes for the associated array -or- + /// or is less than 0 -or- + /// or , + /// and or do not specify a valid section in the associated array. + /// + /// + /// + /// Only use this function if you need a copy of the combined arrays, it will be optimal + /// to use the Linq function if you simply need to + /// iterate over the combined arrays. + /// + /// + /// This function can easily throw an out of memory exception if there is not enough + /// contiguous memory to create an array sized with the combined lengths. + /// + /// + /// of array. + public static T[] Combine(this T[] source, int sourceOffset, int sourceCount, T[] other, int otherOffset, int otherCount) + { + if (source is null) + throw new ArgumentNullException(nameof(source)); + + if (other is null) + throw new ArgumentNullException(nameof(other)); + + if (sourceOffset < 0) + throw new ArgumentOutOfRangeException(nameof(sourceOffset), "cannot be negative"); + + if (otherOffset < 0) + throw new ArgumentOutOfRangeException(nameof(otherOffset), "cannot be negative"); + + if (sourceCount < 0) + throw new ArgumentOutOfRangeException(nameof(sourceCount), "cannot be negative"); + + if (otherCount < 0) + throw new ArgumentOutOfRangeException(nameof(otherCount), "cannot be negative"); + + if (sourceOffset >= source.Length) + throw new ArgumentOutOfRangeException(nameof(sourceOffset), "not a valid index into source array"); + + if (otherOffset >= other.Length) + throw new ArgumentOutOfRangeException(nameof(otherOffset), "not a valid index into other array"); + + if (sourceOffset + sourceCount > source.Length) + throw new ArgumentOutOfRangeException(nameof(sourceCount), "exceeds source array size"); + + if (otherOffset + otherCount > other.Length) + throw new ArgumentOutOfRangeException(nameof(otherCount), "exceeds other array size"); + + // Overflow is possible, but unlikely. Therefore, this is omitted for performance + // if ((int.MaxValue - sourceCount - otherCount) < 0) + // throw new ArgumentOutOfRangeException("sourceCount + otherCount", "exceeds maximum array size"); + + // Combine arrays together as a single image + T[] combinedBuffer = new T[sourceCount + otherCount]; + + if (typeof(T).IsPrimitive) + { + Buffer.BlockCopy(source, sourceOffset, combinedBuffer, 0, sourceCount); + Buffer.BlockCopy(other, otherOffset, combinedBuffer, sourceCount, otherCount); + } + else + { + Array.Copy(source, sourceOffset, combinedBuffer, 0, sourceCount); + Array.Copy(other, otherOffset, combinedBuffer, sourceCount, otherCount); + } + + return combinedBuffer; + } + + /// + /// Combines arrays together into a single array. + /// + /// Source array. + /// First array to combine to array. + /// Second array to combine to array. + /// Combined arrays. + /// + /// + /// Only use this function if you need a copy of the combined arrays, it will be optimal + /// to use the Linq function if you simply need to + /// iterate over the combined arrays. + /// + /// + /// This function can easily throw an out of memory exception if there is not enough + /// contiguous memory to create an array sized with the combined lengths. + /// + /// + /// of array. + public static T[] Combine(this T[] source, T[] other1, T[] other2) + { + return new[] { source, other1, other2 }.Combine(); + } + + /// + /// Combines arrays together into a single array. + /// + /// Source array. + /// First array to combine to array. + /// Second array to combine to array. + /// Third array to combine to array. + /// Combined arrays. + /// + /// + /// Only use this function if you need a copy of the combined arrays, it will be optimal + /// to use the Linq function if you simply need to + /// iterate over the combined arrays. + /// + /// + /// This function can easily throw an out of memory exception if there is not enough + /// contiguous memory to create an array sized with the combined lengths. + /// + /// + /// of array. + public static T[] Combine(this T[] source, T[] other1, T[] other2, T[] other3) + { + return new[] { source, other1, other2, other3 }.Combine(); + } + + /// + /// Combines arrays together into a single array. + /// + /// Source array. + /// First array to combine to array. + /// Second array to combine to array. + /// Third array to combine to array. + /// Fourth array to combine to array. + /// Combined arrays. + /// + /// + /// Only use this function if you need a copy of the combined arrays, it will be optimal + /// to use the Linq function if you simply need to + /// iterate over the combined arrays. + /// + /// + /// This function can easily throw an out of memory exception if there is not enough + /// contiguous memory to create an array sized with the combined lengths. + /// + /// + /// of array. + public static T[] Combine(this T[] source, T[] other1, T[] other2, T[] other3, T[] other4) + { + return new[] { source, other1, other2, other3, other4 }.Combine(); + } + + /// + /// Combines array of arrays together into a single array. + /// + /// Array of arrays to combine. + /// Combined arrays. + /// + /// + /// Only use this function if you need a copy of the combined arrays, it will be optimal + /// to use the Linq function if you simply need to + /// iterate over the combined arrays. + /// + /// + /// This function can easily throw an out of memory exception if there is not enough + /// contiguous memory to create an array sized with the combined lengths. + /// + /// + /// of arrays. + public static T[] Combine(this T[][] arrays) + { + if (arrays is null) + throw new ArgumentNullException(nameof(arrays)); + + int size = arrays.Sum(array => array.Length); + int offset = 0; + + // Combine arrays together as a single image + T[] combinedBuffer = new T[size]; + + for (int i = 0; i < arrays.Length; i++) + { + if (arrays[i] is null) + throw new ArgumentNullException($"arrays[{i}]"); + + int length = arrays[i].Length; + + if (length == 0) + continue; + + Array.Copy(arrays[i], 0, combinedBuffer, offset, length); + + offset += length; + } + + return combinedBuffer; + } + + /// + /// Searches for the specified and returns the index of the first occurrence within the . + /// + /// Array to search. + /// Sequence of items to search for. + /// The zero-based index of the first occurrence of the in the , if found; otherwise, -1. + /// of array. + public static int IndexOfSequence(this T[] array, T[] sequenceToFind) where T : IComparable + { + if (array is null) + throw new ArgumentNullException(nameof(array)); + + if (sequenceToFind is null) + throw new ArgumentNullException(nameof(sequenceToFind)); + + return array.IndexOfSequence(sequenceToFind, 0, array.Length); + } + + /// + /// Searches for the specified and returns the index of the first occurrence within the range of elements in the + /// that starts at the specified index. + /// + /// Array to search. + /// Sequence of items to search for. + /// Start index in the to start searching. + /// The zero-based index of the first occurrence of the in the , if found; otherwise, -1. + /// of array. + public static int IndexOfSequence(this T[] array, T[] sequenceToFind, int startIndex) where T : IComparable + { + if (array is null) + throw new ArgumentNullException(nameof(array)); + + if (sequenceToFind is null) + throw new ArgumentNullException(nameof(sequenceToFind)); + + return array.IndexOfSequence(sequenceToFind, startIndex, array.Length - startIndex); + } + + /// + /// Searches for the specified and returns the index of the first occurrence within the range of elements in the + /// that starts at the specified index and contains the specified number of elements. + /// + /// Array to search. + /// Sequence of items to search for. + /// Start index in the to start searching. + /// Number of bytes in the to search through. + /// The zero-based index of the first occurrence of the in the , if found; otherwise, -1. + /// + /// is null or has zero length. + /// + /// + /// is outside the range of valid indexes for the source array -or- + /// is less than 0. + /// + /// of array. + public static int IndexOfSequence(this T[] array, T[] sequenceToFind, int startIndex, int length) where T : IComparable + { + if (array is null) + throw new ArgumentNullException(nameof(array)); + + if (sequenceToFind is null || sequenceToFind.Length == 0) + throw new ArgumentNullException(nameof(sequenceToFind)); + + if (startIndex < 0) + throw new ArgumentOutOfRangeException(nameof(startIndex), "cannot be negative"); + + if (length < 0) + throw new ArgumentOutOfRangeException(nameof(length), "cannot be negative"); + + if (startIndex >= array.Length) + throw new ArgumentOutOfRangeException(nameof(startIndex), "not a valid index into source array"); + + if (startIndex + length > array.Length) + throw new ArgumentOutOfRangeException(nameof(length), "exceeds array size"); + + // Overflow is possible, but unlikely. Therefore, this is omitted for performance + // if ((int.MaxValue - startIndex - length) < 0) + // throw new ArgumentOutOfRangeException("startIndex + length", "exceeds maximum array size"); + + // Search for first item in the sequence, if this doesn't exist then sequence doesn't exist + int index = Array.IndexOf(array, sequenceToFind[0], startIndex, length); + + if (sequenceToFind.Length <= 1) + return index; + + bool foundSequence = false; + + while (index > -1 && !foundSequence) + { + // See if next bytes in sequence match + for (int x = 1; x < sequenceToFind.Length; x++) + { + // Make sure there's enough array remaining to accommodate this item + if (index + x < startIndex + length) + { + // If sequence doesn't match, search for next first-item + if (array[index + x].CompareTo(sequenceToFind[x]) != 0) + { + index = Array.IndexOf(array, sequenceToFind[0], index + 1, startIndex + length - (index + 1)); + break; + } + + // If each item to find matched, we found the sequence + foundSequence = x == sequenceToFind.Length - 1; + } + else + { + // Ran out of array, return -1 + index = -1; + } + } + } + + return index; + } + + /// + /// Searches for the specified and returns the occurrence count within the . + /// + /// Array to search. + /// Sequence of items to search for. + /// The occurrence count of the in the , if found; otherwise, -1. + /// of array. + public static int CountOfSequence(this T[] array, T[] sequenceToCount) where T : IComparable + { + if (array is null) + throw new ArgumentNullException(nameof(array)); + + if (sequenceToCount is null) + throw new ArgumentNullException(nameof(sequenceToCount)); + + return array.CountOfSequence(sequenceToCount, 0, array.Length); + } + + /// + /// Searches for the specified and returns the occurence count within the range of elements in the + /// that starts at the specified index. + /// + /// Array to search. + /// Sequence of items to search for. + /// Start index in the to start searching. + /// The occurrence count of the in the , if found; otherwise, -1. + /// of array. + public static int CountOfSequence(this T[] array, T[] sequenceToCount, int startIndex) where T : IComparable + { + if (array is null) + throw new ArgumentNullException(nameof(array)); + + if (sequenceToCount is null) + throw new ArgumentNullException(nameof(sequenceToCount)); + + return array.CountOfSequence(sequenceToCount, startIndex, array.Length - startIndex); + } + + /// + /// Searches for the specified and returns the occurrence count within the range of elements in the + /// that starts at the specified index and contains the specified number of elements. + /// + /// Array to search. + /// Sequence of items to search for. + /// Start index in the to start searching. + /// Number of bytes in the to search through. + /// The occurrence count of the in the , if found; otherwise, -1. + /// + /// is null or has zero length. + /// + /// + /// is outside the range of valid indexes for the source array -or- + /// is less than 0. + /// + /// of array. + public static int CountOfSequence(this T[] array, T[] sequenceToCount, int startIndex, int searchLength) where T : IComparable + { + if (array is null || array.Length == 0) + throw new ArgumentNullException(nameof(array)); + + if (sequenceToCount is null || sequenceToCount.Length == 0) + throw new ArgumentNullException(nameof(sequenceToCount)); + + if (startIndex < 0) + throw new ArgumentOutOfRangeException(nameof(startIndex), "cannot be negative"); + + if (startIndex >= array.Length) + throw new ArgumentOutOfRangeException(nameof(startIndex), "not a valid index into source array"); + + if (searchLength < 0) + throw new ArgumentOutOfRangeException(nameof(searchLength), "cannot be negative"); + + if (startIndex + searchLength > array.Length) + throw new ArgumentOutOfRangeException(nameof(searchLength), "exceeds array size"); + + // Overflow is possible, but unlikely. Therefore, this is omitted for performance + // if ((int.MaxValue - startIndex - length) < 0) + // throw new ArgumentOutOfRangeException("startIndex + length", "exceeds maximum array size"); + + // Search for first item in the sequence, if this doesn't exist then sequence doesn't exist + int index = Array.IndexOf(array, sequenceToCount[0], startIndex, searchLength); + + if (index < 0) + return 0; + + // Occurrences counter + int foundCount = 0; + + // Search when the first array element is found, and the sequence can fit in the search range + bool searching = sequenceToCount.Length <= startIndex + searchLength - index; + + while (searching) + { + // See if bytes in sequence match + for (int x = 0; x < sequenceToCount.Length; x++) + { + // If sequence doesn't match, search for next item + if (array[index + x].CompareTo(sequenceToCount[x]) != 0) + { + index++; + index = Array.IndexOf(array, sequenceToCount[0], index, startIndex + searchLength - index); + break; + } + + // When each item to find matched, we found the sequence + if (x == sequenceToCount.Length - 1) + { + foundCount++; + index++; + index = Array.IndexOf(array, sequenceToCount[0], index, startIndex + searchLength - index); + } + } + + // Continue searching if the array remaining can accommodate the sequence to find + searching = index > -1 && sequenceToCount.Length <= startIndex + searchLength - index; + } + + return foundCount; + } + + /// Returns comparison results of two binary arrays. + /// Source array. + /// Other array to compare to array. + /// + /// Note that if both arrays are null the arrays will be considered equal. + /// If one array is null and the other array is not null, the non-null array will be considered larger. + /// If the array lengths are not equal, the array with the larger length will be considered larger. + /// If the array lengths are equal, the arrays will be compared based on content. + /// + /// + /// + /// A signed integer that indicates the relative comparison of array and array. + /// + /// + /// + /// + /// Return Value + /// Description + /// + /// + /// Less than zero + /// Source array is less than other array. + /// + /// + /// Zero + /// Source array is equal to other array. + /// + /// + /// Greater than zero + /// Source array is greater than other array. + /// + /// + /// + /// + /// of array. + public static int CompareTo(this T[]? source, T[]? other) where T : IComparable + { + // If both arrays are assumed equal if both are nothing + if (source is null && other is null) + return 0; + + // If other array has data and source array is nothing, other array is assumed larger + if (source is null) + return -1; + + // If source array has data and other array is nothing, source array is assumed larger + if (other is null) + return 1; + + int length1 = source.Length; + int length2 = other.Length; + + // If array lengths are unequal, array with the largest number of elements is assumed to be largest + if (length1 != length2) + return length1.CompareTo(length2); + + int comparison = 0; + + // Compares elements of arrays that are of equal size. + for (int x = 0; x < length1; x++) + { + comparison = source[x].CompareTo(other[x]); + + if (comparison != 0) + break; + } + + return comparison; + } + + /// + /// Returns comparison results of two binary arrays. + /// + /// Source array. + /// Offset into array to begin compare. + /// Other array to compare to array. + /// Offset into array to begin compare. + /// Number of bytes to compare in both arrays. + /// + /// Note that if both arrays are null the arrays will be considered equal. + /// If one array is null and the other array is not null, the non-null array will be considered larger. + /// + /// + /// + /// A signed integer that indicates the relative comparison of array and array. + /// + /// + /// + /// + /// Return Value + /// Description + /// + /// + /// Less than zero + /// Source array is less than other array. + /// + /// + /// Zero + /// Source array is equal to other array. + /// + /// + /// Greater than zero + /// Source array is greater than other array. + /// + /// + /// + /// + /// + /// or is outside the range of valid indexes for the associated array -or- + /// is less than 0 -or- + /// or and do not specify a valid section in the associated array. + /// + /// of array. + public static int CompareTo(this T[]? source, int sourceOffset, T[]? other, int otherOffset, int count) where T : IComparable + { + // If both arrays are assumed equal if both are nothing + if (source is null && other is null) + return 0; + + // If other array has data and source array is nothing, other array is assumed larger + if (source is null) + return -1; + + // If source array has data and other array is nothing, source array is assumed larger + if (other is null) + return 1; + + if (sourceOffset < 0) + throw new ArgumentOutOfRangeException(nameof(sourceOffset), "cannot be negative"); + + if (otherOffset < 0) + throw new ArgumentOutOfRangeException(nameof(otherOffset), "cannot be negative"); + + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count), "cannot be negative"); + + if (sourceOffset >= source.Length) + throw new ArgumentOutOfRangeException(nameof(sourceOffset), "not a valid index into source array"); + + if (otherOffset >= other.Length) + throw new ArgumentOutOfRangeException(nameof(otherOffset), "not a valid index into other array"); + + if (sourceOffset + count > source.Length) + throw new ArgumentOutOfRangeException(nameof(count), "exceeds source array size"); + + if (otherOffset + count > other.Length) + throw new ArgumentOutOfRangeException(nameof(count), "exceeds other array size"); + + // Overflow is possible, but unlikely. Therefore, this is omitted for performance + // if ((int.MaxValue - sourceOffset - count) < 0) + // throw new ArgumentOutOfRangeException("sourceOffset + count", "exceeds maximum array size"); + + // Overflow is possible, but unlikely. Therefore, this is omitted for performance + // if ((int.MaxValue - otherOffset - count) < 0) + // throw new ArgumentOutOfRangeException("sourceOffset + count", "exceeds maximum array size"); + + int comparison = 0; + + // Compares elements of arrays that are of equal size. + for (int x = 0; x < count; x++) + { + comparison = source[sourceOffset + x].CompareTo(other[otherOffset + x]); + + if (comparison != 0) + break; + } + + return comparison; + } + + // Handling byte arrays as a special case for combining multiple buffers since this can + // use a block allocated memory stream + + /// + /// Combines buffers together as a single image. + /// + /// Source buffer. + /// First buffer to combine to buffer. + /// Second buffer to combine to buffer. + /// Combined buffers. + /// Cannot create a byte array with more than 2,147,483,591 elements. + /// + /// Only use this function if you need a copy of the combined buffers, it will be optimal + /// to use the Linq function if you simply need to + /// iterate over the combined buffers. + /// + public static byte[] Combine(this byte[] source, byte[] other1, byte[] other2) + { + return new[] { source, other1, other2 }.Combine(); + } + + /// + /// Combines buffers together as a single image. + /// + /// Source buffer. + /// First buffer to combine to buffer. + /// Second buffer to combine to buffer. + /// Third buffer to combine to buffer. + /// Combined buffers. + /// Cannot create a byte array with more than 2,147,483,591 elements. + /// + /// Only use this function if you need a copy of the combined buffers, it will be optimal + /// to use the Linq function if you simply need to + /// iterate over the combined buffers. + /// + public static byte[] Combine(this byte[] source, byte[] other1, byte[] other2, byte[] other3) + { + return new[] { source, other1, other2, other3 }.Combine(); + } + + /// + /// Combines buffers together as a single image. + /// + /// Source buffer. + /// First buffer to combine to buffer. + /// Second buffer to combine to buffer. + /// Third buffer to combine to buffer. + /// Fourth buffer to combine to buffer. + /// Combined buffers. + /// Cannot create a byte array with more than 2,147,483,591 elements. + /// + /// Only use this function if you need a copy of the combined buffers, it will be optimal + /// to use the Linq function if you simply need to + /// iterate over the combined buffers. + /// + public static byte[] Combine(this byte[] source, byte[] other1, byte[] other2, byte[] other3, byte[] other4) + { + return new[] { source, other1, other2, other3, other4 }.Combine(); + } + + /// + /// Combines an array of buffers together as a single image. + /// + /// Array of byte buffers. + /// Combined buffers. + /// Cannot create a byte array with more than 2,147,483,591 elements. + /// + /// Only use this function if you need a copy of the combined buffers, it will be optimal + /// to use the Linq function if you simply need to + /// iterate over the combined buffers. + /// + public static byte[] Combine(this byte[][] buffers) + { + if (buffers is null) + throw new ArgumentNullException(nameof(buffers)); + + using BlockAllocatedMemoryStream combinedBuffer = new(); + + // Combine all currently queued buffers + for (int x = 0; x < buffers.Length; x++) + { + if (buffers[x] is null) + throw new ArgumentNullException($"buffers[{x}]"); + + combinedBuffer.Write(buffers[x], 0, buffers[x].Length); + } + + // return combined data buffers + return combinedBuffer.ToArray(); + } + + /// + /// Reads a structure from a byte array. + /// + /// Type of structure to read. + /// Bytes containing structure. + /// A structure from . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe T? ReadStructure(this byte[] bytes) where T : struct + { + T? structure; + + fixed (byte* ptrToBytes = bytes) + structure = (T?)Marshal.PtrToStructure(new IntPtr(ptrToBytes), typeof(T)); + + return structure; + } + + /// + /// Reads a structure from a . + /// + /// Type of structure to read. + /// positioned at desired structure. + /// A structure read from . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T? ReadStructure(this BinaryReader reader) where T : struct => + reader.ReadBytes(Marshal.SizeOf(typeof(T))).ReadStructure(); + + + #region [ Block Allocated Memory Stream ] + + /// + /// Defines a stream whose backing store is memory. Externally this class operates similar to a , + /// internally it uses dynamically allocated buffer blocks instead of one large contiguous array of data. + /// + /// + /// + /// The has two primary benefits over a normal , first, the + /// allocation of a large contiguous array of data in can fail when the requested amount of contiguous + /// memory is unavailable - the prevents this; second, a will + /// constantly reallocate the buffer size as the stream grows and shrinks and then copy all the data from the old buffer to the + /// new - the maintains its blocks over its life cycle, unless manually cleared, thus + /// eliminating unnecessary allocations and garbage collections when growing and reusing a stream. + /// + /// + /// Important: Unlike , the will not use a user provided buffer + /// as its backing buffer. Any user provided buffers used to instantiate the class will be copied into internally managed reusable + /// memory buffers. Subsequently, the does not support the notion of a non-expandable + /// stream. If you are using a with your own buffer, the will + /// not provide any immediate benefit. + /// + /// + /// Note that the will maintain all allocated blocks for stream use until the + /// method is called or the class is disposed. + /// + /// + /// No members in the are guaranteed to be thread safe. Make sure any calls are + /// synchronized when simultaneously accessed from different threads. + /// + /// + public class BlockAllocatedMemoryStream : Stream + { + // Note: Since byte blocks are pooled, they will not be + // initialized unless a Read/Write operation occurs + // when m_position > m_length + + #region [ Members ] + + // Constants + private const int BlockSize = 8 * 1024; + private const int ShiftBits = 3 + 10; + private const int BlockMask = BlockSize - 1; + + // Fields + private List m_blocks; + private long m_length; + private long m_position; + private long m_capacity; + private bool m_disposed; + + #endregion + + #region [ Constructors ] + + /// + /// Initializes a new instance of . + /// + public BlockAllocatedMemoryStream() => m_blocks = new List(); + + /// + /// Initializes a new instance of from specified . + /// + /// Initial buffer to copy into stream. + /// is null. + /// + /// Unlike , the will not use the provided + /// as its backing buffer. The buffer will be copied into internally managed reusable + /// memory buffers. Subsequently, the notion of a non-expandable stream is not supported. + /// + public BlockAllocatedMemoryStream(byte[] buffer) : this(buffer, 0, buffer.Length) + { + } + + /// + /// Initializes a new instance of from specified region of . + /// + /// Initial buffer to copy into stream. + /// 0-based start index into the . + /// Valid number of bytes within from . + /// is null. + /// + /// or is less than 0 -or- + /// and will exceed length. + /// + /// + /// Unlike , the will not use the provided + /// as its backing buffer. The buffer will be copied into internally managed reusable + /// memory buffers. Subsequently, the notion of a non-expandable stream is not supported. + /// + public BlockAllocatedMemoryStream(byte[] buffer, int startIndex, int length) : this() + { + buffer.ValidateParameters(startIndex, length); + Write(buffer, startIndex, length); + } + + /// + /// Initializes a new instance of for specified . + /// + /// Initial length of the stream. + public BlockAllocatedMemoryStream(int capacity) : this() => SetLength(capacity); + + #endregion + + #region [ Properties ] + + /// + /// Gets a value that indicates whether the object supports reading. + /// + /// + /// This is always true. + /// + public override bool CanRead => true; + + /// + /// Gets a value that indicates whether the object supports seeking. + /// + /// + /// This is always true. + /// + public override bool CanSeek => true; + + /// + /// Gets a value that indicates whether the object supports writing. + /// + /// + /// This is always true. + /// + public override bool CanWrite => true; + + /// + /// Gets current stream length for this instance. + /// + /// The stream is closed. + public override long Length + { + get + { + if (m_disposed) + throw new ObjectDisposedException(nameof(BlockAllocatedMemoryStream), "The stream is closed."); + + return m_length; + } + } + + /// + /// Gets current stream position for this instance. + /// + /// Seeking was attempted before the beginning of the stream. + /// The stream is closed. + public override long Position + { + get + { + if (m_disposed) + throw new ObjectDisposedException(nameof(BlockAllocatedMemoryStream), "The stream is closed."); + + return m_position; + } + set + { + if (m_disposed) + throw new ObjectDisposedException(nameof(BlockAllocatedMemoryStream), "The stream is closed."); + + if (value < 0L) + throw new IOException("Seek was attempted before the beginning of the stream."); + + m_position = value; + } + } + + #endregion + + #region [ Methods ] + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; otherwise, false to release only unmanaged resources. + protected override void Dispose(bool disposing) + { + if (m_disposed) + return; + + try + { + // Make sure buffer blocks get returned to the pool + if (disposing) + Clear(); + } + finally + { + m_disposed = true; // Prevent duplicate dispose. + base.Dispose(disposing); // Call base class Dispose(). + } + } + + /// + /// Clears the entire contents and releases any allocated memory blocks. + /// + public void Clear() + { + m_position = 0; + m_length = 0; + m_capacity = 0; + + // In the event that an exception occurs, we don't want to have released blocks that are still in this memory stream. + List blocks = m_blocks; + + m_blocks = new List(); + + foreach (byte[] block in blocks) + s_memoryBlockPool.Enqueue(block); + } + + /// + /// Sets the within the current stream to the specified value relative the . + /// + /// + /// The new position within the stream, calculated by combining the initial reference point and the offset. + /// + /// The new position within the stream. This is relative to the parameter, and can be positive or negative. + /// A value of type , which acts as the seek reference point. + /// Seeking was attempted before the beginning of the stream. + /// The stream is closed. + public override long Seek(long offset, SeekOrigin origin) + { + if (m_disposed) + throw new ObjectDisposedException(nameof(BlockAllocatedMemoryStream), "The stream is closed."); + + switch (origin) + { + case SeekOrigin.Begin: + if (offset < 0L) + throw new IOException("Seek was attempted before the beginning of the stream."); + + m_position = offset; + break; + case SeekOrigin.Current: + if (m_position + offset < 0L) + throw new IOException("Seek was attempted before the beginning of the stream."); + + m_position += offset; + break; + case SeekOrigin.End: + if (m_length + offset < 0L) + throw new IOException("Seek was attempted before the beginning of the stream."); + + m_position = m_length + offset; + break; + } + + // Note: the length is not adjusted after this seek to reflect what MemoryStream.Seek does + return m_position; + } + + /// + /// Sets the length of the current stream to the specified value. + /// + /// The value at which to set the length. + /// + /// If this length is larger than the previous length, the data is initialized to 0's between the previous length and the current length. + /// + public override void SetLength(long value) + { + if (value > m_capacity) + EnsureCapacity(value); + + if (m_length < value) + InitializeToPosition(value); + + m_length = value; + + if (m_position > m_length) + m_position = m_length; + } + + /// + /// Reads a block of bytes from the current stream and writes the data to . + /// + /// When this method returns, contains the specified byte array with the values between and ( + - 1) replaced by the characters read from the current stream. + /// The byte offset in at which to begin reading. + /// The maximum number of bytes to read. + /// + /// The total number of bytes written into the buffer. This can be less than the number of bytes requested if that number of bytes are not currently available, or zero if the end of the stream is reached before any bytes are read. + /// + /// is null. + /// + /// or is less than 0 -or- + /// and will exceed length. + /// + /// The stream is closed. + public override int Read(byte[] buffer, int startIndex, int length) + { + if (m_disposed) + throw new ObjectDisposedException(nameof(BlockAllocatedMemoryStream), "The stream is closed."); + + buffer.ValidateParameters(startIndex, length); + + // Do not read beyond the end of the stream + long remainingBytes = m_length - m_position; + + if (remainingBytes <= 0) + return 0; + + if (length > remainingBytes) + length = (int)remainingBytes; + + int bytesRead = length; + + // Must read 1 block at a time + do + { + int blockOffset = (int)(m_position & BlockMask); + int bytesToRead = Math.Min(length, BlockSize - blockOffset); + + Buffer.BlockCopy(m_blocks[(int)(m_position >> ShiftBits)], blockOffset, buffer, startIndex, bytesToRead); + + length -= bytesToRead; + startIndex += bytesToRead; + m_position += bytesToRead; + } + while (length > 0); + + return bytesRead; + } + + /// + /// Reads a byte from the current stream. + /// + /// + /// The current byte cast to an , or -1 if the end of the stream has been reached. + /// + /// The stream is closed. + public override int ReadByte() + { + if (m_disposed) + throw new ObjectDisposedException(nameof(BlockAllocatedMemoryStream), "The stream is closed."); + + if (m_position >= m_length) + return -1; + + byte value = m_blocks[(int)(m_position >> ShiftBits)][(int)(m_position & BlockMask)]; + m_position++; + + return value; + } + + /// + /// Writes a block of bytes to the current stream using data read from . + /// + /// The buffer to write data from. + /// The byte offset in at which to begin writing from. + /// The maximum number of bytes to write. + /// is null. + /// + /// or is less than 0 -or- + /// and will exceed length. + /// + /// The stream is closed. + public override void Write(byte[] buffer, int startIndex, int length) + { + if (m_disposed) + throw new ObjectDisposedException(nameof(BlockAllocatedMemoryStream), "The stream is closed."); + + buffer.ValidateParameters(startIndex, length); + + if (m_position + length > m_capacity) + EnsureCapacity(m_position + length); + + if (m_position > m_length) + InitializeToPosition(m_position); + + if (m_length < m_position + length) + m_length = m_position + length; + + if (length == 0) + return; + + do + { + int blockOffset = (int)(m_position & BlockMask); + int bytesToWrite = Math.Min(length, BlockSize - blockOffset); + + Buffer.BlockCopy(buffer, startIndex, m_blocks[(int)(m_position >> ShiftBits)], blockOffset, bytesToWrite); + + length -= bytesToWrite; + startIndex += bytesToWrite; + m_position += bytesToWrite; + } + while (length > 0); + } + + /// + /// Writes a byte to the current stream at the current position. + /// + /// The byte to write. + /// The stream is closed. + public override void WriteByte(byte value) + { + if (m_disposed) + throw new ObjectDisposedException(nameof(BlockAllocatedMemoryStream), "The stream is closed."); + + if (m_position + 1 > m_capacity) + EnsureCapacity(m_position + 1); + + if (m_position > m_length) + InitializeToPosition(m_position); + + if (m_length < m_position + 1) + m_length = m_position + 1; + + m_blocks[(int)(m_position >> ShiftBits)][m_position & BlockMask] = value; + m_position++; + } + + /// + /// Writes the stream contents to a byte array, regardless of the property. + /// + /// A [] containing the current data in the stream + /// + /// This may fail if there is not enough contiguous memory available to hold current size of stream. + /// When possible use methods which operate on streams directly instead. + /// + /// Cannot create a byte array with more than 2,147,483,591 elements. + /// The stream is closed. + public byte[] ToArray() + { + if (m_disposed) + throw new ObjectDisposedException(nameof(BlockAllocatedMemoryStream), "The stream is closed."); + + if (m_length > 0x7FFFFFC7L) + throw new InvalidOperationException($"Cannot create a byte array of size {m_length}"); + + byte[] destination = new byte[m_length]; + long originalPosition = m_position; + + m_position = 0; + Read(destination, 0, (int)m_length); + m_position = originalPosition; + + return destination; + } + + /// + /// Reads specified number of bytes from source stream into this + /// starting at the current position. + /// + /// The stream containing the data to copy + /// The number of bytes to copy + /// The stream is closed. + public void ReadFrom(Stream source, long length) + { + // Note: A faster way would be to write directly to the BlockAllocatedMemoryStream + if (m_disposed) + throw new ObjectDisposedException(nameof(BlockAllocatedMemoryStream), "The stream is closed."); + + byte[] buffer = s_memoryBlockPool.Dequeue(); + + do + { + int bytesRead = source.Read(buffer, 0, (int)Math.Min(BlockSize, length)); + + if (bytesRead == 0) + throw new EndOfStreamException(); + + length -= bytesRead; + Write(buffer, 0, bytesRead); + } + while (length > 0); + + s_memoryBlockPool.Enqueue(buffer); + } + + /// + /// Writes the entire stream into destination, regardless of , which remains unchanged. + /// + /// The stream onto which to write the current contents. + /// The stream is closed. + public void WriteTo(Stream destination) + { + if (m_disposed) + throw new ObjectDisposedException(nameof(BlockAllocatedMemoryStream), "The stream is closed."); + + long originalPosition = m_position; + m_position = 0; + + CopyTo(destination); + + m_position = originalPosition; + } + + /// + /// Overrides the method so that no action is performed. + /// + /// + /// + /// This method overrides the method. + /// + /// + /// Because any data written to a object is + /// written into RAM, this method is superfluous. + /// + /// + public override void Flush() + { + // Nothing to flush... + } + + /// + /// Makes sure desired can be accommodated by future data accesses. + /// + /// Minimum desired stream capacity. + private void EnsureCapacity(long length) + { + while (m_capacity < length) + { + m_blocks.Add(s_memoryBlockPool.Dequeue()); + m_capacity += BlockSize; + } + } + + /// + /// Initializes all of the bytes to zero. + /// + private void InitializeToPosition(long position) + { + long bytesToClear = position - m_length; + + while (bytesToClear > 0) + { + int bytesToClearInBlock = (int)Math.Min(bytesToClear, BlockSize - (m_length & BlockMask)); + Array.Clear(m_blocks[(int)(m_length >> ShiftBits)], (int)(m_length & BlockMask), bytesToClearInBlock); + m_length += bytesToClearInBlock; + bytesToClear = position - m_length; + } + } + + #endregion + + #region [ Static ] + + // Static Fields + + // Allow up to 100 items of 8KB items to remain on the buffer pool. This might need to be increased if the buffer pool becomes more + // extensively used. Allocation Statistics will be logged in the Logger. + private static readonly BufferPool s_memoryBlockPool = new(BlockSize, 100); + + #endregion + } + + #endregion + + #region [ Buffer Pool ] + + /// + /// Provides a thread safe queue that acts as a buffer pool. + /// + internal class BufferPool + { + private readonly int m_bufferSize; + private readonly ConcurrentQueue m_buffers; + private readonly Queue m_countHistory; + private readonly int m_targetCount; + private int m_objectsCreated; + + /// + /// Creates a new . + /// + /// The size of buffers in the pool. + /// the ideal number of buffers that are always pending on the queue. + public BufferPool(int bufferSize, int targetCount) + { + m_bufferSize = bufferSize; + m_targetCount = targetCount; + m_countHistory = new Queue(100); + m_buffers = new ConcurrentQueue(); + + new Action(RunCollection).DelayAndExecute(1000); + } + + private void RunCollection() + { + try + { + m_countHistory.Enqueue(m_buffers.Count); + + if (m_countHistory.Count < 60) + return; + + int objectsCreated = Interlocked.Exchange(ref m_objectsCreated, 0); + + // If there were ever more than the target items in the queue over the past 60 seconds remove some items. + // However, don't remove items if the pool ever got to 0 and had objects that had to be created. + int min = m_countHistory.Min(); + m_countHistory.Clear(); + + if (objectsCreated != 0) + return; + + while (min > m_targetCount) + { + if (!m_buffers.TryDequeue(out _)) + return; + + min--; + } + } + finally + { + new Action(RunCollection).DelayAndExecute(1000); + } + } + + /// + /// Removes a buffer from the queue. If one does not exist, one is created. + /// + /// + public byte[] Dequeue() + { + if (m_buffers.TryDequeue(out byte[]? item)) + return item; + + Interlocked.Increment(ref m_objectsCreated); + return new byte[m_bufferSize]; + } + + /// + /// Adds a buffer back to the queue. + /// + /// The buffer to queue. + public void Enqueue(byte[] buffer) => m_buffers.Enqueue(buffer); + } + + #endregion + +} diff --git a/VG Music Studio - Core/Util/DialogUtils.cs b/VG Music Studio - Core/Util/DialogUtils.cs new file mode 100644 index 0000000..0018d33 --- /dev/null +++ b/VG Music Studio - Core/Util/DialogUtils.cs @@ -0,0 +1,27 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.Util +{ + public abstract class DialogUtils + { + public abstract string CreateLoadDialog( + string title, string filterName = "", + string fileExtensions = "", bool isFile = false, + bool allowAllFiles = false, object? parent = null); + public abstract string CreateLoadDialog( + string title, string filterName, + Span fileExtensions, bool isFile = false, + bool allowAllFiles = false, object? parent = null); + + public abstract string CreateSaveDialog( + string fileName, string title, + string filterName = "", string fileExtension = "", + bool isFile = false, bool allowAllFiles = false, + object? parent = null); + public abstract string CreateSaveDialog( + string fileName, string title, + string filterName, Span fileExtensions, + bool isFile = false, bool allowAllFiles = false, + object? parent = null); + } +} diff --git a/VG Music Studio - Core/Util/GUIUtils.cs b/VG Music Studio - Core/Util/GUIUtils.cs new file mode 100644 index 0000000..8cdd39a --- /dev/null +++ b/VG Music Studio - Core/Util/GUIUtils.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Kermalis.VGMusicStudio.Core.Util; + +public static class GUIUtils +{ + private static readonly Random _rng = new(); + + public static string Print(this IEnumerable source, bool parenthesis = true) + { + string str = parenthesis ? "( " : ""; + str += string.Join(", ", source); + str += parenthesis ? " )" : ""; + return str; + } + /// Fisher-Yates Shuffle + public static void Shuffle(this IList source) + { + for (int a = 0; a < source.Count - 1; a++) + { + int b = _rng.Next(a, source.Count); + (source[b], source[a]) = (source[a], source[b]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static float Lerp(float progress, float from, float to) + { + return from + ((to - from) * progress); + } + /// Maps a value in the range [a1, a2] to [b1, b2]. Divide by zero occurs if a1 and a2 are equal + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static float Lerp(float value, float a1, float a2, float b1, float b2) + { + return b1 + ((value - a1) / (a2 - a1) * (b2 - b1)); + } +} diff --git a/VG Music Studio - Core/Util/Int24.cs b/VG Music Studio - Core/Util/Int24.cs new file mode 100644 index 0000000..28f9492 --- /dev/null +++ b/VG Music Studio - Core/Util/Int24.cs @@ -0,0 +1,1515 @@ +//****************************************************************************************************** +// Int24.cs - Gbtc +// +// Copyright © 2012, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://www.opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 11/12/2004 - J. Ritchie Carroll +// Initial version of source generated. +// 08/3/2009 - Josh L. Patterson +// Updated comments. +// 08/11/2009 - Josh L. Patterson +// Updated comments. +// 09/14/2009 - Stephen C. Wills +// Added new header and license agreement. +// 12/14/2012 - Starlynn Danyelle Gilliam +// Modified Header. +// +//****************************************************************************************************** + +#region [ Contributor License Agreements ] + +/**************************************************************************\ + Copyright © 2009 - J. Ritchie Carroll + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +\**************************************************************************/ + +#endregion + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace System; + +/// Represents a 3-byte, 24-bit signed integer. +/// +/// +/// This class behaves like most other intrinsic signed integers but allows a 3-byte, 24-bit integer implementation +/// that is often found in many digital-signal processing arenas and different kinds of protocol parsing. A signed +/// 24-bit integer is typically used to save storage space on disk where its value range of -8388608 to 8388607 is +/// sufficient, but the signed Int16 value range of -32768 to 32767 is too small. +/// +/// +/// This structure uses an Int32 internally for storage and most other common expected integer functionality, so using +/// a 24-bit integer will not save memory. However, if the 24-bit signed integer range (-8388608 to 8388607) suits your +/// data needs you can save disk space by only storing the three bytes that this integer actually consumes. You can do +/// this by calling the Int24.GetBytes function to return a three byte binary array that can be serialized to the desired +/// destination and then calling the Int24.GetValue function to restore the Int24 value from those three bytes. +/// +/// +/// All the standard operators for the Int24 have been fully defined for use with both Int24 and Int32 signed integers; +/// you should find that without the exception Int24 can be compared and numerically calculated with an Int24 or Int32. +/// Necessary casting should be minimal and typical use should be very simple - just as if you are using any other native +/// signed integer. +/// +/// +[Serializable] +public struct Int24 : IComparable, IFormattable, IConvertible, IComparable, IComparable, IEquatable, IEquatable +{ + #region [ Members ] + + // Constants + private const int MaxValue32 = 8388607; // Represents the largest possible value of an Int24 as an Int32. + private const int MinValue32 = -8388608; // Represents the smallest possible value of an Int24 as an Int32. + + /// High byte bit-mask used when a 24-bit integer is stored within a 32-bit integer. This field is constant. + public const int BitMask = -16777216; + + // Fields + private readonly int m_value; // We internally store the Int24 value in a 4-byte integer for convenience + + #endregion + + #region [ Constructors ] + + /// Creates 24-bit signed integer from an existing 24-bit signed integer. + /// 24-but signed integer to create new Int24 from. + public Int24(Int24 value) + { + m_value = ApplyBitMask(value); + } + + /// Creates 24-bit signed integer from a 32-bit signed integer. + /// 32-bit signed integer to use as new 24-bit signed integer value. + /// Source values outside 24-bit min/max range will cause an overflow exception. + public Int24(int value) + { + ValidateNumericRange(value); + m_value = ApplyBitMask(value); + } + + /// Creates 24-bit signed integer from three bytes at a specified position in a byte array. + /// An array of bytes. + /// The starting position within . + /// + /// You can use this constructor in-lieu of a System.BitConverter.ToInt24 function. + /// Bytes endian order assumed to match that of currently executing process architecture (little-endian on Intel platforms). + /// + /// cannot be null. + /// is greater than length. + /// length from is too small to represent a . + public Int24(byte[] value, int startIndex) + { + m_value = GetValue(value, startIndex).m_value; + } + + #endregion + + #region [ Methods ] + + /// Returns the Int24 value as an array of three bytes. + /// An array of bytes with length 3. + /// + /// You can use this function in-lieu of a System.BitConverter.GetBytes function. + /// Bytes will be returned in endian order of currently executing process architecture (little-endian on Intel platforms). + /// + public byte[] GetBytes() + { + // Return serialized 3-byte representation of Int24 + return GetBytes(this); + } + + /// + /// Compares this instance to a specified object and returns an indication of their relative values. + /// + /// An object to compare, or null. + /// + /// A signed number indicating the relative values of this instance and value. Returns less than zero + /// if this instance is less than value, zero if this instance is equal to value, or greater than zero + /// if this instance is greater than value. + /// + /// value is not an Int32 or Int24. + public int CompareTo(object? value) + { + if (value is null) + return 1; + + if (value is not int && value is not Int24) + throw new ArgumentException("Argument must be an Int32 or an Int24"); + + int num = (int)value; + + return m_value < num ? -1 : m_value > num ? 1 : 0; + } + + /// + /// Compares this instance to a specified 24-bit signed integer and returns an indication of their + /// relative values. + /// + /// An integer to compare. + /// + /// A signed number indicating the relative values of this instance and value. Returns less than zero + /// if this instance is less than value, zero if this instance is equal to value, or greater than zero + /// if this instance is greater than value. + /// + public int CompareTo(Int24 value) + { + return CompareTo((int)value); + } + + /// + /// Compares this instance to a specified 32-bit signed integer and returns an indication of their + /// relative values. + /// + /// An integer to compare. + /// + /// A signed number indicating the relative values of this instance and value. Returns less than zero + /// if this instance is less than value, zero if this instance is equal to value, or greater than zero + /// if this instance is greater than value. + /// + public int CompareTo(int value) + { + return m_value < value ? -1 : m_value > value ? 1 : 0; + } + + /// + /// Returns a value indicating whether this instance is equal to a specified object. + /// + /// An object to compare, or null. + /// + /// True if obj is an instance of Int32 or Int24 and equals the value of this instance; + /// otherwise, False. + /// + public override bool Equals(object? obj) + { + if (obj is int or Int24) + return Equals((int)obj); + + return false; + } + + /// + /// Returns a value indicating whether this instance is equal to a specified Int24 value. + /// + /// An Int24 value to compare to this instance. + /// + /// True if obj has the same value as this instance; otherwise, False. + /// + public bool Equals(Int24 obj) + { + return Equals((int)obj); + } + + /// + /// Returns a value indicating whether this instance is equal to a specified Int32 value. + /// + /// An Int32 value to compare to this instance. + /// + /// True if obj has the same value as this instance; otherwise, False. + /// + public bool Equals(int obj) + { + return m_value == obj; + } + + /// + /// Returns the hash code for this instance. + /// + /// + /// A 32-bit signed integer hash code. + /// + public override int GetHashCode() + { + return m_value; + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation. + /// + /// + /// The string representation of the value of this instance, consisting of a minus sign if + /// the value is negative, and a sequence of digits ranging from 0 to 9 with no leading zeroes. + /// + public override string ToString() + { + return m_value.ToString(); + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation, using + /// the specified format. + /// + /// A format string. + /// + /// The string representation of the value of this instance as specified by format. + /// + public string ToString(string? format) + { + return m_value.ToString(format); + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation using the + /// specified culture-specific format information. + /// + /// + /// A that supplies culture-specific formatting information. + /// + /// + /// The string representation of the value of this instance as specified by provider. + /// + public string ToString(IFormatProvider? provider) + { + return m_value.ToString(provider); + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation using the + /// specified format and culture-specific format information. + /// + /// A format specification. + /// + /// A that supplies culture-specific formatting information. + /// + /// + /// The string representation of the value of this instance as specified by format and provider. + /// + public string ToString(string? format, IFormatProvider? provider) + { + return m_value.ToString(format, provider); + } + + /// + /// Converts the string representation of a number to its 24-bit signed integer equivalent. + /// + /// A string containing a number to convert. + /// + /// A 24-bit signed integer equivalent to the number contained in s. + /// + /// s is null. + /// + /// s represents a number less than Int24.MinValue or greater than Int24.MaxValue. + /// + /// s is not in the correct format. + public static Int24 Parse(string s) + { + return (Int24)int.Parse(s); + } + + /// + /// Converts the string representation of a number in a specified style to its 24-bit signed integer equivalent. + /// + /// A string containing a number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates the permitted format of s. + /// A typical value to specify is System.Globalization.NumberStyles.Integer. + /// + /// + /// A 24-bit signed integer equivalent to the number contained in s. + /// + /// + /// style is not a System.Globalization.NumberStyles value. -or- style is not a combination of + /// System.Globalization.NumberStyles.AllowHexSpecifier and System.Globalization.NumberStyles.HexNumber values. + /// + /// s is null. + /// + /// s represents a number less than Int24.MinValue or greater than Int24.MaxValue. + /// + /// s is not in a format compliant with style. + public static Int24 Parse(string s, NumberStyles style) + { + return (Int24)int.Parse(s, style); + } + + /// + /// Converts the string representation of a number in a specified culture-specific format to its 24-bit + /// signed integer equivalent. + /// + /// A string containing a number to convert. + /// + /// A that supplies culture-specific formatting information about s. + /// + /// + /// A 24-bit signed integer equivalent to the number contained in s. + /// + /// s is null. + /// + /// s represents a number less than Int24.MinValue or greater than Int24.MaxValue. + /// + /// s is not in the correct format. + public static Int24 Parse(string s, IFormatProvider? provider) + { + return (Int24)int.Parse(s, provider); + } + + /// + /// Converts the string representation of a number in a specified style and culture-specific format to its 24-bit + /// signed integer equivalent. + /// + /// A string containing a number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates the permitted format of s. + /// A typical value to specify is System.Globalization.NumberStyles.Integer. + /// + /// + /// A that supplies culture-specific formatting information about s. + /// + /// + /// A 24-bit signed integer equivalent to the number contained in s. + /// + /// + /// style is not a System.Globalization.NumberStyles value. -or- style is not a combination of + /// System.Globalization.NumberStyles.AllowHexSpecifier and System.Globalization.NumberStyles.HexNumber values. + /// + /// s is null. + /// + /// s represents a number less than Int24.MinValue or greater than Int24.MaxValue. + /// + /// s is not in a format compliant with style. + public static Int24 Parse(string s, NumberStyles style, IFormatProvider? provider) + { + return (Int24)int.Parse(s, style, provider); + } + + /// + /// Converts the string representation of a number to its 24-bit signed integer equivalent. A return value + /// indicates whether the conversion succeeded or failed. + /// + /// A string containing a number to convert. + /// + /// When this method returns, contains the 24-bit signed integer value equivalent to the number contained in s, + /// if the conversion succeeded, or zero if the conversion failed. The conversion fails if the s parameter is null, + /// is not of the correct format, or represents a number less than Int24.MinValue or greater than Int24.MaxValue. + /// This parameter is passed uninitialized. + /// + /// true if s was converted successfully; otherwise, false. + public static bool TryParse(string s, out Int24 result) + { + bool parseResponse = int.TryParse(s, out int parseResult); + + try + { + result = (Int24)parseResult; + } + catch + { + result = (Int24)0; + parseResponse = false; + } + + return parseResponse; + } + + + /// + /// Converts the string representation of a number in a specified style and culture-specific format to its + /// 24-bit signed integer equivalent. A return value indicates whether the conversion succeeded or failed. + /// + /// A string containing a number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates the permitted format of s. + /// A typical value to specify is System.Globalization.NumberStyles.Integer. + /// + /// + /// When this method returns, contains the 24-bit signed integer value equivalent to the number contained in s, + /// if the conversion succeeded, or zero if the conversion failed. The conversion fails if the s parameter is null, + /// is not in a format compliant with style, or represents a number less than Int24.MinValue or greater than + /// Int24.MaxValue. This parameter is passed uninitialized. + /// + /// + /// A object that supplies culture-specific formatting information about s. + /// + /// true if s was converted successfully; otherwise, false. + /// + /// style is not a System.Globalization.NumberStyles value. -or- style is not a combination of + /// System.Globalization.NumberStyles.AllowHexSpecifier and System.Globalization.NumberStyles.HexNumber values. + /// + public static bool TryParse(string s, NumberStyles style, IFormatProvider provider, out Int24 result) + { + bool parseResponse = int.TryParse(s, style, provider, out int parseResult); + + try + { + result = (Int24)parseResult; + } + catch + { + result = (Int24)0; + parseResponse = false; + } + + return parseResponse; + } + + /// + /// Returns the System.TypeCode for value type System.Int32 (there is no defined type code for an Int24). + /// + /// The enumerated constant, System.TypeCode.Int32. + /// + /// There is no defined Int24 type code and since an Int24 will easily fit inside an Int32, the + /// Int32 type code is returned. + /// + public TypeCode GetTypeCode() + { + return TypeCode.Int32; + } + + #region [ Explicit IConvertible Implementation ] + + // These are explicitly implemented on the native integer implementations, so we do the same... + + bool IConvertible.ToBoolean(IFormatProvider? provider) + { + return Convert.ToBoolean(m_value, provider); + } + + char IConvertible.ToChar(IFormatProvider? provider) + { + return Convert.ToChar(m_value, provider); + } + + sbyte IConvertible.ToSByte(IFormatProvider? provider) + { + return Convert.ToSByte(m_value, provider); + } + + byte IConvertible.ToByte(IFormatProvider? provider) + { + return Convert.ToByte(m_value, provider); + } + + short IConvertible.ToInt16(IFormatProvider? provider) + { + return Convert.ToInt16(m_value, provider); + } + + ushort IConvertible.ToUInt16(IFormatProvider? provider) + { + return Convert.ToUInt16(m_value, provider); + } + + int IConvertible.ToInt32(IFormatProvider? provider) + { + return m_value; + } + + uint IConvertible.ToUInt32(IFormatProvider? provider) + { + return Convert.ToUInt32(m_value, provider); + } + + long IConvertible.ToInt64(IFormatProvider? provider) + { + return Convert.ToInt64(m_value, provider); + } + + ulong IConvertible.ToUInt64(IFormatProvider? provider) + { + return Convert.ToUInt64(m_value, provider); + } + + float IConvertible.ToSingle(IFormatProvider? provider) + { + return Convert.ToSingle(m_value, provider); + } + + double IConvertible.ToDouble(IFormatProvider? provider) + { + return Convert.ToDouble(m_value, provider); + } + + decimal IConvertible.ToDecimal(IFormatProvider? provider) + { + return Convert.ToDecimal(m_value, provider); + } + + DateTime IConvertible.ToDateTime(IFormatProvider? provider) + { + return Convert.ToDateTime(m_value, provider); + } + + object IConvertible.ToType(Type type, IFormatProvider? provider) + { + return Convert.ChangeType(m_value, type, provider); + } + + #endregion + + #endregion + + #region [ Operators ] + + // Every effort has been made to make Int24 as cleanly interoperable with Int32 as possible... + + #region [ Comparison Operators ] + + /// + /// Compares the two values for equality. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean value indicating equality. + public static bool operator ==(Int24 value1, Int24 value2) + { + return value1.Equals(value2); + } + + /// + /// Compares the two values for equality. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean value indicating equality. + public static bool operator ==(int value1, Int24 value2) + { + return value1.Equals(value2); + } + + /// + /// Compares the two values for equality. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean value indicating equality. + public static bool operator ==(Int24 value1, int value2) + { + return ((int)value1).Equals(value2); + } + + /// + /// Compares the two values for inequality. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating the result of the inequality. + public static bool operator !=(Int24 value1, Int24 value2) + { + return !value1.Equals(value2); + } + + /// + /// Compares the two values for inequality. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating the result of the inequality. + public static bool operator !=(int value1, Int24 value2) + { + return !value1.Equals(value2); + } + + /// + /// Compares the two values for inequality. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating the result of the inequality. + public static bool operator !=(Int24 value1, int value2) + { + return !((int)value1).Equals(value2); + } + + /// + /// Returns true if left value is less than right value. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating whether the left value was less than the right value. + public static bool operator <(Int24 value1, Int24 value2) + { + return value1.CompareTo(value2) < 0; + } + + /// + /// Returns true if left value is less than right value. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating whether the left value was less than the right value. + public static bool operator <(int value1, Int24 value2) + { + return value1.CompareTo(value2) < 0; + } + + /// + /// Returns true if left value is less than right value. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating whether the left value was less than the right value. + public static bool operator <(Int24 value1, int value2) + { + return value1.CompareTo(value2) < 0; + } + + /// + /// Returns true if left value is less or equal to than right value. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating whether the left value was less than the right value. + public static bool operator <=(Int24 value1, Int24 value2) + { + return value1.CompareTo(value2) <= 0; + } + + /// + /// Returns true if left value is less or equal to than right value. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating whether the left value was less than the right value. + public static bool operator <=(int value1, Int24 value2) + { + return value1.CompareTo(value2) <= 0; + } + + /// + /// Returns true if left value is less or equal to than right value. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating whether the left value was less than the right value. + public static bool operator <=(Int24 value1, int value2) + { + return value1.CompareTo(value2) <= 0; + } + + /// + /// Returns true if left value is greater than right value. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating whether the left value was greater than the right value. + public static bool operator >(Int24 value1, Int24 value2) + { + return value1.CompareTo(value2) > 0; + } + + /// + /// Returns true if left value is greater than right value. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating whether the left value was greater than the right value. + public static bool operator >(int value1, Int24 value2) + { + return value1.CompareTo(value2) > 0; + } + + /// + /// Returns true if left value is greater than right value. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating whether the left value was greater than the right value. + public static bool operator >(Int24 value1, int value2) + { + return value1.CompareTo(value2) > 0; + } + + /// + /// Returns true if left value is greater than or equal to right value. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating whether the left value was greater than or equal to the right value. + public static bool operator >=(Int24 value1, Int24 value2) + { + return value1.CompareTo(value2) >= 0; + } + + /// + /// Returns true if left value is greater than or equal to right value. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating whether the left value was greater than or equal to the right value. + public static bool operator >=(int value1, Int24 value2) + { + return value1.CompareTo(value2) >= 0; + } + + /// + /// Returns true if left value is greater than or equal to right value. + /// + /// Left hand operand. + /// Right hand operand. + /// Boolean indicating whether the left value was greater than or equal to the right value. + public static bool operator >=(Int24 value1, int value2) + { + return value1.CompareTo(value2) >= 0; + } + + #endregion + + #region [ Type Conversion Operators ] + + #region [ Explicit Narrowing Conversions ] + + /// + /// Explicitly converts value to an . + /// + /// Enum value that is converted. + /// Int24 + public static explicit operator Int24(Enum value) + { + return new Int24(Convert.ToInt32(value)); + } + + /// + /// Explicitly converts value to an . + /// + /// String value that is converted. + /// Int24 + public static explicit operator Int24(string value) + { + return new Int24(Convert.ToInt32(value)); + } + + /// + /// Explicitly converts value to an . + /// + /// Decimal value that is converted. + /// Int24 + public static explicit operator Int24(decimal value) + { + return new Int24(Convert.ToInt32(value)); + } + + /// + /// Explicitly converts value to an . + /// + /// Double value that is converted. + /// Int24 + public static explicit operator Int24(double value) + { + return new Int24(Convert.ToInt32(value)); + } + + /// + /// Explicitly converts value to an . + /// + /// Float value that is converted. + /// Int24 + public static explicit operator Int24(float value) + { + return new Int24(Convert.ToInt32(value)); + } + + /// + /// Explicitly converts value to an . + /// + /// Long value that is converted. + /// Int24 + public static explicit operator Int24(long value) + { + return new Int24(Convert.ToInt32(value)); + } + + /// + /// Explicitly converts value to an . + /// + /// Integer value that is converted. + /// Int24 + public static explicit operator Int24(int value) + { + return new Int24(value); + } + + /// + /// Explicitly converts to . + /// + /// Int24 value that is converted. + /// Short + public static explicit operator short(Int24 value) + { + return (short)(int)value; + } + + /// + /// Explicitly converts to . + /// + /// Int24 value that is converted. + /// Unsigned Short + public static explicit operator ushort(Int24 value) + { + return (ushort)(uint)value; + } + + /// + /// Explicitly converts to . + /// + /// Int24 value that is converted. + /// Byte + public static explicit operator byte(Int24 value) + { + return (byte)(int)value; + } + + #endregion + + #region [ Implicit Widening Conversions ] + + /// + /// Implicitly converts value to an . + /// + /// Byte value that is converted to an . + /// An value. + public static implicit operator Int24(byte value) + { + return new Int24((int)value); + } + + /// + /// Implicitly converts value to an . + /// + /// Char value that is converted to an . + /// An value. + public static implicit operator Int24(char value) + { + return new Int24((int)value); + } + + /// + /// Implicitly converts value to an . + /// + /// Short value that is converted to an . + /// An value. + public static implicit operator Int24(short value) + { + return new Int24((int)value); + } + + /// + /// Implicitly converts to . + /// + /// value that is converted to an . + /// An value. + public static implicit operator int(Int24 value) + { + return ((IConvertible)value).ToInt32(null); + } + + /// + /// Implicitly converts to . + /// + /// value that is converted to an unsigned integer. + /// Unsigned integer + public static implicit operator uint(Int24 value) + { + return ((IConvertible)value).ToUInt32(null); + } + + /// + /// Implicitly converts to . + /// + /// value that is converted to an . + /// An value. + public static implicit operator long(Int24 value) + { + return ((IConvertible)value).ToInt64(null); + } + + /// + /// Implicitly converts to . + /// + /// value that is converted to an . + /// An value. + public static implicit operator ulong(Int24 value) + { + return ((IConvertible)value).ToUInt64(null); + } + + /// + /// Implicitly converts to . + /// + /// value that is converted to an . + /// A value. + public static implicit operator double(Int24 value) + { + return ((IConvertible)value).ToDouble(null); + } + + /// + /// Implicitly converts to . + /// + /// value that is converted to an . + /// A value. + public static implicit operator float(Int24 value) + { + return ((IConvertible)value).ToSingle(null); + } + + /// + /// Implicitly converts to . + /// + /// value that is converted to an . + /// A value. + public static implicit operator decimal(Int24 value) + { + return ((IConvertible)value).ToDecimal(null); + } + + /// + /// Implicitly converts to . + /// + /// value that is converted to an . + /// A value. + public static implicit operator string(Int24 value) + { + return value.ToString(CultureInfo.InvariantCulture); + } + + #endregion + + #endregion + + #region [ Boolean and Bitwise Operators ] + + /// + /// Returns true if value is not zero. + /// + /// Int24 value to test. + /// Boolean to indicate whether the value was not equal to zero. + public static bool operator true(Int24 value) + { + return value != 0; + } + + /// + /// Returns true if value is equal to zero. + /// + /// Int24 value to test. + /// Boolean to indicate whether the value was equal to zero. + public static bool operator false(Int24 value) + { + return value == 0; + } + + /// + /// Returns bitwise complement of value. + /// + /// value as operand. + /// as result. + public static Int24 operator ~(Int24 value) + { + return (Int24)ApplyBitMask(~(int)value); + } + + /// + /// Returns logical bitwise AND of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Int24 as result of operation. + public static Int24 operator &(Int24 value1, Int24 value2) + { + return (Int24)ApplyBitMask((int)value1 & (int)value2); + } + + /// + /// Returns logical bitwise AND of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Integer as result of operation. + public static int operator &(int value1, Int24 value2) + { + return value1 & (int)value2; + } + + /// + /// Returns logical bitwise AND of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Integer as result of operation. + public static int operator &(Int24 value1, int value2) + { + return (int)value1 & value2; + } + + /// + /// Returns logical bitwise OR of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Int24 as result of operation. + public static Int24 operator |(Int24 value1, Int24 value2) + { + return (Int24)ApplyBitMask((int)value1 | (int)value2); + } + + /// + /// Returns logical bitwise OR of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Integer as result of operation. + public static int operator |(int value1, Int24 value2) + { + return value1 | (int)value2; + } + + /// + /// Returns logical bitwise OR of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Integer as result of operation. + public static int operator |(Int24 value1, int value2) + { + return (int)value1 | value2; + } + + /// + /// Returns logical bitwise exclusive-OR of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Integer value of the resulting exclusive-OR operation. + public static Int24 operator ^(Int24 value1, Int24 value2) + { + return (Int24)ApplyBitMask((int)value1 ^ (int)value2); + } + + /// + /// Returns logical bitwise exclusive-OR of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Integer value of the resulting exclusive-OR operation. + public static int operator ^(int value1, Int24 value2) + { + return value1 ^ (int)value2; + } + + /// + /// Returns logical bitwise exclusive-OR of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Integer value of the resulting exclusive-OR operation. + public static int operator ^(Int24 value1, int value2) + { + return (int)value1 ^ value2; + } + + /// + /// Returns value after right shifts of first value by the number of bits specified by second value. + /// + /// value to shift. + /// shifts indicates how many places to shift. + /// An value. + public static Int24 operator >>(Int24 value, int shifts) + { + return (Int24)ApplyBitMask((int)value >> shifts); + } + + /// + /// Returns value after left shifts of first value by the number of bits specified by second value. + /// + /// value to shift. + /// shifts indicates how many places to shift. + /// An value. + public static Int24 operator <<(Int24 value, int shifts) + { + return (Int24)ApplyBitMask((int)value << shifts); + } + + #endregion + + #region [ Arithmetic Operators ] + + /// + /// Returns computed remainder after dividing first value by the second. + /// + /// value as numerator. + /// value as denominator. + /// as remainder + public static Int24 operator %(Int24 value1, Int24 value2) + { + return (Int24)((int)value1 % (int)value2); + } + + /// + /// Returns computed remainder after dividing first value by the second. + /// + /// value as numerator. + /// value as denominator. + /// as remainder + public static int operator %(int value1, Int24 value2) + { + return value1 % (int)value2; + } + + /// + /// Returns computed remainder after dividing first value by the second. + /// + /// value as numerator. + /// value as denominator. + /// as remainder + public static int operator %(Int24 value1, int value2) + { + return (int)value1 % value2; + } + + /// + /// Returns computed sum of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Int24 result of addition. + public static Int24 operator +(Int24 value1, Int24 value2) + { + return (Int24)((int)value1 + (int)value2); + } + + /// + /// Returns computed sum of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Integer result of addition. + public static int operator +(int value1, Int24 value2) + { + return value1 + (int)value2; + } + + /// + /// Returns computed sum of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Integer result of addition. + public static int operator +(Int24 value1, int value2) + { + return (int)value1 + value2; + } + + /// + /// Returns computed difference of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Int24 result of subtraction. + public static Int24 operator -(Int24 value1, Int24 value2) + { + return (Int24)((int)value1 - (int)value2); + } + + /// + /// Returns computed difference of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Integer result of subtraction. + public static int operator -(int value1, Int24 value2) + { + return value1 - (int)value2; + } + + /// + /// Returns computed difference of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Integer result of subtraction. + public static int operator -(Int24 value1, int value2) + { + return (int)value1 - value2; + } + + /// + /// Returns incremented value. + /// + /// The operand. + /// Int24 result of increment. + public static Int24 operator ++(Int24 value) + { + return (Int24)(value + 1); + } + + /// + /// Returns decremented value. + /// + /// The operand. + /// Int24 result of decrement. + public static Int24 operator --(Int24 value) + { + return (Int24)(value - 1); + } + + /// + /// Returns computed product of values. + /// + /// value as left hand operand. + /// value as right hand operand. + /// as result + public static Int24 operator *(Int24 value1, Int24 value2) + { + return (Int24)((int)value1 * (int)value2); + } + + /// + /// Returns computed product of values. + /// + /// value as left hand operand. + /// value as right hand operand. + /// as result + public static int operator *(int value1, Int24 value2) + { + return value1 * (int)value2; + } + + /// + /// Returns computed product of values. + /// + /// value as left hand operand. + /// value as right hand operand. + /// as result + public static int operator *(Int24 value1, int value2) + { + return (int)value1 * value2; + } + + // Integer division operators + + /// + /// Returns computed division of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Int24 result of operation. + public static Int24 operator /(Int24 value1, Int24 value2) + { + return (Int24)((int)value1 / (int)value2); + } + + /// + /// Returns computed division of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Integer result of operation. + public static int operator /(int value1, Int24 value2) + { + return value1 / (int)value2; + } + + /// + /// Returns computed division of values. + /// + /// Left hand operand. + /// Right hand operand. + /// Integer result of operation. + public static int operator /(Int24 value1, int value2) + { + return (int)value1 / value2; + } + + //// Standard division operators + //public static double operator /(Int24 value1, Int24 value2) + //{ + // return ((double)value1 / (double)value2); + //} + + //public static double operator /(int value1, Int24 value2) + //{ + // return ((double)value1 / (double)value2); + //} + + //public static double operator /(Int24 value1, int value2) + //{ + // return ((double)value1 / (double)value2); + //} + + // C# doesn't expose an exponent operator but some other .NET languages do, + // so we expose the operator via its native special IL function name + + /// + /// Returns result of first value raised to power of second value. + /// + /// Left hand operand. + /// Right hand operand. + /// Double that is the result of the operation. + [EditorBrowsable(EditorBrowsableState.Advanced), SpecialName] + public static double op_Exponent(Int24 value1, Int24 value2) + { + return Math.Pow(value1, value2); + } + + /// + /// Returns result of first value raised to power of second value. + /// + /// Left hand operand. + /// Right hand operand. + /// Double that is the result of the operation. + [EditorBrowsable(EditorBrowsableState.Advanced), SpecialName] + public static double op_Exponent(int value1, Int24 value2) + { + return Math.Pow(value1, value2); + } + + /// + /// Returns result of first value raised to power of second value. + /// + /// Left hand operand. + /// Right hand operand. + /// Double that is the result of the operation. + [EditorBrowsable(EditorBrowsableState.Advanced), SpecialName] + public static double op_Exponent(Int24 value1, int value2) + { + return Math.Pow(value1, value2); + } + + #endregion + + #endregion + + #region [ Static ] + + /// + /// Represents the largest possible value of an Int24. This field is constant. + /// + public static readonly Int24 MaxValue = (Int24)MaxValue32; + + /// + /// Represents the smallest possible value of an Int24. This field is constant. + /// + public static readonly Int24 MinValue = (Int24)MinValue32; + + /// Returns the specified Int24 value as an array of three bytes. + /// Int24 value to convert to bytes. + /// An array of bytes with length 3. + /// + /// You can use this function in-lieu of a System.BitConverter.GetBytes(Int24) function. + /// Bytes will be returned in endian order of currently executing process architecture (little-endian on Intel platforms). + /// + public static byte[] GetBytes(Int24 value) + { + // We use a 32-bit integer to store 24-bit integer internally + byte[] data = new byte[3]; + int valueInt = value; + + if (BitConverter.IsLittleEndian) + { + data[0] = (byte)valueInt; + data[1] = (byte)(valueInt >> 8); + data[2] = (byte)(valueInt >> 16); + } + else + { + data[0] = (byte)(valueInt >> 16); + data[1] = (byte)(valueInt >> 8); + data[2] = (byte)valueInt; + } + + // Return serialized 3-byte representation of Int24 + return data; + } + + /// Returns a 24-bit signed integer from three bytes at a specified position in a byte array. + /// An array of bytes. + /// The starting position within value. + /// A 24-bit signed integer formed by three bytes beginning at startIndex. + /// + /// You can use this function in-lieu of a System.BitConverter.ToInt24 function. + /// Bytes endian order assumed to match that of currently executing process architecture (little-endian on Intel platforms). + /// + /// cannot be null. + /// is greater than length. + /// length from is too small to represent an . + public static Int24 GetValue(byte[] value, int startIndex) + { + value.ValidateParameters(startIndex, 3); + int valueInt; + + if (BitConverter.IsLittleEndian) + { + valueInt = value[startIndex] | + value[startIndex + 1] << 8 | + value[startIndex + 2] << 16; + } + else + { + valueInt = value[startIndex] << 16 | + value[startIndex + 1] << 8 | + value[startIndex + 2]; + } + + // Deserialize value + return (Int24)ApplyBitMask(valueInt); + } + + private static void ValidateNumericRange(int value) + { + if (value is > MaxValue32 + 1 or < MinValue32) + throw new OverflowException($"Value of {value} will not fit in a 24-bit signed integer"); + } + + private static int ApplyBitMask(int value) + { + // Check bit 23, the sign bit in a signed 24-bit integer + if ((value & 0x00800000) > 0) + { + // If the sign-bit is set, this number will be negative - set all high-byte bits (keeps 32-bit number in 24-bit range) + value |= BitMask; + } + else + { + // If the sign-bit is not set, this number will be positive - clear all high-byte bits (keeps 32-bit number in 24-bit range) + value &= ~BitMask; + } + + return value; + } + + #endregion +} diff --git a/VG Music Studio - Core/Util/SampleUtils.cs b/VG Music Studio - Core/Util/SampleUtils.cs index cbce3fb..56e8e81 100644 --- a/VG Music Studio - Core/Util/SampleUtils.cs +++ b/VG Music Studio - Core/Util/SampleUtils.cs @@ -4,12 +4,36 @@ namespace Kermalis.VGMusicStudio.Core.Util; internal static class SampleUtils { - public static void PCMU8ToPCM16(ReadOnlySpan src, Span dest) - { - for (int i = 0; i < src.Length; i++) - { - byte b = src[i]; - dest[i] = (short)((b - 0x80) << 8); - } - } + public static void PCMU8ToPCM16(ReadOnlySpan src, Span dest) + { + for (int i = 0; i < src.Length; i++) + { + byte b = src[i]; + dest[i] = (short)((b - 0x80) << 8); + } + } + public static void PCM16ToPCMU8(ReadOnlySpan src, Span dest) + { + for (int i = 0; i < src.Length; i++) + { + short b = src[i]; + dest[i] = (byte)((b + 0x8000) >> 8); + } + } + public static void PCM16ToPCM24(ReadOnlySpan src, Span dest) + { + for (int i = 0; i < src.Length; i++) + { + short b = src[i]; + dest[i] = (Int24)(b << 8); + } + } + public static void PCM24ToPCM16(ReadOnlySpan src, Span dest) + { + for (int i = 0; i < src.Length; i++) + { + Int24 b = src[i]; + dest[i] = (short)(b >> 8); + } + } } diff --git a/VG Music Studio - Core/Util/UInt24.cs b/VG Music Studio - Core/Util/UInt24.cs new file mode 100644 index 0000000..428ab6c --- /dev/null +++ b/VG Music Studio - Core/Util/UInt24.cs @@ -0,0 +1,1517 @@ +//****************************************************************************************************** +// UInt24.cs - Gbtc +// +// Copyright © 2012, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://www.opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 11/12/2004 - J. Ritchie Carroll +// Initial version of source generated. +// 08/4/2009 - Josh L. Patterson +// Edited Code Comments. +// 09/14/2009 - Stephen C. Wills +// Added new header and license agreement. +// 12/14/2012 - Starlynn Danyelle Gilliam +// Modified Header. +// +//****************************************************************************************************** + +#region [ Contributor License Agreements ] + +/**************************************************************************\ + Copyright © 2009 - J. Ritchie Carroll + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +\**************************************************************************/ + +#endregion + +using System.ComponentModel; +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace System; + +/// Represents a 3-byte, 24-bit unsigned integer. +/// +/// +/// This class behaves like most other intrinsic unsigned integers but allows a 3-byte, 24-bit integer implementation +/// that is often found in many digital-signal processing arenas and different kinds of protocol parsing. An unsigned +/// 24-bit integer is typically used to save storage space on disk where its value range of 0 to 16777215 is sufficient, +/// but the unsigned Int16 value range of 0 to 65535 is too small. +/// +/// +/// This structure uses an UInt32 internally for storage and most other common expected integer functionality, so using +/// a 24-bit integer will not save memory. However, if the 24-bit unsigned integer range (0 to 16777215) suits your +/// data needs you can save disk space by only storing the three bytes that this integer actually consumes. You can do +/// this by calling the UInt24.GetBytes function to return a three byte binary array that can be serialized to the desired +/// destination and then calling the UInt24.GetValue function to restore the UInt24 value from those three bytes. +/// +/// +/// All the standard operators for the UInt24 have been fully defined for use with both UInt24 and UInt32 unsigned integers; +/// you should find that without the exception UInt24 can be compared and numerically calculated with an UInt24 or UInt32. +/// Necessary casting should be minimal and typical use should be very simple - just as if you are using any other native +/// unsigned integer. +/// +/// +[Serializable] +public struct UInt24 : IComparable, IFormattable, IConvertible, IComparable, IComparable, IEquatable, IEquatable +{ + #region [ Members ] + + // Constants + private const uint MaxValue32 = 0x00ffffff; // Represents the largest possible value of an UInt24 as an UInt32. + private const uint MinValue32 = 0x00000000; // Represents the smallest possible value of an UInt24 as an UInt32. + + /// High byte bit-mask used when a 24-bit integer is stored within a 32-bit integer. This field is constant. + public const uint BitMask = 0xff000000; + + // Fields + private readonly uint m_value; // We internally store the UInt24 value in a 4-byte unsigned integer for convenience + + #endregion + + #region [ Constructors ] + + /// Creates 24-bit unsigned integer from an existing 24-bit unsigned integer. + /// A to create the new value from. + public UInt24(UInt24 value) + { + m_value = ApplyBitMask(value); + } + + /// Creates 24-bit unsigned integer from a 32-bit unsigned integer. + /// 32-bit unsigned integer to use as new 24-bit unsigned integer value. + /// Source values over 24-bit max range will cause an overflow exception. + public UInt24(uint value) + { + ValidateNumericRange(value); + m_value = ApplyBitMask(value); + } + + /// Creates 24-bit unsigned integer from three bytes at a specified position in a byte array. + /// An array of bytes. + /// The starting position within . + /// + /// You can use this constructor in-lieu of a System.BitConverter.ToUInt24 function. + /// Bytes endian order assumed to match that of currently executing process architecture (little-endian on Intel platforms). + /// + /// cannot be null. + /// is greater than length. + /// length from is too small to represent a . + public UInt24(byte[] value, int startIndex) + { + m_value = GetValue(value, startIndex).m_value; + } + + #endregion + + #region [ Methods ] + + /// Returns the UInt24 value as an array of three bytes. + /// An array of bytes with length 3. + /// + /// You can use this function in-lieu of a System.BitConverter.GetBytes function. + /// Bytes will be returned in endian order of currently executing process architecture (little-endian on Intel platforms). + /// + public byte[] GetBytes() + { + // Return serialized 3-byte representation of UInt24 + return GetBytes(this); + } + + /// + /// Compares this instance to a specified object and returns an indication of their relative values. + /// + /// An object to compare, or null. + /// + /// A signed number indicating the relative values of this instance and value. Returns less than zero + /// if this instance is less than value, zero if this instance is equal to value, or greater than zero + /// if this instance is greater than value. + /// + /// value is not an UInt32 or UInt24. + public int CompareTo(object? value) + { + if (value is null) + return 1; + + if (value is not uint && value is not UInt24) + throw new ArgumentException("Argument must be an UInt32 or an UInt24"); + + uint num = (uint)value; + + return m_value < num ? -1 : m_value > num ? 1 : 0; + } + + /// + /// Compares this instance to a specified 24-bit unsigned integer and returns an indication of their + /// relative values. + /// + /// An integer to compare. + /// + /// A signed number indicating the relative values of this instance and value. Returns less than zero + /// if this instance is less than value, zero if this instance is equal to value, or greater than zero + /// if this instance is greater than value. + /// + public int CompareTo(UInt24 value) + { + return CompareTo((uint)value); + } + + /// + /// Compares this instance to a specified 32-bit unsigned integer and returns an indication of their + /// relative values. + /// + /// An integer to compare. + /// + /// A signed number indicating the relative values of this instance and value. Returns less than zero + /// if this instance is less than value, zero if this instance is equal to value, or greater than zero + /// if this instance is greater than value. + /// + public int CompareTo(uint value) + { + return m_value < value ? -1 : m_value > value ? 1 : 0; + } + + /// + /// Returns a value indicating whether this instance is equal to a specified object. + /// + /// An object to compare, or null. + /// + /// True if obj is an instance of UInt32 or UInt24 and equals the value of this instance; + /// otherwise, False. + /// + public override bool Equals(object? obj) + { + if (obj is uint or UInt24) + return Equals((uint)obj); + + return false; + } + + /// + /// Returns a value indicating whether this instance is equal to a specified UInt24 value. + /// + /// An UInt24 value to compare to this instance. + /// + /// True if obj has the same value as this instance; otherwise, False. + /// + public bool Equals(UInt24 obj) + { + return Equals((uint)obj); + } + + /// + /// Returns a value indicating whether this instance is equal to a specified uint value. + /// + /// An UInt32 value to compare to this instance. + /// + /// True if obj has the same value as this instance; otherwise, False. + /// + public bool Equals(uint obj) + { + return m_value == obj; + } + + /// + /// Returns the hash code for this instance. + /// + /// + /// A 32-bit unsigned integer hash code. + /// + public override int GetHashCode() + { + unchecked + { + return (int)m_value; + } + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation. + /// + /// + /// The string representation of the value of this instance, consisting of a minus sign if + /// the value is negative, and a sequence of digits ranging from 0 to 9 with no leading zeroes. + /// + public override string ToString() + { + return m_value.ToString(); + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation, using + /// the specified format. + /// + /// A format string. + /// + /// The string representation of the value of this instance as specified by format. + /// + public string ToString(string? format) + { + return m_value.ToString(format); + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation using the + /// specified culture-specific format information. + /// + /// + /// A that supplies culture-specific formatting information. + /// + /// + /// The string representation of the value of this instance as specified by provider. + /// + public string ToString(IFormatProvider? provider) + { + return m_value.ToString(provider); + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation using the + /// specified format and culture-specific format information. + /// + /// A format specification. + /// + /// A that supplies culture-specific formatting information. + /// + /// + /// The string representation of the value of this instance as specified by format and provider. + /// + public string ToString(string? format, IFormatProvider? provider) + { + return m_value.ToString(format, provider); + } + + /// + /// Converts the string representation of a number to its 24-bit unsigned integer equivalent. + /// + /// A string containing a number to convert. + /// + /// A 24-bit unsigned integer equivalent to the number contained in s. + /// + /// s is null. + /// + /// s represents a number less than UInt24.MinValue or greater than UInt24.MaxValue. + /// + /// s is not in the correct format. + public static UInt24 Parse(string s) + { + return (UInt24)uint.Parse(s); + } + + /// + /// Converts the string representation of a number in a specified style to its 24-bit unsigned integer equivalent. + /// + /// A string containing a number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates the permitted format of s. + /// A typical value to specify is System.Globalization.NumberStyles.Integer. + /// + /// + /// A 24-bit unsigned integer equivalent to the number contained in s. + /// + /// + /// style is not a System.Globalization.NumberStyles value. -or- style is not a combination of + /// System.Globalization.NumberStyles.AllowHexSpecifier and System.Globalization.NumberStyles.HexNumber values. + /// + /// s is null. + /// + /// s represents a number less than UInt24.MinValue or greater than UInt24.MaxValue. + /// + /// s is not in a format compliant with style. + public static UInt24 Parse(string s, NumberStyles style) + { + return (UInt24)uint.Parse(s, style); + } + + /// + /// Converts the string representation of a number in a specified culture-specific format to its 24-bit + /// unsigned integer equivalent. + /// + /// A string containing a number to convert. + /// + /// A that supplies culture-specific formatting information about s. + /// + /// + /// A 24-bit unsigned integer equivalent to the number contained in s. + /// + /// s is null. + /// + /// s represents a number less than UInt24.MinValue or greater than UInt24.MaxValue. + /// + /// s is not in the correct format. + public static UInt24 Parse(string s, IFormatProvider? provider) + { + return (UInt24)uint.Parse(s, provider); + } + + /// + /// Converts the string representation of a number in a specified style and culture-specific format to its 24-bit + /// unsigned integer equivalent. + /// + /// A string containing a number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates the permitted format of s. + /// A typical value to specify is System.Globalization.NumberStyles.Integer. + /// + /// + /// A that supplies culture-specific formatting information about s. + /// + /// + /// A 24-bit unsigned integer equivalent to the number contained in s. + /// + /// + /// style is not a System.Globalization.NumberStyles value. -or- style is not a combination of + /// System.Globalization.NumberStyles.AllowHexSpecifier and System.Globalization.NumberStyles.HexNumber values. + /// + /// s is null. + /// + /// s represents a number less than UInt24.MinValue or greater than UInt24.MaxValue. + /// + /// s is not in a format compliant with style. + public static UInt24 Parse(string s, NumberStyles style, IFormatProvider? provider) + { + return (UInt24)uint.Parse(s, style, provider); + } + + /// + /// Converts the string representation of a number to its 24-bit unsigned integer equivalent. A return value + /// indicates whether the conversion succeeded or failed. + /// + /// A string containing a number to convert. + /// + /// When this method returns, contains the 24-bit unsigned integer value equivalent to the number contained in s, + /// if the conversion succeeded, or zero if the conversion failed. The conversion fails if the s parameter is null, + /// is not of the correct format, or represents a number less than UInt24.MinValue or greater than UInt24.MaxValue. + /// This parameter is passed uninitialized. + /// + /// true if s was converted successfully; otherwise, false. + public static bool TryParse(string s, out UInt24 result) + { + bool parseResponse = uint.TryParse(s, out uint parseResult); + + try + { + result = (UInt24)parseResult; + } + catch + { + result = 0; + parseResponse = false; + } + + return parseResponse; + } + + /// + /// Converts the string representation of a number in a specified style and culture-specific format to its + /// 24-bit unsigned integer equivalent. A return value indicates whether the conversion succeeded or failed. + /// + /// A string containing a number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates the permitted format of s. + /// A typical value to specify is System.Globalization.NumberStyles.Integer. + /// + /// + /// When this method returns, contains the 24-bit unsigned integer value equivalent to the number contained in s, + /// if the conversion succeeded, or zero if the conversion failed. The conversion fails if the s parameter is null, + /// is not in a format compliant with style, or represents a number less than UInt24.MinValue or greater than + /// UInt24.MaxValue. This parameter is passed uninitialized. + /// + /// + /// A object that supplies culture-specific formatting information about s. + /// + /// true if s was converted successfully; otherwise, false. + /// + /// style is not a System.Globalization.NumberStyles value. -or- style is not a combination of + /// System.Globalization.NumberStyles.AllowHexSpecifier and System.Globalization.NumberStyles.HexNumber values. + /// + public static bool TryParse(string s, NumberStyles style, IFormatProvider provider, out UInt24 result) + { + bool parseResponse = uint.TryParse(s, style, provider, out uint parseResult); + + try + { + result = (UInt24)parseResult; + } + catch + { + result = 0; + parseResponse = false; + } + + return parseResponse; + } + + /// + /// Returns the System.TypeCode for value type System.UInt32 (there is no defined type code for an UInt24). + /// + /// The enumerated constant, System.TypeCode.UInt32. + /// + /// There is no defined UInt24 type code and since an UInt24 will easily fit inside an UInt32, the + /// UInt32 type code is returned. + /// + public TypeCode GetTypeCode() + { + return TypeCode.UInt32; + } + + #region [ Explicit IConvertible Implementation ] + + // These are explicitly implemented on the native integer implementations, so we do the same... + + bool IConvertible.ToBoolean(IFormatProvider? provider) + { + return Convert.ToBoolean(m_value, provider); + } + + char IConvertible.ToChar(IFormatProvider? provider) + { + return Convert.ToChar(m_value, provider); + } + + sbyte IConvertible.ToSByte(IFormatProvider? provider) + { + return Convert.ToSByte(m_value, provider); + } + + byte IConvertible.ToByte(IFormatProvider? provider) + { + return Convert.ToByte(m_value, provider); + } + + short IConvertible.ToInt16(IFormatProvider? provider) + { + return Convert.ToInt16(m_value, provider); + } + + ushort IConvertible.ToUInt16(IFormatProvider? provider) + { + return Convert.ToUInt16(m_value, provider); + } + + int IConvertible.ToInt32(IFormatProvider? provider) + { + return Convert.ToInt32(m_value, provider); + } + + uint IConvertible.ToUInt32(IFormatProvider? provider) + { + return m_value; + } + + long IConvertible.ToInt64(IFormatProvider? provider) + { + return Convert.ToInt64(m_value, provider); + } + + ulong IConvertible.ToUInt64(IFormatProvider? provider) + { + return Convert.ToUInt64(m_value, provider); + } + + float IConvertible.ToSingle(IFormatProvider? provider) + { + return Convert.ToSingle(m_value, provider); + } + + double IConvertible.ToDouble(IFormatProvider? provider) + { + return Convert.ToDouble(m_value, provider); + } + + decimal IConvertible.ToDecimal(IFormatProvider? provider) + { + return Convert.ToDecimal(m_value, provider); + } + + DateTime IConvertible.ToDateTime(IFormatProvider? provider) + { + return Convert.ToDateTime(m_value, provider); + } + + object IConvertible.ToType(Type type, IFormatProvider? provider) + { + return Convert.ChangeType(m_value, type, provider); + } + + #endregion + + #endregion + + #region [ Operators ] + + // Every effort has been made to make UInt24 as cleanly interoperable with UInt32 as possible... + + #region [ Comparison Operators ] + + /// + /// Compares the two values for equality. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator ==(UInt24 value1, UInt24 value2) + { + return value1.Equals(value2); + } + + /// + /// Compares the two values for equality. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator ==(uint value1, UInt24 value2) + { + return value1.Equals(value2); + } + + /// + /// Compares the two values for equality. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator ==(UInt24 value1, uint value2) + { + return ((uint)value1).Equals(value2); + } + + /// + /// Compares the two values for inequality. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator !=(UInt24 value1, UInt24 value2) + { + return !value1.Equals(value2); + } + + /// + /// Compares the two values for inequality. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator !=(uint value1, UInt24 value2) + { + return !value1.Equals(value2); + } + + /// + /// Compares the two values for inequality. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator !=(UInt24 value1, uint value2) + { + return !((uint)value1).Equals(value2); + } + + /// + /// Returns true if left value is less than right value. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator <(UInt24 value1, UInt24 value2) + { + return value1.CompareTo(value2) < 0; + } + + /// + /// Returns true if left value is less than right value. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator <(uint value1, UInt24 value2) + { + return value1.CompareTo(value2) < 0; + } + + /// + /// Returns true if left value is less than right value. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator <(UInt24 value1, uint value2) + { + return value1.CompareTo(value2) < 0; + } + + /// + /// Returns true if left value is less or equal to than right value. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator <=(UInt24 value1, UInt24 value2) + { + return value1.CompareTo(value2) <= 0; + } + + /// + /// Returns true if left value is less or equal to than right value. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator <=(uint value1, UInt24 value2) + { + return value1.CompareTo(value2) <= 0; + } + + /// + /// Returns true if left value is less or equal to than right value. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator <=(UInt24 value1, uint value2) + { + return value1.CompareTo(value2) <= 0; + } + + /// + /// Returns true if left value is greater than right value. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator >(UInt24 value1, UInt24 value2) + { + return value1.CompareTo(value2) > 0; + } + + /// + /// Returns true if left value is greater than right value. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator >(uint value1, UInt24 value2) + { + return value1.CompareTo(value2) > 0; + } + + /// + /// Returns true if left value is greater than right value. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator >(UInt24 value1, uint value2) + { + return value1.CompareTo(value2) > 0; + } + + /// + /// Returns true if left value is greater than or equal to right value. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator >=(UInt24 value1, UInt24 value2) + { + return value1.CompareTo(value2) >= 0; + } + + /// + /// Returns true if left value is greater than or equal to right value. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator >=(uint value1, UInt24 value2) + { + return value1.CompareTo(value2) >= 0; + } + + /// + /// Returns true if left value is greater than or equal to right value. + /// + /// left hand operand. + /// right hand operand. + /// value representing the result. + public static bool operator >=(UInt24 value1, uint value2) + { + return value1.CompareTo(value2) >= 0; + } + + #endregion + + #region [ Type Conversion Operators ] + + #region [ Explicit Narrowing Conversions ] + + /// + /// Explicitly converts value to an . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static explicit operator UInt24(Enum value) + { + return new UInt24(Convert.ToUInt32(value)); + } + + /// + /// Explicitly converts value to an . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static explicit operator UInt24(string value) + { + return new UInt24(Convert.ToUInt32(value)); + } + + /// + /// Explicitly converts value to an . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static explicit operator UInt24(decimal value) + { + return new UInt24(Convert.ToUInt32(value)); + } + + /// + /// Explicitly converts value to an . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static explicit operator UInt24(double value) + { + return new UInt24(Convert.ToUInt32(value)); + } + + /// + /// Explicitly converts value to an . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static explicit operator UInt24(float value) + { + return new UInt24(Convert.ToUInt32(value)); + } + + /// + /// Explicitly converts value to an . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static explicit operator UInt24(ulong value) + { + return new UInt24(Convert.ToUInt32(value)); + } + + /// + /// Explicitly converts value to an . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static explicit operator UInt24(uint value) + { + return new UInt24(value); + } + + /// + /// Explicitly converts value to an . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static explicit operator UInt24(Int24 value) + { + return new UInt24(value); + } + + /// + /// Explicitly converts value to an . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static explicit operator Int24(UInt24 value) + { + return new Int24(value); + } + + /// + /// Explicitly converts to . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static explicit operator short(UInt24 value) + { + return (short)(uint)value; + } + + /// + /// Explicitly converts to . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static explicit operator ushort(UInt24 value) + { + return (ushort)(uint)value; + } + + /// + /// Explicitly converts to . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static explicit operator byte(UInt24 value) + { + return (byte)(uint)value; + } + + #endregion + + #region [ Implicit Widening Conversions ] + + /// + /// Implicitly converts value to an . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static implicit operator UInt24(byte value) + { + return new UInt24((uint)value); + } + + /// + /// Implicitly converts value to an . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static implicit operator UInt24(char value) + { + return new UInt24((uint)value); + } + + /// + /// Implicitly converts value to an . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static implicit operator UInt24(ushort value) + { + return new UInt24((uint)value); + } + + /// + /// Implicitly converts to . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static implicit operator int(UInt24 value) + { + return ((IConvertible)value).ToInt32(null); + } + + /// + /// Implicitly converts to . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static implicit operator uint(UInt24 value) + { + return ((IConvertible)value).ToUInt32(null); + } + + /// + /// Implicitly converts to . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static implicit operator long(UInt24 value) + { + return ((IConvertible)value).ToInt64(null); + } + + /// + /// Implicitly converts to . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static implicit operator ulong(UInt24 value) + { + return ((IConvertible)value).ToUInt64(null); + } + + /// + /// Implicitly converts to . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static implicit operator double(UInt24 value) + { + return ((IConvertible)value).ToDouble(null); + } + + /// + /// Implicitly converts to . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static implicit operator float(UInt24 value) + { + return ((IConvertible)value).ToSingle(null); + } + + /// + /// Implicitly converts to . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static implicit operator decimal(UInt24 value) + { + return ((IConvertible)value).ToDecimal(null); + } + + /// + /// Implicitly converts to . + /// + /// value to be converted. + /// value that is the result of the conversion. + public static implicit operator string(UInt24 value) + { + return value.ToString(CultureInfo.InvariantCulture); + } + + #endregion + + #endregion + + #region [ Boolean and Bitwise Operators ] + + /// + /// Returns true if value is greater than zero. + /// + /// value to evaluate. + /// value indicating whether the value is greater than zero. + public static bool operator true(UInt24 value) + { + return value > 0; + } + + /// + /// Returns true if value is equal to zero. + /// + /// value to evaluate. + /// value indicating whether the value is equal than zero. + public static bool operator false(UInt24 value) + { + return value == 0; + } + + /// + /// Returns bitwise complement of value. + /// + /// value to evaluate. + /// value representing the complement of the input value. + public static UInt24 operator ~(UInt24 value) + { + return (UInt24)ApplyBitMask(~(uint)value); + } + + /// + /// Returns logical bitwise AND of values. + /// + /// left hand operand. + /// right hand operand. + /// value representing the logical bitwise AND of the values. + public static UInt24 operator &(UInt24 value1, UInt24 value2) + { + return (UInt24)ApplyBitMask((uint)value1 & (uint)value2); + } + + /// + /// Returns logical bitwise AND of values. + /// + /// left hand operand. + /// right hand operand. + /// value representing the logical bitwise AND of the values. + public static uint operator &(uint value1, UInt24 value2) + { + return value1 & (uint)value2; + } + + /// + /// Returns logical bitwise AND of values. + /// + /// left hand operand. + /// right hand operand. + /// value representing the logical bitwise AND of the values. + public static uint operator &(UInt24 value1, uint value2) + { + return (uint)value1 & value2; + } + + /// + /// Returns logical bitwise OR of values. + /// + /// left hand operand. + /// right hand operand. + /// value representing the logical bitwise OR of the values. + public static UInt24 operator |(UInt24 value1, UInt24 value2) + { + return (UInt24)ApplyBitMask((uint)value1 | (uint)value2); + } + + /// + /// Returns logical bitwise OR of values. + /// + /// left hand operand. + /// right hand operand. + /// value representing the logical bitwise OR of the values. + public static uint operator |(uint value1, UInt24 value2) + { + return value1 | (uint)value2; + } + + /// + /// Returns logical bitwise OR of values. + /// + /// left hand operand. + /// right hand operand. + /// value representing the logical bitwise OR of the values. + public static uint operator |(UInt24 value1, uint value2) + { + return (uint)value1 | value2; + } + + /// + /// Returns logical bitwise exclusive-OR of values. + /// + /// left hand operand. + /// right hand operand. + /// value representing the logical bitwise exclusive-OR of the values. + public static UInt24 operator ^(UInt24 value1, UInt24 value2) + { + return (UInt24)ApplyBitMask((uint)value1 ^ (uint)value2); + } + + /// + /// Returns logical bitwise exclusive-OR of values. + /// + /// left hand operand. + /// right hand operand. + /// value representing the logical bitwise exclusive-OR of the values. + public static uint operator ^(uint value1, UInt24 value2) + { + return value1 ^ (uint)value2; + } + + /// + /// Returns logical bitwise exclusive-OR of values. + /// + /// left hand operand. + /// right hand operand. + /// value representing the logical bitwise exclusive-OR of the values. + public static uint operator ^(UInt24 value1, uint value2) + { + return (uint)value1 ^ value2; + } + + /// + /// Returns value after right shifts of first value by the number of bits specified by second value. + /// + /// value to right shift. + /// value indicating the number of bits to right shift by. + /// value as result of right shift operation. + public static UInt24 operator >>(UInt24 value, int shifts) + { + return (UInt24)ApplyBitMask((uint)value >> shifts); + } + + /// + /// Returns value after left shifts of first value by the number of bits specified by second value. + /// + /// value to left shift. + /// value indicating the number of bits to left shift by. + /// value as result of left shift operation. + public static UInt24 operator <<(UInt24 value, int shifts) + { + return (UInt24)ApplyBitMask((uint)value << shifts); + } + + #endregion + + #region [ Arithmetic Operators ] + + /// + /// Returns computed remainder after dividing first value by the second. + /// + /// left hand operand. + /// right hand operand. + /// value as result of modulus operation. + public static UInt24 operator %(UInt24 value1, UInt24 value2) + { + return (UInt24)((uint)value1 % (uint)value2); + } + + /// + /// Returns computed remainder after dividing first value by the second. + /// + /// left hand operand. + /// right hand operand. + /// value as result of modulus operation. + public static uint operator %(uint value1, UInt24 value2) + { + return value1 % (uint)value2; + } + + /// + /// Returns computed remainder after dividing first value by the second. + /// + /// left hand operand. + /// right hand operand. + /// value as result of modulus operation. + public static uint operator %(UInt24 value1, uint value2) + { + return (uint)value1 % value2; + } + + /// + /// Returns computed sum of values. + /// + /// left hand operand. + /// right hand operand. + /// value as result of addition operation. + public static UInt24 operator +(UInt24 value1, UInt24 value2) + { + return (UInt24)((uint)value1 + (uint)value2); + } + + /// + /// Returns computed sum of values. + /// + /// left hand operand. + /// right hand operand. + /// value as result of addition operation. + public static uint operator +(uint value1, UInt24 value2) + { + return value1 + (uint)value2; + } + + /// + /// Returns computed sum of values. + /// + /// left hand operand. + /// right hand operand. + /// value as result of addition operation. + public static uint operator +(UInt24 value1, uint value2) + { + return (uint)value1 + value2; + } + + /// + /// Returns computed difference of values. + /// + /// left hand operand. + /// right hand operand. + /// value as result of subtraction operation. + public static UInt24 operator -(UInt24 value1, UInt24 value2) + { + return (UInt24)((uint)value1 - (uint)value2); + } + + /// + /// Returns computed difference of values. + /// + /// left hand operand. + /// right hand operand. + /// value as result of subtraction operation. + public static uint operator -(uint value1, UInt24 value2) + { + return value1 - (uint)value2; + } + + /// + /// Returns computed difference of values. + /// + /// left hand operand. + /// right hand operand. + /// value as result of subtraction operation. + public static uint operator -(UInt24 value1, uint value2) + { + return (uint)value1 - value2; + } + + /// + /// Returns incremented value. + /// + /// The operand. + /// result of increment. + public static UInt24 operator ++(UInt24 value) + { + return value + 1; + } + + /// + /// Returns decremented value. + /// + /// The operand. + /// result of decrement. + public static UInt24 operator --(UInt24 value) + { + return value - 1; + } + + /// + /// Returns computed product of values. + /// + /// left hand operand. + /// right hand operand. + /// value as result of multiplication operation. + public static UInt24 operator *(UInt24 value1, UInt24 value2) + { + return (UInt24)((uint)value1 * (uint)value2); + } + + /// + /// Returns computed product of values. + /// + /// left hand operand. + /// right hand operand. + /// value as result of multiplication operation. + public static uint operator *(uint value1, UInt24 value2) + { + return value1 * (uint)value2; + } + + /// + /// Returns computed product of values. + /// + /// left hand operand. + /// right hand operand. + /// value as result of multiplication operation. + public static uint operator *(UInt24 value1, uint value2) + { + return (uint)value1 * value2; + } + + // Integer division operators + + /// + /// Returns computed division of values. + /// + /// left hand operand. + /// right hand operand. + /// value as result of division operation. + public static UInt24 operator /(UInt24 value1, UInt24 value2) + { + return (UInt24)((uint)value1 / (uint)value2); + } + + /// + /// Returns computed division of values. + /// + /// left hand operand. + /// right hand operand. + /// value as result of division operation. + public static uint operator /(uint value1, UInt24 value2) + { + return value1 / (uint)value2; + } + + /// + /// Returns computed division of values. + /// + /// left hand operand. + /// right hand operand. + /// value as result of division operation. + public static uint operator /(UInt24 value1, uint value2) + { + return (uint)value1 / value2; + } + + //// Standard division operators + //public static double operator /(UInt24 value1, UInt24 value2) + //{ + // return ((double)value1 / (double)value2); + //} + + //public static double operator /(uint value1, UInt24 value2) + //{ + // return ((double)value1 / (double)value2); + //} + + //public static double operator /(UInt24 value1, uint value2) + //{ + // return ((double)value1 / (double)value2); + //} + + // C# doesn't expose an exponent operator but some other .NET languages do, + // so we expose the operator via its native special IL function name + + /// + /// Returns result of first value raised to power of second value. + /// + /// left hand operand. + /// right hand operand. + /// value as result of operation. + [EditorBrowsable(EditorBrowsableState.Advanced), SpecialName] + public static double op_Exponent(UInt24 value1, UInt24 value2) + { + return Math.Pow(value1, value2); + } + + /// + /// Returns result of first value raised to power of second value. + /// + /// left hand operand. + /// right hand operand. + /// value as result of operation. + [EditorBrowsable(EditorBrowsableState.Advanced), SpecialName] + public static double op_Exponent(int value1, UInt24 value2) + { + return Math.Pow(value1, value2); + } + + /// + /// Returns result of first value raised to power of second value. + /// + /// left hand operand. + /// right hand operand. + /// value as result of operation. + [EditorBrowsable(EditorBrowsableState.Advanced), SpecialName] + public static double op_Exponent(UInt24 value1, int value2) + { + return Math.Pow(value1, value2); + } + + #endregion + + #endregion + + #region [ Static ] + + /// + /// Represents the largest possible value of an Int24. This field is constant. + /// + public static readonly UInt24 MaxValue = (UInt24)MaxValue32; + + /// + /// Represents the smallest possible value of an Int24. This field is constant. + /// + public static readonly UInt24 MinValue = (UInt24)MinValue32; + + /// Returns the specified UInt24 value as an array of three bytes. + /// UInt24 value to convert to bytes. + /// An array of bytes with length 3. + /// + /// You can use this function in-lieu of a System.BitConverter.GetBytes(UInt24) function. + /// Bytes will be returned in endian order of currently executing process architecture (little-endian on Intel platforms). + /// + public static byte[] GetBytes(UInt24 value) + { + // We use a 32-bit integer to store 24-bit integer internally + byte[] data = new byte[3]; + uint valueInt = value; + + if (BitConverter.IsLittleEndian) + { + data[0] = (byte)valueInt; + data[1] = (byte)(valueInt >> 8); + data[2] = (byte)(valueInt >> 16); + } + else + { + data[0] = (byte)(valueInt >> 16); + data[1] = (byte)(valueInt >> 8); + data[2] = (byte)valueInt; + } + + // Return serialized 3-byte representation of Int24 + return data; + } + + /// Returns a 24-bit unsigned integer from three bytes at a specified position in a byte array. + /// An array of bytes. + /// The starting position within value. + /// A 24-bit unsigned integer formed by three bytes beginning at startIndex. + /// + /// You can use this function in-lieu of a System.BitConverter.ToUInt24 function. + /// Bytes endian order assumed to match that of currently executing process architecture (little-endian on Intel platforms). + /// + /// cannot be null. + /// is greater than length. + /// length from is too small to represent an . + public static UInt24 GetValue(byte[] value, int startIndex) + { + value.ValidateParameters(startIndex, 3); + int valueInt; + + if (BitConverter.IsLittleEndian) + { + valueInt = value[startIndex] | + value[startIndex + 1] << 8 | + value[startIndex + 2] << 16; + } + else + { + valueInt = value[startIndex] << 16 | + value[startIndex + 1] << 8 | + value[startIndex + 2]; + } + + // Deserialize value + return (UInt24)ApplyBitMask((uint)valueInt); + } + + private static void ValidateNumericRange(uint value) + { + if (value > MaxValue32) + throw new OverflowException($"Value of {value} will not fit in a 24-bit unsigned integer"); + } + + private static uint ApplyBitMask(uint value) + { + // For unsigned values, all we do is clear all the high bits (keeps 32-bit unsigned number in 24-bit unsigned range)... + return value & ~BitMask; + } + + #endregion +} \ No newline at end of file diff --git a/VG Music Studio - Core/VG Music Studio - Core.csproj b/VG Music Studio - Core/VG Music Studio - Core.csproj index 1d8bb4e..b45bf6a 100644 --- a/VG Music Studio - Core/VG Music Studio - Core.csproj +++ b/VG Music Studio - Core/VG Music Studio - Core.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 Library latest Kermalis.VGMusicStudio.Core @@ -11,16 +11,17 @@ - - - - + + + + + + + + Dependencies\DLS2.dll - - Dependencies\KMIDI.dll - Dependencies\SoundFont2.dll diff --git a/VG Music Studio - GTK3/MainWindow.cs b/VG Music Studio - GTK3/MainWindow.cs new file mode 100644 index 0000000..01921eb --- /dev/null +++ b/VG Music Studio - GTK3/MainWindow.cs @@ -0,0 +1,875 @@ +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 Gtk; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Timers; + +namespace Kermalis.VGMusicStudio.GTK3 +{ + internal sealed class MainWindow : Window + { + private bool _playlistPlaying; + private Config.Playlist _curPlaylist; + private long _curSong = -1; + private readonly List _playedSequences; + private readonly List _remainingSequences; + + private bool _stopUI = false; + + #region Widgets + + // Buttons + private readonly Button _buttonPlay, _buttonPause, _buttonStop; + + // A Box specifically made to contain two contents inside + private readonly Box _splitContainerBox; + + // Spin Button for the numbered tracks + private readonly SpinButton _sequenceNumberSpinButton; + + // Timer + private readonly Timer _timer; + + // Menu Bar + private readonly MenuBar _mainMenu; + + // Menus + private readonly Menu _fileMenu, _dataMenu, _soundtableMenu; + + // Menu Items + private readonly MenuItem _fileItem, _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem, + _dataItem, _trackViewerItem, _exportDLSItem, _exportSF2Item, _exportMIDIItem, _exportWAVItem, _soundtableItem, _endSoundtableItem; + + // Main Box + private Box _mainBox, _configButtonBox, _configPlayerButtonBox, _configSpinButtonBox, _configScaleBox; + + // Volume Button to indicate volume status + private readonly VolumeButton _volumeButton; + + // One Scale controling volume and one Scale for the sequenced track + private readonly Scale _volumeScale, _positionScale; + + // Adjustments are for indicating the numbers and the position of the scale + private Adjustment _volumeAdjustment, _positionAdjustment, _sequenceNumberAdjustment; + + // Tree View + private readonly TreeView _sequencesListView; + private readonly TreeViewColumn _sequencesColumn; + + // List Store + private ListStore _sequencesListStore; + + #endregion + + public MainWindow() : base(ConfigUtils.PROGRAM_NAME) + { + // Main Window + // Sets the default size of the Window + SetDefaultSize(500, 300); + + + // Sets the _playedSequences and _remainingSequences with a List() function to be ready for use + _playedSequences = new List(); + _remainingSequences = new List(); + + // Configures SetVolumeScale method with the MixerVolumeChanged Event action + Mixer.MixerVolumeChanged += SetVolumeScale; + + // Main Menu + _mainMenu = new MenuBar(); + + // File Menu + _fileMenu = new Menu(); + + _fileItem = new MenuItem() { Label = Strings.MenuFile, UseUnderline = true }; + _fileItem.Submenu = _fileMenu; + + _openDSEItem = new MenuItem() { Label = Strings.MenuOpenDSE, UseUnderline = true }; + _openDSEItem.Activated += OpenDSE; + _fileMenu.Append(_openDSEItem); + + _openSDATItem = new MenuItem() { Label = Strings.MenuOpenSDAT, UseUnderline = true }; + _openSDATItem.Activated += OpenSDAT; + _fileMenu.Append(_openSDATItem); + + _openAlphaDreamItem = new MenuItem() { Label = Strings.MenuOpenAlphaDream, UseUnderline = true }; + _openAlphaDreamItem.Activated += OpenAlphaDream; + _fileMenu.Append(_openAlphaDreamItem); + + _openMP2KItem = new MenuItem() { Label = Strings.MenuOpenMP2K, UseUnderline = true }; + _openMP2KItem.Activated += OpenMP2K; + _fileMenu.Append(_openMP2KItem); + + _mainMenu.Append(_fileItem); // Note: It must append the menu item, not the file menu itself + + // Data Menu + _dataMenu = new Menu(); + + _dataItem = new MenuItem() { Label = Strings.MenuData, UseUnderline = true }; + _dataItem.Submenu = _dataMenu; + + _exportDLSItem = new MenuItem() { Sensitive = false, Label = Strings.MenuSaveDLS, UseUnderline = true }; // Sensitive is identical to 'Enabled', so if you're disabling the control, Sensitive must be set to false + _exportDLSItem.Activated += ExportDLS; + _dataMenu.Append(_exportDLSItem); + + _exportSF2Item = new MenuItem() { Sensitive = false, Label = Strings.MenuSaveSF2, UseUnderline = true }; + _exportSF2Item.Activated += ExportSF2; + _dataMenu.Append(_exportSF2Item); + + _exportMIDIItem = new MenuItem() { Sensitive = false, Label = Strings.MenuSaveMIDI, UseUnderline = true }; + _exportMIDIItem.Activated += ExportMIDI; + _dataMenu.Append(_exportMIDIItem); + + _exportWAVItem = new MenuItem() { Sensitive = false, Label = Strings.MenuSaveWAV, UseUnderline = true }; + _exportWAVItem.Activated += ExportWAV; + _dataMenu.Append(_exportWAVItem); + + _mainMenu.Append(_dataItem); + + // Soundtable Menu + _soundtableMenu = new Menu(); + + _soundtableItem = new MenuItem() { Label = Strings.MenuPlaylist, UseUnderline = true }; + _soundtableItem.Submenu = _soundtableMenu; + + _endSoundtableItem = new MenuItem() { Label = Strings.MenuEndPlaylist, UseUnderline = true }; + _endSoundtableItem.Activated += EndCurrentPlaylist; + _soundtableMenu.Append(_endSoundtableItem); + + _mainMenu.Append(_soundtableItem); + + // Buttons + _buttonPlay = new Button() { Sensitive = false, Label = Strings.PlayerPlay }; + _buttonPlay.Clicked += (o, e) => Play(); + _buttonPause = new Button() { Sensitive = false, Label = Strings.PlayerPause }; + _buttonPause.Clicked += (o, e) => Pause(); + _buttonStop = new Button() { Sensitive = false, Label = Strings.PlayerStop }; + _buttonStop.Clicked += (o, e) => Stop(); + + // Spin Button + _sequenceNumberAdjustment = new Adjustment(0, 0, -1, 1, 1, 1); + _sequenceNumberSpinButton = new SpinButton(_sequenceNumberAdjustment, 1, 0) { Sensitive = false, Value = 0, NoShowAll = true, Visible = false }; + _sequenceNumberSpinButton.ValueChanged += SequenceNumberSpinButton_ValueChanged; + + // Timer + _timer = new Timer(); + _timer.Elapsed += UpdateUI; + + // Volume Scale + _volumeAdjustment = new Adjustment(0, 0, 100, 1, 1, 1); + _volumeScale = new Scale(Orientation.Horizontal, _volumeAdjustment) { Sensitive = false, ShowFillLevel = true, DrawValue = false, WidthRequest = 250 }; + _volumeScale.ValueChanged += VolumeScale_ValueChanged; + + // Position Scale + _positionAdjustment = new Adjustment(0, 0, -1, 1, 1, 1); + _positionScale = new Scale(Orientation.Horizontal, _positionAdjustment) { Sensitive = false, ShowFillLevel = true, DrawValue = false, WidthRequest = 250 }; + _positionScale.ButtonReleaseEvent += PositionScale_MouseButtonRelease; // ButtonRelease must go first, otherwise the scale it will follow the mouse cursor upon loading + _positionScale.ButtonPressEvent += PositionScale_MouseButtonPress; + + // Sequences List View + _sequencesListView = new TreeView(); + _sequencesListStore = new ListStore(typeof(string), typeof(string)); + _sequencesColumn = new TreeViewColumn("Name", new CellRendererText(), "text", 1); + _sequencesListView.AppendColumn("#", new CellRendererText(), "text", 0); + _sequencesListView.AppendColumn(_sequencesColumn); + _sequencesListView.Model = _sequencesListStore; + + // Main display + _mainBox = new Box(Orientation.Vertical, 4); + _configButtonBox = new Box(Orientation.Horizontal, 2) { Halign = Align.Center }; + _configPlayerButtonBox = new Box(Orientation.Horizontal, 3) { Halign = Align.Center }; + _configSpinButtonBox = new Box(Orientation.Horizontal, 1) { Halign = Align.Center, WidthRequest = 100 }; + _configScaleBox = new Box(Orientation.Horizontal, 2) { Halign = Align.Center }; + + _mainBox.PackStart(_mainMenu, false, false, 0); + _mainBox.PackStart(_configButtonBox, false, false, 0); + _mainBox.PackStart(_configScaleBox, false, false, 0); + _mainBox.PackStart(_sequencesListView, false, false, 0); + + _configButtonBox.PackStart(_configPlayerButtonBox, false, false, 40); + _configButtonBox.PackStart(_configSpinButtonBox, false, false, 100); + + _configPlayerButtonBox.PackStart(_buttonPlay, false, false, 0); + _configPlayerButtonBox.PackStart(_buttonPause, false, false, 0); + _configPlayerButtonBox.PackStart(_buttonStop, false, false, 0); + + _configSpinButtonBox.PackStart(_sequenceNumberSpinButton, false, false, 0); + + _configScaleBox.PackStart(_volumeScale, false, false, 20); + _configScaleBox.PackStart(_positionScale, false, false, 20); + + Add(_mainBox); + + ShowAll(); + + // Ensures the entire application closes when the window is closed + DeleteEvent += delegate { Application.Quit(); }; + } + + // When the value is changed on the volume scale + private void VolumeScale_ValueChanged(object? sender, EventArgs? e) + { + Engine.Instance.Mixer.SetVolume((float)(_volumeScale.Adjustment!.Value / _volumeAdjustment.Upper)); + } + + // Sets the volume scale to the specified position + public void SetVolumeScale(float volume) + { + _volumeScale.ValueChanged -= VolumeScale_ValueChanged; + _volumeScale.Adjustment!.Value = (int)(volume * _volumeAdjustment.Upper); + _volumeScale.ValueChanged += VolumeScale_ValueChanged; + } + + private bool _positionScaleFree = true; + private void PositionScale_MouseButtonRelease(object? sender, ButtonReleaseEventArgs args) + { + if (args.Event.Button == 1) // Number 1 is Left Mouse Button + { + Engine.Instance.Player.SetCurrentPosition((long)_positionScale.Value); // Sets the value based on the position when mouse button is released + _positionScaleFree = true; // Sets _positionScaleFree to true when mouse button is released + LetUIKnowPlayerIsPlaying(); // This method will run the void that tells the UI that the player is playing a track + } + } + private void PositionScale_MouseButtonPress(object? sender, ButtonPressEventArgs args) + { + if (args.Event.Button == 1) // Number 1 is Left Mouse Button + { + _positionScaleFree = false; + } + } + + private bool _autoplay = false; + private void SequenceNumberSpinButton_ValueChanged(object? sender, EventArgs? e) + { + _sequencesListView.SelectionGet -= SequencesListView_SelectionGet; + + long index = (long)_sequenceNumberAdjustment.Value; + Stop(); + this.Title = ConfigUtils.PROGRAM_NAME; + _sequencesListView.Margin = 0; + //_songInfo.Reset(); + bool success; + try + { + Engine.Instance!.Player.LoadSong(index); + success = Engine.Instance.Player.LoadedSong is not null; // TODO: Make sure loadedsong is null when there are no tracks (for each engine, only mp2k guarantees it rn) + } + catch (Exception ex) + { + new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.YesNo, string.Format(Strings.ErrorLoadSong, Engine.Instance!.Config.GetSongName(index)), ex); + success = false; + } + + //_trackViewer?.UpdateTracks(); + if (success) + { + Config config = Engine.Instance.Config; + List songs = config.Playlists[0].Songs; // Complete "Music" playlist is present in all configs at index 0 + Config.Song? song = songs.SingleOrDefault(s => s.Index == index); + if (song is not null) + { + this.Title = $"{ConfigUtils.PROGRAM_NAME} - {song.Name}"; // TODO: Make this a func + _sequencesColumn.SortColumnId = songs.IndexOf(song) + 1; // + 1 because the "Music" playlist is first in the combobox + } + _positionAdjustment.Upper = Engine.Instance!.Player.LoadedSong!.MaxTicks; + _positionAdjustment.Value = _positionAdjustment.Upper / 10; + _positionAdjustment.Value = _positionAdjustment.Value / 4; + //_songInfo.SetNumTracks(Engine.Instance.Player.LoadedSong.Events.Length); + if (_autoplay) + { + Play(); + } + } + else + { + //_songInfo.SetNumTracks(0); + } + _positionScale.Sensitive = _exportWAVItem.Sensitive = success; + _exportMIDIItem.Sensitive = success && MP2KEngine.MP2KInstance is not null; + _exportDLSItem.Sensitive = _exportSF2Item.Sensitive = success && AlphaDreamEngine.AlphaDreamInstance is not null; + + _autoplay = true; + _sequencesListView.SelectionGet += SequencesListView_SelectionGet; + } + private void SequencesListView_SelectionGet(object? sender, EventArgs? e) + { + var item = _sequencesListView.Selection; + if (item.SelectFunction.Target is Config.Song song) + { + SetAndLoadSequence(song.Index); + } + else if (item.SelectFunction.Target is Config.Playlist playlist) + { + var md = new MessageDialog(this, DialogFlags.Modal, MessageType.Question, ButtonsType.YesNo, string.Format(Strings.PlayPlaylistBody, Environment.NewLine + playlist, Strings.MenuPlaylist)); + if (playlist.Songs.Count > 0 + && md.Run() == (int)ResponseType.Yes) + { + ResetPlaylistStuff(false); + _curPlaylist = playlist; + Engine.Instance.Player.ShouldFadeOut = _playlistPlaying = true; + Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; + _endSoundtableItem.Sensitive = true; + SetAndLoadNextPlaylistSong(); + } + } + } + private void SetAndLoadSequence(long index) + { + _curSong = index; + if (_sequenceNumberSpinButton.Value == index) + { + SequenceNumberSpinButton_ValueChanged(null, null); + } + else + { + _sequenceNumberSpinButton.Value = index; + } + } + + private void SetAndLoadNextPlaylistSong() + { + if (_remainingSequences.Count == 0) + { + _remainingSequences.AddRange(_curPlaylist.Songs.Select(s => s.Index)); + if (GlobalConfig.Instance.PlaylistMode == PlaylistMode.Random) + { + _remainingSequences.Any(); + } + } + long nextSequence = _remainingSequences[0]; + _remainingSequences.RemoveAt(0); + SetAndLoadSequence(nextSequence); + } + private void ResetPlaylistStuff(bool enableds) + { + if (Engine.Instance != null) + { + Engine.Instance.Player.ShouldFadeOut = false; + } + _playlistPlaying = false; + _curPlaylist = null; + _curSong = -1; + _remainingSequences.Clear(); + _playedSequences.Clear(); + _endSoundtableItem.Sensitive = false; + _sequenceNumberSpinButton.Sensitive = _sequencesListView.Sensitive = enableds; + } + private void EndCurrentPlaylist(object? sender, EventArgs? e) + { + var md = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.YesNo, string.Format(Strings.EndPlaylistBody, Strings.MenuPlaylist)); + if (md.Run() == (int)ResponseType.Yes) + { + ResetPlaylistStuff(true); + } + } + + private void OpenDSE(object? sender, EventArgs? e) + { + // To allow the dialog to display in native windowing format, FileChooserNative is used instead of FileChooserDialog + var d = new FileChooserNative( + Strings.MenuOpenDSE, // The title shown in the folder select dialog window + this, // The parent of the dialog window, is the MainWindow itself + FileChooserAction.SelectFolder, "Open", "Cancel"); // To ensure it becomes a folder select dialog window, SelectFolder is used as the FileChooserAction, followed by the accept and cancel button names + + if (d.Run() != (int)ResponseType.Accept) + { + return; + } + + DisposeEngine(); + try + { + _ = new DSEEngine(d.CurrentFolder); + } + catch (Exception ex) + { + new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, Strings.ErrorOpenDSE, ex); + return; + } + + DSEConfig config = DSEEngine.DSEInstance!.Config; + FinishLoading(config.BGMFiles.Length); + _sequenceNumberSpinButton.Visible = false; + _sequenceNumberSpinButton.NoShowAll = true; + _exportDLSItem.Visible = false; + _exportMIDIItem.Visible = false; + _exportSF2Item.Visible = false; + + d.Destroy(); // Ensures disposal of the dialog when closed + } + private void OpenAlphaDream(object? sender, EventArgs? e) + { + var d = new FileChooserNative( + Strings.MenuOpenAlphaDream, + this, + FileChooserAction.Open, "Open", "Cancel"); + + FileFilter filterGBA = new FileFilter() + { + Name = Strings.GTKFilterOpenGBA + }; + filterGBA.AddPattern("*.gba"); + filterGBA.AddPattern("*.srl"); + FileFilter allFiles = new FileFilter() + { + Name = Strings.GTKAllFiles + }; + allFiles.AddPattern("*.*"); + d.AddFilter(filterGBA); + d.AddFilter(allFiles); + + if (d.Run() != (int)ResponseType.Accept) + { + d.Destroy(); + return; + } + + DisposeEngine(); + try + { + _ = new AlphaDreamEngine(File.ReadAllBytes(d.Filename)); + } + catch (Exception ex) + { + new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, Strings.ErrorOpenAlphaDream, ex); + return; + } + + AlphaDreamConfig config = AlphaDreamEngine.AlphaDreamInstance!.Config; + FinishLoading(config.SongTableSizes[0]); + _sequenceNumberSpinButton.Visible = true; + _sequenceNumberSpinButton.NoShowAll = false; + _exportDLSItem.Visible = true; + _exportMIDIItem.Visible = false; + _exportSF2Item.Visible = true; + + d.Destroy(); + } + private void OpenMP2K(object? sender, EventArgs? e) + { + var d = new FileChooserNative( + Strings.MenuOpenMP2K, + this, + FileChooserAction.Open, "Open", "Cancel"); + + FileFilter filterGBA = new FileFilter() + { + Name = Strings.GTKFilterOpenGBA + }; + filterGBA.AddPattern("*.gba"); + filterGBA.AddPattern("*.srl"); + FileFilter allFiles = new FileFilter() + { + Name = Strings.GTKAllFiles + }; + allFiles.AddPattern("*.*"); + d.AddFilter(filterGBA); + d.AddFilter(allFiles); + + if (d.Run() != (int)ResponseType.Accept) + { + d.Destroy(); + return; + } + if (Engine.Instance != null) + { + DisposeEngine(); + } + try + { + _ = new MP2KEngine(File.ReadAllBytes(d.Filename)); + } + catch (Exception ex) + { + new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, Strings.ErrorOpenMP2K, ex); + return; + } + + MP2KConfig config = MP2KEngine.MP2KInstance!.Config; + FinishLoading(config.SongTableSizes[0]); + _sequenceNumberSpinButton.Visible = true; + _sequenceNumberSpinButton.NoShowAll = false; + _exportDLSItem.Visible = false; + _exportMIDIItem.Visible = true; + _exportSF2Item.Visible = false; + + d.Destroy(); + } + private void OpenSDAT(object? sender, EventArgs? e) + { + var d = new FileChooserNative( + Strings.MenuOpenSDAT, + this, + FileChooserAction.Open, "Open", "Cancel"); + + FileFilter filterSDAT = new FileFilter() + { + Name = Strings.GTKFilterOpenSDAT + }; + filterSDAT.AddPattern("*.sdat"); + FileFilter allFiles = new FileFilter() + { + Name = Strings.GTKAllFiles + }; + allFiles.AddPattern("*.*"); + d.AddFilter(filterSDAT); + d.AddFilter(allFiles); + + if (d.Run() != (int)ResponseType.Accept) + { + d.Destroy(); + return; + } + + DisposeEngine(); + try + { + _ = new SDATEngine(new SDAT(File.ReadAllBytes(d.Filename))); + } + catch (Exception ex) + { + new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, Strings.ErrorOpenSDAT, ex); + return; + } + + SDATConfig config = SDATEngine.SDATInstance!.Config; + FinishLoading(config.SDAT.INFOBlock.SequenceInfos.NumEntries); + _sequenceNumberSpinButton.Visible = true; + _sequenceNumberSpinButton.NoShowAll = false; + _exportDLSItem.Visible = false; + _exportMIDIItem.Visible = false; + _exportSF2Item.Visible = false; + + d.Destroy(); + } + + private void ExportDLS(object? sender, EventArgs? e) + { + AlphaDreamConfig cfg = AlphaDreamEngine.AlphaDreamInstance!.Config; + + var d = new FileChooserNative( + Strings.MenuSaveDLS, + this, + FileChooserAction.Save, "Save", "Cancel"); + d.SetFilename(cfg.GetGameName()); + + FileFilter ff = new FileFilter() + { + Name = Strings.GTKFilterSaveDLS + }; + ff.AddPattern("*.dls"); + d.AddFilter(ff); + + if (d.Run() != (int)ResponseType.Accept) + { + d.Destroy(); + return; + } + + try + { + AlphaDreamSoundFontSaver_DLS.Save(cfg, d.Filename); + new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, string.Format(Strings.SuccessSaveDLS, d.Filename)); + } + catch (Exception ex) + { + new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, Strings.ErrorSaveDLS, ex); + } + + d.Destroy(); + } + private void ExportMIDI(object? sender, EventArgs? e) + { + var d = new FileChooserNative( + Strings.MenuSaveMIDI, + this, + FileChooserAction.Save, "Save", "Cancel"); + d.SetFilename(Engine.Instance!.Config.GetSongName((long)_sequenceNumberSpinButton.Value)); + + FileFilter ff = new FileFilter() + { + Name = Strings.GTKFilterSaveMIDI + }; + ff.AddPattern("*.mid"); + ff.AddPattern("*.midi"); + d.AddFilter(ff); + + if (d.Run() != (int)ResponseType.Accept) + { + d.Destroy(); + return; + } + + MP2KPlayer p = MP2KEngine.MP2KInstance!.Player; + var args = new MIDISaveArgs + { + SaveCommandsBeforeTranspose = true, + ReverseVolume = false, + TimeSignatures = new List<(int AbsoluteTick, (byte Numerator, byte Denominator))> + { + (0, (4, 4)), + }, + }; + + try + { + p.SaveAsMIDI(d.Filename, args); + new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, string.Format(Strings.SuccessSaveMIDI, d.Filename)); + } + catch (Exception ex) + { + new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, Strings.ErrorSaveMIDI, ex); + } + } + private void ExportSF2(object? sender, EventArgs? e) + { + AlphaDreamConfig cfg = AlphaDreamEngine.AlphaDreamInstance!.Config; + + var d = new FileChooserNative( + Strings.MenuSaveSF2, + this, + FileChooserAction.Save, "Save", "Cancel"); + + d.SetFilename(cfg.GetGameName()); + + FileFilter ff = new FileFilter() + { + Name = Strings.GTKFilterSaveSF2 + }; + ff.AddPattern("*.sf2"); + d.AddFilter(ff); + + if (d.Run() != (int)ResponseType.Accept) + { + d.Destroy(); + return; + } + + try + { + AlphaDreamSoundFontSaver_SF2.Save(cfg, d.Filename); + new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, string.Format(Strings.SuccessSaveSF2, d.Filename)); + } + catch (Exception ex) + { + new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, Strings.ErrorSaveSF2, ex); + } + } + private void ExportWAV(object? sender, EventArgs? e) + { + var d = new FileChooserNative( + Strings.MenuSaveWAV, + this, + FileChooserAction.Save, "Save", "Cancel"); + + d.SetFilename(Engine.Instance!.Config.GetSongName((long)_sequenceNumberSpinButton.Value)); + + FileFilter ff = new FileFilter() + { + Name = Strings.GTKFilterSaveWAV + }; + ff.AddPattern("*.wav"); + d.AddFilter(ff); + + if (d.Run() != (int)ResponseType.Accept) + { + d.Destroy(); + return; + } + + Stop(); + + IPlayer player = Engine.Instance.Player; + bool oldFade = player.ShouldFadeOut; + long oldLoops = player.NumLoops; + player.ShouldFadeOut = true; + player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; + + try + { + player.Record(d.Filename); + new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, string.Format(Strings.SuccessSaveWAV, d.Filename)); + } + catch (Exception ex) + { + new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, Strings.ErrorSaveWAV, ex); + } + + player.ShouldFadeOut = oldFade; + player.NumLoops = oldLoops; + _stopUI = false; + } + + public void LetUIKnowPlayerIsPlaying() + { + // Prevents method from being used if timer is already active + if (_timer.Enabled) + { + return; + } + + //bool timerValue; // Used for updating _positionAdjustment to be in sync with _timer + + // Configures the buttons when player is playing a sequenced track + _buttonPause.Sensitive = _buttonStop.Sensitive = true; + _buttonPause.Label = Strings.PlayerPause; + GlobalConfig.Init(); + _timer.Interval = (int)(1_000.0 / GlobalConfig.Instance.RefreshRate); + + // Experimental attempt for _positionAdjustment to be synchronized with _timer + //timerValue = _timer.Equals(_positionAdjustment); + //timerValue.CompareTo(_timer); + + _timer.Start(); + } + + private void Play() + { + Engine.Instance!.Player.Play(); + LetUIKnowPlayerIsPlaying(); + } + private void Pause() + { + Engine.Instance!.Player.Pause(); + if (Engine.Instance.Player.State == PlayerState.Paused) + { + _buttonPause.Label = Strings.PlayerUnpause; + _timer.Stop(); + } + else + { + _buttonPause.Label = Strings.PlayerPause; + _timer.Start(); + } + } + private void Stop() + { + Engine.Instance!.Player.Stop(); + _buttonPause.Sensitive = _buttonStop.Sensitive = false; + _buttonPause.Label = Strings.PlayerPause; + _timer.Stop(); + UpdatePositionIndicators(0L); + } + private void TogglePlayback(object? sender, EventArgs? e) + { + switch (Engine.Instance!.Player.State) + { + case PlayerState.Stopped: Play(); break; + case PlayerState.Paused: + case PlayerState.Playing: Pause(); break; + } + } + private void PlayPreviousSequence(object? sender, EventArgs? e) + { + long prevSequence; + if (_playlistPlaying) + { + int index = _playedSequences.Count - 1; + prevSequence = _playedSequences[index]; + _playedSequences.RemoveAt(index); + _playedSequences.Insert(0, _curSong); + } + else + { + prevSequence = (long)_sequenceNumberSpinButton.Value - 1; + } + SetAndLoadSequence(prevSequence); + } + private void PlayNextSong(object? sender, EventArgs? e) + { + if (_playlistPlaying) + { + _playedSequences.Add(_curSong); + SetAndLoadNextPlaylistSong(); + } + else + { + SetAndLoadSequence((long)_sequenceNumberSpinButton.Value + 1); + } + } + + private void FinishLoading(long numSongs) + { + Engine.Instance!.Player.SongEnded += SongEnded; + foreach (Config.Playlist playlist in Engine.Instance.Config.Playlists) + { + _sequencesListStore.AppendValues(playlist); + //_sequencesListStore.AppendValues(playlist.Songs.Select(s => new TreeView(_sequencesListStore)).ToArray()); + } + _sequenceNumberAdjustment.Upper = numSongs - 1; +#if DEBUG + // [Debug methods specific to this UI will go in here] +#endif + _autoplay = false; + SetAndLoadSequence(Engine.Instance.Config.Playlists[0].Songs.Count == 0 ? 0 : Engine.Instance.Config.Playlists[0].Songs[0].Index); + _sequenceNumberSpinButton.Sensitive = _buttonPlay.Sensitive = _volumeScale.Sensitive = true; + ShowAll(); + } + private void DisposeEngine() + { + if (Engine.Instance is not null) + { + Stop(); + Engine.Instance.Dispose(); + } + + //_trackViewer?.UpdateTracks(); + Name = ConfigUtils.PROGRAM_NAME; + //_songInfo.SetNumTracks(0); + //_songInfo.ResetMutes(); + ResetPlaylistStuff(false); + UpdatePositionIndicators(0L); + _sequencesListView.SelectionGet -= SequencesListView_SelectionGet; + _sequenceNumberAdjustment.ValueChanged -= SequenceNumberSpinButton_ValueChanged; + _sequenceNumberSpinButton.Visible = false; + _sequenceNumberSpinButton.Value = _sequenceNumberAdjustment.Upper = 0; + _sequencesListView.Selection.SelectFunction = null; + _sequencesListView.Data.Clear(); + _sequencesListView.SelectionGet += SequencesListView_SelectionGet; + _sequenceNumberSpinButton.ValueChanged += SequenceNumberSpinButton_ValueChanged; + } + + private void UpdateUI(object? sender, EventArgs? e) + { + if (_stopUI) + { + _stopUI = false; + if (_playlistPlaying) + { + _playedSequences.Add(_curSong); + } + else + { + Stop(); + } + } + else + { + UpdatePositionIndicators(Engine.Instance!.Player.LoadedSong!.ElapsedTicks); + } + } + private void SongEnded() + { + _stopUI = true; + } + + // This updates _positionScale and _positionAdjustment to the value specified + // Note: Gtk.Scale is dependent on Gtk.Adjustment, which is why _positionAdjustment is used instead + private void UpdatePositionIndicators(long ticks) + { + if (_positionScaleFree) + { + _positionAdjustment.Value = ticks; // A Gtk.Adjustment field must be used here to avoid issues + } + } + } +} diff --git a/VG Music Studio - GTK3/Program.cs b/VG Music Studio - GTK3/Program.cs new file mode 100644 index 0000000..0f13fcc --- /dev/null +++ b/VG Music Studio - GTK3/Program.cs @@ -0,0 +1,23 @@ +using Gtk; +using System; + +namespace Kermalis.VGMusicStudio.GTK3 +{ + internal class Program + { + [STAThread] + public static void Main(string[] args) + { + Application.Init(); + + var app = new Application("org.Kermalis.VGMusicStudio.GTK3", GLib.ApplicationFlags.None); + app.Register(GLib.Cancellable.Current); + + var win = new MainWindow(); + app.AddWindow(win); + + win.Show(); + Application.Run(); + } + } +} diff --git a/VG Music Studio - GTK3/VG Music Studio - GTK3.csproj b/VG Music Studio - GTK3/VG Music Studio - GTK3.csproj new file mode 100644 index 0000000..f78e051 --- /dev/null +++ b/VG Music Studio - GTK3/VG Music Studio - GTK3.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + + + + + + + + + + + diff --git a/VG Music Studio - GTK4/ExtraLibBindings/Gtk.cs b/VG Music Studio - GTK4/ExtraLibBindings/Gtk.cs new file mode 100644 index 0000000..ebd2741 --- /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 0000000..125c4f7 --- /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/MainWindow.cs b/VG Music Studio - GTK4/MainWindow.cs new file mode 100644 index 0000000..268a558 --- /dev/null +++ b/VG Music Studio - GTK4/MainWindow.cs @@ -0,0 +1,1361 @@ +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 GObject; +using Adw; +using Gtk; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Timers; +using System.Runtime.InteropServices; +using System.Diagnostics; + +using Application = Adw.Application; +using Window = Adw.Window; + +namespace Kermalis.VGMusicStudio.GTK4; + +internal sealed class MainWindow : Window +{ + private int _duration = 0; + private int _position = 0; + + private PlayingPlaylist? _playlist; + private int _curSong = -1; + + private bool _songEnded = false; + private bool _stopUI = false; + private bool _autoplay = false; + + public static Window? Instance { get; private set; } + + #region Widgets + + // Buttons + private Button _buttonPlay, _buttonPause, _buttonStop; + + // Spin Button for the numbered tracks + private readonly SpinButton _sequenceNumberSpinButton; + + // Timer + private GLib.Source _source; + private readonly GLib.Timer _timer; + + // Popover Menu Bar + private readonly PopoverMenuBar _popoverMenuBar; + + // LibAdwaita Header Bar + private readonly Adw.HeaderBar _headerBar; + + // LibAdwaita Application + private readonly Adw.Application _app; + + // Menu Model + //private readonly Gio.MenuModel _mainMenu; + + // Menus + private readonly Gio.Menu _mainMenu, _fileMenu, _dataMenu, _playlistMenu; + + // Menu Labels + private readonly Label _fileLabel, _dataLabel, _playlistLabel; + + // Menu Items + private readonly Gio.MenuItem _fileItem, _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem, + _dataItem, _exportDLSItem, _exportSF2Item, _exportMIDIItem, _exportWAVItem, _playlistItem, _endPlaylistItem; + + // Menu Actions + private Gio.SimpleAction _openDSEAction, _openAlphaDreamAction, _openMP2KAction, _openSDATAction, + _exportDLSAction, _exportSF2Action, _exportMIDIAction, _exportWAVAction, _endPlaylistAction; + + // Main Box + private Box _mainBox, _configButtonBox, _configPlayerButtonBox, _configSpinButtonBox, _configScaleBox; + + // One Scale controling volume and one Scale for the sequenced track + private Scale _volumeBar, _positionBar; + + // Mouse Click Gesture + private GestureClick _positionGestureClick; + + // Adjustments are for indicating the numbers and the position of the scale + private readonly Adjustment _sequenceNumberAdjustment; + //private ScaleControl _positionAdjustment; + + // Sound Sequence List + //private SignalListItemFactory _soundSequenceFactory; + //private SoundSequenceList _soundSequenceList; + //private SoundSequenceListItem _soundSequenceListItem; + //private SortListModel _soundSequenceSortListModel; + //private ListBox _soundSequenceListBox; + //private DropDown _soundSequenceDropDown; + + // Error Handle + private GLib.Internal.ErrorOwnedHandle ErrorHandle = new GLib.Internal.ErrorOwnedHandle(IntPtr.Zero); + + // Signal + //private Signal _signal; + + // Callback + private Gio.Internal.AsyncReadyCallback _saveCallback { get; set; } + private Gio.Internal.AsyncReadyCallback _openCallback { get; set; } + private Gio.Internal.AsyncReadyCallback _selectFolderCallback { get; set; } + private Gio.Internal.AsyncReadyCallback _exceptionCallback { get; set; } + + #endregion + + public MainWindow(Application app) + { + // Main Window + SetDefaultSize(500, 300); // Sets the default size of the Window + Title = ConfigUtils.PROGRAM_NAME; // Sets the title to the name of the program, which is "VG Music Studio" + _app = app; + + // LibAdwaita Header Bar + _headerBar = Adw.HeaderBar.New(); + _headerBar.SetShowEndTitleButtons(true); + + // Main Menu + _mainMenu = Gio.Menu.New(); + + // Popover Menu Bar + _popoverMenuBar = 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 = 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(); + + // Data Menu + _dataMenu = Gio.Menu.New(); + + _dataLabel = Label.NewWithMnemonic(Strings.MenuData); + _dataLabel.GetMnemonicKeyval(); + _dataLabel.SetUseUnderline(true); + _dataItem = Gio.MenuItem.New(_dataLabel.GetLabel(), null); + _popoverMenuBar.AddMnemonicLabel(_dataLabel); + _dataItem.SetSubmenu(_dataMenu); + + _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(); + + _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(); + + _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 = Label.NewWithMnemonic(Strings.MenuPlaylist); + _playlistLabel.GetMnemonicKeyval(); + _playlistLabel.SetUseUnderline(true); + _playlistItem = Gio.MenuItem.New(_playlistLabel.GetLabel(), null); + _popoverMenuBar.AddMnemonicLabel(_playlistLabel); + _playlistItem.SetSubmenu(_playlistMenu); + + _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(); + + // Buttons + _buttonPlay = new Button() { Sensitive = false, Label = Strings.PlayerPlay }; + _buttonPlay.OnClicked += (o, e) => Play(); + _buttonPause = new Button() { Sensitive = false, Label = Strings.PlayerPause }; + _buttonPause.OnClicked += (o, e) => Pause(); + _buttonStop = new Button() { Sensitive = false, Label = Strings.PlayerStop }; + _buttonStop.OnClicked += (o, e) => Stop(); + + // Spin Button + _sequenceNumberAdjustment = Adjustment.New(0, 0, -1, 1, 1, 1); + _sequenceNumberSpinButton = SpinButton.New(_sequenceNumberAdjustment, 1, 0); + _sequenceNumberSpinButton.Sensitive = false; + _sequenceNumberSpinButton.Value = 0; + //_sequenceNumberSpinButton.Visible = false; + _sequenceNumberSpinButton.OnValueChanged += SequenceNumberSpinButton_ValueChanged; + + // // Timer + _timer = GLib.Timer.New(); + // _timer = new Timer(); + // _timer.Elapsed += Timer_Tick; + + // Volume Bar + _volumeBar = Scale.New(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 = Scale.New(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 = GestureClick.New(); + _positionBar.AddController(_positionGestureClick); + _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.OnEnd += PositionBar_MouseButtonRelease; + _positionGestureClick.OnBegin += PositionBar_MouseButtonPress; + + // Sound Sequence List + //_soundSequenceList = new SoundSequenceList { Sensitive = false }; + //_soundSequenceFactory = SignalListItemFactory.New(); + //_soundSequenceListBox = ListBox.New(); + //_soundSequenceDropDown = DropDown.New(Gio.ListStore.New(DropDown.GetGType()), new ConstantExpression(IntPtr.Zero)); + //_soundSequenceDropDown.OnActivate += SequencesListView_SelectionGet; + //_soundSequenceDropDown.ListFactory = _soundSequenceFactory; + //_soundSequenceAction = Gio.SimpleAction.New("soundSequenceList", null); + //_soundSequenceAction.OnActivate += SequencesListView_SelectionGet; + + // Main display + _mainBox = Box.New(Orientation.Vertical, 4); + _configButtonBox = Box.New(Orientation.Horizontal, 2); + _configButtonBox.Halign = Align.Center; + _configPlayerButtonBox = Box.New(Orientation.Horizontal, 3); + _configPlayerButtonBox.Halign = Align.Center; + _configSpinButtonBox = Box.New(Orientation.Horizontal, 1); + _configSpinButtonBox.Halign = Align.Center; + _configSpinButtonBox.WidthRequest = 100; + _configScaleBox = Box.New(Orientation.Horizontal, 2); + _configScaleBox.Halign = 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); + + if (_configSpinButtonBox.GetFirstChild() == null) + { + _sequenceNumberSpinButton.Hide(); + _configSpinButtonBox.Append(_sequenceNumberSpinButton); + } + + _volumeBar.MarginStart = 20; + _volumeBar.MarginEnd = 20; + _configScaleBox.Append(_volumeBar); + _positionBar.MarginStart = 20; + _positionBar.MarginEnd = 20; + _configScaleBox.Append(_positionBar); + + _mainBox.Append(_headerBar); + _mainBox.Append(_popoverMenuBar); + _mainBox.Append(_configButtonBox); + _mainBox.Append(_configScaleBox); + //_mainBox.Append(_soundSequenceListBox); + + SetContent(_mainBox); + + Instance = this; + + Show(); + + // Ensures the entire application gets closed when the main window is closed + OnCloseRequest += (sender, args) => + { + DisposeEngine(); // Engine must be disposed first, otherwise the window will softlock when closing + _app.Quit(); + return true; + }; + } + + // 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 void PositionBar_MouseButtonRelease(object sender, EventArgs args) + { + 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 + LetUIKnowPlayerIsPlaying(); // This method will run the void that tells the UI that the player is playing a track + } + private bool PositionBar_ChangeValue(object sender, EventArgs args) + { + _positionBarFree = false; + + return false; + } + private void PositionBar_MoveSlider(object sender, EventArgs args) + { + 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 PositionBar_MouseButtonPress(object sender, EventArgs args) + { + _positionBarFree = false; + } + + private void SequenceNumberSpinButton_ValueChanged(object sender, EventArgs e) + { + //_sequencesGestureClick.OnBegin -= SequencesListView_SelectionGet; + //_signal.Connect(_sequencesListFactory, SequencesListView_SelectionGet, false, null); + + int index = (int)_sequenceNumberAdjustment.Value; + Stop(); + this.Title = ConfigUtils.PROGRAM_NAME; + //_sequencesListView.Margin = 0; + //_songInfo.Reset(); + 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 + { + player.LoadSong(index); + success = Engine.Instance.Player.LoadedSong is not null; // TODO: Make sure loadedsong is null when there are no tracks (for each engine, only mp2k guarantees it rn) + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, string.Format(Strings.ErrorLoadSong, Engine.Instance!.Config.GetSongName(index))); + success = false; + } + + //_trackViewer?.UpdateTracks(); + ILoadedSong? loadedSong = player.LoadedSong; // LoadedSong is still null when there are no tracks + if (success) + { + List songs = cfg.Playlists[0].Songs; // Complete "Music" playlist is present in all configs at index 0 + int songIndex = songs.FindIndex(s => s.Index == index); + if (songIndex != -1) + { + this.Title = $"{ConfigUtils.PROGRAM_NAME} - {songs[songIndex].Name}"; // TODO: Make this a func + //_sequencesColumnView.SortColumnId = songs.IndexOf(song) + 1; // + 1 because the "Music" playlist is first in the combobox + } + _positionBar.Adjustment!.Upper = loadedSong!.MaxTicks; + _positionBar.SetRange(0, loadedSong.MaxTicks); + //_songInfo.SetNumTracks(Engine.Instance.Player.LoadedSong.Events.Length); + if (_autoplay) + { + Show(); + Play(); + } + } + else + { + //_songInfo.SetNumTracks(0); + } + _positionBar.Sensitive = _exportWAVAction.Enabled = success; + _exportMIDIAction.Enabled = success && MP2KEngine.MP2KInstance is not null; + _exportDLSAction.Enabled = _exportSF2Action.Enabled = success && AlphaDreamEngine.AlphaDreamInstance is not null; + + _autoplay = true; + //_sequencesGestureClick.OnEnd += SequencesListView_SelectionGet; + //_signal.Connect(_sequencesListFactory, SequencesListView_SelectionGet, true, null); + Show(); + } + //private void SequencesListView_SelectionGet(object sender, EventArgs e) + //{ + // var item = _soundSequenceList.SelectedItem; + // if (item is Config.Song song) + // { + // SetAndLoadSong(song.Index); + // } + // else if (item is Config.Playlist playlist) + // { + // if (playlist.Songs.Count > 0 + // && FlexibleMessageBox.Show(string.Format(Strings.PlayPlaylistBody, Environment.NewLine + playlist), Strings.MenuPlaylist, ButtonsType.YesNo) == ResponseType.Yes) + // { + // ResetPlaylistStuff(false); + // _curPlaylist = playlist; + // Engine.Instance.Player.ShouldFadeOut = _playlistPlaying = true; + // Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; + // _endPlaylistAction.Enabled = true; + // SetAndLoadNextPlaylistSong(); + // } + // } + //} + public void SetAndLoadSong(int index) + { + _curSong = index; + if (_sequenceNumberSpinButton.Value == index) + { + SequenceNumberSpinButton_ValueChanged(this, EventArgs.Empty); + } + else + { + _sequenceNumberSpinButton.Value = index; + } + } + + //private void SetAndLoadNextPlaylistSong() + //{ + // if (_remainingSequences.Count == 0) + // { + // _remainingSequences.AddRange(_curPlaylist.Songs.Select(s => s.Index)); + // if (GlobalConfig.Instance.PlaylistMode == PlaylistMode.Random) + // { + // _remainingSequences.Any(); + // } + // } + // long nextSequence = _remainingSequences[0]; + // _remainingSequences.RemoveAt(0); + // SetAndLoadSong(nextSequence); + //} + private void ResetPlaylistStuff(bool spinButtonAndListBoxEnabled) + { + if (Engine.Instance != null) + { + Engine.Instance.Player.ShouldFadeOut = false; + } + _curSong = -1; + _endPlaylistAction.Enabled = false; + _sequenceNumberSpinButton.Sensitive = /* _soundSequenceListBox.Sensitive = */ spinButtonAndListBoxEnabled; + } + private void EndCurrentPlaylist(object sender, EventArgs e) + { + if (FlexibleMessageBox.Show(Strings.EndPlaylistBody, Strings.MenuPlaylist, ButtonsType.YesNo) == ResponseType.Yes) + { + ResetPlaylistStuff(true); + } + } + + private void OpenDSE(Gio.SimpleAction sender, EventArgs e) + { + if (Gtk.Functions.GetMinorVersion() <= 8) // There's a bug in Gtk 4.09 and later that has broken FileChooserNative functionality, causing icons and thumbnails to appear broken + { + // To allow the dialog to display in native windowing format, FileChooserNative is used instead of FileChooserDialog + var d = FileChooserNative.New( + Strings.MenuOpenDSE, // The title shown in the folder select dialog window + this, // 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 += (sender, 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.Unref(); + return; + } + var path = d.GetCurrentFolder()!.GetPath() ?? ""; + d.GetData(path); + OpenDSEFinish(path); + d.Unref(); // Ensures disposal of the dialog when closed + return; + }; + d.Show(); + } + else + { + var d = FileDialog.New(); + d.SetTitle(Strings.MenuOpenDSE); + + _selectFolderCallback = (source, res, data) => + { + var folderHandle = Gtk.Internal.FileDialog.SelectFolderFinish(d.Handle, res, out ErrorHandle); + if (folderHandle != IntPtr.Zero) + { + var path = Marshal.PtrToStringUTF8(Gio.Internal.File.GetPath(folderHandle).DangerousGetHandle()); + OpenDSEFinish(path!); + d.Unref(); + } + d.Unref(); + }; + Gtk.Internal.FileDialog.SelectFolder(d.Handle, Handle, IntPtr.Zero, _selectFolderCallback, IntPtr.Zero); // 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 + //d.SelectFolder(Handle, IntPtr.Zero, _selectFolderCallback, IntPtr.Zero); + } + } + private void OpenDSEFinish(string path) + { + DisposeEngine(); + try + { + _ = new DSEEngine(path); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorOpenDSE); + return; + } + DSEConfig config = DSEEngine.DSEInstance!.Config; + FinishLoading(config.BGMFiles.Length); + _sequenceNumberSpinButton.Visible = false; + _sequenceNumberSpinButton.Hide(); + _mainMenu.AppendItem(_playlistItem); + _exportDLSAction.Enabled = false; + _exportMIDIAction.Enabled = false; + _exportSF2Action.Enabled = false; + } + private void OpenSDAT(Gio.SimpleAction sender, EventArgs e) + { + var filterSDAT = FileFilter.New(); + filterSDAT.SetName(Strings.FilterOpenSDAT); + filterSDAT.AddPattern("*.sdat"); + var allFiles = FileFilter.New(); + allFiles.SetName(Strings.FilterAllFiles); + allFiles.AddPattern("*.*"); + + if (Gtk.Functions.GetMinorVersion() <= 8) + { + var d = FileChooserNative.New( + Strings.MenuOpenSDAT, + this, + FileChooserAction.Open, + "Open", + "Cancel"); + + d.SetModal(true); + + d.AddFilter(filterSDAT); + d.AddFilter(allFiles); + + d.OnResponse += (sender, e) => + { + if (e.ResponseId != (int)ResponseType.Accept) + { + d.Unref(); + return; + } + + var path = d.GetFile()!.GetPath() ?? ""; + d.GetData(path); + OpenSDATFinish(path); + d.Unref(); + }; + d.Show(); + } + else + { + var d = FileDialog.New(); + d.SetTitle(Strings.MenuOpenSDAT); + var filters = Gio.ListStore.New(FileFilter.GetGType()); + filters.Append(filterSDAT); + filters.Append(allFiles); + d.SetFilters(filters); + _openCallback = (source, res, data) => + { + var fileHandle = Gtk.Internal.FileDialog.OpenFinish(d.Handle, res, out ErrorHandle); + if (fileHandle != IntPtr.Zero) + { + var path = Marshal.PtrToStringUTF8(Gio.Internal.File.GetPath(fileHandle).DangerousGetHandle()); + OpenSDATFinish(path!); + d.Unref(); + } + d.Unref(); + }; + Gtk.Internal.FileDialog.Open(d.Handle, Handle, IntPtr.Zero, _openCallback, IntPtr.Zero); + //d.Open(Handle, IntPtr.Zero, _openCallback, IntPtr.Zero); + } + } + private void OpenSDATFinish(string path) + { + DisposeEngine(); + try + { + using (FileStream stream = File.OpenRead(path)) + { + _ = new SDATEngine(new SDAT(stream)); + } + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorOpenSDAT); + return; + } + + SDATConfig config = SDATEngine.SDATInstance!.Config; + FinishLoading(config.SDAT.INFOBlock.SequenceInfos.NumEntries); + _sequenceNumberSpinButton.Visible = true; + _sequenceNumberSpinButton.Show(); + _exportDLSAction.Enabled = false; + _exportMIDIAction.Enabled = false; + _exportSF2Action.Enabled = false; + } + private void OpenAlphaDream(Gio.SimpleAction sender, EventArgs e) + { + var filterGBA = FileFilter.New(); + filterGBA.SetName(Strings.FilterOpenGBA); + filterGBA.AddPattern("*.gba"); + filterGBA.AddPattern("*.srl"); + var allFiles = FileFilter.New(); + allFiles.SetName(Name = Strings.FilterAllFiles); + allFiles.AddPattern("*.*"); + + if (Gtk.Functions.GetMinorVersion() <= 8) + { + var d = FileChooserNative.New( + Strings.MenuOpenAlphaDream, + this, + FileChooserAction.Open, + "Open", + "Cancel"); + d.SetModal(true); + + d.AddFilter(filterGBA); + d.AddFilter(allFiles); + + d.OnResponse += (sender, e) => + { + if (e.ResponseId != (int)ResponseType.Accept) + { + d.Unref(); + return; + } + var path = d.GetFile()!.GetPath() ?? ""; + d.GetData(path); + OpenAlphaDreamFinish(path); + d.Unref(); + }; + d.Show(); + } + else + { + var d = FileDialog.New(); + d.SetTitle(Strings.MenuOpenAlphaDream); + var filters = Gio.ListStore.New(FileFilter.GetGType()); + filters.Append(filterGBA); + filters.Append(allFiles); + d.SetFilters(filters); + _openCallback = (source, res, data) => + { + var fileHandle = Gtk.Internal.FileDialog.OpenFinish(d.Handle, res, out ErrorHandle); + if (fileHandle != IntPtr.Zero) + { + var path = Marshal.PtrToStringUTF8(Gio.Internal.File.GetPath(fileHandle).DangerousGetHandle()); + OpenAlphaDreamFinish(path!); + d.Unref(); + } + d.Unref(); + }; + Gtk.Internal.FileDialog.Open(d.Handle, Handle, IntPtr.Zero, _openCallback, IntPtr.Zero); + //d.Open(Handle, IntPtr.Zero, _openCallback, IntPtr.Zero); + } + } + private void OpenAlphaDreamFinish(string path) + { + DisposeEngine(); + try + { + _ = new AlphaDreamEngine(File.ReadAllBytes(path)); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorOpenAlphaDream); + return; + } + + AlphaDreamConfig config = AlphaDreamEngine.AlphaDreamInstance!.Config; + FinishLoading(config.SongTableSizes[0]); + _sequenceNumberSpinButton.Visible = true; + _sequenceNumberSpinButton.Show(); + _mainMenu.AppendItem(_dataItem); + _mainMenu.AppendItem(_playlistItem); + _exportDLSAction.Enabled = true; + _exportMIDIAction.Enabled = false; + _exportSF2Action.Enabled = true; + } + private void OpenMP2K(Gio.SimpleAction sender, EventArgs e) + { + //var inFile = GTK4Utils.CreateLoadDialog(["*.gba", "*.srl"], Strings.MenuOpenMP2K, Strings.FilterOpenGBA); + //if (inFile is not null) + //{ + // OpenMP2KFinish(inFile); + //} + + + FileFilter filterGBA = FileFilter.New(); + filterGBA.SetName(Strings.FilterOpenGBA); + filterGBA.AddPattern("*.gba"); + filterGBA.AddPattern("*.srl"); + FileFilter allFiles = FileFilter.New(); + allFiles.SetName(Strings.FilterAllFiles); + allFiles.AddPattern("*.*"); + + if (Gtk.Functions.GetMinorVersion() <= 8) + { + var d = FileChooserNative.New( + Strings.MenuOpenMP2K, + this, + FileChooserAction.Open, + "Open", + "Cancel"); + + + d.AddFilter(filterGBA); + d.AddFilter(allFiles); + + d.OnResponse += (sender, e) => + { + if (e.ResponseId != (int)ResponseType.Accept) + { + d.Unref(); + return; + } + var path = d.GetFile()!.GetPath() ?? ""; + OpenMP2KFinish(path); + d.Unref(); + }; + d.Show(); + } + else + { + var d = FileDialog.New(); + d.SetTitle(Strings.MenuOpenMP2K); + var filters = Gio.ListStore.New(FileFilter.GetGType()); + filters.Append(filterGBA); + filters.Append(allFiles); + d.SetFilters(filters); + _openCallback = (source, res, data) => + { + var fileHandle = Gtk.Internal.FileDialog.OpenFinish(d.Handle, res, out ErrorHandle); + if (fileHandle != IntPtr.Zero) + { + var path = Marshal.PtrToStringUTF8(Gio.Internal.File.GetPath(fileHandle).DangerousGetHandle()); + OpenMP2KFinish(path!); + filterGBA.Unref(); + allFiles.Unref(); + filters.Unref(); + GObject.Internal.Object.Unref(fileHandle); + d.Unref(); + return; + } + d.Unref(); + }; + Gtk.Internal.FileDialog.Open(d.Handle, Handle, IntPtr.Zero, _openCallback, IntPtr.Zero); + //d.Open(Handle, IntPtr.Zero, _openCallback, IntPtr.Zero); + } + } + private void OpenMP2KFinish(string path) + { + if (Engine.Instance is not null) + { + DisposeEngine(); + } + + try + { + _ = new MP2KEngine(File.ReadAllBytes(path)); + } + catch (Exception ex) + { + //_dialog = Adw.MessageDialog.New(this, Strings.ErrorOpenMP2K, ex.ToString()); + //FlexibleMessageBox.Show(ex, Strings.ErrorOpenMP2K); + DisposeEngine(); + ExceptionDialog(ex, Strings.ErrorOpenMP2K); + return; + } + + MP2KConfig config = MP2KEngine.MP2KInstance!.Config; + FinishLoading(config.SongTableSizes[0]); + _sequenceNumberSpinButton.Visible = true; + _sequenceNumberSpinButton.Show(); + //_mainMenu.AppendItem(_dataItem); + //_mainMenu.AppendItem(_playlistItem); + _exportDLSAction.Enabled = false; + _exportMIDIAction.Enabled = true; + _exportSF2Action.Enabled = false; + } + private void ExportDLS(Gio.SimpleAction sender, EventArgs e) + { + AlphaDreamConfig cfg = AlphaDreamEngine.AlphaDreamInstance!.Config; + + FileFilter ff = FileFilter.New(); + ff.SetName(Strings.FilterSaveDLS); + ff.AddPattern("*.dls"); + + if (Gtk.Functions.GetMinorVersion() <= 8) + { + var d = FileChooserNative.New( + Strings.MenuSaveDLS, + this, + FileChooserAction.Save, + "Save", + "Cancel"); + d.SetCurrentName(cfg.GetGameName()); + d.AddFilter(ff); + + d.OnResponse += (sender, e) => + { + if (e.ResponseId != (int)ResponseType.Accept) + { + d.Unref(); + return; + } + + var path = d.GetFile()!.GetPath() ?? ""; + ExportDLSFinish(cfg, path); + d.Unref(); + }; + d.Show(); + } + else + { + var d = FileDialog.New(); + d.SetTitle(Strings.MenuSaveDLS); + var filters = Gio.ListStore.New(FileFilter.GetGType()); + filters.Append(ff); + d.SetFilters(filters); + _saveCallback = (source, res, data) => + { + var fileHandle = Gtk.Internal.FileDialog.SaveFinish(d.Handle, res, out ErrorHandle); + if (fileHandle != IntPtr.Zero) + { + var path = Marshal.PtrToStringUTF8(Gio.Internal.File.GetPath(fileHandle).DangerousGetHandle()); + ExportDLSFinish(cfg, path!); + d.Unref(); + } + d.Unref(); + }; + Gtk.Internal.FileDialog.Save(d.Handle, Handle, IntPtr.Zero, _saveCallback, IntPtr.Zero); + //d.Save(Handle, IntPtr.Zero, _saveCallback, IntPtr.Zero); + } + } + private void ExportDLSFinish(AlphaDreamConfig config, string path) + { + try + { + AlphaDreamSoundFontSaver_DLS.Save(config, path); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveDLS, path), Strings.SuccessSaveDLS); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveDLS); + } + } + private void ExportMIDI(Gio.SimpleAction sender, EventArgs e) + { + FileFilter ff = FileFilter.New(); + ff.SetName(Strings.FilterSaveMIDI); + ff.AddPattern("*.mid"); + ff.AddPattern("*.midi"); + + if (Gtk.Functions.GetMinorVersion() <= 8) + { + var d = FileChooserNative.New( + Strings.MenuSaveMIDI, + this, + FileChooserAction.Save, + "Save", + "Cancel"); + d.SetCurrentName(Engine.Instance!.Config.GetSongName((int)_sequenceNumberSpinButton.Value)); + d.AddFilter(ff); + + d.OnResponse += (sender, e) => + { + if (e.ResponseId != (int)ResponseType.Accept) + { + d.Unref(); + return; + } + + var path = d.GetFile()!.GetPath() ?? ""; + ExportMIDIFinish(path); + d.Unref(); + }; + d.Show(); + } + else + { + var d = FileDialog.New(); + d.SetTitle(Strings.MenuSaveMIDI); + var filters = Gio.ListStore.New(FileFilter.GetGType()); + filters.Append(ff); + d.SetFilters(filters); + _saveCallback = (source, res, data) => + { + var fileHandle = Gtk.Internal.FileDialog.SaveFinish(d.Handle, res, out ErrorHandle); + if (fileHandle != IntPtr.Zero) + { + var path = Marshal.PtrToStringUTF8(Gio.Internal.File.GetPath(fileHandle).DangerousGetHandle()); + ExportMIDIFinish(path!); + d.Unref(); + } + d.Unref(); + }; + Gtk.Internal.FileDialog.Save(d.Handle, Handle, IntPtr.Zero, _saveCallback, IntPtr.Zero); + //d.Save(Handle, IntPtr.Zero, _saveCallback, IntPtr.Zero); + } + } + private void ExportMIDIFinish(string path) + { + MP2KPlayer p = MP2KEngine.MP2KInstance!.Player; + var args = new MIDISaveArgs(true, false, new (int AbsoluteTick, (byte Numerator, byte Denominator))[] + { + (0, (4, 4)), + }); + + try + { + p.SaveAsMIDI(path, args); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveMIDI, path), Strings.SuccessSaveMIDI); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveMIDI); + } + } + private void ExportSF2(Gio.SimpleAction sender, EventArgs e) + { + AlphaDreamConfig cfg = AlphaDreamEngine.AlphaDreamInstance!.Config; + + FileFilter ff = FileFilter.New(); + ff.SetName(Strings.FilterSaveSF2); + ff.AddPattern("*.sf2"); + + if (Gtk.Functions.GetMinorVersion() <= 8) + { + var d = FileChooserNative.New( + Strings.MenuSaveSF2, + this, + FileChooserAction.Save, + "Save", + "Cancel"); + + d.SetCurrentName(cfg.GetGameName()); + d.AddFilter(ff); + + d.OnResponse += (sender, e) => + { + if (e.ResponseId != (int)ResponseType.Accept) + { + d.Unref(); + return; + } + + var path = d.GetFile()!.GetPath() ?? ""; + ExportSF2Finish(path, cfg); + d.Unref(); + }; + d.Show(); + } + else + { + var d = FileDialog.New(); + d.SetTitle(Strings.MenuSaveSF2); + var filters = Gio.ListStore.New(FileFilter.GetGType()); + filters.Append(ff); + d.SetFilters(filters); + _saveCallback = (source, res, data) => + { + var fileHandle = Gtk.Internal.FileDialog.SaveFinish(d.Handle, res, out ErrorHandle); + if (fileHandle != IntPtr.Zero) + { + var path = Marshal.PtrToStringUTF8(Gio.Internal.File.GetPath(fileHandle).DangerousGetHandle()); + ExportSF2Finish(path!, cfg); + d.Unref(); + } + d.Unref(); + }; + Gtk.Internal.FileDialog.Save(d.Handle, Handle, IntPtr.Zero, _saveCallback, IntPtr.Zero); + //d.Save(Handle, IntPtr.Zero, _saveCallback, IntPtr.Zero); + } + } + private void ExportSF2Finish(string path, AlphaDreamConfig config) + { + try + { + AlphaDreamSoundFontSaver_SF2.Save(path, config); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveSF2, path), Strings.SuccessSaveSF2); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveSF2); + } + } + private void ExportWAV(Gio.SimpleAction sender, EventArgs e) + { + FileFilter ff = FileFilter.New(); + ff.SetName(Strings.FilterSaveWAV); + ff.AddPattern("*.wav"); + + if (Gtk.Functions.GetMinorVersion() <= 8) + { + var d = FileChooserNative.New( + Strings.MenuSaveWAV, + this, + FileChooserAction.Save, + "Save", + "Cancel"); + + d.SetCurrentName(Engine.Instance!.Config.GetSongName((int)_sequenceNumberSpinButton.Value)); + d.AddFilter(ff); + + d.OnResponse += (sender, e) => + { + if (e.ResponseId != (int)ResponseType.Accept) + { + d.Unref(); + return; + } + + var path = d.GetFile()!.GetPath() ?? ""; + ExportWAVFinish(path); + d.Unref(); + }; + d.Show(); + } + else + { + var d = FileDialog.New(); + d.SetTitle(Strings.MenuSaveWAV); + var filters = Gio.ListStore.New(FileFilter.GetGType()); + filters.Append(ff); + d.SetFilters(filters); + _saveCallback = (source, res, data) => + { + var fileHandle = Gtk.Internal.FileDialog.SaveFinish(d.Handle, res, out ErrorHandle); + if (fileHandle != IntPtr.Zero) + { + var path = Marshal.PtrToStringUTF8(Gio.Internal.File.GetPath(fileHandle).DangerousGetHandle()); + ExportWAVFinish(path!); + d.Unref(); + } + d.Unref(); + }; + Gtk.Internal.FileDialog.Save(d.Handle, Handle, IntPtr.Zero, _saveCallback, IntPtr.Zero); + //d.Save(Handle, IntPtr.Zero, _saveCallback, IntPtr.Zero); + } + } + private void ExportWAVFinish(string path) + { + 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); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveWAV, path), Strings.SuccessSaveWAV); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveWAV); + } + + player.ShouldFadeOut = oldFade; + player.NumLoops = oldLoops; + _stopUI = false; + } + + public void ExceptionDialog(Exception error, string heading) + { + Debug.WriteLine(error.Message); + var md = Adw.MessageDialog.New(this, heading, error.Message); + md.SetModal(true); + md.AddResponse("ok", ("_OK")); + md.SetResponseAppearance("ok", ResponseAppearance.Default); + md.SetDefaultResponse("ok"); + md.SetCloseResponse("ok"); + _exceptionCallback = (source, res, data) => + { + md.Destroy(); + }; + md.Activate(); + md.Show(); + } + + public void LetUIKnowPlayerIsPlaying() + { + // Prevents method from being used if timer is already active + // if (_timer.Enabled) + // { + // return; + // } + + // 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 + } + + // 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.Label = Strings.PlayerPause; + var context = GLib.MainContext.GetThreadDefault(); + var source = GLib.Functions.TimeoutSourceNew((uint)(1_000.0 / GlobalConfig.Instance!.RefreshRate)); + source.SetCallback(CheckPlayback); + var microsec = (ulong)source.Attach(context); + _timer.Elapsed(ref microsec); + //GLib.Functions.TimeoutAdd(0, (uint)(1_000.0 / GlobalConfig.Instance!.RefreshRate), new GLib.SourceFunc(CheckPlayback)); + //GLib.Functions.TestTimerStart(); + // _timer.Interval = (int)(1_000.0 / GlobalConfig.Instance!.RefreshRate); + _timer.Start(); + Show(); + } + + private void Play() + { + Engine.Instance!.Player.Play(); + LetUIKnowPlayerIsPlaying(); + } + private void Pause() + { + Engine.Instance!.Player.TogglePlaying(); + if (Engine.Instance.Player.State == PlayerState.Paused) + { + _buttonPause.Label = Strings.PlayerUnpause; + _timer.Stop(); + } + else + { + _buttonPause.Label = Strings.PlayerPause; + _timer.Start(); + } + } + private void Stop() + { + if (Engine.Instance == null) + { + return; // This is here to ensure that it returns if the Engine.Instance is null while closing the main window + } + Engine.Instance!.Player.Stop(); + _buttonPause.Sensitive = _buttonStop.Sensitive = false; + _buttonPause.Label = Strings.PlayerPause; + _timer.Stop(); + UpdatePositionIndicators(0L); + Show(); + } + private void TogglePlayback() + { + switch (Engine.Instance!.Player.State) + { + case PlayerState.Stopped: Play(); break; + case PlayerState.Paused: + case PlayerState.Playing: Pause(); break; + } + } + private void PlayPreviousSequence() + { + + if (_playlist is not null) + { + _playlist.UndoThenSetAndLoadPrevSong(this, _curSong); + } + else + { + SetAndLoadSong((int)_sequenceNumberSpinButton.Value - 1); + } + } + private void PlayNextSong(object? sender, EventArgs? e) + { + if (_playlist is not null) + { + _playlist.AdvanceThenSetAndLoadNextSong(this, _curSong); + } + else + { + SetAndLoadSong((int)_sequenceNumberSpinButton.Value + 1); + } + } + + private void FinishLoading(long numSongs) + { + Engine.Instance!.Player.SongEnded += SongEnded; + //foreach (Config.Playlist playlist in Engine.Instance.Config.Playlists) + //{ + // _soundSequenceListBox.Insert(Label.New(playlist.Name), playlist.Songs.Count); + // _soundSequenceList.Add(new SoundSequenceListItem(playlist)); + // _soundSequenceList.AddRange(playlist.Songs.Select(s => new SoundSequenceListItem(s)).ToArray()); + //} + _sequenceNumberAdjustment.Upper = numSongs - 1; +#if DEBUG + // [Debug methods specific to this GUI will go in here] +#endif + _autoplay = false; + SetAndLoadSong(Engine.Instance.Config.Playlists[0].Songs.Count == 0 ? 0 : Engine.Instance.Config.Playlists[0].Songs[0].Index); + _sequenceNumberSpinButton.Sensitive = _buttonPlay.Sensitive = _volumeBar.Sensitive = true; + _volumeBar.SetValue(100); + Show(); + } + private void DisposeEngine() + { + if (Engine.Instance is not null) + { + Stop(); + Engine.Instance.Dispose(); + } + + //_trackViewer?.UpdateTracks(); + Name = ConfigUtils.PROGRAM_NAME; + //_songInfo.SetNumTracks(0); + //_songInfo.ResetMutes(); + ResetPlaylistStuff(false); + UpdatePositionIndicators(0L); + //_signal.Connect(_sequencesListFactory, SequencesListView_SelectionGet, false, null); + _sequenceNumberAdjustment.OnValueChanged -= SequenceNumberSpinButton_ValueChanged; + _sequenceNumberSpinButton.Visible = false; + _sequenceNumberSpinButton.Value = _sequenceNumberAdjustment.Upper = 0; + //_sequencesListView.Selection.SelectFunction = null; + //_sequencesColumnView.Unref(); + //_signal.Connect(_sequencesListFactory, SequencesListView_SelectionGet, true, null); + _sequenceNumberSpinButton.OnValueChanged += SequenceNumberSpinButton_ValueChanged; + } + + private bool CheckPlayback() + { + if (Engine.Instance is not null) + { + if (_positionBarFree) + { + UpdatePositionIndicators(Engine.Instance!.Player.ElapsedTicks); + return true; + } + } + return false; + } + + private void Timer_Tick(object? sender, EventArgs e) + { + if (_songEnded) + { + _songEnded = false; + if (_playlist is not null) + { + _playlist.AdvanceThenSetAndLoadNextSong(this, _curSong); + } + else + { + Stop(); + } + } + else + { + if (Engine.Instance is not null) + { + Player player = Engine.Instance!.Player; + UpdatePositionIndicators(player.ElapsedTicks); + } + } + } + private void SongEnded() + { + _songEnded = true; + _stopUI = true; + } + + // This updates _positionBar and _positionAdjustment to the value specified + // Note: Gtk.Scale is dependent on Gtk.Adjustment, which is why _positionAdjustment is used instead + private void UpdatePositionIndicators(long ticks) + { + if (_positionBarFree) + { + _positionBar.Adjustment!.SetValue(ticks); + } + } +} diff --git a/VG Music Studio - GTK4/PlayingPlaylist.cs b/VG Music Studio - GTK4/PlayingPlaylist.cs new file mode 100644 index 0000000..14b7bbb --- /dev/null +++ b/VG Music Studio - GTK4/PlayingPlaylist.cs @@ -0,0 +1,53 @@ +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Core.Util; +using Kermalis.VGMusicStudio.GTK4.Util; +using Gtk; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +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 = new List(); + _remainingSongs = new List(); + _curPlaylist = play; + } + + public void AdvanceThenSetAndLoadNextSong(MainWindow parent, int curSong) + { + _playedSongs.Add(curSong); + SetAndLoadNextSong(parent); + } + public void UndoThenSetAndLoadPrevSong(MainWindow parent, int curSong) + { + int prevIndex = _playedSongs.Count - 1; + int prevSong = _playedSongs[prevIndex]; + _playedSongs.RemoveAt(prevIndex); + _remainingSongs.Insert(0, curSong); + parent.SetAndLoadSong(prevSong); + } + public void SetAndLoadNextSong(MainWindow parent) + { + 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); + parent.SetAndLoadSong(nextSong); + } +} diff --git a/VG Music Studio - GTK4/Program.cs b/VG Music Studio - GTK4/Program.cs new file mode 100644 index 0000000..46ca59f --- /dev/null +++ b/VG Music Studio - GTK4/Program.cs @@ -0,0 +1,49 @@ +using Adw; +using System; +using System.Runtime.InteropServices; + +namespace Kermalis.VGMusicStudio.GTK4 +{ + internal class Program + { + private static readonly Adw.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; + + [STAThread] + public static void Main(string[] args) + { + _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; + + void OnActivate(Gio.Application sender, EventArgs e) + { + + } + + 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.Show(); + _app.Run(args.Length, args); + } + } +} diff --git a/VG Music Studio - GTK4/Theme.cs b/VG Music Studio - GTK4/Theme.cs new file mode 100644 index 0000000..1fd8cf1 --- /dev/null +++ b/VG Music Studio - GTK4/Theme.cs @@ -0,0 +1,224 @@ +using Gtk; +using Kermalis.VGMusicStudio.Core.Util; +using Cairo; +using System.Reflection.Metadata; +using System.Runtime.InteropServices; +using System; +using Pango; +using Window = Gtk.Window; +using Context = Cairo.Context; + +namespace Kermalis.VGMusicStudio.GTK4; + +/// +/// LibAdwaita theme selection enumerations. +/// +public enum ThemeType +{ + Light = 0, // Light Theme + Dark, // Dark Theme + System // System Default Theme +} + +internal class Theme +{ + + public Theme ThemeType { get; set; } + + //[StructLayout(LayoutKind.Sequential)] + //public struct Color + //{ + // public float Red; + // public float Green; + // public float Blue; + // public float Alpha; + //} + + //[DllImport("libadwaita-1.so.0")] + //[return: MarshalAs(UnmanagedType.I1)] + //private static extern bool gdk_rgba_parse(ref Color rgba, string spec); + + //[DllImport("libadwaita-1.so.0")] + //private static extern string gdk_rgba_to_string(ref Color rgba); + + //[DllImport("libadwaita-1.so.0")] + //private static extern void gtk_color_chooser_get_rgba(nint chooser, ref Color rgba); + + //[DllImport("libadwaita-1.so.0")] + //private static extern void gtk_color_chooser_set_rgba(nint chooser, ref Color rgba); + + //public static Color FromArgb(int r, int g, int b) + //{ + // Color color = new Color(); + // r = (int)color.Red; + // g = (int)color.Green; + // b = (int)color.Blue; + + // return color; + //} + + //public static readonly Font Font = new("Segoe UI", 8f, FontStyle.Bold); + //public static readonly Color + // BackColor = Color.FromArgb(33, 33, 39), + // BackColorDisabled = Color.FromArgb(35, 42, 47), + // BackColorMouseOver = Color.FromArgb(32, 37, 47), + // BorderColor = Color.FromArgb(25, 120, 186), + // BorderColorDisabled = Color.FromArgb(47, 55, 60), + // ForeColor = Color.FromArgb(94, 159, 230), + // PlayerColor = Color.FromArgb(8, 8, 8), + // SelectionColor = Color.FromArgb(7, 51, 141), + // TitleBar = Color.FromArgb(16, 40, 63); + + + + //public static Color DrainColor(Color c) + //{ + // var hsl = new HSLColor(c); + // return HSLColor.ToColor(hsl.H, (byte)(hsl.S / 2.5), hsl.L); + //} +} + +internal sealed class ThemedButton : Button +{ + public ResponseType ResponseType; + public ThemedButton() + { + //FlatAppearance.MouseOverBackColor = Theme.BackColorMouseOver; + //FlatStyle = FlatStyle.Flat; + //Font = Theme.FontType; + //ForeColor = Theme.ForeColor; + } + protected void OnEnabledChanged(EventArgs e) + { + //base.OnEnabledChanged(e); + //BackColor = Enabled ? Theme.BackColor : Theme.BackColorDisabled; + //FlatAppearance.BorderColor = Enabled ? Theme.BorderColor : Theme.BorderColorDisabled; + } + protected void OnDraw(Context c) + { + //base.OnPaint(e); + //if (!Enabled) + //{ + // TextRenderer.DrawText(e.Graphics, Text, Font, ClientRectangle, Theme.DrainColor(ForeColor), BackColor); + //} + } + //protected override bool ShowFocusCues => false; +} +internal sealed class ThemedLabel : Label +{ + public ThemedLabel() + { + //Font = Theme.Font; + //ForeColor = Theme.ForeColor; + } +} +internal class ThemedWindow : Window +{ + public ThemedWindow() + { + //BackColor = Theme.BackColor; + //Icon = Resources.Icon; + } +} +internal class ThemedBox : Box +{ + public ThemedBox() + { + //SetStyle(ControlStyles.UserPaint | ControlStyles.ResizeRedraw | ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); + } + protected void OnDraw(Context c) + { + //base.OnPaint(e); + //using (var b = new SolidBrush(BackColor)) + //{ + // e.Graphics.FillRectangle(b, e.ClipRectangle); + //} + //using (var b = new SolidBrush(Theme.BorderColor)) + //using (var p = new Pen(b, 2)) + //{ + // e.Graphics.DrawRectangle(p, e.ClipRectangle); + //} + } + private const int WM_PAINT = 0xF; + //protected void WndProc(ref Message m) + //{ + // if (m.Msg == WM_PAINT) + // { + // Invalidate(); + // } + // base.WndProc(ref m); + //} +} +internal class ThemedTextBox : Adw.Window +{ + public Box Box; + public Text Text; + public ThemedTextBox() + { + //BackColor = Theme.BackColor; + //Font = Theme.Font; + //ForeColor = Theme.ForeColor; + Box = Box.New(Orientation.Horizontal, 0); + Text = Text.New(); + Box.Append(Text); + } + //[DllImport("user32.dll")] + //private static extern IntPtr GetWindowDC(IntPtr hWnd); + //[DllImport("user32.dll")] + //private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC); + //[DllImport("user32.dll")] + //private static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprc, IntPtr hrgn, uint flags); + //private const int WM_NCPAINT = 0x85; + //private const uint RDW_INVALIDATE = 0x1; + //private const uint RDW_IUPDATENOW = 0x100; + //private const uint RDW_FRAME = 0x400; + //protected override void WndProc(ref Message m) + //{ + // base.WndProc(ref m); + // if (m.Msg == WM_NCPAINT && BorderStyle == BorderStyle.Fixed3D) + // { + // IntPtr hdc = GetWindowDC(Handle); + // using (var g = Graphics.FromHdcInternal(hdc)) + // using (var p = new Pen(Theme.BorderColor)) + // { + // g.DrawRectangle(p, new Rectangle(0, 0, Width - 1, Height - 1)); + // } + // ReleaseDC(Handle, hdc); + // } + //} + protected void OnSizeChanged(EventArgs e) + { + //base.OnSizeChanged(e); + //RedrawWindow(Handle, IntPtr.Zero, IntPtr.Zero, RDW_FRAME | RDW_IUPDATENOW | RDW_INVALIDATE); + } +} +internal sealed class ThemedRichTextBox : Adw.Window +{ + public Box Box; + public Text Text; + public ThemedRichTextBox() + { + //BackColor = Theme.BackColor; + //Font = Theme.Font; + //ForeColor = Theme.ForeColor; + //SelectionColor = Theme.SelectionColor; + Box = Box.New(Orientation.Horizontal, 0); + Text = Text.New(); + Box.Append(Text); + } +} +internal sealed class ThemedNumeric : SpinButton +{ + public ThemedNumeric() + { + //BackColor = Theme.BackColor; + //Font = new Font(Theme.Font.FontFamily, 7.5f, Theme.Font.Style); + //ForeColor = Theme.ForeColor; + //TextAlign = HorizontalAlignment.Center; + } + protected void OnDraw(Context c) + { + //base.OnPaint(e); + //ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Enabled ? Theme.BorderColor : Theme.BorderColorDisabled, ButtonBorderStyle.Solid); + } +} \ No newline at end of file diff --git a/VG Music Studio - GTK4/Util/FlexibleMessageBox.cs b/VG Music Studio - GTK4/Util/FlexibleMessageBox.cs new file mode 100644 index 0000000..621175f --- /dev/null +++ b/VG Music Studio - GTK4/Util/FlexibleMessageBox.cs @@ -0,0 +1,763 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using Adw; + +namespace Kermalis.VGMusicStudio.GTK4.Util; + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * FlexibleMessageBox + * + * A Message Box completely rewritten by Davin (Platinum Lucario) for use with Gir.Core (GTK4 and LibAdwaita) + * on VG Music Studio, modified from the WinForms-based FlexibleMessageBox originally made by Jörg Reichert. + * + * This uses Adw.Window to create a window similar to MessageDialog, since + * MessageDialog and many Gtk.Dialog functions are deprecated since GTK version 4.10, + * Adw.Window and Gtk.Window are better supported (and probably won't be deprecated until several major versions later). + * + * Features include: + * - Extra options for a dialog box style Adw.Window with the Show() function + * - Displays a vertical scrollbar, just like the original one did + * - Only one source file is used + * - Much less lines of code than the original, due to built-in GTK4 and LibAdwaita functions + * - All WinForms functions removed and replaced with GObject library functions via Gir.Core + * + * GitHub: https://github.com/PlatinumLucario + * Repository: https://github.com/PlatinumLucario/VGMusicStudio/ + * + * | Original Author can be found below: | + * v v + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#region Original Author +/* FlexibleMessageBox – A flexible replacement for the .NET MessageBox + * + * Author: Jörg Reichert (public@jreichert.de) + * Contributors: Thanks to: David Hall, Roink + * Version: 1.3 + * Published at: http://www.codeproject.com/Articles/601900/FlexibleMessageBox + * + ************************************************************************************************************ + * Features: + * - It can be simply used instead of MessageBox since all important static "Show"-Functions are supported + * - It is small, only one source file, which could be added easily to each solution + * - It can be resized and the content is correctly word-wrapped + * - It tries to auto-size the width to show the longest text row + * - It never exceeds the current desktop working area + * - It displays a vertical scrollbar when needed + * - It does support hyperlinks in text + * + * Because the interface is identical to MessageBox, you can add this single source file to your project + * and use the FlexibleMessageBox almost everywhere you use a standard MessageBox. + * The goal was NOT to produce as many features as possible but to provide a simple replacement to fit my + * own needs. Feel free to add additional features on your own, but please left my credits in this class. + * + ************************************************************************************************************ + * Usage examples: + * + * FlexibleMessageBox.Show("Just a text"); + * + * FlexibleMessageBox.Show("A text", + * "A caption"); + * + * FlexibleMessageBox.Show("Some text with a link: www.google.com", + * "Some caption", + * MessageBoxButtons.AbortRetryIgnore, + * MessageBoxIcon.Information, + * MessageBoxDefaultButton.Button2); + * + * var dialogResult = FlexibleMessageBox.Show("Do you know the answer to life the universe and everything?", + * "One short question", + * MessageBoxButtons.YesNo); + * + ************************************************************************************************************ + * THE SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS", WITHOUT WARRANTY + * OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL THE AUTHOR BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF THIS + * SOFTWARE. + * + ************************************************************************************************************ + * History: + * Version 1.3 - 19.Dezember 2014 + * - Added refactoring function GetButtonText() + * - Used CurrentUICulture instead of InstalledUICulture + * - Added more button localizations. Supported languages are now: ENGLISH, GERMAN, SPANISH, ITALIAN + * - Added standard MessageBox handling for "copy to clipboard" with + and + + * - Tab handling is now corrected (only tabbing over the visible buttons) + * - Added standard MessageBox handling for ALT-Keyboard shortcuts + * - SetDialogSizes: Refactored completely: Corrected sizing and added caption driven sizing + * + * Version 1.2 - 10.August 2013 + * - Do not ShowInTaskbar anymore (original MessageBox is also hidden in taskbar) + * - Added handling for Escape-Button + * - Adapted top right close button (red X) to behave like MessageBox (but hidden instead of deactivated) + * + * Version 1.1 - 14.June 2013 + * - Some Refactoring + * - Added internal form class + * - Added missing code comments, etc. + * + * Version 1.0 - 15.April 2013 + * - Initial Version + */ +#endregion + +internal class FlexibleMessageBox +{ + #region Public statics + + /// + /// Defines the maximum width for all FlexibleMessageBox instances in percent of the working area. + /// + /// Allowed values are 0.2 - 1.0 where: + /// 0.2 means: The FlexibleMessageBox can be at most half as wide as the working area. + /// 1.0 means: The FlexibleMessageBox can be as wide as the working area. + /// + /// Default is: 70% of the working area width. + /// + //public static double MAX_WIDTH_FACTOR = 0.7; + + /// + /// Defines the maximum height for all FlexibleMessageBox instances in percent of the working area. + /// + /// Allowed values are 0.2 - 1.0 where: + /// 0.2 means: The FlexibleMessageBox can be at most half as high as the working area. + /// 1.0 means: The FlexibleMessageBox can be as high as the working area. + /// + /// Default is: 90% of the working area height. + /// + //public static double MAX_HEIGHT_FACTOR = 0.9; + + /// + /// Defines the font for all FlexibleMessageBox instances. + /// + /// Default is: Theme.Font + /// + //public static Font FONT = Theme.Font; + + #endregion + + #region Public show functions + + public static Gtk.ResponseType Show(string text) + { + return FlexibleMessageBoxWindow.Show(null, text, string.Empty, Gtk.ButtonsType.Ok, Gtk.MessageType.Other, Gtk.ResponseType.Ok); + } + public static Gtk.ResponseType Show(Window owner, string text) + { + return FlexibleMessageBoxWindow.Show(owner, text, string.Empty, Gtk.ButtonsType.Ok, Gtk.MessageType.Other, Gtk.ResponseType.Ok); + } + public static Gtk.ResponseType Show(string text, string caption) + { + return FlexibleMessageBoxWindow.Show(null, text, caption, Gtk.ButtonsType.Ok, Gtk.MessageType.Other, Gtk.ResponseType.Ok); + } + public static Gtk.ResponseType Show(Exception ex, string caption) + { + return FlexibleMessageBoxWindow.Show(null, string.Format("Error Details:{1}{1}{0}{1}{2}", ex.Message, Environment.NewLine, ex.StackTrace), caption, Gtk.ButtonsType.Ok, Gtk.MessageType.Other, Gtk.ResponseType.Ok); + } + public static Gtk.ResponseType Show(Window owner, string text, string caption) + { + return FlexibleMessageBoxWindow.Show(owner, text, caption, Gtk.ButtonsType.Ok, Gtk.MessageType.Other, Gtk.ResponseType.Ok); + } + public static Gtk.ResponseType Show(string text, string caption, Gtk.ButtonsType buttons) + { + return FlexibleMessageBoxWindow.Show(null, text, caption, buttons, Gtk.MessageType.Other, Gtk.ResponseType.Ok); + } + public static Gtk.ResponseType Show(Window owner, string text, string caption, Gtk.ButtonsType buttons) + { + return FlexibleMessageBoxWindow.Show(owner, text, caption, buttons, Gtk.MessageType.Other, Gtk.ResponseType.Ok); + } + public static Gtk.ResponseType Show(string text, string caption, Gtk.ButtonsType buttons, Gtk.MessageType icon) + { + return FlexibleMessageBoxWindow.Show(null, text, caption, buttons, icon, Gtk.ResponseType.Ok); + } + public static Gtk.ResponseType Show(Window owner, string text, string caption, Gtk.ButtonsType buttons, Gtk.MessageType icon) + { + return FlexibleMessageBoxWindow.Show(owner, text, caption, buttons, icon, Gtk.ResponseType.Ok); + } + public static Gtk.ResponseType Show(string text, string caption, Gtk.ButtonsType buttons, Gtk.MessageType icon, Gtk.ResponseType defaultButton) + { + return FlexibleMessageBoxWindow.Show(null, text, caption, buttons, icon, defaultButton); + } + public static Gtk.ResponseType Show(Window owner, string text, string caption, Gtk.ButtonsType buttons, Gtk.MessageType icon, Gtk.ResponseType defaultButton) + { + return FlexibleMessageBoxWindow.Show(owner, text, caption, buttons, icon, defaultButton); + } + + #endregion + + #region Internal form classes + + internal sealed class FlexibleButton : Gtk.Button + { + public Gtk.ButtonsType ButtonsType; + public Gtk.ResponseType ResponseType; + + private FlexibleButton() + { + ResponseType = new Gtk.ResponseType(); + } + } + + internal sealed class FlexibleContentBox : Gtk.Box + { + public Gtk.Text Text; + + private FlexibleContentBox() + { + Text = Gtk.Text.New(); + } + } + + class FlexibleMessageBoxWindow : Window + { + //IContainer components = null; + + protected void Dispose(bool disposing) + { + if (disposing && richTextBoxMessage != null) + { + richTextBoxMessage.Dispose(); + } + base.Dispose(); + } + void InitializeComponent() + { + //components = new Container(); + richTextBoxMessage = (FlexibleContentBox)Gtk.Box.New(Gtk.Orientation.Vertical, 0); + button1 = (FlexibleButton)Gtk.Button.New(); + //FlexibleMessageBoxFormBindingSource = new BindingSource(components); + panel1 = (FlexibleContentBox)Gtk.Box.New(Gtk.Orientation.Vertical, 0); + pictureBoxForIcon = Gtk.Image.New(); + button2 = (FlexibleButton)Gtk.Button.New(); + button3 = (FlexibleButton)Gtk.Button.New(); + //((ISupportInitialize)FlexibleMessageBoxFormBindingSource).BeginInit(); + //panel1.SuspendLayout(); + //((ISupportInitialize)pictureBoxForIcon).BeginInit(); + //SuspendLayout(); + // + // button1 + // + //button1.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + //button1.AutoSize = true; + button1.ResponseType = Gtk.ResponseType.Ok; + //button1.Location = new Point(11, 67); + //button1.MinimumSize = new Size(0, 24); + button1.Name = "button1"; + //button1.Size = new Size(75, 24); + button1.WidthRequest = 75; + button1.HeightRequest = 24; + //button1.TabIndex = 2; + button1.Label = "OK"; + //button1.UseVisualStyleBackColor = true; + button1.Visible = false; + // + // richTextBoxMessage + // + //richTextBoxMessage.Anchor = AnchorStyles.Top | AnchorStyles.Bottom + //| AnchorStyles.Left + //| AnchorStyles.Right; + //richTextBoxMessage.BorderStyle = BorderStyle.None; + richTextBoxMessage.BindProperty("Text", FlexibleMessageBoxFormBindingSource, "MessageText", GObject.BindingFlags.Default); + //richTextBoxMessage.Font = new Font(Theme.Font.FontFamily, 9); + //richTextBoxMessage.Location = new Point(50, 26); + //richTextBoxMessage.Margin = new Padding(0); + richTextBoxMessage.Name = "richTextBoxMessage"; + //richTextBoxMessage.ReadOnly = true; + richTextBoxMessage.Text.Editable = false; + //richTextBoxMessage.ScrollBars = RichTextBoxScrollBars.Vertical; + scrollbar = Gtk.Scrollbar.New(Gtk.Orientation.Vertical, null); + scrollbar.SetParent(richTextBoxMessage); + //richTextBoxMessage.Size = new Size(200, 20); + richTextBoxMessage.WidthRequest = 200; + richTextBoxMessage.HeightRequest = 20; + //richTextBoxMessage.TabIndex = 0; + //richTextBoxMessage.TabStop = false; + richTextBoxMessage.Text.SetText(""); + //richTextBoxMessage.LinkClicked += new LinkClickedEventHandler(LinkClicked); + // + // panel1 + // + //panel1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom + //| AnchorStyles.Left + //| AnchorStyles.Right; + //panel1.Controls.Add(pictureBoxForIcon); + panel1.Append(pictureBoxForIcon); + //panel1.Controls.Add(richTextBoxMessage); + panel1.Append(richTextBoxMessage); + //panel1.Location = new Point(-3, -4); + panel1.Name = "panel1"; + //panel1.Size = new Size(268, 59); + panel1.WidthRequest = 268; + panel1.HeightRequest = 59; + //panel1.TabIndex = 1; + // + // pictureBoxForIcon + // + //pictureBoxForIcon.BackColor = Color.Transparent; + //pictureBoxForIcon.Location = new Point(15, 19); + pictureBoxForIcon.Name = "pictureBoxForIcon"; + //pictureBoxForIcon.Size = new Size(32, 32); + pictureBoxForIcon.WidthRequest = 32; + pictureBoxForIcon.HeightRequest = 32; + //pictureBoxForIcon.TabIndex = 8; + //pictureBoxForIcon.TabStop = false; + // + // button2 + // + //button2.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + button2.ResponseType = Gtk.ResponseType.Ok; + //button2.Location = new Point(92, 67); + //button2.MinimumSize = new Size(0, 24); + button2.Name = "button2"; + //button2.Size = new Size(75, 24); + button2.WidthRequest = 75; + button2.HeightRequest = 24; + //button2.TabIndex = 3; + button2.Label = "OK"; + //button2.UseVisualStyleBackColor = true; + button2.Visible = false; + // + // button3 + // + //button3.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + //button3.AutoSize = true; + button3.ResponseType = Gtk.ResponseType.Ok; + //button3.Location = new Point(173, 67); + //button3.MinimumSize = new Size(0, 24); + button3.Name = "button3"; + //button3.Size = new Size(75, 24); + button3.WidthRequest = 75; + button3.HeightRequest = 24; + //button3.TabIndex = 0; + button3.Label = "OK"; + //button3.UseVisualStyleBackColor = true; + button3.Visible = false; + // + // FlexibleMessageBoxForm + // + //AutoScaleDimensions = new SizeF(6F, 13F); + //AutoScaleMode = AutoScaleMode.Font; + //ClientSize = new Size(260, 102); + //Controls.Add(button3); + SetChild(button3); + //Controls.Add(button2); + SetChild(button2); + //Controls.Add(panel1); + SetChild(panel1); + //Controls.Add(button1); + SetChild(button1); + //DataBindings.Add(new Binding("Text", FlexibleMessageBoxFormBindingSource, "CaptionText", true)); + //Icon = Properties.Resources.Icon; + //MaximizeBox = false; + //MinimizeBox = false; + //MinimumSize = new Size(276, 140); + //Name = "FlexibleMessageBoxForm"; + //SizeGripStyle = SizeGripStyle.Show; + //StartPosition = FormStartPosition.CenterParent; + //Text = ""; + //Shown += new EventHandler(FlexibleMessageBoxForm_Shown); + //((ISupportInitialize)FlexibleMessageBoxFormBindingSource).EndInit(); + //panel1.ResumeLayout(false); + //((ISupportInitialize)pictureBoxForIcon).EndInit(); + //ResumeLayout(false); + //PerformLayout(); + } + + private FlexibleButton button1, button2, button3; + private GObject.Object FlexibleMessageBoxFormBindingSource; + private FlexibleContentBox richTextBoxMessage, panel1; + private Gtk.Scrollbar scrollbar; + private Gtk.Image pictureBoxForIcon; + + #region Private constants + + //These separators are used for the "copy to clipboard" standard operation, triggered by Ctrl + C (behavior and clipboard format is like in a standard MessageBox) + static readonly string STANDARD_MESSAGEBOX_SEPARATOR_LINES = "---------------------------\n"; + static readonly string STANDARD_MESSAGEBOX_SEPARATOR_SPACES = " "; + + //These are the possible buttons (in a standard MessageBox) + private enum ButtonID { OK = 0, CANCEL, YES, NO, ABORT, RETRY, IGNORE }; + + //These are the buttons texts for different languages. + //If you want to add a new language, add it here and in the GetButtonText-Function + private enum TwoLetterISOLanguageID { en, de, es, it }; + static readonly string[] BUTTON_TEXTS_ENGLISH_EN = { "OK", "Cancel", "&Yes", "&No", "&Abort", "&Retry", "&Ignore" }; //Note: This is also the fallback language + static readonly string[] BUTTON_TEXTS_GERMAN_DE = { "OK", "Abbrechen", "&Ja", "&Nein", "&Abbrechen", "&Wiederholen", "&Ignorieren" }; + static readonly string[] BUTTON_TEXTS_SPANISH_ES = { "Aceptar", "Cancelar", "&Sí", "&No", "&Abortar", "&Reintentar", "&Ignorar" }; + static readonly string[] BUTTON_TEXTS_ITALIAN_IT = { "OK", "Annulla", "&Sì", "&No", "&Interrompi", "&Riprova", "&Ignora" }; + + #endregion + + #region Private members + + Gtk.ResponseType defaultButton; + int visibleButtonsCount; + readonly TwoLetterISOLanguageID languageID = TwoLetterISOLanguageID.en; + + #endregion + + #region Private constructors + + private FlexibleMessageBoxWindow() + { + InitializeComponent(); + + //Try to evaluate the language. If this fails, the fallback language English will be used + Enum.TryParse(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, out languageID); + + //KeyPreview = true; + //KeyUp += FlexibleMessageBoxForm_KeyUp; + } + + #endregion + + #region Private helper functions + + static string[] GetStringRows(string message) + { + if (string.IsNullOrEmpty(message)) + { + return null; + } + + string[] messageRows = message.Split(new char[] { '\n' }, StringSplitOptions.None); + return messageRows; + } + + string GetButtonText(ButtonID buttonID) + { + int buttonTextArrayIndex = Convert.ToInt32(buttonID); + + switch (languageID) + { + case TwoLetterISOLanguageID.de: return BUTTON_TEXTS_GERMAN_DE[buttonTextArrayIndex]; + case TwoLetterISOLanguageID.es: return BUTTON_TEXTS_SPANISH_ES[buttonTextArrayIndex]; + case TwoLetterISOLanguageID.it: return BUTTON_TEXTS_ITALIAN_IT[buttonTextArrayIndex]; + + default: return BUTTON_TEXTS_ENGLISH_EN[buttonTextArrayIndex]; + } + } + + static double GetCorrectedWorkingAreaFactor(double workingAreaFactor) + { + const double MIN_FACTOR = 0.2; + const double MAX_FACTOR = 1.0; + + if (workingAreaFactor < MIN_FACTOR) + { + return MIN_FACTOR; + } + + if (workingAreaFactor > MAX_FACTOR) + { + return MAX_FACTOR; + } + + return workingAreaFactor; + } + + static void SetDialogStartPosition(FlexibleMessageBoxWindow flexibleMessageBoxForm, Window owner) + { + //If no owner given: Center on current screen + if (owner == null) + { + //var screen = Screen.FromPoint(Cursor.Position); + //flexibleMessageBoxForm.StartPosition = FormStartPosition.Manual; + //flexibleMessageBoxForm.Left = screen.Bounds.Left + screen.Bounds.Width / 2 - flexibleMessageBoxForm.Width / 2; + //flexibleMessageBoxForm.Top = screen.Bounds.Top + screen.Bounds.Height / 2 - flexibleMessageBoxForm.Height / 2; + } + } + + static void SetDialogSizes(FlexibleMessageBoxWindow flexibleMessageBoxForm, string text, string caption) + { + //First set the bounds for the maximum dialog size + //flexibleMessageBoxForm.MaximumSize = new Size(Convert.ToInt32(SystemInformation.WorkingArea.Width * GetCorrectedWorkingAreaFactor(MAX_WIDTH_FACTOR)), + // Convert.ToInt32(SystemInformation.WorkingArea.Height * GetCorrectedWorkingAreaFactor(MAX_HEIGHT_FACTOR))); + + //Get rows. Exit if there are no rows to render... + string[] stringRows = GetStringRows(text); + if (stringRows == null) + { + return; + } + + //Calculate whole text height + //int textHeight = TextRenderer.MeasureText(text, FONT).Height; + + //Calculate width for longest text line + //const int SCROLLBAR_WIDTH_OFFSET = 15; + //int longestTextRowWidth = stringRows.Max(textForRow => TextRenderer.MeasureText(textForRow, FONT).Width); + //int captionWidth = TextRenderer.MeasureText(caption, SystemFonts.CaptionFont).Width; + //int textWidth = Math.Max(longestTextRowWidth + SCROLLBAR_WIDTH_OFFSET, captionWidth); + + //Calculate margins + int marginWidth = flexibleMessageBoxForm.WidthRequest - flexibleMessageBoxForm.richTextBoxMessage.WidthRequest; + int marginHeight = flexibleMessageBoxForm.HeightRequest - flexibleMessageBoxForm.richTextBoxMessage.HeightRequest; + + //Set calculated dialog size (if the calculated values exceed the maximums, they were cut by windows forms automatically) + //flexibleMessageBoxForm.Size = new Size(textWidth + marginWidth, + // textHeight + marginHeight); + } + + static void SetDialogIcon(FlexibleMessageBoxWindow flexibleMessageBoxForm, Gtk.MessageType icon) + { + switch (icon) + { + case Gtk.MessageType.Info: + flexibleMessageBoxForm.pictureBoxForIcon.SetFromIconName("dialog-information-symbolic"); + break; + case Gtk.MessageType.Warning: + flexibleMessageBoxForm.pictureBoxForIcon.SetFromIconName("dialog-warning-symbolic"); + break; + case Gtk.MessageType.Error: + flexibleMessageBoxForm.pictureBoxForIcon.SetFromIconName("dialog-error-symbolic"); + break; + case Gtk.MessageType.Question: + flexibleMessageBoxForm.pictureBoxForIcon.SetFromIconName("dialog-question-symbolic"); + break; + default: + //When no icon is used: Correct placement and width of rich text box. + flexibleMessageBoxForm.pictureBoxForIcon.Visible = false; + //flexibleMessageBoxForm.richTextBoxMessage.Left -= flexibleMessageBoxForm.pictureBoxForIcon.Width; + //flexibleMessageBoxForm.richTextBoxMessage.Width += flexibleMessageBoxForm.pictureBoxForIcon.Width; + break; + } + } + + static void SetDialogButtons(FlexibleMessageBoxWindow flexibleMessageBoxForm, Gtk.ButtonsType buttons, Gtk.ResponseType defaultButton) + { + //Set the buttons visibilities and texts + switch (buttons) + { + case 0: + flexibleMessageBoxForm.visibleButtonsCount = 3; + + flexibleMessageBoxForm.button1.Visible = true; + flexibleMessageBoxForm.button1.Label = flexibleMessageBoxForm.GetButtonText(ButtonID.ABORT); + flexibleMessageBoxForm.button1.ResponseType = Gtk.ResponseType.Reject; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Label = flexibleMessageBoxForm.GetButtonText(ButtonID.RETRY); + flexibleMessageBoxForm.button2.ResponseType = Gtk.ResponseType.Ok; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Label = flexibleMessageBoxForm.GetButtonText(ButtonID.IGNORE); + flexibleMessageBoxForm.button3.ResponseType = Gtk.ResponseType.Cancel; + + //flexibleMessageBoxForm.ControlBox = false; + break; + + case (Gtk.ButtonsType)1: + flexibleMessageBoxForm.visibleButtonsCount = 2; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Label = flexibleMessageBoxForm.GetButtonText(ButtonID.OK); + flexibleMessageBoxForm.button2.ResponseType = Gtk.ResponseType.Ok; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Label = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); + flexibleMessageBoxForm.button3.ResponseType = Gtk.ResponseType.Cancel; + + //flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + + case (Gtk.ButtonsType)2: + flexibleMessageBoxForm.visibleButtonsCount = 2; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Label = flexibleMessageBoxForm.GetButtonText(ButtonID.RETRY); + flexibleMessageBoxForm.button2.ResponseType = Gtk.ResponseType.Ok; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Label = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); + flexibleMessageBoxForm.button3.ResponseType = Gtk.ResponseType.Cancel; + + //flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + + case (Gtk.ButtonsType)3: + flexibleMessageBoxForm.visibleButtonsCount = 2; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Label = flexibleMessageBoxForm.GetButtonText(ButtonID.YES); + flexibleMessageBoxForm.button2.ResponseType = Gtk.ResponseType.Yes; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Label = flexibleMessageBoxForm.GetButtonText(ButtonID.NO); + flexibleMessageBoxForm.button3.ResponseType = Gtk.ResponseType.No; + + //flexibleMessageBoxForm.ControlBox = false; + break; + + case (Gtk.ButtonsType)4: + flexibleMessageBoxForm.visibleButtonsCount = 3; + + flexibleMessageBoxForm.button1.Visible = true; + flexibleMessageBoxForm.button1.Label = flexibleMessageBoxForm.GetButtonText(ButtonID.YES); + flexibleMessageBoxForm.button1.ResponseType = Gtk.ResponseType.Yes; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Label = flexibleMessageBoxForm.GetButtonText(ButtonID.NO); + flexibleMessageBoxForm.button2.ResponseType = Gtk.ResponseType.No; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Label = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); + flexibleMessageBoxForm.button3.ResponseType = Gtk.ResponseType.Cancel; + + //flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + + case (Gtk.ButtonsType)5: + default: + flexibleMessageBoxForm.visibleButtonsCount = 1; + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Label = flexibleMessageBoxForm.GetButtonText(ButtonID.OK); + flexibleMessageBoxForm.button3.ResponseType = Gtk.ResponseType.Ok; + + //flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + } + + //Set default button (used in FlexibleMessageBoxWindow_Shown) + flexibleMessageBoxForm.defaultButton = defaultButton; + } + + #endregion + + #region Private event handlers + + void FlexibleMessageBoxWindow_Shown(object sender, EventArgs e) + { + int buttonIndexToFocus = 1; + Gtk.Widget buttonToFocus; + + //Set the default button... + //switch (defaultButton) + //{ + // case MessageBoxDefaultButton.Button1: + // default: + // buttonIndexToFocus = 1; + // break; + // case MessageBoxDefaultButton.Button2: + // buttonIndexToFocus = 2; + // break; + // case MessageBoxDefaultButton.Button3: + // buttonIndexToFocus = 3; + // break; + //} + + if (buttonIndexToFocus > visibleButtonsCount) + { + buttonIndexToFocus = visibleButtonsCount; + } + + if (buttonIndexToFocus == 3) + { + buttonToFocus = button3; + } + else if (buttonIndexToFocus == 2) + { + buttonToFocus = button2; + } + else + { + buttonToFocus = button1; + } + + buttonToFocus.IsFocus(); + } + + //void LinkClicked(object sender, LinkClickedEventArgs e) + //{ + // try + // { + // Cursor.Current = Cursors.WaitCursor; + // Process.Start(e.LinkText); + // } + // catch (Exception) + // { + // //Let the caller of FlexibleMessageBoxWindow decide what to do with this exception... + // throw; + // } + // finally + // { + // Cursor.Current = Cursors.Default; + // } + //} + + //void FlexibleMessageBoxWindow_KeyUp(object sender, KeyEventArgs e) + //{ + // //Handle standard key strikes for clipboard copy: "Ctrl + C" and "Ctrl + Insert" + // if (e.Control && (e.KeyCode == Keys.C || e.KeyCode == Keys.Insert)) + // { + // string buttonsTextLine = (button1.Visible ? button1.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty) + // + (button2.Visible ? button2.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty) + // + (button3.Visible ? button3.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty); + + // //Build same clipboard text like the standard .Net MessageBox + // string textForClipboard = STANDARD_MESSAGEBOX_SEPARATOR_LINES + // + Text + Environment.NewLine + // + STANDARD_MESSAGEBOX_SEPARATOR_LINES + // + richTextBoxMessage.Text + Environment.NewLine + // + STANDARD_MESSAGEBOX_SEPARATOR_LINES + // + buttonsTextLine.Replace("&", string.Empty) + Environment.NewLine + // + STANDARD_MESSAGEBOX_SEPARATOR_LINES; + + // //Set text in clipboard + // Clipboard.SetText(textForClipboard); + // } + //} + + #endregion + + #region Properties (only used for binding) + + public string CaptionText { get; set; } + public string MessageText { get; set; } + + #endregion + + #region Public show function + + public static Gtk.ResponseType Show(Window owner, string text, string caption, Gtk.ButtonsType buttons, Gtk.MessageType icon, Gtk.ResponseType defaultButton) + { + //Create a new instance of the FlexibleMessageBox form + var flexibleMessageBoxForm = new FlexibleMessageBoxWindow + { + //ShowInTaskbar = false, + + //Bind the caption and the message text + CaptionText = caption, + MessageText = text + }; + //flexibleMessageBoxForm.FlexibleMessageBoxWindowBindingSource.DataSource = flexibleMessageBoxForm; + + //Set the buttons visibilities and texts. Also set a default button. + SetDialogButtons(flexibleMessageBoxForm, buttons, defaultButton); + + //Set the dialogs icon. When no icon is used: Correct placement and width of rich text box. + SetDialogIcon(flexibleMessageBoxForm, icon); + + //Set the font for all controls + //flexibleMessageBoxForm.Font = FONT; + //flexibleMessageBoxForm.richTextBoxMessage.Font = FONT; + + //Calculate the dialogs start size (Try to auto-size width to show longest text row). Also set the maximum dialog size. + SetDialogSizes(flexibleMessageBoxForm, text, caption); + + //Set the dialogs start position when given. Otherwise center the dialog on the current screen. + SetDialogStartPosition(flexibleMessageBoxForm, owner); + + //Show the dialog + return Show(owner, text, caption, buttons, icon, defaultButton); + } + + #endregion + } //class FlexibleMessageBoxForm + + #endregion +} diff --git a/VG Music Studio - GTK4/Util/GTK4Utils.cs b/VG Music Studio - GTK4/Util/GTK4Utils.cs new file mode 100644 index 0000000..c45cccf --- /dev/null +++ b/VG Music Studio - GTK4/Util/GTK4Utils.cs @@ -0,0 +1,213 @@ +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; } + + + + 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]); + } + } + else + { + fileFilter.AddPattern(fn[1]); + } + } + } + else + { + fileFilter.SetName(filterName); + for (int i = 0; i < fileExtensions.Length; i++) + { + fileFilter.AddPattern(fileExtensions[i]); + } + } + } + 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, object parent = null!) => + new GTK4Utils().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, parent!); + public override string CreateLoadDialog(string title, string filterName, Span fileExtensions, bool isFile, bool allowAllFiles, object? parent) + { + if (isFile) + { + var ff = FileFilter.New(); + Convert(filterName, fileExtensions, ff); + + var d = FileDialog.New(); + d.SetTitle(title); + var filters = Gio.ListStore.New(FileFilter.GetGType()); + filters.Append(ff); + if (allowAllFiles) + { + var allFiles = FileFilter.New(); + allFiles.SetName(Strings.FilterAllFiles); + allFiles.AddPattern("*.*"); + filters.Append(allFiles); + } + d.SetFilters(filters); + string? path = null; + _openCallback = (source, res, data) => + { + var errorHandle = new GLib.Internal.ErrorOwnedHandle(IntPtr.Zero); + var fileHandle = Gtk.Internal.FileDialog.OpenFinish(d.Handle, res, out errorHandle); + if (fileHandle != IntPtr.Zero) + { + path = Marshal.PtrToStringUTF8(Gio.Internal.File.GetPath(fileHandle).DangerousGetHandle()); + } + d.Unref(); + }; + if (path != null) + { + d.Unref(); + return path; + } + if (parent == Adw.Window.New()) + { + var p = (Adw.Window)parent; + // 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 + Gtk.Internal.FileDialog.Open(d.Handle, p.Handle, IntPtr.Zero, _openCallback, IntPtr.Zero); + } + else if (parent == Gtk.Window.New()) + { + var p = (Gtk.Window)parent; + Gtk.Internal.FileDialog.Open(d.Handle, p.Handle, IntPtr.Zero, _openCallback, IntPtr.Zero); + } + else + { + var p = MainWindow.Instance; + Gtk.Internal.FileDialog.Open(d.Handle, p!.Handle, IntPtr.Zero, _openCallback, IntPtr.Zero); + } + //d.Open(Handle, IntPtr.Zero, _openCallback, IntPtr.Zero); + return null!; + } + else + { + var d = FileDialog.New(); + d.SetTitle(title); + + string? path = null; + _selectFolderCallback = (source, res, data) => + { + var errorHandle = new GLib.Internal.ErrorOwnedHandle(IntPtr.Zero); + var folderHandle = Gtk.Internal.FileDialog.SelectFolderFinish(d.Handle, res, out errorHandle); + if (folderHandle != IntPtr.Zero) + { + var path = Marshal.PtrToStringUTF8(Gio.Internal.File.GetPath(folderHandle).DangerousGetHandle()); + } + d.Unref(); + }; + if (path != null) + { + d.Unref(); + return path; + } + if (parent == Adw.Window.New()) + { + var p = (Adw.Window)parent; + Gtk.Internal.FileDialog.SelectFolder(d.Handle, p.Handle, IntPtr.Zero, _selectFolderCallback, IntPtr.Zero); + } + else if (parent == Gtk.Window.New()) + { + var p = (Gtk.Window)parent; + Gtk.Internal.FileDialog.SelectFolder(d.Handle, p.Handle, IntPtr.Zero, _selectFolderCallback, IntPtr.Zero); + } + else + { + var p = Gtk.Window.New(); + Gtk.Internal.FileDialog.SelectFolder(d.Handle, p.Handle, IntPtr.Zero, _selectFolderCallback, IntPtr.Zero); + } + return path!; + } + } + + public static string CreateSaveDialog(string fileName, string extension, string title, string filter, object parent = null!) => + new GTK4Utils().CreateSaveDialog(fileName, title, filter, [extension], false, false, parent); + public static string CreateSaveDialog(string fileName, Span extensions, string title, string filter, object parent = null!) => + new GTK4Utils().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) + { + var ff = FileFilter.New(); + Convert(filterName, fileExtensions, ff); + + var d = FileDialog.New(); + d.SetTitle(title); + d.SetInitialName(fileName); + var filters = Gio.ListStore.New(FileFilter.GetGType()); + filters.Append(ff); + if (allowAllFiles) + { + var allFiles = FileFilter.New(); + allFiles.SetName(Strings.FilterAllFiles); + allFiles.AddPattern("*.*"); + filters.Append(allFiles); + } + d.SetFilters(filters); + string? path = null; + _saveCallback = (source, res, data) => + { + var errorHandle = new GLib.Internal.ErrorOwnedHandle(IntPtr.Zero); + var fileHandle = Gtk.Internal.FileDialog.SaveFinish(d.Handle, res, out errorHandle); + if (fileHandle != IntPtr.Zero) + { + path = Marshal.PtrToStringUTF8(Gio.Internal.File.GetPath(fileHandle).DangerousGetHandle()); + } + d.Unref(); + }; + if (path != null) + { + d.Unref(); + return path; + } + if (parent == Adw.Window.New()) + { + var p = (Adw.Window)parent; + Gtk.Internal.FileDialog.Save(d.Handle, p.Handle, IntPtr.Zero, _saveCallback, IntPtr.Zero); + } + else if (parent == Gtk.Window.New()) + { + var p = (Gtk.Window)parent; + Gtk.Internal.FileDialog.Save(d.Handle, p.Handle, IntPtr.Zero, _saveCallback, IntPtr.Zero); + } + else + { + var p = Gtk.Window.New(); + Gtk.Internal.FileDialog.Save(d.Handle, p.Handle, IntPtr.Zero, _saveCallback, IntPtr.Zero); + } + //d.Open(Handle, IntPtr.Zero, _openCallback, IntPtr.Zero); + return null!; + } +} diff --git a/VG Music Studio - GTK4/Util/ScaleControl.cs b/VG Music Studio - GTK4/Util/ScaleControl.cs new file mode 100644 index 0000000..6d21dc2 --- /dev/null +++ b/VG Music Studio - GTK4/Util/ScaleControl.cs @@ -0,0 +1,86 @@ +/* + * Modified by Davin Ockerby (Platinum Lucario) for use with GTK4 + * and VG Music Studio. Originally made by Fabrice Lacharme for use + * on WinForms. Modified since 2023-08-04 at 00:32. + */ + +#region Original License + +/* Copyright (c) 2017 Fabrice Lacharme + * This code is inspired from Michal Brylka + * https://www.codeproject.com/Articles/17395/Owner-drawn-trackbar-slider + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + + +using Gtk; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Kermalis.VGMusicStudio.GTK4.Util; + +internal class ScaleControl : Adjustment +{ + internal Adjustment Instance { get; } + + internal ScaleControl(double value, double lower, double upper, double stepIncrement, double pageIncrement, double pageSize) + { + Instance = New(value, lower, upper, stepIncrement, pageIncrement, pageSize); + } + + private double _smallChange = 1L; + public double SmallChange + { + get => _smallChange; + set + { + if (value >= 0) + { + _smallChange = value; + } + else + { + throw new ArgumentOutOfRangeException(nameof(SmallChange), $"{nameof(SmallChange)} must be greater than or equal to 0."); + } + } + } + private double _largeChange = 5L; + public double LargeChange + { + get => _largeChange; + set + { + if (value >= 0) + { + _largeChange = value; + } + else + { + throw new ArgumentOutOfRangeException(nameof(LargeChange), $"{nameof(LargeChange)} must be greater than or equal to 0."); + } + } + } + +} diff --git a/VG Music Studio - GTK4/Util/SoundSequenceList.cs b/VG Music Studio - GTK4/Util/SoundSequenceList.cs new file mode 100644 index 0000000..6077bf9 --- /dev/null +++ b/VG Music Studio - GTK4/Util/SoundSequenceList.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Gtk; +using Kermalis.VGMusicStudio.Core; +using Pango; + +namespace Kermalis.VGMusicStudio.GTK4.Util; + +internal class SoundSequenceList : Widget, IDisposable +{ + internal static ListItem? ListItem { get; set; } + internal static long? Index { get; set; } + internal static new string? Name { get; set; } + internal static List? Songs { get; set; } + //internal SingleSelection Selection { get; set; } + //private SignalListItemFactory Factory; + + internal SoundSequenceList() + { + var box = Box.New(Orientation.Horizontal, 0); + var label = Label.New(""); + label.SetWidthChars(2); + label.SetHexpand(true); + box.Append(label); + + var sw = ScrolledWindow.New(); + sw.SetPropagateNaturalWidth(true); + var listView = Create(label); + sw.SetChild(listView); + box.Prepend(sw); + } + + private static void SetupLabel(SignalListItemFactory factory, EventArgs e) + { + var label = Label.New(""); + label.SetXalign(0); + ListItem!.SetChild(label); + //e.Equals(label); + } + private static void BindName(SignalListItemFactory factory, EventArgs e) + { + var label = ListItem!.GetChild(); + var item = ListItem!.GetItem(); + var name = item.Equals(Name); + + label!.SetName(name.ToString()); + } + + private static Widget Create(object item) + { + if (item is Config.Song song) + { + Index = song.Index; + Name = song.Name; + } + else if (item is Config.Playlist playlist) + { + Songs = playlist.Songs; + Name = playlist.Name; + } + var model = Gio.ListStore.New(ColumnView.GetGType()); + + var selection = SingleSelection.New(model); + selection.SetAutoselect(true); + selection.SetCanUnselect(false); + + + var cv = ColumnView.New(selection); + cv.SetShowColumnSeparators(true); + cv.SetShowRowSeparators(true); + + var factory = SignalListItemFactory.New(); + factory.OnSetup += SetupLabel; + factory.OnBind += BindName; + var column = ColumnViewColumn.New("Name", factory); + column.SetResizable(true); + cv.AppendColumn(column); + column.Unref(); + + return cv; + } + + internal int Add(object item) + { + return Add(item); + } + internal int AddRange(Span items) + { + foreach (object item in items) + { + Create(item); + } + return AddRange(items); + } + + //internal SignalListItemFactory Items + //{ + // get + // { + // if (Factory is null) + // { + // Factory = SignalListItemFactory.New(); + // } + + // return Factory; + // } + //} + + internal object SelectedItem + { + get + { + int index = (int)Index!; + return (index == -1) ? null : ListItem.Item.Equals(index); + } + set + { + int x = -1; + + if (ListItem is not null) + { + // + if (value is not null) + { + x = ListItem.GetPosition().CompareTo(value); + } + else + { + Index = -1; + } + } + + if (x != -1) + { + Index = x; + } + } + } +} + +internal class SoundSequenceListItem +{ + internal object Item { get; } + internal SoundSequenceListItem(object item) + { + Item = item; + } + + public override string ToString() + { + return Item.ToString(); + } +} 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 0000000..08aa569 --- /dev/null +++ b/VG Music Studio - GTK4/VG Music Studio - GTK4.csproj @@ -0,0 +1,545 @@ + + + + Exe + net8.0 + enable + true + + + + + + + + + + %(Filename)%(Extension) + + + + + + + + + + Always + %(Filename)%(Extension) + + + Always + ../../../share/glib-2.0/%(RecursiveDir)/%(Filename)%(Extension) + + + Always + ../../../share/glib-2.0/%(RecursiveDir)/%(Filename)%(Extension) + + + Always + ..\..\..\share\glib-2.0\%(RecursiveDir)\%(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + Always + %(Filename)%(Extension) + + + + diff --git a/VG Music Studio - GTK4/mainwindow.xml b/VG Music Studio - GTK4/mainwindow.xml new file mode 100644 index 0000000..0488032 --- /dev/null +++ b/VG Music Studio - GTK4/mainwindow.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + vertical + + + + + Open + + + + + + + + + + + + + 60 + + + vertical + 15 + + + 175 + + + media-playback-start + False + + + + + media-playback-pause + False + + + + + media-playback-stop + False + + + + + + + + + 10.0 + 1.0 + 100.0 + + + False + + + + + + + vertical + 15 + 175 + + + + + 1.0 + 1.0 + 1.0 + -1.0 + + + 1.0 + False + False + + + + + + + 10.0 + 1.0 + 100.0 + + + False + + + + + + + + + + + +
+ + + + +
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ diff --git a/VG Music Studio - WinForms/MainForm.cs b/VG Music Studio - WinForms/MainForm.cs index 8820c08..970ff26 100644 --- a/VG Music Studio - WinForms/MainForm.cs +++ b/VG Music Studio - WinForms/MainForm.cs @@ -218,7 +218,7 @@ 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: @@ -737,7 +737,7 @@ private void Timer_Tick(object? sender, EventArgs e) } private void Mixer_VolumeChanged(float volume) { - _volumeBar.ValueChanged -= VolumeBar_ValueChanged; + //_volumeBar.ValueChanged -= VolumeBar_ValueChanged; _volumeBar.Value = (int)(volume * _volumeBar.Maximum); _volumeBar.ValueChanged += VolumeBar_ValueChanged; } diff --git a/VG Music Studio - WinForms/PlayingPlaylist.cs b/VG Music Studio - WinForms/PlayingPlaylist.cs index 100f88f..2719753 100644 --- a/VG Music Studio - WinForms/PlayingPlaylist.cs +++ b/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/SongInfoControl.cs b/VG Music Studio - WinForms/SongInfoControl.cs index 36cfe49..f177d83 100644 --- a/VG Music Studio - WinForms/SongInfoControl.cs +++ b/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); diff --git a/VG Music Studio - WinForms/Util/WinFormsUtils.cs b/VG Music Studio - WinForms/Util/WinFormsUtils.cs index 33a0766..9b79cc4 100644 --- a/VG Music Studio - WinForms/Util/WinFormsUtils.cs +++ b/VG Music Studio - WinForms/Util/WinFormsUtils.cs @@ -1,76 +1,140 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; +using Kermalis.VGMusicStudio.Core.Properties; +using Kermalis.VGMusicStudio.Core.Util; +using System; using System.Windows.Forms; namespace Kermalis.VGMusicStudio.WinForms.Util; -internal static class WinFormsUtils +internal class WinFormsUtils : DialogUtils { - private static readonly Random _rng = new(); + 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 Print(this IEnumerable source, bool parenthesis = true) - { - string str = parenthesis ? "( " : ""; - str += string.Join(", ", source); - str += parenthesis ? " )" : ""; - return str; - } - /// Fisher-Yates Shuffle - public static void Shuffle(this IList source) - { - for (int a = 0; a < source.Count - 1; a++) - { - int b = _rng.Next(a, source.Count); - (source[b], source[a]) = (source[a], source[b]); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static float Lerp(float progress, float from, float to) - { - return from + ((to - from) * progress); - } - /// Maps a value in the range [a1, a2] to [b1, b2]. Divide by zero occurs if a1 and a2 are equal - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static float Lerp(float value, float a1, float a2, float b1, float b2) - { - return b1 + ((value - a1) / (a2 - a1) * (b2 - b1)); - } - - public static string? CreateLoadDialog(string extension, string title, string filter) - { - var d = new OpenFileDialog - { - DefaultExt = extension, - ValidateNames = true, - CheckFileExists = true, - CheckPathExists = true, - Title = title, - Filter = $"{filter}|All files (*.*)|*.*", - }; - if (d.ShowDialog() == DialogResult.OK) - { - return d.FileName; - } - return null; - } - public static string? CreateSaveDialog(string fileName, string extension, string title, string filter) - { - var d = new SaveFileDialog - { - FileName = fileName, - DefaultExt = extension, - AddExtension = true, - ValidateNames = true, - CheckPathExists = true, - Title = title, - Filter = $"{filter}|All files (*.*)|*.*", - }; - if (d.ShowDialog() == DialogResult.OK) - { - return d.FileName; - } - return null; - } + 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/VG Music Studio - WinForms.csproj b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj index cd3552b..5a159a1 100644 --- a/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj +++ b/VG Music Studio - WinForms/VG Music Studio - WinForms.csproj @@ -1,7 +1,7 @@  - net7.0-windows + net8.0-windows WinExe latest Kermalis.VGMusicStudio.WinForms @@ -21,7 +21,7 @@ - + diff --git a/VG Music Studio.sln b/VG Music Studio.sln index 31bb2a2..6d9df17 100644 --- a/VG Music Studio.sln +++ b/VG Music Studio.sln @@ -7,20 +7,68 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VG Music Studio - WinForms" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VG Music Studio - Core", "VG Music Studio - Core\VG Music Studio - Core.csproj", "{5DC1E437-AEA1-4C0E-A57F-09D3DC9F4E7D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VG Music Studio - GTK3", "VG Music Studio - GTK3\VG Music Studio - GTK3.csproj", "{A9471061-10D2-41AE-86C9-1D927D7B33B8}" +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 Release|Any CPU = Release|Any CPU + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 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}.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 {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}.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 + {A9471061-10D2-41AE-86C9-1D927D7B33B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9471061-10D2-41AE-86C9-1D927D7B33B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9471061-10D2-41AE-86C9-1D927D7B33B8}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {A9471061-10D2-41AE-86C9-1D927D7B33B8}.Debug|ARM64.Build.0 = Debug|Any CPU + {A9471061-10D2-41AE-86C9-1D927D7B33B8}.Debug|x64.ActiveCfg = Debug|Any CPU + {A9471061-10D2-41AE-86C9-1D927D7B33B8}.Debug|x64.Build.0 = Debug|Any CPU + {A9471061-10D2-41AE-86C9-1D927D7B33B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9471061-10D2-41AE-86C9-1D927D7B33B8}.Release|Any CPU.Build.0 = Release|Any CPU + {A9471061-10D2-41AE-86C9-1D927D7B33B8}.Release|ARM64.ActiveCfg = Release|Any CPU + {A9471061-10D2-41AE-86C9-1D927D7B33B8}.Release|ARM64.Build.0 = Release|Any CPU + {A9471061-10D2-41AE-86C9-1D927D7B33B8}.Release|x64.ActiveCfg = Release|Any CPU + {A9471061-10D2-41AE-86C9-1D927D7B33B8}.Release|x64.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}.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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE