From 1a2d8f207187de395b972966daa3a5755ce1effe Mon Sep 17 00:00:00 2001 From: David Racine Date: Thu, 27 Aug 2026 16:38:58 -0400 Subject: [PATCH] Keep streamed music alive across the enemy bgm crossfade Fixes #5706. Audio_SplitBgmChannels stops the channels of the quieter bgm player so it gives its notes back to the pool. A sequence loses nothing, since its script issues fresh notes every tick, but streamed music is a single note that lasts the whole song and then waits on a long delay: stopping its channel ends the track for good, and neither the mask clearing nor the volume returning brings it back. Both players are stopped together while volSplit is between 40 and 87, and the sub player is also stopped at the instant enemy mode engages, before the queued sequence has reached it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011Ex3z29fQzPEZgwH4EA641 --- soh/include/z64audio.h | 3 +++ soh/src/code/audio_load.c | 5 +++++ soh/src/code/code_800EC960.c | 18 ++++++++++++------ soh/src/code/code_800F9280.c | 5 +++++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/soh/include/z64audio.h b/soh/include/z64audio.h index 6fed61399d6..74781a6d439 100644 --- a/soh/include/z64audio.h +++ b/soh/include/z64audio.h @@ -26,6 +26,9 @@ extern "C" { //#define MAX_SEQUENCES 0x800 extern size_t sequenceMapSize; +// Streamed audio rather than a sequence script, per sequence and per sequence player. +extern u8* gSeqIsStreamed; +extern u8 gSeqPlayerIsStreamed[4]; extern size_t fontMapSize; extern char** fontMap; diff --git a/soh/src/code/audio_load.c b/soh/src/code/audio_load.c index 239e312f32a..e54a3c9aa67 100644 --- a/soh/src/code/audio_load.c +++ b/soh/src/code/audio_load.c @@ -85,6 +85,7 @@ void* sUnusedHandler = NULL; s32 gAudioContextInitalized = false; char** sequenceMap; +u8* gSeqIsStreamed; size_t sequenceMapSize; // A map of authentic sequence IDs to their cache policies, for use with sequence swapping. u8 seqCachePolicyMap[MAX_AUTHENTIC_SEQID]; @@ -1344,6 +1345,7 @@ void AudioLoad_Init(void* heap, size_t heapSize) { sequenceMapSize = (size_t)(seqListSize + customSeqListSize); // calloc: unassigned slots stay NULL for the guard in AudioLoad_SyncInitSeqPlayerInternal(). sequenceMap = calloc(sequenceMapSize + 0xF, sizeof(char*)); + gSeqIsStreamed = calloc(sequenceMapSize + 0xF, sizeof(u8)); // SOH [Bugfix] Size to match sequenceMap (+ 0xF); custom ids can exceed sequenceMapSize. gAudioContext.seqLoadStatus = malloc(sequenceMapSize + 0xF); @@ -1405,6 +1407,8 @@ void AudioLoad_Init(void* heap, size_t heapSize) { // ensure that what would be the next sequence number is actually unassigned in AudioCollection int j = i - startingSeqNum; SequenceData* sDat = ResourceMgr_LoadSeqPtrByName(customSeqList[j]); + // Read before the branch below rewrites numFonts. + u8 isStreamed = (sDat->numFonts == -1); if (sDat->numFonts == -1) { uint64_t crc; @@ -1449,6 +1453,7 @@ void AudioLoad_Init(void* heap, size_t heapSize) { sDat->seqNumber = seqNum; LUSLOG_DEBUG("Registered custom sequence \"%s\" as seqNum %d", customSeqList[j], seqNum); sequenceMap[sDat->seqNumber] = strdup(customSeqList[j]); + gSeqIsStreamed[sDat->seqNumber] = isStreamed; seqNum++; } diff --git a/soh/src/code/code_800EC960.c b/soh/src/code/code_800EC960.c index 3fea838e192..314d7c8bac7 100644 --- a/soh/src/code/code_800EC960.c +++ b/soh/src/code/code_800EC960.c @@ -4760,12 +4760,18 @@ void Audio_SplitBgmChannels(s8 volSplit) { } channelBits = 0; - for (channelIdx = 0; channelIdx < 16; channelIdx++) { - if (notePriority > gAudioContext.seqPlayers[bgmPlayers[i]].channels[channelIdx]->notePriority) { - // If the note currently playing in the channel is a high enough priority, - // then keep the channel on by setting a channelBit - // If this condition fails, then the channel will be shut off - channelBits += (1 << channelIdx); + + // Streamed music is one note for the whole song, so stopping a channel ends it + // for good. An empty player is about to be handed one: the enemy sequence is + // queued a step before this runs. + if (gAudioContext.seqPlayers[bgmPlayers[i]].enabled && !gSeqPlayerIsStreamed[bgmPlayers[i]]) { + for (channelIdx = 0; channelIdx < 16; channelIdx++) { + if (notePriority > gAudioContext.seqPlayers[bgmPlayers[i]].channels[channelIdx]->notePriority) { + // If the note currently playing in the channel is a high enough priority, + // then keep the channel on by setting a channelBit + // If this condition fails, then the channel will be shut off + channelBits += (1 << channelIdx); + } } } diff --git a/soh/src/code/code_800F9280.c b/soh/src/code/code_800F9280.c index 5cf4622992a..b9997751915 100644 --- a/soh/src/code/code_800F9280.c +++ b/soh/src/code/code_800F9280.c @@ -15,6 +15,8 @@ Struct_8016E320 D_8016E320[4][5]; u8 sNumSeqRequests[4]; u32 sAudioSeqCmds[0x100]; ActiveSequence gActiveSeqs[4]; +// Filled in here because this is where the audio editor mapping is resolved. +u8 gSeqPlayerIsStreamed[4]; u8 sSeqCmdWrPos = 0; u8 sSeqCmdRdPos = 0; @@ -64,6 +66,8 @@ void Audio_StartSequence(u8 playerIdx, u8 seqId, u8 arg2, u16 fadeTimer) { gActiveSeqs[playerIdx].seqId = seqId | (arg2 << 8); gActiveSeqs[playerIdx].prevSeqId = seqId | (arg2 << 8); + gSeqPlayerIsStreamed[playerIdx] = (gSeqIsStreamed != NULL) && ((size_t)resolvedSeqId < sequenceMapSize + 0xF) && + (gSeqIsStreamed[resolvedSeqId] != 0); if (gActiveSeqs[playerIdx].volCur != 1.0f) { Audio_QueueCmdF32(0x41000000 | _SHIFTL(playerIdx, 16, 8), gActiveSeqs[playerIdx].volCur); @@ -89,6 +93,7 @@ void func_800F9474(u8 playerIdx, u16 arg1) { Audio_QueueCmdS32(0x83000000 | ((u8)playerIdx << 16), (arg1 * (u16)gAudioContext.audioBufferParameters.updatesPerFrame) / 4); gActiveSeqs[playerIdx].seqId = NA_BGM_DISABLED; + gSeqPlayerIsStreamed[playerIdx] = false; } typedef enum {