fix: previews follow the app volume; volume bar fill always accent
The preview backend never received a volume — clips played at the crate default (0.7) regardless of the slider, even muted. The session now sets the preview volume from the app store (mute → 0) before the first load and mirrors it live while the modal is open; the debug overlay's PREVIEW row exposes the backend's resting volume as proof. The player-bar volume (and fallback seek) fill is now always the accent color — matching the waveform scrubber's played region — stepping up to the hover shade on hover instead of white-until-hover. Also syncs package-lock to the soulstream name + alpha version.
This commit is contained in:
1 parent
793f78370e
commit
691fdcdc9a
10 files changed
+63
-10
No files matched your search
Generated
+4
-4
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "soulseekplus",
|
||||
"version": "0.1.0",
|
||||
"name": "soulstream",
|
||||
"version": "0.1.0-alpha.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "soulseekplus",
|
||||
"version": "0.1.0",
|
||||
"name": "soulstream",
|
||||
"version": "0.1.0-alpha.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2"
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "soulstream",
|
||||
"version": "0.1.0-alpha.1",
|
||||
"type": "module",
|
||||
"description": "SoulStream \u2014 music player, downloader and streamer for the Soulseek network",
|
||||
"description": "SoulStream — music player, downloader and streamer for the Soulseek network",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -79,6 +79,8 @@ pub struct DebugStats {
|
||||
pub normalization_enabled: bool,
|
||||
pub target_lufs: f64,
|
||||
pub loudness: Option<crate::loudness::CurrentTrack>,
|
||||
/// The preview voices' resting volume (mirrors the app volume; 0 = muted).
|
||||
pub preview_volume: Option<f32>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -163,6 +165,9 @@ pub fn debug_stats(app: tauri::AppHandle, state: State<'_, DiagState>) -> DebugS
|
||||
normalization_enabled,
|
||||
target_lufs,
|
||||
loudness,
|
||||
preview_volume: app
|
||||
.try_state::<crate::preview::PreviewState>()
|
||||
.map(|s| crate::preview::target_volume(&s)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,9 +34,10 @@ const PREVIEW_POSITION_INTERVAL: Duration = Duration::from_millis(100);
|
||||
/// smooth — while each step is one cheap volume command to the sink.
|
||||
const FADE_TICK: Duration = Duration::from_millis(15);
|
||||
|
||||
/// The preview voices' resting volume. The player crate's own default; the
|
||||
/// renderer never sets a preview volume today, so fades ramp to this unless
|
||||
/// a `volume` control changes the target.
|
||||
/// The preview voices' resting volume before the renderer speaks. Since
|
||||
/// 2026-07-23 the renderer mirrors the APP volume (mute → 0) via the
|
||||
/// `volume` control at session open and live while the modal is up, so
|
||||
/// this default only covers the instant before that first set arrives.
|
||||
const DEFAULT_TARGET_VOL: f32 = 0.7;
|
||||
|
||||
/// Lease-lifetime constants, mirroring `src/lib/preview/lease.js` — the two
|
||||
@@ -111,6 +112,12 @@ impl PreviewState {
|
||||
}
|
||||
}
|
||||
|
||||
/// The preview voices' current resting volume — diagnostics readout, so the
|
||||
/// debug overlay can prove the renderer's app-volume mirror actually lands.
|
||||
pub fn target_volume(state: &PreviewState) -> f32 {
|
||||
state.target()
|
||||
}
|
||||
|
||||
/// How long a crossfade waits for the incoming voice to become audible
|
||||
/// before ramping anyway. Local-file decode start is tens of ms; hitting
|
||||
/// this means the clip is broken (its error event fails the item) and the
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
<b>PREVIEW</b>
|
||||
{previewUi.snapshot?.phase ?? "—"} · gen {previewUi.snapshot?.requestGeneration ?? "—"} ·
|
||||
idx {previewUi.snapshot?.activeManifestIndex ?? "—"}
|
||||
{#if stats.previewVolume != null}· vol {stats.previewVolume.toFixed(2)}{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -706,15 +706,17 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Always the accent — matching the waveform scrubber's played region
|
||||
(user decision 2026-07-23; the old white-until-hover read as off). */
|
||||
.fill {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background: var(--text-primary);
|
||||
background: var(--accent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.track:hover .fill {
|
||||
background: var(--accent);
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* waveform scrubber */
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
previewCommands,
|
||||
closePreview,
|
||||
} from "../lib/preview/previewSession.svelte.js";
|
||||
import { player } from "../lib/state.svelte.js";
|
||||
import {
|
||||
playableManifestIndices,
|
||||
playableCursor,
|
||||
@@ -25,6 +26,14 @@
|
||||
} from "../lib/preview/session.js";
|
||||
import { accentFromImage } from "../lib/color.js";
|
||||
|
||||
// Preview audio follows the APP volume — mute means silent previews
|
||||
// too. Mirrored live while the modal is open (the session also sets it
|
||||
// once at open, before the first load).
|
||||
$effect(() => {
|
||||
if (!previewUi.open) return;
|
||||
previewCommands.setVolume(player.muted ? 0 : player.volume);
|
||||
});
|
||||
|
||||
const st = $derived(previewUi.snapshot);
|
||||
const manifest = $derived(st?.manifest ?? null);
|
||||
const items = $derived(manifest?.items ?? []);
|
||||
|
||||
@@ -30,6 +30,7 @@ let service = null;
|
||||
let unsubscribe = null;
|
||||
let focusParts = null; // { coordinator, gateway }
|
||||
let demoTicker = null;
|
||||
let readPlayerRef = null; // injected with the bridge; app volume source
|
||||
|
||||
// ------------------------------------------------------------- gateway ----
|
||||
|
||||
@@ -50,6 +51,7 @@ export function notePlaybackGeneration() {
|
||||
|
||||
/** One-time wiring from state.svelte.js. */
|
||||
export function initPreviewBridge({ readPlayer, resumeMain }) {
|
||||
readPlayerRef = readPlayer;
|
||||
focusParts = createProductionFocus({
|
||||
readPlayer,
|
||||
readPlaybackGeneration: () => playbackGeneration,
|
||||
@@ -81,6 +83,11 @@ export async function openPreview(manifest, { playFull } = {}) {
|
||||
focus: focusParts,
|
||||
})
|
||||
);
|
||||
// Preview volume follows the APP volume (mute = 0) — set it BEFORE the
|
||||
// first load so the opening clip can't blast at the backend default.
|
||||
// The overlay keeps it mirrored live while the modal is open.
|
||||
const p = readPlayerRef?.();
|
||||
if (p) service.setVolume(p.muted ? 0 : (p.volume ?? 0.7));
|
||||
previewUi.open = true;
|
||||
await service.open({ sessionId: crypto.randomUUID(), manifest });
|
||||
}
|
||||
@@ -176,4 +183,5 @@ export const previewCommands = {
|
||||
seek: (ms) => service?.seek(ms),
|
||||
setMuted: (v) => service?.setMuted(v),
|
||||
playFullTrack: (starter) => service?.playFullTrack(starter),
|
||||
setVolume: (v) => service?.setVolume(v),
|
||||
};
|
||||
@@ -348,6 +348,13 @@ export function createPreviewService({
|
||||
if (value && state.phase === PHASE.PLAYING) void transport.pause();
|
||||
},
|
||||
|
||||
/** Mirror the APP's volume (and mute, as 0) onto the preview
|
||||
* transport — previews must never play louder than the player bar
|
||||
* says. No-op for transports without a volume control (fixture). */
|
||||
setVolume(v) {
|
||||
void transport.setVolume?.(v);
|
||||
},
|
||||
|
||||
async togglePlay() {
|
||||
if (!state) return;
|
||||
const sid = state.sessionId;
|
||||
|
||||
@@ -72,6 +72,9 @@ function makeTransport() {
|
||||
async seek(ms) {
|
||||
log.push(["seek", ms]);
|
||||
},
|
||||
async setVolume(v) {
|
||||
log.push(["setVolume", v]);
|
||||
},
|
||||
async stop() {
|
||||
ctx = null;
|
||||
log.push(["stop"]);
|
||||
@@ -259,6 +262,17 @@ test("auto-play: the LAST playable track never advances early", async () => {
|
||||
assert.equal(service.getState().phase, PHASE.ENDED);
|
||||
});
|
||||
|
||||
test("setVolume forwards the app volume to the transport", async () => {
|
||||
const { service, transport, manifest } = makeService();
|
||||
await service.open({ sessionId: SID, manifest });
|
||||
service.setVolume(0.35);
|
||||
service.setVolume(0); // app mute → silent previews
|
||||
assert.deepEqual(
|
||||
transport.log.filter((e) => e[0] === "setVolume"),
|
||||
[["setVolume", 0.35], ["setVolume", 0]]
|
||||
);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------- resume memory ----
|
||||
|
||||
test("resume: stepping back to a track picks up where it left off", async () => {
|
||||
|
||||
Reference in new issue
Block a user