Skip to content

Claude/midi audit auto routing i43men - #1895

Merged
glloq merged 14 commits into
mainfrom
claude/midi-audit-auto-routing-i43men
Aug 6, 2026
Merged

Claude/midi audit auto routing i43men#1895
glloq merged 14 commits into
mainfrom
claude/midi-audit-auto-routing-i43men

Conversation

@glloq

@glloq glloq commented Aug 6, 2026

Copy link
Copy Markdown
Owner

No description provided.

claude added 14 commits August 6, 2026 04:32
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
@glloq
glloq merged commit 5382a15 into main Aug 6, 2026
4 of 5 checks passed
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