Encode the menu theme as AAC
8.3 MB of WAV down to 768 KB, on a file that downloads on every page load before the player has decided to play anything. loopBed takes the full filename now, because the beds are a mix of .wav and .m4a — fadeBed's key has to match it exactly, extension included, or a duck silently no-ops. It also takes an explicit loopEnd, and the theme passes 48. AAC pads its last packet, so Chrome decodes this 48.000s master to a 48.0174s buffer: 17ms of silence spliced in at every seam, once a loop, on the title screen. Trimming the seam is what keeps the loop seamless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c567ff68a6
commit
cf3498c049
5 files changed
+33
-11
No files matched your search
@@ -77,7 +77,7 @@ 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) {
|
||||
function loopBed(file, lowcut, vol, loopEnd = 0) {
|
||||
// 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
|
||||
@@ -86,11 +86,16 @@ function loopBed(file, lowcut, vol) {
|
||||
const ac = audio();
|
||||
if (beds.has(file)) return;
|
||||
beds.set(file, null);
|
||||
fetch('assets/audio/' + file + '.wav')
|
||||
fetch('assets/audio/' + file) // full filename: the beds are a mix of .wav and .m4a, see below
|
||||
.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;
|
||||
// loopEnd = the source's TRUE length, for a bed that is compressed. AAC pads the last packet,
|
||||
// and Chrome hands back a buffer ~17 ms longer than the master — silence spliced in at every
|
||||
// seam, once a loop, on the title screen. Trimming the seam is what keeps the loop seamless.
|
||||
// Zero (the default, and every uncompressed bed) leaves the whole buffer in play.
|
||||
if (loopEnd) src.loopEnd = loopEnd;
|
||||
const hp = ac.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = lowcut;
|
||||
// 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
|
||||
@@ -101,13 +106,18 @@ function loopBed(file, lowcut, vol) {
|
||||
})
|
||||
.catch(() => beds.delete(file)); // failed start (decode/network) can retry next call
|
||||
}
|
||||
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
|
||||
export const ambient = () => loopBed('tennis-ambient-loop.wav', 500, 0.035); // whole-scene atmosphere, from first tap
|
||||
export const zombieMusic = () => loopBed('zombie-mode.wav', 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.5);
|
||||
// AAC, not WAV: 48 s of stereo music is 8.3 MB as a WAV and this one downloads on every page load,
|
||||
// before the player has decided to play anything. 768 KB encoded, and the master is exactly 48.000 s
|
||||
// — which is the loopEnd, because the decoded buffer is NOT (measured 48.0174 s in Chrome; see the
|
||||
// padding note in loopBed). Re-encode the file and you must re-measure that number:
|
||||
// fetch(f).then(r=>r.arrayBuffer()).then(a=>new AudioContext().decodeAudioData(a)).then(b=>b.duration)
|
||||
export const menuMusic = () => loopBed('background-music.m4a', 30, 0.5, 48);
|
||||
|
||||
// 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) {
|
||||
@@ -119,10 +129,10 @@ function fadeBed(file, out, secs = 0.7) {
|
||||
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);
|
||||
export const zombieDuck = out => fadeBed('zombie-mode.wav', out); // key must match loopBed's, filename and all
|
||||
// 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);
|
||||
export const menuDuck = out => fadeBed('background-music.m4a', 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