Skip to content

Commit

Permalink
Support ALSA stop_threshold software parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
borine committed Jun 1, 2024
1 parent edb0120 commit dbb0715
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 27 deletions.
98 changes: 71 additions & 27 deletions src/asound/bluealsa-pcm.c
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ struct bluealsa_pcm {
_Atomic snd_pcm_uframes_t io_avail_min;
/* Raise poll event at every period end */
atomic_int io_period_event;
/* Permit the application to modify XRUN behavior. */
_Atomic snd_pcm_uframes_t io_stop_threshold;
pthread_t io_thread;
bool io_started;

Expand Down Expand Up @@ -133,20 +135,45 @@ struct bluealsa_pcm {
/**
* Get the available frames.
*
* This function is available in alsa-lib since version 1.1.6. For older
* These functions are available in alsa-lib since version 1.1.6. For older
* alsa-lib versions we need to provide our own implementation. */

/* The number of frames available to the application. May be larger than the
* buffer size if stop_threshold > buffer_size. */
static snd_pcm_uframes_t snd_pcm_ioplug_avail(const snd_pcm_ioplug_t * const io,
const snd_pcm_uframes_t hw_ptr, const snd_pcm_uframes_t appl_ptr) {
struct bluealsa_pcm *pcm = io->private_data;
snd_pcm_sframes_t avail;

if (io->stream == SND_PCM_STREAM_PLAYBACK) {
avail = hw_ptr + io->buffer_size - appl_ptr;
if (avail < 0)
avail += pcm->io_hw_boundary;
else if ((snd_pcm_uframes_t) avail >= pcm->io_hw_boundary)
avail -= pcm->io_hw_boundary;
}
else {
avail = hw_ptr - appl_ptr;
if (avail < 0)
avail += pcm->io_hw_boundary;
}

return avail;
}

/* The number of frames available to the bluealsa I/O thread. Can never be
* larger than the buffer size. */
static snd_pcm_uframes_t snd_pcm_ioplug_hw_avail(const snd_pcm_ioplug_t * const io,
const snd_pcm_uframes_t hw_ptr, const snd_pcm_uframes_t appl_ptr) {
struct bluealsa_pcm *pcm = io->private_data;
snd_pcm_sframes_t diff;
if (io->stream == SND_PCM_STREAM_PLAYBACK)
diff = appl_ptr - hw_ptr;
else
diff = io->buffer_size - hw_ptr + appl_ptr;
if (diff < 0)
diff += pcm->io_hw_boundary;
snd_pcm_uframes_t diff_ = diff;
return diff_ <= io->buffer_size ? diff_ : 0;
snd_pcm_uframes_t avail;

avail = snd_pcm_ioplug_avail(ioplug, hw_ptr, appl_ptr);

if (avail > io->buffer_size)
return 0;

return io->buffer_size - avail;
}
#endif

Expand Down Expand Up @@ -262,15 +289,27 @@ static void *io_thread(snd_pcm_ioplug_t *io) {
io_hw_ptr = pcm->io_hw_ptr;
}

/* There are 2 reasons why the number of available frames may be
* zero: XRUN or drained final samples; we set the HW pointer to
* -1 to indicate we have no work to do. */
snd_pcm_uframes_t avail;
if ((avail = snd_pcm_ioplug_hw_avail(io, io_hw_ptr, io->appl_ptr)) == 0) {
pcm->io_hw_ptr = io_hw_ptr = -1;
io_thread_update_delay(pcm, io_hw_ptr);
eventfd_write(pcm->event_fd, 1);
continue;
snd_pcm_uframes_t appl_avail = snd_pcm_ioplug_avail(io, io_hw_ptr, io->appl_ptr);
snd_pcm_uframes_t avail = appl_avail < io->buffer_size ? io->buffer_size - appl_avail : io->period_size;
snd_pcm_uframes_t stop_threshold = io->state == SND_PCM_STATE_DRAINING ? io->buffer_size : pcm->io_stop_threshold;

/* If the stop_threshold == boundary then we ignore the application
* pointer and continue to send one period of frames on each iteration.
*/
if (stop_threshold == pcm->io_hw_boundary) {
appl_avail = io->buffer_size;
avail = io->period_size;
}
else {
/* Otherwise we must stop the pcm when the stop threshold is
* reached; we set the HW pointer to -1 to indicate we have no work
* to do. */
if (appl_avail >= stop_threshold) {
pcm->io_hw_ptr = io_hw_ptr = -1;
io_thread_update_delay(pcm, io_hw_ptr);
eventfd_write(pcm->event_fd, 1);
continue;
}
}

/* current offset of the head pointer in the IO buffer */
Expand Down Expand Up @@ -363,7 +402,9 @@ static void *io_thread(snd_pcm_ioplug_t *io) {

/* Wake application thread if enough space/frames is available, or
* io_period_event is set. */
if (frames + io->buffer_size - avail >= pcm->io_avail_min || pcm->io_period_event)
if (frames + appl_avail >= pcm->io_avail_min ||
pcm->io_stop_threshold > io->buffer_size ||
pcm->io_period_event)
eventfd_write(pcm->event_fd, 1);

}
Expand Down Expand Up @@ -394,7 +435,7 @@ static int bluealsa_start(snd_pcm_ioplug_t *io) {
* so, to be consistent with results from testing the hw plugin, when
* snd_pcm_start() is called on a playback stream with the buffer empty we
* leave the state unchanged (SND_PCM_STATE_PREPARED) but return -EPIPE. */
if (io->stream == SND_PCM_STREAM_PLAYBACK && io->appl_ptr == 0)
if (io->stream == SND_PCM_STREAM_PLAYBACK && io->appl_ptr == 0 && pcm->io_stop_threshold < pcm->io_hw_boundary)
return -EPIPE;

/* If the IO thread is already started, skip thread creation. Otherwise,
Expand Down Expand Up @@ -655,6 +696,14 @@ static int bluealsa_sw_params(snd_pcm_ioplug_t *io, snd_pcm_sw_params_t *params)
debug2("Changing SW avail min: %zu -> %zu", pcm->io_avail_min, avail_min);
pcm->io_avail_min = avail_min;
}
snd_pcm_uframes_t stop_threshold;
snd_pcm_sw_params_get_stop_threshold(params, &stop_threshold);
if (stop_threshold >= pcm->io_hw_boundary)
stop_threshold = pcm->io_hw_boundary;
if (stop_threshold != pcm->io_stop_threshold) {
debug2("Changing SW stop threshold: %zu -> %zu", pcm->io_stop_threshold, stop_threshold);
pcm->io_stop_threshold = stop_threshold;
}

int period_event;
snd_pcm_sw_params_get_period_event(params, &period_event);
Expand Down Expand Up @@ -1062,12 +1111,7 @@ static int bluealsa_poll_revents(snd_pcm_ioplug_t *io, struct pollfd *pfd,
ready = false;
break;
case SND_PCM_STATE_PREPARED:
/* capture poll should block forever */
if (io->stream == SND_PCM_STREAM_CAPTURE) {
ready = false;
*revents = 0;
}
else if ((snd_pcm_uframes_t)avail < pcm->io_avail_min) {
if ((snd_pcm_uframes_t)avail < pcm->io_avail_min) {
ready = false;
*revents = 0;
}
Expand Down
101 changes: 101 additions & 0 deletions test/test-alsa-pcm.c
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,106 @@ CK_START_TEST(test_playback_period_event) {

} CK_END_TEST

CK_START_TEST(test_playback_stop_threshold) {

unsigned int buffer_time = 200000;
unsigned int period_time = 25000;
snd_pcm_uframes_t buffer_size;
snd_pcm_uframes_t period_size;
struct spawn_process sp_ba_mock;
snd_pcm_t *pcm = NULL;
size_t i;

ck_assert_int_eq(test_pcm_open(&sp_ba_mock, &pcm, SND_PCM_STREAM_PLAYBACK), 0);
ck_assert_int_eq(set_hw_params(pcm, pcm_format, pcm_channels, pcm_sampling,
&buffer_time, &period_time), 0);
ck_assert_int_eq(snd_pcm_get_params(pcm, &buffer_size, &period_size), 0);

ck_assert_int_eq(snd_pcm_prepare(pcm), 0);

snd_pcm_sw_params_t *sw_params;
snd_pcm_sw_params_alloca(&sw_params);
ck_assert_int_eq(snd_pcm_sw_params_current(pcm, sw_params), 0);

/* setup PCM not to start automatically */
snd_pcm_uframes_t boundary;
ck_assert_int_eq(snd_pcm_sw_params_get_boundary(sw_params, &boundary), 0);
ck_assert_int_eq(snd_pcm_sw_params_set_start_threshold(pcm, sw_params, boundary), 0);
ck_assert_int_eq(snd_pcm_sw_params(pcm, sw_params), 0);

/* write four periods */
for (i = 0; i < 4; i++)
ck_assert_int_eq(snd_pcm_writei(pcm, test_sine_s16le(period_size), period_size), period_size);

/* setup PCM to stop before the buffer is emptied */
ck_assert_int_eq(snd_pcm_sw_params_set_stop_threshold(pcm, sw_params, period_size * 6), 0);
ck_assert_int_eq(snd_pcm_sw_params(pcm, sw_params), 0);

/* start the PCM */
ck_assert_int_eq(snd_pcm_start(pcm), 0);

/* check PCM is running */
ck_assert_int_eq(snd_pcm_state_runtime(pcm), SND_PCM_STATE_RUNNING);

/* sleep for just over 2 periods */
usleep(100 + 2 * period_time);

/* check that PCM has stopped */
ck_assert_int_eq(snd_pcm_state_runtime(pcm), SND_PCM_STATE_XRUN);
ck_assert_int_eq(snd_pcm_avail(pcm), -EPIPE);

/* reset the PCM */
ck_assert_int_eq(snd_pcm_drop(pcm), 0);
ck_assert_int_eq(snd_pcm_prepare(pcm), 0);

/* setup PCM to stop three periods after the buffer is emptied */
ck_assert_int_eq(snd_pcm_sw_params_set_stop_threshold(pcm, sw_params, buffer_size + 3 * period_size), 0);
ck_assert_int_eq(snd_pcm_sw_params(pcm, sw_params), 0);

/* write one period */
ck_assert_int_eq(snd_pcm_writei(pcm, test_sine_s16le(period_size), period_size), period_size);

/* start the PCM */
ck_assert_int_eq(snd_pcm_start(pcm), 0);

/* check PCM is running */
ck_assert_int_eq(snd_pcm_state_runtime(pcm), SND_PCM_STATE_RUNNING);

/* sleep for just over two periods */
usleep(100 + 2 * period_time);

/* check PCM is still running */
ck_assert_int_eq(snd_pcm_state_runtime(pcm), SND_PCM_STATE_RUNNING);

/* sleep for two more periods */
usleep(2 * period_time);

/* check PCM has stopped */
ck_assert_int_eq(snd_pcm_state_runtime(pcm), SND_PCM_STATE_XRUN);

/* reset the PCM */
ck_assert_int_eq(snd_pcm_drop(pcm), 0);
ck_assert_int_eq(snd_pcm_prepare(pcm), 0);

/* setup PCM to run continuously */
ck_assert_int_eq(snd_pcm_sw_params_set_stop_threshold(pcm, sw_params, boundary), 0);
ck_assert_int_eq(snd_pcm_sw_params(pcm, sw_params), 0);

/* start the PCM */
ck_assert_int_eq(snd_pcm_start(pcm), 0);

/* check PCM is running */
ck_assert_int_eq(snd_pcm_state_runtime(pcm), SND_PCM_STATE_RUNNING);

/* sleep for more than the buffer time */
usleep(buffer_time + period_time);

/* check PCM is still running */
ck_assert_int_eq(snd_pcm_state_runtime(pcm), SND_PCM_STATE_RUNNING);

ck_assert_int_eq(test_pcm_close(&sp_ba_mock, pcm), 0);

} CK_END_TEST

int main(int argc, char *argv[], char *envp[]) {
preload(argc, argv, envp, ".libs/aloader.so");
Expand Down Expand Up @@ -1288,6 +1388,7 @@ int main(int argc, char *argv[], char *envp[]) {
tcase_add_test(tc, test_playback_underrun);
tcase_add_test(tc, ba_test_playback_device_unplug);
tcase_add_test(tc, test_playback_period_event);
tcase_add_test(tc, test_playback_stop_threshold);
suite_add_tcase(s, tc);
}

Expand Down

0 comments on commit dbb0715

Please sign in to comment.