smpl · part of LEMON

smpl

An audio-analysis toolchain you pipe like jq.

Each stage passes along a small self-describing record that points at the audio, so a pipe can split a sample apart, reshape one piece, and hand back measurements with units. Nothing exotic underneath: one line of JSON per step, ordinary audio files on disk, and standard tools like sox and ffmpeg plug straight in.

$ smpl read loop.wav | smpl loudness | smpl view > /dev/null

In plain words: load loop.wav, measure its loudness, and print a readable report. This whole pipe ships in the light core: no model download, separate heavy tool, or API key needed.

Part of the LEMON house · lemon-agent.dev teaches the agent method · lemon.audio is the music side of the house

Waveform of the demo loop: eight kick hits with hats and an offbeat bassline over four seconds
the demo loop this page keeps reusing · 4.0 s · 44.1 kHz · every number below was measured, not typed

Your first measured loop

Start from an empty terminal and use the demo audio shipped with this site. The path below installs only the core and ends with a report you can check against known output.

1 Install the light core

Install uv first if the uv command is not already available, then run:

$ uv tool install git+https://github.com/chronick/smpl#subdirectory=packages/smpl \
    --with git+https://github.com/chronick/smpl#subdirectory=packages/smplstream \
    --with git+https://github.com/chronick/smpl#subdirectory=packages/smpl-analysis

2 Download the shipped loop

This is the same four-second WAV used by the players and measured examples on this page.

$ curl -LO https://chronick.github.io/smpl/assets/loop.wav

3 Run the core-only pipe

view writes the readable report to the terminal; the redirect hides the NDJSON frames that remain available on stdout for another pipe.

$ smpl read loop.wav | smpl loudness | smpl view > /dev/null
EXPECTED

The terminal prints a Markdown report. In its feature table, look for integrated loudness around -20.79 LUFS and true peak around -6.71 dBTP:

| `loudness.integrated_lufs` | -20.79 | LUFS | loudness | loudness |
| `loudness.true_peak_dbtp` | -6.71 | dBTP | loudness | loudness |

Small last-decimal differences across platforms are fine; a report with those two measured rows means the install and first pipe worked.

Three ideas, no magic

Frames, not bytes

One JSON object per line flows between stages: what the audio is, where it came from, what has been measured so far. The heavy bytes stay on disk. jq, grep, and a text editor all work on the stream.

Content-addressed audio

Audio lives in a local store keyed by a hash of the decoded PCM, not the file bytes. Two identical stems share one blob, and any frame can be turned back into a file path with smpl resolve.

Ops you can trust

Every operation records its name, version, parameters, and inputs on the frame it emits. Results carry their own lineage, identical audio shares one address in the store, and any run can be reproduced exactly.

Before and after, with receipts

Five real runs on generated test audio. The commands shown are the commands that made the pictures and the numbers, and the players hold the actual sound that went in and came out.

Never used a terminal pipeline? Three symbols carry everything on this page. A line starting with $ is a command you paste into a terminal (the $ itself is not typed). The | is a pipe: it feeds the output of the command on its left into the command on its right, so a chain reads left to right like an assembly line. A trailing \ only continues a long command onto the next line.

Level a quiet bounce

The loop was exported too quiet. One command raises it to a standard streaming loudness, with a safety ceiling so no peak distorts on the way up.

$ smpl read loop.wav | smpl normalize --lufs -14 | smpl write leveled.wav

read loads the file, normalize raises it toward -14 LUFS without letting any peak past the ceiling, and write saves the result as a new file.

IN
Input waveform: the loop at low level, peaks well under full scale

integrated -20.79 LUFS · true peak -6.71 dBTP

OUT
Output waveform: the same loop visibly louder after normalization

integrated -15.08 LUFS · true peak -1.00 dBTP

The honest part: the target was -14 LUFS, and the result is -15.08. Reaching -14 would have pushed the true peak past the default -1 dBTP ceiling, so the ceiling won and the output sits at exactly -1.00 dBTP. The tool tells you what it did instead of pretending it hit the number.

Reshape a pad into a pluck

A sustained pad goes in; a plucked version comes out. Only the loudness contour changes. The tone underneath stays the same, which is why the after still sounds like the same chord.

$ smpl read pad.wav | smpl env --pluck --release 0.6 | smpl write pluck.wav

read loads the pad, env reshapes its volume over time (a fast attack, then a 0.6 second decay), and write saves it.

IN
Input waveform: a sustained pad holding steady level for three seconds

a sustained chord, level held for 3 s

OUT
Output waveform: the same pad with a fast attack and exponential decay

same audio under a pluck envelope, 0.6 s decay

The amplitude envelope family (env) also does fades and gates. Like every edit stage it emits a new frame with role source.wet; the dry frame stays in the stream untouched.

Strip the low end

Everything below 200 Hz goes. The kick's body and the bass fundamentals drop out, and what is left is the top of the kit: the click, the hats, the harmonics.

$ smpl read loop.wav | smpl filter --hp 200 | smpl write thin.wav

read loads the loop, filter keeps only what sits above 200 Hz (hp is short for high-pass), and write saves the thin version.

IN
Input waveform: full loop with heavy kick and bass energy

kick and bass carry most of the level

OUT
Output waveform: the same loop much thinner after a 200 Hz high-pass

high-passed at 200 Hz: hats and bass harmonics remain

Find the hits

Nothing is changed here. The tool listens for the start of each drum hit and writes the times down as data you can use: for chopping a loop into slices, or for lining edits up with the hits.

$ smpl read loop.wav | smpl slice

read loads the loop and slice detects where each hit begins, emitting the list of times alongside the audio.

The loop waveform with 18 dashed onset marker lines drawn where hits were detected

18 onsets detected; the amber dashes are the marker frame drawn over the input

Markers are frames too: slice emits a marker frame with a timestamp per onset (0.244 s, 0.488 s, 0.743 s, …), and can materialize each slice as its own audio frame. Downstream stages can select one hit by role, the way the command at the top of this page picks out the drums.

Catch a WAV that used to be an MP3

Someone hands you a WAV file. Was it recorded that way, or was it once a small MP3 dressed up in a WAV container? MP3 encoders throw away the highest frequencies, and that scar never heals. The tools answer with a picture and a number.

$ smpl read suspect.wav | smpl qc | smpl spectrogram

read loads the file, qc measures where the high frequencies stop, and spectrogram draws the picture (time runs left to right, low notes at the bottom, highs at the top). Add | smpl view on the end for the full report.

CLEAN
Mel spectrogram of the clean recording: energy reaches the top of the frequency range
energy all the way up · cutoff 22,018 Hz ≈ Nyquist · lossy confidence 0.0
SUSPECT
Mel spectrogram of the round-tripped file: a hard black band caps the spectrum above 16 kHz
a hard ceiling appears · cutoff 16,462 Hz · lossy confidence 0.856

The suspect file is the clean one after a round trip through a 64 kbps MP3. The encoder's low-pass shows up as the black cap on the spectrogram, and qc reports it as numbers: measured cutoff 16,462 Hz against an expected 22,050 Hz, confidence 0.856. These spectrograms are smpl's own output images, copied straight out of the store.

Every number and picture in this section comes from docs/make_assets.sh, which generates the audio, runs the pipes shown, and saves the results (plus a numbers.json receipt). When the tools change, re-running that script either regenerates the page cleanly or visibly breaks it.

One pipe, step by step

The same mechanics as every example above, slowed all the way down. One fixed pipe, four stages; each stage does its one job and passes everything it received along, plus what it added:

$ smpl read loop.wav | smpl loudness | smpl qc | smpl view
smpl read loop.wav

Loads the file into the local store and describes it. No audio flows in the pipe itself, just the description.

smpl loudness

Measures loudness. The audio frame passes through untouched; a frame of numbers joins it.

smpl qc

Runs the health checks on the same stream, and marks where any defects sit in time.

smpl view

Reads everything that accumulated and renders one report, every number with its source.

Nothing here re-opened the audio file or re-ran an earlier step. Each stage saw everything the previous stages knew, because the stream carries the knowledge forward. That is the whole trick, and every pipe on this page is this same picture with different stages.

Pipes compose

The point of the pipe design is that stages combine in ways nobody planned. Three examples of what that buys:

Measure, then read the stream like any other stream

$ smpl read loop.wav | smpl loudness | jq 'select(.kind=="feature").data'
{
  "loudness.integrated_lufs": -20.79,
  "loudness.true_peak_dbtp": -6.71,
  "loudness.max_short_term_lufs": -20.71
}

Ask for loudness, then pluck the answer out of the stream with jq, the standard command-line JSON tool. No parser, no export step: measurements are already plain JSON, one object per line.

Splice sox or ffmpeg into the middle

$ smpl read x.wav | smpl as-wav | sox - -t wav - reverb 50 \
    | smpl from-wav --role x.wet --derives-from source | smpl describe

Here sox, a classic audio tool from 1991, adds the reverb. as-wav hands it plain audio, it does its thing, and from-wav stores the result as a new frame that still knows what it came from. Any tool that reads and writes WAV through a pipe, sox and ffmpeg included, becomes an smpl stage this way.

Isolate, reshape, then describe just that piece

$ smpl read kick.wav | smpl stems | smpl select --role stem:drums \
    | smpl filter --hp 200 | smpl env --pluck | smpl describe | smpl view

The tail of the pipe sees the whole lineage: original, stems, the filtered subcomponent. describe measures whatever is selected, and view renders the report for exactly that piece, not the whole file. (stems is a heavy tool with its own install; see the tiers below.)

The tools

One smpl command, 47 built-in subcommands, each doing one thing well. Nearly all of them read the stream in and pass the stream along, so they can sit anywhere in a pipe; the exceptions are the deliberate edges, like as-wav (raw audio out, for the bridge) and write (a file, at the end). The highlights, from smpl --help:

Ingest and store

read / writefile in, frames out; selected frame back to a file
resolvehash, id, or role to a real file path
convertformat, sample rate, bit depth via ffmpeg
as-wav / from-wavthe raw-WAV bridge to sox and ffmpeg
gccollect unreferenced blobs from the store

Measure

loudnessintegrated LUFS, true peak dBTP, short-term LUFS
spectralflatness, crest, spread, rolloff, contrast, slope
qcclipping, phase, DC offset, SNR, lossy-origin cutoff
describe-allthe whole light tier in one stage, plus caption and image

See and report

spectrogrammel, CQT, HPSS, and waveform renders as image frames
cat / describedescribe as a filter: passthrough plus features, caption, image
viewone markdown report over everything in the stream

Shape

gain · normalize · limitdB gain, LUFS target with a true-peak ceiling, peak cap
maximize · compresslook-ahead brickwall drive; downward compression
eq · filterpeaking and shelf EQ; high, low, band pass
env · fx · automateenvelopes; sox reverb and delay; parameter motion over time
stereoize · widenmono to wide, and M-S width with a mono-safe low end
spectral-matchEQ one sound toward a reference's balance

Cut and pick

sliceonset detection to marker frames, optionally sliced audio
selectfilter the stream by role or kind
patterna step-grid drum-loop DSL with velocity, pitch, swing, nudge

Heavy tier · separate installs, found by name

Anything with big machine-learning dependencies lives in its own isolated install, so the core never pays for it at startup. smpl stems simply hands off to smpl-stems if you have installed it; if you have not, it stops and names the missing tool.

stemssource separation into drums, bass, vocals, other
transcribeWhisper speech and lyrics with srt/lrc/vtt export
embedMERT and CLAP embeddings plus a similarity index
gen · cloudlocal and provider-API audio generation
synthSuperCollider renders, offline
transcribe-midi · render-midiaudio to MIDI and MIDI to audio, offline

Not every subcommand made this table; crop, reverse, stretch, pitch, stats, and friends are in smpl --help.

Built for agents

The design bet: the deterministic tier does the measuring, the model does the interpreting. An agent should never guess a LUFS value, and with smpl it never has to.

What the agent reads

smpl view renders the stream as a markdown report: the measurements, with units on the keys that define them, markers tied to time, and image frames it can open. These are rows from the actual report for the demo loop (rows selected and two columns dropped for width):

# smpl analysis report

**13 frame(s):** 1× audio, 9× feature,
1× image, 1× marker, 1× text

| key | value | unit |
|---|---|---|
| loudness.integrated_lufs | -20.79 | LUFS |
| loudness.true_peak_dbtp | -6.71 | dBTP |
| lowlevel.spectral_flatness_db | -49.3421 (±22.5402) | dB |
| lowlevel.spectral_rolloff | 5228.3865 (±7381.6941) |  |
| qc.clipping.detected | false |  |
| qc.dc_offset_dbfs | -68.9 | dBFS |
| qc.lossy.confidence | 0.001 | 0–1 |
| envelope.attack_ms_10_90 | 10.385 | ms |
| envelope.sustain_ratio_150ms | 0.0203 | ratio |
| movement.sidechain_db | 10.454 | dB |
…

What the agent sees

Image frames are real annotated figures. A model that can read images can look at the spectrogram and say where the energy sits, then cite the feature table for the numbers.

Annotated mel spectrogram of the demo loop as produced by smpl spectrogram: kick columns, bass energy at the bottom, axis labels and a dB colorbar
smpl spectrogram --kind mel · the demo loop · straight from the store

Install the skills

Two agent skills ship in this repo, in the same installable format the rest of the LEMON house uses. They teach a coding agent when to reach for which pipe and how to report what came back:

$ npx skills add chronick/smpl --global --agent codex claude-code --yes
smpl-dissect

The microscope. Isolate a stem, a slice, or a filtered band, then describe exactly that piece with cited numbers and the spectrogram in front of the model.

smpl-audit

The bounce check. Loudness, true peak, clipping, DC, noise, and lossy-origin forensics, reported as a verdict where every claim carries a measured value and its unit.

Both skills expect the smpl command to be installed, and say so plainly when it is missing. The skill files are plain markdown in skills/; read them before you install them.

The wire protocol

A frame is one line of NDJSON that describes itself: what kind of thing it is, which operation produced it, what it derives from, and where its bytes live. This is the real first frame of the demo loop:

{
 "kind": "audio",
 "hash": "blake3:432c046f870dfb38cda28552dc94013f38…",
 "media": "audio/wav",
 "meta": { "sr": 44100, "ch": 1, "dur": 4.0, "fmt": "WAV" },
 "role": "source",
 "op": "read",
 "op_version": "read@1",
 "params": { "source": "loop.wav" },
 "v": 1,
 "id": "blake3:ec7431cbee8874db9d667ef9d546705e…"
}

Eight kinds cover the stream: audio, image, text, vector, marker, feature, midi, error. Hashing is defined over the canonical decoded PCM, so the same sound has the same address no matter which container it arrived in. The full contract, including the memo key and store integrity rules, is spec.md, versioned like an API.

Install

The core is light on purpose: it does not load big machine-learning libraries just to start up; the heavy pieces load only inside the subcommands that use them. Installing needs uv, the standard Python tool manager.

# the light core, one isolated install
$ uv tool install git+https://github.com/chronick/smpl#subdirectory=packages/smpl \
    --with git+https://github.com/chronick/smpl#subdirectory=packages/smplstream \
    --with git+https://github.com/chronick/smpl#subdirectory=packages/smpl-analysis
# heavy tools install separately, each into its own venv
$ uv tool install git+https://github.com/chronick/smpl#subdirectory=tools/smpl-stems

Having ffmpeg and sox installed unlocks convert, fx, and the raw-WAV bridge. MIT licensed.