diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index 3f4e7b8f..7ac76c3a 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2016 Espen Jürgensen + * Copyright (C) 2015-2019 Espen Jürgensen * Copyright (C) 2010 Julien BLACHE * * This program is free software; you can redistribute it and/or modify @@ -29,7 +29,6 @@ #include #include -#include #include #include "misc.h" @@ -50,28 +49,8 @@ // within the 10 seconds where we measure latency each second. #define ALSA_MAX_LATENCY_VARIANCE 352 -// TODO Unglobalise these and add support for multiple sound cards -static char *card_name; -static char *mixer_name; -static char *mixer_device_name; -static snd_pcm_t *hdl; -static snd_mixer_t *mixer_hdl; -static snd_mixer_elem_t *vol_elem; -static long vol_min; -static long vol_max; -static int offset; -static int adjust_period_seconds; - #define ALSA_F_STARTED (1 << 15) -enum alsa_state -{ - ALSA_STATE_FAILED = 0, - ALSA_STATE_STOPPED = 1, - ALSA_STATE_STARTED = ALSA_F_STARTED, - ALSA_STATE_STREAMING = ALSA_F_STARTED | 0x01, -}; - enum alsa_sync_state { ALSA_SYNC_OK, @@ -81,69 +60,694 @@ enum alsa_sync_state struct alsa_session { - enum alsa_state state; + enum output_device_state state; - char *devname; + uint64_t device_id; + int callback_id; - uint64_t pos; - uint64_t start_pos; + const char *devname; + const char *card_name; + const char *mixer_name; + const char *mixer_device_name; + snd_pcm_status_t *pcm_status; + + struct media_quality quality; + + int buffer_nsamp; + + uint32_t pos; + uint32_t start_pos; + + struct timespec start_pts; + struct timespec last_pts; + snd_htimestamp_t dev_start_ts; + + snd_pcm_sframes_t last_avail; int32_t last_latency; - int sync_counter; - unsigned source_sample_rate; // raw input audio sample rate in Hz - unsigned target_sample_rate; // output rate in Hz to configure ALSA device - // An array that will hold the packets we prebuffer. The length of the array - // is prebuf_len (measured in rtp_packets) - uint8_t *prebuf; - uint32_t prebuf_len; - uint32_t prebuf_head; - uint32_t prebuf_tail; + // Here we buffer samples during startup + struct ringbuffer prebuf; + + int offset; + int adjust_period_seconds; int volume; + long vol_min; + long vol_max; - struct event *deferredev; - output_status_cb defer_cb; - - struct output_device *device; - output_status_cb status_cb; + snd_pcm_t *hdl; + snd_mixer_t *mixer_hdl; + snd_mixer_elem_t *vol_elem; struct alsa_session *next; }; -/* From player.c */ -extern struct event_base *evbase_player; - static struct alsa_session *sessions; -/* Forwards */ +// We will try to play the music with the source quality, but if the card +// doesn't support that we resample to the fallback quality +static struct media_quality alsa_fallback_quality = { 44100, 16, 2 }; +static struct media_quality alsa_last_quality; + + +/* -------------------------------- FORWARDS -------------------------------- */ + static void -defer_cb(int fd, short what, void *arg); +alsa_status(struct alsa_session *as); + + +/* ------------------------------- MISC HELPERS ----------------------------- */ + +static void +dump_config(struct alsa_session *as) +{ + snd_output_t *output; + char *debug_pcm_cfg; + int ret; + + // Dump PCM config data for E_DBG logging + ret = snd_output_buffer_open(&output); + if (ret == 0) + { + if (snd_pcm_dump_setup(as->hdl, output) == 0) + { + snd_output_buffer_string(output, &debug_pcm_cfg); + DPRINTF(E_DBG, L_LAUDIO, "Dump of sound device config:\n%s\n", debug_pcm_cfg); + } + + snd_output_close(output); + } +} + +static int +mixer_open(struct alsa_session *as) +{ + snd_mixer_elem_t *elem; + snd_mixer_elem_t *master; + snd_mixer_elem_t *pcm; + snd_mixer_elem_t *custom; + snd_mixer_selem_id_t *sid; + int ret; + + ret = snd_mixer_open(&as->mixer_hdl, 0); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Failed to open mixer: %s\n", snd_strerror(ret)); + as->mixer_hdl = NULL; + return -1; + } + + ret = snd_mixer_attach(as->mixer_hdl, as->mixer_device_name); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Failed to attach mixer: %s\n", snd_strerror(ret)); + goto out_close; + } + + ret = snd_mixer_selem_register(as->mixer_hdl, NULL, NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Failed to register mixer: %s\n", snd_strerror(ret)); + goto out_detach; + } + + ret = snd_mixer_load(as->mixer_hdl); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Failed to load mixer: %s\n", snd_strerror(ret)); + goto out_detach; + } + + // Grab interesting elements + snd_mixer_selem_id_alloca(&sid); + + pcm = NULL; + master = NULL; + custom = NULL; + for (elem = snd_mixer_first_elem(as->mixer_hdl); elem; elem = snd_mixer_elem_next(elem)) + { + snd_mixer_selem_get_id(elem, sid); + + if (as->mixer_name && (strcmp(snd_mixer_selem_id_get_name(sid), as->mixer_name) == 0)) + { + custom = elem; + break; + } + else if (strcmp(snd_mixer_selem_id_get_name(sid), "PCM") == 0) + pcm = elem; + else if (strcmp(snd_mixer_selem_id_get_name(sid), "Master") == 0) + master = elem; + } + + if (as->mixer_name) + { + if (custom) + as->vol_elem = custom; + else + { + DPRINTF(E_LOG, L_LAUDIO, "Failed to open configured mixer element '%s'\n", as->mixer_name); + + goto out_detach; + } + } + else if (pcm) + as->vol_elem = pcm; + else if (master) + as->vol_elem = master; + else + { + DPRINTF(E_LOG, L_LAUDIO, "Failed to open PCM or Master mixer element\n"); + + goto out_detach; + } + + // Get min & max volume + snd_mixer_selem_get_playback_volume_range(as->vol_elem, &as->vol_min, &as->vol_max); + + return 0; + + out_detach: + snd_mixer_detach(as->mixer_hdl, as->devname); + out_close: + snd_mixer_close(as->mixer_hdl); + as->mixer_hdl = NULL; + as->vol_elem = NULL; + + return -1; +} + +static int +device_open(struct alsa_session *as) +{ + snd_pcm_hw_params_t *hw_params; + snd_pcm_uframes_t bufsize; + int ret; + + hw_params = NULL; + + ret = snd_pcm_open(&as->hdl, as->devname, SND_PCM_STREAM_PLAYBACK, 0); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not open playback device: %s\n", snd_strerror(ret)); + return -1; + } + + // HW params + ret = snd_pcm_hw_params_malloc(&hw_params); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not allocate hw params: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_hw_params_any(as->hdl, hw_params); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve hw params: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_hw_params_set_access(as->hdl, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not set access method: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_hw_params_get_buffer_size_max(hw_params, &bufsize); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not get max buffer size: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_hw_params_set_buffer_size_max(as->hdl, hw_params, &bufsize); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not set buffer size to max: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_hw_params(as->hdl, hw_params); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not set hw params: %s\n", snd_strerror(ret)); + goto out_fail; + } + + snd_pcm_hw_params_free(hw_params); + hw_params = NULL; + + ret = mixer_open(as); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not open mixer\n"); + goto out_fail; + } + + return 0; + + out_fail: + if (hw_params) + snd_pcm_hw_params_free(hw_params); + + snd_pcm_close(as->hdl); + as->hdl = NULL; + + return -1; +} + +static int +device_quality_set(struct alsa_session *as, struct media_quality *quality, char **errmsg) +{ + snd_pcm_hw_params_t *hw_params; + snd_pcm_format_t format; + int ret; + + ret = snd_pcm_hw_params_malloc(&hw_params); + if (ret < 0) + { + *errmsg = safe_asprintf("Could not allocate hw params: %s", snd_strerror(ret)); + return -1; + } + + ret = snd_pcm_hw_params_any(as->hdl, hw_params); + if (ret < 0) + { + *errmsg = safe_asprintf("Could not retrieve hw params: %s", snd_strerror(ret)); + goto free_params; + } + + ret = snd_pcm_hw_params_set_rate(as->hdl, hw_params, quality->sample_rate, 0); + if (ret < 0) + { + *errmsg = safe_asprintf("Hardware doesn't support %d Hz: %s", quality->sample_rate, snd_strerror(ret)); + goto free_params; + } + + switch (quality->bits_per_sample) + { + case 16: + format = SND_PCM_FORMAT_S16_LE; + break; + case 24: + format = SND_PCM_FORMAT_S24_LE; + break; + case 32: + format = SND_PCM_FORMAT_S32_LE; + break; + default: + *errmsg = safe_asprintf("Unrecognized number of bits per sample: %d", quality->bits_per_sample); + goto free_params; + } + + ret = snd_pcm_hw_params_set_format(as->hdl, hw_params, format); + if (ret < 0) + { + *errmsg = safe_asprintf("Could not set %d bits per sample: %s", quality->bits_per_sample, snd_strerror(ret)); + goto free_params; + } + + ret = snd_pcm_hw_params_set_channels(as->hdl, hw_params, quality->channels); + if (ret < 0) + { + *errmsg = safe_asprintf("Could not set channel number (%d): %s", quality->channels, snd_strerror(ret)); + goto free_params; + } + + ret = snd_pcm_hw_params(as->hdl, hw_params); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not set hw params: %s\n", snd_strerror(ret)); + goto free_params; + } + + snd_pcm_hw_params_free(hw_params); + return 0; + + free_params: + snd_pcm_hw_params_free(hw_params); + return -1; +} + +static int +device_configure(struct alsa_session *as) +{ + snd_pcm_sw_params_t *sw_params; + int ret; + + ret = snd_pcm_sw_params_malloc(&sw_params); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not allocate sw params: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_sw_params_current(as->hdl, sw_params); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve current sw params: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_sw_params_set_tstamp_type(as->hdl, sw_params, SND_PCM_TSTAMP_TYPE_MONOTONIC); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not set tstamp type: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_sw_params_set_tstamp_mode(as->hdl, sw_params, SND_PCM_TSTAMP_ENABLE); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not set tstamp mode: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_sw_params(as->hdl, sw_params); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not set sw params: %s\n", snd_strerror(ret)); + goto out_fail; + } + + return 0; + + out_fail: + snd_pcm_sw_params_free(sw_params); + + return -1; +} + +static void +device_close(struct alsa_session *as) +{ + snd_pcm_close(as->hdl); + as->hdl = NULL; + + if (as->mixer_hdl) + { + snd_mixer_detach(as->mixer_hdl, as->devname); + snd_mixer_close(as->mixer_hdl); + + as->mixer_hdl = NULL; + as->vol_elem = NULL; + } +} + +static inline void +device_timestamp(struct alsa_session *as, snd_pcm_sframes_t *delay, snd_pcm_sframes_t *avail, snd_htimestamp_t *ts) +{ + snd_pcm_status(as->hdl, as->pcm_status); + + if (delay) + *delay = snd_pcm_status_get_delay(as->pcm_status); + if (avail) + *avail = snd_pcm_status_get_avail(as->pcm_status); + + snd_pcm_status_get_htstamp(as->pcm_status, ts); +} + +static void +playback_restart(struct alsa_session *as, struct output_buffer *obuf) +{ + snd_pcm_state_t state; + snd_pcm_sframes_t delay; + size_t size; + char *errmsg; + int ret; + + DPRINTF(E_INFO, L_LAUDIO, "Starting ALSA device '%s'\n", as->devname); + + state = snd_pcm_state(as->hdl); + if (state != SND_PCM_STATE_PREPARED) + { + if (state == SND_PCM_STATE_RUNNING) + snd_pcm_drop(as->hdl); // FIXME not great to do this during playback - would mean new quality drops audio? + + ret = snd_pcm_prepare(as->hdl); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not prepare ALSA device '%s' (state %d): %s\n", as->devname, state, snd_strerror(ret)); + return; + } + } + + // Negotiate quality (sample rate) with device - first we try to use the source quality + as->quality = obuf->data[0].quality; + ret = device_quality_set(as, &as->quality, &errmsg); + if (ret < 0) + { + DPRINTF(E_INFO, L_LAUDIO, "Input quality (%d/%d/%d) not supported, falling back to default. ALSA said: %s\n", + as->quality.sample_rate, as->quality.bits_per_sample, as->quality.channels, errmsg); + free(errmsg); + as->quality = alsa_fallback_quality; + ret = device_quality_set(as, &as->quality, &errmsg); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "ALSA device failed setting fallback quality: %s\n", errmsg); + free(errmsg); + as->state = OUTPUT_STATE_FAILED; + alsa_status(as); + return; + } + } + + dump_config(as); + + // Clear prebuffer in case start got called twice without a stop in between + ringbuffer_free(&as->prebuf, 1); + + as->start_pos = 0; + as->pos = 0; + + // Time stamps used for syncing + as->start_pts = obuf->pts; + + device_timestamp(as, &delay, &as->last_avail, &as->dev_start_ts); + if (as->dev_start_ts.tv_sec == 0 && as->dev_start_ts.tv_nsec == 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Can't get timestamps from ALSA, sync check is disabled\n"); + } + + // The difference between pos and start_pos should match the 2 second buffer + // that AirPlay uses (OUTPUTS_BUFFER_DURATION). We will not use alsa's buffer + // for the initial buffering, because my sound card's start_threshold is not + // to be counted on. Instead we allocate our own buffer, and when it is time + // to play we write as much as we can to alsa's buffer. Delay might be + // non-zero if we are restarting (?). + as->buffer_nsamp = OUTPUTS_BUFFER_DURATION * as->quality.sample_rate - delay; + size = STOB(as->buffer_nsamp, as->quality.bits_per_sample, as->quality.channels); + ringbuffer_init(&as->prebuf, size); + + as->state = OUTPUT_STATE_STREAMING; +} + +// This function writes the sample buf into either the prebuffer or directly to +// ALSA, depending on how much room there is in ALSA, and whether we are +// prebuffering or not. It also transfers from the the prebuffer to ALSA, if +// needed. Returns 0 on success, negative on error. +static int +buffer_write(struct alsa_session *as, struct output_data *odata, snd_pcm_sframes_t avail) +{ + uint8_t *buf; + size_t bufsize; + size_t wrote; + snd_pcm_sframes_t nsamp; + snd_pcm_sframes_t ret; + + // Prebuffering, no actual writing + if (avail == 0) + { + wrote = ringbuffer_write(&as->prebuf, odata->buffer, odata->bufsize); + nsamp = BTOS(wrote, as->quality.bits_per_sample, as->quality.channels); + return nsamp; + } + + // Read from prebuffer if it has data and write to device + if (as->prebuf.read_avail != 0) + { + // Maximum amount of bytes we want to read + bufsize = STOB(avail, as->quality.bits_per_sample, as->quality.channels); + + bufsize = ringbuffer_read(&buf, bufsize, &as->prebuf); + if (bufsize == 0) + return 0; + + nsamp = BTOS(bufsize, as->quality.bits_per_sample, as->quality.channels); + ret = snd_pcm_writei(as->hdl, buf, nsamp); + if (ret < 0) + return -1; + + avail -= ret; + } + + // Write to prebuffer if device buffer does not have availability. Note that + // if the prebuffer doesn't have enough room, which can happen if avail stays + // low, i.e. device buffer is overrunning, then the extra samples get dropped + if (odata->samples > avail) + { + ringbuffer_write(&as->prebuf, odata->buffer, odata->bufsize); + return odata->samples; + } + + ret = snd_pcm_writei(as->hdl, odata->buffer, odata->samples); + if (ret < 0) + return ret; + + if (ret != odata->samples) + DPRINTF(E_WARN, L_LAUDIO, "ALSA partial write detected\n"); + + return ret; +} + +static enum alsa_sync_state +sync_check(snd_pcm_sframes_t *delay, snd_pcm_sframes_t *avail, struct alsa_session *as, struct timespec pts) +{ + enum alsa_sync_state sync; + snd_htimestamp_t ts; + uint64_t elapsed; + uint64_t dev_elapsed; + uint64_t pos; + uint64_t dev_pos; + uint32_t buffered_samples; + int32_t latency; + + // We don't need avail for the sync check, but to reduce querying we retrieve + // it here as a service for the caller + device_timestamp(as, delay, avail, &ts); + if (ts.tv_sec == 0 && ts.tv_nsec == 0) + return ALSA_SYNC_OK; + + // Here we calculate elapsed time since we started, or since we last reset the + // sync timers: elapsed is how long the player thinks has elapsed, dev_elapsed + // is how long ALSA thinks has elapsed. If these are different, but the + // playback positition is the same, then the ALSA clock has drifted and we are + // coming out of sync. Unit is milliseconds. + elapsed = (pts.tv_sec - as->start_pts.tv_sec) * 1000L + (pts.tv_nsec - as->start_pts.tv_nsec) / 1000000; + dev_elapsed = (ts.tv_sec - as->dev_start_ts.tv_sec) * 1000L + (ts.tv_nsec - as->dev_start_ts.tv_nsec) / 1000000; + + // Now calculate playback positions. The pos is where we should be, dev_pos is + // where we actually are. + pos = as->start_pos + (elapsed - 1000 * OUTPUTS_BUFFER_DURATION) * as->quality.sample_rate / 1000; + buffered_samples = *delay + BTOS(as->prebuf.read_avail, as->quality.bits_per_sample, as->quality.channels); + dev_pos = as->start_pos + dev_elapsed * as->quality.sample_rate / 1000 - buffered_samples; + + // TODO calculate below and above more efficiently? + latency = pos - dev_pos; + + // If the latency is low or very different from our last measurement, we will wait and see + if (abs(latency) < ALSA_MAX_LATENCY || abs(as->last_latency - latency) > ALSA_MAX_LATENCY_VARIANCE) + sync = ALSA_SYNC_OK; + else if (latency > 0) + sync = ALSA_SYNC_BEHIND; + else + sync = ALSA_SYNC_AHEAD; + + // The will be used by sync_correct, so it knows how much we are out of sync + as->last_latency = latency; + + DPRINTF(E_DBG, L_LAUDIO, "Sync=%d, pos=%lu, as->pos=%u, dev_pos=%lu, latency=%d, delay=%li, avail=%li, elapsed=%lu, dev_elapsed=%lu\n", + sync, pos, as->pos, dev_pos, latency, *delay, *avail, elapsed / 1000000, dev_elapsed / 1000000); + + return sync; +} + +static void +sync_correct(void) +{ + // Not implemented yet +} + +static void +playback_write(struct alsa_session *as, struct output_buffer *obuf) +{ + snd_pcm_sframes_t ret; + snd_pcm_sframes_t delay; + enum alsa_sync_state sync; + bool prebuffering; + int i; + + // Find the quality we want + for (i = 0; obuf->data[i].buffer; i++) + { + if (quality_is_equal(&as->quality, &obuf->data[i].quality)) + break; + } + + if (!obuf->data[i].buffer) + { + DPRINTF(E_LOG, L_LAUDIO, "Output not delivering required data quality, aborting\n"); + as->state = OUTPUT_STATE_FAILED; + alsa_status(as); + return; + } + + prebuffering = (as->pos < as->buffer_nsamp); + if (prebuffering) + { + // Can never fail since we don't actually write to the device + as->pos += buffer_write(as, &obuf->data[i], 0); + return; + } + + // Check sync each time a second has passed + if (obuf->pts.tv_sec != as->last_pts.tv_sec) + { + sync = sync_check(&delay, &as->last_avail, as, obuf->pts); + if (sync != ALSA_SYNC_OK) + sync_correct(); + + as->last_pts = obuf->pts; + } + + ret = buffer_write(as, &obuf->data[i], as->last_avail); + if (ret < 0) + goto alsa_error; + + as->pos += ret; + + return; + + alsa_error: + if (ret == -EPIPE) + { + DPRINTF(E_WARN, L_LAUDIO, "ALSA buffer underrun\n"); + + ret = snd_pcm_prepare(as->hdl); + if (ret < 0) + { + DPRINTF(E_WARN, L_LAUDIO, "ALSA couldn't recover from underrun: %s\n", snd_strerror(ret)); + return; + } + + // Fill the prebuf with audio before restarting, so we don't underrun again + playback_restart(as, obuf); + return; + } + + DPRINTF(E_LOG, L_LAUDIO, "ALSA write error: %s\n", snd_strerror(ret)); + + as->state = OUTPUT_STATE_FAILED; + alsa_status(as); +} + /* ---------------------------- SESSION HANDLING ---------------------------- */ -static void -prebuf_free(struct alsa_session *as) -{ - if (as->prebuf) - free(as->prebuf); - - as->prebuf = NULL; - as->prebuf_len = 0; - as->prebuf_head = 0; - as->prebuf_tail = 0; -} - static void alsa_session_free(struct alsa_session *as) { if (!as) return; - if (as->deferredev) - event_free(as->deferredev); + device_close(as); - prebuf_free(as); + outputs_quality_unsubscribe(&alsa_fallback_quality); + + ringbuffer_free(&as->prebuf, 1); + snd_pcm_status_free(as->pcm_status); free(as); } @@ -166,883 +770,289 @@ alsa_session_cleanup(struct alsa_session *as) s->next = as->next; } - as->device->session = NULL; + outputs_device_session_remove(as->device_id); alsa_session_free(as); } static struct alsa_session * -alsa_session_make(struct output_device *device, output_status_cb cb) +alsa_session_make(struct output_device *device, int callback_id) { struct alsa_session *as; + cfg_t *cfg_audio; + char *errmsg; + int original_adjust; + int ret; CHECK_NULL(L_LAUDIO, as = calloc(1, sizeof(struct alsa_session))); - as->deferredev = evtimer_new(evbase_player, defer_cb, as); - if (!as->deferredev) + as->device_id = device->id; + as->callback_id = callback_id; + as->volume = device->volume; + + cfg_audio = cfg_getsec(cfg, "audio"); + + as->devname = cfg_getstr(cfg_audio, "card"); + as->mixer_name = cfg_getstr(cfg_audio, "mixer"); + as->mixer_device_name = cfg_getstr(cfg_audio, "mixer_device"); + if (!as->mixer_device_name || strlen(as->mixer_device_name) == 0) + as->mixer_device_name = cfg_getstr(cfg_audio, "card"); + + // TODO implement + as->offset = cfg_getint(cfg_audio, "offset"); + if (abs(as->offset) > 44100) { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA deferred event\n"); - goto failure_cleanup; + DPRINTF(E_LOG, L_LAUDIO, "The ALSA offset (%d) set in the configuration is out of bounds\n", as->offset); + as->offset = 44100 * (as->offset/abs(as->offset)); } - as->state = ALSA_STATE_STOPPED; - as->device = device; - as->status_cb = cb; - as->volume = device->volume; - as->devname = card_name; - as->source_sample_rate = 44100; - as->target_sample_rate = 44100; // TODO: make ALSA device sample rate configurable + // TODO implement + original_adjust = cfg_getint(cfg_audio, "adjust_period_seconds"); + if (original_adjust < 1) + as->adjust_period_seconds = 1; + else if (original_adjust > 20) + as->adjust_period_seconds = 20; + else + as->adjust_period_seconds = original_adjust; + if (as->adjust_period_seconds != original_adjust) + DPRINTF(E_LOG, L_LAUDIO, "Clamped ALSA adjust_period_seconds to %d\n", as->adjust_period_seconds); + snd_pcm_status_malloc(&as->pcm_status); + + ret = device_open(as); + if (ret < 0) + goto out_free_session; + + ret = device_quality_set(as, &alsa_fallback_quality, &errmsg); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "%s\n", errmsg); + free(errmsg); + goto out_device_close; + } + + // If this fails it just means we won't get timestamps, which we can handle + device_configure(as); + + ret = outputs_quality_subscribe(&alsa_fallback_quality); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not subscribe to fallback audio quality\n"); + goto out_device_close; + } + + as->state = OUTPUT_STATE_CONNECTED; as->next = sessions; sessions = as; - outputs_device_session_add(device, as); + // as is now the official device session + outputs_device_session_add(device->id, as); return as; - failure_cleanup: - alsa_session_free(as); + out_device_close: + device_close(as); + out_free_session: + free(as); return NULL; } - -/* ---------------------------- STATUS HANDLERS ----------------------------- */ - -// Maps our internal state to the generic output state and then makes a callback -// to the player to tell that state -static void -defer_cb(int fd, short what, void *arg) -{ - struct alsa_session *as = arg; - enum output_device_state state; - - switch (as->state) - { - case ALSA_STATE_FAILED: - state = OUTPUT_STATE_FAILED; - break; - case ALSA_STATE_STOPPED: - state = OUTPUT_STATE_STOPPED; - break; - case ALSA_STATE_STARTED: - state = OUTPUT_STATE_CONNECTED; - break; - case ALSA_STATE_STREAMING: - state = OUTPUT_STATE_STREAMING; - break; - default: - DPRINTF(E_LOG, L_LAUDIO, "Bug! Unhandled state in alsa_status()\n"); - state = OUTPUT_STATE_FAILED; - } - - if (as->defer_cb) - as->defer_cb(as->device, as->output_session, state); - - if (!(as->state & ALSA_F_STARTED)) - alsa_session_cleanup(as); -} - -// Note: alsa_states also nukes the session if it is not ALSA_F_STARTED static void alsa_status(struct alsa_session *as) { - as->defer_cb = as->status_cb; - event_active(as->deferredev, 0, 0); - as->status_cb = NULL; + outputs_cb(as->callback_id, as->device_id, as->state); + as->callback_id = -1; + + if (as->state == OUTPUT_STATE_FAILED || as->state == OUTPUT_STATE_STOPPED) + alsa_session_cleanup(as); } -/* ------------------------------- MISC HELPERS ----------------------------- */ - -/*static int -start_threshold_set(snd_pcm_uframes_t threshold) -{ - snd_pcm_sw_params_t *sw_params; - int ret; - - ret = snd_pcm_sw_params_malloc(&sw_params); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not allocate sw params: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_sw_params_current(hdl, sw_params); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve current sw params: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_sw_params_set_start_threshold(hdl, sw_params, threshold); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not set start threshold: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_sw_params(hdl, sw_params); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not set sw params: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - return 0; - - out_fail: - snd_pcm_sw_params_free(sw_params); - - return -1; -} -*/ - -static int -mixer_open(void) -{ - snd_mixer_elem_t *elem; - snd_mixer_elem_t *master; - snd_mixer_elem_t *pcm; - snd_mixer_elem_t *custom; - snd_mixer_selem_id_t *sid; - int ret; - - ret = snd_mixer_open(&mixer_hdl, 0); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Failed to open mixer: %s\n", snd_strerror(ret)); - - mixer_hdl = NULL; - return -1; - } - - ret = snd_mixer_attach(mixer_hdl, mixer_device_name); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Failed to attach mixer: %s\n", snd_strerror(ret)); - - goto out_close; - } - - ret = snd_mixer_selem_register(mixer_hdl, NULL, NULL); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Failed to register mixer: %s\n", snd_strerror(ret)); - - goto out_detach; - } - - ret = snd_mixer_load(mixer_hdl); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Failed to load mixer: %s\n", snd_strerror(ret)); - - goto out_detach; - } - - // Grab interesting elements - snd_mixer_selem_id_alloca(&sid); - - pcm = NULL; - master = NULL; - custom = NULL; - for (elem = snd_mixer_first_elem(mixer_hdl); elem; elem = snd_mixer_elem_next(elem)) - { - snd_mixer_selem_get_id(elem, sid); - - if (mixer_name && (strcmp(snd_mixer_selem_id_get_name(sid), mixer_name) == 0)) - { - custom = elem; - break; - } - else if (strcmp(snd_mixer_selem_id_get_name(sid), "PCM") == 0) - pcm = elem; - else if (strcmp(snd_mixer_selem_id_get_name(sid), "Master") == 0) - master = elem; - } - - if (mixer_name) - { - if (custom) - vol_elem = custom; - else - { - DPRINTF(E_LOG, L_LAUDIO, "Failed to open configured mixer element '%s'\n", mixer_name); - - goto out_detach; - } - } - else if (pcm) - vol_elem = pcm; - else if (master) - vol_elem = master; - else - { - DPRINTF(E_LOG, L_LAUDIO, "Failed to open PCM or Master mixer element\n"); - - goto out_detach; - } - - // Get min & max volume - snd_mixer_selem_get_playback_volume_range(vol_elem, &vol_min, &vol_max); - - return 0; - - out_detach: - snd_mixer_detach(mixer_hdl, card_name); - out_close: - snd_mixer_close(mixer_hdl); - mixer_hdl = NULL; - vol_elem = NULL; - - return -1; -} - -static int -device_open(struct alsa_session *as) -{ - snd_pcm_hw_params_t *hw_params; - snd_pcm_uframes_t bufsize; - int ret; - - hw_params = NULL; - - ret = snd_pcm_open(&hdl, card_name, SND_PCM_STREAM_PLAYBACK, 0); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not open playback device: %s\n", snd_strerror(ret)); - - return -1; - } - - // HW params - ret = snd_pcm_hw_params_malloc(&hw_params); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not allocate hw params: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params_any(hdl, hw_params); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve hw params: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params_set_access(hdl, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not set access method: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params_set_format(hdl, hw_params, SND_PCM_FORMAT_S16_LE); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not set S16LE format: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params_set_channels(hdl, hw_params, 2); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not set stereo output: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params_set_rate(hdl, hw_params, as->target_sample_rate, 0); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Hardware doesn't support %u Hz: %s\n", as->target_sample_rate, snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params_get_buffer_size_max(hw_params, &bufsize); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not get max buffer size: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params_set_buffer_size_max(hdl, hw_params, &bufsize); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not set buffer size to max: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params(hdl, hw_params); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not set hw params: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - snd_pcm_hw_params_free(hw_params); - hw_params = NULL; - - ret = mixer_open(); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not open mixer\n"); - - goto out_fail; - } - - return 0; - - out_fail: - if (hw_params) - snd_pcm_hw_params_free(hw_params); - - snd_pcm_close(hdl); - hdl = NULL; - - return -1; -} - -static void -device_close(void) -{ - snd_pcm_close(hdl); - hdl = NULL; - - if (mixer_hdl) - { - snd_mixer_detach(mixer_hdl, card_name); - snd_mixer_close(mixer_hdl); - - mixer_hdl = NULL; - vol_elem = NULL; - } -} - -static void -playback_start(struct alsa_session *as, uint64_t pos, uint64_t start_pos) -{ - snd_output_t *output; - snd_pcm_state_t state; - char *debug_pcm_cfg; - int ret; - - state = snd_pcm_state(hdl); - if (state != SND_PCM_STATE_PREPARED) - { - if (state == SND_PCM_STATE_RUNNING) - snd_pcm_drop(hdl); - - ret = snd_pcm_prepare(hdl); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not prepare ALSA device '%s' (state %d): %s\n", as->devname, state, snd_strerror(ret)); - return; - } - } - - // Clear prebuffer in case start somehow got called twice without a stop in between - prebuf_free(as); - - // Adjust the starting position with the configured value - start_pos -= offset; - - // The difference between pos and start_pos should match the 2 second - // buffer that AirPlay uses. We will not use alsa's buffer for the initial - // buffering, because my sound card's start_threshold is not to be counted on. - // Instead we allocate our own buffer, and when it is time to play we write as - // much as we can to alsa's buffer. - as->prebuf_len = (start_pos - pos) / ALSA_SAMPLES_PER_PACKET + 1; - if (as->prebuf_len > (3 * 44100 - offset) / ALSA_SAMPLES_PER_PACKET) - { - DPRINTF(E_LOG, L_LAUDIO, "Sanity check of prebuf_len (%" PRIu32 " packets) failed\n", as->prebuf_len); - return; - } - DPRINTF(E_DBG, L_LAUDIO, "Will prebuffer %d packets\n", as->prebuf_len); - - as->prebuf = malloc(as->prebuf_len * ALSA_PACKET_SIZE); - if (!as->prebuf) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for audio buffer (requested %" PRIu32 " packets)\n", as->prebuf_len); - return; - } - - as->pos = pos; - as->start_pos = start_pos - ALSA_SAMPLES_PER_PACKET; - - // Dump PCM config data for E_DBG logging - ret = snd_output_buffer_open(&output); - if (ret == 0) - { - if (snd_pcm_dump_setup(hdl, output) == 0) - { - snd_output_buffer_string(output, &debug_pcm_cfg); - DPRINTF(E_DBG, L_LAUDIO, "Dump of sound device config:\n%s\n", debug_pcm_cfg); - } - - snd_output_close(output); - } - - as->state = ALSA_STATE_STREAMING; -} - - -// This function writes the sample buf into either the prebuffer or directly to -// ALSA, depending on how much room there is in ALSA, and whether we are -// prebuffering or not. It also transfers from the the prebuffer to ALSA, if -// needed. Returns 0 on success, negative on error. -static int -buffer_write(struct alsa_session *as, uint8_t *buf, snd_pcm_sframes_t *avail, int prebuffering, int prebuf_empty) -{ - uint8_t *pkt; - int npackets; - snd_pcm_sframes_t nsamp; - snd_pcm_sframes_t ret; - - nsamp = ALSA_SAMPLES_PER_PACKET; - - if (as->prebuf && (prebuffering || !prebuf_empty || *avail < ALSA_SAMPLES_PER_PACKET)) - { - pkt = &as->prebuf[as->prebuf_head * ALSA_PACKET_SIZE]; - - memcpy(pkt, buf, ALSA_PACKET_SIZE); - - as->prebuf_head = (as->prebuf_head + 1) % as->prebuf_len; - - if (prebuffering || *avail < ALSA_SAMPLES_PER_PACKET) - return 0; // No actual writing - - // We will now set buf so that we will transfer as much as possible to ALSA - buf = &as->prebuf[as->prebuf_tail * ALSA_PACKET_SIZE]; - - if (as->prebuf_head > as->prebuf_tail) - npackets = as->prebuf_head - as->prebuf_tail; - else - npackets = as->prebuf_len - as->prebuf_tail; - - nsamp = npackets * ALSA_SAMPLES_PER_PACKET; - while (nsamp > *avail) - { - npackets -= 1; - nsamp -= ALSA_SAMPLES_PER_PACKET; - } - - as->prebuf_tail = (as->prebuf_tail + npackets) % as->prebuf_len; - } - - ret = snd_pcm_writei(hdl, buf, nsamp); - if (ret < 0) - return ret; - - if (ret != nsamp) - DPRINTF(E_WARN, L_LAUDIO, "ALSA partial write detected\n"); - - if (avail) - *avail -= ret; - - return 0; -} - -// Checks if ALSA's playback position is ahead or behind the player's -enum alsa_sync_state -sync_check(struct alsa_session *as, uint64_t rtptime, snd_pcm_sframes_t delay, int prebuf_empty) -{ - enum alsa_sync_state sync; - struct timespec now; - uint64_t cur_pos; - uint64_t pb_pos; - int32_t latency; - int npackets; - - sync = ALSA_SYNC_OK; - - if (player_get_current_pos(&cur_pos, &now, 0) != 0) - return sync; - - if (!prebuf_empty) - npackets = (as->prebuf_head - (as->prebuf_tail + 1) + as->prebuf_len) % as->prebuf_len + 1; - else - npackets = 0; - - pb_pos = rtptime - delay - ALSA_SAMPLES_PER_PACKET * npackets; - latency = cur_pos - (pb_pos - offset); - - // If the latency is low or very different from our last measurement, we reset the sync_counter - if (abs(latency) < ALSA_MAX_LATENCY || abs(as->last_latency - latency) > ALSA_MAX_LATENCY_VARIANCE) - { - as->sync_counter = 0; - sync = ALSA_SYNC_OK; - } - // If we have measured a consistent latency for configured period, then we take action - else if (as->sync_counter >= adjust_period_seconds * 126) - { - DPRINTF(E_INFO, L_LAUDIO, "Taking action to compensate for ALSA latency of %d samples\n", latency); - - as->sync_counter = 0; - if (latency > 0) - sync = ALSA_SYNC_BEHIND; - else - sync = ALSA_SYNC_AHEAD; - } - - as->last_latency = latency; - - if (latency) - DPRINTF(E_SPAM, L_LAUDIO, "Sync %d cur_pos %" PRIu64 ", pb_pos %" PRIu64 " (diff %d, delay %li), pos %" PRIu64 "\n", sync, cur_pos, pb_pos, latency, delay, as->pos); - - return sync; -} - -static void -playback_write(struct alsa_session *as, uint8_t *buf, uint64_t rtptime) -{ - snd_pcm_sframes_t ret; - snd_pcm_sframes_t avail; - snd_pcm_sframes_t delay; - enum alsa_sync_state sync; - int prebuffering; - int prebuf_empty; - - prebuffering = (as->pos < as->start_pos); - prebuf_empty = (as->prebuf_head == as->prebuf_tail); - - as->pos += ALSA_SAMPLES_PER_PACKET; - - if (prebuffering) - { - buffer_write(as, buf, NULL, prebuffering, prebuf_empty); - return; - } - - ret = snd_pcm_avail_delay(hdl, &avail, &delay); - if (ret < 0) - goto alsa_error; - - // Every second we do a sync check - sync = ALSA_SYNC_OK; - as->sync_counter++; - if (as->sync_counter % 126 == 0) - sync = sync_check(as, rtptime, delay, prebuf_empty); - - // Skip write -> reduce the delay - if (sync == ALSA_SYNC_BEHIND) - return; - - ret = buffer_write(as, buf, &avail, prebuffering, prebuf_empty); - // Double write -> increase the delay - if (sync == ALSA_SYNC_AHEAD && (ret == 0)) - ret = buffer_write(as, buf, &avail, prebuffering, prebuf_empty); - if (ret < 0) - goto alsa_error; - - return; - - alsa_error: - if (ret == -EPIPE) - { - DPRINTF(E_WARN, L_LAUDIO, "ALSA buffer underrun\n"); - - ret = snd_pcm_prepare(hdl); - if (ret < 0) - { - DPRINTF(E_WARN, L_LAUDIO, "ALSA couldn't recover from underrun: %s\n", snd_strerror(ret)); - return; - } - - // Fill the prebuf with audio before restarting, so we don't underrun again - as->start_pos = as->pos + ALSA_SAMPLES_PER_PACKET * (as->prebuf_len - 1); - - return; - } - - DPRINTF(E_LOG, L_LAUDIO, "ALSA write error: %s\n", snd_strerror(ret)); - - as->state = ALSA_STATE_FAILED; - alsa_status(as); -} - -static void -playback_pos_get(uint64_t *pos, uint64_t next_pkt) -{ - uint64_t cur_pos; - struct timespec now; - int ret; - - ret = player_get_current_pos(&cur_pos, &now, 0); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not get playback position, setting to next_pkt - 2 seconds\n"); - cur_pos = next_pkt - 88200; - } - - // Make pos the rtptime of the packet containing cur_pos - *pos = next_pkt; - while (*pos > cur_pos) - *pos -= ALSA_SAMPLES_PER_PACKET; -} - /* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */ static int -alsa_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) +alsa_device_start(struct output_device *device, int callback_id) { struct alsa_session *as; - int ret; - as = alsa_session_make(device, cb); + as = alsa_session_make(device, callback_id); if (!as) return -1; - ret = device_open(as); - if (ret < 0) - { - alsa_session_cleanup(as); - return -1; - } - - as->state = ALSA_STATE_STARTED; + as->state = OUTPUT_STATE_CONNECTED; alsa_status(as); return 0; } -static void -alsa_device_stop(struct output_session *session) +static int +alsa_device_stop(struct output_device *device, int callback_id) { - struct alsa_session *as = session->session; + struct alsa_session *as = device->session; - device_close(); + as->callback_id = callback_id; + as->state = OUTPUT_STATE_STOPPED; + alsa_status(as); // Will terminate the session since the state is STOPPED - as->state = ALSA_STATE_STOPPED; - alsa_status(as); + return 0; } static int alsa_device_flush(struct output_device *device, int callback_id) { struct alsa_session *as = device->session; - int i; - // TODO close device? + snd_pcm_drop(as->hdl); - snd_pcm_drop(hdl); - prebuf_free(as); + ringbuffer_free(&as->prebuf, 1); as->callback_id = callback_id; - as->state = ALSA_STATE_STARTED; + as->state = OUTPUT_STATE_CONNECTED; alsa_status(as); return 0; } static int -alsa_device_probe(struct output_device *device, output_status_cb cb) +alsa_device_probe(struct output_device *device, int callback_id) { struct alsa_session *as; - int ret; - as = alsa_session_make(device, cb); + as = alsa_session_make(device, callback_id); if (!as) return -1; - ret = device_open(as); - if (ret < 0) - { - alsa_session_cleanup(as); - return -1; - } - - device_close(); - - as->state = ALSA_STATE_STOPPED; - alsa_status(as); + as->state = OUTPUT_STATE_STOPPED; + alsa_status(as); // Will terminate the session since the state is STOPPED return 0; } static int -alsa_device_volume_set(struct output_device *device, output_status_cb cb) +alsa_device_volume_set(struct output_device *device, int callback_id) { - struct alsa_session *as; + struct alsa_session *as = device->session; int pcm_vol; - if (!device->session || !device->session->session) + if (!as) return 0; - as = device->session->session; + snd_mixer_handle_events(as->mixer_hdl); - if (!mixer_hdl || !vol_elem) - return 0; - - snd_mixer_handle_events(mixer_hdl); - - if (!snd_mixer_selem_is_active(vol_elem)) + if (!snd_mixer_selem_is_active(as->vol_elem)) return 0; switch (device->volume) { case 0: - pcm_vol = vol_min; + pcm_vol = as->vol_min; break; case 100: - pcm_vol = vol_max; + pcm_vol = as->vol_max; break; default: - pcm_vol = vol_min + (device->volume * (vol_max - vol_min)) / 100; + pcm_vol = as->vol_min + (device->volume * (as->vol_max - as->vol_min)) / 100; break; } DPRINTF(E_DBG, L_LAUDIO, "Setting ALSA volume to %d (%d)\n", pcm_vol, device->volume); - snd_mixer_selem_set_playback_volume_all(vol_elem, pcm_vol); + snd_mixer_selem_set_playback_volume_all(as->vol_elem, pcm_vol); - as->status_cb = cb; + as->callback_id = callback_id; alsa_status(as); return 1; } static void -alsa_playback_start(uint64_t next_pkt, struct timespec *ts) +alsa_device_cb_set(struct output_device *device, int callback_id) { - struct alsa_session *as; - uint64_t pos; + struct alsa_session *as = device->session; - if (!sessions) - return; - - playback_pos_get(&pos, next_pkt); - - DPRINTF(E_DBG, L_LAUDIO, "Starting ALSA audio (pos %" PRIu64 ", next_pkt %" PRIu64 ")\n", pos, next_pkt); - - for (as = sessions; as; as = as->next) - playback_start(as, pos, next_pkt); + as->callback_id = callback_id; } static void alsa_playback_stop(void) { struct alsa_session *as; + struct alsa_session *next; - for (as = sessions; as; as = as->next) + for (as = sessions; as; as = next) { - snd_pcm_drop(hdl); - prebuf_free(as); + next = as->next; + snd_pcm_drop(as->hdl); - as->state = ALSA_STATE_STARTED; - alsa_status(as); + as->state = OUTPUT_STATE_STOPPED; + alsa_status(as); // Will stop the session } } static void -alsa_write(uint8_t *buf, uint64_t rtptime) +alsa_write(struct output_buffer *obuf) { struct alsa_session *as; - uint64_t pos; + struct alsa_session *next; - for (as = sessions; as; as = as->next) + for (as = sessions; as; as = next) { - if (as->state == ALSA_STATE_STARTED) - { - playback_pos_get(&pos, rtptime); + next = as->next; + // Need to adjust buffers and device params if sample rate changed, or if + // this was the first write to the device + if (!quality_is_equal(&obuf->data[0].quality, &alsa_last_quality) || as->state == OUTPUT_STATE_CONNECTED) + playback_restart(as, obuf); - DPRINTF(E_DBG, L_LAUDIO, "Starting ALSA device '%s' (pos %" PRIu64 ", rtptime %" PRIu64 ")\n", as->devname, pos, rtptime); + playback_write(as, obuf); - playback_start(as, pos, rtptime); - } - - playback_write(as, buf, rtptime); + alsa_last_quality = obuf->data[0].quality; } } -static void -alsa_set_status_cb(struct output_session *session, output_status_cb cb) -{ - struct alsa_session *as = session->session; - - as->status_cb = cb; -} - static int alsa_init(void) { struct output_device *device; cfg_t *cfg_audio; - char *nickname; - char *type; - int original_adjust; + const char *type; + // Is ALSA enabled in config? cfg_audio = cfg_getsec(cfg, "audio"); type = cfg_getstr(cfg_audio, "type"); - if (type && (strcasecmp(type, "alsa") != 0)) return -1; - card_name = cfg_getstr(cfg_audio, "card"); - mixer_name = cfg_getstr(cfg_audio, "mixer"); - mixer_device_name = cfg_getstr(cfg_audio, "mixer_device"); - if (mixer_device_name == NULL || strlen(mixer_device_name) == 0) - mixer_device_name = card_name; - nickname = cfg_getstr(cfg_audio, "nickname"); - offset = cfg_getint(cfg_audio, "offset"); - if (abs(offset) > 44100) - { - DPRINTF(E_LOG, L_LAUDIO, "The ALSA offset (%d) set in the configuration is out of bounds\n", offset); - offset = 44100 * (offset/abs(offset)); - } - - original_adjust = adjust_period_seconds = cfg_getint(cfg_audio, "adjust_period_seconds"); - if (adjust_period_seconds < 1) - adjust_period_seconds = 1; - else if (adjust_period_seconds > 20) - adjust_period_seconds = 20; - if (original_adjust != adjust_period_seconds) - DPRINTF(E_LOG, L_LAUDIO, "Clamped ALSA adjust_period_seconds from %d to %d\n", original_adjust, adjust_period_seconds); - - device = calloc(1, sizeof(struct output_device)); - if (!device) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA device\n"); - return -1; - } + CHECK_NULL(L_LAUDIO, device = calloc(1, sizeof(struct output_device))); device->id = 0; - device->name = strdup(nickname); + device->name = strdup(cfg_getstr(cfg_audio, "nickname")); device->type = OUTPUT_TYPE_ALSA; device->type_name = outputs_name(device->type); device->has_video = 0; - DPRINTF(E_INFO, L_LAUDIO, "Adding ALSA device '%s' with name '%s'\n", card_name, nickname); + DPRINTF(E_INFO, L_LAUDIO, "Adding ALSA device '%s' with name '%s'\n", cfg_getstr(cfg_audio, "card"), device->name); player_device_add(device); snd_lib_error_set_handler(logger_alsa); - hdl = NULL; - mixer_hdl = NULL; - vol_elem = NULL; - return 0; } static void alsa_deinit(void) { + struct alsa_session *as; + snd_lib_error_set_handler(NULL); + + for (as = sessions; sessions; as = sessions) + { + sessions = as->next; + alsa_session_free(as); + } } struct output_definition output_alsa = @@ -1058,8 +1068,7 @@ struct output_definition output_alsa = .device_flush = alsa_device_flush, .device_probe = alsa_device_probe, .device_volume_set = alsa_device_volume_set, - .playback_start = alsa_playback_start, + .device_cb_set = alsa_device_cb_set, .playback_stop = alsa_playback_stop, .write = alsa_write, - .status_cb = alsa_set_status_cb, };