@hraness/dawg 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/DAWG.md +608 -39
  3. package/README.md +4 -4
  4. package/core/chords.ts +1724 -0
  5. package/core/diff.ts +13 -1
  6. package/core/euclid.ts +670 -0
  7. package/core/fx.ts +1065 -0
  8. package/core/kits.ts +320 -0
  9. package/core/params.ts +111 -0
  10. package/core/rhythm.ts +287 -0
  11. package/core/score.ts +735 -18
  12. package/core/sdk/eval-child.ts +34 -22
  13. package/core/sdk/eval.ts +4 -1
  14. package/core/sdk/print.ts +304 -22
  15. package/core/sdk/sync-chords.ts +54 -0
  16. package/core/sdk/v1.ts +3226 -21
  17. package/core/synth.ts +1001 -0
  18. package/package.json +1 -1
  19. package/src/agent/agent.ts +38 -3
  20. package/src/agent/brief.ts +29 -4
  21. package/src/agent/chord-tools.ts +357 -0
  22. package/src/agent/drum-tools.ts +135 -0
  23. package/src/agent/models.ts +4 -4
  24. package/src/agent/pack-tools.ts +369 -0
  25. package/src/agent/planner.ts +17 -2
  26. package/src/agent/preview-tool.ts +270 -0
  27. package/src/agent/rhythm-tools.ts +145 -0
  28. package/src/agent/tools.ts +308 -14
  29. package/src/agent/xcb-agent.ts +8 -2
  30. package/src/audio/audition.ts +93 -0
  31. package/src/audio/cache.ts +160 -0
  32. package/src/audio/effects/bus.ts +147 -0
  33. package/src/audio/effects/chain.ts +109 -0
  34. package/src/audio/effects/common.ts +251 -0
  35. package/src/audio/effects/convolution.ts +339 -0
  36. package/src/audio/effects/drive.ts +142 -0
  37. package/src/audio/effects/duck.ts +122 -0
  38. package/src/audio/effects/dynamics.ts +121 -0
  39. package/src/audio/effects/filter.ts +319 -0
  40. package/src/audio/effects/modulation.ts +174 -0
  41. package/src/audio/effects/space.ts +394 -0
  42. package/src/audio/engine.ts +112 -4
  43. package/src/audio/kits.ts +200 -0
  44. package/src/audio/packs.ts +1787 -0
  45. package/src/audio/preview.ts +481 -0
  46. package/src/audio/random.ts +15 -0
  47. package/src/audio/sampler.ts +157 -29
  48. package/src/audio/samples.ts +386 -44
  49. package/src/audio/synth/oscillators.ts +268 -0
  50. package/src/audio/synth/voice.ts +555 -0
  51. package/src/audio/synth/zzfx.ts +137 -0
  52. package/src/audio/wav.ts +411 -295
  53. package/src/audio/wavetable-maker.ts +717 -0
  54. package/src/audio/wavetable.ts +624 -0
  55. package/src/commands/drums.ts +299 -0
  56. package/src/commands/edit.ts +26 -4
  57. package/src/commands/fx.ts +360 -0
  58. package/src/commands/help.ts +298 -10
  59. package/src/commands/music.ts +10 -8
  60. package/src/commands/pack.ts +423 -0
  61. package/src/commands/rhythm.ts +230 -0
  62. package/src/commands/sample.ts +162 -2
  63. package/src/commands/synth.ts +229 -0
  64. package/src/commands/wavetable.ts +370 -0
  65. package/src/main.ts +1222 -30
  66. package/src/media/cli.ts +15 -2
  67. package/src/media/tools.ts +106 -1
  68. package/src/media/wavetable.ts +202 -0
  69. package/src/render.ts +22 -1
  70. package/src/session/naming.ts +3 -1
  71. package/src/tui/audition.ts +521 -0
  72. package/src/tui/euclid.ts +516 -0
  73. package/src/tui/menu.ts +1305 -244
  74. package/src/tui/play-chords.ts +630 -0
  75. package/src/tui/play-session.ts +364 -16
  76. package/src/tui/sketch.ts +108 -0
  77. package/src/web/search.ts +4 -1
  78. package/tui/app.ts +161 -39
  79. package/tui/grammar.ts +286 -0
  80. package/tui/highway.ts +7 -1
  81. package/tui/play-strip.ts +59 -14
package/DAWG.md CHANGED
@@ -27,13 +27,15 @@ Auto-naming (`src/session/naming.ts`) is gated on a local musical fingerprint: t
27
27
 
28
28
  The `dawgd` protocol is newline-delimited JSON over a Unix socket in the session directory, or a hashed path under `$TMPDIR` when that path would exceed the platform socket-path limit. Every frame carries `v: 1`, frames are size-bounded, and every inbound frame is parsed from `unknown`. Clients send `hello`, `apply`, `transport`, `sync`, `focus`, `claim`, `meta`, and `ping`; the daemon replies with `welcome`, `result`, `snapshot`, `claimed`, `pong`, and typed `error` frames, and pushes `commit`, `transport`, `presence`, and `meta`.
29
29
 
30
- The header (`dawg` · track · ▶/⏸ BPM · key · session · N windows, then model · rev · sync on the right; `test/tui.test.ts` freezes the order) sits above the highway. The highway sits above the activity strip and the prompt. Notes stream toward the hit line, and velocity sets glyph density (`░▒▓█`) and saturation. Beat and bar rules get stronger at each level. Sustains draw as beams with a decaying tail and a short ghost after release. Each hit runs approach glow → flash/burst at the line → fade. The hit line pulses on the beat, and a sweep marks the loop wrap. A track with no hits draws `<track> · empty · add C4 at 0 to start` (or `hit kick at 0` on a kit) in place of bar numbers and lane labels; the hit line stays. Drum tracks use one lane per voice with a legend; lane projection is pluggable (`LaneProjection` in `tui/highway.ts`). By default every unmuted track is overlaid through its own projection and accent, the focused track drawn on top at full strength and the others dimmed; `/view focus` restores the single-track view. Animation is derived from transport time, so frame rate never changes timing. Frames render into a retained cell buffer, only changed rows are written, and output is capped at about 30 fps.
30
+ The header (`dawg` · track · ▶/⏸ BPM · key · session · N windows, then model · rev · sync on the right; `test/tui.test.ts` freezes the order) sits above the highway. The highway sits above the activity strip and the prompt. Notes stream toward the hit line, and velocity sets glyph density (`░▒▓█`) and saturation. Beat and bar rules get stronger at each level. Sustains draw as beams with a decaying tail and a short ghost after release. Each hit runs approach glow → flash/burst at the line → fade. The hit line pulses on the beat, and a sweep marks the loop wrap. A track with no hits draws `<track> · empty · type a request · ctrl-p play · ctrl-k menu` (when narrower: `<track> · empty · add C4 at 0 to start`, or `hit kick at 0` on a kit) in place of bar numbers and lane labels; the hit line stays. Drum tracks use one lane per voice with a legend; lane projection is pluggable (`LaneProjection` in `tui/highway.ts`). By default every unmuted track is overlaid through its own projection and accent, the focused track drawn on top at full strength and the others dimmed; `/view focus` restores the single-track view. Animation is derived from transport time, so frame rate never changes timing. Frames render into a retained cell buffer, only changed rows are written, and output is capped at about 30 fps.
31
31
 
32
32
  Keys: `Space` (empty prompt) toggles playback. `Enter` submits, or queues in QUEUE mode. `Shift+Enter`/`Ctrl+J` inserts a newline, `Alt+Enter` queues and `Ctrl+Q` toggles STEER/QUEUE. `Ctrl+Z`/`Ctrl+Y` undo and redo, and `Ctrl+O` or `/transcript` opens the transcript, which scrolls with ↑/↓, PgUp/PgDn and Home/End, and `/` cycles its filter (all, requests, ops, errors). `Esc` cancels an agent turn, closes the overlay or clears the draft. `Ctrl+L` redraws and `Ctrl+C` exits. Bracketed paste keeps multiline text intact.
33
33
 
34
34
  Themes: `/theme default|high-contrast|mono`, `--theme`, `DAWG_THEME`. Color falls back through truecolor, 256-color, 16-color and monochrome. `NO_COLOR` and `TERM=dumb` are supported. `/motion off`, `--reduce-motion` and `DAWG_REDUCE_MOTION=1` switch to static states in the same positions.
35
35
 
36
- Other code reports into the activity strip through `ActivityFeed` (`tui/activity.ts`): `pushCard(text, {tone, baseRevision, resultRevision, hint})`, `pushError(text)`, `setSpinner(label | undefined)`, `setQueueDepth(n)` and `applyAgentEvent(event)`, which accepts the agent's streaming `AgentEvent`s unchanged. Command handlers return a `Receipt` (`{ok: true | false | "warn", text}`), so a failure such as `main is not a drum track` is red because it says so, not because of its wording; `receiptTone` only classifies the legacy strings that remain. The `^z undo` hint rides only on receipts that changed the score, and only the first three in a session. `/help`, `/sessions` and `/tracks` open a scrollable text overlay (`TuiApp.openText`) and leave one summary card in the strip; `/help` is generated from `src/commands/help.ts`, which also feeds `dawg --help` and the usage hints that answer an unknown `/word` or a near-miss such as `pan 3` without a model call.
36
+ Other code reports into the activity strip through `ActivityFeed` (`tui/activity.ts`): `pushCard(text, {tone, baseRevision, resultRevision, hint})`, `pushError(text)`, `setSpinner(label | undefined)`, `setQueueDepth(n)` and `applyAgentEvent(event)`, which accepts the agent's streaming `AgentEvent`s unchanged. Command handlers return a `Receipt` (`{ok: true | false | "warn", text}`), so a failure such as `main is not a drum track` is red because it says so, not because of its wording; `receiptTone` only classifies the legacy strings that remain. The `^z undo` hint rides only on receipts that changed the score, and only the first three in a session. `/help`, `/sessions` and `/tracks` open a scrollable text overlay (`TuiApp.openText`) and leave one summary card in the strip; `/help` is generated from `src/commands/help.ts`, which also feeds `dawg --help` and the usage hints that answer an unknown `/word` or a near-miss such as `pan 3` without a model call. A known verb followed by a sentence (three or more words, no numbers: `add a walking bass in A minor`) goes to the agent instead. An unknown `/word` names the nearest command (`unknown command /clik · did you mean /click? · /help`); a bare word one edit from a command whose remaining words parse as its arguments is suggested locally (`tempoo 90 · did you mean tempo 90?`); other bare words are requests for the agent.
37
+
38
+ `/help` opens a short, task-first guide: **start here** (type a request, Ctrl-P, Ctrl-K, `?`, undo), **play notes**, **make drums**, **shape the sound**, **chords**, and **more**. `/help all` is the full reference; `/help music`, `/help session`, `/help window` and `/help keys` show one group.
37
39
 
38
40
  The local command path understands requests such as:
39
41
 
@@ -89,6 +91,7 @@ duration note <id> 0.25
89
91
  /import loop.track.json
90
92
  /model opus-5.5
91
93
  /help
94
+ /help all
92
95
  ```
93
96
 
94
97
  `/track <name>` focuses a track in this window and creates it when it is new; when another live window has it focused the reply is `<name> is open in another window` and focus stays put. Music words are bare and app commands take a slash; `tracks`, `export`, `import` and `track` still work bare as aliases but are listed once.
@@ -134,20 +137,21 @@ The project directory (the directory `dawg` runs in) is the agent's workspace. S
134
137
  - `list_files`, `read_file`: anywhere in the project except `.dawg/`, which dawg owns. Listings stop at 500 entries, reads at 256 KiB per call (1-based `offset` and `limit` page through larger files), and binary files (WAV, AIFF, FLAC, MP3, MIDI, images, archives) report type and size instead of bytes.
135
138
  - `write_file`, `edit_file`: only `song.ts` and the focused track's `tracks/<slug>/` directory (`core/slug.ts` derives the slug from the track name). Writes are atomic (temp file and rename), capped at 1 MiB, and create parent directories. `edit_file` replaces exactly one occurrence of `old`; zero or several matches return a count and nothing changes. `tracks/<slug>/notes.md` is the model's scratchpad and is never parsed. After a write, the optional host hook `onWorkspaceWrite(path)` can append text to the tool result (the project lane uses it to report how `track.ts` applied).
136
139
  - Every path is resolved lexically and then through `realpath`; `..`, absolute paths outside the root, symlinks that leave the project and anything under `.dawg/` are rejected with a diagnostic naming the writable roots. The brief gains a `project` entry with a tree of at most 30 lines and the first 1 KiB of the focused `notes.md`; both are shed before track summaries when the 12 KiB budget is tight.
137
- - `web_search` returns up to 8 `{title, url, snippet}` results. Providers, first match wins: `BRAVE_SEARCH_API_KEY` (explicit override); an AI Gateway key, which makes one non-streaming `anthropic/claude-haiku-4.5` call with the gateway's server-side search tool (`DAWG_WEB_SEARCH=exa|perplexity|parallel|browserbase`, default `exa`; the gateway bills the search, about $0.007 for Exa); an OpenRouter key (`OPENROUTER_API_KEY`), which uses the `web` plugin and its `url_citation` annotations; else DuckDuckGo's HTML endpoint. `DAWG_WEB_SEARCH` can also pin a backend (`duckduckgo`, `openrouter`, `gateway`, `brave`) when its credentials exist. A failing paid provider falls through to DuckDuckGo and the result says so. The activity card names the answering provider (`searched via gateway · exa`), and `WebHost.onSpend` reports each billed search for the spend ledger.
140
+ - `web_search` returns up to 8 `{title, url, snippet}` results. Providers, first match wins: `BRAVE_SEARCH_API_KEY` (explicit override); an AI Gateway key, which makes one non-streaming `anthropic/claude-haiku-4.5` call with the gateway's server-side search tool (`DAWG_WEB_SEARCH=exa|perplexity|parallel|browserbase`, default `exa`; the gateway bills the search, about $0.007 for Exa; its reported cost already includes that fee, so dawg records it as-is and uses the fee as an estimate only when no cost is reported; a live Exa search with three results cost $0.013); an OpenRouter key (`OPENROUTER_API_KEY`), which uses the `web` plugin and its `url_citation` annotations; else DuckDuckGo's HTML endpoint. `DAWG_WEB_SEARCH` can also pin a backend (`duckduckgo`, `openrouter`, `gateway`, `brave`) when its credentials exist. A failing paid provider falls through to DuckDuckGo and the result says so. The activity card names the answering provider (`searched via gateway · exa`), and `WebHost.onSpend` reports each billed search for the spend ledger.
138
141
  - `fetch_url` fetches one public http(s) URL locally: hostnames are resolved first and loopback, private, link-local, CGNAT and multicast addresses (IPv4, IPv6 and mapped) are refused, redirects (at most 3) are re-checked per hop, bodies stop at 2 MiB and the text handed to the model at 32 KiB. HTML is reduced to headings, lists, links and paragraphs; scripts, styles and navigation are dropped. Fetched text is untrusted and the system prompt says so.
139
142
 
140
143
  All limits live in `WORKSPACE_LIMITS`, `SEARCH_LIMITS` and `FETCH_LIMITS`. Search and fetch take an injectable `fetch` (and `lookup`), so tests run on fixtures in `src/web/fixtures/` without network. The gateway search fixture is derived from the documented response shape; capture a live response once to confirm it.
141
144
 
142
145
  ### Media tools
143
146
 
144
- Six more `AGENT_TOOLS` entries (`src/media/`) turn reference audio into material for a track. They work on files under the focused track's `tracks/<slug>/downloads/` (the same slug and write scope as `write_file`), return project-relative output paths so the model can chain them, and never install anything: a missing binary is reported with its install command. `dawg media <verb>` runs the same code from the shell, and `dawg media doctor` lists the backend, each binary, how it runs and how to install it.
147
+ Seven more `AGENT_TOOLS` entries (`src/media/`) turn reference audio into material for a track. They work on files under the focused track's `tracks/<slug>/downloads/` (the same slug and write scope as `write_file`), return project-relative output paths so the model can chain them, and never install anything: a missing binary is reported with its install command. `dawg media <verb>` runs the same code from the shell, and `dawg media doctor` lists the backend, each binary, how it runs and how to install it.
145
148
 
146
149
  - `download_audio {url, name?}`: YouTube only (`youtube.com`, `youtu.be`, `music.youtube.com`). Writes `<name>.wav` plus a `<name>.json` sidecar (title, duration, source URL, backend, sha256, time). The same source URL is reused instead of downloaded again. yt-dlp runs with `--no-playlist`, `--max-filesize 500m` and a 15 min budget.
147
150
  - `split_stems {file}`: six stems (vocals, drums, bass, guitar, piano, other) into `<base>.stems/`, cached once present. 20 min budget.
148
151
  - `analyze_audio {file}`: ffprobe metadata, then tempo, key, a beat grid and 240 waveform peaks computed in TypeScript, written to `<base>.analysis.json`; the model sees 48 peaks.
149
152
  - `transcribe_notes {file, kind?, from?, to?}`: drums use a vendored onset classifier mapped to dawg kit voices (kick, snare, clap, tom, hat, openhat, rim); pitched stems run `basic-pitch`. Notes are quantized onto the analysis beat grid (run first when missing) and returned as a `note()`/`hit()` snippet plus `<base>.<kind>.notes.json`, at most 2048 notes.
150
153
  - `import_sample {file, name, begin?, end?, root?}`: ffmpeg converts the file (or a trimmed window) to 48 kHz stereo PCM16 at `tracks/<slug>/samples/<name>.wav` and returns its sha256, duration and a `sampler({ name: "samples/<name>.wav" })` snippet (`{src, root, begin, end}` when given; `begin`/`end` are fractions of the file, `root` defaults to C4; a root adds `{ mode: "keyed" }`).
154
+ - `make_wavetable {file, name, frames?, start?, end?, method?, smooth?, normalize?}`: reads the file (WAV directly, anything else through ffmpeg as 48 kHz mono, at most 10 minutes) and writes a float32 wavetable of `frames` (default 64, at most 256) 2048-sample frames with a `clm ` chunk to `tracks/<slug>/wavetables/<name>.wav`. See [Wavetables from audio](#wavetables-from-audio).
151
155
  - `transcribe_lyrics {file, lang?}`: whisper-cli on a 16 kHz mono copy, writing `<base>.lyrics.json` (segments with seconds) and `.lyrics.txt`. The `ggml-base.en.bin` model (about 141 MiB) is announced and then downloaded once into `~/.cache/dawg/whisper/`.
152
156
 
153
157
  **Backends.** When StemDeck answers `GET /api/health` at `DAWG_STEMDECK_URL` (default `http://127.0.0.1:8000`, no credentials or query allowed in the URL), downloads and stems go through its job API and the job id is kept in the sidecar so `analyze_audio` reuses StemDeck's beat grid. Otherwise dawg runs the binaries directly: `yt-dlp`, `ffmpeg`, `ffprobe`, `whisper-cli` (`brew install yt-dlp ffmpeg whisper-cpp`), and `demucs`/`basic-pitch` through `uv tool run` when `uv tool list` shows them (`uv tool install demucs`, `uv tool install basic-pitch`). demucs downloads its model on first run.
@@ -172,7 +176,7 @@ Every subprocess goes through the injectable `CommandRunner` in `src/auth/runner
172
176
 
173
177
  `/login` in the TUI calls `handoff()`: it stops the frame timer, detaches stdin, leaves raw mode, bracketed paste and the alternate screen, runs the same flow on the real terminal, then re-enters, clears and forces a full redraw.
174
178
 
175
- The gateway and OpenRouter share `src/agent/gateway.ts`, an OpenAI-compatible streaming client with tool calls that requests `stream_options.include_usage`. `src/agent/usage.ts` prices each usage chunk, using the provider's own `cost` when present and otherwise tokens × the models.dev price. It keeps the session total and a daily ledger in `~/.config/dawg/usage.json` (31 days, lock file plus atomic rename) that windows share, and draws the spend line under the prompt (`$0.12 session · $0.48 today · opus-5.5 · gateway`; `subscription` for xcb; `no model · dawg login` offline; it narrows by dropping today, then session). Billed web searches add to the same meter. Prices come from `https://models.dev/api.json`, cached in `~/.config/dawg/cache/` for 24 h, fetched with a timeout and size cap, with a stale cache preferred to nothing offline; on OpenRouter its own `/models` prices win. The picker's `~$0.004/prompt` is `TYPICAL_PROMPT` (≈ 9,400 input + 600 output tokens, measured from the system prompt, tool schemas and a fixture brief over about 2 requests) × price.
179
+ The gateway and OpenRouter share `src/agent/gateway.ts`, an OpenAI-compatible streaming client with tool calls that requests `stream_options.include_usage`. `src/agent/usage.ts` prices each usage chunk, using the provider's own `cost` when present and otherwise tokens × the models.dev price. It keeps the session total and a daily ledger in `~/.config/dawg/usage.json` (31 days, lock file plus atomic rename) that windows share, and draws the spend line under the prompt (`$0.12 session · $0.48 today · opus-5.5 · gateway`; `subscription` for xcb; `no model · dawg login` offline; it narrows by dropping today, then session). Billed web searches add to the same meter. Prices come from `https://models.dev/api.json`, cached in `~/.config/dawg/cache/` for 24 h, fetched with a timeout and size cap, with a stale cache preferred to nothing offline; on OpenRouter its own `/models` prices win. The picker's `~$0.004/prompt` is `TYPICAL_PROMPT` (≈ 13,200 input + 600 output tokens, measured from the system prompt, tool schemas and a fixture brief over about 2 requests) × price.
176
180
 
177
181
  The xcb provider (`src/agent/xcb.ts`, `src/agent/xcb-agent.ts`) calls `xcb --json generate` with one `{version:1, account, model, prompt, timeoutMs, maxOutputBytes}` request on stdin. xcb exposes zero tools and does not stream, so the prompt carries the system rules, the composition brief and the tool catalog as JSON schemas, and asks for exactly one `{ops:[{tool,args}], say?, done}` object. The reply is untrusted. dawg takes the first balanced JSON object in at most 64 KiB, allows at most 16 ops and caps `say` at 400 characters. Each op then goes through `executeCall`, the same argument checks, operation validator, reducer dry run and per-op revision commit used by the gateway loop, and emits the same `tool-applied`/`tool-rejected`/`text-delta` events. If a reply cannot be parsed, an op is rejected or `done` is false, dawg makes another call with the per-op results, up to 3 calls and within the normal turn budgets. Esc aborts the turn, which terminates the xcb child and keeps every accepted revision. The child timeout is `timeoutMs + 75 s`, because the first `generate` per binding (and after an xcb or provider update) admits the account and can take up to a minute longer. A `busy` result, when two first calls hit one account, is retried with backoff. Accounts come from `xcb --json generate --capabilities`, parsed field by field from `unknown`. dawg never runs the xcb installer.
178
182
 
@@ -202,7 +206,7 @@ tracks/<slug>/samples/ audio a sampler references by relative path
202
206
 
203
207
  `init` appends `.dawg/*` and `!.dawg/sdk/` to `.gitignore`, so sessions and caches stay local while the vendored SDK is committed with the project. The slug is `trackSlug(name)` from `core/slug.ts`; duplicates get `-2`, `-3`. The full design is in [docs/project-format.md](./docs/project-format.md).
204
208
 
205
- SDK. `core/sdk/v1.ts` is one dependency-free file with JSDoc on every export, because its signatures are what an agent reads. Authors write beats; `song()` returns a `track.loop/v1` document in integer ticks (`round(beat × ticksPerBeat)`). Builders: `note(pitch, start, length = 1, velocity = 0.8)`, `seq("E2 . G2", {from, step, len, vel})` (`.`, `-`, `_` rest), `hit(voice, start, velocity, length = 0.25)` and `hits(voice, beats)` for `kit` voices (`kick`, `snare`, `hat`, …) and sampler voices, `every(step, {from, until})`, `sampler(voices, {mode})`, `slices(src, count)`, `track({...})` and `song({...})`. Note ids are content hashes, so the files never carry them and dawg keeps the session's ids for notes that did not change.
209
+ SDK. `core/sdk/v1.ts` is one dependency-free file with JSDoc on every export, because its signatures are what an agent reads. Authors write beats; `song()` returns a `track.loop/v1` document in integer ticks (`round(beat × ticksPerBeat)`). Builders: `note(pitch, start, length = 1, velocity = 0.8)`, `seq("E2 . G2", {from, step, len, vel})` (`.`, `-`, `_` rest), `hit(voice, start, velocity, length = 0.25)` and `hits(voice, beats)` for `kit` voices (`kick`, `snare`, `hat`, …) and sampler voices, `every(step, {from, until})`, `sampler(voices, {mode})`, `slices(src, count)`, `chord(symbol, start, length, opts)`, `progression(chords, opts)` (see [Chords](#chords)), `track({...})` and `song({...})`. Note ids are content hashes, so the files never carry them and dawg keeps the session's ids for notes that did not change.
206
210
 
207
211
  Evaluation. `evaluateProject(dir)` (`core/sdk/eval.ts`) imports `song.ts` in a fresh `bun --no-addons --no-install` child with cwd at the project, an environment of only `PATH`, `HOME` and `TMPDIR`, a 10 s timeout and 1 MiB of output, then decodes the document through the ordinary score validator. Failures are diagnostics `file:line:col message`, never throws. Evaluation is a guard against mistakes, not a sandbox: project code runs with your user's file access, like any build script.
208
212
 
@@ -220,7 +224,251 @@ The printer (`core/sdk/print.ts`) is deterministic and Prettier-stable (`prettie
220
224
 
221
225
  `dawg check` typechecks and evaluates the project, prints diagnostics to stderr and `ok · 3 tracks, 12 notes · types 26 ms · eval 21 ms` on success, and exits 1 on any problem or outside a project.
222
226
 
223
- Score format. The score stays `track.loop/v1` with `version: 1`: every addition is an optional field, so older documents still parse and older dawg versions reject only documents that use the new fields. Tracks may carry `sampler: {mode: "oneshot" | "keyed", voices: {name: {src, sha256?, root?, begin?, end?, gain?, speed?, loop?, choke?}}}` with bounds in `SCORE_LIMITS` (64 voices, 256-character relative `src`, gain ≤ 2, speed ≤ 8). One-shot voices map to pitches from 36 in voice-name order. Sampler tracks play their samples; see [Samples](#samples). `diffScores` uses four operations added alongside: `removeTrack`, `moveTrack`, `setKey` and `setMeter`, which dawgd rebases and the planner accepts.
227
+ Score format. The score stays `track.loop/v1` with `version: 1`: every addition is an optional field, so older documents still parse and older dawg versions reject only documents that use the new fields. Tracks may carry `sampler: {mode: "oneshot" | "keyed", voices: {name: {src, sha256?, url?, license?, root?, begin?, end?, gain?, speed?, loop?, choke?}}}` with bounds in `SCORE_LIMITS` (64 voices, 256-character relative `src`, gain ≤ 2, speed ≤ 8). One-shot voices map to pitches from 36 in voice-name order. Sampler tracks play their samples; see [Samples](#samples). `diffScores` uses four operations added alongside: `removeTrack`, `moveTrack`, `setKey` and `setMeter`, which dawgd rebases and the planner accepts.
228
+
229
+ ## Effects
230
+
231
+ Every track has one fixed effects chain (`FX_CHAIN` in `core/fx.ts`, DSP in `src/audio/effects/`):
232
+
233
+ ```text
234
+ filter → djf → autofilter → vowel → crush → distort → tremolo → compressor → pan → phaser → chorus → leslie → postgain → delay → reverb → [mix: orbit → duck]
235
+ ```
236
+
237
+ Stages before `pan` run on the track's mono voice sum; pan spreads it to stereo with the equal-power law; the rest run on the stereo pair. An effect that is off costs nothing. The core set — **filter, auto filter, distortion, tremolo, compressor, chorus, delay, reverb** — leads the Effects menu and the agent brief; dj filter, vowel, bitcrush, phaser, leslie, post gain, orbit and duck are under **more effects** for Strudel parity.
238
+
239
+ `orbit` and `duck` act where track stems are summed (`src/audio/effects/duck.ts`). Every track plays on an orbit (1 unless `fx orbit <n>` sets one). A track with `duck` is a sidechain trigger: each of its note onsets dips every other audible track on the target orbit by `depth`, reaching it over `onset` and recovering linearly over `attack`. Overlapping dips take the deepest; a ducker never ducks itself; loop renders wrap a dip across the loop end. The gain is computed from note onsets, not audio, so it is deterministic and costs one multiply per sample on ducked tracks. Typical use: `fx orbit 2` on the pad and bass, `fx duck preset pump` on the kick.
240
+
241
+ **Orbit buses** (`src/audio/effects/bus.ts`): `fx orbit shared on` makes a track send to its orbit's one shared delay and one shared reverb instead of running its own, as Strudel orbits do. Each member's stem (its chain up to delay) feeds the bus delay at its `delay.mix` and the bus reverb at its `reverb.mix`, both following their lanes; the bus delay's time, feedback, ping-pong and high-cut come from the first member in score order with a delay, the bus reverb's settings from the first member with a reverb. The two run in parallel and join the mix after the stems. Both are linear, so a one-member bus sounds exactly like that track's own delay and reverb; tracks without `shared` are unaffected.
242
+
243
+ **Convolution reverb** (`src/audio/effects/convolution.ts`, Strudel `iresponse`/`ir`): `fx reverb ir hall` (or `fx ir hall`) convolves with a generated impulse, `room`, `hall` or `plate`; `fx ir pack:<pack>/<sound>` or `fx ir samples/church.wav` uses a sample, pinned by sha256 like sampler files; `fx ir off` returns to the algorithmic tail. `mix` and `predelay` still apply; `size`, `fade` and `dim` do not. Uniformly partitioned FFT convolution, energy-normalized so a given `mix` sounds about as loud as the algorithmic tail; impulses are cut at 10 s.
244
+
245
+ `filter`, `delay` and `reverb` stay where they were on the track (older documents decode and render byte-for-byte as before; the new fields `filter.type`/`ftype`, `delay.time`/`pingpong`/`highcut` and `reverb.fade`/`lowpass`/`dim`/`predelay` are optional). The other effects live in `track.fx` keyed by name, and their parameter lanes in `track.fxAutomation` keyed `<effect>-<param>` (`autofilter-cutoff`, `distort-drive`, `reverb-mix`…). Omitted parameters take the defaults below, and turning an effect on with no parameters gives a good starting sound: the delay is a 3/16 (dotted-eighth) stereo ping-pong with feedback 0.35, mix 0.25 and a 5 kHz high-cut on the repeats.
246
+
247
+ Prompt grammar (one undo step per command; parameter names are dawg's or any Strudel name in the table):
248
+
249
+ ```text
250
+ fx list the focused track's effects
251
+ fx <effect> on|off|reset on with defaults, remove, back to defaults
252
+ fx <effect> preset <name> load a preset
253
+ fx <effect> <param> <value> [<param> <value> …]
254
+ fx delay mix 0.3 fx filter type hpf cutoff 300
255
+ fx distort drive 4 tone 5000 fx autofilter shape random sync 0.25
256
+ fx tremolo depth 0.8 fx delay delayfeedback 0.4
257
+ fx orbit 2 fx duck preset pump (one number sets the first param)
258
+ automate distort-drive points 0:1 8:6 every numeric fx param has a lane
259
+ ```
260
+
261
+ Aliases: `dist`, `comp`, `room`, `bitcrush`, `trem`, `auto-filter`, `bus`/`o` (orbit), `sidechain`/`duckorbit` (duck), `lpf`/`hpf`/`bpf` (filter with that type). The menu's Effects section opens each effect on its on/off toggle, presets and simple parameters; **advanced** lists every parameter with its Strudel names. The agent's `set_fx` tool takes the same names and presets.
262
+
263
+ Presets: filter `warm dark acid thin telephone`; autofilter `slow-sweep wobble s&h hpf-rise env-follow`; distort `warm crunch fuzz fold shape`; tremolo `gentle eighth-chop pulse`; compressor `gentle punch squash`; chorus `subtle wide seasick`; delay `ping-pong dotted-eighth slapback dub`; reverb `room hall plate ambient`; djf `dark thin`; vowel `a o ee`; crush `8-bit lofi destroy`; phaser `slow fast`; leslie `fast slow`; duck `pump subtle gate`.
264
+
265
+ The DSP is clean-room, written from public documentation of the parameters and standard literature (RBJ biquads, a Stilson/Smith-style ladder, Freeverb-style combs and allpasses, the Giannoulis–Massberg–Reiss compressor), not from Strudel or superdough source (AGPL). Renders stay deterministic: the random S&H shape hashes the cycle index, so cold, cached and worker renders are byte-identical (`src/audio/renderer.test.ts`).
266
+
267
+ Parameters (**bold** effect = shown in the simple menu; Lane = automation lane):
268
+
269
+ | Effect | Param | Range | Default | Strudel | Lane |
270
+ | -------------- | ------------------- | --------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------- | ---------------------- |
271
+ | **filter** | type (optional) | lpf / hpf / bpf | lpf | `lpf`, `hpf`, `bpf` | |
272
+ | filter | ftype (optional) | 12db / 24db / ladder | 12db | `ftype` | |
273
+ | **filter** | cutoff | 20..20000 Hz | 2000 | `lpf`, `cutoff`, `ctf`, `lp`, `hpf`, `hcutoff`, `bpf`, `bandf` | `filter` |
274
+ | **filter** | resonance | 0..1 | 0 | `lpq`, `resonance`, `hpq`, `hresonance`, `bpq`, `bandq` | `resonance` |
275
+ | **djf** | value | 0..1 | 0.5 | `djf` | `djf-value` |
276
+ | **autofilter** | type | lpf / hpf / bpf | lpf | `ftype-like: lpf/hpf/bpf` | |
277
+ | **autofilter** | cutoff | 20..20000 Hz | 1200 | `lpf`, `cutoff` | `autofilter-cutoff` |
278
+ | autofilter | resonance | 0..1 | 0.3 | `lpq`, `resonance` | `autofilter-resonance` |
279
+ | **autofilter** | depth | 0..6 oct | 2 | | `autofilter-depth` |
280
+ | **autofilter** | sync | 0..64 beats | 4 | | |
281
+ | autofilter | rate | 0.01..40 Hz | 0.5 | | `autofilter-rate` |
282
+ | **autofilter** | shape | sine / tri / square / saw / ramp / random | sine | | |
283
+ | autofilter | phase | 0..1 | 0 | | |
284
+ | autofilter | follow | -6..6 oct | 0 | `lpenv (per note, see synth)` | `autofilter-follow` |
285
+ | **vowel** | vowel | a / e / i / o / u / ae / aa / oe / ue / y / uh / un / en / an / on | a | `vowel` | |
286
+ | **vowel** | mix | 0..1 | 1 | | `vowel-mix` |
287
+ | **crush** | bits | 1..16 | 8 | `crush` | `crush-bits` |
288
+ | **crush** | coarse | 1..64 | 1 | `coarse` | |
289
+ | **crush** | mix | 0..1 | 1 | | `crush-mix` |
290
+ | **distort** | drive | 0..10 | 2 | `distort`, `dist` | `distort-drive` |
291
+ | distort | type | soft / hard / cubic / diode / asym / fold / sinefold / chebyshev / scurve / shape | soft | `distort type (3rd field)`, `shape → type shape` | |
292
+ | **distort** | tone | 200..20000 Hz | 8000 | | `distort-tone` |
293
+ | **distort** | mix | 0..1 | 1 | | `distort-mix` |
294
+ | distort | postgain | 0..2 | 1 | `distort postgain` | |
295
+ | **tremolo** | sync | 0..64 beats | 0.5 | `tremolosync`, `tremsync` | |
296
+ | tremolo | rate | 0.01..40 Hz | 4 | `tremolo` | `tremolo-rate` |
297
+ | **tremolo** | depth | 0..1 | 0.5 | `tremolodepth`, `tremdepth` | `tremolo-depth` |
298
+ | **tremolo** | shape | sine / tri / square / saw / ramp | sine | `tremoloshape`, `tremshape` | |
299
+ | tremolo | skew | 0..1 | 0.5 | `tremoloskew`, `tremskew` | |
300
+ | tremolo | phase | 0..1 | 0 | `tremolophase`, `tremphase` | |
301
+ | **compressor** | threshold | -60..0 dB | -18 | `compressor threshold` | `compressor-threshold` |
302
+ | **compressor** | ratio | 1..20 | 4 | `compressorRatio` | |
303
+ | compressor | knee | 0..24 dB | 6 | `compressorKnee` | |
304
+ | compressor | attack | 0.0001..1 s | 0.01 | `compressorAttack` | |
305
+ | compressor | release | 0.01..2 s | 0.15 | `compressorRelease` | |
306
+ | **compressor** | makeup | 0..24 dB | 5 | | `compressor-makeup` |
307
+ | **phaser** | rate | 0.01..40 Hz | 0.5 | `phaser`, `ph` | `phaser-rate` |
308
+ | phaser | sync | 0..64 beats | 0 | | |
309
+ | **phaser** | depth | 0..1 | 0.75 | `phaserdepth`, `phd`, `phasdp` | `phaser-depth` |
310
+ | phaser | center | 100..10000 Hz | 1000 | `phasercenter`, `phc` | |
311
+ | phaser | sweep | 0..8000 Hz | 2000 | `phasersweep`, `phs` | |
312
+ | **chorus** | rate | 0.01..40 Hz | 0.8 | | `chorus-rate` |
313
+ | **chorus** | depth | 0..1 | 0.4 | | `chorus-depth` |
314
+ | **chorus** | mix | 0..1 | 0.5 | | `chorus-mix` |
315
+ | **leslie** | mix | 0..1 | 1 | `leslie` | `leslie-mix` |
316
+ | **leslie** | rate | 0.01..40 Hz | 6.7 | `lrate` | `leslie-rate` |
317
+ | leslie | size | 0..1 | 0.5 | `lsize` | |
318
+ | **postgain** | gain | 0..4 | 1 | `postgain`, `post` | `postgain-gain` |
319
+ | **delay** | beats | 0.0625..4 beats | 0.75 | `delaytime (seconds = beats·60/bpm)` | |
320
+ | **delay** | feedback | 0..0.9 | 0.35 | `delayfeedback`, `delayfb`, `dfb` | `delay-feedback` |
321
+ | **delay** | mix | 0..1 | 0.25 | `delay` | `delay-mix` |
322
+ | delay | time (optional) | 0..4 s | 0 | `delaytime`, `delayt`, `dt` | |
323
+ | delay | pingpong (optional) | on/off | true | | |
324
+ | delay | highcut (optional) | 500..20000 Hz | 5000 | | |
325
+ | **reverb** | mix | 0..1 | 0.3 | `room` | `reverb-mix` |
326
+ | **reverb** | size | 0..1 | 0.5 | `roomsize`, `rsize`, `sz`, `size` | |
327
+ | reverb | fade (optional) | 0.1..20 s | 2 | `roomfade`, `rfade` | |
328
+ | reverb | lowpass (optional) | 200..20000 Hz | 8000 | `roomlp`, `rlp` | |
329
+ | reverb | dim (optional) | 200..20000 Hz | 3000 | `roomdim`, `rdim` | |
330
+ | reverb | predelay (optional) | 0..0.5 s | 0.02 | | |
331
+ | reverb | ir (optional) | `builtin:room\|hall\|plate`, pack sound or project WAV | off | `iresponse`, `ir` | |
332
+ | orbit | orbit | 1..16 (integer) | 2 | `orbit`, `o` | |
333
+ | orbit | shared (optional) | on/off | off | | |
334
+ | duck | orbit | 1..16 (integer) | 1 | `duckorbit`, `duck` | |
335
+ | duck | depth | 0..1 | 1 | `duckdepth` | |
336
+ | duck | attack | 0.001..4 s | 0.1 | `duckattack`, `duckatt`, `datt` | |
337
+ | duck | onset | 0..0.5 s | 0.003 | `duckonset` | |
338
+
339
+ Strudel mapping notes: Strudel's `lpf`/`hpf`/`bpf` each set a separate filter; dawg has one track filter whose `type` selects the response, so `lpf(800)` is `filter {type: "lpf", cutoff: 800}` and `lpq`/`hpq`/`bpq` map to `resonance`. `delay` in Strudel is the wet level (dawg `delay.mix`), `delaytime` is seconds (dawg `delay.time`; `beats` is the tempo-synced form), `delayfeedback` is `delay.feedback`. `room` is `reverb.mix`, `size`/`roomsize` is `reverb.size`, `roomfade`/`roomlp`/`roomdim` are `fade`/`lowpass`/`dim`. `distort` and `shape` are the distortion drive with `type: "shape"` for Strudel's `shape` curve; `crush` is bits and `coarse` is the sample-hold factor. `phaser`/`phaserdepth`/`phasercenter`/`phasersweep`, `tremolo*`, `leslie`/`lrate`/`lsize`, `postgain` and `compressor` keep their names. `orbit` groups tracks for `duckorbit`/`duckdepth`/`duckattack`/`duckonset` sidechaining; By default each track keeps its own delay and reverb; `fx orbit 2 shared on` makes the track send to its orbit's one shared delay and reverb, as Strudel orbits do (see **Orbit buses**). `iresponse`/`ir` is `reverb.ir`.
340
+
341
+ ## Synth
342
+
343
+ A synth track's voice is shaped by `track.synth`, a map of Strudel (superdough) parameter names to values. Parameter names are Strudel's wherever one means the same thing, and every Strudel alias is accepted on input (`att`, `lpe`, `fmi`, `vmod`…); the stored and printed form is the canonical name in the table. Only the parameters a document sets are stored, and an unset one takes its default, as in Strudel. A track with no `synth` and one of dawg's original instruments (`sine piano pluck bass saw square triangle`) renders byte-for-byte as before.
344
+
345
+ Sounds (`instrument`): `sine`, `sawtooth` (`saw` stays the legacy voice until `synth` is set), `square`, `triangle`, `supersaw`, `pulse`, `user` (additive, from `partials`/`phases`), and noise `white`, `pink`, `brown`, `crackle`, plus the ZzFX sounds `z_sine`, `z_triangle`, `z_sawtooth`, `z_square`, `z_tan`, `z_noise`. Aliases: `sin`, `tri`, `sqr`, `noise`/`whitenoise`, `pinknoise`, `brownnoise`. Any oscillator can be mixed with noise (`noise`, and `density` for crackle), detuned into a unison stack (`unison`, `detune`, `spread`), pulse-width modulated (`pw`, `pwrate`, `pwsweep`), and frequency-modulated by up to eight operators (`fm`…`fm8`, each with `fmh`, an ADSR, `fmenv` lin/exp and `fmwave`). The operators modulate the carrier's phase in parallel, each at its own ratio `fmh`. The pitch envelope (`penv` semitones with `pattack/pdecay/psustain/prelease`, `pcurve`, `panchor`) and vibrato (`vib` Hz, `vibmod` semitones) bend pitch. Three per-voice filters (`lpf`, `hpf`, `bpf`, in that order) each have `q`, an envelope depth in octaves (`lpenv`…) and their own ADSR; `ftype` chooses 12 dB, 24 dB or ladder for the low-pass and `fanchor` sets where the envelope sits relative to the cutoff. A filter whose cutoff is unset is off.
346
+
347
+ ZzFX sounds (`z_*`) run their own small procedural generator (`src/audio/synth/zzfx.ts`) and then the same ADSR and per-voice filters: frequency = note·(1 ± `zrand`) + 500·`slide` Hz/s + 250·`deltaSlide`·t² Hz, plus `pitchJump` Hz once `pitchJumpTime` seconds have passed; `lfo` (seconds) restarts slide and pitch jump every period and sets the period of `tremolo` (volume modulation amount); `zmod` is an FM rate in Hz at ±50 % depth; `noise` jitters the phase increment (on other sounds it mixes pink noise); `curve` bends the wave (sign·|x|^curve, 0 squares it); `zcrush` holds samples (0..1 → 1..100 samples at 44.1 kHz); `zdelay` adds one echo at half level. Strudel documents names and intent, not units, so these units are dawg's.
348
+
349
+ The oscillator is chosen in one place, `resolveOscillator()` in `src/audio/synth/oscillators.ts`, which maps a sound name to an oscillator factory that can read the track's synth parameters. Another module can add sounds with `registerOscillatorResolver()` and reuse the voice's ADSR, filter envelopes, unison and FM.
350
+
351
+ Prompt grammar (one undo step per command):
352
+
353
+ ```text
354
+ synth list what this track sets
355
+ synth preset <name> instrument + parameters
356
+ synth lpf 800 lpenv 3 lpdecay 0.2 any parameter by name or Strudel alias
357
+ synth fm 4 fmh 1.5 synth adsr 0.01 0.2 0.5 0.3
358
+ synth partials 1 0.5 0.33 0.25 additive harmonics (also phases)
359
+ synth lpf off unset one parameter
360
+ synth reset unset everything
361
+ synth zzfx ,,129,.01,,.15,2 a raw ZzFX array (or paste zzfx(...[…]))
362
+ automate synth-lpf points 0:400 8:4000 numeric parameters have lanes
363
+ ```
364
+
365
+ Raw ZzFX arrays (Strudel `zzfx([...])`) use ZzFX's documented positional layout: volume→`gain`, randomness→`zrand`, frequency (the note), attack, sustain time (the note's length), release, shape 0–5→`z_sine`/`z_triangle`/`z_sawtooth`/`z_tan`/`z_noise`/`z_square`, shapeCurve→`curve`, `slide`, `deltaSlide`, `pitchJump`, `pitchJumpTime`, repeatTime→`lfo`, `noise`, modulation→`zmod`, bitCrush→`zcrush`, delay→`zdelay`, sustainVolume→`sustain`, `decay`, `tremolo`, filter (> 0 high-pass Hz, < 0 low-pass Hz). Empty slots take ZzFX's defaults; the result is stored as the named controls, so it prints and edits like any synth track. The SDK has `zzfx([...])` to spread into `track({...})` and `set_synth` takes `zzfx`.
366
+
367
+ Presets: `pad` (supersaw: slow, wide detuned saws through a soft low-pass), `lead` (sawtooth: bright saw with a short filter blip and delayed vibrato feel), `pluck` (pulse: short percussive pulse with a fast filter envelope), `bass` (sawtooth: round saw bass, 24 dB low-pass with a little bite), `sub` (sine: clean sine sub with a tiny pitch drop on each note), `acid` (sawtooth: ladder low-pass with high resonance and a snappy envelope), `keys` (sine: electric-piano style 1:1 FM with a decaying modulator), `bell` (sine: inharmonic FM bell with a long ring), `organ` (user: drawbar-style additive organ with a gentle vibrato), `strings` (supersaw: softer ensemble: slow attack, gentle vibrato, darker filter), `brass` (sawtooth: filter swell on attack like a brass section), `wind` (pink: breathy band-passed noise that swells and fades), `chip` (pulse: 8-bit square lead with slow pulse-width motion), `zap` (z_square: ZzFX laser zap: a square that dives in pitch).
368
+
369
+ The menu's **Parameters** section shows the instrument, preset and the simple parameters (ADSR, lpf/lpq/lpenv, detune, vib, fm); **advanced** groups every parameter (amplitude, oscillator, vibrato, pitch envelope, the three filters, FM 1–8, ZzFX, partials) with its Strudel aliases. The agent's `set_synth` tool takes the same names and presets.
370
+
371
+ Automation lanes `synth-<param>` are read at each note's onset, as Strudel reads a patterned control once per event. Note velocity scales the voice as Strudel's `velocity` does, and `gain` is the voice gain before the effects chain.
372
+
373
+ The DSP is dawg's own, clean-room from public documentation and standard literature (PolyBLEP oscillators, Paul Kellet's pink-noise filter, leaky-integrated brown noise, RBJ biquads and a Stilson/Smith-style ladder, linear and exponential ADSRs, phase-modulation FM), not from Strudel or superdough source (AGPL-3.0). Noise comes from a PRNG seeded by each note, so renders stay byte-identical across cold, cached and worker paths.
374
+
375
+ | Param | Range | Default | Strudel names | Lane |
376
+ | ------------- | ----------------------------------- | ------- | ---------------------------- | --------------------- |
377
+ | **attack** | 0..10 s | 0.003 | `attack`, `att` | `synth-attack` |
378
+ | **decay** | 0..10 s | 0.05 | `decay`, `dec` | `synth-decay` |
379
+ | **sustain** | 0..1 | 1 | `sustain`, `sus` | `synth-sustain` |
380
+ | **release** | 0..10 s | 0.05 | `release`, `rel` | `synth-release` |
381
+ | gain | 0..4 | 1 | `gain` | `synth-gain` |
382
+ | noise | 0..1 | 0 | `noise` | `synth-noise` |
383
+ | density | 0..1 | 0.03 | `density` | `synth-density` |
384
+ | unison | 1..16 | 1 | `unison` | |
385
+ | **detune** | 0..12 st | 0.2 | `detune` | `synth-detune` |
386
+ | spread | 0..1 | 0.6 | `spread` | `synth-spread` |
387
+ | pw | 0..1 | 0.5 | `pw` | `synth-pw` |
388
+ | pwrate | 0..40 Hz | 1 | `pwrate` | `synth-pwrate` |
389
+ | pwsweep | 0..1 | 0 | `pwsweep` | `synth-pwsweep` |
390
+ | **vib** | 0..64 Hz | 0 | `vib`, `vibrato`, `v` | `synth-vib` |
391
+ | vibmod | 0..24 st | 0.5 | `vibmod`, `vmod` | `synth-vibmod` |
392
+ | penv | -48..48 st | 0 | `penv` | `synth-penv` |
393
+ | pattack | 0..10 s | 0.2 | `pattack`, `patt` | `synth-pattack` |
394
+ | pdecay | 0..10 s | 0 | `pdecay`, `pdec` | `synth-pdecay` |
395
+ | psustain | 0..1 | 1 | `psustain`, `psus` | `synth-psustain` |
396
+ | prelease | 0..10 s | 0 | `prelease`, `prel` | `synth-prelease` |
397
+ | pcurve | 0..1 | 0 | `pcurve` | |
398
+ | panchor | 0..1 | 0 | `panchor` | |
399
+ | **lpf** | 20..20000 Hz | 2000 | `lpf`, `cutoff`, `ctf`, `lp` | `synth-lpf` |
400
+ | **lpq** | 0..50 | 1 | `lpq`, `resonance` | `synth-lpq` |
401
+ | **lpenv** | -10..10 oct | 0 | `lpenv`, `lpe` | `synth-lpenv` |
402
+ | lpattack | 0..10 s | 0.005 | `lpattack`, `lpa` | `synth-lpattack` |
403
+ | lpdecay | 0..10 s | 0.15 | `lpdecay`, `lpd` | `synth-lpdecay` |
404
+ | lpsustain | 0..1 | 0 | `lpsustain`, `lps` | `synth-lpsustain` |
405
+ | lprelease | 0..10 s | 0.1 | `lprelease`, `lpr` | `synth-lprelease` |
406
+ | hpf | 20..20000 Hz | 200 | `hpf`, `hcutoff`, `hp` | `synth-hpf` |
407
+ | hpq | 0..50 | 1 | `hpq`, `hresonance` | `synth-hpq` |
408
+ | hpenv | -10..10 oct | 0 | `hpenv`, `hpe` | `synth-hpenv` |
409
+ | hpattack | 0..10 s | 0.005 | `hpattack`, `hpa` | `synth-hpattack` |
410
+ | hpdecay | 0..10 s | 0.15 | `hpdecay`, `hpd` | `synth-hpdecay` |
411
+ | hpsustain | 0..1 | 0 | `hpsustain`, `hps` | `synth-hpsustain` |
412
+ | hprelease | 0..10 s | 0.1 | `hprelease`, `hpr` | `synth-hprelease` |
413
+ | bpf | 20..20000 Hz | 1000 | `bpf`, `bandf`, `bp` | `synth-bpf` |
414
+ | bpq | 0..50 | 1 | `bpq`, `bandq` | `synth-bpq` |
415
+ | bpenv | -10..10 oct | 0 | `bpenv`, `bpe` | `synth-bpenv` |
416
+ | bpattack | 0..10 s | 0.005 | `bpattack`, `bpa` | `synth-bpattack` |
417
+ | bpdecay | 0..10 s | 0.15 | `bpdecay`, `bpd` | `synth-bpdecay` |
418
+ | bpsustain | 0..1 | 0 | `bpsustain`, `bps` | `synth-bpsustain` |
419
+ | bprelease | 0..10 s | 0.1 | `bprelease`, `bpr` | `synth-bprelease` |
420
+ | ftype | 12db / 24db / ladder | 12db | `ftype` | |
421
+ | fanchor | 0..1 | 0 | `fanchor` | `synth-fanchor` |
422
+ | **fm** | 0..64 | 0 | `fm`, `fmi` | `synth-fm` |
423
+ | fmh | 0..32 | 1 | `fmh` | `synth-fmh` |
424
+ | fmattack | 0..10 s | 0 | `fmattack`, `fmatt` | `synth-fmattack` |
425
+ | fmdecay | 0..10 s | 0 | `fmdecay`, `fmdec` | `synth-fmdecay` |
426
+ | fmsustain | 0..1 | 1 | `fmsustain`, `fmsus` | `synth-fmsustain` |
427
+ | fmrelease | 0..10 s | 0 | `fmrelease`, `fmrel` | `synth-fmrelease` |
428
+ | fmenv | lin / exp | lin | `fmenv`, `fme` | |
429
+ | fmwave | sine / sawtooth / square / triangle | sine | `fmwave` | |
430
+ | zrand | 0..1 | 0 | `zrand` | `synth-zrand` |
431
+ | curve | 0..3 | 1 | `curve` | `synth-curve` |
432
+ | slide | -20..20 | 0 | `slide` | `synth-slide` |
433
+ | deltaSlide | -20..20 | 0 | `deltaSlide` | `synth-deltaSlide` |
434
+ | pitchJump | -2000..2000 Hz | 0 | `pitchJump` | `synth-pitchJump` |
435
+ | pitchJumpTime | 0..10 s | 0 | `pitchJumpTime` | `synth-pitchJumpTime` |
436
+ | lfo | 0..10 s | 0 | `lfo` | `synth-lfo` |
437
+ | zmod | 0..1000 Hz | 0 | `zmod` | `synth-zmod` |
438
+ | zcrush | 0..1 | 0 | `zcrush` | `synth-zcrush` |
439
+ | zdelay | 0..1 s | 0 | `zdelay` | `synth-zdelay` |
440
+ | tremolo | 0..1 | 0 | `tremolo` | `synth-tremolo` |
441
+ | partials | up to 64 numbers -1..1 | — | `partials` | |
442
+ | phases | up to 64 numbers 0..1 | — | `phases` | |
443
+
444
+ FM operators 2–8 repeat the `fm` rows with a suffix (`fm2`, `fmh2`, `fmattack2` … `fmwave8`), each with its lane.
445
+
446
+ ### Strudel parity
447
+
448
+ | Strudel | dawg | Status |
449
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------- |
450
+ | `s`/`sound` sine, sawtooth, square, triangle, supersaw, pulse, user, white, pink, brown, crackle | `instrument` | done |
451
+ | `noise`, `density` | `synth.noise`, `synth.density` | done |
452
+ | `unison`, `spread`, `detune` | `synth.*` | done |
453
+ | `pw`, `pwrate`, `pwsweep` | `synth.*` | done |
454
+ | `fm`/`fmi`, `fmh`, `fmattack/fmdecay/fmsustain/fmrelease`, `fmenv`, `fmwave`, operators 2–8 | `synth.*` | done |
455
+ | `attack/decay/sustain/release`, `adsr`, `gain`, `velocity` | `synth.*`; `adsr` is command shorthand; velocity is the note's | done |
456
+ | `penv`, `pattack/pdecay/psustain/prelease`, `pcurve`, `panchor` | `synth.*` | done |
457
+ | `vib`/`vibrato`, `vibmod` | `synth.*` | done |
458
+ | `lpf/hpf/bpf`, `lpq/hpq/bpq`, `lpenv/hpenv/bpenv` and their ADSRs, `ftype`, `fanchor` | `synth.*` (per voice); also the track `filter` effect | done |
459
+ | `partials`, `phases` | `synth.partials`, `synth.phases` | done |
460
+ | `vowel`, `coarse`, `crush`, `shape`, `distort`, `djf` | effects `vowel`, `crush`, `distort`, `djf` | done (see Effects) |
461
+ | `phaser*`, `tremolo*`, `leslie`/`lrate`/`lsize`, `compressor*`, `postgain` | effects of the same names | done |
462
+ | `room`, `size`, `roomfade`, `roomlp`, `roomdim` | `reverb` | done |
463
+ | `delay`, `delaytime`, `delayfeedback` | `delay` | done |
464
+ | `pan` | track `pan` | done |
465
+ | `orbit`, `duckorbit`/`duckdepth`/`duckattack`/`duckonset` | effects `orbit` (+ `shared`), `duck` | done: `shared` sends to one delay + reverb per orbit |
466
+ | `iresponse`/`ir` | `reverb.ir` | done: FFT convolution; built-ins or a pinned sample |
467
+ | `z_sine`…`z_noise`; `zrand`, `curve`, `slide`, `deltaSlide`, `pitchJump`, `pitchJumpTime`, `lfo`, `noise`, `zmod`, `zcrush`, `zdelay`, `tremolo` | ZzFX sounds, `synth.*` | done (clean-room; units documented above) |
468
+ | zzfx `duration` | note length | done: a note's length is its duration |
469
+ | raw `zzfx([...])` parameter array | `synth zzfx …`, SDK `zzfx([...])`, `set_synth {zzfx}` | done: ZzFX's documented layout → named controls |
470
+ | soundfonts `gm_*`, drum banks, dirt-samples | sampler and sample packs | not this engine: hosted samples, see Sample packs |
471
+ | sample controls `begin`, `end`, `speed`, `unit`, `loop`, `loopBegin`/`loopb`, `loopEnd`/`loope`, `clip`/`legato`, `fit`, `loopAt`, `accelerate`, `squiz`, `cut`, `gain` | sampler voice fields; `/sample set`, `set_sample` | done (see Samples) |
224
472
 
225
473
  ## Samples
226
474
 
@@ -247,17 +495,26 @@ export default track({
247
495
 
248
496
  Semantics follow Strudel's sampler:
249
497
 
250
- | Strudel | dawg | Behaviour |
251
- | ------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
252
- | `samples({ kick: "kick.wav" })` | `sampler({ kick: "samples/kick.wav" })` | one voice per name |
253
- | `s("kick hat")` | `hits("kick", …)`, `hit("hat", …)` (oneshot mode) | voices take pitch slots 36, 37, … in name order; a hit plays the whole sample, whatever the note length |
254
- | `note("c4 e4").s("vox")` | `sampler({ vox: { src, root: "C4" } }, { mode: "keyed" })` | rate = 2^((pitch − root)/12); the note's length holds it, then a 10 ms release; several roots multi-sample |
255
- | `.begin(0.25)` / `.end(0.5)` | `begin: 0.25`, `end: 0.5` | 0..1 fractions of the file |
256
- | `.speed(2)` / `.speed(-1)` | `speed: 2` / `speed: -1` | rate and pitch together; negative plays the window backwards |
257
- | `.loop(1)` | `loop: true` | repeats begin..end (5 ms crossfade) for the note's length, oneshot or keyed |
258
- | `.cut(1)` | `choke: "hats"` | a new hit in the group stops the sounding voice with a 5 ms fade |
259
- | `.gain(0.8)` | `gain: 0.8` | 0..2, times velocity and the track volume |
260
- | `.slice(8, …)` / `.chop(8)` | `slices("samples/break.wav", 8)` | eight voices with begin/end windows |
498
+ | Strudel | dawg | Behaviour |
499
+ | ------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
500
+ | `samples({ kick: "kick.wav" })` | `sampler({ kick: "samples/kick.wav" })` | one voice per name |
501
+ | `s("kick hat")` | `hits("kick", …)`, `hit("hat", …)` (oneshot mode) | voices take pitch slots 36, 37, … in name order; a hit plays the whole sample, whatever the note length |
502
+ | `note("c4 e4").s("vox")` | `sampler({ vox: { src, root: "C4" } }, { mode: "keyed" })` | rate = 2^((pitch − root)/12); the note's length holds it, then a 10 ms release; several roots multi-sample |
503
+ | `.begin(0.25)` / `.end(0.5)` | `begin: 0.25`, `end: 0.5` | 0..1 fractions of the file |
504
+ | `.speed(2)` / `.speed(-1)` | `speed: 2` / `speed: -1` | rate and pitch together; negative plays the window backwards |
505
+ | `.loop(1)` | `loop: true` | repeats begin..end (5 ms crossfade) for the note's length, oneshot or keyed |
506
+ | `.cut(1)` | `choke: "hats"` | a new hit in the group stops the sounding voice with a 5 ms fade |
507
+ | `.gain(0.8)` | `gain: 0.8` | 0..2, times velocity and the track volume |
508
+ | `.slice(8, …)` / `.chop(8)` | `slices("samples/break.wav", 8)` | eight voices with begin/end windows |
509
+ | `.loopBegin(0.25)` / `.loopEnd(0.75)` (`loopb`/`loope`) | `loopBegin: 0.25`, `loopEnd: 0.75` | with `loop`, the first pass plays from `begin`, then repeats only loopBegin..loopEnd (file fractions inside the window) |
510
+ | `.clip(1)` / `.legato(1)` | `clip: 1` | the voice lasts note length × clip (then a 10 ms release), cutting a long oneshot |
511
+ | `.fit()` | `fit: true` | the window is stretched or squeezed (by rate, so pitch follows) to last the note |
512
+ | `.unit("c")` | `unit: "c"`, `speed: n` | `speed` becomes a duration: the window lasts 1/n bars; `unit: "s"`: `speed` seconds |
513
+ | `.loopAt(2)` | `speed: 0.5, unit: "c"` (`/sample set brk loopAt 2`) | the window lasts 2 bars |
514
+ | `.accelerate(1)` | `accelerate: 1` | the rate ramps linearly by +1× over the voice (−8..8); a ramp that reaches rate 0 ends the voice |
515
+ | `.squiz(2)` | `squiz: 2` | each zero-crossing cycle is replayed `squiz`× faster, raising pitch without shortening (1..32; implemented from the Tidal/SuperDirt description) |
516
+
517
+ `/sample set <voice> <control> <value>…` edits these on the focused sampler track (`/sample set brk fit on clip 1`, `/sample set hat cut hats`, `off` unsets one), each voice in the menu's Parameters section has the same controls, and the agent's `set_sample` tool takes them by name.
261
518
 
262
519
  Every voice starts and stops with a 1–3 ms fade, so cuts do not click. There is no time-stretch, as in Strudel's default. A sampler track goes through the same volume and pan automation, filter, delay and reverb as any other track and is a cached stem like any other; the stem's cache key includes each voice's sha256, so replacing a file re-renders it.
263
520
 
@@ -265,6 +522,196 @@ Decoding: WAV (PCM 16/24/32-bit integer and 32-bit float, any channel count and
265
522
 
266
523
  In the TUI, oneshot sampler tracks show one highway lane per voice, labelled by name; keyed tracks use the pitch axis. `/tracks` shows each sampler's sample count and how many failed to load. `/sample <path> [as <voice>]` adds a voice to the focused track: a file outside the track directory is copied into `tracks/<slug>/samples/`, the voice name defaults to the file name, and a focused synth track that already has notes gets a new `samples` track instead. Existing hits keep their voice when the new name shifts the slots. `/sample` alone lists the voices. The agent's `import_sample` media tool writes 48 kHz stereo WAVs to the same folder.
267
524
 
525
+ ## Sample packs
526
+
527
+ dawg reads Strudel's sample-pack manifests, so the packs Strudel users know work here, without any Strudel code (Strudel is AGPL; dawg's loader in `src/audio/packs.ts` is written from the documented manifest format only). A manifest is JSON: `{"_base": "<url>/", "<sound>": ["a.wav", "b.wav"] | {"c4": "c4.wav"}}`. A list is a set of variations addressed `<sound>:<n>`; a note map is a keyed instrument. `github:<user>/<repo>[/<branch>]` means `https://raw.githubusercontent.com/<user>/<repo>/<branch or main>/strudel.json`, the rule Strudel documents. General MIDI soundfonts load from gleitz/midi-js-soundfonts' `names.json` and become keyed samplers with one zone per sampled note.
528
+
529
+ Fetching is lazy. Adding a pack fetches only its manifest. A sample file is fetched the first time a track uses it, decoded, and stored in the existing `.dawg/assets/<sha256>.pcm` cache (with a copy of the raw file in the pack cache), so a pack never lands in the repo or the npm package. Only HTTPS is accepted (loopback HTTP only under `DAWG_PACKS_ALLOW_LOOPBACK_HTTP=1`, for tests), URLs may not carry credentials, every fetch has a timeout, and files keep the same size and duration limits as local samples. Manifests and files are cached under `$XDG_CACHE_HOME/dawg/packs` (default `~/.cache/dawg/packs`; `DAWG_PACKS_DIR` overrides), so a pack sound used once plays offline afterwards.
530
+
531
+ A sampler voice references a pack sound as `pack:<pack>/<sound>[:<n>]`, like Strudel's `s("bd:3")`; banks follow Strudel's `bank("RolandTR909")` naming (`RolandTR909_bd`). When the sound is first used dawg pins it in the track: `{src: "pack:tidal-drum-machines/RolandTR909_bd", sha256, url, license}`. Renders load the pinned sha256, so they stay reproducible even if the pack changes upstream; a pin whose file no longer matches is reported, not silently replaced. This is an additive field set on `SampleRef`, and older documents decode unchanged.
532
+
533
+ ```ts
534
+ instrument: sampler({
535
+ kick: "pack:tidal-drum-machines/RolandTR909_bd",
536
+ hat: "pack:vcsl/hihat:2",
537
+ }),
538
+ ```
539
+
540
+ | Command | Does |
541
+ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
542
+ | `/pack` or `/pack list` | the built-in catalog and your added packs, with licenses |
543
+ | `/pack add <url \| github:user/repo[/branch]> [as <name>]` | register a manifest (fetches the manifest only) |
544
+ | `/pack info <name>` | license, source, sound names |
545
+ | `/pack remove <name>` | forget an added pack (built-ins stay) |
546
+ | `/pack use <pack>/<sound>[:<n>] [as <voice>]` | add one pack sound as a voice on the focused track |
547
+ | `/pack cache [prune [<size>] \| clear]` | disk used by pack downloads and decoded audio against their caps; `prune` evicts down to the cap (or `<size>`, e.g. `500M`), `clear` evicts everything this project does not use |
548
+ | `/kit [<kit>]` | with no name, one picker of every kit (synth kits first, then sample kits); a synth kit name (`syn808`, `syn909`, `acoustic`, `lofi`, `electro`, `trap`, `default`) sets the offline drum synth; a bank (`909`, `808`, `707`, `606`, `linn`, `lm1`, `dmx`, `cr78`, `uzu`, `dirt`, or any bank name like `RolandTR909`) turns the focused drum track into a sampler on it; `/kit list` lists both |
549
+
550
+ **Bank nicknames.** Strudel's REPL registers short names for the drum machines with `aliasBank("https://strudel.b-cdn.net/tidal-drum-machines-alias.json")`, a JSON map of bank → nickname (`{"RolandTR909": "TR909", "AkaiLinn": "Linn", "EmuSP12": "SP12", …}`, 66 entries). dawg ships a snapshot of that file (taken 2026-10-07) so nicknames work offline, and refreshes it whenever it fetches the `tidal-drum-machines` manifest. A nickname works wherever a bank does: `/kit TR909`, `/kit tr808`, `/pack use tidal-drum-machines/TR909`, `pack:tidal-drum-machines/TR909_bd` in `track.ts`, the agent's `use_sound`, and the menu's **Drum kits → Strudel banks** list. Resolution order: a nickname in its exact case (`Linn` → `AkaiLinn`, as in Strudel), then dawg's short names (`909`, `linn` → `LinnDrum`, unchanged from before), then a bank name in any case, then a nickname in any case (`sp12` → `EmuSP12`), then a bank suffix. Pins always store the full bank name (`pack:tidal-drum-machines/RolandTR909_bd`), so documents never depend on alias data.
551
+
552
+ **Cache sizes.** Pack downloads (`~/.cache/dawg/packs/files/`, shared by every project) are capped at 2 GiB and decoded audio (`<project>/.dawg/assets/`) at 1 GiB per project; `DAWG_PACKS_CACHE_MAX` and `DAWG_ASSETS_CACHE_MAX` override them (`500M`, `4G`, or bytes). Both evict least recently used files first, and neither evicts a file the open project's score uses, even when that project alone is over the cap. An evicted pack file is fetched again from its pinned URL and checked against its pinned sha256 the next time it plays, so eviction only ever costs a download. Manifests are small and never evicted. Decoded samples also share a 512 MiB in-memory LRU per process.
553
+
554
+ `/menu` (Ctrl-K) has a **Sounds** section: drum kits, instruments (the Salamander piano and `gm_*` soundfonts, Strudel naming), "use a sound", and packs. The agent has `list_packs`, `search_sounds {query}` and `use_sound {sound, track?, voice?}`. A pack sound plays from keyboard play mode like any sampler voice. `kitFromBank(bank)` in `src/audio/packs.ts` returns the voice map for a bank (kick, snare, hat, …) for other kit lists.
555
+
556
+ Every pack is fully supported, whatever its license; dawg records each sample's pack and license (or `none stated`) in the pinned voice. A render that uses pack sounds names the packs in the WAV's INFO comment and prints a `credits ·` line, and a project that uses a CC-BY or CC-BY-SA pack gets a `CREDITS.md` with the required attribution (dawg leaves a hand-written `CREDITS.md` alone).
557
+
558
+ Built-in catalog (manifests are the GitHub-raw equivalents of the files Strudel's REPL loads from its CDN; licenses read from each repository on 2026-10-07):
559
+
560
+ | Pack | Manifest | Samples from | License |
561
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------------------- |
562
+ | `tidal-drum-machines` | `https://raw.githubusercontent.com/felixroos/dough-samples/main/tidal-drum-machines.json` | ritchse/tidal-drum-machines (684 sounds: TR-808, TR-909, LinnDrum, …) | none stated |
563
+ | `dirt-samples` | `github:tidalcycles/dirt-samples` → `https://raw.githubusercontent.com/tidalcycles/dirt-samples/main/strudel.json` | tidalcycles/Dirt-Samples (219 sounds) | none stated |
564
+ | `uzu-drumkit` | `github:tidalcycles/uzu-drumkit` | tidalcycles/uzu-drumkit | Unlicense |
565
+ | `vcsl` | `https://raw.githubusercontent.com/felixroos/dough-samples/main/vcsl.json` | sgossner/VCSL (fetched per file; the repo is ~4 GB) | CC0-1.0 |
566
+ | `piano` | `https://raw.githubusercontent.com/felixroos/dough-samples/main/piano.json` | Salamander Grand Piano V3, Alexander Holm | CC-BY-3.0 |
567
+ | `mridangam` | `https://raw.githubusercontent.com/felixroos/dough-samples/main/mridangam.json` | yaxu/mrid, Arthur Carabott 2022 | CC-BY-SA-4.0 (per its README) |
568
+ | `emu-sp12` | `https://raw.githubusercontent.com/felixroos/dough-samples/main/EmuSP12.json` | ritchse/tidal-drum-machines | none stated |
569
+ | `gm` | `https://gleitz.github.io/midi-js-soundfonts/FluidR3_GM/names.json` | FluidR3_GM via gleitz/midi-js-soundfonts (code MIT) | CC-BY-3.0 |
570
+ | `gm-musyngkite` | `https://gleitz.github.io/midi-js-soundfonts/MusyngKite/names.json` | Musyng Kite via gleitz/midi-js-soundfonts | CC-BY-SA-3.0 |
571
+
572
+ felixroos/dough-samples, the manifest host, has no license file. Strudel's own `gm_*` sounds come from a different soundfont set; dawg uses FluidR3_GM with the same names.
573
+
574
+ ## Wavetable synth
575
+
576
+ `instrument: "wavetable"` turns a track into a wavetable oscillator. A wavetable is a stack of single-cycle frames; the position `wt` (0..1) scans across them, mixing neighbouring frames smoothly. Parameter names and meanings follow Strudel's documented controls (`packages/core/controls.mjs` JSDoc on strudel.cc); the oscillator in `src/audio/wavetable.ts` is dawg's own, written from those docs and the WAV format, with no Strudel code.
577
+
578
+ | Parameter | Range | Default | Meaning |
579
+ | -------------------------------------------- | -------------- | ----------------- | ------------------------------------------------------------------------- |
580
+ | `wt` | 0..1 | 0 | position in the table (automatable: `automate wt points 0:0 4:1`) |
581
+ | `wtenv` | -1..1 | 0 | position envelope amount, added to `wt` |
582
+ | `wtattack` `wtdecay` `wtsustain` `wtrelease` | s, s, 0..1, s | 0.01, 0.1, 1, 0.1 | position envelope shape |
583
+ | `wtrate` `wtdepth` | 0..50 Hz, 0..1 | 0, 0 | sine LFO on the position |
584
+ | `warp` `warpmode` | 0..1, mode | 0, `none` | bends the read phase: `asym`, `bendp`, `bendm`, `bendmp`, `sync`, `quant` |
585
+ | `wtphaserand` | 0..1 | 0 | start phase randomness, seeded per note so renders stay reproducible |
586
+
587
+ Tables. Four built-ins are generated in code and work offline: `basic` (sine → triangle → saw → square), `pwm` (pulse 50% → 5%), `formant` (vowels a → e → i → o → u) and `harmonics` (1 → 32 harmonics). Strudel's `wt_` sounds come from the `uzu-wavetables` pack (`github:tidalcycles/uzu-wavetables`, Unlicense) through the normal pack path: `wt_digital:2` means `pack:uzu-wavetables/wt_digital:2`, fetched once, pinned by sha256 and cached like any pack sound. Any other pack sound works too. Frames follow the Serum/Vital WAV convention: a `clm ` chunk reading `<!>2048 …` gives the frame length; otherwise a file whose length is a multiple of 2048 samples is 2048-sample frames, and anything else is one single-cycle frame (the AKWF convention).
588
+
589
+ Band-limiting. Each frame is kept as its harmonic spectrum and rendered into one table per octave holding only the harmonics below Nyquist for that octave, so a high note never aliases. Tables are oversampled and read with 4-point Hermite interpolation. The arithmetic is plain float64 in a fixed order, so inline, worker and cold or warm cache renders are byte-identical (a renderer parity test checks it).
590
+
591
+ The wavetable is an oscillator of the synth voice (see [Synth](#synth)): a wavetable track also takes every `synth` parameter, so `attack`/`release`, the `lpf`/`lpenv` filter envelopes, `fm`, `unison`/`detune`/`spread`, `vib` and `penv` shape it as they shape a saw. The band-limit level follows the instantaneous pitch, so vibrato, pitch envelopes and detuned unison voices stay alias-free. Live play mode plays wavetable tracks.
592
+
593
+ ```ts
594
+ instrument: wavetable("wt_digital:2", { wt: 0.3, wtenv: 0.5, wtdecay: 0.4, warp: 0.2, warpmode: "bendp" }),
595
+ ```
596
+
597
+ | Command | Does |
598
+ | ------------------------------------------ | ----------------------------------------------------------------------------------------------- |
599
+ | `/wt` · `/wt list` | the focused track's wavetable · built-in tables and the `wt_` sets |
600
+ | `/wt <table>` | make the focused track a wavetable track (`basic`, `wt_vgame:3`, `pack:…`, a project `vox.wav`) |
601
+ | `/wt <0..1>` | set the position |
602
+ | `/wtenv`, `/wtattack` … `/wtphaserand <n>` | set one parameter; `/warpmode <mode>` sets the warp mode |
603
+
604
+ The menu's **Parameters** section lists the table picker and every wavetable parameter for a wavetable track, and **Sounds** has a "wavetable synth" row. The agent's `set_wavetable` tool takes the same table names and parameters.
605
+
606
+ ### Wavetables from audio
607
+
608
+ The agent's `make_wavetable` tool (`src/audio/wavetable-maker.ts`, `src/media/wavetable.ts`; `dawg media wavetable <file> <name>`) builds a table from any audio file in the project:
609
+
610
+ - **Region.** `start`/`end` in seconds, or automatic: the most stable tonal stretch of up to 2 s (high YIN clarity, steady pitch, enough level), else the loudest 2 s.
611
+ - **Method.** `slice` reads one pitch period at each frame's time (YIN-style pitch detection through the FFT), resamples it to 2048 samples with cubic interpolation and spreads the loop-point mismatch over the cycle so it does not click. `spectral` takes, for each frame, the source's magnitude around each harmonic of the reference pitch and uses a fixed phase per harmonic, so frames morph without phase cancellation; it suits vocals, pads and noise. `auto` (the default) picks `slice` when the region is clearly periodic.
612
+ - **Clean-up.** DC removed, fundamental rotated to start as a rising sine (so neighbouring frames line up), optional `smooth` across neighbours, and per-frame (default), whole-table or no normalisation to 0.98 peak.
613
+ - **Report.** The result gives the path, sha256, frame count, method, region, detected pitch and a short sweep description (spectral centroid per frame span, how smooth the morph is), and the `set_wavetable` call that plays it.
614
+
615
+ All arithmetic is float64 in a fixed order with no randomness, so the same input and options give the same bytes. Project tables live under `tracks/<slug>/wavetables/`; `/wt list` and the menu's table picker list them, `/wt vox.wav` (or a full `tracks/…` path) picks one for the focused track, and `track.ts` refers to it as `wavetable("./wavetables/vox.wav")`. The score keeps the project path and its sha256; evaluation re-hashes it like sampler files. A file that changed since it was picked plays with a warning; a missing one is a load problem naming `make_wavetable`.
616
+
617
+ ## Rhythm (Euclidean rows)
618
+
619
+ A drum part can be stored as generators instead of notes: each row owns one voice of a `kit` or oneshot `sampler` track and dawg expands it into ordinary notes, so rendering, diffs and sync are unchanged while you, the agent and `track.ts` edit four numbers instead of sixteen hits. The model follows the Torso T-1's Shape and Groove sections; the Euclidean patterns and rotation match Strudel's `euclid`/`euclidRot` exactly (`E(3,8)` is `x..x..x.`, a positive rotate moves the pattern later).
620
+
621
+ ```ts
622
+ // tracks/drums/track.ts
623
+ import { track, euclid, grid } from "dawg";
624
+
625
+ export default track({
626
+ name: "drums",
627
+ instrument: "kit",
628
+ rhythm: [
629
+ euclid("kick", 4, 16),
630
+ euclid("hat", 7, 16, 2, {
631
+ velocity: 0.5,
632
+ accent: 0.6,
633
+ accents: 3,
634
+ swing: 0.15,
635
+ }),
636
+ grid("snare", "....X.......x..."),
637
+ euclid({
638
+ voice: "openhat",
639
+ pulses: 1,
640
+ steps: 16,
641
+ rotate: 14,
642
+ repeats: 3,
643
+ time: "1/32",
644
+ ramp: -0.6,
645
+ }),
646
+ ],
647
+ });
648
+ ```
649
+
650
+ | Field | Range (default) | T-1 parameter | Behaviour |
651
+ | -------------------- | ------------------------------------ | ---------------- | --------------------------------------------------------------------------------------------- |
652
+ | `steps` | 1..64 (16) | Steps | Length of one pass; the row repeats every pass to the end of the loop. |
653
+ | `pulses` | 0..steps (4) | Pulses | Hits spread over the steps by Bjorklund's algorithm. |
654
+ | `rotate` | -64..64 (0) | Rotate | Shifts the pattern later by n steps (negative: earlier), Strudel's direction. |
655
+ | `division` | 1/32, 1/16t, 1/16, 1/8t … 1/1 (1/16) | Division | Length of one step. |
656
+ | `grid` | `x` hit, `X` accent, `.` rest | per-step editing | Explicit steps instead of pulses; its length is the step count. |
657
+ | `repeats` | 0..16 (0) | Repeats | Extra hits after each pulse, cut off by the next pulse (T-1 "choke" mode). |
658
+ | `time` | a division (= `division`) | Time | Spacing of those repeats. |
659
+ | `pace` | -1..1 (0) | Pace | > 0 slows the repeats down progressively, < 0 speeds them up. |
660
+ | `ramp` | -1..1 (0) | Ramp | Velocity across the repeats: > 0 builds, < 0 fades. |
661
+ | `velocity` | 0..1 (0.8) | Velocity | Base velocity. |
662
+ | `accent`, `accents` | 0..1 (0), 1..pulses (1) | Accent | Lifts `E(accents, pulses)` of the pulses (or the `X` steps) towards full velocity. |
663
+ | `gate`, `legato` | 0.05..4 steps (1), boolean | Sustain | Note length in steps; `legato` holds each hit to the next one (Strudel `euclidLegato`). |
664
+ | `probability`,`seed` | 0..1 (1), 0..1e6 (0) | Probability | Drops pulses (and their repeats) by a seeded hash: the same seed always drops the same hits. |
665
+ | `swing` | -0.5..0.5 step (0) | Timing | Every second step later (> 0) or earlier. |
666
+ | `nudge` | -0.5..0.5 step (0) | Delay | The whole row later or earlier. |
667
+ | `cycles` | 1..16 entries | Cycles | Per-pass overrides of `pulses`, `rotate`, `repeats`, `probability`, `velocity`, used in turn. |
668
+
669
+ Rows regenerate when the loop length or meter changes. Editing a generated lane by hand (play-mode recording, `hit`, the agent's `add_drums`) freezes that row: the row is dropped and its notes stay as plain notes. `euclid <voice> freeze` does the same on purpose, `euclid <voice> off` removes the row and its notes.
670
+
671
+ Prompt grammar: `euclid kick 4 16`, `euclid hat 7 16 rotate 2`, `euclid hat swing 0.2 prob 0.8 seed 3` (named fields merge into the existing row, `default` resets one), `euclid snare off|freeze`, `grid snare ....X.......x...`. The agent's `set_rhythm` tool takes the same rows and its prompt prefers it for drums.
672
+
673
+ **Editor.** `/euclid [voice]`, or Rhythm in `/menu`, opens a T-1-style editor on the focused kit or oneshot sampler track: one row per voice with its step grid (`x` hit, `X` accent, `·` rest) and summary (`E(4,16)`). Every change runs one `euclid …` command, so it is one receipt and one undo step, and the edited voice plays once (audition) after it lands. The hits show on the highway like any notes.
674
+
675
+ | Key | Action |
676
+ | --------------------------- | ------------------------------------------------- |
677
+ | `↑ ↓` / `j k` | select voice |
678
+ | `← →` / `h l` / `- +` | nudge the selected parameter |
679
+ | `Tab` / `Shift-Tab` (`] [`) | next / previous parameter |
680
+ | digits, `.`, `-`, Backspace | type a value, Enter applies |
681
+ | Enter | add a row for a voice without one; keep (looping) |
682
+ | Space | start or stop the audition loop (staging) |
683
+ | `a` / `c` | A/B / solo ↔ in context, while the loop plays |
684
+ | `x` / Delete, `f` | remove the row and its notes / freeze it to notes |
685
+ | Esc | cancel typing, revert staged changes, then close |
686
+
687
+ ## Chords
688
+
689
+ `core/chords.ts` is one pure, deterministic chord engine shared by the agent tools, the SDK helpers and play mode. Its input model follows the Telepathic Instruments ORC-1 Orchid; the parts Orchid does not document are dawg's own and are marked so.
690
+
691
+ From Orchid's documentation and reviews:
692
+
693
+ - Four chord-type buttons, `dim min maj sus`, and four extension buttons, `6 m7 M7 9`. Hold a type and press a root for the chord; extensions add notes on top of a type (or of a Key-mode chord) and any number of them combine (Maj + M7 + C = Cmaj7). Extensions never play alone.
694
+ - Key mode: once a key is set, every key plays the chord that fits the key (C major: D plays Dm). Type and extension buttons still work on top for less obvious choices.
695
+ - Voicing dial: each click moves the chord's lowest note up an octave, or its highest note down, walking through inversions and up or down the keyboard.
696
+ - Bass: an optional engine that plays the chord's root under every chord. Its menu (manual 10.2, "How to use Bass on Orchid") has Chords Only (bass only under chords), Unison (single notes play bass and treble together), Single Notes (single notes play only bass; the treble sounds only for chords) and Solo (the treble is muted, even for chords).
697
+ - Performance modes: Strum (and 2-octave), Slop (random timing per note for a humanised feel that varies with every press), Arpeggiator (and 2-octave, tempo-synced; more chord notes make a longer pattern), Pattern (fixed rhythms) and Harp (a sweep across several octaves).
698
+ - "Secret chords" (firmware 3.84+, Orchid manual section 14.8): two type buttons held together play extra chords. dim+sus is a power chord (C5), maj+sus augmented (C+), min+sus Cm(add4); min+dim with the 6 button is Cm(b6), maj+dim with 6 is C(b6), and maj+min with m7 is C7♯9. dawg's `COMBINED_TYPES` is this table.
699
+ - Orchid has no generator that writes a progression for you. Key mode is its "easy chord progressions" feature: you pick the order, every key is in key.
700
+
701
+ dawg's own design:
702
+
703
+ - Secret chords play without their listed extension, since a latched pair is already deliberate; the listed extension is part of the chord and is not stacked again. Other extensions add on top (`Cm(add4,7)`). With three types latched, the first two in `dim min maj sus` order count.
704
+ - Key-mode chords are the diatonic triads (sevenths when asked) of `major`, `minor`, `dorian`, `phrygian`, `lydian`, `mixolydian`, `locrian` and `harmonic-minor`. A key outside the scale plays the chord borrowed from the parallel major or minor when that scale has the note (C major: E♭, A♭, B♭), otherwise a passing diminished seventh.
705
+ - Voice leading: a voicing is the chord in root position from C4, rotated by the dial. When there is a previous chord, every rotation within one octave of the dial is scored by movement (each new voice's distance to the nearest old voice, plus the reverse, so common tones are free), kept within C3–G5 where it fits, and the cheapest wins; ties go to the rotation nearest the dial. Spread `open` drops the second voice from the top an octave (drop 2), `wide` also the fourth.
706
+ - Bass is the root (or slash bass) in C2–B2, one sustained note per chord. The five bass modes `off`, `chords`, `unison`, `single` and `solo` follow Orchid's menu; under `unison` the agent and SDK use the chord's root and ignore a slash bass, and in play mode a single note's bass is the same pitch class two octaves under the pressed octave.
707
+ - Pattern perform mode: Orchid ships fixed rhythm patterns but does not publish them, so dawg's thirteen are its own (`eighths`, `sixteenths`, `offbeat`, `pop`, `charleston`, `bossa`, `skank`, `gallop`, `half-time`, `tresillo`, `oom-pah`, `roll`, `pick`), chosen by name or number 1–13. Each is a list of hits (beat, length, accent, which voices: all, the upper voices, the root an octave down, or chosen chord tones) over one or two bars, repeated over the held length and cut at its end. Accents scale the press velocity.
708
+ - Perform modes `block`, `strum-up`, `strum-down` (1/32-beat gap), `arp-up`, `arp-down`, `arp-updown`, `arp-random` (seeded) with a grid-aligned `rate` and 1–4 `octaves`, `harp` (a 1/16-beat upward sweep across the octaves that rings to the end of the chord), and `slop` (Orchid's humanised block chord: each voice lands up to 1/16 beat late, chosen by the press's seed, so repeats differ but a recording replays exactly).
709
+ - Progressions (dawg's "auto"): eleven presets (`axis` I–V–vi–IV, `sad-pop` vi–IV–I–V, `fifties` I–vi–IV–V, `ii-v-i`, `turnaround` I–vi–ii–V, `canon`, `aeolian` i–VI–III–VII, `andalusian` i–VII–VI–V, `minor-ii-v`, `dorian-vamp`, `mixolydian-rock` I–♭VII–IV–I) and four styles, `pop`, `jazz`, `modal` and `classical`, that walk a weighted graph of scale-degree transitions (tonic → predominant → dominant → tonic, with plagal and vi–IV moves for pop and the cycle of fifths for jazz) from I with a seeded PRNG. A progression of four or more chords ends on a dominant-function chord (V or vii°; IV or vii in modal) so the loop leads home. The same key, style, length and seed always give the same chords.
710
+
711
+ Agent. `suggest_progression {key?, chords? | style?, length?, seed?, sevenths?, inversion?, spread?}` (read-only) returns each chord's name, roman numeral, voicing and bass. `write_chords {trackId?, chords, key?, start?, beatsPerChord?, perform?, pattern?, rate?, octaves?, strum?, velocity?, bass?, bassMode?, bassTrackId?, inversion?, spread?}` writes them as one revision. `chords` takes roman numerals in the key (`ii7`, `bVII`, `V/V`) or symbols (`Cm7`, `F/A`). The system prompt tells the agent to use these tools for chord parts, so its chords are diatonic and voice-led rather than hand-stacked.
712
+
713
+ SDK. `chord("Cm7", start, length, opts)` and `progression("ii7 V7 Imaj7", { key, from, each, perform, pattern, rate, octaves, strum, seed, voicing, spread, part, bass })` expand to notes at evaluation; see [docs/project-format.md](./docs/project-format.md). The vendored SDK stays one import-free file: `core/sdk/v1.ts` carries a generated copy of the engine (`bun core/sdk/sync-chords.ts`, checked by a test).
714
+
268
715
  ## Play mode (computer keyboard)
269
716
 
270
717
  `Ctrl-P` or `/play` turns the computer keyboard into a piano for the focused track, using the "musical typing" layout GarageBand, Logic, BandLab, FL Studio and Ableton share. `Esc` or `/play off` leaves it and every normal binding is back. Typing `/` starts a slash command without leaving the mode (`/click 40%`, `/play off`).
@@ -281,11 +728,40 @@ In the TUI, oneshot sampler tracks show one highway lane per voice, labelled by
281
728
  | `Shift-R` | replace: bars you play over are cleared first (default: overdub) |
282
729
  | `M` | click on/off |
283
730
  | `Space` | play/stop; with record armed and stopped, counts in, then records |
731
+ | `?` | the play-mode keys and current settings (any key closes) |
732
+ | `/` | type a slash command without leaving (`/click 40%`) |
284
733
  | `Esc` | leave play mode |
285
734
 
735
+ ### Chord mode
736
+
737
+ Play mode has a chord sub-mode modelled on the Orchid's Key mode. It is `auto` by default when the focused track can play chords (pitched synths, piano, soundfonts, keyed samplers; not tracks whose instrument, name or id says bass, kit, drum or perc), otherwise `manual`. Choosing a mode by hand (`Q`, `/chords`, the menu) sticks for the session.
738
+
739
+ - `auto`: each note key plays the diatonic chord of the song key on that root (C major: `S` plays Dm, `G` plays G). Keys outside the scale borrow from the parallel major or minor. The strip labels every white and black key with its chord.
740
+ - `manual`: note keys play single notes as before; latch a chord type or extension and they play that chord on the pressed root.
741
+ - `off`: plain play mode; the chord keys below go back to being unmapped.
742
+
743
+ Terminals send no key releases, so the Orchid's held left-hand buttons are latches here: press once to latch, again to release, `0` clears them all.
744
+
745
+ | Key | Does (chord mode on) |
746
+ | --------- | ------------------------------------------------------------------------------- |
747
+ | `Q` | auto ⇄ manual |
748
+ | `1 2 3 4` | latch chord type dim / min / maj / sus (two latched make a combined chord) |
749
+ | `5 6 7 8` | latch extension 6 / m7 / M7 / 9 (any number; on top of the type or auto chord) |
750
+ | `0` | clear every latch |
751
+ | `-` / `=` | voicing dial down / up (-12..12; walks inversions) |
752
+ | `9` | next perform mode (block, strum-up, strum-down, arp-up, …, harp, slop, pattern) |
753
+ | `B` | next bass mode: off, chords, unison, single, solo (bass in C2–B2) |
754
+ | `N` | play the suggested next chord (the `next` chord in the header) |
755
+
756
+ The header gains `AUTO C major · Dm (ii) · next G`: mode, key (`(assumed)` when the score has none and C major is used), the last chord with its numeral, and the suggested next chord. A legend row under the keyboard strip lists the number-row latches (`1 dim 2 min 3 maj 4 sus 5 6 6 m7 7 M7 8 9 0 clear -= voicing 0 9 block b bass off n next q auto`), with latched ones lit; at 80 columns the row ends where it fits. The full chord state is in the `?` panel, in the header's words: `chords AUTO C major · Dm (ii) · next G · min+m7 · voicing +1 · arp-up · bass chords`. The suggestion comes from the progression engine: the next chord of the chosen preset when the last chord is in it, otherwise a seeded step of the style's transition graph.
757
+
758
+ Each chord is voice-led from the previous one and sounds through the live voice path. Recording quantizes the press like a note and lays the chord out with the perform mode over its held length (arpeggios at `rate`, `grid` by default; patterns from the press's quantized start), plus the bass note. Under `unison`, `single` and `solo` a single note in manual mode also records its bass (and, for `unison`, the note itself); `solo` records chords as bass only; each bar is still one revision and one undo step.
759
+
760
+ `/chords` with no argument prints the settings; `/chords auto|manual|off`, `voicing <n>`, `spread close|open|wide`, `bass off|chords|unison|single|solo` (`on` means `chords`), `sevenths on|off`, `perform <mode>`, `pattern <1..13|name>` (also selects the pattern perform mode), `rate grid|1/4|1/8|1/16|1/32`, `octaves 1..4`, `preset <name>|none`, `style pop|jazz|modal|classical`. `key <tonic> <mode>` (`key A minor`, `key F# dorian`, `key none`) sets the song key as one score edit. The same settings and the key are in `/menu` under Chords.
761
+
286
762
  The base octave follows the instrument: C3 (MIDI 48) by default, C2 for bass instruments or tracks named bass, C4 for saw/square/triangle/pluck leads. Kits start at C2, so `A` is the GM kick, `S` the snare, `T` the closed hat. On a one-shot sampler track the keys walk the voices in name order from slot 36 (`A` the first voice, `W` the second, chromatically), and the strip shows voice names; a keyed sampler starts at the C below its lowest root and repitches from it.
287
763
 
288
- The header reads `PLAY C3–F4 vel 100 ● REC click ✓ grid 1/16` with a beat flash, and the row under it is the keyboard with sounding keys lit. Both repaint in place; nothing scrolls per note.
764
+ The header reads `PLAY C3–F4 ● REC` with a beat flash and ends in `? keys · esc leave`; velocity, grid, click and count-in are in the `?` panel, and a key that changes one (`C`, `V`, `M`) says so in the header's status for a moment. The row under it is the keyboard with sounding keys lit. Both repaint in place; nothing scrolls per note.
289
765
 
290
766
  Notes sound through the track's own instrument, effects and volume, rendered by the same per-instrument voice code as the loop, and mix into the stream about 60 ms ahead of now (play mode lowers the queue lead from 200 ms and restores it on exit). That works over silence and over the playing loop. A muted or unsoloed track still sounds while you play it. With audio backend `none` the keys still record.
291
767
 
@@ -297,31 +773,124 @@ Recording: with record armed and the transport running, each note is quantized t
297
773
 
298
774
  `/click on|off|<volume>` (`/click 40%`, `/click 0.4`) or `M` in play mode. An accented downbeat and lighter beats at the transport tempo and the score's meter, mixed as a separate monitoring bus. It is never part of a loop render, a stem, `dawg render`, or `/export`; tests compare those byte for byte with the click on. `/count-in 0|1|2` sets how many bars of click play before recording starts (default 1); the header counts down and flashes the beat, so it also works with backend `none`.
299
775
 
776
+ ## Drum patterns and kits
777
+
778
+ **Patterns.** dawg ships a library of 31 starting grooves, written for dawg from the defining placements of each style (no transcriptions). A pattern is a set of rhythm rows, one per voice, so after applying it every part is still a few Euclidean or grid parameters you can change in `/euclid`, with the prompt grammar, or in `track.ts`.
779
+
780
+ | Command | Does |
781
+ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
782
+ | `/pattern` | picker of every pattern; moving the cursor plays one bar of it (silent while the loop plays), typing filters, Enter applies |
783
+ | `/pattern <name>` | replace the focused drum track's notes and rows with the pattern; the tempo moves to the pattern's tempo only when it is outside the pattern's range |
784
+ | `/pattern <name> keep-tempo` / `tempo` | never / always move the tempo |
785
+ | `/pattern list` | the library as text: name, tempo range, tags, voices |
786
+
787
+ Applying to a missing track creates a kit track; an empty melodic track becomes a kit track; a melodic track with notes is refused. Voices the track lacks (a sampler kit without a rim, say) are skipped and named in the receipt. Every apply is one revision and one undo step. In `track.ts`, `pattern("boom-bap")` returns the rows: `rhythm: pattern("boom-bap")`, or `[...pattern("house"), euclid("rim", 5, 16)]` to add one.
788
+
789
+ Patterns: `house`, `disco`, `techno`, `minimal`, `electro`, `breakbeat`, `amen-style`, `dnb`, `halftime`, `boom-bap`, `lofi`, `trap`, `drill`, `reggaeton`, `dancehall`, `one-drop`, `afrobeat`, `afrobeats`, `bembe`, `tresillo`, `son-clave`, `bossa-nova`, `samba`, `cumbia`, `garage`, `jersey-club`, `footwork`, `rock`, `funk`, `shuffle`, `euclid-poly`. Sounds → Drum patterns in `/menu` lists them too.
790
+
791
+ **Kits.** A `kit` track plays the built-in drum synth. `kit: "<name>"` on the track (`/kit <name>`, or `set_drum_kit` for the agent) chooses one of six synthesized kits, all offline and deterministic; a track without `kit` sounds exactly as before.
792
+
793
+ | Kit | Sound |
794
+ | ---------- | ------------------------------------------------------------ |
795
+ | `default` | the original voices |
796
+ | `syn808` | long sub boom, snappy snare, metallic hats |
797
+ | `syn909` | punchy clicky kick, bright noisy snare |
798
+ | `acoustic` | beater kick, wire snare, darker cymbals |
799
+ | `lofi` | soft round kick, crushed and dark (alias `dusty`) |
800
+ | `electro` | tight short kick, clicky rim, ticking hats (alias `minimal`) |
801
+ | `trap` | distorted long 808, crisp hats, high snare |
802
+
803
+ Sample kits from packs (`/kit 909` and the rest, see **Sample packs**) sit in the same picker after the synth kits. `/kit syn909` on a sampler kit turns it back into a synth kit track, moving hits and rows to the drum voices of the same name. The agent has `list_drum_patterns`, `apply_drum_pattern {name, trackId?, tempo: auto|keep|set}` and `set_drum_kit {kit, trackId?}`, and its prompt starts genre grooves from a pattern.
804
+
300
805
  ## Menus
301
806
 
302
- `/menu` or `Ctrl-K` (on an empty prompt, in play mode too) opens the edit menu, drawn with the same overlay as the model picker. Every edit the agent can make is reachable from it with keys alone, and each row shows its current value and the command it runs, so the menu teaches the commands. `/menu effects` opens a section directly.
303
-
304
- | Section | Rows |
305
- | ---------- | ---------------------------------------------------------------------------------------------- |
306
- | Track | name, instrument, mute, solo, volume, pan |
307
- | Parameters | instrument; a sampler's mode and voices (synths have no knobs beyond the instrument) |
308
- | Effects | filter (on, cutoff, resonance), delay (on, beats, feedback, mix), reverb (on, mix, size) |
309
- | Automation | each `AUTOMATION_LANES` lane: its points as `beat N value` rows, add points, ramp, clear lane |
310
- | Mix | every track's volume, pan, mute and solo; choosing another track focuses it first |
311
- | Transport | play, tempo, beats per bar, loop bars, grid, click, count-in |
312
-
313
- | Key | Does |
314
- | --------------------------- | -------------------------------------------------------------------------- |
315
- | `↑` `↓` / `k` `j` | move |
316
- | `Enter` / `Space` | open a section, toggle, pick from a list, or start typing a value |
317
- | `→` `←` / `l` `h` / `+` `-` | nudge a number by its step (cutoff moves 25%), cycle a choice, open / back |
318
- | digits | type a value; `Enter` sets it, `Esc` cancels |
319
- | `/` | filter the current list by name, value or command |
320
- | `x` / `Delete` | remove the selected automation point |
321
- | `Esc` | clear the filter, then back one level, then close |
807
+ `/menu` or `Ctrl-K` (on an empty prompt, in play mode too) opens the edit menu, drawn with the same overlay as the model picker. Every edit the agent can make is reachable from it with keys alone. Each row shows a plain label and the current value with its unit (s, Hz, oct, st, dB, BPM, bars); the line under the list describes the focused row and shows, dimmed, the prompt command the row runs, so the menu teaches the commands. `/menu <section>` opens a section directly (`/menu effects`); the old names `parameters`, `sounds`, `track`, `automation` and `transport` still work.
808
+
809
+ | Section | Rows (most used first) |
810
+ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
811
+ | Sound | instrument, preset; a synth's attack, decay, sustain, release, filter cutoff/res/env, detune, vibrato, FM amount, then **advanced** with every synth parameter; a wavetable track's table and wavetable parameters; a sampler's mode and voices; **browse sounds** (instruments, wavetables, packs) |
812
+ | Effects | the core effects (filter, auto filter, distortion, tremolo, compressor, chorus, delay, reverb) with presets and simple parameters, **more effects** (dj filter, vowel, bitcrush, phaser, leslie, post gain, orbit, duck), and **advanced** per effect (see Effects) |
813
+ | Rhythm | the euclid editor (`/euclid`), drum patterns (`/pattern`), drum kits (`/kit`, synth then samples) |
814
+ | Chords | play-mode chord mode, key tonic and mode, voicing, spread, bass, sevenths, perform, pattern, arp rate, arp octaves, progression, style |
815
+ | Mix & automation | the focused track's name, mute, solo, volume, pan; **all tracks** (choosing one focuses it); **automation**: each `AUTOMATION_LANES` lane with its points as `beat N value` rows, add points, ramp, clear lane |
816
+ | Project | play, tempo, beats per bar, loop length, grid, click, count-in bars |
817
+
818
+ Every list, picker and editor uses the same keys (see **Keys** below). In the menu:
819
+
820
+ | Key | Does |
821
+ | --------------------------- | ------------------------------------------------------------------------- |
822
+ | `↑` `↓` / `k` `j` | move |
823
+ | `Enter` / `→` / `l` | open a section, pick from a list, or start typing a value |
824
+ | `←` `→` / `h` `l` / `-` `+` | adjust a value by its step (cutoff moves 25%) or cycle a choice |
825
+ | `Space` | toggle on/off; elsewhere, hear the focused track (see Previewing changes) |
826
+ | digits | type a value on a focused value row; `Enter` sets it, `Esc` cancels |
827
+ | `/` | filter the current list by name, value or command |
828
+ | `x` / `Delete` | reset the focused value to its default; on an automation point, remove it |
829
+ | `Esc` / `←` / `h` | clear the filter, then back one level, then close |
830
+ | `?` | the keys for this screen |
322
831
 
323
832
  Automation rows take `beat:value` pairs (`2:800` or `0:200 4:8000`); a ramp is two pairs, start and end, and the renderer interpolates between points. Turning an effect's first field up switches it on with defaults. Each change runs the command it shows through the normal prompt path, so it is one `ScoreOperation`, one receipt, one undo step, and it syncs to other windows and the project files.
324
833
 
834
+ ## Previewing changes
835
+
836
+ Hear a sound change before you keep it. In the edit menu (every section: Sound, Effects, Rhythm, Chords, Mix), `Space` starts a short loop of the focused track; `Space` again stops it. The song pauses while the loop plays, so only one thing sounds at a time.
837
+
838
+ The loop is the track's own notes over its loop region when that is four bars or shorter, otherwise the two bars under the playhead (or the track's first two bars with notes, if those are empty). A track with no notes plays a short phrase by role: a chord for pads and keys, a riff for bass and leads, a groove for kits and drums, and one held note for wavetables so position and envelope changes are audible. It plays solo by default; `c` switches to the whole mix with the track in it.
839
+
840
+ While the loop plays, each change you make is **staged**, not committed. The loop re-renders only the changed track through the normal renderer and stem cache, in the render worker, and swaps it in within about 100 ms (a reverb mix change re-mixes the cached tail and is heard in about 65 ms). Every swap, here and in the song player, crossfades old to new over 20 ms at the same beat, so changes never click; renders and exports never pass through the crossfade. Held keys are coalesced so only the latest value renders. The menu title shows `●` and `B staged N`, and each changed row shows the staged value beside the committed one (`mix 0.5 ← 0.3`).
841
+
842
+ | Key | While auditioning |
843
+ | ------- | ------------------------------------------------------------ |
844
+ | `Space` | start or stop the loop of the focused track |
845
+ | `c` | solo ↔ in context (the whole mix with the track) |
846
+ | `←` `→` | change the focused value; staged and heard at once |
847
+ | `a` | A/B: flip between the committed sound (A) and the staged (B) |
848
+ | `Enter` | keep every staged change as one revision and one undo step |
849
+ | `Esc` | revert staged changes (the score is untouched); again: back |
850
+ | `?` | the keys for this screen |
851
+
852
+ Kept changes are one `ScoreOperation` (`preview.commit`, listing the commands), so `Ctrl-Z` takes them all back at once, and they sync to other windows and the project files like any edit. If the score changes underneath (another window, the agent, an undo), the staged commands are re-applied on top of the new score; any that no longer apply are dropped, with a notice. Leaving the menu reverts anything staged. With the loop off, the menu behaves as before: each change is committed right away.
853
+
854
+ **The rhythm editor and the chord settings stage too.** `/euclid` and the chord settings (the menu's Chords section, `/menu chords`) use the same loop and keys. In `/euclid`, `Space` loops the drum track; pulses, steps, rotate, typed values, a new row, `off` and `freeze` are staged, the title shows `●` and `B staged N`, and a changed lane shows `E(5,16) ← E(4,16)`. Chord settings are window settings rather than score edits, so while the Chords section is open the loop plays the focused track's chord phrase (two bars of the song key's progression, voiced and performed by the current settings: inversion, spread, bass, sevenths, block/strum/arp, pattern); a staged setting changes the B phrase, and `a` flips back to the committed settings. `Enter` keeps every staged change as one undo step: rhythm edits as one `preview.commit` revision, and chord settings applied at once (a key change, the only one stored in the score, as one revision). `Esc` reverts with nothing written. With the loop off, both screens commit each change at once, as before.
855
+
856
+ **Lists audition on hover.** With the loop on, moving the cursor through a list plays the highlighted item on the loop: the wavetable list (built-in, pack and project tables), instruments, drum kits and patterns, and every choice list (filter type, warp mode, presets). It is the browser-preview model of Ableton and Bitwig, applied to the loop you are already hearing. Each move replaces the previous hover, so the staged count stays at one, and fast moves skip straight to the latest item. A pack item that has to be fetched shows `fetching…` in the title; the cursor keeps moving and the item plays once it arrives. `Enter` chooses the item (it stays staged until you keep), `Esc` or `←` leaves the list and drops the hover. The `/kit` and `/pattern` pickers work the same way: `Space` starts the loop, moving hears each kit or groove, `Enter` keeps it, `Esc` cancels.
857
+
858
+ | Key in a list | While auditioning |
859
+ | ------------- | --------------------------------------------------- |
860
+ | `↑` `↓` | move and hear the highlighted item on the loop |
861
+ | `Enter` | choose it (menu) or keep it (picker), one undo step |
862
+ | `Esc` / `←` | leave the list; the hover is dropped |
863
+ | `a` / `c` | A/B against the committed sound / solo ↔ in context |
864
+
865
+ **Try a prompt command.** `/try <sound command>` stages one command on the loop instead of committing it: `/try fx reverb mix 0.6`, `/try synth cutoff 800`, `/try wt pwm`. A small panel offers keep or revert; `a` flips A/B, `Enter` on keep commits it as one undo step, `Esc` drops it. Only sound commands can be tried (fx, synth, wt, kit, pattern, pack use, volume, pan and similar).
866
+
867
+ **What you see.** While the loop plays, the menu title adds a level meter of the looping track or mix: RMS as an eight-cell bar over -48..0 dBFS, the peak in dB, and `!` in the last cell when the loop clips (`♪ solo · B staged 2 · 64 ms · █████··· -9 dB`; the `ms` is the last key-to-swap time). A focused cutoff row draws its low- or high-pass curve on a log axis, an attack/decay/sustain/release row draws the envelope with the other stages, and the wavetable position row marks its place in the table:
868
+
869
+ ```text
870
+ │ cutoff (lpf/hpf) or centre (bpf) frequency ▇▇▇▇▇▇▇▇▇▇▇▅▂▁▁▁ › fx filt… │
871
+ ```
872
+
873
+ **The agent can listen too.** The `preview_sound {trackId?, changes?, bars?, context?, play?}` tool renders the same loop score for a track, or for candidate sound tool calls (`changes: [{tool: "set_wavetable", args: {...}}]`, any of `set_fx`, `set_synth`, `set_wavetable`, `set_instrument`, `set_sample`, `set_effects`, `set_mix`, `set_automation`, `set_drum_kit`, `use_sound`) applied to a copy of the score. Nothing is committed. It returns RMS and peak dBFS, the spectral centroid and a one-line description for the current and the candidate sound, plus a comparison (`3.0 dB louder, brighter (×2.00 centroid)`). When the window is quiet (no song or audition loop playing) it plays the candidate once, so the agent can say how it sounds before committing with the normal tools. `/try agent off` keeps agent previews silent (numbers only), `/try agent on` turns them back on; `DAWG_AGENT_PREVIEW=off` starts with them off.
874
+
875
+ The preview renders through the same path as the song, so filters, automation, shared orbit buses, impulse responses and ZzFX voices sound the same as those bars of the full mix. With no audio device (tests, CI, SSH) the loop does nothing audible and everything else works.
876
+
877
+ ## Keys
878
+
879
+ One grammar for every picker (`/model`, `/pattern`, `/kit`, `/resume`, the wavetable and pack lists), the menu, the `/euclid` editor and the text panels (`/help`, `/tracks`, the transcript):
880
+
881
+ | Key | Does |
882
+ | --------------------------- | -------------------------------------------------------------- |
883
+ | `↑` `↓` / `j` `k` | move (scroll in a text panel); PgUp/PgDn/Home/End page |
884
+ | `←` `→` / `h` `l` / `-` `+` | adjust the focused value |
885
+ | `Enter` | open or confirm |
886
+ | `Space` | audition or toggle |
887
+ | `/` | filter; typing then narrows the list |
888
+ | `Esc` | back one level: clears the filter or a typed value first |
889
+ | `?` | the keys for the current screen, drawn over it; any key closes |
890
+ | digits | type a value, only where a value is focused |
891
+
892
+ Every screen ends in a one-line footer of its keys that fits 80 columns (parts drop from the middle when narrower; `esc` and `? keys` stay). `?` on an empty prompt lists the prompt keys and the three ways in. Play mode is the one exception: its letters and number row are piano keys and chord latches (the GarageBand "Musical Typing" convention); its `?` panel says so.
893
+
325
894
  ## Release
326
895
 
327
896
  Bump `version` in `package.json` and add its section to `CHANGELOG.md` in a pull request, then merge it. When Check passes on `main`, the annotated `v<version>` tag, the immutable GitHub Release (tarball, `SHA256SUMS` and a provenance attestation) and the npm publish of `@hraness/dawg` follow automatically. See [docs/publishing.md](./docs/publishing.md).