diff --git a/.github/workflows/apt-deps.txt b/.github/workflows/apt-deps.txt index 26e920d174..7fbbe6b024 100644 --- a/.github/workflows/apt-deps.txt +++ b/.github/workflows/apt-deps.txt @@ -1 +1 @@ -libusb-dev libusb-1.0-0-dev libsdl2-dev libsdl2-net-dev libpng-dev libglew-dev nlohmann-json3-dev libtinyxml2-dev libspdlog-dev ninja-build +libusb-dev libusb-1.0-0-dev libsdl2-dev libsdl2-net-dev libpng-dev libglew-dev nlohmann-json3-dev libtinyxml2-dev libspdlog-dev ninja-build libogg-dev libopus-dev opus-tools libopusfile-dev libvorbis-dev diff --git a/.github/workflows/macports-deps.txt b/.github/workflows/macports-deps.txt index 9a3c7b9c02..db16dc0526 100644 --- a/.github/workflows/macports-deps.txt +++ b/.github/workflows/macports-deps.txt @@ -1 +1 @@ -libsdl2 +universal libsdl2_net +universal libpng +universal glew +universal libzip +universal nlohmann-json +universal tinyxml2 +universal +libsdl2 +universal libsdl2_net +universal libpng +universal glew +universal libzip +universal nlohmann-json +universal tinyxml2 +universal libogg +universal libopus +universal opusfile +universal libvorbis +universal diff --git a/.gitmodules b/.gitmodules index 357af3d88d..cf6a7fd78d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,4 +6,4 @@ url = https://github.com/HarbourMasters/OTRExporter.git [submodule "ZAPDTR"] path = ZAPDTR - url = https://github.com/louist103/ZAPDTR.git + url = https://github.com/louist103/ZAPDTR.git \ No newline at end of file diff --git a/CMake/FindOgg.cmake b/CMake/FindOgg.cmake new file mode 100644 index 0000000000..9cf5ce4309 --- /dev/null +++ b/CMake/FindOgg.cmake @@ -0,0 +1,61 @@ +# - Find ogg +# Find the native ogg includes and libraries +# +# OGG_INCLUDE_DIRS - where to find ogg.h, etc. +# OGG_LIBRARIES - List of libraries when using ogg. +# OGG_FOUND - True if ogg found. + +if (OGG_INCLUDE_DIR) + # Already in cache, be silent + set(OGG_FIND_QUIETLY TRUE) +endif () + +find_package (PkgConfig QUIET) +pkg_check_modules (PC_OGG QUIET ogg>=1.3.0) + +set (OGG_VERSION ${PC_OGG_VERSION}) + +find_path (OGG_INCLUDE_DIR ogg/ogg.h + HINTS + ${PC_OGG_INCLUDEDIR} + ${PC_OGG_INCLUDE_DIRS} + ${OGG_ROOT} + ) +# MSVC built ogg may be named ogg_static. +# The provided project files name the library with the lib prefix. +find_library (OGG_LIBRARY + NAMES + ogg + ogg_static + libogg + libogg_static + HINTS + ${PC_OGG_LIBDIR} + ${PC_OGG_LIBRARY_DIRS} + ${OGG_ROOT} + ) +# Handle the QUIETLY and REQUIRED arguments and set OGG_FOUND +# to TRUE if all listed variables are TRUE. +include (FindPackageHandleStandardArgs) +find_package_handle_standard_args (Ogg + REQUIRED_VARS + OGG_LIBRARY + OGG_INCLUDE_DIR + VERSION_VAR + OGG_VERSION + ) + +if (OGG_FOUND) + set (OGG_LIBRARIES ${OGG_LIBRARY}) + set (OGG_INCLUDE_DIRS ${OGG_INCLUDE_DIR}) + + if(NOT TARGET Ogg::ogg) + add_library(Ogg::ogg UNKNOWN IMPORTED) + set_target_properties(Ogg::ogg PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${OGG_INCLUDE_DIRS}" + IMPORTED_LOCATION "${OGG_LIBRARIES}" + ) + endif () +endif () + +mark_as_advanced (OGG_INCLUDE_DIR OGG_LIBRARY) diff --git a/CMake/FindOpus.cmake b/CMake/FindOpus.cmake new file mode 100644 index 0000000000..1e6e30d680 --- /dev/null +++ b/CMake/FindOpus.cmake @@ -0,0 +1,44 @@ +# - FindOpus.cmake +# Find the native opus includes and libraries +# +# OPUS_INCLUDE_DIRS - where to find opus/opus.h, etc. +# OPUS_LIBRARIES - List of libraries when using libopus(file). +# OPUS_FOUND - True if libopus found. + +if(OPUS_INCLUDE_DIR AND OPUS_LIBRARY AND OPUSFILE_LIBRARY) + # Already in cache, be silent + set(OPUS_FIND_QUIETLY TRUE) +endif(OPUS_INCLUDE_DIR AND OPUS_LIBRARY AND OPUSFILE_LIBRARY) + +find_path(OPUS_INCLUDE_DIR + NAMES opusfile.h + PATH_SUFFIXES opus +) + +# MSVC built opus may be named opus_static +# The provided project files name the library with the lib prefix. +find_library(OPUS_LIBRARY + NAMES opus opus_static libopus libopus_static +) +#find_library(OPUSFILE_LIBRARY +# NAMES opusfile opusfile_static libopusfile libopusfile_static +#) + +# Handle the QUIETLY and REQUIRED arguments and set OPUS_FOUND +# to TRUE if all listed variables are TRUE. +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Opus DEFAULT_MSG + OPUS_LIBRARY OPUS_INCLUDE_DIR +) + +if(OPUS_FOUND) + set(OPUS_LIBRARIES ${OPUS_LIBRARY}) + set(OPUS_INCLUDE_DIRS ${OPUS_INCLUDE_DIR}) + if(NOT TARGET Opus::opus) + add_library(Opus::opus UNKNOWN IMPORTED) + set_target_properties(Opus::opus PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${OPUS_INCLUDE_DIRS}" + IMPORTED_LOCATION "${OPUS_LIBRARIES}" + ) + endif() +endif(OPUS_FOUND) diff --git a/CMake/FindOpusFile.cmake b/CMake/FindOpusFile.cmake new file mode 100644 index 0000000000..26344becb2 --- /dev/null +++ b/CMake/FindOpusFile.cmake @@ -0,0 +1,55 @@ +# FindOpusFile.cmake +# Locate the libopusfile library and its dependencies (libopus and libogg). +# Defines the following variables on success: +# OPUSFILE_FOUND - Indicates if opusfile was found +# OPUSFILE_INCLUDE_DIR - Directory containing opusfile.h +# OPUSFILE_LIBRARY - Path to the opusfile library +# OPUSFILE_LIBRARIES - Full list of libraries to link (opusfile, opus, ogg) + +# Search for the OpusFile header +find_path(OPUSFILE_INCLUDE_DIR + NAMES opusfile.h + PATHS /usr/include/opus /usr/local/include/opus /opt/local/include/opus + DOC "Directory where opusfile.h is located" +) + +# Search for the OpusFile library +find_library(OPUSFILE_LIBRARY + NAMES opusfile + DOC "Path to the libopusfile library" +) + +# Search for the Opus library (dependency of OpusFile) +find_library(OPUS_LIBRARY + NAMES opus + DOC "Path to the libopus library (dependency of libopusfile)" +) + +# Search for the Ogg library (dependency of OpusFile) +find_library(OGG_LIBRARY + NAMES ogg + DOC "Path to the libogg library (dependency of libopusfile)" +) + +# Check if all required components are found +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(OpusFile + REQUIRED_VARS OPUSFILE_LIBRARY OPUSFILE_INCLUDE_DIR OPUS_LIBRARY OGG_LIBRARY + VERSION_VAR OPUSFILE_VERSION +) + +# Define an imported target if everything is found +if (OPUSFILE_FOUND) + add_library(Opusfile::Opusfile INTERFACE IMPORTED) + + set_target_properties(Opusfile::Opusfile PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${OPUSFILE_INCLUDE_DIR}" + INTERFACE_LINK_LIBRARIES "${OPUSFILE_LIBRARY};${OPUS_LIBRARY};${OGG_LIBRARY}" + ) + + # Optionally expose the include and libraries separately + set(OPUSFILE_LIBRARIES ${OPUSFILE_LIBRARY} ${OPUS_LIBRARY} ${OGG_LIBRARY}) + set(OPUSFILE_INCLUDE_DIRS ${OPUSFILE_INCLUDE_DIR}) +else() + set(OPUSFILE_FOUND FALSE) +endif() diff --git a/CMake/FindVorbis.cmake b/CMake/FindVorbis.cmake new file mode 100644 index 0000000000..57e60557f7 --- /dev/null +++ b/CMake/FindVorbis.cmake @@ -0,0 +1,210 @@ +#[=======================================================================[.rst: +FindVorbis +---------- + +Finds the native vorbis, vorbisenc amd vorbisfile includes and libraries. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if found: + +``Vorbis::vorbis`` + The Vorbis library +``Vorbis::vorbisenc`` + The VorbisEnc library +``Vorbis::vorbisfile`` + The VorbisFile library + +Result Variables +^^^^^^^^^^^^^^^^ + +This will define the following variables: + +``Vorbis_Vorbis_INCLUDE_DIRS`` + List of include directories when using vorbis. +``Vorbis_Enc_INCLUDE_DIRS`` + List of include directories when using vorbisenc. +``Vorbis_File_INCLUDE_DIRS`` + List of include directories when using vorbisfile. +``Vorbis_Vorbis_LIBRARIES`` + List of libraries when using vorbis. +``Vorbis_Enc_LIBRARIES`` + List of libraries when using vorbisenc. +``Vorbis_File_LIBRARIES`` + List of libraries when using vorbisfile. +``Vorbis_FOUND`` + True if vorbis and requested components found. +``Vorbis_Vorbis_FOUND`` + True if vorbis found. +``Vorbis_Enc_FOUND`` + True if vorbisenc found. +``Vorbis_Enc_FOUND`` + True if vorbisfile found. + +Cache variables +^^^^^^^^^^^^^^^ + +The following cache variables may also be set: + +``Vorbis_Vorbis_INCLUDE_DIR`` + The directory containing ``vorbis/vorbis.h``. +``Vorbis_Enc_INCLUDE_DIR`` + The directory containing ``vorbis/vorbisenc.h``. +``Vorbis_File_INCLUDE_DIR`` + The directory containing ``vorbis/vorbisenc.h``. +``Vorbis_Vorbis_LIBRARY`` + The path to the vorbis library. +``Vorbis_Enc_LIBRARY`` + The path to the vorbisenc library. +``Vorbis_File_LIBRARY`` + The path to the vorbisfile library. + +Hints +^^^^^ + +A user may set ``Vorbis_ROOT`` to a vorbis installation root to tell this module where to look. + +#]=======================================================================] + +if (Vorbis_Vorbis_INCLUDE_DIR) + # Already in cache, be silent + set (Vorbis_FIND_QUIETLY TRUE) +endif () + +set (Vorbis_Vorbis_FIND_QUIETLY TRUE) +set (Vorbis_Enc_FIND_QUIETLY TRUE) +set (Vorbis_File_FIND_QUIETLY TRUE) + +find_package (Ogg QUIET) + +find_package (PkgConfig QUIET) +pkg_check_modules (PC_Vorbis_Vorbis QUIET vorbis) +pkg_check_modules (PC_Vorbis_Enc QUIET vorbisenc) +pkg_check_modules (PC_Vorbis_File QUIET vorbisfile) + +set (Vorbis_VERSION ${PC_Vorbis_Vorbis_VERSION}) + +find_path (Vorbis_Vorbis_INCLUDE_DIR vorbis/codec.h + HINTS + ${PC_Vorbis_Vorbis_INCLUDEDIR} + ${PC_Vorbis_Vorbis_INCLUDE_DIRS} + ${Vorbis_ROOT} + ) + +find_path (Vorbis_Enc_INCLUDE_DIR vorbis/vorbisenc.h + HINTS + ${PC_Vorbis_Enc_INCLUDEDIR} + ${PC_Vorbis_Enc_INCLUDE_DIRS} + ${Vorbis_ROOT} + ) + +find_path (Vorbis_File_INCLUDE_DIR vorbis/vorbisfile.h + HINTS + ${PC_Vorbis_File_INCLUDEDIR} + ${PC_Vorbis_File_INCLUDE_DIRS} + ${Vorbis_ROOT} + ) + +find_library (Vorbis_Vorbis_LIBRARY + NAMES + vorbis + vorbis_static + libvorbis + libvorbis_static + HINTS + ${PC_Vorbis_Vorbis_LIBDIR} + ${PC_Vorbis_Vorbis_LIBRARY_DIRS} + ${Vorbis_ROOT} + ) + +find_library (Vorbis_Enc_LIBRARY + NAMES + vorbisenc + vorbisenc_static + libvorbisenc + libvorbisenc_static + HINTS + ${PC_Vorbis_Enc_LIBDIR} + ${PC_Vorbis_Enc_LIBRARY_DIRS} + ${Vorbis_ROOT} + ) + +find_library (Vorbis_File_LIBRARY + NAMES + vorbisfile + vorbisfile_static + libvorbisfile + libvorbisfile_static + HINTS + ${PC_Vorbis_File_LIBDIR} + ${PC_Vorbis_File_LIBRARY_DIRS} + ${Vorbis_ROOT} + ) + +include (FindPackageHandleStandardArgs) + +if (Vorbis_Vorbis_LIBRARY AND Vorbis_Vorbis_INCLUDE_DIR AND Ogg_FOUND) + set (Vorbis_Vorbis_FOUND TRUE) +endif () + +if (Vorbis_Enc_LIBRARY AND Vorbis_Enc_INCLUDE_DIR AND Vorbis_Vorbis_FOUND) + set (Vorbis_Enc_FOUND TRUE) +endif () + +if (Vorbis_Vorbis_FOUND AND Vorbis_File_LIBRARY AND Vorbis_File_INCLUDE_DIR) + set (Vorbis_File_FOUND TRUE) +endif () + +find_package_handle_standard_args (Vorbis + REQUIRED_VARS + Vorbis_Vorbis_LIBRARY + Vorbis_Vorbis_INCLUDE_DIR + Ogg_FOUND + HANDLE_COMPONENTS + VERSION_VAR Vorbis_VERSION) + + +if (Vorbis_Vorbis_FOUND) + set (Vorbis_Vorbis_INCLUDE_DIRS ${VORBIS_INCLUDE_DIR}) + set (Vorbis_Vorbis_LIBRARIES ${VORBIS_LIBRARY} ${OGG_LIBRARIES}) + if (NOT TARGET Vorbis::vorbis) + add_library (Vorbis::vorbis UNKNOWN IMPORTED) + set_target_properties (Vorbis::vorbis PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${Vorbis_Vorbis_INCLUDE_DIR}" + IMPORTED_LOCATION "${Vorbis_Vorbis_LIBRARY}" + INTERFACE_LINK_LIBRARIES Ogg::ogg + ) + endif () + + if (Vorbis_Enc_FOUND) + set (Vorbis_Enc_INCLUDE_DIRS ${Vorbis_Enc_INCLUDE_DIR}) + set (Vorbis_Enc_LIBRARIES ${Vorbis_Enc_LIBRARY} ${Vorbis_Enc_LIBRARIES}) + if (NOT TARGET Vorbis::vorbisenc) + add_library (Vorbis::vorbisenc UNKNOWN IMPORTED) + set_target_properties (Vorbis::vorbisenc PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${Vorbis_Enc_INCLUDE_DIR}" + IMPORTED_LOCATION "${Vorbis_Enc_LIBRARY}" + INTERFACE_LINK_LIBRARIES Vorbis::vorbis + ) + endif () + endif () + + if (Vorbis_File_FOUND) + set (Vorbis_File_INCLUDE_DIRS ${Vorbis_File_INCLUDE_DIR}) + set (Vorbis_File_LIBRARIES ${Vorbis_File_LIBRARY} ${Vorbis_File_LIBRARIES}) + if (NOT TARGET Vorbis::vorbisfile) + add_library (Vorbis::vorbisfile UNKNOWN IMPORTED) + set_target_properties (Vorbis::vorbisfile PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${Vorbis_File_INCLUDE_DIR}" + IMPORTED_LOCATION "${Vorbis_File_LIBRARY}" + INTERFACE_LINK_LIBRARIES Vorbis::vorbis + ) + endif () + endif () + +endif () + +mark_as_advanced (Vorbis_Vorbis_INCLUDE_DIR Vorbis_Vorbis_LIBRARY) +mark_as_advanced (Vorbis_Enc_INCLUDE_DIR Vorbis_Enc_LIBRARY) +mark_as_advanced (Vorbis_File_INCLUDE_DIR Vorbis_File_LIBRARY) diff --git a/CMakeLists.txt b/CMakeLists.txt index 493973dd25..2f2e35f567 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,8 @@ set(GFX_DEBUG_DISASSEMBLER ON) # Tell LUS we're using F3DEX_GBI_2 (in a way that doesn't break libgfxd) set(GBI_UCODE F3DEX_GBI_2) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMake") + ################################################################################ # Set target arch type if empty. Visual studio solution generator provides it. ################################################################################ @@ -98,7 +100,7 @@ elseif ("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x64") endif() vcpkg_bootstrap() -vcpkg_install_packages(zlib bzip2 libzip libpng sdl2 sdl2-net glew glfw3 nlohmann-json tinyxml2 spdlog) +vcpkg_install_packages(zlib bzip2 libzip libpng sdl2 sdl2-net glew glfw3 nlohmann-json tinyxml2 spdlog libogg libvorbis opus opusfile) endif() ################################################################################ diff --git a/OTRExporter b/OTRExporter index e9667ee75f..fd8cc87147 160000 --- a/OTRExporter +++ b/OTRExporter @@ -1 +1 @@ -Subproject commit e9667ee75f6a1676b206192d4bba18005f4f6dff +Subproject commit fd8cc87147680d8f1b4e41d37aee45c2b1996267 diff --git a/ZAPDTR b/ZAPDTR index b002f28757..4129f346f2 160000 --- a/ZAPDTR +++ b/ZAPDTR @@ -1 +1 @@ -Subproject commit b002f28757a7ff9f928a4d9dd1834137edb144bb +Subproject commit 4129f346f2ecd01e030fffab9574d7825c2b83d6 diff --git a/docs/CUSTOM_SEQUENCED_MUSIC.md b/docs/CUSTOM_SEQUENCED_MUSIC.md new file mode 100644 index 0000000000..83e339e625 --- /dev/null +++ b/docs/CUSTOM_SEQUENCED_MUSIC.md @@ -0,0 +1,26 @@ +### Custom Music + +We support importing custom [Seq64](https://github.com/sauraen/seq64) files to replace the in game music and fanfares (Sound effect and instrument replacement is currently not supported). + +First you will need to prepare a folder with the desired sequences. Every sequence requires two files with the same name and different extensions - a `.seq` Seq64 file and a `.meta` plaintext file. + +The `.meta` file requires two lines - the first line is the name that will be displayed in the SFX editor, the second line is the instrument set number in `base16` format, and the third line optionally contains 'bgm' or 'fanfare'. For example, if there is a sequence file `Foo.seq` then you need a meta file `Foo.meta` that could contain: +``` +Awesome Name +C +``` + +Once you have prepared your sequences folder: +1. Download and open [Future] (https://github.com/louist103/future/). +1. Choose the "Create OTR/O2R" option. +1. Choose the "Custom Audio" option. +1. Choose the "Sequenced" option +1. Using the file selection screen, choose the sequences folder you prepared in the previous instructions. +1. Click the slider to pack an archive instead. +1. Select either OTR or O2R. +1. Click the 'Set Save Path' button to set the save location of the archive, if in create archive mode. + (*NOTE:* 2Ship can handle 1024 custom sequence in total. This includes the original music. Keep that in mind!) +1. Click the "Pack Archive" button.. +1. An archive will be created in the location selected with the 'Set Save Path' step. If that location wasn't the 'mods' folder, move the archive there. The mods folder must be in the same location as the game executable and mm.o2r. + +Assuming you have done everything correctly, boot up 2Ship and select the SFX Editor from the enhancements dropdown menu. You should now be able to swap out any of the in game sequences/fanfares for the sequences added in your newly generated OTR file. If you have any trouble with this process, please reach out in the support section of the Discord. diff --git a/docs/CUSTOM_STREAMED_MUSIC.md b/docs/CUSTOM_STREAMED_MUSIC.md new file mode 100644 index 0000000000..fe6d85bc87 --- /dev/null +++ b/docs/CUSTOM_STREAMED_MUSIC.md @@ -0,0 +1,30 @@ + +### Custom Audio +We support importing streamed (MP3, WAV, OGG, etc.) files to replace in game music, fanfares, sound effects, and instruments. + +First you will need to prepare a folder with the desired songs. These can be `.wav`, `.ogg`, `.mp3`, and `.flac`. Place these files in an empty folder and change their name to what you want to appear in game. No `.meta` files are required for streamed songs. + + +Once you have prepared your sequences folder: +1. Download and open [Future] (https://github.com/louist103/future/). +1. Choose the "Create OTR/O2R" option. +1. Choose the "Custom Audio" option. +1. Choose the "Streamed" option +1. Using the file selection screen, choose the sequences folder you prepared in the previous instructions. +1. Click the slider to pack an archive instead. +1. Select either OTR or O2R. +1. Click the 'Set Save Path' button to set the save location of the archive, if in create archive mode. + (*NOTE:* 2Ship can handle 1024 custom sequence in total. This includes the original music. Keep that in mind!) +1. Click the "Pack Archive" button.. +1. An archive will be created in the location selected with the 'Set Save Path' step. If that location wasn't the 'mods' folder, move the archive there. The mods folder must be in the same location as the game executable and mm.o2r. + +Replacing sound effects and instruments it a little more complicated. +Both of these store extra data in the first sound font, currently named `Soundfont_0`. +1. First open Soundfont_0. This file is an XML document. +2. Next find the sample or sound effect you want to replace. +3. Replace the `SampleRef` field with the path of the new audio file. +4. Set the `Tuning` value using this calculation: $\dfrac{sample rate}{32000} * channels$. This is handled by future for songs. +5. Save the Soundfont XML and replace the one in the original archive. +6. Save the sample file in the archive, giving it the same path as the `SampleRef` + +Assuming you have done everything correctly, boot up 2Ship and select the Audio Editor from the enhancements dropdown menu. You should now be able to swap out any of the in game sequences/fanfares for the sequences added in your newly generated OTR/O2R file. If you have any trouble with this process, please reach out in the support section of the Discord. diff --git a/mm/2s2h/BenGui/BenGui.cpp b/mm/2s2h/BenGui/BenGui.cpp index ce70ed9292..f912cfbfaa 100644 --- a/mm/2s2h/BenGui/BenGui.cpp +++ b/mm/2s2h/BenGui/BenGui.cpp @@ -7,6 +7,7 @@ #include #include "UIWidgets.hpp" #include "HudEditor.h" +#include "2s2h/Enhancements/Audio/AudioEditor.h" #include "CosmeticEditor.h" #include "Notification.h" #include "2s2h/Rando/CheckTracker/CheckTracker.h" @@ -44,6 +45,7 @@ std::shared_ptr mCosmeticEditorWindow; std::shared_ptr mActorViewerWindow; std::shared_ptr mCollisionViewerWindow; std::shared_ptr mEventLogWindow; +std::shared_ptr mAudioEditorWindow; std::shared_ptr mBenMenu; std::shared_ptr mNotificationWindow; std::shared_ptr mRandoCheckTrackerWindow; @@ -118,6 +120,9 @@ void SetupGuiElements() { mEventLogWindow = std::make_shared("gWindows.EventLog", "Event Log", ImVec2(520, 600)); gui->AddGuiWindow(mEventLogWindow); + mAudioEditorWindow = std::make_shared("gWindows.AudioEditor", "Audio Editor", ImVec2(520, 600)); + gui->AddGuiWindow(mAudioEditorWindow); + mItemTrackerWindow = std::make_shared("gWindows.ItemTracker", "Item Tracker"); gui->AddGuiWindow(mItemTrackerWindow); @@ -162,6 +167,7 @@ void Destroy() { mHudEditorWindow = nullptr; mCosmeticEditorWindow = nullptr; mActorViewerWindow = nullptr; + mAudioEditorWindow = nullptr; mItemTrackerWindow = nullptr; mItemTrackerSettingsWindow = nullptr; } diff --git a/mm/2s2h/BenGui/BenMenu.cpp b/mm/2s2h/BenGui/BenMenu.cpp index 6c083b3986..9f2c436536 100644 --- a/mm/2s2h/BenGui/BenMenu.cpp +++ b/mm/2s2h/BenGui/BenMenu.cpp @@ -1360,6 +1360,13 @@ void BenMenu::AddEnhancements() { AddWidget(path, "Popout Settings", WIDGET_WINDOW_BUTTON) .CVar("gWindows.ItemTrackerSettings") .WindowName("Item Tracker Settings"); + + // Audio Editor + path = { "Enhancements", "Audio Editor", 1 }; + AddSidebarEntry("Enhancements", "Audio Editor", 1); + AddWidget(path, "Popout Audio Editor", WIDGET_WINDOW_BUTTON) + .CVar("gWindows.AudioEditor") + .WindowName("Audio Editor"); } void BenMenu::AddDevTools() { diff --git a/mm/2s2h/BenPort.cpp b/mm/2s2h/BenPort.cpp index f0081409d8..a635a1e98a 100644 --- a/mm/2s2h/BenPort.cpp +++ b/mm/2s2h/BenPort.cpp @@ -106,9 +106,11 @@ CrowdControl* CrowdControl::Instance; #include "2s2h/resource/importer/KeyFrameFactory.h" #include "window/gui/resource/Font.h" #include "window/gui/resource/FontFactory.h" +#include "2s2h/Enhancements/Audio/AudioCollection.h" OTRGlobals* OTRGlobals::Instance; GameInteractor* GameInteractor::Instance; +AudioCollection* AudioCollection::Instance; extern "C" char** cameraStrings; bool prevAltAssets = false; @@ -246,14 +248,24 @@ OTRGlobals::OTRGlobals() { "Cutscene", static_cast(SOH::ResourceType::SOH_Cutscene), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "TextMM", static_cast(SOH::ResourceType::TSH_TextMM), 0); + loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "AudioSample", static_cast(SOH::ResourceType::SOH_AudioSample), 2); + loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_XML, + "Sample", static_cast(SOH::ResourceType::SOH_AudioSample), 0); + loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "AudioSoundFont", static_cast(SOH::ResourceType::SOH_AudioSoundFont), 2); + loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_XML, + "SoundFont", static_cast(SOH::ResourceType::SOH_AudioSoundFont), 0); + loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "AudioSequence", static_cast(SOH::ResourceType::SOH_AudioSequence), 2); + loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_XML, + "Sequence", static_cast(SOH::ResourceType::SOH_AudioSequence), 0); + loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "Background", static_cast(SOH::ResourceType::SOH_Background), 0); loader->RegisterResourceFactory(std::make_shared(), @@ -415,6 +427,12 @@ extern "C" void OTRAudio_Init() { } } +extern "C" char** gSequenceMap; +extern "C" size_t gSequenceMapSize; + +extern "C" char** gFontMap; +extern "C" size_t gFontMapSize; + extern "C" void OTRAudio_Exit() { // Tell the audio thread to stop { @@ -425,6 +443,17 @@ extern "C" void OTRAudio_Exit() { // Wait until the audio thread quit audio.thread.join(); + for (size_t i = 0; i < gSequenceMapSize; i++) { + free(gSequenceMap[i]); + } + free(gSequenceMap); + + for (size_t i = 0; i < gFontMapSize; i++) { + free(gFontMap[i]); + } + free(gFontMap); + free(gAudioCtx.seqLoadStatus); + free(gAudioCtx.fontLoadStatus); } extern "C" void OTRExtScanner() { @@ -688,6 +717,7 @@ extern "C" void InitOTR() { OTRGlobals::Instance = new OTRGlobals(); GameInteractor::Instance = new GameInteractor(); + AudioCollection::Instance = new AudioCollection(); LoadGuiTextures(); BenGui::SetupGuiElements(); ShipInit::InitAll(); @@ -745,6 +775,7 @@ extern "C" void DeinitOTR() { BenGui::Destroy(); OTRGlobals::Instance->context = nullptr; + delete AudioCollection::Instance; } #ifdef _WIN32 @@ -1345,6 +1376,10 @@ extern "C" SequenceData ResourceMgr_LoadSeqByName(const char* path) { SequenceData* sequence = (SequenceData*)ResourceGetDataByName(path); return *sequence; } +extern "C" SequenceData* ResourceMgr_LoadSeqPtrByName(const char* path) { + SequenceData* sequence = (SequenceData*)ResourceGetDataByName(path); + return sequence; +} extern "C" KeyFrameSkeleton* ResourceMgr_LoadKeyFrameSkelByName(const char* path) { return (KeyFrameSkeleton*)ResourceGetDataByName(path); } @@ -1415,9 +1450,14 @@ extern "C" SoundFontSample* ResourceMgr_LoadAudioSample(const char* path) { } #endif -extern "C" SoundFont* ResourceMgr_LoadAudioSoundFont(const char* path) { +extern "C" SoundFont* ResourceMgr_LoadAudioSoundFontByName(const char* path) { return (SoundFont*)ResourceGetDataByName(path); } + +extern "C" SoundFont* ResourceMgr_LoadAudioSoundFontByCRC(uint64_t crc) { + return (SoundFont*)ResourceGetDataByCrc(crc); +} + extern "C" int ResourceMgr_OTRSigCheck(char* imgData) { uintptr_t i = (uintptr_t)(imgData); @@ -1826,6 +1866,10 @@ extern "C" int Controller_ShouldRumble(size_t slot) { return 1; } +extern "C" void Messagebox_ShowErrorBox(char* title, char* body) { + Extractor::ShowErrorBox(title, body); +} + // Helper to redirect the user to the boot screen in place of known console crash scenarios, and emits a notification extern "C" bool Ship_HandleConsoleCrashAsReset() { // If fix crashes is on, return false and let fallback handling process in source diff --git a/mm/2s2h/BenPort.h b/mm/2s2h/BenPort.h index f7287b13e4..8a7fef0bd4 100644 --- a/mm/2s2h/BenPort.h +++ b/mm/2s2h/BenPort.h @@ -99,8 +99,8 @@ Vtx* ResourceMgr_LoadVtxByCRC(uint64_t crc); char* ResourceMgr_LoadVtxArrayByName(const char* path); size_t ResourceMgr_GetVtxArraySizeByName(const char* path); Vtx* ResourceMgr_LoadVtxByName(char* path); +SequenceData* ResourceMgr_LoadSeqPtrByName(const char* path); Mtx* ResourceMgr_LoadMtxByName(char* path); - KeyFrameSkeleton* ResourceMgr_LoadKeyFrameSkelByName(const char* path); KeyFrameAnimation* ResourceMgr_LoadKeyFrameAnimByName(const char* path); @@ -143,6 +143,7 @@ void Gfx_UnregisterBlendedTexture(const char* name); void Gfx_TextureCacheDelete(const uint8_t* texAddr); void CheckTracker_OnMessageClose(); +void Messagebox_ShowErrorBox(char* title, char* body); bool Ship_HandleConsoleCrashAsReset(); int32_t GetGIID(uint32_t itemID); diff --git a/mm/2s2h/DeveloperTools/DebugConsole.cpp b/mm/2s2h/DeveloperTools/DebugConsole.cpp index 900c5e7e56..5b42b521aa 100644 --- a/mm/2s2h/DeveloperTools/DebugConsole.cpp +++ b/mm/2s2h/DeveloperTools/DebugConsole.cpp @@ -86,7 +86,7 @@ static bool ActorSpawnHandler(std::shared_ptr Console, const std: static bool LoadSceneHandler(std::shared_ptr Console, const std::vector&, std::string* output) { gSaveContext.respawnFlag = 0; - gSaveContext.seqId = 0xFF; + gSaveContext.seqId = NA_BGM_DISABLED; gSaveContext.gameMode = 0; return 0; diff --git a/mm/2s2h/DeveloperTools/SaveEditor.cpp b/mm/2s2h/DeveloperTools/SaveEditor.cpp index 6de35bd135..ca4365b0fe 100644 --- a/mm/2s2h/DeveloperTools/SaveEditor.cpp +++ b/mm/2s2h/DeveloperTools/SaveEditor.cpp @@ -197,7 +197,7 @@ void UpdateGameTime(u16 gameTime) { if (gPlayState->sequenceCtx.ambienceId != AMBIENCE_ID_13) { SEQCMD_STOP_SEQUENCE(SEQ_PLAYER_AMBIENCE, 0); SEQCMD_STOP_SEQUENCE(SEQ_PLAYER_BGM_MAIN, 240); - gSaveContext.seqId = (u8)NA_BGM_DISABLED; + gSaveContext.seqId = NA_BGM_DISABLED; gSaveContext.ambienceId = AMBIENCE_ID_DISABLED; Environment_PlaySceneSequence(gPlayState); } diff --git a/mm/2s2h/DeveloperTools/WarpPoint.cpp b/mm/2s2h/DeveloperTools/WarpPoint.cpp index 68f8325e4a..3fd911fcdb 100644 --- a/mm/2s2h/DeveloperTools/WarpPoint.cpp +++ b/mm/2s2h/DeveloperTools/WarpPoint.cpp @@ -77,7 +77,7 @@ void RegisterWarpPoint() { if (!CVarGetInteger("gEnhancements.Cutscenes.SkipToFileSelect", 0) && CVarGetInteger(WARP_POINT_CVAR "BootToWarpPoint", 0) && CVarGetInteger(WARP_POINT_CVAR "Saved", 0)) { // Normally called on console logo screen - gSaveContext.seqId = (u8)NA_BGM_DISABLED; + gSaveContext.seqId = NA_BGM_DISABLED; gSaveContext.ambienceId = AMBIENCE_ID_DISABLED; gSaveContext.gameMode = GAMEMODE_TITLE_SCREEN; Warp(); diff --git a/mm/2s2h/Enhancements/Audio/AudioCollection.cpp b/mm/2s2h/Enhancements/Audio/AudioCollection.cpp new file mode 100644 index 0000000000..238a5071e8 --- /dev/null +++ b/mm/2s2h/Enhancements/Audio/AudioCollection.cpp @@ -0,0 +1,376 @@ +#include "AudioCollection.h" +#include "sequence.h" +#include "sfx.h" +#include +#include +#include +#include +#include <2s2h/BenPort.h> +#include +#include + +#define SEQUENCE_MAP_ENTRY(sequenceId, label, sfxKey, category, canBeReplaced, canBeUsedAsReplacement) \ + { \ + sequenceId, { \ + sequenceId, label, sfxKey, category, canBeReplaced, canBeUsedAsReplacement \ + } \ + } + +AudioCollection::AudioCollection() { + // (originalSequenceId, label, sfxKey, + // category, canBeReplaced, canBeUsedAsReplacement), + mSequenceMap = { + SEQUENCE_MAP_ENTRY(NA_BGM_GENERAL_SFX, "General SFX", "SEQUENCE_MAP_ENTRY", SEQ_SFX, false, false), + SEQUENCE_MAP_ENTRY(NA_BGM_AMBIENCE, "Ambience", "NA_BGM_AMBIENCE", SEQ_BGM_WORLD, false, false), + SEQUENCE_MAP_ENTRY(NA_BGM_TERMINA_FIELD, "Termina Field", "NA_BGM_TERMINA_FIELD", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_CHASE, "Chase", "NA_BGM_CHASE", SEQ_BGM_BATTLE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_MAJORAS_THEME, "Majora's Theme", "NA_BGM_MAJORAS_THEME", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_CLOCK_TOWER, "Clock Tower", "NA_BGM_CLOCK_TOWER", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_STONE_TOWER_TEMPLE, "Stone Tower Temple", "NA_BGM_STONE_TOWER_TEMPLE", SEQ_BGM_WORLD, + true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_INV_STONE_TOWER_TEMPLE, "Inverted Stone Tower Temple", + "NA_BGM_INV_STONE_TOWER_TEMPLE", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_FAILURE_0, "Missed Event 0", "NA_BGM_FAILURE_0", SEQ_BGM_EVENT, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_FAILURE_1, "Missed Event 1", "NA_BGM_FAILURE_1", SEQ_BGM_EVENT, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_HAPPY_MASK_SALESMAN, "Happy Mask Salesman's Theme", "NA_BGM_HAPPY_MASK_SALESMAN", + SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_SONG_OF_HEALING, "Song Of Healing", "NA_BGM_SONG_OF_HEALING", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_SWAMP_REGION, "Southern Swamp", "NA_BGM_SWAMP_REGION", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_ALIEN_INVASION, "Alien Invasion", "NA_BGM_ALIEN_INVASION", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_SWAMP_CRUISE, "Boat Cruise", "NA_BGM_SWAMP_CRUISE", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_SHARPS_CURSE, "Sharp's Curse", "NA_BGM_SHARPS_CURSE", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_GREAT_BAY_REGION, "Great Bay", "NA_BGM_GREAT_BAY_REGION", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_IKANA_REGION, "Ikana", "NA_BGM_IKANA_REGION", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_DEKU_PALACE, "Deku Palace", "NA_BGM_DEKU_PALACE", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_MOUNTAIN_REGION, "Mountain Region", "NA_BGM_MOUNTAIN_REGION", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_PIRATES_FORTRESS, "Pirates Fortress", "NA_BGM_PIRATES_FORTRESS", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_CLOCK_TOWN_DAY_1, "Clock Town Day 1", "NA_BGM_CLOCK_TOWN_DAY_1", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_CLOCK_TOWN_DAY_2, "Clock Town Day 2", "NA_BGM_CLOCK_TOWN_DAY_2", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_CLOCK_TOWN_DAY_3, "Clock Town Day 3", "NA_BGM_CLOCK_TOWN_DAY_3", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_FILE_SELECT, "File Select", "NA_BGM_FILE_SELECT", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_CLEAR_EVENT, "Clear Event", "NA_BGM_CLEAR_EVENT", SEQ_BGM_EVENT, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_ENEMY, "Enemy", "NA_BGM_ENEMY", SEQ_BGM_BATTLE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_BOSS, "Boss", "NA_BGM_BOSS", SEQ_BGM_BATTLE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_WOODFALL_TEMPLE, "Woodfall Temple", "NA_BGM_WOODFALL_TEMPLE", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_CLOCK_TOWN_MAIN_SEQUENCE, "Clock Town Main Sequence", + "NA_BGM_CLOCK_TOWN_MAIN_SEQUENCE", SEQ_BGM_WORLD, false, false), + SEQUENCE_MAP_ENTRY(NA_BGM_OPENING, "Opening", "NA_BGM_OPENING", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_INSIDE_A_HOUSE, "Inside House", "NA_BGM_INSIDE_A_HOUSE", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_GAME_OVER, "Game Over", "NA_BGM_GAME_OVER", SEQ_BGM_EVENT, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_CLEAR_BOSS, "Clear Boss", "NA_BGM_CLEAR_BOSS", SEQ_BGM_BATTLE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_GET_ITEM, "Get Item", "NA_BGM_GET_ITEM", SEQ_FANFARE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_CLOCK_TOWN_DAY_2_PTR, "Clock Town Day 2 (Alt)", "NA_BGM_CLOCK_TOWN_DAY_2_PTR", + SEQ_BGM_WORLD, false, false), + SEQUENCE_MAP_ENTRY(NA_BGM_GET_HEART, "Get Heart", "NA_BGM_GET_HEART", SEQ_FANFARE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_TIMED_MINI_GAME, "Timed Minigame", "NA_BGM_TIMED_MINI_GAME", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_GORON_RACE, "Goron Race", "NA_BGM_GORON_RACE", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_MUSIC_BOX_HOUSE, "Music Box House", "NA_BGM_MUSIC_BOX_HOUSE", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_FAIRY_FOUNTAIN, "Fairy Fountain", "NA_BGM_FAIRY_FOUNTAIN", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_ZELDAS_LULLABY, "Zelda's Lullaby", "NA_BGM_ZELDAS_LULLABY", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_ROSA_SISTERS, "Rosa Sisters", "NA_BGM_ROSA_SISTERS", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OPEN_CHEST, "Open Chest", "NA_BGM_OPEN_CHEST", SEQ_BGM_EVENT, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_MARINE_RESEARCH_LAB, "Marine Research Lab", "NA_BGM_MARINE_RESEARCH_LAB", + SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_GIANTS_THEME, "Giants Theme", "NA_BGM_GIANTS_THEME", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_SONG_OF_STORMS, "Song Of Storms", "NA_BGM_SONG_OF_STORMS", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_ROMANI_RANCH, "Romani Ranch", "NA_BGM_ROMANI_RANCH", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_GORON_VILLAGE, "Goron Village", "NA_BGM_GORON_VILLAGE", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_MAYORS_OFFICE, "Mayors Office", "NA_BGM_MAYORS_OFFICE", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OCARINA_EPONA, "Epona's Song", "NA_BGM_OCARINA_EPONA", SEQ_OCARINA, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OCARINA_SUNS, "Sun's Song", "NA_BGM_OCARINA_SUNS", SEQ_OCARINA, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OCARINA_TIME, "Song Of Time", "NA_BGM_OCARINA_TIME", SEQ_OCARINA, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OCARINA_STORM, "Song of Storms (Ocarina)", "NA_BGM_OCARINA_STORM", SEQ_OCARINA, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_ZORA_HALL, "Zora Hall", "NA_BGM_ZORA_HALL", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_GET_NEW_MASK, "Get Mask", "NA_BGM_GET_NEW_MASK", SEQ_FANFARE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_MINI_BOSS, "Mini Boss", "NA_BGM_MINI_BOSS", SEQ_BGM_BATTLE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_GET_SMALL_ITEM, "Get Small Item", "NA_BGM_GET_SMALL_ITEM", SEQ_FANFARE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_ASTRAL_OBSERVATORY, "Astral Observatory", "NA_BGM_ASTRAL_OBSERVATORY", SEQ_BGM_WORLD, + true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_CAVERN, "Cavern", "NA_BGM_CAVERN", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_MILK_BAR, "Milk Bar Day", "NA_BGM_MILK_BAR", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_ZELDA_APPEAR, "Zelda Appear", "NA_BGM_ZELDA_APPEAR", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_SARIAS_SONG, "Saria's Song", "NA_BGM_SARIAS_SONG", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_GORON_GOAL, "Goron Race Goal", "NA_BGM_GORON_GOAL", SEQ_FANFARE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_HORSE, "Horse Race", "NA_BGM_HORSE", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_HORSE_GOAL, "Horse Race Goal", "NA_BGM_HORSE_GOAL", SEQ_FANFARE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_INGO, "Ingo", "NA_BGM_INGO", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_KOTAKE_POTION_SHOP, "Potion Shop (Kotake)", "NA_BGM_KOTAKE_POTION_SHOP", + SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_SHOP, "Shop", "NA_BGM_SHOP", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OWL, "Owl", "NA_BGM_OWL", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_SHOOTING_GALLERY, "Shooting Gallery", "NA_BGM_SHOOTING_GALLERY", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_OCARINA_SOARING, "Song Of Soaring", "NA_BGM_OCARINA_SOARING", SEQ_OCARINA, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_OCARINA_HEALING, "Song Of Healing", "NA_BGM_OCARINA_HEALING", SEQ_OCARINA, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_INVERTED_SONG_OF_TIME, "Inverted Song Of Time", "NA_BGM_INVERTED_SONG_OF_TIME", + SEQ_OCARINA, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_SONG_OF_DOUBLE_TIME, "Song Of Double Time", "NA_BGM_SONG_OF_DOUBLE_TIME", SEQ_OCARINA, + true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_SONATA_OF_AWAKENING, "Sonata Of Awakening", "NA_BGM_SONATA_OF_AWAKENING", + SEQ_BGM_SONGS, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_GORON_LULLABY, "Goron Lullaby", "NA_BGM_GORON_LULLABY", SEQ_BGM_SONGS, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_NEW_WAVE_BOSSA_NOVA, "New Wave Bossa Nova", "NA_BGM_NEW_WAVE_BOSSA_NOVA", + SEQ_BGM_SONGS, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_ELEGY_OF_EMPTINESS, "Elegy Of Emptiness", "NA_BGM_ELEGY_OF_EMPTINESS", SEQ_BGM_SONGS, + true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OATH_TO_ORDER, "Oath To Order", "NA_BGM_OATH_TO_ORDER", SEQ_BGM_SONGS, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_SWORD_TRAINING_HALL, "Sword Training", "NA_BGM_SWORD_TRAINING_HALL", SEQ_BGM_WORLD, + true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OCARINA_LULLABY_INTRO, "Lullaby Intro", "NA_BGM_OCARINA_LULLABY_INTRO", SEQ_OCARINA, + true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_LEARNED_NEW_SONG, "Get Song", "NA_BGM_LEARNED_NEW_SONG", SEQ_FANFARE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_BREMEN_MARCH, "Bremen March", "NA_BGM_BREMEN_MARCH", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_BALLAD_OF_THE_WIND_FISH, "Ballad Of The Wind Fish", "NA_BGM_BALLAD_OF_THE_WIND_FISH", + SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_SONG_OF_SOARING, "Song Of Soaring", "NA_BGM_SONG_OF_SOARING", SEQ_BGM_SONGS, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_MILK_BAR_DUPLICATE, "Milk Bar Night", "NA_BGM_MILK_BAR_DUPLICATE", SEQ_BGM_WORLD, + true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_FINAL_HOURS, "Final Hours", "NA_BGM_FINAL_HOURS", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_MIKAU_RIFF, "Mikau Riff", "NA_BGM_MIKAU_RIFF", SEQ_BGM_SONGS, true, + true), // Looping instrument + SEQUENCE_MAP_ENTRY(NA_BGM_MIKAU_FINALE, "Mikau Finale", "NA_BGM_MIKAU_FINALE", SEQ_BGM_SONGS, true, + true), // Instrument finale + SEQUENCE_MAP_ENTRY(NA_BGM_FROG_SONG, "Frog Song", "NA_BGM_FROG_SONG", SEQ_BGM_WORLD, true, true), // Looping BGM + SEQUENCE_MAP_ENTRY(NA_BGM_OCARINA_SONATA, "Sonata Of Awakening (Ocarina)", "NA_BGM_OCARINA_SONATA", SEQ_OCARINA, + true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OCARINA_LULLABY, "Goron Lullaby", "NA_BGM_OCARINA_LULLABY", SEQ_OCARINA, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OCARINA_NEW_WAVE, "New Wave Bossa Nova (Ocarina)", "NA_BGM_OCARINA_NEW_WAVE", + SEQ_OCARINA, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OCARINA_ELEGY, "Elegy Of Emptiness (Ocarina)", "NA_BGM_OCARINA_ELEGY", SEQ_OCARINA, + true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OCARINA_OATH, "Oath To Order (Ocarina)", "NA_BGM_OCARINA_OATH", SEQ_OCARINA, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_MAJORAS_LAIR, "Majora's Lair", "NA_BGM_MAJORAS_LAIR", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OCARINA_LULLABY_INTRO_PTR, "Lullaby Intro Pointer", + "NA_BGM_OCARINA_LULLABY_INTRO_PTR", SEQ_OCARINA, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OCARINA_GUITAR_BASS_SESSION, "Jam Session Bass", "NA_BGM_OCARINA_GUITAR_BASS_SESSION", + SEQ_BGM_SONGS, true, true), // Instrument session + SEQUENCE_MAP_ENTRY(NA_BGM_PIANO_SESSION, "Jam Session Piano", "NA_BGM_PIANO_SESSION", SEQ_BGM_SONGS, true, + true), // Instrument session + SEQUENCE_MAP_ENTRY(NA_BGM_INDIGO_GO_SESSION, "Indigo Go Session (Credits)", "NA_BGM_INDIGO_GO_SESSION", + SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_SNOWHEAD_TEMPLE, "Snowhead Temple", "NA_BGM_SNOWHEAD_TEMPLE", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_GREAT_BAY_TEMPLE, "Great Bay Temple", "NA_BGM_GREAT_BAY_TEMPLE", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_NEW_WAVE_SAXOPHONE, "New Wave Saxophone", "NA_BGM_NEW_WAVE_SAXOPHONE", SEQ_BGM_SONGS, + false, false), // Doesn't play outside the original cutscene + SEQUENCE_MAP_ENTRY(NA_BGM_NEW_WAVE_VOCAL, "New Wave Vocal", "NA_BGM_NEW_WAVE_VOCAL", SEQ_BGM_SONGS, true, + true), // Vocal + SEQUENCE_MAP_ENTRY(NA_BGM_MAJORAS_WRATH, "Majora's Wrath", "NA_BGM_MAJORAS_WRATH", SEQ_BGM_BATTLE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_MAJORAS_INCARNATION, "Majora's Incarnation", "NA_BGM_MAJORAS_INCARNATION", + SEQ_BGM_BATTLE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_MAJORAS_MASK, "Majora's Mask", "NA_BGM_MAJORAS_MASK", SEQ_BGM_BATTLE, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_BASS_PLAY, "Bass Play", "NA_BGM_BASS_PLAY", SEQ_BGM_SONGS, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_DRUMS_PLAY, "Drums Play", "NA_BGM_DRUMS_PLAY", SEQ_BGM_SONGS, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_PIANO_PLAY, "Piano Play", "NA_BGM_PIANO_PLAY", SEQ_BGM_SONGS, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_IKANA_CASTLE, "Ikana Castle", "NA_BGM_IKANA_CASTLE", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_GATHERING_GIANTS, "Gathering Giants", "NA_BGM_GATHERING_GIANTS", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_KAMARO_DANCE, "Kamaro Dance", "NA_BGM_KAMARO_DANCE", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_CREMIA_CARRIAGE, "Cremia Carriage", "NA_BGM_CREMIA_CARRIAGE", SEQ_BGM_WORLD, true, + true), + SEQUENCE_MAP_ENTRY(NA_BGM_KEATON_QUIZ, "Keaton Quiz", "NA_BGM_KEATON_QUIZ", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_END_CREDITS, "Credits (First Half)", "NA_BGM_END_CREDITS", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_OPENING_LOOP, "Opening Loop", "NA_BGM_OPENING_LOOP", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_TITLE_THEME, "Title Theme", "NA_BGM_TITLE_THEME", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_DUNGEON_APPEAR, "Dungeon Appear", "NA_BGM_DUNGEON_APPEAR", SEQ_BGM_EVENT, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_WOODFALL_CLEAR, "Woodfall Clear", "NA_BGM_WOODFALL_CLEAR", SEQ_BGM_EVENT, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_SNOWHEAD_CLEAR, "Snowhead Clear", "NA_BGM_SNOWHEAD_CLEAR", SEQ_BGM_EVENT, true, true), + SEQUENCE_MAP_ENTRY(0x7A, "IDK But there is an entry missing", "SEND_HELP", SEQ_NOSHUFFLE, false, false), + SEQUENCE_MAP_ENTRY(NA_BGM_INTO_THE_MOON, "Enter Moon", "NA_BGM_INTO_THE_MOON", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_GOODBYE_GIANT, "Giants Leave", "NA_BGM_GOODBYE_GIANT", SEQ_BGM_EVENT, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_TATL_AND_TAEL, "Tatl & Tale", "NA_BGM_TATL_AND_TAEL", SEQ_BGM_WORLD, true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_MOONS_DESTRUCTION, "Moon's Destruction", "NA_BGM_MOONS_DESTRUCTION", SEQ_BGM_EVENT, + true, true), + SEQUENCE_MAP_ENTRY(NA_BGM_END_CREDITS_SECOND_HALF, "Credits (Second Half)", "NA_BGM_END_CREDITS_SECOND_HALF", + SEQ_BGM_WORLD, true, true), + }; + + // Initialize counts for each type + for (const auto& [seqId, seqInfo] : mSequenceMap) { + // Don't count sequences that are marked as custom BGM + if (!(seqInfo.category == SEQ_BGM_CUSTOM)) { + // Check each type flag individually + if (seqInfo.category & SEQ_BGM_WORLD) + mSequenceTypeCounts[SEQ_BGM_WORLD]++; + if (seqInfo.category & SEQ_BGM_EVENT) + mSequenceTypeCounts[SEQ_BGM_EVENT]++; + if (seqInfo.category & SEQ_BGM_BATTLE) + mSequenceTypeCounts[SEQ_BGM_BATTLE]++; + if (seqInfo.category & SEQ_OCARINA) + mSequenceTypeCounts[SEQ_OCARINA]++; + if (seqInfo.category & SEQ_FANFARE) + mSequenceTypeCounts[SEQ_FANFARE]++; + if (seqInfo.category & SEQ_SFX) + mSequenceTypeCounts[SEQ_SFX]++; + if (seqInfo.category & SEQ_BGM_SONGS) + mSequenceTypeCounts[SEQ_BGM_SONGS]++; + if (seqInfo.category & SEQ_VOICE) + mSequenceTypeCounts[SEQ_VOICE]++; + } + } +} +#define CVAR_AUDIO(var) CVAR_PREFIX_AUDIO "." var +std::string AudioCollection::GetCvarKey(std::string sfxKey) { + auto prefix = CVAR_AUDIO("ReplacedSequences."); + return prefix + sfxKey + ".value"; +} + +std::string AudioCollection::GetCvarLockKey(std::string sfxKey) { + auto prefix = std::string(CVAR_AUDIO("ReplacedSequences.")); + return prefix + sfxKey + ".locked"; +} + +void AudioCollection::AddToCollection(char* otrPath, uint16_t seqNum) { + std::string fileName = std::filesystem::path(otrPath).filename().string(); + size_t underscorePos = fileName.find_last_of('_') + 1; + SeqType type = SEQ_BGM_CUSTOM; + if (underscorePos != std::string::npos) { + std::string typeString = fileName.substr(underscorePos); + std::locale loc; + for (size_t i = 0; i < typeString.length(); i++) { + typeString[i] = std::tolower(typeString[i], loc); + } + if (typeString == "fanfare") { + type = SEQ_BGM_CUSTOM_FANFARE; + } + } + std::string sequenceName = fileName.substr(0, underscorePos - 1); + SequenceInfo info = { seqNum, + sequenceName, + StringHelper::Replace( + StringHelper::Replace(StringHelper::Replace(sequenceName, " ", "_"), "~", "-"), ".", ""), + type, + false, + true }; + mSequenceMap.emplace(seqNum, info); +} + +uint16_t AudioCollection::GetReplacementSequence(uint16_t seqId) { + if (mSequenceMap.find(seqId) == mSequenceMap.end()) { + return seqId; + } + + const auto& sequenceInfo = mSequenceMap.at(seqId); + const std::string cvarKey = GetCvarKey(sequenceInfo.sfxKey); + int replacementSeq = CVarGetInteger(cvarKey.c_str(), seqId); + if (!mSequenceMap.contains(replacementSeq)) { + replacementSeq = seqId; + } + return static_cast(replacementSeq); +} + +// For custom sequences, we need to get the original sequence ID for sequence flag lookups +uint16_t AudioCollection::GetOriginalSequence(uint16_t seqId) { + // BENTODO there is probably a better way to do this. + // There are 127 original sequences. If the ID is less than that we don't need to do + // any lookups + if (seqId <= GetMaxOriginalSeqId()) { + return seqId; + } + + for (const auto& a : mSequenceMap) { + const std::string cvarKey = GetCvarKey(a.second.sfxKey); + int replacementSeq = CVarGetInteger(cvarKey.c_str(), NA_BGM_DISABLED); + if (replacementSeq == seqId) { + return a.first; + } + } + return 0; +} + +void AudioCollection::RemoveFromShufflePool(SequenceInfo* seqInfo) { + const std::string cvarKey = std::string(CVAR_AUDIO("Excluded.")) + seqInfo->sfxKey; + excludedSequences.insert(seqInfo); + includedSequences.erase(seqInfo); + CVarSetInteger(cvarKey.c_str(), 1); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); +} + +void AudioCollection::AddToShufflePool(SequenceInfo* seqInfo) { + const std::string cvarKey = std::string(CVAR_AUDIO("Excluded.")) + seqInfo->sfxKey; + includedSequences.insert(seqInfo); + excludedSequences.erase(seqInfo); + CVarClear(cvarKey.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); +} + +void AudioCollection::InitializeShufflePool() { + if (shufflePoolInitialized) + return; + + for (auto& [seqId, seqInfo] : mSequenceMap) { + if (!seqInfo.canBeUsedAsReplacement) + continue; + const std::string cvarKey = std::string(CVAR_AUDIO("Excluded.")) + seqInfo.sfxKey; + if (CVarGetInteger(cvarKey.c_str(), 0)) { + excludedSequences.insert(&seqInfo); + } else { + includedSequences.insert(&seqInfo); + } + } + + shufflePoolInitialized = true; +}; + +extern "C" void AudioCollection_AddToCollection(char* otrPath, uint16_t seqNum) { + AudioCollection::Instance->AddToCollection(otrPath, seqNum); +} + +bool AudioCollection::HasSequenceNum(uint16_t seqId) { + return mSequenceMap.contains(seqId); +} + +const char* AudioCollection::GetSequenceName(uint16_t seqId) { + auto seqIt = mSequenceMap.find(seqId); + if (seqIt != mSequenceMap.end()) { + return seqIt->second.label.c_str(); + } + return nullptr; +} + +size_t AudioCollection::SequenceMapSize() { + return mSequenceMap.size(); +} + +extern "C" const char* AudioCollection_GetSequenceName(uint16_t seqId) { + return AudioCollection::Instance->GetSequenceName(seqId); +} + +extern "C" bool AudioCollection_HasSequenceNum(uint16_t seqId) { + return AudioCollection::Instance->HasSequenceNum(seqId); +} + +extern "C" size_t AudioCollection_SequenceMapSize() { + return AudioCollection::Instance->SequenceMapSize(); +} + +size_t AudioCollection::CountSequencesByType(SeqType type) { + auto it = mSequenceTypeCounts.find(type); + return it != mSequenceTypeCounts.end() ? it->second : 0; +} + +uint16_t AudioCollection::GetMaxOriginalSeqId() const { + uint16_t maxId = 0; + for (const auto& [seqId, seqInfo] : mSequenceMap) { + if (!(seqInfo.category & SEQ_BGM_CUSTOM) && seqId > maxId) { + maxId = seqId; + } + } + return maxId; +} diff --git a/mm/2s2h/Enhancements/Audio/AudioCollection.h b/mm/2s2h/Enhancements/Audio/AudioCollection.h new file mode 100644 index 0000000000..b596c65b7e --- /dev/null +++ b/mm/2s2h/Enhancements/Audio/AudioCollection.h @@ -0,0 +1,83 @@ +#pragma once +#ifdef __cplusplus +#include +#include +#include +#include + +enum SeqType { + SEQ_NOSHUFFLE = 0, + SEQ_BGM_WORLD = 1 << 0, + SEQ_BGM_EVENT = 1 << 1, + SEQ_BGM_BATTLE = 1 << 2, + SEQ_OCARINA = 1 << 3, + SEQ_FANFARE = 1 << 4, + SEQ_BGM_ERROR = 1 << 5, + SEQ_SFX = 1 << 6, + SEQ_INSTRUMENT = 1 << 7, + SEQ_VOICE = 1 << 8, + SEQ_BGM_SONGS = 1 << 9, + SEQ_BGM_CUSTOM = SEQ_BGM_WORLD | SEQ_BGM_EVENT | SEQ_BGM_BATTLE, + SEQ_BGM_CUSTOM_FANFARE = SEQ_FANFARE | SEQ_OCARINA | SEQ_BGM_SONGS, +}; + +#define INSTRUMENT_OFFSET 0x81 + +struct SequenceInfo { + uint16_t sequenceId; + std::string label; + std::string sfxKey; + SeqType category; + bool canBeReplaced; + bool canBeUsedAsReplacement; +}; + +class AudioCollection { + private: + // All Loaded Audio + std::map mSequenceMap; + + // Sequences/SFX to include in/exclude from shuffle pool + struct compareSequenceLabel { + bool operator()(SequenceInfo* a, SequenceInfo* b) const { + return a->label < b->label; + }; + }; + std::set includedSequences; + std::set excludedSequences; + bool shufflePoolInitialized = false; + + std::map mSequenceTypeCounts; + + public: + static AudioCollection* Instance; + AudioCollection(); + std::map GetAllSequences() const { + return mSequenceMap; + } + std::set GetIncludedSequences() const { + return includedSequences; + }; + std::set GetExcludedSequences() const { + return excludedSequences; + }; + void AddToShufflePool(SequenceInfo*); + void RemoveFromShufflePool(SequenceInfo*); + void AddToCollection(char* otrPath, uint16_t seqNum); + uint16_t GetReplacementSequence(uint16_t seqId); + uint16_t GetOriginalSequence(uint16_t seqId); + void InitializeShufflePool(); + const char* GetSequenceName(uint16_t seqId); + bool HasSequenceNum(uint16_t seqId); + size_t SequenceMapSize(); + std::string GetCvarKey(std::string sfxKey); + std::string GetCvarLockKey(std::string sfxKey); + size_t CountSequencesByType(SeqType type); + uint16_t GetMaxOriginalSeqId() const; +}; +#else +void AudioCollection_AddToCollection(char* otrPath, uint16_t seqNum); +const char* AudioCollection_GetSequenceName(uint16_t seqId); +bool AudioCollection_HasSequenceNum(uint16_t seqId); +size_t AudioCollection_SequenceMapSize(); +#endif \ No newline at end of file diff --git a/mm/2s2h/Enhancements/Audio/AudioEditor.cpp b/mm/2s2h/Enhancements/Audio/AudioEditor.cpp new file mode 100644 index 0000000000..0fcd47a3a2 --- /dev/null +++ b/mm/2s2h/Enhancements/Audio/AudioEditor.cpp @@ -0,0 +1,764 @@ +#include "AudioEditor.h" +#include "sequence.h" + +#include +#include +#include +#include +#include +//#include "../randomizer/3drando/random.hpp" +#include "../../BenPort.h" +#include +#include "../../BenGui/UIWidgets.hpp" +#include "AudioCollection.h" +#include "GameInteractor/GameInteractor.h" +#include + +extern "C" Vec3f gZeroVec3f; +extern "C" f32 gSfxDefaultFreqAndVolScale; +extern "C" s8 gSfxDefaultReverb; + +// Authentic sequence counts +// used to ensure we have enough to shuffle +#define SEQ_COUNT_BGM_WORLD 30 +#define SEQ_COUNT_BGM_BATTLE 6 +#define SEQ_COUNT_FANFARE 15 +#define SEQ_COUNT_OCARINA 12 +#define SEQ_COUNT_NOSHUFFLE 6 +#define SEQ_COUNT_BGM_EVENT 17 +#define SEQ_COUNT_INSTRUMENT 6 +#define SEQ_COUNT_SFX 57 +#define SEQ_COUNT_VOICE 108 + +size_t AuthenticCountBySequenceType(SeqType type) { + return AudioCollection::Instance->CountSequencesByType(type); +} + +#define CVAR_AUDIO(var) CVAR_PREFIX_AUDIO "." var + +// Grabs the current BGM sequence ID and replays it +// which will lookup the proper override, or reset back to vanilla +void ReplayCurrentBGM() { + u16 curSeqId = AudioSeq_GetActiveSeqId(SEQ_PLAYER_BGM_MAIN); + // TODO: replace with Audio_StartSeq when the macro is shared + // The fade time and audio player flags will always be 0 in the case of replaying the BGM, so they are not set here + // AudioSeq_QueueSeqCmd(0x00000000 | curSeqId); + SEQCMD_PLAY_SEQUENCE(SEQ_PLAYER_BGM_MAIN, 0, curSeqId); +} + +// Attempt to update the BGM if it matches the current sequence that is being played +// The seqKey that is passed in should be the vanilla ID, not the override ID +void UpdateCurrentBGM(u16 seqKey, SeqType seqType) { + if (seqType != SEQ_BGM_WORLD) { + return; + } + + u16 curSeqId = AudioSeq_GetActiveSeqId(SEQ_PLAYER_BGM_MAIN); + if (curSeqId == seqKey) { + ReplayCurrentBGM(); + } +} + +void RandomizeGroup(SeqType type) { + std::vector values; + + // An empty IncludedSequences set means that the AudioEditor window has never been drawn + if (AudioCollection::Instance->GetIncludedSequences().empty()) { + AudioCollection::Instance->InitializeShufflePool(); + } + + // use a while loop to add duplicates if we don't have enough included sequences + while (values.size() < AudioCollection::Instance->CountSequencesByType(type)) { + size_t initialSize = values.size(); + for (const auto& seqData : AudioCollection::Instance->GetIncludedSequences()) { + if (seqData->category & type && seqData->canBeUsedAsReplacement) { + values.push_back(seqData->sequenceId); + } + } + + // if we didn't add any new values, return early to prevent an infinite loop + if (values.size() == initialSize) { + return; + } + } + + if (values.empty()) { + return; + } + std::random_device rd; + std::mt19937 g(rd()); + + std::shuffle(values.begin(), values.end(), g); + for (const auto& [seqId, seqData] : AudioCollection::Instance->GetAllSequences()) { + const std::string cvarKey = AudioCollection::Instance->GetCvarKey(seqData.sfxKey); + const std::string cvarLockKey = AudioCollection::Instance->GetCvarLockKey(seqData.sfxKey); + // don't randomize locked entries + if ((seqData.category & type) && CVarGetInteger(cvarLockKey.c_str(), 0) == 0) { + // Only save authentic sequence CVars + if ((((seqData.category & SEQ_BGM_CUSTOM) || seqData.category == SEQ_FANFARE) && + seqData.sequenceId >= MAX_AUTHENTIC_SEQID) || + seqData.canBeReplaced == false) { + continue; + } + if (!values.empty()) { + const int randomValue = values.back(); + CVarSetInteger(cvarKey.c_str(), randomValue); + values.pop_back(); + } + } + } +} + +void ResetGroup(const std::map& map, SeqType type) { + for (const auto& [defaultValue, seqData] : map) { + if (seqData.category == type) { + // Only save authentic sequence CVars + if (seqData.category == SEQ_FANFARE && defaultValue >= MAX_AUTHENTIC_SEQID) { + continue; + } + const std::string cvarKey = AudioCollection::Instance->GetCvarKey(seqData.sfxKey); + const std::string cvarLockKey = AudioCollection::Instance->GetCvarLockKey(seqData.sfxKey); + if (CVarGetInteger(cvarLockKey.c_str(), 0) == 0) { + CVarClear(cvarKey.c_str()); + } + } + } +} + +void LockGroup(const std::map& map, SeqType type) { + for (const auto& [defaultValue, seqData] : map) { + if (seqData.category == type) { + // Only save authentic sequence CVars + if (seqData.category == SEQ_FANFARE && defaultValue >= MAX_AUTHENTIC_SEQID) { + continue; + } + const std::string cvarKey = AudioCollection::Instance->GetCvarKey(seqData.sfxKey); + const std::string cvarLockKey = AudioCollection::Instance->GetCvarLockKey(seqData.sfxKey); + CVarSetInteger(cvarLockKey.c_str(), 1); + } + } +} + +void UnlockGroup(const std::map& map, SeqType type) { + for (const auto& [defaultValue, seqData] : map) { + if (seqData.category == type) { + // Only save authentic sequence CVars + if (seqData.category == SEQ_FANFARE && defaultValue >= MAX_AUTHENTIC_SEQID) { + continue; + } + const std::string cvarKey = AudioCollection::Instance->GetCvarKey(seqData.sfxKey); + const std::string cvarLockKey = AudioCollection::Instance->GetCvarLockKey(seqData.sfxKey); + CVarSetInteger(cvarLockKey.c_str(), 0); + } + } +} +extern "C" void Audio_ForceRestorePreviousBgm(void); +extern "C" void PreviewSequence(u16 seqId); +void DrawPreviewButton(uint16_t sequenceId, std::string sfxKey, SeqType sequenceType) { + const std::string cvarKey = AudioCollection::Instance->GetCvarKey(sfxKey); + const std::string hiddenKey = "##" + cvarKey; + const std::string stopButton = ICON_FA_STOP + hiddenKey; + const std::string previewButton = ICON_FA_PLAY + hiddenKey; + + if (CVarGetInteger(CVAR_AUDIO("Playing"), 0) == sequenceId) { + if (ImGui::Button(stopButton.c_str())) { + Audio_ForceRestorePreviousBgm(); + CVarSetInteger(CVAR_AUDIO("Playing"), 0); + } + UIWidgets::Tooltip("Stop Preview"); + } else { + if (ImGui::Button(previewButton.c_str())) { + if (CVarGetInteger(CVAR_AUDIO("Playing"), 0) != 0) { + Audio_ForceRestorePreviousBgm(); + CVarSetInteger(CVAR_AUDIO("Playing"), 0); + } else { + if (sequenceType == SEQ_SFX || sequenceType == SEQ_VOICE) { + AudioSfx_PlaySfx(sequenceId, &gZeroVec3f, 4, &gSfxDefaultFreqAndVolScale, + &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); + } else if (sequenceType == SEQ_INSTRUMENT) { + AudioOcarina_SetInstrument(sequenceId - INSTRUMENT_OFFSET); + AudioOcarina_SetPlaybackSong(9, 1); + } else { + // TODO: Cant do both here, so have to click preview button twice + PreviewSequence(sequenceId); + CVarSetInteger(CVAR_AUDIO("Playing"), sequenceId); + } + } + } + UIWidgets::Tooltip("Play Preview"); + } +} + +void Draw_SfxTab(const std::string& tabId, SeqType type) { + const std::map& map = AudioCollection::Instance->GetAllSequences(); + + const std::string hiddenTabId = "##" + tabId; + const std::string resetAllButton = "Reset All" + hiddenTabId; + const std::string randomizeAllButton = "Randomize All" + hiddenTabId; + const std::string lockAllButton = "Lock All" + hiddenTabId; + const std::string unlockAllButton = "Unlock All" + hiddenTabId; + if (ImGui::Button(resetAllButton.c_str())) { + auto currentBGM = AudioSeq_GetActiveSeqId(SEQ_PLAYER_BGM_MAIN); + auto prevReplacement = AudioCollection::Instance->GetReplacementSequence(currentBGM); + ResetGroup(map, type); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + auto curReplacement = AudioCollection::Instance->GetReplacementSequence(currentBGM); + if (type == SEQ_BGM_WORLD && prevReplacement != curReplacement) { + ReplayCurrentBGM(); + } + } + ImGui::SameLine(); + if (ImGui::Button(randomizeAllButton.c_str())) { + auto currentBGM = AudioSeq_GetActiveSeqId(SEQ_PLAYER_BGM_MAIN); + auto prevReplacement = AudioCollection::Instance->GetReplacementSequence(currentBGM); + RandomizeGroup(type); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + auto curReplacement = AudioCollection::Instance->GetReplacementSequence(currentBGM); + if (type == SEQ_BGM_WORLD && prevReplacement != curReplacement) { + ReplayCurrentBGM(); + } + } + ImGui::SameLine(); + if (ImGui::Button(lockAllButton.c_str())) { + auto currentBGM = AudioSeq_GetActiveSeqId(SEQ_PLAYER_BGM_MAIN); + auto prevReplacement = AudioCollection::Instance->GetReplacementSequence(currentBGM); + LockGroup(map, type); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + auto curReplacement = AudioCollection::Instance->GetReplacementSequence(currentBGM); + if (type == SEQ_BGM_WORLD && prevReplacement != curReplacement) { + ReplayCurrentBGM(); + } + } + ImGui::SameLine(); + if (ImGui::Button(unlockAllButton.c_str())) { + auto currentBGM = AudioSeq_GetActiveSeqId(SEQ_PLAYER_BGM_MAIN); + auto prevReplacement = AudioCollection::Instance->GetReplacementSequence(currentBGM); + UnlockGroup(map, type); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + auto curReplacement = AudioCollection::Instance->GetReplacementSequence(currentBGM); + if (type == SEQ_BGM_WORLD && prevReplacement != curReplacement) { + ReplayCurrentBGM(); + } + } + + ImGui::BeginTable(tabId.c_str(), 3, ImGuiTableFlags_SizingFixedFit); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 100.0f); + for (const auto& [defaultValue, seqData] : map) { + if (~(seqData.category) & type) { + continue; + } + // Do not display custom sequences in the list + if ((((seqData.category & SEQ_BGM_CUSTOM) || seqData.category == SEQ_BGM_CUSTOM_FANFARE) && + defaultValue >= MAX_AUTHENTIC_SEQID) || + seqData.canBeReplaced == false) { + continue; + } + + const std::string initialSfxKey = seqData.sfxKey; + const std::string cvarKey = AudioCollection::Instance->GetCvarKey(seqData.sfxKey); + const std::string cvarLockKey = AudioCollection::Instance->GetCvarLockKey(seqData.sfxKey); + const std::string hiddenKey = "##" + cvarKey; + const std::string resetButton = ICON_FA_UNDO + hiddenKey; + const std::string randomizeButton = ICON_FA_RANDOM + hiddenKey; + const std::string lockedButton = ICON_FA_LOCK + hiddenKey; + const std::string unlockedButton = ICON_FA_UNLOCK + hiddenKey; + const int currentValue = CVarGetInteger(cvarKey.c_str(), defaultValue); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%s", seqData.label.c_str()); + ImGui::TableNextColumn(); + ImGui::PushItemWidth(-FLT_MIN); + const int initialValue = map.contains(currentValue) ? currentValue : defaultValue; + if (ImGui::BeginCombo(hiddenKey.c_str(), map.at(initialValue).label.c_str())) { + for (const auto& [value, seqData] : map) { + // If excluded as a replacement sequence, don't show in other dropdowns except the effect's own + // dropdown. + if (~(seqData.category) & type || + (!seqData.canBeUsedAsReplacement && initialSfxKey != seqData.sfxKey)) { + continue; + } + + if (ImGui::Selectable(seqData.label.c_str())) { + CVarSetInteger(cvarKey.c_str(), value); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + UpdateCurrentBGM(defaultValue, type); + } + + if (currentValue == value) { + ImGui::SetItemDefaultFocus(); + } + } + + ImGui::EndCombo(); + } + ImGui::TableNextColumn(); + ImGui::PushItemWidth(-FLT_MIN); + // BENTODO ship checks for some values and passes either the replaced value or the default value. For some + // reason, likely an edge case, the original -> replacement lookup is done twice. Passing the original value + // here seems to work. + DrawPreviewButton(defaultValue, seqData.sfxKey, type); + auto locked = CVarGetInteger(cvarLockKey.c_str(), 0) == 1; + ImGui::SameLine(); + ImGui::PushItemWidth(-FLT_MIN); + if (ImGui::Button(resetButton.c_str())) { + CVarClear(cvarKey.c_str()); + CVarClear(cvarLockKey.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + UpdateCurrentBGM(defaultValue, seqData.category); + } + UIWidgets::Tooltip("Reset to default"); + ImGui::SameLine(); + ImGui::PushItemWidth(-FLT_MIN); + if (ImGui::Button(randomizeButton.c_str())) { + std::vector validSequences = {}; + for (const auto seqInfo : AudioCollection::Instance->GetIncludedSequences()) { + if (seqInfo->category & type) { + validSequences.push_back(seqInfo); + } + } + + if (validSequences.size()) { + auto it = validSequences.begin(); + const auto& seqData = *std::next(it, rand() % validSequences.size()); + CVarSetInteger(cvarKey.c_str(), seqData->sequenceId); + if (locked) { + CVarClear(cvarLockKey.c_str()); + } + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + UpdateCurrentBGM(defaultValue, type); + } + } + UIWidgets::Tooltip("Randomize this sound"); + ImGui::SameLine(); + ImGui::PushItemWidth(-FLT_MIN); + if (ImGui::Button(locked ? lockedButton.c_str() : unlockedButton.c_str())) { + if (locked) { + CVarClear(cvarLockKey.c_str()); + } else { + CVarSetInteger(cvarLockKey.c_str(), 1); + } + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + UIWidgets::Tooltip(locked ? "Sound locked" : "Sound unlocked"); + } + ImGui::EndTable(); +} + +extern "C" u16 AudioEditor_GetReplacementSeq(u16 seqId) { + return AudioCollection::Instance->GetReplacementSequence(seqId); +} + +extern "C" u16 AudioEditor_GetOriginalSeq(u16 seqId) { + return AudioCollection::Instance->GetOriginalSequence(seqId); +} + +const char* GetSequenceTypeName(SeqType type) { + switch (type) { + case SEQ_NOSHUFFLE: + return "No Shuffle"; + case SEQ_BGM_WORLD: + return "World"; + case SEQ_BGM_EVENT: + return "Event"; + case SEQ_BGM_BATTLE: + return "Battle"; + case SEQ_OCARINA: + return "Ocarina"; + case SEQ_FANFARE: + return "Fanfare"; + case SEQ_BGM_ERROR: + return "Error"; + case SEQ_SFX: + return "SFX"; + case SEQ_VOICE: + return "Voice"; + case SEQ_INSTRUMENT: + return "Instrument"; + case SEQ_BGM_CUSTOM: + return "Custom"; + default: + return "No Sequence Type"; + } +} + +ImVec4 GetSequenceTypeColor(SeqType type) { + switch (type) { + case SEQ_BGM_WORLD: + return ImVec4(0.0f, 0.2f, 0.0f, 1.0f); + case SEQ_BGM_EVENT: + return ImVec4(0.3f, 0.0f, 0.15f, 1.0f); + case SEQ_BGM_BATTLE: + return ImVec4(0.2f, 0.07f, 0.0f, 1.0f); + case SEQ_OCARINA: + return ImVec4(0.0f, 0.0f, 0.4f, 1.0f); + case SEQ_FANFARE: + return ImVec4(0.3f, 0.0f, 0.3f, 1.0f); + case SEQ_SFX: + return ImVec4(0.4f, 0.33f, 0.0f, 1.0f); + case SEQ_VOICE: + return ImVec4(0.3f, 0.42f, 0.09f, 1.0f); + case SEQ_INSTRUMENT: + return ImVec4(0.0f, 0.25f, 0.5f, 1.0f); + case SEQ_BGM_CUSTOM: + return ImVec4(0.9f, 0.0f, 0.9f, 1.0f); + default: + return ImVec4(1.0f, 0.0f, 0.0f, 1.0f); + } +} + +void DrawTypeChip(SeqType type) { + ImGui::BeginDisabled(); + ImGui::PushStyleColor(ImGuiCol_Button, GetSequenceTypeColor(type)); + ImGui::SmallButton(GetSequenceTypeName(type)); + ImGui::PopStyleColor(); + ImGui::EndDisabled(); +} + +void AudioEditorRegisterOnSceneInitHook() { + // BENTODO implement this + // GameInteractor::Instance->RegisterGameHook([](int16_t sceneNum) { + // if (CVarGetInteger(CVAR_AUDIO("RandomizeAllOnNewScene"), 0)) { + // AudioEditor_RandomizeAll(); + // } + //}); +} + +void AudioEditor::InitElement() { + AudioEditorRegisterOnSceneInitHook(); +} + +void AudioEditor::DrawElement() { + AudioCollection::Instance->InitializeShufflePool(); + + float buttonSegments = ImGui::GetContentRegionAvail().x / 4; + if (ImGui::Button("Randomize All Groups", ImVec2(buttonSegments, 30.0f))) { + AudioEditor_RandomizeAll(); + } + UIWidgets::Tooltip("Randomizes all unlocked music and sound effects across tab groups"); + ImGui::SameLine(); + if (ImGui::Button("Reset All Groups", ImVec2(buttonSegments, 30.0f))) { + AudioEditor_ResetAll(); + } + UIWidgets::Tooltip("Resets all unlocked music and sound effects across tab groups"); + ImGui::SameLine(); + if (ImGui::Button("Lock All Groups", ImVec2(buttonSegments, 30.0f))) { + AudioEditor_LockAll(); + } + UIWidgets::Tooltip("Locks all music and sound effects across tab groups"); + ImGui::SameLine(); + if (ImGui::Button("Unlock All Groups", ImVec2(buttonSegments, 30.0f))) { + AudioEditor_UnlockAll(); + } + UIWidgets::Tooltip("Unlocks all music and sound effects across tab groups"); + + if (ImGui::BeginTabBar("SfxContextTabBar", ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)) { + if (ImGui::BeginTabItem("Background Music")) { + Draw_SfxTab("backgroundMusic", SEQ_BGM_WORLD); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Fanfares")) { + Draw_SfxTab("fanfares", SEQ_FANFARE); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Events")) { + Draw_SfxTab("event", SEQ_BGM_EVENT); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Battle Music")) { + Draw_SfxTab("battleMusic", SEQ_BGM_BATTLE); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Ocarina")) { + Draw_SfxTab("ocarina", SEQ_OCARINA); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Instrument")) { + Draw_SfxTab("instrument", SEQ_INSTRUMENT); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Songs")) { + Draw_SfxTab("songs", SEQ_BGM_SONGS); + ImGui::EndTabItem(); + } +#if 0 + if (ImGui::BeginTabItem("Sound Effects")) { + Draw_SfxTab("sfx", SEQ_SFX); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Voices")) { + Draw_SfxTab("voice", SEQ_VOICE); + ImGui::EndTabItem(); +#endif + static ImVec2 cellPadding(8.0f, 8.0f); +#if 0 + if (ImGui::BeginTabItem("Options")) { + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, cellPadding); + ImGui::BeginTable("Options", 1, ImGuiTableFlags_SizingStretchSame); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::BeginChild("SfxOptions", ImVec2(0, -8))) { + ImGui::PushItemWidth(-FLT_MIN); + UIWidgets::EnhancementCheckbox("Disable Enemy Proximity Music", CVAR_AUDIO("EnemyBGMDisable")); + UIWidgets::InsertHelpHoverText( + "Disables the music change when getting close to enemies. Useful for hearing " + "your custom music for each scene more often."); + UIWidgets::EnhancementCheckbox("Disable Leading Music in Lost Woods", + CVAR_AUDIO("LostWoodsConsistentVolume")); + UIWidgets::InsertHelpHoverText( + "Disables the volume shifting in the Lost Woods. Useful for hearing " + "your custom music in the Lost Woods if you don't need the navigation assitance " + "the volume changing provides. If toggling this while in the Lost Woods, reload " + "the area for the effect to kick in."); + UIWidgets::EnhancementCheckbox("Display Sequence Name on Overlay", CVAR_AUDIO("SeqNameOverlay")); + UIWidgets::InsertHelpHoverText( + "Displays the name of the current sequence in the corner of the screen whenever a new sequence " + "is loaded to the main sequence player (does not apply to fanfares or enemy BGM)."); + ImGui::SameLine(); + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x); + UIWidgets::EnhancementSliderInt("Overlay Duration: %d seconds", "##SeqNameOverlayDuration", + CVAR_AUDIO("SeqNameOverlayDuration"), 1, 10, "", 5); + ImGui::PopItemWidth(); + ImGui::NewLine(); + ImGui::PopItemWidth(); + UIWidgets::EnhancementSliderFloat("Link's voice pitch multiplier: %.1f %%", "##linkVoiceFreqMultiplier", + CVAR_AUDIO("LinkVoiceFreqMultiplier"), 0.4, 2.5, "", 1.0, true, true); + ImGui::SameLine(); + const std::string resetButton = "Reset##linkVoiceFreqMultiplier"; + if (ImGui::Button(resetButton.c_str())) { + CVarSetFloat(CVAR_AUDIO("LinkVoiceFreqMultiplier"), 1.0f); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + + ImGui::NewLine(); + UIWidgets::EnhancementCheckbox("Randomize All Music and Sound Effects on New Scene", + CVAR_AUDIO("RandomizeAllOnNewScene")); + UIWidgets::Tooltip( + "Enables randomizing all unlocked music and sound effects when you enter a new scene."); + + ImGui::NewLine(); + ImGui::PushItemWidth(-FLT_MIN); + UIWidgets::PaddedSeparator(); + UIWidgets::PaddedText("The following options are experimental and may cause music\nto sound odd or " + "have other undesireable effects."); + UIWidgets::EnhancementCheckbox("Lower Octaves of Unplayable High Notes", + CVAR_AUDIO("ExperimentalOctaveDrop")); + UIWidgets::Tooltip( + "Some custom sequences may have notes that are too high for the game's audio " + "engine to play. Enabling this checkbox will cause these notes to drop a " + "couple of octaves so they can still harmonize with the other notes of the " + "sequence."); + ImGui::PopItemWidth(); + } + ImGui::EndChild(); + ImGui::EndTable(); + ImGui::PopStyleVar(1); + ImGui::EndTabItem(); + } +#endif + + static bool excludeTabOpen = false; + if (ImGui::BeginTabItem("Audio Shuffle Pool Management")) { + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, cellPadding); + if (!excludeTabOpen) { + excludeTabOpen = true; + } + + static std::map showType{ { SEQ_BGM_WORLD, true }, { SEQ_BGM_EVENT, true }, + { SEQ_BGM_BATTLE, true }, { SEQ_OCARINA, true }, + { SEQ_FANFARE, true }, { SEQ_SFX, true }, + { SEQ_VOICE, true }, { SEQ_INSTRUMENT, true }, + { SEQ_BGM_CUSTOM, true } }; + + // make temporary sets because removing from the set we're iterating through crashes ImGui + std::set seqsToInclude = {}; + std::set seqsToExclude = {}; + + static ImGuiTextFilter sequenceSearch; + sequenceSearch.Draw("Filter (inc,-exc)", 490.0f); + ImGui::SameLine(); + if (ImGui::Button("Exclude All")) { + for (auto seqInfo : AudioCollection::Instance->GetIncludedSequences()) { + if (sequenceSearch.PassFilter(seqInfo->label.c_str()) && showType[seqInfo->category]) { + seqsToExclude.insert(seqInfo); + } + } + } + ImGui::SameLine(); + if (ImGui::Button("Include All")) { + for (auto seqInfo : AudioCollection::Instance->GetExcludedSequences()) { + if (sequenceSearch.PassFilter(seqInfo->label.c_str()) && showType[seqInfo->category]) { + seqsToInclude.insert(seqInfo); + } + } + } + + ImGui::BeginTable("sequenceTypes", 9, + ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_Borders); + + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Header, GetSequenceTypeColor(SEQ_BGM_WORLD)); + ImGui::Selectable(GetSequenceTypeName(SEQ_BGM_WORLD), &showType[SEQ_BGM_WORLD]); + ImGui::PopStyleColor(1); + + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Header, GetSequenceTypeColor(SEQ_BGM_EVENT)); + ImGui::Selectable(GetSequenceTypeName(SEQ_BGM_EVENT), &showType[SEQ_BGM_EVENT]); + ImGui::PopStyleColor(1); + + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Header, GetSequenceTypeColor(SEQ_BGM_BATTLE)); + ImGui::Selectable(GetSequenceTypeName(SEQ_BGM_BATTLE), &showType[SEQ_BGM_BATTLE]); + ImGui::PopStyleColor(1); + + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Header, GetSequenceTypeColor(SEQ_OCARINA)); + ImGui::Selectable(GetSequenceTypeName(SEQ_OCARINA), &showType[SEQ_OCARINA]); + ImGui::PopStyleColor(1); + + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Header, GetSequenceTypeColor(SEQ_FANFARE)); + ImGui::Selectable(GetSequenceTypeName(SEQ_FANFARE), &showType[SEQ_FANFARE]); + ImGui::PopStyleColor(1); + + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Header, GetSequenceTypeColor(SEQ_SFX)); + ImGui::Selectable(GetSequenceTypeName(SEQ_SFX), &showType[SEQ_SFX]); + ImGui::PopStyleColor(1); + + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Header, GetSequenceTypeColor(SEQ_VOICE)); + ImGui::Selectable(GetSequenceTypeName(SEQ_VOICE), &showType[SEQ_VOICE]); + ImGui::PopStyleColor(1); + + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Header, GetSequenceTypeColor(SEQ_INSTRUMENT)); + ImGui::Selectable(GetSequenceTypeName(SEQ_INSTRUMENT), &showType[SEQ_INSTRUMENT]); + ImGui::PopStyleColor(1); + + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Header, GetSequenceTypeColor(SEQ_BGM_CUSTOM)); + ImGui::Selectable(GetSequenceTypeName(SEQ_BGM_CUSTOM), &showType[SEQ_BGM_CUSTOM]); + ImGui::PopStyleColor(1); + + ImGui::EndTable(); + + if (ImGui::BeginTable("tableAllSequences", 2, ImGuiTableFlags_BordersH | ImGuiTableFlags_BordersV)) { + ImGui::TableSetupColumn("Included", ImGuiTableColumnFlags_WidthStretch, 200.0f); + ImGui::TableSetupColumn("Excluded", ImGuiTableColumnFlags_WidthStretch, 200.0f); + ImGui::TableHeadersRow(); + ImGui::TableNextRow(); + + // COLUMN 1 - INCLUDED SEQUENCES + ImGui::TableNextColumn(); + + ImGui::BeginChild("ChildIncludedSequences", ImVec2(0, -8)); + for (auto seqInfo : AudioCollection::Instance->GetIncludedSequences()) { + if (sequenceSearch.PassFilter(seqInfo->label.c_str()) && showType[seqInfo->category]) { + if (ImGui::Button(std::string(ICON_FA_TIMES "##" + seqInfo->sfxKey).c_str())) { + seqsToExclude.insert(seqInfo); + } + ImGui::SameLine(); + DrawPreviewButton(seqInfo->sequenceId, seqInfo->sfxKey, seqInfo->category); + ImGui::SameLine(); + DrawTypeChip(seqInfo->category); + ImGui::SameLine(); + ImGui::Text("%s", seqInfo->label.c_str()); + } + } + ImGui::EndChild(); + + // remove the sequences we added to the temp set + for (auto seqInfo : seqsToExclude) { + AudioCollection::Instance->RemoveFromShufflePool(seqInfo); + } + + // COLUMN 2 - EXCLUDED SEQUENCES + ImGui::TableNextColumn(); + + ImGui::BeginChild("ChildExcludedSequences", ImVec2(0, -8)); + for (auto seqInfo : AudioCollection::Instance->GetExcludedSequences()) { + if (sequenceSearch.PassFilter(seqInfo->label.c_str()) && showType[seqInfo->category]) { + if (ImGui::Button(std::string(ICON_FA_PLUS "##" + seqInfo->sfxKey).c_str())) { + seqsToInclude.insert(seqInfo); + } + ImGui::SameLine(); + DrawPreviewButton(seqInfo->sequenceId, seqInfo->sfxKey, seqInfo->category); + ImGui::SameLine(); + DrawTypeChip(seqInfo->category); + ImGui::SameLine(); + ImGui::Text("%s", seqInfo->label.c_str()); + } + } + ImGui::EndChild(); + + // add the sequences we added to the temp set + for (auto seqInfo : seqsToInclude) { + AudioCollection::Instance->AddToShufflePool(seqInfo); + } + + ImGui::EndTable(); + } + ImGui::PopStyleVar(1); + ImGui::EndTabItem(); + } else { + excludeTabOpen = false; + } + + ImGui::EndTabBar(); + } +} + +static std::array allTypes = { SEQ_BGM_WORLD, SEQ_BGM_EVENT, SEQ_BGM_BATTLE, SEQ_OCARINA, + SEQ_FANFARE, SEQ_INSTRUMENT, SEQ_SFX, SEQ_VOICE }; + +void AudioEditor_RandomizeAll() { + for (auto type : allTypes) { + RandomizeGroup(type); + } + + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ReplayCurrentBGM(); +} + +void AudioEditor_RandomizeGroup(SeqType group) { + RandomizeGroup(group); + + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ReplayCurrentBGM(); +} + +void AudioEditor_ResetAll() { + for (auto type : allTypes) { + ResetGroup(AudioCollection::Instance->GetAllSequences(), type); + } + + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ReplayCurrentBGM(); +} + +void AudioEditor_ResetGroup(SeqType group) { + ResetGroup(AudioCollection::Instance->GetAllSequences(), group); + + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ReplayCurrentBGM(); +} + +void AudioEditor_LockAll() { + for (auto type : allTypes) { + LockGroup(AudioCollection::Instance->GetAllSequences(), type); + } + + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); +} + +void AudioEditor_UnlockAll() { + for (auto type : allTypes) { + UnlockGroup(AudioCollection::Instance->GetAllSequences(), type); + } + + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); +} \ No newline at end of file diff --git a/mm/2s2h/Enhancements/Audio/AudioEditor.h b/mm/2s2h/Enhancements/Audio/AudioEditor.h new file mode 100644 index 0000000000..702620bae9 --- /dev/null +++ b/mm/2s2h/Enhancements/Audio/AudioEditor.h @@ -0,0 +1,44 @@ +#pragma once +#include "stdint.h" + +#include "libultraship/libultra/types.h" +#ifdef __cplusplus +#include "window/gui/Gui.h" +#include "window/gui/GuiWindow.h" +#include "AudioCollection.h" + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif + +class AudioEditor : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + + void DrawElement() override; + void InitElement() override; + void UpdateElement() override{}; + ~AudioEditor(){}; + + private: + // void DrawPreviewButton(uint16_t sequenceId, std::string sfxKey, SeqType sequenceType); + // void Draw_SfxTab(const std::string& tabId, SeqType type); + uint16_t mPlayingSeq = 0; +}; + +void AudioEditor_RandomizeAll(); +void AudioEditor_RandomizeGroup(SeqType group); +void AudioEditor_ResetAll(); +void AudioEditor_ResetGroup(SeqType group); +void AudioEditor_LockAll(); +void AudioEditor_UnlockAll(); + +extern "C" { +#endif + +u16 AudioEditor_GetReplacementSeq(u16 seqId); +u16 AudioEditor_GetOriginalSeq(u16 seqId); + +#ifdef __cplusplus +} +#endif diff --git a/mm/2s2h/Enhancements/Audio/AudioSeqQueue.cpp b/mm/2s2h/Enhancements/Audio/AudioSeqQueue.cpp new file mode 100644 index 0000000000..8c36409bb9 --- /dev/null +++ b/mm/2s2h/Enhancements/Audio/AudioSeqQueue.cpp @@ -0,0 +1,41 @@ +#if 0 +#include "AudioSeqQueue.h" + +#include "resource/type/AudioSequence.h" +#include "Context.h" + +static SafeQueue audioQueue; + +extern "C" { + +void AudioQueue_Enqueue(char* seqId) { + audioQueue.enqueue(seqId); +} + +char* AudioQueue_Dequeue(void) { + return audioQueue.dequeue(); +} + +int32_t AudioQueue_IsEmpty(void) { + return audioQueue.isEmpty(); +} + +void AudioQueue_GetSeqInfo(const char* path, uint64_t* numFrames, uint32_t* numChannels, uint32_t* sampleRate, + int16_t** sampleData) { + auto seqData = + static_pointer_cast(Ship::Context::GetInstance()->GetResourceManager()->LoadResource(path)); + if (numFrames != nullptr) { + *numFrames = seqData->sequence.seqDataSize / sizeof(uint16_t); + } + + if (numChannels != nullptr) { + *numChannels = seqData->numChannels; + } + + if (numChannels != nullptr) { + *sampleRate = seqData->sampleRate; + } + *sampleData = (s16*)seqData->sequence.seqData; +} +} +#endif \ No newline at end of file diff --git a/mm/2s2h/Enhancements/Audio/AudioSeqQueue.h b/mm/2s2h/Enhancements/Audio/AudioSeqQueue.h new file mode 100644 index 0000000000..1076d6ac80 --- /dev/null +++ b/mm/2s2h/Enhancements/Audio/AudioSeqQueue.h @@ -0,0 +1,66 @@ +#ifndef AUDIO_SEQ_QUEUE_H +#define AUDIO_SEQ_QUEUE_H + +#include + +#ifdef __cplusplus + +#include +#include +#include + +// A threadsafe-queue. +template class SafeQueue { + public: + SafeQueue(void) : q(), m(), c() { + } + + ~SafeQueue(void) { + } + + // Add an element to the queue. + void enqueue(T t) { + std::lock_guard lock(m); + q.push(t); + c.notify_one(); + } + + // Get the "front"-element. + // If the queue is empty, wait till a element is avaiable. + T dequeue(void) { + std::unique_lock lock(m); + while (q.empty()) { + // release lock as long as the wait and reaquire it afterwards. + c.wait(lock); + } + T val = q.front(); + q.pop(); + return val; + } + + int32_t isEmpty() { + return static_cast(q.empty()); + } + + private: + std::queue q; + mutable std::mutex m; + std::condition_variable c; +}; +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +void AudioQueue_Enqueue(char* seqId); +char* AudioQueue_Dequeue(void); +int32_t AudioQueue_IsEmpty(void); +void AudioQueue_GetSeqInfo(const char* path, uint64_t* numFrames, uint32_t* numChannels, uint32_t* sampleRate, + int16_t** sampleData); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/mm/2s2h/Enhancements/Cutscenes/SkipToFileSelect.cpp b/mm/2s2h/Enhancements/Cutscenes/SkipToFileSelect.cpp index 58d612bfa8..c77bf3761b 100644 --- a/mm/2s2h/Enhancements/Cutscenes/SkipToFileSelect.cpp +++ b/mm/2s2h/Enhancements/Cutscenes/SkipToFileSelect.cpp @@ -21,7 +21,7 @@ void RegisterSkipToFileSelect() { // We need to call it manually before file select creates RNG values for new saves Rand_Seed(osGetTime()); // Normally called on console logo screen - gSaveContext.seqId = (u8)NA_BGM_DISABLED; + gSaveContext.seqId = NA_BGM_DISABLED; gSaveContext.ambienceId = AMBIENCE_ID_DISABLED; gSaveContext.gameMode = GAMEMODE_TITLE_SCREEN; diff --git a/mm/2s2h/mixer.c b/mm/2s2h/mixer.c index 2e6e45cc94..6364fa5dda 100644 --- a/mm/2s2h/mixer.c +++ b/mm/2s2h/mixer.c @@ -1,10 +1,11 @@ +//! This file is always optimized by a rule in the CMakeList. This is done because the SIMD functions are very large +//! when unoptimized and clang does not allow optimizing a single function. #include #include #include #include #include "mixer.h" - #ifndef __clang__ #pragma GCC optimize("unroll-loops") #endif @@ -68,6 +69,9 @@ static int16_t resample_table[64][4] = { { 0xffdf, 0x0d46, 0x66ad, 0x0c39 } }; +static void aMixImplSSE2(uint16_t count, int16_t gain, uint16_t in_addr, uint16_t out_addr); +static void aMixImplNEON(uint16_t count, int16_t gain, uint16_t in_addr, uint16_t out_addr); + static inline int16_t clamp16(int32_t v) { if (v < -0x8000) { return -0x8000; @@ -101,6 +105,33 @@ void aLoadBufferImpl(const void* source_addr, uint16_t dest_addr, uint16_t nbyte #endif } +#include +#include + +void aOPUSdecImpl(void* source_addr, uint16_t dest_addr, uint16_t nbytes, struct OggOpusFile** decState, int32_t pos, + uint32_t size) { + int readSamples = 0; + if (*decState == NULL) { + *decState = op_open_memory(source_addr, size, NULL); + } + op_pcm_seek(*decState, pos); + int ret = op_read(*decState, BUF_S16(dest_addr), nbytes / 2, NULL); + if (ret < 0) { + return; + } + readSamples += ret; + while (readSamples < nbytes / 2) { + ret = op_read(*decState, BUF_S16(dest_addr + readSamples * 2), (nbytes - readSamples * 2) / 2, NULL); + if (ret == 0) + break; + readSamples += ret; + } +} + +void aOPUSFree(struct OggOpusFile* opusFile) { + op_free(opusFile); +} + void aSaveBufferImpl(uint16_t source_addr, int16_t* dest_addr, uint16_t nbytes) { memcpy(dest_addr, BUF_S16(source_addr), ROUND_DOWN_16(nbytes)); } @@ -298,7 +329,7 @@ void aEnvMixerImpl(uint16_t in_addr, uint16_t n_samples, bool swap_reverb, bool } while (n > 0); } -void aMixImpl(uint16_t count, int16_t gain, uint16_t in_addr, uint16_t out_addr) { +static void aMixImplRef(uint16_t count, int16_t gain, uint16_t in_addr, uint16_t out_addr) { int nbytes = ROUND_UP_32(ROUND_DOWN_16(count << 4)); int16_t* in = BUF_S16(in_addr); int16_t* out = BUF_S16(out_addr); @@ -325,6 +356,16 @@ void aMixImpl(uint16_t count, int16_t gain, uint16_t in_addr, uint16_t out_addr) } } +void aMixImpl(uint16_t count, int16_t gain, uint16_t in_addr, uint16_t out_addr) { +#if defined(__SSE2__) || defined(_M_AMD64) + aMixImplSSE2(count, gain, in_addr, out_addr); +#elif defined(__ARM_NEON) + aMixImplNEON(count, gain, in_addr, out_addr); +#else + aMixImplRef(count, gain, in_addr, out_addr); +#endif +} + void aS8DecImpl(uint8_t flags, ADPCM_STATE state) { uint8_t* in = BUF_U8(rspa.in); int16_t* out = BUF_S16(rspa.out); @@ -557,3 +598,222 @@ void aUnkCmd19Impl(uint8_t f, uint16_t count, uint16_t out_addr, uint16_t in_add nbytes -= 32 * sizeof(int16_t); } while (nbytes > 0); } + +// From here on there are SIMD implementations of the various mixer functions. +// A note about FORCE_OPTIMIZE... +// Compilers don't handle SIMD code well when not optimizing. It is unlikely that this code will need to be debugged +// outside of specific audio issues. We can assume it should always be optimized. + +// SIMD operations expect aligned data +#include "align_asset_macro.h" + +#if defined(__SSE2__) || defined(_M_AMD64) +#include + +static const ALIGN_ASSET(16) int16_t x7fff[8] = { + 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, +}; +static const ALIGN_ASSET(16) int32_t x4000[4] = { + 0x4000, + 0x4000, + 0x4000, + 0x4000, +}; + +static void aMixImplSSE2(uint16_t count, int16_t gain, uint16_t in_addr, uint16_t out_addr) { + int nbytes = ROUND_UP_32(ROUND_DOWN_16(count << 4)); + int16_t* in = BUF_S16(in_addr); + int16_t* out = BUF_S16(out_addr); + int i; + int32_t sample; + if (gain == -0x8000) { + while (nbytes > 0) { + for (unsigned int i = 0; i < 2; i++) { + __m128i outVec = _mm_loadu_si128((__m128i*)out); + __m128i inVec = _mm_loadu_si128((__m128i*)in); + __m128i subsVec = _mm_subs_epi16(outVec, inVec); + _mm_storeu_si128(out, subsVec); + nbytes -= 8 * sizeof(int16_t); + in += 8; + out += 8; + } + } + } + // Load constants into vectors from aligned memory. + __m128i x7fffVec = _mm_load_si128((__m128i*)x7fff); + __m128i x4000Vec = _mm_load_si128((__m128i*)x4000); + __m128i gainVec = _mm_set1_epi16(gain); + while (nbytes > 0) { + for (unsigned int i = 0; i < 2; i++) { + // Load input and output data into vectors + __m128i outVec = _mm_loadu_si128((__m128i*)out); + __m128i inVec = _mm_loadu_si128((__m128i*)in); + // Multiply `out` by `0x7FFF` producing 32 bit results, and store the upper and lower bits in each vector. + // Equivalent to `out[0..8] * 0x7FFF` + __m128i outx7fffLoVec = _mm_mullo_epi16(outVec, x7fffVec); + __m128i outx7fffHiVec = _mm_mulhi_epi16(outVec, x7fffVec); + // Same as above but for in and gain. Equivalent to `in[0..8] * gain` + __m128i inxGainLoVec = _mm_mullo_epi16(inVec, gainVec); + __m128i inxGainHiVec = _mm_mulhi_epi16(inVec, gainVec); + + // Interleave the lo and hi bits into one 32 bit value for each vector element. + // So now we have 4 full elements in each vector instead of 8 half elements. + outx7fffLoVec = _mm_unpacklo_epi16(outx7fffLoVec, outx7fffHiVec); + outx7fffHiVec = _mm_unpackhi_epi16(outx7fffLoVec, outx7fffHiVec); + inxGainLoVec = _mm_unpacklo_epi16(inxGainLoVec, inxGainHiVec); + inxGainHiVec = _mm_unpackhi_epi16(inxGainLoVec, inxGainHiVec); + + // Now we have 4 32 bit elements. Continue the calculaton per the reference implementation. + // We already did out + 0x7fff and in * gain. + // *out * 0x7fff + *in++ * gain is the final result of these two calculations. + __m128i addLoVec = _mm_add_epi32(outx7fffLoVec, inxGainLoVec); + __m128i addHiVec = _mm_add_epi32(outx7fffHiVec, inxGainHiVec); + // Add 0x4000 to each element + addLoVec = _mm_add_epi32(addLoVec, x4000Vec); + addHiVec = _mm_add_epi32(addHiVec, x4000Vec); + // Shift each element over by 15 + __m128i shiftedLoVec = _mm_srai_epi32(addLoVec, 15); + __m128i shiftedHiVec = _mm_srai_epi32(addHiVec, 15); + // Convert each 32 bit element to 16 bit with saturation (clamp) and store in `outVec` + outVec = _mm_packs_epi32(shiftedLoVec, shiftedHiVec); + // Write the final vector back to memory + // The final calculation is ((out[0..8] * 0x7fff + in[0..8] * gain) + 0x4000) >> 15; + _mm_storeu_si128((__m128i*)out, outVec); + + in += 8; + out += 8; + nbytes -= 8 * sizeof(int16_t); + } + } +} +#endif +#if defined(__ARM_NEON) +#include +static const int32_t x4000Arr[4] = { 0x4000, 0x4000, 0x4000, 0x4000 }; +void aMixImplNEON(uint16_t count, int16_t gain, uint16_t in_addr, uint16_t out_addr) { + int nbytes = ROUND_UP_32(ROUND_DOWN_16(count << 4)); + int16_t* in = BUF_S16(in_addr); + int16_t* out = BUF_S16(out_addr); + int i; + int32_t sample; + + if (gain == -0x8000) { + while (nbytes > 0) { + for (unsigned int i = 0; i < 2; i++) { + int16x8_t outVec = vld1q_s16(out); + int16x8_t inVec = vld1q_s16(in); + int16x8_t subVec = vqsubq_s16(outVec, inVec); + vst1q_s16(out, subVec); + nbytes -= 8 * sizeof(int16_t); + out += 8; + in += 8; + } + } + } + int16x8_t gainVec = vdupq_n_s16(gain); + int32x4_t x4000Vec = vld1q_s32(x4000Arr); + while (nbytes > 0) { + for (unsigned int i = 0; i < 2; i++) { + // for (i = 0; i < 16; i++) { + int16x8_t outVec = vld1q_s16(out); + int16x8_t inVec = vld1q_s16(in); + int16x4_t outLoVec = vget_low_s16(outVec); + int16x8_t outLoVec2 = vcombine_s16(outLoVec, outLoVec); + int16x4_t inLoVec = vget_low_s16(inVec); + int16x8_t inLoVec2 = vcombine_s16(inLoVec, inLoVec); + int32x4_t outX7fffHiVec = vmull_high_n_s16(outVec, 0x7FFF); + int32x4_t outX7fffLoVec = vmull_high_n_s16(outLoVec2, 0x7FFF); + + int32x4_t inGainLoVec = vmull_high_s16(inLoVec2, gainVec); + int32x4_t inGainHiVec = vmull_high_s16(inVec, gainVec); + int32x4_t addVecLo = vaddq_s32(outX7fffLoVec, inGainLoVec); + int32x4_t addVecHi = vaddq_s32(outX7fffHiVec, inGainHiVec); + addVecHi = vaddq_s32(addVecHi, x4000Vec); + addVecLo = vaddq_s32(addVecLo, x4000Vec); + int32x4_t shiftVecHi = vshrq_n_s32(addVecHi, 15); + int32x4_t shiftVecLo = vshrq_n_s32(addVecLo, 15); + int16x4_t shiftedNarrowHiVec = vqmovn_s32(shiftVecHi); + int16x4_t shiftedNarrowLoVec = vqmovn_s32(shiftVecLo); + vst1_s16(out, shiftedNarrowLoVec); + out += 4; + vst1_s16(out, shiftedNarrowHiVec); + // int16x8_t finalVec = vcombine_s16(shiftedNarrowLoVec, shiftedNarrowHiVec); + // vst1q_s16(out, finalVec); + out += 4; + in += 8; + + nbytes -= 8 * sizeof(int16_t); + } + } +} +#endif + +#if 0 +static const ALIGN_ASSET(32) int16_t x7fff[16] = { 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF,}; +static const ALIGN_ASSET(32) int32_t x4000[8] = { 0x4000, 0x4000, 0x4000, 0x4000, 0x4000, 0x4000, 0x4000, 0x4000}; + +#pragma GCC target("avx2") +// AVX2 version of the SSE2 implementation above. AVX2 wasn't released until 2014 and I don't have a good way of checking for it at compile time. +void aMixImpl256(uint16_t count, int16_t gain, uint16_t in_addr, uint16_t out_addr) { + int nbytes = ROUND_UP_32(ROUND_DOWN_16(count << 4)); + int16_t* in = BUF_S16(in_addr); + int16_t* out = BUF_S16(out_addr); + int i; + int32_t sample; + if (gain == -0x8000) { + while (nbytes > 0) { + __m256i outVec =_mm256_loadu_si256((__m256*)out); + __m256i inVec =_mm256_loadu_si256((__m256i*)in); + __m256i subsVec =_mm256_subs_epi16(outVec, inVec); + _mm256_storeu_si256(out, subsVec); + in += 16; + out += 16; + nbytes -= 16 * sizeof(int16_t); + } + } + // Load constants into vectors from aligned memory. + __m256i x7fffVec = _mm256_load_si256((__m256i*)x7fff); + __m256i x4000Vec = _mm256_load_si256((__m256i*)x4000); + __m256i gainVec = _mm256_set1_epi16(gain); + while (nbytes > 0) { + // Load input and output data into vectors + __m256i outVec = _mm256_loadu_si256((__m256i*)out); + __m256i inVec = _mm256_loadu_si256((__m256i*)in); + // Multiply `out` by `0x7FFF` producing 32 bit results, and store the upper and lower bits in each vector. + // Equivalent to `out[0..16] * 0x7FFF` + __m256i outx7fffLoVec = _mm256_mullo_epi16(outVec, x7fffVec); + __m256i outx7fffHiVec = _mm256_mulhi_epi16(outVec, x7fffVec); + // Same as above but for in and gain. Equivalent to `in[0..16] * gain` + __m256i inxGainLoVec = _mm256_mullo_epi16(inVec, gainVec); + __m256i inxGainHiVec = _mm256_mulhi_epi16(inVec, gainVec); + + // Interleave the lo and hi bits into one 32 bit value for each vector element. + // So now we have 8 full elements in each vector instead of 16 half elements. + outx7fffLoVec = _mm256_unpacklo_epi16(outx7fffLoVec, outx7fffHiVec); + outx7fffHiVec = _mm256_unpackhi_epi16(outx7fffLoVec, outx7fffHiVec); + inxGainLoVec = _mm256_unpacklo_epi16(inxGainLoVec, inxGainHiVec); + inxGainHiVec = _mm256_unpackhi_epi16(inxGainLoVec, inxGainHiVec); + + // Now we have 8 32 bit elements. Continue the calculaton per the reference implementation. + // We already did out + 0x7fff and in * gain. + // *out * 0x7fff + *in++ * gain is the final result of these two calculations. + __m256i addLoVec = _mm256_add_epi32(outx7fffLoVec, inxGainLoVec); + __m256i addHiVec = _mm256_add_epi32(outx7fffHiVec, inxGainHiVec); + // Add 0x4000 to each element + addLoVec = _mm256_add_epi32(addLoVec, x4000Vec); + addHiVec = _mm256_add_epi32(addHiVec, x4000Vec); + // Shift each element over by 15 + __m256i shiftedLoVec = _mm256_srai_epi32(addLoVec, 15); + __m256i shiftedHiVec = _mm256_srai_epi32(addHiVec, 15); + // Convert each 32 bit element to 16 bit with saturation (clamp) and store in `outVec` + outVec = _mm256_packs_epi32(shiftedLoVec, shiftedHiVec); + // Write the final vector back to memory + // The final calculation is ((out[0..16] * 0x7fff + in[0..16] * gain) + 0x4000) >> 15; + _mm256_storeu_si256((__m256i*)out, outVec); + + in += 16; + out += 16; + nbytes -= 16 * sizeof(int16_t); + } +} +#endif diff --git a/mm/2s2h/mixer.h b/mm/2s2h/mixer.h index 6bde9621ee..048135e448 100644 --- a/mm/2s2h/mixer.h +++ b/mm/2s2h/mixer.h @@ -57,6 +57,11 @@ void aHiLoGainImpl(uint8_t g, uint16_t count, uint16_t addr); void aUnkCmd3Impl(uint16_t a, uint16_t b, uint16_t c); void aUnkCmd19Impl(uint8_t f, uint16_t count, uint16_t out_addr, uint16_t in_addr); +struct OggOpusFile; + +void aOPUSdecImpl(void* source_addr, uint16_t dest_addr, uint16_t nbytes, struct OggOpusFile** decState, int32_t pos, + uint32_t size); + #define aSegment(pkt, s, b) \ do { \ } while (0) diff --git a/mm/2s2h/resource/importer/AudioSampleFactory.cpp b/mm/2s2h/resource/importer/AudioSampleFactory.cpp index 3884365384..e06bd04445 100644 --- a/mm/2s2h/resource/importer/AudioSampleFactory.cpp +++ b/mm/2s2h/resource/importer/AudioSampleFactory.cpp @@ -1,6 +1,189 @@ #include "2s2h/resource/importer/AudioSampleFactory.h" #include "2s2h/resource/type/AudioSample.h" -#include "spdlog/spdlog.h" +#include "2s2h/resource/importer/AudioSoundFontFactory.h" +#include "audio/soundfont.h" +#include "Context.h" +#include "resource/archive/Archive.h" +#define DR_WAV_IMPLEMENTATION +#include + +#define DR_MP3_IMPLEMENTATION +#include + +#define DR_FLAC_IMPLEMENTATION +#include + +#include +#include +#include "vorbis/vorbisfile.h" +#include + +struct OggFileData { + void* data; + size_t pos; + size_t size; +}; + +typedef enum class OggType { + None = -1, + Vorbis, + Opus, +} OggType; + +static size_t VorbisReadCallback(void* out, size_t size, size_t elems, void* src) { + OggFileData* data = static_cast(src); + size_t toRead = size * elems; + + if (toRead > data->size - data->pos) { + toRead = data->size - data->pos; + } + + memcpy(out, static_cast(data->data) + data->pos, toRead); + data->pos += toRead; + + return toRead / size; +} + +static int VorbisSeekCallback(void* src, ogg_int64_t pos, int whence) { + OggFileData* data = static_cast(src); + size_t newPos; + + switch (whence) { + case SEEK_SET: + newPos = pos; + break; + case SEEK_CUR: + newPos = data->pos + pos; + break; + case SEEK_END: + newPos = data->size + pos; + break; + default: + return -1; + } + if (newPos > data->size) { + return -1; + } + data->pos = newPos; + return 0; +} + +static int VorbisCloseCallback([[maybe_unused]] void* src) { + return 0; +} + +static long VorbisTellCallback(void* src) { + OggFileData* data = static_cast(src); + return data->pos; +} + +static const ov_callbacks vorbisCallbacks = { + VorbisReadCallback, + VorbisSeekCallback, + VorbisCloseCallback, + VorbisTellCallback, +}; + +static OggType GetOggType(OggFileData* data) { + ogg_sync_state oy; + ogg_stream_state os; + ogg_page og; + ogg_packet op; + OggType type; + // The first page as the header information, containing, among other things, what kind of data this ogg holds. + ogg_sync_init(&oy); + char* buffer = ogg_sync_buffer(&oy, 4096); + VorbisReadCallback(buffer, 4096, 1, data); + ogg_sync_wrote(&oy, 4096); + + ogg_sync_pageout(&oy, &og); + ogg_stream_init(&os, ogg_page_serialno(&og)); + ogg_stream_pagein(&os, &og); + ogg_stream_packetout(&os, &op); + + // Can't use strmp because op.packet isn't a null terminated string. + if (memcmp((char*)op.packet, "\x01vorbis", 7) == 0) { + type = OggType::Vorbis; + } else if (memcmp((char*)op.packet, "OpusHead", 8) == 0) { + type = OggType::Opus; + } else { + type = OggType::None; + } + ogg_stream_clear(&os); + ogg_sync_clear(&oy); + return type; +} + +static void Mp3DecoderWorker(std::shared_ptr audioSample, std::shared_ptr sampleFile) { + drmp3 mp3; + drwav_uint64 numFrames; + drmp3_bool32 ret = + drmp3_init_memory(&mp3, sampleFile->Buffer.get()->data(), sampleFile->Buffer.get()->size(), nullptr); + numFrames = drmp3_get_pcm_frame_count(&mp3); + drwav_uint64 channels = mp3.channels; + drwav_uint64 sampleRate = mp3.sampleRate; + + audioSample->sample.sampleAddr = new uint8_t[numFrames * channels * 2]; + drmp3_read_pcm_frames_s16(&mp3, numFrames, (int16_t*)audioSample->sample.sampleAddr); +} + +static void FlacDecoderWorker(std::shared_ptr audioSample, std::shared_ptr sampleFile) { + drflac* flac = drflac_open_memory(sampleFile->Buffer.get()->data(), sampleFile->Buffer.get()->size(), nullptr); + drflac_uint64 numFrames = flac->totalPCMFrameCount; + audioSample->sample.sampleAddr = new uint8_t[numFrames * flac->channels * 2]; + drflac_read_pcm_frames_s16(flac, numFrames, (int16_t*)audioSample->sample.sampleAddr); + drflac_close(flac); +} + +static void OggDecoderWorker(std::shared_ptr audioSample, std::shared_ptr sampleFile) { + OggVorbis_File vf; + char dataBuff[4096]; + long read = 0; + size_t pos = 0; + + OggFileData fileData = { + .data = sampleFile->Buffer.get()->data(), + .pos = 0, + .size = sampleFile->Buffer.get()->size(), + }; + switch (GetOggType(&fileData)) { + case OggType::Vorbis: { + // Getting the type advanced the position. We are going to use a different library to decode the file which + // assumes the file starts at 0 + fileData.pos = 0; + int ret = ov_open_callbacks(&fileData, &vf, nullptr, 0, vorbisCallbacks); + + vorbis_info* vi = ov_info(&vf, -1); + + uint64_t numFrames = ov_pcm_total(&vf, -1); + uint64_t sampleRate = vi->rate; + uint64_t numChannels = vi->channels; + int bitStream = 0; + size_t toRead = numFrames * numChannels * 2; + audioSample->sample.sampleAddr = new uint8_t[toRead]; + do { + read = ov_read(&vf, dataBuff, 4096, 0, 2, 1, &bitStream); + memcpy(audioSample->sample.sampleAddr + pos, dataBuff, read); + pos += read; + } while (read != 0); + ov_clear(&vf); + break; + } + case OggType::Opus: { + // OPUS encoded data is decoded by the audio driver. + audioSample->sample.codec = CODEC_OPUS; + audioSample->sample.sampleAddr = new uint8_t[sampleFile->Buffer.get()->size()]; + memcpy(audioSample->sample.sampleAddr, sampleFile->Buffer.get()->data(), sampleFile->Buffer.get()->size()); + break; + } + case OggType::None: { + char buff[2048]; + snprintf(buff, 2048, "Ogg file %s is not Vorbis or OPUS", sampleFile->InitData->Path.c_str()); + throw std::runtime_error(buff); + break; + } + } +} namespace SOH { std::shared_ptr ResourceFactoryBinaryAudioSampleV2::ReadResource(std::shared_ptr file) { @@ -14,108 +197,160 @@ std::shared_ptr ResourceFactoryBinaryAudioSampleV2::ReadResourc audioSample->sample.codec = reader->ReadUByte(); audioSample->sample.medium = reader->ReadUByte(); audioSample->sample.unk_bit26 = reader->ReadUByte(); - audioSample->sample.unk_bit25 = reader->ReadUByte(); + audioSample->sample.isRelocated = reader->ReadUByte(); audioSample->sample.size = reader->ReadUInt32(); - audioSample->audioSampleData.reserve(audioSample->sample.size); + audioSample->sample.sampleAddr = new uint8_t[audioSample->sample.size]; for (uint32_t i = 0; i < audioSample->sample.size; i++) { - audioSample->audioSampleData.push_back(reader->ReadUByte()); + audioSample->sample.sampleAddr[i] = reader->ReadUByte(); } - audioSample->sample.sampleAddr = audioSample->audioSampleData.data(); audioSample->loop.start = reader->ReadUInt32(); audioSample->loop.end = reader->ReadUInt32(); audioSample->loop.count = reader->ReadUInt32(); - audioSample->loopStateCount = reader->ReadUInt32(); + // This always seems to be 16. Can it be removed in V3? + uint32_t loopStateCount = reader->ReadUInt32(); for (int i = 0; i < 16; i++) { audioSample->loop.state[i] = 0; } - for (uint32_t i = 0; i < audioSample->loopStateCount; i++) { + for (uint32_t i = 0; i < loopStateCount; i++) { audioSample->loop.state[i] = reader->ReadInt16(); } audioSample->sample.loop = &audioSample->loop; audioSample->book.order = reader->ReadInt32(); audioSample->book.npredictors = reader->ReadInt32(); - audioSample->bookDataCount = reader->ReadUInt32(); + uint32_t bookDataCount = reader->ReadUInt32(); - audioSample->bookData.reserve(audioSample->bookDataCount); - for (uint32_t i = 0; i < audioSample->bookDataCount; i++) { - audioSample->bookData.push_back(reader->ReadInt16()); + audioSample->book.book = new int16_t[bookDataCount]; + + for (uint32_t i = 0; i < bookDataCount; i++) { + audioSample->book.book[i] = reader->ReadInt16(); } - audioSample->book.book = audioSample->bookData.data(); audioSample->sample.book = &audioSample->book; return audioSample; } -} // namespace SOH -/* -in ResourceMgr_LoadAudioSample we used to have --------------- - if (cachedCustomSFs.find(path) != cachedCustomSFs.end()) - return cachedCustomSFs[path]; +std::shared_ptr ResourceFactoryXMLAudioSampleV0::ReadResource(std::shared_ptr file) { + if (!FileHasValidFormatAndReader(file)) { + return nullptr; + } - SoundFontSample* cSample = ReadCustomSample(path); + auto audioSample = std::make_shared(file->InitData); + auto child = std::get>(file->Reader)->FirstChildElement(); + std::shared_ptr initData = std::make_shared(); + const char* customFormatStr = child->Attribute("CustomFormat"); + memset(&audioSample->sample, 0, sizeof(audioSample->sample)); + audioSample->sample.isRelocated = 0; + audioSample->sample.codec = CodecStrToInt(child->Attribute("Codec"), file->InitData->Path.c_str()); + audioSample->sample.medium = + ResourceFactoryXMLSoundFontV0::MediumStrToInt(child->Attribute("Medium"), file->InitData->Path.c_str()); + audioSample->sample.unk_bit26 = child->IntAttribute("bit26"); - if (cSample != nullptr) - return cSample; --------------- -before the rest of the standard sample reading, this is the ReadCustomSample code we used to have + tinyxml2::XMLElement* loopRoot = child->FirstChildElement("ADPCMLoop"); + if (loopRoot != nullptr) { + size_t i = 0; + audioSample->loop.start = loopRoot->UnsignedAttribute("Start"); + audioSample->loop.end = loopRoot->UnsignedAttribute("End"); + audioSample->loop.count = loopRoot->UnsignedAttribute("Count"); + tinyxml2::XMLElement* predictor = loopRoot->FirstChildElement("Predictor"); + while (predictor != nullptr) { + audioSample->loop.state[i++] = predictor->IntAttribute("State"); + predictor = predictor->NextSiblingElement(); + } + } -extern "C" SoundFontSample* ReadCustomSample(const char* path) { + tinyxml2::XMLElement* bookRoot = child->FirstChildElement("ADPCMBook"); + if (bookRoot != nullptr) { + size_t i = 0; + audioSample->book.npredictors = bookRoot->IntAttribute("Npredictors"); + audioSample->book.order = bookRoot->IntAttribute("Order"); + tinyxml2::XMLElement* book = bookRoot->FirstChildElement("Book"); + size_t numBooks = audioSample->book.npredictors * audioSample->book.order * 8; + audioSample->book.book = new int16_t[numBooks]; + while (book != nullptr) { + audioSample->book.book[i++] = book->IntAttribute("Page"); + book = book->NextSiblingElement(); + } + audioSample->sample.book = &audioSample->book; + } - if (!ExtensionCache.contains(path)) - return nullptr; + audioSample->sample.loop = &audioSample->loop; + size_t size = child->Int64Attribute("Size"); + audioSample->sample.size = size; + + const char* path = child->Attribute("Path"); + initData->Path = path; + initData->IsCustom = false; + initData->ByteOrder = Ship::Endianness::Native; + auto sampleFile = Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->LoadFile(path, initData); + audioSample->sample.fileSize = sampleFile->Buffer.get()->size(); + if (customFormatStr != nullptr) { + // Compressed files can take a really long time to decode (~250ms per). + // This worked when we tested it (09/04/2024) (Works on my machine) + if (strcmp(customFormatStr, "wav") == 0) { + drwav wav; + drwav_uint64 numFrames; + + drwav_bool32 ret = + drwav_init_memory(&wav, sampleFile->Buffer.get()->data(), sampleFile->Buffer.get()->size(), nullptr); + + drwav_get_length_in_pcm_frames(&wav, &numFrames); + + audioSample->tuning = (wav.sampleRate * wav.channels) / 32000.0f; + audioSample->sample.sampleAddr = new uint8_t[numFrames * wav.channels * 2]; - ExtensionEntry entry = ExtensionCache[path]; - - auto sampleRaw = Ship::Context::GetInstance()->GetResourceManager()->LoadFile(entry.path); - uint32_t* strem = (uint32_t*)sampleRaw->Buffer.get(); - uint8_t* strem2 = (uint8_t*)strem; - - SoundFontSample* sampleC = new SoundFontSample; - - if (entry.ext == "wav") { - drwav_uint32 channels; - drwav_uint32 sampleRate; - drwav_uint64 totalPcm; - drmp3_int16* pcmData = - drwav_open_memory_and_read_pcm_frames_s16(strem2, sampleRaw->BufferSize, &channels, &sampleRate, &totalPcm, -NULL); sampleC->size = totalPcm; sampleC->sampleAddr = (uint8_t*)pcmData; sampleC->codec = CODEC_S16; - - sampleC->loop = new AdpcmLoop; - sampleC->loop->start = 0; - sampleC->loop->end = sampleC->size - 1; - sampleC->loop->count = 0; - sampleC->sampleRateMagicValue = 'RIFF'; - sampleC->sampleRate = sampleRate; - - cachedCustomSFs[path] = sampleC; - return sampleC; - } else if (entry.ext == "mp3") { - drmp3_config mp3Info; - drmp3_uint64 totalPcm; - drmp3_int16* pcmData = - drmp3_open_memory_and_read_pcm_frames_s16(strem2, sampleRaw->BufferSize, &mp3Info, &totalPcm, NULL); - - sampleC->size = totalPcm * mp3Info.channels * sizeof(short); - sampleC->sampleAddr = (uint8_t*)pcmData; - sampleC->codec = CODEC_S16; - - sampleC->loop = new AdpcmLoop; - sampleC->loop->start = 0; - sampleC->loop->end = sampleC->size; - sampleC->loop->count = 0; - sampleC->sampleRateMagicValue = 'RIFF'; - sampleC->sampleRate = mp3Info.sampleRate; - - cachedCustomSFs[path] = sampleC; - return sampleC; - } - - return nullptr; + drwav_read_pcm_frames_s16(&wav, numFrames, (int16_t*)audioSample->sample.sampleAddr); + return audioSample; + } else if (strcmp(customFormatStr, "mp3") == 0) { + std::thread fileDecoderThread = std::thread(Mp3DecoderWorker, audioSample, sampleFile); + fileDecoderThread.detach(); + return audioSample; + } else if (strcmp(customFormatStr, "ogg") == 0) { + std::thread fileDecoderThread = std::thread(OggDecoderWorker, audioSample, sampleFile); + fileDecoderThread.detach(); + return audioSample; + } else if (strcmp(customFormatStr, "flac") == 0) { + std::thread fileDecoderThread = std::thread(FlacDecoderWorker, audioSample, sampleFile); + fileDecoderThread.detach(); + return audioSample; + } + } + // Not a normal streamed sample. Fallback to the original ADPCM sample to be decoded by the audio engine. + audioSample->sample.sampleAddr = new uint8_t[size]; + // Can't use memcpy due to endianness issues. + for (uint32_t i = 0; i < size; i++) { + audioSample->sample.sampleAddr[i] = sampleFile->Buffer.get()->data()[i]; + } + + return audioSample; } -*/ +uint8_t ResourceFactoryXMLAudioSampleV0::CodecStrToInt(const char* str, const char* file) { + if (strcmp("ADPCM", str) == 0) { + return CODEC_ADPCM; + } else if (strcmp("S8", str) == 0) { + return CODEC_S8; + } else if (strcmp("S16MEM", str) == 0) { + return CODEC_S16_INMEMORY; + } else if (strcmp("ADPCMSMALL", str) == 0) { + return CODEC_SMALL_ADPCM; + } else if (strcmp("REVERB", str) == 0) { + return CODEC_REVERB; + } else if (strcmp("S16", str) == 0) { + return CODEC_S16; + } else if (strcmp("UNK6", str) == 0) { + return CODEC_UNK6; + } else if (strcmp("UNK7", str) == 0) { + return CODEC_UNK7; + } else { + char buff[2048]; + snprintf(buff, 2048, + "Invalid codec in %s. Got %s, expected ADPCM, S8, S16MEM, ADPCMSMALL, REVERB, S16, UNK6, UNK7.", file, + str); + throw std::runtime_error(buff); + } +} +} // namespace SOH diff --git a/mm/2s2h/resource/importer/AudioSampleFactory.h b/mm/2s2h/resource/importer/AudioSampleFactory.h index 372e8a3104..2fdc69b009 100644 --- a/mm/2s2h/resource/importer/AudioSampleFactory.h +++ b/mm/2s2h/resource/importer/AudioSampleFactory.h @@ -2,10 +2,20 @@ #include "Resource.h" #include "ResourceFactoryBinary.h" +#include "ResourceFactoryXML.h" namespace SOH { class ResourceFactoryBinaryAudioSampleV2 : public Ship::ResourceFactoryBinary { public: std::shared_ptr ReadResource(std::shared_ptr file) override; }; + +class ResourceFactoryXMLAudioSampleV0 : public Ship::ResourceFactoryXML { + public: + std::shared_ptr ReadResource(std::shared_ptr file) override; + + private: + static uint8_t CodecStrToInt(const char* str, const char* file); +}; + } // namespace SOH diff --git a/mm/2s2h/resource/importer/AudioSequenceFactory.cpp b/mm/2s2h/resource/importer/AudioSequenceFactory.cpp index 5b8899501e..e51d751047 100644 --- a/mm/2s2h/resource/importer/AudioSequenceFactory.cpp +++ b/mm/2s2h/resource/importer/AudioSequenceFactory.cpp @@ -1,6 +1,10 @@ #include "2s2h/resource/importer/AudioSequenceFactory.h" #include "2s2h/resource/type/AudioSequence.h" -#include "spdlog/spdlog.h" +#include "2s2h/resource/importer/AudioSoundFontFactory.h" +#include "Context.h" +#include "resource/archive/Archive.h" +#include "BinaryWriter.h" +#include namespace SOH { std::shared_ptr ResourceFactoryBinaryAudioSequenceV2::ReadResource(std::shared_ptr file) { @@ -11,12 +15,11 @@ std::shared_ptr ResourceFactoryBinaryAudioSequenceV2::ReadResou auto audioSequence = std::make_shared(file->InitData); auto reader = std::get>(file->Reader); - audioSequence->sequence.seqDataSize = reader->ReadInt32(); - audioSequence->sequenceData.reserve(audioSequence->sequence.seqDataSize); + audioSequence->sequence.seqDataSize = reader->ReadUInt32(); + audioSequence->sequence.seqData = new char[audioSequence->sequence.seqDataSize]; for (uint32_t i = 0; i < audioSequence->sequence.seqDataSize; i++) { - audioSequence->sequenceData.push_back(reader->ReadChar()); + audioSequence->sequence.seqData[i] = reader->ReadChar(); } - audioSequence->sequence.seqData = audioSequence->sequenceData.data(); audioSequence->sequence.seqNumber = reader->ReadUByte(); audioSequence->sequence.medium = reader->ReadUByte(); @@ -32,4 +35,357 @@ std::shared_ptr ResourceFactoryBinaryAudioSequenceV2::ReadResou return audioSequence; } + +template static void WriteInsnOneArg(Ship::BinaryWriter* writer, uint8_t opcode, T arg) { + static_assert(std::is_fundamental::value); + writer->Write(opcode); + writer->Write(arg); +} + +template +static void WriteInsnTwoArg(Ship::BinaryWriter* writer, uint8_t opcode, T1 arg1, T2 arg2) { + static_assert(std::is_fundamental::value && std::is_fundamental::value); + writer->Write(opcode); + writer->Write(arg1); + writer->Write(arg2); +} + +template +static void WriteInsnThreeArg(Ship::BinaryWriter* writer, uint8_t opcode, T1 arg1, T2 arg2, T3 arg3) { + static_assert(std::is_fundamental::value && std::is_fundamental::value); + writer->Write(opcode); + writer->Write(arg1); + writer->Write(arg2); + writer->Write(arg3); +} + +static void WriteInsnNoArg(Ship::BinaryWriter* writer, uint8_t opcode) { + writer->Write(opcode); +} + +static void WriteLegato(Ship::BinaryWriter* writer) { + WriteInsnNoArg(writer, 0xC4); +} + +static void WriteNoLegato(Ship::BinaryWriter* writer) { + WriteInsnNoArg(writer, 0xC5); +} + +static void WriteMuteBhv(Ship::BinaryWriter* writer, uint8_t arg) { + WriteInsnOneArg(writer, 0xD3, arg); +} + +static void WriteMuteScale(Ship::BinaryWriter* writer, uint8_t arg) { + WriteInsnOneArg(writer, 0xD5, arg); +} + +static void WriteInitchan(Ship::BinaryWriter* writer, uint16_t channels) { + WriteInsnOneArg(writer, 0xD7, channels); +} + +static void WriteLdchan(Ship::BinaryWriter* writer, uint8_t channel, uint16_t offset) { + WriteInsnOneArg(writer, 0x90 | channel, offset); +} + +static void WriteVolSHeader(Ship::BinaryWriter* writer, uint8_t vol) { + WriteInsnOneArg(writer, 0xDB, vol); +} + +static void WriteVolCHeader(Ship::BinaryWriter* writer, uint8_t vol) { + WriteInsnOneArg(writer, 0xDF, vol); +} + +static void WriteTempo(Ship::BinaryWriter* writer, uint8_t tempo) { + WriteInsnOneArg(writer, 0xDD, tempo); +} + +static void WriteJump(Ship::BinaryWriter* writer, uint16_t offset) { + WriteInsnOneArg(writer, 0xFB, offset); +} + +static void WriteDisablecan(Ship::BinaryWriter* writer, uint16_t channels) { + WriteInsnOneArg(writer, 0xD6, channels); +} + +static void WriteNoshort(Ship::BinaryWriter* writer) { + WriteInsnNoArg(writer, 0xC4); +} + +static void WriteLdlayer(Ship::BinaryWriter* writer, uint8_t layer, uint16_t offset) { + WriteInsnOneArg(writer, 0x88 | layer, offset); +} + +static void WritePan(Ship::BinaryWriter* writer, uint8_t pan) { + WriteInsnOneArg(writer, 0xDD, pan); +} + +static void WriteBend(Ship::BinaryWriter* writer, uint8_t bend) { + WriteInsnOneArg(writer, 0xD3, bend); +} + +static void WriteInstrument(Ship::BinaryWriter* writer, uint8_t instrument) { + WriteInsnOneArg(writer, 0xC1, instrument); +} + +static void WriteTranspose(Ship::BinaryWriter* writer, int8_t transpose) { + WriteInsnOneArg(writer, 0xC2, transpose); +} + +static void WriteDelay(Ship::BinaryWriter* writer, uint16_t delay) { + if (delay > 0x7F) { + WriteInsnOneArg(writer, 0xFD, static_cast(delay | 0x8000)); + } else { + WriteInsnOneArg(writer, 0xFD, static_cast(delay)); + } +} + +template static void WriteLDelay(Ship::BinaryWriter* writer, T delay) { + WriteInsnOneArg(writer, 0xC0, delay); +} + +template static void WriteNotedv(Ship::BinaryWriter* writer, uint8_t note, T delay, uint8_t velocity) { + WriteInsnTwoArg(writer, note, delay, velocity); +} + +static void WriteNotedvg(Ship::BinaryWriter* writer, uint8_t note, uint16_t delay, uint8_t velocity, uint8_t gateTime) { + if (delay > 0x7F) { + WriteInsnThreeArg(writer, note, static_cast(delay | 0x8000), velocity, gateTime); + } else { + WriteInsnThreeArg(writer, note, static_cast(delay), velocity, gateTime); + } +} + +static void WriteMonoSingleSeq(Ship::BinaryWriter* writer, uint16_t delay, uint8_t tempo, bool looped) { + uint16_t channelStart; + uint16_t channelPlaceholderOff; + uint16_t loopPoint; + uint16_t layerPlaceholderOff; + uint16_t layerStart; + if (looped) { + delay = 0x7FFF; + } + // Write seq header + + // These two values are always the same in OOT and MM + WriteMuteBhv(writer, 0x20); + WriteMuteScale(writer, 0x32); + + // We only have one channel + WriteInitchan(writer, 0b11); + // Store the current position so we can write the address of the channel when we are ready. + channelPlaceholderOff = writer->GetBaseAddress(); + // Store the current position so we can loop here after the song ends. + loopPoint = writer->GetBaseAddress(); + WriteLdchan(writer, 0, 0); // Fill in the actual address later + + WriteVolSHeader(writer, 127); // Max volume + WriteTempo(writer, tempo); + + WriteDelay(writer, delay); + if (looped) { + WriteJump(writer, loopPoint); + } + WriteDisablecan(writer, 0b11); + writer->Write(static_cast(0xFF)); + + // Fill in the ldchan from before + channelStart = writer->GetBaseAddress(); + writer->Seek(channelPlaceholderOff, Ship::SeekOffsetType::Start); + WriteLdchan(writer, 0, channelStart); + writer->Seek(channelStart, Ship::SeekOffsetType::Start); + + // Channel header + layerPlaceholderOff = writer->GetBaseAddress(); + WriteNoshort(writer); + WriteLdlayer(writer, 0, 0); + WritePan(writer, 64); + WriteVolCHeader(writer, 127); // Max volume + WriteBend(writer, 0); + WriteInstrument(writer, 0); + WriteDelay(writer, delay); + writer->Write(static_cast(0xFF)); + + layerStart = writer->GetBaseAddress(); + writer->Seek(layerPlaceholderOff, Ship::SeekOffsetType::Start); + WriteLdlayer(writer, 0, layerStart); + writer->Seek(layerStart, Ship::SeekOffsetType::Start); + + // Note layer + WriteLegato(writer); + WriteNotedvg(writer, 39, 0x7FFF - 1, static_cast(0x7F), static_cast(1)); + writer->Write(static_cast(0xFF)); +} + +static void WriteStereoSingleSeq(Ship::BinaryWriter* writer, uint16_t delay, uint8_t tempo, bool looped) { + uint16_t lChannelStart; + uint16_t rChannelStart; + uint16_t channelPlaceholderOff; + uint16_t loopPoint; + uint16_t lLayerPlaceholderOff; + uint16_t rLayerPlaceholderOff; + uint16_t lLayerOffset; + uint16_t rLayerOffset; + + uint16_t layerStart; + // Write seq header + if (looped) { + delay = 0x7FFF; + } + // These two values are always the same in OOT and MM + WriteMuteBhv(writer, 0x20); + WriteMuteScale(writer, 0x32); + + // We only have one channel + WriteInitchan(writer, 0b11); + // Store the current position so we can write the address of the channel when we are ready. + channelPlaceholderOff = writer->GetBaseAddress(); + // Store the current position so we can loop here after the song ends. + loopPoint = writer->GetBaseAddress(); + // Left note channel + WriteLdchan(writer, 0, 0); // Fill in the actual address later + // Right note channel + WriteLdchan(writer, 1, 0); // Fill in the actual address later + + WriteVolSHeader(writer, 127); // Max volume + WriteTempo(writer, tempo); + + WriteDelay(writer, delay); + if (looped) { + WriteJump(writer, loopPoint); + } + WriteDisablecan(writer, 0b11); + writer->Write(static_cast(0xFF)); + + lChannelStart = writer->GetBaseAddress(); + // Left Channel header + WriteNoshort(writer); + lLayerPlaceholderOff = writer->GetBaseAddress(); + WriteLdlayer(writer, 0, 0); + WritePan(writer, 0); + WriteVolCHeader(writer, 127); // Max volume + WriteBend(writer, 0); + WriteInstrument(writer, 0); + WriteDelay(writer, delay); + writer->Write(static_cast(0xFF)); + + rChannelStart = writer->GetBaseAddress(); + // Right Channel header + WriteNoshort(writer); + rLayerPlaceholderOff = writer->GetBaseAddress(); + WriteLdlayer(writer, 1, 0); + WritePan(writer, 127); + WriteVolCHeader(writer, 127); // Max volume + WriteBend(writer, 0); + WriteInstrument(writer, 1); + WriteDelay(writer, delay); + writer->Write(static_cast(0xFF)); + uint16_t placeHolder = writer->GetBaseAddress(); + writer->Seek(channelPlaceholderOff, Ship::SeekOffsetType::Start); + WriteLdchan(writer, 0, lChannelStart); + WriteLdchan(writer, 1, rChannelStart); + writer->Seek(placeHolder, Ship::SeekOffsetType::Start); + + // Left Note layer + lLayerOffset = writer->GetBaseAddress(); + WriteLegato(writer); + WriteNotedvg(writer, 39, 0x7FFF - 1, static_cast(0x7F), static_cast(1)); + writer->Write(static_cast(0xFF)); + + // Right Note layer + rLayerOffset = writer->GetBaseAddress(); + WriteLegato(writer); + WriteNotedvg(writer, 39, 0x7FFF - 1, static_cast(0x7F), static_cast(1)); + writer->Write(static_cast(0xFF)); + + writer->Seek(lLayerPlaceholderOff, Ship::SeekOffsetType::Start); + WriteLdlayer(writer, 0, lLayerOffset); + writer->Seek(rLayerPlaceholderOff, Ship::SeekOffsetType::Start); + WriteLdlayer(writer, 1, rLayerOffset); +} + +std::shared_ptr ResourceFactoryXMLAudioSequenceV0::ReadResource(std::shared_ptr file) { + if (!FileHasValidFormatAndReader(file)) { + return nullptr; + } + + auto sequence = std::make_shared(file->InitData); + auto child = std::get>(file->Reader)->FirstChildElement(); + unsigned int i = 0; + std::shared_ptr initData = std::make_shared(); + + sequence->sequence.medium = + ResourceFactoryXMLSoundFontV0::MediumStrToInt(child->Attribute("Medium"), file->InitData->Path.c_str()); + sequence->sequence.cachePolicy = + ResourceFactoryXMLSoundFontV0::CachePolicyToInt(child->Attribute("CachePolicy"), file->InitData->Path.c_str()); + sequence->sequence.seqDataSize = child->IntAttribute("Size"); + sequence->sequence.seqNumber = child->IntAttribute("Index"); + bool streamed = child->BoolAttribute("Streamed"); + + memset(sequence->sequence.fonts, 0, sizeof(sequence->sequence.fonts)); + + tinyxml2::XMLElement* fontsElement = child->FirstChildElement(); + tinyxml2::XMLElement* fontElement = fontsElement->FirstChildElement(); + while (fontElement != nullptr) { + sequence->sequence.fonts[i] = fontElement->IntAttribute("FontIdx"); + fontElement = fontElement->NextSiblingElement(); + i++; + } + sequence->sequence.numFonts = i; + + const char* path = child->Attribute("Path"); + std::shared_ptr seqFile; + if (path != nullptr) { + initData->Path = path; + initData->IsCustom = false; + initData->ByteOrder = Ship::Endianness::Native; + seqFile = Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->LoadFile(path, initData); + } + + if (!streamed) { + sequence->sequence.seqDataSize = seqFile->Buffer.get()->size(); + sequence->sequence.seqData = new char[seqFile->Buffer.get()->size()]; + memcpy(sequence->sequence.seqData, seqFile->Buffer.get()->data(), seqFile->Buffer.get()->size()); + } else { + // setting numFonts to -1 tells the game's audio engine the sound font to used is CRC64 encoded in the font + // indicies. + sequence->sequence.numFonts = -1; + if (path != nullptr) { + sequence->sequence.seqDataSize = seqFile->Buffer.get()->size(); + sequence->sequence.seqData = new char[seqFile->Buffer.get()->size()]; + memcpy(sequence->sequence.seqData, seqFile->Buffer.get()->data(), seqFile->Buffer.get()->size()); + } else { + unsigned int length = child->UnsignedAttribute("Length"); + bool looped = child->BoolAttribute("Looped", true); + bool stereo = child->BoolAttribute("Stereo", false); + Ship::BinaryWriter writer = Ship::BinaryWriter(); + writer.SetEndianness(Ship::Endianness::Big); + + // 1 second worth of ticks can be found by using `ticks = 60 / (bpm * 48)` + // Get the number of ticks per second and then divide the length by this number to get the number of ticks + // for the song. + constexpr uint8_t TEMPO = 1; + constexpr float TEMPO_F = TEMPO; + // Use floats for this first calculation so we can round up + float delayF = length / (60.0f / (TEMPO_F * 48.0f)); + // Convert to u16. This way this value is encoded changes depending on the value. + // It can be at most 0xFFFF so store it in a u16 for now. + uint16_t delay; + if (delayF >= 65535.0f) { + delay = 0x7FFF; + } else { + delay = delayF; + } + if (stereo) { + WriteStereoSingleSeq(&writer, delay, TEMPO, looped); + } else { + WriteMonoSingleSeq(&writer, delay, TEMPO, looped); + } + sequence->sequence.seqDataSize = writer.ToVector().size(); + sequence->sequence.seqData = new char[sequence->sequence.seqDataSize]; + memcpy(sequence->sequence.seqData, writer.ToVector().data(), sequence->sequence.seqDataSize); + } + } + + return sequence; +} } // namespace SOH diff --git a/mm/2s2h/resource/importer/AudioSequenceFactory.h b/mm/2s2h/resource/importer/AudioSequenceFactory.h index 35fd1ebef5..68b4439b73 100644 --- a/mm/2s2h/resource/importer/AudioSequenceFactory.h +++ b/mm/2s2h/resource/importer/AudioSequenceFactory.h @@ -2,10 +2,17 @@ #include "Resource.h" #include "ResourceFactoryBinary.h" +#include "ResourceFactoryXML.h" namespace SOH { class ResourceFactoryBinaryAudioSequenceV2 : public Ship::ResourceFactoryBinary { public: std::shared_ptr ReadResource(std::shared_ptr file) override; }; + +class ResourceFactoryXMLAudioSequenceV0 : public Ship::ResourceFactoryXML { + public: + std::shared_ptr ReadResource(std::shared_ptr file) override; +}; + } // namespace SOH diff --git a/mm/2s2h/resource/importer/AudioSoundFontFactory.cpp b/mm/2s2h/resource/importer/AudioSoundFontFactory.cpp index 39cc6f47fe..ad8e87a5e9 100644 --- a/mm/2s2h/resource/importer/AudioSoundFontFactory.cpp +++ b/mm/2s2h/resource/importer/AudioSoundFontFactory.cpp @@ -1,7 +1,9 @@ #include "2s2h/resource/importer/AudioSoundFontFactory.h" #include "2s2h/resource/type/AudioSoundFont.h" -#include "spdlog/spdlog.h" -#include "libultraship/libultraship.h" +#include "audio/soundfont.h" +#include "audio/load.h" +#include "Context.h" +#include "resource/archive/Archive.h" namespace SOH { std::shared_ptr ResourceFactoryBinaryAudioSoundFontV2::ReadResource(std::shared_ptr file) { @@ -33,122 +35,115 @@ std::shared_ptr ResourceFactoryBinaryAudioSoundFontV2::ReadReso audioSoundFont->soundFont.numSfx = soundEffectCount; // 🥁 DRUMS 🥁 - audioSoundFont->drums.reserve(audioSoundFont->soundFont.numDrums); + // audioSoundFont->drums.reserve(audioSoundFont->soundFont.numDrums); audioSoundFont->drumAddresses.reserve(audioSoundFont->soundFont.numDrums); for (uint32_t i = 0; i < audioSoundFont->soundFont.numDrums; i++) { - Drum drum; - drum.releaseRate = reader->ReadUByte(); - drum.pan = reader->ReadUByte(); - drum.loaded = reader->ReadUByte(); - drum.loaded = 0; // this was always getting set to zero in ResourceMgr_LoadAudioSoundFont + Drum* drum = new Drum; + drum->releaseRate = reader->ReadUByte(); + drum->pan = reader->ReadUByte(); + drum->loaded = reader->ReadUByte(); + drum->loaded = 0; // this was always getting set to zero in ResourceMgr_LoadAudioSoundFontByName uint32_t envelopeCount = reader->ReadUInt32(); - audioSoundFont->drumEnvelopeCounts.push_back(envelopeCount); - std::vector drumEnvelopes; - drumEnvelopes.reserve(audioSoundFont->drumEnvelopeCounts[i]); - for (uint32_t j = 0; j < audioSoundFont->drumEnvelopeCounts.back(); j++) { - AdsrEnvelope env; - + drum->envelope = new AdsrEnvelope[envelopeCount]; + for (uint32_t j = 0; j < envelopeCount; j++) { int16_t delay = reader->ReadInt16(); int16_t arg = reader->ReadInt16(); - env.delay = BE16SWAP(delay); - env.arg = BE16SWAP(arg); - - drumEnvelopes.push_back(env); + drum->envelope[j].delay = BE16SWAP(delay); + drum->envelope[j].arg = BE16SWAP(arg); } - audioSoundFont->drumEnvelopeArrays.push_back(drumEnvelopes); - drum.envelope = audioSoundFont->drumEnvelopeArrays.back().data(); bool hasSample = reader->ReadInt8(); std::string sampleFileName = reader->ReadString(); - drum.sound.tuning = reader->ReadFloat(); + drum->sound.tuning = reader->ReadFloat(); if (sampleFileName.empty()) { - drum.sound.sample = nullptr; + drum->sound.sample = nullptr; } else { auto res = Ship::Context::GetInstance()->GetResourceManager()->LoadResourceProcess(sampleFileName.c_str()); - drum.sound.sample = static_cast(res ? res->GetRawPointer() : nullptr); + drum->sound.sample = static_cast(res ? res->GetRawPointer() : nullptr); } - audioSoundFont->drums.push_back(drum); - // BENTODO clean this up in V3. - if (drum.sound.sample == nullptr) { + // audioSoundFont->drums.push_back(drum); + // BENTODO clean this up in V3. + if (drum->sound.sample == nullptr) { + delete[] drum->envelope; + delete drum; audioSoundFont->drumAddresses.push_back(nullptr); } else { - audioSoundFont->drumAddresses.push_back(&audioSoundFont->drums.back()); + audioSoundFont->drumAddresses.push_back(drum); } } audioSoundFont->soundFont.drums = audioSoundFont->drumAddresses.data(); // 🎺🎻🎷🎸🎹 INSTRUMENTS 🎹🎸🎷🎻🎺 - audioSoundFont->instruments.reserve(audioSoundFont->soundFont.numInstruments); for (uint32_t i = 0; i < audioSoundFont->soundFont.numInstruments; i++) { - Instrument instrument; + Instrument* instrument = new Instrument; uint8_t isValidEntry = reader->ReadUByte(); - instrument.loaded = reader->ReadUByte(); - instrument.loaded = 0; // this was always getting set to zero in ResourceMgr_LoadAudioSoundFont + instrument->loaded = reader->ReadUByte(); + instrument->loaded = 0; // this was always getting set to zero in ResourceMgr_LoadAudioSoundFontByName - instrument.normalRangeLo = reader->ReadUByte(); - instrument.normalRangeHi = reader->ReadUByte(); - instrument.releaseRate = reader->ReadUByte(); + instrument->normalRangeLo = reader->ReadUByte(); + instrument->normalRangeHi = reader->ReadUByte(); + instrument->releaseRate = reader->ReadUByte(); uint32_t envelopeCount = reader->ReadInt32(); - audioSoundFont->instrumentEnvelopeCounts.push_back(envelopeCount); - std::vector instrumentEnvelopes; - for (uint32_t j = 0; j < audioSoundFont->instrumentEnvelopeCounts.back(); j++) { - AdsrEnvelope env; + instrument->envelope = new AdsrEnvelope[envelopeCount]; + for (uint32_t j = 0; j < envelopeCount; j++) { int16_t delay = reader->ReadInt16(); int16_t arg = reader->ReadInt16(); - env.delay = BE16SWAP(delay); - env.arg = BE16SWAP(arg); - - instrumentEnvelopes.push_back(env); + instrument->envelope[j].delay = BE16SWAP(delay); + instrument->envelope[j].arg = BE16SWAP(arg); } - audioSoundFont->instrumentEnvelopeArrays.push_back(instrumentEnvelopes); - instrument.envelope = audioSoundFont->instrumentEnvelopeArrays.back().data(); bool hasLowNoteSoundFontEntry = reader->ReadInt8(); if (hasLowNoteSoundFontEntry) { bool hasSampleRef = reader->ReadInt8(); std::string sampleFileName = reader->ReadString(); - instrument.lowNotesSound.tuning = reader->ReadFloat(); + instrument->lowNotesSound.tuning = reader->ReadFloat(); auto res = Ship::Context::GetInstance()->GetResourceManager()->LoadResourceProcess(sampleFileName.c_str()); - instrument.lowNotesSound.sample = static_cast(res ? res->GetRawPointer() : nullptr); + instrument->lowNotesSound.sample = static_cast(res ? res->GetRawPointer() : nullptr); } else { - instrument.lowNotesSound.sample = nullptr; - instrument.lowNotesSound.tuning = 0; + instrument->lowNotesSound.sample = nullptr; + instrument->lowNotesSound.tuning = 0; } bool hasNormalNoteSoundFontEntry = reader->ReadInt8(); if (hasNormalNoteSoundFontEntry) { + // BENTODO remove in V3 bool hasSampleRef = reader->ReadInt8(); std::string sampleFileName = reader->ReadString(); - instrument.normalNotesSound.tuning = reader->ReadFloat(); + instrument->normalNotesSound.tuning = reader->ReadFloat(); auto res = Ship::Context::GetInstance()->GetResourceManager()->LoadResourceProcess(sampleFileName.c_str()); - instrument.normalNotesSound.sample = static_cast(res ? res->GetRawPointer() : nullptr); + instrument->normalNotesSound.sample = static_cast(res ? res->GetRawPointer() : nullptr); } else { - instrument.normalNotesSound.sample = nullptr; - instrument.normalNotesSound.tuning = 0; + instrument->normalNotesSound.sample = nullptr; + instrument->normalNotesSound.tuning = 0; } bool hasHighNoteSoundFontEntry = reader->ReadInt8(); if (hasHighNoteSoundFontEntry) { bool hasSampleRef = reader->ReadInt8(); std::string sampleFileName = reader->ReadString(); - instrument.highNotesSound.tuning = reader->ReadFloat(); + instrument->highNotesSound.tuning = reader->ReadFloat(); auto res = Ship::Context::GetInstance()->GetResourceManager()->LoadResourceProcess(sampleFileName.c_str()); - instrument.highNotesSound.sample = static_cast(res ? res->GetRawPointer() : nullptr); + instrument->highNotesSound.sample = static_cast(res ? res->GetRawPointer() : nullptr); } else { - instrument.highNotesSound.sample = nullptr; - instrument.highNotesSound.tuning = 0; + instrument->highNotesSound.sample = nullptr; + instrument->highNotesSound.tuning = 0; } - audioSoundFont->instruments.push_back(instrument); - audioSoundFont->instrumentAddresses.push_back(isValidEntry ? &audioSoundFont->instruments.back() : nullptr); + if (isValidEntry) { + audioSoundFont->instrumentAddresses.push_back(instrument); + } else { + delete[] instrument->envelope; + delete instrument; + audioSoundFont->instrumentAddresses.push_back(nullptr); + } } audioSoundFont->soundFont.instruments = audioSoundFont->instrumentAddresses.data(); @@ -172,4 +167,302 @@ std::shared_ptr ResourceFactoryBinaryAudioSoundFontV2::ReadReso return audioSoundFont; } + +int8_t ResourceFactoryXMLSoundFontV0::MediumStrToInt(const char* str, const char* file) { + if (!strcmp("Ram", str)) { + return MEDIUM_RAM; + } else if (!strcmp("Unk", str)) { + return MEDIUM_UNK; + } else if (!strcmp("Cart", str)) { + return MEDIUM_CART; + } else if (!strcmp("Disk", str)) { + return MEDIUM_DISK_DRIVE; + // 4 is skipped + } else if (!strcmp("RamUnloaded", str)) { + return MEDIUM_RAM_UNLOADED; + } else { + char buff[2048]; + snprintf(buff, 2048, "Bad medium value in %s. Got %s, expected Ram, Unk, Cart, or Disk.", file, str); + throw std::runtime_error(buff); + } +} + +int8_t ResourceFactoryXMLSoundFontV0::CachePolicyToInt(const char* str, const char* file) { + if (!strcmp("Temporary", str)) { + return CACHE_TEMPORARY; + } else if (!strcmp("Persistent", str)) { + return CACHE_PERSISTENT; + } else if (!strcmp("Either", str)) { + return CACHE_EITHER; + } else if (!strcmp("Permanent", str)) { + return CACHE_PERMANENT; + } else { + char buff[2048]; + snprintf(buff, 2048, + "Bad cache policy value in %s. Got %s, expected Temporary, Persistent, Either, or Permanent.", file, + str); + throw std::runtime_error(buff); + } +} + +void ResourceFactoryXMLSoundFontV0::ParseDrums(AudioSoundFont* soundFont, tinyxml2::XMLElement* element) { + element = element->FirstChildElement(); + // No drums + if (element == nullptr) { + soundFont->soundFont.drums = nullptr; + soundFont->soundFont.numDrums = 0; + return; + } + + do { + int patch = element->IntAttribute("Patches", -1); + Drum* drum; + if (patch != -1) { + drum = soundFont->drumAddresses[patch]; + } else { + drum = new Drum; + } + std::vector envelopes; + drum->releaseRate = element->IntAttribute("ReleaseRate"); + drum->pan = element->IntAttribute("Pan"); + drum->loaded = element->IntAttribute("Loaded"); + drum->sound.tuning = element->FloatAttribute("Tuning"); + const char* sampleStr = element->Attribute("SampleRef"); + + if (sampleStr != nullptr && sampleStr[0] != 0) { + auto res = Ship::Context::GetInstance()->GetResourceManager()->LoadResourceProcess(sampleStr); + drum->sound.sample = static_cast(res ? res->GetRawPointer() : nullptr); + } else { + drum->sound.sample = nullptr; + } + + element = element->FirstChildElement(); + if (!strcmp(element->Name(), "Envelopes")) { + // element = (tinyxml2::XMLElement*)element->FirstChildElement(); + unsigned int envCount = 0; + envelopes = ParseEnvelopes(soundFont, element, &envCount); + element = (tinyxml2::XMLElement*)element->Parent(); + soundFont->drumEnvelopeArrays.push_back(envelopes); + // If we are applying a patch the envelopes are already allocated + // TODO revert this if we enable editing envelopes in a patch + if (patch == -1) { + drum->envelope = new AdsrEnvelope[envelopes.size()]; + } + memcpy(drum->envelope, envelopes.data(), envelopes.size() * sizeof(AdsrEnvelope)); + } else { + drum->envelope = nullptr; + } + + if (drum->sound.sample == nullptr) { + soundFont->drumAddresses.push_back(nullptr); + } else { + soundFont->drumAddresses.push_back(drum); + } + + element = element->NextSiblingElement(); + } while (element != nullptr); + + soundFont->soundFont.numDrums = soundFont->drumAddresses.size(); + soundFont->soundFont.drums = soundFont->drumAddresses.data(); +} + +void ResourceFactoryXMLSoundFontV0::ParseInstruments(AudioSoundFont* soundFont, tinyxml2::XMLElement* element) { + element = element->FirstChildElement(); + if (element == nullptr) { + return; + } + do { + int patch = element->IntAttribute("Patches", -1); + Instrument* instrument; + // Same as drums, if applying a patch, don't re-allocate and clear. + if (patch != -1) { + instrument = soundFont->instrumentAddresses[patch]; + } else { + instrument = new Instrument; + memset(instrument, 0, sizeof(Instrument)); + } + unsigned int envCount = 0; + std::vector envelopes; + + int isValid = element->BoolAttribute("IsValid"); + instrument->loaded = element->IntAttribute("Loaded"); + instrument->normalRangeLo = element->IntAttribute("NormalRangeLo"); + instrument->normalRangeHi = element->IntAttribute("NormalRangeHi"); + instrument->releaseRate = element->IntAttribute("ReleaseRate"); + tinyxml2::XMLElement* instrumentElement = element->FirstChildElement(); + tinyxml2::XMLElement* instrumentElementCopy = instrumentElement; + + if (instrumentElement != nullptr && !strcmp(instrumentElement->Name(), "Envelopes")) { + envelopes = ParseEnvelopes(soundFont, instrumentElement, &envCount); + if (patch == -1) { + instrument->envelope = new AdsrEnvelope[envelopes.size()]; + } + memcpy(instrument->envelope, envelopes.data(), envelopes.size() * sizeof(AdsrEnvelope)); + instrumentElement = instrumentElement->NextSiblingElement(); + } + + if (instrumentElement != nullptr && !strcmp("LowNotesSound", instrumentElement->Name())) { + instrument->lowNotesSound.tuning = instrumentElement->FloatAttribute("Tuning"); + const char* sampleStr = instrumentElement->Attribute("SampleRef"); + if (sampleStr != nullptr && sampleStr[0] != 0) { + std::shared_ptr res = static_pointer_cast( + Ship::Context::GetInstance()->GetResourceManager()->LoadResourceProcess(sampleStr)); + if (res->tuning != -1.0f) { + instrument->lowNotesSound.tuning = res->tuning; + } + instrument->lowNotesSound.sample = static_cast(res ? res->GetRawPointer() : nullptr); + } + instrumentElement = instrumentElement->NextSiblingElement(); + } + + if (instrumentElement != nullptr && !strcmp("NormalNotesSound", instrumentElement->Name())) { + instrument->normalNotesSound.tuning = instrumentElement->FloatAttribute("Tuning"); + const char* sampleStr = instrumentElement->Attribute("SampleRef"); + if (sampleStr != nullptr && sampleStr[0] != 0) { + std::shared_ptr res = static_pointer_cast( + Ship::Context::GetInstance()->GetResourceManager()->LoadResourceProcess(sampleStr)); + if (res->tuning != -1.0f) { + instrument->normalNotesSound.tuning = res->tuning; + } + instrument->normalNotesSound.sample = static_cast(res ? res->GetRawPointer() : nullptr); + } + instrumentElement = instrumentElement->NextSiblingElement(); + } + + if (instrumentElement != nullptr && !strcmp("HighNotesSound", instrumentElement->Name())) { + instrument->highNotesSound.tuning = instrumentElement->FloatAttribute("Tuning"); + const char* sampleStr = instrumentElement->Attribute("SampleRef"); + if (sampleStr != nullptr && sampleStr[0] != 0) { + std::shared_ptr res = static_pointer_cast( + Ship::Context::GetInstance()->GetResourceManager()->LoadResourceProcess(sampleStr)); + if (res->tuning != -1.0f) { + instrument->highNotesSound.tuning = res->tuning; + } + instrument->highNotesSound.sample = static_cast(res ? res->GetRawPointer() : nullptr); + } + instrumentElement = instrumentElement->NextSiblingElement(); + } + // Don't add it to the list if applying a patch + if (patch == -1) { + soundFont->instrumentAddresses.push_back(instrument); + } + element = instrumentElementCopy; + element = (tinyxml2::XMLElement*)element->Parent(); + element = element->NextSiblingElement(); + } while (element != nullptr); + + soundFont->soundFont.instruments = soundFont->instrumentAddresses.data(); + soundFont->soundFont.numInstruments = soundFont->instrumentAddresses.size(); +} + +void ResourceFactoryXMLSoundFontV0::ParseSfxTable(AudioSoundFont* soundFont, tinyxml2::XMLElement* element) { + size_t count = element->IntAttribute("Count"); + + element = element->FirstChildElement(); + + while (element != nullptr) { + int patch = element->IntAttribute("Patches", -1); + + SoundFontSound sound = {}; + + const char* sampleStr = element->Attribute("SampleRef"); + // Insert an empty sound effect. The game assumes the empty slots are + // filled so we can't just skip them + if (sampleStr == nullptr) + goto skip; + + sound.tuning = element->FloatAttribute("Tuning"); + if (sampleStr[0] != 0) { + auto res = static_pointer_cast( + Ship::Context::GetInstance()->GetResourceManager()->LoadResourceProcess(sampleStr)); + if (res->tuning != -1.0f) { + sound.tuning = res->tuning; + } + sound.sample = static_cast(res ? res->GetRawPointer() : nullptr); + } + skip: + element = element->NextSiblingElement(); + if (patch != -1) { + soundFont->soundEffects[patch] = sound; + } else { + soundFont->soundEffects.push_back(sound); + } + } + soundFont->soundFont.soundEffects = soundFont->soundEffects.data(); + soundFont->soundFont.numSfx = soundFont->soundEffects.size(); +} + +std::vector SOH::ResourceFactoryXMLSoundFontV0::ParseEnvelopes(AudioSoundFont* soundFont, + tinyxml2::XMLElement* element, + unsigned int* count) { + std::vector envelopes; + unsigned int total = 0; + element = element->FirstChildElement("Envelope"); + while (element != nullptr) { + AdsrEnvelope env = { + .delay = (s16)element->IntAttribute("Delay"), + .arg = (s16)element->IntAttribute("Arg"), + }; + env.delay = BSWAP16(env.delay); + env.arg = BSWAP16(env.arg); + envelopes.emplace_back(env); + element = element->NextSiblingElement("Envelope"); + total++; + } + *count = total; + return envelopes; +} + +std::shared_ptr ResourceFactoryXMLSoundFontV0::ReadResource(std::shared_ptr file) { + if (!FileHasValidFormatAndReader(file)) { + return nullptr; + } + + auto child = std::get>(file->Reader)->FirstChildElement(); + const char* patch = child->Attribute("Patches"); + std::shared_ptr sf; + std::shared_ptr audioSoundFont; + // If we are patching an existing SF, load the original, otherwise create and clear a new one. + if (patch != nullptr) { + std::string origName = "audio/fonts/"; + origName += patch; + audioSoundFont = dynamic_pointer_cast( + Ship::Context::GetInstance()->GetResourceManager()->LoadResourceProcess(origName)); + } else { + audioSoundFont = std::make_shared(file->InitData); + memset(&audioSoundFont->soundFont, 0, sizeof(audioSoundFont->soundFont)); + } + // Header data + audioSoundFont->soundFont.fntIndex = child->IntAttribute("Num", 0); + + const char* mediumStr = child->Attribute("Medium"); + audioSoundFont->medium = MediumStrToInt(mediumStr, file->InitData->Path.c_str()); + + const char* cachePolicyStr = child->Attribute("CachePolicy"); + audioSoundFont->cachePolicy = CachePolicyToInt(cachePolicyStr, file->InitData->Path.c_str()); + + audioSoundFont->data1 = child->IntAttribute("Data1"); + audioSoundFont->data2 = child->IntAttribute("Data2"); + audioSoundFont->data3 = child->IntAttribute("Data3"); + + audioSoundFont->soundFont.sampleBankId1 = audioSoundFont->data1 >> 8; + audioSoundFont->soundFont.sampleBankId2 = audioSoundFont->data1 & 0xFF; + + child = (tinyxml2::XMLElement*)child->FirstChildElement(); + + while (child != nullptr) { + const char* name = child->Name(); + + if (!strcmp(name, "Drums")) { + ParseDrums(audioSoundFont.get(), child); + } else if (!strcmp(name, "Instruments")) { + ParseInstruments(audioSoundFont.get(), child); + } else if (!strcmp(name, "SfxTable")) { + ParseSfxTable(audioSoundFont.get(), child); + } + child = child->NextSiblingElement(); + } + return audioSoundFont; +} + } // namespace SOH diff --git a/mm/2s2h/resource/importer/AudioSoundFontFactory.h b/mm/2s2h/resource/importer/AudioSoundFontFactory.h index a80b8fe972..a390ab42b7 100644 --- a/mm/2s2h/resource/importer/AudioSoundFontFactory.h +++ b/mm/2s2h/resource/importer/AudioSoundFontFactory.h @@ -2,10 +2,27 @@ #include "Resource.h" #include "ResourceFactoryBinary.h" +#include "ResourceFactoryXML.h" +#include "resource/type/AudioSoundFont.h" namespace SOH { class ResourceFactoryBinaryAudioSoundFontV2 : public Ship::ResourceFactoryBinary { public: std::shared_ptr ReadResource(std::shared_ptr file) override; }; + +class ResourceFactoryXMLSoundFontV0 : public Ship::ResourceFactoryXML { + public: + std::shared_ptr ReadResource(std::shared_ptr file) override; + static int8_t MediumStrToInt(const char* str, const char* file); + static int8_t CachePolicyToInt(const char* str, const char* file); + + private: + void ParseDrums(AudioSoundFont* soundFont, tinyxml2::XMLElement* element); + void ParseInstruments(AudioSoundFont* soundFont, tinyxml2::XMLElement* element); + void ParseSfxTable(AudioSoundFont* soundFont, tinyxml2::XMLElement* element); + std::vector ParseEnvelopes(AudioSoundFont* soundFont, tinyxml2::XMLElement* element, + unsigned int* count); +}; + } // namespace SOH diff --git a/mm/2s2h/resource/type/AudioSample.cpp b/mm/2s2h/resource/type/AudioSample.cpp index 2887658224..b4a5397576 100644 --- a/mm/2s2h/resource/type/AudioSample.cpp +++ b/mm/2s2h/resource/type/AudioSample.cpp @@ -1,6 +1,14 @@ #include "AudioSample.h" namespace SOH { +AudioSample::~AudioSample() { + if (sample.book != nullptr && sample.book->book != nullptr) { + delete[] sample.book->book; + } + if (sample.sampleAddr != nullptr) { + delete[] sample.sampleAddr; + } +} Sample* AudioSample::GetPointer() { return &sample; } diff --git a/mm/2s2h/resource/type/AudioSample.h b/mm/2s2h/resource/type/AudioSample.h index 192b5a6f52..c81c5a73b8 100644 --- a/mm/2s2h/resource/type/AudioSample.h +++ b/mm/2s2h/resource/type/AudioSample.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include "Resource.h" #include @@ -20,24 +19,26 @@ typedef struct { /* 0x08 */ s16* book; // size 8 * order * npredictors. 8-byte aligned } AdpcmBook; // s -typedef struct { +typedef struct Sample { union { struct { - /* 0x00 */ u32 codec : 4; - /* 0x00 */ u32 medium : 2; - /* 0x00 */ u32 unk_bit26 : 1; - /* 0x00 */ u32 unk_bit25 : 1; // this has been named isRelocated in zret - /* 0x01 */ u32 size : 24; + ///* 0x0 */ u32 unk_0 : 1; + /* 0x0 */ u32 codec : 4; // The state of compression or decompression, See `SampleCodec` + /* 0x0 */ u32 medium : 2; // Medium where sample is currently stored. See `SampleMedium` + /* 0x0 */ u32 unk_bit26 : 1; + /* 0x0 */ u32 isRelocated : 1; // Has the sample header been relocated (offsets to pointers) }; u32 asU32; }; - - /* 0x04 */ u8* sampleAddr; - /* 0x08 */ AdpcmLoop* loop; - /* 0x0C */ AdpcmBook* book; - u32 sampleRateMagicValue; // For wav samples only... - s32 sampleRate; // For wav samples only... -} Sample; // size = 0x10 + /* 0x1 */ u32 size; // Size of the sample + u32 fileSize; + /* 0x4 */ u8* sampleAddr; // Raw sample data. Offset from the start of the sample bank or absolute address to either + // rom or ram + /* 0x8 */ AdpcmLoop* + loop; // Adpcm loop parameters used by the sample. Offset from the start of the sound font / pointer to ram + /* 0xC */ AdpcmBook* + book; // Adpcm book parameters used by the sample. Offset from the start of the sound font / pointer to ram +} Sample; // size = 0x10 class AudioSample : public Ship::Resource { public: @@ -45,18 +46,15 @@ class AudioSample : public Ship::Resource { AudioSample() : Resource(std::shared_ptr()) { } + ~AudioSample(); Sample* GetPointer(); size_t GetPointerSize(); Sample sample; - std::vector audioSampleData; - AdpcmLoop loop; - uint32_t loopStateCount; - AdpcmBook book; - uint32_t bookDataCount; - std::vector bookData; + // Only applies to streamed audio + float tuning = -1.0f; }; }; // namespace SOH diff --git a/mm/2s2h/resource/type/AudioSequence.cpp b/mm/2s2h/resource/type/AudioSequence.cpp index 41029d47d7..8b55a4afc3 100644 --- a/mm/2s2h/resource/type/AudioSequence.cpp +++ b/mm/2s2h/resource/type/AudioSequence.cpp @@ -9,4 +9,10 @@ Sequence* AudioSequence::GetPointer() { size_t AudioSequence::GetPointerSize() { return sizeof(Sequence); } + +AudioSequence::~AudioSequence() { + delete[] sequence.seqData; + sequence.seqData = nullptr; +} + } // namespace SOH diff --git a/mm/2s2h/resource/type/AudioSequence.h b/mm/2s2h/resource/type/AudioSequence.h index 5ff4c9a351..2eb50c3f1e 100644 --- a/mm/2s2h/resource/type/AudioSequence.h +++ b/mm/2s2h/resource/type/AudioSequence.h @@ -9,11 +9,11 @@ namespace SOH { typedef struct { char* seqData; - int32_t seqDataSize; + uint32_t seqDataSize; uint16_t seqNumber; uint8_t medium; uint8_t cachePolicy; - int32_t numFonts; + uint32_t numFonts; uint8_t fonts[16]; } Sequence; @@ -23,11 +23,11 @@ class AudioSequence : public Ship::Resource { AudioSequence() : Resource(std::shared_ptr()) { } + ~AudioSequence(); Sequence* GetPointer(); size_t GetPointerSize(); Sequence sequence; - std::vector sequenceData; }; }; // namespace SOH diff --git a/mm/2s2h/resource/type/AudioSoundFont.cpp b/mm/2s2h/resource/type/AudioSoundFont.cpp index 12218cb648..7f323ddf48 100644 --- a/mm/2s2h/resource/type/AudioSoundFont.cpp +++ b/mm/2s2h/resource/type/AudioSoundFont.cpp @@ -1,6 +1,23 @@ #include "AudioSoundFont.h" namespace SOH { + +AudioSoundFont::~AudioSoundFont() { + for (auto i : instrumentAddresses) { + if (i != nullptr) { + delete[] i->envelope; + delete i; + } + } + + for (auto d : drumAddresses) { + if (d != nullptr) { + delete[] d->envelope; + delete d; + } + } +} + SoundFont* AudioSoundFont::GetPointer() { return &soundFont; } diff --git a/mm/2s2h/resource/type/AudioSoundFont.h b/mm/2s2h/resource/type/AudioSoundFont.h index 302c182d21..bdc662add4 100644 --- a/mm/2s2h/resource/type/AudioSoundFont.h +++ b/mm/2s2h/resource/type/AudioSoundFont.h @@ -58,6 +58,7 @@ class AudioSoundFont : public Ship::Resource { AudioSoundFont() : Resource(std::shared_ptr()) { } + ~AudioSoundFont(); SoundFont* GetPointer(); size_t GetPointerSize(); @@ -68,15 +69,10 @@ class AudioSoundFont : public Ship::Resource { uint16_t data2; uint16_t data3; - std::vector drums; std::vector drumAddresses; - std::vector drumEnvelopeCounts; std::vector> drumEnvelopeArrays; - std::vector instruments; std::vector instrumentAddresses; - std::vector instrumentEnvelopeCounts; - std::vector> instrumentEnvelopeArrays; std::vector soundEffects; diff --git a/mm/2s2h/z_scene_2SH.cpp b/mm/2s2h/z_scene_2SH.cpp index 513215857f..57021b4507 100644 --- a/mm/2s2h/z_scene_2SH.cpp +++ b/mm/2s2h/z_scene_2SH.cpp @@ -340,8 +340,7 @@ void Scene_CommandSoundSettings(PlayState* play, SOH::ISceneCommand* cmd) { play->sequenceCtx.seqId = settings->settings.seqId; play->sequenceCtx.ambienceId = settings->settings.natureAmbienceId; - if (gSaveContext.seqId == (u8)NA_BGM_DISABLED || - AudioSeq_GetActiveSeqId(SEQ_PLAYER_BGM_MAIN) == NA_BGM_FINAL_HOURS) { + if (gSaveContext.seqId == NA_BGM_DISABLED || AudioSeq_GetActiveSeqId(SEQ_PLAYER_BGM_MAIN) == NA_BGM_FINAL_HOURS) { Audio_SetSpec(settings->settings.reverb); // BENTODO Verify if this should be reverb } } diff --git a/mm/CMakeLists.txt b/mm/CMakeLists.txt index 23b2228ad4..88e750b965 100644 --- a/mm/CMakeLists.txt +++ b/mm/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.26.0 FATAL_ERROR) +cmake_minimum_required(VERSION 3.26.0 FATAL_ERROR) set(CMAKE_SYSTEM_VERSION 10.0 CACHE STRING "" FORCE) @@ -29,6 +29,14 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") endif() endif() + +FetchContent_Declare( + dr_libs + GIT_REPOSITORY https://github.com/mackron/dr_libs.git + GIT_TAG da35f9d6c7374a95353fd1df1d394d44ab66cf01 +) +FetchContent_MakeAvailable(dr_libs) + ################################################################################ # Global configuration types ################################################################################ @@ -315,6 +323,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE assets ${SDL2-NET-INCLUDE} ${BOOST-INCLUDE} ${CMAKE_CURRENT_SOURCE_DIR}/assets/ + ${dr_libs_SOURCE_DIR} . ) @@ -521,6 +530,12 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang") ) endif() endif() + +# This file contains a few SIMD functions. Those functions are much more inefficient without optimizations. MSVC and GCC allow for optimizing a single function, but clang does not. +set_source_files_properties(2s2h/mixer.c PROPERTIES +COMPILE_FLAGS "-O2" +) + ################################################################################ # Pre build events ################################################################################ @@ -556,6 +571,17 @@ endif() if (CMAKE_SYSTEM_NAME STREQUAL "Windows") find_package(glfw3 REQUIRED) + + find_package(Ogg CONFIG REQUIRED) + link_libraries(Ogg::ogg) + + find_package(Vorbis CONFIG REQUIRED) + link_libraries(Vorbis::vorbisfile) + find_package(Opus CONFIG REQUIRED) + link_libraries(Opus::opus) + find_package(OpusFile CONFIG REQUIRED) + link_libraries(OpusFile::opusfile CONFIG REQUIRED) + set(ADDITIONAL_LIBRARY_DEPENDENCIES "libultraship;" "ZAPDLib;" @@ -567,7 +593,13 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") "winmm;" "imm32;" "version;" - "setupapi" + "setupapi;" + "Ogg::ogg" + "Opus::opus" + "Vorbis::vorbis" + "Vorbis::vorbisenc" + "Vorbis::vorbisfile" + "OpusFile::opusfile" ) elseif(CMAKE_SYSTEM_NAME STREQUAL "NintendoSwitch") find_package(SDL2) @@ -593,15 +625,27 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "CafeOS") else() find_package(SDL2) set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) - set(ADDITIONAL_LIBRARY_DEPENDENCIES + find_package(Ogg REQUIRED) + find_package(Vorbis REQUIRED) + find_package(Opus REQUIRED) + find_package(OpusFile REQUIRED) + set(ADDITIONAL_LIBRARY_DEPENDENCIES "libultraship;" "ZAPDLib;" + "Ogg::ogg" + "Vorbis::vorbis" + "Vorbis::vorbisenc" + "Vorbis::vorbisfile" + "Opus::opus" + "Opusfile::Opusfile" SDL2::SDL2 # "$<$:SDL2_net::SDL2_net>" ${CMAKE_DL_LIBS} Threads::Threads ) + endif() if(NOT CMAKE_SYSTEM_NAME MATCHES "Darwin|NintendoSwitch|CafeOS") diff --git a/mm/assets/xml/GC_US/audio/Audio.xml b/mm/assets/xml/GC_US/audio/Audio.xml index ea72d02dd3..3fbcf95a5a 100644 --- a/mm/assets/xml/GC_US/audio/Audio.xml +++ b/mm/assets/xml/GC_US/audio/Audio.xml @@ -1,3321 +1,818 @@ -