# Soundfish API reference

## Document

The immutable document protocol is `soundfish.document/v1`:

```ts
{
  protocol: "soundfish.document/v1";
  title: string;
  bpm: 40..240 integer;
  bars: 1..16;
  effects: LoopEffects;
  tracks: Array<NotesTrack | DrumsTrack>;
}
```

A **NotesTrack** has an opaque document-wide `trk_` ID, editable name, zero-based `gmProgram`, production, lineage, and pitched events. A **DrumsTrack** has the same identity, name, production, and lineage fields, omits `gmProgram`, may carry an optional GS drum `kit` program from 0 through 127 (default 0, Standard), and accepts GM percussion pitches 35–81. Drum hits occupy one step. Events have unique `evt_` IDs, MIDI pitch, half-open step interval, and velocity 1–127.

A document may also carry an optional integer `swing` from 0 through 100 (default 0): every odd sixteenth step is delayed by `swing / 100 × step / 2`, so 66 approximates a triplet feel. An optional `meta` map describes the music without changing what plays: `tonic` 0–11 (C = 0), `mode` from major, minor, dorian, mixolydian, lydian, phrygian, aeolian, harmonic-minor, and `genre` of at most 32 UTF-8 bytes.

Every optional field uses omit-at-default encoding: `kit`, `swing`, `meta`, and the `pan` and `cutoff` lanes are absent from canonical bytes exactly when they hold their defaults, a present default is non-canonical, and `meta` never enters MusicCID. A document that uses none of them keeps the canonical bytes, MusicCID, and transport digest it had before the 2026-09 revision.

Loop titles and track names are trimmed before validation and must remain nonempty, contain well-formed Unicode, and fit their UTF-8 byte limits. Track IDs match `^trk_[a-z0-9][a-z0-9_-]{0,94}$`, event IDs match `^evt_[a-z0-9][a-z0-9_-]{0,94}$`, and operation IDs match `^op_[a-z0-9][a-z0-9_-]{0,94}$`. Track and event IDs are unique across the complete document.

Time is fixed 4/4: 4 beats and 16 sixteenth-note steps per bar. Empty documents and tracks are valid.

## Production and automation

Every track owns `volume`, `reverbSend`, and `delaySend` automation lanes and may author optional `pan` and `cutoff` lanes. The document owns `reverb.return`, `delay.return`, `delay.feedback`, and `delay.timeMilliBeats` lanes. Each lane is `{ base, points }`; each point is `{ step, value, curve: "hold" | "linear" }`.

Points have unique steps, are sorted by step, and must fall inside the composition. The base holds before the first point. A point's curve controls only the segment to the next explicit point. The final point holds to the end of the cycle. At the loop seam the lane resets to its base; there is no implicit ramp from the final point to the base.

| Lane | Range | Default base |
| --- | ---: | ---: |
| Track volume | 0–10000 basis points | 10000 |
| Track reverb send | 0–10000 basis points | 0 |
| Track delay send | 0–10000 basis points | 0 |
| Track pan (0 left, 10000 right) | 0–10000 basis points | 5000 |
| Track cutoff (200 Hz–20 kHz, logarithmic) | 0–10000 basis points | 10000 |
| Reverb return | 0–10000 basis points | 2000 |
| Delay return | 0–10000 basis points | 2500 |
| Delay feedback | 0–9500 basis points | 3500 |
| Delay time | 62–4000 milli-beats | 500 |

The editor exposes authored volume, pan, cutoff, sends, shared effect bases, and an automation strip for their points. Agents and the CLI use the same canonical lanes. Session master gain, mute, and solo are monitoring state and do not enter the document.

## Self-contained loop URL

```text
#s1.<music-cid>.<codec>.<payload>.<transport-digest>
```

MusicCID is lowercase base32 CIDv1 using the raw codec and a SHA-256 multihash over a domain-separated semantic CBOR projection. That projection includes BPM, bars, swing where present, effects, track kind, GM program or drum kit where present, production including pan and cutoff lanes where present, events, and all automation. It excludes composition and track titles, `meta`, track and event IDs, lineage, operation IDs, the fixed sound-bank bytes, runtime versions, and display order. Semantic tracks are sorted by bytes, while duplicates remain repeated.

Codec `c` carries the uncompressed canonical CBOR bytes. Codec `d` carries raw DEFLATE of those same bytes. The payload and transport digest use unpadded base64url. The transport digest is SHA-256 of the entire uncompressed canonical document, including titles, IDs, lineage, and display order. Compression changes neither digest. In auto mode the encoder selects `d` only when its compressed payload is strictly smaller; explicit codec selection can still force `d`. Both codecs can therefore produce different valid fragments for the same canonical document, MusicCID, and transport digest. The uncompressed document bytes are canonical; the URL transport spelling is not unique. A fragment is not sent in ordinary HTTP requests.

The two hashes serve different purposes: MusicCID addresses authored musical meaning; the transport digest detects any change to the transported document. Neither identifies or authenticates an author.

## Self-contained song URL

```text
#a1.<arrangement-cid>.<codec>.<payload>.<transport-digest>
```

The `soundfish.arrangement/v1` canonical JSON payload contains a deduplicated roster of complete `soundfish.document/v1` children and an ordered sequence of `{ loop, label, repeats }` entries. Structural parsing validates shape, compatibility, and expansion bounds. Async verification re-derives every child MusicCID and returns a deeply frozen verified document required by playback. URL decoding also verifies transport integrity, canonical encoding, and ArrangementCID. All children must share BPM and swing and compatible track IDs, kinds, GM programs, and drum kits; every embedded child must be referenced. Children that differ only in `meta` are compatible, and the first segment's `meta` is kept.

Arrangement titles and section labels are trimmed before validation and must remain nonempty, contain well-formed Unicode, and fit their UTF-8 byte limits. The roster has one canonical loop body per MusicCID. If two build segments have the same MusicCID but different canonical loop bytes—for example, because only titles, IDs, lineage, or display order differ—the v1 builder rejects the arrangement as ambiguous instead of discarding either body.

ArrangementCID is lowercase base32 CIDv1 raw SHA-256 over a domain-separated canonical projection of the expanded ordered child-MusicCID sequence plus sorted stable-slot semantic track trajectories. It includes occurrence order, count, and routing while excluding title, section labels, repeat grouping, display order, and one consistent global track-ID rename. The transport digest covers the complete uncompressed canonical JSON. Codec `c` carries those JSON bytes and codec `d` carries their raw-DEFLATE form; the same auto-versus-explicit selection rule applies.

URL decoding is host-independent: it accepts a fragment alone or reads the verified fragment from any absolute URL. The browser's **Copy link** action normalizes the copied origin to `https://sound.fish/`. A loaded loop may also be rewritten to the auto-selected `c` or `d` spelling; a loaded arrangement retains its exact valid incoming fragment when copied.

The hard URL limits below are Soundfish encode/decode bounds, not cross-application sharing guarantees. Long arrangement links can exceed limits in chat, email, command shells, QR tools, or embedded browsers. Use the editor's lossless Soundfish JSON export when a link is truncated. For CLI URL input, only the read-only address commands (`inspect`, `verify`, `handoff`, `analyze`, `lint`, `render`, `export-json`, and `export-midi`) accept `-` in the address position to read standard input.

The local resolver offsets notes and automation into a bounded runtime playback score and resets each lane at section boundaries. That score is not a canonical loop document, cannot enter loop history or the loop library, and is never encoded as `#s1`. **Edit a copy** creates a new editable loop from the selected canonical child instead.

## Fixed sound release

Program and percussion numbers select from one global Soundfish catalog. The first runtime release uses MuseScore General 0.2.0 through SpessaSynth. Sound-bank samples, the fixed reverb impulse response, and engine build are deliberately outside the document and MusicCID. All addresses play through the current fixed release, allowing Soundfish to improve rendering without changing the symbolic composition or its music address.

The active `soundfish.sound-release/v1` identity is `sha256:bf3a64a6e587acefd1091371943c65ca3230541659b5bff77de93e51cd92ad54`. It binds the exact bank, worklet, renderer versions, effect graph, automation model, impulse algorithm, limiter, delay bounds, and defaults. The root document exposes the same value as `data-sound-release` for deployment verification. It does not enter MusicCID.

Each loop remains one canonical block. Song resolution uses only the child loops embedded in its canonical arrangement block; there is no resolver dependency or remote Merkle traversal. A future storage graph must reconstruct the same child-sequence and stable-track-trajectory projections and cannot change MusicCID or ArrangementCID.

## Compact agent text

Version `S1` is a lossless tab-separated view in canonical order:

```text
S1
D <json-title> <bpm> <bars> <reverb-return-base> <delay-return-base> <delay-feedback-base> <delay-time-millibeats-base> [swing|-] [tonic|-] [mode|-] [json-genre|-]
A D <document-lane:r|d|f|t> <step> <value> <curve:h|l>
T <track-id> <track-kind:n|d> <json-name> <gm-program|kit|-> <lineage-kind:o|d> [operation-id source-count] <volume-base> <reverb-send-base> <delay-send-base> [pan-base|-] [cutoff-base|-]
S <source-track-id> <source-digest>     # repeated source-count times
A T <track-lane:v|r|d|p|c> <step> <value> <curve:h|l>
E <event-id> <pitch> <startStep> <endStep> <velocity>
X
Z

Records are tab-separated. T..X repeats once per track. In the track-kind
column, n means a notes track and d means a drums track. Notes tracks require
a zero-based GM program; drums tracks carry their GS drum kit program there,
and - means kit 0. In the separate lineage-kind column, o means original and
d means derived. JSON strings use canonical JSON escaping.
Event endStep is the exclusive absolute endpoint, not a duration.
Automation curves control the segment leaving their point. h means hold and
l means linear. Track lanes are v volume, r reverb send, d delay send, p pan,
and c cutoff.

Bracketed trailing columns are optional and omitted when every later column
is also at its default: swing 0, no tonic, no mode, no genre, pan base 5000,
cutoff base 10000. A column at its default is spelled - only when a later
column is present. Modes are major, minor, dorian, mixolydian, lydian,
phrygian, aeolian, and harmonic-minor. Records, tracks, sources, automation
points, and events must already be in canonical order.
```

The `D` record carries shared effect bases, then optional swing, tonic, mode, and genre columns, and `A D` records carry shared effect automation. The `T` record uses `n` for a notes track and `d` for a drums track; its program column carries the GM program on notes and the drum kit on drums, where `-` means kit 0. Track production bases follow the lineage, with optional pan and cutoff base columns. A trailing optional column is omitted when it and every later column are at their defaults and spelled `-` when a later column is present. `A T` records carry track automation on lanes `v`, `r`, `d`, `p`, and `c`. Lineage is `o` for original or `d` followed by an operation ID and checked source records.

## Compose

Compose is the hand-written authoring format. The CLI compiles it into one canonical `soundfish.document/v1` loop, so it adds nothing to the wire: the document, agent text, CBOR, and MusicCID are unchanged. Every position is explicit, which is why it suits an agent better than accumulated durations: a note is `pitch@slot>length`, the slot is 1 through 16 inside its bar, bars are separated by `|`, and errors name the line.

```text
# comment to the end of the line
title Night Drive          # optional; the rest of the line is the title
bpm 124                    # 40 to 240, default 120
bars 4                     # 1 to 16; default: the longest line
key F# minor               # optional tonic and mode (meta only); key C means C major
genre house                # optional, at most 32 bytes (meta only)
swing 20                   # 0 to 100, default 0
kit 808                    # default kit for drums blocks: name or 0 to 127
fx reverb 2000 delay 2500 feedback 3500 time 500   # document effect bases

drums                      # or: drums 25, drums 808, drums "Lo-fi"
name Kit                   # optional track name (default Drums)
vol 9000 rev 500           # vol rev del pan cut, any subset, basis points
kick:  x...x...x...x...    # x accent 110, o hit 90, . rest
snare: ....x.......x...
hh:    o.o.o.o.o.o.o.o. @70/100   # @hit or @hit/accent velocity override
ohh:   ..............x.
clap:  ....x.......x... | ....x.......x.x.   # | separates bars; 2 bars repeat twice

bass gm:34                 # 1-based General MIDI number 1 to 128 ...
vel 100                    # ... or a name: bass "Fingered Bass"; vel is the default velocity
C2@1>4 C2@7>2 G1@9>4 Bb1@13>4=80 | C2@1>4 C2@7>2 F2@9>4 G2@13>2
  # pitch@slot>len=vel: slot 1 to 16 within its bar, len in sixteenth steps (default 1),
  # =vel overrides the velocity, | separates bars; C4 is MIDI 60; C7 is a note, Cdom7 a chord

keys "Electric Piano 1"    # chords voice from octave 3: Am7 is A3+C4+E4+G4, C is C3+E3+G3
Am7@1>16 | Fmaj7@1>16 | Cmaj7@1>16 | G@1>8 Gsus4@9>8   # + stacks pitches; more lines add voices
```

Header lines may appear in any order and every one is optional: `title`, `bpm` (default 120), `bars` (default: the longest line), `key` (tonic and mode; `key C` is C major, `tonic` and `mode` set one alone), `genre`, `swing`, `kit` (the default kit for drums blocks), and `fx` (reverb return, delay return, feedback, and time bases). A `#` starts a comment. Blocks follow in track order.

A drums block starts with `drums` and an optional kit by GS program number or name. Each row is a drum name, a colon, and a step string: `x` is an accent (velocity 110), `o` a hit (90), and `.` a rest; `@hit` or `@hit/accent` overrides a row's velocities. Rows name a General MIDI percussion sound, a short alias (`kick`, `snare`, `hh`, `ohh`, `clap`, `rim`, `ride`, `crash`), or a pitch from 35 through 81.

An instrument block starts with a track name and an instrument: `gm:N` with the conventional 1-based General MIDI number from 1 through 128 (the stored program is N − 1), or a quoted General MIDI name. Note lines hold tokens `pitch@slot`, `pitch@slot>length`, or `pitch@slot>length=velocity` (default velocity 96, or the block's `vel` line). A pitch is a note name with octave (`C2`, `F#3`, `Bb1`; C4 is MIDI 60), a MIDI number, or a chord symbol such as `Am7`, `Fmaj7`, `Gsus4`, or `Cdom7`, voiced from octave 3; `+` joins simultaneous pitches. A letter with only an octave digit is always a note, so `C7` is the note and `Cdom7` the chord. A pattern of one bar, or of any bar count that divides `bars`, repeats to fill the loop; a line starting with `|` continues the previous note line. A production line `vol rev del pan cut` sets bases in basis points, and `name` renames the track.

`soundfish compose loop.compose` prints the URL, and `soundfish inspect <url> --format compose` prints a loop back in this form with program names as comments. That view keeps every note, hit, velocity, program, kit, base, swing, and meta value, so compiling it reproduces the MusicCID only for documents without automation points. It does not show automation points, lineage, or IDs; use S1 or Soundfish JSON for a lossless round trip.

## Published CLI and skill

Run `bunx @hraness/soundfish --help` without installing, or install the command with `bun add --global @hraness/soundfish`. The primary [Soundfish skill](https://sound.fish/skills/soundfish.zip) has one top-level `soundfish/` directory and must be inspected before extraction. The former edit-soundfish archive remains a compatibility entry point. The skill ships its reference files, templates, and examples; each reference file is also served on its own: [gm-programs.md](https://sound.fish/skills/soundfish/reference/gm-programs.md), [drums.md](https://sound.fish/skills/soundfish/reference/drums.md), [mixing.md](https://sound.fish/skills/soundfish/reference/mixing.md), [critique.md](https://sound.fish/skills/soundfish/reference/critique.md), [cli.md](https://sound.fish/skills/soundfish/reference/cli.md), [genres/house.md](https://sound.fish/skills/soundfish/reference/genres/house.md), [genres/boom-bap.md](https://sound.fish/skills/soundfish/reference/genres/boom-bap.md), [genres/half-time.md](https://sound.fish/skills/soundfish/reference/genres/half-time.md), [genres/breakbeat.md](https://sound.fish/skills/soundfish/reference/genres/breakbeat.md), [genres/jazz.md](https://sound.fish/skills/soundfish/reference/genres/jazz.md), [genres/orchestral.md](https://sound.fish/skills/soundfish/reference/genres/orchestral.md). The [sound guide](https://sound.fish/docs/music.md) condenses them.

```sh
bunx @hraness/soundfish --help
bun add --global @hraness/soundfish
soundfish templates list [--kind grooves|progressions|bass|songs]
soundfish templates apply grooves/house-basic --key Am --bpm 124 [--compose]
soundfish compose loop.compose
soundfish inspect '<loop-or-song-url>' [--format agent|json|compose] [--annotate]
soundfish verify '<loop-or-song-url>'
soundfish handoff - < final.url > reply.md
soundfish handoff - --check reply.md < final.url
soundfish analyze '<loop-or-song-url>' [--format json]
soundfish lint '<loop-or-song-url>' [--stats stats.json|--render]
soundfish render --dry '<loop-url>' loop.wav [--format json]
soundfish export-json '<loop-or-song-url>' composition.soundfish.json
soundfish export-midi '<loop-or-song-url>' composition.mid
soundfish import-json composition.soundfish.json
soundfish encode document.soundfish [--strict]
soundfish fmt document.soundfish
soundfish add-track '<loop-url>' track.soundfish --operation op_name --from trk_source
soundfish replace-track '<loop-url>' trk_target track.soundfish --operation op_name --from trk_target
soundfish remove-track '<loop-url>' trk_target --operation op_name
soundfish resize '<loop-url>' 8 --operation op_name
soundfish set-tempo '<loop-url>' 124 --operation op_name
soundfish set-title '<loop-url>' 'Night drive' --operation op_name
soundfish set-effects '<loop-url>' --reverb 2500 --time 750 --operation op_name
soundfish set-document-automation '<loop-url>' delay-return --points 0:0:linear,32:4000 --operation op_name
soundfish add-event '<loop-url>' trk_bass --pitch C2 --start 0 --end 4 --velocity 100 --operation op_name
soundfish move-event '<loop-url>' trk_bass evt_id --start 8 --operation op_name
soundfish remove-event '<loop-url>' trk_bass evt_id --operation op_name
soundfish set-kit '<loop-url>' trk_drums 25 --operation op_name
soundfish set-pan '<loop-url>' trk_keys --base 3500 --operation op_name
soundfish set-cutoff '<loop-url>' trk_pad --points 0:2000:linear,63:10000 --operation op_name
soundfish set-swing '<loop-url>' 55 --operation op_name
soundfish set-meta '<loop-url>' --key Am --genre house --operation op_name
soundfish transpose '<loop-url>' trk_bass -12 --operation op_name
soundfish shift '<loop-url>' trk_hats 2 --operation op_name
soundfish humanize '<loop-url>' trk_drums --velocity 12 --seed 7 --operation op_name
soundfish duplicate-bars '<loop-url>' trk_bass 1-2 --to 3 --operation op_name
soundfish double '<loop-url>' --operation op_name
soundfish fill '<loop-url>' trk_drums --bar 4 --seed 3 --operation op_name
soundfish variation '<loop-url>' trk_drums --bars 2,4 --density -20 --seed 3 --operation op_name
soundfish accent '<loop-url>' trk_hats --pattern 100,60,80,60 --operation op_name
soundfish thin '<loop-url>' trk_keys --to-bytes 20000 --operation op_name
soundfish duck '<loop-url>' trk_bass --against trk_drums --pitch 36 --depth 3000 --release 2 --operation op_name
soundfish set-program '<loop-url>' trk_bass 'Electric Bass (finger)' --operation op_name
soundfish set-volume '<loop-url>' trk_bass 8000 --operation op_name
soundfish set-send '<loop-url>' trk_keys --reverb 1500 --delay 500 --operation op_name
soundfish rename '<loop-url>' trk_keys 'Rhodes' --operation op_name
soundfish legato '<loop-url>' trk_pad --operation op_name
soundfish import-midi input.mid [--as-song]
soundfish song build --title 'Night drive' --section 'Intro:<loop-url>x2' --section 'Drop:<loop-url>'
soundfish song sections '<song-url>'
soundfish song detach '<song-url>' 2
soundfish song apply '<song-url>' -- set-tempo 124 --operation op_name
soundfish song doctor
soundfish song import 'https://www.youtube.com/watch?v=VIDEO_ID' --output artifacts/song-imports
soundfish song recut <run-directory> --loop-bars 4
soundfish <command> --help
```

The `inspect`, `verify`, `handoff`, `analyze`, `lint`, `export-json`, and `export-midi` commands accept a loop or song URL; those commands, `render`, and the song verbs accept `-` in the address position to read one URL from standard input. `compose -` reads compose text, `encode -` and `fmt -` read agent text, `import-json -` reads Soundfish JSON, and `add-track` or `replace-track` may use `-` only in the track-input position. Their base loop URL must be an explicit argument. Loop inspection defaults to compact agent text, `--format compose` prints the authoring view, and `--annotate` adds `#` comment lines naming each track's program, kit, and drum sounds; song inspection defaults to lossless JSON. `encode` accepts valid non-canonical agent text (CRLF, blank or comment lines, unsorted events or automation, explicit defaults), prints the URL, and reports `canonicalized: line N` on standard error; `--strict` rejects anything but canonical text, and `fmt` prints the canonical spelling. Every command prints its own usage with `--help`. Every command accepts `--format json`, which prints one object carrying the URL, the fragment, the MusicCID or ArrangementCID, the transport digest, the canonical byte count, and for an operation its ID, base digest, and whether it applied; `--base-url <url>` chooses the site prefix of a printed URL and `--fragment-only` prints the `#` fragment alone. The `handoff` command rejects `--fragment-only` because delivery requires a complete URL. `verify` prints `valid`, the kind, the MusicCID or ArrangementCID, the transport digest, and for a song its section count. Exit codes are fixed: 0 success, 2 usage, 3 invalid input, 4 conflict (a stale base digest or a derived ID that already exists), 5 rejected by the protocol, and 1 for an unexpected failure. Standard output remains machine-composable, while warnings and failures use standard error.

Loop operation verbs take the base loop URL and `--operation <op_id>` and print one new URL: `add-track`, `replace-track`, `remove-track`, `resize`, `set-tempo`, `set-title`, `set-effects`, `set-document-automation`, `add-event`, `move-event`, `remove-event`, `set-kit`, `set-pan`, `set-cutoff`, `set-swing`, and `set-meta`. Each is recorded as one `soundfish.operation/v1` envelope fenced on the base URL's transport digest, so the same operation ID against the same base reproduces the same URL byte for byte, and a base that has changed exits 4. Track transforms are recorded through `replace-track` with one-hop lineage from the transformed track and a derived track ID: `transpose`, `shift`, `humanize`, `duplicate-bars`, `double`, `fill`, `variation`, `accent`, `thin`, `duck`, `set-program`, `set-volume`, `set-send`, `rename`, and `legato`; `swing` is an alias of `set-swing`, a document operation. `humanize`, `fill`, and `variation` take `--seed` and are pure functions of the base, their parameters, and that seed. `duck` writes linear volume automation on the target track that dips at every hit of `--pitch` in the `--against` track and recovers `--release` steps later; `thin` drops the quietest events until the loop measures at most `--to-bytes` canonical bytes; `double` runs one fenced `resize`, one `replace-track` per track (`<op_id>_1`, `<op_id>_2`, and so on), and one `set-document` (`<op_id>_fx`) when the shared effects carry points.

Save final CLI URL output directly as `final.url`. Run `soundfish handoff - < final.url > reply.md` to generate one complete verified Markdown link. Keep the generated link line intact and add only plain prose around it; the entire reply must contain no code or HTML. Run `soundfish handoff - --check reply.md < final.url` before returning the checked reply verbatim. The check requires exactly one standalone Markdown link with the same complete canonical document, including title, IDs, and lineage, and the delivery origin, path, and query selected by `--base-url` (default `https://sound.fish/`). Equivalent compressed and uncompressed spellings are accepted. To read the reply from stdin, supply the source URL explicitly: `soundfish handoff '<url>' --check - < reply.md`; both inputs cannot use stdin. Retain the files for recovery. Regenerate a truncated link from the saved URL or from `import-json` of a lossless export, inspect it, then repeat the handoff procedure; never guess missing fragment characters. This checks the saved draft; later copying or delivery can still alter it.

Song verbs build and edit `#a1` addresses without leaving the CLI. `song build --title <text> --section "Label:<loop-url>[xN]" ...` assembles loops that share one tempo, one swing, and one track roster; `song sections <song-url>` prints one tab-separated line per section with its index, label, repeat count, bars, and MusicCID; `song detach <song-url> <index>` prints the loop URL of one section, whose MusicCID equals the embedded child; and `song apply <song-url> -- <loop verb ...>` runs one fenced loop operation with the same operation ID on every distinct section and rebuilds the song, so derived IDs match across sections and the roster stays compatible. A loop operation given a song address directly is refused and pointed to `song apply` or `song detach`.

`templates list` prints the hand-authored templates the skill ships (twenty grooves, ten progressions and ten bass idioms written in scale degrees, and three songs), and `templates apply <kind/name> --key <key> --bpm <n>` resolves the degrees to the key, sets the tempo, compiles the compose text, and prints the loop URL (or the song URL for a `songs/` template); `--compose` prints the resolved compose text instead. The templates are read from the `templates/` directory beside the skill's `scripts/` directory, so the bare bundle without it reports that and stops. A mutation printed with `--format json` also carries `tracks` (id, name, kind), because a transform replaces its track under a new derived ID.

Diagnostics are read-only and advisory. `analyze <url>` prints, without listening, the bar count, canonical bytes used of the 24493-byte ceiling, events used of 832, the MusicCID, the estimated key from the Krumhansl-Schmuckler profiles compared with the declared `meta` key, one chord per bar with its degree, the steps where a kick and a bass onset coincide, and per track the program or kit name, role (drums, bass, chords, lead), register, a per-bar density grid, empty bars, runs of identical bars, production bases, and a velocity histogram; a song prints its roster and one analysis per section. `lint <url>` applies the mechanical half of the critique rubric and prints one finding per line with a stable code (`empty-bar`, `empty-track`, `identical-bars`, `bass-register`, `chord-register`, `low-end-send`, `delay-feedback`, `byte-budget`, `flat-hats`, `volume-headroom`), a severity, the track and bars it refers to, and the verb that fixes it; it always exits 0. `render --dry <url> <out.wav>` renders one loop cycle in Bun through `spessasynth_core` and the pinned sound set as per-track dry stems (authored volume, pan, and cutoff and the fixed master trim; no sends, returns, compressor, or ceiling), writes the stereo pre-limiter mix as a 16-bit WAV whose duration equals the loop length, and prints peak, RMS, crest factor, and the share of energy below 120 Hz, from 120 to 500 Hz, from 500 Hz to 2 kHz, and above 2 kHz per track and for the mix; the PCM drum kits and SF3 overrides the document's programs use are layered above the base bank in the browser's priority order, so every stem uses the sound the browser plays, and a layer file that is missing or fails its digest is reported and falls back to the base bank. `lint --stats <file>` reads the `render --dry --format json` output, or `lint --render` renders first, and adds `bass-band`, `sub-band-crowding`, and `mix-clipping`. The render needs the repository checkout; the published CLI reports that and stops.

## Operations and lineage

`soundfish.operation/v1` envelopes carry an operation ID, exact base transport digest, and one discriminated operation: `create-document`, `add-track`, `replace-track`, `remove-track`, `move-event`, `add-event`, `remove-event`, `resize-document`, `set-kit`, `set-pan`, `set-cutoff`, `set-swing`, `set-meta`, or `set-document` (title, tempo, and shared effects with their automation). Owned track and event IDs are derived as 8 hex characters from the operation ID and ordinal. Applying one returns `applied`, `unchanged`, `conflict`, or `rejected`. Conflicts and rejections preserve the base. A rejection carries a code; a result that is valid music but exceeds the canonical byte ceiling is `byte-budget-exceeded` and one over the event budget is `too-many-events`, so a caller can thin the loop rather than look for a malformed field.

There is no unauthenticated hosted mutation endpoint. The CLI applies operations locally and emits a new self-contained URL.

## Standard MIDI interchange

Loop export writes canonical Type 1 MIDI at 96 PPQ (24 ticks per Soundfish step), one conductor track, one track per Soundfish track, independent melodic channels for notes, and channel 10 for drums with a program change for a non-zero kit. A non-default pan base is written as controller 10 at tick zero, and swing delays the note-on and note-off of every odd step by `round(swing / 100 × 12)` ticks. Soundfish sequencer metadata preserves the loop's titles, IDs, lineage, authored production, effects, automation, swing, and meta for exact re-import when another tool retains it.

Song export uses the same MIDI timing projection and adds standard marker events at section starts. It carries fixed tempo and meter, track names, GM programs, notes or drum hits, velocities, duration, and section labels. It does not carry complete Soundfish effects, automation, identities, lineage, or arrangement routing. Soundfish JSON is the lossless export for both loops and songs; MIDI is the interoperable performance projection.

MIDI import creates 1–16-bar loops. A channel-10 program change at tick zero becomes the drum kit, and controllers 7, 10, 91, and 93 at tick zero become the volume, pan, reverb send, and delay send bases. Common export habits are repaired and reported as warnings rather than rejected: a track that ends short of a bar line is padded to the next bar, note-off release velocities are dropped, drum hits longer than one step are shortened to one step, and bank select, reset-all-controllers, and all-notes-off messages are ignored. Section markers are reported, and `import-midi --as-song` builds one loop per marker span and assembles them into a song when every span starts on a bar line and covers 1 to 16 bars, falling back to one flat loop with a warning otherwise. Import still rejects what the loop model cannot represent: tempo or meter changes, off-grid notes, other controllers, pitch bend, aftertouch, SysEx, and SMPTE timing. Raw SMF is interchange, not a duplicate canonical storage field.

## Local song import

The published CLI exposes the local importer as `soundfish song doctor`, `soundfish song import <youtube-url> --output <directory>`, and `soundfish song recut <run-directory>`, which re-cuts a retained run with new cut options (bars, range, sections, swing detection) in about a second without StemDeck or Basic Pitch. It orchestrates a separately running StemDeck service for yt-dlp, Demucs separation, beat/downbeat analysis, and optional section analysis, plus a separately installed Basic Pitch process for pitched-stem transcription. A successful run writes local stems and analysis, JSON/URL/MIDI for each loop, and JSON/URL/MIDI for the complete song. Standard output is only the final `#a1` URL.

Import only media you have the right to download and transform. Separation, beat detection, section analysis, drum classification, and MIDI transcription are estimates; audition and correct the result. Source and stem audio never enter a Soundfish URL or JSON music address.

## Browser and performance profile

The supported floor is Safari and iOS Safari 16.4+, Chrome and Edge 113+, and Firefox 113+ over HTTPS. The runtime requires AudioWorklet, Web Crypto SHA-256, IndexedDB, and raw-DEFLATE streams. Embedded webviews and in-app browsers are best effort until certified.

The lower-tier target is an A13-class iPhone or newer, or a current browser on Android 10+ with about 4 GB of memory. Automated validation uses a 360×800 touch viewport, DPR 3, four reported hardware threads, and Chrome DevTools Protocol CPU throttling. A calibrated mid-tier profile is preferred; 6× throttling is the reproducible blocking fallback and 20× is diagnostic only. This emulates main-thread pressure, not mobile GPU, memory bandwidth, thermal behavior, or audio hardware, so it does not replace physical-device checks.

## Hard limits

| Value | Limit |
| --- | ---: |
| BPM | Integer 40–240 |
| Bars | 1–16 |
| Tracks | 16 |
| Notes tracks | 15 |
| GM1 programs | 128 |
| Events per track | 512 |
| Events per document | 832 |
| Drum kit | 0–127 |
| Swing | 0–100 |
| Title | Nonempty after trimming; 80 UTF-8 bytes |
| Track name | Nonempty after trimming; 48 UTF-8 bytes |
| Meta genre | 32 UTF-8 bytes |
| Canonical CBOR | 24493 bytes (the binding limit) |
| Loop fragment | 32768 characters |
| Absolute loop URL | 65536 characters |
| Agent text | 524288 UTF-8 bytes |
| Standard MIDI input | 524288 bytes |
| Arrangement occurrences | 512 |
| Embedded arrangement loops | 128 |
| Arrangement sequence entries | 512 |
| Repeats per sequence entry | 64 |
| Arrangement resolved bars | 4096 |
| Arrangement resolved events | 65536 |
| Arrangement automation/reset points | 65536 |
| Arrangement title | Nonempty after trimming; 160 UTF-8 bytes |
| Arrangement label | Nonempty after trimming; 80 UTF-8 bytes |
| Canonical arrangement JSON | 392192 bytes |
| Arrangement fragment | 524288 characters |
| Absolute arrangement URL | 1048576 characters |

Canonical CBOR bytes are the binding loop limit. The event cap is a secondary guard: 832 events with derived IDs fit the byte ceiling across 16 tracks and 16 bars with default production, while dense automation, long lineage, or long names exhaust the bytes sooner.
