From c5bb83480d0352a25d2a89714bdba2c0c163cb0d Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 4 Apr 2016 16:58:07 +0200 Subject: [PATCH] [alsa] Add rewritten ALSA to generic outputs interface --- src/Makefile.am | 2 +- src/laudio.c | 6 - src/outputs.c | 8 +- src/outputs.h | 34 +- src/outputs/alsa.c | 934 +++++++++++++++++++++++++++++++++++++++++++++ src/player.c | 28 +- 6 files changed, 992 insertions(+), 20 deletions(-) create mode 100644 src/outputs/alsa.c diff --git a/src/Makefile.am b/src/Makefile.am index 168c619f..f3cee113 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -26,7 +26,7 @@ MPD_SRC=mpd.c mpd.h endif if COND_ALSA -ALSA_SRC=laudio_alsa.c +ALSA_SRC=outputs/alsa.c endif if COND_OSS4 diff --git a/src/laudio.c b/src/laudio.c index 31943479..a86ad9db 100644 --- a/src/laudio.c +++ b/src/laudio.c @@ -33,9 +33,6 @@ #include "player.h" #include "laudio.h" -#ifdef ALSA -extern audio_output audio_alsa; -#endif #ifdef OSS4 extern audio_output audio_oss4; #endif @@ -43,9 +40,6 @@ extern audio_output audio_oss4; extern audio_output audio_dummy; static audio_output *outputs[] = { -#ifdef ALSA - &audio_alsa, -#endif #ifdef OSS4 &audio_oss4, #endif diff --git a/src/outputs.c b/src/outputs.c index 01356357..fc720928 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -32,14 +32,14 @@ #include "outputs.h" extern struct output_definition output_raop; +extern struct output_definition output_streaming; #ifdef CHROMECAST extern struct output_definition output_cast; #endif -extern struct output_definition output_streaming; -/* TODO #ifdef ALSA extern struct output_definition output_alsa; #endif +/* TODO #ifdef OSS4 extern struct output_definition output_oss4; #endif @@ -49,14 +49,14 @@ extern struct output_definition output_dummy; // Must be in sync with enum output_types static struct output_definition *outputs[] = { &output_raop, + &output_streaming, #ifdef CHROMECAST &output_cast, #endif - &output_streaming, -/* TODO #ifdef ALSA &output_alsa, #endif +/* TODO #ifdef OSS4 &output_oss4, #endif diff --git a/src/outputs.h b/src/outputs.h index ca21e457..0c5bf1e2 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -14,18 +14,48 @@ * When a device is started the output backend will typically create a session. * This session is only passed around as an opaque object in this interface. * + * Here is the sequence of commands from the player to the outputs, and the + * callback from the output once the command has been executed. Commands marked + * with * may make multiple callbacks if multiple sessions are affected. + * (TODO should callbacks always be deferred?) + * + * PLAYER OUTPUT PLAYER CB + * speaker_activate -> device_start -> device_activate_cb + * -> (if playback) -> playback_start -> device_streaming_cb* (or no cb) + * -> (else if playback not active) -> device_streaming_cb + * -> (fail) -> device_stop -> device_lost_cb + * speaker_activate -> device_probe -> device_probe_cb + * speaker_deactivate -> device_stop -> device_shutdown_cb + * volume_set -> device_volume_set -> device_command_cb + * -> -> device_streaming_cb + * (volume_setrel/abs_speaker is the same) + * playback_start_item -> device_start -> device_restart_cb + * -> (success) -> device_streaming_cb + * -> (fail) -> device_stop -> device_lost_cb + * playback_start_bh -> playback_start -> device_streaming_cb* (or no cb) + * playback_stop -> flush -> device_command_cb* + * -> -> device_streaming_cb* + * -> -> playback_stop -> device_streaming_cb* + * playback_pause -> flush -> device_command_cb* + * -> -> device_streaming_cb* + * -> -> playback_stop -> device_streaming_cb* + * playback_abort -> playback_stop -> device_streaming_cb* (or no cb) + * device_streaming_cb -> device_streaming_cb (re-add) + * */ // Must be in sync with outputs[] in outputs.c enum output_types { OUTPUT_TYPE_RAOP, + OUTPUT_TYPE_STREAMING, #ifdef CHROMECAST OUTPUT_TYPE_CAST, #endif - OUTPUT_TYPE_STREAMING, -/* TODO +#ifdef ALSA OUTPUT_TYPE_ALSA, +#endif +/* TODO OUTPUT_TYPE_OSS, OUTPUT_TYPE_DUMMY, */ diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c new file mode 100644 index 00000000..3a7a2139 --- /dev/null +++ b/src/outputs/alsa.c @@ -0,0 +1,934 @@ +/* + * Copyright (C) 2015-2016 Espen Jürgensen + * Copyright (C) 2010 Julien BLACHE + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "conffile.h" +#include "logger.h" +#include "player.h" +#include "outputs.h" + +#define ALSA_PACKETS_WRITE_MAX 10 +#define PACKET_SIZE STOB(AIRTUNES_V2_PACKET_SAMPLES) + +// TODO Unglobalise these and add support for multiple sound cards +static char *card_name; +static char *mixer_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; + +#define ALSA_F_STARTED (1 << 15) + +enum alsa_state +{ + ALSA_STATE_STOPPED = 0, + ALSA_STATE_STARTED = ALSA_F_STARTED, + ALSA_STATE_STREAMING = ALSA_F_STARTED | 0x01, + + ALSA_STATE_FAILED = -1, +}; + +struct alsa_session +{ + enum alsa_state state; + + char *devname; + + uint64_t pos; + uint64_t start_pos; + + // 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; + + int volume; + + struct event *deferredev; + output_status_cb defer_cb; + + /* Do not dereference - only passed to the status cb */ + struct output_device *device; + struct output_session *output_session; + output_status_cb status_cb; + + struct alsa_session *next; +}; + +/* From player.c */ +extern struct event_base *evbase_player; + +static struct alsa_session *sessions; + +/* Forwards */ +static void +defer_cb(int fd, short what, void *arg); + +/* ---------------------------- 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) +{ + event_free(as->deferredev); + + prebuf_free(as); + + free(as->output_session); + free(as); + + as = NULL; +} + +static void +alsa_session_cleanup(struct alsa_session *as) +{ + struct alsa_session *s; + + if (as == sessions) + sessions = sessions->next; + else + { + for (s = sessions; s && (s->next != as); s = s->next) + ; /* EMPTY */ + + if (!s) + DPRINTF(E_WARN, L_LAUDIO, "WARNING: struct alsa_session not found in list; BUG!\n"); + else + s->next = as->next; + } + + alsa_session_free(as); +} + +static struct alsa_session * +alsa_session_make(struct output_device *device, output_status_cb cb) +{ + struct output_session *os; + struct alsa_session *as; + + os = calloc(1, sizeof(struct output_session)); + as = calloc(1, sizeof(struct alsa_session)); + if (!os || !as) + { + DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA session\n"); + return NULL; + } + + as->deferredev = evtimer_new(evbase_player, defer_cb, as); + if (!as->deferredev) + { + DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA deferred event\n"); + free(os); + free(as); + return NULL; + } + + os->session = as; + os->type = device->type; + + as->output_session = os; + as->state = ALSA_STATE_STOPPED; + as->device = device; + as->status_cb = cb; + as->volume = device->volume; + as->devname = card_name; + + as->next = sessions; + sessions = as; + + return as; +} + + +/* ---------------------------- 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; +} + + +/* ------------------------------- 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, card_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(void) +{ + 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, 44100, 0); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Hardware doesn't support 44.1 kHz: %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(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; + char *debug_pcm_cfg; + int ret; + + ret = snd_pcm_prepare(hdl); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not prepare ALSA device '%s': %s\n", as->devname, snd_strerror(ret)); + return; + } + + // Clear prebuffer in case start somehow got called twice without a stop in between + prebuf_free(as); + + // 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) / AIRTUNES_V2_PACKET_SAMPLES + 1; + if (as->prebuf_len > 2*126) + { + 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 * 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; + + // 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; +} + +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 nsamp; + uint8_t *pkt; + int prebuffering; + int prebuf_empty; + int npackets; + + prebuffering = (as->pos < as->start_pos); + prebuf_empty = (as->prebuf_head == as->prebuf_tail); + + as->pos += AIRTUNES_V2_PACKET_SAMPLES; + + // We need to copy to the prebuffer if we are prebuffering OR if the + // prebuffer has not been emptied yet + if (prebuffering || !prebuf_empty) + { + pkt = &as->prebuf[as->prebuf_head * PACKET_SIZE]; + + memcpy(pkt, buf, PACKET_SIZE); + + as->prebuf_head = (as->prebuf_head + 1) % as->prebuf_len; + } + + if (prebuffering) + return; + + ret = snd_pcm_avail_update(hdl); + if (ret < 0) + goto alsa_error; + + avail = ret; + if (avail < AIRTUNES_V2_PACKET_SAMPLES) + return; + + // If we have data in prebuf we send as much as we can + if (!prebuf_empty) + { + buf = &as->prebuf[as->prebuf_tail * 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 * AIRTUNES_V2_PACKET_SAMPLES; + while (nsamp > avail) + { + npackets -= 1; + nsamp -= AIRTUNES_V2_PACKET_SAMPLES; + } + + as->prebuf_tail = (as->prebuf_tail + npackets) % as->prebuf_len; + } + else + nsamp = AIRTUNES_V2_PACKET_SAMPLES; + + ret = snd_pcm_writei(hdl, buf, nsamp); + if (ret < 0) + goto alsa_error; + else if (ret != nsamp) + DPRINTF(E_WARN, L_LAUDIO, "ALSA partial write detected\n"); + + 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 + AIRTUNES_V2_PACKET_SAMPLES * (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 -= AIRTUNES_V2_PACKET_SAMPLES; +} + +/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */ + +static int +alsa_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) +{ + struct alsa_session *as; + int ret; + + as = alsa_session_make(device, cb); + if (!as) + return -1; + + ret = device_open(); + if (ret < 0) + return -1; + + as->state = ALSA_STATE_STARTED; + alsa_status(as); + + return 0; +} + +static void +alsa_device_stop(struct output_session *session) +{ + struct alsa_session *as = session->session; + + device_close(); + + as->state = ALSA_STATE_STOPPED; + alsa_status(as); +} + +static int +alsa_device_probe(struct output_device *device, output_status_cb cb) +{ + struct alsa_session *as; + int ret; + + as = alsa_session_make(device, cb); + if (!as) + return -1; + + ret = device_open(); + if (ret < 0) + { + alsa_session_cleanup(as); + return -1; + } + + device_close(); + + as->state = ALSA_STATE_STOPPED; + alsa_status(as); + + return 0; +} + +static int +alsa_device_volume_set(struct output_device *device, output_status_cb cb) +{ + struct alsa_session *as; + int pcm_vol; + + if (!device->session || !device->session->session) + return 0; + + as = device->session->session; + + if (!mixer_hdl || !vol_elem) + return -1; + + snd_mixer_handle_events(mixer_hdl); + + if (!snd_mixer_selem_is_active(vol_elem)) + return -1; + + switch (device->volume) + { + case 0: + pcm_vol = vol_min; + break; + + case 100: + pcm_vol = vol_max; + break; + + default: + pcm_vol = vol_min + (device->volume * (vol_max - 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); + + alsa_status(as); + + return 0; +} + +static void +alsa_playback_start(uint64_t next_pkt, struct timespec *ts) +{ + struct alsa_session *as; + uint64_t pos; + + 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); +} + +static void +alsa_playback_stop(void) +{ + struct alsa_session *as; + + for (as = sessions; as; as = as->next) + { + snd_pcm_drop(hdl); + prebuf_free(as); + + as->state = ALSA_STATE_STARTED; + alsa_status(as); + } +} + +static void +alsa_write(uint8_t *buf, uint64_t rtptime) +{ + struct alsa_session *as; + uint64_t pos; + + for (as = sessions; as; as = as->next) + { + if (as->state == ALSA_STATE_STARTED) + { + playback_pos_get(&pos, rtptime); + + DPRINTF(E_DBG, L_LAUDIO, "Starting ALSA device '%s' (pos %" PRIu64 ", rtptime %" PRIu64 ")\n", as->devname, pos, rtptime); + + playback_start(as, pos, rtptime); + } + + playback_write(as, buf, rtptime); + } +} + +static int +alsa_flush(output_status_cb cb, uint64_t rtptime) +{ + struct alsa_session *as; + int i; + + i = 0; + for (as = sessions; as; as = as->next) + { + i++; + + snd_pcm_drop(hdl); + prebuf_free(as); + + as->status_cb = cb; + as->state = ALSA_STATE_STARTED; + alsa_status(as); + } + + return i; +} + +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; + + 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"); + nickname = cfg_getstr(cfg_audio, "nickname"); + + device = calloc(1, sizeof(struct output_device)); + if (!device) + { + DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA device\n"); + return -1; + } + + device->id = 0; + device->name = nickname; + device->type = OUTPUT_TYPE_ALSA; + device->type_name = outputs_name(device->type); + device->advertised = 1; + device->has_video = 0; + + DPRINTF(E_INFO, L_LAUDIO, "Adding ALSA device '%s' using friendly name '%s'\n", card_name, nickname); + + 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) +{ + snd_lib_error_set_handler(NULL); +} + +struct output_definition output_alsa = +{ + .name = "ALSA", + .type = OUTPUT_TYPE_ALSA, + .priority = 3, + .disabled = 0, + .init = alsa_init, + .deinit = alsa_deinit, + .device_start = alsa_device_start, + .device_stop = alsa_device_stop, + .device_probe = alsa_device_probe, + .device_volume_set = alsa_device_volume_set, + .playback_start = alsa_playback_start, + .playback_stop = alsa_playback_stop, + .write = alsa_write, + .flush = alsa_flush, + .status_cb = alsa_set_status_cb, +}; diff --git a/src/player.c b/src/player.c index 80954b58..8214d67a 100644 --- a/src/player.c +++ b/src/player.c @@ -1770,6 +1770,8 @@ device_streaming_cb(struct output_device *device, struct output_session *session { int ret; + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_streaming_cb\n", outputs_name(device->type)); + ret = device_check(device); if (ret < 0) { @@ -1814,6 +1816,8 @@ device_streaming_cb(struct output_device *device, struct output_session *session static void device_command_cb(struct output_device *device, struct output_session *session, enum output_device_state status) { + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_command_cb\n", outputs_name(device->type)); + cur_cmd->output_requests_pending--; outputs_status_cb(session, device_streaming_cb); @@ -1837,6 +1841,8 @@ device_shutdown_cb(struct output_device *device, struct output_session *session, { int ret; + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_shutdown_cb\n", outputs_name(device->type)); + cur_cmd->output_requests_pending--; if (output_sessions) @@ -1871,6 +1877,8 @@ device_shutdown_cb(struct output_device *device, struct output_session *session, static void device_lost_cb(struct output_device *device, struct output_session *session, enum output_device_state status) { + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_lost_cb\n", outputs_name(device->type)); + /* We lost that device during startup for some reason, not much we can do here */ if (status == OUTPUT_STATE_FAILED) DPRINTF(E_WARN, L_PLAYER, "Failed to stop lost device\n"); @@ -1884,6 +1892,8 @@ device_activate_cb(struct output_device *device, struct output_session *session, struct timespec ts; int ret; + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_activate_cb\n", outputs_name(device->type)); + cur_cmd->output_requests_pending--; ret = device_check(device); @@ -1955,6 +1965,8 @@ device_probe_cb(struct output_device *device, struct output_session *session, en { int ret; + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_probe_cb\n", outputs_name(device->type)); + cur_cmd->output_requests_pending--; ret = device_check(device); @@ -2002,6 +2014,8 @@ device_restart_cb(struct output_device *device, struct output_session *session, { int ret; + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_restart_cb\n", outputs_name(device->type)); + cur_cmd->output_requests_pending--; ret = device_check(device); @@ -2233,7 +2247,7 @@ playback_stop(struct player_command *cmd) metadata_purge(); - /* We're async if we need to flush RAOP devices */ + /* We're async if we need to flush devices */ if (cmd->output_requests_pending > 0) return 1; /* async */ @@ -2408,7 +2422,7 @@ playback_start_item(struct player_command *cmd, struct queue_item *qii) return -1; } - /* We're async if we need to start RAOP devices */ + /* We're async if we need to start devices */ if (cmd->output_requests_pending > 0) return 1; /* async */ @@ -2663,7 +2677,7 @@ playback_pause(struct player_command *cmd) metadata_purge(); - /* We're async if we need to flush RAOP devices */ + /* We're async if we need to flush devices */ if (cmd->output_requests_pending > 0) return 1; /* async */ @@ -2809,8 +2823,8 @@ speaker_set(struct player_command *cmd) if (cmd->ret != -2) cmd->ret = -1; } - - cmd->output_requests_pending += 1; + else + cmd->output_requests_pending++; } } else @@ -2830,8 +2844,8 @@ speaker_set(struct player_command *cmd) if (cmd->ret != -2) cmd->ret = -1; } - - cmd->output_requests_pending += 1; + else + cmd->output_requests_pending++; } } }