cbd-web: end the volume slider where the server clamps

The slider was `max="1.5"` while the engine clamps to 1.1, so its right
third was unreachable — the fill stopped at 73% of the track and the rest
stayed empty however far the thumb was dragged.

`max` is now a named `MAX_VOLUME` that has to track
`PlayerEngine::set_volume`'s clamp. It cannot be imported: `audio-player`
is native-only and this client is wasm, so a comment on each end keeps
the pair honest. Raising the engine clamp was the alternative, but 1.1 is
deliberate headroom and more gain risks clipping, so the slider moved.

The tooltip now reports the level as a percentage too — the web
counterpart of the TUI's `Volume: 85%`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test User 2026-07-27 09:18:20 +02:00
parent 034737d14f
commit 1db88e0f5a
3 changed files with 36 additions and 3 deletions

View File

@ -21,6 +21,13 @@ use crate::state::{
const VOLUME_STEP: f32 = 0.1; const VOLUME_STEP: f32 = 0.1;
/// The highest level the server will accept — it clamps to this in
/// `PlayerEngine::set_volume`, and now reports back the level it took, so a
/// slider that ran past this point had a dead stretch at its right end that
/// filled and then sprang back. Must track the engine's clamp; the audio
/// crate is native-only, so the value cannot be imported from it.
const MAX_VOLUME: f32 = 1.1;
/// How far one seek press moves the playing position, in milliseconds — the /// How far one seek press moves the playing position, in milliseconds — the
/// unit the wire speaks (architecture/seek.md D3). The step lives in the /// unit the wire speaks (architecture/seek.md D3). The step lives in the
/// client; the server only ever receives an offset and applies it to the live /// client; the server only ever receives an offset and applies it to the live
@ -1266,10 +1273,10 @@ fn Transport(store: Store) -> impl IntoView {
<input <input
type="range" type="range"
min="0" min="0"
max="1.5" max=MAX_VOLUME.to_string()
step="0.05" step="0.05"
prop:value=move || store.volume.get().to_string() prop:value=move || store.volume.get().to_string()
title="volume (J/K)" title=move || format!("volume {:.0}% (J/K)", store.volume.get() * 100.0)
on:input=on_volume on:input=on_volume
/> />
</div> </div>

View File

@ -18,7 +18,13 @@ the queue toolbar shows how many entries are waiting). Seek is there too —
`,` and `.` move 15 seconds back and forward (`<` and `>` skip a whole `,` and `.` move 15 seconds back and forward (`<` and `>` skip a whole
track), as do the `⏪`/`⏩` track), as do the `⏪`/`⏩`
buttons in buttons in
the transport bar. The `/` live filter is TUI-only for now. the transport bar, and the progress bar is clickable — a click seeks to
that point in the track. The `/` live filter is TUI-only for now.
The volume slider spans the server's whole accepted range, ending at 110%
— the server clamps there, so a slider that went further would have a
dead stretch at its right end. Its tooltip reports the current level as a
percentage; the TUI shows the same figure in its now-playing pane.
## How it is served ## How it is served

View File

@ -1587,3 +1587,23 @@ Not covered by a test: `Player::new` is only reachable through `Playback::new`,
and the engine thread opens an audio device, so there is no device-free way to and the engine thread opens an audio device, so there is no device-free way to
construct one here. `is_muted()` (a new engine getter — only `toggle_mute` construct one here. `is_muted()` (a new engine getter — only `toggle_mute`
existed, which cannot be used to *ask*) is likewise verified by reading. existed, which cannot be used to *ask*) is likewise verified by reading.
## Web volume slider bound (2026-07-27)
The slider was `max="1.5"` while the engine clamps to `1.1`, so its right third
was unreachable: the fill stopped at 73% of the track and the rest stayed empty
no matter how far the thumb was dragged. Fixing the server to broadcast the
level it actually took (previous entry) is what made this visible — the thumb
now springs back from that dead stretch instead of sitting where it was
dropped.
`max` is now a named `MAX_VOLUME` constant that has to track
`PlayerEngine::set_volume`'s clamp. It cannot be imported: `audio-player` is
native-only and the web client is wasm, so the two ends of this pair are kept
in agreement by the comment on each.
Raising the engine's clamp instead was the alternative — 1.1 is deliberate
headroom, and more gain risks clipping — so the slider was the side to move.
The tooltip also reports the level as a percentage now, which is the web
counterpart of the TUI's `Volume: 85%`.