2016-04-04 10:58:07 -04:00
|
|
|
/*
|
2019-02-22 02:41:33 -05:00
|
|
|
* Copyright (C) 2015-2019 Espen Jürgensen <espenjurgensen@gmail.com>
|
2016-04-04 10:58:07 -04:00
|
|
|
* 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>
|
|
|
|
|
2016-12-26 13:29:47 -05:00
|
|
|
#include "misc.h"
|
2016-04-04 10:58:07 -04:00
|
|
|
#include "conffile.h"
|
|
|
|
#include "logger.h"
|
|
|
|
#include "player.h"
|
|
|
|
#include "outputs.h"
|
|
|
|
|
2019-04-07 18:50:20 -04:00
|
|
|
// We measure latency each second, and after a number of measurements determined
|
|
|
|
// by adjust_period_seconds we try to determine drift and latency. If both are
|
|
|
|
// below the two thresholds set by the below, we don't do anything. Otherwise we
|
|
|
|
// may attempt compensation by resampling. Latency is measured in samples, and
|
|
|
|
// drift is change of latency per second. Both are floats.
|
|
|
|
#define ALSA_MAX_LATENCY 480.0
|
|
|
|
#define ALSA_MAX_DRIFT 16.0
|
2016-04-07 17:35:34 -04:00
|
|
|
// If latency is jumping up and down we don't do compensation since we probably
|
2019-04-07 18:50:20 -04:00
|
|
|
// wouldn't do a good job. We use linear regression to determine the trend, but
|
|
|
|
// if r2 is below this value we won't attempt to correct sync.
|
2019-04-10 16:38:48 -04:00
|
|
|
#define ALSA_MAX_VARIANCE 0.3
|
2019-04-07 18:50:20 -04:00
|
|
|
|
|
|
|
// We correct latency by adjusting the sample rate in steps. However, if the
|
|
|
|
// latency keeps drifting we give up after reaching this step.
|
|
|
|
#define ALSA_RESAMPLE_STEP_MAX 8
|
|
|
|
// The sample rate gets adjusted by a multiple of this number. The number of
|
|
|
|
// multiples depends on the sample rate, i.e. a low sample rate may get stepped
|
|
|
|
// by 16, while high one would get stepped by 4 x 16
|
|
|
|
#define ALSA_RESAMPLE_STEP_MULTIPLE 2
|
2016-04-04 10:58:07 -04:00
|
|
|
|
|
|
|
#define ALSA_F_STARTED (1 << 15)
|
|
|
|
|
2016-04-07 17:35:34 -04:00
|
|
|
enum alsa_sync_state
|
|
|
|
{
|
|
|
|
ALSA_SYNC_OK,
|
|
|
|
ALSA_SYNC_AHEAD,
|
|
|
|
ALSA_SYNC_BEHIND,
|
|
|
|
};
|
|
|
|
|
2016-04-04 10:58:07 -04:00
|
|
|
struct alsa_session
|
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
enum output_device_state state;
|
2017-10-05 16:13:01 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
uint64_t device_id;
|
|
|
|
int callback_id;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
const char *devname;
|
|
|
|
const char *card_name;
|
|
|
|
const char *mixer_name;
|
|
|
|
const char *mixer_device_name;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
snd_pcm_status_t *pcm_status;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
struct media_quality quality;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
int buffer_nsamp;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
uint32_t pos;
|
2019-02-23 17:24:36 -05:00
|
|
|
|
|
|
|
uint32_t last_pos;
|
|
|
|
uint32_t last_buflen;
|
2019-02-10 17:27:29 -05:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
struct timespec last_pts;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-04-07 18:50:20 -04:00
|
|
|
// Used for syncing with the clock
|
|
|
|
struct timespec stamp_pts;
|
|
|
|
uint64_t stamp_pos;
|
|
|
|
|
|
|
|
// Array of latency calculations, where latency_counter tells how many are
|
|
|
|
// currently in the array
|
2019-04-09 15:45:16 -04:00
|
|
|
double *latency_history;
|
2019-04-07 18:50:20 -04:00
|
|
|
int latency_counter;
|
|
|
|
|
|
|
|
int sync_resample_step;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
// Here we buffer samples during startup
|
|
|
|
struct ringbuffer prebuf;
|
2018-10-14 14:40:36 -04:00
|
|
|
|
2019-02-23 17:24:36 -05:00
|
|
|
int offset_ms;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
int volume;
|
|
|
|
long vol_min;
|
|
|
|
long vol_max;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
snd_pcm_t *hdl;
|
|
|
|
snd_mixer_t *mixer_hdl;
|
|
|
|
snd_mixer_elem_t *vol_elem;
|
2019-02-10 17:27:29 -05:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
struct alsa_session *next;
|
|
|
|
};
|
2019-02-10 17:27:29 -05:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
static struct alsa_session *sessions;
|
2018-10-14 14:40:36 -04:00
|
|
|
|
2019-04-09 15:45:16 -04:00
|
|
|
static bool alsa_sync_disable;
|
|
|
|
static int alsa_latency_history_size;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
// 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;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
/* -------------------------------- FORWARDS -------------------------------- */
|
2016-04-04 10:58:07 -04:00
|
|
|
|
|
|
|
static void
|
2019-02-22 02:41:33 -05:00
|
|
|
alsa_status(struct alsa_session *as);
|
2016-04-04 10:58:07 -04:00
|
|
|
|
|
|
|
|
|
|
|
/* ------------------------------- MISC HELPERS ----------------------------- */
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
static void
|
|
|
|
dump_config(struct alsa_session *as)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
snd_output_t *output;
|
|
|
|
char *debug_pcm_cfg;
|
2016-04-04 10:58:07 -04:00
|
|
|
int ret;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
// Dump PCM config data for E_DBG logging
|
|
|
|
ret = snd_output_buffer_open(&output);
|
|
|
|
if (ret == 0)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
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);
|
|
|
|
}
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
snd_output_close(output);
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-12 13:40:33 -04:00
|
|
|
static snd_pcm_format_t
|
|
|
|
bps2format(int bits_per_sample)
|
|
|
|
{
|
|
|
|
if (bits_per_sample == 16)
|
|
|
|
return SND_PCM_FORMAT_S16_LE;
|
|
|
|
else if (bits_per_sample == 24)
|
|
|
|
return SND_PCM_FORMAT_S24_LE;
|
|
|
|
else if (bits_per_sample == 32)
|
|
|
|
return SND_PCM_FORMAT_S32_LE;
|
|
|
|
else
|
|
|
|
return SND_PCM_FORMAT_UNKNOWN;
|
|
|
|
}
|
|
|
|
|
2016-04-04 10:58:07 -04:00
|
|
|
static int
|
2019-02-22 02:41:33 -05:00
|
|
|
mixer_open(struct alsa_session *as)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
|
|
|
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;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ret = snd_mixer_open(&as->mixer_hdl, 0);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (ret < 0)
|
|
|
|
{
|
|
|
|
DPRINTF(E_LOG, L_LAUDIO, "Failed to open mixer: %s\n", snd_strerror(ret));
|
2019-02-22 02:41:33 -05:00
|
|
|
as->mixer_hdl = NULL;
|
2016-04-04 10:58:07 -04:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ret = snd_mixer_attach(as->mixer_hdl, as->mixer_device_name);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (ret < 0)
|
|
|
|
{
|
|
|
|
DPRINTF(E_LOG, L_LAUDIO, "Failed to attach mixer: %s\n", snd_strerror(ret));
|
|
|
|
goto out_close;
|
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ret = snd_mixer_selem_register(as->mixer_hdl, NULL, NULL);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (ret < 0)
|
|
|
|
{
|
|
|
|
DPRINTF(E_LOG, L_LAUDIO, "Failed to register mixer: %s\n", snd_strerror(ret));
|
|
|
|
goto out_detach;
|
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ret = snd_mixer_load(as->mixer_hdl);
|
2016-04-04 10:58:07 -04:00
|
|
|
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;
|
2019-02-22 02:41:33 -05:00
|
|
|
for (elem = snd_mixer_first_elem(as->mixer_hdl); elem; elem = snd_mixer_elem_next(elem))
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
|
|
|
snd_mixer_selem_get_id(elem, sid);
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
if (as->mixer_name && (strcmp(snd_mixer_selem_id_get_name(sid), as->mixer_name) == 0))
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
if (as->mixer_name)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
|
|
|
if (custom)
|
2019-02-22 02:41:33 -05:00
|
|
|
as->vol_elem = custom;
|
2016-04-04 10:58:07 -04:00
|
|
|
else
|
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
DPRINTF(E_LOG, L_LAUDIO, "Failed to open configured mixer element '%s'\n", as->mixer_name);
|
2016-04-04 10:58:07 -04:00
|
|
|
|
|
|
|
goto out_detach;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (pcm)
|
2019-02-22 02:41:33 -05:00
|
|
|
as->vol_elem = pcm;
|
2016-04-04 10:58:07 -04:00
|
|
|
else if (master)
|
2019-02-22 02:41:33 -05:00
|
|
|
as->vol_elem = master;
|
2016-04-04 10:58:07 -04:00
|
|
|
else
|
|
|
|
{
|
|
|
|
DPRINTF(E_LOG, L_LAUDIO, "Failed to open PCM or Master mixer element\n");
|
|
|
|
|
|
|
|
goto out_detach;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get min & max volume
|
2019-02-22 02:41:33 -05:00
|
|
|
snd_mixer_selem_get_playback_volume_range(as->vol_elem, &as->vol_min, &as->vol_max);
|
2016-04-04 10:58:07 -04:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
out_detach:
|
2019-02-22 02:41:33 -05:00
|
|
|
snd_mixer_detach(as->mixer_hdl, as->devname);
|
2016-04-04 10:58:07 -04:00
|
|
|
out_close:
|
2019-02-22 02:41:33 -05:00
|
|
|
snd_mixer_close(as->mixer_hdl);
|
|
|
|
as->mixer_hdl = NULL;
|
|
|
|
as->vol_elem = NULL;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
2016-04-06 03:09:28 -04:00
|
|
|
device_open(struct alsa_session *as)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
|
|
|
snd_pcm_hw_params_t *hw_params;
|
|
|
|
snd_pcm_uframes_t bufsize;
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
hw_params = NULL;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ret = snd_pcm_open(&as->hdl, as->devname, SND_PCM_STREAM_PLAYBACK, 0);
|
2016-04-04 10:58:07 -04:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ret = snd_pcm_hw_params_any(as->hdl, hw_params);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (ret < 0)
|
|
|
|
{
|
|
|
|
DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve hw params: %s\n", snd_strerror(ret));
|
|
|
|
goto out_fail;
|
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ret = snd_pcm_hw_params_set_access(as->hdl, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (ret < 0)
|
|
|
|
{
|
|
|
|
DPRINTF(E_LOG, L_LAUDIO, "Could not set access method: %s\n", snd_strerror(ret));
|
|
|
|
goto out_fail;
|
|
|
|
}
|
|
|
|
|
2019-04-12 13:40:33 -04:00
|
|
|
// 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));
|
|
|
|
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));
|
|
|
|
|
|
|
|
goto out_fail;
|
|
|
|
}
|
|
|
|
|
|
|
|
ret = snd_pcm_hw_params_set_channels(as->hdl, hw_params, alsa_fallback_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);
|
|
|
|
if (ret < 0)
|
|
|
|
{
|
|
|
|
DPRINTF(E_LOG, L_LAUDIO, "Hardware doesn't support %u Hz: %s\n", alsa_fallback_quality.sample_rate, snd_strerror(ret));
|
|
|
|
|
|
|
|
goto out_fail;
|
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ret = snd_pcm_hw_params_get_buffer_size_max(hw_params, &bufsize);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (ret < 0)
|
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
DPRINTF(E_LOG, L_LAUDIO, "Could not get max buffer size: %s\n", snd_strerror(ret));
|
|
|
|
goto out_fail;
|
|
|
|
}
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
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));
|
2016-04-04 10:58:07 -04:00
|
|
|
goto out_fail;
|
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ret = snd_pcm_hw_params(as->hdl, hw_params);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (ret < 0)
|
|
|
|
{
|
2019-04-12 13:40:33 -04:00
|
|
|
DPRINTF(E_LOG, L_LAUDIO, "Could not set hw params in device_open(): %s\n", snd_strerror(ret));
|
2019-02-22 02:41:33 -05:00
|
|
|
goto out_fail;
|
|
|
|
}
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
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");
|
2016-04-04 10:58:07 -04:00
|
|
|
goto out_fail;
|
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
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;
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
ret = snd_pcm_hw_params_malloc(&hw_params);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (ret < 0)
|
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
*errmsg = safe_asprintf("Could not allocate hw params: %s", snd_strerror(ret));
|
|
|
|
return -1;
|
|
|
|
}
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
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;
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
|
2019-04-12 13:40:33 -04:00
|
|
|
// 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);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (ret < 0)
|
|
|
|
{
|
2019-04-12 13:40:33 -04:00
|
|
|
*errmsg = safe_asprintf("Could not set access method: %s\n", snd_strerror(ret));
|
2019-02-22 02:41:33 -05:00
|
|
|
goto free_params;
|
|
|
|
}
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-04-12 13:40:33 -04:00
|
|
|
ret = snd_pcm_hw_params_set_rate(as->hdl, hw_params, quality->sample_rate, 0);
|
|
|
|
if (ret < 0)
|
2019-02-22 02:41:33 -05:00
|
|
|
{
|
2019-04-12 13:40:33 -04:00
|
|
|
*errmsg = safe_asprintf("Hardware doesn't support %d Hz: %s", quality->sample_rate, snd_strerror(ret));
|
|
|
|
goto free_params;
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
|
2019-04-12 13:40:33 -04:00
|
|
|
ret = snd_pcm_hw_params_set_format(as->hdl, hw_params, bps2format(quality->bits_per_sample));
|
2016-04-04 10:58:07 -04:00
|
|
|
if (ret < 0)
|
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
*errmsg = safe_asprintf("Could not set %d bits per sample: %s", quality->bits_per_sample, snd_strerror(ret));
|
|
|
|
goto free_params;
|
|
|
|
}
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
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;
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ret = snd_pcm_hw_params(as->hdl, hw_params);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (ret < 0)
|
|
|
|
{
|
2019-04-12 13:40:33 -04:00
|
|
|
*errmsg = safe_asprintf("Could not set hw params in device_quality_set(): %s\n", snd_strerror(ret));
|
2019-02-22 02:41:33 -05:00
|
|
|
goto free_params;
|
|
|
|
}
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
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));
|
2016-04-04 10:58:07 -04:00
|
|
|
goto out_fail;
|
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
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;
|
|
|
|
}
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ret = snd_pcm_sw_params_set_tstamp_type(as->hdl, sw_params, SND_PCM_TSTAMP_TYPE_MONOTONIC);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (ret < 0)
|
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
DPRINTF(E_LOG, L_LAUDIO, "Could not set tstamp type: %s\n", snd_strerror(ret));
|
|
|
|
goto out_fail;
|
|
|
|
}
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
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));
|
2016-04-04 10:58:07 -04:00
|
|
|
goto out_fail;
|
|
|
|
}
|
|
|
|
|
2019-03-26 17:08:15 -04:00
|
|
|
snd_pcm_sw_params_free(sw_params);
|
|
|
|
|
2016-04-04 10:58:07 -04:00
|
|
|
return 0;
|
|
|
|
|
|
|
|
out_fail:
|
2019-02-22 02:41:33 -05:00
|
|
|
snd_pcm_sw_params_free(sw_params);
|
2016-04-04 10:58:07 -04:00
|
|
|
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2019-02-22 02:41:33 -05:00
|
|
|
device_close(struct alsa_session *as)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
snd_pcm_close(as->hdl);
|
|
|
|
as->hdl = NULL;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
if (as->mixer_hdl)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
snd_mixer_detach(as->mixer_hdl, as->devname);
|
|
|
|
snd_mixer_close(as->mixer_hdl);
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
as->mixer_hdl = NULL;
|
|
|
|
as->vol_elem = NULL;
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2019-02-22 02:41:33 -05:00
|
|
|
playback_restart(struct alsa_session *as, struct output_buffer *obuf)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
2019-02-23 17:24:36 -05:00
|
|
|
struct timespec ts;
|
2016-04-12 17:23:29 -04:00
|
|
|
snd_pcm_state_t state;
|
2019-02-23 17:24:36 -05:00
|
|
|
snd_pcm_sframes_t offset_nsamp;
|
2019-02-22 02:41:33 -05:00
|
|
|
size_t size;
|
|
|
|
char *errmsg;
|
2016-04-04 10:58:07 -04:00
|
|
|
int ret;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
DPRINTF(E_INFO, L_LAUDIO, "Starting ALSA device '%s'\n", as->devname);
|
|
|
|
|
|
|
|
state = snd_pcm_state(as->hdl);
|
2016-04-12 17:23:29 -04:00
|
|
|
if (state != SND_PCM_STATE_PREPARED)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
2016-04-12 17:23:29 -04:00
|
|
|
if (state == SND_PCM_STATE_RUNNING)
|
2019-02-22 02:41:33 -05:00
|
|
|
snd_pcm_drop(as->hdl); // FIXME not great to do this during playback - would mean new quality drops audio?
|
2016-04-12 17:23:29 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ret = snd_pcm_prepare(as->hdl);
|
2016-04-12 17:23:29 -04:00
|
|
|
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;
|
|
|
|
}
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
// 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)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
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;
|
|
|
|
}
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
dump_config(as);
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
// Clear prebuffer in case start got called twice without a stop in between
|
|
|
|
ringbuffer_free(&as->prebuf, 1);
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
as->pos = 0;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-23 17:24:36 -05:00
|
|
|
// 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;
|
2019-04-07 18:50:20 -04:00
|
|
|
as->stamp_pts = timespec_add(obuf->pts, ts);
|
2019-02-22 02:41:33 -05:00
|
|
|
|
2019-02-23 17:24:36 -05:00
|
|
|
// The difference between pos and start pos should match the 2 second buffer
|
|
|
|
// that AirPlay uses (OUTPUTS_BUFFER_DURATION) + user configured offset_ms. 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.
|
|
|
|
offset_nsamp = (as->offset_ms * as->quality.sample_rate / 1000);
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-23 17:24:36 -05:00
|
|
|
as->buffer_nsamp = OUTPUTS_BUFFER_DURATION * as->quality.sample_rate + offset_nsamp;
|
2019-02-22 02:41:33 -05:00
|
|
|
size = STOB(as->buffer_nsamp, as->quality.bits_per_sample, as->quality.channels);
|
|
|
|
ringbuffer_init(&as->prebuf, size);
|
|
|
|
|
|
|
|
as->state = OUTPUT_STATE_STREAMING;
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
|
2016-04-07 17:35:34 -04:00
|
|
|
// 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
|
2019-02-22 02:41:33 -05:00
|
|
|
buffer_write(struct alsa_session *as, struct output_data *odata, snd_pcm_sframes_t avail)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
uint8_t *buf;
|
|
|
|
size_t bufsize;
|
|
|
|
size_t wrote;
|
2016-04-07 17:35:34 -04:00
|
|
|
snd_pcm_sframes_t nsamp;
|
|
|
|
snd_pcm_sframes_t ret;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
// Prebuffering, no actual writing
|
|
|
|
if (avail == 0)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
wrote = ringbuffer_write(&as->prebuf, odata->buffer, odata->bufsize);
|
|
|
|
nsamp = BTOS(wrote, as->quality.bits_per_sample, as->quality.channels);
|
|
|
|
return nsamp;
|
|
|
|
}
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
// 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);
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
bufsize = ringbuffer_read(&buf, bufsize, &as->prebuf);
|
|
|
|
if (bufsize == 0)
|
|
|
|
return 0;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
nsamp = BTOS(bufsize, as->quality.bits_per_sample, as->quality.channels);
|
|
|
|
ret = snd_pcm_writei(as->hdl, buf, nsamp);
|
|
|
|
if (ret < 0)
|
2019-05-15 05:53:13 -04:00
|
|
|
return ret;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
avail -= ret;
|
|
|
|
}
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
// 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;
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ret = snd_pcm_writei(as->hdl, odata->buffer, odata->samples);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (ret < 0)
|
2016-04-07 17:35:34 -04:00
|
|
|
return ret;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
if (ret != odata->samples)
|
2016-04-04 10:58:07 -04:00
|
|
|
DPRINTF(E_WARN, L_LAUDIO, "ALSA partial write detected\n");
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
return ret;
|
2016-04-07 17:35:34 -04:00
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
static enum alsa_sync_state
|
2019-04-07 18:50:20 -04:00
|
|
|
sync_check(double *drift, double *latency, struct alsa_session *as, snd_pcm_sframes_t delay)
|
2016-04-07 17:35:34 -04:00
|
|
|
{
|
|
|
|
enum alsa_sync_state sync;
|
2019-02-23 17:24:36 -05:00
|
|
|
struct timespec ts;
|
|
|
|
int elapsed;
|
|
|
|
uint64_t cur_pos;
|
|
|
|
uint64_t exp_pos;
|
2019-04-07 18:50:20 -04:00
|
|
|
int32_t diff;
|
|
|
|
double r2;
|
2019-02-23 17:24:36 -05:00
|
|
|
int ret;
|
2016-04-07 17:35:34 -04:00
|
|
|
|
2019-02-23 17:24:36 -05:00
|
|
|
// Would be nice to use snd_pcm_status_get_audio_htstamp here, but it doesn't
|
|
|
|
// seem to be supported on my computer
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
2019-02-22 02:41:33 -05:00
|
|
|
|
2019-04-07 18:50:20 -04:00
|
|
|
// Here we calculate elapsed time since last reference position (which is
|
|
|
|
// equal to playback start time, unless we have reset due to sync correction),
|
|
|
|
// 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;
|
2019-02-23 17:24:36 -05:00
|
|
|
if (elapsed < 0)
|
|
|
|
return ALSA_SYNC_OK;
|
2019-02-22 02:41:33 -05:00
|
|
|
|
2019-04-07 18:50:20 -04:00
|
|
|
cur_pos = (uint64_t)as->pos - as->stamp_pos - (delay + BTOS(as->prebuf.read_avail, as->quality.bits_per_sample, as->quality.channels));
|
2019-02-23 17:24:36 -05:00
|
|
|
exp_pos = (uint64_t)elapsed * as->quality.sample_rate / 1000;
|
2019-04-07 18:50:20 -04:00
|
|
|
diff = cur_pos - exp_pos;
|
2019-02-22 02:41:33 -05:00
|
|
|
|
2019-04-09 15:45:16 -04:00
|
|
|
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);
|
2019-04-07 18:50:20 -04:00
|
|
|
|
|
|
|
// Add the latency to our measurement history
|
|
|
|
as->latency_history[as->latency_counter] = (double)diff;
|
|
|
|
as->latency_counter++;
|
|
|
|
|
|
|
|
// Haven't collected enough samples for sync evaluation yet, so just return
|
2019-04-09 15:45:16 -04:00
|
|
|
if (as->latency_counter < alsa_latency_history_size)
|
2019-04-07 18:50:20 -04:00
|
|
|
return ALSA_SYNC_OK;
|
|
|
|
|
|
|
|
as->latency_counter = 0;
|
|
|
|
|
2019-04-09 15:45:16 -04:00
|
|
|
ret = linear_regression(drift, latency, &r2, NULL, as->latency_history, alsa_latency_history_size);
|
2019-04-07 18:50:20 -04:00
|
|
|
if (ret < 0)
|
2019-02-23 17:24:36 -05:00
|
|
|
{
|
2019-04-07 18:50:20 -04:00
|
|
|
DPRINTF(E_WARN, L_LAUDIO, "Linear regression of collected latency samples failed\n");
|
|
|
|
return ALSA_SYNC_OK;
|
2019-02-23 17:24:36 -05:00
|
|
|
}
|
2019-04-07 18:50:20 -04:00
|
|
|
|
|
|
|
// Set *latency to the "average" within the period
|
2019-04-09 15:45:16 -04:00
|
|
|
*latency = (*drift) * alsa_latency_history_size / 2 + (*latency);
|
2019-04-07 18:50:20 -04:00
|
|
|
|
|
|
|
if (abs(*latency) < ALSA_MAX_LATENCY && abs(*drift) < ALSA_MAX_DRIFT)
|
|
|
|
sync = ALSA_SYNC_OK; // If both latency and drift are within thresholds -> no action
|
|
|
|
else if (*latency > 0 && *drift > 0)
|
|
|
|
sync = ALSA_SYNC_AHEAD;
|
|
|
|
else if (*latency < 0 && *drift < 0)
|
|
|
|
sync = ALSA_SYNC_BEHIND;
|
2019-02-22 02:41:33 -05:00
|
|
|
else
|
2019-04-07 18:50:20 -04:00
|
|
|
sync = ALSA_SYNC_OK; // Drift is counteracting latency -> no action
|
2016-04-07 17:35:34 -04:00
|
|
|
|
2019-04-07 18:50:20 -04:00
|
|
|
if (sync != ALSA_SYNC_OK && r2 < ALSA_MAX_VARIANCE)
|
|
|
|
{
|
|
|
|
DPRINTF(E_DBG, L_LAUDIO, "Too much variance in latency measurements (r2=%f/%f), won't try to compensate\n", r2, ALSA_MAX_VARIANCE);
|
|
|
|
sync = ALSA_SYNC_OK;
|
|
|
|
}
|
2016-04-07 17:35:34 -04:00
|
|
|
|
2019-04-07 18:50:20 -04:00
|
|
|
DPRINTF(E_DBG, L_LAUDIO, "Sync check result: drift=%f, latency=%f, r2=%f, sync=%d\n", *drift, *latency, r2, sync);
|
2016-04-07 17:35:34 -04:00
|
|
|
|
|
|
|
return sync;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2019-04-07 18:50:20 -04:00
|
|
|
sync_correct(struct alsa_session *as, double drift, double latency, struct timespec pts, snd_pcm_sframes_t delay)
|
2019-02-22 02:41:33 -05:00
|
|
|
{
|
2019-04-07 18:50:20 -04:00
|
|
|
int step;
|
|
|
|
int sign;
|
|
|
|
|
|
|
|
// We change the sample_rate in steps that are a multiple of 50. So we might
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
sign = (drift < 0) ? -1 : 1;
|
|
|
|
|
|
|
|
if (abs(as->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;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
else if (abs(as->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);
|
|
|
|
|
|
|
|
as->sync_resample_step += sign;
|
|
|
|
as->quality.sample_rate += sign * step;
|
|
|
|
|
|
|
|
if (as->sync_resample_step != 0)
|
|
|
|
outputs_quality_subscribe(&as->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;
|
|
|
|
|
|
|
|
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);
|
2019-02-22 02:41:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
playback_write(struct alsa_session *as, struct output_buffer *obuf)
|
2016-04-07 17:35:34 -04:00
|
|
|
{
|
|
|
|
snd_pcm_sframes_t ret;
|
2019-02-23 17:24:36 -05:00
|
|
|
snd_pcm_sframes_t avail;
|
2019-04-07 18:50:20 -04:00
|
|
|
snd_pcm_sframes_t delay;
|
2016-04-07 17:35:34 -04:00
|
|
|
enum alsa_sync_state sync;
|
2019-04-07 18:50:20 -04:00
|
|
|
double drift;
|
|
|
|
double latency;
|
2019-02-22 02:41:33 -05:00
|
|
|
bool prebuffering;
|
|
|
|
int i;
|
2016-04-07 17:35:34 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
// Find the quality we want
|
|
|
|
for (i = 0; obuf->data[i].buffer; i++)
|
|
|
|
{
|
|
|
|
if (quality_is_equal(&as->quality, &obuf->data[i].quality))
|
|
|
|
break;
|
|
|
|
}
|
2016-04-07 17:35:34 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
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;
|
|
|
|
}
|
2016-04-07 17:35:34 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
prebuffering = (as->pos < as->buffer_nsamp);
|
2016-04-07 17:35:34 -04:00
|
|
|
if (prebuffering)
|
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
// Can never fail since we don't actually write to the device
|
|
|
|
as->pos += buffer_write(as, &obuf->data[i], 0);
|
2016-04-07 17:35:34 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-02-23 17:24:36 -05:00
|
|
|
// Check sync each second (or if this is first write where last_pts is zero)
|
2019-04-09 15:45:16 -04:00
|
|
|
if (!alsa_sync_disable && (obuf->pts.tv_sec != as->last_pts.tv_sec))
|
2019-02-22 02:41:33 -05:00
|
|
|
{
|
2019-04-07 18:50:20 -04:00
|
|
|
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);
|
|
|
|
}
|
2016-04-07 17:35:34 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
as->last_pts = obuf->pts;
|
|
|
|
}
|
2016-04-07 17:35:34 -04:00
|
|
|
|
2019-02-23 17:24:36 -05:00
|
|
|
avail = snd_pcm_avail(as->hdl);
|
|
|
|
|
|
|
|
ret = buffer_write(as, &obuf->data[i], avail);
|
2016-04-07 17:35:34 -04:00
|
|
|
if (ret < 0)
|
|
|
|
goto alsa_error;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
as->pos += ret;
|
|
|
|
|
2016-04-04 10:58:07 -04:00
|
|
|
return;
|
|
|
|
|
|
|
|
alsa_error:
|
|
|
|
if (ret == -EPIPE)
|
|
|
|
{
|
|
|
|
DPRINTF(E_WARN, L_LAUDIO, "ALSA buffer underrun\n");
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ret = snd_pcm_prepare(as->hdl);
|
2016-04-04 10:58:07 -04:00
|
|
|
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
|
2019-02-22 02:41:33 -05:00
|
|
|
playback_restart(as, obuf);
|
2016-04-04 10:58:07 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
DPRINTF(E_LOG, L_LAUDIO, "ALSA write error: %s\n", snd_strerror(ret));
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
as->state = OUTPUT_STATE_FAILED;
|
2016-04-04 10:58:07 -04:00
|
|
|
alsa_status(as);
|
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
|
|
|
|
/* ---------------------------- 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);
|
|
|
|
}
|
|
|
|
|
2016-04-04 10:58:07 -04:00
|
|
|
static void
|
2019-02-22 02:41:33 -05:00
|
|
|
alsa_session_cleanup(struct alsa_session *as)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
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;
|
2016-04-04 10:58:07 -04:00
|
|
|
int ret;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
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");
|
|
|
|
|
2019-02-23 17:24:36 -05:00
|
|
|
as->offset_ms = cfg_getint(cfg_audio, "offset_ms");
|
|
|
|
if (abs(as->offset_ms) > 1000)
|
2019-02-22 02:41:33 -05:00
|
|
|
{
|
2019-02-23 17:24:36 -05:00
|
|
|
DPRINTF(E_LOG, L_LAUDIO, "The ALSA offset_ms (%d) set in the configuration is out of bounds\n", as->offset_ms);
|
|
|
|
as->offset_ms = 1000 * (as->offset_ms/abs(as->offset_ms));
|
2019-02-22 02:41:33 -05:00
|
|
|
}
|
|
|
|
|
2019-04-09 15:45:16 -04:00
|
|
|
CHECK_NULL(L_LAUDIO, as->latency_history = calloc(alsa_latency_history_size, sizeof(double)));
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
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);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (ret < 0)
|
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
DPRINTF(E_LOG, L_LAUDIO, "%s\n", errmsg);
|
|
|
|
free(errmsg);
|
|
|
|
goto out_device_close;
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
// 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);
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
|
2016-04-04 10:58:07 -04:00
|
|
|
/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */
|
|
|
|
|
|
|
|
static int
|
2019-02-22 02:41:33 -05:00
|
|
|
alsa_device_start(struct output_device *device, int callback_id)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
|
|
|
struct alsa_session *as;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
as = alsa_session_make(device, callback_id);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (!as)
|
|
|
|
return -1;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
as->state = OUTPUT_STATE_CONNECTED;
|
2016-04-04 10:58:07 -04:00
|
|
|
alsa_status(as);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
static int
|
|
|
|
alsa_device_stop(struct output_device *device, int callback_id)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
struct alsa_session *as = device->session;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
as->callback_id = callback_id;
|
|
|
|
as->state = OUTPUT_STATE_STOPPED;
|
|
|
|
alsa_status(as); // Will terminate the session since the state is STOPPED
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
return 0;
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
|
2019-02-16 18:19:13 -05:00
|
|
|
static int
|
|
|
|
alsa_device_flush(struct output_device *device, int callback_id)
|
|
|
|
{
|
|
|
|
struct alsa_session *as = device->session;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
snd_pcm_drop(as->hdl);
|
2019-02-16 18:19:13 -05:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
ringbuffer_free(&as->prebuf, 1);
|
2019-02-16 18:19:13 -05:00
|
|
|
|
|
|
|
as->callback_id = callback_id;
|
2019-02-22 02:41:33 -05:00
|
|
|
as->state = OUTPUT_STATE_CONNECTED;
|
2019-02-16 18:19:13 -05:00
|
|
|
alsa_status(as);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-04-04 10:58:07 -04:00
|
|
|
static int
|
2019-02-22 02:41:33 -05:00
|
|
|
alsa_device_probe(struct output_device *device, int callback_id)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
|
|
|
struct alsa_session *as;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
as = alsa_session_make(device, callback_id);
|
2016-04-04 10:58:07 -04:00
|
|
|
if (!as)
|
|
|
|
return -1;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
as->state = OUTPUT_STATE_STOPPED;
|
|
|
|
alsa_status(as); // Will terminate the session since the state is STOPPED
|
2016-04-04 10:58:07 -04:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
2019-02-22 02:41:33 -05:00
|
|
|
alsa_device_volume_set(struct output_device *device, int callback_id)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
struct alsa_session *as = device->session;
|
2016-04-04 10:58:07 -04:00
|
|
|
int pcm_vol;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
if (!as)
|
2016-04-04 17:42:52 -04:00
|
|
|
return 0;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
snd_mixer_handle_events(as->mixer_hdl);
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
if (!snd_mixer_selem_is_active(as->vol_elem))
|
2016-04-04 17:42:52 -04:00
|
|
|
return 0;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
|
|
|
switch (device->volume)
|
|
|
|
{
|
|
|
|
case 0:
|
2019-02-22 02:41:33 -05:00
|
|
|
pcm_vol = as->vol_min;
|
2016-04-04 10:58:07 -04:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 100:
|
2019-02-22 02:41:33 -05:00
|
|
|
pcm_vol = as->vol_max;
|
2016-04-04 10:58:07 -04:00
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
2019-02-22 02:41:33 -05:00
|
|
|
pcm_vol = as->vol_min + (device->volume * (as->vol_max - as->vol_min)) / 100;
|
2016-04-04 10:58:07 -04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
DPRINTF(E_DBG, L_LAUDIO, "Setting ALSA volume to %d (%d)\n", pcm_vol, device->volume);
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
snd_mixer_selem_set_playback_volume_all(as->vol_elem, pcm_vol);
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
as->callback_id = callback_id;
|
2016-04-04 10:58:07 -04:00
|
|
|
alsa_status(as);
|
|
|
|
|
2016-04-04 17:42:52 -04:00
|
|
|
return 1;
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2019-02-22 02:41:33 -05:00
|
|
|
alsa_device_cb_set(struct output_device *device, int callback_id)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
struct alsa_session *as = device->session;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
as->callback_id = callback_id;
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2019-02-22 02:41:33 -05:00
|
|
|
alsa_write(struct output_buffer *obuf)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
|
|
|
struct alsa_session *as;
|
2019-02-22 02:41:33 -05:00
|
|
|
struct alsa_session *next;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
for (as = sessions; as; as = next)
|
2016-04-04 10:58:07 -04:00
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
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);
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
playback_write(as, obuf);
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
alsa_last_quality = obuf->data[0].quality;
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
alsa_init(void)
|
|
|
|
{
|
|
|
|
struct output_device *device;
|
|
|
|
cfg_t *cfg_audio;
|
2019-02-22 02:41:33 -05:00
|
|
|
const char *type;
|
2016-04-04 10:58:07 -04:00
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
// Is ALSA enabled in config?
|
2016-04-04 10:58:07 -04:00
|
|
|
cfg_audio = cfg_getsec(cfg, "audio");
|
|
|
|
type = cfg_getstr(cfg_audio, "type");
|
|
|
|
if (type && (strcasecmp(type, "alsa") != 0))
|
|
|
|
return -1;
|
|
|
|
|
2019-04-09 15:45:16 -04:00
|
|
|
alsa_sync_disable = cfg_getbool(cfg_audio, "sync_disable");
|
|
|
|
alsa_latency_history_size = cfg_getint(cfg_audio, "adjust_period_seconds");
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
CHECK_NULL(L_LAUDIO, device = calloc(1, sizeof(struct output_device)));
|
2016-04-04 10:58:07 -04:00
|
|
|
|
|
|
|
device->id = 0;
|
2019-02-22 02:41:33 -05:00
|
|
|
device->name = strdup(cfg_getstr(cfg_audio, "nickname"));
|
2016-04-04 10:58:07 -04:00
|
|
|
device->type = OUTPUT_TYPE_ALSA;
|
|
|
|
device->type_name = outputs_name(device->type);
|
|
|
|
device->has_video = 0;
|
|
|
|
|
2019-02-22 02:41:33 -05:00
|
|
|
DPRINTF(E_INFO, L_LAUDIO, "Adding ALSA device '%s' with name '%s'\n", cfg_getstr(cfg_audio, "card"), device->name);
|
2016-04-04 10:58:07 -04:00
|
|
|
|
|
|
|
player_device_add(device);
|
|
|
|
|
|
|
|
snd_lib_error_set_handler(logger_alsa);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
alsa_deinit(void)
|
|
|
|
{
|
2019-02-22 02:41:33 -05:00
|
|
|
struct alsa_session *as;
|
|
|
|
|
2016-04-04 10:58:07 -04:00
|
|
|
snd_lib_error_set_handler(NULL);
|
2019-02-22 02:41:33 -05:00
|
|
|
|
|
|
|
for (as = sessions; sessions; as = sessions)
|
|
|
|
{
|
|
|
|
sessions = as->next;
|
|
|
|
alsa_session_free(as);
|
|
|
|
}
|
2016-04-04 10:58:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2019-02-16 18:19:13 -05:00
|
|
|
.device_flush = alsa_device_flush,
|
2016-04-04 10:58:07 -04:00
|
|
|
.device_probe = alsa_device_probe,
|
|
|
|
.device_volume_set = alsa_device_volume_set,
|
2019-02-22 02:41:33 -05:00
|
|
|
.device_cb_set = alsa_device_cb_set,
|
2016-04-04 10:58:07 -04:00
|
|
|
.write = alsa_write,
|
|
|
|
};
|