Sound was dead in the app because status 0 is not a failure; and back is a swipe to the RIGHT
Oliver on the first shell build: *"I don't hear any of the games sounds.. daniel the umpire is synthesized which is horrible and no music is playing on start"* — two independent causes. - **Opaque fetch.** Capacitor serves the bundle through a custom WKURLSchemeHandler, and `fetch` reports those responses as `status: 0, ok: false` with the whole body present and decoding perfectly. All three loads in `audio.js` guarded with `if (!r.ok) throw`, so inside the app every recorded sound failed at the door: the umpire fell through to the synthesized `say()`, no bed started, no sample played. The synthesized sfx kept working, which made it read as a mixing bug rather than a loader one. Fixed once, in the shared `bytes()` helper every audio fetch now routes through — a status of 0 is the ABSENCE of an HTTP status, so trust it only when there is one and a real 404 still falls back. Any future `!r.ok` on a bundle-relative fetch is this same bug. - **The ring switch.** WKWebView's WebAudio runs in the SoloAmbient session, which the hardware silent switch mutes outright — a silent game on a phone that lives on silent, with nothing on screen to explain it. AppDelegate now sets AVAudioSession `.playback`, the games category. Confirmed in the shell: the bed reaches PLAYING, the context is `running` with no gesture at all (autoplay really is allowed in WKWebView, as LATER predicted), and `say()` never fires. On the web, a genuinely missing file still falls back — checked both. And the swipe flips: **right goes back**, the way iOS's own pop gesture goes (*"that's the iOS way"*). Left now does nothing. Noted in PROJECT.md that on a platform gesture the platform's convention beats the literal wording of the brief, and that is worth saying up front. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d71a26831a
commit
76ea1d0d79
6 files changed
+67
-20
No files matched your search
+17
-6
@@ -7,6 +7,20 @@ export const audio = () => {
|
||||
return AC;
|
||||
};
|
||||
|
||||
// EVERY audio file is fetched through here, and the reason is one line of it. Inside the Capacitor
|
||||
// shell the bundle is served by a custom WKURLSchemeHandler, and `fetch` reports those responses as
|
||||
// OPAQUE: `status` 0 and `ok` false, even though the bytes are all present and decode perfectly.
|
||||
// The old guard was a plain `if (!r.ok) throw` — so in the app EVERY recorded sound failed at the
|
||||
// door: the umpire fell through to the synthesized speech fallback, no bed ever started, and not a
|
||||
// single sample played, while the synthesized sfx kept working and made it look like a mixing bug
|
||||
// (Oliver, 2026-08-20, on the first shell build: *"daniel the umpire is synthesized which is
|
||||
// horrible and no music is playing on start"*). A status of 0 is not a failure, it is the absence
|
||||
// of an HTTP status — so trust the status only when there IS one, and let a real 404 still throw.
|
||||
const bytes = url => fetch(url).then(r => {
|
||||
if (r.status && !r.ok) throw new Error(url + ' ' + r.status);
|
||||
return r.arrayBuffer();
|
||||
});
|
||||
|
||||
// Umpire lines are pre-recorded files (assets/audio/*.m4a) played via WebAudio —
|
||||
// works on mobile where speechSynthesis is unreliable. say() remains the fallback.
|
||||
const VOICE = {};
|
||||
@@ -48,8 +62,7 @@ export function voice(name, delay = 0, rate = 1) {
|
||||
|
||||
function loadVoice(name) {
|
||||
if (VOICE[name]) return Promise.resolve(VOICE[name]);
|
||||
return fetch('assets/audio/' + name + '.m4a')
|
||||
.then(r => { if (!r.ok) throw 0; return r.arrayBuffer(); })
|
||||
return bytes('assets/audio/' + name + '.m4a')
|
||||
.then(ab => audio().decodeAudioData(ab))
|
||||
.then(buf => (VOICE[name] = buf));
|
||||
}
|
||||
@@ -119,8 +132,7 @@ export function sfx(name, vol = 1, opts = {}) {
|
||||
};
|
||||
if (SFX[name]) return play(SFX[name]);
|
||||
// a name with an extension is used as-is (weapon-click2.mp3); bare names keep the .wav default
|
||||
fetch('assets/audio/' + (name.includes('.') ? name : name + '.wav'))
|
||||
.then(r => { if (!r.ok) throw 0; return r.arrayBuffer(); })
|
||||
bytes('assets/audio/' + (name.includes('.') ? name : name + '.wav'))
|
||||
.then(ab => ac.decodeAudioData(ab))
|
||||
.then(buf => { SFX[name] = buf; play(buf); })
|
||||
.catch(() => {}); // missing sample = silence, never a thrown error mid-rally
|
||||
@@ -141,8 +153,7 @@ function loopBed(file, lowcut, vol, loopEnd = 0) {
|
||||
const ac = audio();
|
||||
if (beds.has(file)) return;
|
||||
beds.set(file, null);
|
||||
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(); })
|
||||
bytes('assets/audio/' + file) // full filename: the beds are a mix of .wav and .m4a, see below
|
||||
.then(ab => ac.decodeAudioData(ab))
|
||||
.then(buf => {
|
||||
// RESUME AGAIN, HERE. The gesture that unlocked the context is long gone by now: the menu
|
||||
|
||||
+4
-3
@@ -537,7 +537,8 @@ document.getElementById('sBack').addEventListener('click', () => {
|
||||
});
|
||||
document.getElementById('bsBack').addEventListener('click', () => go('secret'));
|
||||
|
||||
// SWIPE TO GO BACK. Every screen but the splash wears exactly one chevron in its header (.hdr in
|
||||
// SWIPE TO GO BACK — RIGHTWARD, the way iOS's own pop gesture goes (Oliver, 2026-08-20: *"that's
|
||||
// the iOS way"*). Every screen but the splash wears exactly one chevron in its header (.hdr in
|
||||
// index.html) and that button ALREADY knows where back is for its own screen — the wizard's shape
|
||||
// lives in the handlers above and nowhere else. So the gesture finds the one visible chevron and
|
||||
// clicks it, rather than keeping a second map that would drift the first time a stage is added.
|
||||
@@ -553,8 +554,8 @@ intro.addEventListener('touchend', e => {
|
||||
if (!from || e.touches.length) return;
|
||||
const t = e.changedTouches[0];
|
||||
const dx = t.clientX - from.x, dy = t.clientY - from.y;
|
||||
// leftward, and clearly horizontal: the stats and board screens scroll vertically under the finger
|
||||
if (dx > -SWIPE_MIN || Math.abs(dy) > Math.abs(dx) * 0.7) return;
|
||||
// rightward, and clearly horizontal: the stats and board screens scroll vertically under the finger
|
||||
if (dx < SWIPE_MIN || Math.abs(dy) > Math.abs(dx) * 0.7) return;
|
||||
const back = [...intro.querySelectorAll('.chev')].find(b => b.offsetParent); // the one on screen
|
||||
if (!back) return;
|
||||
// NOT passive, and this is why: a swipe that starts and ends inside one of the big poster cards
|
||||
|
||||
Reference in new issue
Block a user