From 913f7d70eab19e28a34c8cbc7114a28271864d7c Mon Sep 17 00:00:00 2001 From: Rick Rezinas Date: Sun, 19 Jul 2026 11:41:10 -0700 Subject: [PATCH] fix(voice): honour the volume argument on Linux players MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit playAudio() threads a volume multiplier through to buildArgs(), and the macOS afplay branch uses it, but every Linux candidate declares buildArgs: (file) => and silently discards it. Per-voice volume config and the notification API's volume field therefore do nothing on Linux — playback is always at system level, with no error. Map the multiplier (1.0 = normal, matching afplay -v) onto each player's own integer scale, read from their help output: ffplay -volume is 0..100, paplay --volume is linear 0..65536. A shared helper clamps and rounds; a non-finite or negative value falls back to normal rather than silencing playback. aplay has no volume-set option and is left alone. mpg123 scales with -f but its range was not verified here, so it is left alone rather than guessing a factor — both noted inline. No change to macOS behaviour. --- .../install/LIFEOS/PULSE/VoiceServer/voice.ts | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts b/LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts index 94086ad36f..9724991020 100644 --- a/LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts +++ b/LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts @@ -356,6 +356,15 @@ interface AudioPlayer { buildArgs: (file: string, volume: number) => string[] } +// `volume` is a multiplier where 1.0 is normal, matching afplay's -v. Each Linux +// player expresses volume on its own integer scale, so map onto that range and +// clamp. A non-finite or negative value falls back to the player's normal level +// rather than silencing playback. +function scaleVolume(volume: number, max: number): number { + if (!Number.isFinite(volume) || volume < 0) return max + return Math.min(max, Math.round(volume * max)) +} + let resolvedPlayer: AudioPlayer | null | undefined = undefined function resolveAudioPlayer(): AudioPlayer | null { @@ -365,9 +374,30 @@ function resolveAudioPlayer(): AudioPlayer | null { process.platform === "darwin" ? [{ cmd: "afplay", buildArgs: (file, volume) => ["-v", volume.toString(), file] }] : [ - { cmd: "ffplay", buildArgs: (file) => ["-nodisp", "-autoexit", "-loglevel", "quiet", file] }, + { + cmd: "ffplay", + // ffplay -h: "-volume volume set startup volume 0=min 100=max" + buildArgs: (file, volume) => [ + "-nodisp", + "-autoexit", + "-loglevel", + "quiet", + "-volume", + String(scaleVolume(volume, 100)), + file, + ], + }, + // mpg123 scales with -f, but its range was not verified here; left as-is + // rather than guessing a factor. { cmd: "mpg123", buildArgs: (file) => ["-q", file] }, - { cmd: "paplay", buildArgs: (file) => [file] }, + { + cmd: "paplay", + // paplay --help: "--volume=VOLUME Specify the initial (linear) volume + // in range 0...65536" + buildArgs: (file, volume) => [`--volume=${scaleVolume(volume, 65536)}`, file], + }, + // aplay has no volume-set option (only --disable-softvol), so volume + // cannot be honoured on this fallback. { cmd: "aplay", buildArgs: (file) => ["-q", file] }, ]