diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index 1a32aad1..fb545f46 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -57,7 +57,11 @@ // by 16, while high one would get stepped by 4 x 16 #define ALSA_RESAMPLE_STEP_MULTIPLE 2 -#define ALSA_F_STARTED (1 << 15) +#define ALSA_ERROR_WRITE -1 +#define ALSA_ERROR_UNDERRUN -2 +#define ALSA_ERROR_SESSION -3 +#define ALSA_ERROR_DEVICE -4 +#define ALSA_ERROR_DEVICE_BUSY -5 enum alsa_sync_state { @@ -66,21 +70,18 @@ enum alsa_sync_state ALSA_SYNC_BEHIND, }; -struct alsa_session +struct alsa_mixer { - enum output_device_state state; + snd_mixer_t *hdl; + snd_mixer_elem_t *vol_elem; - uint64_t device_id; - int callback_id; + long vol_min; + long vol_max; +}; - 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; +struct alsa_playback_session +{ + snd_pcm_t *pcm; int buffer_nsamp; @@ -89,6 +90,7 @@ struct alsa_session uint32_t last_pos; uint32_t last_buflen; + struct media_quality quality; struct timespec last_pts; // Used for syncing with the clock @@ -105,15 +107,27 @@ struct alsa_session // Here we buffer samples during startup struct ringbuffer prebuf; + struct alsa_playback_session *next; +}; + +struct alsa_session +{ + enum output_device_state state; + + uint64_t device_id; + int callback_id; + + const char *devname; + const char *card_name; + const char *mixer_name; + const char *mixer_device_name; + + struct alsa_mixer mixer; + int offset_ms; - int volume; - long vol_min; - long vol_max; - - snd_pcm_t *hdl; - snd_mixer_t *mixer_hdl; - snd_mixer_elem_t *vol_elem; + // A session will have multiple playback sessions when the quality changes + struct alsa_playback_session *pb; struct alsa_session *next; }; @@ -138,7 +152,7 @@ alsa_status(struct alsa_session *as); /* ------------------------------- MISC HELPERS ----------------------------- */ static void -dump_config(struct alsa_session *as) +dump_config(snd_pcm_t *pcm) { snd_output_t *output; char *debug_pcm_cfg; @@ -148,7 +162,7 @@ dump_config(struct alsa_session *as) ret = snd_output_buffer_open(&output); if (ret == 0) { - if (snd_pcm_dump_setup(as->hdl, output) == 0) + if (snd_pcm_dump_setup(pcm, 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); @@ -172,41 +186,76 @@ bps2format(int bits_per_sample) } static int -mixer_open(struct alsa_session *as) +volume_set(struct alsa_mixer *mixer, int volume) { + int pcm_vol; + + snd_mixer_handle_events(mixer->hdl); + + if (!snd_mixer_selem_is_active(mixer->vol_elem)) + return -1; + + switch (volume) + { + case 0: + pcm_vol = mixer->vol_min; + break; + + case 100: + pcm_vol = mixer->vol_max; + break; + + default: + pcm_vol = mixer->vol_min + (volume * (mixer->vol_max - mixer->vol_min)) / 100; + break; + } + + DPRINTF(E_DBG, L_LAUDIO, "Setting ALSA volume to %d (%d)\n", pcm_vol, volume); + + snd_mixer_selem_set_playback_volume_all(mixer->vol_elem, pcm_vol); + + return 0; +} + +static int +mixer_open(struct alsa_mixer *mixer, const char *mixer_device_name, const char *mixer_name) +{ + snd_mixer_t *mixer_hdl; + snd_mixer_elem_t *vol_elem; 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; + long vol_min; + long vol_max; int ret; - ret = snd_mixer_open(&as->mixer_hdl, 0); + ret = snd_mixer_open(&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); + 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)); + DPRINTF(E_LOG, L_LAUDIO, "Failed to attach mixer '%s': %s\n", mixer_device_name, snd_strerror(ret)); goto out_close; } - ret = snd_mixer_selem_register(as->mixer_hdl, NULL, NULL); + 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)); + DPRINTF(E_LOG, L_LAUDIO, "Failed to register mixer '%s': %s\n", mixer_device_name, snd_strerror(ret)); goto out_detach; } - ret = snd_mixer_load(as->mixer_hdl); + ret = snd_mixer_load(mixer_hdl); if (ret < 0) { - DPRINTF(E_LOG, L_LAUDIO, "Failed to load mixer: %s\n", snd_strerror(ret)); + DPRINTF(E_LOG, L_LAUDIO, "Failed to load mixer '%s': %s\n", mixer_device_name, snd_strerror(ret)); goto out_detach; } @@ -216,11 +265,11 @@ mixer_open(struct alsa_session *as) pcm = NULL; master = NULL; custom = NULL; - for (elem = snd_mixer_first_elem(as->mixer_hdl); elem; elem = snd_mixer_elem_next(elem)) + for (elem = snd_mixer_first_elem(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)) + if (mixer_name && (strcmp(snd_mixer_selem_id_get_name(sid), mixer_name) == 0)) { custom = elem; break; @@ -231,57 +280,71 @@ mixer_open(struct alsa_session *as) master = elem; } - if (as->mixer_name) + if (mixer_name) { if (custom) - as->vol_elem = custom; + vol_elem = custom; else { - DPRINTF(E_LOG, L_LAUDIO, "Failed to open configured mixer element '%s'\n", as->mixer_name); - + DPRINTF(E_LOG, L_LAUDIO, "Failed to open configured mixer element '%s'\n", mixer_name); goto out_detach; } } else if (pcm) - as->vol_elem = pcm; + vol_elem = pcm; else if (master) - as->vol_elem = 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(as->vol_elem, &as->vol_min, &as->vol_max); + snd_mixer_selem_get_playback_volume_range(vol_elem, &vol_min, &vol_max); + + // All done, export + mixer->hdl = mixer_hdl; + mixer->vol_elem = vol_elem; + mixer->vol_min = vol_min; + mixer->vol_max = vol_max; return 0; out_detach: - snd_mixer_detach(as->mixer_hdl, as->devname); + snd_mixer_detach(mixer_hdl, mixer_device_name); out_close: - snd_mixer_close(as->mixer_hdl); - as->mixer_hdl = NULL; - as->vol_elem = NULL; + snd_mixer_close(mixer_hdl); return -1; } -static int -device_open(struct alsa_session *as) +static void +mixer_close(struct alsa_mixer *mixer, const char *mixer_device_name) { + if (!mixer || !mixer->hdl) + return; + + snd_mixer_detach(mixer->hdl, mixer_device_name); + snd_mixer_close(mixer->hdl); +} + +static int +pcm_open(snd_pcm_t **pcm, const char *device_name, struct media_quality *quality) +{ + snd_pcm_t *hdl; 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); + ret = snd_pcm_open(&hdl, device_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; + if (ret == -EBUSY) + return ALSA_ERROR_DEVICE_BUSY; + + DPRINTF(E_LOG, L_LAUDIO, "Could not open playback device '%s': %s\n", device_name, snd_strerror(ret)); + return ALSA_ERROR_DEVICE; } // HW params @@ -292,42 +355,38 @@ device_open(struct alsa_session *as) goto out_fail; } - ret = snd_pcm_hw_params_any(as->hdl, hw_params); + 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(as->hdl, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); + 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; } - // Some devices (like the allo Boss DAC on RPi) fail to open w/o quality set - ret = snd_pcm_hw_params_set_format(as->hdl, hw_params, bps2format(alsa_fallback_quality.bits_per_sample)); + ret = snd_pcm_hw_params_set_format(hdl, hw_params, bps2format(quality->bits_per_sample)); if (ret < 0) { - DPRINTF(E_LOG, L_LAUDIO, "Could not set format (bits per sample %d): %s\n", alsa_fallback_quality.bits_per_sample, snd_strerror(ret)); - + DPRINTF(E_LOG, L_LAUDIO, "Could not set format (bits per sample %d): %s\n", quality->bits_per_sample, snd_strerror(ret)); goto out_fail; } - ret = snd_pcm_hw_params_set_channels(as->hdl, hw_params, alsa_fallback_quality.channels); + ret = snd_pcm_hw_params_set_channels(hdl, hw_params, quality->channels); 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(as->hdl, hw_params, alsa_fallback_quality.sample_rate, 0); + ret = snd_pcm_hw_params_set_rate(hdl, hw_params, quality->sample_rate, 0); if (ret < 0) { - DPRINTF(E_LOG, L_LAUDIO, "Hardware doesn't support %u Hz: %s\n", alsa_fallback_quality.sample_rate, snd_strerror(ret)); - + DPRINTF(E_LOG, L_LAUDIO, "Hardware doesn't support %u Hz: %s\n", quality->sample_rate, snd_strerror(ret)); goto out_fail; } @@ -338,109 +397,47 @@ device_open(struct alsa_session *as) goto out_fail; } - ret = snd_pcm_hw_params_set_buffer_size_max(as->hdl, hw_params, &bufsize); + // Enable this line to simulate devices with low buffer size + //bufsize = 32768; + + 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(as->hdl, hw_params); + ret = snd_pcm_hw_params(hdl, hw_params); if (ret < 0) { - DPRINTF(E_LOG, L_LAUDIO, "Could not set hw params in device_open(): %s\n", snd_strerror(ret)); + DPRINTF(E_LOG, L_LAUDIO, "Could not set hw params in pcm_open(): %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; - } + *pcm = hdl; return 0; out_fail: - if (hw_params) - snd_pcm_hw_params_free(hw_params); + snd_pcm_hw_params_free(hw_params); + snd_pcm_close(hdl); - snd_pcm_close(as->hdl); - as->hdl = NULL; - - return -1; + return ALSA_ERROR_DEVICE; } -static int -device_quality_set(struct alsa_session *as, struct media_quality *quality, char **errmsg) +static void +pcm_close(snd_pcm_t *hdl) { - snd_pcm_hw_params_t *hw_params; - int ret; + if (!hdl) + return; - 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; - } - - // Some devices, e.g. the RPi1, require this to be set again, even though it - // is also set by device_open() - ret = snd_pcm_hw_params_set_access(as->hdl, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); - if (ret < 0) - { - *errmsg = safe_asprintf("Could not set access method: %s\n", 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; - } - - ret = snd_pcm_hw_params_set_format(as->hdl, hw_params, bps2format(quality->bits_per_sample)); - 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) - { - *errmsg = safe_asprintf("Could not set hw params in device_quality_set(): %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; + snd_pcm_close(hdl); } static int -device_configure(struct alsa_session *as) +pcm_configure(snd_pcm_t *hdl) { snd_pcm_sw_params_t *sw_params; int ret; @@ -452,28 +449,28 @@ device_configure(struct alsa_session *as) goto out_fail; } - ret = snd_pcm_sw_params_current(as->hdl, sw_params); + 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_tstamp_type(as->hdl, sw_params, SND_PCM_TSTAMP_TYPE_MONOTONIC); + ret = snd_pcm_sw_params_set_tstamp_type(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); + ret = snd_pcm_sw_params_set_tstamp_mode(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); + 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)); @@ -491,78 +488,108 @@ device_configure(struct alsa_session *as) } static void -device_close(struct alsa_session *as) +playback_session_free(struct alsa_playback_session *pb) { - snd_pcm_close(as->hdl); - as->hdl = NULL; + if (!pb) + return; - if (as->mixer_hdl) - { - snd_mixer_detach(as->mixer_hdl, as->devname); - snd_mixer_close(as->mixer_hdl); + pcm_close(pb->pcm); - as->mixer_hdl = NULL; - as->vol_elem = NULL; - } + ringbuffer_free(&pb->prebuf, 1); + + free(pb->latency_history); + free(pb); } static void -playback_restart(struct alsa_session *as, struct output_buffer *obuf) +playback_session_remove(struct alsa_session *as, struct alsa_playback_session *pb) { + struct alsa_playback_session *s; + + DPRINTF(E_DBG, L_LAUDIO, "Removing playback session (quality %d/%d/%d) from ALSA device '%s'\n", + pb->quality.sample_rate, pb->quality.bits_per_sample, pb->quality.channels, as->devname); + + if (pb == as->pb) + as->pb = as->pb->next; + else + { + for (s = as->pb; s && (s->next != pb); s = s->next) + ; /* EMPTY */ + + if (!s) + DPRINTF(E_WARN, L_LAUDIO, "WARNING: struct alsa_playback_session not found in list; BUG!\n"); + else + s->next = pb->next; + } + + playback_session_free(pb); +} + +static void +playback_session_remove_all(struct alsa_session *as) +{ + struct alsa_playback_session *s; + + for (s = as->pb; s; s = as->pb) + { + as->pb = s->next; + playback_session_free(s); + } +} + +static int +playback_session_add(struct alsa_session *as, struct media_quality *quality, struct timespec pts) +{ + struct alsa_playback_session *pb; + struct alsa_playback_session *tail_pb; struct timespec ts; - snd_pcm_state_t state; snd_pcm_sframes_t offset_nsamp; size_t size; - char *errmsg; int ret; - DPRINTF(E_INFO, L_LAUDIO, "Starting ALSA device '%s'\n", as->devname); + DPRINTF(E_DBG, L_LAUDIO, "Adding playback session (quality %d/%d/%d) to ALSA device '%s'\n", + quality->sample_rate, quality->bits_per_sample, quality->channels, as->devname); - state = snd_pcm_state(as->hdl); - if (state != SND_PCM_STATE_PREPARED) + CHECK_NULL(L_LAUDIO, pb = calloc(1, sizeof(struct alsa_playback_session))); + CHECK_NULL(L_LAUDIO, pb->latency_history = calloc(alsa_latency_history_size, sizeof(double))); + + ret = pcm_open(&pb->pcm, as->devname, quality); + if (ret == ALSA_ERROR_DEVICE_BUSY) { - 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, "ALSA device '%s' won't open due to existing session (no support for concurrent audio), truncating audio\n", as->devname); + playback_session_remove_all(as); + ret = pcm_open(&pb->pcm, as->devname, quality); + if (ret == ALSA_ERROR_DEVICE_BUSY) { - DPRINTF(E_LOG, L_LAUDIO, "Could not prepare ALSA device '%s' (state %d): %s\n", as->devname, state, snd_strerror(ret)); - return; + DPRINTF(E_LOG, L_LAUDIO, "ALSA device '%s' failed: Device still busy after closing previous sessions\n", as->devname); + goto error; } } - // 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); + DPRINTF(E_LOG, L_LAUDIO, "Device '%s' does not support quality (%d/%d/%d), falling back to default\n", as->devname, quality->sample_rate, quality->bits_per_sample, quality->channels); + ret = pcm_open(&pb->pcm, as->devname, &alsa_fallback_quality); 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; + DPRINTF(E_LOG, L_LAUDIO, "ALSA device failed setting fallback quality\n"); + goto error; } + + pb->quality = alsa_fallback_quality; } + else + pb->quality = *quality; - dump_config(as); + // If this fails it just means we won't get timestamps, which we can handle + pcm_configure(pb->pcm); - // Clear prebuffer in case start got called twice without a stop in between - ringbuffer_free(&as->prebuf, 1); - - as->pos = 0; + dump_config(pb->pcm); // Time stamps used for syncing, here we set when playback should start ts.tv_sec = OUTPUTS_BUFFER_DURATION; ts.tv_nsec = (uint64_t)as->offset_ms * 1000000UL; - as->stamp_pts = timespec_add(obuf->pts, ts); + pb->stamp_pts = timespec_add(pts, ts); // The difference between pos and start pos should match the 2 second buffer // that AirPlay uses (OUTPUTS_BUFFER_DURATION) + user configured offset_ms. We @@ -570,13 +597,29 @@ playback_restart(struct alsa_session *as, struct output_buffer *obuf) // 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. - offset_nsamp = (as->offset_ms * as->quality.sample_rate / 1000); + offset_nsamp = (as->offset_ms * pb->quality.sample_rate / 1000); - as->buffer_nsamp = OUTPUTS_BUFFER_DURATION * as->quality.sample_rate + offset_nsamp; - size = STOB(as->buffer_nsamp, as->quality.bits_per_sample, as->quality.channels); - ringbuffer_init(&as->prebuf, size); + pb->buffer_nsamp = OUTPUTS_BUFFER_DURATION * pb->quality.sample_rate + offset_nsamp; + size = STOB(pb->buffer_nsamp, pb->quality.bits_per_sample, pb->quality.channels); + ringbuffer_init(&pb->prebuf, size); - as->state = OUTPUT_STATE_STREAMING; + // Add to the end of the list, because when we iterate through it in + // alsa_write() we want to write data from the oldest playback session first + if (as->pb) + { + for (tail_pb = as->pb; tail_pb->next; tail_pb = tail_pb->next) + ; // Fast forward + tail_pb->next = pb; + } + else + as->pb = pb; + + return 0; + + error: + playback_session_free(pb); + + return -1; } // This function writes the sample buf into either the prebuffer or directly to @@ -584,7 +627,7 @@ playback_restart(struct alsa_session *as, struct output_buffer *obuf) // 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) +buffer_write(struct alsa_playback_session *pb, struct output_data *odata, snd_pcm_sframes_t avail) { uint8_t *buf; size_t bufsize; @@ -595,39 +638,48 @@ buffer_write(struct alsa_session *as, struct output_data *odata, snd_pcm_sframes // 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); + wrote = ringbuffer_write(&pb->prebuf, odata->buffer, odata->bufsize); + if (wrote < odata->bufsize) + DPRINTF(E_WARN, L_LAUDIO, "Bug! Partial prebuf write %zu/%zu\n", wrote, odata->bufsize); + + nsamp = BTOS(wrote, pb->quality.bits_per_sample, pb->quality.channels); return nsamp; } // Read from prebuffer if it has data and write to device - if (as->prebuf.read_avail != 0) + if (pb->prebuf.read_avail != 0) { // Maximum amount of bytes we want to read - bufsize = STOB(avail, as->quality.bits_per_sample, as->quality.channels); + bufsize = STOB(avail, pb->quality.bits_per_sample, pb->quality.channels); - bufsize = ringbuffer_read(&buf, bufsize, &as->prebuf); + bufsize = ringbuffer_read(&buf, bufsize, &pb->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); +// DPRINTF(E_DBG, L_LAUDIO, "Writing prebuffer (read_avail=%zu, bufsize=%zu, avail=%li)\n", pb->prebuf.read_avail, bufsize, avail); + + nsamp = BTOS(bufsize, pb->quality.bits_per_sample, pb->quality.channels); + ret = snd_pcm_writei(pb->pcm, buf, nsamp); if (ret < 0) return ret; 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) + // Write to prebuffer if device buffer does not have availability or if we are + // still prebuffering. 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 || pb->prebuf.read_avail != 0) { - ringbuffer_write(&as->prebuf, odata->buffer, odata->bufsize); + wrote = ringbuffer_write(&pb->prebuf, odata->buffer, odata->bufsize); + if (wrote < odata->bufsize) + DPRINTF(E_WARN, L_LAUDIO, "Dropped %zu bytes of audio - device is overrunning!\n", odata->bufsize - wrote); + return odata->samples; } - ret = snd_pcm_writei(as->hdl, odata->buffer, odata->samples); + ret = snd_pcm_writei(pb->pcm, odata->buffer, odata->samples); if (ret < 0) return ret; @@ -638,7 +690,7 @@ buffer_write(struct alsa_session *as, struct output_data *odata, snd_pcm_sframes } static enum alsa_sync_state -sync_check(double *drift, double *latency, struct alsa_session *as, snd_pcm_sframes_t delay) +sync_check(double *drift, double *latency, struct alsa_playback_session *pb, snd_pcm_sframes_t delay) { enum alsa_sync_state sync; struct timespec ts; @@ -658,28 +710,28 @@ sync_check(double *drift, double *latency, struct alsa_session *as, snd_pcm_sfra // taking into account buffer time and configuration of offset_ms. We then // calculate our expected position based on elapsed time, and if different // from where we are + what is in the buffers then ALSA is out of sync. - elapsed = (ts.tv_sec - as->stamp_pts.tv_sec) * 1000L + (ts.tv_nsec - as->stamp_pts.tv_nsec) / 1000000; + elapsed = (ts.tv_sec - pb->stamp_pts.tv_sec) * 1000L + (ts.tv_nsec - pb->stamp_pts.tv_nsec) / 1000000; if (elapsed < 0) return ALSA_SYNC_OK; - cur_pos = (uint64_t)as->pos - as->stamp_pos - (delay + BTOS(as->prebuf.read_avail, as->quality.bits_per_sample, as->quality.channels)); - exp_pos = (uint64_t)elapsed * as->quality.sample_rate / 1000; + cur_pos = (uint64_t)pb->pos - pb->stamp_pos - (delay + BTOS(pb->prebuf.read_avail, pb->quality.bits_per_sample, pb->quality.channels)); + exp_pos = (uint64_t)elapsed * pb->quality.sample_rate / 1000; diff = cur_pos - exp_pos; DPRINTF(E_SPAM, L_LAUDIO, "counter %d/%d, stamp %lu:%lu, now %lu:%lu, elapsed is %d ms, cur_pos=%" PRIu64 ", exp_pos=%" PRIu64 ", diff=%d\n", - as->latency_counter, alsa_latency_history_size, as->stamp_pts.tv_sec, as->stamp_pts.tv_nsec / 1000000, ts.tv_sec, ts.tv_nsec / 1000000, elapsed, cur_pos, exp_pos, diff); + pb->latency_counter, alsa_latency_history_size, pb->stamp_pts.tv_sec, pb->stamp_pts.tv_nsec / 1000000, ts.tv_sec, ts.tv_nsec / 1000000, elapsed, cur_pos, exp_pos, diff); // Add the latency to our measurement history - as->latency_history[as->latency_counter] = (double)diff; - as->latency_counter++; + pb->latency_history[pb->latency_counter] = (double)diff; + pb->latency_counter++; // Haven't collected enough samples for sync evaluation yet, so just return - if (as->latency_counter < alsa_latency_history_size) + if (pb->latency_counter < alsa_latency_history_size) return ALSA_SYNC_OK; - as->latency_counter = 0; + pb->latency_counter = 0; - ret = linear_regression(drift, latency, &r2, NULL, as->latency_history, alsa_latency_history_size); + ret = linear_regression(drift, latency, &r2, NULL, pb->latency_history, alsa_latency_history_size); if (ret < 0) { DPRINTF(E_WARN, L_LAUDIO, "Linear regression of collected latency samples failed\n"); @@ -710,7 +762,7 @@ sync_check(double *drift, double *latency, struct alsa_session *as, snd_pcm_sfra } static void -sync_correct(struct alsa_session *as, double drift, double latency, struct timespec pts, snd_pcm_sframes_t delay) +sync_correct(struct alsa_playback_session *pb, double drift, double latency, struct timespec pts, snd_pcm_sframes_t delay) { int step; int sign; @@ -719,118 +771,149 @@ sync_correct(struct alsa_session *as, double drift, double latency, struct times // step 44100 -> 44000 -> 40900 -> 44000 -> 44100. If we used percentages to // to step, we would have to deal with rounding; we don't want to step 44100 // -> 39996 -> 44099. - step = ALSA_RESAMPLE_STEP_MULTIPLE * (as->quality.sample_rate / 20000); + step = ALSA_RESAMPLE_STEP_MULTIPLE * (pb->quality.sample_rate / 20000); sign = (drift < 0) ? -1 : 1; - if (abs(as->sync_resample_step) == ALSA_RESAMPLE_STEP_MAX) + if (abs(pb->sync_resample_step) == ALSA_RESAMPLE_STEP_MAX) { - DPRINTF(E_LOG, L_LAUDIO, "The sync of ALSA device '%s' cannot be corrected (drift=%f, latency=%f)\n", as->devname, drift, latency); - as->sync_resample_step += sign; + DPRINTF(E_LOG, L_LAUDIO, "The sync of ALSA device cannot be corrected (drift=%f, latency=%f)\n", drift, latency); + pb->sync_resample_step += sign; return; } - else if (abs(as->sync_resample_step) > ALSA_RESAMPLE_STEP_MAX) + else if (abs(pb->sync_resample_step) > ALSA_RESAMPLE_STEP_MAX) return; // Don't do anything, we have given up // Step 0 is the original audio quality (or the fallback quality), which we // will just keep receiving - if (as->sync_resample_step != 0) - outputs_quality_unsubscribe(&as->quality); + if (pb->sync_resample_step != 0) + outputs_quality_unsubscribe(&pb->quality); - as->sync_resample_step += sign; - as->quality.sample_rate += sign * step; + pb->sync_resample_step += sign; + pb->quality.sample_rate += sign * step; - if (as->sync_resample_step != 0) - outputs_quality_subscribe(&as->quality); + if (pb->sync_resample_step != 0) + outputs_quality_subscribe(&pb->quality); // Reset position so next sync_correct latency correction is only based on // what has elapsed since our correction - as->stamp_pos = (uint64_t)as->pos - (delay + BTOS(as->prebuf.read_avail, as->quality.bits_per_sample, as->quality.channels));; - as->stamp_pts = pts; + pb->stamp_pos = (uint64_t)pb->pos - (delay + BTOS(pb->prebuf.read_avail, pb->quality.bits_per_sample, pb->quality.channels));; + pb->stamp_pts = pts; - DPRINTF(E_INFO, L_LAUDIO, "Adjusted sample rate to %d to sync ALSA device '%s' (drift=%f, latency=%f)\n", as->quality.sample_rate, as->devname, drift, latency); + DPRINTF(E_INFO, L_LAUDIO, "Adjusted sample rate to %d to sync ALSA device (drift=%f, latency=%f)\n", pb->quality.sample_rate, drift, latency); } -static void -playback_write(struct alsa_session *as, struct output_buffer *obuf) +static int +playback_drain(struct alsa_playback_session *pb) +{ + uint8_t *buf; + size_t bufsize; + snd_pcm_state_t state; + snd_pcm_sframes_t avail; + snd_pcm_sframes_t delay; + snd_pcm_sframes_t nsamp; + int ret; + + state = snd_pcm_state(pb->pcm); + if (state == SND_PCM_STATE_DRAINING) + return 0; + else if (state != SND_PCM_STATE_RUNNING) + return ALSA_ERROR_SESSION; // We are probably done draining, so this makes the caller close the pb session + + // If the prebuffer is empty we are done writing to this pcm + if (pb->prebuf.read_avail == 0) + { + snd_pcm_drain(pb->pcm); // Plays pending frames and then stops the pcm + return 0; + } + + ret = snd_pcm_avail_delay(pb->pcm, &avail, &delay); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Error getting avail/delay: %s\n", snd_strerror(ret)); + return ALSA_ERROR_SESSION; + } + + // Maximum amount of bytes we want to read + bufsize = STOB(avail, pb->quality.bits_per_sample, pb->quality.channels); + + bufsize = ringbuffer_read(&buf, bufsize, &pb->prebuf); + if (bufsize == 0) + return 0; // avail too low to actually write anything + +// DPRINTF(E_DBG, L_LAUDIO, "Draining prebuffer (read_avail=%zu, bufsize=%zu, avail=%li)\n", pb->prebuf.read_avail / 4, bufsize, avail); + + nsamp = BTOS(bufsize, pb->quality.bits_per_sample, pb->quality.channels); + + ret = snd_pcm_writei(pb->pcm, buf, nsamp); + + return ((ret < 0) ? ALSA_ERROR_SESSION : 0); +} + +static int +playback_write(struct alsa_playback_session *pb, struct output_buffer *obuf) { - snd_pcm_sframes_t ret; snd_pcm_sframes_t avail; snd_pcm_sframes_t delay; enum alsa_sync_state sync; double drift; double latency; bool prebuffering; + int ret; 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)) + if (quality_is_equal(&pb->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; + return -1; } - prebuffering = (as->pos < as->buffer_nsamp); + prebuffering = (pb->pos + obuf->data[i].bufsize <= pb->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; + pb->pos += buffer_write(pb, &obuf->data[i], 0); + return 0; } - // Check sync each second (or if this is first write where last_pts is zero) - if (!alsa_sync_disable && (obuf->pts.tv_sec != as->last_pts.tv_sec)) - { - ret = snd_pcm_delay(as->hdl, &delay); - if (ret == 0) - { - sync = sync_check(&drift, &latency, as, delay); - if (sync != ALSA_SYNC_OK) - sync_correct(as, drift, latency, obuf->pts, delay); - } - - as->last_pts = obuf->pts; - } - - avail = snd_pcm_avail(as->hdl); - - ret = buffer_write(as, &obuf->data[i], avail); + ret = snd_pcm_avail_delay(pb->pcm, &avail, &delay); if (ret < 0) goto alsa_error; - as->pos += ret; + // Check sync each second (or if this is first write where last_pts is zero) + if (!alsa_sync_disable && (obuf->pts.tv_sec != pb->last_pts.tv_sec)) + { + sync = sync_check(&drift, &latency, pb, delay); + if (sync != ALSA_SYNC_OK) + sync_correct(pb, drift, latency, obuf->pts, delay); - return; + pb->last_pts = obuf->pts; + } + + ret = buffer_write(pb, &obuf->data[i], avail); + if (ret < 0) + goto alsa_error; + + pb->pos += ret; + + return 0; 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_WARN, L_LAUDIO, "ALSA buffer underrun, restarting session\n"); + return ALSA_ERROR_UNDERRUN; } DPRINTF(E_LOG, L_LAUDIO, "ALSA write error: %s\n", snd_strerror(ret)); - - as->state = OUTPUT_STATE_FAILED; - alsa_status(as); + return ALSA_ERROR_WRITE; } @@ -842,12 +925,11 @@ alsa_session_free(struct alsa_session *as) if (!as) return; - device_close(as); - outputs_quality_unsubscribe(&alsa_fallback_quality); - ringbuffer_free(&as->prebuf, 1); - snd_pcm_status_free(as->pcm_status); + playback_session_remove_all(as); + + mixer_close(&as->mixer, as->mixer_device_name); free(as); } @@ -880,14 +962,12 @@ alsa_session_make(struct output_device *device, int callback_id) { struct alsa_session *as; cfg_t *cfg_audio; - char *errmsg; int ret; CHECK_NULL(L_LAUDIO, as = calloc(1, sizeof(struct alsa_session))); as->device_id = device->id; as->callback_id = callback_id; - as->volume = device->volume; cfg_audio = cfg_getsec(cfg, "audio"); @@ -904,30 +984,18 @@ alsa_session_make(struct output_device *device, int callback_id) as->offset_ms = 1000 * (as->offset_ms/abs(as->offset_ms)); } - CHECK_NULL(L_LAUDIO, as->latency_history = calloc(alsa_latency_history_size, sizeof(double))); - - 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); + ret = mixer_open(&as->mixer, as->mixer_device_name, as->mixer_name); if (ret < 0) { - DPRINTF(E_LOG, L_LAUDIO, "%s\n", errmsg); - free(errmsg); - goto out_device_close; + DPRINTF(E_LOG, L_LAUDIO, "Could not open mixer '%s' ('%s')\n", as->mixer_device_name, as->mixer_name); + goto error_free_session; } - // 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; + goto error_mixer_close; } as->state = OUTPUT_STATE_CONNECTED; @@ -939,9 +1007,9 @@ alsa_session_make(struct output_device *device, int callback_id) return as; - out_device_close: - device_close(as); - out_free_session: + error_mixer_close: + mixer_close(&as->mixer, as->mixer_device_name); + error_free_session: free(as); return NULL; } @@ -991,9 +1059,7 @@ alsa_device_flush(struct output_device *device, int callback_id) { struct alsa_session *as = device->session; - snd_pcm_drop(as->hdl); - - ringbuffer_free(&as->prebuf, 1); + playback_session_remove_all(as); as->callback_id = callback_id; as->state = OUTPUT_STATE_CONNECTED; @@ -1021,34 +1087,11 @@ static int alsa_device_volume_set(struct output_device *device, int callback_id) { struct alsa_session *as = device->session; - int pcm_vol; if (!as) return 0; - snd_mixer_handle_events(as->mixer_hdl); - - if (!snd_mixer_selem_is_active(as->vol_elem)) - return 0; - - switch (device->volume) - { - case 0: - pcm_vol = as->vol_min; - break; - - case 100: - pcm_vol = as->vol_max; - break; - - default: - 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(as->vol_elem, pcm_vol); + volume_set(&as->mixer, device->volume); as->callback_id = callback_id; alsa_status(as); @@ -1068,19 +1111,59 @@ static void alsa_write(struct output_buffer *obuf) { struct alsa_session *as; - struct alsa_session *next; + struct alsa_session *as_next; + struct alsa_playback_session *pb; + struct alsa_playback_session *pb_next; + bool quality_changed; + int ret; - for (as = sessions; as; as = next) + quality_changed = !quality_is_equal(&obuf->data[0].quality, &alsa_last_quality); + alsa_last_quality = obuf->data[0].quality; + + for (as = sessions; as; as = as->next) { - 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); + if (quality_changed || as->state == OUTPUT_STATE_CONNECTED) + { + ret = playback_session_add(as, &obuf->data[0].quality, obuf->pts); + if (ret < 0) + { + as->state = OUTPUT_STATE_FAILED; + continue; + } - playback_write(as, obuf); + as->state = OUTPUT_STATE_STREAMING; + } - alsa_last_quality = obuf->data[0].quality; + for (pb = as->pb; pb; pb = pb_next) + { + pb_next = pb->next; + + // If !pb_next then it means that it is the most recent session, so it + // is setup with the quality level that matches obuf. The other pb's + // may still have data that needs to be written before removal. + if (!pb_next) + ret = playback_write(pb, obuf); + else + ret = playback_drain(pb); + + if (ret < 0) + { + playback_session_remove(as, pb); // pb becomes invalid + if (ret == ALSA_ERROR_WRITE) + as->state = OUTPUT_STATE_FAILED; + else if (ret == ALSA_ERROR_UNDERRUN) + as->state = OUTPUT_STATE_CONNECTED; + } + } + } + + // Cleanup failed sessions + for (as = sessions; as; as = as_next) + { + as_next = as->next; + + if (as->state == OUTPUT_STATE_FAILED) + alsa_status(as); // as becomes invalid } }