Skip to content

Commit

Permalink
Improve PCM plugin drain
Browse files Browse the repository at this point in the history
Implement support for applications that use snd_pcm_abort() or
snd_pcm_nonblock(pcm, 2) to request that blocking calls are
aborted by a signal. Also apply a timeout to the local drain so
that an application can not be blocked indefinitely should the
bluealsa server stop reading from the PCM FIFO.
  • Loading branch information
borine committed Sep 25, 2023
1 parent 221d096 commit ae1f6b5
Showing 1 changed file with 52 additions and 30 deletions.
82 changes: 52 additions & 30 deletions src/asound/bluealsa-pcm.c
Original file line number Diff line number Diff line change
Expand Up @@ -666,47 +666,69 @@ static int bluealsa_drain(snd_pcm_ioplug_t *io) {
struct bluealsa_pcm *pcm = io->private_data;
debug2("Draining");

if (!pcm->connected) {
snd_pcm_ioplug_set_state(io, SND_PCM_STATE_DISCONNECTED);
return -ENODEV;
}

/* A bug in the ioplug drain implementation means that snd_pcm_drain()
* always either finishes in state SND_PCM_STATE_SETUP or returns an error.
* It is not possible to finish in state SND_PCM_STATE_DRAINING and return
* success; therefore is is impossible to correctly implement capture
* drain logic. So for capture PCMs we do nothing and return success. */
if (io->stream == SND_PCM_STREAM_CAPTURE)
return 0;

if (io->stream == SND_PCM_STREAM_PLAYBACK) {

/* We must ensure that all remaining frames in the ring buffer are
* flushed to the FIFO by the I/O thread. It is possible that the
* client has called snd_pcm_drain() without the start_threshold
* having been reached, or while paused, so we must first ensure that
* the IO thread is running. */
if (bluealsa_start(io) < 0)
return 0;

/* Block until the drain is complete. */
struct pollfd pfd = { pcm->event_fd, POLLIN, 0 };
while (bluealsa_pointer(io) >= 0 && io->state == SND_PCM_STATE_DRAINING) {
if (poll(&pfd, 1, -1) == -1) {
if (errno == EINTR)
continue;
break;
}
if (pfd.revents & POLLIN) {
eventfd_t event;
eventfd_read(pcm->event_fd, &event);
if (event & 0xDEAD0000)
break;
}
/* We must ensure that all remaining frames in the ring buffer are flushed
* to the FIFO by the I/O thread. It is possible that the client has called
* snd_pcm_drain() without the start_threshold having been reached, or
* while paused, so we must first ensure that the IO thread is running. */
if (bluealsa_start(io) < 0)
return 0;

bool aborted = false;

/* Temporarily set avail_min such that the IO thread does not wake us
* until the buffer is empty. */
snd_pcm_uframes_t saved_avail_min = pcm->io_avail_min;
pcm->io_avail_min = io->buffer_size;

struct pollfd pfd = { pcm->event_fd, POLLIN, 0 };
snd_pcm_sframes_t hw_ptr;
while ((hw_ptr = bluealsa_pointer(io)) >= 0 && io->state == SND_PCM_STATE_DRAINING) {

/* We set a timeout to ensure that the plugin cannot block forever in
* case the server has stopped reading from the FIFO. There may be a
* wait of up to one period before the IO thread next writes to the
* FIFO, so we allow for that in setting the maximum wait time. If the
* wait is re-started after being interrupted by a signal then we must
* re-calculate the maximum waiting time that remains. */
snd_pcm_uframes_t avail = snd_pcm_ioplug_hw_avail(io, hw_ptr, io->appl_ptr);
int timeout = (avail + io->period_size) * 1000 / io->rate;

int nready = poll(&pfd, 1, timeout);
if (nready == -1) {
/* It is not well documented by ALSA, but if the application
* has requested that the PCM should be aborted by a signal
* then the ioplug nonblock flag is set to the special value 2. */
if (errno == EINTR && io->nonblock != 2)
continue;
/* Application has aborted the drain. */
aborted = true;
break;
}
if (nready == 0)
/* Timeout - do not wait any longer. */
break;

bluealsa_dbus_pcm_ctrl_send_drain(pcm->ba_pcm_ctrl_fd, NULL);
if (pfd.revents & POLLIN) {
eventfd_t event;
eventfd_read(pcm->event_fd, &event);
}

}

/* Restore the avail_min parameter */
pcm->io_avail_min = saved_avail_min;

if (io->state == SND_PCM_STATE_DRAINING && !aborted)
bluealsa_dbus_pcm_ctrl_send_drain(pcm->ba_pcm_ctrl_fd, NULL);

/* We cannot recover from an error here. By returning zero we ensure that
* ioplug stops the pcm. Returning an error code would be interpreted by
* ioplug as an incomplete drain and would it leave the pcm running. */
Expand Down

0 comments on commit ae1f6b5

Please sign in to comment.