diff --git a/utils/aplay/alsa-pcm.c b/utils/aplay/alsa-pcm.c index 527fc1dc8..c80f1f115 100644 --- a/utils/aplay/alsa-pcm.c +++ b/utils/aplay/alsa-pcm.c @@ -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; @@ -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( @@ -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; @@ -196,7 +199,6 @@ int alsa_pcm_open( *msg = strdup(buf); free(tmp); return err; - } void alsa_pcm_close(struct alsa_pcm *pcm) { @@ -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; } diff --git a/utils/aplay/alsa-pcm.h b/utils/aplay/alsa-pcm.h index 0231fbeaf..5b4617eec 100644 --- a/utils/aplay/alsa-pcm.h +++ b/utils/aplay/alsa-pcm.h @@ -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. */ @@ -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) { @@ -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, diff --git a/utils/aplay/aplay.c b/utils/aplay/aplay.c index 7c071c861..fa3cd0f22 100644 --- a/utils/aplay/aplay.c +++ b/utils/aplay/aplay.c @@ -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; } @@ -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; @@ -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) @@ -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("Dropping PCM frames: %zu", discard_samples / w->ba_pcm.channels); + 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)); @@ -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 */ @@ -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) { @@ -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) { @@ -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. */ @@ -672,15 +680,15 @@ 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 (!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); @@ -689,9 +697,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); } @@ -741,11 +755,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); diff --git a/utils/aplay/delay-report.c b/utils/aplay/delay-report.c index 24dadd5b3..7266690e9 100644 --- a/utils/aplay/delay-report.c +++ b/utils/aplay/delay-report.c @@ -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); @@ -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;