Skip to main content
Glama

Sublevel

A reference-matching low-end fixer for Ableton Live: an MCP server Claude drives and a Max for Live device on the master, both over one engine, backed by a corpus of 798 analyzed electronic tracks.

Mixing low end without accurate monitoring is guessing. Everything below about 150Hz is inaudible on laptop speakers and misrepresented on consumer headphones. Sublevel replaces the ear with measurement where measurement works, and says how confident it is by placing every number against records you actually listen to.

What makes it trustworthy

There is not a single invented threshold in this repo. A mix is not "thin" because a diff crossed -3dB; it is thin because it sits below p10 of tracks in its own genre. Every claim carries its percentile so it can be checked.

sub vs body: you -3.42dB (p8), reference 3.92dB (p82), -7.34dB.
Kick has punch but no weight underneath it.

The corpus is scoped by genre, because the genres genuinely differ - House carries about 2dB less sub relative to kick body than Tech House or Minimal, and that is not a rounding artifact.

Related MCP server: Ableton Cookbook MCP

Status

Built and verified on a real set: measurement core, 798-track corpus with section-aware finish-line profiles, genre-scoped calibration, the low-end and club-ready planners, the guarded engine (measure, apply, verify, keep or revert, with undo and A/B), headless Vital patch design, bassline writing, the MCP server (36 tools) and the Max for Live panel. ARCHITECTURE.md has the phase table; 251 tests, including the architecture rules, pass.

Needs you, once, in Live: python -m sublevel install-remote-script, then select AbletonMCP as a Control Surface (Preferences > Link, Tempo & MIDI; Input and Output both None) and restart Live.

Setup

python3.13 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest

The MCP server is registered for this project in .mcp.json (python -m sublevel mcp). See ARCHITECTURE.md for the v2 layout and the tool contracts.

The Max for Live device

The same loop from inside Live, without Claude: aim at a genre or a record, measure, fix the low end, A/B, undo, read the changes.

.venv/bin/python -m sublevel m4l build     # writes Sublevel.amxd into Live's User Library
.venv/bin/python -m sublevel serve         # the HTTP shim the panel talks to, on 127.0.0.1:8766

Drop Sublevel.amxd on the master (it passes audio through). The device is a web view over surfaces/m4l/ui.html; every button is a POST to /api/<tool> that calls the same function the MCP tool does, so the panel cannot do anything Claude cannot, and nothing Claude cannot undo. The panel and the MCP server each hold their own session and share the one Remote Script socket: run one at a time.

Rebuilding the calibration

python -m sublevel.corpus extract ~/Downloads/beatport_tracks_* --out calibration/corpus.json
python -m sublevel.calibration build calibration/corpus.json
python -m sublevel.calibration show calibration/thresholds.json --genre "Tech House"

Extraction is resumable and keyed by content hash, so re-running skips what is already analyzed. 798 tracks takes about 14 minutes on 5 workers.

MCP tools

The agent-facing surface (python -m sublevel mcp). Long operations return a job id; poll with job.

tool

what it does

session

the open set: tracks, chains, target, budget, bypass state, jobs

set_target

genre / reference / artist / favorites / file; acceptance bands from the drop of finished records

measure, compare, readiness

loop-aligned capture; distance and gaps to the target; the club floor

map_mix, mixmap

which track owns which band

plan, apply, iterate, verify

pure plan, guarded write, the loop; goals lowend and club_ready

undo, revert_all, ab, rate, history

the undo stack, the A/B bypass, what it sounded like, the change log

monitor

read-only readout every N bars while you play

mastering_check

what the master's limiter and compressors are contributing

find_kicks, find_bass_patches, place_sample

the sample library and preset bank against the target's kick and bass

design_bass, deliver_patch

design a bass toward the reference (Vital offline, or a rack in Live in place) and put it on the bass track

detect_key, read_notes, write_bassline, automate

the key, a clip's notes, a bassline under the kick, clip automation

find_references, add_reference, finishline

the corpus and the named library

Sound design

.vital presets are plain JSON with human-named parameters, and pedalboard renders Vital offline, so the measurement engine doubles as a fitness function: write a patch, render a note, measure, adjust - about 200 candidates a minute, entirely headless, with only the winner going into Live. Targeting a real tech-house bass preset from Vital's init patch converges in ~25 seconds.

Corpus percentiles are deliberately not used here. They describe finished, mastered records; a solo'd bass patch judged against them would produce nonsense with a confident percentile attached.

Safety

Sublevel writes to Live through exactly one path, Engine.apply, and every write is measured before and after. A move is kept only if the objective improved without costing kick punch, crest, mono compatibility or headroom; otherwise it is put back. Every kept and reverted move is appended to calibration/changes.jsonl with device, parameter, old value, new value, the reason, the proposal it came from and the verdict. undo, revert_all and the ab bypass are always available.

Two notes on the Ableton bridge:

  • The Remote Script Live runs is the upstream ableton-mcp script, patched and vendored at src/sublevel/live/remote_script/. Upstream binds to 0.0.0.0; the vendored copy binds to 127.0.0.1.

  • The third-party ableton MCP server in .mcp.json has telemetry that records the originating prompt and an opt-in dataset upload. It is registered with DISABLE_TELEMETRY=1; consent is yours to give.

What it does not do

It does not judge groove, sound design, arrangement, or whether a record is good. It speaks with confidence about sub, weight, punch, width and dynamics - the parts monitoring problems hide - and reports midrange as context rather than instruction, because most midrange problems are masking and arrangement, where the fix is removing a layer and no analyzer can tell you that.

Notes on the corpus

All 798 tracks are 320kbps MP3. That is transparent in the bottom two octaves, so sub, punch, dynamics and mono correlation are trustworthy. It is not transparent at the extremes: codec reconstruction overshoots sample peaks (every track in this corpus reads above 0dBTP), and everything is lowpassed around 20kHz. true_peak_db and the high band are marked advisory wherever they surface, and are never presented as calibrated fact.

Available Tools

36 tools
abC

A/B by ear: bypass=true writes back every kept move's old value (the set as it was); bypass=false puts sublevel's moves back in. The undo stack is untouched.

Inputs: bypass (required). Returns: result

add_referenceA

Ingest a local audio file into the reference library (and optionally tag it as a favourite so set_target(kind="favorites") uses it).

Inputs: path (required), name, favorite. Returns: result

applyA

Write a proposal to Live. With verify (default) it measures before and after and keeps the moves only if the objective improved without costing punch, crest, mono or headroom; otherwise it puts them back. Runs as a job: poll job(job_id). dry_run only runs the policy filter.

Inputs: proposal_id (required), verify, dry_run. Returns: result

automateB

Write automation for one device parameter into a session clip as steps: points = [{"time": beats, "value": v}, ...]. Needs the extended Remote Script.

Inputs: track (required), device (required), points (required), parameter (required), +2 more. Returns: result

cancelB

Stop a running job between steps.

Inputs: job_id (required). Returns: result

compareA

Compare a measurement (default: the latest) with the target: per-axis gaps to the acceptance band, the 20-320Hz curve error per band, and what each gap means in plain words.

Inputs: measurement_id. Returns: result

deliver_patchA

Put a designed Vital candidate into Live: written as an AU preset, loaded onto the "Sublevel Bass" track (or track) with a 4-beat clip at the root note.

Inputs: candidate_id (required), track. Returns: result

design_bassA

Design a bass patch toward the target's bass profile (or a few words such as "deep round tight"). backend "vital": offline search, then deliver_patch loads it into Live. backend "live": searches the instrument on the "Sublevel Bass" track in place (creating the track with a Drift bass if missing) by writing its macros, firing a clip and recording. Runs as a job.

Inputs: note, brief, steps, passes, +1 more. Returns: result

detect_keyC

The key: from the notes of a clip (the bass track's, or track's) when there are enough, else from the last capture's audio, else the reference's.

Inputs: slot, track. Returns: result

find_bass_patchesB

Rank the Vital preset bank against the target's bass profile (sub/body, brightness, sustain). Seeds for design_bass.

Inputs: n. Returns: result

find_kicksA

Rank every kick one-shot on the machine against the target's kick profile (fundamental, decay, click, sub/body); pack narrows to one library. Needs a target with a profile (a reference, or a genre with the finish line built).

Inputs: n, pack. Returns: result

find_referencesD

Records to aim at. like: nearest neighbours of a named track. character: words such as "heavy sub, tight punch, pumping".

Inputs: n, like, genre, character. Returns: result

finishlineA

What a finished record measures in the drop, for a genre / family / tempo band (default: the current target's scope): acceptance bands per axis, kick and bass profiles, arrangement, pump. From the deep corpus.

Inputs: scope. Returns: result

historyB

The change log for this set: every write with old/new value, reason, proposal id and verdict (kept / reverted / undone / rated).

Inputs: limit. Returns: result

iterateB

The loop: measure, plan, apply, verify, repeat until the goal is met or the policy's round cap. goal: "lowend" or "club_ready". Runs as a job.

Inputs: goal, track, rounds. Returns: result

jobA

Poll a job: status, progress lines after since, and the result when done.

Inputs: job_id (required), since. Returns: result

map_mixA

Work out which track owns each band by pulling each one 12dB down in turn and re-measuring the master from the same bar. Needed before the planner can correct the low-end curve on the tracks that carry it. Live must be playing with the loop brace on. Runs as a job (one capture per track).

Inputs: seconds, only_tracks. Returns: result

mastering_checkA

Bypass the master's limiter / compressors, measure, restore, and say what they contribute: loudness added, crest and punch lost, whether the level is earned by the mix or borrowed from the ceiling. Runs as a job.

Inputs: no named parameters. Returns: result

measureA

Record the mix from the top of the loop and measure it: the low-end axes, dynamics, stereo, resonances, and the club-readiness floor. Live must be playing with the loop brace on. Returns a measurement id for compare().

Inputs: bars, section. Returns: result

mixmapB

The stored mix map for this set: per track, how much of each band it carries.

Inputs: no named parameters. Returns: result

monitorA

A live readout while you play: every interval_bars, capture (without seeking) and report distance and gaps against the target. Read-only. action "start" returns a job id whose progress lines are the readout; "stop" cancels it. windows=0 runs until stopped.

Inputs: action, windows, interval_bars. Returns: result

place_sampleB

Put an audio file into Live: a new audio track (or track) gets a clip in slot with the sample. Logged like any other change.

Inputs: path (required), slot, track. Returns: result

planA

Plan moves toward the target without touching Live. goal: "lowend" steers the low end into the target's band; "club_ready" fixes whatever the club floor failed (mono, headroom, resonances, loudness, plus the low-end axes). track: force the moves onto one track index instead of the mix map's owner. Returns a proposal id for apply(), the moves, anything refused, and the devices it would need added.

Inputs: goal, track. Returns: result

prepare_trackB

Give a track the chain finished projects put on its role (kick: Saturator, Drum Buss, EQ Eight; bass: Saturator, EQ Eight, Glue; master: Glue, Saturator, EQ Eight, Limiter, Utility). role is read from the track name unless given. Adds only what is missing, in order, at the end of the chain.

Inputs: track (required), role, dry_run. Returns: result

project_referenceB

What finished projects are built like: with no name, the profile across all indexed .als files (what sits on kicks, basses, buses and the master, tempo, length); with a name, that project's tracks and chains.

Inputs: name. Returns: result

rateA

Record what a kept proposal sounded like: verdict "good" or "bad", with an optional note. Goes into the change log next to the numbers.

Inputs: verdict (required), proposal_id (required), note. Returns: result

readinessB

The club floor on a measurement (default: latest): seven pass/fail checks plus the DJ-mixer boost test, each with its consequence and the fix.

Inputs: measurement_id. Returns: result

read_notesD

The notes of a MIDI clip.

Inputs: track (required), slot. Returns: result

revert_allC

Put every parameter sublevel changed in this session back.

Inputs: no named parameters. Returns: result

samplesA

What the sample index holds: one-shots per kind, and the packs the kicks come from. Build or refresh it with python -m sublevel samples index.

Inputs: no named parameters. Returns: result

scaffoldA

Set an empty project up like a finished one: tempo, KICK / BASS / DRUMS / PERC / SYNTHS / VOCALS / FX tracks with the device chains finished projects put on each role, two returns, and the master chain (Glue, Saturator, EQ Eight, Limiter, Utility). dry_run lists it; dry_run=false builds it. Groups have to be made by hand afterwards - the Remote Script cannot create them.

Inputs: dry_run. Returns: result

sessionA

What sublevel knows about the open Live set: tracks and their device chains, the current target, budget spent, whether sublevel's moves are bypassed, open proposals and running jobs. Call this first.

Inputs: no named parameters. Returns: result

set_targetB

Choose what the mix is steered toward.

kind: "genre" (value = genre name, or blank for the session's best guess), "reference" (value = a record name from the library or corpus, e.g. "Eating Glue"), "artist" (value = artist name; median of their corpus tracks), "favorites" (median of references tagged favourite), "file" (value = path). Returns the target's acceptance ranges (p25-p75) per axis and any warning that the chosen record is an outlier for its genre.

Inputs: kind, genre, value. Returns: result

undoB

Undo the last n kept proposals (most recent first).

Inputs: n. Returns: result

verifyA

Re-measure now and compare with the baseline taken before the first apply.

Inputs: no named parameters. Returns: result

write_basslineA

A bassline that sits under the kick: root from the reference's bass (or the key, or root as a MIDI pitch), kick grid from a drum clip or the last capture. style: sub_hold, rolling_offbeat, follow_kick, two_note. dry_run shows the piano roll; dry_run=false writes it to slot slot of the bass track (created if missing) or track, replacing what is there.

Inputs: bars, root, slot, style, +2 more. Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    C2.9/5.0

    Scored across 36 tools

    Disambiguation4/5

    Tools are grouped into clear functional clusters—reference, measurement, mixing moves, and job control—and most have distinct resource-action purposes. The measurement family (measure/compare/verify/monitor) and revert family (undo/revert_all/ab) could be confused, but their scopes are different enough that a careful agent should select correctly.

    Naming Consistency4/5

    Most actions use an imperative verb_target pattern and information tools are named as plain nouns (session, history, samples, mixmap), which is a consistent semantic convention. A few bare verbs or abbreviations like ab, job, and rate break the pattern slightly, but the all-snake_case style keeps it predictable.

    Tool Count2/5

    At 36 tools, this is well beyond the 25-tool 'too many' threshold, making the surface heavy even for a broad DAW-assistant domain. Several tools could be consolidated into grouped subcommands—especially the measurement, reference, and job-related tools—without losing clarity.

    Completeness4/5

    The set covers the full workflow: references and target selection, measurement and comparison, planning/apply/iterate, undo and history, plus creative helpers like bassline writing, patch design, and project scaffolding. Minor gaps like reference-library removal/editing exist, but core workflows are complete with no dead ends.

    Related MCP Connectors

    Related MCP Servers