shairport-sync/audio_sndio.c

309 lines
10 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 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;
int at_least_one_onmove_cb_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"); }
void onmove_cb(__attribute__((unused)) void *arg, int delta) {
time_of_last_onmove_cb = get_absolute_time_in_ns();
at_least_one_onmove_cb_seen = 1;
played += delta;
}
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;
time_of_last_onmove_cb = 0;
at_least_one_onmove_cb_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);
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, __attribute__((unused)) int sample_type,
__attribute__((unused)) uint32_t timestamp,
__attribute__((unused)) uint64_t playtime) {
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;
pthread_cleanup_pop(1); // unlock the mutex
}
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_absolute_time_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)
*delay = (written / framesize) - (played + estimated_extra_frames_output);
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;
// pthread_mutex_unlock(&sndio_mutex);
pthread_cleanup_pop(1); // unlock the mutex
}
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 = NULL,
.play = &play,
.volume = NULL,
.parameters = NULL,
.mute = NULL};