From 317b81cc7012f510dfc93bd66227886d4e52dbdd Mon Sep 17 00:00:00 2001 From: ChrisBeWithYou Date: Fri, 17 Jul 2026 08:00:33 -0500 Subject: [PATCH 1/2] feat(editor): master mix strip in the Tracks pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The master row carried no inline M/S/fader — its controls lived only in the mixer drawer (a deliberate early carve-out). Dogfooding kept reaching for a pane mute on the master, so the pane now mirrors the drawer: the mixControls gate collapses to "every row with a strip key" (new _trackRowShowsStripPure), riding the same key-generic mix-mute/mix-solo/mix-vol handlers and the same canonical partMix. Master keeps its output-bus semantics — its own mute silences it, another track''s solo never does (_mixerPartAudiblePure''s carve-out). Tests: tests/track_strip_master.test.mjs — the master-row strip predicate fails on main; stem/transcription rows keep strips; folders and malformed rows never strip. Co-Authored-By: Claude Fable 5 Signed-off-by: ChrisBeWithYou --- CHANGELOG.md | 5 +++ src/track-session.js | 25 ++++++++----- tests/track_strip_master.test.mjs | 58 +++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 tests/track_strip_master.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index c9b46e04..9b4f2f87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **The master mix can be muted (and soloed, and faded) from the Tracks + pane.** The master row used to carry no inline strip — its controls lived + only in the mixer drawer. The pane now mirrors the drawer: same M/S/fader, + same canonical mix state, and the master keeps its output-bus semantics + (its own mute silences it; another track's solo never does). - **The master mix is a channel strip in the mixer.** Every audio source now has a vertical strip in the mixer drawer — the master mix leads the audio band (matching the DAW console), followed by the stems, then the MIDI parts and the diff --git a/src/track-session.js b/src/track-session.js index 537ed624..2d51ba2f 100644 --- a/src/track-session.js +++ b/src/track-session.js @@ -338,6 +338,13 @@ export function _trackSessionDensityPure(width) { const value = Number(width) || 0; return value < 230 ? 'compact' : value < 400 ? 'normal' : 'wide'; } +// Which Tracks-pane rows show the inline M/S/fader strip: every row with a +// mix key — transcription parts, stem audio rows, AND the master mix (its +// old master-excluded carve-out is gone: the pane mirrors the mixer drawer; +// see mixControls in render()). Folders have no strip key and never strip. +export function _trackRowShowsStripPure(row) { + return !!(row && row.mixKey); +} // Logic-style auto-fit, deliberately modest: spare viewport height improves // readability but the automatic bonus caps at 32px so a two-track song does // not turn into two enormous empty slabs. Never shrinks below authored. @@ -641,16 +648,16 @@ function render() { const guideName = guideRow ? guideRow.name : ((sources.find(s => s.id === model.tempoGuideSourceId) || sources[0] || {}).name || 'No guide'); // Per-part M/S/fader — the SAME canonical partMix the mixer panel owns - // (band-mode gains ramp off it live). Transcription parts AND stem audio - // rows get strips inline here. The MASTER mix is DELIBERATELY excluded from - // the left Tracks pane: it lives as a channel strip in the mixer drawer (its - // fader/mute/solo are real there — reference playback routes through a - // per-source gain, audio.js), and Christian wants the left pane kept to the - // tracks themselves, not the master-out or bus mixes. + // (band-mode gains ramp off it live). EVERY row with a strip key gets the + // inline controls, the master mix included: it used to be deliberately + // excluded here (mixer-drawer-only, an early preference), but muting the + // master from the pane is a real workflow — dogfooding sessions kept + // reaching for it — so the pane now mirrors the drawer. Same keys, same + // handlers (mix-mute/mix-solo/mix-vol are key-generic), and the master + // keeps its output-bus semantics: its own mute silences it, other tracks' + // solo never does (_mixerPartAudiblePure's 'audio:master' carve-out). const mixControls = row => { - const stripped = row.type === 'transcription' - || (row.type === 'audio' && row.sourceKind !== 'master'); - if (!row.mixKey || !stripped) return ''; + if (!_trackRowShowsStripPure(row)) return ''; const key = _editorEscHtml(row.mixKey); const st = _mixerPartStatePure(S.partMix, row.mixKey); return `` diff --git a/tests/track_strip_master.test.mjs b/tests/track_strip_master.test.mjs new file mode 100644 index 00000000..13202730 --- /dev/null +++ b/tests/track_strip_master.test.mjs @@ -0,0 +1,58 @@ +/* + * Tracks-pane strip visibility (src/track-session.js _trackRowShowsStripPure). + * + * The master mix row now shows the same inline M/S/fader strip as every other + * strip-keyed row — its old deliberate carve-out (mixer-drawer-only) is gone. + * Pinned here so the master can't silently lose its pane mute again, and so + * folders (no strip key) can't grow one. + * + * Run: node tests/track_strip_master.test.mjs + */ +import assert from 'node:assert'; + +const { + _trackRowShowsStripPure, + _trackSessionNormalizePure, + _trackSessionRowsPure, +} = await import('../src/track-session.js'); + +let pass = 0, fail = 0; +function t(name, fn) { + try { fn(); pass++; console.log(' ok ' + name); } + catch (e) { fail++; console.error(' FAIL ' + name + ': ' + e.message); } +} + +const sources = [ + { id: 'master', name: 'Full Mix', kind: 'master', url: '/full.ogg' }, + { id: 'Guitar_L', name: 'Guitar_L', kind: 'stem', url: '/s1.ogg' }, +]; +const arrangements = [{ name: 'Lead' }]; +const drumTab = { name: 'Drums', hits: [{ t: 0 }] }; + +t('the MASTER row shows the strip — the old carve-out is gone', () => { + const model = _trackSessionNormalizePure(null, sources, arrangements, drumTab); + const { rows } = _trackSessionRowsPure(model, sources, arrangements, drumTab, {}); + const master = rows.find(r => r.type === 'audio' && r.sourceKind === 'master'); + assert.ok(master, 'master row exists'); + assert.strictEqual(master.mixKey, 'audio:master', 'master strips on the output-bus key'); + assert.strictEqual(_trackRowShowsStripPure(master), true, + 'master must show M/S/fader in the pane (the fix)'); +}); + +t('stem audio and transcription rows keep their strips', () => { + const model = _trackSessionNormalizePure(null, sources, arrangements, drumTab); + const { rows } = _trackSessionRowsPure(model, sources, arrangements, drumTab, {}); + for (const row of rows.filter(r => r.type !== 'folder')) { + assert.strictEqual(_trackRowShowsStripPure(row), true, `${row.id} shows a strip`); + } +}); + +t('rows without a strip key never strip (folders, malformed rows)', () => { + assert.strictEqual(_trackRowShowsStripPure({ type: 'folder', id: 'f1' }), false); + assert.strictEqual(_trackRowShowsStripPure({ mixKey: '' }), false); + assert.strictEqual(_trackRowShowsStripPure(null), false); + assert.strictEqual(_trackRowShowsStripPure(undefined), false); +}); + +console.log(`\n${pass} passed, ${fail} failed`); +process.exit(fail ? 1 : 0); From b1ff1cea08c32dc1c7e4418d18af4c06fa8a8785 Mon Sep 17 00:00:00 2001 From: ChrisBeWithYou Date: Fri, 17 Jul 2026 16:02:06 -0500 Subject: [PATCH 2/2] Align master strip changelog --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b4f2f87..2297e52b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,9 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 SOURCE/GUIDE/CLICK/MASTER buses. Its fader, mute, and solo are real: the active source's reference playback now routes through its own per-source gain before the SOURCE submix, so riding the master strip actually changes its level. The - Tracks pane still lists the master as a selectable source row (click it to - chart against the full mix), but its strip **controls** — fader, mute, solo — - live only in the mixer, not the pane. + Tracks pane also exposes the same master fader, mute, and solo controls + inline, so the pane and mixer drawer share one canonical mix state. - **Click a track to chart against it.** Selecting an audio track (the master mix or any stem) in the Tracks column now makes it the **active source**: the main waveform shows that track and the onset tools (Suggest, snapping)