pulseaudio: delay app's output by time amount

The loopback module will allow you to add up to 2 seconds of delay between a source and a sink.


I would like to share my bash script (called ”pa-delay”) that uses the loopback and null-sink modules already mentioned by Ignacio Vazquez-Abrams to establish a chain of virtual sinks and loopbacks of arbitrary length. I am actively using this script. The sinks are called Delay1, Delay2, … Delayn and the script first unloads any sinks of that name and any loopbacks using these sinks that may have been created by a previous run. Then it builds the chain to the length required by the requested delay, which is specified in milliseconds as a command line parameter. (A delay of 0 will make the script remove the previous chain and then exit.) The first virtual sink, Delay1, is always the one to connect to in order to get the full delay. The intermediate sinks could theoretically be used, too, and they are all given descriptions stating the amount of delay that would result from connecting to them. It’s important that Delay1 always is the sink to use because (at least for me in XFCE) the system “remembers” which sink a program was connected to. A program that is connected to the Delay1 sink will revert to it even if the chain had been inexistent for a while (during which time the program will be connected to the default output device). So I can call pa-delay as often as required to adjust the delay length and without further intervention any program using the delay will use the new chain established by each run. This also applies after I have called pa-delay 0 to have undelayed audio for a longer time. I find that after suspending my computer the delay tends to be longer than specified (which is in line with the warning that the latency_msec parameter for the loopback module is “only a friendly request”), so I also change to the “pa-delay 0” state before suspending.

#! /bin/bash

delay_msec="$1"
if [[ ! $delay_msec =~ ^[0-9]+$ ]]; then
    echo "Usage: $( basename "$0" ) delay_milliseconds" >&2
    exit 2
fi
max_loopback_delay=2000

list_delay_loopback_modules() {
    pactl list modules short | grep -P '\tmodule-loopback\t(.*\s)?source=Delay[1-9][0-9]*[.]monitor(\s|$)' | cut -f1
}

list_delay_null_modules() {
    pactl list modules short | grep -P '\tmodule-null-sink\t(.*\s)?sink_name=Delay[1-9][0-9]*(\s|$)' | cut -f1
}

build_module_array() {
    local object_type="$1"
    local array="$( echo "$1" | sed 's/-/_/g' )"
    typeset -n array
    local object_list="$( 
        pactl list "$object_type" |
        perl -00 -p -e ' chomp; s{\s*\n\s*}{|}mg; s{$}{\n}; ' |
        sed -En -e 's/^[a-zA-Z ]* #([0-9]+)[|].*[|]Owner Module: ([0-9]+)[|].*/\1:\2/p'
    )"
    while IFS=: read object module; do
        array[$module]=$object
    done <<<"$object_list"
}

for module in $( list_delay_loopback_modules ); do
    pactl unload-module "$module"
done
for module in $( list_delay_null_modules ); do
    pactl unload-module "$module"
done

last_loopback_delay=$(( (delay_msec + max_loopback_delay - 1) % max_loopback_delay + 1 ))
loops=$(( (delay_msec - last_loopback_delay) / max_loopback_delay + 1 ))
(( loops > 0 )) || exit 0

nbsp="$( echo -e '\u00a0' )"
narrownbsp="$( echo -e '\u202f' )"
i=1
module="$( pactl load-module module-null-sink sink_name="Delay$i" sink_properties="device.description=\"Delay:${nbsp}${delay_msec}${narrownbsp}ms\"" )"
while (( ++i <= loops )); do
    module="$( pactl load-module module-null-sink sink_name="Delay$i" sink_properties="device.description=\"Delay:${nbsp}$(( delay_msec - (i - 1) * max_loopback_delay ))${narrownbsp}ms\"" )"
done
i=$loops
module="$( pactl load-module module-loopback source="Delay$i.monitor" latency_msec=$last_loopback_delay )"
last_loopback_module=$module
while (( --i > 0 )); do
    module="$( pactl load-module module-loopback source="Delay$i.monitor" sink=Delay$(( i + 1 )) latency_msec=$max_loopback_delay )"
done

build_module_array sinks
build_module_array sources
build_module_array sink-inputs
build_module_array source-outputs

for module in $( list_delay_null_modules ); do
    pactl set-source-volume ${sources[$module]} '100%'
    pactl set-sink-volume   ${sinks[$module]}   '100%'
done
for module in $( list_delay_loopback_modules ); do
    pactl set-source-output-volume ${source_outputs[$module]} '100%'
    test "$module" == "$last_loopback_module" && continue
    pactl set-sink-input-volume    ${sink_inputs[$module]}    '100%'
done