music/ renders public/audio/score.wav (24-bit 48 kHz, exactly 120 s) with plain TypeScript DSP in Node (`npm run music`): D major on the 王道進行 "royal road" progression (G△9 – A9 – F#m7 – Bm7), a two-bar hook in the yo scale over the three drops, sidechained supersaws, a pluck arpeggio, FM bells on the notification pings and glass chimes for the cron jobs. Every drum hit is scheduled from src/score.ts, so the kicks sit within 0.2 ms of the grid the picture pulses on. check-music.mjs re-measures loudness (−15 LUFS, −1.3 dBTP), per-chapter levels and kick alignment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
439 lines
18 KiB
TypeScript
439 lines
18 KiB
TypeScript
/**
|
||
* The Tokunia score: 120 s at 128 BPM in D major.
|
||
*
|
||
* Two halves meet here:
|
||
* - the grid-locked half (stems.ts): drums, crashes, impacts, risers,
|
||
* downlifters, pings and chimes, mixed sample-exactly from src/score.ts;
|
||
* - the played half (below): supersaw pads and stabs, a pluck arp, sub and
|
||
* mid bass ducked on every kick, the HOOK and its counter-line (synth.ts),
|
||
* through a plate, a hall and a dotted-8th ping-pong delay (mixer.ts).
|
||
* master.ts then brings the mix to −15 LUFS / −1.3 dBTP.
|
||
*
|
||
* Everything is plain TypeScript DSP on whole-film buffers, rendered by
|
||
* music/render-music.mjs in Node. It is deterministic (seeded noise, no
|
||
* Math.random), so two renders are bit-identical, and bar 1 beat 1 is
|
||
* sample 0.
|
||
*/
|
||
import { RISERS } from "../src/score";
|
||
import { at, BEAT, SIXTEENTH } from "../src/timeline";
|
||
import { SR, Stem, db, mtof, type Stereo } from "./dsp";
|
||
import { reverbIR } from "./fx";
|
||
import {
|
||
ARP_STEPS,
|
||
ARP_VEL,
|
||
BASS_RHYTHM,
|
||
COUNTER,
|
||
HOOK,
|
||
HOOK_ANSWER,
|
||
NIGHT_VOICING,
|
||
STAB_RHYTHM,
|
||
VOICING,
|
||
chordAt,
|
||
type ChordName,
|
||
type Note,
|
||
} from "./harmony";
|
||
import { master, type MasterReport } from "./master";
|
||
import { convolve, curve, duck, eq, fader, filter, gain, mixInto, pingPong, sendInto, type Point } from "./mixer";
|
||
import { gridStems, LENGTH } from "./stems";
|
||
import { Synth, glide, type Env, type VoiceSpec } from "./synth";
|
||
|
||
export { wav } from "./master";
|
||
|
||
// ─── helpers ──────────────────────────────────────────────────────────────
|
||
|
||
/** Seconds of 16th `pos` in `bar`. */
|
||
const t16 = (bar: number, pos: number) => at(bar) + pos * SIXTEENTH;
|
||
const hz = (notes: number[]) => notes.map(mtof);
|
||
const within = (bar: number, ranges: [number, number][]) => ranges.some(([a, b]) => bar >= a && bar <= b);
|
||
/** No note may start in bar 30's dropped fourth beat or in bar 38's silent last 8th. */
|
||
const muted = (bar: number, pos: number) => (bar === 30 && pos >= 12) || (bar === 38 && pos >= 14);
|
||
const OFF = -Infinity;
|
||
/** The drum stems' level in the mix (dB). */
|
||
const DRUMS = -3;
|
||
|
||
// ─── the instruments ──────────────────────────────────────────────────────
|
||
|
||
const saw = (count: number, spread: number, width: number, env: Env): VoiceSpec => ({ wave: "saw", unison: { count, spread, width }, env });
|
||
const LEAD_ENV: Env = { attack: 0.004, decay: 0.3, sustain: 0.55, release: 0.2 };
|
||
/** held hook notes start to sing after ~180 ms */
|
||
const LEAD_VIBRATO = { cents: 14, rate: 5.6, delay: 0.18 };
|
||
|
||
/** Every instrument and its level (dB) into its bus. Seeds keep the unison phases fixed between renders. */
|
||
function instruments() {
|
||
const make = (spec: VoiceSpec, volume: number, seed: number) => ({ synth: new Synth(spec, seed), volume });
|
||
return {
|
||
pad: make(saw(7, 42, 0.85, { attack: 0.16, decay: 0.6, sustain: 0.8, release: 0.7 }), -10, 1),
|
||
stab: make(saw(7, 44, 0.9, { attack: 0.002, decay: 0.24, sustain: 0.22, release: 0.16 }), -4, 2),
|
||
stabHigh: make(saw(5, 30, 1, { attack: 0.002, decay: 0.2, sustain: 0.18, release: 0.14 }), -11, 16),
|
||
night: make(saw(5, 26, 0.7, { attack: 1.3, decay: 1, sustain: 0.85, release: 2.4 }), -9, 3),
|
||
ring: make(saw(7, 48, 1, { attack: 0.003, decay: 2.6, sustain: 0, release: 0.6 }), -8, 4),
|
||
arp: make(
|
||
{
|
||
...saw(2, 12, 0.5, { attack: 0.0015, decay: 0.2, sustain: 0, release: 0.08 }),
|
||
filter: { base: 420, octaves: 4.2, env: { attack: 0.001, decay: 0.14, sustain: 0.08, release: 0.1 }, q: 1.2 },
|
||
},
|
||
1,
|
||
5,
|
||
),
|
||
sub: make({ wave: "sine", env: { attack: 0.006, decay: 0.05, sustain: 1, release: 0.08 } }, -6, 6),
|
||
drone: make({ wave: "sine", env: { attack: 1.4, decay: 0.1, sustain: 1, release: 1.8 } }, -22, 7),
|
||
lastSub: make({ wave: "sine", env: { attack: 0.004, decay: 2.8, sustain: 0, release: 0.4 } }, -8, 8),
|
||
mid: make(
|
||
{
|
||
...saw(2, 14, 0.15, { attack: 0.002, decay: 0.18, sustain: 0.5, release: 0.06 }),
|
||
filter: { base: 170, octaves: 3.4, env: { attack: 0.002, decay: 0.13, sustain: 0.25, release: 0.09 }, q: 2.2 },
|
||
},
|
||
-11,
|
||
9,
|
||
),
|
||
lead: make({ ...saw(3, 18, 0.35, LEAD_ENV), vibrato: LEAD_VIBRATO }, -3.5, 10),
|
||
leadPluck: make({ wave: "square", env: { attack: 0.001, decay: 0.11, sustain: 0, release: 0.06 } }, -12.5, 11),
|
||
leadBody: make({ wave: "triangle", env: LEAD_ENV, vibrato: LEAD_VIBRATO }, -10.5, 12),
|
||
leadOct: make({ ...saw(3, 24, 0.5, LEAD_ENV), vibrato: LEAD_VIBRATO }, -10.5, 13),
|
||
counter: make({ wave: "square", unison: { count: 2, spread: 10, width: 0.4 }, env: { attack: 0.03, decay: 0.5, sustain: 0.65, release: 0.35 }, vibrato: { cents: 10, rate: 5, delay: 0.25 } }, -7, 14),
|
||
counterOct: make({ wave: "triangle", env: { attack: 0.02, decay: 0.4, sustain: 0.6, release: 0.3 } }, -12, 15),
|
||
};
|
||
}
|
||
type Parts = { [K in keyof ReturnType<typeof instruments>]: Synth };
|
||
|
||
// ─── the arrangement ──────────────────────────────────────────────────────
|
||
|
||
/** Bars → chord spans: consecutive half-bars on the same chord merge into one. */
|
||
function spans(from: number, to: number): { t0: number; t1: number; chord: ChordName }[] {
|
||
const out: { t0: number; t1: number; chord: ChordName }[] = [];
|
||
for (let bar = from; bar <= to; bar++) {
|
||
for (const beat of [1, 3]) {
|
||
const chord = chordAt(bar, beat);
|
||
const t = at(bar, beat);
|
||
const last = out[out.length - 1];
|
||
if (last && last.chord === chord && Math.abs(last.t1 - t) < 1e-9) last.t1 = t + 2 * BEAT;
|
||
else out.push({ t0: t, t1: t + 2 * BEAT, chord });
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** The three drops, where the hook plays: HOOK, HOOK_ANSWER, HOOK… two bars each. */
|
||
const DROP_BARS: [number, number][] = [
|
||
[5, 8],
|
||
[39, 44],
|
||
[59, 62],
|
||
];
|
||
const GROOVE_BARS: [number, number][] = [
|
||
[9, 24],
|
||
[45, 58],
|
||
];
|
||
const PLAYING: [number, number][] = [
|
||
[1, 30],
|
||
[35, 62],
|
||
];
|
||
|
||
/** Play `notes` from `bar`'s downbeat; each bar's first note is accented. */
|
||
function melody(notes: Note[], bar: number, play: (t: number, dur: number, midi: number, v: number) => void) {
|
||
for (const [start, len, midi] of notes) play(at(bar) + start * SIXTEENTH, len * SIXTEENTH * 0.92, midi, start % 16 === 0 ? 0.95 : 0.82);
|
||
}
|
||
|
||
function arrange(p: Parts) {
|
||
// pads: every chord, overlapping into the next (the night has its own pad)
|
||
for (const [a, b] of PLAYING) for (const s of spans(a, b)) p.pad.chord(hz(VOICING[s.chord].chord), s.t0, s.t1 - s.t0 + 0.04, 0.75);
|
||
|
||
// stabs: syncopated in the drops, on the offbeats in the grooves
|
||
for (let bar = 5; bar <= 62; bar++) {
|
||
const drop = within(bar, DROP_BARS);
|
||
if (!drop && !within(bar, GROOVE_BARS)) continue;
|
||
for (const pos of drop ? STAB_RHYTHM.drop : STAB_RHYTHM.offbeat) {
|
||
const v = drop ? (pos === 0 ? 0.95 : pos === 6 || pos === 13 ? 0.88 : 0.8) : 0.62;
|
||
const chord = VOICING[chordAt(bar, Math.floor(pos / 4) + 1)].chord;
|
||
p.stab.chord(hz(chord), t16(bar, pos), (drop ? 1.7 : 1.3) * SIXTEENTH, v);
|
||
// the drops add the top three chord tones an octave up: the same stab, twice as wide and bright
|
||
if (drop) p.stabHigh.chord(hz(chord.slice(-3).map((m) => m + 12)), t16(bar, pos), 1.5 * SIXTEENTH, v);
|
||
}
|
||
}
|
||
// the build's stabs: 8ths in bar 37, 16ths in bar 38, swelling into the gap
|
||
for (let pos = 0; pos < 16; pos += 2) p.stab.chord(hz(VOICING.G.chord), t16(37, pos), 1.2 * SIXTEENTH, 0.5 + 0.15 * (pos / 14));
|
||
for (let pos = 0; pos < 14; pos++) p.stab.chord(hz(VOICING[chordAt(38, Math.floor(pos / 4) + 1)].chord), t16(38, pos), 0.9 * SIXTEENTH, 0.62 + 0.38 * (pos / 13));
|
||
|
||
// the night pad: Bm, then G△7, two bars each
|
||
for (const s of spans(31, 34)) p.night.chord(hz(NIGHT_VOICING[s.chord] ?? VOICING[s.chord].chord), s.t0, s.t1 - s.t0 + 0.3, 0.8);
|
||
|
||
// the last chord, struck on bar 63 and left to ring
|
||
p.ring.chord(hz(VOICING.D.chord), at(63), 2.6, 0.9);
|
||
|
||
// the arp: 16ths through every chord
|
||
for (let bar = 1; bar <= 62; bar++) {
|
||
if (!within(bar, PLAYING)) continue;
|
||
for (let pos = 0; pos < 16; pos++) {
|
||
if (muted(bar, pos)) continue;
|
||
const tones = VOICING[chordAt(bar, Math.floor(pos / 4) + 1)].arp;
|
||
p.arp.note(mtof(tones[ARP_STEPS[pos]]), t16(bar, pos), 0.9 * SIXTEENTH, ARP_VEL[pos]);
|
||
}
|
||
}
|
||
|
||
// sub: the chord root, held legato; a slow drone through the night; one last D under the final chord
|
||
for (const [a, b] of [
|
||
[5, 30],
|
||
[35, 62],
|
||
]) {
|
||
for (const s of spans(a, b)) {
|
||
const end = s.t1 > at(30, 4) && s.t0 < at(31) ? at(30, 4) : s.t1;
|
||
p.sub.note(mtof(VOICING[s.chord].root), s.t0, end - s.t0 + (end === s.t1 ? 0.02 : 0), 0.9);
|
||
}
|
||
}
|
||
for (const s of spans(31, 34)) p.drone.note(mtof(VOICING[s.chord].root + 12), s.t0, s.t1 - s.t0 + 0.02, 0.7);
|
||
p.lastSub.note(mtof(VOICING.D.root), at(63), 3, 0.9);
|
||
|
||
// mid bass
|
||
const bassRhythm = (bar: number) => {
|
||
if (within(bar, [[25, 30]])) return BASS_RHYTHM.bounce;
|
||
if (within(bar, [[37, 38]])) return BASS_RHYTHM.build;
|
||
if (within(bar, [
|
||
[9, 16],
|
||
[45, 50],
|
||
]))
|
||
return BASS_RHYTHM.offbeat;
|
||
if (within(bar, [
|
||
[5, 8],
|
||
[17, 24],
|
||
[39, 44],
|
||
[51, 62],
|
||
]))
|
||
return BASS_RHYTHM.gallop;
|
||
return null;
|
||
};
|
||
for (let bar = 5; bar <= 62; bar++) {
|
||
for (const [pos, len, oct] of bassRhythm(bar) ?? []) {
|
||
if (muted(bar, pos)) continue;
|
||
const root = VOICING[chordAt(bar, Math.floor(pos / 4) + 1)].root + 12;
|
||
p.mid.note(mtof(root + oct), t16(bar, pos), len * SIXTEENTH * 0.85, oct ? 0.75 : 0.9);
|
||
}
|
||
}
|
||
|
||
// the hook, in the three drops; the octave layer only in drop 3
|
||
for (const [a, b] of DROP_BARS) {
|
||
for (let bar = a, k = 0; bar < b; bar += 2, k++) {
|
||
melody(k % 2 ? HOOK_ANSWER : HOOK, bar, (t, d, m, v) => {
|
||
p.lead.note(mtof(m), t, d, v);
|
||
p.leadPluck.note(mtof(m), t, d, v);
|
||
p.leadBody.note(mtof(m - 12), t, d, v);
|
||
if (a === 59) p.leadOct.note(mtof(m + 12), t, d, v);
|
||
});
|
||
}
|
||
}
|
||
|
||
// the counter-line: subdued at the end of chapter 01, full in 02, back in 06 and the montage
|
||
const counterAt: [number, number, number][] = [
|
||
[13, 16, 0.5],
|
||
[17, 24, 0.85],
|
||
[45, 50, 0.7],
|
||
[51, 58, 0.9],
|
||
];
|
||
for (const [a, b, vel] of counterAt) {
|
||
for (let bar = a; bar <= b; bar += 4) {
|
||
const notes = COUNTER.filter(([start]) => start < (b - bar + 1) * 16);
|
||
melody(notes, bar, (t, d, m, v) => {
|
||
p.counter.note(mtof(m), t, d, v * vel);
|
||
if (a === 51) p.counterOct.note(mtof(m + 12), t, d, v * vel);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── the mix ──────────────────────────────────────────────────────────────
|
||
|
||
export interface RenderOptions {
|
||
/** keep only these buses (and their reverb/delay): a balancing aid */
|
||
solo?: string[];
|
||
/** skip the master stage: the raw mix */
|
||
raw?: boolean;
|
||
/** also return each bus as it enters the mix (post-fader), and each return: balancing aid */
|
||
stems?: boolean;
|
||
}
|
||
|
||
export interface RenderResult {
|
||
L: Float32Array;
|
||
R: Float32Array;
|
||
master: MasterReport | null;
|
||
seconds: Record<string, number>;
|
||
stems?: Record<string, Stereo>;
|
||
}
|
||
|
||
/** Faders (dB) and filter cutoffs (Hz) by bar position. Bar 30's fourth beat and bar 38's last 8th drop out. */
|
||
const AUTOMATION: Record<string, Point[]> = {
|
||
padLevel: [[1, -4], [3, -2, "ramp"], [4.99, 0, "ramp"], [5, 0], [9, -3], [17, -1], [25, -5], [30.75, OFF], [35, -9], [38.85, -1, "ramp"], [38.875, OFF], [39, 1], [45, -3], [51, -1], [59, 1.5], [63, OFF]],
|
||
padCut: [[1, 650], [3, 1300, "ramp"], [4.99, 5500, "ramp"], [5, 6000], [9, 3600], [17, 4600], [25, 2100], [35, 800], [38.875, 7000, "ramp"], [39, 6500], [45, 3600], [51, 5000], [59, 7000]],
|
||
stabLevel: [[1, OFF], [5, 0], [9, -6], [17, -3], [25, OFF], [37, -7], [38.85, -2, "ramp"], [38.875, OFF], [39, 2], [45, -5], [51, -2], [59, 2.5], [63, OFF]],
|
||
stabCut: [[1, 3200], [5, 7500], [9, 3200], [17, 4500], [25, 3000], [37, 1400], [38.875, 8000, "ramp"], [39, 7500], [45, 3400], [51, 4600], [59, 8500]],
|
||
nightLevel: [[1, OFF], [31, -8], [35, -10], [36, OFF, "ramp"]],
|
||
nightCut: [[1, 600], [31, 600], [34.99, 1100, "ramp"]],
|
||
arpLevel: [[1, -9], [3, -6, "ramp"], [4.99, -3, "ramp"], [5, -9], [9, -4], [17, -3], [25, -9], [30.75, OFF], [35, -15], [38.85, -3, "ramp"], [38.875, OFF], [39, -8], [45, -4], [51, -3], [59, -7], [63, OFF]],
|
||
arpCut: [[1, 500], [3, 1100, "ramp"], [4.99, 7000, "ramp"], [5, 5200], [9, 4600], [17, 9000], [25, 2200], [35, 600], [38.875, 9000, "ramp"], [39, 5500], [45, 5000], [51, 9000], [59, 6000]],
|
||
bassLevel: [[1, 0], [30.75, OFF], [31, 0], [38.875, OFF], [39, 0]],
|
||
leadLevel: [[1, 0], [59, 1]],
|
||
leadCut: [[1, 6000], [39, 7000], [59, 8200]],
|
||
counterLevel: [[1, -7], [17, 0], [45, -3], [51, 0]],
|
||
};
|
||
|
||
export function renderScore(opts: RenderOptions = {}): RenderResult {
|
||
const seconds: Record<string, number> = {};
|
||
let clock = performance.now();
|
||
const lap = (name: string) => {
|
||
const now = performance.now();
|
||
seconds[name] = (now - clock) / 1000;
|
||
clock = now;
|
||
};
|
||
const N = LENGTH;
|
||
const audible = (bus: string) => !opts.solo || opts.solo.includes(bus);
|
||
const level = (name: string) => curve(AUTOMATION[name], N, fader);
|
||
const cutoff = (name: string) => curve(AUTOMATION[name], N);
|
||
|
||
const grid = gridStems();
|
||
lap("grid");
|
||
|
||
const mix = new Stem(N);
|
||
const sends = { plate: new Float32Array(N), hall: new Float32Array(N), delay: new Float32Array(N) };
|
||
type Sends = Partial<Record<keyof typeof sends, number>>;
|
||
const stems: Record<string, Stereo> = {};
|
||
const keep = (name: string, s: Stereo) => {
|
||
if (!opts.stems) return;
|
||
if (stems[name]) mixInto(stems[name], s);
|
||
else stems[name] = { L: s.L.slice(), R: s.R.slice() };
|
||
};
|
||
const out = (bus: string, s: Stereo, sendDb: Sends = {}, stem = bus) => {
|
||
if (!audible(bus)) return;
|
||
keep(stem, s);
|
||
mixInto(mix, s);
|
||
for (const [k, v] of Object.entries(sendDb) as [keyof typeof sends, number][]) sendInto(sends[k], s, db(v));
|
||
};
|
||
|
||
// ── grid stems, plus the synth risers (a saw stack climbing 2–3 octaves into each drop) ──
|
||
for (const st of [grid.kick, grid.hats, grid.snap, grid.crash]) gain(st, db(DRUMS));
|
||
out("drums", grid.kick, {}, "kick");
|
||
out("drums", grid.hats, {}, "hats");
|
||
out("drums", grid.snap, { plate: -15 }, "snap");
|
||
out("drums", grid.crash, { hall: -20 }, "crash");
|
||
const risers = new Stem(N);
|
||
for (const r of RISERS) {
|
||
const t0 = at(r.fromBar);
|
||
const n = Math.round((at(r.toBar) - t0) * SR);
|
||
const g = glide(n, mtof(45), mtof(45 + 12 * (r.toBar - r.fromBar > 2 ? 3 : 2)), 5, 40, 90 + r.fromBar);
|
||
for (let i = 0; i < n; i++) {
|
||
const amp = 10 ** ((-60 + 60 * (i / n) ** 0.6) / 20) * Math.min(1, (n - i) / (0.004 * SR));
|
||
g.L[i] *= amp;
|
||
g.R[i] *= amp;
|
||
}
|
||
risers.add(t0, g, db(-17));
|
||
}
|
||
filter(risers, "hp", 300, 0.7);
|
||
mixInto(grid.fx, risers);
|
||
out("fx", grid.fx, { hall: -14 });
|
||
out("bells", grid.bells, { delay: -9, plate: -13, hall: -12 });
|
||
lap("stems");
|
||
|
||
// ── the played layers ──
|
||
const inst = instruments();
|
||
const parts = Object.fromEntries(Object.entries(inst).map(([k, v]) => [k, v.synth])) as Parts;
|
||
arrange(parts);
|
||
const render = (...names: (keyof Parts)[]) => {
|
||
const s = new Stem(N);
|
||
for (const k of names) {
|
||
const part = new Stem(N);
|
||
inst[k].synth.render(part);
|
||
mixInto(s, part, db(inst[k].volume));
|
||
}
|
||
return s;
|
||
};
|
||
|
||
if (audible("pad")) {
|
||
const s = render("pad");
|
||
filter(s, "hp", 180);
|
||
eq(s, 320, 0.9, -3); // the mud: supersaws pile up at 300 Hz
|
||
filter(s, "lp", cutoff("padCut"), 0.5, 4);
|
||
duck(s, grid.duckLong, 0.72);
|
||
gain(s, level("padLevel"));
|
||
out("pad", s, { hall: -10 });
|
||
}
|
||
if (audible("stab")) {
|
||
const s = render("stab", "stabHigh");
|
||
filter(s, "hp", 210);
|
||
eq(s, 320, 0.9, -3);
|
||
filter(s, "lp", cutoff("stabCut"), 0.9, 4);
|
||
duck(s, grid.duckLong, 0.5);
|
||
gain(s, level("stabLevel"));
|
||
out("stab", s, { plate: -14, hall: -16 });
|
||
}
|
||
if (audible("night")) {
|
||
const s = render("night");
|
||
filter(s, "hp", 120);
|
||
filter(s, "lp", cutoff("nightCut"), 0.4);
|
||
gain(s, level("nightLevel"));
|
||
out("night", s, { hall: -6 });
|
||
}
|
||
if (audible("ring")) {
|
||
const s = render("ring");
|
||
filter(s, "hp", 150);
|
||
filter(s, "lp", 5200, 0.5);
|
||
out("ring", s, { hall: -4 });
|
||
}
|
||
if (audible("arp")) {
|
||
const s = render("arp");
|
||
filter(s, "hp", 260);
|
||
filter(s, "lp", cutoff("arpCut"), 0.6);
|
||
duck(s, grid.duckLong, 0.3);
|
||
gain(s, level("arpLevel"));
|
||
out("arp", s, { delay: -8, plate: -14 });
|
||
}
|
||
if (audible("bass")) {
|
||
const mid = render("mid");
|
||
filter(mid, "hp", 75);
|
||
filter(mid, "lp", 2400);
|
||
const s = render("sub", "drone", "lastSub");
|
||
mixInto(s, mid);
|
||
duck(s, grid.duckShort, 0.93);
|
||
gain(s, level("bassLevel"));
|
||
out("bass", s);
|
||
}
|
||
if (audible("lead")) {
|
||
const s = render("lead", "leadPluck", "leadBody", "leadOct");
|
||
filter(s, "hp", 250);
|
||
filter(s, "lp", cutoff("leadCut"), 0.8);
|
||
duck(s, grid.duckLong, 0.15);
|
||
gain(s, level("leadLevel"));
|
||
out("lead", s, { delay: -8, hall: -13 });
|
||
}
|
||
if (audible("counter")) {
|
||
const s = render("counter", "counterOct");
|
||
filter(s, "hp", 230);
|
||
filter(s, "lp", 2600, 0.5);
|
||
duck(s, grid.duckLong, 0.25);
|
||
gain(s, level("counterLevel"));
|
||
out("counter", s, { delay: -9, hall: -11 });
|
||
}
|
||
lap("synths");
|
||
|
||
// ── returns: the dotted-8th ping-pong (which also feeds the hall), the plate, the hall ──
|
||
const delay = pingPong(sends.delay, Math.round(3 * SIXTEENTH * SR), 0.3);
|
||
filter(delay, "hp", 380);
|
||
filter(delay, "lp", 6000);
|
||
gain(delay, db(-3));
|
||
keep("delay", delay);
|
||
mixInto(mix, delay);
|
||
sendInto(sends.hall, delay, db(-14 + 3));
|
||
const plate = convolve(sends.plate, reverbIR(1.6, 0.012, 2600, 801));
|
||
filter(plate, "hp", 320);
|
||
filter(plate, "lp", 9000);
|
||
gain(plate, db(-2));
|
||
keep("plate", plate);
|
||
mixInto(mix, plate);
|
||
const hall = convolve(sends.hall, reverbIR(3.0, 0.028, 1500, 811));
|
||
filter(hall, "hp", 240);
|
||
filter(hall, "lp", 7000);
|
||
gain(hall, db(-2));
|
||
keep("hall", hall);
|
||
mixInto(mix, hall);
|
||
lap("returns");
|
||
|
||
const report = opts.raw ? null : master(mix.L, mix.R, { lufs: -15, ceiling: -1.3, fadeOut: 0.4 });
|
||
lap("master");
|
||
return { L: mix.L, R: mix.R, master: report, seconds, stems: opts.stems ? stems : undefined };
|
||
}
|