On 15.11.2015 22:08, Alexander E. Patrakov wrote:
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.

Thank you a lot!



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 implementation.

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:

http://kokkinizita.linuxaudio.org/linuxaudio/zita-ajbridge-doc/quickguide.html

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 invalid.

I wonder why the phase is not continuously growing in the final plot. I would expect this
because the controller snaps to the base rate.


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.


Could you use 2 s as adjust time? 10 seconds is definitely too slow to achieve good results. I would expect a similar result but with much smaller amplitude of the phase oscillation.


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


What is the problem with the dead band? There are physical limitations to what makes
sense to correct and the dead band just takes care of those limits.


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 code:

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.


I'll send you a patch privately later today. I kept the controller and implemented a more or less independent controller which takes care of the clock skew / latency drift. I'd like to discuss
this approach first with you before I send another patch to the list.


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 https://bugs.launchpad.net/ubuntu/+source/zita-ajbridge/+bug/1368716


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 soundcards.


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.


_______________________________________________
pulseaudio-discuss mailing list
[email protected]
http://lists.freedesktop.org/mailman/listinfo/pulseaudio-discuss

Reply via email to