Skip to content

0.16 review: standalone settings persistence and output channel policy - #547

Merged
defiantnerd merged 2 commits into
nextfrom
0.16-fix-standalone
Sep 14, 2026
Merged

defiantnerd merged 2 commits into
nextfrom
0.16-fix-standalone

Conversation

@defiantnerd

@defiantnerd defiantnerd commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Third batch from the 0.16 release review of main...next. Independent of #545 and #546.

What is here

SA-2 and SA-4 — one bug wearing two hats. captureAudioSettings() wrote the resolved device names and used-flags back to disk. Those describe the stream as it was actually opened, not what the user asked for: resolveOutputDevice() falls back to the default when the saved device is absent, and startAudioThreadOn() clears the used flag when a side cannot be opened. Since saveSettings() runs on every window move and on WM_DESTROY, the fallback was persisted before the user could ever notice.

  • SA-2: launch once with your interface unplugged and outputDeviceName becomes Speakers (Realtek). Plug it back in and you are on laptop speakers, with no indication why.
  • SA-4: first run inside an RDP session with audio redirection off saves audioOutputUsed=false, and applyAudioSettings() ANDs the stored flag with the probe — so output is disabled on every later launch at the console, while the settings panel cheerfully shows the default output selected.

Reading every use of audioOutputUsed showed it was really runtime state ("open this side now"), clobbered by three different functions, with genuine user intent only ever entering through the mute menu and the device combos. The two meanings are now separate: settings.* holds intent, written by the frontend at the point of choice — the same split openMidiPorts() already had with settings.midiPortNames — and the runtime flags are recomputed as intent AND probe each launch and never persisted.

Side effect, and an intended one: the mute-input check mark now reads intent, so it no longer goes stale on a machine with no capture device, and a mute survives an API switch — which the check mark already claimed it did.

SA-3 — a hand-edited config could terminate the process. clapProcess() calls std::terminate() if a callback delivers utilityBufferSize frames or more, RtAudio opens a WASAPI stream at whatever size it is asked for, and standalone_settings.h advertises the .conf as the supported way to configure a build with no settings UI. bufferSize=100000 took the standalone down on the first callback. Now clamped to the largest size the settings panel itself offers. The std::terminate stays — it is a real invariant for the utility buffers; the fix is to never reach it. Sample rate was checked and needs no clamp; the reasoning is recorded next to it.

SA-5 — surplus output channels carried a duplicated channel. The interleave repeated the plugin's last main-bus channel into every surplus device channel. Indistinguishable from a mono rule while devices were always opened with two channels — but the Windows frontend now opens them at full channel count, so a stereo plugin on an 8-output interface put its right channel on outputs 3 through 8, a hard-right signal into whatever those feed (typically a cue mix or a second monitor pair).

New policy: a mono bus duplicates onto the device's first two channels, anything else maps channel-for-channel up to min(bus, device), and every device channel the plugin does not address is silent.

The input path is deliberately unchanged

It uses the same repeat-last rule in the other direction: a plugin input bus wider than the device repeats the last device channel into the surplus (mono mic → both channels of a stereo bus, but a stereo device into a 6-channel bus puts the right input on bus channels 3–6), and a device wider than the bus has its surplus dropped (a stereo plugin on an 8-in interface reads inputs 1/2 only).

That has different ergonomics from the output case and nobody has reported a problem with it, so it is left for a separate decision. The comment that claimed output and input shared one policy is corrected rather than left to mislead.

Reported, not changed

A saved sample rate the device does not support still falls back to preferred, and that fallback is persisted — the same class as SA-2. An input-only stream with a garbage rate reaches openStream(), which errors rather than crashes.

Four review findings in the standalone host. SA-2 and SA-4 are one change
- they are the same confusion between what the user asked for and what
the machine turned out to have - and SA-3 and SA-5 are small and sit in
the same two functions.

SA-2 and SA-4, intent versus runtime. captureAudioSettings() wrote the
*resolved* device names and used flags back to disk, but those describe
the stream as it was actually opened: resolveOutputDevice() falls back to
the default when the saved device is absent, and startAudioThreadOn()
clears the used flag when a side cannot be opened. saveSettings() runs on
every window move and on WM_DESTROY, so one launch with the interface
unplugged persisted the laptop speakers over the user's choice before
they could notice, and one first run in an RDP session with no playback
endpoint disabled output on every later launch at the console, because
applyAudioSettings() ANDs the stored flag with the probe.

Split the two meanings. settings.*DeviceName and settings.audio*Used now
hold only what the user asked for, written by the frontend at the point
of choice - a device combo, the mute-input menu - which is the split
openMidiPorts() already had with settings.midiPortNames. The runtime
flags stay runtime: selectDefaultDevices() computes them as intent AND
probe, and nothing writes them back. The menu check mark reads intent, so
it no longer goes stale when a machine has no capture device, and a mute
survives an API switch, which the check mark already claimed it did.

SA-3, untrusted config. The .conf is the supported way to configure a
build with no settings UI, and clapProcess() terminates the process if a
callback delivers utilityBufferSize frames or more. RtAudio opens a WASAPI
stream at whatever size it is asked for, so a hand-edited
bufferSize=100000 took the standalone down on the first callback. Clamp
to the largest size the settings panel itself offers. The sample rate
needs no clamp and the reasoning is recorded next to it.

SA-5, output channel policy. The interleave repeated the plugin's last
main-bus channel into every surplus device channel. That was
indistinguishable from a mono rule while devices were always opened with
two channels, but the Windows frontend now opens them at their full
count, so a stereo plugin on an 8-output interface put its right channel
on outputs 3 to 8 - a hard-right signal into whatever those feed. Now: a
mono bus is duplicated onto the first two device channels, anything else
maps channel-for-channel, and every device channel the plugin does not
address is silent.

The input path keeps the repeat-last rule deliberately, to be decided
separately; the comment that claimed output and input shared one policy
is corrected rather than left to mislead.

Verified: standalone target builds clean (MSVC, exit 0, no warnings in
the touched files), clang-format clean. Runtime behaviour - an RDP
session, an unplugged interface, an 8-channel device - was not exercised;
there is no such hardware here.

Reported, not changed: a saved sample rate the device does not support
still falls back to preferred and that fallback is persisted, which is
the same class as SA-2. An input-only stream with a garbage rate reaches
openStream(), which errors rather than crashes.

@baconpaul baconpaul left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again this code is all fine but we really do need to do a comments cleanup pass

Keep the why, drop the narration; no code changes.
@defiantnerd
defiantnerd merged commit 5ab316e into next Sep 14, 2026
28 checks passed
@defiantnerd
defiantnerd deleted the 0.16-fix-standalone branch September 14, 2026 18:32
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