Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

scorekit is an Agent-oriented music compiler for game and film-style scoring workflows. It compiles a reviewable YAML scene into deterministic MIDI, then delegates audio rendering and export to established external tools.

The compiler produces seamless loops, sample-aligned instrument/texture stems, suite sections, metadata, and OGG or WAV assets. Texture tracks bring deterministic field recordings, ambience, and SFX into the score timeline. Creative decisions stay in the upstream Agent and the text scene; scorekit does not contain a generative model.

scene.yaml -> Score IR -> MIDI -> renderer -> WAV -> FFmpeg -> game assets

The supported render backends are FluidSynth and TiMidity++ for SF2 SoundFonts, and sfizz for SFZ sample libraries.

Design priorities

  • Deterministic MIDI for the same scene and toolchain.
  • Portable, diff-friendly text inputs.
  • Atomic file output: failed builds do not leave partial assets.
  • External DSP and rendering tools instead of in-house synthesis.
  • Machine-readable schemas, diagnostics, and reports for Agent workflows.

Installation

  1. Install scorekit:
brew install talkincode/tap/scorekit
  1. Optional: install SFZ rendering backend (--renderer sfizz):
brew install talkincode/tap/scorekit-sfizz
  1. Verify the install:
scorekit --version
scorekit doctor

If doctor reports ready renderers and dependencies, you’re ready to build scenes.

Homebrew installs the matching prebuilt scorekit archive, FFmpeg, FluidSynth, the Agent skill, and the default MuseScore General SoundFont. scorekit-sfizz adds the optional sfizz_render backend for SFZ profiles. The installed wrapper sets SCOREKIT_SOUND_LIBRARY_DIR to Homebrew-managed sounds.

The Homebrew formulae are updated automatically on tagged releases.

Install from prebuilt binaries (no package manager)

GitHub Releases publish:

  • scorekit-x86_64-unknown-linux-gnu.tar.gz
  • scorekit-aarch64-unknown-linux-gnu.tar.gz
  • scorekit-x86_64-apple-darwin.tar.gz
  • scorekit-aarch64-apple-darwin.tar.gz
  • scorekit-x86_64-pc-windows-msvc.zip

Each release also includes SHA256SUMS. Extract the matching archive, place scorekit (or scorekit.exe) on PATH, then run:

scorekit --version
scorekit doctor

Install from a source checkout

A source build requires Rust plus runtime tools (fluidsynth and ffmpeg).

git clone https://github.com/talkincode/scorekit.git
cd scorekit
make install
scorekit --version
scorekit doctor

By default this installs scorekit and sfizz_render to ~/.local/bin/, installs the Agent skill to ~/.agents/skills/scorekit, creates a user-managed sound root at ~/.local/share/scorekit/sounds/, and downloads the official MIT-licensed MuseScore General 0.2.0 SoundFont.

make install PREFIX=/usr/local
make install-skill SKILLS_DIR="$HOME/.codex/skills"
make install-sound-dir SCOREKIT_SOUND_LIBRARY_DIR="/Volumes/Samples/scorekit"
make install-default-soundfont

The sound root contains sf2/, sfz/, and profiles/. SF2 builds default to sf2/MuseScore_General.sf2; an explicit --soundfont overrides it. SFZ builds still require an explicit --orchestration profile (which routes to one or more certified renderer profiles under profiles/).

Using a project-managed sound library

If you do not want the Homebrew-managed default, set your own sound root:

export SCOREKIT_SOUND_LIBRARY_DIR="$HOME/.local/share/scorekit/sounds"
scorekit doctor

Or pass --soundfont explicitly for a build.

Quick Start

Check the local environment first:

scorekit doctor
scorekit --json doctor

Validate and build the bundled forest scene with an SF2 SoundFont:

scorekit validate examples/scenes/forest.yaml
scorekit build examples/scenes/forest.yaml \
  --stems \
  -o out/forest.ogg

The build writes the full mix, one sample-aligned file per track, and a metadata file containing exact sample counts and loop information.

For an Agent-authored scene, query the live schema before writing YAML:

scorekit schema > scene.schema.json
scorekit --json validate scene.yaml

Use scorekit diff old.yaml new.yaml to review musical changes independently of YAML formatting.

Scene Protocol

A scene file is not a configuration file. It is the wire format between a composing agent and the compiler — the input half of scorekit’s machine contract, exactly as the Machine Interface is the invocation half. An agent that emits a valid scene document is speaking a protocol: every field has one deterministic, observable compile semantic, the format is versioned under the same semver as the binary, and violations are rejected loudly with machine-readable positions, never silently ignored.

This chapter is the normative specification of that protocol. The live, machine-readable source of truth is always scorekit schema (JSON Schema); when this prose and the binary disagree, the binary wins.

Why a protocol, not just a format

Five properties separate a protocol from a mere file format, and the scene DSL commits to all of them:

  1. Strictness. Unknown fields are rejected (deny_unknown_fields at every level). A typo, or a scene written for a newer scorekit, fails with exit 2 and a line/column — it is never silently dropped. Silent tolerance is how formats rot; loud rejection is how protocols stay honest.
  2. Deterministic semantics. The same scene + the same scorekit version compiles to byte-identical MIDI, always. There is no “interpretation”, no randomness outside the seeded performance.humanize, and no field whose effect depends on the machine it runs on.
  3. No dead fields. Every field must have a deterministic compile semantic. Mood tags, game-state hints, and creative intent (avoid: too_bright) are by design not part of the protocol — they belong in the agent’s prompt space. A field that “doesn’t do anything” is a protocol violation waiting to be invented twice.
  4. Machine discoverability. scorekit schema exports the full JSON Schema, including field docs, ranges, and defaults. An agent can learn to write valid scenes from the schema plus one error message, without reading source code.
  5. Text-native. UTF-8 YAML, line-oriented, diff-friendly. Version control, review, merge, and rollback are git’s job; the protocol’s job is to make diffs mean something (see scorekit diff for semantic comparison that ignores formatting).

Envelope

  • One UTF-8 YAML document per file; one document = one scene (or one suite, when sections is present).
  • Field names are snake_case. Unknown fields anywhere are an error.
  • Validation is two-layered, and both layers are part of the contract:
    • Structural — shape and types, as exported by scorekit schema. Parse errors report location: {line, column}.
    • Semantic — ranges, cross-field rules (e.g. motif required iff pattern: melody), reference checks. Violations report a field path like tracks[2].glide.

Versioning and stability

The scene protocol is versioned by the scorekit binary’s semantic version. There is deliberately no version field inside the scene file: strict unknown-field rejection already makes version skew detectable at the only moment it matters (compile time), and a self-declared version number that the compiler must trust would be weaker than one it derives from its own schema.

Within a major version:

  • Additive only. New fields are optional with defaults that preserve prior output — a scene that does not use a new field compiles to byte-identical MIDI before and after the addition. (Precedent: when pan/reverb/glide landed in 0.2.0, the golden SMF byte-comparison test did not change.)
  • No repurposing. An existing field’s name, type, range, default, or compile semantic does not change. The normative transform order (below) is part of the semantic and is equally frozen.
  • Old scene, new binary: always valid, same MIDI bytes.
  • New scene, old binary: rejected with exit 2 and the offending field path — a readable upgrade signal, not a corrupted asset.

Anything that breaks these rules is a major-version change. Determinism across tool versions (FluidSynth/FFmpeg) is a separate boundary owned by the Machine Interface: pin your toolchain for identical audio; MIDI needs only the scorekit version pinned.

Rules for extending the protocol

A proposed field must clear all of these gates (this is the governance encoded in the project’s iron rules):

  • It has exactly one deterministic compile semantic, statable in one sentence (“compiles to X”).
  • It is achievable with free/open sound sources — the schema must not contain fields only commercial libraries can honor.
  • Its absence compiles byte-identically to the world before the field existed.
  • It ships in the same change as at least one happy-path E2E test, one failure-path test if validation is involved, and an acceptance-matrix row.

Scene document

FieldType / rangeDefaultCompile semantic
titlestringinformational only; never affects output
storystringinformational only; never affects output — freeform narrative brief (theme, mood, dramatic intent) carried into meta.json so review agents can audit the music against its story
tempoint, 20..=300requiredBPM; sets the SMF tempo meta event
key<Note>_<major|minor> (C_major, F#_minor, Eb_major, …)C_majorroot + scale for all degree/numeral resolution
time_signatureN/D, N 1..=12, D ∈ {2,4,8,16}4/4bar length in ticks; drives pattern shapes
barsint, 1..=256requiredscene length; total ticks = bars × N × (PPQ·4/D)
loopboolfalsetrue = seamless-loop asset (sample-exact length, sealed seam); false = one-shot with --tail decay
harmony[numeral, …], diatonic i..vii (case per quality)major I-V-vi-IV, minor i-VI-III-VIIone triad per bar, cycled to fill; sustain/arpeggio/bass derive from it
motifs{name: [note, …]}{}melodies referenced by pattern: melody tracks; map is order-insensitive (sorted for determinism)
performanceobjectabsentdeterministic humanization (below); absent = exact mechanical rendering
textures[texture, …], ≤16[]deterministic ambience/SFX layers; source names bind through --texture-profile at build time and never affect MIDI
tracks[track, …], 1..=16required≤15 melodic + ≤1 percussion (drums or tabla)
sections[section, …][]turns the scene into a suite; one output asset per section

Track

FieldType / rangeDefaultCompile semantic
id[a-z][a-z0-9_-]{0,63}, unique per scenerequiredstable scene-local identity referenced by sections[].mute, midi --solo, stem file names (NN-<id>.ext), and every meta.json/inspect-instruments report; itself never affects compiled MIDI
palette[a-z][a-z0-9_-]{0,63}orchestration’s default_palettelogical orchestration-palette selector (--orchestration, --renderer sfizz only); pure routing metadata — never changes compiled MIDI and has no effect on SF2/TiMidity backends
instrumentportable name from scorekit schemarequiredexact GM program when one exists; standalone midi rejects profile-only melodic identities, while sfizz’s internal MIDI emits no false program; drums/tabla use channel 10
patternsustain arpeggio bass drums melody tablarequirednote-generation algorithm (below); drums and tabla pair only with their namesake instruments
motifmotif namerequired iff pattern: melody; must exist in motifs
intensity0.0..=1.00.6scales note velocities
articulationsustain staccato spiccato pizzicato tremolo mutesustainrender-time SFZ sample selector only; never changes the compiled MIDI
pan0.0..=1.0absentCC10 = round(v·127) once at tick 0; absent = no CC10 emitted
reverb0.0..=1.0absentCC91 = round(v·127) once at tick 0; absent = no CC91 emitted
glide0.0..=1.0, melody-onlyabsenttail portamento via pitch bend, clamped ±2 semitones (GM default bend range); loops glide last note → first note, seam-continuous

Texture track

Texture tracks are score-timeline material: field recordings, ambience, and sound effects used as part of the composition. They do not model runtime world audio such as distance attenuation, weather state, or engine RPM.

FieldType / rangeDefaultCompile semantic
source[a-z][a-z0-9_-]{0,63}requiredportable key resolved by --texture-profile; scene files never contain audio paths
modeloop | one_shotrequiredcontinuous source repetition or one full source playback at each trigger
start_beatquarter-note beat ≥00loop-only start position; must be 0 when the scene or any section loops
at[beat, …], 1..=64 entries[]one-shot-only trigger positions; the same schedule repeats in every scene-loop pass
gain0.0..=1.01.0linear gain applied to the arranged texture stem before summation

Beat positions are quantized to the nearest PPQ-480 tick, then converted to sample frames using the same quantized MIDI tempo as musical tracks. Loop textures run continuously across the two render passes; the normal loop seal therefore joins adjacent source frames even when the source duration does not divide the scene duration. The recording itself should be prepared as a loop-ready asset because scorekit does not conceal clicks inside the source. For a looping scene, a one-shot source must not be longer than one complete scene pass; this guarantees that one prior pass contains all tail material needed at the loop boundary.

Motif note

FieldType / rangeSemantic
degreeint, -21..=21scale step in the scene’s key; 0 = rest; 8 = tonic an octave up; negatives descend
beats0.125..=16duration in quarter-note beats

Section

FieldType / rangeDefaultSemantic
name[A-Za-z0-9_-]+, uniquerequiredoutput asset suffix (out-<name>.ogg)
bars1..=256requiredsection length
tempo20..=300scene tempoper-section override
loopboolfalseper-section loop treatment
mute[track id, …][]stable track ids silenced this section; muting every track is rejected
intensity0.0..=2.01.0multiplier on each track’s intensity

Sections inherit the scene’s key, tracks (including spatial fields), textures, motifs, harmony, and performance. A shared texture schedule must be valid for every section: each trigger is checked against the shortest derived section timeline, preventing a trigger from silently wrapping into a later pass of a shorter cue.

Performance

All optional, all deterministic:

FieldType / rangeEffect
humanize{timing_ms: 0..=50, velocity: 0..=30, seed: u64}seeded jitter — same seed, same bytes
swing0.0..=0.5delays off-beat eighths
legatoboolextends non-drum durations ~12%
dynamics{start, peak} of pp p mp mf f ffloop-safe arch start→peak→start

Normative compile semantics

These are observable guarantees an integration may rely on; changing any of them is a breaking protocol change.

Time and encoding. Output is SMF format 1, PPQ 480. A bar is N × (480·4/D) ticks. Events are encoded in the canonical order (tick, kind rank: note-off < pitch-bend < note-on, key) — this ordering is the byte-determinism guarantee.

Channels and programs. Melodic tracks take channels in declaration order (0, 1, 2, …), skipping channel 10 (index 9), which is reserved for the single percussion track (drums or tabla). An exact GM identity opens with its program change. Internal MIDI rendered against an exact sfizz patch leaves a profile-only melodic channel programless; standalone scorekit midi rejects that case before writing a file rather than publish MIDI that generic players interpret as program 0 (piano). Tabla remains programless on channel 10. pan/reverb CCs follow immediately at tick 0. Channel state persists across loop passes, so CCs are emitted once.

Patterns.

  • sustain — the bar’s full triad held for the whole bar.
  • arpeggio — eighth notes cycling chord tones in the fixed order root, third, fifth, third.
  • bass — chord root two octaves down; two half-bar notes when the meter’s numerator is even and ≥4, else one whole-bar note.
  • drums — kick on beat 1 (plus the midpoint beat when N ≥ 4), snare on off-beats, closed hi-hat on every beat and half-beat.
  • tabla — one channel-10 stroke per notated beat, cycling the fixed 16-beat sequence dha dhin dhin dha | dha dhin dhin dha | dha tin tin ta | ta dhin dhin dha across bar boundaries.
  • melody — the named motif, looped or truncated to exactly fill the length; degrees resolve in the scene key.

Transform order. swing → dynamics → legato → humanize, then glide bend computation (it must observe final onsets), then loop-pass duplication. Loop math is applied last so looped scenes stay sample-exact under every transform.

Harmony. One numeral per bar, cycled. Numerals resolve to diatonic triads of the scene key; sustain/arpeggio/bass all read the same per-bar chord, which is what keeps multi-track scenes harmonically coherent by construction.

Texture assembly. FFmpeg first normalizes every referenced recording to stereo signed 16-bit PCM at the requested build sample rate. scorekit then performs only deterministic cutting, repetition, zero-padding, placement, linear gain, and summation. Texture stems use the same exact output window and loop seal as instrument stems, so every stem stays sample-aligned and sums back to the full mix within the existing rounding tolerance.

Error contract

Protocol violations follow the machine-interface error shape: exit 2, and with --json a single structured object on stderr carrying location (parse) or field (semantic). The field path grammar is stable: tracks[i].pan, sections[i].mute[j], harmony[i], performance.swing. An agent is expected to repair a scene from field + message alone; that expectation is part of the protocol’s design, not a nicety.

What the protocol will not carry

  • No mood/state/intent fields (emotion, danger, avoid) — no deterministic compile semantic exists for them; they live in the agent’s brief.
  • No renderer or recording paths (.sf2/.sfz/audio locations) — sound sources are bound at invocation time (--soundfont, --orchestration, --texture-profile), so a scene never bakes in one machine’s disk layout. A track’s id/palette are portable routing metadata, not paths — the orchestration profile they route through resolves to real files, never the scene.
  • No embedded version negotiation, includes, or macros — one file, one scene, strict fields. Composition happens in the agent, not in a template engine.

Best Practices

This chapter collects proven scene patterns and workflow discipline from the bundled examples (examples/scenes/). Each recipe is a professional starting point: copy the shape, keep the discipline, change the music.

Workflow discipline

Follow the same loop for every scene, whether authored by a human or an Agent:

scorekit schema > scene.schema.json   # 1. learn the live contract first
scorekit validate scene.yaml          # 2. validate before every build
scorekit diff old.yaml new.yaml       # 3. review musical changes, not YAML noise
scorekit build scene.yaml --stems -o out/scene.ogg   # 4. build atomically
  • Schema first. Never write a scene from memory of an older version; scorekit schema is the live source of truth and strict unknown-field rejection will catch drift loudly (exit 2, field path).
  • Validate early and often. Validation is cheap and errors carry a machine-readable field/location; fix at the YAML layer, not by inspecting audio.
  • Review with diff, not git diff. scorekit diff compares compiled semantics, so a reformat shows as “no musical change” while a single intensity tweak is surfaced precisely.
  • Pin the toolchain for audio. The same scene + scorekit version always yields byte-identical MIDI; identical audio additionally requires pinning FluidSynth/sfizz/FFmpeg versions and the sound source. Record all of them next to the scene in version control.
  • Trust atomic output. Failed builds never leave partial assets, so a build script may safely overwrite in place and treat exit code 0 as “asset is complete”.

Recipe: seamless ambient loop

The bread-and-butter game asset — background music that loops without an audible seam (see examples/scenes/forest.yaml):

title: Forest Theme
story: >-
  Calm nocturnal forest ambience for the exploration loop — soft,
  unhurried, no threat.
tempo: 92
key: D_minor
time_signature: "4/4"
bars: 8
loop: true
tracks:
  - id: harmony
    instrument: strings
    pattern: sustain
    intensity: 0.4
  - id: motion
    instrument: piano
    pattern: arpeggio
    intensity: 0.55
  - id: foundation
    instrument: bass
    pattern: bass
    intensity: 0.45
  - id: pulse
    instrument: drums
    pattern: drums
    intensity: 0.3

Practices that make a loop work:

  • loop: true is the whole seam contract. The compiler seals note tails across the boundary and the build emits a sample-exact asset; do not try to fade the seam in post.
  • Match bars to the harmony cycle. With a 4-numeral progression, use 4, 8, or 16 bars so the loop point lands on the start of the progression, not mid-cycle.
  • Keep ambient intensities low and close together (0.3–0.6). A loop heard for minutes must leave headroom and avoid one element dominating.
  • Write the story field. It never affects output, but it travels into meta.json so a reviewing Agent (or a colleague, months later) can audit the music against its intent.

Recipe: one file, one theme, four game states

Instead of composing four unrelated cues, write one scene with sections that share key, motif, and instrumentation (see examples/scenes/forest_suite.yaml):

motifs:
  theme:                      # the shared melodic identity
    - { degree: 1, beats: 1.5 }
    - { degree: 3, beats: 0.5 }
    - { degree: 5, beats: 1 }
    - { degree: 4, beats: 1 }
    - { degree: 3, beats: 1.5 }
    - { degree: 2, beats: 0.5 }
    - { degree: 1, beats: 2 }

tracks:
  - { id: lead, instrument: flute, pattern: melody, motif: theme, intensity: 0.7 }
  - { id: harmony, instrument: strings, pattern: sustain, intensity: 0.5 }
  - { id: motion, instrument: harp, pattern: arpeggio, intensity: 0.5 }
  - { id: foundation, instrument: bass, pattern: bass, intensity: 0.5 }
  - { id: pulse, instrument: drums, pattern: drums, intensity: 0.4 }

sections:
  - name: intro               # theme alone, no rhythm section
    bars: 4
    mute: [foundation, pulse]
    intensity: 0.7
  - name: explore             # full band, seamless loop
    bars: 8
    loop: true
  - name: combat              # same theme, faster and harder
    bars: 8
    loop: true
    tempo: 132
    intensity: 1.4
  - name: victory             # short sting with natural decay
    bars: 2
    mute: [pulse]
  • Vary state with tempo, mute, and the intensity multiplier — not new material. The player must always recognize the same place; the shared motif is what carries that identity across intro, exploration, combat, and victory.
  • Transitions are just short non-loop sections. A 2-bar sting with a couple of tracks muted is a victory fanfare; no separate scene file needed.
  • One file per location keeps review honest. A scorekit diff on the suite shows exactly which dramatic state changed.

Recipe: film-style expressive cue

For narrative scoring, the performance block turns a mechanical render into a played one (see examples/scenes/elegy.yaml):

tempo: 58
key: D_minor
harmony: [i, iv, VI, v]       # plagal grief — override the default progression

performance:
  humanize: { timing_ms: 18, velocity: 10, seed: 7 }
  legato: true                # bow stays on the string
  dynamics: { start: pp, peak: mf }   # one long arch, returning to silence

tracks:
  - id: lead
    instrument: violin        # the lone voice
    pattern: melody
    motif: lament
    intensity: 0.65
  - id: motion
    instrument: harp          # sparse consolation
    pattern: arpeggio
    intensity: 0.3
  • Always seed humanize. The jitter is deterministic per seed; commit the seed and every rebuild is byte-identical. Change the seed only when you want a different performance.
  • dynamics is loop-safe by construction (start → peak → start), so an expressive arch still works inside a looping cue.
  • Slow music needs rests. End a lament motif with { degree: 0, beats: 1 } — degree 0 is a rest, and the breath before the phrase returns is part of the phrase.
  • Choose harmony deliberately. The default progressions are safe, not expressive; four numerals ([i, iv, VI, v]) are often the single most characterful line in the file.

Motif craftsmanship

Motifs are where the musical identity lives; the patterns around them are accompaniment.

  • Give a motif an arch or a hook, not a scale. The elegy’s lament reaches up (5 → 8) then descends stepwise to rest; the chiptune hook (examples/scenes/chiptune.yaml) bounces between chord tones in octaves. Both are singable after one hearing.
  • Size the motif to the harmony. A motif whose total beats equal one or two bars re-aligns with the chord cycle naturally; odd lengths create drifting phase — use that only on purpose.
  • Reuse one motif across tracks and sections rather than inventing several. Recognition beats variety in functional scoring.
  • Reserve glide for one lead voice. Portamento on everything is mud; on a single melody track it is expressive, and in loops it glides seam-continuously from last note back to first.

Mixing inside the scene

The scene is also the mix, and it is fully deterministic:

  • Build a depth hierarchy with intensity: lead ≈ 0.65–0.7, harmonic bed ≈ 0.4–0.5, drums lowest. Leave the 0.8+ range for moments that must cut through.
  • Separate voices with pan — e.g. harp 0.35, strings 0.65 — instead of hoping the renderer sorts it out. pan/reverb compile to single CCs at tick 0: predictable, diff-able, portable across renderers.
  • articulation never changes the MIDI. It only selects SFZ samples at render time, so switching sustaintremolo is a render decision you can audition freely without invalidating the compiled score.
  • Use --stems when the game engine mixes. Sample-aligned per-track files let the engine duck, mute, or crossfade layers at runtime — often better than baking several intensity variants.
  • Give every id a role, not a number (lead, harmony, foundation, pulse) — it names the stem file, survives track reordering, and reads clearly in sections[].mute/--solo years later.
  • Use palette to layer a soloist over a section under --renderer sfizz --orchestration <file>. One violin track on a solo palette next to another violin track left on the default ensemble palette resolves each through its own certified renderer profile — no cross-palette fallback, no path in the scene.

Style governance with grammar profiles

When several scenes (or several Agents) must share an aesthetic, encode the aesthetic as measurable rules (see examples/grammars/grief.yaml):

name: grief
rules:
  tempo_max: 60
  melodic_voices_max: 2
  melody_rest_ratio_min: 0.35
  resolution: incomplete
  harmony_allowed: [i, iv, v, VI, VII]
  require_performance: true
scorekit lint scene.yaml --grammar examples/grammars/grief.yaml
  • A grammar is a constitution, not a composition. It says what the style is allowed to sound like; the scene says what it does sound like. Keep them in separate files, both under version control.
  • Lint in CI. Every rule is deterministic and machine-checkable, so a pull request that breaks the project’s musical language fails the same way one that breaks the schema does.

Batch and CI

For a project with many scenes:

scorekit --json doctor                          # gate: fail fast on missing tools
scorekit batch scenes/*.yaml --out-dir out/     # one JSON report for all assets
  • Gate CI on doctor so a missing renderer fails with exit 3 and a clear diagnosis instead of a mid-batch surprise.
  • Parse the batch JSON report rather than scraping logs; per-scene status plus stable exit codes (0/1/2/3/4) is the whole integration contract.
  • Cache by content, not by mtime. Because compilation is deterministic, the hash of (scene file + scorekit version) is a valid cache key for MIDI; add tool and sound-source versions to the key for audio.

Command Reference

All commands accept the global --json flag. Successful diagnostic commands write JSON to stdout; errors write one JSON object to stderr.

CommandPurpose
doctorProbe the platform, architecture, FFmpeg, and render backends
validate <scene>Validate scene syntax and semantics
schemaPrint the scene JSON Schema
schema --grammarPrint the grammar-profile schema
schema --profilePrint the leaf renderer-profile schema
schema --orchestrationPrint the orchestration-profile schema
schema --texture-profilePrint the texture-source profile schema
schema --resolverPrint the instrument-resolver config schema
lint <scene> --grammar <file>Check compiled music against measurable style rules
midi <scene> -o <file>Compile deterministic Standard MIDI
render <midi> -o <wav>Render one MIDI file through a selected backend
export <audio> -o <file>Convert or trim audio through FFmpeg
build <scene> -o <file>Run the complete asset pipeline
profile check <profile>Render probes through every mapped SFZ patch in a leaf renderer profile
orchestration check <file>Validate palette bindings and every referenced leaf profile/SFZ file
inspect-instruments <scene>Resolve every track’s instrument and report substitutions and gaps
diff <old> <new>Compare scene semantics
batch <scenes...> --out-dir <dir>Build several scenes and write a JSON report
mcpServe MCP (Model Context Protocol) over stdio; each tool wraps one CLI command

Exit codes are stable: 0 success, 1 I/O failure, 2 invalid input, 3 missing dependency, and 4 external tool failure.

Run scorekit <command> --help for the complete flag list shipped by the installed binary.

Numeric audio-command options reject non-finite or out-of-range values before resolving tools or writing files: --sample-rate is 8000..=384000 Hz, --gain is 0.0..=8.0, --quality is 0..=10, --tail is 0.0..=3600.0 seconds, and --crossfade-ms is 0..=60000.

Scenes with textures require build/batch --texture-profile <file>. This flag is independent of the musical renderer: it works alongside either an SF2 --soundfont or an sfizz --orchestration profile.

Standalone midi can encode melodic instruments with exact GM programs plus channel-10 drums/tabla. It rejects profile-only melodic identities before writing the output: the command has no renderer-profile input, and omitting a program change would make a generic GM player select piano. Use build --renderer sfizz --orchestration <file> to render those identities.

Instrument resolution

build and batch resolve every track’s instrument against what the selected backend actually provides before anything is rendered. SF2 backends resolve the 60-instrument core plus exact named extension programs (shakuhachi, sitar, shamisen); non-GM world identities fail rather than defaulting to piano. With --renderer sfizz availability is each track’s effective palette’s leaf renderer profile (routed through --orchestration <file>, the only sfizz routing input for build/batch/inspect-instruments), and unmapped instruments go through a scored fallback policy confined to that one palette — resolution never crosses palettes, even when another palette in the same orchestration happens to map the same instrument:

  • --fallback-mode conservative (default) substitutes within the same instrument family only, minimum score 0.70. Missing instruments never silently become strings — substituting into strings always requires an explicit allowed_families: [strings] opt-in in the resolver config.
  • --fallback-mode strict performs no substitution: an unmapped instrument fails the build (exit 2, code resolution) and the error names the best candidate it refused to use.
  • --fallback-mode flexible also reaches related families and synth stand-ins.
  • World instruments are exact-source-only under every mode: no fallback may enter, leave, or occur within that family.
  • --resolver <file> supplies a config (see scorekit schema --resolver) with default_mode, minimum_score, allow_cross_family, allow_synth, allowed_families, and excluded_families.

Every substitution prints one WARN instrument fallback: line with its score and reasons; meta.json embeds the full instrument_resolution report, including each track’s id, declared/effective palette, resolved leaf profile, requested/effective articulation, and resolved SFZ path. Unresolved instruments abort before staging, leaving no partial artifacts.

inspect-instruments <scene> [--orchestration <file>] [--resolver <file>] [--fallback-mode <mode>] [--verbose] prints the same resolution standalone: per-track status (exact/alias/fallback/missing/rejected), scores, reasons, palette/profile/SFZ routing, and the missing-instrument list. It exits 2 when instruments are unresolved, and --verbose lists every scored candidate. Alias spellings (e.g. french_horn for horn) are pure surface syntax and never change MIDI bytes.

Rendering and Dependencies

scorekit intentionally delegates synthesis and post-processing. doctor considers the environment ready when FFmpeg and at least one renderer are executable on PATH.

ToolRoleRequirement
FFmpegAudio conversion and exportRequired for complete audio builds
FluidSynthPrimary SF2 renderer; uses MuseScore General by defaultAt least one renderer is required
TiMidity++Alternate SF2 rendererOptional
sfizz_renderSFZ rendererOptional

On macOS, install the standard dependencies with:

brew install fluid-synth timidity ffmpeg

On Debian or Ubuntu:

sudo apt-get install fluidsynth timidity ffmpeg

Homebrew and prebuilt scorekit archives do not bundle sfizz_render inside the main scorekit package. Install the optional backend with brew install talkincode/tap/scorekit-sfizz, or from a source checkout use make install / make sfizz. Apple Silicon builds sfizz_render from source because upstream macOS binaries are x86_64-only.

make install downloads the official MuseScore General 0.2.0 SF2 and its MIT license to ~/.local/share/scorekit/sounds/sf2/, or a custom SCOREKIT_SOUND_LIBRARY_DIR. FluidSynth and TiMidity use this file when --soundfont is omitted. An explicit SF2 overrides the default; sfizz still requires an explicit --orchestration profile (which itself routes to one or more certified renderer profiles). doctor validates the default file’s SF2 header and reports ok, missing, or invalid.

Texture recordings may be WAV, FLAC, OGG, or any other format the installed FFmpeg can decode. Before placement, FFmpeg converts them to stereo 16-bit PCM at the build’s --sample-rate; scorekit does not implement resampling or channel conversion. The normalized intermediates are command-scoped and are removed on success and failure.

Orchestration and Renderer Profiles

--renderer sfizz builds are routed by an orchestration profile — the only sfizz build/inspect input — which maps each scene track’s logical palette to an independently certified leaf renderer profile. The leaf profile is unchanged from earlier releases: it maps scorekit instruments and articulations to local SFZ patches, keeping machine-specific sample paths out of portable scene files. For where the patches themselves come from — public acquisition channels, the directory/manifest contract, and the certification workflow — see Building a Sound Library.

Orchestration profiles

# orchestration.yaml
schema_version: 1
name: hybrid-cinematic
default_palette: ensemble
palettes:
  solo:
    profile: ../renderers/scoredata-chamber.yaml
  ensemble:
    profile: ../renderers/scoredata-symphonic.yaml
scorekit schema --orchestration
scorekit orchestration check orchestration.yaml
scorekit --json orchestration check orchestration.yaml

orchestration check loads every referenced leaf profile relative to the orchestration file, resolves every leaf profile’s .sfz paths, and fails loudly — with no partial output — if a palette’s leaf profile or any mapped SFZ file is missing.

Each scene track carries a stable id and an optional palette ([a-z][a-z0-9_-]{0,63}, same syntax as id). A track that omits palette uses the orchestration’s default_palette. palette/id are routing metadata only — they never change compiled MIDI bytes, and sections[].mute and midi --solo already select by id. Resolution and instrument fallback are strictly isolated per palette: a track routed to solo only ever resolves against solo’s leaf profile, never falling back to ensemble’s mappings even if ensemble happens to cover the same instrument. Declaring several tracks with different palette values is how a scene expresses an explicit soloist-over-section layering (e.g. one violin track pinned to solo, another violin track left on the default ensemble palette) — both render and mix independently, each through its own leaf profile.

scorekit build scene.yaml --renderer sfizz \
    --orchestration orchestration.yaml -o scene.ogg --stems
scorekit inspect-instruments scene.yaml --orchestration orchestration.yaml

There is no per-build --profile flag for build/batch/inspect-instruments any more — --orchestration is the only routing input for sfizz. The low-level render command is unaffected: render --sfz <file> still renders one .sfz instrument directly, outside any orchestration.

Leaf renderer profiles

# renderers/scoredata-symphonic.yaml
name: scoredata-symphonic
root: /Volumes/Samples
instruments:
  violin:
    sustain: VSCO/Strings/Violin-Sustain.sfz
    pizzicato: VSCO/Strings/Violin-Pizzicato.sfz
  drums:
    sustain: Virtuosity/Programs/01-basic-kit.sfz

Every instrument requires a sustain mapping, which is also the fallback when a dedicated articulation is absent. A leaf profile’s own path in palettes.<name>.profile resolves relative to the orchestration file that references it; its .sfz paths then resolve relative to its own root (or its own file’s directory), exactly as before — orchestration only routes a palette name to one of these files, it never rewrites sample paths.

Certify a leaf profile before wiring it into any orchestration:

scorekit profile check profile.yaml
scorekit --json profile check profile.yaml > profile-report.json

The check deduplicates shared patch paths, renders melodic or drum probes twice, rejects missing and silent patches, captures sfizz warnings, and checks repeatability. If one physical patch backs both melodic and percussion mappings, it remains one patch report but must pass both probes; the report’s probes field records that evidence. Each passing patch reports a render_sha256 golden hash, so a saved report acts as a baseline: re-running the check after a tool or library change and diffing the hashes reveals exactly which patches drifted. If a comparison fails on the first attempt, the check records diagnostics (load average, tool identity, both render hashes, timings) and re-runs that patch once in isolation — a pass is reported as ok with a load_sensitive_flake warning and the evidence kept under flake_diagnostics; a repeat failure is final. Temporary probe files are removed on success and failure; set SCOREKIT_TMPDIR to place them on another disk (created if absent, otherwise the system temp dir is used).

Probe renders are bounded: sfizz_render runs with --use-eot (stop at the MIDI end instead of waiting for output silence, which a looping sustain patch never reaches), and a watchdog kills any tool that exceeds its wall-clock timeout or output-size cap, reporting the patch as render_failed. SCOREKIT_TOOL_TIMEOUT_SECS and SCOREKIT_TOOL_MAX_OUTPUT_MB override the limits.

scorekit schema --profile prints a leaf profile’s JSON Schema.

Instrument resolution and fallback

Leaf profile mappings are the author’s ground truth: a mapped instrument always resolves exactly. Instruments a scene requests but the active palette’s leaf profile does not map go through the instrument resolver instead of failing outright — scoped to that one palette only:

  • Same-family substitutes are scored on range, articulation, envelope, role, and timbre; the best candidate above the minimum score (default 0.70) is used, and the build prints a WARN instrument fallback: line naming the substitute, its score, and its reasons.
  • Strings are never a default absorber: no missing brass, woodwind, or plucked instrument falls back to a string patch unless the resolver config explicitly lists strings in allowed_families.
  • Drums are never substituted in either direction, and synth stand-ins require --fallback-mode flexible or allow_synth: true.
  • World identities (erhu, pipa, guzheng, dizi, shakuhachi, shamisen, sitar, tabla, oud, ney, duduk) are exact-source-only. Resolution never substitutes into, out of, or within that family: an absent pipa remains absent rather than becoming a guitar, shamisen, or koto.
  • When nothing qualifies the build fails with exit 2 (code resolution) before any file is staged, and the error carries the full resolution report including the best rejected candidate.
  • Fallback never crosses palettes. A track routed to solo is only ever scored against solo’s leaf profile’s own vocabulary, even when a different palette in the same orchestration maps the same instrument.

Preview the outcome without building — scorekit inspect-instruments scene.yaml --orchestration orchestration.yaml — or pin behavior with a resolver config (scorekit schema --resolver prints its schema):

# resolver.yaml
default_mode: conservative   # strict | conservative | flexible
minimum_score: 0.70
allow_cross_family: false
allow_synth: false
excluded_families: []        # families never used as substitutes
allowed_families: []         # explicit cross-family opt-ins (incl. strings)

Substitution only changes which SFZ patch is rendered; MIDI bytes, stem names, and meta.json track names keep the requested instrument. The inspect-instruments and build-time instrument_resolution reports carry, per track: track_id, declared/effective palette, the resolved leaf profile’s name/profile_path, requested/effective articulation, and the resolved sfz path.

Texture source profiles

Texture profiles solve the same portability problem for field recordings, ambience, and SFX. A scene uses a stable logical name; the external profile binds it to a machine-local audio file:

# scene.yaml
textures:
  - { source: river, mode: loop, gain: 0.25 }
  - { source: birds, mode: one_shot, at: [2, 10], gain: 0.5 }
# textures.yaml
schema_version: 1
name: forest
root: /Volumes/Samples
sources:
  river:
    path: ambience/river.flac
    description: Wide river bed, mid-distance, no bird calls
    category: organic
    tags: [water, flowing, continuous]
    playback:
      modes: [loop]
      default_mode: loop
    use_cases: [forest, travel]
    provenance:
      library: [email protected]
  birds:
    path: wildlife/birds.wav
    description: Single dawn chorus swell, ends on silence
    category: organic
    tags: [wildlife, chirping, transient]
    playback:
      modes: [one_shot]
      default_mode: one_shot
    use_cases: [forest, dawn]
    provenance:
      library: [email protected]
scorekit schema --texture-profile
scorekit texture inspect textures.yaml --category organic --tag water
scorekit texture check textures.yaml
scorekit build scene.yaml --texture-profile textures.yaml -o scene.ogg --stems

Source keys match [a-z][a-z0-9_-]{0,63}. Paths may be relative to root (or the profile directory when root is absent) or absolute. Every source a scene uses must be mapped to a real file; failure leaves no normalized, arranged, output, stem, or metadata artifact behind.

For compatibility, a legacy binding such as river: ambience/river.flac can still be used by build. It cannot be enumerated or certified: texture inspect and texture check reject profiles containing path-only bindings until they are migrated to structured sources. This preserves old renders without fabricating category, playback, or provenance facts.

Why every source carries metadata

A profile is not only a path table — it is the discovery contract an authoring agent reads before it writes textures[].source. Without it, two entries named amb1 and amb2 are indistinguishable without opening the files, and there is no way to conclude that nothing in the profile fits.

For every structured source, the descriptive fields are required, not optional: optional metadata makes the discovery contract only as good as the laziest entry.

  • description — one line on what is actually audible.
  • category — a closed vocabulary compiled into the binary, so it stays a stable filter axis instead of drifting into ambience / ambient / ambiences. Run scorekit schema --texture-profile to see the full list with per-value meaning: ambience, foley, impact, transition, tonal, industrial, organic, sound_design.
  • tags and use_cases — open vocabularies ([a-z][a-z0-9_-]{0,31}, 1–16 entries) for expressive room the closed enum deliberately lacks.
  • playback.modes — which of loop / one_shot the recording actually supports, plus a default_mode that must be one of them. This is enforced at build time: a scene that loops a one-shot is rejected before staging.
  • provenance.library — the versioned library identity the source came from.

Physics is deliberately not declared here. Duration, sample rate, peak and checksum are measured by scorekit texture check, because a hand-written duration is a fact nothing can verify and every re-export silently invalidates.

Finding sources: texture inspect

scorekit texture inspect textures.yaml                      # enumerate everything
scorekit texture inspect textures.yaml --category impact
scorekit texture inspect textures.yaml --tag rain --tag soft
scorekit texture inspect textures.yaml --mode loop --use-case forest
scorekit --json texture inspect textures.yaml --source river

Filters are exact and conjunctive: repeated --tag intersects, and scorekit never ranks by similarity. It answers “which sources satisfy all of these constraints”, never “which is closest” — so "status": "no_match" is a truthful answer rather than a plausible wrong pick, and it exits 0 the way diff does. A --category outside the closed vocabulary is a typo, not an empty result set, and exits 2.

Certifying sources: texture check

scorekit texture check textures.yaml
scorekit --json texture check textures.yaml

Every declared source is resolved, normalized through FFmpeg, and measured: sha256 (of the source file, so it identifies the recording rather than the decoder version), duration_seconds, frames, peak_abs, and rms. A source is reported missing, undecodable, or silent when it fails — silent being the failure mode that survives every structural check and only surfaces as an absent layer in the finished mix. Exit 4 if anything is undecodable, 2 for other failures, 0 when the whole profile certifies. Scratch renders are swept on every path, including failures.

Building a Sound Library

scorekit ships no samples beyond the default MuseScore General SF2. The reference sample corpus used to develop and certify the open renderer profile (internally called ScoreData) is not distributed — partly because several upstream licenses permit music use but restrict repackaging (Virtual Playing Orchestra explicitly forbids redistribution), and partly on principle: the corpus is a private, disk-local asset; what is public is the recipe. This page is that recipe. Every library in the corpus comes from a public channel listed below, so a third party can rebuild an equivalent corpus from scratch and certify it with scorekit profile check.

Design rules

The corpus follows the anti-homogenization program in docs/roadmap.md (section “Sound library & orchestration program”). The load-bearing rules:

  1. Versioned identity. A library enters the corpus only with a publisher/version (or commit) identity, a license record, and checksums. Downloads retain archive checksums; first-party generated libraries retain per-file SHA256SUMS, the pinned recipe, and generator provenance.
  2. Certification before use. A profile mapping counts as coverage only after scorekit profile check passes it: rendered twice, deterministic, non-silent, golden render_sha256 recorded.
  3. Gaps close with real sources, never wider fallbacks. A missing instrument is either closed with a genuinely fitting library or stays a visible, honest gap. Binding an unrelated patch to silence a warning is the one move that is always wrong.
  4. Additive mappings. New libraries add mappings; they never silently rebind existing instruments to a different timbre. Rebinding is an audible style change and must be an explicit, reviewed edit.
  5. License evidence stays literal. CC0, CC-BY, GPL-with-sampling-exception, and similar are preferred. No NC/ND variants and no commercial-SoundFont conversions. A public-domain declaration with an incomplete original sample chain can be accepted only when the declaration, missing evidence, and decision are permanently disclosed; it must never be relabelled CC0.

Directory contract

<corpus root>/                      # any disk location; not a git repo
  libraries/<publisher>/<lib>/<version>/   # extracted library content
  archives/<publisher>/<lib>/<version>/    # the original downloaded archive
  manifests/
    sources.tsv                     # acquisition ledger: archive, version, license, official URL
    archive-sha256sums              # checksums of every archive (verify from this directory)
    libraries/<lib>.yaml            # one manifest per library (identity, path, formats, license)
    patches/                        # diffs for locally repaired upstream files
  profiles/
    renderers/<name>.yaml           # scorekit leaf renderer profiles (instrument -> .sfz)
    orchestrations/<name>.yaml      # scorekit orchestration profiles (palette -> renderers/<name>.yaml)
    textures/<name>.yaml            # scorekit texture profiles (source name -> audio file)
  sf2/                              # SF2 soundfonts (GM tier)
  catalog/                          # generated inventory + stored certification reports
  incoming/                         # scratch area for downloads under evaluation

Two invariants keep the corpus auditable:

  • manifests/sources.tsv is the append-only acquisition ledger — one line per downloaded archive with its official source URL. First-party generated libraries instead carry their recipe and generator.json in the versioned library directory.
  • shasum -a 256 -c archive-sha256sums (run inside manifests/) must always pass; the certified profile check --json report stored under catalog/ doubles as a golden-render baseline for every patch.

Acquisition channels

Everything below is publicly downloadable. Versions are the ones the reference profile was certified against; newer upstream versions usually work but re-certify after any change.

Foundation (orchestra, keyboards, percussion)

LibraryVersionLicenseChannel
VSCO 2 Community Edition1.1.0CC0-1.0https://versilian-studios.com/vsco-community/ (also github.com/sgossner/VSCO-2-CE)
Versilian Community Sample Library (VCSL)1.2.2-rcCC0-1.0https://github.com/sgossner/VCSL
Virtual Playing Orchestra (waves)3.2VPO mixed-open: music use unrestricted, no repackaginghttp://virtualplaying.com
Virtual Playing Orchestra SFZ scripts3.3same as abovevirtualplaying.com/vp-downloads/Virtual-Playing-Orchestra3-3-standard-scripts.zip + ...-performance-scripts.zip
MuseScore General (SF2, GM tier)0.2.0MIT (samples: public domain / CC)https://ftp.osuosl.org/pub/musescore/soundfont/MuseScore_General/ (fetched by make install)

The VPO 3.3 SFZ scripts are overlaid onto the 3.2 wave set (merge the standard and performance script trees into the extracted 3.2 library); this is how the choir, solo voice, celesta, and english horn mappings are sourced.

Guitars, basses, drums, e-pianos (sfzinstruments / Karoryfer)

LibraryVersionLicenseChannel
Karoryfer Black & Green Guitars1.000CC0-1.0github.com/sfzinstruments/karoryfer.black-and-green-guitars (releases)
Karoryfer Black & Blue Basses1.002CC0-1.0github.com/sfzinstruments/karoryfer.black-and-blue-basses (releases)
Virtuosity Drums0.925CC0-1.0github.com/sfzinstruments/virtuosity_drums (releases)
Greg Sullivan E-Pianoscommit 8c3e581CC-BY-3.0github.com/sfzinstruments/GregSullivan.E-Pianos

FreePats (synths, pads, folk & fretted instruments)

All from https://freepats.zenvoid.org or github.com/freepats releases; CC0-1.0 unless noted.

LibraryVersionNotes
Synth Square / Synth Bass Lead / Synth Bass 1 / Synth Bass 22020-05-12 / 2020-05-22 / 2019-07-23 / 2021-04-05
Lately Bass2024-04-09
Synth Strings 1 / Synth Strings 22020-05-28
Synth Pad Bowed / Synth Pad Choir / Sweep Pad / New Age2019-07-19 / 2020-05-16 / 2019-08-13 / 2019-07-30
Spanish Classical Guitar2019-06-18nylon guitar
FSS Steel String Guitar2020-05-21GPL-3.0-or-later with FreePats sampling exception (rendered music is unencumbered; see the package’s readme.txt)
Button Accordion HN2024-03-29
MuldjordKit (acoustic drums)2020-10-18CC-BY-4.0

Community one-offs

LibraryVersionLicenseChannel
SamsterBirdies Pan Flutecommit 60d4974CC0-1.0github.com/SamsterBirdies/panflute
GM LightCool Fretless Bassartifact 5323Public-Domain-Claimed; original Freesound rights chain undisclosedmusical-artifacts.com/artifacts/5323
NeoSoundFonts BMS Pull Slap Basscommit d03fc7eCC0-1.0github.com/NeoSoundFonts/SP-BMS-Slap-Pull

The pan flute ships with a defective SFZ (see next section) — repair it before mapping. The two bass sources ship as SF2. Preserve each original, export with the pinned Polyphone release, audit paired opcodes and non-silent WAV files, retain raw and normalized SFZ checksums, then certify the normalized mapping. Artifact 5323’s metadata says only that its samples came from Freesound; it does not identify those files or their original licenses. Its manifest and SOURCE-DECLARATION.md therefore keep that limitation visible.

World instruments

The world-instrument vocabulary is additive: the original 60-instrument coverage target stays intact, while each of these 11 identities is admitted only with an exact source.

InstrumentCertified sourceLicense / status
ErhuAliExpress Erhu v1.000, tag/commit 6615047b, corrected sustain keymap + upstream short programCC0-1.0; release archive SHA-256 2f54cc1a19ccd842f1c05eeb136416e680633ff4112201d14f6bf9931aad3fe0
TablaSubodh Deolekar, Tabla strokes dataset, 650 original WAVs + deterministic 550-region SFZCC-BY-4.0; archive SHA-256 513ea8b7cae6e5ec7038a1dd3e3054e061739c64c13603faa6c3df8ea1468fa2
ShakuhachiMuseScore General zero-based GM program 77MIT SoundFont; exact SF2 tier
SitarMuseScore General zero-based GM program 104MIT SoundFont; exact SF2 tier
ShamisenMuseScore General zero-based GM program 106MIT SoundFont; exact SF2 tier
Pipa, guzheng, diziPending an exact, primary-source licensed payload
Oud, ney, dudukPending an exact, primary-source licensed payload

Acquire Erhu from the pinned v1.000 release; the tag resolves to commit 6615047b2fd06126877483e97b8bb4af9d00b080. manifests/recipes/build_erhu_clean_sfz.py maps the unchanged sustain WAVs one octave higher to their measured MIDI pitches, removes the upstream synthetic wobble/unison layers, restores velocity response, and sets the bend range to two semitones. The profile uses the generated clean patch for sustain and 03-erhu_short.sfz for staccato. The upstream marcato program layers those two recordings and is not relabelled as another technique.

For Tabla, retain the Zenodo archive and all 650 WAV files byte-for-byte. manifests/recipes/build_tabla_sfz.py verifies the complete Zenodo MD5 inventory plus 44.1 kHz/stereo/16-bit PCM structure, then writes 11 keys (MIDI 36–46), each using 50 sequential seq_position takes. It performs no normalization, trimming, resampling, effects, or random selection. Attribution to Subodh Deolekar and the CC BY 4.0 license stay beside the payload.

Private/commercial source boundary

A locally licensed source may close any remaining identity without changing the DSL: keep its payload outside the scorekit repository, give it a versioned manifest in the corpus, map the exact instrument in a private leaf renderer profile, and run scorekit profile check. Only vendor-supplied SFZ or an explicitly permitted WAV/SFZ export is admissible. Do not extract or convert Kontakt/Pianobook packages whose terms forbid sampler conversion, and never commit their samples. Until such a payload exists, the identity must remain unmapped and fail visibly.

First-party synthesized (scoredata-forge)

LibraryVersionLicenseRebuild source
ScoreData Music Box1.0.0CC0-1.0scoredata-forge/recipes/music-box.toml
ScoreData Whistle1.0.0CC0-1.0scoredata-forge/recipes/whistle.toml

These two ordinary WAV+SFZ libraries close gaps for structurally simple timbres without putting synthesis inside scorekit. Build and verify them in the separate scoredata-forge repository; install the full versioned output directory so recipe.toml, generator.json, SHA256SUMS, and the CC0 dedication remain with the samples. The producing platform proves a byte-identical rebuild with forge verify; the installed consumer boundary is independently gated by scorekit profile check.

Textures

The reference texture profile exposes 93 certified sources from libraries already in the corpus — VCSL and VSCO 2 CE “Miscellania” — spanning 2 ambiences, 9 foley gestures, 25 impacts, 12 transitions, 15 tonal gestures, 12 industrial mechanisms, and 18 abstract sound-design sources. Every source has a unique path and source hash; three are authored for looping and the rest are explicit one-shots. A full scan of the other manifested libraries found no honest additions: they contain melodic multisamples, release triggers, or round-robin drum banks rather than standalone textures, so they were not bulk-imported merely to inflate the count. The organic category remains an explicit gap in this open reference profile: simulated surf from an ocean drum is labelled as such rather than presented as a natural field recording. Texture profiles use the same portable-name-to-local-path model as renderer profiles, plus required discovery metadata (description, category, tags, playback modes, use cases, provenance) so an agent can pick a source without opening the files; see Orchestration and Renderer Profiles.

A texture source enters the corpus the same way an instrument does: acquire → manifest → map → certify. scorekit texture check is the texture-side counterpart of scorekit profile check — it proves every declared source exists, decodes, and is audible before a scene depends on it, and records a sha256 of the source file so a swapped recording is detectable.

Optional private nature profile

The local ScoreData corpus also has a deliberately isolated, non-commercial nature layer. It acquires 775 unique upstream files and maps 774 one-shot sources after quality gating: 720 ESC-50 WAV excerpts, 49 National Park Service recordings, and 5 NOAA PMEL underwater recordings. The resulting scoredata-nature-personal profile contains 530 organic and 244 ambience sources; scoredata-personal-all combines those 774 with the 93-source open reference set for 867/867 certified sources. One NPS stream recording remains in the archive but is not mapped because MP3 decoding overshoots 0 dBFS.

This layer does not close the open-profile organic gap:

  • ESC-50 is pinned at commit 33c8ce9eb2cf0b1c2f8bcf322eb349b6be34dbb6 (archive SHA-256 661183a6f53ef04f12c9bd618fed0ddc1713280d6c94a5a5431e844ba6f6a21f). Select the 18 categories cat, chirping_birds, cow, crackling_fire, crickets, crow, dog, frog, hen, insects, pig, rain, rooster, sea_waves, sheep, thunderstorm, water_drops, and wind. Preserve the WAV bytes, LICENSE, README, complete metadata CSV, per-file Freesound IDs, and attribution. The dataset-wide license is CC BY-NC 3.0, so these sources must never enter commercial work or the default open profile.
  • Crawl only the Amphibians, Birds, Geological, Hydrological, Insects, Mammals, Meteorological, and Reptiles sections of the NPS Natural Sounds Gallery. Save the gallery’s public-domain declaration, each detail page, direct audio URL, NPS credit, and source checksum. Fifty files are acquired; Singing Sands exposes no downloadable audio and the clipping gate excludes one stream file, leaving 49 mapped sources.
  • Download the five WAV links exposed by the NOAA PMEL Acoustics gallery. NOAA pages can contain partner material, so these remain tagged rights_review until each asset’s rights chain is independently cleared.

Keep the three libraries in separate versioned directories and retain an attribution ledger. After generating either personal profile, certify it with:

scorekit texture check profiles/textures/scoredata-nature-personal.yaml
scorekit texture check profiles/textures/scoredata-personal-all.yaml

Repairing defective upstream files

Occasionally an upstream file is broken as shipped. Two repairs exist so far: the pan flute’s SFZ was exported with every lokey/hikey, lovel/hivel, and loop_start/loop_end pair reversed (every region empty, rendering silence), and the MuldjordKit SFZ uses DrumGizmo’s nonstandard drum keymap (kick on 48, snare on 50…) instead of General MIDI (remapped to GM keys: kick 36, snare 38, hats 42/46…). The repair convention:

  1. Keep the upstream file byte-intact.
  2. Place the repaired copy alongside it with a .scoredata-fixN. infix and a header comment stating what changed and why.
  3. Store the diff under manifests/patches/ and record a structured transforms: entry in the library manifest (upstream and normalized SHA-256, patch path, type, reason, reversibility).
  4. Map only the repaired file.

Anyone rebuilding the corpus can re-apply the published patch or re-derive the fix from its description; nothing about the repair lives only in git history or someone’s memory.

Not every defect is repairable. VPO’s all-brass-SEC-sustain, all-brass-SOLO-sustain and all-strings-SOLO-sustain ensemble files hang sfizz 1.2.3 (rendering never finishes); bisecting shows any 4 of a file’s 7 groups render fine while any 5 hang — a cumulative voice-count interaction with no single broken opcode to patch. Such files are recorded as do-not-map in the library manifest’s notes: and profiles re-orchestrate around them visibly (map the individual section patches instead) — never by silently substituting a different sound.

Certification workflow

After placing libraries, write a renderer profile mapping scorekit instrument names to .sfz paths (see Orchestration and Renderer Profiles), then:

# 1. Archive integrity (inside manifests/)
shasum -a 256 -c archive-sha256sums

# 2. Certify every mapping: rendered twice, deterministic, non-silent
scorekit profile check profiles/renderers/<name>.yaml

# 3. Store the machine-readable report as the golden baseline
scorekit --json profile check profiles/renderers/<name>.yaml > catalog/reports/<name>.json

Each passing patch reports a render_sha256; diffing two stored reports pinpoints exactly which patches changed after a library or tool upgrade. A failing comparison is retried once in isolation with diagnostics recorded (load_sensitive_flake) so a loaded machine does not produce false nondeterminism verdicts — see Orchestration and Renderer Profiles.

The corpus currently certifies four renderer profiles — 242 mappings over 180 unique patches, 0 failures:

  • scoredata-open — broad reference: 108 mappings / 92 patches, covering all 60 core DSL instruments plus exact Erhu (sustain/staccato) and Tabla. First-party CC0 music-box and whistle libraries cover the structurally simple timbres; the declared-public-domain fretless and NeoSoundFonts CC0 pull/slap libraries cover the technique-specific basses. The sample tier and GM SF2 tier both retain 60/60 core coverage; world coverage is tracked separately (2/11 sample tier, 3/11 exact GM).
  • scoredata-chamber — one player per part: VPO SOLO strings/winds/brass, VSCO 2 CE upright piano and quiet organ, VCSL harpsichord and recorder (49 / 42). No percussion, drums, synths or ensemble patches — deliberate identity gaps.
  • scoredata-symphonic — full sections: VPO SEC strings/winds/brass, orchestral percussion, harp, celesta and choirs, VCSL Steinway B, VSCO 2 CE loud organ (64 / 54). No synths or electric instruments.
  • scoredata-synth — FreePats synth basses/leads/pads/strings, Karoryfer electric guitar and bass, Wurlitzer EP200, MuldjordKit drums (21 / 15). The acoustic orchestra is intentionally absent.

The chamber/symphonic pair doubles as the documented solo-vs-section variant pair: same score, audibly different orchestration identity. Binding both under one orchestration profile (e.g. solo: scoredata-chamber, ensemble: scoredata-symphonic) lets a single scene layer a soloist over a section without ever naming a local path — see Orchestration and Renderer Profiles.

Minimal rebuild walkthrough

ROOT=/path/to/my-sound-corpus
mkdir -p $ROOT/{libraries,archives,manifests/{libraries,patches},profiles/{renderers,orchestrations},catalog/reports,incoming}

# For each library in the tables above:
#   1. download the pinned version/commit from its channel into incoming/
#   2. verify + record:  shasum -a 256 <archive> >> $ROOT/manifests/archive-sha256sums
#   3. append a line to $ROOT/manifests/sources.tsv  (archive, version, license, URL)
#   4. extract into $ROOT/libraries/<publisher>/<lib>/<version>/
#   5. move the archive to $ROOT/archives/<publisher>/<lib>/<version>/
#   6. write $ROOT/manifests/libraries/<lib>.yaml  (id, name, publisher,
#      version, path, formats, tags, license, license_file, archive)

# Write a renderer profile over the extracted .sfz files, then certify:
scorekit profile check $ROOT/profiles/renderers/my-profile.yaml

# Wire it into an orchestration profile (palette -> renderer profile):
cat > $ROOT/profiles/orchestrations/my-orchestration.yaml <<'EOF'
schema_version: 1
name: my-orchestration
default_palette: main
palettes:
  main:
    profile: ../renderers/my-profile.yaml
EOF
scorekit orchestration check $ROOT/profiles/orchestrations/my-orchestration.yaml

# Point scorekit at the corpus:
export SCOREKIT_SOUND_LIBRARY_DIR=$ROOT
scorekit build scene.yaml --renderer sfizz \
    --orchestration $ROOT/profiles/orchestrations/my-orchestration.yaml -o out.ogg

A rebuilt corpus will not be byte-identical to the reference one (different download dates, archive re-compressions), but after certification it gives the same guarantee that matters: every mapped patch renders, is audible, and is deterministic — and your own stored report becomes your baseline.

Agent Skill

The release archive and source repository include an Agent skill under skills/scorekit. It teaches a skill-capable coding Agent how to query the schema, write scenes, validate musical structure, apply grammar profiles, and build assets.

Install it together with the local binary:

make install

Install only the skill or choose another skill root:

make install-skill
make install-skill SKILLS_DIR="$HOME/.codex/skills"

The default destination is ~/.agents/skills/scorekit. The installed skill contains SKILL.md and the detailed command and DSL reference.

The bundled examples/narrative-film-score.md demonstrates a complete Agent prompt, narrative brief, deterministic musical translation, validation/build commands, and completion report. Its companion examples/exile-in-the-dunes.yaml is a schema-validated 24-bar scene that can be copied into a project and rendered directly.

Machine Interface

The scorekit CLI is the SDK. Every command completes in a single invocation, reports errors as structured JSON, and exports its schemas on demand. Third parties who want to build servers, MCP tools, CI bots, or editor plugins on top of scorekit should wrap the CLI as a subprocess — there is no separate library to link, and none is needed: audio rendering already spawns external processes (FluidSynth, FFmpeg, sfizz), so a linked library would save nothing while losing process isolation.

Stability contract

The machine interface follows semantic versioning. Within a major version, the following are stable and safe to program against:

  • Command names and documented flags in the Command Reference.
  • The scene DSL itself, specified normatively in Scene Protocol — field semantics, additive-only evolution, and byte-identical compilation for existing scenes.
  • Exit codes: 0 success · 1 I/O failure · 2 invalid input · 3 missing dependency · 4 external tool failure.
  • The --json error object shape on stderr (below).
  • The JSON Schemas exported by scorekit schema, schema --grammar, schema --profile, schema --orchestration, schema --texture-profile, and schema --resolver (fields are added, not removed or repurposed).
  • The meta.json / report.json artifact fields.

Determinism boundary: the same scene + same sound source + same tool versions produces byte-identical MIDI, and audio identical within documented tolerances. Reproducibility across different FluidSynth/FFmpeg versions is explicitly not promised — pin your toolchain (e.g. in a container image) if you need cross-machine identical audio.

Structured errors (--json)

With the global --json flag, every failure prints exactly one JSON object to stderr and exits non-zero:

{
  "code": "validation",
  "message": "invalid value at `tracks[0].role`: unknown role `bass2`",
  "location": null,
  "field": "tracks[0].role",
  "exit_code": 2
}
  • code — stable machine tag: io, parse, validation, lint, profile_check, resolution, doctor, missing_dependency, tool_failure.
  • location{ "line": N, "column": N } for parse errors, else null.
  • field — the offending DSL field path for validation errors, else null.
  • lint errors carry a violations array; profile_check and doctor carry a full report object instead of location/field.

Successful diagnostic commands (doctor, profile check, diff, batch reports) write JSON to stdout.

Wrapping the CLI

Everything an integration needs is a subprocess call plus JSON parsing. Python:

import json, subprocess

def scorekit(*args):
    p = subprocess.run(["scorekit", "--json", *args],
                       capture_output=True, text=True)
    if p.returncode != 0:
        raise RuntimeError(json.loads(p.stderr.splitlines()[-1]))
    return p.stdout

scorekit("validate", "scene.yaml")
scorekit("build", "scene.yaml", "-o", "out/scene.ogg", "--stems")
meta = json.load(open("out/scene.meta.json"))

Node.js:

const { execFileSync } = require("node:child_process");

function scorekit(...args) {
  try {
    return execFileSync("scorekit", ["--json", ...args], { encoding: "utf8" });
  } catch (e) {
    throw JSON.parse(e.stderr.trim().split("\n").pop());
  }
}

const schema = JSON.parse(scorekit("schema")); // feed to an LLM / form generator
scorekit("validate", "scene.yaml");

This is the complete recipe for a custom MCP server or any other integration: expose one tool per command, shell out, and pass the structured error through to the model. The error field/location data is designed so an Agent can repair a scene from the message alone.

Built-in MCP server (scorekit mcp)

For the common case you don’t need to write the wrapper at all:

scorekit mcp        # MCP over stdio (newline-delimited JSON-RPC 2.0)

scorekit mcp exposes doctor, validate, schema, lint, build, inspect_instruments, orchestration_check, inspect_textures, texture_check, and diff as MCP tools. It is a pure protocol adapter: each tool call re-invokes the scorekit binary with --json, and the structured stdout/stderr is passed through verbatim as the tool result (isError: true carries the exact error object shown above). No HTTP, no auth, no resident state — determinism is untouched, and MCP clients get the same contract as subprocess callers. The build and inspect_instruments tools take orchestration (the sfizz routing input) instead of the retired per-build profile argument.

Example client registration (Claude Desktop / any MCP client):

{
  "mcpServers": {
    "scorekit": { "command": "scorekit", "args": ["mcp"] }
  }
}

Deployment

For cloud or CI use, the repository root ships a Dockerfile that pins scorekit, FluidSynth, FFmpeg, and the SHA-256-verified default SoundFont. Multi-arch images (linux/amd64, linux/arm64) are published on every release to Docker Hub (talkincode/scorekit) and GHCR (ghcr.io/talkincode/scorekit):

docker pull talkincode/scorekit          # or ghcr.io/talkincode/scorekit
docker run --rm -v "$PWD:/work" -w /work talkincode/scorekit build scene.yaml -o scene.ogg --stems
docker run --rm -i talkincode/scorekit mcp   # stdio MCP server in the pinned toolchain
docker build -t scorekit .               # or build the image locally

Pinning is what carries the determinism guarantee across machines. If you need an HTTP API, put your own thin gateway in front of the image — that layer belongs to the deployer, not the compiler.

What scorekit will not ship

  • No embedded HTTP/API server. scorekit stays a thin, single-invocation compiler; a resident service belongs to whoever deploys it and is a 20-line gateway in front of this interface. (scorekit mcp does not contradict this: stdio, one client, no state.)
  • No Rust library crate for now. Splitting a public scorekit-core crate is on hold until a real third-party consumer demonstrates a need that a subprocess cannot serve (for example, direct access to the Score IR). See the roadmap’s “Direction & intent (on hold, pending evidence)” section.

Architecture and Guarantees

scorekit is a thin compiler and orchestration layer.

YAML scene
  -> semantic validation
  -> Score IR
  -> deterministic MIDI
  -> external renderer
  -> PCM audio
  -> external FFmpeg export

Guarantees

  • The same DSL and compiler inputs produce byte-identical MIDI.
  • Loop and stem lengths are derived from quantized musical time.
  • File-writing commands stage output and publish it atomically.
  • JSON Schema and structured errors expose the same contract used by the CLI.
  • Renderer, orchestration, and texture-source profiles are external data, so scenes remain portable: an orchestration profile routes each track’s logical palette to an independently certified renderer profile, never the reverse.

Deliberate boundaries

scorekit does not implement synthesis, reverb, compression, a DAW, version control, or embedded compositional intelligence. It also does not require fields that only a commercial sound source can satisfy.

The upstream Agent owns narrative and creative decisions. scorekit owns deterministic compilation, validation, orchestration, and artifact integrity.