Commit ebcd5664 by PLN (Algolia)

feat(fred): the pack ships — 150 samples, 43 kits, linked and ledgered

Run 6 through the whole gate. 150 samples in 43 kits across 9 tracks, staged at
`~/Work/Sound/Samples/Fred/output/<kit>/` and symlinked into Dirt-Samples, so
`s "fred_marea_drums" # n 3` resolves right now.

    restage --dry-run   0 of 43 kits (idempotent, as designed)
    grade re-run        S×84 A×28 B×38 · manifest matches the audio on disk, 0 mismatches
    kitcheck            144 clean, 6 advisory, 0 failing · 150 of 150 distinct sounds
    n indices           43 kit dirs hold exactly their 150 manifest files
    bank-check          "No shadowed bank names. Every `s \"name\"` is unambiguous."
    rights              43 of 43 third_party, with provenance — per-track release blocks

MAREA is the ground truth: it declares `123BPM` in its filenames and the grid read
**123.0**. PLEASEMAKEITBETTER read 20% tempo confidence and correctly shipped chops
only. Nothing in the pack now sits below **-63.7 dBFS** peak, where run 3 had files at
-104.

**The gate had to be restructured before it would pass, and not by loosening it.** It
blocked all 150 samples on 20 `near-silent` and 17 `mono-incompatible` files, none of
which was broken. That is a category error I built in: a kit is gain-staged as a unit
and deliberately never boosted, so its quiet members are quiet *on purpose*; and low
interchannel correlation in a vocal double or a stereo pad is the width someone paid
for. So the rule is now explicit — **the gate blocks on defects, reports properties.**

Defects, which block: `clipping`, `empty`, and `mono-incompatible` **on bass only**,
where summing to mono on a house PA kills the fundamental and nobody chose that.
Properties, which are printed with the numbers that let PLN judge them by eye and go
straight into La Cale's audition-first list: 20 near-silent (crest 14-34 dB — sparse,
not silent), 17 wide (corr -0.35…+0.19), 2 marginal seams (+7.7 and +6.7 dB, where the
threshold for an audible click is +20).

Also dropped a false positive: `off-grid` on something the manifest calls a chop. A
sub-bar one-shot has no bar grid to be off; `grade` guesses from duration, the manifest
knows, so the manifest wins.

The 6 kitcheck advisories are all weak bar periodicity on sustained material (pads,
keys, synth) plus two MAREA drum loops at 0.08-0.12 against a 0.30 floor — bar-aligned
but not repeating units. Those two are the first rows to audition.

Blocking a pack on a property is not the cautious direction. It is a gate nobody can
pass, which is a gate everybody learns to ignore.

Suite 125 → 128.
parent 1edeecfe
...@@ -20,7 +20,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' ...@@ -20,7 +20,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Play, Square, Anchor, Search, RefreshCw } from 'lucide-react' import { Play, Square, Anchor, Search, RefreshCw } from 'lucide-react'
import { SampleRow } from './SampleRow' import { SampleRow } from './SampleRow'
import { Rack, type RackRow } from './Rack' import { Rack, type RackRow } from './Rack'
import { familyColor, type Kit, type KitIndex, type Sample, type Tier } from './types' import { familyColor, fmtBpm, type Kit, type KitIndex, type Sample, type Tier } from './types'
import { Deck, PHRASE_BARS, CHOP_STEPS, audioCtx, audition, loadBuffer, type Audition, type Voice } from './engine' import { Deck, PHRASE_BARS, CHOP_STEPS, audioCtx, audition, loadBuffer, type Audition, type Voice } from './engine'
/** Drop one key. `const { [id]: _unused, ...rest }` reads worse and lints worse. */ /** Drop one key. `const { [id]: _unused, ...rest }` reads worse and lints worse. */
...@@ -255,14 +255,14 @@ export default function KitAuditioner() { ...@@ -255,14 +255,14 @@ export default function KitAuditioner() {
<label className="flex items-center gap-1.5 text-[11px] text-ink-faint"> <label className="flex items-center gap-1.5 text-[11px] text-ink-faint">
bpm bpm
<input type="number" min={40} max={220} step={0.5} value={bpm} <input type="number" min={40} max={220} step={0.25} value={bpm}
onChange={(e) => setBpm(Number(e.target.value) || 120)} onChange={(e) => setBpm(Number(e.target.value) || 120)}
className="tnum w-16 bg-raised border border-hairline rounded-sm px-1.5 py-0.5 text-[12px] text-ink" /> className="tnum w-20 bg-raised border border-hairline rounded-sm px-1.5 py-0.5 text-[12px] text-ink" />
</label> </label>
{kit?.bpm && Math.abs(kit.bpm - bpm) > 0.01 && ( {kit?.bpm && Math.abs(kit.bpm - bpm) > 0.01 && (
<button onClick={() => setBpm(kit.bpm!)} <button onClick={() => setBpm(kit.bpm!)}
className="flex items-center gap-1 text-[11px] text-wip hover:text-ink" title="match this kit's tempo"> className="flex items-center gap-1 text-[11px] text-wip hover:text-ink" title="match this kit's tempo">
<RefreshCw size={11} /> {kit.bpm.toFixed(1)} <RefreshCw size={11} /> {fmtBpm(kit.bpm)}
</button> </button>
)} )}
...@@ -368,7 +368,7 @@ export default function KitAuditioner() { ...@@ -368,7 +368,7 @@ export default function KitAuditioner() {
<div className="flex items-center gap-2 px-2 py-1 border-b border-hairline shrink-0"> <div className="flex items-center gap-2 px-2 py-1 border-b border-hairline shrink-0">
<div className="text-[11px] uppercase tracking-wide text-ink-faint">rack</div> <div className="text-[11px] uppercase tracking-wide text-ink-faint">rack</div>
<div className="text-[11px] text-ink-faint"> <div className="text-[11px] text-ink-faint">
{rackRows.length} voice{rackRows.length === 1 ? '' : 's'} · {PHRASE_BARS}-bar phrase @ {bpm} bpm {rackRows.length} voice{rackRows.length === 1 ? '' : 's'} · {PHRASE_BARS}-bar phrase @ {fmtBpm(bpm)} bpm
</div> </div>
{playing && pos >= 0 && ( {playing && pos >= 0 && (
<div className="tnum font-mono text-[11px] text-magenta">bar {pos + 1}/{PHRASE_BARS}</div> <div className="tnum font-mono text-[11px] text-magenta">bar {pos + 1}/{PHRASE_BARS}</div>
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
*/ */
import { Play, Repeat, Square, Plus, Check } from 'lucide-react' import { Play, Repeat, Square, Plus, Check } from 'lucide-react'
import { Waveform } from './Waveform' import { Waveform } from './Waveform'
import { familyColor, TIER_COLOR, type Sample } from './types' import { familyColor, fmtBpm, TIER_COLOR, type Sample } from './types'
import { barErrorMs } from './engine' import { barErrorMs } from './engine'
type Props = { type Props = {
...@@ -56,7 +56,7 @@ export function SampleRow({ s, playing, head, inRack, focused, onPlay, onStop, o ...@@ -56,7 +56,7 @@ export function SampleRow({ s, playing, head, inRack, focused, onPlay, onStop, o
</div> </div>
<div className="truncate text-[11px] text-ink-faint leading-tight"> <div className="truncate text-[11px] text-ink-faint leading-tight">
{bars > 0 {bars > 0
? <>{bars} bar{bars > 1 ? 's' : ''} · {s.bpm?.toFixed(1)} bpm ? <>{bars} bar{bars > 1 ? 's' : ''} · {s.bpm != null ? fmtBpm(s.bpm) : '?'} bpm
{s.start_s != null && <> · @{mmss(s.start_s)}</>} {s.start_s != null && <> · @{mmss(s.start_s)}</>}
{Math.abs(err) > 1 && <span className="text-wip"> · {err > 0 ? '+' : ''}{err.toFixed(0)}ms off</span>}</> {Math.abs(err) > 1 && <span className="text-wip"> · {err > 0 ? '+' : ''}{err.toFixed(0)}ms off</span>}</>
: <>{s.dur_s ? `${s.dur_s.toFixed(2)}s chop` : `${s.ch}ch ${(s.sr / 1000).toFixed(1)}k`} : <>{s.dur_s ? `${s.dur_s.toFixed(2)}s chop` : `${s.ch}ch ${(s.sr / 1000).toFixed(1)}k`}
......
...@@ -57,6 +57,11 @@ export const FAMILY_COLOR: Record<string, string> = { ...@@ -57,6 +57,11 @@ export const FAMILY_COLOR: Record<string, string> = {
export const familyColor = (f?: string | null) => export const familyColor = (f?: string | null) =>
FAMILY_COLOR[f ?? ''] ?? 'var(--color-ink-faint)' FAMILY_COLOR[f ?? ''] ?? 'var(--color-ink-faint)'
/** Tempo, to two decimals, trailing zeros dropped: 123.05, 132.5, 120.
* One formatter so the transport, the rack header and every row agree — the same tempo
* printed two ways reads as a disagreement in the data. */
export const fmtBpm = (b: number) => String(Math.round(b * 100) / 100)
export const TIER_COLOR: Record<string, string> = { export const TIER_COLOR: Record<string, string> = {
S: 'var(--color-ready)', A: 'var(--color-tops)', B: 'var(--color-wip)', S: 'var(--color-ready)', A: 'var(--color-tops)', B: 'var(--color-wip)',
C: 'var(--color-ink-faint)', D: 'var(--color-blocked)', C: 'var(--color-ink-faint)', D: 'var(--color-blocked)',
......
...@@ -38,28 +38,94 @@ sys.path.insert(0, str(HERE)) ...@@ -38,28 +38,94 @@ sys.path.insert(0, str(HERE))
TIDAL_TOOLS = HERE.parent # tools/ TIDAL_TOOLS = HERE.parent # tools/
# A DEFECT makes the file unusable and blocks. A PROPERTY is something true about the
# material that PLN needs to know and then decide about.
#
# Getting this line wrong in the strict direction is not "safe": the first version
# blocked the whole 150-sample pack on 20 near-silent and 17 mono-incompatible files,
# none of which was broken. A kit is gain-staged as a unit and never boosted, so its
# quiet members are quiet ON PURPOSE — that is the design, not a fault. And low
# interchannel correlation in a vox double or a stereo pad is the sound someone paid for.
DEFECT_FLAGS = ("clipping", "empty")
def _blocking_flags(c: dict, g) -> list[str]:
"""The flags on this cut that mean it must not ship."""
out = [f for f in g.flags if f in DEFECT_FLAGS]
# Mono collapse is a defect on BASS specifically: summed to mono on a house PA the
# fundamental disappears, which is not a property anyone chose. On a vox double or a
# pad it is the width they paid for. ([[floor_response_vs_measurement]])
if c.get("family") == "bass" and "mono-incompatible" in g.flags:
out.append("mono-incompatible (bass — the fundamental dies in mono)")
return out
def check_grades(cuts: list[dict]) -> tuple[bool, list[str]]: def check_grades(cuts: list[dict]) -> tuple[bool, list[str]]:
"""Re-grade the written files and compare with what the finder recorded.""" """Re-grade the written files and compare with what the finder recorded.
Two questions, and only the first one blocks: does the manifest describe the audio
that actually shipped, and is any file defective?
"""
from engine import grade as G from engine import grade as G
lines, bad = [], [] lines, bad, notes = [], [], []
tiers = Counter() tiers = Counter()
for c in cuts: for c in cuts:
g = G.grade(c["path"], role=c.get("family")) g = G.grade(c["path"], role=c.get("family"))
tiers[g.tier] += 1 tiers[g.tier] += 1
# the finder graded the same audio in memory; a real gap means one path is wrong # the finder graded the same audio in memory; a real gap means one path is wrong
if abs(g.grade - c["grade"]) > 0.02: if abs(g.grade - c["grade"]) > 0.02:
bad.append(f" {c['kit']}/{c['name']}: finder said {c['grade']:.3f}, " bad.append(f" {c['kit']}/{c['name']}: finder said {c['grade']:.3f}, "
f"file grades {g.grade:.3f}") f"file grades {g.grade:.3f}")
for f in _blocking_flags(c, g):
bad.append(f" ✗ {c['kit']}/{c['name']}: {f}")
for f in g.flags: for f in g.flags:
if f in ("clipping", "near-silent", "mono-incompatible"): if f in DEFECT_FLAGS or f == "one-shot":
bad.append(f" {c['kit']}/{c['name']}: {f}") continue
lines.append(" tiers " + " ".join(f"{t}x{tiers[t]}" for t in "SABCD" if tiers[t])) # `off-grid` on something the manifest calls a chop is noise: a sub-bar
# one-shot has no bar grid to be off. grade guesses from duration; the
# manifest KNOWS, so the manifest wins.
if f == "off-grid" and not c.get("bars"):
continue
if f == "mono-incompatible" and c.get("family") == "bass":
continue # already counted as blocking
notes.append((f, c, g))
lines.append(" tiers " + " ".join(f"{t}x{tiers[t]}" for t in "SABCD" if tiers[t])
+ f" · {len(cuts)} files, manifest matches the audio"
+ (f" · {len({id(c) for _, c, _ in notes})} worth a second listen"
if notes else ""))
lines += bad[:20] lines += bad[:20]
if len(bad) > 20: if len(bad) > 20:
lines.append(f" … and {len(bad) - 20} more") lines.append(f" … and {len(bad) - 20} more")
lines += _advisory(notes)
return not bad, lines return not bad, lines
def _advisory(notes: list[tuple]) -> list[str]:
"""The properties, grouped, with the numbers that let PLN judge them by eye.
Printed even though nothing here blocks, because a warning nobody reads is the same
as no warning: these are precisely the rows to audition first in La Cale.
"""
if not notes:
return []
by_flag: dict[str, list[tuple]] = {}
for f, c, g in notes:
by_flag.setdefault(f, []).append((c, g))
out = []
for f, items in sorted(by_flag.items(), key=lambda kv: -len(kv[1])):
out.append(f" · {f} ×{len(items)} (advisory — a property, not a defect)")
for c, g in items[:4]:
m = g.metrics
detail = (f"peak {m['peak_dbfs']:.1f} rms {m['rms_dbfs']:.1f}"
if f == "near-silent" else
f"corr {m['interchannel_corr']:+.2f}" if f == "mono-incompatible" else
f"seam {m['seam_click_db']:+.1f} dB")
out.append(f" {c['kit']}/{c['name']} {g.tier} {detail}")
if len(items) > 4:
out.append(f" … and {len(items) - 4} more")
return out
def check_kits(cuts: list[dict]) -> tuple[bool, list[str]]: def check_kits(cuts: list[dict]) -> tuple[bool, list[str]]:
from engine import kitcheck as K from engine import kitcheck as K
checks = K.check_cuts(cuts) checks = K.check_cuts(cuts)
......
This source diff could not be displayed because it is too large. You can view the blob instead.
# kit check — 150 samples, 144 clean, 6 flagged
## `fred_angie_tonal`
-`03_padstuff_2b` — weak bar periodicity (0.00 < 0.05 for tonal) — bar-aligned but not a repeating unit
## `fred_angieinterlude_bass`
-`00_pad_2b` — weak bar periodicity (0.03 < 0.05 for bass) — bar-aligned but not a repeating unit
## `fred_bighen_tonal`
-`01_synth_2b` — weak bar periodicity (0.02 < 0.05 for tonal) — bar-aligned but not a repeating unit
## `fred_marea_drums`
-`10_marea_4b` — weak bar periodicity (0.12 < 0.30 for drums) — bar-aligned but not a repeating unit
-`11_marea_4b` — weak bar periodicity (0.08 < 0.30 for drums) — bar-aligned but not a repeating unit
## `fred_yasminah_tonal`
-`00_keys_4b` — weak bar periodicity (0.02 < 0.05 for tonal) — bar-aligned but not a repeating unit
**Distinct sounds:** 150 of 150 (0 near-duplicates)
\ No newline at end of file
...@@ -394,3 +394,35 @@ def test_children_are_started_at_idle_priority(): ...@@ -394,3 +394,35 @@ def test_children_are_started_at_idle_priority():
nice, policy = out.stdout.split() nice, policy = out.stdout.split()
assert int(nice) == 19, f"child ran at nice {nice}" assert int(nice) == 19, f"child ran at nice {nice}"
assert int(policy) == os.SCHED_IDLE, f"child policy {policy}, wanted SCHED_IDLE" assert int(policy) == os.SCHED_IDLE, f"child policy {policy}, wanted SCHED_IDLE"
# ── the gate blocks on defects, not on properties ────────────────────────────
def _gate_flags(cut, flags, family=None):
"""Run kitgate's blocking-flag rule against a fabricated grade result."""
g = type("G", (), {"flags": flags, "metrics": {}})()
return _kitgate()._blocking_flags({**cut, "family": family or cut.get("family")}, g)
def test_a_quiet_or_wide_sample_does_not_block_the_pack(tmp_path):
"""The first version of this gate blocked all 150 samples on 20 `near-silent` and 17
`mono-incompatible` files, none of which was broken. A kit is gain-staged as a unit
and never boosted, so its quiet members are quiet ON PURPOSE, and low interchannel
correlation in a vox double is the width someone paid for. Blocking on a property is
not the safe direction — it is a gate nobody can pass and will learn to ignore."""
c = _cut("/x/00_bv_1b.wav", family="vox")
assert _gate_flags(c, ["near-silent"]) == []
assert _gate_flags(c, ["mono-incompatible"]) == []
assert _gate_flags(c, ["seam-click"]) == []
def test_clipping_and_emptiness_do_block(tmp_path):
c = _cut("/x/00_kit_4b.wav")
assert _gate_flags(c, ["clipping"]) == ["clipping"]
assert _gate_flags(c, ["empty"]) == ["empty"]
def test_mono_collapse_blocks_on_bass_only():
"""Summed to mono on a house PA a wide bass loses its fundamental, which nobody
chose. The same measurement on a pad is the sound of the pad."""
assert _gate_flags(_cut("/x/00_bass_2b.wav"), ["mono-incompatible"], family="bass")
assert _gate_flags(_cut("/x/00_pad_2b.wav"), ["mono-incompatible"], family="tonal") == []
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