subverselab-sampler
YouTube or local audio to a sliced sample pack, with a waveform UI and a chop bench that writes into an Ableton project.
Links
README
From the repo.
SubverseLab Sampler
Builds a sample pack from a YouTube link (or a local audio/video file). It
downloads the audio, runs tempo and transient analysis, and writes WAV slices
plus a manifest.json according to the chosen chop mode.
Two interfaces, both driving the same engine (engine/):
a CLI (sampler.py) and a waveform UI (ui/server.py).
Usage
cd ~/Desktop/SubverseLab-Launchpad/01_AI_Tools/subverselab-sampler
# simplest form: transient (hit) slices
./sample "https://www.youtube.com/watch?v=..."
# only 1:12 - 1:44, all four modes
./sample "https://youtu.be/..." --from 1:12 --to 1:44 --mode all
# if you know the tempo, give it by hand for bars mode (detection is a guess)
./sample "https://youtu.be/..." --mode bars --bpm 174 --bars 2
# a local file works too
./sample ~/Desktop/recording.wav --mode transient --normalize
Output goes by default to 04_Sample_Packs/<video-title>_<video-id>/.
04_Sample_Packs/nina-simone-sinnerman_abc123/
_source.wav <- trimmed, decoded full source (44.1k stereo)
manifest.json <- analysis + each slice's time, length and peak
transient/001_transient_0m01.93s.wav
bars/001_bars_0m11.87s.wav
...
The waveform UI
python3 ui/server.py # http://127.0.0.1:8787
The server binds to 127.0.0.1 only; it is not exposed.
It has the one thing the CLI does not: correcting slice boundaries by hand. The mode produces slices, you fix them on the waveform, and export from there.
- Drag on empty space = new slice · drag a slice = move it · drag its edge = resize
- Two adjacent slices share one split point: pulling it moves both sides.
shift+drag breaks the link and moves each side separately. alt+click = delete · click = select and play ·space= play/stop- Wheel = zoom,
shift+wheel = scroll; the strip on top shows the whole file and the visible range - "Visible range only" = generate slices only within the on-screen time range
The manifest.json written from the UI records how the slices were actually
produced: if you passed --bpm by hand, the manifest writes that grid, not
librosa's estimate.
Also registered in .claude/launch.json as sampler-ui.
Live project layer (liveproj.py)
Live's Python API CANNOT open a project. This layer makes opening, reading state and quitting safe — and, most importantly, verifies from Live's own log whether the open ACTUALLY succeeded.
python3 liveproj.py status
python3 liveproj.py open bench.als --yes
python3 liveproj.py quit
open output:
result : OPENED
window : BENCH_experiments
log : loaded=True repaired=0 corrupt=0
If there is unsaved work, Live's own dialog asks; this layer discards nothing automatically. Because a Remote Script can grow the log by megabytes a minute (768 MB has been seen), the search walks backwards in chunks.
Agent — the layer that tries techniques and has them scored (agent.py)
Division of labour:
| layer | what it does |
|---|---|
| chopper | cuts with a fixed technique; it knows what to cut, it does not experiment |
| agent | scores techniques by trying them, writes the experiments into the project |
| you | rate the experiments 0–10 |
An experiment = one technique tried on one recording. Each experiment is written into the project in its own 32-bar block, on its own tracks, with the technique name dropped on a locator.
python3 agent.py run recording.wav --template p.als --record "recording.wav" \
--record-path /path/recording.wav --out experiments.als --bpm 103
# warp_2bar 4 proposals · mean internal score 79.0 · id 63369f3d
# warp_4bar 4 proposals · mean internal score 74.4 · id 851c7ce5
# oneshot 4 proposals · mean internal score 83.8 · id 665eb903
python3 agent.py rate 63369f3d 8 --note "2 bar is closest to me"
python3 agent.py report
Two signals accumulate: your explicit rating, and what you did in the
project (kept / moved / deleted, via bench.py --diff). The ledger is
_ledger/experiments.jsonl.
If a technique cannot produce a candidate, the experiment is not written — the gap is not filled with a guess.
The bench — a chop that writes into the project (bench.py)
It writes no WAVs. It writes the proposals directly into an Ableton project, each candidate in its own 32-bar block on its own tracks. The clips point at the recording's original file; the cut is the clip's region — so in Live you move it, stretch it, delete it.
python3 plan.py recording.wav --bpm 103 --count 10 --json > plan.json
python3 bench.py --template project.als --record "recording.wav" --plan plan.json --out bench.als
You work on the bench, save, and then:
python3 bench.py --diff bench.proposal.json what_you_saved.als
# {"kept": 5, "moved": 2, "deleted": 3, "total": 10}
Every decision carries the proposal's score. As these accumulate, "does a high score actually survive" becomes measurable — that is the learning signal.
One tempo. A project carries a single BPM; section-level tempo cannot be
written from an extension in Live (there is no automation API in the SDK, and
rootNote/scaleName are read-only). The measured real BPM and key are written
into the locator name instead.
Limit 0 — the output must be INSIDE THE PROJECT FOLDER. Live resolves
samples by relative path first; if the set is written to another folder the
audio appears offline even when the absolute path exists on disk. Measured: 15
sounds offline outside the project, zero inside it. bench.py refuses before
writing rather than producing that.
New-recording support. If the recording does not appear in the template, the bench takes any clip as a skeleton and rebinds it to the new recording from scratch: file path, size, frame count, sample rate and warp map are rebuilt. The warp map is two markers under a constant-tempo assumption (0 s → 0 beats, duration → duration*bpm/60); if the recording's tempo is not actually constant it is fixed by hand in Live — that is the only thing guessed here. Verified: a set written with an mp3 that appears in no project opened in Live with 0 corruption, 0 repairs and 0 unopenable files.
Limit 2: headless verification shows the structure is correct (clip, track and locator counts and positions, by writing and reading back). It does not show that Live opens the file. That needs opening it by hand once.
Plan mode — a scored proposal table
python3 plan.py recording.wav --bpm 103 --count 20 --mix warp=60,oneshot=25,drum=15
python3 plan.py recording.wav --count 12 --write # also write the WAVs
python3 plan.py recording.wav --json # JSON instead of a table
It evaluates the recording in three modes, scores the candidates, distributes them by percentage, and shows each proposal on one line:
# MODE SLICE WHAT WAS DONE INTENDED RESULT SCORE
1 warp 0:19.84-0:24.50 warped to 103 BPM, 2 bars carries the track end to end 80.5
9 oneshot 0:10.52-0:11.22 cut at transient, 700 ms clean single hit - layer off-loop 94.9
13 drum 1:53.17-1:53.45 kick band 4.4x the average Drum Rack C1 - kick layer 78.3
What the score is made of (components are written out individually in
plan.json):
| Mode | Components |
|---|---|
warp | level · RMS flatness (loop evenness) · chroma stability (staying in one harmony) · grid fit |
oneshot | attack strength · silence before the attack · decay time to −30 dB · peak level |
drum | the above + band emphasis: the ratio of the candidate's kick/snare/hat band share to THE RECORDING'S OWN average |
Band classification does not look at the raw share — the snare band is the widest, so doing that made everything a snare. Instead it measures "how much more bass- or treble-weighted is this hit than the record in general", and a candidate that does not clear a 1.3× threshold is not counted as a drum. A candidate pinned to the upper limit that never decays is not counted as a one-shot either.
If a mode cannot produce the requested number of candidates, the shortfall is not filled with guesses; how many it could produce is printed under the table.
Chop modes
| Mode | What it does | What for |
|---|---|---|
transient | makes each attack a slice, running to the next attack | drum/percussion one-shots |
bars | equal N-bar loops on the detected beat grid | pulling melodic loops |
fixed | fixed-length slices (--seconds) | coarse scanning |
silence | splits on silence (--top-db) | speech, vocals, section splitting |
gaps | the ranges where the vocal is silent (needs --vocal-ref) | intro + violin answers + instrumental sections |
Modes can be combined with commas, e.g. --mode transient,bars; --mode all
runs all four. Each mode writes into its own subfolder.
gaps mode — the vocal-free ranges
If you separate the vocal with Ableton's stem splitter and pass the stem as a reference, the tool extracts the recording's choppable instrumental regions directly: the intro, the answer gaps between vocal phrases, and the instrumental sections.
./sample "no_vocals.wav" --vocal-ref "vocals.wav" --mode gaps --min-len 4.0
Vocal activity is decided by the vocal/instrumental ratio, not an absolute threshold (default: the vocal is active when it is 10 dB above the instrumental). An absolute threshold mistook separation bleed for vocal and reported one recording's instrumental share as 0%.
Example output (Müslüm Gürses – Bir Bilebilsen, assuming 103 BPM):
0.0 → 54.7 s 54.7 s ≈23.5 bars <- intro
76.4 → 87.1 s 10.7 s ≈ 4.6 bars <- answer
119.7 → 157.3 s 37.6 s ≈16.1 bars <- instrumental section
179.3 → 189.7 s 10.4 s ≈ 4.5 bars <- answer
227.4 → 254.1 s 26.7 s ≈11.5 bars <- outro
Important parameters
| Flag | Default | Note |
|---|---|---|
--from / --to | none | 83, 1:23, 1:23.5, 01:02:03 |
--min-len | 0.08 | shortest slice. In transient mode it also filters double triggers |
--max-len | unlimited | upper bound on a transient slice |
--tail | 0.0 | tail appended to the end of a slice |
--bars / --beats-per-bar | 2 / 4 | bars mode |
--bpm / --grid-offset | none | manual grid instead of detection. The grid starts at the first transient by default |
--vocal-ref | none | separated vocal stem for gaps mode |
--seconds | 2.0 | fixed mode |
--top-db | 30 | silence mode threshold. Lowering it gives fewer/longer slices |
--fade | 5 ms | anti-click in/out fade |
--normalize [DBFS] | off | per-slice peak normalise, default target -1.0 |
--bit-depth | 24 | 16 / 24 / 32 |
--max-slices | 200 | upper bound per mode |
--analyze-only | — | writes nothing, just prints the analysis |
Things you need to know (measured, not guessed)
- Tempo detection is a guess. It uses
librosa.beat.beat_trackand there is no downbeat detection — so a slice boundary is a beat boundary, not necessarily bar 1. If the tempo comes out wrong, override it with--bpm. - In a synthetic test, detection returned 80.75 BPM on a 120 BPM file (because of
the silent gaps). With
--bpm 120 --grid-offset 0the slices came out at exactly 4.000 s. - In
transientmode,onset_detect(backtrack=True)can fire twice on a single hit;--min-lenis the distance that filters it. - Slices below
-60 dBFSare not written and are counted inmanifest.jsonasskipped_silent. Slice numbers can therefore have gaps, so that the numbering stays aligned with the timeline. engine/compat.pyapplies a shim: the installed librosa 0.10.1 callsscipy.signal.hann, which newer scipy moved toscipy.signal.windows. Because the other tools on this machine depend on the same versions, librosa was not upgraded and the shim was preferred.
Requirements
All installed and verified on this machine: yt-dlp (>= 2026.08.19 — older
versions get 403 from YouTube), ffmpeg, python3 + librosa, soundfile,
numpy, scipy.
Layout
sampler.py CLI (slicing)
plan.py scored proposal table
bench.py the project-writing bench + learning diff
sample CLI shortcut
engine/ shared engine
fetch.py yt-dlp + ffmpeg -> 44.1k stereo WAV
analyze.py tempo, transients, silence + --bpm override
chop.py four modes -> slice boundaries
write.py WAV writing, fades, normalise, manifest
peaks.py min/max envelope for the waveform (optional window)
score.py warp/one-shot/drum candidate scoring
compat.py librosa/scipy shim
ui/
server.py stdlib HTTP server (no extra dependencies)
index.html single-file UI (no external JS libraries)
Collected info
- ★ 0 stars
- Language: Python
- Source updated: 9/18/2026
Config for your environment
Replace {MCP_ENDPOINT_URL} with this MCP’s endpoint URL (from its repo or docs above). No API key — you connect directly.
Tool
OS
Config file: ~/.cursor/mcp.json
{
"mcpServers": {
"mcp-server": {
"url": "{MCP_ENDPOINT_URL}"
}
}
}Paste into mcpServers in the config file. Restart Cursor after saving.
If this MCP is also published on mcpchannel.ai, you can subscribe from Browse and use the gateway config there instead.