Commit 9f48601e by PLN (Algolia)

feat(setbuilder): declared bpm and sample weight, because the set changed shape

PLN, at the desk: "cyber hump and wap and bombe dj not adequate too sample
heavy its a nujazz/chill dnb set i think is the right one."

That is an ear judgement the tooling cannot make, but it can hand him the two
handles the judgement needs. Every card now carries the BPM the track declares
and how many distinct sample banks it pulls from, and the list sorts by hands
first, lightest on samples, or slowest BPM.

Bank count is a proxy and it is labelled as one — it is not busyness or
loudness, just breadth of folders. It does separate the three he named
(wap 11, cafe_glace 10, bombe_dj 9) from the light end, which is the job.

BPM is DECLARED, never measured, and the legend says so: tempo-lens.py opens
by warning that setcps is not the tempo, since a track at setcps 80 with
everything on the half-note performs at 160. Right handle for ordering an arc,
wrong one for claiming a speed.

`declared_bpm` goes into setlist_samples.py, where corpus syntax belongs, not
into the generator — a second private regex for the corpus's own idiom is how
the parser rule gets broken one convenience at a time. tools/lcxl3-driver.py
carries an identical `parse_bpm`; it predates this and it is load-bearing two
days out, so the duplication is recorded in a comment and collapses after
Thursday rather than tonight.
parent 26f20c02
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -58,6 +58,10 @@
.pill.warn{background:rgba(160,106,20,.16);color:var(--warn)}
.pill.played{background:rgba(181,83,58,.14);color:var(--accent)}
.pill.cold{background:rgba(138,122,104,.14);color:#6f6152}
.meta{font-family:var(--mono);font-size:.7rem;color:var(--dim);margin-left:auto}
.card.on .meta{margin-left:8px}
select{font-family:var(--mono);font-size:.78rem;color:var(--ink);background:#efe8d8;
border:1px solid var(--line);border-radius:6px;padding:3px 6px}
.note{color:#5a4a3c;font-size:.82rem;margin-top:5px}
.card.warnrow .note{color:#8a5a14}
.path{font-family:var(--mono);font-size:.72rem;color:var(--dim);margin-top:5px;
......@@ -117,6 +121,13 @@
<label><input type="checkbox" id="warnon"> include silent-orbit <span id="nwarn"></span></label>
<label><input type="checkbox" id="handson"> played on this rig only</label>
<label><input type="checkbox" id="warmon"> fully warm only</label>
<label>sort
<select id="sort">
<option value="hands">hands first</option>
<option value="light">lightest on samples</option>
<option value="bpm">slowest bpm first</option>
</select>
</label>
</div>
<div class="stat" id="stat"></div>
</div>
......@@ -230,7 +241,17 @@ function el(tag, cls, text){
}
/* ── filter state ──────────────────────────────────────────────────────── */
var filt = {q:"", area:"", warn:false, hands:false, warm:false};
var filt = {q:"", area:"", warn:false, hands:false, warm:false, sort:"hands"};
/* Sample WEIGHT is the axis PLN asked for by ear: "cyber hump and wap and
bombe dj not adequate too sample heavy its a nujazz/chill dnb set". Bank
count is the honest proxy — it is not loudness or busyness, just how many
distinct folders the track pulls from. */
var SORTS = {
hands: function(a,b){ return (b.n - a.n) || a.p.localeCompare(b.p); },
light: function(a,b){ return (a.b - b.b) || a.p.localeCompare(b.p); },
bpm: function(a,b){ return (a.m - b.m) || a.p.localeCompare(b.p); }
};
function matches(t){
if(t.v === 1 && !filt.warn) return false;
......@@ -268,7 +289,7 @@ function renderLib(){
var box = document.getElementById("lib");
box.textContent = "";
var hits = TRACKS.filter(matches);
var hits = TRACKS.filter(matches).sort(SORTS[filt.sort] || SORTS.hands);
var shown = hits.slice(0, CAP);
shown.forEach(function(t){
......@@ -287,6 +308,8 @@ function renderLib(){
if(t.b > 0 && t.w < t.b){
top.appendChild(el("span", "pill cold", (t.b - t.w) + " cold of " + t.b));
}
top.appendChild(el("span", "meta", t.m + " bpm · " + t.b + " bank" +
(t.b === 1 ? "" : "s")));
if(i >= 0) top.appendChild(el("span", "added", "in set #" + (i+1) + " — tap to remove"));
card.appendChild(top);
if(NOTES[t.p]) card.appendChild(el("div", "note", NOTES[t.p]));
......@@ -450,6 +473,9 @@ document.getElementById("handson").addEventListener("change", function(){
document.getElementById("warmon").addEventListener("change", function(){
filt.warm = this.checked; renderLib();
});
document.getElementById("sort").addEventListener("change", function(){
filt.sort = this.value; renderLib();
});
(function header(){
var c = CAT.counts || {};
......@@ -469,6 +495,10 @@ document.getElementById("warmon").addEventListener("change", function(){
" tracks), not all time.",
"N cold of M — sample banks the preload plan does not warm. Not a fault: they " +
"lazy-load on first hit.",
"bpm — what the track DECLARES via setcps, not a measured tempo: setcps 80 " +
"with everything on the half-note performs at 160.",
"banks — distinct sample folders the track pulls from. The closest the " +
"tooling gets to \u2018sample heavy\u2019; sort by it to find the light ones.",
(c.COMPILE_FAIL || 0) + " tracks that do not compile are not listed at all.",
"measured " + (CAT.generated || "?") + " from " + (CAT.source || "?")
].forEach(function(line){
......
......@@ -22,6 +22,14 @@ Signals, and what each one is worth:
played how many times the driver journal saw this track loaded. Journal
retention is short, so this is "seen in the window", not all-time —
but it is the only record of what PLN's hands actually reach for.
bpm what the track DECLARES via setcps. tempo-lens.py opens by warning
that setcps is not the tempo — a track at setcps 80 with everything
on the half-note performs at 160 — so this is a handle for picking an
arc, never a claim about how fast it sounds.
weight the bank count again, read as sample load. PLN, 2026-09-22: "cyber
hump and wap and bombe dj not adequate too sample heavy its a
nujazz/chill dnb set". A count of distinct banks is the honest proxy
the tooling can offer for that; his ear is the judge.
Sample-name extraction is imported from tools/setlist_samples.py, never
re-implemented here: a `.tidal` file is a program, and `#` is `|>`, so a
......@@ -42,7 +50,8 @@ from datetime import datetime, timezone
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from setlist_samples import extract_names, build_index, bank_file_count # noqa: E402
from setlist_samples import (extract_names, build_index, bank_file_count, # noqa: E402
declared_bpm)
REPO = Path(__file__).resolve().parent.parent
TSV = REPO / "docs/2026-09-22-catalog-playability.tsv"
......@@ -120,6 +129,7 @@ def main():
banks_warm += 1
stem = Path(rel).stem
area, sub = area_of(rel)
bpm = declared_bpm(src.read_text(errors="replace")) if src.is_file() else 0
rows.append({
"p": rel, # repo-relative path = the identity
"t": stem, # display title
......@@ -129,6 +139,7 @@ def main():
"b": banks_total,
"w": banks_warm,
"n": plays.get(stem, 0),
"m": round(bpm),
})
# Hands first, then warmth, then name — the order PLN would pick in.
......
......@@ -96,6 +96,30 @@ def bank_file_count(folder):
return n
# `setcps (BPM/60/4)` is the corpus's tempo idiom, so reading it belongs with the
# other corpus-syntax knowledge rather than in each caller. tools/lcxl3-driver.py
# carries an identical `parse_bpm` — it predates this one and it is load-bearing
# two days before a gig, so it is NOT being rewired tonight. Collapse it into this
# after Thursday; that is the whole debt.
#
# DECLARED, not measured. tempo-lens.py opens by warning that `setcps` is not the
# BPM: a track written at setcps 80 with everything on the half-note performs at
# 160. So this orders a set by what the file SAYS, which is the right handle for
# picking, and the wrong one for claiming a tempo.
_SETCPS = re.compile(r"setcps\s*\(?\s*([0-9.]+)\s*/\s*60")
def declared_bpm(text, default=120.0):
"""The BPM a track declares via setcps, or `default` when it declares none."""
m = _SETCPS.search(text)
if not m:
return default
try:
return float(m.group(1))
except ValueError:
return default
def read_setlist(path):
"""One track name/path per line; `#` comments and blank lines ignored.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment