Automatic volume normalization with PulseAudio

Last modified: Wed Oct 28 09:01:51 EDT 2020

This page documents what I did to destroy dynamic range on a system burdened with the evil daemon PulseAudio.  The first two pieces are based on the advice in ArchWiki; the last step (fixing things in pavucontrol) came from vaguely related troubleshooting Q&A elsewhere.

1.  Install LADSPA filter

PulseAudio supports only LADSPA filters.  Non-support of libavfilter is a serious disappointment, but for this simple task, a sufficient LADSPA filter is gettable (almost).  I used the "Fast Lookahead limiter" from Steve Harris, but with the following modification to allow greater compression:

diff -ur ladspa-0.4.17/fast_lookahead_limiter_1913.xml ladspa-0.4.17-dwf/fast_lookahead_limiter_1913.xml
--- ladspa-0.4.17/fast_lookahead_limiter_1913.xml	2016-10-17 05:05:54.000000000 -0400
+++ ladspa-0.4.17-dwf/fast_lookahead_limiter_1913.xml	2020-10-22 17:04:00.886286144 -0400
@@ -193,7 +193,7 @@
       <name>Input gain (dB)</name>
       <p>Gain that is applied to the input stage. Can be used to trim gain to
 bring it roughly under the limit or to push the signal against the limit.</p>
-      <range min="-20" max="20"/>
+      <range min="-70" max="70"/>
     <port label="limit" dir="input" type="control" hint="default_0">

Build the plugin for the architecture of the target machine and install in /usr/local/lib/ladspa.  Nothing else from the plugins distribution is required on the target machine.

For figuring out how to invoke this plugin and others, the analyseplugin program of the LADSPA SDK was very useful:

bash-4.3$ ladspa/bin/analyseplugin fast_lookahead_limiter_1913

Plugin Name: "Fast Lookahead limiter"
Plugin Label: "fastLookaheadLimiter"
Plugin Unique ID: 1913
Maker: "Steve Harris <email redacted>"
Copyright: "GPL"
Must Run Real-Time: No
Has activate() Function: Yes
Has deactivate() Function: No
Has run_adding() Function: Yes
Environment: Normal or Hard Real-Time
Ports:  "Input gain (dB)" input, control, -20 to 20, default 0
        "Limit (dB)" input, control, -20 to 0, default 0
        "Release time (s)" input, control, 0.01 to 2, default 0.5075
        "Attenuation (dB)" output, control, 0 to 70
        "Input 1" input, audio
        "Input 2" input, audio
        "Output 1" output, audio
        "Output 2" output, audio
        "latency" output, control

2.  Configure PulseAudio to enable the LADSPA filter

As described by ArchWiki, use pacmd list-sinks to identify the existing output sink.  Mine was called alsa_output.pci-0000_00_0e.0.hdmi-stereo.

The following goes in ~/.config/pulse/

.include /etc/pulse/

set-default-sink alsa_output.pci-0000_00_0e.0.hdmi-stereo
load-module module-ladspa-sink sink_name=squash sink_master=alsa_output.pci-0000_00_0e.0.hdmi-stereo plugin=fast_lookahead_limiter_1913 label=fastLookaheadLimiter control=30,0,0.5
set-default-sink squash

Then restart the daemon or reboot.

The parameters to be provided after control= are those described as "input, control" in the ports listing of analyseplugin, in the order listed.  The parameters that I use smash the input range of −30 dB to 0 dB up to maximum volume while inputs below −30 dB drop off linearly:

The set-default-sink command sets the "fallback" sink, which supposedly somehow acts as some sort of default sometimes.  But in fact, it had no effect on anything as far as I could tell.

3.  Force apps to actually use the filter

There are at least three different ways that apps decide which output sink to use, none of which are determined by set-default-sink:  the other default, an app setting in pavucontrol, or an app-internal setting.

3.1.  The other default (e.g., GNOME Videos)

Install pavucontrol:  sudo apt install pavucontrol

On the Output Devices tab of pavucontrol, there appears to be a default output selection that is independent of set-default-sink.  Clicking the check mark next to the LADSPA plugin caused GNOME Videos to switch its output.

3.2.  App setting in pavucontrol (e.g., Firefox 82.0)

This setting is hidden like the Doors of Durin.  If you look in Firefox, you find nothing relevant.  If you look in pavucontrol, you also find nothing relevant.  But if you launch Firefox and start it playing some audio and then look in the Playback tab of pavucontrol while Firefox is running, you can switch Firefox's output to the LADSPA plugin.  Once done, it sticks for future Firefox processes.

Firefox will probably implement its own app-internal setting in the future, but this will serve as an example for other apps that do not.

3.3.  App-internal setting (e.g., VLC)

VLC has its own setting to explicitly choose the output.

Other notes

PulseAudio logging

In theory, you can get the log from a PulseAudio daemon with journalctl -b -t pulseaudio -p 7.  In reality, the logging level is such that nothing useful is normally preserved.

Prevent automatic respawning of PulseAudio by putting autospawn = no in ~/.config/pulse/client.conf.  Do pulseaudio -k to kill the PulseAudio daemon if it is running, then run it in a terminal with pulseaudio -vvvvv.

These low-priority log messages might have something to do with why set-default-sink had no effect:

I: [pulseaudio] core.c: configured_default_sink: (unset) -> squash
I: [pulseaudio] core.c: configured_default_sink: squash -> alsa_output.pci-0000_00_0e.0.hdmi-stereo
I: [pulseaudio] core.c: configured_default_sink: alsa_output.pci-0000_00_0e.0.hdmi-stereo -> squash
I: [pulseaudio] core.c: default_sink: alsa_output.pci-0000_00_0e.0.hdmi-stereo -> squash
D: [pulseaudio] sink-input.c: Can't connect input to squash, as that would create a cycle.
I: [pulseaudio] module-switch-on-connect.c: Failed to move sink input 0 "(null)" to squash.

I've no idea why it would think it had a cycle.

If you exceed the range of an input control for the LADSPA filter, PulseAudio logs the following and dies:

W: [pulseaudio] module-ladspa-sink.c: Control value 0 over upper bound: 30.000000 (upper bound: 20.000000)
E: [pulseaudio] module-ladspa-sink.c: Failed to parse, validate or set control parameters
E: [pulseaudio] module.c: Failed to load module "module-ladspa-sink" (argument: "sink_name=squash sink_master=alsa_output.pci-0000_00_0e.0.hdmi-stereo plugin=fast_lookahead_limiter_1913 label=fastLookaheadLimiter control=30,0,0.5"): initialization failed.
E: [pulseaudio] main.c: Module load failed.
E: [pulseaudio] main.c: Failed to initialize daemon.

LADSPA plugin testing

The applyplugin program of the LADSPA SDK is useful for testing plugins on wav files, but it's picky about the format and chokes on the metadata that ffmpeg automatically adds.  To remove the metadata from a wav file:

ffmpeg -i orig.wav -fflags +bitexact -flags:a +bitexact -codec copy clean.wav

Then this works:

ladspa/bin/applyplugin clean.wav out.wav \
  fast_lookahead_limiter_1913 fastLookaheadLimiter \
  30 0 0.5
Peak output: 32767.5

The limits on control input parameters are not enforced when the plugin is used this way.