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.
# 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.
| 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 |
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 280Scoring 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.
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
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.
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.py200 .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.
python3 scripts/used_samples_report.py --out data/used_samples.jsonJoins 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.
python3 tests/test_samplereader.py16 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.
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.
The layer that finds source records on the internet. Awaiting a decision — there
is no Logs/, the discussion is in chat.