/* * BlueALSA - alsa-pcm.c * SPDX-FileCopyrightText: 2016-2025 BlueALSA developers * SPDX-License-Identifier: MIT */ #include "alsa-pcm.h" #include #include #include #include #include #include "shared/log.h" static int alsa_pcm_set_hw_params( struct alsa_pcm *pcm, snd_pcm_format_t format_1, snd_pcm_format_t format_2, snd_pcm_format_t *format, unsigned int channels, unsigned int *rate, bool exact_rate, unsigned int *buffer_time, unsigned int *period_time, char **msg) { const snd_pcm_access_t access = SND_PCM_ACCESS_RW_INTERLEAVED; snd_pcm_t *snd_pcm = pcm->pcm; snd_pcm_hw_params_t *params; char buf[256]; int dir; int err; snd_pcm_hw_params_alloca(¶ms); *format = SND_PCM_FORMAT_UNKNOWN; if ((err = snd_pcm_hw_params_any(snd_pcm, params)) < 0) { snprintf(buf, sizeof(buf), "Set all possible ranges: %s", snd_strerror(err)); goto fail; } if ((err = snd_pcm_hw_params_set_access(snd_pcm, params, access)) != 0) { snprintf(buf, sizeof(buf), "Set assess type: %s: %s", snd_strerror(err), snd_pcm_access_name(access)); goto fail; } /* Prefer the first format if it is supported by the device. */ if ((err = snd_pcm_hw_params_set_format(snd_pcm, params, format_1)) == 0) *format = format_1; /* Otherwise try second format. */ else if (format_2 != SND_PCM_FORMAT_UNKNOWN) { if ((err = snd_pcm_hw_params_set_format(snd_pcm, params, format_2)) == 0) *format = format_2; else { snprintf(buf, sizeof(buf), "Set format: %s: %s and %s", snd_strerror(err), snd_pcm_format_name(format_1), snd_pcm_format_name(format_2)); goto fail; } } else { snprintf(buf, sizeof(buf), "Set format: %s: %s", snd_strerror(err), snd_pcm_format_name(format_1)); goto fail; } if ((err = snd_pcm_hw_params_set_channels(snd_pcm, params, channels)) != 0) { snprintf(buf, sizeof(buf), "Set channels: %s: %d", snd_strerror(err), channels); goto fail; } dir = 0; if (exact_rate) err = snd_pcm_hw_params_set_rate(snd_pcm, params, *rate, dir); else err = snd_pcm_hw_params_set_rate_near(snd_pcm, params, rate, &dir); if (err != 0) { snprintf(buf, sizeof(buf), "Set sample rate: %s: %u", snd_strerror(err), *rate); goto fail; } dir = 0; if ((err = snd_pcm_hw_params_set_period_time_near(snd_pcm, params, period_time, &dir)) != 0) { snprintf(buf, sizeof(buf), "Set period time: %s: %u", snd_strerror(err), *period_time); goto fail; } dir = 0; if ((err = snd_pcm_hw_params_set_buffer_time_near(snd_pcm, params, buffer_time, &dir)) != 0) { snprintf(buf, sizeof(buf), "Set buffer time: %s: %u", snd_strerror(err), *buffer_time); goto fail; } if ((err = snd_pcm_hw_params(snd_pcm, params)) != 0) { snprintf(buf, sizeof(buf), "%s", snd_strerror(err)); goto fail; } return 0; fail: if (msg != NULL) *msg = strdup(buf); return err; } static int alsa_pcm_set_sw_params( struct alsa_pcm *pcm, snd_pcm_uframes_t start_threshold, char **msg) { snd_pcm_t *snd_pcm = pcm->pcm; snd_pcm_sw_params_t *params; char buf[256]; int err; snd_pcm_sw_params_alloca(¶ms); if ((err = snd_pcm_sw_params_current(snd_pcm, params)) != 0) { snprintf(buf, sizeof(buf), "Get current params: %s", snd_strerror(err)); goto fail; } if ((err = snd_pcm_sw_params_set_start_threshold(snd_pcm, params, start_threshold)) != 0) { snprintf(buf, sizeof(buf), "Set start threshold: %s: %lu", snd_strerror(err), start_threshold); goto fail; } if ((err = snd_pcm_sw_params(snd_pcm, params)) != 0) { snprintf(buf, sizeof(buf), "%s", snd_strerror(err)); goto fail; } return 0; fail: if (msg != NULL) *msg = strdup(buf); return err; } void alsa_pcm_init(struct alsa_pcm *pcm) { memset(pcm, 0, sizeof(*pcm)); } int alsa_pcm_open( struct alsa_pcm *pcm, const char *name, snd_pcm_format_t format_1, snd_pcm_format_t format_2, unsigned int channels, unsigned int rate, unsigned int buffer_time, unsigned int period_time, int flags, char **msg) { char *tmp = NULL; char buf[256]; unsigned int actual_buffer_time = buffer_time; unsigned int actual_period_time = period_time; unsigned int actual_rate = rate; const bool exact_rate = !(flags & SND_PCM_NO_AUTO_RESAMPLE); int err; if ((err = snd_pcm_open(&pcm->pcm, name, SND_PCM_STREAM_PLAYBACK, flags)) != 0) { snprintf(buf, sizeof(buf), "Open PCM: %s", snd_strerror(err)); goto fail; } if ((err = alsa_pcm_set_hw_params(pcm, format_1, format_2, &pcm->format, channels, &actual_rate, exact_rate, &actual_buffer_time, &actual_period_time, &tmp)) != 0) { snprintf(buf, sizeof(buf), "Set HW params: %s", tmp); goto fail; } snd_pcm_uframes_t buffer_size, period_size; if ((err = snd_pcm_get_params(pcm->pcm, &buffer_size, &period_size)) != 0) { snprintf(buf, sizeof(buf), "Get params: %s", snd_strerror(err)); goto fail; } /* Start the transfer when three requested periods have been written (or * when the buffer is full if it holds less than three requested periods. */ snd_pcm_uframes_t start_threshold = ((snd_pcm_uframes_t)period_time * 3 / 1000) * (rate / 1000); if (start_threshold > buffer_size) start_threshold = buffer_size; if ((err = alsa_pcm_set_sw_params(pcm, start_threshold, &tmp)) != 0) { snprintf(buf, sizeof(buf), "Set SW params: %s", tmp); goto fail; } if ((err = snd_pcm_prepare(pcm->pcm)) != 0) { snprintf(buf, sizeof(buf), "Prepare: %s", snd_strerror(err)); goto fail; } pcm->channels = channels; pcm->sample_size = snd_pcm_format_size(pcm->format, 1); pcm->frame_size = snd_pcm_format_size(pcm->format, channels); pcm->rate = actual_rate; pcm->buffer_time = actual_buffer_time; pcm->period_time = actual_period_time; pcm->buffer_frames = buffer_size; pcm->period_frames = period_size; pcm->start_threshold = start_threshold; pcm->delay = 0; pcm->hw_avail = 0; return 0; fail: alsa_pcm_close(pcm); if (msg != NULL) *msg = strdup(buf); free(tmp); return err; } void alsa_pcm_close(struct alsa_pcm *pcm) { if (pcm->pcm != NULL) snd_pcm_close(pcm->pcm); pcm->pcm = NULL; } int alsa_pcm_write( struct alsa_pcm *pcm, ffb_t *buffer, bool drain) { snd_pcm_sframes_t avail = 0; snd_pcm_sframes_t delay = 0; snd_pcm_sframes_t ret; pcm->underrun = false; if ((ret = snd_pcm_avail_delay(pcm->pcm, &avail, &delay)) < 0) { if (ret == -EPIPE) { warn("ALSA playback PCM underrun"); snd_pcm_prepare(pcm->pcm); pcm->underrun = true; avail = pcm->buffer_frames; delay = 0; } else { error("ALSA playback PCM error: %s", snd_strerror(ret)); return -1; } } snd_pcm_sframes_t frames = ffb_len_out(buffer) / pcm->channels; snd_pcm_uframes_t hw_avail = pcm->buffer_frames - avail; snd_pcm_uframes_t written_frames = 0; if (!drain) { if (frames + hw_avail < pcm->period_frames && snd_pcm_state(pcm->pcm) == SND_PCM_STATE_RUNNING) { /* When the stream runs dry we drain the ALSA buffer and leave the * ALSA device stopped until fresh frames arrive from the server. */ warn("Draining ALSA playback PCM to avoid underrun"); if (frames > 0) { /* Ignore any error reported by the write. */ snd_pcm_writei(pcm->pcm, buffer->data, frames); ffb_rewind(buffer); } snd_pcm_drain(pcm->pcm); if ((ret = snd_pcm_prepare(pcm->pcm)) < 0) { error("ALSA playback PCM error: %s", snd_strerror(ret)); return -1; } /* Flag an underrun to indicate that there has been a discontinuity * in the input stream. */ pcm->underrun = true; pcm->hw_avail = 0; pcm->delay = 0; return 0; } if (frames > avail) { /* Write only as many frames as possible without blocking. */ frames = avail; } } while (frames > 0) { ret = snd_pcm_writei(pcm->pcm, buffer->data, frames); if (ret < 0) switch (-ret) { case EINTR: continue; case EPIPE: warn("ALSA playback PCM underrun"); snd_pcm_prepare(pcm->pcm); pcm->underrun = true; continue; default: error("ALSA playback PCM write error: %s", snd_strerror(ret)); return -1; } else { written_frames += ret; frames -= ret; } } if (drain) { snd_pcm_drain(pcm->pcm); pcm->hw_avail = 0; pcm->delay = 0; ffb_rewind(buffer); return 0; } pcm->hw_avail = hw_avail + written_frames; 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; } void alsa_pcm_dump(const struct alsa_pcm *pcm, FILE *fp) { snd_output_t *out; snd_output_stdio_attach(&out, fp, 0); snd_pcm_dump(pcm->pcm, out); snd_output_close(out); }