Claude/midi audit auto routing i43men - #1895
Merged
Merged
Conversation
Six-axis audit of the file→adaptation→auto-routing pipeline (channel analysis/scoring, melodic adaptation, drum remap, physical planning, apply/persistence, playback). 11 functional defects (P1), 9 verified end-to-end against the midi-file parser and JSON converters; plus robustness gaps (P2) and minor/doc items (P3). Highlights (all file:line + repro in the doc): - MidiTransposer removes events without redistributing deltaTime → timing corruption when poly-drop/suppress is active. - CC remap and poly-aftertouch branches are dead code (test wrong event type vs the parser's 'controller'/'noteAftertouch'). - density always 0 and tempo always 120 (metadata never attached to the analysed midiData) → degraded type detection and timing scoring. - Re-apply leaves orphan split routings; file-list status over-counts split rows (false "playable" badge); apply_assignments is non-atomic. - Drum '-1' depth means "omit" in code but "unlimited substitution" in the default config → auxiliary percussion silently dropped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ekccgnr9tm3Jt5x91X5nHt
…ng status Batch 1 of the 2026-08-06 adaptation/auto-routing audit fixes (all with regression tests; full backend suite green, 1247 tests): - P1-1 MidiTransposer: redistribute a removed event's deltaTime onto the next surviving event instead of dropping it, so suppress/poly-drop no longer rushes the rest of the channel ahead. - P1-2 MidiTransposer: CC remap/suppress now matches the parser's `type:'controller'` + `controllerType` (was dead code testing `controlChange`/`cc`); `ccsRemapped` is real again. - P1-3 MidiTransposer: poly-aftertouch is transposed/remapped (`type:'noteAftertouch'`, was dead code testing `keyPressure`). - P1-4/P1-5 ChannelAnalyzer: derive tempo (BPM) and duration (s) from the parsed data when the converter omits them, fixing density that was always 0 and a timing penalty that always assumed 120 BPM. - P1-7 routing status: count DISTINCT channels, not rows, in getRoutingCountsByFiles (COUNT(DISTINCT channel)) and FileManager getFileMetadata, so a split channel no longer over-counts and shows a file with an unrouted channel as "playable". - P1-11 apply_assignments: include notesDropped/notesShortened/ccsRemapped in the hasModifications gate so polyphony/duration/CC-only adaptations are persisted instead of silently discarded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ekccgnr9tm3Jt5x91X5nHt
… drum fallback Batch 2 of the 2026-08-06 adaptation/auto-routing audit fixes (full backend suite green, 1248 tests): - P1-9 ScoringConfig: the default drumFallback used -1 for the nice-to-have percussion categories, but -1 means "ignore/omit" (a real per-category setting exposed by the settings UI), not "unlimited substitution" as the comments claimed. Those categories are now omitted from the default so they default to unlimited substitution (getMaxDepthForNote → Infinity) instead of silently dropping latin / shaker / whistle / cuica / triangle hits. -1 stays available as an explicit per-category "ignore". - P1-6 apply_assignments: clear the target file's routings (deleteByFileId) before writing the new set, so re-apply is idempotent — switching a channel split→single no longer leaves orphan split rows (the non-split upsert's partial index can't overwrite them) and a deselected channel no longer keeps a stale routing. - P1-8 apply_assignments: push a routing into the returned array only after the DB accepted it, collect failed channels, and surface them (failedChannels/partial + a warning) instead of always reporting a full success. Full transactional atomicity remains a follow-up (saveSplit opens its own transaction; better-sqlite3 has no nested transactions). Regression tests for the drum default and the timing/CC/aftertouch/ deltaTime fixes live in tests/adaptation-audit-fixes-2026-08-06.test.js; the audit doc tracks delivered vs remaining items. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ekccgnr9tm3Jt5x91X5nHt
…e split aftertouch Batch 3 — safe P2 audit fixes (no scoring-assignment change; full backend suite green, 1251 tests): - P2-5 InstrumentMatcher.isDrumsInstrument now matches AutoAssigner's predicate (instrument_type='drums' or channel 9, in addition to discrete mode / GM 112-119), so a drums-typed range-mode kit is no longer offered for the drum channel and then scored ~0 as "non-drum". - P2-2 update_instrument_capabilities emits instrument_settings_changed per updated instrument, matching the sibling handlers, so runtime caches (CapabilityResolver, CompensationService, MidiRouter, PlaybackScheduler, MidiClockGenerator) refresh instead of clamping with stale capabilities. - P2-11 MidiTransposer.splitChannelInFile routes poly-aftertouch to the segment that owns its note (peek the active-note stack) instead of broadcasting it to every segment's channel. P2-10 (poly-drop Set→count) deferred: it restructures the polyphony measure and carries regression risk; documented in the audit doc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ekccgnr9tm3Jt5x91X5nHt
…g window by note duration Batch 4a — robustness audit fixes (full backend suite green, 1254 tests): - P2-3 FileRoutingSyncService: a channel whose destination device is momentarily offline is now persisted DISABLED instead of being dropped by the up-front deleteNonSplitByFileId. planChannelRouting keeps the skip-device action (for invalid-device reporting) but also returns the routing payload with enabled:false; syncFile/bulkSync persist it. It re-enables on the next sync once the device returns. - P2-9 TablatureConverter: _getOccupiedStrings and _pruneRecentEvents used a hardcoded 7680-tick lookback, so a still-sounding note longer than ~16 beats (a cello/bass drone) was pruned from the occupied-string set and a second pitch could be booked on the same physical string. The window is now bounded by the longest actual note duration (floored at 7680). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ekccgnr9tm3Jt5x91X5nHt
…ram Change Batch 4b — P2-6 (full backend suite green, 1254 tests): - scoreInstrumentType fell back to the literal 'unknown' category for a channel with no Program Change, collapsing the type score to a neutral 0.5*maxScore and discarding the detected heuristic type (e.g. 'bass'). It now prefers the heuristic generic type when the category is 'unknown' (symmetric on both channel and instrument sides), so a PC-less bass channel scores a real match/mismatch instead of neutral. Audit doc updated with delivered items and a documented rationale for the deferred findings (P1-10/P2-4 scoring-core rework, P2-1 error-format divergence, P2-8 reporting-integrity, P2-7/P2-10/P3, axis 6). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ekccgnr9tm3Jt5x91X5nHt
…live re-assign Axis-6 (playback/live routing parity) audit fixes (full backend suite green, 1258 tests): - axis6-1 (CRITICAL) PlaybackScheduler.scheduleEvent passed the raw event.type to getOutputForChannel, so a velocity-0 note-on (running- status note-off) took the note-ON split path — incrementing the round-robin/alternate counter and pushing a segment assignment instead of popping it — while dispatch emitted an actual note-off. Every note stranded on the wrong segment for stateful overlap strategies. It now passes the LOGICAL type (velocity-0 note-on → noteOff). - axis6-2 (HIGH) setChannelRouting / setChannelNoteRemapping / setChannelSplitRouting changed a channel's destination/mapping mid- playback without releasing sounding notes (unlike the transpose setters), so the file's note-off went to the new device/mapping and the old note hung. They now _panicChannel (All-Notes-Off via the OLD routing) before applying the change, when playing and actually changing. Also documents the full axis-6 findings (axis6-3 clamp-parity ch9, axis6-4 sustain-not-reset-on-loop, axis6-5 deferred note-off instance, axis6-6 compensation reorder) with status in the audit doc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ekccgnr9tm3Jt5x91X5nHt
…n doesn't carry over axis6-4 (full backend suite green, 1260 tests): - A backward seek (scrub-back) and the loop-to-start path went through seek(), which sent All-Notes-Off (CC123) and reconstructed channel state — but reconstruction is set-only: it re-applies controllers PRESENT at the target and never clears ones still held from the later position. A sustain (CC64) held near the end of a file therefore stayed down after looping, over-sustaining / hanging the next iteration's notes. - seek() now also sends Reset-All-Controllers (CC121) to every routed channel when the target is earlier than the current position. Refactored the scheduler's device/channel fan-out into a shared _broadcastCC used by both sendAllNotesOff (CC123) and the new resetControllers (CC121). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ekccgnr9tm3Jt5x91X5nHt
…er-hand semitones) P2-7 (#1, #3) — full backend suite green, 1262 tests: - _scoreHandPositionFeasibility (frets) took polyphony capacity only from max_fingers; for a fret_sliding_fingers effector the validator requires num_fingers and leaves max_fingers optional, so a 2-finger robot was reported feasible for a 6-note chord. It now falls back to num_fingers. - (semitones) totalFingers was hardcoded hands.length * 5, ignoring per-hand num_fingers, so a 2-hand × 3-finger robot read as 10 fingers. It now sums each hand's num_fingers (fallback 5). P2-7 #2 (deriving a fret span from the canonical hand_span_mm + scale_length_mm for the shift warning) is left as a follow-up — it needs a field outside hands_config and a non-linear mm→fret approximation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ekccgnr9tm3Jt5x91X5nHt
…fer a split P1-10 + P2-4 (decision: partial score by playable ratio, prefer split) — full backend suite green, 1262 tests: - scoreNoteCompatibility no longer returns 0/incompatible when the channel span exceeds the instrument or no octave/transposing shift aligns the ranges. A new _scoreBestEffortWrapping applies a center-point octave shift, octave-wraps the notes still out of range (the same fold the playback engine performs), and scores by the fraction of notes playable after adaptation — so a capable instrument is offered "playable with wrapping" instead of being discarded. - This also populates the octaveWrapping / octaveWrappingEnabled payload that was previously always null (P2-4). - AutoAssigner.evaluateChannelSplits now treats a channel assigned via octave-wrapping as a split candidate even when its total score is acceptable, so a lossless multi-instrument split is proposed and can be preferred over the lossy single-instrument wrap. The old "incompatible when span too wide" test is updated to the new contract (compatible, partial score, populated wrapping map). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ekccgnr9tm3Jt5x91X5nHt
… to its instance
P2-10 + axis6-5 (full backend suite green, 1266 tests):
- P2-10 MidiTransposer polyphony drop: active voices are now tracked as a
list of {note,index} (so two simultaneous same-pitch notes each count)
and dropped notes as a per-note count (so each dropped voice's note-off
is swallowed exactly once). The old Map<note,index> + Set under-counted
unison — common after clamp/transpose folds a chord onto one pitch — and
could strand a note-off across two drop episodes on the same pitch.
- axis6-5 PlaybackScheduler: a note-off deferred for min_note_duration now
captures a per-pitch instance id and only fires if it still matches. A
fast same-pitch retrigger within the defer window bumps the instance, so
the previous note's late release is skipped instead of cutting the
retriggered note short; its own note-off releases it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ekccgnr9tm3Jt5x91X5nHt
…e-on reorder axis6-6 (full backend suite green, 1268 tests): - MidiRouter computed the relative compensation delay per message. If a settings recalibration lowered maxComp between a note-on and its note-off (with >=2 destinations from one source), the note-off could be delayed LESS than the still-pending note-on and reach the device first, stranding the note. The delay is now latched at note-on (keyed like the existing pitch latch, and likewise not cleared on settings change) and reused for the matching note-off / poly-aftertouch, so their order is preserved. Non-note messages still compute fresh. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ekccgnr9tm3Jt5x91X5nHt
…es with the code
P3 doc drift, item 2 of the follow-up:
- Scoring Algorithm: the doc claimed weights 30/25/15/15/10/5 with a 6th
"Channel Special" criterion. Updated to the real ScoringConfig.weights
(noteRange 40, programMatch 22, instrumentType 20, polyphony 13,
ccSupport 5 = 100) plus the separate percussion bonus/penalty and timing
penalty adjustments; noted the octave-wrapping partial-score / split-
candidate behavior and the heuristic type for PC-less channels.
- API example: scoreDetails → the real scoreBreakdown keys and {score,max}
shape (program/noteRange/polyphony/ccSupport/instrumentType/percussion/
timing).
- Configuration: real weight keys/values.
- Drum Categories: replaced the 7-group prose with the 12 actual
DRUM_CATEGORIES (kicks/snares/hiHats/toms/crashes/rides/latin/shakers/
woodsMetal/pitched/cuicas/triangles) and explained the drumFallback depth
semantics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ekccgnr9tm3Jt5x91X5nHt
…ck parity) axis6-3 (decision: source channel) — full backend suite green, 1270 tests: - MidiRouter._clampToCapabilities skipped the capability clamp when the MAPPED/destination channel was 9, while file playback keys the same decision on the SOURCE channel. A live route remapping across channel 9 therefore clamped (or skipped) opposite to playback. "Drum sound vs pitch" is a property of the incoming content (GM channel 9 = drums), so the source channel is now threaded through _sendAndEmit and the drum skip keys on it — a drum source is never clamped even when remapped onto a melodic channel, and a pitched source is always clamped even when remapped onto channel 9. This matches file playback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ekccgnr9tm3Jt5x91X5nHt
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.
No description provided.