From 4b96fdee9d1770bf7e75bc944442fb1cce578fe8 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 23 Sep 2026 13:48:04 -0600 Subject: [PATCH 1/5] WIP new MusyX backend --- CMakeLists.txt | 2 + extern/musyx | 2 +- include/Kyoto/Audio/CStaticAudioPlayer.hpp | 20 ++++ include/Kyoto/Graphics/CMoviePlayer.hpp | 13 +++ include/Metaforce/Audio.hpp | 16 +++ include/Metaforce/AudioAssets.hpp | 20 ++++ src/Kyoto/Audio/CDSPStream.cpp | 36 +++++++ src/Kyoto/Audio/CDSPStreamManager.cpp | 47 ++++++++- src/Kyoto/Audio/CMidiManager.cpp | 38 ++++++- src/Kyoto/Audio/CStaticAudioPlayer.cpp | 83 ++++++++++++++- src/Kyoto/Audio/DolphinCAudioGroupSet.cpp | 32 +++++- src/Kyoto/Audio/DolphinCAudioSys.cpp | 15 ++- src/Kyoto/Graphics/CCubeMoviePlayer.cpp | 111 +++++++++++++++++++-- src/Metaforce/Audio.cpp | 47 +++++++++ src/Metaforce/AudioAssets.cpp | 79 +++++++++++++++ src/Metaforce/Runtime.cpp | 17 +++- src/Metaforce/Stubs.cpp | 7 +- src/MetroidPrime/main.cpp | 2 - 18 files changed, 558 insertions(+), 29 deletions(-) create mode 100644 include/Metaforce/Audio.hpp create mode 100644 include/Metaforce/AudioAssets.hpp create mode 100644 src/Metaforce/Audio.cpp create mode 100644 src/Metaforce/AudioAssets.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ce651a66b..111d63fc43 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -696,6 +696,8 @@ set(GAME_FILES extern/rstl/src/rstl/rstl_strings.cpp src/Metaforce/ARAMToken.cpp src/Metaforce/Alloc.cpp + src/Metaforce/Audio.cpp + src/Metaforce/AudioAssets.cpp src/Metaforce/CModelSectionReader.cpp src/Metaforce/MapData.cpp src/Metaforce/PathData.cpp diff --git a/extern/musyx b/extern/musyx index 1735ee9ffe..b6ebaeb3b5 160000 --- a/extern/musyx +++ b/extern/musyx @@ -1 +1 @@ -Subproject commit 1735ee9ffe8dfc54e5446880b43b8b9b4a620d09 +Subproject commit b6ebaeb3b5c4fbd8cc84b8f2f44b25c5e251cdc0 diff --git a/include/Kyoto/Audio/CStaticAudioPlayer.hpp b/include/Kyoto/Audio/CStaticAudioPlayer.hpp index 604c085d5e..4ffc2ee035 100644 --- a/include/Kyoto/Audio/CStaticAudioPlayer.hpp +++ b/include/Kyoto/Audio/CStaticAudioPlayer.hpp @@ -9,7 +9,11 @@ #include class CDvdRequest; +#if defined(TARGET_PC) +typedef void (*FAudioCallback)(short* output, size_t frames, u32 rate, u32 channels); +#else typedef void (*FAudioCallback)(); +#endif class CStaticAudioPlayer { public: @@ -20,12 +24,21 @@ class CStaticAudioPlayer { void StartMixOut(); void StopMixOut(); +#if defined(TARGET_PC) + static void MixCallback(short* output, size_t frames, u32 rate, u32 channels); + void DoMix(short* output, size_t frames, u32 rate, u32 channels); +#else static void MixCallback(); void DoMix(); +#endif static void RunDMACallback(FAudioCallback); static void CancelDMACallback(FAudioCallback); static void InstallAICallback(); +#if defined(TARGET_PC) + static void AICallback(short* output, size_t frames, u32 rate, u32 channels); +#else static void AICallback(); +#endif void Decode(const ushort* bufIn, ushort* bufOut, int numSamples); void DecodeMonoAndMix(ushort* bufIn, ushort* bufOut, int numSamples, @@ -48,6 +61,13 @@ class CStaticAudioPlayer { g72x_state x58_leftState; g72x_state x8c_rightState; ushort xc0_volume; +#if defined(TARGET_PC) + u32 m_audioPhase = 0; + short m_audioHistory[2][2] = {}; + short m_audioPair[4] = {}; + u32 m_audioPairRead = 2; + bool m_audioPrimed = false; +#endif }; #endif // _CSTATICAUDIOPLAYER diff --git a/include/Kyoto/Graphics/CMoviePlayer.hpp b/include/Kyoto/Graphics/CMoviePlayer.hpp index e0b59040c1..77aa447bb9 100644 --- a/include/Kyoto/Graphics/CMoviePlayer.hpp +++ b/include/Kyoto/Graphics/CMoviePlayer.hpp @@ -131,7 +131,11 @@ class CMoviePlayer { private: struct SIndexLoad; static void VerifyCallbackStatus(); +#if defined(TARGET_PC) + static void StaticMyAudioCallback(short* output, size_t frames, u32 rate, u32 channels); +#else static void StaticMyAudioCallback(); +#endif void InitializeTextures(); void ReadCompleted(); void PostDVDReadRequestIfNeeded(); @@ -139,7 +143,11 @@ class CMoviePlayer { THPHeader x28_header; THPFrameCompInfo x58_thpComponents; THPVideoInfoOld x6c_videoInfo; +#if defined(TARGET_PC) + THPAudioInfoOld x74_audioInfo{}; +#else THPAudioInfoOld x74_audioInfo; +#endif rstl::vector< CTHPTextureSet > x80_textures; rstl::auto_ptr< uchar > x90_requestBuffer; rstl::single_ptr< CDvdRequest > x98_request; @@ -168,6 +176,11 @@ class CMoviePlayer { bool xf4_27_fieldFlip : 1; uint xf8_cachedBytes; int xfc_fieldIndex; +#if defined(TARGET_PC) + u32 m_audioPhase = 0; + short m_audioHistory[2][2] = {}; + bool m_audioPrimed = false; +#endif }; CHECK_SIZEOF(CMoviePlayer, 0x100) diff --git a/include/Metaforce/Audio.hpp b/include/Metaforce/Audio.hpp new file mode 100644 index 0000000000..f28a6a29ea --- /dev/null +++ b/include/Metaforce/Audio.hpp @@ -0,0 +1,16 @@ +#pragma once +#include + +namespace metaforce { +void ConfigureAudio(SND_PC_CONFIG preferred, bool disabled); +void InitializeAudio(u8 voices, u8 music, u8 sfx, u32 flags); +void UpdateAudio(); +void LockAudio(); +void UnlockAudio(); +struct AudioLockGuard { + AudioLockGuard() { LockAudio(); } + ~AudioLockGuard() { UnlockAudio(); } + AudioLockGuard(const AudioLockGuard&) = delete; + AudioLockGuard& operator=(const AudioLockGuard&) = delete; +}; +} // namespace metaforce diff --git a/include/Metaforce/AudioAssets.hpp b/include/Metaforce/AudioAssets.hpp new file mode 100644 index 0000000000..b207938aa0 --- /dev/null +++ b/include/Metaforce/AudioAssets.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +struct dspadpcm_header; + +namespace metaforce { +struct AudioGroupView { + std::string_view baseDirectory, name; + SND_PC_GROUP_ASSETS assets{}; +}; +struct AudioSongHeader { + u32 version, song, group, audioGroup, length; +}; +bool ReadAudioGroup(std::span bytes, AudioGroupView& result); +bool ReadAudioSongHeader(std::span bytes, AudioSongHeader& result); +bool ReadDSPHeader(std::span bytes, dspadpcm_header& result); +} // namespace metaforce diff --git a/src/Kyoto/Audio/CDSPStream.cpp b/src/Kyoto/Audio/CDSPStream.cpp index 02aa8628f4..a854e665db 100644 --- a/src/Kyoto/Audio/CDSPStream.cpp +++ b/src/Kyoto/Audio/CDSPStream.cpp @@ -8,6 +8,9 @@ #include "dolphin/os.h" #include +#if defined(TARGET_PC) +#include "Metaforce/Audio.hpp" +#endif static struct { CDSPStream streams[4]; @@ -109,10 +112,37 @@ void CDSPStream::Initialize() { } void CDSPStream::FreeAllStreams() { +#if defined(TARGET_PC) + { + metaforce::AudioLockGuard lock; + for (auto& stream : g_Streams) { + stream.xe8_silenced = 1; + stream.xf0_stopRequested = 1; + if (stream.xc8_streamId != SND_ID_ERROR) sndStreamDeactivate(stream.xc8_streamId); + } + } + // DVD completion takes the same lock. Wait without holding it, and keep + // refill buffers alive until both requests and their callbacks have retired. + for (auto& stream : g_Streams) { + DVDCancel(&stream.x50_fileInfo1.cb); + DVDCancel(&stream.x8c_fileInfo2.cb); + } + metaforce::AudioLockGuard lock; + for (auto& stream : g_Streams) { + if (stream.x0_state != 0) stream.CloseFiles(); + if (stream.xc8_streamId != SND_ID_ERROR) sndStreamFree(stream.xc8_streamId); + CMemory::Free(stream.xd4_buffer); + stream.xd4_buffer = nullptr; + stream.xc8_streamId = SND_ID_ERROR; + stream.x0_state = 0; + stream.x8_right = stream.xc_left = nullptr; + } +#else for (uint i = 0; i < 4; ++i) { sndStreamFree(g_Streams[i].xc8_streamId); CMemory::Free(g_Streams[i].xd4_buffer); } +#endif } uint CDSPStream::AllocateStream(const SStreamInfo& info, char vol, char pan) { @@ -410,6 +440,9 @@ int CDSPStream::InitializeStream() { } void CDSPStream::ReadCompleted(s32, DVDFileInfo* fileInfo) { +#if defined(TARGET_PC) + metaforce::AudioLockGuard lock; +#endif int idx = 0; CDSPStream* s = g_Streams; for (; idx < 4; ++idx, ++s) { @@ -418,6 +451,9 @@ void CDSPStream::ReadCompleted(s32, DVDFileInfo* fileInfo) { } } +#if defined(TARGET_PC) + if (idx == 4) return; +#endif CDSPStream& stream = g_Streams[idx]; stream.xec_readsPending--; if (stream.xec_readsPending != 0) { diff --git a/src/Kyoto/Audio/CDSPStreamManager.cpp b/src/Kyoto/Audio/CDSPStreamManager.cpp index 710b9fb336..9bf9933ec3 100644 --- a/src/Kyoto/Audio/CDSPStreamManager.cpp +++ b/src/Kyoto/Audio/CDSPStreamManager.cpp @@ -8,6 +8,10 @@ #include "dolphin/os/OSThread.h" #include +#if defined(TARGET_PC) +#include "Metaforce/Audio.hpp" +#include "Metaforce/AudioAssets.hpp" +#endif CDSPStreamManager g_Streams[4] = {CDSPStreamManager(), CDSPStreamManager(), CDSPStreamManager(), CDSPStreamManager()}; @@ -64,14 +68,36 @@ bool CDSPStreamManager::StartHeaderRead(DVDCallback callback) { return true; } -bool CDSPStreamManager::HasSupportedSampleRate() { return x0_header.x8_sampleRate == 32000; } +bool CDSPStreamManager::HasSupportedSampleRate() { +#if defined(TARGET_PC) + return x0_header.x8_sampleRate > 0 && x0_header.x8_sampleRate <= 65535; +#else + return x0_header.x8_sampleRate == 32000; +#endif +} void CDSPStreamManager::WaitForReadCompletion() { +#if defined(TARGET_PC) + BOOL ints = OSEnableInterrupts(); + for (;;) { + bool complete; + { + metaforce::AudioLockGuard lock; + complete = x70_26_headerReadState != kHRS_Reading; + } + if (complete) { + OSRestoreInterrupts(ints); + return; + } + OSYieldThread(); + } +#else BOOL ints = OSEnableInterrupts(); while (x70_26_headerReadState == 1) { OSYieldThread(); } OSRestoreInterrupts(ints); +#endif } CDSPStreamManager& CDSPStreamManager::operator=(const CDSPStreamManager& other) { @@ -88,6 +114,13 @@ void CDSPStreamManager::Initialize() { } void CDSPStreamManager::Shutdown() { +#if defined(TARGET_PC) + { + metaforce::AudioLockGuard lock; + for (auto& stream : g_Streams) stream.x70_25_headerReadCancelled = true; + } + for (auto& stream : g_Streams) stream.WaitForReadCompletion(); +#endif CDSPStream::FreeAllStreams(); for (int i = 0; i < 4; ++i) { g_Streams[i] = CDSPStreamManager(); @@ -312,12 +345,24 @@ CDSPStreamManager::EState CDSPStreamManager::GetStreamState(int handle) { } void CDSPStreamManager::HeaderReadComplete(s32 result, DVDFileInfo* fileInfo) { +#if defined(TARGET_PC) + metaforce::AudioLockGuard lock; +#endif DVDClose(fileInfo); for (int idx = 0; idx < 4; ++idx) { CDSPStreamManager* stream = &g_Streams[idx]; if (&stream->x80_dvdFile == fileInfo && !stream->x70_24_unclaimed) { CInterruptGuard interrupts; +#if defined(TARGET_PC) + if (result != 0x60 || !metaforce::ReadDSPHeader( + {reinterpret_cast(&stream->x0_header), 0x60}, stream->x0_header) || + fileInfo->length < 0x60 || + (u64(stream->x0_header.x4_numNibbles) + 1) / 2 > fileInfo->length - 0x60) { + *stream = CDSPStreamManager(); + return; + } +#endif if (result <= 0 || !stream->HasSupportedSampleRate()) { *stream = CDSPStreamManager(); return; diff --git a/src/Kyoto/Audio/CMidiManager.cpp b/src/Kyoto/Audio/CMidiManager.cpp index c1d7c9746c..68978aab63 100644 --- a/src/Kyoto/Audio/CMidiManager.cpp +++ b/src/Kyoto/Audio/CMidiManager.cpp @@ -5,10 +5,20 @@ #include #include +#if defined(TARGET_PC) +#include "Metaforce/AudioAssets.hpp" +#include "Metaforce/Common.hpp" +namespace { constexpr borealis::Log Log{"song assets"}; } +#endif rstl::reserved_vector< CMidiManager::CMidiWrapper, 3 > CMidiManager::mMidiWrappers; -CMidiManager::CMidiWrapper::CMidiWrapper() : x0_sysHandle(0), xa_available(true) {} +CMidiManager::CMidiWrapper::CMidiWrapper() +#if defined(TARGET_PC) +: x0_sysHandle(0), x8_songId(-1), xa_available(true) {} +#else +: x0_sysHandle(0), xa_available(true) {} +#endif const CSfxHandle& CMidiManager::CMidiWrapper::GetManagerHandle() const { return x4_midiHandle; } @@ -39,6 +49,11 @@ CSfxHandle CMidiManager::Play(const CMidiData& data, unsigned short fadeTime, bo wrapper.SetMidiHandle(handle); if (stopExisting) { for (int i = 0; i < mMidiWrappers.size(); ++i) { +#if defined(TARGET_PC) + if (&mMidiWrappers[i] == &wrapper) { + continue; + } +#endif if (mMidiWrappers[i].IsAvailable()) { continue; } @@ -58,6 +73,12 @@ CSfxHandle CMidiManager::Play(const CMidiData& data, unsigned short fadeTime, bo wrapper.SetSongId(data.GetSongId()); } else { u32 sysHandle = CAudioSys::SeqPlayEx(data.GetGroupId(), data.GetSongId(), data.GetData(), nullptr, 0); +#if defined(TARGET_PC) + if (sysHandle == SND_ID_ERROR) { + wrapper.SetAvailable(true); + return CSfxHandle(); + } +#endif if (fadeTime != 0) { CAudioSys::SeqVolume(0, 0, sysHandle, 0); } @@ -113,6 +134,20 @@ CSfxHandle CMidiManager::LocateHandle() { CMidiManager::CMidiData::CMidiData(CInputStream& in) : x0_songId(-1), x2_groupId(-1), x4_agscId(-1) { +#if defined(TARGET_PC) + u8 bytes[20]; + metaforce::AudioSongHeader header{}; + REQUIRE(in.ReadBytes(bytes, sizeof(bytes)) == sizeof(bytes) && + metaforce::ReadAudioSongHeader(bytes, header), "Invalid CSNG header"); + x0_songId = header.song; + x2_groupId = header.group; + x4_agscId = header.audioGroup; + x8_data = rs_new uchar[header.length]; + REQUIRE(in.ReadBytes(x8_data.get(), header.length) == header.length, "Truncated CSNG arrangement"); + SND_PC_ASSET_ERROR error{}; + REQUIRE(sndPCValidateArrangement({x8_data.get(), header.length}, &error), + "Invalid CSNG arrangement at {}: {}", error.offset, error.reason ? error.reason : ""); +#else in.ReadLong(); x0_songId = in.ReadLong(); x2_groupId = in.ReadLong(); @@ -120,6 +155,7 @@ CMidiManager::CMidiData::CMidiData(CInputStream& in) int len = in.ReadInt32(); x8_data = rs_new uchar[len]; in.Get(x8_data.get(), len); +#endif } const CFactoryFnReturn FMidiDataFactory(const SObjectTag& tag, CInputStream& in, const CVParamTransfer&) { diff --git a/src/Kyoto/Audio/CStaticAudioPlayer.cpp b/src/Kyoto/Audio/CStaticAudioPlayer.cpp index 6d44c6c6de..a15f5677a0 100644 --- a/src/Kyoto/Audio/CStaticAudioPlayer.cpp +++ b/src/Kyoto/Audio/CStaticAudioPlayer.cpp @@ -12,12 +12,22 @@ #include #include +#if defined(TARGET_PC) +#include "Metaforce/Audio.hpp" +#include +#endif + static CStaticAudioPlayer* sCurrentPlayer = nullptr; static rstl::reserved_vector< FAudioCallback, 4 > sAICallbacks; +#if !defined(TARGET_PC) ATTRIBUTE_ALIGN_DECL(8, static bool sDMACallbackInstalled) = false; static FAudioCallback sOldDMACallback = nullptr; +#endif void CStaticAudioPlayer::InstallAICallback() { +#if defined(TARGET_PC) + sndPCSetOutputCallback(sAICallbacks.empty() ? nullptr : AICallback); +#else bool old = CAudioSys::IsAICallbackEnabled(); CAudioSys::EnableAICallback(true); @@ -31,8 +41,16 @@ void CStaticAudioPlayer::InstallAICallback() { } CAudioSys::EnableAICallback(old); +#endif } +#if defined(TARGET_PC) +void CStaticAudioPlayer::AICallback(short* output, size_t frames, u32 rate, u32 channels) { + for (int i = 0; i < sAICallbacks.size(); ++i) { + sAICallbacks[i](output, frames, rate, channels); + } +} +#else void CStaticAudioPlayer::AICallback() { sOldDMACallback(); @@ -40,6 +58,7 @@ void CStaticAudioPlayer::AICallback() { sAICallbacks[i](); } } +#endif void CStaticAudioPlayer::RunDMACallback(const FAudioCallback callback) { volatile const bool old = OSDisableInterrupts(); @@ -103,6 +122,9 @@ const bool CStaticAudioPlayer::IsReady() const { } void CStaticAudioPlayer::StartMixOut() { +#if defined(TARGET_PC) + const metaforce::AudioLockGuard lock; +#endif if (sCurrentPlayer == this) { return; } @@ -111,21 +133,75 @@ void CStaticAudioPlayer::StartMixOut() { x18_curSamp = 0; g72x_init_state(&x58_leftState); g72x_init_state(&x8c_rightState); +#if defined(TARGET_PC) + m_audioPhase = 0; + m_audioPairRead = 2; + m_audioPrimed = false; +#endif sCurrentPlayer = this; RunDMACallback(MixCallback); } void CStaticAudioPlayer::StopMixOut() { +#if defined(TARGET_PC) + const metaforce::AudioLockGuard lock; +#endif if (sCurrentPlayer == this) { CancelDMACallback(MixCallback); sCurrentPlayer = NULL; } } +#if defined(TARGET_PC) +void CStaticAudioPlayer::MixCallback(short* output, size_t frames, u32 rate, u32 channels) { + if (sCurrentPlayer != nullptr) { + sCurrentPlayer->DoMix(output, frames, rate, channels); + } +} + +void CStaticAudioPlayer::DoMix(short* output, size_t frames, u32 rate, u32 channels) { + // RSF stores two 32 kHz G.721 channels, with two samples packed in each byte. + // Keep decoding pairs even when the output rate requires fractional frames. + const auto readFrame = [this](short* frame) { + if (m_audioPairRead == 2) { + std::fill_n(m_audioPair, 4, short(0)); + ushort* pair = reinterpret_cast< ushort* >(m_audioPair); + Decode(pair, pair, 2); + m_audioPairRead = 0; + } + frame[0] = m_audioPair[m_audioPairRead * 2]; + frame[1] = m_audioPair[m_audioPairRead * 2 + 1]; + ++m_audioPairRead; + }; + if (!m_audioPrimed) { + readFrame(m_audioHistory[0]); + readFrame(m_audioHistory[1]); + m_audioPrimed = true; + } + + for (size_t frame = 0; frame < frames; ++frame, output += channels) { + for (u32 channel = 0; channel < 2; ++channel) { + // The original DMA buffer is R,L; native output uses FL,FR. + const s32 a = m_audioHistory[0][1 - channel]; + const s32 b = m_audioHistory[1][1 - channel]; + const s32 sample = a + (s64(b - a) * m_audioPhase) / rate; + if (xc0_volume != 0) { + output[channel] = std::clamp(s32(output[channel]) + sample, -32768, 32767); + } + } + m_audioPhase += 32000; + while (m_audioPhase >= rate) { + m_audioPhase -= rate; + m_audioHistory[0][0] = m_audioHistory[1][0]; + m_audioHistory[0][1] = m_audioHistory[1][1]; + readFrame(m_audioHistory[1]); + } + } +} +#else void CStaticAudioPlayer::MixCallback() { sCurrentPlayer->DoMix(); } void CStaticAudioPlayer::DoMix() { -#if !defined(TARGET_PC) // TODO u32 aiStart = OSCachedToPhysical(AIGetDMAStartAddr()); x24_curBuf ^= 1; uintptr_t buf = @@ -140,8 +216,8 @@ void CStaticAudioPlayer::DoMix() { Decode((ushort*)buf, (ushort*)aiStart, 160); DCFlushRange((void*)buf, 0x280); OSRestoreInterrupts(cookie); -#endif } +#endif void CStaticAudioPlayer::Decode(const ushort* bufIn, ushort* bufOut, int numSamples) { int curSamp = x18_curSamp / 2; @@ -222,6 +298,9 @@ void CStaticAudioPlayer::DecodeMonoAndMix(ushort* bufIn, ushort* bufOut, int num } void CStaticAudioPlayer::SetVolume(uchar vol) { +#if defined(TARGET_PC) + const metaforce::AudioLockGuard lock; +#endif if (static_cast< uchar >(vol) > 127) { vol = 127; } diff --git a/src/Kyoto/Audio/DolphinCAudioGroupSet.cpp b/src/Kyoto/Audio/DolphinCAudioGroupSet.cpp index 99f6fa36d9..26c3eb794f 100644 --- a/src/Kyoto/Audio/DolphinCAudioGroupSet.cpp +++ b/src/Kyoto/Audio/DolphinCAudioGroupSet.cpp @@ -6,6 +6,11 @@ #include "Kyoto/Alloc/CMemory.hpp" #include "rstl/auto_ptr.hpp" #include +#if defined(TARGET_PC) +#include "Metaforce/AudioAssets.hpp" +#include "Metaforce/Common.hpp" +namespace { constexpr borealis::Log Log{"audio assets"}; } +#endif CAudioGroupSet::CAudioGroupSet(const TLockedToken< CAudioGrpSetLoc >& group) : x0_baseDir(group->GetBaseDirName()) @@ -25,17 +30,37 @@ CAudioGrpSetLoc::CAudioGrpSetLoc(const rstl::auto_ptr< uchar >& data, int length , x38_project(nullptr) , x3c_sampleDir(nullptr) , x40_samples(nullptr) { +#if defined(TARGET_PC) + metaforce::AudioGroupView view; + REQUIRE(length >= 0 && metaforce::ReadAudioGroup({x0_data.get(), size_t(length)}, view), + "Invalid AGSC wrapper ({} bytes)", length); + SND_PC_ASSET_ERROR error{}; + REQUIRE(sndPCValidateGroup(&view.assets, &error), "Invalid AGSC {} at {}+{}: {}", view.name, + error.section ? error.section : "", error.offset, error.reason ? error.reason : ""); + x10_baseDirName = rstl::string(view.baseDirectory.data(), view.baseDirectory.size()); + x20_groupSetName = rstl::string(view.name.data(), view.name.size()); + const size_t poolSize = (view.assets.pool.size + 3) & ~size_t(3); + const size_t projectSize = (view.assets.project.size + 3) & ~size_t(3); + x8_groupData = rstl::auto_ptr(static_cast( + CMemory::Alloc(poolSize + projectSize + view.assets.directory.size, IAllocator::kHI_RoundUpLen))); + x34_pool = x8_groupData.get(); + x38_project = x34_pool + poolSize; + x3c_sampleDir = x38_project + projectSize; + memcpy(x34_pool, view.assets.pool.data, view.assets.pool.size); + memcpy(x38_project, view.assets.project.data, view.assets.project.size); + memcpy(x3c_sampleDir, view.assets.directory.data, view.assets.directory.size); + x40_samples = const_cast(static_cast(view.assets.samples.data)); + x30_aramSize = view.assets.samples.size; +#else uint readPosition; const uint poolSize = ReadHeader(data.get(), length, readPosition); CAudioSys::GetVerbose(); -#if !defined(TARGET_PC) // TODO: audio const uint projectOffset = readPosition + poolSize; #if TARGET_LITTLE_ENDIAN const uint projectSize = CBasics::SwapBytes(*reinterpret_cast< uint* >(data.get() + projectOffset)); #else const uint projectSize = *reinterpret_cast< uint* >(data.get() + projectOffset); -#endif CAudioSys::GetVerbose(); const uint sampOffset = 4 + projectSize + projectOffset; @@ -71,13 +96,12 @@ CAudioGrpSetLoc::CAudioGrpSetLoc(const rstl::auto_ptr< uchar >& data, int length memcpy(x3c_sampleDir, ptr + (sdirOffset + 4), sdirSize); x40_samples = &ptr[sampOffset + 4]; #endif +#endif } void CAudioGrpSetLoc::FreeSampleBuffer() { -#if !defined(TARGET_PC) x0_data = nullptr; x40_samples = nullptr; -#endif } template <> diff --git a/src/Kyoto/Audio/DolphinCAudioSys.cpp b/src/Kyoto/Audio/DolphinCAudioSys.cpp index bc1dcd4d57..a5cb596ff7 100644 --- a/src/Kyoto/Audio/DolphinCAudioSys.cpp +++ b/src/Kyoto/Audio/DolphinCAudioSys.cpp @@ -13,6 +13,9 @@ #include #include +#if defined(TARGET_PC) +#include "Metaforce/Audio.hpp" +#endif const ushort CAudioSys::kVolumeTable[] = { // pow(i / 127, 2) * 32768 @@ -189,11 +192,15 @@ CAudioSys::CAudioSys(const uchar numVoices, const uchar numMusic, const uchar nu SND_HOOKS hooks = {DoMalloc, DoFree}; AIInit(NULL); sndSetHooks(&hooks); +#if defined(TARGET_PC) + metaforce::InitializeAudio(numVoices, numMusic, numSfx, mProLogic2 ? 1 : 0); +#else if (mProLogic2) { sndInit(numVoices, numMusic, numSfx, 1, 1, aramSize); } else { sndInit(numVoices, numMusic, numSfx, 1, 0, aramSize); } +#endif DTKInit(); mpGroupSetDB = rs_new rstl::map< rstl::string, rstl::ncrc_ptr< CAudioGroupSet > >(); @@ -241,16 +248,12 @@ CAudioSys::~CAudioSys() { } void CAudioSys::SysSetVolume(const uchar volume, const ushort time, const uchar group) { -#if !defined(TARGET_PC) // TODO: audio sndVolume(volume, time, group); -#endif } void CAudioSys::SysSetSfxVolume(const uchar volume, const ushort time, const uchar music, const uchar fx) { -#if !defined(TARGET_PC) // TODO: audio sndMasterVolume(volume, time, music, fx); -#endif } bool CAudioSys::SysLoadGroupSet(CSimplePool* pool, const uint id) { @@ -341,7 +344,6 @@ void* CAudioSys::SampleDataUploadCallback(u32 address, u32 bytes) { } bool CAudioSys::SysPushGroupIntoARAM(const rstl::string& name, const uchar groupId) { -#if !defined(TARGET_PC) // TODO: audio rstl::ncrc_ptr< CAudioGroupSet > groupSet = FindGroupSet(name); CAudioGroupSet* group = groupSet.GetPtr(); if (group) { @@ -361,14 +363,11 @@ bool CAudioSys::SysPushGroupIntoARAM(const rstl::string& name, const uchar group return result; #endif } -#endif return false; } void CAudioSys::SysPopGroupFromARAM() { -#if !defined(TARGET_PC) // TODO: audio sndPopGroup(); -#endif } const rstl::string& CAudioSys::SysGetGroupSetName(const uint id) { diff --git a/src/Kyoto/Graphics/CCubeMoviePlayer.cpp b/src/Kyoto/Graphics/CCubeMoviePlayer.cpp index 0631a353b5..1e83a79284 100644 --- a/src/Kyoto/Graphics/CCubeMoviePlayer.cpp +++ b/src/Kyoto/Graphics/CCubeMoviePlayer.cpp @@ -18,12 +18,18 @@ #include "dolphin/gx.h" #include "dolphin/thp.h" +#if defined(TARGET_PC) +#include "Metaforce/Audio.hpp" +#include +#endif static int sNumReferences = 0; static CMoviePlayer* sAudioPlayer; +#if !defined(TARGET_PC) static const short* curAudioBuffer; static int soundBufferIndex; ATTRIBUTE_ALIGN_DECL(32, static short soundBuffer[2][320]); +#endif static bool sAudioEnabled = true; static uchar sSfxVolume = 127; @@ -192,8 +198,15 @@ CMoviePlayer::CMoviePlayer(const char* path, const float preLoadSeconds, const b sThpInitialized = true; THPInit(); } - ++sNumReferences; - VerifyCallbackStatus(); +#if defined(TARGET_PC) + { + const metaforce::AudioLockGuard lock; +#endif + ++sNumReferences; + VerifyCallbackStatus(); +#if defined(TARGET_PC) + } +#endif xac_indexLoad->x0_headerRequest = x0_dvdFile.SyncRead(xac_indexLoad->xc_buffer.get(), 64); } @@ -313,6 +326,9 @@ bool CMoviePlayer::PumpIndexLoad() { } CMoviePlayer::~CMoviePlayer() { +#if defined(TARGET_PC) + const metaforce::AudioLockGuard lock; +#endif --sNumReferences; VerifyCallbackStatus(); if (sAudioPlayer == this) { @@ -321,6 +337,9 @@ CMoviePlayer::~CMoviePlayer() { } void CMoviePlayer::InitializeTextures() { +#if defined(TARGET_PC) + const metaforce::AudioLockGuard lock; +#endif const uint ySize = OSRoundUp32B(x6c_videoInfo.mXSize * x6c_videoInfo.mYSize); const uint uvSize = OSRoundUp32B(x6c_videoInfo.mXSize * x6c_videoInfo.mYSize / 4); const uint audioSize = x28_header.mAudioMaxSamples * 4; @@ -387,14 +406,21 @@ void CMoviePlayer::DecodeFromRead(const void* ptr) { const uchar* dataStart = static_cast< const uchar* >(ptr) + 8 + x58_thpComponents.mNumComponents * 4; uint offset = 0; +#if !defined(TARGET_PC) texture.SetAudioSamplesConsumed(0); texture.SetAudioSamples(0); +#endif for (uint i = 0; i < x58_thpComponents.mNumComponents; ++i) { const uchar* data = dataStart + offset; if (x58_thpComponents.mFrameComp[i] == 0) { THPVideoDecode(const_cast< uchar* >(data), texture.Y(), texture.U(), texture.V(), alignedWork); } else if (x58_thpComponents.mFrameComp[i] == 1) { +#if defined(TARGET_PC) + // Publish PCM and its counters together; the mixer may be reading this + // slot while the game thread decodes the next video frame. + const metaforce::AudioLockGuard lock; +#endif const uint samples = THPAudioDecode(static_cast< short* >(texture.Audio()), const_cast< uchar* >(data), 0); const BOOL interrupts = OSDisableInterrupts(); @@ -519,7 +545,12 @@ void CMoviePlayer::DrawFrame(const CVector3f& v1, const CVector3f& v2, const CVe ++xfc_fieldIndex; } -void CMoviePlayer::SetPlayMode(const EPlayMode mode) { xe0_playMode = mode; } +void CMoviePlayer::SetPlayMode(const EPlayMode mode) { +#if defined(TARGET_PC) + const metaforce::AudioLockGuard lock; +#endif + xe0_playMode = mode; +} float CMoviePlayer::GetTotalSeconds() const { return xe4_totalSeconds; } @@ -537,6 +568,11 @@ void CMoviePlayer::Rewind() { x98_request = nullptr; } +#if defined(TARGET_PC) + const metaforce::AudioLockGuard lock; + m_audioPhase = 0; + m_audioPrimed = false; +#endif x90_requestBuffer = rstl::auto_ptr< uchar >(nullptr); xb0_nextReadSize = x28_header.mFirstFrameSize; xb4_nextReadOff = x28_header.mMovieDataOffsets; @@ -554,8 +590,52 @@ void CMoviePlayer::Rewind() { x80_textures.clear(); } +#if defined(TARGET_PC) +void CMoviePlayer::StaticMyAudioCallback(short* output, size_t frames, u32 rate, u32 channels) { + CMoviePlayer* player = sAudioPlayer; + if (!player || player->xe0_playMode != kPM_Playing || + player->xd4_audioSlot < 0 || player->x80_textures.empty()) { + return; + } + const u32 sourceRate = player->x74_audioInfo.mSndFrequency; + if (!sourceRate || sourceRate > 96000) { + return; + } + + for (size_t frame = 0; frame < frames; ++frame, output += channels) { + short audio[2]; + if (sourceRate == rate) { + player->MixAudio(audio, nullptr, 1); + } else { + if (!player->m_audioPrimed) { + player->MixAudio(player->m_audioHistory[0], nullptr, 1); + player->MixAudio(player->m_audioHistory[1], nullptr, 1); + player->m_audioPrimed = true; + } + for (u32 channel = 0; channel < 2; ++channel) { + const s32 a = player->m_audioHistory[0][channel]; + const s32 b = player->m_audioHistory[1][channel]; + audio[channel] = a + (s64(b - a) * player->m_audioPhase) / rate; + } + player->m_audioPhase += sourceRate; + while (player->m_audioPhase >= rate) { + player->m_audioPhase -= rate; + for (u32 channel = 0; channel < 2; ++channel) { + player->m_audioHistory[0][channel] = player->m_audioHistory[1][channel]; + } + player->MixAudio(player->m_audioHistory[1], nullptr, 1); + } + } + // THPAudioDecode follows the console's R,L ordering. Native output is L,R; + // movies remain a stereo source in every speaker layout. + if (sAudioEnabled && sSfxVolume != 0) { + output[0] = std::clamp(s32(output[0]) + audio[1], -32768, 32767); + output[1] = std::clamp(s32(output[1]) + audio[0], -32768, 32767); + } + } +} +#else void CMoviePlayer::StaticMyAudioCallback() { -#if !defined(TARGET_PC) // TODO: audio if (sAudioPlayer != nullptr && sAudioPlayer->xf4_26_hasAudio) { curAudioBuffer = static_cast< const short* >(OSPhysicalToCached(AIGetDMAStartAddr())); soundBufferIndex ^= 1; @@ -569,8 +649,8 @@ void CMoviePlayer::StaticMyAudioCallback() { DCFlushRange(buffer, sizeof(soundBuffer[0])); OSRestoreInterrupts(interrupts); } -#endif } +#endif void CMoviePlayer::MixAudio(short* out, const short* in, unsigned long samples) { const short* input = in; @@ -647,8 +727,23 @@ uint CMoviePlayer::GetWidth() const { return x6c_videoInfo.mXSize; } uint CMoviePlayer::GetHeight() const { return x6c_videoInfo.mYSize; } -void CMoviePlayer::SetAudioEnabled(bool enabled) { sAudioEnabled = enabled; } +void CMoviePlayer::SetAudioEnabled(bool enabled) { +#if defined(TARGET_PC) + const metaforce::AudioLockGuard lock; +#endif + sAudioEnabled = enabled; +} -bool CMoviePlayer::GetAudioEnabled() { return sAudioEnabled; } +bool CMoviePlayer::GetAudioEnabled() { +#if defined(TARGET_PC) + const metaforce::AudioLockGuard lock; +#endif + return sAudioEnabled; +} -void CMoviePlayer::SetSfxVolume(uchar volume) { sSfxVolume = rstl::min_val(uchar(127), volume); } +void CMoviePlayer::SetSfxVolume(uchar volume) { +#if defined(TARGET_PC) + const metaforce::AudioLockGuard lock; +#endif + sSfxVolume = rstl::min_val(uchar(127), volume); +} diff --git a/src/Metaforce/Audio.cpp b/src/Metaforce/Audio.cpp new file mode 100644 index 0000000000..864849db95 --- /dev/null +++ b/src/Metaforce/Audio.cpp @@ -0,0 +1,47 @@ +#include "Metaforce/Audio.hpp" + +#include +#include + +namespace metaforce { +namespace { +constexpr borealis::Log Log{"audio"}; +SND_PC_CONFIG preferredConfig{}; +bool disabled, offline; +u32 frameRemainder; +} // namespace + +void ConfigureAudio(SND_PC_CONFIG preferred, bool noDevice) { + preferredConfig = preferred; + disabled = noDevice; +} + +void InitializeAudio(u8 voices, u8 music, u8 sfx, u32 flags) { + sndPCSetSynchronization(LockAudio, UnlockAudio); + SND_PC_CONFIG selected = preferredConfig; + bool opened = !disabled && sndPCOpenAudio(&preferredConfig, &selected); + if (!opened) { + if (!selected.mixRate) selected.mixRate = 48000; + if (!selected.channels) selected.channels = 2; + if (!sndPCConfigure(&selected)) throw std::runtime_error("Invalid audio configuration"); + } + if (sndInit(voices, music, sfx, 1, flags, 0) != 0) { + sndPCStopAudio(); + throw std::runtime_error("MusyX initialization failed"); + } + offline = !opened || !sndPCStartAudio(); + frameRemainder = 0; + if (offline && !disabled) Log.warn("Audio device unavailable; advancing sound without playback"); + Log.info("MusyX: {} Hz, {} channels{}", selected.mixRate, selected.channels, offline ? " (silent)" : ""); +} + +void UpdateAudio() { + if (!offline || !sndIsInstalled()) return; + const auto info = sndPCGetRenderInfo(); + // The game advances at 60 logical frames per second, including headless runs. + frameRemainder += info.mixRate; + s16 discarded[1600 * 8]; + sndPCRender(discarded, frameRemainder / 60); + frameRemainder %= 60; +} +} // namespace metaforce diff --git a/src/Metaforce/AudioAssets.cpp b/src/Metaforce/AudioAssets.cpp new file mode 100644 index 0000000000..e2444d4ada --- /dev/null +++ b/src/Metaforce/AudioAssets.cpp @@ -0,0 +1,79 @@ +#include "Metaforce/AudioAssets.hpp" +#include "Kyoto/Audio/CDSPStream.hpp" + +#include + +namespace metaforce { +namespace { +u16 Read16(const u8* p) { return (u16(p[0]) << 8) | p[1]; } +u32 Read32(const u8* p) { return (u32(Read16(p)) << 16) | Read16(p + 2); } +bool ReadString(std::span& bytes, std::string_view& value) { + const auto* end = static_cast(std::memchr(bytes.data(), 0, bytes.size())); + if (!end) return false; + size_t size = end - bytes.data(); + value = {reinterpret_cast(bytes.data()), size}; + bytes = bytes.subspan(size + 1); + return true; +} +bool ReadSection(std::span& bytes, SND_PC_SPAN& value) { + if (bytes.size() < 4) return false; + u32 size = Read32(bytes.data()); + bytes = bytes.subspan(4); + if (size > bytes.size()) return false; + value = {bytes.data(), size}; + bytes = bytes.subspan(size); + return true; +} +} // namespace + +bool ReadAudioGroup(std::span bytes, AudioGroupView& result) { + AudioGroupView view; + if (!ReadString(bytes, view.baseDirectory) || !ReadString(bytes, view.name) || + !ReadSection(bytes, view.assets.pool) || !ReadSection(bytes, view.assets.project) || + !ReadSection(bytes, view.assets.samples) || !ReadSection(bytes, view.assets.directory)) + return false; + result = view; + return true; +} + +bool ReadAudioSongHeader(std::span bytes, AudioSongHeader& result) { + if (bytes.size() < 20) return false; + AudioSongHeader header{Read32(bytes.data()), Read32(bytes.data() + 4), Read32(bytes.data() + 8), + Read32(bytes.data() + 12), Read32(bytes.data() + 16)}; + if (header.song > 0xffff || header.group > 0xffff || header.length < 24 || header.length > 64 * 1024 * 1024) + return false; + result = header; + return true; +} + +bool ReadDSPHeader(std::span bytes, dspadpcm_header& result) { + if (bytes.size() < 0x60) return false; + const u8* p = bytes.data(); + dspadpcm_header header{}; + header.x0_numSamples = Read32(p); + header.x4_numNibbles = Read32(p + 4); + header.x8_sampleRate = Read32(p + 8); + header.xc_loopFlag = Read16(p + 12); + header.xe_format = Read16(p + 14); + header.x10_loopStartNibble = Read32(p + 16); + header.x14_loopEndNibble = Read32(p + 20); + header.x18_currentAddress = Read32(p + 24); + for (u32 i = 0; i < 16; ++i) header.x1c_coef[i / 2][i % 2] = static_cast(Read16(p + 28 + i * 2)); + header.x3c_gain = Read16(p + 60); + header.x3e_predScale = Read16(p + 62); + header.x40_hist1 = Read16(p + 64); + header.x42_hist2 = Read16(p + 66); + header.x44_loopPredScale = Read16(p + 68); + header.x46_loopHist1 = Read16(p + 70); + header.x48_loopHist2 = Read16(p + 72); + u64 samples = u64(header.x4_numNibbles / 16) * 14; + if (header.x4_numNibbles % 16 > 2) samples += header.x4_numNibbles % 16 - 2; + if (!header.x0_numSamples || header.x0_numSamples > samples || !header.x8_sampleRate || + header.x8_sampleRate > 65535 || header.xc_loopFlag > 1 || header.xe_format != 0 || + (header.xc_loopFlag && (header.x10_loopStartNibble >= header.x14_loopEndNibble || + header.x14_loopEndNibble >= header.x4_numNibbles))) + return false; + result = header; + return true; +} +} // namespace metaforce diff --git a/src/Metaforce/Runtime.cpp b/src/Metaforce/Runtime.cpp index d78aa4acec..ce6e8412f2 100644 --- a/src/Metaforce/Runtime.cpp +++ b/src/Metaforce/Runtime.cpp @@ -1,4 +1,5 @@ #include "Metaforce/Runtime.hpp" +#include "Metaforce/Audio.hpp" #include "Kyoto/Basics/COsContext.hpp" #include "Kyoto/CResFactory.hpp" @@ -325,7 +326,12 @@ int Initialize(int argc, char** argv) { cxxopts::value< AuroraBackend >()->default_value("auto"))( "warp", "Start at WORLD,AREA[,LAYERBITS][,0xRELAY...]", cxxopts::value< std::vector< std::string > >(), - "WORLD,AREA,...")("load-save", "Load save slot (1-3)", cxxopts::value< int >(), "N"); + "WORLD,AREA,...")("load-save", "Load save slot (1-3)", cxxopts::value< int >(), "N")( + "audio-rate", "Native audio rate (0 selects the opened device)", + cxxopts::value()->default_value("0"))( + "audio-channels", "Audio channels: 0 (device), 2, 4, 6 or 8", + cxxopts::value()->default_value("0"))( + "no-audio", "Advance sound without opening an audio device"); options.parse_positional("dvd"); options.positional_help(""); options.allow_unrecognised_options(); @@ -335,6 +341,12 @@ int Initialize(int argc, char** argv) { try { args = options.parse(argc, argv); standardOptions = borealis::cli::parse(args); + const auto rate = args["audio-rate"].as(); + const auto channels = args["audio-channels"].as(); + if ((rate && (rate < 8000 || rate > 96000)) || + (channels && channels != 2 && channels != 4 && channels != 6 && channels != 8)) + throw cxxopts::exceptions::parsing("Invalid audio configuration (8000..96000 Hz, 2/4/6/8 channels)"); + ConfigureAudio({rate, channels}, args.count("no-audio") != 0); if (args.count("warp") > 1 || args.count("load-save") > 1) { throw cxxopts::exceptions::parsing("--warp and --load-save may each be specified only once"); } @@ -433,6 +445,8 @@ int Initialize(int argc, char** argv) { void Shutdown() { startup.reset(); + if (sndIsInstalled()) sndQuit(); + sndPCStopAudio(); aurora_dvd_close(); aurora_shutdown(); borealis::log::shutdown(); @@ -444,6 +458,7 @@ bool HasStartupRequest() { return startup.has_value(); } bool BeginFrame() { limiter.Sleep(16670000); + UpdateAudio(); for (const AuroraEvent* event = aurora_update(); event && event->type != AURORA_NONE; ++event) { if (event->type == AURORA_EXIT) { shouldTerminate = true; diff --git a/src/Metaforce/Stubs.cpp b/src/Metaforce/Stubs.cpp index 6d4ccca11d..6319e1516d 100644 --- a/src/Metaforce/Stubs.cpp +++ b/src/Metaforce/Stubs.cpp @@ -18,7 +18,7 @@ #include namespace { -std::mutex interruptMutex; +std::recursive_mutex interruptMutex; thread_local bool interruptsEnabled = true; u32 soundMode = OS_SOUND_MODE_STEREO; u32 progressiveMode = 1; @@ -28,6 +28,11 @@ void* savedRegionStart = nullptr; void* savedRegionEnd = nullptr; } // namespace +namespace metaforce { +void LockAudio() { interruptMutex.lock(); } +void UnlockAudio() { interruptMutex.unlock(); } +} // namespace metaforce + extern "C" { void PPCSync() { std::atomic_thread_fence(std::memory_order_seq_cst); } void PPCSetFpIEEEMode() {} diff --git a/src/MetroidPrime/main.cpp b/src/MetroidPrime/main.cpp index afea9dce91..31ebde4e62 100644 --- a/src/MetroidPrime/main.cpp +++ b/src/MetroidPrime/main.cpp @@ -576,11 +576,9 @@ bool CGameArchitectureSupport::LoadAudio() { } bool CMain::LoadAudio() { -#if !defined(TARGET_PC) // TODO: audio if (x164_archSupport != nullptr) { return x164_archSupport->LoadAudio(); } -#endif return true; } From 6038af3d3d5146b30be1620dae27e7eda71e2209 Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Wed, 23 Sep 2026 13:41:47 -0700 Subject: [PATCH 2/5] Update musyx --- extern/musyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/musyx b/extern/musyx index b6ebaeb3b5..9119ece9ec 160000 --- a/extern/musyx +++ b/extern/musyx @@ -1 +1 @@ -Subproject commit b6ebaeb3b5c4fbd8cc84b8f2f44b25c5e251cdc0 +Subproject commit 9119ece9eccccad5d1f4885bbba0ffde8a4bd901 From 452df0eb7cf5cf01a17c8fe6d5b23580649fe666 Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Wed, 23 Sep 2026 13:57:43 -0700 Subject: [PATCH 3/5] Revert update --- extern/musyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/musyx b/extern/musyx index 9119ece9ec..c40c74718e 160000 --- a/extern/musyx +++ b/extern/musyx @@ -1 +1 @@ -Subproject commit 9119ece9eccccad5d1f4885bbba0ffde8a4bd901 +Subproject commit c40c74718e5fb7d296de483dc594c887d0b56b1a From 92ca76500a700f4abfb0cd674738ace87fd2a8d5 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 23 Sep 2026 15:00:34 -0600 Subject: [PATCH 4/5] Tidy --- CMakeLists.txt | 1 - include/Metaforce/Audio.hpp | 18 ++++ include/Metaforce/AudioAssets.hpp | 20 ---- src/Kyoto/Audio/CDSPStreamManager.cpp | 2 +- src/Kyoto/Audio/CMidiManager.cpp | 29 +++-- src/Kyoto/Audio/DolphinCAudioGroupSet.cpp | 17 +-- src/Metaforce/Audio.cpp | 126 ++++++++++++++++++++-- src/Metaforce/AudioAssets.cpp | 79 -------------- src/Metaforce/Runtime.cpp | 20 ++-- 9 files changed, 178 insertions(+), 134 deletions(-) delete mode 100644 include/Metaforce/AudioAssets.hpp delete mode 100644 src/Metaforce/AudioAssets.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 111d63fc43..5291c96c4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -697,7 +697,6 @@ set(GAME_FILES src/Metaforce/ARAMToken.cpp src/Metaforce/Alloc.cpp src/Metaforce/Audio.cpp - src/Metaforce/AudioAssets.cpp src/Metaforce/CModelSectionReader.cpp src/Metaforce/MapData.cpp src/Metaforce/PathData.cpp diff --git a/include/Metaforce/Audio.hpp b/include/Metaforce/Audio.hpp index f28a6a29ea..485320e583 100644 --- a/include/Metaforce/Audio.hpp +++ b/include/Metaforce/Audio.hpp @@ -1,16 +1,34 @@ #pragma once + #include +#include +#include + +struct dspadpcm_header; namespace metaforce { +struct AudioGroupView { + std::string_view baseDirectory, name; + SND_PC_GROUP_ASSETS assets{}; +}; +struct AudioSongHeader { + u32 version, song, group, audioGroup, length; +}; + void ConfigureAudio(SND_PC_CONFIG preferred, bool disabled); void InitializeAudio(u8 voices, u8 music, u8 sfx, u32 flags); void UpdateAudio(); void LockAudio(); void UnlockAudio(); + struct AudioLockGuard { AudioLockGuard() { LockAudio(); } ~AudioLockGuard() { UnlockAudio(); } AudioLockGuard(const AudioLockGuard&) = delete; AudioLockGuard& operator=(const AudioLockGuard&) = delete; }; + +bool ReadAudioGroup(std::span< const u8 > bytes, AudioGroupView& result); +bool ReadAudioSongHeader(std::span< const u8 > bytes, AudioSongHeader& result); +bool ReadDSPHeader(std::span< const u8 > bytes, dspadpcm_header& result); } // namespace metaforce diff --git a/include/Metaforce/AudioAssets.hpp b/include/Metaforce/AudioAssets.hpp deleted file mode 100644 index b207938aa0..0000000000 --- a/include/Metaforce/AudioAssets.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include -#include -#include - -struct dspadpcm_header; - -namespace metaforce { -struct AudioGroupView { - std::string_view baseDirectory, name; - SND_PC_GROUP_ASSETS assets{}; -}; -struct AudioSongHeader { - u32 version, song, group, audioGroup, length; -}; -bool ReadAudioGroup(std::span bytes, AudioGroupView& result); -bool ReadAudioSongHeader(std::span bytes, AudioSongHeader& result); -bool ReadDSPHeader(std::span bytes, dspadpcm_header& result); -} // namespace metaforce diff --git a/src/Kyoto/Audio/CDSPStreamManager.cpp b/src/Kyoto/Audio/CDSPStreamManager.cpp index 9bf9933ec3..5f6e87243d 100644 --- a/src/Kyoto/Audio/CDSPStreamManager.cpp +++ b/src/Kyoto/Audio/CDSPStreamManager.cpp @@ -8,9 +8,9 @@ #include "dolphin/os/OSThread.h" #include + #if defined(TARGET_PC) #include "Metaforce/Audio.hpp" -#include "Metaforce/AudioAssets.hpp" #endif CDSPStreamManager g_Streams[4] = {CDSPStreamManager(), CDSPStreamManager(), CDSPStreamManager(), diff --git a/src/Kyoto/Audio/CMidiManager.cpp b/src/Kyoto/Audio/CMidiManager.cpp index 68978aab63..881742b692 100644 --- a/src/Kyoto/Audio/CMidiManager.cpp +++ b/src/Kyoto/Audio/CMidiManager.cpp @@ -5,20 +5,25 @@ #include #include + #if defined(TARGET_PC) -#include "Metaforce/AudioAssets.hpp" +#include "Metaforce/Audio.hpp" #include "Metaforce/Common.hpp" -namespace { constexpr borealis::Log Log{"song assets"}; } + +namespace { +constexpr borealis::Log Log{"CMidiManager"}; +} #endif rstl::reserved_vector< CMidiManager::CMidiWrapper, 3 > CMidiManager::mMidiWrappers; CMidiManager::CMidiWrapper::CMidiWrapper() +: x0_sysHandle(0) #if defined(TARGET_PC) -: x0_sysHandle(0), x8_songId(-1), xa_available(true) {} -#else -: x0_sysHandle(0), xa_available(true) {} +, x8_songId(-1) #endif +, xa_available(true) { +} const CSfxHandle& CMidiManager::CMidiWrapper::GetManagerHandle() const { return x4_midiHandle; } @@ -72,11 +77,12 @@ CSfxHandle CMidiManager::Play(const CMidiData& data, unsigned short fadeTime, bo wrapper.SetAudioSysHandle(sysHandle); wrapper.SetSongId(data.GetSongId()); } else { - u32 sysHandle = CAudioSys::SeqPlayEx(data.GetGroupId(), data.GetSongId(), data.GetData(), nullptr, 0); + u32 sysHandle = + CAudioSys::SeqPlayEx(data.GetGroupId(), data.GetSongId(), data.GetData(), nullptr, 0); #if defined(TARGET_PC) if (sysHandle == SND_ID_ERROR) { wrapper.SetAvailable(true); - return CSfxHandle(); + return {}; } #endif if (fadeTime != 0) { @@ -138,12 +144,14 @@ CMidiManager::CMidiData::CMidiData(CInputStream& in) u8 bytes[20]; metaforce::AudioSongHeader header{}; REQUIRE(in.ReadBytes(bytes, sizeof(bytes)) == sizeof(bytes) && - metaforce::ReadAudioSongHeader(bytes, header), "Invalid CSNG header"); + metaforce::ReadAudioSongHeader(bytes, header), + "Invalid CSNG header"); x0_songId = header.song; x2_groupId = header.group; x4_agscId = header.audioGroup; x8_data = rs_new uchar[header.length]; - REQUIRE(in.ReadBytes(x8_data.get(), header.length) == header.length, "Truncated CSNG arrangement"); + REQUIRE(in.ReadBytes(x8_data.get(), header.length) == header.length, + "Truncated CSNG arrangement"); SND_PC_ASSET_ERROR error{}; REQUIRE(sndPCValidateArrangement({x8_data.get(), header.length}, &error), "Invalid CSNG arrangement at {}: {}", error.offset, error.reason ? error.reason : ""); @@ -158,6 +166,7 @@ CMidiManager::CMidiData::CMidiData(CInputStream& in) #endif } -const CFactoryFnReturn FMidiDataFactory(const SObjectTag& tag, CInputStream& in, const CVParamTransfer&) { +const CFactoryFnReturn FMidiDataFactory(const SObjectTag& tag, CInputStream& in, + const CVParamTransfer&) { return rs_new CMidiManager::CMidiData(in); } diff --git a/src/Kyoto/Audio/DolphinCAudioGroupSet.cpp b/src/Kyoto/Audio/DolphinCAudioGroupSet.cpp index 26c3eb794f..5da79134c5 100644 --- a/src/Kyoto/Audio/DolphinCAudioGroupSet.cpp +++ b/src/Kyoto/Audio/DolphinCAudioGroupSet.cpp @@ -6,10 +6,14 @@ #include "Kyoto/Alloc/CMemory.hpp" #include "rstl/auto_ptr.hpp" #include + #if defined(TARGET_PC) -#include "Metaforce/AudioAssets.hpp" +#include "Metaforce/Audio.hpp" #include "Metaforce/Common.hpp" -namespace { constexpr borealis::Log Log{"audio assets"}; } + +namespace { +constexpr borealis::Log Log{"CAudioGroupSet"}; +} #endif CAudioGroupSet::CAudioGroupSet(const TLockedToken< CAudioGrpSetLoc >& group) @@ -41,15 +45,15 @@ CAudioGrpSetLoc::CAudioGrpSetLoc(const rstl::auto_ptr< uchar >& data, int length x20_groupSetName = rstl::string(view.name.data(), view.name.size()); const size_t poolSize = (view.assets.pool.size + 3) & ~size_t(3); const size_t projectSize = (view.assets.project.size + 3) & ~size_t(3); - x8_groupData = rstl::auto_ptr(static_cast( - CMemory::Alloc(poolSize + projectSize + view.assets.directory.size, IAllocator::kHI_RoundUpLen))); + x8_groupData = rstl::auto_ptr< uchar >(static_cast< uchar* >(CMemory::Alloc( + poolSize + projectSize + view.assets.directory.size, IAllocator::kHI_RoundUpLen))); x34_pool = x8_groupData.get(); x38_project = x34_pool + poolSize; x3c_sampleDir = x38_project + projectSize; memcpy(x34_pool, view.assets.pool.data, view.assets.pool.size); memcpy(x38_project, view.assets.project.data, view.assets.project.size); memcpy(x3c_sampleDir, view.assets.directory.data, view.assets.directory.size); - x40_samples = const_cast(static_cast(view.assets.samples.data)); + x40_samples = const_cast< uchar* >(static_cast< const uchar* >(view.assets.samples.data)); x30_aramSize = view.assets.samples.size; #else uint readPosition; @@ -58,7 +62,8 @@ CAudioGrpSetLoc::CAudioGrpSetLoc(const rstl::auto_ptr< uchar >& data, int length const uint projectOffset = readPosition + poolSize; #if TARGET_LITTLE_ENDIAN - const uint projectSize = CBasics::SwapBytes(*reinterpret_cast< uint* >(data.get() + projectOffset)); + const uint projectSize = + CBasics::SwapBytes(*reinterpret_cast< uint* >(data.get() + projectOffset)); #else const uint projectSize = *reinterpret_cast< uint* >(data.get() + projectOffset); CAudioSys::GetVerbose(); diff --git a/src/Metaforce/Audio.cpp b/src/Metaforce/Audio.cpp index 864849db95..3994bca864 100644 --- a/src/Metaforce/Audio.cpp +++ b/src/Metaforce/Audio.cpp @@ -1,14 +1,43 @@ #include "Metaforce/Audio.hpp" +#include "Kyoto/Audio/CDSPStream.hpp" +#include "Metaforce/Endian.hpp" #include -#include +#include +#include namespace metaforce { namespace { -constexpr borealis::Log Log{"audio"}; +constexpr borealis::Log Log{"metaforce::audio"}; + SND_PC_CONFIG preferredConfig{}; bool disabled, offline; u32 frameRemainder; + +bool ReadString(std::span< const u8 >& bytes, std::string_view& value) { + const auto* end = static_cast< const u8* >(std::memchr(bytes.data(), 0, bytes.size())); + if (!end) { + return false; + } + size_t size = end - bytes.data(); + value = {reinterpret_cast< const char* >(bytes.data()), size}; + bytes = bytes.subspan(size + 1); + return true; +} + +bool ReadSection(std::span< const u8 >& bytes, SND_PC_SPAN& value) { + if (bytes.size() < 4) { + return false; + } + u32 size = read_bits< uint >(bytes.data()); + bytes = bytes.subspan(4); + if (size > bytes.size()) { + return false; + } + value = {bytes.data(), size}; + bytes = bytes.subspan(size); + return true; +} } // namespace void ConfigureAudio(SND_PC_CONFIG preferred, bool noDevice) { @@ -21,22 +50,33 @@ void InitializeAudio(u8 voices, u8 music, u8 sfx, u32 flags) { SND_PC_CONFIG selected = preferredConfig; bool opened = !disabled && sndPCOpenAudio(&preferredConfig, &selected); if (!opened) { - if (!selected.mixRate) selected.mixRate = 48000; - if (!selected.channels) selected.channels = 2; - if (!sndPCConfigure(&selected)) throw std::runtime_error("Invalid audio configuration"); + if (!selected.mixRate) { + selected.mixRate = 48000; + } + if (!selected.channels) { + selected.channels = 2; + } + if (!sndPCConfigure(&selected)) { + Log.fatal("Invalid audio configuration"); + } } if (sndInit(voices, music, sfx, 1, flags, 0) != 0) { sndPCStopAudio(); - throw std::runtime_error("MusyX initialization failed"); + Log.fatal("MusyX initialization failed"); } offline = !opened || !sndPCStartAudio(); frameRemainder = 0; - if (offline && !disabled) Log.warn("Audio device unavailable; advancing sound without playback"); - Log.info("MusyX: {} Hz, {} channels{}", selected.mixRate, selected.channels, offline ? " (silent)" : ""); + if (offline && !disabled) { + Log.warn("Audio device unavailable"); + } + Log.info("MusyX initialized: {} Hz, {} channels{}", selected.mixRate, selected.channels, + offline ? " (silent)" : ""); } void UpdateAudio() { - if (!offline || !sndIsInstalled()) return; + if (!offline || !sndIsInstalled()) { + return; + } const auto info = sndPCGetRenderInfo(); // The game advances at 60 logical frames per second, including headless runs. frameRemainder += info.mixRate; @@ -44,4 +84,72 @@ void UpdateAudio() { sndPCRender(discarded, frameRemainder / 60); frameRemainder %= 60; } + +bool ReadAudioGroup(std::span< const u8 > bytes, AudioGroupView& result) { + AudioGroupView view; + if (!ReadString(bytes, view.baseDirectory) || !ReadString(bytes, view.name) || + !ReadSection(bytes, view.assets.pool) || !ReadSection(bytes, view.assets.project) || + !ReadSection(bytes, view.assets.samples) || !ReadSection(bytes, view.assets.directory)) { + return false; + } + result = view; + return true; +} + +bool ReadAudioSongHeader(std::span< const u8 > bytes, AudioSongHeader& result) { + if (bytes.size() < 20) { + return false; + } + const AudioSongHeader header{ + .version = read_bits< uint >(bytes.data()), + .song = read_bits< uint >(bytes.data() + 4), + .group = read_bits< uint >(bytes.data() + 8), + .audioGroup = read_bits< uint >(bytes.data() + 12), + .length = read_bits< uint >(bytes.data() + 16), + }; + if (header.song > 0xffff || header.group > 0xffff || header.length < 24 || + header.length > 64 * 1024 * 1024) { + return false; + } + result = header; + return true; +} + +bool ReadDSPHeader(std::span< const u8 > bytes, dspadpcm_header& result) { + if (bytes.size() < 0x60) { + return false; + } + const u8* p = bytes.data(); + dspadpcm_header header{}; + header.x0_numSamples = read_bits< uint >(p); + header.x4_numNibbles = read_bits< uint >(p + 4); + header.x8_sampleRate = read_bits< uint >(p + 8); + header.xc_loopFlag = read_bits< ushort >(p + 12); + header.xe_format = read_bits< ushort >(p + 14); + header.x10_loopStartNibble = read_bits< uint >(p + 16); + header.x14_loopEndNibble = read_bits< uint >(p + 20); + header.x18_currentAddress = read_bits< uint >(p + 24); + for (u32 i = 0; i < 16; ++i) { + header.x1c_coef[i / 2][i % 2] = read_bits< short >(p + 28 + i * 2); + } + header.x3c_gain = read_bits< short >(p + 60); + header.x3e_predScale = read_bits< short >(p + 62); + header.x40_hist1 = read_bits< short >(p + 64); + header.x42_hist2 = read_bits< short >(p + 66); + header.x44_loopPredScale = read_bits< short >(p + 68); + header.x46_loopHist1 = read_bits< short >(p + 70); + header.x48_loopHist2 = read_bits< short >(p + 72); + u64 samples = static_cast< u64 >(header.x4_numNibbles / 16) * 14; + if (header.x4_numNibbles % 16 > 2) { + samples += header.x4_numNibbles % 16 - 2; + } + if (!header.x0_numSamples || header.x0_numSamples > samples || !header.x8_sampleRate || + header.x8_sampleRate > 65535 || header.xc_loopFlag > 1 || header.xe_format != 0 || + (header.xc_loopFlag && (header.x10_loopStartNibble >= header.x14_loopEndNibble || + header.x14_loopEndNibble >= header.x4_numNibbles))) { + return false; + } + result = header; + return true; +} } // namespace metaforce diff --git a/src/Metaforce/AudioAssets.cpp b/src/Metaforce/AudioAssets.cpp deleted file mode 100644 index e2444d4ada..0000000000 --- a/src/Metaforce/AudioAssets.cpp +++ /dev/null @@ -1,79 +0,0 @@ -#include "Metaforce/AudioAssets.hpp" -#include "Kyoto/Audio/CDSPStream.hpp" - -#include - -namespace metaforce { -namespace { -u16 Read16(const u8* p) { return (u16(p[0]) << 8) | p[1]; } -u32 Read32(const u8* p) { return (u32(Read16(p)) << 16) | Read16(p + 2); } -bool ReadString(std::span& bytes, std::string_view& value) { - const auto* end = static_cast(std::memchr(bytes.data(), 0, bytes.size())); - if (!end) return false; - size_t size = end - bytes.data(); - value = {reinterpret_cast(bytes.data()), size}; - bytes = bytes.subspan(size + 1); - return true; -} -bool ReadSection(std::span& bytes, SND_PC_SPAN& value) { - if (bytes.size() < 4) return false; - u32 size = Read32(bytes.data()); - bytes = bytes.subspan(4); - if (size > bytes.size()) return false; - value = {bytes.data(), size}; - bytes = bytes.subspan(size); - return true; -} -} // namespace - -bool ReadAudioGroup(std::span bytes, AudioGroupView& result) { - AudioGroupView view; - if (!ReadString(bytes, view.baseDirectory) || !ReadString(bytes, view.name) || - !ReadSection(bytes, view.assets.pool) || !ReadSection(bytes, view.assets.project) || - !ReadSection(bytes, view.assets.samples) || !ReadSection(bytes, view.assets.directory)) - return false; - result = view; - return true; -} - -bool ReadAudioSongHeader(std::span bytes, AudioSongHeader& result) { - if (bytes.size() < 20) return false; - AudioSongHeader header{Read32(bytes.data()), Read32(bytes.data() + 4), Read32(bytes.data() + 8), - Read32(bytes.data() + 12), Read32(bytes.data() + 16)}; - if (header.song > 0xffff || header.group > 0xffff || header.length < 24 || header.length > 64 * 1024 * 1024) - return false; - result = header; - return true; -} - -bool ReadDSPHeader(std::span bytes, dspadpcm_header& result) { - if (bytes.size() < 0x60) return false; - const u8* p = bytes.data(); - dspadpcm_header header{}; - header.x0_numSamples = Read32(p); - header.x4_numNibbles = Read32(p + 4); - header.x8_sampleRate = Read32(p + 8); - header.xc_loopFlag = Read16(p + 12); - header.xe_format = Read16(p + 14); - header.x10_loopStartNibble = Read32(p + 16); - header.x14_loopEndNibble = Read32(p + 20); - header.x18_currentAddress = Read32(p + 24); - for (u32 i = 0; i < 16; ++i) header.x1c_coef[i / 2][i % 2] = static_cast(Read16(p + 28 + i * 2)); - header.x3c_gain = Read16(p + 60); - header.x3e_predScale = Read16(p + 62); - header.x40_hist1 = Read16(p + 64); - header.x42_hist2 = Read16(p + 66); - header.x44_loopPredScale = Read16(p + 68); - header.x46_loopHist1 = Read16(p + 70); - header.x48_loopHist2 = Read16(p + 72); - u64 samples = u64(header.x4_numNibbles / 16) * 14; - if (header.x4_numNibbles % 16 > 2) samples += header.x4_numNibbles % 16 - 2; - if (!header.x0_numSamples || header.x0_numSamples > samples || !header.x8_sampleRate || - header.x8_sampleRate > 65535 || header.xc_loopFlag > 1 || header.xe_format != 0 || - (header.xc_loopFlag && (header.x10_loopStartNibble >= header.x14_loopEndNibble || - header.x14_loopEndNibble >= header.x4_numNibbles))) - return false; - result = header; - return true; -} -} // namespace metaforce diff --git a/src/Metaforce/Runtime.cpp b/src/Metaforce/Runtime.cpp index ce6e8412f2..76ef9b8918 100644 --- a/src/Metaforce/Runtime.cpp +++ b/src/Metaforce/Runtime.cpp @@ -327,11 +327,11 @@ int Initialize(int argc, char** argv) { "warp", "Start at WORLD,AREA[,LAYERBITS][,0xRELAY...]", cxxopts::value< std::vector< std::string > >(), "WORLD,AREA,...")("load-save", "Load save slot (1-3)", cxxopts::value< int >(), "N")( - "audio-rate", "Native audio rate (0 selects the opened device)", + "audio-rate", "Audio sample rate (0 for default)", cxxopts::value()->default_value("0"))( - "audio-channels", "Audio channels: 0 (device), 2, 4, 6 or 8", + "audio-channels", "Audio channels: 0 (default), 2, 4, 6 or 8", cxxopts::value()->default_value("0"))( - "no-audio", "Advance sound without opening an audio device"); + "no-audio", "Disable audio output"); options.parse_positional("dvd"); options.positional_help(""); options.allow_unrecognised_options(); @@ -341,11 +341,13 @@ int Initialize(int argc, char** argv) { try { args = options.parse(argc, argv); standardOptions = borealis::cli::parse(args); - const auto rate = args["audio-rate"].as(); - const auto channels = args["audio-channels"].as(); + const auto rate = args["audio-rate"].as< unsigned int >(); + const auto channels = args["audio-channels"].as< unsigned int >(); if ((rate && (rate < 8000 || rate > 96000)) || - (channels && channels != 2 && channels != 4 && channels != 6 && channels != 8)) - throw cxxopts::exceptions::parsing("Invalid audio configuration (8000..96000 Hz, 2/4/6/8 channels)"); + (channels && channels != 2 && channels != 4 && channels != 6 && channels != 8)) { + throw cxxopts::exceptions::parsing( + "Invalid audio configuration (8000..96000 Hz, 2/4/6/8 channels)"); + } ConfigureAudio({rate, channels}, args.count("no-audio") != 0); if (args.count("warp") > 1 || args.count("load-save") > 1) { throw cxxopts::exceptions::parsing("--warp and --load-save may each be specified only once"); @@ -445,7 +447,9 @@ int Initialize(int argc, char** argv) { void Shutdown() { startup.reset(); - if (sndIsInstalled()) sndQuit(); + if (sndIsInstalled()) { + sndQuit(); + } sndPCStopAudio(); aurora_dvd_close(); aurora_shutdown(); From 1885a76132b80d9eb0a31c4cb94d435dc016c46c Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Wed, 23 Sep 2026 14:32:01 -0700 Subject: [PATCH 5/5] Update musyx, for real this time --- extern/musyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/musyx b/extern/musyx index c40c74718e..9fc772d478 160000 --- a/extern/musyx +++ b/extern/musyx @@ -1 +1 @@ -Subproject commit c40c74718e5fb7d296de483dc594c887d0b56b1a +Subproject commit 9fc772d4787ec728361c29301248db1a6942167c