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 >
*
2020-08-17 09:13:11 -04:00
* Copyright ( c ) 2010 Clemens Ladisch < clemens @ ladisch . de >
* from alsa - utils / alsamixer / volume_mapping . c
* use_linear_dB_scale ( )
* lrint_dir ( )
2020-08-22 15:36:10 -04:00
* volume_normalized_set ( )
2020-08-17 09:13:11 -04:00
*
2016-04-04 10:58:07 -04:00
* 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>
2019-09-18 15:28:15 -04:00
# include <math.h>
2016-04-04 10:58:07 -04:00
2019-06-11 23:10:04 -04:00
# include <alsa/asoundlib.h>
2016-04-04 10:58:07 -04:00
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"
2020-08-17 09:13:11 -04:00
// For setting volume, treat everything below this as linear scale
# define MAX_LINEAR_DB_SCALE 24
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
2019-06-22 15:53:09 -04:00
# define ALSA_ERROR_WRITE -1
# define ALSA_ERROR_UNDERRUN -2
# define ALSA_ERROR_SESSION -3
# define ALSA_ERROR_DEVICE -4
# define ALSA_ERROR_DEVICE_BUSY -5
2016-04-04 10:58:07 -04:00
2016-04-07 17:35:34 -04:00
enum alsa_sync_state
{
ALSA_SYNC_OK ,
ALSA_SYNC_AHEAD ,
ALSA_SYNC_BEHIND ,
} ;
2019-06-22 15:53:09 -04:00
struct alsa_mixer
2016-04-04 10:58:07 -04:00
{
2019-06-22 15:53:09 -04:00
snd_mixer_t * hdl ;
snd_mixer_elem_t * vol_elem ;
2016-04-04 10:58:07 -04:00
2019-06-22 15:53:09 -04:00
long vol_min ;
long vol_max ;
} ;
2016-04-04 10:58:07 -04:00
2019-06-22 15:53:09 -04:00
struct alsa_playback_session
{
snd_pcm_t * pcm ;
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-06-22 15:53:09 -04:00
struct media_quality quality ;
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-06-22 15:53:09 -04:00
struct alsa_playback_session * next ;
} ;
2016-04-04 10:58:07 -04:00
2020-01-03 04:06:58 -05:00
// Info about the device, which is not required by the player, only internally
struct alsa_extra
{
const char * card_name ;
const char * mixer_name ;
const char * mixer_device_name ;
int offset_ms ;
} ;
2019-06-22 15:53:09 -04:00
struct alsa_session
{
enum output_device_state state ;
2016-04-04 10:58:07 -04:00
2019-06-22 15:53:09 -04:00
uint64_t device_id ;
int callback_id ;
const char * devname ;
const char * mixer_name ;
const char * mixer_device_name ;
struct alsa_mixer mixer ;
int offset_ms ;
// A session will have multiple playback sessions when the quality changes
struct alsa_playback_session * pb ;
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
2019-08-23 11:22:11 -04:00
static struct media_quality alsa_fallback_quality = { 44100 , 16 , 2 , 0 } ;
2019-02-22 02:41:33 -05:00
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
2019-06-22 15:53:09 -04:00
dump_config ( snd_pcm_t * pcm )
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-06-22 15:53:09 -04:00
if ( snd_pcm_dump_setup ( pcm , output ) = = 0 )
2019-02-22 02:41:33 -05:00
{
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
}
}
2020-04-18 15:35:47 -04:00
static void
dump_card ( int card , snd_ctl_card_info_t * info )
{
char hwdev [ 14 ] ; // 'hw:' (3) + max_uint (10)
snd_ctl_t * hdl ;
snd_mixer_t * mixer ;
snd_mixer_elem_t * elem ;
char mixerstr [ 256 ] ;
int err ;
snprintf ( hwdev , sizeof ( hwdev ) , " hw:%d " , card ) ;
err = snd_ctl_open ( & hdl , hwdev , 0 ) ;
if ( err < 0 )
{
DPRINTF ( E_WARN , L_LAUDIO , " Failed to probe ALSA card=%d - %s \n " , card , snd_strerror ( err ) ) ;
return ;
}
err = snd_ctl_card_info ( hdl , info ) ;
if ( err < 0 )
{
DPRINTF ( E_WARN , L_LAUDIO , " Failed to probe ALSA (info) card=%d - %s \n " , card , snd_strerror ( err ) ) ;
goto error ;
}
err = snd_mixer_open ( & mixer , 0 ) ;
if ( err < 0 )
{
DPRINTF ( E_WARN , L_LAUDIO , " Failed to probe ALSA (mixer open) card=%d - %s \n " , card , snd_strerror ( err ) ) ;
goto error ;
}
err = snd_mixer_attach ( mixer , hwdev ) ;
if ( err < 0 )
{
DPRINTF ( E_WARN , L_LAUDIO , " Failed to probe ALSA (mixer attach) card=%d - %s \n " , card , snd_strerror ( err ) ) ;
goto errormixer ;
}
err = snd_mixer_selem_register ( mixer , NULL , NULL ) ;
if ( err < 0 )
{
DPRINTF ( E_WARN , L_LAUDIO , " Failed to probe ALSA (mixer setup) card=%d - %s \n " , card , snd_strerror ( err ) ) ;
goto errormixer ;
}
err = snd_mixer_load ( mixer ) ;
if ( err < 0 )
{
DPRINTF ( E_WARN , L_LAUDIO , " Failed to probe ALSA (mixer setup) card=%d - %s \n " , card , snd_strerror ( err ) ) ;
goto errormixer ;
}
memset ( mixerstr , 0 , sizeof ( mixerstr ) ) ;
for ( elem = snd_mixer_first_elem ( mixer ) ; elem ; elem = snd_mixer_elem_next ( elem ) )
{
if ( snd_mixer_selem_has_common_volume ( elem ) | | ! snd_mixer_selem_has_playback_volume ( elem ) )
continue ;
safe_snprintf_cat ( mixerstr , sizeof ( mixerstr ) , " '%s' " , snd_mixer_selem_get_name ( elem ) ) ;
}
if ( mixerstr [ 0 ] = = ' \0 ' )
sprintf ( mixerstr , " (no mixers found) " ) ;
2020-07-14 16:43:45 -04:00
DPRINTF ( E_INFO , L_LAUDIO , " Available ALSA playback mixer(s) on %s CARD=%s (%s):%s \n " , hwdev , snd_ctl_card_info_get_id ( info ) , snd_ctl_card_info_get_name ( info ) , mixerstr ) ;
2020-04-18 15:35:47 -04:00
errormixer :
snd_mixer_close ( mixer ) ;
error :
snd_ctl_close ( hdl ) ;
}
// Walk all the alsa devices here and log valid playback mixers
static void
cards_list ( )
{
snd_ctl_card_info_t * info = NULL ;
int card = 0 ;
snd_ctl_card_info_alloca ( & info ) ;
if ( ! info )
return ;
while ( card > = 0 )
{
dump_card ( card , info ) ;
if ( snd_card_next ( & card ) < 0 )
break ;
}
}
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 )
2019-10-22 13:41:48 -04:00
return SND_PCM_FORMAT_S24_3LE ;
2019-04-12 13:40:33 -04:00
else if ( bits_per_sample = = 32 )
return SND_PCM_FORMAT_S32_LE ;
else
return SND_PCM_FORMAT_UNKNOWN ;
}
2020-08-17 09:13:11 -04:00
/* from alsa-utils/alsamixer/volume_mapping.c
*
* The mapping is designed so that the position in the interval is proportional
* to the volume as a human ear would perceive it ( i . e . , the position is the
* cubic root of the linear sample multiplication factor ) . For controls with
* a small range ( 24 dB or less ) , the mapping is linear in the dB values so
* that each step has the same size visually . Only for controls without dB
* information , a linear mapping of the hardware volume register values is used
* ( this is the same algorithm as used in the old alsamixer ) .
*
* When setting the volume , ' dir ' is the rounding direction :
* - 1 / 0 / 1 = down / nearest / up .
*/
static inline bool
use_linear_dB_scale ( long dBmin , long dBmax )
2016-04-04 10:58:07 -04:00
{
2020-08-17 09:13:11 -04:00
return dBmax - dBmin < = MAX_LINEAR_DB_SCALE * 100 ;
}
2019-06-22 15:53:09 -04:00
2020-08-22 15:36:10 -04:00
static long
lrint_dir ( double x , int dir )
2020-08-17 09:13:11 -04:00
{
if ( dir > 0 )
return lrint ( ceil ( x ) ) ;
else if ( dir < 0 )
return lrint ( floor ( x ) ) ;
else
return lrint ( x ) ;
}
2019-06-22 15:53:09 -04:00
2020-08-17 09:13:11 -04:00
// from alsamixer/volume-mapping.c, sets volume in line with human perception
static int
2020-08-22 15:36:10 -04:00
volume_normalized_set ( snd_mixer_elem_t * elem , double volume , int dir )
2020-08-17 09:13:11 -04:00
{
long min , max , value ;
double min_norm ;
int err ;
2019-06-22 15:53:09 -04:00
2020-08-17 09:13:11 -04:00
err = snd_mixer_selem_get_playback_dB_range ( elem , & min , & max ) ;
if ( err < 0 | | min > = max )
2019-06-22 15:53:09 -04:00
{
2020-08-17 09:13:11 -04:00
err = snd_mixer_selem_get_playback_volume_range ( elem , & min , & max ) ;
if ( err < 0 )
return err ;
2019-06-22 15:53:09 -04:00
2020-08-17 09:13:11 -04:00
value = lrint_dir ( volume * ( max - min ) , dir ) + min ;
return snd_mixer_selem_set_playback_volume_all ( elem , value ) ;
}
2019-06-22 15:53:09 -04:00
2020-08-17 09:13:11 -04:00
// Corner case from mpd - log10() expects non-zero
if ( volume < = 0 )
return snd_mixer_selem_set_playback_dB_all ( elem , min , dir ) ;
else if ( volume > = 1 )
return snd_mixer_selem_set_playback_dB_all ( elem , max , dir ) ;
if ( use_linear_dB_scale ( min , max ) )
{
value = lrint_dir ( volume * ( max - min ) , dir ) + min ;
return snd_mixer_selem_set_playback_dB_all ( elem , value , dir ) ;
2019-06-22 15:53:09 -04:00
}
2020-08-17 09:13:11 -04:00
if ( min ! = SND_CTL_TLV_DB_GAIN_MUTE )
{
min_norm = pow ( 10 , ( min - max ) / 6000.0 ) ;
volume = volume * ( 1 - min_norm ) + min_norm ;
}
2020-08-22 15:36:10 -04:00
2020-08-17 09:13:11 -04:00
value = lrint_dir ( 6000.0 * log10 ( volume ) , dir ) + max ;
return snd_mixer_selem_set_playback_dB_all ( elem , value , dir ) ;
}
2019-06-22 15:53:09 -04:00
2020-08-17 09:13:11 -04:00
static int
volume_set ( struct alsa_mixer * mixer , int volume )
{
int ret ;
2019-06-22 15:53:09 -04:00
2020-08-17 09:13:11 -04:00
snd_mixer_handle_events ( mixer - > hdl ) ;
if ( ! snd_mixer_selem_is_active ( mixer - > vol_elem ) )
return - 1 ;
DPRINTF ( E_DBG , L_LAUDIO , " Setting ALSA volume to %d \n " , volume ) ;
2020-08-22 15:36:10 -04:00
ret = volume_normalized_set ( mixer - > vol_elem , volume > = 0 & & volume < = 100 ? volume / 100.0 : 0.75 , 0 ) ;
2020-08-17 09:13:11 -04:00
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_LAUDIO , " Failed to set ALSA volume to %d \n : %s " , volume , snd_strerror ( ret ) ) ;
return - 1 ;
}
2020-08-22 15:36:10 -04:00
2019-06-22 15:53:09 -04:00
return 0 ;
}
static int
mixer_open ( struct alsa_mixer * mixer , const char * mixer_device_name , const char * mixer_name )
{
snd_mixer_t * mixer_hdl ;
snd_mixer_elem_t * vol_elem ;
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 ;
2019-06-22 15:53:09 -04:00
long vol_min ;
long vol_max ;
2016-04-04 10:58:07 -04:00
int ret ;
2019-06-22 15:53:09 -04:00
ret = snd_mixer_open ( & 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 ) ) ;
return - 1 ;
}
2019-06-22 15:53:09 -04:00
ret = snd_mixer_attach ( mixer_hdl , mixer_device_name ) ;
2016-04-04 10:58:07 -04:00
if ( ret < 0 )
{
2019-06-22 15:53:09 -04:00
DPRINTF ( E_LOG , L_LAUDIO , " Failed to attach mixer '%s': %s \n " , mixer_device_name , snd_strerror ( ret ) ) ;
2016-04-04 10:58:07 -04:00
goto out_close ;
}
2019-06-22 15:53:09 -04:00
ret = snd_mixer_selem_register ( mixer_hdl , NULL , NULL ) ;
2016-04-04 10:58:07 -04:00
if ( ret < 0 )
{
2019-06-22 15:53:09 -04:00
DPRINTF ( E_LOG , L_LAUDIO , " Failed to register mixer '%s': %s \n " , mixer_device_name , snd_strerror ( ret ) ) ;
2016-04-04 10:58:07 -04:00
goto out_detach ;
}
2019-06-22 15:53:09 -04:00
ret = snd_mixer_load ( mixer_hdl ) ;
2016-04-04 10:58:07 -04:00
if ( ret < 0 )
{
2019-06-22 15:53:09 -04:00
DPRINTF ( E_LOG , L_LAUDIO , " Failed to load mixer '%s': %s \n " , mixer_device_name , snd_strerror ( ret ) ) ;
2016-04-04 10:58:07 -04:00
goto out_detach ;
}
// Grab interesting elements
snd_mixer_selem_id_alloca ( & sid ) ;
pcm = NULL ;
master = NULL ;
custom = NULL ;
2019-06-22 15:53:09 -04:00
for ( elem = snd_mixer_first_elem ( 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-06-22 15:53:09 -04:00
if ( mixer_name & & ( strcmp ( snd_mixer_selem_id_get_name ( sid ) , 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-06-22 15:53:09 -04:00
if ( mixer_name )
2016-04-04 10:58:07 -04:00
{
if ( custom )
2019-06-22 15:53:09 -04:00
vol_elem = custom ;
2016-04-04 10:58:07 -04:00
else
{
2019-06-22 15:53:09 -04:00
DPRINTF ( E_LOG , L_LAUDIO , " Failed to open configured mixer element '%s' \n " , mixer_name ) ;
2016-04-04 10:58:07 -04:00
goto out_detach ;
}
}
else if ( pcm )
2019-06-22 15:53:09 -04:00
vol_elem = pcm ;
2016-04-04 10:58:07 -04:00
else if ( master )
2019-06-22 15:53:09 -04:00
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-06-22 15:53:09 -04:00
snd_mixer_selem_get_playback_volume_range ( vol_elem , & vol_min , & vol_max ) ;
// All done, export
mixer - > hdl = mixer_hdl ;
mixer - > vol_elem = vol_elem ;
mixer - > vol_min = vol_min ;
mixer - > vol_max = vol_max ;
2016-04-04 10:58:07 -04:00
return 0 ;
out_detach :
2019-06-22 15:53:09 -04:00
snd_mixer_detach ( mixer_hdl , mixer_device_name ) ;
2016-04-04 10:58:07 -04:00
out_close :
2019-06-22 15:53:09 -04:00
snd_mixer_close ( mixer_hdl ) ;
2016-04-04 10:58:07 -04:00
return - 1 ;
}
2019-06-22 15:53:09 -04:00
static void
mixer_close ( struct alsa_mixer * mixer , const char * mixer_device_name )
{
if ( ! mixer | | ! mixer - > hdl )
return ;
snd_mixer_detach ( mixer - > hdl , mixer_device_name ) ;
snd_mixer_close ( mixer - > hdl ) ;
}
2016-04-04 10:58:07 -04:00
static int
2019-06-22 15:53:09 -04:00
pcm_open ( snd_pcm_t * * pcm , const char * device_name , struct media_quality * quality )
2016-04-04 10:58:07 -04:00
{
2019-06-22 15:53:09 -04:00
snd_pcm_t * hdl ;
2016-04-04 10:58:07 -04:00
snd_pcm_hw_params_t * hw_params ;
snd_pcm_uframes_t bufsize ;
int ret ;
2019-06-22 15:53:09 -04:00
ret = snd_pcm_open ( & hdl , device_name , SND_PCM_STREAM_PLAYBACK , 0 ) ;
2016-04-04 10:58:07 -04:00
if ( ret < 0 )
{
2019-06-22 15:53:09 -04:00
if ( ret = = - EBUSY )
return ALSA_ERROR_DEVICE_BUSY ;
DPRINTF ( E_LOG , L_LAUDIO , " Could not open playback device '%s': %s \n " , device_name , snd_strerror ( ret ) ) ;
return ALSA_ERROR_DEVICE ;
2016-04-04 10:58:07 -04:00
}
// 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-06-22 15:53:09 -04:00
ret = snd_pcm_hw_params_any ( 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-06-22 15:53:09 -04:00
ret = snd_pcm_hw_params_set_access ( 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-06-22 15:53:09 -04:00
ret = snd_pcm_hw_params_set_format ( hdl , hw_params , bps2format ( quality - > bits_per_sample ) ) ;
2019-04-12 13:40:33 -04:00
if ( ret < 0 )
{
2019-06-22 15:53:09 -04:00
DPRINTF ( E_LOG , L_LAUDIO , " Could not set format (bits per sample %d): %s \n " , quality - > bits_per_sample , snd_strerror ( ret ) ) ;
2019-04-12 13:40:33 -04:00
goto out_fail ;
}
2019-06-22 15:53:09 -04:00
ret = snd_pcm_hw_params_set_channels ( hdl , hw_params , quality - > channels ) ;
2019-04-12 13:40:33 -04:00
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_LAUDIO , " Could not set stereo output: %s \n " , snd_strerror ( ret ) ) ;
goto out_fail ;
}
2019-06-22 15:53:09 -04:00
ret = snd_pcm_hw_params_set_rate ( hdl , hw_params , quality - > sample_rate , 0 ) ;
2019-04-12 13:40:33 -04:00
if ( ret < 0 )
{
2019-06-22 15:53:09 -04:00
DPRINTF ( E_LOG , L_LAUDIO , " Hardware doesn't support %u Hz: %s \n " , quality - > sample_rate , snd_strerror ( ret ) ) ;
2019-04-12 13:40:33 -04:00
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-06-22 15:53:09 -04:00
// Enable this line to simulate devices with low buffer size
//bufsize = 32768;
ret = snd_pcm_hw_params_set_buffer_size_max ( hdl , hw_params , & bufsize ) ;
2019-02-22 02:41:33 -05:00
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-06-22 15:53:09 -04:00
ret = snd_pcm_hw_params ( hdl , hw_params ) ;
2016-04-04 10:58:07 -04:00
if ( ret < 0 )
{
2019-06-22 15:53:09 -04:00
DPRINTF ( E_LOG , L_LAUDIO , " Could not set hw params in pcm_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 ) ;
2019-06-22 15:53:09 -04:00
* pcm = hdl ;
2016-04-04 10:58:07 -04:00
2019-02-22 02:41:33 -05:00
return 0 ;
out_fail :
2019-06-22 15:53:09 -04:00
snd_pcm_hw_params_free ( hw_params ) ;
snd_pcm_close ( hdl ) ;
2019-02-22 02:41:33 -05:00
2019-06-22 15:53:09 -04:00
return ALSA_ERROR_DEVICE ;
2019-02-22 02:41:33 -05:00
}
2019-06-22 15:53:09 -04:00
static void
pcm_close ( snd_pcm_t * hdl )
2019-02-22 02:41:33 -05:00
{
2019-06-22 15:53:09 -04:00
if ( ! hdl )
return ;
2019-02-22 02:41:33 -05:00
2019-06-22 15:53:09 -04:00
snd_pcm_close ( hdl ) ;
2019-02-22 02:41:33 -05:00
}
static int
2019-06-22 15:53:09 -04:00
pcm_configure ( snd_pcm_t * hdl )
2019-02-22 02:41:33 -05:00
{
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-06-22 15:53:09 -04:00
ret = snd_pcm_sw_params_current ( hdl , sw_params ) ;
2019-02-22 02:41:33 -05:00
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-06-22 15:53:09 -04:00
ret = snd_pcm_sw_params_set_tstamp_type ( 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-06-22 15:53:09 -04:00
ret = snd_pcm_sw_params_set_tstamp_mode ( hdl , sw_params , SND_PCM_TSTAMP_ENABLE ) ;
2019-02-22 02:41:33 -05:00
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_LAUDIO , " Could not set tstamp mode: %s \n " , snd_strerror ( ret ) ) ;
goto out_fail ;
}
2019-06-22 15:53:09 -04:00
ret = snd_pcm_sw_params ( hdl , sw_params ) ;
2019-02-22 02:41:33 -05:00
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-06-22 15:53:09 -04:00
playback_session_free ( struct alsa_playback_session * pb )
{
if ( ! pb )
return ;
2019-12-27 05:29:36 -05:00
// Unsubscribe from qualities that sync_correct() might have requested
if ( pb - > sync_resample_step ! = 0 )
outputs_quality_unsubscribe ( & pb - > quality ) ;
2019-06-22 15:53:09 -04:00
pcm_close ( pb - > pcm ) ;
ringbuffer_free ( & pb - > prebuf , 1 ) ;
free ( pb - > latency_history ) ;
free ( pb ) ;
}
static void
playback_session_remove ( struct alsa_session * as , struct alsa_playback_session * pb )
2016-04-04 10:58:07 -04:00
{
2019-06-22 15:53:09 -04:00
struct alsa_playback_session * s ;
DPRINTF ( E_DBG , L_LAUDIO , " Removing playback session (quality %d/%d/%d) from ALSA device '%s' \n " ,
pb - > quality . sample_rate , pb - > quality . bits_per_sample , pb - > quality . channels , as - > devname ) ;
2016-04-04 10:58:07 -04:00
2019-06-22 15:53:09 -04:00
if ( pb = = as - > pb )
as - > pb = as - > pb - > next ;
else
2016-04-04 10:58:07 -04:00
{
2019-06-22 15:53:09 -04:00
for ( s = as - > pb ; s & & ( s - > next ! = pb ) ; s = s - > next )
; /* EMPTY */
2016-04-04 10:58:07 -04:00
2019-06-22 15:53:09 -04:00
if ( ! s )
DPRINTF ( E_WARN , L_LAUDIO , " WARNING: struct alsa_playback_session not found in list; BUG! \n " ) ;
else
s - > next = pb - > next ;
2016-04-04 10:58:07 -04:00
}
2019-06-22 15:53:09 -04:00
playback_session_free ( pb ) ;
2016-04-04 10:58:07 -04:00
}
static void
2019-06-22 15:53:09 -04:00
playback_session_remove_all ( struct alsa_session * as )
{
struct alsa_playback_session * s ;
for ( s = as - > pb ; s ; s = as - > pb )
{
as - > pb = s - > next ;
playback_session_free ( s ) ;
}
}
static int
playback_session_add ( struct alsa_session * as , struct media_quality * quality , struct timespec pts )
2016-04-04 10:58:07 -04:00
{
2019-06-22 15:53:09 -04:00
struct alsa_playback_session * pb ;
struct alsa_playback_session * tail_pb ;
2019-02-23 17:24:36 -05:00
struct timespec ts ;
snd_pcm_sframes_t offset_nsamp ;
2019-02-22 02:41:33 -05:00
size_t size ;
2016-04-04 10:58:07 -04:00
int ret ;
2019-06-22 15:53:09 -04:00
DPRINTF ( E_DBG , L_LAUDIO , " Adding playback session (quality %d/%d/%d) to ALSA device '%s' \n " ,
quality - > sample_rate , quality - > bits_per_sample , quality - > channels , as - > devname ) ;
2019-02-22 02:41:33 -05:00
2019-06-22 15:53:09 -04:00
CHECK_NULL ( L_LAUDIO , pb = calloc ( 1 , sizeof ( struct alsa_playback_session ) ) ) ;
CHECK_NULL ( L_LAUDIO , pb - > latency_history = calloc ( alsa_latency_history_size , sizeof ( double ) ) ) ;
2016-04-12 17:23:29 -04:00
2019-06-22 15:53:09 -04:00
ret = pcm_open ( & pb - > pcm , as - > devname , quality ) ;
if ( ret = = ALSA_ERROR_DEVICE_BUSY )
{
DPRINTF ( E_LOG , L_LAUDIO , " ALSA device '%s' won't open due to existing session (no support for concurrent audio), truncating audio \n " , as - > devname ) ;
playback_session_remove_all ( as ) ;
ret = pcm_open ( & pb - > pcm , as - > devname , quality ) ;
if ( ret = = ALSA_ERROR_DEVICE_BUSY )
2016-04-12 17:23:29 -04:00
{
2019-06-22 15:53:09 -04:00
DPRINTF ( E_LOG , L_LAUDIO , " ALSA device '%s' failed: Device still busy after closing previous sessions \n " , as - > devname ) ;
goto error ;
2016-04-12 17:23:29 -04:00
}
2016-04-04 10:58:07 -04:00
}
2019-02-22 02:41:33 -05:00
if ( ret < 0 )
2016-04-04 10:58:07 -04:00
{
2019-06-22 15:53:09 -04:00
DPRINTF ( E_LOG , L_LAUDIO , " Device '%s' does not support quality (%d/%d/%d), falling back to default \n " , as - > devname , quality - > sample_rate , quality - > bits_per_sample , quality - > channels ) ;
ret = pcm_open ( & pb - > pcm , as - > devname , & alsa_fallback_quality ) ;
2019-02-22 02:41:33 -05:00
if ( ret < 0 )
{
2019-06-22 15:53:09 -04:00
DPRINTF ( E_LOG , L_LAUDIO , " ALSA device failed setting fallback quality \n " ) ;
goto error ;
2019-02-22 02:41:33 -05:00
}
2016-04-04 10:58:07 -04:00
2019-06-22 15:53:09 -04:00
pb - > quality = alsa_fallback_quality ;
}
else
pb - > quality = * quality ;
2016-04-04 10:58:07 -04:00
2019-06-22 15:53:09 -04:00
// If this fails it just means we won't get timestamps, which we can handle
pcm_configure ( pb - > pcm ) ;
2016-04-04 10:58:07 -04:00
2019-06-22 15:53:09 -04:00
dump_config ( pb - > pcm ) ;
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-06-22 15:53:09 -04:00
pb - > stamp_pts = timespec_add ( 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.
2019-06-22 15:53:09 -04:00
offset_nsamp = ( as - > offset_ms * pb - > quality . sample_rate / 1000 ) ;
2016-04-04 10:58:07 -04:00
2019-06-22 15:53:09 -04:00
pb - > buffer_nsamp = OUTPUTS_BUFFER_DURATION * pb - > quality . sample_rate + offset_nsamp ;
size = STOB ( pb - > buffer_nsamp , pb - > quality . bits_per_sample , pb - > quality . channels ) ;
ringbuffer_init ( & pb - > prebuf , size ) ;
2019-02-22 02:41:33 -05:00
2019-06-22 15:53:09 -04:00
// Add to the end of the list, because when we iterate through it in
// alsa_write() we want to write data from the oldest playback session first
if ( as - > pb )
{
for ( tail_pb = as - > pb ; tail_pb - > next ; tail_pb = tail_pb - > next )
; // Fast forward
tail_pb - > next = pb ;
}
else
as - > pb = pb ;
return 0 ;
error :
playback_session_free ( pb ) ;
return - 1 ;
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-06-22 15:53:09 -04:00
buffer_write ( struct alsa_playback_session * pb , 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 ;
2019-10-22 13:41:48 -04:00
ssize_t bufsize ;
2019-02-22 02:41:33 -05:00
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-06-22 15:53:09 -04:00
wrote = ringbuffer_write ( & pb - > prebuf , odata - > buffer , odata - > bufsize ) ;
if ( wrote < odata - > bufsize )
DPRINTF ( E_WARN , L_LAUDIO , " Bug! Partial prebuf write %zu/%zu \n " , wrote , odata - > bufsize ) ;
2019-10-22 13:41:48 -04:00
nsamp = snd_pcm_bytes_to_frames ( pb - > pcm , wrote ) ;
2019-02-22 02:41:33 -05:00
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
2019-06-22 15:53:09 -04:00
if ( pb - > prebuf . read_avail ! = 0 )
2019-02-22 02:41:33 -05:00
{
// Maximum amount of bytes we want to read
2019-10-22 13:41:48 -04:00
bufsize = snd_pcm_frames_to_bytes ( pb - > pcm , avail ) ;
2016-04-04 10:58:07 -04:00
2019-06-22 15:53:09 -04:00
bufsize = ringbuffer_read ( & buf , bufsize , & pb - > prebuf ) ;
2019-02-22 02:41:33 -05:00
if ( bufsize = = 0 )
return 0 ;
2016-04-04 10:58:07 -04:00
2019-06-22 15:53:09 -04:00
// DPRINTF(E_DBG, L_LAUDIO, "Writing prebuffer (read_avail=%zu, bufsize=%zu, avail=%li)\n", pb->prebuf.read_avail, bufsize, avail);
2019-10-22 13:41:48 -04:00
nsamp = snd_pcm_bytes_to_frames ( pb - > pcm , bufsize ) ;
2019-06-22 15:53:09 -04:00
ret = snd_pcm_writei ( pb - > pcm , buf , nsamp ) ;
2019-02-22 02:41:33 -05:00
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-06-22 15:53:09 -04:00
// Write to prebuffer if device buffer does not have availability or if we are
// still prebuffering. Note that if the prebuffer doesn't have enough room,
// which can happen if avail stays low, i.e. device buffer is overrunning,
// then the extra samples get dropped
if ( odata - > samples > avail | | pb - > prebuf . read_avail ! = 0 )
2019-02-22 02:41:33 -05:00
{
2019-06-22 15:53:09 -04:00
wrote = ringbuffer_write ( & pb - > prebuf , odata - > buffer , odata - > bufsize ) ;
if ( wrote < odata - > bufsize )
DPRINTF ( E_WARN , L_LAUDIO , " Dropped %zu bytes of audio - device is overrunning! \n " , odata - > bufsize - wrote ) ;
2019-02-22 02:41:33 -05:00
return odata - > samples ;
2016-04-04 10:58:07 -04:00
}
2019-10-22 13:41:48 -04:00
nsamp = snd_pcm_bytes_to_frames ( pb - > pcm , odata - > bufsize ) ;
ret = snd_pcm_writei ( pb - > pcm , odata - > buffer , nsamp ) ;
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-06-22 15:53:09 -04:00
sync_check ( double * drift , double * latency , struct alsa_playback_session * pb , 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.
2019-06-22 15:53:09 -04:00
elapsed = ( ts . tv_sec - pb - > stamp_pts . tv_sec ) * 1000L + ( ts . tv_nsec - pb - > 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-06-22 15:53:09 -04:00
cur_pos = ( uint64_t ) pb - > pos - pb - > stamp_pos - ( delay + BTOS ( pb - > prebuf . read_avail , pb - > quality . bits_per_sample , pb - > quality . channels ) ) ;
exp_pos = ( uint64_t ) elapsed * pb - > quality . sample_rate / 1000 ;
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 " ,
2019-06-22 15:53:09 -04:00
pb - > latency_counter , alsa_latency_history_size , pb - > stamp_pts . tv_sec , pb - > stamp_pts . tv_nsec / 1000000 , ts . tv_sec , ts . tv_nsec / 1000000 , elapsed , cur_pos , exp_pos , diff ) ;
2019-04-07 18:50:20 -04:00
// Add the latency to our measurement history
2019-06-22 15:53:09 -04:00
pb - > latency_history [ pb - > latency_counter ] = ( double ) diff ;
pb - > latency_counter + + ;
2019-04-07 18:50:20 -04:00
// Haven't collected enough samples for sync evaluation yet, so just return
2019-06-22 15:53:09 -04:00
if ( pb - > latency_counter < alsa_latency_history_size )
2019-04-07 18:50:20 -04:00
return ALSA_SYNC_OK ;
2019-06-22 15:53:09 -04:00
pb - > latency_counter = 0 ;
2019-04-07 18:50:20 -04:00
2019-06-22 15:53:09 -04:00
ret = linear_regression ( drift , latency , & r2 , NULL , pb - > 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
2019-09-18 15:28:15 -04:00
if ( fabs ( * latency ) < ALSA_MAX_LATENCY & & fabs ( * drift ) < ALSA_MAX_DRIFT )
2019-04-07 18:50:20 -04:00
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-06-22 15:53:09 -04:00
sync_correct ( struct alsa_playback_session * pb , 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 ;
2022-01-20 14:08:47 -05:00
int ret ;
2019-04-07 18:50:20 -04:00
// 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.
2019-06-22 15:53:09 -04:00
step = ALSA_RESAMPLE_STEP_MULTIPLE * ( pb - > quality . sample_rate / 20000 ) ;
2019-04-07 18:50:20 -04:00
sign = ( drift < 0 ) ? - 1 : 1 ;
2019-06-22 15:53:09 -04:00
if ( abs ( pb - > sync_resample_step ) = = ALSA_RESAMPLE_STEP_MAX )
2019-04-07 18:50:20 -04:00
{
2019-06-22 15:53:09 -04:00
DPRINTF ( E_LOG , L_LAUDIO , " The sync of ALSA device cannot be corrected (drift=%f, latency=%f) \n " , drift , latency ) ;
pb - > sync_resample_step + = sign ;
2019-04-07 18:50:20 -04:00
return ;
}
2019-06-22 15:53:09 -04:00
else if ( abs ( pb - > sync_resample_step ) > ALSA_RESAMPLE_STEP_MAX )
2019-04-07 18:50:20 -04:00
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
2019-06-22 15:53:09 -04:00
if ( pb - > sync_resample_step ! = 0 )
outputs_quality_unsubscribe ( & pb - > quality ) ;
2019-04-07 18:50:20 -04:00
2019-06-22 15:53:09 -04:00
pb - > sync_resample_step + = sign ;
pb - > quality . sample_rate + = sign * step ;
2019-04-07 18:50:20 -04:00
2019-06-22 15:53:09 -04:00
if ( pb - > sync_resample_step ! = 0 )
2022-01-20 14:08:47 -05:00
{
ret = outputs_quality_subscribe ( & pb - > quality ) ;
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_LAUDIO , " Error adjusting sample rate to %d to maintain sync \n " , pb - > quality . sample_rate ) ;
return ;
}
}
2019-04-07 18:50:20 -04:00
// Reset position so next sync_correct latency correction is only based on
// what has elapsed since our correction
2019-06-22 15:53:09 -04:00
pb - > stamp_pos = ( uint64_t ) pb - > pos - ( delay + BTOS ( pb - > prebuf . read_avail , pb - > quality . bits_per_sample , pb - > quality . channels ) ) ; ;
pb - > stamp_pts = pts ;
2019-04-07 18:50:20 -04:00
2019-06-22 15:53:09 -04:00
DPRINTF ( E_INFO , L_LAUDIO , " Adjusted sample rate to %d to sync ALSA device (drift=%f, latency=%f) \n " , pb - > quality . sample_rate , drift , latency ) ;
2019-02-22 02:41:33 -05:00
}
2019-06-22 15:53:09 -04:00
static int
playback_drain ( struct alsa_playback_session * pb )
{
uint8_t * buf ;
2019-10-22 13:41:48 -04:00
ssize_t bufsize ;
2019-06-22 15:53:09 -04:00
snd_pcm_state_t state ;
snd_pcm_sframes_t avail ;
snd_pcm_sframes_t delay ;
snd_pcm_sframes_t nsamp ;
int ret ;
state = snd_pcm_state ( pb - > pcm ) ;
if ( state = = SND_PCM_STATE_DRAINING )
return 0 ;
else if ( state ! = SND_PCM_STATE_RUNNING )
return ALSA_ERROR_SESSION ; // We are probably done draining, so this makes the caller close the pb session
// If the prebuffer is empty we are done writing to this pcm
if ( pb - > prebuf . read_avail = = 0 )
{
snd_pcm_drain ( pb - > pcm ) ; // Plays pending frames and then stops the pcm
return 0 ;
}
ret = snd_pcm_avail_delay ( pb - > pcm , & avail , & delay ) ;
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_LAUDIO , " Error getting avail/delay: %s \n " , snd_strerror ( ret ) ) ;
return ALSA_ERROR_SESSION ;
}
// Maximum amount of bytes we want to read
2019-10-22 13:41:48 -04:00
bufsize = snd_pcm_frames_to_bytes ( pb - > pcm , avail ) ;
2019-06-22 15:53:09 -04:00
bufsize = ringbuffer_read ( & buf , bufsize , & pb - > prebuf ) ;
if ( bufsize = = 0 )
return 0 ; // avail too low to actually write anything
// DPRINTF(E_DBG, L_LAUDIO, "Draining prebuffer (read_avail=%zu, bufsize=%zu, avail=%li)\n", pb->prebuf.read_avail / 4, bufsize, avail);
2019-10-22 13:41:48 -04:00
nsamp = snd_pcm_bytes_to_frames ( pb - > pcm , bufsize ) ;
2019-06-22 15:53:09 -04:00
ret = snd_pcm_writei ( pb - > pcm , buf , nsamp ) ;
return ( ( ret < 0 ) ? ALSA_ERROR_SESSION : 0 ) ;
}
static int
playback_write ( struct alsa_playback_session * pb , struct output_buffer * obuf )
2016-04-07 17:35:34 -04:00
{
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 ;
2019-06-22 15:53:09 -04:00
int ret ;
2019-02-22 02:41:33 -05:00
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 + + )
{
2019-06-22 15:53:09 -04:00
if ( quality_is_equal ( & pb - > quality , & obuf - > data [ i ] . quality ) )
2019-02-22 02:41:33 -05:00
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 " ) ;
2019-06-22 15:53:09 -04:00
return - 1 ;
2019-02-22 02:41:33 -05:00
}
2016-04-07 17:35:34 -04:00
2019-06-22 15:53:09 -04:00
prebuffering = ( pb - > pos + obuf - > data [ i ] . bufsize < = pb - > 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
2019-06-22 15:53:09 -04:00
pb - > pos + = buffer_write ( pb , & obuf - > data [ i ] , 0 ) ;
return 0 ;
2016-04-07 17:35:34 -04:00
}
2019-06-22 15:53:09 -04:00
ret = snd_pcm_avail_delay ( pb - > pcm , & avail , & delay ) ;
if ( ret < 0 )
goto alsa_error ;
2019-02-23 17:24:36 -05:00
// Check sync each second (or if this is first write where last_pts is zero)
2019-06-22 15:53:09 -04:00
if ( ! alsa_sync_disable & & ( obuf - > pts . tv_sec ! = pb - > last_pts . tv_sec ) )
2019-02-22 02:41:33 -05:00
{
2019-06-22 15:53:09 -04:00
sync = sync_check ( & drift , & latency , pb , delay ) ;
if ( sync ! = ALSA_SYNC_OK )
sync_correct ( pb , drift , latency , obuf - > pts , delay ) ;
2016-04-07 17:35:34 -04:00
2019-06-22 15:53:09 -04:00
pb - > last_pts = obuf - > pts ;
2019-02-22 02:41:33 -05:00
}
2016-04-07 17:35:34 -04:00
2019-06-22 15:53:09 -04:00
ret = buffer_write ( pb , & obuf - > data [ i ] , avail ) ;
2016-04-07 17:35:34 -04:00
if ( ret < 0 )
goto alsa_error ;
2019-06-22 15:53:09 -04:00
pb - > pos + = ret ;
2019-02-22 02:41:33 -05:00
2019-06-22 15:53:09 -04:00
return 0 ;
2016-04-04 10:58:07 -04:00
alsa_error :
if ( ret = = - EPIPE )
{
2019-06-22 15:53:09 -04:00
DPRINTF ( E_WARN , L_LAUDIO , " ALSA buffer underrun, restarting session \n " ) ;
return ALSA_ERROR_UNDERRUN ;
2016-04-04 10:58:07 -04:00
}
DPRINTF ( E_LOG , L_LAUDIO , " ALSA write error: %s \n " , snd_strerror ( ret ) ) ;
2019-06-22 15:53:09 -04:00
return ALSA_ERROR_WRITE ;
2016-04-04 10:58:07 -04:00
}
2019-02-22 02:41:33 -05:00
/* ---------------------------- SESSION HANDLING ---------------------------- */
static void
alsa_session_free ( struct alsa_session * as )
{
if ( ! as )
return ;
outputs_quality_unsubscribe ( & alsa_fallback_quality ) ;
2019-06-22 15:53:09 -04:00
playback_session_remove_all ( as ) ;
mixer_close ( & as - > mixer , as - > mixer_device_name ) ;
2019-02-22 02:41:33 -05:00
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 ;
2020-01-03 04:06:58 -05:00
struct alsa_extra * ae ;
2016-04-04 10:58:07 -04:00
int ret ;
2020-01-03 04:06:58 -05:00
ae = device - > extra_device_info ;
2019-02-22 02:41:33 -05:00
2020-01-03 04:06:58 -05:00
CHECK_NULL ( L_LAUDIO , as = calloc ( 1 , sizeof ( struct alsa_session ) ) ) ;
2019-12-22 14:08:43 -05:00
2019-02-22 02:41:33 -05:00
as - > device_id = device - > id ;
as - > callback_id = callback_id ;
2020-01-03 04:06:58 -05:00
as - > devname = ae - > card_name ;
as - > mixer_name = ae - > mixer_name ;
as - > mixer_device_name = ae - > mixer_device_name ;
as - > offset_ms = ae - > offset_ms ;
2019-02-22 02:41:33 -05:00
2019-06-22 15:53:09 -04:00
ret = mixer_open ( & as - > mixer , as - > mixer_device_name , as - > mixer_name ) ;
2016-04-04 10:58:07 -04:00
if ( ret < 0 )
{
2019-06-22 15:53:09 -04:00
DPRINTF ( E_LOG , L_LAUDIO , " Could not open mixer '%s' ('%s') \n " , as - > mixer_device_name , as - > mixer_name ) ;
goto error_free_session ;
2016-04-04 10:58:07 -04:00
}
2019-02-22 02:41:33 -05:00
ret = outputs_quality_subscribe ( & alsa_fallback_quality ) ;
if ( ret < 0 )
{
DPRINTF ( E_LOG , L_LAUDIO , " Could not subscribe to fallback audio quality \n " ) ;
2019-06-22 15:53:09 -04:00
goto error_mixer_close ;
2019-02-22 02:41:33 -05:00
}
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 ;
2019-06-22 15:53:09 -04:00
error_mixer_close :
mixer_close ( & as - > mixer , as - > mixer_device_name ) ;
error_free_session :
2019-02-22 02:41:33 -05:00
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 ;
2020-04-14 02:09:07 -04:00
volume_set ( & as - > mixer , device - > volume ) ;
2019-02-22 02:41:33 -05:00
as - > state = OUTPUT_STATE_CONNECTED ;
2016-04-04 10:58:07 -04:00
alsa_status ( as ) ;
2020-05-01 14:11:45 -04:00
return 1 ;
2016-04-04 10:58:07 -04:00
}
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
2020-05-01 14:11:45 -04:00
return 1 ;
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-06-22 15:53:09 -04:00
playback_session_remove_all ( as ) ;
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 ) ;
2020-05-01 14:11:45 -04:00
return 1 ;
2019-02-16 18:19:13 -05:00
}
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
2020-05-01 14:11:45 -04:00
return 1 ;
2016-04-04 10:58:07 -04:00
}
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
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-06-22 15:53:09 -04:00
volume_set ( & as - > mixer , device - > volume ) ;
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
}
2020-01-03 04:06:58 -05:00
static void
alsa_device_free_extra ( struct output_device * device )
{
struct alsa_extra * ae = device - > extra_device_info ;
free ( ae ) ;
}
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-06-22 15:53:09 -04:00
struct alsa_session * as_next ;
struct alsa_playback_session * pb ;
struct alsa_playback_session * pb_next ;
bool quality_changed ;
int ret ;
quality_changed = ! quality_is_equal ( & obuf - > data [ 0 ] . quality , & alsa_last_quality ) ;
alsa_last_quality = obuf - > data [ 0 ] . quality ;
2016-04-04 10:58:07 -04:00
2019-06-22 15:53:09 -04:00
for ( as = sessions ; as ; as = as - > next )
2016-04-04 10:58:07 -04:00
{
2019-06-22 15:53:09 -04:00
if ( quality_changed | | as - > state = = OUTPUT_STATE_CONNECTED )
{
ret = playback_session_add ( as , & obuf - > data [ 0 ] . quality , obuf - > pts ) ;
if ( ret < 0 )
{
as - > state = OUTPUT_STATE_FAILED ;
continue ;
}
as - > state = OUTPUT_STATE_STREAMING ;
}
2016-04-04 10:58:07 -04:00
2019-06-22 15:53:09 -04:00
for ( pb = as - > pb ; pb ; pb = pb_next )
{
pb_next = pb - > next ;
// If !pb_next then it means that it is the most recent session, so it
// is setup with the quality level that matches obuf. The other pb's
// may still have data that needs to be written before removal.
if ( ! pb_next )
ret = playback_write ( pb , obuf ) ;
else
ret = playback_drain ( pb ) ;
if ( ret < 0 )
{
playback_session_remove ( as , pb ) ; // pb becomes invalid
if ( ret = = ALSA_ERROR_WRITE )
as - > state = OUTPUT_STATE_FAILED ;
else if ( ret = = ALSA_ERROR_UNDERRUN )
as - > state = OUTPUT_STATE_CONNECTED ;
}
}
}
// Cleanup failed sessions
for ( as = sessions ; as ; as = as_next )
{
as_next = as - > next ;
2016-04-04 10:58:07 -04:00
2019-06-22 15:53:09 -04:00
if ( as - > state = = OUTPUT_STATE_FAILED )
alsa_status ( as ) ; // as becomes invalid
2016-04-04 10:58:07 -04:00
}
}
2019-12-22 14:08:43 -05:00
static void
2020-01-03 04:06:58 -05:00
alsa_device_add ( cfg_t * cfg_audio , int id )
2019-12-22 14:08:43 -05:00
{
struct output_device * device ;
2020-01-03 04:06:58 -05:00
struct alsa_extra * ae ;
2020-01-06 10:38:32 -05:00
const char * nickname ;
2020-01-03 04:06:58 -05:00
int ret ;
2019-12-22 14:08:43 -05:00
CHECK_NULL ( L_LAUDIO , device = calloc ( 1 , sizeof ( struct output_device ) ) ) ;
2020-01-03 04:06:58 -05:00
CHECK_NULL ( L_LAUDIO , ae = calloc ( 1 , sizeof ( struct alsa_extra ) ) ) ;
2019-12-22 14:08:43 -05:00
device - > id = id ;
device - > type = OUTPUT_TYPE_ALSA ;
device - > type_name = outputs_name ( device - > type ) ;
2020-01-03 04:06:58 -05:00
device - > extra_device_info = ae ;
// The audio section will have no title, so there we get the value from the
// "card" option
ae - > card_name = cfg_title ( cfg_audio ) ;
if ( ! ae - > card_name )
ae - > card_name = cfg_getstr ( cfg_audio , " card " ) ;
2019-12-22 14:08:43 -05:00
2020-01-06 10:38:32 -05:00
nickname = cfg_getstr ( cfg_audio , " nickname " ) ;
device - > name = strdup ( nickname ? nickname : ae - > card_name ) ;
2020-01-03 04:06:58 -05:00
ae - > mixer_name = cfg_getstr ( cfg_audio , " mixer " ) ;
2020-01-03 14:37:07 -05:00
ae - > mixer_device_name = cfg_getstr ( cfg_audio , " mixer_device " ) ;
2020-01-03 04:06:58 -05:00
if ( ! ae - > mixer_device_name | | strlen ( ae - > mixer_device_name ) = = 0 )
ae - > mixer_device_name = ae - > card_name ;
2019-12-22 14:08:43 -05:00
2020-01-03 04:06:58 -05:00
ae - > offset_ms = cfg_getint ( cfg_audio , " offset_ms " ) ;
if ( abs ( ae - > offset_ms ) > 1000 )
{
DPRINTF ( E_LOG , L_LAUDIO , " The ALSA offset_ms (%d) set in the configuration is out of bounds \n " , ae - > offset_ms ) ;
ae - > offset_ms = 1000 * ( ae - > offset_ms / abs ( ae - > offset_ms ) ) ;
}
DPRINTF ( E_INFO , L_LAUDIO , " Adding ALSA device '%s' with name '%s' \n " , ae - > card_name , device - > name ) ;
ret = player_device_add ( device ) ;
if ( ret < 0 )
outputs_device_free ( device ) ;
2019-12-22 14:08:43 -05:00
}
2016-04-04 10:58:07 -04:00
static int
alsa_init ( void )
{
cfg_t * cfg_audio ;
2019-12-22 14:08:43 -05:00
cfg_t * cfg_alsasec ;
2019-02-22 02:41:33 -05:00
const char * type ;
2019-12-22 14:08:43 -05:00
int i ;
int alsa_cfg_secn ;
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 ;
2020-04-18 15:35:47 -04:00
cards_list ( ) ;
2020-04-16 12:11:05 -04:00
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-12-22 14:08:43 -05:00
alsa_cfg_secn = cfg_size ( cfg , " alsa " ) ;
if ( alsa_cfg_secn = = 0 )
{
2020-01-03 04:06:58 -05:00
alsa_device_add ( cfg_audio , 0 ) ;
2019-12-22 14:08:43 -05:00
}
else
{
for ( i = 0 ; i < alsa_cfg_secn ; + + i )
{
cfg_alsasec = cfg_getnsec ( cfg , " alsa " , i ) ;
2020-01-03 04:06:58 -05:00
alsa_device_add ( cfg_alsasec , i ) ;
2019-12-22 14:08:43 -05:00
}
}
2016-04-04 10:58:07 -04:00
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 ,
2020-01-03 04:06:58 -05:00
. device_free_extra = alsa_device_free_extra ,
2016-04-04 10:58:07 -04:00
. write = alsa_write ,
} ;