diff options
Diffstat (limited to 'audio/out/ao_alsa.c')
-rw-r--r-- | audio/out/ao_alsa.c | 419 |
1 files changed, 163 insertions, 256 deletions
diff --git a/audio/out/ao_alsa.c b/audio/out/ao_alsa.c index df78a67178..92ea0db237 100644 --- a/audio/out/ao_alsa.c +++ b/audio/out/ao_alsa.c @@ -34,7 +34,6 @@ #include <math.h> #include <string.h> -#include "config.h" #include "options/options.h" #include "options/m_config.h" #include "options/m_option.h" @@ -57,9 +56,9 @@ struct ao_alsa_opts { char *mixer_device; char *mixer_name; int mixer_index; - int resample; - int ni; - int ignore_chmap; + bool resample; + bool ni; + bool ignore_chmap; int buffer_time; int frags; }; @@ -67,21 +66,19 @@ struct ao_alsa_opts { #define OPT_BASE_STRUCT struct ao_alsa_opts static const struct m_sub_options ao_alsa_conf = { .opts = (const struct m_option[]) { - OPT_FLAG("alsa-resample", resample, 0), - OPT_STRING("alsa-mixer-device", mixer_device, 0), - OPT_STRING("alsa-mixer-name", mixer_name, 0), - OPT_INTRANGE("alsa-mixer-index", mixer_index, 0, 0, 99), - OPT_FLAG("alsa-non-interleaved", ni, 0), - OPT_FLAG("alsa-ignore-chmap", ignore_chmap, 0), - OPT_INTRANGE("alsa-buffer-time", buffer_time, 0, 0, INT_MAX), - OPT_INTRANGE("alsa-periods", frags, 0, 0, INT_MAX), + {"alsa-resample", OPT_BOOL(resample)}, + {"alsa-mixer-device", OPT_STRING(mixer_device)}, + {"alsa-mixer-name", OPT_STRING(mixer_name)}, + {"alsa-mixer-index", OPT_INT(mixer_index), M_RANGE(0, 99)}, + {"alsa-non-interleaved", OPT_BOOL(ni)}, + {"alsa-ignore-chmap", OPT_BOOL(ignore_chmap)}, + {"alsa-buffer-time", OPT_INT(buffer_time), M_RANGE(0, INT_MAX)}, + {"alsa-periods", OPT_INT(frags), M_RANGE(0, INT_MAX)}, {0} }, .defaults = &(const struct ao_alsa_opts) { .mixer_device = "default", .mixer_name = "Master", - .mixer_index = 0, - .ni = 0, .buffer_time = 100000, .frags = 4, }, @@ -93,9 +90,6 @@ struct priv { bool device_lost; snd_pcm_format_t alsa_fmt; bool can_pause; - bool paused; - snd_pcm_sframes_t prepause_frames; - double delay_before_pause; snd_pcm_uframes_t buffersize; snd_pcm_uframes_t outburst; @@ -120,21 +114,6 @@ struct priv { MP_WARN(ao, "%s: %s\n", (message), snd_strerror(err)); \ } while (0) -// Common code for handling ENODEV, which happens if a device gets "lost", and -// can't be used anymore. Returns true if alsa_err is not ENODEV. -static bool check_device_present(struct ao *ao, int alsa_err) -{ - struct priv *p = ao->priv; - if (alsa_err != -ENODEV) - return true; - if (!p->device_lost) { - MP_WARN(ao, "Device lost, trying to recover...\n"); - ao_request_reload(ao); - p->device_lost = true; - } - return false; -} - static int control(struct ao *ao, enum aocontrol cmd, void *arg) { struct priv *p = ao->priv; @@ -186,15 +165,13 @@ static int control(struct ao *ao, enum aocontrol cmd, void *arg) switch (cmd) { case AOCONTROL_SET_VOLUME: { - ao_control_vol_t *vol = arg; - set_vol = vol->left / f_multi + pmin + 0.5; + float *vol = arg; + set_vol = *vol / f_multi + pmin + 0.5; err = snd_mixer_selem_set_playback_volume(elem, 0, set_vol); CHECK_ALSA_ERROR("Error setting left channel"); MP_DBG(ao, "left=%li, ", set_vol); - set_vol = vol->right / f_multi + pmin + 0.5; - err = snd_mixer_selem_set_playback_volume(elem, 1, set_vol); CHECK_ALSA_ERROR("Error setting right channel"); MP_DBG(ao, "right=%li, pmin=%li, pmax=%li, mult=%f\n", @@ -202,12 +179,14 @@ static int control(struct ao *ao, enum aocontrol cmd, void *arg) break; } case AOCONTROL_GET_VOLUME: { - ao_control_vol_t *vol = arg; + float *vol = arg; + float left, right; snd_mixer_selem_get_playback_volume(elem, 0, &get_vol); - vol->left = (get_vol - pmin) * f_multi; + left = (get_vol - pmin) * f_multi; snd_mixer_selem_get_playback_volume(elem, 1, &get_vol); - vol->right = (get_vol - pmin) * f_multi; - MP_DBG(ao, "left=%f, right=%f\n", vol->left, vol->right); + right = (get_vol - pmin) * f_multi; + *vol = (left + right) / 2.0; + MP_DBG(ao, "vol=%f\n", *vol); break; } case AOCONTROL_SET_MUTE: { @@ -581,7 +560,7 @@ static char *append_params(void *ta_parent, const char *device, const char *p) /* a simple list of parameters: add it at the end of the list */ return talloc_asprintf(ta_parent, "%s,%s", device, p); } - abort(); + MP_ASSERT_UNREACHABLE(); } static int try_open_device(struct ao *ao, const char *device, int mode) @@ -644,7 +623,8 @@ static void uninit(struct ao *ao) CHECK_ALSA_ERROR("pcm close error"); } -alsa_error: ; +alsa_error: + snd_config_update_free_global(); } #define INIT_DEVICE_ERR_GENERIC -1 @@ -851,9 +831,8 @@ static int init_device(struct ao *ao, int mode) err = snd_pcm_sw_params_get_boundary(alsa_swparams, &boundary); CHECK_ALSA_ERROR("Unable to get boundary"); - /* start playing when one period has been written */ - err = snd_pcm_sw_params_set_start_threshold - (p->alsa, alsa_swparams, p->outburst); + // Manual trigger; INT_MAX as suggested by ALSA doxygen (they call it MAXINT). + err = snd_pcm_sw_params_set_start_threshold(p->alsa, alsa_swparams, INT_MAX); CHECK_ALSA_ERROR("Unable to set start threshold"); /* play silence when there is an underrun */ @@ -869,10 +848,12 @@ static int init_device(struct ao *ao, int mode) MP_VERBOSE(ao, "period size: %d samples\n", (int)p->outburst); ao->device_buffer = p->buffersize; - ao->period_size = p->outburst; p->convert.channels = ao->channels.num; + err = snd_pcm_prepare(p->alsa); + CHECK_ALSA_ERROR("pcm prepare error"); + return 0; alsa_error: @@ -930,264 +911,194 @@ static int init(struct ao *ao) return r; } -static void drain(struct ao *ao) +// Function for dealing with playback state. This attempts to recover the ALSA +// state (bring it into SND_PCM_STATE_{PREPARED,RUNNING,PAUSED,UNDERRUN}). If +// state!=NULL, fill it after recovery is attempted. +// Returns true if PCM is in one the expected states. +static bool recover_and_get_state(struct ao *ao, struct mp_pcm_state *state) { struct priv *p = ao->priv; - snd_pcm_drain(p->alsa); -} + int err; -static int get_space(struct ao *ao) -{ - struct priv *p = ao->priv; + snd_pcm_status_t *st; + snd_pcm_status_alloca(&st); + + bool state_ok = false; + snd_pcm_state_t pcmst = SND_PCM_STATE_DISCONNECTED; + + // Give it a number of chances to recover. This tries to deal with the fact + // that the API is asynchronous, and to account for some past cargo-cult + // (where things were retried in a loop). + for (int n = 0; n < 10; n++) { + err = snd_pcm_status(p->alsa, st); + if (err == -EPIPE) { + // ALSA APIs can return -EPIPE when an XRUN happens, + // we skip right to handling it by setting pcmst + // manually. + pcmst = SND_PCM_STATE_XRUN; + } else { + // Otherwise do error checking and query the PCM state properly. + CHECK_ALSA_ERROR("snd_pcm_status"); - // in case of pausing or the device still being configured, - // just return our buffer size. - if (p->paused || snd_pcm_state(p->alsa) == SND_PCM_STATE_SETUP) - return p->buffersize; - - snd_pcm_sframes_t space = snd_pcm_avail(p->alsa); - if (space < 0) { - if (space == -EPIPE) { - MP_WARN(ao, "ALSA XRUN hit, attempting to recover...\n"); - int err = snd_pcm_prepare(p->alsa); - CHECK_ALSA_ERROR("Unable to recover from under/overrun!"); - return p->buffersize; + pcmst = snd_pcm_status_get_state(st); } - MP_ERR(ao, "Error received from snd_pcm_avail " - "(%ld, %s with ALSA state %s)!\n", - space, snd_strerror(space), - snd_pcm_state_name(snd_pcm_state(p->alsa))); - - // request a reload of the AO if device is not present, - // then error out. - check_device_present(ao, space); - goto alsa_error; - } - - if (space > p->buffersize) // Buffer underrun? - space = p->buffersize; - return space / p->outburst * p->outburst; - -alsa_error: - return 0; -} - -/* delay in seconds between first and last sample in buffer */ -static double get_delay(struct ao *ao) -{ - struct priv *p = ao->priv; - snd_pcm_sframes_t delay; - - if (p->paused) - return p->delay_before_pause; - - if (snd_pcm_delay(p->alsa, &delay) < 0) - return 0; + if (pcmst == SND_PCM_STATE_PREPARED || + pcmst == SND_PCM_STATE_RUNNING || + pcmst == SND_PCM_STATE_PAUSED) + { + state_ok = true; + break; + } - if (delay < 0) { - /* underrun - move the application pointer forward to catch up */ - snd_pcm_forward(p->alsa, -delay); - delay = 0; - } - return delay / (double)ao->samplerate; -} + MP_VERBOSE(ao, "attempt %d to recover from state '%s'...\n", + n + 1, snd_pcm_state_name(pcmst)); -// For stream-silence mode: replace remaining buffer with silence. -// Tries to cause an instant buffer underrun. -static void soft_reset(struct ao *ao) -{ - struct priv *p = ao->priv; - snd_pcm_sframes_t frames = snd_pcm_rewindable(p->alsa); - if (frames > 0 && snd_pcm_state(p->alsa) == SND_PCM_STATE_RUNNING) { - frames = snd_pcm_rewind(p->alsa, frames); - if (frames < 0) { - int err = frames; - CHECK_ALSA_WARN("pcm rewind error"); + switch (pcmst) { + // Underrun; recover. (We never use draining.) + case SND_PCM_STATE_XRUN: + case SND_PCM_STATE_DRAINING: + err = snd_pcm_prepare(p->alsa); + CHECK_ALSA_ERROR("pcm prepare error"); + continue; + // Hardware suspend. + case SND_PCM_STATE_SUSPENDED: + MP_INFO(ao, "PCM in suspend mode, trying to resume.\n"); + err = snd_pcm_resume(p->alsa); + if (err == -EAGAIN) { + // Cargo-cult from decades ago, with a cargo cult timeout. + MP_INFO(ao, "PCM resume EAGAIN - retrying.\n"); + sleep(1); + continue; + } + if (err == -ENOSYS) { + // As suggested by ALSA doxygen. + MP_VERBOSE(ao, "ENOSYS, retrying with snd_pcm_prepare().\n"); + err = snd_pcm_prepare(p->alsa); + } + if (err < 0) + MP_ERR(ao, "resuming from SUSPENDED: %s\n", snd_strerror(err)); + continue; + // Device lost. OPEN/SETUP are states we never enter after init, so + // treat them like DISCONNECTED. + case SND_PCM_STATE_DISCONNECTED: + case SND_PCM_STATE_OPEN: + case SND_PCM_STATE_SETUP: + default: + if (!p->device_lost) { + MP_WARN(ao, "Device lost, trying to recover...\n"); + ao_request_reload(ao); + p->device_lost = true; + } + goto alsa_error; } } -} - -static void audio_pause(struct ao *ao) -{ - struct priv *p = ao->priv; - int err; - if (p->paused) - return; + if (!state_ok) { + MP_ERR(ao, "could not recover\n"); + } - p->delay_before_pause = get_delay(ao); - p->prepause_frames = p->delay_before_pause * ao->samplerate; +alsa_error: - if (ao->stream_silence) { - soft_reset(ao); - } else if (p->can_pause) { - if (snd_pcm_state(p->alsa) == SND_PCM_STATE_RUNNING) { - err = snd_pcm_pause(p->alsa, 1); - CHECK_ALSA_ERROR("pcm pause error"); - p->prepause_frames = 0; - } - } else { - err = snd_pcm_drop(p->alsa); - CHECK_ALSA_ERROR("pcm drop error"); + if (state) { + snd_pcm_sframes_t del = state_ok ? snd_pcm_status_get_delay(st) : 0; + state->delay = MPMAX(del, 0) / (double)ao->samplerate; + state->free_samples = state_ok ? snd_pcm_status_get_avail(st) : 0; + state->free_samples = MPCLAMP(state->free_samples, 0, ao->device_buffer); + // Align to period size. + state->free_samples = state->free_samples / p->outburst * p->outburst; + state->queued_samples = ao->device_buffer - state->free_samples; + state->playing = pcmst == SND_PCM_STATE_RUNNING || + pcmst == SND_PCM_STATE_PAUSED; } - p->paused = true; - -alsa_error: ; + return state_ok; } -static void resume_device(struct ao *ao) +static void audio_get_state(struct ao *ao, struct mp_pcm_state *state) { - struct priv *p = ao->priv; - int err; - - if (snd_pcm_state(p->alsa) == SND_PCM_STATE_SUSPENDED) { - MP_INFO(ao, "PCM in suspend mode, trying to resume.\n"); - - while ((err = snd_pcm_resume(p->alsa)) == -EAGAIN) - sleep(1); - } + recover_and_get_state(ao, state); } -static void audio_resume(struct ao *ao) +static void audio_start(struct ao *ao) { struct priv *p = ao->priv; int err; - if (!p->paused) - return; - - resume_device(ao); - - if (ao->stream_silence) { - p->paused = false; - get_delay(ao); // recovers from underrun (as a side-effect) - } else if (p->can_pause) { - if (snd_pcm_state(p->alsa) == SND_PCM_STATE_PAUSED) { - err = snd_pcm_pause(p->alsa, 0); - CHECK_ALSA_ERROR("pcm resume error"); - } - } else { - MP_VERBOSE(ao, "resume not supported by hardware\n"); - err = snd_pcm_prepare(p->alsa); - CHECK_ALSA_ERROR("pcm prepare error"); - } + recover_and_get_state(ao, NULL); - if (p->prepause_frames) - ao_play_silence(ao, p->prepause_frames); + err = snd_pcm_start(p->alsa); + CHECK_ALSA_ERROR("pcm start error"); alsa_error: ; - p->paused = false; } -static void reset(struct ao *ao) +static void audio_reset(struct ao *ao) { struct priv *p = ao->priv; int err; - p->paused = false; - p->prepause_frames = 0; - p->delay_before_pause = 0; + err = snd_pcm_drop(p->alsa); + CHECK_ALSA_ERROR("pcm drop error"); + err = snd_pcm_prepare(p->alsa); + CHECK_ALSA_ERROR("pcm prepare error"); - if (ao->stream_silence) { - soft_reset(ao); - } else { - err = snd_pcm_drop(p->alsa); - CHECK_ALSA_ERROR("pcm prepare error"); - err = snd_pcm_prepare(p->alsa); - CHECK_ALSA_ERROR("pcm prepare error"); - } + recover_and_get_state(ao, NULL); alsa_error: ; } -static int play(struct ao *ao, void **data, int samples, int flags) +static bool audio_set_paused(struct ao *ao, bool paused) { struct priv *p = ao->priv; - snd_pcm_sframes_t res = 0; - if (!(flags & AOPLAY_FINAL_CHUNK)) - samples = samples / p->outburst * p->outburst; - - if (samples == 0) - return 0; - ao_convert_inplace(&p->convert, data, samples); + int err; - do { - if (af_fmt_is_planar(ao->format)) { - res = snd_pcm_writen(p->alsa, data, samples); - } else { - res = snd_pcm_writei(p->alsa, data[0], samples); - } + recover_and_get_state(ao, NULL); - if (res == -EINTR || res == -EAGAIN) { /* retry */ - res = 0; - } else if (!check_device_present(ao, res)) { - goto alsa_error; - } else if (res < 0) { - if (res == -ESTRPIPE) { /* suspend */ - resume_device(ao); - } else if (res == -EPIPE) { - // For some reason, writing a smaller fragment at the end - // immediately underruns. - if (!(flags & AOPLAY_FINAL_CHUNK)) - MP_WARN(ao, "Device underrun detected.\n"); - } else { - MP_ERR(ao, "Write error: %s\n", snd_strerror(res)); - } - res = snd_pcm_prepare(p->alsa); - int err = res; - CHECK_ALSA_ERROR("pcm prepare error"); - res = 0; - } - } while (res == 0); + if (!p->can_pause) + return false; - p->paused = false; + snd_pcm_state_t pcmst = snd_pcm_state(p->alsa); + if (pcmst == SND_PCM_STATE_RUNNING && paused) { + err = snd_pcm_pause(p->alsa, 1); + CHECK_ALSA_ERROR("pcm pause error"); + } else if (pcmst == SND_PCM_STATE_PAUSED && !paused) { + err = snd_pcm_pause(p->alsa, 0); + CHECK_ALSA_ERROR("pcm resume error"); + } - return res < 0 ? -1 : res; + return true; alsa_error: - return -1; + return false; } -#define MAX_POLL_FDS 20 -static int audio_wait(struct ao *ao, pthread_mutex_t *lock) +static bool audio_write(struct ao *ao, void **data, int samples) { struct priv *p = ao->priv; - int err; - int num_fds = snd_pcm_poll_descriptors_count(p->alsa); - if (num_fds <= 0 || num_fds >= MAX_POLL_FDS) - goto alsa_error; - - struct pollfd fds[MAX_POLL_FDS]; - err = snd_pcm_poll_descriptors(p->alsa, fds, num_fds); - CHECK_ALSA_ERROR("cannot get pollfds"); - - while (1) { - int r = ao_wait_poll(ao, fds, num_fds, lock); - if (r) - return r; + ao_convert_inplace(&p->convert, data, samples); - unsigned short revents; - err = snd_pcm_poll_descriptors_revents(p->alsa, fds, num_fds, &revents); - CHECK_ALSA_ERROR("cannot read poll events"); + if (!recover_and_get_state(ao, NULL)) + return false; - if (revents & POLLERR) { - snd_pcm_status_t *status; - snd_pcm_status_alloca(&status); + snd_pcm_sframes_t err = 0; + if (af_fmt_is_planar(ao->format)) { + err = snd_pcm_writen(p->alsa, data, samples); + } else { + err = snd_pcm_writei(p->alsa, data[0], samples); + } - err = snd_pcm_status(p->alsa, status); - check_device_present(ao, err); - return -1; - } - if (revents & POLLOUT) - return 0; + CHECK_ALSA_ERROR("pcm write error"); + if (err >= 0 && err != samples) { + MP_ERR(ao, "unexpected partial write (%d of %d frames), dropping audio\n", + (int)err, samples); } - return 0; + + return true; alsa_error: - return -1; + return false; } static bool is_useless_device(char *name) @@ -1240,15 +1151,11 @@ const struct ao_driver audio_out_alsa = { .init = init, .uninit = uninit, .control = control, - .get_space = get_space, - .play = play, - .get_delay = get_delay, - .pause = audio_pause, - .resume = audio_resume, - .reset = reset, - .drain = drain, - .wait = audio_wait, - .wakeup = ao_wakeup_poll, + .get_state = audio_get_state, + .write = audio_write, + .start = audio_start, + .set_pause = audio_set_paused, + .reset = audio_reset, .list_devs = list_devs, .priv_size = sizeof(struct priv), .global_opts = &ao_alsa_conf, |