Skip to content

viz: music-synced timeline playback + onset-timings extraction#65

Open
kumavis wants to merge 7 commits into
claude/charming-archimedes-98yb48from
claude/music-synced-playback-fpv9si
Open

viz: music-synced timeline playback + onset-timings extraction#65
kumavis wants to merge 7 commits into
claude/charming-archimedes-98yb48from
claude/music-synced-playback-fpv9si

Conversation

@kumavis

@kumavis kumavis commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Targets the PR #64 branch (claude/charming-archimedes-98yb48).

What this adds

An alternate play mechanism for the propagator-network visualizer that advances the execution timeline in time with a song — load an audio file + a timings file (hit onsets in seconds, e.g. a song's xylophone hits), play the audio, and each onset advances one step, so the on-network reduction plays back synced to the music.

It sits next to the existing timer-based control and is fully independent (starting the audio pauses the timer).

tools/viz/index.html

  • parseTimings + stepForTime — pure, node-verified helpers in the @PURE core.
  • App-level audio/timings state + a requestAnimationFrame loop mapping audio.currentTime → step index via the onset list (binary search).
  • A music-synced playback sidebar panel: audio + timings file pickers, an inline <audio> element, and a live status readout.
  • Legend now sticks to the bottom of the sidebar (sticky-bottom + flex column), per the request.

tools/viz/extract-song-timings.py

URL/file → yt-dlpffmpegspectral-flux onset detection → timings JSON. Tuned for sharp percussive (xylophone) hits, with --threshold / --min-gap knobs. numpy only — no librosa.

tools/viz/examples/

A small, copyright-free synthetic xylophone demo (demo-xylophone.mp3 + demo-xylophone.timings.json) plus make-demo-song.py to regenerate it. The detector recovers all 13 onsets within ~30 ms, so the demo doubles as an end-to-end pipeline check. Drop a vizTrace, then load these two files in the music panel and press play.

Timings file format

Accepts a bare JSON array [0.30, 0.62, …], an object keyed by hits/timings/beats/onsets/times, or whitespace/comma/newline-separated plain text.

On the YouTube extraction

The request was to pull audio from https://youtu.be/PoH5hC5PzSQ and extract xylophone-hit timings. I built and validated the full yt-dlp → ffmpeg → onset-detection pipeline, but the actual download is blocked from this environment: YouTube serves only PO-token/DRM-gated formats to the datacenter IP and falls back to "confirm you're not a bot." The pipeline runs cleanly on a machine with a logged-in browser via --cookies-from-browser:

python3 tools/viz/extract-song-timings.py 'https://youtu.be/PoH5hC5PzSQ' \
    -o song.timings.json --keep-audio song.mp3 --cookies-from-browser chrome

So the committed demo timings are the synthetic phrase (clearly labeled), and the real song's timings are one command away on a browser-equipped machine.

Verification

  • node tools/viz/check.js tools/viz/index.htmlALL CHECKS PASS (incl. new parseTimings/stepForTime unit tests).
  • The <script type="text/babel"> block transforms cleanly under @babel/preset-react (no JSX/syntax errors).
  • Onset detector round-trips the demo: 13/13 known onsets recovered within ~30 ms.

https://claude.ai/code/session_01HcdtkcuDMUFokytmRhkQcK


Generated by Claude Code

claude added 7 commits June 15, 2026 20:23
Add an alternate play mechanism to the propagator-network visualizer that
advances the execution timeline in time with a song: load an audio file and a
timings file (hit onsets in seconds), play the audio, and each onset advances
one step. Runs alongside the existing timer ▶ control (independent; starting
audio pauses the timer).

- index.html: parseTimings + stepForTime pure helpers (node-verified in
  check.js); App-level audio/timings state + a requestAnimationFrame loop that
  maps audio.currentTime → step index via the onsets; a MusicSync sidebar panel
  (audio + timings file pickers, an <audio> element, status readout).
- Legend now sticks to the bottom of the sidebar (sticky bottom + flex column).
- extract-song-timings.py: URL/file → yt-dlp → ffmpeg → spectral-flux onset
  detection → timings JSON. Tuned for sharp percussive (xylophone) hits; numpy
  only, no librosa.
- examples/: a small copyright-free synthetic xylophone demo (mp3 + timings)
  plus make-demo-song.py to regenerate it; the detector recovers all 13 onsets,
  so it doubles as an end-to-end pipeline check.
- README.md documenting playback, timings format, extraction, and the demo.

Timings format accepts a bare JSON array, an object keyed by
hits/timings/beats/onsets/times, or whitespace-separated plain text.
The actual song audio was provided, so extract real timings from it.

- extract-song-timings.py: add --hifreq, weighting spectral flux toward high
  frequencies (>~1.5 kHz) to isolate bright mallet/xylophone/glockenspiel hits
  from bass/kick/vocals.
- examples/praise-the-lamb.timings.json: 1549 --hifreq onsets from
  Cult of the Lamb — Praise the Lamb (youtu.be/PoH5hC5PzSQ). Audio not committed
  (copyright); pair with your own copy in the music panel.
- README: document --hifreq and the real timings file.
…trings)

Replace the crude --hifreq flag (>1.5 kHz, which would lock onto the
arpeggiated strings) with frequency-band selection. Spectrum analysis of the
opening hit (xylophone note + cymbal crash) shows the xylophone fundamental at
~145 Hz, so the melodic line sits low.

- extract-song-timings.py: --band-low / --band-high (Hz) restrict the spectral
  flux to one instrument's band; isolates the low xylophone from kick/bass below
  and strings above.
- examples/praise-the-lamb.timings.json: regenerated in the low band
  (--band-low 120 --band-high 520). Band/threshold still being auditioned
  against the audio — may be refined.
- README: document band selection.
Confirmed by audio audition: band 110–300 Hz (threshold 1.5, min-gap 0.11)
tracks the low xylophone. Regenerate examples/praise-the-lamb.timings.json
(1690 onsets) and set the README example to the confirmed settings.
The 110–300 Hz band still caught quiet bass/percussion bleed (~4.7/s); the
real low xylophone is ~1-2 notes/s ("bong bing bong bing"). Add a --min-strength
loudness floor (fraction of the 99.5th-pct flux) that keeps only the loud mallet
hits while preserving the close bong+bing pairs (unlike a larger min-gap, which
merges them).

- extract-song-timings.py: add --min-strength gate.
- examples/praise-the-lamb.timings.json: regenerate with
  --band-low 110 --band-high 300 --min-strength 0.5 --min-gap 0.12 → 634 onsets
  (~1.8/s; intro ~1.7/s, clean bong-bing-bong-bing).
- Untrack a stray __pycache__/*.pyc and gitignore Python bytecode.
- README: document --min-strength and the updated settings.
…cking

The low xylophone is metronomic but quieter than the kick/snare on the loud
beats, so onset-picking grabbed the loud off-beat transients and missed the
quiet xylophone notes (e.g. detected 1.44/1.67/2.32 where the real notes are
1.90/2.80). Waveform ground truth showed a steady ~0.89s pulse.

Add --grid mode: estimate tempo from the band onset-envelope autocorrelation,
anchor the phase to the instrument's first hit (--phase; blind phase-finding
locks onto the louder beat), and emit phase + k·period. Joint period+phase
refinement keeps a constant-tempo track aligned with no drift over its length
(verified to ±15ms at the 5-minute mark).

- extract-song-timings.py: band_onset_envelope + beat_grid; --grid/--bpm/--phase
  /--tempo-min/--tempo-max; record mode/bpm/period/phase in the JSON.
- examples/praise-the-lamb.timings.json: regenerate in --grid mode → 407 beats
  @ 67.5 BPM (~1.1/s), matching the waveform (0.08, 0.97, 1.86, 2.75, 3.64…).
- README: document beat-grid mode and the tradeoff (pulses through rests).
Alternate playback take: hold the slow xylophone pulse through the intro, then
rush at the drop. Add --segments 'startSec:bpm,...' for a piecewise-constant beat
grid; a tempo switch lands a beat exactly on the boundary.

- extract-song-timings.py: tempo_map() + --segments; record segments in the JSON.
- examples/praise-the-lamb-tempomap.timings.json: '0:67.5,28.5:675' → slow 67.5
  BPM intro, 10x (675 BPM) burst from 28.5s (3773 beats).
- README: document --segments.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants