diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ce651a66b..5291c96c4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -696,6 +696,7 @@ set(GAME_FILES extern/rstl/src/rstl/rstl_strings.cpp src/Metaforce/ARAMToken.cpp src/Metaforce/Alloc.cpp + src/Metaforce/Audio.cpp src/Metaforce/CModelSectionReader.cpp src/Metaforce/MapData.cpp src/Metaforce/PathData.cpp diff --git a/extern/musyx b/extern/musyx index 1735ee9ffe..9fc772d478 160000 --- a/extern/musyx +++ b/extern/musyx @@ -1 +1 @@ -Subproject commit 1735ee9ffe8dfc54e5446880b43b8b9b4a620d09 +Subproject commit 9fc772d4787ec728361c29301248db1a6942167c 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..485320e583 --- /dev/null +++ b/include/Metaforce/Audio.hpp @@ -0,0 +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/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..5f6e87243d 100644 --- a/src/Kyoto/Audio/CDSPStreamManager.cpp +++ b/src/Kyoto/Audio/CDSPStreamManager.cpp @@ -9,6 +9,10 @@ #include +#if defined(TARGET_PC) +#include "Metaforce/Audio.hpp" +#endif + CDSPStreamManager g_Streams[4] = {CDSPStreamManager(), CDSPStreamManager(), CDSPStreamManager(), CDSPStreamManager()}; static int sHandleCounter; @@ -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..881742b692 100644 --- a/src/Kyoto/Audio/CMidiManager.cpp +++ b/src/Kyoto/Audio/CMidiManager.cpp @@ -6,9 +6,24 @@ #include #include +#if defined(TARGET_PC) +#include "Metaforce/Audio.hpp" +#include "Metaforce/Common.hpp" + +namespace { +constexpr borealis::Log Log{"CMidiManager"}; +} +#endif + rstl::reserved_vector< CMidiManager::CMidiWrapper, 3 > CMidiManager::mMidiWrappers; -CMidiManager::CMidiWrapper::CMidiWrapper() : x0_sysHandle(0), xa_available(true) {} +CMidiManager::CMidiWrapper::CMidiWrapper() +: x0_sysHandle(0) +#if defined(TARGET_PC) +, x8_songId(-1) +#endif +, xa_available(true) { +} const CSfxHandle& CMidiManager::CMidiWrapper::GetManagerHandle() const { return x4_midiHandle; } @@ -39,6 +54,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; } @@ -57,7 +77,14 @@ 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 {}; + } +#endif if (fadeTime != 0) { CAudioSys::SeqVolume(0, 0, sysHandle, 0); } @@ -113,6 +140,22 @@ 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,8 +163,10 @@ 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&) { +const CFactoryFnReturn FMidiDataFactory(const SObjectTag& tag, CInputStream& in, + const CVParamTransfer&) { return rs_new CMidiManager::CMidiData(in); } 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..5da79134c5 100644 --- a/src/Kyoto/Audio/DolphinCAudioGroupSet.cpp +++ b/src/Kyoto/Audio/DolphinCAudioGroupSet.cpp @@ -7,6 +7,15 @@ #include "rstl/auto_ptr.hpp" #include +#if defined(TARGET_PC) +#include "Metaforce/Audio.hpp" +#include "Metaforce/Common.hpp" + +namespace { +constexpr borealis::Log Log{"CAudioGroupSet"}; +} +#endif + CAudioGroupSet::CAudioGroupSet(const TLockedToken< CAudioGrpSetLoc >& group) : x0_baseDir(group->GetBaseDirName()) , x10_groupSetName(group->GetGroupSetName()) @@ -25,17 +34,38 @@ 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< 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< uchar* >(static_cast< const uchar* >(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)); + 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 +101,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..3994bca864 --- /dev/null +++ b/src/Metaforce/Audio.cpp @@ -0,0 +1,155 @@ +#include "Metaforce/Audio.hpp" +#include "Kyoto/Audio/CDSPStream.hpp" +#include "Metaforce/Endian.hpp" + +#include +#include +#include + +namespace metaforce { +namespace { +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) { + 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)) { + Log.fatal("Invalid audio configuration"); + } + } + if (sndInit(voices, music, sfx, 1, flags, 0) != 0) { + sndPCStopAudio(); + Log.fatal("MusyX initialization failed"); + } + offline = !opened || !sndPCStartAudio(); + frameRemainder = 0; + 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; + } + 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; +} + +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/Runtime.cpp b/src/Metaforce/Runtime.cpp index d78aa4acec..76ef9b8918 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", "Audio sample rate (0 for default)", + cxxopts::value()->default_value("0"))( + "audio-channels", "Audio channels: 0 (default), 2, 4, 6 or 8", + cxxopts::value()->default_value("0"))( + "no-audio", "Disable audio output"); options.parse_positional("dvd"); options.positional_help(""); options.allow_unrecognised_options(); @@ -335,6 +341,14 @@ int Initialize(int argc, char** argv) { try { args = options.parse(argc, argv); standardOptions = borealis::cli::parse(args); + 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)"); + } + 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 +447,10 @@ int Initialize(int argc, char** argv) { void Shutdown() { startup.reset(); + if (sndIsInstalled()) { + sndQuit(); + } + sndPCStopAudio(); aurora_dvd_close(); aurora_shutdown(); borealis::log::shutdown(); @@ -444,6 +462,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; }