Don't use strndup as old versions of OpenWrt don't seem to have it
Update FREEBSD.md
Fix comparison between old and new metadata strings by checking they have equal lengths as well.
Remove a redundant definition to prevent use on an uninitialised copy, clean up some of the less-used backends
Remove some unwanted and unused variable declarations from audio_dummy.c and audio_soundio.c
Add the -fno-common flag to compilation.
Treat the "mper" attribute as the 64-bit item that it is rather than a 32-bit item as hithereto. Output it as a hexadecimal number to correspond with the format of the track id obtained from AppleScript
Make changes to that it compiles under gcc-10 with -fno-common
Add a SIGCHLD handler to reap zombie processes generated after script invocations where wait_for_completion is set to "no".
Update and rename CONTRIBUTING.md to REPORTING ISSUES.md
Add a few more commands and clarify some text.
Change from talking about a server to talking about a client. Technically, e.g. iTunes is a client of Shairport Sync.
Store the UserAgent so as to recognise forked-daapd so as to always send a revision_number of 1 when asking for playerstatus of forked-daapd.
Add or update some of the copyright notices
Add the ability to set the volume directly to the D-Bus RemoteControl interface and to the MPRIS interface. Both use the recently-discovered ability to set the device_volume.
Merge in Pieter De Gendt's work on resampling on the jack backend.
Merge pull request #939 from pdgendt/feature/jack-soxr-resampling
Add SetAirplayVolume to the D-Bus RemoteControl interface.
Add SetVolume to the MPRIS interface.
Hook up the Volume property in the MPRIS interface.
Modify RemoteCommand in the D-Bus interface to return the HTTP status and response.
Change the type of airplay_volume from int to double in the metadata hub.
Add a few sample commands in the D-Bus document.
1889 lines
71 KiB
C
1889 lines
71 KiB
C
/*
|
|
* Shairport, an Apple Airplay receiver
|
|
* Copyright (c) James Laird 2013
|
|
* All rights reserved.
|
|
* Modifications and additions (c) Mike Brady 2014--2019
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or
|
|
* sell copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <getopt.h>
|
|
#include <libconfig.h>
|
|
#include <libgen.h>
|
|
#include <memory.h>
|
|
#include <net/if.h>
|
|
#include <popt.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include "config.h"
|
|
|
|
#ifdef CONFIG_MBEDTLS
|
|
#include <mbedtls/md5.h>
|
|
#include <mbedtls/version.h>
|
|
#endif
|
|
|
|
#ifdef CONFIG_POLARSSL
|
|
#include <polarssl/md5.h>
|
|
#endif
|
|
|
|
#ifdef CONFIG_OPENSSL
|
|
#include <openssl/md5.h>
|
|
#endif
|
|
|
|
#if defined(CONFIG_DBUS_INTERFACE)
|
|
#include <glib.h>
|
|
#endif
|
|
|
|
#include "activity_monitor.h"
|
|
#include "common.h"
|
|
#include "rtp.h"
|
|
#include "rtsp.h"
|
|
|
|
#if defined(CONFIG_DACP_CLIENT)
|
|
#include "dacp.h"
|
|
#endif
|
|
|
|
#if defined(CONFIG_METADATA_HUB)
|
|
#include "metadata_hub.h"
|
|
#endif
|
|
|
|
#ifdef CONFIG_DBUS_INTERFACE
|
|
#include "dbus-service.h"
|
|
#endif
|
|
|
|
#ifdef CONFIG_MQTT
|
|
#include "mqtt.h"
|
|
#endif
|
|
|
|
#ifdef CONFIG_MPRIS_INTERFACE
|
|
#include "mpris-service.h"
|
|
#endif
|
|
|
|
#ifdef CONFIG_LIBDAEMON
|
|
#include <libdaemon/dexec.h>
|
|
#include <libdaemon/dfork.h>
|
|
#include <libdaemon/dlog.h>
|
|
#include <libdaemon/dpid.h>
|
|
#include <libdaemon/dsignal.h>
|
|
#else
|
|
#include <syslog.h>
|
|
#endif
|
|
|
|
#ifdef CONFIG_SOXR
|
|
#include <math.h>
|
|
#include <soxr.h>
|
|
#endif
|
|
|
|
#ifdef CONFIG_CONVOLUTION
|
|
#include <FFTConvolver/convolver.h>
|
|
#endif
|
|
|
|
#ifdef CONFIG_LIBDAEMON
|
|
pid_t pid;
|
|
int this_is_the_daemon_process = 0;
|
|
#endif
|
|
|
|
int killOption = 0;
|
|
int daemonisewith = 0;
|
|
int daemonisewithout = 0;
|
|
|
|
// static int shutting_down = 0;
|
|
char configuration_file_path[4096 + 1];
|
|
char actual_configuration_file_path[4096 + 1];
|
|
|
|
void print_version(void) {
|
|
char *version_string = get_version_string();
|
|
if (version_string) {
|
|
printf("%s\n", version_string);
|
|
free(version_string);
|
|
} else {
|
|
debug(1, "Can't print version string!");
|
|
}
|
|
}
|
|
|
|
#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 -- no need to be able to cancel it, do _don't even try_!
|
|
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(2, "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);
|
|
printf("\n");
|
|
printf("Options:\n");
|
|
printf(" -h, --help show this help.\n");
|
|
#ifdef CONFIG_LIBDAEMON
|
|
printf(" -d, --daemon daemonise.\n");
|
|
printf(" -j, --justDaemoniseNoPIDFile daemonise without a PID file.\n");
|
|
printf(" -k, --kill kill the existing shairport daemon.\n");
|
|
#endif
|
|
printf(" -V, --version show version information.\n");
|
|
printf(" -c, --configfile=FILE read configuration settings from FILE. Default is "
|
|
"/etc/shairport-sync.conf.\n");
|
|
|
|
printf("\n");
|
|
printf("The following general options are for backward compatibility. These and all new options "
|
|
"have settings in the configuration file, by default /etc/shairport-sync.conf:\n");
|
|
printf(" -v, --verbose -v print debug information; -vv more; -vvv lots.\n");
|
|
printf(" -p, --port=PORT set RTSP listening port.\n");
|
|
printf(" -a, --name=NAME set advertised name.\n");
|
|
printf(" -L, --latency=FRAMES [Deprecated] Set the latency for audio sent from an unknown "
|
|
"device.\n");
|
|
printf(" The default is to set it automatically.\n");
|
|
printf(" -S, --stuffing=MODE set how to adjust current latency to match desired latency, "
|
|
"where \n");
|
|
printf(" \"basic\" inserts or deletes audio frames from "
|
|
"packet frames with low processor overhead, and \n");
|
|
printf(" \"soxr\" uses libsoxr to minimally resample packet frames -- "
|
|
"moderate processor overhead.\n");
|
|
printf(
|
|
" \"soxr\" option only available if built with soxr support.\n");
|
|
printf(" -B, --on-start=PROGRAM run PROGRAM when playback is about to begin.\n");
|
|
printf(" -E, --on-stop=PROGRAM run PROGRAM when playback has ended.\n");
|
|
printf(" For -B and -E options, specify the full path to the program, "
|
|
"e.g. /usr/bin/logger.\n");
|
|
printf(
|
|
" Executable scripts work, but must have the appropriate shebang "
|
|
"(#!/bin/sh) in the headline.\n");
|
|
printf(
|
|
" -w, --wait-cmd wait until the -B or -E programs finish before continuing.\n");
|
|
printf(" -o, --output=BACKEND select audio output method.\n");
|
|
printf(" -m, --mdns=BACKEND force the use of BACKEND to advertize the service.\n");
|
|
printf(" if no mdns provider is specified,\n");
|
|
printf(" shairport tries them all until one works.\n");
|
|
printf(" -r, --resync=THRESHOLD [Deprecated] resync if error exceeds this number of frames. "
|
|
"Set to 0 to "
|
|
"stop resyncing.\n");
|
|
printf(" -t, --timeout=SECONDS go back to idle mode from play mode after a break in "
|
|
"communications of this many seconds (default 120). Set to 0 never to exit play mode.\n");
|
|
printf(" --statistics print some interesting statistics -- output to the logfile "
|
|
"if running as a daemon.\n");
|
|
printf(" --tolerance=TOLERANCE [Deprecated] allow a synchronization error of TOLERANCE "
|
|
"frames (default "
|
|
"88) before trying to correct it.\n");
|
|
printf(" --password=PASSWORD require PASSWORD to connect. Default is not to require a "
|
|
"password.\n");
|
|
printf(" --logOutputLevel log the output level setting -- useful for setting maximum "
|
|
"volume.\n");
|
|
#ifdef CONFIG_METADATA
|
|
printf(" -M, --metadata-enable ask for metadata from the source and process it.\n");
|
|
printf(" --metadata-pipename=PIPE send metadata to PIPE, e.g. "
|
|
"--metadata-pipename=/tmp/shairport-sync-metadata.\n");
|
|
printf(" The default is /tmp/shairport-sync-metadata.\n");
|
|
printf(" --get-coverart send cover art through the metadata pipe.\n");
|
|
#endif
|
|
printf(" -u, --use-stderr log messages through STDERR rather than syslog.\n");
|
|
printf("\n");
|
|
mdns_ls_backends();
|
|
printf("\n");
|
|
audio_ls_outputs();
|
|
}
|
|
|
|
int parse_options(int argc, char **argv) {
|
|
// there are potential memory leaks here -- it's called a second time, previously allocated
|
|
// strings will dangle.
|
|
char *raw_service_name = NULL; /* Used to pick up the service name before possibly expanding it */
|
|
char *stuffing = NULL; /* used for picking up the stuffing option */
|
|
signed char c; /* used for argument parsing */
|
|
// int i = 0; /* used for tracking options */
|
|
int fResyncthreshold = (int)(config.resyncthreshold * 44100);
|
|
int fTolerance = (int)(config.tolerance * 44100);
|
|
poptContext optCon; /* context for parsing command-line options */
|
|
struct poptOption optionsTable[] = {
|
|
{"verbose", 'v', POPT_ARG_NONE, NULL, 'v', NULL, NULL},
|
|
{"kill", 'k', POPT_ARG_NONE, &killOption, 0, NULL, NULL},
|
|
{"daemon", 'd', POPT_ARG_NONE, &daemonisewith, 0, NULL, NULL},
|
|
{"justDaemoniseNoPIDFile", 'j', POPT_ARG_NONE, &daemonisewithout, 0, NULL, NULL},
|
|
{"configfile", 'c', POPT_ARG_STRING, &config.configfile, 0, NULL, NULL},
|
|
{"statistics", 0, POPT_ARG_NONE, &config.statistics_requested, 0, NULL, NULL},
|
|
{"logOutputLevel", 0, POPT_ARG_NONE, &config.logOutputLevel, 0, NULL, NULL},
|
|
{"version", 'V', POPT_ARG_NONE, NULL, 0, NULL, NULL},
|
|
{"port", 'p', POPT_ARG_INT, &config.port, 0, NULL, NULL},
|
|
{"name", 'a', POPT_ARG_STRING, &raw_service_name, 0, NULL, NULL},
|
|
{"output", 'o', POPT_ARG_STRING, &config.output_name, 0, NULL, NULL},
|
|
{"on-start", 'B', POPT_ARG_STRING, &config.cmd_start, 0, NULL, NULL},
|
|
{"on-stop", 'E', POPT_ARG_STRING, &config.cmd_stop, 0, NULL, NULL},
|
|
{"wait-cmd", 'w', POPT_ARG_NONE, &config.cmd_blocking, 0, NULL, NULL},
|
|
{"mdns", 'm', POPT_ARG_STRING, &config.mdns_name, 0, NULL, NULL},
|
|
{"latency", 'L', POPT_ARG_INT, &config.userSuppliedLatency, 0, NULL, NULL},
|
|
{"stuffing", 'S', POPT_ARG_STRING, &stuffing, 'S', NULL, NULL},
|
|
{"resync", 'r', POPT_ARG_INT, &fResyncthreshold, 0, NULL, NULL},
|
|
{"timeout", 't', POPT_ARG_INT, &config.timeout, 't', NULL, NULL},
|
|
{"password", 0, POPT_ARG_STRING, &config.password, 0, NULL, NULL},
|
|
{"tolerance", 'z', POPT_ARG_INT, &fTolerance, 0, NULL, NULL},
|
|
{"use-stderr", 'u', POPT_ARG_NONE, NULL, 'u', NULL, NULL},
|
|
#ifdef CONFIG_METADATA
|
|
{"metadata-enable", 'M', POPT_ARG_NONE, &config.metadata_enabled, 'M', NULL, NULL},
|
|
{"metadata-pipename", 0, POPT_ARG_STRING, &config.metadata_pipename, 0, NULL, NULL},
|
|
{"get-coverart", 'g', POPT_ARG_NONE, &config.get_coverart, 'g', NULL, NULL},
|
|
#endif
|
|
POPT_AUTOHELP{NULL, 0, 0, NULL, 0, NULL, NULL}};
|
|
|
|
// we have to parse the command line arguments to look for a config file
|
|
int optind;
|
|
optind = argc;
|
|
int j;
|
|
for (j = 0; j < argc; j++)
|
|
if (strcmp(argv[j], "--") == 0)
|
|
optind = j;
|
|
|
|
optCon = poptGetContext(NULL, optind, (const char **)argv, optionsTable, 0);
|
|
if (optCon == NULL)
|
|
die("Can not get a secondary popt context.");
|
|
poptSetOtherOptionHelp(optCon, "[OPTIONS]* ");
|
|
|
|
/* Now do options processing just to get a debug level */
|
|
debuglev = 0;
|
|
while ((c = poptGetNextOpt(optCon)) >= 0) {
|
|
switch (c) {
|
|
case 'v':
|
|
debuglev++;
|
|
break;
|
|
case 'u':
|
|
log_to_stderr();
|
|
break;
|
|
case 'D':
|
|
inform("Warning: the option -D or --disconnectFromOutput is deprecated.");
|
|
break;
|
|
case 'R':
|
|
inform("Warning: the option -R or --reconnectToOutput is deprecated.");
|
|
break;
|
|
case 'A':
|
|
inform("Warning: the option -A or --AirPlayLatency is deprecated and ignored. This setting "
|
|
"is now "
|
|
"automatically received from the AirPlay device.");
|
|
break;
|
|
case 'i':
|
|
inform("Warning: the option -i or --iTunesLatency is deprecated and ignored. This setting is "
|
|
"now "
|
|
"automatically received from iTunes");
|
|
break;
|
|
case 'f':
|
|
inform(
|
|
"Warning: the option --forkedDaapdLatency is deprecated and ignored. This setting is now "
|
|
"automatically received from forkedDaapd");
|
|
break;
|
|
case 'r':
|
|
inform("Warning: the option -r or --resync is deprecated. Please use the "
|
|
"\"resync_threshold_in_seconds\" setting in the config file instead.");
|
|
break;
|
|
case 'z':
|
|
inform("Warning: the option --tolerance is deprecated. Please use the "
|
|
"\"drift_tolerance_in_seconds\" setting in the config file instead.");
|
|
break;
|
|
}
|
|
}
|
|
if (c < -1) {
|
|
die("%s: %s", poptBadOption(optCon, POPT_BADOPTION_NOALIAS), poptStrerror(c));
|
|
}
|
|
|
|
poptFreeContext(optCon);
|
|
|
|
#ifdef CONFIG_LIBDAEMON
|
|
if ((daemonisewith) && (daemonisewithout))
|
|
die("Select either daemonize_with_pid_file or daemonize_without_pid_file -- you have selected "
|
|
"both!");
|
|
if ((daemonisewith) || (daemonisewithout)) {
|
|
config.daemonise = 1;
|
|
if (daemonisewith)
|
|
config.daemonise_store_pid = 1;
|
|
};
|
|
#endif
|
|
|
|
config.resyncthreshold = 1.0 * fResyncthreshold / 44100;
|
|
config.tolerance = 1.0 * fTolerance / 44100;
|
|
config.audio_backend_silent_lead_in_time = -1.0; // flag to indicate it has not been set
|
|
config.airplay_volume = -18.0; // if no volume is ever set, default to initial default value if
|
|
// nothing else comes in first.
|
|
config.fixedLatencyOffset = 11025; // this sounds like it works properly.
|
|
config.diagnostic_drop_packet_fraction = 0.0;
|
|
config.active_state_timeout = 10.0;
|
|
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
|
|
config.resend_control_first_check_time = 0.10; // wait this many seconds before requesting the resending of a missing packet
|
|
config.resend_control_check_interval_time = 0.25; // wait this many seconds before again requesting the resending of a missing packet
|
|
config.resend_control_last_check_time = 0.10; // give up if the packet is still missing this close to when it's needed
|
|
|
|
#ifdef CONFIG_METADATA_HUB
|
|
config.cover_art_cache_dir = "/tmp/shairport-sync/.cache/coverart";
|
|
config.scan_interval_when_active =
|
|
1; // number of seconds between DACP server scans when playing something
|
|
config.scan_interval_when_inactive =
|
|
1; // number of seconds between DACP server scans when playing nothing
|
|
config.scan_max_bad_response_count =
|
|
5; // number of successive bad results to ignore before giving up
|
|
//config.scan_max_inactive_count =
|
|
// (365 * 24 * 60 * 60) / config.scan_interval_when_inactive; // number of scans to do before stopping if
|
|
// not made active again (not used)
|
|
#endif
|
|
|
|
// config_setting_t *setting;
|
|
const char *str = 0;
|
|
int value = 0;
|
|
double dvalue = 0.0;
|
|
|
|
// debug(1, "Looking for the configuration file \"%s\".", config.configfile);
|
|
|
|
config_init(&config_file_stuff);
|
|
|
|
char *config_file_real_path = realpath(config.configfile, NULL);
|
|
if (config_file_real_path == NULL) {
|
|
debug(2, "Can't resolve the configuration file \"%s\".", config.configfile);
|
|
} else {
|
|
debug(2, "Looking for configuration file at full path \"%s\"", config_file_real_path);
|
|
/* Read the file. If there is an error, report it and exit. */
|
|
if (config_read_file(&config_file_stuff, config_file_real_path)) {
|
|
free(config_file_real_path);
|
|
config_set_auto_convert(&config_file_stuff,
|
|
1); // allow autoconversion from int/float to int/float
|
|
// make config.cfg point to it
|
|
config.cfg = &config_file_stuff;
|
|
/* Get the Service Name. */
|
|
if (config_lookup_string(config.cfg, "general.name", &str)) {
|
|
raw_service_name = (char *)str;
|
|
}
|
|
#ifdef CONFIG_LIBDAEMON
|
|
/* Get the Daemonize setting. */
|
|
config_set_lookup_bool(config.cfg, "sessioncontrol.daemonize_with_pid_file", &daemonisewith);
|
|
|
|
/* Get the Just_Daemonize setting. */
|
|
config_set_lookup_bool(config.cfg, "sessioncontrol.daemonize_without_pid_file",
|
|
&daemonisewithout);
|
|
|
|
/* Get the directory path for the pid file created when the program is daemonised. */
|
|
if (config_lookup_string(config.cfg, "sessioncontrol.daemon_pid_dir", &str))
|
|
config.piddir = (char *)str;
|
|
#endif
|
|
|
|
/* Get the mdns_backend setting. */
|
|
if (config_lookup_string(config.cfg, "general.mdns_backend", &str))
|
|
config.mdns_name = (char *)str;
|
|
|
|
/* Get the output_backend setting. */
|
|
if (config_lookup_string(config.cfg, "general.output_backend", &str))
|
|
config.output_name = (char *)str;
|
|
|
|
/* Get the port setting. */
|
|
if (config_lookup_int(config.cfg, "general.port", &value)) {
|
|
if ((value < 0) || (value > 65535))
|
|
die("Invalid port number \"%sd\". It should be between 0 and 65535, default is 5000",
|
|
value);
|
|
else
|
|
config.port = value;
|
|
}
|
|
|
|
/* Get the udp port base setting. */
|
|
if (config_lookup_int(config.cfg, "general.udp_port_base", &value)) {
|
|
if ((value < 0) || (value > 65535))
|
|
die("Invalid port number \"%sd\". It should be between 0 and 65535, default is 6001",
|
|
value);
|
|
else
|
|
config.udp_port_base = value;
|
|
}
|
|
|
|
/* Get the udp port range setting. This is number of ports that will be tried for free ports ,
|
|
* starting at the port base. Only three ports are needed. */
|
|
if (config_lookup_int(config.cfg, "general.udp_port_range", &value)) {
|
|
if ((value < 3) || (value > 65535))
|
|
die("Invalid port range \"%sd\". It should be between 3 and 65535, default is 10",
|
|
value);
|
|
else
|
|
config.udp_port_range = value;
|
|
}
|
|
|
|
/* Get the password setting. */
|
|
if (config_lookup_string(config.cfg, "general.password", &str))
|
|
config.password = (char *)str;
|
|
|
|
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;
|
|
#else
|
|
warn("The soxr option not available because this version of shairport-sync was built "
|
|
"without libsoxr "
|
|
"support. Change the \"general/interpolation\" setting in the configuration file.");
|
|
#endif
|
|
else
|
|
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",
|
|
&(config.statistics_requested))) {
|
|
warn("The \"general\" \"statistics\" setting is deprecated. Please use the \"diagnostics\" "
|
|
"\"statistics\" setting instead.");
|
|
}
|
|
|
|
/* The old drift tolerance setting. */
|
|
if (config_lookup_int(config.cfg, "general.drift", &value)) {
|
|
inform("The drift setting is deprecated. Use "
|
|
"drift_tolerance_in_seconds instead");
|
|
config.tolerance = 1.0 * value / 44100;
|
|
}
|
|
|
|
/* The old resync setting. */
|
|
if (config_lookup_int(config.cfg, "general.resync_threshold", &value)) {
|
|
inform("The resync_threshold setting is deprecated. Use "
|
|
"resync_threshold_in_seconds instead");
|
|
config.resyncthreshold = 1.0 * value / 44100;
|
|
}
|
|
|
|
/* Get the drift tolerance setting. */
|
|
if (config_lookup_float(config.cfg, "general.drift_tolerance_in_seconds", &dvalue))
|
|
config.tolerance = dvalue;
|
|
|
|
/* Get the resync setting. */
|
|
if (config_lookup_float(config.cfg, "general.resync_threshold_in_seconds", &dvalue))
|
|
config.resyncthreshold = dvalue;
|
|
|
|
/* Get the verbosity setting. */
|
|
if (config_lookup_int(config.cfg, "general.log_verbosity", &value)) {
|
|
warn("The \"general\" \"log_verbosity\" setting is deprecated. Please use the "
|
|
"\"diagnostics\" \"log_verbosity\" setting instead.");
|
|
if ((value >= 0) && (value <= 3))
|
|
debuglev = value;
|
|
else
|
|
die("Invalid log verbosity setting option choice \"%d\". It should be between 0 and 3, "
|
|
"inclusive.",
|
|
value);
|
|
}
|
|
|
|
/* Get the verbosity setting. */
|
|
if (config_lookup_int(config.cfg, "diagnostics.log_verbosity", &value)) {
|
|
if ((value >= 0) && (value <= 3))
|
|
debuglev = value;
|
|
else
|
|
die("Invalid diagnostics log_verbosity setting option choice \"%d\". It should be "
|
|
"between 0 and 3, "
|
|
"inclusive.",
|
|
value);
|
|
}
|
|
|
|
/* Get the config.debugger_show_file_and_line in debug messages setting. */
|
|
if (config_lookup_string(config.cfg, "diagnostics.log_show_file_and_line", &str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.debugger_show_file_and_line = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.debugger_show_file_and_line = 1;
|
|
else
|
|
die("Invalid diagnostics log_show_file_and_line option choice \"%s\". It should be "
|
|
"\"yes\" or \"no\"");
|
|
}
|
|
|
|
/* Get the show elapsed time in debug messages setting. */
|
|
if (config_lookup_string(config.cfg, "diagnostics.log_show_time_since_startup", &str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.debugger_show_elapsed_time = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.debugger_show_elapsed_time = 1;
|
|
else
|
|
die("Invalid diagnostics log_show_time_since_startup option choice \"%s\". It should be "
|
|
"\"yes\" or \"no\"");
|
|
}
|
|
|
|
/* Get the show relative time in debug messages setting. */
|
|
if (config_lookup_string(config.cfg, "diagnostics.log_show_time_since_last_message", &str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.debugger_show_relative_time = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.debugger_show_relative_time = 1;
|
|
else
|
|
die("Invalid diagnostics log_show_time_since_last_message option choice \"%s\". It "
|
|
"should be \"yes\" or \"no\"");
|
|
}
|
|
|
|
/* Get the statistics setting. */
|
|
if (config_lookup_string(config.cfg, "diagnostics.statistics", &str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.statistics_requested = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.statistics_requested = 1;
|
|
else
|
|
die("Invalid diagnostics statistics option choice \"%s\". It should be \"yes\" or "
|
|
"\"no\"");
|
|
}
|
|
|
|
/* Get the disable_resend_requests setting. */
|
|
if (config_lookup_string(config.cfg, "diagnostics.disable_resend_requests", &str)) {
|
|
config.disable_resend_requests = 0; // this is for legacy -- only set by -t 0
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.disable_resend_requests = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.disable_resend_requests = 1;
|
|
else
|
|
die("Invalid diagnostic disable_resend_requests option choice \"%s\". It should be "
|
|
"\"yes\" "
|
|
"or \"no\"");
|
|
}
|
|
|
|
/* Get the drop packets setting. */
|
|
if (config_lookup_float(config.cfg, "diagnostics.drop_this_fraction_of_audio_packets",
|
|
&dvalue)) {
|
|
if ((dvalue >= 0.0) && (dvalue <= 3.0))
|
|
config.diagnostic_drop_packet_fraction = dvalue;
|
|
else
|
|
die("Invalid diagnostics drop_this_fraction_of_audio_packets setting \"%d\". It should "
|
|
"be "
|
|
"between 0.0 and 1.0, "
|
|
"inclusive.",
|
|
dvalue);
|
|
}
|
|
|
|
/* Get the ignore_volume_control setting. */
|
|
if (config_lookup_string(config.cfg, "general.ignore_volume_control", &str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.ignore_volume_control = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.ignore_volume_control = 1;
|
|
else
|
|
die("Invalid ignore_volume_control option choice \"%s\". It should be \"yes\" or \"no\"");
|
|
}
|
|
|
|
/* Get the optional volume_max_db setting. */
|
|
if (config_lookup_float(config.cfg, "general.volume_max_db", &dvalue)) {
|
|
// debug(1, "Max volume setting of %f dB", dvalue);
|
|
config.volume_max_db = dvalue;
|
|
config.volume_max_db_set = 1;
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "general.run_this_when_volume_is_set", &str)) {
|
|
config.cmd_set_volume = (char *)str;
|
|
}
|
|
|
|
/* Get the playback_mode setting */
|
|
if (config_lookup_string(config.cfg, "general.playback_mode", &str)) {
|
|
if (strcasecmp(str, "stereo") == 0)
|
|
config.playback_mode = ST_stereo;
|
|
else if (strcasecmp(str, "mono") == 0)
|
|
config.playback_mode = ST_mono;
|
|
else if (strcasecmp(str, "reverse stereo") == 0)
|
|
config.playback_mode = ST_reverse_stereo;
|
|
else if (strcasecmp(str, "both left") == 0)
|
|
config.playback_mode = ST_left_only;
|
|
else if (strcasecmp(str, "both right") == 0)
|
|
config.playback_mode = ST_right_only;
|
|
else
|
|
die("Invalid playback_mode choice \"%s\". It should be \"stereo\" (default), \"mono\", "
|
|
"\"reverse stereo\", \"both left\", \"both right\"");
|
|
}
|
|
|
|
/* Get the volume control profile setting -- "standard" or "flat" */
|
|
if (config_lookup_string(config.cfg, "general.volume_control_profile", &str)) {
|
|
if (strcasecmp(str, "standard") == 0)
|
|
config.volume_control_profile = VCP_standard;
|
|
else if (strcasecmp(str, "flat") == 0)
|
|
config.volume_control_profile = VCP_flat;
|
|
else
|
|
die("Invalid volume_control_profile choice \"%s\". It should be \"standard\" (default) "
|
|
"or \"flat\"");
|
|
}
|
|
|
|
config_set_lookup_bool(config.cfg, "general.volume_control_combined_hardware_priority",
|
|
&config.volume_range_hw_priority);
|
|
|
|
/* Get the interface to listen on, if specified Default is all interfaces */
|
|
/* we keep the interface name and the index */
|
|
|
|
if (config_lookup_string(config.cfg, "general.interface", &str))
|
|
config.interface = strdup(str);
|
|
|
|
if (config_lookup_string(config.cfg, "general.interface", &str)) {
|
|
int specified_interface_found = 0;
|
|
|
|
struct if_nameindex *if_ni, *i;
|
|
|
|
if_ni = if_nameindex();
|
|
if (if_ni == NULL) {
|
|
debug(1, "Can't get a list of interface names.");
|
|
} else {
|
|
for (i = if_ni; !(i->if_index == 0 && i->if_name == NULL); i++) {
|
|
// printf("%u: %s\n", i->if_index, i->if_name);
|
|
if (strcmp(i->if_name, str) == 0) {
|
|
config.interface_index = i->if_index;
|
|
specified_interface_found = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if_freenameindex(if_ni);
|
|
|
|
if (specified_interface_found == 0) {
|
|
inform(
|
|
"The mdns service interface \"%s\" was not found, so the setting has been ignored.",
|
|
config.interface);
|
|
free(config.interface);
|
|
config.interface = NULL;
|
|
config.interface_index = 0;
|
|
}
|
|
}
|
|
|
|
/* Get the regtype -- the service type and protocol, separated by a dot. Default is
|
|
* "_raop._tcp" */
|
|
if (config_lookup_string(config.cfg, "general.regtype", &str))
|
|
config.regtype = strdup(str);
|
|
|
|
/* Get the volume range, in dB, that should be used If not set, it means you just use the
|
|
* range set by the mixer. */
|
|
if (config_lookup_int(config.cfg, "general.volume_range_db", &value)) {
|
|
if ((value < 30) || (value > 150))
|
|
die("Invalid volume range \"%sd\". It should be between 30 and 150 dB. Zero means use "
|
|
"the mixer's native range",
|
|
value);
|
|
else
|
|
config.volume_range_db = value;
|
|
}
|
|
|
|
/* Get the alac_decoder setting. */
|
|
if (config_lookup_string(config.cfg, "general.alac_decoder", &str)) {
|
|
if (strcasecmp(str, "hammerton") == 0)
|
|
config.use_apple_decoder = 0;
|
|
else if (strcasecmp(str, "apple") == 0) {
|
|
if ((config.decoders_supported & 1 << decoder_apple_alac) != 0)
|
|
config.use_apple_decoder = 1;
|
|
else
|
|
inform("Support for the Apple ALAC decoder has not been compiled into this version of "
|
|
"Shairport Sync. The default decoder will be used.");
|
|
} else
|
|
die("Invalid alac_decoder option choice \"%s\". It should be \"hammerton\" or \"apple\"");
|
|
}
|
|
|
|
|
|
/* Get the resend control settings. */
|
|
if (config_lookup_float(config.cfg, "general.resend_control_first_check_time",
|
|
&dvalue)) {
|
|
if ((dvalue >= 0.0) && (dvalue <= 3.0))
|
|
config.resend_control_first_check_time = dvalue;
|
|
else
|
|
warn("Invalid general resend_control_first_check_time setting \"%d\". It should "
|
|
"be "
|
|
"between 0.0 and 3.0, "
|
|
"inclusive. The setting remains at %f seconds.",
|
|
dvalue, config.resend_control_first_check_time);
|
|
}
|
|
|
|
if (config_lookup_float(config.cfg, "general.resend_control_check_interval_time",
|
|
&dvalue)) {
|
|
if ((dvalue >= 0.0) && (dvalue <= 3.0))
|
|
config.resend_control_check_interval_time = dvalue;
|
|
else
|
|
warn("Invalid general resend_control_check_interval_time setting \"%d\". It should "
|
|
"be "
|
|
"between 0.0 and 3.0, "
|
|
"inclusive. The setting remains at %f seconds.",
|
|
dvalue, config.resend_control_check_interval_time);
|
|
}
|
|
|
|
if (config_lookup_float(config.cfg, "general.resend_control_last_check_time",
|
|
&dvalue)) {
|
|
if ((dvalue >= 0.0) && (dvalue <= 3.0))
|
|
config.resend_control_last_check_time = dvalue;
|
|
else
|
|
warn("Invalid general resend_control_last_check_time setting \"%d\". It should "
|
|
"be "
|
|
"between 0.0 and 3.0, "
|
|
"inclusive. The setting remains at %f seconds.",
|
|
dvalue, config.resend_control_last_check_time);
|
|
}
|
|
|
|
/* Get the default latency. Deprecated! */
|
|
if (config_lookup_int(config.cfg, "latencies.default", &value))
|
|
config.userSuppliedLatency = value;
|
|
|
|
#ifdef CONFIG_METADATA
|
|
/* Get the metadata setting. */
|
|
config.metadata_enabled = 1; // if metadata support is included, then enable it by default
|
|
if (config_lookup_string(config.cfg, "metadata.enabled", &str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.metadata_enabled = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.metadata_enabled = 1;
|
|
else
|
|
die("Invalid metadata enabled option choice \"%s\". It should be \"yes\" or \"no\"");
|
|
}
|
|
|
|
config.get_coverart = 1; // if metadata support is included, then enable it by default
|
|
if (config_lookup_string(config.cfg, "metadata.include_cover_art", &str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.get_coverart = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.get_coverart = 1;
|
|
else
|
|
die("Invalid metadata include_cover_art option choice \"%s\". It should be \"yes\" or "
|
|
"\"no\"");
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "metadata.pipe_name", &str)) {
|
|
config.metadata_pipename = (char *)str;
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "metadata.socket_address", &str)) {
|
|
config.metadata_sockaddr = (char *)str;
|
|
}
|
|
if (config_lookup_int(config.cfg, "metadata.socket_port", &value)) {
|
|
config.metadata_sockport = value;
|
|
}
|
|
config.metadata_sockmsglength = 500;
|
|
if (config_lookup_int(config.cfg, "metadata.socket_msglength", &value)) {
|
|
config.metadata_sockmsglength = value < 500 ? 500 : value > 65000 ? 65000 : value;
|
|
}
|
|
|
|
#endif
|
|
|
|
#ifdef CONFIG_METADATA_HUB
|
|
if (config_lookup_string(config.cfg, "metadata.cover_art_cache_directory", &str)) {
|
|
config.cover_art_cache_dir = (char *)str;
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "diagnostics.retain_cover_art", &str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.retain_coverart = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.retain_coverart = 1;
|
|
else
|
|
die("Invalid metadata retain_cover_art option choice \"%s\". It should be \"yes\" or "
|
|
"\"no\"");
|
|
}
|
|
#endif
|
|
|
|
if (config_lookup_string(config.cfg, "sessioncontrol.run_this_before_play_begins", &str)) {
|
|
config.cmd_start = (char *)str;
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "sessioncontrol.run_this_after_play_ends", &str)) {
|
|
config.cmd_stop = (char *)str;
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "sessioncontrol.run_this_before_entering_active_state",
|
|
&str)) {
|
|
config.cmd_active_start = (char *)str;
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "sessioncontrol.run_this_after_exiting_active_state",
|
|
&str)) {
|
|
config.cmd_active_stop = (char *)str;
|
|
}
|
|
|
|
if (config_lookup_float(config.cfg, "sessioncontrol.active_state_timeout", &dvalue)) {
|
|
if (dvalue < 0.0)
|
|
warn("Invalid value \"%f\" for sessioncontrol.active_state_timeout. It must be positive. "
|
|
"The default of %f will be used instead.",
|
|
dvalue, config.active_state_timeout);
|
|
else
|
|
config.active_state_timeout = dvalue;
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg,
|
|
"sessioncontrol.run_this_if_an_unfixable_error_is_detected", &str)) {
|
|
config.cmd_unfixable = (char *)str;
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "sessioncontrol.wait_for_completion", &str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.cmd_blocking = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.cmd_blocking = 1;
|
|
else
|
|
die("Invalid session control wait_for_completion option choice \"%s\". It should be "
|
|
"\"yes\" or \"no\"");
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "sessioncontrol.before_play_begins_returns_output",
|
|
&str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.cmd_start_returns_output = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.cmd_start_returns_output = 1;
|
|
else
|
|
die("Invalid session control before_play_begins_returns_output option choice \"%s\". It "
|
|
"should be "
|
|
"\"yes\" or \"no\"");
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "sessioncontrol.allow_session_interruption", &str)) {
|
|
config.dont_check_timeout = 0; // this is for legacy -- only set by -t 0
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.allow_session_interruption = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.allow_session_interruption = 1;
|
|
else
|
|
die("Invalid session control allow_interruption option choice \"%s\". It should be "
|
|
"\"yes\" "
|
|
"or \"no\"");
|
|
}
|
|
|
|
if (config_lookup_int(config.cfg, "sessioncontrol.session_timeout", &value)) {
|
|
config.timeout = value;
|
|
config.dont_check_timeout = 0; // this is for legacy -- only set by -t 0
|
|
}
|
|
|
|
#ifdef CONFIG_CONVOLUTION
|
|
if (config_lookup_string(config.cfg, "dsp.convolution", &str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.convolution = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.convolution = 1;
|
|
else
|
|
die("Invalid dsp.convolution. It should be \"yes\" or \"no\"");
|
|
}
|
|
|
|
if (config_lookup_float(config.cfg, "dsp.convolution_gain", &dvalue)) {
|
|
config.convolution_gain = dvalue;
|
|
if (dvalue > 10 || dvalue < -50)
|
|
die("Invalid value \"%f\" for dsp.convolution_gain. It should be between -50 and +10 dB",
|
|
dvalue);
|
|
}
|
|
|
|
config.convolution_max_length = 8192;
|
|
if (config_lookup_int(config.cfg, "dsp.convolution_max_length", &value)) {
|
|
config.convolution_max_length = value;
|
|
|
|
if (value < 1 || value > 200000)
|
|
die("dsp.convolution_max_length must be within 1 and 200000");
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "dsp.convolution_ir_file", &str)) {
|
|
config.convolution_ir_file = strdup(str);
|
|
config.convolver_valid = convolver_init(config.convolution_ir_file, config.convolution_max_length);
|
|
}
|
|
|
|
if (config.convolution && config.convolution_ir_file == NULL) {
|
|
warn("Convolution enabled but no convolution_ir_file provided");
|
|
}
|
|
#endif
|
|
if (config_lookup_string(config.cfg, "dsp.loudness", &str)) {
|
|
if (strcasecmp(str, "no") == 0)
|
|
config.loudness = 0;
|
|
else if (strcasecmp(str, "yes") == 0)
|
|
config.loudness = 1;
|
|
else
|
|
die("Invalid dsp.convolution. It should be \"yes\" or \"no\"");
|
|
}
|
|
|
|
config.loudness_reference_volume_db = -20;
|
|
if (config_lookup_float(config.cfg, "dsp.loudness_reference_volume_db", &dvalue)) {
|
|
config.loudness_reference_volume_db = dvalue;
|
|
if (dvalue > 0 || dvalue < -100)
|
|
die("Invalid value \"%f\" for dsp.loudness_reference_volume_db. It should be between "
|
|
"-100 and 0",
|
|
dvalue);
|
|
}
|
|
|
|
if (config.loudness == 1 && config_lookup_string(config.cfg, "alsa.mixer_control_name", &str))
|
|
die("Loudness activated but hardware volume is active. You must remove "
|
|
"\"alsa.mixer_control_name\" to use the loudness filter.");
|
|
|
|
} else {
|
|
if (config_error_type(&config_file_stuff) == CONFIG_ERR_FILE_IO)
|
|
debug(2, "Error reading configuration file \"%s\": \"%s\".",
|
|
config_error_file(&config_file_stuff), config_error_text(&config_file_stuff));
|
|
else {
|
|
die("Line %d of the configuration file \"%s\":\n%s", config_error_line(&config_file_stuff),
|
|
config_error_file(&config_file_stuff), config_error_text(&config_file_stuff));
|
|
}
|
|
}
|
|
#if defined(CONFIG_DBUS_INTERFACE)
|
|
/* Get the dbus service sbus setting. */
|
|
if (config_lookup_string(config.cfg, "general.dbus_service_bus", &str)) {
|
|
if (strcasecmp(str, "system") == 0)
|
|
config.dbus_service_bus_type = DBT_system;
|
|
else if (strcasecmp(str, "session") == 0)
|
|
config.dbus_service_bus_type = DBT_session;
|
|
else
|
|
die("Invalid dbus_service_bus option choice \"%s\". It should be \"system\" (default) or "
|
|
"\"session\"");
|
|
}
|
|
#endif
|
|
|
|
#if defined(CONFIG_MPRIS_INTERFACE)
|
|
/* Get the mpris service sbus setting. */
|
|
if (config_lookup_string(config.cfg, "general.mpris_service_bus", &str)) {
|
|
if (strcasecmp(str, "system") == 0)
|
|
config.mpris_service_bus_type = DBT_system;
|
|
else if (strcasecmp(str, "session") == 0)
|
|
config.mpris_service_bus_type = DBT_session;
|
|
else
|
|
die("Invalid mpris_service_bus option choice \"%s\". It should be \"system\" (default) or "
|
|
"\"session\"");
|
|
}
|
|
#endif
|
|
|
|
#ifdef CONFIG_MQTT
|
|
config_set_lookup_bool(config.cfg, "mqtt.enabled", &config.mqtt_enabled);
|
|
if (config.mqtt_enabled && !config.metadata_enabled) {
|
|
die("You need to have metadata enabled in order to use mqtt");
|
|
}
|
|
if (config_lookup_string(config.cfg, "mqtt.hostname", &str)) {
|
|
config.mqtt_hostname = (char *)str;
|
|
// TODO: Document that, if this is false, whole mqtt func is disabled
|
|
}
|
|
config.mqtt_port = 1883;
|
|
if (config_lookup_int(config.cfg, "mqtt.port", &value)) {
|
|
if ((value < 0) || (value > 65535))
|
|
die("Invalid mqtt port number \"%sd\". It should be between 0 and 65535, default is 1883",
|
|
value);
|
|
else
|
|
config.mqtt_port = value;
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "mqtt.username", &str)) {
|
|
config.mqtt_username = (char *)str;
|
|
}
|
|
if (config_lookup_string(config.cfg, "mqtt.password", &str)) {
|
|
config.mqtt_password = (char *)str;
|
|
}
|
|
int capath = 0;
|
|
if (config_lookup_string(config.cfg, "mqtt.capath", &str)) {
|
|
config.mqtt_capath = (char *)str;
|
|
capath = 1;
|
|
}
|
|
if (config_lookup_string(config.cfg, "mqtt.cafile", &str)) {
|
|
if (capath)
|
|
die("Supply either mqtt cafile or mqtt capath -- you have supplied both!");
|
|
config.mqtt_cafile = (char *)str;
|
|
}
|
|
int certkeynum = 0;
|
|
if (config_lookup_string(config.cfg, "mqtt.certfile", &str)) {
|
|
config.mqtt_certfile = (char *)str;
|
|
certkeynum++;
|
|
}
|
|
if (config_lookup_string(config.cfg, "mqtt.keyfile", &str)) {
|
|
config.mqtt_keyfile = (char *)str;
|
|
certkeynum++;
|
|
}
|
|
if (certkeynum != 0 && certkeynum != 2) {
|
|
die("If you want to use TLS Client Authentication, you have to specify "
|
|
"mqtt.certfile AND mqtt.keyfile.\nYou have supplied only one of them.\n"
|
|
"If you do not want to use TLS Client Authentication, leave both empty.");
|
|
}
|
|
|
|
if (config_lookup_string(config.cfg, "mqtt.topic", &str)) {
|
|
config.mqtt_topic = (char *)str;
|
|
}
|
|
config_set_lookup_bool(config.cfg, "mqtt.publish_raw", &config.mqtt_publish_raw);
|
|
config_set_lookup_bool(config.cfg, "mqtt.publish_parsed", &config.mqtt_publish_parsed);
|
|
config_set_lookup_bool(config.cfg, "mqtt.publish_cover", &config.mqtt_publish_cover);
|
|
if (config.mqtt_publish_cover && !config.get_coverart) {
|
|
die("You need to have metadata.include_cover_art enabled in order to use mqtt.publish_cover");
|
|
}
|
|
config_set_lookup_bool(config.cfg, "mqtt.enable_remote", &config.mqtt_enable_remote);
|
|
#ifndef CONFIG_AVAHI
|
|
if (config.mqtt_enable_remote) {
|
|
die("You have enabled MQTT remote control which requires shairport-sync to be built with "
|
|
"Avahi, but your installation is not using avahi. Please reinstall/recompile with "
|
|
"avahi enabled, or disable remote control.");
|
|
}
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
// now, do the command line options again, but this time do them fully -- it's a unix convention
|
|
// that command line
|
|
// arguments have precedence over configuration file settings.
|
|
|
|
optind = argc;
|
|
for (j = 0; j < argc; j++)
|
|
if (strcmp(argv[j], "--") == 0)
|
|
optind = j;
|
|
|
|
optCon = poptGetContext(NULL, optind, (const char **)argv, optionsTable, 0);
|
|
if (optCon == NULL)
|
|
die("Can not get a popt context.");
|
|
poptSetOtherOptionHelp(optCon, "[OPTIONS]* ");
|
|
|
|
/* Now do options processing, get portname */
|
|
int tdebuglev = 0;
|
|
while ((c = poptGetNextOpt(optCon)) >= 0) {
|
|
switch (c) {
|
|
case 'v':
|
|
tdebuglev++;
|
|
break;
|
|
case 't':
|
|
if (config.timeout == 0) {
|
|
config.dont_check_timeout = 1;
|
|
config.allow_session_interruption = 1;
|
|
} else {
|
|
config.dont_check_timeout = 0;
|
|
config.allow_session_interruption = 0;
|
|
}
|
|
break;
|
|
#ifdef CONFIG_METADATA
|
|
case 'M':
|
|
config.metadata_enabled = 1;
|
|
break;
|
|
case 'g':
|
|
if (config.metadata_enabled == 0)
|
|
die("If you want to get cover art, you must also select the --metadata-pipename option.");
|
|
break;
|
|
#endif
|
|
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;
|
|
#else
|
|
die("The soxr option not available because this version of shairport-sync was built "
|
|
"without libsoxr "
|
|
"support. Change the -S option setting.");
|
|
#endif
|
|
else
|
|
die("Illegal stuffing option \"%s\" -- must be \"basic\" or \"soxr\"", stuffing);
|
|
break;
|
|
}
|
|
}
|
|
if (c < -1) {
|
|
die("%s: %s", poptBadOption(optCon, POPT_BADOPTION_NOALIAS), poptStrerror(c));
|
|
}
|
|
|
|
poptFreeContext(optCon);
|
|
|
|
// here, we are finally finished reading the options
|
|
|
|
#ifdef CONFIG_LIBDAEMON
|
|
if ((daemonisewith) && (daemonisewithout))
|
|
die("Select either daemonize_with_pid_file or daemonize_without_pid_file -- you have selected "
|
|
"both!");
|
|
if ((daemonisewith) || (daemonisewithout)) {
|
|
config.daemonise = 1;
|
|
if (daemonisewith)
|
|
config.daemonise_store_pid = 1;
|
|
};
|
|
#else
|
|
/* Check if we are called with -d or --daemon or -j or justDaemoniseNoPIDFile options*/
|
|
if ((daemonisewith != 0) || (daemonisewithout != 0)) {
|
|
fprintf(stderr, "%s was built without libdaemon, so does not support daemonisation using the "
|
|
"-d, --daemon, -j or --justDaemoniseNoPIDFile options\n",
|
|
config.appName);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
#endif
|
|
|
|
#ifdef CONFIG_METADATA
|
|
if ((config.metadata_enabled == 1) && (config.metadata_pipename == NULL))
|
|
config.metadata_pipename = strdup("/tmp/shairport-sync-metadata");
|
|
#endif
|
|
|
|
/* if the regtype hasn't been set, do it now */
|
|
if (config.regtype == NULL)
|
|
config.regtype = strdup("_raop._tcp");
|
|
|
|
if (tdebuglev != 0)
|
|
debuglev = tdebuglev;
|
|
|
|
// now, do the substitutions in the service name
|
|
char hostname[100];
|
|
gethostname(hostname, 100);
|
|
|
|
|
|
|
|
char *i0;
|
|
if (raw_service_name == NULL)
|
|
i0 = strdup("%H"); // this is the default it the Service Name wasn't specified
|
|
else
|
|
i0 = strdup(raw_service_name);
|
|
|
|
// here, do the substitutions for %h, %H, %v and %V
|
|
char *i1 = str_replace(i0, "%h", hostname);
|
|
if ((hostname[0] >= 'a') && (hostname[0] <= 'z'))
|
|
hostname[0] = hostname[0] - 0x20; // convert a lowercase first letter into a capital letter
|
|
char *i2 = str_replace(i1, "%H", hostname);
|
|
char *i3 = str_replace(i2, "%v", PACKAGE_VERSION);
|
|
char *vs = get_version_string();
|
|
config.service_name = str_replace(i3, "%V", vs); // service name complete
|
|
free(i0);
|
|
free(i1);
|
|
free(i2);
|
|
free(i3);
|
|
free(vs);
|
|
|
|
#ifdef CONFIG_MQTT
|
|
// mqtt topic was not set. As we have the service name just now, set it
|
|
if (config.mqtt_topic == NULL) {
|
|
int topic_length = 1 + strlen(config.service_name) + 1;
|
|
char *topic = malloc(topic_length + 1);
|
|
snprintf(topic, topic_length, "/%s/", config.service_name);
|
|
config.mqtt_topic = topic;
|
|
}
|
|
#endif
|
|
|
|
#ifdef CONFIG_LIBDAEMON
|
|
|
|
// now, check and calculate the pid directory
|
|
#ifdef DEFINED_CUSTOM_PID_DIR
|
|
char *use_this_pid_dir = PIDDIR;
|
|
#else
|
|
char *use_this_pid_dir = "/var/run/shairport-sync";
|
|
#endif
|
|
// debug(1,"config.piddir \"%s\".",config.piddir);
|
|
if (config.piddir)
|
|
use_this_pid_dir = config.piddir;
|
|
if (use_this_pid_dir)
|
|
config.computed_piddir = strdup(use_this_pid_dir);
|
|
#endif
|
|
return optind + 1;
|
|
}
|
|
|
|
#if defined(CONFIG_DBUS_INTERFACE) || defined(CONFIG_MPRIS_INTERFACE)
|
|
static GMainLoop *g_main_loop = NULL;
|
|
|
|
pthread_t dbus_thread;
|
|
void *dbus_thread_func(__attribute__((unused)) void *arg) {
|
|
g_main_loop = g_main_loop_new(NULL, FALSE);
|
|
g_main_loop_run(g_main_loop);
|
|
debug(2, "g_main_loop thread exit");
|
|
pthread_exit(NULL);
|
|
}
|
|
#endif
|
|
|
|
#ifdef CONFIG_LIBDAEMON
|
|
char pid_file_path_string[4096] = "\0";
|
|
|
|
const char *pid_file_proc(void) {
|
|
snprintf(pid_file_path_string, sizeof(pid_file_path_string), "%s/%s.pid", config.computed_piddir,
|
|
daemon_pid_file_ident ? daemon_pid_file_ident : "unknown");
|
|
// debug(1,"pid_file_path_string \"%s\".",pid_file_path_string);
|
|
return pid_file_path_string;
|
|
}
|
|
#endif
|
|
|
|
|
|
void exit_function() {
|
|
|
|
// the following is to ensure that if libdaemon has been included
|
|
// that most of this code will be skipped when the parent process is exiting
|
|
// exec
|
|
#ifdef CONFIG_LIBDAEMON
|
|
if (this_is_the_daemon_process) { //this is the daemon that is exiting
|
|
#endif
|
|
debug(1, "exit function called...");
|
|
|
|
/*
|
|
Actually, there is no terminate_mqtt() function.
|
|
#ifdef CONFIG_MQTT
|
|
if (config.mqtt_enabled) {
|
|
terminate_mqtt();
|
|
}
|
|
#endif
|
|
*/
|
|
|
|
#if defined(CONFIG_DBUS_INTERFACE) || defined(CONFIG_MPRIS_INTERFACE)
|
|
|
|
/*
|
|
Actually, there is no stop_mpris_service() function.
|
|
#ifdef CONFIG_MPRIS_INTERFACE
|
|
stop_mpris_service();
|
|
#endif
|
|
*/
|
|
#ifdef CONFIG_DBUS_INTERFACE
|
|
stop_dbus_service();
|
|
#endif
|
|
if (g_main_loop) {
|
|
debug(2, "Stopping DBUS Loop Thread");
|
|
g_main_loop_quit(g_main_loop);
|
|
pthread_join(dbus_thread, NULL);
|
|
}
|
|
#endif
|
|
|
|
#ifdef CONFIG_DACP_CLIENT
|
|
debug(2, "Stopping DACP Monitor");
|
|
dacp_monitor_stop();
|
|
#endif
|
|
|
|
#ifdef CONFIG_METADATA_HUB
|
|
debug(2, "Stopping metadata hub");
|
|
metadata_hub_stop();
|
|
#endif
|
|
|
|
#ifdef CONFIG_METADATA
|
|
metadata_stop(); // close down the metadata pipe
|
|
#endif
|
|
|
|
activity_monitor_stop(0);
|
|
|
|
if ((config.output) && (config.output->deinit)) {
|
|
debug(2, "Deinitialise the audio backend.");
|
|
config.output->deinit();
|
|
}
|
|
|
|
#ifdef CONFIG_SOXR
|
|
// be careful -- not sure if the thread can be cancelled cleanly, so wait for it to shut down
|
|
pthread_join(soxr_time_check_thread, NULL);
|
|
#endif
|
|
|
|
|
|
if (conns)
|
|
free(conns); // make sure the connections have been deleted first
|
|
|
|
if (config.service_name)
|
|
free(config.service_name);
|
|
|
|
#ifdef CONFIG_CONVOLUTION
|
|
if (config.convolution_ir_file)
|
|
free(config.convolution_ir_file);
|
|
#endif
|
|
|
|
if (config.regtype)
|
|
free(config.regtype);
|
|
|
|
#ifdef CONFIG_LIBDAEMON
|
|
daemon_retval_send(0);
|
|
daemon_pid_file_remove();
|
|
daemon_signal_done();
|
|
if (config.computed_piddir)
|
|
free(config.computed_piddir);
|
|
}
|
|
#endif
|
|
|
|
if (config.cfg)
|
|
config_destroy(config.cfg);
|
|
if (config.appName)
|
|
free(config.appName);
|
|
// probably should be freeing malloc'ed memory here, including strdup-created strings...
|
|
}
|
|
|
|
// for removing zombie script processes
|
|
// see: http://www.microhowto.info/howto/reap_zombie_processes_using_a_sigchld_handler.html
|
|
// used with thanks.
|
|
|
|
void handle_sigchld(__attribute__((unused)) int sig) {
|
|
int saved_errno = errno;
|
|
while (waitpid((pid_t)(-1), 0, WNOHANG) > 0) {}
|
|
errno = saved_errno;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
/* Check if we are called with -V or --version parameter */
|
|
if (argc >= 2 && ((strcmp(argv[1], "-V") == 0) || (strcmp(argv[1], "--version") == 0))) {
|
|
print_version();
|
|
exit(EXIT_SUCCESS);
|
|
}
|
|
|
|
/* Check if we are called with -h or --help parameter */
|
|
if (argc >= 2 && ((strcmp(argv[1], "-h") == 0) || (strcmp(argv[1], "--help") == 0))) {
|
|
usage(argv[0]);
|
|
exit(EXIT_SUCCESS);
|
|
}
|
|
|
|
#ifdef CONFIG_LIBDAEMON
|
|
pid = getpid();
|
|
#endif
|
|
conns = NULL; // no connections active
|
|
memset((void *)&main_thread_id, 0, sizeof(main_thread_id));
|
|
memset(&config, 0, sizeof(config)); // also clears all strings, BTW
|
|
fp_time_at_startup = get_absolute_time_in_fp();
|
|
fp_time_at_last_debug_message = fp_time_at_startup;
|
|
// this is a bit weird, but necessary -- basename() may modify the argument passed in
|
|
char *basec = strdup(argv[0]);
|
|
char *bname = basename(basec);
|
|
config.appName = strdup(bname);
|
|
if (config.appName == NULL)
|
|
die("can not allocate memory for the app name!");
|
|
free(basec);
|
|
|
|
// debug(1,"startup");
|
|
#ifdef CONFIG_LIBDAEMON
|
|
daemon_set_verbosity(LOG_DEBUG);
|
|
#else
|
|
setlogmask(LOG_UPTO(LOG_DEBUG));
|
|
openlog(NULL, 0, LOG_DAEMON);
|
|
#endif
|
|
atexit(exit_function);
|
|
|
|
// set defaults
|
|
|
|
// get the endianness
|
|
union {
|
|
uint32_t u32;
|
|
uint8_t arr[4];
|
|
} xn;
|
|
|
|
xn.arr[0] = 0x44; /* Lowest-address byte */
|
|
xn.arr[1] = 0x33;
|
|
xn.arr[2] = 0x22;
|
|
xn.arr[3] = 0x11; /* Highest-address byte */
|
|
|
|
if (xn.u32 == 0x11223344)
|
|
config.endianness = SS_LITTLE_ENDIAN;
|
|
else if (xn.u32 == 0x33441122)
|
|
config.endianness = SS_PDP_ENDIAN;
|
|
else if (xn.u32 == 0x44332211)
|
|
config.endianness = SS_BIG_ENDIAN;
|
|
else
|
|
die("Can not recognise the endianness of the processor.");
|
|
|
|
// set non-zero / non-NULL default values here
|
|
// but note that audio back ends also have a chance to set defaults
|
|
|
|
strcpy(configuration_file_path, SYSCONFDIR);
|
|
// strcat(configuration_file_path, "/shairport-sync"); // thinking about adding a special
|
|
// shairport-sync directory
|
|
strcat(configuration_file_path, "/");
|
|
strcat(configuration_file_path, config.appName);
|
|
strcat(configuration_file_path, ".conf");
|
|
config.configfile = configuration_file_path;
|
|
|
|
// config.statistics_requested = 0; // don't print stats in the log
|
|
// config.userSuppliedLatency = 0; // zero means none supplied
|
|
|
|
config.debugger_show_file_and_line =
|
|
1; // by default, log the file and line of the originating message
|
|
config.debugger_show_relative_time =
|
|
1; // by default, log the time back to the previous debug message
|
|
config.resyncthreshold = 0.05; // 50 ms
|
|
config.timeout = 120; // this number of seconds to wait for [more] audio before switching to idle.
|
|
config.tolerance =
|
|
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_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);
|
|
// snprintf(config.service_name, 20 + 100, "Shairport Sync on %s", hostname);
|
|
set_requested_connection_state_to_output(
|
|
1); // we expect to be able to connect to the output device
|
|
config.audio_backend_buffer_desired_length = 6615; // 0.15 seconds.
|
|
config.udp_port_base = 6001;
|
|
config.udp_port_range = 10;
|
|
config.output_format = SPS_FORMAT_S16_LE; // default
|
|
config.output_format_auto_requested = 1; // default auto select format
|
|
config.output_rate = 44100; // default
|
|
config.output_rate_auto_requested = 1; // default auto select format
|
|
config.decoders_supported =
|
|
1 << decoder_hammerton; // David Hammerton's decoder supported by default
|
|
#ifdef CONFIG_APPLE_ALAC
|
|
config.decoders_supported += 1 << decoder_apple_alac;
|
|
config.use_apple_decoder = 1; // use the ALAC decoder by default if support has been included
|
|
#endif
|
|
|
|
// initialise random number generator
|
|
|
|
r64init(0);
|
|
|
|
#ifdef CONFIG_LIBDAEMON
|
|
|
|
/* Reset signal handlers */
|
|
if (daemon_reset_sigs(-1) < 0) {
|
|
daemon_log(LOG_ERR, "Failed to reset all signal handlers: %s", strerror(errno));
|
|
return 1;
|
|
}
|
|
|
|
/* Unblock signals */
|
|
if (daemon_unblock_sigs(-1) < 0) {
|
|
daemon_log(LOG_ERR, "Failed to unblock all signals: %s", strerror(errno));
|
|
return 1;
|
|
}
|
|
|
|
/* Set identification string for the daemon for both syslog and PID file */
|
|
daemon_pid_file_ident = daemon_log_ident = daemon_ident_from_argv0(argv[0]);
|
|
|
|
daemon_pid_file_proc = pid_file_proc;
|
|
|
|
#endif
|
|
|
|
// parse arguments into config -- needed to locate pid_dir
|
|
int audio_arg = parse_options(argc, argv);
|
|
|
|
// mDNS supports maximum of 63-character names (we append 13).
|
|
if (strlen(config.service_name) > 50) {
|
|
warn("Supplied name too long (max 50 characters)");
|
|
config.service_name[50] = '\0'; // truncate it and carry on...
|
|
}
|
|
|
|
/* Check if we are called with -k or --kill option */
|
|
if (killOption != 0) {
|
|
#ifdef CONFIG_LIBDAEMON
|
|
int ret;
|
|
|
|
/* Kill daemon with SIGTERM */
|
|
/* Check if the new function daemon_pid_file_kill_wait() is available, if it is, use it. */
|
|
if ((ret = daemon_pid_file_kill_wait(SIGTERM, 5)) < 0)
|
|
daemon_log(LOG_WARNING, "Failed to kill daemon: %s", strerror(errno));
|
|
else {
|
|
// debug(1,"Successfully killed the shairport sync daemon.");
|
|
}
|
|
return ret < 0 ? 1 : 0;
|
|
#else
|
|
fprintf(stderr, "%s was built without libdaemon, so does not support the -k or --kill option\n",
|
|
config.appName);
|
|
return 1;
|
|
#endif
|
|
}
|
|
|
|
#ifdef CONFIG_LIBDAEMON
|
|
/* If we are going to daemonise, check that the daemon is not running already.*/
|
|
if ((config.daemonise) && ((pid = daemon_pid_file_is_running()) >= 0)) {
|
|
daemon_log(LOG_ERR, "Daemon already running on PID file %u", pid);
|
|
return 1;
|
|
}
|
|
|
|
/* here, daemonise with libdaemon */
|
|
|
|
if (config.daemonise) {
|
|
/* Prepare for return value passing from the initialization procedure of the daemon process */
|
|
if (daemon_retval_init() < 0) {
|
|
daemon_log(LOG_ERR, "Failed to create pipe.");
|
|
return 1;
|
|
}
|
|
|
|
/* Do the fork */
|
|
if ((pid = daemon_fork()) < 0) {
|
|
|
|
/* Exit on error */
|
|
daemon_retval_done();
|
|
return 1;
|
|
|
|
} else if (pid) { /* The parent */
|
|
int ret;
|
|
|
|
/* Wait for 20 seconds for the return value passed from the daemon process */
|
|
if ((ret = daemon_retval_wait(20)) < 0) {
|
|
daemon_log(LOG_ERR, "Could not receive return value from daemon process: %s",
|
|
strerror(errno));
|
|
return 255;
|
|
}
|
|
|
|
switch (ret) {
|
|
case 0:
|
|
break;
|
|
case 1:
|
|
daemon_log(LOG_ERR,
|
|
"daemon failed to launch: could not close open file descriptors after forking.");
|
|
break;
|
|
case 2:
|
|
daemon_log(LOG_ERR, "daemon failed to launch: could not create PID file.");
|
|
break;
|
|
case 3:
|
|
daemon_log(LOG_ERR, "daemon failed to launch: could not create or access PID directory.");
|
|
break;
|
|
default:
|
|
daemon_log(LOG_ERR, "daemon failed to launch, error %i.", ret);
|
|
}
|
|
return ret;
|
|
} else { /* pid == 0 means we are the daemon */
|
|
|
|
this_is_the_daemon_process = 1; //
|
|
|
|
/* Close FDs */
|
|
if (daemon_close_all(-1) < 0) {
|
|
daemon_log(LOG_ERR, "Failed to close all file descriptors: %s", strerror(errno));
|
|
/* Send the error condition to the parent process */
|
|
daemon_retval_send(1);
|
|
|
|
daemon_signal_done();
|
|
return 0;
|
|
}
|
|
|
|
/* Create the PID file if required */
|
|
if (config.daemonise_store_pid) {
|
|
/* Create the PID directory if required -- we don't really care about the result */
|
|
printf("PID directory is \"%s\".", config.computed_piddir);
|
|
int result = mkpath(config.computed_piddir, 0700);
|
|
if ((result != 0) && (result != -EEXIST)) {
|
|
// error creating or accessing the PID file directory
|
|
daemon_retval_send(3);
|
|
|
|
daemon_signal_done();
|
|
return 0;
|
|
}
|
|
|
|
if (daemon_pid_file_create() < 0) {
|
|
daemon_log(LOG_ERR, "Could not create PID file (%s).", strerror(errno));
|
|
|
|
daemon_retval_send(2);
|
|
daemon_signal_done();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* Send OK to parent process */
|
|
daemon_retval_send(0);
|
|
}
|
|
/* end libdaemon stuff */
|
|
}
|
|
|
|
#endif
|
|
debug(1, "Started!");
|
|
|
|
// install a zombie process reaper
|
|
// see: http://www.microhowto.info/howto/reap_zombie_processes_using_a_sigchld_handler.html
|
|
struct sigaction sa;
|
|
sa.sa_handler = &handle_sigchld;
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
|
|
if (sigaction(SIGCHLD, &sa, 0) == -1) {
|
|
perror(0);
|
|
exit(1);
|
|
}
|
|
|
|
main_thread_id = pthread_self();
|
|
if (!main_thread_id)
|
|
debug(1, "Main thread is set up to be NULL!");
|
|
|
|
// 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();
|
|
die("Invalid audio output specified!");
|
|
}
|
|
config.output->init(argc - audio_arg, argv + audio_arg);
|
|
|
|
// pthread_cleanup_push(main_cleanup_handler, NULL);
|
|
|
|
// daemon_log(LOG_NOTICE, "startup");
|
|
|
|
switch (config.endianness) {
|
|
case SS_LITTLE_ENDIAN:
|
|
debug(2, "The processor is running little-endian.");
|
|
break;
|
|
case SS_BIG_ENDIAN:
|
|
debug(2, "The processor is running big-endian.");
|
|
break;
|
|
case SS_PDP_ENDIAN:
|
|
debug(2, "The processor is running pdp-endian.");
|
|
break;
|
|
}
|
|
|
|
/* Mess around with the latency options */
|
|
// Basically, we expect the source to set the latency and add a fixed offset of 11025 frames to
|
|
// it, which sounds right
|
|
// If this latency is outside the max and min latensies that may be set by the source, clamp it to
|
|
// fit.
|
|
|
|
// If they specify a non-standard latency, we suggest the user to use the
|
|
// audio_backend_latency_offset instead.
|
|
|
|
if (config.userSuppliedLatency) {
|
|
inform("The fixed latency setting is deprecated, as Shairport Sync gets the correct "
|
|
"latency automatically from the source.");
|
|
inform("Use the audio_backend_latency_offset_in_seconds setting "
|
|
"instead to compensate for timing issues.");
|
|
if ((config.userSuppliedLatency != 0) &&
|
|
((config.userSuppliedLatency < 4410) ||
|
|
(config.userSuppliedLatency > BUFFER_FRAMES * 352 - 22050)))
|
|
die("An out-of-range fixed latency has been specified. It must be between 4410 and %d (at "
|
|
"44100 frames per second).",
|
|
BUFFER_FRAMES * 352 - 22050);
|
|
}
|
|
|
|
/* Print out options */
|
|
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.",
|
|
config.diagnostic_drop_packet_fraction);
|
|
debug(1, "statistics_requester status is %d.", config.statistics_requested);
|
|
#if CONFIG_LIBDAEMON
|
|
debug(1, "daemon status is %d.", config.daemonise);
|
|
debug(1, "daemon pid file path is \"%s\".", pid_file_proc());
|
|
#endif
|
|
debug(1, "rtsp listening port is %d.", config.port);
|
|
debug(1, "udp base port is %d.", config.udp_port_base);
|
|
debug(1, "udp port range is %d.", config.udp_port_range);
|
|
debug(1, "player name is \"%s\".", config.service_name);
|
|
debug(1, "backend is \"%s\".", config.output_name);
|
|
debug(1, "run_this_before_play_begins action is \"%s\".", config.cmd_start);
|
|
debug(1, "run_this_after_play_ends action is \"%s\".", config.cmd_stop);
|
|
debug(1, "wait-cmd status is %d.", config.cmd_blocking);
|
|
debug(1, "run_this_before_play_begins may return output is %d.", config.cmd_start_returns_output);
|
|
debug(1, "run_this_if_an_unfixable_error_is_detected action is \"%s\".", config.cmd_unfixable);
|
|
debug(1, "run_this_before_entering_active_state action is \"%s\".", config.cmd_active_start);
|
|
debug(1, "run_this_after_exiting_active_state action is \"%s\".", config.cmd_active_stop);
|
|
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, "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);
|
|
debug(1, "drift tolerance is %f seconds.", config.tolerance);
|
|
debug(1, "password is \"%s\".", config.password);
|
|
debug(1, "ignore_volume_control is %d.", config.ignore_volume_control);
|
|
if (config.volume_max_db_set)
|
|
debug(1, "volume_max_db is %d.", config.volume_max_db);
|
|
else
|
|
debug(1, "volume_max_db is not set");
|
|
debug(1, "volume range in dB (zero means use the range specified by the mixer): %u.",
|
|
config.volume_range_db);
|
|
debug(1, "volume_range_combined_hardware_priority (1 means hardware mixer attenuation is used "
|
|
"first) is %d.",
|
|
config.volume_range_hw_priority);
|
|
debug(1, "playback_mode is %d (0-stereo, 1-mono, 1-reverse_stereo, 2-both_left, 3-both_right).",
|
|
config.playback_mode);
|
|
debug(1, "disable_synchronization is %d.", config.no_sync);
|
|
debug(1, "use_mmap_if_available is %d.", config.no_mmap ? 0 : 1);
|
|
debug(1, "output_format automatic selection is %sabled.",
|
|
config.output_format_auto_requested ? "en" : "dis");
|
|
if (config.output_format_auto_requested == 0)
|
|
debug(1, "output_format is \"%s\".", sps_format_description_string(config.output_format));
|
|
debug(1, "output_rate automatic selection is %sabled.",
|
|
config.output_rate_auto_requested ? "en" : "dis");
|
|
if (config.output_rate_auto_requested == 0)
|
|
debug(1, "output_rate is %d.", config.output_rate);
|
|
debug(1, "audio backend desired buffer length is %f seconds.",
|
|
config.audio_backend_buffer_desired_length);
|
|
debug(1, "audio_backend_buffer_interpolation_threshold_in_seconds is %f seconds.",
|
|
config.audio_backend_buffer_interpolation_threshold_in_seconds);
|
|
debug(1, "audio backend latency offset is %f seconds.", config.audio_backend_latency_offset);
|
|
debug(1, "audio backend silence lead-in time is %f seconds. A value -1.0 means use the default.",
|
|
config.audio_backend_silent_lead_in_time);
|
|
debug(1, "zeroconf regtype is \"%s\".", config.regtype);
|
|
debug(1, "decoders_supported field is %d.", config.decoders_supported);
|
|
debug(1, "use_apple_decoder is %d.", config.use_apple_decoder);
|
|
debug(1, "alsa_use_hardware_mute is %d.", config.alsa_use_hardware_mute);
|
|
if (config.interface)
|
|
debug(1, "mdns service interface \"%s\" requested.", config.interface);
|
|
else
|
|
debug(1, "no special mdns service interface was requested.");
|
|
char *realConfigPath = realpath(config.configfile, NULL);
|
|
if (realConfigPath) {
|
|
debug(1, "configuration file name \"%s\" resolves to \"%s\".", config.configfile,
|
|
realConfigPath);
|
|
free(realConfigPath);
|
|
} else {
|
|
debug(1, "configuration file name \"%s\" can not be resolved.", config.configfile);
|
|
}
|
|
#ifdef CONFIG_METADATA
|
|
debug(1, "metadata enabled is %d.", config.metadata_enabled);
|
|
debug(1, "metadata pipename is \"%s\".", config.metadata_pipename);
|
|
debug(1, "metadata socket address is \"%s\" port %d.", config.metadata_sockaddr,
|
|
config.metadata_sockport);
|
|
debug(1, "metadata socket packet size is \"%d\".", config.metadata_sockmsglength);
|
|
debug(1, "get-coverart is %d.", config.get_coverart);
|
|
#endif
|
|
#ifdef CONFIG_MQTT
|
|
debug(1, "mqtt is %sabled.", config.mqtt_enabled ? "en" : "dis");
|
|
debug(1, "mqtt hostname is %s, port is %d.", config.mqtt_hostname, config.mqtt_port);
|
|
debug(1, "mqtt topic is %s.", config.mqtt_topic);
|
|
debug(1, "mqtt will%s publish raw metadata.", config.mqtt_publish_raw ? "" : " not");
|
|
debug(1, "mqtt will%s publish parsed metadata.", config.mqtt_publish_parsed ? "" : " not");
|
|
debug(1, "mqtt will%s publish cover Art.", config.mqtt_publish_cover ? "" : " not");
|
|
debug(1, "mqtt remote control is %sabled.", config.mqtt_enable_remote ? "en" : "dis");
|
|
#endif
|
|
|
|
#ifdef CONFIG_CONVOLUTION
|
|
debug(1, "convolution is %d.", config.convolution);
|
|
debug(1, "convolution IR file is \"%s\"", config.convolution_ir_file);
|
|
debug(1, "convolution max length %d", config.convolution_max_length);
|
|
debug(1, "convolution gain is %f", config.convolution_gain);
|
|
#endif
|
|
debug(1, "loudness is %d.", config.loudness);
|
|
debug(1, "loudness reference level is %f", config.loudness_reference_volume_db);
|
|
|
|
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);
|
|
MD5_Update(&ctx, config.service_name, strlen(config.service_name));
|
|
MD5_Final(ap_md5, &ctx);
|
|
#endif
|
|
|
|
#ifdef CONFIG_MBEDTLS
|
|
#if MBEDTLS_VERSION_MINOR >= 7
|
|
mbedtls_md5_context tctx;
|
|
mbedtls_md5_starts_ret(&tctx);
|
|
mbedtls_md5_update_ret(&tctx, (unsigned char *)config.service_name, strlen(config.service_name));
|
|
mbedtls_md5_finish_ret(&tctx, ap_md5);
|
|
#else
|
|
mbedtls_md5_context tctx;
|
|
mbedtls_md5_starts(&tctx);
|
|
mbedtls_md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name));
|
|
mbedtls_md5_finish(&tctx, ap_md5);
|
|
#endif
|
|
#endif
|
|
|
|
#ifdef CONFIG_POLARSSL
|
|
md5_context tctx;
|
|
md5_starts(&tctx);
|
|
md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name));
|
|
md5_finish(&tctx, ap_md5);
|
|
#endif
|
|
memcpy(config.hw_addr, ap_md5, sizeof(config.hw_addr));
|
|
#ifdef CONFIG_METADATA
|
|
metadata_init(); // create the metadata pipe if necessary
|
|
#endif
|
|
|
|
#ifdef CONFIG_METADATA_HUB
|
|
// debug(1, "Initialising metadata hub");
|
|
metadata_hub_init();
|
|
#endif
|
|
|
|
#ifdef CONFIG_DACP_CLIENT
|
|
// debug(1, "Requesting DACP Monitor");
|
|
dacp_monitor_start();
|
|
#endif
|
|
|
|
#if defined(CONFIG_DBUS_INTERFACE) || defined(CONFIG_MPRIS_INTERFACE)
|
|
// Start up DBUS services after initial settings are all made
|
|
// debug(1, "Starting up D-Bus services");
|
|
pthread_create(&dbus_thread, NULL, &dbus_thread_func, NULL);
|
|
#ifdef CONFIG_DBUS_INTERFACE
|
|
start_dbus_service();
|
|
#endif
|
|
#ifdef CONFIG_MPRIS_INTERFACE
|
|
start_mpris_service();
|
|
#endif
|
|
#endif
|
|
|
|
#ifdef CONFIG_MQTT
|
|
if (config.mqtt_enabled) {
|
|
initialise_mqtt();
|
|
}
|
|
#endif
|
|
|
|
activity_monitor_start();
|
|
rtsp_listen_loop();
|
|
return 0;
|
|
}
|