Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ 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).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- **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
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)
Expand Down
25 changes: 16 additions & 9 deletions src/track-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 `<button class="editor-track-ms" data-track-action="mix-mute" data-mix-key="${key}" aria-pressed="${st.mute}" title="Mute track">M</button>`
Expand Down
58 changes: 58 additions & 0 deletions tests/track_strip_master.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
Loading