owntone-server/src/outputs/alsa.c

1075 lines
27 KiB
C
Raw Normal View History

/*
* Copyright (C) 2015-2019 Espen Jürgensen <espenjurgensen@gmail.com>
* Copyright (C) 2010 Julien BLACHE <jb@jblache.org>
*
* 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 <config.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <inttypes.h>
#include <asoundlib.h>
#include "misc.h"
#include "conffile.h"
#include "logger.h"
#include "player.h"
#include "outputs.h"
// Same as Airplay - maybe not optimal?
#define ALSA_SAMPLES_PER_PACKET 352
#define ALSA_PACKET_SIZE STOB(ALSA_SAMPLES_PER_PACKET, 16, 2)
// The maximum number of samples that the output is allowed to get behind (or
// ahead) of the player position, before compensation is attempted
#define ALSA_MAX_LATENCY 352
// If latency is jumping up and down we don't do compensation since we probably
// wouldn't do a good job. This sets the maximum the latency is allowed to vary
// within the 10 seconds where we measure latency each second.
#define ALSA_MAX_LATENCY_VARIANCE 352
#define ALSA_F_STARTED (1 << 15)
enum alsa_sync_state
{
ALSA_SYNC_OK,
ALSA_SYNC_AHEAD,
ALSA_SYNC_BEHIND,
};
struct alsa_session
{
enum output_device_state state;
[-] Fix alsa.c null pointer deref + some minor bugs and do some housekeeping Thanks to Denis Denisov and cppcheck for notifying about the below. The leaks are edge cases, but the warning of dereference of avail in alsa.c points at a bug that could probably cause actual crashes. [src/evrtsp/rtsp.c:1352]: (warning) Assignment of function parameter has no effect outside the function. Did you forget dereferencing it? [src/httpd_daap.c:228]: (error) Memory leak: s [src/library.c:280]: (warning) %d in format string (no. 2) requires 'int' but the argument type is 'unsigned int'. [src/library.c:284]: (warning) %d in format string (no. 2) requires 'int' but the argument type is 'unsigned int'. [src/library/filescanner_playlist.c:251]: (error) Resource leak: fp [src/library/filescanner_playlist.c:273]: (error) Resource leak: fp [src/outputs/alsa.c:143]: (warning) Assignment of function parameter has no effect outside the function. Did you forget dereferencing it? [src/outputs/alsa.c:657]: (warning) Possible null pointer dereference: avail [src/outputs/dummy.c:75]: (warning) Assignment of function parameter has no effect outside the function. Did you forget dereferencing it? [src/outputs/fifo.c:245]: (warning) Assignment of function parameter has no effect outside the function. Did you forget dereferencing it? [src/outputs/raop.c:1806]: (warning) Assignment of function parameter has no effect outside the function. Did you forget dereferencing it? [src/outputs/raop.c:1371]: (warning) %u in format string (no. 1) requires 'unsigned int' but the argument type is 'signed int'. [src/outputs/raop.c:1471]: (warning) %u in format string (no. 1) requires 'unsigned int' but the argument type is 'signed int'. [src/outputs/raop_verification.c:705] -> [src/outputs/raop_verification.c:667]: (warning) Either the condition 'if(len_M)' is redundant or there is possible null pointer dereference: len_M.
2017-10-05 16:13:01 -04:00
uint64_t device_id;
int callback_id;
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;
// Here we buffer samples during startup
struct ringbuffer prebuf;
int offset;
int adjust_period_seconds;
int volume;
long vol_min;
long vol_max;
snd_pcm_t *hdl;
snd_mixer_t *mixer_hdl;
snd_mixer_elem_t *vol_elem;
struct alsa_session *next;
};
static struct alsa_session *sessions;
// 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
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
2016-04-06 03:09:28 -04:00
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;
2019-02-22 03:12:38 -05:00
DPRINTF(E_DBG, L_LAUDIO, "Sync=%d, pos=%" PRIu64 ", as->pos=%u, dev_pos=%" PRIu64 ", latency=%d, delay=%li, avail=%li, elapsed=%" PRIu64 ", dev_elapsed=%" PRIu64 "\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
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);
free(as);
}
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;
}
outputs_device_session_remove(as->device_id);
alsa_session_free(as);
}
static struct alsa_session *
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->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, "The ALSA offset (%d) set in the configuration is out of bounds\n", as->offset);
as->offset = 44100 * (as->offset/abs(as->offset));
}
// 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;
// as is now the official device session
outputs_device_session_add(device->id, as);
return as;
out_device_close:
device_close(as);
out_free_session:
free(as);
return NULL;
}
static void
alsa_status(struct alsa_session *as)
{
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);
}
/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */
static int
alsa_device_start(struct output_device *device, int callback_id)
{
struct alsa_session *as;
as = alsa_session_make(device, callback_id);
if (!as)
return -1;
as->state = OUTPUT_STATE_CONNECTED;
alsa_status(as);
return 0;
}
static int
alsa_device_stop(struct output_device *device, int callback_id)
{
struct alsa_session *as = device->session;
as->callback_id = callback_id;
as->state = OUTPUT_STATE_STOPPED;
alsa_status(as); // Will terminate the session since the state is STOPPED
return 0;
}
static int
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);
as->callback_id = callback_id;
as->state = OUTPUT_STATE_CONNECTED;
alsa_status(as);
return 0;
}
static int
alsa_device_probe(struct output_device *device, int callback_id)
{
struct alsa_session *as;
as = alsa_session_make(device, callback_id);
if (!as)
return -1;
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, int callback_id)
{
struct alsa_session *as = device->session;
int pcm_vol;
if (!as)
2016-04-04 17:42:52 -04:00
return 0;
snd_mixer_handle_events(as->mixer_hdl);
if (!snd_mixer_selem_is_active(as->vol_elem))
2016-04-04 17:42:52 -04:00
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);
as->callback_id = callback_id;
alsa_status(as);
2016-04-04 17:42:52 -04:00
return 1;
}
static void
alsa_device_cb_set(struct output_device *device, int callback_id)
{
struct alsa_session *as = device->session;
as->callback_id = callback_id;
}
static void
alsa_playback_stop(void)
{
struct alsa_session *as;
struct alsa_session *next;
for (as = sessions; as; as = next)
{
next = as->next;
snd_pcm_drop(as->hdl);
as->state = OUTPUT_STATE_STOPPED;
alsa_status(as); // Will stop the session
}
}
static void
alsa_write(struct output_buffer *obuf)
{
struct alsa_session *as;
struct alsa_session *next;
for (as = sessions; 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);
playback_write(as, obuf);
alsa_last_quality = obuf->data[0].quality;
}
}
static int
alsa_init(void)
{
struct output_device *device;
cfg_t *cfg_audio;
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;
CHECK_NULL(L_LAUDIO, device = calloc(1, sizeof(struct output_device)));
device->id = 0;
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", cfg_getstr(cfg_audio, "card"), device->name);
player_device_add(device);
snd_lib_error_set_handler(logger_alsa);
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 =
{
.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_flush = alsa_device_flush,
.device_probe = alsa_device_probe,
.device_volume_set = alsa_device_volume_set,
.device_cb_set = alsa_device_cb_set,
.playback_stop = alsa_playback_stop,
.write = alsa_write,
};