audio-compressor plugin: add Auto Track Gain to boost quiet tracks#4440
audio-compressor plugin: add Auto Track Gain to boost quiet tracks#4440emilichu wants to merge 1 commit intopear-devs:masterfrom
Conversation
📝 WalkthroughWalkthroughThe audio-compressor plugin now rebuilds audio routing via a dedicated Chain and applies configurable track-gain compensation computed from YouTube loudness metadata. New i18n keys add menu labels for enabling auto track gain and selecting a maximum gain cap. Changes
Sequence DiagramsequenceDiagram
participant Plugin as Audio-Compressor Plugin
participant Player as YouTube Player
participant Chain as Audio Chain
participant Context as AudioContext
Player->>Plugin: peard:audio-can-play
Plugin->>Player: request loudnessDb / perceptualLoudnessDb (with retries)
Player-->>Plugin: loudness metadata (or null)
Plugin->>Plugin: compute track gain (clamp to maxTrackGainDb)
Plugin->>Chain: rebuild routing (disconnect source → destination)
Chain->>Context: connect source → compressor → trackGain → destination
Plugin->>Chain: apply linear ramp to trackGain
Chain-->>Context: adjusted output level
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
src/plugins/audio-compressor.ts (3)
14-14: Consider exporting/aligningMAX_TRACK_GAIN_CHOICESwith the config default.
maxTrackGainDbdefaults to12(line 103, 182), which is a member ofMAX_TRACK_GAIN_CHOICES. If a user (or future config migration) setsmaxTrackGainDbto a value not in this list, no radio item will be checked in the menu — the user has no visual indication of the current setting. Either narrow the type ((typeof MAX_TRACK_GAIN_CHOICES)[number]) or add a defensive fallback that highlights the closest choice.-const MAX_TRACK_GAIN_CHOICES = [6, 9, 12, 15, 18, 24] as const; +const MAX_TRACK_GAIN_CHOICES = [6, 9, 12, 15, 18, 24] as const; +type MaxTrackGainDb = (typeof MAX_TRACK_GAIN_CHOICES)[number];…and tighten
maxTrackGainDb: MaxTrackGainDbinAudioCompressorPluginConfig.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/plugins/audio-compressor.ts` at line 14, Export and narrow MAX_TRACK_GAIN_CHOICES and align the config type so maxTrackGainDb is constrained to those choices: change MAX_TRACK_GAIN_CHOICES to an exported const and use a union type like (typeof MAX_TRACK_GAIN_CHOICES)[number] for AudioCompressorPluginConfig.maxTrackGainDb (i.e., tighten the type on maxTrackGainDb in AudioCompressorPluginConfig). Also add a defensive runtime fallback wherever the UI builds the radio items (or reads maxTrackGainDb) to map any unexpected numeric value to the closest entry in MAX_TRACK_GAIN_CHOICES so at least one radio item is checked and the UI reflects the effective setting.
106-128: Add a clarifying comment explaining theperceptualLoudnessDbfallback.The fallback from
loudnessDbtoperceptualLoudnessDbis semantically different:loudnessDbrepresents standard loudness normalization (LUFS-based), whileperceptualLoudnessDbuses a psychoacoustic model weighted for human perception. Both serve loudness normalization but measure different aspects. A brief inline comment explaining why this fallback is acceptable (e.g., "fallback for content without explicit loudness metadata, perceptual loudness provides reasonable estimate") would clarify the choice. No change required if behavior matches PR testing.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/plugins/audio-compressor.ts` around lines 106 - 128, Add a brief inline comment inside getContentLoudnessDb explaining why perceptualLoudnessDb is used as a fallback for loudnessDb: note that loudnessDb is the LUFS/standard loudness value while perceptualLoudnessDb is a psychoacoustic estimate and is acceptable as a fallback for content lacking explicit loudness metadata; place this comment near the assignment of loudnessDb (the expression using response?.playerConfig?.audioConfig?.loudnessDb ?? response?.playerConfig?.audioConfig?.perceptualLoudnessDb) so readers understand the semantic difference and rationale.
71-77: Cancel scheduled gain values before scheduling a new ramp.
linearRampToValueAtTimeappends a new ramp event to the AudioParam's timeline, starting from the value at the previous event's end time. IfapplyTrackGainis invoked again before the prior 0.1s ramp completes (e.g., rapid track switching oronConfigChangefiring during playback), the new ramp will start from a partially-completed value, creating audible artifacts or incorrect target gain. Mixing direct value writes and ramps without canceling scheduled events is error-prone.♻️ Anchor the current value, then ramp
applyTrackGain(gainDb: number) { if (!this.context || !this.trackGain) return; + const now = this.context.currentTime; + this.trackGain.gain.cancelScheduledValues(now); + this.trackGain.gain.setValueAtTime(this.trackGain.gain.value, now); this.trackGain.gain.linearRampToValueAtTime( dbToLinear(gainDb), - this.context.currentTime + 0.1, + now + 0.1, ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/plugins/audio-compressor.ts` around lines 71 - 77, applyTrackGain currently appends ramps without clearing prior scheduled events causing ramps to start from in-flight values; update applyTrackGain to first anchor and cancel previous schedules on trackGain.gain by calling cancelScheduledValues(this.context.currentTime) (or cancelAndHoldAtTime where supported), set the current instantaneous value with setValueAtTime(dbToLinear(currentDb) or getValueAtTime equivalent) at this.context.currentTime, then schedule the linearRampToValueAtTime to dbToLinear(gainDb) at this.context.currentTime + 0.1; use the existing symbols applyTrackGain, this.trackGain.gain, dbToLinear and this.context.currentTime to locate and modify the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/plugins/audio-compressor.ts`:
- Around line 130-156: The retry timeouts in updateTrackGain() must be tracked
and cancelled to avoid races and work after teardown: store the timeout id when
calling setTimeout in updateTrackGain (e.g., lastUpdateTrackGainTimer),
clearTimeout(lastUpdateTrackGainTimer) before scheduling a new retry (at the top
of audioCanPlayHandler) and in stop() to cancel pending retries; keep the
retriesLeft logic but ensure the scheduled function uses the stored id
lifecycle. Also gate the console.log behind a debug flag in currentConfig (or
remove it for release) so logs only emit when currentConfig.debug is true.
- Around line 158-163: The chain is always rebuilt with a new GainNode in
audioCanPlayHandler (calling chain.build(audioSource, audioContext)) even when
autoTrackGain is disabled; change the logic so the chain topology only includes
the trackGain node when currentConfig.autoTrackGain is true: either (A) wrap the
chain.build/updateTrackGain calls in audioCanPlayHandler with a conditional on
currentConfig.autoTrackGain so you only create/insert trackGain when enabled, or
(B) modify chain.build(...) to accept a flag or read currentConfig.autoTrackGain
and conditionally create/attach trackGain inside chain.build; ensure
applyTrackGain(0) is only used to set gain when the node exists and
updateTrackGain and any references to trackGain handle the absent node safely.
---
Nitpick comments:
In `@src/plugins/audio-compressor.ts`:
- Line 14: Export and narrow MAX_TRACK_GAIN_CHOICES and align the config type so
maxTrackGainDb is constrained to those choices: change MAX_TRACK_GAIN_CHOICES to
an exported const and use a union type like (typeof
MAX_TRACK_GAIN_CHOICES)[number] for AudioCompressorPluginConfig.maxTrackGainDb
(i.e., tighten the type on maxTrackGainDb in AudioCompressorPluginConfig). Also
add a defensive runtime fallback wherever the UI builds the radio items (or
reads maxTrackGainDb) to map any unexpected numeric value to the closest entry
in MAX_TRACK_GAIN_CHOICES so at least one radio item is checked and the UI
reflects the effective setting.
- Around line 106-128: Add a brief inline comment inside getContentLoudnessDb
explaining why perceptualLoudnessDb is used as a fallback for loudnessDb: note
that loudnessDb is the LUFS/standard loudness value while perceptualLoudnessDb
is a psychoacoustic estimate and is acceptable as a fallback for content lacking
explicit loudness metadata; place this comment near the assignment of loudnessDb
(the expression using response?.playerConfig?.audioConfig?.loudnessDb ??
response?.playerConfig?.audioConfig?.perceptualLoudnessDb) so readers understand
the semantic difference and rationale.
- Around line 71-77: applyTrackGain currently appends ramps without clearing
prior scheduled events causing ramps to start from in-flight values; update
applyTrackGain to first anchor and cancel previous schedules on trackGain.gain
by calling cancelScheduledValues(this.context.currentTime) (or
cancelAndHoldAtTime where supported), set the current instantaneous value with
setValueAtTime(dbToLinear(currentDb) or getValueAtTime equivalent) at
this.context.currentTime, then schedule the linearRampToValueAtTime to
dbToLinear(gainDb) at this.context.currentTime + 0.1; use the existing symbols
applyTrackGain, this.trackGain.gain, dbToLinear and this.context.currentTime to
locate and modify the code.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5468164e-7988-4516-962b-b1f3bd1e9ce9
📒 Files selected for processing (2)
src/i18n/resources/en.jsonsrc/plugins/audio-compressor.ts
| const audioCanPlayHandler = ({ | ||
| detail: { audioSource, audioContext }, | ||
| }: CustomEvent<Compressor>) => { | ||
| storage.connectToCompressor( | ||
| audioSource, | ||
| audioContext, | ||
| createCompressorNode(audioContext), | ||
| ); | ||
| chain.build(audioSource, audioContext); | ||
| updateTrackGain(); | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does inserting an extra GainNode (gain=1) into a Web Audio graph cause any audible difference or measurable latency?
💡 Result:
No, inserting an extra GainNode with gain=1 into a Web Audio graph does not cause any audible difference and adds no measurable latency.
Citations:
- 1: https://stackoverflow.com/questions/33422371/simple-pass-through-audionode-in-web-audio-api/33424384
- 2: https://padenot.github.io/web-audio-perf/
- 3: https://developer.mozilla.org/en-US/docs/Web/API/GainNode
🏁 Script executed:
# First, let's look at the structure of the audio-compressor.ts file around the mentioned lines
cat -n src/plugins/audio-compressor.ts | head -180 | tail -50Repository: pear-devs/pear-desktop
Length of output: 1911
🏁 Script executed:
# Also get the full context of the handler and related code
wc -l src/plugins/audio-compressor.tsRepository: pear-devs/pear-desktop
Length of output: 102
🏁 Script executed:
# Search for chain.build implementation and autoTrackGain usage
rg "chain\.build" src/plugins/audio-compressor.ts -B 2 -A 5Repository: pear-devs/pear-desktop
Length of output: 562
🏁 Script executed:
# Search for autoTrackGain references to understand how it gates behavior
rg "autoTrackGain" src/plugins/audio-compressor.ts -B 2 -A 2Repository: pear-devs/pear-desktop
Length of output: 788
🏁 Script executed:
# Look for onConfigChange handler to understand config change handling
rg "onConfigChange|currentConfig" src/plugins/audio-compressor.ts -B 1 -A 3Repository: pear-devs/pear-desktop
Length of output: 886
🏁 Script executed:
# Find where chain is defined
rg "const chain|let chain|var chain|export.*chain" src/plugins/audio-compressor.ts -B 2 -A 5Repository: pear-devs/pear-desktop
Length of output: 212
🏁 Script executed:
# Look for the chain implementation - it might be in a separate file or defined in this file
rg "chain\s*=" src/plugins/audio-compressor.ts -B 2 -A 20 | head -100Repository: pear-devs/pear-desktop
Length of output: 637
🏁 Script executed:
# Search for chain object definition - might be createChain or similar
rg "createChain|new.*Chain|function.*chain" src/plugins/audio-compressor.ts -B 2 -A 10Repository: pear-devs/pear-desktop
Length of output: 338
🏁 Script executed:
# Find the Chain class definition - it's likely imported or in this file
rg "class Chain|import.*Chain" src/plugins/audio-compressor.ts -B 2 -A 20Repository: pear-devs/pear-desktop
Length of output: 615
🏁 Script executed:
# If Chain is defined in another file, find it
fd "chain" -t f src/Repository: pear-devs/pear-desktop
Length of output: 48
🏁 Script executed:
# Search for Chain in the entire plugins directory
rg "class Chain" src/plugins/Repository: pear-devs/pear-desktop
Length of output: 112
🏁 Script executed:
# Get the full Chain class definition including the build method
cat -n src/plugins/audio-compressor.ts | sed -n '1,246p' | grep -A 100 "class Chain"Repository: pear-devs/pear-desktop
Length of output: 3521
🏁 Script executed:
# Or more specifically, get lines from the Chain class
sed -n '/^class Chain/,/^}/p' src/plugins/audio-compressor.tsRepository: pear-devs/pear-desktop
Length of output: 1841
Consider whether the extra GainNode should be inserted conditionally based on autoTrackGain.
The chain is rebuilt on every audio-can-play event with a new GainNode inserted between the compressor and destination, regardless of whether autoTrackGain is enabled. When autoTrackGain is disabled, applyTrackGain(0) sets the gain to 1, making the node transparent—but the node remains in the audio graph and routing changes from the original (likely direct compressor-to-destination path).
While a GainNode with gain=1 causes no audible difference or measurable latency, this still represents an unconditional topology change. If the intent is to keep the old path identical when auto-track-gain is off, consider conditionally building the chain only when currentConfig.autoTrackGain === true, or restructure to conditionally insert the trackGain node during chain.build() based on the config.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/plugins/audio-compressor.ts` around lines 158 - 163, The chain is always
rebuilt with a new GainNode in audioCanPlayHandler (calling
chain.build(audioSource, audioContext)) even when autoTrackGain is disabled;
change the logic so the chain topology only includes the trackGain node when
currentConfig.autoTrackGain is true: either (A) wrap the
chain.build/updateTrackGain calls in audioCanPlayHandler with a conditional on
currentConfig.autoTrackGain so you only create/insert trackGain when enabled, or
(B) modify chain.build(...) to accept a flag or read currentConfig.autoTrackGain
and conditionally create/attach trackGain inside chain.build; ensure
applyTrackGain(0) is only used to set gain when the node exists and
updateTrackGain and any references to trackGain handle the absent node safely.
There was a problem hiding this comment.
The always-present GainNode is intentional, it's what enables autoTrackGain to be toggled live (mid-track) without rebuilding the audio chain. The earlier draft of this PR conditionally rebuilt the chain on config change and I hit a class of bugs where toggles only took effect on the first switch (the second toggle would silently no-op due to disconnect/reconnect ordering issues with the source node). Keeping a transparent gain node always in the graph and only mutating its gain.value sidesteps that entirely.
Since (as the web search confirms) a GainNode with gain=1 has zero audible/perf cost, I'd rather keep the topology stable and the toggle behavior reliable than save one node when the feature is off.
Happy to revisit if a maintainer prefers the conditional-insert path though.
There was a problem hiding this comment.
Only users with a collaborator, contributor, member, or owner role can interact with CodeRabbit.
Reads YouTube's per-track loudnessDb metadata and applies compensating gain to tracks quieter than the reference level. Closes pear-devs#3032
6e19d66 to
57f7fc6
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/plugins/audio-compressor.ts (1)
71-77: Anchor current value before scheduling a fresh ramp.
linearRampToValueAtTimeappends an automation event without invalidating any previously scheduled ones. Two ramps scheduled within the 0.1 s window (e.g., a fastonConfigChangefollowed by anupdateTrackGainretry resolving) can produce a piecewise trajectory that overshoots or stalls before reaching the new target. The conventional Web Audio idiom is to cancel future automation and anchor the current value first.♻️ Suggested fix
applyTrackGain(gainDb: number) { if (!this.context || !this.trackGain) return; - this.trackGain.gain.linearRampToValueAtTime( - dbToLinear(gainDb), - this.context.currentTime + 0.1, - ); + const now = this.context.currentTime; + const gain = this.trackGain.gain; + gain.cancelScheduledValues(now); + gain.setValueAtTime(gain.value, now); + gain.linearRampToValueAtTime(dbToLinear(gainDb), now + 0.1); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/plugins/audio-compressor.ts` around lines 71 - 77, In applyTrackGain, before calling this.trackGain.gain.linearRampToValueAtTime, cancel any future automation and anchor the current value so we don't create overlapping ramps: call this.trackGain.gain.cancelScheduledValues(this.context.currentTime), set the gain to the current instantaneous value (e.g., this.trackGain.gain.setValueAtTime(this.trackGain.gain.value, this.context.currentTime)) and then schedule linearRampToValueAtTime(dbToLinear(gainDb), this.context.currentTime + 0.1); this uses this.context, this.trackGain and dbToLinear to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/plugins/audio-compressor.ts`:
- Around line 71-77: In applyTrackGain, before calling
this.trackGain.gain.linearRampToValueAtTime, cancel any future automation and
anchor the current value so we don't create overlapping ramps: call
this.trackGain.gain.cancelScheduledValues(this.context.currentTime), set the
gain to the current instantaneous value (e.g.,
this.trackGain.gain.setValueAtTime(this.trackGain.gain.value,
this.context.currentTime)) and then schedule
linearRampToValueAtTime(dbToLinear(gainDb), this.context.currentTime + 0.1);
this uses this.context, this.trackGain and dbToLinear to locate the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 323cffb0-19bf-430a-b0a8-b43bfd49a306
📒 Files selected for processing (2)
src/i18n/resources/en.jsonsrc/plugins/audio-compressor.ts
✅ Files skipped from review due to trivial changes (1)
- src/i18n/resources/en.json
Summary
The Audio Compressor plugin tames loud peaks but never raises quiet tracks, so playlists with mixed loudness still
need constant manual volume adjustment. This adds an opt-in Auto Track Gain mode that reads YouTube's
playerConfig.audioConfig.loudnessDbper track and applies compensating gain (capped, configurable) to anything belowthe reference level.
Closes #3032.
Changes
Chainclass wiressource → DynamicsCompressor → trackGain → destinationonce onpeard:audio-can-playandadjusts
trackGainper track instead of swapping nodes.onConfigChangeupdates the liveGainNodeinstead of rebuilding the chain.enabled.
plugins.audio-compressor.menu.Test plan
[audio-compressor] track loudness X.X dB → track gain Y.Y dBon each track change.Summary by CodeRabbit
New Features
Localization