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>
184 lines
5.8 KiB
TypeScript
184 lines
5.8 KiB
TypeScript
/**
|
||
* The harmony and the tunes, as data.
|
||
*
|
||
* Key: D major. The loop is 王道進行, the J-pop "royal road" — IV△7 – V7 –
|
||
* iii7 – vi (G△7 – A7 – F#m7 – Bm7), one chord per bar — a nod to Tokunia's
|
||
* Japanese name. The night breakdown sits on Bm and G△7, the build climbs
|
||
* Em7 – F#m7 – G△7 – A9sus4 → A, and the film ends on a held D(add9).
|
||
*
|
||
* The HOOK is built from E-F#-A-B-C# — the 陽音階 (yo scale) on E — so it
|
||
* sits on every chord of the loop: the same two bars over G–A and F#m–Bm.
|
||
*/
|
||
|
||
export type ChordName = "G" | "A" | "F#m" | "Bm" | "Em" | "Asus" | "D";
|
||
|
||
/** MIDI notes per chord: the sub root, the chord voicing (pads + stabs), the arp's five tones. */
|
||
export const VOICING: Record<ChordName, { root: number; chord: number[]; arp: number[] }> = {
|
||
G: { root: 31, chord: [55, 59, 62, 66, 69], arp: [67, 71, 74, 78, 81] }, // G△9
|
||
A: { root: 33, chord: [57, 61, 64, 67, 71], arp: [69, 73, 76, 79, 83] }, // A9
|
||
"F#m": { root: 30, chord: [54, 57, 61, 64, 69], arp: [66, 69, 73, 76, 78] }, // F#m7
|
||
Bm: { root: 35, chord: [54, 59, 62, 66, 69], arp: [66, 71, 74, 78, 81] }, // Bm7
|
||
Em: { root: 28, chord: [52, 55, 59, 62, 66], arp: [64, 67, 71, 74, 78] }, // Em9
|
||
Asus: { root: 33, chord: [57, 62, 64, 67, 71], arp: [69, 74, 76, 79, 81] }, // A9sus4
|
||
D: { root: 38, chord: [50, 57, 62, 64, 66, 69], arp: [74, 76, 78, 81, 86] }, // D(add9)
|
||
};
|
||
|
||
/** The night's open, low voicings (bars 31–34). */
|
||
export const NIGHT_VOICING: Partial<Record<ChordName, number[]>> = {
|
||
Bm: [47, 54, 62, 69, 73], // Bm9: B2 F#3 D4 A4 C#5
|
||
G: [43, 50, 59, 66, 69], // G△9: G2 D3 B3 F#4 A4
|
||
};
|
||
|
||
const LOOP: ChordName[] = ["G", "A", "F#m", "Bm"];
|
||
|
||
/** Where the royal-road loop (re)starts on G. */
|
||
const LOOP_STARTS = [1, 5, 39, 45, 51, 59];
|
||
|
||
/** The chord sounding at `bar`, `beat` (1-indexed). */
|
||
export function chordAt(bar: number, beat = 1): ChordName {
|
||
if (bar >= 63) return "D";
|
||
if (bar >= 31 && bar <= 32) return "Bm";
|
||
if (bar >= 33 && bar <= 34) return "G";
|
||
if (bar === 35) return "Em";
|
||
if (bar === 36) return "F#m";
|
||
if (bar === 37) return "G";
|
||
if (bar === 38) return beat < 3 ? "Asus" : "A";
|
||
// the last bar of drop 3 leans on the dominant for the final cadence
|
||
if (bar === 62 && beat >= 3) return "Asus";
|
||
const start = Math.max(...LOOP_STARTS.filter((s) => s <= bar));
|
||
return LOOP[(bar - start) % 4];
|
||
}
|
||
|
||
/** A melody note: start and length in 16ths from the phrase's downbeat, MIDI pitch. */
|
||
export type Note = [start: number, len: number, midi: number];
|
||
|
||
const F5 = 78; // F#5
|
||
const A5 = 81;
|
||
const B5 = 83;
|
||
const E5 = 76;
|
||
const D5 = 74;
|
||
const CS5 = 73; // C#5
|
||
|
||
/** Bar 1 of the hook — the 3+3+2 / 3+3+2 call over IV△7 (or iii7). */
|
||
const HOOK_CALL: Note[] = [
|
||
[0, 3, F5],
|
||
[3, 3, A5],
|
||
[6, 2, F5],
|
||
[8, 3, E5],
|
||
[11, 3, F5],
|
||
[14, 2, A5],
|
||
];
|
||
|
||
/** THE HOOK — two bars, the film's theme: call, then the answer falling to E. */
|
||
export const HOOK: Note[] = [...HOOK_CALL, [16, 3, B5], [19, 3, A5], [22, 2, F5], [24, 6, E5]];
|
||
|
||
/** The hook's second statement in a 4-bar phrase: same call, the answer resolves to D. */
|
||
export const HOOK_ANSWER: Note[] = [
|
||
...HOOK_CALL,
|
||
[16, 3, B5],
|
||
[19, 3, A5],
|
||
[22, 2, F5],
|
||
[24, 2, E5],
|
||
[26, 2, F5],
|
||
[28, 4, D5],
|
||
];
|
||
|
||
/**
|
||
* The counter-line (4 bars over G–A–F#m–Bm): the hook in augmentation — its
|
||
* notes at half speed — so the chapters between the drops still hum the theme.
|
||
*/
|
||
export const COUNTER: Note[] = [
|
||
[0, 6, F5],
|
||
[6, 6, A5],
|
||
[12, 4, F5],
|
||
[16, 6, E5],
|
||
[22, 6, F5],
|
||
[28, 4, A5],
|
||
[32, 6, B5], // a 4–3 suspension over F#m7…
|
||
[38, 6, A5], // …resolving
|
||
[44, 4, F5],
|
||
[48, 8, D5],
|
||
[56, 4, CS5],
|
||
[60, 4, E5],
|
||
];
|
||
|
||
/** The arp: 16 steps into the chord's five arp tones — 3+3+3+3+4, syncopated against the kick. */
|
||
export const ARP_STEPS = [0, 2, 4, 1, 3, 4, 0, 2, 4, 1, 3, 4, 0, 2, 3, 4];
|
||
/** Accents on each group's first note. */
|
||
export const ARP_VEL = [1, 0.62, 0.7, 0.92, 0.62, 0.7, 0.95, 0.62, 0.7, 0.92, 0.62, 0.7, 0.95, 0.62, 0.7, 0.66];
|
||
|
||
/** Mid-bass rhythms: [16th position, length in 16ths, octave offset in semitones]. */
|
||
export const BASS_RHYTHM = {
|
||
/** classic house offbeats with an octave pickup */
|
||
offbeat: [
|
||
[2, 2, 0],
|
||
[6, 2, 0],
|
||
[10, 2, 0],
|
||
[14, 1, 0],
|
||
[15, 1, 12],
|
||
],
|
||
/** galloping offbeats — the drive chapters and the drops */
|
||
gallop: [
|
||
[2, 1, 0],
|
||
[3, 1, 0],
|
||
[6, 1, 0],
|
||
[7, 1, 12],
|
||
[10, 1, 0],
|
||
[11, 1, 0],
|
||
[14, 1, 0],
|
||
[15, 1, 12],
|
||
],
|
||
/** octave bounce under the notifications */
|
||
bounce: [
|
||
[2, 2, 0],
|
||
[6, 2, 12],
|
||
[10, 2, 0],
|
||
[14, 2, 12],
|
||
],
|
||
/** straight eighths for the last two bars of the build */
|
||
build: [
|
||
[0, 2, 0],
|
||
[2, 2, 0],
|
||
[4, 2, 0],
|
||
[6, 2, 0],
|
||
[8, 2, 0],
|
||
[10, 2, 0],
|
||
[12, 2, 0],
|
||
[14, 2, 12],
|
||
],
|
||
} satisfies Record<string, [number, number, number][]>;
|
||
|
||
/** Chord-stab rhythms (16th positions). */
|
||
export const STAB_RHYTHM = {
|
||
/** the drops: 3+3+4+3+3, locked to the hook's syncopation */
|
||
drop: [0, 3, 6, 10, 13],
|
||
/** the grooves: on the offbeats, between the kicks */
|
||
offbeat: [2, 6, 10, 14],
|
||
};
|
||
|
||
/**
|
||
* The notification bells, one per score PINGS entry (same order). A second
|
||
* pitch is a grace note a 16th later: the "ask" rises, the "answer" settles.
|
||
* The cascade climbs the scale to C#7, the dominant's third, into the dropout.
|
||
*/
|
||
export const PING_NOTES: (number | [number, number])[] = [
|
||
81, // 25.1 A5
|
||
86, // 25.3 D6
|
||
85, // 26.1 C#6
|
||
88, // 26.3 E6
|
||
[83, 88], // 27.1 the ask: B5 → E6
|
||
[90, 86], // 28.1 the answer: F#6 → D6
|
||
78, // 29.1 cascade: F#5
|
||
81, // 29.2 A5
|
||
83, // 29.3 B5
|
||
86, // 29.4 D6
|
||
88, // 30.1 E6
|
||
90, // 30.1+ F#6
|
||
93, // 30.2 A6
|
||
95, // 30.2+ B6
|
||
97, // 30.3 C#7
|
||
];
|
||
|
||
/** The cron chimes (CHIMES, same order): the hook's first four notes, slowed to a crawl. */
|
||
export const CHIME_NOTES = [90, 93, 90, 88]; // F#6 A6 F#6 E6
|