Skip to content

feat: automatically equalize participant audio levels - #1689

Open
Prateek007rai wants to merge 1 commit into
suitenumerique:mainfrom
Prateek007rai:feat/audio-level-equalization
Open

Prateek007rai wants to merge 1 commit into
suitenumerique:mainfrom
Prateek007rai:feat/audio-level-equalization

Conversation

@Prateek007rai

@Prateek007rai Prateek007rai commented Sep 8, 2026

Copy link
Copy Markdown

Closes #1345

  • New hook useAudioLevelEqualization: samples remote participant audio levels every 500ms, smooths via EMA, and gradually adjusts each participant's HTML audio element volume toward a shared target level. Gain clamped to [0.2, 3.0]. Resets to 1.0 when disabled.
  • Store: adds audioLevelEqualizationEnabled (default off) to userChoicesStore
  • Settings: toggle in Audio tab under a new 'Audio level equalization' section
  • i18n: EN, FR, DE, NL translations added

Closes suitenumerique#1345

- New hook useAudioLevelEqualization: samples remote participant audio
  levels every 500ms, smooths via EMA, and gradually adjusts each
  participant's HTML audio element volume toward a shared target level.
  Gain clamped to [0.2, 3.0]. Resets to 1.0 when disabled.
- Store: adds audioLevelEqualizationEnabled (default off) to userChoicesStore
- Settings: toggle in Audio tab under a new 'Audio level equalization' section
- i18n: EN, FR, DE, NL translations added
@sonarqubecloud

sonarqubecloud Bot commented Sep 8, 2026

Copy link
Copy Markdown

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add automatic participant audio level equalization

✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Equalizes remote participant volumes using smoothed audio-level sampling and bounded adaptive
 gain.
• Adds an opt-in, persisted Audio settings toggle with activation telemetry.
• Localizes equalization controls in English, French, German, and Dutch.
Diagram

graph TD
  A["Audio settings"] -->|saves preference| B["Choices store"] -->|enables processing| D["Equalization hook"] -->|samples levels| E["LiveKit participants"] -->|updates state| F["EMA gain"] -->|sets volume| G["Audio elements"]
  C["Video conference"] -->|mounts| D
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Web Audio per-participant gain
  • ➕ Supports amplification above 1.0 through GainNode, unlike HTMLMediaElement.volume.
  • ➕ Avoids coupling equalization to queried renderer DOM elements.
  • ➕ Provides a better foundation for compression, limiting, and smoother gain ramps.
  • ➖ Requires AudioContext creation, autoplay handling, and per-track graph lifecycle management.
  • ➖ Adds browser audio-pipeline complexity and needs broader compatibility testing.
2. Attenuation-only element normalization
  • ➕ Retains the simple HTML audio element implementation.
  • ➕ Safely equalizes louder participants without creating a Web Audio processing graph.
  • ➖ Cannot amplify quiet participants.
  • ➖ May reduce the overall conference volume substantially.

Recommendation: Use a Web Audio GainNode when amplification above unity is required. HTMLMediaElement.volume only accepts values from 0 to 1, so the current maximum gain of 3.0 cannot be applied safely. The participant cleanup should also compare against an explicit SID set because LiveKit's remote participant map is identity-keyed.

Files changed (8) +156 / -1

Enhancement (4) +120 / -1
useAudioLevelEqualization.tsAdd adaptive remote-participant volume equalization hook +93/-0

Add adaptive remote-participant volume equalization hook

• Introduces a periodic controller that smooths LiveKit audio levels, calculates bounded per-participant gain, and applies it to microphone audio elements. It resets volumes when disabled and removes state for departed participants.

src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts

VideoConference.tsxMount audio equalization in video conferences +2/-0

Mount audio equalization in video conferences

• Invokes the equalization hook from the main conference prefab so it follows the active room lifecycle.

src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx

AudioTab.tsxAdd beta audio equalization setting +18/-1

Add beta audio equalization setting

• Adds a persisted Audio-tab switch for enabling equalization. Enabling the feature also emits an analytics event.

src/frontend/src/features/settings/components/tabs/AudioTab.tsx

userChoices.tsPersist the equalization preference +7/-0

Persist the equalization preference

• Extends local user choices with an audio equalization flag that defaults to disabled. Exposes a setter integrated with the store's existing persistence subscription.

src/frontend/src/stores/userChoices.ts

Documentation (4) +36 / -0
settings.jsonAdd German equalization translations +9/-0

Add German equalization translations

• Adds German labels, description, heading, and accessible enable/disable text for the new setting.

src/frontend/src/locales/de/settings.json

settings.jsonAdd English equalization copy +9/-0

Add English equalization copy

• Adds English labels, description, heading, and accessible enable/disable text for the new setting.

src/frontend/src/locales/en/settings.json

settings.jsonAdd French equalization translations +9/-0

Add French equalization translations

• Adds French labels, description, heading, and accessible enable/disable text for the new setting.

src/frontend/src/locales/fr/settings.json

settings.jsonAdd Dutch equalization translations +9/-0

Add Dutch equalization translations

• Adds Dutch labels, description, heading, and accessible enable/disable text for the new setting.

src/frontend/src/locales/nl/settings.json

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (2) 📜 Skill insights (0)

Grey Divider


Action required

1. Quiet speakers halt audio leveling 📎 Requirement gap ≡ Correctness
Description
tick clamps state.currentGain at 3.0 and writes it directly to HTMLMediaElement.volume,
whose valid range ends at 1.0. Whenever a participant’s sampled level is below the 0.12 target,
the calculated ratio can exceed one—for example, a level of 0.02 yields 1.25 on the first
update—so the assignment throws, prevents that quiet speaker from being boosted, and aborts the
interval callback before later participants are adjusted.
Code

src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[R64-67]

+        state.currentGain = Math.min(MAX_GAIN, Math.max(MIN_GAIN, state.currentGain))
+
+        const audioElement = getAudioElement(participant)
+        if (audioElement) audioElement.volume = state.currentGain
Evidence
The configured maximum gain is 3.0, and the ratio calculation produces values above 1.0 whenever
the sampled level is below the 0.12 target. Those values are assigned directly to the media
element’s bounded volume property, demonstrating how a quiet participant can throw during an
update and interrupt the stable per-participant adaptive gain processing required by compliance rule
2.

Use LiveKit participant audio levels for adaptive gain control
src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[59-67]
src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[7-10]
src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[58-67]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The equalizer writes gain values as high as `3.0` to `HTMLMediaElement.volume`, which only accepts values from `0` through `1`. This causes a range error instead of amplifying quiet participants and can stop the current interval callback before remaining participants are processed.

## Issue Context
The algorithm intentionally supports gains through `3.0`, so amplification requires an audio path that accepts gain above unity, such as a Web Audio `GainNode`. Tie the graph lifecycle and cleanup to each remote track, and ensure one participant’s gain failure cannot prevent processing the remaining participants. Do not merely clamp the value at `1.0`, because that would still fail the quiet-speaker use case.

## Fix Focus Areas
- src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[7-10]
- src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[58-67]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Users miss the setting explanation 📎 Requirement gap ≡ Correctness
Description
AudioTab renders an empty <div /> beside the switch instead of the translated
audio.audioLevelEqualization.description value. Whenever users evaluate this beta setting, they
see its label but not the available explanation of its continuous per-participant behavior.
Code

src/frontend/src/features/settings/components/tabs/AudioTab.tsx[R152-154]

+          {t('audio.audioLevelEqualization.label')}
+        </Switch>
+        <div />
Evidence
Compliance rule 4 requires the toggle to be accompanied by a one-line explanation. The settings row
ends with an empty element, while the English locale contains an unused description specifically
written for this setting.

Provide a clearly labeled audio-leveling setting
src/frontend/src/features/settings/components/tabs/AudioTab.tsx[140-155]
src/frontend/src/locales/en/settings.json[35-42]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The audio-leveling control omits the required one-line explanation even though translated description text was added.

## Issue Context
Render the description alongside the toggle using the settings tab's established text styling and translation function.

## Fix Focus Areas
- src/frontend/src/features/settings/components/tabs/AudioTab.tsx[140-155]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Equalization never reaches remote audio 🐞 Bug ≡ Correctness
Description
getAudioElement searches for an audio[data-lk-sid] node, but remote playback is delegated to
RoomAudioRenderer and no repository-owned renderer creates an audio element with that attribute.
Every tick therefore receives null and skips the only volume assignment, so enabling the setting
has no effect for remote participants.
Code

src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[R90-92]

+  return document.querySelector<HTMLAudioElement>(
+    `audio[data-lk-sid="${micPub.trackSid}"]`
+  )
Evidence
The conference mounts RoomAudioRenderer as its room-wide playback component, while
getAudioElement requires a track-SID attribute and the periodic path only changes volume when that
lookup succeeds. The participant tile likewise delegates audio rendering to LiveKit's AudioTrack
rather than creating an element carrying the queried attribute.

src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx[143-155]
src/frontend/src/features/participantTile/components/ParticipantTile.tsx[132-147]
src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[66-67]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Audio equalization searches for a `data-lk-sid` audio element that the current remote-audio rendering path does not expose, leaving the feature ineffective.

## Issue Context
Remote audio is rendered through LiveKit's `RoomAudioRenderer`. Integrate equalization with the actual rendered track or element rather than relying on an unsupported DOM attribute.

## Fix Focus Areas
- src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[83-92]
- src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx[143-155]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Pauses make the next words too loud 🐞 Bug ≡ Correctness
Description
smoothedLevel continues incorporating zero samples, while the speaking guard tests that slowly
decaying average rather than the current sample. After someone stops speaking the state remains
above 0.01 for many 500 ms ticks and keeps increasing the gain, so their next utterance starts
over-amplified.
Code

src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[R55-58]

+          state.smoothedLevel * (1 - ADAPTATION_RATE) + audioLevel * ADAPTATION_RATE
+
+        // Only adjust gain when participant is speaking, not on silence/noise floor
+        if (state.smoothedLevel > 0.01) {
Evidence
Ticks run every 500 ms with a 0.05 smoothing rate. Starting from a target-level average of 0.12,
repeated zero samples leave the average above the 0.01 guard for roughly 49 ticks, while the
target-to-average ratio grows throughout that period.

src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[7-11]
src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[42-61]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Silence is folded into the level average, but the previous average is then treated as evidence that the participant is still speaking. This drives gain upward during ordinary pauses.

## Issue Context
Gate adaptation using a current speaking signal or current raw level, and retain or initialize the speaking-level estimate separately so silence does not lower the reference used to calculate gain.

## Fix Focus Areas
- src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[42-62]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: ⚖️ Balanced: This adds new runtime audio-processing logic that interacts with LiveKit audio elements, persisted settings, and UI behavior, creating meaningful correctness and user-impact risks despite the localized scope.

Grey Divider

Tip of the day
💡 Did you know, you can tweak Display preferences with a live preview to see your comment before it ships

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +64 to +67
state.currentGain = Math.min(MAX_GAIN, Math.max(MIN_GAIN, state.currentGain))

const audioElement = getAudioElement(participant)
if (audioElement) audioElement.volume = state.currentGain

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Quiet speakers halt audio leveling 📎 Requirement gap ≡ Correctness

tick clamps state.currentGain at 3.0 and writes it directly to HTMLMediaElement.volume,
whose valid range ends at 1.0. Whenever a participant’s sampled level is below the 0.12 target,
the calculated ratio can exceed one—for example, a level of 0.02 yields 1.25 on the first
update—so the assignment throws, prevents that quiet speaker from being boosted, and aborts the
interval callback before later participants are adjusted.
Agent Prompt
## Issue description
The equalizer writes gain values as high as `3.0` to `HTMLMediaElement.volume`, which only accepts values from `0` through `1`. This causes a range error instead of amplifying quiet participants and can stop the current interval callback before remaining participants are processed.

## Issue Context
The algorithm intentionally supports gains through `3.0`, so amplification requires an audio path that accepts gain above unity, such as a Web Audio `GainNode`. Tie the graph lifecycle and cleanup to each remote track, and ensure one participant’s gain failure cannot prevent processing the remaining participants. Do not merely clamp the value at `1.0`, because that would still fail the quiet-speaker use case.

## Fix Focus Areas
- src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[7-10]
- src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[58-67]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +152 to +154
{t('audio.audioLevelEqualization.label')}
</Switch>
<div />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Users miss the setting explanation 📎 Requirement gap ≡ Correctness

AudioTab renders an empty <div /> beside the switch instead of the translated
audio.audioLevelEqualization.description value. Whenever users evaluate this beta setting, they
see its label but not the available explanation of its continuous per-participant behavior.
Agent Prompt
## Issue description
The audio-leveling control omits the required one-line explanation even though translated description text was added.

## Issue Context
Render the description alongside the toggle using the settings tab's established text styling and translation function.

## Fix Focus Areas
- src/frontend/src/features/settings/components/tabs/AudioTab.tsx[140-155]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +90 to +92
return document.querySelector<HTMLAudioElement>(
`audio[data-lk-sid="${micPub.trackSid}"]`
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Equalization never reaches remote audio 🐞 Bug ≡ Correctness

getAudioElement searches for an audio[data-lk-sid] node, but remote playback is delegated to
RoomAudioRenderer and no repository-owned renderer creates an audio element with that attribute.
Every tick therefore receives null and skips the only volume assignment, so enabling the setting
has no effect for remote participants.
Agent Prompt
## Issue description
Audio equalization searches for a `data-lk-sid` audio element that the current remote-audio rendering path does not expose, leaving the feature ineffective.

## Issue Context
Remote audio is rendered through LiveKit's `RoomAudioRenderer`. Integrate equalization with the actual rendered track or element rather than relying on an unsupported DOM attribute.

## Fix Focus Areas
- src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[83-92]
- src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx[143-155]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +55 to +58
state.smoothedLevel * (1 - ADAPTATION_RATE) + audioLevel * ADAPTATION_RATE

// Only adjust gain when participant is speaking, not on silence/noise floor
if (state.smoothedLevel > 0.01) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Pauses make the next words too loud 🐞 Bug ≡ Correctness

smoothedLevel continues incorporating zero samples, while the speaking guard tests that slowly
decaying average rather than the current sample. After someone stops speaking the state remains
above 0.01 for many 500 ms ticks and keeps increasing the gain, so their next utterance starts
over-amplified.
Agent Prompt
## Issue description
Silence is folded into the level average, but the previous average is then treated as evidence that the participant is still speaking. This drives gain upward during ordinary pauses.

## Issue Context
Gate adaptation using a current speaking signal or current raw level, and retain or initialize the speaking-level estimate separately so silence does not lower the reference used to calculate gain.

## Fix Focus Areas
- src/frontend/src/features/rooms/livekit/hooks/useAudioLevelEqualization.ts[42-62]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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.

Automatically equalize participant audio levels

1 participant