From fd9f4d3a7463e1612f9ad478ed15a2fd5cb99a18 Mon Sep 17 00:00:00 2001 From: David Racine Date: Sun, 30 Aug 2026 23:15:29 -0400 Subject: [PATCH] Reopen the OPUS decoder when a note moves to another sample The decoder cached on a note was keyed on nothing, so a note reused for a different streamed sample carried on decoding the previous track. Audible as the wrong custom music: the sequence and soundfont the audio editor reports are correct, only the samples reaching the mixer are not. Reproduced on the game-start cutscene chain with a streamed music pack in 4 of 6 runs; 0 of 6 after. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011Ex3z29fQzPEZgwH4EA641 --- soh/include/z64audio.h | 1 + soh/soh/mixer.h | 1 + soh/src/code/audio_playback.c | 3 ++- soh/src/code/audio_synthesis.c | 7 +++++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/soh/include/z64audio.h b/soh/include/z64audio.h index 6fed61399d6..026dffd684d 100644 --- a/soh/include/z64audio.h +++ b/soh/include/z64audio.h @@ -489,6 +489,7 @@ typedef struct { /* 0x1C */ u16 unk_1C; /* 0x1E */ u16 unk_1E; struct OggOpusFile* opusFile; // Only for streamed opus audio + uintptr_t opusSampleAddr; // sample opusFile was opened for } NoteSynthesisState; // size = 0x20 typedef struct { diff --git a/soh/soh/mixer.h b/soh/soh/mixer.h index 048135e4485..c4626cc2330 100644 --- a/soh/soh/mixer.h +++ b/soh/soh/mixer.h @@ -61,6 +61,7 @@ struct OggOpusFile; void aOPUSdecImpl(void* source_addr, uint16_t dest_addr, uint16_t nbytes, struct OggOpusFile** decState, int32_t pos, uint32_t size); +void aOPUSFree(struct OggOpusFile* opusFile); #define aSegment(pkt, s, b) \ do { \ diff --git a/soh/src/code/audio_playback.c b/soh/src/code/audio_playback.c index ce6c5d6f19b..e32a56306b0 100644 --- a/soh/src/code/audio_playback.c +++ b/soh/src/code/audio_playback.c @@ -1,5 +1,6 @@ #include "global.h" #include "soh/ResourceManagerHelpers.h" +#include "soh/mixer.h" extern bool gUseLegacySD; @@ -150,7 +151,6 @@ void Audio_NoteInit(Note* note) { note->noteSubEu = gDefaultNoteSub; } -extern void aOPUSFree(struct OggOpusFile* opusFile); void Audio_NoteDisable(Note* note) { if (note->noteSubEu.bitField0.needsInit == true) { note->noteSubEu.bitField0.needsInit = false; @@ -166,6 +166,7 @@ void Audio_NoteDisable(Note* note) { if (note->synthesisState.opusFile != NULL) { aOPUSFree(note->synthesisState.opusFile); note->synthesisState.opusFile = NULL; + note->synthesisState.opusSampleAddr = 0; } } diff --git a/soh/src/code/audio_synthesis.c b/soh/src/code/audio_synthesis.c index 7b2db226799..c8cd068fc76 100644 --- a/soh/src/code/audio_synthesis.c +++ b/soh/src/code/audio_synthesis.c @@ -876,6 +876,13 @@ Acmd* AudioSynth_ProcessNote(s32 noteIndex, NoteSubEu* noteSubEu, NoteSynthesisS // 2S2H [Port] [Custom audio] Handle decoding OPUS data if (audioFontSample->codec == CODEC_OPUS) { + // A note reused for another streamed sample keeps the decoder its predecessor opened, + // so the previous track carries on playing under the new note. + if (synthState->opusFile != NULL && synthState->opusSampleAddr != sampleAddr) { + aOPUSFree(synthState->opusFile); + synthState->opusFile = NULL; + } + synthState->opusSampleAddr = sampleAddr; aOPUSdecImpl(sampleAddr, DMEM_UNCOMPRESSED_NOTE + s5, bytesToRead, &synthState->opusFile, synthState->samplePosInt, audioFontSample->fileSize); } else {