{
  "markdown": "# cochlea\n\n[![CI](https://github.com/richer-richard/cochlea/actions/workflows/ci.yml/badge.svg)](https://github.com/richer-richard/cochlea/actions/workflows/ci.yml)\n[![docs](https://img.shields.io/badge/docs-book-blue)](https://richer-richard.github.io/cochlea/)\n\n**A headless audio engine for agents.** Write a score as data, render it\noffline to deterministic PCM, then *listen through numbers* — loudness,\nonsets, pitch, key, spectrograms — and assert what you heard. Compose →\nrender → probe → verify, with no human ear (and no audio device) in the\nloop.\n\n![Mel spectrogram of first_light.ron: six note onsets followed by a reverb tail decaying to silence](docs/assets/first_light_spectro.png)\n\n*What the agent sees: the mel spectrogram of `examples/scores/first_light.ron`\n— the score used in the example below — after render and probe. No PCM in\nsight.*\n\n```rust\nuse cochlea_score::*;\n\nlet score = Score::new(SampleRate(48_000), Ppq(960))\n    .time_signature(4, 4)\n    .tempo(Ticks(0), Bpm(120.0))\n    .track(\"lead\", Instrument::preset(\"saw_lead\"))\n    .note(\"lead\", bar(1).beat(1), Dur::quarter(), Pitch::A4, Vel(96))\n    .automate(\"lead\", Param::CUTOFF_HZ,\n        keys![(bar(1), 400.0, ease_in_out()), (bar(3), 4_000.0)]);\n\nlet rendered = cochlea_render::render(&score)?;\nrendered.write_wav(\"mix.wav\")?;\n\nuse cochlea_verify::{VerifyExt, Tol, Ms, Cents, Db};\nlet report = rendered.verify(&score)\n    .true_peak_below(-1.0)\n    .pitch_matches_score(\"lead\", Cents(10.0))\n    .monotone(\"lead\", Param::CUTOFF_HZ, bar(1)..bar(3))\n    .silent_after(bar(5))\n    .run();\nassert!(report.passed);\n```\n\nOr entirely from the command line, score as RON:\n\n```\ncochlea render score.ron --out mix.wav --stems stems/ --verify\ncochlea probe input.wav --json report.json --spectro spec.png\ncochlea probe input.wav --digest --window-ms 500\ncochlea probe input.mp3 --from 42.0 --to 44.5      # zoom into a window, any format\ncochlea diff a.wav b.wav --tier2 --spectro delta.png\ncochlea lint score.ron\ncochlea spectro input.wav --out spec.png --annotate  # draw beats/onsets/pitch on the image\ncochlea import song.mid --out score.ron              # SMF -> score, timing exact\ncochlea transcribe solo.wav --out score.ron          # audio -> score, the inverse of render\ncochlea reference    # the full score-authoring reference, generated from the live preset bank\n```\n\n`cochlea probe` works on **any** WAV, plus FLAC (decoded bit-exact), mp3,\nand ogg — still without ffmpeg, and with no score required. That's the\nfront door: point it at audio you didn't render, and you get the same JSON\nreport and spectrogram an agent uses to review its own work.\n\n## How an agent listens\n\ncompose → render → probe (JSON) → spectrogram (one vision call) → verify\n\n1. **compose** a score as data (RON, or the Rust builder above).\n2. **render** it to deterministic PCM — `cochlea render score.ron --out mix.wav`.\n3. **probe** the mix into a compact JSON report (loudness, onsets, pitch,\n   key, silence, clipping) — `cochlea probe mix.wav --json report.json`.\n   No image, no audio: the agent reads numbers.\n4. **look**, when numbers aren't enough — `cochlea spectro mix.wav --out\n   spec.png` renders one small PNG the agent reviews in a single vision\n   call instead of reasoning about raw samples.\n5. **verify** — `cochlea render score.ron --verify` runs the score's\n   embedded assertions and exits nonzero on failure. An agent can retry on\n   its own, without a human confirming \"yes, that sounds right.\"\n\nWhen something in the middle of a long file needs a closer listen, every\nread tool takes `--from/--to`. Probe just bars 17–19, or draw a\nspectrogram of just the drop. The cut is frame-exact, report times are\nrelative to it, and `source.start_ms` records where it came from. That\nturns the whole stack into a zoom lens instead of a whole-file-only\nreport.\n\nThe economics are the point here, not an afterthought. The `first_light`\nrender above is 7 seconds of 48 kHz 32-bit-float PCM, which is 2.7 MB. A\n3-minute piece at the same settings runs about 66 MB. You would not hand\nthat to an agent as text, and reading it sample by sample is worse.\n\nIts probe report is a few KB of JSON instead. Here is schema v5, trimmed\nto the interesting fields. Note `pitch.melody`: the piece's bass line and\nmelody read back as *note events*, which is the read-back half of the\ncompose loop.\n\n```json\n{\n  \"schema_version\": 5,\n  \"source\": { \"sample_rate\": 48000, \"channels\": 2, \"duration_ms\": 7035.708333333333, \"start_ms\": 0.0 },\n  \"loudness\": { \"integrated_lufs\": -22.700454879284784, \"true_peak_dbtp\": -15.910817022082783, \"lra\": 10.607660373688798 },\n  \"onsets\": { \"count\": 6, \"times_ms\": [1077.33, 2149.33, 2346.67, 3221.33, 4538.67, 5034.67] },\n  \"pitch\": { \"voiced_ratio\": 0.9847560975609756, \"median_f0_hz\": 110.00194603797897,\n             \"melody\": [ { \"name\": \"A2\", \"start_ms\": 0.0, \"end_ms\": 1045.3, \"cents_off\": 0.1 },\n                         { \"name\": \"E2\", \"start_ms\": 1077.3, \"end_ms\": 2112.0, \"cents_off\": 0.3 },\n                         { \"name\": \"F#2\", \"start_ms\": 2154.7, \"end_ms\": 3178.7, \"cents_off\": 0.2 },\n                         { \"name\": \"E2\", \"start_ms\": 3210.7, \"end_ms\": 4384.0, \"cents_off\": 0.3 },\n                         { \"name\": \"E5\", \"start_ms\": 4394.7, \"end_ms\": 5813.3, \"cents_off\": -0.4 } ] },\n  \"timbre\": { \"mfcc_mean\": [-37.64, 14.47, -4.44, 1.24, \"...\"], \"mfcc_std\": [\"...\"], \"frames\": 656 },\n  \"key\": { \"tonic\": \"E\", \"mode\": \"major\", \"confidence\": 0.8093960265638273 },\n  \"tempo\": { \"bpm\": 55.97014925373134, \"confidence\": 0.6633739386089712, \"stability\": 0.3333333333333333,\n             \"candidates\": [ { \"bpm\": 55.97014925373134, \"salience\": 0.6633739386089712 },\n                             { \"bpm\": 112.5, \"salience\": 0.21588204941945222 } ] },\n  \"rhythm\": { \"grid_alignment\": 0.8333333333333334, \"grid\": \"straight\", \"offbeat_ratio\": 0.4, \"clear_rhythm\": true },\n  \"stereo\": { \"width\": 0.02967719705208343, \"correlation\": 0.9981362354107913, \"balance\": -0.0016380539212361243 },\n  \"structure\": { \"section_count\": 1, \"confidence\": 0.0 },\n  \"silence\": { \"trailing_ms\": 2485.708333333333 },\n  \"clipping\": { \"clipped_samples\": 0, \"true_peak_over_0dbtp\": false }\n}\n```\n\nAnd the spectrogram is one small image. Here's the `title_cue` demo — a\npad whose `cutoff_hz` automation sweeps 250 Hz → 5000 Hz across bars 1–3:\n\n![Mel spectrogram of the title_cue demo: the quiet band at the top of the frame narrows across the first two bars as the filter sweep lets more high-frequency energy through](docs/assets/title_cue_spectro.png)\n\n*The dark band at the top of the frame narrows as the sweep runs, letting\nmore high-frequency energy through over time. An agent reads that\nstraight off the image. The demo's `Monotone(track: \"pad\", param:\n\"cutoff_hz\", ...)` assertion checks the same thing numerically.*\n\nTo get a whole piece in one image no matter how long it is, `--sheet`\ntiles the spectrogram into a contact sheet instead of one long strip.\nTwo bars per tile here, via `--bars-per-tile 2`:\n\n![Contact-sheet spectrogram of first_light.ron tiled two bars per row](docs/assets/first_light_sheet.png)\n\n## Reading audio without a context window\n\n`probe --digest` skips JSON and prints a deterministic text summary\ninstead: one line per feature dimension, then a windowed timeline capped\nat about 40 rows. Here's real output for the `drum_groove` demo — 20.8\nseconds and four tracks, with the rhythm, stereo, and structure\ndimensions all in one screenful:\n\n```\ncochlea digest: 20.755s  2ch  48000Hz\nloudness: integrated=-24.06  momentary_max=-22.42  true_peak=-5.95  lra=1.61\nkey: A# minor (conf 0.54)  pitch: voiced=23%  median=63.8Hz (C2 -42.8c)\nmelody: 6 notes  C2 C2 C2 C2 A1 A1\ntempo: 110.3bpm (conf 0.79, stability 1.00)  alts: 54.9bpm(0.89), 36.6bpm(0.79)\nrhythm: clear=true  grid_align=0.98 (straight)  offbeat=0.56\nstereo: width=0.07 corr=0.99 bal=-0.01\nstructure: 1 section\nonsets: count=58  rate=2.79/s\nsilence: leading=0ms  trailing=2545ms\nclipping: clipped=0  over_0dbtp=false\ntimeline: window=1000ms  bucket=1x  rows=21\n   idx        t(s)     rms   peak  ons     f0  flags\n     0   0.000-1.000   -25.55  -7.36    4    64.0  -\n     1   1.000-2.000   -25.61  -8.37    3    63.4  -\n     ...\n```\n\nTempo and rhythm are reported as *separate axes*, because they change\nindependently. A drum solo can hold a rock-steady pulse while its pattern\nturns unrecognizable, and that difference is exactly what an agent needs\nto see.\n\nHere the tempo reads 110.3 BPM, matching the authored 110, and\n`stability 1.00` says the speed never moves across the piece. The `alts`\nlist surfaces the genuine half-tempo reading at 54.9 BPM — which is\nactually the stronger raw peak, with the octave prior breaking the tie\ntoward the beat. Metrical ambiguity like that is data an agent can weigh,\nrather than a coin flip hidden inside the detector.\n\nThe `rhythm` line then says how the *hits* relate to that pulse: 98% of\nonsets sit on the beat-subdivision grid and 56% land on off-beat\nsubdivisions, so `clear=true`. That's an eighth-note hat groove, with its\nsyncopation reported as a number.\n\nUnder the pre-0.2.0 metric this same groove read `clear_rhythm=false` at\nconfidence 0.01. Layering hats, kick, snare, and pad across three\nmetrical levels diluted every lag's share of a mass-fraction score. The\ngrid-based rule asks the right question instead.\n\nThe `(straight)` tag is the grid *hypothesis test*: alignment is measured\nagainst both straight sixteenths and eighth-note triplets, and the report\ncarries whichever more hits land on. A shuffle or swing take reads\n`grid_align=1.00 (triplet)` — recognized as an aligned triplet rhythm —\ninstead of being force-fit to sixteenths and scored sloppy.\n\n`cochlea diff` compares two files in feature space instead of byte-for-byte\n— \"did my change do what I meant,\" not \"is the file bitwise equal.\" Real\noutput diffing `first_light.wav` against `title_cue.wav`:\n\n```\nverdict: different (duration, loudness, onsets, key)\nduration     a->b +1264.3 ms\nloudness     integrated -5.95 LU  true_peak +5.70 dB  lra -8.88 LU\nonsets       matched=0  mean_offset=-  max_offset=-  unmatched_a=6  unmatched_b=5\npitch        delta +0.5 cents\nkey          a=E major (conf 0.81)  b=A minor (conf 0.86)  changed=true\nsegments     max_abs_rms_delta 120.99 dB at idx=7\ntempo        bpm -24.01 bpm  stability -0.33\nrhythm       clear_rhythm_changed=false  grid_align -0.03  grid_changed=true\ntimbre       mfcc_distance 4.00\nstereo       width +0.14  correlation -0.08  balance -0.01\nstructure    section_count +0\n```\n\nThe `timbre` row is an MFCC spectral-shape distance, with `c0` excluded\nsince that's just loudness. The same instrument re-rendered measures\naround 0, while swapping a sine for a saw at matched loudness measures\nwell above it. It's the \"did the re-render keep the instrument's\ncharacter\" axis, which loudness and pitch can't see.\n\nAdd `--spectro delta.png` and the diff also renders a **signed difference\nheat map**: red where B is louder, blue where it's quieter, black where\nnothing changed. A moved onset shows up as a blue/red vertical pair, and\na brightened sweep as a red wedge. What changed becomes visible\nstructure, not just a number.\n\nDiff a render against itself, or a re-render of the same score, and the\nverdict reads `byte-identical` instead — the determinism contract above,\nchecked from the outside. `--tier2` turns that verdict into a gate: exit\n0 for byte-identical or Tier-2-equivalent, exit 1 otherwise, so a CI job\nor an agent can catch a regression without ever reading a raw sample.\n\n## Agents as MCP clients\n\n`cochlea-mcp` is a stdio MCP server built on the same libraries the CLI\nuses. It exposes twelve tools — `render_score`, `probe_audio`,\n`spectrogram`, `lint_score`, `probe_digest`, `loudness_timeline`,\n`beat_grid`, `audio_diff`, `import_midi`, `export_midi`,\n`transcribe_audio`, and `score_reference` — each a thin wrapper over the\nmatching library call. Any MCP client gets the same compose → render →\nprobe → spectrogram → verify loop as tool calls, rather than as\nshelled-out subprocesses.\n\n```\ncargo install cochlea-mcp\nclaude mcp add cochlea -- cochlea-mcp\n```\n\nWhat makes it agent-native rather than a CLI in a trenchcoat:\n\n- **It teaches itself.** `score_reference` returns the complete authoring\n  reference — the RON grammar, the live preset catalog with every\n  automatable parameter (generated from the same registry that validates\n  scores, so it can't go stale), all `verify:` assertions, and a worked\n  example the test suite itself renders. An agent connected cold can\n  compose without ever seeing this repo.\n- **It shows, not points.** `spectrogram` returns the image *inline* as\n  MCP image content (base64 PNG, size-capped), so a client with no\n  filesystem access still gets the one-vision-call review; `out_path` is\n  optional. `annotate: true` draws the detected beats, onsets, and pitch\n  onto the image, and `audio_diff` can return the signed difference heat\n  map the same way.\n- **It zooms.** `probe_audio` and `spectrogram` take `from_s`/`to_s` —\n  lean into 42.0–44.5 s of a long file the way a human replays a bar,\n  instead of paying for whole-file analysis every call.\n- **It can be confined.** `cochlea-mcp --root DIR` refuses any read or\n  write that resolves (canonically — symlinks and `..` included) outside\n  `DIR`, before touching the filesystem.\n\nFull tool schemas, arguments, and the JSON-RPC framing are in\n[`docs/mcp.md`](docs/mcp.md).\n\n## Install\n\nAll nine crates are on [crates.io](https://crates.io/crates/cochlea):\n\n```\ncargo install cochlea        # the CLI: render / probe / diff / lint / spectro / reference\ncargo install cochlea-mcp    # the MCP stdio server\ncargo add cochlea-features   # or any crate as a library dependency\n```\n\nOr from source: `git clone https://github.com/richer-richard/cochlea &&\ncd cochlea && cargo install --path crates/cli`.\n\n## Concepts\n\n**Score IR** (`cochlea-score`). A score is plain data: tracks, notes,\nper-parameter automation, a tempo map of step changes, and an optional\nmaster section. It serializes to RON (`version: 1`) and round-trips both\nways under test.\n\nPositions read the way you'd say them — `bar(3).beat(2)` — and durations\nare exact fractions: `Dur::quarter()`, `\"3/16\"`, with dotted and triplet\nsugar. A position that doesn't land on the tick grid is an error, not\nsomething quietly rounded. `cochlea import` reads Standard MIDI Files with\ntiming intact: SMF ticks land on the grid verbatim, and GM programs become\nlabeled preset guesses.\n\n**Integer time is ground truth.** Everything is ticks at 960 PPQ. BPM is\nconverted once, up front, to integer nanoseconds per quarter note. Turning\nticks into samples is exact rational u64/u128 arithmetic (`fenestra-anim`'s\n`mul_div`), applied once when events are scheduled. Nothing accumulates\nfloating-point seconds, nothing reads a wall clock, and a property test\nholds it drift-free across 10⁹ ticks.\n\n**Synth** (`cochlea-synth`). Eleven presets built on [fundsp]. Eight are\nsubtractive: `sine`, `saw_lead`, `square_bass`, `chord_pad` (genuinely\nstereo — its detuned saws pan apart), `noise_hat`, `pluck`, `kick`, and\n`snare`. Three are not: `fm_bell` (harmonic FM with an automatable\n`brightness`), `marimba` (a modal struck bar), and `organ` (an additive\ndrawbar). There's also a `reverb` insert.\n\nEach instrument declares its automatable params with a name, unit, range,\nand default. Scores are validated against that registry, and the same\nregistry generates the authoring reference, so the docs can't drift from\nthe code. All noise comes from a counter-based RNG keyed on\n`(seed, sample_index)` — random access, with no stateful generator\nanywhere.\n\n**Renderer** (`cochlea-render`). Audio is rendered in 64-sample blocks,\nsplit at event boundaries, so note timing is sample-accurate while\nautomation runs at control rate (~1.3 ms at 48 kHz). Tracks render\nindependently, which is both the parallelism unit and where stems come\nfrom for free. Voice allocation and oldest-note stealing are pure\nfunctions of the schedule.\n\nThe master bus sums stems at f64 in fixed track order, then applies the\nscore's optional master stage: an output gain, and a brick-wall lookahead\nlimiter whose sample-peak ceiling holds exactly (offline, lookahead is\njust a forward window maximum — no delay line). That's the tool for\nhitting a LUFS target while leaving `TruePeakBelow` headroom. With no\nmaster section, the mix is byte-equal to the sum of the stems, both by\ndefinition and by test.\n\n**Features** (`cochlea-features`). One schema-versioned JSON report,\ncovering:\n\n- loudness — integrated LUFS, momentary max, true peak, and LRA, via\n  [ebur128];\n- onsets from spectral flux, and YIN pitch with cents deviation;\n- a quantized **melody**: note events an agent can diff against what it\n  wrote;\n- an MFCC **timbre** digest, and chroma plus Krumhansl-Schmuckler key;\n- tempo and rhythm as separate axes — tempo gives pulse clarity, octave\n  alternatives, and windowed stability, while rhythm gives grid alignment\n  (with a straight-vs-triplet hypothesis test), offbeat ratio, and a\n  calibrated `clear_rhythm`;\n- stereo width, correlation, and balance;\n- structure boundaries via Foote novelty, plus silence, tail, and\n  clipping.\n\nOn top of that: a windowed segment timeline, an LLM-sized text digest, a\nfeature-space diff between two files, and frame-exact windowing\n(`Audio::window`) behind every `--from/--to`.\n\n**Spectro** (`cochlea-spectro`). Mel spectrogram PNGs, with an HTK\nfilterbank, viridis colors, a time ruler, and bar markers. It can draw\nanalysis overlays on the image (beat grid, onsets, pitch), render a signed\nA→B difference heat map, and tile a whole piece into a contact sheet so an\nagent can review it in a single vision call.\n\n**Verify** (`cochlea-verify`). The assertion DSL shown above. The same\nassertions embed in score RON under `verify:`, and `cochlea render\nscore.ron --verify` runs them, exiting nonzero with a machine-readable\nJSON failure report.\n\n## Determinism, precisely scoped\n\nAudio is a fold, not a map: filters and delays carry state, so per-sample\npurity is not the contract. The contract is three tiers:\n\n| Tier | Claim | Where |\n|------|-------|-------|\n| 1 | **Byte-identical PCM** for identical inputs | pinned CI target (x86_64-linux, pinned toolchain); same-machine repeatability tested on every platform |\n| 2 | **Feature tolerances** across platforms | integrated LUFS ±0.1 LU, onsets ±2 ms, pitch ±5 cents |\n| 3 | **Spectrogram sentinels** | image diff with per-pixel tolerance |\n\nTier 1 is bought with a specific set of choices:\n\n- **`libm` for every transcendental in a DSP path.** The std float methods\n  are banned by clippy config, not by convention.\n- **No fast-math and no implicit FMA.** `mul_add` is banned too.\n- **Denormals are honored everywhere.** Flushing them is a realtime\n  performance hack, and x86 and aarch64 can't even do it uniformly. We\n  render offline and eat the rare slow tail.\n- **Fixed summation order, and an f64 master bus.**\n- **Voices tick sample by sample.** fundsp's SIMD block path provably\n  diverges from its scalar path, so it's banned.\n- **Analysis FFTs use `FftPlannerScalar`**, which does no runtime CPU\n  dispatch.\n\nThe full audit trail — per fundsp node family, ebur128's internals,\nrustfft's dispatch — lives in\n[`docs/determinism.md`](docs/determinism.md).\n\n## Feature accuracy (synthesized ground truth, 48 kHz)\n\n| Feature | Fixture | Measured |\n|---------|---------|----------|\n| Pitch (YIN) | 440 Hz sine | 440.017 Hz — 0.07 cents off A4 |\n| Onsets | click track, 0.5 s grid | ≤ 4 ms offset (frame-center convention, 256-sample hop) |\n| Key | C major triad | C major, confidence 0.79 |\n| Key | I–IV–V–I pad progression (demo) | C major |\n| Loudness | −18 dBFS-peak 997 Hz sine | −21.0 LUFS (≈ −3 LU sine crest factor — physics, not error) |\n| Silence/tail | 1 s tone + 1 s silence | trailing 960 ms, last-audible within one RMS window |\n| Clipping | driven square, clamped | counted; true-peak-over-0 flagged |\n| Tempo | 120/90 BPM click track | ±1 BPM, pulse clarity 0.96, `clear_rhythm=true` |\n| Tempo | `drum_groove` demo (110 BPM groove) | 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 |\n| Rhythm | quarter-note clicks vs straight eighths | grid alignment 1.0 for both; offbeat ratio 0.0 vs 0.49 — syncopation as a number |\n| Rhythm robustness | click track, ±5/±10 ms human timing jitter | BPM exact, alignment 1.0, `clear_rhythm` holds (clarity 0.77 / 0.51) |\n| 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 `clear_rhythm` holds |\n| Rhythm robustness | one dropped + one extra hit in 22 | BPM and `clear_rhythm` unaffected |\n| 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 |\n| 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 |\n| Tempo vs rhythm | real speed change (100 → 140 BPM mid-buffer) | stability drops ≤ 0.75 — the axis that separates the two |\n| Swing | shuffle (beats + upbeats at 2/3 beat) | `grid: triplet`, alignment 1.0, `clear_rhythm` holds — recognized, not scored sloppy |\n| Melody | three authored tones (A4 C5 E5) | reads back as `A4 C5 E5`, starts within 60 ms, centers within 5 cents |\n| Timbre | sine vs saw, same note, same level | MFCC distance separates decisively; identical input measures exactly 0 |\n| Lossy decode | the same tone via WAV, mp3, and ogg | pitch agrees within 5 cents across codecs |\n| Structure | two 8 s segments, distinct timbre | boundary within 1.5 s of the true 8.0 s cut |\n| Structure | three 8 s segments (A/B/A) | boundaries within 1.5 s of the true 8.0 s and 16.0 s cuts |\n\n## ffmpeg-free by design\n\ncochlea reads WAV, FLAC, mp3, and ogg/vorbis using `hound` and\n`symphonia`, both pure Rust. It writes plain WAV, and renders PNGs on the\nCPU with `rustfft`, a hand-rolled mel filterbank, a viridis LUT, and\n`image`.\n\nThere are no subprocess calls, no system codecs, no GPU, and no audio\ndevice. The whole pipeline is a pure Rust dependency graph, and CI blocks\nGUI, GPU, and device crates from ever entering `Cargo.lock` (`deny.toml`).\n\nDecoding comes with two different promises, and it's worth being clear\nabout which is which. WAV and FLAC decode **bit-exact** — FLAC is lossless\nby spec, and it's checked against WAV twins in-tree. mp3 and ogg are\n**analysis input only**: reproducible for a given build, but a lossy codec\nalready threw the original samples away, so there's no exactness claim\nleft to make.\n\n## Assertion cookbook\n\n```rust\nuse cochlea_verify::{VerifyExt, Tol, Ms, Cents, Db};\n\nrendered.verify(&score)\n    // Mix-level loudness and headroom:\n    .integrated_lufs(-14.0, Tol(0.5))     // streaming-loudness target\n    .true_peak_below(-1.0)                 // intersample-safe headroom\n    // Timing: did the hit land where the score says?\n    .onset_at(\"drums\", bar(17).beat(1), Ms(5.0))\n    // Intonation: does every note read as written? (monophonic tracks)\n    .pitch_matches_score(\"lead\", Cents(10.0))\n    // Was the sweep *written*? (authored curve, block-rate — a score lint)\n    .monotone(\"lead\", Param::CUTOFF_HZ, bar(1)..bar(3))\n    // ...and did it audibly *happen*? (rendered stem's spectral centroid)\n    .brightness_rises(\"lead\", bar(1)..bar(3), 1.3)\n    // Do the hits land on the detected beat grid?\n    .grid_alignment_at_least(0.9)\n    // Click detection away from note boundaries:\n    .no_discontinuity(\"lead\", Db(40.0))\n    // Does the piece actually end?\n    .silent_after(bar(64))\n    .run();\n```\n\nThe same assertions embed in score RON:\n\n```ron\nverify: [\n    IntegratedLufs(target: -14.0, tol: 0.5),\n    TruePeakBelow(dbtp: -1.0),\n    OnsetAt(track: \"drums\", at: (17, 1), tol_ms: 5.0),\n    PitchMatchesScore(track: \"lead\", tol_cents: 10.0),\n    Monotone(track: \"pad\", param: \"cutoff_hz\", from: (1, 1), to: (3, 1), direction: Rising),\n    BrightnessRises(track: \"pad\", from: (1, 1), to: (3, 1), min_ratio: 1.3),\n    NoDiscontinuity(track: \"lead\", db: 40.0),\n    SilentAfter(at: (64, 1)),\n    TempoIs(bpm: 110.0, tol_bpm: 2.0),\n    HasClearRhythm(expected: true),\n    GridAlignmentAtLeast(min: 0.9),\n]\n```\n\n`cochlea render score.ron --verify` runs them; failures come back as JSON\n(`{\"passed\": false, \"checks\": [...]}`) and a nonzero exit.\n\nTo actually *hit* a loudness target rather than just assert it, give the\nscore a master bus — gain to push, a limiter to hold the ceiling:\n\n```ron\nmaster: Master(\n    gain_db: 4.0,\n    limiter: Limiter(ceiling_db: -2.0),   // sample-peak ceiling holds exactly\n),\nverify: [\n    IntegratedLufs(target: -14.0, tol: 0.5),\n    TruePeakBelow(dbtp: -1.0),   // ~1 dB headroom over the ceiling: true peak is inter-sample\n]\n```\n\nFour worked demos live in [`demos/`](demos/):\n\n- **`metronome`** — sample-exact scheduling and onset tolerances.\n- **`chord_pad`** — harmony reads back as written.\n- **`title_cue`** — a four-bar cinematic sting that asserts a LUFS target,\n  a monotone filter sweep, click-freedom, and silence after the fade.\n- **`drum_groove`** — a 110 BPM eight-bar groove on the real `kick` and\n  `snare` patches, hats panned right and snare left. It asserts detected\n  tempo, `HasClearRhythm(true)` with grid alignment ≥ 0.9, stereo width,\n  loudness range, and section count.\n\n`drum_groove` is also the fixture that motivated splitting tempo from\nrhythm: the old single confidence metric read it as rhythm-less at 0.01,\ndespite getting the BPM spot on.\n\n## Workspace\n\n```\ncrates/\n  score      # IR: ticks, tempo map, bar/beat math, notes, automation, master, RON form, MIDI import\n  synth      # Patch trait over fundsp, eleven presets, param registry, counter RNG\n  render     # block engine, voices, stems, f64 master sum + gain/limiter, WAV out\n  features   # LUFS/true peak, onsets, pitch+melody, timbre, chroma/key, tempo, rhythm, stereo, structure\n  decode     # WAV + FLAC (bit-exact) + mp3 + ogg (analysis) -> Audio, pure Rust\n  spectro    # mel spectrogram -> PNG, overlays, diff heat maps, contact sheets\n  verify     # assertion DSL + RON-embeddable specs + JSON reports\n  cli        # the `cochlea` binary\n  mcp        # MCP stdio server (agents call compose/render/probe/verify as tools)\n```\n\n`features` and `spectro` depend on neither `score` nor `synth` — enforced\nin CI — which is why `probe` works on arbitrary audio files with no score\nin sight.\n\n## License\n\nMIT OR Apache-2.0, at your option.\n\n[fundsp]: https://crates.io/crates/fundsp\n[ebur128]: https://crates.io/crates/ebur128\n",
  "bytes": 26806,
  "sha": "7813b360fb98c39258a3269686dee0006d3e66b395498962d456af82324648d8",
  "repo_slug": "richer-richard/cochlea",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_richer_richard_cochlea_mcp_eee5053e/readme"
}