From bdc2dca3ec485f8afec8a3b8d2d06550dd49a0ac Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 6 Jul 2020 17:09:15 +0100 Subject: [PATCH] 3.3.7rc0 --- FREEBSD.md | 2 +- README.md | 30 +- activity_monitor.c | 20 +- alac.h | 1 - audio.c | 42 +- audio_alsa.c | 366 +++-- audio_dummy.c | 2 +- audio_jack.c | 61 +- audio_pa.c | 8 +- audio_pipe.c | 50 +- audio_sndio.c | 14 +- audio_soundio.c | 5 +- audio_stdout.c | 2 +- common.c | 303 ++-- common.h | 44 +- dacp.c | 723 +++++----- dbus-service.c | 69 +- man/shairport-sync.7 | 20 +- man/shairport-sync.7.xml | 634 ++++----- man/shairport-sync.html | 1219 +--------------- mdns_avahi.c | 12 +- mdns_dns_sd.c | 2 +- mdns_external.c | 2 +- metadata_hub.c | 199 +-- metadata_hub.h | 16 +- mpris-service.c | 19 +- mqtt.c | 14 +- player.c | 1618 +++++++++++----------- player.h | 27 +- rtp.c | 402 +++--- rtsp.c | 446 ++++-- scripts/shairport-sync-dbus-policy.conf | 6 +- scripts/shairport-sync-mpris-policy.conf | 5 +- scripts/shairport-sync.conf | 49 +- shairport-sync-dbus-test-client.c | 26 +- shairport.c | 344 +++-- tinysvcmdns.c | 7 +- 37 files changed, 3138 insertions(+), 3671 deletions(-) diff --git a/FREEBSD.md b/FREEBSD.md index 057a96f2c..fe3a2a5e8 100644 --- a/FREEBSD.md +++ b/FREEBSD.md @@ -60,7 +60,7 @@ Next, configure the build and compile it: ``` $ autoreconf -i -f -./configure --with-libdaemon --with-avahi --with-ssl=openssl --with-sndio --with-libdaemon --with-os=freebsd --with-freebsd-service +$ ./configure --with-libdaemon --with-avahi --with-ssl=openssl --with-sndio --with-libdaemon --with-os=freebsd --with-freebsd-service $ make ``` Add `--with-alsa` if you wish to include the ALSA back end. Omit the `--with-sndio` if you don't want the `sndio` back end. Omit the `--with-freebsd-service` if you don't want to install a FreeBSD startup script, runtime folder and user and group -- see below for more details. diff --git a/README.md b/README.md index a6821f312..fde314f9f 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ For more about the motivation behind Shairport Sync, please see the wiki at http Synchronisation, Latency, "Stuffing" --------- -The AirPlay protocol uses an agreed *latency* – the time difference, or delay, between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). The latency to be used is specified by the audio source when it negotiates with Shairport Sync. Most sources set a latency of two seconds. Recent versions of iTunes and forkedDaapd use a latency of just over 2.25 seconds. A latency of this length allows AirPlay players to correct for network delays, processing time variations and so on. +The AirPlay protocol uses an agreed *latency* – the time difference, or delay, between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). The latency to be used is specified by the audio source when it negotiates with Shairport Sync. Most sources set a latency of two seconds. Recent versions of iTunes and forkedDaapd use a latency of just over 2.25 seconds. A latency of this length allows AirPlay players to correct for network delays, processing time variations and so on. As mentioned previously, Shairport Sync implements full audio synchronisation when used with `alsa`, `sndio` or PulseAudio systems. This is done by monitoring the timestamps present in data coming from the audio source and the timing information from the audio system, e.g. `alsa`. To maintain the latency required for exact synchronisation, if the output device is running slow relative to the source, Shairport Sync will delete frames of audio to allow the device to keep up. If the output device is running fast, Shairport Sync will insert frames to keep time. The number of frames inserted or deleted is so small as to be almost inaudible on normal audio material. Frames are inserted or deleted as necessary at pseudorandom intervals. Alternatively, with `libsoxr` support, Shairport Sync can resample the audio feed to ensure the output device can keep up. This is less obtrusive than insertion and deletion but requires a good deal of processing power — most embedded devices probably can't support it. The process of insertion/deletion or resampling is rather inelegantly called “stuffing”. @@ -42,7 +42,7 @@ What else? * An [MPRIS](https://specifications.freedesktop.org/mpris-spec/2.2/) interface, partially complete and very functional, including access to metadata and artwork, and some limited remote control. * An interface to [MQTT](https://en.wikipedia.org/wiki/MQTT), an often-used protocol in home automation projects. * A native D-Bus interface, including access to metadata and artwork, some limited remote control and some system settings. - + Heritage ------- Shairport Sync is a substantial rewrite of the fantastic work done in Shairport 1.0 by James Laird and others — please see https://github.com/abrasive/shairport/blob/master/README.md#contributors-to-version-1x for a list of the contributors to Shairport 1.x and Shairport 0.x. From a "heritage" point of view, Shairport Sync is a fork of Shairport 1.0. @@ -67,7 +67,7 @@ See [here](https://github.com/mikebrady/shairport-sync/blob/master/INSTALL.md) f Shairport Sync may already be available as a package in your Linux distribution (search for `shairport-sync` – the package named `shairport` is a different program). Packages are available on recent versions of Debian, Ubuntu, Arch, OpenWrt and possibly more: -**Ubuntu:** A `shairport-sync` installer package is available for Ubuntu. Additionally, a Personal Package Archives for Shairport Sync master and development branches are available at https://launchpad.net/~dantheperson. +**Ubuntu:** A `shairport-sync` installer package is available for Ubuntu. Additionally, a Personal Package Archives for Shairport Sync master and development branches are available at https://launchpad.net/~dantheperson. **Debian:** shairport-sync is in the Debian archive. @@ -77,7 +77,7 @@ Shairport Sync may already be available as a package in your Linux distribution To build and install the latest version of Shairport Sync, an [Arch Linux build and installation guide](https://github.com/mikebrady/shairport-sync-for-arch-linux) is available, based on original work by [Elia Cereda](https://github.com/EliaCereda). -**Mac OS X:** A HomeBrew package exists for Shairport Sync. With HomeBrew installed, Shairport Sync can be installed using the command: +**Mac OS X:** A HomeBrew package exists for Shairport Sync. With HomeBrew installed, Shairport Sync can be installed using the command: ``` $brew install shairport-sync ``` @@ -93,7 +93,7 @@ If you wish to build and install the latest version of Shairport Sync on Debian, **Remove Old Versions of Shairport Sync and its Startup Scripts** -You should check to see if `shairport-sync` is already installed – you can use the command `$ which shairport-sync` to find where it is located, if installed. If it is installed you should delete it – you may need superuser privileges. After deleting, check again in case further copies are installed elsewhere. +You should check to see if `shairport-sync` is already installed – you can use the command `$ which shairport-sync` to find where it is located, if installed. If it is installed you should delete it – you may need superuser privileges. After deleting, check again in case further copies are installed elsewhere. You should also remove the startup script files `/etc/systemd/system/shairport-sync.service`, `/lib/systemd/system/shairport-sync.service` and `/etc/init.d/shairport-sync` if they exist – new ones will be installed in necessary. @@ -125,13 +125,13 @@ If PulseAudio in not installed, you'll get something like this: ``` $ pactl info -bash: pactl: command not found -$ +$ ``` If your system does not use PulseAudio, then it is likely that it uses the Advanced Linux Sound Architecture (ALSA), so you should build Shairport Sync with the ALSA backend. By the way, many systems with PulseAudio also have ALSA (in fact, PulseAudio is effectively a client of ALSA); in those cases you should choose the PulseAudio backend. If PulseAudio is not installed, there is no necessity to install it for Shairport Sync. In fact, Shairport Sync works better without it. -**Building** +**Building** To build Shairport Sync from sources on Debian, Ubuntu, Raspbian, etc. follow these instructions. @@ -181,7 +181,7 @@ $ autoreconf -i -f (Don't worry -- there's a recommended set of configuration options further down.) -- `--with-alsa` include the ALSA backend module to audio to be output through the Advanced Linux Sound Architecture (ALSA) system directly. This is recommended for highest quality. +- `--with-alsa` include the ALSA backend module to audio to be output through the Advanced Linux Sound Architecture (ALSA) system directly. This is recommended for highest quality. - `--with-pa` include the PulseAudio audio back end. This is recommended if your Linux installation already has PulseAudio installed. Although ALSA would be better, it requires direct and exclusive access to to a real (hardware) soundcard, and this is often impractical if PulseAudio is installed. - `--with-stdout` include an optional backend module to enable raw audio to be output through standard output (stdout). - `--with-pipe` include an optional backend module to enable raw audio to be output through a unix pipe. @@ -201,9 +201,9 @@ $ autoreconf -i -f **Determine if it's a `systemd` or a "System V" installation:** -If you wish to have Shairport Sync start automatically when your system boots, you need to figure out what so-called "init system" your system is using. (If you are using Shairport Sync with PulseAudio, as installed in many desktop systems, this section doesn't apply.) +If you wish to have Shairport Sync start automatically when your system boots, you need to figure out what so-called "init system" your system is using. (If you are using Shairport Sync with PulseAudio, as installed in many desktop systems, this section doesn't apply.) -There are a number of init systems in use: `systemd`, `upstart` and "System V" among others, and it's actually difficult to be certain which one your system is using. Fortunately, for Shairport Sync, all you have to do is figure out if it's a `systemd` init system or not. If it is not a `systemd` init system, you can assume that it is either a System V init system or else it is compatible with a System V init system. Recent systems tend to use `systemd`, whereas older systems use `upstart` or the earlier System V init system. +There are a number of init systems in use: `systemd`, `upstart` and "System V" among others, and it's actually difficult to be certain which one your system is using. Fortunately, for Shairport Sync, all you have to do is figure out if it's a `systemd` init system or not. If it is not a `systemd` init system, you can assume that it is either a System V init system or else it is compatible with a System V init system. Recent systems tend to use `systemd`, whereas older systems use `upstart` or the earlier System V init system. The easiest way is to look at the first few lines of the `init` manual. Enter the command: @@ -365,7 +365,7 @@ Note: Shairport Sync can take configuration settings from command line options. **Raspberry Pi** -The Raspberry Pi Models A and B have a built-in audio DAC that is connected to the device's headphone jack. Apart from a loud click when used for the first time after power-up, it is now quite adequate for casual listening. +The Raspberry Pi Models A and B have a built-in audio DAC that is connected to the device's headphone jack. Apart from a loud click when used for the first time after power-up, it is now quite adequate for casual listening. To get the benefits of improvements in the Pi's software and firmware, you should update to the Raspian release of October 2018 or later, as a number of improvements have been made to the built-in DAC. @@ -405,7 +405,7 @@ The System V init script at `/etc/init.d/shairport-sync` has a bare minimum : Examples -------- -Here are some examples of complete configuration files. +Here are some examples of complete configuration files. ``` general = { @@ -518,7 +518,7 @@ The UDP metadata format is very simple - the first four bytes are the metadata * Latency ------- -Latency is the exact time from a sound signal's original timestamp until that signal actually "appears" on the output of the audio output device, usually a Digital to Audio Converter (DAC), irrespective of any internal delays, processing times, etc. in the computer. +Latency is the exact time from a sound signal's original timestamp until that signal actually "appears" on the output of the audio output device, usually a Digital to Audio Converter (DAC), irrespective of any internal delays, processing times, etc. in the computer. Shairport Sync uses latencies supplied by the source, typically either 2 seconds or just over 2.25 seconds. You shouldn't need to change them. @@ -526,7 +526,7 @@ Problems can arise when you are trying to synchronise with speaker systems — t Resynchronisation ------------- -Shairport Sync actively maintains synchronisation with the source. +Shairport Sync actively maintains synchronisation with the source. If synchronisation is lost — say due to a busy source or a congested network — Shairport Sync will mute its output and resynchronise. The loss-of-sync threshold is a very conservative 0.050 seconds — i.e. the actual time and the expected time must differ by more than 50 ms to trigger a resynchronisation. Smaller disparities are corrected by insertions or deletions, as described above. * You can vary the resync threshold, or turn resync off completely, with the `general` `resync_threshold_in_seconds` setting. @@ -537,7 +537,7 @@ Playback synchronisation is allowed to wander — to "drift" — a small amount Some Statistics --------------- -If you turn on the `general` `statistics` setting, a heading like this will be output to the console or log file: +If you turn on the `general` `statistics` setting, a heading like this will be output to the log file (or to `STDERR` if the `-u` command line option is chosen): ``` sync error in milliseconds, net correction in ppm, corrections in ppm, total packets, missing packets, late packets, too late packets, resend requests, min DAC queue size, min buffer occupancy, max buffer occupancy, source nominal frames per second, source actual frames per second, output frames per second, source clock drift in ppm, source clock drift sample count, rough calculated correction in ppm ``` diff --git a/activity_monitor.c b/activity_monitor.c index bcc25d245..735fd03ad 100644 --- a/activity_monitor.c +++ b/activity_monitor.c @@ -62,7 +62,7 @@ void going_active(int block) { command_execute(config.cmd_active_start, "", block); #ifdef CONFIG_METADATA debug(2, "abeg"); // active mode begin - send_ssnc_metadata('pend', NULL, 0, 1); // contains cancellation points + send_ssnc_metadata('abeg', NULL, 0, 1); // contains cancellation points #endif #ifdef CONFIG_DBUS_INTERFACE @@ -89,7 +89,7 @@ void going_inactive(int block) { command_execute(config.cmd_active_stop, "", block); #ifdef CONFIG_METADATA debug(2, "aend"); // active mode end - send_ssnc_metadata('pend', NULL, 0, 1); // contains cancellation points + send_ssnc_metadata('aend', NULL, 0, 1); // contains cancellation points #endif #ifdef CONFIG_DBUS_INTERFACE @@ -196,21 +196,19 @@ void *activity_monitor_thread_code(void *arg) { } else { state = am_timing_out; - uint64_t time_to_wait_for_wakeup_fp = - (uint64_t)(config.active_state_timeout * 1000000); // resolution of microseconds - time_to_wait_for_wakeup_fp = time_to_wait_for_wakeup_fp << 32; - time_to_wait_for_wakeup_fp = time_to_wait_for_wakeup_fp / 1000000; + uint64_t time_to_wait_for_wakeup_ns = (uint64_t)(config.active_state_timeout * 1000000000); #ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD - uint64_t time_of_wakeup_fp = get_absolute_time_in_fp() + time_to_wait_for_wakeup_fp; - sec = time_of_wakeup_fp >> 32; - nsec = ((time_of_wakeup_fp & 0xffffffff) * 1000000000) >> 32; + uint64_t time_of_wakeup_ns = get_absolute_time_in_ns() + time_to_wait_for_wakeup_ns; + sec = time_of_wakeup_ns / 1000000000; + nsec = time_of_wakeup_ns % 1000000000; time_for_wait.tv_sec = sec; time_for_wait.tv_nsec = nsec; #endif + #ifdef COMPILE_FOR_OSX - sec = time_to_wait_for_wakeup_fp >> 32; - nsec = ((time_to_wait_for_wakeup_fp & 0xffffffff) * 1000000000) >> 32; + sec = time_to_wait_for_wakeup_ns / 1000000000; + nsec = time_to_wait_for_wakeup_ns % 1000000000; time_for_wait.tv_sec = sec; time_for_wait.tv_nsec = nsec; #endif diff --git a/alac.h b/alac.h index 7cdd507df..51db2c60c 100644 --- a/alac.h +++ b/alac.h @@ -27,7 +27,6 @@ * */ - #ifndef __ALAC__DECOMP_H #define __ALAC__DECOMP_H diff --git a/audio.c b/audio.c index 8b2762f45..23b1760dc 100644 --- a/audio.c +++ b/audio.c @@ -126,6 +126,7 @@ void parse_general_audio_options(void) { * are set before any options are chosen */ int value; double dvalue; + const char *str = 0; if (config.cfg != NULL) { /* Get the desired buffer size setting (deprecated). */ @@ -147,10 +148,10 @@ void parse_general_audio_options(void) { /* Get the desired buffer size setting in seconds. */ if (config_lookup_float(config.cfg, "general.audio_backend_buffer_desired_length_in_seconds", &dvalue)) { - if ((dvalue < 0) || (dvalue > 1.5)) { + if (dvalue < 0) { die("Invalid audio_backend_buffer_desired_length_in_seconds value: \"%f\". It " - "should be between 0 and " - "1.5, default is %.3f seconds", + "should be 0.0 or greater." + " The default is %.3f seconds", dvalue, config.audio_backend_buffer_desired_length); } else { config.audio_backend_buffer_desired_length = dvalue; @@ -190,23 +191,42 @@ void parse_general_audio_options(void) { /* Get the latency offset in seconds. */ if (config_lookup_float(config.cfg, "general.audio_backend_latency_offset_in_seconds", &dvalue)) { - if ((dvalue < -1.75) || (dvalue > 1.75)) { - die("Invalid audio_backend_latency_offset_in_seconds \"%f\". It " - "should be between -1.75 and +1.75, default is 0 seconds", - dvalue); + config.audio_backend_latency_offset = dvalue; + } + + /* Check if the length of the silent lead-in ia set to \"auto\". */ + if (config_lookup_string(config.cfg, "general.audio_backend_silent_lead_in_time", &str)) { + if (strcasecmp(str, "auto") == 0) { + config.audio_backend_silent_lead_in_time_auto = 1; } else { - config.audio_backend_latency_offset = dvalue; + if (config.audio_backend_silent_lead_in_time_auto == 1) + warn("Invalid audio_backend_silent_lead_in_time \"%s\". It should be \"auto\" or the " + "lead-in time in seconds. " + "It remains set to \"auto\". Note: numbers should not be placed in quotes.", + str); + else + warn("Invalid output rate \"%s\". It should be \"auto\" or the lead-in time in seconds. " + "It remains set to %f. Note: numbers should not be placed in quotes.", + str, config.audio_backend_silent_lead_in_time); } } /* Get the desired length of the silent lead-in. */ if (config_lookup_float(config.cfg, "general.audio_backend_silent_lead_in_time", &dvalue)) { if ((dvalue < 0.0) || (dvalue > 4)) { - die("Invalid audio_backend_silent_lead_in_time \"%f\". It " - "must be between 0.0 and 4.0 seconds. Omit setting to use the default value", - dvalue); + if (config.audio_backend_silent_lead_in_time_auto == 1) + warn("Invalid audio_backend_silent_lead_in_time \"%f\". It " + "must be between 0.0 and 4.0 seconds. Omit the setting to use the automatic value. " + "The setting remains at \"auto\".", + dvalue); + else + warn("Invalid audio_backend_silent_lead_in_time \"%f\". It " + "must be between 0.0 and 4.0 seconds. Omit the setting to use the automatic value. " + "It remains set to %f.", + dvalue, config.audio_backend_silent_lead_in_time); } else { config.audio_backend_silent_lead_in_time = dvalue; + config.audio_backend_silent_lead_in_time_auto = 0; } } } diff --git a/audio_alsa.c b/audio_alsa.c index 39e4d0514..69722d12f 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -210,9 +210,6 @@ int precision_delay_available() { generate_zero_frames(silence, frames_of_silence, config.output_format, use_dither, // i.e. with dither dither_random_number_store); - // debug(1,"Play %d frames of silence with most_recent_write_time of - // %" PRIx64 ".", - // frames_of_silence,most_recent_write_time); do_play(silence, frames_of_silence); pthread_cleanup_pop(1); // now we can get the delay, and we'll note if it uses update timestamps @@ -255,8 +252,6 @@ static uint64_t frames_played_at_measurement_start_time; static uint64_t measurement_time; static uint64_t frames_played_at_measurement_time; -volatile uint64_t most_recent_write_time; - static uint64_t frames_sent_for_playing; static uint64_t frame_index; static int measurement_data_is_valid; @@ -342,8 +337,9 @@ void actual_close_alsa_device() { if (alsa_handle) { int derr; if ((derr = snd_pcm_hw_free(alsa_handle))) - debug(1, "Error %d (\"%s\") freeing the output device hardware while " - "closing it.", + debug(1, + "Error %d (\"%s\") freeing the output device hardware while " + "closing it.", derr, snd_strerror(derr)); if ((derr = snd_pcm_close(alsa_handle))) @@ -358,7 +354,10 @@ void actual_close_alsa_device() { // The lowest rate that the DAC is capable of is chosen. unsigned int auto_speed_output_rates[] = { - 44100, 88200, 176400, 352800, + 44100, + 88200, + 176400, + 352800, }; // This array is of all the formats known to Shairport Sync, in order of the SPS_FORMAT definitions, @@ -707,9 +706,10 @@ int actual_open_alsa_device(int do_auto_setup) { buffer_size); } */ - debug(1, "The alsa buffer is smaller (%lu bytes) than the desired backend " - "buffer " - "length (%ld) you have chosen.", + debug(1, + "The alsa buffer is smaller (%lu bytes) than the desired backend " + "buffer " + "length (%ld) you have chosen.", actual_buffer_length, config.audio_backend_buffer_desired_length); } @@ -869,120 +869,117 @@ int prepare_mixer() { // do any alsa device initialisation (general case) // at present, this is only needed if a hardware mixer is being used // if there's a hardware mixer, it needs to be initialised before use - if (alsa_mix_ctrl == NULL) { - audio_alsa.volume = NULL; - audio_alsa.parameters = NULL; - audio_alsa.mute = NULL; - } else { - debug(2, "alsa: hardware mixer prepare"); - int oldState; - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable - - if (alsa_mix_dev == NULL) - alsa_mix_dev = alsa_out_dev; - - // Now, start trying to initialise the alsa device with the settings - // obtained - pthread_cleanup_debug_mutex_lock(&alsa_mixer_mutex, 1000, 1); - if (open_mixer() == 1) { - if (snd_mixer_selem_get_playback_volume_range(alsa_mix_elem, &alsa_mix_minv, - &alsa_mix_maxv) < 0) - debug(1, "Can't read mixer's [linear] min and max volumes."); - else { - if (snd_mixer_selem_get_playback_dB_range(alsa_mix_elem, &alsa_mix_mindb, - &alsa_mix_maxdb) == 0) { - - audio_alsa.volume = &volume; // insert the volume function now we - // know it can do dB stuff - audio_alsa.parameters = ¶meters; // likewise the parameters stuff - if (alsa_mix_mindb == SND_CTL_TLV_DB_GAIN_MUTE) { - // For instance, the Raspberry Pi does this - debug(1, "Lowest dB value is a mute"); - mixer_volume_setting_gives_mute = 1; - alsa_mix_mute = SND_CTL_TLV_DB_GAIN_MUTE; // this may not be - // necessary -- it's - // always - // going to be SND_CTL_TLV_DB_GAIN_MUTE, right? - // debug(1, "Try minimum volume + 1 as lowest true attenuation - // value"); - if (snd_mixer_selem_ask_playback_vol_dB(alsa_mix_elem, alsa_mix_minv + 1, - &alsa_mix_mindb) != 0) - debug(1, "Can't get dB value corresponding to a minimum volume " - "+ 1."); - } - debug(3, "Hardware mixer has dB volume from %f to %f.", (1.0 * alsa_mix_mindb) / 100.0, - (1.0 * alsa_mix_maxdb) / 100.0); + if (alsa_mix_ctrl == NULL) { + audio_alsa.volume = NULL; + audio_alsa.parameters = NULL; + audio_alsa.mute = NULL; + } else { + debug(2, "alsa: hardware mixer prepare"); + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable + + if (alsa_mix_dev == NULL) + alsa_mix_dev = alsa_out_dev; + + // Now, start trying to initialise the alsa device with the settings + // obtained + pthread_cleanup_debug_mutex_lock(&alsa_mixer_mutex, 1000, 1); + if (open_mixer() == 1) { + if (snd_mixer_selem_get_playback_volume_range(alsa_mix_elem, &alsa_mix_minv, &alsa_mix_maxv) < + 0) + debug(1, "Can't read mixer's [linear] min and max volumes."); + else { + if (snd_mixer_selem_get_playback_dB_range(alsa_mix_elem, &alsa_mix_mindb, + &alsa_mix_maxdb) == 0) { + + audio_alsa.volume = &volume; // insert the volume function now we + // know it can do dB stuff + audio_alsa.parameters = ¶meters; // likewise the parameters stuff + if (alsa_mix_mindb == SND_CTL_TLV_DB_GAIN_MUTE) { + // For instance, the Raspberry Pi does this + debug(1, "Lowest dB value is a mute"); + mixer_volume_setting_gives_mute = 1; + alsa_mix_mute = SND_CTL_TLV_DB_GAIN_MUTE; // this may not be + // necessary -- it's + // always + // going to be SND_CTL_TLV_DB_GAIN_MUTE, right? + // debug(1, "Try minimum volume + 1 as lowest true attenuation + // value"); + if (snd_mixer_selem_ask_playback_vol_dB(alsa_mix_elem, alsa_mix_minv + 1, + &alsa_mix_mindb) != 0) + debug(1, "Can't get dB value corresponding to a minimum volume " + "+ 1."); + } + debug(3, "Hardware mixer has dB volume from %f to %f.", (1.0 * alsa_mix_mindb) / 100.0, + (1.0 * alsa_mix_maxdb) / 100.0); + } else { + // use the linear scale and do the db conversion ourselves + warn("The hardware mixer specified -- \"%s\" -- does not have " + "a dB volume scale.", + alsa_mix_ctrl); + + if (snd_ctl_open(&ctl, alsa_mix_dev, 0) < 0) { + warn("Cannot open control \"%s\"", alsa_mix_dev); + response = -1; + } + if (snd_ctl_elem_id_malloc(&elem_id) < 0) { + debug(1, "Cannot allocate memory for control \"%s\"", alsa_mix_dev); + elem_id = NULL; + response = -2; } else { - // use the linear scale and do the db conversion ourselves - warn("The hardware mixer specified -- \"%s\" -- does not have " - "a dB volume scale.", - alsa_mix_ctrl); - - if (snd_ctl_open(&ctl, alsa_mix_dev, 0) < 0) { - warn("Cannot open control \"%s\"", alsa_mix_dev); - response = -1; - } - if (snd_ctl_elem_id_malloc(&elem_id) < 0) { - debug(1, "Cannot allocate memory for control \"%s\"", alsa_mix_dev); - elem_id = NULL; - response = -2; + snd_ctl_elem_id_set_interface(elem_id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(elem_id, alsa_mix_ctrl); + + if (snd_ctl_get_dB_range(ctl, elem_id, &alsa_mix_mindb, &alsa_mix_maxdb) == 0) { + debug(1, + "alsa: hardware mixer \"%s\" selected, with dB volume " + "from %f to %f.", + alsa_mix_ctrl, (1.0 * alsa_mix_mindb) / 100.0, (1.0 * alsa_mix_maxdb) / 100.0); + has_softvol = 1; + audio_alsa.volume = &volume; // insert the volume function now + // we know it can do dB stuff + audio_alsa.parameters = ¶meters; // likewise the parameters stuff } else { - snd_ctl_elem_id_set_interface(elem_id, SND_CTL_ELEM_IFACE_MIXER); - snd_ctl_elem_id_set_name(elem_id, alsa_mix_ctrl); - - if (snd_ctl_get_dB_range(ctl, elem_id, &alsa_mix_mindb, &alsa_mix_maxdb) == 0) { - debug(1, "alsa: hardware mixer \"%s\" selected, with dB volume " - "from %f to %f.", - alsa_mix_ctrl, (1.0 * alsa_mix_mindb) / 100.0, - (1.0 * alsa_mix_maxdb) / 100.0); - has_softvol = 1; - audio_alsa.volume = &volume; // insert the volume function now - // we know it can do dB stuff - audio_alsa.parameters = ¶meters; // likewise the parameters stuff - } else { - debug(1, "Cannot get the dB range from the volume control \"%s\"", alsa_mix_ctrl); - } + debug(1, "Cannot get the dB range from the volume control \"%s\"", alsa_mix_ctrl); } - /* - debug(1, "Min and max volumes are %d and - %d.",alsa_mix_minv,alsa_mix_maxv); - alsa_mix_maxdb = 0; - if ((alsa_mix_maxv!=0) && (alsa_mix_minv!=0)) - alsa_mix_mindb = - -20*100*(log10(alsa_mix_maxv*1.0)-log10(alsa_mix_minv*1.0)); - else if (alsa_mix_maxv!=0) - alsa_mix_mindb = -20*100*log10(alsa_mix_maxv*1.0); - audio_alsa.volume = &linear_volume; // insert the linear volume - function - audio_alsa.parameters = ¶meters; // likewise the parameters - stuff - debug(1,"Max and min dB calculated are %d and - %d.",alsa_mix_maxdb,alsa_mix_mindb); - */ } + /* + debug(1, "Min and max volumes are %d and + %d.",alsa_mix_minv,alsa_mix_maxv); + alsa_mix_maxdb = 0; + if ((alsa_mix_maxv!=0) && (alsa_mix_minv!=0)) + alsa_mix_mindb = + -20*100*(log10(alsa_mix_maxv*1.0)-log10(alsa_mix_minv*1.0)); + else if (alsa_mix_maxv!=0) + alsa_mix_mindb = -20*100*log10(alsa_mix_maxv*1.0); + audio_alsa.volume = &linear_volume; // insert the linear volume + function + audio_alsa.parameters = ¶meters; // likewise the parameters + stuff + debug(1,"Max and min dB calculated are %d and + %d.",alsa_mix_maxdb,alsa_mix_mindb); + */ } - if (((config.alsa_use_hardware_mute == 1) && - (snd_mixer_selem_has_playback_switch(alsa_mix_elem))) || - mixer_volume_setting_gives_mute) { - audio_alsa.mute = &mute; // insert the mute function now we know it - // can do muting stuff - // debug(1, "Has mixer and mute ability we will use."); - } else { - // debug(1, "Has mixer but not using hardware mute."); - } - close_mixer(); } - debug_mutex_unlock(&alsa_mixer_mutex, 3); // release the mutex - pthread_cleanup_pop(0); - pthread_setcancelstate(oldState, NULL); + if (((config.alsa_use_hardware_mute == 1) && + (snd_mixer_selem_has_playback_switch(alsa_mix_elem))) || + mixer_volume_setting_gives_mute) { + audio_alsa.mute = &mute; // insert the mute function now we know it + // can do muting stuff + // debug(1, "Has mixer and mute ability we will use."); + } else { + // debug(1, "Has mixer but not using hardware mute."); + } + close_mixer(); } + debug_mutex_unlock(&alsa_mixer_mutex, 3); // release the mutex + pthread_cleanup_pop(0); + pthread_setcancelstate(oldState, NULL); + } return response; } -int alsa_device_init() { - return prepare_mixer(); -} - +int alsa_device_init() { return prepare_mixer(); } static int init(int argc, char **argv) { // for debugging @@ -1140,8 +1137,10 @@ static int init(int argc, char **argv) { "\"S16\", \"S24\", \"S24_LE\", \"S24_BE\", " "\"S24_3LE\", \"S24_3BE\" or " "\"S32\", \"S32_LE\", \"S32_BE\". It remains set to \"%s\".", - str, config.output_format_auto_requested == 1 ? "auto" : sps_format_description_string( - config.output_format)); + str, + config.output_format_auto_requested == 1 + ? "auto" + : sps_format_description_string(config.output_format)); } } @@ -1291,9 +1290,9 @@ static int init(int argc, char **argv) { warn("Invalid use_precision_timing option choice \"%s\". It should be " "\"yes\", \"auto\" or \"no\". " "It remains set to \"%s\".", - config.use_precision_timing == YNA_NO ? "no" : config.use_precision_timing == YNA_AUTO - ? "auto" - : "yes"); + config.use_precision_timing == YNA_NO + ? "no" + : config.use_precision_timing == YNA_AUTO ? "auto" : "yes"); } } @@ -1350,7 +1349,6 @@ static int init(int argc, char **argv) { // length of the queue // if the queue gets too short, stuff it with silence - most_recent_write_time = 0; // could be used by the alsa_buffer_monitor_thread_code pthread_create(&alsa_buffer_monitor_thread, NULL, &alsa_buffer_monitor_thread_code, NULL); return response; @@ -1525,14 +1523,16 @@ int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, if (((update_timestamp_ns - stall_monitor_start_time) > stall_monitor_error_threshold) || ((time_now_ns - stall_monitor_start_time) > stall_monitor_error_threshold)) { - debug(2, "DAC seems to have stalled with time_now_ns: %" PRIX64 - ", update_timestamp_ns: %" PRIX64 ", stall_monitor_start_time %" PRIX64 - ", stall_monitor_error_threshold %" PRIX64 ".", + debug(2, + "DAC seems to have stalled with time_now_ns: %" PRIX64 + ", update_timestamp_ns: %" PRIX64 ", stall_monitor_start_time %" PRIX64 + ", stall_monitor_error_threshold %" PRIX64 ".", time_now_ns, update_timestamp_ns, stall_monitor_start_time, stall_monitor_error_threshold); - debug(2, "DAC seems to have stalled with time_now: %lx,%lx" - ", update_timestamp: %lx,%lx, stall_monitor_start_time %" PRIX64 - ", stall_monitor_error_threshold %" PRIX64 ".", + debug(2, + "DAC seems to have stalled with time_now: %lx,%lx" + ", update_timestamp: %lx,%lx, stall_monitor_start_time %" PRIX64 + ", stall_monitor_error_threshold %" PRIX64 ".", tn.tv_sec, tn.tv_nsec, update_timestamp.tv_sec, update_timestamp.tv_nsec, stall_monitor_start_time, stall_monitor_error_threshold); ret = sps_extra_code_output_stalled; @@ -1605,6 +1605,7 @@ int delay(long *the_delay) { } int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played) { + // elapsed_time is in nanoseconds int response = 0; // zero means okay if (measurement_data_is_valid) { *elapsed_time = measurement_time - measurement_start_time; @@ -1657,7 +1658,7 @@ int do_play(void *buf, int samples) { if ((frame_index == start_measurement_from_this_frame) || ((frame_index > start_measurement_from_this_frame) && (frame_index % 32 == 0))) { - measurement_time = get_absolute_time_in_fp(); + measurement_time = get_absolute_time_in_ns(); frames_played_at_measurement_time = frames_sent_for_playing - my_delay - samples; if (frame_index == start_measurement_from_this_frame) { @@ -1697,8 +1698,9 @@ int do_play(void *buf, int samples) { } } } else { - debug(1, "alsa: device status returns fault status %d and SND_PCM_STATE_* " - "%d for play.", + debug(1, + "alsa: device status returns fault status %d and SND_PCM_STATE_* " + "%d for play.", ret, state); frame_index = 0; measurement_data_is_valid = 0; @@ -1744,6 +1746,7 @@ int do_close() { // debug(1,"alsa: do_close() -- closing the output device"); if ((derr = snd_pcm_drop(alsa_handle))) debug(1, "Error %d (\"%s\") dropping output device.", derr, snd_strerror(derr)); + usleep(5000); if ((derr = snd_pcm_hw_free(alsa_handle))) debug(1, "Error %d (\"%s\") freeing the output device hardware.", derr, snd_strerror(derr)); @@ -1946,7 +1949,7 @@ void *alsa_buffer_monitor_thread_code(__attribute__((unused)) void *arg) { alsa_device_init(); alsa_device_initialised = 1; } - int sleep_time_ms = (int)(config.disable_standby_mode_silence_scan_interval * 1000); + int sleep_time_us = (int)(config.disable_standby_mode_silence_scan_interval * 1000000); pthread_cleanup_debug_mutex_lock(&alsa_mutex, 200000, 0); // check possible state transitions here if ((alsa_backend_state == abm_disconnected) && (config.keep_dac_busy != 0)) { @@ -1974,65 +1977,60 @@ void *alsa_buffer_monitor_thread_code(__attribute__((unused)) void *arg) { int reply; long buffer_size = 0; snd_pcm_state_t state; - uint64_t present_time = get_absolute_time_in_fp(); - if ((most_recent_write_time == 0) || (present_time > most_recent_write_time)) { - reply = delay_and_status(&state, &buffer_size, NULL); - if (reply != 0) { - buffer_size = 0; - char errorstring[1024]; - strerror_r(-reply, (char *)errorstring, sizeof(errorstring)); - debug(1, "alsa: alsa_buffer_monitor_thread_code delay error %d: \"%s\".", reply, - (char *)errorstring); - } - long buffer_size_threshold = - (long)(config.disable_standby_mode_silence_threshold * config.output_rate); - size_t size_of_silence_buffer; - if (buffer_size < buffer_size_threshold) { - uint64_t sleep_time_in_fp = sleep_time_ms; - sleep_time_in_fp = sleep_time_in_fp << 32; - sleep_time_in_fp = sleep_time_in_fp / 1000; - int frames_of_silence = 1024; - size_of_silence_buffer = frames_of_silence * frame_size; - void *silence = malloc(size_of_silence_buffer); - if (silence == NULL) { - warn("disable_standby_mode has been turned off because a memory allocation error " - "occurred."); - error_detected = 1; - } else { - int ret; - pthread_cleanup_push(malloc_cleanup, silence); - int use_dither = 0; - if ((alsa_mix_ctrl == NULL) && (config.ignore_volume_control == 0) && - (config.airplay_volume != 0.0)) - use_dither = 1; - dither_random_number_store = - generate_zero_frames(silence, frames_of_silence, config.output_format, - use_dither, // i.e. with dither - dither_random_number_store); - ret = do_play(silence, frames_of_silence); - frame_count++; - pthread_cleanup_pop(1); // free malloced buffer - if (ret < 0) { - error_count++; - char errorstring[1024]; - strerror_r(-ret, (char *)errorstring, sizeof(errorstring)); - debug(2, "alsa: alsa_buffer_monitor_thread_code error %d (\"%s\") writing %d samples " - "to alsa device -- %d errors in %d trials.", - ret, (char *)errorstring, frames_of_silence, error_count, frame_count); - if ((error_count > 40) && (frame_count < 100)) { - warn("disable_standby_mode has been turned off because too many underruns " - "occurred. Is Shairport Sync outputting to a virtual device or running in a " - "virtual machine?"); - error_detected = 1; - } + reply = delay_and_status(&state, &buffer_size, NULL); + if (reply != 0) { + buffer_size = 0; + char errorstring[1024]; + strerror_r(-reply, (char *)errorstring, sizeof(errorstring)); + debug(1, "alsa: alsa_buffer_monitor_thread_code delay error %d: \"%s\".", reply, + (char *)errorstring); + } + long buffer_size_threshold = + (long)(config.disable_standby_mode_silence_threshold * config.output_rate); + size_t size_of_silence_buffer; + if (buffer_size < buffer_size_threshold) { + int frames_of_silence = 1024; + size_of_silence_buffer = frames_of_silence * frame_size; + void *silence = malloc(size_of_silence_buffer); + if (silence == NULL) { + warn("disable_standby_mode has been turned off because a memory allocation error " + "occurred."); + error_detected = 1; + } else { + int ret; + pthread_cleanup_push(malloc_cleanup, silence); + int use_dither = 0; + if ((alsa_mix_ctrl == NULL) && (config.ignore_volume_control == 0) && + (config.airplay_volume != 0.0)) + use_dither = 1; + dither_random_number_store = + generate_zero_frames(silence, frames_of_silence, config.output_format, + use_dither, // i.e. with dither + dither_random_number_store); + ret = do_play(silence, frames_of_silence); + frame_count++; + pthread_cleanup_pop(1); // free malloced buffer + if (ret < 0) { + error_count++; + char errorstring[1024]; + strerror_r(-ret, (char *)errorstring, sizeof(errorstring)); + debug(2, + "alsa: alsa_buffer_monitor_thread_code error %d (\"%s\") writing %d samples " + "to alsa device -- %d errors in %d trials.", + ret, (char *)errorstring, frames_of_silence, error_count, frame_count); + if ((error_count > 40) && (frame_count < 100)) { + warn("disable_standby_mode has been turned off because too many underruns " + "occurred. Is Shairport Sync outputting to a virtual device or running in a " + "virtual machine?"); + error_detected = 1; } } } } } debug_mutex_unlock(&alsa_mutex, 0); - pthread_cleanup_pop(0); // release the mutex - usleep(sleep_time_ms * 1000); // has a cancellation point in it + pthread_cleanup_pop(0); // release the mutex + usleep(sleep_time_us); // has a cancellation point in it } pthread_exit(NULL); } diff --git a/audio_dummy.c b/audio_dummy.c index 5d64ecf23..5d16bf95e 100644 --- a/audio_dummy.c +++ b/audio_dummy.c @@ -39,7 +39,7 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char * static void deinit(void) {} static void start(int sample_rate, __attribute__((unused)) int sample_format) { - debug(1, "dummy audio output started at Fs=%d Hz\n", sample_rate); + debug(1, "dummy audio output started at %d frames per second.", sample_rate); } static int play(__attribute__((unused)) void *buf, __attribute__((unused)) int samples) { diff --git a/audio_jack.c b/audio_jack.c index db6e18994..21c3e5431 100644 --- a/audio_jack.c +++ b/audio_jack.c @@ -89,14 +89,9 @@ typedef struct soxr_quality { const char *name; } soxr_quality_t; -static soxr_quality_t soxr_quality_table[] = { - { SOXR_VHQ, "very high" }, - { SOXR_HQ, "high" }, - { SOXR_MQ, "medium" }, - { SOXR_LQ, "low" }, - { SOXR_QQ, "quick" }, - { -1, NULL } -}; +static soxr_quality_t soxr_quality_table[] = {{SOXR_VHQ, "very high"}, {SOXR_HQ, "high"}, + {SOXR_MQ, "medium"}, {SOXR_LQ, "low"}, + {SOXR_QQ, "quick"}, {-1, NULL}}; static int parse_soxr_quality_name(const char *name) { for (soxr_quality_t *s = soxr_quality_table; s->name != NULL; ++s) { @@ -107,9 +102,9 @@ static int parse_soxr_quality_name(const char *name) { return -1; } -static soxr_t soxr = NULL; +static soxr_t soxr = NULL; static soxr_quality_spec_t quality_spec; -static soxr_io_spec_t io_spec; +static soxr_io_spec_t io_spec; #endif static inline sample_t sample_conv(short sample) { @@ -122,8 +117,7 @@ static inline sample_t sample_conv(short sample) { return ((sample < 0) ? (-1.0 * sample / SHRT_MIN) : (1.0 * sample / SHRT_MAX)); } -static void deinterleave(const char *interleaved_input_buffer, - sample_t *jack_output_buffer[], +static void deinterleave(const char *interleaved_input_buffer, sample_t *jack_output_buffer[], jack_nframes_t offset, jack_nframes_t nframes) { jack_nframes_t f; // We're dealing with 16bit audio here: @@ -271,7 +265,7 @@ int jack_init(__attribute__((unused)) int argc, __attribute__((unused)) char **a io_spec = soxr_io_spec(SOXR_INT16_I, SOXR_FLOAT32_I); } else #endif - if (sample_rate != 44100) { + if (sample_rate != 44100) { die("The JACK server is running at the wrong sample rate (%d) for Shairport Sync." " Must be 44100 Hz.", sample_rate); @@ -353,8 +347,7 @@ void jack_deinit() { #endif } -void jack_start(int i_sample_rate, - __attribute__((unused)) int i_sample_format) { +void jack_start(int i_sample_rate, __attribute__((unused)) int i_sample_format) { // Nothing to do, JACK client has already been set up at jack_init(). // Also, we have no say over the sample rate or sample format of JACK, // We convert the 16bit samples to float, and die if the sample rate is != 44k1 without soxr. @@ -365,13 +358,7 @@ void jack_start(int i_sample_rate, soxr_delete(soxr); } soxr_error_t e = NULL; - soxr = soxr_create(i_sample_rate, - sample_rate, - NPORTS, - &e, - &io_spec, - &quality_spec, - NULL); + soxr = soxr_create(i_sample_rate, sample_rate, NPORTS, &e, &io_spec, &quality_spec, NULL); if (!soxr) { die("Unable to create soxr resampler for JACK: %s", e); } @@ -395,13 +382,13 @@ int jack_delay(long *the_delay) { // occupancy after another transfer had occurred, so we could "lose" a full transfer // (e.g. 1024 frames @ 44,100 fps ~ 23.2 milliseconds) pthread_mutex_lock(&buffer_mutex); - int64_t time_now = get_absolute_time_in_fp(); - int64_t delta = time_now - time_of_latest_transfer; + int64_t time_now = get_absolute_time_in_ns(); + int64_t delta = time_now - time_of_latest_transfer; // nanoseconds size_t audio_occupancy_now = jack_ringbuffer_read_space(jackbuf) / bytes_per_frame; debug(2, "audio_occupancy_now is %d.", audio_occupancy_now); pthread_mutex_unlock(&buffer_mutex); - int64_t frames_processed_since_latest_latency_check = (delta * sample_rate) >> 32; + int64_t frames_processed_since_latest_latency_check = (delta * sample_rate) / 1000000000; // debug(1,"delta: %" PRId64 " frames.",frames_processed_since_latest_latency_check); // jack_latency is set by the graph() callback, it's the average of the maximum // latencies of all our output ports. Adjust this constant baseline delay according @@ -428,13 +415,7 @@ int play(void *buf, int samples) { size_t i_done, o_done; soxr_error_t e; while (samples > 0 && thisbuf > 0) { - e = soxr_process(soxr, - (soxr_in_t)in, - samples, - &i_done, - (soxr_out_t)out, - thisbuf, - &o_done); + e = soxr_process(soxr, (soxr_in_t)in, samples, &i_done, (soxr_out_t)out, thisbuf, &o_done); if (e) die("Error during soxr process: %s", e); @@ -445,18 +426,18 @@ int play(void *buf, int samples) { } } else { #endif - j = 0; - for (j = 0; j < thisbuf && samples > 0; ++j) { - for (c = 0; c < NPORTS; ++c) - out[j * NPORTS + c] = sample_conv(*in++); - --samples; - } - jack_ringbuffer_write_advance(jackbuf, j * jack_sample_size * NPORTS); + j = 0; + for (j = 0; j < thisbuf && samples > 0; ++j) { + for (c = 0; c < NPORTS; ++c) + out[j * NPORTS + c] = sample_conv(*in++); + --samples; + } + jack_ringbuffer_write_advance(jackbuf, j * jack_sample_size * NPORTS); #ifdef CONFIG_SOXR } #endif } - time_of_latest_transfer = get_absolute_time_in_fp(); + time_of_latest_transfer = get_absolute_time_in_ns(); pthread_mutex_unlock(&buffer_mutex); if (samples) { warn("JACK ringbuffer overrun. Dropped %d samples.", samples); diff --git a/audio_pa.c b/audio_pa.c index e6a5b5767..951860beb 100644 --- a/audio_pa.c +++ b/audio_pa.c @@ -85,6 +85,11 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char * if (config.cfg != NULL) { const char *str; + /* Get the PulseAudio server name. */ + if (config_lookup_string(config.cfg, "pa.server", &str)) { + config.pa_server = (char *)str; + } + /* Get the Application Name. */ if (config_lookup_string(config.cfg, "pa.application_name", &str)) { config.pa_application_name = (char *)str; @@ -126,7 +131,8 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char * // Start the mainloop if (pa_threaded_mainloop_start(mainloop) != 0) die("could not start the pulseaudio threaded mainloop"); - if (pa_context_connect(context, NULL, 0, NULL) != 0) + + if (pa_context_connect(context, config.pa_server, 0, NULL) != 0) die("failed to connect to the pulseaudio context -- the error message is \"%s\".", pa_strerror(pa_context_errno(context))); diff --git a/audio_pipe.c b/audio_pipe.c index 4ec9b2402..6dedc464b 100644 --- a/audio_pipe.c +++ b/audio_pipe.c @@ -42,7 +42,6 @@ static int fd = -1; char *pipename = NULL; -int warned = 0; static void start(__attribute__((unused)) int sample_rate, __attribute__((unused)) int sample_format) { @@ -51,47 +50,46 @@ static void start(__attribute__((unused)) int sample_rate, // 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) && (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; - } + + fd = try_to_open_pipe_for_writing(pipename); + // we check that it's not a "real" error. 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." Which is okay. + if ((fd == -1) && (errno != ENXIO)) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "audio_pipe start -- error %d (\"%s\") opening pipe: \"%s\".", errno, + (char *)errorstring, pipename); + warn("can not open audio pipe -- error %d (\"%s\") opening pipe: \"%s\".", errno, + (char *)errorstring, pipename); + } } static int play(void *buf, int samples) { // if the file is not open, try to open it. char errorstring[1024]; if (fd == -1) { - fd = open(pipename, O_WRONLY | O_NONBLOCK); + fd = try_to_open_pipe_for_writing(pipename); } // if it's got a reader, write to it. if (fd > 0) { - int rc = non_blocking_write(fd, buf, samples * 4); - if ((rc < 0) && (warned == 0)) { + //int rc = non_blocking_write(fd, buf, samples * 4); + int rc = write(fd, buf, samples * 4); + if ((rc < 0) && (errno != EPIPE)) { strerror_r(errno, (char *)errorstring, 1024); - warn("Error %d writing to the pipe named \"%s\": \"%s\".", errno, pipename, errorstring); - warned = 1; + debug(1, "audio_pip play: error %d writing to the pipe named \"%s\": \"%s\".", errno, pipename, errorstring); } - } 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; } - return warned; + return 0; } static void stop(void) { // Don't close the pipe just because a play session has stopped. - // if (fd > 0) - // close(fd); + } static int init(int argc, char **argv) { - debug(1, "pipe init"); + // debug(1, "pipe init"); // const char *str; // int value; // double dvalue; @@ -122,10 +120,12 @@ static int init(int argc, char **argv) { pipename = strdup(argv[0]); // here, create the pipe + mode_t oldumask = umask(000); if (mkfifo(pipename, 0644) && errno != EEXIST) - die("Could not create output pipe \"%s\"", pipename); + die("Could not create audio pipe \"%s\"", pipename); + umask(oldumask); - debug(1, "Pipename is \"%s\"", pipename); + debug(1, "audio pipe name is \"%s\"", pipename); return 0; } diff --git a/audio_sndio.c b/audio_sndio.c index ac5cfa380..f8582c8ab 100644 --- a/audio_sndio.c +++ b/audio_sndio.c @@ -247,7 +247,7 @@ static void stop() { } static void onmove_cb(__attribute__((unused)) void *arg, int delta) { - time_of_last_onmove_cb = get_absolute_time_in_fp(); + time_of_last_onmove_cb = get_absolute_time_in_ns(); at_least_one_onmove_cb_seen = 1; played += delta; } @@ -258,10 +258,14 @@ static int delay(long *_delay) { if (at_least_one_onmove_cb_seen) { // when output starts, the onmove_cb callback will be made // calculate the difference in time between now and when the last callback occurred, // and use it to estimate the frames that would have been output - uint64_t time_difference = get_absolute_time_in_fp() - time_of_last_onmove_cb; - uint64_t frame_difference = time_difference * par.rate; - uint64_t frame_difference_big_integer = frame_difference >> 32; - estimated_extra_frames_output = frame_difference_big_integer; + uint64_t time_difference = get_absolute_time_in_ns() - time_of_last_onmove_cb; + uint64_t frame_difference = (time_difference * par.rate) / 1000000000; + estimated_extra_frames_output = frame_difference; + // sanity check -- total estimate can not exceed frames written. + if ((estimated_extra_frames_output + played) > written / framesize) { + // debug(1,"play estimate fails sanity check, possibly due to running on a VM"); + estimated_extra_frames_output = 0; // can't make any sensible guess + } // debug(1,"Frames played to last cb: %d, estimated to current time: // %d.",played,estimated_extra_frames_output); } diff --git a/audio_soundio.c b/audio_soundio.c index 119f7670e..501c6db2d 100644 --- a/audio_soundio.c +++ b/audio_soundio.c @@ -24,8 +24,9 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m int fill_bytes = soundio_ring_buffer_fill_count(ring_buffer); int fill_count = fill_bytes / outstream->bytes_per_frame; - debug(3, "[--->>] frame_count_min: %d , frame_count_max: %d , fill_bytes: %d , fill_count: %d , " - "outstream->bytes_per_frame: %d", + debug(3, + "[--->>] frame_count_min: %d , frame_count_max: %d , fill_bytes: %d , fill_count: %d , " + "outstream->bytes_per_frame: %d", frame_count_min, frame_count_max, fill_bytes, fill_count, outstream->bytes_per_frame); if (frame_count_min > fill_count) { diff --git a/audio_stdout.c b/audio_stdout.c index 00ded166e..8b37883e0 100644 --- a/audio_stdout.c +++ b/audio_stdout.c @@ -56,7 +56,7 @@ static int play(void *buf, int samples) { } static void stop(void) { - // don't close stdout + // Do nothing when play stops } static int init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) { diff --git a/common.c b/common.c index 228dfec97..39c4d62ee 100644 --- a/common.c +++ b/common.c @@ -40,6 +40,8 @@ #include #include #include +#include +#include #ifdef COMPILE_FOR_OSX #include @@ -88,10 +90,11 @@ void set_alsa_out_dev(char *); #endif config_t config_file_stuff; +int emergency_exit; pthread_t main_thread_id; -uint64_t fp_time_at_startup, fp_time_at_last_debug_message; +uint64_t ns_time_at_startup, ns_time_at_last_debug_message; -// always lock use this when accessing the fp_time_at_last_debug_message +// always lock use this when accessing the ns_time_at_last_debug_message static pthread_mutex_t debug_timing_lock = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t the_conn_lock = PTHREAD_MUTEX_INITIALIZER; @@ -101,7 +104,7 @@ const char *sps_format_description_string_array[] = { "S24_BE", "S24_3LE", "S24_3BE", "S32", "S32_LE", "S32_BE", "auto", "invalid"}; const char *sps_format_description_string(sps_format_t format) { - if ((format >= SPS_FORMAT_UNKNOWN) && (format <= SPS_FORMAT_AUTO)) + if (format <= SPS_FORMAT_AUTO) return sps_format_description_string_array[format]; else return sps_format_description_string_array[SPS_FORMAT_INVALID]; @@ -120,7 +123,7 @@ static void (*sps_log)(int prio, const char *t, ...) = daemon_log; static void (*sps_log)(int prio, const char *t, ...) = syslog; #endif -void do_sps_log(__attribute__((unused)) int prio, const char *t, ...) { +void do_sps_log_to_stderr(__attribute__((unused)) int prio, const char *t, ...) { char s[1024]; va_list args; va_start(args, t); @@ -129,7 +132,86 @@ void do_sps_log(__attribute__((unused)) int prio, const char *t, ...) { fprintf(stderr, "%s\n", s); } -void log_to_stderr() { sps_log = do_sps_log; } +void do_sps_log_to_stdout(__attribute__((unused)) int prio, const char *t, ...) { + char s[1024]; + va_list args; + va_start(args, t); + vsnprintf(s, sizeof(s), t, args); + va_end(args); + fprintf(stdout, "%s\n", s); +} + +int create_log_file(const char* path) { + int fd = -1; + if (path != NULL) { + char *dirc = strdup(path); + if (dirc) { + char *dname = dirname(dirc); + // create the directory, if necessary + int result = 0; + if (dname) { + char *pdir = realpath(dname, NULL); // will return a NULL if the directory doesn't exist + if (pdir == NULL) { + mode_t oldumask = umask(000); + result = mkpath(dname, 0777); + umask(oldumask); + } else { + free(pdir); + } + if ((result == 0) || (result == -EEXIST)) { + // now open the file + fd = open(path, O_WRONLY | O_NONBLOCK | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if ((fd == -1) && (errno == EEXIST)) + fd = open(path, O_WRONLY | O_APPEND | O_NONBLOCK); + + if (fd >= 0) { + // now we switch to blocking mode + int flags = fcntl(fd, F_GETFL); + if (flags == -1) { +// strerror_r(errno, (char *)errorstring, sizeof(errorstring)); +// debug(1, "create_log_file -- error %d (\"%s\") getting flags of pipe: \"%s\".", errno, +// (char *)errorstring, pathname); + } else { + flags = fcntl(fd, F_SETFL,flags & ~O_NONBLOCK); +// if (flags == -1) { +// strerror_r(errno, (char *)errorstring, sizeof(errorstring)); +// debug(1, "create_log_file -- error %d (\"%s\") unsetting NONBLOCK of pipe: \"%s\".", errno, +// (char *)errorstring, pathname); + } + } + } + } + free(dirc); + } + } + return fd; +} + +void do_sps_log_to_fd(__attribute__((unused)) int prio, const char *t, ...) { + char s[1024]; + va_list args; + va_start(args, t); + vsnprintf(s, sizeof(s), t, args); + va_end(args); + if (config.log_fd == -1) + config.log_fd = create_log_file(config.log_file_path); + if (config.log_fd >= 0) { + dprintf(config.log_fd, "%s\n", s); + } else if (errno != ENXIO) { // maybe there is a pipe there but not hooked up + fprintf(stderr, "%s\n", s); + } +} + +void log_to_stderr() { sps_log = do_sps_log_to_stderr; } +void log_to_stdout() { sps_log = do_sps_log_to_stdout; } +void log_to_file() { sps_log = do_sps_log_to_fd; } +void log_to_syslog() { +#ifdef CONFIG_LIBDAEMON + sps_log = daemon_log; +#else + sps_log = syslog; +#endif +} shairport_cfg config; @@ -202,10 +284,10 @@ char *generate_preliminary_string(char *buffer, size_t buffer_length, double tss insertion_point = insertion_point + strlen(insertion_point); space_remaining = space_remaining - strlen(insertion_point); } - if (prefix) { snprintf(insertion_point, space_remaining, "%s", prefix); insertion_point = insertion_point + strlen(insertion_point); + space_remaining = space_remaining - strlen(insertion_point); } return insertion_point; } @@ -218,17 +300,17 @@ void _die(const char *filename, const int linenumber, const char *format, ...) { char *s; if (debuglev) { pthread_mutex_lock(&debug_timing_lock); - uint64_t time_now = get_absolute_time_in_fp(); - uint64_t time_since_start = time_now - fp_time_at_startup; - uint64_t time_since_last_debug_message = time_now - fp_time_at_last_debug_message; - fp_time_at_last_debug_message = time_now; + uint64_t time_now = get_absolute_time_in_ns(); + uint64_t time_since_start = time_now - ns_time_at_startup; + uint64_t time_since_last_debug_message = time_now - ns_time_at_last_debug_message; + ns_time_at_last_debug_message = time_now; pthread_mutex_unlock(&debug_timing_lock); - uint64_t divisor = (uint64_t)1 << 32; - s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / divisor, - 1.0 * time_since_last_debug_message / divisor, filename, + s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / 1000000000, + 1.0 * time_since_last_debug_message / 1000000000, filename, linenumber, " *fatal error: "); } else { - s = b; + strncpy(b, "fatal error: ", sizeof(b)); + s = b+strlen(b); } va_list args; va_start(args, format); @@ -236,7 +318,8 @@ void _die(const char *filename, const int linenumber, const char *format, ...) { va_end(args); sps_log(LOG_ERR, "%s", b); pthread_setcancelstate(oldState, NULL); - abort(); // exit() doesn't always work, by heaven. + emergency_exit = 1; + exit(EXIT_FAILURE); } void _warn(const char *filename, const int linenumber, const char *format, ...) { @@ -247,17 +330,17 @@ void _warn(const char *filename, const int linenumber, const char *format, ...) char *s; if (debuglev) { pthread_mutex_lock(&debug_timing_lock); - uint64_t time_now = get_absolute_time_in_fp(); - uint64_t time_since_start = time_now - fp_time_at_startup; - uint64_t time_since_last_debug_message = time_now - fp_time_at_last_debug_message; - fp_time_at_last_debug_message = time_now; + uint64_t time_now = get_absolute_time_in_ns(); + uint64_t time_since_start = time_now - ns_time_at_startup; + uint64_t time_since_last_debug_message = time_now - ns_time_at_last_debug_message; + ns_time_at_last_debug_message = time_now; pthread_mutex_unlock(&debug_timing_lock); - uint64_t divisor = (uint64_t)1 << 32; - s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / divisor, - 1.0 * time_since_last_debug_message / divisor, filename, + s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / 1000000000, + 1.0 * time_since_last_debug_message / 1000000000, filename, linenumber, " *warning: "); } else { - s = b; + strncpy(b, "warning: ", sizeof(b)); + s = b+strlen(b); } va_list args; va_start(args, format); @@ -275,14 +358,13 @@ void _debug(const char *filename, const int linenumber, int level, const char *f char b[1024]; b[0] = 0; pthread_mutex_lock(&debug_timing_lock); - uint64_t time_now = get_absolute_time_in_fp(); - uint64_t time_since_start = time_now - fp_time_at_startup; - uint64_t time_since_last_debug_message = time_now - fp_time_at_last_debug_message; - fp_time_at_last_debug_message = time_now; + uint64_t time_now = get_absolute_time_in_ns(); + uint64_t time_since_start = time_now - ns_time_at_startup; + uint64_t time_since_last_debug_message = time_now - ns_time_at_last_debug_message; + ns_time_at_last_debug_message = time_now; pthread_mutex_unlock(&debug_timing_lock); - uint64_t divisor = (uint64_t)1 << 32; - char *s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / divisor, - 1.0 * time_since_last_debug_message / divisor, filename, + char *s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / 1000000000, + 1.0 * time_since_last_debug_message / 1000000000, filename, linenumber, " "); va_list args; va_start(args, format); @@ -300,14 +382,13 @@ void _inform(const char *filename, const int linenumber, const char *format, ... char *s; if (debuglev) { pthread_mutex_lock(&debug_timing_lock); - uint64_t time_now = get_absolute_time_in_fp(); - uint64_t time_since_start = time_now - fp_time_at_startup; - uint64_t time_since_last_debug_message = time_now - fp_time_at_last_debug_message; - fp_time_at_last_debug_message = time_now; + uint64_t time_now = get_absolute_time_in_ns(); + uint64_t time_since_start = time_now - ns_time_at_startup; + uint64_t time_since_last_debug_message = time_now - ns_time_at_last_debug_message; + ns_time_at_last_debug_message = time_now; pthread_mutex_unlock(&debug_timing_lock); - uint64_t divisor = (uint64_t)1 << 32; - s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / divisor, - 1.0 * time_since_last_debug_message / divisor, filename, + s = generate_preliminary_string(b, sizeof(b), 1.0 * time_since_start / 1000000000, + 1.0 * time_since_last_debug_message / 1000000000, filename, linenumber, " "); } else { s = b; @@ -1013,43 +1094,77 @@ uint64_t get_absolute_time_in_fp() { return time_now_fp; } -ssize_t non_blocking_write_with_timeout(int fd, const void *buf, size_t count, int timeout) { - // timeout is in milliseconds - void *ibuf = (void *)buf; - size_t bytes_remaining = count; - int rc = 1; - struct pollfd ufds[1]; - while ((bytes_remaining > 0) && (rc > 0)) { - // check that we can do some writing - ufds[0].fd = fd; - ufds[0].events = POLLOUT; - rc = poll(ufds, 1, timeout); - if (rc < 0) { - // debug(1, "non-blocking write error waiting for pipe to become ready for writing..."); - } else if (rc == 0) { - // warn("non-blocking write timeout waiting for pipe to become ready for writing"); - rc = -1; - errno = -ETIMEDOUT; - } else { // rc > 0, implying it might be ready - ssize_t bytes_written = write(fd, ibuf, bytes_remaining); - if (bytes_written == -1) { - // debug(1,"Error %d in non_blocking_write: \"%s\".",errno,strerror(errno)); - rc = bytes_written; // to imitate the return from write() - } else { - ibuf += bytes_written; - bytes_remaining -= bytes_written; - } - } +uint64_t get_absolute_time_in_ns() { + uint64_t time_now_ns; + +#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD + struct timespec tn; + // can't use CLOCK_MONOTONIC_RAW as it's not implemented in OpenWrt + clock_gettime(CLOCK_MONOTONIC, &tn); + uint64_t tnnsec = tn.tv_sec; + tnnsec = tnnsec * 1000000000; + uint64_t tnjnsec = tn.tv_nsec; + time_now_ns = tnnsec + tnjnsec; +#endif + +#ifdef COMPILE_FOR_OSX + uint64_t time_now_mach; + uint64_t elapsedNano; + static mach_timebase_info_data_t sTimebaseInfo = {0, 0}; + + time_now_mach = mach_absolute_time(); + + // If this is the first time we've run, get the timebase. + // We can use denom == 0 to indicate that sTimebaseInfo is + // uninitialised because it makes no sense to have a zero + // denominator in a fraction. + + if (sTimebaseInfo.denom == 0) { + debug(1, "Mac initialise timebase info."); + (void)mach_timebase_info(&sTimebaseInfo); } - if (rc > 0) - return count - bytes_remaining; // this is just to mimic a normal write/3. - else - return rc; - // return write(fd,buf,count); + + // Do the maths. We hope that the multiplication doesn't + // overflow; the price you pay for working in fixed point. + + // this gives us nanoseconds + time_now_ns = time_now_mach * sTimebaseInfo.numer / sTimebaseInfo.denom; +#endif + + return time_now_ns; } -ssize_t non_blocking_write(int fd, const void *buf, size_t count) { - return non_blocking_write_with_timeout(fd, buf, count, 5000); // default is 5 seconds. +int try_to_open_pipe_for_writing(const char* pathname) { + // tries to open the pipe in non-blocking mode first. + // if it succeeds, it sets it to blocking. + // if not, it returns -1. + + int fdis = open(pathname, O_WRONLY | O_NONBLOCK); // open it in non blocking mode first + + // we check that it's not a "real" error. 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." Which is okay. + // This is checked by the caller. + + if (fdis >= 0) { + // now we switch to blocking mode + int flags = fcntl(fdis, F_GETFL); + if (flags == -1) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "try_to_open_pipe -- error %d (\"%s\") getting flags of pipe: \"%s\".", errno, + (char *)errorstring, pathname); + } else { + flags = fcntl(fdis, F_SETFL,flags & ~O_NONBLOCK); + if (flags == -1) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "try_to_open_pipe -- error %d (\"%s\") unsetting NONBLOCK of pipe: \"%s\".", errno, + (char *)errorstring, pathname); + } + } + } + return fdis; } /* from @@ -1093,7 +1208,9 @@ char *str_replace(const char *string, const char *substr, const char *replacemen /* from http://burtleburtle.net/bob/rand/smallprng.html */ -// this is not thread-safe, so we need a mutex on it to use it properly// always lock use this when accessing the fp_time_at_last_debug_message +// this is not thread-safe, so we need a mutex on it to use it properly. +// always lock use this when accessing the fp_time_at_last_debug_message + pthread_mutex_t r64_mutex = PTHREAD_MUTEX_INITIALIZER; // typedef uint64_t u8; @@ -1201,18 +1318,17 @@ int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time, timeoutTime.tv_sec = time_then; timeoutTime.tv_nsec = time_then_nsec; - int64_t start_time = get_absolute_time_in_fp(); + uint64_t start_time = get_absolute_time_in_ns(); int r = pthread_mutex_timedlock(mutex, &timeoutTime); - int64_t et = get_absolute_time_in_fp() - start_time; + uint64_t et = get_absolute_time_in_ns() - start_time; if ((debuglevel != 0) && (r != 0) && (debugmessage != NULL)) { - et = (et * 1000000) >> 32; // microseconds char errstr[1000]; if (r == ETIMEDOUT) debug(debuglevel, - "timed out waiting for a mutex, having waiting %f seconds, with a maximum " + "timed out waiting for a mutex, having waited %f microseconds, with a maximum " "waiting time of %d microseconds. \"%s\".", - (1.0 * et) / 1000000, dally_time, debugmessage); + (1.0E6 * et) / 1000000000, dally_time, debugmessage); else debug(debuglevel, "error %d: \"%s\" waiting for a mutex: \"%s\".", r, strerror_r(r, errstr, sizeof(errstr)), debugmessage); @@ -1261,7 +1377,7 @@ int _debug_mutex_lock(pthread_mutex_t *mutex, useconds_t dally_time, const char return pthread_mutex_lock(mutex); int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - uint64_t time_at_start = get_absolute_time_in_fp(); + uint64_t time_at_start = get_absolute_time_in_ns(); char dstring[1000]; memset(dstring, 0, sizeof(dstring)); snprintf(dstring, sizeof(dstring), "%s:%d", filename, line); @@ -1270,12 +1386,10 @@ int _debug_mutex_lock(pthread_mutex_t *mutex, useconds_t dally_time, const char int result = sps_pthread_mutex_timedlock(mutex, dally_time, dstring, debuglevel); if (result == ETIMEDOUT) { result = pthread_mutex_lock(mutex); - uint64_t time_delay = get_absolute_time_in_fp() - time_at_start; - uint64_t divisor = (uint64_t)1 << 32; - double delay = 1.0 * time_delay / divisor; + uint64_t time_delay = get_absolute_time_in_ns() - time_at_start; debug(debuglevel, - "mutex_lock \"%s\" at \"%s\" expected max wait: %0.9f, actual wait: %0.9f sec.", - mutexname, dstring, (1.0 * dally_time) / 1000000, delay); + "mutex_lock \"%s\" at \"%s\" expected max wait: %0.9f, actual wait: %0.9f microseconds.", + mutexname, dstring, (1.0 * dally_time), 0.001 * time_delay); } pthread_setcancelstate(oldState, NULL); return result; @@ -1313,6 +1427,9 @@ char *get_version_string() { if (version_string) { strcpy(version_string, PACKAGE_VERSION); +#ifdef CONFIG_APPLE_ALAC + strcat(version_string, "-alac"); +#endif #ifdef CONFIG_LIBDAEMON strcat(version_string, "-libdaemon"); #endif @@ -1543,9 +1660,11 @@ int64_t generate_zero_frames(char *outp, size_t number_of_frames, sps_format_t f return previous_random_number; } -// This will check the incoming string "s" of length "len" with the existing NUL-terminated string "str" and update "flag" accordingly. +// This will check the incoming string "s" of length "len" with the existing NUL-terminated string +// "str" and update "flag" accordingly. -// Note: if the incoming string length is zero, then the a NULL is used; i.e. no zero-length strings are stored. +// Note: if the incoming string length is zero, then the a NULL is used; i.e. no zero-length strings +// are stored. // If the strings are different, the str is free'd and replaced by a pointer // to a newly strdup'd string and the flag is set @@ -1558,7 +1677,7 @@ int string_update_with_size(char **str, int *flag, char *s, size_t len) { free(*str); //*str = strndup(s, len); // it seems that OpenWrt 12 doesn't have this char *p = malloc(len + 1); - memcpy(p,s,len); + memcpy(p, s, len); p[len] = '\0'; *str = p; *flag = 1; @@ -1575,7 +1694,7 @@ int string_update_with_size(char **str, int *flag, char *s, size_t len) { if ((s) && (len)) { //*str = strndup(s, len); // it seems that OpenWrt 12 doesn't have this char *p = malloc(len + 1); - memcpy(p,s,len); + memcpy(p, s, len); p[len] = '\0'; *str = p; *flag = 1; @@ -1586,3 +1705,13 @@ int string_update_with_size(char **str, int *flag, char *s, size_t len) { } return *flag; } + +// from https://stackoverflow.com/questions/13663617/memdup-function-in-c, with thanks +void* memdup(const void* mem, size_t size) { + void* out = malloc(size); + + if(out != NULL) + memcpy(out, mem, size); + + return out; +} diff --git a/common.h b/common.h index 2c9c05bdb..1f5d1c87b 100644 --- a/common.h +++ b/common.h @@ -100,9 +100,13 @@ typedef enum { const char *sps_format_description_string(sps_format_t format); typedef struct { - double resend_control_first_check_time; // wait this long before asking for a missing packet to be resent + double missing_port_dacp_scan_interval_seconds; // if no DACP port number can be found, check at + // these intervals + double resend_control_first_check_time; // wait this long before asking for a missing packet to be + // resent double resend_control_check_interval_time; // wait this long between making requests - double resend_control_last_check_time; // if the packet is missing this close to the time of use, give up + double resend_control_last_check_time; // if the packet is missing this close to the time of use, + // give up pthread_mutex_t lock; config_t *cfg; int endianness; @@ -113,6 +117,7 @@ typedef struct { // on host %h" #ifdef CONFIG_PA + char *pa_server; // the pulseaudio server address that Shairport Sync will play on. char *pa_application_name; // the name under which Shairport Sync shows up as an "Application" in // the Sound Preferences in most desktop Linuxes. // Defaults to "Shairport Sync". Shairport Sync must be playing to see it. @@ -177,6 +182,8 @@ typedef struct { char *pidfile; #endif + int log_fd; // file descriptor of the file or pipe to log stuff to. + char *log_file_path; // path to file or pipe to log to, if any int logOutputLevel; // log output level int debugger_show_elapsed_time; // in the debug message, display the time since startup int debugger_show_relative_time; // in the debug message, display the time since the last one @@ -212,7 +219,11 @@ typedef struct { double audio_backend_latency_offset; // this will be the offset in seconds to compensate for any // fixed latency there might be in the audio path + int audio_backend_silent_lead_in_time_auto; // true if the lead-in time should be from as soon as + // packets are received double audio_backend_silent_lead_in_time; // the length of the silence that should precede a play. + uint32_t minimum_free_buffer_headroom; // when effective latency is calculated, ensure this number + // of buffers are unallocated double active_state_timeout; // the amount of time from when play ends to when the system leaves // into the "active" mode. uint32_t volume_range_db; // the range, in dB, from max dB to min dB. Zero means use the mixer's @@ -274,7 +285,10 @@ typedef struct { int jack_soxr_resample_quality; #endif #endif - + void *gradients; // a linked list of the clock gradients discovered for all DACP IDs + // can't use IP numbers as they might be given to different devices + // can't get hold of MAC addresses. + // can't define the nvll linked list struct here } shairport_cfg; // accessors to config for multi-thread access @@ -286,7 +300,12 @@ uint16_t nctohs(const uint8_t *p); // read 2 characters from *p and do ntohs on void memory_barrier(); -void log_to_stderr(); // call this to director logging to stderr; +void log_to_stderr(); // call this to direct logging to stderr; +void log_to_stdout(); // call this to direct logging to stdout; +void log_to_syslog(); // call this to direct logging to the system log; +void log_to_file(); // call this to direct logging to a file or (pre-existing) pipe; + + // true if Shairport Sync is supposed to be sending output to the output device, false otherwise @@ -294,10 +313,7 @@ int get_requested_connection_state_to_output(); void set_requested_connection_state_to_output(int v); -ssize_t non_blocking_write_with_timeout(int fd, const void *buf, size_t count, - int timeout); // timeout in milliseconds - -ssize_t non_blocking_write(int fd, const void *buf, size_t count); // used in a few places +int try_to_open_pipe_for_writing(const char* pathname); // open it without blocking if it's not hooked up /* from * http://coding.debuntu.org/c-implementing-str_replace-replace-all-occurrences-substring#comment-722 @@ -349,10 +365,11 @@ double flat_vol2attn(double vol, long max_db, long min_db); double vol2attn(double vol, long max_db, long min_db); // return a monolithic (always increasing) time in nanoseconds -uint64_t get_absolute_time_in_fp(void); +// uint64_t get_absolute_time_in_fp(void); // obselete +uint64_t get_absolute_time_in_ns(void); // time at startup for debugging timing -extern uint64_t fp_time_at_startup, fp_time_at_last_debug_message; +extern uint64_t ns_time_at_startup, ns_time_at_last_debug_message; // this is for reading an unsigned 32 bit number, such as an RTP timestamp @@ -363,6 +380,7 @@ extern pthread_t main_thread_id; extern shairport_cfg config; extern config_t config_file_stuff; +extern int emergency_exit; int config_set_lookup_bool(config_t *cfg, char *where, int *dst); @@ -420,9 +438,6 @@ extern pthread_mutex_t r64_mutex; char *get_version_string(); // mallocs a string space -- remember to free it afterwards -void sps_nanosleep(const time_t sec, - const long nanosec); // waits for this time, even through interruptions - int64_t generate_zero_frames(char *outp, size_t number_of_frames, sps_format_t format, int with_dither, int64_t random_number_in); @@ -430,4 +445,7 @@ void malloc_cleanup(void *arg); int string_update_with_size(char **str, int *flag, char *s, size_t len); +// from https://stackoverflow.com/questions/13663617/memdup-function-in-c, with thanks +void* memdup(const void* mem, size_t size); + #endif // _COMMON_H diff --git a/dacp.c b/dacp.c index f3111a838..c6b9968fd 100644 --- a/dacp.c +++ b/dacp.c @@ -1,6 +1,6 @@ /* * DACP protocol handler. This file is part of Shairport Sync. - * Copyright (c) Mike Brady 2017 -- 2019 + * Copyright (c) Mike Brady 2017 -- 2020 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -49,7 +49,7 @@ typedef struct { int players_connection_thread_index; // the connection thread index when a player thread is // associated with this, zero otherwise - int scan_enable; // set to 1 if if sacanning should be considered + int scan_enable; // set to 1 if scanning should be considered char dacp_id[256]; // the DACP ID string uint16_t port; // zero if no port discovered short connection_family; // AF_INET6 or AF_INET @@ -123,7 +123,7 @@ static const struct http_funcs responseFuncs = { // static pthread_mutex_t dacp_server_information_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t dacp_conversation_lock; static pthread_mutex_t dacp_server_information_lock; -static pthread_cond_t dacp_server_information_cv = PTHREAD_COND_INITIALIZER; +static pthread_cond_t dacp_server_information_cv; void addrinfo_cleanup(void *arg) { // debug(1, "addrinfo cleanup called."); @@ -154,7 +154,7 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) { // debug(1,"dacp_send_command: command is: \"%s\".",command); if (dacp_server.port == 0) { - debug(1, "No DACP port specified yet"); + debug(3, "No DACP port specified yet"); result = 490; // no port specified } else { @@ -169,6 +169,7 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) { // 493 Client failed to send a message // 492 Argument out of range // 491 Client refused connection + // 490 No port specified struct addrinfo hints, *res; int sockfd; @@ -201,7 +202,7 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) { // debug(1,"Error %d \"%s\" at getaddrinfo.",ires,gai_strerror(ires)); response.code = 498; // Bad Address information for the DACP server } else { - uint64_t start_time = get_absolute_time_in_fp(); + uint64_t start_time = get_absolute_time_in_ns(); pthread_cleanup_push(addrinfo_cleanup, (void *)&res); // only do this one at a time -- not sure it is necessary, but better safe than sorry @@ -221,18 +222,16 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) { pthread_cleanup_push(connect_cleanup, (void *)&sockfd); // debug(2, "dacp_send_command: open socket %d.",sockfd); - // This is for limiting the time to be spent waiting for a response. struct timeval tv; tv.tv_sec = 0; tv.tv_usec = 500000; if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) == -1) - debug(1, "dacp_send_command: error %d setting receive timeout.", errno); + debug(1, "dacp_send_command: error %d setting receive timeout.", errno); if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof tv) == -1) debug(1, "dacp_send_command: error %d setting send timeout.", errno); - // connect! // debug(1, "DACP socket created."); if (connect(sockfd, res->ai_addr, res->ai_addrlen) < 0) { @@ -352,10 +351,9 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) { } pthread_cleanup_pop(1); // this should free the addrinfo // freeaddrinfo(res); - uint64_t et = get_absolute_time_in_fp() - start_time; - et = (et * 1000000) >> 32; // microseconds + uint64_t et = get_absolute_time_in_ns() - start_time; // this will be in nanoseconds debug(3, "dacp_send_command: %f seconds, response code %d, command \"%s\".", - (1.0 * et) / 1000000, response.code, command); + (1.0 * et) / 1000000000, response.code, command); } *body = response.body; *bodysize = response.size; @@ -418,13 +416,11 @@ void set_dacp_server_information(rtsp_conn_info *conn) { // which return immediately with a 403 code if there are no changes. dacp_server.always_use_revision_number_1 = 0; char *p = strstr(conn->UserAgent, "forked-daapd"); - if ((p != 0) && (p == conn->UserAgent)) {// must exist and be at the start of the UserAgent string + if ((p != 0) && + (p == conn->UserAgent)) { // must exist and be at the start of the UserAgent string dacp_server.always_use_revision_number_1 = 1; } - - mdns_dacp_monitor_set_id(dacp_server.dacp_id); - metadata_hub_modify_prolog(); int ch = metadata_store.dacp_server_active != dacp_server.scan_enable; metadata_store.dacp_server_active = dacp_server.scan_enable; @@ -508,381 +504,418 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) { (metadata_store.advanced_dacp_server_active != 0); metadata_store.dacp_server_active = 0; metadata_store.advanced_dacp_server_active = 0; - debug(2, + debug(3, "setting metadata_store.dacp_server_active and " "metadata_store.advanced_dacp_server_active to 0 with an update " "flag value of %d", ch); metadata_hub_modify_epilog(ch); - while (dacp_server.scan_enable == 0) { - // debug(1, "dacp_monitor_thread_code wait for an event to possible enable scan"); - pthread_cond_wait(&dacp_server_information_cv, &dacp_server_information_lock); - // debug(1,"dacp_monitor_thread_code wake up."); + + uint64_t time_to_wait_for_wakeup_ns = + (uint64_t)(1000000000 * config.missing_port_dacp_scan_interval_seconds); + +#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD + uint64_t time_of_wakeup_ns = get_absolute_time_in_ns() + time_to_wait_for_wakeup_ns; + uint64_t sec = time_of_wakeup_ns / 1000000000; + uint64_t nsec = time_of_wakeup_ns % 1000000000; +#endif +#ifdef COMPILE_FOR_OSX + uint64_t sec = time_to_wait_for_wakeup_ns / 1000000000; + uint64_t nsec = time_to_wait_for_wakeup_ns % 1000000000; +#endif + + struct timespec time_to_wait; + time_to_wait.tv_sec = sec; + time_to_wait.tv_nsec = nsec; + + while ((dacp_server.scan_enable == 0) && (result != ETIMEDOUT)) { + // debug(1, "dacp_monitor_thread_code wait for an event to possibly enable scan"); + +#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD + result = pthread_cond_timedwait(&dacp_server_information_cv, &dacp_server_information_lock, + &time_to_wait); // this is a pthread cancellation point + // debug(1, "result is %d, dacp_server.scan_enable is %d, time_to_wait_for_wakeup_ns is %" + // PRId64 ".", result, dacp_server.scan_enable, time_to_wait_for_wakeup_ns); + +#endif +#ifdef COMPILE_FOR_OSX + result = pthread_cond_timedwait_relative_np(&dacp_server_information_cv, + &dacp_server_information_lock, &time_to_wait); +#endif + } + if (dacp_server.scan_enable == 1) { + bad_result_count = 0; + idle_scan_count = 0; } - // so dacp_server.scan_enable will be true at this point - bad_result_count = 0; - idle_scan_count = 0; } - always_use_revision_number_1 = dacp_server.always_use_revision_number_1; // set this while access is locked + always_use_revision_number_1 = + dacp_server.always_use_revision_number_1; // set this while access is locked result = dacp_get_volume(&the_volume); // just want the http code pthread_cleanup_pop(1); - scan_index++; - // debug(1,"DACP Scan Result: %d.", result); - - if ((result == 200) || (result == 400)) { - bad_result_count = 0; + if (result == 490) { // 490 means no port was specified + if (((char *)dacp_server.dacp_id != NULL) && (strlen(dacp_server.dacp_id) != 0)) { + // debug(1,"mdns_dacp_monitor_set_id"); + mdns_dacp_monitor_set_id(dacp_server.dacp_id); + } } else { - if (bad_result_count < config.scan_max_bad_response_count) // limit to some reasonable value - bad_result_count++; - } - - // here, do the debouncing calculations to see - // if the dacp server and the - // advanced dacp server are available + scan_index++; + // debug(1,"DACP Scan Result: %d.", result); - // -1 means we don't know because some bad statuses have been reported - // 0 means definitely no - // +1 means definitely yes - - int dacp_server_status_now = -1; - int advanced_dacp_server_status_now = -1; + if ((result == 200) || (result == 400)) { + bad_result_count = 0; + } else { + if (bad_result_count < config.scan_max_bad_response_count) // limit to some reasonable value + bad_result_count++; + } - if (bad_result_count == 0) { - dacp_server_status_now = 1; - if (result == 200) - advanced_dacp_server_status_now = 1; - else if (result == 400) + // here, do the debouncing calculations to see + // if the dacp server and the + // advanced dacp server are available + + // -1 means we don't know because some bad statuses have been reported + // 0 means definitely no + // +1 means definitely yes + + int dacp_server_status_now = -1; + int advanced_dacp_server_status_now = -1; + + if (bad_result_count == 0) { + dacp_server_status_now = 1; + if (result == 200) + advanced_dacp_server_status_now = 1; + else if (result == 400) + advanced_dacp_server_status_now = 0; + } else if (bad_result_count == + config.scan_max_bad_response_count) { // if a sequence of bad return codes occurs, + // then it's gone + dacp_server_status_now = 0; advanced_dacp_server_status_now = 0; - } else if (bad_result_count == - config.scan_max_bad_response_count) { // if a sequence of bad return codes occurs, - // then it's gone - dacp_server_status_now = 0; - advanced_dacp_server_status_now = 0; - } + } - if (metadata_store.player_thread_active == 0) - idle_scan_count++; - else - idle_scan_count = 0; + if (metadata_store.player_thread_active == 0) + idle_scan_count++; + else + idle_scan_count = 0; - debug(3, "Scan Result: %d, Bad Scan Count: %d, Idle Scan Count: %d.", result, bad_result_count, - idle_scan_count); + debug(3, "Scan Result: %d, Bad Scan Count: %d, Idle Scan Count: %d.", result, + bad_result_count, idle_scan_count); - /* not used - // decide if idle for too long - if (idle_scan_count == config.scan_max_inactive_count) { - debug(2, "DACP server status scanning stopped."); - dacp_server.scan_enable = 0; - } - */ + /* not used + // decide if idle for too long + if (idle_scan_count == config.scan_max_inactive_count) { + debug(2, "DACP server status scanning stopped."); + dacp_server.scan_enable = 0; + } + */ - int update_needed = 0; - metadata_hub_modify_prolog(); - if (dacp_server_status_now != -1) { // if dacp_server_status_now is actually known... - if (metadata_store.dacp_server_active != dacp_server_status_now) { - debug(2, "metadata_store.dacp_server_active set to %d.", dacp_server_status_now); - metadata_store.dacp_server_active = dacp_server_status_now; - update_needed = 1; + int update_needed = 0; + metadata_hub_modify_prolog(); + if (dacp_server_status_now != -1) { // if dacp_server_status_now is actually known... + if (metadata_store.dacp_server_active != dacp_server_status_now) { + debug(2, "metadata_store.dacp_server_active set to %d.", dacp_server_status_now); + metadata_store.dacp_server_active = dacp_server_status_now; + update_needed = 1; + } } - } - if (advanced_dacp_server_status_now != - -1) { // if advanced_dacp_server_status_now is actually known... - if (metadata_store.advanced_dacp_server_active != advanced_dacp_server_status_now) { - debug(2, "metadata_store.advanced_dacp_server_active set to %d.", dacp_server_status_now); - metadata_store.advanced_dacp_server_active = advanced_dacp_server_status_now; - update_needed = 1; + if (advanced_dacp_server_status_now != + -1) { // if advanced_dacp_server_status_now is actually known... + if (metadata_store.advanced_dacp_server_active != advanced_dacp_server_status_now) { + debug(2, "metadata_store.advanced_dacp_server_active set to %d.", dacp_server_status_now); + metadata_store.advanced_dacp_server_active = advanced_dacp_server_status_now; + update_needed = 1; + } } - } - metadata_hub_modify_epilog(update_needed); + metadata_hub_modify_epilog(update_needed); - // pthread_mutex_unlock(&dacp_server_information_lock); - // debug(1, "DACP Server ID \"%u\" at \"%s:%u\", scan %d.", dacp_server.active_remote_id, - // dacp_server.ip_string, dacp_server.port, scan_index); + // pthread_mutex_unlock(&dacp_server_information_lock); + // debug(1, "DACP Server ID \"%u\" at \"%s:%u\", scan %d.", dacp_server.active_remote_id, + // dacp_server.ip_string, dacp_server.port, scan_index); - if (result == 200) { - metadata_hub_modify_prolog(); - int diff = metadata_store.speaker_volume != the_volume; - if (diff) - metadata_store.speaker_volume = the_volume; - metadata_hub_modify_epilog(diff); - - ssize_t le; - char *response = NULL; - int32_t item_size; - char command[1024] = ""; - if (always_use_revision_number_1 != 0) // for forked-daapd - revision_number = 1; - snprintf(command, sizeof(command) - 1, "playstatusupdate?revision-number=%d", - revision_number); - // debug(1,"dacp_monitor_thread_code: command: \"%s\"",command); - result = dacp_send_command(command, &response, &le); - // debug(1,"Response to \"%s\" is %d.",command,result); - // remember: unless the revision_number you pass in is 1, - // response will be 200 only if there's something new to report. if (result == 200) { - // if (0) { - char *sp = response; - if (le >= 8) { - // here start looking for the contents of the status update - if (dacp_tlv_crawl(&sp, &item_size) == 'cmst') { // status - // here, we know that we are receiving playerstatusupdates, so set a flag - metadata_hub_modify_prolog(); - // debug(1, "playstatusupdate release track metadata"); - // metadata_hub_reset_track_metadata(); - // metadata_store.playerstatusupdates_are_received = 1; - sp -= item_size; // drop down into the array -- don't skip over it - le -= 8; - // char typestring[5]; - // we need to acquire the metadata data structure and possibly update it - while (le >= 8) { - uint32_t type = dacp_tlv_crawl(&sp, &item_size); - le -= item_size + 8; - char *t; - // char u; - // char *st; - int32_t r; - uint32_t ui; - // uint64_t v; - // int i; - - switch (type) { - case 'cmsr': // revision number - t = sp - item_size; - revision_number = ntohl(*(uint32_t *)(t)); - // debug(1,"New revision number received: %d", revision_number); - break; - case 'caps': // play status - t = sp - item_size; - r = *(unsigned char *)(t); - switch (r) { - case 2: - if (metadata_store.play_status != PS_STOPPED) { - metadata_store.play_status = PS_STOPPED; - debug(2, "Play status is \"stopped\"."); - } + metadata_hub_modify_prolog(); + int diff = metadata_store.speaker_volume != the_volume; + if (diff) + metadata_store.speaker_volume = the_volume; + metadata_hub_modify_epilog(diff); + + ssize_t le; + char *response = NULL; + int32_t item_size; + char command[1024] = ""; + if (always_use_revision_number_1 != 0) // for forked-daapd + revision_number = 1; + snprintf(command, sizeof(command) - 1, "playstatusupdate?revision-number=%d", + revision_number); + // debug(1,"dacp_monitor_thread_code: command: \"%s\"",command); + result = dacp_send_command(command, &response, &le); + // debug(1,"Response to \"%s\" is %d.",command,result); + // remember: unless the revision_number you pass in is 1, + // response will be 200 only if there's something new to report. + if (result == 200) { + // if (0) { + char *sp = response; + if (le >= 8) { + // here start looking for the contents of the status update + if (dacp_tlv_crawl(&sp, &item_size) == 'cmst') { // status + // here, we know that we are receiving playerstatusupdates, so set a flag + metadata_hub_modify_prolog(); + // debug(1, "playstatusupdate release track metadata"); + // metadata_hub_reset_track_metadata(); + // metadata_store.playerstatusupdates_are_received = 1; + sp -= item_size; // drop down into the array -- don't skip over it + le -= 8; + // char typestring[5]; + // we need to acquire the metadata data structure and possibly update it + while (le >= 8) { + uint32_t type = dacp_tlv_crawl(&sp, &item_size); + le -= item_size + 8; + char *t; + // char u; + // char *st; + int32_t r; + uint32_t ui; + // uint64_t v; + // int i; + + switch (type) { + case 'cmsr': // revision number + t = sp - item_size; + revision_number = ntohl(*(uint32_t *)(t)); + // debug(1,"New revision number received: %d", revision_number); break; - case 3: - if (metadata_store.play_status != PS_PAUSED) { - metadata_store.play_status = PS_PAUSED; - debug(2, "Play status is \"paused\"."); + case 'caps': // play status + t = sp - item_size; + r = *(unsigned char *)(t); + switch (r) { + case 2: + if (metadata_store.play_status != PS_STOPPED) { + metadata_store.play_status = PS_STOPPED; + debug(2, "Play status is \"stopped\"."); + } + break; + case 3: + if (metadata_store.play_status != PS_PAUSED) { + metadata_store.play_status = PS_PAUSED; + debug(2, "Play status is \"paused\"."); + } + break; + case 4: + if (metadata_store.play_status != PS_PLAYING) { + metadata_store.play_status = PS_PLAYING; + debug(2, "Play status changed to \"playing\"."); + } + break; + default: + debug(1, "Unrecognised play status %d received.", r); + break; } break; - case 4: - if (metadata_store.play_status != PS_PLAYING) { - metadata_store.play_status = PS_PLAYING; - debug(2, "Play status changed to \"playing\"."); + case 'cash': // shuffle status + t = sp - item_size; + r = *(unsigned char *)(t); + switch (r) { + case 0: + if (metadata_store.shuffle_status != SS_OFF) { + metadata_store.shuffle_status = SS_OFF; + debug(2, "Shuffle status is \"off\"."); + } + break; + case 1: + if (metadata_store.shuffle_status != SS_ON) { + metadata_store.shuffle_status = SS_ON; + debug(2, "Shuffle status is \"on\"."); + } + break; + default: + debug(1, "Unrecognised shuffle status %d received.", r); + break; } break; - default: - debug(1, "Unrecognised play status %d received.", r); + case 'carp': // repeat status + t = sp - item_size; + r = *(unsigned char *)(t); + switch (r) { + case 0: + if (metadata_store.repeat_status != RS_OFF) { + metadata_store.repeat_status = RS_OFF; + debug(2, "Repeat status is \"none\"."); + } + break; + case 1: + if (metadata_store.repeat_status != RS_ONE) { + metadata_store.repeat_status = RS_ONE; + debug(2, "Repeat status is \"one\"."); + } + break; + case 2: + if (metadata_store.repeat_status != RS_ALL) { + metadata_store.repeat_status = RS_ALL; + debug(2, "Repeat status is \"all\"."); + } + break; + default: + debug(1, "Unrecognised repeat status %d received.", r); + break; + } break; - } - break; - case 'cash': // shuffle status - t = sp - item_size; - r = *(unsigned char *)(t); - switch (r) { - case 0: - if (metadata_store.shuffle_status != SS_OFF) { - metadata_store.shuffle_status = SS_OFF; - debug(2, "Shuffle status is \"off\"."); + case 'cann': // track name + debug(2, "DACP Track Name seen"); + if (string_update_with_size(&metadata_store.track_name, + &metadata_store.track_name_changed, sp - item_size, + item_size)) { + debug(2, "DACP Track Name set to: \"%s\"", metadata_store.track_name); } break; - case 1: - if (metadata_store.shuffle_status != SS_ON) { - metadata_store.shuffle_status = SS_ON; - debug(2, "Shuffle status is \"on\"."); + case 'cana': // artist name + debug(2, "DACP Artist Name seen"); + if (string_update_with_size(&metadata_store.artist_name, + &metadata_store.artist_name_changed, sp - item_size, + item_size)) { + debug(2, "DACP Artist Name set to: \"%s\"", metadata_store.artist_name); } break; - default: - debug(1, "Unrecognised shuffle status %d received.", r); + case 'canl': // album name + debug(2, "DACP Album Name seen"); + if (string_update_with_size(&metadata_store.album_name, + &metadata_store.album_name_changed, sp - item_size, + item_size)) { + debug(2, "DACP Album Name set to: \"%s\"", metadata_store.album_name); + } break; - } - break; - case 'carp': // repeat status - t = sp - item_size; - r = *(unsigned char *)(t); - switch (r) { - case 0: - if (metadata_store.repeat_status != RS_OFF) { - metadata_store.repeat_status = RS_OFF; - debug(2, "Repeat status is \"none\"."); + case 'cang': // genre + debug(2, "DACP Genre seen"); + if (string_update_with_size(&metadata_store.genre, &metadata_store.genre_changed, + sp - item_size, item_size)) { + debug(2, "DACP Genre set to: \"%s\"", metadata_store.genre); } break; - case 1: - if (metadata_store.repeat_status != RS_ONE) { - metadata_store.repeat_status = RS_ONE; - debug(2, "Repeat status is \"one\"."); + case 'canp': // nowplaying 4 ids: dbid, plid, playlistItem, itemid (from mellowware + // see reference above) + debug(2, "DACP Composite ID seen"); + if (memcmp(metadata_store.item_composite_id, sp - item_size, + sizeof(metadata_store.item_composite_id)) != 0) { + memcpy(metadata_store.item_composite_id, sp - item_size, + sizeof(metadata_store.item_composite_id)); + char st[33]; + char *pt = st; + int it; + for (it = 0; it < 16; it++) { + snprintf(pt, 3, "%02X", metadata_store.item_composite_id[it]); + pt += 2; + } + *pt = 0; + debug(2, "Item composite ID changed to 0x%s.", st); + metadata_store.item_composite_id_changed = 1; } break; - case 2: - if (metadata_store.repeat_status != RS_ALL) { - metadata_store.repeat_status = RS_ALL; - debug(2, "Repeat status is \"all\"."); + case 'astm': + t = sp - item_size; + ui = ntohl(*(uint32_t *)(t)); + debug(2, "DACP Song Time seen: \"%u\" of length %u.", ui, item_size); + if (ui != metadata_store.songtime_in_milliseconds) { + metadata_store.songtime_in_milliseconds = ui; + metadata_store.songtime_in_milliseconds_changed = 1; + debug(2, "DACP Song Time set to: \"%u\"", + metadata_store.songtime_in_milliseconds); } break; + + /* + case 'mstt': + case 'cant': + case 'cast': + case 'cmmk': + case 'caas': + case 'caar': + t = sp - item_size; + r = ntohl(*(uint32_t *)(t)); + printf(" %d", r); + printf(" (0x"); + t = sp - item_size; + for (i = 0; i < item_size; i++) { + printf("%02x", *t & 0xff); + t++; + } + printf(")"); + break; + case 'asai': + t = sp - item_size; + s = ntohl(*(uint32_t *)(t)); + s = s << 32; + t += 4; + v = (ntohl(*(uint32_t *)(t))) & 0xffffffff; + s += v; + printf(" %lu", s); + printf(" (0x"); + t = sp - item_size; + for (i = 0; i < item_size; i++) { + printf("%02x", *t & 0xff); + t++; + } + printf(")"); + break; + */ default: - debug(1, "Unrecognised repeat status %d received.", r); + /* + printf(" 0x"); + t = sp - item_size; + for (i = 0; i < item_size; i++) { + printf("%02x", *t & 0xff); + t++; + } + */ break; } - break; - case 'cann': // track name - debug(2, "DACP Track Name seen"); - if (string_update_with_size(&metadata_store.track_name, - &metadata_store.track_name_changed, sp - item_size, - item_size)) { - debug(2, "DACP Track Name set to: \"%s\"", metadata_store.track_name); - } - break; - case 'cana': // artist name - debug(2, "DACP Artist Name seen"); - if (string_update_with_size(&metadata_store.artist_name, - &metadata_store.artist_name_changed, sp - item_size, - item_size)) { - debug(2, "DACP Artist Name set to: \"%s\"", metadata_store.artist_name); - } - break; - case 'canl': // album name - debug(2, "DACP Album Name seen"); - if (string_update_with_size(&metadata_store.album_name, - &metadata_store.album_name_changed, sp - item_size, - item_size)) { - debug(2, "DACP Album Name set to: \"%s\"", metadata_store.album_name); - } - break; - case 'cang': // genre - debug(2, "DACP Genre seen"); - if (string_update_with_size(&metadata_store.genre, &metadata_store.genre_changed, - sp - item_size, item_size)) { - debug(2, "DACP Genre set to: \"%s\"", metadata_store.genre); - } - break; - case 'canp': // nowplaying 4 ids: dbid, plid, playlistItem, itemid (from mellowware - // see reference above) - debug(2, "DACP Composite ID seen"); - if (memcmp(metadata_store.item_composite_id, sp - item_size, - sizeof(metadata_store.item_composite_id)) != 0) { - memcpy(metadata_store.item_composite_id, sp - item_size, - sizeof(metadata_store.item_composite_id)); - char st[33]; - char *pt = st; - int it; - for (it = 0; it < 16; it++) { - snprintf(pt, 3, "%02X", metadata_store.item_composite_id[it]); - pt += 2; - } - *pt = 0; - debug(2, "Item composite ID changed to 0x%s.", st); - metadata_store.item_composite_id_changed = 1; - } - break; - case 'astm': - t = sp - item_size; - ui = ntohl(*(uint32_t *)(t)); - debug(2, "DACP Song Time seen: \"%u\" of length %u.", ui, item_size); - if (ui != metadata_store.songtime_in_milliseconds) { - metadata_store.songtime_in_milliseconds = ui; - metadata_store.songtime_in_milliseconds_changed = 1; - debug(2, "DACP Song Time set to: \"%u\"", - metadata_store.songtime_in_milliseconds); - } - break; - - /* - case 'mstt': - case 'cant': - case 'cast': - case 'cmmk': - case 'caas': - case 'caar': - t = sp - item_size; - r = ntohl(*(uint32_t *)(t)); - printf(" %d", r); - printf(" (0x"); - t = sp - item_size; - for (i = 0; i < item_size; i++) { - printf("%02x", *t & 0xff); - t++; - } - printf(")"); - break; - case 'asai': - t = sp - item_size; - s = ntohl(*(uint32_t *)(t)); - s = s << 32; - t += 4; - v = (ntohl(*(uint32_t *)(t))) & 0xffffffff; - s += v; - printf(" %lu", s); - printf(" (0x"); - t = sp - item_size; - for (i = 0; i < item_size; i++) { - printf("%02x", *t & 0xff); - t++; - } - printf(")"); - break; - */ - default: - /* - printf(" 0x"); - t = sp - item_size; - for (i = 0; i < item_size; i++) { - printf("%02x", *t & 0xff); - t++; - } - */ - break; + // printf("\n"); } - // printf("\n"); - } - // finished possibly writing to the metadata hub - metadata_hub_modify_epilog( - 1); // should really see if this can be made responsive to changes + // finished possibly writing to the metadata hub + metadata_hub_modify_epilog( + 1); // should really see if this can be made responsive to changes + } else { + debug(1, "Status Update not found.\n"); + } } else { - debug(1, "Status Update not found.\n"); + debug(1, "Can't find any content in playerstatusupdate request"); } - } else { - debug(1, "Can't find any content in playerstatusupdate request"); - } - } /* else { - if (result != 403) - debug(1, "Unexpected response %d to playerstatusupdate request", result); - } */ + } /* else { + if (result != 403) + debug(1, "Unexpected response %d to playerstatusupdate request", result); + } */ + if (response) { + free(response); + response = NULL; + }; + }; + /* + strcpy(command,"nowplayingartwork?mw=320&mh=320"); + debug(1,"Command: \"%s\", result is %d",command, dacp_send_command(command, &response, &le)); if (response) { free(response); response = NULL; - }; - }; - /* - strcpy(command,"nowplayingartwork?mw=320&mh=320"); - debug(1,"Command: \"%s\", result is %d",command, dacp_send_command(command, &response, &le)); - if (response) { - free(response); - response = NULL; - } - strcpy(command,"getproperty?properties=dmcp.volume"); - debug(1,"Command: \"%s\", result is %d",command, dacp_send_command(command, &response, &le)); - if (response) { - free(response); - response = NULL; - } - strcpy(command,"setproperty?dmcp.volume=100.000000"); - debug(1,"Command: \"%s\", result is %d",command, dacp_send_command(command, &response, &le)); - if (response) { - free(response); - response = NULL; + } + strcpy(command,"getproperty?properties=dmcp.volume"); + debug(1,"Command: \"%s\", result is %d",command, dacp_send_command(command, &response, &le)); + if (response) { + free(response); + response = NULL; + } + strcpy(command,"setproperty?dmcp.volume=100.000000"); + debug(1,"Command: \"%s\", result is %d",command, dacp_send_command(command, &response, &le)); + if (response) { + free(response); + response = NULL; + } + */ + if (metadata_store.player_thread_active) + sleep(config.scan_interval_when_active); + else + sleep(config.scan_interval_when_inactive); } - */ - if (metadata_store.player_thread_active) - sleep(config.scan_interval_when_active); - else - sleep(config.scan_interval_when_inactive); } debug(1, "DACP monitor thread exiting -- should never happen."); pthread_exit(NULL); @@ -890,6 +923,20 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) { void dacp_monitor_start() { int rc; + +#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD + pthread_condattr_t attr; + pthread_condattr_init(&attr); + pthread_condattr_setclock(&attr, CLOCK_MONOTONIC); // can't do this in OS X, and don't need it. + rc = pthread_cond_init(&dacp_server_information_cv, &attr); + if (rc) + debug(1, "Error initialising the DACP Server Information Condition Variable"); + pthread_condattr_destroy(&attr); +#endif +#ifdef COMPILE_FOR_OSX + rc = pthread_cond_init(&dacp_server_information_cv, NULL); +#endif + pthread_mutexattr_t mta; rc = pthread_mutexattr_init(&mta); @@ -948,6 +995,8 @@ void dacp_monitor_stop() { pthread_mutex_destroy(&dacp_server_information_lock); debug(3, "DACP Conversation Lock Mutex Destroyed"); pthread_mutex_destroy(&dacp_conversation_lock); + pthread_cond_destroy(&dacp_server_information_cv); + debug(3, "DACP Server Information Condition Variable destroyed."); } } @@ -1200,7 +1249,7 @@ int dacp_get_volume(int32_t *the_actual_volume) { } else { debug(1, "Unexpected return code %d from dacp_get_speaker_list.", http_response); } - } else { + } else if (http_response != 400) { debug(3, "Unexpected return code %d from dacp_get_client_volume.", http_response); } if (the_actual_volume) { diff --git a/dbus-service.c b/dbus-service.c index 457aefed9..bbd5dcd45 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -24,10 +24,10 @@ * OTHER DEALINGS IN THE SOFTWARE. */ +#include #include #include #include -#include #include "config.h" @@ -69,8 +69,8 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) shairport_sync_remote_control_set_client(shairportSyncRemoteControlSkeleton, argc->client_ip); - - // although it's a DACP server, the server is in fact, part of the the AirPlay "client" (their term). + // although it's a DACP server, the server is in fact, part of the the AirPlay "client" (their + // term). if (argc->dacp_server_active) { shairport_sync_remote_control_set_available(shairportSyncRemoteControlSkeleton, TRUE); } else { @@ -166,7 +166,6 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) shairportSyncAdvancedRemoteControlSkeleton, response); } - switch (argc->shuffle_status) { case SS_NOT_AVAILABLE: new_status = FALSE; @@ -188,8 +187,8 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) // only set this if it's different if (current_status != new_status) { debug(3, "Shuffle State should be changed"); - shairport_sync_advanced_remote_control_set_shuffle( - shairportSyncAdvancedRemoteControlSkeleton, new_status); + shairport_sync_advanced_remote_control_set_shuffle(shairportSyncAdvancedRemoteControlSkeleton, + new_status); } // Build the metadata array @@ -369,8 +368,9 @@ static gboolean on_handle_volume_down(ShairportSyncRemoteControl *skeleton, } static gboolean on_handle_set_airplay_volume(ShairportSyncRemoteControl *skeleton, - GDBusMethodInvocation *invocation, const gdouble volume, - __attribute__((unused)) gpointer user_data) { + GDBusMethodInvocation *invocation, + const gdouble volume, + __attribute__((unused)) gpointer user_data) { debug(2, "Set airplay volume to %.6f.", volume); char command[256] = ""; snprintf(command, sizeof(command), "setproperty?dmcp.device-volume=%.6f", volume); @@ -379,8 +379,6 @@ static gboolean on_handle_set_airplay_volume(ShairportSyncRemoteControl *skeleto return TRUE; } - - gboolean notify_elapsed_time_callback(ShairportSyncDiagnostics *skeleton, __attribute__((unused)) gpointer user_data) { // debug(1, "\"notify_elapsed_time_callback\" called."); @@ -408,7 +406,7 @@ gboolean notify_delta_time_callback(ShairportSyncDiagnostics *skeleton, } gboolean notify_file_and_line_callback(ShairportSyncDiagnostics *skeleton, - __attribute__((unused)) gpointer user_data) { + __attribute__((unused)) gpointer user_data) { // debug(1, "\"notify_file_and_line_callback\" called."); if (shairport_sync_diagnostics_get_file_and_line(skeleton)) { config.debugger_show_file_and_line = 1; @@ -463,12 +461,13 @@ gboolean notify_disable_standby_callback(ShairportSync *skeleton, #ifdef CONFIG_CONVOLUTION gboolean notify_convolution_callback(ShairportSync *skeleton, - __attribute__((unused)) gpointer user_data) { + __attribute__((unused)) gpointer user_data) { // debug(1, "\"notify_convolution_callback\" called."); if (shairport_sync_get_convolution(skeleton)) { debug(1, ">> activating convolution"); config.convolution = 1; - config.convolver_valid = convolver_init(config.convolution_ir_file, config.convolution_max_length); + config.convolver_valid = + convolver_init(config.convolution_ir_file, config.convolution_max_length); } else { debug(1, ">> deactivating convolution"); config.convolution = 0; @@ -477,7 +476,7 @@ gboolean notify_convolution_callback(ShairportSync *skeleton, } #else gboolean notify_convolution_callback(__attribute__((unused)) ShairportSync *skeleton, - __attribute__((unused)) gpointer user_data) { + __attribute__((unused)) gpointer user_data) { warn(">> Convolution support is not built in to this build of Shairport Sync."); return TRUE; } @@ -485,7 +484,7 @@ gboolean notify_convolution_callback(__attribute__((unused)) ShairportSync *skel #ifdef CONFIG_CONVOLUTION gboolean notify_convolution_gain_callback(ShairportSync *skeleton, - __attribute__((unused)) gpointer user_data) { + __attribute__((unused)) gpointer user_data) { gdouble th = shairport_sync_get_convolution_gain(skeleton); if ((th <= 0.0) && (th >= -100.0)) { @@ -499,34 +498,38 @@ gboolean notify_convolution_gain_callback(ShairportSync *skeleton, } #else gboolean notify_convolution_gain_callback(__attribute__((unused)) ShairportSync *skeleton, - __attribute__((unused)) gpointer user_data) { + __attribute__((unused)) gpointer user_data) { warn(">> Convolution support is not built in to this build of Shairport Sync."); return TRUE; } #endif #ifdef CONFIG_CONVOLUTION gboolean notify_convolution_impulse_response_file_callback(ShairportSync *skeleton, - __attribute__((unused)) gpointer user_data) { + __attribute__((unused)) + gpointer user_data) { char *th = (char *)shairport_sync_get_convolution_impulse_response_file(skeleton); if (config.convolution_ir_file) free(config.convolution_ir_file); config.convolution_ir_file = strdup(th); - debug(1, ">> setting configuration impulse response filter file to \"%s\".", config.convolution_ir_file); - config.convolver_valid = convolver_init(config.convolution_ir_file, config.convolution_max_length); + debug(1, ">> setting configuration impulse response filter file to \"%s\".", + config.convolution_ir_file); + config.convolver_valid = + convolver_init(config.convolution_ir_file, config.convolution_max_length); return TRUE; } #else -gboolean notify_convolution_impulse_response_file_callback(__attribute__((unused)) ShairportSync *skeleton, - __attribute__((unused)) gpointer user_data) { - __attribute__((unused)) char *th = (char *)shairport_sync_get_convolution_impulse_response_file(skeleton); +gboolean notify_convolution_impulse_response_file_callback(__attribute__((unused)) + ShairportSync *skeleton, + __attribute__((unused)) + gpointer user_data) { + __attribute__((unused)) char *th = + (char *)shairport_sync_get_convolution_impulse_response_file(skeleton); return TRUE; } #endif - - gboolean notify_loudness_callback(ShairportSync *skeleton, - __attribute__((unused)) gpointer user_data) { + __attribute__((unused)) gpointer user_data) { // debug(1, "\"notify_loudness_callback\" called."); if (shairport_sync_get_loudness(skeleton)) { debug(1, ">> activating loudness"); @@ -781,7 +784,6 @@ static gboolean on_handle_remote_command(ShairportSync *skeleton, GDBusMethodInv return TRUE; } - static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name, __attribute__((unused)) gpointer user_data) { @@ -823,8 +825,8 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name G_CALLBACK(notify_convolution_gain_callback), NULL); g_signal_connect(shairportSyncSkeleton, "notify::convolution-impulse-response-file", G_CALLBACK(notify_convolution_impulse_response_file_callback), NULL); - g_signal_connect(shairportSyncSkeleton, "notify::loudness", - G_CALLBACK(notify_loudness_callback), NULL); + g_signal_connect(shairportSyncSkeleton, "notify::loudness", G_CALLBACK(notify_loudness_callback), + NULL); g_signal_connect(shairportSyncSkeleton, "notify::loudness-threshold", G_CALLBACK(notify_loudness_threshold_callback), NULL); g_signal_connect(shairportSyncSkeleton, "notify::drift-tolerance", @@ -879,7 +881,6 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name g_signal_connect(shairportSyncRemoteControlSkeleton, "handle-set-airplay-volume", G_CALLBACK(on_handle_set_airplay_volume), NULL); - g_signal_connect(shairportSyncAdvancedRemoteControlSkeleton, "handle-set-volume", G_CALLBACK(on_handle_set_volume), NULL); @@ -975,9 +976,11 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name shairport_sync_set_convolution(SHAIRPORT_SYNC(shairportSyncSkeleton), TRUE); } if (config.convolution_ir_file) - shairport_sync_set_convolution_impulse_response_file(SHAIRPORT_SYNC(shairportSyncSkeleton), config.convolution_ir_file); + shairport_sync_set_convolution_impulse_response_file(SHAIRPORT_SYNC(shairportSyncSkeleton), + config.convolution_ir_file); // else -// shairport_sync_set_convolution_impulse_response_file(SHAIRPORT_SYNC(shairportSyncSkeleton), NULL); +// shairport_sync_set_convolution_impulse_response_file(SHAIRPORT_SYNC(shairportSyncSkeleton), +// NULL); #endif shairport_sync_set_version(SHAIRPORT_SYNC(shairportSyncSkeleton), PACKAGE_VERSION); @@ -1047,14 +1050,14 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name static void on_dbus_name_lost_again(__attribute__((unused)) GDBusConnection *connection, __attribute__((unused)) const gchar *name, __attribute__((unused)) gpointer user_data) { - warn("Could not acquire a Shairport Sync native D-Bus interface \"%s\" on the %s bus.", name, + warn("could not acquire a Shairport Sync native D-Bus interface \"%s\" on the %s bus.", name, (config.dbus_service_bus_type == DBT_session) ? "session" : "system"); } static void on_dbus_name_lost(__attribute__((unused)) GDBusConnection *connection, __attribute__((unused)) const gchar *name, __attribute__((unused)) gpointer user_data) { - // debug(1, "Could not acquire a Shairport Sync native D-Bus interface \"%s\" on the %s bus -- + // debug(1, "could not acquire a Shairport Sync native D-Bus interface \"%s\" on the %s bus -- // will try adding the process " // "number to the end of it.", // name, (config.dbus_service_bus_type == DBT_session) ? "session" : "system"); diff --git a/man/shairport-sync.7 b/man/shairport-sync.7 index 9048b5510..7446662cc 100644 --- a/man/shairport-sync.7 +++ b/man/shairport-sync.7 @@ -35,8 +35,6 @@ Within the configuration file, settings are organised into groups, for example, \fBname = "Mike's Boombox";\f1 -\fBinterpolation = "soxr";\f1 - \fBpassword = "secret";\f1 \fBoutput_backend = "alsa";\f1 @@ -53,7 +51,7 @@ Within the configuration file, settings are organised into groups, for example, \fB};\f1 -Most settings have sensible default values, so -- as in the example above -- users generally only need to set (1) the service name, (2) a password (if desired) and (3) the output device. If the output device has a mixer that can be used for volume control, then (4) the volume control's name should be specified. It is highly desirable to use the output device's mixer for volume control, if available -- response time is reduced to zero and the processor load is reduced. In the example above, "soxr" interpolation was also enabled. +Most settings have sensible default values, so -- as in the example above -- users generally only need to set (1) the service name, (2) a password (if desired) and (3) the output device. If the output device has a mixer that can be used for volume control, then (4) the mixer name should be specified. It is important to do this if the mixer exists. Otherwise, the maximum output from the output device will be whatever setting the mixer happens to have, which will be a matter of chance and which could be very low or even silent. A sample configuration file with all possible settings, but with all of them commented out, is installed at \fIshairport-sync.conf.sample\f1, within the System Configuration Directory -- \fI/etc\f1 in Linux, \fI/usr/local/etc\f1 in BSD unixes. @@ -67,7 +65,7 @@ These are the settings available within the \fBgeneral\f1 group: \fBname=\f1\fI"service_name"\f1\fB;\f1 Use this \fIservice_name\f1 to identify this player in iTunes, etc. -The following substitutions are allowed: \fB%h\f1 for the computer's hostname, \fB%H\f1 for the computer's hostname with the first letter capitalised (ASCII only), \fB%v\f1 for the shairport-sync version number, e.g. "3.0.1" and \fB%V\f1 for the shairport-sync version string, e.g. "3.0.1-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc". +The following substitutions are allowed: \fB%h\f1 for the computer's hostname, \fB%H\f1 for the computer's hostname with the first letter capitalised (ASCII only), \fB%v\f1 for the shairport-sync version number, e.g. "3.3.6" and \fB%V\f1 for the shairport-sync version string, e.g. "3.3.6-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc". The default is "%H", which is replaced by the hostname with the first letter capitalised. .TP @@ -75,7 +73,7 @@ The default is "%H", which is replaced by the hostname with the first letter cap Require the password \fIpassword\f1 to connect to the service. If you leave this setting commented out, no password is needed. .TP \fBinterpolation=\f1\fI"mode"\f1\fB;\f1 -Interpolate, or "stuff", the audio stream using the \fImode\f1. Interpolation here refers to the process of adding or removing frames of audio to or from the stream sent to the output device to keep it exactly in synchrony with the player. The default mode, "basic", is normally almost completely inaudible. The alternative mode, "soxr", is even less obtrusive but requires much more processing power. For this mode, support for libsoxr, the SoX Resampler Library, must be selected when shairport-sync is compiled. +Interpolate, or "stuff", the audio stream using the \fImode\f1. Interpolation here refers to the process of adding or removing frames of audio to or from the stream sent to the output device to keep it exactly synchronised with the player. The "basic" mode is normally almost completely inaudible. The alternative mode, "soxr", is even less obtrusive but requires much more processing power. For this mode, support for libsoxr, the SoX Resampler Library, must be selected when shairport-sync is compiled. The default setting is "auto", which will choose "soxr" if support for it has been compiled into the build of Shairport Synce and if the CPU is fast enough. Otherwise, "basic" stuffing will be chosen. .TP \fBoutput_backend=\f1\fI"backend"\f1\fB;\f1 shairport-sync has a number of modules of code ("backends") through which audio is output. Normally, the first audio backend that works is selected. This setting forces the selection of the specific audio \fIbackend\f1. Perform the command \fBshairport-sync -h\f1 to get a list of available audio backends -- the default is the first on this list. Only the "alsa", "sndio" and "pa" backends support synchronisation. @@ -218,14 +216,14 @@ Specify the \fIname\f1 of the mixer control to be used by shairport-sync to cont By default, the mixer is assumed to be output_device. Use this setting to specify a device other than the output device. .TP \fBoutput_rate=\f1\fIframe rate\f1\fB;\f1 -Use this setting to specify the frame rate to output to the ALSA device. Allowable values are 44100 (default), 88200, 176400 and 352800. The device must have the capability to accept the format you specify. There is no particular reason to use anything other than 44100 if it is available. +Use this setting to specify the frame rate to output to the ALSA device. Allowable values are "auto" (default), 44100, 88200, 176400 and 352800. The device must have the capability to accept the rate you specify. There is no particular reason to use anything other than 44100 if it is available, and if "auto" is selected, the lowest of these rates available, starting at 44100, will be selected. .TP \fBoutput_format=\f1\fI"format"\f1\fB;\f1 -Use this setting to specify the format that should be used to send data to the ALSA device. Allowable values are "U8", "S8", "S16", "S24", "S24_3LE", "S24_3BE" or "S32". The device must have the capability to accept the format you specify. +Use this setting to specify the format that should be used to send data to the ALSA device. Allowable values are "auto" (default), "U8", "S8", "S16", "S24", "S24_3LE", "S24_3BE" or "S32". The device must have the capability to accept the format you specify. "S" means signed; "U" means unsigned; BE means big-endian and LE means little-endian. Except where stated (using *LE or *BE), endianness matches that of the processor. The default is "S16". -If you are using a hardware mixer, the best setting is S16, as audio will pass through Shairport Sync unmodified except for interpolation. If you are using the software mixer, use 32- or 24-bit, if your device is capable of it, to get the lowest possible levels of dither. +If you are using a hardware mixer, S16 is fine, as audio will pass through Shairport Sync unmodified except for interpolation, but any of the higher-resolution formats are okay too. If you are using the software mixer, use 32- or 24-bit, if your device is capable of it, in order to get the lowest possible levels of dither. The "auto" setting will cause Shairport Sync to choose the highest resolution available. .TP \fBdisable_synchronization=\f1\fI"no"\f1\fB;\f1 This is an advanced setting and is for debugging only. Set to \fI"yes"\f1 to disable synchronization. Default is \fI"no"\f1. If you use it to disable synchronisation, then sooner or later you'll experience audio glitches due to audio buffer overflow or underflow. @@ -326,10 +324,10 @@ The maximum packet size for any UDP metadata. This must be between 500 or 65000. \fB"DIAGNOSTICS" SETTINGS\f1 .TP \fBstatistics=\f1\fI"setting"\f1\fB;\f1 -Use this \fIsetting\f1 to enable ("yes") or disable ("no") the output of some statistical information on the console or in the log. The default is to disable statistics. +Use this \fIsetting\f1 to enable ("yes") or disable ("no") the output of some statistical information to the system log (or to \fISTDERR\f1 if the \fB-u\f1 command line option is chosen). The default is to disable statistics. .TP \fBlog_verbosity=\f1\fI0\f1\fB;\f1 -Use this to specify how much debugging information should be output or logged. The value \fI0\f1 means no debug information, \fI3\f1 means most debug information. The default is \fI0\f1. +Use this to specify how much debugging information should sent to the system log (or to \fISTDERR\f1 if the \fB-u\f1 command line option is chosen). The value \fI0\f1 means no debug information, \fI3\f1 means most debug information. The default is \fI0\f1. .SH OPTIONS This section is about the command-line options available in shairport-sync. @@ -425,7 +423,7 @@ If you are running shairport-sync from the command line and want logs to appear Print version information and exit. .TP \fB-v | --verbose\f1 -Print debug information. Repeat up to three times to get more detail. +Print debug information to the system log, or or to \fISTDERR\f1 if the \fB-u\f1 command line option is also chosen. Repeat up to three times to get more detail. .TP \fB-w | --wait-cmd\f1 Wait for commands specified using \fB-B\f1 or \fB-E\f1 to complete before continuing execution. diff --git a/man/shairport-sync.7.xml b/man/shairport-sync.7.xml index 93b24146d..e5dc1c568 100644 --- a/man/shairport-sync.7.xml +++ b/man/shairport-sync.7.xml @@ -7,7 +7,7 @@ Copyright (c) Mike Brady 2014-2019 All rights reserved. - + 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 @@ -15,10 +15,10 @@ 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 @@ -61,64 +61,63 @@ -

Shairport Sync plays - audio streamed from iTunes - or from an AirPlay device to an ALSA-compatible audio output device (available on - Linux and FreeBSD), to a "sndio" output device (available on OpenBSD, FreeBSD and +

Shairport Sync plays + audio streamed from iTunes + or from an AirPlay device to an ALSA-compatible audio output device (available on + Linux and FreeBSD), to a "sndio" output device (available on OpenBSD, FreeBSD and Linux), to a PulseAudio output stream or to Jack Audio.

- +

Shairport Sync offers full audio synchronisation. - Full audio synchronisation means that audio is played on the output device at exactly + Full audio synchronisation means that audio is played on the output device at exactly the time specified by the audio source. This means that if many devices are playing the same stream at the same time, all the outputs will stay in synchrony with one another. - This allows multiple devices to play the same source without getting out of step with + This allows multiple devices to play the same source without getting out of step with one another, enabling, for example, simultaneous multi-room operation.

- -

Shairport Sync can stream synchronised audio to a unix - pipe or to standard output, or to audio systems that do not provide timing - information. This could perhaps be described as partial audio synchronisation, where - synchronised audio is provided by Shairport Sync, but what happens to it in the - subsequent processing chain, before it reaches the listener's ear, is outside the + +

Shairport Sync can stream synchronised audio to a unix + pipe or to standard output, or to audio systems that do not provide timing + information. This could perhaps be described as partial audio synchronisation, where + synchronised audio is provided by Shairport Sync, but what happens to it in the + subsequent processing chain, before it reaches the listener's ear, is outside the control of shairport-sync.

-

Shairport Sync can be compiled to stream metadata, including cover art, to a pipe +

Shairport Sync can be compiled to stream metadata, including cover art, to a pipe or socket.

-

Shairport Sync can be compiled to offer a standard MPRIS interface, a "native" - D-Bus interface and an MQTT client interface. Through these interfaces, it can provide +

Shairport Sync can be compiled to offer a standard MPRIS interface, a "native" + D-Bus interface and an MQTT client interface. Through these interfaces, it can provide metadata, including cover art, and can offer remote control of the audio source.

- -

Settings can be made using the configuration file (recommended for all new + +

Settings can be made using the configuration file (recommended for all new installations) or by using command-line options.

- +

The name of the Shairport Sync executable is shairport-sync. Both names are used in these man pages.

- +
- - + +

You should use the configuration file for setting up Shairport Sync. This file is usually shairport-sync.conf and is generally located in the - System Configuration Directory, which is normally the /etc directory in + System Configuration Directory, which is normally the /etc directory in Linux or the /usr/local/etc directory in BSD unixes. You may need to have root privileges to modify it.

- -

(Note: Shairport Sync may have been compiled to use a different configuration - directory. You can determine which by performing the command $ shairport-sync - -V. One of the items in the output string is the value of the + +

(Note: Shairport Sync may have been compiled to use a different configuration + directory. You can determine which by performing the command $ shairport-sync + -V. One of the items in the output string is the value of the sysconfdir, i.e. the System Configuration Directory.)

- - + +

Within the configuration file, settings are organised into groups, for example, there is a "general" group of standard settings, and there is an "alsa" group with settings that pertain to the ALSA back end. Here is an example of a typical configuration file:

- +

general = {

name = "Mike's Boombox";

-

interpolation = "soxr";

password = "secret";

output_backend = "alsa";

};

@@ -127,20 +126,20 @@

output_device = "hw:0";

mixer_control_name = "PCM";

};

- -

Most settings have sensible default values, so -- as in the example above -- users + +

Most settings have sensible default values, so -- as in the example above -- users generally only need to set (1) the service name, (2) a password (if desired) and - (3) the output device. If the output device has a mixer that can be used for volume - control, then (4) the volume control's name should be specified. It is highly - desirable to use the output device's mixer for volume control, if available -- - response time is reduced to zero and the processor load is reduced. In the example - above, "soxr" interpolation was also enabled.

- -

A sample configuration file with all possible settings, but with all of them - commented out, is installed at shairport-sync.conf.sample, within the - System Configuration Directory -- /etc in Linux, + (3) the output device. If the output device has a mixer that can be used for volume + control, then (4) the mixer name should be specified. It is important + to do this if the mixer exists. Otherwise, the + maximum output from the output device will be whatever setting the mixer happens to + have, which will be a matter of chance and which could be very low or even silent.

+ +

A sample configuration file with all possible settings, but with all of them + commented out, is installed at shairport-sync.conf.sample, within the + System Configuration Directory -- /etc in Linux, /usr/local/etc in BSD unixes.

- +

To retain backwards compatibility with previous versions of shairport-sync you can use still use command line options, but any new features, etc. will be available only via configuration file settings.

@@ -157,88 +156,92 @@

Use this service_name to identify this player in iTunes, etc.

The following substitutions are allowed: %h for the computer's hostname, - %H for the computer's hostname with the first letter capitalised (ASCII + %H for the computer's hostname with the first letter capitalised (ASCII only), - %v for the shairport-sync version number, e.g. "3.0.1" and - %V for the shairport-sync version string, e.g. - "3.0.1-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc".

-

The default is "%H", which is replaced by the hostname with the first letter + %v for the shairport-sync version number, e.g. "3.3.6" and + %V for the shairport-sync version string, e.g. + "3.3.6-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc".

+

The default is "%H", which is replaced by the hostname with the first letter capitalised.

- + - + @@ -256,71 +259,71 @@ - + - + - - + - + - + @@ -401,25 +404,25 @@ @@ -427,7 +430,7 @@ @@ -435,207 +438,210 @@ - + - + -

These settings are for the ALSA back end, used to communicate with audio output - devices in the ALSA system. (By the way, you can use tools such as +

These settings are for the ALSA back end, used to communicate with audio output + devices in the ALSA system. (By the way, you can use tools such as alsamixer or aplay to discover what devices are available.) - Use these settings to select the output device and the mixer control to be used to + Use these settings to select the output device and the mixer control to be used to control the output volume. - You can additionally set the desired size of the output buffer and you can adjust + You can additionally set the desired size of the output buffer and you can adjust overall latency. Here are the alsa group settings:

- + - + -

These settings are for the SNDIO back end, used to communicate with audio output +

These settings are for the SNDIO back end, used to communicate with audio output devices in the SNDIO system.

@@ -712,113 +718,114 @@ -

These settings are for the PIPE backend, used to route audio to a named unix pipe. - The audio is in raw CD audio format: PCM 16 bit little endian, 44,100 samples per +

These settings are for the PIPE backend, used to route audio to a named unix pipe. + The audio is in raw CD audio format: PCM 16 bit little endian, 44,100 samples per second, interleaved stereo.

- +

There are no settings for the STDOUT backend.

- +

There are no configuration file settings for the AO backend.

-

shairport-sync can process metadata provided by the source, such as Track Number, +

shairport-sync can process metadata provided by the source, such as Track Number, Album Name, cover art, etc. and can provide additional metadata such as volume level, - pause/resume, etc. It sends the metadata to a pipe, by default + pause/resume, etc. It sends the metadata to a pipe, by default /tmp/shairport-sync-metadata. - To process metadata, shairport-sync must have been compiled with metadata support + To process metadata, shairport-sync must have been compiled with metadata support included. - You can check that this is so by running the command $ shairport-sync -V; + You can check that this is so by running the command $ shairport-sync -V; the identification string will contain the word metadata.

-

Please note that different sources provide different levels of metadata. Some +

Please note that different sources provide different levels of metadata. Some provide a lot; some provide almost none.

-

The metadata group of settings allow you to enable metadata handling and +

The metadata group of settings allow you to enable metadata handling and to control certain aspects of it:

- + - +

This section is about the command-line options available in shairport-sync.

-

Note: if you are setting up shairport-sync for the first time or are updating an - existing installation, you are encouraged to use the configuration file settings +

Note: if you are setting up shairport-sync for the first time or are updating an + existing installation, you are encouraged to use the configuration file settings described above. Most of the command-line options described below - simply replicate the configuration settings and are retained to provide backward + simply replicate the configuration settings and are retained to provide backward compatibility with older installations of shairport-sync.

- +

Many command-line options take sensible default values, so you can normally ignore most of them. See the EXAMPLES section for typical usages.

@@ -827,25 +834,25 @@ Program options are always listed first, followed by any audio backend options, preceded by a -- symbol.

- +

These command-line options are used by shairport-sync itself.

- + @@ -853,11 +860,11 @@ @@ -889,7 +896,7 @@ @@ -1010,7 +1017,7 @@ If the output timing differs from the source timing by more than the threshold, output will be muted and a full resynchronisation will occur. The default threshold is 2,205 frames, i.e. 50 - milliseconds. Specify 0 to disable resynchronisation. This setting is + milliseconds. Specify 0 to disable resynchronisation. This setting is deprecated and will be removed in a future version of shairport-sync.

@@ -1056,13 +1063,13 @@ @@ -1085,7 +1092,8 @@ @@ -1098,23 +1106,23 @@
-

These command-line options are passed to the chosen audio backend. The audio +

These command-line options are passed to the chosen audio backend. The audio backend options are preceded by a -- symbol to introduce them and to separate them from any program options. In this way, option letters can be used as program options and also as audio backend options without ambiguity.

- +

In the ALSA backend, audio is sent to an output device which you can specify using the -d option. - The output level (the "volume") is controlled using a level control associated with a + The output level (the "volume") is controlled using a level control associated with a mixer. By default, the mixer is implemented in shairport-sync itself in software. - To use a hardware level control on a mixer on the sound card, specify the name of the + To use a hardware level control on a mixer on the sound card, specify the name of the mixer control with the -c option. - If the mixer is not associated with the output device, then you need to specify where + If the mixer is not associated with the output device, then you need to specify where the mixer is to be found with the -m option.

- + @@ -1168,7 +1176,7 @@ -- -d hw:1,0 -m hw:1 - -c PCM + -c PCM

The program will run in daemon mode ( -d ), will be visible as "Joe's Stereo" ( -a "Joe's Stereo" ) and will use the SoX Resampler @@ -1178,17 +1186,17 @@ ( -d hw:1,0 ) and will take advantage of the same sound card's mixer ( -m hw:1 ) using the level control named "PCM" ( -c "PCM" ).

-

The example above is slightly contrived in order to show the use of the -m - option. Typically, output 0 is the default output of a card, so the output device could +

The example above is slightly contrived in order to show the use of the -m + option. Typically, output 0 is the default output of a card, so the output device could be written -d hw:1 and then the mixer option would be unnecessary, giving the following, simpler, command:

shairport-sync -d -a "Joe's Stereo" -S soxr -- -d hw:1 - -c PCM + -c PCM - +
diff --git a/man/shairport-sync.html b/man/shairport-sync.html index c16a56ea0..5d51b3c0a 100644 --- a/man/shairport-sync.html +++ b/man/shairport-sync.html @@ -1,1199 +1,28 @@

shairport-sync

Synchronised Audio Player for iTunes / AirPlay

- - -

Synopsis

- - - shairport-sync [-djvuw] - [-a name] - [-A latency] - [-B command] - [-c configurationfile] - [-E command] - [--get-cover-art] - [--logOutputLevel] - [-L latency] - [-m backend] - [--meta-dir=directory] - [-o backend] - [--password=secret] - [-r threshold] - [--statistics] - [-S mode] - [-t timeout] - [--tolerance=frames] - [-- audio_backend_options] -
- - shairport-sync -k
- - shairport-sync -h
- - shairport-sync -V
- -
- - -

Description

- -

Shairport Sync plays - audio streamed from iTunes - or from an AirPlay device to an ALSA-compatible audio output device (available on - Linux and FreeBSD), to a "sndio" output device (available on OpenBSD, FreeBSD and - Linux), to a PulseAudio output stream or to Jack Audio.

- -

Shairport Sync offers full audio synchronisation. - Full audio synchronisation means that audio is played on the output device at exactly - the time specified by the audio source. - This means that if many devices are playing the same stream at the same - time, all the outputs will stay in synchrony with one another. - This allows multiple devices to play the same source without getting out of step with - one another, enabling, for example, simultaneous multi-room operation. -

- -

Shairport Sync can stream synchronised audio to a unix - pipe or to standard output, or to audio systems that do not provide timing - information. This could perhaps be described as partial audio synchronisation, where - synchronised audio is provided by Shairport Sync, but what happens to it in the - subsequent processing chain, before it reaches the listener's ear, is outside the - control of shairport-sync.

-

Shairport Sync can be compiled to stream metadata, including cover art, to a pipe - or socket.

-

Shairport Sync can be compiled to offer a standard MPRIS interface, a "native" - D-Bus interface and an MQTT client interface. Through these interfaces, it can provide - metadata, including cover art, and can offer remote control of the audio source.

- -

Settings can be made using the configuration file (recommended for all new - installations) or by using command-line options.

- -

The name of the Shairport Sync executable is shairport-sync. - Both names are used in these man pages.

- - - - - -

Configuration File Settings

- -

You should use the configuration file for setting up Shairport Sync. - This file is usually shairport-sync.conf and is generally located in the - System Configuration Directory, which is normally the /etc directory in - Linux or the /usr/local/etc directory in BSD unixes. - You may need to have root privileges to modify it.

- -

(Note: Shairport Sync may have been compiled to use a different configuration - directory. You can determine which by performing the command $ shairport-sync - -V. One of the items in the output string is the value of the - sysconfdir, - i.e. the System Configuration Directory.)

- - -

Within the configuraton file, settings are organised into groups, for - example, there is a "general" group of - standard settings, and there is an "alsa" group with settings that pertain to the ALSA - back end. Here is an example of a typical configuration file:

- -

general = {

-

name = "Mike's Boombox";

-

interpolation = "soxr";

-

password = "secret";

-

output_backend = "alsa";

-

};

-

-

alsa = {

-

output_device = "hw:0";

-

mixer_control_name = "PCM";

-

};

- -

Most settings have sensible default values, so -- as in the example above -- users - generally only need to set (1) the service name, (2) a password (if desired) and - (3) the output device. If the output device has a mixer that can be used for volume - control, then (4) the volume control's name should be specified. It is highly - desirable to use the output device's mixer for volume control, if available -- - response time is reduced to zero and the processor load is reduced. In the example - above, "soxr" interpolation was also enabled.

- -

A sample configuration file with all possible settings, but with all of them - commented out, is installed at shairport-sync.conf.sample, within the - System Configuration Directory -- /etc in Linux, - /usr/local/etc in BSD unixes.

- -

To retain backwards compatibility with previous versions of shairport-sync - you can use still use command line options, but any new features, etc. will - be available only via configuration file settings.

- -

The configuration file is processed using the libconfig library - -- see http://www.hyperrealm.com/libconfig/libconfig_manual.html.

- -

"GENERAL" SETTINGS

-

These are the settings available within the general group:

- - -

name="service_name";

- -

Use this service_name to identify this player in iTunes, etc.

-

The following substitutions are allowed: - %h for the computer's hostname, - %H for the computer's hostname with the first letter capitalised (ASCII - only), - %v for the shairport-sync version number, e.g. "3.0.1" and - %V for the shairport-sync version string, e.g. - "3.0.1-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc".

-

The default is "%H", which is replaced by the hostname with the first letter - capitalised.

- - - - -

password="password";

-

Require the password password to connect to the service. If you - leave this setting commented out, no password is needed.

- - - -

interpolation="mode";

-

Interpolate, or "stuff", the audio stream using the mode. - Interpolation here refers to the - process of adding or removing frames of audio to or from the - stream sent to the output device to keep it exactly in synchrony - with the player. - The default mode, "basic", is normally almost completely inaudible. - The alternative mode, "soxr", is even less obtrusive but - requires much more processing power. For this mode, support for - libsoxr, the SoX Resampler Library, must be selected when - shairport-sync is compiled. -

- - - -

output_backend="backend";

-

shairport-sync has a number of modules of code ("backends") through which - audio is output. Normally, the first audio backend that works is selected. This - setting forces the selection of the specific audio backend. Perform the - command shairport-sync -h to get a list of available audio backends -- the - default is the first on this list. Only the "alsa", "sndio" and "pa" backends support - synchronisation.

- - - -

mdns_backend="backend";

-

shairport-sync has a number of modules of code ("backends") for - interacting with the mDNS service to be used to advertise itself. Normally, the first - mDNS backend that works is selected. This setting forces the selection of the specific - mDNS backend. The default is "avahi". Perform the command - shairport-sync -h to get a list of available mDNS modules.

- - - -

port=portnumber;

-

Use this to specify the portnumber shairport-sync uses to - listen for service requests from iTunes, etc. The default is port 5000.

- - - -

udp_port_base=portnumber;

-

When shairport-sync starts to play audio, it establises three UDP - connections to the audio source. Use this setting to specify the starting - portnumber for these three ports. It will pick the first three unused ports - starting from portnumber. The default is port 6001.

- - - -

udp_port_range=range;

-

Use this in conjunction with the prevous setting to specify the - range of ports that can be checked for availability. Only three ports are - needed. - The default is 100, thus 100 ports will be checked from port 6001 upwards until three - are found.

- - - -

drift_tolerance_in_seconds=seconds;

-

Allow playback to drift up to seconds out of exact - synchronization before attempting to correct it. - The default is 0.002 seconds, i.e. 2 milliseconds. The smaller the tolerance, the more - likely it is that overcorrection will occur. - Overcorrection is when more corrections (insertions and deletions) are made than are - strictly necessary to keep the stream in sync. Use the statistics setting - to monitor correction levels. Corrections should not greatly exceed net corrections. - This setting replaces the deprecated drift setting. -

- - - -

resync_threshold_in_seconds=threshold;

-

Resynchronise if timings differ by more than threshold seconds. - If the output timing differs from the source timing by more than - the threshold, output will be muted and a full resynchronisation - will occur. The default threshold is 0.050 seconds, i.e. 50 - milliseconds. Specify 0.0 to disable resynchronisation. - This setting replaces the deprecated resync_threshold setting. -

- - - -

ignore_volume_control="choice";

-

Set this choice to "yes" if you want the volume to - be at 100% no matter what the source's volume control is set to. - This might be useful if you want to set the volume on the output device, independently - of the setting at the source. The default is "no".

- - - -

volume_range_db=dBvalue;

-

Use this dBvalue to reduce or increase the attenuation range, - in decibels, between the minimum and maximum volume.

-

For example, if a mixer has a minimum volume of -80 dB and a maximum of +20 dB, you - might wish to use only 60 dB of the 100 dB available. - This might be because the sound becomes inaudible at the lowest setting and unbearably - loud at the highest setting -- - indeed, many domestic HiFi systems have a volume control range of just 60 to 80dB.

-

Another potential use might be where the range specified by the mixer does not - match the capabilities of the device. - For example, the Raspberry Pi's DAC that feeds the built-in audio jack claims a range - of 106 dB but has a useful range of only about 30 dB. - The setting allows you to specify the maximum range from highest to lowest. - The range suggested for the Raspberry Pi's built-in audio DAC, which feeds the - headphone jack, is 30. - Using it in this case gives the volume control a much more useful range of - settings.

-

As a third example, you can actually extend the range provided by a mixer. - Many cheaper DACs have hardware mixers that offer a restricted attenuation range. - If you specify a volume range greater than the range of the mixer, software - attenuation and hardware attenuation will be combined to give the specified range.

-

If you omit this setting, the native range of the mixer is used.

- - - -

volume_max_db=dBvalue;

-

Specify the maximum output level to be used with the hardware mixer, if - used. If no hardware mixed is used, this setting specifies the maximum setting - permissible in the software mixer, which has an attenuation range from 0.0 dB down to - -96.3 dB. -

- - - -

volume_control_profile="choice";

-

Use this advanced setting to specify how the airplay volume is transferred - to the mixer volume. The "standard" profile, which is the default, makes - the volume change more quickly at lower volumes and slower at higher volumes. Choose - the "flat" profile to makes the volume change at the same rate at all - volume levels. -

- - - -

volume_range_combined_hardware_priority= - "choice";

-

Use this advanced setting to specify how to combine the hardware - attenuator with software attenuation to provide a greater attenuation range than the - hardware attenuator alone can provide. Choosing "yes" means that when - attenuation is required, the hardware attenuator will be used in preference. - If more attenuation than it can provide is needed, the hardware attenuator is set to - its greatest attenuation and software attenuation is added.

-

For example, if 40 dB of attenuation is required and the hardware attenuator - offers a maximum of 30 dB, then the hardware attenuator will be set to give 30 dB - attenuation and 10 dB of software attenuation will be added.

-

Unfortunately, certain hardware attenuators will mute at their greatest - attenuation, so can't be combined with software attenuation in this way. Choosing - "no" means that software attenuation is used to bring the remaining - attenuation required into the range offered by the hardware attenuator. - This is the default. -

- - - -

run_this_when_volume_is_set= - "/full/path/to/application/and/args";

-

Here you can specify a program and its arguments that will be run when the - volume is set or changed. Be careful to include the full path to the application. - The application must be marked as executable and, if it is a script, its first line - must begin with the standard shebang #!/bin/... as appropriate.

-

The desired AirPlay volume is appended to the end of the command line -- leave a - space at the end of the command line you specify here if you want it treated as an - extra argument. - AirPlay volume goes from 0.0 to -30.0 and -144.0 means "mute".

- - - -

regtype="regTypeString";

-

Use this advanced setting to set the service type and transport to be - advertised by Zeroconf/Bonjour. Default is "_raop._tcp".

- - - -

playback_mode="mode";

-

The mode can be "stereo", "mono", "reverse stereo", "both left" - or "both right". Default is "stereo". Note that dither will be added to the signal in - the mono mode.

- - - -

alac_decoder="decodername";

-

This can be "hammerton" or "apple". This advanced setting allows you to - choose the original Shairport decoder by David Hammerton or the Apple Lossless Audio - Codec (ALAC) decoder written by Apple. Shairport Sync must have been compiled with the - configuration setting "--with-apple-alac" and the Apple ALAC decoder library must be - present for this to work.

- - - -

interface="name";

-

Use this advanced setting if you want to confine Shairport Sync to the - named interface. Leave it commented out to get the default bahaviour.

- - - -

audio_backend_latency_offset_in_seconds= - offset_in_seconds;

-

Set this offset_in_seconds to compensate for a fixed delay in - the audio back end. For example, if the output device delays by 100 ms, set this to - -0.1.

- - - -

audio_backend_buffer_desired_length_in_seconds= - length_in_seconds;

-

Use this length_in_seconds to set the desired length of the - queue of audio frames in the backend's output buffer.

-

The default is 0.15 seconds for the ALSA backend, 0.35 seconds for the PA backend - and one second for all other backends.

-

If this value is set too small, underflow may occur on low-powered machines. - If set too large, the response times to the volume control may become excessive, or it - may exceed the backend's buffer size. - It may need to be larger on low-powered machines that are also performing other tasks, - such as processing metadata.

- - - -

audio_backend_buffer_interpolation_threshold_in_seconds= - time_in_seconds;

-

This is an advanced feature. If the length of the audio backend buffer - size drops below this, it's a sign that shairport sync can not process frames of audio - quickly enough. It this threshold is reached, shairport sync will stop using - time-consuming interpolation like soxr to avoid underruns.

- - - -

audio_backend_silent_lead_in_time= - lead_in_time_in_seconds;

-

This is an advanced setting. Use the lead_in_time_in_seconds to - set the desired length of the period of silence (a "silent lead-in") played before a - play session begins.

-

The purpose of this silent lead-in is to give the backend sufficient time to - prepare for operation and to make an estimate (and, importantly, to correct the - estimate) of the exact time at which to begin playing audio to achieve initial - synchronisation. The value can be from 0.0 up to a maximum of either 4.0 seconds. The - actual duration will be close to the setting but can not exceed the latency set by the - client, usually 2 seconds or a little more.

-

If the value chosen is too short for synchronised backends such as the ALSA, sndio - or PA backends, then audio will not be synchronised correctly at the start of play. - The default is to have a silent lead-in of approximately the same time as the latency - set by the client.

- - - -

dbus_service_bus= - "bus_name";

-

If shairport sync is compiled with the D-Bus interface, it can offer it on - the "system" or the "session" D-Bus "bus". - Use this to specify which. The default is to use the "system" bus.

- - - -

mpris_service_bus= - "bus_name";

-

If shairport sync is compiled with the MPRIS interface, it can offer the - service on the "system" or the "session" D-Bus "bus". - Use this to specify which. The default is to use the "system" bus.

- - -

"SESSIONCONTROL" SETTINGS

- - -

run_this_before_play_begins="/path/to/application and - args";

-

Here you can specify a program and its arguments that will be run just - before a play session begins. Be careful to include the full path to the application. - The application must be marked as executable and, if it is a script, its first line - must begin with the standard shebang #!/bin/... as - appropriate.

- - - -

run_this_after_play_ends="/path/to/application and - args";

-

Here you can specify a program and its arguments that will be run just - after a play session ends. Be careful to include the full path to the application. - The application must be marked as executable and, if it is a script, its first line - must begin with the standard shebang #!/bin/... as - appropriate.

- - - -

run_this_before_entering_active_state="/path/to/application and - args";

-

Here you can specify a program and its arguments that will be run just - before shairport-sync goes active.

- -

Shairport Sync goes "active" when a play session starts. When the play - session ends, the system will stay active until the time - specified in the active_state_timeout setting elapses. - If a new play session starts before that, the system will remain active. Otherwise, - the system will go inactive. -

- -

Be careful to include the full path to the application. - The application must be marked as executable and, if it is a script, its first line - must begin with the standard shebang #!/bin/... as - appropriate.

- - - -

run_this_after_exiting_active_state="/path/to/application and - args";

-

Here you can specify a program and its arguments that will be run just - after shairport-sync goes inactive (see the previous entry for an explanation - of the idea). - Be careful to include the full path to the application. - The application must be marked as executable and, if it is a script, its first line - must begin with the standard shebang #!/bin/... as - appropriate.

- - - -

active_state_timeout=seconds;

-

After a play session has ended, the system will remain active for - seconds seconds. If a new play session starts before this time has elapsed, - the system will remain active. However, if no new session starts in the interval, the - system will go inactive at the end of it. The default is 10 seconds.

- - - -

run_this_if_an_unfixable_error_is_detected="/path/to/application - and args";

-

Here you can specify a program and its arguments that will be run if the - system detects an unfixable error. At present, there are two types of - unfixable errors. One is where a play session cannot be terminated. - The second is if an output device has "stalled" -- that is, if an output device - refuses to accept any more output frames.

-

Although the first problem could, in principle, be fixed by restarting - Shairport Sync, it is usually caused by a malfunctioning output device. - Typically, the most reliable way to recover from either of these errors - is to reboot the entire machine.

-

Be careful to include the full path to the application. - The application must be marked as executable and, if it is a script, its first line - must begin with the standard shebang #!/bin/... as - appropriate.

- - - -

wait_for_completion="choice";

-

Set choice to "yes" to make shairport-sync wait until the - programs specified in the run_this_... settings have - completed execution before continuing. The default is "no".

- - - -

allow_session_interruption="choice";

-

If choice is set to "yes", then another source will be able to - interrupt an existing play session and start a new one. - When set to "no" (the default), other devices attempting to interrupt a session will - fail, receiving a busy signal.

- - - -

session_timeout=seconds;

-

If a play session has been established and the source disappears without - warning (such as a device going out of range of a network) - then wait for the number of seconds specified before ending the session. - Once the session has terminated, other devices can use it. The default is 120 - seconds.

- - - -

"ALSA" SETTINGS

-

These settings are for the ALSA back end, used to communicate with audio output - devices in the ALSA system. (By the way, you can use tools such as - alsamixer or aplay to discover what devices are available.) - Use these settings to select the output device and the mixer control to be used to - control the output volume. - You can additionally set the desired size of the output buffer and you can adjust - overall latency. Here are the alsa group settings:

- - -

output_device="output_device";

-

Use the output device called output_device. The default is the - device called "default".

- - - -

mixer_control_name="name";

-

Specify the name of the mixer control to be used by - shairport-sync to control the volume. - The mixer control must be on the mixer device, which by default is the output device. - If you do not specify a mixer control name, shairport-sync will adjust the volume in - software.

- - - -

mixer_device="mixer_device";

-

By default, the mixer is assumed to be output_device. Use this setting to - specify a device other than the output device.

- - - -

output_rate=frame rate;

-

Use this setting to specify the frame rate to output to the ALSA device. - Allowable values are 44100 (default), 88200, 176400 and 352800. The device must have - the capability to accept the format you specify. There is no particular reason to use - anything other than 44100 if it is available. -

- - - -

output_format="format";

-

Use this setting to specify the format that should be used to send data to - the ALSA device. Allowable values are "U8", "S8", "S16", "S24", "S24_3LE", "S24_3BE" - or "S32". The device must have the capability to accept the format you - specify.

"S" means signed; "U" means unsigned; BE means big-endian and LE means - little-endian. Except where stated (using *LE or *BE), endianness matches that of the - processor. The default is "S16".

If you are using a hardware mixer, the best - setting is S16, as audio will pass through Shairport Sync unmodifed except for - interpolation. If you are using the software mixer, use 32- or 24-bit, if your device - is capable of it, to get the lowest possible levels of dither. -

- - - -

disable_synchronization="no";

-

This is an advanced setting and is for debugging only. Set to - "yes" to disable synchronization. Default is "no". - If you use it to disable synchronisation, then sooner or later you'll experience audio - glitches due to audio buffer overflow or underflow. -

- - - -

period_size=number;

-

Use this optional advanced setting to set the alsa period size near to - this value.

- - - -

buffer_size=number;

-

Use this optional advanced setting to set the alsa buffer size near to - this value.

- - - -

use_mmap_if_available="yes";

-

Use this optional advanced setting to control whether MMAP-based output - is used to communicate with the DAC. Default is "yes".

- - - -

mute_using_playback_switch="no";

- -

This is an advanced setting and the default is "no". If it is set to - "yes", hardware mute will be used where it is available. - Set it to "no" to prevent the hardware mute being used.

-

If Shairport Sync is sharing the output device with other applications, it is best - to leave this set to "no" for compatibility with those applications.

-

Another motivation for this is to allow the ALSA function call - "snd_mixer_selem_set_playback_switch_all" to be avoided. It is incorrectly implemented - on certain soundcards, including the emulated card in VMWare Fusion 8.5.

- - - - -

maximum_stall_time=seconds;

-

If an output device fails to accept any audio frames for more than the - time, in seconds, specified here (0.2 seconds by default), - it is considered to have malfunctioned. It will result in the - run_this_if_an_unfixable_error_is_detected program, - if any, being called.

-

Implemented for the ALSA back end only.

- - - - -

disable_standby_mode="never";

- -

Shairport Sync has a "Disable Standby" feature to eliminate certain - faint-but-annoying audible pops and clicks. When activsted, it prevents - an output device from entering standby mode and thus it minimises standby/busy - transitions, which can sometimes be heard. Use this setting to control when the - Disable Standby feature is active: "never" means it will never be activated, "always" - means it will be active as soon as shairport-sync starts running, and "auto" - means it will be active while shairport-sync is in the "active" state.

-

Shairport Sync goes "active" when a play session starts. When the play - session ends, the system will stay active until the time - specified in the active_state_timeout setting elapses. - If a new play session starts before that, the system will remain active. Otherwise, - the system will go inactive. -

- - - -

"SNDIO" SETTINGS

-

These settings are for the SNDIO back end, used to communicate with audio output - devices in the SNDIO system.

- - -

device="snd/0";

-

Use this optional setting to specify the name of the output device, e.g. - "snd/0". The default is to use the SNDIO system's default.

- - - -

rate=44100;

-

Use this optional setting to specify the output rate in frames per second. - Valid rates are 44100, 88200, 176400 or 352800. - The output device must have the capability to accept data at the specified rate. The - default is 44100.

- - - -

format="S16";

-

Use this optional setting to specify the output format. Allowable values - are "U8", "S8", "S16", "S24", "S24_3LE", "S24_3BE" or "S32". - The device must have the capability to accept the format you specify.

"S" means - signed; "U" means unsigned; BE means big-endian and LE means little-endian. - Except where stated (using *LE or *BE), endianness matches that of the processor. The - default is "S16".

- Since the SNDIO backend does not use a hardware mixer for volume control, dither will - be introduced into the output if it is less than full volume. - Thus, (unless you are ignoring the volume control setting), - consider using 32- or 24-bit output if your device is capable of it, to get the lowest - possible levels of dither.

-

Please note that 32- or 24-bit has not been extensively tested on - SNDIO.

- - - -

round=value;

-

Use this optional advanced setting to specify the period size of the SNDIO - channel. If omitted, a SNDIO system default value will be used.

- - - -

bufsiz=value;

-

Use this optional advanced setting to specify the buffer size of the SNDIO - channel. If omitted, a SNDIO system default value will be used.

- - -

"PA" SETTINGS

-

These settings are for the new PulseAudio backend.

- - -

application_name="Shairport Sync";

-

Use this to set the name to appear in the Sounds "Applications" tab when - Shairport Sync is active. The default is the name "Shairport Sync".

- - -

"PIPE" SETTINGS

-

These settings are for the PIPE backend, used to route audio to a named unix pipe. - The audio is in raw CD audio format: PCM 16 bit little endian, 44,100 samples per - second, interleaved stereo.

- - -

name="/path/to/pipe";

-

Use this to specify the name and location of the pipe. The pipe will be - created and opened when shairport-sync starts up and will be closed upon shutdown. - Frames of audio will be sent to the pipe in packets of 352 frames and will be - discarded if the pipe has not have a reader attached. - The sender will wait for up to five seconds for a packet to be written before - discarding it.

- - -

"STDOUT" SETTINGS

-

There are no settings for the STDOUT backend.

- -

"AO" SETTINGS

-

There are no configuration file settings for the AO backend.

- -

"METADATA" SETTINGS

-

shairport-sync can process metadata provided by the source, such as Track Number, - Album Name, cover art, etc. and can provide additional metadata such as volume level, - pause/resume, etc. It sends the metadata to a pipe, by default - /tmp/shairport-sync-metadata. - To process metadata, shairport-sync must have been compiled with metadata support - included. - You can check that this is so by running the command $ shairport-sync -V; - the identification string will contain the word metadata.

-

Please note that different sources provide different levels of metadata. Some - provide a lot; some provide almost none.

-

The metadata group of settings allow you to enable metadata handling and - to control certain aspects of it:

- - -

enabled="choice";

-

Set the choice to "yes" to enable shairport-sync to look for - metadata from the audio source and to forward it, along with metadata generated by - shairport-sync itself, to the metadata pipe. The default is "no".

- - - -

include_cover_art="choice";

-

Set the choice to "yes" to enable shairport-sync to look for - cover art from the audio source and to include it in the feed to the metadata pipe. - You must also enable metadata (see above). - One reason for not including cover art is that the images can sometimes be very large - and may delay transmission of subsequent metadata through the pipe. - The default is "no".

- - - -

pipe_name="filepathname";

-

Specify the absolute path name of the pipe through which metadata should - be sent The default is /tmp/shairport-sync-metadata.

- - - -

socket_address="hostnameOrIP";

-

If hostnameOrIP is set to a host name or and IP address, UDP - packets containing metadata will be sent to this address. - May be a multicast address. Additionally, socket-port must be non-zero and - enabled must be set to "yes".

- - - -

socket_port=port;

-

If socket_address is set, use port to specify the - port to send UDP packets to. Must not be zero.

- - - -

socket_msglength=65000;

-

The maximum packet size for any UDP metadata. This must be between 500 or - 65000. The default is 500.

- - -

"DIAGNOSTICS" SETTINGS

- -

statistics="setting";

-

Use this setting to enable ("yes") or disable ("no") the output - of some statistical information on the console or in the log. The default is to - disable statistics.

- - - -

log_verbosity=0;

-

Use this to specify how much debugging information should be output or - logged. The value 0 means no debug information, 3 means most - debug information. The default is 0.

- - - - - -

Options

- -

This section is about the command-line options available in shairport-sync.

-

Note: if you are setting up shairport-sync for the first time or are updating an - existing installation, you are encouraged to use the configuration file settings - described above. Most of the command-line options described below - simply replicate the configuration settings and are retained to provide backward - compatibility with older installations of shairport-sync.

- -

Many command-line options take sensible default values, so you can normally - ignore most of them. See the EXAMPLES section for typical usages.

- -

There are two kinds of command-line options for shairport-sync: - regular program options and audio backend options. - Program options are - always listed first, followed by any audio backend options, preceded by - a -- symbol.

- -

Program Options

- -

These command-line options are used by shairport-sync itself.

- - - - -

-a service name | --name=service - name

-

- Use this service name to identify this player in iTunes, etc.

- -

The following substitutions are allowed: - %h for the computer's hostname, - %H for the computer's hostname with the first letter capitalised (ASCII - only), - %v for the shairport-sync version number, e.g. "3.0.1" and - %V for the shairport-sync version string, e.g. - "3.0.1-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc".

-

The default is "%H", which is replaced by the hostname with the first letter - capitalised.

- - - - -

-B program | --on-start=program

-

- Execute program when playback is about to begin. Specify the - full path to the program, e.g. /usr/bin/logger. - Executable scripts can be used, but they must have the appropriate shebang - (#!/bin/sh in the headline.

- -

If you want shairport-sync to wait until the command has - completed before starting to play, select the -w option as well. -

- - - -

-c filename | --configfile=filename

-

- Read configuration settings from filename. The default is to read them from - the shairport-sync.conf in the System Configuration Directory -- - /etc in Linux, /usr/local/etc in BSD unixes. - For information about configuration settings, see the "Configuration File Settings" - section above. -

- - - -

-d | --daemon

-

- Instruct shairport-sync to demonise itself. It will write its - Process ID (PID) to a file, usually at - /var/run/shairport-sync/shairport-sync.pid, which is used by the - -k, -D and -R options to locate - the daemon at a later time. See also the -j option. Only available if - shaiport-sync has been compiled with libdaemon support. -

- - - -

-E program | --on-stop=program

-

- Execute program when playback has ended. Specify the - full path to the program, e.g. /usr/bin/logger. - Executable scripts can be used, but they must have the appropriate shebang - (#!/bin/sh in the headline.

-

If you want shairport-sync to wait until the command has - completed before continuing, select the -w option as well. -

- - - -

--get-coverart

-

- This option requires the --meta-dir option to be set, and enables - shairport-sync to request cover art from the source and to transmit it through - the metadata pipe.

-

Please note that cover art data may be very large, and may place too great a - burden on your network. -

- - - -

-h | --help

-

- Print brief help message and exit. -

- - - -

-j

-

- Instruct shairport-sync to demonise itself. Unlike the -d option, it will - not write a Process ID (PID) to a file -- it will just (hence the "j") demonise - itself. Only available if shaiport-sync has been compiled with libdaemon support. -

- - - -

-k | --kill

-

- Kill the shairport-sync daemon and exit. (Requires that the daemon has - written its PID to an agreed file -- see the -d option. Only available if - shaiport-sync has been compiled with libdaemon support.) -

- - - -

--logOutputLevel

-

- Use this to log the volume level when the volume is changed. It may be useful if you - are trying to determine a suitable value for the maximum volume level. Not available - as a configuration file setting. -

- - - - -

-L | --latency=latency

-

- Use this to set the default latency, in frames, for audio coming from an - unidentified source or from an iTunes Version 9 or earlier source. The standard value - for the default latency is 88,200 frames, where there are 44,100 - frames to the second. -

-

Please note that this feature is deprecated and will be removed in a future version - of shairport-sync.

- - - - -

--meta-dir=directory

-

- Listen for metadata coming from the source and send it, along with metadata from - shairport-sync itself, to a pipe called shairport-sync-metadata - in the directory you specify. If you add the --get-cover-art - then cover art will be sent through the pipe too. See https://github.com/mikebrady/shairport-sync-metadata-reader - for a sample metadata reader. -

- - - -

-m mdnsbackend | --mdns=mdnsbackend

-

- Force the use of the specified mDNS backend to advertise the - player on the network. The default is to try all mDNS backends until one - works. -

- - - -

-o outputbackend | - --output=outputbackend

-

- Force the use of the specified output backend to play the audio. - The default is to try the first one. -

- - - -

-p port | --port=port

-

- Listen for play requests on port. The default is to use port - 5000. -

- - - -

--password=secret

-

- Require the password secret to be able to connect and stream to the - service. -

- - - -

-r threshold | --resync=threshold

-

- Resynchronise if timings differ by more than threshold frames. - If the output timing differs from the source timing by more than - the threshold, output will be muted and a full resynchronisation - will occur. The default threshold is 2,205 frames, i.e. 50 - milliseconds. Specify 0 to disable resynchronisation. This setting is - deprecated and will be removed in a future version of shairport-sync. -

- - - -

--statistics

-

- Print some statistics in the standard output, or in the logfile if in daemon mode. -

- - - -

-S mode | --stuffing=mode

-

- Stuff the audio stream using the mode. "Stuffing" refers to the - process of adding or removing frames of audio to or from the - stream sent to the output device to keep it exactly in synchrony - with the player. - The default mode, basic, is normally almost completely inaudible. - The alternative mode, soxr, is even less obtrusive but - requires much more processing power. For this mode, support for - libsoxr, the SoX Resampler Library, must be selected when - shairport-sync is compiled. -

- - - -

-t timeout | --timeout=timeout

-

- Exit play mode if the stream disappears for more than timeout - seconds.

-

When shairport-sync plays an audio stream, it starts a play - session and will return a busy signal to any other sources that - attempt to use it. If the audio stream disappears for longer - than timeout seconds, the play session will be terminated. - If you specify a timeout time of 0, - shairport-sync will never signal that - it is busy and will not prevent other sources from "barging in" - on an existing play session. The default value is 120 seconds. -

- - - -

--tolerance=frames

-

- Allow playback to be up to frames out of exact synchronization before - attempting to correct it. - The default is 88 frames, i.e. 2 ms. The smaller the tolerance, the more likely it is - that overcorrection will occur. - Overcorrection is when more corrections (insertions and deletions) are made than are - strictly necessary to keep the stream in sync. Use the --statistics option - to monitor correction levels. Corrections should not greatly exceed net corrections. - This setting is deprecated and will be removed in a future version of shairport-sync. -

- - - -

-u

-

- If you are running shairport-sync from the command line and want logs to appear there, - use this option. Otherwise, logs may go to the system log. -

- - - -

-V | --version

-

- Print version information and exit. -

- - - -

-v | --verbose

-

- Print debug information. Repeat up to three times to get more detail. -

- - - -

-w | --wait-cmd

-

- Wait for commands specified using -B or -E to complete before - continuing execution. -

- - -

Audio Backend Options

- -

These command-line options are passed to the chosen audio backend. The audio - backend options are - preceded by a -- symbol to introduce them and to separate them from any - program options. In this way, option letters can be used as program - options and also as audio backend options without ambiguity.

- -

In the ALSA backend, audio is sent to an output device - which you can specify using the -d option. - The output level (the "volume") is controlled using a level control associated with a - mixer. - By default, the mixer is implemented in shairport-sync itself in software. - To use a hardware level control on a mixer on the sound card, specify the name of the - mixer control with the -c option. - If the mixer is not associated with the output device, then you need to specify where - the mixer is to be found with the -m option.

- - - - -

-c controlname

-

- Use the level control called controlname on the hardware mixer for - controlling volume. - This is needed if the mixer type is specified, using the -t option, - to be hardware. There is no default. -

- - - -

-d device

-

- Use the specified output device. You may specify a card, e.g. - hw:0, in which case the default output device on the card will be chosen. - Alternatively, you can specify a specific device on a card, e.g. hw:0,0. - The default is the device named default. -

- - - -

-m mixer

-

- Use the specified hardware mixer for volume control. Use this to specify - where the mixer is to be found. For example, if the mixer is associated with a card, - as is often the case, specify the card, e.g. hw:0. - If (unusually) the mixer is associated with a specific device on a card, - specify the device, e.g. hw:0,1. - The default is the device named in the -d option, - if given, or the device named default. -

- - - -

-t devicetype

- -

- This option is deprecated and is ignored. For your information, its functionality has - been automatically incorporated in the -c option - -- if you specify a mixer name with the -c option, it is assumed that the mixer is - implemented in hardware. -

- - - -

Examples

- -

Here is a slightly contrived example:

- shairport-sync -d - -a "Joe's Stereo" - -S soxr - -- - -d hw:1,0 - -m hw:1 - -c PCM -
- -

The program will run in daemon mode ( -d ), will be visible as - "Joe's Stereo" ( -a "Joe's Stereo" ) and will use the SoX Resampler - Library-based stuffing ( -S soxr ). - The audio backend options following the -- separator specify - that the audio will be output on output 0 of soundcard 1 - ( -d hw:1,0 ) and will take advantage of the same sound card's mixer - ( -m hw:1 ) using the level control named "PCM" ( -c "PCM" ). -

-

The example above is slightly contrived in order to show the use of the -m - option. Typically, output 0 is the default output of a card, so the output device could - be written -d hw:1 and then the mixer option would be unnecessary, giving the following, simpler, command:

- shairport-sync -d - -a "Joe's Stereo" - -S soxr - -- - -d hw:1 - -c PCM -
- - - - - -

Credits

- -

Mike Brady developed shairport-sync from the original Shairport by James Laird.

-

shairport-sync can be found at - https://github.com/mikebrady/shairport-sync.

-

Shairport can be found at - https://github.com/abrasive/shairport.

- - - -

Comments

- -

This man page was written using xml2man by Oliver Kurth.

- - - -
+

Synopsis

+
+
+
+
+
+

Description

+

+

Configuration File Settings

+

http://www.hyperrealm.com/libconfig/libconfig_manual.html

+

Options

+

Program Options

+

+

https://github.com/mikebrady/shairport-sync-metadata-reader

Audio Backend Options

+

+

Examples

+


+


+ +

Credits

+

https://github.com/mikebrady/shairport-sync.

https://github.com/abrasive/shairport.

+

Comments

+

xml2man

+ diff --git a/mdns_avahi.c b/mdns_avahi.c index caec909d4..f3c1f1c95 100644 --- a/mdns_avahi.c +++ b/mdns_avahi.c @@ -96,6 +96,8 @@ static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIn char *dacpid = strstr(name, "iTunes_Ctrl_"); if (dacpid) { dacpid += strlen("iTunes_Ctrl_"); + while (*dacpid == '0') + dacpid++; // remove any leading zeroes if (strcmp(dacpid, dbs->dacp_id) == 0) { debug(3, "resolve_callback: client dacp_id \"%s\" dacp port: %u.", dbs->dacp_id, port); #ifdef CONFIG_DACP_CLIENT @@ -340,17 +342,17 @@ static int avahi_register(char *srvname, int srvport) { static void avahi_unregister(void) { // debug(1, "avahi_unregister."); if (tpoll) { - debug(1, "avahi: stop the threaded poll."); + debug(2, "avahi: stop the threaded poll."); avahi_threaded_poll_stop(tpoll); if (client) { - debug(1, "avahi: free the client."); + debug(2, "avahi: free the client."); avahi_client_free(client); client = NULL; } else { debug(1, "avahi attempting to unregister a NULL client"); } - debug(1, "avahi: free the threaded poll."); + debug(2, "avahi: free the threaded poll."); avahi_threaded_poll_free(tpoll); tpoll = NULL; } else { @@ -358,7 +360,7 @@ static void avahi_unregister(void) { } if (service_name) { - debug(1, "avahi: free the service name."); + debug(2, "avahi: free the service name."); free(service_name); } else debug(1, "avahi attempt to free NULL service name"); @@ -412,7 +414,7 @@ void avahi_dacp_monitor_stop() { } avahi_threaded_poll_unlock(tpoll); free(dbs->dacp_id); - debug(1, "avahi_dacp_monitor_stop Avahi DACP monitor successfully stopped"); + debug(2, "avahi_dacp_monitor_stop Avahi DACP monitor successfully stopped"); } mdns_backend mdns_avahi = {.name = "avahi", diff --git a/mdns_dns_sd.c b/mdns_dns_sd.c index 95bae443a..cf178f1d7 100644 --- a/mdns_dns_sd.c +++ b/mdns_dns_sd.c @@ -24,8 +24,8 @@ * OTHER DEALINGS IN THE SOFTWARE. */ -#include "mdns.h" #include "common.h" +#include "mdns.h" #include #include #include diff --git a/mdns_external.c b/mdns_external.c index 2ae23c61f..974aa85bb 100644 --- a/mdns_external.c +++ b/mdns_external.c @@ -25,8 +25,8 @@ * OTHER DEALINGS IN THE SOFTWARE. */ -#include "mdns.h" #include "common.h" +#include "mdns.h" #include #include #include diff --git a/metadata_hub.c b/metadata_hub.c index f079ea2af..190895c0a 100644 --- a/metadata_hub.c +++ b/metadata_hub.c @@ -35,10 +35,10 @@ #include #include #include +#include #include #include #include -#include #include "config.h" @@ -92,13 +92,6 @@ void add_metadata_watcher(metadata_watcher fn, void *userdata) { } } -/* -void metadata_hub_unlock_hub_mutex_cleanup(__attribute__((unused)) void *arg) { - // debug(1, "metadata_hub_unlock_hub_mutex_cleanup called."); - pthread_rwlock_unlock(&metadata_hub_re_lock); -} -*/ - void run_metadata_watchers(void) { int i; for (i = 0; i < number_of_watchers; i++) { @@ -129,83 +122,112 @@ void run_metadata_watchers(void) { metadata_store.songtime_in_milliseconds_changed = 0; } -void metadata_hub_modify_prolog(void) { +void metadata_hub_unlock_hub_mutex_cleanup(__attribute__((unused)) void *arg) { + debug(1, "metadata_hub_unlock_hub_mutex_cleanup called."); + metadata_hub_modify_epilog(0); +} + +char *last_metadata_hub_modify_prolog_file = NULL; +int last_metadata_hub_modify_prolog_line; +int metadata_hub_re_lock_access_is_delayed; + +void _metadata_hub_modify_prolog(const char *filename, const int linenumber) { // always run this before changing an entry or a sequence of entries in the metadata_hub // debug(1, "locking metadata hub for writing"); if (pthread_rwlock_trywrlock(&metadata_hub_re_lock) != 0) { - debug(2, "Metadata_hub write lock is already taken -- must wait."); + if (last_metadata_hub_modify_prolog_file) + debug(2, "Metadata_hub write lock at \"%s:%d\" is already taken at \"%s:%d\" -- must wait.", filename, linenumber, last_metadata_hub_modify_prolog_file, last_metadata_hub_modify_prolog_line); + else + debug(2, "Metadata_hub write lock is already taken by unknown -- must wait."); + metadata_hub_re_lock_access_is_delayed = 0; pthread_rwlock_wrlock(&metadata_hub_re_lock); - debug(2, "Okay -- acquired the metadata_hub write lock."); + debug(2, "Okay -- acquired the metadata_hub write lock at \"%s:%d\".", filename, linenumber); } else { + if (last_metadata_hub_modify_prolog_file) { + free(last_metadata_hub_modify_prolog_file); + } + last_metadata_hub_modify_prolog_file = strdup(filename); + last_metadata_hub_modify_prolog_line = linenumber; debug(3, "Metadata_hub write lock acquired."); } + metadata_hub_re_lock_access_is_delayed = 0; } -void metadata_hub_modify_epilog(int modified) { +void _metadata_hub_modify_epilog(int modified, const char *filename, const int linenumber) { metadata_store.dacp_server_has_been_active = metadata_store.dacp_server_active; // set the scanner_has_been_active now. if (modified) { run_metadata_watchers(); } + if (metadata_hub_re_lock_access_is_delayed) { + if (last_metadata_hub_modify_prolog_file) { + debug(1, "Metadata_hub write lock taken at \"%s:%d\" is freed at \"%s:%d\".", last_metadata_hub_modify_prolog_file, last_metadata_hub_modify_prolog_line, filename, linenumber); + free(last_metadata_hub_modify_prolog_file); + last_metadata_hub_modify_prolog_file = NULL; + } else { + debug(1, "Metadata_hub write lock taken at an unknown place is freed at \"%s:%d\".", filename, linenumber); + } + } pthread_rwlock_unlock(&metadata_hub_re_lock); debug(3, "Metadata_hub write lock unlocked."); } -void metadata_hub_read_prolog(void) { +/* +void _metadata_hub_read_prolog(const char *filename, const int linenumber) { // always run this before reading an entry or a sequence of entries in the metadata_hub // debug(1, "locking metadata hub for reading"); if (pthread_rwlock_tryrdlock(&metadata_hub_re_lock) != 0) { - debug(2, "Metadata_hub read lock is already taken -- must wait."); + debug(1, "Metadata_hub read lock is already taken -- must wait."); pthread_rwlock_rdlock(&metadata_hub_re_lock); - debug(2, "Okay -- acquired the metadata_hub read lock."); + debug(1, "Okay -- acquired the metadata_hub read lock."); } } -void metadata_hub_read_epilog(void) { +void _metadata_hub_read_epilog(const char *filename, const int linenumber) { // always run this after reading an entry or a sequence of entries in the metadata_hub // debug(1, "unlocking metadata hub for reading"); pthread_rwlock_unlock(&metadata_hub_re_lock); } - +*/ char *metadata_write_image_file(const char *buf, int len) { // warning -- this removes all files from the directory apart from this one, if it exists // it will return a path to the image file allocated with malloc. // free it if you don't need it. - char *path = NULL; // this will be what is returned - if (strcmp(config.cover_art_cache_dir,"") != 0) { // an empty string means do not write the file + char *path = NULL; // this will be what is returned + if (strcmp(config.cover_art_cache_dir, "") != 0) { // an empty string means do not write the file uint8_t img_md5[16]; // uint8_t ap_md5[16]; - #ifdef CONFIG_OPENSSL +#ifdef CONFIG_OPENSSL MD5_CTX ctx; MD5_Init(&ctx); MD5_Update(&ctx, buf, len); MD5_Final(img_md5, &ctx); - #endif +#endif - #ifdef CONFIG_MBEDTLS - #if MBEDTLS_VERSION_MINOR >= 7 +#ifdef CONFIG_MBEDTLS +#if MBEDTLS_VERSION_MINOR >= 7 mbedtls_md5_context tctx; mbedtls_md5_starts_ret(&tctx); mbedtls_md5_update_ret(&tctx, (const unsigned char *)buf, len); mbedtls_md5_finish_ret(&tctx, img_md5); - #else +#else mbedtls_md5_context tctx; mbedtls_md5_starts(&tctx); mbedtls_md5_update(&tctx, (const unsigned char *)buf, len); mbedtls_md5_finish(&tctx, img_md5); - #endif - #endif +#endif +#endif - #ifdef CONFIG_POLARSSL +#ifdef CONFIG_POLARSSL md5_context tctx; md5_starts(&tctx); md5_update(&tctx, (const unsigned char *)buf, len); md5_finish(&tctx, img_md5); - #endif +#endif char img_md5_str[33]; memset(img_md5_str, 0, sizeof(img_md5_str)); @@ -232,8 +254,8 @@ char *metadata_write_image_file(const char *buf, int len) { // if it exists, we're done char *prefix = "cover-"; - size_t pl = strlen(config.cover_art_cache_dir) + 1 + strlen(prefix) + strlen(img_md5_str) + 1 + - strlen(ext); + size_t pl = strlen(config.cover_art_cache_dir) + 1 + strlen(prefix) + strlen(img_md5_str) + + 1 + strlen(ext); path = malloc(pl + 1); snprintf(path, pl + 1, "%s/%s%s.%s", config.cover_art_cache_dir, prefix, img_md5_str, ext); @@ -303,40 +325,47 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin // all the following items of metadata are contained in one metadata packet // they are preceded by an 'ssnc' 'mdst' item and followed by an 'ssnc 'mden' item. + // we don't set "changed" for them individually; instead we set it when the 'mden' token + // comes in if the metadata_packet_item_changed is set. - char *cs; int changed = 0; + int metadata_packet_item_changed = 0; + metadata_hub_modify_prolog(); + pthread_cleanup_push(metadata_hub_unlock_hub_mutex_cleanup, NULL); + + char *cs; if (type == 'core') { switch (code) { case 'mper': { - // get the 64-bit number as a uint64_t by reading two uint32_t s and combining them - uint64_t vl = ntohl(*(uint32_t*)data); // get the high order 32 bits - vl = vl << 32; // shift them into the correct location - uint64_t ul = ntohl(*(uint32_t*)(data+sizeof(uint32_t))); // and the low order 32 bits - vl = vl + ul; - debug(2, "MH Item ID seen: \"%" PRIx64 "\" of length %u.", vl, length); - if (vl != metadata_store.item_id) { - metadata_store.item_id = vl; - metadata_store.item_id_changed = 1; - metadata_store.item_id_received = 1; - debug(2, "MH Item ID set to: \"%" PRIx64 "\"", metadata_store.item_id); - } - } - break; + // get the 64-bit number as a uint64_t by reading two uint32_t s and combining them + uint64_t vl = ntohl(*(uint32_t *)data); // get the high order 32 bits + vl = vl << 32; // shift them into the correct location + uint64_t ul = ntohl(*(uint32_t *)(data + sizeof(uint32_t))); // and the low order 32 bits + vl = vl + ul; + debug(2, "MH Item ID seen: \"%" PRIx64 "\" of length %u.", vl, length); + if (vl != metadata_store.item_id) { + metadata_store.item_id = vl; + metadata_store.item_id_changed = 1; + metadata_store.item_id_received = 1; + debug(2, "MH Item ID set to: \"%" PRIx64 "\"", metadata_store.item_id); + metadata_packet_item_changed = 1; + } + } break; case 'astm': { - uint32_t ui = ntohl(*(uint32_t *)data); - debug(2, "MH Song Time seen: \"%u\" of length %u.", ui, length); - if (ui != metadata_store.songtime_in_milliseconds) { - metadata_store.songtime_in_milliseconds = ui; - metadata_store.songtime_in_milliseconds_changed = 1; - debug(2, "MH Song Time set to: \"%u\"", metadata_store.songtime_in_milliseconds); - } + uint32_t ui = ntohl(*(uint32_t *)data); + debug(2, "MH Song Time seen: \"%u\" of length %u.", ui, length); + if (ui != metadata_store.songtime_in_milliseconds) { + metadata_store.songtime_in_milliseconds = ui; + metadata_store.songtime_in_milliseconds_changed = 1; + debug(2, "MH Song Time set to: \"%u\"", metadata_store.songtime_in_milliseconds); + metadata_packet_item_changed = 1; } - break; + } break; case 'asal': cs = strndup(data, length); if (string_update(&metadata_store.album_name, &metadata_store.album_name_changed, cs)) { debug(2, "MH Album name set to: \"%s\"", metadata_store.album_name); + metadata_packet_item_changed = 1; } free(cs); break; @@ -344,6 +373,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin cs = strndup(data, length); if (string_update(&metadata_store.artist_name, &metadata_store.artist_name_changed, cs)) { debug(2, "MH Artist name set to: \"%s\"", metadata_store.artist_name); + metadata_packet_item_changed = 1; } free(cs); break; @@ -352,6 +382,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin if (string_update(&metadata_store.album_artist_name, &metadata_store.album_artist_name_changed, cs)) { debug(2, "MH Album Artist name set to: \"%s\"", metadata_store.album_artist_name); + metadata_packet_item_changed = 1; } free(cs); break; @@ -359,6 +390,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin cs = strndup(data, length); if (string_update(&metadata_store.comment, &metadata_store.comment_changed, cs)) { debug(2, "MH Comment set to: \"%s\"", metadata_store.comment); + metadata_packet_item_changed = 1; } free(cs); break; @@ -366,6 +398,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin cs = strndup(data, length); if (string_update(&metadata_store.genre, &metadata_store.genre_changed, cs)) { debug(2, "MH Genre set to: \"%s\"", metadata_store.genre); + metadata_packet_item_changed = 1; } free(cs); break; @@ -373,6 +406,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin cs = strndup(data, length); if (string_update(&metadata_store.track_name, &metadata_store.track_name_changed, cs)) { debug(2, "MH Track Name set to: \"%s\"", metadata_store.track_name); + metadata_packet_item_changed = 1; } free(cs); break; @@ -380,6 +414,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin cs = strndup(data, length); if (string_update(&metadata_store.composer, &metadata_store.composer_changed, cs)) { debug(2, "MH Composer set to: \"%s\"", metadata_store.composer); + metadata_packet_item_changed = 1; } free(cs); break; @@ -396,6 +431,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin if (string_update(&metadata_store.song_album_artist, &metadata_store.song_album_artist_changed, cs)) { debug(2, "MH Song Album Artist set to: \"%s\"", metadata_store.song_album_artist); + metadata_packet_item_changed = 1; } free(cs); break; @@ -403,6 +439,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin cs = strndup(data, length); if (string_update(&metadata_store.sort_name, &metadata_store.sort_name_changed, cs)) { debug(2, "MH Sort Name set to: \"%s\"", metadata_store.sort_name); + metadata_packet_item_changed = 1; } free(cs); break; @@ -410,6 +447,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin cs = strndup(data, length); if (string_update(&metadata_store.sort_artist, &metadata_store.sort_artist_changed, cs)) { debug(2, "MH Sort Artist set to: \"%s\"", metadata_store.sort_artist); + metadata_packet_item_changed = 1; } free(cs); break; @@ -417,6 +455,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin cs = strndup(data, length); if (string_update(&metadata_store.sort_album, &metadata_store.sort_album_changed, cs)) { debug(2, "MH Sort Album set to: \"%s\"", metadata_store.sort_album); + metadata_packet_item_changed = 1; } free(cs); break; @@ -424,6 +463,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin cs = strndup(data, length); if (string_update(&metadata_store.sort_composer, &metadata_store.sort_composer_changed, cs)) { debug(2, "MH Sort Composer set to: \"%s\"", metadata_store.sort_composer); + metadata_packet_item_changed = 1; } free(cs); default: @@ -456,43 +496,45 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin break; case 'mdst': debug(2, "MH Metadata stream processing start."); - metadata_hub_modify_prolog(); + metadata_packet_item_changed = 0; break; case 'mden': debug(2, "MH Metadata stream processing end."); - metadata_hub_modify_epilog(1); - debug(2, "MH Metadata stream processing epilog complete."); + changed = metadata_packet_item_changed; break; case 'PICT': - metadata_hub_modify_prolog(); debug(2, "MH Picture received, length %u bytes.", length); + char uri[2048]; if ((length > 16) && (strcmp(config.cover_art_cache_dir,"")!=0)) { // if it's okay to write the file + // make this uncancellable + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable char *pathname = metadata_write_image_file(data, length); snprintf(uri, sizeof(uri), "file://%s", pathname); free(pathname); + pthread_setcancelstate(oldState, NULL); + } else { uri[0] = '\0'; } if (string_update(&metadata_store.cover_art_pathname, &metadata_store.cover_art_pathname_changed, uri)) // if the picture's file path is different from the stored one... - metadata_hub_modify_epilog(1); + changed = 1; else - metadata_hub_modify_epilog(0); + changed = 0; +// pthread_cleanup_pop(0); // don't remove the lock -- it'll have been done break; case 'clip': - metadata_hub_modify_prolog(); cs = strndup(data, length); if (string_update(&metadata_store.client_ip, &metadata_store.client_ip_changed, cs)) { changed = 1; debug(2, "MH Client IP set to: \"%s\"", metadata_store.client_ip); } free(cs); - metadata_hub_modify_epilog(changed); break; case 'prgr': - metadata_hub_modify_prolog(); cs = strndup(data, length); if (string_update(&metadata_store.progress_string, &metadata_store.progress_string_changed, cs)) { @@ -500,64 +542,50 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin debug(2, "MH Progress String set to: \"%s\"", metadata_store.progress_string); } free(cs); - metadata_hub_modify_epilog(changed); break; case 'svip': - metadata_hub_modify_prolog(); cs = strndup(data, length); if (string_update(&metadata_store.server_ip, &metadata_store.server_ip_changed, cs)) { changed = 1; debug(2, "MH Server IP set to: \"%s\"", metadata_store.server_ip); } free(cs); - metadata_hub_modify_epilog(changed); break; case 'abeg': - metadata_hub_modify_prolog(); changed = (metadata_store.active_state != AM_ACTIVE); metadata_store.active_state = AM_ACTIVE; - metadata_hub_modify_epilog(changed); break; case 'aend': - metadata_hub_modify_prolog(); changed = (metadata_store.active_state != AM_INACTIVE); metadata_store.active_state = AM_INACTIVE; - metadata_hub_modify_epilog(changed); break; case 'pbeg': - metadata_hub_modify_prolog(); - changed = ((metadata_store.player_state != PS_PLAYING) || (metadata_store.player_thread_active == 0)); + changed = ((metadata_store.player_state != PS_PLAYING) || + (metadata_store.player_thread_active == 0)); metadata_store.player_state = PS_PLAYING; metadata_store.player_thread_active = 1; - metadata_hub_modify_epilog(changed); break; case 'pend': - metadata_hub_modify_prolog(); - changed = ((metadata_store.player_state != PS_STOPPED) || (metadata_store.player_thread_active == 1)); + changed = ((metadata_store.player_state != PS_STOPPED) || + (metadata_store.player_thread_active == 1)); metadata_store.player_state = PS_STOPPED; metadata_store.player_thread_active = 0; - metadata_hub_modify_epilog(changed); break; case 'pfls': - metadata_hub_modify_prolog(); changed = (metadata_store.player_state != PS_PAUSED); metadata_store.player_state = PS_PAUSED; - metadata_hub_modify_epilog(changed); break; case 'pffr': // this is sent when the first frame has been received case 'prsm': - metadata_hub_modify_prolog(); changed = (metadata_store.player_state != PS_PLAYING); metadata_store.player_state = PS_PLAYING; - metadata_hub_modify_epilog(changed); break; case 'pvol': { - metadata_hub_modify_prolog(); // Note: it's assumed that the config.airplay volume has already been correctly set. - //int32_t actual_volume; - //int gv = dacp_get_volume(&actual_volume); - //metadata_hub_modify_prolog(); - //if ((gv == 200) && (metadata_store.speaker_volume != actual_volume)) { + // int32_t actual_volume; + // int gv = dacp_get_volume(&actual_volume); + // metadata_hub_modify_prolog(); + // if ((gv == 200) && (metadata_store.speaker_volume != actual_volume)) { // metadata_store.speaker_volume = actual_volume; // changed = 1; //} @@ -565,7 +593,6 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin metadata_store.airplay_volume = config.airplay_volume; changed = 1; } - metadata_hub_modify_epilog(changed); // change } break; default: { @@ -588,4 +615,6 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin } } } + pthread_cleanup_pop(0); // don't remove the lock + metadata_hub_modify_epilog(changed); } diff --git a/metadata_hub.h b/metadata_hub.h index 75d2f8527..760a848bd 100644 --- a/metadata_hub.h +++ b/metadata_hub.h @@ -148,9 +148,17 @@ void metadata_hub_release_track_artwork(void); // these functions lock and unlock the read-write mutex on the metadata hub and run the watchers // afterwards -void metadata_hub_modify_prolog(void); -void metadata_hub_modify_epilog(int modified); // set to true if modifications occurred, 0 otherwise +void _metadata_hub_modify_prolog(const char *filename, const int linenumber); +void _metadata_hub_modify_epilog(int modified, const char *filename, const int linenumber); // set to true if modifications occurred, 0 otherwise +/* // these are for safe reading -void metadata_hub_read_prolog(void); -void metadata_hub_read_epilog(void); +void _metadata_hub_read_prolog(const char *filename, const int linenumber); +void _metadata_hub_read_epilog(const char *filename, const int linenumber); +*/ + +#define metadata_hub_modify_prolog(void) _metadata_hub_modify_prolog(__FILE__, __LINE__) +#define metadata_hub_modify_epilog(modified) _metadata_hub_modify_epilog(modified, __FILE__, __LINE__) + +#define metadata_hub_read_prolog(void) _metadata_hub_read_prolog(__FILE__, __LINE__) +#define metadata_hub_read_epilog(void) _metadata_hub_modify_epilog(__FILE__, __LINE__) diff --git a/mpris-service.c b/mpris-service.c index 125398a1a..9851f0b72 100644 --- a/mpris-service.c +++ b/mpris-service.c @@ -23,9 +23,9 @@ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ +#include #include #include -#include #include "config.h" @@ -48,12 +48,12 @@ double airplay_volume_to_mpris_volume(double sp) { sp = -30.0; if (sp > 0.0) sp = 0.0; - sp = (sp/30.0)+1; + sp = (sp / 30.0) + 1; return sp; } double mpris_volume_to_airplay_volume(double sp) { - sp = (sp-1.0)*30.0; + sp = (sp - 1.0) * 30.0; if (sp < -30.0) sp = -30.0; if (sp > 0.0) @@ -64,7 +64,8 @@ double mpris_volume_to_airplay_volume(double sp) { void mpris_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) void *userdata) { // debug(1, "MPRIS metadata watcher called"); char response[100]; - media_player2_player_set_volume(mprisPlayerPlayerSkeleton, airplay_volume_to_mpris_volume(argc->airplay_volume)); + media_player2_player_set_volume(mprisPlayerPlayerSkeleton, + airplay_volume_to_mpris_volume(argc->airplay_volume)); switch (argc->repeat_status) { case RS_NOT_AVAILABLE: strcpy(response, "Not Available"); @@ -175,7 +176,8 @@ void mpris_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused) // Add in the Track ID based on the 'mper' metadata if it is non-zero if (argc->item_id != 0) { char trackidstring[128]; - snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/%" PRIX64 "", argc->item_id); + snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/%" PRIX64 "", + argc->item_id); GVariant *trackid = g_variant_new("o", trackidstring); g_variant_builder_add(dict_builder, "{sv}", "mpris:trackid", trackid); } @@ -276,8 +278,8 @@ static gboolean on_handle_play(MediaPlayer2Player *skeleton, GDBusMethodInvocati } static gboolean on_handle_set_volume(MediaPlayer2Player *skeleton, - GDBusMethodInvocation *invocation, const gdouble volume, - __attribute__((unused)) gpointer user_data) { + GDBusMethodInvocation *invocation, const gdouble volume, + __attribute__((unused)) gpointer user_data) { double ap_volume = mpris_volume_to_airplay_volume(volume); debug(2, "Set mpris volume to %.6f, i.e. airplay volume to %.6f.", volume, ap_volume); char command[256] = ""; @@ -334,7 +336,6 @@ static void on_mpris_name_acquired(GDBusConnection *connection, const gchar *nam g_signal_connect(mprisPlayerPlayerSkeleton, "handle-set-volume", G_CALLBACK(on_handle_set_volume), NULL); - add_metadata_watcher(mpris_metadata_watcher, NULL); debug(1, "MPRIS service started at \"%s\" on the %s bus.", name, @@ -344,7 +345,7 @@ static void on_mpris_name_acquired(GDBusConnection *connection, const gchar *nam static void on_mpris_name_lost_again(__attribute__((unused)) GDBusConnection *connection, const gchar *name, __attribute__((unused)) gpointer user_data) { - warn("Could not acquire an MPRIS interface named \"%s\" on the %s bus.", name, + warn("could not acquire an MPRIS interface named \"%s\" on the %s bus.", name, (config.mpris_service_bus_type == DBT_session) ? "session" : "system"); } diff --git a/mqtt.c b/mqtt.c index f1726f219..ad5a4c407 100644 --- a/mqtt.c +++ b/mqtt.c @@ -24,10 +24,10 @@ void _cb_log(__attribute__((unused)) struct mosquitto *mosq, __attribute__((unus int level, const char *str) { switch (level) { case MOSQ_LOG_DEBUG: - debug(1, str); + debug(3, str); break; case MOSQ_LOG_INFO: - debug(2, str); + debug(3, str); break; case MOSQ_LOG_NOTICE: debug(3, str); @@ -50,7 +50,7 @@ void on_message(__attribute__((unused)) struct mosquitto *mosq, memcpy(payload, msg->payload, msg->payloadlen); payload[msg->payloadlen] = 0; - debug(1, "[MQTT]: received Message on topic %s: %s\n", msg->topic, payload); + debug(2, "[MQTT]: received Message on topic %s: %s\n", msg->topic, payload); // All recognized commands char *commands[] = {"command", "beginff", "beginrew", "mutetoggle", "nextitem", @@ -63,7 +63,7 @@ void on_message(__attribute__((unused)) struct mosquitto *mosq, while (commands[it] != NULL) { if ((size_t)msg->payloadlen >= strlen(commands[it]) && strncmp(msg->payload, commands[it], strlen(commands[it])) == 0) { - debug(1, "[MQTT]: DACP Command: %s\n", commands[it]); + debug(2, "[MQTT]: DACP Command: %s\n", commands[it]); send_simple_dacp_command(commands[it]); break; } @@ -74,13 +74,13 @@ void on_message(__attribute__((unused)) struct mosquitto *mosq, void on_disconnect(__attribute__((unused)) struct mosquitto *mosq, __attribute__((unused)) void *userdata, __attribute__((unused)) int rc) { connected = 0; - debug(1, "[MQTT]: disconnected"); + debug(2, "[MQTT]: disconnected"); } void on_connect(struct mosquitto *mosq, __attribute__((unused)) void *userdata, __attribute__((unused)) int rc) { connected = 1; - debug(1, "[MQTT]: connected"); + debug(2, "[MQTT]: connected"); // subscribe if requested if (config.mqtt_enable_remote) { @@ -95,7 +95,7 @@ void mqtt_publish(char *topic, char *data, uint32_t length) { char fulltopic[strlen(config.mqtt_topic) + strlen(topic) + 3]; snprintf(fulltopic, strlen(config.mqtt_topic) + strlen(topic) + 2, "%s/%s", config.mqtt_topic, topic); - debug(1, "[MQTT]: publishing under %s", fulltopic); + debug(2, "[MQTT]: publishing under %s", fulltopic); int rc; if ((rc = mosquitto_publish(global_mosq, NULL, fulltopic, length, data, 0, 0)) != diff --git a/player.c b/player.c index 5876bd334..0ce0df850 100644 --- a/player.c +++ b/player.c @@ -4,7 +4,7 @@ * All rights reserved. * * Modifications for audio synchronisation - * and related work, copyright (c) Mike Brady 2014 -- 2019 + * and related work, copyright (c) Mike Brady 2014 -- 2020 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -91,6 +91,14 @@ #include "activity_monitor.h" +// make the first audio packet deliberately early to bias the sync error of +// the very first packet, making the error more likely to be too early +// rather than too late. It it's too early, +// a delay exactly compensating for it can be sent just before the +// first packet. This should exactly compensate for the error. + +int64_t first_frame_early_bias = 8; + // default buffer size // needs to be a power of 2 because of the way BUFIDX(seqno) works //#define BUFFER_FRAMES 512 @@ -123,8 +131,11 @@ static void ab_resync(rtsp_conn_info *conn) { for (i = 0; i < BUFFER_FRAMES; i++) { conn->audio_buffer[i].ready = 0; conn->audio_buffer[i].resend_request_number = 0; - conn->audio_buffer[i].resend_time = 0; // this is either zero or the time the last resend was requested. - conn->audio_buffer[i].initialisation_time = 0; // this is either the time the packet was received or the time it was noticed the packet was missing. + conn->audio_buffer[i].resend_time = + 0; // this is either zero or the time the last resend was requested. + conn->audio_buffer[i].initialisation_time = + 0; // this is either the time the packet was received or the time it was noticed the packet + // was missing. conn->audio_buffer[i].sequence_number = 0; } conn->ab_synced = 0; @@ -132,6 +143,7 @@ static void ab_resync(rtsp_conn_info *conn) { conn->ab_buffering = 1; } + // given starting and ending points as unsigned 16-bit integers running modulo 2^16, returns the // position of x in the interval in *pos // returns true if x is actually within the buffer @@ -162,80 +174,57 @@ int position_in_modulo_uint16_t_buffer(uint16_t x, uint16_t start, uint16_t end, return response; } -// given starting and ending points as unsigned 32-bit integers running modulo 2^32, returns the -// position of x in the interval in *pos -// returns true if x is actually within the buffer - -int position_in_modulo_uint32_t_buffer(uint32_t x, uint32_t start, uint32_t end, uint32_t *pos) { - int response = 0; // not in the buffer - if (start <= end) { - if (x < start) { - if (pos) - *pos = UINT32_MAX - start + 1 + x; - } else { - if (pos) - *pos = x - start; - if (x < end) - response = 1; - } - } else if ((x >= start)) { // && (x <= UINT32_MAX)) { // always true - response = 1; - if (pos) - *pos = x - start; - } else { - if (pos) - *pos = UINT32_MAX - start + 1 + x; - if (x < end) { - response = 1; - } - } - return response; -} - -// this is used. static inline seq_t SUCCESSOR(seq_t x) { - uint32_t p = x & 0xffff; - p += 1; - p = p & 0xffff; - return p; + return x + 1; } -// used in seq_diff and seq_order - -// anything with ORDINATE in it must be protected by the ab_mutex -static inline int32_t ORDINATE(seq_t x, seq_t base) { - int32_t p = x; // int32_t from seq_t, i.e. uint16_t, so okay - int32_t q = base; // int32_t from seq_t, i.e. uint16_t, so okay - int32_t t = (p + 0x10000 - q) & 0xffff; - // we definitely will get a positive number in t at this point, but it might be a - // positive alias of a negative number, i.e. x might actually be "before" ab_read - // So, if the result is greater than 32767, we will assume its an - // alias and subtract 65536 from it - if (t >= 32767) { - // debug(1,"OOB: %u, ab_r: %u, ab_w: %u",x,ab_read,ab_write); - t -= 65536; - } - return t; +// a minus b +int16_t seq_diff(seq_t a, seq_t b) { + int16_t response; + seq_t diff = a - b; + seq_t invdiff = b - a; + if (diff < invdiff) + response = diff; + else + response = -invdiff; + return response; } -// wrapped number between two seq_t. -int32_t seq_diff(seq_t a, seq_t b, seq_t base) { - int32_t diff = ORDINATE(b, base) - ORDINATE(a, base); - return diff; +static inline seq_t seq_sum(seq_t a, seq_t b) { + return a + b; } -// the sequence numbers will wrap pretty often. -// this returns true if the second arg is after the first -static inline int seq_order(seq_t a, seq_t b, seq_t base) { - int32_t d = ORDINATE(b, base) - ORDINATE(a, base); - return d > 0; -} - -static inline seq_t seq_sum(seq_t a, seq_t b) { - // uint32_t p = a & 0xffff; - // uint32_t q = b & 0x0ffff; - uint32_t r = (a + b) & 0xffff; - return r; +// This orders u and v by picking the smaller of the two modulo differences +// in unsigned modulo arithmetic and setting the sign of the result accordingly. + +// if u - v gives the smaller modulo difference, then that modulo difference is returned +// otherwise the negative of the v - u modulo difference is returned. + +// (Think of a modulo ring starting at 0 and circling around clockwise finally to +// modulo-2, modulo-1 to 0 again. To determine the ordering of the two numbers, +// Find the shorter path between them around the ring. +// If you go from v to u in the positive (0, 1, 2 ... or clockwise) direction, +// then u is greater than (or "after") v.) + +// The result will always fit in an int64_t as it must be less or equal to half way +// around the ring. +// Due to the asymmetry of 2's complement representation, however, +// if the difference is equal to modulo / 2, it is ambiguous as to whether +// u is "after" or "before" (i.e. greater or less than) v. +// and will be returned as -modulo / 2, that is, "before". +// If that ever happens and there is a real ambiguity in the application, +// the modulo chosen is too small. + + +int64_t int64_mod_difference(const uint64_t u, const uint64_t v, const uint64_t modulo) { + int64_t response; + uint64_t diff = (u - v) % modulo; + uint64_t invdiff = (v - u) % modulo; + if (diff < invdiff) + response = diff; + else + response = -invdiff; + return response; } void reset_input_flow_metrics(rtsp_conn_info *conn) { @@ -323,16 +312,18 @@ int audio_packet_decode(short *dest, int *destlen, uint8_t *buf, int len, rtsp_c } if (outsize > maximum_possible_outsize) { - debug(2, "Output from alac_decode larger (%d bytes, not frames) than expected (%d bytes) -- " - "truncated, but buffer overflow possible! Encrypted = %d.", + debug(2, + "Output from alac_decode larger (%d bytes, not frames) than expected (%d bytes) -- " + "truncated, but buffer overflow possible! Encrypted = %d.", outsize, maximum_possible_outsize, conn->stream.encrypted); reply = -1; // output packet is the wrong size } *destlen = outsize / conn->input_bytes_per_frame; if ((outsize % conn->input_bytes_per_frame) != 0) - debug(1, "Number of audio frames (%d) does not correspond exactly to the number of bytes (%d) " - "and the audio frame size (%d).", + debug(1, + "Number of audio frames (%d) does not correspond exactly to the number of bytes (%d) " + "and the audio frame size (%d).", *destlen, outsize, conn->input_bytes_per_frame); return reply; } @@ -469,183 +460,178 @@ int first_possibly_missing_frame = -1; void player_put_packet(seq_t seqno, uint32_t actual_timestamp, uint8_t *data, int len, rtsp_conn_info *conn) { - // ignore a request to flush that has been made before the first packet... - if (conn->packet_count == 0) { - debug_mutex_lock(&conn->flush_mutex, 1000, 1); - conn->flush_requested = 0; - conn->flush_rtp_timestamp = 0; - debug_mutex_unlock(&conn->flush_mutex, 3); - } - debug_mutex_lock(&conn->ab_mutex, 30000, 0); - uint64_t time_now = get_absolute_time_in_fp(); + uint64_t time_now = get_absolute_time_in_ns(); conn->packet_count++; conn->packet_count_since_flush++; conn->time_of_last_audio_packet = time_now; if (conn->connection_state_to_output) { // if we are supposed to be processing these packets - if ((conn->flush_rtp_timestamp != 0) && (actual_timestamp != conn->flush_rtp_timestamp) && - (modulo_32_offset(actual_timestamp, conn->flush_rtp_timestamp) < - conn->input_rate * 10)) { // if it's less than 10 seconds - debug(3, "Dropping flushed packet in player_put_packet, seqno %u, timestamp %" PRIu32 - ", flushing to " - "timestamp: %" PRIu32 ".", - seqno, actual_timestamp, conn->flush_rtp_timestamp); - conn->initial_reference_time = 0; - conn->initial_reference_timestamp = 0; - } else { - abuf_t *abuf = 0; - if (!conn->ab_synced) { - // if this is the first packet... - debug(3, "syncing to seqno %u.", seqno); - conn->ab_write = seqno; - conn->ab_read = seqno; - conn->ab_synced = 1; - } - if (conn->ab_write == - seqno) { // if this is the expected packet (which could be the first packet...) - if (conn->input_frame_rate_starting_point_is_valid == 0) { - if ((conn->packet_count_since_flush >= 500) && (conn->packet_count_since_flush <= 510)) { - conn->frames_inward_measurement_start_time = time_now; - conn->frames_inward_frames_received_at_measurement_start_time = actual_timestamp; - conn->input_frame_rate_starting_point_is_valid = 1; // valid now - } - } - conn->frames_inward_measurement_time = time_now; - conn->frames_inward_frames_received_at_measurement_time = actual_timestamp; - abuf = conn->audio_buffer + BUFIDX(seqno); - conn->ab_write = SUCCESSOR(seqno); // move the write pointer to the next free space - } else if (seq_order(conn->ab_write, seqno, conn->ab_read)) { // newer than expected - int32_t gap = seq_diff(conn->ab_write, seqno, conn->ab_read); - if (gap <= 0) - debug(1, "Unexpected gap size: %d.", gap); - int i; - for (i = 0; i < gap; i++) { - abuf = conn->audio_buffer + BUFIDX(seq_sum(conn->ab_write, i)); - abuf->ready = 0; // to be sure, to be sure - abuf->resend_request_number = 0; - abuf->initialisation_time = time_now; // this represents when the packet was noticed to be missing - abuf->status = 1 << 0; // signifying missing - abuf->resend_time = 0; - abuf->given_timestamp = 0; - abuf->sequence_number = 0; - } - // debug(1,"N %d s %u.",seq_diff(ab_write,PREDECESSOR(seqno))+1,ab_write); - abuf = conn->audio_buffer + BUFIDX(seqno); - // rtp_request_resend(ab_write, gap); - // resend_requests++; - conn->ab_write = SUCCESSOR(seqno); - } else if (seq_order(conn->ab_read, seqno, conn->ab_read)) { // older than expected but not too late - conn->late_packets++; - abuf = conn->audio_buffer + BUFIDX(seqno); - } else { // too late. - conn->too_late_packets++; - } - - if (abuf) { - int datalen = conn->max_frames_per_packet; - abuf->initialisation_time = time_now; - abuf->resend_time = 0; - if (audio_packet_decode(abuf->data, &datalen, data, len, conn) == 0) { - abuf->ready = 1; - abuf->status = 0; // signifying that it was received - abuf->length = datalen; - abuf->given_timestamp = actual_timestamp; - abuf->sequence_number = seqno; - } else { - debug(1, "Bad audio packet detected and discarded."); - abuf->ready = 0; - abuf->status = 1 << 1; // bad packet, discarded - abuf->resend_request_number = 0; - abuf->given_timestamp = 0; - abuf->sequence_number = 0; - } - } - - int rc = pthread_cond_signal(&conn->flowcontrol); - if (rc) - debug(1, "Error signalling flowcontrol."); - - // resend checks - { - uint64_t minimum_wait_time = (uint64_t)(config.resend_control_first_check_time * (uint64_t)0x100000000); - uint64_t resend_repeat_interval = (uint64_t)(config.resend_control_check_interval_time * (uint64_t)0x100000000); - uint64_t minimum_remaining_time = (uint64_t)((config.resend_control_last_check_time + config.audio_backend_buffer_desired_length)* (uint64_t)0x100000000); - uint64_t latency_time = (uint64_t)(conn->latency * (uint64_t)0x100000000); - latency_time = latency_time / (uint64_t)conn->input_rate; - - int x; // this is the first frame to be checked - // if we detected a first empty frame before and if it's still in the buffer! - if ((first_possibly_missing_frame >= 0) && (position_in_modulo_uint16_t_buffer(first_possibly_missing_frame, conn->ab_read, conn->ab_write, NULL))) { - x = first_possibly_missing_frame; - } else { - x = conn->ab_read; - } - - first_possibly_missing_frame = -1; // has not been set - - int missing_frame_run_count = 0; - int start_of_missing_frame_run = -1; - int number_of_missing_frames = 0; - while (x != conn->ab_write) { - abuf_t *check_buf = conn->audio_buffer + BUFIDX(x); - if (!check_buf->ready) { - if (first_possibly_missing_frame < 0) - first_possibly_missing_frame = x; - number_of_missing_frames++; - // debug(1, "frame %u's initialisation_time is 0x%" PRIx64 ", latency_time is 0x%" PRIx64 ", time_now is 0x%" PRIx64 ", minimum_remaining_time is 0x%" PRIx64 ".", x, check_buf->initialisation_time, latency_time, time_now, minimum_remaining_time); - int too_late = ((check_buf->initialisation_time < (time_now - latency_time)) || ((check_buf->initialisation_time - (time_now - latency_time)) < minimum_remaining_time)); - int too_early = ((time_now - check_buf->initialisation_time) < minimum_wait_time); - int too_soon_after_last_request = ((check_buf->resend_time != 0) && ((time_now - check_buf->resend_time) < resend_repeat_interval)); // time_now can never be less than the time_tag - - if (too_late) - check_buf->status |= 1<<2; // too late - else - check_buf->status &= 0xFF-(1<<2); // not too late - if (too_early) - check_buf->status |= 1<<3; // too early - else - check_buf->status &= 0xFF-(1<<3); // not too early - if (too_soon_after_last_request) - check_buf->status |= 1<<4; // too soon after last request - else - check_buf->status &= 0xFF-(1<<4); // not too soon after last request - - if ((!too_soon_after_last_request) && (!too_late) && (!too_early)){ - if (start_of_missing_frame_run == -1) { - start_of_missing_frame_run = x; - missing_frame_run_count = 1; - } else { - missing_frame_run_count++; - } - check_buf->resend_time = time_now; // setting the time to now because we are definitely going to take action - check_buf->resend_request_number++; - debug(3,"Frame %d is missing with ab_read of %u and ab_write of %u.", x, conn->ab_read, conn->ab_write); - } - // if (too_late) { - // debug(1,"too late to get missing frame %u.", x); - // } - } - //if (number_of_missing_frames != 0) - // debug(1,"check with x = %u, ab_read = %u, ab_write = %u, first_possibly_missing_frame = %d.", x, conn->ab_read, conn->ab_write, first_possibly_missing_frame); - x = (x + 1) & 0xffff; - if (((check_buf->ready) || (x == conn->ab_write)) && (missing_frame_run_count > 0)) { - // send a resend request - if (missing_frame_run_count > 1) - debug(2,"request resend of %d packets starting at seqno %u.", missing_frame_run_count, start_of_missing_frame_run); - if (config.disable_resend_requests == 0) { - debug_mutex_unlock(&conn->ab_mutex, 3); - rtp_request_resend(start_of_missing_frame_run, missing_frame_run_count, conn); - debug_mutex_lock(&conn->ab_mutex, 20000, 1); - conn->resend_requests++; - } - start_of_missing_frame_run = -1; - missing_frame_run_count = 0; - } - } - if (number_of_missing_frames == 0) - first_possibly_missing_frame = conn->ab_write; - } - } + abuf_t *abuf = 0; + if (!conn->ab_synced) { + // if this is the first packet... + debug(3, "syncing to seqno %u.", seqno); + conn->ab_write = seqno; + conn->ab_read = seqno; + conn->ab_synced = 1; + } + int16_t write_point_gap = seq_diff(seqno,conn->ab_write); // this is the difference between + // the incoming packet number and the packet number that was expected. + if (write_point_gap == 0) { // if this is the expected packet (which could be the first packet...) + if (conn->input_frame_rate_starting_point_is_valid == 0) { + if ((conn->packet_count_since_flush >= 500) && (conn->packet_count_since_flush <= 510)) { + conn->frames_inward_measurement_start_time = time_now; + conn->frames_inward_frames_received_at_measurement_start_time = actual_timestamp; + conn->input_frame_rate_starting_point_is_valid = 1; // valid now + } + } + conn->frames_inward_measurement_time = time_now; + conn->frames_inward_frames_received_at_measurement_time = actual_timestamp; + abuf = conn->audio_buffer + BUFIDX(seqno); + conn->ab_write = SUCCESSOR(seqno); // move the write pointer to the next free space + } else if (write_point_gap > 0) { // newer than expected + // initialise the frames in between + int i; + for (i = 0; i < write_point_gap; i++) { + abuf = conn->audio_buffer + BUFIDX(seq_sum(conn->ab_write, i)); + abuf->ready = 0; // to be sure, to be sure + abuf->resend_request_number = 0; + abuf->initialisation_time = + time_now; // this represents when the packet was noticed to be missing + abuf->status = 1 << 0; // signifying missing + abuf->resend_time = 0; + abuf->given_timestamp = 0; + abuf->sequence_number = 0; + } + abuf = conn->audio_buffer + BUFIDX(seqno); + conn->ab_write = SUCCESSOR(seqno); + } else if (seq_diff(seqno,conn->ab_read) > 0) { // older than expected but still not too late + conn->late_packets++; + abuf = conn->audio_buffer + BUFIDX(seqno); + } else { // too late. + conn->too_late_packets++; + } + + if (abuf) { + int datalen = conn->max_frames_per_packet; + abuf->initialisation_time = time_now; + abuf->resend_time = 0; + if (audio_packet_decode(abuf->data, &datalen, data, len, conn) == 0) { + abuf->ready = 1; + abuf->status = 0; // signifying that it was received + abuf->length = datalen; + abuf->given_timestamp = actual_timestamp; + abuf->sequence_number = seqno; + } else { + debug(1, "Bad audio packet detected and discarded."); + abuf->ready = 0; + abuf->status = 1 << 1; // bad packet, discarded + abuf->resend_request_number = 0; + abuf->given_timestamp = 0; + abuf->sequence_number = 0; + } + } + + int rc = pthread_cond_signal(&conn->flowcontrol); + if (rc) + debug(1, "Error signalling flowcontrol."); + + // resend checks + { + uint64_t minimum_wait_time = + (uint64_t)(config.resend_control_first_check_time * (uint64_t)1000000000); + uint64_t resend_repeat_interval = + (uint64_t)(config.resend_control_check_interval_time * (uint64_t)1000000000); + uint64_t minimum_remaining_time = (uint64_t)( + (config.resend_control_last_check_time + config.audio_backend_buffer_desired_length) * + (uint64_t)1000000000); + uint64_t latency_time = (uint64_t)(conn->latency * (uint64_t)1000000000); + latency_time = latency_time / (uint64_t)conn->input_rate; + + int x; // this is the first frame to be checked + // if we detected a first empty frame before and if it's still in the buffer! + if ((first_possibly_missing_frame >= 0) && + (position_in_modulo_uint16_t_buffer(first_possibly_missing_frame, conn->ab_read, + conn->ab_write, NULL))) { + x = first_possibly_missing_frame; + } else { + x = conn->ab_read; + } + + first_possibly_missing_frame = -1; // has not been set + + int missing_frame_run_count = 0; + int start_of_missing_frame_run = -1; + int number_of_missing_frames = 0; + while (x != conn->ab_write) { + abuf_t *check_buf = conn->audio_buffer + BUFIDX(x); + if (!check_buf->ready) { + if (first_possibly_missing_frame < 0) + first_possibly_missing_frame = x; + number_of_missing_frames++; + // debug(1, "frame %u's initialisation_time is 0x%" PRIx64 ", latency_time is 0x%" + // PRIx64 ", time_now is 0x%" PRIx64 ", minimum_remaining_time is 0x%" PRIx64 ".", x, + // check_buf->initialisation_time, latency_time, time_now, minimum_remaining_time); + int too_late = ((check_buf->initialisation_time < (time_now - latency_time)) || + ((check_buf->initialisation_time - (time_now - latency_time)) < + minimum_remaining_time)); + int too_early = ((time_now - check_buf->initialisation_time) < minimum_wait_time); + int too_soon_after_last_request = + ((check_buf->resend_time != 0) && + ((time_now - check_buf->resend_time) < + resend_repeat_interval)); // time_now can never be less than the time_tag + + if (too_late) + check_buf->status |= 1 << 2; // too late + else + check_buf->status &= 0xFF - (1 << 2); // not too late + if (too_early) + check_buf->status |= 1 << 3; // too early + else + check_buf->status &= 0xFF - (1 << 3); // not too early + if (too_soon_after_last_request) + check_buf->status |= 1 << 4; // too soon after last request + else + check_buf->status &= 0xFF - (1 << 4); // not too soon after last request + + if ((!too_soon_after_last_request) && (!too_late) && (!too_early)) { + if (start_of_missing_frame_run == -1) { + start_of_missing_frame_run = x; + missing_frame_run_count = 1; + } else { + missing_frame_run_count++; + } + check_buf->resend_time = time_now; // setting the time to now because we are + // definitely going to take action + check_buf->resend_request_number++; + debug(3, "Frame %d is missing with ab_read of %u and ab_write of %u.", x, + conn->ab_read, conn->ab_write); + } + // if (too_late) { + // debug(1,"too late to get missing frame %u.", x); + // } + } + // if (number_of_missing_frames != 0) + // debug(1,"check with x = %u, ab_read = %u, ab_write = %u, first_possibly_missing_frame + // = %d.", x, conn->ab_read, conn->ab_write, first_possibly_missing_frame); + x = (x + 1) & 0xffff; + if (((check_buf->ready) || (x == conn->ab_write)) && (missing_frame_run_count > 0)) { + // send a resend request + if (missing_frame_run_count > 1) + debug(3, "request resend of %d packets starting at seqno %u.", + missing_frame_run_count, start_of_missing_frame_run); + if (config.disable_resend_requests == 0) { + debug_mutex_unlock(&conn->ab_mutex, 3); + rtp_request_resend(start_of_missing_frame_run, missing_frame_run_count, conn); + debug_mutex_lock(&conn->ab_mutex, 20000, 1); + conn->resend_requests++; + } + start_of_missing_frame_run = -1; + missing_frame_run_count = 0; + } + } + if (number_of_missing_frames == 0) + first_possibly_missing_frame = conn->ab_write; + } } debug_mutex_unlock(&conn->ab_mutex, 0); } @@ -660,6 +646,31 @@ int32_t rand_in_range(int32_t exclusive_range_limit) { return sp >> 32; } +int get_and_check_effective_latency(rtsp_conn_info *conn, uint32_t *effective_latency, + double offset_time) { + // check that the overall effective latency remains positive and is not greater than the capacity + // of the buffers return 0 if okay, -1 if the latency would be negative, +1 if it would be too + // large + + int result = 0; + if (offset_time >= 0.0) { + uint32_t latency_addition = (uint32_t)(offset_time * conn->input_rate); + // keep about one second of buffers back + if ((*effective_latency + latency_addition) <= + (conn->max_frames_per_packet * (BUFFER_FRAMES - config.minimum_free_buffer_headroom))) + *effective_latency += latency_addition; + else + result = 1; + } else { + uint32_t latency_reduction = (uint32_t)((-offset_time) * conn->input_rate); + if (latency_reduction <= *effective_latency) + *effective_latency -= latency_reduction; + else + result = -1; + } + return result; +} + static inline void process_sample(int32_t sample, char **outp, sps_format_t format, int volume, int dither, rtsp_conn_info *conn) { /* @@ -898,11 +909,14 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { int wait; long dac_delay = 0; // long because alsa returns a long + int have_sent_prefiller_silence = + 0; // set to true when we have sent at least one silent frame to the DAC + pthread_cleanup_push(buffer_get_frame_cleanup_handler, (void *)conn); // undo what's been done so far do { // get the time - local_time_now = get_absolute_time_in_fp(); // type okay + local_time_now = get_absolute_time_in_ns(); // type okay // debug(3, "buffer_get_frame is iterating"); int rco = get_requested_connection_state_to_output(); @@ -911,32 +925,97 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { conn->connection_state_to_output = rco; // change happening if (conn->connection_state_to_output == 0) { // going off + debug(2, "request flush because connection_state_to_output is off"); debug_mutex_lock(&conn->flush_mutex, 1000, 1); conn->flush_requested = 1; + conn->flush_rtp_timestamp = 0; debug_mutex_unlock(&conn->flush_mutex, 3); } } if (config.output->is_running) if (config.output->is_running() != 0) { // if the back end isn't running for any reason - debug(3, "not running"); + debug(2, "request flush because back end is not running"); debug_mutex_lock(&conn->flush_mutex, 1000, 0); conn->flush_requested = 1; + conn->flush_rtp_timestamp = 0; debug_mutex_unlock(&conn->flush_mutex, 0); } debug_mutex_lock(&conn->flush_mutex, 1000, 0); if (conn->flush_requested == 1) { - if (config.output->flush) - config.output->flush(); // no cancellation points - ab_resync(conn); // no cancellation points - conn->first_packet_timestamp = 0; - conn->first_packet_time_to_play = 0; - conn->time_since_play_started = 0; - conn->flush_requested = 0; + if (conn->flush_output_flushed == 0) + if (config.output->flush) { + config.output->flush(); // no cancellation points + debug(2, "flush request: flush output device."); + } + conn->flush_output_flushed = 1; + // now check to see it the flush request is for frames in the buffer or not + // if the first_packet_timestamp is zero, don't check + int flush_needed = 0; + int drop_request = 0; + if (conn->flush_rtp_timestamp == 0) { + debug(1, "flush request: flush frame 0 -- flush assumed to be needed."); + flush_needed = 1; + drop_request = 1; + } else { + if ((conn->ab_synced) && ((conn->ab_write - conn->ab_read) > 0)) { + abuf_t *firstPacket = conn->audio_buffer + BUFIDX(conn->ab_read); + abuf_t *lastPacket = conn->audio_buffer + BUFIDX(conn->ab_write - 1); + if ((firstPacket != NULL) && (firstPacket->ready)) { + // discard flushes more than 10 seconds into the future -- they are probably bogus + uint32_t first_frame_in_buffer = firstPacket->given_timestamp; + int32_t offset_from_first_frame = (int32_t)(conn->flush_rtp_timestamp - first_frame_in_buffer); + if (offset_from_first_frame > (int)conn->input_rate * 10) { + debug(1, "flush request: sanity check -- flush frame %u is too far into the future from the first frame %u -- discarded.", conn->flush_rtp_timestamp, first_frame_in_buffer); + drop_request = 1; + } else { + if ((lastPacket != NULL) && (lastPacket->ready)) { + // we have enough information to check if the flush is needed or can be discarded + uint32_t last_frame_in_buffer = lastPacket->given_timestamp + lastPacket->length - 1; + // now we have to work out if the flush frame is in the buffer + // if it is later than the end of the buffer, flush everything and keep the request active. + // if it is in the buffer, we need to flush part of the buffer. Actually we flush the entire buffer and drop the request. + // if it is before the buffer, no flush is needed. Drop the request. + if (offset_from_first_frame > 0) { + int32_t offset_to_last_frame = (int32_t)(last_frame_in_buffer - conn->flush_rtp_timestamp); + if (offset_to_last_frame >= 0) { + debug(2,"flush request: flush frame %u active -- buffer contains %u frames, from %u to %u", conn->flush_rtp_timestamp, last_frame_in_buffer - first_frame_in_buffer + 1, first_frame_in_buffer, last_frame_in_buffer); + drop_request = 1; + flush_needed = 1; + } else { + debug(2,"flush request: flush frame %u pending -- buffer contains %u frames, from %u to %u", conn->flush_rtp_timestamp, last_frame_in_buffer - first_frame_in_buffer + 1, first_frame_in_buffer, last_frame_in_buffer); + flush_needed = 1; + } + } else { + debug(2,"flush request: flush frame %u expired -- buffer contains %u frames, from %u to %u", conn->flush_rtp_timestamp, last_frame_in_buffer - first_frame_in_buffer + 1, first_frame_in_buffer, last_frame_in_buffer); + drop_request = 1; + } + } + } + } + } else { + debug(2, "flush request: flush frame %u -- buffer not synced or empty: synced: %d, ab_read: %u, ab_write: %u", conn->flush_rtp_timestamp, conn->ab_synced, conn->ab_read, conn->ab_write); + // leave flush request pending and don't do a buffer flush, because there isn't one + } + } + if (flush_needed) { + debug(2, "flush request: flush done."); + ab_resync(conn); // no cancellation points + conn->first_packet_timestamp = 0; + conn->first_packet_time_to_play = 0; + conn->time_since_play_started = 0; + have_sent_prefiller_silence = 0; + dac_delay = 0; + } + if (drop_request) { + debug(2, "flush request: request dropped."); + conn->flush_requested = 0; + conn->flush_rtp_timestamp = 0; + conn->flush_output_flushed = 0; + } } debug_mutex_unlock(&conn->flush_mutex, 0); - if (conn->ab_synced) { curframe = conn->audio_buffer + BUFIDX(conn->ab_read); @@ -948,71 +1027,29 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { if (curframe->sequence_number != conn->ab_read) { // some kind of sync problem has occurred. if (BUFIDX(curframe->sequence_number) == BUFIDX(conn->ab_read)) { - // it looks like some kind of aliasing has happened - if (seq_order(conn->ab_read, curframe->sequence_number, conn->ab_read)) { - conn->ab_read = curframe->sequence_number; - debug(1, "Aliasing of buffer index -- reset."); - } + // it looks like aliasing has happened + // jump to the new incoming stuff... + conn->ab_read = curframe->sequence_number; + debug(1, "Aliasing of buffer index -- reset."); } else { debug(1, "Inconsistent sequence numbers detected"); } } - - // if (conn->flush_rtp_timestamp != 0) - // debug(2,"flush_rtp_timestamp is %" PRIx32 " and curframe->given_timestamp is %" PRIx32 - // ".", conn->flush_rtp_timestamp , curframe->given_timestamp); - - if ((conn->flush_rtp_timestamp != 0) && - (curframe->given_timestamp != conn->flush_rtp_timestamp) && - (modulo_32_offset(curframe->given_timestamp, conn->flush_rtp_timestamp) < - conn->input_rate * 10)) { // if it's less than ten seconds - debug(3, "Dropping flushed packet in buffer_get_frame, seqno %u, timestamp %" PRIu32 - ", flushing to " - "timestamp: %" PRIu32 ".", - curframe->sequence_number, curframe->given_timestamp, conn->flush_rtp_timestamp); - curframe->ready = 0; - curframe->resend_request_number = 0; - curframe = NULL; // this will be returned and will cause the loop to go around again - conn->initial_reference_time = 0; - conn->initial_reference_timestamp = 0; - } else if ((conn->flush_rtp_timestamp != 0) && - (modulo_32_offset(conn->flush_rtp_timestamp, curframe->given_timestamp) > - conn->input_rate / 5) && - (modulo_32_offset(conn->flush_rtp_timestamp, curframe->given_timestamp) < - conn->input_rate * 10)) { - debug(3, "Dropping flush request in buffer_get_frame"); - conn->flush_rtp_timestamp = 0; - } } + if ((curframe) && (curframe->ready)) { notified_buffer_empty = 0; // at least one buffer now -- diagnostic only. if (conn->ab_buffering) { // if we are getting packets but not yet forwarding them to the // player - int have_sent_prefiller_silence = 1; // set true when we have sent some silent frames to - // the DAC - /* - int64_t reference_timestamp; - uint64_t reference_timestamp_time, remote_reference_timestamp_time; - get_reference_timestamp_stuff(&reference_timestamp, &reference_timestamp_time, - &remote_reference_timestamp_time, conn); - reference_timestamp *= conn->output_sample_ratio; - */ - if (conn->first_packet_timestamp == 0) { // if this is the very first packet - // debug(1,"First frame seen, time %u, with %d - // frames...",curframe->timestamp,seq_diff(ab_read, ab_write)); - + if (conn->first_packet_timestamp == 0) { // if this is the very first packet if (have_timestamp_timing_information(conn)) { // if we have a reference time // debug(1,"First frame seen with timestamp..."); conn->first_packet_timestamp = 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 #ifdef CONFIG_METADATA + // say we have started receiving frames here debug(2, "pffr"); send_ssnc_metadata( 'pffr', NULL, 0, @@ -1020,12 +1057,10 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { #endif // Here, calculate when we should start playing. We need to know when to allow the // packets to be sent to the player. - // We will send packets of silence from now until that time and then we will send the - // first packet, which will be followed by the subsequent packets. - // we will get a fix every second or so, which will be stored as a pair consisting of - // the time when the packet with a particular timestamp should be played, neglecting - // latencies, etc. + + // every second or so, we get a reference on when a particular packet should be + // played. // It probably won't be the timestamp of our first packet, however, so we might have // to do some calculations. @@ -1038,254 +1073,207 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { // that the audio back end has a latency of 100 ms, we would // ask for the first packet to be emitted 100 ms earlier than it should, i.e. -4410 // frames, so that when it got through the audio back end, - // if would be in sync. To do this, we would give it a latency offset of -100 ms, i.e. - // -4410 frames. - // debug(1, "Output sample ratio is %d", conn->output_sample_ratio); + // if would be in sync. To do this, we would give it a latency offset of -0.1 sec, + // i.e. -4410 frames. + + uint64_t should_be_time; + uint32_t effective_latency = conn->latency; + + get_and_check_effective_latency(conn, &effective_latency, + config.audio_backend_latency_offset); + // we are ignoring the returned status because it will be captured on subsequent + // frames, below. // what we are asking for here is "what is the local time at which time the calculated // frame should be played" - uint64_t should_be_time; - frame_to_local_time(conn->first_packet_timestamp + conn->latency + - (uint32_t)(config.audio_backend_latency_offset * - conn->input_rate), // this will go modulo 2^32 - &should_be_time, - conn); + frame_to_local_time(conn->first_packet_timestamp + + effective_latency, // this will go modulo 2^32 + &should_be_time, conn); conn->first_packet_time_to_play = should_be_time; + debug(2,"first_packet_time set for frame %u.", conn->first_packet_timestamp); + if (local_time_now > conn->first_packet_time_to_play) { uint64_t lateness = local_time_now - conn->first_packet_time_to_play; - lateness = (lateness * 1000000) >> 32; // microseconds - debug(3, "First packet is %" PRIu64 " microseconds late! Flushing 0.5 seconds", + debug(2, "First packet is %" PRIu64 " nanoseconds late! Flushing 0.5 seconds", lateness); do_flush(conn->first_packet_timestamp + 5 * 4410, conn); } } } - if (conn->first_packet_time_to_play != 0) { - // recalculate conn->first_packet_time_to_play -- the latency might change - - uint64_t should_be_time; - frame_to_local_time(conn->first_packet_timestamp + conn->latency + - (uint32_t)(config.audio_backend_latency_offset * - conn->input_rate), // this should go modulo 2^32 - &should_be_time, - 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 - // 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); - max_dac_delay = max_dac_delay / 4; - // debug(1,"max_dac_delay is %" PRIu64 " frames.", max_dac_delay); - - int64_t filler_size = max_dac_delay; - - if (local_time_now > conn->first_packet_time_to_play) { - uint64_t lateness = local_time_now - conn->first_packet_time_to_play; - lateness = (lateness * 1000000) >> 32; // microseconds - debug(3, "Gone past starting time by %" PRIu64 " microseconds.", lateness); - have_sent_prefiller_silence = 1; - conn->ab_buffering = 0; - - // we've gone past the time... - // debug(1,"Run past the exact start time by %llu frames, with time now of %llx, fpttp - // of %llx and dac_delay of %d and %d packets; - // flush.",(((tn-conn->first_packet_time_to_play)*config.output_rate)>>32)+dac_delay,tn,conn->first_packet_time_to_play,dac_delay,seq_diff(ab_read, - // ab_write)); - - /* - if (config.output->flush) - config.output->flush(); - ab_resync(conn); - conn->first_packet_timestamp = 0; - conn->first_packet_time_to_play = 0; - conn->time_since_play_started = 0; - */ - } else { - // do some calculations - int64_t lead_time = conn->first_packet_time_to_play - local_time_now; - // an audio_backend_silent_lead_in_time of less than zero means start filling ASAP - int64_t lead_in_time = -1; - if (config.audio_backend_silent_lead_in_time >= 0) - lead_in_time = - (int64_t)(config.audio_backend_silent_lead_in_time * (int64_t)0x100000000); - // debug(1,"Lead time is %llx at fpttp - // %llx.",lead_time,conn->first_packet_time_to_play); - if ((lead_in_time < 0) || (lead_time <= lead_in_time)) { - // debug(1,"Lead time is %" PRIx64 ", lead-in time is %" PRIx64 " at fpttp - // %llx.",lead_time,conn->first_packet_time_to_play); - - // debug(1,"Checking"); - if (config.output->delay) { - // conn->first_packet_time_to_play is definitely later than local_time_now - int resp = 0; - dac_delay = 0; - if (have_sent_prefiller_silence != 0) - resp = config.output->delay(&dac_delay); - if (resp == 0) { - int64_t gross_frame_gap = - ((conn->first_packet_time_to_play - local_time_now) * config.output_rate) >> - 32; - int64_t exact_frame_gap = gross_frame_gap - dac_delay; - // debug(1,"Exact and gross frame gaps are %" PRId64 " and %" PRId64 " frames, - // and the dac delay is %ld.", exact_frame_gap, gross_frame_gap, dac_delay); - if (exact_frame_gap < 0) { - // we've gone past the time... - // debug(1,"Run past time."); - - // this might happen if a big clock adjustment was made at just the wrong - // time. - - debug(1, "Run a bit past the exact start time by %" PRId64 - " frames with a DAC delay of %ld frames.", - -exact_frame_gap, dac_delay); - if (config.output->flush) - config.output->flush(); - ab_resync(conn); - conn->first_packet_timestamp = 0; - conn->first_packet_time_to_play = 0; - } else { - int64_t fs = filler_size; - if (fs > (max_dac_delay - dac_delay)) - fs = max_dac_delay - dac_delay; - if (fs < 0) { - // this could happen if the dac delay mysteriously grows between samples, - // which could happen in a transition between having no interpolation and - // having interpolated buffer numbers. - - // this will happen benignly if standby is being prevented, because a - // thread in the alsa back end will be stuffing frames of silence in there - // to keep it busy - - debug(3, - "frame size (fs) < 0 with max_dac_delay of %lld and dac_delay of %ld", - max_dac_delay, dac_delay); - fs = 0; - } - if ((exact_frame_gap <= fs) || - (exact_frame_gap <= conn->max_frames_per_packet * 2)) { - fs = exact_frame_gap; - // debug(1,"Exact frame gap is %llu; play %d frames of silence. Dac_delay is - // %d, - // with %d packets, ab_read is %04x, ab_write is - // %04x.",exact_frame_gap,fs,dac_delay,seq_diff(ab_read, - // ab_write),ab_read,ab_write); - conn->ab_buffering = 0; - } - void *silence; - // if (fs==0) - // debug(2,"Zero length silence buffer needed with gross_frame_gap of %lld - // and - // dac_delay of %lld.",gross_frame_gap,dac_delay); - // the fs (number of frames of silence to play) can be zero in the DAC doesn't - // start - // outputting frames for a while -- it could get loaded up but not start - // responding - // for many milliseconds. - if (fs > 0) { - silence = malloc(conn->output_bytes_per_frame * fs); - if (silence == NULL) - debug(1, "Failed to allocate %d byte silence buffer.", fs); - else { - - conn->previous_random_number = generate_zero_frames( - silence, fs, config.output_format, conn->enable_dither, - conn->previous_random_number); - - // debug(1,"Frames to start: %llu, DAC delay %d, buffer: %d - // packets.",exact_frame_gap,dac_delay,seq_diff(conn->ab_read, - // conn->ab_write, conn->ab_read)); - config.output->play(silence, fs); - // debug(1,"Sent %" PRId64 " frames of silence",fs); - free(silence); - } - } - have_sent_prefiller_silence = - 1; // even if we haven't sent silence because it's zero frames long... - } - } else { - if ((resp == sps_extra_code_output_stalled) && - (conn->unfixable_error_reported == 0)) { - conn->unfixable_error_reported = 1; - if (config.cmd_unfixable) { - command_execute(config.cmd_unfixable, "output_device_stalled", 1); - } else { - warn( - "an unrecoverable error, \"output_device_stalled\", has been detected.", - conn->connection_number); - } - } - } - } else { - // no delay function on back end -- just send the prefiller silence - // debug(1,"Back end has no delay function."); - // send the appropriate prefiller here... - - void *silence; - if (lead_time != 0) { - int64_t frame_gap = (lead_time * config.output_rate) >> 32; - // debug(1,"%d frames needed.",frame_gap); - while (frame_gap > 0) { - ssize_t fs = config.output_rate / 10; - if (fs > frame_gap) - fs = frame_gap; - - silence = malloc(conn->output_bytes_per_frame * fs); - if (silence == NULL) - debug(1, "Failed to allocate %d frame silence buffer.", fs); - else { - // debug(1, "No delay function -- outputting %d frames of silence.", fs); - conn->previous_random_number = - generate_zero_frames(silence, fs, config.output_format, - conn->enable_dither, conn->previous_random_number); - config.output->play(silence, fs); - free(silence); - } - frame_gap -= fs; - } - } - have_sent_prefiller_silence = 1; - conn->ab_buffering = 0; - } - } - } - } - if (conn->ab_buffering == 0) { -/* - // note the time of the playing of the first frame - uint64_t reference_timestamp_time; // don't need this... - get_reference_timestamp_stuff(&conn->play_segment_reference_frame, - &reference_timestamp_time, - &conn->play_segment_reference_frame_remote_time, conn); - conn->play_segment_reference_frame *= conn->output_sample_ratio; -*/ + if (conn->first_packet_time_to_play != 0) { + // Now that we know the timing of the first packet... + if (config.output->delay) { + // and that the output device is capable of synchronization... + + // We may send packets of + // silence from now until the time the first audio packet should be sent + // and then we will send the first packet, which will be followed by + // the subsequent packets. + // here, we figure out whether and what silence to send. + + uint64_t should_be_time; + uint32_t effective_latency = conn->latency; + + switch (get_and_check_effective_latency(conn, &effective_latency, + config.audio_backend_latency_offset)) { + case -1: + if (conn->unachievable_audio_backend_latency_offset_notified == 0) { + warn("Negative latency! A latency of %d frames requested by the player, when " + "combined with an audio_backend_latency_offset of %f seconds, would make the " + "overall latency negative. The audio_backend_latency_offset setting is " + "ignored.", + conn->latency, config.audio_backend_latency_offset); + config.audio_backend_latency_offset = 0; // set it to zero + conn->unachievable_audio_backend_latency_offset_notified = 1; + }; + break; + case 1: + if (conn->unachievable_audio_backend_latency_offset_notified == 0) { + warn("An audio_backend_latency_offset of %f seconds may exceed the frame buffering " + "capacity -- the setting is ignored.", + config.audio_backend_latency_offset); + config.audio_backend_latency_offset = 0; // set it to zero; + conn->unachievable_audio_backend_latency_offset_notified = 1; + }; + break; + default: + break; + } + + // readjust first packet time to play + frame_to_local_time(conn->first_packet_timestamp + + effective_latency, // this will go modulo 2^32 + &should_be_time, conn); + + int64_t change_in_should_be_time = (int64_t)(should_be_time - conn->first_packet_time_to_play); + + if (fabs(0.000001*change_in_should_be_time) > 0.001) // the clock drift estimation might be nudging the estimate, and we can ignore this unless if's more than a microsecond + debug(2,"Change in estimated first_packet_time: %8.4f milliseconds.", 0.000001*change_in_should_be_time); + + conn->first_packet_time_to_play = should_be_time; + + if (local_time_now > conn->first_packet_time_to_play) { + uint64_t lateness = local_time_now - conn->first_packet_time_to_play; + debug(2, "Gone past starting time by %" PRIu64 " nanoseconds.", lateness); + conn->ab_buffering = 0; + } else { + // do some calculations + int64_t lead_time = conn->first_packet_time_to_play - local_time_now; + if ((config.audio_backend_silent_lead_in_time_auto == 1) || + (lead_time <= + (int64_t)(config.audio_backend_silent_lead_in_time * (int64_t)1000000000))) { + // debug(1, "Lead time: %" PRId64 " nanoseconds.", lead_time); + int resp = 0; + dac_delay = 0; + if (have_sent_prefiller_silence != 0) + resp = config.output->delay(&dac_delay); // we know the output device must have a delay function + if (resp == 0) { + int64_t gross_frame_gap = + ((conn->first_packet_time_to_play - local_time_now) * config.output_rate) / + 1000000000; + int64_t exact_frame_gap = gross_frame_gap - dac_delay; + int64_t frames_needed_to_maintain_desired_buffer = + (int64_t)(config.audio_backend_buffer_desired_length * config.output_rate) - + dac_delay; + // below, remember that exact_frame_gap and + // frames_needed_to_maintain_desired_buffer could both be negative + int64_t fs = frames_needed_to_maintain_desired_buffer; + + // if there isn't enough time to have the desired buffer size + if (exact_frame_gap <= frames_needed_to_maintain_desired_buffer) { + fs = conn->max_frames_per_packet * 2; + } + + // if we are very close to the end of buffering, i.e. within two frame-lengths, + // add the remaining silence needed and end buffering + if (exact_frame_gap <= conn->max_frames_per_packet * 2) { + fs = exact_frame_gap; + if (fs > first_frame_early_bias) + fs = fs - first_frame_early_bias; // deliberately make the first packet a tiny bit early so that the player may compensate for it at the last minute + conn->ab_buffering = 0; + } + void *silence; + if (fs > 0) { + silence = malloc(conn->output_bytes_per_frame * fs); + if (silence == NULL) + debug(1, "Failed to allocate %d byte silence buffer.", fs); + else { + // generate frames of silence with dither if necessary + conn->previous_random_number = + generate_zero_frames(silence, fs, config.output_format, + conn->enable_dither, conn->previous_random_number); + config.output->play(silence, fs); + // debug(1, "Sent %" PRId64 " frames of silence", fs); + free(silence); + have_sent_prefiller_silence = 1; + } + } + } else { + + if (resp == sps_extra_code_output_stalled) { + if (conn->unfixable_error_reported == 0) { + conn->unfixable_error_reported = 1; + if (config.cmd_unfixable) { + command_execute(config.cmd_unfixable, "output_device_stalled", 1); + } else { + warn("an unrecoverable error, \"output_device_stalled\", has been " + "detected.", + conn->connection_number); + } + } + } else { + debug(2, "Unexpected response to getting dac delay: %d.", resp); + } + } + } + } + } else { + // if the output device doesn't have a delay, we simply send the lead-in + int64_t lead_time = conn->first_packet_time_to_play - local_time_now; // negative if we are late + void *silence; + int64_t frame_gap = (lead_time * config.output_rate) / 1000000000; + // debug(1,"%d frames needed.",frame_gap); + while (frame_gap > 0) { + ssize_t fs = config.output_rate / 10; + if (fs > frame_gap) + fs = frame_gap; + + silence = malloc(conn->output_bytes_per_frame * fs); + if (silence == NULL) + debug(1, "Failed to allocate %d frame silence buffer.", fs); + else { + // debug(1, "No delay function -- outputting %d frames of silence.", fs); + conn->previous_random_number = + generate_zero_frames(silence, fs, config.output_format, + conn->enable_dither, conn->previous_random_number); + config.output->play(silence, fs); + free(silence); + } + frame_gap -= fs; + } + conn->ab_buffering = 0; + } + } #ifdef CONFIG_METADATA - debug(2, "prsm"); - send_ssnc_metadata('prsm', NULL, 0, - 0); // "resume", but don't wait if the queue is locked + if (conn->ab_buffering == 0) { + debug(2, "prsm"); + send_ssnc_metadata('prsm', NULL, 0, + 0); // "resume", but don't wait if the queue is locked + } #endif } } } - } // Here, we work out whether to release a packet or wait - // We release a buffer when the time is right. + // We release a packet when the time is right. // To work out when the time is right, we need to take account of (1) the actual time the packet // should be released, @@ -1306,12 +1294,50 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { if (have_timestamp_timing_information(conn)) { // if we have a reference time uint64_t time_to_play; - frame_to_local_time(curframe->given_timestamp + conn->latency + - (uint32_t)(config.audio_backend_latency_offset * conn->input_rate) - - (uint32_t)(config.audio_backend_buffer_desired_length * - conn->input_rate), // this will go modulo 2^32 - &time_to_play, - conn); + uint32_t effective_latency = conn->latency; + + switch (get_and_check_effective_latency(conn, &effective_latency, + config.audio_backend_latency_offset - + config.audio_backend_buffer_desired_length)) { + case -1: + // this means that the latency is negative, i.e. the packet must be played before its + // time. This is clearly a mistake, and can arise if the combination of a large negative + // latency offset and the desired buffer length are greater than the latency chosen by the + // player. For example if the latency is 88200 frames (2 seconds), the + // audio_backend_latency_offset is -1.9 and the audio_backend_buffer_desired_length is 0.2 + // the effective latency is -0.1, so the frame must be played 0.1 seconds before it is + // time-tagged for, and in reality before it arrives at the player. + + // to deal with the problem, rather than block the packets, we'll just let 'em go... + + // + if (conn->unachievable_audio_backend_latency_offset_notified == 0) { + warn("Negative latency! A latency of %d frames requested by the player, when combined " + "with an audio_backend_latency_offset of %f seconds an " + "audio_backend_buffer_desired_length of %f seconds, would make the overall " + "latency negative. No latency is used. Synchronisation may fail.", + conn->latency, config.audio_backend_latency_offset, + config.audio_backend_buffer_desired_length); + conn->unachievable_audio_backend_latency_offset_notified = 1; + }; + time_to_play = local_time_now; // pretend the frame should be played now... + break; + case 1: + if (conn->unachievable_audio_backend_latency_offset_notified == 0) { + warn("Latency too long! An audio_backend_latency_offset of %f seconds combined with an " + "audio_backend_buffer_desired_length of %f seconds may exceed the frame " + "buffering capacity.", + config.audio_backend_latency_offset, config.audio_backend_buffer_desired_length); + conn->unachievable_audio_backend_latency_offset_notified = 1; + }; + time_to_play = local_time_now; // pretend the frame should be played now... + break; + default: + frame_to_local_time(curframe->given_timestamp + + effective_latency, // this will go modulo 2^32 + &time_to_play, conn); + break; + } if (local_time_now >= time_to_play) { do_wait = 0; @@ -1323,22 +1349,24 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { if (notified_buffer_empty == 0) { debug(3, "Buffers exhausted."); notified_buffer_empty = 1; - reset_input_flow_metrics(conn); + // reset_input_flow_metrics(conn); // don't do a full flush parameters reset + conn->initial_reference_time = 0; + conn->initial_reference_timestamp = 0; } do_wait = 1; } wait = (conn->ab_buffering || (do_wait != 0) || (!conn->ab_synced)); if (wait) { - uint64_t time_to_wait_for_wakeup_fp = - ((uint64_t)1 << 32) / conn->input_rate; // this is time period of one frame - time_to_wait_for_wakeup_fp *= 2 * 352; // two full 352-frame packets - time_to_wait_for_wakeup_fp /= 3; // two thirds of a packet time + uint64_t time_to_wait_for_wakeup_ns = + 1000000000 / conn->input_rate; // this is time period of one frame + time_to_wait_for_wakeup_ns *= 2 * 352; // two full 352-frame packets + time_to_wait_for_wakeup_ns /= 3; // two thirds of a packet time #ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD - uint64_t time_of_wakeup_fp = local_time_now + time_to_wait_for_wakeup_fp; - uint64_t sec = time_of_wakeup_fp >> 32; - uint64_t nsec = ((time_of_wakeup_fp & 0xffffffff) * 1000000000) >> 32; + uint64_t time_of_wakeup_ns = local_time_now + time_to_wait_for_wakeup_ns; + uint64_t sec = time_of_wakeup_ns / 1000000000; + uint64_t nsec = time_of_wakeup_ns % 1000000000; struct timespec time_of_wakeup; time_of_wakeup.tv_sec = sec; @@ -1350,8 +1378,8 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { debug(3, "pthread_cond_timedwait returned error code %d.", rc); #endif #ifdef COMPILE_FOR_OSX - uint64_t sec = time_to_wait_for_wakeup_fp >> 32; - uint64_t nsec = ((time_to_wait_for_wakeup_fp & 0xffffffff) * 1000000000) >> 32; + uint64_t sec = time_to_wait_for_wakeup_ns / 1000000000; + uint64_t nsec = time_to_wait_for_wakeup_ns % 1000000000; struct timespec time_to_wait; time_to_wait.tv_sec = sec; time_to_wait.tv_nsec = nsec; @@ -1454,7 +1482,7 @@ static int stuff_buffer_basic_32(int32_t *inptr, int length, sps_format_t l_outp int32_t stat_n = 0; double stat_mean = 0.0; double stat_M2 = 0.0; -double longest_soxr_execution_time_us = 0.0; +double longest_soxr_execution_time = 0.0; int64_t packets_processed = 0; int stuff_buffer_soxr_32(int32_t *inptr, int32_t *scratchBuffer, int length, @@ -1482,7 +1510,7 @@ 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(); + uint64_t soxr_start_time = get_absolute_time_in_ns(); soxr_error_t error = soxr_oneshot(length, length + tstuff, 2, // Rates and # of chans. inptr, length, NULL, // Input. @@ -1499,15 +1527,14 @@ int stuff_buffer_soxr_32(int32_t *inptr, int32_t *scratchBuffer, int length, // 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; + double soxr_execution_time = (get_absolute_time_in_ns() - soxr_start_time) * 0.000000001; // 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; + if (soxr_execution_time > longest_soxr_execution_time) + longest_soxr_execution_time = soxr_execution_time; stat_n += 1; - double stat_delta = soxr_execution_time_us - stat_mean; + double stat_delta = soxr_execution_time - stat_mean; stat_mean += stat_delta / stat_n; - stat_M2 += stat_delta * (soxr_execution_time_us - stat_mean); + stat_M2 += stat_delta * (soxr_execution_time - stat_mean); int i; int32_t *ip, *op; @@ -1553,15 +1580,16 @@ int stuff_buffer_soxr_32(int32_t *inptr, int32_t *scratchBuffer, int length, } if (packets_processed % 1250 == 0) { - debug(3, "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.", + debug(3, + "soxr_oneshot execution time in nanoseconds: mean, standard deviation and max " + "for %" PRId32 " interpolations in the last " + "1250 packets. %10.6f, %10.6f, %10.6f.", stat_n, stat_mean, stat_n <= 1 ? 0.0 : sqrtf(stat_M2 / (stat_n - 1)), - longest_soxr_execution_time_us); + longest_soxr_execution_time); stat_n = 0; stat_mean = 0.0; stat_M2 = 0.0; - longest_soxr_execution_time_us = 0.0; + longest_soxr_execution_time = 0.0; } conn->amountStuffed = tstuff; @@ -1569,10 +1597,6 @@ int stuff_buffer_soxr_32(int32_t *inptr, int32_t *scratchBuffer, int length, } #endif -typedef struct stats { // statistics for running averages - int64_t sync_error, correction, drift; -} stats_t; - void player_thread_initial_cleanup_handler(__attribute__((unused)) void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; debug(3, "Connection %d: player thread main loop exit via player_thread_initial_cleanup_handler.", @@ -1639,6 +1663,11 @@ void player_thread_cleanup_handler(void *arg) { free(conn->tbuf); conn->tbuf = NULL; } + + if (conn->statistics) { + free(conn->statistics); + conn->statistics = NULL; + } free_audio_buffers(conn); if (conn->stream.type == ast_apple_lossless) terminate_decoders(conn); @@ -1660,6 +1689,9 @@ void *player_thread_func(void *arg) { conn->ab_synced = 0; conn->first_packet_timestamp = 0; conn->flush_requested = 0; + conn->flush_output_flushed = 0; // only send a flush command to the output device once + conn->flush_rtp_timestamp = 0; // it seems this number has a special significance -- it seems to + // be used as a null operand, so we'll use it like that too conn->fix_volume = 0x10000; if (conn->latency == 0) { @@ -1727,9 +1759,9 @@ void *player_thread_func(void *arg) { debug(3, "Output frame bytes is %d.", conn->output_bytes_per_frame); - conn->dac_buffer_queue_minimum_length = (int64_t)( + conn->dac_buffer_queue_minimum_length = (uint64_t)( config.audio_backend_buffer_interpolation_threshold_in_seconds * config.output_rate); - debug(3, "dac_buffer_queue_minimum_length is %" PRId64 " frames.", + debug(3, "dac_buffer_queue_minimum_length is %" PRIu64 " frames.", conn->dac_buffer_queue_minimum_length); conn->session_corrections = 0; @@ -1746,15 +1778,17 @@ void *player_thread_func(void *arg) { conn->connection_state_to_output = get_requested_connection_state_to_output(); // this is about half a minute //#define trend_interval 3758 + +// this is about 8 seconds #define trend_interval 1003 - stats_t statistics[trend_interval]; int number_of_statistics, oldest_statistic, newest_statistic; int at_least_one_frame_seen = 0; + int at_least_one_frame_seen_this_session = 0; int64_t tsum_of_sync_errors, tsum_of_corrections, tsum_of_insertions_and_deletions, tsum_of_drifts; int64_t previous_sync_error = 0, previous_correction = 0; - int64_t minimum_dac_queue_size = INT64_MAX; + uint64_t minimum_dac_queue_size = UINT64_MAX; int32_t minimum_buffer_occupancy = INT32_MAX; int32_t maximum_buffer_occupancy = INT32_MIN; @@ -1769,7 +1803,7 @@ void *player_thread_func(void *arg) { conn->buffer_occupancy = 0; int play_samples = 0; - int64_t current_delay; + uint64_t current_delay; int play_number = 0; conn->play_number_after_flush = 0; // int last_timestamp = 0; // for debugging only @@ -1842,20 +1876,17 @@ void *player_thread_func(void *arg) { // we need an intermediate "transition" buffer - // if ((input_rate!=config.output_rate) || (input_bit_depth!=output_bit_depth)) { - // debug(1,"Define tbuf of length - // %d.",output_bytes_per_frame*(max_frames_per_packet*output_sample_ratio+max_frame_size_change)); - conn->tbuf = - malloc(sizeof(int32_t) * 2 * (conn->max_frames_per_packet * conn->output_sample_ratio + - conn->max_frame_size_change)); + conn->tbuf = malloc( + sizeof(int32_t) * 2 * + (conn->max_frames_per_packet * conn->output_sample_ratio + conn->max_frame_size_change)); if (conn->tbuf == NULL) die("Failed to allocate memory for the transition buffer."); // initialise this, because soxr stuffing might be chosen later - conn->sbuf = - malloc(sizeof(int32_t) * 2 * (conn->max_frames_per_packet * conn->output_sample_ratio + - conn->max_frame_size_change)); + conn->sbuf = malloc( + sizeof(int32_t) * 2 * + (conn->max_frames_per_packet * conn->output_sample_ratio + conn->max_frame_size_change)); if (conn->sbuf == NULL) die("Failed to allocate memory for the sbuf buffer."); @@ -1868,11 +1899,13 @@ void *player_thread_func(void *arg) { die("Failed to allocate memory for an output buffer."); conn->first_packet_timestamp = 0; conn->missing_packets = conn->late_packets = conn->too_late_packets = conn->resend_requests = 0; - conn->flush_rtp_timestamp = 0; // it seems this number has a special significance -- it seems to - // be used as a null operand, so we'll use it like that too int sync_error_out_of_bounds = 0; // number of times in a row that there's been a serious sync error + conn->statistics = malloc(sizeof(stats_t)*trend_interval); + if (conn->statistics == NULL) + die("Failed to allocate a statistics buffer"); + conn->framesProcessedInThisEpoch = 0; conn->framesGeneratedInThisEpoch = 0; conn->correctionsRequestedInThisEpoch = 0; @@ -1913,8 +1946,7 @@ void *player_thread_func(void *arg) { "source clock drift sample count"); } } else { - inform("sync error in milliseconds, " - "total packets, " + inform("total packets, " "missing packets, " "late packets, " "too late packets, " @@ -1965,10 +1997,12 @@ void *player_thread_func(void *arg) { // debug(3, "Play frame %d.", play_number); conn->play_number_after_flush++; if (inframe->given_timestamp == 0) { - debug(1, "Player has supplied a silent frame, (possibly frame %u) for play number %d, status 0x%X after %u resend requests.", - SUCCESSOR(conn->last_seqno_read), play_number, inframe->status, inframe->resend_request_number); - conn->last_seqno_read = (SUCCESSOR(conn->last_seqno_read) & - 0xffff); // manage the packet out of sequence minder + debug(1, + "Player has supplied a silent frame, (possibly frame %u) for play number %d, " + "status 0x%X after %u resend requests.", + SUCCESSOR(conn->last_seqno_read), play_number, inframe->status, + inframe->resend_request_number); + conn->last_seqno_read = SUCCESSOR(conn->last_seqno_read); // manage the packet out of sequence minder void *silence = malloc(conn->output_bytes_per_frame * conn->max_frames_per_packet * conn->output_sample_ratio); @@ -2078,7 +2112,8 @@ void *player_thread_func(void *arg) { die("Shairport Sync only supports 16 bit input"); } - inbuflength *= conn->output_sample_ratio; + + at_least_one_frame_seen = 1; // We have a frame of data. We need to see if we want to add or remove a frame from it to // keep in sync. @@ -2087,47 +2122,27 @@ void *player_thread_func(void *arg) { // If it's late, we remove an audio frame from this frame to bring a subsequent frame // forward in time - at_least_one_frame_seen = 1; - // now, go back as far as the total latency less, say, 100 ms, and check the presence of // frames from then onwards + inbuflength *= conn->output_sample_ratio; + /* uint32_t reference_timestamp; uint64_t reference_timestamp_time, remote_reference_timestamp_time; get_reference_timestamp_stuff(&reference_timestamp, &reference_timestamp_time, &remote_reference_timestamp_time, conn); // types okay - int64_t rt, nt; - rt = reference_timestamp; // uint32_t to int64_t + */ + + // nt is the rtp timestamp of the first frame of the current packet of + // frames from the source. + // multiply it by the output frame ratio to get, effectively, the rtp timestamp + // of the first frame of the corresponding packet of output frames. + + uint64_t nt; nt = inframe->given_timestamp; // uint32_t to int64_t - rt = rt * conn->output_sample_ratio; nt = nt * conn->output_sample_ratio; - uint64_t local_time_now = get_absolute_time_in_fp(); // types okay - // struct timespec tn; - // clock_gettime(CLOCK_MONOTONIC,&tn); - // uint64_t - // local_time_now=((uint64_t)tn.tv_sec<<32)+((uint64_t)tn.tv_nsec<<32)/1000000000; - - int64_t td = 0; // td is the time difference between the reference timestamp time and the - // present time. Only used to calculate td_in_frames - int64_t td_in_frames = 0; // td_in_frames is the number of frames between between the - // reference timestamp time and the present time - - if (local_time_now >= reference_timestamp_time) { - td = local_time_now - reference_timestamp_time; // this is the positive value. - // Conversion is positive uint64_t to - // int64_t, thus okay - td_in_frames = (td * config.output_rate) >> 32; - } else { - td = reference_timestamp_time - local_time_now; // this is the absolute value, which - // should be negated. Conversion is - // positive uint64_t to int64_t, thus - // okay. - td_in_frames = (td * config.output_rate) >> - 32; // use the absolute td value for the present. Types okay - td_in_frames = -td_in_frames; - td = -td; // should be okay, as the range of values should be very small w.r.t 64 bits - } + uint64_t local_time_now = get_absolute_time_in_ns(); // types okay // This is the timing error for the next audio frame in the DAC, if applicable int64_t sync_error = 0; @@ -2143,15 +2158,16 @@ void *player_thread_func(void *arg) { SUCCESSOR(conn->last_seqno_read); // int32_t from seq_t, i.e. uint16_t, so okay. if (inframe->sequence_number != conn->last_seqno_read) { // seq_t, ei.e. uint16_t and int32_t, so okay - debug(2, "Player: packets out of sequence: expected: %u, got: %u, with ab_read: %u " - "and ab_write: %u.", + debug(2, + "Player: packets out of sequence: expected: %u, got: %u, with ab_read: %u " + "and ab_write: %u.", conn->last_seqno_read, inframe->sequence_number, conn->ab_read, conn->ab_write); conn->last_seqno_read = inframe->sequence_number; // reset warning... } } conn->buffer_occupancy = - seq_diff(conn->ab_read, conn->ab_write, conn->ab_read); // int32_t from int32 + seq_diff(conn->ab_write, conn->ab_read); // int32_t from int16_t if (conn->buffer_occupancy < minimum_buffer_occupancy) minimum_buffer_occupancy = conn->buffer_occupancy; @@ -2159,21 +2175,25 @@ void *player_thread_func(void *arg) { if (conn->buffer_occupancy > maximum_buffer_occupancy) maximum_buffer_occupancy = conn->buffer_occupancy; + + + // here, we want to check (a) if we are meant to do synchronisation, // (b) if we have a delay procedure, (c) if we can get the delay. // If any of these are false, we don't do any synchronisation stuff int resp = -1; // use this as a flag -- if negative, we can't rely on a real known delay - current_delay = -1; // use this as a failure flag if (config.output->delay) { long l_delay; resp = config.output->delay(&l_delay); if (resp == 0) { // no error current_delay = l_delay; - if (current_delay < 0) { - debug(2, "Underrun of %lld frames reported, but ignored.", current_delay); + if (l_delay >= 0) + current_delay = l_delay; + else { + debug(2, "Underrun of %ld frames reported, but ignored.", l_delay); current_delay = 0; // could get a negative value if there was underrun, but ignore it. } @@ -2207,37 +2227,62 @@ void *player_thread_func(void *arg) { uint32_t should_be_frame_32; local_time_to_frame(local_time_now, &should_be_frame_32, conn); - int64_t should_be_frame = ((int64_t)should_be_frame_32) * conn->output_sample_ratio; - - // int64_t absolute_difference_in_frames = td_in_frames + rt - should_be_frame; - // if (absolute_difference_in_frames < 0) - // absolute_difference_in_frames = -absolute_difference_in_frames; + // int64_t should_be_frame = ((int64_t)should_be_frame_32) * conn->output_sample_ratio; - // if (absolute_difference_in_frames > 10 * conn->output_sample_ratio) - // debug(1, "Difference between old and new frame number is %" PRId64 " frames.", - // td_in_frames + rt - should_be_frame); - // this is the actual delay, including the latency we actually want, which will - // fluctuate a good bit about a potentially rising or falling trend. + int64_t delay = int64_mod_difference(should_be_frame_32 * conn->output_sample_ratio, nt - current_delay, UINT32_MAX * conn->output_sample_ratio); - // int64_t delay = td_in_frames + rt - (nt - current_delay); // all int64_t - // cut over to the new calculation method - int64_t delay = should_be_frame - (nt - current_delay); // all int64_t + //int64_t delay = should_be_frame - (nt - current_delay); // all int64_t - // td_in_frames + rt is the frame number that should be output at local_time_now. + // the original frame numbers are unsigned 32-bit integers that roll over modulo 2^32 + // hence these delay figures will be unsigned numbers that roll over modulo 2^32 * conn->output_sample_ratio; + // therefore, calculating the delay must be done in the light of possible rollover - // This is the timing error for the next audio frame in the DAC. - // if positive, it means that the packet will be late -- the delay is longer than - // requested - // if negative, the packet will be early -- the delay is less than expected. sync_error = delay - ((int64_t)conn->latency * conn->output_sample_ratio + (int64_t)(config.audio_backend_latency_offset * config.output_rate)); // int64_t from int64_t - int32_t, so okay - // debug(1,"%" PRId64 "",sync_error,inbuflength); + if (at_least_one_frame_seen_this_session == 0) { + at_least_one_frame_seen_this_session = 1; + + // debug(2,"first frame real sync error (positive --> late): %" PRId64 " frames.", sync_error); + + // this is a sneaky attempt to make a final adjustment to the timing of the first packet + + // the very first packet generally has a first_frame_early_bias subtracted from its timing + // to make it more likely that it will be early than late, + // making it possible to compensate for it be adding a few frames of silence. + + // debug(2,"first frame real sync error (positive --> late): %" PRId64 " frames.", sync_error); + // remove the bias when reporting the error to make it the true error + + debug(2,"first frame sync error (positive --> late): %" PRId64 " frames, %.3f mS at %d frames per second output.", sync_error+first_frame_early_bias, (1000.0*(sync_error+first_frame_early_bias))/config.output_rate, config.output_rate); + + // if the packet is early, add the frames needed to put it in sync. + if (sync_error < 0) { + size_t final_adjustment_length_sized = -sync_error; + char *final_adjustment_silence = malloc(conn->output_bytes_per_frame * final_adjustment_length_sized); + if (final_adjustment_silence) { + + conn->previous_random_number = + generate_zero_frames(final_adjustment_silence, final_adjustment_length_sized, config.output_format, + conn->enable_dither, conn->previous_random_number); + int final_adjustment = -sync_error; + final_adjustment = final_adjustment - first_frame_early_bias; + debug(2, "final sync adjustment: %" PRId64 " silent frames added with a bias of %" PRId64 " frames.", -sync_error, first_frame_early_bias); + config.output->play(final_adjustment_silence, final_adjustment_length_sized); + free(final_adjustment_silence); + } else { + warn("Failed to allocate memory for a final_adjustment_silence buffer of %d frames for a " + "sync error of %d frames.", + final_adjustment_length_sized, sync_error); + } + sync_error = 0; // say the error was fixed! + } + } // not too sure if abs() is implemented for int64_t, so we'll do it manually int64_t abs_sync_error = sync_error; if (abs_sync_error < 0) @@ -2346,8 +2391,9 @@ void *player_thread_func(void *arg) { if ((local_time_now) && (conn->first_packet_time_to_play) && (local_time_now >= conn->first_packet_time_to_play)) { - int64_t tp = (local_time_now - conn->first_packet_time_to_play) >> - 32; // seconds int64_t from uint64_t which is always positive, so ok + int64_t tp = + (local_time_now - conn->first_packet_time_to_play) / + 1000000000; // seconds int64_t from uint64_t which is always positive, so ok if (tp < 5) amount_to_stuff = 0; // wait at least five seconds @@ -2365,29 +2411,30 @@ void *player_thread_func(void *arg) { amount_to_stuff = 0; // no stuffing if it's been disabled // Apply DSP here - - // check the state of loudness and convolution flags here and don't change them for the frame - + + // check the state of loudness and convolution flags here and don't change them for + // the frame + int do_loudness = config.loudness; - #ifdef CONFIG_CONVOLUTION int do_convolution = 0; if ((config.convolution) && (config.convolver_valid)) do_convolution = 1; - // we will apply the convolution gain if convolution is enabled, even if there is no valid convolution happening - + // we will apply the convolution gain if convolution is enabled, even if there is no + // valid convolution happening + int convolution_is_enabled = 0; if (config.convolution) convolution_is_enabled = 1; #endif - + if (do_loudness #ifdef CONFIG_CONVOLUTION || convolution_is_enabled #endif - ) { + ) { int32_t *tbuf32 = (int32_t *)conn->tbuf; float fbuf_l[inbuflength]; float fbuf_r[inbuflength]; @@ -2442,7 +2489,7 @@ void *player_thread_func(void *arg) { ((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, @@ -2509,8 +2556,36 @@ void *player_thread_func(void *arg) { */ } } else { - // if there is no delay procedure, or it's not working or not allowed, there can be no - // synchronising + + // if there is no delay procedure, then we should be sending the packet + // to the output at the time determined by + // the packet's time to play + requested latency + requested offset. +/* + // This is just for checking during development + + uint32_t should_be_frame_32; + local_time_to_frame(local_time_now, &should_be_frame_32, conn); + // int64_t should_be_frame = ((int64_t)should_be_frame_32) * conn->output_sample_ratio; + + int32_t ilatency = (int32_t)((config.audio_backend_latency_offset - config.audio_backend_buffer_desired_length) * conn->input_rate) + conn->latency; + if (ilatency < 0) + debug(1,"incorrect latency %d.", ilatency); + + int32_t idelay = (int32_t)(should_be_frame_32 - inframe->given_timestamp); + + idelay = idelay - ilatency; + + debug(2,"delay is %d input frames.", idelay); +*/ + + // if this is the first frame, see if it's close to when it's supposed to be + // release, which will be its time plus latency and any offset_time + if (at_least_one_frame_seen_this_session == 0) { + at_least_one_frame_seen_this_session = 1; + + + } + play_samples = stuff_buffer_basic_32((int32_t *)conn->tbuf, inbuflength, config.output_format, conn->outbuf, 0, conn->enable_dither, conn); @@ -2533,7 +2608,7 @@ void *player_thread_func(void *arg) { // update the watchdog if ((config.dont_check_timeout == 0) && (config.timeout != 0)) { - uint64_t time_now = get_absolute_time_in_fp(); + uint64_t time_now = get_absolute_time_in_ns(); debug_mutex_lock(&conn->watchdog_mutex, 1000, 0); conn->watchdog_bark_time = time_now; debug_mutex_unlock(&conn->watchdog_mutex, 0); @@ -2548,34 +2623,34 @@ void *player_thread_func(void *arg) { // valid samples and the number of times sync wasn't checked due to non availability of a // delay figure. // for the present, stats are only updated when sync has been checked - if (sync_error != -1) { + if (config.output->delay != NULL) { if (number_of_statistics == trend_interval) { // here we remove the oldest statistical data and take it from the summaries as well - tsum_of_sync_errors -= statistics[oldest_statistic].sync_error; - tsum_of_drifts -= statistics[oldest_statistic].drift; - if (statistics[oldest_statistic].correction > 0) - tsum_of_insertions_and_deletions -= statistics[oldest_statistic].correction; + tsum_of_sync_errors -= conn->statistics[oldest_statistic].sync_error; + tsum_of_drifts -= conn->statistics[oldest_statistic].drift; + if (conn->statistics[oldest_statistic].correction > 0) + tsum_of_insertions_and_deletions -= conn->statistics[oldest_statistic].correction; else - tsum_of_insertions_and_deletions += statistics[oldest_statistic].correction; - tsum_of_corrections -= statistics[oldest_statistic].correction; + tsum_of_insertions_and_deletions += conn->statistics[oldest_statistic].correction; + tsum_of_corrections -= conn->statistics[oldest_statistic].correction; oldest_statistic = (oldest_statistic + 1) % trend_interval; number_of_statistics--; } - statistics[newest_statistic].sync_error = sync_error; - statistics[newest_statistic].correction = conn->amountStuffed; + conn->statistics[newest_statistic].sync_error = sync_error; + conn->statistics[newest_statistic].correction = conn->amountStuffed; if (number_of_statistics == 0) - statistics[newest_statistic].drift = 0; + conn->statistics[newest_statistic].drift = 0; else - statistics[newest_statistic].drift = + conn->statistics[newest_statistic].drift = sync_error - previous_sync_error - previous_correction; previous_sync_error = sync_error; previous_correction = conn->amountStuffed; tsum_of_sync_errors += sync_error; - tsum_of_drifts += statistics[newest_statistic].drift; + tsum_of_drifts += conn->statistics[newest_statistic].drift; if (conn->amountStuffed > 0) { tsum_of_insertions_and_deletions += conn->amountStuffed; } else { @@ -2601,13 +2676,8 @@ void *player_thread_func(void *arg) { frames_received = conn->frames_inward_frames_received_at_measurement_time - conn->frames_inward_frames_received_at_measurement_start_time; conn->input_frame_rate = - (1.0 * frames_received) / + (1.0E9 * frames_received) / elapsed_reception_time; // an IEEE double calculation with two 64-bit integers - conn->input_frame_rate = - conn->input_frame_rate * (uint64_t)0x100000000; // this should just change the - // [binary] exponent in the IEEE FP - // representation; the mantissa - // should be unaffected. } else { conn->input_frame_rate = 0.0; } @@ -2620,12 +2690,8 @@ void *player_thread_func(void *arg) { conn->frame_rate_status = 0; if (conn->frame_rate_status) { conn->frame_rate = - (1.0 * frames_played) / + (1.0E9 * frames_played) / elapsed_play_time; // an IEEE double calculation with two 64-bit integers - conn->frame_rate = - conn->frame_rate * (uint64_t)0x100000000; // this should just change the [binary] - // exponent in the IEEE FP representation; - // the mantissa should be unaffected. } else { conn->frame_rate = 0.0; } @@ -2644,42 +2710,41 @@ void *player_thread_func(void *arg) { if ((config.output->delay)) { if (config.no_sync == 0) { - inform("%*.2f," /* Sync error in milliseconds */ - "%*.1f," /* net correction in ppm */ - "%*.1f," /* corrections in ppm */ - "%*d," /* total packets */ - "%*" PRIu64 "," /* missing packets */ - "%*" PRIu64 "," /* late packets */ - "%*" PRIu64 "," /* too late packets */ - "%*" PRIu64 "," /* resend requests */ - "%*" PRId64 "," /* min DAC queue size */ - "%*" PRId32 "," /* min buffer occupancy */ - "%*" PRId32 "," /* max buffer occupancy */ - "%*.2f," /* source nominal frame rate */ - "%*.2f," /* source actual (average) frame rate */ - "%*.2f," /* output frame rate */ - "%*.2f," /* source clock drift */ - "%*d," /* source clock drift sample count */ - "%*.2f", /* rough calculated correction in ppm */ - 10, - 1000 * moving_average_sync_error / config.output_rate, 10, - moving_average_correction * 1000000 / (352 * conn->output_sample_ratio), - 10, moving_average_insertions_plus_deletions * 1000000 / - (352 * conn->output_sample_ratio), - 12, play_number, 7, conn->missing_packets, 7, conn->late_packets, 7, - conn->too_late_packets, 7, conn->resend_requests, 7, - minimum_dac_queue_size, 5, minimum_buffer_occupancy, 5, - maximum_buffer_occupancy, 11, conn->remote_frame_rate, 11, - conn->input_frame_rate, 11, conn->frame_rate, 10, - (conn->local_to_remote_time_gradient - 1.0) * 1000000, 6, - conn->local_to_remote_time_gradient_sample_count, 10, - (conn->frame_rate > 0.0) - ? ((conn->frame_rate - - conn->remote_frame_rate * conn->output_sample_ratio * - conn->local_to_remote_time_gradient) * - 1000000) / - conn->frame_rate - : 0.0); + inform( + "%*.2f," /* Sync error in milliseconds */ + "%*.1f," /* net correction in ppm */ + "%*.1f," /* corrections in ppm */ + "%*d," /* total packets */ + "%*" PRIu64 "," /* missing packets */ + "%*" PRIu64 "," /* late packets */ + "%*" PRIu64 "," /* too late packets */ + "%*" PRIu64 "," /* resend requests */ + "%*" PRIu64 "," /* min DAC queue size */ + "%*" PRId32 "," /* min buffer occupancy */ + "%*" PRId32 "," /* max buffer occupancy */ + "%*.2f," /* source nominal frame rate */ + "%*.2f," /* source actual (average) frame rate */ + "%*.2f," /* output frame rate */ + "%*.2f," /* source clock drift */ + "%*d," /* source clock drift sample count */ + "%*.2f", /* rough calculated correction in ppm */ + 10, 1000 * moving_average_sync_error / config.output_rate, 10, + moving_average_correction * 1000000 / (352 * conn->output_sample_ratio), 10, + moving_average_insertions_plus_deletions * 1000000 / + (352 * conn->output_sample_ratio), + 12, play_number, 7, conn->missing_packets, 7, conn->late_packets, 7, + conn->too_late_packets, 7, conn->resend_requests, 7, minimum_dac_queue_size, + 5, minimum_buffer_occupancy, 5, maximum_buffer_occupancy, 11, + conn->remote_frame_rate, 11, conn->input_frame_rate, 11, conn->frame_rate, 10, + (conn->local_to_remote_time_gradient - 1.0) * 1000000, 6, + conn->local_to_remote_time_gradient_sample_count, 10, + (conn->frame_rate > 0.0) + ? ((conn->frame_rate - conn->remote_frame_rate * + conn->output_sample_ratio * + conn->local_to_remote_time_gradient) * + 1000000) / + conn->frame_rate + : 0.0); } else { inform("%*.2f," /* Sync error in milliseconds */ "%*d," /* total packets */ @@ -2687,25 +2752,23 @@ void *player_thread_func(void *arg) { "%*" PRIu64 "," /* late packets */ "%*" PRIu64 "," /* too late packets */ "%*" PRIu64 "," /* resend requests */ - "%*" PRId64 "," /* min DAC queue size */ + "%*" PRIu64 "," /* min DAC queue size */ "%*" PRId32 "," /* min buffer occupancy */ "%*" PRId32 "," /* max buffer occupancy */ "%*.2f," /* source nominal frame rate */ "%*.2f," /* source actual (average) frame rate */ "%*.2f," /* source clock drift */ "%*d", /* source clock drift sample count */ - 10, - 1000 * moving_average_sync_error / config.output_rate, 12, play_number, 7, - conn->missing_packets, 7, conn->late_packets, 7, conn->too_late_packets, 7, - conn->resend_requests, 7, minimum_dac_queue_size, 5, + 10, 1000 * moving_average_sync_error / config.output_rate, 12, play_number, + 7, conn->missing_packets, 7, conn->late_packets, 7, conn->too_late_packets, + 7, conn->resend_requests, 7, minimum_dac_queue_size, 5, minimum_buffer_occupancy, 5, maximum_buffer_occupancy, 11, conn->remote_frame_rate, 11, conn->input_frame_rate, 10, (conn->local_to_remote_time_gradient - 1.0) * 1000000, 6, conn->local_to_remote_time_gradient_sample_count); } } else { - inform("%*.2f," /* Sync error in milliseconds */ - "%*d," /* total packets */ + inform("%*d," /* total packets */ "%*" PRIu64 "," /* missing packets */ "%*" PRIu64 "," /* late packets */ "%*" PRIu64 "," /* too late packets */ @@ -2716,10 +2779,9 @@ void *player_thread_func(void *arg) { "%*.2f," /* source actual (average) frame rate */ "%*.2f," /* source clock drift */ "%*d", /* source clock drift sample count */ - 10, - 1000 * moving_average_sync_error / config.output_rate, 12, play_number, 7, - conn->missing_packets, 7, conn->late_packets, 7, conn->too_late_packets, 7, - conn->resend_requests, 5, minimum_buffer_occupancy, 5, + 12, play_number, + 7, conn->missing_packets, 7, conn->late_packets, 7, conn->too_late_packets, + 7, conn->resend_requests, 5, minimum_buffer_occupancy, 5, maximum_buffer_occupancy, 11, conn->remote_frame_rate, 11, conn->input_frame_rate, 10, (conn->local_to_remote_time_gradient - 1.0) * 1000000, 6, @@ -2729,7 +2791,7 @@ void *player_thread_func(void *arg) { inform("No frames received in the last sampling interval."); } } - minimum_dac_queue_size = INT64_MAX; // hack reset + minimum_dac_queue_size = UINT64_MAX; // hack reset maximum_buffer_occupancy = INT32_MIN; // can't be less than this minimum_buffer_occupancy = INT32_MAX; // can't be more than this at_least_one_frame_seen = 0; @@ -2758,7 +2820,7 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c int32_t hw_max_db = 0, hw_min_db = 0; // zeroed to quieten an incorrect uninitialised warning int32_t sw_max_db = 0, sw_min_db = -9630; - + if (config.output->parameters) { volume_mode = vol_hw_only; audio_parameters audio_information; @@ -2819,13 +2881,15 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c if (airplay_volume == -144.0) { if ((config.output->mute) && (config.output->mute(1) == 0)) - debug(2, "player_volume_without_notification: volume mode is %d, airplay_volume is %f, " - "hardware mute is enabled.", + debug(2, + "player_volume_without_notification: volume mode is %d, airplay_volume is %f, " + "hardware mute is enabled.", volume_mode, airplay_volume); else { conn->software_mute_enabled = 1; - debug(2, "player_volume_without_notification: volume mode is %d, airplay_volume is %f, " - "software mute is enabled.", + debug(2, + "player_volume_without_notification: volume mode is %d, airplay_volume is %f, " + "software mute is enabled.", volume_mode, airplay_volume); } @@ -2928,51 +2992,47 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c if (config.logOutputLevel) { inform("Output Level set to: %.2f dB.", scaled_attenuation / 100.0); } - + #ifdef CONFIG_METADATA - // here, send the 'pvol' metadata message when the airplay volume information - // is being used by shairport sync to control the output volume - char *dv = malloc(128); // will be freed in the metadata thread - if (dv) { - memset(dv, 0, 128); - if (volume_mode == vol_both) { - // normalise the maximum output to the hardware device's max output - snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, (scaled_attenuation - max_db + hw_max_db) / 100.0, - (min_db - max_db + hw_max_db) / 100.0, (max_db - max_db + hw_max_db) / 100.0); - } else { - snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, scaled_attenuation / 100.0, - min_db / 100.0, max_db / 100.0); - } - send_ssnc_metadata('pvol', dv, strlen(dv), 1); - } + // here, send the 'pvol' metadata message when the airplay volume information + // is being used by shairport sync to control the output volume + char dv[128]; + memset(dv, 0, 128); + if (volume_mode == vol_both) { + // normalise the maximum output to the hardware device's max output + snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, + (scaled_attenuation - max_db + hw_max_db) / 100.0, + (min_db - max_db + hw_max_db) / 100.0, (max_db - max_db + hw_max_db) / 100.0); + } else { + snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, scaled_attenuation / 100.0, + min_db / 100.0, max_db / 100.0); + } + send_ssnc_metadata('pvol', dv, strlen(dv), 1); #endif - if (config.output->mute) config.output->mute(0); conn->software_mute_enabled = 0; - debug(2, "player_volume_without_notification: volume mode is %d, airplay volume is %f, " - "software_attenuation: %f, hardware_attenuation: %f, muting " - "is disabled.", + debug(2, + "player_volume_without_notification: volume mode is %d, airplay volume is %f, " + "software_attenuation: %f, hardware_attenuation: %f, muting " + "is disabled.", volume_mode, airplay_volume, software_attenuation, hardware_attenuation); } } #ifdef CONFIG_METADATA - else { - // here, send the 'pvol' metadata message when the airplay volume information - // is being used by shairport sync to control the output volume - char *dv = malloc(128); // will be freed in the metadata thread - if (dv) { - memset(dv, 0, 128); - snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, 0.0, 0.0, 0.0); - send_ssnc_metadata('pvol', dv, strlen(dv), 1); - } + else { + // here, send the 'pvol' metadata message when the airplay volume information + // is being used by shairport sync to control the output volume + char dv[128]; + memset(dv, 0, 128); + snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, 0.0, 0.0, 0.0); + send_ssnc_metadata('pvol', dv, strlen(dv), 1); } #endif - // here, store the volume for possible use in the future config.airplay_volume = airplay_volume; debug_mutex_unlock(&conn->volume_control_mutex, 3); @@ -2985,27 +3045,22 @@ void player_volume(double airplay_volume, rtsp_conn_info *conn) { void do_flush(uint32_t timestamp, rtsp_conn_info *conn) { - debug(3, "Flush requested up to %u. It seems as if 0 is special.", timestamp); + debug(2, "do_flush: flush to %u.", timestamp); debug_mutex_lock(&conn->flush_mutex, 1000, 1); conn->flush_requested = 1; - // if (timestamp!=0) - conn->flush_rtp_timestamp = timestamp; // flush all packets up to (and including?) this - // conn->play_segment_reference_frame = 0; + conn->flush_rtp_timestamp = timestamp; // flush all packets up to, but not including, this one. reset_input_flow_metrics(conn); debug_mutex_unlock(&conn->flush_mutex, 3); - debug(3, "Flush request made."); } void player_flush(uint32_t timestamp, rtsp_conn_info *conn) { debug(3, "player_flush"); do_flush(timestamp, conn); #ifdef CONFIG_METADATA - // only send a flush metadata message if the first packet has been seen -- it's a bogus message - // otherwise - if (conn->first_packet_timestamp) { - debug(2, "pfls"); - send_ssnc_metadata('pfls', NULL, 0, 1); // contains cancellation points - } + debug(2, "pfls"); + char numbuf[32]; + snprintf(numbuf, sizeof(numbuf),"%u",timestamp); + send_ssnc_metadata('pfls', numbuf, strlen(numbuf), 1); // contains cancellation points #endif } @@ -3027,17 +3082,10 @@ int player_play(rtsp_conn_info *conn) { if (pt == NULL) die("Couldn't allocate space for pthread_t"); conn->player_thread = pt; - size_t size = (PTHREAD_STACK_MIN + 256 * 1024); - pthread_attr_t tattr; - pthread_attr_init(&tattr); - int rc = pthread_attr_setstacksize(&tattr, size); - if (rc) - debug(1, "Error setting stack size for player_thread: %s", strerror(errno)); - // finished initialising. - rc = pthread_create(pt, &tattr, player_thread_func, (void *)conn); + int rc = pthread_create(pt, NULL, player_thread_func, (void *)conn); if (rc) debug(1, "Error creating player_thread: %s", strerror(errno)); - pthread_attr_destroy(&tattr); + #ifdef CONFIG_METADATA debug(2, "pbeg"); send_ssnc_metadata('pbeg', NULL, 0, 1); // contains cancellation points diff --git a/player.h b/player.h index b6cae8a96..f6b86cd43 100644 --- a/player.h +++ b/player.h @@ -24,10 +24,10 @@ #include "alac.h" #include "audio.h" -#define time_ping_history 128 // at 1 per three seconds, approximately six minutes of records +#define time_ping_history_power_of_two 7 +#define time_ping_history (1 << time_ping_history_power_of_two) // 2^7 is 128. At 1 per three seconds, approximately six minutes of records typedef struct time_ping_record { - uint64_t local_to_remote_difference; uint64_t dispersion; uint64_t local_time; uint64_t remote_time; @@ -43,12 +43,17 @@ typedef struct audio_buffer_entry { // decoded audio packets uint16_t resend_request_number; signed short *data; seq_t sequence_number; - uint64_t initialisation_time; // the time the packet was added or the time it was noticed the packet was missing - uint64_t resend_time; // time of last resend request or zero - uint32_t given_timestamp; // for debugging and checking - int length; // the length of the decoded data + uint64_t initialisation_time; // the time the packet was added or the time it was noticed the + // packet was missing + uint64_t resend_time; // time of last resend request or zero + uint32_t given_timestamp; // for debugging and checking + int length; // the length of the decoded data } abuf_t; +typedef struct stats { // statistics for running averages + int64_t sync_error, correction, drift; +} stats_t; + // default buffer size // This needs to be a power of 2 because of the way BUFIDX(seqno) works. // 512 is the minimum for normal operation -- it gives 512*352/44100 or just over 4 seconds of @@ -87,6 +92,7 @@ typedef struct { uint32_t maximum_latency; // set if an a=max-latency: line appears in the ANNOUNCE message; zero // otherwise int software_mute_enabled; // if we don't have a real mute that we can use + int unachievable_audio_backend_latency_offset_notified; // set when a latency warning is issued int fd; int authorized; // set if a password is required and has been supplied @@ -108,6 +114,10 @@ typedef struct { int32_t *sbuf; char *outbuf; + // for generating running statistics... + + stats_t *statistics; + // for holding the output rate information until printed out at the end of a session double frame_rate; int frame_rate_status; @@ -151,6 +161,7 @@ typedef struct { int ab_buffering, ab_synced; int64_t first_packet_timestamp; int flush_requested; + int flush_output_flushed; // true if the output device has been flushed. uint32_t flush_rtp_timestamp; uint64_t time_of_last_audio_packet; seq_t ab_read, ab_write; @@ -177,7 +188,7 @@ typedef struct { // RTP stuff // only one RTP session can be active at a time. int rtp_running; - uint64_t rtp_time_of_last_resend_request_error_fp; + uint64_t rtp_time_of_last_resend_request_error_ns; char client_ip_string[INET6_ADDRSTRLEN]; // the ip string pointing to the client char self_ip_string[INET6_ADDRSTRLEN]; // the ip string being used by this program -- it @@ -256,7 +267,7 @@ typedef struct { void *dapo_private_storage; // this is used for compatibility, if dacp stuff isn't enabled. int enable_dither; // needed for filling silences before play actually starts - int64_t dac_buffer_queue_minimum_length; + uint64_t dac_buffer_queue_minimum_length; } rtsp_conn_info; uint32_t modulo_32_offset(uint32_t from, uint32_t to); diff --git a/rtp.c b/rtp.c index 6a90a5cda..e56c5454f 100644 --- a/rtp.c +++ b/rtp.c @@ -45,11 +45,19 @@ #include #include +struct Nvll { + char* name; + double value; + struct Nvll *next; +}; + +typedef struct Nvll nvll; + uint64_t local_to_remote_time_jitter; uint64_t local_to_remote_time_jitter_count; void rtp_initialise(rtsp_conn_info *conn) { - conn->rtp_time_of_last_resend_request_error_fp = 0; + conn->rtp_time_of_last_resend_request_error_ns = 0; conn->rtp_running = 0; // initialise the timer mutex int rc = pthread_mutex_init(&conn->reference_time_mutex, NULL); @@ -69,32 +77,20 @@ uint64_t local_to_remote_time_difference_now(rtsp_conn_info *conn) { // this is an attempt to compensate for clock drift since the last time ping that was used // so, if we have a non-zero clock drift, we will calculate the drift there would // be from the time of the last time ping - uint64_t local_time_now_fp = get_absolute_time_in_fp(); uint64_t time_since_last_local_to_remote_time_difference_measurement = - local_time_now_fp - conn->local_to_remote_time_difference_measurement_time; - - uint64_t remote_time_since_last_local_to_remote_time_difference_measurement = - (uint64_t)(conn->local_to_remote_time_gradient * - time_since_last_local_to_remote_time_difference_measurement); - - double drift; - if (remote_time_since_last_local_to_remote_time_difference_measurement >= - time_since_last_local_to_remote_time_difference_measurement) - drift = (1.0 * (remote_time_since_last_local_to_remote_time_difference_measurement - - time_since_last_local_to_remote_time_difference_measurement)) / - (uint64_t)0x100000000; - else - drift = -((1.0 * (time_since_last_local_to_remote_time_difference_measurement - - remote_time_since_last_local_to_remote_time_difference_measurement)) / - (uint64_t)0x100000000); - - // double interval_ms = - // 1.0*(((time_since_last_local_to_remote_time_difference_measurement)*1000)>>32); - // debug(1,"Measurement drift is %.2f microseconds (0x%" PRIx64 " in 64-bit fp) over %.2f - // milliseconds with drift of %.2f - // ppm.",drift*1000000,(uint64_t)(drift*(uint64_t)0x100000000),interval_ms,(1.0-conn->local_to_remote_time_gradient)*1000000); - // return conn->local_to_remote_time_difference + (uint64_t)(drift*(uint64_t 0x100000000)); - return conn->local_to_remote_time_difference + (uint64_t)(drift * (uint64_t)0x100000000); + get_absolute_time_in_ns() - conn->local_to_remote_time_difference_measurement_time; + + uint64_t result = conn->local_to_remote_time_difference; + if (conn->local_to_remote_time_gradient >= 1.0) { + result = conn->local_to_remote_time_difference + + (uint64_t)((conn->local_to_remote_time_gradient - 1.0) * + time_since_last_local_to_remote_time_difference_measurement); + } else { + result = conn->local_to_remote_time_difference - + (uint64_t)((1.0 - conn->local_to_remote_time_gradient) * + time_since_last_local_to_remote_time_difference_measurement); + } + return result; } void rtp_audio_receiver_cleanup_handler(__attribute__((unused)) void *arg) { @@ -108,7 +104,7 @@ void *rtp_audio_receiver(void *arg) { int32_t last_seqno = -1; uint8_t packet[2048], *pktp; - uint64_t time_of_previous_packet_fp = 0; + uint64_t time_of_previous_packet_ns = 0; float longest_packet_time_interval_us = 0.0; // mean and variance calculations from "online_variance" algorithm at @@ -125,11 +121,10 @@ void *rtp_audio_receiver(void *arg) { frame_count++; - uint64_t local_time_now_fp = get_absolute_time_in_fp(); - if (time_of_previous_packet_fp) { - float time_interval_us = - (((local_time_now_fp - time_of_previous_packet_fp) * 1000000) >> 32) * 1.0; - time_of_previous_packet_fp = local_time_now_fp; + uint64_t local_time_now_ns = get_absolute_time_in_ns(); + if (time_of_previous_packet_ns) { + float time_interval_us = (local_time_now_ns - time_of_previous_packet_ns) * 0.001; + time_of_previous_packet_ns = local_time_now_ns; if (time_interval_us > longest_packet_time_interval_us) longest_packet_time_interval_us = time_interval_us; stat_n += 1; @@ -137,17 +132,18 @@ void *rtp_audio_receiver(void *arg) { stat_mean += stat_delta / stat_n; stat_M2 += stat_delta * (time_interval_us - stat_mean); if (stat_n % 2500 == 0) { - debug(2, "Packet reception interval stats: mean, standard deviation and max for the last " - "2,500 packets in microseconds: %10.1f, %10.1f, %10.1f.", + debug(2, + "Packet reception interval stats: mean, standard deviation and max for the last " + "2,500 packets in microseconds: %10.1f, %10.1f, %10.1f.", stat_mean, sqrtf(stat_M2 / (stat_n - 1)), longest_packet_time_interval_us); stat_n = 0; stat_mean = 0.0; stat_M2 = 0.0; - time_of_previous_packet_fp = 0; + time_of_previous_packet_ns = 0; longest_packet_time_interval_us = 0.0; } } else { - time_of_previous_packet_fp = local_time_now_fp; + time_of_previous_packet_ns = local_time_now_ns; } if (nread >= 0) { @@ -248,9 +244,6 @@ void *rtp_control_receiver(void *arg) { ssize_t nread; while (1) { nread = recv(conn->control_socket, packet, sizeof(packet), 0); - // local_time_now = get_absolute_time_in_fp(); - // clock_gettime(CLOCK_MONOTONIC,&tn); - // local_time_now=((uint64_t)tn.tv_sec<<32)+((uint64_t)tn.tv_nsec<<32)/1000000000; if (nread >= 0) { @@ -270,8 +263,8 @@ void *rtp_control_receiver(void *arg) { obfp += 2; }; *obfp = 0; - - + + // get raw timestamp information // I think that a good way to understand these timestamps is that // (1) the rtlt below is the timestamp of the frame that should be playing at the @@ -282,19 +275,19 @@ void *rtp_control_receiver(void *arg) { // Thus, (3) the latency can be calculated by subtracting the second from the // first. // There must be more to it -- there something missing. - + // In addition, it seems that if the value of the short represented by the second // pair of bytes in the packet is 7 // then an extra time lag is expected to be added, presumably by // the AirPort Express. - + // Best guess is that this delay is 11,025 frames. - + uint32_t rtlt = nctohl(&packet[4]); // raw timestamp less latency uint32_t rt = nctohl(&packet[16]); // raw timestamp - + uint32_t fl = nctohs(&packet[2]); // - + debug(1,"Sync Packet of %d bytes received: \"%s\", flags: %d, timestamps %u and %u, giving a latency of %d frames.",plen,obf,fl,rt,rtlt,rt-rtlt); //debug(1,"Monotonic timestamps are: %" PRId64 " and %" PRId64 " @@ -303,11 +296,16 @@ void *rtp_control_receiver(void *arg) { */ if (conn->local_to_remote_time_difference) { // need a time packet to be interchanged // first... + uint64_t ps, pn; - remote_time_of_sync = (uint64_t)nctohl(&packet[8]) << 32; - remote_time_of_sync += nctohl(&packet[12]); + ps = nctohl(&packet[8]); + ps = ps * 1000000000; // this many nanoseconds from the whole seconds + pn = nctohl(&packet[12]); + pn = pn * 1000000000; + pn = pn >> 32; // this many nanoseconds from the fractional part + remote_time_of_sync = ps + pn; - // debug(1,"Remote Sync Time: %0llx.",remote_time_of_sync); + // debug(1,"Remote Sync Time: " PRIu64 "",remote_time_of_sync); sync_rtp_timestamp = nctohl(&packet[16]); uint32_t rtp_timestamp_less_latency = nctohl(&packet[4]); @@ -368,9 +366,10 @@ void *rtp_control_receiver(void *arg) { if (la != conn->latency) { conn->latency = la; - debug(3, "New latency detected: %" PRIu32 ", sync latency: %" PRIu32 - ", minimum latency: %" PRIu32 ", maximum " - "latency: %" PRIu32 ", fixed offset: %" PRIu32 ".", + debug(3, + "New latency detected: %" PRIu32 ", sync latency: %" PRIu32 + ", minimum latency: %" PRIu32 ", maximum " + "latency: %" PRIu32 ", fixed offset: %" PRIu32 ".", la, sync_rtp_timestamp - rtp_timestamp_less_latency, conn->minimum_latency, conn->maximum_latency, config.fixedLatencyOffset); } @@ -390,15 +389,8 @@ void *rtp_control_receiver(void *arg) { conn->initial_reference_time; // here, this should never be zero if (remote_frame_time_interval) { conn->remote_frame_rate = - (1.0 * (conn->reference_timestamp - conn->initial_reference_timestamp)) / - remote_frame_time_interval; // an IEEE double calculation with a 32-bit - // numerator and 64-bit denominator - // integers - conn->remote_frame_rate = - conn->remote_frame_rate * (uint64_t)0x100000000; // this should just change the - // [binary] exponent in the IEEE - // FP representation; the - // mantissa should be unaffected. + (1.0E9 * (conn->reference_timestamp - conn->initial_reference_timestamp)) / + remote_frame_time_interval; } else { conn->remote_frame_rate = 0.0; // use as a flag. } @@ -423,28 +415,6 @@ void *rtp_control_receiver(void *arg) { else conn->reference_to_previous_frame_difference = sync_rtp_timestamp - old_reference_timestamp; - - // int64_t delayed_frame_difference = rtp_timestamp_less_latency - - // old_latency_delayed_timestamp; - - /* - if (old_remote_reference_time) - debug(1,"Time difference: %" PRIu64 " reference and delayed frame differences: %" - PRId64 " and %" PRId64 ", giving rates _at source!!_ of %f and %f respectively.", - (conn->reference_to_previous_time_difference*1000000)>>32,conn->reference_to_previous_frame_difference,delayed_frame_difference, - (1.0*(conn->reference_to_previous_frame_difference*10000000))/((conn->reference_to_previous_time_difference*10000000)>>32),(1.0*(delayed_frame_difference*10000000))/((conn->reference_to_previous_time_difference*10000000)>>32)); - else - debug(1,"First sync received"); - */ - - // debug(1,"New Reference timestamp and timestamp time..."); - // get estimated remote time now - // remote_time_now = local_time_now + local_to_remote_time_difference; - - // debug(1,"Sync Time is %lld us late (remote - // times).",((remote_time_now-remote_time_of_sync)*1000000)>>32); - // debug(1,"Sync Time is %lld us late (local - // times).",((local_time_now-reference_timestamp_time)*1000000)>>32); } else { debug(2, "Sync packet received before we got a timing packet back."); } @@ -520,8 +490,7 @@ void *rtp_timing_sender(void *arg) { req.filler = 0; req.origin = req.receive = req.transmit = 0; - // clock_gettime(CLOCK_MONOTONIC,&dtt); - conn->departure_time = get_absolute_time_in_fp(); + conn->departure_time = get_absolute_time_in_ns(); socklen_t msgsize = sizeof(struct sockaddr_in); #ifdef AF_INET6 if (conn->rtp_client_timing_socket.SAFAMILY == AF_INET6) { @@ -542,8 +511,8 @@ void *rtp_timing_sender(void *arg) { request_number++; - if (request_number <= 4) - usleep(500000); // these are thread cancellation points + if (request_number <= 6) + usleep(300000); // these are thread cancellation points else usleep(3000000); } @@ -553,8 +522,29 @@ void *rtp_timing_sender(void *arg) { } void rtp_timing_receiver_cleanup_handler(void *arg) { - debug(3, "Timing Receiver Cleanup."); rtsp_conn_info *conn = (rtsp_conn_info *)arg; + debug(3, "Timing Receiver Cleanup."); + // walk down the list of DACP / gradient pairs, if any + nvll *gradients = config.gradients; + if (conn->dacp_id) + while ((gradients) && (strcasecmp((const char *)&conn->client_ip_string,gradients->name) != 0)) + gradients = gradients->next; + + // if gradients comes out of this non-null, it is pointing to the DACP and it's last-known gradient + if (gradients) { + gradients->value = conn->local_to_remote_time_gradient; + // debug(1,"Updating a drift of %.2f ppm for \"%s\".", (conn->local_to_remote_time_gradient - 1.0)*1000000, gradients->name); + } else { + nvll *new_entry = (nvll*)malloc(sizeof(nvll)); + if (new_entry) { + new_entry->name = strdup((const char *)&conn->client_ip_string); + new_entry->value = conn->local_to_remote_time_gradient; + new_entry->next = config.gradients; + config.gradients = new_entry; + // debug(1,"Setting a new drift of %.2f ppm for \"%s\".", (conn->local_to_remote_time_gradient - 1.0)*1000000, new_entry->name); + } + } + debug(3, "Cancel Timing Requester."); pthread_cancel(conn->timer_requester); int oldState; @@ -580,6 +570,34 @@ void *rtp_timing_receiver(void *arg) { // uint64_t first_local_time = 0; uint64_t first_local_to_remote_time_difference = 0; + + conn->local_to_remote_time_gradient = 1.0; // initial value. + // walk down the list of DACP / gradient pairs, if any + nvll *gradients = config.gradients; + while ((gradients) && (strcasecmp((const char *)&conn->client_ip_string,gradients->name) != 0)) + gradients = gradients->next; + + // if gradients comes out of this non-null, it is pointing to the IP and it's last-known gradient + if (gradients) { + conn->local_to_remote_time_gradient = gradients->value; + // debug(1,"Using a stored drift of %.2f ppm for \"%s\".", (conn->local_to_remote_time_gradient - 1.0)*1000000, gradients->name); + } + + // calculate diffusion factor + + // at the end of the array of time pings, the diffusion factor + // must be diffusion_expansion_factor + // this, at each step, the diffusion multiplication constant must + // be the nth root of diffusion_expansion_factor + // where n is the number of elements in the array + + const double diffusion_expansion_factor = 10; + double log_of_multiplier = log10(diffusion_expansion_factor)/time_ping_history; + double multiplier = pow(10,log_of_multiplier); + uint64_t dispersion_factor = (uint64_t)(multiplier * 100); + // debug(1,"dispersion factor is %" PRIu64 ".", dispersion_factor); + + // uint64_t first_local_to_remote_time_difference_time; // uint64_t l2rtd = 0; int sequence_number = 0; @@ -596,45 +614,37 @@ void *rtp_timing_receiver(void *arg) { if ((config.diagnostic_drop_packet_fraction == 0.0) || (drand48() > config.diagnostic_drop_packet_fraction)) { - arrival_time = get_absolute_time_in_fp(); + arrival_time = get_absolute_time_in_ns(); // ssize_t plen = nread; // debug(1,"Packet Received on Timing Port."); if (packet[1] == 0xd3) { // timing reply - /* - char obf[4096]; - char *obfp = obf; - int obfc; - for (obfc=0;obfcdeparture_time; + debug(3,"clock synchronisation request: return time is %8.3f milliseconds.",0.000001*return_time); - // uint64_t rtus = (return_time * 1000000) >> 32; - - if (((return_time * 1000000) >> 32) < 300000) { - - // debug(2,"Synchronisation ping return time is %f milliseconds.",(rtus*1.0)/1000); - + if (return_time < 200000000) { // must be less than 0.2 seconds // distant_receive_time = // ((uint64_t)ntohl(*((uint32_t*)&packet[16])))<<32+ntohl(*((uint32_t*)&packet[20])); - distant_receive_time = (uint64_t)nctohl(&packet[16]) << 32; - distant_receive_time += nctohl(&packet[20]); + uint64_t ps, pn; + + ps = nctohl(&packet[16]); + ps = ps * 1000000000; // this many nanoseconds from the whole seconds + pn = nctohl(&packet[20]); + pn = pn * 1000000000; + pn = pn >> 32; // this many nanoseconds from the fractional part + distant_receive_time = ps + pn; // distant_transmit_time = // ((uint64_t)ntohl(*((uint32_t*)&packet[24])))<<32+ntohl(*((uint32_t*)&packet[28])); - distant_transmit_time = (uint64_t)nctohl(&packet[24]) << 32; - distant_transmit_time += nctohl(&packet[28]); + ps = nctohl(&packet[24]); + ps = ps * 1000000000; // this many nanoseconds from the whole seconds + pn = nctohl(&packet[28]); + pn = pn * 1000000000; + pn = pn >> 32; // this many nanoseconds from the fractional part + distant_transmit_time = ps + pn; uint64_t remote_processing_time = 0; @@ -644,13 +654,8 @@ void *rtp_timing_receiver(void *arg) { debug(1, "Yikes: distant_transmit_time is before distant_receive_time; remote " "processing time set to zero."); } - // debug(1,"Return trip time: %" PRIu64 " uS, remote processing time: %" PRIu64 " - // uS.",(return_time*1000000)>>32,(remote_processing_time*1000000)>>32); - - uint64_t local_time_by_remote_clock = distant_transmit_time + return_time / 2; - - // remove the remote processing time from the record of the return time, as long at the - // processing time looks sensible. + // debug(1,"Return trip time: %" PRIu64 " nS, remote processing time: %" PRIu64 " + // nS.",return_time, remote_processing_time); if (remote_processing_time < return_time) return_time -= remote_processing_time; @@ -658,24 +663,25 @@ void *rtp_timing_receiver(void *arg) { debug(1, "Remote processing time greater than return time -- ignored."); int cc; + // debug(1, "time ping history is %d entries.", time_ping_history); for (cc = time_ping_history - 1; cc > 0; cc--) { conn->time_pings[cc] = conn->time_pings[cc - 1]; // if ((conn->time_ping_count) && (conn->time_ping_count < 10)) // conn->time_pings[cc].dispersion = // conn->time_pings[cc].dispersion * pow(2.14, // 1.0/conn->time_ping_count); - conn->time_pings[cc].dispersion = - (conn->time_pings[cc].dispersion * 110) / + if (conn->time_pings[cc].dispersion > UINT64_MAX / dispersion_factor) + debug(1,"dispersion factor is too large at %" PRIu64 "."); + else + conn->time_pings[cc].dispersion = + (conn->time_pings[cc].dispersion * dispersion_factor) / 100; // make the dispersions 'age' by this rational factor } // these are used for doing a least squares calculation to get the drift conn->time_pings[0].local_time = arrival_time; - conn->time_pings[0].remote_time = distant_transmit_time; + conn->time_pings[0].remote_time = distant_transmit_time + return_time / 2; conn->time_pings[0].sequence_number = sequence_number++; conn->time_pings[0].chosen = 0; - - conn->time_pings[0].local_to_remote_difference = - local_time_by_remote_clock - arrival_time; conn->time_pings[0].dispersion = return_time; if (conn->time_ping_count < time_ping_history) conn->time_ping_count++; @@ -685,28 +691,27 @@ void *rtp_timing_receiver(void *arg) { // mean and variance calculations from "online_variance" algorithm at // https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm - double rtfus = 1.0 * ((return_time * 1000000) >> 32); stat_n += 1; - double stat_delta = rtfus - stat_mean; + double stat_delta = return_time - stat_mean; stat_mean += stat_delta / stat_n; - stat_M2 += stat_delta * (rtfus - stat_mean); + stat_M2 += stat_delta * (return_time - stat_mean); // debug(1, "Timing packet return time stats: current, mean and standard deviation over - // %d packets: %.1f, %.1f, %.1f (microseconds).", - // stat_n,rtfus,stat_mean, sqrtf(stat_M2 / (stat_n - 1))); + // %d packets: %.1f, %.1f, %.1f (nanoseconds).", + // stat_n,return_time,stat_mean, sqrtf(stat_M2 / (stat_n - 1))); // here, pick the record with the least dispersion, and record that it's been chosen // uint64_t local_time_chosen = arrival_time; // uint64_t remote_time_chosen = distant_transmit_time; // now pick the timestamp with the lowest dispersion - uint64_t l2rtd = conn->time_pings[0].local_to_remote_difference; + uint64_t rt = conn->time_pings[0].remote_time; uint64_t lt = conn->time_pings[0].local_time; uint64_t tld = conn->time_pings[0].dispersion; int chosen = 0; for (cc = 1; cc < conn->time_ping_count; cc++) if (conn->time_pings[cc].dispersion < tld) { chosen = cc; - l2rtd = conn->time_pings[cc].local_to_remote_difference; + rt = conn->time_pings[cc].remote_time; lt = conn->time_pings[cc].local_time; tld = conn->time_pings[cc].dispersion; // local_time_chosen = conn->time_pings[cc].local_time; @@ -716,46 +721,8 @@ void *rtp_timing_receiver(void *arg) { // dispersion.",chosen,1.0*((tld * 1000000) >> 32)); conn->time_pings[chosen].chosen = 1; // record the fact that it has been used for timing - /* - // calculate the jitter -- the absolute time between the current - local_to_remote_time_difference and the new one and add it to the total jitter count - int64_t ji; - int64_t ltd =0; // local time difference for the jitter - - if (conn->time_ping_count > 1) { - if (l2rtd > conn->local_to_remote_time_difference) { - local_to_remote_time_jitter = - local_to_remote_time_jitter + l2rtd - conn->local_to_remote_time_difference; - ji = l2rtd - conn->local_to_remote_time_difference; // this is the difference - between the present local-to-remote-time-difference and the new one, i.e. the jitter - step - } else { - local_to_remote_time_jitter = - local_to_remote_time_jitter + conn->local_to_remote_time_difference - l2rtd; - ji = -(conn->local_to_remote_time_difference - l2rtd); - } - local_to_remote_time_jitter_count += 1; - } - if (conn->local_to_remote_time_difference_measurement_time < lt) - ltd = lt-conn->local_to_remote_time_difference_measurement_time; - else - ltd = -(conn->local_to_remote_time_difference_measurement_time-lt); - - if (ltd) { - debug(1,"Jitter: %" PRId64 " microseconds in %" PRId64 " microseconds.", (ji * - (int64_t)1000000)>>32, (ltd * (int64_t)1000000)>>32); - debug(1,"Source clock to local clock drift: %.2f ppm.",((1.0*ji)/ltd)*1000000.0); - } - // uncomment below to print jitter between client's clock and our clock - - if (ji) { - int64_t rtus = (tld*1000000)>>32; - debug(1,"Choosing time difference[%d] with dispersion of %" PRId64 " us with an - adjustment of %" PRId64 " us",chosen, rtus, (ji*1000000)>>32); - } - */ conn->local_to_remote_time_difference = - l2rtd; // make this the new local-to-remote-time-difference + rt - lt; // make this the new local-to-remote-time-difference conn->local_to_remote_time_difference_measurement_time = lt; // done at this time. if (first_local_to_remote_time_difference == 0) { @@ -785,53 +752,68 @@ void *rtp_timing_receiver(void *arg) { if ((conn->time_pings[cc].chosen) && (conn->time_pings[cc].sequence_number > (settling_time / 3))) { // wait for a approximate settling time - y_bar += (conn->time_pings[cc].remote_time >> - 12); // precision is down to 1/4th of a microsecond - x_bar += (conn->time_pings[cc].local_time >> 12); + // have to scale them down so that the sum, possibly over every term in the array, doesn't overflow + y_bar += (conn->time_pings[cc].remote_time >> time_ping_history_power_of_two); + x_bar += (conn->time_pings[cc].local_time >> time_ping_history_power_of_two); sample_count++; } + conn->local_to_remote_time_gradient_sample_count = sample_count; if (sample_count > sample_point_minimum) { y_bar = y_bar / sample_count; x_bar = x_bar / sample_count; + + int64_t xid, yid; - int64_t mtl, mbl; + double mtl, mbl; mtl = 0; mbl = 0; for (cc = 0; cc < conn->time_ping_count; cc++) if ((conn->time_pings[cc].chosen) && (conn->time_pings[cc].sequence_number > (settling_time / 3))) { - uint64_t slt = conn->time_pings[cc].local_time >> 12; + uint64_t slt = conn->time_pings[cc].local_time >> time_ping_history_power_of_two; if (slt > x_bar) xid = slt - x_bar; else xid = -(x_bar - slt); - uint64_t srt = conn->time_pings[cc].remote_time >> 12; + uint64_t srt = conn->time_pings[cc].remote_time >> time_ping_history_power_of_two; if (srt > y_bar) yid = srt - y_bar; else yid = -(y_bar - srt); - mtl = mtl + xid * yid; - mbl = mbl + xid * xid; + mtl = mtl + (1.0 * xid) * yid; + mbl = mbl + (1.0 * xid) * xid; } - conn->local_to_remote_time_gradient_sample_count = sample_count; if (mbl) - conn->local_to_remote_time_gradient = (1.0 * mtl) / mbl; + conn->local_to_remote_time_gradient = mtl / mbl; else { - conn->local_to_remote_time_gradient = 1.0; - debug(1, "rtp_timing_receiver: mbl is 0"); + // conn->local_to_remote_time_gradient = 1.0; + debug(1,"mbl is zero. Drift remains at %.2f ppm.", (conn->local_to_remote_time_gradient - 1.0)*1000000); } + + // scale the numbers back up + uint64_t ybf = y_bar << time_ping_history_power_of_two; + uint64_t xbf = x_bar << time_ping_history_power_of_two; + + conn->local_to_remote_time_difference = + ybf - xbf; // make this the new local-to-remote-time-difference + conn->local_to_remote_time_difference_measurement_time = xbf; + } else { - conn->local_to_remote_time_gradient = 1.0; + debug(3,"not enough samples to estimate drift -- remaining at %.2f ppm.", (conn->local_to_remote_time_gradient - 1.0)*1000000); + // conn->local_to_remote_time_gradient = 1.0; } // debug(1,"local to remote time gradient is %12.2f ppm, based on %d // samples.",conn->local_to_remote_time_gradient*1000000,sample_count); + } else { - debug(2, "Time ping turnaround time: %lld us -- it looks like a timing ping was lost.", - (return_time * 1000000) >> 32); + debug(1, + "Time ping turnaround time: %" PRIu64 + " ns -- it looks like a timing ping was lost.", + return_time); } } else { debug(1, "Timing port -- Unknown RTP packet of type 0x%02X length %d.", packet[1], nread); @@ -1039,16 +1021,13 @@ void rtp_setup(SOCKADDR *local, SOCKADDR *remote, uint16_t cport, uint16_t tport conn->local_control_port, conn->local_timing_port); conn->reference_timestamp = 0; - // pthread_create(&rtp_audio_thread, NULL, &rtp_audio_receiver, NULL); - // pthread_create(&rtp_control_thread, NULL, &rtp_control_receiver, NULL); - // pthread_create(&rtp_timing_thread, NULL, &rtp_timing_receiver, NULL); conn->request_sent = 0; conn->rtp_running = 1; #ifdef CONFIG_METADATA - send_ssnc_metadata('clip', strdup(conn->client_ip_string), strlen(conn->client_ip_string), 1); - send_ssnc_metadata('svip', strdup(conn->self_ip_string), strlen(conn->self_ip_string), 1); + send_ssnc_metadata('clip', conn->client_ip_string, strlen(conn->client_ip_string), 1); + send_ssnc_metadata('svip', conn->self_ip_string, strlen(conn->self_ip_string), 1); #endif } } @@ -1061,9 +1040,6 @@ void get_reference_timestamp_stuff(uint32_t *timestamp, uint64_t *timestamp_time *remote_timestamp_time = conn->remote_reference_timestamp_time; *timestamp_time = conn->remote_reference_timestamp_time - local_to_remote_time_difference_now(conn); - // if ((*timestamp == 0) && (*timestamp_time == 0)) { - // debug(1,"Reference timestamp is invalid."); - //} debug_mutex_unlock(&conn->reference_time_mutex, 0); } @@ -1088,9 +1064,8 @@ const int use_nominal_rate = 0; // specify whether to use the nominal input rate int sanitised_source_rate_information(uint32_t *frames, uint64_t *time, rtsp_conn_info *conn) { int result = 1; uint32_t fs = conn->input_rate; - *frames = fs; - uint64_t one_fp = (uint64_t)(0x100000000); // one second in fp form - *time = one_fp; + *frames = fs; // default value to return + *time = 1000000000; // default value to return if ((conn->initial_reference_time) && (conn->initial_reference_timestamp)) { // uint32_t local_frames = conn->reference_timestamp - conn->initial_reference_timestamp; uint32_t local_frames = @@ -1101,7 +1076,7 @@ int sanitised_source_rate_information(uint32_t *frames, uint64_t *time, rtsp_con } else { double calculated_frame_rate = conn->input_rate; if (local_time) - calculated_frame_rate = ((1.0 * local_frames) / local_time) * one_fp; + calculated_frame_rate = (1.0E9 * local_frames) / local_time; else debug(1, "sanitised_source_rate_information: local_time is zero"); if ((local_time == 0) || ((calculated_frame_rate / conn->input_rate) > 1.002) || @@ -1173,7 +1148,7 @@ int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) { // here, we calculate the time interval, in terms of remote time uint64_t offset = modulo_64_offset(conn->remote_reference_timestamp_time, remote_time); - int reference_time_was_earlier = (offset <= (uint64_t)0x100000000 * 3600); + int reference_time_was_earlier = (offset <= (uint64_t)3600000000000); if (reference_time_was_earlier) // if we haven't had a reference within the last hour, it'll be // taken as afterwards time_interval = remote_time - conn->remote_reference_timestamp_time; @@ -1217,11 +1192,10 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { msgsize = sizeof(struct sockaddr_in6); } #endif - uint64_t time_of_sending_fp = get_absolute_time_in_fp(); - uint64_t resend_error_backoff_time = (uint64_t)1000000 * 0.3; // 0.3 seconds - resend_error_backoff_time = (resend_error_backoff_time << 32) / 1000000; - if ((conn->rtp_time_of_last_resend_request_error_fp == 0) || - ((time_of_sending_fp - conn->rtp_time_of_last_resend_request_error_fp) > + uint64_t time_of_sending_ns = get_absolute_time_in_ns(); + uint64_t resend_error_backoff_time = 300000000; // 0.3 seconds + if ((conn->rtp_time_of_last_resend_request_error_ns == 0) || + ((time_of_sending_ns - conn->rtp_time_of_last_resend_request_error_ns) > resend_error_backoff_time)) { if ((config.diagnostic_drop_packet_fraction == 0.0) || (drand48() > config.diagnostic_drop_packet_fraction)) { @@ -1239,22 +1213,20 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) { (struct sockaddr *)&conn->rtp_client_control_socket, msgsize) == -1) { char em[1024]; strerror_r(errno, em, sizeof(em)); - debug(2, "Error %d using sendto to request a resend: \"%s\".", - errno, em); - conn->rtp_time_of_last_resend_request_error_fp = time_of_sending_fp; + debug(2, "Error %d using sendto to request a resend: \"%s\".", errno, em); + conn->rtp_time_of_last_resend_request_error_ns = time_of_sending_ns; } else { - conn->rtp_time_of_last_resend_request_error_fp = 0; + conn->rtp_time_of_last_resend_request_error_ns = 0; } } else { - debug( - 3, - "Dropping resend request packet to simulate a bad network. Backing off for 0.3 " - "second."); - conn->rtp_time_of_last_resend_request_error_fp = time_of_sending_fp; + debug(3, "Dropping resend request packet to simulate a bad network. Backing off for 0.3 " + "second."); + conn->rtp_time_of_last_resend_request_error_ns = time_of_sending_ns; } } else { - debug(1, "Suppressing a resend request due to a resend sendto error in the last 0.3 seconds."); + debug(1, + "Suppressing a resend request due to a resend sendto error in the last 0.3 seconds."); } } else { // if (!request_sent) { diff --git a/rtsp.c b/rtsp.c index 6cf539276..2febacd08 100644 --- a/rtsp.c +++ b/rtsp.c @@ -114,7 +114,8 @@ typedef struct { pthread_mutex_t pc_queue_lock; pthread_cond_t pc_queue_item_added_signal; pthread_cond_t pc_queue_item_removed_signal; - size_t item_size; // number of bytes in each item + char *name; + size_t item_size; // number of bytes in each item uint32_t count; // number of items in the queue uint32_t capacity; // maximum number of items uint32_t toq; // first item to take @@ -151,7 +152,11 @@ typedef struct { rtsp_message *carrier; } metadata_package; -void pc_queue_init(pc_queue *the_queue, char *items, size_t item_size, uint32_t number_of_items) { +void pc_queue_init(pc_queue *the_queue, char *items, size_t item_size, uint32_t number_of_items, const char* name) { + if (name) + debug(2, "Creating metadata queue \"%s\".", name); + else + debug(1, "Creating an unnamed metadata queue."); pthread_mutex_init(&the_queue->pc_queue_lock, NULL); pthread_cond_init(&the_queue->pc_queue_item_added_signal, NULL); pthread_cond_init(&the_queue->pc_queue_item_removed_signal, NULL); @@ -161,12 +166,26 @@ void pc_queue_init(pc_queue *the_queue, char *items, size_t item_size, uint32_t the_queue->capacity = number_of_items; the_queue->toq = 0; the_queue->eoq = 0; + if (name == NULL) + the_queue->name = NULL; + else + the_queue->name = strdup(name); } void pc_queue_delete(pc_queue *the_queue) { + if (the_queue->name) + debug(2, "Deleting metadata queue \"%s\".", the_queue->name); + else + debug(1, "Deleting an unnamed metadata queue."); + if (the_queue->name != NULL) + free(the_queue->name); + // debug(2, "destroying pc_queue_item_removed_signal"); pthread_cond_destroy(&the_queue->pc_queue_item_removed_signal); + // debug(2, "destroying pc_queue_item_added_signal"); pthread_cond_destroy(&the_queue->pc_queue_item_added_signal); + // debug(2, "destroying pc_queue_lock"); pthread_mutex_destroy(&the_queue->pc_queue_lock); + // debug(2, "destroying signals and locks done"); } int send_metadata(uint32_t type, uint32_t code, char *data, uint32_t length, rtsp_message *carrier, @@ -185,6 +204,7 @@ void pc_queue_cleanup_handler(void *arg) { } int pc_queue_add_item(pc_queue *the_queue, const void *the_stuff, int block) { + int response = 0; int rc; if (the_queue) { if (block == 0) { @@ -196,33 +216,43 @@ int pc_queue_add_item(pc_queue *the_queue, const void *the_stuff, int block) { if (rc) debug(1, "Error locking for pc_queue_add_item"); pthread_cleanup_push(pc_queue_cleanup_handler, (void *)the_queue); - while (the_queue->count == the_queue->capacity) { - rc = pthread_cond_wait(&the_queue->pc_queue_item_removed_signal, &the_queue->pc_queue_lock); - if (rc) - debug(1, "Error waiting for item to be removed"); + // leave this out if you want this to return if the queue is already full + // irrespective of the block flag. + /* + while (the_queue->count == the_queue->capacity) { + rc = pthread_cond_wait(&the_queue->pc_queue_item_removed_signal, &the_queue->pc_queue_lock); + if (rc) + debug(1, "Error waiting for item to be removed"); + } + */ + if (the_queue->count < the_queue->capacity) { + uint32_t i = the_queue->eoq; + void *p = the_queue->items + the_queue->item_size * i; + // void * p = &the_queue->qbase + the_queue->item_size*the_queue->eoq; + memcpy(p, the_stuff, the_queue->item_size); + + // update the pointer + i++; + if (i == the_queue->capacity) + // fold pointer if necessary + i = 0; + the_queue->eoq = i; + the_queue->count++; + //debug(2,"metadata queue+ \"%s\" %d/%d.", the_queue->name, the_queue->count, the_queue->capacity); + if (the_queue->count == the_queue->capacity) + debug(3, "metadata queue \"%s\": is now full with %d items in it!", the_queue->name, the_queue->count); + rc = pthread_cond_signal(&the_queue->pc_queue_item_added_signal); + if (rc) + debug(1, "metadata queue \"%s\": error signalling after pc_queue_add_item", the_queue->name); + } else { + response = EWOULDBLOCK; // a bit arbitrary, this. + debug(3,"metadata queue \"%s\": is already full with %d items in it. Not adding this item to the queue.", the_queue->name, the_queue->count); } - uint32_t i = the_queue->eoq; - void *p = the_queue->items + the_queue->item_size * i; - // void * p = &the_queue->qbase + the_queue->item_size*the_queue->eoq; - memcpy(p, the_stuff, the_queue->item_size); - - // update the pointer - i++; - if (i == the_queue->capacity) - // fold pointer if necessary - i = 0; - the_queue->eoq = i; - the_queue->count++; - if (the_queue->count == the_queue->capacity) - debug(1, "pc_queue is full with %d items in it!", the_queue->count); - rc = pthread_cond_signal(&the_queue->pc_queue_item_added_signal); - if (rc) - debug(1, "Error signalling after pc_queue_add_item"); pthread_cleanup_pop(1); // unlock the queue lock. } else { debug(1, "Adding an item to a NULL queue"); } - return 0; + return response; } int pc_queue_get_item(pc_queue *the_queue, void *the_stuff) { @@ -230,12 +260,12 @@ int pc_queue_get_item(pc_queue *the_queue, void *the_stuff) { if (the_queue) { rc = pthread_mutex_lock(&the_queue->pc_queue_lock); if (rc) - debug(1, "Error locking for pc_queue_get_item"); + debug(1, "metadata queue \"%s\": error locking for pc_queue_get_item", the_queue->name); pthread_cleanup_push(pc_queue_cleanup_handler, (void *)the_queue); while (the_queue->count == 0) { rc = pthread_cond_wait(&the_queue->pc_queue_item_added_signal, &the_queue->pc_queue_lock); if (rc) - debug(1, "Error waiting for item to be added"); + debug(1, "metadata queue \"%s\": error waiting for item to be added", the_queue->name); } uint32_t i = the_queue->toq; // void * p = &the_queue->qbase + the_queue->item_size*the_queue->toq; @@ -249,9 +279,10 @@ int pc_queue_get_item(pc_queue *the_queue, void *the_stuff) { i = 0; the_queue->toq = i; the_queue->count--; + debug(3,"metadata queue- \"%s\" %d/%d.", the_queue->name, the_queue->count, the_queue->capacity); rc = pthread_cond_signal(&the_queue->pc_queue_item_removed_signal); if (rc) - debug(1, "Error signalling after pc_queue_removed_item"); + debug(1, "metadata queue \"%s\": error signalling after pc_queue_get_item", the_queue->name); pthread_cleanup_pop(1); // unlock the queue lock. } else { debug(1, "Removing an item from a NULL queue"); @@ -286,7 +317,7 @@ void *player_watchdog_thread_code(void *arg) { uint64_t last_watchdog_bark_time = conn->watchdog_bark_time; debug_mutex_unlock(&conn->watchdog_mutex, 0); if (last_watchdog_bark_time != 0) { - uint64_t time_since_last_bark = (get_absolute_time_in_fp() - last_watchdog_bark_time) >> 32; + uint64_t time_since_last_bark = (get_absolute_time_in_ns() - last_watchdog_bark_time) / 1000000000; uint64_t ct = config.timeout; // go from int to 64-bit int if (time_since_last_bark >= ct) { @@ -337,11 +368,11 @@ static void track_thread(rtsp_conn_info *conn) { void cancel_all_RTSP_threads(void) { int i; for (i = 0; i < nconns; i++) { - debug(1, "Connection %d: cancelling.", conns[i]->connection_number); + debug(2, "Connection %d: cancelling.", conns[i]->connection_number); pthread_cancel(conns[i]->thread); } for (i = 0; i < nconns; i++) { - debug(1, "Connection %d: joining.", conns[i]->connection_number); + debug(2, "Connection %d: joining.", conns[i]->connection_number); pthread_join(conns[i]->thread, NULL); free(conns[i]); } @@ -415,6 +446,7 @@ void msg_retain(rtsp_message *msg) { debug(1, "Error %d locking reference counter lock"); if (msg > (rtsp_message *)0x00010000) { msg->referenceCount++; + debug(3,"msg_free increment reference counter message %d to %d.", msg->index_number, msg->referenceCount); // debug(1,"msg_retain -- item %d reference count %d.", msg->index_number, msg->referenceCount); rc = pthread_mutex_unlock(&reference_counter_lock); if (rc) @@ -430,6 +462,7 @@ rtsp_message *msg_init(void) { memset(msg, 0, sizeof(rtsp_message)); msg->referenceCount = 1; // from now on, any access to this must be protected with the lock msg->index_number = msg_indexes++; + debug(3,"msg_init message %d", msg->index_number); } else { die("msg_init -- can not allocate memory for rtsp_message %d.", msg_indexes); } @@ -493,6 +526,8 @@ void msg_free(rtsp_message **msgh) { if (*msgh > (rtsp_message *)0x00010000) { rtsp_message *msg = *msgh; msg->referenceCount--; + if (msg->referenceCount) + debug(3,"msg_free decrement reference counter message %d to %d", msg->index_number, msg->referenceCount); if (msg->referenceCount == 0) { unsigned int i; for (i = 0; i < msg->nheaders; i++) { @@ -507,6 +542,7 @@ void msg_free(rtsp_message **msgh) { index = 0x10000; // ensure it doesn't fold to zero. *msgh = (rtsp_message *)(index); // put a version of the index number of the freed message in here + debug(3,"msg_free freed message %d", msg->index_number); free(msg); } else { // debug(1,"msg_free item %d -- decrement reference to @@ -666,7 +702,7 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes } uint64_t threshold_time = - get_absolute_time_in_fp() + ((uint64_t)15 << 32); // i.e. fifteen seconds from now + get_absolute_time_in_ns() + ((uint64_t)15000000000); // i.e. fifteen seconds from now int warning_message_sent = 0; const size_t max_read_chunk = 1024 * 1024 / 16; @@ -679,7 +715,7 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes // metadata if (warning_message_sent == 0) { - uint64_t time_now = get_absolute_time_in_fp(); + uint64_t time_now = get_absolute_time_in_ns(); if (time_now > threshold_time) { // it's taking too long debug(1, "Error receiving metadata from source -- transmission seems " "to be stalled."); @@ -894,12 +930,17 @@ void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { } } // debug(1,"RTSP Flush Requested: %u.",rtptime); + +// the following is now done better by the player_flush routine as a 'pfls' +/* #ifdef CONFIG_METADATA if (p) send_metadata('ssnc', 'flsr', p + 1, strlen(p + 1), req, 1); else send_metadata('ssnc', 'flsr', NULL, 0, NULL, 0); #endif +*/ + player_flush(rtptime, conn); // will not crash even it there is no player thread. resp->respcode = 200; @@ -1070,7 +1111,7 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, char *progress = cp + strlen("progress: "); // debug(2, "progress: \"%s\"",progress); // rtpstampstart/rtpstampnow/rtpstampend 44100 per // second - send_ssnc_metadata('prgr', strdup(progress), strlen(progress), 1); + send_ssnc_metadata('prgr', progress, strlen(progress), 1); } else #endif @@ -1113,7 +1154,10 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, // 'aend' -- active mode exited. No arguments // 'pbeg' -- play stream begin. No arguments // 'pend' -- play stream end. No arguments -// 'pfls' -- play stream flush. No arguments +// 'pfls' -- play stream flush. The argument is an unsigned 32-bit +// frame number. It seems that all frames up to but not +// including this frame are to be flushed. +// // 'prsm' -- play stream resume. No arguments // `pffr` -- the first frame of a play session has been received and has been validly // timed. @@ -1153,8 +1197,11 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, // to send commands to the source's remote control (if it has one). // `clip` -- the payload is the IP number of the client, i.e. the sender of audio. // Can be an IPv4 or an IPv6 number. +// `svip` -- the payload is the IP number of the server, i.e. the player itself. +// Can be an IPv4 or an IPv6 number. // `dapo` -- the payload is the port number (as text) on the server to which remote // control commands should be sent. It is 3689 for iTunes but varies for iOS devices. + // A special sub-protocol is used for sending large data items over UDP // If the payload exceeded 4 MB, it is chunked using the following format: // "ssnc", "chnk", packet_ix, packet_counts, packet_tag, packet_type, chunked_data. @@ -1217,14 +1264,36 @@ char *base64_encode_so(const unsigned char *data, size_t input_length, char *enc static int fd = -1; // static int dirty = 0; + + + pc_queue metadata_queue; +#define metadata_queue_size 500 +metadata_package metadata_queue_items[metadata_queue_size]; +pthread_t metadata_thread; + +#ifdef CONFIG_METADATA_HUB +pc_queue metadata_hub_queue; +#define metadata_hub_queue_size 500 +metadata_package metadata_hub_queue_items[metadata_hub_queue_size]; +pthread_t metadata_hub_thread; +#endif + +#ifdef CONFIG_MQTT +pc_queue metadata_mqtt_queue; +#define metadata_mqtt_queue_size 500 +metadata_package metadata_mqtt_queue_items[metadata_mqtt_queue_size]; +pthread_t metadata_mqtt_thread; +#endif + static int metadata_sock = -1; static struct sockaddr_in metadata_sockaddr; static char *metadata_sockmsg; -#define metadata_queue_size 500 -metadata_package metadata_queue_items[metadata_queue_size]; +pc_queue metadata_multicast_queue; +#define metadata_multicast_queue_size 500 +metadata_package metadata_multicast_queue_items[metadata_queue_size]; +pthread_t metadata_multicast_thread; -pthread_t metadata_thread; void metadata_create_multicast_socket(void) { if (config.metadata_enabled == 0) @@ -1252,19 +1321,6 @@ void metadata_create_multicast_socket(void) { } } } - - size_t pl = strlen(config.metadata_pipename) + 1; - - char *path = malloc(pl + 1); - snprintf(path, pl + 1, "%s", config.metadata_pipename); - - mode_t oldumask = umask(000); - - if (mkfifo(path, 0666) && errno != EEXIST) - die("Could not create metadata FIFO %s", path); - - free(path); - umask(oldumask); } void metadata_delete_multicast_socket(void) { @@ -1276,6 +1332,7 @@ void metadata_delete_multicast_socket(void) { free(metadata_sockmsg); } + void metadata_open(void) { if (config.metadata_enabled == 0) return; @@ -1285,11 +1342,7 @@ void metadata_open(void) { char *path = malloc(pl + 1); snprintf(path, pl + 1, "%s", config.metadata_pipename); - fd = open(path, O_WRONLY | O_NONBLOCK); - // if (fd < 0) - // debug(1, "Could not open metadata FIFO %s. Will try again later.", - // path); - + fd = try_to_open_pipe_for_writing(path); free(path); } @@ -1300,10 +1353,8 @@ static void metadata_close(void) { fd = -1; } -void metadata_process(uint32_t type, uint32_t code, char *data, uint32_t length) { - // debug(2, "Process metadata with type %x, code %x and length %u.", type, code, length); - int ret; - +void metadata_multicast_process(uint32_t type, uint32_t code, char *data, uint32_t length) { + // debug(1, "Process multicast metadata with type %x, code %x and length %u.", type, code, length); if (metadata_sock >= 0 && length < config.metadata_sockmsglength - 8) { char *ptr = metadata_sockmsg; uint32_t v; @@ -1358,8 +1409,13 @@ void metadata_process(uint32_t type, uint32_t code, char *data, uint32_t length) break; } while (1); } +} +void metadata_process(uint32_t type, uint32_t code, char *data, uint32_t length) { + // debug(1, "Process metadata with type %x, code %x and length %u.", type, code, length); + int ret = 0; // readers may go away and come back + if (fd < 0) metadata_open(); if (fd < 0) @@ -1367,14 +1423,16 @@ void metadata_process(uint32_t type, uint32_t code, char *data, uint32_t length) char thestring[1024]; snprintf(thestring, 1024, "%x%x%u", type, code, length); - ret = non_blocking_write(fd, thestring, strlen(thestring)); + // ret = non_blocking_write(fd, thestring, strlen(thestring)); + ret = write(fd, thestring, strlen(thestring)); if (ret < 0) { // debug(1,"metadata_process error %d exit 1",ret); return; } if ((data != NULL) && (length > 0)) { snprintf(thestring, 1024, "\n\n"); - ret = non_blocking_write(fd, thestring, strlen(thestring)); + // ret = non_blocking_write(fd, thestring, strlen(thestring)); + ret = write(fd, thestring, strlen(thestring)); if (ret < 0) { // debug(1,"metadata_process error %d exit 2",ret); return; @@ -1397,7 +1455,8 @@ void metadata_process(uint32_t type, uint32_t code, char *data, uint32_t length) debug(1, "Error encoding base64 data."); // debug(1,"Remaining count: %d ret: %d, outbuf_size: // %d.",remaining_count,ret,outbuf_size); - ret = non_blocking_write(fd, outbuf, outbuf_size); + //ret = non_blocking_write(fd, outbuf, outbuf_size); + ret = write(fd, outbuf, outbuf_size); if (ret < 0) { // debug(1,"metadata_process error %d exit 3",ret); return; @@ -1406,27 +1465,22 @@ void metadata_process(uint32_t type, uint32_t code, char *data, uint32_t length) remaining_count -= towrite_count; } snprintf(thestring, 1024, ""); - ret = non_blocking_write(fd, thestring, strlen(thestring)); + // ret = non_blocking_write(fd, thestring, strlen(thestring)); + ret = write(fd, thestring, strlen(thestring)); if (ret < 0) { // debug(1,"metadata_process error %d exit 4",ret); return; } } snprintf(thestring, 1024, "\n"); - ret = non_blocking_write(fd, thestring, strlen(thestring)); + //ret = non_blocking_write(fd, thestring, strlen(thestring)); + ret = write(fd, thestring, strlen(thestring)); if (ret < 0) { // debug(1,"metadata_process error %d exit 5",ret); return; } } -void metadata_thread_cleanup_function(__attribute__((unused)) void *arg) { - debug(2, "metadata_thread_cleanup_function called"); - metadata_delete_multicast_socket(); - metadata_close(); - pc_queue_delete(&metadata_queue); -} - void metadata_pack_cleanup_function(void *arg) { // debug(1, "metadata_pack_cleanup_function called"); metadata_package *pack = (metadata_package *)arg; @@ -1434,12 +1488,19 @@ void metadata_pack_cleanup_function(void *arg) { msg_free(&pack->carrier); // release the message else if (pack->data) free(pack->data); + // debug(1, "metadata_pack_cleanup_function exit"); +} + +void metadata_thread_cleanup_function(__attribute__((unused)) void *arg) { + // debug(2, "metadata_thread_cleanup_function called"); + metadata_close(); + pc_queue_delete(&metadata_queue); } void *metadata_thread_function(__attribute__((unused)) void *ignore) { // create a pc_queue for passing information to a threaded metadata handler pc_queue_init(&metadata_queue, (char *)&metadata_queue_items, sizeof(metadata_package), - metadata_queue_size); + metadata_queue_size, "pipe"); metadata_create_multicast_socket(); metadata_package pack; pthread_cleanup_push(metadata_thread_cleanup_function, NULL); @@ -1447,39 +1508,199 @@ void *metadata_thread_function(__attribute__((unused)) void *ignore) { pc_queue_get_item(&metadata_queue, &pack); pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack); if (config.metadata_enabled) { + if (pack.carrier) { + debug(3, " pipe: type %x, code %x, length %u, message %d.", pack.type, pack.code, pack.length, pack.carrier->index_number); + } else { + debug(3, " pipe: type %x, code %x, length %u.", pack.type, pack.code, pack.length); + } metadata_process(pack.type, pack.code, pack.data, pack.length); + debug(3, " pipe: done."); + } + pthread_cleanup_pop(1); + } + pthread_cleanup_pop(1); // will never happen + pthread_exit(NULL); +} + +void metadata_multicast_thread_cleanup_function(__attribute__((unused)) void *arg) { + // debug(2, "metadata_multicast_thread_cleanup_function called"); + metadata_delete_multicast_socket(); + pc_queue_delete(&metadata_multicast_queue); +} + +void *metadata_multicast_thread_function(__attribute__((unused)) void *ignore) { + // create a pc_queue for passing information to a threaded metadata handler + pc_queue_init(&metadata_multicast_queue, (char *)&metadata_multicast_queue_items, sizeof(metadata_package), + metadata_multicast_queue_size, "multicast"); + metadata_create_multicast_socket(); + metadata_package pack; + pthread_cleanup_push(metadata_multicast_thread_cleanup_function, NULL); + while (1) { + pc_queue_get_item(&metadata_multicast_queue, &pack); + pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack); + if (config.metadata_enabled) { + if (pack.carrier) { + debug(3, " multicast: type %x, code %x, length %u, message %d.", pack.type, pack.code, pack.length, pack.carrier->index_number); + } else { + debug(3, " multicast: type %x, code %x, length %u.", pack.type, pack.code, pack.length); + } + metadata_multicast_process(pack.type, pack.code, pack.data, pack.length); + debug(3, " multicast: done."); + } + pthread_cleanup_pop(1); + } + pthread_cleanup_pop(1); // will never happen + pthread_exit(NULL); +} + + #ifdef CONFIG_METADATA_HUB - metadata_hub_process_metadata(pack.type, pack.code, pack.data, pack.length); +void metadata_hub_close(void) { +} + +void metadata_hub_thread_cleanup_function(__attribute__((unused)) void *arg) { + // debug(2, "metadata_hub_thread_cleanup_function called"); + metadata_hub_close(); + pc_queue_delete(&metadata_hub_queue); +} + +void *metadata_hub_thread_function(__attribute__((unused)) void *ignore) { + + // create the fifo, if necessary + size_t pl = strlen(config.metadata_pipename) + 1; + char *path = malloc(pl + 1); + snprintf(path, pl + 1, "%s", config.metadata_pipename); + + mode_t oldumask = umask(000); + if (mkfifo(path, 0644) && errno != EEXIST) + die("Could not create metadata pipe \"%s\".", path); + umask(oldumask); + debug(1, "metadata pipe name is \"%s\".", path); + + // try to open it + fd = try_to_open_pipe_for_writing(path); + // we check that it's not a "real" error. 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." Which is okay. + if ((fd == -1) && (errno != ENXIO)) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "metadata_hub_thread_function -- error %d (\"%s\") opening pipe: \"%s\".", errno, + (char *)errorstring, path); + warn("can not open metadata pipe -- error %d (\"%s\") opening pipe: \"%s\".", errno, + (char *)errorstring, path); + } + free(path); + + // create a pc_queue for passing information to a threaded metadata handler + pc_queue_init(&metadata_hub_queue, (char *)&metadata_hub_queue_items, sizeof(metadata_package), + metadata_hub_queue_size, "hub"); + metadata_package pack; + pthread_cleanup_push(metadata_hub_thread_cleanup_function, NULL); + while (1) { + pc_queue_get_item(&metadata_hub_queue, &pack); + pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack); + if (pack.carrier) { + debug(3, " hub: type %x, code %x, length %u, message %d.", pack.type, pack.code, pack.length, pack.carrier->index_number); + } else { + debug(3, " hub: type %x, code %x, length %u.", pack.type, pack.code, pack.length); + } + metadata_hub_process_metadata(pack.type, pack.code, pack.data, pack.length); + debug(3, " hub: done."); + pthread_cleanup_pop(1); + } + pthread_cleanup_pop(1); // will never happen + pthread_exit(NULL); +} #endif #ifdef CONFIG_MQTT - if (config.mqtt_enabled) { - mqtt_process_metadata(pack.type, pack.code, pack.data, pack.length); - } -#endif - } +void metadata_mqtt_close(void) { +} + +void metadata_mqtt_thread_cleanup_function(__attribute__((unused)) void *arg) { + // debug(2, "metadata_mqtt_thread_cleanup_function called"); + metadata_mqtt_close(); + pc_queue_delete(&metadata_mqtt_queue); + // debug(2, "metadata_mqtt_thread_cleanup_function done"); +} + +void *metadata_mqtt_thread_function(__attribute__((unused)) void *ignore) { + // create a pc_queue for passing information to a threaded metadata handler + pc_queue_init(&metadata_mqtt_queue, (char *)&metadata_mqtt_queue_items, sizeof(metadata_package), + metadata_mqtt_queue_size, "mqtt"); + metadata_package pack; + pthread_cleanup_push(metadata_mqtt_thread_cleanup_function, NULL); + while (1) { + pc_queue_get_item(&metadata_mqtt_queue, &pack); + pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack); + if (config.mqtt_enabled) { + if (pack.carrier) { + debug(3, " mqtt: type %x, code %x, length %u, message %d.", pack.type, pack.code, pack.length, pack.carrier->index_number); + } else { + debug(3, " mqtt: type %x, code %x, length %u.", pack.type, pack.code, pack.length); + } + mqtt_process_metadata(pack.type, pack.code, pack.data, pack.length); + debug(3, " mqtt: done."); + } + pthread_cleanup_pop(1); } pthread_cleanup_pop(1); // will never happen pthread_exit(NULL); } +#endif void metadata_init(void) { int ret = pthread_create(&metadata_thread, NULL, metadata_thread_function, NULL); if (ret) debug(1, "Failed to create metadata thread!"); + + ret = pthread_create(&metadata_multicast_thread, NULL, metadata_multicast_thread_function, NULL); + if (ret) + debug(1, "Failed to create metadata multicast thread!"); + +#ifdef CONFIG_METADATA_HUB + ret = pthread_create(&metadata_hub_thread, NULL, metadata_hub_thread_function, NULL); + if (ret) + debug(1, "Failed to create metadata hub thread!"); +#endif +#ifdef CONFIG_MQTT + ret = pthread_create(&metadata_mqtt_thread, NULL, metadata_mqtt_thread_function, NULL); + if (ret) + debug(1, "Failed to create metadata mqtt thread!"); +#endif metadata_running = 1; } void metadata_stop(void) { if (metadata_running) { debug(2, "metadata_stop called."); +#ifdef CONFIG_MQTT + // debug(2, "metadata stop mqtt thread."); + pthread_cancel(metadata_mqtt_thread); + pthread_join(metadata_mqtt_thread, NULL); + // debug(2, "metadata stop mqtt done."); +#endif +#ifdef CONFIG_METADATA_HUB + // debug(2, "metadata stop hub thread."); + pthread_cancel(metadata_hub_thread); + pthread_join(metadata_hub_thread, NULL); + // debug(2, "metadata stop hub done."); +#endif + // debug(2, "metadata stop multicast thread."); + pthread_cancel(metadata_multicast_thread); + pthread_join(metadata_multicast_thread, NULL); + // debug(2, "metadata stop multicast done."); + + // debug(2, "metadata stop metadata_thread thread."); pthread_cancel(metadata_thread); pthread_join(metadata_thread, NULL); + // debug(2, "metadata_stop finished successfully."); } } -int send_metadata(uint32_t type, uint32_t code, char *data, uint32_t length, rtsp_message *carrier, +int send_metadata_to_queue(pc_queue* queue, uint32_t type, uint32_t code, char *data, uint32_t length, rtsp_message *carrier, int block) { // parameters: type, code, pointer to data or NULL, length of data or NULL, @@ -1496,37 +1717,64 @@ int send_metadata(uint32_t type, uint32_t code, char *data, uint32_t length, rts // The reading of the parameters is a bit complex // If the rtsp_message field is non-null, then it represents an rtsp_message - // which should be freed - // in the thread handler when the parameter pointed to by the pointer and - // specified by the length - // is finished with - // If the rtsp_message is NULL, then if the pointer is non-null, it points to - // a malloc'ed block - // and should be freed when the thread is finished with it. The length of the - // data in the block is - // given in length + // and the data pointer is assumed to point to something within it. + // The reference counter of the rtsp_message is incremented here and + // should be decremented by the metadata handler when finished. + // If the reference count reduces to zero, the message will be freed. + + // If the rtsp_message is NULL, then if the pointer is non-null then the data it + // points to, of the length specified, is memcpy'd and passed to the metadata + // handler. The handler should free it when done. // If the rtsp_message is NULL and the pointer is also NULL, nothing further // is done. metadata_package pack; pack.type = type; pack.code = code; - pack.data = data; pack.length = length; pack.carrier = carrier; - if (pack.carrier) + pack.data = data; + if (pack.carrier) { msg_retain(pack.carrier); - int rc = pc_queue_add_item(&metadata_queue, &pack, block); - if (rc == EBUSY) { - if (pack.carrier) + } else { + if (data) + pack.data = memdup(data,length); // only if it's not a null + } + int rc = pc_queue_add_item(queue, &pack, block); + if (rc != 0) { + if (pack.carrier) { + if (rc == EWOULDBLOCK) + debug(2, "metadata queue \"%s\" full, dropping message item: type %x, code %x, data %x, length %u, message %d.", queue->name, pack.type, pack.code, pack.data, pack.length, pack.carrier->index_number); msg_free(&pack.carrier); - else if (data) - free(data); - warn("Metadata queue is busy, discarding message of type 0x%08X, code 0x%08X.", type, code); + } else { + if (rc == EWOULDBLOCK) + debug(2, "metadata queue \"%s\" full, dropping data item: type %x, code %x, data %x, length %u.", queue->name, pack.type, pack.code, pack.data, pack.length); + if (pack.data) + free(pack.data); + } } return rc; } +int send_metadata(uint32_t type, uint32_t code, char *data, uint32_t length, rtsp_message *carrier, + int block) { + int rc; + rc = send_metadata_to_queue(&metadata_queue, type, code, data, length, carrier, block); + +#ifdef CONFIG_METADATA_HUB + rc = send_metadata_to_queue(&metadata_hub_queue, type, code, data, length, carrier, block); +#endif + +#ifdef CONFIG_MQTT + rc = send_metadata_to_queue(&metadata_mqtt_queue, type, code, data, length, carrier, block); +#endif + + return rc; +} + + + + static void handle_set_parameter_metadata(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message *req, __attribute__((unused)) rtsp_message *resp) { @@ -2319,7 +2567,7 @@ static void *rtsp_conversation_thread_func(void *pconn) { rtsp_conn_info *conn = pconn; // create the watchdog mutex, initialise the watchdog time and start the watchdog thread; - conn->watchdog_bark_time = get_absolute_time_in_fp(); + conn->watchdog_bark_time = get_absolute_time_in_ns(); pthread_mutex_init(&conn->watchdog_mutex, NULL); pthread_create(&conn->player_watchdog_thread, NULL, &player_watchdog_thread_code, (void *)conn); @@ -2514,7 +2762,7 @@ static const char *format_address(struct sockaddr *fsa) { void rtsp_listen_loop_cleanup_handler(__attribute__((unused)) void *arg) { int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - debug(1, "rtsp_listen_loop_cleanup_handler called."); + debug(2, "rtsp_listen_loop_cleanup_handler called."); cancel_all_RTSP_threads(); int *sockfd = (int *)arg; mdns_unregister(); @@ -2733,7 +2981,7 @@ void rtsp_listen_loop(void) { } while (1); pthread_cleanup_pop(1); // should never happen } else { - warn("could not establish a service on port %d -- program terminating. Is another instance of " + die("could not establish a service on port %d -- program terminating. Is another instance of " "Shairport Sync running on this device?", config.port); } diff --git a/scripts/shairport-sync-dbus-policy.conf b/scripts/shairport-sync-dbus-policy.conf index 497dd8852..2d42619ce 100644 --- a/scripts/shairport-sync-dbus-policy.conf +++ b/scripts/shairport-sync-dbus-policy.conf @@ -4,11 +4,15 @@ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> - + + + + + diff --git a/scripts/shairport-sync-mpris-policy.conf b/scripts/shairport-sync-mpris-policy.conf index 1efb00720..17ab847ac 100644 --- a/scripts/shairport-sync-mpris-policy.conf +++ b/scripts/shairport-sync-mpris-policy.conf @@ -4,10 +4,13 @@ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> - + + + + diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 9c875156c..efa349965 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -18,11 +18,20 @@ general = // 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. +// interface = "name"; // Use this advanced setting to specify the interface on which Shairport Sync should provide its service. Leave it commented out to get the default, which is to select the interface(s) automatically. // port = 5000; // Listen for service requests on this port // udp_port_base = 6001; // start allocating UDP ports from this port number when needed // udp_port_range = 10; // look for free ports in this number of places, starting at the UDP port base. Allow at least 10, though only three are needed in a steady state. +// regtype = "_raop._tcp"; // Use this advanced setting to set the service type and transport to be advertised by Zeroconf/Bonjour. Default is "_raop._tcp". + // drift_tolerance_in_seconds = 0.002; // allow a timing error of this number of seconds of drift away from exact synchronisation before attempting to correct it // resync_threshold_in_seconds = 0.050; // a synchronisation error greater than this number of seconds will cause resynchronisation; 0 disables it + +// playback_mode = "stereo"; // This can be "stereo", "mono", "reverse stereo", "both left" or "both right". Default is "stereo". +// alac_decoder = "hammerton"; // This can be "hammerton" or "apple". This advanced setting allows you to choose +// the original Shairport decoder by David Hammerton or the Apple Lossless Audio Codec (ALAC) decoder written by Apple. +// If you build Shairport Sync with the flag --with-apple-alac, the Apple ALAC decoder will be chosen by default. + // ignore_volume_control = "no"; // set this to "yes" if you want the volume to be at 100% no matter what the source's volume control is set to. // volume_range_db = 60 ; // use this advanced setting to set the range, in dB, you want between the maximum volume and the minimum volume. Range is 30 to 150 dB. Leave it commented out to use mixer's native range. // volume_max_db = 0.0 ; // use this advanced setting, which must have a decimal point in it, to set the maximum volume, in dB, you wish to use. @@ -36,38 +45,40 @@ general = // The desired AirPlay volume is appended to the end of the command line – leave a space if you want it treated as an extra argument. // AirPlay volume goes from 0 to -30 and -144 means "mute". -// regtype = "_raop._tcp"; // Use this advanced setting to set the service type and transport to be advertised by Zeroconf/Bonjour. Default is "_raop._tcp". -// playback_mode = "stereo"; // This can be "stereo", "mono", "reverse stereo", "both left" or "both right". Default is "stereo". -// alac_decoder = "hammerton"; // This can be "hammerton" or "apple". This advanced setting allows you to choose -// the original Shairport decoder by David Hammerton or the Apple Lossless Audio Codec (ALAC) decoder written by Apple. -// If you build Shairport Sync with the flag --with-apple-alac, the Apple ALAC decoder will be chosen by default. -// interface = "name"; // Use this advanced setting to specify the interface on which Shairport Sync should provide its service. Leave it commented out to get the default, which is to select the interface(s) automatically. - -// audio_backend_latency_offset_in_seconds = 0.0; // Set this offset to compensate for a fixed delay in the audio back end. E.g. if the output device delays by 100 ms, set this to -0.1. -// audio_backend_buffer_desired_length_in_seconds = 0.2; // If set too small, buffer underflow occurs on low-powered machines. Too long and the response time to volume changes becomes annoying. Default is 0.15 seconds in the alsa backend, 0.35 seconds in the pa backend and 1.0 seconds otherwise. +// audio_backend_latency_offset_in_seconds = 0.0; // This is added to the latency requested by the player to delay or advance the output by a fixed amount. +// Use it, for example, to compensate for a fixed delay in the audio back end. +// E.g. if the output device, e.g. a soundbar, takes 100 ms to process audio, set this to -0.1 to deliver the audio +// to the output device 100 ms early, allowing it time to process the audio and output it perfectly in sync. +// audio_backend_buffer_desired_length_in_seconds = 0.2; // If set too small, buffer underflow occurs on low-powered machines. +// Too long and the response time to volume changes becomes annoying. +// Default is 0.2 seconds in the alsa backend, 0.35 seconds in the pa backend and 1.0 seconds otherwise. // audio_backend_buffer_interpolation_threshold_in_seconds = 0.075; // Advanced feature. If the buffer size drops below this, stop using time-consuming interpolation like soxr to avoid dropouts due to underrun. -// audio_backend_silent_lead_in_time = 2.0; // This optional advanced setting, from 0.0 and 4.0 seconds, sets the length of the period of silence that precedes the start of the audio. The default is the latency, usually 2.0 seconds. Values greater than the latency are ignored. Values that are too low will affect initial synchronisation. +// audio_backend_silent_lead_in_time = "auto"; // This optional advanced setting, either "auto" or a positive number, sets the length of the period of silence that precedes the start of the audio. +// The default is "auto" -- the silent lead-in starts as soon as the player starts sending packets. +// Values greater than the latency are ignored. Values that are too low will affect initial synchronisation. + // dbus_service_bus = "system"; // The Shairport Sync dbus interface, if selected at compilation, will appear // as "org.gnome.ShairportSync" on the whichever bus you specify here: "system" (default) or "session". // mpris_service_bus = "system"; // The Shairport Sync mpris interface, if selected at compilation, will appear // as "org.gnome.ShairportSync" on the whichever bus you specify here: "system" (default) or "session". + // resend_control_first_check_time = 0.10; // Use this optional advanced setting to set the wait time in seconds before deciding a packet is missing. // resend_control_check_interval_time = 0.25; // Use this optional advanced setting to set the time in seconds between requests for a missing packet. // resend_control_last_check_time = 0.10; // Use this optional advanced setting to set the latest time, in seconds, by which the last check should be done before the estimated time of a missing packet's transfer to the output buffer. -// +// missing_port_dacp_scan_interval_seconds = 2.0; // Use this optional advanced setting to set the time interval between scans for a DACP port number if no port number has been provided by the player for remote control commands }; // Advanced parameters for controlling how Shairport Sync stays active and how it runs a session sessioncontrol = { -// run_this_before_play_begins = "/full/path/to/application and args"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line -// run_this_after_play_ends = "/full/path/to/application and args"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line - // "active" state starts when play begins and ends when the active_state_timeout has elapsed after play ends, unless another play session starts before the timeout has fully elapsed. // run_this_before_entering_active_state = "/full/path/to/application and args"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line // run_this_after_exiting_active_state = "/full/path/to/application and args"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line // active_state_timeout = 10.0; // wait for this number of seconds after play ends before leaving the active state, unless another play session begins. +// run_this_before_play_begins = "/full/path/to/application and args"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line +// run_this_after_play_ends = "/full/path/to/application and args"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line + // run_this_if_an_unfixable_error_is_detected = "/full/path/to/application and args"; // if a problem occurs that can't be cleared by Shairport Sync itself, hook a program on here to deal with it. An error code-string is passed as the last argument. // Many of these "unfixable" problems are caused by malfunctioning output devices, and sometimes it is necessary to restart the whole device to clear the problem. // You could hook on a program to do this automatically, but beware -- the device may then power off and restart without warning! @@ -84,18 +95,22 @@ sessioncontrol = // --with-alsa alsa = { -// output_device = "default"; // the name of the alsa output device. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc. +// output_device = "default"; // the name of the alsa output device. Use "shairport-sync -h" to discover the names of ALSA hardware devices. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc. // mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software. // mixer_device = "default"; // the mixer_device default is whatever the output_device is. Normally you wouldn't have to use this. + // output_rate = "auto"; // can be "auto", 44100, 88200, 176400 or 352800, but the device must have the capability. // output_format = "auto"; // can be "auto", "U8", "S8", "S16", "S16_LE", "S16_BE", "S24", "S24_LE", "S24_BE", "S24_3LE", "S24_3BE", "S32", "S32_LE" or "S32_BE" but the device must have the capability. Except where stated using (*LE or *BE), endianness matches that of the processor. -// disable_synchronization = "no"; // Set to "yes" to disable synchronization. Default is "no". + +// disable_synchronization = "no"; // Set to "yes" to disable synchronization. Default is "no" This is really meant for troubleshootingG. + // period_size = ; // Use this optional advanced setting to set the alsa period size near to this value // buffer_size = ; // Use this optional advanced setting to set the alsa buffer size near to this value // use_mmap_if_available = "yes"; // Use this optional advanced setting to control whether MMAP-based output is used to communicate with the DAC. Default is "yes" // use_hardware_mute_if_available = "no"; // Use this optional advanced setting to control whether the hardware in the DAC is used for muting. Default is "no", for compatibility with other audio players. // maximum_stall_time = 0.200; // Use this optional advanced setting to control how long to wait for data to be consumed by the output device before considering it an error. It should never approach 200 ms. // use_precision_timing = "auto"; // Use this optional advanced setting to control how Shairport Sync gathers timing information. When set to "auto", if the output device is a real hardware device, precision timing will be used. Choose "no" for more compatible standard timing, choose "yes" to force the use of precision timing, which may cause problems. + // disable_standby_mode = "never"; // This setting prevents the DAC from entering the standby mode. Some DACs make small "popping" noises when they go in and out of standby mode. Settings can be: "always", "auto" or "never". Default is "never", but only for backwards compatibility. The "auto" setting prevents entry to standby mode while Shairport Sync is in the "active" mode. You can use "yes" instead of "always" and "no" instead of "never". // disable_standby_mode_silence_threshold = 0.040; // Use this optional advanced setting to control how little audio should remain in the output buffer before the disable_standby code should start sending silence to the output device. // disable_standby_mode_silence_scan_interval = 0.004; // Use this optional advanced setting to control how often the amount of audio remaining in the output buffer should be checked. @@ -118,6 +133,7 @@ sndio = // --with-pa pa = { +// server = "host"; // Set this to override the default pulseaudio server that should be used. // application_name = "Shairport Sync"; //Set this to the name that should appear in the Sounds "Applications" tab when Shairport Sync is active. }; @@ -229,6 +245,7 @@ mqtt = diagnostics = { // disable_resend_requests = "no"; // set this to yes to stop Shairport Sync from requesting the retransmission of missing packets. Default is "no". +// log_output_to = "syslog"; // set this to "syslog" (default), "stderr" or "stdout" or a file or pipe path to specify were all logs, statistics and diagnostic messages are written to. If there's anything wrong with the file spec, output will be to "stderr". // statistics = "no"; // set to "yes" to print statistics in the log // log_verbosity = 0; // "0" means no debug verbosity, "3" is most verbose. // log_show_file_and_line = "yes"; // set this to yes if you want the file and line number of the message source in the log file diff --git a/shairport-sync-dbus-test-client.c b/shairport-sync-dbus-test-client.c index d0b15bb29..2433fd69e 100644 --- a/shairport-sync-dbus-test-client.c +++ b/shairport-sync-dbus-test-client.c @@ -186,11 +186,10 @@ int main(int argc, char *argv[]) { g_signal_connect(proxy4, "notify::volume", G_CALLBACK(notify_volume_callback), "ShairportSync.AdvancedRemoteControl"); - - g_print("Starting test...\n"); - g_print("Using the RemoteControl interface, play for five seconds, pause for five seconds and then resume play...\n"); + g_print("Using the RemoteControl interface, play for five seconds, pause for five seconds and " + "then resume play...\n"); g_print("Play...\n"); shairport_sync_remote_control_call_play(SHAIRPORT_SYNC_REMOTE_CONTROL(proxy3), NULL, NULL, 0); sleep(5); @@ -200,24 +199,31 @@ int main(int argc, char *argv[]) { g_print("Play...\n"); shairport_sync_remote_control_call_play(SHAIRPORT_SYNC_REMOTE_CONTROL(proxy3), NULL, NULL, 0); sleep(5); - g_print("Using the RemoteControl interface, set AirPlay Volume (range -30 to 0) to -30, -20, -10, 0 and -15 for five seconds each...\n"); + g_print("Using the RemoteControl interface, set AirPlay Volume (range -30 to 0) to -30, -20, " + "-10, 0 and -15 for five seconds each...\n"); g_print("Set AirPlay Volume (range -30 to 0) to -30\n"); - shairport_sync_remote_control_call_set_airplay_volume(SHAIRPORT_SYNC_REMOTE_CONTROL(proxy3), -30, NULL, NULL, 0); + shairport_sync_remote_control_call_set_airplay_volume(SHAIRPORT_SYNC_REMOTE_CONTROL(proxy3), -30, + NULL, NULL, 0); sleep(5); g_print("Set AirPlay Volume (range -30 to 0) to -20\n"); - shairport_sync_remote_control_call_set_airplay_volume(SHAIRPORT_SYNC_REMOTE_CONTROL(proxy3), -20, NULL, NULL, 0); + shairport_sync_remote_control_call_set_airplay_volume(SHAIRPORT_SYNC_REMOTE_CONTROL(proxy3), -20, + NULL, NULL, 0); sleep(5); g_print("Set AirPlay Volume (range -30 to 0) to -10\n"); - shairport_sync_remote_control_call_set_airplay_volume(SHAIRPORT_SYNC_REMOTE_CONTROL(proxy3), -10, NULL, NULL, 0); + shairport_sync_remote_control_call_set_airplay_volume(SHAIRPORT_SYNC_REMOTE_CONTROL(proxy3), -10, + NULL, NULL, 0); sleep(5); g_print("Set AirPlay Volume (range -30 to 0) to -0\n"); - shairport_sync_remote_control_call_set_airplay_volume(SHAIRPORT_SYNC_REMOTE_CONTROL(proxy3), 0, NULL, NULL, 0); + shairport_sync_remote_control_call_set_airplay_volume(SHAIRPORT_SYNC_REMOTE_CONTROL(proxy3), 0, + NULL, NULL, 0); sleep(5); g_print("Set AirPlay Volume (range -30 to 0) to -15\n"); - shairport_sync_remote_control_call_set_airplay_volume(SHAIRPORT_SYNC_REMOTE_CONTROL(proxy3), -15, NULL, NULL, 0); + shairport_sync_remote_control_call_set_airplay_volume(SHAIRPORT_SYNC_REMOTE_CONTROL(proxy3), -15, + NULL, NULL, 0); sleep(5); - g_print("Using the AdvancedRemoteControl interface, set Volume to 20%%, 100%%, 40%% and 60%% for five seconds each...\n"); + g_print("Using the AdvancedRemoteControl interface, set Volume to 20%%, 100%%, 40%% and 60%% for " + "five seconds each...\n"); g_print("Set Volume to 20%%\n"); shairport_sync_advanced_remote_control_call_set_volume( SHAIRPORT_SYNC_ADVANCED_REMOTE_CONTROL(proxy4), 20, NULL, NULL, 0); diff --git a/shairport.c b/shairport.c index 5e375916c..14e507257 100644 --- a/shairport.c +++ b/shairport.c @@ -143,10 +143,11 @@ void *soxr_time_check(__attribute__((unused)) void *arg) { int i; int number_of_iterations = 0; - uint64_t soxr_start_time = get_absolute_time_in_fp(); + uint64_t soxr_start_time = get_absolute_time_in_ns(); 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) { + (uint64_t)1500000000 + 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_ns() < loop_until_time) { number_of_iterations++; for (i = 0; i < buffer_length; i++) { @@ -184,11 +185,10 @@ void *soxr_time_check(__attribute__((unused)) void *arg) { NULL, NULL); // Default configuration. } - double soxr_execution_time_us = - (((get_absolute_time_in_fp() - soxr_start_time) * 1000000) >> 32) * 1.0; + double soxr_execution_time_ns = (get_absolute_time_in_ns() - soxr_start_time) * 1.0; // free(outbuffer); // free(inbuffer); - config.soxr_delay_index = (int)(0.9 + soxr_execution_time_us / (number_of_iterations * 1000)); + config.soxr_delay_index = (int)(0.9 + soxr_execution_time_ns / (number_of_iterations * 1000000)); debug(2, "soxr_delay_index: %d.", config.soxr_delay_index); if ((config.packet_stuffing == ST_soxr) && (config.soxr_delay_index > config.soxr_delay_threshold)) @@ -268,7 +268,7 @@ void usage(char *progname) { 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(" -u, --use-stderr log messages through STDERR rather than the system log.\n"); printf("\n"); mdns_ls_backends(); printf("\n"); @@ -388,7 +388,8 @@ int parse_options(int argc, char **argv) { 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.audio_backend_silent_lead_in_time_auto = + 1; // start outputting silence as soon as packets start arriving 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. @@ -399,12 +400,23 @@ int parse_options(int argc, char **argv) { // 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 + // 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 + config.missing_port_dacp_scan_interval_seconds = + 2.0; // check at this interval if no DACP port number is known + + config.minimum_free_buffer_headroom = 125; // leave approximately one second's worth of buffers + // free after calculating the effective latency. + // e.g. if we have 1024 buffers or 352 frames = 8.17 seconds and we have a nominal latency of 2.0 + // seconds then we can add an offset of 5.17 seconds and still leave a second's worth of buffers + // for unexpected circumstances #ifdef CONFIG_METADATA_HUB config.cover_art_cache_dir = "/tmp/shairport-sync/.cache/coverart"; @@ -414,9 +426,10 @@ int parse_options(int argc, char **argv) { 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) + // 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; @@ -430,9 +443,9 @@ int parse_options(int argc, char **argv) { 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); + 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); + 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); @@ -649,6 +662,20 @@ int parse_options(int argc, char **argv) { dvalue); } + /* Get the diagnostics output default. */ + if (config_lookup_string(config.cfg, "diagnostics.log_output_to", &str)) { + if (strcasecmp(str, "syslog") == 0) + log_to_syslog(); + else if (strcasecmp(str, "stdout") == 0) { + log_to_stdout(); + } else if (strcasecmp(str, "stderr") == 0) { + log_to_stderr(); + } else { + config.log_file_path = (char *)str; + config.log_fd = -1; + log_to_file(); + } + } /* Get the ignore_volume_control setting. */ if (config_lookup_string(config.cfg, "general.ignore_volume_control", &str)) { if (strcasecmp(str, "no") == 0) @@ -746,9 +773,9 @@ int parse_options(int argc, char **argv) { * 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); + die("Invalid volume range %d dB. It should be between 30 and 150 dB. Zero means use " + "the mixer's native range. The setting reamins at %d.", + value, config.volume_range_db); else config.volume_range_db = value; } @@ -767,42 +794,50 @@ int parse_options(int argc, char **argv) { 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 (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); + warn("Invalid general resend_control_first_check_time setting \"%f\". 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 (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); + warn("Invalid general resend_control_check_interval_time setting \"%f\". 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 (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); + warn("Invalid general resend_control_last_check_time setting \"%f\". It should " + "be " + "between 0.0 and 3.0, " + "inclusive. The setting remains at %f seconds.", + dvalue, config.resend_control_last_check_time); + } + + if (config_lookup_float(config.cfg, "general.missing_port_dacp_scan_interval_seconds", + &dvalue)) { + if ((dvalue >= 0.0) && (dvalue <= 300.0)) + config.missing_port_dacp_scan_interval_seconds = dvalue; + else + warn("Invalid general missing_port_dacp_scan_interval_seconds setting \"%f\". It should " + "be " + "between 0.0 and 300.0, " + "inclusive. The setting remains at %f seconds.", + dvalue, config.missing_port_dacp_scan_interval_seconds); } /* Get the default latency. Deprecated! */ @@ -963,7 +998,8 @@ int parse_options(int argc, char **argv) { 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); + config.convolver_valid = + convolver_init(config.convolution_ir_file, config.convolution_max_length); } if (config.convolution && config.convolution_ir_file == NULL) { @@ -1159,7 +1195,7 @@ int parse_options(int argc, char **argv) { poptFreeContext(optCon); -// here, we are finally finished reading the options + // here, we are finally finished reading the options #ifdef CONFIG_LIBDAEMON if ((daemonisewith) && (daemonisewithout)) @@ -1173,8 +1209,9 @@ int parse_options(int argc, char **argv) { #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", + 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); } @@ -1197,8 +1234,6 @@ int parse_options(int argc, char **argv) { 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 @@ -1269,99 +1304,115 @@ const char *pid_file_proc(void) { } #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 + if (emergency_exit == 0) { + // 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 ((this_is_the_daemon_process) || (config.daemonise == 0)) { // if this is the daemon process that is exiting or it's not actually deamonised at all +#endif + debug(2, "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 -*/ + /* + Actually, there is no stop_mpris_service() function. + #ifdef CONFIG_MPRIS_INTERFACE + stop_mpris_service(); + #endif + */ #ifdef CONFIG_DBUS_INTERFACE - stop_dbus_service(); + 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); - } + 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(); + debug(2, "Stopping DACP Monitor"); + dacp_monitor_stop(); #endif #ifdef CONFIG_METADATA_HUB - debug(2, "Stopping metadata hub"); - metadata_hub_stop(); + debug(2, "Stopping metadata hub"); + metadata_hub_stop(); #endif #ifdef CONFIG_METADATA - metadata_stop(); // close down the metadata pipe + metadata_stop(); // close down the metadata pipe #endif - activity_monitor_stop(0); + activity_monitor_stop(0); - if ((config.output) && (config.output->deinit)) { - debug(2, "Deinitialise the audio backend."); - config.output->deinit(); - } + 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); + // 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 (conns) - free(conns); // make sure the connections have been deleted first - - if (config.service_name) - free(config.service_name); + if (config.service_name) + free(config.service_name); #ifdef CONFIG_CONVOLUTION - if (config.convolution_ir_file) - free(config.convolution_ir_file); + if (config.convolution_ir_file) + free(config.convolution_ir_file); #endif - if (config.regtype) - free(config.regtype); + 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); - } + if (this_is_the_daemon_process) { + 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... + 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... + + +#ifdef CONFIG_LIBDAEMON + if (this_is_the_daemon_process) { // this is the daemon that is exiting + debug(1,"libdaemon daemon exit"); + } else { + if (config.daemonise) + debug(1,"libdaemon parent exit"); + else + debug(1,"exit"); + } +#else + debug(1,"exit"); +#endif + } else { + debug(1,"emergency exit"); + } } // for removing zombie script processes @@ -1370,10 +1421,17 @@ Actually, there is no stop_mpris_service() function. void handle_sigchld(__attribute__((unused)) int sig) { int saved_errno = errno; - while (waitpid((pid_t)(-1), 0, WNOHANG) > 0) {} + while (waitpid((pid_t)(-1), 0, WNOHANG) > 0) { + } errno = saved_errno; } +void main_thread_cleanup_handler(__attribute__((unused)) void *arg) { + debug(2,"main thread cleanup handler called"); + exit(EXIT_SUCCESS); +} + + 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))) { @@ -1390,11 +1448,12 @@ int main(int argc, char **argv) { #ifdef CONFIG_LIBDAEMON pid = getpid(); #endif + config.log_fd = -1; 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; + ns_time_at_startup = get_absolute_time_in_ns(); + ns_time_at_last_debug_message = ns_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); @@ -1410,6 +1469,7 @@ int main(int argc, char **argv) { setlogmask(LOG_UPTO(LOG_DEBUG)); openlog(NULL, 0, LOG_DAEMON); #endif + emergency_exit = 0; // what to do or skip in the exit_function atexit(exit_function); // set defaults @@ -1449,7 +1509,7 @@ int main(int argc, char **argv) { // 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 + 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 @@ -1472,7 +1532,7 @@ int main(int argc, char **argv) { // 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.audio_backend_buffer_desired_length = 0.15; // seconds config.udp_port_base = 6001; config.udp_port_range = 10; config.output_format = SPS_FORMAT_S16_LE; // default @@ -1527,10 +1587,17 @@ int main(int argc, char **argv) { /* 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."); + if ((ret = daemon_pid_file_kill_wait(SIGTERM, 5)) < 0) { + if (errno == ENOENT) + daemon_log(LOG_WARNING, "Failed to kill %s daemon: PID file not found.", config.appName); + else + daemon_log(LOG_WARNING, "Failed to kill %s daemon: \"%s\", errno %u.", config.appName, strerror(errno), errno); + } else { + // debug(1,"Successfully killed the %s daemon.", config.appName); + if (daemon_pid_file_remove() == 0) + debug(2, "killed the %s daemon.", config.appName); + else + daemon_log(LOG_WARNING, "killed the %s deamon, but cannot remove old PID file: \"%s\", errno %u.", config.appName, strerror(errno), errno); } return ret < 0 ? 1 : 0; #else @@ -1543,7 +1610,7 @@ int main(int argc, char **argv) { #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); + daemon_log(LOG_ERR, "The %s daemon is already running as PID %u", config.appName, pid); return 1; } @@ -1578,16 +1645,16 @@ int main(int argc, char **argv) { break; case 1: daemon_log(LOG_ERR, - "daemon failed to launch: could not close open file descriptors after forking."); + "the %s daemon failed to launch: could not close open file descriptors after forking.", config.appName); break; case 2: - daemon_log(LOG_ERR, "daemon failed to launch: could not create PID file."); + daemon_log(LOG_ERR, "the %s daemon failed to launch: could not create PID file.", config.appName); break; case 3: - daemon_log(LOG_ERR, "daemon failed to launch: could not create or access PID directory."); + daemon_log(LOG_ERR, "the %s daemon failed to launch: could not create or access PID directory.", config.appName); break; default: - daemon_log(LOG_ERR, "daemon failed to launch, error %i.", ret); + daemon_log(LOG_ERR, "the %s daemon failed to launch, error %i.", config.appName, ret); } return ret; } else { /* pid == 0 means we are the daemon */ @@ -1635,6 +1702,9 @@ int main(int argc, char **argv) { #endif debug(1, "Started!"); + // stop a pipe signal from killing the program + signal(SIGPIPE, SIG_IGN); + // install a zombie process reaper // see: http://www.microhowto.info/howto/reap_zombie_processes_using_a_sigchld_handler.html struct sigaction sa; @@ -1672,7 +1742,7 @@ int main(int argc, char **argv) { } config.output->init(argc - audio_arg, argv + audio_arg); - // pthread_cleanup_push(main_cleanup_handler, NULL); + pthread_cleanup_push(main_thread_cleanup_handler, NULL); // daemon_log(LOG_NOTICE, "startup"); @@ -1712,8 +1782,9 @@ int main(int argc, char **argv) { /* 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.", + 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 @@ -1736,8 +1807,8 @@ int main(int argc, char **argv) { 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"); + 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); @@ -1751,8 +1822,9 @@ int main(int argc, char **argv) { 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.", + 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); @@ -1771,8 +1843,11 @@ int main(int argc, char **argv) { 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); + if (config.audio_backend_silent_lead_in_time_auto == 1) + debug(1, "audio backend silence lead-in time is \"auto\"."); + else + debug(1, "audio backend silence lead-in time is %f seconds.", + 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); @@ -1884,5 +1959,6 @@ int main(int argc, char **argv) { activity_monitor_start(); rtsp_listen_loop(); + pthread_cleanup_pop(1); return 0; } diff --git a/tinysvcmdns.c b/tinysvcmdns.c index 069a6436c..9e605e078 100644 --- a/tinysvcmdns.c +++ b/tinysvcmdns.c @@ -1466,7 +1466,7 @@ int close_pipe(int s) { // main loop to receive, process and send out MDNS replies // also handles MDNS service announces -void* main_loop(struct mdnsd *svr) { +void *main_loop(struct mdnsd *svr) { fd_set sockfd_set; int max_fd = svr->sockfd; char notify_buf[2]; // buffer for reading of notify_pipe @@ -1724,7 +1724,7 @@ struct mdnsd *mdnsd_start() { pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); - if (pthread_create(&tid, &attr, (void * (*)(void *))&main_loop, (void *)server) != 0) { + if (pthread_create(&tid, &attr, (void *(*)(void *)) & main_loop, (void *)server) != 0) { pthread_mutex_destroy(&server->data_lock); free(server); return NULL; @@ -1737,7 +1737,8 @@ void mdnsd_stop(struct mdnsd *s) { assert(s != NULL); struct timeval tv = { - .tv_sec = 0, .tv_usec = 500 * 1000, + .tv_sec = 0, + .tv_usec = 500 * 1000, }; s->stop_flag = 1;