Lane C S13: sky.setMute(on) — the tap A's M key was waiting on
One flag plus a gain write, but the flag is the point: a mute pressed on the splash screen, before the first gesture builds the audio graph, is remembered and honoured by unlock(). main.js already feature-detects sky.setMute, so the M key lights itself up on this landing. audio.masterGain getter added so the assert reads the real bus, not the flag back. Selftest 336/0/0 (was 335, +1 test). Mutation-checked both ways: a no-op setMute and an unlock that forgets a pre-unlock mute both go red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a495fcdba6
commit
8a3dc323bc
@ -342,8 +342,11 @@ function cloudTexture(size = 256, seed = 7) {
|
|||||||
// ---------------------------------------------------------------- audio
|
// ---------------------------------------------------------------- audio
|
||||||
// Synthesized layers. WebAudio won't start until a gesture (browser rule), so
|
// Synthesized layers. WebAudio won't start until a gesture (browser rule), so
|
||||||
// everything is built lazily on unlock() and silently absent before it.
|
// everything is built lazily on unlock() and silently absent before it.
|
||||||
|
const MASTER_GAIN = 0.55;
|
||||||
|
|
||||||
function createAudio(seed = 1) {
|
function createAudio(seed = 1) {
|
||||||
let ctx = null, master = null;
|
let ctx = null, master = null;
|
||||||
|
let muted = false;
|
||||||
let windGain, windFilter, windHowl, howlGain;
|
let windGain, windFilter, windHowl, howlGain;
|
||||||
let rainGain, rainFilter;
|
let rainGain, rainFilter;
|
||||||
let gustGain, gustFilter;
|
let gustGain, gustFilter;
|
||||||
@ -380,6 +383,15 @@ function createAudio(seed = 1) {
|
|||||||
/** 'running' | 'suspended' | 'closed' | 'none'. `ready` only means the graph
|
/** 'running' | 'suspended' | 'closed' | 'none'. `ready` only means the graph
|
||||||
* got built — a suspended context is still silent, so the HUD reports this. */
|
* got built — a suspended context is still silent, so the HUD reports this. */
|
||||||
get state() { return ctx ? ctx.state : 'none'; },
|
get state() { return ctx ? ctx.state : 'none'; },
|
||||||
|
/** A's M key (gate 3.3). A flag, not just a gain write, so muting works
|
||||||
|
* before unlock() has built the graph and survives it. */
|
||||||
|
setMute(on) {
|
||||||
|
muted = !!on;
|
||||||
|
if (master) master.gain.value = muted ? 0 : MASTER_GAIN;
|
||||||
|
},
|
||||||
|
get muted() { return muted; },
|
||||||
|
/** Real bus gain, for asserts — null until unlock() builds the graph. */
|
||||||
|
get masterGain() { return master ? master.gain.value : null; },
|
||||||
/** Current layer gains — for the HUD and for asserting the bed tracks wind. */
|
/** Current layer gains — for the HUD and for asserting the bed tracks wind. */
|
||||||
levels() {
|
levels() {
|
||||||
if (!started) return null;
|
if (!started) return null;
|
||||||
@ -399,7 +411,9 @@ function createAudio(seed = 1) {
|
|||||||
ctx = new AC();
|
ctx = new AC();
|
||||||
if (ctx.state === 'suspended') ctx.resume();
|
if (ctx.state === 'suspended') ctx.resume();
|
||||||
master = ctx.createGain();
|
master = ctx.createGain();
|
||||||
master.gain.value = 0.55;
|
// honour a mute that landed BEFORE the first gesture built the graph —
|
||||||
|
// M on the splash screen must not be forgotten by the unlock
|
||||||
|
master.gain.value = muted ? 0 : MASTER_GAIN;
|
||||||
master.connect(ctx.destination);
|
master.connect(ctx.destination);
|
||||||
noiseBuf = noiseBuffer(ctx);
|
noiseBuf = noiseBuffer(ctx);
|
||||||
|
|
||||||
@ -792,6 +806,16 @@ export function createSkyFx(o = {}) {
|
|||||||
/** Wire to the first click/keydown — browsers won't start audio otherwise. */
|
/** Wire to the first click/keydown — browsers won't start audio otherwise. */
|
||||||
unlockAudio() { audio.unlock(); },
|
unlockAudio() { audio.unlock(); },
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A's M key (gate 3.3): mute the whole audio bus. main.js feature-detects
|
||||||
|
* this (`typeof sky.setMute === 'function'`) and only advertises M once it
|
||||||
|
* exists — so this method appearing is what lights the key up. Safe at any
|
||||||
|
* time: before unlock it sets a flag the unlock honours, after unlock it
|
||||||
|
* writes the master gain directly.
|
||||||
|
*/
|
||||||
|
setMute(on) { audio.setMute(on); },
|
||||||
|
get muted() { return audio.muted; },
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} dt
|
* @param {number} dt
|
||||||
* @param {number} t storm time
|
* @param {number} t storm time
|
||||||
|
|||||||
@ -172,6 +172,27 @@ export default async function run(t) {
|
|||||||
`skyfx left ${scene.children.length - before.children} object(s) in the scene`);
|
`skyfx left ${scene.children.length - before.children} object(s) in the scene`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// SPRINT13 gate 3.3 — A's M key. main.js feature-detects sky.setMute and only
|
||||||
|
// advertises M once it exists, so this contract is what lights the key up.
|
||||||
|
// The pre-unlock case is the one that bites: M pressed on the splash screen,
|
||||||
|
// BEFORE the first gesture builds the audio graph, must survive the unlock.
|
||||||
|
t.test('M-mute: setMute silences the bus, and a pre-unlock mute survives unlock', () => {
|
||||||
|
const wind = createWind(storms.storm_02_wildnight);
|
||||||
|
const sky = createSkyFx({ wind }); // headless — the bus needs no scene
|
||||||
|
assert(typeof sky.setMute === 'function', "no setMute — A's M key has nothing to call");
|
||||||
|
assert(sky.muted === false, 'a fresh sky must not start muted');
|
||||||
|
sky.setMute(true); // before unlock — the splash case
|
||||||
|
assert(sky.muted === true, 'setMute(true) did not take');
|
||||||
|
sky.unlockAudio();
|
||||||
|
if (sky.audio.ready) { // no AudioContext = nothing to silence
|
||||||
|
assert(sky.audio.masterGain === 0,
|
||||||
|
`unlock forgot a pre-unlock mute: master at ${sky.audio.masterGain}`);
|
||||||
|
sky.setMute(false);
|
||||||
|
assert(sky.audio.masterGain > 0, 'unmute left the master gain at 0');
|
||||||
|
}
|
||||||
|
sky.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
// --- SPRINT2 §Lane C.3: rain has to stop at the cloth ---
|
// --- SPRINT2 §Lane C.3: rain has to stop at the cloth ---
|
||||||
// Driven with a synthetic 4×4 m panel rather than a whole cloth sim: the thing
|
// Driven with a synthetic 4×4 m panel rather than a whole cloth sim: the thing
|
||||||
// under test is the projection, and a flat panel makes the right answer
|
// under test is the projection, and a flat panel makes the right answer
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user