diff mbox

Improving status timestamp accuracy

Message ID 577FC0C4.1060508@IEE.org (mailing list archive)
State New, archived
Headers show

Commit Message

Alan Young July 8, 2016, 3:03 p.m. UTC
On 07/06/16 07:44, Alan Young wrote:
> I'll work on developing and testing a patch for consideration before 
> coming back to the last. It will be easier to discuss the merits or 
> otherwise of my proposal with a concrete, working patch to consider.


Well, it has been a few weeks but this is what I have come up with.

It is not perfect because of the issue noted in the comments but so far 
I have not been able to discover any downside. It many (most) cases, the 
reported delay (and audio_tstamp) is more accurate than it was before. 
In other cases there is no change.

Alan.

Comments

Pierre-Louis Bossart July 15, 2016, 8:13 p.m. UTC | #1
On 7/8/16 10:03 AM, Alan Young wrote:
> On 07/06/16 07:44, Alan Young wrote:
>> I'll work on developing and testing a patch for consideration before
>> coming back to the last. It will be easier to discuss the merits or
>> otherwise of my proposal with a concrete, working patch to consider.
>
>
> Well, it has been a few weeks but this is what I have come up with.
>
> It is not perfect because of the issue noted in the comments but so far
> I have not been able to discover any downside. It many (most) cases, the
> reported delay (and audio_tstamp) is more accurate than it was before.
> In other cases there is no change.

I just looked at the code and I am probably missing something.

in update_delay() you apply a delta between the last timestamp and the 
current one and modify the runtime->delay.

Immediately after, in update_audio_tstamp() runtime->delay is used as 
well to compute audio_frames which in turn is used to find the 
audio_tstamp, on which another delta between current tstamp and last 
timestamp is applied.

Overall it looks like you are correcting twice for the same delay?

Even if this was correct, you would want to make sure the delta is 
positive to keep audio timestamps monotonous.
Alan Young July 19, 2016, 3:33 p.m. UTC | #2
On 15/07/16 21:13, Pierre-Louis Bossart wrote:
> in update_delay() you apply a delta between the last timestamp and the 
> current one and modify the runtime->delay.
>
> Immediately after, in update_audio_tstamp() runtime->delay is used as 
> well to compute audio_frames which in turn is used to find the 
> audio_tstamp, on which another delta between current tstamp and last 
> timestamp is applied.
>
> Overall it looks like you are correcting twice for the same delay?
>
In update_audio_tstamp() it either usedruntime->delay, if 
runtime->audio_tstamp_config.report_delay is true, or applies a delta - 
not both.

> Even if this was correct, you would want to make sure the delta is 
> positive to keep audio timestamps monotonous. 

Hmm, maybe. In what circumstances could the delay ever be negative?
Pierre-Louis Bossart July 19, 2016, 3:58 p.m. UTC | #3
On 7/19/16 10:33 AM, Alan Young wrote:
> On 15/07/16 21:13, Pierre-Louis Bossart wrote:
>> in update_delay() you apply a delta between the last timestamp and the
>> current one and modify the runtime->delay.
>>
>> Immediately after, in update_audio_tstamp() runtime->delay is used as
>> well to compute audio_frames which in turn is used to find the
>> audio_tstamp, on which another delta between current tstamp and last
>> timestamp is applied.
>>
>> Overall it looks like you are correcting twice for the same delay?
>>
> In update_audio_tstamp() it either usedruntime->delay, if
> runtime->audio_tstamp_config.report_delay is true, or applies a delta -
> not both.

ah yes, I did miss it in the code. maybe a comment would be nice to 
avoid being thrown.

I still have mixed feelings about the code, it seems to make sense for 
the case where the .pointer is updated at every period, but it's not 
using the regular BATCH definition. With the tests such as
runtime->status->hw_ptr == runtime->hw_ptr_interrupt you could end-up 
modifying the results by a small amount for other hardware platforms 
depending on when the timestamp is taken (in other words scheduling 
latency would affect audio timestamps).

>
>> Even if this was correct, you would want to make sure the delta is
>> positive to keep audio timestamps monotonous.
>
> Hmm, maybe. In what circumstances could the delay ever be negative?

if your timestamps are REALTIME since they can jump backwards. The 
expectation is to use MONOTONOUS (or better MONOTONOUS_RAW to avoid  NTP 
corrections), but with the ALSA API the application can choose REALTIME.
Alan Young July 20, 2016, 6:59 a.m. UTC | #4
On 19/07/16 16:58, Pierre-Louis Bossart wrote:
>> In update_audio_tstamp() it either usedruntime->delay, if
>> runtime->audio_tstamp_config.report_delay is true, or applies a delta -
>> not both.
>
> ah yes, I did miss it in the code. maybe a comment would be nice to 
> avoid being thrown.
ok

> I still have mixed feelings about the code, it seems to make sense for 
> the case where the .pointer is updated at every period, but it's not 
> using the regular BATCH definition. With the tests such as
> runtime->status->hw_ptr == runtime->hw_ptr_interrupt you could end-up 
> modifying the results by a small amount for other hardware platforms 
> depending on when the timestamp is taken (in other words scheduling 
> latency would affect audio timestamps).
>

Yes, that could be true - there could be some jitter -  but I think it 
will still result in more accurate results. Note that the adjustment to 
the reported audio_tstamp will only occur for the 
AUDIO_TSTAMP_TYPE_DEFAULT case and when the platform has not updated the 
(hw_ptr) position outside of the interrupt callback independent of 
whether the BATCH flag is used.

There is actually an argument for being less restrictive. Hardware 
platform updates to position, where they happen outside of an interrupt, 
may (generally will) be less accurate than the update mechanism that I 
propose because such position updates are mostly restricted to the level 
of DMA residue granularity, which is relatively coarse (usually).

>
> if your timestamps are REALTIME since they can jump backwards. The 
> expectation is to use MONOTONOUS (or better MONOTONOUS_RAW to avoid  
> NTP corrections), but with the ALSA API the application can choose 
> REALTIME. 

Ok, I'll put in  a check. Of course there are cases where one might 
actually want REALTIME.

Note: For my application, I only actually care about the changes 
implemented using update_delay(). The refinement to 
update_audio_tstamp() just seemed to me to be part of the same issue. If 
the update_audio_tstamp() change is considered too controversial then 
I'd be happy to drop it.
Pierre-Louis Bossart Aug. 1, 2016, 9:56 p.m. UTC | #5
On 7/20/16 1:59 AM, Alan Young wrote:
> On 19/07/16 16:58, Pierre-Louis Bossart wrote:
>>> In update_audio_tstamp() it either usedruntime->delay, if
>>> runtime->audio_tstamp_config.report_delay is true, or applies a delta -
>>> not both.
>>
>> ah yes, I did miss it in the code. maybe a comment would be nice to
>> avoid being thrown.
> ok
>
>> I still have mixed feelings about the code, it seems to make sense for
>> the case where the .pointer is updated at every period, but it's not
>> using the regular BATCH definition. With the tests such as
>> runtime->status->hw_ptr == runtime->hw_ptr_interrupt you could end-up
>> modifying the results by a small amount for other hardware platforms
>> depending on when the timestamp is taken (in other words scheduling
>> latency would affect audio timestamps).
>>
>
> Yes, that could be true - there could be some jitter -  but I think it
> will still result in more accurate results. Note that the adjustment to
> the reported audio_tstamp will only occur for the
> AUDIO_TSTAMP_TYPE_DEFAULT case and when the platform has not updated the
> (hw_ptr) position outside of the interrupt callback independent of
> whether the BATCH flag is used.
>
> There is actually an argument for being less restrictive. Hardware
> platform updates to position, where they happen outside of an interrupt,
> may (generally will) be less accurate than the update mechanism that I
> propose because such position updates are mostly restricted to the level
> of DMA residue granularity, which is relatively coarse (usually).

I am not hot on changing a default behavior and end-up with platforms 
getting worse results and some getting better.
It'd really be better if you used a new timestamp (I added the 
LINK_ESTIMATED_ATIME that isn't used by anyone and could be reclaimed) 
and modified the delay estimation in your own driver rather than in the 
core.

>
>>
>> if your timestamps are REALTIME since they can jump backwards. The
>> expectation is to use MONOTONOUS (or better MONOTONOUS_RAW to avoid
>> NTP corrections), but with the ALSA API the application can choose
>> REALTIME.
>
> Ok, I'll put in  a check. Of course there are cases where one might
> actually want REALTIME.
>
> Note: For my application, I only actually care about the changes
> implemented using update_delay(). The refinement to
> update_audio_tstamp() just seemed to me to be part of the same issue. If
> the update_audio_tstamp() change is considered too controversial then
> I'd be happy to drop it.

if you change the delay by default then it changes the audio timestamp 
as well, not sure how you can isolate the two parts.
Alan Young Aug. 2, 2016, 7:30 a.m. UTC | #6
Hi Pierre,

Thanks for your continued engagement on this thread.

On 01/08/16 22:56, Pierre-Louis Bossart wrote:
> On 7/20/16 1:59 AM, Alan Young wrote:
>>
>> Yes, that could be true - there could be some jitter -  but I think it
>> will still result in more accurate results. Note that the adjustment to
>> the reported audio_tstamp will only occur for the
>> AUDIO_TSTAMP_TYPE_DEFAULT case and when the platform has not updated the
>> (hw_ptr) position outside of the interrupt callback independent of
>> whether the BATCH flag is used.
>>
>> There is actually an argument for being less restrictive. Hardware
>> platform updates to position, where they happen outside of an interrupt,
>> may (generally will) be less accurate than the update mechanism that I
>> propose because such position updates are mostly restricted to the level
>> of DMA residue granularity, which is relatively coarse (usually).
>
> I am not hot on changing a default behavior and end-up with platforms 
> getting worse results and some getting better. 

I am not sure that any platforms would get worse results 
(notwithstanding the jitter point above). Some would get better results.
>
> It'd really be better if you used a new timestamp (I added the 
> LINK_ESTIMATED_ATIME that isn't used by anyone and could be reclaimed) 
> and modified the delay estimation in your own driver rather than in 
> the core.
>

Well, I'm not looking at a single driver here. I am looking at several 
that use large parts of the common soc framework in various ways.

I'll look at LINK_ESTIMATED_ATIME and see if I could adopt that. I'm not 
sure how much it will help with the delay calculation but I suspect that 
the right answer could be deduced.

>> Note: For my application, I only actually care about the changes
>> implemented using update_delay(). The refinement to
>> update_audio_tstamp() just seemed to me to be part of the same issue. If
>> the update_audio_tstamp() change is considered too controversial then
>> I'd be happy to drop it.
>
> if you change the delay by default then it changes the audio timestamp 
> as well, not sure how you can isolate the two parts.
>

It only changes the audio timestamp if the user requests that the delay 
be included in it.


Stepping back for a moment, the delay calculation essentially consists 
of two parts:

 1. How much data is still in the ring buffer.
 2. How much data has been removed from the ring buffer but not yet
    played out.

In many respects it is artificial to separate these but that is what the 
API does.

In some cases the first factor is dominant, because DMA is consuming the 
buffer and one has - at the very best - only coarse-grained data about 
the position at any moment. It is unlikely ever to be sample-position 
accurate and, for most platforms, is much poorer.

In some cases the second factor is dominant because some data has been 
taken from the ring buffer and is then in some other significant size 
buffer. USB is a good example and, in that case, one can see that the 
generic driver does indeed used an elapsed-time calculation to generate 
(estimate) the delay report.

The more that I think about it the more it seems to me that using a 
time-based estimate for position (hw_ptr), outside of an interrupt 
callback, will always be more accurate than that returned by 
substream->ops->pointer(). Perhaps the results of that call should 
simply be ignored outside an interrupt callback - the call not even 
made, so as not to pollute the estimate with changed delay data.

Alan.
Clemens Ladisch Aug. 2, 2016, 7:55 a.m. UTC | #7
Alan Young wrote:
> Stepping back for a moment, the delay calculation essentially
> consists of two parts:
>
> 1. How much data is still in the ring buffer.
> 2. How much data has been removed from the ring buffer but not yet
>    played out.
> [...]
> The more that I think about it the more it seems to me that using
> a time-based estimate for position (hw_ptr), outside of an interrupt
> callback, will always be more accurate than that returned by
> substream->ops->pointer().

Please note that "hw_ptr" (or "avail" in the API) specifies the DMA
position, i.e., it is guaranteed that the samples up to this position
have been handled by the DMA controller and can now be accessed by the
program.  An estimate that gets this wrong can lead to data corruption.

Using an estimate for the delay is perfectly fine.


Regards,
Clemens
Pierre-Louis Bossart Aug. 2, 2016, 4:25 p.m. UTC | #8
>> It'd really be better if you used a new timestamp (I added the
>> LINK_ESTIMATED_ATIME that isn't used by anyone and could be reclaimed)
>> and modified the delay estimation in your own driver rather than in
>> the core.
>>
>
> Well, I'm not looking at a single driver here. I am looking at several
> that use large parts of the common soc framework in various ways.
>
> I'll look at LINK_ESTIMATED_ATIME and see if I could adopt that. I'm not
> sure how much it will help with the delay calculation but I suspect that
> the right answer could be deduced.

The LINK timestamps are supposed to be read from hardware counters close 
to the interface for better accuracy. When I added the LINK_ESTIMATED 
part, I thought this could be useful when those counters are not 
supported, but realized that if the delay is accurate you can just as 
well use the default method of adding the delay information to the DMA 
position to get the timestamps - there is really no benefit in defining 
a new timestamp type.
In the case where the delay is not accurate (on the platforms you are 
experimenting with) then this might be the right solution. And we could 
add this timestamp in the core rather than in each driver for simplicity.
It would be interesting if you share results with different timestamps 
to see if there is any benefit (with e.g. alsa-lib/test/audio_time.c)

> The more that I think about it the more it seems to me that using a
> time-based estimate for position (hw_ptr), outside of an interrupt
> callback, will always be more accurate than that returned by
> substream->ops->pointer(). Perhaps the results of that call should
> simply be ignored outside an interrupt callback - the call not even
> made, so as not to pollute the estimate with changed delay data.

then you'd have to account for drift between audio and timer source, 
it's not simpler at all and as Clemens wrote can lead to corrupted 
pointers if you screw up.
diff mbox

Patch

From 0c1378b8faa91e7bebf63a60ece3c5c942652a53 Mon Sep 17 00:00:00 2001
From: Alan Young <consult.awy@gmail.com>
Date: Fri, 8 Jul 2016 14:08:10 +0100
Subject: [PATCH] Improve accuracy of delay and audio_tstamp reporting.

pcm_lib.c:snd_pcm_update_hw_ptr0() is responsible for updating the
data used for various status reporting calls.  Many drivers do not
update the position upon a call to substream->ops->pointer() other
then within an interrupt callback. Most drivers also do not update
substream->runtime->delay (in the pointer() call) -- the main exception
being USB drivers. Consequently reported delay and audio_tstamp values
will, in these cases, be inaccurate by the time elapsed since the last
interrupt callback.

By recording the delay at the time of an interrupt callback, and the
timestamp at that time, the reported delay and audio_tstamp values
can be adjusted to compensate for the time elapsed since the last
interrupt. These adjustments are only made if the reported position or
delay (as appropriate) have not changed since those recorded at the time
of that interrupt.

This approach fails if reported delay is adjusted to account from some
data (such as the CODEC delay) but the position is not adjusted to the
(probably more significant) current DMA offset.
---
 include/sound/pcm.h  |  2 ++
 sound/core/pcm_lib.c | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 50 insertions(+)

diff --git a/include/sound/pcm.h b/include/sound/pcm.h
index b0be092..55da224 100644
--- a/include/sound/pcm.h
+++ b/include/sound/pcm.h
@@ -417,6 +417,8 @@  struct snd_pcm_runtime {
 	struct snd_pcm_audio_tstamp_config audio_tstamp_config;
 	struct snd_pcm_audio_tstamp_report audio_tstamp_report;
 	struct timespec driver_tstamp;
+	snd_pcm_sframes_t interrupt_delay;
+	struct timespec interrupt_tstamp;
 
 #if defined(CONFIG_SND_PCM_OSS) || defined(CONFIG_SND_PCM_OSS_MODULE)
 	/* -- OSS things -- */
diff --git a/sound/core/pcm_lib.c b/sound/core/pcm_lib.c
index 6b5a811..96cde4e 100644
--- a/sound/core/pcm_lib.c
+++ b/sound/core/pcm_lib.c
@@ -263,6 +263,12 @@  static void update_audio_tstamp(struct snd_pcm_substream *substream,
 		audio_nsecs = div_u64(audio_frames * 1000000000LL,
 				runtime->rate);
 		*audio_tstamp = ns_to_timespec(audio_nsecs);
+
+		if (!runtime->audio_tstamp_config.report_delay && runtime->interrupt_tstamp.tv_sec
+				&& runtime->status->hw_ptr == runtime->hw_ptr_interrupt) {
+			struct timespec delta_time = timespec_sub(*curr_tstamp, runtime->interrupt_tstamp);
+			*audio_tstamp = timespec_add(*audio_tstamp, delta_time);
+		}
 	}
 	runtime->status->audio_tstamp = *audio_tstamp;
 	runtime->status->tstamp = *curr_tstamp;
@@ -275,6 +281,40 @@  static void update_audio_tstamp(struct snd_pcm_substream *substream,
 	runtime->driver_tstamp = driver_tstamp;
 }
 
+static void update_delay(struct snd_pcm_substream *substream,
+			struct timespec *curr_tstamp)
+{
+	struct snd_pcm_runtime *runtime = substream->runtime;
+	struct timespec delta_time;
+	snd_pcm_sframes_t delta;
+
+	/* Can only adjust the delay if we have base timestamp. */
+	if (runtime->tstamp_mode != SNDRV_PCM_TSTAMP_ENABLE || !runtime->interrupt_tstamp.tv_sec)
+		return;
+
+	if (runtime->delay != runtime->interrupt_delay) {
+		/*
+		 *  Assume accurate if changed,
+		 *  which is not correct if driver supports variable
+		 *  codec delay reporting or similar
+		 */
+		return;
+	}
+
+	delta_time = timespec_sub(*curr_tstamp, runtime->interrupt_tstamp);
+	delta = (delta_time.tv_sec * USEC_PER_SEC + delta_time.tv_nsec / NSEC_PER_USEC)
+			* runtime->rate / USEC_PER_SEC;
+
+	/* sanity check */
+	if (delta > runtime->period_size)
+		return;
+
+	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
+		runtime->delay = runtime->interrupt_delay - delta;
+	else
+		runtime->delay = runtime->interrupt_delay + delta;
+}
+
 static int snd_pcm_update_hw_ptr0(struct snd_pcm_substream *substream,
 				  unsigned int in_interrupt)
 {
@@ -311,6 +351,11 @@  static int snd_pcm_update_hw_ptr0(struct snd_pcm_substream *substream,
 				snd_pcm_gettime(runtime, (struct timespec *)&curr_tstamp);
 		} else
 			snd_pcm_gettime(runtime, (struct timespec *)&curr_tstamp);
+
+		if (in_interrupt) {
+			runtime->interrupt_delay = runtime->delay;
+			runtime->interrupt_tstamp = curr_tstamp;
+		}
 	}
 
 	if (pos == SNDRV_PCM_POS_XRUN) {
@@ -453,6 +498,9 @@  static int snd_pcm_update_hw_ptr0(struct snd_pcm_substream *substream,
 	}
 
  no_delta_check:
+	if (!in_interrupt && new_hw_ptr == runtime->hw_ptr_interrupt) {
+		update_delay(substream, &curr_tstamp);
+	}
 	if (runtime->status->hw_ptr == new_hw_ptr) {
 		update_audio_tstamp(substream, &curr_tstamp, &audio_tstamp);
 		return 0;
-- 
2.5.5