Files
shairport-sync/audio_sndio.c
T

388 lines
13 KiB
C

/*
* sndio output driver. This file is part of Shairport Sync.
* Copyright (c) 2013 Dimitri Sokolyuk <demon@dim13.org>
* Copyright (c) 2017 Tobias Kortkamp <t@tobik.me>
*
* Modifications for audio synchronisation
* and related work, copyright (c) Mike Brady 2014 -- 2022
* All rights reserved.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include "audio.h"
#include "common.h"
#include <pthread.h>
#include <sndio.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static void help(void);
static int init(int, char **);
static void onmove_cb(void *, int);
static void deinit(void);
static void start(int, int);
static int play(void *, int);
static void stop(void);
static void onmove_cb(void *, int);
static int delay(long *);
static void flush(void);
int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time, uint64_t *the_delay,
uint64_t *frames_sent_to_dac);
audio_output audio_sndio = {.name = "sndio",
.help = &help,
.init = &init,
.deinit = &deinit,
.prepare = NULL,
.start = &start,
.stop = &stop,
.is_running = NULL,
.flush = &flush,
.delay = &delay,
.stats = &stats,
.play = &play,
.volume = NULL,
.parameters = NULL,
.mute = NULL};
static pthread_mutex_t sndio_mutex = PTHREAD_MUTEX_INITIALIZER;
static struct sio_hdl *hdl;
static int framesize;
static size_t played;
static size_t written;
uint64_t time_of_last_onmove_cb;
uint64_t corrected_time_of_last_onmove_cb;
int at_least_one_onmove_cb_seen;
static uint64_t frames_sent_for_playing;
// set to true if there has been a discontinuity between the last reported value for "played"
// and the present reported "played"
// Note that it will be set when the device is opened, as any previous figures for
// "played" (which Shairport Sync might hold) would be invalid.
static int frames_sent_break_occurred;
static int underrun_seen;
struct sio_par par;
struct sndio_formats {
const char *name;
sps_format_t fmt;
unsigned int rate;
unsigned int bits;
unsigned int bps;
unsigned int sig;
unsigned int le;
};
static struct sndio_formats formats[] = {{"S8", SPS_FORMAT_S8, 44100, 8, 1, 1, SIO_LE_NATIVE},
{"U8", SPS_FORMAT_U8, 44100, 8, 1, 0, SIO_LE_NATIVE},
{"S16", SPS_FORMAT_S16, 44100, 16, 2, 1, SIO_LE_NATIVE},
{"AUTOMATIC", SPS_FORMAT_S16, 44100, 16, 2, 1,
SIO_LE_NATIVE}, // TODO: make this really automatic?
{"S24", SPS_FORMAT_S24, 44100, 24, 4, 1, SIO_LE_NATIVE},
{"S24_3LE", SPS_FORMAT_S24_3LE, 44100, 24, 3, 1, 1},
{"S24_3BE", SPS_FORMAT_S24_3BE, 44100, 24, 3, 1, 0},
{"S32", SPS_FORMAT_S32, 44100, 32, 4, 1, SIO_LE_NATIVE}};
static void help() { printf(" -d output-device set the output device [default*|...]\n"); }
uint64_t get_uptime_in_ns() {
uint64_t time_now_ns;
struct timespec tn;
clock_gettime(CLOCK_UPTIME_PRECISE, &tn);
uint64_t tnnsec = tn.tv_sec;
tnnsec = tnnsec * 1000000000;
uint64_t tnjnsec = tn.tv_nsec;
time_now_ns = tnnsec + tnjnsec;
return time_now_ns;
}
static int init(int argc, char **argv) {
int found, opt, round, rate, bufsz;
unsigned int i;
const char *devname, *tmp;
// set up default values first
sio_initpar(&par);
par.rate = 44100;
par.pchan = 2;
par.bits = 16;
par.bps = SIO_BPS(par.bits);
par.le = 1;
par.sig = 1;
devname = SIO_DEVANY;
config.audio_backend_buffer_desired_length = 1.0;
config.audio_backend_buffer_interpolation_threshold_in_seconds =
0.25; // below this, soxr interpolation will not occur -- it'll be basic interpolation
// instead.
config.audio_backend_latency_offset = 0;
// get settings from settings file
// do the "general" audio options. Note, these options are in the "general" stanza!
parse_general_audio_options();
// get the specific settings
if (config.cfg != NULL) {
if (!config_lookup_string(config.cfg, "sndio.device", &devname))
devname = SIO_DEVANY;
if (config_lookup_int(config.cfg, "sndio.rate", &rate)) {
if (rate % 44100 == 0 && rate >= 44100 && rate <= 352800) {
par.rate = rate;
} else {
die("sndio: output rate must be a multiple of 44100 and 44100 <= rate <= "
"352800");
}
}
if (config_lookup_int(config.cfg, "sndio.bufsz", &bufsz)) {
if (bufsz > 0) {
par.appbufsz = bufsz;
} else {
die("sndio: bufsz must be > 0");
}
}
if (config_lookup_int(config.cfg, "sndio.round", &round)) {
if (round > 0) {
par.round = round;
} else {
die("sndio: round must be > 0");
}
}
if (config_lookup_string(config.cfg, "sndio.format", &tmp)) {
for (i = 0, found = 0; i < sizeof(formats) / sizeof(formats[0]); i++) {
if (strcasecmp(formats[i].name, tmp) == 0) {
config.output_format = formats[i].fmt;
found = 1;
break;
}
}
if (!found)
die("Invalid output format \"%s\". Should be one of: S8, U8, S16, S24, "
"S24_3LE, S24_3BE, S32, Automatic",
tmp);
}
}
optind = 1; // optind=0 is equivalent to optind=1 plus special behaviour
argv--; // so we shift the arguments to satisfy getopt()
argc++;
while ((opt = getopt(argc, argv, "d:")) > 0) {
switch (opt) {
case 'd':
devname = optarg;
break;
default:
help();
die("Invalid audio option -%c specified", opt);
}
}
if (optind < argc)
die("Invalid audio argument: %s", argv[optind]);
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
// pthread_mutex_lock(&sndio_mutex);
debug(1, "sndio: output device name is \"%s\".", devname);
debug(1, "sndio: rate: %u.",par.rate);
debug(1, "sndio: bits: %u.",par.bits);
hdl = sio_open(devname, SIO_PLAY, 0);
if (!hdl)
die("sndio: cannot open audio device");
written = played = 0;
frames_sent_for_playing = 0;
time_of_last_onmove_cb = 0;
at_least_one_onmove_cb_seen = 0;
underrun_seen = 0;
for (i = 0; i < sizeof(formats) / sizeof(formats[0]); i++) {
if (formats[i].fmt == config.output_format) {
par.bits = formats[i].bits;
par.bps = formats[i].bps;
par.sig = formats[i].sig;
par.le = formats[i].le;
break;
}
}
if (!sio_setpar(hdl, &par) || !sio_getpar(hdl, &par))
die("sndio: failed to set audio parameters");
for (i = 0, found = 0; i < sizeof(formats) / sizeof(formats[0]); i++) {
if (formats[i].bits == par.bits && formats[i].bps == par.bps && formats[i].sig == par.sig &&
formats[i].le == par.le && formats[i].rate == par.rate) {
config.output_format = formats[i].fmt;
found = 1;
break;
}
}
if (!found)
die("sndio: could not set output device to the required format and rate.");
framesize = par.bps * par.pchan;
config.output_rate = par.rate;
if (par.rate == 0) {
die("sndio: par.rate set to zero.");
}
config.audio_backend_buffer_desired_length = 1.0 * par.bufsz / par.rate;
config.audio_backend_latency_offset = 0;
sio_onmove(hdl, onmove_cb, NULL);
// pthread_mutex_unlock(&sndio_mutex);
pthread_cleanup_pop(1); // unlock the mutex
if (framesize == 0) {
die("sndio: framesize set to zero.");
}
return 0;
}
static void deinit() {
// pthread_mutex_lock(&sndio_mutex);
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
sio_close(hdl);
// pthread_mutex_unlock(&sndio_mutex);
pthread_cleanup_pop(1); // unlock the mutex
}
static void start(__attribute__((unused)) int sample_rate,
__attribute__((unused)) int sample_format) {
// pthread_mutex_lock(&sndio_mutex);
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
frames_sent_break_occurred = 1; // there is a discontinuity with
at_least_one_onmove_cb_seen = 0;
// any previously-reported frame count
if (!sio_start(hdl))
die("sndio: unable to start");
written = played = 0;
time_of_last_onmove_cb = 0;
at_least_one_onmove_cb_seen = 0;
// pthread_mutex_unlock(&sndio_mutex);
pthread_cleanup_pop(1); // unlock the mutex
}
static int play(void *buf, int frames) {
if (frames > 0) {
// pthread_mutex_lock(&sndio_mutex);
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
written += sio_write(hdl, buf, frames * framesize);
// pthread_mutex_unlock(&sndio_mutex);
pthread_cleanup_pop(1); // unlock the mutex
}
return 0;
}
static void stop() {
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
if (!sio_stop(hdl))
die("sndio: unable to stop");
written = played = 0;
frames_sent_break_occurred = 1;
pthread_cleanup_pop(1); // unlock the mutex
}
static void onmove_cb(__attribute__((unused)) void *arg, int delta) {
time_of_last_onmove_cb = get_uptime_in_ns(); // this is not (?) adjusted ("disciplined") by NTP
corrected_time_of_last_onmove_cb = get_monotonic_time_in_ns(); // this is ("disciplined") by NTP
at_least_one_onmove_cb_seen = 1;
played += delta;
frames_sent_for_playing += delta;
if (delta == 0) {
debug(1,"No frames written in interval -- is this underrun?");
underrun_seen = 1;
frames_sent_break_occurred = 1;
} else {
underrun_seen = 0;
}
}
int get_delay(long *delay) {
int response = 0;
size_t estimated_extra_frames_output = 0;
if (at_least_one_onmove_cb_seen) { // when output starts, the onmove_cb callback will be made
// calculate the difference in time between now and when the last callback occurred,
// and use it to estimate the frames that would have been output
uint64_t time_difference = get_uptime_in_ns() - time_of_last_onmove_cb;
uint64_t frame_difference = (time_difference * par.rate) / 1000000000;
estimated_extra_frames_output = frame_difference;
// sanity check -- total estimate can not exceed frames written.
if ((estimated_extra_frames_output + played) > written / framesize) {
// debug(1,"play estimate fails sanity check, possibly due to running on a VM");
estimated_extra_frames_output = 0; // can't make any sensible guess
}
// debug(1,"Frames played to last cb: %d, estimated to current time:
// %d.",played,estimated_extra_frames_output);
}
if ((delay != NULL) && (underrun_seen == 0))
*delay = (written / framesize) - (played + estimated_extra_frames_output);
if (underrun_seen != 0)
response = 1;
return response;
}
static int delay(long *delay) {
int result = 0;
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
result = get_delay(delay);
pthread_cleanup_pop(1); // unlock the mutex
return result;
}
static void flush() {
// pthread_mutex_lock(&sndio_mutex);
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
if (!sio_stop(hdl) || !sio_start(hdl))
die("sndio: unable to flush");
written = played = 0;
frames_sent_break_occurred = 1;
// pthread_mutex_unlock(&sndio_mutex);
pthread_cleanup_pop(1); // unlock the mutex
}
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
// returns the actual delay if running or 0 if prepared in *the_delay
// returns the present estimated value of frames played
// otherwise return a non-zero value
int ret = 0;
*the_delay = 0;
int oldState;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
long my_delay = 0; // initialised to stop a compiler warning
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
*raw_measurement_time = time_of_last_onmove_cb;
*corrected_measurement_time = corrected_time_of_last_onmove_cb; // this is ("disciplined") by NTP
get_delay(&my_delay);
ret = frames_sent_break_occurred; // will be zero unless an error like an underrun occurred
frames_sent_break_occurred = 0; // reset it.
if (frames_sent_to_dac != NULL)
*frames_sent_to_dac = frames_sent_for_playing;
pthread_cleanup_pop(1); // unlock the mutex
pthread_setcancelstate(oldState, NULL);
uint64_t hd = my_delay; // note: snd_pcm_sframes_t is a long
*the_delay = hd;
if (at_least_one_onmove_cb_seen == 0)
ret = 1;
return ret;
}