From 8bf26365beb9b67a971962f05cb89fcfcf1bf59c Mon Sep 17 00:00:00 2001 From: David Racine Date: Sun, 30 Aug 2026 10:03:45 -0400 Subject: [PATCH] Honour the loop end when reading streamed music (#7129) Streamed samples were read a whole block at a time regardless of where the loop ended, so playback ran past it and only then jumped back. The seam therefore landed at a random offset up to a block late instead of where the sample asked for it, which is why a loop that is clean in an editor pops in game. Fixes #5780. Claude-Session: https://claude.ai/code/session_011Ex3z29fQzPEZgwH4EA641 Co-authored-by: Claude Opus 5 --- soh/src/code/audio_synthesis.c | 35 ++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/soh/src/code/audio_synthesis.c b/soh/src/code/audio_synthesis.c index d283d4cfbb..7b2db22679 100644 --- a/soh/src/code/audio_synthesis.c +++ b/soh/src/code/audio_synthesis.c @@ -851,28 +851,43 @@ Acmd* AudioSynth_ProcessNote(s32 noteIndex, NoteSubEu* noteSubEu, NoteSynthesisS s5 = samplesLenAdjusted; goto skip; case CODEC_S16: - case CODEC_OPUS: - AudioSynth_ClearBuffer(cmd++, DMEM_UNCOMPRESSED_NOTE, (samplesLenAdjusted + 16) * 2); + case CODEC_OPUS: { + if (nSamplesProcessed == 0) { + AudioSynth_ClearBuffer(cmd++, DMEM_UNCOMPRESSED_NOTE, (samplesLenAdjusted + 16) * 2); + } flags = A_CONTINUE; skipBytes = 0; - size_t bytesToRead; - nSamplesProcessed += samplesLenAdjusted; + s32 nSamplesToRead = nSamplesToProcess; - if (((synthState->samplePosInt * 2) + (samplesLenAdjusted)*2) < audioFontSample->size) { - bytesToRead = (samplesLenAdjusted)*2; - } else { - bytesToRead = audioFontSample->size - (synthState->samplePosInt * 2); + // Reading a whole block past the loop end plays back whatever the song has after + // it and only then jumps, landing the seam at a random offset rather than the one + // the sample asked for. + if (nSamplesUntilLoopEnd > 0 && nSamplesToRead > nSamplesUntilLoopEnd) { + nSamplesToRead = nSamplesUntilLoopEnd; } + + size_t bytesToRead = nSamplesToRead * 2; + size_t bytesAvailable = (synthState->samplePosInt * 2) < audioFontSample->size + ? audioFontSample->size - (synthState->samplePosInt * 2) + : 0; + if (bytesToRead > bytesAvailable) { + bytesToRead = bytesAvailable; + } + // 2S2H [Port] [Custom audio] Handle decoding OPUS data if (audioFontSample->codec == CODEC_OPUS) { - aOPUSdecImpl(sampleAddr, DMEM_UNCOMPRESSED_NOTE, bytesToRead, &synthState->opusFile, + aOPUSdecImpl(sampleAddr, DMEM_UNCOMPRESSED_NOTE + s5, bytesToRead, &synthState->opusFile, synthState->samplePosInt, audioFontSample->fileSize); } else { - aLoadBuffer(cmd++, sampleAddr + (synthState->samplePosInt * 2), DMEM_UNCOMPRESSED_NOTE, + aLoadBuffer(cmd++, sampleAddr + (synthState->samplePosInt * 2), DMEM_UNCOMPRESSED_NOTE + s5, bytesToRead); } + nSamplesProcessed += nSamplesToRead; + s5 += nSamplesToRead * 2; + goto skip; + } case CODEC_REVERB: break; }