Skip to content

feat(studio,cli): music beat detection with timeline guides + headless beats CLI#1424

Open
vanceingalls wants to merge 3 commits into
fix/parent-proxy-trimfrom
feat/music-beat-detection
Open

feat(studio,cli): music beat detection with timeline guides + headless beats CLI#1424
vanceingalls wants to merge 3 commits into
fix/parent-proxy-trimfrom
feat/music-beat-detection

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds music beat detection to HyperFrames. The Studio draws beat guides on the timeline; beats are user-editable and persist to a per-audio project file; and a new hyperframes beats CLI generates that file headlessly so guides are ready before the Studio is ever opened.

Beat detection is music-only: an <audio data-timeline-role="music"> (or an id like music/bgm/soundtrack) is analyzed; voiceover and other audio are excluded by the shared isMusicTrack rule.

What's included

Detection — @hyperframes/core/beats (shared by Studio + CLI)

  • Energy-onset detector cross-validated with bpm-detective. When the two tempos agree, beats are regularized to an octave-aligned grid (so a half-time reading doesn't double the density); otherwise raw onsets are used.
  • Silence gating drops beats in intro/outro/quiet regions.
  • Each beat carries a loudness 0–1 (local RMS ÷ track peak), used for dot/line brightness.
  • bpm-detective is loaded lazily (it touches window at module top-level) so the module is safe to import in Node/SSR/tests.

Studio

  • Full-height green beat lines behind the tracks + green dots on the music track's row (brightness/size scale with loudness).
  • Add beat at the playhead (drum-icon toolbar button next to split), drag to move, double-click to delete. Strength is always measured from the audio — never set by hand.
  • Editing (and playhead scrubbing) scrubs the music audio at 25% volume.
  • Undo/redo for beat ops, interleaved with the file-history stack by timestamp.
  • Edits persist to beats/<audio>.json in audio-file coordinates, so they survive trimming/moving the music clip and are matched back to the same audio if it's removed and re-added.
  • Clip edges snap to the beat grid (clamped to the clip's minDuration and source/composition limits).

CLI — hyperframes beats [dir]

  • Finds the music track, runs the same detection in headless Chrome (identical Web Audio decode + bpm-detective), and writes the beat file the Studio loads as-is.
  • The browser bundle is prebuilt into dist at CLI build time (build:beat-analyzer), so it works for published users without shipping source; a dev fallback bundles from core source.
  • Surfaces in-page errors, guards oversized inputs, and points at browser ensure when Chrome is missing.

Beat file format

{ "version": 1, "audio": "music.wav", "beats": [{ "time": 1.23, "strength": 0.8 }] }

Notes

  • Results are near-identical between CLI and Studio (both Chrome); a different headless audio sample rate can shift beat times by a frame or two.
  • examples/ is gitignored, so the demo composition/audio used during development isn't part of this PR.

Testing

  • core (1466), studio (798), cli typecheck/lint all green; pre-commit hooks (lint, format, fallow complexity gate, filesize, typecheck) pass.
  • Verified end-to-end: hyperframes beats on a demo composition analyzes the music track only (voiceover excluded) and writes a file the Studio reloads with edits intact.

🤖 Generated with Claude Code

Timeline UX refinements (follow-up)

  • Center-anchored magnify — zooming the timeline via the toolbar/slider keeps the time at the viewport center fixed instead of anchoring at the left edge; pinch-to-zoom still anchors at the cursor.

  • Move-snap to beats — dragging a clip snaps whichever edge (start or end) is nearest a beat, matching the existing resize-edge snapping.

  • Beat lines on track backgrounds — faint full-height beat lines now paint behind the clips on every track lane (brightness scales with loudness); the green dots stay on the active track's top bar.

  • Waveform follows zoom — waveform bars fill the full clip width and resample the windowed peaks, so the waveform stretches with zoom instead of stopping partway across a widened clip.

  • Dots centered in the top bar — the dot band aligns to the clip top so the dots sit vertically centered in the dark top bar instead of being bisected by the clip's selection border.

  • Trim survives moving other clips — re-applies the cached probe sourceDuration on every element re-derivation, so moving a non-music clip no longer drops the music clip's source length (which made the trimmed waveform reset to the full source pinned at the track start).

@mintlify

mintlify Bot commented Jun 14, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
hyperframes 🟢 Ready View Preview Jun 14, 2026, 2:22 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

// blob:/data: URLs have no stable identity across sessions — not persistable.
if (/^(blob:|data:)/i.test(src)) return null;
// Studio preview URLs: /api/projects/<id>/preview[/comp]/<relpath>
const previewMatch = src.match(/\/preview\/(?:comp\/)?(.+?)(?:[?#].*)?$/);
@vanceingalls vanceingalls force-pushed the feat/music-beat-detection branch from 4711e7b to f355498 Compare June 14, 2026 07:09
@vanceingalls vanceingalls changed the base branch from main to fix/parent-proxy-trim June 14, 2026 07:10

Copy link
Copy Markdown
Collaborator Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

…s beats CLI

Beat detection for music tracks: the Studio draws beat guides on the active
track, beats are user-editable and persist to a project file, and a new
`hyperframes beats` CLI generates that file headlessly before the Studio opens.

Detection lives in @hyperframes/core/beats (shared by Studio + CLI): an energy
onset detector cross-validated with bpm-detective, regularized to an octave-
aligned grid, silence-gated, with per-beat loudness. Music-only — an
<audio data-timeline-role="music"> is analyzed; voiceover is excluded.

Studio: green beat lines + draggable dots on the selected track; add at playhead,
drag to move, double-click to delete (audio scrubs); edits persist to
beats/<audio>.json and are undoable (interleaved with file history).

CLI: `hyperframes beats [dir]` runs the same detection in headless Chrome
(prebuilt browser bundle in dist) and writes the beat file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
vanceingalls and others added 2 commits June 14, 2026 01:37
- Center-anchored magnify: zooming via the toolbar/slider keeps the time
  at the viewport center fixed instead of anchoring at the left. Pinch
  still anchors at the cursor.
- Move-snap to beats: dragging a clip snaps whichever edge (start or end)
  is nearest a beat, matching the existing resize-edge snapping.
- Beat lines on track backgrounds: faint full-height beat lines now paint
  behind the clips on every track lane (brightness scales with loudness);
  the green dots stay on the active track's top bar.
- Waveform follows zoom: bars fill the full clip width and resample the
  windowed peaks, so the waveform stretches with zoom instead of stopping
  partway across a widened clip.
- Beat dots centered in the top bar: align the dot band to the clip top
  (CLIP_Y) so the dots sit centered in the dark bar instead of being
  bisected by the clip's top border.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
Moving a non-music clip re-derived the timeline elements into fresh
objects whose sourceDuration the DOM scan hadn't loaded yet. The async
probe skips srcs already in its cache, so the value was silently
dropped — trimFractions then returned no window and the trimmed music
waveform reset to the full source pinned at the track start.

Re-apply the cached probe duration synchronously on every derivation
(applyCachedSourceDurations) and extract the async probe loop into
probeMissingSourceDurations to keep useTimelinePlayer within the file
size limit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
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