@hraness/dawg 0.2.0 → 0.4.0
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.
- package/CHANGELOG.md +201 -0
- package/DAWG.md +762 -11
- package/README.md +79 -30
- package/core/chords.ts +1724 -0
- package/core/diff.ts +261 -0
- package/core/euclid.ts +670 -0
- package/core/fx.ts +1065 -0
- package/core/key.ts +43 -0
- package/core/kits.ts +320 -0
- package/core/params.ts +111 -0
- package/core/pitch.ts +60 -0
- package/core/rhythm.ts +287 -0
- package/core/score.ts +1054 -14
- package/core/sdk/eval-child.ts +125 -0
- package/core/sdk/eval.ts +260 -0
- package/core/sdk/print.ts +675 -0
- package/core/sdk/sync-chords.ts +54 -0
- package/core/sdk/v1.ts +4159 -0
- package/core/slug.ts +19 -0
- package/core/synth.ts +1001 -0
- package/package.json +8 -5
- package/src/agent/agent.ts +317 -16
- package/src/agent/brief.ts +50 -5
- package/src/agent/chord-tools.ts +357 -0
- package/src/agent/drum-tools.ts +135 -0
- package/src/agent/gateway.ts +215 -41
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +3 -18
- package/src/agent/pack-tools.ts +369 -0
- package/src/agent/planner.ts +55 -2
- package/src/agent/preview-tool.ts +270 -0
- package/src/agent/provider.ts +234 -106
- package/src/agent/rhythm-tools.ts +145 -0
- package/src/agent/sse.ts +31 -7
- package/src/agent/tools.ts +705 -14
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +35 -14
- package/src/agent/xcb.ts +233 -21
- package/src/audio/audition.ts +93 -0
- package/src/audio/cache.ts +160 -0
- package/src/audio/click.ts +125 -0
- package/src/audio/effects/bus.ts +147 -0
- package/src/audio/effects/chain.ts +99 -0
- package/src/audio/effects/common.ts +251 -0
- package/src/audio/effects/convolution.ts +301 -0
- package/src/audio/effects/drive.ts +142 -0
- package/src/audio/effects/duck.ts +122 -0
- package/src/audio/effects/dynamics.ts +121 -0
- package/src/audio/effects/filter.ts +319 -0
- package/src/audio/effects/modulation.ts +174 -0
- package/src/audio/effects/space.ts +337 -0
- package/src/audio/engine.ts +430 -33
- package/src/audio/kits.ts +200 -0
- package/src/audio/live.ts +152 -0
- package/src/audio/packs.ts +1787 -0
- package/src/audio/player.ts +19 -4
- package/src/audio/preview.ts +470 -0
- package/src/audio/random.ts +15 -0
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +420 -0
- package/src/audio/samples.ts +1025 -0
- package/src/audio/synth/oscillators.ts +268 -0
- package/src/audio/synth/voice.ts +555 -0
- package/src/audio/synth/zzfx.ts +137 -0
- package/src/audio/wav.ts +550 -334
- package/src/audio/wavetable-maker.ts +717 -0
- package/src/audio/wavetable.ts +624 -0
- package/src/auth/cli.ts +146 -27
- package/src/auth/credentials.ts +167 -41
- package/src/auth/discover.ts +481 -0
- package/src/auth/login.ts +885 -128
- package/src/auth/openrouter.ts +206 -0
- package/src/auth/picker.ts +282 -0
- package/src/auth/runner.ts +25 -2
- package/src/auth/tui.ts +60 -43
- package/src/commands/drums.ts +299 -0
- package/src/commands/edit.ts +192 -0
- package/src/commands/fx.ts +360 -0
- package/src/commands/help.ts +535 -0
- package/src/commands/history.ts +32 -19
- package/src/commands/music.ts +34 -15
- package/src/commands/pack.ts +423 -0
- package/src/commands/rhythm.ts +230 -0
- package/src/commands/sample.ts +462 -0
- package/src/commands/synth.ts +229 -0
- package/src/commands/wavetable.ts +370 -0
- package/src/main.ts +2136 -170
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +186 -0
- package/src/media/download.ts +281 -0
- package/src/media/dsp.ts +281 -0
- package/src/media/import.ts +130 -0
- package/src/media/lyrics.ts +201 -0
- package/src/media/notes.ts +363 -0
- package/src/media/paths.ts +168 -0
- package/src/media/process.ts +226 -0
- package/src/media/registry.ts +9 -0
- package/src/media/sidecar.ts +72 -0
- package/src/media/stemdeck.ts +254 -0
- package/src/media/stems.ts +173 -0
- package/src/media/tools.ts +397 -0
- package/src/media/types.ts +92 -0
- package/src/media/vendor/basic-pitch.ts +261 -0
- package/src/media/vendor/drums.ts +817 -0
- package/src/media/vendor/grid.ts +203 -0
- package/src/media/vendor/util.ts +139 -0
- package/src/media/vendor/wav.ts +233 -0
- package/src/media/wavetable.ts +202 -0
- package/src/project/check.ts +80 -0
- package/src/project/init.ts +253 -0
- package/src/project/sync.ts +432 -0
- package/src/project/typecheck.ts +149 -0
- package/src/render.ts +49 -6
- package/src/session/attach.ts +3 -4
- package/src/session/daemon.ts +25 -8
- package/src/session/delta.ts +249 -0
- package/src/session/naming.ts +6 -38
- package/src/session/port.ts +26 -6
- package/src/session/rebase.ts +38 -8
- package/src/session/store.ts +116 -21
- package/src/tui/audition.ts +501 -0
- package/src/tui/euclid.ts +472 -0
- package/src/tui/menu.ts +2125 -0
- package/src/tui/play-chords.ts +538 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +984 -0
- package/src/tui/sketch.ts +108 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +684 -0
- package/tui/activity.ts +42 -3
- package/tui/app.ts +398 -33
- package/tui/drums.ts +44 -0
- package/tui/grammar.ts +281 -0
- package/tui/highway.ts +24 -2
- package/tui/layers.ts +14 -2
- package/tui/play-strip.ts +188 -0
package/DAWG.md
CHANGED
|
@@ -11,7 +11,7 @@ bun install
|
|
|
11
11
|
bun run dawg
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Run `dawg` from any directory. It creates `.dawg/session` on first use and reuses that session in later terminal windows. Use `dawg --new` for a separate composition, `dawg --session <id>` to attach explicitly, and `dawg --track bass` to focus a named track. Every window connects to `dawgd`, a per-session daemon the first window starts in the background. It is the single writer: windows send operations with a base revision and an idempotency key, duplicate keys are no-ops, a stale full composition receives a typed rebase diagnostic, and a stale `operations` intent (what the agent sends) is replayed on the current score when nothing it touches changed since its base (the notes it updates or removes, the tracks it rewrites or clears, tempo and length, and ids it creates; at most 64 revisions back, with the base recovered
|
|
14
|
+
Run `dawg` from any directory. It creates `.dawg/session` on first use and reuses that session in later terminal windows. Use `dawg --new` for a separate composition, `dawg --session <name|id>` to attach explicitly (an unknown value is `no session named "…" · dawg sessions`, never a new session), and `dawg --track bass` to focus a named track. `dawg --version` prints the version; unknown subcommands and options are rejected with usage before `.dawg/` exists, and the launch that creates `.dawg/` says `created .dawg/ · add it to .gitignore` in the strip. Every window connects to `dawgd`, a per-session daemon the first window starts in the background. It is the single writer: windows send operations with a base revision and an idempotency key, duplicate keys are no-ops, a stale full composition receives a typed rebase diagnostic, and a stale `operations` intent (what the agent sends) is replayed on the current score when nothing it touches changed since its base (the notes it updates or removes, the tracks it rewrites or clears, tempo and length, and ids it creates; at most 64 revisions back, with the base recovered by rewinding the event log); anything else still gets the rebase diagnostic. Every event stores a compact reverse delta (`rewind`) rather than a copy of the previous score, so a session record grows by about the size of each edit; when it nears its 4 MiB cap the oldest rewinds are compacted away and only that older history becomes unreachable. Rebased events record `rebasedFrom`, and their rewind leads back to the score they replayed on, so undo drops only that change, and on the file fallback the agent commits the full composition with the strict base check. Accepted commits are persisted through the same atomic snapshot store before being broadcast to every window. The daemon also owns the only transport and audio player, broadcasting play, pause, seek, and tempo with a timestamp so every window draws the same hit line. It keeps a presence table (`clientId`, `pid`, focused track) and can atomically claim the first unfocused track for a new window. The daemon exits 30 seconds after its last window closes, removes its socket on SIGTERM, and a crashed daemon's socket and lock are reclaimed by the next window. If `dawgd` cannot be started (or `DAWG_DAEMON=0`), windows fall back to the snapshot under the file lock, watching the session directory with `fs.watch` (so renames and edits from other windows arrive immediately) with a 1 s backstop poll, or a 200 ms poll where watching is unavailable, with presence kept in per-window heartbeat files. `dawg sessions` lists sessions in the current workspace. `dawg render <out.wav> [--session <name|id>] [--import <file>]` reads the session record from disk (no daemon, no audio) and writes a stereo 16-bit WAV through the playback renderer; the same score always yields the same bytes, and the command prints the size and sha256. `/status` reports `status · <name> · rev <n> · <digest> · shared via dawgd` (or `saved locally · no daemon`), where the digest is the 16-hex composition digest dawgd broadcasts. In demo mode a drum track is seeded with a one-bar kick, snare and hat groove instead of melodic notes.
|
|
15
15
|
|
|
16
16
|
### Sessions, names and forks
|
|
17
17
|
|
|
@@ -25,15 +25,17 @@ The header shows the session name and, when more than one window is open, the wi
|
|
|
25
25
|
|
|
26
26
|
Auto-naming (`src/session/naming.ts`) is gated on a local musical fingerprint: tempo, a scale-fit key estimate from the pitch-class histogram (tonic and fifth weighted), sorted instrument families, register and density buckets, and effects, hashed into an order- and id-independent digest. After an accepted turn the namer waits for a quiet period, and skips the model when the fingerprint equals the one the current name came from or when fewer than three turns have passed since the last name (a structure change, such as a new instrument family, bypasses the turn gate). The prompt is one fingerprint line, the last two prompts truncated to 60 characters each, and the current name, with `max_tokens: 12`. The reply is untrusted: it is stripped to 2–4 lowercase words of at most 32 characters, and a reply equal to the current name keeps it (hysteresis). The write is conditional on the name and `nameSource` the request started from, and a newer request supersedes an older one, so a slow provider (xcb takes about 7 s) can never overwrite a user rename. Forks keep their ` N` suffix through auto-renames, and a name already used by another session is suffixed. The generator is an injectable `NameGenerator`; the default calls `generateText` from `src/agent/provider.ts` and falls back to the deterministic local name.
|
|
27
27
|
|
|
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`, and `ping`; the daemon replies with `welcome`, `result`, `snapshot`, `claimed`, `pong`, and typed `error` frames, and pushes `commit`, `transport`, and `
|
|
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 (
|
|
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
|
-
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 `/
|
|
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.
|
|
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
|
|
|
@@ -50,7 +52,7 @@ automate volume at 4 1
|
|
|
50
52
|
automate pan at 0 -1
|
|
51
53
|
automate pan at 4 1
|
|
52
54
|
clear pan automation
|
|
53
|
-
track drums
|
|
55
|
+
/track drums
|
|
54
56
|
instrument kit
|
|
55
57
|
hit kick at 0
|
|
56
58
|
hit snare at 1 vel 0.7
|
|
@@ -88,8 +90,12 @@ duration note <id> 0.25
|
|
|
88
90
|
/export loop.track.json
|
|
89
91
|
/import loop.track.json
|
|
90
92
|
/model opus-5.5
|
|
93
|
+
/help
|
|
94
|
+
/help all
|
|
91
95
|
```
|
|
92
96
|
|
|
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.
|
|
98
|
+
|
|
93
99
|
Drum tracks use the `kit` instrument (a track named `drums` gets it automatically). Notes on a kit track keep the score's MIDI pitch field, using General MIDI percussion numbers (kick 36, rim 37, snare 38, clap 39, closed hat 42, tom 45, open hat 46), so drum hits round-trip through `track.loop/v1` unchanged and the highway draws them in one lane per voice. Grammar, one command per prompt, beats in score beats:
|
|
94
100
|
|
|
95
101
|
```text
|
|
@@ -104,13 +110,17 @@ automate filter at <beat> <cutoff> | clear filter automation
|
|
|
104
110
|
automate resonance at <beat> <0..1> | clear resonance automation
|
|
105
111
|
automate delay-feedback at <beat> <0..0.9> | clear delay-feedback automation
|
|
106
112
|
automate delay-mix at <beat> <0..1> | clear delay-mix automation
|
|
113
|
+
automate <lane> points <beat:value> [<beat:value> ...] merge points into a lane
|
|
114
|
+
automate <lane> remove <beat> drop one point
|
|
115
|
+
track name <text> rename the focused track
|
|
116
|
+
meter <beats per bar 1..16>
|
|
107
117
|
solo | unsolo
|
|
108
118
|
undo | redo
|
|
109
119
|
```
|
|
110
120
|
|
|
111
121
|
Effects live on the track as optional `filter {cutoff, resonance}`, `delay {beats, feedback, mix}`, `reverb {mix, size}`, `filterAutomation`, `resonanceAutomation`, `delayFeedbackAutomation`, `delayMixAutomation`, and `solo` fields. Effect lanes modulate an existing effect: a resonance lane needs a filter and the delay lanes need a delay. Documents written before these fields existed still parse; out-of-range or non-finite values are rejected. Undo and redo append ordinary session events, so history is shared by every window and a new edit clears the redo stack.
|
|
112
122
|
|
|
113
|
-
Unrecognized prompts go to the agent whenever a provider is configured (`DAWG_AI=0` disables it)
|
|
123
|
+
Unrecognized prompts go to the agent whenever a provider is configured (`DAWG_AI=0` disables it); see **Providers and auth** below. `src/agent/models.ts` holds the model catalog: frontier (`opus-5.5`, `fable-5.1`, `sol-6.1`, `gemini-3.1-pro`), fast (`sonnet-5.5`, `haiku-4.5`, `gpt-5.4-mini`, `gemini-3.8-flash`) and open weights (`deepseek-v4-pro`, `kimi-k3`, `qwen3.8-27b`, `glm-5.3`, `llama-4-maverick`), each with its gateway and OpenRouter ID, checked against the live gateway and OpenRouter model lists and tagged for tool use. The `/model` picker joins the catalog with the live list and shows only tool-calling models; any other `vendor/model` ID the provider lists is accepted too. `DAWG_MODEL` picks the model for one run, and an unknown value fails at startup with the valid list. The default is `opus-5.5`. `DAWG_OPUS_MODEL` / `DAWG_SOL_MODEL` still remap those two aliases.
|
|
114
124
|
|
|
115
125
|
### Agent turns
|
|
116
126
|
|
|
@@ -120,15 +130,57 @@ When a tool call finishes streaming, it passes three checks: the tool's own argu
|
|
|
120
130
|
|
|
121
131
|
`runAgentTurn` (`src/agent/agent.ts`) emits structured progress events for the TUI: `step`, `text-delta`, `tool-start`, `tool-applied` (with `summary`, `baseRevision`, `resultRevision` and `trackId`), `tool-rejected` (with `diagnostic`), and a final `done` or `error` (`aborted`, `timeout`, `budget`, `provider`). To add an operation family, append a tool to `AGENT_TOOLS` in `src/agent/tools.ts`. The schema, dispatch and validation all come from that one entry.
|
|
122
132
|
|
|
133
|
+
### Workspace and web tools
|
|
134
|
+
|
|
135
|
+
The project directory (the directory `dawg` runs in) is the agent's workspace. Six more entries in `AGENT_TOOLS` give the model bounded file and web access on both the gateway and xcb paths; `src/agent/workspace.ts` holds the path policy and `src/web/` the network side.
|
|
136
|
+
|
|
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.
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
142
|
+
|
|
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.
|
|
144
|
+
|
|
145
|
+
### Media tools
|
|
146
|
+
|
|
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.
|
|
148
|
+
|
|
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.
|
|
150
|
+
- `split_stems {file}`: six stems (vocals, drums, bass, guitar, piano, other) into `<base>.stems/`, cached once present. 20 min budget.
|
|
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.
|
|
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.
|
|
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).
|
|
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/`.
|
|
156
|
+
|
|
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.
|
|
158
|
+
|
|
159
|
+
**Bounds.** Every subprocess goes through the injectable `CommandRunner` with a per-tool timeout, 1 MiB of captured output, and SIGTERM then SIGKILL on Esc or abort. Helper output becomes throttled one-line progress (`demucs 42%`, `stemdeck separating 42%`) on the activity card through the `tool-progress` event. The turn deadline is paused while a media helper runs, so a 10 minute separation does not time out the turn; the other turn budgets still apply. WAVs read into memory are capped at 256 MiB. Input paths must stay inside the project. URLs are never logged with credentials. The drum classifier, beat-grid fitting, WAV codec and basic-pitch CSV parser are vendored from soundfish in `src/media/vendor/` with a header crediting it; tests stub every binary and StemDeck with `scriptedRunner`, fetch fixtures and a drum excerpt in `src/media/fixtures/`. `DAWG_LIVE_MEDIA=1 bun test src/media` adds one live ffprobe smoke test.
|
|
160
|
+
|
|
123
161
|
### Providers and auth
|
|
124
162
|
|
|
125
|
-
`src/agent/provider.ts` picks a backend per turn: `DAWG_PROVIDER`, then the choice saved
|
|
163
|
+
`src/agent/provider.ts` picks a backend per turn: `DAWG_PROVIDER`, then the choice saved in `~/.config/dawg/config.json`, then `auto` (AI Gateway, then OpenRouter, then a ready Codex or Claude subscription, else offline with a `dawg login` hint). The config holds `provider`, the model and, for subscriptions, the xcb account. It is written atomically with 0600 permissions and never holds a key. A saved choice that stops working (revoked key, account gone) comes back as `offline` with `invalidSaved` and the reason; startup and `dawg login` say so once and open the picker rather than switching providers. Only `dawg logout`, `/logout`, `dawg login <provider>` and `/model` change it.
|
|
164
|
+
|
|
165
|
+
Keys (`ai-gateway`, `openrouter`) resolve from the environment (`AI_GATEWAY_API_KEY`, `OPENROUTER_API_KEY`), then the macOS Keychain (service `dawg`), then `~/.config/dawg/credentials.json` (0600 under a 0700 directory, written atomically through a temp file and rename). Storing uses `security -i` with the command on stdin, so a key never appears on an argv. Keys must match `[A-Za-z0-9._-]{16,256}` and are shown only masked (`vck_…abcd`). Nothing auth-related goes to `.dawg/`.
|
|
166
|
+
|
|
167
|
+
`src/auth/discover.ts` probes all four options in parallel, each with a 4 s cap and no network beyond the local CLIs: environment and stored keys, `vercel whoami --format json --non-interactive`, `VERCEL_OIDC_TOKEN` (a usable gateway credential in a linked project), and `xcb --version` plus `xcb --json generate --capabilities` split by provider family. xcb accounts are usable iff `available === true`; `admission` (`pending | admitted | qualified | null`) is informational, and pending accounts are usable with a slower first call. Accounts reporting `models_unavailable` are refreshed once per process (`xcb accounts refresh <id>`, in parallel, 10 s each) and re-read. Reasons map to one-line hints (`application_disabled` → `xcb application enable`, `admission_failed` → retry after 15 minutes); xcb older than 0.20.0 is reported with an upgrade hint.
|
|
168
|
+
|
|
169
|
+
`src/auth/login.ts` turns the probes into one picker (`src/auth/picker.ts`, a pure model the shell and TUI both render: arrows, numbers, Enter, type-to-filter). Per option:
|
|
126
170
|
|
|
127
|
-
|
|
171
|
+
- **Gateway**: with the Vercel CLI, `vercel login` gets the terminal when needed, then `vercel ai-gateway api-keys create --name dawg-<host> --non-interactive [--limit <dollars>]` prints only the key on stdout. Without it, dawg opens the gateway keys page and reads a pasted key with echo off. Validation is `GET /v1/credits`.
|
|
172
|
+
- **OpenRouter** (`src/auth/openrouter.ts`): OAuth PKCE. A server on `127.0.0.1:<ephemeral>` waits for `/callback` and checks a random `state`. The browser opens `https://openrouter.ai/auth?callback_url=…&code_challenge=<S256>&code_challenge_method=S256`, and the code goes to `POST /api/v1/auth/keys` with the verifier. The URL is printed for headless use, the server closes after 5 minutes, and validation is `GET /api/v1/key`.
|
|
173
|
+
- **Codex / Claude**: lists usable xcb accounts of that family and the models xcb reports for them (only models with an admission state). With none ready, dawg runs `xcb setup <family>` with the terminal; otherwise it prints the command.
|
|
128
174
|
|
|
129
|
-
|
|
175
|
+
Every subprocess goes through the injectable `CommandRunner` in `src/auth/runner.ts`, which bounds output, writes stdin and on abort sends SIGTERM (SIGKILL after 15 s) and waits for exit. Tests script `vercel`, `security` and `xcb` through it and run OpenRouter against a local fake server.
|
|
130
176
|
|
|
131
|
-
`
|
|
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.
|
|
178
|
+
|
|
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.
|
|
180
|
+
|
|
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.
|
|
182
|
+
|
|
183
|
+
`generateText(prompt, {maxTokens, signal?, timeoutMs?, selection?})` from `src/agent/provider.ts` is a tool-free one-shot completion for helpers like session naming. On the gateway and OpenRouter it uses `anthropic/claude-haiku-4.5` with `max_tokens`. On xcb it uses the selected account with `maxOutputBytes ≈ 8 × maxTokens`. It returns at most 512 trimmed characters of untrusted text and throws when offline, so callers should fall back to a local default.
|
|
132
184
|
|
|
133
185
|
Playback renders the score to interleaved stereo 16-bit PCM with deterministic sine, piano, pluck, bass, saw, square, and triangle voices and a synthesized kit whose noise comes from a PRNG seeded by each note, so every render is byte-identical. Track volume and pan automation, the low-pass filter (with cutoff and resonance lanes), the delay send (with feedback and mix lanes), and the reverb send are applied per track; mute always silences a track and any solo silences unsoloed tracks. Pan uses an equal-power law (-1 left, 1 right). The delay is a stereo ping-pong (first repeat on the panned side, later repeats alternate) and the reverb is a Freeverb-style network of eight parallel damped combs and four series allpasses per channel, with the right channel's delay lines offset for width; both use only integer delay lengths and fixed coefficients, so renders stay deterministic.
|
|
134
186
|
|
|
@@ -137,6 +189,705 @@ Set `DAWG_AUDIO=0` for headless sessions.
|
|
|
137
189
|
|
|
138
190
|
Use `DAWG_DEMO=1 bun run src/main.ts` for a deterministic non-interactive frame stream while developing the renderer.
|
|
139
191
|
|
|
192
|
+
## Project files and SDK
|
|
193
|
+
|
|
194
|
+
A directory with a `dawg.json` is a project: its score lives in typechecked TypeScript files that you, an editor or the agent can edit, and every window keeps those files and the session in step. `dawg init [dir]` creates one and is idempotent; it never touches anything outside the target.
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
dawg.json {"format":"dawg.project/v1","sdk":1}
|
|
198
|
+
tsconfig.json extends .dawg/sdk/tsconfig.json; paths {"dawg": ["./.dawg/sdk/v1.ts"]}
|
|
199
|
+
song.ts tempo, meter, bars, key, track order; imports tracks/*/track.ts
|
|
200
|
+
tracks/<slug>/track.ts one track: instrument or sampler, mix, effects, automation, notes
|
|
201
|
+
tracks/<slug>/samples/ audio a sampler references by relative path
|
|
202
|
+
.dawg/sdk/v1.ts vendored SDK (committed), refreshed by init when a newer 1.x ships
|
|
203
|
+
.dawg/sync.json hashes of the files dawg last wrote (runtime, gitignored)
|
|
204
|
+
.dawg/tsbuild/ incremental typecheck state (runtime, gitignored)
|
|
205
|
+
```
|
|
206
|
+
|
|
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).
|
|
208
|
+
|
|
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.
|
|
210
|
+
|
|
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.
|
|
212
|
+
|
|
213
|
+
Typecheck. `typecheckProject(dir)` (`src/project/typecheck.ts`) runs the native TypeScript 7 compiler from the `typescript` dependency with `--incremental` state in `.dawg/tsbuild`. A cold check of a three-track project takes about 65 ms and a warm one about 26 ms on an M-series Mac. The header shows `types ✓` or `types ✗ N`.
|
|
214
|
+
|
|
215
|
+
Two-way sync (`src/project/sync.ts`) runs in every window of a project:
|
|
216
|
+
|
|
217
|
+
- Files to score: `fs.watch` on the project and `tracks/` (plus a 1.5 s poll) with a 150 ms debounce. A changed source is evaluated, its notes adopt the session's ids, and `diffScores` (`core/diff.ts`) turns the difference into the smallest list of score operations, committed as one `files.apply` revision through the session port, so dawgd rebases it like an agent intent and undo drops it as one step. The window shows `applied from files · 2 notes, 1 track`. A file that fails to evaluate leaves the score untouched and shows `files rejected · <diagnostic>` once per distinct error.
|
|
218
|
+
- Score to files: after any accepted revision (TUI, agent, another window) the window reprints only the files whose bytes would change. A file whose evaluation already equals the score is never rewritten, so hand formatting and comments survive until the content they describe changes. A track file left behind by a rename or removal is deleted only if its hash still matches what dawg wrote; a hand-edited one is kept.
|
|
219
|
+
- Startup: if any source differs from the hash in `.dawg/sync.json` (edited while dawg was closed, or never written by dawg), the files win; otherwise the session wins and the files are reprinted.
|
|
220
|
+
- Echo: a window skips files whose hashes it already evaluated; another window's write costs one no-op evaluation.
|
|
221
|
+
- The agent's `write_file`/`edit_file` on a `.ts` source applies before the tool result returns; the result carries the outcome line, `types ✓` or `types ✗ N` and up to eight diagnostics.
|
|
222
|
+
|
|
223
|
+
The printer (`core/sdk/print.ts`) is deterministic and Prettier-stable (`prettier --check` passes on its output), prints only non-default fields, and satisfies `print(evaluate(print(score))) = print(score)`.
|
|
224
|
+
|
|
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.
|
|
226
|
+
|
|
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) |
|
|
472
|
+
|
|
473
|
+
## Samples
|
|
474
|
+
|
|
475
|
+
A track whose instrument is `sampler(...)` plays audio files instead of a synth. Voices live in `tracks/<slug>/samples/` and `src` is relative to the track directory (`samples/kick.wav`); a project-relative `tracks/<slug>/samples/kick.wav` works too.
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
// tracks/drums/track.ts
|
|
479
|
+
import { track, sampler, hits } from "dawg";
|
|
480
|
+
|
|
481
|
+
export default track({
|
|
482
|
+
name: "drums",
|
|
483
|
+
instrument: sampler({
|
|
484
|
+
kick: "samples/kick.wav",
|
|
485
|
+
hat: { src: "samples/hat.wav", choke: "hats", gain: 0.6 },
|
|
486
|
+
open: { src: "samples/open.wav", choke: "hats" },
|
|
487
|
+
}),
|
|
488
|
+
notes: [
|
|
489
|
+
...hits("kick", [0, 1, 2, 3]),
|
|
490
|
+
...hits("hat", [0.5, 1.5]),
|
|
491
|
+
...hits("open", [3.5]),
|
|
492
|
+
],
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Semantics follow Strudel's sampler:
|
|
497
|
+
|
|
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.
|
|
518
|
+
|
|
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.
|
|
520
|
+
|
|
521
|
+
Decoding: WAV (PCM 16/24/32-bit integer and 32-bit float, any channel count and rate) and AIFF/AIFF-C (8/16/24/32-bit) decode natively, mixed to mono and resampled to the engine rate on the fly with linear interpolation. MP3, FLAC, Ogg, M4A and anything else decode through `ffmpeg` when it is on `PATH` (dawg never installs it); without it the voice is skipped with `<voice> · <path> · not WAV/AIFF and ffmpeg is not on PATH · convert it to WAV, or install ffmpeg (e.g. brew install ffmpeg) and reload`. Decoded PCM is cached at `.dawg/assets/<sha256>.pcm`, least recently used first out past 512 MiB. Files over 50 MiB or 10 minutes, paths that leave the project (including through a symlink), and more than 64 voices are rejected. A `sha256` that no longer matches the file is a warning and the file still plays. Problems appear as receipts in the TUI and on stderr from `dawg render`; the track renders without the missing voices and nothing crashes.
|
|
522
|
+
|
|
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.
|
|
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 |
|
|
682
|
+
| Space | audition the voice |
|
|
683
|
+
| `x` / Delete, `f` | remove the row and its notes / freeze it to notes |
|
|
684
|
+
| Esc | back (cancels typing, then closes) |
|
|
685
|
+
|
|
686
|
+
## Chords
|
|
687
|
+
|
|
688
|
+
`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.
|
|
689
|
+
|
|
690
|
+
From Orchid's documentation and reviews:
|
|
691
|
+
|
|
692
|
+
- 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.
|
|
693
|
+
- 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.
|
|
694
|
+
- 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.
|
|
695
|
+
- 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).
|
|
696
|
+
- 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).
|
|
697
|
+
- "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.
|
|
698
|
+
- 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.
|
|
699
|
+
|
|
700
|
+
dawg's own design:
|
|
701
|
+
|
|
702
|
+
- 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.
|
|
703
|
+
- 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.
|
|
704
|
+
- 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.
|
|
705
|
+
- 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.
|
|
706
|
+
- 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.
|
|
707
|
+
- 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).
|
|
708
|
+
- 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.
|
|
709
|
+
|
|
710
|
+
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.
|
|
711
|
+
|
|
712
|
+
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).
|
|
713
|
+
|
|
714
|
+
## Play mode (computer keyboard)
|
|
715
|
+
|
|
716
|
+
`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`).
|
|
717
|
+
|
|
718
|
+
| Key | Does |
|
|
719
|
+
| ----------------------- | ------------------------------------------------------------------ |
|
|
720
|
+
| `A S D F G H J K L ; '` | white keys C D E F G A B C D E F from the base octave |
|
|
721
|
+
| `W E T Y U O P` | black keys C♯ D♯ F♯ G♯ A♯ C♯ D♯ (none on `R` or `I`, like a piano) |
|
|
722
|
+
| `Z` / `X` | octave down / up (clamped to the score's pitch range) |
|
|
723
|
+
| `C` / `V` | velocity down / up in steps of 16 (1–127, shown in the header) |
|
|
724
|
+
| Shift + note | sustained note: rings until a plain key or `Tab` |
|
|
725
|
+
| `Tab` | sustain latch on/off (off releases every sustained note) |
|
|
726
|
+
| `R` | record arm on/off |
|
|
727
|
+
| `Shift-R` | replace: bars you play over are cleared first (default: overdub) |
|
|
728
|
+
| `M` | click on/off |
|
|
729
|
+
| `Space` | play/stop; with record armed and stopped, counts in, then records |
|
|
730
|
+
| `?` | the play-mode keys and current settings (any key closes) |
|
|
731
|
+
| `/` | type a slash command without leaving (`/click 40%`) |
|
|
732
|
+
| `Esc` | leave play mode |
|
|
733
|
+
|
|
734
|
+
### Chord mode
|
|
735
|
+
|
|
736
|
+
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.
|
|
737
|
+
|
|
738
|
+
- `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.
|
|
739
|
+
- `manual`: note keys play single notes as before; latch a chord type or extension and they play that chord on the pressed root.
|
|
740
|
+
- `off`: plain play mode; the chord keys below go back to being unmapped.
|
|
741
|
+
|
|
742
|
+
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.
|
|
743
|
+
|
|
744
|
+
| Key | Does (chord mode on) |
|
|
745
|
+
| --------- | ------------------------------------------------------------------------------- |
|
|
746
|
+
| `Q` | auto ⇄ manual |
|
|
747
|
+
| `1 2 3 4` | latch chord type dim / min / maj / sus (two latched make a combined chord) |
|
|
748
|
+
| `5 6 7 8` | latch extension 6 / m7 / M7 / 9 (any number; on top of the type or auto chord) |
|
|
749
|
+
| `0` | clear every latch |
|
|
750
|
+
| `-` / `=` | voicing dial down / up (-12..12; walks inversions) |
|
|
751
|
+
| `9` | next perform mode (block, strum-up, strum-down, arp-up, …, harp, slop, pattern) |
|
|
752
|
+
| `B` | next bass mode: off, chords, unison, single, solo (bass in C2–B2) |
|
|
753
|
+
| `N` | play the suggested next chord (the `next` chord in the header) |
|
|
754
|
+
|
|
755
|
+
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.
|
|
756
|
+
|
|
757
|
+
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.
|
|
758
|
+
|
|
759
|
+
`/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.
|
|
760
|
+
|
|
761
|
+
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.
|
|
762
|
+
|
|
763
|
+
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.
|
|
764
|
+
|
|
765
|
+
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.
|
|
766
|
+
|
|
767
|
+
Terminals send key presses and auto-repeats, never key releases, so held notes are synthesized. A press sounds for one grid step; holding the key keeps it sounding while the OS auto-repeats it (after its repeat delay, usually 250–700 ms), and it ends about 120 ms after the last repeat. Hold notes shorter than the repeat delay come out one grid step long. Use Shift or the `Tab` latch for long notes.
|
|
768
|
+
|
|
769
|
+
Recording: with record armed and the transport running, each note is quantized to the grid (`/grid 1/16` by default; `1/4 1/8 1/8T 1/16 1/16T 1/32`), wrapped into the loop, and appended to the focused track as `addNote` operations when the playhead leaves the bar, so each recorded bar is one revision: one `Ctrl-Z` undoes a bar, other windows and the project files see it like any edit. Stopping commits the rest. The same pitch on the same step twice is one note. Replace removes the bar's earlier notes in the same revision. No agent and no network are involved.
|
|
770
|
+
|
|
771
|
+
## Click track
|
|
772
|
+
|
|
773
|
+
`/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`.
|
|
774
|
+
|
|
775
|
+
## Drum patterns and kits
|
|
776
|
+
|
|
777
|
+
**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`.
|
|
778
|
+
|
|
779
|
+
| Command | Does |
|
|
780
|
+
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
781
|
+
| `/pattern` | picker of every pattern; moving the cursor plays one bar of it (silent while the loop plays), typing filters, Enter applies |
|
|
782
|
+
| `/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 |
|
|
783
|
+
| `/pattern <name> keep-tempo` / `tempo` | never / always move the tempo |
|
|
784
|
+
| `/pattern list` | the library as text: name, tempo range, tags, voices |
|
|
785
|
+
|
|
786
|
+
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.
|
|
787
|
+
|
|
788
|
+
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.
|
|
789
|
+
|
|
790
|
+
**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.
|
|
791
|
+
|
|
792
|
+
| Kit | Sound |
|
|
793
|
+
| ---------- | ------------------------------------------------------------ |
|
|
794
|
+
| `default` | the original voices |
|
|
795
|
+
| `syn808` | long sub boom, snappy snare, metallic hats |
|
|
796
|
+
| `syn909` | punchy clicky kick, bright noisy snare |
|
|
797
|
+
| `acoustic` | beater kick, wire snare, darker cymbals |
|
|
798
|
+
| `lofi` | soft round kick, crushed and dark (alias `dusty`) |
|
|
799
|
+
| `electro` | tight short kick, clicky rim, ticking hats (alias `minimal`) |
|
|
800
|
+
| `trap` | distorted long 808, crisp hats, high snare |
|
|
801
|
+
|
|
802
|
+
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.
|
|
803
|
+
|
|
804
|
+
## Menus
|
|
805
|
+
|
|
806
|
+
`/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.
|
|
807
|
+
|
|
808
|
+
| Section | Rows (most used first) |
|
|
809
|
+
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
810
|
+
| 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) |
|
|
811
|
+
| 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) |
|
|
812
|
+
| Rhythm | the euclid editor (`/euclid`), drum patterns (`/pattern`), drum kits (`/kit`, synth then samples) |
|
|
813
|
+
| Chords | play-mode chord mode, key tonic and mode, voicing, spread, bass, sevenths, perform, pattern, arp rate, arp octaves, progression, style |
|
|
814
|
+
| 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 |
|
|
815
|
+
| Project | play, tempo, beats per bar, loop length, grid, click, count-in bars |
|
|
816
|
+
|
|
817
|
+
Every list, picker and editor uses the same keys (see **Keys** below). In the menu:
|
|
818
|
+
|
|
819
|
+
| Key | Does |
|
|
820
|
+
| --------------------------- | ------------------------------------------------------------------------- |
|
|
821
|
+
| `↑` `↓` / `k` `j` | move |
|
|
822
|
+
| `Enter` / `→` / `l` | open a section, pick from a list, or start typing a value |
|
|
823
|
+
| `←` `→` / `h` `l` / `-` `+` | adjust a value by its step (cutoff moves 25%) or cycle a choice |
|
|
824
|
+
| `Space` | toggle on/off; elsewhere, hear the focused track (see Previewing changes) |
|
|
825
|
+
| digits | type a value on a focused value row; `Enter` sets it, `Esc` cancels |
|
|
826
|
+
| `/` | filter the current list by name, value or command |
|
|
827
|
+
| `x` / `Delete` | reset the focused value to its default; on an automation point, remove it |
|
|
828
|
+
| `Esc` / `←` / `h` | clear the filter, then back one level, then close |
|
|
829
|
+
| `?` | the keys for this screen |
|
|
830
|
+
|
|
831
|
+
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.
|
|
832
|
+
|
|
833
|
+
## Previewing changes
|
|
834
|
+
|
|
835
|
+
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.
|
|
836
|
+
|
|
837
|
+
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.
|
|
838
|
+
|
|
839
|
+
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. 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`).
|
|
840
|
+
|
|
841
|
+
| Key | While auditioning |
|
|
842
|
+
| ------- | ------------------------------------------------------------ |
|
|
843
|
+
| `Space` | start or stop the loop of the focused track |
|
|
844
|
+
| `c` | solo ↔ in context (the whole mix with the track) |
|
|
845
|
+
| `←` `→` | change the focused value; staged and heard at once |
|
|
846
|
+
| `a` | A/B: flip between the committed sound (A) and the staged (B) |
|
|
847
|
+
| `Enter` | keep every staged change as one revision and one undo step |
|
|
848
|
+
| `Esc` | revert staged changes (the score is untouched); again: back |
|
|
849
|
+
| `?` | the keys for this screen |
|
|
850
|
+
|
|
851
|
+
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.
|
|
852
|
+
|
|
853
|
+
**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.
|
|
854
|
+
|
|
855
|
+
| Key in a list | While auditioning |
|
|
856
|
+
| ------------- | --------------------------------------------------- |
|
|
857
|
+
| `↑` `↓` | move and hear the highlighted item on the loop |
|
|
858
|
+
| `Enter` | choose it (menu) or keep it (picker), one undo step |
|
|
859
|
+
| `Esc` / `←` | leave the list; the hover is dropped |
|
|
860
|
+
| `a` / `c` | A/B against the committed sound / solo ↔ in context |
|
|
861
|
+
|
|
862
|
+
**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).
|
|
863
|
+
|
|
864
|
+
**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:
|
|
865
|
+
|
|
866
|
+
```text
|
|
867
|
+
│ cutoff (lpf/hpf) or centre (bpf) frequency ▇▇▇▇▇▇▇▇▇▇▇▅▂▁▁▁ › fx filt… │
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
**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.
|
|
871
|
+
|
|
872
|
+
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.
|
|
873
|
+
|
|
874
|
+
## Keys
|
|
875
|
+
|
|
876
|
+
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):
|
|
877
|
+
|
|
878
|
+
| Key | Does |
|
|
879
|
+
| --------------------------- | -------------------------------------------------------------- |
|
|
880
|
+
| `↑` `↓` / `j` `k` | move (scroll in a text panel); PgUp/PgDn/Home/End page |
|
|
881
|
+
| `←` `→` / `h` `l` / `-` `+` | adjust the focused value |
|
|
882
|
+
| `Enter` | open or confirm |
|
|
883
|
+
| `Space` | audition or toggle |
|
|
884
|
+
| `/` | filter; typing then narrows the list |
|
|
885
|
+
| `Esc` | back one level: clears the filter or a typed value first |
|
|
886
|
+
| `?` | the keys for the current screen, drawn over it; any key closes |
|
|
887
|
+
| digits | type a value, only where a value is focused |
|
|
888
|
+
|
|
889
|
+
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.
|
|
890
|
+
|
|
140
891
|
## Release
|
|
141
892
|
|
|
142
893
|
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).
|