Commit 1edeecfe by PLN (Algolia)

fix(cale): a rounded tempo is a stretched loop, and three loops off one stem read alike

Screenshotted the Fred view instead of trusting it, and it showed two things.

The transport read **133 bpm on a 132.5 bpm kit**, because selecting a kit rounded its
tempo. Harmless in a display, not harmless here: the rack sets `rate = dur / (bars ×
barlen)`, so 133 stretches every loop in that kit by 0.4% and the mismatch goes straight
into the audio. Half-integer tempos are ordinary in this corpus. No rounding, and the
"match this kit" button now compares to 0.01 bpm so it stops offering a value it already
has.

And `fred_bighen_bass` listed three loops all labelled `bass`, two of them also both
"8 bars · 132.5 bpm" — the kit is named for the stem, so the stem name distinguishes
nothing. The manifest already knew `start_s`, so rows now carry `@2:14`: where in the
source track this loop was cut from, which is the thing you actually want when choosing
between two loops off one stem.
parent c45fddd0
......@@ -73,7 +73,10 @@ export default function KitAuditioner() {
const selectKit = useCallback((name: string, kitBpm?: number | null) => {
setKitSel(name)
setFocus(0)
if (kitBpm) setBpm(Math.round(kitBpm))
// NOT rounded. Half-integer tempos are normal in this corpus (BIG HEN is 132.5),
// and 133 would stretch every loop in the kit by 0.4% — `loopAt` is exact, so the
// rounding error goes straight into the audio.
if (kitBpm) setBpm(kitBpm)
}, [])
useEffect(() => {
......@@ -256,8 +259,8 @@ export default function KitAuditioner() {
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" />
</label>
{kit?.bpm && Math.round(kit.bpm) !== Math.round(bpm) && (
<button onClick={() => setBpm(Math.round(kit.bpm!))}
{kit?.bpm && Math.abs(kit.bpm - bpm) > 0.01 && (
<button onClick={() => setBpm(kit.bpm!)}
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)}
</button>
......
......@@ -30,6 +30,9 @@ type Props = {
const TAG_AXES = ['instrument', 'texture', 'mood'] as const
/** Where in the source track, so two loops off the same stem are tellable apart. */
const mmss = (s: number) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`
export function SampleRow({ s, playing, head, inRack, focused, onPlay, onStop, onRack, onFocus }: Props) {
const col = familyColor(s.family)
const bars = s.bars ?? 0
......@@ -54,8 +57,10 @@ export function SampleRow({ s, playing, head, inRack, focused, onPlay, onStop, o
<div className="truncate text-[11px] text-ink-faint leading-tight">
{bars > 0
? <>{bars} bar{bars > 1 ? 's' : ''} · {s.bpm?.toFixed(1)} bpm
{s.start_s != null && <> · @{mmss(s.start_s)}</>}
{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`}
{s.start_s != null && <> · @{mmss(s.start_s)}</>}</>}
</div>
</div>
......
......@@ -16,6 +16,7 @@ export type Sample = {
// present only for kits the Foundry cut — a hand-made kit has none of this
bars: number | null
bpm: number | null
start_s: number | null // where in the source track it was cut from
dur_s: number | null
tier: Tier | null
grade: number | null
......
......@@ -157,6 +157,9 @@ def build(roots: list[Path], mount_root: Path, cuts: dict[str, dict],
"env": env,
# from the manifest when the Foundry cut it, absent otherwise
"bars": c.get("bars"), "bpm": c.get("bpm"),
# where in the source track this was cut from: three loops off one stem
# are otherwise indistinguishable in a list, both named for the stem
"start_s": c.get("start_s"),
"dur_s": c.get("dur_s"), "tier": c.get("tier"),
"grade": c.get("grade"), "family": c.get("family"),
"stem_role": c.get("stem_role"), "track": c.get("track"),
......
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