Skip to content

Cap the level a session starts at when the slider is routed - #52

Merged
trvny merged 8 commits into
mainfrom
fix/safe-start-volume-with-slider
Aug 15, 2026
Merged

Cap the level a session starts at when the slider is routed#52
trvny merged 8 commits into
mainfrom
fix/safe-start-volume-with-slider

Conversation

@trvny

@trvny trvny commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

The owner has reported the speaker starting far too loud several times. Three explanations were tried and measured away today; this is the one that survived.

What it is

With hardware_volume=1 the component passes the slider level as an explicit --volume. choose_start_volume returns an explicit level on its first line, before it ever looks at max_start_volume. So the safe start exists, is passed on the paths that do not need it, and is skipped on the one that does — routing the slider silently disables the protection it looks like it respects.

A slider left high after an evening of listening then becomes the level the next track opens at, on material whose loudness nobody knows yet.

What changes

start_volume_max, raw step 0..30, default 3, 0 disables. It caps the first helper of a playback session and lifts as soon as one reports PLAYING, so a seek cannot turn down a level chosen mid-session. Not a volume limit: the slider governs everything after the start and reaches the speaker in about a second over the held connection.

The component also moves the slider to the level the helper reports in WAMBRIDGE PLAYING volume=<step>. Without that a capped start leaves the slider pointing at a level the speaker is not playing, and the first pixel of movement jumps there — the same surprise, deferred.

What was measured, and how well

By ear on the M5, slider mapped onto 0..10, stepping up one at a time:

raw heard as
1 inaudible
2 barely there
3 a little more
4 clearly louder
5 distinct
6 cuts through conversation in the room
7 comfortable listening

No cliff in it — the mapping was never the problem. Reaching 7 by dragging sounds fine; reaching it cold is what made the owner jump. Hence a cap on the starting point rather than a different curve.

Weaknesses worth knowing: this is a listener judgement on live radio, and the owner flagged mid-run that one step coincided with a louder passage. Whether the M5 raw steps are even in dB is still unmeasured and the docs still say so. An instrumented rerun on a steady tone is planned.

Not verified

281 tests pass. No build has been installed, so nothing here has been heard yet — including whether starting at 3 is pleasant in practice or merely safe.

With hardware_volume=1 the component passes the slider level as an
explicit --volume, and choose_start_volume returns an explicit level
before it ever looks at max_start_volume. The safe start therefore
existed, was passed on the paths that did not need it, and was skipped
on the one that did: a slider left high after an evening of listening
became the level the next track opened at, on material whose loudness
nobody knew yet.

start_volume_max caps the first helper of a session and lifts as soon as
one reports PLAYING, so a seek cannot turn down a level chosen mid
session. It is not a volume limit - the slider still governs everything
after the start and reaches the speaker in about a second.

The slider now also follows the level the helper reports, because a cap
that leaves the slider pointing somewhere the speaker is not just defers
the jump to the first touch of it.

The mapping itself is exonerated. Measured by ear on the M5 over a
slider mapped onto 0..10: 1 inaudible, 3 a little more, 5 distinct, 6
cuts through conversation, 7 comfortable. No cliff in it. Reaching 7 by
dragging is fine; reaching it cold is not, so this caps the starting
point rather than bending the curve.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@kanarek-companion

kanarek-companion Bot commented Aug 15, 2026

Copy link
Copy Markdown

🐤 Kanarek · 🟣 merged

main

Merged. Kanarek applies the tiny official beak stamp.

Documentation, Foobar, Tests · 4 files

devin-ai-integration[bot]

This comment was marked as resolved.

@trvny

trvny commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Triage of the three open PRs, not a new finding on this one: the slider-sync race Devin flagged is the blocker here. A second 55001 client during active playback is the topology AGENTS.md records as having starved a stream before. The routed >= 0 bypass is the same bug from the other side — no slider notification means the helper falls back to its own DEFAULT_MAX_START_VOLUME = 10, which is the loud start this PR exists to prevent.

Merge order matters: #48, #51 and #52 all branch from d8a9e93 and all three edit the constants block at foo_out_wam.cpp:39-56 and the tail of foobar.ini.example. GitHub reports clean because it only diffs against main. Suggested order: #51#52#48.


Generated by Claude Code

Two real problems in the previous commit, both found in review.

Moving the slider sends the level back out, and at PLAYING the control
channel has not been announced yet - it arrives on the next line. The
send therefore found no socket and fell back to launching a process,
opening a second connection to 55001 while audio streams, which
AGENTS.md calls out as able to starve the stream. The level is now held
until CONTROL_PORT connects and applied there, over the socket the
helper already holds. It is generation-checked too, so a PLAYING left in
a retired helper pipe cannot move the slider for a helper being killed.

The cap also only covered the routed-slider branch. A first helper with
no slider reading and no configured volume fell through to no clamp at
all and used the helper default of 10 - the exact loud start this
setting exists to stop, arriving through the one branch with nothing
watching it. That path now carries start_volume_max. A configured INI
volume stays uncapped on purpose: it is a choice somebody made, where a
slider position is leftover state.

Also: a malformed volume= no longer parses as 0, which is a level and
would have silenced the speaker; volume_max reports an out-of-range
value instead of falling back in silence, which is what the docs already
claimed; and start_volume_max joins the key reference.
devin-ai-integration[bot]

This comment was marked as resolved.

trvny and others added 3 commits August 15, 2026 20:43
Four from review, all correct.

The slider sync could still spawn a control process: connect_control_
channel can fail, and the sync ran regardless, so the level went out
over a newly launched second connection to 55001 mid-stream. It now
reports whether the socket came up and the sync waits on that.

Disabling the cap made starts louder than before this branch existed.
The disabled case passed the speaker maximum, overriding the helper own
default clamp of 10; it now omits the argument, which is what the code
did before.

The cap also reached setups where its justification does not hold. With
routing off the slider is a host-side gain that never touches the
speaker, so "the slider governs everything after the start" is false and
a capped start would be raisable only one menu press at a time. It is
now gated on hardware_volume.

And the sync is skipped when the reported step is above volume_max: the
inverse mapping clamps to the ceiling, so syncing a higher level would
write the ceiling back and turn the speaker down.
Found while answering the ordering question rather than raised by it.
The level was cleared on CONTROL_PORT, so a helper that reached PLAYING
and died before announcing its channel left it behind for the next
helper to apply on its own CONTROL_PORT line. The generation check
cannot catch that: by then the generation is legitimately current. It
now resets with the rest of the per-helper state in start_child.

Also corrects the guard comment, which claimed parity with the test
set_protocol_state_if_current applies. It deliberately omits the flush
check - that one exists so state cannot restart a clock for a stream
being torn down, and a slider position is not clock state.
devin-ai-integration[bot]

This comment was marked as resolved.

claudiusz69 Bot added 2 commits August 15, 2026 20:54
The cap held only until the first seek. A replacement helper reads the
remembered slider step and passes it straight through, so a slider still
sitting at last night level handed the speaker that level and undid the
quiet start. The slider sync cannot carry this on its own - it is
conditional on the control socket and on volume_max, and this has to
hold whether or not it ran - so the capped level is now what gets
remembered, rather than what the slider said.
The declaration claimed only the pipe-reading thread does, which stopped
being true when start_child began clearing it. They still cannot overlap
- start_child calls stop_child first and that joins the protocol thread
- so no atomic is needed, but the reason is the join rather than a
single owner.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 5 new potential issues.

Open in Devin Review

Comment thread foobar/foo_out_wam.cpp
Comment thread foobar/foo_out_wam.cpp
Comment thread foobar/foo_out_wam.cpp
Comment thread tests/test_foobar_source.py Outdated
Comment thread docs/FOOBAR_PLUGIN.md
The test asserted on the routed branch while calling itself unrouted,
which would send the next reader looking for a bug that is not there.
And the paragraph claiming nothing is ignored quietly did not mention
volume_max or start_volume_max - volume_max being the one this PR
stopped ignoring quietly.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 3 new potential issues.

Open in Devin Review

Comment thread foobar/foo_out_wam.cpp
Comment thread foobar/foo_out_wam.cpp
Comment thread docs/FOOBAR_PLUGIN.md
@claudiusz69

claudiusz69 Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Verified on the physical M5 before merging, with the build from e944eb1 installed (69/69 hashes matched, nothing outside the package).

  • quiet start: INFO: Speaker volume is 0; starting PCM playback at 3 with the slider sitting at 7 — the cap binds
  • survives a seek: the second start of the session logged Speaker volume is 3; starting PCM playback at 3
  • slider sync: the owner confirmed the slider had moved to near the far left, so it followed the capped level over the held socket rather than spawning anything

One thing the run exposed that this PR does not fix and does not claim to. foobar’s volume slider is not linear in pixels: a small movement near the left edge went from −42 dB to −21.5 dB. Our mapping spreads 60 dB over 10 steps, so those 20 dB are three raw steps — measured earlier the same day as the difference between "a little more" and "loud enough to cut through conversation". The start is no longer a surprise; the first adjustment after it still can be. Tracked separately.

@trvny
trvny merged commit 1b4766f into main Aug 15, 2026
2 checks passed
@trvny
trvny deleted the fix/safe-start-volume-with-slider branch August 15, 2026 19:23
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.

1 participant