From b9ea98d2c5b8d07330af5efbbefd7b143d40d32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 29 Sep 2026 14:02:45 -0600 Subject: [PATCH] Atrac3: Set up the decoder the way libatrac3plus does libatrac3plus picks the codec parameter from the frame size and the header's joint stereo flag, and the channel count plays no part. We guessed joint stereo from the frame size and channel count instead, so LocoRoco 2's MuiMui house music never started: the game writes a 2-channel normal-stereo header for every track it streams, and that 0xC0 track holds one mono sound unit per frame. Taken as joint stereo, its first frame failed during setup, and the game retried forever. Now the joint stereo flag comes from the track header, and the decoder's channel count from the parameter it maps to, so that track decodes as mono into both output channels, as on hardware. Low-level decoding, which has no header, still goes by the frame size. Atrac2 saves the flag; older states fall back to the guess. Fixes #8647. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/HLE/AtracBase.h | 3 ++- Core/HLE/AtracCtx.cpp | 25 +++++++++++++------------ Core/HLE/AtracCtx2.cpp | 17 +++++++++++++---- Core/HLE/AtracCtx2.h | 3 +++ Core/HLE/sceAudiocodec.cpp | 14 ++++++++++++++ Core/HLE/sceAudiocodec.h | 2 ++ 6 files changed, 47 insertions(+), 17 deletions(-) diff --git a/Core/HLE/AtracBase.h b/Core/HLE/AtracBase.h index 73b373f7b9..e5329befa1 100644 --- a/Core/HLE/AtracBase.h +++ b/Core/HLE/AtracBase.h @@ -206,7 +206,8 @@ public: return decoder_; } - void CreateDecoder(int codecType, int bytesPerFrame, int channels); + // jointStereo only matters for Atrac3. Take it from the track header where there is one. + void CreateDecoder(int codecType, int bytesPerFrame, int channels, bool jointStereo); virtual void NotifyGetContextAddress() = 0; diff --git a/Core/HLE/AtracCtx.cpp b/Core/HLE/AtracCtx.cpp index a18337ea90..8ae9d2814a 100644 --- a/Core/HLE/AtracCtx.cpp +++ b/Core/HLE/AtracCtx.cpp @@ -141,7 +141,7 @@ void Atrac::DoState(PointerWrap &p) { // Make sure to do this late; it depends on track parameters. if (p.mode == p.MODE_READ && bufferState_ != ATRAC_STATUS_NO_DATA) { - CreateDecoder(track_.codecType, track_.bytesPerFrame, track_.channels); + CreateDecoder(track_.codecType, track_.bytesPerFrame, track_.channels, track_.jointStereo != 0); } if (s >= 2 && s < 9) { @@ -301,26 +301,27 @@ void Atrac::CalculateStreamInfo(u32 *outReadOffset) { } } -void AtracBase::CreateDecoder(int codecType, int bytesPerFrame, int channels) { +void AtracBase::CreateDecoder(int codecType, int bytesPerFrame, int channels, bool jointStereo) { if (decoder_) { delete decoder_; } // First, init the standalone decoder. if (codecType == PSP_CODEC_AT3) { - // TODO: This is maybe not entirely reliable? Mui Mui house in LocoRoco 2 fails. Although also fails - // when I override this, so maybe the issue is something different... - bool jointStereo = IsAtrac3StreamJointStereo(codecType, bytesPerFrame, channels); - - // We don't pull this from the RIFF so that we can support OMA also. + // The layout comes from the frame size and the joint stereo flag, as on hardware. A mono + // decoder still fills both output channels when the track says stereo. + int decoderChannels = channels; + if (!Atrac3DecoderChannels(bytesPerFrame, jointStereo, &decoderChannels)) { + WARN_LOG(Log::ME, "Atrac3: %d-byte frames with joint stereo %d aren't in libatrac3plus's table, decoding as %d channels", bytesPerFrame, (int)jointStereo, channels); + } + // Built here rather than taken from the RIFF, so that OMA works too. uint8_t extraData[14]{}; - // The only thing that changes are the jointStereo_ values. extraData[0] = 1; - extraData[3] = channels << 3; + extraData[3] = decoderChannels << 3; extraData[6] = jointStereo; extraData[8] = jointStereo; extraData[10] = 1; - decoder_ = CreateAtrac3Audio(channels, bytesPerFrame, extraData, sizeof(extraData)); + decoder_ = CreateAtrac3Audio(decoderChannels, bytesPerFrame, extraData, sizeof(extraData)); } else { decoder_ = CreateAtrac3PlusAudio(channels, bytesPerFrame); } @@ -465,7 +466,7 @@ int Atrac::SetData(const Track &track, u32 buffer, u32 readSize, u32 bufferSize, u32 copybytes = std::min(bufferSize, track_.fileSize); Memory::Memcpy(dataBuf_, buffer, copybytes, "AtracSetData"); } - CreateDecoder(track.codecType, track.bytesPerFrame, track.channels); + CreateDecoder(track.codecType, track.bytesPerFrame, track.channels, track.jointStereo != 0); INFO_LOG(Log::Atrac, "Atrac::SetData (buffer=%08x, readSize=%d, bufferSize=%d): %s %s (%d channels) audio", buffer, readSize, bufferSize, codecName, channelName, track_.channels); INFO_LOG(Log::Atrac, "BufferState: %s", AtracStatusToString(bufferState_)); INFO_LOG(Log::Atrac, @@ -942,7 +943,7 @@ void Atrac::InitLowLevel(const Atrac3LowLevelParams ¶ms, int codecType) { track_.fileSize = track_.bytesPerFrame; // not really meaningful bufferState_ = ATRAC_STATUS_LOW_LEVEL; currentSample_ = 0; - CreateDecoder(codecType, track_.bytesPerFrame, track_.channels); + CreateDecoder(codecType, track_.bytesPerFrame, track_.channels, track_.jointStereo != 0); WriteContextToPSPMem(); } diff --git a/Core/HLE/AtracCtx2.cpp b/Core/HLE/AtracCtx2.cpp index 5e7d4f5858..501876cb0a 100644 --- a/Core/HLE/AtracCtx2.cpp +++ b/Core/HLE/AtracCtx2.cpp @@ -337,7 +337,7 @@ void Atrac2::DumpBufferToFile() { } void Atrac2::DoState(PointerWrap &p) { - auto s = p.Section("Atrac2", 1, 3); + auto s = p.Section("Atrac2", 1, 4); if (!s) return; @@ -362,8 +362,14 @@ void Atrac2::DoState(PointerWrap &p) { } const SceAtracIdInfo &info = context_->info; + if (s >= 4) { + Do(p, jointStereo_); + } else if (p.mode == p.MODE_READ) { + jointStereo_ = IsAtrac3StreamJointStereo(info.codec, info.sampleSize, info.numChan); + } + if (p.mode == p.MODE_READ && info.state != ATRAC_STATUS_NO_DATA) { - CreateDecoder(info.codec, info.sampleSize, info.numChan); + CreateDecoder(info.codec, info.sampleSize, info.numChan, jointStereo_); } } @@ -1004,7 +1010,8 @@ int Atrac2::SetData(const Track &track, u32 bufferAddr, u32 readSize, u32 buffer SceAtracIdInfo &info = context_->info; - CreateDecoder(info.codec, info.sampleSize, info.numChan); + jointStereo_ = track.jointStereo != 0; + CreateDecoder(info.codec, info.sampleSize, info.numChan, jointStereo_); outputChannels_ = outputChannels; @@ -1140,7 +1147,9 @@ void Atrac2::InitLowLevel(const Atrac3LowLevelParams ¶ms, int codecType) { info.dataOff = 0; info.decodePos = 0; info.state = ATRAC_STATUS_LOW_LEVEL; - CreateDecoder(codecType, info.sampleSize, info.numChan); + // There's no track header here, so go by the bitrate. + jointStereo_ = IsAtrac3StreamJointStereo(codecType, info.sampleSize, info.numChan); + CreateDecoder(codecType, info.sampleSize, info.numChan, jointStereo_); } int Atrac2::DecodeLowLevel(const u8 *srcData, int *bytesConsumed, s16 *dstData, int *bytesWritten) { diff --git a/Core/HLE/AtracCtx2.h b/Core/HLE/AtracCtx2.h index ab553aa4c5..1ee03dfd89 100644 --- a/Core/HLE/AtracCtx2.h +++ b/Core/HLE/AtracCtx2.h @@ -77,6 +77,9 @@ private: // But it doesn't really matter whether it's here or there. AtracSasStreamState sas_; + // From the track header (RIFF fmt or OMA). The bitrate alone doesn't determine it (#8647). + bool jointStereo_ = false; + std::vector dumpBuffer_; // Used for dumping audio data to files. bool dumped_ = false; // Whether we already dumped the audio data to a file. }; diff --git a/Core/HLE/sceAudiocodec.cpp b/Core/HLE/sceAudiocodec.cpp index c6947622c8..d16e2736ed 100644 --- a/Core/HLE/sceAudiocodec.cpp +++ b/Core/HLE/sceAudiocodec.cpp @@ -757,6 +757,20 @@ static bool Atrac3LayoutFromContext(const SceAudiocodecCodec *ctx, int *bytesPer return true; } +// libatrac3plus.prx's SetData picks the parameter by frame size and the header's joint +// stereo flag, scanning from the last entry, and the channel count plays no part. LocoRoco 2 writes +// 2 channels into every track header it builds, and its 0xC0 MuiMui house track is mono (#8647). +// No match fails SetData with 0x80630008. +bool Atrac3DecoderChannels(int bytesPerFrame, bool jointStereo, int *channels) { + for (int i = ARRAY_SIZE(at3Params) - 1; i >= 0; i--) { + if (at3Params[i].bytes == bytesPerFrame && (at3Params[i].jointStereo != 0) == jointStereo) { + *channels = at3Params[i].channels; + return true; + } + } + return false; +} + bool IsAtrac3StreamJointStereo(int codecType, int bytesPerFrame, int channels) { if (codecType != PSP_CODEC_AT3) { // Well, might actually be, but it's not used in codec setup. diff --git a/Core/HLE/sceAudiocodec.h b/Core/HLE/sceAudiocodec.h index 138005cd3a..15f51bd3c4 100644 --- a/Core/HLE/sceAudiocodec.h +++ b/Core/HLE/sceAudiocodec.h @@ -128,6 +128,8 @@ class AudioDecoder; extern std::map g_audioDecoderContexts; bool IsAtrac3StreamJointStereo(int codecType, int bytesPerFrame, int channels); +// The channel count the Atrac3 decoder for a track works with, which can differ from the header's. +bool Atrac3DecoderChannels(int bytesPerFrame, bool jointStereo, int *channels); // ME time at the default clock, for sceAtrac too (it drives the same decoder): setting up a decoder, // and decoding one frame. AudioCodecDecodeUs takes Atrac3 and Atrac3+ only.