Files
shairport-sync/audio_sndio.c
T
Mike Brady fd880056fb Squashed commit of the following:
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
2022-09-12 15:19:52 +01:00

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};