Skip to content

Commit

Permalink
Support ALSA silence software parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
borine committed Apr 15, 2024
1 parent e6f3bf6 commit b648193
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 16 deletions.
197 changes: 181 additions & 16 deletions src/asound/bluealsa-pcm.c
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ struct bluealsa_pcm {
int event_fd;

/* virtual hardware - ring buffer */
char * _Atomic io_hw_buffer;
uint8_t *io_hw_buffer;
snd_pcm_channel_area_t *channel_areas;
/* The IO thread is responsible for maintaining the hardware pointer
* (pcm->io_hw_ptr), the application is responsible for the application
* pointer (io->appl_ptr). These pointers should be atomic as they are
Expand All @@ -99,6 +100,14 @@ struct bluealsa_pcm {
atomic_int io_period_event;
/* Permit the application to modify XRUN behavior. */
_Atomic snd_pcm_uframes_t io_stop_threshold;
_Atomic snd_pcm_uframes_t io_silence_threshold;
_Atomic snd_pcm_uframes_t io_silence_size;

/* When the silence_size sw param is enabled, we maintain a region of
* silence in the ring buffer, defined by its start offset and length */
snd_pcm_uframes_t silence_start;
snd_pcm_uframes_t silence_len;

pthread_t io_thread;
bool io_started;

Expand Down Expand Up @@ -228,6 +237,50 @@ static void io_thread_update_delay(struct bluealsa_pcm *pcm,

}

static void bluealsa_update_silence_region(snd_pcm_ioplug_t *io, snd_pcm_uframes_t appl_ptr) {
struct bluealsa_pcm *pcm = io->private_data;
if (appl_ptr != pcm->silence_start) {
snd_pcm_sframes_t silence_lost = appl_ptr - pcm->silence_start;
if (silence_lost < 0)
silence_lost += pcm->io_hw_boundary;
if (silence_lost < (snd_pcm_sframes_t)pcm->silence_len)
pcm->silence_len -= silence_lost;
else
pcm->silence_len = 0;

pcm->silence_start = appl_ptr;
}
}

/**
* Overwrite a section of the ring buffer with silence. */
static void bluealsa_silence(snd_pcm_ioplug_t *io, snd_pcm_uframes_t appl_ptr, snd_pcm_uframes_t hw_ptr) {
struct bluealsa_pcm *pcm = io->private_data;
snd_pcm_uframes_t frames = 0;

if (pcm->io_silence_threshold > 0) {
snd_pcm_sframes_t hw_avail = appl_ptr - hw_ptr;
if (hw_avail < 0)
hw_avail += pcm->io_hw_boundary;
snd_pcm_uframes_t noise_distance = hw_avail + pcm->silence_len;
if (noise_distance < pcm->io_silence_threshold) {
frames = pcm->io_silence_threshold - noise_distance;
if (frames > pcm->io_silence_size)
frames = pcm->io_silence_size;
}
}
else {
frames = io->buffer_size + hw_ptr - appl_ptr;
if (frames > io->buffer_size)
frames %= io->buffer_size;
}

snd_pcm_uframes_t offset = (pcm->silence_start + pcm->silence_len) % io->buffer_size;
pthread_mutex_lock(&pcm->mutex);
snd_pcm_areas_silence(pcm->channel_areas, offset, io->channels, frames, io->format);
pthread_mutex_unlock(&pcm->mutex);
}

/**
* IO thread, which facilitates ring buffer. */
static void *io_thread(snd_pcm_ioplug_t *io) {
Expand Down Expand Up @@ -289,7 +342,8 @@ static void *io_thread(snd_pcm_ioplug_t *io) {
io_hw_ptr = pcm->io_hw_ptr;
}

snd_pcm_uframes_t appl_avail = snd_pcm_ioplug_avail(io, io_hw_ptr, io->appl_ptr);
snd_pcm_sframes_t appl_ptr = io->appl_ptr;
snd_pcm_uframes_t appl_avail = snd_pcm_ioplug_avail(io, io_hw_ptr, 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;

Expand Down Expand Up @@ -341,7 +395,7 @@ static void *io_thread(snd_pcm_ioplug_t *io) {

/* IO operation size in bytes */
size_t len = chunk * pcm->frame_size;
char *head = pcm->io_hw_buffer + offset * pcm->frame_size;
uint8_t *head = pcm->io_hw_buffer + offset * pcm->frame_size;

ssize_t ret = 0;
if (io->stream == SND_PCM_STREAM_CAPTURE) {
Expand Down Expand Up @@ -369,6 +423,8 @@ static void *io_thread(snd_pcm_ioplug_t *io) {
}
else {

uint8_t *start = head;

/* Perform atomic write - see the explanation above. */
do {
if ((ret = write(pcm->ba_pcm_fd, head, len)) == -1) {
Expand All @@ -383,6 +439,11 @@ static void *io_thread(snd_pcm_ioplug_t *io) {
len -= ret;
} while (len != 0);

if (pcm->io_silence_size >= pcm->io_hw_boundary) {
/* Special case: fill just-written buffer frames with silence.*/
snd_pcm_format_set_silence(io->format, start, frames * io->channels);
}

}

frames_transfered += chunk;
Expand All @@ -393,10 +454,17 @@ static void *io_thread(snd_pcm_ioplug_t *io) {

io_thread_update_delay(pcm, io_hw_ptr);

/* synchronize playback time */
if (io->stream == SND_PCM_STREAM_PLAYBACK)
if (io->stream == SND_PCM_STREAM_PLAYBACK) {
/* synchronize playback time */
asrsync_sync(&asrs, frames);

/* Apply silence sw parameter settings */
if (io->state == SND_PCM_STATE_RUNNING &&
pcm->io_silence_size > 0 &&
pcm->io_silence_size < pcm->io_hw_boundary)
bluealsa_silence(io, appl_ptr, io_hw_ptr);
}

/* Make the new HW pointer value visible to the ioplug. */
pcm->io_hw_ptr = io_hw_ptr;

Expand Down Expand Up @@ -438,6 +506,15 @@ static int bluealsa_start(snd_pcm_ioplug_t *io) {
if (io->stream == SND_PCM_STREAM_PLAYBACK && io->appl_ptr == 0 && pcm->io_stop_threshold < pcm->io_hw_boundary)
return -EPIPE;

/* Special case: fill unused portion of buffer with silence.*/
if (io->stream == SND_PCM_STREAM_PLAYBACK &&
pcm->io_silence_size >= pcm->io_hw_boundary &&
pcm->io_silence_threshold == 0) {
const size_t offset = io->appl_ptr * pcm->frame_size;
unsigned int samples = (io->buffer_size - io->appl_ptr) * io->channels;
snd_pcm_format_set_silence(io->format, pcm->io_hw_buffer + offset, samples);
}

/* If the IO thread is already started, skip thread creation. Otherwise,
* we might end up with a bunch of IO threads reading or writing to the
* same FIFO simultaneously. Instead, just send resume signal. */
Expand Down Expand Up @@ -510,11 +587,14 @@ static snd_pcm_sframes_t bluealsa_pointer(snd_pcm_ioplug_t *io) {
if (!pcm->connected)
snd_pcm_ioplug_set_state(io, SND_PCM_STATE_DISCONNECTED);

#ifndef SND_PCM_IOPLUG_FLAG_BOUNDARY_WA
if (pcm->io_hw_ptr != -1)
return pcm->io_hw_ptr % io->buffer_size;
#endif
return pcm->io_hw_ptr;
snd_pcm_sframes_t hw_ptr = pcm->io_hw_ptr;
if (hw_ptr == -1)
return hw_ptr;

#ifndef SND_PCM_IOPLUG_FLAG_BOUNDARY_WA
return hw_ptr % io->buffer_size;
#endif
return hw_ptr;
}

static int bluealsa_close(snd_pcm_ioplug_t *io) {
Expand Down Expand Up @@ -626,6 +706,18 @@ static int bluealsa_hw_params(snd_pcm_ioplug_t *io, snd_pcm_hw_params_t *params)

pcm->frame_size = (snd_pcm_format_physical_width(io->format) * io->channels) / 8;

if ((pcm->io_hw_buffer = malloc(io->buffer_size * pcm->frame_size)) == NULL)
return -ENOMEM;

pcm->channel_areas = malloc(sizeof(snd_pcm_channel_area_t) * io->channels);
snd_pcm_channel_area_t *area = pcm->channel_areas;
unsigned channel;
for (channel = 0; channel < io->channels; ++channel, ++area) {
area->addr = pcm->io_hw_buffer;
area->first = channel * snd_pcm_format_physical_width(io->format);
area->step = pcm->frame_size * 8;
}

DBusError err = DBUS_ERROR_INIT;
if (!ba_dbus_pcm_open(&pcm->dbus_ctx, pcm->ba_pcm.pcm_path,
&pcm->ba_pcm_fd, &pcm->ba_pcm_ctrl_fd, &err)) {
Expand Down Expand Up @@ -679,6 +771,10 @@ static int bluealsa_hw_free(snd_pcm_ioplug_t *io) {
pcm->ba_pcm_ctrl_fd = -1;
pcm->connected = false;

free(pcm->io_hw_buffer);
free(pcm->channel_areas);
pcm->io_hw_buffer = NULL;
pcm->channel_areas = NULL;
return rv == 0 ? 0 : -errno;
}

Expand All @@ -690,12 +786,38 @@ static int bluealsa_sw_params(snd_pcm_ioplug_t *io, snd_pcm_sw_params_t *params)
snd_pcm_sw_params_get_boundary(params, &boundary);
pcm->io_hw_boundary = boundary;

snd_pcm_uframes_t silence_threshold;
snd_pcm_sw_params_get_silence_threshold(params, &silence_threshold);
snd_pcm_uframes_t silence_size;
snd_pcm_sw_params_get_silence_size(params, &silence_size);
if (silence_size >= boundary) {
if (silence_threshold != 0)
return -EINVAL;
}
else if (silence_size > silence_threshold)
return -EINVAL;

if (silence_threshold != pcm->io_silence_threshold) {
debug2("Changing SW silence threshold: %zu -> %zu", pcm->io_silence_threshold, silence_threshold);
pcm->io_silence_threshold = silence_threshold;
}
if (silence_size != pcm->io_silence_size) {
debug2("Changing SW silence size: %zu -> %zu", pcm->io_silence_size, silence_size);
pcm->io_silence_size = silence_size;
if (io->stream == SND_PCM_STREAM_PLAYBACK && pcm->io_silence_size >= pcm->io_hw_boundary) {
snd_pcm_sframes_t appl_ptr = io->appl_ptr;
bluealsa_update_silence_region(io, appl_ptr);
bluealsa_silence(io, appl_ptr, pcm->io_hw_ptr);
}
}

snd_pcm_uframes_t avail_min;
snd_pcm_sw_params_get_avail_min(params, &avail_min);
if (avail_min != pcm->io_avail_min) {
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)
Expand Down Expand Up @@ -727,12 +849,10 @@ static int bluealsa_prepare(snd_pcm_ioplug_t *io) {
/* initialize ring buffer */
pcm->io_hw_ptr = 0;

/* The ioplug allocates and configures its channel area buffer when the
* HW parameters are fixed, but after calling bluealsa_hw_params(). So,
* this is the earliest opportunity for us to safely cache the ring
* buffer start address. */
const snd_pcm_channel_area_t *areas = snd_pcm_ioplug_mmap_areas(io);
pcm->io_hw_buffer = (char *)areas->addr + areas->first / 8;
if (pcm->io_silence_size >= pcm->io_hw_boundary) {
/* Special case: fill buffer with silence.*/
snd_pcm_format_set_silence(io->format, pcm->io_hw_buffer, io->buffer_size * io->channels);
}

/* Indicate that our PCM is ready for IO, even though is is not 100%
* true - the IO thread may not be running yet. Applications using
Expand Down Expand Up @@ -1160,6 +1280,50 @@ static int bluealsa_poll_revents(snd_pcm_ioplug_t *io, struct pollfd *pfd,
return -ENODEV;
}

snd_pcm_sframes_t bluealsa_transfer(snd_pcm_ioplug_t *io, const snd_pcm_channel_area_t *areas, snd_pcm_uframes_t offset, snd_pcm_uframes_t size) {
struct bluealsa_pcm *pcm = io->private_data;
int result;

pthread_mutex_lock(&pcm->mutex);

if (io->stream == SND_PCM_STREAM_CAPTURE) {
result = snd_pcm_areas_copy_wrap(
areas,
offset,
size + offset,
pcm->channel_areas,
io->appl_ptr % io->buffer_size,
io->buffer_size,
io->channels,
size,
io->format);
}
else {
result = snd_pcm_areas_copy_wrap(
pcm->channel_areas,
io->appl_ptr % io->buffer_size,
io->buffer_size,
areas,
offset,
size + offset,
io->channels,
size,
io->format);

if (result > 0) {
/* This transfer may have overwritten part of our silence region */
snd_pcm_uframes_t new_appl_ptr = io->appl_ptr + result;
if (new_appl_ptr >= pcm->io_hw_boundary)
new_appl_ptr -= pcm->io_hw_boundary;

bluealsa_update_silence_region(io, new_appl_ptr);
}
}
pthread_mutex_unlock(&pcm->mutex);

return result < 0 ? result : (snd_pcm_sframes_t) size;
}

static const snd_pcm_ioplug_callback_t bluealsa_callback = {
.start = bluealsa_start,
.stop = bluealsa_stop,
Expand All @@ -1176,6 +1340,7 @@ static const snd_pcm_ioplug_callback_t bluealsa_callback = {
.poll_descriptors_count = bluealsa_poll_descriptors_count,
.poll_descriptors = bluealsa_poll_descriptors,
.poll_revents = bluealsa_poll_revents,
.transfer = bluealsa_transfer,
};

static int str2bdaddr(const char *str, bdaddr_t *ba) {
Expand Down
55 changes: 55 additions & 0 deletions test/test-alsa-pcm.c
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,60 @@ CK_START_TEST(test_playback_stop_threshold) {

} CK_END_TEST

CK_START_TEST(test_playback_silence) {

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;

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);

/* Set silence_size greater than silence_threshold */
ck_assert_int_eq(snd_pcm_sw_params_set_silence_size(pcm, sw_params, 2 * period_size), 0);
ck_assert_int_eq(snd_pcm_sw_params_set_silence_threshold(pcm, sw_params, period_size), 0);

/* check the params are rejected */
ck_assert_int_eq(snd_pcm_sw_params(pcm, sw_params), -EINVAL);

/* set silence_size to boundary with silence threshold non-zero */
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_silence_size(pcm, sw_params, boundary), 0);
ck_assert_int_eq(snd_pcm_sw_params_set_silence_threshold(pcm, sw_params, period_size), 0);

/* check the params are rejected */
ck_assert_int_eq(snd_pcm_sw_params(pcm, sw_params), -EINVAL);

/* set silence_size to boundary with silence threshold zero */
ck_assert_int_eq(snd_pcm_sw_params_set_silence_threshold(pcm, sw_params, 0), 0);
ck_assert_int_eq(snd_pcm_sw_params_set_silence_size(pcm, sw_params, boundary), 0);
ck_assert_int_eq(snd_pcm_sw_params(pcm, sw_params), 0);

/* test here ?? */

/* set silence_size and silence threshold to period_size */
ck_assert_int_eq(snd_pcm_sw_params_set_silence_threshold(pcm, sw_params, period_size), 0);
ck_assert_int_eq(snd_pcm_sw_params_set_silence_size(pcm, sw_params, period_size), 0);
ck_assert_int_eq(snd_pcm_sw_params(pcm, sw_params), 0);

/* test here ?? */

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 @@ -1389,6 +1443,7 @@ int main(int argc, char *argv[], char *envp[]) {
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);
tcase_add_test(tc, test_playback_silence);
suite_add_tcase(s, tc);
}

Expand Down

0 comments on commit b648193

Please sign in to comment.