From a3b0d6f4386e63529bfb950afb84539d59b30124 Mon Sep 17 00:00:00 2001 From: Jeany Date: Sun, 25 Oct 2026 22:47:25 -0600 Subject: [PATCH] A kept function's errors: its own file's name for it, said at the function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit functionName read a node's text through the body being compiled, so a handler written in main.ts and kept by a library (Signal.on stores it in signals.ts) was named by a slice of the library's text at main.ts's offsets: "the function handed to ignal } from \"tri". The name is now read from the file the function is written in, and so is a parameter's. The check that a kept function gives back a number, a boolean, a text, a unit, a function or nothing is now said at the function itself, not where it is kept, which may be a library's line far from the code to change. signals.ts's header showed the very handler that is refused, `(wave) => setResources(…)`; it has braces now. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01U79fdupFuMLxYQmMJtffPN --- compiler/generated/stdlib.ts | 2 +- compiler/structured.ts | 17 +++++++++++------ stdlib/signals.ts | 2 +- tests/function-values.test.ts | 4 ++++ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/compiler/generated/stdlib.ts b/compiler/generated/stdlib.ts index 9aa321b..a658cf7 100644 --- a/compiler/generated/stdlib.ts +++ b/compiler/generated/stdlib.ts @@ -6,7 +6,7 @@ export const STDLIB_SOURCES: Readonly> = { "music": "/**\n * Music in a program: the standard library's \"trigscript/music\" (TrigScript 4).\n *\n * import { music } from \"trigscript/music\";\n * import pluck from \"./pluck.sound\";\n *\n * const lead = music.synth({ wave: \"square\", volume: 0.2 });\n * const bass = music.sampler(pluck, \"A3\");\n * const drums = music.kit({ k: kick, s: snare, h: hat });\n *\n * const song = music.song({ bpm: 120 },\n * music.track(lead, \"C5 - E5 - G5 - - . | A5 - G5 - E5 - - .\"),\n * music.track(bass, \"C3 - - - - - - - | F3 - - - - - - -\"),\n * music.track(drums, \"kh h sh h kh h sh h | kh h sh h kh kh sh h\"),\n * );\n *\n * program(async () => {\n * start(music.loop(song)); // the song again and again, beside the program\n * playWav(lead.note(\"C6\", 0.1)); // an instrument's note on its own, for a sound effect\n * });\n *\n * A pattern is steps, a word each, a quarter of a beat each by default: a note's name starts it (\"C4\", \"F#5\", \"Bb2\"),\n * several with + sound together (\"C4+E4+G4\"), \"-\" holds what sounds a step longer, \".\" is a rest, \"|\" is only for the\n * eye; \"@\" after a step's notes says how loud (\"C4@0.5\"). A kit's step is its sounds' letters (\"kh\": kick and hat).\n * Everything is worked out when the script is built: each note an instrument plays becomes a clip once (`trigscript`'s\n * Clip, put in the map once however often it plays), and the song a table of which clip starts on which frame, which\n * `play` reads while the game runs.\n *\n * A longer song is written in sections — parts named once, each section saying what they play in it, a pattern played\n * again until the section is full — and chords by name, their notes drawn by a template:\n *\n * const parts = { keys: music.part(music.presets.keys()), bass: music.part(music.presets.bass()), drums: music.part(music.drums()) };\n * const verse = music.section(4, {\n * keys: music.chords(\"Am F C G\", \"1+2+3 - . 1+2+3 . . 1+2+3 -\", { octave: 4, lead: true }),\n * bass: music.chords(\"Am F C G\", \"1 - . 1 . 1 3 -\", { octave: 2 }),\n * drums: music.beat({ k: [0, 3, 4], s: [2, 6], h: [0, 1, 2, 3, 4, 5, 6, 7] }, 8),\n * });\n * const song = music.arrange({ bpm: 120, steps: 2 }, parts, intro, verse, verse.with({ keys: null }), outro);\n *\n * Music that follows the game is a song of sections cut into bars and layers, a clip each, which the program steers\n * bar by bar — which section, which layers, a stinger on the next beat:\n *\n * const cut = music.stems(song, { layers: { drums: [\"drums\"], rest: [\"keys\", \"bass\"] } });\n * program(async () => {\n * const band = new Band(); // import { music, Band } from \"trigscript/music\"\n * start(music.conduct(cut, band));\n * if (danger) band.go(cut.section(\"chorus\")); // at the next bar line\n * if (cleared) band.sting(cut.stinger(\"crash\")); // on the next beat\n * });\n *\n * What the game does with it (the sound probe, 2026-10-19): a bar is a whole number of frames (42 ms on Fastest), so\n * the tempo is the nearest that fits — `song.bpm` says which; a sound starts within 21 ms of its frame; 32 sound at\n * once, and a song that would need more is refused; notes that overlap add, with no limiter, so keep the volumes down.\n * `render` mixes a song into one clip when the script is built instead: every note exactly in time, one sound playing,\n * nothing to change while it plays. A program plays to its own player; a program for each player plays to each.\n */\nimport { game, sleep, frames, playWav, tone, noise, fm, pluck, mixdown, together, memory, type Clip, type Notes } from \"trigscript\";\n\n/** Seconds a frame lasts on Fastest, the speed a map is played at. */\nconst FRAME = 0.042;\n/** Sounds the game plays at once. */\nconst VOICES = 32;\n\n/** What plays a pattern's notes: each note a clip, made when the script is built. */\nexport interface Instrument {\n readonly kind: \"notes\";\n /**\n * Note `name` (\"C4\", \"F#5\") lasting `seconds`, release and all: a clip to play on its own, too. `rate`: samples a\n * second, for an instrument that makes its notes (render asks for 44 100).\n */\n note(name: string, seconds: number, rate?: number): Clip;\n}\n\n/** What plays a pattern's letters: a clip a letter. */\nexport interface Kit {\n readonly kind: \"kit\";\n readonly hits: Readonly>;\n}\n\nconst SEMITONE: Readonly> = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };\n\n/** A note's number, 69 for A4: \"C4\" is 60, \"C#4\" 61, \"Db4\" 61. */\nfunction noteNumber(name: string): number {\n const m = /^([A-Ga-g])([#b]?)(-?\\d)$/.exec(name);\n if (!m) throw new Error(`A note is a name with its octave — \"C4\", \"F#5\", \"Bb2\" — not ${JSON.stringify(name)}.`);\n return 12 * (Number(m[3]) + 1) + SEMITONE[m[1].toLowerCase()] + (m[2] === \"#\" ? 1 : m[2] === \"b\" ? -1 : 0);\n}\n\n/** Notes kept as they are made, so a note played many times is the same clip (and the same file in the map). */\nfunction remembering(make: (name: string, seconds: number, rate?: number) => Clip): (name: string, seconds: number, rate?: number) => Clip {\n const made = new Map();\n return (name, seconds, rate) => {\n const key = `${name} ${seconds.toFixed(4)} ${rate ?? \"\"}`;\n let c = made.get(key);\n if (!c) { c = make(name, seconds, rate); made.set(key, c); }\n return c;\n };\n}\n\n/**\n * An instrument from a recording of one note, `root` (\"A3\"): every other note is it played faster or slower, as a\n * sampler does, cut to the note's length with a short release. Where the recording is shorter than a note, the note\n * is as long as the recording.\n */\nexport function sampler(clip: Clip, root: string = \"C4\", options: { readonly release?: number; readonly volume?: number } = {}): Instrument {\n const base = noteNumber(root), release = options.release ?? 0.05, volume = options.volume ?? 1;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds) => {\n const pitched = clip.pitch(noteNumber(name) - base).gain(volume);\n return pitched.trim(0, Math.min(pitched.seconds, seconds)).fade(0, Math.min(release, seconds / 2));\n }),\n });\n}\n\n/** An attack, a decay to the sustain level, and a release after the note's length, in seconds (sustain 0 … 1). */\nexport interface Shape { readonly attack?: number; readonly decay?: number; readonly sustain?: number; readonly release?: number }\n\n/** A shape's points for a note held `held` seconds, as levels 0 … 1: the attack from 0, the release to 0. */\nfunction shapePoints(shape: Required, held: number): [number, number][] {\n const { attack, decay, sustain, release } = shape;\n const h = Math.max(attack + decay, held);\n return [[0, 0], [attack, 1], [attack + decay, sustain], [h, sustain], [h + release, 0]];\n}\nconst full = (s: Shape | undefined, d: Required): Required => ({ attack: s?.attack ?? d.attack, decay: s?.decay ?? d.decay, sustain: s?.sustain ?? d.sustain, release: s?.release ?? d.release });\n\n/** A synth's filter: its type, the cutoff it rests at, its resonance, and how many octaves above it a note's envelope opens it. */\nexport interface SynthFilter {\n readonly type?: \"lowpass\" | \"highpass\" | \"bandpass\";\n readonly cutoff: number;\n readonly resonance?: number;\n /** Octaves above the cutoff at the envelope's top (0: the filter does not move). */\n readonly amount?: number;\n /** The filter's own attack, decay, sustain and release, as a note's loudness has. */\n readonly envelope?: Shape;\n}\n\nexport interface SynthOptions extends Shape {\n readonly wave?: \"sine\" | \"square\" | \"triangle\" | \"saw\";\n readonly volume?: number;\n /** Copies of each note (1 to 16), `detune` cents apart from lowest to highest, `width` 0 … 1 across left and right. */\n readonly voices?: number;\n readonly detune?: number;\n readonly width?: number;\n readonly filter?: SynthFilter;\n /** Saturation after the filter, dB of push (`Clip.drive`). */\n readonly drive?: number;\n /** Semitones each note slides up from (a negative number: from below) over `glideTime` seconds. */\n readonly glide?: number;\n readonly glideTime?: number;\n}\n\n/**\n * An instrument made of tones: `wave` (\"sine\", \"square\", \"triangle\", \"saw\") shaped by an attack, a decay to the\n * sustain level, and a release after the note's length — a stack of detuned copies with `voices`, through a filter\n * that opens and closes with each note with `filter`, saturated with `drive`.\n */\nexport function synth(options: SynthOptions = {}): Instrument {\n const wave = options.wave ?? \"square\", volume = options.volume ?? 0.2;\n const amp = full(options, { attack: 0.005, decay: 0.08, sustain: 0.7, release: 0.06 });\n const f = options.filter, fshape = full(f?.envelope, { attack: 0.005, decay: 0.25, sustain: 0.2, release: 0.2 });\n const voices = options.voices ?? 1, detune = options.detune ?? (voices > 1 ? 20 : 0), width = options.width ?? 0;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds, rate) => {\n const held = Math.max(amp.attack + amp.decay, seconds - amp.release), length = held + amp.release;\n const r = rate === undefined ? {} : { rate };\n const pitch = options.glide ? [[0, 440 * 2 ** ((noteNumber(name) + options.glide - 69) / 12)], [options.glideTime ?? 0.06, name]] as [number, number | string][] : name;\n let c = tone(pitch, length, { wave, volume, voices, detune, width, ...r });\n if (f) {\n const amount = f.amount ?? 0, base = f.cutoff;\n const cutoff = amount === 0 ? base : shapePoints(fshape, held).map(([t, level]) => [t, base * 2 ** (amount * level)] as [number, number]);\n c = c.filter(f.type ?? \"lowpass\", cutoff, { resonance: f.resonance ?? 0.1 });\n }\n if (options.drive) c = c.drive(options.drive);\n return c.envelope(...shapePoints(amp, held));\n }),\n });\n}\n\n/**\n * An FM instrument (`fm()`): `ratio` the modulator's pitch to the note's, `index` how bright a note starts, falling to\n * `index × brightness` over `bright` seconds — an electric piano (ratio 1), a bell (3.5), a bass (0.5).\n */\nexport function fmSynth(options: Shape & { readonly ratio?: number; readonly index?: number; readonly brightness?: number; readonly bright?: number; readonly feedback?: number; readonly volume?: number } = {}): Instrument {\n const amp = full(options, { attack: 0.002, decay: 1.2, sustain: 0.35, release: 0.3 });\n const ratio = options.ratio ?? 1, index = options.index ?? 3, after = index * (options.brightness ?? 0.2), bright = options.bright ?? 0.8, volume = options.volume ?? 0.25;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds, rate) => {\n const held = Math.max(amp.attack + amp.decay, seconds - amp.release), length = held + amp.release;\n return fm(name, length, { ratio, index: [[0, index], [bright, after]], feedback: options.feedback ?? 0, volume, ...(rate === undefined ? {} : { rate }) }).envelope(...shapePoints(amp, held));\n }),\n });\n}\n\n/** A plucked-string instrument (`pluck()`): `brightness` 0 … 1, `decay` seconds; a note rings out its decay unless `cut`. */\nexport function strings(options: { readonly brightness?: number; readonly decay?: number; readonly volume?: number; readonly cut?: boolean } = {}): Instrument {\n const brightness = options.brightness ?? 0.55, decay = options.decay ?? 1.5, volume = options.volume ?? 0.3;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds, rate) => {\n const length = options.cut ? seconds + 0.05 : Math.max(seconds + 0.05, Math.min(decay, 4));\n return pluck(name, length, { brightness, decay, volume, ...(rate === undefined ? {} : { rate }) }).fade(0, 0.05);\n }),\n });\n}\n\n/**\n * Instruments to start from, each a function that takes the same options as the instrument it is made with, over its\n * own: `music.presets.supersaw({ volume: 0.1 })`.\n */\nexport const presets = {\n /** Seven detuned saws through a filter that opens on each note: a trance lead, big chords. */\n supersaw: (o: SynthOptions = {}) => synth({ wave: \"saw\", voices: 7, detune: 28, width: 0.7, volume: 0.16, attack: 0.005, decay: 0.25, sustain: 0.8, release: 0.18, filter: { cutoff: 1800, amount: 2, resonance: 0.15, envelope: { decay: 0.35, sustain: 0.35 } }, ...o }),\n /** A slow, wide pad: five saws, the filter low and slowly opening. Give its track a chorus and the reverb. */\n pad: (o: SynthOptions = {}) => synth({ wave: \"saw\", voices: 5, detune: 18, width: 1, volume: 0.1, attack: 0.35, decay: 0.6, sustain: 0.85, release: 0.8, filter: { cutoff: 700, amount: 1.3, resonance: 0.1, envelope: { attack: 0.6, decay: 1.2, sustain: 0.5, release: 0.8 } }, ...o }),\n /** A round, punchy bass: two saws, the filter snapping shut after each note, a little drive. */\n bass: (o: SynthOptions = {}) => synth({ wave: \"saw\", voices: 2, detune: 8, volume: 0.32, attack: 0.002, decay: 0.12, sustain: 0.9, release: 0.04, filter: { cutoff: 180, amount: 2.6, resonance: 0.25, envelope: { decay: 0.18, sustain: 0.15, release: 0.05 } }, drive: 6, ...o }),\n /** A squelchy, resonant line (a 303's): one saw, the filter wide open on each note and shutting fast. */\n acid: (o: SynthOptions = {}) => synth({ wave: \"saw\", volume: 0.22, attack: 0.002, decay: 0.1, sustain: 0.8, release: 0.05, filter: { cutoff: 260, amount: 3.6, resonance: 0.82, envelope: { decay: 0.16, sustain: 0 } }, drive: 10, ...o }),\n /** A warm lead: two square waves a little apart, softened by a filter. */\n lead: (o: SynthOptions = {}) => synth({ wave: \"square\", voices: 2, detune: 12, width: 0.3, volume: 0.15, attack: 0.004, decay: 0.15, sustain: 0.7, release: 0.12, filter: { cutoff: 2200, amount: 1.2, resonance: 0.2, envelope: { decay: 0.25, sustain: 0.4 } }, ...o }),\n /** The old consoles' lead: a bare square wave. */\n chip: (o: SynthOptions = {}) => synth({ wave: \"square\", volume: 0.14, attack: 0.002, decay: 0.05, sustain: 0.8, release: 0.03, ...o }),\n /** An electric piano: FM at ratio 1, bright on the strike and mellowing. */\n keys: (o: Parameters[0] = {}) => fmSynth({ ratio: 1, index: 2.6, brightness: 0.25, bright: 0.6, volume: 0.22, decay: 1.4, sustain: 0.3, release: 0.35, ...o }),\n /** A bell: FM at ratio 3.5, long and shimmering. */\n bell: (o: Parameters[0] = {}) => fmSynth({ ratio: 3.5, index: 5, brightness: 0.08, bright: 1.6, volume: 0.16, decay: 2.5, sustain: 0, release: 1.2, ...o }),\n /** A plucked string, a little bright. */\n pluck: (o: Parameters[0] = {}) => strings({ brightness: 0.6, decay: 1.4, ...o }),\n};\n\n/**\n * A drum kit made from nothing, a letter a drum: k kick, s snare, c clap, h closed hat, o open hat, t tom, r rim, x crash. At\n * 44 100 Hz (`rate`), so the hats keep their top; `volume` 0 … 1 over the whole kit (1).\n */\nexport function drums(options: { readonly rate?: number; readonly volume?: number } = {}): Kit {\n const rate = options.rate ?? 44100, v = options.volume ?? 1;\n const metal = (seconds: number) => [263, 400, 421, 474, 587, 845].map((hz) => tone(hz * 2.6, seconds, { wave: \"square\", volume: 0.16, rate })).reduce((a, b) => a.mix(b));\n const kick = tone([[0, 170], [0.025, 85], [0.3, 46]], 0.42, { volume: 0.95, rate }).envelope([0, 1], [0.08, 0.85], [0.42, 0])\n .mix(noise(0.006, { volume: 0.35, rate }).filter(\"highpass\", 3000)).drive(5);\n const snare = tone([[0, 260], [0.04, 185]], 0.18, { wave: \"triangle\", volume: 0.6, rate }).envelope([0, 1], [0.15, 0])\n .mix(noise(0.28, { volume: 0.6, rate }).filter(\"highpass\", 1400).filter(\"peak\", 5000, { gain: 4 }).envelope([0, 1], [0.03, 0.55], [0.26, 0])).drive(4);\n const burst = (at: number) => ({ clip: noise(0.012, { volume: 0.6, rate }).envelope([0, 1], [0.012, 0.2]), at });\n const clap = mixdown([burst(0), burst(0.011), burst(0.023), { clip: noise(0.22, { volume: 0.5, rate }).envelope([0, 1], [0.22, 0]), at: 0.03 }], undefined, rate)\n .filter(\"bandpass\", 1300, { resonance: 0.35 }).gain(1.6);\n const hat = (seconds: number) => metal(seconds).mix(noise(seconds, { volume: 0.4, rate })).filter(\"highpass\", 7200).filter(\"highshelf\", 10000, { gain: 3 }).envelope([0, 1], [seconds * 0.25, 0.4], [seconds, 0]);\n const tom = tone([[0, 190], [0.2, 120]], 0.4, { volume: 0.8, rate }).envelope([0, 1], [0.4, 0]).mix(noise(0.01, { volume: 0.2, rate }).filter(\"bandpass\", 2500));\n const rim = tone(1700, 0.025, { wave: \"triangle\", volume: 0.5, rate }).mix(noise(0.012, { volume: 0.3, rate }).filter(\"highpass\", 4000)).envelope([0, 1], [0.025, 0]);\n const crash = metal(2).mix(noise(2, { volume: 0.5, rate })).filter(\"highpass\", 4500).filter(\"peak\", 7500, { gain: 3 }).envelope([0, 1], [0.08, 0.5], [2, 0]);\n return kit({ k: kick.gain(v), s: snare.gain(v), c: clap.gain(v), h: hat(0.07).gain(0.55 * v), o: hat(0.4).gain(0.5 * v), t: tom.gain(v), r: rim.gain(v), x: crash.gain(0.45 * v) });\n}\n\n/** A kit: a clip a letter, played by a pattern's letters (\"kh\" plays k and h together). */\nexport function kit(hits: Readonly>): Kit {\n for (const k of Object.keys(hits)) if (k.length !== 1 || k === \".\" || k === \"-\" || k === \"|\") throw new Error(`A kit's sounds are named by one letter each, not ${JSON.stringify(k)}.`);\n return Object.freeze({ kind: \"kit\" as const, hits: Object.freeze({ ...hits }) });\n}\n\n/* ── Patterns: what is written, and what writes it ── */\n\n/** A pattern: steps a word each, as one text or a text a bar. */\nexport type Pattern = string | readonly string[];\n\n/** A pattern's steps, the \"|\" left out. */\nconst wordsOf = (p: Pattern): string[] => (typeof p === \"string\" ? p : p.join(\" \")).split(/\\s+/).filter((w) => w !== \"\" && w !== \"|\");\n\n/** A step's sounds and how loud: \"C4+E4@0.6\" is C4 and E4 at 0.6; \"kh\" k and h at 1. */\nfunction velocityOf(word: string, step: number): { body: string; velocity: number } {\n const at = word.indexOf(\"@\");\n if (at < 0) return { body: word, velocity: 1 };\n const body = word.slice(0, at), v = Number(word.slice(at + 1));\n if (body === \"\" || body === \"-\" || body === \".\" || !(v >= 0 && v <= 4)) throw new Error(`Step ${step + 1}: \"@\" follows a note, a chord or a kit's letters, with how loud they play, 0 to 4 (1 as written): \"C4@0.6\", \"kh@1.3\" — not ${JSON.stringify(word)}.`);\n return { body, velocity: v };\n}\n\nconst NAMES = [\"C\", \"C#\", \"D\", \"D#\", \"E\", \"F\", \"F#\", \"G\", \"G#\", \"A\", \"A#\", \"B\"];\n/** A note's name from its number: 60 is \"C4\". Sharps, not flats. */\nfunction nameOf(n: number): string {\n const octave = Math.floor(n / 12) - 1;\n if (octave < -1 || octave > 9) throw new Error(`A note goes from C-1 to B9: this one would be ${NAMES[((n % 12) + 12) % 12]}${octave}.`);\n return NAMES[((n % 12) + 12) % 12] + octave;\n}\n\n/** Each note of a step moved by a function of its number; holds, rests, kit letters and \"@\" kept. */\nconst eachNote = (p: Pattern, move: (n: number) => number): string[] => wordsOf(p).map((w) => w.replace(/[A-Ga-g][#b]?-?\\d/g, (name) => nameOf(move(noteNumber(name)))));\n\n/** A pattern `semitones` up (or down: a negative number): `transpose(tune, 12)` is the tune an octave up. A text a bar stays a text a bar. */\nexport function transpose(p: Pattern, semitones: number): Pattern {\n if (!Number.isInteger(semitones)) throw new Error(`transpose() moves by whole semitones, not ${semitones}.`);\n if (semitones === 0) return p;\n return typeof p === \"string\" ? eachNote(p, (n) => n + semitones).join(\" \") : p.map((bar) => eachNote(bar, (n) => n + semitones).join(\" \"));\n}\n\n/** A pattern with each step `by` steps long — eighths written as sixteenths with `stretch(p, 2)`. A text a bar stays a text a bar. */\nexport function stretch(p: Pattern, by: number): Pattern {\n if (!Number.isInteger(by) || by < 1) throw new Error(`stretch() makes each step a whole number of steps long, 1 or more — not ${by}.`);\n const one = (text: string) => wordsOf(text).map((w) => [w, ...Array.from({ length: by - 1 }, () => (w.startsWith(\".\") ? \".\" : \"-\"))].join(\" \")).join(\" \");\n return typeof p === \"string\" ? one(p) : p.map(one);\n}\n\n/** A pattern `n` times over, as one text. */\nexport function repeat(p: Pattern, n: number): string {\n if (!Number.isInteger(n) || n < 1) throw new Error(`repeat() plays a pattern a whole number of times, 1 or more — not ${n}.`);\n return Array.from({ length: n }, () => wordsOf(p).join(\" \")).join(\" \");\n}\n\n/** A kit's bar: the steps (from 0) each letter plays on, `steps` steps long — `beat({ k: [0, 8], s: [4, 12] })`. */\nexport function beat(hits: Readonly>, steps: number = 16): string {\n for (const [k, at] of Object.entries(hits)) for (const s of at) if (!Number.isInteger(s) || s < 0 || s >= steps) throw new Error(`beat(): ${k} on step ${s}, of steps 0 to ${steps - 1}.`);\n return Array.from({ length: steps }, (_, i) => Object.keys(hits).filter((k) => hits[k].includes(i)).join(\"\") || \".\").join(\" \");\n}\n\n/** A chord's notes above its root, by what follows the root's name. */\nconst QUALITIES: Readonly> = {\n \"\": [0, 4, 7], m: [0, 3, 7], dim: [0, 3, 6], aug: [0, 4, 8], \"+\": [0, 4, 8], sus2: [0, 2, 7], sus4: [0, 5, 7], sus: [0, 5, 7], \"5\": [0, 7],\n \"6\": [0, 4, 7, 9], m6: [0, 3, 7, 9], \"7\": [0, 4, 7, 10], maj7: [0, 4, 7, 11], M7: [0, 4, 7, 11], m7: [0, 3, 7, 10], dim7: [0, 3, 6, 9],\n m7b5: [0, 3, 6, 10], \"7sus4\": [0, 5, 7, 10], \"9\": [0, 4, 7, 10, 14], maj9: [0, 4, 7, 11, 14], m9: [0, 3, 7, 10, 14], add9: [0, 4, 7, 14], madd9: [0, 3, 7, 14],\n};\n\n/** A chord's notes as numbers, lowest first. */\nfunction chordNumbers(symbol: string, octave: number, inversion: number): number[] {\n const m = /^([A-G][#b]?)([^/]*)(?:\\/([A-G][#b]?))?$/.exec(symbol);\n const steps = m ? QUALITIES[m[2]] : undefined;\n if (!m || !steps) throw new Error(`A chord is a root and what kind — \"Am\", \"C\", \"G7\", \"Fmaj7\", \"Dm7\", \"Esus4\", \"Bdim\", \"C/E\" — not ${JSON.stringify(symbol)}. The kinds: ${Object.keys(QUALITIES).map((q) => `\"${q}\"`).join(\", \")}.`);\n if (!Number.isInteger(inversion) || inversion < 0 || inversion >= steps.length) throw new Error(`${symbol} has ${steps.length} notes: its inversion is 0 to ${steps.length - 1}, not ${inversion}.`);\n const root = noteNumber(m[1] + octave);\n const notes = steps.map((s) => root + s);\n for (let k = 0; k < inversion; k++) notes.push(notes.shift()! + 12);\n if (m[3]) {\n // Over another bass: that note, below the rest.\n let bass = noteNumber(m[3] + octave);\n while (bass >= notes[0]) bass -= 12;\n notes.unshift(bass);\n }\n return notes;\n}\n\n/**\n * A chord's notes, as a step plays them: `chord(\"Am\")` is \"A4+C5+E5\", its root in octave 4 (`octave`); `inversion`\n * moves its lowest notes up an octave, one each — `chord(\"E\", 3, 1)` is \"G#3+B3+E4\". \"C/E\" is C over an E below it.\n */\nexport function chord(symbol: string, octave: number = 4, inversion: number = 0): string {\n return chordNumbers(symbol, octave, inversion).map(nameOf).join(\"+\");\n}\n\n/** Each chord's notes as numbers, lowest first: the ones written, or the symbol's at `octave`. */\nconst notesOf = (c: string, octave: number): number[] => (/\\d/.test(c) ? c.split(\"+\").map(noteNumber).sort((a, b) => a - b) : chordNumbers(c, octave, 0));\n\n/**\n * A chord played as `template` says: its steps name the chord's notes by number, 1 the lowest — \"1 3 2 3\", \"1+2+3 - - -\"\n * — past the last, round again an octave up (in a three-note chord, 4 is 1 an octave up); each ^ after a number takes it\n * an octave higher, each _ an octave lower (\"1__\" the root two octaves down, for a bass). \"-\", \".\" and \"@\" as in any\n * pattern. `notes` is a chord's notes (\"A3+C4+E4\") or its name (\"Am\", with its root in octave `octave`).\n */\nexport function over(notes: string, template: Pattern, octave: number = 4): string {\n const n = notesOf(notes, octave);\n return wordsOf(template).map((w, s) => {\n const { body, velocity } = velocityOf(w, s);\n if (body === \"-\" || body === \".\") return w;\n const named = body.split(\"+\").map((d) => {\n const m = /^(\\d+)([\\^_]*)$/.exec(d);\n if (!m || Number(m[1]) < 1) throw new Error(`Step ${s + 1} of a chord's template: its notes are numbers from 1, the lowest, with ^ or _ for an octave up or down — \"1\", \"3^\", \"1__\" — not ${JSON.stringify(d)}.`);\n const k = Number(m[1]) - 1, up = Math.floor(k / n.length) + (m[2].match(/\\^/g)?.length ?? 0) - (m[2].match(/_/g)?.length ?? 0);\n return nameOf(n[k % n.length] + 12 * up);\n });\n return named.join(\"+\") + (velocity === 1 ? \"\" : `@${velocity}`);\n }).join(\" \");\n}\n\n/**\n * A progression played as `template` says, a template a chord: `chords(\"Am Am E Am Dm C E Am\", \"1_ 1 2 3\")` — as\n * `over` plays one chord. Each chord's root in octave `octave` (4); `lead: true` turns each chord after the first to\n * the inversion nearest the one before (its notes move least), as a player's hands do. A text a chord.\n */\nexport function chords(progression: string | readonly string[], template: Pattern, options: { readonly octave?: number; readonly lead?: boolean } = {}): string[] {\n const octave = options.octave ?? 4, list = wordsOf(progression);\n let before: number[] | null = null;\n return list.map((c) => {\n let n = notesOf(c, octave);\n if (options.lead && before && !/\\d/.test(c)) {\n // Each inversion, an octave either side: the one whose notes are nearest those before.\n const prev: number[] = before, mean = prev.reduce((a, b) => a + b, 0) / prev.length;\n let best = n, cost = Infinity;\n for (let inv = 0; inv < n.length; inv++) for (const shift of [-12, 0, 12]) {\n const cand = chordNumbers(c, octave, inv).map((x) => x + shift);\n const d = cand.length === prev.length ? cand.reduce((s, x, i) => s + Math.abs(x - prev[i]), 0) : Math.abs(cand.reduce((a, b) => a + b, 0) / cand.length - mean) * cand.length;\n if (d < cost) { cost = d; best = cand; }\n }\n n = best;\n }\n before = n;\n return over(n.map(nameOf).join(\"+\"), template);\n });\n}\n\n/** Seconds `n` bars last at `bpm` as a song plays it (its bar a whole number of frames): how long to make a riser. */\nexport function bars(bpm: number, n: number = 1, beats: number = 4): number {\n return n * Math.max(1, Math.round((60 / bpm) * beats / FRAME)) * FRAME;\n}\n\n/**\n * What makes a part breathe with the beat, as a mix does under a kick: down to `1 − depth` on each beat (`beat`\n * seconds apart, from the clip's start) and back over `release` of a beat — a part's fx, or a section's:\n * `{ fx: music.pump(music.bars(143) / 4) }`.\n */\nexport function pump(beat: number, depth: number = 0.45, release: number = 0.55): (clip: Clip) => Clip {\n if (!(beat > 0) || !(depth >= 0 && depth <= 1) || !(release > 0 && release <= 1)) throw new Error(\"pump() takes seconds a beat, how deep (0 … 1) and how long it takes to come back, as a part of a beat (0 … 1).\");\n return (c) => {\n const points: [number, number][] = [];\n for (let t = 0; t < c.seconds; t += beat) points.push([t, 1 - depth], [Math.min(c.seconds, t + release * beat), 1], [Math.min(c.seconds, t + beat - 0.004), 1]);\n points.push([c.seconds, 1]);\n return c.envelope(...points);\n };\n}\n\n/* ── Sounds between sections ── */\n\n/** Noise rising for `seconds` through a filter opening, loudest at the end: what leads into a drop. */\nexport function riser(seconds: number, options: { readonly volume?: number; readonly rate?: number } = {}): Clip {\n const rate = options.rate ?? 44100, v = options.volume ?? 0.5, s = seconds;\n const swept = noise(s, { volume: v, rate }).filter(\"bandpass\", [[0, 350], [s * 0.6, 1800], [s, 9000]], { resonance: 0.55 });\n const lift = tone([[0, 220], [s, 880]], s, { wave: \"saw\", voices: 3, detune: 25, width: 0.8, volume: v * 0.25, rate }).filter(\"lowpass\", [[0, 500], [s, 6000]]);\n return swept.mix(lift).envelope([0, 0], [s * 0.5, 0.25], [s * 0.9, 0.85], [s, 1]).fade(0, 0.01);\n}\n\n/** A boom to land on: a falling sine under a burst of noise, ringing `seconds` (1.5). */\nexport function impact(options: { readonly seconds?: number; readonly volume?: number; readonly rate?: number } = {}): Clip {\n const rate = options.rate ?? 44100, v = options.volume ?? 0.8, s = options.seconds ?? 1.5;\n return tone([[0, 90], [0.4, 38], [s, 30]], s, { volume: v, rate }).envelope([0, 1], [0.15, 0.7], [s, 0])\n .mix(noise(s, { volume: v * 0.5, rate }).filter(\"lowpass\", [[0, 6000], [s, 300]]).envelope([0, 1], [0.05, 0.4], [s, 0])).drive(3);\n}\n\n/** How a track or a part is heard. */\nexport interface PartOptions {\n /** Where it is heard, −1 left to 1 right; 0, the middle, when left out. */\n readonly pan?: number;\n /** How loud, 1 as the instrument plays. */\n readonly volume?: number;\n /** What it goes through: its whole part in `render`, each note on its own in `play`. */\n readonly fx?: (clip: Clip) => Clip;\n /** How much of it goes to the song's reverb in `render`, 0 … 1 (0). */\n readonly reverb?: number;\n}\n\n/** What an instrument plays, step by step. */\nexport interface Track {\n readonly instrument: Instrument | Kit;\n readonly steps: readonly string[];\n /** Where it is heard, −1 left to 1 right; 0, the middle, when left out. */\n readonly pan: number;\n /** How loud, 1 as the instrument plays. */\n readonly volume: number;\n /** What the track goes through: its whole part in `render`, each note on its own in `play`. */\n readonly fx?: (clip: Clip) => Clip;\n /** How much of it goes to the song's reverb in `render`, 0 … 1 (0). */\n readonly reverb: number;\n}\n\n/** A part's options checked, and its defaults filled in. */\nfunction heard(options: PartOptions, what: string): { pan: number; volume: number; reverb: number; fx?: (clip: Clip) => Clip } {\n const pan = options.pan ?? 0, volume = options.volume ?? 1, reverb = options.reverb ?? 0;\n if (!(pan >= -1 && pan <= 1)) throw new Error(`A ${what}'s pan is −1 (left) to 1 (right), not ${pan}.`);\n if (!(volume >= 0)) throw new Error(`A ${what}'s volume is 0 or more, not ${volume}.`);\n if (!(reverb >= 0 && reverb <= 1)) throw new Error(`A ${what}'s reverb is 0 to 1, not ${reverb}.`);\n return { pan, volume, reverb, ...(options.fx ? { fx: options.fx } : {}) };\n}\n\n/**\n * A pattern for an instrument: steps a word each — a note or notes joined by +, \"-\" to hold, \".\" to rest, \"|\" for the\n * eye; \"@\" after a step's notes says how loud, \"C4@0.6\". `pan`: where it is heard, −1 left to 1 right (a panned track's\n * notes have two channels); `volume`: 1 as the instrument plays; `fx`: what the track goes through —\n * `(c) => c.delay(0.21).chorus()` — its whole part when the song is rendered (as a mixer's insert), each note on its own\n * when it is played note by note; `reverb`: how much goes to the song's reverb when it is rendered, 0 … 1.\n */\nexport function track(instrument: Instrument | Kit, pattern: Pattern, options: PartOptions = {}): Track {\n return Object.freeze({ instrument, steps: Object.freeze(wordsOf(pattern)), ...heard(options, \"track\") });\n}\n\n/** An instrument and how it is heard, which a song's sections give its notes: `arrange`. */\nexport interface Part extends Omit { readonly kind: \"part\" }\n\n/** An instrument as a part of a song made of sections (`arrange`), heard as `track` says. */\nexport function part(instrument: Instrument | Kit, options: PartOptions = {}): Part {\n return Object.freeze({ kind: \"part\" as const, instrument, ...heard(options, \"part\") });\n}\n\n/** How long a section lasts, as its effects need it for their points: seconds, its bars, and seconds a bar. */\nexport interface SectionTime { readonly seconds: number; readonly bars: number; readonly bar: number }\n\n/** What a section is and does, besides its notes. */\nexport interface SectionOptions {\n /** What `song.sections` calls it. */\n readonly name?: string;\n /**\n * What a part goes through in this section only, given how long the section is — a filter opening over a build:\n * `{ pad: (c, t) => c.filter(\"lowpass\", [[0, 400], [t.seconds, 8000]]) }`. Its notes from this section, tails and\n * all, before the part's own `fx`. Mixed by `render` only: a song with one is not played note by note.\n */\n readonly fx?: Readonly Clip>>;\n /** How loud a part is in this section only, 1 as it is. */\n readonly volume?: Readonly>;\n}\n\n/** Bars of a song: what each part plays in them. */\nexport interface Section {\n readonly kind: \"section\";\n readonly name: string;\n readonly bars: number;\n readonly parts: Readonly>;\n readonly fx: Readonly Clip>>;\n readonly volume: Readonly>;\n /** This section with some parts written again — null: that part rests — and these options over its own. */\n with(parts: Readonly>, options?: SectionOptions): Section;\n}\n\n/**\n * `bars` bars of a song (`arrange`), and what each part plays in them, by the part's name: `{ lead: tune, drums: beat }`.\n * A pattern shorter than the section plays again until it is filled, so a bar of drums fills eight; a part not named\n * rests. `options`: its name, a part's effects or volume for this section only.\n */\nexport function section(bars: number, parts: Readonly>, options: SectionOptions = {}): Section {\n if (!Number.isInteger(bars) || bars < 1) throw new Error(`A section is a whole number of bars, 1 or more — not ${bars}.`);\n for (const [k, v] of Object.entries(options.volume ?? {})) if (!(v >= 0)) throw new Error(`A section's volume for ${k} is 0 or more, not ${v}.`);\n const made: Section = Object.freeze({\n kind: \"section\" as const, name: options.name ?? \"\", bars,\n parts: Object.freeze({ ...parts }), fx: Object.freeze({ ...(options.fx ?? {}) }), volume: Object.freeze({ ...(options.volume ?? {}) }),\n with(changes: Readonly>, more: SectionOptions = {}): Section {\n const next: Record = { ...parts };\n for (const [k, p] of Object.entries(changes)) { if (p === null) delete next[k]; else next[k] = p; }\n return section(bars, next, { name: more.name ?? made.name, fx: { ...made.fx, ...(more.fx ?? {}) }, volume: { ...made.volume, ...(more.volume ?? {}) } });\n },\n });\n return made;\n}\n\n/** A song's tempo and grid. */\nexport interface SongOptions {\n readonly bpm: number;\n /** Steps a beat (4: sixteenths). */\n readonly steps?: number;\n /** Beats a bar (4). */\n readonly beats?: number;\n /** How late every second step starts, as a part of a step: 0 straight (as it is when left out), 1/3 a triplet's lilt. */\n readonly swing?: number;\n}\n\n/** Where a song's section is: from bar `bar` (0 the first) for `bars`, from frame `frame`. */\nexport interface SongSection { readonly name: string; readonly bar: number; readonly bars: number; readonly frame: number }\n\n/** Steps of a track that belong to one section, and what that section does to them. */\ninterface Piece { readonly from: number; readonly to: number; readonly volume: number; readonly fx?: (clip: Clip, time: SectionTime) => Clip; readonly time: SectionTime }\nconst piecesOf = new WeakMap();\n/** A song of sections' parts, by name, and the track each became: what `stems` cuts. */\nconst partsOf = new WeakMap();\n\n/**\n * A song made of sections, one after another: `parts` names each instrument and how it is heard; each section says\n * what the parts play in it. A section can be written once and played again — `arrange(o, parts, intro, verse, verse,\n * chorus)` — or played again changed: `verse.with({ lead: transpose(tune, 12) })`.\n */\nexport function arrange(options: SongOptions, parts: Readonly>, ...sections: readonly Section[]): Song {\n const { perBar, barFrames } = grid(options);\n if (sections.length === 0) throw new Error(\"arrange() takes the song's sections, one at least.\");\n const names = Object.keys(parts);\n for (const s of sections) for (const k of [...Object.keys(s.parts), ...Object.keys(s.fx), ...Object.keys(s.volume)]) {\n if (!(k in parts)) throw new Error(`A section${s.name ? ` (${s.name})` : \"\"} names ${JSON.stringify(k)}, which is no part of the song: its parts are ${names.join(\", \")}.`);\n }\n const tracks = names.map((k) => {\n const steps: string[] = [], pieces: Piece[] = [];\n for (const s of sections) {\n const length = s.bars * perBar, from = steps.length, p = s.parts[k];\n if (p === undefined) for (let i = 0; i < length; i++) steps.push(\".\");\n else {\n const words = wordsOf(p);\n if (words.length === 0 || length % words.length !== 0) throw new Error(`${k}${s.name ? ` in ${s.name}` : \"\"}: a pattern of ${words.length} steps does not fill ${s.bars} bar${s.bars === 1 ? \"\" : \"s\"} of ${perBar} steps evenly.`);\n for (let i = 0; i < length; i++) steps.push(words[i % words.length]);\n }\n const fx = k in s.fx ? s.fx[k] : undefined, volume = s.volume[k] ?? 1;\n if (fx || volume !== 1) pieces.push({ from, to: from + length, volume, ...(fx ? { fx } : {}), time: { seconds: s.bars * barFrames * FRAME, bars: s.bars, bar: barFrames * FRAME } });\n }\n const t = track(parts[k].instrument, steps, parts[k]);\n if (pieces.length) piecesOf.set(t, pieces);\n return t;\n });\n let bar = 0;\n const placedSections = sections.map((s) => { const at = { name: s.name, bar, bars: s.bars, frame: bar * barFrames }; bar += s.bars; return Object.freeze(at); });\n const made = compose(options, tracks, placedSections);\n partsOf.set(made, names.map((k, i) => ({ name: k, track: tracks[i] })));\n return made;\n}\n\n/** A clip as a track plays it note by note: through its fx, at its volume and its pan, made once a clip a track. */\nfunction placed(t: Track, made: Map, c: Clip): Clip {\n if (t.pan === 0 && t.volume === 1 && !t.fx) return c;\n let p = made.get(c);\n if (!p) {\n p = t.fx ? t.fx(c) : c;\n if (t.volume !== 1) p = p.gain(t.volume);\n if (t.pan !== 0) p = p.pan(t.pan);\n made.set(c, p);\n }\n return p;\n}\n\n/** What a song's notes are, kept for render, which makes them again at its own rate; `piece`, the section whose effects it goes through. */\ninterface Score { readonly at: number; readonly track: Track; readonly made: Map; readonly velocity: number; readonly piece?: Piece; readonly name?: string; readonly seconds?: number; readonly hit?: Clip }\nconst scores = new WeakMap();\n\n/** A song, when the script is built: the tables `play` reads, and what it takes. */\nexport interface Song {\n /**\n * Frames a step lasts: a bar is a whole number of frames, a step need not be (2.5 for sixteenths at 143 beats a\n * minute). Played note by note, a note starts on the frame nearest its time — within the 21 ms the game's own\n * mixer starts a sound in anyway; rendered, exactly on it.\n */\n readonly stepFrames: number;\n /** Frames a bar lasts, a whole number. */\n readonly barFrames: number;\n /** Beats a bar. */\n readonly beats: number;\n /** The tempo it plays at: the nearest to the one asked for whose bar is a whole number of frames. */\n readonly bpm: number;\n /** Steps, the longest track's. */\n readonly steps: number;\n /** Its sections, made by `arrange`, in the order they play (none for a song of tracks). */\n readonly sections: readonly SongSection[];\n /** Frames it lasts, its last note's release included. */\n readonly frames: number;\n readonly seconds: number;\n /** Each clip the song plays, once. */\n readonly clips: readonly Clip[];\n /** Each note in the order they start: the frame it starts on, from 0, and which of `clips` it is. */\n readonly at: readonly number[];\n readonly which: readonly number[];\n /** The most notes sounding at once: `play` refuses a song of more than 32 (`render` has no limit). */\n readonly voices: number;\n}\n\n/** A song's grid: steps a bar, and frames a bar and a step. */\nfunction grid(options: SongOptions): { perBar: number; barFrames: number; stepFrames: number } {\n const perBeat = options.steps ?? 4, beats = options.beats ?? 4, swing = options.swing ?? 0;\n if (!(options.bpm > 0) || !(perBeat > 0) || !(beats > 0)) throw new Error(\"A song takes a tempo, { bpm: 120 }, steps a beat (4 by default) and beats a bar (4).\");\n if (!(swing >= 0 && swing < 1)) throw new Error(`A song's swing is 0 (straight) to under 1 (a step late), not ${swing}.`);\n const barFrames = Math.max(1, Math.round((60 / options.bpm) * beats / FRAME));\n return { perBar: perBeat * beats, barFrames, stepFrames: barFrames / (beats * perBeat) };\n}\n\n/**\n * Tracks played together from their first step: `bpm` beats a minute, `steps` steps a beat (4 by default), `swing`\n * every second step late. Played note by note (`play`), a song is refused when more than 32 notes would sound at once;\n * rendered, it has no limit. A song of sections: `arrange`.\n */\nexport function song(options: SongOptions, ...tracks: readonly Track[]): Song {\n return compose(options, tracks, []);\n}\n\nfunction compose(options: SongOptions, tracks: readonly Track[], sections: readonly SongSection[]): Song {\n const { barFrames, stepFrames } = grid(options), beats = options.beats ?? 4, swing = options.swing ?? 0;\n const stepSeconds = stepFrames * FRAME;\n // Where step s starts, in frames: every second step `swing` of a step late.\n const time = (s: number) => (s + (s % 2 === 1 ? swing : 0)) * stepFrames;\n const score: Score[] = [];\n let steps = 0, effects = false;\n for (const t of tracks) {\n const ws = t.steps, made = new Map(), pieces = piecesOf.get(t) ?? [];\n if (pieces.some((p) => p.fx)) effects = true;\n steps = Math.max(steps, ws.length);\n let k = 0;\n ws.forEach((w, s) => {\n while (k < pieces.length && pieces[k].to <= s) k++;\n const piece = k < pieces.length && pieces[k].from <= s ? pieces[k] : undefined;\n const { body, velocity: v } = velocityOf(w, s);\n if (body === \"-\" || body === \".\") return;\n const velocity = v * (piece?.volume ?? 1), into = piece ? { piece } : {};\n if (t.instrument.kind === \"kit\") {\n for (const letter of body) {\n const hit = t.instrument.hits[letter];\n if (!hit) throw new Error(`Step ${s + 1}: the kit has no ${JSON.stringify(letter)} (it has ${Object.keys(t.instrument.hits).join(\", \")}).`);\n score.push({ at: time(s), track: t, made, velocity, ...into, hit });\n }\n return;\n }\n // A note lasts its step and every \"-\" after it.\n let length = 1;\n while (s + length < ws.length && ws[s + length] === \"-\") length++;\n const seconds = swing === 0 ? length * stepSeconds : (time(s + length) - time(s)) * FRAME;\n for (const name of body.split(\"+\")) {\n noteNumber(name);\n score.push({ at: time(s), track: t, made, velocity, ...into, name, seconds });\n }\n });\n }\n score.sort((a, b) => a.at - b.at);\n const base = (n: Score) => n.hit ?? (n.track.instrument as Instrument).note(n.name!, n.seconds!);\n // The tables `play` reads, made when they are first read: a song only rendered never makes its notes for the game.\n let table: { clips: readonly Clip[]; at: readonly number[]; which: readonly number[]; voices: number; peakStep: number; frames: number } | null = null;\n const tables = () => {\n if (table) return table;\n const notes = score.map((n) => ({ at: Math.round(n.at), clip: placed(n.track, n.made, n.velocity === 1 ? base(n) : base(n).gain(n.velocity)) }));\n // How many sound at once: at each note's start, those started before it and not yet over.\n let voices = 0, peakStep = 0;\n notes.forEach((n, k) => {\n let sounding = 0;\n for (let j = 0; j <= k; j++) if (notes[j].at + notes[j].clip.seconds / FRAME > n.at) sounding++;\n if (sounding > voices) { voices = sounding; peakStep = Math.round(n.at / stepFrames) + 1; }\n });\n const clips: Clip[] = [];\n const which = notes.map((n) => { let k = clips.indexOf(n.clip); if (k < 0) { clips.push(n.clip); k = clips.length - 1; } return k; });\n const frames = Math.max(Math.round(steps * stepFrames), ...notes.map((n) => n.at + Math.ceil(n.clip.seconds / FRAME)));\n table = { clips: Object.freeze(clips), at: Object.freeze(notes.map((n) => n.at)), which: Object.freeze(which), voices, peakStep, frames };\n return table;\n };\n // What the game plays note by note: refused, when it is read, if more than 32 would sound at once (render has no limit).\n const playable = () => {\n if (effects) throw new Error(\"A section of this song puts a part through effects of its own, which only music.render mixes: render the song, or give the part those effects for the whole song.\");\n const t = tables();\n if (t.voices > VOICES) throw new Error(`At step ${t.peakStep}, ${t.voices} notes would sound at once; the game plays ${VOICES} note by note. Shorten notes or thin the chords — or music.render the song, one sound however many it mixes.`);\n return t;\n };\n const made: Song = Object.freeze({\n stepFrames, barFrames, beats, bpm: (60 * beats) / (barFrames * FRAME), steps, sections: Object.freeze([...sections]),\n get frames() { return tables().frames; },\n get seconds() { return tables().frames * FRAME; },\n get clips() { return playable().clips; },\n get at() { return playable().at; },\n get which() { return playable().which; },\n get voices() { return tables().voices; },\n });\n scores.set(made, score);\n return made;\n}\n\n/** The General MIDI drums, by number, as `drums()` has them: kicks k, snares s, the clap c, hats h and o, toms t, the rim r, crashes x. */\nconst GM_DRUMS: Readonly> = {\n 35: \"k\", 36: \"k\", 37: \"r\", 38: \"s\", 40: \"s\", 39: \"c\", 42: \"h\", 44: \"h\", 51: \"h\", 53: \"h\", 59: \"h\", 46: \"o\",\n 41: \"t\", 43: \"t\", 45: \"t\", 47: \"t\", 48: \"t\", 50: \"t\", 49: \"x\", 52: \"x\", 55: \"x\", 57: \"x\",\n};\n\n/** How imported notes are made a song: its grid, and which kit letter a drum's number plays. */\nexport interface MidiOptions extends Omit {\n /** Beats a minute: the file's own when left out. */\n readonly bpm?: number;\n /** A drum's MIDI number to a kit's letter, over General MIDI's (36 \"k\", 38 \"s\", 42 \"h\", 46 \"o\", 49 \"x\", …). */\n readonly drums?: Readonly>;\n}\n\n/**\n * A song of imported notes (`import tune from \"./tune.notes\"`): each track `parts` names — by its name, or its place\n * in `tune.tracks` from 0 — played by an instrument, a kit (the drums by their General MIDI numbers) or a `part`;\n * the tracks not named are left out. Each note starts on the step nearest its time and lasts the steps nearest its\n * length (`steps` a beat, 4: sixteenths); how hard it was struck is how loud it plays, 100 as written. A track whose\n * notes overlap becomes several, so the song can hold them. At the file's first tempo unless `bpm` says.\n */\nexport function midi(notes: Notes, parts: Readonly>, options: MidiOptions = {}): Song {\n const perBeat = options.steps ?? 4, map = { ...GM_DRUMS, ...(options.drums ?? {}) };\n const names = notes.tracks.map((t) => t.name);\n const tracks: Track[] = [];\n for (const [key, given] of Object.entries(parts)) {\n const index = names.indexOf(key) >= 0 ? names.indexOf(key) : /^\\d+$/.test(key) ? Number(key) : -1;\n const source = notes.tracks[index];\n if (!source) throw new Error(`The notes have no track ${JSON.stringify(key)}: their tracks are ${names.map((n) => JSON.stringify(n)).join(\", \")} (or 0 to ${names.length - 1}).`);\n const instrument = given.kind === \"part\" ? given.instrument : given, how = given.kind === \"part\" ? given : {};\n const kit = instrument.kind === \"kit\" ? instrument : null;\n // Voices: steps that each hold one chord at a time; a note joins a chord struck with it, or the first voice free.\n type Word = { sounds: string[]; velocity: number; length: number };\n const voices: { words: Map; end: number; last: number }[] = [];\n const placed = source.notes.map((n) => {\n const from = Math.round(n.at * perBeat);\n return { from, length: kit ? 1 : Math.max(1, Math.round((n.at + n.beats) * perBeat) - from), sound: kit ? map[n.pitch] : nameOf(n.pitch), velocity: Math.max(0.05, Math.round((n.velocity / 100) * 20) / 20) };\n }).filter((n) => n.sound !== undefined && (!kit || n.sound in kit.hits)).sort((a, b) => a.from - b.from || b.length - a.length);\n for (const n of placed) {\n let voice = voices.find((v) => { const w = v.words.get(v.last); return v.last === n.from && w !== undefined && w.length === n.length && w.velocity === n.velocity; });\n if (voice) { const w = voice.words.get(n.from)!; if (!w.sounds.includes(n.sound)) w.sounds.push(n.sound); continue; }\n voice = voices.find((v) => v.end <= n.from);\n if (!voice) { voice = { words: new Map(), end: 0, last: -1 }; voices.push(voice); }\n voice.words.set(n.from, { sounds: [n.sound], velocity: n.velocity, length: n.length });\n voice.end = n.from + n.length;\n voice.last = n.from;\n }\n for (const v of voices) {\n const steps: string[] = Array.from({ length: v.end }, () => \".\");\n for (const [at, w] of v.words) {\n steps[at] = (kit ? w.sounds.join(\"\") : w.sounds.join(\"+\")) + (w.velocity === 1 ? \"\" : `@${w.velocity}`);\n if (!kit) for (let k = 1; k < w.length; k++) steps[at + k] = \"-\";\n }\n tracks.push(track(instrument, steps, how));\n }\n }\n return compose({ ...options, bpm: options.bpm ?? notes.bpm, beats: options.beats ?? notes.beats }, tracks, []);\n}\n\n/** The song's reverb in `render`, as `Clip.reverb` takes it, the dry part left out. */\nexport interface ReverbOptions { readonly decay?: number; readonly damping?: number; readonly predelay?: number; readonly width?: number }\n\n/**\n * A song mixed into one clip when the script is built: every note exactly in time, one sound playing. Mixed as a\n * mixing desk does: each track's notes made again at `rate` (44 100 when left out: what the ear hears up to 20 kHz; a\n * note played in the game note by note is made at 22 050, to keep the map small) into its own part, through a\n * section's effects where it has them, then its `fx`, at its volume, to its side; what each track sends (its `reverb`)\n * into one reverb shared by them all (`reverb`, its room); the whole through `master` —\n * `(mix) => mix.compress({ threshold: -14, ratio: 2 })` — and last a limiter (`Clip.limit`) so it never goes over\n * −1.5 dB however the parts add, between the samples too: `limit: false` leaves it as mixed, `{ ceiling, release }` sets it. A long clip a\n * program plays goes in the map as Ogg Vorbis, two channels when it has two.\n */\nexport function render(s: Song, options: { readonly rate?: number; readonly limit?: boolean | { readonly ceiling?: number; readonly release?: number }; readonly reverb?: ReverbOptions; readonly master?: (mix: Clip) => Clip } = {}): Clip {\n const rate = options.rate ?? 44100, score = scores.get(s), least = s.steps * s.stepFrames * FRAME;\n let mixed: Clip;\n if (!score) {\n // (A song from elsewhere, without its score: its own clips, at their rate.)\n mixed = mixdown(s.at.map((at, k) => ({ clip: s.clips[s.which[k]], at: at * FRAME })), s.seconds, rate);\n } else {\n const parts: { clip: Clip; at: number }[] = [], sends: { clip: Clip; at: number }[] = [];\n const byTrack = new Map();\n for (const n of score) { const list = byTrack.get(n.track); if (list) list.push(n); else byTrack.set(n.track, [n]); }\n for (const [t, notes] of byTrack) {\n const clipOf = (n: Score) => { const c = n.hit ?? (t.instrument as Instrument).note(n.name!, n.seconds!, rate); return n.velocity === 1 ? c : c.gain(n.velocity); };\n // The notes as they are, and each section's that has effects of its own mixed apart and through them.\n const entries: { clip: Clip; at: number }[] = [], apart = new Map();\n for (const n of notes) {\n if (n.piece?.fx) { const list = apart.get(n.piece); if (list) list.push(n); else apart.set(n.piece, [n]); }\n else entries.push({ clip: clipOf(n), at: n.at * FRAME });\n }\n for (const [p, list] of apart) {\n const from = p.from * s.stepFrames;\n const sub = mixdown(list.map((n) => ({ clip: clipOf(n), at: (n.at - from) * FRAME })), p.time.seconds, rate);\n entries.push({ clip: p.fx!(sub, p.time), at: from * FRAME });\n }\n let part = mixdown(entries, least, rate);\n if (t.fx) part = t.fx(part);\n if (t.volume !== 1) part = part.gain(t.volume);\n if (t.pan !== 0) part = part.pan(t.pan);\n parts.push({ clip: part, at: 0 });\n if (t.reverb > 0) sends.push({ clip: part.gain(t.reverb), at: 0 });\n }\n if (sends.length) {\n const r = options.reverb ?? {};\n parts.push({ clip: mixdown(sends, least, rate).reverb({ decay: r.decay ?? 0.7, damping: r.damping ?? 0.45, predelay: r.predelay ?? 0.02, width: r.width ?? 1, mix: 1, dry: 0 }), at: 0 });\n }\n mixed = mixdown(parts, least, rate);\n }\n if (options.master) mixed = options.master(mixed);\n const limit = options.limit ?? true;\n return limit === false ? mixed : mixed.limit(limit === true ? {} : limit);\n}\n\n/* ── Music that follows the game: a song cut into pieces, played piece by piece ── */\n\n/** How a song of sections is cut for the game to play piece by piece (`stems`). */\nexport interface StemsOptions {\n /**\n * Which parts each layer holds, by name — `{ drums: [\"kit\", \"hats\"], bass: [\"bass\"], melody: [\"lead\", \"saw\"] }` — a\n * layer the game can turn on and off where a piece begins. A part in no layer is left out.\n */\n readonly layers: Readonly>;\n /**\n * Beats a piece: where the game can go to another section or turn a layer on or off (a bar's beats when left out).\n * Two in a bar of four halves the wait for a change, for about half as much map again and more sounds at once.\n */\n readonly every?: number;\n /** Samples a second (44 100). */\n readonly rate?: number;\n /** The reverb's room, as `render` takes it. */\n readonly reverb?: ReverbOptions;\n /** What each piece's clip goes through last — an EQ, not a compressor: each is on its own, the game adds them up. */\n readonly master?: (clip: Clip) => Clip;\n /** The loudest the pieces may add up to as the song plays through, every layer on (0.84, −1.5 dB). */\n readonly peak?: number;\n /**\n * Seconds a piece's clip rings on after its notes end (2), its echoes and reverb faded out over the last of them: the\n * pieces after it cover the rest, and a shorter clip is a smaller map. A layer's last piece keeps all of its tail.\n */\n readonly tail?: number;\n /** The Ogg Vorbis quality of the pieces' clips (4: a little smaller than a song's 5, as many play at once). */\n readonly quality?: number;\n /**\n * Sounds to play on the next beat when the game asks (`band.sting`): a crash for a cleared line, a hit for a boss.\n * Turned down with the pieces, which leave room for one on any beat.\n */\n readonly stingers?: Readonly>;\n}\n\n/** A song cut into pieces for the game to play piece by piece, with `conduct` and a `Band`: made when the script is built. */\nexport interface Stems {\n readonly kind: \"stems\";\n /** Frames a bar lasts. */\n readonly barFrames: number;\n readonly bpm: number;\n /** Beats a bar, and beats a piece. */\n readonly beats: number;\n readonly every: number;\n /** Frames each beat of a piece lasts, whole frames (beats of a bar not a whole number of frames take turns). */\n readonly waits: readonly number[];\n /** The layers' names, in the order `band.on` has them. */\n readonly layers: readonly string[];\n /** The sections, in the order the song plays them: their names and how many bars. */\n readonly sections: readonly SongSection[];\n /**\n * Section `s`'s piece `p` in layer `l`: its notes, their tails and echoes and reverb with them, or null where it plays\n * none. A section of `bars` bars has bars × beats / every pieces.\n */\n readonly pieces: readonly (readonly (readonly (Clip | null)[])[])[];\n readonly stingers: readonly Clip[];\n /** A section's place in `sections`, by its name: `band.go(stems.section(\"drop\"))`. */\n section(name: string): number;\n /** A layer's place in `layers`, by its name: `band.mute(stems.layer(\"drums\"))`. */\n layer(name: string): number;\n /** A stinger's place, by its name: `band.sting(stems.stinger(\"crash\"))`. */\n stinger(name: string): number;\n /** The most clips sounding at once as the song plays through, every layer on: of the game's 32, what is left for the rest. */\n readonly voices: number;\n /** The pieces added up as the game plays the song through, every layer on, each on its frame: to hear it, or compare. */\n readonly playthrough: Clip;\n}\n\n/**\n * A song of sections (`arrange`) cut into pieces — a bar each, or `every` beats — a clip each piece of each layer, for\n * music that follows the game: where each piece begins the program says which section plays and which layers (`Band`,\n * `conduct`), and a stinger plays on the next beat. A piece's clip holds the notes that start in it, with their tails,\n * echoes and reverb, so it overlaps the next and nothing is cut off, whichever comes after it; a piece the same as\n * another is the same clip, one file in the map. Each layer is mixed as `render` mixes the song, but with no master\n * and no limiter between the parts — the game adds the clips up — so all of them are turned down together until the\n * song played through stays under `peak`.\n */\nexport function stems(s: Song, options: StemsOptions): Stems {\n const parts = partsOf.get(s), score = scores.get(s);\n if (!parts || !score) throw new Error(\"stems() cuts a song made of sections, by music.arrange.\");\n const rate = options.rate ?? 44100, layers = Object.keys(options.layers), r = options.reverb ?? {}, tail = options.tail ?? 2;\n if (!(tail > 0)) throw new Error(`stems()' tail is seconds after a piece, more than 0, not ${tail}.`);\n if (layers.length === 0 || layers.length > 8) throw new Error(`stems() takes one to eight layers, not ${layers.length}.`);\n const beats = s.beats, every = options.every ?? beats, piece = (s.barFrames * every) / beats;\n if (!Number.isInteger(every) || every < 1 || every > beats || beats % every !== 0) throw new Error(`stems() cuts every few beats of a bar's ${beats}, a number that divides it — not every ${every}.`);\n if (!Number.isInteger(piece)) throw new Error(`A piece of ${every} beats would be ${piece.toFixed(2)} frames: the game plays a sound on a frame. Make it a bar (every: ${beats}), or a tempo whose bar divides into whole frames.`);\n const perBar = beats / every;\n const layerOf = new Map();\n layers.forEach((name, l) => {\n for (const p of options.layers[name]) {\n const found = parts.find((x) => x.name === p);\n if (!found) throw new Error(`The layer ${name} names ${JSON.stringify(p)}, which is no part of the song: its parts are ${parts.map((x) => x.name).join(\", \")}.`);\n if (layerOf.has(found.track)) throw new Error(`${p} is in two layers: a part is in one at most.`);\n layerOf.set(found.track, l);\n }\n });\n const pieceSeconds = piece * FRAME, total = s.sections.reduce((n, x) => n + x.bars, 0) * perBar;\n // The notes of each piece of each layer, by part.\n const cells: Map[][] = Array.from({ length: total }, () => layers.map(() => new Map()));\n for (const n of score) {\n const l = layerOf.get(n.track);\n if (l === undefined) continue;\n const g = Math.min(total - 1, Math.floor((n.at + 1e-6) / piece));\n const m = cells[g][l], list = m.get(n.track);\n if (list) list.push(n); else m.set(n.track, [n]);\n }\n const clipOf = (t: Track, n: Score) => { const c = n.hit ?? (t.instrument as Instrument).note(n.name!, n.seconds!, rate); return n.velocity === 1 ? c : c.gain(n.velocity); };\n // Each layer's last piece with notes: it rings out whole, as the song ends.\n const last = layers.map((_, l) => { let g = total - 1; while (g > 0 && cells[g][l].size === 0) g--; return g; });\n // A piece of a layer, mixed: each part's notes from where it begins, through what the part goes through, and the reverb.\n const raw: (Clip | null)[][] = cells.map((byLayer, g) => byLayer.map((m, l) => {\n const out: { clip: Clip; at: number }[] = [], sends: { clip: Clip; at: number }[] = [];\n // Where the piece's last note ends, its release and all: a riser four bars long is kept whole.\n let end = pieceSeconds;\n for (const [t, notes] of m) for (const n of notes) end = Math.max(end, (n.at - g * piece) * FRAME + clipOf(t, n).seconds);\n for (const [t, notes] of m) {\n const plain = notes.filter((n) => !n.piece?.fx), apart = notes.filter((n) => n.piece?.fx);\n const entries: { clip: Clip; at: number }[] = plain.map((n) => ({ clip: clipOf(t, n), at: (n.at - g * piece) * FRAME }));\n if (apart.length) {\n // A section's effects take the whole section: this piece's notes where they fall in it, then the piece cut out.\n const p = apart[0].piece!, from = p.from * s.stepFrames, into = (g * piece - from) * FRAME;\n const whole = p.fx!(mixdown(apart.map((n) => ({ clip: clipOf(t, n), at: (n.at - from) * FRAME })), undefined, rate), p.time);\n entries.push({ clip: whole.trim(into, whole.seconds), at: 0 });\n }\n let part = mixdown(entries, undefined, rate);\n if (t.fx) part = t.fx(part);\n if (t.volume !== 1) part = part.gain(t.volume);\n if (t.pan !== 0) part = part.pan(t.pan);\n out.push({ clip: part, at: 0 });\n if (t.reverb > 0) sends.push({ clip: part.gain(t.reverb), at: 0 });\n }\n if (out.length === 0) return null;\n if (sends.length) out.push({ clip: mixdown(sends, undefined, rate).reverb({ decay: r.decay ?? 0.7, damping: r.damping ?? 0.45, predelay: r.predelay ?? 0.02, width: r.width ?? 1, mix: 1, dry: 0 }), at: 0 });\n let mixed = mixdown(out, undefined, rate);\n if (options.master) mixed = options.master(mixed);\n // What rings on after the piece's notes, kept for `tail` seconds and faded over the last of them — the pieces\n // after cover the rest — but all of it after the layer's last piece.\n const keep = end + tail;\n return g < last[l] && mixed.seconds > keep ? mixed.trim(0, keep).fade(0, Math.min(1, tail / 2)) : mixed;\n }));\n // All of them turned down together, as the song plays through.\n const placed: { clip: Clip; at: number; g: number; l: number }[] = [];\n raw.forEach((byLayer, g) => byLayer.forEach((c, l) => { if (c) placed.push({ clip: c, at: g * pieceSeconds, g, l }); }));\n if (placed.length === 0) throw new Error(\"stems(): the layers play no notes.\");\n // The stingers may play on any beat: turned down with the pieces, and room left for them over the loudest.\n const waits = Array.from({ length: every }, (_, j) => Math.round(((j + 1) * piece) / every) - Math.round((j * piece) / every));\n const stingerNames = Object.keys(options.stingers ?? {});\n const lines: number[] = [];\n for (let g = 0; g < total; g++) { let at = g * piece; for (const w of waits) { lines.push(at * FRAME); at += w; } }\n const fitted = together(placed, { peak: options.peak ?? 0.84, over: stingerNames.map((k) => ({ clip: options.stingers![k], at: lines })) });\n const grid: (Clip | null)[][] = raw.map((byLayer) => byLayer.map(() => null));\n const quality = options.quality ?? 4;\n placed.forEach((p, k) => { grid[p.g][p.l] = fitted[k].ogg(quality); });\n const stingers = stingerNames.map((_, k) => fitted[placed.length + k]);\n let start = 0;\n const pieces = s.sections.map((sec) => { const n = sec.bars * perBar, rows = grid.slice(start, start + n).map((row) => Object.freeze(row)); start += n; return Object.freeze(rows); });\n const lookup = (what: string, list: readonly string[], name: string) => {\n const k = list.indexOf(name);\n if (k < 0) throw new Error(`There is no ${what} ${JSON.stringify(name)}: there are ${list.map((x) => JSON.stringify(x)).join(\", \") || \"none\"}.`);\n return k;\n };\n let voices = -1;\n return Object.freeze({\n kind: \"stems\" as const, barFrames: s.barFrames, bpm: s.bpm, beats, every, waits: Object.freeze(waits),\n layers: Object.freeze(layers), sections: s.sections, pieces: Object.freeze(pieces), stingers: Object.freeze(stingers),\n section: (name: string) => lookup(\"section\", s.sections.map((x) => x.name), name),\n layer: (name: string) => lookup(\"layer\", layers, name),\n stinger: (name: string) => lookup(\"stinger\", stingerNames, name),\n get playthrough() {\n return mixdown(placed.map((p) => ({ clip: grid[p.g][p.l]!, at: p.at })));\n },\n get voices() {\n if (voices < 0) {\n voices = 0;\n for (const p of placed) { let n = 0; for (const q of placed) if (q.at <= p.at && q.at + q.clip.seconds > p.at) n++; voices = Math.max(voices, n); }\n }\n return voices;\n },\n });\n}\n\n/**\n * The music as the game steers it: which section plays, which piece of it, which layers. Make one inside the program\n * that steers the music and hand it to `conduct`, which plays a piece where each begins as it says:\n *\n * import { music, Band } from \"trigscript/music\";\n * const band = new Band();\n * start(music.conduct(stems, band));\n * band.go(stems.section(\"drop\")); // where the next piece begins\n * band.mute(stems.layer(\"drums\"));\n * band.sting(stems.stinger(\"crash\")); // on the next beat\n *\n * With nothing said, the song plays through its sections in order and starts again.\n */\nexport class Band {\n /** The section playing, its place in `stems.sections`; the piece of it (a bar, unless `stems.every` says), from 0; the beat of the piece. */\n section = 0;\n piece = 0;\n beat = 0;\n /** Pieces played since it started. */\n count = 0;\n /** The game's frame the beat playing began on: how far into the beat it is, for what moves with the music. */\n at = 0;\n /** The section to go to, and whether where the next piece begins (`go`) or when this section ends (`queue`); −1 for none. */\n next = -1;\n now = false;\n /** Whether a section plays again when it ends, instead of going on to the next. */\n loop = false;\n /** Which layers play, by their place in `stems.layers`. */\n on = [true, true, true, true, true, true, true, true];\n /** A stinger to play on the next beat, by its place; −1 for none. */\n stung = -1;\n /** Whether it plays: false stops it where the next piece would begin (what sounds rings out). */\n playing = true;\n\n /** Go to a section where the next piece begins, from its first piece. */\n go(section: number) {\n this.next = section;\n this.now = true;\n }\n /** Go to a section when the one playing ends. */\n queue(section: number) {\n this.next = section;\n this.now = false;\n }\n /** Turn a layer off, or on, from where the next piece begins. */\n mute(layer: number) {\n this.on[layer] = false;\n }\n unmute(layer: number) {\n this.on[layer] = true;\n }\n /** Play a stinger on the next beat. */\n sting(stinger: number) {\n this.stung = stinger;\n }\n /** Stop where the next piece would begin. */\n stop() {\n this.playing = false;\n }\n}\n\n/** Play `stems` as `band` says, beat by beat, until it stops: `start(music.conduct(stems, band))` beside the program. */\nexport const conduct = game(async (s: Stems, band: Band) => {\n band.playing = true;\n band.beat = 0;\n while (band.playing || band.beat > 0) {\n band.at = memory.game.frame;\n if (band.beat === 0) {\n // Where a piece begins: where to be, then the piece's clip of each layer that is on. (Which clip, by test: a\n // clip is no value a program holds.)\n if (band.next >= 0 && (band.now || band.piece === 0)) {\n band.section = band.next;\n band.piece = 0;\n band.next = -1;\n }\n for (let k = 0; k < s.sections.length; k++) if (band.section === k) {\n for (let p = 0; p < s.pieces[k].length; p++) if (band.piece === p) {\n for (let l = 0; l < s.layers.length; l++) if (s.pieces[k][p][l]) if (band.on[l]) playWav(s.pieces[k][p][l]!, 0);\n }\n }\n }\n for (let j = 0; j < s.stingers.length; j++) if (band.stung === j) playWav(s.stingers[j], 0);\n band.stung = -1;\n for (let j = 0; j < s.every; j++) if (band.beat === j) await sleep(frames(s.waits[j]));\n band.beat++;\n if (band.beat >= s.every) {\n band.beat = 0;\n band.count++;\n band.piece++;\n // A section ended: again, or on to the next (after the last, the first).\n for (let k = 0; k < s.sections.length; k++) if (band.section === k) if (band.piece >= s.pieces[k].length) {\n band.piece = 0;\n if (!band.loop) band.section = k + 1 < s.sections.length ? k + 1 : 0;\n }\n if (!band.playing) band.beat = 0;\n }\n }\n});\n\n/**\n * Play a song, note by note, inside an async program: \\`await music.play(song)\\` resolves at its end, or\n * \\`start(music.play(song))\\` plays it beside the program.\n */\nexport const play = game(async (s: Song) => {\n const at = s.at;\n const which = s.which;\n let i = 0;\n let now = 0;\n while (i < at.length) {\n if (at[i] > now) {\n await sleep(frames(at[i] - now));\n now = at[i];\n }\n const k = which[i];\n // Which clip, by its number: a test a clip (a loop over a length known when the script is built is unrolled, and\n // the clips stay the song's, known when it is built — a clip is no value a program holds).\n for (let j = 0; j < s.clips.length; j++) if (k === j) playWav(s.clips[j], 0);\n i++;\n }\n if (s.frames > now) await sleep(frames(s.frames - now));\n});\n\n/** Play a song again and again; start it beside a program: \\`start(music.loop(song))\\`. */\nexport const loop = game(async (s: Song) => {\n // (\\`play\\` written out again, so that the wait at the end of each turn is where the loop shows it — at least a frame,\n // so that a song with nothing left at its end does not turn round within one frame for ever.)\n const at = s.at;\n const which = s.which;\n while (true) {\n let i = 0;\n let now = 0;\n while (i < at.length) {\n if (at[i] > now) {\n await sleep(frames(at[i] - now));\n now = at[i];\n }\n const k = which[i];\n for (let j = 0; j < s.clips.length; j++) if (k === j) playWav(s.clips[j], 0);\n i++;\n }\n await sleep(frames(Math.max(s.frames - now, 1)));\n }\n});\n\n/** Everything above as one name: import { music } from \"trigscript/music\"; music.play(song). */\nexport const music = {\n sampler, synth, fmSynth, strings, presets, drums, kit, track, song, render, play, loop,\n part, section, arrange, transpose, stretch, repeat, beat, chord, over, chords, bars, pump, riser, impact, midi, stems, conduct,\n};\n", "random": "/**\n * Chance: the standard library's \"trigscript/random\" (TrigScript 4).\n *\n * import { chance, range, pick, shuffle, weighted, Random } from \"trigscript/random\";\n *\n * if (chance(25)) dropLoot(); // one time in four\n * const lane = range(1, 3); // 1, 2 or 3\n * const boss = pick(BOSSES); // any one of a list\n * shuffle(order); // an array of the program, in place\n * const tier = weighted([70, 25, 5]); // 0 most of the time, 2 rarely\n *\n * const rng = new Random(seed); // the same numbers every time for the same seed\n * const x = rng.range(0, 63);\n *\n * The functions use the game's own random numbers, which every player's game draws alike, so nothing goes out of step\n * in a game of several players. `Random` is a generator of your own (xorshift), seeded: the same seed gives the same\n * numbers, game after game — a level made from a seed, a save code that holds one.\n */\nimport { random } from \"trigscript\";\n\n/** True `percent` times in a hundred (25: one time in four). */\nexport function chance(percent: number): boolean {\n return random(100) < percent;\n}\n\n/** A whole number from `min` to `max`, both included. */\nexport function range(min: number, max: number): number {\n return min + random(max - min + 1);\n}\n\n/** The total of `count` dice of `sides` sides each (3, 6: from 3 to 18). */\nexport function dice(count: number, sides: number): number {\n let total = 0;\n let i = 0;\n while (i < count) { total += 1 + random(sides); i++; }\n return total;\n}\n\n/** One of a list, any one alike; a list the script has, or an array of the program (the first's 0 when it is empty). */\nexport function pick(items: readonly T[]): T {\n return items[random(items.length)];\n}\n\n/** The place of one of the weights, each as likely as its share of their total: [70, 25, 5] gives 0 most of the time. */\nexport function weighted(weights: readonly number[]): number {\n let total = 0;\n for (const w of weights) total += w;\n let roll = random(total);\n let i = 0;\n while (i < weights.length) {\n if (roll < weights[i]) return i;\n roll -= weights[i];\n i++;\n }\n return weights.length - 1;\n}\n\n/** An array of the program in a random order, in place (Fisher–Yates: every order alike). */\nexport function shuffle(items: T[]) {\n let i = items.length - 1;\n while (i > 0) {\n const j = random(i + 1);\n const held = items[i];\n items[i] = items[j];\n items[j] = held;\n i--;\n }\n}\n\n/**\n * A generator of random numbers of your own, seeded: the same seed gives the same numbers, in the game and when the\n * script is tested. Make it in the program (`const rng = new Random(1234)`).\n */\nexport class Random {\n state: number;\n\n /** Seeded with any whole number (0 is taken as 1). */\n constructor(seed: number) {\n this.state = seed === 0 ? 1 : seed;\n }\n\n /** The next number, from 0 to 2 147 483 647. */\n next(): number {\n let x = this.state;\n x ^= x << 13;\n x ^= x >>> 17;\n x ^= x << 5;\n this.state = x;\n return x & 0x7fffffff;\n }\n\n /** A whole number from 0 to n − 1. */\n int(n: number): number {\n return this.next() % n;\n }\n\n /** A whole number from `min` to `max`, both included. */\n range(min: number, max: number): number {\n return min + this.int(max - min + 1);\n }\n\n /** True `percent` times in a hundred. */\n chance(percent: number): boolean {\n return this.int(100) < percent;\n }\n\n /** One of a list, any one alike. */\n pick(items: readonly T[]): T {\n return items[this.int(items.length)];\n }\n\n /** An array of the program in a random order, in place. */\n shuffle(items: T[]) {\n let i = items.length - 1;\n while (i > 0) {\n const j = this.int(i + 1);\n const held = items[i];\n items[i] = items[j];\n items[j] = held;\n i--;\n }\n }\n}\n", "screen": "/**\n * Pixel screens: the standard library's \"trigscript/screen\" (TrigScript 4).\n *\n * import { Screen, ScreenView, sprite } from \"trigscript/screen\";\n *\n * const SHIP = sprite([\".1.\", \"111\", \"1.1\"]);\n *\n * program(async () => {\n * const screen = new Screen(4, 4, 24, 16); // 24 × 16 pixels, its corner on tile (4, 4)\n * let x = 10;\n * while (true) {\n * screen.clear();\n * screen.rect(0, 15, 24, 1, 5); // the ground, in colour 5\n * screen.blit(SHIP, x, 12);\n * screen.number(score, 1, 1, 2); // digits 3 × 5 pixels, in colour 2\n * screen.present(); // on the map, and so on the minimap (screen.reveal(P1) once first)\n * await nextFrame();\n * }\n * });\n *\n * A screen is a grid of pixels, each 0 (empty) or a colour from 1 to 7; drawing changes the grid, and `present()`\n * shows it: a pixel is a unit on the map, owned by the player whose colour it is, so the minimap shows the picture —\n * neighbouring pixels of one colour join into a solid block there. Only the pixels that changed since the last\n * `present()` are made or taken away, and only the pixels drawn since are looked at, so a still picture costs nothing to\n * show. Draw through the methods (`set`, `clear`, `rect`, …): a pixel written into `pixels` directly is not seen. The same grid can be shown in a\n * player's text lines too, as squares (`ScreenView` in a `ui.lines` view): one game, two screens, as the Tetris probe has it.\n *\n * A player sees the pixels only where they have vision: `screen.reveal(P1)` once gives them Map Revealers over it.\n *\n * Two looks (the constructor's last argument): \"dots\", Overlords, which fly — the minimap draws each as a dot with a gap\n * round it, the screen may be over Space; and \"solid\", Pylons, which the minimap draws as their whole two-tile\n * footprint, so pixels side by side are one block — on buildable ground (probes/lib/screen-map.mts --ground).\n *\n * What the map needs, found playing the unit-pixel probe in StarCraft: Remastered (2026-10-16): the owners of the\n * pixels (P3, P4, P5, P1, P6, P7, P8 below; their colours are theirs on the map, so give them the colours of TEXT_COLORS) must not\n * be computers — a computer moves its own units — and Remastered will not start a game without a computer, so one slot\n * (P2 here) is a computer that owns no pixel. A pixel is an Overlord, which sits on any ground and on nothing else; keep\n * the screen's tiles clear of buildings. A Pylon needs buildable ground, and none of the map's own units in the way. About 80 pixels made and taken away in a frame did not slow the game.\n */\nimport { centerLocation, createUnit, removeUnitAt, reserveLocation, textColors, units, P1, P3, P4, P5, P6, P7, P8, type Player } from \"trigscript\";\n\n/**\n * Whose units show colours 1 to 7 on the map, in the order of TEXT_COLORS: give each player that colour in the map\n * (cyan, yellow, purple, green, red, blue, orange). Green is P1's — your own units are green on the minimap anyway.\n * Not computers.\n */\nexport const OWNERS: readonly Player[] = [P3, P4, P5, P1, P6, P7, P8];\n/** The same colours in the text lines (`ScreenView`), 0 an empty square: cyan, yellow, purple, green, red, blue, orange. */\nexport const TEXT_COLORS: readonly number[] = [0, textColors.cyan, textColors.yellow, textColors.purple, textColors.green, textColors.red, textColors.blue, textColors.orange];\n\n/** Where each pixel's unit is made: a location the map does not use, taken when the script is built. */\nconst SPOT = reserveLocation(\"screen: where each pixel is made\");\n\n/** A picture known when the script is built, to `blit` onto a screen: its size, and its pixels a row at a time (0 is see-through). */\nexport interface Sprite { readonly width: number; readonly height: number; readonly cells: readonly number[] }\n\n/**\n * A sprite from rows of text, when the script is built: \"1\" … \"7\" a pixel of that colour, anything else see-through.\n *\n * const SHIP = sprite([\".1.\", \"111\", \"1.1\"]);\n */\nexport function sprite(rows: readonly string[]): Sprite {\n const width = Math.max(0, ...rows.map((r) => r.length));\n const cells: number[] = [];\n for (const row of rows) for (let x = 0; x < width; x++) { const c = Number(row[x]); cells.push(c >= 1 && c <= 7 ? c : 0); }\n return { width, height: rows.length, cells };\n}\n\n/** The digits 0 to 9, three pixels wide and five high, for `number()`: each fifteen pixels, a row at a time. */\nconst DIGITS: readonly number[] = [\n \"111101101101111\", \"010110010010111\", \"111001111100111\", \"111001111001111\", \"101101111001001\",\n \"111100111001111\", \"111100111101111\", \"111001001010010\", \"111101111101111\", \"111101111001111\",\n].flatMap((d) => d.split(\"\").map(Number));\n\n/** The most pixels a screen has: 32 × 32 at two tiles a pixel covers a 64 × 64 map, 64 × 32 half of a 128 × 128 one. */\nexport const MAX_PIXELS = 2048;\n\nexport class Screen {\n /** The picture being drawn: a colour a pixel, a row at a time (the first width × height of them). */\n pixels: number[] = new Array(MAX_PIXELS).fill(0);\n /** What the map shows now: the picture as it was at the last `present()`. */\n shown: number[] = new Array(MAX_PIXELS).fill(0);\n /** The pixels drawn a colour they were not since the last `present()`, in order: each one's place and its x and y. */\n drawnAt: number[] = new Array(MAX_PIXELS).fill(0);\n drawnX: number[] = new Array(MAX_PIXELS).fill(0);\n drawnY: number[] = new Array(MAX_PIXELS).fill(0);\n drawn = 0;\n /** More drawn than the list holds: `present()` looks at every pixel instead. */\n overflow = false;\n x: number;\n y: number;\n width: number;\n height: number;\n cell: number;\n /** The unit a pixel is: an Overlord (\"dots\"), or a Pylon (\"solid\"). */\n unit = 0;\n\n /**\n * A screen `width` × `height` pixels (MAX_PIXELS at most), its top left corner on tile (x, y) of the map, a pixel two\n * tiles across.\n * @param look \"dots\" (the default): a pixel is an Overlord, which the minimap draws as a dot, a gap round it — it flies,\n * so the screen may be over anything, Space too. \"solid\": a pixel is a Pylon, which the minimap draws as its whole\n * footprint, two tiles square, so pixels side by side are one block — it needs buildable ground under the screen,\n * and gives its owner Protoss supply. (Overlords a tile apart do not fit: the game will not place most of them.)\n */\n constructor(x: number, y: number, width: number, height: number, look: \"dots\" | \"solid\" = \"dots\") {\n this.x = x;\n this.y = y;\n this.width = width;\n this.height = height;\n this.cell = 64;\n this.unit = look === \"solid\" ? units.ProtossPylon : units.ZergOverlord;\n }\n\n /** Every pixel the colour (0, empty, unless it says). */\n clear(color: number = 0) {\n let i = 0;\n let y = 0;\n while (y < this.height) {\n let x = 0;\n while (x < this.width) { this.put(i, x, y, color); i++; x++; }\n y++;\n }\n }\n\n /** One pixel; nothing happens outside the screen. */\n set(x: number, y: number, color: number) {\n if (x >= 0 && y >= 0 && x < this.width && y < this.height) this.put(y * this.width + x, x, y, color);\n }\n\n /** A pixel written, and listed for `present()` when its colour changed. */\n private put(i: number, x: number, y: number, color: number) {\n if (this.pixels[i] === color) return;\n this.pixels[i] = color;\n if (this.drawn < MAX_PIXELS) {\n this.drawnAt[this.drawn] = i;\n this.drawnX[this.drawn] = x;\n this.drawnY[this.drawn] = y;\n this.drawn++;\n } else this.overflow = true;\n }\n\n /** The colour of a pixel; 0 outside the screen. */\n get(x: number, y: number): number {\n if (x >= 0 && y >= 0 && x < this.width && y < this.height) return this.pixels[y * this.width + x];\n return 0;\n }\n\n /** A filled rectangle, its top left at (x, y); what is outside the screen is left out. */\n rect(x: number, y: number, width: number, height: number, color: number) {\n let j = 0;\n while (j < height) {\n let i = 0;\n while (i < width) { this.set(x + i, y + j, color); i++; }\n j++;\n }\n }\n\n /** A line from (x0, y0) to (x1, y1), both ends drawn. */\n line(x0: number, y0: number, x1: number, y1: number, color: number) {\n const dx = Math.abs(x1 - x0);\n const dy = -Math.abs(y1 - y0);\n const sx = x0 < x1 ? 1 : -1;\n const sy = y0 < y1 ? 1 : -1;\n let err = dx + dy;\n let x = x0;\n let y = y0;\n while (true) {\n this.set(x, y, color);\n if (x === x1 && y === y1) break;\n const e2 = 2 * err;\n if (e2 >= dy) { err += dy; x += sx; }\n if (e2 <= dx) { err += dx; y += sy; }\n }\n }\n\n /** A sprite with its top left at (x, y): its see-through pixels leave what is under them. */\n blit(s: Sprite, x: number, y: number) {\n let k = 0;\n let j = 0;\n while (j < s.height) {\n let i = 0;\n while (i < s.width) {\n const c = s.cells[k];\n if (c !== 0) this.set(x + i, y + j, c);\n k++;\n i++;\n }\n j++;\n }\n }\n\n /** A whole number's digits, 3 × 5 pixels each with a pixel between, its top left at (x, y): a score on the minimap. */\n number(n: number, x: number, y: number, color: number) {\n let rest = n < 0 ? -n : n;\n let count = 1;\n let probe = rest;\n while (probe >= 10) { probe = Math.floor(probe / 10); count++; }\n let place = x + (count - 1) * 4;\n while (true) {\n const d = rest % 10;\n let k = 0;\n while (k < 15) {\n if (DIGITS[d * 15 + k] === 1) this.set(place + (k % 3), y + Math.floor(k / 3), color);\n k++;\n }\n rest = Math.floor(rest / 10);\n place -= 4;\n if (rest === 0) break;\n }\n }\n\n /**\n * Let a player see the screen: Map Revealers of theirs over it, for the rest of the game. Without them its pixels,\n * owned by other players, are hidden by the fog of war — on the map and on the minimap. Once, before the first\n * `present()`.\n * @param player Who sees it: P1 … P8, known when the script is built.\n */\n reveal(player: Player) {\n // A Map Revealer sees ten tiles round; one every six tiles leaves no gap.\n const right = this.x * 32 + this.width * this.cell;\n const bottom = this.y * 32 + this.height * this.cell;\n let py = this.y * 32 + 96;\n while (py < bottom + 96) {\n let px = this.x * 32 + 96;\n while (px < right + 96) {\n centerLocation(SPOT, px, py, 32, 32);\n createUnit(player, units.MapRevealer, 1, SPOT);\n px += 192;\n }\n py += 192;\n }\n }\n\n /**\n * The picture on the map: each pixel that changed since the last time taken away and made again in its colour. Only\n * the pixels drawn since are looked at (one drawn and drawn back is passed over), unless more were drawn than the list\n * holds.\n */\n present() {\n if (this.overflow) {\n let i = 0;\n let y = 0;\n while (y < this.height) {\n let x = 0;\n while (x < this.width) { this.show(i, x, y); i++; x++; }\n y++;\n }\n this.overflow = false;\n } else {\n for (let k = 0; k < this.drawn; k++) this.show(this.drawnAt[k], this.drawnX[k], this.drawnY[k]);\n }\n this.drawn = 0;\n }\n\n /** One pixel on the map as it is in the picture, when it is not already. */\n private show(i: number, x: number, y: number) {\n const now = this.pixels[i];\n const was = this.shown[i];\n if (now === was) return;\n const half = this.cell >> 1;\n centerLocation(SPOT, this.x * 32 + x * this.cell + half, this.y * 32 + y * this.cell + half, 32, 32);\n OWNERS.forEach((owner, k) => { if (was === k + 1) removeUnitAt(owner, this.unit, 1, SPOT); });\n OWNERS.forEach((owner, k) => { if (now === k + 1) createUnit(owner, this.unit, 1, SPOT); });\n this.shown[i] = now;\n }\n}\n\n/**\n * A screen in the text lines, a square a pixel — filled in its colour, or empty — a line a row. For a `ui.lines` view:\n * ``, the screen's size known when the script is built. Eleven lines\n * show at once, sixty squares across.\n */\nexport function ScreenView({ screen, width, height }: { screen: Screen; width: number; height: number }) {\n return <>{UP_TO_60.map((y) => y < height && {UP_TO_60.map((x) => x < width && )})};\n}\n\n/** 0 … 59: the places a view's rows and columns are counted over, the ones past its size left out when the script is built. */\nconst UP_TO_60: readonly number[] = Array.from({ length: 60 }, (_, i) => i);\n", - "signals": "/**\n * Events of your own: the standard library's \"trigscript/signals\" (TrigScript 4).\n *\n * import { Signal } from \"trigscript/signals\";\n *\n * const waveCleared = new Signal();\n *\n * program(async () => {\n * waveCleared.on((wave) => setResources(P1, \"add\", 100 * wave, \"ore\"));\n * waveCleared.once((wave) => print(`First wave down!`));\n * const id = waveCleared.on((wave) => print(`Wave ${wave} cleared`));\n * …\n * waveCleared.emit(3); // every handler, in the order they were added\n * waveCleared.off(id);\n * });\n *\n * A signal lets one part of a map say that something happened without knowing who listens: the shop, the score and the\n * music each add a handler, and the wave code only emits. What it carries is one value of the type it is made with —\n * a number, a boolean, a text or a unit (`new Signal()`). Its handlers are functions kept as values: they may use\n * what the block they were written in has, and they run to their end, one after another, within the frame.\n *\n * Make a signal inside the program that uses it (`const s = new Signal()` at its top): a signal is variables\n * of a program, as anything a program writes is.\n */\n\nexport class Signal {\n /** The handlers, by the number `on` gave; one taken off is undefined, and its place is given to the next one added. */\n handlers: (((value: T) => void) | undefined)[] = [];\n /** Which handlers go once they have run once. */\n onceOnly: boolean[] = [];\n\n /**\n * Add a handler: it runs at every `emit`, handed the value.\n * @returns Its number, for `off`.\n */\n on(handler: (value: T) => void): number {\n return this.add(handler, false);\n }\n\n /** Add a handler that runs at the next `emit` only. @returns Its number, for `off` before then. */\n once(handler: (value: T) => void): number {\n return this.add(handler, true);\n }\n\n /** Take a handler off, by the number `on` or `once` gave. */\n off(id: number) {\n if (id >= 0 && id < this.handlers.length) this.handlers[id] = undefined;\n }\n\n /** Run every handler, in the order they were added, each handed `value`. */\n emit(value: T) {\n let i = 0;\n while (i < this.handlers.length) {\n const h = this.handlers[i];\n if (h) {\n if (this.onceOnly[i]) this.handlers[i] = undefined;\n h(value);\n }\n i++;\n }\n }\n\n /** How many handlers it has. */\n count(): number {\n let n = 0;\n for (const h of this.handlers) if (h) n++;\n return n;\n }\n\n /** Take every handler off. */\n clear() {\n let i = 0;\n while (i < this.handlers.length) { this.handlers[i] = undefined; i++; }\n }\n\n private add(handler: (value: T) => void, once: boolean): number {\n // A place one was taken off from, or a new one at the end.\n let i = 0;\n while (i < this.handlers.length) {\n if (!this.handlers[i]) {\n this.handlers[i] = handler;\n this.onceOnly[i] = once;\n return i;\n }\n i++;\n }\n this.handlers.push(handler);\n this.onceOnly.push(once);\n return this.handlers.length - 1;\n }\n}\n", + "signals": "/**\n * Events of your own: the standard library's \"trigscript/signals\" (TrigScript 4).\n *\n * import { Signal } from \"trigscript/signals\";\n *\n * const waveCleared = new Signal();\n *\n * program(async () => {\n * waveCleared.on((wave) => { setResources(P1, \"add\", 100 * wave, \"ore\"); }); // braces: a handler gives back no action\n * waveCleared.once((wave) => print(`First wave down!`));\n * const id = waveCleared.on((wave) => print(`Wave ${wave} cleared`));\n * …\n * waveCleared.emit(3); // every handler, in the order they were added\n * waveCleared.off(id);\n * });\n *\n * A signal lets one part of a map say that something happened without knowing who listens: the shop, the score and the\n * music each add a handler, and the wave code only emits. What it carries is one value of the type it is made with —\n * a number, a boolean, a text or a unit (`new Signal()`). Its handlers are functions kept as values: they may use\n * what the block they were written in has, and they run to their end, one after another, within the frame.\n *\n * Make a signal inside the program that uses it (`const s = new Signal()` at its top): a signal is variables\n * of a program, as anything a program writes is.\n */\n\nexport class Signal {\n /** The handlers, by the number `on` gave; one taken off is undefined, and its place is given to the next one added. */\n handlers: (((value: T) => void) | undefined)[] = [];\n /** Which handlers go once they have run once. */\n onceOnly: boolean[] = [];\n\n /**\n * Add a handler: it runs at every `emit`, handed the value.\n * @returns Its number, for `off`.\n */\n on(handler: (value: T) => void): number {\n return this.add(handler, false);\n }\n\n /** Add a handler that runs at the next `emit` only. @returns Its number, for `off` before then. */\n once(handler: (value: T) => void): number {\n return this.add(handler, true);\n }\n\n /** Take a handler off, by the number `on` or `once` gave. */\n off(id: number) {\n if (id >= 0 && id < this.handlers.length) this.handlers[id] = undefined;\n }\n\n /** Run every handler, in the order they were added, each handed `value`. */\n emit(value: T) {\n let i = 0;\n while (i < this.handlers.length) {\n const h = this.handlers[i];\n if (h) {\n if (this.onceOnly[i]) this.handlers[i] = undefined;\n h(value);\n }\n i++;\n }\n }\n\n /** How many handlers it has. */\n count(): number {\n let n = 0;\n for (const h of this.handlers) if (h) n++;\n return n;\n }\n\n /** Take every handler off. */\n clear() {\n let i = 0;\n while (i < this.handlers.length) { this.handlers[i] = undefined; i++; }\n }\n\n private add(handler: (value: T) => void, once: boolean): number {\n // A place one was taken off from, or a new one at the end.\n let i = 0;\n while (i < this.handlers.length) {\n if (!this.handlers[i]) {\n this.handlers[i] = handler;\n this.onceOnly[i] = once;\n return i;\n }\n i++;\n }\n this.handlers.push(handler);\n this.onceOnly.push(once);\n return this.handlers.length - 1;\n }\n}\n", "timers": "/**\n * Time in a program: the standard library's \"trigscript/timers\" (TrigScript 4).\n *\n * import { every, after, Cooldown, Countdown, SECOND } from \"trigscript/timers\";\n *\n * program(async () => {\n * const spawner = every(10 * SECOND, (n) => spawnWave(n)); // every ten seconds, the wave's number handed over\n * after(3 * SECOND, () => print(\"Go!\"));\n * const shot = new Cooldown(12);\n * while (true) {\n * if (keyPressed(P1, \"Space\") && shot.use()) fire();\n * if (boss.hp <= 0) spawner.cancel();\n * await nextFrame();\n * }\n * });\n *\n * for await (const b of over(bullets, { per: 50 })) b.x += b.dx; // fifty a frame, on to the next frame after each fifty\n *\n * Time is counted in frames of the game, 24 a second on Fastest (`SECOND`). `every` and `after` run their function in a\n * task beside the program, so the program goes on at once; the Task they give back stops it (`cancel()`) and says\n * whether it still runs (`running`). Their function is kept for the task, with what it uses of the block it was written\n * in — written in a loop, each turn's is its own, as JavaScript has it — and it runs to its end within the frame (it\n * does not wait). Sixteen of one `every` or `after` written once can run at a time. `Cooldown`, `Countdown` and\n * `Stopwatch` read the game's frame counter: no task, nothing to stop.\n */\nimport { game, start, sleep, frames, nextFrame, memory, type Task } from \"trigscript\";\n\n/** Frames a second of game time on Fastest, the speed a map is played at. */\nexport const SECOND = 24;\n\n/** The frames of game time since the game began. */\nexport function now(): number {\n return memory.game.frame;\n}\n\nconst repeating = game(async (period: number, fn: (count: number) => void, times: number) => {\n let count = 0;\n while (times <= 0 || count < times) {\n await sleep(frames(period < 1 ? 1 : period));\n count++;\n fn(count);\n }\n});\n\nconst later = game(async (delay: number, fn: () => void) => {\n await sleep(frames(delay < 1 ? 1 : delay));\n fn();\n});\n\n/**\n * Run `fn` every `period` frames, from `period` frames from now: forever, or `times` times. It is handed how many times\n * it has run, 1 the first time.\n * @param period Frames between one run and the next (24 a second).\n * @param fn What to run; it runs to its end within the frame.\n * @param times How many times, or 0 (the default) for as long as the game lasts.\n * @returns The task: `cancel()` stops it.\n */\nexport const every = game((period: number, fn: (count: number) => void, times: number = 0): Task => start(repeating(period, fn, times)));\n\n/**\n * Run `fn` once, `delay` frames from now.\n * @param delay Frames from now (24 a second).\n * @param fn What to run; it runs to its end within the frame.\n * @returns The task: `cancel()` before then and it never runs.\n */\nexport const after = game((delay: number, fn: () => void): Task => start(later(delay, fn)));\n\n/**\n * The elements of `list`, `per` of them a frame: a loop spread over frames, for work that need not be done in one —\n * a thousand bullets moved, a map's cells looked at. What a frame costs stays what `per` turns cost.\n *\n * for await (const b of over(bullets, { per: 50 })) b.x += b.dx;\n *\n * It reads the list's length as it goes, as `for…of` does: what is pushed meanwhile is reached too, and an element\n * removed before it is reached moves the rest up, so one is passed over, as with an index. Each element is the list's\n * own: what is written to it stays. The loop goes on to the next frame only between elements, never after the last.\n * @param list The array to walk.\n * @param options `per`: how many elements a frame (1 at least).\n */\nexport async function* over(list: T[], options: { per: number }) {\n const per = options.per < 1 ? 1 : options.per;\n let n = 0;\n for (let i = 0; i < list.length; i++) {\n yield list[i];\n n++;\n if (n >= per && i + 1 < list.length) {\n n = 0;\n await nextFrame();\n }\n }\n}\n\n/**\n * 0, 1, … `count` − 1, `per` of them a frame: `over` for a count rather than an array.\n *\n * for await (const y of spread(96, { per: 8 })) drawRow(y); // twelve frames for 96 rows\n * @param count How many numbers, from 0.\n * @param options `per`: how many a frame (1 at least).\n */\nexport async function* spread(count: number, options: { per: number }) {\n const per = options.per < 1 ? 1 : options.per;\n let n = 0;\n for (let i = 0; i < count; i++) {\n yield i;\n n++;\n if (n >= per && i + 1 < count) {\n n = 0;\n await nextFrame();\n }\n }\n}\n\n/**\n * Something that can be done once every so many frames: a weapon's reload, a spell's cooldown.\n *\n * const blink = new Cooldown(5 * SECOND);\n * if (keyPressed(P1, \"B\") && blink.use()) teleport();\n */\nexport class Cooldown {\n period: number;\n until = 0;\n\n /** Ready at once, then once every `period` frames. */\n constructor(period: number) {\n this.period = period;\n }\n\n /** Whether it is ready now. */\n ready(): boolean {\n return now() >= this.until;\n }\n\n /** If it is ready, use it — it is ready again `period` frames from now — and say true; else false. */\n use(): boolean {\n if (now() < this.until) return false;\n this.until = now() + this.period;\n return true;\n }\n\n /** Frames until it is ready again: 0 when it is. */\n left(): number {\n return this.until > now() ? this.until - now() : 0;\n }\n\n /** Ready again at once. */\n reset() {\n this.until = 0;\n }\n}\n\n/**\n * A length of time that runs down while the game runs: a round's clock, a bomb's fuse.\n *\n * const round = new Countdown(90 * SECOND);\n * ui.lines(P1, () => {clock(round.left())});\n * await until(() => round.done());\n */\nexport class Countdown {\n end = 0;\n\n /** Running, done `length` frames from now (0: done already). */\n constructor(length: number = 0) {\n this.end = now() + length;\n }\n\n /** Start again, done `length` frames from now. */\n restart(length: number) {\n this.end = now() + length;\n }\n\n /** Frames left: 0 once it is done. */\n left(): number {\n return this.end > now() ? this.end - now() : 0;\n }\n\n /** Whether it has run out. */\n done(): boolean {\n return now() >= this.end;\n }\n\n /** More time, or less (a number below zero). */\n add(frames: number) {\n this.end += frames;\n }\n}\n\n/** How long since it was made, or last reset: a lap's time, a survival score. */\nexport class Stopwatch {\n from = 0;\n\n constructor() {\n this.from = now();\n }\n\n /** Frames since it started. */\n elapsed(): number {\n return now() - this.from;\n }\n\n /** From 0 again. */\n reset() {\n this.from = now();\n }\n}\n", "ui": "/**\n * A kit for the game's text lines: the standard library's \"trigscript/ui\" (TrigScript 4).\n *\n * import { Meter, Panel, MenuList, DialogBox, choose, say, clock } from \"trigscript/ui\";\n *\n * const ITEMS = [\"Marine (50)\", \"Medic (75)\", \"Leave\"];\n * const shop = { selected: 0, open: false };\n * const talk = { speaker: \"\", text: \"\", open: false };\n *\n * program(async () => {\n * ui.lines(P1, () => (\n * <>\n * \n * \n * \n * {shop.open && }\n * \n * \n * ));\n * await say(P1, talk, \"Raynor\", \"Hold the bridge until dawn.\");\n * const pick = await choose(P1, ITEMS.length, shop);\n * });\n *\n * The text lines are one player's: a view is shown to that player only, rebuilt when what it reads changes (a function\n * it calls included) and shown again once a second as it was, so a menu, a dialog and a meter cost nothing while nothing\n * changes. Give a player one `ui.lines` and\n * put everything in it, as the example does: the components are pieces of one view, and the state they show is a\n * record of yours (`shop`, `talk`) that the functions here change.\n *\n * What the text lines can show, played in StarCraft: Remastered (the UI spike, 2026-10-16): eleven lines; colour codes\n * take no width; ■ □ ● are one width; after grey on a line every later colour code is ignored, so grey goes last.\n */\nimport { keyPressed, nextFrame, sleep, frames, game, textColors, type Player, type TextColor } from \"trigscript\";\n\n/** Frames a second of game time on Fastest, the speed a map is played at. */\nconst PER_SECOND = 24;\n\n/** \"1:05\": frames of game time as minutes and seconds (24 frames a second, on Fastest). */\nexport function clock(frames: number): string {\n const seconds = Math.floor(frames / PER_SECOND);\n return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, \"0\")}`;\n}\n\n/** \"12,345\": a whole number with its thousands marked. */\nexport function thousands(n: number): string {\n let rest = n < 0 ? -n : n;\n let out = \"\";\n while (rest >= 1000) {\n out = `,${String(rest % 1000).padStart(3, \"0\")}${out}`;\n rest = Math.floor(rest / 1000);\n }\n return `${n < 0 ? \"-\" : \"\"}${rest}${out}`;\n}\n\n/**\n * A labelled bar on a line of its own — \"Base ■■■■■■□□□□ 900/1500\" — green above half, yellow above a quarter, red below.\n * @param label What the bar is of.\n * @param value How much there is.\n * @param max How much there can be.\n * @param width How many squares the bar is (10 unless it says).\n */\nexport function Meter({ label, value, max, width }: { label: string; value: number; max: number; width?: number }) {\n return (\n \n {label} max ? textColors.green : value * 4 > max ? textColors.yellow : textColors.red} /> {value}/{max}\n \n );\n}\n\n/**\n * A title over what is inside it: \"-- Wave 3 --\" in its colour (yellow unless it says), then the lines inside.\n * @param title The title.\n * @param color The title's colour.\n */\nexport function Panel({ title, children, color }: { title: string; children?: Children; color?: TextColor }) {\n return (\n <>\n -- {title} --\n {children}\n \n );\n}\n\n/**\n * A menu's lines: each item on a line of its own, the one at `selected` marked \"> \" and in its colour (cyan unless it\n * says). `items` is a list the script has, or an array of texts of the program.\n */\nexport function MenuList({ items, selected, color }: { items: readonly string[]; selected: number; color?: TextColor }) {\n return (\n <>\n {items.map((item, i) => (\n \n {i === selected ? \"> \" : \" \"}\n {item}\n \n ))}\n \n );\n}\n\n/**\n * Let a player choose one of `count` things: the menu opens (`menu.open`), W and S (or Up and Down) move\n * `menu.selected` (round from the last to the first), and Space chooses — the place chosen, once it is. (Not Enter:\n * in a game of several players Enter opens the chat.) Show it with `MenuList`, in the\n * player's `ui.lines`: `{shop.open && }`.\n * @param player Who chooses: P1 … P8, known when the script is built.\n * @param count How many things there are to choose from.\n * @param menu The record a view shows: which is marked, and whether the menu is open.\n */\nexport const choose = game(async (player: Player, count: number, menu: { selected: number; open: boolean }): Promise => {\n menu.open = true;\n while (true) {\n if (keyPressed(player, \"W\") || keyPressed(player, \"Up\")) menu.selected = (menu.selected + count - 1) % count;\n if (keyPressed(player, \"S\") || keyPressed(player, \"Down\")) menu.selected = (menu.selected + 1) % count;\n if (keyPressed(player, \"Space\")) break;\n await nextFrame();\n }\n menu.open = false;\n return menu.selected;\n});\n\n/**\n * A text put into `into.text` a character at a time, one every `every` frames (2 unless it says): a view that shows\n * `into.text` shows it being typed. Done when the whole text is there.\n */\nexport const typewriter = game(async (into: { text: string }, text: string, every: number = 2) => {\n let n = 0;\n while (n < text.length) {\n n++;\n into.text = text.slice(0, n);\n await sleep(frames(every));\n }\n});\n\n/**\n * Someone says something to a player: the dialog opens (`box.open`), the speaker's name and the words typed in, and it\n * closes when the player presses Space. Show it with `DialogBox`.\n * @param player Who reads it: P1 … P8, known when the script is built.\n * @param box The record a view shows.\n * @param speaker Who speaks.\n * @param text What they say.\n */\nexport const say = game(async (player: Player, box: { speaker: string; text: string; open: boolean }, speaker: string, text: string) => {\n box.open = true;\n box.speaker = speaker;\n box.text = \"\";\n await typewriter(box, text);\n while (!keyPressed(player, \"Space\")) await nextFrame();\n box.open = false;\n});\n\n/** A dialog's lines while it is open: the speaker in yellow, the words, and what to press (Space). Nothing while it is closed. */\nexport function DialogBox({ box }: { box: { speaker: string; text: string; open: boolean } }) {\n return box.open ? (\n <>\n {box.speaker}\n {box.text}\n (Space)\n \n ) : null;\n}\n\n/** A line at the end of a log that keeps the last `keep` (6 unless it says), the oldest taken off the front. */\nexport const log = game((lines: string[], text: string, keep: number = 6) => {\n lines.push(text);\n while (lines.length > keep) lines.shift();\n});\n\n/** A log's lines, oldest first, each in its colour (white unless it says). */\nexport function LogLines({ lines, color }: { lines: readonly string[]; color?: TextColor }) {\n return <>{lines.map((text) => {text})};\n}\n" }; diff --git a/compiler/structured.ts b/compiler/structured.ts index 913a173..f8e2956 100644 --- a/compiler/structured.ts +++ b/compiler/structured.ts @@ -9940,21 +9940,25 @@ export class Structured { return fn; } - /** A name for a function kept as a value: its own, or what holds it where it is written. */ + /** + * A name for a function kept as a value: its own, or what holds it where it is written. Read from the file the + * function is written in, which need not be the body's: a handler written in main.ts reaches a library's body. + */ private functionName(fn: FnValue): string { const { ts } = this; + const sf = fn.decl.getSourceFile(); if (fn.name) return fn.name; const p = fn.decl.parent; if (p && (ts.isVariableDeclaration(p) || ts.isPropertyAssignment(p) || ts.isPropertyDeclaration(p)) && ts.isIdentifier(p.name)) return p.name.text; - if (p && ts.isBinaryExpression(p) && p.right === fn.decl) return p.left.getText(this.body.sf); + if (p && ts.isBinaryExpression(p) && p.right === fn.decl) return p.left.getText(sf); // `return (x) => x + k`: the function its maker returns. for (let up: TS.Node | undefined = p; up; up = up.parent) { if (ts.isReturnStatement(up) || (ts.isArrowFunction(up) && up.body === fn.decl)) continue; - if ((ts.isFunctionDeclaration(up) || ts.isMethodDeclaration(up)) && up.name) return `the function ${up.name.getText(this.body.sf)} returns`; + if ((ts.isFunctionDeclaration(up) || ts.isMethodDeclaration(up)) && up.name) return `the function ${up.name.getText(sf)} returns`; if (ts.isArrowFunction(up) && ts.isVariableDeclaration(up.parent) && ts.isIdentifier(up.parent.name)) return `the function ${up.parent.name.text} returns`; if (!ts.isParenthesizedExpression(up) && !ts.isBlock(up) && !ts.isReturnStatement(up)) break; } - if (p && ts.isCallExpression(p) && p.arguments.includes(fn.decl as TS.Expression)) return `the function handed to ${p.expression.getText(this.body.sf)}`; + if (p && ts.isCallExpression(p) && p.arguments.includes(fn.decl as TS.Expression)) return `the function handed to ${p.expression.getText(sf)}`; return "the function"; } @@ -9982,7 +9986,7 @@ export class Structured { for (const p of fn.parameters) { const k = this.valueKind(this.c.checker.getTypeAtLocation(p.name)); if (p.dotDotDotToken || !ts.isIdentifier(p.name) || !k) { - this.c.error(p, `${name} is kept as a value, so it is called through its number, and its parameters are numbers, booleans, texts, units or other functions — ${p.name.getText(this.body.sf)} is not one of those.`); + this.c.error(p, `${name} is kept as a value, so it is called through its number, and its parameters are numbers, booleans, texts, units or other functions — ${p.name.getText(p.getSourceFile())} is not one of those.`); return null; } params.push(k); @@ -9990,7 +9994,8 @@ export class Structured { const returned = this.c.checker.getReturnTypeOfSignature(signature); const result: ValueKind | "void" = (returned.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined)) !== 0 ? "void" : this.valueKind(returned) ?? "void"; if (result === "void" && (returned.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Never)) === 0) { - this.c.error(node, `${name} is kept as a value, so it is called through its number, and it returns a number, a boolean, a text, a unit, another function or nothing — not ${this.c.checker.typeToString(returned)}.`); + // (Said at the function itself: where it is kept may be a library's line, far from the code to change.) + this.c.error(fn.decl, `${name} is kept as a value, so it is called through its number, and it returns a number, a boolean, a text, a unit, another function or nothing — not ${this.c.checker.typeToString(returned)}.`); return null; } const target = fn.target as Body; diff --git a/stdlib/signals.ts b/stdlib/signals.ts index dd104bc..e8d72ce 100644 --- a/stdlib/signals.ts +++ b/stdlib/signals.ts @@ -6,7 +6,7 @@ * const waveCleared = new Signal(); * * program(async () => { - * waveCleared.on((wave) => setResources(P1, "add", 100 * wave, "ore")); + * waveCleared.on((wave) => { setResources(P1, "add", 100 * wave, "ore"); }); // braces: a handler gives back no action * waveCleared.once((wave) => print(`First wave down!`)); * const id = waveCleared.on((wave) => print(`Wave ${wave} cleared`)); * … diff --git a/tests/function-values.test.ts b/tests/function-values.test.ts index 5607831..46823ed 100644 --- a/tests/function-values.test.ts +++ b/tests/function-values.test.ts @@ -111,4 +111,8 @@ describe("what a function kept as a value may do, and not", () => { expect(messages("let f: () => Promise = async () => { await sleep(frames(1)); }; await f();").join("\n")).toMatch(/async/); expect(messages("let f = (xs: number[]) => xs.length; let n = 0; n = f([1]);").join("\n")).toMatch(/its parameters are numbers, booleans, texts, units or other functions/); }); + it("give back an action: said at the function, by the name of what it was handed to, also when a library keeps it", () => { + const r = compileScript(ts, { "main.ts": `import { Signal } from "trigscript/signals";\nprogram(() => {\n const cleared = new Signal();\n cleared.on((wave) => setResources(P1, "add", wave, "ore"));\n cleared.emit(1);\n});` }, NAMES, { lib: LIB }); + expect(r.diagnostics.map((d) => [d.file, d.line, d.message.slice(0, 54)])).toEqual([["main.ts", 4, "the function handed to cleared.on is kept as a value, "]]); + }); });