# Reel.sound: the builders' cheat sheet

Every sound in the library is synthesized live with the Web Audio API. The one exception is a reel's **narration**, a recorded voice track the engine mixes in (see Narration below); your `sound(cue, k)` never deals with it.

- You write a `sound(cue, k)` function in your piece. It runs once, up front, to collect events.
- The engine expands them into a sorted score. The player plays that score, and it can seek anywhere.
- Test bench: `dev/sound.html` has a button for every effect, instrument, bed and ambience, plus a demo score.
- Lab: `node tools/sound-lab.mjs` renders everything offline and measures loudness, peaks and spectrograms.

```js
sound(cue, k) {
  cue.bed(0, 24, 'noir', { gain: 0.3 });             // a quiet music bed for the whole reel
  cue.amb(0, 24, 'rain', { gain: 0.15 });            // a looping ambience under it
  cue(1.2, 'whoosh', { dur: 0.5 });                  // effects on visual events
  cue(4.5, 'thud');
  cue.duck(4.4, 6.0, 0.5);                           // dip the bed and ambience under the moment
  for (let i = 0; i < 12; i++) cue(8 + i * 0.11, 'key', { gain: 0.6 });  // typing
  cue(9.5, 'return');
  cue.chord(14, 'Am9', { inst: 'rhodes', dur: 2, strum: 0.03 });
  cue.seq(18, 'E5 G5 A5 - C6 . . .', { inst: 'celesta', step: 0.2 });
}
```

All times are in piece seconds. All randomness is seeded, so the same piece always sounds the same.

## The cue API

| Call | What it does |
|---|---|
| `cue(t, name, opts)` | An **effect**. opts: `{ gain=1, pitch=0 (semitones), pan=0 (-1..1), dur, tone=0.5 (0 dark .. 1 bright), seed, variant, count, send, vary }` |
| `cue.note(t, note, opts)` | An **instrument note**. `note` is `'C4'`, `'F#3'`, `'Bb2'`, a MIDI number or `{ hz }`. opts: `{ inst='piano', dur=0.5, gain=0.6, pan=0, vel=0.7, tone=0.5 }`, plus per-instrument extras |
| `cue.chord(t, notes, opts)` | A **chord**. `notes` is a chord name (`'Cmaj7'`, `'Dm9'`, `'G7'`, `'Am'`, `'F/A'`, `'Bbsus4'`, `'C#m7b5'`), an array of notes, or a string of notes (`'C4 E4 G4'`). opts are as for a note, plus `strum` (seconds between notes; negative strums downward) and `octave` (the root's octave, default 4). The gain is shared across the notes, so a chord is about as loud as one note. |
| `cue.seq(t0, pattern, opts)` | A **sequence**. Whitespace-separated steps: a note, a MIDI number, a chord name, `C4+E4` for stacked notes, `-` to hold the last note one more step, or `.` for a rest. opts: `{ step=0.25, inst, gain, dur }`. It **returns the end time**. |
| `cue.bed(t0, t1, style, opts)` | A **music bed** from t0 to t1. opts: `{ gain=0.3, bpm, key='C', seed=1, fadeIn=1, fadeOut=1.5, swing, parts, mute }` |
| `cue.amb(t0, t1, name, opts)` | A looping **ambience**. opts: `{ gain=0.2, fadeIn=0.6, fadeOut=0.8, seed, tone, pitch }` |
| `cue.duck(t0, t1, amount=0.5)` | Ducks beds and ambiences: down by `amount` 0.12 s before t0, held, then back up over 0.4 s after t1. Overlapping ducks take the deepest. |
| `cue.hz(note)`, `cue.midi(note)` | Helpers: a note's frequency or MIDI number. |

How the opts work:
- **Variation.** Each repeat of an effect gets a slightly different pitch and noise grain, from a seed derived from its name and its count. Pass `seed` to pin one take, or `vary: 0` to remove the pitch wobble.
- **Bad input.** Unknown names and bad times are skipped and listed in `compiled.warnings`, so check them with `tools/check.mjs`.
- **Chord parsing.** These qualities are understood: maj, M, m, min, -, 5, 6, m6, 69, 7, maj7, M7, m7, mmaj7, 9, maj9, m9, add9, madd9, 11, m11, 13, maj13, m13, sus2, sus4, 7sus4, 9sus4, dim, dim7, m7b5 (ø), aug (+), 7#5, 7b9, 7#9, 7#11, 7b5, maj7#11 and 7alt, plus slash basses.

## Levels at a glance

| Layer | Default gain | Level at the default |
|---|---|---|
| Effect | 1 | peaks −4.7 to −3.4 dBFS |
| Instrument note | 0.6 | a phrase sits around −20 LUFS |
| Bed | 0.3 | −24 LUFS |
| Ambience | 0.2 | −30 LUFS |

- The master chain is a gentle glue compressor, then a limiter, then a soft ceiling at about −1 dBFS. You can't clip it, but you can squash it, so leave headroom.
- A typical busy piece lands around −17 to −18 LUFS integrated. A piece with more or louder hits gets close to −16.

## Effects

Each effect is listed with what it sounds like. *Variants* go in `variant: '...'`; other useful opts are named. `dur` matters only where it's mentioned.

**Clicks and taps**
- `tick`: a clock tick, bright and woody. Variant `tock` is the lower half of the pair.
- `click`: a switch or mouse click.
- `tap`: a fingertip on a wooden table.
- `pop`: a bubbly cork pop that rises.
- `blip`: a soft triangle-wave UI blip at A5. `pitch` picks the note.
- `beep`: a 1 kHz electronic beep. Use `dur` (0.16), and `tone` above 0.6 for a squarer, more radio-like beep (Sputnik).

**Bells**
- `chime`: a bright two-note notification chime. Variant `single` gives one strike.
- `bell`: a bronze bell on A4 with an inharmonic hum, tierce and nominal. It rings about 4 s.
- `ding`: a service-desk bell.
- `gong`: a big tam-tam. Its upper partials bloom after the strike, and it rings about 6 s.

**Impacts**
- `thud`: a dull, heavy thud (a book on a desk).
- `boom`: a deep cinematic boom: a sub drop, a rumble and a crack.
- `hit`: a punchy trailer hit with a metallic tail.
- `slam`: a door slam with a frame rattle and a room.

**Air**
- `whoosh`: a pass-by swoosh whose band sweeps up and down. It pans left to right. `dur` (0.6); `pan` offsets it.
- `swish`: a quick, thin swish (a sword, cloth, a flick). `dur` (0.26).
- `riser`: a tension riser. Noise and a saw climb two octaves to a peak at the end. `dur` (2).
- `drop`: a downlifter, the opposite of the riser: a falling sweep. `dur` (1). Variant `water` is a single water droplet.
- `sparkle`: a magic shimmer, a cascade of high pentatonic pings. `dur` (0.7).

**Paper and pens**
- `paper`: a sheet handled or crinkled. `dur` (0.45).
- `page`: a page turn: a swoosh and a soft flop.
- `pen`: a ballpoint scribble of a few strokes. `dur` (0.5).
- `pencil`: graphite on paper, grainier and higher. `dur` (0.5).
- `chalk`: chalk on slate. Gritty strokes, each starting with a tap, and the odd squeak. `dur` (0.6) is about one word.
- `marker`: a felt marker squeaking on a whiteboard. `dur` (0.45).

**Typing**
- `key`: a typewriter key, with a click, the type-bar clack and the body. Variant `modern` is a computer keyboard (good for terminals). Space keys 0.08–0.14 s apart at `gain` 0.5–0.8.
- `return`: a typewriter carriage return: the bell, the ratchet zip and the clunk. Variant `modern` is an Enter key.
- `stamp`: a rubber stamp: the pad thump, the paper slap and the squish.

**Camera and money**
- `shutter`: a camera shutter (two clicks and the mirror). Variant `film` adds the wind-on lever.
- `coin`: a coin dropped on a table, with two bounces.
- `chip`: two clay poker chips clacking. Variant `stack` riffles a stack.
- `card`: a card dealt onto felt. Variant `shuffle` is a riffle shuffle.

**Split-flap**
- `flap`: one module flipping, a plastic snap.
- `clatter`: a whole board cascading, dense then thinning as modules settle. `dur` (0.9).

**Noise textures**
- `static`: radio static with a faint whistle. Variant `tuning` sweeps across the band. `dur` (0.8).
- `crackle`: a burst of vinyl or fire crackle. `dur` (1).
- `buzz`: a neon or mains buzz with flicker. Variant `zap` is an electrical spark. `dur` (0.6).

**Water and weather**
- `wind`: one gust, with a whistle. `dur` (2).
- `waves`: one wave breaking and washing out. `dur` (3).
- `splash`: something dropped into water: the impact, the spray and the bubbles.

**Kitchen**
- `sizzle`: frying in a pan. `dur` (1.5).
- `crunch`: biting crisp toast.
- `slice`: a knife slicing through, then the board.
- `pour`: liquid pouring; the pitch rises as the vessel fills. `dur` (2).
- `steam`: steam hissing (an espresso wand or a kettle). `dur` (1.5).

**Engines and craft**
- `rumble`: a low rumble. `dur` (2).
- `rocket`: a rocket's ignition and roar with exhaust crackle. `dur` (4).
- `engine`: a car engine revving. `dur` (1.5).
- `sonar`: a sonar ping with two echoes.

**Drums and bass**
- `heartbeat`: a lub-dub.
- `kick`: a kick drum. Variants: `soft` (feathered, for lofi and jazz), `808` (a long boom) and `chip` (8-bit).
- `snare`: a snare. Variants: `brush` (a tap), `swirl` (a brush sweep; `dur`), `rim` (a cross-stick), `gated` (the big 80s snare), `chip` (8-bit) and `roll` (a buzz roll that crescendos; `dur`).
- `hat`: a closed hi-hat. Variants: `open`, `pedal` (the foot chick), `ride` (a ride cymbal with stick ping), `bell` (the ride bell), `crash`, `shaker`, `tamb` (tambourine) and `chip`.
- `clap`: a hand clap (layered). Variant `single` is one clap.
- `bass`: a sub-bass note hit, default C2. Use `note: 'E1'` or `pitch`.

**Mechanisms**
- `marble`: a marble rolling along a track. Variant `click` is two glass marbles colliding. `dur` (1).
- `spring`: a cartoon boing.
- `squeak`: a rusty hinge. Variant `toy` is a rubber toy. `dur` (0.45).
- `ratchet`: a ratchet winding, about 22 clicks a second. `dur` (0.6).
- `gavel`: a gavel on its block, with a courtroom echo. `count` sets the strikes, about 0.38 s apart.
- `glass`: two glasses clinking. Variant `break` is a shatter.
- `bubble`: a bubble bloop. `count` gives a string of them.
- `toaster`: a toaster popping: the latch, the spring twang, the chassis and the toast flying up. Variant `down` is the lever pushed down.

**Animals and voices**
- `crowd`: a crowd murmur that swells and fades. Variant `gasp`. `dur` (2).
- `cheer`: applause with cheering. `dur` (2.5).
- `horn`: a two-tone car horn. Variants: `ship` (a foghorn) and `air` (an air horn). `dur` (0.5).
- `whistle`: a referee's pea whistle. Variants: `wolf` (the two-note whistle), `slide` (a slide whistle) and `train` (a steam whistle). `dur` (0.5).
- `goat`: a goat bleating "meh-eh-eh". Each repeat varies the pitch, so a few cues make a herd. `dur` (0.8).
- `morse`: a Morse code tone at 700 Hz. `text` ('SOS') and `wpm` (20).

## Instruments (`cue.note`, `cue.chord`, `cue.seq`)

| inst | Sound | Extras |
|---|---|---|
| `piano` | Acoustic piano: two detuned strings, hammer noise, brightness falling as it decays | `vel` for brightness |
| `rhodes` | An FM electric piano with a tine bark | `wobble: 1` for a lofi pitch drift |
| `pluck` | A Karplus-Strong plucked string (a nylon guitar) | `variant`: `guitar`, `harp`, `lute`, `synth` (a subtractive synth pluck) |
| `bass` | A warm synth bass: saw and square through a filter sweep, plus a sub | `tone` |
| `upright` | A pizzicato upright bass: a plucked string with a thump | |
| `bell` | A tuned bell with inharmonic partials; it rings past `dur` | |
| `celesta` | A celesta or music box | |
| `organ` | A drawbar organ with a key click | `variant: 'reed'` for a reedy drone (harmonium, hurdy-gurdy) |
| `brass` | A brass section: detuned saws, a filter swell and a scoop | `fall: true` for a big-band fall-off |
| `trumpet` | A solo trumpet with vibrato | `mute: true` for a Harmon mute (noir); `fall` |
| `strings` | A string ensemble pad | `attack` (0.3), `release` (0.5) |
| `flute` | A flute with breath and delayed vibrato | `variant: 'recorder'` |
| `square` | A chiptune pulse | `wave`: `square`, `pulse25`, `pulse12`, `tri`; `vib` |
| `saw` | A sawtooth synth lead or arp | `attack`, `release` |
| `sine` | A pure sine | `attack`, `release` |
| `marimba` | A wooden marimba bar | |
| `koto` | A koto: a bright plucked string with a little bend | |

## Beds (`cue.bed`)

- Beds expand at compile time into notes and drum hits. They're deterministic for a given `seed`, which picks the progression and the tune.
- `key` sets the tonic, e.g. `'D'`, `'Bb'` or `'F#'`. Add `m` or `minor` (`'Am'`), `major`, or a mode name such as `'D dorian'` to change the mode. Most styles have a minor and a major set of changes.
- `bpm` overrides the tempo. Beat n falls at `t0 + n * 60 / bpm`, so you can line visual beats up with it.
- `parts: ['ride', 'bass']` keeps only those parts, and `mute: ['lead']` drops parts. The part names are below.
- The bed's level and fades sit on its own fader, and ducks apply on top.

| Style | Tempo | Mode | Feel | Parts |
|---|---|---|---|---|
| `noir` | 72 | minor | A dark string pad on a line cliché (m, m(maj7), m7, m6), a muted trumpet sighing in harmonic minor, a two-feel upright and soft brushes | `pad, bass, brushes, lead` |
| `synthwave` | 104 | minor | A 16th-note saw arpeggio, a wide pad, octave-pumping bass, kick on 1 and 3 and a gated snare on 2 and 4 | `pad, arp, bass, drums, hats` |
| `chiptune` | 140 | major | 25% pulse arps, a square lead, a triangle bass and noise drums | `arp, lead, bass, drums` |
| `jazz` | 120 | major | ii–V–I changes: a walking upright, ride "spang-a-lang", hi-hat on 2 and 4, Rhodes comping in rootless voicings, swing 0.6 | `bass, ride, hat, snare, comp` |
| `swing` | 132 | major | Big-band brass riffs and stabs (with a fall every 8 bars), a swung ride, a walking bass on every beat, a feathered kick, rim on 2 and 4 and piano plinks, swing 0.66 | `bass, ride, hat, kick, snare, brass, piano` |
| `march` | 116 | major | A snare with accents and rolls into the bar, bass drum on 1 and 3, cymbals on 2 and 4, an oom-pah tuba and a trumpet tune | `snare, drums, tuba, chords, melody` |
| `calm` | 70 | major | Slow maj9 and add9 string pads, a high sine air, a sub, a few soft piano notes | `pad, air, bass, piano` |
| `ambient` | 60 | major | An evolving drone (a sine tonic and a fifth), slow re-voiced pads, sparse bell and celesta shimmer | `drone, pad, shimmer` |
| `lofi` | 82 | major | Wobbly Rhodes maj7 and m9 chords, a warm bass, a lazy late snare, soft kicks, vinyl crackle, 16th swing | `keys, bass, drums, hats, vinyl` |
| `tension` | 100 | minor | A pulsing 8th-note bass ostinato, a high minor-second string cluster, a soft heartbeat kick, ticking hats, a boom every 4 bars and a riser that peaks exactly at t1 | `pulse, strings, heart, ticks, hits, riser` |
| `bright` | 116 | major | Syncopated plucked-guitar chord stabs, a marimba tune, bass, claps on 2 and 4 and a 16th shaker | `plucks, melody, bass, drums, shaker` |
| `folk` | 72 (12/8 lilt) | major (pentatonic tune) | A fingerpicked guitar in triplets, a flute tune, a frame-drum tap | `guitar, flute, drum` |
| `sea` | 66 | major | Waves and gusting wind with a sparse harp and a faint pad | `waves, wind, harp, pad` |
| `space` | 72 | major (II major, lydian) | Shimmering string and sine pads, a sub, telemetry blips and celesta twinkles | `pad, shimmer, sub, blips, twinkle` |
| `medieval` | 92 (3/4) | dorian | A reedy tonic-and-fifth drone, lute plucks, a recorder tune, a frame drum (dum, tek tek) | `drone, lute, recorder, drum` |
| `clockwork` | 120 | major | Tick-tock on the beat, a celesta music-box tune and arpeggios, a marimba bass and gear ratchets | `ticks, melody, arp, bass, gears` |

**Swing** (`jazz`, `swing`, `lofi`, and `noir`'s brushes):
- `swing` takes `0` for straight eighths, `0.66` for a triplet feel (long-short, 2:1), or `1` for dotted (3:1). Lofi swings 16ths.
- It can also step in time, as `[[t, value], ...]` in piece seconds. "What Makes It Swing" can play one continuous bed that turns from straight to swung at 10 s:

```js
cue.bed(0, 24, 'swing', { swing: [[0, 0], [10, 0.66]], parts: ['ride', 'bass', 'hat', 'snare'] });
```

## Ambiences (`cue.amb`)

They're all stereo, seamless and seekable. Gusts, swells, drips and clinks come from the seed.

| Name | Sound |
|---|---|
| `room` | A quiet room tone, the "silence" of a space |
| `rain` | Steady rain with patter and the odd drip |
| `wind` | Gusting wind, with a whistle on strong gusts |
| `waves` | A low surf wash with a wave breaking every 4–8 s |
| `crowd` | A large-room murmur |
| `cafe` | A warmer murmur, cups and spoons, and now and then the espresso wand |
| `vinyl` | Record crackle, hiss, rumble and a soft tick each revolution |
| `hiss` | Tape hiss |
| `hum` | Mains hum (`hz: 60`, or 50 in Europe) with a little buzz |
| `fire` | A crackling fireplace: the roar, the crackle and wood snaps |
| `night` | Crickets (three of them, panned) over soft night air |
| `engine` | A steady engine idle (`hz` sets the firing rumble) |
| `machine` | A rhythmic factory loop (`bpm`, default 75): thud, ratchet, hiss, tick |

## Mixing tips

**Effects**
- Effects at `gain` 1 are "full". Most should sit at 0.4–0.8; save 1–1.5 for the one or two hits that matter.
- Don't fire more than three or four effects on the same frame. Offsetting them by 20–60 ms sounds more natural and keeps the limiter quiet.

**Beds and ambiences**
- A bed is wallpaper. Stay between 0.2 and 0.35, use one bed per reel (or two that crossfade), and let it fade out about 1.5 s before `duration`, so the still hold is calm.
- Ambiences sit under everything at 0.1–0.25. Pair one with a bed only when the bed is sparse (`calm`, `ambient`, `space`).

**Ducking**
- Duck under big moments: `cue.duck(t - 0.1, t + 1.2, 0.5)` around a `boom`, a `gong` or a reveal.
- Use 0.3 for a subtle dip and 0.7 to all but clear the stage.

**Matching the picture**
- Match the medium: chalkboards get `chalk`, `tap` and `room`; terminals get `key` (`modern`) and `hum`; split-flap boards get `flap` and `clatter`.
- Every visual event that would make a sound in that medium should get one. Things that wouldn't make a sound get none.
- To tie music to the picture, use the bed's tempo: for `jazz` at 120 bpm, beat n is at `t0 + n * 0.5`.

**Keep it tasteful**
- Nothing harsh: `tone` 0.3–0.5 darkens bright effects for quiet reels.
- Repeated effects vary on their own; pass the same `seed` only when you want identical takes.

## Narration

Most reels have a short voiceover: `voice/<id>.mp3` (mono, the length of the piece, aligned to t = 0, silent between lines, levelled to −16 LUFS) and `voice/<id>.json` (`{ voice, engine, lines: [{ at, end, text }] }`). `tools/voice.mjs` makes both from `content/narration/<id>.json`; `content/narration/CASTING.md` has the cast and the rules.

**What it does to the mix**
- **The bed.** While a narration plays, music, effects, ambience and reverb all sit `bed` dB lower for the whole piece (default −9 dB, roughly half as loud to the ear), so the voice leads. Without a narration nothing changes.
- **The duck.** Each line also dips music and ambience (and their reverb send) by `duck` (default 0.7, so about −3 dB more) over a window from `at − 0.12` to `end + 0.3`, with the usual duck shape around it: down over 0.12 s before the window, back up over 0.4 s after. So the bed is already down when the first word lands and stays down through the last word's tail.
  - Voice ducks merge with your own `cue.duck` calls; where they overlap, the deepest wins.
  - **Effects are never ducked.** A big hit under a line will fight the voice, which is why the narration scripts leave room for a reel's signature sounds. Don't add ducks for the narration yourself.
- **The voice** joins the master's input after the duck stage, so nothing ever ducks it. It shares the glue compressor and the limiter with everything else (and their 12 ms look-ahead, so it stays in sync with the picture). A mono track goes to both sides at −3 dB, the equal-power centre every other sound uses, so the −16 LUFS track still measures −16 LUFS in the stereo mix.
- **By the numbers** (the noir bed under a test track, measured offline): music and ambience sit 6.0 dB lower under a line once the duck has settled (5.7 dB averaged over whole lines, since reverb from before the duck rings on for a moment). The voice lands about 12–13 dB above the ducked bed. Between lines, the mix is exactly what it is without narration.

**In the player** narration is on by default, but it plays only when sound is on. The bar gains a Narration toggle (N) and a CC captions toggle (C), both remembered on the device. The bar's sound toggle (M) still silences everything. Gallery previews stay silent.

**API**
- `Reel.sound.loadVoice(url)` → `Promise<AudioBuffer>`: fetch and decode, cached by URL. It works before `unlock()`: with no live context yet it decodes through a small 48 kHz OfflineAudioContext.
- `Reel.sound.compile(def, k, { voice: { buffer, lines, gain = 1, duck = 0.7, bed = −9 } })`: the same score plus the voice. `lines` are `[{ at, end }]` in piece seconds. The result keeps it as `compiled.voice`, and its `ducks` include the lines (`duckList` entries marked `voice: true`).
- `play(compiled, fromT)` starts the voice with the session, at offset `fromT` (if `fromT` is before the track's end). `stop()` fades it with everything else. `render(compiled)` mixes it the same way, sample-aligned (a render from a seek point matches the full render exactly). Two renders are still bit-identical.
- Without `opts.voice`, compile, play and render are exactly what they were: the same events, the same graph, the same bits.
- `Reel.sound.voiceDefaults` is `{ lead: 0.12, tail: 0.3, duck: 0.7, gain: 1, bed: -9 }`.

## Engine API (player and tools)

- `Reel.sound.compile(def, k, opts)` returns `{ events, duration, faders, ducks, duckList, warnings }`, plus `voice` when `opts.voice` is given (see Narration).
  - `events` is sorted by `t`. Each event is plain data, such as `{ t, type: 'fx'|'note'|'amb', name, len, end, gain, ... }`, and every `name` is one of the public names above.
  - Compile is pure: no `Math.random`, `Date` or `performance.now`. It takes about 0.5 ms for a 30 s piece with a bed.
- `Reel.sound.unlock()` creates or resumes the AudioContext. Call it from a click or a key press. It resolves to the context state.
- `Reel.sound.play(compiled, fromT)` uses a lookahead scheduler (a 25 ms tick and a 150 ms horizon, 1.1 s while the tab is hidden).
  - Events already sounding at `fromT` join mid-way with a 12 ms fade-in.
  - It returns `true` if audio was scheduled, and `false` if the context isn't running yet. Audio then starts by itself once `unlock()` succeeds.
- `Reel.sound.stop()` applies a 15 ms fade to everything, the reverb included.
- `Reel.sound.time()` is the playhead in piece seconds.
  - While playing, it's the audio clock (`ctx.currentTime − start + fromT`), less the master's 12 ms look-ahead and the device's output latency (capped at 100 ms), so it matches what you hear.
  - It holds at `fromT` for the ~50 ms start-up lead, so right after `play()` it trails the wall clock by up to about 0.16 s. A sync tolerance of 0.3 s is safe.
  - Before the context runs, it follows the wall clock. After `stop()` it holds the stopped position; before any play it's 0.
  - It keeps counting past `duration`, so the player decides whether to loop.
- `Reel.sound.volume(v)` takes 0..1 and returns the volume. `Reel.sound.muted` reads and sets the mute (with a 15 ms ramp).
  - Read-only: `Reel.sound.playing`, `Reel.sound.state` and `Reel.sound.ctx`.
- `Reel.sound.render(compiled, { sampleRate = 48000, from, to, master })` resolves to an AudioBuffer from an OfflineAudioContext with the same graph.
  - It's bit-identical every time.
  - `from` renders as if play() had started there; the lab uses it to test seeking.
- `Reel.sound.wav(buffer)` returns a 16-bit PCM WAV Blob.
- `Reel.sound.loadVoice(url)` loads a narration track (see Narration).
- Introspection: `Reel.sound.names` (`effects`, `instruments`, `beds`, `ambiences`), `Reel.sound.has(type, name)` and `Reel.sound.plan(compiled, fromT)`.
