feat: couple terrain surge and sun bloom to the soundtrack's low end

This commit is contained in:
2026-10-10 17:46:17 +03:00
parent 416b930523
commit 0821b542b5
5 changed files with 93 additions and 4 deletions

41
src/audio/music-pulse.ts Normal file
View File

@@ -0,0 +1,41 @@
/**
* A single low-band level shared between the soundtrack and the scene.
* The scene samples it once inside its existing render loop; nothing here
* schedules work of its own. When no soundtrack is audible it reads zero.
*/
let analyser: AnalyserNode | null = null;
let bins: Uint8Array<ArrayBuffer> | null = null;
let audible = false;
export function attachMusicPulse(node: AnalyserNode | null): void {
analyser = node;
bins = node === null ? null : new Uint8Array(new ArrayBuffer(node.frequencyBinCount));
}
export function setMusicPulseAudible(value: boolean): void {
audible = value;
}
/** Bass energy in [0, 1]: the mean of roughly 90-380 Hz. */
export function readMusicPulse(): number {
if (!audible || analyser === null || bins === null) return 0;
analyser.getByteFrequencyData(bins);
const sum = (bins[1] ?? 0) + (bins[2] ?? 0) + (bins[3] ?? 0) + (bins[4] ?? 0);
return sum / (4 * 255);
}
/** Insert a pass-through tap in front of `destination` and analyse it. */
export function createPulseTap(context: BaseAudioContext, destination: AudioNode): AudioNode {
const tap = context.createGain();
tap.connect(destination);
try {
const node = context.createAnalyser();
node.fftSize = 512;
node.smoothingTimeConstant = 0.55;
tap.connect(node);
attachMusicPulse(node);
} catch {
attachMusicPulse(null);
}
return tap;
}

View File

@@ -1,3 +1,4 @@
import { attachMusicPulse, createPulseTap } from './music-pulse';
import {
AFTERGLOW_LOOP_END_SECONDS,
AFTERGLOW_LOOP_START_SECONDS,
@@ -596,12 +597,17 @@ export function createBrowserSoundtrackEngine(
sampleRate: SOUNDTRACK_SAMPLE_RATE,
}),
loadBuffer: loadAudioBuffer,
createMixer: createAdaptiveMixer,
// The mixer feeds a pass-through tap so the scene can read the bass level.
createMixer: (context, destination) => createAdaptiveMixer(context, createPulseTap(context, destination)),
createDriveTexture,
});
let resumeCount = 0;
return {
...graph,
async close() {
attachMusicPulse(null);
await graph.close();
},
async resume() {
resumeCount += 1;
if (isTestFault('F11-audio-resume-rejection') && resumeCount > 1) {