diff --git a/audio_pipe.c b/audio_pipe.c index e97f6034..a26bc9f3 100644 --- a/audio_pipe.c +++ b/audio_pipe.c @@ -46,9 +46,16 @@ int warned = 0; static void start(__attribute__((unused)) int sample_rate, __attribute__((unused)) int sample_format) { - // this will leave fd as -1 if a reader hasn't been attached + + + // this will leave fd as -1 if a reader hasn't been attached to the pipe + // we check that it's not a "real" error though. From the "man 2 open" page: + // "ENXIO O_NONBLOCK | O_WRONLY is set, the named file is a FIFO, and no process has the FIFO open for reading." fd = open(pipename, O_WRONLY | O_NONBLOCK); - if ((fd < -1) && (warned == 0)) { + if ((fd == -1) && (errno != ENXIO) && (warned == 0)) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "pipe: start -- error %d (\"%s\") opening the pipe named \"%s\".", errno, (char*)errorstring, pipename); warn("Error %d opening the pipe named \"%s\".", errno, pipename); warned = 1; } @@ -68,7 +75,7 @@ static int play(void *buf, int samples) { warn("Error %d writing to the pipe named \"%s\": \"%s\".", errno, pipename, errorstring); warned = 1; } - } else if ((fd == -1) && (warned == 0)) { + } else if ((fd == -1) && (errno != ENXIO) && (warned == 0)) { strerror_r(errno, (char *)errorstring, 1024); warn("Error %d opening the pipe named \"%s\": \"%s\".", errno, pipename, errorstring); warned = 1; diff --git a/common.c b/common.c index 5fae31af..4bc54c07 100644 --- a/common.c +++ b/common.c @@ -1282,11 +1282,12 @@ char *get_version_string() { char *version_string = malloc(1024); if (version_string) { strcpy(version_string, PACKAGE_VERSION); + #ifdef CONFIG_LIBDAEMON - strcat(version_string, "-libdaemon"); + strcat(version_string, "-libdaemon"); #endif #ifdef CONFIG_MBEDTLS - strcat(version_string, "-mbedTLS"); + strcat(version_string, "-mbedTLS"); #endif #ifdef CONFIG_POLARSSL strcat(version_string, "-PolarSSL"); diff --git a/common.h b/common.h index 1a2dd8a5..0ba8dc5e 100644 --- a/common.h +++ b/common.h @@ -56,6 +56,7 @@ enum endian_type { enum stuffing_type { ST_basic = 0, // straight deletion or insertion of a frame in a 352-frame packet ST_soxr, // use libsoxr to make a 352 frame packet one frame longer or shorter + ST_auto, // use soxr if compiled for it and if the soxr_index is low enough } s_type; enum playback_mode_type { @@ -179,6 +180,8 @@ typedef struct { int cmd_blocking, cmd_start_returns_output; double tolerance; // allow this much drift before attempting to correct it enum stuffing_type packet_stuffing; + int soxr_delay_index; + int soxr_delay_threshold; // the soxr delay must be less or equal to this for soxr interpolation to be enabled under the auto setting int decoders_supported; int use_apple_decoder; // set to 1 if you want to use the apple decoder instead of the original by // David Hammerton diff --git a/configure.ac b/configure.ac index e466adbe..f019be3f 100644 --- a/configure.ac +++ b/configure.ac @@ -184,8 +184,8 @@ AC_ARG_WITH(ssl, [ choose --with-ssl=openssl, --with-ssl=mbedtls or --with-ssl=p # Look for soxr flag AC_ARG_WITH(soxr, [ --with-soxr = choose libsoxr for high-quality interpolation], [ AC_MSG_RESULT(>>Including support for soxr-based interpolation) + AC_DEFINE([CONFIG_SOXR], 1, [Include support for using the SoX Resampler library for interpolation]) if test "x${with_pkg_config}" = xyes ; then - AC_DEFINE([CONFIG_SOXR], 1, [Include support for using the SoX Resampler library for interpolation]) PKG_CHECK_MODULES( [SOXR], [soxr], [LIBS="${SOXR_LIBS} ${LIBS}"], diff --git a/mdns_external.c b/mdns_external.c index 2eae8204..2ae23c61 100644 --- a/mdns_external.c +++ b/mdns_external.c @@ -1,6 +1,7 @@ /* * mDNS registration handler. This file is part of Shairport. * Copyright (c) Paul Lietar 2013 + * Amendments and updates copyright (c) Mike Brady 2014 -- 2019 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -24,8 +25,8 @@ * OTHER DEALINGS IN THE SOFTWARE. */ -#include "common.h" #include "mdns.h" +#include "common.h" #include #include #include @@ -41,45 +42,42 @@ int mdns_pid = 0; */ static int fork_execvp(const char *file, char *const argv[]) { int execpipe[2]; - int pid = 0; - if (pipe(execpipe) < 0) { - return -1; - } + int response = -1; + if (pipe(execpipe) >= 0) { - if (fcntl(execpipe[1], F_SETFD, fcntl(execpipe[1], F_GETFD) | FD_CLOEXEC) < 0) { - close(execpipe[0]); - close(execpipe[1]); - return -1; - } + if (fcntl(execpipe[1], F_SETFD, fcntl(execpipe[1], F_GETFD) | FD_CLOEXEC) < 0) { + close(execpipe[0]); + close(execpipe[1]); + } else { - pid = fork(); - if (pid < 0) { - close(execpipe[0]); - close(execpipe[1]); - return -1; - } else if (pid == 0) { // Child - close(execpipe[0]); // Close the read end - execvp(file, argv); + int pid = fork(); + if (pid < 0) { + close(execpipe[0]); + close(execpipe[1]); + } else if (pid == 0) { // Child + close(execpipe[0]); // Close the read end + execvp(file, argv); + // If we reach this point then execve has failed. + // Write erno's value into the pipe and exit. + if (write(execpipe[1], &errno, sizeof(errno)) != sizeof(errno)) + debug(1, + "Execve has failed and there was a further error writing an error message, duh."); + die("mdns_external: execve has failed."); + } else { // Parent + close(execpipe[1]); // Close the write end - // If we reach this point then execve has failed. - // Write erno's value into the pipe and exit. - if (write(execpipe[1], &errno, sizeof(errno)) != sizeof(errno)) - debug(1, "Execve has failed and there was a further error writing an error message, duh."); - die("mdns_external: execve has failed."); - // return 0; // Just to make the compiler happy. - } else { // Parent - close(execpipe[1]); // Close the write end - - int childErrno; - // Block until child closes the pipe or sends errno. - if (read(execpipe[0], &childErrno, sizeof(childErrno)) == - sizeof(childErrno)) { // We received errno - errno = childErrno; - return -1; - } else { // Child closed the pipe. execvp was successful. - return pid; + int childErrno; + // Block until child closes the pipe or sends errno. + if (read(execpipe[0], &childErrno, sizeof(childErrno)) == + sizeof(childErrno)) { // We received errno + errno = childErrno; + } else { + response = pid; + } + } } } + return response; } static int mdns_external_avahi_register(char *apname, __attribute__((unused)) int port) { diff --git a/player.c b/player.c index 546b474e..c802ca9d 100644 --- a/player.c +++ b/player.c @@ -891,7 +891,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { curframe->given_timestamp; // we will keep buffering until we are // supposed to start playing this have_sent_prefiller_silence = 0; - + // debug(1, "First packet timestamp is %" PRId64 ".", conn->first_packet_timestamp); // say we have started playing here @@ -959,20 +959,22 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { conn); conn->first_packet_time_to_play = should_be_time; - + // we want the frames of silence sent at the start to be fairly large in case the output - // device's minimum buffer size is large. But they can't be greater than the silent lead_in time - // which is either the agreed latency or the silent lead-in time specified by the setting + // device's minimum buffer size is large. But they can't be greater than the silent + // lead_in time + // which is either the agreed latency or the silent lead-in time specified by the + // setting // In fact, if should be some fraction of them, to allow for adjustment. - + int64_t max_dac_delay = conn->latency; if (config.audio_backend_silent_lead_in_time >= 0) max_dac_delay = - (int64_t)(config.audio_backend_silent_lead_in_time * conn->input_rate); - + (int64_t)(config.audio_backend_silent_lead_in_time * conn->input_rate); + max_dac_delay = max_dac_delay / 4; - - // debug(1,"max_dac_delay is %" PRIu64 " frames.", max_dac_delay); + + // debug(1,"max_dac_delay is %" PRIu64 " frames.", max_dac_delay); int64_t filler_size = max_dac_delay; @@ -1333,12 +1335,19 @@ static int stuff_buffer_basic_32(int32_t *inptr, int length, enum sps_format_t l // (d) outputs the result in the approprate format // formats accepted so far include U8, S8, S16, S24, S24_3LE, S24_3BE and S32 +int32_t stat_n = 0; +double stat_mean = 0.0; +double stat_M2 = 0.0; +double longest_soxr_execution_time_us = 0.0; +int64_t packets_processed = 0; + int stuff_buffer_soxr_32(int32_t *inptr, int32_t *scratchBuffer, int length, enum sps_format_t l_output_format, char *outptr, int stuff, int dither, rtsp_conn_info *conn) { if (scratchBuffer == NULL) { die("soxr scratchBuffer not initialised."); } + packets_processed++; int tstuff = stuff; if ((stuff > 1) || (stuff < -1) || (length < 100)) { // debug(1, "Stuff argument to stuff_buffer must be from -1 to +1 and length >100."); @@ -1357,6 +1366,8 @@ int stuff_buffer_soxr_32(int32_t *inptr, int32_t *scratchBuffer, int length, size_t odone; + uint64_t soxr_start_time = get_absolute_time_in_fp(); + soxr_error_t error = soxr_oneshot(length, length + tstuff, 2, // Rates and # of chans. inptr, length, NULL, // Input. scratchBuffer, length + tstuff, &odone, // Output. @@ -1369,6 +1380,19 @@ int stuff_buffer_soxr_32(int32_t *inptr, int32_t *scratchBuffer, int length, if (odone > (size_t)(length + 1)) die("odone = %u!\n", odone); + // mean and variance calculations from "online_variance" algorithm at + // https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm + + double soxr_execution_time_us = + (((get_absolute_time_in_fp() - soxr_start_time) * 1000000) >> 32) * 1.0; + // debug(1,"soxr_execution_time_us: %10.1f",soxr_execution_time_us); + if (soxr_execution_time_us > longest_soxr_execution_time_us) + longest_soxr_execution_time_us = soxr_execution_time_us; + stat_n += 1; + double stat_delta = soxr_execution_time_us - stat_mean; + stat_mean += stat_delta / stat_n; + stat_M2 += stat_delta * (soxr_execution_time_us - stat_mean); + int i; int32_t *ip, *op; ip = inptr; @@ -1411,6 +1435,18 @@ int stuff_buffer_soxr_32(int32_t *inptr, int32_t *scratchBuffer, int length, process_sample(*ip++, &l_outptr, l_output_format, conn->fix_volume, dither, conn); }; } + + if (packets_processed % 1250 == 0) { + debug(2, "soxr_oneshot execution time in microseconds: mean, standard deviation and max " + "for %" PRId32 " interpolations in the last " + "1250 packets. %10.1f, %10.1f, %10.1f.", + stat_n, stat_mean, sqrtf(stat_M2 / (stat_n - 1)), longest_soxr_execution_time_us); + stat_n = 0; + stat_mean = 0.0; + stat_M2 = 0.0; + longest_soxr_execution_time_us = 0.0; + } + conn->amountStuffed = tstuff; return length + tstuff; } @@ -2246,15 +2282,19 @@ void *player_thread_func(void *arg) { } } +#ifdef CONFIG_SOXR if ((current_delay < conn->dac_buffer_queue_minimum_length) || - (config.packet_stuffing == ST_basic)) { + (config.packet_stuffing == ST_basic) || + (config.soxr_delay_index == 0) || // not computed yet + ((config.packet_stuffing == ST_auto) && (config.soxr_delay_index > config.soxr_delay_threshold)) // if the CPU is deemed too slow + ) { +#endif play_samples = stuff_buffer_basic_32((int32_t *)conn->tbuf, inbuflength, config.output_format, conn->outbuf, amount_to_stuff, conn->enable_dither, conn); - } #ifdef CONFIG_SOXR - else if (config.packet_stuffing == ST_soxr) { - // if (amount_to_stuff) debug(1,"Soxr stuff..."); + } + else { // soxr requested or auto requested with the index less or equal to the threshold play_samples = stuff_buffer_soxr_32((int32_t *)conn->tbuf, (int32_t *)conn->sbuf, inbuflength, config.output_format, conn->outbuf, amount_to_stuff, conn->enable_dither, conn); diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index a979431c..c340ff8f 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -15,8 +15,7 @@ general = // %V for the full version string, e.g. 3.3-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc // Overall length can not exceed 50 characters. Example: "Shairport Sync %v on %H". // password = "secret"; // leave this commented out if you don't want to require a password -// interpolation = "basic"; // aka "stuffing". Default is "basic", alternative is "soxr". Use "soxr" only if you have a reasonably fast processor. -// Note: if Shairport Sync is build with the --with-soxr configuration flag, interpolation will be "soxr" by default. +// interpolation = "auto"; // aka "stuffing". Default is "auto". Alternatives are "basic" or "soxr". Choose "soxr" only if you have a reasonably fast processor and Shairport Sync has been built with "soxr" support. // output_backend = "alsa"; // Run "shairport-sync -h" to get a list of all output_backends, e.g. "alsa", "pipe", "stdout". The default is the first one. // mdns_backend = "avahi"; // Run "shairport-sync -h" to get a list of all mdns_backends. The default is the first one. // port = 5000; // Listen for service requests on this port diff --git a/shairport.c b/shairport.c index 7e98bf27..da3d334d 100644 --- a/shairport.c +++ b/shairport.c @@ -41,6 +41,7 @@ #include #include + #include "config.h" #ifdef CONFIG_MBEDTLS @@ -95,6 +96,11 @@ #include #endif +#ifdef CONFIG_SOXR +#include +#include +#endif + #ifdef CONFIG_CONVOLUTION #include #endif @@ -158,6 +164,78 @@ void print_version(void) { } } +#ifdef CONFIG_SOXR +pthread_t soxr_time_check_thread; +void* soxr_time_check(__attribute__((unused)) void *arg) { + const int buffer_length = 352; + int32_t inbuffer[buffer_length*2]; + int32_t outbuffer[(buffer_length+1)*2]; + + //int32_t *outbuffer = (int32_t*)malloc((buffer_length+1)*2*sizeof(int32_t)); + //int32_t *inbuffer = (int32_t*)malloc((buffer_length)*2*sizeof(int32_t)); + + // generate a sample signal + const double frequency = 440; // + + int i; + + int number_of_iterations = 0; + uint64_t soxr_start_time = get_absolute_time_in_fp(); + uint64_t loop_until_time = (uint64_t)0x180000000 + soxr_start_time; // loop for a second and a half, max + while (get_absolute_time_in_fp() < loop_until_time) { + + number_of_iterations++; + for (i = 0; i < buffer_length ; i++) { + double w = sin(i * (frequency + number_of_iterations * 2) * 2 * M_PI/44100); + int32_t wint = (int32_t)(w * INT32_MAX); + inbuffer[i * 2] = wint; + inbuffer[i * 2 + 1] = wint; + } + + soxr_io_spec_t io_spec; + io_spec.itype = SOXR_INT32_I; + io_spec.otype = SOXR_INT32_I; + io_spec.scale = 1.0; // this seems to crash if not = 1.0 + io_spec.e = NULL; + io_spec.flags = 0; + + size_t odone; + + soxr_oneshot(buffer_length, buffer_length + 1, 2, // Rates and # of chans. + inbuffer, buffer_length, NULL, // Input. + outbuffer, buffer_length + 1, &odone, // Output. + &io_spec, // Input, output and transfer spec. + NULL, NULL); // Default configuration. + + io_spec.itype = SOXR_INT32_I; + io_spec.otype = SOXR_INT32_I; + io_spec.scale = 1.0; // this seems to crash if not = 1.0 + io_spec.e = NULL; + io_spec.flags = 0; + + soxr_oneshot(buffer_length, buffer_length - 1, 2, // Rates and # of chans. + inbuffer, buffer_length, NULL, // Input. + outbuffer, buffer_length - 1, &odone, // Output. + &io_spec, // Input, output and transfer spec. + NULL, NULL); // Default configuration. + + } + + double soxr_execution_time_us = + (((get_absolute_time_in_fp() - soxr_start_time) * 1000000) >> 32) * 1.0; + // free(outbuffer); + // free(inbuffer); + config.soxr_delay_index = (int)(0.9 + soxr_execution_time_us/(number_of_iterations *1000)); + debug(1,"soxr_delay_index: %d.", config.soxr_delay_index); + if ((config.packet_stuffing == ST_soxr) && (config.soxr_delay_index > config.soxr_delay_threshold)) + inform("Note: this device may be too slow for \"soxr\" interpolation. Consider choosing the \"basic\" or \"auto\" interpolation setting."); + if (config.packet_stuffing == ST_auto) + debug(1,"\"%s\" interpolation has been chosen.", config.soxr_delay_index <= config.soxr_delay_threshold ? "soxr" : "basic"); + pthread_exit(NULL); +} + +#endif + void usage(char *progname) { printf("Usage: %s [options...]\n", progname); printf(" or: %s [options...] -- [audio output-specific options]\n", progname); @@ -350,8 +428,8 @@ int parse_options(int argc, char **argv) { config.fixedLatencyOffset = 11025; // this sounds like it works properly. config.diagnostic_drop_packet_fraction = 0.0; config.active_state_timeout = 10.0; - config.volume_range_hw_priority = - 0; // if combining software and hardware volume control, give the software priority + config.soxr_delay_threshold = 30; // the soxr measurement time (milliseconds) of two oneshots must not exceed this if soxr interpolation is to be chosen automatically. + config.volume_range_hw_priority = 0; // if combining software and hardware volume control, give the software priority // i.e. when reducing volume, reduce the sw first before reducing the software. // this is because some hw mixers mute at the bottom of their range, and they don't always advertise this fact @@ -451,6 +529,8 @@ int parse_options(int argc, char **argv) { if (config_lookup_string(config.cfg, "general.interpolation", &str)) { if (strcasecmp(str, "basic") == 0) config.packet_stuffing = ST_basic; + else if (strcasecmp(str, "auto") == 0) + config.packet_stuffing = ST_auto; else if (strcasecmp(str, "soxr") == 0) #ifdef CONFIG_SOXR config.packet_stuffing = ST_soxr; @@ -460,8 +540,21 @@ int parse_options(int argc, char **argv) { "support. Change the \"general/interpolation\" setting in the configuration file."); #endif else - die("Invalid interpolation option choice. It should be \"basic\" or \"soxr\""); + die("Invalid interpolation option choice. It should be \"auto\", \"basic\" or \"soxr\""); } + +#ifdef CONFIG_SOXR + /* Get the soxr_delay_threshold setting. */ + if (config_lookup_int(config.cfg, "general.soxr_delay_threshold", &value)) { + if ((value >= 0) && (value <= 100)) + config.soxr_delay_threshold = value; + else + warn("Invalid general soxr_delay_threshold setting option choice \"%d\". It should be " + "between 0 and 100, " + "inclusive. Default is %d (milliseconds).", + value, config.soxr_delay_threshold); + } +#endif /* Get the statistics setting. */ if (config_set_lookup_bool(config.cfg, "general.statistics", @@ -998,6 +1091,8 @@ int parse_options(int argc, char **argv) { case 'S': if (strcmp(stuffing, "basic") == 0) config.packet_stuffing = ST_basic; + else if (strcmp(stuffing, "auto") == 0) + config.packet_stuffing = ST_auto; else if (strcmp(stuffing, "soxr") == 0) #ifdef CONFIG_SOXR config.packet_stuffing = ST_soxr; @@ -1312,11 +1407,15 @@ int main(int argc, char **argv) { 0.002; // this number of seconds of timing error before attempting to correct it. config.buffer_start_fill = 220; config.port = 5000; + + #ifdef CONFIG_SOXR - config.packet_stuffing = ST_soxr; // use soxr interpolation by default if support has been included + config.packet_stuffing = ST_auto; // use soxr interpolation by default if support has been included and if the CPU is fast enough #else config.packet_stuffing = ST_basic; // simple interpolation or deletion #endif + + // char hostname[100]; // gethostname(hostname, 100); // config.service_name = malloc(20 + 100); @@ -1506,6 +1605,19 @@ int main(int argc, char **argv) { // make sure the program can create files that group and world can read umask(S_IWGRP | S_IWOTH); + + /* print out version */ + + char *version_dbs = get_version_string(); + if (version_dbs) { + debug(1, "software version: \"%s\"", version_dbs); + free(version_dbs); + } else { + debug(1, "can't print the version information!"); + } + + debug(1, "log verbosity is %d.", debuglev); + config.output = audio_get_output(config.output_name); if (!config.output) { audio_ls_outputs(); @@ -1551,18 +1663,7 @@ int main(int argc, char **argv) { BUFFER_FRAMES * 352 - 22050); } - /* print out version */ - - char *version_dbs = get_version_string(); - if (version_dbs) { - debug(1, "software version: \"%s\"", version_dbs); - free(version_dbs); - } else { - debug(1, "can't print the version information!"); - } - /* Print out options */ - debug(1, "log verbosity is %d.", debuglev); debug(1, "disable resend requests is %s.", config.disable_resend_requests ? "on" : "off"); debug(1, "diagnostic_drop_packet_fraction is %f. A value of 0.0 means no packets will be dropped " "deliberately.", @@ -1587,7 +1688,8 @@ int main(int argc, char **argv) { debug(1, "active_state_timeout is %f seconds.", config.active_state_timeout); debug(1, "mdns backend \"%s\".", config.mdns_name); debug(2, "userSuppliedLatency is %d.", config.userSuppliedLatency); - debug(1, "stuffing option is \"%d\" (0-basic, 1-soxr).", config.packet_stuffing); + debug(1, "interpolation setting is \"%s\".", config.packet_stuffing == ST_basic ? "basic" : config.packet_stuffing == ST_soxr ? "soxr" : "auto"); + debug(1, "interpolation soxr_delay_threshold is %d.", config.soxr_delay_threshold); debug(1, "resync time is %f seconds.", config.resyncthreshold); debug(1, "allow a session to be interrupted: %d.", config.allow_session_interruption); debug(1, "busy timeout time is %d.", config.timeout); @@ -1654,6 +1756,10 @@ int main(int argc, char **argv) { uint8_t ap_md5[16]; +#ifdef CONFIG_SOXR + pthread_create(&soxr_time_check_thread, NULL, &soxr_time_check, NULL); +#endif + #ifdef CONFIG_OPENSSL MD5_CTX ctx; MD5_Init(&ctx);