Count-in, and a transport footer rebuilt around the studio's column grid - #369
Merged
Conversation
added 6 commits
August 16, 2026 01:39
…ort footer Count-in (#269): one bar of click count-in leads into playback and into audio exports, independent of the running click track (a clean backing track can still get a count-in). The lead-in math is defined once and mirrored between metronome.js and click_render.py, pinned by parity tests on both sides. - Playback: audioEngine schedules stem playback on a future ctx-time start so the count-in clicks land in the silent gap before the song begins; the metronome schedules them through the same clock mapping the running click already uses. - Export: stems are delayed via ffmpeg's adelay and the click WAV is rendered in output coordinates when a count-in is requested, so it isn't re-trimmed by the region -ss like a plain click. Also rebuilds the transport footer around labelled control groups (Transport, Position, Speed, Click Track) instead of a right-click popover: playback speed collapses to three practice presets (0.25x / 0.5x / 1x), the click track gets an on/off toggle and a count-in switch, and the track-info block collapses from four stacked detail rows to one compact line.
The panel lost its default "hidden" class when it changed from a right-click popover to always-inline (#269 follow-up) -- on a fresh page load, before any track was ever picked, nothing forced it hidden, so "Ready to import a track" showed a full set of live- looking click controls for a track that didn't exist.
…olume readout - Time labels above the footer's mini waveform, matching the main ruler. - Divider marks between control clusters in the footer's controls row, hidden via ResizeObserver when wrapping strands one at the end of a line with nothing after it to separate. - Click volume percentage shown next to the slider again instead of screen-reader-only -- a level you can only learn by hovering isn't one you can reliably match between sessions. - Count-in switched from a checkbox to a press-to-toggle button, matching the click on/off control beside it (both answer "is this on for the next play?", so they read as the same kind of control now).
The chunked engine is the default playback path (engineMode() falls back to "chunked" unless a debug localStorage flag forces "fulldecode") -- but count-in support (play(leadIn), supportsCountIn, a clamped getCurrentTime during the lead-in) was only ever added to audioEngine.js, the full-decode path. Since _armCountIn() bails out whenever eng.supportsCountIn is falsy, count-in silently never armed for any track played through the engine essentially everyone actually uses, and playback started immediately regardless of the toggle. Mirrors the same fix in chunkedAudioEngine.js: play() accepts a leadIn and schedules the first chunk that far in the future (falling back to the existing 10ms/50ms margins when there is no count-in), and getCurrentTime() clamps to the start offset during that gap instead of reading negative. Verified directly against the running engine clock (not just DOM text, which rounds to whole seconds): the position holds at the start offset for the full lead-in and then advances normally, pausing mid-count-in stops cleanly with no phantom scheduled audio, and replaying re-arms a fresh count-in.
…to it The footer's waveform strip ran the full width of the window while the lane waveforms above it start after the 300px stems/mixer panel, so the same position sat at two different x positions in the two strips and neither ruler's ticks lined up with the other's. The footer is now two columns on the studio's own grid. Everything time-related -- the control clusters, the waveform, its ruler and the detection note -- sits in the right column and starts exactly where the lane waveforms start, running flush to the window edge like they do. The track identity (art, title, meta, favourite, Export Mix) moves into the left column under the mixer panel and shares its width and 14px padding, so titles, stem names and the "Mixer" heading share one left edge down the page. That also drops a whole row from the footer: 255px tall where the three stacked tiers were 318px. - The 300px is now --daw-col-w, read by the stems panel, the label cell above it and the footer, instead of being hardcoded in each. - The waveform strip is full-bleed with top/bottom rules rather than a rounded inset panel: a side border would have offset the canvas by its own width, which is exactly the misalignment being fixed. - Both rulers share tickStep(), so a time is labelled at the same x in each. - The export menu opens up and to the right; right-aligned from the left column it would have hung over the sidebar. Grid becomes a press-to-toggle button matching the click and count-in buttons beside it -- click opens the editor and lights it, click again closes it. Its lit state is synced inside toggleBeatGridEditor, the one place every open and close runs through, so Done, Escape and losing the beat grid all leave the button correct. The G shortcut is gone: the button says what it does now, and a single letter bound to a modal editor is easy to hit by accident.
The strip carries only top and bottom rules -- side borders were dropped so the canvas would land exactly on the lane waveforms' left edge -- which left its left end open, the two rules stopping in mid-air. Drawn as an outset box-shadow rather than a border-left: a border sits inside the box and would push the canvas a pixel off the alignment it exists to keep. The line falls on the same x as the stems panel's right border, so that seam now runs unbroken from the top of the mixer to the bottom of the strip.
This was referenced Aug 16, 2026
Every interactive element in the export menu called stopPropagation so the
document-level dismiss handler would not fire, but the two option checkboxes
had no click handler at all -- so ticking one bubbled out and closed the menu
under the pointer.
That was survivable with one checkbox. This branch adds a second ("Add
count-in"), and wanting both is the normal case for practising to a click:
the first tick closed the menu, and the second needed it reopened.
Guard the panel itself rather than adding a third per-element stopPropagation
that the next option added would forget: a click inside a menu is not a click
away from it. Nothing depended on the bubble to close the menu -- the export
actions close it themselves through enterBusy() -> closePanel().
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #269.
Closes #370.
Closes #371.
Count-in (#269)
One bar of click leads into playback and into audio exports, independent of
the running click track — a clean backing track can still get a count-in. The
lead-in math is defined once and mirrored between
metronome.jsandclick_render.py, pinned by parity tests on both sides.audioEngineschedules stem playback on a future ctx-timestart so the count clicks land in the silent gap before the song begins.
adelayand the click WAV isrendered in output coordinates, so it isn't re-trimmed by the region
-sslike a plain click.
Transport footer
The footer was rebuilt around labelled control groups (Transport, Position,
Speed, Click track) instead of a right-click popover, then aligned to the
studio's own column grid:
control clusters, the waveform, its ruler and the detection note — starts
exactly where the lane waveforms start and runs flush to the window edge,
so a position sits at the same x in both strips. The track identity (art,
title, meta, favourite, Export Mix) sits in the left column under the mixer
panel and shares its width and padding. That also drops a row: 255px tall
where the three tiers were 318px.
--daw-col-w, read by the stems panel, thelabel cell above it and the footer, instead of being hardcoded three times.
tickStep(), so a time is labelled at the same x in each.segmented control.
shape and lit state — click on, click off. The
Gshortcut is gone; thebutton says what it does, and a single letter bound to a modal editor is
easy to hit by accident.
syncFooterDividers, usingvisibilityso removing its box can't changethe wrap it just measured.
Testing
tests/js/count-in.test.mjs— lead-in parity, accent handling, multiplierlevels (12 checks).
tests/test_click_render.py— the render side of the same math.export-menu.spec.mjs) pass.loaded, playing and menu-open states. Alignment verified by measurement:
lane waveforms and footer canvas both
690→1600, ruler ticks matching at690, 874, 1059, 1243, 1428.Known limitation
Alignment holds at fit-zoom. Below ~720px of lane width the lanes zoom and
scroll independently while the footer strip always shows the whole song, so
there the two share only a left edge.