diff --git a/media/teaser/README.md b/media/teaser/README.md index cf02186..db4b114 100644 --- a/media/teaser/README.md +++ b/media/teaser/README.md @@ -9,15 +9,15 @@ Real product footage only. Every frame is a pure function of time, rendered from Chrome and encoded with ffmpeg; the music and sound design are synthesized in the same browser (Web Audio `OfflineAudioContext`): no samples, nothing to license. -120 BPM: every cut is a hard cut on a beat (whole half seconds). Before any final render, make the animatic +120 BPM: every cut is a hard cut on a beat (whole half seconds), with a sound accent on each. Before any final render, make the animatic (`--animatic`: a 960x540 draft with sound, a one-frame-per-second contact sheet and the shot list) and get the owner's OK. | s | shows | text | music | |---|---|---|---| -| 0-3 | the demo game's own screens rush past, a push in | Your game is done. | calm pad, a pluck | -| 3-7 | real store work lands one card per beat: App Privacy answers and steps, the Data safety CSV (768 rows), screenshot sizes, price sheets | Then come the stores. | tension, a thud per card | -| 7-9.5 | the real scan finding (BLOCKER 5.1.1(v), Planner/LoginView.swift:11) slams in, a push onto it, the picture darkens | One line. Rejected. | a low boom | -| 9.5-12 | one request typed to the AI agent, sent on the beat | So my AI did it. | near silence, key clicks, a swell | +| 0-3 | the demo game's own screens rush past, a push in | Your game is done. | drives from frame 0: kick, bass, arpeggio | +| 3-7 | real store work lands one card per beat: App Privacy answers and steps, the Data safety CSV (768 rows), screenshot sizes, price sheets | Then come the stores. | tension: B minor pulse, kicks, a thud per card | +| 7-9.5 | the real scan finding (BLOCKER 5.1.1(v), Planner/LoginView.swift:11) slams in, a push onto it, the picture darkens | One line. Rejected. | a low boom, the clock keeps ticking | +| 9.5-12 | one request typed to the AI agent, sent on the beat | So my AI did it. | a short drop: an impact, half-time beat, key clicks, a swell | | 12-15 | `apple build --dry`, `play build --dry`: build 52, versionCode 52 | Builds. | the drop | | 15-18 | the demo game's renders: 7 store sizes x 4 languages fill in | Screenshots, every size. | + hats | | 18-21 | `play products --dry`, `catalog diff` (175 / 174 region prices, 0 mismatches) | Products, 175 countries. | + claps | @@ -26,7 +26,7 @@ Chrome and encoded with ffmpeg; the music and sound design are synthesized in th | 27-31 | `storebound scan`: five findings turn green one per beat, the clean re-scan | Nothing left to reject. | a chime per fix | | 31-33 | the desktop app's Send for review, the camera pushes onto Submit, the pointer arrives | You press Submit. | the music stops | | 33-36 | the click, then the app's own "Waiting for review" | (same line) | the resolve | -| 36-41 | the logo, `npx storebound init` typed out, storebound.dev; holds 4 s | (none) | the chord holds | +| 36-41 | the logo, `npx storebound init` typed out, storebound.dev; holds 4 s | (none) | a confident kick and bass on D, the chord; fades over the last 1 s | The demo game has 4 languages (en, tr, ja, ar), so the screenshot beat says "every size", not a language count. diff --git a/media/teaser/music.js b/media/teaser/music.js index 138885d..cb79024 100644 --- a/media/teaser/music.js +++ b/media/teaser/music.js @@ -1,13 +1,15 @@ -// Original teaser music and sound design (v8, a story arc), synthesized from nothing with the Web Audio API +// Original teaser music and sound design (v9, the tight cut), synthesized from nothing with the Web Audio API // (OfflineAudioContext): no samples, no downloaded audio. 120 BPM (a beat every half second; every cut lands on one). -// calm game a warm D major pad, a slow soft pluck -// tension pile B minor, a low pulse in eighths that grows, a ticking clock, a riser; a thud per card -// dark reject a low boom, a dark drone that sinks to almost nothing -// silence ask room tone, the key clicks of the request, one soft note when it is sent; a swell into the drop +// calm game drives from frame 0: kick on every beat, D major bass eighths, a bright arpeggio (no slow intro) +// tension pile B minor, a low pulse in eighths that grows, kicks, a ticking clock, a riser; a thud per card +// dark reject a low boom, a dark drone, the pulse keeps ticking underneath +// silence ask a short drop: an impact on the cut, then a filtered half-time beat, the key clicks, a swell +// (every cut) an accent: a short noise crash and a low hit // drive montage+scan kick, bass, arpeggio, hats, claps: one layer more per montage beat (I-vi-IV-V, a chord per bar); // a chime per fix, rising // stop button the music stops on the cut; only a quiet low pulse, like holding your breath -// resolve waiting+end the click, then a wide D major add9 swell, bells, a slow arpeggio under the end card +// resolve waiting+end the click, then a wide D major add9 swell, bells; under the end card a confident kick and +// bass on D, the final chord; render.ts fades the last 1 s // Deterministic. renderWav() returns a 32-bit float stereo WAV (48 kHz) as base64; render.ts masters it with ffmpeg. import { CLICK_T, CUTS, DURATION, keyTimes, MONTAGE, scene, ticks } from './script.js'; @@ -133,10 +135,10 @@ export async function renderMusic() { const v = ctx.createGain(); env(v, t, 0.001, g, open ? 0.14 : 0.035); n.connect(hp); hp.connect(v); v.connect(p); p.connect(drive); } /** The bass: a filtered saw + sine, one note. */ - function bass(t, m, len, g = 0.11, cutoff = 520) { + function bass(t, m, len, g = 0.11, cutoff = 520, to = drive) { const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(cutoff * 2.2, t); lp.frequency.exponentialRampToValueAtTime(cutoff, t + len * 0.8); lp.Q.value = 2; const v = ctx.createGain(); v.gain.setValueAtTime(0, t); v.gain.linearRampToValueAtTime(g, t + 0.008); v.gain.setValueAtTime(g * 0.8, t + len * 0.7); v.gain.linearRampToValueAtTime(0, t + len); - lp.connect(v); v.connect(drive); + lp.connect(v); v.connect(to); for (const [type, mul] of [['sawtooth', 0.6], ['sine', 1]]) { const o = ctx.createOscillator(); o.type = type; o.frequency.value = midi(m); const og = ctx.createGain(); og.gain.value = mul; o.connect(og); og.connect(lp); o.start(t); o.stop(t + len + 0.05); @@ -160,13 +162,18 @@ export async function renderMusic() { tick(t, 0.09, 4200); tick(t + 0.055, 0.05, 2800); } - // ---------- calm: the game is done ---------- + // ---------- calm: the game is done (drives from frame 0) ---------- { + const [a, b] = S.calm; const [r, iv] = CH.Dmaj9; - pad(0, S.calm[1] + 0.2, [r, r + iv[1], r + iv[2], r + iv[3]], 0.032, 900, 1.6, 1.2); - pad(0, S.calm[1] + 0.2, [r - 12], 0.02, 300, 1.6, 1.0); - const tones = [62, 66, 69, 73, 74, 73, 69, 66]; - for (let t = 0.5, k = 0; t < S.calm[1] - 0.1; t += 2 * STEP, k++) pluck(t, tones[k % tones.length], 0.036, k % 2 ? 0.25 : -0.25, 1.6); + pad(0, b + 0.05, [r, r + iv[1], r + iv[2], r + iv[3]], 0.026, 1400, 0.02, 0.15, master, 2600); + for (let t = a, k = 0; t < b - 0.01; t += STEP, k++) { + kick(t, k % 4 === 0 ? 0.52 : 0.44, master); + hat(t + STEP / 2, 0.024); + bass(t, 38, STEP / 2 * 0.85, 0.1); bass(t + STEP / 2, k % 4 === 3 ? 45 : 38, STEP / 2 * 0.85, 0.085); + const tones = [74, 78, 81, 85]; + for (let j = 0; j < 4; j++) pluck(t + j * STEP / 4, tones[(k * 4 + j) % 4], 0.028 * (j === 0 ? 1.2 : 1), j % 2 ? 0.3 : -0.3, 0.3, master); + } } // ---------- tension: the stores pile up ---------- @@ -179,10 +186,11 @@ export async function renderMusic() { // the low pulse: eighths on B, louder and brighter towards the rejection for (let t = a, k = 0; t < b - 0.01; t += STEP / 2, k++) { const p = (t - a) / (b - a); - bass(t, k % 8 === 7 ? 42 : 35, STEP / 2 * 0.9, 0.05 + 0.07 * p, 260 + 900 * p); + bass(t, k % 8 === 7 ? 42 : 35, STEP / 2 * 0.9, 0.07 + 0.06 * p, 300 + 900 * p); + if (k % 2 === 0) kick(t, 0.42 + 0.1 * p, master); } // a clock ticking in sixteenths over the second half - for (let t = a + 3, k = 0; t < b - 0.01; t += STEP / 4, k++) tick(t, (k % 4 === 0 ? 0.03 : 0.016) * (0.6 + (t - a) / (b - a) * 0.6), k % 4 === 0 ? 5200 : 6800); + for (let t = a + 1, k = 0; t < b - 0.01; t += STEP / 4, k++) tick(t, (k % 4 === 0 ? 0.03 : 0.016) * (0.6 + (t - a) / (b - a) * 0.6), k % 4 === 0 ? 5200 : 6800); riser(b, 3, 0.07, 300, 6000); } @@ -193,17 +201,23 @@ export async function renderMusic() { const v = pad(a, b - 1.2, [35, 38, 42, 47], 0.04, 420, 0.1, 1.6, master, 260); void v; pad(a, b - 1.4, [59, 60], 0.008, 1800, 1.0, 1.4); // a faint, uneasy cluster above + // the clock keeps ticking: nothing stands still + for (let t = a + STEP, k = 0; t < b - 0.01; t += STEP / 2, k++) tick(t, k % 2 ? 0.014 : 0.026, k % 2 ? 6800 : 5200); + for (let t = a + 2 * STEP; t < b - 0.01; t += STEP) bass(t, 35, STEP * 0.4, 0.06, 240); } - // ---------- near silence: the request ---------- + // ---------- the drop: the request ---------- { const [a, b] = S.silence; - // room tone: very quiet, dark noise - const n = noiseSrc(a, b - a); const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 500; - const v = ctx.createGain(); v.gain.setValueAtTime(0, a); v.gain.linearRampToValueAtTime(0.012, a + 0.8); v.gain.setValueAtTime(0.012, b - 0.6); v.gain.linearRampToValueAtTime(0, b); - n.connect(lp); lp.connect(v); v.connect(master); - pluck(a + 0.5, 66, 0.022, 0, 2.5); - // the swell into the drop + // a short drop on the cut: one impact, then a filtered half-time beat under the key clicks + kick(a, 0.6, master); boom(a, 0.25); + const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(500, a); lp.frequency.exponentialRampToValueAtTime(5000, b); lp.connect(master); + for (let t = a + STEP, k = 1; t < b - 0.01; t += STEP, k++) { + if (k % 2 === 0) kick(t, 0.4, master); + hat(t + STEP / 2, 0.012 + 0.012 * (t - a) / (b - a)); + for (let j = 0; j < 2; j++) pluck(t + j * STEP / 2, [62, 66, 69, 74][(k * 2 + j) % 4] + 12, 0.03, j ? 0.25 : -0.25, 0.4, lp); + } + // the swell into the montage riser(b, 1.5, 0.08, 250, 7000); pad(b - 1.5, b, [62, 69, 74], 0.02, 600, 1.4, 0.05, master, 3000); } @@ -266,13 +280,21 @@ export async function renderMusic() { const e = scene('end').start; bell(e + 0.3, r + 24, 0.05); bell(e + 0.55, r + 31, 0.035, 0.2); + // a confident ending: kick and bass on D under the card, the full chord on the last bar + for (let t = e, k = 0; t < b - 0.01; t += STEP, k++) { + kick(t, k % 4 === 0 ? 0.48 : 0.36, master); + bass(t, 38, STEP * 0.8, 0.09, 600, master); + if (k % 2) tick(t, 0.02, 8000); + } + pad(e, b + 0.6, [r, r + 7, r + 12, r + 16, r + 19], 0.03, 2400, 0.05, 1.0); } - // ---------- soft accents on the story cuts ---------- + // ---------- an accent on every cut ---------- for (const c of CUTS) { - if (MONTAGE.includes(c.id) || c.id === 'scan') continue; - if (c.id === 'reject' || c.id === 'builds') continue; // the boom and the drop are the accents - bell(c.t + 0.01, c.id === 'pile' ? 71 : 74, 0.03, 0.1); + if (c.id === 'button') { bell(c.t + 0.01, 86, 0.04); continue; } // the stop: the music is cut, one bell + const n = noiseSrc(c.t, 0.6); const hp = ctx.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = 3500; + const v = ctx.createGain(); env(v, c.t, 0.002, 0.07, 0.45); n.connect(hp); hp.connect(v); v.connect(sfx); v.connect(sfxVerb); + if (c.id !== 'reject') thud(c.t, 0.14); } // ---------- UI sounds ---------- diff --git a/media/teaser/poster.jpg b/media/teaser/poster.jpg index cca33e3..8617c72 100644 Binary files a/media/teaser/poster.jpg and b/media/teaser/poster.jpg differ diff --git a/media/teaser/render.ts b/media/teaser/render.ts index c8e7dea..a6cd068 100644 --- a/media/teaser/render.ts +++ b/media/teaser/render.ts @@ -155,7 +155,7 @@ async function main(): Promise { const j = JSON.parse(m.stderr.toString().match(/\{[\s\S]*?\}/)![0]) as Record; const gain = Math.min(-16 - Number(j.input_i), -1.5 - Number(j.input_tp)); log(`sound: ${j.input_i} LUFS, LRA ${j.input_lra} LU, true peak ${j.input_tp} dBTP; gain ${gain.toFixed(2)} dB -> ${(Number(j.input_i) + gain).toFixed(1)} LUFS`); - ff('-i', raw, '-af', `volume=${gain.toFixed(2)}dB,afade=t=out:st=${(dur - 1.5).toFixed(2)}:d=1.5,aresample=48000`, + ff('-i', raw, '-af', `volume=${gain.toFixed(2)}dB,afade=t=out:st=${(dur - 1).toFixed(2)}:d=1,aresample=48000`, '-c:a', 'pcm_s16le', '-ar', '48000', '-ac', '2', music); if (!soundOnly) { const silent = join(OUT, `teaser-${FORMAT.w}x${FORMAT.h}-silent.mp4`), out = join(OUT, `teaser-${FORMAT.w}x${FORMAT.h}.mp4`); diff --git a/site/img/teaser-poster.webp b/site/img/teaser-poster.webp index be6384f..d55f9b0 100644 Binary files a/site/img/teaser-poster.webp and b/site/img/teaser-poster.webp differ diff --git a/site/img/teaser.mp4 b/site/img/teaser.mp4 index 24f2a11..3479641 100644 Binary files a/site/img/teaser.mp4 and b/site/img/teaser.mp4 differ diff --git a/site/index.html b/site/index.html index 6e9bd4c..39cc00e 100644 --- a/site/index.html +++ b/site/index.html @@ -73,8 +73,8 @@
-