[pulseaudio-discuss] [PATCH 00/13] loopback: Make module-loopback honor requested latency (v5)

Alexander E. Patrakov patrakov at gmail.com
Sun Nov 15 13:08:37 PST 2015

25.02.2015 23:43, Georg Chini wrote:
> Hello,
> this is the split version of the module-loopback patch. The patch optimizes the latency
> initialization and regulation of the module.
> Many thanks to Alexander Patrakov for splitting and reviewing the patch and also for
> his contribution to the code.
> Georg Chini (13):
>    loopback: Fix the obviously-wrong "buffer+=buffer" logic
>    loopback: Save the timestamps when we snapshot latency
>    loopback: Improved estimation of latency
>    loopback: Adjust rates based on latency difference
>    loopback: Don't track max_request and min_memblockq_length
>    loopback: Restart the timer right away
>    loopback: Refactor latency initialization
>    loopback: Track underruns and cant-peek events
>    loopback: Track the amount of jitter
>    loopback: Added a deadband to reduce rate hunting
>    loopback: Don't change rate abruptly
>    loopback: Validate the rate parameter
>    loopback: add parameter buffer_latency_msec
>   src/modules/module-loopback.c | 578 ++++++++++++++++++++++++++++++++----------
>   src/pulse/sample.c            |   5 +-
>   2 files changed, 443 insertions(+), 140 deletions(-)

I have tested this code again. Here is my opinion.

It would be logical enough to consider the state of the original 
loopback module, and compare the state of affairs after 04/13 and after 
13/13 to it. And compare to other implementations of adaptive resamplers 
as well.

To test the thing, I used my laptop (that has an internal sound card) 
and my desktop (that has an internal PCH soundcard, also I connected my 
Rotel RA-1570 amplifier via USB).

On the laptop, I created a 5-minute wav file containing a sine wave with 
1 kHz frequency. I connected the laptop headphone output to the desktop 
PCH line input, and used the loopback module to transfer audio from PCH 
to the Rotel amplifier. In all cases, the nominal sample rate 44100 Hz 
was used. In all cases, the sound sent to the Rotel amplifier was 
recorded into a wav file, and the resulting file was analysed with a 
Python script.

I have tried two techniques for instrumenting the loopback module.

First, I thought that I can record the sound sent to the Rotel sound 
card by recording from PulseAudio monitor source. However, this turned 
out to be a bad idea: the existing loopback module behaved differently 
(according to its log) when its sink was monitored. Namely, it 
immediately converged to 44100 Hz - something that it could do only 
after two minutes without such monitoring.

So, I created this snippet in .asoundrc:

pcm.savefile {
     type file
     format "wav"
     slave.pcm "hw:PCUSB"
     file "/tmp/save.wav"

and loaded module-alsa-sink so that PulseAudio saved its output to a file.

The most important result is that the old module, both when monitored 
and when not monitored, does not respect the requested latency in this 
test case. It settles down to something between 300 and 400 ms, even 
though 200 ms was requested. There is no such bug with the new 

Now let's talk about the script. It is attached. A mono wav file 
containing some silence, recording of a resampled 1 kHz sine wave, and 
some silence again, should be passed as an argument. The script will try 
to estimate the phase of the sine wave (y = A * sin(omega * t + phi), 
the script tries to estimate phi) at various points in time, and plot 
the result. The plot will generally contain some linear trend (due to 
unsynchronized clocks) and some wobble around that. Then it will plot 
again, with this linear trend eliminated (i.e. will estimate the phase 
again using the correct signal frequency in terms of the sink clock). 
Both results are saved as png files, twice (first the full version, then 
only the second half, to hide the startup process).

I chose this metric because it was used by Fons Adriaensen when 
discussing the merits of zita-ajbridge:


Both my plots and the plots on his page show the unwrapped phase in 
degrees. Phase, in our case, is related to the delay. 360 degrees = 1 
millisecond = 34 cm of air. In other words, 1 degree = 2.77 us = 0.94 mm 
of air.

The first result (https://imgur.com/a/twBEk ) is actually without any 
loopback module. Just a recording from the line input. We can clearly 
see that there is some clock skew between the laptop and desktop 
soundcards, and that the clock ratio is even not constant. OK - this is 
a reference how good any loopback module can be.

The second result (https://imgur.com/a/eVahQ ) is with the old module. 
You see that, with it, the rate oscillates wildly and then snaps to 
44100 Hz. However, the final latency is not correct, and it accumulates 
due to clock mismatch between the two sound cards in the desktop. I have 
not waited enough to see what happens next, but the result is clearly 

The next result (https://imgur.com/a/Bi2Yz ) is with the new module, as 
of PATCH 04/13. There is some "rate hunting", and it results to some 
noise in phase. It is generally contained between -150 and 200 degrees.

Next, let's see (https://imgur.com/a/yQGWL ) what the deadband achieves. 
Result: it helps against rate hunting, but not much against phase 
oscillations. This is because new_rate is snapped to base_rate, which 
would only be correct if the cards actually shared the common clock. So 
here is what happens: the module runs for some time with 1:1 resampling 
(i.e. with the rate strictly equal to 44100 Hz), and the latency 
difference slowly drifts due to the clock mismatch. When the difference 
becomes bigger than the error of its measurement, the module corrects 
it, and then runs at 44100 Hz for some time again.

However, I'd argue that this phase metric can be improved without the 
deadband and beyond what this deadband-based implementation provides.

First, see (https://imgur.com/a/P5Y0A ) what happens, at PATCH 04/13, if 
we just decrease adjust_time to 1 second. The rate oscillations become 
bigger, but phase oscillations stay of the same order as with the 
default value of adjust_time (10 seconds). So not good.

Then, let's change the rate controller so that it does not attempt to 
correct all of the latency difference in one step. The value of 
adjust_time is still 1s, but let's aim to correct the difference after 
10 steps by a factor of 2.71. For simplicity (and for testing purposes 
only), let's destroy the non-linear part of the controller. Here is the 

static uint32_t rate_controller(
                 uint32_t base_rate,
                 pa_usec_t adjust_time,
                 int32_t latency_difference_usec) {

     uint32_t new_rate;

     new_rate = base_rate * (1.0 + 0.1 * (double)latency_difference_usec 
/ adjust_time);

     return new_rate;

As you can see (https://imgur.com/a/eZT8L ), the phase oscillations are 
now much more limited!

I have received a private email from Georg where he proposes to use the 
deadband, but snap to something other than base_rate, e.g. to a result 
of some slow controller. For me, this is equivalent to using this slow 
controller, i.e. a good idea.

Finally, let's compare Georg's code to known competitors.

alsaloop (part of alsa-utils): https://imgur.com/lvZLoli . Could not 
settle after 300 seconds, so I am not uploading any processed plots.

alsa_in (part of jack1): https://imgur.com/a/9PnW3 . Rather big 
oscillations, definitely worse than what's proposed here.

zita-ajbridge: could not run it, because it deadlocks at the start. 
Could be the same as 

Also, let's consider the limitations of what we can achieve with 
different controllers.

First, power-saving proponents will not let the latency be sampled more 
often than once in 100 ms by default. Thus, we can only adjust rates 
once in 100 ms. It is a PulseAudio limitation that rate is an integer. 
If the correct rate is 44100.5 Hz, then a controller can only set it to 
either 44100 or 44101 Hz. In any case, that's 1/88200 seconds per second 
of clock skew. Per our 100 ms period of sampling the latency difference, 
that's 1.13 us. With a 1 kHz test signal (as used in all examples 
above), that's about 0.4 degrees of phase. So, "rate is an integer" is 
not an important limitation - the theoretically achievable resolution is 
only slightly worse than the natural clock instability of consumer 

TL;DR: I'm in favor of merging up to PATCH 05/13, and, if someone else 
reviews patches 06 and 07, would recommend merging up to PATCH 08/13. I 
would recommend waiting two weeks before merging further patches, as the 
deadband stuff is likely to get undone and redone if the rate controller 
gets replaced. And I am going to experiment further with different rate 
controllers on the next weekends.

Alexander E. Patrakov
-------------- next part --------------
A non-text attachment was scrubbed...
Name: analyze-phase.py
Type: text/x-python
Size: 2496 bytes
Desc: not available
URL: <http://lists.freedesktop.org/archives/pulseaudio-discuss/attachments/20151116/589275b3/attachment.py>

More information about the pulseaudio-discuss mailing list