Files
shairport-sync/audio_sndio.c
T

280 lines
9.1 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 -- 2017
* 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);
audio_output audio_sndio = {.name = "sndio",
.help = &help,
.init = &init,
.deinit = &deinit,
.start = &start,
.stop = &stop,
.is_running = NULL,
.flush = &flush,
.delay = &delay,
.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;
int64_t time_of_last_onmove_cb;
int at_least_one_onmove_cb_seen;
struct sio_par par;
struct sndio_formats {
const char *name;
enum 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, 24, 4, 1, SIO_LE_NATIVE}};
static void help() { printf(" -d output-device set the output device [default*|...]\n"); }
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_mutex_lock(&sndio_mutex);
debug(1, "Output device name is \"%s\".", devname);
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;
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);
return 0;
}
static void deinit() {
pthread_mutex_lock(&sndio_mutex);
sio_close(hdl);
pthread_mutex_unlock(&sndio_mutex);
}
static void start(__attribute__((unused)) int sample_rate,
__attribute__((unused)) int sample_format) {
pthread_mutex_lock(&sndio_mutex);
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);
}
static int play(void *buf, int frames) {
if (frames > 0) {
pthread_mutex_lock(&sndio_mutex);
written += sio_write(hdl, buf, frames * framesize);
pthread_mutex_unlock(&sndio_mutex);
}
return 0;
}
static void stop() {
pthread_mutex_lock(&sndio_mutex);
if (!sio_stop(hdl))
die("sndio: unable to stop");
written = played = 0;
pthread_mutex_unlock(&sndio_mutex);
}
static void onmove_cb(__attribute__((unused)) void *arg, int delta) {
time_of_last_onmove_cb = get_absolute_time_in_fp();
at_least_one_onmove_cb_seen = 1;
played += delta;
}
static int delay(long *_delay) {
pthread_mutex_lock(&sndio_mutex);
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 occoured,
// and use it to estimate the frames that would have been output
uint64_t time_difference = get_absolute_time_in_fp() - time_of_last_onmove_cb;
uint64_t frame_difference = time_difference * par.rate;
uint64_t frame_difference_big_integer = frame_difference >> 32;
estimated_extra_frames_output = frame_difference_big_integer;
// debug(1,"Frames played to last cb: %d, estimated to current time:
// %d.",played,estimated_extra_frames_output);
}
*_delay = (written / framesize) - (played + estimated_extra_frames_output);
pthread_mutex_unlock(&sndio_mutex);
return 0;
}
static void flush() {
pthread_mutex_lock(&sndio_mutex);
if (!sio_stop(hdl) || !sio_start(hdl))
die("sndio: unable to flush");
written = played = 0;
pthread_mutex_unlock(&sndio_mutex);
}