From 8f9b823d69f87a09365773ce1fff2af7351c937b Mon Sep 17 00:00:00 2001 From: byGalax Date: Wed, 22 Apr 2026 21:09:45 +0200 Subject: [PATCH] feat(call): per-participant volume up to 200% via WebAudio gain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HTMLMediaElement.volume caps at 1.0, so boosting a quiet peer past 100% needs an explicit GainNode in the output chain. New remoteAudioPipelines module owns one AudioContext + GainNode per remote audio track; attachTrack / detachTrack now create and tear down the pipeline alongside the LiveKit element. Once a track is on the WebAudio path its direct output is diverted (createMediaElementSource semantics), so audio.muted / volume can't drive output anymore. Deafen, watch-state, manual screen-share mute and per-user volume are collapsed into one effective-gain formula that gets recomputed on every state flip — the effect subscribes to both participantVolumes and screenShareVolumes for live slider drags. Slider ranges updated to 0–200% across: - ParticipantVolumeMenu (per-user right-click menu) - ParticipantsPopover (in-call participant list) - ScreenShareContextMenu (per-share right-click) Values above 100% render the percentage in amber as a soft hint that clipping is possible. Clamp in both volume stores extended to [0, 2] so persisted values survive. setAudioOutputDevice now additionally routes via AudioContext.setSinkId (Chrome 115+) for the WebAudio graph; the HTMLAudioElement.setSinkId fallback stays for older runtimes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/ParticipantVolumeMenu.tsx | 17 +- .../src/components/ParticipantsPopover.tsx | 9 +- .../src/components/ScreenShareContextMenu.tsx | 13 +- apps/desktop/src/context/CallContext.tsx | 184 +++++++++++------- apps/desktop/src/lib/participantVolumes.ts | 7 +- apps/desktop/src/lib/remoteAudioPipelines.ts | 151 ++++++++++++++ apps/desktop/src/lib/screenShareVolumes.ts | 6 +- 7 files changed, 311 insertions(+), 76 deletions(-) create mode 100644 apps/desktop/src/lib/remoteAudioPipelines.ts diff --git a/apps/desktop/src/components/ParticipantVolumeMenu.tsx b/apps/desktop/src/components/ParticipantVolumeMenu.tsx index 108b0a2..252a926 100644 --- a/apps/desktop/src/components/ParticipantVolumeMenu.tsx +++ b/apps/desktop/src/components/ParticipantVolumeMenu.tsx @@ -16,7 +16,7 @@ interface Props { } const MENU_W = 240; -const MENU_H = 84; +const MENU_H = 96; export function ParticipantVolumeMenu({ userId, @@ -63,14 +63,20 @@ export function ParticipantVolumeMenu({ >
{displayName} - + 1 ? 'text-amber-500' : 'text-fg-muted') + } + > {Math.round(volume * 100)}%
+ {/* Up to 200% via WebAudio gain. Values above 100% can clip on loud + mics — the amber count-up hints at that without a verbose warning. */} { @@ -81,6 +87,11 @@ export function ParticipantVolumeMenu({ aria-label={'Lautstärke ' + displayName} className="w-full accent-accent" /> +
+ 0% + 100% + 200% +
, document.body, ); diff --git a/apps/desktop/src/components/ParticipantsPopover.tsx b/apps/desktop/src/components/ParticipantsPopover.tsx index bf39f1f..e5acb6e 100644 --- a/apps/desktop/src/components/ParticipantsPopover.tsx +++ b/apps/desktop/src/components/ParticipantsPopover.tsx @@ -185,7 +185,7 @@ function Row({ row, speaking }: { row: ParticipantRow; speaking: boolean }) { { @@ -196,7 +196,12 @@ function Row({ row, speaking }: { row: ParticipantRow; speaking: boolean }) { aria-label={'Lautstärke ' + row.displayName} className="flex-1 accent-accent" /> - + 1 ? 'text-amber-500' : '') + } + > {Math.round(volume * 100)}% diff --git a/apps/desktop/src/components/ScreenShareContextMenu.tsx b/apps/desktop/src/components/ScreenShareContextMenu.tsx index 9550d0e..5c4fc76 100644 --- a/apps/desktop/src/components/ScreenShareContextMenu.tsx +++ b/apps/desktop/src/components/ScreenShareContextMenu.tsx @@ -104,14 +104,18 @@ export function ScreenShareContextMenu({ {t('app:call.share_volume', { defaultValue: 'Lautstärke' })} - + 1 ? 'text-amber-500' : 'text-fg-muted') + } + > {Math.round(volume * 100)}% { @@ -122,6 +126,11 @@ export function ScreenShareContextMenu({ aria-label={t('app:call.share_volume', { defaultValue: 'Lautstärke' })} className="w-full accent-accent" /> +
+ 0% + 100% + 200% +