diff --git a/lith/Caddyfile b/lith/Caddyfile index 92b689643d..d5884481a1 100644 --- a/lith/Caddyfile +++ b/lith/Caddyfile @@ -890,6 +890,14 @@ root * /opt/ac/system/public file_server } + # Apex → Mac App Store product page (the store listing is the + # converting front door; @jeffrey, 2026-08-31). Only the bare root + # forwards — /privacy.html, /support.html, /redeem.html, /advanced.html + # must stay served here because App Review points at them. 302, not + # 301, so the front door can move back without fighting browser caches. + handle / { + redir https://apps.apple.com/app/id6767311903 302 + } handle { root * /opt/ac/system/public/menuband try_files {path} {path}.html /index.html -- 2.51.2 From ef6afbab70c15d13ef018679cf2b0ec36539c537 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 31 Aug 2026 17:32:01 -0400 Subject: [PATCH 2/6] Tighten Cult's intro to two bars so the message lands at 0:04 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CXGQNxg1kLT79iwuuyTx2H --- pop/cult/bin/cut-final.sh | 8 ++++---- pop/cult/bin/cut-v10.sh | 10 +++++----- pop/cult/viz/review-score.py | 14 +++++++------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pop/cult/bin/cut-final.sh b/pop/cult/bin/cut-final.sh index 15931ace0f..3017069934 100644 --- a/pop/cult/bin/cut-final.sh +++ b/pop/cult/bin/cut-final.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # cut-final.sh — the release master (pop / DistroKid), proper loudness. # -# Chain: fresh render → four-bar intro edit → the Peep-pass space (bass shelf + +# Chain: fresh render → two-bar intro edit → the Peep-pass space (bass shelf + # cathedral convolution, same as cut-v10) → MEASURE → one static dB to # −11.5 LUFS integrated → true-peak limiter with a −1.0 dBTP ceiling. # Never a second loudnorm. Deliverables: @@ -21,10 +21,10 @@ FULL="$OUT/cult-remix-v10-full.wav" TRIM="$OUT/.final-trim.wav" SPACE="$OUT/.final-space.wav" -echo "→ four-bar intro edit (keep bars 8–11, cut bars 12–23)" +echo "→ two-bar intro edit (keep bars 8–9, cut bars 10–28)" ffmpeg -y -v error -i "$FULL" -filter_complex \ - "[0:a]atrim=start=15.95:end=24,asetpts=PTS-STARTPTS[a];\ -[0:a]atrim=start=48,asetpts=PTS-STARTPTS[b];\ + "[0:a]atrim=start=15.95:end=20,asetpts=PTS-STARTPTS[a];\ +[0:a]atrim=start=58,asetpts=PTS-STARTPTS[b];\ [a][b]concat=n=2:v=0:a=1,afade=t=in:st=0:d=0.02[out]" \ -map "[out]" -c:a pcm_s24le "$TRIM" diff --git a/pop/cult/bin/cut-v10.sh b/pop/cult/bin/cut-v10.sh index 0c671a2bb8..e8bdeecb45 100644 --- a/pop/cult/bin/cut-v10.sh +++ b/pop/cult/bin/cut-v10.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # cut-v10.sh — master the v10 render and cut the mp3. # -# v10 starts when the kicks start, keeps four bars of the three-voice intro, -# then cuts render bars 12–23 so THE MESSAGE arrives at 0:08 instead of 0:32. +# v10 starts when the kicks start, keeps two bars of the three-voice intro, +# then cuts render bars 10–28 so the sentence itself arrives at about 0:04. # The full renderer and receipt remain archival; this is the release edit. # # Same mastering law as v3/v5/v9: MEASURE → one static dB → true-peak @@ -18,10 +18,10 @@ TRIM="$OUT/cult-remix-v10-full-trim.wav" SRC="$OUT/cult-remix-v10-full-master.wav" if [ ! -f "$TRIM" ] || [ "$FULL" -nt "$TRIM" ] || [ "$0" -nt "$TRIM" ]; then - echo "→ four-bar intro edit (keep bars 8–11, cut bars 12–23)" + echo "→ two-bar intro edit (keep bars 8–9, cut bars 10–28)" ffmpeg -y -v error -i "$FULL" -filter_complex \ - "[0:a]atrim=start=15.95:end=24,asetpts=PTS-STARTPTS[a];\ -[0:a]atrim=start=48,asetpts=PTS-STARTPTS[b];\ + "[0:a]atrim=start=15.95:end=20,asetpts=PTS-STARTPTS[a];\ +[0:a]atrim=start=58,asetpts=PTS-STARTPTS[b];\ [a][b]concat=n=2:v=0:a=1,afade=t=in:st=0:d=0.02[out]" \ -map "[out]" -c:a pcm_s24le "$TRIM" fi diff --git a/pop/cult/viz/review-score.py b/pop/cult/viz/review-score.py index 681970590b..db7893616f 100644 --- a/pop/cult/viz/review-score.py +++ b/pop/cult/viz/review-score.py @@ -13,8 +13,8 @@ # · out/cult-remix-v10.events.json — the score receipt (regenerate with # `node pop/cult/bin/render10.mjs`); word mapping ported from # bin/transcript.mjs. Receipt times are FULL-render seconds; the -# release edit starts TRIM=15.95 s in, keeps render bars 8–11, then -# removes bars 12–23 so bar 24 (THE MESSAGE) lands at about 0:08. +# release edit starts TRIM=15.95 s in, keeps render bars 8–9, then +# removes bars 10–28 so bar 29 (the sentence) lands at about 0:04. # · out/stems/v10-*.wav — TRUE per-bus stems from # `node pop/cult/bin/render10.mjs --stems` (vox / tube / music / # drums / signal). The music and drums buses are band-split into @@ -81,10 +81,10 @@ EIGHTH = BEAT / 2 TRIM = 15.95 # shipped t = full-render t − TRIM GRID0 = 8 * BAR - TRIM # 0.05 — bar 8's downbeat in shipped time BAR0 = 8 # ruler numbers speak render bars (hook = 29) -INTRO_CUT = (12 * BAR, 24 * BAR) # remove full-render bars 12–23 (24 s) +INTRO_CUT = (10 * BAR, 29 * BAR) # remove full-render bars 10–28 (38 s) def ship_time(full_t): - """Map full-render receipt time into the four-bar-intro release edit.""" + """Map full-render receipt time into the two-bar-intro release edit.""" if full_t < INTRO_CUT[0]: return full_t - TRIM if full_t < INTRO_CUT[1]: @@ -131,7 +131,7 @@ def intro_cut(sig): b = int(round((INTRO_CUT[1] - TRIM) * sr)) return np.concatenate([sig[:a], sig[b:]]) -def stem(name, af=None): # bus stem, trimmed + four-bar-intro edit +def stem(name, af=None): # bus stem, trimmed + two-bar-intro edit sig = load(os.path.join(STEMS, f"v10-{name}.wav"), af=af, ss=TRIM) return fit(intro_cut(sig)) @@ -560,10 +560,10 @@ PAD_L = PLAY_X - GUT PAD_R = W - PLAY_X STRIP_W = SWm + PAD_L + PAD_R -# Acts in release time. Render bars 12–23 are absent from this edit. +# Acts in release time. Render bars 10–28 are absent from this edit. ACTS = [ (0.00, "II THREE VOICES", (64, 190, 180)), - (ship_time(24 * BAR), "III THE MESSAGE", (235, 150, 70)), + (ship_time(29 * BAR), "III THE MESSAGE", (235, 150, 70)), (ship_time(40 * BAR), "IV THE SECRET", (205, 75, 85)), (ship_time(48 * BAR), "V THE REPLY", (120, 200, 120)), (ship_time(64 * BAR), "VI IT SPREADS", (170, 130, 230)), -- 2.51.2 From b949595858e60add3fe848800f8f43090bfec661 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 31 Aug 2026 17:32:01 -0400 Subject: [PATCH 3/6] Give Oskiewar a voice with a shared gesture unlock Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CXGQNxg1kLT79iwuuyTx2H --- xbox/live/mac-test.html | 58 +- xbox/live/oskiewar-voice.mjs | 670 ++++++++++++++++++++++++ xbox/live/tests/oskiewar-voice.test.mjs | 335 ++++++++++++ 3 files changed, 1034 insertions(+), 29 deletions(-) create mode 100644 xbox/live/oskiewar-voice.mjs create mode 100644 xbox/live/tests/oskiewar-voice.test.mjs diff --git a/xbox/live/mac-test.html b/xbox/live/mac-test.html index ee17feade6..29497194f6 100644 --- a/xbox/live/mac-test.html +++ b/xbox/live/mac-test.html @@ -328,6 +328,15 @@ return audioContext; } + // Every gesture that can start the game goes through here. Both banks are + // gesture-locked — the voice doubly so, because iOS only grants + // speechSynthesis from inside the first touch that asks for it. + function unlockAudio() { + ensureAudio(); + globalThis.__oskiewarSfx?.unlock().catch(() => {}); + globalThis.__oskiewarVoice?.unlock().catch(() => {}); + } + // Cached ElevenLabs female:3 rendition of "Oskie War". Separate media // elements create a compact feedback echo without requiring cross-origin // Web Audio access to the public AC audio CDN. @@ -382,8 +391,7 @@ } dismissTitle(); keys[0].add("A"); - ensureAudio(); - globalThis.__oskiewarSfx?.unlock().catch(() => {}); + unlockAudio(); setTimeout(() => keys[0].delete("A"), 90); return true; } @@ -1070,8 +1078,7 @@ event.preventDefault(); dismissTitle(); keys[mapped[0]].add(mapped[1]); - ensureAudio(); - globalThis.__oskiewarSfx?.unlock().catch(() => {}); + unlockAudio(); }); addEventListener("keyup", (event) => { const mapped = keyMap.get(event.code); @@ -1126,8 +1133,7 @@ canvas.setPointerCapture?.(event.pointerId); touchPointers.set(event.pointerId, { key: touchKey }); keys[0].add(touchKey); - ensureAudio(); - globalThis.__oskiewarSfx?.unlock().catch(() => {}); + unlockAudio(); return; } if (tapTitle(point)) { @@ -1152,8 +1158,7 @@ } Object.assign(globalThis.__oskiewarTouch.pointer, point, { active: true }); selectTaps.push(point); - ensureAudio(); - globalThis.__oskiewarSfx?.unlock().catch(() => {}); + unlockAudio(); event.preventDefault(); }); for (const type of ["pointerup", "pointercancel", "lostpointercapture"]) @@ -1172,6 +1177,7 @@