cochlea
The server exposes a set of MCP tools for programmatic audio composition, rendering, analysis, verification, and editing—letting an agent compose scores, render them to deterministic WAV, probe audio into numeric reports and spectrograms, verify assertions, and convert between formats.
Compose & render:
render_scoreturns a RON score into a WAV mix (optionally with stems and embedded assertion checking);lint_scorevalidates scores before rendering.Analyze audio:
probe_audioreturns a rich JSON feature report (loudness, onsets, pitch/melody, key, tempo, rhythm, stereo, structure, silence, clipping);probe_digestgives a token-cheap text summary;loudness_timelineandbeat_gridprovide detailed time-varying data.Visual inspection:
spectrogramrenders mel spectrograms or contact sheets (optionally annotated with beats/onsets/pitch);audio_diffcan produce a signed difference heat map.Verify and compare:
audio_diffcompares two audio files in feature space and reports a verdict (byte-identical, tier-2 equivalent, or different).Convert and transcribe:
import_midiandexport_midiconvert between SMF and RON scores;transcribe_audioturns an audio file into an editable RON score draft.Learn the format:
score_referencereturns the full authoring reference—grammar, preset catalog, and assertions—so an agent can compose without external docs.
Allows importing Standard MIDI Files (format 0 or 1) into cochlea RON scores and exporting cochlea scores as Standard MIDI Files (format 1).
cochlea

A headless audio engine for agents. Write a score as data, render it offline to deterministic PCM, then listen through numbers — loudness, onsets, pitch, key, spectrograms — and assert what you heard. Compose → render → probe → verify, with no human ear (and no audio device) in the loop.

What the agent sees: the mel spectrogram of examples/scores/first_light.ron
— the score used in the example below — after render and probe. No PCM in
sight.
use cochlea_score::*;
let score = Score::new(SampleRate(48_000), Ppq(960))
.time_signature(4, 4)
.tempo(Ticks(0), Bpm(120.0))
.track("lead", Instrument::preset("saw_lead"))
.note("lead", bar(1).beat(1), Dur::quarter(), Pitch::A4, Vel(96))
.automate("lead", Param::CUTOFF_HZ,
keys![(bar(1), 400.0, ease_in_out()), (bar(3), 4_000.0)]);
let rendered = cochlea_render::render(&score)?;
rendered.write_wav("mix.wav")?;
use cochlea_verify::{VerifyExt, Tol, Ms, Cents, Db};
let report = rendered.verify(&score)
.true_peak_below(-1.0)
.pitch_matches_score("lead", Cents(10.0))
.monotone("lead", Param::CUTOFF_HZ, bar(1)..bar(3))
.silent_after(bar(5))
.run();
assert!(report.passed);Or entirely from the command line, score as RON:
cochlea render score.ron --out mix.wav --stems stems/ --verify
cochlea probe input.wav --json report.json --spectro spec.png
cochlea probe input.wav --digest --window-ms 500
cochlea probe input.mp3 --from 42.0 --to 44.5 # zoom into a window, any format
cochlea diff a.wav b.wav --tier2 --spectro delta.png
cochlea lint score.ron
cochlea spectro input.wav --out spec.png --annotate # draw beats/onsets/pitch on the image
cochlea import song.mid --out score.ron # SMF -> score, timing exact
cochlea transcribe solo.wav --out score.ron # audio -> score, the inverse of render
cochlea reference # the full score-authoring reference, generated from the live preset bankcochlea probe works on any WAV, plus FLAC (decoded bit-exact), mp3,
and ogg — still without ffmpeg, and with no score required. That's the
front door: point it at audio you didn't render, and you get the same JSON
report and spectrogram an agent uses to review its own work.
How an agent listens
compose → render → probe (JSON) → spectrogram (one vision call) → verify
compose a score as data (RON, or the Rust builder above).
render it to deterministic PCM —
cochlea render score.ron --out mix.wav.probe the mix into a compact JSON report (loudness, onsets, pitch, key, silence, clipping) —
cochlea probe mix.wav --json report.json. No image, no audio: the agent reads numbers.look, when numbers aren't enough —
cochlea spectro mix.wav --out spec.pngrenders one small PNG the agent reviews in a single vision call instead of reasoning about raw samples.verify —
cochlea render score.ron --verifyruns the score's embedded assertions and exits nonzero on failure. An agent can retry on its own, without a human confirming "yes, that sounds right."
When something in the middle of a long file needs a closer listen, every
read tool takes --from/--to. Probe just bars 17–19, or draw a
spectrogram of just the drop. The cut is frame-exact, report times are
relative to it, and source.start_ms records where it came from. That
turns the whole stack into a zoom lens instead of a whole-file-only
report.
The economics are the point here, not an afterthought. The first_light
render above is 7 seconds of 48 kHz 32-bit-float PCM, which is 2.7 MB. A
3-minute piece at the same settings runs about 66 MB. You would not hand
that to an agent as text, and reading it sample by sample is worse.
Its probe report is a few KB of JSON instead. Here is schema v5, trimmed
to the interesting fields. Note pitch.melody: the piece's bass line and
melody read back as note events, which is the read-back half of the
compose loop.
{
"schema_version": 5,
"source": { "sample_rate": 48000, "channels": 2, "duration_ms": 7035.708333333333, "start_ms": 0.0 },
"loudness": { "integrated_lufs": -22.700454879284784, "true_peak_dbtp": -15.910817022082783, "lra": 10.607660373688798 },
"onsets": { "count": 6, "times_ms": [1077.33, 2149.33, 2346.67, 3221.33, 4538.67, 5034.67] },
"pitch": { "voiced_ratio": 0.9847560975609756, "median_f0_hz": 110.00194603797897,
"melody": [ { "name": "A2", "start_ms": 0.0, "end_ms": 1045.3, "cents_off": 0.1 },
{ "name": "E2", "start_ms": 1077.3, "end_ms": 2112.0, "cents_off": 0.3 },
{ "name": "F#2", "start_ms": 2154.7, "end_ms": 3178.7, "cents_off": 0.2 },
{ "name": "E2", "start_ms": 3210.7, "end_ms": 4384.0, "cents_off": 0.3 },
{ "name": "E5", "start_ms": 4394.7, "end_ms": 5813.3, "cents_off": -0.4 } ] },
"timbre": { "mfcc_mean": [-37.64, 14.47, -4.44, 1.24, "..."], "mfcc_std": ["..."], "frames": 656 },
"key": { "tonic": "E", "mode": "major", "confidence": 0.8093960265638273 },
"tempo": { "bpm": 55.97014925373134, "confidence": 0.6633739386089712, "stability": 0.3333333333333333,
"candidates": [ { "bpm": 55.97014925373134, "salience": 0.6633739386089712 },
{ "bpm": 112.5, "salience": 0.21588204941945222 } ] },
"rhythm": { "grid_alignment": 0.8333333333333334, "grid": "straight", "offbeat_ratio": 0.4, "clear_rhythm": true },
"stereo": { "width": 0.02967719705208343, "correlation": 0.9981362354107913, "balance": -0.0016380539212361243 },
"structure": { "section_count": 1, "confidence": 0.0 },
"silence": { "trailing_ms": 2485.708333333333 },
"clipping": { "clipped_samples": 0, "true_peak_over_0dbtp": false }
}And the spectrogram is one small image. Here's the title_cue demo — a
pad whose cutoff_hz automation sweeps 250 Hz → 5000 Hz across bars 1–3:

The dark band at the top of the frame narrows as the sweep runs, letting
more high-frequency energy through over time. An agent reads that
straight off the image. The demo's Monotone(track: "pad", param: "cutoff_hz", ...) assertion checks the same thing numerically.
To get a whole piece in one image no matter how long it is, --sheet
tiles the spectrogram into a contact sheet instead of one long strip.
Two bars per tile here, via --bars-per-tile 2:

Related MCP server: Talky Talky
Reading audio without a context window
probe --digest skips JSON and prints a deterministic text summary
instead: one line per feature dimension, then a windowed timeline capped
at about 40 rows. Here's real output for the drum_groove demo — 20.8
seconds and four tracks, with the rhythm, stereo, and structure
dimensions all in one screenful:
cochlea digest: 20.755s 2ch 48000Hz
loudness: integrated=-24.06 momentary_max=-22.42 true_peak=-5.95 lra=1.61
key: A# minor (conf 0.54) pitch: voiced=23% median=63.8Hz (C2 -42.8c)
melody: 6 notes C2 C2 C2 C2 A1 A1
tempo: 110.3bpm (conf 0.79, stability 1.00) alts: 54.9bpm(0.89), 36.6bpm(0.79)
rhythm: clear=true grid_align=0.98 (straight) offbeat=0.56
stereo: width=0.07 corr=0.99 bal=-0.01
structure: 1 section
onsets: count=58 rate=2.79/s
silence: leading=0ms trailing=2545ms
clipping: clipped=0 over_0dbtp=false
timeline: window=1000ms bucket=1x rows=21
idx t(s) rms peak ons f0 flags
0 0.000-1.000 -25.55 -7.36 4 64.0 -
1 1.000-2.000 -25.61 -8.37 3 63.4 -
...Tempo and rhythm are reported as separate axes, because they change independently. A drum solo can hold a rock-steady pulse while its pattern turns unrecognizable, and that difference is exactly what an agent needs to see.
Here the tempo reads 110.3 BPM, matching the authored 110, and
stability 1.00 says the speed never moves across the piece. The alts
list surfaces the genuine half-tempo reading at 54.9 BPM — which is
actually the stronger raw peak, with the octave prior breaking the tie
toward the beat. Metrical ambiguity like that is data an agent can weigh,
rather than a coin flip hidden inside the detector.
The rhythm line then says how the hits relate to that pulse: 98% of
onsets sit on the beat-subdivision grid and 56% land on off-beat
subdivisions, so clear=true. That's an eighth-note hat groove, with its
syncopation reported as a number.
Under the pre-0.2.0 metric this same groove read clear_rhythm=false at
confidence 0.01. Layering hats, kick, snare, and pad across three
metrical levels diluted every lag's share of a mass-fraction score. The
grid-based rule asks the right question instead.
The (straight) tag is the grid hypothesis test: alignment is measured
against both straight sixteenths and eighth-note triplets, and the report
carries whichever more hits land on. A shuffle or swing take reads
grid_align=1.00 (triplet) — recognized as an aligned triplet rhythm —
instead of being force-fit to sixteenths and scored sloppy.
cochlea diff compares two files in feature space instead of byte-for-byte
— "did my change do what I meant," not "is the file bitwise equal." Real
output diffing first_light.wav against title_cue.wav:
verdict: different (duration, loudness, onsets, key)
duration a->b +1264.3 ms
loudness integrated -5.95 LU true_peak +5.70 dB lra -8.88 LU
onsets matched=0 mean_offset=- max_offset=- unmatched_a=6 unmatched_b=5
pitch delta +0.5 cents
key a=E major (conf 0.81) b=A minor (conf 0.86) changed=true
segments max_abs_rms_delta 120.99 dB at idx=7
tempo bpm -24.01 bpm stability -0.33
rhythm clear_rhythm_changed=false grid_align -0.03 grid_changed=true
timbre mfcc_distance 4.00
stereo width +0.14 correlation -0.08 balance -0.01
structure section_count +0The timbre row is an MFCC spectral-shape distance, with c0 excluded
since that's just loudness. The same instrument re-rendered measures
around 0, while swapping a sine for a saw at matched loudness measures
well above it. It's the "did the re-render keep the instrument's
character" axis, which loudness and pitch can't see.
Add --spectro delta.png and the diff also renders a signed difference
heat map: red where B is louder, blue where it's quieter, black where
nothing changed. A moved onset shows up as a blue/red vertical pair, and
a brightened sweep as a red wedge. What changed becomes visible
structure, not just a number.
Diff a render against itself, or a re-render of the same score, and the
verdict reads byte-identical instead — the determinism contract above,
checked from the outside. --tier2 turns that verdict into a gate: exit
0 for byte-identical or Tier-2-equivalent, exit 1 otherwise, so a CI job
or an agent can catch a regression without ever reading a raw sample.
Agents as MCP clients
cochlea-mcp is a stdio MCP server built on the same libraries the CLI
uses. It exposes twelve tools — render_score, probe_audio,
spectrogram, lint_score, probe_digest, loudness_timeline,
beat_grid, audio_diff, import_midi, export_midi,
transcribe_audio, and score_reference — each a thin wrapper over the
matching library call. Any MCP client gets the same compose → render →
probe → spectrogram → verify loop as tool calls, rather than as
shelled-out subprocesses.
cargo install cochlea-mcp
claude mcp add cochlea -- cochlea-mcpWhat makes it agent-native rather than a CLI in a trenchcoat:
It teaches itself.
score_referencereturns the complete authoring reference — the RON grammar, the live preset catalog with every automatable parameter (generated from the same registry that validates scores, so it can't go stale), allverify:assertions, and a worked example the test suite itself renders. An agent connected cold can compose without ever seeing this repo.It shows, not points.
spectrogramreturns the image inline as MCP image content (base64 PNG, size-capped), so a client with no filesystem access still gets the one-vision-call review;out_pathis optional.annotate: truedraws the detected beats, onsets, and pitch onto the image, andaudio_diffcan return the signed difference heat map the same way.It zooms.
probe_audioandspectrogramtakefrom_s/to_s— lean into 42.0–44.5 s of a long file the way a human replays a bar, instead of paying for whole-file analysis every call.It can be confined.
cochlea-mcp --root DIRrefuses any read or write that resolves (canonically — symlinks and..included) outsideDIR, before touching the filesystem.
Full tool schemas, arguments, and the JSON-RPC framing are in
docs/mcp.md.
Install
All nine crates are on crates.io:
cargo install cochlea # the CLI: render / probe / diff / lint / spectro / reference
cargo install cochlea-mcp # the MCP stdio server
cargo add cochlea-features # or any crate as a library dependencyOr from source: git clone https://github.com/richer-richard/cochlea && cd cochlea && cargo install --path crates/cli.
Concepts
Score IR (cochlea-score). A score is plain data: tracks, notes,
per-parameter automation, a tempo map of step changes, and an optional
master section. It serializes to RON (version: 1) and round-trips both
ways under test.
Positions read the way you'd say them — bar(3).beat(2) — and durations
are exact fractions: Dur::quarter(), "3/16", with dotted and triplet
sugar. A position that doesn't land on the tick grid is an error, not
something quietly rounded. cochlea import reads Standard MIDI Files with
timing intact: SMF ticks land on the grid verbatim, and GM programs become
labeled preset guesses.
Integer time is ground truth. Everything is ticks at 960 PPQ. BPM is
converted once, up front, to integer nanoseconds per quarter note. Turning
ticks into samples is exact rational u64/u128 arithmetic (fenestra-anim's
mul_div), applied once when events are scheduled. Nothing accumulates
floating-point seconds, nothing reads a wall clock, and a property test
holds it drift-free across 10⁹ ticks.
Synth (cochlea-synth). Eleven presets built on fundsp. Eight are
subtractive: sine, saw_lead, square_bass, chord_pad (genuinely
stereo — its detuned saws pan apart), noise_hat, pluck, kick, and
snare. Three are not: fm_bell (harmonic FM with an automatable
brightness), marimba (a modal struck bar), and organ (an additive
drawbar). There's also a reverb insert.
Each instrument declares its automatable params with a name, unit, range,
and default. Scores are validated against that registry, and the same
registry generates the authoring reference, so the docs can't drift from
the code. All noise comes from a counter-based RNG keyed on
(seed, sample_index) — random access, with no stateful generator
anywhere.
Renderer (cochlea-render). Audio is rendered in 64-sample blocks,
split at event boundaries, so note timing is sample-accurate while
automation runs at control rate (~1.3 ms at 48 kHz). Tracks render
independently, which is both the parallelism unit and where stems come
from for free. Voice allocation and oldest-note stealing are pure
functions of the schedule.
The master bus sums stems at f64 in fixed track order, then applies the
score's optional master stage: an output gain, and a brick-wall lookahead
limiter whose sample-peak ceiling holds exactly (offline, lookahead is
just a forward window maximum — no delay line). That's the tool for
hitting a LUFS target while leaving TruePeakBelow headroom. With no
master section, the mix is byte-equal to the sum of the stems, both by
definition and by test.
Features (cochlea-features). One schema-versioned JSON report,
covering:
loudness — integrated LUFS, momentary max, true peak, and LRA, via ebur128;
onsets from spectral flux, and YIN pitch with cents deviation;
a quantized melody: note events an agent can diff against what it wrote;
an MFCC timbre digest, and chroma plus Krumhansl-Schmuckler key;
tempo and rhythm as separate axes — tempo gives pulse clarity, octave alternatives, and windowed stability, while rhythm gives grid alignment (with a straight-vs-triplet hypothesis test), offbeat ratio, and a calibrated
clear_rhythm;stereo width, correlation, and balance;
structure boundaries via Foote novelty, plus silence, tail, and clipping.
On top of that: a windowed segment timeline, an LLM-sized text digest, a
feature-space diff between two files, and frame-exact windowing
(Audio::window) behind every --from/--to.
Spectro (cochlea-spectro). Mel spectrogram PNGs, with an HTK
filterbank, viridis colors, a time ruler, and bar markers. It can draw
analysis overlays on the image (beat grid, onsets, pitch), render a signed
A→B difference heat map, and tile a whole piece into a contact sheet so an
agent can review it in a single vision call.
Verify (cochlea-verify). The assertion DSL shown above. The same
assertions embed in score RON under verify:, and cochlea render score.ron --verify runs them, exiting nonzero with a machine-readable
JSON failure report.
Determinism, precisely scoped
Audio is a fold, not a map: filters and delays carry state, so per-sample purity is not the contract. The contract is three tiers:
Tier | Claim | Where |
1 | Byte-identical PCM for identical inputs | pinned CI target (x86_64-linux, pinned toolchain); same-machine repeatability tested on every platform |
2 | Feature tolerances across platforms | integrated LUFS ±0.1 LU, onsets ±2 ms, pitch ±5 cents |
3 | Spectrogram sentinels | image diff with per-pixel tolerance |
Tier 1 is bought with a specific set of choices:
libmfor every transcendental in a DSP path. The std float methods are banned by clippy config, not by convention.No fast-math and no implicit FMA.
mul_addis banned too.Denormals are honored everywhere. Flushing them is a realtime performance hack, and x86 and aarch64 can't even do it uniformly. We render offline and eat the rare slow tail.
Fixed summation order, and an f64 master bus.
Voices tick sample by sample. fundsp's SIMD block path provably diverges from its scalar path, so it's banned.
Analysis FFTs use
FftPlannerScalar, which does no runtime CPU dispatch.
The full audit trail — per fundsp node family, ebur128's internals,
rustfft's dispatch — lives in
docs/determinism.md.
Feature accuracy (synthesized ground truth, 48 kHz)
Feature | Fixture | Measured |
Pitch (YIN) | 440 Hz sine | 440.017 Hz — 0.07 cents off A4 |
Onsets | click track, 0.5 s grid | ≤ 4 ms offset (frame-center convention, 256-sample hop) |
Key | C major triad | C major, confidence 0.79 |
Key | I–IV–V–I pad progression (demo) | C major |
Loudness | −18 dBFS-peak 997 Hz sine | −21.0 LUFS (≈ −3 LU sine crest factor — physics, not error) |
Silence/tail | 1 s tone + 1 s silence | trailing 960 ms, last-audible within one RMS window |
Clipping | driven square, clamped | counted; true-peak-over-0 flagged |
Tempo | 120/90 BPM click track | ±1 BPM, pulse clarity 0.96, |
Tempo |
| 110.29 BPM (Δ 0.01), pulse clarity 0.79, stability 1.0; the 55 BPM half-tempo surfaces as a candidate (salience 0.89) instead of a hidden coin flip |
Rhythm | quarter-note clicks vs straight eighths | grid alignment 1.0 for both; offbeat ratio 0.0 vs 0.49 — syncopation as a number |
Rhythm robustness | click track, ±5/±10 ms human timing jitter | BPM exact, alignment 1.0, |
Rhythm robustness | click track, ±20/±30 ms jitter | BPM octave-folds to the half tempo (smeared beats make the two-beat lag as clear as one) — but alignment stays 1.0 and |
Rhythm robustness | one dropped + one extra hit in 22 | BPM and |
Rhythm false-positive guard | uniformly random onset times | alignment 0.57 (vs the 0.7 clear-rhythm bar), pulse clarity 0.10 — rejected on two independent gates |
Tempo vs rhythm | pattern change at constant speed (quarters → dense eighths) | stability stays ≥ 0.75 — the drum-solo case: the rhythm changed, the speed didn't |
Tempo vs rhythm | real speed change (100 → 140 BPM mid-buffer) | stability drops ≤ 0.75 — the axis that separates the two |
Swing | shuffle (beats + upbeats at 2/3 beat) |
|
Melody | three authored tones (A4 C5 E5) | reads back as |
Timbre | sine vs saw, same note, same level | MFCC distance separates decisively; identical input measures exactly 0 |
Lossy decode | the same tone via WAV, mp3, and ogg | pitch agrees within 5 cents across codecs |
Structure | two 8 s segments, distinct timbre | boundary within 1.5 s of the true 8.0 s cut |
Structure | three 8 s segments (A/B/A) | boundaries within 1.5 s of the true 8.0 s and 16.0 s cuts |
ffmpeg-free by design
cochlea reads WAV, FLAC, mp3, and ogg/vorbis using hound and
symphonia, both pure Rust. It writes plain WAV, and renders PNGs on the
CPU with rustfft, a hand-rolled mel filterbank, a viridis LUT, and
image.
There are no subprocess calls, no system codecs, no GPU, and no audio
device. The whole pipeline is a pure Rust dependency graph, and CI blocks
GUI, GPU, and device crates from ever entering Cargo.lock (deny.toml).
Decoding comes with two different promises, and it's worth being clear about which is which. WAV and FLAC decode bit-exact — FLAC is lossless by spec, and it's checked against WAV twins in-tree. mp3 and ogg are analysis input only: reproducible for a given build, but a lossy codec already threw the original samples away, so there's no exactness claim left to make.
Assertion cookbook
use cochlea_verify::{VerifyExt, Tol, Ms, Cents, Db};
rendered.verify(&score)
// Mix-level loudness and headroom:
.integrated_lufs(-14.0, Tol(0.5)) // streaming-loudness target
.true_peak_below(-1.0) // intersample-safe headroom
// Timing: did the hit land where the score says?
.onset_at("drums", bar(17).beat(1), Ms(5.0))
// Intonation: does every note read as written? (monophonic tracks)
.pitch_matches_score("lead", Cents(10.0))
// Was the sweep *written*? (authored curve, block-rate — a score lint)
.monotone("lead", Param::CUTOFF_HZ, bar(1)..bar(3))
// ...and did it audibly *happen*? (rendered stem's spectral centroid)
.brightness_rises("lead", bar(1)..bar(3), 1.3)
// Do the hits land on the detected beat grid?
.grid_alignment_at_least(0.9)
// Click detection away from note boundaries:
.no_discontinuity("lead", Db(40.0))
// Does the piece actually end?
.silent_after(bar(64))
.run();The same assertions embed in score RON:
verify: [
IntegratedLufs(target: -14.0, tol: 0.5),
TruePeakBelow(dbtp: -1.0),
OnsetAt(track: "drums", at: (17, 1), tol_ms: 5.0),
PitchMatchesScore(track: "lead", tol_cents: 10.0),
Monotone(track: "pad", param: "cutoff_hz", from: (1, 1), to: (3, 1), direction: Rising),
BrightnessRises(track: "pad", from: (1, 1), to: (3, 1), min_ratio: 1.3),
NoDiscontinuity(track: "lead", db: 40.0),
SilentAfter(at: (64, 1)),
TempoIs(bpm: 110.0, tol_bpm: 2.0),
HasClearRhythm(expected: true),
GridAlignmentAtLeast(min: 0.9),
]cochlea render score.ron --verify runs them; failures come back as JSON
({"passed": false, "checks": [...]}) and a nonzero exit.
To actually hit a loudness target rather than just assert it, give the score a master bus — gain to push, a limiter to hold the ceiling:
master: Master(
gain_db: 4.0,
limiter: Limiter(ceiling_db: -2.0), // sample-peak ceiling holds exactly
),
verify: [
IntegratedLufs(target: -14.0, tol: 0.5),
TruePeakBelow(dbtp: -1.0), // ~1 dB headroom over the ceiling: true peak is inter-sample
]Four worked demos live in demos/:
metronome— sample-exact scheduling and onset tolerances.chord_pad— harmony reads back as written.title_cue— a four-bar cinematic sting that asserts a LUFS target, a monotone filter sweep, click-freedom, and silence after the fade.drum_groove— a 110 BPM eight-bar groove on the realkickandsnarepatches, hats panned right and snare left. It asserts detected tempo,HasClearRhythm(true)with grid alignment ≥ 0.9, stereo width, loudness range, and section count.
drum_groove is also the fixture that motivated splitting tempo from
rhythm: the old single confidence metric read it as rhythm-less at 0.01,
despite getting the BPM spot on.
Workspace
crates/
score # IR: ticks, tempo map, bar/beat math, notes, automation, master, RON form, MIDI import
synth # Patch trait over fundsp, eleven presets, param registry, counter RNG
render # block engine, voices, stems, f64 master sum + gain/limiter, WAV out
features # LUFS/true peak, onsets, pitch+melody, timbre, chroma/key, tempo, rhythm, stereo, structure
decode # WAV + FLAC (bit-exact) + mp3 + ogg (analysis) -> Audio, pure Rust
spectro # mel spectrogram -> PNG, overlays, diff heat maps, contact sheets
verify # assertion DSL + RON-embeddable specs + JSON reports
cli # the `cochlea` binary
mcp # MCP stdio server (agents call compose/render/probe/verify as tools)features and spectro depend on neither score nor synth — enforced
in CI — which is why probe works on arbitrary audio files with no score
in sight.
License
MIT OR Apache-2.0, at your option.
Available Tools
12 toolsaudio_diffA
Compare two audio files (WAV, FLAC, mp3, or ogg) in feature space (loudness, onsets, pitch, key, timbre distance, per-segment RMS) rather than byte-for-byte, and report a verdict: byte-identical, tier-2 equivalent (within this workspace's cross-platform tolerances), or different (naming which dimensions diverge). Set spectrogram=true to also get a signed A→B difference heat map inline (red = louder in B, blue = quieter, black = unchanged) — 'what changed' as visible structure. Use this to check whether a re-render, edit, or platform change actually altered the audio in a way that matters — a different verdict is a normal, successful answer, not a tool failure.
Inputs: audio_path_a (required), audio_path_b (required), json, window_ms, +1 more.
beat_gridA
The full beat grid of a whole WAV, FLAC, mp3, or ogg file, as JSON: every detected beat time (ms), the estimated downbeats, the tempo with its octave-alternative candidates, and a windowed stability score. This is the detail the compact tempo summary inside probe_audio drops — that one keeps only the count and mean interval to stay small. Use it to line events up to the beat, see where the downbeats fall, or weigh a half/double-tempo alternative. All times are measured from the start of the file.
Inputs: audio_path (required).
export_midiA
Convert a cochlea RON score into a Standard MIDI File (format 1) — the inverse of import_midi. Timing exports exactly (score ticks become SMF ticks, the tempo map and time signature carry over); instruments become rough General MIDI program labels, since a synth preset isn't a GM instrument. Use this to hand a composed score to a DAW or notation tool, or to round-trip through external MIDI editing.
Inputs: out_path (required), score_path (required).
import_midiA
Convert a Standard MIDI File (format 0 or 1) into a cochlea RON score. Timing imports exactly (SMF ticks become score ticks, tempo events become the tempo map); General MIDI programs map to rough preset families and channel-10 percussion to kick/snare/hat tracks — every mapping guess comes back in the response so you can re-voice the score afterwards. Use this to bring existing musical material into the compose→render→probe loop.
Inputs: out_path (required), midi_path (required), sample_rate.
lint_scoreA
Statically validate a RON score against the instrument/preset catalog — catches unknown instruments or inserts, empty tracks, and other semantic problems without rendering any audio. Use this before render_score to fail fast on authoring mistakes.
Inputs: score_path (required).
loudness_timelineA
The loudness-over-time curve of a whole WAV, FLAC, mp3, or ogg file, as JSON: momentary (400 ms) and short-term (3 s) LUFS sampled every ~100 ms. This is the dynamics view the single integrated-LUFS / LRA summary in probe_audio can't give — where a mix gets loud, where a gate opens, how the level moves through a build or a chorus. Use it to check whether a change actually moved the dynamics, or to find the loudest moment. Every point's time is measured from the start of the file. (For a windowed analysis with an anchored offset, use probe_audio with from_s/to_s.)
Inputs: audio_path (required), hop_ms.
probe_audioA
Extract the full feature report (integrated LUFS/true peak/LRA, onsets, YIN pitch track plus quantized melody notes, MFCC timbre digest, chroma/key, a chord timeline and per-section key (harmony), tempo with octave-alternative candidates and stability, rhythm with grid alignment and a straight-vs-triplet grid call, stereo image, structural sections, silence, clipping — schema v5) from any WAV, FLAC, mp3, or ogg file, no score needed. Use this to 'listen' to audio through numbers: check loudness targets, confirm onset timing or tempo, read back the melody you composed, or see the chord progression. Pass from_s/to_s to zoom into a time window instead of probing the whole file (report times are then relative to the cut; source.start_ms anchors them).
Inputs: audio_path (required), to_s, from_s.
probe_digestA
The token-cheap way to listen to a WAV or FLAC file: a ~40-line deterministic text digest (duration, loudness, onsets, pitch, key, and a windowed timeline table) instead of a full JSON report or raw PCM. Reach for this first when you just need a sense of what's in a file, and only fall back to probe_audio when you need exact numbers to assert against.
Inputs: audio_path (required), window_ms.
render_scoreA
Render a cochlea RON score (the declarative tick/track/note/automation IR) to a deterministic WAV mix. Use this to turn a composed score into audible PCM before probing or inspecting it. Set verify=true to also run the score's embedded verify: assertions and get the pass/fail report back in the same call.
Inputs: out_path (required), score_path (required), bits, verify, +1 more.
score_referenceA
The score-authoring reference: the complete RON score grammar (tracks, notes, durations, automation, easing), the live instrument-preset catalog with every automatable parameter and range, all embeddable verify: assertions, and a worked example. Call this FIRST when composing — everything render_score accepts is documented here; do not guess the format.
Inputs: no named parameters.
spectrogramA
Render a mel spectrogram (or a tiled contact sheet covering the whole file) from a WAV, FLAC, mp3, or ogg file, for visual inspection of harmonic content, sweeps, or silence. The image comes back inline as MCP image content (base64 PNG) whenever it fits the size cap, so you can look at it directly without filesystem access; pass out_path to also (or instead) write the PNG to disk. Set annotate=true to draw what the analyzers heard onto the image — detected beats (orange ticks, top), onsets (cyan ticks, bottom), pitch segments (magenta lines) — and from_s/to_s to zoom into a time window. Use this when a numeric probe report isn't enough and you want to look at the audio.
Inputs: audio_path (required), to_s, sheet, from_s, +3 more.
transcribe_audioA
Transcribe a WAV, FLAC, mp3, or ogg file into an editable cochlea RON score — the inverse of render_score, and the arrow that closes the compose loop: hear a sketch, get score back, revise it, render it again. Pitch-tracks the melody, reads its timing against a tempo (detected from the audio unless you pass bpm), quantizes to a note grid, and estimates each note's velocity from its peak level. Deliberately monophonic: it hears one line, so chords, drums, and dense mixes come back as whichever line the tracker locked onto. Every assumption — the tempo, the grid, the preset, clamped or dropped notes — comes back in the response, so treat the result as a draft to re-voice rather than a faithful score.
Inputs: out_path (required), audio_path (required), bpm, ppq, +3 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 12 tools
Each tool targets a distinct artifact or operation: score authoring/validation/rendering, MIDI conversion, full/digest/visual audio analysis, and comparison. The analysis tools do overlap in feature space, but the descriptions explicitly differentiate probe_digest, loudness_timeline, beat_grid, and spectrogram from probe_audio, so an agent can reliably pick between them.
The majority of tools follow a clear snake_case two-word pattern (lint_score, render_score, probe_audio, import_midi, transcribe_audio), and the remaining names are still descriptive and readable. Minor deviations exist: spectrogram is a single word, and several output-oriented names like audio_diff, loudness_timeline, and beat_grid are noun phrases rather than verb_noun commands.
Twelve tools is a well-scoped count for a music/audio composition and analysis server. Each tool adds a distinct capability, and there is no obvious redundancy or filler; the set feels intentionally sized for the compose-render-probe-revise workflow.
The server covers the full workflow: score reference, validation, rendering, multiple analysis modalities, audio diffing, MIDI import/export, and audio-to-score transcription. The loop from composing a score to hearing, inspecting, comparing, and revising it is fully closed, with no critical dead ends for the stated purpose.
Maintenance
Related MCP Connectors
Generate game-ready 3D models, textures, and audio from natural language, over MCP.
Audio for your agent: transcribe, speak, translate, summarise, plus sound effects and music.
MCP server for Producer/Riffusion AI music generation
CC0 sound effects API for AI agents — search, preview, and download via MCP.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceAn MCP server that gives AI agents the ability to listen to and understand music/audio files, enabling semantic analysis, stem separation, lyrics transcription, and signal processing via tool calls.1MIT
- AlicenseNot gradedqualityDmaintenanceA comprehensive audio MCP server that enables AI agents to generate speech, transcribe audio, clone voices, analyze speech quality, design soundscapes, and manage audio assets through a standardized interface.2MIT
- AlicenseNot gradedqualityDmaintenanceProvides audio inspection, conversion, processing, and generation capabilities via SoX, enabling AI agents to 'hear' and manipulate audio files through structured JSON interfaces.MIT
- AlicenseNot gradedqualityDmaintenanceExposes audio processing tools (inspect, normalize, trim, fade, gain, filter, speed, reverse) as MCP tools, enabling Claude to manipulate WAV files through natural language commands.MIT