summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Brady <mikebrady@eircom.net>2019-05-24 16:42:23 +0100
committerMike Brady <mikebrady@eircom.net>2019-05-24 16:42:23 +0100
commit13fcd259c3b0eb750cb9eb0d44315242750eecaa (patch)
tree0085075c3126758f5ffee063790e872d3d2ab360
parent9adf8be3561d4e49c5ad1e9e0f34417db7550b44 (diff)
parent57f0b710f86d5f3f5a3b596a00b1440c39b82cb2 (diff)
Merge branch 'development' 3.3rc7+ to 'master'
-rw-r--r--.gitignore1
-rw-r--r--CAR INSTALL.md19
-rw-r--r--CONTRIBUTING.md3
-rw-r--r--CYGWIN.md99
-rw-r--r--FEDORA.md14
-rw-r--r--FFTConvolver/convolver.cpp7
-rw-r--r--FREEBSD.md6
-rw-r--r--INSTALL.md29
-rw-r--r--ISSUES.md15
-rw-r--r--LIBSOXR.md12
-rw-r--r--Makefile.am85
-rw-r--r--OPENBSD.md4
-rw-r--r--README.md19
-rw-r--r--RELEASENOTES.md21
-rw-r--r--TROUBLESHOOTING.md60
-rw-r--r--UPDATING.md2
-rw-r--r--activity_monitor.c275
-rw-r--r--activity_monitor.h8
-rw-r--r--audio.c36
-rw-r--r--audio.h13
-rw-r--r--audio_alsa.c1996
-rw-r--r--audio_ao.c4
-rw-r--r--audio_dummy.c9
-rw-r--r--audio_jack.c350
-rw-r--r--audio_pa.c33
-rw-r--r--audio_pipe.c20
-rw-r--r--audio_sndio.c37
-rw-r--r--audio_soundio.c10
-rw-r--r--audio_stdout.c9
-rw-r--r--common.c518
-rw-r--r--common.h161
-rw-r--r--configure.ac213
-rw-r--r--dacp.c226
-rw-r--r--dacp.h6
-rw-r--r--dbus-service.c198
-rw-r--r--dbus-service.h2
-rw-r--r--documents/sample dbus commands7
-rw-r--r--man/shairport-sync.7165
-rw-r--r--man/shairport-sync.7.xml846
-rw-r--r--man/shairport-sync.html849
-rw-r--r--mdns.c53
-rw-r--r--mdns.h10
-rw-r--r--mdns_avahi.c223
-rw-r--r--mdns_dns_sd.c7
-rw-r--r--mdns_external.c83
-rw-r--r--mdns_tinysvcmdns.c5
-rw-r--r--metadata_hub.c121
-rw-r--r--metadata_hub.h9
-rw-r--r--mpris-service.c17
-rw-r--r--mqtt.c232
-rw-r--r--mqtt.h14
-rw-r--r--org.gnome.ShairportSync.xml5
-rw-r--r--player.c2445
-rw-r--r--player.h132
-rw-r--r--rtp.c656
-rw-r--r--rtp.h11
-rw-r--r--rtsp.c1360
-rw-r--r--rtsp.h6
-rwxr-xr-xscripts/shairport-sync-config169
-rw-r--r--scripts/shairport-sync-dbus-policy-cygwin.conf18
-rw-r--r--scripts/shairport-sync-mpris-policy-cygwin.conf18
-rw-r--r--scripts/shairport-sync.conf104
-rw-r--r--scripts/shairport-sync.freebsd2
-rwxr-xr-xscripts/shairport-sync.in41
-rw-r--r--shairport-sync-dbus-test-client.c2
-rw-r--r--shairport-sync-mpris-test-client.c2
-rw-r--r--shairport-sync.spec5
-rw-r--r--shairport.c769
-rw-r--r--tinysvcmdns.c41
69 files changed, 9051 insertions, 3896 deletions
diff --git a/.gitignore b/.gitignore
index 09c6aa6..6f76aec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,6 +32,7 @@ shairport-sync.core
/org.gnome.ShairportSync.service
/dbus-interface.*
/mpris-interface.*
+/lib_*.*
# Some eclipse project files
.cproject
diff --git a/CAR INSTALL.md b/CAR INSTALL.md
index 671695c..879015a 100644
--- a/CAR INSTALL.md
+++ b/CAR INSTALL.md
@@ -40,26 +40,26 @@ Close the file and carefully dismount and eject the two drives. Remove the SD ca
### Boot, Configure, Update
The first thing to do on a Pi would be to use the `raspi-config` tool to expand the file system to use the entire card. It might be useful to change the `hostname` too. Next, do the usual update and upgrade:
```
-# apt update
-# apt upgrade
+# apt-get update
+# apt-get upgrade
+# rpi-update
```
-**Note:** If you are following this guide but are using a full-size Rapsberry Pi with the built-in DAC, then it is a good idea to update to the Raspian release of October 2018 or later, as a number of changes have been made in the firmware that improve the built-in DAC.
### Shairport Sync
First, install the packages needed by Shairport Sync:
```
-# apt install build-essential git xmltoman autoconf automake libtool libdaemon-dev libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev
+# apt-get install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev
```
Next, download Shairport Sync, configure it, compile and install it:
```
$ git clone https://github.com/mikebrady/shairport-sync.git
$ cd shairport-sync
$ autoreconf -fi
-$ ./configure --sysconfdir=/etc --with-alsa --with-avahi --with-ssl=openssl --with-systemd
+$ ./configure --sysconfdir=/etc --with-alsa --with-avahi --with-ssl=openssl --with-soxr --with-systemd
$ make
$ sudo make install
```
-SoX interpolaton is not included, as the Pi Zero would not be fast enough. *Do not* enable Shairport Sync to automatically start at boot time -- startup is organised differently.
+*Do not* enable Shairport Sync to automatically start at boot time -- startup is organised differently.
Third, finish by configuring Shairport Sync.
Here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`:
@@ -75,19 +75,20 @@ general =
alsa =
{
output_device = "hw:1"; // the name of the alsa output device. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc.
- output_format = "S32"; // can be "U8", "S8", "S16", "S24" or "S32", with be LE or BE depending on the processor, but the device must be capable of it
};
```
Two `general` settings are worth noting. First, the option to ignore the sending device's volume control is enabled -- this means that the car audio's volume control is the only one that affects the audio volume. Of course this is a matter of personal preference.
Second, the maximum output offered by the DAC to the AUX port of the car audio can be reduced if it is overloading the input circuits. Again, that's a matter for personal selection and adjustment.
-The `alsa` settings are specific to the Pimoroni PHAT -- it does not have a hardware mixer and it does have a 32-bit capability which is worth enabling.
+The `alsa` settings are for the Pimoroni PHAT -- it does not have a hardware mixer, so no `mixer_control_name` is given.
+
+Note that the DAC's 32-bit capability is automatically selected if available, so there is no need to set it here. Similarly, since `soxr` support is included in the build, `soxr` interpolation will be automatically enabled if the device is fast enough.
### Extra Packages
A number of packages to enable the Pi to work as a WiFi base station are needed:
```
-# apt install hostapd isc-dhcp-server
+# apt-get install hostapd isc-dhcp-server
```
Disable both of these services from starting at boot time (this is because we will launch them sequentially later on):
```
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9ae6b8e..ab4d544 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,8 +10,7 @@ Changes and additions in the development branch make their way eventually to the
Issue Reports
----
-Issue reports are welcome, but before you report an issue, please check that your version of Shairport Sync is up to date. You'll find details of releases at [Shairport Sync Releases](https://github.com/mikebrady/shairport-sync/releases).
-Then, please have a look though the existing [issues](https://github.com/mikebrady/shairport-sync/issues), both open and closed, and check for hints in the [TROUBLESHOOTING](TROUBLESHOOTING.md) page. It would be great to give some details of the device and version of Linux or FreeBSD in use along with version and build configuration of Shairport Sync you are using (use `$ shairport-sync -V` to get this). Then, if possible, some diagnostic information from the log or logfile would be useful.
+Issue reports are welcome, but before you report an issue, please have a look though the existing [issues](https://github.com/mikebrady/shairport-sync/issues), both open and closed, and check for hints in the [TROUBLESHOOTING](TROUBLESHOOTING.md) page. It would be great to give some details of the device and version of Linux or FreeBSD in use along with the version of Shairport Sync you are using (use `$ shairport-sync -V` to get this). Then, if possible, some diagnostic information from the log or logfile would be useful.
In general, a log verbosity of 2 is adequate (`-vv`, or the relevant entry in the configuration file), and it's usually helpful if statistics have been enabled (`--statistics` on the command line, or the relevant entry in the configuration file).
diff --git a/CYGWIN.md b/CYGWIN.md
index c128388..80cb351 100644
--- a/CYGWIN.md
+++ b/CYGWIN.md
@@ -1,34 +1,52 @@
-Installing to Cygwin
+Installing Shairport Sync into Cygwin
+====
+
+This guide is based on installing onto a fresh installation of Cygwin 2.895 (64-bit installation) running in Windows 10
+inside VMWare Fusion on a Mac.
+
+The end result is a new Windows Service called `CYGWIN Shairport Sync`, providing an AirPlay service by which iOS devices or other AirPlay sources on the network can play audio through the Windows device.
+
+Windows Firewall
+----
+While getting everything working, it is suggested that you temporarily disable the Windows Firewall. Shairport Sync uses port 5000 for TCP and uses three ports for UDP, so you should leave a minimum of three, and preferably at least 10, open from 6001 upwards. The Bonjour Service, used in conjunction with the Avahi daemon, advertises Shairport Sync over a number of further ports. Once everything is working, the firewall can be re-enabled gradually.
+
+Setting up Windows
+----
+Set up Windows 10 and install all updates. Install the `Bonjour Service`, available from Apple in an installer called "Bonjour Print Services for Windows v2.0.2".
+
+* Download and run `Bonjour Print Services for Windows v2.0.2`
+* After accepting conditions and clicking the `Install` button, the installer will do a preliminary installation, installing just the Bonjour Service. It will then pause, inviting you to install Bonjour Print Services. You can decline this, as the Bonjour Service will have been installed during the first part of the installation.
+
+* Check Bonjour Service is running. In Windows, open the `Services` desktop application and ensure that you can see `Bonjour Service` running.
+
+Setting up Cygwin
----
+* Download the Cygwin installer from the [official website](https://cygwin.com/install.html). Save the installer in the Downloads folder.
-This is based on installing onto a fresh default installation of Cygwin 2.4.1 (64-bit installation) running in Windows 10
-inside VMWare Fusion on a Mac.
-
-Note: to enable Cygwin to resolve domain names, it was found necessary to go to Windows `Control Panel` > `Network and Internet` > `Network Connections` >
-`Ethernet0` > `Properties` > `Internet Protocol Version 4 (TCP/IPv4)` > `Properties`, select `Use the following DNS server addresses`
-and enter valid DNS server addresses, e.g. `8.8.8.8`.
-
-* Use `Cygwin Setup` to install the following packages:
- * `pkg-config`
- * `autoconf`
- * `automake`
- * `clang`
- * `libdaemon-devel`
- * `popt-devel`
- * `make`
- * `libao-devel`
- * `openssl-devel`
- * `libtool`
- * `git`
- * `wget` for convenience,
- * `flex` for compiling `libconfig`
- * `bison` for compiling `libconfig`
-
-Assuming the Cygwin setup program (`setup-x86_64.exe` in this case) is in your Downloads directory and the default directory is your home directory, the following command should work:
-```
-.\Downloads\setup-x86_64.exe -P pkg-config,autoconf,automake,clang,libdaemon-devel,popt-devel,make,libao-devel,openssl-devel,libtool,git,wget,flex,bison
+* Open a Windows `Command Prompt` window and enter the following multi-line command, omitting the `C:\Users\mike>` prompt:
```
+C:\Users\mike> Downloads\setup-x86_64.exe -P cygrunsrv,dbus,avahi,avahi-tools,gnome-keyring,libavahi-client-devel,^
+libglib2.0-devel,openssl,pkg-config,autoconf,automake,clang,libdaemon-devel,popt-devel,^
+make,libao-devel,openssl-devel,libtool,git,wget,flex,bison
+```
+This will do a complete installation of Cygwin and all necessary packages.
+* Set up the D-Bus and Avahi Services:
+Open a `Cygwin64 Terminal` window in Administrator mode. Enter the following command:
+```
+$ messagebus-config
+```
+Answer `yes` to all queries. Open the Windows `Services` desktop application (if it's already open, refresh the screen contents: `Actions > Refresh`) and look for the `CYGWIN D-Bus system service`. Open it and start it.
+
+Next, open (or return to) a `Cygwin64 Terminal` window in Administrator mode. Enter the following command:
+```
+$ /usr/sbin/avahi-daemon-config
+```
+Answer `yes` to all queries. Open the Windows `Services` desktop application (if it's already open, refresh the screen contents: `Actions > Refresh`) and look for the `CYGWIN Avahi service`. Open it and start it.
+
+The `libconfig` Library
+----
+Shairport Sync relies on a library – `libconfig` – that is not a Cygwin package, so it must be downloaded, compiled and installed:
* Download, configure, compile and install `libconfig`:
```
$ git clone https://github.com/hyperrealm/libconfig.git
@@ -39,13 +57,34 @@ $ make
$ make install
$ cd ..
```
-* Next, download, configure and compile Shairport Sync:
+
+Shairport Sync
+----
+* Download, configure and compile Shairport Sync:
```
$ git clone https://github.com/mikebrady/shairport-sync.git
$ cd shairport-sync
+$ git checkout development // this is temporary
$ autoreconf -fi
-$ PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./configure --with-ao --with-ssl=openssl --with-tinysvcmdns
+$ PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./configure --with-ao --with-ssl=openssl \
+ --with-avahi --with-dbus-interface --with-libdaemon --sysconfdir=/etc --with-cygwin-service
$ make
$ make install
```
-* That's it. There should be a `shairport-sync.exe` file in your directory.
+* The last step above installs the `shairport-sync` application into `/usr/local/bin` and also installs a configuration file, a service configuration script and two D-Bus policy files.
+
+Shairport Sync Service
+----
+* To install Shairport Sync as a Cygwin Service, open (or return to) a `Cygwin64 Terminal` window in Administrator mode. Enter the following command:
+```
+$ shairport-sync-config
+```
+Answer `yes` to all queries. Open the Windows `Services` desktop application (if it's already open, refresh the screen contents: `Actions > Refresh`) and look for the `CYGWIN Shairport Sync` service. Open it and start it.
+
+An AirPlay player on the local network should now be able to see an AirPlay output device bearing the computer's Device Name, e.g. `DESKTOP-0RHGN0`. You can set a different name by changing the settings in the Shairport Sync configuration file, installed at `/etc/shairport-sync.conf`.
+
+Since Shairport Sync is now a Cygwin Service, you do not need to open Cygwin to launch it – it should launch automatically when Windows is booted up.
+
+Known Issues
+----
+* Shairport Sync cannot access the D-Bus system bus to make its D-Bus interface available. The cause of this problem is unknown. (While the Avahi daemon can access the D-Bus system bus, Shairport Sync can not. The two applications use different D-Bus libraries, so perhaps the issue lies there.)
diff --git a/FEDORA.md b/FEDORA.md
index ae581cb..bb57d58 100644
--- a/FEDORA.md
+++ b/FEDORA.md
@@ -3,22 +3,22 @@ Fedora Installation Guide
Install the toolchain and pre-requisites, if necessary:
```
-% sudo yum install make automake gcc gcc-c++ kernel-devel
-% sudo yum install alsa-lib-devel autoconf automake avahi-devel libconfig-devel libdaemon-devel openssl-devel popt-devel soxr-devel
+# yum install make automake gcc gcc-c++ kernel-devel
+# yum install alsa-lib-devel autoconf automake avahi-devel libconfig-devel libdaemon-devel openssl-devel popt-devel soxr-devel
```
Download the tarball from the "releases" tab on github or use `wget` and then use `rpmbuild`. This example is for version 2.6:
```
-% wget -O shairport-sync-2.6.tar.gz https://github.com/mikebrady/shairport-sync/archive/2.6.tar.gz
-% rpmbuild -ta shairport-sync-2.6.tar.gz
+$ wget -O shairport-sync-2.6.tar.gz https://github.com/mikebrady/shairport-sync/archive/2.6.tar.gz
+$ rpmbuild -ta shairport-sync-2.6.tar.gz
```
The `-ta` means "build all from this tarball".
The RPM will be built in a directory and will have a pathname like, for example, `~/rpmbuild/RPMS/i686/shairport-sync-2.6-1.fc22.i686.rpm` You should then install it with (for this example):
```
-%sudo rpm -i ~/rpmbuild/RPMS/i686/shairport-sync-2.6-1.fc22.i686.rpm
+# rpm -i ~/rpmbuild/RPMS/i686/shairport-sync-2.6-1.fc22.i686.rpm
```
You may have to manually create the directory `/var/shairport-sync` for the installation to succeed. Having edited the configuration file `/etc/shairport-sync.conf` as appropriate (see ("Configuring Shairport Sync")[https://github.com/mikebrady/shairport-sync/blob/master/README.md#configuring-shairport-sync]), enable and start the service with:
```
-%sudo systemctl enable shairport-sync.service
-%sudo systemctl start shairport-sync.service
+# systemctl enable shairport-sync.service
+# systemctl start shairport-sync.service
```
diff --git a/FFTConvolver/convolver.cpp b/FFTConvolver/convolver.cpp
index 4589c8e..f115cf8 100644
--- a/FFTConvolver/convolver.cpp
+++ b/FFTConvolver/convolver.cpp
@@ -4,10 +4,9 @@
#include <sndfile.h>
#include "FFTConvolver.h"
#include "Utilities.h"
-extern "C" {
-#include "../common.h"
-}
+extern "C" void die(const char *format, ...);
+extern "C" void debug(int level, const char *format, ...);
static fftconvolver::FFTConvolver convolver_l;
static fftconvolver::FFTConvolver convolver_r;
@@ -40,7 +39,7 @@ void convolver_init(const char* filename, int max_length)
float buffer_l[size];
float buffer_r[size];
- int i;
+ unsigned int i;
for (i=0; i<size; ++i)
{
buffer_l[i] = buffer[2*i+0];
diff --git a/FREEBSD.md b/FREEBSD.md
index 8242980..63e5768 100644
--- a/FREEBSD.md
+++ b/FREEBSD.md
@@ -49,7 +49,7 @@ Install the packages that are needed for Shairport Sync to be downloaded and bui
```
# pkg install git autotools pkgconf popt libconfig openssl sndio alsa-utils
```
-Omit `alsa-utils` if you're not using ALSA. Likewise, omit `sndio` if you don't intend to use the `sndio` subsystem.
+Add `alsa-utils` if you're wish to use ALSA. Omit `sndio` if you don't intend to use the `sndio` subsystem.
Now, download Shairport Sync from GitHub:
```
@@ -60,10 +60,10 @@ Next, configure the build and compile it:
```
$ autoreconf -i -f
-$ ./configure --with-avahi --with-ssl=openssl --with-alsa --with-sndio --with-os=freebsd --with-freebsd-service
+./configure --with-avahi --with-ssl=openssl --with-sndio --with-libdaemon --with-os=freebsd --with-freebsd-service
$ make
```
-Omit `--with-alsa` if you don't want 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.
+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.
Installation
----
diff --git a/INSTALL.md b/INSTALL.md
index 15a1c64..33058c8 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -7,11 +7,10 @@ In the commands below, note the convention that a `#` prompt means you are in su
### Configure and Update
Do the usual update and upgrade:
```
-# apt update
-# apt upgrade
+# apt-get update
+# apt-get upgrade
+# rpi-update
```
-**Note:** The upgrade step above will automatically update to the most recent stable Rapsberry Pi firmware.
-
(Separately, if you haven't done so already, consider using the `raspi-config` tool to expand the file system to use the entire card.)
### Turn Off WiFi Power Management
@@ -19,14 +18,14 @@ If you are using WiFi, you should turn off WiFi Power Management:
```
# iwconfig wlan0 power off
```
-WiFi Power Management will put the WiFi system in low-power mode when the WiFi system is considered inactive, and in this mode it may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. (See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details.)
+WiFi Power Management will put the WiFi system in low-power mode when the WiFi system considered inactive, and in this mode it may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. (See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details.)
Reboot the Pi.
-### Remove Old Copies and Old Startup Scripts
-Before you begin building Shairport Sync, it's best to search for and remove any existing copies of the application, called `shairport-sync`. Use the command `$ which -a shairport-sync` to find them. For example, if `shairport-sync` has been installed previously, this might happen:
+### Remove Old Copies
+Before you begin building Shairport Sync, it's best to search for and remove any existing copies of the application, called `shairport-sync`. Use the command `$ which shairport-sync` to find them. For example, if `shairport-sync` has been installed previously, this might happen:
```
-$ which -a shairport-sync
+$ which shairport-sync
/usr/local/bin/shairport-sync
```
Remove it as follows:
@@ -35,22 +34,20 @@ Remove it as follows:
```
Do this until no more copies of `shairport-sync` are found.
-You should also remove the initialisation script files `/etc/systemd/system/shairport-sync.service` and `/etc/init.d/shairport-sync` if they exist – new ones will be installed if necessary.
-
### Build and Install
Okay, now let's get the tools and sources for building and installing Shairport Sync.
First, install the packages needed by Shairport Sync:
```
-# apt install build-essential git xmltoman autoconf automake libtool libdaemon-dev \
- libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev
+# apt-get install build-essential git xmltoman autoconf automake libtool \
+ libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev
```
Next, download Shairport Sync, configure it, compile and install it:
```
$ git clone https://github.com/mikebrady/shairport-sync.git
$ cd shairport-sync
$ autoreconf -fi
-$ ./configure --sysconfdir=/etc --with-alsa --with-avahi --with-ssl=openssl --with-systemd
+$ ./configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd
$ make
$ sudo make install
```
@@ -61,7 +58,7 @@ Now to configure Shairport Sync. Here are the important options for the Shairpor
// Sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC
general =
{
- volume_range_db = 60;
+ volume_range_db = 60;
};
alsa =
@@ -71,6 +68,8 @@ alsa =
};
```
+The `volume_range_db = 60;` setting makes Shairport Sync use only the usable part of the built-in audio card mixer's attenuation range.
+
The next step is to enable Shairport Sync to start automatically on boot up:
```
# systemctl enable shairport-sync
@@ -79,6 +78,6 @@ Finally, either reboot the Pi or start the `shairport-sync` service:
```
# systemctl start shairport-sync
```
-The Shairport Sync AirPlay service should now appear on the network with a service name made from the Pi's hostname with the first letter capitalised, e.g. hostname `raspberrypi` gives a service name `Raspberrypi`. You can change the service name and set a password in the configuration file. BTW, you should *never* use an important password as the AirPlay password for a Shairport Sync player – the password is stored in Shairport Sync's configuration file in plain text and is thus completely vulnerable.
+The Shairport Sync AirPlay service should now appear on the network with a service name made from the Pi's hostname with the first letter capitalised, e.g. hostname `raspberrypi` gives a service name `Raspberrypi`. You can change the service name and set a password in the configuration file.
Connect and enjoy...
diff --git a/ISSUES.md b/ISSUES.md
deleted file mode 100644
index 1364553..0000000
--- a/ISSUES.md
+++ /dev/null
@@ -1,15 +0,0 @@
-Issue Reports
-----
-Issue reports are welcome, but before you report an issue, please check that your version of Shairport Sync is up to date. You'll find details of releases at [Shairport Sync Releases](https://github.com/mikebrady/shairport-sync/releases).
-Then, please have a look though the existing [issues](https://github.com/mikebrady/shairport-sync/issues), both open and closed, and check for hints in the [TROUBLESHOOTING](TROUBLESHOOTING.md) page. It would be great to give some details of the device and version of Linux or FreeBSD in use along with version and build configuration of Shairport Sync you are using (use `$ shairport-sync -V` to get this). Then, if possible, some diagnostic information from the log or logfile would be useful.
-
-In general, a log verbosity of 2 is adequate (`-vv`, or the relevant entry in the configuration file), and it's usually helpful if statistics have been enabled (`--statistics` on the command line, or the relevant entry in the configuration file).
-
-If you are pasting in code or a log file, format it as code by preceding it and following it with a line containing exactly three backquotes and nothing else ([see here](https://guides.github.com/features/mastering-markdown/) for more on formatting):
-
-\`\`\`
-```
-code or log file entries
-```
-\`\`\`
-
diff --git a/LIBSOXR.md b/LIBSOXR.md
index cce2f19..8a81cfd 100644
--- a/LIBSOXR.md
+++ b/LIBSOXR.md
@@ -1,7 +1,7 @@
The Raspbian image at the time of writing is the `May 2016` version, with the release date of `2016-05-10`. It does not include `libsoxr`, but it is available as a package via `apt-get`.
-Alternatively, `libsoxr`, part of [The Sox Resampler Library](https://sourceforge.net/projects/soxr/), is very easy to compile. Here are very brief instructions to download, compile and install it:
+Alternatively, `libsoxr` is very easy to compile. Here are very brief instructions to download, compile and install it:
* Install `cmake`. This is used in the building of libsoxr. On Linuxes such as Debian/Ubuntu/Raspbian:
```
@@ -14,14 +14,14 @@ On FreeBSD:
* Download the `libsoxr source`:
```
-$ git clone git://git.code.sf.net/p/soxr/code soxr-code
+$ git clone git://git.code.sf.net/p/soxr/code libsoxr
```
-* `cd` into the `soxr-code` directory and start the build process:
+* `cd` into the `libsoxr` directory and start the build process:
```
-$ cd soxr-code
+$ cd libsoxr
$ ./go
```
-Be patient! This takes a long time on a first-generation Raspberry Pi -- it looks like it gets stuck around 40% or 50%, but it will finish if you let it.
+Be patient! This takes a long time on a Raspberry Pi -- it looks like it gets stuck around 40% or 50%, but it will finish if you let it.
Having compiled `libsoxr`, it must now must be installed:
```
@@ -32,7 +32,7 @@ Finally, for Shairport Sync to be able to locate `libsoxr` during compilation, y
On Linuxes such as Debian/Ubuntu/Raspbian:
```
-# ldconfig
+# ldconfig -v
```
On FreeBSD you must add the location of the `soxr.pc` file to the `PKG_CONFIG_PATH`, if it exists, and define it otherwise. Here is what you do if it doesn't already exist:
```
diff --git a/Makefile.am b/Makefile.am
index 25b87f2..2bbc387 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,19 +1,25 @@
SUBDIRS = man
+ARFLAGS = cr
+lib_dbus_interface_a_CFLAGS = -pthread
+lib_mpris_interface_a_CFLAGS = -pthread
+
bin_PROGRAMS = shairport-sync
# See below for the flags for the test client program
-shairport_sync_SOURCES = shairport.c rtsp.c mdns.c mdns_external.c common.c rtp.c player.c alac.c audio.c loudness.c
+shairport_sync_SOURCES = shairport.c rtsp.c mdns.c common.c rtp.c player.c alac.c audio.c loudness.c activity_monitor.c
-AM_CFLAGS = -Wno-multichar -Wall -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
if BUILD_FOR_FREEBSD
- AM_CPPFLAGS = -I/usr/local/include -Wno-multichar -DSYSCONFDIR=\"$(sysconfdir)\" -O2
+ AM_CXXFLAGS = -I/usr/local/include -Wno-multichar -Wall -Wextra -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
+ AM_CFLAGS = -Wno-multichar -Wall -Wextra -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
else
if BUILD_FOR_OPENBSD
- AM_CPPFLAGS = -I/usr/local/include -Wno-multichar -DSYSCONFDIR=\"$(sysconfdir)\"
+ AM_CXXFLAGS = -I/usr/local/include -Wno-multichar -Wall -Wextra -Wno-clobbered -Wno-psabi -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
+ AM_CFLAGS = -Wno-multichar -Wall -Wextra -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
else
- AM_CPPFLAGS = -Wno-multichar -DSYSCONFDIR=\"$(sysconfdir)\"
+ AM_CXXFLAGS = -Wno-multichar -Wall -Wextra -Wno-clobbered -Wno-psabi -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
+ AM_CFLAGS = -Wno-multichar -Wall -Wextra -Wno-clobbered -Wno-psabi -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
endif
endif
@@ -34,10 +40,18 @@ if USE_TINYSVCMDNS
shairport_sync_SOURCES += mdns_tinysvcmdns.c tinysvcmdns.c
endif
+if USE_EXTERNAL_MDNS
+shairport_sync_SOURCES += mdns_external.c
+endif
+
if USE_ALSA
shairport_sync_SOURCES += audio_alsa.c
endif
+if USE_JACK
+shairport_sync_SOURCES += audio_jack.c
+endif
+
if USE_SNDIO
shairport_sync_SOURCES += audio_sndio.c
endif
@@ -68,7 +82,7 @@ endif
if USE_CONVOLUTION
shairport_sync_SOURCES += FFTConvolver/AudioFFT.cpp FFTConvolver/FFTConvolver.cpp FFTConvolver/Utilities.cpp FFTConvolver/convolver.cpp
-AM_CXXFLAGS = -std=c++11
+AM_CXXFLAGS += -std=c++11
endif
if USE_DNS_SD
@@ -79,6 +93,10 @@ if USE_METADATA_HUB
shairport_sync_SOURCES += metadata_hub.c
endif
+if USE_MQTT
+shairport_sync_SOURCES += mqtt.c
+endif
+
if USE_DACP_CLIENT
shairport_sync_SOURCES += dacp.c tinyhttp/chunk.c tinyhttp/header.c tinyhttp/http.c
endif
@@ -86,29 +104,41 @@ endif
BUILT_SOURCES =
noinst_HEADERS =
CLEANFILES =
+shairport_sync_LDADD =
+noinst_LIBRARIES =
if USE_DBUS
-shairport_sync_SOURCES += dbus-service.c dbus-interface.c
+shairport_sync_LDADD += lib_dbus_interface.a
+noinst_LIBRARIES += lib_dbus_interface.a
+lib_dbus_interface_a_SOURCES = dbus-interface.c
+shairport_sync_SOURCES += dbus-service.c
BUILT_SOURCES += dbus-interface.h dbus-interface.c
# We don't want to install this header
noinst_HEADERS += $(BUILT_SOURCES)
# Correctly clean the generated headers, but keep the xml description
CLEANFILES += $(BUILT_SOURCES)
-dbus-interface.h dbus-interface.c: org.gnome.ShairportSync.xml
+dbus-interface.c: org.gnome.ShairportSync.xml
gdbus-codegen --interface-prefix org.gnome --generate-c-code dbus-interface org.gnome.ShairportSync.xml
+dbus-interface.h: dbus-interface.c
+ touch dbus-interface.h
endif
if USE_MPRIS
-shairport_sync_SOURCES += mpris-service.c mpris-interface.c
+shairport_sync_LDADD += lib_mpris_interface.a
+noinst_LIBRARIES += lib_mpris_interface.a
+lib_mpris_interface_a_SOURCES = mpris-interface.c
+shairport_sync_SOURCES += mpris-service.c
BUILT_SOURCES += mpris-interface.h mpris-interface.c
# We don't want to install this header
noinst_HEADERS += $(BUILT_SOURCES)
# Correctly clean the generated headers, but keep the xml description
CLEANFILES += $(BUILT_SOURCES)
-mpris-interface.h mpris-interface.c: org.mpris.MediaPlayer2.xml
+mpris-interface.c: org.mpris.MediaPlayer2.xml
gdbus-codegen --interface-prefix org.mpris --generate-c-code mpris-interface org.mpris.MediaPlayer2.xml
+mpris-interface.h: mpris-interface.c
+ touch mpris-interface.h
endif
noinst_PROGRAMS =
@@ -116,32 +146,47 @@ noinst_PROGRAMS =
if USE_DBUS_CLIENT
#Make it, but don't install it anywhere
noinst_PROGRAMS += shairport-sync-dbus-test-client
-shairport_sync_dbus_test_client_SOURCES = dbus-interface.c dbus-interface.h shairport-sync-dbus-test-client.c
+shairport_sync_dbus_test_client_SOURCES = shairport-sync-dbus-test-client.c
+shairport_sync_dbus_test_client_LDADD = lib_dbus_interface.a
endif
if USE_MPRIS_CLIENT
#Make it, but don't install it anywhere
noinst_PROGRAMS += shairport-sync-mpris-test-client
-shairport_sync_mpris_test_client_SOURCES = mpris-interface.c mpris-interface.h shairport-sync-mpris-test-client.c
+shairport_sync_mpris_test_client_SOURCES = shairport-sync-mpris-test-client.c
+shairport_sync_mpris_test_client_LDADD = lib_mpris_interface.a
endif
install-exec-hook:
+if BUILD_FOR_LINUX
+DBUS_POLICY_DIR=$(DESTDIR)/etc/dbus-1/system.d
+else
+DBUS_POLICY_DIR=$(DESTDIR)$(sysconfdir)/dbus-1/system.d
+endif
if INSTALL_CONFIG_FILES
[ -e $(DESTDIR)$(sysconfdir) ] || mkdir $(DESTDIR)$(sysconfdir)
cp scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf.sample
[ -f $(DESTDIR)$(sysconfdir)/shairport-sync.conf ] || cp scripts/shairport-sync.conf $(DESTDIR)$(sysconfdir)/shairport-sync.conf
if USE_DBUS
- cp scripts/shairport-sync-dbus-policy.conf $(DESTDIR)$(sysconfdir)/dbus-1/system.d/shairport-sync-dbus.conf
+ [ -e $(DBUS_POLICY_DIR) ] || mkdir -p $(DBUS_POLICY_DIR)
+if INSTALL_CYGWIN_SERVICE
+ cp scripts/shairport-sync-dbus-policy-cygwin.conf $(DBUS_POLICY_DIR)/shairport-sync-dbus.conf
+else
+ cp scripts/shairport-sync-dbus-policy.conf $(DBUS_POLICY_DIR)/shairport-sync-dbus.conf
+endif
endif
if USE_MPRIS
- cp scripts/shairport-sync-mpris-policy.conf $(DESTDIR)$(sysconfdir)/dbus-1/system.d/shairport-sync-mpris.conf
+ [ -e $(DBUS_POLICY_DIR) ] || mkdir -p $(DBUS_POLICY_DIR)
+if INSTALL_CYGWIN_SERVICE
+ cp scripts/shairport-sync-mpris-policy-cygwin.conf $(DBUS_POLICY_DIR)/shairport-sync-mpris.conf
+else
+ cp scripts/shairport-sync-mpris-policy.conf $(DBUS_POLICY_DIR)/shairport-sync-mpris.conf
+endif
endif
endif
if INSTALL_SYSTEMV
getent group shairport-sync &>/dev/null || groupadd -r shairport-sync >/dev/null
getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/bin/nologin -G audio shairport-sync >/dev/null
- [ -e /var/run/shairport-sync ] || mkdir -p /var/run/shairport-sync
- chown shairport-sync:shairport-sync /var/run/shairport-sync
[ -e $(DESTDIR)$(sysconfdir)/init.d ] || mkdir -p $(DESTDIR)$(sysconfdir)/init.d
[ -f $(DESTDIR)$(sysconfdir)/init.d/shairport-sync ] || cp scripts/shairport-sync $(DESTDIR)$(sysconfdir)/init.d/
endif
@@ -152,10 +197,14 @@ if INSTALL_SYSTEMD
[ -f $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || cp scripts/shairport-sync.service $(DESTDIR)$(systemdsystemunitdir)
endif
if INSTALL_FREEBSD_SERVICE
- pw showgroup shairport-sync > /dev/null 2>&1 || pw addgroup shairport-sync > /dev/null 2>&1
- pw showuser shairport-sync > /dev/null 2>&1 || pw adduser shairport-sync > /dev/null 2>&1
+ # Choose a uid and gid of 801 completely arbitrarity, except that it should be below 1000. FreeBSD doesn't seem to allow you to say "an ID in the range of..."
+ pw showgroup shairport-sync > /dev/null 2>&1 || pw addgroup -n shairport-sync -g 801 > /dev/null 2>&1
+ pw showuser shairport-sync > /dev/null 2>&1 || pw adduser -c "shairport-sync unprivileged user" -n shairport-sync -u 801 -s /usr/sbin/nologin -d /nonexistent > /dev/null 2>&1
[ -e /var/run/shairport-sync ] || mkdir -p /var/run/shairport-sync
chown shairport-sync:shairport-sync /var/run/shairport-sync
[ -f /usr/local/etc/rc.d/shairport_sync ] || cp scripts/shairport-sync.freebsd /usr/local/etc/rc.d/shairport_sync
chmod 555 /usr/local/etc/rc.d/shairport_sync
endif
+if INSTALL_CYGWIN_SERVICE
+ cp scripts/shairport-sync-config /usr/local/bin
+endif
diff --git a/OPENBSD.md b/OPENBSD.md
index 7f70881..446e967 100644
--- a/OPENBSD.md
+++ b/OPENBSD.md
@@ -45,7 +45,7 @@ Next, switch to the `development` branch, configure the build and compile it:
```
$ git checkout development
$ autoreconf -i -f
-$ ./configure --sysconfdir=/etc --with-avahi --with-ssl=openssl --with-sndio --with-os=openbsd
+$ ./configure --sysconfdir=/etc --with-avahi --with-ssl=openssl --with-libdaemon --with-sndio --with-os=openbsd
$ make
```
The application is called `shairport-sync`. Check that it's running correctly by executing the following command:
@@ -54,7 +54,7 @@ $ ./shairport-sync -V
```
This will execute the application and it will return its version information and terminate, for example:
```
-3.2-OpenSSL-Avahi-sndio-sysconfdir:/etc
+3.2-libdaemon-OpenSSL-Avahi-sndio-sysconfdir:/etc
```
There is no make install yet -- you're on your own.
diff --git a/README.md b/README.md
index 01e638f..e32aaf8 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+
Shairport Sync
=============
Shairport Sync is an AirPlay audio player – it plays audio streamed from iTunes, iOS, Apple TV and macOS devices and AirPlay sources such as Quicktime Player and [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/), among others.
@@ -6,7 +7,7 @@ Audio played by a Shairport Sync-powered device stays synchronised with the sour
Shairport Sync runs on Linux, FreeBSD and OpenBSD. It does not support AirPlay video or photo streaming.
-This is the stable "master" branch. Changes and updates are incorporated into this branch relatively slowly. To access the development version, where all the latest changes are made first, please switch to the "development" branch.
+This document is being updated.
More Information
----------
@@ -34,7 +35,7 @@ What else?
* Hardware Mute — Shairport Sync can mute properly if the hardware supports it.
* Support for the Apple ALAC decoder.
* Output bit depths of 8, 16, 24 and 32 bits, rather than the standard 16 bits.
-* Fast Response — With hardware volume control, response is instantaneous; otherwise the response time is 0.15 seconds with `alsa`, 0.35 seconds with `sndio`.
+* Fast Response — With hardware volume control, response is instantaneous; otherwise the response time is 0.20 seconds with `alsa`, 0.35 seconds with `sndio`.
* Non-Interruptible — Shairport Sync sends back a "busy" signal if it's already playing audio from another source, so other sources can't disrupt an existing Shairport Sync session. (If a source disappears without warning, the session automatically terminates after two minutes and the device becomes available again.)
* Metadata — Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in iOS.
* Raw Audio — Shairport Sync can deliver raw PCM audio to standard output or to a pipe. This output is delivered synchronously with the source after the appropriate latency and is not interpolated or "stuffed" on its way through Shairport Sync.
@@ -149,7 +150,7 @@ Optional:
* libsoxr
* libalac (This is a library containing the Apple ALAC decoder.)
-Many Linux distributions have Avahi and OpenSSL already in place, so normally it probably makes sense to choose those options rather than tinysvcmdns or mbed TLS. The `libsoxr` library is available in recent Linux distributions, but it requires lots of processor power — chances are an embedded processor won't be able to keep up.
+Many Linux distributions have Avahi and OpenSSL already in place, so normally it probably makes sense to choose those options rather than tinysvcmdns or mbed TLS. The `libsoxr` library is available in recent Linux distributions, but it requires lots of processor power.
Debian, Ubuntu and Raspbian users can get the basics with:
@@ -187,6 +188,7 @@ $ autoreconf -i -f
- `--with-soundio` include an optional backend module to enable raw audio to be output through the soundio system.
- `--with-avahi` or `--with-tinysvcmdns` for mdns support. Avahi is a widely-used system-wide zero-configuration networking (zeroconf) service — it may already be in your system. If you don't have Avahi, or similar, then consider including tinysvcmdns, which is a tiny zeroconf service embedded inside the shairport-sync application itself. To enable multicast for `tinysvcmdns`, you may have to add a default route with the following command: `route add -net 224.0.0.0 netmask 224.0.0.0 eth0` (substitute the correct network port for `eth0`). You should not have more than one zeroconf service on the same system — bad things may happen, according to RFC 6762, §15.
- `--with-ssl=openssl`, `--with-ssl=mbedtls` or `--with-ssl=polarssl` (deprecated) for encryption and related utilities using either OpenSSL, mbed TLS or PolarSSL.
+- `--with-libdaemon` include a demonising library needed if you want to be able to demonise Shairport Sync with the `-d` option. Not needed for `systemd`-based systems which demonise programs differently.
- `--with-soxr` for libsoxr-based resampling.
- `--with-piddir` for specifying where the PID file should be stored. This directory is normally chosen automatically. The directory must be writable. If you use this option, you may have to edit the init script to search for the PID file in your new location.
- `--with-metadata` to add support for Shairport Sync to pipe metadata to a compatible application of your choice. See https://github.com/mikebrady/shairport-sync-metadata-reader for a sample metadata reader.
@@ -242,7 +244,7 @@ SYNOPSIS
...
```
-If your system is definitely a `systemd` system, choose `--with-systemd` below. Otherwise, choose `--with-systemv`.
+If your system is definitely a `systemd` system, choose `--with-libdaemon --with-systemd` below. Otherwise, choose `--with-systemv`.
**Choose the location of the configuration file**
@@ -255,7 +257,7 @@ Here is a recommended set of configuration options suitable for Linux installati
`$ ./configure --sysconfdir=/etc --with-alsa --with-pa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemd`
* Omit the `--with-soxr` if the libsoxr library is not available.
-* For installation into a System V system, replace the `--with-systemd` with `--with-systemv`.
+* For installation into a System V system, replace the `--with-systemd` with `--with-libdaemon --with-systemv`.
* If you intend to use Shairport Sync with PulseAudio in the standard user mode, it can not be a system service, so you should omit both `--with-systemd` and `--with-systemv`.
**Build and Install the Application:**
@@ -312,14 +314,13 @@ For the ALSA backend you may need to (c) specify the output device to use and (d
Shairport Sync reads settings from a configuration file at `/etc/shairport-sync.conf` (note that in FreeBSD it will be at `/usr/local/etc/shairport-sync.conf`). When you run `$sudo make install`, a sample configuration file is installed or updated at `/etc/shairport-sync.conf.sample` (`/usr/local/etc/shairport-sync.conf.sample` in FreeBSD). This contains all the setting groups and all the settings available, but they all are commented out (comments begin with `//`) so that default values are used. The file contains explanations of the settings, useful hints and suggestions. In addition, if the file doesn't already exist, a default configuration is installed, which should work in almost any system with a sound card.
-Settings in the configuration file are grouped. For instance, there is a `general` group within which you can use the `name` tag to set the service name. Suppose you wanted to set the name of the service to `Front Room`, give the service the password `secret` and use `libsoxr` interpolation, then you should do the following:
+Settings in the configuration file are grouped. For instance, there is a `general` group within which you can use the `name` tag to set the service name. Suppose you wanted to set the name of the service to `Front Room` asd give the service the password `secret`, then you should do the following:
```
general =
{
name = "Front Room";
password = "secret";
- interpolation = "soxr";
// ... other general settings
};
```
@@ -430,11 +431,10 @@ alsa = {
};
```
-Here is an example of using soxr-based resampling and driving a Topping TP30 Digital Amplifier, which has an integrated USB DAC and which is connected as audio device `hw:1`:
+Here is an example of driving a Topping TP30 Digital Amplifier, which has an integrated USB DAC and which is connected as audio device `hw:1`:
```
general = {
name = "Kitchen";
- interpolation = "soxr";
};
alsa = {
@@ -590,4 +590,3 @@ If you are using WiFi, you should ensure that WiFi power management is off. See
Troubleshooting
---------------
Please refer to [TROUBLESHOOTING](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md) for a few hints, contributed by users.
-
diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 94cee6b..9181e84 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -1,3 +1,24 @@
+Version 3.3
+====
+Version 3.3 is focused on stability improvements and also offers a number of enhancements. Here is a selection of the most important:
+
+**Enhancements**
+* Automatic `alsa` output device speed and format selection. The greatest bit depth and the lowest multiple of 44,100 frames per second settings are chosen automatically by default. Manual selection is still available.
+* Automatic interpolation selection. If the CPU is fast enough, the better-quality `soxr` interpolation method is chosen. Otherwise `basic` interpolation is used. Manual selection is still available.
+* A new `active` state. New hooks are provided to execute programs before entering and after leaving the `active` state which covers sequences of play sessions separated by short intervals. This simplifies amplifier switch-on and switch-off, for example.
+* DAC crackle minimisation. This new feature is intended to minimise pops and crackles caused in some Digital to Analog Converters (DACs) when they transition between active and idle or standby operation. The new `alsa`-only `disable_standby_mode` prevents the DAC entering the standby mode by keeping it active, either permanently or while Shairport Sync is in the active state. This feature is switched off by default.
+* A new backend to interface Shairport Sync to [Jack Audio](http://jackaudio.org), thanks to the work of [Jörn Nettingsmeier](https://github.com/nettings).
+* A new [MQTT](https://en.wikipedia.org/wiki/MQTT) client interface, thanks to the work of [Till Zimmermann](https://github.com/tillz).
+* Changes in logging -- now you should add `-u` to direct log entries to STDERR (typically the console) rather than the system log. For example, to get logs of verbosity 1 to appear on the console: `$ shairport-sync -v -u`.
+* The help menu now lists all `alsa` output devices found.
+* Better support for big-endian CPUs.
+* Shairport Sync accepts AirPlay streams containing CD-quality uncompressed PCM.
+
+**Bug Fixes**
+* Lots of bugs – too many to list here – have been fixed that significantly improve the stability of Shairport Sync. The full list is in the Release Notes.
+
+For more details of enhancements and bug fixes, please see the [Release Notes for 3.3](https://github.com/mikebrady/shairport-sync/releases/tag/3.3). Special thanks to [gibman](https://github.com/gibman).
+
Version 3.2.2
====
Please see the [Release Notes for 3.2](https://github.com/mikebrady/shairport-sync/releases/tag/3.2).
diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md
index 0215c70..262bca6 100644
--- a/TROUBLESHOOTING.md
+++ b/TROUBLESHOOTING.md
@@ -7,39 +7,29 @@ In this brief document will be listed some problems and some solutions, some pro
1. Before starting, ensure that your software is up-to-date.
2. Set the `interpolation` in the `general` section of the configuration file to `basic` as the `soxr` setting can cause lower-powered devices to bog down at critical times, e.g. see [this report](https://github.com/mikebrady/shairport-sync/issues/631#issuecomment-366305203).
-### WiFi and Microwaves
-Microwaves can interfere with WiFi -- see [here](http://sciabc.us/F6Gaa) for example.
-### WiFi adapter running in power-saving/low-power mode
+### WiFi adapter running in power-saving / low-power mode
+
**Check Throughput**
-You can learn how to check your Wi-Fi's throughput by following this [tutorial](https://thepi.io/how-to-use-your-raspberry-pi-to-monitor-broadband-speed/).
+
+ You can check WiFi throughput using, for example, https://thepi.io/how-to-use-your-raspberry-pi-to-monitor-broadband-speed/
**Problem**
+
Shairport Sync is installed and running, but sometimes it disappears from the network, and sometimes it suffers from long dropouts.
**Possible Cause**
-This can be caused by lots of things, however, the most prominent cause is that the Wi-Fi adapter may be set to run in a low-power/power-saving mode; which after a certain period of time(usually a minute or so), if the adapter is not busy, the system will hold back the active use of the adapter.
-
-This is an unintended scenario where Shairport requires the Wi-Fi adapter at all times to function properly. Hence you need to turn off the power saving/low-power mode.
-
-How you do this varies with platform to platform or adapter to adapter and therefore internet search is your friend or some closed issues on this repo has some more knowledge for you as well.
-
-However, below are the instructions for the onboard Wi-Fi chip that comes with Raspberry Pi, another popular chip(Realtek RTL8188CUS), and, the chip 8192cu.
-Moreover, if you want an in-depth look at Raspberry Pi Wi-Fi adapters, [here](https://www.elinux.org/RPi_USB_Wi-Fi_Adapters) is a good primer.
+This can be caused by lots of things, but one of them is that the WiFi adapter may be set to run in a low-power or power-saving mode. If it's not busy, then after a while it goes into a low-power mode. This is bad as the device needs to be always connected to the network to provide the AirPlay service. You need to turn off power-saving mode. How you do this varies with platform and with WiFi adapter – internet search is your friend. Here, for instance, is the command for the C.H.I.P. from Next Thing Co, which has built in WiFi and Linux and has the `iw` command installed:
-#### Onboard Wi-Fi Chip
-
-Here, for instance, is the command for the C.H.I.P. from Next Thing Co., which has built in WiFi and Linux and has the `iw` command installed:
```
-# iw dev wlan0 set power_save off
+iw dev wlan0 set power_save off
```
-
Here is the command sequence for a Raspberry Pi 3, which has built-in WiFi:
+
```
-# iwconfig wlan0 power off
+sudo iwconfig wlan0 power off
```
-
Alternatively, (also for the Raspberry Pi), add the following line:
```
wireless-power off
@@ -47,8 +37,9 @@ wireless-power off
to the file `/etc/network/interfaces`.
Here is another option, suggested by [davidhq](https://github.com/davidhq) in [#653](https://github.com/mikebrady/shairport-sync/issues/653#issuecomment-391100620):
+
```
-# nano /etc/network/if-up.d/off-power-manager
+$ sudo nano /etc/network/if-up.d/off-power-manager
```
Type:
@@ -56,37 +47,12 @@ Type:
#!/bin/sh
/sbin/iwconfig wlan0 power off
```
-
Then:
```
-# chmod +x /etc/network/if-up.d/off-power-manager
+sudo chmod +x /etc/network/if-up.d/off-power-manager
```
-#### Realtek RTL8188CUS
-
-The chip `Realtek RTL8188CUS` is a popular one used by many Wi-Fi adapters. To disable its power-saving/low-power mode:
-
-- `sudo vim /etc/modprobe.d/r8188eu.conf`,
-- paste the below content into the file,
- - `options r8188eu rtw_power_mgnt=0 rtw_enusbss=0`
-- `sudo reboot`
-
-**After the reboot**, the power-saving/low-power mode should be disabled. You can check it by the following command. In return, it should output `0`.
-
-`cat /sys/module/r8188eu/parameters/rtw_power_mgnt`
-
-#### 8192cu
-
-Another chip you may encounter is `8192cu`. The process is similar to `Realtek RTL8188CUS` – just a simple name change.
-
-- `sudo vim /etc/modprobe.d/8192cu.conf`,
-- paste the below content into the file,
- - `options 8192cu rtw_power_mgnt=0 rtw_enusbss=0`
-- `sudo reboot`
-
-**After the reboot**, the power-saving/low-power mode should be disabled. You can check it by the following command. It should return the value `0`.
-
-`cat /sys/module/8192cu/parameters/rtw_power_mgnt`
+There are some more details in some the closed issues on this repository.
### Faulty WiFi
For an example of what it can take to track down a bad WiFi situation – in this case, a faulty WiFi adapter – please look at [this report](https://github.com/mikebrady/shairport-sync/issues/689).
diff --git a/UPDATING.md b/UPDATING.md
index 8b68186..31b8752 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -37,7 +37,7 @@ Please review the release notes to see if any configuration settings have been c
$ ./configure --with-alsa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemd --sysconfdir=/etc
#OR
#The following is the standard configuration for a Linux that uses the alsa backend and the older System V initialisation system:
-$ ./configure --with-alsa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemv --sysconfdir=/etc
+$ ./configure --with-libdaemon --with-alsa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemv --sysconfdir=/etc
$ make
$ sudo make install
diff --git a/activity_monitor.c b/activity_monitor.c
new file mode 100644
index 0000000..c6966bc
--- /dev/null
+++ b/activity_monitor.c
@@ -0,0 +1,275 @@
+/*
+ * Activity Monitor
+ *
+ * Contains code to run an activity flag and associated timer
+ * A pthread implements a simple state machine with three states,
+ * "idle", "active" and "timing out".
+ *
+ *
+ * This file is part of Shairport Sync.
+ * Copyright (c) Mike Brady 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
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <inttypes.h>
+#include <stdlib.h>
+#include <sys/types.h>
+
+#include "config.h"
+
+#include "activity_monitor.h"
+#include "common.h"
+#include "rtsp.h"
+
+#ifdef CONFIG_DBUS_INTERFACE
+#include "dbus-service.h"
+#endif
+
+
+enum am_state state;
+enum ps_state { ps_inactive, ps_active } player_state;
+
+int activity_monitor_running = 0;
+
+pthread_t activity_monitor_thread;
+pthread_mutex_t activity_monitor_mutex;
+pthread_cond_t activity_monitor_cv;
+
+void going_active(int block) {
+ // debug(1, "activity_monitor: state transitioning to \"active\" with%s blocking", block ? "" :
+ // "out");
+ if (config.cmd_active_start)
+ 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
+#endif
+
+#ifdef CONFIG_DBUS_INTERFACE
+ if (dbus_service_is_running())
+ shairport_sync_set_active(SHAIRPORT_SYNC(shairportSyncSkeleton), TRUE);
+#endif
+
+ if (config.disable_standby_mode == disable_standby_auto) {
+#ifdef CONFIG_DBUS_INTERFACE
+ if (dbus_service_is_running())
+ shairport_sync_set_disable_standby(SHAIRPORT_SYNC(shairportSyncSkeleton), TRUE);
+ else
+ config.keep_dac_busy = 1;
+#else
+ config.keep_dac_busy = 1;
+#endif
+ }
+}
+
+void going_inactive(int block) {
+ // debug(1, "activity_monitor: state transitioning to \"inactive\" with%s blocking", block ? "" :
+ // "out");
+ if (config.cmd_active_stop)
+ 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
+#endif
+
+#ifdef CONFIG_DBUS_INTERFACE
+ if (dbus_service_is_running())
+ shairport_sync_set_active(SHAIRPORT_SYNC(shairportSyncSkeleton), FALSE);
+#endif
+
+ if (config.disable_standby_mode == disable_standby_auto) {
+#ifdef CONFIG_DBUS_INTERFACE
+ if (dbus_service_is_running())
+ shairport_sync_set_disable_standby(SHAIRPORT_SYNC(shairportSyncSkeleton), FALSE);
+ else
+ config.keep_dac_busy = 0;
+#else
+ config.keep_dac_busy = 0;
+#endif
+ }
+}
+
+void activity_monitor_signify_activity(int active) {
+ // this could be pthread_cancelled and they is likely to be cancellation points in the
+ // hooked-on procedures
+ pthread_cleanup_debug_mutex_lock(&activity_monitor_mutex, 10000, 1);
+ player_state = active == 0 ? ps_inactive : ps_active;
+ // Now, although we could simply let the state machine in the activity monitor thread
+ // look after eveything, we will change state here in two situations:
+ // 1. If the state machine is am_inactive and the player is ps_active
+ // we will change the state to am_active and execute the going_active() function.
+ // 2. If the state machine is am_active and the player is ps_inactive and
+ // the activity_idle_timeout is 0, then we will change the state to am_inactive and
+ // execute the going_inactive() function.
+ //
+ // The reason for all this is that we might want to perform the attached scripts
+ // and wait for them to complete before continuing. If they were perfomed in the
+ // activity monitor thread, then we couldn't wait for them to complete.
+
+ // Thus, the only time the thread will execute a going_... function is when a non-zero
+ // timeout actually matures.
+
+ if ((state == am_inactive) && (player_state == ps_active)) {
+ going_active(
+ config.cmd_blocking); // note -- will be executed with the mutex locked, but that's okay
+ } else if ((state == am_active) && (player_state == ps_inactive) &&
+ (config.active_state_timeout == 0.0)) {
+ going_inactive(
+ config.cmd_blocking); // note -- will be executed with the mutex locked, but that's okay
+ }
+
+ pthread_cond_signal(&activity_monitor_cv);
+ pthread_cleanup_pop(1); // release the mutex
+}
+
+void activity_thread_cleanup_handler(__attribute__((unused)) void *arg) {
+ debug(3, "activity_monitor: thread exit.");
+ pthread_cond_destroy(&activity_monitor_cv);
+ pthread_mutex_destroy(&activity_monitor_mutex);
+}
+
+void *activity_monitor_thread_code(void *arg) {
+ int rc = pthread_mutex_init(&activity_monitor_mutex, NULL);
+ if (rc)
+ die("activity_monitor: error %d initialising activity_monitor_mutex.", rc);
+
+// set the flowcontrol condition variable to wait on a monotonic clock
+#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(&activity_monitor_cv, &attr);
+ pthread_condattr_destroy(&attr);
+
+#endif
+#ifdef COMPILE_FOR_OSX
+ rc = pthread_cond_init(&activity_monitor_cv, NULL);
+#endif
+ if (rc)
+ die("activity_monitor: error %d initialising activity_monitor_cv.");
+ pthread_cleanup_push(activity_thread_cleanup_handler, arg);
+
+ uint64_t sec;
+ uint64_t nsec;
+ struct timespec time_for_wait;
+
+ state = am_inactive;
+ player_state = ps_inactive;
+
+ pthread_mutex_lock(&activity_monitor_mutex);
+ do {
+ switch (state) {
+ case am_inactive:
+ // debug(1,"am_state: am_inactive");
+ while (player_state != ps_active)
+ pthread_cond_wait(&activity_monitor_cv, &activity_monitor_mutex);
+ state = am_active;
+ // going_active(); // this is done in activity_monitor_signify_activity
+ break;
+ case am_active:
+ // debug(1,"am_state: am_active");
+ while (player_state != ps_inactive)
+ pthread_cond_wait(&activity_monitor_cv, &activity_monitor_mutex);
+ if (config.active_state_timeout == 0.0) {
+ state = am_inactive;
+ // going_inactive(); // this is done in activity_monitor_signify_activity
+ } 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;
+
+#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;
+ 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;
+ time_for_wait.tv_sec = sec;
+ time_for_wait.tv_nsec = nsec;
+#endif
+ }
+ break;
+ case am_timing_out:
+ // debug(1,"am_state: am_timing_out");
+ rc = 0;
+ while ((player_state != ps_active) && (rc != ETIMEDOUT)) {
+#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD
+ rc = pthread_cond_timedwait(&activity_monitor_cv, &activity_monitor_mutex,
+ &time_for_wait); // this is a pthread cancellation point
+#endif
+#ifdef COMPILE_FOR_OSX
+ rc = pthread_cond_timedwait_relative_np(&activity_monitor_cv, &activity_monitor_mutex,
+ &time_for_wait);
+#endif
+ }
+ if (player_state == ps_active)
+ state = am_active; // player has gone active -- do nothing, because it's still active
+ else if (rc == ETIMEDOUT) {
+ state = am_inactive;
+ pthread_mutex_unlock(&activity_monitor_mutex);
+ going_inactive(0); // don't wait for completion -- it makes no sense
+ pthread_mutex_lock(&activity_monitor_mutex);
+ } else {
+ // activity monitor was woken up in the state am_timing_out, but not by a timeout and player
+ // is not in ps_active state
+ debug(1,
+ "activity monitor was woken up in the state am_timing_out, but didn't change state");
+ }
+ break;
+ default:
+ debug(1, "activity monitor in an illegal state!");
+ state = am_inactive;
+ break;
+ }
+ } while (1);
+ pthread_mutex_unlock(&activity_monitor_mutex);
+ pthread_cleanup_pop(0); // should never happen
+ pthread_exit(NULL);
+}
+
+enum am_state activity_status() {
+ return (state);
+}
+
+void activity_monitor_start() {
+ // debug(1,"activity_monitor_start");
+ pthread_create(&activity_monitor_thread, NULL, activity_monitor_thread_code, NULL);
+ activity_monitor_running = 1;
+}
+
+void activity_monitor_stop() {
+ if (activity_monitor_running) {
+ debug(3, "activity_monitor_stop start...");
+ pthread_cancel(activity_monitor_thread);
+ pthread_join(activity_monitor_thread, NULL);
+ debug(2, "activity_monitor_stop complete");
+ }
+}
diff --git a/activity_monitor.h b/activity_monitor.h
new file mode 100644
index 0000000..d9e1ffd
--- /dev/null
+++ b/activity_monitor.h
@@ -0,0 +1,8 @@
+#pragma once
+
+enum am_state { am_inactive, am_active, am_timing_out };
+
+void activity_monitor_start();
+void activity_monitor_stop();
+void activity_monitor_signify_activity(int active); // 0 means inactive, non-zero means active
+enum am_state activity_status(); // true if non inactive; false if inactive
diff --git a/audio.c b/audio.c
index 058cbfe..7868637 100644
--- a/audio.c
+++ b/audio.c
@@ -1,6 +1,7 @@
/*
* Audio driver handler. This file is part of Shairport.
* Copyright (c) James Laird 2013
+ * Modifications (c) Mike Brady 2014 -- 2018
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -30,6 +31,9 @@
#include <stdio.h>
#include <string.h>
+#ifdef CONFIG_JACK
+extern audio_output audio_jack;
+#endif
#ifdef CONFIG_SNDIO
extern audio_output audio_sndio;
#endif
@@ -65,6 +69,9 @@ static audio_output *outputs[] = {
#ifdef CONFIG_PA
&audio_pa,
#endif
+#ifdef CONFIG_JACK
+ &audio_jack,
+#endif
#ifdef CONFIG_AO
&audio_ao,
#endif
@@ -99,14 +106,18 @@ audio_output *audio_get_output(char *name) {
void audio_ls_outputs(void) {
audio_output **out;
- printf("Available audio outputs:\n");
+ printf("Available audio backends:\n");
for (out = outputs; *out; out++)
printf(" %s%s\n", (*out)->name, out == outputs ? " (default)" : "");
for (out = outputs; *out; out++) {
printf("\n");
- printf("Options for output %s:\n", (*out)->name);
- (*out)->help();
+ if ((*out)->help) {
+ printf("Settings and options for the audio backend \"%s\":\n", (*out)->name);
+ (*out)->help();
+ } else {
+ printf("There are no settings or options for the audio backend \"%s\".\n", (*out)->name);
+ }
}
}
@@ -146,6 +157,21 @@ void parse_general_audio_options(void) {
}
}
+ /* Get the minumum buffer size for fancy interpolation setting in seconds. */
+ if (config_lookup_float(config.cfg,
+ "general.audio_backend_buffer_interpolation_threshold_in_seconds",
+ &dvalue)) {
+ if ((dvalue < 0) || (dvalue > config.audio_backend_buffer_desired_length)) {
+ die("Invalid audio_backend_buffer_interpolation_threshold_in_seconds value: \"%f\". It "
+ "should be between 0 and "
+ "audio_backend_buffer_desired_length_in_seconds of %.3f, default is %.3f seconds",
+ dvalue, config.audio_backend_buffer_desired_length,
+ config.audio_backend_buffer_interpolation_threshold_in_seconds);
+ } else {
+ config.audio_backend_buffer_interpolation_threshold_in_seconds = dvalue;
+ }
+ }
+
/* Get the latency offset (deprecated). */
if (config_lookup_int(config.cfg, "general.audio_backend_latency_offset", &value)) {
if ((value < -66150) || (value > 66150)) {
@@ -164,9 +190,9 @@ 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.0) || (dvalue > 1.5)) {
+ if ((dvalue < -1.75) || (dvalue > 1.75)) {
die("Invalid audio_backend_latency_offset_in_seconds \"%f\". It "
- "should be between -1.0 and +1.5, default is 0 seconds",
+ "should be between -1.75 and +1.75, default is 0 seconds",
dvalue);
} else {
config.audio_backend_latency_offset = dvalue;
diff --git a/audio.h b/audio.h
index dc7eb87..5fc5f39 100644
--- a/audio.h
+++ b/audio.h
@@ -18,13 +18,19 @@ typedef struct {
int (*init)(int argc, char **argv);
// at end of program
void (*deinit)(void);
+
+ int (*prepare)(void); // looks and sets stuff in the config data structure
void (*start)(int sample_rate, int sample_format);
// block of samples
- void (*play)(void *buf, int samples);
+ int (*play)(void *buf, int samples);
void (*stop)(void);
+ // may be null if no implemented
+ int (*is_running)(
+ void); // if implemented, will return 0 if everything is okay, non-zero otherwise
+
// may be null if not implemented
void (*flush)(void);
@@ -33,6 +39,8 @@ typedef struct {
// will change dynamically, so keep watching it. Implemented in ALSA only.
// returns a negative error code if there's a problem
int (*delay)(long *the_delay); // snd_pcm_sframes_t is a signed long
+ int (*rate_info)(uint64_t *elapsed_time,
+ uint64_t *frames_played); // use this to get the true rate of the DAC
// may be NULL, in which case soft volume is applied
void (*volume)(double vol);
@@ -41,7 +49,8 @@ typedef struct {
void (*parameters)(audio_parameters *info);
// may be NULL, in which case software muting is used.
- void (*mute)(int do_mute);
+ // also, will return a 1 if it is actually using the mute facility, 0 otherwise
+ int (*mute)(int do_mute);
} audio_output;
diff --git a/audio_alsa.c b/audio_alsa.c
index e2a53cc..cd692f1 100644
--- a/audio_alsa.c
+++ b/audio_alsa.c
@@ -1,7 +1,7 @@
/*
* libalsa output driver. This file is part of Shairport.
* Copyright (c) Muffinman, Skaman 2013
- * Copyright (c) Mike Brady 2014 -- 2018
+ * Copyright (c) Mike Brady 2014 -- 2019
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -27,54 +27,103 @@
#define ALSA_PCM_NEW_HW_PARAMS_API
-#include "audio.h"
-#include "common.h"
#include <alsa/asoundlib.h>
+#include <inttypes.h>
#include <math.h>
#include <memory.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
+#include "config.h"
+
+#include "common.h"
+#include "activity_monitor.h"
+#include "audio.h"
+
+enum alsa_backend_mode {
+ abm_disconnected,
+ abm_connected,
+ abm_playing
+} alsa_backend_state; // under the control of alsa_mutex
+
+typedef struct {
+ snd_pcm_format_t alsa_code;
+ int frame_size;
+} format_record;
+
static void help(void);
static int init(int argc, char **argv);
static void deinit(void);
static void start(int i_sample_rate, int i_sample_format);
-static void play(void *buf, int samples);
+static int play(void *buf, int samples);
static void stop(void);
static void flush(void);
int delay(long *the_delay);
-void do_mute(int request);
+int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played);
+void *alsa_buffer_monitor_thread_code(void *arg);
static void volume(double vol);
void do_volume(double vol);
+int prepare(void);
static void parameters(audio_parameters *info);
-static void mute(int do_mute);
+int mute(int do_mute); // returns true if it actually is allowed to use the mute
static double set_volume;
-static int output_method_signalled = 0;
+static int output_method_signalled = 0; // for reporting whether it's using mmap or not
+int delay_type_notified = -1; // for controlling the reporting of whether the output device can do precison delays (e.g. alsa->pulsaudio virtual devices can't)
+int use_monotonic_clock = 0; // this value will be set when the hardware is initialised
audio_output audio_alsa = {
.name = "alsa",
.help = &help,
.init = &init,
.deinit = &deinit,
+ .prepare = &prepare,
.start = &start,
.stop = &stop,
+ .is_running = NULL,
.flush = &flush,
.delay = &delay,
.play = &play,
- .mute = NULL, // a function will be provided if it can, and is allowed to, do hardware mute
- .volume = NULL, // a function will be provided if it can do hardware volume
- .parameters = &parameters};
+ .rate_info = &get_rate_information,
+ .mute = NULL, // a function will be provided if it can, and is allowed to,
+ // do hardware mute
+ .volume = NULL, // a function will be provided if it can do hardware volume
+ .parameters = NULL}; // a function will be provided if it can do hardware volume
static pthread_mutex_t alsa_mutex = PTHREAD_MUTEX_INITIALIZER;
+static pthread_mutex_t alsa_mixer_mutex = PTHREAD_MUTEX_INITIALIZER;
+
+pthread_t alsa_buffer_monitor_thread;
+
+// for deciding when to activate mute
+// there are two sources of requests to mute -- the backend itself, e.g. when it
+// is flushing
+// and the player, e.g. when volume goes down to -144, i.e. mute.
-static unsigned int desired_sample_rate;
-static enum sps_format_t sample_format;
+// we may not be allowed to use hardware mute, so we must reflect that too.
-static snd_pcm_t *alsa_handle = NULL;
+int mute_requested_externally = 0;
+int mute_requested_internally = 0;
+
+// for tracking how long the output device has stalled
+uint64_t stall_monitor_start_time; // zero if not initialised / not started /
+ // zeroed by flush
+long stall_monitor_frame_count; // set to delay at start of time, incremented by
+ // any writes
+uint64_t stall_monitor_error_threshold; // if the time is longer than this, it's
+ // an error
+
+static snd_output_t *output = NULL;
+int frame_size; // in bytes for interleaved stereo
+
+int alsa_device_initialised; // boolean to ensure the initialisation is only
+ // done once
+
+snd_pcm_t *alsa_handle = NULL;
static snd_pcm_hw_params_t *alsa_params = NULL;
+static snd_pcm_sw_params_t *alsa_swparams = NULL;
static snd_ctl_t *ctl = NULL;
static snd_ctl_elem_id_t *elem_id = NULL;
static snd_mixer_t *alsa_mix_handle = NULL;
@@ -90,19 +139,35 @@ static int alsa_mix_index = 0;
static int hardware_mixer = 0;
static int has_softvol = 0;
-static int volume_set_request = 0; // set when an external request is made to set the volume.
-int mute_request_pending = 0; // set when an external request is made to mute or unmute.
-int overriding_mute_state_requested = 0; // 1 = mute; 0 = unmute requested
-int mixer_volume_setting_gives_mute =
- 0; // set when it is discovered that particular mixer volume setting causes a mute.
-long alsa_mix_mute; // setting the volume to this value mutes output, if
- // mixer_volume_setting_gives_mute is true
+int64_t dither_random_number_store = 0;
+
+static int volume_set_request = 0; // set when an external request is made to set the volume.
+
+int mixer_volume_setting_gives_mute = 0; // set when it is discovered that
+ // particular mixer volume setting
+ // causes a mute.
+long alsa_mix_mute; // setting the volume to this value mutes output, if
+ // mixer_volume_setting_gives_mute is true
int volume_based_mute_is_active =
0; // set when muting is being done by a setting the volume to a magic value
-static snd_pcm_sframes_t (*alsa_pcm_write)(snd_pcm_t *, const void *,
+// use this to allow the use of snd_pcm_writei or snd_pcm_mmap_writei
+snd_pcm_sframes_t (*alsa_pcm_write)(snd_pcm_t *, const void *,
snd_pcm_uframes_t) = snd_pcm_writei;
+
+int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, enum yndk_type *using_update_timestamps);
+int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, enum yndk_type *using_update_timestamps);
+
+// use this to allow the use of standard or precision delay calculations, with standard the, uh, standard.
+int (*delay_and_status)(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, enum yndk_type *using_update_timestamps) = standard_delay_and_status;
+
+int precision_delay_available() {
+ // this is very crude -- if the device is a hardware device, then it's assumed the delay is precise
+ const char *output_device_name = snd_pcm_name(alsa_handle);
+ return (strstr(output_device_name,"hw:") == output_device_name);
+}
+
// static int play_number;
// static int64_t accumulated_delay, accumulated_da_delay;
int alsa_characteristics_already_listed = 0;
@@ -110,17 +175,31 @@ int alsa_characteristics_already_listed = 0;
static snd_pcm_uframes_t period_size_requested, buffer_size_requested;
static int set_period_size_request, set_buffer_size_request;
+static uint64_t measurement_start_time;
+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;
+
static void help(void) {
- printf(" -d output-device set the output device [default*|...]\n"
- " -m mixer-device set the mixer device ['output-device'*|...]\n"
- " -c mixer-control set the mixer control [Master*|...]\n"
- " -i mixer-index set the mixer index [0*|...]\n"
- " *) default option\n");
+ printf(" -d output-device set the output device, default is \"default\".\n"
+ " -c mixer-control set the mixer control name, default is to use no mixer.\n"
+ " -m mixer-device set the mixer device, default is the output device.\n"
+ " -i mixer-index set the mixer index, default is 0.\n");
+ system("if [ -d /proc/asound ] ; then echo \" hardware output devices:\" ; ls -al /proc/asound/ 2>/dev/null | grep '\\->' | tr -s ' ' | cut -d ' ' -f 9 | while read line; do echo \" \\\"hw:$line\\\"\" ; done ; fi");
}
void set_alsa_out_dev(char *dev) { alsa_out_dev = dev; }
+// assuming pthread cancellation is disabled
int open_mixer() {
+ int response = 0;
if (hardware_mixer) {
debug(3, "Open Mixer");
int ret = 0;
@@ -128,27 +207,41 @@ int open_mixer() {
snd_mixer_selem_id_set_index(alsa_mix_sid, alsa_mix_index);
snd_mixer_selem_id_set_name(alsa_mix_sid, alsa_mix_ctrl);
- if ((snd_mixer_open(&alsa_mix_handle, 0)) < 0)
- die("Failed to open mixer");
- debug(3, "Mixer device name is \"%s\".", alsa_mix_dev);
- if ((snd_mixer_attach(alsa_mix_handle, alsa_mix_dev)) < 0)
- die("Failed to attach mixer");
- if ((snd_mixer_selem_register(alsa_mix_handle, NULL, NULL)) < 0)
- die("Failed to register mixer element");
-
- ret = snd_mixer_load(alsa_mix_handle);
- if (ret < 0)
- die("Failed to load mixer element");
- debug(3, "Mixer Control name is \"%s\".", alsa_mix_ctrl);
- alsa_mix_elem = snd_mixer_find_selem(alsa_mix_handle, alsa_mix_sid);
- if (!alsa_mix_elem)
- die("Failed to find mixer element");
- return 1;
- } else {
- return 0;
+ if ((snd_mixer_open(&alsa_mix_handle, 0)) < 0) {
+ debug(1, "Failed to open mixer");
+ response = -1;
+ } else {
+ debug(3, "Mixer device name is \"%s\".", alsa_mix_dev);
+ if ((snd_mixer_attach(alsa_mix_handle, alsa_mix_dev)) < 0) {
+ debug(1, "Failed to attach mixer");
+ response = -2;
+ } else {
+ if ((snd_mixer_selem_register(alsa_mix_handle, NULL, NULL)) < 0) {
+ debug(1, "Failed to register mixer element");
+ response = -3;
+ } else {
+ ret = snd_mixer_load(alsa_mix_handle);
+ if (ret < 0) {
+ debug(1, "Failed to load mixer element");
+ response = -4;
+ } else {
+ debug(3, "Mixer Control name is \"%s\".", alsa_mix_ctrl);
+ alsa_mix_elem = snd_mixer_find_selem(alsa_mix_handle, alsa_mix_sid);
+ if (!alsa_mix_elem) {
+ warn("failed to find mixer control \"%s\".", alsa_mix_ctrl);
+ response = -5;
+ } else {
+ response = 1; // we found a hardware mixer and successfully opened it
+ }
+ }
+ }
+ }
+ }
}
+ return response;
}
+// assuming pthread cancellation is disabled
void close_mixer() {
if (alsa_mix_handle) {
snd_mixer_close(alsa_mix_handle);
@@ -156,6 +249,7 @@ void close_mixer() {
}
}
+// assuming pthread cancellation is disabled
void do_snd_mixer_selem_set_playback_dB_all(snd_mixer_elem_t *mix_elem, double vol) {
if (snd_mixer_selem_set_playback_dB_all(mix_elem, vol, 0) != 0) {
debug(1, "Can't set playback volume accurately to %f dB.", vol);
@@ -165,353 +259,136 @@ void do_snd_mixer_selem_set_playback_dB_all(snd_mixer_elem_t *mix_elem, double v
}
}
-static int init(int argc, char **argv) {
- debug_mutex_lock(&alsa_mutex, 1000, 1);
- // debug(2,"audio_alsa init called.");
- const char *str;
- int value;
- // double dvalue;
-
- // set up default values first
- set_period_size_request = 0;
- set_buffer_size_request = 0;
- config.alsa_use_hardware_mute = 0; // don't use it by default
-
- config.audio_backend_latency_offset = 0;
- config.audio_backend_buffer_desired_length = 0.15;
-
- // get settings from settings file first, allow them to be overridden by
- // command line options
-
- // do the "general" audio options. Note, these options are in the "general" stanza!
- parse_general_audio_options();
-
- if (config.cfg != NULL) {
-
- /* Get the Output Device Name. */
- if (config_lookup_string(config.cfg, "alsa.output_device", &str)) {
- alsa_out_dev = (char *)str;
- }
-
- /* Get the Mixer Type setting. */
-
- if (config_lookup_string(config.cfg, "alsa.mixer_type", &str)) {
- inform("The alsa mixer_type setting is deprecated and has been ignored. "
- "FYI, using the \"mixer_control_name\" setting automatically "
- "chooses a hardware mixer.");
- }
-
- /* Get the Mixer Device Name. */
- if (config_lookup_string(config.cfg, "alsa.mixer_device", &str)) {
- alsa_mix_dev = (char *)str;
- }
-
- /* Get the Mixer Control Name. */
- if (config_lookup_string(config.cfg, "alsa.mixer_control_name", &str)) {
- alsa_mix_ctrl = (char *)str;
- hardware_mixer = 1;
- }
-
- /* Get the disable_synchronization setting. */
- if (config_lookup_string(config.cfg, "alsa.disable_synchronization", &str)) {
- if (strcasecmp(str, "no") == 0)
- config.no_sync = 0;
- else if (strcasecmp(str, "yes") == 0)
- config.no_sync = 1;
- else {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("Invalid disable_synchronization option choice \"%s\". It should be \"yes\" or \"no\"");
- }
- }
-
- /* Get the mute_using_playback_switch setting. */
- if (config_lookup_string(config.cfg, "alsa.mute_using_playback_switch", &str)) {
- inform("The alsa \"mute_using_playback_switch\" setting is deprecated. "
- "Please use the \"use_hardware_mute_if_available\" setting instead.");
- if (strcasecmp(str, "no") == 0)
- config.alsa_use_hardware_mute = 0;
- else if (strcasecmp(str, "yes") == 0)
- config.alsa_use_hardware_mute = 1;
- else {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("Invalid mute_using_playback_switch option choice \"%s\". It should be \"yes\" or "
- "\"no\"");
- }
- }
-
- /* Get the use_hardware_mute_if_available setting. */
- if (config_lookup_string(config.cfg, "alsa.use_hardware_mute_if_available", &str)) {
- if (strcasecmp(str, "no") == 0)
- config.alsa_use_hardware_mute = 0;
- else if (strcasecmp(str, "yes") == 0)
- config.alsa_use_hardware_mute = 1;
- else {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("Invalid use_hardware_mute_if_available option choice \"%s\". It should be \"yes\" or "
- "\"no\"");
- }
- }
-
- /* Get the output format, using the same names as aplay does*/
- if (config_lookup_string(config.cfg, "alsa.output_format", &str)) {
- if (strcasecmp(str, "S16") == 0)
- config.output_format = SPS_FORMAT_S16;
- else if (strcasecmp(str, "S24") == 0)
- config.output_format = SPS_FORMAT_S24;
- else if (strcasecmp(str, "S24_3LE") == 0)
- config.output_format = SPS_FORMAT_S24_3LE;
- else if (strcasecmp(str, "S24_3BE") == 0)
- config.output_format = SPS_FORMAT_S24_3BE;
- else if (strcasecmp(str, "S32") == 0)
- config.output_format = SPS_FORMAT_S32;
- else if (strcasecmp(str, "U8") == 0)
- config.output_format = SPS_FORMAT_U8;
- else if (strcasecmp(str, "S8") == 0)
- config.output_format = SPS_FORMAT_S8;
- else {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("Invalid output format \"%s\". It should be \"U8\", \"S8\", \"S16\", \"S24\", "
- "\"S24_3LE\", \"S24_3BE\" or "
- "\"S32\"",
- str);
- }
- }
-
- /* Get the output rate, which must be a multiple of 44,100*/
- if (config_lookup_int(config.cfg, "alsa.output_rate", &value)) {
- debug(1, "alsa output rate is %d frames per second", value);
- switch (value) {
- case 44100:
- case 88200:
- case 176400:
- case 352800:
- config.output_rate = value;
- break;
- default:
- debug_mutex_unlock(&alsa_mutex, 3);
- die("Invalid output rate \"%d\". It should be a multiple of 44,100 up to 352,800", value);
- }
- }
-
- /* Get the use_mmap_if_available setting. */
- if (config_lookup_string(config.cfg, "alsa.use_mmap_if_available", &str)) {
- if (strcasecmp(str, "no") == 0)
- config.no_mmap = 1;
- else if (strcasecmp(str, "yes") == 0)
- config.no_mmap = 0;
- else {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("Invalid use_mmap_if_available option choice \"%s\". It should be \"yes\" or \"no\"");
- }
- }
- /* Get the optional period size value */
- if (config_lookup_int(config.cfg, "alsa.period_size", &value)) {
- set_period_size_request = 1;
- debug(1, "Value read for period size is %d.", value);
- if (value < 0) {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("Invalid alsa period size setting \"%d\". It "
- "must be greater than 0.",
- value);
- } else {
- period_size_requested = value;
- }
- }
-
- /* Get the optional buffer size value */
- if (config_lookup_int(config.cfg, "alsa.buffer_size", &value)) {
- set_buffer_size_request = 1;
- debug(1, "Value read for buffer size is %d.", value);
- if (value < 0) {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("Invalid alsa buffer size setting \"%d\". It "
- "must be greater than 0.",
- value);
- } else {
- buffer_size_requested = value;
- }
- }
- }
-
- optind = 1; // optind=0 is equivalent to optind=1 plus special behaviour
- argv--; // so we shift the arguments to satisfy getopt()
- argc++;
- // some platforms apparently require optreset = 1; - which?
- int opt;
- while ((opt = getopt(argc, argv, "d:t:m:c:i:")) > 0) {
- switch (opt) {
- case 'd':
- alsa_out_dev = optarg;
- break;
-
- case 't':
- inform("The alsa backend -t option is deprecated and has been ignored. "
- "FYI, using the -c option automatically chooses a hardware "
- "mixer.");
- break;
-
- case 'm':
- alsa_mix_dev = optarg;
- break;
- case 'c':
- alsa_mix_ctrl = optarg;
- hardware_mixer = 1;
- break;
- case 'i':
- alsa_mix_index = strtol(optarg, NULL, 10);
- break;
- default:
- help();
- debug_mutex_unlock(&alsa_mutex, 3);
- die("Invalid audio option -%c specified", opt);
- }
- }
-
- if (optind < argc) {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("Invalid audio argument: %s", argv[optind]);
- }
-
- debug(1, "alsa output device name is \"%s\".", alsa_out_dev);
-
- if (hardware_mixer) {
-
- if (alsa_mix_dev == NULL)
- alsa_mix_dev = alsa_out_dev;
-
- // Open mixer
-
- open_mixer();
-
- 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 = &parameters; // 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(1, "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
- debug(1, "note: 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) {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("Cannot open control \"%s\"", alsa_mix_dev);
- }
- if (snd_ctl_elem_id_malloc(&elem_id) < 0) {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("Cannot allocate memory for control \"%s\"", alsa_mix_dev);
- }
- 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, "Volume control \"%s\" has 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 = &parameters; // likewise the parameters stuff
- } else {
- debug(1, "Cannot get the dB range from the volume control \"%s\"", alsa_mix_ctrl);
- }
+void actual_close_alsa_device() {
+ debug(1, "actual close");
+ 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.",
+ derr, snd_strerror(derr));
- /*
- 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 = &parameters; // 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();
- } else {
- // debug(1, "Has no mixer and thus no hardware mute.");
+ if ((derr = snd_pcm_close(alsa_handle)))
+ debug(1, "Error %d (\"%s\") closing the output device.", derr, snd_strerror(derr));
+ alsa_handle = NULL;
}
-
- alsa_mix_handle = NULL;
- debug_mutex_unlock(&alsa_mutex, 3);
- return 0;
}
-static void deinit(void) {
- // debug(2,"audio_alsa deinit called.");
- stop();
-}
-
-int open_alsa_device(void) {
+// This array is a sequence of the output rates to be tried if automatic speed selection is requested.
+// There is no benefit to upconverting the frame rate, other than for compatibility.
+// The lowest rate that the DAC is capable of is chosen.
+
+unsigned int auto_speed_output_rates[] = {
+ 44100,
+ 88200,
+ 176400,
+ 352800,
+};
+
+// This array is of all the formats known to Shairport Sync, in order of the SPS_FORMAT definitions, with their equivalent alsa codes and their frame sizes.
+// If just one format is requested, then its entry is searched for in the array and checked on the device
+// If auto format is requested, then each entry in turn is tried until a working format is found.
+// So, it should be in the search order.
+
+ format_record fr[] = {
+ {SND_PCM_FORMAT_UNKNOWN,0}, // unknown
+ {SND_PCM_FORMAT_S8,2},
+ {SND_PCM_FORMAT_U8,2},
+ {SND_PCM_FORMAT_S16,4},
+ {SND_PCM_FORMAT_S16_LE,4},
+ {SND_PCM_FORMAT_S16_BE,4},
+ {SND_PCM_FORMAT_S24,4},
+ {SND_PCM_FORMAT_S24_LE,8},
+ {SND_PCM_FORMAT_S24_BE,8},
+ {SND_PCM_FORMAT_S24_3LE,6},
+ {SND_PCM_FORMAT_S24_3BE,6},
+ {SND_PCM_FORMAT_S32,8},
+ {SND_PCM_FORMAT_S32_LE,8},
+ {SND_PCM_FORMAT_S32_BE,8},
+ {SND_PCM_FORMAT_UNKNOWN,0}, // auto
+ {SND_PCM_FORMAT_UNKNOWN,0}, // illegal
+ };
+
+ // This array is the sequence of formats to be tried if automatic selection of the format is requested.
+ // Ideally, audio should pass through Shairport Sync unaltered, apart from occasional interpolation.
+ // If the user chooses a hardware mixer, then audio could go straight through, unaltered, as signed 16 bit stereo.
+ // However, the user might, at any point, select an option that requires modification, such as stereo to mono mixing,
+ // additional volume attenuation, convolution, and so on. For this reason,
+ // we look for the greatest depth the DAC is capable of, since upconverting it is completely lossless.
+ // If audio processing is required, then the dither that must be added will
+ // be added at the lowest possible level.
+ // Hence, selecting the greatest bit depth is always either beneficial or neutral.
+
+ enum sps_format_t auto_format_check_sequence[] = {
+ SPS_FORMAT_S32,
+ SPS_FORMAT_S32_LE,
+ SPS_FORMAT_S32_BE,
+ SPS_FORMAT_S24,
+ SPS_FORMAT_S24_LE,
+ SPS_FORMAT_S24_BE,
+ SPS_FORMAT_S24_3LE,
+ SPS_FORMAT_S24_3BE,
+ SPS_FORMAT_S16,
+ SPS_FORMAT_S16_LE,
+ SPS_FORMAT_S16_BE,
+ SPS_FORMAT_S8,
+ SPS_FORMAT_U8,
+ };
+
+// assuming pthread cancellation is disabled
+// if do_auto_setting is true and auto format or auto speed has been requested,
+// select the settings as appropriate and store them
+int actual_open_alsa_device(int do_auto_setup) {
// the alsa mutex is already acquired when this is called
-
const snd_pcm_uframes_t minimal_buffer_headroom =
352 * 2; // we accept this much headroom in the hardware buffer, but we'll
// accept less
/*
const snd_pcm_uframes_t requested_buffer_headroom =
minimal_buffer_headroom + 2048; // we ask for this much headroom in the
- // hardware buffer, but we'll accept less
+ // hardware buffer, but we'll accept
+ less
*/
int ret, dir = 0;
- unsigned int my_sample_rate = desired_sample_rate;
+ unsigned int actual_sample_rate; // this will be given the rate requested and will be given the actual rate
// snd_pcm_uframes_t frames = 441 * 10;
snd_pcm_uframes_t actual_buffer_length;
snd_pcm_access_t access;
- // ensure no calls are made to the alsa device enquiring about the buffer length if
+ // ensure no calls are made to the alsa device enquiring about the buffer
+ // length if
// synchronisation is disabled.
if (config.no_sync != 0)
audio_alsa.delay = NULL;
- // ensure no calls are made to the alsa device enquiring about the buffer length if
+ // ensure no calls are made to the alsa device enquiring about the buffer
+ // length if
// synchronisation is disabled.
if (config.no_sync != 0)
audio_alsa.delay = NULL;
ret = snd_pcm_open(&alsa_handle, alsa_out_dev, SND_PCM_STREAM_PLAYBACK, 0);
- if (ret < 0)
- return (ret);
+ if (ret < 0) {
+ if (ret == -ENOENT) {
+ warn("the alsa output_device \"%s\" can not be found.", alsa_out_dev);
+ } else {
+ char errorstring[1024];
+ strerror_r(-ret, (char *)errorstring, sizeof(errorstring));
+ warn("alsa: error %d (\"%s\") opening alsa device \"%s\".", ret, (char *)errorstring, alsa_out_dev);
+ }
+ return ret;
+ }
snd_pcm_hw_params_alloca(&alsa_params);
+ snd_pcm_sw_params_alloca(&alsa_swparams);
ret = snd_pcm_hw_params_any(alsa_handle, alsa_params);
if (ret < 0) {
- debug_mutex_unlock(&alsa_mutex, 3);
- ;
die("audio_alsa: Broken configuration for device \"%s\": no configurations "
- "available",
- alsa_out_dev);
+ "available",
+ alsa_out_dev);
+ return ret;
}
if ((config.no_mmap == 0) &&
@@ -534,70 +411,118 @@ int open_alsa_device(void) {
ret = snd_pcm_hw_params_set_access(alsa_handle, alsa_params, access);
if (ret < 0) {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("audio_alsa: Access type not available for device \"%s\": %s", alsa_out_dev,
- snd_strerror(ret));
- }
- snd_pcm_format_t sf;
- switch (sample_format) {
- case SPS_FORMAT_S8:
- sf = SND_PCM_FORMAT_S8;
- break;
- case SPS_FORMAT_U8:
- sf = SND_PCM_FORMAT_U8;
- break;
- case SPS_FORMAT_S16:
- sf = SND_PCM_FORMAT_S16;
- break;
- case SPS_FORMAT_S24:
- sf = SND_PCM_FORMAT_S24;
- break;
- case SPS_FORMAT_S24_3LE:
- sf = SND_PCM_FORMAT_S24_3LE;
- break;
- case SPS_FORMAT_S24_3BE:
- sf = SND_PCM_FORMAT_S24_3BE;
- break;
- case SPS_FORMAT_S32:
- sf = SND_PCM_FORMAT_S32;
- break;
- default:
- debug_mutex_unlock(&alsa_mutex, 3);
- sf = SND_PCM_FORMAT_S16; // this is just to quieten a compiler warning
- die("Unsupported output format at audio_alsa.c");
- }
- ret = snd_pcm_hw_params_set_format(alsa_handle, alsa_params, sf);
- if (ret < 0) {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("audio_alsa: Sample format %d not available for device \"%s\": %s", sample_format,
- alsa_out_dev, snd_strerror(ret));
+ warn("audio_alsa: Access type not available for device \"%s\": %s", alsa_out_dev,
+ snd_strerror(ret));
+ return ret;
}
+
ret = snd_pcm_hw_params_set_channels(alsa_handle, alsa_params, 2);
if (ret < 0) {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("audio_alsa: Channels count (2) not available for device \"%s\": %s", alsa_out_dev,
- snd_strerror(ret));
+ warn("audio_alsa: Channels count (2) not available for device \"%s\": %s", alsa_out_dev,
+ snd_strerror(ret));
+ return ret;
}
- ret = snd_pcm_hw_params_set_rate_near(alsa_handle, alsa_params, &my_sample_rate, &dir);
- if (ret < 0) {
- die("audio_alsa: Rate %iHz not available for playback: %s", desired_sample_rate,
- snd_strerror(ret));
+ snd_pcm_format_t sf;
+
+ if ((do_auto_setup == 0) || (config.output_format_auto_requested == 0)) { // no auto format
+ if ((config.output_format > SPS_FORMAT_UNKNOWN) && (config.output_format < SPS_FORMAT_AUTO)) {
+ sf = fr[config.output_format].alsa_code;
+ frame_size = fr[config.output_format].frame_size;
+ } else {
+ warn("alsa: unexpected output format %d. Set to S16_LE.",config.output_format);
+ config.output_format = SPS_FORMAT_S16_LE;
+ sf = fr[config.output_format].alsa_code;
+ frame_size = fr[config.output_format].frame_size;
+ }
+ ret = snd_pcm_hw_params_set_format(alsa_handle, alsa_params, sf);
+ if (ret < 0) {
+ warn("audio_alsa: Alsa sample format %d not available for device \"%s\": %s", sf,
+ alsa_out_dev, snd_strerror(ret));
+ return ret;
+ }
+ } else { // auto format
+ int number_of_formats_to_try;
+ enum sps_format_t *formats;
+ formats = auto_format_check_sequence;
+ number_of_formats_to_try = sizeof(auto_format_check_sequence)/sizeof(sps_format_t);
+ int i = 0;
+ int format_found = 0;
+ enum sps_format_t trial_format = SPS_FORMAT_UNKNOWN;
+ while ((i < number_of_formats_to_try) && (format_found == 0)) {
+ trial_format = formats[i];
+ sf = fr[trial_format].alsa_code;
+ frame_size = fr[trial_format].frame_size;
+ ret = snd_pcm_hw_params_set_format(alsa_handle, alsa_params, sf);
+ if (ret == 0)
+ format_found = 1;
+ else
+ i++;
+ }
+ if (ret == 0) {
+ config.output_format = trial_format;
+ debug(1,"alsa: output format chosen is \"%s\".",sps_format_description_string(config.output_format));
+ } else {
+ warn("audio_alsa: Could not automatically set the output format for device \"%s\": %s",
+ alsa_out_dev, snd_strerror(ret));
+ return ret;
+ }
}
-
+
+ if ((do_auto_setup == 0) || (config.output_rate_auto_requested == 0)) { // no auto format
+ actual_sample_rate = config.output_rate; // this is the requested rate -- it'll be changed to the actual rate
+ ret = snd_pcm_hw_params_set_rate_near(alsa_handle, alsa_params, &actual_sample_rate, &dir);
+ if (ret < 0) {
+ warn("audio_alsa: Rate %iHz not available for playback: %s", config.output_rate,
+ snd_strerror(ret));
+ return ret;
+ }
+ } else {
+ int number_of_speeds_to_try;
+ unsigned int *speeds;
+
+ speeds = auto_speed_output_rates;
+ number_of_speeds_to_try = sizeof(auto_speed_output_rates)/sizeof(int);
+
+ int i = 0;
+ int speed_found = 0;
+
+ while ((i < number_of_speeds_to_try) && (speed_found == 0)) {
+ actual_sample_rate = speeds[i];
+ ret = snd_pcm_hw_params_set_rate_near(alsa_handle, alsa_params, &actual_sample_rate, &dir);
+ if (ret == 0) {
+ speed_found = 1;
+ if (actual_sample_rate != speeds[i])
+ warn("Speed requested: %d. Speed available: %d.",speeds[i],actual_sample_rate);
+ } else {
+ i++;
+ }
+ }
+ if (ret == 0) {
+ config.output_rate = actual_sample_rate;
+ debug(1,"alsa: output speed chosen is %d.",config.output_rate);
+ } else {
+ warn("audio_alsa: Could not automatically set the output rate for device \"%s\": %s",
+ alsa_out_dev, snd_strerror(ret));
+ return ret;
+ }
+ }
+
if (set_period_size_request != 0) {
debug(1, "Attempting to set the period size");
ret = snd_pcm_hw_params_set_period_size_near(alsa_handle, alsa_params, &period_size_requested,
&dir);
if (ret < 0) {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("audio_alsa: cannot set period size of %lu: %s", period_size_requested,
- snd_strerror(ret));
+ warn("audio_alsa: cannot set period size of %lu: %s", period_size_requested,
+ snd_strerror(ret));
+ return ret;
+ } else {
snd_pcm_uframes_t actual_period_size;
snd_pcm_hw_params_get_period_size(alsa_params, &actual_period_size, &dir);
if (actual_period_size != period_size_requested)
- inform("Actual period size set to a different value than requested. Requested: %lu, actual "
+ inform("Actual period size set to a different value than requested. "
+ "Requested: %lu, actual "
"setting: %lu",
period_size_requested, actual_period_size);
}
@@ -607,35 +532,91 @@ int open_alsa_device(void) {
debug(1, "Attempting to set the buffer size to %lu", buffer_size_requested);
ret = snd_pcm_hw_params_set_buffer_size_near(alsa_handle, alsa_params, &buffer_size_requested);
if (ret < 0) {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("audio_alsa: cannot set buffer size of %lu: %s", buffer_size_requested,
- snd_strerror(ret));
+ warn("audio_alsa: cannot set buffer size of %lu: %s", buffer_size_requested,
+ snd_strerror(ret));
+ return ret;
+ } else {
+ snd_pcm_uframes_t actual_buffer_size;
+ snd_pcm_hw_params_get_buffer_size(alsa_params, &actual_buffer_size);
+ if (actual_buffer_size != buffer_size_requested)
+ inform("Actual period size set to a different value than requested. "
+ "Requested: %lu, actual "
+ "setting: %lu",
+ buffer_size_requested, actual_buffer_size);
}
+ }
+
+ ret = snd_pcm_hw_params(alsa_handle, alsa_params);
+ if (ret < 0) {
+ warn("audio_alsa: Unable to set hw parameters for device \"%s\": %s.", alsa_out_dev,
+ snd_strerror(ret));
+ return ret;
+ }
+
+ // check parameters after attempting to set them
+
+ if (set_period_size_request != 0) {
+ snd_pcm_uframes_t actual_period_size;
+ snd_pcm_hw_params_get_period_size(alsa_params, &actual_period_size, &dir);
+ if (actual_period_size != period_size_requested)
+ inform("Actual period size set to a different value than requested. "
+ "Requested: %lu, actual "
+ "setting: %lu",
+ period_size_requested, actual_period_size);
+ }
+
+ if (set_buffer_size_request != 0) {
snd_pcm_uframes_t actual_buffer_size;
snd_pcm_hw_params_get_buffer_size(alsa_params, &actual_buffer_size);
if (actual_buffer_size != buffer_size_requested)
- inform("Actual period size set to a different value than requested. Requested: %lu, actual "
+ inform("Actual period size set to a different value than requested. "
+ "Requested: %lu, actual "
"setting: %lu",
buffer_size_requested, actual_buffer_size);
}
- ret = snd_pcm_hw_params(alsa_handle, alsa_params);
+ if (actual_sample_rate != config.output_rate) {
+ warn("Can't set the D/A converter to sample rate %d.", config.output_rate);
+ return -EINVAL;
+ }
+
+ use_monotonic_clock = snd_pcm_hw_params_is_monotonic(alsa_params);
+
+ ret = snd_pcm_hw_params_get_buffer_size(alsa_params, &actual_buffer_length);
if (ret < 0) {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("audio_alsa: Unable to set hw parameters for device \"%s\": %s.", alsa_out_dev,
- snd_strerror(ret));
+ warn("audio_alsa: Unable to get hw buffer length for device \"%s\": %s.", alsa_out_dev,
+ snd_strerror(ret));
+ return ret;
}
- if (my_sample_rate != desired_sample_rate) {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("Can't set the D/A converter to %d.", desired_sample_rate);
+ ret = snd_pcm_sw_params_current(alsa_handle, alsa_swparams);
+ if (ret < 0) {
+ warn("audio_alsa: Unable to get current sw parameters for device \"%s\": "
+ "%s.",
+ alsa_out_dev, snd_strerror(ret));
+ return ret;
}
- ret = snd_pcm_hw_params_get_buffer_size(alsa_params, &actual_buffer_length);
+ ret = snd_pcm_sw_params_set_tstamp_mode(alsa_handle, alsa_swparams, SND_PCM_TSTAMP_ENABLE);
if (ret < 0) {
- debug_mutex_unlock(&alsa_mutex, 3);
- die("audio_alsa: Unable to get hw buffer length for device \"%s\": %s.", alsa_out_dev,
- snd_strerror(ret));
+ warn("audio_alsa: Can't enable timestamp mode of device: \"%s\": %s.", alsa_out_dev,
+ snd_strerror(ret));
+ return ret;
+ }
+
+ /* write the sw parameters */
+ ret = snd_pcm_sw_params(alsa_handle, alsa_swparams);
+ if (ret < 0) {
+ warn("audio_alsa: Unable to set software parameters of device: \"%s\": %s.", alsa_out_dev,
+ snd_strerror(ret));
+ return ret;
+ }
+
+ ret = snd_pcm_prepare(alsa_handle);
+ if (ret < 0) {
+ warn("audio_alsa: Unable to prepare the device: \"%s\": %s.", alsa_out_dev,
+ snd_strerror(ret));
+ return ret;
}
if (actual_buffer_length < config.audio_backend_buffer_desired_length + minimal_buffer_headroom) {
@@ -661,10 +642,21 @@ int open_alsa_device(void) {
buffer_size);
}
*/
- debug(1, "The alsa buffer is smaller (%lu bytes) than the desired backend buffer "
+ 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);
}
+
+
+ if (config.use_precision_timing == YNA_YES)
+ delay_and_status = precision_delay_and_status;
+ else if (config.use_precision_timing == YNA_AUTO) {
+ if (precision_delay_available()) {
+ delay_and_status = precision_delay_and_status;
+ debug(2,"alsa: precision timing selected for \"auto\" mode");
+ }
+ }
if (alsa_characteristics_already_listed == 0) {
alsa_characteristics_already_listed = 1;
@@ -681,14 +673,17 @@ int open_alsa_device(void) {
debug(log_level, "PCM handle name = '%s'", snd_pcm_name(alsa_handle));
- // ret = snd_pcm_hw_params_any(alsa_handle, alsa_params);
- // if (ret < 0) {
- // die("audio_alsa: Cannpot get configuration for device \"%s\": no
+ // ret = snd_pcm_hw_params_any(alsa_handle, alsa_params);
+ // if (ret < 0) {
+ // die("audio_alsa: Cannpot get configuration for
+ // device
+ //\"%s\":
+ // no
// configurations
//"
- // "available",
- // alsa_out_dev);
- // }
+ // "available",
+ // alsa_out_dev);
+ // }
debug(log_level, "alsa device parameters:");
@@ -793,144 +788,933 @@ int open_alsa_device(void) {
break;
}
}
+ return 0;
+}
- return (0);
+int open_alsa_device(int do_auto_setup) {
+ int result;
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
+ result = actual_open_alsa_device(do_auto_setup);
+ pthread_setcancelstate(oldState, NULL);
+ return result;
}
-static void start(int i_sample_rate, int i_sample_format) {
- // debug(2,"audio_alsa start called.");
- if (i_sample_rate == 0)
- desired_sample_rate = 44100; // default
- else
- desired_sample_rate = i_sample_rate; // must be a variable
-
- if (i_sample_format == 0)
- sample_format = SPS_FORMAT_S16; // default
- else
- sample_format = i_sample_format;
+int do_alsa_device_init_if_needed() {
+ int response = 0;
+ // do any alsa device initialisation (general case) if needed
+ // 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 first use
+ if (alsa_device_initialised == 0) {
+ alsa_device_initialised = 1;
+ if (hardware_mixer) {
+ debug(2, "alsa: hardware mixer init");
+ 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 = &parameters; // 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 {
+ 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 = &parameters; // likewise the parameters stuff
+ } else {
+ 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 = &parameters; // 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);
+ }
+ }
+ return response;
}
-int delay(long *the_delay) {
- // snd_pcm_sframes_t is a signed long -- hence the return of a "long"
- int reply;
- // debug(3,"audio_alsa delay called.");
- if (alsa_handle == NULL) {
- return -ENODEV;
- } else {
- debug_mutex_lock(&alsa_mutex, 10000, 1);
- int derr;
- if (snd_pcm_state(alsa_handle) == SND_PCM_STATE_RUNNING) {
- *the_delay = 0; // just to see what happens
- reply = snd_pcm_delay(alsa_handle, the_delay);
- if (reply != 0) {
- debug(1, "Error %d in delay(): \"%s\". Delay reported is %d frames.", reply,
- snd_strerror(reply), *the_delay);
- snd_pcm_recover(alsa_handle, reply, 1);
+static int init(int argc, char **argv) {
+ // for debugging
+ snd_output_stdio_attach(&output, stdout, 0);
+
+ // debug(2,"audio_alsa init called.");
+ int response = 0; // this will be what we return to the caller.
+ alsa_device_initialised = 0;
+ const char *str;
+ int value;
+ // double dvalue;
+
+ // set up default values first
+
+ alsa_backend_state = abm_disconnected; // startup state
+ debug(2, "alsa: init() -- alsa_backend_state => abm_disconnected.");
+ set_period_size_request = 0;
+ set_buffer_size_request = 0;
+ config.alsa_use_hardware_mute = 0; // don't use it by default
+
+ config.audio_backend_latency_offset = 0;
+ config.audio_backend_buffer_desired_length = 0.200;
+ config.audio_backend_buffer_interpolation_threshold_in_seconds =
+ 0.120; // below this, basic interpolation will be used to save time.
+ config.alsa_maximum_stall_time = 0.200; // 200 milliseconds -- if it takes longer, it's a problem
+ config.audio_backend_silence_threshold =
+ 0.040; // start sending silent frames if the delay goes below this time
+ config.audio_backend_silence_scan_interval = 0.004; // check silence threshold this often
+
+ stall_monitor_error_threshold =
+ (uint64_t)1000000 * config.alsa_maximum_stall_time; // stall time max to microseconds;
+ stall_monitor_error_threshold = (stall_monitor_error_threshold << 32) / 1000000; // now in fp form
+ debug(1,
+ "stall_monitor_error_threshold is 0x%" PRIx64 ", with alsa_maximum_stall_time of %f sec.",
+ stall_monitor_error_threshold, config.alsa_maximum_stall_time);
+
+ stall_monitor_start_time = 0;
+ stall_monitor_frame_count = 0;
+
+ config.disable_standby_mode = disable_standby_off;
+ config.keep_dac_busy = 0;
+ config.use_precision_timing = YNA_AUTO;
+
+ // get settings from settings file first, allow them to be overridden by
+ // command line options
+
+ // do the "general" audio options. Note, these options are in the "general"
+ // stanza!
+ parse_general_audio_options();
+
+ if (config.cfg != NULL) {
+ double dvalue;
+
+ /* Get the Output Device Name. */
+ if (config_lookup_string(config.cfg, "alsa.output_device", &str)) {
+ alsa_out_dev = (char *)str;
+ }
+
+ /* Get the Mixer Type setting. */
+
+ if (config_lookup_string(config.cfg, "alsa.mixer_type", &str)) {
+ inform("The alsa mixer_type setting is deprecated and has been ignored. "
+ "FYI, using the \"mixer_control_name\" setting automatically "
+ "chooses a hardware mixer.");
+ }
+
+ /* Get the Mixer Device Name. */
+ if (config_lookup_string(config.cfg, "alsa.mixer_device", &str)) {
+ alsa_mix_dev = (char *)str;
+ }
+
+ /* Get the Mixer Control Name. */
+ if (config_lookup_string(config.cfg, "alsa.mixer_control_name", &str)) {
+ alsa_mix_ctrl = (char *)str;
+ hardware_mixer = 1;
+ }
+
+ /* Get the disable_synchronization setting. */
+ if (config_lookup_string(config.cfg, "alsa.disable_synchronization", &str)) {
+ if (strcasecmp(str, "no") == 0)
+ config.no_sync = 0;
+ else if (strcasecmp(str, "yes") == 0)
+ config.no_sync = 1;
+ else {
+ warn("Invalid disable_synchronization option choice \"%s\". It should "
+ "be \"yes\" or "
+ "\"no\". It is set to \"no\".");
+ config.no_sync = 0;
}
- } else if (snd_pcm_state(alsa_handle) == SND_PCM_STATE_PREPARED) {
- *the_delay = 0;
- reply = 0; // no error
- } else {
- if (snd_pcm_state(alsa_handle) == SND_PCM_STATE_XRUN) {
- *the_delay = 0;
- reply = 0; // no error
+ }
+
+ /* Get the mute_using_playback_switch setting. */
+ if (config_lookup_string(config.cfg, "alsa.mute_using_playback_switch", &str)) {
+ inform("The alsa \"mute_using_playback_switch\" setting is deprecated. "
+ "Please use the \"use_hardware_mute_if_available\" setting instead.");
+ if (strcasecmp(str, "no") == 0)
+ config.alsa_use_hardware_mute = 0;
+ else if (strcasecmp(str, "yes") == 0)
+ config.alsa_use_hardware_mute = 1;
+ else {
+ warn("Invalid mute_using_playback_switch option choice \"%s\". It "
+ "should be \"yes\" or "
+ "\"no\". It is set to \"no\".");
+ config.alsa_use_hardware_mute = 0;
+ }
+ }
+
+ /* Get the use_hardware_mute_if_available setting. */
+ if (config_lookup_string(config.cfg, "alsa.use_hardware_mute_if_available", &str)) {
+ if (strcasecmp(str, "no") == 0)
+ config.alsa_use_hardware_mute = 0;
+ else if (strcasecmp(str, "yes") == 0)
+ config.alsa_use_hardware_mute = 1;
+ else {
+ warn("Invalid use_hardware_mute_if_available option choice \"%s\". It "
+ "should be \"yes\" or "
+ "\"no\". It is set to \"no\".");
+ config.alsa_use_hardware_mute = 0;
+ }
+ }
+
+
+ /* Get the output format, using the same names as aplay does*/
+ if (config_lookup_string(config.cfg, "alsa.output_format", &str)) {
+ if (strcasecmp(str, "S16") == 0)
+ config.output_format = SPS_FORMAT_S16;
+ else if (strcasecmp(str, "S16_LE") == 0)
+ config.output_format = SPS_FORMAT_S16_LE;
+ else if (strcasecmp(str, "S16_BE") == 0)
+ config.output_format = SPS_FORMAT_S16_BE;
+ else if (strcasecmp(str, "S24") == 0)
+ config.output_format = SPS_FORMAT_S24;
+ else if (strcasecmp(str, "S24_LE") == 0)
+ config.output_format = SPS_FORMAT_S24_LE;
+ else if (strcasecmp(str, "S24_BE") == 0)
+ config.output_format = SPS_FORMAT_S24_BE;
+ else if (strcasecmp(str, "S24_3LE") == 0)
+ config.output_format = SPS_FORMAT_S24_3LE;
+ else if (strcasecmp(str, "S24_3BE") == 0)
+ config.output_format = SPS_FORMAT_S24_3BE;
+ else if (strcasecmp(str, "S32") == 0)
+ config.output_format = SPS_FORMAT_S32;
+ else if (strcasecmp(str, "S32_LE") == 0)
+ config.output_format = SPS_FORMAT_S32_LE;
+ else if (strcasecmp(str, "S32_BE") == 0)
+ config.output_format = SPS_FORMAT_S32_BE;
+ else if (strcasecmp(str, "U8") == 0)
+ config.output_format = SPS_FORMAT_U8;
+ else if (strcasecmp(str, "S8") == 0)
+ config.output_format = SPS_FORMAT_S8;
+ else if (strcasecmp(str, "auto") == 0)
+ config.output_format_auto_requested = 1;
+ else {
+ warn("Invalid output format \"%s\". It should be \"U8\", \"S8\", "
+ "\"S16\", \"S24\", \"S24_LE\", \"S24_BE\", "
+ "\"S24_3LE\", \"S24_3BE\" or "
+ "\"S32\", \"S32_LE\", \"S32_BE\". It is set to \"%s\".",
+ sps_format_description_string(config.output_format));
+ }
+ }
+
+ if (config_lookup_string(config.cfg, "alsa.output_rate", &str)) {
+ if (strcasecmp(str, "auto") == 0) {
+ config.output_rate_auto_requested = 1;
+ } else {
+ /* Get the output rate, which must be a multiple of 44,100*/
+ if (config_lookup_int(config.cfg, "alsa.output_rate", &value)) {
+ debug(1, "alsa output rate is %d frames per second", value);
+ switch (value) {
+ case 44100:
+ case 88200:
+ case 176400:
+ case 352800:
+ config.output_rate = value;
+ break;
+ default:
+ warn("Invalid output rate \"%d\". It should be \"auto\" or a multiple of 44,100 up "
+ "to 352,800. It is "
+ "set to %d.",
+ value,config.output_rate);
+ }
+ }
+ }
+ }
+
+ /* Get the use_mmap_if_available setting. */
+ if (config_lookup_string(config.cfg, "alsa.use_mmap_if_available", &str)) {
+ if (strcasecmp(str, "no") == 0)
+ config.no_mmap = 1;
+ else if (strcasecmp(str, "yes") == 0)
+ config.no_mmap = 0;
+ else {
+ warn("Invalid use_mmap_if_available option choice \"%s\". It should be "
+ "\"yes\" or \"no\". "
+ "It is set to \"yes\".");
+ config.no_mmap = 0;
+ }
+ }
+ /* Get the optional period size value */
+ if (config_lookup_int(config.cfg, "alsa.period_size", &value)) {
+ set_period_size_request = 1;
+ debug(1, "Value read for period size is %d.", value);
+ if (value < 0) {
+ warn("Invalid alsa period size setting \"%d\". It "
+ "must be greater than 0. No setting is made.",
+ value);
+ set_period_size_request = 0;
} else {
- reply = -EIO;
- debug(1, "Error -- ALSA delay(): bad state: %d.", snd_pcm_state(alsa_handle));
+ period_size_requested = value;
+ }
+ }
+
+ /* Get the optional buffer size value */
+ if (config_lookup_int(config.cfg, "alsa.buffer_size", &value)) {
+ set_buffer_size_request = 1;
+ debug(1, "Value read for buffer size is %d.", value);
+ if (value < 0) {
+ warn("Invalid alsa buffer size setting \"%d\". It "
+ "must be greater than 0. No setting is made.",
+ value);
+ set_buffer_size_request = 0;
+ } else {
+ buffer_size_requested = value;
+ }
+ }
+
+ /* Get the optional alsa_maximum_stall_time setting. */
+ if (config_lookup_float(config.cfg, "alsa.maximum_stall_time", &dvalue)) {
+ if (dvalue < 0.0) {
+ warn("Invalid alsa maximum write time setting \"%f\". It "
+ "must be greater than 0. Default is \"%f\". No setting is made.",
+ dvalue, config.alsa_maximum_stall_time);
+ } else {
+ config.alsa_maximum_stall_time = dvalue;
+ }
+ }
+
+
+ /* Get the optional disable_standby_mode setting. */
+ if (config_lookup_string(config.cfg, "alsa.disable_standby_mode", &str)) {
+ if ((strcasecmp(str, "no") == 0) || (strcasecmp(str, "off") == 0) || (strcasecmp(str, "never") == 0))
+ config.disable_standby_mode = disable_standby_off;
+ else if ((strcasecmp(str, "yes") == 0) || (strcasecmp(str, "on") == 0) || (strcasecmp(str, "always") == 0)) {
+ config.disable_standby_mode = disable_standby_always;
+ config.keep_dac_busy = 1;
+ } else if (strcasecmp(str, "auto") == 0)
+ config.disable_standby_mode = disable_standby_auto;
+ else {
+ warn("Invalid disable_standby_mode option choice \"%s\". It should be "
+ "\"always\", \"auto\" or \"never\". "
+ "It is set to \"never\".");
+ }
+ }
+
+
+ if (config_lookup_string(config.cfg, "alsa.use_precision_timing", &str)) {
+ if ((strcasecmp(str, "no") == 0) || (strcasecmp(str, "off") == 0) || (strcasecmp(str, "never") == 0))
+ config.use_precision_timing = YNA_NO;
+ else if ((strcasecmp(str, "yes") == 0) || (strcasecmp(str, "on") == 0) || (strcasecmp(str, "always") == 0)) {
+ config.use_precision_timing = YNA_YES;
+ config.keep_dac_busy = 1;
+ } else if (strcasecmp(str, "auto") == 0)
+ config.use_precision_timing = YNA_AUTO;
+ else {
+ warn("Invalid use_precision_timing option choice \"%s\". It should be "
+ "\"yes\", \"auto\" or \"no\". "
+ "It is set to \"auto\".");
+ }
+ }
+
+ debug(1, "alsa: disable_standby_mode is \"%s\".", config.disable_standby_mode == disable_standby_off ? "never" : config.disable_standby_mode == disable_standby_always ? "always" : "auto");
+ }
+
+ optind = 1; // optind=0 is equivalent to optind=1 plus special behaviour
+ argv--; // so we shift the arguments to satisfy getopt()
+ argc++;
+ // some platforms apparently require optreset = 1; - which?
+ int opt;
+ while ((opt = getopt(argc, argv, "d:t:m:c:i:")) > 0) {
+ switch (opt) {
+ case 'd':
+ alsa_out_dev = optarg;
+ break;
+
+ case 't':
+ inform("The alsa backend -t option is deprecated and has been ignored. "
+ "FYI, using the -c option automatically chooses a hardware "
+ "mixer.");
+ break;
+
+ case 'm':
+ alsa_mix_dev = optarg;
+ break;
+ case 'c':
+ alsa_mix_ctrl = optarg;
+ hardware_mixer = 1;
+ break;
+ case 'i':
+ alsa_mix_index = strtol(optarg, NULL, 10);
+ break;
+ default:
+ warn("Invalid audio option \"-%c\" specified -- ignored.", opt);
+ help();
+ }
+ }
+
+ if (optind < argc) {
+ warn("Invalid audio argument: \"%s\" -- ignored", argv[optind]);
+ }
+
+ debug(1, "alsa: output device name is \"%s\".", alsa_out_dev);
+
+ // so, now, if the option to keep the DAC running has been selected, start a
+ // thread to monitor the
+ // 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;
+}
+
+static void deinit(void) {
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
+ // debug(2,"audio_alsa deinit called.");
+ stop();
+ debug(2, "Cancel buffer monitor thread.");
+ pthread_cancel(alsa_buffer_monitor_thread);
+ debug(3, "Join buffer monitor thread.");
+ pthread_join(alsa_buffer_monitor_thread, NULL);
+ pthread_setcancelstate(oldState, NULL);
+}
+
+int set_mute_state() {
+ int response = 1; // some problem expected, e.g. no mixer or not allowed to use it or disconnected
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
+ pthread_cleanup_debug_mutex_lock(&alsa_mixer_mutex, 10000, 0);
+ if ((alsa_backend_state != abm_disconnected) && (config.alsa_use_hardware_mute == 1) &&
+ (open_mixer() == 1)) {
+ response = 0; // okay if actually using the mute facility
+ debug(2, "alsa: actually set_mute_state");
+ int mute = 0;
+ if ((mute_requested_externally != 0) || (mute_requested_internally != 0))
+ mute = 1;
+ if (mute == 1) {
+ debug(2, "alsa: hardware mute switched on");
+ if (snd_mixer_selem_has_playback_switch(alsa_mix_elem))
+ snd_mixer_selem_set_playback_switch_all(alsa_mix_elem, 0);
+ else {
+ volume_based_mute_is_active = 1;
+ do_snd_mixer_selem_set_playback_dB_all(alsa_mix_elem, alsa_mix_mute);
}
- if ((derr = snd_pcm_prepare(alsa_handle))) {
- snd_pcm_recover(alsa_handle, derr, 1);
- debug(1, "Error preparing after delay error: \"%s\".", snd_strerror(derr));
+ } else {
+ debug(2, "alsa: hardware mute switched off");
+ if (snd_mixer_selem_has_playback_switch(alsa_mix_elem))
+ snd_mixer_selem_set_playback_switch_all(alsa_mix_elem, 1);
+ else {
+ volume_based_mute_is_active = 0;
+ do_snd_mixer_selem_set_playback_dB_all(alsa_mix_elem, set_volume);
}
}
- debug_mutex_unlock(&alsa_mutex, 3);
- // here, occasionally pretend there's a problem with pcm_get_delay()
- // if ((random() % 100000) < 3) // keep it pretty rare
- // reply = -EPERM; // pretend something bad has happened
- return reply;
+ close_mixer();
}
+ debug_mutex_unlock(&alsa_mixer_mutex, 3); // release the mutex
+ pthread_cleanup_pop(0); // release the mutex
+ pthread_setcancelstate(oldState, NULL);
+ return response;
}
-static void play(void *buf, int samples) {
- // debug(3,"audio_alsa play called.");
+static void start(__attribute__((unused)) int i_sample_rate, __attribute__((unused)) int i_sample_format) {
+ debug(3, "audio_alsa start called.");
+
+ frame_index = 0;
+ measurement_data_is_valid = 0;
+
+ stall_monitor_start_time = 0;
+ stall_monitor_frame_count = 0;
+ if (alsa_device_initialised == 0) {
+ debug(2, "alsa: start() calling do_alsa_device_init_if_needed.");
+ do_alsa_device_init_if_needed();
+ }
+}
+
+int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, enum yndk_type *using_update_timestamps) {
int ret = 0;
- if (alsa_handle == NULL) {
- debug_mutex_lock(&alsa_mutex, 10000, 1);
- ret = open_alsa_device();
- if (ret == 0) {
- if (audio_alsa.volume)
- do_volume(set_volume);
- if (audio_alsa.mute)
- do_mute(0);
- }
- debug_mutex_unlock(&alsa_mutex, 3);
+ if (using_update_timestamps)
+ *using_update_timestamps = YNDK_NO;
+ *state = snd_pcm_state(alsa_handle);
+ if ((*state == SND_PCM_STATE_RUNNING) || (*state == SND_PCM_STATE_DRAINING)) {
+ ret = snd_pcm_delay(alsa_handle,delay);
+ } else {
+ // not running, thus no delay information, thus can't check for frame
+ // rates
+ frame_index = 0; // we'll be starting over...
+ measurement_data_is_valid = 0;
+ *delay = 0;
}
+
+ stall_monitor_start_time = 0; // zero if not initialised / not started / zeroed by flush
+ stall_monitor_frame_count = 0; // set to delay at start of time, incremented by any writes
+
+ return ret;
+}
+
+int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, enum yndk_type *using_update_timestamps) {
+ snd_pcm_status_t *alsa_snd_pcm_status;
+ snd_pcm_status_alloca(&alsa_snd_pcm_status);
+
+ if (using_update_timestamps)
+ *using_update_timestamps = YNDK_DONT_KNOW;
+
+ struct timespec tn; // time now
+ snd_htimestamp_t update_timestamp; // actually a struct timespec
+
+ int ret = snd_pcm_status(alsa_handle, alsa_snd_pcm_status);
if (ret == 0) {
- debug_mutex_lock(&alsa_mutex, 10000, 1);
- // snd_pcm_sframes_t current_delay = 0;
- int err;
- if (snd_pcm_state(alsa_handle) == SND_PCM_STATE_XRUN) {
- if ((err = snd_pcm_prepare(alsa_handle))) {
- snd_pcm_recover(alsa_handle, err, 1);
- debug(1, "Error preparing after underrun: \"%s\".", snd_strerror(err));
+
+ // must be 1.1 or later to use snd_pcm_status_get_driver_htstamp
+#if SND_LIB_MINOR == 0
+ snd_pcm_status_get_htstamp(alsa_snd_pcm_status, &update_timestamp);
+#else
+ snd_pcm_status_get_driver_htstamp(alsa_snd_pcm_status, &update_timestamp);
+#endif
+
+ *state = snd_pcm_status_get_state(alsa_snd_pcm_status);
+
+ if ((*state == SND_PCM_STATE_RUNNING) || (*state == SND_PCM_STATE_DRAINING)) {
+
+ uint64_t update_timestamp_ns =
+ update_timestamp.tv_sec * (uint64_t)1000000000 + update_timestamp.tv_nsec;
+
+ // if the update_timestamp is zero, we take this to mean that the device doesn't report
+ // interrupt timings. (It could be that it's not a real hardware device.)
+ // so we switch to getting the delay the regular way
+ // i.e. using snd_pcm_delay ()
+ if (using_update_timestamps) {
+ if (update_timestamp_ns == 0)
+ *using_update_timestamps = YNDK_NO;
+ else
+ *using_update_timestamps = YNDK_YES;
+ }
+
+// user information
+ if (update_timestamp_ns == 0) {
+ if (delay_type_notified != 1) {
+ inform("Note: the alsa output device \"%s\" is not capable of high precision delay timing.", snd_pcm_name(alsa_handle));
+ debug(1,"alsa: delay_and_status must use snd_pcm_delay() to calculate delay");
+ delay_type_notified = 1;
+ }
+ } else {
+// diagnostic
+ if (delay_type_notified != 0) {
+ debug(2,"alsa: delay_and_status using snd_pcm_status_get_delay() to calculate delay");
+ delay_type_notified = 0;
+ }
+ }
+
+ if (update_timestamp_ns == 0) {
+ ret = snd_pcm_delay (alsa_handle,delay);
+ } else {
+ *delay = snd_pcm_status_get_delay(alsa_snd_pcm_status);
+
+/*
+// It seems that the alsa library uses CLOCK_REALTIME before 1.0.28, even though
+// the check for monotonic returns true. Might have to watch out for this.
+ #if SND_LIB_MINOR == 0 && SND_LIB_SUBMINOR < 28
+ clock_gettime(CLOCK_REALTIME, &tn);
+ #else
+ clock_gettime(CLOCK_MONOTONIC, &tn);
+ #endif
+*/
+
+ if (use_monotonic_clock)
+ clock_gettime(CLOCK_MONOTONIC, &tn);
+ else
+ clock_gettime(CLOCK_REALTIME, &tn);
+
+ uint64_t time_now_ns = tn.tv_sec * (uint64_t)1000000000 + tn.tv_nsec;
+
+ // see if it's stalled
+
+ if ((stall_monitor_start_time != 0) && (stall_monitor_frame_count == *delay)) {
+ // hasn't outputted anything since the last call to 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 ".",
+ 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 ".",
+ 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;
+ }
+ } else {
+ stall_monitor_start_time = update_timestamp_ns;
+ stall_monitor_frame_count = *delay;
+ }
+
+ if (ret == 0) {
+ uint64_t delta = time_now_ns - update_timestamp_ns;
+
+ uint64_t frames_played_since_last_interrupt =
+ ((uint64_t)config.output_rate * delta) / 1000000000;
+ snd_pcm_sframes_t frames_played_since_last_interrupt_sized =
+ frames_played_since_last_interrupt;
+
+ *delay = *delay - frames_played_since_last_interrupt_sized;
+ }
}
+ } else { // not running, thus no delay information, thus can't check for
+ // stall
+ *delay = 0;
+ stall_monitor_start_time = 0; // zero if not initialised / not started / zeroed by flush
+ stall_monitor_frame_count = 0; // set to delay at start of time, incremented by any writes
+
+ // not running, thus no delay information, thus can't check for frame
+ // rates
+ frame_index = 0; // we'll be starting over...
+ measurement_data_is_valid = 0;
}
- if ((snd_pcm_state(alsa_handle) == SND_PCM_STATE_PREPARED) ||
- (snd_pcm_state(alsa_handle) == SND_PCM_STATE_RUNNING)) {
- if (buf == NULL)
- debug(1, "NULL buffer passed to pcm_writei -- skipping it");
- if (samples == 0)
- debug(1, "empty buffer being passed to pcm_writei -- skipping it");
- if ((samples != 0) && (buf != NULL)) {
- err = alsa_pcm_write(alsa_handle, buf, samples);
- if (err < 0) {
- debug(1, "Error %d writing %d samples in play(): \"%s\".", err, samples,
- snd_strerror(err));
- snd_pcm_recover(alsa_handle, err, 1);
+ } else {
+ debug(1, "alsa: can't get device's status.");
+ }
+ return ret;
+}
+
+int delay(long *the_delay) {
+ // returns 0 if the device is in a valid state -- SND_PCM_STATE_RUNNING or
+ // SND_PCM_STATE_PREPARED
+ // or SND_PCM_STATE_DRAINING
+ // and returns the actual delay if running or 0 if prepared in *the_delay
+
+ // otherwise return an error code
+ // the error code could be a Unix errno code or a snderror code, or
+ // the sps_extra_code_output_stalled or the
+ // sps_extra_code_output_state_cannot_make_ready codes
+ int ret = 0;
+ *the_delay = 0;
+ if (alsa_handle == NULL)
+ ret = ENODEV;
+ else {
+ int oldState;
+
+ snd_pcm_state_t state;
+ snd_pcm_sframes_t my_delay = 0; // this initialisation is to silence a clang warning
+
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
+ pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 0);
+
+ ret = delay_and_status(&state, &my_delay, NULL);
+
+ debug_mutex_unlock(&alsa_mutex, 0);
+ pthread_cleanup_pop(0);
+ pthread_setcancelstate(oldState, NULL);
+
+ *the_delay = my_delay; // note: snd_pcm_sframes_t is a long
+ }
+ return ret;
+}
+
+int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played) {
+ int response = 0; // zero means okay
+ if (measurement_data_is_valid) {
+ *elapsed_time = measurement_time - measurement_start_time;
+ *frames_played = frames_played_at_measurement_time - frames_played_at_measurement_start_time;
+ } else {
+ *elapsed_time = 0;
+ *frames_played = 0;
+ response = -1;
+ }
+ return response;
+}
+
+int do_play(void *buf, int samples) {
+ // assuming the alsa_mutex has been acquired
+ // debug(3,"audio_alsa play called.");
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
+
+ snd_pcm_state_t state;
+ snd_pcm_sframes_t my_delay;
+ int ret = delay_and_status(&state, &my_delay, NULL);
+
+ if (ret == 0) { // will be non-zero if an error or a stall
+
+ if ((samples != 0) && (buf != NULL)) {
+
+ // jut check the state of the DAC
+
+ if ((state != SND_PCM_STATE_PREPARED) && (state != SND_PCM_STATE_RUNNING) &&
+ (state != SND_PCM_STATE_XRUN)) {
+ debug(1, "alsa: DAC in odd SND_PCM_STATE_* %d prior to writing.", state);
+ }
+
+ // debug(3, "write %d frames.", samples);
+ ret = alsa_pcm_write(alsa_handle, buf, samples);
+ if (ret == samples) {
+ stall_monitor_frame_count += samples;
+
+ if (frame_index == 0) {
+ frames_sent_for_playing = samples;
+ } else {
+ frames_sent_for_playing += samples;
}
+
+ const uint64_t start_measurement_from_this_frame =
+ (2 * config.output_rate) / 352; // two seconds of frames
+
+ frame_index++;
+
+ 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();
+ frames_played_at_measurement_time = frames_sent_for_playing - my_delay - samples;
+
+ if (frame_index == start_measurement_from_this_frame) {
+ // debug(1, "Start frame counting");
+ frames_played_at_measurement_start_time = frames_played_at_measurement_time;
+ measurement_start_time = measurement_time;
+ measurement_data_is_valid = 1;
+ }
+ }
+ } else {
+ frame_index = 0;
+ measurement_data_is_valid = 0;
+ if (ret == -EPIPE) { /* underrun */
+ debug(1, "alsa: underrun while writing %d samples to alsa device.", samples);
+ ret = snd_pcm_recover(alsa_handle, ret, debuglev > 0 ? 1 : 0);
+ if (ret < 0) {
+ warn("alsa: can't recover from SND_PCM_STATE_XRUN: %s.", snd_strerror(ret));
+ }
+ } else if (ret == -ESTRPIPE) { /* suspended */
+ debug(1, "alsa: suspended while writing %d samples to alsa device.", samples);
+ while ((ret = snd_pcm_resume(alsa_handle)) == -EAGAIN) {
+ sleep(1); /* wait until the suspend flag is released */
+ if (ret < 0) {
+ warn("alsa: can't recover from SND_PCM_STATE_SUSPENDED state, "
+ "snd_pcm_prepare() "
+ "failed: %s.",
+ snd_strerror(ret));
+ }
+ }
+ } else {
+ char errorstring[1024];
+ strerror_r(-ret, (char *)errorstring, sizeof(errorstring));
+ debug(1, "alsa: error %d (\"%s\") writing %d samples to alsa device.", ret, (char *)errorstring, samples);
+ }
+
}
- } else {
- debug(1, "Error -- ALSA device in incorrect state (%d) for play.",
- snd_pcm_state(alsa_handle));
- if ((err = snd_pcm_prepare(alsa_handle))) {
- snd_pcm_recover(alsa_handle, err, 1);
- debug(1, "Error preparing after play error: \"%s\".", snd_strerror(err));
+ }
+ } else {
+ 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;
+ }
+
+ pthread_setcancelstate(oldState, NULL);
+ return ret;
+}
+
+int do_open(int do_auto_setup) {
+ int ret = 0;
+ if (alsa_backend_state != abm_disconnected)
+ debug(1, "alsa: do_open() -- opening the output device when it is already "
+ "connected");
+ if (alsa_handle == NULL) {
+ // debug(1,"alsa: do_open() -- opening the output device");
+ ret = open_alsa_device(do_auto_setup);
+ if (ret == 0) {
+ mute_requested_internally = 0;
+ if (audio_alsa.volume)
+ do_volume(set_volume);
+ if (audio_alsa.mute) {
+ debug(2, "do_open() set_mute_state");
+ set_mute_state(); // the mute_requested_externally flag will have been
+ // set accordingly
+ // do_mute(0); // complete unmute
}
+
+ alsa_backend_state = abm_connected; // only do this if it really opened it.
}
- debug_mutex_unlock(&alsa_mutex, 3);
+ } else {
+ debug(1, "alsa: do_open() -- output device already open.");
}
+ return ret;
}
-static void flush(void) {
- // debug(2,"audio_alsa flush called.");
- debug_mutex_lock(&alsa_mutex, 10000, 1);
- int derr;
- do_mute(1);
+int do_close() {
+ if (alsa_backend_state == abm_disconnected)
+ debug(1, "alsa: do_close() -- closing the output device when it is already "
+ "disconnected");
+ int derr = 0;
if (alsa_handle) {
-
+ // 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));
-
if ((derr = snd_pcm_hw_free(alsa_handle)))
debug(1, "Error %d (\"%s\") freeing the output device hardware.", derr, snd_strerror(derr));
// flush also closes the device
+ debug(2, "alsa: do_close() -- closing alsa handle");
if ((derr = snd_pcm_close(alsa_handle)))
debug(1, "Error %d (\"%s\") closing the output device.", derr, snd_strerror(derr));
-
alsa_handle = NULL;
+ } else {
+ debug(1, "alsa: do_close() -- output device already closed.");
}
+ alsa_backend_state = abm_disconnected;
+ return derr;
+}
+
+int play(void *buf, int samples) {
+
+ // play() will change the state of the alsa_backend_mode to abm_playing
+ // also, if the present alsa_backend_state is abm_disconnected, then first the
+ // DAC must be
+ // connected
+
+ // debug(3,"audio_alsa play called.");
+ int ret = 0;
+
+ pthread_cleanup_debug_mutex_lock(&alsa_mutex, 50000, 0);
+
+ if (alsa_backend_state == abm_disconnected) {
+ ret = do_open(0); // don't try to auto setup
+ if (ret == 0)
+ debug(2, "alsa: play() -- opened output device");
+ }
+
+ if (ret == 0) {
+ if (alsa_backend_state != abm_playing) {
+ debug(2, "alsa: play() -- alsa_backend_state => abm_playing");
+ alsa_backend_state = abm_playing;
+
+ // mute_requested_internally = 0; // stop requesting a mute for backend's own
+ // reasons, which might have been a flush
+ //debug(2, "play() set_mute_state");
+ //set_mute_state(); // try to action the request and return a status
+ // do_mute(0); // unmute for backend's reason
+ }
+ ret = do_play(buf, samples);
+ }
+
+ debug_mutex_unlock(&alsa_mutex, 0);
+ pthread_cleanup_pop(0); // release the mutex
+ return ret;
+}
+
+int prepare(void) {
+ // this will leave the DAC open / connected.
+ int ret = 0;
+
+ pthread_cleanup_debug_mutex_lock(&alsa_mutex, 50000, 0);
+
+ if (alsa_backend_state == abm_disconnected) {
+ ret = do_open(1); // do auto setup
+ if (ret == 0)
+ debug(2, "alsa: prepare() -- opened output device");
+
+ }
+
+ debug_mutex_unlock(&alsa_mutex, 0);
+ pthread_cleanup_pop(0); // release the mutex
+ return ret;
+}
+
+static void flush(void) {
+ // debug(2,"audio_alsa flush called.");
+ pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1);
+ // mute_requested_internally = 1; // request a mute for backend's reasons
+ // debug(2, "flush() set_mute_state");
+ // set_mute_state();
+ // do_mute(1); // mute for backend's own reasons
+ if (alsa_backend_state != abm_disconnected) { // must be playing or connected...
+ if (config.keep_dac_busy != 0) {
+ debug(2, "alsa: flush() -- alsa_backend_state => abm_connected.");
+ alsa_backend_state = abm_connected;
+ } else {
+ debug(2, "alsa: flush() -- closing the output device");
+ do_close(); // will change the state to disconnected
+ debug(2, "alsa: flush() -- alsa_backend_state => abm_disconnected.");
+ }
+ } else
+ debug(3, "alsa: flush() -- called on a disconnected alsa backend");
debug_mutex_unlock(&alsa_mutex, 3);
+ pthread_cleanup_pop(0); // release the mutex
}
static void stop(void) {
// debug(2,"audio_alsa stop called.");
- // when we want to stop, we want the alsa device
- // to be closed immediately -- we may even be killing the thread, so we
- // don't wish to wait
- // so we should flush first
- flush(); // flush will also close the device
- // close_alsa_device();
+ flush(); // flush will also close the device if appropriate
}
static void parameters(audio_parameters *info) {
@@ -938,10 +1722,14 @@ static void parameters(audio_parameters *info) {
info->maximum_volume_dB = alsa_mix_maxdb;
}
-void do_volume(double vol) { // caller is assumed to have the alsa_mutex when using this function
+void do_volume(double vol) { // caller is assumed to have the alsa_mutex when
+ // using this function
debug(3, "Setting volume db to %f.", vol);
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
set_volume = vol;
- if (volume_set_request && open_mixer()) {
+ pthread_cleanup_debug_mutex_lock(&alsa_mixer_mutex, 1000, 1);
+ if (volume_set_request && (open_mixer() == 1)) {
if (has_softvol) {
if (ctl && elem_id) {
snd_ctl_elem_value_t *value;
@@ -970,13 +1758,14 @@ void do_volume(double vol) { // caller is assumed to have the alsa_mutex when us
volume_set_request = 0; // any external request that has been made is now satisfied
close_mixer();
}
+ debug_mutex_unlock(&alsa_mixer_mutex, 3);
+ pthread_cleanup_pop(0); // release the mutex
+ pthread_setcancelstate(oldState, NULL);
}
void volume(double vol) {
- debug_mutex_lock(&alsa_mutex, 1000, 1);
volume_set_request = 1; // an external request has been made to set the volume
do_volume(vol);
- debug_mutex_unlock(&alsa_mutex, 3);
}
/*
@@ -986,7 +1775,8 @@ static void linear_volume(double vol) {
if (hardware_mixer && alsa_mix_handle) {
double linear_volume = pow(10, vol);
// debug(1,"Linear volume is %f.",linear_volume);
- long int_vol = alsa_mix_minv + (alsa_mix_maxv - alsa_mix_minv) * linear_volume;
+ long int_vol = alsa_mix_minv + (alsa_mix_maxv - alsa_mix_minv) *
+linear_volume;
// debug(1,"Setting volume to %ld, for volume input of %f.",int_vol,vol);
if (alsa_mix_handle) {
if (snd_mixer_selem_set_playback_volume_all(alsa_mix_elem, int_vol) != 0)
@@ -997,55 +1787,111 @@ static void linear_volume(double vol) {
}
*/
-static void mute(int mute_state_requested) {
- // debug(1,"External Mute Request: %d",mute_state_requested);
- debug_mutex_lock(&alsa_mutex, 10000, 1);
- mute_request_pending = 1;
- overriding_mute_state_requested = mute_state_requested;
- do_mute(mute_state_requested);
- debug_mutex_unlock(&alsa_mutex, 3);
+int mute(int mute_state_requested) { // these would be for external reasons, not
+ // because of the
+ // state of the backend.
+ mute_requested_externally = mute_state_requested; // request a mute for external reasons
+ debug(2, "mute(%d) set_mute_state", mute_state_requested);
+ return set_mute_state();
+}
+/*
+void alsa_buffer_monitor_thread_cleanup_function(__attribute__((unused)) void
+*arg) {
+ debug(1, "alsa: alsa_buffer_monitor_thread_cleanup_function called.");
}
+*/
-void do_mute(int mute_state_requested) {
-
- // if a mute is requested now, then
- // if an external mute request is in place, leave everything muted
- // otherwise, if an external mute request is pending, action it
- // otherwise, action the do_mute request
-
- int local_mute_state_requested =
- overriding_mute_state_requested; // go with whatever was asked by the external "mute" call
-
- // The mute state requested will be actioned unless mute_request_pending is set
- // If it is set, then that the pending request will be actioned.
- // If the hardware isn't there, or we are not allowed to use it, nothing will be done
- // The caller must have the alsa mutex
-
- if (config.alsa_use_hardware_mute == 1) {
- if (mute_request_pending == 0)
- local_mute_state_requested = mute_state_requested;
- if (open_mixer()) {
- if (local_mute_state_requested) {
- // debug(1,"Playback Switch mute actually done");
- if (snd_mixer_selem_has_playback_switch(alsa_mix_elem))
- snd_mixer_selem_set_playback_switch_all(alsa_mix_elem, 0);
- else {
- // debug(1,"Activating volume-based mute.");
- volume_based_mute_is_active = 1;
- do_snd_mixer_selem_set_playback_dB_all(alsa_mix_elem, alsa_mix_mute);
+void *alsa_buffer_monitor_thread_code(__attribute__((unused)) void *arg) {
+ int okb = -1;
+ while (1) {
+ if (okb != config.keep_dac_busy) {
+ debug(2,"keep_dac_busy is now \"%s\"",config.keep_dac_busy == 0 ? "no" : "yes");
+ okb = config.keep_dac_busy;
+ }
+ if ((config.keep_dac_busy != 0) && (alsa_device_initialised == 0)) {
+ debug(2, "alsa: alsa_buffer_monitor_thread_code() calling "
+ "do_alsa_device_init_if_needed.");
+ do_alsa_device_init_if_needed();
+ }
+ int sleep_time_ms = (int)(config.audio_backend_silence_scan_interval * 1000);
+ 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)) {
+ // open the dac and move to abm_connected mode
+ if (do_open(1) == 0) // no automatic setup of rate and speed if necessary
+ debug(2, "alsa: alsa_buffer_monitor_thread_code() -- output device opened; "
+ "alsa_backend_state => abm_connected");
+ } else if ((alsa_backend_state == abm_connected) && (config.keep_dac_busy == 0)) {
+ stall_monitor_start_time = 0;
+ frame_index = 0;
+ measurement_data_is_valid = 0;
+ debug(2, "alsa: alsa_buffer_monitor_thread_code() -- closing the output "
+ "device");
+ do_close();
+ debug(2, "alsa: alsa_buffer_monitor_thread_code() -- alsa_backend_state "
+ "=> abm_disconnected");
+ }
+ // now, if the backend is not in the abm_disconnected state
+ // and config.keep_dac_busy is true (at the present, this has to be the case
+ // to be in the
+ // abm_connected state in the first place...) then do the silence-filling
+ // thing, if needed, and if the output device is capable of precision delay.
+ if ((alsa_backend_state != abm_disconnected) && (config.keep_dac_busy != 0) && precision_delay_available()) {
+ 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);
}
- } else if (overriding_mute_state_requested == 0) {
- // debug(1,"Playback Switch unmute actually done");
- if (snd_mixer_selem_has_playback_switch(alsa_mix_elem))
- snd_mixer_selem_set_playback_switch_all(alsa_mix_elem, 1);
- else {
- // debug(1,"Deactivating volume-based mute.");
- volume_based_mute_is_active = 0;
- do_snd_mixer_selem_set_playback_dB_all(alsa_mix_elem, set_volume);
+ long buffer_size_threshold =
+ (long)(config.audio_backend_silence_threshold * config.output_rate);
+ 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;
+ // debug(1,"alsa: sleep_time: %d ms or 0x%" PRIx64 " in fp
+ // form.",sleep_time_ms,sleep_time_in_fp); int frames_of_silence =
+ // (config.output_rate *
+ // sleep_time_ms * 2) / 1000;
+ int frames_of_silence = 1024;
+ size_t size_of_silence_buffer = frames_of_silence * frame_size;
+ // debug(1, "alsa: alsa_buffer_monitor_thread_code -- silence buffer
+ // length: %u bytes.",
+ // size_of_silence_buffer);
+ void *silence = malloc(size_of_silence_buffer);
+ if (silence == NULL) {
+ debug(1, "alsa: alsa_buffer_monitor_thread_code -- failed to "
+ "allocate memory for a "
+ "silent frame buffer.");
+ } else {
+ pthread_cleanup_push(malloc_cleanup, silence);
+ int use_dither = 0;
+ if ((hardware_mixer == 0) && (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);
+ // 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);
+ }
}
}
- close_mixer();
}
+ debug_mutex_unlock(&alsa_mutex, 0);
+ pthread_cleanup_pop(0); // release the mutex
+ usleep(sleep_time_ms * 1000); // has a cancellation point in it
}
- mute_request_pending = 0;
+ pthread_exit(NULL);
}
diff --git a/audio_ao.c b/audio_ao.c
index 8a78e88..38953b6 100644
--- a/audio_ao.c
+++ b/audio_ao.c
@@ -121,7 +121,7 @@ static void deinit(void) {
static void start(__attribute__((unused)) int sample_rate,
__attribute__((unused)) int sample_format) {}
-static void play(void *buf, int samples) { ao_play(dev, buf, samples * 4); }
+static int play(void *buf, int samples) { return ao_play(dev, buf, samples * 4); }
static void stop(void) {}
@@ -129,8 +129,10 @@ audio_output audio_ao = {.name = "ao",
.help = &help,
.init = &init,
.deinit = &deinit,
+ .prepare = NULL,
.start = &start,
.stop = &stop,
+ .is_running = NULL,
.flush = NULL,
.delay = NULL,
.play = &play,
diff --git a/audio_dummy.c b/audio_dummy.c
index 5f55bab..483dbe4 100644
--- a/audio_dummy.c
+++ b/audio_dummy.c
@@ -48,18 +48,21 @@ static void start(int sample_rate, __attribute__((unused)) int sample_format) {
debug(1, "dummy audio output started at Fs=%d Hz\n", sample_rate);
}
-static void play(__attribute__((unused)) void *buf, __attribute__((unused)) int samples) {}
+static int play(__attribute__((unused)) void *buf, __attribute__((unused)) int samples) {
+ return 0;
+}
static void stop(void) { debug(1, "dummy audio stopped\n"); }
-static void help(void) { printf(" There are no options for dummy audio.\n"); }
audio_output audio_dummy = {.name = "dummy",
- .help = &help,
+ .help = NULL,
.init = &init,
.deinit = &deinit,
+ .prepare = NULL,
.start = &start,
.stop = &stop,
+ .is_running = NULL,
.flush = NULL,
.delay = NULL,
.play = &play,
diff --git a/audio_jack.c b/audio_jack.c
new file mode 100644
index 0000000..746d3d6
--- /dev/null
+++ b/audio_jack.c
@@ -0,0 +1,350 @@
+/*
+ * jack output driver. This file is part of Shairport Sync.
+ * Copyright (c) 2019 Mike Brady <mikebrady@iercom.net>,
+ * Jörn Nettingsmeier <nettings@luchtbeweging.nl>
+ *
+ * All rights reserved.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "audio.h"
+#include "common.h"
+#include <stdlib.h>
+#include <errno.h>
+#include <limits.h>
+#include <pthread.h>
+#include <string.h>
+
+#include <jack/jack.h>
+#include <jack/ringbuffer.h>
+
+// Two-channel, 16bit audio:
+static const int bytes_per_frame = 4;
+// Four seconds buffer -- should be plenty
+#define buffer_size (44100 * 4 * bytes_per_frame)
+
+static pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
+static pthread_mutex_t client_mutex = PTHREAD_MUTEX_INITIALIZER;
+
+int jack_init(int, char **);
+void jack_deinit(void);
+void jack_start(int, int);
+int play(void *, int);
+void jack_stop(void);
+int jack_delay(long *);
+void jack_flush(void);
+
+audio_output audio_jack = {.name = "jack",
+ .help = NULL,
+ .init = &jack_init,
+ .deinit = &jack_deinit,
+ .prepare = NULL,
+ .start = &jack_start,
+ .stop = NULL,
+ .is_running = NULL,
+ .flush = &jack_flush,
+ .delay = &jack_delay,
+ .play = &play,
+ .volume = NULL,
+ .parameters = NULL,
+ .mute = NULL};
+
+// This also affects deinterlacing.
+// So make it exactly the number of incoming audio channels!
+#define NPORTS 2
+static jack_port_t *port[NPORTS];
+static const char* port_name[NPORTS] = { "out_L", "out_R" };
+
+static jack_client_t *client;
+static jack_nframes_t sample_rate;
+static jack_nframes_t jack_latency;
+
+static jack_ringbuffer_t *jackbuf;
+static int flush_please = 0;
+
+static jack_latency_range_t latest_latency_range[NPORTS];
+static int64_t time_of_latest_transfer;
+
+
+static inline jack_default_audio_sample_t sample_conv(short sample) {
+ // It sounds correct, but I don't understand it.
+ // Zero int needs to be zero float. Check.
+ // Plus 32767 int is 1.0. Check.
+ // Minus 32767 int is -0.99997. And here my brain shuts down.
+ // In my head, it should be 1.0, and we should tolerate an overflow
+ // at minus 32768. But I'm sure there's a textbook explanation somewhere.
+ return ((sample < 0) ? (-1.0 * sample / SHRT_MIN) : (1.0 * sample / SHRT_MAX));
+}
+
+static void deinterleave_and_convert(const char *interleaved_input_buffer,
+ jack_default_audio_sample_t* jack_output_buffer[],
+ jack_nframes_t offset,
+ jack_nframes_t nframes) {
+ jack_nframes_t f;
+ // We're dealing with 16bit audio here:
+ short *ifp = (short *)interleaved_input_buffer;
+ // Zero-copy, we're working directly on the target and destination buffers,
+ // so deal with an offset for the second part of the input ringbuffer
+ for (f = offset; f < (nframes + offset); f++) {
+ for (int i = 0; i < NPORTS; i++) {
+ jack_output_buffer[i][f] = sample_conv(*ifp++);
+ }
+ }
+}
+
+// This is the JACK process callback. We don't decide when it runs.
+// It must be hard-realtime safe (i.e. fully deterministic, with constant CPU
+// usage. No calls to anything that could ever block: no syscalls, no screen
+// output, no file access, no mutexes...
+// The JACK ringbuffer we use to get the data in here is explicitly lock-free.
+static int process(jack_nframes_t nframes, __attribute__((unused)) void *arg) {
+ jack_default_audio_sample_t *buffer[NPORTS];
+ // Expect an array of two elements because of possible ringbuffer wrap-around:
+ jack_ringbuffer_data_t v[2] = { 0 };
+ jack_nframes_t i, thisbuf;
+ int frames_written = 0;
+ int frames_required = 0;
+
+ for (i = 0; i < NPORTS; i++) {
+ buffer[i] = (jack_default_audio_sample_t *)jack_port_get_buffer(port[i], nframes);
+ }
+ if (flush_please) {
+ // We just move the read pointer ahead without doing anything with the data.
+ jack_ringbuffer_read_advance(jackbuf, jack_ringbuffer_read_space(jackbuf));
+ flush_please = 0;
+ // Since we don't change nframes, the whole buffer will be zeroed later.
+ } else {
+ jack_ringbuffer_get_read_vector(jackbuf, v);
+ for (i = 0; i < 2; i++) {
+ thisbuf = v[i].len / bytes_per_frame;
+ if (thisbuf > nframes) {
+ frames_required = nframes;
+ } else {
+ frames_required = thisbuf;
+ }
+ deinterleave_and_convert(v[i].buf, buffer, frames_written, frames_required);
+ frames_written += frames_required;
+ nframes -= frames_required;
+ }
+ jack_ringbuffer_read_advance(jackbuf, frames_written * bytes_per_frame);
+ }
+ // If there are any more frames to put into the buffer, fill them with
+ // silence. This is a critical underflow situation. Let's at least keep the JACK
+ // graph humming along while preventing the motorboat sound of a repeating buffer.
+ while (nframes > 0) {
+ for (i = 0; i < NPORTS; i++) {
+ buffer[i][frames_written] = 0.0;
+ }
+ frames_written++;
+ nframes--;
+ }
+ return 0; // Tell JACK that all is well.
+}
+
+// This is the JACK graph reorder callback. Now we know some JACK connections
+// have changed, so we recompute the latency.
+static int graph(__attribute__((unused)) void * arg) {
+ int latency = 0;
+ debug(2, "JACK graph reorder callback called.");
+ for (int i=0; i<NPORTS; i++) {
+ jack_port_get_latency_range(port[i], JackPlaybackLatency, &latest_latency_range[i]);
+ debug(2, "JACK latency for port %s\tmin: %d\t max: %d",
+ port_name[i], latest_latency_range[i].min, latest_latency_range[i].max);
+ latency += latest_latency_range[i].max;
+ }
+ latency /= NPORTS;
+ jack_latency = latency;
+ debug(1, "Average maximum JACK latency across all ports: %d", jack_latency);
+ return 0;
+}
+
+// This the function JACK will call in case of an error in the library.
+static void error(const char *desc) {
+ warn("JACK error: \"%s\"", desc);
+}
+
+// This is the function JACK will call in case of a non-critical event in the library.
+static void info(const char *desc) {
+ inform("JACK information: \"%s\"", desc);
+}
+
+int jack_init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) {
+ int i;
+ config.audio_backend_latency_offset = 0;
+ config.audio_backend_buffer_desired_length = 0.500;
+ // Below this, soxr interpolation will not occur -- it'll be basic interpolation
+ // instead.
+ config.audio_backend_buffer_interpolation_threshold_in_seconds = 0.25;
+
+ // Do the "general" audio options. Note, these options are in the "general" stanza!
+ parse_general_audio_options();
+
+ // Now the options specific to the backend, from the "jack" stanza:
+ if (config.cfg != NULL) {
+ const char *str;
+ if (config_lookup_string(config.cfg, "jack.client_name", &str)) {
+ config.jack_client_name = (char *)str;
+ }
+ if (config_lookup_string(config.cfg, "jack.autoconnect_pattern", &str)) {
+ config.jack_autoconnect_pattern = (char *)str;
+ }
+ }
+ if (config.jack_client_name == NULL)
+ config.jack_client_name = strdup("shairport-sync");
+
+ jackbuf = jack_ringbuffer_create(buffer_size);
+ if (jackbuf == NULL)
+ die("Can't allocate %d bytes for the JACK ringbuffer.", buffer_size);
+ // Lock the ringbuffer into memory so that it never gets paged out, which would
+ // break realtime constraints.
+ jack_ringbuffer_mlock(jackbuf);
+ // This mutex should not be necessary, but removing it causes segfaults on
+ // shutdown. Apparently, there are multiple threads in the main program trying
+ // to do stuff. FIXME: Try to consolidate into one thread and get rid of this lock.
+ pthread_mutex_lock(&client_mutex);
+ jack_status_t status;
+ client = jack_client_open(config.jack_client_name, JackNoStartServer, &status);
+ if (!client) {
+ die("Could not start JACK server. JackStatus is %x", status);
+ }
+ sample_rate = jack_get_sample_rate(client);
+ 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);
+ }
+ jack_set_process_callback(client, &process, NULL);
+ jack_set_graph_order_callback(client, &graph, NULL);
+ jack_set_error_function(&error);
+ jack_set_info_function(&info);
+ for (i=0; i < NPORTS; i++) {
+ port[i] = jack_port_register(client, port_name[i], JACK_DEFAULT_AUDIO_TYPE,
+ JackPortIsOutput, 0);
+ }
+ if (jack_activate(client)) {
+ die("Could not activate %s JACK client.", config.jack_client_name);
+ } else {
+ debug(2, "JACK client %s activated sucessfully.", config.jack_client_name);
+ }
+ if (config.jack_autoconnect_pattern != NULL) {
+ inform("config.jack_autoconnect_pattern is %s. If you see the program die after this,"
+ "you made a syntax error.", config.jack_autoconnect_pattern);
+ // Sadly, this will throw a segfault if the user provides a syntactically incorrect regex.
+ // I've reported it to the jack-devel mailing list, they're in a better place to fix it.
+ const char** port_list = jack_get_ports(client, config.jack_autoconnect_pattern,
+ JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput);
+ if (port_list != NULL) {
+ for (i = 0; i < NPORTS ; i++) {
+ char* full_port_name[NPORTS];
+ full_port_name[i] = malloc(sizeof(char) * jack_port_name_size());
+ sprintf(full_port_name[i], "%s:%s", config.jack_client_name, port_name[i]);
+ if (port_list[i] != NULL) {
+ int err;
+ debug(2, "Connecting %s to %s.", full_port_name[i], port_list[i]);
+ err = jack_connect(client, full_port_name[i], port_list[i]);
+ switch (err) {
+ case EEXIST:
+ inform("The requested connection from %s to %s already exists.",
+ full_port_name[i], port_list[i]);
+ break;
+ case 0:
+ // success
+ break;
+ default:
+ warn("JACK error no. %d occured while trying to connect %s to %s.",
+ err, full_port_name[i], port_list[i]);
+ break;
+ }
+ } else {
+ inform("No matching port found in %s to connect %s to. You may not hear audio.",
+ config.jack_autoconnect_pattern, full_port_name[i]);
+ }
+ free(full_port_name[i]);
+ }
+ while (port_list[i++] != NULL) {
+ inform("Additional matching port %s found. Check that the connections are what you intended.");
+ }
+ jack_free(port_list);
+ }
+ }
+ pthread_mutex_unlock(&client_mutex);
+
+ return 0;
+}
+
+void jack_deinit() {
+ pthread_mutex_lock(&client_mutex);
+ if (jack_deactivate(client))
+ warn("Error deactivating jack client");
+ if (jack_client_close(client))
+ warn("Error closing jack client");
+ pthread_mutex_unlock(&client_mutex);
+ jack_ringbuffer_free(jackbuf);
+}
+
+void jack_start(__attribute__((unused)) 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.
+ // FIXME: later, resampling would be nice. Fold into soxr if possible.
+}
+
+void jack_flush() {
+ debug(2, "Only the consumer can safely flush a lock-free ringbuffer. Asking the"
+ " process callback to do it...");
+ flush_please = 1;
+}
+
+int jack_delay(long *the_delay) {
+ // Semantics change: we now look at the last transfer into the lock-free
+ // ringbuffer, not into the jack buffers directly (because locking those would
+ // violate real-time constraints). On average, that should lead to just a
+ // constant additional latency.
+ // Without the mutex, we could get the time of what is the last transfer of data
+ // to a jack buffer, but then a transfer could occur and we would get the buffer
+ // 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;
+ 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 * 44100) >> 32;
+ // 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
+ // to the buffer fill level:
+ *the_delay = jack_latency + audio_occupancy_now - frames_processed_since_latest_latency_check;
+ // debug(1,"reporting a delay of %d frames",*the_delay);
+ return 0;
+}
+
+int play(void *buf, int samples) {
+ // debug(1,"jack_play of %d samples.",samples);
+ // copy the samples into the queue
+ size_t bytes_to_transfer, bytes_transferred;
+ bytes_to_transfer = samples * bytes_per_frame;
+ // It's ok to lock here since we're not in the realtime callback:
+ pthread_mutex_lock(&buffer_mutex);
+ bytes_transferred = jack_ringbuffer_write(jackbuf, buf, bytes_to_transfer);
+ time_of_latest_transfer = get_absolute_time_in_fp();
+ pthread_mutex_unlock(&buffer_mutex);
+ if (bytes_transferred < bytes_to_transfer) {
+ warn("JACK ringbuffer overrun. Only wrote %d of %d bytes.",
+ bytes_transferred, bytes_to_transfer);
+ }
+ return 0;
+}
diff --git a/audio_pa.c b/audio_pa.c
index b46054d..e6a5b57 100644
--- a/audio_pa.c
+++ b/audio_pa.c
@@ -70,6 +70,10 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char *
// set up default values first
config.audio_backend_buffer_desired_length = 0.35;
+ config.audio_backend_buffer_interpolation_threshold_in_seconds =
+ 0.02; // below this, soxr interpolation will not occur -- it'll be basic interpolation
+ // instead.
+
config.audio_backend_latency_offset = 0;
// get settings from settings file
@@ -85,6 +89,11 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char *
if (config_lookup_string(config.cfg, "pa.application_name", &str)) {
config.pa_application_name = (char *)str;
}
+
+ /* Get the PulseAudio sink name. */
+ if (config_lookup_string(config.cfg, "pa.sink", &str)) {
+ config.pa_sink = (char *)str;
+ }
}
// finish collecting settings
@@ -177,8 +186,19 @@ static void start(__attribute__((unused)) int sample_rate,
// PA_STREAM_AUTO_TIMING_UPDATE;
PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY;
- // Connect stream to the default audio output sink
- if (pa_stream_connect_playback(stream, NULL, &buffer_attr, stream_flags, NULL, NULL) != 0)
+ int connect_result;
+
+ if (config.pa_sink) {
+ // Connect stream to the sink specified in the config
+ connect_result =
+ pa_stream_connect_playback(stream, config.pa_sink, &buffer_attr, stream_flags, NULL, NULL);
+ } else {
+ // Connect stream to the default audio output sink
+ connect_result =
+ pa_stream_connect_playback(stream, NULL, &buffer_attr, stream_flags, NULL, NULL);
+ }
+
+ if (connect_result != 0)
die("could not connect to the pulseaudio playback stream -- the error message is \"%s\".",
pa_strerror(pa_context_errno(context)));
@@ -197,7 +217,7 @@ static void start(__attribute__((unused)) int sample_rate,
pa_threaded_mainloop_unlock(mainloop);
}
-static void play(void *buf, int samples) {
+static int play(void *buf, int samples) {
// debug(1,"pa_play of %d samples.",samples);
// copy the samples into the queue
size_t bytes_to_transfer = samples * 2 * 2;
@@ -223,6 +243,7 @@ static void play(void *buf, int samples) {
pa_stream_cork(stream, 0, stream_success_cb, mainloop);
pa_threaded_mainloop_unlock(mainloop);
}
+ return 0;
}
int pa_delay(long *the_delay) {
@@ -278,14 +299,14 @@ static void stop(void) {
pa_stream_disconnect(stream);
}
-static void help(void) { printf(" no settings.\n"); }
-
audio_output audio_pa = {.name = "pa",
- .help = &help,
+ .help = NULL,
.init = &init,
.deinit = &deinit,
+ .prepare = NULL,
.start = &start,
.stop = &stop,
+ .is_running = NULL,
.flush = &flush,
.delay = &pa_delay,
.play = &play,
diff --git a/audio_pipe.c b/audio_pipe.c
index c080277..f0ced37 100644
--- a/audio_pipe.c
+++ b/audio_pipe.c
@@ -46,15 +46,22 @@ int warned = 0;
static void start(__attribute__((unused)) int sample_rate,
__attribute__((unused)) int sample_format) {
- // this will leave fd as -1 if a reader hasn't been attached
+
+
+ // this will leave fd as -1 if a reader hasn't been attached to the pipe
+ // we check that it's not a "real" error though. From the "man 2 open" page:
+ // "ENXIO O_NONBLOCK | O_WRONLY is set, the named file is a FIFO, and no process has the FIFO open for reading."
fd = open(pipename, O_WRONLY | O_NONBLOCK);
- if ((fd < -1) && (warned == 0)) {
+ if ((fd == -1) && (errno != ENXIO) && (warned == 0)) {
+ char errorstring[1024];
+ strerror_r(errno, (char *)errorstring, sizeof(errorstring));
+ debug(1, "pipe: start -- error %d (\"%s\") opening the pipe named \"%s\".", errno, (char*)errorstring, pipename);
warn("Error %d opening the pipe named \"%s\".", errno, pipename);
warned = 1;
}
}
-static void play(void *buf, int samples) {
+static int play(void *buf, int samples) {
// if the file is not open, try to open it.
char errorstring[1024];
if (fd == -1) {
@@ -68,11 +75,12 @@ static void play(void *buf, int samples) {
warn("Error %d writing to the pipe named \"%s\": \"%s\".", errno, pipename, errorstring);
warned = 1;
}
- } else if ((fd == -1) && (warned == 0)) {
+ } else if ((fd == -1) && (errno != ENXIO) && (warned == 0)) {
strerror_r(errno, (char *)errorstring, 1024);
warn("Error %d opening the pipe named \"%s\": \"%s\".", errno, pipename, errorstring);
warned = 1;
}
+ return warned;
}
static void stop(void) {
@@ -126,14 +134,16 @@ static void deinit(void) {
close(fd);
}
-static void help(void) { printf(" pipe takes 1 argument: the name of the FIFO to write to.\n"); }
+static void help(void) { printf(" specify the pathname of the pipe to write to.\n"); }
audio_output audio_pipe = {.name = "pipe",
.help = &help,
.init = &init,
.deinit = &deinit,
+ .prepare = NULL,
.start = &start,
.stop = &stop,
+ .is_running = NULL,
.flush = NULL,
.delay = NULL,
.play = &play,
diff --git a/audio_sndio.c b/audio_sndio.c
index d9cef65..a591add 100644
--- a/audio_sndio.c
+++ b/audio_sndio.c
@@ -33,7 +33,7 @@ static int init(int, char **);
static void onmove_cb(void *, int);
static void deinit(void);
static void start(int, int);
-static void play(void *, int);
+static int play(void *, int);
static void stop(void);
static void onmove_cb(void *, int);
static int delay(long *);
@@ -43,8 +43,10 @@ audio_output audio_sndio = {.name = "sndio",
.help = &help,
.init = &init,
.deinit = &deinit,
+ .prepare = NULL,
.start = &start,
.stop = &stop,
+ .is_running = NULL,
.flush = &flush,
.delay = &delay,
.play = &play,
@@ -64,20 +66,21 @@ struct sio_par par;
struct sndio_formats {
const char *name;
enum sps_format_t fmt;
-
+ unsigned int rate;
unsigned int bits;
unsigned int bps;
unsigned int sig;
unsigned int le;
};
-static struct sndio_formats formats[] = {{"S8", SPS_FORMAT_S8, 8, 1, 1, SIO_LE_NATIVE},
- {"U8", SPS_FORMAT_U8, 8, 1, 0, SIO_LE_NATIVE},
- {"S16", SPS_FORMAT_S16, 16, 2, 1, SIO_LE_NATIVE},
- {"S24", SPS_FORMAT_S24, 24, 4, 1, SIO_LE_NATIVE},
- {"S24_3LE", SPS_FORMAT_S24_3LE, 24, 3, 1, 1},
- {"S24_3BE", SPS_FORMAT_S24_3BE, 24, 3, 1, 0},
- {"S32", SPS_FORMAT_S32, 24, 4, 1, SIO_LE_NATIVE}};
+static struct sndio_formats formats[] = {{"S8", SPS_FORMAT_S8, 44100, 8, 1, 1, SIO_LE_NATIVE},
+ {"U8", SPS_FORMAT_U8, 44100, 8, 1, 0, SIO_LE_NATIVE},
+ {"S16", SPS_FORMAT_S16, 44100, 16, 2, 1, SIO_LE_NATIVE},
+ {"AUTOMATIC", SPS_FORMAT_S16, 44100, 16, 2, 1, SIO_LE_NATIVE}, // TODO: make this really automatic?
+ {"S24", SPS_FORMAT_S24, 44100, 24, 4, 1, SIO_LE_NATIVE},
+ {"S24_3LE", SPS_FORMAT_S24_3LE, 44100, 24, 3, 1, 1},
+ {"S24_3BE", SPS_FORMAT_S24_3BE, 44100, 24, 3, 1, 0},
+ {"S32", SPS_FORMAT_S32, 44100, 24, 4, 1, SIO_LE_NATIVE}};
static void help() { printf(" -d output-device set the output device [default*|...]\n"); }
@@ -98,6 +101,9 @@ static int init(int argc, char **argv) {
devname = SIO_DEVANY;
config.audio_backend_buffer_desired_length = 1.0;
+ config.audio_backend_buffer_interpolation_threshold_in_seconds =
+ 0.25; // below this, soxr interpolation will not occur -- it'll be basic interpolation
+ // instead.
config.audio_backend_latency_offset = 0;
// get settings from settings file
@@ -142,7 +148,7 @@ static int init(int argc, char **argv) {
}
if (!found)
die("Invalid output format \"%s\". Should be one of: S8, U8, S16, S24, "
- "S24_3LE, S24_3BE, S32",
+ "S24_3LE, S24_3BE, S32, Automatic",
tmp);
}
}
@@ -170,6 +176,7 @@ static int init(int argc, char **argv) {
written = played = 0;
time_of_last_onmove_cb = 0;
at_least_one_onmove_cb_seen = 0;
+
for (i = 0; i < sizeof(formats) / sizeof(formats[0]); i++) {
if (formats[i].fmt == config.output_format) {
@@ -180,19 +187,20 @@ static int init(int argc, char **argv) {
break;
}
}
-
+
if (!sio_setpar(hdl, &par) || !sio_getpar(hdl, &par))
die("sndio: failed to set audio parameters");
for (i = 0, found = 0; i < sizeof(formats) / sizeof(formats[0]); i++) {
if (formats[i].bits == par.bits && formats[i].bps == par.bps && formats[i].sig == par.sig &&
- formats[i].le == par.le) {
+ formats[i].le == par.le && formats[i].rate == par.rate) {
config.output_format = formats[i].fmt;
found = 1;
break;
}
}
if (!found)
- die("sndio: failed to negotiate audio parameters");
+ die("sndio: could not set output device to the required format and rate.");
+
framesize = par.bps * par.pchan;
config.output_rate = par.rate;
@@ -222,12 +230,13 @@ static void start(__attribute__((unused)) int sample_rate,
pthread_mutex_unlock(&sndio_mutex);
}
-static void play(void *buf, int frames) {
+static int play(void *buf, int frames) {
if (frames > 0) {
pthread_mutex_lock(&sndio_mutex);
written += sio_write(hdl, buf, frames * framesize);
pthread_mutex_unlock(&sndio_mutex);
}
+ return 0;
}
static void stop() {
diff --git a/audio_soundio.c b/audio_soundio.c
index c46e00d..7983733 100644
--- a/audio_soundio.c
+++ b/audio_soundio.c
@@ -27,8 +27,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) {
@@ -167,7 +168,7 @@ static void start(int sample_rate, int sample_format) {
debug(1, "libsoundio output started\n");
}
-static void play(void *buf, int samples) {
+static int play(void *buf, int samples) {
// int err;
int free_bytes = soundio_ring_buffer_free_count(ring_buffer);
int written_bytes = 0;
@@ -186,6 +187,7 @@ static void play(void *buf, int samples) {
soundio_ring_buffer_advance_write_ptr(ring_buffer, write_bytes);
debug(3, "[<<---] Written to buffer : %d\n", written_bytes);
}
+ return 0;
}
static void parameters(audio_parameters *info) {
@@ -214,8 +216,10 @@ audio_output audio_soundio = {.name = "soundio",
.help = &help,
.init = &init,
.deinit = &deinit,
+ .prepare = NULL,
.start = &start,
.stop = &stop,
+ .is_running = NULL,
.flush = &flush,
.delay = NULL,
.play = &play,
diff --git a/audio_stdout.c b/audio_stdout.c
index 3e27ad1..00ded16 100644
--- a/audio_stdout.c
+++ b/audio_stdout.c
@@ -43,7 +43,7 @@ static void start(__attribute__((unused)) int sample_rate,
fd = STDOUT_FILENO;
}
-static void play(void *buf, int samples) {
+static int play(void *buf, int samples) {
char errorstring[1024];
int warned = 0;
int rc = write(fd, buf, samples * 4);
@@ -52,6 +52,7 @@ static void play(void *buf, int samples) {
warn("Error %d writing to stdout: \"%s\".", errno, errorstring);
warned = 1;
}
+ return rc;
}
static void stop(void) {
@@ -73,14 +74,14 @@ static void deinit(void) {
// don't close stdout
}
-static void help(void) { printf(" stdout takes no arguments\n"); }
-
audio_output audio_stdout = {.name = "stdout",
- .help = &help,
+ .help = NULL,
.init = &init,
.deinit = &deinit,
+ .prepare = NULL,
.start = &start,
.stop = &stop,
+ .is_running = NULL,
.flush = NULL,
.delay = NULL,
.play = &play,
diff --git a/common.c b/common.c
index e6974bf..b097dda 100644
--- a/common.c
+++ b/common.c
@@ -2,6 +2,7 @@
* Utility routines. This file is part of Shairport.
* Copyright (c) James Laird 2013
* The volume to attenuation function vol2attn copyright (c) Mike Brady 2014
+ * Further changes and additions (c) Mike Brady 2014 -- 2019
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -46,7 +47,7 @@
#include <mach/mach_time.h>
#endif
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
#include <openssl/bio.h>
#include <openssl/buffer.h>
#include <openssl/evp.h>
@@ -54,7 +55,7 @@
#include <openssl/rsa.h>
#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
#include "polarssl/ctr_drbg.h"
#include "polarssl/entropy.h"
#include <polarssl/base64.h>
@@ -67,37 +68,100 @@
#endif
#endif
-#ifdef HAVE_LIBMBEDTLS
+#ifdef CONFIG_MBEDTLS
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/entropy.h"
#include <mbedtls/base64.h>
#include <mbedtls/md.h>
#include <mbedtls/version.h>
#include <mbedtls/x509.h>
-
#endif
+#ifdef CONFIG_LIBDAEMON
#include <libdaemon/dlog.h>
+#else
+#include <syslog.h>
+#endif
#ifdef CONFIG_ALSA
void set_alsa_out_dev(char *);
#endif
+const char * sps_format_description_string_array[] = {"unknown", "S8", "U8" ,"S16", "S16_LE", "S16_BE", "S24", "S24_LE", "S24_BE", "S24_3LE", "S24_3BE", "S32", "S32_LE", "S32_BE", "auto", "invalid" };
+
+const char * sps_format_description_string(enum sps_format_t format) {
+ if ((format >= SPS_FORMAT_UNKNOWN) && (format <= SPS_FORMAT_AUTO))
+ return sps_format_description_string_array[format];
+ else
+ return sps_format_description_string_array[SPS_FORMAT_INVALID];
+}
+
// true if Shairport Sync is supposed to be sending output to the output device, false otherwise
static volatile int requested_connection_state_to_output = 1;
+// this stuff is to direct logging to syslog via libdaemon or directly
+// alternatively you can direct it to stderr using a command line option
+
+#ifdef CONFIG_LIBDAEMON
+static void (*sps_log)(int prio, const char *t, ...) = daemon_log;
+#else
+static void (*sps_log)(int prio, const char *t, ...) = syslog;
+#endif
+
+void do_sps_log(__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(stderr,"%s\n",s);
+}
+
+void log_to_stderr() {
+ sps_log = do_sps_log;
+}
+
shairport_cfg config;
-int debuglev = 0;
+volatile int debuglev = 0;
sigset_t pselect_sigset;
+int usleep_uncancellable(useconds_t usec) {
+ int response;
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
+ response = usleep(usec);
+ pthread_setcancelstate(oldState, NULL);
+ return response;
+}
+
+static uint16_t UDPPortIndex = 0;
+
+void resetFreeUDPPort() {
+ debug(3, "Resetting UDP Port Suggestion to %u", config.udp_port_base);
+ UDPPortIndex = 0;
+}
+
+uint16_t nextFreeUDPPort() {
+ if (UDPPortIndex == 0)
+ UDPPortIndex = config.udp_port_base;
+ else if (UDPPortIndex == (config.udp_port_base + config.udp_port_range - 1))
+ UDPPortIndex = config.udp_port_base + 3; // avoid wrapping back to the first three, as they can
+ // be assigned by resetFreeUDPPort without checking
+ else
+ UDPPortIndex++;
+ return UDPPortIndex;
+}
+
int get_requested_connection_state_to_output() { return requested_connection_state_to_output; }
void set_requested_connection_state_to_output(int v) { requested_connection_state_to_output = v; }
void die(const char *format, ...) {
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
char s[1024];
s[0] = 0;
uint64_t time_now = get_absolute_time_in_fp();
@@ -113,18 +177,20 @@ void die(const char *format, ...) {
va_end(args);
if ((debuglev) && (config.debugger_show_elapsed_time) && (config.debugger_show_relative_time))
- daemon_log(LOG_EMERG, "|% 20.9f|% 20.9f|*fatal error: %s", tss, tsl, s);
+ sps_log(LOG_ERR, "|% 20.9f|% 20.9f|*fatal error: %s", tss, tsl, s);
else if ((debuglev) && (config.debugger_show_relative_time))
- daemon_log(LOG_EMERG, "% 20.9f|*fatal error: %s", tsl, s);
+ sps_log(LOG_ERR, "% 20.9f|*fatal error: %s", tsl, s);
else if ((debuglev) && (config.debugger_show_elapsed_time))
- daemon_log(LOG_EMERG, "% 20.9f|*fatal error: %s", tss, s);
+ sps_log(LOG_ERR, "% 20.9f|*fatal error: %s", tss, s);
else
- daemon_log(LOG_EMERG, "fatal error: %s", s);
- shairport_shutdown();
- exit(1);
+ sps_log(LOG_ERR, "fatal error: %s", s);
+ pthread_setcancelstate(oldState, NULL);
+ abort(); // exit() doesn't always work, by heaven.
}
void warn(const char *format, ...) {
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
char s[1024];
s[0] = 0;
uint64_t time_now = get_absolute_time_in_fp();
@@ -138,20 +204,22 @@ void warn(const char *format, ...) {
va_start(args, format);
vsnprintf(s, sizeof(s), format, args);
va_end(args);
-
if ((debuglev) && (config.debugger_show_elapsed_time) && (config.debugger_show_relative_time))
- daemon_log(LOG_WARNING, "|% 20.9f|% 20.9f|*warning: %s", tss, tsl, s);
+ sps_log(LOG_WARNING, "|% 20.9f|% 20.9f|*warning: %s", tss, tsl, s);
else if ((debuglev) && (config.debugger_show_relative_time))
- daemon_log(LOG_WARNING, "% 20.9f|*warning: %s", tsl, s);
+ sps_log(LOG_WARNING, "% 20.9f|*warning: %s", tsl, s);
else if ((debuglev) && (config.debugger_show_elapsed_time))
- daemon_log(LOG_WARNING, "% 20.9f|*warning: %s", tss, s);
+ sps_log(LOG_WARNING, "% 20.9f|*warning: %s", tss, s);
else
- daemon_log(LOG_WARNING, "%s", s);
+ sps_log(LOG_WARNING, "%s", s);
+ pthread_setcancelstate(oldState, NULL);
}
void debug(int level, const char *format, ...) {
if (level > debuglev)
return;
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
char s[1024];
s[0] = 0;
uint64_t time_now = get_absolute_time_in_fp();
@@ -166,23 +234,41 @@ void debug(int level, const char *format, ...) {
vsnprintf(s, sizeof(s), format, args);
va_end(args);
if ((config.debugger_show_elapsed_time) && (config.debugger_show_relative_time))
- daemon_log(LOG_DEBUG, "|% 20.9f|% 20.9f|%s", tss, tsl, s);
+ sps_log(LOG_DEBUG, "|% 20.9f|% 20.9f|%s", tss, tsl, s);
else if (config.debugger_show_relative_time)
- daemon_log(LOG_DEBUG, "% 20.9f|%s", tsl, s);
+ sps_log(LOG_DEBUG, "% 20.9f|%s", tsl, s);
else if (config.debugger_show_elapsed_time)
- daemon_log(LOG_DEBUG, "% 20.9f|%s", tss, s);
+ sps_log(LOG_DEBUG, "% 20.9f|%s", tss, s);
else
- daemon_log(LOG_DEBUG, "%s", s);
+ sps_log(LOG_DEBUG, "%s", s);
+ pthread_setcancelstate(oldState, NULL);
}
void inform(const char *format, ...) {
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
char s[1024];
s[0] = 0;
+ 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 divisor = (uint64_t)1 << 32;
+ double tss = 1.0 * time_since_start / divisor;
+ double tsl = 1.0 * time_since_last_debug_message / divisor;
va_list args;
va_start(args, format);
vsnprintf(s, sizeof(s), format, args);
va_end(args);
- daemon_log(LOG_INFO, "%s", s);
+ if ((debuglev) && (config.debugger_show_elapsed_time) && (config.debugger_show_relative_time))
+ sps_log(LOG_INFO, "|% 20.9f|% 20.9f|%s", tss, tsl, s);
+ else if ((debuglev) && (config.debugger_show_relative_time))
+ sps_log(LOG_INFO, "% 20.9f|%s", tsl, s);
+ else if ((debuglev) && (config.debugger_show_elapsed_time))
+ sps_log(LOG_INFO, "% 20.9f|%s", tss, s);
+ else
+ sps_log(LOG_INFO, "%s", s);
+ pthread_setcancelstate(oldState, NULL);
}
// The following two functions are adapted slightly and with thanks from Jonathan Leffler's sample
@@ -233,7 +319,7 @@ int mkpath(const char *path, mode_t mode) {
return (status);
}
-#ifdef HAVE_LIBMBEDTLS
+#ifdef CONFIG_MBEDTLS
char *base64_enc(uint8_t *input, int length) {
char *buf = NULL;
size_t dlen = 0;
@@ -286,7 +372,7 @@ uint8_t *base64_dec(char *input, int *outlen) {
}
#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
char *base64_enc(uint8_t *input, int length) {
char *buf = NULL;
size_t dlen = 0;
@@ -339,8 +425,10 @@ uint8_t *base64_dec(char *input, int *outlen) {
}
#endif
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
char *base64_enc(uint8_t *input, int length) {
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
BIO *bmem, *b64;
BUF_MEM *bptr;
b64 = BIO_new(BIO_f_base64());
@@ -348,7 +436,7 @@ char *base64_enc(uint8_t *input, int length) {
b64 = BIO_push(b64, bmem);
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
BIO_write(b64, input, length);
- BIO_flush(b64);
+ (void) BIO_flush(b64);
BIO_get_mem_ptr(b64, &bptr);
char *buf = (char *)malloc(bptr->length);
@@ -359,12 +447,15 @@ char *base64_enc(uint8_t *input, int length) {
buf[bptr->length - 1] = 0;
}
- BIO_free_all(bmem);
+ BIO_free_all(b64);
+ pthread_setcancelstate(oldState, NULL);
return buf;
}
uint8_t *base64_dec(char *input, int *outlen) {
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
BIO *bmem, *b64;
int inlen = strlen(input);
@@ -377,7 +468,7 @@ uint8_t *base64_dec(char *input, int *outlen) {
BIO_write(bmem, input, inlen);
while (inlen++ & 3)
BIO_write(bmem, "=", 1);
- BIO_flush(bmem);
+ (void) BIO_flush(bmem);
int bufsize = strlen(input) * 3 / 4 + 1;
uint8_t *buf = malloc(bufsize);
@@ -385,9 +476,10 @@ uint8_t *base64_dec(char *input, int *outlen) {
nread = BIO_read(b64, buf, bufsize);
- BIO_free_all(bmem);
+ BIO_free_all(b64);
*outlen = nread;
+ pthread_setcancelstate(oldState, NULL);
return buf;
}
#endif
@@ -417,10 +509,11 @@ static char super_secret_key[] =
"2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY=\n"
"-----END RSA PRIVATE KEY-----\0";
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
uint8_t *rsa_apply(uint8_t *input, int inlen, int *outlen, int mode) {
- static RSA *rsa = NULL;
-
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
+ RSA *rsa = NULL;
if (!rsa) {
BIO *bmem = BIO_new_mem_buf(super_secret_key, -1);
rsa = PEM_read_bio_RSAPrivateKey(bmem, NULL, NULL, NULL);
@@ -438,11 +531,13 @@ uint8_t *rsa_apply(uint8_t *input, int inlen, int *outlen, int mode) {
default:
die("bad rsa mode");
}
+ RSA_free(rsa);
+ pthread_setcancelstate(oldState, NULL);
return out;
}
#endif
-#ifdef HAVE_LIBMBEDTLS
+#ifdef CONFIG_MBEDTLS
uint8_t *rsa_apply(uint8_t *input, int inlen, int *outlen, int mode) {
mbedtls_pk_context pkctx;
mbedtls_rsa_context *trsa;
@@ -499,7 +594,7 @@ uint8_t *rsa_apply(uint8_t *input, int inlen, int *outlen, int mode) {
}
#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
uint8_t *rsa_apply(uint8_t *input, int inlen, int *outlen, int mode) {
rsa_context trsa;
const char *pers = "rsa_encrypt";
@@ -554,7 +649,26 @@ uint8_t *rsa_apply(uint8_t *input, int inlen, int *outlen, int mode) {
}
#endif
+int config_set_lookup_bool(config_t *cfg, char *where, int *dst) {
+ const char *str = 0;
+ if (config_lookup_string(cfg, where, &str)) {
+ if (strcasecmp(str, "no") == 0) {
+ (*dst) = 0;
+ return 1;
+ } else if (strcasecmp(str, "yes") == 0) {
+ (*dst) = 1;
+ return 1;
+ } else {
+ die("Invalid %s option choice \"%s\". It should be \"yes\" or \"no\"", where, str);
+ return 0;
+ }
+ } else {
+ return 0;
+ }
+}
+
void command_set_volume(double volume) {
+ // this has a cancellation point if waiting is enabled
if (config.cmd_set_volume) {
/*Spawn a child to run the program.*/
pid_t pid = fork();
@@ -582,7 +696,7 @@ void command_set_volume(double volume) {
execv(argV[0], argV);
warn("Execution of on-set-volume command \"%s\" failed to start", config.cmd_set_volume);
// debug(1, "Error executing on-set-volume command %s", config.cmd_set_volume);
- exit(127); /* only if execv fails */
+ exit(EXIT_FAILURE); /* only if execv fails */
}
}
@@ -601,6 +715,7 @@ void command_set_volume(double volume) {
}
void command_start(void) {
+ // this has a cancellation point if waiting is enabled or a response is awaited
if (config.cmd_start) {
pid_t pid;
int pipes[2];
@@ -636,14 +751,15 @@ void command_start(void) {
execv(argV[0], argV);
warn("Execution of on-start command failed to start");
debug(1, "Error executing on-start command %s", config.cmd_start);
- exit(127); /* only if execv fails */
+ exit(EXIT_FAILURE); /* only if execv fails */
}
} else {
if (config.cmd_blocking || config.cmd_start_returns_output) { /* pid!=0 means parent process
and if blocking is true, wait for
process to finish */
pid_t rc = waitpid(pid, 0, 0); /* wait for child to exit */
- if (rc != pid) {
+ if ((rc != pid) && (errno != ECHILD)) {
+ // In this context, ECHILD means that the child process has already completed, I think!
warn("Execution of on-start command returned an error.");
debug(1, "on-start command %s finished with error %d", config.cmd_start, errno);
}
@@ -666,40 +782,54 @@ void command_start(void) {
}
}
}
+void command_execute(const char *command, const char *extra_argument, const int block) {
+ // this has a cancellation point if waiting is enabled
+ if (command) {
+ char new_command_buffer[1024];
+ char *full_command = (char *)command;
+ if (extra_argument != NULL) {
+ memset(new_command_buffer, 0, sizeof(new_command_buffer));
+ snprintf(new_command_buffer, sizeof(new_command_buffer), "%s %s", command, extra_argument);
+ full_command = new_command_buffer;
+ }
-void command_stop(void) {
- if (config.cmd_stop) {
/*Spawn a child to run the program.*/
pid_t pid = fork();
if (pid == 0) { /* child process */
int argC;
char **argV;
- // debug(1,"on-stop command found.");
- if (poptParseArgvString(config.cmd_stop, &argC, (const char ***)&argV) !=
+ if (poptParseArgvString(full_command, &argC, (const char ***)&argV) !=
0) // note that argV should be free()'d after use, but we expect this fork to exit
// eventually.
- debug(1, "Can't decipher on-stop command arguments");
+ debug(1, "Can't decipher command arguments in \"%s\".", full_command);
else {
- // debug(1,"Executing on-stop command %s",config.cmd_stop);
+ // debug(1,"Executing command %s",full_command);
execv(argV[0], argV);
- warn("Execution of on-stop command failed to start");
- debug(1, "Error executing on-stop command %s", config.cmd_stop);
- exit(127); /* only if execv fails */
+ warn("Execution of command \"%s\" failed to start", full_command);
+ debug(1, "Error executing command \"%s\".", full_command);
+ exit(EXIT_FAILURE); /* only if execv fails */
}
} else {
- if (config.cmd_blocking) { /* pid!=0 means parent process and if blocking is true, wait for
+ if (block) { /* pid!=0 means parent process and if blocking is true, wait for
process to finish */
pid_t rc = waitpid(pid, 0, 0); /* wait for child to exit */
- if (rc != pid) {
- warn("Execution of on-stop command returned an error.");
- debug(1, "Stop command %s finished with error %d", config.cmd_stop, errno);
+ if ((rc != pid) && (errno != ECHILD)) {
+ // In this context, ECHILD means that the child process has already completed, I think!
+ warn("Execution of command \"%s\" returned an error.", full_command);
+ debug(1, "Command \"%s\" finished with error %d", full_command, errno);
}
}
- // debug(1,"Continue after on-stop command");
+ // debug(1,"Continue after on-unfixable command");
}
}
}
+void command_stop(void) {
+ // this has a cancellation point if waiting is enabled
+ if (config.cmd_stop)
+ command_execute(config.cmd_stop, "", config.cmd_blocking);
+}
+
// this is for reading an unsigned 32 bit number, such as an RTP timestamp
uint32_t uatoi(const char *nptr) {
@@ -732,21 +862,17 @@ double flat_vol2attn(double vol, long max_db, long min_db) {
double vol2attn(double vol, long max_db, long min_db) {
-// We use a little coordinate geometry to build a transfer function from the volume passed in to the
-// device's dynamic range.
-// (See the diagram in the documents folder.)
-// The x axis is the "volume in" which will be from -30 to 0. The y axis will be the "volume out"
-// which will be from the bottom of the range to the top.
-// We build the transfer function from one or more lines. We characterise each line with two
-// numbers:
-// the first is where on x the line starts when y=0 (x can be from 0 to -30); the second is where on
-// y the line stops when when x is -30.
-// thus, if the line was characterised as {0,-30}, it would be an identity transfer.
-// Assuming, for example, a dynamic range of lv=-60 to hv=0
-// Typically we'll use three lines -- a three order transfer function
-// First: {0,30} giving a gentle slope -- the 30 comes from half the dynamic range
-// Second: {-5,-30-(lv+30)/2} giving a faster slope from y=0 at x=-12 to y=-42.5 at x=-30
-// Third: {-17,lv} giving a fast slope from y=0 at x=-19 to y=-60 at x=-30
+ // We use a little coordinate geometry to build a transfer function from the volume passed in to
+ // the device's dynamic range. (See the diagram in the documents folder.) The x axis is the
+ // "volume in" which will be from -30 to 0. The y axis will be the "volume out" which will be from
+ // the bottom of the range to the top. We build the transfer function from one or more lines. We
+ // characterise each line with two numbers: the first is where on x the line starts when y=0 (x
+ // can be from 0 to -30); the second is where on y the line stops when when x is -30. thus, if the
+ // line was characterised as {0,-30}, it would be an identity transfer. Assuming, for example, a
+ // dynamic range of lv=-60 to hv=0 Typically we'll use three lines -- a three order transfer
+ // function First: {0,30} giving a gentle slope -- the 30 comes from half the dynamic range
+ // Second: {-5,-30-(lv+30)/2} giving a faster slope from y=0 at x=-12 to y=-42.5 at x=-30
+ // Third: {-17,lv} giving a fast slope from y=0 at x=-19 to y=-60 at x=-30
#define order 3
@@ -838,7 +964,8 @@ uint64_t get_absolute_time_in_fp() {
return time_now_fp;
}
-ssize_t non_blocking_write(int fd, const void *buf, size_t count) {
+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;
@@ -847,7 +974,7 @@ ssize_t non_blocking_write(int fd, const void *buf, size_t count) {
// check that we can do some writing
ufds[0].fd = fd;
ufds[0].events = POLLOUT;
- rc = poll(ufds, 1, 5000);
+ 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) {
@@ -858,20 +985,24 @@ ssize_t non_blocking_write(int fd, const void *buf, size_t count) {
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 = -1;
+ rc = bytes_written; // to imitate the return from write()
} else {
ibuf += bytes_written;
bytes_remaining -= bytes_written;
}
}
}
- if (rc == 0)
+ if (rc > 0)
return count - bytes_remaining; // this is just to mimic a normal write/3.
else
return rc;
// return write(fd,buf,count);
}
+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.
+}
+
/* from
* http://coding.debuntu.org/c-implementing-str_replace-replace-all-occurrences-substring#comment-722
*/
@@ -948,9 +1079,7 @@ uint64_t r64u() { return (ranval(&rx)); }
int64_t r64i() { return (ranval(&rx) >> 1); }
/* generate an array of 64-bit random numbers */
-const int ranarraylength = 1009; // these will be 8-byte numbers.
-
-uint64_t *ranarray;
+const int ranarraylength = 1009 * 203; // these will be 8-byte numbers.
int ranarraynext;
@@ -975,9 +1104,11 @@ uint64_t ranarrayval() {
void r64arrayinit() { ranarrayinit(); }
-uint64_t ranarray64u() { return (ranarrayval()); }
+// uint64_t ranarray64u() { return (ranarrayval()); }
+uint64_t ranarray64u() { return (ranval(&rx)); }
-int64_t ranarray64i() { return (ranarrayval() >> 1); }
+// int64_t ranarray64i() { return (ranarrayval() >> 1); }
+int64_t ranarray64i() { return (ranval(&rx) >> 1); }
uint32_t nctohl(const uint8_t *p) { // read 4 characters from *p and do ntohl on them
// this is to avoid possible aliasing violations
@@ -1020,6 +1151,8 @@ void sps_nanosleep(const time_t sec, const long nanosec) {
int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time,
const char *debugmessage, int debuglevel) {
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
struct timespec tn;
clock_gettime(CLOCK_REALTIME, &tn);
uint64_t tnfpsec = tn.tv_sec;
@@ -1048,19 +1181,23 @@ 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();
int r = pthread_mutex_timedlock(mutex, &timeoutTime);
+ int64_t et = get_absolute_time_in_fp() - start_time;
- if ((r != 0) && (debugmessage != NULL)) {
+ if ((debuglevel != 0) && (r != 0) && (debugmessage != NULL)) {
+ et = (et * 1000000) >> 32; // microseconds
char errstr[1000];
if (r == ETIMEDOUT)
debug(debuglevel,
- "waiting for a mutex, maximum expected time of %d microseconds exceeded \"%s\".",
- dally_time, debugmessage);
+ "timed out waiting for a mutex, having waiting %f seconds, with a maximum "
+ "waiting time of %d microseconds. \"%s\".",
+ (1.0 * et) / 1000000, dally_time, debugmessage);
else
debug(debuglevel, "error %d: \"%s\" waiting for a mutex: \"%s\".", r,
strerror_r(r, errstr, sizeof(errstr)), debugmessage);
}
+ pthread_setcancelstate(oldState, NULL);
return r;
}
#endif
@@ -1068,17 +1205,20 @@ int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time,
int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time,
const char *debugmessage, int debuglevel) {
- useconds_t time_to_wait = dally_time;
+ // this is not pthread_cancellation safe because is contains a cancellation point
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
+ int time_to_wait = dally_time;
int r = pthread_mutex_trylock(mutex);
while ((r == EBUSY) && (time_to_wait > 0)) {
- useconds_t st = time_to_wait;
+ int st = time_to_wait;
if (st > 1000)
st = 1000;
- sps_nanosleep(0, st * 1000);
+ sps_nanosleep(0, st * 1000); // this contains a cancellation point
time_to_wait -= st;
r = pthread_mutex_trylock(mutex);
}
- if ((r != 0) && (debugmessage != NULL)) {
+ if ((debuglevel != 0) && (r != 0) && (debugmessage != NULL)) {
char errstr[1000];
if (r == EBUSY) {
debug(debuglevel,
@@ -1090,17 +1230,23 @@ int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time,
strerror_r(r, errstr, sizeof(errstr)), debugmessage);
}
}
+ pthread_setcancelstate(oldState, NULL);
return r;
}
#endif
-int _debug_mutex_lock(pthread_mutex_t *mutex, useconds_t dally_time, const char *filename,
- const int line, int debuglevel) {
+int _debug_mutex_lock(pthread_mutex_t *mutex, useconds_t dally_time, const char *mutexname,
+ const char *filename, const int line, int debuglevel) {
+ if ((debuglevel > debuglev) || (debuglevel == 0))
+ return pthread_mutex_lock(mutex);
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
uint64_t time_at_start = get_absolute_time_in_fp();
char dstring[1000];
memset(dstring, 0, sizeof(dstring));
snprintf(dstring, sizeof(dstring), "%s:%d", filename, line);
- debug(3, "debug_mutex_lock at \"%s\".", dstring);
+ if (debuglevel != 0)
+ debug(3, "mutex_lock \"%s\" at \"%s\".", mutexname, dstring); // only if you really ask for it!
int result = sps_pthread_mutex_timedlock(mutex, dally_time, dstring, debuglevel);
if (result == ETIMEDOUT) {
result = pthread_mutex_lock(mutex);
@@ -1108,37 +1254,54 @@ int _debug_mutex_lock(pthread_mutex_t *mutex, useconds_t dally_time, const char
uint64_t divisor = (uint64_t)1 << 32;
double delay = 1.0 * time_delay / divisor;
debug(debuglevel,
- "debug_mutex_lock at \"%s\" expected max wait: %0.9f, actual wait: %0.9f sec.", dstring,
- (1.0 * dally_time) / 1000000, delay);
+ "mutex_lock \"%s\" at \"%s\" expected max wait: %0.9f, actual wait: %0.9f sec.",
+ mutexname, dstring, (1.0 * dally_time) / 1000000, delay);
}
+ pthread_setcancelstate(oldState, NULL);
return result;
}
-int _debug_mutex_unlock(pthread_mutex_t *mutex, const char *filename, const int line,
- int debuglevel) {
+int _debug_mutex_unlock(pthread_mutex_t *mutex, const char *mutexname, const char *filename,
+ const int line, int debuglevel) {
+ if ((debuglevel > debuglev) || (debuglevel == 0))
+ return pthread_mutex_unlock(mutex);
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
char dstring[1000];
char errstr[512];
memset(dstring, 0, sizeof(dstring));
snprintf(dstring, sizeof(dstring), "%s:%d", filename, line);
- debug(debuglevel, "debug_mutex_unlock at \"%s\".", dstring);
+ debug(debuglevel, "mutex_unlock \"%s\" at \"%s\".", mutexname, dstring);
int r = pthread_mutex_unlock(mutex);
- if (r != 0)
- debug(1, "error %d: \"%s\" unlocking a mutex: \"%s\".", r,
- strerror_r(r, errstr, sizeof(errstr)), dstring);
+ if ((debuglevel != 0) && (r != 0))
+ debug(1, "error %d: \"%s\" unlocking mutex \"%s\" at \"%s\".", r,
+ strerror_r(r, errstr, sizeof(errstr)), mutexname, dstring);
+ pthread_setcancelstate(oldState, NULL);
return r;
}
+void malloc_cleanup(void *arg) {
+ // debug(1, "malloc cleanup called.");
+ free(arg);
+}
+
+void pthread_cleanup_debug_mutex_unlock(void *arg) { pthread_mutex_unlock((pthread_mutex_t *)arg); }
+
char *get_version_string() {
- char *version_string = malloc(200);
+ char *version_string = malloc(1024);
if (version_string) {
strcpy(version_string, PACKAGE_VERSION);
-#ifdef HAVE_LIBMBEDTLS
- strcat(version_string, "-mbedTLS");
+
+#ifdef CONFIG_LIBDAEMON
+ strcat(version_string, "-libdaemon");
+#endif
+#ifdef CONFIG_MBEDTLS
+ strcat(version_string, "-mbedTLS");
#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
strcat(version_string, "-PolarSSL");
#endif
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
strcat(version_string, "-OpenSSL");
#endif
#ifdef CONFIG_TINYSVCMDNS
@@ -1150,6 +1313,9 @@ char *get_version_string() {
#ifdef CONFIG_DNS_SD
strcat(version_string, "-dns_sd");
#endif
+#ifdef CONFIG_EXTERNAL_MDNS
+ strcat(version_string, "-external_mdns");
+#endif
#ifdef CONFIG_ALSA
strcat(version_string, "-ALSA");
#endif
@@ -1174,7 +1340,7 @@ char *get_version_string() {
#ifdef CONFIG_PIPE
strcat(version_string, "-pipe");
#endif
-#ifdef HAVE_LIBSOXR
+#ifdef CONFIG_SOXR
strcat(version_string, "-soxr");
#endif
#ifdef CONFIG_CONVOLUTION
@@ -1183,10 +1349,13 @@ char *get_version_string() {
#ifdef CONFIG_METADATA
strcat(version_string, "-metadata");
#endif
-#ifdef HAVE_DBUS
+#ifdef CONFIG_MQTT
+ strcat(version_string, "-mqtt");
+#endif
+#ifdef CONFIG_DBUS_INTERFACE
strcat(version_string, "-dbus");
#endif
-#ifdef HAVE_MPRIS
+#ifdef CONFIG_MPRIS_INTERFACE
strcat(version_string, "-mpris");
#endif
strcat(version_string, "-sysconfdir:");
@@ -1194,3 +1363,150 @@ char *get_version_string() {
}
return version_string;
}
+
+int64_t generate_zero_frames(char *outp, size_t number_of_frames, enum sps_format_t format,
+ int with_dither, int64_t random_number_in) {
+ // return the last random number used
+ // assuming the buffer has been assigned
+
+ int64_t previous_random_number = random_number_in;
+ char *p = outp;
+ size_t sample_number;
+ for (sample_number = 0; sample_number < number_of_frames * 2; sample_number++) {
+
+ int64_t hyper_sample = 0;
+ // add a TPDF dither -- see
+ // http://www.users.qwest.net/%7Evolt42/cadenzarecording/DitherExplained.pdf
+ // and the discussion around https://www.hydrogenaud.io/forums/index.php?showtopic=16963&st=25
+
+ // I think, for a 32 --> 16 bits, the range of
+ // random numbers needs to be from -2^16 to 2^16, i.e. from -65536 to 65536 inclusive, not from
+ // -32768 to +32767
+
+ // See the original paper at
+ // http://www.ece.rochester.edu/courses/ECE472/resources/Papers/Lipshitz_1992.pdf
+ // by Lipshitz, Wannamaker and Vanderkooy, 1992.
+
+ int64_t dither_mask = 0;
+ switch (format) {
+ case SPS_FORMAT_S32:
+ case SPS_FORMAT_S32_LE:
+ case SPS_FORMAT_S32_BE:
+ dither_mask = (int64_t)1 << (64 + 1 - 32);
+ break;
+ case SPS_FORMAT_S24:
+ case SPS_FORMAT_S24_LE:
+ case SPS_FORMAT_S24_BE:
+ case SPS_FORMAT_S24_3LE:
+ case SPS_FORMAT_S24_3BE:
+ dither_mask = (int64_t)1 << (64 + 1 - 24);
+ break;
+ case SPS_FORMAT_S16:
+ case SPS_FORMAT_S16_LE:
+ case SPS_FORMAT_S16_BE:
+ dither_mask = (int64_t)1 << (64 + 1 - 16);
+ break;
+ case SPS_FORMAT_S8:
+ case SPS_FORMAT_U8:
+ dither_mask = (int64_t)1 << (64 + 1 - 8);
+ break;
+ case SPS_FORMAT_UNKNOWN:
+ die("Unexpected SPS_FORMAT_UNKNOWN while calculating dither mask.");
+ break;
+ case SPS_FORMAT_AUTO:
+ die("Unexpected SPS_FORMAT_AUTO while calculating dither mask.");
+ break;
+ case SPS_FORMAT_INVALID:
+ die("Unexpected SPS_FORMAT_INVALID while calculating dither mask.");
+ break;
+ }
+ dither_mask -= 1;
+ // int64_t r = r64i();
+ int64_t r = ranarray64i();
+
+ int64_t tpdf = (r & dither_mask) - (previous_random_number & dither_mask);
+
+ // add dither if permitted -- no need to check for clipping, as the sample is, uh, zero
+
+ if (with_dither != 0)
+ hyper_sample += tpdf;
+
+ // move the result to the desired position in the int64_t
+ char *op = p;
+ int result; // this is the length of the sample
+
+ uint8_t byt;
+ switch (format) {
+ case SPS_FORMAT_S32:
+ hyper_sample >>= (64 - 32);
+ *(int32_t *)op = hyper_sample;
+ result = 4;
+ break;
+ case SPS_FORMAT_S24_3LE:
+ hyper_sample >>= (64 - 24);
+ byt = (uint8_t)hyper_sample;
+ *op++ = byt;
+ byt = (uint8_t)(hyper_sample >> 8);
+ *op++ = byt;
+ byt = (uint8_t)(hyper_sample >> 16);
+ *op++ = byt;
+ result = 3;
+ break;
+ case SPS_FORMAT_S24_3BE:
+ hyper_sample >>= (64 - 24);
+ byt = (uint8_t)(hyper_sample >> 16);
+ *op++ = byt;
+ byt = (uint8_t)(hyper_sample >> 8);
+ *op++ = byt;
+ byt = (uint8_t)hyper_sample;
+ *op++ = byt;
+ result = 3;
+ break;
+ case SPS_FORMAT_S24:
+ hyper_sample >>= (64 - 24);
+ *(int32_t *)op = hyper_sample;
+ result = 4;
+ break;
+ case SPS_FORMAT_S16_LE:
+ hyper_sample >>= (64 - 16);
+ byt = (uint8_t)hyper_sample;
+ *op++ = byt;
+ byt = (uint8_t)(hyper_sample >> 8);
+ *op++ = byt;
+ result = 2;
+ break;
+ case SPS_FORMAT_S16_BE:
+ hyper_sample >>= (64 - 16);
+ byt = (uint8_t)(hyper_sample >> 8);
+ *op++ = byt;
+ byt = (uint8_t)hyper_sample;
+ *op++ = byt;
+ result = 2;
+ break;
+ case SPS_FORMAT_S16:
+ hyper_sample >>= (64 - 16);
+ *(int16_t *)op = (int16_t)hyper_sample;
+ result = 2;
+ break;
+ case SPS_FORMAT_S8:
+ hyper_sample >>= (int8_t)(64 - 8);
+ *op = hyper_sample;
+ result = 1;
+ break;
+ case SPS_FORMAT_U8:
+ hyper_sample >>= (uint8_t)(64 - 8);
+ hyper_sample += 128;
+ *op = hyper_sample;
+ result = 1;
+ break;
+ default:
+ result = 0; // stop a compiler warning
+ die("Unexpected SPS_FORMAT_UNKNOWN while outputting samples");
+ }
+ p += result;
+ previous_random_number = r;
+ }
+ // hack
+ // memset(outp,0,number_of_frames * 4);
+ return previous_random_number;
+}
diff --git a/common.h b/common.h
index 87493cb..ae7f5cd 100644
--- a/common.h
+++ b/common.h
@@ -2,6 +2,7 @@
#define _COMMON_H
#include <libconfig.h>
+#include <pthread.h>
#include <signal.h>
#include <stdint.h>
#include <sys/socket.h>
@@ -22,13 +23,30 @@
#define SAFAMILY sa_family
#endif
-#if defined(HAVE_DBUS) || defined(HAVE_MPRIS)
+#if defined(CONFIG_DBUS_INTERFACE) || defined(CONFIG_MPRIS_INTERFACE)
enum dbus_session_type {
DBT_system = 0, // use the session bus
DBT_session, // use the system bus
} dbt_type;
#endif
+#define sps_extra_code_output_stalled 32768
+#define sps_extra_code_output_state_cannot_make_ready 32769
+
+// yeah/no/auto
+enum yna_type {
+ YNA_AUTO = -1,
+ YNA_NO = 0,
+ YNA_YES = 1
+} yna_type;
+
+// yeah/no/dont-care
+enum yndk_type {
+ YNDK_DONT_KNOW = -1,
+ YNDK_NO = 0,
+ YNDK_YES = 1
+} yndk_type;
+
enum endian_type {
SS_LITTLE_ENDIAN = 0,
SS_PDP_ENDIAN,
@@ -38,6 +56,7 @@ enum endian_type {
enum stuffing_type {
ST_basic = 0, // straight deletion or insertion of a frame in a 352-frame packet
ST_soxr, // use libsoxr to make a 352 frame packet one frame longer or shorter
+ ST_auto, // use soxr if compiled for it and if the soxr_index is low enough
} s_type;
enum playback_mode_type {
@@ -58,6 +77,12 @@ enum decoders_supported_type {
decoder_apple_alac,
} decoders_supported_type;
+enum disable_standby_mode_type {
+ disable_standby_off = 0,
+ disable_standby_auto,
+ disable_standby_always
+};
+
// the following enum is for the formats recognised -- currently only S16LE is recognised for input,
// so these are output only for the present
@@ -66,23 +91,37 @@ enum sps_format_t {
SPS_FORMAT_S8,
SPS_FORMAT_U8,
SPS_FORMAT_S16,
+ SPS_FORMAT_S16_LE,
+ SPS_FORMAT_S16_BE,
SPS_FORMAT_S24,
+ SPS_FORMAT_S24_LE,
+ SPS_FORMAT_S24_BE,
SPS_FORMAT_S24_3LE,
SPS_FORMAT_S24_3BE,
SPS_FORMAT_S32,
+ SPS_FORMAT_S32_LE,
+ SPS_FORMAT_S32_BE,
+ SPS_FORMAT_AUTO,
+ SPS_FORMAT_INVALID,
} sps_format_t;
+const char * sps_format_description_string(enum sps_format_t format);
+
typedef struct {
config_t *cfg;
+ int endianness;
double airplay_volume; // stored here for reloading when necessary
char *appName; // normally the app is called shairport-syn, but it may be symlinked
char *password;
char *service_name; // the name for the shairport service, e.g. "Shairport Sync Version %v running
// on host %h"
+
#ifdef CONFIG_PA
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.
+ // Defaults to "Shairport Sync". Shairport Sync must be playing to see it.
+
+ char *pa_sink; // the name (or id) of the sink that Shairport Sync will play on.
#endif
#ifdef CONFIG_METADATA
int metadata_enabled;
@@ -92,6 +131,22 @@ typedef struct {
size_t metadata_sockmsglength;
int get_coverart;
#endif
+#ifdef CONFIG_MQTT
+ int mqtt_enabled;
+ char *mqtt_hostname;
+ int mqtt_port;
+ char *mqtt_username;
+ char *mqtt_password;
+ char *mqtt_capath;
+ char *mqtt_cafile;
+ char *mqtt_certfile;
+ char *mqtt_keyfile;
+ char *mqtt_topic;
+ int mqtt_publish_raw;
+ int mqtt_publish_parsed;
+ int mqtt_publish_cover;
+ int mqtt_enable_remote;
+#endif
uint8_t hw_addr[6];
int port;
int udp_port_base;
@@ -114,28 +169,33 @@ typedef struct {
char *mdns_name;
mdns_backend *mdns;
int buffer_start_fill;
- int64_t userSuppliedLatency; // overrides all other latencies -- use with caution
- int64_t fixedLatencyOffset; // add this to all automatic latencies supplied to get the actual
- // total latency
+ uint32_t userSuppliedLatency; // overrides all other latencies -- use with caution
+ uint32_t fixedLatencyOffset; // add this to all automatic latencies supplied to get the actual
+ // total latency
// the total latency will be limited to the min and max-latency values, if supplied
+#ifdef CONFIG_LIBDAEMON
int daemonise;
int daemonise_store_pid; // don't try to save a PID file
char *piddir;
char *computed_piddir; // the actual pid directory to create, if any
+ char *pidfile;
+#endif
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
int statistics_requested, use_negotiated_latencies;
enum playback_mode_type playback_mode;
- char *cmd_start, *cmd_stop, *cmd_set_volume;
+ char *cmd_start, *cmd_stop, *cmd_set_volume, *cmd_unfixable;
+ char *cmd_active_start, *cmd_active_stop;
int cmd_blocking, cmd_start_returns_output;
double tolerance; // allow this much drift before attempting to correct it
enum stuffing_type packet_stuffing;
+ int soxr_delay_index;
+ int soxr_delay_threshold; // the soxr delay must be less or equal to this for soxr interpolation to be enabled under the auto setting
int decoders_supported;
int use_apple_decoder; // set to 1 if you want to use the apple decoder instead of the original by
// David Hammerton
- char *pidfile;
// char *logfile;
// char *errfile;
char *configfile;
@@ -145,14 +205,27 @@ typedef struct {
int interface_index; // only valid if the interface string is non-NULL
double audio_backend_buffer_desired_length; // this will be the length in seconds of the
// audio backend buffer -- the DAC buffer for ALSA
+ double audio_backend_buffer_interpolation_threshold_in_seconds; // below this, soxr interpolation
+ // will not occur -- it'll be
+ // basic interpolation instead.
+ double audio_backend_silence_threshold; // below this, silence will be added to the output buffer
+ double audio_backend_silence_scan_interval; // check the threshold this often
+
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
double audio_backend_silent_lead_in_time; // the length of the silence that should precede a play.
- uint32_t volume_range_db; // the range, in dB, from max dB to min dB. Zero means use the mixer's
- // native range.
- enum sps_format_t output_format;
+ 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
+ // native range.
+ int volume_range_hw_priority; // when extending the volume range by combining sw and hw attenuators, lowering the volume, use all the hw attenuation before using
+ // sw attenuation
enum volume_control_profile_type volume_control_profile;
- int output_rate;
+
+ int output_format_auto_requested; // true if the configuration requests auto configuration
+ enum sps_format_t output_format;
+ int output_rate_auto_requested; // true if the configuration requests auto configuration
+ unsigned int output_rate;
#ifdef CONFIG_CONVOLUTION
int convolution;
@@ -164,14 +237,19 @@ typedef struct {
int loudness;
float loudness_reference_volume_db;
int alsa_use_hardware_mute;
-#if defined(HAVE_DBUS)
+ double alsa_maximum_stall_time;
+ enum disable_standby_mode_type disable_standby_mode;
+ volatile int keep_dac_busy;
+ enum yna_type use_precision_timing; // defaults to no
+
+#if defined(CONFIG_DBUS_INTERFACE)
enum dbus_session_type dbus_service_bus_type;
#endif
-#if defined(HAVE_MPRIS)
+#if defined(CONFIG_MPRIS_INTERFACE)
enum dbus_session_type mpris_service_bus_type;
#endif
-#ifdef HAVE_METADATA_HUB
+#ifdef CONFIG_METADATA_HUB
char *cover_art_cache_dir;
int scan_interval_when_active; // number of seconds between DACP server scans when playing
// something (1)
@@ -185,6 +263,11 @@ typedef struct {
int disable_resend_requests; // set this to stop resend request being made for missing packets
double diagnostic_drop_packet_fraction; // pseudo randomly drop this fraction of packets, for
// debugging. Currently audio packets only...
+#ifdef CONFIG_JACK
+ char *jack_client_name;
+ char *jack_autoconnect_pattern;
+#endif
+
} shairport_cfg;
uint32_t nctohl(const uint8_t *p); // read 4 characters from *p and do ntohl on them
@@ -192,12 +275,16 @@ 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;
+
// true if Shairport Sync is supposed to be sending output to the output device, false otherwise
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
/* from
@@ -211,11 +298,24 @@ void r64init(uint64_t seed);
uint64_t r64u();
int64_t r64i();
+uint64_t *ranarray;
void r64arrayinit();
uint64_t ranarray64u();
int64_t ranarray64i();
-extern int debuglev;
+// if you are breaking in to a session, you need to avoid the ports of the current session
+// if you are law-abiding, then you can reuse the ports.
+// so, you can reset the free UDP ports minder when you're legit, and leave it otherwise
+
+// the downside of using different ports each time is that it might make the firewall
+// rules a bit more complex, as they need to allow more than the minimum three ports.
+// a range of 10 is suggested anyway
+
+void resetFreeUDPPort();
+uint16_t nextFreeUDPPort();
+
+volatile int debuglev;
+
void die(const char *format, ...);
void warn(const char *format, ...);
void inform(const char *format, ...);
@@ -244,20 +344,24 @@ uint64_t fp_time_at_startup, fp_time_at_last_debug_message;
// this is for reading an unsigned 32 bit number, such as an RTP timestamp
-long endianness;
uint32_t uatoi(const char *nptr);
+// this is for allowing us to cancel the whole program
+pthread_t main_thread_id;
+
shairport_cfg config;
config_t config_file_stuff;
+int config_set_lookup_bool(config_t *cfg, char *where, int *dst);
+
void command_start(void);
void command_stop(void);
+void command_execute(const char *command, const char *extra_argument, const int block);
void command_set_volume(double volume);
int mkpath(const char *path, mode_t mode);
void shairport_shutdown();
-// void shairport_startup_complete(void);
extern sigset_t pselect_sigset;
@@ -266,19 +370,30 @@ int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time,
const char *debugmessage, int debuglevel);
// wait for the specified time, checking every 20 milliseconds, and block if it can't acquire the
// lock
-int _debug_mutex_lock(pthread_mutex_t *mutex, useconds_t dally_time, const char *filename,
- const int line, int debuglevel);
+int _debug_mutex_lock(pthread_mutex_t *mutex, useconds_t dally_time, const char *mutexName,
+ const char *filename, const int line, int debuglevel);
+
+#define debug_mutex_lock(mu, t, d) _debug_mutex_lock(mu, t, #mu, __FILE__, __LINE__, d)
-#define debug_mutex_lock(mu, t, d) _debug_mutex_lock(mu, t, __FILE__, __LINE__, d)
+int _debug_mutex_unlock(pthread_mutex_t *mutex, const char *mutexName, const char *filename,
+ const int line, int debuglevel);
-int _debug_mutex_unlock(pthread_mutex_t *mutex, const char *filename, const int line,
- int debuglevel);
+#define debug_mutex_unlock(mu, d) _debug_mutex_unlock(mu, #mu, __FILE__, __LINE__, d)
-#define debug_mutex_unlock(mu, d) _debug_mutex_unlock(mu, __FILE__, __LINE__, d)
+void pthread_cleanup_debug_mutex_unlock(void *arg);
+
+#define pthread_cleanup_debug_mutex_lock(mu, t, d) \
+ if (_debug_mutex_lock(mu, t, #mu, __FILE__, __LINE__, d) == 0) \
+ pthread_cleanup_push(pthread_cleanup_debug_mutex_unlock, (void *)mu)
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, enum sps_format_t format,
+ int with_dither, int64_t random_number_in);
+
+void malloc_cleanup(void *arg);
+
#endif // _COMMON_H
diff --git a/configure.ac b/configure.ac
index 37f8a73..3df35d4 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2,10 +2,11 @@
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.50])
-AC_INIT([shairport-sync], [3.2.2], [mikebrady@eircom.net])
+AC_INIT([shairport-sync], [3.3], [mikebrady@eircom.net])
AM_INIT_AUTOMAKE
AC_CONFIG_SRCDIR([shairport.c])
AC_CONFIG_HEADERS([config.h])
+AC_PROG_RANLIB
# Derived from the Avahi configure.ac file
# Specifying the OS type, defaulting to linux.
@@ -32,25 +33,17 @@ if test "x${with_os}" = xopenbsd ; then
AC_CHECK_LIB([c],[clock_gettime], , AC_MSG_ERROR(libc needed))
fi
+AM_CONDITIONAL([BUILD_FOR_LINUX], [test "x${with_os}" = xlinux ])
AM_CONDITIONAL([BUILD_FOR_FREEBSD], [test "x${with_os}" = xfreebsd ])
-
AM_CONDITIONAL([BUILD_FOR_OPENBSD], [test "x${with_os}" = xopenbsd ])
+
##### Some build systems are not fully using pkg-config, so we can use the flag ${with_pkg_config} on a case-by-case basis
##### to control how to deal with them
AC_ARG_WITH([pkg_config],
[ --with-pkg-config = use pkg-config to find libraries], ,[with_pkg_config=yes])
-AC_MSG_RESULT(>>Including libdaemon)
-if test "x${with_pkg_config}" = xyes ; then
- PKG_CHECK_MODULES(
- [DAEMON], [libdaemon],
- [LIBS="${DAEMON_LIBS} ${LIBS}"])
-else
- AC_CHECK_LIB([daemon],[daemon_fork], , AC_MSG_ERROR(libdaemon needed))
-fi
-
##### The following check for the pthreads library doesn't put the compiler into the correct pthread mode
##### so we add the -pthread compilation flag in AMFLAGS in the Makefile.am as well.
@@ -62,18 +55,15 @@ if test "x${with_pkg_config}" = xyes ; then
PKG_CHECK_MODULES(
[POPT], [popt],
[CFLAGS="${POPT_CFLAGS} ${CFLAGS}"
- LIBS="${POPT_LIBS} ${LIBS}"
- AC_DEFINE([HAVE_LIBPOPT],[1],[Define to 1 if you have popt])])
+ LIBS="${POPT_LIBS} ${LIBS}"])
else
AC_CHECK_LIB([popt],[poptGetContext], , AC_MSG_ERROR(libpopt needed))
- AC_DEFINE([HAVE_LIBPOPT],[1],[Define to 1 if you have popt])
fi
AC_ARG_WITH([dummy],[ --with-dummy = include the dummy audio back end ],[AC_MSG_RESULT(>>Including the dummy audio back end) AC_DEFINE([CONFIG_DUMMY], 1, [Needed by the compiler.]) ], )
AM_CONDITIONAL([USE_DUMMY], [test "x$with_dummy" = "xyes" ])
AC_ARG_WITH([stdout],[ --with-stdout = include the stdout audio back end ],[ AC_MSG_RESULT(>>Including the stdout audio back end) AC_DEFINE([CONFIG_STDOUT], 1, [Needed by the compiler.]) ], )
-
AM_CONDITIONAL([USE_STDOUT], [test "x$with_stdout" = "xyes" ])
AC_ARG_WITH([pipe],[ --with-pipe = include the pipe audio back end ],[ AC_MSG_RESULT(>>Including the pipe audio back end) AC_DEFINE([CONFIG_PIPE], 1, [Needed by the compiler.]) ], )
@@ -95,6 +85,13 @@ AC_ARG_WITH([freebsd-service],
[ --with-freebsd-service = install a FreeBSD startup script during a make install], , )
AM_CONDITIONAL([INSTALL_FREEBSD_SERVICE], [test "x$with_freebsd_service" = "xyes"])
+AC_ARG_WITH([cygwin-service],
+[ --with-cygwin-service = install a CYGWIN config script during a make install], , )
+AM_CONDITIONAL([INSTALL_CYGWIN_SERVICE], [test "x$with_cygwin_service" = "xyes"])
+
+AC_ARG_WITH([external-mdns],[ --with-external-mdns = support the use of 'avahi-publish-service' or 'mDNSPublish' to advertise the service on Bonjour/ZeroConf ],[ AC_MSG_RESULT(>>Including external mdns support) AC_DEFINE([CONFIG_EXTERNAL_MDNS], 1, [Needed by the compiler.]) ], )
+AM_CONDITIONAL([USE_EXTERNAL_MDNS], [test "x$with_external_mdns" = "xyes" ])
+
# Add the libconfig package
if test "x${with_pkg_config}" = xyes ; then
PKG_CHECK_MODULES(
@@ -111,19 +108,16 @@ AM_CONDITIONAL([INSTALL_CONFIG_FILES], [test "x$with_configfiles" = "xyes"])
# Look for Apple ALAC flag
AC_ARG_WITH(apple-alac, [ --with-apple-alac = include support for the Apple ALAC decoder],
[AC_MSG_RESULT(>>Including the Apple ALAC Decoder)
- HAS_APPLE_ALAC=1
- AM_CONDITIONAL([USE_APPLE_ALAC], [test 0])
- AC_DEFINE([CONFIG_APPLE_ALAC], 1, [Needed by the compiler.])
+ AC_DEFINE([CONFIG_APPLE_ALAC], 1, [Include support for using the Apple ALAC Decoder])
+ REQUESTED_APPLE_ALAC=1
if test "x${with_pkg_config}" = xyes ; then
PKG_CHECK_MODULES(
[ALAC], [alac],
- [LIBS="${ALAC_LIBS} ${LIBS}"
- AC_DEFINE([HAVE_APPLE_ALAC],[1],[Define to 1 if you are using the Apple ALAC Decoder])])
+ [LIBS="${ALAC_LIBS} ${LIBS}"])
else
AC_CHECK_LIB([alac], [BitBufferInit], , AC_MSG_ERROR(Apple ALAC Decoder support requires the alac library!))
- AC_DEFINE([HAVE_APPLE_ALAC],[1],[Define to 1 if you have the Apple ALAC library])
fi ])
-AM_CONDITIONAL([USE_APPLE_ALAC], [test "x$HAS_APPLE_ALAC" = "x1"])
+AM_CONDITIONAL([USE_APPLE_ALAC], [test "x$REQUESTED_APPLE_ALAC" = "x1"])
# Look for piddir flag
AC_ARG_WITH(piddir, [ --with-piddir=<pathname> Specify a pathname to a directory in which to write the PID file.], [
@@ -133,10 +127,26 @@ AC_ARG_WITH(piddir, [ --with-piddir=<pathname> Specify a pathname to a directory
AC_MSG_ERROR(when you use the --with-piddir directive you must specify the pathname of the directory into which the PID file will be written)
fi
AC_MSG_RESULT(${with_piddir})
- HAS_CUSTOMPIDDIR=1
+ REQUESTED_CUSTOMPIDDIR=1
AC_SUBST(CUSTOM_PID_DIR,["${with_piddir}"])
- AC_DEFINE([USE_CUSTOM_PID_DIR],1,[Hook up special proc to libdaemon to point to this directory])], )
-AM_CONDITIONAL([USE_CUSTOMPIDDIR], [test "x$HAS_CUSTOMPIDDIR" = "x1"])
+ AC_DEFINE([DEFINED_CUSTOM_PID_DIR],1,[Hook up special proc to libdaemon to point to this directory])], )
+AM_CONDITIONAL([USE_CUSTOMPIDDIR], [test "x$REQUESTED_CUSTOMPIDDIR" = "x1"])
+
+
+# Look for libdaemon
+AC_ARG_WITH(libdaemon, [--with-libdaemon = include support for daemonising in non-systemd systems], REQUESTED_LIBDAEMON=1, )
+if test "x$REQUESTED_LIBDAEMON" = "x1"; then
+ AC_MSG_RESULT(>>Including libdaemon support for older systems)
+ AC_DEFINE([CONFIG_LIBDAEMON], 1, [Include libdaemon])
+ if test "x${with_pkg_config}" = xyes ; then
+ PKG_CHECK_MODULES(
+ [DAEMON], [libdaemon],
+ [LIBS="${DAEMON_LIBS} ${LIBS}"])
+ else
+ AC_CHECK_LIB([daemon],[daemon_fork], , AC_MSG_ERROR(libdaemon needed))
+ fi
+fi
+AM_CONDITIONAL([USE_LIBDAEMON], [test "x$REQUESTED_LIBDAEMON" = "x1"])
# Check --with-ssl=argument
AC_ARG_WITH(ssl, [ choose --with-ssl=openssl, --with-ssl=mbedtls or --with-ssl=polarssl (deprecated) for encryption services ], [
@@ -146,19 +156,17 @@ AC_ARG_WITH(ssl, [ choose --with-ssl=openssl, --with-ssl=mbedtls or --with-ssl=p
AC_MSG_ERROR(choose "openssl", "mbedtls" or "polarssl" encryption)
fi
if test "x${with_ssl}" = xopenssl ; then
+ AC_DEFINE([CONFIG_OPENSSL], 1, [Use the OpenSSL libraries for encryption and encoding and decoding])
if test "x${with_pkg_config}" = xyes ; then
PKG_CHECK_MODULES(
[SSL], [libssl,libcrypto],
- [LIBS="${SSL_LIBS} ${LIBS}"
- AC_DEFINE([HAVE_LIBCRYPTO],[1],[Define to 1 if you have libcrypto])
- AC_DEFINE([HAVE_LIBSSL],[1],[Define to 1 if you have libssl])])
+ [LIBS="${SSL_LIBS} ${LIBS}"])
else
AC_CHECK_LIB([crypto], [main], , AC_MSG_ERROR(libcrypto selected but the library cannot be found!))
AC_CHECK_LIB([ssl], [main], , AC_MSG_ERROR(libssl selected but the library cannot be found!))
- AC_DEFINE([HAVE_LIBCRYPTO],[1],[Define to 1 if you have libcrypto])
- AC_DEFINE([HAVE_LIBSSL],[1],[Define to 1 if you have libssl])
fi
elif test "x${with_ssl}" = xmbedtls ; then
+ AC_DEFINE([CONFIG_MBEDTLS], 1, [Use the mbed TLS libraries for encryption and encoding and decoding])
AC_CHECK_LIB([mbedtls],[mbedtls_ssl_init],,
[AC_MSG_ERROR([mbed tls support requires the mbedtls library -- libmbedtls-dev suggested],1)])
AC_CHECK_LIB([mbedcrypto], [mbedtls_entropy_func],,
@@ -166,6 +174,7 @@ AC_ARG_WITH(ssl, [ choose --with-ssl=openssl, --with-ssl=mbedtls or --with-ssl=p
AC_CHECK_LIB([mbedx509], [mbedtls_pk_init],,
[AC_MSG_ERROR([mbed tls support requires the mbedx509 library -- libmbedx509-0 suggested],1)])
elif test "x${with_ssl}" = xpolarssl ; then
+ AC_DEFINE([CONFIG_POLARSSL], 1, [Use the PolarSSL libraries for encryption and encoding and decoding])
AC_CHECK_LIB([polarssl],[ssl_init], , AC_MSG_ERROR(PolarSSL is selected but the library cannot be found and is deprecated. Consider selecting mbed TLS instead using --with-ssl=mbedtls.))
else
AC_MSG_ERROR(unknown option "${with_ssl}"." Please choose with "openssl", "mbedtls" or "polarssl")
@@ -175,13 +184,26 @@ AC_ARG_WITH(ssl, [ choose --with-ssl=openssl, --with-ssl=mbedtls or --with-ssl=p
# Look for soxr flag
AC_ARG_WITH(soxr, [ --with-soxr = choose libsoxr for high-quality interpolation], [
AC_MSG_RESULT(>>Including support for soxr-based interpolation)
- AC_CHECK_LIB([soxr],[soxr_create], , AC_MSG_ERROR(soxr support requested but libsoxr not found!))], )
+ AC_DEFINE([CONFIG_SOXR], 1, [Include support for using the SoX Resampler library for interpolation])
+ if test "x${with_pkg_config}" = xyes ; then
+ PKG_CHECK_MODULES(
+ [SOXR], [soxr],
+ [LIBS="${SOXR_LIBS} ${LIBS}"],
+ [AC_MSG_ERROR(soxr support requires the libsoxr library -- libsoxr-dev suggested!)])
+ else
+ AC_CHECK_LIB([avutil],[av_get_cpu_flags])
+ if test "x${ac_cv_lib_avutil_av_get_cpu_flags}" = xyes ; then
+ # soxr may link against libavutil, depending on the architecture, but for the sake of simplicity link with it if it is found
+ AC_CHECK_LIB([soxr],[soxr_create], , AC_MSG_ERROR(soxr support requires the libsoxr library -- libsoxr-dev suggested!), [-lavutil])
+ else
+ AC_CHECK_LIB([soxr],[soxr_create], , AC_MSG_ERROR(soxr support requires the libsoxr library -- libsoxr-dev suggested!))
+ fi
+ fi
+], )
-# Look for metadata flag -- set flag for conditional compilation
+# Look for metadata flag and resolve it further down the script
AC_ARG_WITH(metadata, [ --with-metadata = include support for a metadata feed], [
- AC_MSG_RESULT(>>Including metadata support)
- AC_DEFINE([CONFIG_METADATA], 1, [Needed by the compiler.])], )
-AM_CONDITIONAL([USE_METADATA], [test "x$with_metadata" = "xyes"])
+ REQUESTED_METADATA=1], )
# What follows is a bit messy, because if the relevant library is requested, a compiler flag is defined, a file is included in the compilation
# and the relevant link files are added.
@@ -189,64 +211,76 @@ AM_CONDITIONAL([USE_METADATA], [test "x$with_metadata" = "xyes"])
# Look for avahi flag
AC_ARG_WITH(avahi, [ --with-avahi = choose Avahi-based mDNS support], [
AC_MSG_RESULT(>>Including Avahi mDNS support)
- HAS_AVAHI=1
+ REQUESTED_AVAHI=1
AC_DEFINE([CONFIG_AVAHI], 1, [Needed by the compiler.])
AC_CHECK_LIB([avahi-client], [avahi_client_new], , AC_MSG_ERROR(Avahi support requires the avahi-client library!))
AC_CHECK_LIB([avahi-common],[avahi_strerror], , AC_MSG_ERROR(Avahi support requires the avahi-common library!))], )
-AM_CONDITIONAL([USE_AVAHI], [test "x$HAS_AVAHI" = "x1"])
+AM_CONDITIONAL([USE_AVAHI], [test "x$REQUESTED_AVAHI" = "x1"])
# Look for tinysvcmdns flag
AC_ARG_WITH(tinysvcmdns, [ --with-tinysvcmdns = choose tinysvcmdns-based mDNS support], [
AC_MSG_RESULT(>>Including tinysvcmdns mDNS support)
- HAS_TINYSVCMDNS=1
+ REQUESTED_TINYSVCMDNS=1
AC_DEFINE([CONFIG_TINYSVCMDNS], 1, [Needed by the compiler.])], )
-AM_CONDITIONAL([USE_TINYSVCMDNS], [test "x$HAS_TINYSVCMDNS" = "x1"])
+AM_CONDITIONAL([USE_TINYSVCMDNS], [test "x$REQUESTED_TINYSVCMDNS" = "x1"])
# Look for ALSA flag
AC_ARG_WITH(alsa, [ --with-alsa = choose ALSA API support (GNU/Linux only)],
[AC_MSG_RESULT(>>Including an ALSA back end)
- HAS_ALSA=1
- AM_CONDITIONAL([USE_ALSA], [test 0])
+ REQUESTED_ALSA=1
AC_DEFINE([CONFIG_ALSA], 1, [Needed by the compiler.])
if test "x${with_pkg_config}" = xyes ; then
PKG_CHECK_MODULES(
[ALSA], [alsa],
- [LIBS="${ALSA_LIBS} ${LIBS}"
- AC_DEFINE([HAVE_LIBASOUND],[1],[Define to 1 if you have ALSA])])
+ [LIBS="${ALSA_LIBS} ${LIBS}"])
else
AC_CHECK_LIB([asound], [snd_pcm_open], , AC_MSG_ERROR(ALSA support requires the asound library!))
- AC_DEFINE([HAVE_LIBASOUND],[1],[Define to 1 if you have ALSA])
fi ])
-AM_CONDITIONAL([USE_ALSA], [test "x$HAS_ALSA" = "x1"])
+AM_CONDITIONAL([USE_ALSA], [test "x$REQUESTED_ALSA" = "x1"])
+
+# Look for jack flag
+AC_ARG_WITH(jack, [ --with-jack = include a Jack Audio Connection Kit (jack) backend], [
+ AC_MSG_RESULT(>>Including a Jack Audio Connection Kit (jack) back end)
+ REQUESTED_JACK=1
+ AC_DEFINE([CONFIG_JACK], 1, [Needed by the compiler.])
+ if test "x${with_pkg_config}" = xyes ; then
+ PKG_CHECK_MODULES(
+ [JACK], [jack],
+ [LIBS="${JACK_LIBS} ${LIBS}"],
+ [AC_MSG_ERROR(Jack Audio Connection Kit support requires the jack library -- libjack-dev suggested!)])
+ else
+ AC_CHECK_LIB([jack], [jack_client_open], , AC_MSG_ERROR(Jack Audio Connection Kit support requires the jack library -- libjack-dev suggested!))
+ fi ])
+AM_CONDITIONAL([USE_JACK], [test "x$REQUESTED_JACK" = "x1"])
# Look for SNDIO flag
AC_ARG_WITH(sndio, [ --with-sndio = choose SNDIO API support], [
AC_MSG_RESULT(>>Including a SNDIO back end)
- HAS_SNDIO=1
+ REQUESTED_SNDIO=1
AC_DEFINE([CONFIG_SNDIO], 1, [Needed by the compiler.])
AC_CHECK_LIB([sndio], [sio_open], , AC_MSG_ERROR(SNDIO support requires the sndio library -- libsndio-dev suggested))], )
-AM_CONDITIONAL([USE_SNDIO], [test "x$HAS_SNDIO" = "x1"])
+AM_CONDITIONAL([USE_SNDIO], [test "x$REQUESTED_SNDIO" = "x1"])
# Look for AO flag
AC_ARG_WITH(ao, [ --with-ao = choose AO (Audio Output?) API support. N.B. no synchronisation -- so underflow or overflow is inevitable!], [
AC_MSG_RESULT(>>Including an AO back end. N.B. no synchronisation -- so underflow or overflow is inevitable!)
- HAS_AO=1
+ REQUESTED_AO=1
AC_DEFINE([CONFIG_AO], 1, [Needed by the compiler.])
AC_CHECK_LIB([ao], [ao_initialize], , AC_MSG_ERROR(AO support requires the ao library -- libao-dev suggested))], )
-AM_CONDITIONAL([USE_AO], [test "x$HAS_AO" = "x1"])
+AM_CONDITIONAL([USE_AO], [test "x$REQUESTED_AO" = "x1"])
# Look for Soundio flag
AC_ARG_WITH(soundio, [ --with-soundio = choose soundio API support.], [
AC_MSG_RESULT(>>Including an soundio back end)
- HAS_SOUNDIO=1
+ REQUESTED_SOUNDIO=1
AC_DEFINE([CONFIG_SOUNDIO], 1, [Needed by the compiler.])
AC_CHECK_LIB([soundio], [soundio_create], , AC_MSG_ERROR(soundio support requires the soundio library -- libsoundio-dev suggested))], )
-AM_CONDITIONAL([USE_SOUNDIO], [test "x$HAS_SOUNDIO" = "x1"])
+AM_CONDITIONAL([USE_SOUNDIO], [test "x$REQUESTED_SOUNDIO" = "x1"])
# Look for pulseaudio flag
AC_ARG_WITH(pa, [ --with-pa = choose PulseAudio support.], [
AC_MSG_RESULT(>>Including a PulseAudio back end.)
- HAS_PA=1
+ REQUESTED_PA=1
AC_DEFINE([CONFIG_PA], 1, [Needed by the compiler.])
if test "x${with_pkg_config}" = xyes ; then
PKG_CHECK_MODULES(
@@ -256,74 +290,86 @@ AC_ARG_WITH(pa, [ --with-pa = choose PulseAudio support.], [
AC_CHECK_LIB([pulse-simple], [pa_simple_new], , AC_MSG_ERROR(PulseAudio support requires the libpulse library!))
AC_CHECK_LIB([pulse], [pa_stream_peek], , AC_MSG_ERROR(PulseAudio support requires the libpulse-dev library.))
fi ])
-AM_CONDITIONAL([USE_PA], [test "x$HAS_PA" = "x1"])
+AM_CONDITIONAL([USE_PA], [test "x$REQUESTED_PA" = "x1"])
# Look for Convolution flag
AC_ARG_WITH(convolution, [ --with-convolution = choose audio DSP convolution support], [
AC_MSG_RESULT(>>Including convolution support)
- HAS_CONVOLUTION=1
+ REQUESTED_CONVOLUTION=1
AM_INIT_AUTOMAKE([subdir-objects])
AC_DEFINE([CONFIG_CONVOLUTION], 1, [Needed by the compiler.])
AC_CHECK_LIB([sndfile], [sf_open], , AC_MSG_ERROR(Convolution support requires the sndfile library!))], )
-AM_CONDITIONAL([USE_CONVOLUTION], [test "x$HAS_CONVOLUTION" = "x1"])
+AM_CONDITIONAL([USE_CONVOLUTION], [test "x$REQUESTED_CONVOLUTION" = "x1"])
# Look for dns_sd flag
AC_ARG_WITH(dns_sd, [ --with-dns_sd = choose dns_sd mDNS support], [
AC_MSG_RESULT(>>Including dns_sd for mDNS support)
- HAS_DNS_SD=1
- AC_DEFINE([CONFIG_HAVE_DNS_SD_H], 1, [Needed by the compiler.])
+ REQUESTED_DNS_SD=1
+ AC_DEFINE([CONFIG_DNS_SD], 1, [Needed by the compiler.])
AC_SEARCH_LIBS([DNSServiceRefDeallocate], [dns_sd], , AC_MSG_ERROR(dns_sd support requires the dns_sd library!))], )
-AM_CONDITIONAL([USE_DNS_SD], [test "x$HAS_DNS_SD" = "x1"])
+AM_CONDITIONAL([USE_DNS_SD], [test "x$REQUESTED_DNS_SD" = "x1"])
# Look for dbus flag
AC_ARG_WITH(dbus-interface, [ --with-dbus-interface = include support for the native Shairport Sync D-Bus interface], [
AC_MSG_RESULT(>>Including dbus support)
- HAS_DBUS=1
- AC_DEFINE([HAVE_DBUS], 1, [Needed by the compiler.])
+ AC_DEFINE([CONFIG_DBUS_INTERFACE], 1, [Include support for the native Shairport Sync D-Bus interface])
+ REQUESTED_DBUS=1
PKG_CHECK_MODULES([GIO_UNIX], [gio-unix-2.0 >= 2.30.0],[CFLAGS="${GIO_UNIX_CFLAGS} ${CFLAGS}" LIBS="${GIO_UNIX_LIBS} ${LIBS}"],[AC_MSG_ERROR(dbus messaging support requires the glib 2.0 library -- libglib2.0-dev suggested!)])
], )
-AM_CONDITIONAL([USE_DBUS], [test "x$HAS_DBUS" = "x1"])
+AM_CONDITIONAL([USE_DBUS], [test "x$REQUESTED_DBUS" = "x1"])
# Look for dbus test client flag
AC_ARG_WITH(dbus-test-client, [ --with-dbus-test-client = compile dbus test client], [
AC_MSG_RESULT(>>Including dbus test client)
- HAS_DBUS_CLIENT=1
- AC_DEFINE([HAVE_DBUS_CLIENT], 1, [Needed by the compiler.])
+ REQUESTED_DBUS_CLIENT=1
PKG_CHECK_MODULES([GIO_UNIX], [gio-unix-2.0 >= 2.30.0],[CFLAGS="${GIO_UNIX_CFLAGS} ${CFLAGS}" LIBS="${GIO_UNIX_LIBS} ${LIBS}"],[AC_MSG_ERROR(dbus client support requires the glib 2.0 library -- libglib2.0-dev suggested!)])
], )
-AM_CONDITIONAL([USE_DBUS_CLIENT], [test "x$HAS_DBUS_CLIENT" = "x1"])
+AM_CONDITIONAL([USE_DBUS_CLIENT], [test "x$REQUESTED_DBUS_CLIENT" = "x1"])
# Look for mpris flag
-AC_ARG_WITH(mpris-interface, [ --with-mpris-interface = include support for a Shairport Sync D-Bus interface conforming to the MPRIS standard], [
+AC_ARG_WITH(mpris-interface, [ --with-mpris-interface = include support for a D-Bus interface conforming to the MPRIS standard], [
AC_MSG_RESULT(>>Including the MPRIS D-Bus Interface)
- HAS_MPRIS=1
- AC_DEFINE([HAVE_MPRIS], 1, [Needed by the compiler.])
+ AC_DEFINE([CONFIG_MPRIS_INTERFACE], 1, [Include support for a D-Bus interface conforming to the MPRIS standard])
+ REQUESTED_MPRIS=1
PKG_CHECK_MODULES([GIO_UNIX], [gio-unix-2.0 >= 2.30.0],[CFLAGS="${GIO_UNIX_CFLAGS} ${CFLAGS}" LIBS="${GIO_UNIX_LIBS} ${LIBS}"],[AC_MSG_ERROR(dbus messaging support for mpris requires the glib 2.0 library -- libglib2.0-dev suggested!)])
], )
-AM_CONDITIONAL([USE_MPRIS], [test "x$HAS_MPRIS" = "x1"])
+AM_CONDITIONAL([USE_MPRIS], [test "x$REQUESTED_MPRIS" = "x1"])
# Look for mpris test client flag
AC_ARG_WITH(mpris-test-client, [ --with-mpris-test-client = compile mpris test client], [
AC_MSG_RESULT(>>Including mpris test client)
- HAS_MPRIS_CLIENT=1
- AC_DEFINE([HAVE_MPRIS_CLIENT], 1, [Needed by the compiler.])
+ REQUESTED_MPRIS_CLIENT=1
PKG_CHECK_MODULES([GIO_UNIX], [gio-unix-2.0 >= 2.30.0],[CFLAGS="${GIO_UNIX_CFLAGS} ${CFLAGS}" LIBS="${GIO_UNIX_LIBS} ${LIBS}"],[AC_MSG_ERROR(mpris client support requires the glib 2.0 library -- libglib2.0-dev suggested!)])
], )
-AM_CONDITIONAL([USE_MPRIS_CLIENT], [test "x$HAS_MPRIS_CLIENT" = "x1"])
-
-#AM_CONDITIONAL([USE_DBUS_CORE_AND_DACP], [test "x$HAS_MPRIS" = "x1" -o "x$HAS_DBUS" = "x1"])
+AM_CONDITIONAL([USE_MPRIS_CLIENT], [test "x$REQUESTED_MPRIS_CLIENT" = "x1"])
+
+# Look for mqtt flag
+AC_ARG_WITH(mqtt-client, [ --with-mqtt-client = include a client for MQTT -- the Message Queuing Telemetry Transport protocol], [
+ AC_DEFINE([CONFIG_MQTT], 1, [Include a client for MQTT, the Message Queuing Telemetry Transport protocol])
+ AC_MSG_RESULT(>>Including MQTT support)
+ REQUESTED_MQTT=1
+ AC_CHECK_LIB([mosquitto], [mosquitto_lib_init], , AC_MSG_ERROR(MQTT support requires the mosquitto library -- libmosquitto-dev suggested!))
+ ],)
+AM_CONDITIONAL([USE_MQTT], [test "x$REQUESTED_MQTT" = "x1"])
+
+if test "x$REQUESTED_MQTT" = "x1" && test "x$REQUESTED_AVAHI" != "x1"; then
+ AC_MSG_WARN([>>MQTT needs Avahi to allow remote control functionality. Only Metadata publishing will be supported])
+fi
-if test "x$HAS_MPRIS" = "x1" -o "x$HAS_DBUS" = "x1" ; then
- AC_MSG_RESULT(>>Including the metadata hub)
- HAS_METADATA_HUB=1
- AC_DEFINE([HAVE_METADATA_HUB], 1, [Needed by the compiler.])
- AC_MSG_RESULT(>>Including the DACP client)
- HAS_DACP_CLIENT=1
- AC_DEFINE([HAVE_DACP_CLIENT], 1, [Needed by the compiler.])
+if test "x$REQUESTED_MPRIS" = "x1" || test "x$REQUESTED_DBUS" = "x1" || test "x$REQUESTED_MQTT" = "x1"; then
+ AC_MSG_RESULT(>>Including extended metadata and DACP client support)
+ REQUESTED_EXTENDED_METADATA_SUPPORT=1
+ AC_DEFINE([CONFIG_METADATA_HUB], 1, [Needed by the compiler.])
+ AC_DEFINE([CONFIG_DACP_CLIENT], 1, [Needed by the compiler.])
fi
-AM_CONDITIONAL([USE_METADATA_HUB], [test "x$HAS_METADATA_HUB" = "x1"])
-AM_CONDITIONAL([USE_DACP_CLIENT], [test "x$HAS_DACP_CLIENT" = "x1"])
+AM_CONDITIONAL([USE_METADATA_HUB], [test "x$REQUESTED_EXTENDED_METADATA_SUPPORT" = "x1"])
+AM_CONDITIONAL([USE_DACP_CLIENT], [test "x$REQUESTED_EXTENDED_METADATA_SUPPORT" = "x1"])
+if test "x$REQUESTED_EXTENDED_METADATA_SUPPORT" = "x1" || test "x$REQUESTED_METADATA" = "x1"; then
+ AC_MSG_RESULT(>>Including metadata support)
+ AC_DEFINE([CONFIG_METADATA], 1, [Needed by the compiler.])
+fi
+AM_CONDITIONAL([USE_METADATA], [test "x$REQUESTED_METADATA" = "x1"])
if test "x${with_systemd}" = xyes ; then
# Find systemd unit dir
@@ -340,9 +386,6 @@ if test "x${with_systemd}" = xyes ; then
[with_systemdsystemunitdir="$def_systemdsystemunitdir"])])
AS_IF([test "x$with_systemdsystemunitdir" != "xno"],
[AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir])])
- AM_CONDITIONAL([HAVE_SYSTEMD], [test "x$with_systemdsystemunitdir" != "xno"])
-else
- AM_CONDITIONAL([HAVE_SYSTEMD], false)
fi
# Look for xmltoman
diff --git a/dacp.c b/dacp.c
index 53b7661..cea3e7f 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
+ * Copyright (c) Mike Brady 2017 -- 2019
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -58,8 +58,10 @@ typedef struct {
void *port_monitor_private_storage;
} dacp_server_record;
+int dacp_monitor_initialised = 0;
pthread_t dacp_monitor_thread;
dacp_server_record dacp_server;
+void *mdns_dacp_monitor_private_storage_pointer;
// HTTP Response data/funcs (See the tinyhttp example.cpp file for more on this.)
struct HttpResponse {
@@ -90,8 +92,7 @@ void response_body(void *opaque, const char *data, int size) {
if (t)
response->body = t;
else {
- debug(1, "Can't allocate any more space for parser.\n");
- exit(-1);
+ die("dacp: can't allocate any more space for parser.");
}
}
memcpy(response->body + response->size, data, size);
@@ -110,7 +111,10 @@ static void response_code(void *opaque, int code) {
}
static const struct http_funcs responseFuncs = {
- response_realloc, response_body, response_header, response_code,
+ response_realloc,
+ response_body,
+ response_header,
+ response_code,
};
// static pthread_mutex_t dacp_conversation_lock = PTHREAD_MUTEX_INITIALIZER;
@@ -119,6 +123,30 @@ 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;
+void addrinfo_cleanup(void *arg) {
+ // debug(1, "addrinfo cleanup called.");
+ struct addrinfo **info = (struct addrinfo **)arg;
+ freeaddrinfo(*info);
+}
+
+void mutex_lock_cleanup(void *arg) {
+ pthread_mutex_t *m = (pthread_mutex_t *)arg;
+ if (pthread_mutex_unlock(m))
+ debug(1, "Error releasing mutex.");
+}
+
+void connect_cleanup(void *arg) {
+ int *fd = (int *)arg;
+ // debug(2, "dacp_send_command: close socket %d.",*fd);
+ close(*fd);
+}
+
+void http_cleanup(void *arg) {
+ // debug(1, "http cleanup called.");
+ struct http_roundtripper *rt = (struct http_roundtripper *)arg;
+ http_free(rt);
+}
+
int dacp_send_command(const char *command, char **body, ssize_t *bodysize) {
// will malloc space for the body or set it to NULL -- the caller should free it.
@@ -163,13 +191,14 @@ 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();
+ pthread_cleanup_push(addrinfo_cleanup, (void *)&res);
// only do this one at a time -- not sure it is necessary, but better safe than sorry
int mutex_reply = sps_pthread_mutex_timedlock(&dacp_conversation_lock, 2000000, command, 1);
// int mutex_reply = pthread_mutex_lock(&dacp_conversation_lock);
if (mutex_reply == 0) {
- // debug(1,"dacp_conversation_lock acquired for command \"%s\".",command);
+ pthread_cleanup_push(mutex_lock_cleanup, (void *)&dacp_conversation_lock);
// make a socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
@@ -178,18 +207,21 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) {
// debug(1, "DACP socket could not be created -- error %d: \"%s\".",errno,strerror(errno));
response.code = 497; // Can't establish a socket to the DACP server
} else {
+ pthread_cleanup_push(connect_cleanup, (void *)&sockfd);
+ // debug(2, "dacp_send_command: open socket %d.",sockfd);
+
struct timeval tv;
- tv.tv_sec = 2;
- tv.tv_usec = 0;
+ tv.tv_sec = 0;
+ tv.tv_usec = 80000;
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) == -1)
- debug(1, "Error %d setting receive timeout for DACP service.", 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, "Error %d setting send timeout for DACP service.", errno);
+ 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) {
- debug(3, "DACP connect failed with errno %d.", errno);
+ debug(3, "dacp_send_command: connect failed with errno %d.", errno);
response.code = 496; // Can't connect to the DACP server
} else {
// debug(1,"DACP connect succeeded.");
@@ -199,20 +231,32 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) {
command, dacp_server.ip_string, dacp_server.port, dacp_server.active_remote_id);
// Send command
- // debug(1,"DACP connect message: \"%s\".",message);
- if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof tv) == -1)
- debug(1, "Error %d setting send timeout for DACP service.", errno);
- if (send(sockfd, message, strlen(message), 0) != (ssize_t)strlen(message)) {
- // debug(1, "Send failed");
+ debug(3, "dacp_send_command: \"%s\".", command);
+ ssize_t wresp = send(sockfd, message, strlen(message), 0);
+ if (wresp == -1) {
+ char errorstring[1024];
+ strerror_r(errno, (char *)errorstring, sizeof(errorstring));
+ debug(2, "dacp_send_command: write error %d: \"%s\".", errno, (char *)errorstring);
+ struct linger so_linger;
+ so_linger.l_onoff = 1; // "true"
+ so_linger.l_linger = 0;
+ int err = setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof so_linger);
+ if (err)
+ debug(1, "Could not set the dacp socket to abort due to a write error on closing.");
+ }
+ if (wresp != (ssize_t)strlen(message)) {
+ // debug(1, "dacp_send_command: send failed.");
response.code = 493; // Client failed to send a message
} else {
response.body = malloc(2048); // it can resize this if necessary
response.malloced_size = 2048;
+ pthread_cleanup_push(malloc_cleanup, response.body);
struct http_roundtripper rt;
http_init(&rt, responseFuncs, &response);
+ pthread_cleanup_push(http_cleanup, &rt);
int needmore = 1;
int looperror = 0;
@@ -221,12 +265,24 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) {
while (needmore && !looperror) {
const char *data = buffer;
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) == -1)
- debug(1, "Error %d setting receive timeout for DACP service.", errno);
- int ndata = recv(sockfd, buffer, sizeof(buffer), 0);
+ debug(1, "dacp_send_command: error %d setting receive timeout.", errno);
+ ssize_t ndata = recv(sockfd, buffer, sizeof(buffer), 0);
// debug(3, "Received %d bytes: \"%s\".", ndata, buffer);
if (ndata <= 0) {
- debug(1, "dacp_send_command -- error receiving response for command \"%s\".",
- command);
+ if (ndata == -1) {
+ char errorstring[1024];
+ strerror_r(errno, (char *)errorstring, sizeof(errorstring));
+ debug(2, "dacp_send_command: receiving error %d: \"%s\".", errno,
+ (char *)errorstring);
+ struct linger so_linger;
+ so_linger.l_onoff = 1; // "true"
+ so_linger.l_linger = 0;
+ int err = setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof so_linger);
+ if (err)
+ debug(1,
+ "Could not set the dacp socket to abort due to a read error on closing.");
+ }
+
free(response.body);
response.body = NULL;
response.malloced_size = 0;
@@ -244,28 +300,41 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) {
}
if (http_iserror(&rt)) {
- debug(1, "Error parsing data.");
+ debug(3, "dacp_send_command: error parsing data.");
free(response.body);
response.body = NULL;
response.malloced_size = 0;
response.size = 0;
}
// debug(1,"Size of response body is %d",response.size);
- http_free(&rt);
+ pthread_cleanup_pop(1); // this should call http_cleanup
+ // http_free(&rt);
+ pthread_cleanup_pop(
+ 0); // this should *not* free the malloced buffer -- just pop the malloc cleanup
}
}
- close(sockfd);
- // debug(1,"DACP socket closed.");
+ pthread_cleanup_pop(1); // this should close the socket
+ // close(sockfd);
+ // debug(1,"DACP socket closed.");
}
- pthread_mutex_unlock(&dacp_conversation_lock);
+ pthread_cleanup_pop(1); // this should unlock the dacp_conversation_lock);
+ // pthread_mutex_unlock(&dacp_conversation_lock);
// debug(1,"Sent command\"%s\" with a response body of size %d.",command,response.size);
// debug(1,"dacp_conversation_lock released.");
} else {
- debug(3, "Could not acquire a lock on the dacp transmit/receive section when attempting to "
- "send the command \"%s\". Possible timeout?",
+ debug(3,
+ "dacp_send_command: could not acquire a lock on the dacp transmit/receive section "
+ "when attempting to "
+ "send the command \"%s\". Possible timeout?",
command);
response.code = 494; // This client is already busy
}
+ 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
+ debug(3, "dacp_send_command: %f seconds, response code %d, command \"%s\".",
+ (1.0 * et) / 1000000, response.code, command);
}
*body = response.body;
*bodysize = response.size;
@@ -290,12 +359,10 @@ void relinquish_dacp_server_information(rtsp_conn_info *conn) {
// as the conn's connection number
// this is to signify that the player has stopped, but only if another thread (with a different
// index) hasn't already taken over the dacp service
- sps_pthread_mutex_timedlock(
- &dacp_server_information_lock, 500000,
- "set_dacp_server_information couldn't get DACP server information lock in 0.5 second!.", 2);
+ debug_mutex_lock(&dacp_server_information_lock, 500000, 2);
if (dacp_server.players_connection_thread_index == conn->connection_number)
dacp_server.players_connection_thread_index = 0;
- pthread_mutex_unlock(&dacp_server_information_lock);
+ debug_mutex_unlock(&dacp_server_information_lock, 3);
}
// this will be running on the thread of its caller, not of the conversation thread...
@@ -305,14 +372,12 @@ void relinquish_dacp_server_information(rtsp_conn_info *conn) {
// Thus, we can keep the DACP port that might have previously been discovered
void set_dacp_server_information(rtsp_conn_info *conn) {
// debug(1, "set_dacp_server_information");
- sps_pthread_mutex_timedlock(
- &dacp_server_information_lock, 500000,
- "set_dacp_server_information couldn't get DACP server information lock in 0.5 second!.", 2);
+ debug_mutex_lock(&dacp_server_information_lock, 500000, 2);
dacp_server.players_connection_thread_index = conn->connection_number;
if ((conn->dacp_id == NULL) || (strcmp(conn->dacp_id, dacp_server.dacp_id) != 0)) {
if (conn->dacp_id)
- strncpy(dacp_server.dacp_id, conn->dacp_id, sizeof(dacp_server.dacp_id));
+ strncpy(dacp_server.dacp_id, conn->dacp_id, sizeof(dacp_server.dacp_id)-1);
else
dacp_server.dacp_id[0] = '\0';
dacp_server.port = 0;
@@ -320,14 +385,10 @@ void set_dacp_server_information(rtsp_conn_info *conn) {
dacp_server.connection_family = conn->connection_ip_family;
dacp_server.scope_id = conn->self_scope_id;
strncpy(dacp_server.ip_string, conn->client_ip_string, INET6_ADDRSTRLEN);
- debug(2, "set_dacp_server_information set IP to \"%s\" and DACP id to \"%s\".",
+ debug(3, "set_dacp_server_information set IP to \"%s\" and DACP id to \"%s\".",
dacp_server.ip_string, dacp_server.dacp_id);
- if (dacp_server.port_monitor_private_storage) // if there's is a monitor already active...
- mdns_dacp_dont_monitor(dacp_server.port_monitor_private_storage); // let it go.
- dacp_server.port_monitor_private_storage =
- mdns_dacp_monitor(dacp_server.dacp_id); // create a new one for us if a DACP-ID is provided,
- // otherwise will return a NULL
+ mdns_dacp_monitor_set_id(dacp_server.dacp_id);
metadata_hub_modify_prolog();
int ch = metadata_store.dacp_server_active != dacp_server.scan_enable;
@@ -353,26 +414,25 @@ void set_dacp_server_information(rtsp_conn_info *conn) {
}
dacp_server.active_remote_id = conn->dacp_active_remote; // even if the dacp_id remains the same,
// the active remote will change.
- debug(2, "set_dacp_server_information set active-remote id to %" PRIu32 ".",
+ debug(3, "set_dacp_server_information set active-remote id to %" PRIu32 ".",
dacp_server.active_remote_id);
pthread_cond_signal(&dacp_server_information_cv);
- pthread_mutex_unlock(&dacp_server_information_lock);
+ debug_mutex_unlock(&dacp_server_information_lock, 3);
}
void dacp_monitor_port_update_callback(char *dacp_id, uint16_t port) {
- debug(2, "dacp_monitor_port_update_callback with Remote ID \"%s\" and port number %d.", dacp_id,
- port);
- sps_pthread_mutex_timedlock(
- &dacp_server_information_lock, 500000,
- "dacp_monitor_port_update_callback couldn't get DACP server information lock in 0.5 second!.",
- 2);
+ debug_mutex_lock(&dacp_server_information_lock, 500000, 2);
+ debug(3,
+ "dacp_monitor_port_update_callback with Remote ID \"%s\", target ID \"%s\" and port "
+ "number %d.",
+ dacp_id, dacp_server.dacp_id, port);
if (strcmp(dacp_id, dacp_server.dacp_id) == 0) {
dacp_server.port = port;
if (port == 0)
dacp_server.scan_enable = 0;
else {
dacp_server.scan_enable = 1;
- debug(2, "dacp_monitor_port_update_callback enables scan");
+ // debug(2, "dacp_monitor_port_update_callback enables scan");
}
// metadata_hub_modify_prolog();
// int ch = metadata_store.dacp_server_active != dacp_server.scan_enable;
@@ -382,8 +442,14 @@ void dacp_monitor_port_update_callback(char *dacp_id, uint16_t port) {
debug(1, "dacp port monitor reporting on an out-of-use remote.");
}
pthread_cond_signal(&dacp_server_information_cv);
+ debug_mutex_unlock(&dacp_server_information_lock, 3);
+}
+
+void dacp_monitor_thread_code_cleanup(__attribute__((unused)) void *arg) {
+ // debug(1, "dacp_monitor_thread_code_cleanup called.");
pthread_mutex_unlock(&dacp_server_information_lock);
}
+
void *dacp_monitor_thread_code(__attribute__((unused)) void *na) {
int scan_index = 0;
// char server_reply[10000];
@@ -397,14 +463,18 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) {
sps_pthread_mutex_timedlock(
&dacp_server_information_lock, 500000,
"dacp_monitor_thread_code couldn't get DACP server information lock in 0.5 second!.", 2);
+ int32_t the_volume;
+
+ pthread_cleanup_push(dacp_monitor_thread_code_cleanup, NULL);
if (dacp_server.scan_enable == 0) {
metadata_hub_modify_prolog();
int ch = (metadata_store.dacp_server_active != 0) ||
(metadata_store.advanced_dacp_server_active != 0);
metadata_store.dacp_server_active = 0;
metadata_store.advanced_dacp_server_active = 0;
- debug(2, "setting dacp_server_active and advanced_dacp_server_active to 0 with an update "
- "flag value of %d",
+ debug(2,
+ "setting dacp_server_active and 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) {
@@ -416,7 +486,6 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) {
idle_scan_count = 0;
}
scan_index++;
- int32_t the_volume;
result = dacp_get_volume(&the_volume); // just want the http code
if ((result == 496) || (result == 403) || (result == 501)) {
@@ -435,10 +504,12 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) {
if ((bad_result_count == config.scan_max_bad_response_count) ||
(idle_scan_count == config.scan_max_inactive_count)) {
- debug(1, "DACP server status scanning stopped.");
+ debug(2, "DACP server status scanning stopped.");
dacp_server.scan_enable = 0;
}
- pthread_mutex_unlock(&dacp_server_information_lock);
+ pthread_cleanup_pop(1);
+
+ // 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);
@@ -764,7 +835,7 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) {
sleep(config.scan_interval_when_inactive);
}
}
- debug(1, "DACP monitor thread exiting.");
+ debug(1, "DACP monitor thread exiting -- should never happen.");
pthread_exit(NULL);
}
@@ -787,6 +858,8 @@ void dacp_monitor_start() {
rc = pthread_mutex_init(&dacp_conversation_lock, &mta);
if (rc)
debug(1, "Error creating the DACP Conversation Lock Mutex Init");
+ // else
+ // debug(1, "DACP Conversation Lock Mutex Init");
rc = pthread_mutexattr_destroy(&mta);
if (rc)
@@ -813,7 +886,20 @@ void dacp_monitor_start() {
debug(1, "Error creating the DACP Server Information Lock Attr Destroy");
memset(&dacp_server, 0, sizeof(dacp_server_record));
+
pthread_create(&dacp_monitor_thread, NULL, dacp_monitor_thread_code, NULL);
+ dacp_monitor_initialised = 1;
+}
+
+void dacp_monitor_stop() {
+ if (dacp_monitor_initialised) { // only if it's been started and initialised
+ debug(2, "dacp_monitor_stop");
+ pthread_cancel(dacp_monitor_thread);
+ pthread_join(dacp_monitor_thread, NULL);
+ pthread_mutex_destroy(&dacp_server_information_lock);
+ debug(3, "DACP Conversation Lock Mutex Destroyed");
+ pthread_mutex_destroy(&dacp_conversation_lock);
+ }
}
uint32_t dacp_tlv_crawl(char **p, int32_t *length) {
@@ -857,10 +943,14 @@ int dacp_get_client_volume(int32_t *result) {
debug(1, "Too short a response from getproperty?properties=dmcp.volume");
}
// debug(1, "Overall Volume is %d.", overall_volume);
+ }
+
+ if (server_reply) {
+ // debug(1, "Freeing response memory.");
free(server_reply);
- } /* else {
- debug(1, "Unexpected response %d to dacp volume control request", response);
- } */
+ server_reply = NULL;
+ }
+
if (result) {
*result = overall_volume;
// debug(1,"dacp_get_client_volume returns: %" PRId32 ".",overall_volume);
@@ -914,12 +1004,13 @@ int dacp_get_speaker_list(dacp_spkr_stuff *speaker_info, int max_size_of_array,
sp -= item_size;
le -= 8;
speaker_index++;
- if (speaker_index == max_size_of_array)
+ if (speaker_index == max_size_of_array) {
return 413; // Payload Too Large -- too many speakers
+ }
speaker_info[speaker_index].active = 0;
speaker_info[speaker_index].speaker_number = 0;
speaker_info[speaker_index].volume = 0;
- speaker_info[speaker_index].name = NULL;
+ speaker_info[speaker_index].name[0] = '\0';
} else {
le -= item_size + 8;
char *t;
@@ -929,8 +1020,10 @@ int dacp_get_speaker_list(dacp_spkr_stuff *speaker_info, int max_size_of_array,
switch (type) {
case 'minm':
t = sp - item_size;
- speaker_info[speaker_index].name = strndup(t, item_size);
- // debug(1," \"%s\"",speaker_info[speaker_index].name);
+ strncpy((char *)&speaker_info[speaker_index].name, t,
+ sizeof(speaker_info[speaker_index].name));
+ speaker_info[speaker_index].name[sizeof(speaker_info[speaker_index].name) - 1] =
+ '\0'; // just in case
break;
case 'cmvo':
t = sp - item_size;
@@ -995,7 +1088,12 @@ int dacp_get_speaker_list(dacp_spkr_stuff *speaker_info, int max_size_of_array,
free(server_reply);
server_reply = NULL;
} else {
- debug(1, "Unexpected response %d to dacp speakers request", response);
+ // debug(1, "Unexpected response %d to dacp speakers request", response);
+ if (server_reply) {
+ debug(1, "Freeing response memory.");
+ free(server_reply);
+ server_reply = NULL;
+ }
}
if (actual_speaker_count)
*actual_speaker_count = speaker_count;
diff --git a/dacp.h b/dacp.h
index 09dd477..ce1e127 100644
--- a/dacp.h
+++ b/dacp.h
@@ -10,10 +10,11 @@ typedef struct dacp_speaker_stuff {
int64_t speaker_number;
int active;
int32_t volume;
- char *name; // this is really just for debugging
+ char name[128]; // this is really just for debugging
} dacp_spkr_stuff;
void dacp_monitor_start();
+void dacp_monitor_stop();
uint32_t dacp_tlv_crawl(
char **p,
@@ -26,6 +27,9 @@ int dacp_get_speaker_list(dacp_spkr_stuff *speaker_array, int max_size_of_array,
void set_dacp_server_information(rtsp_conn_info *conn); // tell the DACP conversation thread that
// the dacp server information has been set
// or changed
+void relinquish_dacp_server_information(rtsp_conn_info *conn); // tell the DACP conversation thread
+ // that the player thread is no
+ // longer associated with it.
void dacp_monitor_port_update_callback(
char *dacp_id, uint16_t port); // a callback to say the port is no longer in use
int send_simple_dacp_command(const char *command);
diff --git a/dbus-service.c b/dbus-service.c
index ebcd337..47c5b84 100644
--- a/dbus-service.c
+++ b/dbus-service.c
@@ -15,10 +15,14 @@
#include "dbus-service.h"
+int service_is_running = 0;
+
ShairportSyncDiagnostics *shairportSyncDiagnosticsSkeleton = NULL;
ShairportSyncRemoteControl *shairportSyncRemoteControlSkeleton = NULL;
ShairportSyncAdvancedRemoteControl *shairportSyncAdvancedRemoteControlSkeleton = NULL;
+guint ownerID = 0;
+
void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) void *userdata) {
char response[100];
const char *th;
@@ -43,16 +47,15 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused))
shairport_sync_advanced_remote_control_set_available(shairportSyncAdvancedRemoteControlSkeleton,
FALSE);
}
-
+
if (argc->progress_string) {
- // debug(1, "Check progress string");
- th = shairport_sync_remote_control_get_progress_string(
- shairportSyncRemoteControlSkeleton);
- if ((th == NULL) || (strcasecmp(th, argc->progress_string) != 0)) {
- // debug(1, "Progress string should be changed");
- shairport_sync_remote_control_set_progress_string(
- shairportSyncRemoteControlSkeleton, argc->progress_string);
- }
+ // debug(1, "Check progress string");
+ th = shairport_sync_remote_control_get_progress_string(shairportSyncRemoteControlSkeleton);
+ if ((th == NULL) || (strcasecmp(th, argc->progress_string) != 0)) {
+ // debug(1, "Progress string should be changed");
+ shairport_sync_remote_control_set_progress_string(shairportSyncRemoteControlSkeleton,
+ argc->progress_string);
+ }
}
switch (argc->player_state) {
@@ -162,7 +165,8 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused))
if ((argc->track_metadata) && (argc->track_metadata->item_id)) {
char trackidstring[128];
// debug(1, "Set ID using mper ID: \"%u\".",argc->item_id);
- snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/mper_%u", argc->track_metadata->item_id);
+ snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/mper_%u",
+ argc->track_metadata->item_id);
GVariant *trackid = g_variant_new("o", trackidstring);
g_variant_builder_add(dict_builder, "{sv}", "mpris:trackid", trackid);
}
@@ -376,14 +380,27 @@ gboolean notify_verbosity_callback(ShairportSyncDiagnostics *skeleton,
return TRUE;
}
+gboolean notify_disable_standby_callback(ShairportSync *skeleton,
+ __attribute__((unused)) gpointer user_data) {
+ // debug(1, "\"notify_disable_standby_callback\" called.");
+ if (shairport_sync_get_disable_standby(skeleton)) {
+ debug(1, ">> activating disable standby");
+ config.keep_dac_busy = 1;
+ } else {
+ debug(1, ">> deactivating disable standby");
+ config.keep_dac_busy = 0;
+ }
+ return TRUE;
+}
+
gboolean notify_loudness_filter_active_callback(ShairportSync *skeleton,
__attribute__((unused)) gpointer user_data) {
- debug(1, "\"notify_loudness_filter_active_callback\" called.");
+ // debug(1, "\"notify_loudness_filter_active_callback\" called.");
if (shairport_sync_get_loudness_filter_active(skeleton)) {
- debug(1, "activating loudness filter");
+ debug(1, ">> activating loudness filter");
config.loudness = 1;
} else {
- debug(1, "deactivating loudness filter");
+ debug(1, ">> deactivating loudness filter");
config.loudness = 0;
}
return TRUE;
@@ -393,19 +410,62 @@ gboolean notify_loudness_threshold_callback(ShairportSync *skeleton,
__attribute__((unused)) gpointer user_data) {
gdouble th = shairport_sync_get_loudness_threshold(skeleton);
if ((th <= 0.0) && (th >= -100.0)) {
- debug(1, "Setting loudness threshhold to %f.", th);
+ debug(1, ">> setting loudness threshhold to %f.", th);
config.loudness_reference_volume_db = th;
} else {
- debug(1, "Invalid loudness threshhold: %f. Ignored.", th);
+ debug(1, ">> invalid loudness threshhold: %f. Ignored.", th);
shairport_sync_set_loudness_threshold(skeleton, config.loudness_reference_volume_db);
}
return TRUE;
}
+gboolean notify_drift_tolerance_callback(ShairportSync *skeleton,
+ __attribute__((unused)) gpointer user_data) {
+ gdouble dt = shairport_sync_get_drift_tolerance(skeleton);
+ if ((dt >= 0.0) && (dt <= 2.0)) {
+ debug(1, ">> setting drift tolerance to %f seconds", dt);
+ config.tolerance = dt;
+ } else {
+ debug(1, ">> invalid drift tolerance: %f seconds. Ignored.", dt);
+ shairport_sync_set_drift_tolerance(skeleton, config.tolerance);
+ }
+ return TRUE;
+}
+
+gboolean notify_disable_standby_mode_callback(ShairportSync *skeleton,
+ __attribute__((unused)) gpointer user_data) {
+ char *th = (char *)shairport_sync_get_disable_standby_mode(skeleton);
+ if ((strcasecmp(th, "no") == 0) || (strcasecmp(th, "off") == 0) || (strcasecmp(th, "never") == 0)) {
+ config.disable_standby_mode = disable_standby_off;
+ config.keep_dac_busy = 0;
+ } else if ((strcasecmp(th, "yes") == 0) || (strcasecmp(th, "on") == 0) || (strcasecmp(th, "always") == 0)) {
+ config.disable_standby_mode = disable_standby_always;
+ config.keep_dac_busy = 1;
+ } else if (strcasecmp(th, "auto") == 0)
+ config.disable_standby_mode = disable_standby_auto;
+ else {
+ warn("An unrecognised disable_standby_mode: \"%s\" was requested via D-Bus interface.", th);
+ switch (config.disable_standby_mode) {
+ case disable_standby_off:
+ shairport_sync_set_disable_standby_mode(skeleton, "off");
+ break;
+ case disable_standby_always:
+ shairport_sync_set_disable_standby_mode(skeleton, "always");
+ break;
+ case disable_standby_auto:
+ shairport_sync_set_disable_standby_mode(skeleton, "auto");
+ break;
+ default:
+ break;
+ }
+ }
+ return TRUE;
+}
+
gboolean notify_alacdecoder_callback(ShairportSync *skeleton,
__attribute__((unused)) gpointer user_data) {
char *th = (char *)shairport_sync_get_alacdecoder(skeleton);
-#ifdef HAVE_APPLE_ALAC
+#ifdef CONFIG_APPLE_ALAC
if (strcasecmp(th, "hammerton") == 0)
config.use_apple_decoder = 0;
else if (strcasecmp(th, "apple") == 0)
@@ -436,11 +496,13 @@ gboolean notify_alacdecoder_callback(ShairportSync *skeleton,
gboolean notify_interpolation_callback(ShairportSync *skeleton,
__attribute__((unused)) gpointer user_data) {
char *th = (char *)shairport_sync_get_interpolation(skeleton);
-#ifdef HAVE_LIBSOXR
+#ifdef CONFIG_SOXR
if (strcasecmp(th, "basic") == 0)
config.packet_stuffing = ST_basic;
else if (strcasecmp(th, "soxr") == 0)
config.packet_stuffing = ST_soxr;
+ else if (strcasecmp(th, "auto") == 0)
+ config.packet_stuffing = ST_auto;
else {
warn("An unrecognised interpolation method: \"%s\" was requested via the D-Bus interface.", th);
switch (config.packet_stuffing) {
@@ -450,6 +512,9 @@ gboolean notify_interpolation_callback(ShairportSync *skeleton,
case ST_soxr:
shairport_sync_set_interpolation(skeleton, "soxr");
break;
+ case ST_auto:
+ shairport_sync_set_interpolation(skeleton, "auto");
+ break;
default:
debug(1, "This should never happen!");
shairport_sync_set_interpolation(skeleton, "basic");
@@ -544,6 +609,18 @@ gboolean notify_loop_status_callback(ShairportSyncAdvancedRemoteControl *skeleto
return TRUE;
}
+static gboolean on_handle_quit(ShairportSync *skeleton, GDBusMethodInvocation *invocation,
+ __attribute__((unused)) const gchar *command,
+ __attribute__((unused)) gpointer user_data) {
+ debug(1, "quit requested (native interface)");
+ if (main_thread_id)
+ debug(1, "Cancelling main thread results in %d.", pthread_cancel(main_thread_id));
+ else
+ debug(1, "Main thread ID is NULL.");
+ shairport_sync_complete_quit(skeleton, invocation);
+ return TRUE;
+}
+
static gboolean on_handle_remote_command(ShairportSync *skeleton, GDBusMethodInvocation *invocation,
const gchar *command,
__attribute__((unused)) gpointer user_data) {
@@ -582,12 +659,20 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
G_CALLBACK(notify_interpolation_callback), NULL);
g_signal_connect(shairportSyncSkeleton, "notify::alacdecoder",
G_CALLBACK(notify_alacdecoder_callback), NULL);
- g_signal_connect(shairportSyncSkeleton, "notify::volume-control-profile",
+ g_signal_connect(shairportSyncSkeleton, "notify::disable-standby-mode",
+ G_CALLBACK(notify_disable_standby_mode_callback), NULL);
+ g_signal_connect(shairportSyncSkeleton, "notify::volume-control-profile",
G_CALLBACK(notify_volume_control_profile_callback), NULL);
+ g_signal_connect(shairportSyncSkeleton, "notify::disable-standby",
+ G_CALLBACK(notify_disable_standby_callback), NULL);
g_signal_connect(shairportSyncSkeleton, "notify::loudness-filter-active",
G_CALLBACK(notify_loudness_filter_active_callback), NULL);
g_signal_connect(shairportSyncSkeleton, "notify::loudness-threshold",
G_CALLBACK(notify_loudness_threshold_callback), NULL);
+ g_signal_connect(shairportSyncSkeleton, "notify::drift-tolerance",
+ G_CALLBACK(notify_drift_tolerance_callback), NULL);
+
+ g_signal_connect(shairportSyncSkeleton, "handle-quit", G_CALLBACK(on_handle_quit), NULL);
g_signal_connect(shairportSyncSkeleton, "handle-remote-command",
G_CALLBACK(on_handle_remote_command), NULL);
@@ -644,23 +729,62 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
shairport_sync_set_loudness_threshold(SHAIRPORT_SYNC(shairportSyncSkeleton),
config.loudness_reference_volume_db);
+ shairport_sync_set_drift_tolerance(SHAIRPORT_SYNC(shairportSyncSkeleton), config.tolerance);
-#ifdef HAVE_APPLE_ALAC
- if (config.use_apple_decoder == 0)
+#ifdef CONFIG_APPLE_ALAC
+ if (config.use_apple_decoder == 0) {
shairport_sync_set_alacdecoder(SHAIRPORT_SYNC(shairportSyncSkeleton), "hammerton");
- else
+ debug(1, ">> ALACDecoder set to \"hammerton\"");
+ } else {
shairport_sync_set_alacdecoder(SHAIRPORT_SYNC(shairportSyncSkeleton), "apple");
+ debug(1, ">> ALACDecoder set to \"apple\"");
+ }
#else
shairport_sync_set_alacdecoder(SHAIRPORT_SYNC(shairportSyncSkeleton), "hammerton");
+ debug(1, ">> ALACDecoder set to \"hammerton\"");
+
#endif
-#ifdef HAVE_SOXR
- if (config.packet_stuffing == ST_basic)
+ shairport_sync_set_active(SHAIRPORT_SYNC(shairportSyncSkeleton), FALSE);
+ debug(1, ">> Active set to \"false\"");
+
+ switch (config.disable_standby_mode) {
+ case disable_standby_off:
+ shairport_sync_set_disable_standby_mode(SHAIRPORT_SYNC(shairportSyncSkeleton), "off");
+ debug(1, ">> disable standby mode set to \"off\"");
+ break;
+ case disable_standby_always:
+ shairport_sync_set_disable_standby_mode(SHAIRPORT_SYNC(shairportSyncSkeleton), "always");
+ debug(1, ">> disable standby mode set to \"always\"");
+ break;
+ case disable_standby_auto:
+ shairport_sync_set_disable_standby_mode(SHAIRPORT_SYNC(shairportSyncSkeleton), "auto");
+ debug(1, ">> disable standby mode set to \"auto\"");
+ break;
+ default:
+ debug(1,"invalid disable_standby mode!");
+ break;
+ }
+
+#ifdef CONFIG_SOXR
+ if (config.packet_stuffing == ST_basic) {
shairport_sync_set_interpolation(SHAIRPORT_SYNC(shairportSyncSkeleton), "basic");
- else
+ debug(1, ">> interpolation set to \"basic\" (soxr support built in)");
+ } else if (config.packet_stuffing == ST_auto) {
+ shairport_sync_set_interpolation(SHAIRPORT_SYNC(shairportSyncSkeleton), "auto");
+ debug(1, ">> interpolation set to \"auto\" (soxr support built in)");
+ } else {
shairport_sync_set_interpolation(SHAIRPORT_SYNC(shairportSyncSkeleton), "soxr");
+ debug(1, ">> interpolation set to \"soxr\"");
+ }
#else
- shairport_sync_set_interpolation(SHAIRPORT_SYNC(shairportSyncSkeleton), "basic");
+ if (config.packet_stuffing == ST_basic) {
+ shairport_sync_set_interpolation(SHAIRPORT_SYNC(shairportSyncSkeleton), "basic");
+ debug(1, ">> interpolation set to \"basic\" (no soxr support)");
+ } else if (config.packet_stuffing == ST_auto) {
+ shairport_sync_set_interpolation(SHAIRPORT_SYNC(shairportSyncSkeleton), "auto");
+ debug(1, ">> interpolation set to \"auto\" (no soxr support)");
+ }
#endif
if (config.volume_control_profile == VCP_standard)
@@ -668,6 +792,12 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
else
shairport_sync_set_volume_control_profile(SHAIRPORT_SYNC(shairportSyncSkeleton), "flat");
+ if (config.keep_dac_busy == 0) {
+ shairport_sync_set_disable_standby(SHAIRPORT_SYNC(shairportSyncSkeleton), FALSE);
+ } else {
+ shairport_sync_set_disable_standby(SHAIRPORT_SYNC(shairportSyncSkeleton), TRUE);
+ }
+
if (config.loudness == 0) {
shairport_sync_set_loudness_filter_active(SHAIRPORT_SYNC(shairportSyncSkeleton), FALSE);
} else {
@@ -725,6 +855,7 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
debug(1, "Shairport Sync native D-Bus service started at \"%s\" on the %s bus.", name,
(config.dbus_service_bus_type == DBT_session) ? "session" : "system");
+ service_is_running = 1;
}
static void on_dbus_name_lost_again(__attribute__((unused)) GDBusConnection *connection,
@@ -760,7 +891,20 @@ int start_dbus_service() {
dbus_bus_type = G_BUS_TYPE_SESSION;
// debug(1, "Looking for a Shairport Sync native D-Bus interface \"org.gnome.ShairportSync\" on
// the %s bus.",(config.dbus_service_bus_type == DBT_session) ? "session" : "system");
- g_bus_own_name(dbus_bus_type, "org.gnome.ShairportSync", G_BUS_NAME_OWNER_FLAGS_NONE, NULL,
- on_dbus_name_acquired, on_dbus_name_lost, NULL, NULL);
+ ownerID = g_bus_own_name(dbus_bus_type, "org.gnome.ShairportSync", G_BUS_NAME_OWNER_FLAGS_NONE,
+ NULL, on_dbus_name_acquired, on_dbus_name_lost, NULL, NULL);
return 0; // this is just to quieten a compiler warning
}
+
+void stop_dbus_service() {
+ debug(2, "stopping dbus service");
+ if (ownerID)
+ g_bus_unown_name(ownerID);
+ else
+ debug(1, "Zero OwnerID for \"org.gnome.ShairportSync\".");
+ service_is_running = 0;
+}
+
+int dbus_service_is_running() {
+ return service_is_running;
+}
diff --git a/dbus-service.h b/dbus-service.h
index ea6e976..5cfc02e 100644
--- a/dbus-service.h
+++ b/dbus-service.h
@@ -6,5 +6,7 @@
ShairportSync *shairportSyncSkeleton;
int start_dbus_service();
+void stop_dbus_service();
+int dbus_service_is_running();
#endif /* #ifndef DBUS_SERVICE_H */
diff --git a/documents/sample dbus commands b/documents/sample dbus commands
index 0a7c398..2380415 100644
--- a/documents/sample dbus commands
+++ b/documents/sample dbus commands
@@ -20,4 +20,9 @@ dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSy
#Remote Control commands include: Play, Pause, PlayPause, Resume, Stop, Next, Previous, VolumeUp, VolumeDown, ToggleMute, FastForward, Rewind, ShuffleSongs
# Set Volume using Advanced Remote Control
-dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.AdvancedRemoteControl.SetVolume int32:50 \ No newline at end of file
+dbus-send --system --print-reply --type=method_call --dest=org.gnome.ShairportSync '/org/gnome/ShairportSync' org.gnome.ShairportSync.AdvancedRemoteControl.SetVolume int32:50
+
+# Get Drift Tolerance
+dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Get string:org.gnome.ShairportSync string:DriftTolerance
+# Set Drift Tolerance to 1 millisecond
+dbus-send --print-reply --system --dest=org.gnome.ShairportSync /org/gnome/ShairportSync org.freedesktop.DBus.Properties.Set string:org.gnome.ShairportSync string:DriftTolerance variant:double:0.001
diff --git a/man/shairport-sync.7 b/man/shairport-sync.7
index 9ff7dd1..0116333 100644
--- a/man/shairport-sync.7
+++ b/man/shairport-sync.7
@@ -2,30 +2,30 @@
.SH NAME
shairport-sync \- Synchronised Audio Player for iTunes / AirPlay
.SH SYNOPSIS
-\fBshairport-sync [-djvw]\fB [-a \fB\fIname\fB]\fB [-A \fB\fIlatency\fB]\fB [-B \fB\fIcommand\fB]\fB [-c \fB\fIconfigurationfile\fB]\fB [-E \fB\fIcommand\fB]\fB [--get-cover-art]\fB [--logOutputLevel]\fB [-L \fB\fIlatency\fB]\fB [-m \fB\fIbackend\fB]\fB [--meta-dir=\fB\fIdirectory\fB]\fB [-o \fB\fIbackend\fB]\fB [--password=\fB\fIsecret\fB]\fB [-r \fB\fIthreshold\fB]\fB [--statistics]\fB [-S \fB\fImode\fB]\fB [-t \fB\fItimeout\fB]\fB [--tolerance=\fB\fIframes\fB]\fB [-- \fB\fIaudio_backend_options\fB]\fB
-
-shairport-sync -D\fB
+\fBshairport-sync [-djvuw]\fB [-a \fB\fIname\fB]\fB [-A \fB\fIlatency\fB]\fB [-B \fB\fIcommand\fB]\fB [-c \fB\fIconfigurationfile\fB]\fB [-E \fB\fIcommand\fB]\fB [--get-cover-art]\fB [--logOutputLevel]\fB [-L \fB\fIlatency\fB]\fB [-m \fB\fIbackend\fB]\fB [--meta-dir=\fB\fIdirectory\fB]\fB [-o \fB\fIbackend\fB]\fB [--password=\fB\fIsecret\fB]\fB [-r \fB\fIthreshold\fB]\fB [--statistics]\fB [-S \fB\fImode\fB]\fB [-t \fB\fItimeout\fB]\fB [--tolerance=\fB\fIframes\fB]\fB [-- \fB\fIaudio_backend_options\fB]\fB
shairport-sync -k\fB
shairport-sync -h\fB
-shairport-sync -R\fB
-
shairport-sync -V\fB
\f1
.SH 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) or to a PulseAudio output stream (available on Linux).
+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.
-A feature of shairport-sync is that it 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 step with one another. This allows multiple devices to play the same source without getting out of phase 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 audio synchronised audio output 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 stream metadata 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 \fBshairport-sync\f1. Both names are used in these man pages.
.SH CONFIGURATION FILE SETTINGS
-You should use the configuration file for setting up shairport-sync. This file is usually \fIshairport-sync.conf\f1 and is generally located in the System Configuration Directory, which is normally the \fI/etc\f1 directory in Linux or the \fI/usr/local/etc\f1 directory in BSD unixes. You may need to have root privileges to modify it.
+You should use the configuration file for setting up Shairport Sync. This file is usually \fIshairport-sync.conf\f1 and is generally located in the System Configuration Directory, which is normally the \fI/etc\f1 directory in Linux or the \fI/usr/local/etc\f1 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 \fI$ shairport-sync -V\f1. One of the items in the output string is the value of the \fBsysconfdir\f1, i.e. the System Configuration Directory.)
@@ -77,15 +77,12 @@ Require the password \fIpassword\f1 to connect to the service. If you leave this
\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.
.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.
+\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.
.TP
\fBmdns_backend=\f1\fI"backend"\f1\fB;\f1
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 \fIbackend\f1. The default is "avahi". Perform the command \fBshairport-sync -h\f1 to get a list of available mDNS modules.
.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.
-.TP
\fBport=\f1\fIportnumber\f1\fB;\f1
Use this to specify the \fIportnumber\f1 shairport-sync uses to listen for service requests from iTunes, etc. The default is port 5000.
.TP
@@ -101,15 +98,9 @@ Allow playback to drift up to \fIseconds\f1 out of exact synchronization before
\fBresync_threshold_in_seconds=\f1\fIthreshold\f1\fB;\f1
Resynchronise if timings differ by more than \fIthreshold\f1 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 \fBresync_threshold\f1 setting.
.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.
-.TP
\fBignore_volume_control=\f1\fI"choice"\f1\fB;\f1
Set this \fIchoice\f1 to \fI"yes"\f1 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 \fI"no"\f1.
.TP
-\fBvolume_max_db=\f1\fIdBvalue\f1\fB;\f1
-Specify the maximum output level to be used with the hardware mixer, if used. If no hardware mixed is used, this setting speciies the maximum setting permissible in the software mixer, which has an attenuation of from 0.0 dB down to -96.3 dB.
-.TP
\fBvolume_range_db=\f1\fIdBvalue\f1\fB;\f1
Use this \fIdBvalue\f1 to reduce or increase the attenuation range, in decibels, between the minimum and maximum volume.
@@ -121,39 +112,98 @@ As a third example, you can actually extend the range provided by a mixer. Many
If you omit this setting, the native range of the mixer is used.
.TP
+\fBvolume_max_db=\f1\fIdBvalue\f1\fB;\f1
+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.
+.TP
+\fBvolume_control_profile=\f1\fI"choice"\f1\fB;\f1
+Use this advanced setting to specify how the airplay volume is transferred to the mixer volume. The \fI"standard"\f1 profile, which is the default, makes the volume change more quickly at lower volumes and slower at higher volumes. Choose the \fI"flat"\f1 profile to makes the volume change at the same rate at all volume levels.
+.TP
+\fBvolume_range_combined_hardware_priority=\f1 \fI"choice"\f1\fB;\f1
+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 \fI"yes"\f1 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 \fI"no"\f1 means that software attenuation is used to bring the remaining attenuation required into the range offered by the hardware attenuator. This is the default.
+.TP
+\fBrun_this_when_volume_is_set=\f1 \fI"/full/path/to/application/and/args"\f1\fB;\f1
+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 \fI#!/bin/...\f1 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".
+.TP
\fBregtype=\f1\fI"regTypeString"\f1\fB;\f1
Use this advanced setting to set the service type and transport to be advertised by Zeroconf/Bonjour. Default is \fI"_raop._tcp"\f1.
.TP
\fBplayback_mode=\f1\fI"mode"\f1\fB;\f1
-The \fImode\f1 can be "stereo", "mono", "reverse stereo", "both left" or "both right". Default is "stereo".
-.TP
-\fBinterface=\f1\fI"name"\f1\fB;\f1
-Use this advanced setting if you want to confine Shairport Sync to the named interface. Leave it commented out to get the default bahaviour.
+The \fImode\f1 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.
.TP
\fBalac_decoder=\f1\fI"decodername"\f1\fB;\f1
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.
.TP
-\fBaudio_backend_latency_offset_in_seconds=\f1\fIoffset_in_seconds\f1\fB;\f1
+\fBinterface=\f1\fI"name"\f1\fB;\f1
+Use this advanced setting if you want to confine Shairport Sync to the named interface. Leave it commented out to get the default bahaviour.
+.TP
+\fBaudio_backend_latency_offset_in_seconds=\f1 \fIoffset_in_seconds\f1\fB;\f1
Set this \fIoffset_in_seconds\f1 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.
.TP
-\fBaudio_backend_buffer_desired_length_in_seconds=\f1\fIlength_in_seconds\f1\fB;\f1
+\fBaudio_backend_buffer_desired_length_in_seconds=\f1 \fIlength_in_seconds\f1\fB;\f1
Use this \fIlength_in_seconds\f1 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.
.TP
-\fBaudio_backend_silent_lead_in_time=\f1\fIlead_in_time_in_seconds\f1\fB;\f1
+\fBaudio_backend_buffer_interpolation_threshold_in_seconds=\f1 \fItime_in_seconds\f1\fB;\f1
+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.
+.TP
+\fBaudio_backend_silent_lead_in_time=\f1 \fIlead_in_time_in_seconds\f1\fB;\f1
This is an advanced setting. Use the \fIlead_in_time_in_seconds\f1 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.
.TP
-\fBrun_this_when_volume_is_set=\f1\fI"/full/path/to/application/and/args"\f1\fB;\f1
-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 \fI#!/bin/...\f1 as appropriate.
+\fBdbus_service_bus=\f1 \fI"bus_name"\f1\fB;\f1
+If shairport sync is compiled with the D-Bus interface, it can offer it on the \fI"system"\f1 or the \fI"session"\f1 D-Bus "bus". Use this to specify which. The default is to use the "system" bus.
+.TP
+\fBmpris_service_bus=\f1 \fI"bus_name"\f1\fB;\f1
+If shairport sync is compiled with the MPRIS interface, it can offer the service on the \fI"system"\f1 or the \fI"session"\f1 D-Bus "bus". Use this to specify which. The default is to use the "system" bus.
+.TP
+\fB"SESSIONCONTROL" SETTINGS\f1
+.TP
+\fBrun_this_before_play_begins=\f1\fI"/path/to/application and args"\f1\fB;\f1
+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 \fI#!/bin/...\f1 as appropriate.
+.TP
+\fBrun_this_after_play_ends=\f1\fI"/path/to/application and args"\f1\fB;\f1
+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 \fI#!/bin/...\f1 as appropriate.
+.TP
+\fBrun_this_before_entering_active_state=\f1\fI"/path/to/application and args"\f1\fB;\f1
+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 \fBactive_state_timeout\f1 setting elapses. If a new play session starts before that, the system will remain active. Otherwise, the system will go inactive.
-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.0 to -30.0 and -144.0 means "mute".
+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 \fI#!/bin/...\f1 as appropriate.
+.TP
+\fBrun_this_after_exiting_active_state=\f1\fI"/path/to/application and args"\f1\fB;\f1
+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 \fI#!/bin/...\f1 as appropriate.
+.TP
+\fBactive_state_timeout=\f1\fIseconds\f1\fB;\f1
+After a play session has ended, the system will remain active for \fIseconds\f1 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.
+.TP
+\fBrun_this_if_an_unfixable_error_is_detected=\f1\fI"/path/to/application and args"\f1\fB;\f1
+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 \fI#!/bin/...\f1 as appropriate.
+.TP
+\fBwait_for_completion=\f1\fI"choice"\f1\fB;\f1
+Set \fIchoice\f1 to "yes" to make shairport-sync wait until the programs specified in the \fBrun_this_...\f1 settings have completed execution before continuing. The default is "no".
+.TP
+\fBallow_session_interruption=\f1\fI"choice"\f1\fB;\f1
+If \fBchoice\f1 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.
+.TP
+\fBsession_timeout=\f1\fIseconds\f1\fB;\f1
+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.
.TP
\fB"ALSA" SETTINGS\f1
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 \fBalsamixer\f1 or \fBaplay\f1 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 \fBalsa\f1 group settings:
@@ -190,11 +240,21 @@ Use this optional advanced setting to set the alsa buffer size near to this valu
Use this optional advanced setting to control whether MMAP-based output is used to communicate with the DAC. Default is \fI"yes"\f1.
.TP
\fBmute_using_playback_switch=\f1\fI"no"\f1\fB;\f1
-This is an advanced setting and the default is \fI"no"\f1. If it is set to \fI"yes"\f1, hardware mute will be implemented using a feature called a 'playback switch', where one is available. Set it to \fI"no"\f1 to prevent the playback switch being used.
+This is an advanced setting and the default is \fI"no"\f1. If it is set to \fI"yes"\f1, hardware mute will be used where it is available. Set it to \fI"no"\f1 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 \fI"no"\f1 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.
+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.
+.TP
+\fBmaximum_stall_time=\f1\fIseconds\f1\fB;\f1
+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 \fBrun_this_if_an_unfixable_error_is_detected\f1 program, if any, being called.
+
+Implemented for the ALSA back end only.
+.TP
+\fBdisable_standby_mode=\f1\fI"never"\f1\fB;\f1
+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.
.TP
\fB"SNDIO" SETTINGS\f1
These settings are for the SNDIO back end, used to communicate with audio output devices in the SNDIO system.
@@ -263,23 +323,13 @@ If \fBsocket_address\f1 is set, use \fIport\f1 to specify the port to send UDP p
\fBsocket_msglength=\f1\fI65000\f1\fB;\f1
The maximum packet size for any UDP metadata. This must be between 500 or 65000. The default is 500.
.TP
-\fB"SESSIONCONTROL" SETTINGS\f1
-shairport-sync can run programs just before it starts to play an audio stream and just after it finishes. You specify them using the sessioncontrol group settings run_this_before_play_begins and run_this_after_play_ends.
+\fB"DIAGNOSTICS" SETTINGS\f1
.TP
-\fBrun_this_before_play_begins=\f1\fI"/path/to/application and args"\f1\fB;\f1
-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 \fI#!/bin/...\f1 as appropriate.
-.TP
-\fBrun_this_after_play_ends=\f1\fI"/path/to/application and args"\f1\fB;\f1
-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 \fI#!/bin/...\f1 as appropriate.
-.TP
-\fBwait_for_completion=\f1\fI"choice"\f1\fB;\f1
-Set \fIchoice\f1 to "yes" to make shairport-sync wait until the programs specified in the \fBrun_this_before_play_begins\f1, \fBrun_this_after_play_ends\f1 and \fBrun_this_when_volume_is_set\f1 have completed execution before continuing. The default is "no".
-.TP
-\fBallow_session_interruption=\f1\fI"choice"\f1\fB;\f1
-If \fBchoice\f1 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.
+\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.
.TP
-\fBsession_timeout=\f1\fIseconds\f1\fB;\f1
-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 \fIseconds\f1 seconds before ending the session. Once the session has terminated, other devices can use it. The default is 120 seconds.
+\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.
.SH OPTIONS
This section is about the command-line options available in shairport-sync.
@@ -306,13 +356,8 @@ If you want shairport-sync to wait until the command has completed before starti
\fB-c \f1\fIfilename\f1\fB | --configfile=\f1\fIfilename\f1
Read configuration settings from \fIfilename\f1. The default is to read them from the \fIshairport-sync.conf\f1 in the System Configuration Directory -- \fI/etc\f1 in Linux, \fI/usr/local/etc\f1 in BSD unixes. For information about configuration settings, see the "Configuration File Settings" section above.
.TP
-\fB-D | --disconnectFromOutput\f1
-Disconnect the shairport-sync daemon from the output device and exit. (Requires that the daemon has written its PID to an agreed file -- see the \fB-d\f1 option).
-
-Please note that this feature is deprecated and will be removed in a future version of shairport-sync.
-.TP
\fB-d | --daemon\f1
-Instruct shairport-sync to demonise itself. It will write its Process ID (PID) to a file, usually at \fI/var/run/shairport-sync/shairport-sync.pid\f1, which is used by the \fB-k\f1, \fB-D\f1 and \fB-R\f1 options to locate the daemon at a later time. See also the \fB-j\f1 option.
+Instruct shairport-sync to demonise itself. It will write its Process ID (PID) to a file, usually at \fI/var/run/shairport-sync/shairport-sync.pid\f1, which is used by the \fB-k\f1, \fB-D\f1 and \fB-R\f1 options to locate the daemon at a later time. See also the \fB-j\f1 option. Only available if shaiport-sync has been compiled with libdaemon support.
.TP
\fB-E \f1\fIprogram\f1\fB | --on-stop=\f1\fIprogram\f1
Execute \fIprogram\f1 when playback has ended. Specify the full path to the program, e.g. \fI/usr/bin/logger\f1. Executable scripts can be used, but they must have the appropriate shebang (\fI#!/bin/sh\f1 in the headline.
@@ -328,10 +373,10 @@ Please note that cover art data may be very large, and may place too great a bur
Print brief help message and exit.
.TP
\fB-j\f1
-Instruct shairport-sync to demonise itself. Unlike the \fB-d\f1 option, it will not write a Process ID (PID) to a file -- it will just (hence the "j") demonise itself.
+Instruct shairport-sync to demonise itself. Unlike the \fB-d\f1 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.
.TP
\fB-k | --kill\f1
-Kill the shairport-sync daemon and exit. (Requires that the daemon has written its PID to an agreed file -- see the \fB-d\f1 option).
+Kill the shairport-sync daemon and exit. (Requires that the daemon has written its PID to an agreed file -- see the \fB-d\f1 option. Only available if shaiport-sync has been compiled with libdaemon support.)
.TP
\fB--logOutputLevel\f1
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.
@@ -356,11 +401,6 @@ Listen for play requests on \fIport\f1. The default is to use port 5000.
\fB--password=\f1\fIsecret\f1
Require the password \fIsecret\f1 to be able to connect and stream to the service.
.TP
-\fB-R | --reconnectToOutput\f1
-Reconnect the shairport-sync daemon to the output device and exit. It may take a few seconds to synchronise. (Requires that the daemon has written its PID to an agreed file -- see the \fB-d\f1 option).
-
-Please note that this feature is deprecated and will be removed in a future version of shairport-sync.
-.TP
\fB-r \f1\fIthreshold\f1\fB | --resync=\f1\fIthreshold\f1
Resynchronise if timings differ by more than \fIthreshold\f1 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 \fB0\f1 to disable resynchronisation. This setting is deprecated and will be removed in a future version of shairport-sync.
.TP
@@ -378,6 +418,9 @@ When shairport-sync plays an audio stream, it starts a play session and will ret
\fB--tolerance=\f1\fIframes\f1
Allow playback to be up to \fIframes\f1 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 \fB--statistics\f1 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.
.TP
+\fB-u\f1
+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.
+.TP
\fB-V | --version\f1
Print version information and exit.
.TP
diff --git a/man/shairport-sync.7.xml b/man/shairport-sync.7.xml
index 6be15b1..199e97f 100644
--- a/man/shairport-sync.7.xml
+++ b/man/shairport-sync.7.xml
@@ -4,7 +4,7 @@
<!--
This man page source file is part of shairport-sync.
- Copyright (c) Mike Brady 2014-2018
+ Copyright (c) Mike Brady 2014-2019
All rights reserved.
@@ -35,7 +35,7 @@
<!--
<cmd>shairport-sync <opt>-D</opt> <arg>interface</arg></cmd>
-->
- <cmd>shairport-sync <opt>[-djvw]</opt>
+ <cmd>shairport-sync <opt>[-djvuw]</opt>
<opt>[-a </opt><arg>name</arg><opt>]</opt>
<opt>[-A </opt><arg>latency</arg><opt>]</opt>
<opt>[-B </opt><arg>command</arg><opt>]</opt>
@@ -55,41 +55,64 @@
<opt>[--tolerance=</opt><arg>frames</arg><opt>]</opt>
<opt>[-- </opt><arg>audio_backend_options</arg><opt>]</opt>
</cmd>
- <cmd>shairport-sync <opt>-D</opt></cmd>
<cmd>shairport-sync <opt>-k</opt></cmd>
<cmd>shairport-sync <opt>-h</opt></cmd>
- <cmd>shairport-sync <opt>-R</opt></cmd>
<cmd>shairport-sync <opt>-V</opt></cmd>
</synopsis>
<description>
- <p>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) or to a PulseAudio output stream (available on Linux).</p>
- <p>A feature of shairport-sync is that it 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.
+ <p>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.</p>
+
+ <p>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 step with one another.
- This allows multiple devices to play the same source without getting out of phase with one another,
- enabling, for example, simultaneous multi-room operation.
+ 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.
</p>
- <p>shairport-sync can be compiled to stream audio synchronised audio output 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.</p>
- <p>shairport-sync can be compiled to stream metadata to a pipe or socket.</p>
+ <p>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.</p>
+ <p>Shairport Sync can be compiled to stream metadata, including cover art, to a pipe
+ or socket.</p>
+ <p>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.</p>
+
+ <p>Settings can be made using the configuration file (recommended for all new
+ installations) or by using command-line options.</p>
+
+ <p>The name of the Shairport Sync executable is <opt>shairport-sync</opt>.
+ Both names are used in these man pages.</p>
- <p>Settings can be made using the configuration file (recommended for all new installations) or by using command-line options.</p>
-
</description>
+
<section name="Configuration File Settings">
- <p>You should use the configuration file for setting up shairport-sync.
- This file is usually <file>shairport-sync.conf</file> and is generally located in the System Configuration Directory, which is normally the <file>/etc</file> directory in Linux or the <file>/usr/local/etc</file> directory in BSD unixes.
+ <p>You should use the configuration file for setting up Shairport Sync.
+ This file is usually <file>shairport-sync.conf</file> and is generally located in the
+ System Configuration Directory, which is normally the <file>/etc</file> directory in
+ Linux or the <file>/usr/local/etc</file> directory in BSD unixes.
You may need to have root privileges to modify it.</p>
- <p>(Note: Shairport Sync may have been compiled to use a different configuration directory. You can determine which by performing the command <file>$ shairport-sync -V</file>. One of the items in the output string is the value of the <opt>sysconfdir</opt>,
+ <p>(Note: Shairport Sync may have been compiled to use a different configuration
+ directory. You can determine which by performing the command <file>$ shairport-sync
+ -V</file>. One of the items in the output string is the value of the
+ <opt>sysconfdir</opt>,
i.e. the System Configuration Directory.)</p>
- <p>Within the configuraton file, settings are organised into <i>groups</i>, for example, there is a "general" group of
+ <p>Within the configuraton file, settings are organised into <i>groups</i>, 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:</p>
@@ -105,10 +128,18 @@
<p><p><opt>mixer_control_name = "PCM";</opt></p></p>
<p><opt>};</opt></p>
- <p>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.</p>
+ <p>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.</p>
- <p>A sample configuration file with all possible settings, but with all of them commented out, is installed at <file>shairport-sync.conf.sample</file>, within the System Configuration Directory -- <file>/etc</file> in Linux, <file>/usr/local/etc</file> in BSD unixes.</p>
+ <p>A sample configuration file with all possible settings, but with all of them
+ commented out, is installed at <file>shairport-sync.conf.sample</file>, within the
+ System Configuration Directory -- <file>/etc</file> in Linux,
+ <file>/usr/local/etc</file> in BSD unixes.</p>
<p>To retain backwards compatibility with previous versions of shairport-sync
you can use still use command line options, but any new features, etc. will
@@ -126,20 +157,26 @@
<p>Use this <arg>service_name</arg> to identify this player in iTunes, etc.</p>
<p>The following substitutions are allowed:
<opt>%h</opt> for the computer's hostname,
- <opt>%H</opt> for the computer's hostname with the first letter capitalised (ASCII only),
+ <opt>%H</opt> for the computer's hostname with the first letter capitalised (ASCII
+ only),
<opt>%v</opt> for the shairport-sync version number, e.g. "3.0.1" and
- <opt>%V</opt> for the shairport-sync version string, e.g. "3.0.1-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc".</p>
- <p>The default is "%H", which is replaced by the hostname with the first letter capitalised.</p>
+ <opt>%V</opt> for the shairport-sync version string, e.g.
+ "3.0.1-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc".</p>
+ <p>The default is "%H", which is replaced by the hostname with the first letter
+ capitalised.</p>
</optdesc>
</option>
<option>
<p><opt>password=</opt><arg>"password"</arg><opt>;</opt></p>
- <optdesc><p>Require the password <arg>password</arg> to connect to the service. If you leave this setting commented out, no password is needed.</p></optdesc>
+ <optdesc><p>Require the password <arg>password</arg> to connect to the service. If you
+ leave this setting commented out, no password is needed.</p></optdesc>
</option>
+
<option>
<p><opt>interpolation=</opt><arg>"mode"</arg><opt>;</opt></p>
- <optdesc><p>Interpolate, or "stuff", the audio stream using the <arg>mode</arg>. Interpolation here refers to the
+ <optdesc><p>Interpolate, or "stuff", the audio stream using the <arg>mode</arg>.
+ 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.
@@ -152,39 +189,60 @@
</option>
<option>
- <p><opt>statistics=</opt><arg>"setting"</arg><opt>;</opt></p>
- <optdesc><p>Use this <arg>setting</arg> 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.</p></optdesc>
+ <p><opt>output_backend=</opt><arg>"backend"</arg><opt>;</opt></p>
+ <optdesc><p>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 <arg>backend</arg>. Perform the
+ command <opt>shairport-sync -h</opt> 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.</p></optdesc>
</option>
<option>
<p><opt>mdns_backend=</opt><arg>"backend"</arg><opt>;</opt></p>
- <optdesc><p>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 <arg>backend</arg>. The default is "avahi". Perform the command <opt>shairport-sync -h</opt> to get a list of available mDNS modules.</p></optdesc>
- </option>
- <option>
- <p><opt>output_backend=</opt><arg>"backend"</arg><opt>;</opt></p>
- <optdesc><p>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 <arg>backend</arg>. Perform the command <opt>shairport-sync -h</opt> 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.</p></optdesc>
+ <optdesc><p>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 <arg>backend</arg>. The default is "avahi". Perform the command
+ <opt>shairport-sync -h</opt> to get a list of available mDNS modules.</p></optdesc>
</option>
+
<option>
<p><opt>port=</opt><arg>portnumber</arg><opt>;</opt></p>
- <optdesc><p>Use this to specify the <arg>portnumber</arg> shairport-sync uses to listen for service requests from iTunes, etc. The default is port 5000.</p></optdesc>
+ <optdesc><p>Use this to specify the <arg>portnumber</arg> shairport-sync uses to
+ listen for service requests from iTunes, etc. The default is port 5000.</p></optdesc>
</option>
+
<option>
<p><opt>udp_port_base=</opt><arg>portnumber</arg><opt>;</opt></p>
- <optdesc><p>When shairport-sync starts to play audio, it establises three UDP connections to the audio source. Use this setting to specify the starting <arg>portnumber</arg> for these three ports. It will pick the first three unused ports starting from <arg>portnumber</arg>. The default is port 6001.</p></optdesc>
+ <optdesc><p>When shairport-sync starts to play audio, it establises three UDP
+ connections to the audio source. Use this setting to specify the starting
+ <arg>portnumber</arg> for these three ports. It will pick the first three unused ports
+ starting from <arg>portnumber</arg>. The default is port 6001.</p></optdesc>
</option>
+
<option>
<p><opt>udp_port_range=</opt><arg>range</arg><opt>;</opt></p>
- <optdesc><p>Use this in conjunction with the prevous setting to specify the <arg>range</arg> 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.</p></optdesc>
+ <optdesc><p>Use this in conjunction with the prevous setting to specify the
+ <arg>range</arg> 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.</p></optdesc>
</option>
+
<option>
<p><opt>drift_tolerance_in_seconds=</opt><arg>seconds</arg><opt>;</opt></p>
- <optdesc><p>Allow playback to drift up to <arg>seconds</arg> 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 <opt>statistics</opt> setting to
- monitor correction levels. Corrections should not greatly exceed net corrections. This setting replaces the deprecated <opt>drift</opt> setting.
+ <optdesc><p>Allow playback to drift up to <arg>seconds</arg> 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 <opt>statistics</opt> setting
+ to monitor correction levels. Corrections should not greatly exceed net corrections.
+ This setting replaces the deprecated <opt>drift</opt> setting.
</p></optdesc>
</option>
+
<option>
<p><opt>resync_threshold_in_seconds=</opt><arg>threshold</arg><opt>;</opt></p>
<optdesc><p>Resynchronise if timings differ by more than <arg>threshold</arg> seconds.
@@ -195,201 +253,482 @@
This setting replaces the deprecated <opt>resync_threshold</opt> setting.
</p></optdesc>
</option>
- <option>
- <p><opt>log_verbosity=</opt><arg>0</arg><opt>;</opt></p>
- <optdesc><p>Use this to specify how much debugging information should be output or logged. The value <arg>0</arg> means no debug information, <arg>3</arg> means most debug information. The default is <arg>0</arg>.</p></optdesc>
- </option>
+
<option>
<p><opt>ignore_volume_control=</opt><arg>"choice"</arg><opt>;</opt></p>
- <optdesc><p>Set this <arg>choice</arg> to <arg>"yes"</arg> 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 <arg>"no"</arg>.</p></optdesc>
- </option>
-
- <option>
- <p><opt>volume_max_db=</opt><arg>dBvalue</arg><opt>;</opt></p>
- <optdesc><p>Specify the maximum output level to be used with the hardware mixer, if used. If no hardware mixed is used, this setting speciies the maximum setting permissible in the software mixer, which has an attenuation of from 0.0 dB down to -96.3 dB.
- </p></optdesc>
+ <optdesc><p>Set this <arg>choice</arg> to <arg>"yes"</arg> 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 <arg>"no"</arg>.</p></optdesc>
</option>
<option>
<p><opt>volume_range_db=</opt><arg>dBvalue</arg><opt>;</opt></p>
- <optdesc><p>Use this <arg>dBvalue</arg> to reduce or increase the attenuation range, in decibels, between the minimum and maximum volume.</p>
-
- <p>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 --
+ <optdesc><p>Use this <arg>dBvalue</arg> to reduce or increase the attenuation range,
+ in decibels, between the minimum and maximum volume.</p>
+ <p>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.</p>
- <p>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.
+ <p>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.</p>
+ 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.</p>
<p>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.</p>
+ 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.</p>
<p>If you omit this setting, the native range of the mixer is used.</p></optdesc>
</option>
+
+ <option>
+ <p><opt>volume_max_db=</opt><arg>dBvalue</arg><opt>;</opt></p>
+ <optdesc><p>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.
+ </p></optdesc>
+ </option>
+
+ <option>
+ <p><opt>volume_control_profile=</opt><arg>"choice"</arg><opt>;</opt></p>
+ <optdesc><p>Use this advanced setting to specify how the airplay volume is transferred
+ to the mixer volume. The <arg>"standard"</arg> profile, which is the default, makes
+ the volume change more quickly at lower volumes and slower at higher volumes. Choose
+ the <arg>"flat"</arg> profile to makes the volume change at the same rate at all
+ volume levels.
+ </p></optdesc>
+ </option>
+
+ <option>
+ <p><opt>volume_range_combined_hardware_priority=</opt>
+ <arg>"choice"</arg><opt>;</opt></p>
+ <optdesc><p>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 <arg>"yes"</arg> 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.</p>
+ <p>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.</p>
+ <p>Unfortunately, certain hardware attenuators will mute at their greatest
+ attenuation, so can't be combined with software attenuation in this way. Choosing
+ <arg>"no"</arg> means that software attenuation is used to bring the remaining
+ attenuation required into the range offered by the hardware attenuator.
+ This is the default.
+ </p></optdesc>
+ </option>
+
+ <option>
+ <p><opt>run_this_when_volume_is_set=</opt>
+ <arg>"/full/path/to/application/and/args"</arg><opt>;</opt></p>
+ <optdesc><p>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 <file>#!/bin/...</file> as appropriate.</p>
+ <p>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".</p></optdesc>
+ </option>
+
<option>
<p><opt>regtype=</opt><arg>"regTypeString"</arg><opt>;</opt></p>
- <optdesc><p>Use this advanced setting to set the service type and transport to be advertised by Zeroconf/Bonjour. Default is <arg>"_raop._tcp"</arg>.</p></optdesc>
+ <optdesc><p>Use this advanced setting to set the service type and transport to be
+ advertised by Zeroconf/Bonjour. Default is <arg>"_raop._tcp"</arg>.</p></optdesc>
</option>
<option>
<p><opt>playback_mode=</opt><arg>"mode"</arg><opt>;</opt></p>
- <optdesc><p>The <arg>mode</arg> can be "stereo", "mono", "reverse stereo", "both left" or "both right". Default is "stereo".</p></optdesc>
+ <optdesc><p>The <arg>mode</arg> 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.</p></optdesc>
</option>
<option>
+ <p><opt>alac_decoder=</opt><arg>"decodername"</arg><opt>;</opt></p>
+ <optdesc><p>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.</p></optdesc>
+ </option>
+
+ <option>
<p><opt>interface=</opt><arg>"name"</arg><opt>;</opt></p>
- <optdesc><p>Use this advanced setting if you want to confine Shairport Sync to the named interface. Leave it commented out to get the default bahaviour.</p></optdesc>
+ <optdesc><p>Use this advanced setting if you want to confine Shairport Sync to the
+ named interface. Leave it commented out to get the default bahaviour.</p></optdesc>
</option>
<option>
- <p><opt>alac_decoder=</opt><arg>"decodername"</arg><opt>;</opt></p>
- <optdesc><p>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.</p></optdesc>
+ <p><opt>audio_backend_latency_offset_in_seconds=</opt>
+ <arg>offset_in_seconds</arg><opt>;</opt></p>
+ <optdesc><p>Set this <arg>offset_in_seconds</arg> 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.</p></optdesc>
</option>
+
<option>
- <p><opt>audio_backend_latency_offset_in_seconds=</opt><arg>offset_in_seconds</arg><opt>;</opt></p>
- <optdesc><p>Set this <arg>offset_in_seconds</arg> 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.</p></optdesc>
+ <p><opt>audio_backend_buffer_desired_length_in_seconds=</opt>
+ <arg>length_in_seconds</arg><opt>;</opt></p>
+ <optdesc><p>Use this <arg>length_in_seconds</arg> to set the desired length of the
+ queue of audio frames in the backend's output buffer.</p>
+ <p>The default is 0.15 seconds for the ALSA backend, 0.35 seconds for the PA backend
+ and one second for all other backends.</p>
+ <p>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.</p></optdesc>
</option>
+
<option>
- <p><opt>audio_backend_buffer_desired_length_in_seconds=</opt><arg>length_in_seconds</arg><opt>;</opt></p>
- <optdesc><p>Use this <arg>length_in_seconds</arg> to set the desired length of the queue of audio frames in the backend's output buffer.</p>
- <p>The default is 0.15 seconds for the ALSA backend, 0.35 seconds for the PA backend and one second for all other backends.</p>
- <p>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.</p></optdesc>
+ <p><opt>audio_backend_buffer_interpolation_threshold_in_seconds=</opt>
+ <arg>time_in_seconds</arg><opt>;</opt></p>
+ <optdesc><p>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.</p></optdesc>
</option>
+
<option>
- <p><opt>audio_backend_silent_lead_in_time=</opt><arg>lead_in_time_in_seconds</arg><opt>;</opt></p>
- <optdesc><p>This is an advanced setting. Use the <arg>lead_in_time_in_seconds</arg> to set the desired length of the period of silence
- (a "silent lead-in") played before a play session begins.</p>
- <p>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.</p>
- <p>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.</p></optdesc>
+ <p><opt>audio_backend_silent_lead_in_time=</opt>
+ <arg>lead_in_time_in_seconds</arg><opt>;</opt></p>
+ <optdesc><p>This is an advanced setting. Use the <arg>lead_in_time_in_seconds</arg> to
+ set the desired length of the period of silence (a "silent lead-in") played before a
+ play session begins.</p>
+ <p>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.</p>
+ <p>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.</p></optdesc>
</option>
+
<option>
- <p><opt>run_this_when_volume_is_set=</opt><arg>"/full/path/to/application/and/args"</arg><opt>;</opt></p>
- <optdesc><p>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 <file>#!/bin/...</file> as appropriate.</p>
- <p>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.0 to -30.0 and -144.0 means "mute".</p></optdesc>
+ <p><opt>dbus_service_bus=</opt>
+ <arg>"bus_name"</arg><opt>;</opt></p>
+ <optdesc><p>If shairport sync is compiled with the D-Bus interface, it can offer it on
+ the <arg>"system"</arg> or the <arg>"session"</arg> D-Bus "bus".
+ Use this to specify which. The default is to use the "system" bus.</p></optdesc>
+ </option>
+
+ <option>
+ <p><opt>mpris_service_bus=</opt>
+ <arg>"bus_name"</arg><opt>;</opt></p>
+ <optdesc><p>If shairport sync is compiled with the MPRIS interface, it can offer the
+ service on the <arg>"system"</arg> or the <arg>"session"</arg> D-Bus "bus".
+ Use this to specify which. The default is to use the "system" bus.</p></optdesc>
+ </option>
+
+ <option><p><opt>"SESSIONCONTROL" SETTINGS</opt></p></option>
+
+ <option>
+ <p><opt>run_this_before_play_begins=</opt><arg>"/path/to/application and
+ args"</arg><opt>;</opt></p>
+ <optdesc><p>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 <file>#!/bin/...</file> as
+ appropriate.</p></optdesc>
</option>
+ <option>
+ <p><opt>run_this_after_play_ends=</opt><arg>"/path/to/application and
+ args"</arg><opt>;</opt></p>
+ <optdesc><p>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 <file>#!/bin/...</file> as
+ appropriate.</p></optdesc>
+ </option>
+
+ <option>
+ <p><opt>run_this_before_entering_active_state=</opt><arg>"/path/to/application and
+ args"</arg><opt>;</opt></p>
+ <optdesc><p>Here you can specify a program and its arguments that will be run just
+ before shairport-sync goes active.</p>
+
+ <p>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 <opt>active_state_timeout</opt> setting elapses.
+ If a new play session starts before that, the system will remain active. Otherwise,
+ the system will go inactive.
+ </p>
+
+ <p>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 <file>#!/bin/...</file> as
+ appropriate.</p></optdesc>
+ </option>
+
+ <option>
+ <p><opt>run_this_after_exiting_active_state=</opt><arg>"/path/to/application and
+ args"</arg><opt>;</opt></p>
+ <optdesc><p>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 <file>#!/bin/...</file> as
+ appropriate.</p></optdesc>
+ </option>
+
+ <option>
+ <p><opt>active_state_timeout=</opt><arg>seconds</arg><opt>;</opt></p>
+ <optdesc><p>After a play session has ended, the system will remain active for
+ <arg>seconds</arg> 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.</p></optdesc>
+ </option>
+
+ <option>
+ <p><opt>run_this_if_an_unfixable_error_is_detected=</opt><arg>"/path/to/application
+ and args"</arg><opt>;</opt></p>
+ <optdesc><p>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.</p>
+ <p>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.</p>
+ <p>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 <file>#!/bin/...</file> as
+ appropriate.</p></optdesc>
+ </option>
+
+ <option>
+ <p><opt>wait_for_completion=</opt><arg>"choice"</arg><opt>;</opt></p>
+ <optdesc><p>Set <arg>choice</arg> to "yes" to make shairport-sync wait until the
+ programs specified in the <opt>run_this_...</opt> settings have
+ completed execution before continuing. The default is "no".</p></optdesc>
+ </option>
+
+ <option>
+ <p><opt>allow_session_interruption=</opt><arg>"choice"</arg><opt>;</opt></p>
+ <optdesc><p>If <opt>choice</opt> 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.</p></optdesc>
+ </option>
+
+ <option>
+ <p><opt>session_timeout=</opt><arg>seconds</arg><opt>;</opt></p>
+ <optdesc><p>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.</p></optdesc>
+ </option>
+
<option><p><opt>"ALSA" SETTINGS</opt></p></option>
- <p>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 <opt>alsamixer</opt> or <opt>aplay</opt> 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 <opt>alsa</opt> group settings:</p>
+ <p>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
+ <opt>alsamixer</opt> or <opt>aplay</opt> 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 <opt>alsa</opt> group settings:</p>
<option>
<p><opt>output_device=</opt><arg>"output_device"</arg><opt>;</opt></p>
- <optdesc><p>Use the output device called <arg>output_device</arg>. The default is the device called <arg>"default"</arg>.</p></optdesc>
+ <optdesc><p>Use the output device called <arg>output_device</arg>. The default is the
+ device called <arg>"default"</arg>.</p></optdesc>
</option>
+
<option>
<p><opt>mixer_control_name=</opt><arg>"name"</arg><opt>;</opt></p>
- <optdesc><p>Specify the <arg>name</arg> of the mixer control to be used by shairport-sync to control the volume.
+ <optdesc><p>Specify the <arg>name</arg> 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.</p></optdesc>
+ If you do not specify a mixer control name, shairport-sync will adjust the volume in
+ software.</p></optdesc>
</option>
+
<option>
<p><opt>mixer_device=</opt><arg>"mixer_device"</arg><opt>;</opt></p>
- <optdesc><p>By default, the mixer is assumed to be output_device. Use this setting to specify a device other than the output device.</p></optdesc>
+ <optdesc><p>By default, the mixer is assumed to be output_device. Use this setting to
+ specify a device other than the output device.</p></optdesc>
</option>
<option>
<p><opt>output_rate=</opt><arg>frame rate</arg><opt>;</opt></p>
- <optdesc><p>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.
+ <optdesc><p>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.
</p></optdesc>
</option>
<option>
<p><opt>output_format=</opt><arg>"format"</arg><opt>;</opt></p>
- <optdesc><p>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.</p><p>"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".</p><p>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.
+ <optdesc><p>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.</p><p>"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".</p><p>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.
</p></optdesc>
</option>
<option>
<p><opt>disable_synchronization=</opt><arg>"no"</arg><opt>;</opt></p>
- <optdesc><p>This is an advanced setting and is for debugging only. Set to <arg>"yes"</arg> to disable synchronization. Default is <arg>"no"</arg>.
- If you use it to disable synchronisation, then sooner or later you'll experience audio glitches due to
- audio buffer overflow or underflow.
+ <optdesc><p>This is an advanced setting and is for debugging only. Set to
+ <arg>"yes"</arg> to disable synchronization. Default is <arg>"no"</arg>.
+ If you use it to disable synchronisation, then sooner or later you'll experience audio
+ glitches due to audio buffer overflow or underflow.
</p></optdesc>
</option>
+
<option>
<p><opt>period_size=</opt><arg>number</arg><opt>;</opt></p>
- <optdesc><p>Use this optional advanced setting to set the alsa period size near to this value.</p></optdesc>
+ <optdesc><p>Use this optional advanced setting to set the alsa period size near to
+ this value.</p></optdesc>
</option>
+
<option>
<p><opt>buffer_size=</opt><arg>number</arg><opt>;</opt></p>
- <optdesc><p>Use this optional advanced setting to set the alsa buffer size near to this value.</p></optdesc>
+ <optdesc><p>Use this optional advanced setting to set the alsa buffer size near to
+ this value.</p></optdesc>
</option>
+
<option>
<p><opt>use_mmap_if_available=</opt><arg>"yes"</arg><opt>;</opt></p>
- <optdesc><p> Use this optional advanced setting to control whether MMAP-based output is used to communicate with the DAC. Default is <arg>"yes"</arg>.</p></optdesc>
+ <optdesc><p> Use this optional advanced setting to control whether MMAP-based output
+ is used to communicate with the DAC. Default is <arg>"yes"</arg>.</p></optdesc>
</option>
<option>
<p><opt>mute_using_playback_switch=</opt><arg>"no"</arg><opt>;</opt></p>
<optdesc>
- <p>This is an advanced setting and the default is <arg>"no"</arg>. If it is set to <arg>"yes"</arg>, hardware mute will be implemented using a feature called a 'playback switch', where one is available. Set it to <arg>"no"</arg> to prevent the playback switch being used.</p>
- <p>If Shairport Sync is sharing the output device with other applications, it is best to leave this set to <arg>"no"</arg> for compatibility with those applications.</p>
- <p>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.</p>
+ <p>This is an advanced setting and the default is <arg>"no"</arg>. If it is set to
+ <arg>"yes"</arg>, hardware mute will be used where it is available.
+ Set it to <arg>"no"</arg> to prevent the hardware mute being used.</p>
+ <p>If Shairport Sync is sharing the output device with other applications, it is best
+ to leave this set to <arg>"no"</arg> for compatibility with those applications.</p>
+ <p>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.</p>
+ </optdesc>
+ </option>
+
+ <option>
+ <p><opt>maximum_stall_time=</opt><arg>seconds</arg><opt>;</opt></p>
+ <optdesc><p>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
+ <opt>run_this_if_an_unfixable_error_is_detected</opt> program,
+ if any, being called.</p>
+ <p>Implemented for the ALSA back end only.</p>
</optdesc>
</option>
+
+ <option>
+ <p><opt>disable_standby_mode=</opt><arg>"never"</arg><opt>;</opt></p>
+ <optdesc>
+ <p>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.</p>
+ <p>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.
+ </p>
+ </optdesc>
+ </option>
+
<option><p><opt>"SNDIO" SETTINGS</opt></p></option>
- <p>These settings are for the SNDIO back end, used to communicate with audio output devices in the SNDIO system.</p>
+ <p>These settings are for the SNDIO back end, used to communicate with audio output
+ devices in the SNDIO system.</p>
+
<option>
<p><opt>device=</opt><arg>"snd/0"</arg><opt>;</opt></p>
- <optdesc><p>Use this optional setting to specify the name of the output device, e.g. <arg>"snd/0"</arg>. The default is to use the SNDIO system's default.</p></optdesc>
+ <optdesc><p>Use this optional setting to specify the name of the output device, e.g.
+ <arg>"snd/0"</arg>. The default is to use the SNDIO system's default.</p></optdesc>
</option>
+
<option>
<p><opt>rate=</opt><arg>44100</arg><opt>;</opt></p>
- <optdesc><p>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.</p></optdesc>
+ <optdesc><p>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.</p></optdesc>
</option>
+
<option>
<p><opt>format=</opt><arg>"S16"</arg><opt>;</opt></p>
- <optdesc><p>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.</p><p>"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".</p><p>
- 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.
+ <optdesc><p>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.</p><p>"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".</p><p>
+ 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.</p>
- <p>Please note that 32- or 24-bit has not been extensively tested on SNDIO.</p></optdesc>
+ consider using 32- or 24-bit output if your device is capable of it, to get the lowest
+ possible levels of dither.</p>
+ <p>Please note that 32- or 24-bit has not been extensively tested on
+ SNDIO.</p></optdesc>
</option>
+
<option>
<p><opt>round=</opt><arg>value</arg><opt>;</opt></p>
- <optdesc><p>Use this optional advanced setting to specify the period size of the SNDIO channel. If omitted, a SNDIO system default value will be used.</p></optdesc>
+ <optdesc><p>Use this optional advanced setting to specify the period size of the SNDIO
+ channel. If omitted, a SNDIO system default value will be used.</p></optdesc>
</option>
+
<option>
<p><opt>bufsiz=</opt><arg>value</arg><opt>;</opt></p>
- <optdesc><p>Use this optional advanced setting to specify the buffer size of the SNDIO channel. If omitted, a SNDIO system default value will be used.</p></optdesc>
+ <optdesc><p>Use this optional advanced setting to specify the buffer size of the SNDIO
+ channel. If omitted, a SNDIO system default value will be used.</p></optdesc>
</option>
-
<option><p><opt>"PA" SETTINGS</opt></p></option>
<p>These settings are for the new PulseAudio backend.</p>
+
<option>
<p><opt>application_name=</opt><arg>"Shairport Sync"</arg><opt>;</opt></p>
- <optdesc><p>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".</p></optdesc>
+ <optdesc><p>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".</p></optdesc>
</option>
<option><p><opt>"PIPE" SETTINGS</opt></p></option>
- <p>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.</p>
+ <p>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.</p>
+
<option>
<p><opt>name=</opt><arg>"/path/to/pipe"</arg><opt>;</opt></p>
- <optdesc><p>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.</p></optdesc>
+ <optdesc><p>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.</p></optdesc>
</option>
<option><p><opt>"STDOUT" SETTINGS</opt></p></option>
@@ -398,86 +737,87 @@
<option><p><opt>"AO" SETTINGS</opt></p></option>
<p>There are no configuration file settings for the AO backend.</p>
-
<option><p><opt>"METADATA" SETTINGS</opt></p></option>
- <p>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 <file>/tmp/shairport-sync-metadata</file>.
- To process metadata, shairport-sync must have been compiled with metadata support included.
- You can check that this is so by running the command <opt>$ shairport-sync -V</opt>; the identification string will contain the word <opt>metadata</opt>.</p>
- <p>Please note that different sources provide different levels of metadata. Some provide a lot; some provide almost none.</p>
- <p>The <opt>metadata</opt> group of settings allow you to enable metadata handling and to control certain aspects of it:</p>
-
+ <p>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
+ <file>/tmp/shairport-sync-metadata</file>.
+ To process metadata, shairport-sync must have been compiled with metadata support
+ included.
+ You can check that this is so by running the command <opt>$ shairport-sync -V</opt>;
+ the identification string will contain the word <opt>metadata</opt>.</p>
+ <p>Please note that different sources provide different levels of metadata. Some
+ provide a lot; some provide almost none.</p>
+ <p>The <opt>metadata</opt> group of settings allow you to enable metadata handling and
+ to control certain aspects of it:</p>
<option>
<p><opt>enabled=</opt><arg>"choice"</arg><opt>;</opt></p>
- <optdesc><p>Set the <arg>choice</arg> 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".</p></optdesc>
+ <optdesc><p>Set the <arg>choice</arg> 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".</p></optdesc>
</option>
+
<option>
<p><opt>include_cover_art=</opt><arg>"choice"</arg><opt>;</opt></p>
- <optdesc><p>Set the <arg>choice</arg> 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.
+ <optdesc><p>Set the <arg>choice</arg> 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.
+ 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".</p></optdesc>
</option>
+
<option>
<p><opt>pipe_name=</opt><arg>"filepathname"</arg><opt>;</opt></p>
- <optdesc><p>Specify the absolute path name of the pipe through which metadata should be sent The default is <file>/tmp/shairport-sync-metadata</file>.</p></optdesc>
+ <optdesc><p>Specify the absolute path name of the pipe through which metadata should
+ be sent The default is <file>/tmp/shairport-sync-metadata</file>.</p></optdesc>
</option>
<option>
<p><opt>socket_address=</opt><arg>"hostnameOrIP"</arg><opt>;</opt></p>
- <optdesc><p>If <arg>hostnameOrIP</arg> 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, <arg>socket-port</arg> must be non-zero and <arg>enabled</arg> must be set to "yes".</p></optdesc>
+ <optdesc><p>If <arg>hostnameOrIP</arg> 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, <arg>socket-port</arg> must be non-zero and
+ <arg>enabled</arg> must be set to "yes".</p></optdesc>
</option>
+
<option>
<p><opt>socket_port=</opt><arg>port</arg><opt>;</opt></p>
- <optdesc><p>If <opt>socket_address</opt> is set, use <arg>port</arg> to specify the port to send UDP packets to. Must not be zero.</p></optdesc>
+ <optdesc><p>If <opt>socket_address</opt> is set, use <arg>port</arg> to specify the
+ port to send UDP packets to. Must not be zero.</p></optdesc>
</option>
+
<option>
<p><opt>socket_msglength=</opt><arg>65000</arg><opt>;</opt></p>
- <optdesc><p>The maximum packet size for any UDP metadata. This must be between 500 or 65000. The default is 500.</p></optdesc>
+ <optdesc><p>The maximum packet size for any UDP metadata. This must be between 500 or
+ 65000. The default is 500.</p></optdesc>
</option>
- <option><p><opt>"SESSIONCONTROL" SETTINGS</opt></p></option>
- <p>shairport-sync can run programs just before it starts to play an audio stream and just after it finishes.
- You specify them using the sessioncontrol group settings run_this_before_play_begins and run_this_after_play_ends.</p>
-
- <option>
- <p><opt>run_this_before_play_begins=</opt><arg>"/path/to/application and args"</arg><opt>;</opt></p>
- <optdesc><p>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 <file>#!/bin/...</file> as appropriate.</p></optdesc>
- </option>
- <option>
- <p><opt>run_this_after_play_ends=</opt><arg>"/path/to/application and args"</arg><opt>;</opt></p>
- <optdesc><p>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 <file>#!/bin/...</file> as appropriate.</p></optdesc>
- </option>
+ <option><p><opt>"DIAGNOSTICS" SETTINGS</opt></p></option>
<option>
- <p><opt>wait_for_completion=</opt><arg>"choice"</arg><opt>;</opt></p>
- <optdesc><p>Set <arg>choice</arg> to "yes" to make shairport-sync wait until the programs specified in the <opt>run_this_before_play_begins</opt>,
- <opt>run_this_after_play_ends</opt> and <opt>run_this_when_volume_is_set</opt> have completed execution before continuing. The default is "no".</p></optdesc>
- </option>
- <option>
- <p><opt>allow_session_interruption=</opt><arg>"choice"</arg><opt>;</opt></p>
- <optdesc><p>If <opt>choice</opt> 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.</p></optdesc>
+ <p><opt>statistics=</opt><arg>"setting"</arg><opt>;</opt></p>
+ <optdesc><p>Use this <arg>setting</arg> 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.</p></optdesc>
</option>
+
<option>
- <p><opt>session_timeout=</opt><arg>seconds</arg><opt>;</opt></p>
- <optdesc><p>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 <arg>seconds</arg> seconds before ending the session. Once the session has terminated, other devices can use it.
- The default is 120 seconds.</p></optdesc>
+ <p><opt>log_verbosity=</opt><arg>0</arg><opt>;</opt></p>
+ <optdesc><p>Use this to specify how much debugging information should be output or
+ logged. The value <arg>0</arg> means no debug information, <arg>3</arg> means most
+ debug information. The default is <arg>0</arg>.</p></optdesc>
</option>
</section>
<options>
<p>This section is about the command-line options available in shairport-sync.</p>
-
- <p>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.</p>
+ <p>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.</p>
<p>Many command-line options take sensible default values, so you can normally
ignore most of them. See the EXAMPLES section for typical usages.</p>
@@ -493,16 +833,20 @@
</section>
<option>
- <p><opt>-a </opt><arg>service name</arg><opt> | --name=</opt><arg>service name</arg></p>
+ <p><opt>-a </opt><arg>service name</arg><opt> | --name=</opt><arg>service
+ name</arg></p>
<optdesc><p>
Use this <arg>service name</arg> to identify this player in iTunes, etc.</p>
<p>The following substitutions are allowed:
<opt>%h</opt> for the computer's hostname,
- <opt>%H</opt> for the computer's hostname with the first letter capitalised (ASCII only),
+ <opt>%H</opt> for the computer's hostname with the first letter capitalised (ASCII
+ only),
<opt>%v</opt> for the shairport-sync version number, e.g. "3.0.1" and
- <opt>%V</opt> for the shairport-sync version string, e.g. "3.0.1-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc".</p>
- <p>The default is "%H", which is replaced by the hostname with the first letter capitalised.</p>
+ <opt>%V</opt> for the shairport-sync version string, e.g.
+ "3.0.1-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc".</p>
+ <p>The default is "%H", which is replaced by the hostname with the first letter
+ capitalised.</p>
</optdesc>
</option>
@@ -522,30 +866,23 @@
<option>
<p><opt>-c </opt><arg>filename</arg><opt> | --configfile=</opt><arg>filename</arg></p>
<optdesc><p>
- Read configuration settings from <arg>filename</arg>. The default is to read them from the <file>shairport-sync.conf</file> in the System Configuration Directory -- <file>/etc</file> in Linux, <file>/usr/local/etc</file> in BSD unixes.
- For information about configuration settings, see the "Configuration File Settings" section above.
+ Read configuration settings from <arg>filename</arg>. The default is to read them from
+ the <file>shairport-sync.conf</file> in the System Configuration Directory --
+ <file>/etc</file> in Linux, <file>/usr/local/etc</file> in BSD unixes.
+ For information about configuration settings, see the "Configuration File Settings"
+ section above.
</p></optdesc>
</option>
<option>
- <p><opt>-D | --disconnectFromOutput</opt></p>
- <optdesc><p>
- Disconnect the shairport-sync daemon from the output device and
- exit. (Requires that the daemon has written its PID to an agreed
- file -- see the <opt>-d</opt> option).
- </p>
- <p>Please note that this feature is deprecated and will be removed in a future version of shairport-sync.</p>
- </optdesc>
- </option>
-
- <option>
<p><opt>-d | --daemon</opt></p>
<optdesc><p>
Instruct shairport-sync to demonise itself. It will write its
Process ID (PID) to a file, usually at
<file>/var/run/shairport-sync/shairport-sync.pid</file>, which is used by the
<opt>-k</opt>, <opt>-D</opt> and <opt>-R</opt> options to locate
- the daemon at a later time. See also the <opt>-j</opt> option.
+ the daemon at a later time. See also the <opt>-j</opt> option. Only available if
+ shaiport-sync has been compiled with libdaemon support.
</p></optdesc>
</option>
@@ -582,8 +919,9 @@
<option>
<p><opt>-j</opt></p>
<optdesc><p>
- Instruct shairport-sync to demonise itself. Unlike the <opt>-d</opt> option, it will not write a
- Process ID (PID) to a file -- it will just (hence the "j") demonise itself.
+ Instruct shairport-sync to demonise itself. Unlike the <opt>-d</opt> 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.
</p></optdesc>
</option>
@@ -591,15 +929,17 @@
<p><opt>-k | --kill</opt></p>
<optdesc><p>
Kill the shairport-sync daemon and exit. (Requires that the daemon has
- written its PID to an agreed file -- see the <opt>-d</opt> option).
+ written its PID to an agreed file -- see the <opt>-d</opt> option. Only available if
+ shaiport-sync has been compiled with libdaemon support.)
</p></optdesc>
</option>
<option>
<p><opt>--logOutputLevel</opt></p>
<optdesc><p>
- 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.
+ 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.
</p>
</optdesc>
</option>
@@ -607,10 +947,13 @@
<option>
<p><opt>-L | --latency=</opt><arg>latency</arg></p>
<optdesc><p>
- Use this to set the <arg>default latency</arg>, in frames, for audio coming from an unidentified source or from an iTunes Version 9 or earlier source. The standard value for the <arg>default latency</arg> is 88,200 frames, where there are 44,100
+ Use this to set the <arg>default latency</arg>, in frames, for audio coming from an
+ unidentified source or from an iTunes Version 9 or earlier source. The standard value
+ for the <arg>default latency</arg> is 88,200 frames, where there are 44,100
frames to the second.
</p>
- <p>Please note that this feature is deprecated and will be removed in a future version of shairport-sync.</p>
+ <p>Please note that this feature is deprecated and will be removed in a future version
+ of shairport-sync.</p>
</optdesc>
</option>
@@ -619,8 +962,9 @@
<optdesc><p>
Listen for metadata coming from the source and send it, along with metadata from
shairport-sync itself, to a pipe called <arg>shairport-sync-metadata</arg>
- in the <arg>directory</arg> you specify. If you add the <opt>--get-cover-art</opt> then
- cover art will be sent through the pipe too. See <url href="https://github.com/mikebrady/shairport-sync-metadata-reader"/>
+ in the <arg>directory</arg> you specify. If you add the <opt>--get-cover-art</opt>
+ then cover art will be sent through the pipe too. See <url
+ href="https://github.com/mikebrady/shairport-sync-metadata-reader"/>
for a sample metadata reader.
</p></optdesc>
</option>
@@ -635,7 +979,8 @@
</option>
<option>
- <p><opt>-o </opt><arg>outputbackend</arg><opt> | --output=</opt><arg>outputbackend</arg></p>
+ <p><opt>-o </opt><arg>outputbackend</arg><opt> |
+ --output=</opt><arg>outputbackend</arg></p>
<optdesc><p>
Force the use of the specified output backend to play the audio.
The default is to try the first one.
@@ -653,30 +998,20 @@
<option>
<p><opt>--password=</opt><arg>secret</arg></p>
<optdesc><p>
- Require the password <arg>secret</arg> to be able to connect and stream to the service.
+ Require the password <arg>secret</arg> to be able to connect and stream to the
+ service.
</p></optdesc>
</option>
<option>
- <p><opt>-R | --reconnectToOutput</opt></p>
- <optdesc><p>
- Reconnect the shairport-sync daemon to the output device and
- exit. It may take a few seconds to synchronise. (Requires that
- the daemon has written its PID to an agreed file -- see the <opt>-d</opt>
- option).
- </p>
- <p>Please note that this feature is deprecated and will be removed in a future version of shairport-sync.</p>
- </optdesc>
- </option>
-
- <option>
<p><opt>-r </opt><arg>threshold</arg><opt> | --resync=</opt><arg>threshold</arg></p>
<optdesc><p>
Resynchronise if timings differ by more than <arg>threshold</arg> 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 <opt>0</opt> to disable resynchronisation. This setting is deprecated and will be removed in a future version of shairport-sync.
+ milliseconds. Specify <opt>0</opt> to disable resynchronisation. This setting is
+ deprecated and will be removed in a future version of shairport-sync.
</p></optdesc>
</option>
@@ -721,10 +1056,22 @@
<option>
<p><opt>--tolerance=</opt><arg>frames</arg></p>
<optdesc><p>
- Allow playback to be up to <arg>frames</arg> 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 <opt>--statistics</opt> 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.
+ Allow playback to be up to <arg>frames</arg> 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 <opt>--statistics</opt> 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.
+ </p></optdesc>
+ </option>
+
+ <option>
+ <p><opt>-u</opt></p>
+ <optdesc><p>
+ 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.
</p></optdesc>
</option>
@@ -751,17 +1098,21 @@
</option>
<section name="Audio Backend Options">
- <p>These command-line options are passed to the chosen audio backend. The audio backend options are
+ <p>These command-line options are passed to the chosen audio backend. The audio
+ backend options are
preceded by a <opt>--</opt> 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.</p>
<p>In the ALSA backend, audio is sent to an output device
which you can specify using the <opt>-d</opt> option.
- The output level (the "volume") is controlled using a level control associated with a mixer.
+ 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 <opt>-c</opt> 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 <opt>-m</opt> option.</p>
+ To use a hardware level control on a mixer on the sound card, specify the name of the
+ mixer control with the <opt>-c</opt> 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 <opt>-m</opt> option.</p>
</section>
<option>
@@ -777,8 +1128,8 @@
<option>
<p><opt>-d </opt><arg>device</arg></p>
<optdesc><p>
- Use the specified output <arg>device</arg>. You may specify a card, e.g. <opt>hw:0</opt>, in
- which case the default output device on the card will be chosen.
+ Use the specified output <arg>device</arg>. You may specify a card, e.g.
+ <opt>hw:0</opt>, 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. <opt>hw:0,0</opt>.
The default is the device named <opt>default</opt>.
</p></optdesc>
@@ -787,8 +1138,8 @@
<option>
<p><opt>-m </opt><arg>mixer</arg></p>
<optdesc><p>
- Use the specified hardware <arg>mixer</arg> for volume control. Use this to specify where
- the mixer is to be found. For example, if the mixer is associated with a card,
+ Use the specified hardware <arg>mixer</arg> 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. <opt>hw:0</opt>.
If (unusually) the mixer is associated with a specific device on a card,
specify the device, e.g. <opt>hw:0,1</opt>.
@@ -801,8 +1152,10 @@
<p><opt>-t </opt><arg>devicetype</arg></p>
<optdesc>
<p>
- 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.
+ 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.
</p></optdesc>
</option>
</options>
@@ -821,14 +1174,13 @@
"Joe's Stereo" ( <opt>-a "Joe's Stereo"</opt> ) and will use the SoX Resampler
Library-based stuffing ( <opt>-S soxr</opt> ).
The audio backend options following the <opt>--</opt> separator specify
- that the audio will be output on output 0 of soundcard 1 (
- <opt>-d hw:1,0</opt> ) and will take advantage of the same sound card's mixer ( <opt>-m hw:1</opt> )
- using the level control named "PCM" ( <opt>-c "PCM"</opt> ).
+ that the audio will be output on output 0 of soundcard 1
+ ( <opt>-d hw:1,0</opt> ) and will take advantage of the same sound card's mixer
+ ( <opt>-m hw:1</opt> ) using the level control named "PCM" ( <opt>-c "PCM"</opt> ).
</p>
- <p>The example above is slightly contrived in order to show the use of the <opt>-m</opt> option.
- Typically, output 0 is the default output of a card,
- so the output device could be written <opt>-d hw:1</opt> and
- then the mixer option would be unnecessary, giving the following, simpler, command:</p>
+ <p>The example above is slightly contrived in order to show the use of the <opt>-m</opt>
+ option. Typically, output 0 is the default output of a card, so the output device could
+ be written <opt>-d hw:1</opt> and then the mixer option would be unnecessary, giving the following, simpler, command:</p>
<cmd>shairport-sync <opt>-d</opt>
<opt>-a "Joe's Stereo"</opt>
<opt>-S soxr</opt>
@@ -841,13 +1193,15 @@
<section name="Credits">
<p>Mike Brady developed shairport-sync from the original Shairport by James Laird.</p>
- <p>shairport-sync can be found at <url href="https://github.com/mikebrady/shairport-sync."/></p>
- <p>Shairport can be found at <url href="https://github.com/abrasive/shairport."/></p>
+ <p>shairport-sync can be found at
+ <url href="https://github.com/mikebrady/shairport-sync."/></p>
+ <p>Shairport can be found at
+ <url href="https://github.com/abrasive/shairport."/></p>
</section>
<section name="Comments">
- <p>This man page was written using <manref name="xml2man" section="1"
- href="http://masqmail.cx/xml2man/"/> by Oliver Kurth.</p>
+ <p>This man page was written using <manref name="xml2man" section="1"
+ href="http://masqmail.cx/xml2man/"/> by Oliver Kurth.</p>
</section>
</manpage>
diff --git a/man/shairport-sync.html b/man/shairport-sync.html
index 6a677a3..c16a56e 100644
--- a/man/shairport-sync.html
+++ b/man/shairport-sync.html
@@ -6,7 +6,7 @@
<h2>Synopsis</h2>
<b>
- shairport-sync <b>[-djvw]</b>
+ shairport-sync <b>[-djvuw]</b>
<b>[-a </b><em>name</em><b>]</b>
<b>[-A </b><em>latency</em><b>]</b>
<b>[-B </b><em>command</em><b>]</b>
@@ -27,14 +27,10 @@
<b>[-- </b><em>audio_backend_options</em><b>]</b>
<br>
- shairport-sync <b>-D</b><br>
-
shairport-sync <b>-k</b><br>
shairport-sync <b>-h</b><br>
- shairport-sync <b>-R</b><br>
-
shairport-sync <b>-V</b><br>
</b>
@@ -42,35 +38,60 @@
<h2>Description</h2>
- <p>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 &quot;sndio&quot; output device (available on OpenBSD, FreeBSD and Linux) or to a PulseAudio output stream (available on Linux).</p>
- <p>A feature of shairport-sync is that it 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.
+ <p>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 &quot;sndio&quot; output device (available on OpenBSD, FreeBSD and
+ Linux), to a PulseAudio output stream or to Jack Audio.</p>
+
+ <p>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 step with one another.
- This allows multiple devices to play the same source without getting out of phase with one another,
- enabling, for example, simultaneous multi-room operation.
+ 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.
</p>
- <p>shairport-sync can be compiled to stream audio synchronised audio output 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.</p>
- <p>shairport-sync can be compiled to stream metadata to a pipe or socket.</p>
+ <p>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.</p>
+ <p>Shairport Sync can be compiled to stream metadata, including cover art, to a pipe
+ or socket.</p>
+ <p>Shairport Sync can be compiled to offer a standard MPRIS interface, a &quot;native&quot;
+ 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.</p>
+
+ <p>Settings can be made using the configuration file (recommended for all new
+ installations) or by using command-line options.</p>
+
+ <p>The name of the Shairport Sync executable is <b>shairport-sync</b>.
+ Both names are used in these man pages.</p>
- <p>Settings can be made using the configuration file (recommended for all new installations) or by using command-line options.</p>
-
+
<h2>Configuration File Settings</h2>
- <p>You should use the configuration file for setting up shairport-sync.
- This file is usually <em>shairport-sync.conf</em> and is generally located in the System Configuration Directory, which is normally the <em>/etc</em> directory in Linux or the <em>/usr/local/etc</em> directory in BSD unixes.
+ <p>You should use the configuration file for setting up Shairport Sync.
+ This file is usually <em>shairport-sync.conf</em> and is generally located in the
+ System Configuration Directory, which is normally the <em>/etc</em> directory in
+ Linux or the <em>/usr/local/etc</em> directory in BSD unixes.
You may need to have root privileges to modify it.</p>
- <p>(Note: Shairport Sync may have been compiled to use a different configuration directory. You can determine which by performing the command <em>$ shairport-sync -V</em>. One of the items in the output string is the value of the <b>sysconfdir</b>,
+ <p>(Note: Shairport Sync may have been compiled to use a different configuration
+ directory. You can determine which by performing the command <em>$ shairport-sync
+ -V</em>. One of the items in the output string is the value of the
+ <b>sysconfdir</b>,
i.e. the System Configuration Directory.)</p>
- <p>Within the configuraton file, settings are organised into groups, for example, there is a &quot;general&quot; group of
+ <p>Within the configuraton file, settings are organised into groups, for
+ example, there is a &quot;general&quot; group of
standard settings, and there is an &quot;alsa&quot; group with settings that pertain to the ALSA
back end. Here is an example of a typical configuration file:</p>
@@ -86,10 +107,18 @@
<p><p><b>mixer_control_name = &quot;PCM&quot;;</b></p></p>
<p><b>};</b></p>
- <p>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, &quot;soxr&quot; interpolation was also enabled.</p>
+ <p>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, &quot;soxr&quot; interpolation was also enabled.</p>
- <p>A sample configuration file with all possible settings, but with all of them commented out, is installed at <em>shairport-sync.conf.sample</em>, within the System Configuration Directory -- <em>/etc</em> in Linux, <em>/usr/local/etc</em> in BSD unixes.</p>
+ <p>A sample configuration file with all possible settings, but with all of them
+ commented out, is installed at <em>shairport-sync.conf.sample</em>, within the
+ System Configuration Directory -- <em>/etc</em> in Linux,
+ <em>/usr/local/etc</em> in BSD unixes.</p>
<p>To retain backwards compatibility with previous versions of shairport-sync
you can use still use command line options, but any new features, etc. will
@@ -107,20 +136,26 @@
<p>Use this <em>service_name</em> to identify this player in iTunes, etc.</p>
<p>The following substitutions are allowed:
<b>%h</b> for the computer's hostname,
- <b>%H</b> for the computer's hostname with the first letter capitalised (ASCII only),
+ <b>%H</b> for the computer's hostname with the first letter capitalised (ASCII
+ only),
<b>%v</b> for the shairport-sync version number, e.g. &quot;3.0.1&quot; and
- <b>%V</b> for the shairport-sync version string, e.g. &quot;3.0.1-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc&quot;.</p>
- <p>The default is &quot;%H&quot;, which is replaced by the hostname with the first letter capitalised.</p>
+ <b>%V</b> for the shairport-sync version string, e.g.
+ &quot;3.0.1-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc&quot;.</p>
+ <p>The default is &quot;%H&quot;, which is replaced by the hostname with the first letter
+ capitalised.</p>
<p><b>password=</b><em>&quot;password&quot;</em><b>;</b></p>
- <p>Require the password <em>password</em> to connect to the service. If you leave this setting commented out, no password is needed.</p>
+ <p>Require the password <em>password</em> to connect to the service. If you
+ leave this setting commented out, no password is needed.</p>
+
<p><b>interpolation=</b><em>&quot;mode&quot;</em><b>;</b></p>
- <p>Interpolate, or &quot;stuff&quot;, the audio stream using the <em>mode</em>. Interpolation here refers to the
+ <p>Interpolate, or &quot;stuff&quot;, the audio stream using the <em>mode</em>.
+ 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.
@@ -133,39 +168,60 @@
- <p><b>statistics=</b><em>&quot;setting&quot;</em><b>;</b></p>
- <p>Use this <em>setting</em> to enable (&quot;yes&quot;) or disable (&quot;no&quot;) the output of some statistical information on the console or in the log. The default is to disable statistics.</p>
+ <p><b>output_backend=</b><em>&quot;backend&quot;</em><b>;</b></p>
+ <p>shairport-sync has a number of modules of code (&quot;backends&quot;) through which
+ audio is output. Normally, the first audio backend that works is selected. This
+ setting forces the selection of the specific audio <em>backend</em>. Perform the
+ command <b>shairport-sync -h</b> to get a list of available audio backends -- the
+ default is the first on this list. Only the &quot;alsa&quot;, &quot;sndio&quot; and &quot;pa&quot; backends support
+ synchronisation.</p>
<p><b>mdns_backend=</b><em>&quot;backend&quot;</em><b>;</b></p>
- <p>shairport-sync has a number of modules of code (&quot;backends&quot;) 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 <em>backend</em>. The default is &quot;avahi&quot;. Perform the command <b>shairport-sync -h</b> to get a list of available mDNS modules.</p>
-
-
- <p><b>output_backend=</b><em>&quot;backend&quot;</em><b>;</b></p>
- <p>shairport-sync has a number of modules of code (&quot;backends&quot;) through which audio is output. Normally, the first audio backend that works is selected. This setting forces the selection of the specific audio <em>backend</em>. Perform the command <b>shairport-sync -h</b> to get a list of available audio backends -- the default is the first on this list. Only the &quot;alsa&quot;, &quot;sndio&quot; and &quot;pa&quot; backends support synchronisation.</p>
+ <p>shairport-sync has a number of modules of code (&quot;backends&quot;) 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 <em>backend</em>. The default is &quot;avahi&quot;. Perform the command
+ <b>shairport-sync -h</b> to get a list of available mDNS modules.</p>
+
<p><b>port=</b><em>portnumber</em><b>;</b></p>
- <p>Use this to specify the <em>portnumber</em> shairport-sync uses to listen for service requests from iTunes, etc. The default is port 5000.</p>
+ <p>Use this to specify the <em>portnumber</em> shairport-sync uses to
+ listen for service requests from iTunes, etc. The default is port 5000.</p>
+
<p><b>udp_port_base=</b><em>portnumber</em><b>;</b></p>
- <p>When shairport-sync starts to play audio, it establises three UDP connections to the audio source. Use this setting to specify the starting <em>portnumber</em> for these three ports. It will pick the first three unused ports starting from <em>portnumber</em>. The default is port 6001.</p>
+ <p>When shairport-sync starts to play audio, it establises three UDP
+ connections to the audio source. Use this setting to specify the starting
+ <em>portnumber</em> for these three ports. It will pick the first three unused ports
+ starting from <em>portnumber</em>. The default is port 6001.</p>
+
<p><b>udp_port_range=</b><em>range</em><b>;</b></p>
- <p>Use this in conjunction with the prevous setting to specify the <em>range</em> 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.</p>
+ <p>Use this in conjunction with the prevous setting to specify the
+ <em>range</em> 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.</p>
+
<p><b>drift_tolerance_in_seconds=</b><em>seconds</em><b>;</b></p>
- <p>Allow playback to drift up to <em>seconds</em> 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 <b>statistics</b> setting to
- monitor correction levels. Corrections should not greatly exceed net corrections. This setting replaces the deprecated <b>drift</b> setting.
+ <p>Allow playback to drift up to <em>seconds</em> 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 <b>statistics</b> setting
+ to monitor correction levels. Corrections should not greatly exceed net corrections.
+ This setting replaces the deprecated <b>drift</b> setting.
</p>
+
<p><b>resync_threshold_in_seconds=</b><em>threshold</em><b>;</b></p>
<p>Resynchronise if timings differ by more than <em>threshold</em> seconds.
@@ -176,201 +232,482 @@
This setting replaces the deprecated <b>resync_threshold</b> setting.
</p>
+
- <p><b>log_verbosity=</b><em>0</em><b>;</b></p>
- <p>Use this to specify how much debugging information should be output or logged. The value <em>0</em> means no debug information, <em>3</em> means most debug information. The default is <em>0</em>.</p>
+ <p><b>ignore_volume_control=</b><em>&quot;choice&quot;</em><b>;</b></p>
+ <p>Set this <em>choice</em> to <em>&quot;yes&quot;</em> 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 <em>&quot;no&quot;</em>.</p>
- <p><b>ignore_volume_control=</b><em>&quot;choice&quot;</em><b>;</b></p>
- <p>Set this <em>choice</em> to <em>&quot;yes&quot;</em> 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 <em>&quot;no&quot;</em>.</p>
+ <p><b>volume_range_db=</b><em>dBvalue</em><b>;</b></p>
+ <p>Use this <em>dBvalue</em> to reduce or increase the attenuation range,
+ in decibels, between the minimum and maximum volume.</p>
+ <p>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.</p>
+ <p>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.</p>
+ <p>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.</p>
+ <p>If you omit this setting, the native range of the mixer is used.</p>
+
<p><b>volume_max_db=</b><em>dBvalue</em><b>;</b></p>
- <p>Specify the maximum output level to be used with the hardware mixer, if used. If no hardware mixed is used, this setting speciies the maximum setting permissible in the software mixer, which has an attenuation of from 0.0 dB down to -96.3 dB.
+ <p>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.
</p>
- <p><b>volume_range_db=</b><em>dBvalue</em><b>;</b></p>
- <p>Use this <em>dBvalue</em> to reduce or increase the attenuation range, in decibels, between the minimum and maximum volume.</p>
+ <p><b>volume_control_profile=</b><em>&quot;choice&quot;</em><b>;</b></p>
+ <p>Use this advanced setting to specify how the airplay volume is transferred
+ to the mixer volume. The <em>&quot;standard&quot;</em> profile, which is the default, makes
+ the volume change more quickly at lower volumes and slower at higher volumes. Choose
+ the <em>&quot;flat&quot;</em> profile to makes the volume change at the same rate at all
+ volume levels.
+ </p>
- <p>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.</p>
- <p>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.</p>
- <p>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.</p>
- <p>If you omit this setting, the native range of the mixer is used.</p>
+
+
+ <p><b>volume_range_combined_hardware_priority=</b>
+ <em>&quot;choice&quot;</em><b>;</b></p>
+ <p>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 <em>&quot;yes&quot;</em> 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.</p>
+ <p>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.</p>
+ <p>Unfortunately, certain hardware attenuators will mute at their greatest
+ attenuation, so can't be combined with software attenuation in this way. Choosing
+ <em>&quot;no&quot;</em> means that software attenuation is used to bring the remaining
+ attenuation required into the range offered by the hardware attenuator.
+ This is the default.
+ </p>
+
+
+ <p><b>run_this_when_volume_is_set=</b>
+ <em>&quot;/full/path/to/application/and/args&quot;</em><b>;</b></p>
+ <p>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 <em>#!/bin/...</em> as appropriate.</p>
+ <p>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 &quot;mute&quot;.</p>
+
+
<p><b>regtype=</b><em>&quot;regTypeString&quot;</em><b>;</b></p>
- <p>Use this advanced setting to set the service type and transport to be advertised by Zeroconf/Bonjour. Default is <em>&quot;_raop._tcp&quot;</em>.</p>
+ <p>Use this advanced setting to set the service type and transport to be
+ advertised by Zeroconf/Bonjour. Default is <em>&quot;_raop._tcp&quot;</em>.</p>
<p><b>playback_mode=</b><em>&quot;mode&quot;</em><b>;</b></p>
- <p>The <em>mode</em> can be &quot;stereo&quot;, &quot;mono&quot;, &quot;reverse stereo&quot;, &quot;both left&quot; or &quot;both right&quot;. Default is &quot;stereo&quot;.</p>
+ <p>The <em>mode</em> can be &quot;stereo&quot;, &quot;mono&quot;, &quot;reverse stereo&quot;, &quot;both left&quot;
+ or &quot;both right&quot;. Default is &quot;stereo&quot;. Note that dither will be added to the signal in
+ the mono mode.</p>
- <p><b>interface=</b><em>&quot;name&quot;</em><b>;</b></p>
- <p>Use this advanced setting if you want to confine Shairport Sync to the named interface. Leave it commented out to get the default bahaviour.</p>
+ <p><b>alac_decoder=</b><em>&quot;decodername&quot;</em><b>;</b></p>
+ <p>This can be &quot;hammerton&quot; or &quot;apple&quot;. 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 &quot;--with-apple-alac&quot; and the Apple ALAC decoder library must be
+ present for this to work.</p>
+
+ <p><b>interface=</b><em>&quot;name&quot;</em><b>;</b></p>
+ <p>Use this advanced setting if you want to confine Shairport Sync to the
+ named interface. Leave it commented out to get the default bahaviour.</p>
- <p><b>alac_decoder=</b><em>&quot;decodername&quot;</em><b>;</b></p>
- <p>This can be &quot;hammerton&quot; or &quot;apple&quot;. 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 &quot;--with-apple-alac&quot; and the Apple ALAC decoder library must be present for this to work.</p>
- <p><b>audio_backend_latency_offset_in_seconds=</b><em>offset_in_seconds</em><b>;</b></p>
- <p>Set this <em>offset_in_seconds</em> 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.</p>
+ <p><b>audio_backend_latency_offset_in_seconds=</b>
+ <em>offset_in_seconds</em><b>;</b></p>
+ <p>Set this <em>offset_in_seconds</em> 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.</p>
+
- <p><b>audio_backend_buffer_desired_length_in_seconds=</b><em>length_in_seconds</em><b>;</b></p>
- <p>Use this <em>length_in_seconds</em> to set the desired length of the queue of audio frames in the backend's output buffer.</p>
- <p>The default is 0.15 seconds for the ALSA backend, 0.35 seconds for the PA backend and one second for all other backends.</p>
+ <p><b>audio_backend_buffer_desired_length_in_seconds=</b>
+ <em>length_in_seconds</em><b>;</b></p>
+ <p>Use this <em>length_in_seconds</em> to set the desired length of the
+ queue of audio frames in the backend's output buffer.</p>
+ <p>The default is 0.15 seconds for the ALSA backend, 0.35 seconds for the PA backend
+ and one second for all other backends.</p>
<p>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.</p>
+ 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.</p>
+
- <p><b>audio_backend_silent_lead_in_time=</b><em>lead_in_time_in_seconds</em><b>;</b></p>
- <p>This is an advanced setting. Use the <em>lead_in_time_in_seconds</em> to set the desired length of the period of silence
- (a &quot;silent lead-in&quot;) played before a play session begins.</p>
- <p>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.</p>
- <p>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.</p>
+ <p><b>audio_backend_buffer_interpolation_threshold_in_seconds=</b>
+ <em>time_in_seconds</em><b>;</b></p>
+ <p>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.</p>
+
- <p><b>run_this_when_volume_is_set=</b><em>&quot;/full/path/to/application/and/args&quot;</em><b>;</b></p>
- <p>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 appropriate shebang <em>#!/bin/...</em>.</p>
- <p>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.0 to -30.0 and -144.0 means &quot;mute&quot;.</p>
+ <p><b>audio_backend_silent_lead_in_time=</b>
+ <em>lead_in_time_in_seconds</em><b>;</b></p>
+ <p>This is an advanced setting. Use the <em>lead_in_time_in_seconds</em> to
+ set the desired length of the period of silence (a &quot;silent lead-in&quot;) played before a
+ play session begins.</p>
+ <p>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.</p>
+ <p>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.</p>
+
+
+
+ <p><b>dbus_service_bus=</b>
+ <em>&quot;bus_name&quot;</em><b>;</b></p>
+ <p>If shairport sync is compiled with the D-Bus interface, it can offer it on
+ the <em>&quot;system&quot;</em> or the <em>&quot;session&quot;</em> D-Bus &quot;bus&quot;.
+ Use this to specify which. The default is to use the &quot;system&quot; bus.</p>
+
+
+
+ <p><b>mpris_service_bus=</b>
+ <em>&quot;bus_name&quot;</em><b>;</b></p>
+ <p>If shairport sync is compiled with the MPRIS interface, it can offer the
+ service on the <em>&quot;system&quot;</em> or the <em>&quot;session&quot;</em> D-Bus &quot;bus&quot;.
+ Use this to specify which. The default is to use the &quot;system&quot; bus.</p>
+
+
+ <p><b>&quot;SESSIONCONTROL&quot; SETTINGS</b></p>
+
+
+ <p><b>run_this_before_play_begins=</b><em>&quot;/path/to/application and
+ args&quot;</em><b>;</b></p>
+ <p>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 <em>#!/bin/...</em> as
+ appropriate.</p>
+
+
+
+ <p><b>run_this_after_play_ends=</b><em>&quot;/path/to/application and
+ args&quot;</em><b>;</b></p>
+ <p>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 <em>#!/bin/...</em> as
+ appropriate.</p>
+
+
+
+ <p><b>run_this_before_entering_active_state=</b><em>&quot;/path/to/application and
+ args&quot;</em><b>;</b></p>
+ <p>Here you can specify a program and its arguments that will be run just
+ before shairport-sync goes active.</p>
+
+ <p>Shairport Sync goes &quot;active&quot; when a play session starts. When the play
+ session ends, the system will stay active until the time
+ specified in the <b>active_state_timeout</b> setting elapses.
+ If a new play session starts before that, the system will remain active. Otherwise,
+ the system will go inactive.
+ </p>
+
+ <p>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 <em>#!/bin/...</em> as
+ appropriate.</p>
+
+
+
+ <p><b>run_this_after_exiting_active_state=</b><em>&quot;/path/to/application and
+ args&quot;</em><b>;</b></p>
+ <p>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 <em>#!/bin/...</em> as
+ appropriate.</p>
+
+
+
+ <p><b>active_state_timeout=</b><em>seconds</em><b>;</b></p>
+ <p>After a play session has ended, the system will remain active for
+ <em>seconds</em> 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.</p>
+
+
+
+ <p><b>run_this_if_an_unfixable_error_is_detected=</b><em>&quot;/path/to/application
+ and args&quot;</em><b>;</b></p>
+ <p>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 &quot;stalled&quot; -- that is, if an output device
+ refuses to accept any more output frames.</p>
+ <p>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.</p>
+ <p>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 <em>#!/bin/...</em> as
+ appropriate.</p>
+
+
+
+ <p><b>wait_for_completion=</b><em>&quot;choice&quot;</em><b>;</b></p>
+ <p>Set <em>choice</em> to &quot;yes&quot; to make shairport-sync wait until the
+ programs specified in the <b>run_this_...</b> settings have
+ completed execution before continuing. The default is &quot;no&quot;.</p>
+
+
+
+ <p><b>allow_session_interruption=</b><em>&quot;choice&quot;</em><b>;</b></p>
+ <p>If <b>choice</b> is set to &quot;yes&quot;, then another source will be able to
+ interrupt an existing play session and start a new one.
+ When set to &quot;no&quot; (the default), other devices attempting to interrupt a session will
+ fail, receiving a busy signal.</p>
+
+ <p><b>session_timeout=</b><em>seconds</em><b>;</b></p>
+ <p>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.</p>
+
+
<p><b>&quot;ALSA&quot; SETTINGS</b></p>
- <p>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 <b>alsamixer</b> or <b>aplay</b> 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 <b>alsa</b> group settings:</p>
+ <p>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
+ <b>alsamixer</b> or <b>aplay</b> 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 <b>alsa</b> group settings:</p>
<p><b>output_device=</b><em>&quot;output_device&quot;</em><b>;</b></p>
- <p>Use the output device called <em>output_device</em>. The default is the device called <em>&quot;default&quot;</em>.</p>
+ <p>Use the output device called <em>output_device</em>. The default is the
+ device called <em>&quot;default&quot;</em>.</p>
+
<p><b>mixer_control_name=</b><em>&quot;name&quot;</em><b>;</b></p>
- <p>Specify the <em>name</em> of the mixer control to be used by shairport-sync to control the volume.
+ <p>Specify the <em>name</em> 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.</p>
+ If you do not specify a mixer control name, shairport-sync will adjust the volume in
+ software.</p>
+
<p><b>mixer_device=</b><em>&quot;mixer_device&quot;</em><b>;</b></p>
- <p>By default, the mixer is assumed to be output_device. Use this setting to specify a device other than the output device.</p>
+ <p>By default, the mixer is assumed to be output_device. Use this setting to
+ specify a device other than the output device.</p>
<p><b>output_rate=</b><em>frame rate</em><b>;</b></p>
- <p>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.
+ <p>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.
</p>
<p><b>output_format=</b><em>&quot;format&quot;</em><b>;</b></p>
- <p>Use this setting to specify the format that should be used to send data to the ALSA device. Allowable values are &quot;U8&quot;, &quot;S8&quot;, &quot;S16&quot;, &quot;S24&quot;, &quot;S24_3LE&quot;, &quot;S24_3BE&quot; or &quot;S32&quot;. The device must have the capability to accept the format you specify.</p><p>&quot;S&quot; means signed; &quot;U&quot; 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 &quot;S16&quot;.</p><p>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.
+ <p>Use this setting to specify the format that should be used to send data to
+ the ALSA device. Allowable values are &quot;U8&quot;, &quot;S8&quot;, &quot;S16&quot;, &quot;S24&quot;, &quot;S24_3LE&quot;, &quot;S24_3BE&quot;
+ or &quot;S32&quot;. The device must have the capability to accept the format you
+ specify.</p><p>&quot;S&quot; means signed; &quot;U&quot; 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 &quot;S16&quot;.</p><p>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.
</p>
<p><b>disable_synchronization=</b><em>&quot;no&quot;</em><b>;</b></p>
- <p>This is an advanced setting and is for debugging only. Set to <em>&quot;yes&quot;</em> to disable synchronization. Default is <em>&quot;no&quot;</em>.
- If you use it to disable synchronisation, then sooner or later you'll experience audio glitches due to
- audio buffer overflow or underflow.
+ <p>This is an advanced setting and is for debugging only. Set to
+ <em>&quot;yes&quot;</em> to disable synchronization. Default is <em>&quot;no&quot;</em>.
+ If you use it to disable synchronisation, then sooner or later you'll experience audio
+ glitches due to audio buffer overflow or underflow.
</p>
+
<p><b>period_size=</b><em>number</em><b>;</b></p>
- <p>Use this optional advanced setting to set the alsa period size near to this value.</p>
+ <p>Use this optional advanced setting to set the alsa period size near to
+ this value.</p>
+
<p><b>buffer_size=</b><em>number</em><b>;</b></p>
- <p>Use this optional advanced setting to set the alsa buffer size near to this value.</p>
+ <p>Use this optional advanced setting to set the alsa buffer size near to
+ this value.</p>
+
<p><b>use_mmap_if_available=</b><em>&quot;yes&quot;</em><b>;</b></p>
- <p> Use this optional advanced setting to control whether MMAP-based output is used to communicate with the DAC. Default is <em>&quot;yes&quot;</em>.</p>
+ <p> Use this optional advanced setting to control whether MMAP-based output
+ is used to communicate with the DAC. Default is <em>&quot;yes&quot;</em>.</p>
<p><b>mute_using_playback_switch=</b><em>&quot;no&quot;</em><b>;</b></p>
- <p>This is an advanced setting and the default is <em>&quot;no&quot;</em>. If it is set to <em>&quot;yes&quot;</em>, hardware mute will be implemented using a feature called a 'playback switch', where one is available. Set it to <em>&quot;no&quot;</em> to prevent the playback switch being used.</p>
- <p>If Shairport Sync is sharing the output device with other applications, it is best to leave this set to <em>&quot;no&quot;</em> for compatibility with those applications.</p>
- <p>Another motivation for this is to allow the alsa function call
- &quot;snd_mixer_selem_set_playback_switch_all&quot; to be avoided. It is incorrectly implemented on certain soundcards, including the emulated card in VMWare Fusion 8.5.</p>
+ <p>This is an advanced setting and the default is <em>&quot;no&quot;</em>. If it is set to
+ <em>&quot;yes&quot;</em>, hardware mute will be used where it is available.
+ Set it to <em>&quot;no&quot;</em> to prevent the hardware mute being used.</p>
+ <p>If Shairport Sync is sharing the output device with other applications, it is best
+ to leave this set to <em>&quot;no&quot;</em> for compatibility with those applications.</p>
+ <p>Another motivation for this is to allow the ALSA function call
+ &quot;snd_mixer_selem_set_playback_switch_all&quot; to be avoided. It is incorrectly implemented
+ on certain soundcards, including the emulated card in VMWare Fusion 8.5.</p>
+
+
+
+
+ <p><b>maximum_stall_time=</b><em>seconds</em><b>;</b></p>
+ <p>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
+ <b>run_this_if_an_unfixable_error_is_detected</b> program,
+ if any, being called.</p>
+ <p>Implemented for the ALSA back end only.</p>
+
+
+ <p><b>disable_standby_mode=</b><em>&quot;never&quot;</em><b>;</b></p>
+
+ <p>Shairport Sync has a &quot;Disable Standby&quot; 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: &quot;never&quot; means it will never be activated, &quot;always&quot;
+ means it will be active as soon as shairport-sync starts running, and &quot;auto&quot;
+ means it will be active while shairport-sync is in the &quot;active&quot; state.</p>
+ <p>Shairport Sync goes &quot;active&quot; 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.
+ </p>
+
+
+
<p><b>&quot;SNDIO&quot; SETTINGS</b></p>
- <p>These settings are for the SNDIO back end, used to communicate with audio output devices in the SNDIO system.</p>
+ <p>These settings are for the SNDIO back end, used to communicate with audio output
+ devices in the SNDIO system.</p>
+
<p><b>device=</b><em>&quot;snd/0&quot;</em><b>;</b></p>
- <p>Use this optional setting to specify the name of the output device, e.g. <em>&quot;snd/0&quot;</em>. The default is to use the SNDIO system's default.</p>
+ <p>Use this optional setting to specify the name of the output device, e.g.
+ <em>&quot;snd/0&quot;</em>. The default is to use the SNDIO system's default.</p>
+
<p><b>rate=</b><em>44100</em><b>;</b></p>
- <p>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.</p>
+ <p>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.</p>
+
<p><b>format=</b><em>&quot;S16&quot;</em><b>;</b></p>
- <p>Use this optional setting to specify the output format. Allowable values are &quot;U8&quot;, &quot;S8&quot;, &quot;S16&quot;, &quot;S24&quot;, &quot;S24_3LE&quot;, &quot;S24_3BE&quot; or &quot;S32&quot;.
- The device must have the capability to accept the format you specify.</p><p>&quot;S&quot; means signed; &quot;U&quot; 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 &quot;S16&quot;.</p><p>
- 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.
+ <p>Use this optional setting to specify the output format. Allowable values
+ are &quot;U8&quot;, &quot;S8&quot;, &quot;S16&quot;, &quot;S24&quot;, &quot;S24_3LE&quot;, &quot;S24_3BE&quot; or &quot;S32&quot;.
+ The device must have the capability to accept the format you specify.</p><p>&quot;S&quot; means
+ signed; &quot;U&quot; 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 &quot;S16&quot;.</p><p>
+ 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.</p>
- <p>Please note that 32- or 24-bit has not been extensively tested on SNDIO.</p>
+ consider using 32- or 24-bit output if your device is capable of it, to get the lowest
+ possible levels of dither.</p>
+ <p>Please note that 32- or 24-bit has not been extensively tested on
+ SNDIO.</p>
+
<p><b>round=</b><em>value</em><b>;</b></p>
- <p>Use this optional advanced setting to specify the period size of the SNDIO channel. If omitted, a SNDIO system default value will be used.</p>
+ <p>Use this optional advanced setting to specify the period size of the SNDIO
+ channel. If omitted, a SNDIO system default value will be used.</p>
+
<p><b>bufsiz=</b><em>value</em><b>;</b></p>
- <p>Use this optional advanced setting to specify the buffer size of the SNDIO channel. If omitted, a SNDIO system default value will be used.</p>
+ <p>Use this optional advanced setting to specify the buffer size of the SNDIO
+ channel. If omitted, a SNDIO system default value will be used.</p>
-
<p><b>&quot;PA&quot; SETTINGS</b></p>
<p>These settings are for the new PulseAudio backend.</p>
+
<p><b>application_name=</b><em>&quot;Shairport Sync&quot;</em><b>;</b></p>
- <p>Use this to set the name to appear in the Sounds &quot;Applications&quot; tab when Shairport Sync is active. The default is the name &quot;Shairport Sync&quot;.</p>
+ <p>Use this to set the name to appear in the Sounds &quot;Applications&quot; tab when
+ Shairport Sync is active. The default is the name &quot;Shairport Sync&quot;.</p>
<p><b>&quot;PIPE&quot; SETTINGS</b></p>
- <p>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.</p>
+ <p>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.</p>
+
<p><b>name=</b><em>&quot;/path/to/pipe&quot;</em><b>;</b></p>
- <p>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.</p>
+ <p>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.</p>
<p><b>&quot;STDOUT&quot; SETTINGS</b></p>
@@ -379,76 +716,76 @@
<p><b>&quot;AO&quot; SETTINGS</b></p>
<p>There are no configuration file settings for the AO backend.</p>
-
<p><b>&quot;METADATA&quot; SETTINGS</b></p>
- <p>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 <em>/tmp/shairport-sync-metadata</em>.
- To process metadata, shairport-sync must have been compiled with metadata support included.
- You can check that this is so by running the command <b>$ shairport-sync -V</b>; the identification string will contain the word <b>metadata</b>.</p>
- <p>Please note that different sources provide different levels of metadata. Some provide a lot; some provide almost none.</p>
- <p>The <b>metadata</b> group of settings allow you to enable metadata handling and to control certain aspects of it:</p>
-
+ <p>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
+ <em>/tmp/shairport-sync-metadata</em>.
+ To process metadata, shairport-sync must have been compiled with metadata support
+ included.
+ You can check that this is so by running the command <b>$ shairport-sync -V</b>;
+ the identification string will contain the word <b>metadata</b>.</p>
+ <p>Please note that different sources provide different levels of metadata. Some
+ provide a lot; some provide almost none.</p>
+ <p>The <b>metadata</b> group of settings allow you to enable metadata handling and
+ to control certain aspects of it:</p>
<p><b>enabled=</b><em>&quot;choice&quot;</em><b>;</b></p>
- <p>Set the <em>choice</em> to &quot;yes&quot; 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 &quot;no&quot;.</p>
+ <p>Set the <em>choice</em> to &quot;yes&quot; 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 &quot;no&quot;.</p>
+
<p><b>include_cover_art=</b><em>&quot;choice&quot;</em><b>;</b></p>
- <p>Set the <em>choice</em> to &quot;yes&quot; to enable shairport-sync to look for cover art from the audio source and to include it in the feed to the metadata pipe.
+ <p>Set the <em>choice</em> to &quot;yes&quot; 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.
+ 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 &quot;no&quot;.</p>
+
<p><b>pipe_name=</b><em>&quot;filepathname&quot;</em><b>;</b></p>
- <p>Specify the absolute path name of the pipe through which metadata should be sent The default is <em>/tmp/shairport-sync-metadata</em>.</p>
+ <p>Specify the absolute path name of the pipe through which metadata should
+ be sent The default is <em>/tmp/shairport-sync-metadata</em>.</p>
<p><b>socket_address=</b><em>&quot;hostnameOrIP&quot;</em><b>;</b></p>
- <p>If <em>hostnameOrIP</em> 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, <em>socket-port</em> must be non-zero and <em>enabled</em> must be set to &quot;yes&quot;.</p>
+ <p>If <em>hostnameOrIP</em> 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, <em>socket-port</em> must be non-zero and
+ <em>enabled</em> must be set to &quot;yes&quot;.</p>
+
<p><b>socket_port=</b><em>port</em><b>;</b></p>
- <p>If <b>socket_address</b> is set, use <em>port</em> to specify the port to send UDP packets to. Must not be zero.</p>
+ <p>If <b>socket_address</b> is set, use <em>port</em> to specify the
+ port to send UDP packets to. Must not be zero.</p>
+
<p><b>socket_msglength=</b><em>65000</em><b>;</b></p>
- <p>The maximum packet size for any UDP metadata. This must be between 500 or 65000. The default is 500.</p>
+ <p>The maximum packet size for any UDP metadata. This must be between 500 or
+ 65000. The default is 500.</p>
- <p><b>&quot;SESSIONCONTROL&quot; SETTINGS</b></p>
- <p>shairport-sync can run programs just before it starts to play an audio stream and just after it finishes.
- You specify them using the sessioncontrol group settings run_this_before_play_begins and run_this_after_play_ends.</p>
-
-
- <p><b>run_this_before_play_begins=</b><em>&quot;/path/to/application and args&quot;</em><b>;</b></p>
- <p>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 appropriate shebang <em>#!/bin/...</em>.</p>
+ <p><b>&quot;DIAGNOSTICS&quot; SETTINGS</b></p>
+ <p><b>statistics=</b><em>&quot;setting&quot;</em><b>;</b></p>
+ <p>Use this <em>setting</em> to enable (&quot;yes&quot;) or disable (&quot;no&quot;) the output
+ of some statistical information on the console or in the log. The default is to
+ disable statistics.</p>
- <p><b>run_this_after_play_ends=</b><em>&quot;/path/to/application and args&quot;</em><b>;</b></p>
- <p>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 appropriate shebang <em>#!/bin/...</em>.</p>
-
-
- <p><b>wait_for_completion=</b><em>&quot;choice&quot;</em><b>;</b></p>
- <p>Set <em>choice</em> to &quot;yes&quot; to make shairport-sync wait until the programs specified in the <b>run_this_before_play_begins</b>,
- <b>run_this_after_play_ends</b> and <b>run_this_when_volume_is_set</b> have completed execution before continuing. The default is &quot;no&quot;.</p>
-
-
- <p><b>allow_session_interruption=</b><em>&quot;choice&quot;</em><b>;</b></p>
- <p>If <b>choice</b> is set to &quot;yes&quot;, then another source will be able to interrupt an existing play session and start a new one.
- When set to &quot;no&quot; (the default), other devices attempting to interrupt a session will fail, receiving a busy signal.</p>
-
+
- <p><b>session_timeout=</b><em>seconds</em><b>;</b></p>
- <p>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 <em>seconds</em> seconds before ending the session. Once the session has terminated, other devices can use it.
- The default is 120 seconds.</p>
+ <p><b>log_verbosity=</b><em>0</em><b>;</b></p>
+ <p>Use this to specify how much debugging information should be output or
+ logged. The value <em>0</em> means no debug information, <em>3</em> means most
+ debug information. The default is <em>0</em>.</p>
@@ -457,10 +794,11 @@
<h2>Options</h2>
<p>This section is about the command-line options available in shairport-sync.</p>
-
- <p>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.</p>
+ <p>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.</p>
<p>Many command-line options take sensible default values, so you can normally
ignore most of them. See the EXAMPLES section for typical usages.</p>
@@ -478,16 +816,20 @@
- <p><b>-a </b><em>service name</em><b> | --name=</b><em>service name</em></p>
+ <p><b>-a </b><em>service name</em><b> | --name=</b><em>service
+ name</em></p>
<p>
Use this <em>service name</em> to identify this player in iTunes, etc.</p>
<p>The following substitutions are allowed:
<b>%h</b> for the computer's hostname,
- <b>%H</b> for the computer's hostname with the first letter capitalised (ASCII only),
+ <b>%H</b> for the computer's hostname with the first letter capitalised (ASCII
+ only),
<b>%v</b> for the shairport-sync version number, e.g. &quot;3.0.1&quot; and
- <b>%V</b> for the shairport-sync version string, e.g. &quot;3.0.1-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc&quot;.</p>
- <p>The default is &quot;%H&quot;, which is replaced by the hostname with the first letter capitalised.</p>
+ <b>%V</b> for the shairport-sync version string, e.g.
+ &quot;3.0.1-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc&quot;.</p>
+ <p>The default is &quot;%H&quot;, which is replaced by the hostname with the first letter
+ capitalised.</p>
@@ -496,8 +838,8 @@
<p>
Execute <em>program</em> when playback is about to begin. Specify the
full path to the program, e.g. <em>/usr/bin/logger</em>.
- Executable scripts can be used, but they must have the appropriate shebang
- <em>#!/bin/sh</em> in the headline.</p>
+ Executable scripts can be used, but they must have the appropriate shebang
+ (<em>#!/bin/sh</em> in the headline.</p>
<p>If you want shairport-sync to wait until the command has
completed before starting to play, select the <b>-w</b> option as well.
@@ -507,30 +849,23 @@
<p><b>-c </b><em>filename</em><b> | --configfile=</b><em>filename</em></p>
<p>
- Read configuration settings from <em>filename</em>. The default is to read them from the <em>shairport-sync.conf</em> in the System Configuration Directory -- <em>/etc</em> in Linux, <em>/usr/local/etc</em> in BSD unixes.
- For information about configuration settings, see the &quot;Configuration File Settings&quot; section above.
+ Read configuration settings from <em>filename</em>. The default is to read them from
+ the <em>shairport-sync.conf</em> in the System Configuration Directory --
+ <em>/etc</em> in Linux, <em>/usr/local/etc</em> in BSD unixes.
+ For information about configuration settings, see the &quot;Configuration File Settings&quot;
+ section above.
</p>
- <p><b>-D | --disconnectFromOutput</b></p>
- <p>
- Disconnect the shairport-sync daemon from the output device and
- exit. (Requires that the daemon has written its PID to an agreed
- file -- see the <b>-d</b> option).
- </p>
- <p>Please note that this feature is deprecated and will be removed in a future version of shairport-sync.</p>
-
-
-
-
<p><b>-d | --daemon</b></p>
<p>
Instruct shairport-sync to demonise itself. It will write its
Process ID (PID) to a file, usually at
<em>/var/run/shairport-sync/shairport-sync.pid</em>, which is used by the
<b>-k</b>, <b>-D</b> and <b>-R</b> options to locate
- the daemon at a later time. See also the <b>-j</b> option.
+ the daemon at a later time. See also the <b>-j</b> option. Only available if
+ shaiport-sync has been compiled with libdaemon support.
</p>
@@ -539,8 +874,8 @@
<p>
Execute <em>program</em> when playback has ended. Specify the
full path to the program, e.g. <em>/usr/bin/logger</em>.
- Executable scripts can be used, but they must have the appropriate shebang
- <em>#!/bin/sh</em> in the headline.</p>
+ Executable scripts can be used, but they must have the appropriate shebang
+ (<em>#!/bin/sh</em> in the headline.</p>
<p>If you want shairport-sync to wait until the command has
completed before continuing, select the <b>-w</b> option as well.
</p>
@@ -567,8 +902,9 @@
<p><b>-j</b></p>
<p>
- Instruct shairport-sync to demonise itself. Unlike the <b>-d</b> option, it will not write a
- Process ID (PID) to a file -- it will just (hence the &quot;j&quot;) demonise itself.
+ Instruct shairport-sync to demonise itself. Unlike the <b>-d</b> option, it will
+ not write a Process ID (PID) to a file -- it will just (hence the &quot;j&quot;) demonise
+ itself. Only available if shaiport-sync has been compiled with libdaemon support.
</p>
@@ -576,15 +912,17 @@
<p><b>-k | --kill</b></p>
<p>
Kill the shairport-sync daemon and exit. (Requires that the daemon has
- written its PID to an agreed file -- see the <b>-d</b> option).
+ written its PID to an agreed file -- see the <b>-d</b> option. Only available if
+ shaiport-sync has been compiled with libdaemon support.)
</p>
<p><b>--logOutputLevel</b></p>
<p>
- 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.
+ 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.
</p>
@@ -592,10 +930,13 @@
<p><b>-L | --latency=</b><em>latency</em></p>
<p>
- Use this to set the <em>default latency</em>, in frames, for audio coming from an unidentified source or from an iTunes Version 9 or earlier source. The standard value for the <em>default latency</em> is 88,200 frames, where there are 44,100
+ Use this to set the <em>default latency</em>, in frames, for audio coming from an
+ unidentified source or from an iTunes Version 9 or earlier source. The standard value
+ for the <em>default latency</em> is 88,200 frames, where there are 44,100
frames to the second.
</p>
- <p>Please note that this feature is deprecated and will be removed in a future version of shairport-sync.</p>
+ <p>Please note that this feature is deprecated and will be removed in a future version
+ of shairport-sync.</p>
@@ -604,8 +945,8 @@
<p>
Listen for metadata coming from the source and send it, along with metadata from
shairport-sync itself, to a pipe called <em>shairport-sync-metadata</em>
- in the <em>directory</em> you specify. If you add the <b>--get-cover-art</b> then
- cover art will be sent through the pipe too. See <a href = "https://github.com/mikebrady/shairport-sync-metadata-reader">https://github.com/mikebrady/shairport-sync-metadata-reader</a>
+ in the <em>directory</em> you specify. If you add the <b>--get-cover-art</b>
+ then cover art will be sent through the pipe too. See <a href = "https://github.com/mikebrady/shairport-sync-metadata-reader">https://github.com/mikebrady/shairport-sync-metadata-reader</a>
for a sample metadata reader.
</p>
@@ -620,7 +961,8 @@
- <p><b>-o </b><em>outputbackend</em><b> | --output=</b><em>outputbackend</em></p>
+ <p><b>-o </b><em>outputbackend</em><b> |
+ --output=</b><em>outputbackend</em></p>
<p>
Force the use of the specified output backend to play the audio.
The default is to try the first one.
@@ -638,30 +980,20 @@
<p><b>--password=</b><em>secret</em></p>
<p>
- Require the password <em>secret</em> to be able to connect and stream to the service.
+ Require the password <em>secret</em> to be able to connect and stream to the
+ service.
</p>
- <p><b>-R | --reconnectToOutput</b></p>
- <p>
- Reconnect the shairport-sync daemon to the output device and
- exit. It may take a few seconds to synchronise. (Requires that
- the daemon has written its PID to an agreed file -- see the <b>-d</b>
- option).
- </p>
- <p>Please note that this feature is deprecated and will be removed in a future version of shairport-sync.</p>
-
-
-
-
<p><b>-r </b><em>threshold</em><b> | --resync=</b><em>threshold</em></p>
<p>
Resynchronise if timings differ by more than <em>threshold</em> 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 <b>0</b> to disable resynchronisation. This setting is deprecated and will be removed in a future version of shairport-sync.
+ milliseconds. Specify <b>0</b> to disable resynchronisation. This setting is
+ deprecated and will be removed in a future version of shairport-sync.
</p>
@@ -706,10 +1038,22 @@
<p><b>--tolerance=</b><em>frames</em></p>
<p>
- Allow playback to be up to <em>frames</em> 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 <b>--statistics</b> 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.
+ Allow playback to be up to <em>frames</em> 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 <b>--statistics</b> 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.
+ </p>
+
+
+
+ <p><b>-u</b></p>
+ <p>
+ 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.
</p>
@@ -737,17 +1081,21 @@
<h2>Audio Backend Options</h2>
- <p>These command-line options are passed to the chosen audio backend. The audio backend options are
+ <p>These command-line options are passed to the chosen audio backend. The audio
+ backend options are
preceded by a <b>--</b> 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.</p>
<p>In the ALSA backend, audio is sent to an output device
which you can specify using the <b>-d</b> option.
- The output level (the &quot;volume&quot;) is controlled using a level control associated with a mixer.
+ The output level (the &quot;volume&quot;) 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 <b>-c</b> 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 <b>-m</b> option.</p>
+ To use a hardware level control on a mixer on the sound card, specify the name of the
+ mixer control with the <b>-c</b> 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 <b>-m</b> option.</p>
@@ -764,8 +1112,8 @@
<p><b>-d </b><em>device</em></p>
<p>
- Use the specified output <em>device</em>. You may specify a card, e.g. <b>hw:0</b>, in
- which case the default output device on the card will be chosen.
+ Use the specified output <em>device</em>. You may specify a card, e.g.
+ <b>hw:0</b>, 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. <b>hw:0,0</b>.
The default is the device named <b>default</b>.
</p>
@@ -774,8 +1122,8 @@
<p><b>-m </b><em>mixer</em></p>
<p>
- Use the specified hardware <em>mixer</em> for volume control. Use this to specify where
- the mixer is to be found. For example, if the mixer is associated with a card,
+ Use the specified hardware <em>mixer</em> 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. <b>hw:0</b>.
If (unusually) the mixer is associated with a specific device on a card,
specify the device, e.g. <b>hw:0,1</b>.
@@ -788,8 +1136,10 @@
<p><b>-t </b><em>devicetype</em></p>
<p>
- 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.
+ 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.
</p>
@@ -810,14 +1160,13 @@
&quot;Joe's Stereo&quot; ( <b>-a &quot;Joe's Stereo&quot;</b> ) and will use the SoX Resampler
Library-based stuffing ( <b>-S soxr</b> ).
The audio backend options following the <b>--</b> separator specify
- that the audio will be output on output 0 of soundcard 1 (
- <b>-d hw:1,0</b> ) and will take advantage of the same sound card's mixer ( <b>-m hw:1</b> )
- using the level control named &quot;PCM&quot; ( <b>-c &quot;PCM&quot;</b> ).
+ that the audio will be output on output 0 of soundcard 1
+ ( <b>-d hw:1,0</b> ) and will take advantage of the same sound card's mixer
+ ( <b>-m hw:1</b> ) using the level control named &quot;PCM&quot; ( <b>-c &quot;PCM&quot;</b> ).
</p>
- <p>The example above is slightly contrived in order to show the use of the <b>-m</b> option.
- Typically, output 0 is the default output of a card,
- so the output device could be written <b>-d hw:1</b> and
- then the mixer option would be unnecessary, giving the following, simpler, command:</p>
+ <p>The example above is slightly contrived in order to show the use of the <b>-m</b>
+ option. Typically, output 0 is the default output of a card, so the output device could
+ be written <b>-d hw:1</b> and then the mixer option would be unnecessary, giving the following, simpler, command:</p>
shairport-sync <b>-d</b>
<b>-a &quot;Joe's Stereo&quot;</b>
<b>-S soxr</b>
@@ -833,14 +1182,16 @@
<h2>Credits</h2>
<p>Mike Brady developed shairport-sync from the original Shairport by James Laird.</p>
- <p>shairport-sync can be found at <a href = "https://github.com/mikebrady/shairport-sync.">https://github.com/mikebrady/shairport-sync.</a></p>
- <p>Shairport can be found at <a href = "https://github.com/abrasive/shairport.">https://github.com/abrasive/shairport.</a></p>
+ <p>shairport-sync can be found at
+ <a href = "https://github.com/mikebrady/shairport-sync.">https://github.com/mikebrady/shairport-sync.</a></p>
+ <p>Shairport can be found at
+ <a href = "https://github.com/abrasive/shairport.">https://github.com/abrasive/shairport.</a></p>
<h2>Comments</h2>
- <p>This man page was written using <a href="http://masqmail.cx/xml2man/">xml2man</a> by Oliver Kurth.</p>
+ <p>This man page was written using <a href="http://masqmail.cx/xml2man/">xml2man</a> by Oliver Kurth.</p>
diff --git a/mdns.c b/mdns.c
index b8d870c..0e305c0 100644
--- a/mdns.c
+++ b/mdns.c
@@ -34,28 +34,33 @@
#ifdef CONFIG_AVAHI
extern mdns_backend mdns_avahi;
-extern mdns_backend mdns_external_avahi;
#endif
-#ifdef CONFIG_HAVE_DNS_SD_H
+#ifdef CONFIG_DNS_SD
extern mdns_backend mdns_dns_sd;
-extern mdns_backend mdns_external_dns_sd;
#endif
#ifdef CONFIG_TINYSVCMDNS
extern mdns_backend mdns_tinysvcmdns;
#endif
+#ifdef CONFIG_EXTERNAL_MDNS
+extern mdns_backend mdns_external_avahi;
+extern mdns_backend mdns_external_dns_sd;
+#endif
+
static mdns_backend *mdns_backends[] = {
#ifdef CONFIG_AVAHI
&mdns_avahi,
- &mdns_external_avahi,
#endif
-#ifdef CONFIG_HAVE_DNS_SD_H
+#ifdef CONFIG_DNS_SD
&mdns_dns_sd,
- &mdns_external_dns_sd,
#endif
#ifdef CONFIG_TINYSVCMDNS
&mdns_tinysvcmdns,
#endif
+#ifdef CONFIG_EXTERNAL_MDNS
+ &mdns_external_avahi,
+ &mdns_external_dns_sd,
+#endif
NULL};
void mdns_register(void) {
@@ -96,34 +101,38 @@ void mdns_register(void) {
if (config.mdns == NULL)
die("Could not establish mDNS advertisement!");
+
+ mdns_dacp_monitor_start(); // create a dacp monitor thread
}
void mdns_unregister(void) {
+ mdns_dacp_monitor_stop();
if (config.mdns) {
config.mdns->mdns_unregister();
}
}
-void *mdns_dacp_monitor(char *dacp_id) {
- void *reply = NULL;
- if ((dacp_id != NULL) && (*dacp_id != '\0')) {
- if ((config.mdns) && (config.mdns->mdns_dacp_monitor)) {
- reply = config.mdns->mdns_dacp_monitor(dacp_id);
- if (reply == NULL) {
- debug(1, "Error starting a DACP monitor.");
- }
- } else
- debug(3, "Can't start a DACP monitor -- none registered.");
- }
- return reply;
+void mdns_dacp_monitor_start(void) {
+ if ((config.mdns) && (config.mdns->mdns_dacp_monitor_start)) {
+ config.mdns->mdns_dacp_monitor_start();
+ } else
+ debug(3, "Can't start a DACP monitor -- no mdns_dacp_monitor start registered.");
}
-void mdns_dacp_dont_monitor(void *userdata) {
- if ((config.mdns) && (config.mdns->mdns_dacp_dont_monitor)) {
- config.mdns->mdns_dacp_dont_monitor(userdata);
+void mdns_dacp_monitor_stop() {
+ if ((config.mdns) && (config.mdns->mdns_dacp_monitor_stop)) {
+ config.mdns->mdns_dacp_monitor_stop();
} else
- debug(3, "Can't stop a DACP monitor -- none registered.");
+ debug(3, "Can't stop a DACP monitor -- no mdns_dacp_monitor_stop registered.");
}
+
+void mdns_dacp_monitor_set_id(const char *dacp_id) {
+ if ((config.mdns) && (config.mdns->mdns_dacp_monitor_set_id)) {
+ config.mdns->mdns_dacp_monitor_set_id(dacp_id);
+ } else
+ debug(3, "Can't set dacp_id -- no mdns_dacp_set_id registered.");
+}
+
void mdns_ls_backends(void) {
mdns_backend **b = NULL;
printf("Available mDNS backends: \n");
diff --git a/mdns.h b/mdns.h
index 9f96a9f..8dc919f 100644
--- a/mdns.h
+++ b/mdns.h
@@ -9,8 +9,9 @@ extern int mdns_pid;
void mdns_unregister(void);
void mdns_register(void);
-void *mdns_dacp_monitor(char *dacp_id);
-void mdns_dacp_dont_monitor(void *userdata);
+void mdns_dacp_monitor_start();
+void mdns_dacp_monitor_stop(void);
+void mdns_dacp_monitor_set_id(const char *dacp_id);
void mdns_ls_backends(void);
@@ -18,8 +19,9 @@ typedef struct {
char *name;
int (*mdns_register)(char *apname, int port);
void (*mdns_unregister)(void);
- void *(*mdns_dacp_monitor)(char *);
- void (*mdns_dacp_dont_monitor)(void *);
+ void (*mdns_dacp_monitor_start)();
+ void (*mdns_dacp_monitor_set_id)(const char *);
+ void (*mdns_dacp_monitor_stop)();
} mdns_backend;
#ifdef CONFIG_METADATA
diff --git a/mdns_avahi.c b/mdns_avahi.c
index 28aa9ec..2d5cf56 100644
--- a/mdns_avahi.c
+++ b/mdns_avahi.c
@@ -33,7 +33,7 @@
#include "common.h"
#include "mdns.h"
#include "rtsp.h"
-#ifdef HAVE_DACP_CLIENT
+#ifdef CONFIG_DACP_CLIENT
#include "dacp.h"
#endif
#include <string.h>
@@ -61,6 +61,8 @@ typedef struct {
char *dacp_id;
} dacp_browser_struct;
+dacp_browser_struct private_dbs;
+
// static AvahiServiceBrowser *sb = NULL;
static AvahiClient *client = NULL;
// static AvahiClient *service_client = NULL;
@@ -84,33 +86,35 @@ static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIn
/* Called whenever a service has been resolved successfully or timed out */
switch (event) {
- case AVAHI_RESOLVER_FAILURE:
- debug(2, "(Resolver) Failed to resolve service '%s' of type '%s' in domain '%s': %s.", name,
- type, domain, avahi_strerror(avahi_client_errno(avahi_service_resolver_get_client(r))));
- break;
- case AVAHI_RESOLVER_FOUND: {
- // char a[AVAHI_ADDRESS_STR_MAX], *t;
- // debug(1, "Resolve callback: Service '%s' of type '%s' in domain '%s':", name, type, domain);
+ case AVAHI_RESOLVER_FAILURE:
+ debug(2, "(Resolver) Failed to resolve service '%s' of type '%s' in domain '%s': %s.", name,
+ type, domain, avahi_strerror(avahi_client_errno(avahi_service_resolver_get_client(r))));
+ break;
+ case AVAHI_RESOLVER_FOUND: {
+ // char a[AVAHI_ADDRESS_STR_MAX], *t;
+ debug(3, "Resolve callback: Service '%s' of type '%s' in domain '%s':", name, type, domain);
+ if (dbs->dacp_id) {
char *dacpid = strstr(name, "iTunes_Ctrl_");
if (dacpid) {
dacpid += strlen("iTunes_Ctrl_");
if (strcmp(dacpid, dbs->dacp_id) == 0) {
- debug(3, "Client's DACP port: %u.", port);
- #ifdef HAVE_DACP_CLIENT
+ debug(3, "Client \"%s\"'s DACP port: %u.", dbs->dacp_id, port);
+#ifdef CONFIG_DACP_CLIENT
dacp_monitor_port_update_callback(dacpid, port);
- #endif
- #ifdef CONFIG_METADATA
+#endif
+#ifdef CONFIG_METADATA
char portstring[20];
memset(portstring, 0, sizeof(portstring));
snprintf(portstring, sizeof(portstring), "%u", port);
send_ssnc_metadata('dapo', strdup(portstring), strlen(portstring), 0);
- #endif
+#endif
}
} else {
debug(1, "Resolve callback: Can't see a DACP string in a DACP Record!");
}
}
}
+ }
// debug(1,"service resolver freed by resolve_callback");
check_avahi_response(1, avahi_service_resolver_free(r));
}
@@ -128,7 +132,7 @@ static void browse_callback(AvahiServiceBrowser *b, AvahiIfIndex interface, Avah
avahi_threaded_poll_quit(tpoll);
break;
case AVAHI_BROWSER_NEW:
- debug(3, "(Browser) NEW: service '%s' of type '%s' in domain '%s'.", name, type, domain);
+ // debug(1, "(Browser) NEW: service '%s' of type '%s' in domain '%s'.", name, type, domain);
/* We ignore the returned resolver object. In the callback
function we free it. If the server is terminated before
the callback function is called the server will free
@@ -140,11 +144,11 @@ static void browse_callback(AvahiServiceBrowser *b, AvahiIfIndex interface, Avah
break;
case AVAHI_BROWSER_REMOVE:
debug(3, "(Browser) REMOVE: service '%s' of type '%s' in domain '%s'.", name, type, domain);
-#ifdef HAVE_DACP_CLIENT
+#ifdef CONFIG_DACP_CLIENT
char *dacpid = strstr(name, "iTunes_Ctrl_");
if (dacpid) {
dacpid += strlen("iTunes_Ctrl_");
- if (strcmp(dacpid, dbs->dacp_id) == 0)
+ if ((dbs->dacp_id) && (strcmp(dacpid, dbs->dacp_id) == 0))
dacp_monitor_port_update_callback(dbs->dacp_id, 0); // say the port is withdrawn
} else {
debug(1, "Browse callback: Can't see a DACP string in a DACP Record!");
@@ -164,47 +168,47 @@ static void register_service(AvahiClient *c);
static void egroup_callback(AvahiEntryGroup *g, AvahiEntryGroupState state,
AVAHI_GCC_UNUSED void *userdata) {
switch (state) {
- case AVAHI_ENTRY_GROUP_ESTABLISHED:
- /* The entry group has been established successfully */
- debug(1, "avahi: service '%s' successfully added.", service_name);
- break;
-
- case AVAHI_ENTRY_GROUP_COLLISION: {
- char *n;
-
- /* A service name collision with a remote service
- * happened. Let's pick a new name */
- debug(1, "avahi name collision -- look for another");
- n = avahi_alternative_service_name(service_name);
- if (service_name)
- avahi_free(service_name);
- else
- debug(1, "avahi attempt to free a NULL service name");
- service_name = n;
+ case AVAHI_ENTRY_GROUP_ESTABLISHED:
+ /* The entry group has been established successfully */
+ debug(1, "avahi: service '%s' successfully added.", service_name);
+ break;
- debug(2, "avahi: service name collision, renaming service to '%s'", service_name);
+ case AVAHI_ENTRY_GROUP_COLLISION: {
+ char *n;
- /* And recreate the services */
- register_service(avahi_entry_group_get_client(g));
- break;
- }
+ /* A service name collision with a remote service
+ * happened. Let's pick a new name */
+ debug(1, "avahi name collision -- look for another");
+ n = avahi_alternative_service_name(service_name);
+ if (service_name)
+ avahi_free(service_name);
+ else
+ debug(1, "avahi attempt to free a NULL service name");
+ service_name = n;
+
+ debug(2, "avahi: service name collision, renaming service to '%s'", service_name);
- case AVAHI_ENTRY_GROUP_FAILURE:
- debug(1, "avahi: entry group failure: %s",
- avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))));
- break;
+ /* And recreate the services */
+ register_service(avahi_entry_group_get_client(g));
+ break;
+ }
+
+ case AVAHI_ENTRY_GROUP_FAILURE:
+ debug(1, "avahi: entry group failure: %s",
+ avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))));
+ break;
- case AVAHI_ENTRY_GROUP_UNCOMMITED:
- debug(2, "avahi: service '%s' group is not yet committed.", service_name);
- break;
+ case AVAHI_ENTRY_GROUP_UNCOMMITED:
+ debug(2, "avahi: service '%s' group is not yet committed.", service_name);
+ break;
- case AVAHI_ENTRY_GROUP_REGISTERING:
- debug(2, "avahi: service '%s' group is registering.", service_name);
- break;
+ case AVAHI_ENTRY_GROUP_REGISTERING:
+ debug(2, "avahi: service '%s' group is registering.", service_name);
+ break;
- default:
- debug(1, "avahi: unhandled egroup state: %d", state);
- break;
+ default:
+ debug(1, "avahi: unhandled egroup state: %d", state);
+ break;
}
}
@@ -385,45 +389,40 @@ static int avahi_register(char *srvname, int srvport) {
static void avahi_unregister(void) {
debug(1, "avahi: avahi_unregister.");
if (tpoll) {
+ debug(1, "avahi: stop the threaded poll.");
avahi_threaded_poll_stop(tpoll);
if (client) {
+ debug(1, "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.");
avahi_threaded_poll_free(tpoll);
tpoll = NULL;
+ } else {
+ debug(1, "No avahi threaded poll.");
}
- if (service_name)
+ if (service_name) {
+ debug(1, "avahi: free the service name.");
free(service_name);
- else
+ } else
debug(1, "avahi attempt to free NULL service name");
service_name = NULL;
}
-void *avahi_dacp_monitor(char *dacp_id) {
- dacp_browser_struct *dbs = (dacp_browser_struct *)malloc(sizeof(dacp_browser_struct));
-
- if (dbs == NULL)
- die("can not allocate a dacp_browser_struct.");
-
- char *t = strdup(dacp_id);
- if (t)
- dbs->dacp_id = t;
- else {
- die("can not allocate a dacp_id string in dacp_browser_struct.");
- }
+void avahi_dacp_monitor_start(void) {
+ dacp_browser_struct *dbs = &private_dbs;
+ memset((void *)&private_dbs, 0, sizeof(dacp_browser_struct));
// create the threaded poll code
int err;
if (!(dbs->service_poll = avahi_threaded_poll_new())) {
warn("couldn't create avahi threaded service_poll!");
- free(dbs->dacp_id);
- free((char *)dbs);
- return NULL;
+ return;
}
// create the service client
@@ -432,66 +431,72 @@ void *avahi_dacp_monitor(char *dacp_id) {
service_client_callback, (void *)dbs, &err))) {
warn("couldn't create avahi service client: %s!", avahi_strerror(err));
avahi_threaded_poll_free(dbs->service_poll);
- free(dbs->dacp_id);
- free((char *)dbs);
- return NULL;
+ return;
}
- /* Create the service browser */
- if (!(dbs->service_browser =
- avahi_service_browser_new(dbs->service_client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
- "_dacp._tcp", NULL, 0, browse_callback, (void *)dbs))) {
- warn("failed to create avahi service browser: %s\n",
- avahi_strerror(avahi_client_errno(dbs->service_client)));
- avahi_client_free(dbs->service_client);
- avahi_threaded_poll_free(dbs->service_poll);
- free(dbs->dacp_id);
- free((char *)dbs);
- return NULL;
- }
// start the polling thread
if (avahi_threaded_poll_start(dbs->service_poll) < 0) {
warn("couldn't start avahi service_poll thread");
avahi_service_browser_free(dbs->service_browser);
avahi_client_free(dbs->service_client);
avahi_threaded_poll_free(dbs->service_poll);
- free(dbs->dacp_id);
- free((char *)dbs);
- return NULL;
+ return;
}
debug(3, "Avahi DACP monitor successfully started");
- return (void *)dbs;
+ return;
}
-void avahi_dacp_dont_monitor(void *userdata) {
- debug(3,"avahi_dacp_dont_monitor");
- if (userdata) {
- dacp_browser_struct *dbs = (dacp_browser_struct *)userdata;
- // stop and dispose of everything
- /*if (dbs->service_poll)
- avahi_threaded_poll_stop((dbs)->service_poll);
- */
- if (dbs->service_poll) {
- avahi_threaded_poll_stop(dbs->service_poll);
+void avahi_dacp_monitor_set_id(const char *dacp_id) {
+ debug(3, "avahi_dacp_dont_monitor");
+ dacp_browser_struct *dbs = &private_dbs;
+
+ if (dbs->dacp_id)
+ free(dbs->dacp_id);
+ if (dacp_id == NULL)
+ dbs->dacp_id = NULL;
+ else {
+ char *t = strdup(dacp_id);
+ if (t) {
+ dbs->dacp_id = t;
avahi_threaded_poll_lock(dbs->service_poll);
if (dbs->service_browser)
avahi_service_browser_free(dbs->service_browser);
- if (dbs->service_client)
- avahi_client_free(dbs->service_client);
- avahi_threaded_poll_unlock(dbs->service_poll);
- avahi_threaded_poll_free(dbs->service_poll);
+
+ if (!(dbs->service_browser =
+ avahi_service_browser_new(dbs->service_client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
+ "_dacp._tcp", NULL, 0, browse_callback, (void *)dbs))) {
+ warn("failed to create avahi service browser: %s\n",
+ avahi_strerror(avahi_client_errno(dbs->service_client)));
+ }
+ avahi_threaded_poll_unlock(dbs->service_poll);
+ } else {
+ warn("avahi_dacp_set_id: can not allocate a dacp_id string in dacp_browser_struct.");
}
- free(dbs->dacp_id);
- free(userdata);
- debug(3, "Avahi DACP monitor successfully stopped");
- } else {
- debug(1, "Avahi DACP Monitor is not running.");
+ debug(3, "Search for DACP ID \"%s\".", t);
+ }
+}
+
+void avahi_dacp_monitor_stop() {
+ debug(3, "avahi_dacp_dont_monitor");
+ dacp_browser_struct *dbs = &private_dbs;
+ // stop and dispose of everything
+ if (dbs->service_poll) {
+ avahi_threaded_poll_stop(dbs->service_poll);
+ avahi_threaded_poll_lock(dbs->service_poll);
+ if (dbs->service_browser)
+ avahi_service_browser_free(dbs->service_browser);
+ if (dbs->service_client)
+ avahi_client_free(dbs->service_client);
+ avahi_threaded_poll_unlock(dbs->service_poll);
+ avahi_threaded_poll_free(dbs->service_poll);
}
- debug(3,"avahi_dacp_dont_monitor exit");
+ free(dbs->dacp_id);
+ debug(3, "Avahi DACP monitor successfully stopped");
}
mdns_backend mdns_avahi = {.name = "avahi",
.mdns_register = avahi_register,
.mdns_unregister = avahi_unregister,
- .mdns_dacp_monitor = avahi_dacp_monitor,
- .mdns_dacp_dont_monitor = avahi_dacp_dont_monitor};
+ .mdns_dacp_monitor_start = avahi_dacp_monitor_start,
+ .mdns_dacp_monitor_set_id = avahi_dacp_monitor_set_id,
+ .mdns_dacp_monitor_stop = avahi_dacp_monitor_stop};
diff --git a/mdns_dns_sd.c b/mdns_dns_sd.c
index 722aadc..cf178f1 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 <arpa/inet.h>
#include <dns_sd.h>
#include <stdlib.h>
@@ -93,5 +93,6 @@ static void mdns_dns_sd_unregister(void) {
mdns_backend mdns_dns_sd = {.name = "dns-sd",
.mdns_register = mdns_dns_sd_register,
.mdns_unregister = mdns_dns_sd_unregister,
- .mdns_dacp_monitor = NULL,
- .mdns_dacp_dont_monitor = NULL};
+ .mdns_dacp_monitor_start = NULL,
+ .mdns_dacp_monitor_set_id = NULL,
+ .mdns_dacp_monitor_stop = NULL};
diff --git a/mdns_external.c b/mdns_external.c
index e3a241f..2ae23c6 100644
--- a/mdns_external.c
+++ b/mdns_external.c
@@ -1,6 +1,7 @@
/*
* mDNS registration handler. This file is part of Shairport.
* Copyright (c) Paul Lietar 2013
+ * Amendments and updates copyright (c) Mike Brady 2014 -- 2019
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -41,46 +42,42 @@ int mdns_pid = 0;
*/
static int fork_execvp(const char *file, char *const argv[]) {
int execpipe[2];
- int pid = 0;
- if (pipe(execpipe) < 0) {
- return -1;
- }
-
- if (fcntl(execpipe[1], F_SETFD, fcntl(execpipe[1], F_GETFD) | FD_CLOEXEC) < 0) {
- close(execpipe[0]);
- close(execpipe[1]);
- return -1;
- }
-
- pid = fork();
- if (pid < 0) {
- close(execpipe[0]);
- close(execpipe[1]);
- return -1;
- } else if (pid == 0) { // Child
- close(execpipe[0]); // Close the read end
- execvp(file, argv);
-
- // If we reach this point then execve has failed.
- // Write erno's value into the pipe and exit.
- if (write(execpipe[1], &errno, sizeof(errno)) != sizeof(errno))
- debug(1, "Execve has failed and there was a further error writing an error message, duh.");
- debug(1, "execve has failed.");
- _exit(-1);
- return 0; // Just to make the compiler happy.
- } else { // Parent
- close(execpipe[1]); // Close the write end
-
- int childErrno;
- // Block until child closes the pipe or sends errno.
- if (read(execpipe[0], &childErrno, sizeof(childErrno)) ==
- sizeof(childErrno)) { // We received errno
- errno = childErrno;
- return -1;
- } else { // Child closed the pipe. execvp was successful.
- return pid;
+ int response = -1;
+ if (pipe(execpipe) >= 0) {
+
+ if (fcntl(execpipe[1], F_SETFD, fcntl(execpipe[1], F_GETFD) | FD_CLOEXEC) < 0) {
+ close(execpipe[0]);
+ close(execpipe[1]);
+ } else {
+
+ int pid = fork();
+ if (pid < 0) {
+ close(execpipe[0]);
+ close(execpipe[1]);
+ } else if (pid == 0) { // Child
+ close(execpipe[0]); // Close the read end
+ execvp(file, argv);
+ // If we reach this point then execve has failed.
+ // Write erno's value into the pipe and exit.
+ if (write(execpipe[1], &errno, sizeof(errno)) != sizeof(errno))
+ debug(1,
+ "Execve has failed and there was a further error writing an error message, duh.");
+ die("mdns_external: execve has failed.");
+ } else { // Parent
+ close(execpipe[1]); // Close the write end
+
+ int childErrno;
+ // Block until child closes the pipe or sends errno.
+ if (read(execpipe[0], &childErrno, sizeof(childErrno)) ==
+ sizeof(childErrno)) { // We received errno
+ errno = childErrno;
+ } else {
+ response = pid;
+ }
+ }
}
}
+ return response;
}
static int mdns_external_avahi_register(char *apname, __attribute__((unused)) int port) {
@@ -162,11 +159,13 @@ static void kill_mdns_child(void) {
mdns_backend mdns_external_avahi = {.name = "external-avahi",
.mdns_register = mdns_external_avahi_register,
.mdns_unregister = kill_mdns_child,
- .mdns_dacp_monitor = NULL,
- .mdns_dacp_dont_monitor = NULL};
+ .mdns_dacp_monitor_start = NULL,
+ .mdns_dacp_monitor_set_id = NULL,
+ .mdns_dacp_monitor_stop = NULL};
mdns_backend mdns_external_dns_sd = {.name = "external-dns-sd",
.mdns_register = mdns_external_dns_sd_register,
.mdns_unregister = kill_mdns_child,
- .mdns_dacp_monitor = NULL,
- .mdns_dacp_dont_monitor = NULL};
+ .mdns_dacp_monitor_start = NULL,
+ .mdns_dacp_monitor_set_id = NULL,
+ .mdns_dacp_monitor_stop = NULL};
diff --git a/mdns_tinysvcmdns.c b/mdns_tinysvcmdns.c
index 238b68b..ad6be65 100644
--- a/mdns_tinysvcmdns.c
+++ b/mdns_tinysvcmdns.c
@@ -164,5 +164,6 @@ static void mdns_tinysvcmdns_unregister(void) {
mdns_backend mdns_tinysvcmdns = {.name = "tinysvcmdns",
.mdns_register = mdns_tinysvcmdns_register,
.mdns_unregister = mdns_tinysvcmdns_unregister,
- .mdns_dacp_monitor = NULL,
- .mdns_dacp_dont_monitor = NULL};
+ .mdns_dacp_monitor_start = NULL,
+ .mdns_dacp_monitor_set_id = NULL,
+ .mdns_dacp_monitor_stop = NULL};
diff --git a/metadata_hub.c b/metadata_hub.c
index 9d648fc..4d7def3 100644
--- a/metadata_hub.c
+++ b/metadata_hub.c
@@ -45,18 +45,21 @@
#include "dacp.h"
#include "metadata_hub.h"
-#ifdef HAVE_LIBMBEDTLS
+#ifdef CONFIG_MBEDTLS
+#include <mbedtls/version.h>
#include <mbedtls/md5.h>
#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
#include <polarssl/md5.h>
#endif
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
#include <openssl/md5.h>
#endif
+int metadata_hub_initialised = 0;
+
pthread_rwlock_t metadata_hub_re_lock = PTHREAD_RWLOCK_INITIALIZER;
struct track_metadata_bundle *track_metadata; // used for a temporary track metadata store
@@ -90,10 +93,31 @@ void metadata_hub_release_track_metadata(struct track_metadata_bundle *track_met
}
}
+void metadata_hub_release_track_artwork(void) {
+ // debug(1,"release track artwork");
+ release_char_string(&metadata_store.cover_art_pathname);
+}
+
void metadata_hub_init(void) {
// debug(1, "Metadata bundle initialisation.");
memset(&metadata_store, 0, sizeof(metadata_store));
track_metadata = NULL;
+ metadata_hub_initialised = 1;
+}
+
+void metadata_hub_stop(void) {
+ if (metadata_hub_initialised) {
+ debug(2, "metadata_hub_stop.");
+ metadata_hub_release_track_artwork();
+ if (metadata_store.track_metadata) {
+ metadata_hub_release_track_metadata(metadata_store.track_metadata);
+ metadata_store.track_metadata = NULL;
+ }
+ if (track_metadata) {
+ metadata_hub_release_track_metadata(track_metadata);
+ track_metadata = NULL;
+ }
+ }
}
void add_metadata_watcher(metadata_watcher fn, void *userdata) {
@@ -108,32 +132,34 @@ void add_metadata_watcher(metadata_watcher fn, void *userdata) {
}
}
-void metadata_hub_modify_prolog(void) {
- // 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.");
- pthread_rwlock_wrlock(&metadata_hub_re_lock);
- debug(2, "Okay -- acquired the metadata_hub write lock.");
- }
-}
-
-void metadata_hub_release_track_artwork(void) {
- // debug(1,"release track artwork");
- release_char_string(&metadata_store.cover_art_pathname);
+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;
// debug(1, "locking metadata hub for reading");
pthread_rwlock_rdlock(&metadata_hub_re_lock);
+ pthread_cleanup_push(metadata_hub_unlock_hub_mutex_cleanup, NULL);
for (i = 0; i < number_of_watchers; i++) {
if (metadata_store.watchers[i]) {
metadata_store.watchers[i](&metadata_store, metadata_store.watchers_data[i]);
}
}
// debug(1, "unlocking metadata hub for reading");
- pthread_rwlock_unlock(&metadata_hub_re_lock);
+ // pthread_rwlock_unlock(&metadata_hub_re_lock);
+ pthread_cleanup_pop(1);
+}
+
+void metadata_hub_modify_prolog(void) {
+ // 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.");
+ pthread_rwlock_wrlock(&metadata_hub_re_lock);
+ debug(2, "Okay -- acquired the metadata_hub write lock.");
+ }
}
void metadata_hub_modify_epilog(int modified) {
@@ -150,7 +176,7 @@ void metadata_hub_modify_epilog(int modified) {
if ((metadata_store.dacp_server_active == 0) &&
(metadata_store.dacp_server_has_been_active != 0)) {
- debug(1, "dacp_scanner going inactive -- release track metadata and artwork");
+ debug(2, "dacp_scanner going inactive -- release track metadata and artwork");
if (metadata_store.track_metadata) {
m = 1;
metadata_hub_release_track_metadata(metadata_store.track_metadata);
@@ -197,23 +223,30 @@ char *metadata_write_image_file(const char *buf, int len) {
char *path = NULL; // this will be what is returned
uint8_t img_md5[16];
-// uint8_t ap_md5[16];
+ // uint8_t ap_md5[16];
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
MD5_CTX ctx;
MD5_Init(&ctx);
MD5_Update(&ctx, buf, len);
MD5_Final(img_md5, &ctx);
#endif
-#ifdef HAVE_LIBMBEDTLS
- mbedtls_md5_context tctx;
- mbedtls_md5_starts(&tctx);
- mbedtls_md5_update(&tctx, (const unsigned char *)buf, len);
- mbedtls_md5_finish(&tctx, img_md5);
+#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
+ 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
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
md5_context tctx;
md5_starts(&tctx);
md5_update(&tctx, (const unsigned char *)buf, len);
@@ -469,27 +502,15 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
track_metadata = (struct track_metadata_bundle *)malloc(sizeof(struct track_metadata_bundle));
if (track_metadata == NULL)
die("Could not allocate memory for track metadata.");
- memset(track_metadata, 0, sizeof(struct track_metadata_bundle)); // now we have a valid track
- // metadata space, but the
- // metadata itself
- // might turin out to be invalid. Specifically, YouTube on iOS can generate a sequence of
- // track metadata that is invalid.
- // it is distinguished by having an item_id of zero.
+ memset(track_metadata, 0, sizeof(struct track_metadata_bundle));
break;
case 'mden':
if (track_metadata) {
- if ((track_metadata->item_id_received == 0) || (track_metadata->item_id != 0)) {
- // i.e. it's only invalid if it has definitely been given an item_id of zero
- metadata_hub_modify_prolog();
- metadata_hub_release_track_metadata(metadata_store.track_metadata);
- metadata_store.track_metadata = track_metadata;
- track_metadata = NULL;
- metadata_hub_modify_epilog(1);
- } else {
- debug(1, "The track information received is invalid -- dropping it");
- metadata_hub_release_track_metadata(track_metadata);
- track_metadata = NULL;
- }
+ metadata_hub_modify_prolog();
+ metadata_hub_release_track_metadata(metadata_store.track_metadata);
+ metadata_store.track_metadata = track_metadata;
+ track_metadata = NULL;
+ metadata_hub_modify_epilog(1);
}
debug(2, "MH Metadata stream processing end.");
break;
@@ -539,6 +560,18 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
break;
// these could tell us about play / pause etc. but will only occur if metadata is enabled, so
// we'll just ignore them
+ case 'abeg': {
+ metadata_hub_modify_prolog();
+ int 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();
+ int 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();
int changed = (metadata_store.player_state != PS_PLAYING);
diff --git a/metadata_hub.h b/metadata_hub.h
index 66368f9..784f662 100644
--- a/metadata_hub.h
+++ b/metadata_hub.h
@@ -12,6 +12,11 @@ enum play_status_type {
PS_PLAYING,
} play_status_type;
+enum active_state_type {
+ AM_INACTIVE = 0,
+ AM_ACTIVE,
+} active_state_type;
+
enum shuffle_status_type {
SS_NOT_AVAILABLE = 0,
SS_OFF,
@@ -56,7 +61,7 @@ typedef struct metadata_bundle {
char *client_ip; // IP number used by the audio source (i.e. the "client"), which is also the DACP
// server
char *server_ip; // IP number used by Shairport Sync
- char *progress_string; // progress string, emitted by the source from time to time
+ char *progress_string; // progress string, emitted by the source from time to time
int player_thread_active; // true if a play thread is running
int dacp_server_active; // true if there's a reachable DACP server (assumed to be the Airplay
// client) ; false otherwise
@@ -77,6 +82,7 @@ typedef struct metadata_bundle {
enum play_status_type
player_state; // this is the state of the actual player itself, which can be a bit noisy.
+ enum active_state_type active_state;
int speaker_volume; // this is the actual speaker volume, allowing for the main volume and the
// speaker volume control
@@ -92,6 +98,7 @@ struct metadata_bundle metadata_store;
void add_metadata_watcher(metadata_watcher fn, void *userdata);
void metadata_hub_init(void);
+void metadata_hub_stop(void);
void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t length);
void metadata_hub_reset_track_metadata(void);
void metadata_hub_release_track_artwork(void);
diff --git a/mpris-service.c b/mpris-service.c
index 3d6cf96..668d776 100644
--- a/mpris-service.c
+++ b/mpris-service.c
@@ -123,7 +123,8 @@ void mpris_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)
} else if ((argc->track_metadata) && (argc->track_metadata->item_id)) {
char trackidstring[128];
// debug(1, "Set ID using mper ID: \"%u\".",argc->item_id);
- snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/mper_%u", argc->track_metadata->item_id);
+ snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/mper_%u",
+ argc->track_metadata->item_id);
GVariant *trackid = g_variant_new("o", trackidstring);
g_variant_builder_add(dict_builder, "{sv}", "mpris:trackid", trackid);
}
@@ -182,6 +183,14 @@ void mpris_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)
// media_player2_player_set_volume(mprisPlayerPlayerSkeleton, metadata_store.speaker_volume);
}
+static gboolean on_handle_quit(MediaPlayer2 *skeleton, GDBusMethodInvocation *invocation,
+ __attribute__((unused)) gpointer user_data) {
+ debug(1, "quit requested (MPRIS interface).");
+ pthread_cancel(main_thread_id);
+ media_player2_complete_quit(skeleton, invocation);
+ return TRUE;
+}
+
static gboolean on_handle_next(MediaPlayer2Player *skeleton, GDBusMethodInvocation *invocation,
__attribute__((unused)) gpointer user_data) {
send_simple_dacp_command("nextitem");
@@ -242,7 +251,7 @@ static void on_mpris_name_acquired(GDBusConnection *connection, const gchar *nam
media_player2_set_desktop_entry(mprisPlayerSkeleton, "shairport-sync");
media_player2_set_identity(mprisPlayerSkeleton, "Shairport Sync");
- media_player2_set_can_quit(mprisPlayerSkeleton, FALSE);
+ media_player2_set_can_quit(mprisPlayerSkeleton, TRUE);
media_player2_set_can_raise(mprisPlayerSkeleton, FALSE);
media_player2_set_has_track_list(mprisPlayerSkeleton, FALSE);
media_player2_set_supported_uri_schemes(mprisPlayerSkeleton, empty_string_array);
@@ -260,6 +269,8 @@ static void on_mpris_name_acquired(GDBusConnection *connection, const gchar *nam
media_player2_player_set_can_seek(mprisPlayerPlayerSkeleton, FALSE);
media_player2_player_set_can_control(mprisPlayerPlayerSkeleton, TRUE);
+ g_signal_connect(mprisPlayerSkeleton, "handle-quit", G_CALLBACK(on_handle_quit), NULL);
+
g_signal_connect(mprisPlayerPlayerSkeleton, "handle-play", G_CALLBACK(on_handle_play), NULL);
g_signal_connect(mprisPlayerPlayerSkeleton, "handle-pause", G_CALLBACK(on_handle_pause), NULL);
g_signal_connect(mprisPlayerPlayerSkeleton, "handle-play-pause", G_CALLBACK(on_handle_play_pause),
@@ -291,7 +302,7 @@ static void on_mpris_name_lost(__attribute__((unused)) GDBusConnection *connecti
// name,(mpris_bus_type==G_BUS_TYPE_SESSION) ? "session" : "system");
pid_t pid = getpid();
char interface_name[256] = "";
- snprintf(interface_name, sizeof(interface_name), "org.mpris.MediaPlayer2.ShairportSync.i%d", pid);
+ snprintf(interface_name, sizeof(interface_name), "org.mpris.MediaPlayer2.ShairportSync.i%d", pid);
GBusType mpris_bus_type = G_BUS_TYPE_SYSTEM;
if (config.mpris_service_bus_type == DBT_session)
mpris_bus_type = G_BUS_TYPE_SESSION;
diff --git a/mqtt.c b/mqtt.c
new file mode 100644
index 0000000..f1726f2
--- /dev/null
+++ b/mqtt.c
@@ -0,0 +1,232 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "config.h"
+
+#include "common.h"
+#include "player.h"
+#include "rtsp.h"
+
+#include "rtp.h"
+
+#include "dacp.h"
+#include "mqtt.h"
+#include <mosquitto.h>
+
+// this holds the mosquitto client
+struct mosquitto *global_mosq = NULL;
+char *topic = NULL;
+int connected = 0;
+
+// mosquitto logging
+void _cb_log(__attribute__((unused)) struct mosquitto *mosq, __attribute__((unused)) void *userdata,
+ int level, const char *str) {
+ switch (level) {
+ case MOSQ_LOG_DEBUG:
+ debug(1, str);
+ break;
+ case MOSQ_LOG_INFO:
+ debug(2, str);
+ break;
+ case MOSQ_LOG_NOTICE:
+ debug(3, str);
+ break;
+ case MOSQ_LOG_WARNING:
+ inform(str);
+ break;
+ case MOSQ_LOG_ERR: {
+ die("MQTT: Error: %s\n", str);
+ }
+ }
+}
+
+// mosquitto message handler
+void on_message(__attribute__((unused)) struct mosquitto *mosq,
+ __attribute__((unused)) void *userdata, const struct mosquitto_message *msg) {
+
+ // null-terminate the payload
+ char payload[msg->payloadlen + 1];
+ memcpy(payload, msg->payload, msg->payloadlen);
+ payload[msg->payloadlen] = 0;
+
+ debug(1, "[MQTT]: received Message on topic %s: %s\n", msg->topic, payload);
+
+ // All recognized commands
+ char *commands[] = {"command", "beginff", "beginrew", "mutetoggle", "nextitem",
+ "previtem", "pause", "playpause", "play", "stop",
+ "playresume", "shuffle_songs", "volumedown", "volumeup", NULL};
+
+ int it = 0;
+
+ // send command if it's a valid one
+ 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]);
+ send_simple_dacp_command(commands[it]);
+ break;
+ }
+ it++;
+ }
+}
+
+void on_disconnect(__attribute__((unused)) struct mosquitto *mosq,
+ __attribute__((unused)) void *userdata, __attribute__((unused)) int rc) {
+ connected = 0;
+ debug(1, "[MQTT]: disconnected");
+}
+
+void on_connect(struct mosquitto *mosq, __attribute__((unused)) void *userdata,
+ __attribute__((unused)) int rc) {
+ connected = 1;
+ debug(1, "[MQTT]: connected");
+
+ // subscribe if requested
+ if (config.mqtt_enable_remote) {
+ char remotetopic[strlen(config.mqtt_topic) + 8];
+ snprintf(remotetopic, strlen(config.mqtt_topic) + 8, "%s/remote", config.mqtt_topic);
+ mosquitto_subscribe(mosq, NULL, remotetopic, 0);
+ }
+}
+
+// helper function to publish under a topic and automatically append the main topic
+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);
+
+ int rc;
+ if ((rc = mosquitto_publish(global_mosq, NULL, fulltopic, length, data, 0, 0)) !=
+ MOSQ_ERR_SUCCESS) {
+ switch (rc) {
+ case MOSQ_ERR_NO_CONN:
+ debug(1, "[MQTT]: Publish failed: not connected to broker");
+ break;
+ default:
+ debug(1, "[MQTT]: Publish failed: unknown error");
+ break;
+ }
+ }
+}
+
+// handler for incoming metadata
+void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t length) {
+ if (global_mosq == NULL || connected != 1) {
+ debug(3, "[MQTT]: Client not connected, skipping metadata handling");
+ return;
+ }
+ if (config.mqtt_publish_raw) {
+ uint32_t val;
+ char topic[] = "____/____";
+
+ val = htonl(type);
+ memcpy(topic, &val, 4);
+ val = htonl(code);
+ memcpy(topic + 5, &val, 4);
+ mqtt_publish(topic, data, length);
+ }
+ if (config.mqtt_publish_parsed) {
+ if (type == 'core') {
+ switch (code) {
+ case 'asar':
+ mqtt_publish("artist", data, length);
+ break;
+ case 'asal':
+ mqtt_publish("album", data, length);
+ break;
+ case 'minm':
+ mqtt_publish("title", data, length);
+ break;
+ case 'asgn':
+ mqtt_publish("genre", data, length);
+ break;
+ case 'asfm':
+ mqtt_publish("format", data, length);
+ break;
+ }
+ } else if (type == 'ssnc') {
+ switch (code) {
+ case 'asal':
+ mqtt_publish("songalbum", data, length);
+ break;
+ case 'pvol':
+ mqtt_publish("volume", data, length);
+ break;
+ case 'clip':
+ mqtt_publish("client_ip", data, length);
+ break;
+ case 'abeg':
+ mqtt_publish("active_start", data, length);
+ break;
+ case 'aend':
+ mqtt_publish("active_end", data, length);
+ break;
+ case 'pbeg':
+ mqtt_publish("play_start", data, length);
+ break;
+ case 'pend':
+ mqtt_publish("play_end", data, length);
+ break;
+ case 'pfls':
+ mqtt_publish("play_flush", data, length);
+ break;
+ case 'prsm':
+ mqtt_publish("play_resume", data, length);
+ break;
+ case 'PICT':
+ if (config.mqtt_publish_cover) {
+ mqtt_publish("cover", data, length);
+ }
+ break;
+ }
+ }
+ }
+
+ return;
+}
+
+int initialise_mqtt() {
+ debug(1, "Initialising MQTT");
+ if (config.mqtt_hostname == NULL) {
+ debug(1, "[MQTT]: Not initialized, as the hostname is not set");
+ return 0;
+ }
+ int keepalive = 60;
+ mosquitto_lib_init();
+ if (!(global_mosq = mosquitto_new(config.service_name, true, NULL))) {
+ die("[MQTT]: FATAL: Could not create mosquitto object! %d\n", global_mosq);
+ }
+
+ if (config.mqtt_cafile != NULL || config.mqtt_capath != NULL || config.mqtt_certfile != NULL ||
+ config.mqtt_keyfile != NULL) {
+ if (mosquitto_tls_set(global_mosq, config.mqtt_cafile, config.mqtt_capath, config.mqtt_certfile,
+ config.mqtt_keyfile, NULL) != MOSQ_ERR_SUCCESS) {
+ die("[MQTT]: TLS Setup failed");
+ }
+ }
+
+ if (config.mqtt_username != NULL || config.mqtt_password != NULL) {
+ if (mosquitto_username_pw_set(global_mosq, config.mqtt_username, config.mqtt_password) !=
+ MOSQ_ERR_SUCCESS) {
+ die("[MQTT]: Username/Password set failed");
+ }
+ }
+ mosquitto_log_callback_set(global_mosq, _cb_log);
+
+ if (config.mqtt_enable_remote) {
+ mosquitto_message_callback_set(global_mosq, on_message);
+ }
+
+ mosquitto_disconnect_callback_set(global_mosq, on_disconnect);
+ mosquitto_connect_callback_set(global_mosq, on_connect);
+ if (mosquitto_connect(global_mosq, config.mqtt_hostname, config.mqtt_port, keepalive)) {
+ inform("[MQTT]: Could not establish a mqtt connection");
+ }
+ if (mosquitto_loop_start(global_mosq) != MOSQ_ERR_SUCCESS) {
+ inform("[MQTT]: Could start MQTT Main loop");
+ }
+
+ return 0;
+}
diff --git a/mqtt.h b/mqtt.h
new file mode 100644
index 0000000..4bf680e
--- /dev/null
+++ b/mqtt.h
@@ -0,0 +1,14 @@
+#ifndef MQTT_H
+#define MQTT_H
+#include <mosquitto.h>
+#include <stdint.h>
+
+int initialise_mqtt();
+void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t length);
+void mqtt_publish(char *topic, char *data, uint32_t length);
+void mqtt_setup();
+void on_connect(struct mosquitto *mosq, void *userdata, int rc);
+void on_disconnect(struct mosquitto *mosq, void *userdata, int rc);
+void on_message(struct mosquitto *mosq, void *userdata, const struct mosquitto_message *msg);
+void _cb_log(struct mosquitto *mosq, void *userdata, int level, const char *str);
+#endif /* #ifndef MQTT_H */
diff --git a/org.gnome.ShairportSync.xml b/org.gnome.ShairportSync.xml
index aa693fd..cea574b 100644
--- a/org.gnome.ShairportSync.xml
+++ b/org.gnome.ShairportSync.xml
@@ -1,8 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
<interface name="org.gnome.ShairportSync">
+ <method name="Quit"/>
+ <property name='Active' type='b' access='read'/>
+ <property name="DisableStandby" type="b" access="readwrite" />
+ <property name="DisableStandbyMode" type="s" access="readwrite" />
<property name="LoudnessFilterActive" type="b" access="readwrite" />
<property name="LoudnessThreshold" type="d" access="readwrite" />
+ <property name="DriftTolerance" type="d" access="readwrite" />
<method name="RemoteCommand">
<arg name="command" type="s" direction="in" />
</method>
diff --git a/player.c b/player.c
index 3c60e24..08c9203 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 -- 2018
+ * and related work, copyright (c) Mike Brady 2014 -- 2019
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -45,21 +45,21 @@
#include "config.h"
-#ifdef HAVE_LIBMBEDTLS
+#ifdef CONFIG_MBEDTLS
#include <mbedtls/aes.h>
#include <mbedtls/havege.h>
#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
#include <polarssl/aes.h>
#include <polarssl/havege.h>
#endif
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
#include <openssl/aes.h>
#endif
-#ifdef HAVE_LIBSOXR
+#ifdef CONFIG_SOXR
#include <soxr.h>
#endif
@@ -67,98 +67,57 @@
#include <FFTConvolver/convolver.h>
#endif
-#ifdef HAVE_METADATA_HUB
+#ifdef CONFIG_METADATA_HUB
#include "metadata_hub.h"
#endif
-#ifdef HAVE_DACP_CLIENT
+#ifdef CONFIG_DACP_CLIENT
#include "dacp.h"
-#include <glib.h>
-#endif
-
-#ifdef HAVE_DBUS
-#include "dbus-interface.h"
-#include "dbus-service.h"
#endif
#include "common.h"
+#include "mdns.h"
#include "player.h"
#include "rtp.h"
#include "rtsp.h"
#include "alac.h"
-#ifdef HAVE_APPLE_ALAC
+#ifdef CONFIG_APPLE_ALAC
#include "apple_alac.h"
#endif
#include "loudness.h"
+#include "activity_monitor.h"
+
// default buffer size
// needs to be a power of 2 because of the way BUFIDX(seqno) works
//#define BUFFER_FRAMES 512
#define MAX_PACKET 2048
// DAC buffer occupancy stuff
-#define DAC_BUFFER_QUEUE_MINIMUM_LENGTH 600
+#define DAC_BUFFER_QUEUE_MINIMUM_LENGTH 2500
// static abuf_t audio_buffer[BUFFER_FRAMES];
#define BUFIDX(seqno) ((seq_t)(seqno) % BUFFER_FRAMES)
-void do_flush(int64_t timestamp, rtsp_conn_info *conn);
-
-// make timestamps and seqnos definitely monotonic
-
-// add an epoch to the timestamp. The monotonic timestamp guaranteed to start between 2^32 and 2^33
-// frames and continue up to 2^63-1 frames
-// if should never get into the negative range
-// which is about 2*10^8 * 1,000 seconds at 384,000 frames per second -- about 2 trillion seconds or
-// over 50,000 years.
-// also, it won't reach zero until then, if ever, so we can safely say that a null monotonic
-// timestamp can mean something special
-int64_t monotonic_timestamp(uint32_t timestamp, rtsp_conn_info *conn) {
- int64_t previous_value;
- int64_t return_value;
- if (conn->timestamp_epoch == 0) {
- if (timestamp > conn->maximum_timestamp_interval)
- conn->timestamp_epoch = 1;
- else
- conn->timestamp_epoch = 2;
- previous_value = conn->timestamp_epoch;
- previous_value <<= 32;
- previous_value += timestamp;
- } else {
- previous_value = conn->timestamp_epoch;
- previous_value <<= 32;
- previous_value += conn->last_timestamp;
- if (timestamp < conn->last_timestamp) {
- // the incoming timestamp is less than the last one.
- // if the difference is more than a minute, assume it's really from the next epoch
- if ((conn->last_timestamp - timestamp) > conn->maximum_timestamp_interval)
- conn->timestamp_epoch++;
- } else {
- // the incoming timestamp is greater than the last one.
- // if the difference is more than a minute, assume it's really from the previous epoch
- if ((timestamp - conn->last_timestamp) > conn->maximum_timestamp_interval)
- conn->timestamp_epoch--;
- }
- }
- return_value = conn->timestamp_epoch;
- return_value <<= 32;
- return_value += timestamp;
- if (previous_value > return_value) {
- if ((previous_value - return_value) > conn->maximum_timestamp_interval)
- debug(2, "interval between successive rtptimes greater than allowed!");
- } else {
- if ((return_value - previous_value) > conn->maximum_timestamp_interval)
- debug(2, "interval between successive rtptimes greater than allowed!");
- }
- if (return_value < 0)
- debug(1, "monotonic rtptime is negative!");
- conn->last_timestamp = timestamp;
- return return_value;
+uint32_t modulo_32_offset(uint32_t from, uint32_t to) {
+ if (from <= to)
+ return to - from;
+ else
+ return UINT32_MAX - from + to + 1;
+}
+
+uint64_t modulo_64_offset(uint64_t from, uint64_t to) {
+ if (from <= to)
+ return to - from;
+ else
+ return UINT64_MAX - from + to + 1;
}
+void do_flush(uint32_t timestamp, rtsp_conn_info *conn);
+
static void ab_resync(rtsp_conn_info *conn) {
int i;
for (i = 0; i < BUFFER_FRAMES; i++) {
@@ -171,11 +130,37 @@ static void ab_resync(rtsp_conn_info *conn) {
conn->ab_buffering = 1;
}
-// the sequence number is a 16-bit unsigned number which wraps pretty often
-// to work out if one seqno is 'after' another therefore depends whether wrap has occurred
-// this function works out the actual ordinate of the seqno, i.e. the distance up from
-// the zeroth element, at ab_read, taking due account of wrap.
+// 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)) {
+ 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;
@@ -183,12 +168,7 @@ static inline seq_t SUCCESSOR(seq_t x) {
return p;
}
-static inline seq_t PREDECESSOR(seq_t x) {
- uint32_t p = (x & 0xffff) + 0x10000;
- p -= 1;
- p = p & 0xffff;
- return p;
-}
+// used in seq_diff and seq_order
// anything with ORDINATE in it must be proctected by the ab_mutex
static inline int32_t ORDINATE(seq_t x, seq_t base) {
@@ -226,26 +206,54 @@ static inline seq_t seq_sum(seq_t a, seq_t b) {
return r;
}
-// now for 32-bit wrapping in timestamps
+void reset_input_flow_metrics(rtsp_conn_info *conn) {
+ conn->play_number_after_flush = 0;
+ conn->packet_count_since_flush = 0;
+ conn->input_frame_rate_starting_point_is_valid = 0;
+ conn->initial_reference_time = 0;
+ conn->initial_reference_timestamp = 0;
+}
-// this returns true if the second arg is strictly after the first
-// on the assumption that the gap between them is never greater than (2^31)-1
-// Represent a and b in 64 bits
-static inline int seq32_order(uint32_t a, uint32_t b) {
- if (a == b)
- return 0;
- int64_t A = a & 0xffffffff;
- int64_t B = b & 0xffffffff;
- int64_t C = B - A;
- // if bit 31 is set, it means either b is before (i.e. less than) a or
- // b is (2^31)-1 ahead of a.
-
- // If we assume the gap between b and a should never reach 2 billion, then
- // bit 31 == 0 means b is strictly after a
- return (C & 0x80000000) == 0;
+void unencrypted_packet_decode(unsigned char *packet, int length, short *dest, int *outsize,
+ int size_limit, rtsp_conn_info *conn) {
+ if (conn->stream.type == ast_apple_lossless) {
+#ifdef CONFIG_APPLE_ALAC
+ if (config.use_apple_decoder) {
+ if (conn->decoder_in_use != 1 << decoder_apple_alac) {
+ debug(2, "Apple ALAC Decoder used on encrypted audio.");
+ conn->decoder_in_use = 1 << decoder_apple_alac;
+ }
+ apple_alac_decode_frame(packet, length, (unsigned char *)dest, outsize);
+ *outsize = *outsize * 4; // bring the size to bytes
+ } else
+#endif
+ {
+ if (conn->decoder_in_use != 1 << decoder_hammerton) {
+ debug(2, "Hammerton Decoder used on encrypted audio.");
+ conn->decoder_in_use = 1 << decoder_hammerton;
+ }
+ alac_decode_frame(conn->decoder_info, packet, (unsigned char *)dest, outsize);
+ }
+ } else if (conn->stream.type == ast_uncompressed) {
+ int length_to_use = length;
+ if (length_to_use > size_limit) {
+ warn("unencrypted_packet_decode: uncompressed audio packet too long (size: %d bytes) to "
+ "process -- truncated",
+ length);
+ length_to_use = size_limit;
+ }
+ int i;
+ short *source = (short *)packet;
+ for (i = 0; i < (length_to_use / 2); i++) {
+ *dest = ntohs(*source);
+ dest++;
+ source++;
+ }
+ *outsize = length_to_use;
+ }
}
-static int alac_decode(short *dest, int *destlen, uint8_t *buf, int len, rtsp_conn_info *conn) {
+int audio_packet_decode(short *dest, int *destlen, uint8_t *buf, int len, rtsp_conn_info *conn) {
// parameters: where the decoded stuff goes, its length in samples,
// the incoming packet, the length of the incoming packet in bytes
// destlen should contain the allowed max number of samples on entry
@@ -260,78 +268,44 @@ static int alac_decode(short *dest, int *destlen, uint8_t *buf, int len, rtsp_co
assert(len <= MAX_PACKET);
int reply = 0; // everything okay
int outsize = conn->input_bytes_per_frame * (*destlen); // the size the output should be, in bytes
- int toutsize = outsize;
+ int maximum_possible_outsize = outsize;
if (conn->stream.encrypted) {
unsigned char iv[16];
int aeslen = len & ~0xf;
memcpy(iv, conn->stream.aesiv, sizeof(iv));
-#ifdef HAVE_LIBMBEDTLS
+#ifdef CONFIG_MBEDTLS
mbedtls_aes_crypt_cbc(&conn->dctx, MBEDTLS_AES_DECRYPT, aeslen, iv, buf, packet);
#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
aes_crypt_cbc(&conn->dctx, AES_DECRYPT, aeslen, iv, buf, packet);
#endif
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
AES_cbc_encrypt(buf, packet, aeslen, &conn->aes, iv, AES_DECRYPT);
#endif
memcpy(packet + aeslen, buf + aeslen, len - aeslen);
-#ifdef HAVE_APPLE_ALAC
- if (config.use_apple_decoder) {
- if (conn->decoder_in_use != 1 << decoder_apple_alac) {
- debug(2, "Apple ALAC Decoder used on encrypted audio.");
- conn->decoder_in_use = 1 << decoder_apple_alac;
- }
- apple_alac_decode_frame(packet, len, (unsigned char *)dest, &outsize);
- outsize = outsize * 4; // bring the size to bytes
- } else
-#endif
- {
- if (conn->decoder_in_use != 1 << decoder_hammerton) {
- debug(2, "Hammerton Decoder used on encrypted audio.");
- conn->decoder_in_use = 1 << decoder_hammerton;
- }
- alac_decode_frame(conn->decoder_info, packet, (unsigned char *)dest, &outsize);
- }
+ unencrypted_packet_decode(packet, len, dest, &outsize, maximum_possible_outsize, conn);
} else {
-// not encrypted
-#ifdef HAVE_APPLE_ALAC
- if (config.use_apple_decoder) {
- if (conn->decoder_in_use != 1 << decoder_apple_alac) {
- debug(2, "Apple ALAC Decoder used on unencrypted audio.");
- conn->decoder_in_use = 1 << decoder_apple_alac;
- }
- apple_alac_decode_frame(buf, len, (unsigned char *)dest, &outsize);
- outsize = outsize * 4; // bring the size to bytes
- } else
-#endif
- {
- if (conn->decoder_in_use != 1 << decoder_hammerton) {
- debug(2, "Hammerton Decoder used on unencrypted audio.");
- conn->decoder_in_use = 1 << decoder_hammerton;
- }
- alac_decode_frame(conn->decoder_info, buf, dest, &outsize);
- }
+ // not encrypted
+ unencrypted_packet_decode(buf, len, dest, &outsize, maximum_possible_outsize, conn);
}
- if (outsize > toutsize) {
- debug(2,
- "Output from alac_decode larger (%d bytes, not frames) than expected (%d bytes) -- "
- "truncated, but buffer overflow possible! Encrypted = %d.",
- outsize, toutsize, conn->stream.encrypted);
+ 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.",
+ 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;
}
-static int init_decoder(int32_t fmtp[12], rtsp_conn_info *conn) {
+static int init_alac_decoder(int32_t fmtp[12], rtsp_conn_info *conn) {
// This is a guess, but the format of the fmtp looks identical to the format of an
// ALACSpecificCOnfig
@@ -412,15 +386,8 @@ static int init_decoder(int32_t fmtp[12], rtsp_conn_info *conn) {
alac_file *alac;
- conn->max_frames_per_packet = fmtp[1]; // number of audio frames per packet.
-
- conn->input_rate = fmtp[11];
- conn->input_num_channels = fmtp[7];
- conn->input_bit_depth = fmtp[3];
-
- conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8);
-
- alac = alac_create(conn->input_bit_depth, conn->input_num_channels);
+ alac = alac_create(conn->input_bit_depth,
+ conn->input_num_channels); // no pthread cancellation point in here
if (!alac)
return 1;
conn->decoder_info = alac;
@@ -436,10 +403,10 @@ static int init_decoder(int32_t fmtp[12], rtsp_conn_info *conn) {
alac->setinfo_82 = fmtp[9];
alac->setinfo_86 = fmtp[10];
alac->setinfo_8a_rate = fmtp[11];
- alac_allocate_buffers(alac);
+ alac_allocate_buffers(alac); // no pthread cancellation point in here
-#ifdef HAVE_APPLE_ALAC
- apple_alac_init(fmtp);
+#ifdef CONFIG_APPLE_ALAC
+ apple_alac_init(fmtp); // no pthread cancellation point in here
#endif
return 0;
@@ -447,7 +414,7 @@ static int init_decoder(int32_t fmtp[12], rtsp_conn_info *conn) {
static void terminate_decoders(rtsp_conn_info *conn) {
alac_free(conn->decoder_info);
-#ifdef HAVE_APPLE_ALAC
+#ifdef CONFIG_APPLE_ALAC
apple_alac_terminate();
#endif
}
@@ -465,193 +432,184 @@ static void free_audio_buffers(rtsp_conn_info *conn) {
free(conn->audio_buffer[i].data);
}
-void player_thread_lock_cleanup(void *arg) {
- rtsp_conn_info *conn = (rtsp_conn_info *)arg;
- debug(3, "Cleaning up player_thread_lock.");
- pthread_rwlock_unlock(&conn->player_thread_lock);
-}
+void player_put_packet(seq_t seqno, uint32_t actual_timestamp, uint8_t *data, int len,
+ rtsp_conn_info *conn) {
-void player_put_packet(seq_t seqno, uint32_t actual_timestamp, int64_t timestamp, uint8_t *data,
- int len, rtsp_conn_info *conn) {
- if (pthread_rwlock_tryrdlock(&conn->player_thread_lock) == 0) {
- pthread_cleanup_push(player_thread_lock_cleanup, (void *)conn);
- if (conn->player_thread != NULL) {
+ // 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);
+ }
- // all timestamps are done at the output rate
- // the "actual_timestamp" is the one that comes in the packet, and is carried over for
- // debugging
- // and checking only.
+ debug_mutex_lock(&conn->ab_mutex, 30000, 0);
+ conn->packet_count++;
+ conn->packet_count_since_flush++;
+ conn->time_of_last_audio_packet = get_absolute_time_in_fp();
+ 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 {
+ /*
+ if ((conn->flush_rtp_timestamp != 0) &&
+ (modulo_32_offset(conn->flush_rtp_timestamp, actual_timestamp) > conn->input_rate / 5)
+ &&
+ (modulo_32_offset(conn->flush_rtp_timestamp, actual_timestamp) < conn->input_rate)) {
+ // between 0.2 and 1 second
+ debug(2, "Dropping flush request in player_put_packet");
+ conn->flush_rtp_timestamp = 0;
+ }
+ */
- int64_t ltimestamp = timestamp * conn->output_sample_ratio;
+ abuf_t *abuf = 0;
- // 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);
+ 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;
}
- debug_mutex_lock(&conn->ab_mutex, 30000, 1);
- conn->packet_count++;
- conn->time_of_last_audio_packet = get_absolute_time_in_fp();
- if (conn->connection_state_to_output) { // if we are supposed to be processing these packets
-
- // if (flush_rtp_timestamp != 0)
- // debug(1,"Flush_rtp_timestamp is %u",flush_rtp_timestamp);
-
- if ((conn->flush_rtp_timestamp != 0) && (ltimestamp <= conn->flush_rtp_timestamp)) {
- debug(
- 3,
- "Dropping flushed packet in player_put_packet, seqno %u, timestamp %lld, flushing to "
- "timestamp: %lld.",
- seqno, ltimestamp, conn->flush_rtp_timestamp);
- } else {
- if ((conn->flush_rtp_timestamp != 0x0) &&
- (ltimestamp >
- conn->flush_rtp_timestamp)) // if we have gone past the flush boundary time
- conn->flush_rtp_timestamp = 0x0;
-
- abuf_t *abuf = 0;
-
- if (!conn->ab_synced) {
- debug(3, "syncing to seqno %u.", seqno);
- conn->ab_write = seqno;
- conn->ab_read = seqno;
- conn->ab_synced = 1;
- }
+ // here, we should check for missing frames
+ int resend_interval = (((250 * 44100) / 352) / 1000); // approximately 250 ms intervals
+ const int number_of_resend_attempts = 8;
+ int latency_based_resend_interval =
+ (conn->latency) / (number_of_resend_attempts * conn->max_frames_per_packet);
+ if (latency_based_resend_interval > resend_interval)
+ resend_interval = latency_based_resend_interval;
+
+ if (conn->resend_interval != resend_interval) {
+ debug(2, "Resend interval for latency of %u frames is %d frames.", conn->latency,
+ resend_interval);
+ conn->resend_interval = resend_interval;
+ }
- // here, we should check for missing frames
- int resend_interval = (((250 * 44100) / 352) / 1000); // approximately 250 ms intervals
- const int number_of_resend_attempts = 8;
- int latency_based_resend_interval =
- (conn->latency) / (number_of_resend_attempts * conn->max_frames_per_packet);
- if (latency_based_resend_interval > resend_interval)
- resend_interval = latency_based_resend_interval;
-
- if (conn->resend_interval != resend_interval) {
- debug(2, "Resend interval for latency of %" PRId64 " frames is %d frames.",
- conn->latency, resend_interval);
- conn->resend_interval = resend_interval;
+ if (conn->ab_write ==
+ seqno) { // if this is the expected packet (which could be the first packet...)
+ uint64_t reception_time = get_absolute_time_in_fp();
+ 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 = reception_time;
+ conn->frames_inward_frames_received_at_measurement_start_time = actual_timestamp;
+ conn->input_frame_rate_starting_point_is_valid = 1; // valid now
}
+ }
- if (conn->ab_write == seqno) { // expected packet
- abuf = conn->audio_buffer + BUFIDX(seqno);
- conn->ab_write = SUCCESSOR(seqno);
- } else if (seq_order(conn->ab_write, seqno, conn->ab_read)) { // newer than expected
- // if (ORDINATE(seqno)>(BUFFER_FRAMES*7)/8)
- // debug(1,"An interval of %u frames has opened, with ab_read: %u, ab_write: %u and
- // seqno:
- // %u.",seq_diff(ab_read,seqno),ab_read,ab_write,seqno);
- 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_level = 0;
- abuf->timestamp = 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)) { // late but not yet played
- conn->late_packets++;
- abuf = conn->audio_buffer + BUFIDX(seqno);
- /*
- if (abuf->ready)
- debug(1,"Late apparently duplicate packet received that is %d packets
- late.",seq_diff(seqno, conn->ab_write, conn->ab_read));
- else
- debug(1,"Late packet received that is %d packets late.",seq_diff(seqno,
- conn->ab_write, conn->ab_read));
- */
- } else { // too late.
+ conn->frames_inward_measurement_time = reception_time;
+ 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
+ // if (ORDINATE(seqno)>(BUFFER_FRAMES*7)/8)
+ // debug(1,"An interval of %u frames has opened, with ab_read: %u, ab_write: %u and
+ // seqno:
+ // %u.",seq_diff(ab_read,seqno),ab_read,ab_write,seqno);
+ 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_level = 0;
+ // abuf->timestamp = 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)) { // late but not yet played
+ conn->late_packets++;
+ abuf = conn->audio_buffer + BUFIDX(seqno);
+ /*
+ if (abuf->ready)
+ debug(1,"Late apparently duplicate packet received that is %d packets
+ late.",seq_diff(seqno, conn->ab_write, conn->ab_read));
+ else
+ debug(1,"Late packet received that is %d packets late.",seq_diff(seqno,
+ conn->ab_write, conn->ab_read));
+ */
+ } else { // too late.
- // debug(1,"Too late packet received that is %d packets late.",seq_diff(seqno,
- // conn->ab_write, conn->ab_read));
- conn->too_late_packets++;
- }
- // pthread_mutex_unlock(&ab_mutex);
-
- if (abuf) {
- int datalen = conn->max_frames_per_packet;
- if (alac_decode(abuf->data, &datalen, data, len, conn) == 0) {
- abuf->ready = 1;
- abuf->length = datalen;
- abuf->timestamp = ltimestamp;
- abuf->given_timestamp = actual_timestamp;
- abuf->sequence_number = seqno;
- } else {
- debug(1, "Bad audio packet detected and discarded.");
- abuf->ready = 0;
- abuf->resend_level = 0;
- abuf->timestamp = 0;
- abuf->given_timestamp = 0;
- abuf->sequence_number = 0;
- }
- }
+ // debug(1,"Too late packet received that is %d packets late.",seq_diff(seqno,
+ // conn->ab_write, conn->ab_read));
+ conn->too_late_packets++;
+ }
+ // pthread_mutex_unlock(&ab_mutex);
+
+ if (abuf) {
+ int datalen = conn->max_frames_per_packet;
+ if (audio_packet_decode(abuf->data, &datalen, data, len, conn) == 0) {
+ abuf->ready = 1;
+ abuf->length = datalen;
+ // abuf->timestamp = ltimestamp;
+ abuf->given_timestamp = actual_timestamp;
+ abuf->sequence_number = seqno;
+ } else {
+ debug(1, "Bad audio packet detected and discarded.");
+ abuf->ready = 0;
+ abuf->resend_level = 0;
+ // abuf->timestamp = 0;
+ abuf->given_timestamp = 0;
+ abuf->sequence_number = 0;
+ }
+ }
- // pthread_mutex_lock(&ab_mutex);
- int rc = pthread_cond_signal(&conn->flowcontrol);
- if (rc)
- debug(1, "Error signalling flowcontrol.");
-
- // if it's at the expected time, do a look back for missing packets
- // but release the ab_mutex when doing a resend
- if (!conn->ab_buffering) {
- int j;
- for (j = 1; j <= number_of_resend_attempts; j++) {
- // check j times, after a short period of has elapsed, assuming 352 frames per packet
- // the higher the step_exponent, the less it will try. 1 means it will try very
- // hard. 2.0 seems good.
- float step_exponent = 2.0;
- int back_step = (int)(resend_interval * pow(j, step_exponent));
- int k;
- for (k = -1; k <= 1; k++) {
- if ((back_step + k) <
- seq_diff(conn->ab_read, conn->ab_write,
- conn->ab_read)) { // if it's within the range of frames in use...
- int item_to_check = (conn->ab_write - (back_step + k)) & 0xffff;
- seq_t next = item_to_check;
- abuf_t *check_buf = conn->audio_buffer + BUFIDX(next);
- if ((!check_buf->ready) &&
- (check_buf->resend_level <
- j)) { // prevent multiple requests from the same level of lookback
- check_buf->resend_level = j;
- if (config.disable_resend_requests == 0) {
- if (((int)(resend_interval * pow(j + 1, step_exponent)) + k) >=
- seq_diff(conn->ab_read, conn->ab_write, conn->ab_read))
- debug(3,
- "Last-ditch (#%d) resend request for packet %u in range %u to %u. "
- "Looking back %d packets.",
- j, next, conn->ab_read, conn->ab_write, back_step + k);
- debug_mutex_unlock(&conn->ab_mutex, 3);
- rtp_request_resend(next, 1, conn);
- conn->resend_requests++;
- debug_mutex_lock(&conn->ab_mutex, 20000, 1);
- }
- }
+ // pthread_mutex_lock(&ab_mutex);
+ int rc = pthread_cond_signal(&conn->flowcontrol);
+ if (rc)
+ debug(1, "Error signalling flowcontrol.");
+
+ // if it's at the expected time, do a look back for missing packets
+ // but release the ab_mutex when doing a resend
+ if (!conn->ab_buffering) {
+ int j;
+ for (j = 1; j <= number_of_resend_attempts; j++) {
+ // check j times, after a short period of has elapsed, assuming 352 frames per packet
+ // the higher the step_exponent, the less it will try. 1 means it will try very
+ // hard. 2.0 seems good.
+ float step_exponent = 2.0;
+ int back_step = (int)(resend_interval * pow(j, step_exponent));
+ int k;
+ for (k = -1; k <= 1; k++) {
+ if ((back_step + k) <
+ seq_diff(conn->ab_read, conn->ab_write,
+ conn->ab_read)) { // if it's within the range of frames in use...
+ int item_to_check = (conn->ab_write - (back_step + k)) & 0xffff;
+ seq_t next = item_to_check;
+ abuf_t *check_buf = conn->audio_buffer + BUFIDX(next);
+ if ((!check_buf->ready) &&
+ (check_buf->resend_level <
+ j)) { // prevent multiple requests from the same level of lookback
+ check_buf->resend_level = j;
+ if (config.disable_resend_requests == 0) {
+ debug_mutex_unlock(&conn->ab_mutex, 3);
+ rtp_request_resend(next, 1, conn);
+ conn->resend_requests++;
+ debug_mutex_lock(&conn->ab_mutex, 20000, 1);
}
}
}
}
}
}
- debug_mutex_unlock(&conn->ab_mutex, 3);
- } else {
- debug(1, "player_put_packet discarded packet %d because the player thread was gone.");
}
- pthread_cleanup_pop(1);
- // pthread_rwlock_unlock(&conn->player_thread_lock);
- } else {
- debug(1, "player_put_packet discarded packet %d because the player thread was locked.", seqno);
}
+ debug_mutex_unlock(&conn->ab_mutex, 0);
}
int32_t rand_in_range(int32_t exclusive_range_limit) {
@@ -666,6 +624,16 @@ int32_t rand_in_range(int32_t exclusive_range_limit) {
static inline void process_sample(int32_t sample, char **outp, enum sps_format_t format, int volume,
int dither, rtsp_conn_info *conn) {
+ /*
+ {
+ static int old_volume = 0;
+ if (volume != old_volume) {
+ debug(1,"Volume is now %d.",volume);
+ old_volume = volume;
+ }
+ }
+ */
+
int64_t hyper_sample = sample;
int result = 0;
@@ -696,14 +664,20 @@ static inline void process_sample(int32_t sample, char **outp, enum sps_format_t
int64_t dither_mask = 0;
switch (format) {
case SPS_FORMAT_S32:
+ case SPS_FORMAT_S32_LE:
+ case SPS_FORMAT_S32_BE:
dither_mask = (int64_t)1 << (64 + 1 - 32);
break;
case SPS_FORMAT_S24:
+ case SPS_FORMAT_S24_LE:
+ case SPS_FORMAT_S24_BE:
case SPS_FORMAT_S24_3LE:
case SPS_FORMAT_S24_3BE:
dither_mask = (int64_t)1 << (64 + 1 - 24);
break;
case SPS_FORMAT_S16:
+ case SPS_FORMAT_S16_LE:
+ case SPS_FORMAT_S16_BE:
dither_mask = (int64_t)1 << (64 + 1 - 16);
break;
case SPS_FORMAT_S8:
@@ -712,6 +686,13 @@ static inline void process_sample(int32_t sample, char **outp, enum sps_format_t
break;
case SPS_FORMAT_UNKNOWN:
die("Unexpected SPS_FORMAT_UNKNOWN while calculating dither mask.");
+ break;
+ case SPS_FORMAT_AUTO:
+ die("Unexpected SPS_FORMAT_AUTO while calculating dither mask.");
+ break;
+ case SPS_FORMAT_INVALID:
+ die("Unexpected SPS_FORMAT_INVALID while calculating dither mask.");
+ break;
}
dither_mask -= 1;
// int64_t r = r64i();
@@ -740,6 +721,30 @@ static inline void process_sample(int32_t sample, char **outp, enum sps_format_t
char *op = *outp;
uint8_t byt;
switch (format) {
+ case SPS_FORMAT_S32_LE:
+ hyper_sample >>= (64 - 32);
+ byt = (uint8_t)hyper_sample;
+ *op++ = byt;
+ byt = (uint8_t)(hyper_sample >> 8);
+ *op++ = byt;
+ byt = (uint8_t)(hyper_sample >> 16);
+ *op++ = byt;
+ byt = (uint8_t)(hyper_sample >> 24);
+ *op++ = byt;
+ result = 4;
+ break;
+ case SPS_FORMAT_S32_BE:
+ hyper_sample >>= (64 - 32);
+ byt = (uint8_t)(hyper_sample >> 24);
+ *op++ = byt;
+ byt = (uint8_t)(hyper_sample >> 16);
+ *op++ = byt;
+ byt = (uint8_t)(hyper_sample >> 8);
+ *op++ = byt;
+ byt = (uint8_t)hyper_sample;
+ *op++ = byt;
+ result = 4;
+ break;
case SPS_FORMAT_S32:
hyper_sample >>= (64 - 32);
*(int32_t *)op = hyper_sample;
@@ -765,11 +770,49 @@ static inline void process_sample(int32_t sample, char **outp, enum sps_format_t
*op++ = byt;
result = 3;
break;
+ case SPS_FORMAT_S24_LE:
+ hyper_sample >>= (64 - 24);
+ byt = (uint8_t)hyper_sample;
+ *op++ = byt;
+ byt = (uint8_t)(hyper_sample >> 8);
+ *op++ = byt;
+ byt = (uint8_t)(hyper_sample >> 16);
+ *op++ = byt;
+ *op++ = 0;
+ result = 4;
+ break;
+ case SPS_FORMAT_S24_BE:
+ hyper_sample >>= (64 - 24);
+ *op++ = 0;
+ byt = (uint8_t)(hyper_sample >> 16);
+ *op++ = byt;
+ byt = (uint8_t)(hyper_sample >> 8);
+ *op++ = byt;
+ byt = (uint8_t)hyper_sample;
+ *op++ = byt;
+ result = 4;
+ break;
case SPS_FORMAT_S24:
hyper_sample >>= (64 - 24);
*(int32_t *)op = hyper_sample;
result = 4;
break;
+ case SPS_FORMAT_S16_LE:
+ hyper_sample >>= (64 - 16);
+ byt = (uint8_t)hyper_sample;
+ *op++ = byt;
+ byt = (uint8_t)(hyper_sample >> 8);
+ *op++ = byt;
+ result = 2;
+ break;
+ case SPS_FORMAT_S16_BE:
+ hyper_sample >>= (64 - 16);
+ byt = (uint8_t)(hyper_sample >> 8);
+ *op++ = byt;
+ byt = (uint8_t)hyper_sample;
+ *op++ = byt;
+ result = 2;
+ break;
case SPS_FORMAT_S16:
hyper_sample >>= (64 - 16);
*(int16_t *)op = (int16_t)hyper_sample;
@@ -788,46 +831,43 @@ static inline void process_sample(int32_t sample, char **outp, enum sps_format_t
break;
case SPS_FORMAT_UNKNOWN:
die("Unexpected SPS_FORMAT_UNKNOWN while outputting samples");
+ break;
+ case SPS_FORMAT_AUTO:
+ die("Unexpected SPS_FORMAT_AUTO while outputting samples");
+ break;
+ case SPS_FORMAT_INVALID:
+ die("Unexpected SPS_FORMAT_INVALID while outputting samples");
+ break;
}
*outp += result;
}
+void buffer_get_frame_cleanup_handler(void *arg) {
+ rtsp_conn_info *conn = (rtsp_conn_info *)arg;
+ debug_mutex_unlock(&conn->ab_mutex, 0);
+}
+
// get the next frame, when available. return 0 if underrun/stream reset.
static abuf_t *buffer_get_frame(rtsp_conn_info *conn) {
// int16_t buf_fill;
uint64_t local_time_now;
// struct timespec tn;
- abuf_t *curframe = 0;
+ abuf_t *curframe = NULL;
int notified_buffer_empty = 0; // diagnostic only
- debug_mutex_lock(&conn->ab_mutex, 30000, 1);
+ debug_mutex_lock(&conn->ab_mutex, 30000, 0);
+
int wait;
long dac_delay = 0; // long because alsa returns a long
+
+ 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
- debug(3, "buffer_get_frame is iterating");
-
- // if config.timeout (default 120) seconds have elapsed since the last audio packet was
- // received, then we should stop.
- // config.timeout of zero means don't check..., but iTunes may be confused by a long gap
- // followed by a resumption...
-
- if ((conn->time_of_last_audio_packet != 0) && (conn->stop == 0) &&
- (config.dont_check_timeout == 0)) {
- uint64_t ct = config.timeout; // go from int to 64-bit int
- // if (conn->packet_count>500) { //for testing -- about 4 seconds of play first
- if ((local_time_now > conn->time_of_last_audio_packet) &&
- (local_time_now - conn->time_of_last_audio_packet >= ct << 32)) {
- debug(1,
- "As Yeats almost said, \"Too long a silence / can make a stone of the heart\" "
- "from RTSP conversation %d.",
- conn->connection_number);
- conn->stop = 1;
- pthread_kill(conn->thread, SIGUSR1);
- }
- }
+ // debug(3, "buffer_get_frame is iterating");
+
int rco = get_requested_connection_state_to_output();
if (conn->connection_state_to_output != rco) {
@@ -840,81 +880,96 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) {
}
}
- debug_mutex_lock(&conn->flush_mutex, 1000, 1);
+ 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_mutex_lock(&conn->flush_mutex, 1000, 0);
+ conn->flush_requested = 1;
+ 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();
- ab_resync(conn);
+ 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;
}
- debug_mutex_unlock(&conn->flush_mutex, 3);
+ debug_mutex_unlock(&conn->flush_mutex, 0);
- uint32_t flush_limit = 0;
if (conn->ab_synced) {
- do {
- curframe = conn->audio_buffer + BUFIDX(conn->ab_read);
- if ((conn->ab_read != conn->ab_write) &&
- (curframe->ready)) { // it could be synced and empty, under
- // exceptional circumstances, with the
- // frame unused, thus apparently ready
-
- 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.");
- }
- } else {
- debug(1, "Inconsistent sequence numbers detected");
- }
- }
+ curframe = conn->audio_buffer + BUFIDX(conn->ab_read);
- if ((conn->flush_rtp_timestamp != 0) &&
- (curframe->timestamp <= conn->flush_rtp_timestamp)) {
- debug(1, "Dropping flushed packet seqno %u, timestamp %lld", curframe->sequence_number,
- curframe->timestamp);
- curframe->ready = 0;
- curframe->resend_level = 0;
- flush_limit++;
- conn->ab_read = SUCCESSOR(conn->ab_read);
+ if ((conn->ab_read != conn->ab_write) &&
+ (curframe->ready)) { // it could be synced and empty, under
+ // exceptional circumstances, with the
+ // frame unused, thus apparently ready
+
+ 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.");
+ }
+ } else {
+ debug(1, "Inconsistent sequence numbers detected");
}
- if (curframe->timestamp > conn->flush_rtp_timestamp)
- conn->flush_rtp_timestamp = 0;
}
- } while ((conn->flush_rtp_timestamp != 0) && (flush_limit <= 8820) && (curframe->ready == 0));
- if (flush_limit == 8820) {
- debug(1, "Flush hit the 8820 frame limit!");
- flush_limit = 0;
+ // 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_level = 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;
+ }
}
- curframe = conn->audio_buffer + BUFIDX(conn->ab_read);
-
- if (curframe->ready) {
+ 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 (reference_timestamp) { // if we have a reference time
+ if (have_timestamp_timing_information(conn)) { // if we have a reference time
// debug(1,"First frame seen with timestamp...");
conn->first_packet_timestamp =
- curframe->timestamp; // we will keep buffering until we are
- // supposed to start playing this
+ 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);
@@ -951,61 +1006,62 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) {
// debug(1, "Output sample ratio is %d", conn->output_sample_ratio);
- int64_t delta = (conn->first_packet_timestamp - reference_timestamp) +
- conn->latency * conn->output_sample_ratio +
- (int64_t)(config.audio_backend_latency_offset * config.output_rate);
+ // what we are asking for here is "what is the local time at which time the calculated
+ // frame should be played"
- if (delta >= 0) {
- int64_t delta_fp_sec =
- (delta << 32) / config.output_rate; // int64_t which is positive
- conn->first_packet_time_to_play = reference_timestamp_time + delta_fp_sec;
- } else {
- int64_t abs_delta = -delta;
- int64_t delta_fp_sec =
- (abs_delta << 32) / config.output_rate; // int64_t which is positive
- conn->first_packet_time_to_play = reference_timestamp_time - delta_fp_sec;
- }
+ 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);
+
+ conn->first_packet_time_to_play = should_be_time;
- if (local_time_now >= conn->first_packet_time_to_play) {
- debug(
- 1,
- "First packet is late! It should have played before now. Flushing 0.5 seconds");
- player_flush(conn->first_packet_timestamp + 5 * 4410 * conn->output_sample_ratio,
- conn);
+ 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",
+ 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
- int64_t delta = (conn->first_packet_timestamp - reference_timestamp) +
- conn->latency * conn->output_sample_ratio +
- (int64_t)(config.audio_backend_latency_offset * config.output_rate);
-
- if (delta >= 0) {
- int64_t delta_fp_sec =
- (delta << 32) / config.output_rate; // int64_t which is positive
- conn->first_packet_time_to_play = reference_timestamp_time + delta_fp_sec;
- } else {
- int64_t abs_delta = -delta;
- int64_t delta_fp_sec =
- (abs_delta << 32) / config.output_rate; // int64_t which is positive
- conn->first_packet_time_to_play = reference_timestamp_time - delta_fp_sec;
- }
- // now, the size of the initial silence must be affected by the lead-in time.
- // it must be somewhat less than the lead-in time so that dynamic adjustments can be
- // made
- // to compensate for delays due to paging, etc.
- // The suggestion is that it should be at least 100 ms less than the lead-in time.
+ 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 max_dac_delay = config.output_rate / 10; // so the lead-in time must be greater
- // than this, say 0.2 sec, to allow for
- // dynamic adjustment
- int64_t filler_size = max_dac_delay; // 0.1 second -- the maximum we'll add to the DAC
+ int64_t filler_size = max_dac_delay;
- if (local_time_now >= conn->first_packet_time_to_play) {
- // debug(1,"Gone past starting time");
+ 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;
@@ -1026,85 +1082,117 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) {
} else {
// do some calculations
int64_t lead_time = conn->first_packet_time_to_play - local_time_now;
- int64_t lead_in_time =
- (int64_t)(config.audio_backend_silent_lead_in_time * (int64_t)0x100000000);
+ // 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);
- // an audio_backend_silent_lead_in_time of less than zero means start filling ASAP
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
- if (have_sent_prefiller_silence != 0) {
- int resp = config.output->delay(&dac_delay);
+ 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
- if (resp != 0) {
- debug(1, "Error %d getting dac_delay in buffer_get_frame.", resp);
- dac_delay = 0;
+ 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 {
- dac_delay = 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;
- if (exact_frame_gap < 0) {
- // we've gone past the time...
- // debug(1,"Run past time.");
- // debug(1,"Run a bit past the exact start time by %lld frames, with time now of
- // %llx, fpttp of %llx and dac_delay of %d and %d packets;
- // flush.",-exact_frame_gap,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;
- } else {
- int64_t fs = filler_size;
- if (fs > (max_dac_delay - dac_delay))
- fs = max_dac_delay - dac_delay;
- if (fs < 0) {
- debug(2,
- "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 {
- memset(silence, 0, conn->output_bytes_per_frame * fs);
- // 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);
- free(silence);
+ 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);
}
}
- have_sent_prefiller_silence =
- 1; // even if we haven't sent silence because it's zero frames long...
}
} else {
// no delay function on back end -- just send the prefiller silence
@@ -1116,7 +1204,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) {
int64_t frame_gap = (lead_time * config.output_rate) >> 32;
// debug(1,"%d frames needed.",frame_gap);
while (frame_gap > 0) {
- size_t fs = config.output_rate / 10;
+ ssize_t fs = config.output_rate / 10;
if (fs > frame_gap)
fs = frame_gap;
@@ -1125,7 +1213,9 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) {
debug(1, "Failed to allocate %d frame silence buffer.", fs);
else {
// debug(1, "No delay function -- outputting %d frames of silence.", fs);
- memset(silence, 0, conn->output_bytes_per_frame * 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);
}
@@ -1139,12 +1229,14 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) {
}
}
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;
+/*
+ // 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;
+*/
#ifdef CONFIG_METADATA
debug(2, "prsm");
send_ssnc_metadata('prsm', NULL, 0,
@@ -1168,36 +1260,21 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) {
// Note: the last three items are expressed in frames and must be converted to time.
int do_wait = 0; // don't wait unless we can really prove we must
- if ((conn->ab_synced) && (curframe) && (curframe->ready) && (curframe->timestamp)) {
+ if ((conn->ab_synced) && (curframe) && (curframe->ready) && (curframe->given_timestamp)) {
do_wait =
1; // if the current frame exists and is ready, then wait unless it's time to let it go...
- 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); // all types okay
- reference_timestamp *= conn->output_sample_ratio;
- if (reference_timestamp) { // if we have a reference time
- int64_t packet_timestamp = curframe->timestamp; // types okay
- int64_t delta = packet_timestamp - reference_timestamp;
- int64_t offset =
- conn->latency * conn->output_sample_ratio +
- (int64_t)(config.audio_backend_latency_offset * config.output_rate) -
- config.audio_backend_buffer_desired_length *
- config.output_rate; // all arguments are int32_t, so expression promotion okay
- int64_t net_offset = delta + offset; // okay
- uint64_t time_to_play = reference_timestamp_time; // type okay
- if (net_offset >= 0) {
- uint64_t net_offset_fp_sec =
- (net_offset << 32) / config.output_rate; // int64_t which is positive
- time_to_play += net_offset_fp_sec; // using the latency requested...
- // debug(2,"Net Offset: %lld, adjusted: %lld.",net_offset,net_offset_fp_sec);
- } else {
- int64_t abs_net_offset = -net_offset;
- uint64_t net_offset_fp_sec =
- (abs_net_offset << 32) / config.output_rate; // int64_t which is positive
- time_to_play -= net_offset_fp_sec;
- // debug(2,"Net Offset: %lld, adjusted: -%lld.",net_offset,net_offset_fp_sec);
- }
+
+ // here, get the time to play the current frame.
+
+ 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);
if (local_time_now >= time_to_play) {
do_wait = 0;
@@ -1209,17 +1286,17 @@ 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);
}
do_wait = 1;
}
- wait = (conn->ab_buffering || (do_wait != 0) || (!conn->ab_synced)) &&
- (!conn->player_thread_please_stop);
+ 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 *= 4 * 352; // four full 352-frame packets
- time_to_wait_for_wakeup_fp /= 3; // four thirds of a packet time
+ 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
#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;
@@ -1230,8 +1307,9 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) {
time_of_wakeup.tv_sec = sec;
time_of_wakeup.tv_nsec = nsec;
// pthread_cond_timedwait(&conn->flowcontrol, &conn->ab_mutex, &time_of_wakeup);
- int rc = pthread_cond_timedwait(&conn->flowcontrol, &conn->ab_mutex, &time_of_wakeup);
- if (rc != 0)
+ int rc = pthread_cond_timedwait(&conn->flowcontrol, &conn->ab_mutex,
+ &time_of_wakeup); // this is a pthread cancellation point
+ if ((rc != 0) && (rc != ETIMEDOUT))
debug(3, "pthread_cond_timedwait returned error code %d.", rc);
#endif
#ifdef COMPILE_FOR_OSX
@@ -1245,36 +1323,21 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) {
}
} while (wait);
- if (conn->player_thread_please_stop) {
- debug(3, "buffer_get_frame exiting due to thread stop request.");
- debug_mutex_unlock(&conn->ab_mutex, 3);
- return 0;
- }
-
// seq_t read = conn->ab_read;
-
- if (!curframe->ready) {
- // debug(1, "Supplying a silent frame for frame %u", read);
- conn->missing_packets++;
- curframe->timestamp = 0; // indicate a silent frame should be substituted
+ if (curframe) {
+ if (!curframe->ready) {
+ // debug(1, "Supplying a silent frame for frame %u", read);
+ conn->missing_packets++;
+ curframe->given_timestamp = 0; // indicate a silent frame should be substituted
+ }
+ curframe->ready = 0;
+ curframe->resend_level = 0;
}
- curframe->ready = 0;
- curframe->resend_level = 0;
conn->ab_read = SUCCESSOR(conn->ab_read);
- debug_mutex_unlock(&conn->ab_mutex, 3);
+ pthread_cleanup_pop(1);
return curframe;
}
-static inline short shortmean(short a, short b) {
- long al = (long)a;
- long bl = (long)b;
- long longmean = (al + bl) / 2;
- short r = (short)longmean;
- if (r != longmean)
- debug(1, "Error calculating average of two shorts");
- return r;
-}
-
static inline int32_t mean_32(int32_t a, int32_t b) {
int64_t al = a;
int64_t bl = b;
@@ -1342,7 +1405,7 @@ static int stuff_buffer_basic_32(int32_t *inptr, int length, enum sps_format_t l
return length + tstuff;
}
-#ifdef HAVE_LIBSOXR
+#ifdef CONFIG_SOXR
// this takes an array of signed 32-bit integers and
// (a) uses libsoxr to
// resample the array to have one more or one less frame, as specified in
@@ -1352,12 +1415,19 @@ static int stuff_buffer_basic_32(int32_t *inptr, int length, enum sps_format_t l
// (d) outputs the result in the approprate format
// formats accepted so far include U8, S8, S16, S24, S24_3LE, S24_3BE and S32
+int32_t stat_n = 0;
+double stat_mean = 0.0;
+double stat_M2 = 0.0;
+double longest_soxr_execution_time_us = 0.0;
+int64_t packets_processed = 0;
+
int stuff_buffer_soxr_32(int32_t *inptr, int32_t *scratchBuffer, int length,
enum sps_format_t l_output_format, char *outptr, int stuff, int dither,
rtsp_conn_info *conn) {
if (scratchBuffer == NULL) {
die("soxr scratchBuffer not initialised.");
}
+ packets_processed++;
int tstuff = stuff;
if ((stuff > 1) || (stuff < -1) || (length < 100)) {
// debug(1, "Stuff argument to stuff_buffer must be from -1 to +1 and length >100.");
@@ -1376,6 +1446,8 @@ int stuff_buffer_soxr_32(int32_t *inptr, int32_t *scratchBuffer, int length,
size_t odone;
+ uint64_t soxr_start_time = get_absolute_time_in_fp();
+
soxr_error_t error = soxr_oneshot(length, length + tstuff, 2, // Rates and # of chans.
inptr, length, NULL, // Input.
scratchBuffer, length + tstuff, &odone, // Output.
@@ -1388,6 +1460,19 @@ int stuff_buffer_soxr_32(int32_t *inptr, int32_t *scratchBuffer, int length,
if (odone > (size_t)(length + 1))
die("odone = %u!\n", odone);
+ // mean and variance calculations from "online_variance" algorithm at
+ // https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm
+
+ double soxr_execution_time_us =
+ (((get_absolute_time_in_fp() - soxr_start_time) * 1000000) >> 32) * 1.0;
+ // debug(1,"soxr_execution_time_us: %10.1f",soxr_execution_time_us);
+ if (soxr_execution_time_us > longest_soxr_execution_time_us)
+ longest_soxr_execution_time_us = soxr_execution_time_us;
+ stat_n += 1;
+ double stat_delta = soxr_execution_time_us - stat_mean;
+ stat_mean += stat_delta / stat_n;
+ stat_M2 += stat_delta * (soxr_execution_time_us - stat_mean);
+
int i;
int32_t *ip, *op;
ip = inptr;
@@ -1430,6 +1515,18 @@ int stuff_buffer_soxr_32(int32_t *inptr, int32_t *scratchBuffer, int length,
process_sample(*ip++, &l_outptr, l_output_format, conn->fix_volume, dither, conn);
};
}
+
+ if (packets_processed % 1250 == 0) {
+ debug(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.",
+ stat_n, stat_mean, stat_n <= 1 ? 0.0 : sqrtf(stat_M2 / (stat_n - 1)), longest_soxr_execution_time_us);
+ stat_n = 0;
+ stat_mean = 0.0;
+ stat_M2 = 0.0;
+ longest_soxr_execution_time_us = 0.0;
+ }
+
conn->amountStuffed = tstuff;
return length + tstuff;
}
@@ -1439,41 +1536,88 @@ typedef struct stats { // statistics for running averages
int64_t sync_error, correction, drift;
} stats_t;
-void *player_thread_func(void *arg) {
- // note, the thread will be started up with the player_thread_lock locked. You must release it
- // quickly.
+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.",
+ conn->connection_number);
+}
+void player_thread_cleanup_handler(void *arg) {
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
+ debug(3, "Connection %d: player thread main loop exit via player_thread_cleanup_handler.",
+ conn->connection_number);
- pthread_rwlock_wrlock(&conn->player_thread_lock);
+ if (config.output->stop)
+ config.output->stop();
- int rc = pthread_mutex_init(&conn->ab_mutex, NULL);
- if (rc)
- debug(1, "Error initialising ab_mutex.");
- rc = pthread_mutex_init(&conn->flush_mutex, NULL);
- if (rc)
- debug(1, "Error initialising flush_mutex.");
-// set the flowcontrol condition variable to wait on a monotonic clock
-#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(&conn->flowcontrol, &attr);
-#endif
-#ifdef COMPILE_FOR_OSX
- rc = pthread_cond_init(&conn->flowcontrol, NULL);
+ if (config.statistics_requested) {
+ int rawSeconds = (int)difftime(time(NULL), conn->playstart);
+ int elapsedHours = rawSeconds / 3600;
+ int elapsedMin = (rawSeconds / 60) % 60;
+ int elapsedSec = rawSeconds % 60;
+ if (conn->frame_rate_status)
+ inform("Playback Stopped. Total playing time %02d:%02d:%02d. Input: %0.2f, output: %0.2f "
+ "frames per second.",
+ elapsedHours, elapsedMin, elapsedSec, conn->input_frame_rate, conn->frame_rate);
+ else
+ inform("Playback Stopped. Total playing time %02d:%02d:%02d. Input: %0.2f frames per second.",
+ elapsedHours, elapsedMin, elapsedSec, conn->input_frame_rate);
+ }
+
+#ifdef CONFIG_DACP_CLIENT
+ relinquish_dacp_server_information(
+ conn); // say it doesn't belong to this conversation thread any more...
+#else
+ mdns_dacp_monitor_set_id(NULL); // say we're not interested in following that DACP id any more
#endif
- if (rc)
- debug(1, "Error initialising flowcontrol condition variable.");
- pthread_rwlock_unlock(&conn->player_thread_lock);
+ debug(3, "Cancelling timing, control and audio threads...");
+ debug(3, "Cancel timing thread.");
+ pthread_cancel(conn->rtp_timing_thread);
+ debug(3, "Join timing thread.");
+ pthread_join(conn->rtp_timing_thread, NULL);
+ debug(3, "Timing thread terminated.");
+ debug(3, "Cancel control thread.");
+ pthread_cancel(conn->rtp_control_thread);
+ debug(3, "Join control thread.");
+ pthread_join(conn->rtp_control_thread, NULL);
+ debug(3, "Control thread terminated.");
+ debug(3, "Cancel audio thread.");
+ pthread_cancel(conn->rtp_audio_thread);
+ debug(3, "Join audio thread.");
+ pthread_join(conn->rtp_audio_thread, NULL);
+ debug(3, "Audio thread terminated.");
+
+ if (conn->outbuf) {
+ free(conn->outbuf);
+ conn->outbuf = NULL;
+ }
+ if (conn->sbuf) {
+ free(conn->sbuf);
+ conn->sbuf = NULL;
+ }
+ if (conn->tbuf) {
+ free(conn->tbuf);
+ conn->tbuf = NULL;
+ }
+ free_audio_buffers(conn);
+ if (conn->stream.type == ast_apple_lossless)
+ terminate_decoders(conn);
- // it's safe now
+ clear_reference_timestamp(conn);
+ conn->rtp_running = 0;
+ pthread_setcancelstate(oldState, NULL);
+}
+void *player_thread_func(void *arg) {
+ rtsp_conn_info *conn = (rtsp_conn_info *)arg;
+ // pthread_cleanup_push(player_thread_initial_cleanup_handler, arg);
conn->packet_count = 0;
+ conn->packet_count_since_flush = 0;
conn->previous_random_number = 0;
conn->input_bytes_per_frame = 4;
- conn->player_thread_please_stop = 0;
conn->decoder_in_use = 0;
conn->ab_buffering = 1;
conn->ab_synced = 0;
@@ -1487,25 +1631,25 @@ void *player_thread_func(void *arg) {
conn->latency = 88200;
}
- config.output->start(config.output_rate, config.output_format);
-
- init_decoder((int32_t *)&conn->stream.fmtp,
- conn); // this sets up incoming rate, bit depth, channels
- // must be after decoder init
- init_buffer(conn);
+ if (conn->stream.type == ast_apple_lossless)
+ init_alac_decoder((int32_t *)&conn->stream.fmtp,
+ conn); // this sets up incoming rate, bit depth, channels.
+ // No pthread cancellation point in here
+ // This must be after init_alac_decoder
+ init_buffer(conn); // will need a corresponding deallocation. No cancellation points in here
if (conn->stream.encrypted) {
-#ifdef HAVE_LIBMBEDTLS
+#ifdef CONFIG_MBEDTLS
memset(&conn->dctx, 0, sizeof(mbedtls_aes_context));
mbedtls_aes_setkey_dec(&conn->dctx, conn->stream.aeskey, 128);
#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
memset(&conn->dctx, 0, sizeof(aes_context));
aes_setkey_dec(&conn->dctx, conn->stream.aeskey, 128);
#endif
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
AES_set_decrypt_key(conn->stream.aeskey, 128, &conn->aes);
#endif
}
@@ -1541,14 +1685,13 @@ void *player_thread_func(void *arg) {
debug(3, "Output frame bytes is %d.", conn->output_bytes_per_frame);
- // create and start the timing, control and audio receiver threads
- pthread_t rtp_audio_thread, rtp_control_thread, rtp_timing_thread;
- pthread_create(&rtp_audio_thread, NULL, &rtp_audio_receiver, (void *)conn);
- pthread_create(&rtp_control_thread, NULL, &rtp_control_receiver, (void *)conn);
- pthread_create(&rtp_timing_thread, NULL, &rtp_timing_receiver, (void *)conn);
+ conn->dac_buffer_queue_minimum_length = (int64_t)(
+ config.audio_backend_buffer_interpolation_threshold_in_seconds * config.output_rate);
+ debug(3, "dac_buffer_queue_minimum_length is %" PRId64 " frames.",
+ conn->dac_buffer_queue_minimum_length);
conn->session_corrections = 0;
- conn->play_segment_reference_frame = 0; // zero signals that we are not in a play segment
+ // conn->play_segment_reference_frame = 0; // zero signals that we are not in a play segment
// check that there are enough buffers to accommodate the desired latency and the latency offset
@@ -1573,7 +1716,13 @@ void *player_thread_func(void *arg) {
int32_t minimum_buffer_occupancy = INT32_MAX;
int32_t maximum_buffer_occupancy = INT32_MIN;
- time_t playstart = time(NULL);
+ conn->playstart = time(NULL);
+
+ conn->frame_rate = 0.0;
+ conn->frame_rate_status = 0;
+
+ conn->input_frame_rate = 0.0;
+ conn->input_frame_rate_starting_point_is_valid = 0;
conn->buffer_occupancy = 0;
@@ -1594,15 +1743,10 @@ void *player_thread_func(void *arg) {
static char rnstate[256];
initstate(time(NULL), rnstate, 256);
- signed short *inbuf, *tbuf;
-
- int32_t *sbuf;
-
- char *outbuf;
-
+ signed short *inbuf;
int inbuflength;
- int output_bit_depth = 16; // default;
+ unsigned int output_bit_depth = 16; // default;
switch (config.output_format) {
case SPS_FORMAT_S8:
@@ -1610,56 +1754,76 @@ void *player_thread_func(void *arg) {
output_bit_depth = 8;
break;
case SPS_FORMAT_S16:
+ case SPS_FORMAT_S16_LE:
+ case SPS_FORMAT_S16_BE:
output_bit_depth = 16;
break;
case SPS_FORMAT_S24:
+ case SPS_FORMAT_S24_LE:
+ case SPS_FORMAT_S24_BE:
case SPS_FORMAT_S24_3LE:
case SPS_FORMAT_S24_3BE:
output_bit_depth = 24;
break;
case SPS_FORMAT_S32:
+ case SPS_FORMAT_S32_LE:
+ case SPS_FORMAT_S32_BE:
output_bit_depth = 32;
break;
case SPS_FORMAT_UNKNOWN:
die("Unknown format choosing output bit depth");
+ break;
+ case SPS_FORMAT_AUTO:
+ die("Invalid format -- SPS_FORMAT_AUTO -- choosing output bit depth");
+ break;
+ case SPS_FORMAT_INVALID:
+ die("Invalid format -- SPS_FORMAT_INVALID -- choosing output bit depth");
+ break;
}
debug(3, "Output bit depth is %d.", output_bit_depth);
if (conn->input_bit_depth > output_bit_depth) {
- debug(1, "Dithering will be enabled because the input bit depth is greater than the output bit "
+ debug(3, "Dithering will be enabled because the input bit depth is greater than the output bit "
"depth");
}
- if (conn->fix_volume != 0x10000) {
- debug(1, "Dithering will be enabled because the output volume is being altered in software");
+ if (config.output->parameters == NULL) {
+ debug(3, "Dithering will be enabled because the output volume is being altered in software");
}
+ if ((config.output->parameters == NULL) || (conn->input_bit_depth > output_bit_depth) ||
+ (config.playback_mode == ST_mono))
+ conn->enable_dither = 1;
+
+
+ // remember, the output device may never have been initialised prior to this call
+ config.output->start(config.output_rate, config.output_format); // will need a corresponding stop
+
// 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));
- tbuf = malloc(
- sizeof(int32_t) * 2 *
- (conn->max_frames_per_packet * conn->output_sample_ratio + conn->max_frame_size_change));
- if (tbuf == NULL)
+ 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.");
- sbuf = 0;
// initialise this, because soxr stuffing might be chosen later
- sbuf = malloc(
- sizeof(int32_t) * 2 *
- (conn->max_frames_per_packet * conn->output_sample_ratio + conn->max_frame_size_change));
- if (sbuf == NULL)
- debug(1, "Failed to allocate memory for the sbuf buffer.");
+ 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.");
// The size of these dependents on the number of frames, the size of each frame and the maximum
// size change
- outbuf = malloc(
+ conn->outbuf = malloc(
conn->output_bytes_per_frame *
(conn->max_frames_per_packet * conn->output_sample_ratio + conn->max_frame_size_change));
- if (outbuf == NULL)
+ if (conn->outbuf == NULL)
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;
@@ -1668,20 +1832,6 @@ void *player_thread_func(void *arg) {
int sync_error_out_of_bounds =
0; // number of times in a row that there's been a serious sync error
-// stop looking elsewhere for DACP stuff
-#ifdef HAVE_DACP_CLIENT
- set_dacp_server_information(conn); // this will start scanning when a port is registered by the
- // code initiated by the mdns_dacp_monitor
-#else
- // this is only used for compatability, if dacp stuff isn't enabled.
- // start an mdns/zeroconf thread to look for DACP messages containing our DACP_ID and getting the
- // port number
- if (conn->dapo_private_storage)
- debug(1, "DACP monitor already initialised?");
- else
- conn->dapo_private_storage = mdns_dacp_monitor(conn->dacp_id);
-#endif
-
conn->framesProcessedInThisEpoch = 0;
conn->framesGeneratedInThisEpoch = 0;
conn->correctionsRequestedInThisEpoch = 0;
@@ -1699,7 +1849,13 @@ void *player_thread_func(void *arg) {
"resend requests, "
"min DAC queue size, "
"min buffer occupancy, "
- "max 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");
} else {
inform("sync error in milliseconds, "
"total packets, "
@@ -1709,7 +1865,11 @@ void *player_thread_func(void *arg) {
"resend requests, "
"min DAC queue size, "
"min buffer occupancy, "
- "max buffer occupancy");
+ "max buffer occupancy, "
+ "source nominal frames per second, "
+ "source actual frames per second, "
+ "source clock drift in ppm, "
+ "source clock drift sample count");
}
} else {
inform("sync error in milliseconds, "
@@ -1719,18 +1879,42 @@ void *player_thread_func(void *arg) {
"too late packets, "
"resend requests, "
"min buffer occupancy, "
- "max buffer occupancy");
+ "max buffer occupancy, "
+ "source nominal frames per second, "
+ "source actual frames per second, "
+ "source clock drift in ppm, "
+ "source clock drift sample count");
}
}
- // set the default volume to whaterver it was before, as stored in the config airplay_volume
- debug(3, "Set initial volume to %f.", config.airplay_volume);
+ // create and start the timing, control and audio receiver threads
+ pthread_create(&conn->rtp_audio_thread, NULL, &rtp_audio_receiver, (void *)conn);
+ pthread_create(&conn->rtp_control_thread, NULL, &rtp_control_receiver, (void *)conn);
+ pthread_create(&conn->rtp_timing_thread, NULL, &rtp_timing_receiver, (void *)conn);
+
+ pthread_cleanup_push(player_thread_cleanup_handler, arg); // undo what's been done so far
+
+ // stop looking elsewhere for DACP stuff
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
+
+#ifdef CONFIG_DACP_CLIENT
+ set_dacp_server_information(conn);
+#else
+ mdns_dacp_monitor_set_id(conn->dacp_id);
+#endif
+
+ pthread_setcancelstate(oldState, NULL);
- player_volume(config.airplay_volume, conn);
- int64_t frames_to_drop = 0;
- // debug(1, "Play begin");
- while (!conn->player_thread_please_stop) {
- abuf_t *inframe = buffer_get_frame(conn);
+ // set the default volume to whatever it was before, as stored in the config airplay_volume
+ debug(2, "Set initial volume to %f.", config.airplay_volume);
+ player_volume(config.airplay_volume, conn); // will contain a cancellation point if asked to wait
+
+ debug(2, "Play begin");
+ while (1) {
+ pthread_testcancel(); // allow a pthread_cancel request to take effect.
+ abuf_t *inframe = buffer_get_frame(conn); // this has cancellation point(s), but it's not
+ // guaranteed that they'll aways be executed
if (inframe) {
inbuf = inframe->data;
inbuflength = inframe->length;
@@ -1739,7 +1923,7 @@ void *player_thread_func(void *arg) {
// if (play_number % 100 == 0)
// debug(3, "Play frame %d.", play_number);
conn->play_number_after_flush++;
- if (inframe->timestamp == 0) {
+ if (inframe->given_timestamp == 0) {
debug(3, "Player has supplied a silent frame, (possibly frame %u) for play number %d.",
SUCCESSOR(conn->last_seqno_read), play_number);
conn->last_seqno_read = (SUCCESSOR(conn->last_seqno_read) &
@@ -1752,9 +1936,9 @@ void *player_thread_func(void *arg) {
} else {
// the player may change the contents of the buffer, so it has to be zeroed each time;
// might as well malloc and freee it locally
- memset(silence, 0,
- conn->output_bytes_per_frame * conn->max_frames_per_packet *
- conn->output_sample_ratio);
+ conn->previous_random_number = generate_zero_frames(
+ silence, conn->max_frames_per_packet * conn->output_sample_ratio,
+ config.output_format, conn->enable_dither, conn->previous_random_number);
config.output->play(silence, conn->max_frames_per_packet * conn->output_sample_ratio);
free(silence);
}
@@ -1774,28 +1958,20 @@ void *player_thread_func(void *arg) {
} else {
// the player may change the contents of the buffer, so it has to be zeroed each time;
// might as well malloc and freee it locally
- memset(silence, 0,
- conn->output_bytes_per_frame * conn->max_frames_per_packet *
- conn->output_sample_ratio);
+ conn->previous_random_number = generate_zero_frames(
+ silence, conn->max_frames_per_packet * conn->output_sample_ratio,
+ config.output_format, conn->enable_dither, conn->previous_random_number);
config.output->play(silence, conn->max_frames_per_packet * conn->output_sample_ratio);
free(silence);
}
- } else if (frames_to_drop) {
- if (frames_to_drop > 3 * config.output_rate) {
- warn("Shome mhistake shurely: very large number of frames to drop: %" PRId64
- " -- setting it to %" PRId64 ".",
- frames_to_drop, 3 * config.output_rate);
- frames_to_drop = 3 * config.output_rate;
- }
- debug(3, "%" PRId64 " frames to drop.", frames_to_drop);
- frames_to_drop -= inframe->length;
- if (frames_to_drop < 0)
- frames_to_drop = 0;
} else {
- int enable_dither = 0;
- if ((conn->fix_volume != 0x10000) || (conn->input_bit_depth > output_bit_depth) ||
- (config.playback_mode == ST_mono))
- enable_dither = 1;
+
+ if (((config.output->parameters == NULL) && (config.ignore_volume_control == 0) &&
+ (config.airplay_volume != 0.0)) ||
+ (conn->input_bit_depth > output_bit_depth) || (config.playback_mode == ST_mono))
+ conn->enable_dither = 1;
+ else
+ conn->enable_dither = 0;
// here, let's transform the frame of data, if necessary
@@ -1806,7 +1982,7 @@ void *player_thread_func(void *arg) {
int32_t ll = 0, rl = 0;
int16_t *inps = inbuf;
// int16_t *outps = tbuf;
- int32_t *outpl = (int32_t *)tbuf;
+ int32_t *outpl = (int32_t *)conn->tbuf;
for (i = 0; i < inbuflength; i++) {
ls = *inps++;
rs = *inps++;
@@ -1875,14 +2051,15 @@ void *player_thread_func(void *arg) {
// now, go back as far as the total latency less, say, 100 ms, and check the presence of
// frames from then onwards
- int64_t reference_timestamp;
+ 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
- reference_timestamp *= conn->output_sample_ratio;
int64_t rt, nt;
- rt = reference_timestamp; // uint32_t to int64_t
- nt = inframe->timestamp; // uint32_t to int64_t
+ rt = reference_timestamp; // uint32_t to int64_t
+ 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;
@@ -1890,10 +2067,12 @@ void *player_thread_func(void *arg) {
// uint64_t
// local_time_now=((uint64_t)tn.tv_sec<<32)+((uint64_t)tn.tv_nsec<<32)/1000000000;
- int64_t td = 0;
- int64_t td_in_frames = 0;
+ 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) {
- // debug(1,"td is %lld.",td);
td = local_time_now - reference_timestamp_time; // this is the positive value.
// Conversion is positive uint64_t to
// int64_t, thus okay
@@ -1923,9 +2102,8 @@ 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...
}
@@ -1951,10 +2129,10 @@ void *player_thread_func(void *arg) {
if (config.output->delay) {
long l_delay;
resp = config.output->delay(&l_delay);
- current_delay = l_delay;
if (resp == 0) { // no error
+ current_delay = l_delay;
if (current_delay < 0) {
- debug(1, "Underrun of %lld frames reported, but ignored.", current_delay);
+ debug(2, "Underrun of %lld frames reported, but ignored.", current_delay);
current_delay =
0; // could get a negative value if there was underrun, but ignore it.
}
@@ -1962,15 +2140,49 @@ void *player_thread_func(void *arg) {
minimum_dac_queue_size = current_delay; // update for display later
}
} else {
- debug(2, "Delay error %d when checking running latency.", resp);
+ current_delay = 0;
+ if ((resp == sps_extra_code_output_stalled) &&
+ (conn->unfixable_error_reported == 0)) {
+ conn->unfixable_error_reported = 1;
+ if (config.cmd_unfixable) {
+ warn("Connection %d: An unfixable error has been detected -- output device is "
+ "stalled. Executing the "
+ "\"run_this_if_an_unfixable_error_is_detected\" command.",
+ conn->connection_number);
+ command_execute(config.cmd_unfixable, "output_device_stalled", 1);
+ } else {
+ warn("Connection %d: An unfixable error has been detected -- output device is "
+ "stalled. \"No "
+ "run_this_if_an_unfixable_error_is_detected\" command provided -- nothing "
+ "done.",
+ conn->connection_number);
+ }
+ } else
+ debug(3, "Delay error %d when checking running latency.", resp);
}
}
- if (resp >= 0) {
+ if (resp == 0) {
+
+ 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;
+
+ // 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 = td_in_frames + rt - (nt - current_delay); // all int64_t
+
+ // 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
+
+ // td_in_frames + rt is the frame number that should be output at local_time_now.
// This is the timing error for the next audio frame in the DAC.
@@ -1979,25 +2191,30 @@ void *player_thread_func(void *arg) {
// if negative, the packet will be early -- the delay is less than expected.
sync_error =
- delay - (conn->latency * conn->output_sample_ratio +
+ 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);
+
// 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)
abs_sync_error = -abs_sync_error;
- if ((config.no_sync == 0) && (inframe->timestamp != 0) &&
- (!conn->player_thread_please_stop) && (config.resyncthreshold > 0.0) &&
+ if ((config.no_sync == 0) && (inframe->given_timestamp != 0) &&
+ (config.resyncthreshold > 0.0) &&
(abs_sync_error > config.resyncthreshold * config.output_rate)) {
+ /*
if (abs_sync_error > 3 * config.output_rate) {
- warn("Very large sync error: %" PRId64 " frames, with delay: %" PRId64
- ", td_in_frames: %" PRId64 ", rt: %" PRId64 ", nt: %" PRId64
- ", current_delay: %" PRId64 ", seqno: %u, given timestamp: %" PRIu32 ".",
- sync_error, delay, td_in_frames, rt, nt, current_delay,
- inframe->sequence_number, inframe->given_timestamp);
+
+ warn("Very large sync error: %" PRId64 " frames, with should_be_frame: %" PRId64
+ ", nt: %" PRId64 ", current_delay: %" PRId64 ", given timestamp %" PRIX32
+ ", reference timestamp %" PRIX32 ", should_be_frame %" PRIX32 ".",
+ sync_error, should_be_frame, nt, current_delay, inframe->given_timestamp,
+ reference_timestamp, should_be_frame_32);
}
+ */
sync_error_out_of_bounds++;
} else {
sync_error_out_of_bounds = 0;
@@ -2009,28 +2226,50 @@ void *player_thread_func(void *arg) {
// sync_error_out_of_bounds, sync_error);
sync_error_out_of_bounds = 0;
- int filler_length = config.resyncthreshold * config.output_rate; // number of samples
+ int64_t filler_length =
+ (int64_t)(config.resyncthreshold * config.output_rate); // number of samples
if ((sync_error > 0) && (sync_error > filler_length)) {
- // debug(1, "Large positive sync error: %lld.", sync_error);
- frames_to_drop = sync_error / conn->output_sample_ratio;
+ debug(2, "Large positive sync error: %" PRId64 ".", sync_error);
+ int64_t local_frames_to_drop = sync_error / conn->output_sample_ratio;
+ uint32_t frames_to_drop_sized = local_frames_to_drop;
+
+ debug_mutex_lock(&conn->flush_mutex, 1000, 1);
+ conn->flush_rtp_timestamp =
+ inframe->given_timestamp +
+ frames_to_drop_sized; // flush all packets up to (and including?) this
+ reset_input_flow_metrics(conn);
+ debug_mutex_unlock(&conn->flush_mutex, 3);
+
} else if ((sync_error < 0) && ((-sync_error) > filler_length)) {
- // debug(1, "Large negative sync error: %lld. Inserting silence.", sync_error);
- int silence_length = -sync_error;
+ debug(2,
+ "Large negative sync error: %" PRId64 " with should_be_frame_32 of %" PRIu32
+ ", nt of %" PRId64 " and current_delay of %" PRId64 ".",
+ sync_error, should_be_frame_32, nt, current_delay);
+ int64_t silence_length = -sync_error;
if (silence_length > (filler_length * 5))
silence_length = filler_length * 5;
+ size_t silence_length_sized = silence_length;
+ char *long_silence = malloc(conn->output_bytes_per_frame * silence_length_sized);
+ if (long_silence) {
- char *long_silence = malloc(conn->output_bytes_per_frame * silence_length);
- if (long_silence == NULL)
- die("Failed to allocate memory for a long_silence buffer of %d frames.",
- silence_length);
- memset(long_silence, 0, conn->output_bytes_per_frame * silence_length);
- config.output->play(long_silence, silence_length);
- free(long_silence);
+ conn->previous_random_number =
+ generate_zero_frames(long_silence, silence_length_sized, config.output_format,
+ conn->enable_dither, conn->previous_random_number);
+
+ debug(2, "Play a silence of %d frames.", silence_length_sized);
+ config.output->play(long_silence, silence_length_sized);
+ free(long_silence);
+ } else {
+ warn("Failed to allocate memory for a long_silence buffer of %d frames for a "
+ "sync error of %d frames.",
+ silence_length_sized, sync_error);
+ }
+ reset_input_flow_metrics(conn);
}
} else {
+ /*
// before we finally commit to this frame, check its sequencing and timing
-
// require a certain error before bothering to fix it...
if (sync_error > config.tolerance * config.output_rate) { // int64_t > int, okay
amount_to_stuff = -1;
@@ -2038,11 +2277,24 @@ void *player_thread_func(void *arg) {
if (sync_error < -config.tolerance * config.output_rate) {
amount_to_stuff = 1;
}
+ */
- // only allow stuffing if there is enough time to do it -- check DAC buffer...
- if (current_delay < DAC_BUFFER_QUEUE_MINIMUM_LENGTH) {
- // debug(2,"DAC buffer too short(at %lld frames) to allow stuffing.",current_delay);
- amount_to_stuff = 0;
+ if (amount_to_stuff == 0) {
+ // use a "V" shaped function to decide if stuffing should occur
+ int64_t s = r64i();
+ s = s >> 31;
+ s = s * config.tolerance * config.output_rate;
+ s = (s >> 32) + config.tolerance * config.output_rate; // should be a number from 0
+ // to config.tolerance *
+ // config.output_rate;
+ if ((sync_error > 0) && (sync_error > s)) {
+ // debug(1,"Extra stuff -1");
+ amount_to_stuff = -1;
+ }
+ if ((sync_error < 0) && (sync_error < (-s))) {
+ // debug(1,"Extra stuff +1");
+ amount_to_stuff = 1;
+ }
}
// try to keep the corrections definitely below 1 in 1000 audio frames
@@ -2076,8 +2328,8 @@ void *player_thread_func(void *arg) {
#ifdef CONFIG_CONVOLUTION
|| config.convolution
#endif
- ) {
- int32_t *tbuf32 = (int32_t *)tbuf;
+ ) {
+ int32_t *tbuf32 = (int32_t *)conn->tbuf;
float fbuf_l[inbuflength];
float fbuf_r[inbuflength];
@@ -2123,22 +2375,24 @@ void *player_thread_func(void *arg) {
}
}
- switch (config.packet_stuffing) {
- case ST_basic:
- // if (amount_to_stuff) debug(1,"Basic stuff...");
- play_samples =
- stuff_buffer_basic_32((int32_t *)tbuf, inbuflength, config.output_format,
- outbuf, amount_to_stuff, enable_dither, conn);
- break;
- case ST_soxr:
-#ifdef HAVE_LIBSOXR
- // if (amount_to_stuff) debug(1,"Soxr stuff...");
- play_samples = stuff_buffer_soxr_32((int32_t *)tbuf, (int32_t *)sbuf, inbuflength,
- config.output_format, outbuf, amount_to_stuff,
- enable_dither, conn);
+#ifdef CONFIG_SOXR
+ if ((current_delay < conn->dac_buffer_queue_minimum_length) ||
+ (config.packet_stuffing == ST_basic) ||
+ (config.soxr_delay_index == 0) || // not computed yet
+ ((config.packet_stuffing == ST_auto) && (config.soxr_delay_index > config.soxr_delay_threshold)) // if the CPU is deemed too slow
+ ) {
#endif
- break;
+ play_samples =
+ stuff_buffer_basic_32((int32_t *)conn->tbuf, inbuflength, config.output_format,
+ conn->outbuf, amount_to_stuff, conn->enable_dither, conn);
+#ifdef CONFIG_SOXR
}
+ else { // soxr requested or auto requested with the index less or equal to the threshold
+ play_samples = stuff_buffer_soxr_32((int32_t *)conn->tbuf, (int32_t *)conn->sbuf,
+ inbuflength, config.output_format, conn->outbuf,
+ amount_to_stuff, conn->enable_dither, conn);
+ }
+#endif
/*
{
@@ -2155,20 +2409,25 @@ void *player_thread_func(void *arg) {
}
*/
- if (outbuf == NULL)
+ if (conn->outbuf == NULL)
debug(1, "NULL outbuf to play -- skipping it.");
else {
if (play_samples == 0)
debug(1, "play_samples==0 skipping it (1).");
- else
- config.output->play(outbuf, play_samples);
+ else {
+ if (conn->software_mute_enabled) {
+ generate_zero_frames(conn->outbuf, play_samples, config.output_format,
+ conn->enable_dither, conn->previous_random_number);
+ }
+ config.output->play(conn->outbuf, play_samples);
+ }
}
// check for loss of sync
// timestamp of zero means an inserted silent frame in place of a missing frame
/*
if ((config.no_sync == 0) && (inframe->timestamp != 0) &&
- (!conn->player_thread_please_stop) && (config.resyncthreshold > 0.0) &&
+ && (config.resyncthreshold > 0.0) &&
(abs_sync_error > config.resyncthreshold * config.output_rate)) {
sync_error_out_of_bounds++;
// debug(1,"Sync error out of bounds: Error: %lld; previous error: %lld; DAC: %lld;
@@ -2190,18 +2449,32 @@ 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
- play_samples = stuff_buffer_basic_32((int32_t *)tbuf, inbuflength, config.output_format,
- outbuf, 0, enable_dither, conn);
- if (outbuf == NULL)
+ play_samples =
+ stuff_buffer_basic_32((int32_t *)conn->tbuf, inbuflength, config.output_format,
+ conn->outbuf, 0, conn->enable_dither, conn);
+ if (conn->outbuf == NULL)
debug(1, "NULL outbuf to play -- skipping it.");
- else
- config.output->play(outbuf, play_samples); // remove the (short*)!
+ else {
+ if (conn->software_mute_enabled) {
+ generate_zero_frames(conn->outbuf, play_samples, config.output_format,
+ conn->enable_dither, conn->previous_random_number);
+ }
+ config.output->play(conn->outbuf, play_samples); // remove the (short*)!
+ }
}
// mark the frame as finished
- inframe->timestamp = 0;
+ inframe->given_timestamp = 0;
inframe->sequence_number = 0;
+ // update the watchdog
+ if ((config.dont_check_timeout == 0) && (config.timeout != 0)) {
+ uint64_t time_now = get_absolute_time_in_fp();
+ debug_mutex_lock(&conn->watchdog_mutex, 1000, 0);
+ conn->watchdog_bark_time = time_now;
+ debug_mutex_unlock(&conn->watchdog_mutex, 0);
+ }
+
// debug(1,"Sync error %lld frames. Amount to stuff %d." ,sync_error,amount_to_stuff);
// new stats calculation. We want a running average of sync error, drift, adjustment,
@@ -2252,6 +2525,48 @@ void *player_thread_func(void *arg) {
}
}
if (play_number % print_interval == 0) {
+
+ // here, calculate the input and output frame rates, where possible, even if statistics
+ // have not been requested
+ // this is to calculate them in case they are needed by the D-Bus interface or elsewhere.
+
+ if (conn->input_frame_rate_starting_point_is_valid) {
+ uint64_t elapsed_reception_time, frames_received;
+ elapsed_reception_time =
+ conn->frames_inward_measurement_time - conn->frames_inward_measurement_start_time;
+ 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) /
+ 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;
+ }
+
+ if ((config.output->delay) && (config.no_sync == 0) && (config.output->rate_info)) {
+ uint64_t elapsed_play_time, frames_played;
+ if (config.output->rate_info(&elapsed_play_time, &frames_played) == 0)
+ conn->frame_rate_status = 1;
+ else
+ conn->frame_rate_status = 0;
+ if (conn->frame_rate_status) {
+ conn->frame_rate =
+ (1.0 * 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;
+ }
+ }
+
// we can now calculate running averages for sync error (frames), corrections (ppm),
// insertions plus deletions (ppm), drift (ppm)
double moving_average_sync_error = (1.0 * tsum_of_sync_errors) / number_of_statistics;
@@ -2262,61 +2577,89 @@ void *player_thread_func(void *arg) {
// if ((play_number/print_interval)%20==0)
if (config.statistics_requested) {
if (at_least_one_frame_seen) {
+
if ((config.output->delay)) {
if (config.no_sync == 0) {
- inform(
- " %*.1f," /* Sync error in milliseconds */
- "%*.1f," /* net correction in ppm */
- "%*.1f," /* corrections in ppm */
- "%*d," /* total packets */
- "%*llu," /* missing packets */
- "%*llu," /* late packets */
- "%*llu," /* too late packets */
- "%*llu," /* resend requests */
- "%*lli," /* min DAC queue size */
- "%*d," /* min buffer occupancy */
- "%*d", /* max buffer occupancy */
- 9, /* should be 10, but there's an explicit space at the start to ensure
- alignment */
- 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);
+ 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);
} else {
- inform(" %*.1f," /* Sync error in milliseconds */
- "%*d," /* total packets */
- "%*llu," /* missing packets */
- "%*llu," /* late packets */
- "%*llu," /* too late packets */
- "%*llu," /* resend requests */
- "%*lli," /* min DAC queue size */
- "%*d," /* min buffer occupancy */
- "%*d", /* max buffer occupancy */
- 9, /* should be 10, but there's an explicit space at the start to ensure
- alignment */
+ inform("%*.2f," /* Sync error in milliseconds */
+ "%*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," /* 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,
- minimum_buffer_occupancy, 5, maximum_buffer_occupancy);
+ 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(" %*.1f," /* Sync error in milliseconds */
- "%*d," /* total packets */
- "%*llu," /* missing packets */
- "%*llu," /* late packets */
- "%*llu," /* too late packets */
- "%*llu," /* resend requests */
- "%*d," /* min buffer occupancy */
- "%*d", /* max buffer occupancy */
- 9, /* should be 10, but there's an explicit space at the start to ensure
- alignment */
+ inform("%*.2f," /* Sync error in milliseconds */
+ "%*d," /* total packets */
+ "%*" PRIu64 "," /* missing packets */
+ "%*" PRIu64 "," /* late packets */
+ "%*" PRIu64 "," /* too late packets */
+ "%*" PRIu64 "," /* resend requests */
+ "%*" 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, 5, minimum_buffer_occupancy, 5,
- maximum_buffer_occupancy);
+ 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("No frames received in the last sampling interval.");
@@ -2331,307 +2674,223 @@ void *player_thread_func(void *arg) {
}
}
- debug(3, "Connection %d: player thread main loop exit.", conn->connection_number);
-
- if (config.statistics_requested) {
- int rawSeconds = (int)difftime(time(NULL), playstart);
- int elapsedHours = rawSeconds / 3600;
- int elapsedMin = (rawSeconds / 60) % 60;
- int elapsedSec = rawSeconds % 60;
- inform("Playback Stopped. Total playing time %02d:%02d:%02d.", elapsedHours, elapsedMin,
- elapsedSec);
- }
-
-#ifndef HAVE_DACP_CLIENT
- // stop watching for DACP port number stuff
- // this is only used for compatability, if dacp stuff isn't enabled.
- if (conn->dapo_private_storage) {
- mdns_dacp_dont_monitor(conn->dapo_private_storage);
- conn->dapo_private_storage = NULL;
- } else {
- debug(2, "DACP Monitor already stopped");
- }
-#endif
-
- debug(3, "Connection %d: stopping output device.", conn->connection_number);
-
- if (config.output->stop)
- config.output->stop();
-
- debug(2, "Cancelling timing, control and audio threads...");
- debug(2, "Cancel timing thread.");
- pthread_cancel(rtp_timing_thread);
- debug(2, "Join timing thread.");
- pthread_join(rtp_timing_thread, NULL);
- debug(2, "Timing thread terminated.");
- debug(2, "Cancel control thread.");
- pthread_cancel(rtp_control_thread);
- debug(2, "Join control thread.");
- pthread_join(rtp_control_thread, NULL);
- debug(2, "Control thread terminated.");
- debug(2, "Cancel audio thread.");
- pthread_cancel(rtp_audio_thread);
- debug(2, "Join audio thread.");
- pthread_join(rtp_audio_thread, NULL);
- debug(2, "Audio thread terminated.");
- clear_reference_timestamp(conn);
- conn->rtp_running = 0;
-
- debug(2, "Freeing audio buffers and decoders.");
-
- free_audio_buffers(conn);
- terminate_decoders(conn);
- // remove flow control and mutexes
- rc = pthread_cond_destroy(&conn->flowcontrol);
- if (rc)
- debug(1, "Error destroying flowcontrol condition variable.");
- rc = pthread_mutex_destroy(&conn->flush_mutex);
- if (rc)
- debug(1, "Error destroying flush_mutex variable.");
- rc = pthread_mutex_destroy(&conn->ab_mutex);
- if (rc)
- debug(1, "Error destroying ab_mutex variable.");
- debug(2, "Connection %d: player thread terminated.", conn->connection_number);
- if (conn->dacp_id) {
- free(conn->dacp_id);
- conn->dacp_id = NULL;
- }
- if (outbuf)
- free(outbuf);
- if (tbuf)
- free(tbuf);
- if (sbuf)
- free(sbuf);
+ debug(1, "This should never be called.");
+ pthread_cleanup_pop(1); // pop the cleanup handler
+ // debug(1, "This should never be called either.");
+ // pthread_cleanup_pop(1); // pop the initial cleanup handler
pthread_exit(NULL);
}
-// takes the volume as specified by the airplay protocol
void player_volume_without_notification(double airplay_volume, rtsp_conn_info *conn) {
+ debug_mutex_lock(&conn->volume_control_mutex, 5000, 1);
+ debug(2, "player_volume_without_notification %f", airplay_volume);
+ // first, see if we are hw only, sw only, both with hw attenuation on the top or both with sw
+ // attenuation on top
+
+ enum volume_mode_type { vol_sw_only, vol_hw_only, vol_both } volume_mode;
- // The volume ranges -144.0 (mute) or -30 -- 0. See
- // http://git.zx2c4.com/Airtunes2/about/#setting-volume
- // By examination, the -30 -- 0 range is linear on the slider; i.e. the slider is calibrated in 30
- // equal increments. Since the human ear's response is roughly logarithmic, we imagine these to
- // be power dB, i.e. from -30dB to 0dB.
-
- // We may have a hardware mixer, and if so, we will give it priority.
- // If a desired volume range is given, then we will try to accommodate it from
- // the top of the hardware mixer's range downwards.
-
- // If no desired volume range is given, we will use the native resolution of the hardware mixer,
- // if any,
- // or failing that, the software mixer. The software mixer has a range of from -96.3 dB up to 0
- // dB,
- // corresponding to a multiplier of 1 to 65535.
-
- // Otherwise, we will accommodate the desired volume range in the combination of the software and
- // hardware mixer
- // Intuitively (!), it seems best to give the hardware mixer as big a role as possible, so
- // we will use its full range and then accommodate the rest of the attenuation in software.
- // A problem is that we don't know whether the lowest hardware volume actually mutes the output
- // so we must assume that it does, and for this reason, the volume control goes at the "bottom" of
- // the adjustment range
-
- // The dB range of a value from 1 to 65536 is about 96.3 dB (log10 of 65536 is 4.8164).
- // Since the levels correspond with amplitude, they correspond to voltage, hence voltage dB,
- // or 20 times the log of the ratio. Then multiplied by 100 for convenience.
- // Thus, we ask our vol2attn function for an appropriate dB between -96.3 and 0 dB and translate
- // it back to a number.
-
- int32_t hw_min_db, hw_max_db, hw_range_db, min_db,
- max_db; // hw_range_db is a flag; if 0 means no mixer
+ // take account of whether there is a hardware mixer, if a max volume has been specified and if a
+ // range has been specified
+ // the range might imply that both hw and software mixers are needed, so calculate this
+ 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;
- // have a hardware mixer
config.output->parameters(&audio_information);
hw_max_db = audio_information.maximum_volume_dB;
hw_min_db = audio_information.minimum_volume_dB;
- hw_range_db = hw_max_db - hw_min_db;
- } else {
- // don't have a hardware mixer
- hw_max_db = hw_min_db = hw_range_db = 0;
- }
-
- int32_t sw_min_db = -9630;
- int32_t sw_max_db = 0;
- int32_t sw_range_db = sw_max_db - sw_min_db;
- int32_t desired_range_db = 0; // this is also used as a flag; if 0 means no desired range
-
- if (config.volume_range_db)
- desired_range_db = (int32_t)trunc(config.volume_range_db * 100);
-
- // This is wrong, I think -- it doesn't work properly if the volume range is composite and you
- // want to set a maximum value which should affect the hardware mixer.
-
- if (config.volume_max_db_set) {
- if (hw_range_db) {
- if (((config.volume_max_db * 100) < hw_max_db) &&
- ((config.volume_max_db * 100) > hw_min_db)) {
- hw_max_db = (int)config.volume_max_db * 100;
- hw_range_db = hw_max_db - hw_min_db;
- } else {
- inform("The volume_max_db setting is out of range of the hardware mixers's limits of %d dB "
- "to %d dB. It will be ignored.",
- (int)(hw_max_db / 100), (int)(hw_min_db / 100));
- }
- } else {
- if (((config.volume_max_db * 100) < sw_max_db) &&
- ((config.volume_max_db * 100) > sw_min_db)) {
- sw_max_db = (int)config.volume_max_db * 100;
- sw_range_db = sw_max_db - sw_min_db;
+ if (config.volume_max_db_set) {
+ if (((config.volume_max_db * 100) <= hw_max_db) &&
+ ((config.volume_max_db * 100) >= hw_min_db))
+ hw_max_db = (int32_t)config.volume_max_db * 100;
+ else if (config.volume_range_db) {
+ hw_max_db = hw_min_db;
+ sw_max_db = (config.volume_max_db * 100) - hw_min_db;
} else {
- inform("The volume_max_db setting is out of range of the software attenuation's limits of "
- "0 dB to -96.3 dB. It will be ignored.");
+ warn("The maximum output level is outside the range of the hardware mixer -- ignored");
}
}
- }
- if (desired_range_db) {
- // debug(1,"An attenuation range of %d is requested.",desired_range_db);
- // we have a desired volume range.
- if (hw_range_db) {
- // we have a hardware mixer
- if (hw_range_db >= desired_range_db) {
- // the hardware mixer can accommodate the desired range
- max_db = hw_max_db;
- min_db = max_db - desired_range_db;
- } else {
- // we have a hardware mixer and a desired range greater than the mixer's range.
- if ((hw_range_db + sw_range_db) < desired_range_db) {
- inform("The volume attenuation range %f is greater than can be accommodated by the "
- "hardware and software -- set to %f.",
- config.volume_range_db, hw_range_db + sw_range_db);
- desired_range_db = hw_range_db + sw_range_db;
- }
- min_db = hw_min_db;
- max_db = min_db + desired_range_db;
- }
- } else {
- // we have a desired volume range and no hardware mixer
- if (sw_range_db < desired_range_db) {
- inform("The volume attenuation range %f is greater than can be accommodated by the "
- "software -- set to %f.",
- config.volume_range_db, sw_range_db);
- desired_range_db = sw_range_db;
+ // here, we have set limits on the hw_max_db and the sw_max_db
+ // but we haven't actually decided whether we need both hw and software attenuation
+ // only if a range is specified could we need both
+ if (config.volume_range_db) {
+ // see if the range requested exceeds the hardware range available
+ int32_t desired_range_db = (int32_t)trunc(config.volume_range_db * 100);
+ if ((desired_range_db) > (hw_max_db - hw_min_db)) {
+ volume_mode = vol_both;
+ int32_t desired_sw_range = desired_range_db - (hw_max_db - hw_min_db);
+ if ((sw_max_db - desired_sw_range) < sw_min_db)
+ warn("The range requested is too large to accommodate -- ignored.");
+ else
+ sw_min_db = (sw_max_db - desired_sw_range);
}
- max_db = sw_max_db;
- min_db = max_db - desired_range_db;
}
} else {
- // we do not have a desired volume range, so use the mixer's volume range, if there is one.
- // debug(1,"No attenuation range requested.");
- if (hw_range_db) {
- min_db = hw_min_db;
- max_db = hw_max_db;
- } else {
- min_db = sw_min_db;
- max_db = sw_max_db;
+ // debug(1,"has no hardware mixer");
+ volume_mode = vol_sw_only;
+ if (config.volume_max_db_set) {
+ if (((config.volume_max_db * 100) <= sw_max_db) &&
+ ((config.volume_max_db * 100) >= sw_min_db))
+ sw_max_db = (int32_t)config.volume_max_db * 100;
+ }
+ if (config.volume_range_db) {
+ // see if the range requested exceeds the software range available
+ int32_t desired_range_db = (int32_t)trunc(config.volume_range_db * 100);
+ if ((desired_range_db) > (sw_max_db - sw_min_db))
+ warn("The range requested is too large to accommodate -- ignored.");
+ else
+ sw_min_db = (sw_max_db - desired_range_db);
}
}
- /*
- if (config.volume_max_db_set) {
- if ((config.volume_max_db*100<=max_db) && (config.volume_max_db*100>=min_db)) {
- debug(1,"Reducing the maximum volume from %d to %d.",max_db/100,config.volume_max_db);
- max_db = (int)(config.volume_max_db*100);
- } else {
- inform("The value of volume_max_db is invalid. It must be in the range %d to
- %d.",max_db,min_db);
+ // here, we know whether it's hw volume control only, sw only or both, and we have the hw and sw
+ // limits.
+ // if it's both, we haven't decided whether hw or sw should be on top
+ // we have to consider the settings ignore_volume_control and mute.
+
+ if (config.ignore_volume_control == 0) {
+ 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.",
+ 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.",
+ volume_mode, airplay_volume);
}
- }
- */
- double hardware_attenuation = 0.0, software_attenuation = 0.0;
- double scaled_attenuation = hw_min_db + sw_min_db;
-
- // now, we can map the input to the desired output volume
- if ((airplay_volume == -144.0) && (config.ignore_volume_control == 0)) {
- // do a mute
- // needed even with hardware mute, as when sound is unmuted it might otherwise be very loud.
- hardware_attenuation = hw_min_db;
- if (config.output->mute) {
- // allow the audio material to reach the mixer, but mute the mixer
- // it the mute is removed externally, the material with be there
- software_attenuation = sw_max_db - (max_db - hw_max_db); // e.g. if the hw_max_db is +4 and
- // the max is +40, this will be -36
- // (all by 100, of course)
- // debug(1,"Mute, with hardware mute and software_attenuation set to
- // %d.",software_attenuation);
- config.output->mute(1); // use real mute if it's there
+
} else {
- if (config.output->volume == NULL) { // if there is also no hardware volume control
- software_attenuation = sw_min_db; // set any software output to zero too
- // debug(1,"Mute, with no hardware mute and software_attenuation set to
- // %d.",software_attenuation);
+ int32_t max_db = 0, min_db = 0;
+ switch (volume_mode) {
+ case vol_hw_only:
+ max_db = hw_max_db;
+ min_db = hw_min_db;
+ break;
+ case vol_sw_only:
+ max_db = sw_max_db;
+ min_db = sw_min_db;
+ break;
+ case vol_both:
+ // debug(1, "dB range passed is hw: %d, sw: %d, total: %d", hw_max_db - hw_min_db,
+ // sw_max_db - sw_min_db, (hw_max_db - hw_min_db) + (sw_max_db - sw_min_db));
+ max_db =
+ (hw_max_db - hw_min_db) + (sw_max_db - sw_min_db); // this should be the range requested
+ min_db = 0;
+ break;
+ default:
+ debug(1, "player_volume_without_notification: error: not in a volume mode");
+ break;
}
- }
- } else {
- if (config.output->mute)
- config.output->mute(0); // unmute mute if it's there
- if (config.ignore_volume_control == 1)
- scaled_attenuation = max_db;
- else if (config.volume_control_profile == VCP_standard)
- scaled_attenuation = vol2attn(airplay_volume, max_db, min_db);
- else if (config.volume_control_profile == VCP_flat)
- scaled_attenuation = flat_vol2attn(airplay_volume, max_db, min_db);
- else
- debug(1, "Unrecognised volume control profile");
+ double scaled_attenuation = 0.0;
+ if (config.volume_control_profile == VCP_standard)
+ scaled_attenuation = vol2attn(airplay_volume, max_db, min_db); // no cancellation points
+ else if (config.volume_control_profile == VCP_flat)
+ scaled_attenuation =
+ flat_vol2attn(airplay_volume, max_db, min_db); // no cancellation points
+ else
+ debug(1, "player_volume_without_notification: unrecognised volume control profile");
- if (hw_range_db) {
- // if there is a hardware mixer
- if (scaled_attenuation <= hw_max_db) {
- // the attenuation is so low that's it's in the hardware mixer's range
- // debug(1,"Attenuation all taken care of by the hardware mixer.");
+ // so here we have the scaled attenuation. If it's for hw or sw only, it's straightforward.
+ double hardware_attenuation = 0.0;
+ double software_attenuation = 0.0;
+
+ switch (volume_mode) {
+ case vol_hw_only:
hardware_attenuation = scaled_attenuation;
- software_attenuation = sw_max_db - (max_db - hw_max_db); // e.g. if the hw_max_db is +4 and
- // the max is +40, this will be -36
- // (all by 100, of course)
- } else {
- // debug(1,"Attenuation taken care of by hardware and software mixer.");
- hardware_attenuation = hw_max_db; // the hardware mixer is turned up full
- software_attenuation = sw_max_db - (max_db - scaled_attenuation);
+ break;
+ case vol_sw_only:
+ software_attenuation = scaled_attenuation;
+ break;
+ case vol_both:
+ // here, we now the attenuation required, so we have to apportion it to the sw and hw mixers
+ // if we give the hw priority, that means when lowering the volume, set the hw volume to its
+ // lowest
+ // before using the sw attenuation.
+ // similarly, if we give the sw priority, that means when lowering the volume, set the sw
+ // volume to its lowest
+ // before using the hw attenuation.
+ // one imagines that hw priority is likely to be much better
+ // if (config.volume_range_hw_priority) {
+ if (config.volume_range_hw_priority != 0) {
+ // hw priority
+ if ((sw_max_db - sw_min_db) > scaled_attenuation) {
+ software_attenuation = sw_min_db + scaled_attenuation;
+ hardware_attenuation = hw_min_db;
+ } else {
+ software_attenuation = sw_max_db;
+ hardware_attenuation = hw_min_db + scaled_attenuation - (sw_max_db - sw_min_db);
+ }
+ } else {
+ // sw priority
+ if ((hw_max_db - hw_min_db) > scaled_attenuation) {
+ hardware_attenuation = hw_min_db + scaled_attenuation;
+ software_attenuation = sw_min_db;
+ } else {
+ hardware_attenuation = hw_max_db;
+ software_attenuation = sw_min_db + scaled_attenuation - (hw_max_db - hw_min_db);
+ }
+ }
+ break;
+ default:
+ debug(1, "player_volume_without_notification: error: not in a volume mode");
+ break;
}
- } else {
- // if there is no hardware mixer, the scaled_volume is the software volume
- // debug(1,"Attenuation all taken care of by the software mixer.");
- software_attenuation = scaled_attenuation;
- }
- }
- if ((config.output->volume) && (hw_range_db)) {
- config.output->volume(hardware_attenuation); // otherwise set the output to the lowest value
- // debug(1,"Hardware attenuation set to %f for airplay volume of
- // %f.",hardware_attenuation,airplay_volume);
- }
- double temp_fix_volume = 65536.0 * pow(10, software_attenuation / 2000);
- // debug(1,"Software attenuation set to %f, i.e %f out of 65,536, for airplay volume of
- // %f",software_attenuation,temp_fix_volume,airplay_volume);
+ if (((volume_mode == vol_hw_only) || (volume_mode == vol_both)) && (config.output->volume)) {
+ config.output->volume(hardware_attenuation); // otherwise set the output to the lowest value
+ // debug(1,"Hardware attenuation set to %f for airplay volume of
+ // %f.",hardware_attenuation,airplay_volume);
+ if (volume_mode == vol_hw_only)
+ conn->fix_volume = 0x10000;
+ }
- conn->fix_volume = temp_fix_volume;
- memory_barrier();
+ if ((volume_mode == vol_sw_only) || (volume_mode == vol_both)) {
+ double temp_fix_volume = 65536.0 * pow(10, software_attenuation / 2000);
+ // debug(1,"Software attenuation set to %f, i.e %f out of 65,536, for airplay volume of
+ // %f",software_attenuation,temp_fix_volume,airplay_volume);
- if (config.loudness)
- loudness_set_volume(software_attenuation / 100);
+ conn->fix_volume = temp_fix_volume;
- if (config.logOutputLevel) {
- inform("Output Level set to: %.2f dB.", scaled_attenuation / 100.0);
- }
+ if (config.loudness)
+ loudness_set_volume(software_attenuation / 100);
+ }
+
+ if (config.logOutputLevel) {
+ inform("Output Level set to: %.2f dB.", scaled_attenuation / 100.0);
+ }
#ifdef CONFIG_METADATA
- char *dv = malloc(128); // will be freed in the metadata thread
- if (dv) {
- memset(dv, 0, 128);
- if (config.ignore_volume_control == 1)
- snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, 0.0, 0.0, 0.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);
- }
+ char *dv = malloc(128); // will be freed in the metadata thread
+ if (dv) {
+ memset(dv, 0, 128);
+ if (config.ignore_volume_control == 1)
+ snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, 0.0, 0.0, 0.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
+ // here, store the volume for possible use in the future
+
+ if (config.output->mute)
+ config.output->mute(0);
+ conn->software_mute_enabled = 0;
- // here, store the volume for possible use in the future
+ 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);
+ }
+ }
config.airplay_volume = airplay_volume;
+ debug_mutex_unlock(&conn->volume_control_mutex, 3);
}
void player_volume(double airplay_volume, rtsp_conn_info *conn) {
@@ -2639,37 +2898,30 @@ void player_volume(double airplay_volume, rtsp_conn_info *conn) {
player_volume_without_notification(airplay_volume, conn);
}
-void do_flush(int64_t timestamp, 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_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;
+ reset_input_flow_metrics(conn);
debug_mutex_unlock(&conn->flush_mutex, 3);
- conn->play_segment_reference_frame = 0;
- conn->play_number_after_flush = 0;
+ 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);
+ send_ssnc_metadata('pfls', NULL, 0, 1); // contains cancellation points
}
#endif
- debug(3, "Flush request made.");
-}
-
-void player_flush(int64_t timestamp, rtsp_conn_info *conn) {
- debug(3, "player_flush");
- if (pthread_rwlock_tryrdlock(&conn->player_thread_lock) == 0) {
- if (conn->player_thread != NULL)
- do_flush(timestamp, conn);
- else
- debug(1, "Flush requested when player thread is gone.");
- pthread_rwlock_unlock(&conn->player_thread_lock);
- } else {
- debug(3, "Can't acquire a read lock for a flush -- ignored.");
- }
}
int player_play(rtsp_conn_info *conn) {
@@ -2679,11 +2931,13 @@ int player_play(rtsp_conn_info *conn) {
if (config.buffer_start_fill > BUFFER_FRAMES)
die("specified buffer starting fill %d > buffer size %d", config.buffer_start_fill,
BUFFER_FRAMES);
+ activity_monitor_signify_activity(
+ 1); // active, and should be before play's command hook, command_start()
command_start();
-#ifdef CONFIG_METADATA
- debug(2, "pbeg");
- send_ssnc_metadata('pbeg', NULL, 0, 1);
-#endif
+ // call on the output device to prepare itself
+ if ((config.output) && (config.output->prepare))
+ config.output->prepare();
+
pthread_t *pt = malloc(sizeof(pthread_t));
if (pt == NULL)
die("Couldn't allocate space for pthread_t");
@@ -2694,37 +2948,48 @@ int player_play(rtsp_conn_info *conn) {
int rc = pthread_attr_setstacksize(&tattr, size);
if (rc)
debug(1, "Error setting stack size for player_thread: %s", strerror(errno));
-
- // hack alert -- the player thread itself releases the player_thread_lock rwlock as soon as it's
// finished initialising.
- pthread_create(pt, &tattr, player_thread_func, (void *)conn);
+ rc = pthread_create(pt, &tattr, 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
+#endif
return 0;
}
int player_stop(rtsp_conn_info *conn) {
+ // note -- this may be called from another connection thread.
+ // int dl = debuglev;
+ // debuglev = 3;
debug(3, "player_stop");
- pthread_rwlock_wrlock(&conn->player_thread_lock);
- debug(3, "player_thread_lock acquired");
if (conn->player_thread) {
- debug(3, "player_thread exists");
- conn->player_thread_please_stop = 1;
- pthread_cond_signal(&conn->flowcontrol); // tell it to give up
- pthread_kill(*conn->player_thread, SIGUSR1);
- debug(3, "player_thread signalled");
- pthread_join(*conn->player_thread, NULL);
+ debug(3, "player_thread cancel...");
+ pthread_cancel(*conn->player_thread);
+ debug(3, "player_thread join...");
+ if (pthread_join(*conn->player_thread, NULL) == -1) {
+ char errorstring[1024];
+ strerror_r(errno, (char *)errorstring, sizeof(errorstring));
+ debug(1, "Connection %d: error %d joining player thread: \"%s\".", conn->connection_number,
+ errno, (char *)errorstring);
+ } else {
+ debug(3, "player_thread joined.");
+ }
free(conn->player_thread);
conn->player_thread = NULL;
- pthread_rwlock_unlock(&conn->player_thread_lock);
#ifdef CONFIG_METADATA
debug(2, "pend");
- send_ssnc_metadata('pend', NULL, 0, 1);
+ send_ssnc_metadata('pend', NULL, 0, 1); // contains cancellation points
#endif
+ // debuglev = dl;
command_stop();
+ activity_monitor_signify_activity(0); // inactive, and should be after command_stop()
return 0;
} else {
- pthread_rwlock_unlock(&conn->player_thread_lock);
- debug(3, "player thread of RTSP conversation %d is already deleted.", conn->connection_number);
+ debug(3, "Connection %d: player thread already deleted.", conn->connection_number);
+ // debuglev = dl;
return -1;
}
}
diff --git a/player.h b/player.h
index 3274d64..cb197e4 100644
--- a/player.h
+++ b/player.h
@@ -7,30 +7,32 @@
#include "config.h"
#include "definitions.h"
-#ifdef HAVE_LIBMBEDTLS
+#ifdef CONFIG_MBEDTLS
#include <mbedtls/aes.h>
#include <mbedtls/havege.h>
#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
#include <polarssl/aes.h>
#include <polarssl/havege.h>
#endif
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
#include <openssl/aes.h>
#endif
#include "alac.h"
#include "audio.h"
-#define time_ping_history 8
+#define time_ping_history 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;
+ int sequence_number;
+ int chosen;
} time_ping_record;
typedef uint16_t seq_t;
@@ -38,7 +40,7 @@ typedef uint16_t seq_t;
typedef struct audio_buffer_entry { // decoded audio packets
int ready;
int resend_level;
- int64_t timestamp;
+ // int64_t timestamp;
seq_t sequence_number;
uint32_t given_timestamp; // for debugging and checking
signed short *data;
@@ -46,7 +48,7 @@ typedef struct audio_buffer_entry { // decoded audio packets
} abuf_t;
// default buffer size
-// This eeds to be a power of 2 because of the way BUFIDX(seqno) works.
+// 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
// buffers.
// For at least 10 seconds, you need to go to 2048.
@@ -59,43 +61,76 @@ typedef struct audio_buffer_entry { // decoded audio packets
#define BUFFER_FRAMES 1024
+enum audio_stream_type {
+ ast_unknown,
+ ast_uncompressed, // L16/44100/2
+ ast_apple_lossless,
+} ast_type;
+
typedef struct {
int encrypted;
uint8_t aesiv[16], aeskey[16];
int32_t fmtp[12];
+ enum audio_stream_type type;
} stream_cfg;
typedef struct {
- int connection_number; // for debug ID purposes, nothing else...
- int resend_interval; // this is really just for debugging
- int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency
- int64_t latency; // the actual latency used for this play session
- int64_t minimum_latency; // set if an a=min-latency: line appears in the ANNOUNCE message; zero
- // otherwise
- int64_t maximum_latency; // set if an a=max-latency: line appears in the ANNOUNCE message; zero
- // otherwise
+ int connection_number; // for debug ID purposes, nothing else...
+ int resend_interval; // this is really just for debugging
+ int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency
+ uint32_t latency; // the actual latency used for this play session
+ uint32_t minimum_latency; // set if an a=min-latency: line appears in the ANNOUNCE message; zero
+ // otherwise
+ 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 fd;
- int authorized; // set if a password is required and has been supplied
+ int authorized; // set if a password is required and has been supplied
+ char *auth_nonce; // the session nonce, if needed
stream_cfg stream;
SOCKADDR remote, local;
- int stop;
- int running;
- pthread_t thread, timer_requester;
+ volatile int stop;
+ volatile int running;
+ volatile uint64_t watchdog_bark_time;
+ volatile int watchdog_barks; // number of times the watchdog has timed out and done something
+ int unfixable_error_reported; // set when an unfixable error command has been executed.
- // pthread_t *ptp;
- pthread_t *player_thread;
- pthread_rwlock_t player_thread_lock; // used to control access by "outsiders"
+ time_t playstart;
+ pthread_t thread, timer_requester, rtp_audio_thread, rtp_control_thread, rtp_timing_thread,
+ player_watchdog_thread;
+
+ // buffers to delete on exit
+ signed short *tbuf;
+ int32_t *sbuf;
+ char *outbuf;
+
+ // for holding the output rate information until printed out at the end of a session
+ double frame_rate;
+ int frame_rate_status;
+
+ // for holding input rate information until printed out at the end of a session
+
+ double input_frame_rate;
+ int input_frame_rate_starting_point_is_valid;
+
+ uint64_t frames_inward_measurement_start_time;
+ uint32_t frames_inward_frames_received_at_measurement_start_time;
+
+ uint64_t frames_inward_measurement_time;
+ uint32_t frames_inward_frames_received_at_measurement_time;
+ // other stuff...
+ pthread_t *player_thread;
abuf_t audio_buffer[BUFFER_FRAMES];
- int max_frames_per_packet, input_num_channels, input_bit_depth, input_rate;
+ unsigned int max_frames_per_packet, input_num_channels, input_bit_depth, input_rate;
int input_bytes_per_frame, output_bytes_per_frame, output_sample_ratio;
int max_frame_size_change;
int64_t previous_random_number;
alac_file *decoder_info;
uint64_t packet_count;
+ uint64_t packet_count_since_flush;
int connection_state_to_output;
- int player_thread_please_stop;
uint64_t first_packet_time_to_play;
int64_t time_since_play_started; // nanoseconds
// stats
@@ -105,7 +140,7 @@ typedef struct {
int32_t last_seqno_read;
// mutexes and condition variables
pthread_cond_t flowcontrol;
- pthread_mutex_t ab_mutex, flush_mutex;
+ pthread_mutex_t ab_mutex, flush_mutex, volume_control_mutex;
int fix_volume;
uint32_t timestamp_epoch, last_timestamp,
maximum_timestamp_interval; // timestamp_epoch of zero means not initialised, could start at 2
@@ -113,19 +148,19 @@ typedef struct {
int ab_buffering, ab_synced;
int64_t first_packet_timestamp;
int flush_requested;
- int64_t flush_rtp_timestamp;
+ uint32_t flush_rtp_timestamp;
uint64_t time_of_last_audio_packet;
seq_t ab_read, ab_write;
-#ifdef HAVE_LIBMBEDTLS
+#ifdef CONFIG_MBEDTLS
mbedtls_aes_context dctx;
#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
aes_context dctx;
#endif
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
AES_KEY aes;
#endif
@@ -161,27 +196,46 @@ typedef struct {
uint16_t local_timing_port;
int64_t latency_delayed_timestamp; // this is for debugging only...
- int64_t reference_timestamp;
- uint64_t reference_timestamp_time;
+
+ // this is what connects an rtp timestamp to the remote time
+
+ uint32_t reference_timestamp;
uint64_t remote_reference_timestamp_time;
+ // used as the initials values for calculating the rate at which the source thinks it's sending
+ // frames
+ uint32_t initial_reference_timestamp;
+ uint64_t initial_reference_time;
+ double remote_frame_rate;
+
+ // the ratio of the following should give us the operating rate, nominally 44,100
+ int64_t reference_to_previous_frame_difference;
+ uint64_t reference_to_previous_time_difference;
+
// debug variables
int request_sent;
- uint8_t time_ping_count;
+ int time_ping_count;
struct time_ping_record time_pings[time_ping_history];
uint64_t departure_time; // dangerous -- this assumes that there will never be two timing
// request in flight at the same time
pthread_mutex_t reference_time_mutex;
+ pthread_mutex_t watchdog_mutex;
+ double local_to_remote_time_gradient; // if no drift, this would be exactly 1.0; likely it's
+ // slightly above or below.
+ int local_to_remote_time_gradient_sample_count; // the number of samples used to calculate the
+ // gradient
+ // add the following to the local time to get the remote time modulo 2^64
uint64_t local_to_remote_time_difference; // used to switch between local and remote clocks
+ uint64_t local_to_remote_time_difference_measurement_time; // when the above was calculated
int last_stuff_request;
- int64_t play_segment_reference_frame;
- uint64_t play_segment_reference_frame_remote_time;
+ // int64_t play_segment_reference_frame;
+ // uint64_t play_segment_reference_frame_remote_time;
int32_t buffer_occupancy; // allow it to be negative because seq_diff may be negative
int64_t session_corrections;
@@ -197,16 +251,22 @@ typedef struct {
// zero
uint32_t dacp_active_remote; // key to send to the remote controller
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;
} rtsp_conn_info;
+uint32_t modulo_32_offset(uint32_t from, uint32_t to);
+uint64_t modulo_64_offset(uint64_t from, uint64_t to);
+
int player_play(rtsp_conn_info *conn);
int player_stop(rtsp_conn_info *conn);
void player_volume(double f, rtsp_conn_info *conn);
void player_volume_without_notification(double f, rtsp_conn_info *conn);
-void player_flush(int64_t timestamp, rtsp_conn_info *conn);
-void player_put_packet(seq_t seqno, uint32_t actual_timestamp, int64_t timestamp, uint8_t *data,
- int len, rtsp_conn_info *conn);
+void player_flush(uint32_t timestamp, rtsp_conn_info *conn);
+void player_put_packet(seq_t seqno, uint32_t actual_timestamp, uint8_t *data, int len,
+ rtsp_conn_info *conn);
int64_t monotonic_timestamp(uint32_t timestamp,
rtsp_conn_info *conn); // add an epoch to the timestamp. The monotonic
// timestamp guaranteed to start between 2^32 2^33
diff --git a/rtp.c b/rtp.c
index a5db0aa..7496cc7 100644
--- a/rtp.c
+++ b/rtp.c
@@ -1,7 +1,7 @@
/*
* Apple RTP protocol handler. This file is part of Shairport.
* Copyright (c) James Laird 2013
- * Copyright (c) Mike Brady 2014 -- 2018
+ * Copyright (c) Mike Brady 2014 -- 2019
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -45,8 +45,8 @@
#include <time.h>
#include <unistd.h>
-uint64_t local_to_remote_time_jitters;
-uint64_t local_to_remote_time_jitters_count;
+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;
@@ -58,21 +58,47 @@ void rtp_initialise(rtsp_conn_info *conn) {
}
void rtp_terminate(rtsp_conn_info *conn) {
-
+ conn->reference_timestamp = 0;
// destroy the timer mutex
int rc = pthread_mutex_destroy(&conn->reference_time_mutex);
if (rc)
debug(1, "Error destroying reference_time_mutex variable.");
}
-void rtp_audio_receiver_cleanup_handler(void *arg) {
- debug(3, "Audio Receiver Cleanup.");
- rtsp_conn_info *conn = (rtsp_conn_info *)arg;
- debug(1,"shutdown audio socket.");
- shutdown(conn->audio_socket,SHUT_RDWR);
- debug(1,"close audio socket.");
- close(conn->audio_socket);
- debug(3, "Audio Receiver Cleanup Successful.");
+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);
+}
+
+void rtp_audio_receiver_cleanup_handler(__attribute__((unused)) void *arg) {
+ debug(3, "Audio Receiver Cleanup Done.");
}
void *rtp_audio_receiver(void *arg) {
@@ -92,10 +118,13 @@ void *rtp_audio_receiver(void *arg) {
float stat_mean = 0.0;
float stat_M2 = 0.0;
+ int frame_count = 0;
ssize_t nread;
while (1) {
nread = recv(conn->audio_socket, packet, sizeof(packet), 0);
+ frame_count++;
+
uint64_t local_time_now_fp = get_absolute_time_in_fp();
if (time_of_previous_packet_fp) {
float time_interval_us =
@@ -108,8 +137,9 @@ 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;
@@ -134,6 +164,19 @@ void *rtp_audio_receiver(void *arg) {
// increment last_seqno and see if it's the same as the incoming seqno
if (type == 0x60) { // regular audio data
+
+ /*
+ char obf[4096];
+ char *obfp = obf;
+ int obfc;
+ for (obfc=0;obfc<plen;obfc++) {
+ snprintf(obfp, 3, "%02X", pktp[obfc]);
+ obfp+=2;
+ };
+ *obfp=0;
+ debug(1,"Audio Packet Received: \"%s\"",obf);
+ */
+
if (last_seqno == -1)
last_seqno = seqno;
else {
@@ -147,7 +190,9 @@ void *rtp_audio_receiver(void *arg) {
}
uint32_t actual_timestamp = ntohl(*(uint32_t *)(pktp + 4));
- int64_t timestamp = monotonic_timestamp(actual_timestamp, conn);
+
+ // uint32_t ssid = ntohl(*(uint32_t *)(pktp + 8));
+ // debug(1, "Audio packet SSID: %08X,%u", ssid,ssid);
// if (packet[1]&0x10)
// debug(1,"Audio packet Extension bit set.");
@@ -159,7 +204,7 @@ void *rtp_audio_receiver(void *arg) {
if (plen >= 16) {
if ((config.diagnostic_drop_packet_fraction == 0.0) ||
(drand48() > config.diagnostic_drop_packet_fraction))
- player_put_packet(seqno, actual_timestamp, timestamp, pktp, plen, conn);
+ player_put_packet(seqno, actual_timestamp, pktp, plen, conn);
else
debug(3, "Dropping audio packet %u to simulate a bad connection.", seqno);
continue;
@@ -188,14 +233,8 @@ void *rtp_audio_receiver(void *arg) {
pthread_exit(NULL);
}
-void rtp_control_handler_cleanup_handler(void *arg) {
- debug(3, "Control Receiver Cleanup.");
- rtsp_conn_info *conn = (rtsp_conn_info *)arg;
- debug(1,"shutdown control socket.");
- shutdown(conn->control_socket,SHUT_RDWR);
- debug(1,"close control socket.");
- close(conn->control_socket);
- debug(3, "Control Receiver Cleanup Successful.");
+void rtp_control_handler_cleanup_handler(__attribute__((unused)) void *arg) {
+ debug(3, "Control Receiver Cleanup Done.");
}
void *rtp_control_receiver(void *arg) {
@@ -206,7 +245,7 @@ void *rtp_control_receiver(void *arg) {
uint8_t packet[2048], *pktp;
// struct timespec tn;
uint64_t remote_time_of_sync;
- int64_t sync_rtp_timestamp;
+ uint32_t sync_rtp_timestamp;
ssize_t nread;
while (1) {
nread = recv(conn->control_socket, packet, sizeof(packet), 0);
@@ -246,22 +285,21 @@ void *rtp_control_receiver(void *arg) {
// 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 packe is 7
+ // 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 rtlt = nctohl(&packet[4]); // raw timestamp less latency
+ uint32_t rt = nctohl(&packet[16]); // raw timestamp
- // uint32_t fl = nctohs(&packet[2]); //
+ 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 "
- respectively.",monotonic_timestamp(rt, conn),monotonic_timestamp(rtlt, conn));
+ 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 "
+ respectively.",monotonic_timestamp(rt, conn),monotonic_timestamp(rtlt, conn));
}
*/
if (conn->local_to_remote_time_difference) { // need a time packet to be interchanged
@@ -272,14 +310,14 @@ void *rtp_control_receiver(void *arg) {
// debug(1,"Remote Sync Time: %0llx.",remote_time_of_sync);
- sync_rtp_timestamp = monotonic_timestamp(nctohl(&packet[16]), conn);
- int64_t rtp_timestamp_less_latency = monotonic_timestamp(nctohl(&packet[4]), conn);
+ sync_rtp_timestamp = nctohl(&packet[16]);
+ uint32_t rtp_timestamp_less_latency = nctohl(&packet[4]);
// debug(1,"Sync timestamp is %u.",ntohl(*((uint32_t *)&packet[16])));
if (config.userSuppliedLatency) {
if (config.userSuppliedLatency != conn->latency) {
- debug(1, "Using the user-supplied latency: %" PRId64 ".",
+ debug(1, "Using the user-supplied latency: %" PRIu32 ".",
config.userSuppliedLatency);
}
conn->latency = config.userSuppliedLatency;
@@ -298,9 +336,14 @@ void *rtp_control_receiver(void *arg) {
// Sigh, it would be nice to have a published protocol...
uint16_t flags = nctohs(&packet[2]);
- int64_t la = sync_rtp_timestamp - rtp_timestamp_less_latency;
- // debug(3, "Latency derived just from the sync packet is %" PRId64 " frames.", la);
- if ((flags == 7) || ((conn->AirPlayVersion > 0) && (conn->AirPlayVersion <= 353)) || ((conn->AirPlayVersion > 0) && (conn->AirPlayVersion >= 371))) {
+ uint32_t la = sync_rtp_timestamp - rtp_timestamp_less_latency; // note, this might
+ // loop around in
+ // modulo. Not sure if
+ // you'll get an error!
+ // debug(3, "Latency derived just from the sync packet is %" PRIu32 " frames.", la);
+
+ if ((flags == 7) || ((conn->AirPlayVersion > 0) && (conn->AirPlayVersion <= 353)) ||
+ ((conn->AirPlayVersion > 0) && (conn->AirPlayVersion >= 371))) {
la += config.fixedLatencyOffset;
// debug(3, "A fixed latency offset of %d frames has been added, giving a latency of
// "
@@ -314,51 +357,84 @@ void *rtp_control_receiver(void *arg) {
if ((conn->minimum_latency) && (conn->minimum_latency > la))
la = conn->minimum_latency;
- const int max_frames = ((3 * BUFFER_FRAMES * 352) / 4) - 11025;
+ const uint32_t max_frames = ((3 * BUFFER_FRAMES * 352) / 4) - 11025;
- if ((la < 0) || (la > max_frames)) {
- warn("An out-of-range latency request of %" PRId64
- " frames was ignored. Must be %d frames or less (44,100 frames per second). "
- "Latency remains at %" PRId64 " frames.",
+ if (la > max_frames) {
+ warn("An out-of-range latency request of %" PRIu32
+ " frames was ignored. Must be %" PRIu32
+ " frames or less (44,100 frames per second). "
+ "Latency remains at %" PRIu32 " frames.",
la, max_frames, conn->latency);
} else {
if (la != conn->latency) {
conn->latency = la;
- debug(3, "New latency detected: %" PRId64 ", sync latency: %" PRId64
- ", minimum latency: %" PRId64 ", maximum "
- "latency: %" PRId64 ", fixed offset: %" PRId64 ".",
+ 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);
}
}
}
- debug_mutex_lock(&conn->reference_time_mutex, 1000, 1);
+ debug_mutex_lock(&conn->reference_time_mutex, 1000, 0);
+
+ if (conn->initial_reference_time == 0) {
+ if (conn->packet_count_since_flush > 0) {
+ conn->initial_reference_time = remote_time_of_sync;
+ conn->initial_reference_timestamp = sync_rtp_timestamp;
+ }
+ } else {
+ uint64_t remote_frame_time_interval =
+ conn->remote_reference_timestamp_time -
+ 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.
+ } else {
+ conn->remote_frame_rate = 0.0; // use as a flag.
+ }
+ }
// this is for debugging
- // uint64_t old_remote_reference_time = conn->remote_reference_timestamp_time;
- // int64_t old_reference_timestamp = conn->reference_timestamp;
+ uint64_t old_remote_reference_time = conn->remote_reference_timestamp_time;
+ uint32_t old_reference_timestamp = conn->reference_timestamp;
// int64_t old_latency_delayed_timestamp = conn->latency_delayed_timestamp;
+
conn->remote_reference_timestamp_time = remote_time_of_sync;
- conn->reference_timestamp_time =
- remote_time_of_sync - conn->local_to_remote_time_difference;
+ // conn->reference_timestamp_time =
+ // remote_time_of_sync - local_to_remote_time_difference_now(conn);
conn->reference_timestamp = sync_rtp_timestamp;
conn->latency_delayed_timestamp = rtp_timestamp_less_latency;
- debug_mutex_unlock(&conn->reference_time_mutex, 3);
+ debug_mutex_unlock(&conn->reference_time_mutex, 0);
- // this is for debugging
- /*
- uint64_t time_difference = remote_time_of_sync - old_remote_reference_time;
- int64_t reference_frame_difference = sync_rtp_timestamp - old_reference_timestamp;
- int64_t delayed_frame_difference = rtp_timestamp_less_latency -
- old_latency_delayed_timestamp;
+ conn->reference_to_previous_time_difference =
+ remote_time_of_sync - old_remote_reference_time;
+ if (old_reference_timestamp == 0)
+ conn->reference_to_previous_frame_difference = 0;
+ 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 of %f and %f respectively.",
- (time_difference*1000000)>>32,reference_frame_difference,delayed_frame_difference,(1.0*(reference_frame_difference*10000000))/((time_difference*10000000)>>32),(1.0*(delayed_frame_difference*10000000))/((time_difference*10000000)>>32));
+ 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");
*/
@@ -381,14 +457,13 @@ void *rtp_control_receiver(void *arg) {
debug(3, "Control Receiver -- Retransmitted Audio Data Packet %u received.", seqno);
uint32_t actual_timestamp = ntohl(*(uint32_t *)(pktp + 4));
- int64_t timestamp = monotonic_timestamp(actual_timestamp, conn);
pktp += 12;
plen -= 12;
// check if packet contains enough content to be reasonable
if (plen >= 16) {
- player_put_packet(seqno, actual_timestamp, timestamp, pktp, plen, conn);
+ player_put_packet(seqno, actual_timestamp, pktp, plen, conn);
continue;
} else {
debug(3, "Too-short retransmitted audio packet received in control port, ignored.");
@@ -409,7 +484,13 @@ void *rtp_control_receiver(void *arg) {
pthread_exit(NULL);
}
+void rtp_timing_sender_cleanup_handler(void *arg) {
+ rtsp_conn_info *conn = (rtsp_conn_info *)arg;
+ debug(3, "Connection %d: Timing Sender Cleanup.", conn->connection_number);
+}
+
void *rtp_timing_sender(void *arg) {
+ pthread_cleanup_push(rtp_timing_sender_cleanup_handler, arg);
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
struct timing_request {
char leader;
@@ -469,19 +550,21 @@ void *rtp_timing_sender(void *arg) {
usleep(3000000);
}
debug(3, "rtp_timing_sender thread interrupted. This should never happen.");
+ pthread_cleanup_pop(0); // don't execute anything here.
pthread_exit(NULL);
}
void rtp_timing_receiver_cleanup_handler(void *arg) {
debug(3, "Timing Receiver Cleanup.");
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
+ debug(3, "Cancel Timing Requester.");
pthread_cancel(conn->timer_requester);
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
+ debug(3, "Join Timing Requester.");
pthread_join(conn->timer_requester, NULL);
- debug(1,"shutdown timing socket.");
- shutdown(conn->timing_socket,SHUT_RDWR);
- debug(1,"close timing socket.");
- close(conn->timing_socket);
debug(3, "Timing Receiver Cleanup Successful.");
+ pthread_setcancelstate(oldState, NULL);
}
void *rtp_timing_receiver(void *arg) {
@@ -493,14 +576,21 @@ void *rtp_timing_receiver(void *arg) {
pthread_create(&conn->timer_requester, NULL, &rtp_timing_sender, arg);
// struct timespec att;
uint64_t distant_receive_time, distant_transmit_time, arrival_time, return_time;
- local_to_remote_time_jitters = 0;
- local_to_remote_time_jitters_count = 0;
+ local_to_remote_time_jitter = 0;
+ local_to_remote_time_jitter_count = 0;
// uint64_t first_remote_time = 0;
- uint64_t first_local_time = 0;
+ // uint64_t first_local_time = 0;
uint64_t first_local_to_remote_time_difference = 0;
// uint64_t first_local_to_remote_time_difference_time;
// uint64_t l2rtd = 0;
+ int sequence_number = 0;
+
+ // for getting mean and sd of return times
+ int32_t stat_n = 0;
+ double stat_mean = 0.0;
+ double stat_M2 = 0.0;
+
while (1) {
nread = recv(conn->timing_socket, packet, sizeof(packet), 0);
@@ -522,7 +612,7 @@ void *rtp_timing_receiver(void *arg) {
obfp+=2;
};
*obfp=0;
- //debug(1,"Timing Packet Received: \"%s\"",obf);
+ debug(1,"Timing Packet Received: \"%s\"",obf);
*/
// arrival_time = ((uint64_t)att.tv_sec<<32)+((uint64_t)att.tv_nsec<<32)/1000000000;
@@ -548,23 +638,43 @@ void *rtp_timing_receiver(void *arg) {
distant_transmit_time = (uint64_t)nctohl(&packet[24]) << 32;
distant_transmit_time += nctohl(&packet[28]);
- // processing_time = distant_transmit_time - distant_receive_time;
+ uint64_t remote_processing_time = 0;
- // debug(1,"Return trip time: %lluuS, remote processing time:
- // %lluuS.",(return_time*1000000)>>32,(processing_time*1000000)>>32);
+ if (distant_transmit_time >= distant_receive_time)
+ remote_processing_time = distant_transmit_time - distant_receive_time;
+ else {
+ 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;
- unsigned int cc;
+ // remove the remote processing time from the record of the return time, as long at the
+ // processing time looks sensible.
+
+ if (remote_processing_time < return_time)
+ return_time -= remote_processing_time;
+ else
+ debug(1, "Remote processing time greater than return time -- ignored.");
+
+ int cc;
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) /
100; // make the dispersions 'age' by this rational factor
}
- // these are for diagnostics only -- not used
+ // 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].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;
@@ -572,127 +682,155 @@ void *rtp_timing_receiver(void *arg) {
if (conn->time_ping_count < time_ping_history)
conn->time_ping_count++;
- uint64_t local_time_chosen = arrival_time;
+ // here, calculate the mean and standard deviation of the return times
+
+ // 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;
+ stat_mean += stat_delta / stat_n;
+ stat_M2 += stat_delta * (rtfus - 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)));
+ // 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 lt = conn->time_pings[0].local_time;
uint64_t tld = conn->time_pings[0].dispersion;
- // chosen = 0;
+ 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;
- // chosen = cc;
+ lt = conn->time_pings[cc].local_time;
tld = conn->time_pings[cc].dispersion;
- local_time_chosen = conn->time_pings[cc].local_time;
+ // local_time_chosen = conn->time_pings[cc].local_time;
// remote_time_chosen = conn->time_pings[cc].remote_time;
}
- // int64_t ji;
+ // debug(1,"Record %d has the lowest dispersion with %0.2f us
+ // 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_jitters =
- local_to_remote_time_jitters + l2rtd - conn->local_to_remote_time_difference;
- // ji = 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_jitters =
- local_to_remote_time_jitters + conn->local_to_remote_time_difference - l2rtd;
- // ji = -(conn->local_to_remote_time_difference - l2rtd);
+ 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_jitters_count += 1;
+ local_to_remote_time_jitter_count += 1;
}
- // uncomment below to print jitter between client's clock and oour clock
- // int64_t rtus = (tld*1000000)>>32; ji = (ji*1000000)>>32; debug(1,"Choosing time
- // difference
- // with dispersion of %lld us with delta of %lld us",rtus,ji);
+ 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
+ conn->local_to_remote_time_difference_measurement_time = lt; // done at this time.
- conn->local_to_remote_time_difference = l2rtd;
if (first_local_to_remote_time_difference == 0) {
first_local_to_remote_time_difference = conn->local_to_remote_time_difference;
// first_local_to_remote_time_difference_time = get_absolute_time_in_fp();
}
- // int64_t clock_drift;
- // int64_t clock_drift_in_usec;
- // double clock_drift_ppm = 0.0;
- if (first_local_time == 0) {
- first_local_time = local_time_chosen;
- // first_remote_time = remote_time_chosen;
- // clock_drift = 0;
+ // here, let's try to use the timing pings that were selected because of their short
+ // return times to
+ // estimate a figure for drift between the local clock (x) and the remote clock (y)
+
+ // if we plug in a local interval, we will get back what that is in remote time
+
+ // calculate the line of best fit for relating the local time and the remote time
+ // we will calculate the slope, which is the drift
+ // see https://www.varsitytutors.com/hotmath/hotmath_help/topics/line-of-best-fit
+
+ uint64_t y_bar = 0; // remote timestamp average
+ uint64_t x_bar = 0; // local timestamp average
+ int sample_count = 0;
+
+ // approximate time in seconds to let the system settle down
+ const int settling_time = 60;
+ // number of points to have for calculating a valid drift
+ const int sample_point_minimum = 8;
+ for (cc = 0; cc < conn->time_ping_count; cc++)
+ 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);
+ 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;
+ 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;
+ if (slt > x_bar)
+ xid = slt - x_bar;
+ else
+ xid = -(x_bar - slt);
+
+ uint64_t srt = conn->time_pings[cc].remote_time >> 12;
+ if (srt > y_bar)
+ yid = srt - y_bar;
+ else
+ yid = -(y_bar - srt);
+
+ mtl = mtl + xid * yid;
+ mbl = mbl + xid * xid;
+ }
+ conn->local_to_remote_time_gradient_sample_count = sample_count;
+ if (mbl)
+ conn->local_to_remote_time_gradient = (1.0 * mtl) / mbl;
+ else {
+ conn->local_to_remote_time_gradient = 1.0;
+ debug(1,"rtp_timing_receiver: mbl is 0");
+ }
} else {
- // uint64_t local_time_change = local_time_chosen - first_local_time;
- // uint64_t remote_time_change = remote_time_chosen - first_remote_time;
-
- /*
- if (remote_time_change >= local_time_change)
- clock_drift = remote_time_change - local_time_change;
- else
- clock_drift = -(local_time_change - remote_time_change);
- */
- /*
- if (clock_drift >= 0)
- clock_drift_in_usec = (clock_drift * 1000000) >> 32;
- else
- clock_drift_in_usec = -(((-clock_drift) * 1000000) >> 32);
- */
-
- // clock_drift_ppm = (1.0 * clock_drift_in_usec) / (local_time_change >> 32);
+ conn->local_to_remote_time_gradient = 1.0;
}
-
- int64_t source_drift_usec;
- if (conn->play_segment_reference_frame != 0) {
- 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);
- uint64_t frame_difference = 0;
- if (reference_timestamp >= conn->play_segment_reference_frame)
- frame_difference =
- (uint64_t)reference_timestamp - (uint64_t)conn->play_segment_reference_frame;
- else // rollover
- frame_difference = (uint64_t)reference_timestamp + 0x100000000 -
- (uint64_t)conn->play_segment_reference_frame;
- uint64_t frame_time_difference_calculated =
- (((uint64_t)frame_difference << 32) / 44100);
- uint64_t frame_time_difference_actual =
- remote_reference_timestamp_time -
- conn->play_segment_reference_frame_remote_time; // this is all done by reference
- // to
- // the
- // sources' system clock
- // debug(1,"%llu frames since play started, %llu usec calculated, %llu usec
- // actual",frame_difference, (frame_time_difference_calculated*1000000)>>32,
- // (frame_time_difference_actual*1000000)>>32);
- if (frame_time_difference_calculated >=
- frame_time_difference_actual) // i.e. if the time it should have taken to send the
- // packets is greater than the actual time difference
- // measured on the source clock
- // then the source DAC's clock is running fast relative to the source system clock
- source_drift_usec = frame_time_difference_calculated - frame_time_difference_actual;
- else
- // otherwise the source DAC's clock is running slow relative to the source system
- // clock
- source_drift_usec =
- -(frame_time_difference_actual - frame_time_difference_calculated);
- } else
- source_drift_usec = 0;
- source_drift_usec = (source_drift_usec * 1000000) >> 32; // turn it to microseconds
-
- // long current_delay = 0;
- // if (config.output->delay) {
- // config.output->delay(&current_delay);
- //}
- // Useful for troubleshooting:
- // debug(1, "clock_drift_ppm %f\tchosen %5d\tsource_drift_usec
- // %10.1lld\treturn_time_in_usec
- // %10.1llu",
- // clock_drift_ppm,
- // chosen,
- //(session_corrections*1000000)/44100,
- // current_delay,
- // source_drift_usec,
- // buffer_occupancy,
- //(return_time*1000000)>>32);
+ // 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);
@@ -717,14 +855,28 @@ void *rtp_timing_receiver(void *arg) {
static uint16_t bind_port(int ip_family, const char *self_ip_address, uint32_t scope_id,
int *sock) {
// look for a port in the range, if any was specified.
- uint16_t desired_port = config.udp_port_base;
int ret = 0;
int local_socket = socket(ip_family, SOCK_DGRAM, IPPROTO_UDP);
if (local_socket == -1)
die("Could not allocate a socket.");
+
+ /*
+ int val = 1;
+ ret = setsockopt(local_socket, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
+ if (ret < 0) {
+ char errorstring[1024];
+ strerror_r(errno, (char *)errorstring, sizeof(errorstring));
+ debug(1, "Error %d: \"%s\". Couldn't set SO_REUSEADDR");
+ }
+ */
+
SOCKADDR myaddr;
+ int tryCount = 0;
+ uint16_t desired_port;
do {
+ tryCount++;
+ desired_port = nextFreeUDPPort();
memset(&myaddr, 0, sizeof(myaddr));
if (ip_family == AF_INET) {
struct sockaddr_in *sa = (struct sockaddr_in *)&myaddr;
@@ -745,14 +897,20 @@ static uint16_t bind_port(int ip_family, const char *self_ip_address, uint32_t s
#endif
} while ((ret < 0) && (errno == EADDRINUSE) && (desired_port != 0) &&
- (++desired_port < config.udp_port_base + config.udp_port_range));
+ (tryCount < config.udp_port_range));
// debug(1,"UDP port chosen: %d.",desired_port);
if (ret < 0) {
close(local_socket);
- die("error: could not bind a UDP port! Check the udp_port_range is large enough (>= 10) or "
- "check for restrictive firewall settings or a bad router!");
+ char errorstring[1024];
+ strerror_r(errno, (char *)errorstring, sizeof(errorstring));
+ die("error %d: \"%s\". Could not bind a UDP port! Check the udp_port_range is large enough -- "
+ "it must be "
+ "at least 3, and 10 or more is suggested -- or "
+ "check for restrictive firewall settings or a bad router! UDP base is %u, range is %u and "
+ "current suggestion is %u.",
+ errno, errorstring, config.udp_port_base, config.udp_port_range, desired_port);
}
uint16_t sport;
@@ -897,26 +1055,151 @@ void rtp_setup(SOCKADDR *local, SOCKADDR *remote, uint16_t cport, uint16_t tport
}
}
-void get_reference_timestamp_stuff(int64_t *timestamp, uint64_t *timestamp_time,
+void get_reference_timestamp_stuff(uint32_t *timestamp, uint64_t *timestamp_time,
uint64_t *remote_timestamp_time, rtsp_conn_info *conn) {
// types okay
- debug_mutex_lock(&conn->reference_time_mutex, 1000, 1);
+ debug_mutex_lock(&conn->reference_time_mutex, 1000, 0);
*timestamp = conn->reference_timestamp;
- *timestamp_time = conn->reference_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.");
//}
- *remote_timestamp_time = conn->remote_reference_timestamp_time;
- debug_mutex_unlock(&conn->reference_time_mutex, 3);
+ debug_mutex_unlock(&conn->reference_time_mutex, 0);
}
void clear_reference_timestamp(rtsp_conn_info *conn) {
debug_mutex_lock(&conn->reference_time_mutex, 1000, 1);
conn->reference_timestamp = 0;
- conn->reference_timestamp_time = 0;
+ conn->remote_reference_timestamp_time = 0;
debug_mutex_unlock(&conn->reference_time_mutex, 3);
}
+int have_timestamp_timing_information(rtsp_conn_info *conn) {
+ if (conn->reference_timestamp == 0)
+ return 0;
+ else
+ return 1;
+}
+
+// set this to zero to use the rates supplied by the sources, which might not always be completely
+// right...
+const int use_nominal_rate = 0; // specify whether to use the nominal input rate, usually 44100 fps
+
+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;
+ if ((conn->initial_reference_time) && (conn->initial_reference_timestamp)) {
+ // uint32_t local_frames = conn->reference_timestamp - conn->initial_reference_timestamp;
+ uint32_t local_frames =
+ modulo_32_offset(conn->initial_reference_timestamp, conn->reference_timestamp);
+ uint64_t local_time = conn->remote_reference_timestamp_time - conn->initial_reference_time;
+ if ((local_frames == 0) || (local_time == 0) || (use_nominal_rate)) {
+ result = 1;
+ } else {
+ double calculated_frame_rate = conn->input_rate;
+ if (local_time)
+ calculated_frame_rate = ((1.0 * local_frames) / local_time) * one_fp;
+ else
+ debug(1,"sanitised_source_rate_information: local_time is zero");
+ if ((local_time == 0) || ((calculated_frame_rate / conn->input_rate) > 1.002) ||
+ ((calculated_frame_rate / conn->input_rate) < 0.998)) {
+ debug(3, "input frame rate out of bounds at %.2f fps.", calculated_frame_rate);
+ result = 1;
+ } else {
+ *frames = local_frames;
+ *time = local_time;
+ result = 0;
+ }
+ }
+ }
+ return result;
+}
+
+// the timestamp is a timestamp calculated at the input rate
+// the reference timestamps are denominated in terms of the input rate
+
+int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn) {
+ debug_mutex_lock(&conn->reference_time_mutex, 1000, 0);
+ int result = 0;
+ uint64_t time_difference;
+ uint32_t frame_difference;
+ result = sanitised_source_rate_information(&frame_difference, &time_difference, conn);
+
+ uint64_t timestamp_interval_time;
+ uint64_t remote_time_of_timestamp;
+ uint32_t timestamp_interval = modulo_32_offset(conn->reference_timestamp, timestamp);
+ if (timestamp_interval <=
+ conn->input_rate * 3600) { // i.e. timestamp was really after the reference timestamp
+ timestamp_interval_time = (timestamp_interval * time_difference) /
+ frame_difference; // this is the nominal time, based on the
+ // fps specified between current and
+ // previous sync frame.
+ remote_time_of_timestamp = conn->remote_reference_timestamp_time +
+ timestamp_interval_time; // based on the reference timestamp time
+ // plus the time interval calculated based
+ // on the specified fps.
+ } else { // i.e. timestamp was actually before the reference timestamp
+ timestamp_interval =
+ modulo_32_offset(timestamp, conn->reference_timestamp); // fix the calculation
+ timestamp_interval_time = (timestamp_interval * time_difference) /
+ frame_difference; // this is the nominal time, based on the
+ // fps specified between current and
+ // previous sync frame.
+ remote_time_of_timestamp = conn->remote_reference_timestamp_time -
+ timestamp_interval_time; // based on the reference timestamp time
+ // plus the time interval calculated based
+ // on the specified fps.
+ }
+ *time = remote_time_of_timestamp - local_to_remote_time_difference_now(conn);
+ debug_mutex_unlock(&conn->reference_time_mutex, 0);
+ return result;
+}
+
+int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn) {
+ debug_mutex_lock(&conn->reference_time_mutex, 1000, 0);
+ int result = 0;
+
+ uint64_t time_difference;
+ uint32_t frame_difference;
+ result = sanitised_source_rate_information(&frame_difference, &time_difference, conn);
+
+ // first, get from [local] time to remote time.
+ uint64_t remote_time = time + local_to_remote_time_difference_now(conn);
+ // next, get the remote time interval from the remote_time to the reference time
+ uint64_t time_interval;
+
+ // 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);
+ 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;
+ else
+ time_interval = conn->remote_reference_timestamp_time - remote_time;
+
+ // now, convert the remote time interval into frames using the frame rate we have observed or
+ // which has been nominated
+ uint32_t frame_interval = 0;
+ if (time_difference)
+ frame_interval = (time_interval * frame_difference) / time_difference;
+ else
+ debug(1,"local_time_to_frame: time_difference is zero");
+ if (reference_time_was_earlier) {
+ // debug(1,"Frame interval is %" PRId64 " frames.",frame_interval);
+ *frame = (conn->reference_timestamp + frame_interval);
+ } else {
+ // debug(1,"Frame interval is %" PRId64 " frames.",-frame_interval);
+ *frame = (conn->reference_timestamp - frame_interval);
+ }
+ debug_mutex_unlock(&conn->reference_time_mutex, 0);
+ return result;
+}
+
void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) {
if (conn->rtp_running) {
// if (!request_sent) {
@@ -957,8 +1240,9 @@ 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(1, "Error %d using sendto to an audio socket: \"%s\". Backing off for 1/16th of a "
- "second.",
+ debug(1,
+ "Error %d using sendto to an audio socket: \"%s\". Backing off for 1/16th of a "
+ "second.",
errno, em);
conn->rtp_time_of_last_resend_request_error_fp = time_of_sending_fp;
} else {
diff --git a/rtp.h b/rtp.h
index 743cff5..171bd65 100644
--- a/rtp.h
+++ b/rtp.h
@@ -17,8 +17,17 @@ void rtp_setup(SOCKADDR *local, SOCKADDR *remote, uint16_t controlport, uint16_t
void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn);
void rtp_request_client_pause(rtsp_conn_info *conn); // ask the client to pause
-void get_reference_timestamp_stuff(int64_t *timestamp, uint64_t *timestamp_time,
+void get_reference_timestamp_stuff(uint32_t *timestamp, uint64_t *timestamp_time,
uint64_t *remote_timestamp_time, rtsp_conn_info *conn);
void clear_reference_timestamp(rtsp_conn_info *conn);
+int have_timestamp_timing_information(rtsp_conn_info *conn);
+
+int get_frame_play_time(int64_t timestamp, int sample_ratio, uint64_t *time_to_play);
+
+int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn);
+int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn);
+
+int sanitised_source_rate_information(uint32_t *frames, uint64_t *time, rtsp_conn_info *conn);
+
#endif // _RTP_H
diff --git a/rtsp.c b/rtsp.c
index 0267e7f..20fc6b8 100644
--- a/rtsp.c
+++ b/rtsp.c
@@ -3,7 +3,7 @@
* Copyright (c) James Laird 2013
* Modifications associated with audio synchronization, mutithreading and
- * metadata handling copyright (c) Mike Brady 2014-2018
+ * metadata handling copyright (c) Mike Brady 2014-2019
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
@@ -46,15 +46,16 @@
#include "config.h"
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
#include <openssl/md5.h>
#endif
-#ifdef HAVE_LIBMBEDTLS
+#ifdef CONFIG_MBEDTLS
#include <mbedtls/md5.h>
+#include <mbedtls/version.h>
#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
#include <polarssl/md5.h>
#endif
@@ -63,10 +64,14 @@
#include "rtp.h"
#include "rtsp.h"
-#ifdef HAVE_METADATA_HUB
+#ifdef CONFIG_METADATA_HUB
#include "metadata_hub.h"
#endif
+#ifdef CONFIG_MQTT
+#include "mqtt.h"
+#endif
+
#ifdef AF_INET6
#define INETx_ADDRSTRLEN INET6_ADDRSTRLEN
#else
@@ -85,7 +90,11 @@ enum rtsp_read_request_response {
};
// Mike Brady's part...
-static pthread_mutex_t play_lock = PTHREAD_MUTEX_INITIALIZER;
+
+int metadata_running = 0;
+
+// always lock use this when accessing the playing conn value
+static pthread_mutex_t playing_conn_lock = PTHREAD_MUTEX_INITIALIZER;
// every time we want to retain or release a reference count, lock it with this
// if a reference count is read as zero, it means the it's being deallocated.
@@ -97,8 +106,6 @@ static pthread_mutex_t reference_counter_lock = PTHREAD_MUTEX_INITIALIZER;
// static int please_shutdown = 0;
// static pthread_t playing_thread = 0;
-static rtsp_conn_info **conns = NULL;
-
int RTSP_connection_index = 1;
#ifdef CONFIG_METADATA
@@ -115,7 +122,10 @@ typedef struct {
} pc_queue; // producer-consumer queue
#endif
+static int msg_indexes = 1;
+
typedef struct {
+ int index_number;
uint32_t referenceCount; // we might start using this...
unsigned int nheaders;
char *name[16];
@@ -152,6 +162,12 @@ void pc_queue_init(pc_queue *the_queue, char *items, size_t item_size, uint32_t
the_queue->eoq = 0;
}
+void pc_queue_delete(pc_queue *the_queue) {
+ pthread_cond_destroy(&the_queue->pc_queue_item_removed_signal);
+ pthread_cond_destroy(&the_queue->pc_queue_item_added_signal);
+ pthread_mutex_destroy(&the_queue->pc_queue_lock);
+}
+
int send_metadata(uint32_t type, uint32_t code, char *data, uint32_t length, rtsp_message *carrier,
int block);
@@ -159,17 +175,26 @@ int send_ssnc_metadata(uint32_t code, char *data, uint32_t length, int block) {
return send_metadata('ssnc', code, data, length, NULL, block);
}
+void pc_queue_cleanup_handler(void *arg) {
+ // debug(1, "pc_queue_cleanup_handler called.");
+ pc_queue *the_queue = (pc_queue *)arg;
+ int rc = pthread_mutex_unlock(&the_queue->pc_queue_lock);
+ if (rc)
+ debug(1, "Error unlocking for pc_queue_add_item or pc_queue_get_item.");
+}
+
int pc_queue_add_item(pc_queue *the_queue, const void *the_stuff, int block) {
int rc;
if (the_queue) {
if (block == 0) {
- rc = pthread_mutex_trylock(&the_queue->pc_queue_lock);
+ rc = debug_mutex_lock(&the_queue->pc_queue_lock, 10000, 2);
if (rc == EBUSY)
return EBUSY;
} else
rc = pthread_mutex_lock(&the_queue->pc_queue_lock);
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)
@@ -192,9 +217,7 @@ int pc_queue_add_item(pc_queue *the_queue, const void *the_stuff, int block) {
rc = pthread_cond_signal(&the_queue->pc_queue_item_added_signal);
if (rc)
debug(1, "Error signalling after pc_queue_add_item");
- rc = pthread_mutex_unlock(&the_queue->pc_queue_lock);
- if (rc)
- debug(1, "Error unlocking for pc_queue_add_item");
+ pthread_cleanup_pop(1); // unlock the queue lock.
} else {
debug(1, "Adding an item to a NULL queue");
}
@@ -207,6 +230,7 @@ int pc_queue_get_item(pc_queue *the_queue, void *the_stuff) {
rc = pthread_mutex_lock(&the_queue->pc_queue_lock);
if (rc)
debug(1, "Error locking for pc_queue_get_item");
+ 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)
@@ -227,9 +251,7 @@ int pc_queue_get_item(pc_queue *the_queue, void *the_stuff) {
rc = pthread_cond_signal(&the_queue->pc_queue_item_removed_signal);
if (rc)
debug(1, "Error signalling after pc_queue_removed_item");
- rc = pthread_mutex_unlock(&the_queue->pc_queue_lock);
- if (rc)
- debug(1, "Error unlocking for pc_queue_get_item");
+ pthread_cleanup_pop(1); // unlock the queue lock.
} else {
debug(1, "Removing an item from a NULL queue");
}
@@ -238,6 +260,60 @@ int pc_queue_get_item(pc_queue *the_queue, void *the_stuff) {
#endif
+int have_player(rtsp_conn_info *conn) {
+ int response = 0;
+ debug_mutex_lock(&playing_conn_lock, 1000000, 3);
+ if (playing_conn == conn) // this connection definitely has the play lock
+ response = 1;
+ debug_mutex_unlock(&playing_conn_lock, 3);
+ return response;
+}
+
+void player_watchdog_thread_cleanup_handler(void *arg) {
+ rtsp_conn_info *conn = (rtsp_conn_info *)arg;
+ debug(3, "Connection %d: Watchdog Exit.", conn->connection_number);
+}
+
+void *player_watchdog_thread_code(void *arg) {
+ pthread_cleanup_push(player_watchdog_thread_cleanup_handler, arg);
+ rtsp_conn_info *conn = (rtsp_conn_info *)arg;
+ do {
+ usleep(2000000); // check every two seconds
+ // debug(3, "Connection %d: Check the thread is doing something...", conn->connection_number);
+ if ((config.dont_check_timeout == 0) && (config.timeout != 0)) {
+ debug_mutex_lock(&conn->watchdog_mutex, 1000, 0);
+ 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 ct = config.timeout; // go from int to 64-bit int
+
+ if (time_since_last_bark >= ct) {
+ conn->watchdog_barks++;
+ if (conn->watchdog_barks == 1) {
+ // debuglev = 3; // tell us everything.
+ debug(1, "Connection %d: As Yeats almost said, \"Too long a silence / can make a stone "
+ "of the heart\".",
+ conn->connection_number);
+ conn->stop = 1;
+ pthread_cancel(conn->thread);
+ } else if (conn->watchdog_barks == 3) {
+ if ((config.cmd_unfixable) && (conn->unfixable_error_reported == 0)) {
+ conn->unfixable_error_reported = 1;
+ command_execute(config.cmd_unfixable, "unable_to_cancel_play_session", 1);
+ } else {
+ warn("an unrecoverable error, \"unable_to_cancel_play_session\", has been detected.",
+ conn->connection_number);
+ }
+ }
+ }
+ }
+ }
+ } while (1);
+ pthread_cleanup_pop(0); // should never happen
+ pthread_exit(NULL);
+}
+
void ask_other_rtsp_conversation_threads_to_stop(pthread_t except_this_thread);
void rtsp_request_shutdown_stream(void) {
@@ -257,7 +333,20 @@ static void track_thread(rtsp_conn_info *conn) {
}
}
-static void cleanup_threads(void) {
+void cancel_all_RTSP_threads(void) {
+ int i;
+ for (i = 0; i < nconns; i++) {
+ debug(1, "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);
+ pthread_join(conns[i]->thread, NULL);
+ free(conns[i]);
+ }
+}
+
+void cleanup_threads(void) {
void *retval;
int i;
// debug(2, "culling threads.");
@@ -267,8 +356,6 @@ static void cleanup_threads(void) {
conns[i]->connection_number);
pthread_join(conns[i]->thread, &retval);
debug(3, "RTSP connection thread %d deleted...", conns[i]->connection_number);
- if (conns[i] == playing_conn)
- playing_conn = NULL;
free(conns[i]);
nconns--;
if (nconns)
@@ -288,8 +375,11 @@ void ask_other_rtsp_conversation_threads_to_stop(pthread_t except_this_thread) {
for (i = 0; i < nconns; i++) {
if (((except_this_thread == 0) || (pthread_equal(conns[i]->thread, except_this_thread) == 0)) &&
(conns[i]->running != 0)) {
- conns[i]->stop = 1;
- pthread_kill(conns[i]->thread, SIGUSR1);
+ pthread_cancel(conns[i]->thread);
+ pthread_join(conns[i]->thread, NULL);
+ debug(1, "Connection %d: asked to stop.", conns[i]->connection_number);
+ // conns[i]->stop = 1;
+ // pthread_kill(conns[i]->thread, SIGUSR1);
}
}
}
@@ -317,32 +407,35 @@ static char *nextline(char *in, int inbuf) {
return out;
}
-static void msg_retain(rtsp_message *msg) {
- if (msg) {
- int rc = pthread_mutex_lock(&reference_counter_lock);
- if (rc)
- debug(1, "Error %d locking reference counter lock");
+void msg_retain(rtsp_message *msg) {
+ int rc = pthread_mutex_lock(&reference_counter_lock);
+ if (rc)
+ debug(1, "Error %d locking reference counter lock");
+ if (msg > (rtsp_message *)0x00010000) {
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)
debug(1, "Error %d unlocking reference counter lock");
} else {
- debug(1, "null rtsp_message pointer passed to retain");
+ debug(1, "invalid rtsp_message pointer 0x%x passed to retain", (uintptr_t)msg);
}
}
-static rtsp_message *msg_init(void) {
+rtsp_message *msg_init(void) {
rtsp_message *msg = malloc(sizeof(rtsp_message));
if (msg) {
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++;
} else {
- die("can not allocate memory for an rtsp_message.");
+ die("msg_init -- can not allocate memory for rtsp_message %d.", msg_indexes);
}
+ // debug(1,"msg_init -- create item %d.", msg->index_number);
return msg;
}
-static int msg_add_header(rtsp_message *msg, char *name, char *value) {
+int msg_add_header(rtsp_message *msg, char *name, char *value) {
if (msg->nheaders >= sizeof(msg->name) / sizeof(char *)) {
warn("too many headers?!");
return 1;
@@ -355,7 +448,7 @@ static int msg_add_header(rtsp_message *msg, char *name, char *value) {
return 0;
}
-static char *msg_get_header(rtsp_message *msg, char *name) {
+char *msg_get_header(rtsp_message *msg, char *name) {
unsigned int i;
for (i = 0; i < msg->nheaders; i++)
if (!strcasecmp(msg->name[i], name))
@@ -363,7 +456,7 @@ static char *msg_get_header(rtsp_message *msg, char *name) {
return NULL;
}
-static void debug_print_msg_headers(int level, rtsp_message *msg) {
+void debug_print_msg_headers(int level, rtsp_message *msg) {
unsigned int i;
for (i = 0; i < msg->nheaders; i++) {
debug(level, " Type: \"%s\", content: \"%s\"", msg->name[i], msg->value[i]);
@@ -393,16 +486,11 @@ static void debug_print_msg_content(int level, rtsp_message *msg) {
}
*/
-static void msg_free(rtsp_message *msg) {
-
- if (msg) {
- int rc = pthread_mutex_lock(&reference_counter_lock);
- if (rc)
- debug(1, "Error %d locking reference counter lock during msg_free()", rc);
+void msg_free(rtsp_message **msgh) {
+ debug_mutex_lock(&reference_counter_lock, 1000, 0);
+ if (*msgh > (rtsp_message *)0x00010000) {
+ rtsp_message *msg = *msgh;
msg->referenceCount--;
- rc = pthread_mutex_unlock(&reference_counter_lock);
- if (rc)
- debug(1, "Error %d unlocking reference counter lock during msg_free()", rc);
if (msg->referenceCount == 0) {
unsigned int i;
for (i = 0; i < msg->nheaders; i++) {
@@ -411,17 +499,27 @@ static void msg_free(rtsp_message *msg) {
}
if (msg->content)
free(msg->content);
+ // debug(1,"msg_free item %d -- free.",msg->index_number);
+ uintptr_t index = (msg->index_number) & 0xFFFF;
+ if (index == 0)
+ 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
free(msg);
- } // else {
- // debug(1,"rtsp_message reference count non-zero:
- // %d!",msg->referenceCount);
- //}
- } else {
- debug(1, "null rtsp_message pointer passed to msg_free()");
+ } else {
+ // debug(1,"msg_free item %d -- decrement reference to
+ // %d.",msg->index_number,msg->referenceCount);
+ }
+ } else if (*msgh != NULL) {
+ debug(1,
+ "msg_free: error attempting to free an allocated but already-freed rtsp_message, number "
+ "%d.",
+ (uintptr_t)*msgh);
}
+ debug_mutex_unlock(&reference_counter_lock, 0);
}
-static int msg_handle_line(rtsp_message **pmsg, char *line) {
+int msg_handle_line(rtsp_message **pmsg, char *line) {
rtsp_message *msg = *pmsg;
if (!msg) {
@@ -430,7 +528,7 @@ static int msg_handle_line(rtsp_message **pmsg, char *line) {
char *sp, *p;
sp = NULL; // this is to quieten a compiler warning
- // debug(1, "received request: %s", line);
+ debug(3, "RTSP Message Received: \"%s\".", line);
p = strtok_r(line, " ", &sp);
if (!p)
@@ -471,36 +569,43 @@ static int msg_handle_line(rtsp_message **pmsg, char *line) {
}
fail:
- *pmsg = NULL;
- msg_free(msg);
+ msg_free(pmsg);
return 0;
}
-static enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn,
- rtsp_message **the_packet) {
- enum rtsp_read_request_response reply = rtsp_read_request_response_ok;
- ssize_t buflen = 4096;
- char *buf = malloc(buflen + 1);
+enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_message **the_packet) {
- rtsp_message *msg = NULL;
+ *the_packet = NULL; // need this for erro handling
+ enum rtsp_read_request_response reply = rtsp_read_request_response_ok;
+ ssize_t buflen = 4096;
+ int release_buffer = 0; // on exit, don't deallocate the buffer if everything was okay
+ char *buf = malloc(buflen + 1); // add a NUL at the end
+ if (!buf) {
+ warn("rtsp_read_request: can't get a buffer.");
+ return (rtsp_read_request_response_error);
+ }
+ pthread_cleanup_push(malloc_cleanup, buf);
ssize_t nread;
ssize_t inbuf = 0;
int msg_size = -1;
while (msg_size < 0) {
- fd_set readfds;
- FD_ZERO(&readfds);
- FD_SET(conn->fd, &readfds);
- do {
- memory_barrier();
- } while (conn->stop == 0 &&
- pselect(conn->fd + 1, &readfds, NULL, NULL, NULL, &pselect_sigset) <= 0);
+ /*
+fd_set readfds;
+FD_ZERO(&readfds);
+FD_SET(conn->fd, &readfds);
+do {
+ memory_barrier();
+} while (conn->stop == 0 &&
+ pselect(conn->fd + 1, &readfds, NULL, NULL, NULL, &pselect_sigset) <= 0);
+*/
if (conn->stop != 0) {
debug(3, "RTSP conversation thread %d shutdown requested.", conn->connection_number);
reply = rtsp_read_request_response_immediate_shutdown_requested;
goto shutdown;
}
+
nread = read(conn->fd, buf + inbuf, buflen - inbuf);
if (nread == 0) {
@@ -513,9 +618,16 @@ static enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn,
if (nread < 0) {
if (errno == EINTR)
continue;
- char errorstring[1024];
- strerror_r(errno, (char *)errorstring, sizeof(errorstring));
- debug(1, "rtsp_read_request_response_read_error %d: \"%s\".", errno, (char *)errorstring);
+ if (errno == EAGAIN) {
+ debug(1, "Getting Error 11 -- EAGAIN from a blocking read!");
+ continue;
+ }
+ if (errno != ECONNRESET) {
+ char errorstring[1024];
+ strerror_r(errno, (char *)errorstring, sizeof(errorstring));
+ debug(1, "Connection %d: rtsp_read_request_response_read_error %d: \"%s\".",
+ conn->connection_number, errno, (char *)errorstring);
+ }
reply = rtsp_read_request_response_read_error;
goto shutdown;
}
@@ -523,9 +635,9 @@ static enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn,
char *next;
while (msg_size < 0 && (next = nextline(buf, inbuf))) {
- msg_size = msg_handle_line(&msg, buf);
+ msg_size = msg_handle_line(the_packet, buf);
- if (!msg) {
+ if (!(*the_packet)) {
warn("no RTSP header received");
reply = rtsp_read_request_response_bad_packet;
goto shutdown;
@@ -538,7 +650,7 @@ static enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn,
}
if (msg_size > buflen) {
- buf = realloc(buf, msg_size);
+ buf = realloc(buf, msg_size + 1);
if (!buf) {
warn("too much content");
reply = rtsp_read_request_response_error;
@@ -571,6 +683,8 @@ static enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn,
warning_message_sent = 1;
}
}
+
+ /*
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(conn->fd, &readfds);
@@ -578,6 +692,8 @@ static enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn,
memory_barrier();
} while (conn->stop == 0 &&
pselect(conn->fd + 1, &readfds, NULL, NULL, NULL, &pselect_sigset) <= 0);
+ */
+
if (conn->stop != 0) {
debug(1, "RTSP shutdown requested.");
reply = rtsp_read_request_response_immediate_shutdown_requested;
@@ -586,7 +702,7 @@ static enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn,
size_t read_chunk = msg_size - inbuf;
if (read_chunk > max_read_chunk)
read_chunk = max_read_chunk;
- usleep(40000); // wait about 40 milliseconds between reads of up to about 64 kB
+ usleep(80000); // wait about 80 milliseconds between reads of up to about 64 kB
nread = read(conn->fd, buf + inbuf, read_chunk);
if (!nread) {
reply = rtsp_read_request_response_error;
@@ -602,23 +718,22 @@ static enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn,
inbuf += nread;
}
+ rtsp_message *msg = *the_packet;
msg->contentlength = inbuf;
msg->content = buf;
+ char *jp = inbuf + buf;
+ *jp = '\0';
*the_packet = msg;
- return reply;
-
shutdown:
- if (msg) {
- msg_free(msg); // which will free the content and everything else
+ if (reply != rtsp_read_request_response_ok) {
+ msg_free(the_packet);
+ release_buffer = 1; // allow the buffer to be released
}
- // in case the message wasn't formed or wasn't fully initialised
- if ((msg && (msg->content == NULL)) || (!msg))
- free(buf);
- *the_packet = NULL;
+ pthread_cleanup_pop(release_buffer);
return reply;
}
-static void msg_write_response(int fd, rtsp_message *resp) {
+int msg_write_response(int fd, rtsp_message *resp) {
char pkt[2048];
int pktfree = sizeof(pkt);
char *p = pkt;
@@ -636,8 +751,10 @@ static void msg_write_response(int fd, rtsp_message *resp) {
n = snprintf(p, pktfree, "%s: %s\r\n", resp->name[i], resp->value[i]);
pktfree -= n;
p += n;
- if (pktfree <= 1024)
- die("Attempted to write overlong RTSP packet 1");
+ if (pktfree <= 1024) {
+ debug(1, "Attempted to write overlong RTSP packet 1");
+ return -1;
+ }
}
// Here, if there's content, write the Content-Length header ...
@@ -647,8 +764,10 @@ static void msg_write_response(int fd, rtsp_message *resp) {
n = snprintf(p, pktfree, "Content-Length: %d\r\n", resp->contentlength);
pktfree -= n;
p += n;
- if (pktfree <= 1024)
- die("Attempted to write overlong RTSP packet 2");
+ if (pktfree <= 1024) {
+ debug(1, "Attempted to write overlong RTSP packet 2");
+ return -2;
+ }
debug(1, "Content is \"%s\"", resp->content);
memcpy(p, resp->content, resp->contentlength);
pktfree -= resp->contentlength;
@@ -659,212 +778,248 @@ static void msg_write_response(int fd, rtsp_message *resp) {
pktfree -= n;
p += n;
- if (pktfree <= 1024)
- die("Attempted to write overlong RTSP packet 3");
-
- if (write(fd, pkt, p - pkt) != p - pkt)
- debug(1, "Error writing an RTSP packet -- requested bytes not fully written.");
+ if (pktfree <= 1024) {
+ debug(1, "Attempted to write overlong RTSP packet 3");
+ return -3;
+ }
+ ssize_t reply = write(fd, pkt, p - pkt);
+ if (reply == -1) {
+ char errorstring[1024];
+ strerror_r(errno, (char *)errorstring, sizeof(errorstring));
+ debug(1, "msg_write_response error %d: \"%s\".", errno, (char *)errorstring);
+ return -4;
+ }
+ if (reply != p - pkt) {
+ debug(1, "msg_write_response error -- requested bytes: %d not fully written: %d.", p - pkt,
+ reply);
+ return -5;
+ }
+ return 0;
}
-static void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) {
+void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) {
debug(2, "Connection %d: RECORD", conn->connection_number);
+ if (have_player(conn)) {
+ if (conn->player_thread)
+ warn("Connection %d: RECORD: Duplicate RECORD message -- ignored", conn->connection_number);
+ else
+ player_play(conn); // the thread better be 0
- if (conn->player_thread)
- warn("Duplicate RECORD message -- ignored");
- else
- player_play(conn); // the thread better be 0
-
- resp->respcode = 200;
- // I think this is for telling the client what the absolute minimum latency
- // actually is,
- // and when the client specifies a latency, it should be added to this figure.
+ resp->respcode = 200;
+ // I think this is for telling the client what the absolute minimum latency
+ // actually is,
+ // and when the client specifies a latency, it should be added to this figure.
- // Thus, [the old version of] AirPlay's latency figure of 77175, when added to 11025 gives you
- // exactly 88200
- // and iTunes' latency figure of 88553, when added to 11025 gives you 99578,
- // pretty close to the 99400 we guessed.
+ // Thus, [the old version of] AirPlay's latency figure of 77175, when added to 11025 gives you
+ // exactly 88200
+ // and iTunes' latency figure of 88553, when added to 11025 gives you 99578,
+ // pretty close to the 99400 we guessed.
- msg_add_header(resp, "Audio-Latency", "11025");
+ msg_add_header(resp, "Audio-Latency", "11025");
- char *p;
- uint32_t rtptime = 0;
- char *hdr = msg_get_header(req, "RTP-Info");
+ char *p;
+ uint32_t rtptime = 0;
+ char *hdr = msg_get_header(req, "RTP-Info");
- if (hdr) {
- // debug(1,"FLUSH message received: \"%s\".",hdr);
- // get the rtp timestamp
- p = strstr(hdr, "rtptime=");
- if (p) {
- p = strchr(p, '=');
+ if (hdr) {
+ // debug(1,"FLUSH message received: \"%s\".",hdr);
+ // get the rtp timestamp
+ p = strstr(hdr, "rtptime=");
if (p) {
- rtptime = uatoi(p + 1); // unsigned integer -- up to 2^32-1
- // rtptime--;
- // debug(1,"RTSP Flush Requested by handle_record: %u.",rtptime);
- player_flush(rtptime, conn);
+ p = strchr(p, '=');
+ if (p) {
+ rtptime = uatoi(p + 1); // unsigned integer -- up to 2^32-1
+ // rtptime--;
+ // debug(1,"RTSP Flush Requested by handle_record: %u.",rtptime);
+ player_flush(rtptime, conn);
+ }
}
}
+ } else {
+ warn("Connection %d RECORD received without having the player (no ANNOUNCE?)",
+ conn->connection_number);
+ resp->respcode = 451;
}
}
-static void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req,
- rtsp_message *resp) {
+void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req,
+ rtsp_message *resp) {
debug(3, "Connection %d: OPTIONS", conn->connection_number);
resp->respcode = 200;
- msg_add_header(resp, "Public",
- "ANNOUNCE, SETUP, RECORD, "
- "PAUSE, FLUSH, TEARDOWN, "
- "OPTIONS, GET_PARAMETER, SET_PARAMETER");
+ msg_add_header(resp, "Public", "ANNOUNCE, SETUP, RECORD, "
+ "PAUSE, FLUSH, TEARDOWN, "
+ "OPTIONS, GET_PARAMETER, SET_PARAMETER");
}
-static void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req,
- rtsp_message *resp) {
+void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req,
+ rtsp_message *resp) {
debug(2, "Connection %d: TEARDOWN", conn->connection_number);
- // if (!rtsp_playing())
- // debug(1, "This RTSP connection thread (%d) doesn't think it's playing, but "
- // "it's sending a response to teardown anyway",conn->connection_number);
- resp->respcode = 200;
- msg_add_header(resp, "Connection", "close");
-
- debug(3,
+ if (have_player(conn)) {
+ resp->respcode = 200;
+ msg_add_header(resp, "Connection", "close");
+ debug(
+ 3,
"TEARDOWN: synchronously terminating the player thread of RTSP conversation thread %d (2).",
conn->connection_number);
- player_stop(conn);
- debug(3, "TEARDOWN: successful termination of playing thread of RTSP conversation thread %d.",
- conn->connection_number);
+ player_stop(conn);
+ debug(3, "TEARDOWN: successful termination of playing thread of RTSP conversation thread %d.",
+ conn->connection_number);
+ } else {
+ warn("Connection %d TEARDOWN received without having the player (no ANNOUNCE?)",
+ conn->connection_number);
+ resp->respcode = 451;
+ }
}
-static void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) {
+void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) {
debug(3, "Connection %d: FLUSH", conn->connection_number);
- // if (!rtsp_playing())
- // debug(1, "This RTSP conversation thread (%d) doesn't think it's playing, but "
- // "it's sending a response to flush anyway",conn->connection_number);
- char *p = NULL;
- uint32_t rtptime = 0;
- char *hdr = msg_get_header(req, "RTP-Info");
-
- if (hdr) {
- // debug(1,"FLUSH message received: \"%s\".",hdr);
- // get the rtp timestamp
- p = strstr(hdr, "rtptime=");
- if (p) {
- p = strchr(p, '=');
- if (p)
- rtptime = uatoi(p + 1); // unsigned integer -- up to 2^32-1
+ if (have_player(conn)) {
+ char *p = NULL;
+ uint32_t rtptime = 0;
+ char *hdr = msg_get_header(req, "RTP-Info");
+
+ if (hdr) {
+ // debug(1,"FLUSH message received: \"%s\".",hdr);
+ // get the rtp timestamp
+ p = strstr(hdr, "rtptime=");
+ if (p) {
+ p = strchr(p, '=');
+ if (p)
+ rtptime = uatoi(p + 1); // unsigned integer -- up to 2^32-1
+ }
}
- }
// debug(1,"RTSP Flush Requested: %u.",rtptime);
#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);
+ 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;
+ player_flush(rtptime, conn); // will not crash even it there is no player thread.
+ resp->respcode = 200;
+
+ } else {
+ warn("Connection %d FLUSH received without having the player (no ANNOUNCE?)",
+ conn->connection_number);
+ resp->respcode = 451;
+ }
}
-static void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) {
+void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) {
debug(3, "Connection %d: SETUP", conn->connection_number);
- uint16_t cport, tport;
-
- char *ar = msg_get_header(req, "Active-Remote");
- if (ar) {
- debug(2, "Connection %d: SETUP -- Active-Remote string seen: \"%s\".", conn->connection_number,
- ar);
- // get the active remote
- char *p;
- conn->dacp_active_remote = strtoul(ar, &p, 10);
+ resp->respcode = 451; // invalid arguments -- expect them
+ if (have_player(conn)) {
+ uint16_t cport, tport;
+ char *ar = msg_get_header(req, "Active-Remote");
+ if (ar) {
+ debug(2, "Connection %d: SETUP -- Active-Remote string seen: \"%s\".",
+ conn->connection_number, ar);
+ // get the active remote
+ char *p;
+ conn->dacp_active_remote = strtoul(ar, &p, 10);
#ifdef CONFIG_METADATA
- send_metadata('ssnc', 'acre', ar, strlen(ar), req, 1);
+ send_metadata('ssnc', 'acre', ar, strlen(ar), req, 1);
#endif
- } else {
- debug(2, "Connection %d: SETUP -- Note: no Active-Remote information the SETUP Record.",
- conn->connection_number);
- conn->dacp_active_remote = 0;
- }
+ } else {
+ debug(2, "Connection %d: SETUP -- Note: no Active-Remote information the SETUP Record.",
+ conn->connection_number);
+ conn->dacp_active_remote = 0;
+ }
- ar = msg_get_header(req, "DACP-ID");
- if (ar) {
- debug(2, "Connection %d: SETUP -- DACP-ID string seen: \"%s\".", conn->connection_number, ar);
- if (conn->dacp_id) // this is in case SETUP was previously called
- free(conn->dacp_id);
- conn->dacp_id = strdup(ar);
+ ar = msg_get_header(req, "DACP-ID");
+ if (ar) {
+ debug(2, "Connection %d: SETUP -- DACP-ID string seen: \"%s\".", conn->connection_number, ar);
+ if (conn->dacp_id) // this is in case SETUP was previously called
+ free(conn->dacp_id);
+ conn->dacp_id = strdup(ar);
#ifdef CONFIG_METADATA
- send_metadata('ssnc', 'daid', ar, strlen(ar), req, 1);
+ send_metadata('ssnc', 'daid', ar, strlen(ar), req, 1);
#endif
- } else {
- debug(2, "Connection %d: SETUP doesn't include DACP-ID string information.",
- conn->connection_number);
- if (conn->dacp_id) // this is in case SETUP was previously called
- free(conn->dacp_id);
- conn->dacp_id = NULL;
- }
-
- char *hdr = msg_get_header(req, "Transport");
- if (!hdr) {
- debug(1, "Connection %d: SETUP doesn't contain a Transport header.", conn->connection_number);
- goto error;
- }
+ } else {
+ debug(2, "Connection %d: SETUP doesn't include DACP-ID string information.",
+ conn->connection_number);
+ if (conn->dacp_id) // this is in case SETUP was previously called
+ free(conn->dacp_id);
+ conn->dacp_id = NULL;
+ }
- char *p;
- p = strstr(hdr, "control_port=");
- if (!p) {
- debug(1, "Connection %d: SETUP doesn't specify a control_port.", conn->connection_number);
- goto error;
- }
- p = strchr(p, '=') + 1;
- cport = atoi(p);
+ char *hdr = msg_get_header(req, "Transport");
+ if (hdr) {
+ char *p;
+ p = strstr(hdr, "control_port=");
+ if (p) {
+ p = strchr(p, '=') + 1;
+ cport = atoi(p);
- p = strstr(hdr, "timing_port=");
- if (!p) {
- debug(1, "Connection %d: SETUP doesn't specify a timing_port.", conn->connection_number);
- goto error;
- }
- p = strchr(p, '=') + 1;
- tport = atoi(p);
-
- if (conn->rtp_running) {
- if ((conn->remote_control_port != cport) || (conn->remote_timing_port != tport)) {
- warn("Connection %d: Duplicate SETUP message with different control (old %u, new %u) or "
- "timing (old %u, new "
- "%u) ports! This is probably fatal!",
- conn->connection_number, conn->remote_control_port, cport, conn->remote_timing_port,
- tport);
+ p = strstr(hdr, "timing_port=");
+ if (p) {
+ p = strchr(p, '=') + 1;
+ tport = atoi(p);
+
+ if (conn->rtp_running) {
+ if ((conn->remote_control_port != cport) || (conn->remote_timing_port != tport)) {
+ warn("Connection %d: Duplicate SETUP message with different control (old %u, new %u) "
+ "or "
+ "timing (old %u, new "
+ "%u) ports! This is probably fatal!",
+ conn->connection_number, conn->remote_control_port, cport,
+ conn->remote_timing_port, tport);
+ } else {
+ warn("Connection %d: Duplicate SETUP message with the same control (%u) and timing "
+ "(%u) "
+ "ports. This is "
+ "probably not fatal.",
+ conn->connection_number, conn->remote_control_port, conn->remote_timing_port);
+ }
+ } else {
+ rtp_setup(&conn->local, &conn->remote, cport, tport, conn);
+ }
+ if (conn->local_audio_port != 0) {
+
+ char resphdr[256] = "";
+ snprintf(resphdr, sizeof(resphdr),
+ "RTP/AVP/"
+ "UDP;unicast;interleaved=0-1;mode=record;control_port=%d;"
+ "timing_port=%d;server_"
+ "port=%d",
+ conn->local_control_port, conn->local_timing_port, conn->local_audio_port);
+
+ msg_add_header(resp, "Transport", resphdr);
+
+ msg_add_header(resp, "Session", "1");
+
+ resp->respcode = 200; // it all worked out okay
+ debug(1, "Connection %d: SETUP DACP-ID \"%s\" from %s to %s with UDP ports Control: "
+ "%d, Timing: %d and Audio: %d.",
+ conn->connection_number, conn->dacp_id, &conn->client_ip_string,
+ &conn->self_ip_string, conn->local_control_port, conn->local_timing_port,
+ conn->local_audio_port);
+
+ } else {
+ debug(1, "Connection %d: SETUP seems to specify a null audio port.",
+ conn->connection_number);
+ }
+ } else {
+ debug(1, "Connection %d: SETUP doesn't specify a timing_port.", conn->connection_number);
+ }
+ } else {
+ debug(1, "Connection %d: SETUP doesn't specify a control_port.", conn->connection_number);
+ }
} else {
- warn("Connection %d: Duplicate SETUP message with the same control (%u) and timing (%u) "
- "ports. This is "
- "probably not fatal.",
- conn->connection_number, conn->remote_control_port, conn->remote_timing_port);
+ debug(1, "Connection %d: SETUP doesn't contain a Transport header.", conn->connection_number);
}
+ if (resp->respcode != 200) {
+ debug(1, "Connection %d: SETUP error -- releasing the player lock.", conn->connection_number);
+ debug_mutex_lock(&playing_conn_lock, 1000000, 3);
+ if (playing_conn == conn) // if we have the player
+ playing_conn = NULL; // let it go
+ debug_mutex_unlock(&playing_conn_lock, 3);
+ }
+
} else {
- rtp_setup(&conn->local, &conn->remote, cport, tport, conn);
- }
- if (conn->local_audio_port == 0) {
- debug(1, "Connection %d: SETUP seems to specify a null audio port.", conn->connection_number);
- goto error;
+ warn("Connection %d SETUP received without having the player (no ANNOUNCE?)",
+ conn->connection_number);
}
-
- char resphdr[256] = "";
- snprintf(resphdr, sizeof(resphdr),
- "RTP/AVP/"
- "UDP;unicast;interleaved=0-1;mode=record;control_port=%d;"
- "timing_port=%d;server_"
- "port=%d",
- conn->local_control_port, conn->local_timing_port, conn->local_audio_port);
-
- msg_add_header(resp, "Transport", resphdr);
-
- msg_add_header(resp, "Session", "1");
-
- resp->respcode = 200;
- return;
-
-error:
- warn("Connection %d: SETUP -- Error in setup request -- unlocking play lock.",
- conn->connection_number);
- playing_conn = NULL;
- pthread_mutex_unlock(&play_lock);
- resp->respcode = 451; // invalid arguments
}
/*
@@ -874,26 +1029,44 @@ static void handle_ignore(rtsp_conn_info *conn, rtsp_message *req, rtsp_message
}
*/
-static void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req,
- __attribute__((unused)) rtsp_message *resp) {
+void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req,
+ __attribute__((unused)) rtsp_message *resp) {
+
char *cp = req->content;
int cp_left = req->contentlength;
+ /*
+ int k = cp_left;
+ if (k>max_bytes)
+ k = max_bytes;
+ for (i = 0; i < k; i++)
+ snprintf((char *)buf + 2 * i, 3, "%02x", cp[i]);
+ debug(1, "handle_set_parameter_parameter: \"%s\".",buf);
+ */
+
char *next;
while (cp_left && cp) {
next = nextline(cp, cp_left);
- cp_left -= next - cp;
+ // note: "next" will return NULL if there is no \r or \n or \r\n at the end of this
+ // but we are always guaranteed that if cp is not null, it will be pointing to something
+ // NUL-terminated
- if (!strncmp(cp, "volume: ", 8)) {
- float volume = atof(cp + 8);
+ if (next)
+ cp_left -= (next - cp);
+ else
+ cp_left = 0;
+
+ if (!strncmp(cp, "volume: ", strlen("volume: "))) {
+ float volume = atof(cp + strlen("volume: "));
// debug(2, "AirPlay request to set volume to: %f.", volume);
player_volume(volume, conn);
} else
#ifdef CONFIG_METADATA
- if (!strncmp(cp, "progress: ", 10)) {
- char *progress = cp + 10;
- // debug(2, "progress: \"%s\"\n",
- // progress); // rtpstampstart/rtpstampnow/rtpstampend 44100 per second
+ if (!strncmp(cp, "progress: ", strlen("progress: "))) {
+ 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);
+
} else
#endif
{
@@ -931,6 +1104,8 @@ static void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *r
// 'PICT' -- the payload is a picture, either a JPEG or a PNG. Check the
// first few bytes to see
// which.
+// 'abeg' -- active mode entered. No arguments
+// 'aend' -- active mode exited. No arguments
// 'pbeg' -- play stream begin. No arguments
// 'pend' -- play stream end. No arguments
// 'pfls' -- play stream flush. No arguments
@@ -1044,9 +1219,9 @@ static char *metadata_sockmsg;
#define metadata_queue_size 500
metadata_package metadata_queue_items[metadata_queue_size];
-static pthread_t metadata_thread;
+pthread_t metadata_thread;
-void metadata_create(void) {
+void metadata_create_multicast_socket(void) {
if (config.metadata_enabled == 0)
return;
@@ -1068,7 +1243,7 @@ void metadata_create(void) {
if (metadata_sockmsg) {
memset(metadata_sockmsg, 0, config.metadata_sockmsglength);
} else {
- die("Could not malloc metadata socket buffer");
+ die("Could not malloc metadata multicast socket buffer");
}
}
}
@@ -1087,6 +1262,15 @@ void metadata_create(void) {
umask(oldumask);
}
+void metadata_delete_multicast_socket(void) {
+ if (config.metadata_enabled == 0)
+ return;
+ shutdown(metadata_sock, SHUT_RDWR); // we want to immediately deallocate the buffer
+ close(metadata_sock);
+ if (metadata_sockmsg)
+ free(metadata_sockmsg);
+}
+
void metadata_open(void) {
if (config.metadata_enabled == 0)
return;
@@ -1104,12 +1288,12 @@ void metadata_open(void) {
free(path);
}
-/*
static void metadata_close(void) {
+ if (fd < 0)
+ return;
close(fd);
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);
@@ -1231,32 +1415,63 @@ void metadata_process(uint32_t type, uint32_t code, char *data, uint32_t length)
}
}
+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;
+ if (pack->carrier)
+ msg_free(&pack->carrier); // release the message
+ else if (pack->data)
+ free(pack->data);
+}
+
void *metadata_thread_function(__attribute__((unused)) void *ignore) {
- metadata_create();
+ // 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_create_multicast_socket();
metadata_package pack;
+ pthread_cleanup_push(metadata_thread_cleanup_function, NULL);
while (1) {
pc_queue_get_item(&metadata_queue, &pack);
+ pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack);
if (config.metadata_enabled) {
metadata_process(pack.type, pack.code, pack.data, pack.length);
-#ifdef HAVE_METADATA_HUB
+#ifdef CONFIG_METADATA_HUB
metadata_hub_process_metadata(pack.type, pack.code, pack.data, pack.length);
#endif
+
+#ifdef CONFIG_MQTT
+ if (config.mqtt_enabled) {
+ mqtt_process_metadata(pack.type, pack.code, pack.data, pack.length);
+ }
+#endif
}
- if (pack.carrier)
- msg_free(pack.carrier); // release the message
- else if (pack.data)
- free(pack.data);
+ pthread_cleanup_pop(1);
}
+ pthread_cleanup_pop(1); // will never happen
pthread_exit(NULL);
}
void metadata_init(void) {
- // 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);
int ret = pthread_create(&metadata_thread, NULL, metadata_thread_function, NULL);
if (ret)
debug(1, "Failed to create metadata thread!");
+ metadata_running = 1;
+}
+
+void metadata_stop(void) {
+ if (metadata_running) {
+ debug(2, "metadata_stop called.");
+ pthread_cancel(metadata_thread);
+ pthread_join(metadata_thread, NULL);
+ }
}
int send_metadata(uint32_t type, uint32_t code, char *data, uint32_t length, rtsp_message *carrier,
@@ -1293,14 +1508,17 @@ int send_metadata(uint32_t type, uint32_t code, char *data, uint32_t length, rts
pack.code = code;
pack.data = data;
pack.length = length;
- if (carrier)
- msg_retain(carrier);
pack.carrier = carrier;
+ if (pack.carrier)
+ msg_retain(pack.carrier);
int rc = pc_queue_add_item(&metadata_queue, &pack, block);
- if ((rc == EBUSY) && (carrier))
- msg_free(carrier);
- if (rc == EBUSY)
- warn("Metadata queue is busy, dropping message of type 0x%08X, code 0x%08X.", type, code);
+ if (rc == EBUSY) {
+ if (pack.carrier)
+ 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);
+ }
return rc;
}
@@ -1367,7 +1585,7 @@ static void handle_set_parameter(rtsp_conn_info *conn, rtsp_message *req, rtsp_m
char *ct = msg_get_header(req, "Content-Type");
if (ct) {
- // debug(2, "SET_PARAMETER Content-Type:\"%s\".", ct);
+// debug(2, "SET_PARAMETER Content-Type:\"%s\".", ct);
#ifdef CONFIG_METADATA
// It seems that the rtptime of the message is used as a kind of an ID that
@@ -1449,59 +1667,93 @@ static void handle_set_parameter(rtsp_conn_info *conn, rtsp_message *req, rtsp_m
}
static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) {
- debug(2, "Connection %d: ANNOUNCE", conn->connection_number);
+ debug(3, "Connection %d: ANNOUNCE", conn->connection_number);
+
int have_the_player = 0;
+ int should_wait = 0; // this will be true if you're trying to break in to the current session
+ int interrupting_current_session = 0;
+
+ // try to become the current playing_conn
- // interrupt session if permitted
- if (pthread_mutex_trylock(&play_lock) == 0) {
+ debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it
+
+ if (playing_conn == NULL) {
+ playing_conn = conn;
have_the_player = 1;
- } else if ((playing_conn) &&
- (playing_conn->connection_number == conn->connection_number)) { // duplicate ANNOUNCE
- warn("Duplicate ANNOUNCE, by the look of it!");
+ } else if (playing_conn == conn) {
have_the_player = 1;
- } else {
- int should_wait = 0;
-
- if (!playing_conn)
- die("Non existent playing_conn with play_lock enabled.");
- debug(1, "RTSP Conversation thread %d already playing when asked by thread %d.",
- playing_conn->connection_number, conn->connection_number);
- if (playing_conn->stop) {
- debug(1, "Playing connection is already shutting down; waiting for it...");
- should_wait = 1;
- } else if (config.allow_session_interruption == 1) {
- // some other thread has the player ... ask it to relinquish the thread
- debug(1, "ANNOUNCE: playing connection %d being interrupted by connection %d.",
- playing_conn->connection_number, conn->connection_number);
- if (playing_conn == conn) {
- debug(1, "ANNOUNCE asking to stop itself.");
- } else {
- playing_conn->stop = 1;
- memory_barrier();
- pthread_kill(playing_conn->thread, SIGUSR1);
- should_wait = 1;
+ warn("Duplicate ANNOUNCE, by the look of it!");
+ } else if (playing_conn->stop) {
+ debug(1, "Connection %d ANNOUNCE is waiting for connection %d to shut down.",
+ conn->connection_number, playing_conn->connection_number);
+ should_wait = 1;
+ } else if (config.allow_session_interruption == 1) {
+ debug(2, "Connection %d: ANNOUNCE: asking playing connection %d to shut down.",
+ conn->connection_number, playing_conn->connection_number);
+ playing_conn->stop = 1;
+ interrupting_current_session = 1;
+ should_wait = 1;
+ pthread_cancel(playing_conn->thread); // asking the RTSP thread to exit
+ }
+ debug_mutex_unlock(&playing_conn_lock, 3);
+
+ if (should_wait) {
+ int time_remaining = 3000000; // must be signed, as it could go negative...
+
+ while ((time_remaining > 0) && (have_the_player == 0)) {
+ debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it
+ if (playing_conn == NULL) {
+ playing_conn = conn;
+ have_the_player = 1;
+ }
+ debug_mutex_unlock(&playing_conn_lock, 3);
+
+ if (have_the_player == 0) {
+ usleep(100000);
+ time_remaining -= 100000;
}
}
- if (should_wait) {
- usleep(1000000); // here, it is possible for other connections to come in and nab the player.
- debug(1, "Try to get the player now");
+ if ((have_the_player == 1) && (interrupting_current_session == 1)) {
+ debug(2, "Connection %d: ANNOUNCE got the player", conn->connection_number);
+ } else {
+ debug(2, "Connection %d: ANNOUNCE failed to get the player", conn->connection_number);
}
- if (pthread_mutex_trylock(&play_lock) == 0)
- have_the_player = 1;
- else
- debug(1, "ANNOUNCE failed to get the player");
}
if (have_the_player) {
- playing_conn = conn; // the present connection is now playing
- debug(3, "RTSP conversation thread %d has acquired play lock.", conn->connection_number);
+ debug(3, "Connection %d: ANNOUNCE has acquired play lock.", conn->connection_number);
+
+ // now, if this new session did not break in, then it's okay to reset the next UDP ports
+ // to the start of the range
+
+ if (interrupting_current_session == 0) { // will be zero if it wasn't waiting to break in
+ resetFreeUDPPort();
+ }
+
+ /*
+ {
+ char *cp = req->content;
+ int cp_left = req->contentlength;
+ while (cp_left > 1) {
+ if (strlen(cp) != 0)
+ debug(1,">>>>>> %s", cp);
+ cp += strlen(cp) + 1;
+ cp_left -= strlen(cp) + 1;
+ }
+ }
+*/
+
+ conn->stream.type = ast_unknown;
resp->respcode = 456; // 456 - Header Field Not Valid for Resource
+ char *pssid = NULL;
char *paesiv = NULL;
char *prsaaeskey = NULL;
char *pfmtp = NULL;
char *pminlatency = NULL;
char *pmaxlatency = NULL;
+ // char *pAudioMediaInfo = NULL;
+ char *pUncompressedCDAudio = NULL;
char *cp = req->content;
int cp_left = req->contentlength;
char *next;
@@ -1509,24 +1761,67 @@ static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_messag
next = nextline(cp, cp_left);
cp_left -= next - cp;
- if (!strncmp(cp, "a=fmtp:", 7))
- pfmtp = cp + 7;
+ if (!strncmp(cp, "a=rtpmap:96 L16/44100/2", strlen("a=rtpmap:96 L16/44100/2")))
+ pUncompressedCDAudio = cp + strlen("a=rtpmap:96 L16/44100/2");
+
+ // if (!strncmp(cp, "m=audio", strlen("m=audio")))
+ // pAudioMediaInfo = cp + strlen("m=audio");
+
+ if (!strncmp(cp, "o=iTunes", strlen("o=iTunes")))
+ pssid = cp + strlen("o=iTunes");
+
+ if (!strncmp(cp, "a=fmtp:", strlen("a=fmtp:")))
+ pfmtp = cp + strlen("a=fmtp:");
- if (!strncmp(cp, "a=aesiv:", 8))
- paesiv = cp + 8;
+ if (!strncmp(cp, "a=aesiv:", strlen("a=aesiv:")))
+ paesiv = cp + strlen("a=aesiv:");
- if (!strncmp(cp, "a=rsaaeskey:", 12))
- prsaaeskey = cp + 12;
+ if (!strncmp(cp, "a=rsaaeskey:", strlen("a=rsaaeskey:")))
+ prsaaeskey = cp + strlen("a=rsaaeskey:");
- if (!strncmp(cp, "a=min-latency:", 14))
- pminlatency = cp + 14;
+ if (!strncmp(cp, "a=min-latency:", strlen("a=min-latency:")))
+ pminlatency = cp + strlen("a=min-latency:");
- if (!strncmp(cp, "a=max-latency:", 14))
- pmaxlatency = cp + 14;
+ if (!strncmp(cp, "a=max-latency:", strlen("a=max-latency:")))
+ pmaxlatency = cp + strlen("a=max-latency:");
cp = next;
}
+ if (pUncompressedCDAudio) {
+ debug(2, "An uncompressed PCM stream has been detected.");
+ conn->stream.type = ast_uncompressed;
+ conn->max_frames_per_packet = 352; // number of audio frames per packet.
+ conn->input_rate = 44100;
+ conn->input_num_channels = 2;
+ conn->input_bit_depth = 16;
+ conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8);
+
+ /*
+ int y = strlen(pAudioMediaInfo);
+ if (y > 0) {
+ char obf[4096];
+ if (y > 4096)
+ y = 4096;
+ char *p = pAudioMediaInfo;
+ char *obfp = obf;
+ int obfc;
+ for (obfc = 0; obfc < y; obfc++) {
+ snprintf(obfp, 3, "%02X", (unsigned int)*p);
+ p++;
+ obfp += 2;
+ };
+ *obfp = 0;
+ debug(1, "AudioMediaInfo: \"%s\".", obf);
+ }
+ */
+ }
+
+ if (pssid) {
+ uint32_t ssid = uatoi(pssid);
+ debug(3, "Synchronisation Source Identifier: %08X,%u", ssid, ssid);
+ }
+
if (pminlatency) {
conn->minimum_latency = atoi(pminlatency);
debug(3, "Minimum latency %d specified", conn->minimum_latency);
@@ -1545,22 +1840,6 @@ static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_messag
// debug(1,"Encrypted session requested");
}
- if (!pfmtp) {
- warn("FMTP params missing from the following ANNOUNCE message:");
- // print each line of the request content
- // the problem is that nextline has replace all returns, newlines, etc. by
- // NULLs
- char *cp = req->content;
- int cp_left = req->contentlength;
- while (cp_left > 1) {
- if (strlen(cp) != 0)
- warn(" %s", cp);
- cp += strlen(cp) + 1;
- cp_left -= strlen(cp) + 1;
- }
- goto out;
- }
-
if (conn->stream.encrypted) {
int len, keylen;
uint8_t *aesiv = base64_dec(paesiv, &len);
@@ -1583,12 +1862,41 @@ static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_messag
memcpy(conn->stream.aeskey, aeskey, 16);
free(aeskey);
}
- unsigned int i;
- for (i = 0; i < sizeof(conn->stream.fmtp) / sizeof(conn->stream.fmtp[0]); i++)
- conn->stream.fmtp[i] = atoi(strsep(&pfmtp, " \t"));
- // here we should check the sanity ot the fmtp values
- // for (i = 0; i < sizeof(conn->stream.fmtp) / sizeof(conn->stream.fmtp[0]); i++)
- // debug(1," fmtp[%2d] is: %10d",i,conn->stream.fmtp[i]);
+
+ if (pfmtp) {
+ conn->stream.type = ast_apple_lossless;
+ debug(3, "An ALAC stream has been detected.");
+ unsigned int i;
+ for (i = 0; i < sizeof(conn->stream.fmtp) / sizeof(conn->stream.fmtp[0]); i++)
+ conn->stream.fmtp[i] = atoi(strsep(&pfmtp, " \t"));
+ // here we should check the sanity ot the fmtp values
+ // for (i = 0; i < sizeof(conn->stream.fmtp) / sizeof(conn->stream.fmtp[0]); i++)
+ // debug(1," fmtp[%2d] is: %10d",i,conn->stream.fmtp[i]);
+
+ // set the parameters of the player (as distinct from the parameters of the decoder -- that's
+ // done later).
+ conn->max_frames_per_packet = conn->stream.fmtp[1]; // number of audio frames per packet.
+ conn->input_rate = conn->stream.fmtp[11];
+ conn->input_num_channels = conn->stream.fmtp[7];
+ conn->input_bit_depth = conn->stream.fmtp[3];
+ conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8);
+ }
+
+ if (conn->stream.type == ast_unknown) {
+ warn("Can not process the following ANNOUNCE message:");
+ // print each line of the request content
+ // the problem is that nextline has replace all returns, newlines, etc. by
+ // NULLs
+ char *cp = req->content;
+ int cp_left = req->contentlength;
+ while (cp_left > 1) {
+ if (strlen(cp) != 0)
+ warn(" %s", cp);
+ cp += strlen(cp) + 1;
+ cp_left -= strlen(cp) + 1;
+ }
+ goto out;
+ }
char *hdr = msg_get_header(req, "X-Apple-Client-Name");
if (hdr) {
@@ -1621,15 +1929,18 @@ static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_messag
resp->respcode = 200;
} else {
resp->respcode = 453;
- debug(1, "Already playing.");
+ debug(1, "Connection %d: ANNOUNCE failed because another connection is already playing.",
+ conn->connection_number);
}
out:
if (resp->respcode != 200 && resp->respcode != 453) {
- debug(1, "Error in handling ANNOUNCE on conversation thread %d. Unlocking the play lock.",
+ debug(1, "Connection %d: Error in handling ANNOUNCE. Unlocking the play lock.",
conn->connection_number);
- playing_conn = NULL;
- pthread_mutex_unlock(&play_lock);
+ debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it
+ if (playing_conn == conn) // if we managed to acquire it
+ playing_conn = NULL; // let it go
+ debug_mutex_unlock(&playing_conn_lock, 3);
}
}
@@ -1702,21 +2013,21 @@ static void apple_challenge(int fd, rtsp_message *req, rtsp_message *resp) {
if (padding)
*padding = 0;
- msg_add_header(resp, "Apple-Response", encoded);
+ msg_add_header(resp, "Apple-Response", encoded); // will be freed when the response is freed.
free(challresp);
free(encoded);
}
static char *make_nonce(void) {
uint8_t random[8];
- int fd = open("/dev/random", O_RDONLY);
+ int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0)
- die("could not open /dev/random!");
+ die("could not open /dev/urandom!");
// int ignore =
if (read(fd, random, sizeof(random)) != sizeof(random))
- debug(1, "Error reading /dev/random");
+ debug(1, "Error reading /dev/urandom");
close(fd);
- return base64_enc(random, 8);
+ return base64_enc(random, 8); // returns a pointer to malloc'ed memory
}
static int rtsp_auth(char **nonce, rtsp_message *req, rtsp_message *resp) {
@@ -1760,9 +2071,11 @@ static int rtsp_auth(char **nonce, rtsp_message *req, rtsp_message *resp) {
uint8_t digest_urp[16], digest_mu[16], digest_total[16];
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
MD5_CTX ctx;
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
MD5_Init(&ctx);
MD5_Update(&ctx, username, strlen(username));
MD5_Update(&ctx, ":", 1);
@@ -1775,9 +2088,25 @@ static int rtsp_auth(char **nonce, rtsp_message *req, rtsp_message *resp) {
MD5_Update(&ctx, ":", 1);
MD5_Update(&ctx, uri, strlen(uri));
MD5_Final(digest_mu, &ctx);
+ pthread_setcancelstate(oldState, NULL);
#endif
-#ifdef HAVE_LIBMBEDTLS
+#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 *)username, strlen(username));
+ mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1);
+ mbedtls_md5_update_ret(&tctx, (const unsigned char *)realm, strlen(realm));
+ mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1);
+ mbedtls_md5_update_ret(&tctx, (const unsigned char *)config.password, strlen(config.password));
+ mbedtls_md5_finish_ret(&tctx, digest_urp);
+ mbedtls_md5_starts_ret(&tctx);
+ mbedtls_md5_update_ret(&tctx, (const unsigned char *)req->method, strlen(req->method));
+ mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1);
+ mbedtls_md5_update_ret(&tctx, (const unsigned char *)uri, strlen(uri));
+ mbedtls_md5_finish_ret(&tctx, digest_mu);
+#else
mbedtls_md5_context tctx;
mbedtls_md5_starts(&tctx);
mbedtls_md5_update(&tctx, (const unsigned char *)username, strlen(username));
@@ -1792,8 +2121,9 @@ static int rtsp_auth(char **nonce, rtsp_message *req, rtsp_message *resp) {
mbedtls_md5_update(&tctx, (const unsigned char *)uri, strlen(uri));
mbedtls_md5_finish(&tctx, digest_mu);
#endif
+#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
md5_context tctx;
md5_starts(&tctx);
md5_update(&tctx, (const unsigned char *)username, strlen(username));
@@ -1814,7 +2144,8 @@ static int rtsp_auth(char **nonce, rtsp_message *req, rtsp_message *resp) {
for (i = 0; i < 16; i++)
snprintf((char *)buf + 2 * i, 3, "%02x", digest_urp[i]);
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
MD5_Init(&ctx);
MD5_Update(&ctx, buf, 32);
MD5_Update(&ctx, ":", 1);
@@ -1824,9 +2155,21 @@ static int rtsp_auth(char **nonce, rtsp_message *req, rtsp_message *resp) {
snprintf((char *)buf + 2 * i, 3, "%02x", digest_mu[i]);
MD5_Update(&ctx, buf, 32);
MD5_Final(digest_total, &ctx);
+ pthread_setcancelstate(oldState, NULL);
#endif
-#ifdef HAVE_LIBMBEDTLS
+#ifdef CONFIG_MBEDTLS
+#if MBEDTLS_VERSION_MINOR >= 7
+ mbedtls_md5_starts_ret(&tctx);
+ mbedtls_md5_update_ret(&tctx, buf, 32);
+ mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1);
+ mbedtls_md5_update_ret(&tctx, (const unsigned char *)*nonce, strlen(*nonce));
+ mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1);
+ for (i = 0; i < 16; i++)
+ snprintf((char *)buf + 2 * i, 3, "%02x", digest_mu[i]);
+ mbedtls_md5_update_ret(&tctx, buf, 32);
+ mbedtls_md5_finish_ret(&tctx, digest_total);
+#else
mbedtls_md5_starts(&tctx);
mbedtls_md5_update(&tctx, buf, 32);
mbedtls_md5_update(&tctx, (unsigned char *)":", 1);
@@ -1837,8 +2180,9 @@ static int rtsp_auth(char **nonce, rtsp_message *req, rtsp_message *resp) {
mbedtls_md5_update(&tctx, buf, 32);
mbedtls_md5_finish(&tctx, digest_total);
#endif
+#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
md5_starts(&tctx);
md5_update(&tctx, buf, 32);
md5_update(&tctx, (unsigned char *)":", 1);
@@ -1867,37 +2211,138 @@ authenticate:
return 1;
}
+void rtsp_conversation_thread_cleanup_function(void *arg) {
+ rtsp_conn_info *conn = (rtsp_conn_info *)arg;
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
+
+ debug(3, "Connection %d: rtsp_conversation_thread_func_cleanup_function called.",
+ conn->connection_number);
+ if (conn->player_thread)
+ player_stop(conn);
+
+ debug(3, "Closing timing, control and audio sockets...");
+ if (conn->control_socket)
+ close(conn->control_socket);
+ if (conn->timing_socket)
+ close(conn->timing_socket);
+ if (conn->audio_socket)
+ close(conn->audio_socket);
+
+ if (conn->fd > 0) {
+ debug(3, "Connection %d: closing fd %d.", conn->connection_number, conn->fd);
+ close(conn->fd);
+ debug(3, "Connection %d: closed fd %d.", conn->connection_number, conn->fd);
+ }
+ if (conn->auth_nonce) {
+ free(conn->auth_nonce);
+ conn->auth_nonce = NULL;
+ }
+ rtp_terminate(conn);
+
+ if (conn->dacp_id) {
+ free(conn->dacp_id);
+ conn->dacp_id = NULL;
+ }
+
+ // remove flow control and mutexes
+ int rc = pthread_mutex_destroy(&conn->volume_control_mutex);
+ if (rc)
+ debug(1, "Connection %d: error %d destroying volume_control_mutex.", conn->connection_number,
+ rc);
+ rc = pthread_cond_destroy(&conn->flowcontrol);
+ if (rc)
+ debug(1, "Connection %d: error %d destroying flow control condition variable.",
+ conn->connection_number, rc);
+ rc = pthread_mutex_destroy(&conn->ab_mutex);
+ if (rc)
+ debug(1, "Connection %d: error %d destroying ab_mutex.", conn->connection_number, rc);
+ rc = pthread_mutex_destroy(&conn->flush_mutex);
+ if (rc)
+ debug(1, "Connection %d: error %d destroying flush_mutex.", conn->connection_number, rc);
+
+ debug(3, "Cancel watchdog thread.");
+ pthread_cancel(conn->player_watchdog_thread);
+ debug(3, "Join watchdog thread.");
+ pthread_join(conn->player_watchdog_thread, NULL);
+ debug(3, "Delete watchdog mutex.");
+ pthread_mutex_destroy(&conn->watchdog_mutex);
+
+ debug(3, "Connection %d: Checking play lock.", conn->connection_number);
+ debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it
+ if (playing_conn == conn) { // if it's ours
+ debug(3, "Connection %d: Unlocking play lock.", conn->connection_number);
+ playing_conn = NULL; // let it go
+ }
+ debug_mutex_unlock(&playing_conn_lock, 3);
+
+ debug(2, "Connection %d: terminated.", conn->connection_number);
+ conn->running = 0;
+ pthread_setcancelstate(oldState, NULL);
+}
+
+void msg_cleanup_function(void *arg) {
+ // debug(3, "msg_cleanup_function called.");
+ msg_free((rtsp_message **)arg);
+}
+
static void *rtsp_conversation_thread_func(void *pconn) {
rtsp_conn_info *conn = pconn;
- // create the player thread lock.
- int rwli = pthread_rwlock_init(&conn->player_thread_lock, NULL);
- if (rwli != 0)
- die("Error %d initialising player_thread_lock for conversation thread %d.", rwli,
- conn->connection_number);
+ // create the watchdog mutex, initialise the watchdog time and start the watchdog thread;
+ conn->watchdog_bark_time = get_absolute_time_in_fp();
+ pthread_mutex_init(&conn->watchdog_mutex, NULL);
+ pthread_create(&conn->player_watchdog_thread, NULL, &player_watchdog_thread_code, (void *)conn);
+
+ int rc = pthread_mutex_init(&conn->flush_mutex, NULL);
+ if (rc)
+ die("Connection %d: error %d initialising flush_mutex.", conn->connection_number, rc);
+ rc = pthread_mutex_init(&conn->ab_mutex, NULL);
+ if (rc)
+ die("Connection %d: error %d initialising ab_mutex.", conn->connection_number, rc);
+// set the flowcontrol condition variable to wait on a monotonic clock
+#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(&conn->flowcontrol, &attr);
+#endif
+#ifdef COMPILE_FOR_OSX
+ rc = pthread_cond_init(&conn->flowcontrol, NULL);
+#endif
+ if (rc)
+ die("Connection %d: error %d initialising flow control condition variable.",
+ conn->connection_number, rc);
+ rc = pthread_mutex_init(&conn->volume_control_mutex, NULL);
+ if (rc)
+ die("Connection %d: error %d initialising volume_control_mutex.", conn->connection_number, rc);
- rtp_initialise(conn);
+ // nothing before this is cancellable
+ pthread_cleanup_push(rtsp_conversation_thread_cleanup_function, (void *)conn);
- rtsp_message *req, *resp;
- char *hdr, *auth_nonce = NULL;
+ rtp_initialise(conn);
+ char *hdr = NULL;
enum rtsp_read_request_response reply;
int rtsp_read_request_attempt_count = 1; // 1 means exit immediately
+ rtsp_message *req, *resp;
while (conn->stop == 0) {
int debug_level = 3; // for printing the request and response
reply = rtsp_read_request(conn, &req);
if (reply == rtsp_read_request_response_ok) {
+ pthread_cleanup_push(msg_cleanup_function, (void *)&req);
+ resp = msg_init();
+ pthread_cleanup_push(msg_cleanup_function, (void *)&resp);
+ resp->respcode = 400;
+
if (strcmp(req->method, "OPTIONS") !=
0) // the options message is very common, so don't log it until level 3
debug_level = 2;
- debug(debug_level,
- "RTSP thread %d received an RTSP Packet of type \"%s\":", conn->connection_number,
- req->method),
+ debug(debug_level, "Connection %d: Received an RTSP Packet of type \"%s\":",
+ conn->connection_number, req->method),
debug_print_msg_headers(debug_level, req);
- resp = msg_init();
- resp->respcode = 400;
apple_challenge(conn->fd, req, resp);
hdr = msg_get_header(req, "CSeq");
@@ -1906,7 +2351,7 @@ static void *rtsp_conversation_thread_func(void *pconn) {
// msg_add_header(resp, "Audio-Jack-Status", "connected; type=analog");
msg_add_header(resp, "Server", "AirTunes/105.1");
- if ((conn->authorized == 1) || (rtsp_auth(&auth_nonce, req, resp)) == 0) {
+ if ((conn->authorized == 1) || (rtsp_auth(&conn->auth_nonce, req, resp)) == 0) {
conn->authorized = 1; // it must have been authorized or didn't need a password
struct method_handler *mh;
int method_selected = 0;
@@ -1917,12 +2362,32 @@ static void *rtsp_conversation_thread_func(void *pconn) {
break;
}
}
- if (method_selected == 0)
- debug(1, "RTSP thread %d: Unrecognised and unhandled rtsp request \"%s\".",
+ if (method_selected == 0) {
+ debug(3, "Connection %d: Unrecognised and unhandled rtsp request \"%s\".",
conn->connection_number, req->method);
+
+ int y = req->contentlength;
+ if (y > 0) {
+ char obf[4096];
+ if (y > 4096)
+ y = 4096;
+ char *p = req->content;
+ char *obfp = obf;
+ int obfc;
+ for (obfc = 0; obfc < y; obfc++) {
+ snprintf(obfp, 3, "%02X", (unsigned int)*p);
+ p++;
+ obfp += 2;
+ };
+ *obfp = 0;
+ debug(3, "Content: \"%s\".", obf);
+ }
+ }
}
- debug(debug_level, "RTSP thread %d: RTSP Response:", conn->connection_number);
+ debug(debug_level, "Connection %d: RTSP Response:", conn->connection_number);
debug_print_msg_headers(debug_level, resp);
+
+ /*
fd_set writefds;
FD_ZERO(&writefds);
FD_SET(conn->fd, &writefds);
@@ -1930,11 +2395,27 @@ static void *rtsp_conversation_thread_func(void *pconn) {
memory_barrier();
} while (conn->stop == 0 &&
pselect(conn->fd + 1, NULL, &writefds, NULL, NULL, &pselect_sigset) <= 0);
+ */
+
if (conn->stop == 0) {
- msg_write_response(conn->fd, resp);
+ int err = msg_write_response(conn->fd, resp);
+ if (err) {
+ debug(1, "Connection %d: Unable to write an RTSP message response. Terminating the "
+ "connection.",
+ conn->connection_number);
+ struct linger so_linger;
+ so_linger.l_onoff = 1; // "true"
+ so_linger.l_linger = 0;
+ err = setsockopt(conn->fd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof so_linger);
+ if (err)
+ debug(1, "Could not set the RTSP socket to abort due to a write error on closing.");
+ conn->stop = 1;
+ // if (debuglev >= 1)
+ // debuglev = 3; // see what happens next
+ }
}
- msg_free(req);
- msg_free(resp);
+ pthread_cleanup_pop(1);
+ pthread_cleanup_pop(1);
} else {
int tstop = 0;
if (reply == rtsp_read_request_response_immediate_shutdown_requested)
@@ -1943,58 +2424,41 @@ static void *rtsp_conversation_thread_func(void *pconn) {
(reply == rtsp_read_request_response_read_error)) {
if (conn->player_thread) {
rtsp_read_request_attempt_count--;
- if (rtsp_read_request_attempt_count == 0)
+ if (rtsp_read_request_attempt_count == 0) {
tstop = 1;
- else {
+ if (reply == rtsp_read_request_response_read_error) {
+ struct linger so_linger;
+ so_linger.l_onoff = 1; // "true"
+ so_linger.l_linger = 0;
+ int err = setsockopt(conn->fd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof so_linger);
+ if (err)
+ debug(1, "Could not set the RTSP socket to abort due to a read error on closing.");
+ }
+ // debuglev = 3; // see what happens next
+ } else {
if (reply == rtsp_read_request_response_channel_closed)
- debug(2, "RTSP channel unexpectedly closed -- will try again %d time(s).",
- rtsp_read_request_attempt_count);
+ debug(2,
+ "Connection %d: RTSP channel unexpectedly closed -- will try again %d time(s).",
+ conn->connection_number, rtsp_read_request_attempt_count);
if (reply == rtsp_read_request_response_read_error)
- debug(2, "RTSP channel read error -- will try again %d time(s).",
- rtsp_read_request_attempt_count);
+ debug(2, "Connection %d: RTSP channel read error -- will try again %d time(s).",
+ conn->connection_number, rtsp_read_request_attempt_count);
usleep(20000);
}
} else {
tstop = 1;
}
} else {
- debug(1, "rtsp_read_request error %d, packet ignored.", (int)reply);
+ debug(1, "Connection %d: rtsp_read_request error %d, packet ignored.",
+ conn->connection_number, (int)reply);
}
if (tstop) {
- debug(3, "Synchronously terminate playing thread of RTSP conversation thread %d.",
- conn->connection_number);
-
- if (conn->player_thread)
- debug(1, "RTSP Channel unexpectedly closed or a serious error occured -- closing the "
- "player thread.");
- player_stop(conn);
- debug(3, "Successful termination of playing thread of RTSP conversation thread %d.",
- conn->connection_number);
- debug(3, "Request termination of RTSP conversation thread %d.", conn->connection_number);
+ debug(3, "Connection %d: Terminate RTSP connection.", conn->connection_number);
conn->stop = 1;
}
}
}
-
- if (conn->fd > 0)
- close(conn->fd);
- if (auth_nonce)
- free(auth_nonce);
- rtp_terminate(conn);
- if (playing_conn == conn) {
- debug(3, "Unlocking play lock on RTSP conversation thread %d.", conn->connection_number);
- playing_conn = NULL;
- pthread_mutex_unlock(&play_lock);
- }
- debug(2, "Connection %d: RTSP thread terminated.", conn->connection_number);
- conn->running = 0;
-
- // release the player_thread_lock
- int rwld = pthread_rwlock_destroy(&conn->player_thread_lock);
- if (rwld)
- debug(1, "Error %d destroying player_thread_lock for conversation thread %d.", rwld,
- conn->connection_number);
-
+ pthread_cleanup_pop(1);
pthread_exit(NULL);
}
@@ -2017,7 +2481,21 @@ 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.");
+ cancel_all_RTSP_threads();
+ int *sockfd = (int *)arg;
+ mdns_unregister();
+ if (sockfd)
+ free(sockfd);
+ pthread_setcancelstate(oldState, NULL);
+}
+
void rtsp_listen_loop(void) {
+ int oldState;
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
struct addrinfo hints, *info, *p;
char portstr[6];
int *sockfd = NULL;
@@ -2059,6 +2537,18 @@ void rtsp_listen_loop(void) {
fcntl(fd, F_SETFD, FD_CLOEXEC);
ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
+ struct timeval tv;
+ tv.tv_sec = 3; // three seconds write timeout
+ tv.tv_usec = 0;
+ if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof tv) == -1)
+ debug(1, "Error %d setting send timeout for rtsp writeback.", errno);
+
+ if ((config.dont_check_timeout == 0) && (config.timeout != 0)) {
+ tv.tv_sec = config.timeout; // 120 seconds read timeout by default.
+ tv.tv_usec = 0;
+ if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) == -1)
+ debug(1, "Error %d setting read timeout for rtsp connection.", errno);
+ }
#ifdef IPV6_V6ONLY
// some systems don't support v4 access on v6 sockets, but some do.
// since we need to account for two sockets we might as well
@@ -2110,12 +2600,12 @@ void rtsp_listen_loop(void) {
mdns_register();
- // printf("Listening for connections.");
- // shairport_startup_complete();
-
+ pthread_setcancelstate(oldState, NULL);
int acceptfd;
struct timeval tv;
+ pthread_cleanup_push(rtsp_listen_loop_cleanup_handler, (void *)sockfd);
do {
+ pthread_testcancel();
tv.tv_sec = 60;
tv.tv_usec = 0;
@@ -2150,7 +2640,8 @@ void rtsp_listen_loop(void) {
conn->fd = accept(acceptfd, (struct sockaddr *)&conn->remote, &slen);
if (conn->fd < 0) {
- debug(1, "New RTSP connection on port %d not accepted:", config.port);
+ debug(1, "Connection %d: New connection on port %d not accepted:", conn->connection_number,
+ config.port);
perror("failed to accept connection");
free(conn);
} else {
@@ -2169,8 +2660,8 @@ void rtsp_listen_loop(void) {
sa = (struct sockaddr_in *)&conn->remote;
inet_ntop(AF_INET, &(sa->sin_addr), remote_ip4, INET_ADDRSTRLEN);
unsigned short int rport = ntohs(sa->sin_port);
- debug(2, "New RTSP connection from %s:%u to self at %s:%u on conversation thread %d.",
- remote_ip4, rport, ip4, tport, conn->connection_number);
+ debug(2, "Connection %d: new connection from %s:%u to self at %s:%u.",
+ conn->connection_number, remote_ip4, rport, ip4, tport);
}
#ifdef AF_INET6
if (local_info->SAFAMILY == AF_INET6) {
@@ -2186,8 +2677,8 @@ void rtsp_listen_loop(void) {
sa6 = (struct sockaddr_in6 *)&conn->remote; // pretend this is loaded with something
inet_ntop(AF_INET6, &(sa6->sin6_addr), remote_ip6, INET6_ADDRSTRLEN);
u_int16_t rport = ntohs(sa6->sin6_port);
- debug(2, "New RTSP connection from [%s]:%u to self at [%s]:%u on conversation thread %d.",
- remote_ip6, rport, ip6, tport, conn->connection_number);
+ debug(2, "Connection %d: new connection from [%s]:%u to self at [%s]:%u.",
+ conn->connection_number, remote_ip6, rport, ip6, tport);
}
#endif
@@ -2199,23 +2690,22 @@ void rtsp_listen_loop(void) {
// conn->thread = rtsp_conversation_thread;
// conn->stop = 0; // record's memory has been zeroed
// conn->authorized = 0; // record's memory has been zeroed
- fcntl(conn->fd, F_SETFL, O_NONBLOCK);
+ // fcntl(conn->fd, F_SETFL, O_NONBLOCK);
ret = pthread_create(&conn->thread, NULL, rtsp_conversation_thread_func,
conn); // also acts as a memory barrier
- if (ret)
- die("Failed to create RTSP receiver thread %d!", conn->connection_number);
+ if (ret) {
+ char errorstring[1024];
+ strerror_r(ret, (char *)errorstring, sizeof(errorstring));
+ die("Connection %d: cannot create an RTSP conversation thread. Error %d: \"%s\".",
+ conn->connection_number, ret, (char *)errorstring);
+ }
debug(3, "Successfully created RTSP receiver thread %d.", conn->connection_number);
conn->running = 1; // this must happen before the thread is tracked
track_thread(conn);
}
} while (1);
- mdns_unregister();
-
- if (sockfd)
- free(sockfd);
-
- // perror("select");
- // die("fell out of the RTSP select loop");
+ pthread_cleanup_pop(1); // should never happen
+ debug(1, "Oops -- fell out of the RTSP select loop");
}
diff --git a/rtsp.h b/rtsp.h
index eae28df..71e5b22 100644
--- a/rtsp.h
+++ b/rtsp.h
@@ -4,14 +4,18 @@
#include "player.h"
rtsp_conn_info *playing_conn;
+rtsp_conn_info **conns;
void rtsp_listen_loop(void);
// void rtsp_shutdown_stream(void);
void rtsp_request_shutdown_stream(void);
-// initialise the metadata stuff
+void cancel_all_RTSP_threads(void);
+
+// initialise and completely delete the metadata stuff
void metadata_init(void);
+void metadata_stop(void);
// sends metadata out to the metadata pipe, if enabled.
// It is sent with the type 'ssnc' the given code, data and length
diff --git a/scripts/shairport-sync-config b/scripts/shairport-sync-config
new file mode 100755
index 0000000..ce852aa
--- /dev/null
+++ b/scripts/shairport-sync-config
@@ -0,0 +1,169 @@
+#!/bin/sh
+#
+# shairport-sync-config, Copyright 2019 Mike Brady
+#
+# Based, with thanks, on:
+# avahi-daemon-config, Copyright 2011 Yaakov Selkowitz
+#
+# This file is part of the Cygwin port of shairport-sync.
+
+# ======================================================================
+# Initialization
+# ======================================================================
+PROGNAME=$(basename $0)
+_tdir=$(dirname $0)
+PROGDIR=$(cd $_tdir && pwd)
+
+CSIH_SCRIPT=/usr/share/csih/cygwin-service-installation-helper.sh
+
+# Subdirectory where the new package is being installed
+PREFIX=/usr/local/bin
+
+# Directory where the config files are stored
+SYSCONFDIR=/etc
+LOGDIR=/var/log
+SOCKDIR=/var/run
+
+source ${CSIH_SCRIPT}
+
+# ======================================================================
+# Routine: install_service
+# Install shairport-sync as a service
+# ======================================================================
+install_service() {
+
+ if csih_is_nt
+ then
+
+ # Check if shairport-sync is installed and remove on user request.
+ if cygrunsrv -Q shairport-sync > /dev/null 2>&1
+ then
+ csih_warning "The shairport-sync service is already installed."
+ echo
+ if csih_request "Do you want to reinstall it with different args?"
+ then
+ cygrunsrv -E shairport-sync
+ cygrunsrv -R shairport-sync
+ fi
+ fi
+
+ # Install shairport-sync service if it is not already installed
+ if ! cygrunsrv -Q shairport-sync > /dev/null 2>&1
+ then
+ echo
+ csih_warning "The following function requires administrator privileges!"
+ if csih_request "Do you want to install shairport-sync as a service?"
+ then
+ if cygrunsrv -I shairport-sync --disp "CYGWIN Shairport Sync" --desc "AirPlay Audio. Use your computer's speakers or headphones to listen to audio from your iOS devices or other AirPlay sources on your network." --path $PREFIX/shairport-sync --dep avahi-daemon --dep messagebus
+ then
+ echo
+ csih_inform "The shairport-sync service has been installed under the LocalSystem"
+ csih_inform "account (also known as SYSTEM). To start the service now, call"
+ csih_inform "\`net start shairport-sync' or \`cygrunsrv -S shairport-sync'. Otherwise,"
+ csih_inform "it will start automatically after the next reboot."
+ echo
+ csih_inform "Check ${SYSCONFDIR}/shairport-sync.conf first, if it suits your needs."
+ fi
+ fi # user allowed us to install shairport-sync
+ fi # shairport-sync already installed
+ fi # csih_is_nt
+} # --- End of install_service --- #
+
+
+# ======================================================================
+# Main Entry Point
+# ======================================================================
+
+
+# Check how the script has been started. If
+# (1) it has been started by giving the full path and
+# that path is /etc/postinstall, OR
+# (2) Otherwise, if the environment variable
+# CONFIG_AUTO_ANSWER_NO is set
+# then set auto_answer to "no". This allows automatic
+# creation of the config files in /etc w/o overwriting
+# them if they already exist. In both cases, color
+# escape sequences are suppressed, so as to prevent
+# cluttering setup's logfiles.
+if [ "$PROGDIR" = "/etc/postinstall" ]
+then
+ csih_auto_answer="no"
+ csih_disable_color
+fi
+if [ -n "${CONFIG_AUTO_ANSWER_NO}" ]
+then
+ csih_auto_answer="no"
+ csih_disable_color
+fi
+
+
+# ======================================================================
+# Parse options
+# ======================================================================
+while :
+do
+ case $# in
+ 0)
+ break
+ ;;
+ esac
+
+ option=$1
+ shift
+
+ case "$option" in
+ -d | --debug )
+ set -x
+ csih_trace_on
+ ;;
+
+ -y | --yes )
+ csih_auto_answer=yes
+ ;;
+
+ -n | --no )
+ csih_auto_answer=no
+ ;;
+
+ *)
+ echo "usage: ${PROGNAME} [OPTION]..."
+ echo
+ echo "This script creates a basic shairport-sync configuration."
+ echo
+ echo "Options:"
+ echo " --debug -d Enable shell's debug output."
+ echo " --yes -y Answer all questions with \"yes\" automatically."
+ echo " --no -n Answer all questions with \"no\" automatically."
+ echo
+ exit 1
+ ;;
+
+ esac
+done
+
+# ======================================================================
+# Action!
+# ======================================================================
+
+# Check for ${SYSCONFDIR} directory
+csih_make_dir "${SYSCONFDIR}" "Cannot create global configuration files."
+chmod 775 "${SYSCONFDIR}"
+setfacl -m u:system:rwx "${SYSCONFDIR}"
+
+# Check for ${LOGDIR} directory
+csih_make_dir "${LOGDIR}" "Syslogging using shairport-sync will not work."
+chmod 775 "${LOGDIR}"
+setfacl -m u:system:rwx "${LOGDIR}"
+
+# Check for ${SOCKDIR} directory
+csih_make_dir "${SOCKDIR}" "SOCKET files of running processes will not be created."
+chmod 775 "${SOCKDIR}"
+setfacl -m u:system:rwx "${SOCKDIR}"
+
+# maybe: csih_auto_answer=no will skip,
+# interactive user will get a chance to override
+install_service
+
+
+echo
+echo "Configuration finished. Have fun!"
diff --git a/scripts/shairport-sync-dbus-policy-cygwin.conf b/scripts/shairport-sync-dbus-policy-cygwin.conf
new file mode 100644
index 0000000..7c47c01
--- /dev/null
+++ b/scripts/shairport-sync-dbus-policy-cygwin.conf
@@ -0,0 +1,18 @@
+<!-- initial version, based on /etc/dbus-1/system.d/avahi-dbus.conf, with thanks -->
+<!DOCTYPE busconfig PUBLIC
+ "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+
+ <!-- Only user SYSTEM can own the Shairport Sync service -->
+ <policy user="SYSTEM">
+ <allow own="org.gnome.ShairportSync"/>
+ </policy>
+
+ <!-- Allow anyone to invoke methods on Shairport Sync server -->
+ <policy context="default">
+ <allow send_destination="org.gnome.ShairportSync"/>
+ <allow receive_sender="org.gnome.ShairportSync"/>
+ </policy>
+
+</busconfig>
diff --git a/scripts/shairport-sync-mpris-policy-cygwin.conf b/scripts/shairport-sync-mpris-policy-cygwin.conf
new file mode 100644
index 0000000..ea839af
--- /dev/null
+++ b/scripts/shairport-sync-mpris-policy-cygwin.conf
@@ -0,0 +1,18 @@
+<!-- initial version, based on /etc/dbus-1/system.d/avahi-dbus.conf, with thanks -->
+<!DOCTYPE busconfig PUBLIC
+ "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+
+ <!-- Only user "SYSTEM" can own the Shairport Sync MPRIS service -->
+ <policy user="SYSTEM">
+ <allow own="org.mpris.MediaPlayer2.ShairportSync"/>
+ </policy>
+
+ <!-- Allow anyone to invoke methods on Shairport Sync MPRIS server -->
+ <policy context="default">
+ <allow send_destination="org.mpris.MediaPlayer2.ShairportSync"/>
+ <allow receive_sender="org.mpris.MediaPlayer2.ShairportSync"/>
+ </policy>
+
+</busconfig>
diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf
index b388060..7b030d6 100644
--- a/scripts/shairport-sync.conf
+++ b/scripts/shairport-sync.conf
@@ -1,5 +1,7 @@
// Sample Configuration File for Shairport Sync
// Commented out settings are generally the defaults, except where noted.
+// Some sections are operative only if Shairport Sync has been built with the right configuration flags.
+// See the individual sections for details.
// General Settings
general =
@@ -10,15 +12,15 @@ general =
// %h for the hostname,
// %H for the Hostname (i.e. with first letter capitalised (ASCII only)),
// %v for the version number, e.g. 3.0 and
-// %V for the full version string, e.g. 3.0-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc
+// %V for the full version string, e.g. 3.3-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc
// Overall length can not exceed 50 characters. Example: "Shairport Sync %v on %H".
// password = "secret"; // leave this commented out if you don't want to require a password
-// interpolation = "basic"; // aka "stuffing". Default is "basic", alternative is "soxr". Use "soxr" only if you have a reasonably fast processor.
+// interpolation = "auto"; // aka "stuffing". Default is "auto". Alternatives are "basic" or "soxr". Choose "soxr" only if you have a reasonably fast processor and Shairport Sync has been built with "soxr" support.
// output_backend = "alsa"; // Run "shairport-sync -h" to get a list of all output_backends, e.g. "alsa", "pipe", "stdout". The default is the first one.
// mdns_backend = "avahi"; // Run "shairport-sync -h" to get a list of all mdns_backends. The default is the first one.
// port = 5000; // Listen for service requests on this port
// udp_port_base = 6001; // start allocating UDP ports from this port number when needed
-// udp_port_range = 100; // 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.
+// 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.
// 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
// 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.
@@ -29,6 +31,7 @@ general =
// volume_control_profile = "standard" ; // use this advanced setting to specify how the airplay volume is transferred to the mixer volume.
// "standard" makes the volume change more quickly at lower volumes and slower at higher volumes.
// "flat" makes the volume change at the same rate at all volumes.
+// volume_range_combined_hardware_priority = "no"; // when extending the volume range by combining the built-in software attenuator with the hardware mixer attenuator, set this to "yes" to reduce volume by using the hardware mixer first, then the built-in software attenuator.
// run_this_when_volume_is_set = "/full/path/to/application/and/args"; // Run the specified application whenever the volume control is set or changed.
// 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".
@@ -37,10 +40,12 @@ general =
// 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.15; // 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_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_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.
// 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".
@@ -48,12 +53,22 @@ general =
// as "org.gnome.ShairportSync" on the whichever bus you specify here: "system" (default) or "session".
};
-// Advanced parameters for controlling how Shairport Sync runs a play session
+// 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_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!
// wait_for_completion = "no"; // set to "yes" to get Shairport Sync to wait until the "run_this..." applications have terminated before continuing
+
// allow_session_interruption = "no"; // set to "yes" to allow another device to interrupt Shairport Sync while it's playing from an existing audio source
// session_timeout = 120; // wait for this number of seconds after a source disappears before terminating the session and becoming available again.
};
@@ -61,21 +76,28 @@ sessioncontrol =
// Back End Settings
// These are parameters for the "alsa" audio back end.
+// For this section to be operative, Shairport Sync must be built with the following configuration flag:
+// --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.
// 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 = 44100; // can be 44100, 88200, 176400 or 352800, but the device must have the capability.
-// output_format = "S16"; // can be "U8", "S8", "S16", "S24", "S24_3LE", "S24_3BE" or "S32", but the device must have the capability. Except where stated using (*LE or *BE), endianness matches that of the processor.
+// 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".
// 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"
// 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"; // If the output device is a real hardware device, precision timing will be used, which is needed for "disable_standby_mode" below. 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". Needs precision timing to be available.
};
// Parameters for the "sndio" audio back end. All are optional.
+// For this section to be operative, Shairport Sync must be built with the following configuration flag:
+// --with-sndio
sndio =
{
// device = "snd/0"; // optional setting to set the name of the output device. Default is the sndio system default.
@@ -86,23 +108,46 @@ sndio =
};
// Parameters for the "pa" PulseAudio backend.
+// For this section to be operative, Shairport Sync must be built with the following configuration flag:
+// --with-pa
pa =
{
// application_name = "Shairport Sync"; //Set this to the name that should appear in the Sounds "Applications" tab when Shairport Sync is active.
};
+// Parameters for the "jack" JACK Audio Connection Kit backend.
+// For this section to be operative, Shairport Sync must be built with the following configuration flag:
+// --with-jack
+jack =
+{
+// client_name = "shairport-sync"; // Set this to the name of the client that should appear in "Connections" when Shairport Sync is active.
+// autoconnect_pattern = ""; // Set this to a POSIX regular expression pattern that describes the ports you would like to connect to
+// automatically. Examples:
+// "system:playback_[12]"
+// "some_app_[0-9]*:in-[LR]"
+// "jack_mixer:in_2[78]"
+// Beware: if you make a syntax error, libjack might crash. In that case, fix it and start over.
+// For a good overview, look here: https://www.ibm.com/support/knowledgecenter/SS8NLW_11.0.1/com.ibm.swg.im.infosphere.dataexpl.engine.doc/c_posix-regex-examples.html
+};
+
// Parameters for the "pipe" audio back end, a back end that directs raw CD-style audio output to a pipe. No interpolation is done.
+// For this section to be operative, Shairport Sync must have been built with the following configuration flag:
+// --with-pipe
pipe =
{
// name = "/path/to/pipe"; // there is no default pipe name for the output
};
-// These are no configuration file parameters for the "stdout" audio back end. No interpolation is done.
-
-// These are no configuration file parameters for the "ao" audio back end. No interpolation is done.
+// There are no configuration file parameters for the "stdout" audio back end. No interpolation is done.
+// To include support for the "stdout" backend, Shairport Sync must be built with the following configuration flag:
+// --with-stdout
-// Static latency settings are deprecated and the settings have been removed.
+// There are no configuration file parameters for the "ao" audio back end. No interpolation is done.
+// To include support for the "ao" backend, Shairport Sync must be built with the following configuration flag:
+// --with-ao
+// For this section to be operative, Shairport Sync must be built with the following configuration flag:
+// --with-convolution
dsp =
{
@@ -112,7 +157,7 @@ dsp =
// and apply a correction to get a flat response curve.
//////////////////////////////////////////
//
-// convolution = "yes"; // Activate the convolution filter.
+// convolution = "no"; // Set this to "yes" to activate the convolution filter.
// convolution_ir_file = "impulse.wav"; // Impulse Response file to be convolved to the audio stream
// convolution_gain = -4.0; // Static gain applied to prevent clipping during the convolution process
// convolution_max_length = 44100; // Truncate the input file to this length in order to save CPU.
@@ -127,16 +172,19 @@ dsp =
// The setting "loudness_reference_volume_db" should be set at the volume reported by shairport-sync when listening to music at a normal listening volume.
//////////////////////////////////////////
//
-// loudness = "yes"; // Activate the filter
+// loudness = "no"; // Set this to "yes" to activate the loudness filter
// loudness_reference_volume_db = -20.0; // Above this level the filter will have no effect anymore. Below this level it will gradually boost the low frequencies.
};
// How to deal with metadata, including artwork
+// For this section to be operative, Shairport Sync must be built with at one (or more) of the following configuration flags:
+// --with-metadata, --with-dbus-interface, --with-mpris-interface or --with-mqtt-client.
+// In those cases, "enabled" and "include_cover_art" will both be "yes" by default
metadata =
{
-// enabled = "no"; // set this to yes to get Shairport Sync to solicit metadata from the source and to pass it on via a pipe
-// include_cover_art = "no"; // set to "yes" to get Shairport Sync to solicit cover art from the source and pass it via the pipe. You must also set "enabled" to "yes".
+// enabled = "yes"; // set this to yes to get Shairport Sync to solicit metadata from the source and to pass it on via a pipe
+// include_cover_art = "yes"; // set to "yes" to get Shairport Sync to solicit cover art from the source and pass it via the pipe. You must also set "enabled" to "yes".
// pipe_name = "/tmp/shairport-sync-metadata";
// pipe_timeout = 5000; // wait for this number of milliseconds for a blocked pipe to unblock before giving up
// socket_address = "226.0.0.1"; // if set to a host name or IP address, UDP packets containing metadata will be sent to this address. May be a multicast address. "socket-port" must be non-zero and "enabled" must be set to yes"
@@ -144,6 +192,30 @@ metadata =
// socket_msglength = 65000; // the maximum packet size for any UDP metadata. This will be clipped to be between 500 or 65000. The default is 500.
};
+// How to enable the MQTT-metadata/remote-service
+// For this section to be operative, Shairport Sync must be built with the following configuration flag:
+// --with-mqtt-client
+mqtt =
+{
+// enabled = "no"; // set this to yes to enable the mqtt-metadata-service
+// hostname = "iot.eclipse.org"; // Hostname of the MQTT Broker
+// port = "1883";
+// username = NULL; //set this to a string with your username, to enable username authentication
+// password = NULL; //set this to a string with your password, to enable username & password authentication
+// capath = NULL; //set this to the folder with the CA-Certificates to be accepted for the server certificate. If not set, TLS is not used
+// cafile = NULL; //this may be used as an (exclusive) alternative to capath with a single file for all ca-certificates
+// certfile = NULL; //set this to a string to a user certificate to enable MQTT Client certificates. keyfile must also be set!
+// keyfile = NULL; //private key for MQTT Client authentication
+// topic = NULL; //MQTT topic where this instance of shairport-sync should publish. If not set, the general.name value is used.
+// publish_raw = "no"; //whether to publish all available metadata under the codes given in the 'metadata' docs.
+// publish_parsed = "no"; //whether to publish a small (but useful) subset of metadata under human-understandable topics
+// Currently published topics:artist,album,title,genre,format,songalbum,volume,client_ip,
+// Additionally, empty messages at the topics play_start,play_end,play_flush,play_resume are published
+// publish_cover = "no"; //whether to publish the cover over mqtt in binary form. This may lead to a bit of load on the broker
+// enable_remote = "no"; //whether to remote control via MQTT. RC is available under `topic`/remote.
+// Available commands are "command", "beginff", "beginrew", "mutetoggle", "nextitem", "previtem", "pause", "playpause", "play", "stop", "playresume", "shuffle_songs", "volumedown", "volumeup"
+}
+
// Diagnostic settings. These are for diagnostic and debugging only. Normally you sould leave them commented out
diagnostics =
{
@@ -151,6 +223,6 @@ diagnostics =
// 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_time_since_startup = "no"; // set this to yes if you want the time since startup in the debug message -- seconds down to nanoseconds
-// log_show_time_since_last_message = "no"; // set this to yes if you want the time since the last debug message in the debug message -- seconds down to nanoseconds
+// log_show_time_since_last_message = "yes"; // set this to yes if you want the time since the last debug message in the debug message -- seconds down to nanoseconds
// drop_this_fraction_of_audio_packets = 0.0; // use this to simulate a noisy network where this fraction of UDP packets are lost in transmission. E.g. a value of 0.001 would mean an average of 0.1% of packets are lost, which is actually quite a high figure.
};
diff --git a/scripts/shairport-sync.freebsd b/scripts/shairport-sync.freebsd
index 9088bd8..4e812dd 100644
--- a/scripts/shairport-sync.freebsd
+++ b/scripts/shairport-sync.freebsd
@@ -14,7 +14,7 @@ stop_cmd="${name}_stop"
shairport_sync_start()
{
- checkyesno shairport_sync_enable && echo "Starting shairport-sync." && \
+ checkyesno shairport_sync_enable && echo "Starting shairport-sync." && \
su -m shairport-sync -c "/usr/local/bin/shairport-sync -d"
}
diff --git a/scripts/shairport-sync.in b/scripts/shairport-sync.in
index 6bcfe9f..f229f19 100755
--- a/scripts/shairport-sync.in
+++ b/scripts/shairport-sync.in
@@ -23,8 +23,7 @@ DAEMON=@prefix@/bin/$NAME
# impossible to pass as arguments.
# Instead, we add the arguments directly to the relevant line in the do_start() function below
-
-PIDFILE=/var/run/$NAME.pid
+PIDFILE=/var/run/$NAME/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME
# Exit if the package is not installed
@@ -48,31 +47,19 @@ do_start()
{
# Return
# 0 if daemon has been started
- # 1 if daemon was already running
- # 2 if daemon could not be started
- start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
- || return 1
-
-# This script is set to start running after all other services have started.
-# However, if you find that Shairport Sync is still being started before what it needs is ready,
-# uncomment the next line to get the script to wait for three seconds before attempting to start Shairport Sync.
-# sleep 3
-
-# Uncomment just one of the following start-stop-daemon lines, or comment them all out and add your own.
-# Shairport Sync will read settings from the configuration file (/etc/shairport-sync.conf by default) and will then apply any command line options.
-# In the default script, the first line is uncommented, selecting daemon mode (-d). Settings will be taken from the configuration file.
-# BTW, if you're using software volume control, you may have to use alsamixer or similar to set the output device's volume to its maximum level first
-# BTW2, you can use alsamixer to find device identifiers (e.g. hw:1) and mixer names (e.g. "Speaker"). No need to change ALSA's defaults.
-# BTW3, the argument after -a is simply the name the shairport service will be visible as.
- start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- -d || return 2
-# start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- -d -a "Topping TP30 or Griffin iMic on Raspberry Pi" -- -d hw:1 -t hardware -c "PCM" || return 2
-# start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- -d -a "'3D Sound' USB Soundcard on Raspberry Pi" -- -d hw:1 -t hardware -c "Speaker" || return 2
-# BTW, that "3D Sound" USB soundcard sometimes has the mixer name "Headphone" rather than "Speaker" -- use alsamixer to check.
-# start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- -d -a "IQaudIO" -- -d hw:1 -t hardware -c "Playback Digital" || return 2
-# BTW, newer versions of IQaudIO have a different mixer name -- use alsamixer to check.
- # Add code here, if necessary, that waits for the process to be ready
- # to handle requests from services started subsequently which depend
- # on this one. As a last resort, sleep for some time.
+ # 1 if PID directory didn't exist and couldn't be created with approproate permission
+ # 2 if daemon was already running
+ # 3 if daemon could not be started
+ [ -e /var/run/shairport-sync ] || ( mkdir -p /var/run/shairport-sync && chown shairport-sync:shairport-sync /var/run/shairport-sync ) || return 1
+ start-stop-daemon -c shairport-sync:shairport-sync --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null || return 2
+
+ # This script is set to start running after all other services have started.
+ # However, if you find that Shairport Sync is still being started before what it needs is ready,
+ # uncomment the next line to get the script to wait for three seconds before attempting to start Shairport Sync.
+ # sleep 3
+
+ # Settings from the configuration file (@sysconfdir@/shairport-sync.conf by default)
+ start-stop-daemon -c shairport-sync:shairport-sync --start --quiet --pidfile $PIDFILE --exec $DAEMON -- -d || return 3
}
#
diff --git a/shairport-sync-dbus-test-client.c b/shairport-sync-dbus-test-client.c
index 469c98c..4d5703a 100644
--- a/shairport-sync-dbus-test-client.c
+++ b/shairport-sync-dbus-test-client.c
@@ -97,7 +97,7 @@ int main(int argc, char *argv[]) {
if (argc > 2) {
poptPrintHelp(optCon, stderr, 0);
- exit(1);
+ exit(EXIT_FAILURE);
}
/* Now do options processing */
diff --git a/shairport-sync-mpris-test-client.c b/shairport-sync-mpris-test-client.c
index 2ac4f72..c15c71b 100644
--- a/shairport-sync-mpris-test-client.c
+++ b/shairport-sync-mpris-test-client.c
@@ -70,7 +70,7 @@ int main(int argc, char *argv[]) {
if (argc > 2) {
poptPrintHelp(optCon, stderr, 0);
- exit(1);
+ exit(EXIT_FAILURE);
}
/* Now do options processing */
diff --git a/shairport-sync.spec b/shairport-sync.spec
index 9d4f83c..8d9bedd 100644
--- a/shairport-sync.spec
+++ b/shairport-sync.spec
@@ -1,5 +1,5 @@
Name: shairport-sync
-Version: 3.2.2
+Version: 3.3
Release: 1%{?dist}
Summary: AirTunes emulator. Multi-Room with Audio Synchronisation
# MIT licensed except for tinysvcmdns under BSD,
@@ -14,7 +14,6 @@ BuildRequires: systemd
BuildRequires: pkgconfig(libconfig)
BuildRequires: pkgconfig(popt)
BuildRequires: pkgconfig(openssl)
-BuildRequires: pkgconfig(libdaemon)
BuildRequires: pkgconfig(avahi-core)
BuildRequires: pkgconfig(alsa)
BuildRequires: pkgconfig(soxr)
@@ -67,6 +66,8 @@ getent passwd %{name} &> /dev/null || useradd --system -c "%{name} User" \
%license LICENSES
%changelog
+* Fri May 24 2019 Mike Brady <mikebrady@eircom.net) 3.3
+- Audio enhancements, stability improvements, MQTT and Jack Audio interfaces.
* Sun Oct 14 2018 Mike Brady <mikebrady@eircom.net) 3.2.2
- Compatibility with iOS 12 and mac OS Mojave AirPlay latencies. Minor bug fix.
* Fri Jul 13 2018 Mike Brady <mikebrady@eircom.net) 3.2.1
diff --git a/shairport.c b/shairport.c
index 69109e8..9e9400b 100644
--- a/shairport.c
+++ b/shairport.c
@@ -2,7 +2,7 @@
* Shairport, an Apple Airplay receiver
* Copyright (c) James Laird 2013
* All rights reserved.
- * Modifications (c) Mike Brady 2014--2018
+ * Modifications and additions (c) Mike Brady 2014--2019
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
@@ -31,99 +31,112 @@
#include <libconfig.h>
#include <libgen.h>
#include <memory.h>
-#include <net/if.h>
#include <popt.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
-#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
+#include <net/if.h>
#include <unistd.h>
+
#include "config.h"
-#ifdef HAVE_LIBMBEDTLS
+#ifdef CONFIG_MBEDTLS
#include <mbedtls/md5.h>
+#include <mbedtls/version.h>
#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
#include <polarssl/md5.h>
#endif
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_OPENSSL
#include <openssl/md5.h>
#endif
-#if defined(HAVE_DBUS)
+#if defined(CONFIG_DBUS_INTERFACE)
#include <glib.h>
#endif
-#if defined(HAVE_DACP_CLIENT)
+#include "activity_monitor.h"
+#include "common.h"
+#include "rtp.h"
+#include "rtsp.h"
+
+#if defined(CONFIG_DACP_CLIENT)
#include "dacp.h"
#endif
-#if defined(HAVE_METADATA_HUB)
+#if defined(CONFIG_METADATA_HUB)
#include "metadata_hub.h"
#endif
-#ifdef HAVE_DBUS
+#ifdef CONFIG_DBUS_INTERFACE
#include "dbus-service.h"
#endif
-#ifdef HAVE_MPRIS
-#include "mpris-service.h"
+#ifdef CONFIG_MQTT
+#include "mqtt.h"
#endif
-#include "common.h"
-#include "mdns.h"
-#include "rtp.h"
-#include "rtsp.h"
+#ifdef CONFIG_MPRIS_INTERFACE
+#include "mpris-service.h"
+#endif
+#ifdef CONFIG_LIBDAEMON
#include <libdaemon/dexec.h>
#include <libdaemon/dfork.h>
#include <libdaemon/dlog.h>
#include <libdaemon/dpid.h>
#include <libdaemon/dsignal.h>
+#else
+#include <syslog.h>
+#endif
+
+#ifdef CONFIG_SOXR
+#include <soxr.h>
+#include <math.h>
+#endif
#ifdef CONFIG_CONVOLUTION
#include <FFTConvolver/convolver.h>
#endif
-static int shutting_down = 0;
+
+#ifdef CONFIG_LIBDAEMON
+pid_t pid;
+#endif
+
+int killOption = 0;
+int daemonisewith = 0;
+int daemonisewithout = 0;
+
+// static int shutting_down = 0;
char configuration_file_path[4096 + 1];
char actual_configuration_file_path[4096 + 1];
-void shairport_shutdown() {
- if (shutting_down)
- return;
- shutting_down = 1;
- mdns_unregister();
- rtsp_request_shutdown_stream();
- if (config.output)
- config.output->deinit();
-}
-
static void sig_ignore(__attribute__((unused)) int foo, __attribute__((unused)) siginfo_t *bar,
__attribute__((unused)) void *baz) {}
static void sig_shutdown(__attribute__((unused)) int foo, __attribute__((unused)) siginfo_t *bar,
__attribute__((unused)) void *baz) {
- debug(1, "shutdown requested...");
- shairport_shutdown();
- // daemon_log(LOG_NOTICE, "exit...");
- daemon_retval_send(255);
- daemon_pid_file_remove();
- exit(0);
+ debug(2, "shutdown requested...");
+#ifdef CONFIG_LIBDAEMON
+ if (pid == 0) {
+ daemon_retval_send(255);
+ daemon_pid_file_remove();
+ }
+#endif
+ exit(EXIT_SUCCESS);
}
static void sig_child(__attribute__((unused)) int foo, __attribute__((unused)) siginfo_t *bar,
__attribute__((unused)) void *baz) {
+ // wait for child processes to exit
pid_t pid;
while ((pid = waitpid((pid_t)-1, 0, WNOHANG)) > 0) {
- if (pid == mdns_pid && !shutting_down) {
- die("MDNS child process died unexpectedly!");
- }
}
}
@@ -151,18 +164,90 @@ void print_version(void) {
}
}
+#ifdef CONFIG_SOXR
+pthread_t soxr_time_check_thread;
+void* soxr_time_check(__attribute__((unused)) void *arg) {
+ const int buffer_length = 352;
+ int32_t inbuffer[buffer_length*2];
+ int32_t outbuffer[(buffer_length+1)*2];
+
+ //int32_t *outbuffer = (int32_t*)malloc((buffer_length+1)*2*sizeof(int32_t));
+ //int32_t *inbuffer = (int32_t*)malloc((buffer_length)*2*sizeof(int32_t));
+
+ // generate a sample signal
+ const double frequency = 440; //
+
+ int i;
+
+ int number_of_iterations = 0;
+ uint64_t soxr_start_time = get_absolute_time_in_fp();
+ uint64_t loop_until_time = (uint64_t)0x180000000 + soxr_start_time; // loop for a second and a half, max
+ while (get_absolute_time_in_fp() < loop_until_time) {
+
+ number_of_iterations++;
+ for (i = 0; i < buffer_length ; i++) {
+ double w = sin(i * (frequency + number_of_iterations * 2) * 2 * M_PI/44100);
+ int32_t wint = (int32_t)(w * INT32_MAX);
+ inbuffer[i * 2] = wint;
+ inbuffer[i * 2 + 1] = wint;
+ }
+
+ soxr_io_spec_t io_spec;
+ io_spec.itype = SOXR_INT32_I;
+ io_spec.otype = SOXR_INT32_I;
+ io_spec.scale = 1.0; // this seems to crash if not = 1.0
+ io_spec.e = NULL;
+ io_spec.flags = 0;
+
+ size_t odone;
+
+ soxr_oneshot(buffer_length, buffer_length + 1, 2, // Rates and # of chans.
+ inbuffer, buffer_length, NULL, // Input.
+ outbuffer, buffer_length + 1, &odone, // Output.
+ &io_spec, // Input, output and transfer spec.
+ NULL, NULL); // Default configuration.
+
+ io_spec.itype = SOXR_INT32_I;
+ io_spec.otype = SOXR_INT32_I;
+ io_spec.scale = 1.0; // this seems to crash if not = 1.0
+ io_spec.e = NULL;
+ io_spec.flags = 0;
+
+ soxr_oneshot(buffer_length, buffer_length - 1, 2, // Rates and # of chans.
+ inbuffer, buffer_length, NULL, // Input.
+ outbuffer, buffer_length - 1, &odone, // Output.
+ &io_spec, // Input, output and transfer spec.
+ NULL, NULL); // Default configuration.
+
+ }
+
+ double soxr_execution_time_us =
+ (((get_absolute_time_in_fp() - soxr_start_time) * 1000000) >> 32) * 1.0;
+ // free(outbuffer);
+ // free(inbuffer);
+ config.soxr_delay_index = (int)(0.9 + soxr_execution_time_us/(number_of_iterations *1000));
+ debug(2,"soxr_delay_index: %d.", config.soxr_delay_index);
+ if ((config.packet_stuffing == ST_soxr) && (config.soxr_delay_index > config.soxr_delay_threshold))
+ inform("Note: this device may be too slow for \"soxr\" interpolation. Consider choosing the \"basic\" or \"auto\" interpolation setting.");
+ if (config.packet_stuffing == ST_auto)
+ debug(1,"\"%s\" interpolation has been chosen.", config.soxr_delay_index <= config.soxr_delay_threshold ? "soxr" : "basic");
+ pthread_exit(NULL);
+}
+
+#endif
+
void usage(char *progname) {
printf("Usage: %s [options...]\n", progname);
printf(" or: %s [options...] -- [audio output-specific options]\n", progname);
printf("\n");
printf("Options:\n");
printf(" -h, --help show this help.\n");
+#ifdef CONFIG_LIBDAEMON
printf(" -d, --daemon daemonise.\n");
printf(" -j, --justDaemoniseNoPIDFile daemonise without a PID file.\n");
- printf(" -V, --version show version information.\n");
printf(" -k, --kill kill the existing shairport daemon.\n");
- printf(" -D, --disconnectFromOutput disconnect immediately from the output device.\n");
- printf(" -R, --reconnectToOutput reconnect to the output device.\n");
+#endif
+ printf(" -V, --version show version information.\n");
printf(" -c, --configfile=FILE read configuration settings from FILE. Default is "
"/etc/shairport-sync.conf.\n");
@@ -175,10 +260,9 @@ void usage(char *progname) {
printf(" -L, --latency=FRAMES [Deprecated] Set the latency for audio sent from an unknown "
"device.\n");
printf(" The default is to set it automatically.\n");
- printf(" The default is to set it automatically.\n");
printf(" -S, --stuffing=MODE set how to adjust current latency to match desired latency, "
"where \n");
- printf(" \"basic\" (default) inserts or deletes audio frames from "
+ printf(" \"basic\" inserts or deletes audio frames from "
"packet frames with low processor overhead, and \n");
printf(" \"soxr\" uses libsoxr to minimally resample packet frames -- "
"moderate processor overhead.\n");
@@ -212,11 +296,13 @@ void usage(char *progname) {
printf(" --logOutputLevel log the output level setting -- useful for setting maximum "
"volume.\n");
#ifdef CONFIG_METADATA
+ printf(" -M, --metadata-enable ask for metadata from the source and process it.\n");
printf(" --metadata-pipename=PIPE send metadata to PIPE, e.g. "
"--metadata-pipename=/tmp/shairport-sync-metadata.\n");
printf(" The default is /tmp/shairport-sync-metadata.\n");
printf(" --get-coverart send cover art through the metadata pipe.\n");
#endif
+ printf(" -u, --use-stderr log messages through STDERR rather than syslog.\n");
printf("\n");
mdns_ls_backends();
printf("\n");
@@ -233,13 +319,9 @@ int parse_options(int argc, char **argv) {
int fResyncthreshold = (int)(config.resyncthreshold * 44100);
int fTolerance = (int)(config.tolerance * 44100);
poptContext optCon; /* context for parsing command-line options */
- int daemonisewith = 0;
- int daemonisewithout = 0;
struct poptOption optionsTable[] = {
{"verbose", 'v', POPT_ARG_NONE, NULL, 'v', NULL, NULL},
- {"disconnectFromOutput", 'D', POPT_ARG_NONE, NULL, 0, NULL, NULL},
- {"reconnectToOutput", 'R', POPT_ARG_NONE, NULL, 0, NULL, NULL},
- {"kill", 'k', POPT_ARG_NONE, NULL, 0, NULL, NULL},
+ {"kill", 'k', POPT_ARG_NONE, &killOption, 0, NULL, NULL},
{"daemon", 'd', POPT_ARG_NONE, &daemonisewith, 0, NULL, NULL},
{"justDaemoniseNoPIDFile", 'j', POPT_ARG_NONE, &daemonisewithout, 0, NULL, NULL},
{"configfile", 'c', POPT_ARG_STRING, &config.configfile, 0, NULL, NULL},
@@ -259,8 +341,10 @@ int parse_options(int argc, char **argv) {
{"timeout", 't', POPT_ARG_INT, &config.timeout, 't', NULL, NULL},
{"password", 0, POPT_ARG_STRING, &config.password, 0, NULL, NULL},
{"tolerance", 'z', POPT_ARG_INT, &fTolerance, 0, NULL, NULL},
+ {"use-stderr", 'u', POPT_ARG_NONE, NULL, 'u', NULL, NULL},
#ifdef CONFIG_METADATA
- {"metadata-pipename", 'M', POPT_ARG_STRING, &config.metadata_pipename, 'M', NULL, NULL},
+ {"metadata-enable", 'M', POPT_ARG_NONE, &config.metadata_enabled, 'M', NULL, NULL},
+ {"metadata-pipename", 0, POPT_ARG_STRING, &config.metadata_pipename, 0, NULL, NULL},
{"get-coverart", 'g', POPT_ARG_NONE, &config.get_coverart, 'g', NULL, NULL},
#endif
POPT_AUTOHELP{NULL, 0, 0, NULL, 0, NULL, NULL}};
@@ -285,6 +369,9 @@ int parse_options(int argc, char **argv) {
case 'v':
debuglev++;
break;
+ case 'u':
+ log_to_stderr();
+ break;
case 'D':
inform("Warning: the option -D or --disconnectFromOutput is deprecated.");
break;
@@ -322,6 +409,7 @@ int parse_options(int argc, char **argv) {
poptFreeContext(optCon);
+#ifdef CONFIG_LIBDAEMON
if ((daemonisewith) && (daemonisewithout))
die("Select either daemonize_with_pid_file or daemonize_without_pid_file -- you have selected "
"both!");
@@ -330,6 +418,7 @@ int parse_options(int argc, char **argv) {
if (daemonisewith)
config.daemonise_store_pid = 1;
};
+#endif
config.resyncthreshold = 1.0 * fResyncthreshold / 44100;
config.tolerance = 1.0 * fTolerance / 44100;
@@ -338,8 +427,13 @@ int parse_options(int argc, char **argv) {
// nothing else comes in first.
config.fixedLatencyOffset = 11025; // this sounds like it works properly.
config.diagnostic_drop_packet_fraction = 0.0;
+ config.active_state_timeout = 10.0;
+ config.soxr_delay_threshold = 30; // the soxr measurement time (milliseconds) of two oneshots must not exceed this if soxr interpolation is to be chosen automatically.
+ config.volume_range_hw_priority = 0; // if combining software and hardware volume control, give the software priority
+// i.e. when reducing volume, reduce the sw first before reducing the software.
+// this is because some hw mixers mute at the bottom of their range, and they don't always advertise this fact
-#ifdef HAVE_METADATA_HUB
+#ifdef CONFIG_METADATA_HUB
config.cover_art_cache_dir = "/tmp/shairport-sync/.cache/coverart";
config.scan_interval_when_active =
1; // number of seconds between DACP server scans when playing something
@@ -370,46 +464,27 @@ int parse_options(int argc, char **argv) {
debug(2, "Looking for configuration file at full path \"%s\"", config_file_real_path);
/* Read the file. If there is an error, report it and exit. */
if (config_read_file(&config_file_stuff, config_file_real_path)) {
+ free(config_file_real_path);
+ config_set_auto_convert(&config_file_stuff,
+ 1); // allow autoconversion from int/float to int/float
// make config.cfg point to it
config.cfg = &config_file_stuff;
/* Get the Service Name. */
if (config_lookup_string(config.cfg, "general.name", &str)) {
raw_service_name = (char *)str;
}
- int daemonisewithout = 0;
- int daemonisewith = 0;
+#ifdef CONFIG_LIBDAEMON
/* Get the Daemonize setting. */
- if (config_lookup_string(config.cfg, "sessioncontrol.daemonize_with_pid_file", &str)) {
- if (strcasecmp(str, "no") == 0)
- daemonisewith = 0;
- else if (strcasecmp(str, "yes") == 0)
- daemonisewith = 1;
- else
- die("Invalid daemonize_with_pid_file option choice \"%s\". It should be \"yes\" or "
- "\"no\"");
- }
+ config_set_lookup_bool(config.cfg, "sessioncontrol.daemonize_with_pid_file", &daemonisewith);
/* Get the Just_Daemonize setting. */
- if (config_lookup_string(config.cfg, "sessioncontrol.daemonize_without_pid_file", &str)) {
- if (strcasecmp(str, "no") == 0)
- daemonisewithout = 0;
- else if (strcasecmp(str, "yes") == 0)
- daemonisewithout = 1;
- else
- die("Invalid daemonize_without_pid_file option choice \"%s\". It should be \"yes\" or "
- "\"no\"");
- }
- if ((daemonisewith) && (daemonisewithout))
- die("Select either daemonize_with_pid_file or daemonize_without_pid_file -- you have "
- "selected both!");
- if ((daemonisewith) || (daemonisewithout)) {
- config.daemonise = 1;
- if (daemonisewith)
- config.daemonise_store_pid = 1;
- }
+ config_set_lookup_bool(config.cfg, "sessioncontrol.daemonize_without_pid_file",
+ &daemonisewithout);
+
/* Get the directory path for the pid file created when the program is daemonised. */
if (config_lookup_string(config.cfg, "sessioncontrol.daemon_pid_dir", &str))
config.piddir = (char *)str;
+#endif
/* Get the mdns_backend setting. */
if (config_lookup_string(config.cfg, "general.mdns_backend", &str))
@@ -440,8 +515,8 @@ int parse_options(int argc, char **argv) {
/* Get the udp port range setting. This is number of ports that will be tried for free ports ,
* starting at the port base. Only three ports are needed. */
if (config_lookup_int(config.cfg, "general.udp_port_range", &value)) {
- if ((value < 0) || (value > 65535))
- die("Invalid port range \"%sd\". It should be between 0 and 65535, default is 100",
+ if ((value < 3) || (value > 65535))
+ die("Invalid port range \"%sd\". It should be between 3 and 65535, default is 10",
value);
else
config.udp_port_range = value;
@@ -454,28 +529,38 @@ int parse_options(int argc, char **argv) {
if (config_lookup_string(config.cfg, "general.interpolation", &str)) {
if (strcasecmp(str, "basic") == 0)
config.packet_stuffing = ST_basic;
+ else if (strcasecmp(str, "auto") == 0)
+ config.packet_stuffing = ST_auto;
else if (strcasecmp(str, "soxr") == 0)
-#ifdef HAVE_LIBSOXR
+#ifdef CONFIG_SOXR
config.packet_stuffing = ST_soxr;
#else
- die("The soxr option not available because this version of shairport-sync was built "
+ warn("The soxr option not available because this version of shairport-sync was built "
"without libsoxr "
"support. Change the \"general/interpolation\" setting in the configuration file.");
#endif
else
- die("Invalid interpolation option choice. It should be \"basic\" or \"soxr\"");
+ die("Invalid interpolation option choice. It should be \"auto\", \"basic\" or \"soxr\"");
+ }
+
+#ifdef CONFIG_SOXR
+ /* Get the soxr_delay_threshold setting. */
+ if (config_lookup_int(config.cfg, "general.soxr_delay_threshold", &value)) {
+ if ((value >= 0) && (value <= 100))
+ config.soxr_delay_threshold = value;
+ else
+ warn("Invalid general soxr_delay_threshold setting option choice \"%d\". It should be "
+ "between 0 and 100, "
+ "inclusive. Default is %d (milliseconds).",
+ value, config.soxr_delay_threshold);
}
+#endif
/* Get the statistics setting. */
- if (config_lookup_string(config.cfg, "general.statistics", &str)) {
+ if (config_set_lookup_bool(config.cfg, "general.statistics",
+ &(config.statistics_requested))) {
warn("The \"general\" \"statistics\" setting is deprecated. Please use the \"diagnostics\" "
"\"statistics\" setting instead.");
- if (strcasecmp(str, "no") == 0)
- config.statistics_requested = 0;
- else if (strcasecmp(str, "yes") == 0)
- config.statistics_requested = 1;
- else
- die("Invalid statistics option choice \"%s\". It should be \"yes\" or \"no\"");
}
/* The old drift tolerance setting. */
@@ -631,6 +716,9 @@ int parse_options(int argc, char **argv) {
"or \"flat\"");
}
+ config_set_lookup_bool(config.cfg, "general.volume_control_combined_hardware_priority",
+ &config.volume_range_hw_priority);
+
/* Get the interface to listen on, if specified Default is all interfaces */
/* we keep the interface name and the index */
@@ -703,6 +791,7 @@ int parse_options(int argc, char **argv) {
#ifdef CONFIG_METADATA
/* Get the metadata setting. */
+ config.metadata_enabled = 1; // if metadata support is included, then enable it by default
if (config_lookup_string(config.cfg, "metadata.enabled", &str)) {
if (strcasecmp(str, "no") == 0)
config.metadata_enabled = 0;
@@ -712,6 +801,7 @@ int parse_options(int argc, char **argv) {
die("Invalid metadata enabled option choice \"%s\". It should be \"yes\" or \"no\"");
}
+ config.get_coverart = 1; // if metadata support is included, then enable it by default
if (config_lookup_string(config.cfg, "metadata.include_cover_art", &str)) {
if (strcasecmp(str, "no") == 0)
config.get_coverart = 0;
@@ -747,6 +837,30 @@ int parse_options(int argc, char **argv) {
config.cmd_stop = (char *)str;
}
+ if (config_lookup_string(config.cfg, "sessioncontrol.run_this_before_entering_active_state",
+ &str)) {
+ config.cmd_active_start = (char *)str;
+ }
+
+ if (config_lookup_string(config.cfg, "sessioncontrol.run_this_after_exiting_active_state",
+ &str)) {
+ config.cmd_active_stop = (char *)str;
+ }
+
+ if (config_lookup_float(config.cfg, "sessioncontrol.active_state_timeout", &dvalue)) {
+ if (dvalue < 0.0)
+ warn("Invalid value \"%f\" for sessioncontrol.active_state_timeout. It must be positive. "
+ "The default of %f will be used instead.",
+ dvalue, config.active_state_timeout);
+ else
+ config.active_state_timeout = dvalue;
+ }
+
+ if (config_lookup_string(config.cfg,
+ "sessioncontrol.run_this_if_an_unfixable_error_is_detected", &str)) {
+ config.cmd_unfixable = (char *)str;
+ }
+
if (config_lookup_string(config.cfg, "sessioncontrol.wait_for_completion", &str)) {
if (strcasecmp(str, "no") == 0)
config.cmd_blocking = 0;
@@ -787,7 +901,6 @@ int parse_options(int argc, char **argv) {
}
#ifdef CONFIG_CONVOLUTION
-
if (config_lookup_string(config.cfg, "dsp.convolution", &str)) {
if (strcasecmp(str, "no") == 0)
config.convolution = 0;
@@ -821,7 +934,6 @@ int parse_options(int argc, char **argv) {
die("Convolution enabled but no convolution_ir_file provided");
}
#endif
-
if (config_lookup_string(config.cfg, "dsp.loudness", &str)) {
if (strcasecmp(str, "no") == 0)
config.loudness = 0;
@@ -846,14 +958,14 @@ int parse_options(int argc, char **argv) {
} else {
if (config_error_type(&config_file_stuff) == CONFIG_ERR_FILE_IO)
- debug(1, "Error reading configuration file \"%s\": \"%s\".",
+ debug(2, "Error reading configuration file \"%s\": \"%s\".",
config_error_file(&config_file_stuff), config_error_text(&config_file_stuff));
else {
die("Line %d of the configuration file \"%s\":\n%s", config_error_line(&config_file_stuff),
config_error_file(&config_file_stuff), config_error_text(&config_file_stuff));
}
}
-#if defined(HAVE_DBUS)
+#if defined(CONFIG_DBUS_INTERFACE)
/* Get the dbus service sbus setting. */
if (config_lookup_string(config.cfg, "general.dbus_service_bus", &str)) {
if (strcasecmp(str, "system") == 0)
@@ -866,7 +978,7 @@ int parse_options(int argc, char **argv) {
}
#endif
-#if defined(HAVE_MPRIS)
+#if defined(CONFIG_MPRIS_INTERFACE)
/* Get the mpris service sbus setting. */
if (config_lookup_string(config.cfg, "general.mpris_service_bus", &str)) {
if (strcasecmp(str, "system") == 0)
@@ -879,7 +991,72 @@ int parse_options(int argc, char **argv) {
}
#endif
- free(config_file_real_path);
+#ifdef CONFIG_MQTT
+ int tmpval = 0;
+ config_set_lookup_bool(config.cfg, "mqtt.enabled", &config.mqtt_enabled);
+ if (config.mqtt_enabled && !config.metadata_enabled) {
+ die("You need to have metadata enabled in order to use mqtt");
+ }
+ if (config_lookup_string(config.cfg, "mqtt.hostname", &str)) {
+ config.mqtt_hostname = (char *)str;
+ // TODO: Document that, if this is false, whole mqtt func is disabled
+ }
+ if (config_lookup_int(config.cfg, "mqtt.port", &tmpval)) {
+ config.mqtt_port = tmpval;
+ } else {
+ // TODO: Is this the correct way to set a default value?
+ config.mqtt_port = 1883;
+ }
+
+ if (config_lookup_string(config.cfg, "mqtt.username", &str)) {
+ config.mqtt_username = (char *)str;
+ }
+ if (config_lookup_string(config.cfg, "mqtt.password", &str)) {
+ config.mqtt_password = (char *)str;
+ }
+ int capath = 0;
+ if (config_lookup_string(config.cfg, "mqtt.capath", &str)) {
+ config.mqtt_capath = (char *)str;
+ capath = 1;
+ }
+ if (config_lookup_string(config.cfg, "mqtt.cafile", &str)) {
+ if (capath)
+ die("Supply either mqtt cafile or mqtt capath -- you have supplied both!");
+ config.mqtt_cafile = (char *)str;
+ }
+ int certkeynum = 0;
+ if (config_lookup_string(config.cfg, "mqtt.certfile", &str)) {
+ config.mqtt_certfile = (char *)str;
+ certkeynum++;
+ }
+ if (config_lookup_string(config.cfg, "mqtt.keyfile", &str)) {
+ config.mqtt_keyfile = (char *)str;
+ certkeynum++;
+ }
+ if (certkeynum != 0 && certkeynum != 2) {
+ die("If you want to use TLS Client Authentication, you have to specify "
+ "mqtt.certfile AND mqtt.keyfile.\nYou have supplied only one of them.\n"
+ "If you do not want to use TLS Client Authentication, leave both empty.");
+ }
+
+ if (config_lookup_string(config.cfg, "mqtt.topic", &str)) {
+ config.mqtt_topic = (char *)str;
+ }
+ config_set_lookup_bool(config.cfg, "mqtt.publish_raw", &config.mqtt_publish_raw);
+ config_set_lookup_bool(config.cfg, "mqtt.publish_parsed", &config.mqtt_publish_parsed);
+ config_set_lookup_bool(config.cfg, "mqtt.publish_cover", &config.mqtt_publish_cover);
+ if (config.mqtt_publish_cover && !config.get_coverart) {
+ die("You need to have metadata.include_cover_art enabled in order to use mqtt.publish_cover");
+ }
+ config_set_lookup_bool(config.cfg, "mqtt.enable_remote", &config.mqtt_enable_remote);
+#ifndef CONFIG_AVAHI
+ if (config.mqtt_enable_remote) {
+ die("You have enabled MQTT remote control which requires shairport-sync to be built with "
+ "Avahi, but your installation is not using avahi. Please reinstall/recompile with "
+ "avahi enabled, or disable remote control.");
+ }
+#endif
+#endif
}
// now, do the command line options again, but this time do them fully -- it's a unix convention
@@ -924,8 +1101,10 @@ int parse_options(int argc, char **argv) {
case 'S':
if (strcmp(stuffing, "basic") == 0)
config.packet_stuffing = ST_basic;
+ else if (strcmp(stuffing, "auto") == 0)
+ config.packet_stuffing = ST_auto;
else if (strcmp(stuffing, "soxr") == 0)
-#ifdef HAVE_LIBSOXR
+#ifdef CONFIG_SOXR
config.packet_stuffing = ST_soxr;
#else
die("The soxr option not available because this version of shairport-sync was built "
@@ -942,6 +1121,28 @@ int parse_options(int argc, char **argv) {
}
poptFreeContext(optCon);
+
+// here, we are finally finished reading the options
+
+
+#ifdef CONFIG_LIBDAEMON
+ if ((daemonisewith) && (daemonisewithout))
+ die("Select either daemonize_with_pid_file or daemonize_without_pid_file -- you have selected "
+ "both!");
+ if ((daemonisewith) || (daemonisewithout)) {
+ config.daemonise = 1;
+ if (daemonisewith)
+ config.daemonise_store_pid = 1;
+ };
+#else
+ /* Check if we are called with -d or --daemon or -j or justDaemoniseNoPIDFile options*/
+ if ((daemonisewith != 0) || (daemonisewithout != 0)) {
+ fprintf(stderr,"%s was built without libdaemon, so does not support daemonisation using the -d, --deamon, -j or --justDaemoniseNoPIDFile options\n",config.appName);
+ exit(EXIT_FAILURE);
+ }
+
+#endif
+
#ifdef CONFIG_METADATA
if ((config.metadata_enabled == 1) && (config.metadata_pipename == NULL))
config.metadata_pipename = strdup("/tmp/shairport-sync-metadata");
@@ -978,8 +1179,20 @@ int parse_options(int argc, char **argv) {
free(i3);
free(vs);
+#ifdef CONFIG_MQTT
+ // mqtt topic was not set. As we have the service name just now, set it
+ if (config.mqtt_topic == NULL) {
+ int topic_length = 1 + strlen(config.service_name) + 1;
+ char *topic = malloc(topic_length + 1);
+ snprintf(topic, topic_length, "/%s/", config.service_name);
+ config.mqtt_topic = topic;
+ }
+#endif
+
+#ifdef CONFIG_LIBDAEMON
+
// now, check and calculate the pid directory
-#ifdef USE_CUSTOM_PID_DIR
+#ifdef DEFINED_CUSTOM_PID_DIR
char *use_this_pid_dir = PIDDIR;
#else
char *use_this_pid_dir = "/var/run/shairport-sync";
@@ -989,18 +1202,19 @@ int parse_options(int argc, char **argv) {
use_this_pid_dir = config.piddir;
if (use_this_pid_dir)
config.computed_piddir = strdup(use_this_pid_dir);
-
+#endif
return optind + 1;
}
-#if defined(HAVE_DBUS) || defined(HAVE_MPRIS)
-GMainLoop *loop;
+#if defined(CONFIG_DBUS_INTERFACE) || defined(CONFIG_MPRIS_INTERFACE)
+static GMainLoop *g_main_loop = NULL;
pthread_t dbus_thread;
void *dbus_thread_func(__attribute__((unused)) void *arg) {
- loop = g_main_loop_new(NULL, FALSE);
- g_main_loop_run(loop);
- return NULL;
+ g_main_loop = g_main_loop_new(NULL, FALSE);
+ g_main_loop_run(g_main_loop);
+ debug(2, "g_main_loop thread exit");
+ pthread_exit(NULL);
}
#endif
@@ -1044,14 +1258,7 @@ void signal_setup(void) {
sigaction(SIGCHLD, &sa, NULL);
}
-// forked daemon lets the spawner know it's up and running OK
-// should be called only once!
-void shairport_startup_complete(void) {
- if (config.daemonise) {
- // daemon_ready();
- }
-}
-
+#ifdef CONFIG_LIBDAEMON
char pid_file_path_string[4096] = "\0";
const char *pid_file_proc(void) {
@@ -1060,9 +1267,80 @@ const char *pid_file_proc(void) {
// debug(1,"pid_file_path_string \"%s\".",pid_file_path_string);
return pid_file_path_string;
}
+#endif
+
+void main_cleanup_handler(__attribute__((unused)) void *arg) {
+ // it doesn't look like this is called when the main function is cancelled eith a pthread cancel.
+ debug(2, "main cleanup handler called.");
+#ifdef CONFIG_MQTT
+ if (config.mqtt_enabled) {
+ // terminate_mqtt();
+ }
+#endif
+
+#if defined(CONFIG_DBUS_INTERFACE) || defined(CONFIG_MPRIS_INTERFACE)
+#ifdef CONFIG_MPRIS_INTERFACE
+// stop_mpris_service();
+#endif
+#ifdef CONFIG_DBUS_INTERFACE
+ stop_dbus_service();
+#endif
+ if (g_main_loop) {
+ debug(2, "Stopping DBUS Loop Thread");
+ g_main_loop_quit(g_main_loop);
+ pthread_join(dbus_thread, NULL);
+ }
+#endif
+
+#ifdef CONFIG_DACP_CLIENT
+ debug(2, "Stopping DACP Monitor");
+ dacp_monitor_stop();
+#endif
+
+#ifdef CONFIG_METADATA_HUB
+ debug(2, "Stopping metadata hub");
+ metadata_hub_stop();
+#endif
+
+#ifdef CONFIG_METADATA
+ metadata_stop(); // close down the metadata pipe
+#endif
+
+ activity_monitor_stop(0);
+
+ if ((config.output) && (config.output->deinit)) {
+ debug(2, "Deinitialise the audio backend.");
+ config.output->deinit();
+ }
+
+#ifdef CONFIG_LIBDAEMON
+// only do this if you are the daemon
+ if (pid == 0) {
+ daemon_retval_send(0);
+ daemon_pid_file_remove();
+ daemon_signal_done();
+ }
+#endif
+
+ debug(2, "Exit...");
+ exit(EXIT_SUCCESS);
+}
void exit_function() {
- // debug(1, "exit function called...");
+ debug(2, "exit function called...");
+ main_cleanup_handler(NULL);
+ if (conns)
+ free(conns); // make sure the connections have been deleted first
+ if (config.service_name)
+ free(config.service_name);
+ if (config.regtype)
+ free(config.regtype);
+#ifdef CONFIG_LIBDAEMON
+ if (config.computed_piddir)
+ free(config.computed_piddir);
+#endif
+ if (ranarray)
+ free((void *)ranarray);
if (config.cfg)
config_destroy(config.cfg);
if (config.appName)
@@ -1071,14 +1349,15 @@ void exit_function() {
}
int main(int argc, char **argv) {
+#ifdef CONFIG_LIBDAEMON
+ pid = getpid();
+#endif
+ conns = NULL; // no connections active
+ memset((void *)&main_thread_id, 0, sizeof(main_thread_id));
+ memset(&config, 0, sizeof(config)); // also clears all strings, BTW
fp_time_at_startup = get_absolute_time_in_fp();
fp_time_at_last_debug_message = fp_time_at_startup;
- // debug(1,"startup");
- daemon_set_verbosity(LOG_DEBUG);
- memset(&config, 0, sizeof(config)); // also clears all strings, BTW
- atexit(exit_function);
-
- // this is a bit weird, but apparently necessary
+ // this is a bit weird, but necessary -- basename() may modify the argument passed in
char *basec = strdup(argv[0]);
char *bname = basename(basec);
config.appName = strdup(bname);
@@ -1086,9 +1365,18 @@ int main(int argc, char **argv) {
die("can not allocate memory for the app name!");
free(basec);
+ // debug(1,"startup");
+#ifdef CONFIG_LIBDAEMON
+ daemon_set_verbosity(LOG_DEBUG);
+#else
+ setlogmask (LOG_UPTO (LOG_DEBUG));
+ openlog(NULL,0,LOG_DAEMON);
+#endif
+ atexit(exit_function);
+
// set defaults
- // get thje endianness
+ // get the endianness
union {
uint32_t u32;
uint8_t arr[4];
@@ -1100,11 +1388,11 @@ int main(int argc, char **argv) {
xn.arr[3] = 0x11; /* Highest-address byte */
if (xn.u32 == 0x11223344)
- endianness = SS_LITTLE_ENDIAN;
+ config.endianness = SS_LITTLE_ENDIAN;
else if (xn.u32 == 0x33441122)
- endianness = SS_PDP_ENDIAN;
+ config.endianness = SS_PDP_ENDIAN;
else if (xn.u32 == 0x44332211)
- endianness = SS_BIG_ENDIAN;
+ config.endianness = SS_BIG_ENDIAN;
else
die("Can not recognise the endianness of the processor.");
@@ -1121,13 +1409,23 @@ int main(int argc, char **argv) {
// config.statistics_requested = 0; // don't print stats in the log
// config.userSuppliedLatency = 0; // zero means none supplied
+
+ config.debugger_show_relative_time = 1; // by default, log the time back to the previous debug message
config.resyncthreshold = 0.05; // 50 ms
config.timeout = 120; // this number of seconds to wait for [more] audio before switching to idle.
config.tolerance =
0.002; // this number of seconds of timing error before attempting to correct it.
config.buffer_start_fill = 220;
config.port = 5000;
+
+
+#ifdef CONFIG_SOXR
+ config.packet_stuffing = ST_auto; // use soxr interpolation by default if support has been included and if the CPU is fast enough
+#else
config.packet_stuffing = ST_basic; // simple interpolation or deletion
+#endif
+
+
// char hostname[100];
// gethostname(hostname, 100);
// config.service_name = malloc(20 + 100);
@@ -1136,13 +1434,16 @@ int main(int argc, char **argv) {
1); // we expect to be able to connect to the output device
config.audio_backend_buffer_desired_length = 6615; // 0.15 seconds.
config.udp_port_base = 6001;
- config.udp_port_range = 100;
- config.output_format = SPS_FORMAT_S16; // default
- config.output_rate = 44100; // default
+ config.udp_port_range = 10;
+ config.output_format = SPS_FORMAT_S16_LE; // default
+ config.output_format_auto_requested = 1; // default auto select format
+ config.output_rate = 44100; // default
+ config.output_rate_auto_requested = 1; // default auto select format
config.decoders_supported =
1 << decoder_hammerton; // David Hammerton's decoder supported by default
-#ifdef HAVE_APPLE_ALAC
+#ifdef CONFIG_APPLE_ALAC
config.decoders_supported += 1 << decoder_apple_alac;
+ config.use_apple_decoder = 1; // use the ALAC decoder by default if support has been included
#endif
// initialise random number generator
@@ -1156,16 +1457,16 @@ int main(int argc, char **argv) {
/* Check if we are called with -V or --version parameter */
if (argc >= 2 && ((strcmp(argv[1], "-V") == 0) || (strcmp(argv[1], "--version") == 0))) {
print_version();
- exit(1);
+ exit(EXIT_FAILURE);
}
/* Check if we are called with -h or --help parameter */
if (argc >= 2 && ((strcmp(argv[1], "-h") == 0) || (strcmp(argv[1], "--help") == 0))) {
usage(argv[0]);
- exit(1);
+ exit(EXIT_FAILURE);
}
- pid_t pid;
+#ifdef CONFIG_LIBDAEMON
/* Reset signal handlers */
if (daemon_reset_sigs(-1) < 0) {
@@ -1183,68 +1484,45 @@ int main(int argc, char **argv) {
daemon_pid_file_ident = daemon_log_ident = daemon_ident_from_argv0(argv[0]);
daemon_pid_file_proc = pid_file_proc;
-
- /* Check if we are called with -D or --disconnectFromOutput parameter */
- if (argc >= 2 &&
- ((strcmp(argv[1], "-D") == 0) || (strcmp(argv[1], "--disconnectFromOutput") == 0))) {
- if ((pid = daemon_pid_file_is_running()) >= 0) {
- if (kill(pid, SIGUSR2) != 0) { // try to send the signal
- daemon_log(LOG_WARNING,
- "Failed trying to send disconnectFromOutput command to daemon pid: %d: %s", pid,
- strerror(errno));
- }
- } else {
- daemon_log(LOG_WARNING,
- "Can't send a disconnectFromOutput request -- Failed to find daemon: %s",
- strerror(errno));
- }
- exit(1);
- }
- /* Check if we are called with -R or --reconnectToOutput parameter */
- if (argc >= 2 &&
- ((strcmp(argv[1], "-R") == 0) || (strcmp(argv[1], "--reconnectToOutput") == 0))) {
- if ((pid = daemon_pid_file_is_running()) >= 0) {
- if (kill(pid, SIGHUP) != 0) { // try to send the signal
- daemon_log(LOG_WARNING,
- "Failed trying to send reconnectToOutput command to daemon pid: %d: %s", pid,
- strerror(errno));
- }
- } else {
- daemon_log(LOG_WARNING, "Can't send a reconnectToOutput request -- Failed to find daemon: %s",
- strerror(errno));
- }
- exit(1);
- }
+#endif
+
// parse arguments into config -- needed to locate pid_dir
int audio_arg = parse_options(argc, argv);
- /* Check if we are called with -k or --kill parameter */
- if (argc >= 2 && ((strcmp(argv[1], "-k") == 0) || (strcmp(argv[1], "--kill") == 0))) {
+ // mDNS supports maximum of 63-character names (we append 13).
+ if (strlen(config.service_name) > 50) {
+ warn("Supplied name too long (max 50 characters)");
+ config.service_name[50] = '\0'; // truncate it and carry on...
+ }
+
+ /* Check if we are called with -k or --kill option */
+ if (killOption != 0) {
+#ifdef CONFIG_LIBDAEMON
int ret;
/* Kill daemon with SIGTERM */
/* Check if the new function daemon_pid_file_kill_wait() is available, if it is, use it. */
if ((ret = daemon_pid_file_kill_wait(SIGTERM, 5)) < 0)
daemon_log(LOG_WARNING, "Failed to kill daemon: %s", strerror(errno));
- else
- daemon_pid_file_remove();
+ else {
+ // debug(1,"Successfully killed the shairport sync daemon.");
+ }
return ret < 0 ? 1 : 0;
+#else
+ fprintf(stderr,"%s was built without libdaemon, so does not support the -k or --kill option\n",config.appName);
+ return 1;
+#endif
}
+#ifdef CONFIG_LIBDAEMON
/* If we are going to daemonise, check that the daemon is not running already.*/
if ((config.daemonise) && ((pid = daemon_pid_file_is_running()) >= 0)) {
daemon_log(LOG_ERR, "Daemon already running on PID file %u", pid);
return 1;
}
- // mDNS supports maximum of 63-character names (we append 13).
- if (strlen(config.service_name) > 50) {
- warn("Supplied name too long (max 50 characters)");
- config.service_name[50] = '\0'; // truncate it and carry on...
- }
-
/* here, daemonise with libdaemon */
if (config.daemonise) {
@@ -1288,15 +1566,16 @@ int main(int argc, char **argv) {
daemon_log(LOG_ERR, "daemon failed to launch, error %i.", ret);
}
return ret;
- } else { /* The daemon */
+ } else { /* pid == 0 means we are the daemon */
/* Close FDs */
if (daemon_close_all(-1) < 0) {
daemon_log(LOG_ERR, "Failed to close all file descriptors: %s", strerror(errno));
-
/* Send the error condition to the parent process */
daemon_retval_send(1);
- goto finish;
+
+ daemon_signal_done();
+ return 0;
}
/* Create the PID file if required */
@@ -1307,12 +1586,17 @@ int main(int argc, char **argv) {
if ((result != 0) && (result != -EEXIST)) {
// error creating or accessing the PID file directory
daemon_retval_send(3);
- goto finish;
+
+ daemon_signal_done();
+ return 0;
}
+
if (daemon_pid_file_create() < 0) {
daemon_log(LOG_ERR, "Could not create PID file (%s).", strerror(errno));
+
daemon_retval_send(2);
- goto finish;
+ daemon_signal_done();
+ return 0;
}
}
@@ -1322,11 +1606,30 @@ int main(int argc, char **argv) {
/* end libdaemon stuff */
}
+#endif
+ debug(1,"Started!");
+ main_thread_id = pthread_self();
+ if (!main_thread_id)
+ debug(1, "Main thread is set up to be NULL!");
+
signal_setup();
// make sure the program can create files that group and world can read
umask(S_IWGRP | S_IWOTH);
+
+ /* print out version */
+
+ char *version_dbs = get_version_string();
+ if (version_dbs) {
+ debug(1, "software version: \"%s\"", version_dbs);
+ free(version_dbs);
+ } else {
+ debug(1, "can't print the version information!");
+ }
+
+ debug(1, "log verbosity is %d.", debuglev);
+
config.output = audio_get_output(config.output_name);
if (!config.output) {
audio_ls_outputs();
@@ -1334,9 +1637,11 @@ int main(int argc, char **argv) {
}
config.output->init(argc - audio_arg, argv + audio_arg);
+ pthread_cleanup_push(main_cleanup_handler, NULL);
+
// daemon_log(LOG_NOTICE, "startup");
- switch (endianness) {
+ switch (config.endianness) {
case SS_LITTLE_ENDIAN:
debug(2, "The processor is running little-endian.");
break;
@@ -1370,32 +1675,33 @@ int main(int argc, char **argv) {
BUFFER_FRAMES * 352 - 22050);
}
- /* print out version */
-
- char *version_dbs = get_version_string();
- if (version_dbs) {
- debug(1, "Version: \"%s\"", version_dbs);
- free(version_dbs);
- } else {
- debug(1, "Can't print the version information!");
- }
-
/* Print out options */
+ debug(1, "disable resend requests is %s.", config.disable_resend_requests ? "on" : "off");
+ debug(1, "diagnostic_drop_packet_fraction is %f. A value of 0.0 means no packets will be dropped "
+ "deliberately.",
+ config.diagnostic_drop_packet_fraction);
debug(1, "statistics_requester status is %d.", config.statistics_requested);
+#if CONFIG_LIBDAEMON
debug(1, "daemon status is %d.", config.daemonise);
debug(1, "deamon pid file path is \"%s\".", pid_file_proc());
+#endif
debug(1, "rtsp listening port is %d.", config.port);
debug(1, "udp base port is %d.", config.udp_port_base);
debug(1, "udp port range is %d.", config.udp_port_range);
debug(1, "player name is \"%s\".", config.service_name);
debug(1, "backend is \"%s\".", config.output_name);
- debug(1, "on-start action is \"%s\".", config.cmd_start);
- debug(1, "on-stop action is \"%s\".", config.cmd_stop);
+ debug(1, "run_this_before_play_begins action is \"%s\".", config.cmd_start);
+ debug(1, "run_this_after_play_ends action is \"%s\".", config.cmd_stop);
debug(1, "wait-cmd status is %d.", config.cmd_blocking);
- debug(1, "on-start returns output is %d.", config.cmd_start_returns_output);
+ debug(1, "run_this_before_play_begins may return output is %d.", config.cmd_start_returns_output);
+ debug(1, "run_this_if_an_unfixable_error_is_detected action is \"%s\".", config.cmd_unfixable);
+ debug(1, "run_this_before_entering_active_state action is \"%s\".", config.cmd_active_start);
+ debug(1, "run_this_after_exiting_active_state action is \"%s\".", config.cmd_active_stop);
+ debug(1, "active_state_timeout is %f seconds.", config.active_state_timeout);
debug(1, "mdns backend \"%s\".", config.mdns_name);
debug(2, "userSuppliedLatency is %d.", config.userSuppliedLatency);
- debug(1, "stuffing option is \"%d\" (0-basic, 1-soxr).", config.packet_stuffing);
+ debug(1, "interpolation setting is \"%s\".", config.packet_stuffing == ST_basic ? "basic" : config.packet_stuffing == ST_soxr ? "soxr" : "auto");
+ debug(1, "interpolation soxr_delay_threshold is %d.", config.soxr_delay_threshold);
debug(1, "resync time is %f seconds.", config.resyncthreshold);
debug(1, "allow a session to be interrupted: %d.", config.allow_session_interruption);
debug(1, "busy timeout time is %d.", config.timeout);
@@ -1406,21 +1712,30 @@ int main(int argc, char **argv) {
debug(1, "volume_max_db is %d.", config.volume_max_db);
else
debug(1, "volume_max_db is not set");
+ debug(1, "volume range in dB (zero means use the range specified by the mixer): %u.",
+ config.volume_range_db);
+ debug(
+ 1,
+ "volume_range_combined_hardware_priority (1 means hardware mixer attenuation is used first) is %d.",
+ config.volume_range_hw_priority);
debug(1, "playback_mode is %d (0-stereo, 1-mono, 1-reverse_stereo, 2-both_left, 3-both_right).",
config.playback_mode);
debug(1, "disable_synchronization is %d.", config.no_sync);
debug(1, "use_mmap_if_available is %d.", config.no_mmap ? 0 : 1);
- debug(1, "output_rate is %d.", config.output_rate);
- debug(1,
- "output_format is %d (0-unknown, 1-S8, 2-U8, 3-S16, 4-S24, 5-S24_3LE, 6-S24_3BE, 7-S32).",
- config.output_format);
+ debug(1, "output_format automatic selection is %sabled.", config.output_format_auto_requested ? "en" : "dis");
+ if (config.output_format_auto_requested == 0)
+ debug(1,
+ "output_format is \"%s\".",
+ sps_format_description_string(config.output_format));
+ debug(1, "output_rate automatic selection is %sabled.", config.output_rate_auto_requested ? "en" : "dis");
+ if (config.output_rate_auto_requested == 0)
+ debug(1, "output_rate is %d.", config.output_rate);
debug(1, "audio backend desired buffer length is %f seconds.",
config.audio_backend_buffer_desired_length);
+ debug(1, "audio_backend_buffer_interpolation_threshold_in_seconds is %f seconds.", config.audio_backend_buffer_interpolation_threshold_in_seconds);
debug(1, "audio backend latency offset is %f seconds.", config.audio_backend_latency_offset);
debug(1, "audio backend silence lead-in time is %f seconds. A value -1.0 means use the default.",
config.audio_backend_silent_lead_in_time);
- debug(1, "volume range in dB (zero means use the range specified by the mixer): %u.",
- config.volume_range_db);
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);
@@ -1445,6 +1760,15 @@ int main(int argc, char **argv) {
debug(1, "metadata socket packet size is \"%d\".", config.metadata_sockmsglength);
debug(1, "get-coverart is %d.", config.get_coverart);
#endif
+#ifdef CONFIG_MQTT
+ debug(1, "mqtt is %sabled.", config.mqtt_enabled ? "en" : "dis");
+ debug(1, "mqtt hostname is %s, port is %d.", config.mqtt_hostname, config.mqtt_port);
+ debug(1, "mqtt topic is %s.", config.mqtt_topic);
+ debug(1, "mqtt will%s publish raw metadata.", config.mqtt_publish_raw ? "" : " not");
+ debug(1, "mqtt will%s publish parsed metadata.", config.mqtt_publish_parsed ? "" : " not");
+ debug(1, "mqtt will%s publish cover Art.", config.mqtt_publish_cover ? "" : " not");
+ debug(1, "mqtt remote control is %sabled.", config.mqtt_enable_remote ? "en" : "dis");
+#endif
#ifdef CONFIG_CONVOLUTION
debug(1, "convolution is %d.", config.convolution);
@@ -1454,28 +1778,35 @@ int main(int argc, char **argv) {
#endif
debug(1, "loudness is %d.", config.loudness);
debug(1, "loudness reference level is %f", config.loudness_reference_volume_db);
- debug(1, "disable resend requests is %s.", config.disable_resend_requests ? "on" : "off");
- debug(1, "diagnostic_drop_packet_fraction is %f. A value of 0.0 means no packets will be dropped "
- "deliberately.",
- config.diagnostic_drop_packet_fraction);
uint8_t ap_md5[16];
-#ifdef HAVE_LIBSSL
+#ifdef CONFIG_SOXR
+ pthread_create(&soxr_time_check_thread, NULL, &soxr_time_check, NULL);
+#endif
+
+#ifdef CONFIG_OPENSSL
MD5_CTX ctx;
MD5_Init(&ctx);
MD5_Update(&ctx, config.service_name, strlen(config.service_name));
MD5_Final(ap_md5, &ctx);
#endif
-#ifdef HAVE_LIBMBEDTLS
+#ifdef CONFIG_MBEDTLS
+#if MBEDTLS_VERSION_MINOR >= 7
+ mbedtls_md5_context tctx;
+ mbedtls_md5_starts_ret(&tctx);
+ mbedtls_md5_update_ret(&tctx, (unsigned char *)config.service_name, strlen(config.service_name));
+ mbedtls_md5_finish_ret(&tctx, ap_md5);
+#else
mbedtls_md5_context tctx;
mbedtls_md5_starts(&tctx);
mbedtls_md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name));
mbedtls_md5_finish(&tctx, ap_md5);
#endif
+#endif
-#ifdef HAVE_LIBPOLARSSL
+#ifdef CONFIG_POLARSSL
md5_context tctx;
md5_starts(&tctx);
md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name));
@@ -1486,36 +1817,44 @@ int main(int argc, char **argv) {
metadata_init(); // create the metadata pipe if necessary
#endif
-#ifdef HAVE_METADATA_HUB
+#ifdef CONFIG_METADATA_HUB
// debug(1, "Initialising metadata hub");
metadata_hub_init();
#endif
-#ifdef HAVE_DACP_CLIENT
+#ifdef CONFIG_DACP_CLIENT
// debug(1, "Requesting DACP Monitor");
dacp_monitor_start();
#endif
-#if defined(HAVE_DBUS) || defined(HAVE_MPRIS)
+#if defined(CONFIG_DBUS_INTERFACE) || defined(CONFIG_MPRIS_INTERFACE)
// Start up DBUS services after initial settings are all made
// debug(1, "Starting up D-Bus services");
pthread_create(&dbus_thread, NULL, &dbus_thread_func, NULL);
-#ifdef HAVE_DBUS
+#ifdef CONFIG_DBUS_INTERFACE
start_dbus_service();
#endif
-#ifdef HAVE_MPRIS
+#ifdef CONFIG_MPRIS_INTERFACE
start_mpris_service();
#endif
#endif
- // daemon_log(LOG_INFO, "Successful Startup");
+#ifdef CONFIG_MQTT
+ if (config.mqtt_enabled) {
+ initialise_mqtt();
+ }
+#endif
+
+ activity_monitor_start();
+
+ // debug(1, "Successful Startup");
rtsp_listen_loop();
// should not reach this...
- shairport_shutdown();
-finish:
- daemon_log(LOG_NOTICE, "Unexpected exit...");
- daemon_retval_send(255);
- daemon_pid_file_remove();
- return 1;
+ // daemon_log(LOG_NOTICE, "Unexpected exit...");
+ // daemon_retval_send(0);
+ // daemon_pid_file_remove();
+ pthread_cleanup_pop(1);
+ debug(1, "Odd exit point");
+ pthread_exit(NULL);
}
diff --git a/tinysvcmdns.c b/tinysvcmdns.c
index ebf6fea..bb89e50 100644
--- a/tinysvcmdns.c
+++ b/tinysvcmdns.c
@@ -6,6 +6,8 @@
* tinysvcmdns - a tiny MDNS implementation for publishing services
* Copyright (C) 2011 Darell Tan
* All rights reserved.
+ * Updated many times by Mike Brady (c) 2014 -- 2019
+ * Includes fixes for CVE-12087 and CVE-2017-12130
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -81,6 +83,8 @@ struct name_comp {
// duplicates a name
inline uint8_t *dup_nlabel(const uint8_t *n) {
+ if (n == NULL)
+ return NULL;
assert(n[0] <= 63); // prevent mis-use
return (uint8_t *)strdup((char *)n);
}
@@ -125,8 +129,8 @@ char *nlabel_to_str(const uint8_t *name) {
const uint8_t *p;
size_t buf_len = 256;
- assert(name != NULL);
-
+ if (name == NULL)
+ return NULL;
label = labelp = malloc(buf_len);
if (label) {
@@ -186,7 +190,9 @@ uint8_t *create_label(const char *txt) {
int len;
uint8_t *s;
- assert(txt != NULL);
+ // assert(txt != NULL);
+ if (txt == NULL)
+ return NULL;
len = strlen(txt);
if (len > 63)
return NULL;
@@ -416,7 +422,7 @@ int rr_list_append(struct rr_list **rr_head, struct rr_entry *rr) {
if (*rr_head == NULL) {
*rr_head = node;
} else {
- struct rr_list *e = *rr_head, *taile;
+ struct rr_list *e = *rr_head, *taile = NULL;
for (; e; e = e->next) {
// already in list - don't add
if (e->e == rr) {
@@ -426,7 +432,10 @@ int rr_list_append(struct rr_list **rr_head, struct rr_entry *rr) {
if (e->next == NULL)
taile = e;
}
- taile->next = node;
+ if (taile)
+ taile->next = node;
+ else
+ DEBUG_PRINTF("taile not given a value.\n");
}
} else {
die("can not allocate memory for \"node\" in tinysvcmdns.");
@@ -500,7 +509,7 @@ struct rr_entry *rr_create(uint8_t *name, enum rr_type type) {
}
void rr_set_nsec(struct rr_entry *rr_nsec, enum rr_type type) {
- assert(rr_nsec->type = RR_NSEC);
+ assert((rr_nsec->type = RR_NSEC));
assert((type / 8) < sizeof(rr_nsec->data.NSEC.bitmap));
rr_nsec->data.NSEC.bitmap[type / 8] = 1 << (7 - (type % 8));
@@ -675,9 +684,12 @@ static size_t mdns_parse_qn(uint8_t *pkt_buf, size_t pkt_len, size_t off, struct
if (rr)
memset(rr, 0, sizeof(struct rr_entry));
else
- die("could not allocate memory for \"rr\" in tinysvcmdns");
+ goto err;
name = uncompress_nlabel(pkt_buf, pkt_len, off);
+ if (name == NULL)
+ goto err;
+
p += label_len(pkt_buf, pkt_len, off);
rr->name = name;
@@ -691,6 +703,10 @@ static size_t mdns_parse_qn(uint8_t *pkt_buf, size_t pkt_len, size_t off, struct
rr_list_append(&pkt->rr_qn, rr);
return p - (pkt_buf + off);
+
+err:
+ free(rr);
+ return 0;
}
// parse the MDNS RR section
@@ -713,10 +729,13 @@ static size_t mdns_parse_rr(uint8_t *pkt_buf, size_t pkt_len, size_t off, struct
if (rr)
memset(rr, 0, sizeof(struct rr_entry));
else
- die("could not allocate memory for \"rr (2)\" in tinysvcmdns");
+ goto err;
name = uncompress_nlabel(pkt_buf, pkt_len, off);
- p += label_len(pkt_buf, pkt_len, off);
+ if (name == NULL)
+ goto err;
+
+ // parse the MDNS RR section p += label_len(pkt_buf, pkt_len, off);
rr->name = name;
rr->type = mdns_read_u16(p);
@@ -819,6 +838,10 @@ static size_t mdns_parse_rr(uint8_t *pkt_buf, size_t pkt_len, size_t off, struct
rr_list_append(&pkt->rr_ans, rr);
return p - (pkt_buf + off);
+
+err:
+ free(rr);
+ return 0;
}
// parse a MDNS packet into an mdns_pkt struct