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) <[email protected]>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-09-29 17:14:00 -06:00
1 parent ae460c9e1a
commit b9ea98d2c5
6 files changed
+47 -17

No files matched your search

+2 -1
View File
@@ -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;
+13 -12
View File
@@ -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 &params, 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();
}
+13 -4
View File
@@ -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 &params, 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) {
+3
View File
@@ -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<u8> dumpBuffer_; // Used for dumping audio data to files.
bool dumped_ = false; // Whether we already dumped the audio data to a file.
};
+14
View File
@@ -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.
+2
View File
@@ -128,6 +128,8 @@ class AudioDecoder;
extern std::map<u32, AudioDecoder *> 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.