sample-reader
Measures the audio itself, not the filename: tempo, key, brightness, analogue signature - and refuses to answer when the evidence is thin.
Links
README
From the repo.
sample-reader
Measures the audio itself, not the filename.
Loom's existing layer (extract_sound_sources.py) reads sample names out of
.als files. Nowhere was the audio itself being measured. This tool fills that
gap: it opens an audio file and says what is inside it — and when it cannot say,
it does not guess, it writes down why.
Three commands
# 1) Read and measure individual files
python3 -m samplereader read "/some/folder" "/some/file.wav"
# 2) Build a reference profile from our own material
python3 -m samplereader profile "/source/records" --label source-records --out data/profile.json
# 3) Rank candidates against that profile (lower distance = closer)
python3 -m samplereader match data/profile.json "/candidate/folder" --top 25
--workers N reads in parallel (default: cores − 1), --limit N caps the file
count, --json gives machine-readable output.
What it measures
| field | meaning |
|---|---|
duration_s, sample_rate, channels, subtype | read from the header, not decoded |
peak_dbfs, rms_dbfs, crest_db, clipped_samples | level |
noise_floor_dbfs | quietest 5%, digital silence excluded. High on an old transfer (hiss), low on a modern render |
silence_share | how much of the file is exact silence |
stereo_width | 0.0 = both channels identical (a mono record in a stereo file) |
tempo_bpm, tempo_source | tempo and how it was found (loop_length / beat_tracker) |
chop_bpm, in_chop_range | octave-folded, and whether it lands in the 68–98 window |
onset_rate_hz | attacks per second — chop density |
key, key_confidence | Krumhansl-Kessler; if the winner does not clearly beat the runner-up, no key is returned |
centroid_hz, rolloff85_hz, bandwidth_hz | brightness |
low_ratio, air_ratio | share of energy below 120 Hz and above 8 kHz — the signature of an old recording is the absence at the top |
harmonic_ratio | HPSS separation: loops score high, drum breaks low |
Tempo: why it is computed from duration
The first version used beat tracking. Measured against 156 library files with the BPM in their filename — 31% correct. The reason: those files are 1–2 bars, i.e. 2.3–5.3 seconds. A bar is four beats; a beat tracker needs eight.
On a short loop the tempo falls exactly out of the duration:
bpm = bars × 4 × 60 / duration. Duration does not settle the octave (2.67 s is
both one bar at 90 and two bars at 180) — an autocorrelation hint picks that.
Measured result:
| method | accuracy |
|---|---|
| beat tracking (start_bpm=120) | 31% |
| beat tracking (start_bpm=82) | 48% |
| autocorrelation median | 49% |
| loop length + autocorrelation octave choice | 82% |
The reference set was itself checked: file durations sit on exact bar counts (1.000, 2.000, 4.000), so the BPM values in the names are real.
To reproduce:
python3 scripts/benchmark_tempo.py "$HOME/Music/Ableton/Factory Packs/Chop and Swing" --step 3 --cap 280
Scoring is octave-tolerant, because that is what the tool has to get right: the chop is folded to 68–98 anyway, so reading 140 as 70 is a hit.
When it refuses to answer
A wrong BPM is worse than no BPM — the next step will look at it and pick a record.
- file shorter than 1.5 s →
one_shot(...), no tempo and no key - no plausible bar count between 55 and 210 →
no_plausible_bar_count(...) - beat tracker found fewer than eight beats, or the pulse wobbles →
too_few_beats/unsteady_pulse - the top key does not clearly beat the second →
ambiguous(margin=...) - profile built from fewer than 8 files → the profile is not built,
thin_evidence - candidate and profile share fewer than 5 dimensions → not scored
Known limit: Ableton's own codec
Most .aif files in the Factory Packs are in Ableton's own AIFF-C compression
(able tag). libsndfile, ffmpeg and CoreAudio all fail to open them — only Live
can. This is not a bug but a fact about the library; the tool reports it
separately as ableton_compressed rather than folding it into a generic
"corrupt file" error. In the 280-file measurement above, 98 were unreadable for
this reason.
Our own material, measured
data/profile_kaynak_plaklar.json — the chop source records and their demucs
stems (Tövbe, Felekten Beter, Bir Araya Gelemeyiz, Haram), 12 files.
- chop tempo median 84.6, interquartile 78.3 – 89.1 — but see the warning below
- every measured key is minor, most often E min
air_ratiomedian 0.0011 — almost nothing above 8 kHzcentroidmedian 1680 Hz — dark recordings
12 files barely clears the threshold of 8. The profile is valid as it stands but narrow; it sharpens as more source records are measured.
Warning — tempo on long files is unverified. Those 12 files run for minutes,
so their tempo came through beat_tracker, not loop_length, and that path's
accuracy was only measured on short loops. Compared against the projects' own
settings there is at least one outright error: the Tövbe project is at 140 BPM
(folded 70), while the reader says 103.36 for the same source. Treat long-file
tempo values as unverified.
Stronger evidence for tempo habit is the .als files themselves — not a guess,
but the number written into Live:
python3 scripts/project_tempos.py
200 .als files, 29 still on the untouched 120 default (not counted), 171
projects with a chosen tempo: folded median 86, Q1 70, Q3 98, 53% inside
the 68–98 window.
What we have actually used on disk
python3 scripts/used_samples_report.py --out data/used_samples.json
Joins Loom's .als extraction with a disk scan: which sample appeared in which
project, where it sits on disk, which pack it came from, and how the ones that
can be measured sound. Result in data/used_samples.json. Multisample families
collapse to one row — otherwise a 36-note instrument looks like 36 habits.
Tests
python3 tests/test_samplereader.py
16 tests, all on synthesised signals (a click loop at a known BPM, a tone at a known frequency, exact silence). None of them needs the user's audio library.
Dependencies
Installed on the machine: numpy, soundfile, librosa 0.10.1, scipy 1.17.
read.py carries two compatibility patches, both explained in comments:
scipy.signal.hann, which librosa 0.10.1 calls and SciPy 1.13 removed, is put
back; and the autocorrelation tempo estimator is resolved once because of lazy
loading.
Not done
The layer that finds source records on the internet. Awaiting a decision — there
is no Logs/, the discussion is in chat.
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.