Fix Mixer volume/pan and Solo state resetting after Solo toggle + playback - #34676
Fix Mixer volume/pan and Solo state resetting after Solo toggle + playback#34676tharos-devs wants to merge 1 commit into
Conversation
Manually adjusted volume/balance in the Mixer would revert to 0 dB after toggling Solo and playing back, and the Solo button itself could revert to unchecked. Root causes: - PlaybackController::trackControlParams() reused a cached automated control value (volume/balance) even when no automation curve was actually active for the track, so a stale snapshot taken at load time would clobber manual Mixer edits on the next solo/mute-driven resend. - MixerPanelModel's controlParamsChanged handlers never persisted manual volume/balance edits to IProjectAudioSettings, so any later reload of that track's output params would revert to the old value. - MixerChannelItem::loadOutputParams() also applied solo/muted/ forceMute from AudioOutputParams fetched via IProjectAudioSettings, which never actually tracks those fields (they are owned by INotationSoloMuteState / live force-mute computation), so it would reset the Solo/Mute checkboxes back to their default of false. Fixes musescore#34673.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughThe changes restrict cached volume and pan restoration to matching automation curves on valid instrument tracks. Mixer output loading no longer changes solo, mute, or force-mute state. Instrument, auxiliary, and master channel volume and balance changes now persist to their respective project audio settings while continuing to update playback controls. Merge Risk: ⚪ Minimal · up to This change preserves Mixer volume, pan, and Solo state across playback and channel refreshes. No actionable merge-blocking risk remains after normal checks and review. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsLinked repositories: Public OSS repositories can only analyze public repositories installed in this organization. No linked repositories were analyzed; skipped Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Resolves: #34673
Problem
Two related regressions in the Mixer, both triggered by the same reproduction steps:
Steps to reproduce (both bugs)
Bug 2 reproduces on its own too — no volume change is needed, just Solo + playback.
Root causes
These are two independent bugs in the same area, both stemming from the same architectural mistake: reading a field from an
AudioOutputParamssnapshot that isn't actually the authoritative source for that field.Bug 1 — stale automated-control-params cache overrides manual volume/pan
PlaybackController::trackControlParams()caches the last computedControlParamsper track inm_automatedControlParamsCache, so that repeated calls (e.g. fromupdateSoloMuteStates(), withrebuildVolume = rebuildPan = false) don't need to rebuild the automation envelope each time. The problem: the cachedvolume/balancewere reapplied unconditionally, whether or not that track actually has an active automation curve.This cache gets seeded for every track the first time
resendAutomatedControlParams()runs (e.g. on project load, since it's wired tototalPlayTimeChanged), even when no automation exists — in that case it just stores a flat snapshot of whateveroutParams.volume/balancewas at that moment (typically 0 dB / centered pan, the defaults).Sequence: user changes volume or pan in the Mixer → sent straight to the audio engine via
setControlParams, bypassingtrackControlParams()/the cache entirely → toggling Solo callsupdateSoloMuteStates(), which callstrackControlParams(id, params, /*rebuildVolume*/ false, /*rebuildPan*/ false)→ the stale cache entry (from load time) unconditionally overwritescontrol.volume/control.balance, and that stale value gets sent to the engine, audibly resetting the value.Fix: only reuse the cached value when the track actually has a non-empty Volume/Pan automation curve right now. Otherwise,
controlkeeps the volume/balance from the liveoutParamspassed in.Bug 2 — Solo/Mute checkbox reset from a source that never tracks them
MixerChannelItem::loadOutputParams()was also copyingsolo/muted/forceMutefrom whateverAudioOutputParamsit was given. Several code paths call it with anAudioOutputParamsfetched fromIProjectAudioSettings(trackOutputParams()/auxOutputParams()/masterAudioOutputParams()) — but solo/mute state is never written into that store. It's owned byINotationSoloMuteState(instrument tracks) orIProjectAudioSettings::auxSoloMuteState()(aux tracks), andforceMuteis computed live inPlaybackController::updateSoloMuteStates()and sent directly to the engine — neither ever round-trips throughIProjectAudioSettings::AudioOutputParams, so that struct'ssolo/muted/forceMutefields are always at their default offalse.Sequence: user clicks Solo →
MixerChannelItem::setSolo(true)correctly sets the notation-level state and the item's own display state → but the async promise fromplayback()->params(trackId)used to (re)build the channel item (MixerPanelModel::buildInstrumentChannelItem) resolves afterwards and callsloadOutputParams()with anAudioOutputParamsfreshly fetched fromIProjectAudioSettings— whosesolofield isfalse— clobbering the checkbox back to unchecked. The same pattern affects thefxChainParamsChangedreload path and the aux-channel build path.Confirmed via targeted
LOGDtracing:Fix:
loadOutputParams()no longer touchessolo/muted/forceMuteat all — those fields have their own dedicated, correct update path (loadSoloMuteState(), andupdateSoloMuteStates()for the live force-mute computation) and should never be sourced fromIProjectAudioSettings.Additional fix needed to make bug 1's fix effective
Independently of the cache issue,
MixerPanelModel'scontrolParamsChangedhandlers (instrument tracks, aux tracks, and master) never persisted manual volume/balance edits back toIProjectAudioSettings— only the live audio engine got the new value viasetControlParams. So even after fixing the cache bug above, any later rebuild of a track's output params (e.g. fromIProjectAudioSettings::trackOutputParams()) would still read the old, never-updated volume/balance. These handlers now persistvolume/balanceback toIProjectAudioSettings— deliberately not includingsolo/muted/forceMute, per the bug 2 root cause above.Testing
Manually built and tested locally (macOS).
Reproduced both bugs before the fix, confirmed both are resolved after, using several instruments and multiple simultaneous Solo toggles.
Manually re-verified the volume fix; the pan/balance fix follows the exact same code path (both bugs handle
volumeandbalancesymmetrically) but wasn't separately re-verified by ear.Verified with debug logging (
-dflag) that the Solo state is now correctly preserved through the Mixer's channel-item rebuild path.Ran
checkcodestyle.cmakeagainstsrc/— no style issues in the changed files.I signed the CLA as tharosd
The title of the PR describes the problem it addresses.
Each commit's message describes its purpose and effects, and references the issue it resolves. If changes are extensive, there is a sequence of easily reviewable commits.
The code in the PR follows the coding rules.
I understand all aspects of the code I'm contributing and I'm able to explain it if requested.
The code compiles and runs on my machine, preferably after each commit individually. I have manually tested and verified that my changes fulfil their intended purpose.
No prior attempts to resolve this problem exist, or if they do, I listed them in my PR description and described how I avoided repeating past mistakes.
There are no unnecessary changes.
I created a unit test or vtest to verify the changes I made (if applicable).