HEAD-STAGE control bridge: wrap the adapter's setGain / setPolarity so each still runs
(canonical state + surface events) and then mirrors the value onto the lane THE OWNERSHIP
RESOLUTION names — an input strip, or the master lane for MAIN. A failed native call is
logged, never thrown.
WHICH LANE is not decided here. A local stripIndexOf that answers only for input channels
IS audit finding D18: MAIN's /gain, /trim and
/polarity rows moved, reported applied: true, and the master lane never heard them.
ownerOfControl is the only place the question is answered, and the READBACK
(?verify=1) resolves through the same expression, so the two cannot name different slots.
MUTE, FADER and PAN are deliberately not here, and their removal is the point. This bridge
used to wrap setMute and mirror the RAW argument, which is why an individual mute reached the
audio at all — and why touching a channel held by a mute group or a panic opened it: the raw
flag overwrote the composed one in the single C mute field. setFader and setPan were the
same defect on ctrl[lane].fader / ctrl[lane].pan, one field further along: the strip ROWS
(channel-strip-rows.ts, nativeStripDrive) already drove those lanes from their own sink,
so a row PATCH and a mirror replay raced, last writer winning, and the mirror carried none of
the row's refusal or projection verdict. All three fields are owned by their declaration now —
mute by the channel-mute resource, fader and pan by their rows — and the non-REST causes
(session restore, scene recall, the boot push) re-project through the SAME sink
(MixerServer.reprojectChannelMute, MixerServer.reprojectAllChannelLevels). There is
exactly ONE writer of each field, so no composition can be clobbered.
Install-time push — the #122 root-cause fix. The bridge is installed AFTER
MixerServer.start() has already restored the last session onto the software mixer
(boot autoload, operator #51), so the canonical strips ALREADY carry the restored
gain/polarity by the time the wrap goes in — values the native DSP never saw, leaving it at
its C defaults: the session was displayed but not heard. Every INPUT strip's current state is
therefore pushed once at install, exactly like the EQ/gate/comp mirror pushes its stores'
pre-existing state. MAIN is NOT in that loop: the software mixer holds no head-stage record
for it to replay, so a restored MAIN trim reaches the master lane on the operator's next write
and not before — named here rather than papered over, and the residue task #46's PR carries.
The restored fader/pan reach the lanes through the rows' own boot sweep, which is the same
push made by the field's one writer.
Extracted from the demo start-up so it is unit-testable with a fake adapter + fake native.
HEAD-STAGE control bridge: wrap the adapter's
setGain/setPolarityso each still runs (canonical state + surface events) and then mirrors the value onto the lane THE OWNERSHIP RESOLUTION names — an input strip, or the master lane for MAIN. A failed native call is logged, never thrown.WHICH LANE is not decided here. A local
stripIndexOfthat answers only for input channels IS audit finding D18: MAIN's/gain,/trimand/polarityrows moved, reportedapplied: true, and the master lane never heard them.ownerOfControlis the only place the question is answered, and the READBACK (?verify=1) resolves through the same expression, so the two cannot name different slots.MUTE, FADER and PAN are deliberately not here, and their removal is the point. This bridge used to wrap
setMuteand mirror the RAW argument, which is why an individual mute reached the audio at all — and why touching a channel held by a mute group or a panic opened it: the raw flag overwrote the composed one in the single Cmutefield.setFaderandsetPanwere the same defect onctrl[lane].fader/ctrl[lane].pan, one field further along: the strip ROWS (channel-strip-rows.ts,nativeStripDrive) already drove those lanes from their own sink, so a row PATCH and a mirror replay raced, last writer winning, and the mirror carried none of the row's refusal or projection verdict. All three fields are owned by their declaration now — mute by the channel-mute resource, fader and pan by their rows — and the non-REST causes (session restore, scene recall, the boot push) re-project through the SAME sink (MixerServer.reprojectChannelMute,MixerServer.reprojectAllChannelLevels). There is exactly ONE writer of each field, so no composition can be clobbered.Install-time push — the #122 root-cause fix. The bridge is installed AFTER
MixerServer.start()has already restored the last session onto the software mixer (boot autoload, operator #51), so the canonical strips ALREADY carry the restored gain/polarity by the time the wrap goes in — values the native DSP never saw, leaving it at its C defaults: the session was displayed but not heard. Every INPUT strip's current state is therefore pushed once at install, exactly like the EQ/gate/comp mirror pushes its stores' pre-existing state. MAIN is NOT in that loop: the software mixer holds no head-stage record for it to replay, so a restored MAIN trim reaches the master lane on the operator's next write and not before — named here rather than papered over, and the residue task #46's PR carries. The restored fader/pan reach the lanes through the rows' own boot sweep, which is the same push made by the field's one writer.Extracted from the demo start-up so it is unit-testable with a fake adapter + fake native.