summaryrefslogtreecommitdiffstats
path: root/audio/out/ao_alsa.c
diff options
context:
space:
mode:
authorwm4 <wm4@nowhere>2020-05-31 15:00:35 +0200
committerwm4 <wm4@nowhere>2020-06-01 01:08:16 +0200
commitd27ad9654218463694093697e3d09f8983b4ccf3 (patch)
tree54d889bbed1b32b2a702fee6825c66ff6fb60c33 /audio/out/ao_alsa.c
parentd448dd5bf26408ccced1bd6df8f9eaf62a370c8e (diff)
downloadmpv-d27ad9654218463694093697e3d09f8983b4ccf3.tar.bz2
mpv-d27ad9654218463694093697e3d09f8983b4ccf3.tar.xz
audio: redo internal AO API
This affects "pull" AOs only: ao_alsa, ao_pulse, ao_openal, ao_pcm, ao_lavc. There are changes to the other AOs too, but that's only about renaming ao_driver.resume to ao_driver.start. ao_openal is broken because I didn't manage to fix it, so it exits with an error message. If you want it, why don't _you_ put effort into it? I see no reason to waste my own precious lifetime over this (I realize the irony). ao_alsa loses the poll() mechanism, but it was mostly broken and didn't really do what it was supposed to. There doesn't seem to be anything in the ALSA API to watch the playback status without polling (unless you want to use raw UNIX signals). No idea if ao_pulse is correct, or whether it's subtly broken now. There is no documentation, so I can't tell what is correct, without reverse engineering the whole project. I recommend using ALSA. This was supposed to be just a simple fix, but somehow it expanded scope like a train wreck. Very high chance of regressions, but probably only for the AOs listed above. The rest you can figure out from reading the diff.
Diffstat (limited to 'audio/out/ao_alsa.c')
-rw-r--r--audio/out/ao_alsa.c369
1 files changed, 131 insertions, 238 deletions
diff --git a/audio/out/ao_alsa.c b/audio/out/ao_alsa.c
index e100c0fb12..0f375133e1 100644
--- a/audio/out/ao_alsa.c
+++ b/audio/out/ao_alsa.c
@@ -91,12 +91,9 @@ static const struct m_sub_options ao_alsa_conf = {
struct priv {
snd_pcm_t *alsa;
bool device_lost;
+ bool underrun;
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 +118,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;
@@ -865,9 +834,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 */
@@ -887,6 +855,9 @@ static int init_device(struct ao *ao, int mode)
p->convert.channels = ao->channels.num;
+ err = snd_pcm_prepare(p->alsa);
+ CHECK_ALSA_ERROR("pcm prepare error");
+
return 0;
alsa_error:
@@ -944,258 +915,184 @@ static int init(struct ao *ao)
return r;
}
-static int get_space(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}). If state!=NULL,
+// fill it after recovery.
+// 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;
+ int err;
- // 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;
-
-alsa_error:
- return 0;
-}
+ snd_pcm_status_t *st;
+ snd_pcm_status_alloca(&st);
-/* 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;
+ bool state_ok = false;
- if (p->paused)
- return p->delay_before_pause;
+ // 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);
+ CHECK_ALSA_ERROR("snd_pcm_status");
- int err = snd_pcm_delay(p->alsa, &delay);
- if (err < 0) {
- if (err == -EPIPE)
- handle_underrun(ao);
- return 0;
- }
+ snd_pcm_state_t pcmst = snd_pcm_status_get_state(st);
+ 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; note and recover. We never use draining,
+ case SND_PCM_STATE_XRUN:
+ case SND_PCM_STATE_DRAINING:
+ p->underrun = true;
+ 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;
+ }
+ return false;
}
}
-}
-static void audio_pause(struct ao *ao)
-{
- struct priv *p = ao->priv;
- int err;
-
- if (p->paused)
- return;
-
- p->delay_before_pause = get_delay(ao);
- p->prepause_frames = p->delay_before_pause * ao->samplerate;
+ if (!state_ok) {
+ MP_ERR(ao, "could not recover\n");
+ return false;
+ }
- 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 = snd_pcm_status_get_delay(st);
+ state->delay = MPMAX(del, 0) / (double)ao->samplerate;
+ state->free_samples = snd_pcm_status_get_avail(st);
+ state->free_samples = MPCLAMP(state->free_samples, 0, ao->device_buffer);
+ state->queued_samples = ao->device_buffer - state->free_samples;
+ state->underrun = p->underrun;
}
- p->paused = true;
+ return true;
-alsa_error: ;
+alsa_error:
+ return false;
}
-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);
+ p->underrun = false;
}
-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;
- 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;
-
- if (samples == 0)
- goto done;
- 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) {
- 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");
+ ao_convert_inplace(&p->convert, data, samples);
- while (1) {
- int r = ao_wait_poll(ao, fds, num_fds, lock);
- if (r)
- return r;
+ if (!recover_and_get_state(ao, NULL))
+ return false;
- unsigned short revents;
- err = snd_pcm_poll_descriptors_revents(p->alsa, fds, num_fds, &revents);
- CHECK_ALSA_ERROR("cannot read poll events");
+ 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);
+ }
- if (revents & POLLERR) {
- snd_pcm_status_t *status;
- snd_pcm_status_alloca(&status);
+ CHECK_ALSA_ERROR("pcm write error");
+ if (err != samples)
+ MP_WARN(ao, "unexpected short write\n");
- err = snd_pcm_status(p->alsa, status);
- check_device_present(ao, err);
- return -1;
- }
- if (revents & POLLOUT)
- return 0;
- }
- return 0;
+ return true;
alsa_error:
- return -1;
+ return false;
}
static bool is_useless_device(char *name)
@@ -1248,16 +1145,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,
- .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,
};