[pulseaudio-discuss] [PATCH 07/13] loopback: Refactor latency initialization

Georg Chini georg at chini.tk
Thu Nov 26 11:36:24 PST 2015

On 26.11.2015 18:47, Tanu Kaskinen wrote:
> On Thu, 2015-11-26 at 08:41 +0100, Georg Chini wrote:
>> On 26.11.2015 01:49, Tanu Kaskinen wrote:
>>> On Wed, 2015-11-25 at 22:58 +0100, Georg Chini wrote:
>>>> On 25.11.2015 19:49, Tanu Kaskinen wrote:
>>>>> On Wed, 2015-11-25 at 16:05 +0100, Georg Chini wrote:
>>>>>> On 25.11.2015 09:00, Georg Chini wrote:
>>>>>>> OK, understood. Strange that you are talking of 75% and 25%
>>>>>>> average buffer fills. Doesn't that give a hint towards the connection
>>>>>>> between sink latency and buffer_latency?
>>>>>>> I believe I found something in the sink or alsa code back in February
>>>>>>> which at least supported my choice of the 0.75, but I have to admit
>>>>>>> that I can't find it anymore.
>>>>>> Lets take the case I mentioned in my last mail. I have requested
>>>>>> 20 ms for the sink/source latency and 5 ms for the memblockq.
>>>>> What does it mean that you request 20 ms "sink/source latency"? There
>>>>> is the sink latency and the source latency. Does 20 ms "sink/source
>>>>> latency" mean that you want to give 10 ms to the sink and 10 ms to the
>>>>> source? Or 20 ms to both?
>>>> I try to configure source and sink to the same latency, so when I
>>>> say source/sink latency = 20 ms I mean that I configure both to
>>>> 20 ms.
>>>> In the end it may be possible that they are configured to different
>>>> latencies (for example HDA -> USB).
>>>> The minimum necessary buffer_latency is determined by the larger
>>>> of the two.
>>>> For simplicity in this thread I always assume they are both equal.
>>>>>> The
>>>>>> 20 ms cannot be satisfied, I get 25 ms as sink/source latency when
>>>>>> I try to configure it (USB device).
>>>>> I don't understand how you get 25 ms. default_fragment_size was 5 ms
>>>>> and default_fragments was 4, multiply those and you get 20 ms.
>>>> You are right. The configured latency is 20 ms but in fact I am seeing
>>>> up to 25 ms.
>>> 25 ms reported as the sink latency? If the buffer size is 20 ms, then
>>> that would mean that there's 5 ms buffered later in the audio path.
>>> That sounds a bit high to me, but not impossible. My understanding is
>>> that USB transfers audio in 1 ms packets, so there has to be at least 1
>>> ms extra buffer after the basic alsa ringbuffer, maybe the extra buffer
>>> contains several packets.
>> I did not check the exact value, maybe it is not 25 but 24 ms, anyway
>> significantly larger than the configured value.
>>>>>> For the loopback code it means that the target latency is not what
>>>>>> I specified on the command line but the average sum of source and
>>>>>> sink latency + buffer_latency.
>>>>> The target latency should be "configured source latency +
>>>>> buffer_latency + configured sink latency". The average latency of the
>>>>> sink and source don't matter, because you need to be prepared for the
>>>>> worst case scenario, in which the source buffer is full and the sink
>>>>> wants to refill its buffer before the source pushes its buffered audio
>>>>> to the memblockq.
>>>> Using your suggestion would again drastically reduce the possible
>>>> lower limit. Obviously it is not necessary to go to the full range.
>>> How is that obviously not necessary? For an interrupt-driven alsa
>>> source I see how that is not necessary, hence the suggestion for
>>> optimization, but other than that, I don't see the obvious reason.
>> Obviously in the sense that it is working not only for interrupt-driven
>> alsa sources but also for bluetooth devices and timer-based alsa devices.
>> I really spent a lot of time with stability tests, so I know it is working
>> reliable for the devices I could test.
>>>> That special case is also difficult to explain. There are two situations,
>>>> where I use the average sum of source and sink latency.
>>>> 1) The latency specified cannot be satisfied
>>>> 2) sink/source latency and buffer_latency are both specified
>>>> In case 1) the sink/source latency will be set as low as possible
>>>> and buffer_latency will be derived from the sink/source latency
>>>> using my safeguards.
>>>> in case 2) sink/source latency will be set to the nearest possible
>>>> value (but may be higher than specified), and buffer_latency is
>>>> set to the commandline value.
>>>> Now in both cases you have sink/source latency + buffer_latency
>>>> as the target value for the controller - at least if you want to handle
>>>> it similar to the normal operation.
>>>> The problem now is that the configured sink/source latency is
>>>> possibly different from what you get on average. So I replaced
>>>> sink/source latency with the average sum of the measured
>>>> latencies.
>>> Of course the average measured latency of a sink or source is lower
>>> than the configured latency. The configured latency represents the
>>> situation where the sink or source buffer is full, and the buffers
>>> won't be full most of the time. That doesn't mean that the total
>>> latency doesn't need to be big enough to contain both of the configured
>>> latencies, because you need to handle the case where both buffers
>>> happen to be full at the same time.
>> I am not using sink or source latency alone, I am using the
>> average sum of source and sink latency, which is normally
>> slightly higher than a single configured latency.
>> How can it be possible that both buffers are full at the same
>> time? This could only happen if there is some congestion and
>> then there is a problem with the audio anyway. In a steady
>> state, when one buffer is mostly empty, the other one must be
>> mostly full. Otherwise the latency would jump around wildly.
> There are three buffers, not two. The sum of the buffer fill level of
> the sink and source will jump around wildly, but the total latency will
> stay constant, because the memblockq will always contain the empty
> space of the sink and source buffers.
> Note that the measured latency of the sink and source doesn't jump
> around that wildly, because the measurement often causes a reset in the
> buffer fill levels. But every time the sink buffer is refilled or the
> source pushes out data, the latency of the sink or source jumps. The
> sink refills and source emptying don't (generally) happen in a
> synchronized manner, so the latency sum of the sink and source does
> jump around.
> If the sink and source were synchronized, the combined latency wouldn't
> jump around, and you could reduce the total latency. But that's not the
> case.
>>>> The average is also used to compare the "real"
>>>> source/sink latency + buffer_latency
>>>> against the configured overall latency and the larger of the two
>>>> values is the controller target. This is the mechanism used
>>>> to increase the overall latency in case of underruns.
>>> I don't understand this paragraph. I thought the reason why the
>>> measured total latency is compared against the configured total latency
>>> is that you then know whether you should increase or decrease the sink
>>> input rate. I don't see how averaging the measurements helps here.
>> Normally, the configured overall latency is used as a target for
>> the controller. Now there must be some way to detect during
>> runtime if this target is something that can be achieved at all.
> You know whether the target is achievable when you know the maximum
> latency of the sink and source. If the sum of those is larger than the
> target, the target is not achievable. Measuring the average latency
> doesn't bring any new information.
> By the way, the fact that the real sink latency can be higher than the
> configured latency is problematic, when thinking whether the target
> latency can be achieved. The extra latency needs to be compensated by
> decreasing buffer_latency, and if such extra margin doesn't exist, then
> the target latency is not achievable.
> It's not currently possible to separate the alsa ringbuffer latency
> from the total sink latency, so you don't know how much there is such
> extra latency. You could look at the sink latency reports, and if they
> go beyond the configured latency, then you know that there's *at least*
> that much extra latency, but it would be nice if the alsa sink could
> report the ringbuffer and total latencies separately. The alsa API
> supports this, but PulseAudio's own APIs don't. The source has the same
> problem, but it also has the additional problem that the latency
> measurements cause the buffer to be emptied first, so the measurements
> never show larger latencies than configured. I propose that for now we
> ignore such extra latencies. We could add some safety margin to
> buffer_latency to cover these latencies, but it's not nice to force
> that for cases where such extra latencies don't exist.
>> So I compare the target value against buffer_latency +
>> average_sum_of_source_and_sink_latency and set the controller
>> target to the larger of the two.
>> This is the way the underrun protection works. In normal operation,
>> the configured overall latency is larger than the sum above and
>> buffer_latency is not used at all. When underruns occur, buffer_latency
>> is increased until the sum gets larger than the configured latency
>> and the controller switches the target.
>>> And what does this have to do with increasing the latency on underruns?
>>> If you get an underrun, then you know buffer_latency is too low, so you
>>> bump it up by 5 ms (if I recall your earlier email correctly), causing
>>> the configured total latency to go up by 5 ms as well. As far as I can
>>> see, the measured latency is not needed for anything in this operation.
>>> ----
>>> Using your example (usb sound card with 4 * 5 ms sink and source
>>> buffers), my algorithm combined with the alsa source optimization
>>> yields the following results:
>>> configured sink latency = 20 ms
>>> configured source latency = 20 ms
>>> maximum source buffer fill level = 5 ms
>>> buffer_latency = 0 ms
>>> target latency = 25 ms
>>> So you see that the results aren't necessarily overly conservative.
>> That's different from what you proposed above, but sounds
>> like a reasonable approach. The calculation would be slightly
>> different because I defined buffer_latency = 5 ms on the
>> command line. So the result would be 30 ms, which is more
>> sensible. First we already know that the 25 ms won't work.
>> Second, the goal of the calculation was to find a working
>> target latency using the configured buffer_latency, so you
>> can't ignore it.
>> My calculation leads to around 27.5 ms instead of your 30 ms,
>> so the two values are near enough to each other and your
>> proposal has the advantage of being constant.
>> I will replace the average sum by
>> 0.25 * configured_source_latency + configured_sink_latency.
>> in the next version if my tests with that value are successful.
> Do you mean that you're going to use 0.25 as the multiplier regardless
> of the number of fragments?
> Previously I've been saying that in the general case the target latency
> should be "configured source latency + buffer_latency + configured sink
> latency". To generalize the alsa source exception, I'll use the
> following definition instead from now on: "target latency = maximum
> source buffer fill level + buffer_latency + maximum sink buffer fill
> level". Usually the maximum fill levels have to be assumed to be the
> same as the configured latencies, but in the interrupt-driven alsa
> source case the maximum fill level is known to be "configured source
> latency / fragments".

OK, now I finally got you. That has taken quite a bit, sorry.

Let me summarize:

- The minimum achievable latency is maximum source fill + maximum sink fill
- The sink maximum fill level is always 100%, regardless of the device type
- The source maximum fill level depends on device type
    # For interrupt driven alsa sources it is one default-fragment-size
    # For timer-based alsa devices it is 50% of the configured latency
    # For the general case it is unknown and may be 100%

Do we have an agreement so far?

As we are talking about fail-safe measures I would think that it is OK
to assume that 50% is a reasonable value even for the general case.
As already said there is a mechanism in the controller that will handle
the cases when that assumption is not true and I would not want
to throw away the potential reduction of the latency just to be on
the 100% safe side.

If we are on the same page now, I would implement it as stated

More information about the pulseaudio-discuss mailing list