From 4c389a9274d07d55c1547b92a569458492b8c5c5 Mon Sep 17 00:00:00 2001 From: Chris Spencer Date: Mon, 10 Jan 2022 20:45:05 +0000 Subject: [PATCH] [client] audio/pw: flush playback buffers before stopping This stops the end of the playback from being truncated. It also prevents an audible glitch when playback next starts due to the truncated data being left behind in the ring buffer. --- client/audiodevs/PipeWire/pipewire.c | 97 +++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 10 deletions(-) diff --git a/client/audiodevs/PipeWire/pipewire.c b/client/audiodevs/PipeWire/pipewire.c index d0dabfc3..655af6c9 100644 --- a/client/audiodevs/PipeWire/pipewire.c +++ b/client/audiodevs/PipeWire/pipewire.c @@ -29,6 +29,16 @@ #include "common/ringbuffer.h" #include "common/util.h" +typedef enum +{ + STREAM_STATE_INACTIVE, + STREAM_STATE_ACTIVE, + STREAM_STATE_FLUSHING, + STREAM_STATE_DRAINING, + STREAM_STATE_RESTARTING +} +StreamState; + struct PipeWire { struct pw_loop * loop; @@ -43,7 +53,7 @@ struct PipeWire int stride; RingBuffer buffer; - bool active; + StreamState state; } playback; @@ -68,7 +78,17 @@ static void pipewire_onPlaybackProcess(void * userdata) struct pw_buffer * pbuf; if (!ringbuffer_getCount(pw.playback.buffer)) + { + if (pw.playback.state == STREAM_STATE_FLUSHING) + { + pw_thread_loop_lock(pw.thread); + pw_stream_flush(pw.playback.stream, true); + pw.playback.state = STREAM_STATE_DRAINING; + pw_thread_loop_unlock(pw.thread); + } + return; + } if (!(pbuf = pw_stream_dequeue_buffer(pw.playback.stream))) { @@ -93,6 +113,26 @@ static void pipewire_onPlaybackProcess(void * userdata) pw_stream_queue_buffer(pw.playback.stream, pbuf); } +static void pipewire_onPlaybackDrained(void * userdata) +{ + pw_thread_loop_lock(pw.thread); + + if (pw.playback.state == STREAM_STATE_RESTARTING) + { + // A play command was received while we were in the middle of stopping; + // switch straight back into playing + pw_stream_set_active(pw.playback.stream, true); + pw.playback.state = STREAM_STATE_ACTIVE; + } + else + { + pw_stream_set_active(pw.playback.stream, false); + pw.playback.state = STREAM_STATE_INACTIVE; + } + + pw_thread_loop_unlock(pw.thread); +} + static bool pipewire_init(void) { pw_init(NULL, NULL); @@ -138,7 +178,6 @@ static void pipewire_playbackStopStream(void) return; pw_thread_loop_lock(pw.thread); - pw_stream_flush(pw.playback.stream, true); pw_stream_destroy(pw.playback.stream); pw.playback.stream = NULL; pw_thread_loop_unlock(pw.thread); @@ -152,7 +191,8 @@ static void pipewire_playbackStart(int channels, int sampleRate) static const struct pw_stream_events events = { .version = PW_VERSION_STREAM_EVENTS, - .process = pipewire_onPlaybackProcess + .process = pipewire_onPlaybackProcess, + .drained = pipewire_onPlaybackDrained }; if (pw.playback.stream && @@ -217,23 +257,61 @@ static void pipewire_playbackPlay(uint8_t * data, size_t size) ringbuffer_append(pw.playback.buffer, data, size / pw.playback.stride); - if (!pw.playback.active) + if (pw.playback.state != STREAM_STATE_ACTIVE && + pw.playback.state != STREAM_STATE_RESTARTING) { pw_thread_loop_lock(pw.thread); - pw_stream_set_active(pw.playback.stream, true); - pw.playback.active = true; + + switch (pw.playback.state) { + case STREAM_STATE_INACTIVE: + pw_stream_set_active(pw.playback.stream, true); + pw.playback.state = STREAM_STATE_ACTIVE; + break; + + case STREAM_STATE_FLUSHING: + // We were preparing to stop; just carry on as if nothing happened + pw.playback.state = STREAM_STATE_ACTIVE; + break; + + case STREAM_STATE_DRAINING: + // We are in the middle of draining the PipeWire buffers; we will need + // to reactivate the stream once this has completed + pw.playback.state = STREAM_STATE_RESTARTING; + break; + + default: + DEBUG_UNREACHABLE(); + } + pw_thread_loop_unlock(pw.thread); } } static void pipewire_playbackStop(void) { - if (!pw.playback.active) + if (pw.playback.state != STREAM_STATE_ACTIVE && + pw.playback.state != STREAM_STATE_RESTARTING) return; pw_thread_loop_lock(pw.thread); - pw_stream_set_active(pw.playback.stream, false); - pw.playback.active = false; + + switch (pw.playback.state) + { + case STREAM_STATE_ACTIVE: + pw.playback.state = STREAM_STATE_FLUSHING; + break; + + case STREAM_STATE_RESTARTING: + // A stop was requested, and then a start while PipeWire was draining, and + // now another stop. PipeWire hasn't finished draining yet so just switch + // the state back + pw.playback.state = STREAM_STATE_DRAINING; + break; + + default: + DEBUG_UNREACHABLE(); + } + pw_thread_loop_unlock(pw.thread); } @@ -265,7 +343,6 @@ static void pipewire_recordStopStream(void) return; pw_thread_loop_lock(pw.thread); - pw_stream_flush(pw.record.stream, true); pw_stream_destroy(pw.record.stream); pw.record.stream = NULL; pw_thread_loop_unlock(pw.thread);