I've found that the equalizer works weirdly, and makes the output very quiet. I've included Claude's explanation of the issue and a possible solution:
Raising any equalizer band lowers the overall playback level by more than the band gain adds. The effect is immediate and persists until every band is returned to 0, at which point the app disables the effect and normal level returns. Cutting bands does not show the problem.
The volume boost (LoudnessEnhancer) is not involved.
Environment
Reported on Nothing Phone (2), Nothing OS (close to stock AOSP). Expected on any device that uses the AOSP LVM effect bundle.
Steps to reproduce
- Start playback.
- Open Settings > Equalizer.
- Raise any single band by 1 dB or more.
- Overall loudness drops immediately, by more than the band gain.
- Return all bands to 0. Normal loudness returns.
Expected
A +6 dB band gain boosts that band by 6 dB and leaves the rest of the spectrum unchanged.
Actual
The whole output is attenuated. Measured against the AOSP algorithm, for a single band at 230 Hz:
| Band gain set |
Global attenuation |
Net on that band |
Net on all other bands |
| +1 dB |
-3 dB |
-2 dB |
-3 dB |
| +6 dB |
-7 dB |
-1 dB |
-7 dB |
| +15 dB |
-10 dB |
+5 dB |
-10 dB |
Root cause
This is not a defect in our code. It is the documented behaviour of the AOSP equalizer effect that PlaybackEnhancerService uses through android.media.audiofx.Equalizer.
BundleContext::limitLevel() in frameworks/av/media/libeffects/lvm/wrapper/Aidl/ performs automatic headroom management to prevent clipping. It estimates the energy the positive band gains could add, then attenuates the entire output by
that amount:
for each band:
bandFactor = bandGainMb / 1500.0
bandEnergy = bandFactor * coeff[band]^2
if bandEnergy > 0: energyContribution += bandEnergy
totalEnergy = sqrt(energyContribution + crossTerms)
maxLevelRound = (int)(totalEnergy + 0.99) // ceiling
VC_EffectLevel = volumeDb - maxLevelRound // global attenuation
coeff is kBandEnergyCoefficient = {7.56, 9.69, 9.59, 7.37, 2.88} in BundleTypes.h. The correction is recomputed from the current band gains whenever the effect is enabled or the stream volume is updated.
Two consequences match the report exactly:
- Only positive gains contribute (
if bandEnergy > 0), so cuts cause no global attenuation. Only boosts trigger the problem.
- The ceiling rounding means even a 1 dB nudge costs a full 3 dB.
We cannot compensate for this while keeping the current API. The equalizer bundle declares Flags::Volume::CTRL, so it owns the volume stage of the effect chain. There is no gain stage available to us downstream of it.
Recommended fix
Replace android.media.audiofx.Equalizer with android.media.audiofx.DynamicsProcessing.
DynamicsProcessing is available since API 28 and minSdk is already 28. It applies no automatic headroom attenuation, it exposes a real input gain stage, and it provides a limiter stage. A +6 dB band gain becomes an actual +6 dB band gain.
Scope:
PlaybackEnhancerService.attachEqualizer / applyEqualizer: build a DynamicsProcessing.Config with a multi-band post-EQ stage and the limiter stage enabled, then set per-band gains.
EqualizerBandProvider: DynamicsProcessing does not report a device band layout, so define the band layout in the app. Keeping the existing 60 / 230 / 910 / 3600 / 14000 Hz layout and a -15..+15 dB range preserves the current UI. The probe becomes a simple "can the effect be constructed" check.
- No change needed in
EqualizerSettings, PlaybackPreferences, SettingsViewModel or the UI.
- If the effect cannot be created, return
EqualizerCapabilities.Unavailable. The existing code path already hides the equalizer row in that case.
Risks
- The limiter stage must be enabled. Once
DynamicsProcessing replaces the platform equalizer, nothing protects against clipping on large boosts, so distortion would replace the current quieting.
DynamicsProcessing support varies between vendors. Needs testing on real hardware, including at least one non-stock ROM, before release.
- Existing stored gains stay valid; the persisted format does not change.
Out of scope
- No compressor or dynamics feature is being added.
DynamicsProcessing is used only as an equalizer with a gain stage and a limiter.
- No change to the volume boost (
LoudnessEnhancer).
I've found that the equalizer works weirdly, and makes the output very quiet. I've included Claude's explanation of the issue and a possible solution:
Raising any equalizer band lowers the overall playback level by more than the band gain adds. The effect is immediate and persists until every band is returned to 0, at which point the app disables the effect and normal level returns. Cutting bands does not show the problem.
The volume boost (
LoudnessEnhancer) is not involved.Environment
Reported on Nothing Phone (2), Nothing OS (close to stock AOSP). Expected on any device that uses the AOSP LVM effect bundle.
Steps to reproduce
Expected
A +6 dB band gain boosts that band by 6 dB and leaves the rest of the spectrum unchanged.
Actual
The whole output is attenuated. Measured against the AOSP algorithm, for a single band at 230 Hz:
Root cause
This is not a defect in our code. It is the documented behaviour of the AOSP equalizer effect that
PlaybackEnhancerServiceuses throughandroid.media.audiofx.Equalizer.BundleContext::limitLevel()inframeworks/av/media/libeffects/lvm/wrapper/Aidl/performs automatic headroom management to prevent clipping. It estimates the energy the positive band gains could add, then attenuates the entire output bythat amount:
coeffiskBandEnergyCoefficient = {7.56, 9.69, 9.59, 7.37, 2.88}inBundleTypes.h. The correction is recomputed from the current band gains whenever the effect is enabled or the stream volume is updated.Two consequences match the report exactly:
if bandEnergy > 0), so cuts cause no global attenuation. Only boosts trigger the problem.We cannot compensate for this while keeping the current API. The equalizer bundle declares
Flags::Volume::CTRL, so it owns the volume stage of the effect chain. There is no gain stage available to us downstream of it.Recommended fix
Replace
android.media.audiofx.Equalizerwithandroid.media.audiofx.DynamicsProcessing.DynamicsProcessingis available since API 28 andminSdkis already 28. It applies no automatic headroom attenuation, it exposes a real input gain stage, and it provides a limiter stage. A +6 dB band gain becomes an actual +6 dB band gain.Scope:
PlaybackEnhancerService.attachEqualizer/applyEqualizer: build aDynamicsProcessing.Configwith a multi-band post-EQ stage and the limiter stage enabled, then set per-band gains.EqualizerBandProvider:DynamicsProcessingdoes not report a device band layout, so define the band layout in the app. Keeping the existing 60 / 230 / 910 / 3600 / 14000 Hz layout and a -15..+15 dB range preserves the current UI. The probe becomes a simple "can the effect be constructed" check.EqualizerSettings,PlaybackPreferences,SettingsViewModelor the UI.EqualizerCapabilities.Unavailable. The existing code path already hides the equalizer row in that case.Risks
DynamicsProcessingreplaces the platform equalizer, nothing protects against clipping on large boosts, so distortion would replace the current quieting.DynamicsProcessingsupport varies between vendors. Needs testing on real hardware, including at least one non-stock ROM, before release.Out of scope
DynamicsProcessingis used only as an equalizer with a gain stage and a limiter.LoudnessEnhancer).