teaser v8: a 58 s story instead of a feature list (the game is done; the stores pile up; one missing line; hand it to your AI; a montage of builds, screenshots, products, forms, consoles; the scan turns green; one button is yours; Waiting for review; the end card), one short line per scene, cuts on the 120 BPM beat; music rewritten for the arc (calm, tension, near silence, a driving build, a stop, a resolve), mastered with one gain instead of dynamic loudnorm; the pile shows real console plan, Data safety CSV and price sheet lines, the builds beat real apple / play build dry runs; desktop.ts captures submit-ready (Send for review before the press); tests check every line, the beat-aligned cuts and the caption times; two unused teaser icons dropped
This commit is contained in:
parent
d5ffc159c0
commit
0b6c8cd7a0
11 changed files with 1117 additions and 1041 deletions
|
|
@ -1,150 +1,288 @@
|
|||
// Original teaser music and sound design (v4, calm), synthesized from nothing with the Web Audio API
|
||||
// (OfflineAudioContext): no samples, no downloaded audio. D major, one chord per scene: warm pads that cross-fade on the
|
||||
// scene changes, a slow, soft plucked arpeggio, a quiet sub, and only soft accents on the scene changes (a bell and a
|
||||
// short airy swell into each one). On top, quiet UI sounds from script.js: key clicks for typed text, ticks for lines
|
||||
// and rows, a low tone per finding, rising chimes for the fixes, a warm swell under the end card. Deterministic.
|
||||
// renderWav() returns a 32-bit float stereo WAV (48 kHz) as base64; render.ts masters it with ffmpeg loudnorm.
|
||||
import { CUTS, DURATION, keyTimes, SCENES, scene, SUBMIT, ticks } from './script.js';
|
||||
// Original teaser music and sound design (v8, a story arc), 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
|
||||
// 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
|
||||
// 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';
|
||||
|
||||
const SR = 48000;
|
||||
/** One beat (s): 120 BPM. Every scene starts on a whole number of beats (tests/teaser.test.ts). */
|
||||
export const STEP = 0.5;
|
||||
const BAR = STEP * 4;
|
||||
const midi = m => 440 * Math.pow(2, (m - 69) / 12);
|
||||
function rng(seed) { // mulberry32
|
||||
let a = seed >>> 0;
|
||||
return () => { a = (a + 0x6d2b79f5) >>> 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
|
||||
}
|
||||
|
||||
// chords (root midi in octave 3, intervals): one per scene, in scene order
|
||||
const CH = {
|
||||
Bm: [47, [0, 3, 7, 14]], G: [43, [0, 4, 7, 14]], D: [50, [0, 4, 7, 11]], A: [45, [0, 4, 7, 14]], Em: [52, [0, 3, 7, 10]], Dadd9: [50, [0, 4, 7, 14]],
|
||||
};
|
||||
const SCENE_CHORD = { hook: 'Bm', init: 'G', plan: 'D', store: 'A', products: 'Bm', forms: 'G', consoles: 'A', rejections: 'Em', portfolio: 'G', charts: 'D', catalog: 'Bm', resubmit: 'G', submit: 'D' };
|
||||
const ARP = [0, 1, 2, 3, 2, 1, 2, 3];
|
||||
/** One soft note every half second: every scene is a whole number of half seconds long (tests/teaser.test.ts), so each
|
||||
* cut lands on a note of the arpeggio. */
|
||||
export const STEP = 0.5;
|
||||
/** The arc: section boundaries in seconds, from the scenes. */
|
||||
export function sections() {
|
||||
const s = id => scene(id).start, e = id => scene(id).end;
|
||||
return { calm: [0, s('pile')], tension: [s('pile'), s('reject')], dark: [s('reject'), s('ask')], silence: [s('ask'), s('builds')],
|
||||
drive: [s('builds'), s('button')], stop: [s('button'), CLICK_T], resolve: [CLICK_T, e('end')] };
|
||||
}
|
||||
// chords (root midi, intervals)
|
||||
const CH = { D: [50, [0, 4, 7, 14]], Bm: [47, [0, 3, 7, 14]], G: [43, [0, 4, 7, 14]], A: [45, [0, 4, 7, 14]], Dmaj9: [50, [0, 4, 7, 11, 14]] };
|
||||
const PROG = ['D', 'Bm', 'G', 'A'];
|
||||
|
||||
export async function renderMusic() {
|
||||
const ctx = new OfflineAudioContext({ numberOfChannels: 2, length: Math.ceil(DURATION * SR) + SR * 2, sampleRate: SR });
|
||||
const R = rng(20261012);
|
||||
const R = rng(20261013);
|
||||
const S = sections();
|
||||
|
||||
const noise = ctx.createBuffer(2, SR * 3, SR);
|
||||
const noise = ctx.createBuffer(2, SR * 6, SR);
|
||||
for (let c = 0; c < 2; c++) { const d = noise.getChannelData(c); for (let i = 0; i < d.length; i++) d[i] = R() * 2 - 1; }
|
||||
const ir = ctx.createBuffer(2, Math.floor(SR * 3.2), SR);
|
||||
for (let c = 0; c < 2; c++) { const d = ir.getChannelData(c); for (let i = 0; i < d.length; i++) d[i] = (R() * 2 - 1) * Math.pow(1 - i / d.length, 3.6); }
|
||||
const ir = ctx.createBuffer(2, Math.floor(SR * 3.4), SR);
|
||||
for (let c = 0; c < 2; c++) { const d = ir.getChannelData(c); for (let i = 0; i < d.length; i++) d[i] = (R() * 2 - 1) * Math.pow(1 - i / d.length, 3.4); }
|
||||
|
||||
// buses: a gentle compressor, a long room
|
||||
const comp = ctx.createDynamicsCompressor();
|
||||
comp.threshold.value = -18; comp.knee.value = 10; comp.ratio.value = 2.5; comp.attack.value = 0.02; comp.release.value = 0.4;
|
||||
comp.threshold.value = -16; comp.knee.value = 8; comp.ratio.value = 3; comp.attack.value = 0.008; comp.release.value = 0.25;
|
||||
const master = ctx.createGain(); master.gain.value = 0.7;
|
||||
master.connect(comp); comp.connect(ctx.destination);
|
||||
const verb = ctx.createConvolver(); verb.buffer = ir;
|
||||
const verbIn = ctx.createGain(); verbIn.gain.value = 0.55; verbIn.connect(verb); verb.connect(master);
|
||||
const sfx = ctx.createGain(); sfx.gain.value = 0.7; sfx.connect(master);
|
||||
const verbIn = ctx.createGain(); verbIn.gain.value = 0.5; verbIn.connect(verb); verb.connect(master);
|
||||
const sfx = ctx.createGain(); sfx.gain.value = 0.75; sfx.connect(master);
|
||||
const sfxVerb = ctx.createGain(); sfxVerb.gain.value = 0.3; sfxVerb.connect(verbIn);
|
||||
// the drums and bass: their own bus, so the stop can cut them all at once
|
||||
const drive = ctx.createGain(); drive.connect(master);
|
||||
drive.gain.setValueAtTime(1, 0); drive.gain.setValueAtTime(1, S.stop[0] - 0.01); drive.gain.linearRampToValueAtTime(0, S.stop[0] + 0.03);
|
||||
|
||||
const env = (g, t, a, peak, d, end = 0.0003) => {
|
||||
g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(peak, t + a); g.gain.exponentialRampToValueAtTime(end, t + a + d);
|
||||
};
|
||||
const noiseSrc = (t, dur, off = R() * 1.5) => { const s = ctx.createBufferSource(); s.buffer = noise; s.start(t, off, dur); return s; };
|
||||
|
||||
/** A warm pad: detuned saws through a soft low-pass, slow attack and release (cross-fades with the next one). */
|
||||
function pad(t0, t1, notes, g = 0.035, cutoff = 1100) {
|
||||
/** A warm pad: detuned saws through a soft low-pass, slow attack and release. */
|
||||
function pad(t0, t1, notes, g = 0.035, cutoff = 1100, attack = 1.2, release = 1.0, to = master, cutoffTo = null) {
|
||||
const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = cutoff; lp.Q.value = 0.5;
|
||||
if (cutoffTo) { lp.frequency.setValueAtTime(cutoff, t0); lp.frequency.exponentialRampToValueAtTime(cutoffTo, t1); }
|
||||
const v = ctx.createGain();
|
||||
v.gain.setValueAtTime(0, t0); v.gain.linearRampToValueAtTime(g, t0 + 1.2); v.gain.setValueAtTime(g, t1 - 0.2); v.gain.linearRampToValueAtTime(0, t1 + 1.0);
|
||||
lp.connect(v); v.connect(master);
|
||||
v.gain.setValueAtTime(0, t0); v.gain.linearRampToValueAtTime(g, t0 + attack); v.gain.setValueAtTime(g, Math.max(t0 + attack, t1 - 0.05)); v.gain.linearRampToValueAtTime(0, t1 + release);
|
||||
lp.connect(v); v.connect(to);
|
||||
const s = ctx.createGain(); s.gain.value = 0.7; v.connect(s); s.connect(verbIn);
|
||||
for (const m of notes) for (const det of [-7, 7]) {
|
||||
const o = ctx.createOscillator(); o.type = 'sawtooth'; o.frequency.value = midi(m); o.detune.value = det;
|
||||
const p = ctx.createStereoPanner(); p.pan.value = det < 0 ? -0.35 : 0.35;
|
||||
o.connect(p); p.connect(lp); o.start(t0); o.stop(t1 + 1.1);
|
||||
o.connect(p); p.connect(lp); o.start(t0); o.stop(t1 + release + 0.1);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
/** A soft pluck (sine + a little triangle), the arpeggio voice. */
|
||||
function pluck(t, m, g = 0.05, pan = 0) {
|
||||
const v = ctx.createGain(); env(v, t, 0.006, g, 1.1);
|
||||
/** A soft pluck (sine + a little triangle). */
|
||||
function pluck(t, m, g = 0.05, pan = 0, len = 1.1, to = master) {
|
||||
const v = ctx.createGain(); env(v, t, 0.006, g, len);
|
||||
const p = ctx.createStereoPanner(); p.pan.value = pan;
|
||||
v.connect(p); p.connect(master);
|
||||
const s = ctx.createGain(); s.gain.value = 0.6; p.connect(s); s.connect(verbIn);
|
||||
v.connect(p); p.connect(to);
|
||||
const s = ctx.createGain(); s.gain.value = 0.5; p.connect(s); s.connect(verbIn);
|
||||
for (const [type, k, mul] of [['sine', 1, 1], ['triangle', 2, 0.18]]) {
|
||||
const o = ctx.createOscillator(); o.type = type; o.frequency.value = midi(m) * k;
|
||||
const og = ctx.createGain(); og.gain.value = mul; o.connect(og); og.connect(v); o.start(t); o.stop(t + 1.3);
|
||||
const og = ctx.createGain(); og.gain.value = mul; o.connect(og); og.connect(v); o.start(t); o.stop(t + len + 0.2);
|
||||
}
|
||||
}
|
||||
/** A quiet sub under each scene. */
|
||||
function sub(t0, t1, m, g = 0.07) {
|
||||
const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.value = midi(m - 12);
|
||||
const v = ctx.createGain(); v.gain.setValueAtTime(0, t0); v.gain.linearRampToValueAtTime(g, t0 + 0.8); v.gain.setValueAtTime(g, t1 - 0.3); v.gain.linearRampToValueAtTime(0, t1 + 0.4);
|
||||
o.connect(v); v.connect(master); o.start(t0); o.stop(t1 + 0.5);
|
||||
}
|
||||
/** The soft accent on a scene change: a bell an octave or two up, with a long tail. */
|
||||
function bell(t, m, g = 0.05, pan = 0) {
|
||||
const p = ctx.createStereoPanner(); p.pan.value = pan; p.connect(sfx); p.connect(sfxVerb);
|
||||
for (const [k, mul, d] of [[1, 1, 2.2], [2.01, 0.25, 1.2], [3.0, 0.08, 0.7]]) {
|
||||
for (const [k, mul, d] of [[1, 1, 2.4], [2.01, 0.25, 1.3], [3.0, 0.08, 0.7]]) {
|
||||
const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.value = midi(m) * k;
|
||||
const v = ctx.createGain(); env(v, t, 0.004, g * mul, d); o.connect(v); v.connect(p); o.start(t); o.stop(t + d + 0.1);
|
||||
}
|
||||
}
|
||||
/** A short airy swell into a scene change (very quiet). */
|
||||
function swell(t, dur = 0.9, g = 0.03) {
|
||||
const n = noiseSrc(t - dur, dur + 0.1); const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.Q.value = 0.8;
|
||||
bp.frequency.setValueAtTime(600, t - dur); bp.frequency.exponentialRampToValueAtTime(3200, t);
|
||||
const v = ctx.createGain(); v.gain.setValueAtTime(0.0001, t - dur); v.gain.exponentialRampToValueAtTime(g, t - 0.05); v.gain.linearRampToValueAtTime(0, t + 0.1);
|
||||
n.connect(bp); bp.connect(v); v.connect(sfx); v.connect(sfxVerb);
|
||||
}
|
||||
function tone(t, m, g, len = 0.25, type = 'sine', pan = 0) {
|
||||
const o = ctx.createOscillator(); o.type = type; o.frequency.value = midi(m);
|
||||
const p = ctx.createStereoPanner(); p.pan.value = pan;
|
||||
const v = ctx.createGain(); env(v, t, 0.004, g, len); o.connect(v); v.connect(p); p.connect(sfx); p.connect(sfxVerb);
|
||||
o.start(t); o.stop(t + len + 0.05);
|
||||
}
|
||||
function tick(t, g = 0.03, f0 = 3000) {
|
||||
const n = noiseSrc(t, 0.015); const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = f0; bp.Q.value = 3;
|
||||
const v = ctx.createGain(); env(v, t, 0.0006, g, 0.014); n.connect(bp); bp.connect(v); v.connect(sfx);
|
||||
/** Filtered noise: a swell / riser into time t (dur long), band-pass sweeping up. */
|
||||
function riser(t, dur, g = 0.04, f0 = 400, f1 = 5000, to = sfx) {
|
||||
const n = noiseSrc(t - dur, Math.min(dur + 0.1, 4.4)); const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.Q.value = 0.9;
|
||||
bp.frequency.setValueAtTime(f0, t - dur); bp.frequency.exponentialRampToValueAtTime(f1, t);
|
||||
const v = ctx.createGain(); v.gain.setValueAtTime(0.0001, t - dur); v.gain.exponentialRampToValueAtTime(g, t - 0.03); v.gain.linearRampToValueAtTime(0, t + 0.08);
|
||||
n.connect(bp); bp.connect(v); v.connect(to); v.connect(sfxVerb);
|
||||
}
|
||||
function tick(t, g = 0.03, f0 = 3000, to = sfx) {
|
||||
const n = noiseSrc(t, 0.02); const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = f0; bp.Q.value = 3;
|
||||
const v = ctx.createGain(); env(v, t, 0.0006, g, 0.016); n.connect(bp); bp.connect(v); v.connect(to);
|
||||
}
|
||||
function key(t) {
|
||||
const n = noiseSrc(t, 0.02); const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = 2400 + R() * 1800; bp.Q.value = 2.5;
|
||||
const p = ctx.createStereoPanner(); p.pan.value = R() * 0.4 - 0.2;
|
||||
const v = ctx.createGain(); env(v, t, 0.0008, 0.05 * (0.7 + R() * 0.6), 0.016); n.connect(bp); bp.connect(v); v.connect(p); p.connect(sfx);
|
||||
const v = ctx.createGain(); env(v, t, 0.0008, 0.06 * (0.7 + R() * 0.6), 0.018); n.connect(bp); bp.connect(v); v.connect(p); p.connect(sfx);
|
||||
}
|
||||
|
||||
// ---- the bed: one chord per scene ----
|
||||
SCENES.forEach((s, i) => {
|
||||
const [root, iv] = CH[SCENE_CHORD[s.id]];
|
||||
const last = i === SCENES.length - 1;
|
||||
const t0 = Math.max(0, s.start - 0.4), t1 = last ? DURATION + 0.6 : s.end;
|
||||
const notes = [root, root + iv[1], root + iv[2], root + 12];
|
||||
pad(t0, last ? scene('submit').start + SUBMIT.logoAt : t1, notes, s.id === 'hook' ? 0.03 : 0.034, s.id === 'hook' ? 800 : 1100);
|
||||
sub(t0, t1, root, s.id === 'hook' ? 0.05 : 0.065);
|
||||
// the arpeggio: quiet under the hook, then a steady, soft pulse
|
||||
const tones = iv.map(x => root + 12 + x).concat(iv.map(x => root + 24 + x));
|
||||
for (let t = Math.max(0.5, s.start), k = 0; t < (last ? s.start + SUBMIT.logoAt : s.end) - 0.05; t += STEP, k++) {
|
||||
const m = tones[ARP[k % ARP.length] + (k % 16 >= 8 ? 1 : 0)] ?? tones[0];
|
||||
pluck(t, m, s.id === 'hook' ? 0.028 : 0.04, (k % 2 ? 0.25 : -0.25));
|
||||
/** A soft synth kick: a sine with a fast pitch drop. */
|
||||
function kick(t, g = 0.5, to = drive) {
|
||||
const o = ctx.createOscillator(); o.type = 'sine';
|
||||
o.frequency.setValueAtTime(130, t); o.frequency.exponentialRampToValueAtTime(44, t + 0.12);
|
||||
const v = ctx.createGain(); env(v, t, 0.002, g, 0.32); o.connect(v); v.connect(to); o.start(t); o.stop(t + 0.4);
|
||||
tick(t, g * 0.06, 1800, to);
|
||||
}
|
||||
/** A clap / snare: noise through a band-pass, a short tail. */
|
||||
function clap(t, g = 0.12) {
|
||||
const n = noiseSrc(t, 0.25); const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = 1500; bp.Q.value = 0.8;
|
||||
const v = ctx.createGain(); env(v, t, 0.002, g, 0.16); n.connect(bp); bp.connect(v); v.connect(drive);
|
||||
const s = ctx.createGain(); s.gain.value = 0.4; v.connect(s); s.connect(verbIn);
|
||||
}
|
||||
function hat(t, g = 0.03, open = false) {
|
||||
const n = noiseSrc(t, open ? 0.2 : 0.05); const hp = ctx.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = 7500;
|
||||
const p = ctx.createStereoPanner(); p.pan.value = 0.2;
|
||||
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) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
});
|
||||
// the end card: a warm, open chord swell and one bell
|
||||
const end = scene('submit').start + SUBMIT.logoAt;
|
||||
const [er, eiv] = CH.Dadd9;
|
||||
pad(end - 0.3, DURATION + 0.6, [er - 12, er, er + eiv[1], er + eiv[2], er + eiv[3]], 0.045, 1500);
|
||||
bell(end + 0.2, er + 24, 0.06);
|
||||
bell(end + 0.55, er + 24 + 7, 0.04, 0.2);
|
||||
|
||||
// ---- soft accents on the scene changes ----
|
||||
for (const c of CUTS) {
|
||||
const [root, iv] = CH[SCENE_CHORD[c.id]];
|
||||
swell(c.t, 0.9, 0.025);
|
||||
bell(c.t + 0.02, root + 24 + iv[2], 0.045, (c.t * 7) % 2 - 1 > 0 ? 0.15 : -0.15);
|
||||
}
|
||||
/** A deep, slow boom (the rejection): a sine falling from 70 Hz plus low noise, a long room. */
|
||||
function boom(t, g = 0.5) {
|
||||
const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.setValueAtTime(72, t); o.frequency.exponentialRampToValueAtTime(30, t + 1.6);
|
||||
const v = ctx.createGain(); env(v, t, 0.005, g, 2.2); o.connect(v); v.connect(master);
|
||||
const n = noiseSrc(t, 1.2); const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 240;
|
||||
const nv = ctx.createGain(); env(nv, t, 0.004, g * 0.5, 0.9); n.connect(lp); lp.connect(nv); nv.connect(master); nv.connect(verbIn);
|
||||
}
|
||||
function thud(t, g = 0.18) {
|
||||
const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.setValueAtTime(110, t); o.frequency.exponentialRampToValueAtTime(48, t + 0.15);
|
||||
const v = ctx.createGain(); env(v, t, 0.002, g, 0.28); o.connect(v); v.connect(sfx);
|
||||
const n = noiseSrc(t, 0.12); const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 900;
|
||||
const nv = ctx.createGain(); env(nv, t, 0.002, g * 0.35, 0.09); n.connect(lp); lp.connect(nv); nv.connect(sfx); nv.connect(sfxVerb);
|
||||
}
|
||||
/** The mouse click on Submit. */
|
||||
function click(t) {
|
||||
tick(t, 0.09, 4200); tick(t + 0.055, 0.05, 2800);
|
||||
}
|
||||
|
||||
// ---- UI sounds ----
|
||||
// ---------- calm: the game is done ----------
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// ---------- tension: the stores pile up ----------
|
||||
{
|
||||
const [a, b] = S.tension;
|
||||
const [r, iv] = CH.Bm;
|
||||
pad(a - 0.3, b, [r, r + iv[1], r + iv[2], r + 12], 0.03, 700, 1.0, 0.4, master, 2600);
|
||||
// a b9 creeps in over the last two seconds
|
||||
pad(b - 2.2, b, [r + 13], 0.016, 1200, 1.8, 0.3);
|
||||
// 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);
|
||||
}
|
||||
// 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);
|
||||
riser(b, 3, 0.07, 300, 6000);
|
||||
}
|
||||
|
||||
// ---------- dark: one missing line ----------
|
||||
{
|
||||
const [a, b] = S.dark;
|
||||
boom(a, 0.55);
|
||||
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
|
||||
}
|
||||
|
||||
// ---------- near silence: 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
|
||||
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);
|
||||
}
|
||||
|
||||
// ---------- drive: the montage and the scan ----------
|
||||
{
|
||||
const [a, b] = S.drive;
|
||||
const layer = id => scene(id).start;
|
||||
const L = { hats: layer('shots'), claps: layer('products'), sixteenths: layer('forms'), lead: layer('consoles') };
|
||||
for (let t = a, k = 0; t < b - 0.01; t += STEP, k++) {
|
||||
const bar = Math.floor((t - a) / BAR), beat = k % 4;
|
||||
const [root, iv] = CH[PROG[bar % 4]];
|
||||
kick(t, beat === 0 ? 0.55 : 0.46);
|
||||
if (t >= L.claps && (beat === 1 || beat === 3)) clap(t, 0.11);
|
||||
if (t >= L.hats) hat(t + STEP / 2, 0.028, beat === 3);
|
||||
if (t >= L.sixteenths) { hat(t + STEP / 4, 0.014); hat(t + STEP * 3 / 4, 0.014); }
|
||||
// the bass: root eighths, the fifth on the last one of the bar
|
||||
const bm = root - 12 + (root < 46 ? 12 : 0);
|
||||
bass(t, bm, STEP / 2 * 0.85, 0.1);
|
||||
bass(t + STEP / 2, beat === 3 ? bm + 7 : bm, STEP / 2 * 0.85, 0.085);
|
||||
// the arpeggio: sixteenths over the chord, brighter layer by layer
|
||||
const tones = iv.slice(0, 4).map(x => root + 24 + x);
|
||||
for (let j = 0; j < 4; j++) {
|
||||
const m = tones[(k * 4 + j) % tones.length];
|
||||
pluck(t + j * STEP / 4, m, (t >= L.lead ? 0.034 : 0.026) * (j === 0 ? 1.2 : 1), (j % 2 ? 0.3 : -0.3), 0.35, drive);
|
||||
}
|
||||
if (beat === 0) pad(t, t + BAR - 0.05, [root + 12, root + 12 + iv[1], root + 12 + iv[2]], t >= L.lead ? 0.022 : 0.016, t >= L.lead ? 2600 : 1600, 0.05, 0.12, drive);
|
||||
if (beat === 0 && t >= L.lead) pluck(t, root + 36 + iv[2], 0.03, 0, 1.4, drive);
|
||||
}
|
||||
// a cymbal-like swell into each montage cut, a big one into the drop and into the stop
|
||||
for (const id of MONTAGE.slice(1).concat(['scan'])) riser(scene(id).start, 0.5, 0.03, 2000, 9000, drive);
|
||||
riser(b, 2, 0.06, 500, 8000, drive);
|
||||
}
|
||||
|
||||
// ---------- stop: one button is yours ----------
|
||||
{
|
||||
const [a, b] = S.stop;
|
||||
// the last hit rings out into the room, then only a quiet low pulse, slower than the beat
|
||||
bell(a, 74 + 12, 0.035);
|
||||
for (let t = a + 1.0; t < b - 0.3; t += 1.0) {
|
||||
const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.value = midi(38);
|
||||
const v = ctx.createGain(); env(v, t, 0.02, 0.025 * Math.min(1, (t - a) / 3), 0.5); o.connect(v); v.connect(master); o.start(t); o.stop(t + 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- resolve: the click, waiting for review, the end card ----------
|
||||
{
|
||||
const [a, b] = S.resolve;
|
||||
click(a);
|
||||
const [r, iv] = CH.Dmaj9;
|
||||
pad(a + 0.08, b + 0.6, [r - 12, r, r + iv[1], r + iv[2], r + iv[3], r + iv[4]], 0.048, 1600, 0.9, 1.0);
|
||||
pad(a + 0.08, b + 0.6, [r - 24], 0.03, 200, 1.2, 1.0);
|
||||
bell(a + 0.3, r + 24, 0.06);
|
||||
bell(a + 0.65, r + 24 + 7, 0.045, 0.2);
|
||||
bell(a + 1.0, r + 24 + 14, 0.035, -0.2);
|
||||
// a slow arpeggio returns (the calm of the opening, now open)
|
||||
const tones = [74, 78, 81, 85, 86, 85, 81, 78];
|
||||
for (let t = a + 1.5, k = 0; t < b - 1.2; t += STEP, k++) pluck(t, tones[k % tones.length], 0.03, k % 2 ? 0.25 : -0.25, 1.4);
|
||||
// the end card: one more bell as the logo lands
|
||||
const e = scene('end').start;
|
||||
bell(e + 0.3, r + 24, 0.05);
|
||||
bell(e + 0.55, r + 31, 0.035, 0.2);
|
||||
}
|
||||
|
||||
// ---------- soft accents on the story cuts ----------
|
||||
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);
|
||||
}
|
||||
|
||||
// ---------- UI sounds ----------
|
||||
for (const x of ticks()) {
|
||||
if (x.kind === 'tiny') tick(x.t, 0.022, 2800);
|
||||
else if (x.kind === 'soft') { tick(x.t, 0.03, 2400); tone(x.t, 86, 0.012, 0.18); }
|
||||
else if (x.kind === 'mail') { tone(x.t, 81, 0.04, 0.2); tone(x.t + 0.09, 88, 0.035, 0.4); }
|
||||
else if (x.kind === 'low') { tone(x.t, 50, 0.07, 0.5, 'triangle'); tone(x.t, 57, 0.02, 0.3); }
|
||||
else if (x.kind === 'ask') { tone(x.t, 83, 0.035, 0.3); tone(x.t + 0.14, 81, 0.03, 0.45); }
|
||||
else if (x.kind === 'good') { const m = [74, 78, 81][x.step] ?? 81; tone(x.t, m, 0.04, 0.6, 'sine', -0.2 + 0.2 * x.step); tone(x.t, m + 12, 0.012, 0.4); }
|
||||
if (x.kind === 'tiny') tick(x.t, 0.02, 2800);
|
||||
else if (x.kind === 'thud') thud(x.t, 0.12 + x.i * 0.015);
|
||||
else if (x.kind === 'low') tone(x.t, 38, 0.05, 0.9, 'triangle');
|
||||
else if (x.kind === 'send') { tone(x.t, 81, 0.03, 0.3); tone(x.t + 0.1, 86, 0.025, 0.6); }
|
||||
else if (x.kind === 'good') { const m = [74, 76, 78, 81, 83][x.step] ?? 86; tone(x.t, m + 12, 0.045, 0.5, 'sine', -0.3 + 0.15 * x.step); tone(x.t, m + 24, 0.012, 0.3); }
|
||||
else if (x.kind === 'clean') { tone(x.t, 86, 0.04, 0.8); tone(x.t + 0.08, 90, 0.03, 0.9); tone(x.t + 0.16, 93, 0.025, 1.0); }
|
||||
}
|
||||
for (const t of keyTimes()) key(t);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue