feat(editor): adopt a MIDI's own tempo map on import (Use-vs-Keep) - #127
Conversation
DAW roadmap 3.2. Wires core convert_midi_tempo_map (feedback #796) into the
editor's existing single-track keys/drums MIDI import. Until now a .mid import
baked note times but discarded the file's tempo + time-signature grid, so
every import landed with an implied 4/4 and no bars.
Backend (routes.py):
- New module-level _sanitize_midi_tempo_map (pure gate/shape: returns the
{tempos, time_signatures, beats} map only when it carries >=1 numbered
downbeat, else {}) and _safe_midi_tempo_map (wraps the core call; returns {}
on ImportError from an older host or any extraction error — an import never
500s over timing). Both are module-level so they're unit-testable without a
real .mid or the core lib.
- import-keys-midi and import-drums-midi now compute the tempo map in the SAME
worker as the note conversion (both endpoints rmtree the temp dir right
after) and return it as `tempo_map`. track_index is forwarded for SMF type-2.
Frontend (screen.js):
- @pure:midi-tempo-choice block: project-grid detection (>=2 downbeats), beat-
row sanitizing, the Use-vs-Keep default policy (Keep when a grid exists so an
audio-aligned timeline is never stomped; Use when it doesn't), and a summary
string.
- _maybeOfferMidiTempoMap: a radio dialog offered after a MIDI import; Apply
with "Use" runs S.history.exec(new TempoGridCmd(...)) — one undoable step
that re-locks the loop. No-op (no dialog) for a gridless MIDI, a GP import,
or an older host. On a drum import it's chained before the unmapped-notes
triage so the two dialogs never stack.
- TempoGridCmd is now correctly song-scoped (like the drum commands), matching
the EditHistory contract comment. Without it, applying an imported grid right
after a drums import — active part still fretted-in-roll — would be silently
blocked by the #119 read-only-roll lock.
Tests: tests/test_midi_tempo_import.py (gate/shape purity, the two core-call
failure floors via injection, track_index forwarding — 8) and
tests/midi_tempo_import.test.js (grid detection, sanitizing, default policy,
summary, TempoGridCmd songScope + round-trip — 10). Full JS suite green;
editor pytest 221 passed / 2 skipped. Both features absent on main.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzMBtxWnLGHYkMMXtK38Bg
|
Warning Review limit reached
Next review available in: 32 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds MIDI tempo-map import support in the backend and frontend, including optional adoption of the imported timing grid, song-scoped tempo grid application, and coverage for extraction, selection, and rollback behavior. ChangesMIDI Tempo Map Import
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant routes.py
participant screen.js
participant User
participant TempoGridCmd
participant import endpoint
routes.py-->>screen.js: tempo_map in import response
screen.js->>User: offer "Use this MIDI's timing?"
User-->>screen.js: Apply or Skip
screen.js->>TempoGridCmd: exec on Apply
TempoGridCmd-->>screen.js: update S.beats
screen.js-->>import endpoint: continue import flow
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@routes.py`:
- Around line 410-436: The MIDI tempo map returned by _safe_midi_tempo_map is
still in raw MIDI time, while the imported notes/hits are already shifted by
audio_offset, so the grid and chart can diverge on offset imports. Update the
import flow where the result is passed into TempoGridCmd to apply audio_offset
to the tempo-map timing data first, adjusting the beat rows and any matching
tempos/time_signatures timestamps consistently. Use _safe_midi_tempo_map,
audio_offset, and TempoGridCmd as the main points to locate the change.
In `@screen.js`:
- Around line 14360-14399: The default-choice logic in
_midiTempoDefaultChoicePure is too strict for imported MIDI because it uses
_hasProjectGridPure, which requires two downbeats and therefore hides valid
single-bar tempo maps. Update the MIDI-side availability check to treat a
sanitized tempo map with at least one valid downbeat as offerable, while keeping
the stricter _hasProjectGridPure check for the existing projectBeats. Preserve
the current keep-vs-midi decision structure in _midiTempoDefaultChoicePure, but
base “nothing to offer” on a looser imported-grid predicate.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7868b2a2-5d62-4565-b98c-aef9af035fc0
📒 Files selected for processing (5)
CHANGELOG.mdroutes.pyscreen.jstests/midi_tempo_import.test.jstests/test_midi_tempo_import.py
- Shift the adopted MIDI tempo-map grid by audio_offset so bars land on the same absolute-time mapping as the just-imported notes (notes are placed at raw+audio_offset; the grid was emitted at raw times, so Use would misalign downbeats from the content by audio_offset). - Match the frontend Use-vs-Keep offer gate to the backend: a single numbered downbeat is enough (routes.py _sanitize_midi_tempo_map / the backend already ships single-bar maps); the frontend's 2-downbeat project-grid threshold was silently dropping them. - Regression tests (JS + Python) that fail on pre-fix code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eats Core convert_midi_tempo_map may convey a non-4 denominator only in the time_signatures list, leaving downbeat rows bare. The editor's canonical home for the denominator is beat.den (frontend + _build_song_timeline read that, not the list), so adopting such a grid would save e.g. 6/8 as 6/4. Fold the signatures onto downbeats via the existing _apply_timeline_signatures_to_beats inside _sanitize_midi_tempo_map (before the audio_offset shift, while raw times still match). Regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_apply_timeline_signatures_to_beats folded a denominator only onto the exact downbeat matching a time-signature event, but both song_timeline and core MIDI maps emit one event per CHANGE, not per bar. A persistent 7/8 therefore restored/saved as 7/8 on the change bar and /4 on every following bar (latent in the song_timeline load path too, now that the MIDI import path also folds). Apply each event forward until the next change. Regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Both CodeRabbit findings were already addressed in this PR before the review landed:
Also folded time-signature denominators forward across downbeats so a persistent non-4 denominator survives adopt/save. Regression tests added on both sides (JS + pytest). Resolving the threads. |
…ver spill past the ruler Two review fixes on the derived tempo/meter marker lane: - Denominator: a bare downbeat now reads /4 via _tempoNormalizeDenominatorPure — the SAME canonical read the signature inspector uses — instead of carrying the previous explicit den forward. The phantom carry missed the meter change back to /4 after an explicit-den bar and could label a chip with a den the inspector contradicts (real carry-forward is materialized at import by stamping den per downbeat, _apply_timeline_signatures_to_beats / #127). - Chip stacking: the chained 44px heuristic grew one row per chip, so a dense marker run (a hand-fit/suggest-fit rubato map has a tempo chip on nearly every bar) painted chips diagonally down past RULER_H over the chart. _rulerMarkerRowsPure now lays chips into at most 3 rows by actual chip width and drops the overflow — the _rulerBarLabelSkipPure convention: decimate when dense, never spill. Both regression tests fail on the pre-fix code (verified by revert). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DAW roadmap 3.2 (
SONG-EDITOR-DAW-WORKSPACE-DESIGN.md§F3). Newly unblocked: core #796 (convert_midi_tempo_map) merged, which is the gate this slice waited on.The gap
Importing a
.midas keys or drums baked note times but threw away the file's tempo map and time signatures — so every MIDI import landed with an implied 4/4 and no bars, no matter what the file said.What this does
The keys/drums MIDI import now reads the SMF's real grid and, when it carries one, offers a Use MIDI tempo map / Keep project timing choice. The default is honest and never silent:
Applying runs through the existing
TempoGridCmd(one undoable step that re-locks the loop onto the new grid); imported notes stay accurate either way. It degrades cleanly — a gridless MIDI, a GP import, or an older host without the core function simply shows no prompt. On a drum import the timing choice is chained before the unmapped-notes triage so the two dialogs never stack.Backend (
routes.py)_sanitize_midi_tempo_map— pure gate + shape: returns{tempos, time_signatures, beats}only when the map has ≥1 numbered downbeat, else{}. Module-level so it's unit-testable without a real.midor the core lib._safe_midi_tempo_map— wraps the core call; returns{}onImportError(older host predating #796) or any extraction error, so an import never 500s over timing.track_indexis forwarded (SMF type-2 independent timelines).import-keys-midi/import-drums-midicompute the map in the same worker as the note conversion (both endpointsrmtreethe temp dir immediately after) and return it astempo_map.Frontend (
screen.js)@pure:midi-tempo-choiceblock: project-grid detection (≥2 downbeats), beat-row sanitizing, the Use-vs-Keep default policy, and a summary string (e.g.12 bars · 4/4 · 120 BPM)._maybeOfferMidiTempoMap— the radio dialog +TempoGridCmdapply.TempoGridCmdis now song-scoped (like the drum commands), matching theEditHistorycontract comment that already lists "tempo grid" as song-scope. Without it, applying an imported grid right after a drums import — active part still fretted-in-roll — would be silently blocked by the feat(editor): per-part view switcher — any fretted part opens in the piano roll (read-first) #119 read-only-roll lock. A latent bug this surfaces and fixes.Tests
tests/test_midi_tempo_import.py(8) — gate/shape purity, gridless drop, the two core-call failure floors (older core →{}, extraction raising →{}) via injection,track_indexforwarding.tests/midi_tempo_import.test.js(10) — grid detection, sanitizing, default policy, summary,TempoGridCmdsongScope + S.beats round-trip.Scope
Single-track, riding the existing keys/drums pickers — the multitrack mapping-table picker + classifier is 3.3 (separate PR). Provenance (
import_source) is 3.3 too. Fresh region: new module-level helpers among the_timeline_*family; the frontend block sits by the drum-import modals.Summary by CodeRabbit
New Features
Bug Fixes
Tests