Skip to content

Commit

Permalink
aplay: Improve overrun/underrun handling
Browse files Browse the repository at this point in the history
Do not allow audio frames to accumulate in the FIFO, and do not
block when writing to the ALSA PCM. By this means we prevent the
delay from increasing much beyond our chosen buffer sizes, and
ensure that if it becomes necessary to drop audio frames then we
always drop only the oldest ones.

Avoid underruns on the ALSA PCM as far as possible by inserting
silence when there are not enough frames available to maintain the
ALSA buffer fill level above the period size.

Use the BlueALSA PCM "running" property to detect when the transport
becomes idle. This avoids closing the ALSA PCM whenever an unstable
Bluetooth link causes a short break in the stream, allowing
bluealsa-aplay to play silence to keep the ALSA stream running. When
the transport becomes idle, drain the ALSA PCM before closing it to
ensure that all audio frames are played out.
  • Loading branch information
borine authored and arkq committed Feb 6, 2025
1 parent 10c36bb commit 82bbbed
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 72 deletions.
115 changes: 87 additions & 28 deletions utils/aplay/alsa-pcm.c
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,19 @@ static int alsa_pcm_set_sw_params(
goto fail;
}

/* Start the transfer when the buffer is half full - this allows
* spare capacity to accommodate bursts and short breaks in the
* Bluetooth stream. */
snd_pcm_uframes_t threshold = pcm->start_threshold = buffer_size / 2;
/* Start the transfer when three periods have been written (or when the
* buffer is full if it holds less than three periods. */
snd_pcm_uframes_t threshold = period_size * 3;
if (threshold > buffer_size)
threshold = buffer_size;

pcm->start_threshold = threshold;

if ((err = snd_pcm_sw_params_set_start_threshold(snd_pcm, params, threshold)) != 0) {
snprintf(buf, sizeof(buf), "Set start threshold: %s: %lu", snd_strerror(err), threshold);
goto fail;
}

/* Allow the transfer when at least period_size samples can be processed. */
if ((err = snd_pcm_sw_params_set_avail_min(snd_pcm, params, period_size)) != 0) {
snprintf(buf, sizeof(buf), "Set avail min: %s: %lu", snd_strerror(err), period_size);
goto fail;
}

if ((err = snd_pcm_sw_params(snd_pcm, params)) != 0) {
snprintf(buf, sizeof(buf), "%s", snd_strerror(err));
goto fail;
Expand All @@ -131,7 +129,7 @@ static int alsa_pcm_set_sw_params(
}

void alsa_pcm_init(struct alsa_pcm *pcm) {
pcm->pcm = NULL;
memset(pcm, 0, sizeof(*pcm));
}

int alsa_pcm_open(
Expand Down Expand Up @@ -187,6 +185,11 @@ int alsa_pcm_open(
pcm->period_time = actual_period_time;
pcm->buffer_frames = buffer_size;
pcm->period_frames = period_size;
pcm->delay = 0;

/* Maintain buffer fill level above 1 period plus 2ms to allow
* for scheduling delays */
pcm->underrun_threshold = pcm->period_frames + pcm->rate * 2 / 1000;

return 0;

Expand All @@ -196,7 +199,6 @@ int alsa_pcm_open(
*msg = strdup(buf);
free(tmp);
return err;

}

void alsa_pcm_close(struct alsa_pcm *pcm) {
Expand All @@ -205,30 +207,87 @@ void alsa_pcm_close(struct alsa_pcm *pcm) {
pcm->pcm = NULL;
}

int alsa_pcm_write(struct alsa_pcm *pcm, ffb_t *buffer) {
int alsa_pcm_write(
struct alsa_pcm *pcm,
ffb_t *buffer,
bool drain,
unsigned int verbose) {

size_t samples = ffb_len_out(buffer);
snd_pcm_sframes_t frames;
snd_pcm_sframes_t avail = 0;
snd_pcm_sframes_t delay = 0;
snd_pcm_sframes_t ret;

for (;;) {
frames = samples / pcm->channels;
if ((frames = snd_pcm_writei(pcm->pcm, buffer->data, frames)) > 0)
break;
switch (-frames) {
case EINTR:
continue;
case EPIPE:
pcm->underrun = false;
if ((ret = snd_pcm_avail_delay(pcm->pcm, &avail, &delay)) < 0) {
if (ret == -EPIPE) {
debug("ALSA playback PCM underrun");
pcm->underrun = true;
snd_pcm_prepare(pcm->pcm);
continue;
default:
error("ALSA playback PCM write error: %s", snd_strerror(frames));
avail = pcm->buffer_frames;
delay = 0;
}
else {
error("ALSA playback PCM error: %s", snd_strerror(ret));
return -1;
}
}

/* Move leftovers to the beginning of buffer and reposition tail. */
ffb_shift(buffer, frames * pcm->channels);
snd_pcm_sframes_t frames = ffb_len_out(buffer) / pcm->channels;
snd_pcm_sframes_t written_frames = 0;

/* If not draining, write only as many frames as possible without
* blocking. If necessary insert silence frames to prevent underrun. */
if (!drain) {
if (frames > avail)
frames = avail;
else if (pcm->buffer_frames - avail + frames < pcm->underrun_threshold &&
snd_pcm_state(pcm->pcm) == SND_PCM_STATE_RUNNING) {
/* Pad the buffer with enough silence to restore it to the underrun
* threshold. */
const size_t padding_frames = pcm->underrun_threshold - frames;
const size_t padding_samples = padding_frames * pcm->channels;
if (verbose >= 3)
info("Underrun imminent: inserting %zu silence frames", padding_frames);
snd_pcm_format_set_silence(pcm->format, buffer->tail, padding_samples);
ffb_seek(buffer, padding_samples);
frames += padding_frames;
}
}

while (frames > 0) {
ret = snd_pcm_writei(pcm->pcm, buffer->data, frames);
if (ret < 0)
switch (-ret) {
case EINTR:
continue;
case EPIPE:
debug("ALSA playback PCM underrun");
pcm->underrun = true;
snd_pcm_prepare(pcm->pcm);
continue;
default:
error("ALSA playback PCM write error: %s", snd_strerror(ret));
return -1;
}
else {
written_frames += ret;
frames -= ret;
delay += ret;
}
}

if (drain) {
snd_pcm_drain(pcm->pcm);
ffb_rewind(buffer);
return 0;
}

pcm->delay = delay + written_frames;

/* Move leftovers to the beginning and reposition tail. */
if (written_frames > 0)
ffb_shift(buffer, written_frames * pcm->channels);

return 0;
}

Expand Down
16 changes: 9 additions & 7 deletions utils/aplay/alsa-pcm.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ struct alsa_pcm {
* automatic start of the ALSA device. */
snd_pcm_uframes_t start_threshold;

/* The number of frames below which we are going to pad
* the buffer with silence to prevent underrun. */
snd_pcm_uframes_t underrun_threshold;
/* Indicates whether the last write recovered from an underrun. */
bool underrun;

/* The number of bytes in 1 sample. */
size_t sample_size;
/* The number of bytes in 1 frame. */
Expand Down Expand Up @@ -71,12 +77,6 @@ inline static bool alsa_pcm_is_open(
return pcm->pcm != NULL;
}

inline static int alsa_pcm_delay(
const struct alsa_pcm *pcm,
snd_pcm_sframes_t *delay) {
return snd_pcm_delay(pcm->pcm, delay);
}

inline static ssize_t alsa_pcm_frames_to_bytes(
const struct alsa_pcm *pcm,
snd_pcm_sframes_t frames) {
Expand All @@ -85,7 +85,9 @@ inline static ssize_t alsa_pcm_frames_to_bytes(

int alsa_pcm_write(
struct alsa_pcm *pcm,
ffb_t *buffer);
ffb_t *buffer,
bool drain,
unsigned int verbose);

void alsa_pcm_dump(
const struct alsa_pcm *pcm,
Expand Down
80 changes: 50 additions & 30 deletions utils/aplay/aplay.c
Original file line number Diff line number Diff line change
Expand Up @@ -431,8 +431,11 @@ static void *io_worker_routine(struct io_worker *w) {
pthread_cleanup_push(PTHREAD_CLEANUP(io_worker_routine_exit), w);
pthread_cleanup_push(PTHREAD_CLEANUP(ffb_free), &buffer);

/* create buffer big enough to hold 100 ms of PCM data */
if (ffb_init(&buffer, pcm_1s_samples / 10, pcm_format_size) == -1) {
/* Create a buffer big enough to hold enough PCM data for half the
* requested PCM buffer time. This will be revised to match the actual
* ALSA start threshold when the ALSA PCM is opened. */
const size_t nmemb = pcm_buffer_time * pcm_1s_samples / 1000000 / 2;
if (ffb_init(&buffer, nmemb, pcm_format_size) == -1) {
error("Couldn't create PCM buffer: %s", strerror(errno));
goto fail;
}
Expand Down Expand Up @@ -462,11 +465,6 @@ static void *io_worker_routine(struct io_worker *w) {
goto fail;
}

/* Initialize the max read length to 10 ms. Later, when the PCM device
* will be opened, this value will be adjusted to one period size. */
size_t pcm_max_read_len_init = pcm_1s_samples / 100 * pcm_format_size;
size_t pcm_max_read_len = pcm_max_read_len_init;

/* Track the lock state of the single playback mutex within this thread. */
bool single_playback_mutex_locked = false;

Expand Down Expand Up @@ -522,12 +520,9 @@ static void *io_worker_routine(struct io_worker *w) {
error("IO loop poll error: %s", strerror(errno));
goto fail;
case 0:
debug("BT device marked as inactive: %s", w->addr);
pause_retry_pcm_samples = pcm_1s_samples;
pause_retries = 0;
w->active = false;
timeout = -1;
goto close_alsa;
if (!w->ba_pcm.running && ffb_len_out(&buffer) == 0)
goto device_inactive;
break;
}

if (fds[0].revents & POLLIN)
Expand All @@ -539,9 +534,20 @@ static void *io_worker_routine(struct io_worker *w) {
size_t read_samples = 0;
if (fds[1].revents & POLLIN) {

/* If the internal buffer is full then we have an overrun. We must
* discard audio frames in order to continue reading fresh data
* from the server. */
if (ffb_blen_in(&buffer) == 0) {
unsigned int buffered = 0;
ioctl(w->ba_pcm_fd, FIONREAD, &buffered);
const size_t discard_bytes = MIN(buffered, ffb_blen_out(&buffer));
const size_t discard_samples = discard_bytes / pcm_format_size;
warn("Discarding buffered samples: %zu", discard_samples);
ffb_shift(&buffer, discard_samples);
}

ssize_t ret;
size_t _in = MIN(pcm_max_read_len, ffb_blen_in(&buffer));
if ((ret = read(w->ba_pcm_fd, buffer.tail, _in)) == -1) {
if ((ret = read(w->ba_pcm_fd, buffer.tail, ffb_blen_in(&buffer))) == -1) {
if (errno == EINTR)
continue;
error("BlueALSA source PCM read error: %s", strerror(errno));
Expand All @@ -552,6 +558,8 @@ static void *io_worker_routine(struct io_worker *w) {
if (ret % pcm_format_size != 0)
warn("Invalid read from BlueALSA source PCM: %zd %% %zd != 0", ret, pcm_format_size);

ffb_seek(&buffer, read_samples);

}
else if (fds[1].revents & POLLHUP) {
/* source PCM FIFO has been terminated on the writing side */
Expand All @@ -561,9 +569,6 @@ static void *io_worker_routine(struct io_worker *w) {
else if (fds[1].revents)
error("Unexpected BlueALSA source PCM poll event: %#x", fds[1].revents);

if (read_samples == 0)
continue;

/* If current worker is not active and the single playback mode was
* enabled, we have to check if there is any other active worker. */
if (force_single_playback && !w->active) {
Expand Down Expand Up @@ -612,14 +617,17 @@ static void *io_worker_routine(struct io_worker *w) {
if (alsa_pcm_open(&w->alsa_pcm, pcm_device, pcm_format, w->ba_pcm.channels,
w->ba_pcm.rate, pcm_buffer_time, pcm_period_time, 0, &tmp) != 0) {
warn("Couldn't open ALSA playback PCM: %s", tmp);
pcm_max_read_len = pcm_max_read_len_init;
pcm_open_retry_pcm_samples = 0;
pcm_open_retries++;
free(tmp);
continue;
}

pcm_max_read_len = w->alsa_pcm.period_frames * w->alsa_pcm.frame_size;
/* Resize the internal buffer to ensure it is not less than the
* ALSA start threshold. This is to ensure that the PCM re-starts
* quickly after an overrun. */
if (w->alsa_pcm.start_threshold > buffer.nmemb / w->ba_pcm.channels)
ffb_init(&buffer, w->alsa_pcm.start_threshold * w->ba_pcm.channels, buffer.size);

/* Skip mixer setup in case of software volume. */
if (mixer_device != NULL && !w->ba_pcm.soft_volume) {
Expand Down Expand Up @@ -661,9 +669,9 @@ static void *io_worker_routine(struct io_worker *w) {

}

/* mark device as active and set timeout to 500ms */
/* Mark device as active and set timeout to the period time. */
timeout = w->alsa_pcm.period_time / 1000;
w->active = true;
timeout = 500;

/* Current worker was marked as active, so we can safely
* release the single playback mutex if it was locked. */
Expand All @@ -672,15 +680,19 @@ static void *io_worker_routine(struct io_worker *w) {
single_playback_mutex_locked = false;
}

ffb_seek(&buffer, read_samples);
size_t samples = ffb_len_out(&buffer);

if (!w->alsa_mixer.has_mute_switch && pcm_muted)
snd_pcm_format_set_silence(pcm_format, buffer.data, samples);
snd_pcm_format_set_silence(pcm_format, buffer.data, ffb_len_out(&buffer));

if (alsa_pcm_write(&w->alsa_pcm, &buffer) < 0)
if (alsa_pcm_write(&w->alsa_pcm, &buffer, !w->ba_pcm.running, verbose) < 0)
goto close_alsa;

if (!w->ba_pcm.running)
goto device_inactive;

if (w->alsa_pcm.underrun)
/* Reset moving delay window buffer. */
delay_report_reset(&dr);

if (!delay_report_update(&dr, &w->alsa_pcm, w->ba_pcm_fd, &buffer, &err)) {
error("Couldn't update BlueALSA PCM client delay: %s", err.message);
dbus_error_free(&err);
Expand All @@ -689,9 +701,15 @@ static void *io_worker_routine(struct io_worker *w) {

continue;

device_inactive:
debug("BT device marked as inactive: %s", w->addr);
pause_retry_pcm_samples = pcm_1s_samples;
pause_retries = 0;
w->active = false;
timeout = -1;

close_alsa:
ffb_rewind(&buffer);
pcm_max_read_len = pcm_max_read_len_init;
alsa_pcm_close(&w->alsa_pcm);
alsa_mixer_close(&w->alsa_mixer);
}
Expand Down Expand Up @@ -741,11 +759,13 @@ static struct io_worker *supervise_io_worker_start(const struct ba_pcm *ba_pcm)
if (strcmp(workers[i].ba_pcm.pcm_path, ba_pcm->pcm_path) == 0) {
/* If the codec has changed after the device connected, then the
* audio format may have changed. If it has, the worker thread
* needs to be restarted. */
* needs to be restarted. Otherwise, update the running state. */
if (!pcm_hw_params_equal(&workers[i].ba_pcm, ba_pcm))
io_worker_stop(i);
else
else {
workers[i].ba_pcm.running = ba_pcm->running;
return &workers[i];
}
}

pthread_rwlock_wrlock(&workers_lock);
Expand Down
8 changes: 1 addition & 7 deletions utils/aplay/delay-report.c
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,6 @@ bool delay_report_update(
ffb_t *buffer,
DBusError *err) {

int ret;
snd_pcm_sframes_t alsa_delay_frames = 0;
/* Get the delay reported by the ALSA driver. */
if ((ret = alsa_pcm_delay(pcm, &alsa_delay_frames)) != 0)
warn("Couldn't get PCM delay: %s", snd_strerror(ret));

unsigned int ba_pcm_buffered = 0;
/* Get the delay due to BlueALSA PCM FIFO buffering. */
ioctl(ba_pcm_fd, FIONREAD, &ba_pcm_buffered);
Expand All @@ -65,7 +59,7 @@ bool delay_report_update(

const size_t num_values = ARRAYSIZE(dr->values);
/* Store the delay calculated from all components. */
dr->values[dr->values_i % num_values] = alsa_delay_frames + ba_pcm_frames + buffer_frames;
dr->values[dr->values_i % num_values] = pcm->delay + ba_pcm_frames + buffer_frames;
dr->values_i++;

struct timespec ts_now;
Expand Down

0 comments on commit 82bbbed

Please sign in to comment.