diff --git a/libultraship b/libultraship index 8c55f607f..5c8b975de 160000 --- a/libultraship +++ b/libultraship @@ -1 +1 @@ -Subproject commit 8c55f607f2249f3ac696fc0f7277553fe3ce75a6 +Subproject commit 5c8b975de422c71fd6cda44ada5543c9067f57a2 diff --git a/src/audio/effects.c b/src/audio/effects.c index 91b3cc50e..1aa10a32c 100644 --- a/src/audio/effects.c +++ b/src/audio/effects.c @@ -542,3 +542,30 @@ s32 adsr_update(struct AdsrState *adsr) { return 0; #endif } + +/** + * Compute comb filter gain based on Y position (height relative to camera). + * Used for creating height-based audio effects in surround sound. + * + * Returns: + * -32 to -1 (0xE0-0xFF): Sound is below camera (negative Y) + * 0 to 127: Sound is above camera (positive Y) + * Bit 0 is always set (odd number) + */ +s8 audio_compute_comb_filter(f32 posY) { + s8 combFilterGain; + + if (posY < 0.0f) { + // Below camera + if (posY < -625.0f) { + combFilterGain = -32; // Very far below + } else { + combFilterGain = (s8)(((625.0f + posY) / 625.0f) * 31.0f) + 0xE0; + } + } else if (posY > 1250.0f) { + combFilterGain = 127; // Very far above + } else { + combFilterGain = (s8)((posY / 1250.0f) * 126.0f); + } + return combFilterGain | 1; +} diff --git a/src/audio/effects.h b/src/audio/effects.h index b2764a5e9..44cd42cf0 100644 --- a/src/audio/effects.h +++ b/src/audio/effects.h @@ -43,4 +43,6 @@ f32 adsr_update(struct AdsrState *adsr); s32 adsr_update(struct AdsrState *adsr); #endif +s8 audio_compute_comb_filter(f32 posY); + #endif // AUDIO_EFFECTS_H diff --git a/src/audio/external.c b/src/audio/external.c index 8a06a40db..936562fba 100644 --- a/src/audio/external.c +++ b/src/audio/external.c @@ -7,12 +7,14 @@ #include "external.h" #include "playback.h" #include "synthesis.h" +#include "effects.h" #include "game/level_update.h" #include "game/object_list_processor.h" #include "game/camera.h" #include "seq_ids.h" #include "dialog_ids.h" #include +#include #if defined(VERSION_EU) || defined(VERSION_SH) #define EU_FLOAT(x) x##f @@ -1045,6 +1047,45 @@ static f32 get_sound_pan(f32 x, f32 z) { return pan; } +/** + * Calculate surround effect index from Z position (depth relative to camera). + * In SM64's coordinate system, positive Z is behind the camera, negative Z is in front. + * Uses AUDIO_MAX_DISTANCE for scaling, matching pan and volume distance calculations. + * + * Returns: + * 0x00 - 0x3F: Sound is in front of camera (0 = far front, 0x3F = at camera) + * 0x40 - 0x7F: Sound is behind camera (0x40 = at camera, 0x7F = far behind) + * + * Called from threads: thread4_sound, thread5_game_loop (EU only) + */ +static u8 get_sound_surround_effect_index(f32 z) { + f32 absZ; + s32 surroundEffectIndex; + f32 maxZ = 1000.f; + + absZ = (z < 0 ? -z : z); + if (absZ > maxZ) { + absZ = maxZ; + } + + // In SM64, positive z means behind the camera + if (z > 0.0f) { + // Behind camera - surround effect index 0x3F (at camera) to 0x7F (far behind) + surroundEffectIndex = (s32)((absZ / maxZ) * 64.0f) + 0x3F; + if (surroundEffectIndex > 0x7F) { + surroundEffectIndex = 0x7F; + } + } else { + // In front of camera - surround effect index 0x3F (at camera) to 0x00 (far front) + surroundEffectIndex = 0x3F - (s32)((absZ / maxZ) * 63.0f); + if (surroundEffectIndex < 0) { + surroundEffectIndex = 0; + } + } + + return (u8)surroundEffectIndex; +} + /** * Called from threads: thread4_sound, thread5_game_loop (EU only) */ @@ -1225,6 +1266,16 @@ static void update_game_sound(void) { gSequencePlayers[SEQ_PLAYER_SFX].channels[channelIndex]->soundScriptIO[4] = soundId; gSequencePlayers[SEQ_PLAYER_SFX].channels[channelIndex]->soundScriptIO[0] = 1; + // Set surround effect index based on Z depth when starting sound + if (gSoundMode == SOUND_MODE_SURROUND) { + gSequencePlayers[SEQ_PLAYER_SFX].channels[channelIndex]->surroundEffectIndex = + get_sound_surround_effect_index(*sSoundBanks[bank][soundIndex].z); + // Set comb filter gain based on Y height for vertical positioning + gSequencePlayers[SEQ_PLAYER_SFX].channels[channelIndex]->combFilterGain = + audio_compute_comb_filter(*sSoundBanks[bank][soundIndex].y); + gSequencePlayers[SEQ_PLAYER_SFX].channels[channelIndex]->combFilterSize = 16; + } + switch (bank) { case SOUND_BANK_MOVING: if (!(sSoundBanks[bank][soundIndex].soundBits & SOUND_CONSTANT_FREQUENCY)) { @@ -1409,6 +1460,17 @@ static void update_game_sound(void) { // on the same line after preprocessing, and the compiler, // somehow caring about line numbers, makes it not match (it // computes function arguments in the wrong order). + + // Update surround effect index based on Z depth during playback + if (gSoundMode == SOUND_MODE_SURROUND) { + gSequencePlayers[SEQ_PLAYER_SFX].channels[channelIndex]->surroundEffectIndex = + get_sound_surround_effect_index(*sSoundBanks[bank][soundIndex].z); + // Update comb filter gain based on Y height for vertical positioning + gSequencePlayers[SEQ_PLAYER_SFX].channels[channelIndex]->combFilterGain = + audio_compute_comb_filter(*sSoundBanks[bank][soundIndex].y); + gSequencePlayers[SEQ_PLAYER_SFX].channels[channelIndex]->combFilterSize = 0x28; + } + switch (bank) { case SOUND_BANK_MOVING: if (!(sSoundBanks[bank][soundIndex].soundBits & SOUND_CONSTANT_FREQUENCY)) { diff --git a/src/audio/external.h b/src/audio/external.h index 8c7bc1eec..d6471ef14 100644 --- a/src/audio/external.h +++ b/src/audio/external.h @@ -13,6 +13,7 @@ #define SOUND_MODE_STEREO 0 #define SOUND_MODE_MONO 3 #define SOUND_MODE_HEADSET 1 +#define SOUND_MODE_SURROUND 2 #define SEQ_PLAYER_LEVEL 0 // Level background music #define SEQ_PLAYER_ENV 1 // Misc music like the puzzle jingle diff --git a/src/audio/internal.h b/src/audio/internal.h index 5725e9159..3dd4c33b5 100644 --- a/src/audio/internal.h +++ b/src/audio/internal.h @@ -446,6 +446,9 @@ struct SequenceChannel { u8 unkSH06; // some priority #endif /*0x05, 0x06*/ u8 bankId; + /* */ u8 surroundEffectIndex; // Surround depth: 0 = front, 0x7F = behind + /* */ u8 combFilterSize; // Comb filter size (delay in bytes, typically 0x28) + /* */ u16 combFilterGain; // Comb filter gain for surround height effect #if defined(VERSION_EU) || defined(VERSION_SH) /* , 0x07*/ u8 reverbIndex; /* , 0x08, 0x09*/ u8 bookOffset; @@ -567,6 +570,7 @@ struct NoteSynthesisState { /* 0x04*/ u8 reverbVol; /* 0x05*/ u8 unk5; #endif + /* */ u8 combFilterNeedsInit; // TRUE if comb filter state needs to be cleared /*0x04, 0x06*/ u16 samplePosFrac; /*0x08*/ s32 samplePosInt; /*0x0C*/ struct NoteSynthesisBuffers *synthesisBuffers; @@ -641,6 +645,10 @@ struct Note { /*0x04, 0x30, 0x30*/ u8 priority; /* 0x31, 0x31*/ u8 waveId; /* 0x32, 0x32*/ u8 sampleCountIndex; + /* */ u8 surroundEffectIndex; // Index for surround effect pan position + /* */ u8 pan; // Pan position: 0 = left, 128 = center, 255 = right + /* */ u8 combFilterSize; // Comb filter size (delay in bytes) + /* */ u16 combFilterGain; // Comb filter gain for surround height effect #ifdef VERSION_SH /* 0x33*/ u8 bankId; /* 0x34*/ u8 unkSH34; @@ -701,7 +709,11 @@ struct Note { /*0x3C*/ u16 targetVolLeft; // Q1.15, but will always be non-negative /*0x3E*/ u16 targetVolRight; // Q1.15, but will always be non-negative /*0x40*/ u8 reverbVol; // Q1.7 - /*0x41*/ u8 unused1; // never read, set to 0x3f + /*0x41*/ u8 surroundEffectIndex; // Index for surround effect pan position + /*0x42*/ u8 pan; // Pan position: 0 = left, 128 = center, 255 = right + /* */ u8 combFilterSize; // Comb filter size (delay in bytes, typically 0x28) + /* */ u8 combFilterNeedsInit; // TRUE if comb filter state needs to be cleared + /* */ u16 combFilterGain; // Comb filter gain for surround height effect /*0x44*/ struct NoteAttributes attributes; /*0x54, 0x58*/ struct AdsrState adsr; /*0x74, 0x7C*/ struct Portamento portamento; @@ -731,6 +743,7 @@ struct NoteSynthesisBuffers { s16 samples[0x40]; #endif #endif + s16 combFilterState[0x40]; // State buffer for comb filter (stores previous samples for delay) }; #ifdef VERSION_EU diff --git a/src/audio/mixer.c b/src/audio/mixer.c index 67bfa0ff7..ed4c24e15 100644 --- a/src/audio/mixer.c +++ b/src/audio/mixer.c @@ -35,7 +35,7 @@ #define BUF_U8(a) (rspa.buf.as_u8 + ((a) - 0x450)) #define BUF_S16(a) (rspa.buf.as_s16 + ((a) - 0x450) / sizeof(int16_t)) #else -#define BUF_SIZE 2512 +#define BUF_SIZE 2816 // extended to accommodate Surround mode #define BUF_U8(a) (rspa.buf.as_u8 + (a)) #define BUF_S16(a) (rspa.buf.as_s16 + (a) / sizeof(int16_t)) #endif diff --git a/src/audio/playback.c b/src/audio/playback.c index 5f7e0d097..bfa2be310 100644 --- a/src/audio/playback.c +++ b/src/audio/playback.c @@ -9,6 +9,7 @@ #include "synthesis.h" #include "effects.h" #include "external.h" +#include void note_set_resampling_rate(struct Note *note, f32 resamplingRateInput); @@ -46,6 +47,10 @@ void note_set_vel_pan_reverb(struct Note *note, f32 velocity, u8 pan, u8 reverbV pan &= unkMask; #endif + // Store pan as u8 (0=left, 128=center, 255=right) for surround effect + // EU pan is 0-127, scale to 0-255 + note->pan = pan * 2; + if (note->noteSubEu.stereoHeadsetEffects && gSoundMode == SOUND_MODE_HEADSET) { #ifdef VERSION_SH smallPanIndex = pan >> 1; @@ -114,6 +119,24 @@ void note_set_vel_pan_reverb(struct Note *note, f32 velocity, u8 pan, u8 reverbV } else if (gSoundMode == SOUND_MODE_MONO) { volLeft = 0.707f; volRight = 0.707f; + } else if (sub->stereoHeadsetEffects && gSoundMode == SOUND_MODE_SURROUND) { + // TEMPORARY: Surround mode behaves like stereo to test if glitch persists + sub->headsetPanLeft = 0; + sub->headsetPanRight = 0; + sub->usesHeadsetPanEffects = FALSE; + volLeft = gStereoPanVolume[pan]; + volRight = gStereoPanVolume[127 - pan]; + // Use same thresholds as stereo (0x20/0x60) + if (pan < 0x20) { + sub->stereoStrongLeft = TRUE; + sub->stereoStrongRight = FALSE; + } else if (pan > 0x60) { + sub->stereoStrongRight = TRUE; + sub->stereoStrongLeft = FALSE; + } else { + sub->stereoStrongLeft = FALSE; + sub->stereoStrongRight = FALSE; + } } else { volLeft = gDefaultPanVolume[pan]; volRight = gDefaultPanVolume[127 - pan]; @@ -1151,6 +1174,11 @@ void note_init_for_layer(struct Note *note, struct SequenceChannelLayer *seqLaye #endif sub->stereoHeadsetEffects = seqLayer->seqChannel->stereoHeadsetEffects; sub->reverbIndex = seqLayer->seqChannel->reverbIndex & 3; + note->surroundEffectIndex = seqLayer->seqChannel->surroundEffectIndex; + note->combFilterGain = seqLayer->seqChannel->combFilterGain; + note->combFilterSize = seqLayer->seqChannel->combFilterSize; + // EU pan is s32 0-127, scale to u8 0-254 + note->pan = (u8)(seqLayer->seqChannel->pan * 2); } #else s32 note_init_for_layer(struct Note *note, struct SequenceChannelLayer *seqLayer) { @@ -1172,6 +1200,13 @@ s32 note_init_for_layer(struct Note *note, struct SequenceChannelLayer *seqLayer build_synthetic_wave(note, seqLayer); } note_init(note); + // Copy surround index after note_init to avoid being reset by note_init_volume + note->surroundEffectIndex = seqLayer->seqChannel->surroundEffectIndex; + // Copy comb filter settings for height-based surround effect + note->combFilterGain = seqLayer->seqChannel->combFilterGain; + note->combFilterSize = seqLayer->seqChannel->combFilterSize; + // Non-EU pan is f32 0.0-1.0, scale to u8 0-255 + note->pan = (u8)(seqLayer->seqChannel->pan * 255.0f); return FALSE; } #endif @@ -1414,6 +1449,9 @@ void note_init_all(void) { note = &gNotes[i]; #if defined(VERSION_EU) || defined(VERSION_SH) note->noteSubEu = gZeroNoteSub; + note->combFilterGain = 0; + note->combFilterSize = 0; + note->synthesisState.combFilterNeedsInit = TRUE; #else note->enabled = FALSE; note->stereoStrongRight = FALSE; @@ -1437,7 +1475,10 @@ void note_init_all(void) { note->targetVolLeft = 0; note->targetVolRight = 0; note->frequency = 0.0f; - note->unused1 = 0x3f; + note->surroundEffectIndex = 0; + note->combFilterGain = 0; + note->combFilterSize = 0; + note->combFilterNeedsInit = TRUE; #endif note->attributes.velocity = 0.0f; note->adsrVolScale = 0; diff --git a/src/audio/seqplayer.c b/src/audio/seqplayer.c index 52a9772b5..643fa28d8 100644 --- a/src/audio/seqplayer.c +++ b/src/audio/seqplayer.c @@ -37,6 +37,9 @@ void sequence_channel_init(struct SequenceChannel *seqChannel) { seqChannel->stopSomething2 = FALSE; seqChannel->hasInstrument = FALSE; seqChannel->stereoHeadsetEffects = FALSE; + seqChannel->surroundEffectIndex = 0; + seqChannel->combFilterGain = 0; + seqChannel->combFilterSize = 0; seqChannel->transposition = 0; seqChannel->largeNotes = FALSE; #if defined(VERSION_EU) || defined(VERSION_SH) diff --git a/src/audio/synthesis.c b/src/audio/synthesis.c index 94f2644dd..91f6ed9d2 100644 --- a/src/audio/synthesis.c +++ b/src/audio/synthesis.c @@ -10,6 +10,7 @@ #include "external.h" #include "sm64.h" #include "mixer.h" +#include "stdio.h" #define DMEM_ADDR_TEMP 0x0 #define DMEM_ADDR_RESAMPLED 0x20 @@ -23,6 +24,7 @@ #define DMEM_ADDR_RIGHT_CH 0x600 #define DMEM_ADDR_WET_LEFT_CH 0x740 #define DMEM_ADDR_WET_RIGHT_CH 0x880 +#define DMEM_ADDR_COMB_TEMP 0x9C0 #define aSetLoadBufferPair(pkt, c, off) \ aSetBuffer(pkt, 0, c + DMEM_ADDR_WET_LEFT_CH, 0, DEFAULT_LEN_1CH - c); \ @@ -62,6 +64,7 @@ u64 *process_envelope_inner(u64 *cmd, struct Note *note, s32 nSamples, u16 inBuf s32 headsetPanSettings, struct VolumeChange *vol); u64 *note_apply_headset_pan_effects(u64 *cmd, struct Note *note, s32 bufLen, s32 flags, s32 leftRight); #endif +u64 *note_apply_surround_effect(u64 *cmd, struct Note *note, s32 bufLen); #ifdef VERSION_EU struct SynthesisReverb gSynthesisReverbs[4]; @@ -1058,6 +1061,60 @@ u64 *synthesis_process_notes(s16 *aiBuf, s32 bufLen, u64 *cmd) { noteSamplesDmemAddrBeforeResampling, flags); #endif + // Apply comb filter for surround height effect (after resampling, before envelope) + // Only applies to stereoHeadsetEffects notes in surround mode +#ifdef VERSION_EU + if (noteSubEu->stereoHeadsetEffects && (note->combFilterSize != 0) && (note->combFilterGain != 0) && gSoundMode == SOUND_MODE_SURROUND) { + s16 *combFilterState = synthesisState->synthesisBuffers->combFilterState; + u16 combFilterDmem; + // Copy mono signal to comb temp buffer + aDMEMMove(cmd++, DMEM_ADDR_TEMP, DMEM_ADDR_COMB_TEMP, bufLen * 2); + combFilterDmem = DMEM_ADDR_COMB_TEMP - note->combFilterSize; + if (synthesisState->combFilterNeedsInit) { + aClearBuffer(cmd++, combFilterDmem, note->combFilterSize); + synthesisState->combFilterNeedsInit = FALSE; + } else { + aSetBuffer(cmd++, 0, combFilterDmem, 0, note->combFilterSize); + aLoadBuffer(cmd++, VIRTUAL_TO_PHYSICAL2(combFilterState)); + } + // Save current tail samples as new state for next iteration + aSetBuffer(cmd++, 0, 0, DMEM_ADDR_TEMP + (bufLen * 2) - note->combFilterSize, note->combFilterSize); + aSaveBuffer(cmd++, VIRTUAL_TO_PHYSICAL2(combFilterState)); + // Mix delayed signal back (creates comb filter effect) + aSetBuffer(cmd++, 0, 0, 0, bufLen * 2); + aMix(cmd++, 0, note->combFilterGain, DMEM_ADDR_COMB_TEMP, combFilterDmem); + // Copy result back to temp buffer + aDMEMMove(cmd++, combFilterDmem, DMEM_ADDR_TEMP, bufLen * 2); + } else { + synthesisState->combFilterNeedsInit = TRUE; + } +#else + if (note->stereoHeadsetEffects && note->combFilterSize != 0 && note->combFilterGain != 0 && gSoundMode == SOUND_MODE_SURROUND) { + s16 *combFilterState = note->synthesisBuffers->combFilterState; + u16 combFilterDmem; + // Copy mono signal to comb temp buffer + aDMEMMove(cmd++, DMEM_ADDR_TEMP, DMEM_ADDR_COMB_TEMP, bufLen * 2); + combFilterDmem = DMEM_ADDR_COMB_TEMP - note->combFilterSize; + if (note->combFilterNeedsInit) { + aClearBuffer(cmd++, combFilterDmem, note->combFilterSize); + note->combFilterNeedsInit = FALSE; + } else { + aSetBuffer(cmd++, 0, combFilterDmem, 0, note->combFilterSize); + aLoadBuffer(cmd++, VIRTUAL_TO_PHYSICAL2(combFilterState)); + } + // Save current tail samples as new state for next iteration + aSetBuffer(cmd++, 0, 0, DMEM_ADDR_TEMP + (bufLen * 2) - note->combFilterSize, note->combFilterSize); + aSaveBuffer(cmd++, VIRTUAL_TO_PHYSICAL2(combFilterState)); + // Mix delayed signal back (creates comb filter effect) + aSetBuffer(cmd++, 0, 0, 0, bufLen * 2); + aMix(cmd++, 0, note->combFilterGain, DMEM_ADDR_COMB_TEMP, combFilterDmem); + // Copy result back to temp buffer + aDMEMMove(cmd++, combFilterDmem, DMEM_ADDR_TEMP, bufLen * 2); + } else { + note->combFilterNeedsInit = TRUE; + } +#endif + #ifdef VERSION_EU if (noteSubEu->headsetPanRight != 0 || synthesisState->prevHeadsetPanRight != 0) { leftRight = 1; @@ -1088,6 +1145,17 @@ u64 *synthesis_process_notes(s16 *aiBuf, s32 bufLen, u64 *cmd) { cmd = note_apply_headset_pan_effects(cmd, note, bufLen * 2, flags, leftRight); } #endif + + // Apply surround effect when in surround mode (only for sounds with stereo effects) +#ifdef VERSION_EU + if (noteSubEu->stereoHeadsetEffects && gSoundMode == SOUND_MODE_SURROUND) { + cmd = note_apply_surround_effect(cmd, note, bufLen * 2); + } +#else + if (note->stereoHeadsetEffects && gSoundMode == SOUND_MODE_SURROUND) { + cmd = note_apply_surround_effect(cmd, note, bufLen * 2); + } +#endif } #ifndef VERSION_EU } @@ -1424,6 +1492,69 @@ u64 *note_apply_headset_pan_effects(u64 *cmd, struct Note *note, s32 bufLen, s32 return cmd; } +/** + * Apply surround sound effect using matrix encoding based on depth position. + * Uses surroundEffectIndex (0x00-0x7F) calculated from Z position: + * 0x00-0x3F: Sound in front (0 = far front, 0x3F = at camera) - less rear effect + * 0x40-0x7F: Sound behind (0x40 = at camera, 0x7F = far behind) - more rear effect + * + * This creates a rear channel effect by phase-inverting and mixing based on pan and depth. + */ +u64 *note_apply_surround_effect(u64 *cmd, struct Note *note, s32 bufLen) { + s16 dryGain; + s32 wetGain; + f32 depthFactor; + u8 surroundIdx = note->surroundEffectIndex; + + // Calculate depth factor: how much rear channel to add + // surroundEffectIndex: 0 = front, 0x3F = at camera, 0x7F = far behind + // We want sounds behind the camera to have stronger rear channel effect + depthFactor = (f32)surroundIdx / 127.0f; + + // Convert u8 pan (0=left, 128=center, 255=right) to float (0.0-1.0) + f32 panPosition = (f32)note->pan / 255.0f; + + // Calculate base gain from current volume and depth + dryGain = note->curVolLeft > note->curVolRight ? note->curVolLeft : note->curVolRight; + dryGain = (s16)(dryGain * depthFactor); // Scale by depth + dryGain = dryGain >> 2; // Scale down for subtle effect + if (dryGain > 0x1800) { + dryGain = 0x1800; // Limit surround intensity + } + + // Skip if gain is too low + if (dryGain < 0x100) { + return cmd; + } + + // Matrix surround encoding: steer surround based on pan + // The idea: add out-of-phase content to create width/depth + // Left-panned sounds get positive left, negative right (spreads to rear left) + // Right-panned sounds get negative left, positive right (spreads to rear right) + s16 leftGain = (s16)(dryGain * (1.0f - panPosition)); + s16 rightGain = (s16)(dryGain * panPosition); + + // Mix surround contribution into channels + // Left channel gets positive surround from left-panned content + aSetBuffer(cmd++, 0, 0, 0, bufLen); + aMix(cmd++, 0, leftGain, DMEM_ADDR_LEFT_CH, DMEM_ADDR_LEFT_CH); + + // Right channel gets phase-inverted surround contribution for matrix encoding + aMix(cmd++, 0, (s16)(rightGain ^ 0xFFFF), DMEM_ADDR_RIGHT_CH, DMEM_ADDR_RIGHT_CH); + + // Apply to wet (reverb) channels for consistent spatialization + wetGain = (dryGain * note->reverbVol) >> 7; + if (wetGain > 0) { + s16 wetLeftGain = (s16)(wetGain * (1.0f - panPosition)); + s16 wetRightGain = (s16)(wetGain * panPosition); + + aMix(cmd++, 0, wetLeftGain, DMEM_ADDR_WET_LEFT_CH, DMEM_ADDR_WET_LEFT_CH); + aMix(cmd++, 0, (s16)(wetRightGain ^ 0xFFFF), DMEM_ADDR_WET_RIGHT_CH, DMEM_ADDR_WET_RIGHT_CH); + } + + return cmd; +} + #ifndef VERSION_EU // Moved to playback.c in EU @@ -1436,12 +1567,21 @@ void note_init_volume(struct Note *note) { note->curVolLeft = 1; note->curVolRight = 1; note->frequency = 0.0f; + note->surroundEffectIndex = 0; + note->pan = 128; // Center pan + note->combFilterGain = 0; + note->combFilterSize = 0; + note->combFilterNeedsInit = TRUE; } void note_set_vel_pan_reverb(struct Note *note, f32 velocity, f32 pan, u8 reverbVol) { s32 panIndex; f32 volLeft; f32 volRight; + + // Store pan as u8 (0=left, 128=center, 255=right) for surround effect + note->pan = (u8)(pan * 255.0f); + // Anding with 127 avoids out-of-bounds reads when pan is outside of [0, 1]. // This can occur during PU movement -- see the bug comment in get_sound_pan // in external.c. An out-of-bounds read by itself doesn't crash, but if the @@ -1487,6 +1627,25 @@ void note_set_vel_pan_reverb(struct Note *note, f32 velocity, f32 pan, u8 reverb } else if (gSoundMode == SOUND_MODE_MONO) { volLeft = .707f; volRight = .707f; + } else if (note->stereoHeadsetEffects && gSoundMode == SOUND_MODE_SURROUND) { + // TEMPORARY: Surround mode behaves like stereo to test if glitch persists + u8 strongLeft; + u8 strongRight; + strongLeft = FALSE; + strongRight = FALSE; + note->headsetPanLeft = 0; + note->headsetPanRight = 0; + note->usesHeadsetPanEffects = FALSE; + volLeft = gStereoPanVolume[panIndex]; + volRight = gStereoPanVolume[127 - panIndex]; + // Use same thresholds as stereo (0x20/0x60) + if (panIndex < 0x20) { + strongLeft = TRUE; + } else if (panIndex > 0x60) { + strongRight = TRUE; + } + note->stereoStrongRight = strongRight; + note->stereoStrongLeft = strongLeft; } else { volLeft = gDefaultPanVolume[panIndex]; volRight = gDefaultPanVolume[127 - panIndex]; diff --git a/src/game/sound_init.c b/src/game/sound_init.c index bc623c969..1143dadfd 100644 --- a/src/game/sound_init.c +++ b/src/game/sound_init.c @@ -32,7 +32,7 @@ static u16 sCurrentShellMusic = MUSIC_NONE; static u16 sCurrentCapMusic = MUSIC_NONE; static u8 sPlayingInfiniteStairs = FALSE; UNUSED static u8 unused8032C6D8[16] = { 0 }; -static s16 sSoundMenuModeToSoundMode[] = { SOUND_MODE_STEREO, SOUND_MODE_MONO, SOUND_MODE_HEADSET }; +static s16 sSoundMenuModeToSoundMode[] = { SOUND_MODE_STEREO, SOUND_MODE_MONO, SOUND_MODE_HEADSET, SOUND_MODE_SURROUND }; // Only the 20th array element is used. static u32 sMenuSoundsExtra[] = { SOUND_MOVING_TERRAIN_SLIDE + (0 << 16), @@ -139,8 +139,14 @@ void enable_background_sound(void) { * Called from threads: thread5_game_loop */ void set_sound_mode(u16 soundMode) { - if (soundMode < 3) { + if (soundMode < 4) { audio_set_sound_mode(sSoundMenuModeToSoundMode[soundMode]); + // Set audio channels: surround mode uses 5.1, others use stereo + if (soundMode == SOUND_MENU_MODE_SURROUND) { + SetAudioChannels(audioMatrix51); + } else { + SetAudioChannels(audioStereo); + } } } diff --git a/src/game/sound_init.h b/src/game/sound_init.h index 67fa372ca..292ce0f88 100644 --- a/src/game/sound_init.h +++ b/src/game/sound_init.h @@ -17,6 +17,7 @@ #define SOUND_MENU_MODE_STEREO 0 #define SOUND_MENU_MODE_MONO 1 #define SOUND_MENU_MODE_HEADSET 2 +#define SOUND_MENU_MODE_SURROUND 3 void reset_volume(void); void raise_background_noise(s32 a); diff --git a/src/menu/file_select.c b/src/menu/file_select.c index f0fed8eeb..d7beeefa5 100644 --- a/src/menu/file_select.c +++ b/src/menu/file_select.c @@ -154,6 +154,8 @@ static s8 sOpenLangSettings = FALSE; //static unsigned char textNew[] = { TEXT_NEW }; static unsigned char starIcon[] = { GLYPH_STAR, GLYPH_SPACE }; static unsigned char xIcon[] = { GLYPH_MULTIPLY, GLYPH_SPACE }; +// "SURROUND" text for sound mode menu (S=0x1C, U=0x1E, R=0x1B, O=0x18, N=0x17, D=0x0D) +static unsigned char textSurround[] = { 0x1C, 0x1E, 0x1B, 0x1B, 0x18, 0x1E, 0x17, 0x0D, 0xFF }; //#endif // //#ifndef VERSION_EU @@ -1036,16 +1038,20 @@ void check_erase_menu_clicked_buttons(struct Object *eraseButton) { void render_sound_mode_menu_buttons(struct Object *soundModeButton) { // Stereo option button sMainMenuButtons[MENU_BUTTON_STEREO] = spawn_object_rel_with_rot( - soundModeButton, MODEL_MAIN_MENU_GENERIC_BUTTON, bhvMenuButton, 533, SOUND_BUTTON_Y, -100, 0, -0x8000, 0); + soundModeButton, MODEL_MAIN_MENU_GENERIC_BUTTON, bhvMenuButton, 600, SOUND_BUTTON_Y, -100, 0, -0x8000, 0); sMainMenuButtons[MENU_BUTTON_STEREO]->oMenuButtonScale = 0.11111111f; // Mono option button sMainMenuButtons[MENU_BUTTON_MONO] = spawn_object_rel_with_rot( - soundModeButton, MODEL_MAIN_MENU_GENERIC_BUTTON, bhvMenuButton, 0, SOUND_BUTTON_Y, -100, 0, -0x8000, 0); + soundModeButton, MODEL_MAIN_MENU_GENERIC_BUTTON, bhvMenuButton, 200, SOUND_BUTTON_Y, -100, 0, -0x8000, 0); sMainMenuButtons[MENU_BUTTON_MONO]->oMenuButtonScale = 0.11111111f; // Headset option button sMainMenuButtons[MENU_BUTTON_HEADSET] = spawn_object_rel_with_rot( - soundModeButton, MODEL_MAIN_MENU_GENERIC_BUTTON, bhvMenuButton, -533, SOUND_BUTTON_Y, -100, 0, -0x8000, 0); + soundModeButton, MODEL_MAIN_MENU_GENERIC_BUTTON, bhvMenuButton, -200, SOUND_BUTTON_Y, -100, 0, -0x8000, 0); sMainMenuButtons[MENU_BUTTON_HEADSET]->oMenuButtonScale = 0.11111111f; + // Surround option button + sMainMenuButtons[MENU_BUTTON_SURROUND] = spawn_object_rel_with_rot( + soundModeButton, MODEL_MAIN_MENU_GENERIC_BUTTON, bhvMenuButton, -600, SOUND_BUTTON_Y, -100, 0, -0x8000, 0); + sMainMenuButtons[MENU_BUTTON_SURROUND]->oMenuButtonScale = 0.11111111f; #ifdef VERSION_EU // English option button @@ -1088,7 +1094,7 @@ void check_sound_mode_menu_clicked_buttons(struct Object *soundModeButton) { // If sound mode button clicked, select it and define sound mode // The check will always be true because of the group configured above (In JP & US) if (buttonID == MENU_BUTTON_STEREO || buttonID == MENU_BUTTON_MONO - || buttonID == MENU_BUTTON_HEADSET) { + || buttonID == MENU_BUTTON_HEADSET || buttonID == MENU_BUTTON_SURROUND) { if (soundModeButton->oMenuButtonActionPhase == SOUND_MODE_PHASE_MAIN) { play_sound(SOUND_MENU_CLICK_FILE_SELECT, gGlobalSoundSource); #if ENABLE_RUMBLE @@ -1593,6 +1599,9 @@ void bhv_menu_button_manager_loop(void) { case MENU_BUTTON_HEADSET: return_to_main_menu(MENU_BUTTON_SOUND_MODE, sMainMenuButtons[MENU_BUTTON_HEADSET]); break; + case MENU_BUTTON_SURROUND: + return_to_main_menu(MENU_BUTTON_SOUND_MODE, sMainMenuButtons[MENU_BUTTON_SURROUND]); + break; #endif } @@ -1818,7 +1827,8 @@ void print_main_menu_strings(void) { unsigned char* textSoundModes[] = { GameEngine_LoadTranslation("TEXT_STEREO"), GameEngine_LoadTranslation("TEXT_MONO"), - GameEngine_LoadTranslation("TEXT_HEADSET") + GameEngine_LoadTranslation("TEXT_HEADSET"), + textSurround }; sSoundTextX = get_str_x_pos_from_center(254, textSoundModes[sSoundMode], 10.0f); print_generic_string(SOUNDMODE_X1, 39, textSoundModes[sSoundMode]); @@ -2463,17 +2473,18 @@ void print_sound_mode_menu_strings(void) { unsigned char* textSoundModes[] = { GameEngine_LoadTranslation("TEXT_STEREO"), GameEngine_LoadTranslation("TEXT_MONO"), - GameEngine_LoadTranslation("TEXT_HEADSET") + GameEngine_LoadTranslation("TEXT_HEADSET"), + textSurround }; // Print sound mode names - for (mode = 0; mode < 3; mode++) { + for (mode = 0; mode < 4; mode++) { if (mode == sSoundMode) { gDPSetEnvColor(gDisplayListHead++, 255, 255, 255, sTextBaseAlpha); } else { gDPSetEnvColor(gDisplayListHead++, 0, 0, 0, sTextBaseAlpha); } - // Mode names are centered correctly on US and Shindou - textX = get_str_x_pos_from_center(mode * 74 + 87, textSoundModes[mode], 10.0f); + // Mode names centered for 4 sound options (mapped to button positions 600, 200, -200, -600) + textX = get_str_x_pos_from_center(mode * 55 + 77, textSoundModes[mode], 10.0f); print_generic_string(textX, 87, textSoundModes[mode]); } #endif diff --git a/src/menu/file_select.h b/src/menu/file_select.h index 2521efabe..41f97a0a4 100644 --- a/src/menu/file_select.h +++ b/src/menu/file_select.h @@ -77,6 +77,7 @@ enum MenuButtonTypes { MENU_BUTTON_STEREO = MENU_BUTTON_OPTION_MIN, MENU_BUTTON_MONO, MENU_BUTTON_HEADSET, + MENU_BUTTON_SURROUND, #ifdef VERSION_EU // Language Menu