"""Builds the Note Detect Bass Benchmark sloppak (v1). A bass-focused companion to the guitar benchmarks (note_detect_v1 + note_detect_v2). Same 90 BPM click, similar half-note pacing as v2, but the sections are built around what bass actually plays: single- note lines, octave jumps, walking patterns, sustained roots, and two-string double-stops (the closest bass gets to "chords"). Why a separate bass benchmark instead of toggling string count on the guitar one: - Tuning is different — 4-string bass open MIDI is [28, 33, 38, 43] (E1, A1, D2, G2) vs the guitar's [40, 45, 50, 55, 59, 64]. The benchmark needs to produce notes the player can actually play on the instrument they have plugged in. - Bass idioms are different from guitar idioms. Strumming sections don't apply; walking bass + octave patterns do. - Low-frequency detection is materially harder for YIN — E1 at ~41 Hz needs more accumulated samples for confident detection than guitar E2 at ~82 Hz. The benchmark should exercise that regime explicitly so we can spot regressions there. How to run inside the slopsmith container: docker cp docs/benchmarks/note_detect_bass_v1/build_benchmark.py \\ slopsmith-web-1:/tmp/build_benchmark_bass.py docker exec slopsmith-web-1 python /tmp/build_benchmark_bass.py \\ /app/static/sloppak_cache/note_detect_benchmark_bass_v1.sloppak After regenerating, copy the zip output to the tracked path with the `.sloppak` (not `.sloppak.zip`) suffix. """ import json import math import shutil import struct import subprocess import sys import wave from pathlib import Path import yaml # ── Benchmark parameters ──────────────────────────────────────────────── BPM = 90.0 SECONDS_PER_BEAT = 60.0 / BPM BEATS_PER_BAR = 4 BAR_S = BEATS_PER_BAR * SECONDS_PER_BEAT INTRO_BARS = 2 OUTRO_BARS = 2 EXERCISE_BARS = 8 # 4-string bass open MIDI per string, low → high. # Matches lib/tunings convention used by note_detect when the # arrangement is 'bass' and stringCount is 4. OPEN_MIDI = [28, 33, 38, 43] # E1 A1 D2 G2 N_STRINGS = 4 SR = 44100 # ── Click-track audio generator ──────────────────────────────────────── def _sine_burst(freq_hz, duration_s, amplitude): n = int(SR * duration_s) out = [] fade = max(1, int(0.004 * SR)) for i in range(n): env = 1.0 if i < fade: env = i / fade elif i >= n - fade: env = (n - 1 - i) / fade s = math.sin(2 * math.pi * freq_hz * (i / SR)) * amplitude * env out.append(max(-1.0, min(1.0, s))) return out def write_click_wav(path: Path, duration_s: float): total_samples = int(SR * duration_s) pcm = [0] * total_samples beat = 0 t = 0.0 while t < duration_s: is_downbeat = (beat % BEATS_PER_BAR == 0) freq = 1200 if is_downbeat else 800 amp = 0.6 if is_downbeat else 0.35 burst = _sine_burst(freq, 0.040, amp) start = int(t * SR) for i, s in enumerate(burst): j = start + i if 0 <= j < total_samples: pcm[j] = int(max(-1.0, min(1.0, pcm[j] / 32767 + s)) * 32767) t += SECONDS_PER_BEAT beat += 1 pcm = [struct.pack(' 1 else Path('./note_detect_benchmark_bass_v1.sloppak') build(out)