Add three parameters to the backend play() function call -- (1) a flag indicating whether the samples are timed or not. If timed, (2) the timestamp and (3) the local time at which the first frame should be heard.
Update missing pipewire library message.
Makefile.am fix.
Update the SHM version and fix the SHM name so that it works with FreeBSD.
Remove redundant (?) AC_HEADER_STDC check.
Fix compilation and installation under FreeBSD -- changes to allow building in a separate directory broke the FreeBSD build process.
Add AC_CHECK_LIB for gcrypt in case PKG_CHECK_MODULES fails to find it.
Find gcrypt using pkg-config
Add code to detect when the RTSP channel goes idle for a period, and use it to reset clocks and timings, etc. so that SPS resumes correctly where the source has gone to sleep while playing a realtime stream and has subsequently woken up.
Don't exit if a UDP Clock Control packet is empty. Also check minimum packet size.
Remove some very experimental code which may cause memory and port leaks.
Strip file path from the filename used in debug messages, information messages, warnings and fatal error messages.
BB fix to Makefile.am -- overwriting the DBUS stuff with MPRIS definitions, duh.
Fix some problems building on FreeBSD and tidy up the use of the "sed" editor.
Fix some problems building on picore.
Fix an uninitialised variable
start using the "nqptp" SMI
2161 lines
84 KiB
C
2161 lines
84 KiB
C
/*
|
|
* libalsa output driver. This file is part of Shairport.
|
|
* Copyright (c) Muffinman, Skaman 2013
|
|
* Copyright (c) Mike Brady 2014 -- 2022
|
|
* All rights reserved.
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or
|
|
* sell copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
#define ALSA_PCM_NEW_HW_PARAMS_API
|
|
|
|
#include <alsa/asoundlib.h>
|
|
#include <inttypes.h>
|
|
#include <math.h>
|
|
#include <memory.h>
|
|
#include <pthread.h>
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
|
|
#include "config.h"
|
|
|
|
#include "activity_monitor.h"
|
|
#include "audio.h"
|
|
#include "common.h"
|
|
|
|
enum alsa_backend_mode {
|
|
abm_disconnected,
|
|
abm_connected,
|
|
abm_playing
|
|
} alsa_backend_state; // under the control of alsa_mutex
|
|
|
|
typedef struct {
|
|
snd_pcm_format_t alsa_code;
|
|
int frame_size;
|
|
} format_record;
|
|
|
|
int output_method_signalled = 0; // for reporting whether it's using mmap or not
|
|
int delay_type_notified = -1; // for controlling the reporting of whether the output device can do
|
|
// precision delays (e.g. alsa->pulsaudio virtual devices can't)
|
|
int use_monotonic_clock = 0; // this value will be set when the hardware is initialised
|
|
|
|
static void help(void);
|
|
static int init(int argc, char **argv);
|
|
static void deinit(void);
|
|
static void start(int i_sample_rate, int i_sample_format);
|
|
static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
|
__attribute__((unused)) uint32_t timestamp,
|
|
__attribute__((unused)) uint64_t playtime);
|
|
static void stop(void);
|
|
static void flush(void);
|
|
static int delay(long *the_delay);
|
|
static int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time,
|
|
uint64_t *the_delay, uint64_t *frames_sent_to_dac);
|
|
|
|
static void *alsa_buffer_monitor_thread_code(void *arg);
|
|
|
|
static void volume(double vol);
|
|
static void do_volume(double vol);
|
|
static int prepare(void);
|
|
static int do_play(void *buf, int samples);
|
|
|
|
static void parameters(audio_parameters *info);
|
|
static int mute(int do_mute); // returns true if it actually is allowed to use the mute
|
|
static double set_volume;
|
|
audio_output audio_alsa = {
|
|
.name = "alsa",
|
|
.help = &help,
|
|
.init = &init,
|
|
.deinit = &deinit,
|
|
.prepare = &prepare,
|
|
.start = &start,
|
|
.stop = &stop,
|
|
.is_running = NULL,
|
|
.flush = &flush,
|
|
.delay = &delay,
|
|
.play = &play,
|
|
.stats = &stats, // will also include frames of silence sent to stop
|
|
// standby mode
|
|
// .rate_info = NULL,
|
|
.mute = NULL, // a function will be provided if it can, and is allowed to,
|
|
// do hardware mute
|
|
.volume = NULL, // a function will be provided if it can do hardware volume
|
|
.parameters = NULL}; // a function will be provided if it can do hardware volume
|
|
|
|
pthread_mutex_t alsa_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
pthread_mutex_t alsa_mixer_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
pthread_t alsa_buffer_monitor_thread;
|
|
|
|
// for deciding when to activate mute
|
|
// there are two sources of requests to mute -- the backend itself, e.g. when it
|
|
// is flushing
|
|
// and the player, e.g. when volume goes down to -144, i.e. mute.
|
|
|
|
// we may not be allowed to use hardware mute, so we must reflect that too.
|
|
|
|
int mute_requested_externally = 0;
|
|
int mute_requested_internally = 0;
|
|
|
|
// for tracking how long the output device has stalled
|
|
uint64_t stall_monitor_start_time; // zero if not initialised / not started /
|
|
// zeroed by flush
|
|
long stall_monitor_frame_count; // set to delay at start of time, incremented by
|
|
// any writes
|
|
uint64_t stall_monitor_error_threshold; // if the time is longer than this, it's
|
|
// an error
|
|
|
|
snd_output_t *output = NULL;
|
|
int frame_size; // in bytes for interleaved stereo
|
|
|
|
int alsa_device_initialised; // boolean to ensure the initialisation is only
|
|
// done once
|
|
|
|
yndk_type precision_delay_available_status =
|
|
YNDK_DONT_KNOW; // initially, we don't know if the device can do precision delay
|
|
|
|
snd_pcm_t *alsa_handle = NULL;
|
|
int alsa_handle_status =
|
|
ENODEV; // if alsa_handle is NULL, this should say why with a unix error code
|
|
snd_pcm_hw_params_t *alsa_params = NULL;
|
|
snd_pcm_sw_params_t *alsa_swparams = NULL;
|
|
snd_ctl_t *ctl = NULL;
|
|
snd_ctl_elem_id_t *elem_id = NULL;
|
|
snd_mixer_t *alsa_mix_handle = NULL;
|
|
snd_mixer_elem_t *alsa_mix_elem = NULL;
|
|
snd_mixer_selem_id_t *alsa_mix_sid = NULL;
|
|
long alsa_mix_minv, alsa_mix_maxv;
|
|
long alsa_mix_mindb, alsa_mix_maxdb;
|
|
|
|
char *alsa_out_dev = "default";
|
|
char *alsa_mix_dev = NULL;
|
|
char *alsa_mix_ctrl = NULL;
|
|
int alsa_mix_index = 0;
|
|
int has_softvol = 0;
|
|
|
|
int64_t dither_random_number_store = 0;
|
|
|
|
int volume_set_request = 0; // set when an external request is made to set the volume.
|
|
|
|
int mixer_volume_setting_gives_mute = 0; // set when it is discovered that
|
|
// particular mixer volume setting
|
|
// causes a mute.
|
|
long alsa_mix_mute; // setting the volume to this value mutes output, if
|
|
// mixer_volume_setting_gives_mute is true
|
|
int volume_based_mute_is_active =
|
|
0; // set when muting is being done by a setting the volume to a magic value
|
|
|
|
// use this to allow the use of snd_pcm_writei or snd_pcm_mmap_writei
|
|
snd_pcm_sframes_t (*alsa_pcm_write)(snd_pcm_t *, const void *, snd_pcm_uframes_t) = snd_pcm_writei;
|
|
|
|
static int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay,
|
|
yndk_type *using_update_timestamps);
|
|
static int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay,
|
|
yndk_type *using_update_timestamps);
|
|
|
|
// use this to allow the use of standard or precision delay calculations, with standard the, uh,
|
|
// standard.
|
|
int (*delay_and_status)(snd_pcm_state_t *state, snd_pcm_sframes_t *delay,
|
|
yndk_type *using_update_timestamps) = standard_delay_and_status;
|
|
|
|
// this will return true if the DAC can return precision delay information and false if not
|
|
// if it is not yet known, it will test the output device to find out
|
|
|
|
// note -- once it has done the test, it decides -- even if the delay comes back with
|
|
// "don't know", it will take that as a "No" and remember it.
|
|
// If you want it to check again, set precision_delay_available_status to YNDK_DONT_KNOW
|
|
// first.
|
|
|
|
static int precision_delay_available() {
|
|
if (precision_delay_available_status == YNDK_DONT_KNOW) {
|
|
// this is very crude -- if the device is a hardware device, then it's assumed the delay is
|
|
// precise
|
|
const char *output_device_name = snd_pcm_name(alsa_handle);
|
|
int is_a_real_hardware_device = (strstr(output_device_name, "hw:") == output_device_name);
|
|
|
|
// The criteria as to whether precision delay is available
|
|
// is whether the device driver returns non-zero update timestamps
|
|
// If it does, and the device is a hardware device (i.e. its name begins with "hw:"),
|
|
// it is considered that precision delay is available. Otherwise, it's considered to be
|
|
// unavailable.
|
|
|
|
// To test, we play a silence buffer (fairly large to avoid underflow)
|
|
// and then we check the delay return. It will tell us if it
|
|
// was able to use the (non-zero) update timestamps
|
|
|
|
int frames_of_silence = 4410;
|
|
size_t size_of_silence_buffer = frames_of_silence * frame_size;
|
|
void *silence = malloc(size_of_silence_buffer);
|
|
if (silence == NULL) {
|
|
debug(1, "alsa: precision_delay_available -- failed to "
|
|
"allocate memory for a "
|
|
"silent frame buffer.");
|
|
} else {
|
|
pthread_cleanup_push(malloc_cleanup, silence);
|
|
int use_dither = 0;
|
|
if ((alsa_mix_ctrl == NULL) && (config.ignore_volume_control == 0) &&
|
|
(config.airplay_volume != 0.0))
|
|
use_dither = 1;
|
|
dither_random_number_store =
|
|
generate_zero_frames(silence, frames_of_silence, config.output_format,
|
|
use_dither, // i.e. with dither
|
|
dither_random_number_store);
|
|
do_play(silence, frames_of_silence);
|
|
pthread_cleanup_pop(1);
|
|
// now we can get the delay, and we'll note if it uses update timestamps
|
|
yndk_type uses_update_timestamps;
|
|
snd_pcm_state_t state;
|
|
snd_pcm_sframes_t delay;
|
|
int ret = precision_delay_and_status(&state, &delay, &uses_update_timestamps);
|
|
// debug(3,"alsa: precision_delay_available asking for delay and status with a return status
|
|
// of %d, a delay of %ld and a uses_update_timestamps of %d.", ret, delay,
|
|
// uses_update_timestamps);
|
|
if (ret == 0) {
|
|
if ((uses_update_timestamps == YNDK_YES) && (is_a_real_hardware_device)) {
|
|
precision_delay_available_status = YNDK_YES;
|
|
debug(2, "alsa: precision delay timing is available.");
|
|
} else {
|
|
if ((uses_update_timestamps == YNDK_YES) && (!is_a_real_hardware_device)) {
|
|
debug(2, "alsa: precision delay timing is not available because it's not definitely a "
|
|
"hardware device.");
|
|
} else {
|
|
debug(2, "alsa: precision delay timing is not available.");
|
|
}
|
|
precision_delay_available_status = YNDK_NO;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return (precision_delay_available_status == YNDK_YES);
|
|
}
|
|
|
|
int alsa_characteristics_already_listed = 0;
|
|
|
|
snd_pcm_uframes_t period_size_requested, buffer_size_requested;
|
|
int set_period_size_request, set_buffer_size_request;
|
|
|
|
uint64_t frames_sent_for_playing;
|
|
|
|
// set to true if there has been a discontinuity between the last reported frames_sent_for_playing
|
|
// and the present reported frames_sent_for_playing
|
|
// Note that it will be set when the device is opened, as any previous figures for
|
|
// frames_sent_for_playing (which Shairport Sync might hold) would be invalid.
|
|
int frames_sent_break_occurred;
|
|
|
|
static void help(void) {
|
|
printf(" -d output-device set the output device, default is \"default\".\n"
|
|
" -c mixer-control set the mixer control name, default is to use no mixer.\n"
|
|
" -m mixer-device set the mixer device, default is the output device.\n"
|
|
" -i mixer-index set the mixer index, default is 0.\n");
|
|
int r = system("if [ -d /proc/asound ] ; then echo \" hardware output devices:\" ; ls -al "
|
|
"/proc/asound/ 2>/dev/null | grep '\\->' | tr -s ' ' | cut -d ' ' -f 9 | while "
|
|
"read line; do echo \" \\\"hw:$line\\\"\" ; done ; fi");
|
|
if (r != 0)
|
|
debug(2, "error %d executing a script to list alsa hardware device names", r);
|
|
}
|
|
|
|
void set_alsa_out_dev(char *dev) { alsa_out_dev = dev; } // ugh -- not static!
|
|
|
|
// assuming pthread cancellation is disabled
|
|
static int open_mixer() {
|
|
int response = 0;
|
|
if (alsa_mix_ctrl != NULL) {
|
|
debug(3, "Open Mixer");
|
|
int ret = 0;
|
|
snd_mixer_selem_id_alloca(&alsa_mix_sid);
|
|
snd_mixer_selem_id_set_index(alsa_mix_sid, alsa_mix_index);
|
|
snd_mixer_selem_id_set_name(alsa_mix_sid, alsa_mix_ctrl);
|
|
|
|
if ((snd_mixer_open(&alsa_mix_handle, 0)) < 0) {
|
|
debug(1, "Failed to open mixer");
|
|
response = -1;
|
|
} else {
|
|
debug(3, "Mixer device name is \"%s\".", alsa_mix_dev);
|
|
if ((snd_mixer_attach(alsa_mix_handle, alsa_mix_dev)) < 0) {
|
|
debug(1, "Failed to attach mixer");
|
|
response = -2;
|
|
} else {
|
|
if ((snd_mixer_selem_register(alsa_mix_handle, NULL, NULL)) < 0) {
|
|
debug(1, "Failed to register mixer element");
|
|
response = -3;
|
|
} else {
|
|
ret = snd_mixer_load(alsa_mix_handle);
|
|
if (ret < 0) {
|
|
debug(1, "Failed to load mixer element");
|
|
response = -4;
|
|
} else {
|
|
debug(3, "Mixer control is \"%s\",%d.", alsa_mix_ctrl, alsa_mix_index);
|
|
alsa_mix_elem = snd_mixer_find_selem(alsa_mix_handle, alsa_mix_sid);
|
|
if (!alsa_mix_elem) {
|
|
warn("failed to find mixer control \"%s\",%d.", alsa_mix_ctrl, alsa_mix_index);
|
|
response = -5;
|
|
} else {
|
|
response = 1; // we found a hardware mixer and successfully opened it
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return response;
|
|
}
|
|
|
|
// assuming pthread cancellation is disabled
|
|
static void close_mixer() {
|
|
if (alsa_mix_handle) {
|
|
snd_mixer_close(alsa_mix_handle);
|
|
alsa_mix_handle = NULL;
|
|
}
|
|
}
|
|
|
|
// assuming pthread cancellation is disabled
|
|
static void do_snd_mixer_selem_set_playback_dB_all(snd_mixer_elem_t *mix_elem, double vol) {
|
|
if (snd_mixer_selem_set_playback_dB_all(mix_elem, vol, 0) != 0) {
|
|
debug(1, "Can't set playback volume accurately to %f dB.", vol);
|
|
if (snd_mixer_selem_set_playback_dB_all(mix_elem, vol, -1) != 0)
|
|
if (snd_mixer_selem_set_playback_dB_all(mix_elem, vol, 1) != 0)
|
|
debug(1, "Could not set playback dB volume on the mixer.");
|
|
}
|
|
}
|
|
|
|
// This array is a sequence of the output rates to be tried if automatic speed selection is
|
|
// requested.
|
|
// There is no benefit to upconverting the frame rate, other than for compatibility.
|
|
// The lowest rate that the DAC is capable of is chosen.
|
|
|
|
unsigned int auto_speed_output_rates[] = {
|
|
44100,
|
|
88200,
|
|
176400,
|
|
352800,
|
|
};
|
|
|
|
// This array is of all the formats known to Shairport Sync, in order of the SPS_FORMAT definitions,
|
|
// with their equivalent alsa codes and their frame sizes.
|
|
// If just one format is requested, then its entry is searched for in the array and checked on the
|
|
// device
|
|
// If auto format is requested, then each entry in turn is tried until a working format is found.
|
|
// So, it should be in the search order.
|
|
|
|
format_record fr[] = {
|
|
{SND_PCM_FORMAT_UNKNOWN, 0}, // unknown
|
|
{SND_PCM_FORMAT_S8, 2}, {SND_PCM_FORMAT_U8, 2}, {SND_PCM_FORMAT_S16, 4},
|
|
{SND_PCM_FORMAT_S16_LE, 4}, {SND_PCM_FORMAT_S16_BE, 4}, {SND_PCM_FORMAT_S24, 8},
|
|
{SND_PCM_FORMAT_S24_LE, 8}, {SND_PCM_FORMAT_S24_BE, 8}, {SND_PCM_FORMAT_S24_3LE, 6},
|
|
{SND_PCM_FORMAT_S24_3BE, 6}, {SND_PCM_FORMAT_S32, 8}, {SND_PCM_FORMAT_S32_LE, 8},
|
|
{SND_PCM_FORMAT_S32_BE, 8}, {SND_PCM_FORMAT_UNKNOWN, 0}, // auto
|
|
{SND_PCM_FORMAT_UNKNOWN, 0}, // illegal
|
|
};
|
|
|
|
// This array is the sequence of formats to be tried if automatic selection of the format is
|
|
// requested.
|
|
// Ideally, audio should pass through Shairport Sync unaltered, apart from occasional interpolation.
|
|
// If the user chooses a hardware mixer, then audio could go straight through, unaltered, as signed
|
|
// 16 bit stereo.
|
|
// However, the user might, at any point, select an option that requires modification, such as
|
|
// stereo to mono mixing,
|
|
// additional volume attenuation, convolution, and so on. For this reason,
|
|
// we look for the greatest depth the DAC is capable of, since upconverting it is completely
|
|
// lossless.
|
|
// If audio processing is required, then the dither that must be added will
|
|
// be added at the lowest possible level.
|
|
// Hence, selecting the greatest bit depth is always either beneficial or neutral.
|
|
|
|
sps_format_t auto_format_check_sequence[] = {
|
|
SPS_FORMAT_S32, SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S24, SPS_FORMAT_S24_LE,
|
|
SPS_FORMAT_S24_BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S16, SPS_FORMAT_S16_LE,
|
|
SPS_FORMAT_S16_BE, SPS_FORMAT_S8, SPS_FORMAT_U8,
|
|
};
|
|
|
|
// assuming pthread cancellation is disabled
|
|
// if do_auto_setting is true and auto format or auto speed has been requested,
|
|
// select the settings as appropriate and store them
|
|
static int actual_open_alsa_device(int do_auto_setup) {
|
|
// the alsa mutex is already acquired when this is called
|
|
const snd_pcm_uframes_t minimal_buffer_headroom =
|
|
352 * 2; // we accept this much headroom in the hardware buffer, but we'll
|
|
// accept less
|
|
/*
|
|
const snd_pcm_uframes_t requested_buffer_headroom =
|
|
minimal_buffer_headroom + 2048; // we ask for this much headroom in the
|
|
// hardware buffer, but we'll accept
|
|
less
|
|
*/
|
|
|
|
int ret, dir = 0;
|
|
unsigned int
|
|
actual_sample_rate; // this will be given the rate requested and will be given the actual rate
|
|
// snd_pcm_uframes_t frames = 441 * 10;
|
|
snd_pcm_uframes_t actual_buffer_length;
|
|
snd_pcm_access_t access;
|
|
|
|
// ensure no calls are made to the alsa device enquiring about the buffer
|
|
// length if
|
|
// synchronisation is disabled.
|
|
if (config.no_sync != 0)
|
|
audio_alsa.delay = NULL;
|
|
|
|
// ensure no calls are made to the alsa device enquiring about the buffer
|
|
// length if
|
|
// synchronisation is disabled.
|
|
if (config.no_sync != 0)
|
|
audio_alsa.delay = NULL;
|
|
|
|
ret = snd_pcm_open(&alsa_handle, alsa_out_dev, SND_PCM_STREAM_PLAYBACK, 0);
|
|
// EHOSTDOWN seems to signify that it's a PipeWire pseudo device that can't be accessed by this
|
|
// user. So, try the first device ALSA device and log it.
|
|
if ((ret == -EHOSTDOWN) && (strcmp(alsa_out_dev, "default") == 0)) {
|
|
ret = snd_pcm_open(&alsa_handle, "hw:0", SND_PCM_STREAM_PLAYBACK, 0);
|
|
if ((ret == 0) || (ret == -EBUSY)) {
|
|
// being busy should be okay
|
|
inform("the default ALSA device is inaccessible -- \"hw:0\" used instead.", alsa_out_dev);
|
|
set_alsa_out_dev("hw:0");
|
|
}
|
|
}
|
|
if (ret == 0) {
|
|
if (alsa_handle_status == -EBUSY)
|
|
warn("The output device \"%s\" is no longer busy and will be used by Shairport Sync.",
|
|
alsa_out_dev);
|
|
alsa_handle_status = ret; // all cool
|
|
} else {
|
|
alsa_handle = NULL; // to be sure to be sure
|
|
if (ret == -EBUSY) {
|
|
if (alsa_handle_status != -EBUSY)
|
|
warn("The output device \"%s\" is busy and can't be used by Shairport Sync at present.",
|
|
alsa_out_dev);
|
|
debug(2, "the alsa output_device \"%s\" is busy.", alsa_out_dev);
|
|
} else if (ret == -ENOENT) {
|
|
die("the alsa output_device \"%s\" can not be found.", alsa_out_dev);
|
|
} else {
|
|
char errorstring[1024];
|
|
strerror_r(-ret, (char *)errorstring, sizeof(errorstring));
|
|
die("alsa: error %d (\"%s\") opening alsa device \"%s\".", ret, (char *)errorstring,
|
|
alsa_out_dev);
|
|
}
|
|
alsa_handle_status = ret;
|
|
frames_sent_break_occurred = 1;
|
|
return ret;
|
|
}
|
|
|
|
snd_pcm_hw_params_alloca(&alsa_params);
|
|
snd_pcm_sw_params_alloca(&alsa_swparams);
|
|
|
|
ret = snd_pcm_hw_params_any(alsa_handle, alsa_params);
|
|
if (ret < 0) {
|
|
die("audio_alsa: Broken configuration for device \"%s\": no configurations "
|
|
"available",
|
|
alsa_out_dev);
|
|
return ret;
|
|
}
|
|
|
|
if ((config.no_mmap == 0) &&
|
|
(snd_pcm_hw_params_set_access(alsa_handle, alsa_params, SND_PCM_ACCESS_MMAP_INTERLEAVED) >=
|
|
0)) {
|
|
if (output_method_signalled == 0) {
|
|
debug(3, "Output written using MMAP");
|
|
output_method_signalled = 1;
|
|
}
|
|
access = SND_PCM_ACCESS_MMAP_INTERLEAVED;
|
|
alsa_pcm_write = snd_pcm_mmap_writei;
|
|
} else {
|
|
if (output_method_signalled == 0) {
|
|
debug(3, "Output written with RW");
|
|
output_method_signalled = 1;
|
|
}
|
|
access = SND_PCM_ACCESS_RW_INTERLEAVED;
|
|
alsa_pcm_write = snd_pcm_writei;
|
|
}
|
|
|
|
ret = snd_pcm_hw_params_set_access(alsa_handle, alsa_params, access);
|
|
if (ret < 0) {
|
|
die("audio_alsa: Access type not available for device \"%s\": %s", alsa_out_dev,
|
|
snd_strerror(ret));
|
|
return ret;
|
|
}
|
|
|
|
ret = snd_pcm_hw_params_set_channels(alsa_handle, alsa_params, 2);
|
|
if (ret < 0) {
|
|
die("audio_alsa: Channels count (2) not available for device \"%s\": %s", alsa_out_dev,
|
|
snd_strerror(ret));
|
|
return ret;
|
|
}
|
|
|
|
snd_pcm_format_t sf;
|
|
|
|
if ((do_auto_setup == 0) || (config.output_format_auto_requested == 0)) { // no auto format
|
|
if ((config.output_format > SPS_FORMAT_UNKNOWN) && (config.output_format < SPS_FORMAT_AUTO)) {
|
|
sf = fr[config.output_format].alsa_code;
|
|
frame_size = fr[config.output_format].frame_size;
|
|
} else {
|
|
warn("alsa: unexpected output format %d. Set to S16_LE.", config.output_format);
|
|
config.output_format = SPS_FORMAT_S16_LE;
|
|
sf = fr[config.output_format].alsa_code;
|
|
frame_size = fr[config.output_format].frame_size;
|
|
}
|
|
ret = snd_pcm_hw_params_set_format(alsa_handle, alsa_params, sf);
|
|
if (ret < 0) {
|
|
die("audio_alsa: Alsa sample format %d not available for device \"%s\": %s", sf, alsa_out_dev,
|
|
snd_strerror(ret));
|
|
return ret;
|
|
}
|
|
} else { // auto format
|
|
int number_of_formats_to_try;
|
|
sps_format_t *formats;
|
|
formats = auto_format_check_sequence;
|
|
number_of_formats_to_try = sizeof(auto_format_check_sequence) / sizeof(sps_format_t);
|
|
int i = 0;
|
|
int format_found = 0;
|
|
sps_format_t trial_format = SPS_FORMAT_UNKNOWN;
|
|
while ((i < number_of_formats_to_try) && (format_found == 0)) {
|
|
trial_format = formats[i];
|
|
sf = fr[trial_format].alsa_code;
|
|
frame_size = fr[trial_format].frame_size;
|
|
ret = snd_pcm_hw_params_set_format(alsa_handle, alsa_params, sf);
|
|
if (ret == 0)
|
|
format_found = 1;
|
|
else
|
|
i++;
|
|
}
|
|
if (ret == 0) {
|
|
config.output_format = trial_format;
|
|
debug(2, "alsa: output format chosen is \"%s\".",
|
|
sps_format_description_string(config.output_format));
|
|
} else {
|
|
die("audio_alsa: Could not automatically set the output format for device \"%s\": %s",
|
|
alsa_out_dev, snd_strerror(ret));
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
if ((do_auto_setup == 0) || (config.output_rate_auto_requested == 0)) { // no auto format
|
|
actual_sample_rate =
|
|
config.output_rate; // this is the requested rate -- it'll be changed to the actual rate
|
|
ret = snd_pcm_hw_params_set_rate_near(alsa_handle, alsa_params, &actual_sample_rate, &dir);
|
|
if (ret < 0) {
|
|
die("audio_alsa: The frame rate of %i frames per second is not available for playback: %s",
|
|
config.output_rate, snd_strerror(ret));
|
|
return ret;
|
|
}
|
|
} else {
|
|
int number_of_speeds_to_try;
|
|
unsigned int *speeds;
|
|
|
|
speeds = auto_speed_output_rates;
|
|
number_of_speeds_to_try = sizeof(auto_speed_output_rates) / sizeof(int);
|
|
|
|
int i = 0;
|
|
int speed_found = 0;
|
|
|
|
while ((i < number_of_speeds_to_try) && (speed_found == 0)) {
|
|
actual_sample_rate = speeds[i];
|
|
ret = snd_pcm_hw_params_set_rate_near(alsa_handle, alsa_params, &actual_sample_rate, &dir);
|
|
if (ret == 0) {
|
|
speed_found = 1;
|
|
if (actual_sample_rate != speeds[i])
|
|
die("The output DAC can not be set to %d frames per second (fps). The nearest speed "
|
|
"available is %d fps.",
|
|
speeds[i], actual_sample_rate);
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
if (ret == 0) {
|
|
config.output_rate = actual_sample_rate;
|
|
debug(2, "alsa: output speed chosen is %d.", config.output_rate);
|
|
} else {
|
|
die("audio_alsa: Could not automatically set the output rate for device \"%s\": %s",
|
|
alsa_out_dev, snd_strerror(ret));
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
if (set_period_size_request != 0) {
|
|
debug(1, "Attempting to set the period size to %lu", period_size_requested);
|
|
ret = snd_pcm_hw_params_set_period_size_near(alsa_handle, alsa_params, &period_size_requested,
|
|
&dir);
|
|
if (ret < 0) {
|
|
warn("audio_alsa: cannot set period size of %lu: %s", period_size_requested,
|
|
snd_strerror(ret));
|
|
return ret;
|
|
} else {
|
|
snd_pcm_uframes_t actual_period_size;
|
|
snd_pcm_hw_params_get_period_size(alsa_params, &actual_period_size, &dir);
|
|
if (actual_period_size != period_size_requested)
|
|
inform("Actual period size set to a different value than requested. "
|
|
"Requested: %lu, actual "
|
|
"setting: %lu",
|
|
period_size_requested, actual_period_size);
|
|
}
|
|
}
|
|
|
|
if (set_buffer_size_request != 0) {
|
|
debug(1, "Attempting to set the buffer size to %lu", buffer_size_requested);
|
|
ret = snd_pcm_hw_params_set_buffer_size_near(alsa_handle, alsa_params, &buffer_size_requested);
|
|
if (ret < 0) {
|
|
warn("audio_alsa: cannot set buffer size of %lu: %s", buffer_size_requested,
|
|
snd_strerror(ret));
|
|
return ret;
|
|
} else {
|
|
snd_pcm_uframes_t actual_buffer_size;
|
|
snd_pcm_hw_params_get_buffer_size(alsa_params, &actual_buffer_size);
|
|
if (actual_buffer_size != buffer_size_requested)
|
|
inform("Actual period size set to a different value than requested. "
|
|
"Requested: %lu, actual "
|
|
"setting: %lu",
|
|
buffer_size_requested, actual_buffer_size);
|
|
}
|
|
}
|
|
|
|
ret = snd_pcm_hw_params(alsa_handle, alsa_params);
|
|
if (ret < 0) {
|
|
die("audio_alsa: Unable to set hw parameters for device \"%s\": %s.", alsa_out_dev,
|
|
snd_strerror(ret));
|
|
return ret;
|
|
}
|
|
|
|
// check parameters after attempting to set them
|
|
|
|
if (set_period_size_request != 0) {
|
|
snd_pcm_uframes_t actual_period_size;
|
|
snd_pcm_hw_params_get_period_size(alsa_params, &actual_period_size, &dir);
|
|
if (actual_period_size != period_size_requested)
|
|
inform("Actual period size set to a different value than requested. "
|
|
"Requested: %lu, actual "
|
|
"setting: %lu",
|
|
period_size_requested, actual_period_size);
|
|
}
|
|
|
|
if (set_buffer_size_request != 0) {
|
|
snd_pcm_uframes_t actual_buffer_size;
|
|
snd_pcm_hw_params_get_buffer_size(alsa_params, &actual_buffer_size);
|
|
if (actual_buffer_size != buffer_size_requested)
|
|
inform("Actual period size set to a different value than requested. "
|
|
"Requested: %lu, actual "
|
|
"setting: %lu",
|
|
buffer_size_requested, actual_buffer_size);
|
|
}
|
|
|
|
if (actual_sample_rate != config.output_rate) {
|
|
die("Can't set the output DAC to the requested frame rate of %d fps.", config.output_rate);
|
|
return -EINVAL;
|
|
}
|
|
|
|
use_monotonic_clock = snd_pcm_hw_params_is_monotonic(alsa_params);
|
|
|
|
ret = snd_pcm_hw_params_get_buffer_size(alsa_params, &actual_buffer_length);
|
|
if (ret < 0) {
|
|
warn("audio_alsa: Unable to get hw buffer length for device \"%s\": %s.", alsa_out_dev,
|
|
snd_strerror(ret));
|
|
return ret;
|
|
}
|
|
|
|
ret = snd_pcm_sw_params_current(alsa_handle, alsa_swparams);
|
|
if (ret < 0) {
|
|
warn("audio_alsa: Unable to get current sw parameters for device \"%s\": "
|
|
"%s.",
|
|
alsa_out_dev, snd_strerror(ret));
|
|
return ret;
|
|
}
|
|
|
|
ret = snd_pcm_sw_params_set_tstamp_mode(alsa_handle, alsa_swparams, SND_PCM_TSTAMP_ENABLE);
|
|
if (ret < 0) {
|
|
warn("audio_alsa: Can't enable timestamp mode of device: \"%s\": %s.", alsa_out_dev,
|
|
snd_strerror(ret));
|
|
return ret;
|
|
}
|
|
|
|
/* write the sw parameters */
|
|
ret = snd_pcm_sw_params(alsa_handle, alsa_swparams);
|
|
if (ret < 0) {
|
|
warn("audio_alsa: Unable to set software parameters of device: \"%s\": %s.", alsa_out_dev,
|
|
snd_strerror(ret));
|
|
return ret;
|
|
}
|
|
|
|
ret = snd_pcm_prepare(alsa_handle);
|
|
if (ret < 0) {
|
|
warn("audio_alsa: Unable to prepare the device: \"%s\": %s.", alsa_out_dev, snd_strerror(ret));
|
|
return ret;
|
|
}
|
|
|
|
if (actual_buffer_length < config.audio_backend_buffer_desired_length + minimal_buffer_headroom) {
|
|
/*
|
|
// the dac buffer is too small, so let's try to set it
|
|
buffer_size =
|
|
config.audio_backend_buffer_desired_length + requested_buffer_headroom;
|
|
ret = snd_pcm_hw_params_set_buffer_size_near(alsa_handle, alsa_params,
|
|
&buffer_size);
|
|
if (ret < 0)
|
|
die("audio_alsa: Unable to set hw buffer size to %lu for device \"%s\": "
|
|
"%s.",
|
|
config.audio_backend_buffer_desired_length +
|
|
requested_buffer_headroom,
|
|
alsa_out_dev, snd_strerror(ret));
|
|
if (config.audio_backend_buffer_desired_length + minimal_buffer_headroom >
|
|
buffer_size) {
|
|
die("audio_alsa: Can't set hw buffer size to %lu or more for device "
|
|
"\"%s\". Requested size: %lu, granted size: %lu.",
|
|
config.audio_backend_buffer_desired_length + minimal_buffer_headroom,
|
|
alsa_out_dev, config.audio_backend_buffer_desired_length +
|
|
requested_buffer_headroom,
|
|
buffer_size);
|
|
}
|
|
*/
|
|
debug(1,
|
|
"The alsa buffer is smaller (%lu bytes) than the desired backend "
|
|
"buffer "
|
|
"length (%ld) you have chosen.",
|
|
actual_buffer_length, config.audio_backend_buffer_desired_length);
|
|
}
|
|
|
|
if (config.use_precision_timing == YNA_YES)
|
|
delay_and_status = precision_delay_and_status;
|
|
else if (config.use_precision_timing == YNA_AUTO) {
|
|
if (precision_delay_available()) {
|
|
delay_and_status = precision_delay_and_status;
|
|
debug(2, "alsa: precision timing selected for \"auto\" mode");
|
|
}
|
|
}
|
|
|
|
if (alsa_characteristics_already_listed == 0) {
|
|
alsa_characteristics_already_listed = 1;
|
|
int log_level = 2; // the level at which debug information should be output
|
|
// int rc;
|
|
snd_pcm_access_t access_type;
|
|
snd_pcm_format_t format_type;
|
|
snd_pcm_subformat_t subformat_type;
|
|
// unsigned int val, val2;
|
|
unsigned int uval, uval2;
|
|
int sval;
|
|
int dir;
|
|
snd_pcm_uframes_t frames;
|
|
|
|
debug(log_level, "PCM handle name = '%s'", snd_pcm_name(alsa_handle));
|
|
|
|
// ret = snd_pcm_hw_params_any(alsa_handle, alsa_params);
|
|
// if (ret < 0) {
|
|
// die("audio_alsa: Cannpot get configuration for
|
|
// device
|
|
//\"%s\":
|
|
// no
|
|
// configurations
|
|
//"
|
|
// "available",
|
|
// alsa_out_dev);
|
|
// }
|
|
|
|
debug(log_level, "alsa device parameters:");
|
|
|
|
snd_pcm_hw_params_get_access(alsa_params, &access_type);
|
|
debug(log_level, " access type = %s", snd_pcm_access_name(access_type));
|
|
|
|
snd_pcm_hw_params_get_format(alsa_params, &format_type);
|
|
debug(log_level, " format = '%s' (%s)", snd_pcm_format_name(format_type),
|
|
snd_pcm_format_description(format_type));
|
|
|
|
snd_pcm_hw_params_get_subformat(alsa_params, &subformat_type);
|
|
debug(log_level, " subformat = '%s' (%s)", snd_pcm_subformat_name(subformat_type),
|
|
snd_pcm_subformat_description(subformat_type));
|
|
|
|
snd_pcm_hw_params_get_channels(alsa_params, &uval);
|
|
debug(log_level, " number of channels = %u", uval);
|
|
|
|
sval = snd_pcm_hw_params_get_sbits(alsa_params);
|
|
debug(log_level, " number of significant bits = %d", sval);
|
|
|
|
snd_pcm_hw_params_get_rate(alsa_params, &uval, &dir);
|
|
switch (dir) {
|
|
case -1:
|
|
debug(log_level, " rate = %u frames per second (<).", uval);
|
|
break;
|
|
case 0:
|
|
debug(log_level, " rate = %u frames per second (precisely).", uval);
|
|
break;
|
|
case 1:
|
|
debug(log_level, " rate = %u frames per second (>).", uval);
|
|
break;
|
|
}
|
|
|
|
if ((snd_pcm_hw_params_get_rate_numden(alsa_params, &uval, &uval2) == 0) && (uval2 != 0))
|
|
// watch for a divide by zero too!
|
|
debug(log_level, " precise (rational) rate = %.3f frames per second (i.e. %u/%u).", uval,
|
|
uval2, ((double)uval) / uval2);
|
|
else
|
|
debug(log_level, " precise (rational) rate information unavailable.");
|
|
|
|
snd_pcm_hw_params_get_period_time(alsa_params, &uval, &dir);
|
|
switch (dir) {
|
|
case -1:
|
|
debug(log_level, " period_time = %u us (<).", uval);
|
|
break;
|
|
case 0:
|
|
debug(log_level, " period_time = %u us (precisely).", uval);
|
|
break;
|
|
case 1:
|
|
debug(log_level, " period_time = %u us (>).", uval);
|
|
break;
|
|
}
|
|
|
|
snd_pcm_hw_params_get_period_size(alsa_params, &frames, &dir);
|
|
switch (dir) {
|
|
case -1:
|
|
debug(log_level, " period_size = %lu frames (<).", frames);
|
|
break;
|
|
case 0:
|
|
debug(log_level, " period_size = %lu frames (precisely).", frames);
|
|
break;
|
|
case 1:
|
|
debug(log_level, " period_size = %lu frames (>).", frames);
|
|
break;
|
|
}
|
|
|
|
snd_pcm_hw_params_get_buffer_time(alsa_params, &uval, &dir);
|
|
switch (dir) {
|
|
case -1:
|
|
debug(log_level, " buffer_time = %u us (<).", uval);
|
|
break;
|
|
case 0:
|
|
debug(log_level, " buffer_time = %u us (precisely).", uval);
|
|
break;
|
|
case 1:
|
|
debug(log_level, " buffer_time = %u us (>).", uval);
|
|
break;
|
|
}
|
|
|
|
snd_pcm_hw_params_get_buffer_size(alsa_params, &frames);
|
|
switch (dir) {
|
|
case -1:
|
|
debug(log_level, " buffer_size = %lu frames (<).", frames);
|
|
break;
|
|
case 0:
|
|
debug(log_level, " buffer_size = %lu frames (precisely).", frames);
|
|
break;
|
|
case 1:
|
|
debug(log_level, " buffer_size = %lu frames (>).", frames);
|
|
break;
|
|
}
|
|
|
|
snd_pcm_hw_params_get_periods(alsa_params, &uval, &dir);
|
|
switch (dir) {
|
|
case -1:
|
|
debug(log_level, " periods_per_buffer = %u (<).", uval);
|
|
break;
|
|
case 0:
|
|
debug(log_level, " periods_per_buffer = %u (precisely).", uval);
|
|
break;
|
|
case 1:
|
|
debug(log_level, " periods_per_buffer = %u (>).", uval);
|
|
break;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int open_alsa_device(int do_auto_setup) {
|
|
int result;
|
|
int oldState;
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
|
|
result = actual_open_alsa_device(do_auto_setup);
|
|
pthread_setcancelstate(oldState, NULL);
|
|
return result;
|
|
}
|
|
|
|
static int prepare_mixer() {
|
|
int response = 0;
|
|
// do any alsa device initialisation (general case)
|
|
// at present, this is only needed if a hardware mixer is being used
|
|
// if there's a hardware mixer, it needs to be initialised before use
|
|
if (alsa_mix_ctrl == NULL) {
|
|
audio_alsa.volume = NULL;
|
|
audio_alsa.parameters = NULL;
|
|
audio_alsa.mute = NULL;
|
|
} else {
|
|
debug(2, "alsa: hardware mixer prepare");
|
|
int oldState;
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
|
|
|
|
if (alsa_mix_dev == NULL)
|
|
alsa_mix_dev = alsa_out_dev;
|
|
|
|
// Now, start trying to initialise the alsa device with the settings
|
|
// obtained
|
|
pthread_cleanup_debug_mutex_lock(&alsa_mixer_mutex, 1000, 1);
|
|
if (open_mixer() == 1) {
|
|
if (snd_mixer_selem_get_playback_volume_range(alsa_mix_elem, &alsa_mix_minv, &alsa_mix_maxv) <
|
|
0)
|
|
debug(1, "Can't read mixer's [linear] min and max volumes.");
|
|
else {
|
|
if (snd_mixer_selem_get_playback_dB_range(alsa_mix_elem, &alsa_mix_mindb,
|
|
&alsa_mix_maxdb) == 0) {
|
|
|
|
audio_alsa.volume = &volume; // insert the volume function now we
|
|
// know it can do dB stuff
|
|
audio_alsa.parameters = ¶meters; // likewise the parameters stuff
|
|
if (alsa_mix_mindb == SND_CTL_TLV_DB_GAIN_MUTE) {
|
|
// For instance, the Raspberry Pi does this
|
|
debug(2, "Lowest dB value is a mute");
|
|
mixer_volume_setting_gives_mute = 1;
|
|
alsa_mix_mute = SND_CTL_TLV_DB_GAIN_MUTE; // this may not be
|
|
// necessary -- it's
|
|
// always
|
|
// going to be SND_CTL_TLV_DB_GAIN_MUTE, right?
|
|
// debug(1, "Try minimum volume + 1 as lowest true attenuation
|
|
// value");
|
|
if (snd_mixer_selem_ask_playback_vol_dB(alsa_mix_elem, alsa_mix_minv + 1,
|
|
&alsa_mix_mindb) != 0)
|
|
debug(1, "Can't get dB value corresponding to a minimum volume "
|
|
"+ 1.");
|
|
}
|
|
debug(3, "Hardware mixer has dB volume from %f to %f.", (1.0 * alsa_mix_mindb) / 100.0,
|
|
(1.0 * alsa_mix_maxdb) / 100.0);
|
|
} else {
|
|
// use the linear scale and do the db conversion ourselves
|
|
warn("The hardware mixer specified -- \"%s\" -- does not have "
|
|
"a dB volume scale.",
|
|
alsa_mix_ctrl);
|
|
|
|
if (snd_ctl_open(&ctl, alsa_mix_dev, 0) < 0) {
|
|
warn("Cannot open control \"%s\"", alsa_mix_dev);
|
|
response = -1;
|
|
}
|
|
if (snd_ctl_elem_id_malloc(&elem_id) < 0) {
|
|
debug(1, "Cannot allocate memory for control \"%s\"", alsa_mix_dev);
|
|
elem_id = NULL;
|
|
response = -2;
|
|
} else {
|
|
snd_ctl_elem_id_set_interface(elem_id, SND_CTL_ELEM_IFACE_MIXER);
|
|
snd_ctl_elem_id_set_name(elem_id, alsa_mix_ctrl);
|
|
|
|
if (snd_ctl_get_dB_range(ctl, elem_id, &alsa_mix_mindb, &alsa_mix_maxdb) == 0) {
|
|
debug(1,
|
|
"alsa: hardware mixer \"%s\" selected, with dB volume "
|
|
"from %f to %f.",
|
|
alsa_mix_ctrl, (1.0 * alsa_mix_mindb) / 100.0, (1.0 * alsa_mix_maxdb) / 100.0);
|
|
has_softvol = 1;
|
|
audio_alsa.volume = &volume; // insert the volume function now
|
|
// we know it can do dB stuff
|
|
audio_alsa.parameters = ¶meters; // likewise the parameters stuff
|
|
} else {
|
|
debug(1, "Cannot get the dB range from the volume control \"%s\"", alsa_mix_ctrl);
|
|
}
|
|
}
|
|
/*
|
|
debug(1, "Min and max volumes are %d and
|
|
%d.",alsa_mix_minv,alsa_mix_maxv);
|
|
alsa_mix_maxdb = 0;
|
|
if ((alsa_mix_maxv!=0) && (alsa_mix_minv!=0))
|
|
alsa_mix_mindb =
|
|
-20*100*(log10(alsa_mix_maxv*1.0)-log10(alsa_mix_minv*1.0));
|
|
else if (alsa_mix_maxv!=0)
|
|
alsa_mix_mindb = -20*100*log10(alsa_mix_maxv*1.0);
|
|
audio_alsa.volume = &linear_volume; // insert the linear volume
|
|
function
|
|
audio_alsa.parameters = ¶meters; // likewise the parameters
|
|
stuff
|
|
debug(1,"Max and min dB calculated are %d and
|
|
%d.",alsa_mix_maxdb,alsa_mix_mindb);
|
|
*/
|
|
}
|
|
}
|
|
if (((config.alsa_use_hardware_mute == 1) &&
|
|
(snd_mixer_selem_has_playback_switch(alsa_mix_elem))) ||
|
|
mixer_volume_setting_gives_mute) {
|
|
audio_alsa.mute = &mute; // insert the mute function now we know it
|
|
// can do muting stuff
|
|
// debug(1, "Has mixer and mute ability we will use.");
|
|
} else {
|
|
// debug(1, "Has mixer but not using hardware mute.");
|
|
}
|
|
close_mixer();
|
|
}
|
|
debug_mutex_unlock(&alsa_mixer_mutex, 3); // release the mutex
|
|
pthread_cleanup_pop(0);
|
|
pthread_setcancelstate(oldState, NULL);
|
|
}
|
|
return response;
|
|
}
|
|
|
|
static int alsa_device_init() { return prepare_mixer(); }
|
|
|
|
static int init(int argc, char **argv) {
|
|
// for debugging
|
|
snd_output_stdio_attach(&output, stdout, 0);
|
|
|
|
// debug(2,"audio_alsa init called.");
|
|
int response = 0; // this will be what we return to the caller.
|
|
alsa_device_initialised = 0;
|
|
const char *str;
|
|
int value;
|
|
// double dvalue;
|
|
|
|
// set up default values first
|
|
|
|
alsa_backend_state = abm_disconnected; // startup state
|
|
debug(2, "alsa: init() -- alsa_backend_state => abm_disconnected.");
|
|
set_period_size_request = 0;
|
|
set_buffer_size_request = 0;
|
|
config.alsa_use_hardware_mute = 0; // don't use it by default
|
|
|
|
config.audio_backend_latency_offset = 0;
|
|
config.audio_backend_buffer_desired_length = 0.200;
|
|
config.audio_backend_buffer_interpolation_threshold_in_seconds =
|
|
0.120; // below this, basic interpolation will be used to save time.
|
|
config.alsa_maximum_stall_time = 0.200; // 200 milliseconds -- if it takes longer, it's a problem
|
|
config.disable_standby_mode_silence_threshold =
|
|
0.040; // start sending silent frames if the delay goes below this time
|
|
config.disable_standby_mode_silence_scan_interval = 0.004; // check silence threshold this often
|
|
|
|
stall_monitor_error_threshold =
|
|
(uint64_t)1000000 * config.alsa_maximum_stall_time; // stall time max to microseconds;
|
|
stall_monitor_error_threshold = (stall_monitor_error_threshold << 32) / 1000000; // now in fp form
|
|
debug(1, "alsa: alsa_maximum_stall_time of %f sec.", config.alsa_maximum_stall_time);
|
|
|
|
stall_monitor_start_time = 0;
|
|
stall_monitor_frame_count = 0;
|
|
|
|
config.disable_standby_mode = disable_standby_off;
|
|
config.keep_dac_busy = 0;
|
|
config.use_precision_timing = YNA_AUTO;
|
|
|
|
// get settings from settings file first, allow them to be overridden by
|
|
// command line options
|
|
|
|
// do the "general" audio options. Note, these options are in the "general"
|
|
// stanza!
|
|
parse_general_audio_options();
|
|
|
|
if (config.cfg != NULL) {
|
|
double dvalue;
|
|
|
|
/* Get the Output Device Name. */
|
|
if (config_lookup_string(config.cfg, "alsa.output_device", &str)) {
|
|
alsa_out_dev = (char *)str;
|
|
}
|
|
|
|
/* Get the Mixer Type setting. */
|
|
|
|
if (config_lookup_string(config.cfg, "alsa.mixer_type", &str)) {
|
|
inform("The alsa mixer_type setting is deprecated and has been ignored. "
|
|
"FYI, using the \"mixer_control_name\" setting automatically "
|
|
"chooses a hardware mixer.");
|
|
}
|
|
|
|
/* Get the Mixer Device Name. */
|
|
if (config_lookup_string(config.cfg, "alsa.mixer_device", &str)) {
|
|
alsa_mix_dev = (char *)str;
|
|
}
|
|
|
|
/* Get the Mixer Control Name. */
|
|
if (config_lookup_string(config.cfg, "alsa.mixer_control_name", &str)) {
|
|
alsa_mix_ctrl = (char *)str;
|
|
}
|
|
|
|
// Get the Mixer Control Index
|
|
if (config_lookup_int(config.cfg, "alsa.mixer_control_index", &value)) {
|
|
alsa_mix_index = value;
|
|
}
|
|
|
|
/* Get the disable_synchronization setting. */
|
|
if (config_lookup_string(config.cfg, "alsa.disable_synchronization", &str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.no_sync = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.no_sync = 1;
|
|
else {
|
|
warn("Invalid disable_synchronization option choice \"%s\". It should "
|
|
"be \"yes\" or "
|
|
"\"no\". It is set to \"no\".");
|
|
config.no_sync = 0;
|
|
}
|
|
}
|
|
|
|
/* Get the mute_using_playback_switch setting. */
|
|
if (config_lookup_string(config.cfg, "alsa.mute_using_playback_switch", &str)) {
|
|
inform("The alsa \"mute_using_playback_switch\" setting is deprecated. "
|
|
"Please use the \"use_hardware_mute_if_available\" setting instead.");
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.alsa_use_hardware_mute = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.alsa_use_hardware_mute = 1;
|
|
else {
|
|
warn("Invalid mute_using_playback_switch option choice \"%s\". It "
|
|
"should be \"yes\" or "
|
|
"\"no\". It is set to \"no\".");
|
|
config.alsa_use_hardware_mute = 0;
|
|
}
|
|
}
|
|
|
|
/* Get the use_hardware_mute_if_available setting. */
|
|
if (config_lookup_string(config.cfg, "alsa.use_hardware_mute_if_available", &str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.alsa_use_hardware_mute = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.alsa_use_hardware_mute = 1;
|
|
else {
|
|
warn("Invalid use_hardware_mute_if_available option choice \"%s\". It "
|
|
"should be \"yes\" or "
|
|
"\"no\". It is set to \"no\".");
|
|
config.alsa_use_hardware_mute = 0;
|
|
}
|
|
}
|
|
|
|
/* Get the output format, using the same names as aplay does*/
|
|
if (config_lookup_string(config.cfg, "alsa.output_format", &str)) {
|
|
int temp_output_format_auto_requested = config.output_format_auto_requested;
|
|
config.output_format_auto_requested = 0; // assume a valid format will be given.
|
|
if (strcasecmp(str, "S16") == 0)
|
|
config.output_format = SPS_FORMAT_S16;
|
|
else if (strcasecmp(str, "S16_LE") == 0)
|
|
config.output_format = SPS_FORMAT_S16_LE;
|
|
else if (strcasecmp(str, "S16_BE") == 0)
|
|
config.output_format = SPS_FORMAT_S16_BE;
|
|
else if (strcasecmp(str, "S24") == 0)
|
|
config.output_format = SPS_FORMAT_S24;
|
|
else if (strcasecmp(str, "S24_LE") == 0)
|
|
config.output_format = SPS_FORMAT_S24_LE;
|
|
else if (strcasecmp(str, "S24_BE") == 0)
|
|
config.output_format = SPS_FORMAT_S24_BE;
|
|
else if (strcasecmp(str, "S24_3LE") == 0)
|
|
config.output_format = SPS_FORMAT_S24_3LE;
|
|
else if (strcasecmp(str, "S24_3BE") == 0)
|
|
config.output_format = SPS_FORMAT_S24_3BE;
|
|
else if (strcasecmp(str, "S32") == 0)
|
|
config.output_format = SPS_FORMAT_S32;
|
|
else if (strcasecmp(str, "S32_LE") == 0)
|
|
config.output_format = SPS_FORMAT_S32_LE;
|
|
else if (strcasecmp(str, "S32_BE") == 0)
|
|
config.output_format = SPS_FORMAT_S32_BE;
|
|
else if (strcasecmp(str, "U8") == 0)
|
|
config.output_format = SPS_FORMAT_U8;
|
|
else if (strcasecmp(str, "S8") == 0)
|
|
config.output_format = SPS_FORMAT_S8;
|
|
else if (strcasecmp(str, "auto") == 0)
|
|
config.output_format_auto_requested = 1;
|
|
else {
|
|
config.output_format_auto_requested =
|
|
temp_output_format_auto_requested; // format was invalid; recall the original setting
|
|
warn("Invalid output format \"%s\". It should be \"auto\", \"U8\", \"S8\", "
|
|
"\"S16\", \"S24\", \"S24_LE\", \"S24_BE\", "
|
|
"\"S24_3LE\", \"S24_3BE\" or "
|
|
"\"S32\", \"S32_LE\", \"S32_BE\". It remains set to \"%s\".",
|
|
str,
|
|
config.output_format_auto_requested == 1
|
|
? "auto"
|
|
: sps_format_description_string(config.output_format));
|
|
}
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "alsa.output_rate", &str)) {
|
|
if (strcasecmp(str, "auto") == 0) {
|
|
config.output_rate_auto_requested = 1;
|
|
} else {
|
|
if (config.output_rate_auto_requested == 1)
|
|
warn("Invalid output rate \"%s\". It should be \"auto\", 44100, 88200, 176400 or 352800. "
|
|
"It remains set to \"auto\". Note: numbers should not be placed in quotes.",
|
|
str);
|
|
else
|
|
warn("Invalid output rate \"%s\". It should be \"auto\", 44100, 88200, 176400 or 352800. "
|
|
"It remains set to %d. Note: numbers should not be placed in quotes.",
|
|
str, config.output_rate);
|
|
}
|
|
}
|
|
|
|
/* Get the output rate, which must be a multiple of 44,100*/
|
|
if (config_lookup_int(config.cfg, "alsa.output_rate", &value)) {
|
|
debug(1, "alsa output rate is %d frames per second", value);
|
|
switch (value) {
|
|
case 44100:
|
|
case 88200:
|
|
case 176400:
|
|
case 352800:
|
|
config.output_rate = value;
|
|
config.output_rate_auto_requested = 0;
|
|
break;
|
|
default:
|
|
if (config.output_rate_auto_requested == 1)
|
|
warn("Invalid output rate \"%d\". It should be \"auto\", 44100, 88200, 176400 or 352800. "
|
|
"It remains set to \"auto\".",
|
|
value);
|
|
else
|
|
warn("Invalid output rate \"%d\".It should be \"auto\", 44100, 88200, 176400 or 352800. "
|
|
"It remains set to %d.",
|
|
value, config.output_rate);
|
|
}
|
|
}
|
|
|
|
/* Get the use_mmap_if_available setting. */
|
|
if (config_lookup_string(config.cfg, "alsa.use_mmap_if_available", &str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.no_mmap = 1;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.no_mmap = 0;
|
|
else {
|
|
warn("Invalid use_mmap_if_available option choice \"%s\". It should be "
|
|
"\"yes\" or \"no\". "
|
|
"It remains set to \"yes\".");
|
|
config.no_mmap = 0;
|
|
}
|
|
}
|
|
/* Get the optional period size value */
|
|
if (config_lookup_int(config.cfg, "alsa.period_size", &value)) {
|
|
set_period_size_request = 1;
|
|
debug(1, "Value read for period size is %d.", value);
|
|
if (value < 0) {
|
|
warn("Invalid alsa period size setting \"%d\". It "
|
|
"must be greater than 0. No setting is made.",
|
|
value);
|
|
set_period_size_request = 0;
|
|
} else {
|
|
period_size_requested = value;
|
|
}
|
|
}
|
|
|
|
/* Get the optional buffer size value */
|
|
if (config_lookup_int(config.cfg, "alsa.buffer_size", &value)) {
|
|
set_buffer_size_request = 1;
|
|
debug(1, "Value read for buffer size is %d.", value);
|
|
if (value < 0) {
|
|
warn("Invalid alsa buffer size setting \"%d\". It "
|
|
"must be greater than 0. No setting is made.",
|
|
value);
|
|
set_buffer_size_request = 0;
|
|
} else {
|
|
buffer_size_requested = value;
|
|
}
|
|
}
|
|
|
|
/* Get the optional alsa_maximum_stall_time setting. */
|
|
if (config_lookup_float(config.cfg, "alsa.maximum_stall_time", &dvalue)) {
|
|
if (dvalue < 0.0) {
|
|
warn("Invalid alsa maximum write time setting \"%f\". It "
|
|
"must be greater than 0. Default is \"%f\". No setting is made.",
|
|
dvalue, config.alsa_maximum_stall_time);
|
|
} else {
|
|
config.alsa_maximum_stall_time = dvalue;
|
|
}
|
|
}
|
|
|
|
/* Get the optional disable_standby_mode_silence_threshold setting. */
|
|
if (config_lookup_float(config.cfg, "alsa.disable_standby_mode_silence_threshold", &dvalue)) {
|
|
if (dvalue < 0.0) {
|
|
warn("Invalid alsa disable_standby_mode_silence_threshold setting \"%f\". It "
|
|
"must be greater than 0. Default is \"%f\". No setting is made.",
|
|
dvalue, config.disable_standby_mode_silence_threshold);
|
|
} else {
|
|
config.disable_standby_mode_silence_threshold = dvalue;
|
|
}
|
|
}
|
|
|
|
/* Get the optional disable_standby_mode_silence_scan_interval setting. */
|
|
if (config_lookup_float(config.cfg, "alsa.disable_standby_mode_silence_scan_interval",
|
|
&dvalue)) {
|
|
if (dvalue < 0.0) {
|
|
warn("Invalid alsa disable_standby_mode_silence_scan_interval setting \"%f\". It "
|
|
"must be greater than 0. Default is \"%f\". No setting is made.",
|
|
dvalue, config.disable_standby_mode_silence_scan_interval);
|
|
} else {
|
|
config.disable_standby_mode_silence_scan_interval = dvalue;
|
|
}
|
|
}
|
|
|
|
/* Get the optional disable_standby_mode setting. */
|
|
if (config_lookup_string(config.cfg, "alsa.disable_standby_mode", &str)) {
|
|
if ((strcasecmp(str, "no") == 0) || (strcasecmp(str, "off") == 0) ||
|
|
(strcasecmp(str, "never") == 0))
|
|
config.disable_standby_mode = disable_standby_off;
|
|
else if ((strcasecmp(str, "yes") == 0) || (strcasecmp(str, "on") == 0) ||
|
|
(strcasecmp(str, "always") == 0)) {
|
|
config.disable_standby_mode = disable_standby_always;
|
|
config.keep_dac_busy = 1;
|
|
} else if (strcasecmp(str, "auto") == 0)
|
|
config.disable_standby_mode = disable_standby_auto;
|
|
else {
|
|
warn("Invalid disable_standby_mode option choice \"%s\". It should be "
|
|
"\"always\", \"auto\" or \"never\". "
|
|
"It remains set to \"never\".",
|
|
str);
|
|
}
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "alsa.use_precision_timing", &str)) {
|
|
if ((strcasecmp(str, "no") == 0) || (strcasecmp(str, "off") == 0) ||
|
|
(strcasecmp(str, "never") == 0))
|
|
config.use_precision_timing = YNA_NO;
|
|
else if ((strcasecmp(str, "yes") == 0) || (strcasecmp(str, "on") == 0) ||
|
|
(strcasecmp(str, "always") == 0)) {
|
|
config.use_precision_timing = YNA_YES;
|
|
config.keep_dac_busy = 1;
|
|
} else if (strcasecmp(str, "auto") == 0)
|
|
config.use_precision_timing = YNA_AUTO;
|
|
else {
|
|
warn("Invalid use_precision_timing option choice \"%s\". It should be "
|
|
"\"yes\", \"auto\" or \"no\". "
|
|
"It remains set to \"%s\".",
|
|
config.use_precision_timing == YNA_NO ? "no"
|
|
: config.use_precision_timing == YNA_AUTO ? "auto"
|
|
: "yes");
|
|
}
|
|
}
|
|
|
|
debug(1, "alsa: disable_standby_mode is \"%s\".",
|
|
config.disable_standby_mode == disable_standby_off ? "never"
|
|
: config.disable_standby_mode == disable_standby_always ? "always"
|
|
: "auto");
|
|
debug(1, "alsa: disable_standby_mode_silence_threshold is %f seconds.",
|
|
config.disable_standby_mode_silence_threshold);
|
|
debug(1, "alsa: disable_standby_mode_silence_scan_interval is %f seconds.",
|
|
config.disable_standby_mode_silence_scan_interval);
|
|
}
|
|
|
|
optind = 1; // optind=0 is equivalent to optind=1 plus special behaviour
|
|
argv--; // so we shift the arguments to satisfy getopt()
|
|
argc++;
|
|
// some platforms apparently require optreset = 1; - which?
|
|
int opt;
|
|
while ((opt = getopt(argc, argv, "d:t:m:c:i:")) > 0) {
|
|
switch (opt) {
|
|
case 'd':
|
|
alsa_out_dev = optarg;
|
|
break;
|
|
|
|
case 't':
|
|
inform("The alsa backend -t option is deprecated and has been ignored. "
|
|
"FYI, using the -c option automatically chooses a hardware "
|
|
"mixer.");
|
|
break;
|
|
|
|
case 'm':
|
|
alsa_mix_dev = optarg;
|
|
break;
|
|
case 'c':
|
|
alsa_mix_ctrl = optarg;
|
|
break;
|
|
case 'i':
|
|
alsa_mix_index = strtol(optarg, NULL, 10);
|
|
break;
|
|
default:
|
|
warn("Invalid audio option \"-%c\" specified -- ignored.", opt);
|
|
help();
|
|
}
|
|
}
|
|
|
|
if (optind < argc) {
|
|
warn("Invalid audio argument: \"%s\" -- ignored", argv[optind]);
|
|
}
|
|
|
|
debug(1, "alsa: output device name is \"%s\".", alsa_out_dev);
|
|
|
|
// so, now, if the option to keep the DAC running has been selected, start a
|
|
// thread to monitor the
|
|
// length of the queue
|
|
// if the queue gets too short, stuff it with silence
|
|
|
|
pthread_create(&alsa_buffer_monitor_thread, NULL, &alsa_buffer_monitor_thread_code, NULL);
|
|
|
|
return response;
|
|
}
|
|
|
|
static void deinit(void) {
|
|
int oldState;
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
|
|
// debug(2,"audio_alsa deinit called.");
|
|
stop();
|
|
debug(2, "Cancel buffer monitor thread.");
|
|
pthread_cancel(alsa_buffer_monitor_thread);
|
|
debug(3, "Join buffer monitor thread.");
|
|
pthread_join(alsa_buffer_monitor_thread, NULL);
|
|
pthread_setcancelstate(oldState, NULL);
|
|
}
|
|
|
|
static int set_mute_state() {
|
|
int response = 1; // some problem expected, e.g. no mixer or not allowed to use it or disconnected
|
|
int oldState;
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
|
|
pthread_cleanup_debug_mutex_lock(&alsa_mixer_mutex, 10000, 0);
|
|
if ((alsa_backend_state != abm_disconnected) && (config.alsa_use_hardware_mute == 1) &&
|
|
(open_mixer() == 1)) {
|
|
response = 0; // okay if actually using the mute facility
|
|
debug(2, "alsa: actually set_mute_state");
|
|
int mute = 0;
|
|
if ((mute_requested_externally != 0) || (mute_requested_internally != 0))
|
|
mute = 1;
|
|
if (mute == 1) {
|
|
debug(2, "alsa: hardware mute switched on");
|
|
if (snd_mixer_selem_has_playback_switch(alsa_mix_elem))
|
|
snd_mixer_selem_set_playback_switch_all(alsa_mix_elem, 0);
|
|
else {
|
|
volume_based_mute_is_active = 1;
|
|
do_snd_mixer_selem_set_playback_dB_all(alsa_mix_elem, alsa_mix_mute);
|
|
}
|
|
} else {
|
|
debug(2, "alsa: hardware mute switched off");
|
|
if (snd_mixer_selem_has_playback_switch(alsa_mix_elem))
|
|
snd_mixer_selem_set_playback_switch_all(alsa_mix_elem, 1);
|
|
else {
|
|
volume_based_mute_is_active = 0;
|
|
do_snd_mixer_selem_set_playback_dB_all(alsa_mix_elem, set_volume);
|
|
}
|
|
}
|
|
close_mixer();
|
|
}
|
|
debug_mutex_unlock(&alsa_mixer_mutex, 3); // release the mutex
|
|
pthread_cleanup_pop(0); // release the mutex
|
|
pthread_setcancelstate(oldState, NULL);
|
|
return response;
|
|
}
|
|
|
|
static void start(__attribute__((unused)) int i_sample_rate,
|
|
__attribute__((unused)) int i_sample_format) {
|
|
debug(3, "audio_alsa start called.");
|
|
|
|
// frame_index = 0;
|
|
// measurement_data_is_valid = 0;
|
|
|
|
stall_monitor_start_time = 0;
|
|
stall_monitor_frame_count = 0;
|
|
if (alsa_device_initialised == 0) {
|
|
debug(1, "alsa: start() calling alsa_device_init.");
|
|
alsa_device_init();
|
|
alsa_device_initialised = 1;
|
|
}
|
|
}
|
|
|
|
static int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay,
|
|
yndk_type *using_update_timestamps) {
|
|
int ret = alsa_handle_status;
|
|
if (using_update_timestamps)
|
|
*using_update_timestamps = YNDK_NO;
|
|
|
|
snd_pcm_state_t state_temp = SND_PCM_STATE_DISCONNECTED;
|
|
snd_pcm_sframes_t delay_temp = 0;
|
|
if (alsa_handle != NULL) {
|
|
state_temp = snd_pcm_state(alsa_handle);
|
|
if ((state_temp == SND_PCM_STATE_RUNNING) || (state_temp == SND_PCM_STATE_DRAINING)) {
|
|
ret = snd_pcm_delay(alsa_handle, &delay_temp);
|
|
} else {
|
|
// not running, thus no delay information, thus can't check for frame
|
|
// rates
|
|
// frame_index = 0; // we'll be starting over...
|
|
// measurement_data_is_valid = 0;
|
|
// delay_temp = 0;
|
|
ret = 0;
|
|
}
|
|
} // else {
|
|
// debug(1, "alsa_handle is NULL in standard_delay_and_status.");
|
|
// }
|
|
|
|
stall_monitor_start_time = 0; // zero if not initialised / not started / zeroed by flush
|
|
stall_monitor_frame_count = 0; // set to delay at start of time, incremented by any writes
|
|
|
|
if (delay != NULL)
|
|
*delay = delay_temp;
|
|
if (state != NULL)
|
|
*state = state_temp;
|
|
return ret;
|
|
}
|
|
|
|
static int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay,
|
|
yndk_type *using_update_timestamps) {
|
|
snd_pcm_state_t state_temp = SND_PCM_STATE_DISCONNECTED;
|
|
snd_pcm_sframes_t delay_temp = 0;
|
|
if (using_update_timestamps)
|
|
*using_update_timestamps = YNDK_DONT_KNOW;
|
|
int ret = alsa_handle_status;
|
|
|
|
snd_pcm_status_t *alsa_snd_pcm_status;
|
|
snd_pcm_status_alloca(&alsa_snd_pcm_status);
|
|
|
|
struct timespec tn; // time now
|
|
snd_htimestamp_t update_timestamp; // actually a struct timespec
|
|
if (alsa_handle != NULL) {
|
|
ret = snd_pcm_status(alsa_handle, alsa_snd_pcm_status);
|
|
if (ret == 0) {
|
|
snd_pcm_status_get_htstamp(alsa_snd_pcm_status, &update_timestamp);
|
|
|
|
/*
|
|
// must be 1.1 or later to use snd_pcm_status_get_driver_htstamp
|
|
#if SND_LIB_MINOR != 0
|
|
snd_htimestamp_t driver_htstamp;
|
|
snd_pcm_status_get_driver_htstamp(alsa_snd_pcm_status, &driver_htstamp);
|
|
uint64_t driver_htstamp_ns = driver_htstamp.tv_sec;
|
|
driver_htstamp_ns = driver_htstamp_ns * 1000000000;
|
|
driver_htstamp_ns = driver_htstamp_ns + driver_htstamp.tv_nsec;
|
|
debug(1,"driver_htstamp: %f.", driver_htstamp_ns * 0.000000001);
|
|
#endif
|
|
*/
|
|
|
|
state_temp = snd_pcm_status_get_state(alsa_snd_pcm_status);
|
|
|
|
if ((state_temp == SND_PCM_STATE_RUNNING) || (state_temp == SND_PCM_STATE_DRAINING)) {
|
|
|
|
// uint64_t update_timestamp_ns =
|
|
// update_timestamp.tv_sec * (uint64_t)1000000000 + update_timestamp.tv_nsec;
|
|
|
|
uint64_t update_timestamp_ns = update_timestamp.tv_sec;
|
|
update_timestamp_ns = update_timestamp_ns * 1000000000;
|
|
update_timestamp_ns = update_timestamp_ns + update_timestamp.tv_nsec;
|
|
|
|
// if the update_timestamp is zero, we take this to mean that the device doesn't report
|
|
// interrupt timings. (It could be that it's not a real hardware device.)
|
|
// so we switch to getting the delay the regular way
|
|
// i.e. using snd_pcm_delay ()
|
|
if (using_update_timestamps) {
|
|
if (update_timestamp_ns == 0)
|
|
*using_update_timestamps = YNDK_NO;
|
|
else
|
|
*using_update_timestamps = YNDK_YES;
|
|
}
|
|
|
|
// user information
|
|
if (update_timestamp_ns == 0) {
|
|
if (delay_type_notified != 1) {
|
|
debug(2, "alsa: update timestamps unavailable");
|
|
delay_type_notified = 1;
|
|
}
|
|
} else {
|
|
// diagnostic
|
|
if (delay_type_notified != 0) {
|
|
debug(2, "alsa: update timestamps available");
|
|
delay_type_notified = 0;
|
|
}
|
|
}
|
|
|
|
if (update_timestamp_ns == 0) {
|
|
ret = snd_pcm_delay(alsa_handle, &delay_temp);
|
|
} else {
|
|
delay_temp = snd_pcm_status_get_delay(alsa_snd_pcm_status);
|
|
|
|
/*
|
|
// It seems that the alsa library uses CLOCK_REALTIME before 1.0.28, even though
|
|
// the check for monotonic returns true. Might have to watch out for this.
|
|
#if SND_LIB_MINOR == 0 && SND_LIB_SUBMINOR < 28
|
|
clock_gettime(CLOCK_REALTIME, &tn);
|
|
#else
|
|
clock_gettime(CLOCK_MONOTONIC, &tn);
|
|
#endif
|
|
*/
|
|
|
|
if (use_monotonic_clock)
|
|
clock_gettime(CLOCK_MONOTONIC, &tn);
|
|
else
|
|
clock_gettime(CLOCK_REALTIME, &tn);
|
|
|
|
// uint64_t time_now_ns = tn.tv_sec * (uint64_t)1000000000 + tn.tv_nsec;
|
|
uint64_t time_now_ns = tn.tv_sec;
|
|
time_now_ns = time_now_ns * 1000000000;
|
|
time_now_ns = time_now_ns + tn.tv_nsec;
|
|
|
|
// see if it's stalled
|
|
|
|
if ((stall_monitor_start_time != 0) && (stall_monitor_frame_count == delay_temp)) {
|
|
// hasn't outputted anything since the last call to delay()
|
|
|
|
if (((update_timestamp_ns - stall_monitor_start_time) >
|
|
stall_monitor_error_threshold) ||
|
|
((time_now_ns - stall_monitor_start_time) > stall_monitor_error_threshold)) {
|
|
debug(2,
|
|
"DAC seems to have stalled with time_now_ns: %" PRIX64
|
|
", update_timestamp_ns: %" PRIX64 ", stall_monitor_start_time %" PRIX64
|
|
", stall_monitor_error_threshold %" PRIX64 ".",
|
|
time_now_ns, update_timestamp_ns, stall_monitor_start_time,
|
|
stall_monitor_error_threshold);
|
|
debug(2,
|
|
"DAC seems to have stalled with time_now: %lx,%lx"
|
|
", update_timestamp: %lx,%lx, stall_monitor_start_time %" PRIX64
|
|
", stall_monitor_error_threshold %" PRIX64 ".",
|
|
tn.tv_sec, tn.tv_nsec, update_timestamp.tv_sec, update_timestamp.tv_nsec,
|
|
stall_monitor_start_time, stall_monitor_error_threshold);
|
|
ret = sps_extra_code_output_stalled;
|
|
}
|
|
} else {
|
|
stall_monitor_start_time = update_timestamp_ns;
|
|
stall_monitor_frame_count = delay_temp;
|
|
}
|
|
|
|
if (ret == 0) {
|
|
uint64_t delta = time_now_ns - update_timestamp_ns;
|
|
|
|
// uint64_t frames_played_since_last_interrupt =
|
|
// ((uint64_t)config.output_rate * delta) / 1000000000;
|
|
|
|
uint64_t frames_played_since_last_interrupt = config.output_rate;
|
|
frames_played_since_last_interrupt = frames_played_since_last_interrupt * delta;
|
|
frames_played_since_last_interrupt = frames_played_since_last_interrupt / 1000000000;
|
|
|
|
snd_pcm_sframes_t frames_played_since_last_interrupt_sized =
|
|
frames_played_since_last_interrupt;
|
|
if ((frames_played_since_last_interrupt_sized < 0) ||
|
|
((uint64_t)frames_played_since_last_interrupt_sized !=
|
|
frames_played_since_last_interrupt))
|
|
debug(1,
|
|
"overflow resizing frames_played_since_last_interrupt %" PRIx64
|
|
" to frames_played_since_last_interrupt %lx.",
|
|
frames_played_since_last_interrupt, frames_played_since_last_interrupt_sized);
|
|
delay_temp = delay_temp - frames_played_since_last_interrupt_sized;
|
|
}
|
|
}
|
|
} else { // not running, thus no delay information, thus can't check for
|
|
// stall
|
|
delay_temp = 0;
|
|
stall_monitor_start_time = 0; // zero if not initialised / not started / zeroed by flush
|
|
stall_monitor_frame_count = 0; // set to delay at start of time, incremented by any writes
|
|
|
|
// not running, thus no delay information, thus can't check for frame
|
|
// rates
|
|
// frame_index = 0; // we'll be starting over...
|
|
// measurement_data_is_valid = 0;
|
|
}
|
|
} else {
|
|
debug(1, "alsa: can't get device's status.");
|
|
}
|
|
|
|
} else {
|
|
debug(2, "alsa_handle is NULL in precision_delay_and_status!");
|
|
}
|
|
if (delay != NULL)
|
|
*delay = delay_temp;
|
|
if (state != NULL)
|
|
*state = state_temp;
|
|
return ret;
|
|
}
|
|
|
|
static int delay(long *the_delay) {
|
|
// returns 0 if the device is in a valid state -- SND_PCM_STATE_RUNNING or
|
|
// SND_PCM_STATE_PREPARED
|
|
// or SND_PCM_STATE_DRAINING
|
|
// and returns the actual delay if running or 0 if prepared in *the_delay
|
|
|
|
// otherwise return an error code
|
|
// the error code could be a Unix errno code or a snderror code, or
|
|
// the sps_extra_code_output_stalled or the
|
|
// sps_extra_code_output_state_cannot_make_ready codes
|
|
int ret = 0;
|
|
snd_pcm_sframes_t my_delay = 0;
|
|
|
|
int oldState;
|
|
|
|
snd_pcm_state_t state;
|
|
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
|
|
pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 0);
|
|
|
|
ret = delay_and_status(&state, &my_delay, NULL);
|
|
|
|
debug_mutex_unlock(&alsa_mutex, 0);
|
|
pthread_cleanup_pop(0);
|
|
pthread_setcancelstate(oldState, NULL);
|
|
|
|
if (the_delay != NULL) // can't imagine why this might happen
|
|
*the_delay = my_delay; // note: snd_pcm_sframes_t is a long
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time,
|
|
uint64_t *the_delay, uint64_t *frames_sent_to_dac) {
|
|
// returns 0 if the device is in a valid state -- SND_PCM_STATE_RUNNING or
|
|
// SND_PCM_STATE_PREPARED
|
|
// or SND_PCM_STATE_DRAINING.
|
|
// returns the actual delay if running or 0 if prepared in *the_delay
|
|
// returns the present value of frames_sent_for_playing
|
|
// otherwise return a non-zero value
|
|
int ret = 0;
|
|
*the_delay = 0;
|
|
|
|
int oldState;
|
|
|
|
snd_pcm_state_t state;
|
|
snd_pcm_sframes_t my_delay = 0; // this initialisation is to silence a clang warning
|
|
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
|
|
pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 0);
|
|
|
|
if (alsa_handle == NULL) {
|
|
ret = alsa_handle_status;
|
|
} else {
|
|
*raw_measurement_time =
|
|
get_absolute_time_in_ns(); // this is not conditioned ("disciplined") by NTP
|
|
*corrected_measurement_time = get_monotonic_time_in_ns(); // this is ("disciplined") by NTP
|
|
ret = delay_and_status(&state, &my_delay, NULL);
|
|
}
|
|
if (ret == 0)
|
|
ret = frames_sent_break_occurred; // will be zero unless an error like an underrun occurred
|
|
else
|
|
ret = 1; // just indicate there was some kind of a break
|
|
frames_sent_break_occurred = 0; // reset it.
|
|
if (frames_sent_to_dac != NULL)
|
|
*frames_sent_to_dac = frames_sent_for_playing;
|
|
debug_mutex_unlock(&alsa_mutex, 0);
|
|
pthread_cleanup_pop(0);
|
|
pthread_setcancelstate(oldState, NULL);
|
|
uint64_t hd = my_delay; // note: snd_pcm_sframes_t is a long
|
|
*the_delay = hd;
|
|
return ret;
|
|
}
|
|
/*
|
|
static int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played) {
|
|
// elapsed_time is in nanoseconds
|
|
int response = 0; // zero means okay
|
|
if (measurement_data_is_valid) {
|
|
*elapsed_time = measurement_time - measurement_start_time;
|
|
*frames_played = frames_played_at_measurement_time - frames_played_at_measurement_start_time;
|
|
} else {
|
|
*elapsed_time = 0;
|
|
*frames_played = 0;
|
|
response = -1;
|
|
}
|
|
return response;
|
|
}
|
|
*/
|
|
|
|
static int do_play(void *buf, int samples) {
|
|
// assuming the alsa_mutex has been acquired
|
|
// debug(3,"audio_alsa play called.");
|
|
int oldState;
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
|
|
|
|
snd_pcm_state_t state;
|
|
snd_pcm_sframes_t my_delay;
|
|
int ret = delay_and_status(&state, &my_delay, NULL);
|
|
|
|
if (ret == 0) { // will be non-zero if an error or a stall
|
|
|
|
if ((samples != 0) && (buf != NULL)) {
|
|
|
|
// just check the state of the DAC
|
|
|
|
if ((state != SND_PCM_STATE_PREPARED) && (state != SND_PCM_STATE_RUNNING) &&
|
|
(state != SND_PCM_STATE_XRUN)) {
|
|
debug(1, "alsa: DAC in odd SND_PCM_STATE_* %d prior to writing.", state);
|
|
}
|
|
|
|
snd_pcm_state_t prior_state = state; // keep this for afterwards....
|
|
// debug(3, "alsa: write %d frames.", samples);
|
|
ret = alsa_pcm_write(alsa_handle, buf, samples);
|
|
if (ret > 0)
|
|
frames_sent_for_playing += ret; // this is the number of frames accepted
|
|
if (ret == samples) {
|
|
stall_monitor_frame_count += samples;
|
|
} else {
|
|
frames_sent_break_occurred = 1; // note than an output error has occurred
|
|
if (ret == -EPIPE) { /* underrun */
|
|
|
|
// It could be that the DAC was in the SND_PCM_STATE_XRUN state before
|
|
// sending the samples to be output. If so, it will still be in
|
|
// the SND_PCM_STATE_XRUN state after the call and it needs to be recovered.
|
|
|
|
// The underrun occurred in the past, so flagging an
|
|
// error at this point is misleading.
|
|
|
|
// In fact, having put samples in the buffer, we are about to fix it by now
|
|
// issuing a snd_pcm_recover().
|
|
|
|
// So, if state is SND_PCM_STATE_XRUN now, only report it if the state was
|
|
// not SND_PCM_STATE_XRUN prior to the call, i.e. report it only
|
|
// if we are not trying to recover from a previous underrun.
|
|
|
|
if (prior_state == SND_PCM_STATE_XRUN)
|
|
debug(1, "alsa: recovering from a previous underrun.");
|
|
else
|
|
debug(1, "alsa: underrun while writing %d samples to alsa device.", samples);
|
|
int tret = snd_pcm_recover(alsa_handle, ret, 1);
|
|
if (tret < 0) {
|
|
warn("alsa: can't recover from SND_PCM_STATE_XRUN: %s.", snd_strerror(tret));
|
|
}
|
|
} else if (ret == -ESTRPIPE) { /* suspended */
|
|
debug(1, "alsa: suspended while writing %d samples to alsa device.", samples);
|
|
int tret;
|
|
while ((tret = snd_pcm_resume(alsa_handle)) == -EAGAIN) {
|
|
sleep(1); /* wait until the suspend flag is released */
|
|
if (tret < 0) {
|
|
warn("alsa: can't recover from SND_PCM_STATE_SUSPENDED state, "
|
|
"snd_pcm_prepare() "
|
|
"failed: %s.",
|
|
snd_strerror(tret));
|
|
}
|
|
}
|
|
} else {
|
|
char errorstring[1024];
|
|
strerror_r(-ret, (char *)errorstring, sizeof(errorstring));
|
|
debug(1, "alsa: error %d (\"%s\") writing %d samples to alsa device.", ret,
|
|
(char *)errorstring, samples);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
debug(1,
|
|
"alsa: device status returns fault status %d and SND_PCM_STATE_* "
|
|
"%d for play.",
|
|
ret, state);
|
|
}
|
|
|
|
pthread_setcancelstate(oldState, NULL);
|
|
return ret;
|
|
}
|
|
|
|
static int do_open(int do_auto_setup) {
|
|
int ret = 0;
|
|
if (alsa_backend_state != abm_disconnected)
|
|
debug(1, "alsa: do_open() -- opening the output device when it is already "
|
|
"connected");
|
|
if (alsa_handle == NULL) {
|
|
// debug(1,"alsa: do_open() -- opening the output device");
|
|
ret = open_alsa_device(do_auto_setup);
|
|
if (ret == 0) {
|
|
mute_requested_internally = 0;
|
|
if (audio_alsa.volume)
|
|
do_volume(set_volume);
|
|
if (audio_alsa.mute) {
|
|
debug(2, "do_open() set_mute_state");
|
|
set_mute_state(); // the mute_requested_externally flag will have been
|
|
// set accordingly
|
|
// do_mute(0); // complete unmute
|
|
}
|
|
frames_sent_break_occurred = 1; // there is a discontinuity with
|
|
// any previously-reported frame count
|
|
frames_sent_for_playing = 0;
|
|
alsa_backend_state = abm_connected; // only do this if it really opened it.
|
|
}
|
|
} else {
|
|
debug(1, "alsa: do_open() -- output device already open.");
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static int do_close() {
|
|
debug(2, "alsa: do_close()");
|
|
if (alsa_backend_state == abm_disconnected)
|
|
debug(1, "alsa: do_close() -- closing the output device when it is already "
|
|
"disconnected");
|
|
int derr = 0;
|
|
if (alsa_handle) {
|
|
// debug(1,"alsa: do_close() -- closing the output device");
|
|
if ((derr = snd_pcm_drop(alsa_handle)))
|
|
debug(1, "Error %d (\"%s\") dropping output device.", derr, snd_strerror(derr));
|
|
usleep(5000); // this make the function pthread cancellable
|
|
if ((derr = snd_pcm_hw_free(alsa_handle)))
|
|
debug(1, "Error %d (\"%s\") freeing the output device hardware.", derr, snd_strerror(derr));
|
|
debug(2, "alsa: do_close() -- closing alsa handle");
|
|
if ((derr = snd_pcm_close(alsa_handle)))
|
|
debug(1, "Error %d (\"%s\") closing the output device.", derr, snd_strerror(derr));
|
|
alsa_handle = NULL;
|
|
alsa_handle_status = ENODEV; // no device open
|
|
} else {
|
|
debug(1, "alsa: do_close() -- output device already closed.");
|
|
}
|
|
alsa_backend_state = abm_disconnected;
|
|
return derr;
|
|
}
|
|
|
|
static int sub_flush() {
|
|
if (alsa_backend_state == abm_disconnected)
|
|
debug(1, "alsa: do_flush() -- closing the output device when it is already "
|
|
"disconnected");
|
|
int derr = 0;
|
|
if (alsa_handle) {
|
|
debug(2, "alsa: do_flush() -- flushing the output device");
|
|
frames_sent_break_occurred = 1;
|
|
if ((derr = snd_pcm_drop(alsa_handle)))
|
|
debug(1, "Error %d (\"%s\") dropping output device.", derr, snd_strerror(derr));
|
|
if ((derr = snd_pcm_prepare(alsa_handle)))
|
|
debug(1, "Error %d (\"%s\") preparing output device after flush.", derr, snd_strerror(derr));
|
|
alsa_backend_state = abm_connected;
|
|
} else {
|
|
debug(1, "alsa: do_flush() -- output device already closed.");
|
|
}
|
|
return derr;
|
|
}
|
|
|
|
static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
|
__attribute__((unused)) uint32_t timestamp,
|
|
__attribute__((unused)) uint64_t playtime) {
|
|
|
|
// play() will change the state of the alsa_backend_mode to abm_playing
|
|
// also, if the present alsa_backend_state is abm_disconnected, then first the
|
|
// DAC must be
|
|
// connected
|
|
|
|
// debug(3,"audio_alsa play called.");
|
|
int ret = 0;
|
|
|
|
pthread_cleanup_debug_mutex_lock(&alsa_mutex, 50000, 0);
|
|
|
|
if (alsa_backend_state == abm_disconnected) {
|
|
ret = do_open(0); // don't try to auto setup
|
|
if (ret == 0)
|
|
debug(2, "alsa: play() -- opened output device");
|
|
}
|
|
|
|
if (ret == 0) {
|
|
if (alsa_backend_state != abm_playing) {
|
|
debug(2, "alsa: play() -- alsa_backend_state => abm_playing");
|
|
alsa_backend_state = abm_playing;
|
|
|
|
// mute_requested_internally = 0; // stop requesting a mute for backend's own
|
|
// reasons, which might have been a flush
|
|
// debug(2, "play() set_mute_state");
|
|
// set_mute_state(); // try to action the request and return a status
|
|
// do_mute(0); // unmute for backend's reason
|
|
}
|
|
ret = do_play(buf, samples);
|
|
}
|
|
|
|
debug_mutex_unlock(&alsa_mutex, 0);
|
|
pthread_cleanup_pop(0); // release the mutex
|
|
return ret;
|
|
}
|
|
|
|
static int prepare(void) {
|
|
// this will leave the DAC open / connected.
|
|
int ret = 0;
|
|
|
|
pthread_cleanup_debug_mutex_lock(&alsa_mutex, 50000, 0);
|
|
|
|
if (alsa_backend_state == abm_disconnected) {
|
|
if (alsa_device_initialised == 0) {
|
|
// debug(1, "alsa: prepare() calling alsa_device_init.");
|
|
alsa_device_init();
|
|
alsa_device_initialised = 1;
|
|
}
|
|
ret = do_open(1); // do auto setup
|
|
if (ret == 0)
|
|
debug(2, "alsa: prepare() -- opened output device");
|
|
}
|
|
|
|
debug_mutex_unlock(&alsa_mutex, 0);
|
|
pthread_cleanup_pop(0); // release the mutex
|
|
return ret;
|
|
}
|
|
|
|
static void flush(void) {
|
|
// debug(2,"audio_alsa flush called.");
|
|
pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1);
|
|
if (alsa_backend_state != abm_disconnected) { // must be playing or connected...
|
|
// do nothing for a flush if config.keep_dac_busy is true
|
|
if (config.keep_dac_busy == 0) {
|
|
sub_flush();
|
|
}
|
|
} else {
|
|
debug(3, "alsa: flush() -- called on a disconnected alsa backend");
|
|
}
|
|
debug_mutex_unlock(&alsa_mutex, 3);
|
|
pthread_cleanup_pop(0); // release the mutex
|
|
}
|
|
|
|
static void stop(void) {
|
|
pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1);
|
|
if (alsa_backend_state != abm_disconnected) { // must be playing or connected...
|
|
if (config.keep_dac_busy == 0) {
|
|
do_close();
|
|
}
|
|
} else
|
|
debug(3, "alsa: stop() -- called on a disconnected alsa backend");
|
|
debug_mutex_unlock(&alsa_mutex, 3);
|
|
pthread_cleanup_pop(0); // release the mutex
|
|
}
|
|
|
|
static void parameters(audio_parameters *info) {
|
|
info->minimum_volume_dB = alsa_mix_mindb;
|
|
info->maximum_volume_dB = alsa_mix_maxdb;
|
|
}
|
|
|
|
static void do_volume(double vol) { // caller is assumed to have the alsa_mutex when
|
|
// using this function
|
|
debug(3, "Setting volume db to %f.", vol);
|
|
int oldState;
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
|
|
set_volume = vol;
|
|
pthread_cleanup_debug_mutex_lock(&alsa_mixer_mutex, 1000, 1);
|
|
if (volume_set_request && (open_mixer() == 1)) {
|
|
if (has_softvol) {
|
|
if (ctl && elem_id) {
|
|
snd_ctl_elem_value_t *value;
|
|
long raw;
|
|
|
|
if (snd_ctl_convert_from_dB(ctl, elem_id, vol, &raw, 0) < 0)
|
|
debug(1, "Failed converting dB gain to raw volume value for the "
|
|
"software volume control.");
|
|
|
|
snd_ctl_elem_value_alloca(&value);
|
|
snd_ctl_elem_value_set_id(value, elem_id);
|
|
snd_ctl_elem_value_set_integer(value, 0, raw);
|
|
snd_ctl_elem_value_set_integer(value, 1, raw);
|
|
if (snd_ctl_elem_write(ctl, value) < 0)
|
|
debug(1, "Failed to set playback dB volume for the software volume "
|
|
"control.");
|
|
}
|
|
} else {
|
|
if (volume_based_mute_is_active == 0) {
|
|
// debug(1,"Set alsa volume.");
|
|
do_snd_mixer_selem_set_playback_dB_all(alsa_mix_elem, vol);
|
|
} else {
|
|
debug(2, "Not setting volume because volume-based mute is active");
|
|
}
|
|
}
|
|
volume_set_request = 0; // any external request that has been made is now satisfied
|
|
close_mixer();
|
|
}
|
|
debug_mutex_unlock(&alsa_mixer_mutex, 3);
|
|
pthread_cleanup_pop(0); // release the mutex
|
|
pthread_setcancelstate(oldState, NULL);
|
|
}
|
|
|
|
static void volume(double vol) {
|
|
volume_set_request = 1; // an external request has been made to set the volume
|
|
do_volume(vol);
|
|
}
|
|
|
|
/*
|
|
static void linear_volume(double vol) {
|
|
debug(2, "Setting linear volume to %f.", vol);
|
|
set_volume = vol;
|
|
if ((alsa_mix_ctrl == NULL) && alsa_mix_handle) {
|
|
double linear_volume = pow(10, vol);
|
|
// debug(1,"Linear volume is %f.",linear_volume);
|
|
long int_vol = alsa_mix_minv + (alsa_mix_maxv - alsa_mix_minv) *
|
|
linear_volume;
|
|
// debug(1,"Setting volume to %ld, for volume input of %f.",int_vol,vol);
|
|
if (alsa_mix_handle) {
|
|
if (snd_mixer_selem_set_playback_volume_all(alsa_mix_elem, int_vol) != 0)
|
|
die("Failed to set playback volume");
|
|
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
|
|
static int mute(int mute_state_requested) { // these would be for external reasons, not
|
|
// because of the
|
|
// state of the backend.
|
|
mute_requested_externally = mute_state_requested; // request a mute for external reasons
|
|
debug(2, "mute(%d) set_mute_state", mute_state_requested);
|
|
return set_mute_state();
|
|
}
|
|
/*
|
|
static void alsa_buffer_monitor_thread_cleanup_function(__attribute__((unused)) void
|
|
*arg) {
|
|
debug(1, "alsa: alsa_buffer_monitor_thread_cleanup_function called.");
|
|
}
|
|
*/
|
|
|
|
static void *alsa_buffer_monitor_thread_code(__attribute__((unused)) void *arg) {
|
|
int frame_count = 0;
|
|
int error_count = 0;
|
|
int error_detected = 0;
|
|
int okb = -1;
|
|
while (error_detected ==
|
|
0) { // if too many play errors occur early on, we will turn off the disable stanby mode
|
|
if (okb != config.keep_dac_busy) {
|
|
debug(2, "keep_dac_busy is now \"%s\"", config.keep_dac_busy == 0 ? "no" : "yes");
|
|
okb = config.keep_dac_busy;
|
|
}
|
|
if ((config.keep_dac_busy != 0) && (alsa_device_initialised == 0)) {
|
|
debug(2, "alsa: alsa_buffer_monitor_thread_code() calling "
|
|
"alsa_device_init.");
|
|
alsa_device_init();
|
|
alsa_device_initialised = 1;
|
|
}
|
|
int sleep_time_us = (int)(config.disable_standby_mode_silence_scan_interval * 1000000);
|
|
pthread_cleanup_debug_mutex_lock(&alsa_mutex, 200000, 0);
|
|
// check possible state transitions here
|
|
if ((alsa_backend_state == abm_disconnected) && (config.keep_dac_busy != 0)) {
|
|
// open the dac and move to abm_connected mode
|
|
if (do_open(1) == 0) // no automatic setup of rate and speed if necessary
|
|
debug(2, "alsa: alsa_buffer_monitor_thread_code() -- output device opened; "
|
|
"alsa_backend_state => abm_connected");
|
|
} else if ((alsa_backend_state == abm_connected) && (config.keep_dac_busy == 0)) {
|
|
stall_monitor_start_time = 0;
|
|
// frame_index = 0;
|
|
// measurement_data_is_valid = 0;
|
|
debug(2, "alsa: alsa_buffer_monitor_thread_code() -- closing the output "
|
|
"device");
|
|
do_close();
|
|
debug(2, "alsa: alsa_buffer_monitor_thread_code() -- alsa_backend_state "
|
|
"=> abm_disconnected");
|
|
}
|
|
// now, if the backend is not in the abm_disconnected state
|
|
// and config.keep_dac_busy is true (at the present, this has to be the case
|
|
// to be in the
|
|
// abm_connected state in the first place...) then do the silence-filling
|
|
// thing, if needed /* only if the output device is capable of precision delay */.
|
|
if ((alsa_backend_state != abm_disconnected) &&
|
|
(config.keep_dac_busy != 0) /* && precision_delay_available() */) {
|
|
int reply;
|
|
long buffer_size = 0;
|
|
snd_pcm_state_t state;
|
|
reply = delay_and_status(&state, &buffer_size, NULL);
|
|
if (reply != 0) {
|
|
buffer_size = 0;
|
|
char errorstring[1024];
|
|
strerror_r(-reply, (char *)errorstring, sizeof(errorstring));
|
|
debug(1, "alsa: alsa_buffer_monitor_thread_code delay error %d: \"%s\".", reply,
|
|
(char *)errorstring);
|
|
}
|
|
long buffer_size_threshold =
|
|
(long)(config.disable_standby_mode_silence_threshold * config.output_rate);
|
|
size_t size_of_silence_buffer;
|
|
if (buffer_size < buffer_size_threshold) {
|
|
int frames_of_silence = 1024;
|
|
size_of_silence_buffer = frames_of_silence * frame_size;
|
|
void *silence = malloc(size_of_silence_buffer);
|
|
if (silence == NULL) {
|
|
warn("disable_standby_mode has been turned off because a memory allocation error "
|
|
"occurred.");
|
|
error_detected = 1;
|
|
} else {
|
|
int ret;
|
|
pthread_cleanup_push(malloc_cleanup, silence);
|
|
int use_dither = 0;
|
|
if ((alsa_mix_ctrl == NULL) && (config.ignore_volume_control == 0) &&
|
|
(config.airplay_volume != 0.0))
|
|
use_dither = 1;
|
|
dither_random_number_store =
|
|
generate_zero_frames(silence, frames_of_silence, config.output_format,
|
|
use_dither, // i.e. with dither
|
|
dither_random_number_store);
|
|
ret = do_play(silence, frames_of_silence);
|
|
frame_count++;
|
|
pthread_cleanup_pop(1); // free malloced buffer
|
|
if (ret < 0) {
|
|
error_count++;
|
|
char errorstring[1024];
|
|
strerror_r(-ret, (char *)errorstring, sizeof(errorstring));
|
|
debug(2,
|
|
"alsa: alsa_buffer_monitor_thread_code error %d (\"%s\") writing %d samples "
|
|
"to alsa device -- %d errors in %d trials.",
|
|
ret, (char *)errorstring, frames_of_silence, error_count, frame_count);
|
|
if ((error_count > 40) && (frame_count < 100)) {
|
|
warn("disable_standby_mode has been turned off because too many underruns "
|
|
"occurred. Is Shairport Sync outputting to a virtual device or running in a "
|
|
"virtual machine?");
|
|
error_detected = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
debug_mutex_unlock(&alsa_mutex, 0);
|
|
pthread_cleanup_pop(0); // release the mutex
|
|
usleep(sleep_time_us); // has a cancellation point in it
|
|
}
|
|
pthread_exit(NULL);
|
|
}
|