summaryrefslogtreecommitdiffstats
path: root/audio/out/ao_alsa.c
diff options
context:
space:
mode:
Diffstat (limited to 'audio/out/ao_alsa.c')
-rw-r--r--audio/out/ao_alsa.c426
1 files changed, 159 insertions, 267 deletions
diff --git a/audio/out/ao_alsa.c b/audio/out/ao_alsa.c
index b4fa18891b..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,12 +66,12 @@ 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[]) {
- {"alsa-resample", OPT_FLAG(resample)},
+ {"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_FLAG(ni)},
- {"alsa-ignore-chmap", OPT_FLAG(ignore_chmap)},
+ {"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}
@@ -80,8 +79,6 @@ static const struct m_sub_options ao_alsa_conf = {
.defaults = &(const struct ao_alsa_opts) {
.mixer_device = "default",
.mixer_name = "Master",
- .mixer_index = 0,
- .ni = 0,
.buffer_time = 100000,
.frags = 4,
},
@@ -93,10 +90,6 @@ struct priv {
bool device_lost;
snd_pcm_format_t alsa_fmt;
bool can_pause;
- bool paused;
- bool final_chunk_written;
- snd_pcm_sframes_t prepause_frames;
- double delay_before_pause;
snd_pcm_uframes_t buffersize;
snd_pcm_uframes_t outburst;
@@ -121,34 +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 void handle_underrun(struct ao *ao)
-{
- struct priv *p = ao->priv;
-
- if (!p->final_chunk_written) {
- ao_underrun_event(ao);
-
- int err = snd_pcm_prepare(p->alsa);
- CHECK_ALSA_ERROR("pcm prepare error");
- alsa_error: ;
- }
-}
-
static int control(struct ao *ao, enum aocontrol cmd, void *arg)
{
struct priv *p = ao->priv;
@@ -200,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",
@@ -216,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: {
@@ -595,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)
@@ -658,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
@@ -865,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 */
@@ -883,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:
@@ -944,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);
-}
-
-static int get_space(struct ao *ao)
-{
- struct priv *p = ao->priv;
-
- // 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 && space != -EPIPE) {
- 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 == -EPIPE)
- handle_underrun(ao);
-
- if (space > p->buffersize || space < 0) // Buffer underrun?
- space = p->buffersize;
- return space / p->outburst * p->outburst;
+ int err;
-alsa_error:
- return 0;
-}
+ 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");
-/* 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;
+ pcmst = snd_pcm_status_get_state(st);
+ }
- if (p->paused)
- return p->delay_before_pause;
+ if (pcmst == SND_PCM_STATE_PREPARED ||
+ pcmst == SND_PCM_STATE_RUNNING ||
+ pcmst == SND_PCM_STATE_PAUSED)
+ {
+ state_ok = true;
+ break;
+ }
- int err = snd_pcm_delay(p->alsa, &delay);
- if (err < 0) {
- if (err == -EPIPE)
- handle_underrun(ao);
- return 0;
- }
+ MP_VERBOSE(ao, "attempt %d to recover from state '%s'...\n",
+ n + 1, snd_pcm_state_name(pcmst));
- 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;
-}
-
-// 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;
+ recover_and_get_state(ao, NULL);
- 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");
- }
-
- 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;
- p->final_chunk_written = false;
+ 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;
- bool final_chunk = flags & AOPLAY_FINAL_CHUNK;
-
- if (!final_chunk)
- samples = samples / p->outburst * p->outburst;
+ int err;
- if (samples == 0)
- goto done;
- ao_convert_inplace(&p->convert, data, samples);
+ recover_and_get_state(ao, NULL);
- 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);
- }
-
- 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) {
- handle_underrun(ao);
- } else {
- MP_ERR(ao, "Write error: %s\n", snd_strerror(res));
- }
- int err = snd_pcm_prepare(p->alsa);
- 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");
+ }
-done:
- p->final_chunk_written = res == samples && final_chunk;
- 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)
@@ -1254,17 +1151,12 @@ 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,
- .reports_underruns = true,
.priv_size = sizeof(struct priv),
.global_opts = &ao_alsa_conf,
};