diff mbox series

[v2,2/2] ALSA: pcm: auto-fill buffer with silence when draining playback

Message ID 20230420113324.877164-2-oswald.buddenhagen@gmx.de
State New
Headers show
Series [v2,1/2] ALSA: pcm: rewrite snd_pcm_playback_silence() | expand

Commit Message

Oswald Buddenhagen April 20, 2023, 11:33 a.m. UTC
Draining will always playback somewhat beyond the end of the filled
buffer. This would produce artifacts if the user did not set up the
auto-silencing machinery, which is an extremely easy mistake to make, as
the API strongly suggests convenient fire-and-forget semantics. This
patch makes it work out of the box.

Applying this unconditionally is uncontroversial for RW_ACCESS, as the
buffer is fully controlled by the kernel in this case, which a) makes
failure to set up silencing even more likely and b) no detrimental
effects on user space are possible.

MMAP_ACCESS is a different matter:
- It can be argued that the user can be expected to know that the buffer
  needs to be padded one way or another. I dispute that; of the numerous
  resources I surveyed, only one mentioned this. Draining is a
  convenience function also in the mmap case - an application that wants
  to control things finely would just use start/stop and manage the
  timing itself.
- It can be argued that it's a bad thing to overwrite a buffer the user
  has access to without them explicitly requesting it. While technically
  true, I think that's only a hypothetical issue - applications can be
  expected to treat consumed samples as undefined data:
  - The cases where playing back the same samples would be even useful
    and practical are extremely limited.
  - Most user code uses cross-platform/-API abstractions, which makes it
    even less likely that they would get the idea that it's OK to re-use
    buffered samples.

So I think the trade-off between fixing numerous applications and
potentially breaking some is skewed towards the former to the point that
it's not even a question.

We do the filling even if the driver supports exact draining
(SNDRV_PCM_TRIGGER_DRAIN), because a) the cost of filling two periods
from time to time is negligible, so it's not worth complicating the code
and b) so the behavior is consistent between drivers, so hypothetical
problems with the mmap case would be easier to reproduce.

It would be possible to add an opt-in API to the kernel and leave
actually enabling it to alsa-lib. However, this would add significant
overall complexity, for no obvious gain.

Signed-off-by: Oswald Buddenhagen <oswald.buddenhagen@gmx.de>

---
v2:
- fill only up to two periods, to avoid undue load with big buffers
- added discussion to commit message
---
 sound/core/pcm_lib.c    | 47 +++++++++++++++++++++++++----------------
 sound/core/pcm_native.c |  3 ++-
 2 files changed, 31 insertions(+), 19 deletions(-)

Comments

Jaroslav Kysela April 21, 2023, 9:33 a.m. UTC | #1
On 20. 04. 23 13:33, Oswald Buddenhagen wrote:
> Draining will always playback somewhat beyond the end of the filled
> buffer. This would produce artifacts if the user did not set up the
> auto-silencing machinery, which is an extremely easy mistake to make, as
> the API strongly suggests convenient fire-and-forget semantics. This
> patch makes it work out of the box.

NACK. The initial implementation should be put to alsa-lib as discussed.

				Jaroslav
Oswald Buddenhagen April 21, 2023, 10:04 a.m. UTC | #2
On Fri, Apr 21, 2023 at 11:33:35AM +0200, Jaroslav Kysela wrote:
>On 20. 04. 23 13:33, Oswald Buddenhagen wrote:
>> Draining will always playback somewhat beyond the end of the filled
>> buffer. This would produce artifacts if the user did not set up the
>> auto-silencing machinery, which is an extremely easy mistake to make, as
>> the API strongly suggests convenient fire-and-forget semantics. This
>> patch makes it work out of the box.
>
>NACK. The initial implementation should be put to alsa-lib as discussed.
>
as discussed, a user-space only implementation based on the current 
kernel api is not reasonable:
it could either enable auto-silencing on device open (which would be 
unreasonably expensive) or it could enable it on drain (and disable it 
once draining is done, which would be unreasonably complex due to 
needing to handle asynchronous draining completion).

so we would at least need a kernel api to enable silence-on-drain, which 
user space could apply on device open.
this would be easy enough to do, but i really don't see a point in 
adding that complexity, given that it should be always enabled, lest it 
won't have much of a real-world impact.

fwiw, i just realized that the argument against touching the mmap'd 
buffer is even weaker with the updated implementation, as it only clears 
as much as user space would have to clear anyway (pedantically, i could 
round it down to the end of a period rather than filling two whole 
periods, if that's the bit that convinces you).

regards
Jaroslav Kysela April 21, 2023, 1:29 p.m. UTC | #3
On 21. 04. 23 12:04, Oswald Buddenhagen wrote:
> On Fri, Apr 21, 2023 at 11:33:35AM +0200, Jaroslav Kysela wrote:
>> On 20. 04. 23 13:33, Oswald Buddenhagen wrote:
>>> Draining will always playback somewhat beyond the end of the filled
>>> buffer. This would produce artifacts if the user did not set up the
>>> auto-silencing machinery, which is an extremely easy mistake to make, as
>>> the API strongly suggests convenient fire-and-forget semantics. This
>>> patch makes it work out of the box.
>>
>> NACK. The initial implementation should be put to alsa-lib as discussed.
>>
> as discussed, a user-space only implementation based on the current
> kernel api is not reasonable:
> it could either enable auto-silencing on device open (which would be
> unreasonably expensive) or it could enable it on drain (and disable it
> once draining is done, which would be unreasonably complex due to
> needing to handle asynchronous draining completion).

I doubt. We should consider all solutions. The drain ends with the SETUP 
state, thus the application must call prepare again. We can restore the 
sw_params there for all types of i/o access (if the app does not reset 
sw_params itself). We can just set the silence_size (sw_params) in 
snd_pcm_hw_drain() and it's all.

Also, an interrupt can be "lost" or "merged" only for the small periods where 
the system is not able to handle the fast interrupts. For large periods, we 
should not assume that any of the interrupt is lost. Otherwise, it would break 
many things and the driver is really broken in this case. So the drain fill 
size should be updated for the big periods like "fill_align_to_last_period + 
100ms" or so.

					Jaroslav
diff mbox series

Patch

diff --git a/sound/core/pcm_lib.c b/sound/core/pcm_lib.c
index 5bb2129cceac..b8940ceeaedb 100644
--- a/sound/core/pcm_lib.c
+++ b/sound/core/pcm_lib.c
@@ -61,24 +61,35 @@  void snd_pcm_playback_silence(struct snd_pcm_substream *substream)
 		runtime->silence_start = appl_ptr;
 	}
 
-	// This will "legitimately" turn negative on underrun, and will be mangled
-	// into a huge number by the boundary crossing handling. The initial state
-	// might also be not quite sane. The code below MUST account for these cases.
-	hw_avail = appl_ptr - runtime->status->hw_ptr;
-	if (hw_avail < 0)
-		hw_avail += runtime->boundary;
+	if (runtime->state == SNDRV_PCM_STATE_DRAINING) {
+		// We actually need only the next period boundary plus the FIFO size
+		// plus some slack for IRQ delays, but it's not worth calculating that.
+		frames = runtime->period_size * 2 - runtime->silence_filled;
+		if (frames <= 0)
+			return;
+		// Impossible, unless the buffer has only one period.
+		if (frames > runtime->buffer_size)
+			frames = runtime->buffer_size;
+	} else  {
+		// This will "legitimately" turn negative on underrun, and will be mangled
+		// into a huge number by the boundary crossing handling. The initial state
+		// might also be not quite sane. The code below MUST account for these cases.
+		hw_avail = appl_ptr - runtime->status->hw_ptr;
+		if (hw_avail < 0)
+			hw_avail += runtime->boundary;
 
-	noise_dist = hw_avail + runtime->silence_filled;
-	if (runtime->silence_size < runtime->boundary) {
-		frames = runtime->silence_threshold - noise_dist;
-		if (frames <= 0)
-			return;
-		if (frames > runtime->silence_size)
-			frames = runtime->silence_size;
-	} else {
-		frames = runtime->buffer_size - noise_dist;
-		if (frames <= 0)
-			return;
+		noise_dist = hw_avail + runtime->silence_filled;
+		if (runtime->silence_size < runtime->boundary) {
+			frames = runtime->silence_threshold - noise_dist;
+			if (frames <= 0)
+				return;
+			if (frames > runtime->silence_size)
+				frames = runtime->silence_size;
+		} else {
+			frames = runtime->buffer_size - noise_dist;
+			if (frames <= 0)
+				return;
+		}
 	}
 
 	if (snd_BUG_ON(frames > runtime->buffer_size))
@@ -443,7 +454,7 @@  static int snd_pcm_update_hw_ptr0(struct snd_pcm_substream *substream,
 	}
 
 	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK &&
-	    runtime->silence_size > 0)
+	    (runtime->silence_size > 0 || runtime->state == SNDRV_PCM_STATE_DRAINING))
 		snd_pcm_playback_silence(substream);
 
 	update_audio_tstamp(substream, &curr_tstamp, &audio_tstamp);
diff --git a/sound/core/pcm_native.c b/sound/core/pcm_native.c
index 0e3e7997dc58..6ecb6a733606 100644
--- a/sound/core/pcm_native.c
+++ b/sound/core/pcm_native.c
@@ -1454,7 +1454,7 @@  static void snd_pcm_post_start(struct snd_pcm_substream *substream,
 							    runtime->rate;
 	__snd_pcm_set_state(runtime, state);
 	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK &&
-	    runtime->silence_size > 0)
+	    (runtime->silence_size > 0 || state == SNDRV_PCM_STATE_DRAINING))
 		snd_pcm_playback_silence(substream);
 	snd_pcm_timer_notify(substream, SNDRV_TIMER_EVENT_MSTART);
 }
@@ -2045,6 +2045,7 @@  static int snd_pcm_do_drain_init(struct snd_pcm_substream *substream,
 			break;
 		case SNDRV_PCM_STATE_RUNNING:
 			__snd_pcm_set_state(runtime, SNDRV_PCM_STATE_DRAINING);
+			snd_pcm_playback_silence(substream);
 			break;
 		case SNDRV_PCM_STATE_XRUN:
 			__snd_pcm_set_state(runtime, SNDRV_PCM_STATE_SETUP);