Give the menus a theme
background-music.wav loops under the title screen and the court picker, ducks out when a court is picked, and fades back in at the death summary — the end screen is a menu too, so the zombie bed ducks out as this one comes in. It starts at BOOT, not on the first tap, so the title screen already has music. That needed loopBed to call audio() before its already-started early return: the theme plays into a context the autoplay policy holds suspended, and the call that wakes it is a second menuMusic() from the first tap, which used to return at the beds.has guard without reaching AC.resume(). fadeBed also now records a duck for a bed that is still loading, and loopBed starts that bed silent. An 8 MB file ducked mid-fetch would otherwise come up at full level, seconds later, over the match. Same latent bug applied to zombieDuck. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fa30fb94c4
commit
60a74f6a99
6 files changed
+51
-8
No files matched your search
@@ -76,17 +76,26 @@ export function aww(vol = 1) { sfx('tennis-aww', vol * 0.5, { reverb: true }); }
|
||||
|
||||
// Looping background beds (scene atmosphere, mode music): each starts once, highpass 'lowcut' + quiet gain.
|
||||
const beds = new Map(); // file -> {ac, g, vol} once playing (null while loading); see fadeBed
|
||||
const ducked = new Set(); // beds asked to duck while they were STILL LOADING — see fadeBed
|
||||
function loopBed(file, lowcut, vol) {
|
||||
// audio() FIRST, before the already-started early return: it is also what RESUMES a context the
|
||||
// browser suspended under its autoplay policy. The menu theme is started at boot, before any
|
||||
// gesture exists, so it plays into a suspended context and stays silent — and the call that has
|
||||
// to wake it up is a second menuMusic() from the first tap, which would otherwise return here
|
||||
// without ever reaching audio().
|
||||
const ac = audio();
|
||||
if (beds.has(file)) return;
|
||||
beds.set(file, null);
|
||||
const ac = audio();
|
||||
fetch('assets/audio/' + file + '.wav')
|
||||
.then(r => { if (!r.ok) throw 0; return r.arrayBuffer(); })
|
||||
.then(ab => ac.decodeAudioData(ab))
|
||||
.then(buf => {
|
||||
const src = ac.createBufferSource(); src.buffer = buf; src.loop = true;
|
||||
const hp = ac.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = lowcut;
|
||||
const g = ac.createGain(); g.gain.value = vol;
|
||||
// start SILENT if it was ducked while loading: a multi-megabyte file takes seconds to fetch
|
||||
// and decode, and a player who taps straight through the picker would otherwise have the
|
||||
// menu theme come up over the match, several seconds late, at full level.
|
||||
const g = ac.createGain(); g.gain.value = ducked.has(file) ? 0 : vol;
|
||||
src.connect(hp).connect(g).connect(ac.destination); src.start();
|
||||
beds.set(file, { ac, g, vol });
|
||||
})
|
||||
@@ -94,17 +103,26 @@ function loopBed(file, lowcut, vol) {
|
||||
}
|
||||
export const ambient = () => loopBed('tennis-ambient-loop', 500, 0.035); // whole-scene atmosphere, from first tap
|
||||
export const zombieMusic = () => loopBed('zombie-mode', 800, 0.2); // background music once splatter kicks in
|
||||
// Menu theme: the splash, the court picker, the death summary. Barely highpassed — this one is
|
||||
// MUSIC, not an atmosphere bed, so the low end is the point. Started at boot so it is already
|
||||
// running by the time the title is on screen; on a phone the autoplay policy holds the context
|
||||
// suspended until the first tap, and it becomes audible then (see loopBed).
|
||||
export const menuMusic = () => loopBed('background-music', 30, 0.3);
|
||||
|
||||
// duck a bed to silence (out=true) or back to its base level over `secs` — the sudden-death duel mutes the music
|
||||
function fadeBed(file, out, secs = 0.7) {
|
||||
if (out) ducked.add(file); else ducked.delete(file);
|
||||
const b = beds.get(file);
|
||||
if (!b) return;
|
||||
if (!b) return; // still loading — the flag above is the level it will start at
|
||||
const t = b.ac.currentTime;
|
||||
b.g.gain.cancelScheduledValues(t);
|
||||
b.g.gain.setValueAtTime(b.g.gain.value, t); // anchor at current level so a mid-fade toggle stays smooth
|
||||
b.g.gain.linearRampToValueAtTime(out ? 0 : b.vol, t + secs);
|
||||
}
|
||||
export const zombieDuck = out => fadeBed('zombie-mode', out);
|
||||
// The menu theme is never stopped, only faded — the buffer is already decoded, so fading it back up
|
||||
// on the death screen costs nothing and can't miss its cue waiting on a re-fetch.
|
||||
export const menuDuck = out => fadeBed('background-music', out);
|
||||
|
||||
let sysVoice = null;
|
||||
export function say(text, delay = 0) { // fallback when a recorded umpire line is missing
|
||||
|
||||
Reference in new issue
Block a user