diff --git a/Makefile.am b/Makefile.am index 7106f35a..f6d2fdf7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -8,7 +8,7 @@ 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 if BUILD_FOR_FREEBSD AM_CXXFLAGS = -I/usr/local/include -Wno-multichar -Wall -Wextra -pthread -DSYSCONFDIR=\"$(sysconfdir)\" @@ -40,6 +40,10 @@ 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 diff --git a/README.md b/README.md index 0800ae38..9324e75c 100644 --- a/README.md +++ b/README.md @@ -6,544 +6,6 @@ Audio played by a Shairport Sync-powered device stays synchronised with the sour Shairport Sync runs on Linux and FreeBSD. It does not support AirPlay video or photo streaming. -This is the unstable "development" branch. Changes and updates are incorporated into this branch quickly. To access the stable version, where changes are made after due time, please switch to the "master" branch. - -More Information ----------- -Shairport Sync offers *full audio synchronisation*, a feature of AirPlay that previous implementations do not provide. Full audio synchronisation means that audio is played on the output device at exactly the time specified by the audio source. To accomplish this, Shairport Sync needs access to audio systems – such as `alsa` on Linux and `sndio` on FreeBSD – that provide very accurate timing information about output devices. Shairport Sync must have direct access to the output device used, which must be a real sound card capable of working with 44,100, 88,200 or 176,400 samples per second, interleaved PCM stereo of 8, 16, 24 or 32 bits. The default is 44,100 samples per second / 16 bits (you'll get a message in the logfile if there's a problem). - -Alternatively, Shairport Sync works well with PulseAudio, a widely used sound server found on many desktop Linuxes. While the timing information is not as accurate as that of `alsa` or `sndio`, it is often impractical to remove or disable PulseAudio. In that case, the `pa` backend can be used. An older backend for PulseAudio called `pulse` does not support synchronisation and is deprecated. - -For other use cases, Shairport Sync can provide 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. - -For more about the motivation behind Shairport Sync, please see the wiki at https://github.com/mikebrady/shairport-sync/wiki. - -Synchronisation, Latency, "Stuffing" ---------- -The AirPlay protocol uses an agreed *latency* – the time difference, or delay, between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). The latency to be used is specified by the audio source when it negotiates with Shairport Sync. Most sources set a latency of two seconds. Recent versions of iTunes and forkedDaapd use a latency of just over 2.25 seconds. A latency of this length allows AirPlay players to correct for network delays, processing time variations and so on. - -As mentioned previously, Shairport Sync implements full audio synchronisation when used with `alsa`, `sndio` or PulseAudio systems. This is done by monitoring the timestamps present in data coming from the audio source and the timing information from the audio system, e.g. `alsa`. To maintain the latency required for exact synchronisation, if the output device is running slow relative to the source, Shairport Sync will delete frames of audio to allow the device to keep up. If the output device is running fast, Shairport Sync will insert frames to keep time. The number of frames inserted or deleted is so small as to be almost inaudible on normal audio material. Frames are inserted or deleted as necessary at pseudorandom intervals. Alternatively, with `libsoxr` support, Shairport Sync can resample the audio feed to ensure the output device can keep up. This is less obtrusive than insertion and deletion but requires a good deal of processing power — most embedded devices probably can't support it. The process of insertion/deletion or resampling is rather inelegantly called “stuffing”. - -Stuffing is not done for partial audio synchronisation – the audio samples are simply presented at exactly the right time to the next stage in the processing chain. - -Timestamps are referenced relative to the source computer's clock – the `source clock`, but timing must be done relative to the clock of the computer running Shairport Sync – the `local clock`. So, another thing Shairport Sync has to do is to synchronize the source clock and the local clock, and it does this usually to within a fraction of a millisecond, using a variant of NTP synchronisation protocols. - -What else? --------------- -* Better Volume Control — Shairport Sync offers finer control at very top and very bottom of the volume range. See http://tangentsoft.net/audio/atten.html for a good discussion of audio "attenuators", upon which volume control in Shairport Sync is modelled. See also the diagram of the volume transfer function in the documents folder. In addition, Shairport Sync can offer an extended volume control range on devices with a restricted range. -* 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`. -* 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. -* A native [MQTT](https://en.wikipedia.org/wiki/MQTT) interface for metadata and/or controls. -* 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. -* Autotools and Libtool Support — the Shairport Sync build process uses GNU `autotools` and `libtool` to examine and configure the build environment — important for portability and for cross compilation. Previous versions of Shairport looked at the current system to determine which packages were available, instead of looking at the target system for what packages were available. - -Heritage -------- -Shairport Sync is a substantial rewrite of the fantastic work done in Shairport 1.0 by James Laird and others — please see https://github.com/abrasive/shairport/blob/development/README.md#contributors-to-version-1x for a list of the contributors to Shairport 1.x and Shairport 0.x. From a "heritage" point of view, Shairport Sync is a fork of Shairport 1.0. - -Status ------- -Shairport Sync works on a wide variety of Linux devices and FreeBSD. It works on standard Ubuntu laptops, on the Raspberry Pi with Raspbian Stretch, Jessie and Wheezy, Arch Linux and OpenWrt, and it runs on a Linksys NSLU2 and a TP-Link 710N using OpenWrt. It works with built-in audio and with a variety of USB-connected audio amplifiers and DACs, including a cheapo USB "3D Sound" dongle, a first generation iMic and a Topping TP30 amplifier with a USB DAC input. - -Shairport Sync will work with PulseAudio, which is installed in many desktop Linuxes. PulseAudio normally runs in the *user mode* but can be configured to run in *system mode*, though this is not recommended. Shairport Sync can work with it in either mode. - -Shairport Sync runs well on the Raspberry Pi on USB and I2S cards. It can drive the built-in sound card – see the note below on configuring the Raspberry Pi to make best use of it. It runs well on the Raspberry Pi Zero W with a suitable USB or I2S card. - -Shairport Sync runs natively on FreeBSD using the `sndio` sound system. - -Shairport Sync runs on Ubuntu, OpenWrt, Debian, Arch Linux, Fedora and FreeBSD inside VMWare Fusion on a Mac, but synchronisation is inaccurate — possibly because the sound card is being emulated. - -In addition, Shairport Sync can output to standard output, pipes, `soundio` and `ao` devices using appropriate backends. - -For information about changes and updates, please refer to the RELEASENOTES.md file in the distribution. - -Building And Installing the Development Version ---------------------- -See [here](https://github.com/mikebrady/shairport-sync/blob/development/INSTALL.md) for simple installation instructions for modern Linux systems like Rapsberry Pis. - -The following procedures will build and install the `shairport-sync` application into your system. - -**Remove Old Versions of Shairport Sync and its Startup Scripts** - -You should check to see if `shairport-sync` is already installed – you can use the command `$ which shairport-sync` to find where it is located, if installed. If it is installed you should delete it – you may need superuser privileges. After deleting, check again in case further copies are installed elsewhere. - -You should 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 in necessary. - -**FreeBSD** - -To build Shairport Sync from sources on FreeBSD please refer to [FREEBSD.md](https://github.com/mikebrady/shairport-sync/blob/master/FREEBSD.md). - -**Determine The Configuration Needed** - -Shairport Sync has a number of different "backends" that connnect it to the system's audio handling infrastructure. Most recent Linux distributions that have a GUI – including Ubuntu, Debian and others – use PulseAudio to handle sound. In such cases, it is inadvisable to attempt to disable or remove PulseAudio. Thus, if your system uses PulseAudio, you should build Shairport Sync with the PulseAudio backend. You can check to see if PulseAudio is running by opening a Terminal window and entering the command `$ pactl info`. Here is an example of what you'll get if PulseAudio is installed, though the exact details may vary: -``` -$ pactl info -Server String: unix:/run/user/1000/pulse/native -Library Protocol Version: 30 -Server Protocol Version: 30 -Is Local: yes -Client Index: 9 -Tile Size: 65472 -User Name: mike -Host Name: ubuntu -Server Name: pulseaudio -Server Version: 8.0 -Default Sample Specification: s16le 2ch 44100Hz -Default Channel Map: front-left,front-right -Default Sink: alsa_output.pci-0000_02_02.0.analog-stereo -Default Source: alsa_input.pci-0000_02_02.0.analog-stereo -Cookie: 96f9:3e8d -$ -``` -If PulseAudio in not installed, you'll get something like this: -``` -$ pactl info --bash: pactl: command not found -$ -``` -If your system does not use PulseAudio, then it is likely that it uses the Advanced Linux Sound Architecture (ALSA), so you should build Shairport Sync with the ALSA backend. By the way, many systems with PulseAudio also have ALSA (in fact, PulseAudio is effectively a client of ALSA); in those cases you should choose the PulseAudio backend. - -If PulseAudio is not installed, there is no necessity to install it for Shairport Sync. In fact, Shairport Sync works better without it. - -**Building** - -To build Shairport Sync from sources on Debian, Ubuntu, Raspbian, etc. follow these instructions. - -The following libraries are required: -* OpenSSL or mbed TLS (PolarSSL is supported but deprecated) -* Avahi -* ALSA and/or PulseAudio -* libdaemon -* autoconf -* automake -* libtool -* libpopt -* libconfig - -Optional: -* libsoxr -* libalac (This is a library containing the Apple ALAC decoder.) -* libmosquitto (This is to enable the mqtt-metadata-interface.) - -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. - -Debian, Ubuntu and Raspbian users can get the basics with: - -- `# apt-get install build-essential git xmltoman` – these may already be installed. -- `# apt-get install autoconf automake libtool libdaemon-dev libpopt-dev libconfig-dev` -- `# apt-get install libasound2-dev` for the ALSA libraries -- `# apt-get install libpulse-dev` for the PulseAudio libraries -- `# apt-get install avahi-daemon libavahi-client-dev` if you want to use Avahi (recommended). -- `# apt-get install libssl-dev` if you want to use OpenSSL and libcrypto, or use mbed TLS otherwise. -- `# apt-get install libmbedtls-dev` if you want to use mbed TLS, or use OpenSSL/libcrypto otherwise. You can still use PolarSSL with `apt-get install libpolarssl-dev` if you want to use PolarSSL, but it is deprecated as it's not longer being supported. -- `# apt-get install libsoxr-dev` if you want support for libsoxr-based resampling. This library is in many recent distributions; if not, instructions for how to build it from source for Rasbpian/Debian Wheezy are available at [LIBSOXR.md](https://github.com/mikebrady/shairport-sync/blob/master/LIBSOXR.md). - -If you wish to include the Apple ALAC decoder, you need install it first – please refer to the [ALAC](https://github.com/mikebrady/alac) repository for more information. - -**Download Shairport Sync:** -``` -$ git clone https://github.com/mikebrady/shairport-sync.git -``` - -Next, `cd` into the shairport-sync directory and execute the following commands: -``` -$ git checkout development -$ autoreconf -i -f -``` -(Note that the `autoreconf...` step may take some time on less powerful machines.) - -**Choose the appropriate `--with-*` options:** - -(Don't worry -- there's a recommended set of configuration options further down.) - -- `--with-alsa` include the ALSA backend module to audio to be output through the Advanced Linux Sound Architecture (ALSA) system directly. This is recommended for highest quality. -- `--with-pa` include the PulseAudio audio back end. This is recommended if your Linux installation already has PulseAudio installed. Although ALSA would be better, it requires direct and exclusive access to to a real (hardware) soundcard, and this is often impractical if PulseAudio is installed. -- `--with-stdout` include an optional backend module to enable raw audio to be output through standard output (stdout). -- `--with-pipe` include an optional backend module to enable raw audio to be output through a unix pipe. -- `--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-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. -- `--with-mqtt-client` to add support for publishing metadata and remote control via [MQTT](https://en.wikipedia.org/wiki/MQTT). -- `--with-configfiles` to install a configuration file and a separate sample file at the `make install` stage. Default is to install. An existing `/etc/shairport-sync.conf` will not be overwritten. -- `--with-pkg-config` to use pkg-config to find libraries. Default is to use pkg-config — this option is for special purpose use. -- `--with-apple-alac` to include the Apple ALAC Decoder. -- `--with-convolution` to include a convolution filter that can be used to apply effects such as frequency and phase correction, and a loudness filter that compensates for human ear non-linearity. Requires `libsndfile`. -- `--with-systemd` to include a script to create a Shairport Sync service that can optionally launch automatically at startup on `systemd`-based Linuxes. Default is not to to install. -- `--with-systemv` to include a script to create a Shairport Sync service that can optionally launch automatically at startup on System V based Linuxes. Default is not to to install. - -**Determine if it's a `systemd` or a "System V" installation:** - -If you wish to have Shairport Sync start automatically when your system boots, you need to figure out what so-called "init system" your system is using. (If you are using Shairport Sync with PulseAudio, as installed in many desktop systems, this section doesn't apply.) - -There are a number of init systems in use: `systemd`, `upstart` and "System V" among others, and it's actually difficult to be certain which one your system is using. Fortunately, for Shairport Sync, all you have to do is figure out if it's a `systemd` init system or not. If it is not a `systemd` init system, you can assume that it is either a System V init system or else it is compatible with a System V init system. Recent systems tend to use `systemd`, whereas older systems use `upstart` or the earlier System V init system. - -The easiest way to look at the first few lines of the `init` manual. Enter the command: - -``` -$ man init -``` -In a `systemd` system, the top lines of the `man` page make it clear that it's a `systemd` system, as follows (from Ubuntu 16.04): -``` -SYSTEMD(1) systemd SYSTEMD(1) - -NAME - systemd, init - systemd system and service manager -... -``` -Other init systems will look considerably different. For instance, in an `upstart` system, the top lines of the `man` page indicate it's using `upstart` system, as follows (from Ubuntu 14.04): - -``` -init(8) System Manager's Manual init(8) - -NAME - init - Upstart process management daemon - -SYNOPSIS - init [OPTION]... -... -``` -In a System V system, the top lines of the `man` page are as follows (from Debian 7.11): -``` -INIT(8) Linux System Administrator's Manual INIT(8) - -NAME - init, telinit - process control initialization - -SYNOPSIS - /sbin/init [ -a ] [ -s ] [ -b ] [ -z xxx ] [ 0123456Ss ] - /sbin/telinit [ -t SECONDS ] [ 0123456sSQqabcUu ] - /sbin/telinit [ -e VAR[=VAL] ] - -... -``` -If your system is definitely a `systemd` system, choose `--with-systemd` below. Otherwise, choose `--with-systemv`. - -**Choose the location of the configuration file** - -A final consideration is the location of the configuration file `shairport-sync.conf`. This will be placed in the directory specified by the `sysconfdir` configuration variable, which defaults to `/usr/local/etc`. This is normal in BSD Unixes, but is unusual in Linux. Hence, for Linux installations, you need to set the `sysconfdir` variable to `/etc` using the configuration setting `--sysconfdir=/etc`. - -**Sample `./configure` command with parameters for a typical Linux `systemd` installation:** - -Here is a recommended set of configuration options suitable for Linux installations that use `systemd`, such as Ubuntu 15.10 and later, and Raspbian Stretch and Jessie. It specifies both the ALSA and PulseAudio backends and includes a sample configuration file and an script for automatic startup on system boot: - -`$ ./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`. -* 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:** - -Enter: - -``` -$ make -``` - -to build the application. - -Assuming you have used the `./configure` settings suggested above, you can now install the application: - -``` -$ sudo make install -``` -The user and group `shairport-sync` will be created if necessary, and a runtime folder will be created at `/var/run/shairport-sync` if you have chosen `--with-systemv`. In addition, a `man` page, a default configuration file and automatic power-on startup script will be installed. - - -**Complete installation into to a `systemd` system** - -If you have chosen the `--with-systemd` configuration option, then, to enable Shairport Sync to start automatically at system startup, enter: - -``` -$ sudo systemctl enable shairport-sync -``` - -**Complete installation into a System V system** - -If you have chosen the `--with-systemv` configuration option, enter: -``` -$ sudo update-rc.d shairport-sync defaults 90 10 -``` - -**Man Page** - -You can view the man page here: http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/development/man/shairport-sync.html - -Configuring Shairport Sync --------- -There are two logically distinct parts to getting Shairport Sync to run properly on your machine: (1) starting and stopping it and (2) ensuring it has the right settings. - -**(1) Starting and Stopping:** -Starting and stopping Shairport Sync automatically is taken care of differently in different versions of Linux – see the previous section for an example of installing into a `systemd` or a System V based system. - -If you are using PulseAudio in the standard "user" mode, you can not start Shairport Sync automatically at boot time – it must be started when the user logs in to a GUI session. Various GUI tools exist to enable you to build a Startup Application. However, there does not appear to be a similar tool for terminating the program when you log out. Also, it the GUI switches to another user, the user's PulseAudio session is disconnected from the output device. - -**(2) Settings:** -To get the best from Shairport Sync, you’ll need to (a) give Shairport Sync a service name by which it will be seen in iTunes etc. and (b) specify the backend you wish to use - `alsa` for the ALSA backend, or `pa` for the PulseAudio back end. If only one backend is included at compilation, or if the backend is ALSA, there is no need to explicitly specify the backend. - -For the ALSA backend you may need to (c) specify the output device to use and (d) specify the name of the mixer volume control to use to control the output level. To get values for (b) and (c) you might need to explore the ALSA output devices with a program like `alsamixer` or similar. - -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: - -``` -general = -{ - name = "Front Room"; - password = "secret"; - interpolation = "soxr"; - // ... other general settings -}; -``` -(Remember, anything preceded by `//` is a comment and will have no effect on the setting of Shairport Sync.) - -**Important:** 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. - -No backend is specified here, so it will default to the `alsa` backend if more than one back end has been compiled. To route the output to PulseAudio, add: - -``` - output_backend = "pa"; -``` -to the `general` stanza. - -The `alsa` group is used to specify properties of the output device. The most obvious setting is the name of the output device which you can set using the `output_device` tag. - -The following `alsa` group settings are very important for maximum performance. If your audio device has a mixer that can be use to control the volume, then Shairport Sync can use it to give instant response to volume and mute commands and it can offload some work from the processor. -* The `mixer_control_name` tag allows you to specify the name of the mixer volume control. -* The `mixer_device` tag allows you specify where the mixer is. By default, the mixer is on the `output_device`, so you only need to use the `mixer_device` tag if the mixer is elsewhere. This can happen if you specify a *device* rather than a *card* with the `output_device` tag, because normally a mixer is associated with a *card* rather than a device. Suppose you wish to use the output device `5` of card `hw:0` and the mixer volume-control named `PCM`: - -``` -alsa = -{ - output_device = "hw:0,5"; - mixer_device = "hw:0"; - mixer_control_name = "PCM"; - // ... other alsa settings -}; -``` - -The `pa` group is used to specify settings relevant to the PulseAudio backend. You can set the "Application Name" that will appear in the "Sound" control panel. You can also set the sink name that audio will be pushed to. Here's an example `pa` group configuration: - -``` -pa = -{ - application_name = "Shairport Sync - Bedroom"; - sink = "server_bedroom"; // Sink name or ID. If not specified, the default sink will be used. -}; -``` - -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`. This is to facilitate situations where something has to be done before and after playing, e.g. switching on an amplifier beforehand and switching it off afterwards. Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the respective commands have been completed before continuing. - -Note that the full path to the programs must be specified, and script files will not be executed unless they are marked as executable and have the appropriate shebang `#!/bin/...` as the first line. (This behaviour may be different from other Shairports.) - -Shairport Sync can run a program whenever the volume is set or changed. You specify them using the `general` group settings `run_this_when_volume_changes`. This is to facilitate situations where something has to be done when the volume changes, e.g. adjust an external mixer value. Set the `wait_for_completion` value to `"yes"` for Shairport Sync to wait until the command has been completed before continuing. Again, please note that the full path to the program must be specified, and script files will not be executed unless they are marked as executable and have the appropriate shebang `#!/bin/...` as the first line. - -Note: Shairport Sync can take configuration settings from command line options. This is mainly for backward compatibility, but sometimes still useful. For normal use, it is strongly recommended that you use the configuration file method. - -**Raspberry Pi** - -The Raspberry Pi has a built-in audio DAC that is connected to the device's headphone jack. An updated audio driver has greatly improved the quality of the output – see [#525](https://github.com/mikebrady/shairport-sync/issues/525) for details. To activate the updated driver, add the line: -``` -audio_pwm_mode=2 -``` -to `/boot/config.txt` and reboot. -Apart from a loud click when used for the first time after power-up, it is quite adequate for casual listening. A problem is that it declares itself to have a very large mixer volume control range – all the way from -102.38dB up to +4dB, a range of 106.38 dB. In reality, only the top 50 dB of it is in any way usable. To help get the most from the DAC, consider using the `volume_range_db` setting in the `general` group to instruct Shairport Sync to use the top of the DAC mixer's declared range. For example, if you set the `volume_range_db` figure to 50, the top 50 dB of the range will the used. With this setting on the Raspberry Pi, maximum volume will be +4dB and minimum volume will be -46dB, below which muting will occur. - -From a user's point of view, the effect of using this setting is to move the minimum usable volume all the way down to the bottom of the user's volume control, rather than have the minimum usable volume concentrated very close to the maximum volume. - -Another setting to consider is the `general` `drift_tolerance_in_seconds` setting: you should set it to a larger tolerance, say 10 milliseconds – `drift_tolerance_in_seconds=0.010;` – to reduce the amount of overcorrection that seems to occur when using the Raspberry Pi's built-in DAC. - -*Command Line Arguments* - -As previously mentioned, you can use command line arguments to provide settings to Shairport Sync as before, though newer settings will only be available via the configuration file. For full information, please read the Shairport Sync `man` page, also available at http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/development/man/shairport-sync.html. - -Apart from the following options, all command line options can be replaced by settings in the configuration file. Here is a brief description of command line options that are not replicated by settings in the settings file. - -* The `-c` option allows you to specify the location of the configuration file. -* The `-V` option gives you version information about Shairport Sync and then quits. -* The `-d` option causes Shairport Sync to properly daemonise itself, that is, to run in the background. You may need sudo privileges for this. -* The `-k` option causes Shairport Sync to kill an existing Shairport Sync daemon. You may need to have sudo privileges for this. - -The System V init script at `/etc/init.d/shairport-sync` has a bare minimum : -`-d`. Basically all it does is put the program in daemon mode. The program will read its settings from the configuration file. - -Examples --------- - -Here are some examples of complete configuration files. - -``` -general = { - name = "Joe's Stereo"; -}; - -alsa = { - output_device = "hw:0"; -}; -``` - -This gives the service a particular name — "Joe's Stereo" and specifies that audio device hw:0 be used. - -For best results with the `alsa` backend — including getting true mute and instant response to volume control and pause commands — you should access the hardware volume controls. Use `amixer` or `alsamixer` or similar to discover the name of the mixer control to be used as the `mixer_control_name`. - -Here is an example for for a Raspberry Pi using its internal soundcard — device hw:0 — that drives the headphone jack: -``` -general = { - name = "Mike's Boombox"; -}; - -alsa = { - output_device = "hw:0"; - mixer_control_name = "PCM"; -}; -``` - -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`: -``` -general = { - name = "Kitchen"; - interpolation = "soxr"; -}; - -alsa = { - output_device = "hw:1"; - mixer_control_name = "PCM"; -}; -``` - -For a cheapo "3D Sound" USB card (Stereo output and input only) on a Raspberry Pi: -``` -general = { - name = "Front Room"; -}; - -alsa = { - output_device = "hw:1"; - mixer_control_name = "Speaker"; -}; -``` - -For a first generation Griffin iMic on a Raspberry Pi: -``` -general = { - name = "Attic"; -}; - -alsa = { - output_device = "hw:1"; - mixer_control_name = "PCM"; -}; -``` - -For an NSLU2, which has no internal sound card, there appears to be a bug in ALSA — you can not specify a device other than "default". Thus: - -On an NSLU2, to drive a first generation Griffin iMic: -``` -general = { - name = "Den"; -}; - -alsa = { - mixer_control_name = "PCM"; -}; -``` - -On an NSLU2, to drive the "3D Sound" USB card: -``` -general = { - name = "TV Room"; -}; - -alsa = { - mixer_control_name = "Speaker"; -}; -``` - -Finally, here is an example of using the PulseAudio backend: -``` -general = { - name = "Zoe's Computer"; - output_backend = "pa"; -}; - -``` - - -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. - -**Metadata broadcasting over UDP** - -As an alternative to sending metadata to a pipe, the `socket_address` and `socket_port` tags may be set in the metadata group to cause Shairport Sync to broadcast UDP packets containing the track metadata. - -The advantage of UDP is that packets can be sent to a single listener or, if a multicast address is used, to multiple listeners. It also allows metadata to be routed to a different host. However UDP has a maximum packet size of about 65000 bytes; while large enough for most data, Cover Art will often exceed this value. Any metadata exceeding this limit will not be sent over the socket interface. The maximum packet size may be set with the `socket_msglength` tag to any value between 500 and 65000 to control this - lower values may be used to ensure that each UDP packet is sent in a single network frame. The default is 500. Other than this restriction, metadata sent over the socket interface is identical to metadata sent over the pipe interface. - -The UDP metadata format is very simple - the first four bytes are the metadata *type*, and the next four bytes are the metadata *code* (both are sent in network byte order - see https://github.com/mikebrady/shairport-sync-metadata-reader for a definition of those terms). The remaining bytes of the packet, if any, make up the raw value of the metadata. - -Latency -------- -Latency is the exact time from a sound signal's original timestamp until that signal actually "appears" on the output of the audio output device, usually a Digital to Audio Converter (DAC), irrespective of any internal delays, processing times, etc. in the computer. - -Shairport Sync uses latencies supplied by the source, typically either 2 seconds or just over 2.25 seconds. You shouldn't need to change them. - -Problems can arise when you are trying to synchronise with speaker systems — typically surround-sound home theatre systems — that have their own inherent delays. You can compensate for an inherent delay using the appropriate backend (typically `alsa`) `audio_backend_latency_offset_in_seconds`. Set this offset (in frames) 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. - -Resynchronisation -------------- -Shairport Sync actively maintains synchronisation with the source. -If synchronisation is lost — say due to a busy source or a congested network — Shairport Sync will mute its output and resynchronise. The loss-of-sync threshold is a very conservative 0.050 seconds — i.e. the actual time and the expected time must differ by more than 50 ms to trigger a resynchronisation. Smaller disparities are corrected by insertions or deletions, as described above. -* You can vary the resync threshold, or turn resync off completely, with the `general` `resync_threshold_in_seconds` setting. - -Tolerance ---------- -Playback synchronisation is allowed to wander — to "drift" — a small amount before attempting to correct it. The default is 0.002 seconds, i.e. 2 ms. The smaller the tolerance, the more likely it is that overcorrection will occur. Overcorrection is when more corrections (insertions and deletions) are made than are strictly necessary to keep the stream in sync. Use the `statistics` setting to monitor correction levels. Corrections should not greatly exceed net corrections. -* You can vary the tolerance with the `general` `drift_tolerance_in_seconds` setting. - -Some Statistics ---------------- -If you turn on the `general` `statistics` setting, a heading like this will be output to the console or log file: -``` -sync error in milliseconds, net correction in ppm, corrections in ppm, total packets, missing packets, late packets, too late packets, resend requests, min DAC queue size, min buffer occupancy, max buffer occupancy -``` -This will be followed by the statistics themselves at regular intervals, for example: -``` - -1.3, 25.9, 25.9, 3758, 0, 0, 0, 0, 4444, 263, 270 - -2.0, 96.0, 96.0, 7516, 0, 0, 0, 0, 5357, 260, 267 - -2.0, 96.0, 96.0, 11274, 0, 0, 0, 0, 4956, 262, 268 - -2.0, 94.5, 94.5, 15032, 0, 0, 0, 0, 5430, 263, 267 - -2.0, 99.8, 99.8, 18790, 0, 0, 0, 0, 5047, 261, 268 - -2.0, 91.5, 91.5, 22548, 0, 0, 0, 0, 5258, 260, 267 - -2.0, 90.7, 90.7, 26306, 0, 0, 0, 0, 5279, 262, 267 - -2.0, 96.0, 96.0, 30064, 0, 0, 0, 0, 5551, 260, 266 -``` - -"Sync error in milliseconds" is the average deviation from exact synchronisation. The first line of the example above indicates that the output is on average 1.3 milliseconds behind exact synchronisation. Sync is allowed to drift by the `general` `drift_tolerance_in_seconds` setting — (± 0.002 seconds) by default — before a correction will be made. - -"Net correction in ppm" is actually the net sum of corrections — the number of frame insertions less the number of frame deletions — given as a moving average in parts per million. After an initial settling period, it represents the drift — the divergence between the rate at which frames are generated at the source and the rate at which the output device consumes them. The first line of the example above indicates that the output device is consuming frames 25.9 ppm faster than the source is generating them. The subsequent lines indicate that the stable actual difference in the clocks is around 95 ppm. - -"Corrections in ppm" is the number of frame insertions plus the number of frame deletions (i.e. the total number of corrections), given as a moving average in parts per million. The closer this is to the net corrections, the fewer "unnecessary" corrections that are being made. Third party programs tend to have much larger levels of corrections. - -For reference, a drift of one second per day is approximately 11.57 ppm. Left uncorrected, even a drift this small between two audio outputs will be audible after a short time. The above sample is from an Ethernet-connected iMac driving a WiFi-connected Raspberry Pi 3 using an IQaudIO Pi-DigiAMP+. - -It's not unusual to have resend requests, late packets and even missing packets if some part of the connection to the Shairport Sync device is over WiFi. Late packets can sometimes be asked for and received multiple times. Sometimes late packets are sent and arrive too late, but have already been sent and received in time, so weren't needed anyway... - -"Min DAC queue size" is the minimum size the queue of samples in the output device's hardware buffer was measured at. It is meant to stand at 0.15 seconds = 6,615 frames at 44,100 frames per second, and will go low if the processor is very busy. If it goes below about 2,000 then it's an indication that the processor can't really keep up. - -WiFi Issues ---------- -If you are using WiFi, you should ensure that WiFi power management is off. See [TROUBLESHOOTING](https://github.com/mikebrady/shairport-sync/blob/development/TROUBLESHOOTING.md) for more details. - -Troubleshooting ---------------- -Please refer to [TROUBLESHOOTING](https://github.com/mikebrady/shairport-sync/blob/development/TROUBLESHOOTING.md) for a few hints, contributed by users. +**Warning** +This is the "unstable" branch. It is used for very preliminary unfinished or untested code and can change at any time and without warning. diff --git a/activity_monitor.c b/activity_monitor.c new file mode 100644 index 00000000..697bec95 --- /dev/null +++ b/activity_monitor.c @@ -0,0 +1,274 @@ +/* + * 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 +#include +#include +#include + +#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_while_active) { +#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_while_active) { +#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) { + pthread_mutex_lock(&activity_monitor_mutex); + 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 coldn'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_mode_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_mutex_unlock(&activity_monitor_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; + int rc; + 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_mode_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_mode_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(1, "activity_monitor_stop start..."); + pthread_cancel(activity_monitor_thread); + pthread_join(activity_monitor_thread, NULL); + debug(1, "activity_monitor_stop complete"); + } +} diff --git a/activity_monitor.h b/activity_monitor.h new file mode 100644 index 00000000..d9e1ffd6 --- /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 a0b5f74d..78686376 100644 --- a/audio.c +++ b/audio.c @@ -106,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); + } } } @@ -153,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)) { diff --git a/audio.h b/audio.h index a4f0f178..24536b91 100644 --- a/audio.h +++ b/audio.h @@ -47,7 +47,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 f32df151..711a1767 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,8 +27,6 @@ #define ALSA_PCM_NEW_HW_PARAMS_API -#include "audio.h" -#include "common.h" #include #include #include @@ -37,6 +35,18 @@ #include #include +#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 + static void help(void); static int init(int argc, char **argv); static void deinit(void); @@ -45,7 +55,6 @@ 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); @@ -53,9 +62,9 @@ static void volume(double vol); void do_volume(double vol); 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 audio_output audio_alsa = { .name = "alsa", @@ -69,25 +78,42 @@ audio_output audio_alsa = { .delay = &delay, .play = &play, .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 + .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. + +// we may not be allowed to use hardware mute, so we must reflect that too. + +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 +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; static unsigned int desired_sample_rate; static enum sps_format_t sample_format; int frame_size; // in bytes for interleaved stereo -static snd_pcm_t *alsa_handle = NULL; +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; @@ -107,13 +133,13 @@ static int has_softvol = 0; 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 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 +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 @@ -140,11 +166,10 @@ 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"); } void set_alsa_out_dev(char *dev) { alsa_out_dev = dev; } @@ -180,7 +205,7 @@ int open_mixer() { 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) { - debug(1, "Failed to find mixer element"); + warn("failed to find mixer control \"%s\".", alsa_mix_ctrl); response = -5; } else { response = 1; // we found a hardware mixer and successfully opened it @@ -212,11 +237,13 @@ void do_snd_mixer_selem_set_playback_dB_all(snd_mixer_elem_t *mix_elem, double v } 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, "Error %d (\"%s\") freeing the output device hardware while " + "closing it.", + derr, snd_strerror(derr)); if ((derr = snd_pcm_close(alsa_handle))) debug(1, "Error %d (\"%s\") closing the output device.", derr, snd_strerror(derr)); @@ -233,7 +260,8 @@ int actual_open_alsa_device(void) { /* 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; @@ -242,16 +270,20 @@ int actual_open_alsa_device(void) { 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, + // SND_PCM_NONBLOCK); ret = snd_pcm_open(&alsa_handle, alsa_out_dev, SND_PCM_STREAM_PLAYBACK, 0); if (ret < 0) return ret; @@ -360,7 +392,8 @@ int actual_open_alsa_device(void) { 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); } @@ -377,7 +410,8 @@ int actual_open_alsa_device(void) { 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); } @@ -390,13 +424,14 @@ int actual_open_alsa_device(void) { return ret; } - // check parameters after attempting to set them… + // 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 " + inform("Actual period size set to a different value than requested. " + "Requested: %lu, actual " "setting: %lu", period_size_requested, actual_period_size); } @@ -405,7 +440,8 @@ int actual_open_alsa_device(void) { 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); } @@ -424,8 +460,9 @@ int actual_open_alsa_device(void) { 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)); + warn("audio_alsa: Unable to get current sw parameters for device \"%s\": " + "%s.", + alsa_out_dev, snd_strerror(ret)); return ret; } @@ -467,9 +504,9 @@ int actual_open_alsa_device(void) { buffer_size); } */ - debug(1, - "The alsa buffer is smaller (%lu bytes) than the desired backend buffer " - "length (%ld) you have chosen.", + debug(1, "The alsa buffer is smaller (%lu bytes) than the desired backend " + "buffer " + "length (%ld) you have chosen.", actual_buffer_length, config.audio_backend_buffer_desired_length); } @@ -488,14 +525,17 @@ int actual_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:"); @@ -612,30 +652,163 @@ int open_alsa_device(void) { return result; } +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 = ¶meters; // likewise the parameters stuff + if (alsa_mix_mindb == SND_CTL_TLV_DB_GAIN_MUTE) { + // For instance, the Raspberry Pi does this + debug(1, "Lowest dB value is a mute"); + mixer_volume_setting_gives_mute = 1; + alsa_mix_mute = SND_CTL_TLV_DB_GAIN_MUTE; // this may not be + // necessary -- it's + // always + // going to be SND_CTL_TLV_DB_GAIN_MUTE, right? + // debug(1, "Try minimum volume + 1 as lowest true attenuation + // value"); + if (snd_mixer_selem_ask_playback_vol_dB(alsa_mix_elem, alsa_mix_minv + 1, + &alsa_mix_mindb) != 0) + debug(1, "Can't get dB value corresponding to a minimum volume " + "+ 1."); + } + debug(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 + 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 = ¶meters; // 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 = ¶meters; // likewise the parameters + stuff + debug(1,"Max and min dB calculated are %d and + %d.",alsa_mix_maxdb,alsa_mix_mindb); + */ + } + } + if (((config.alsa_use_hardware_mute == 1) && + (snd_mixer_selem_has_playback_switch(alsa_mix_elem))) || + mixer_volume_setting_gives_mute) { + audio_alsa.mute = &mute; // insert the mute function now we know it + // can do muting stuff + // debug(1, "Has mixer and mute ability we will use."); + } else { + // debug(1, "Has mixer but not using hardware mute."); + } + close_mixer(); + } + debug_mutex_unlock(&alsa_mixer_mutex, 3); // release the mutex + pthread_cleanup_pop(0); + pthread_setcancelstate(oldState, NULL); + } + } + return response; +} + 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.15; - + 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; // 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! + // do the "general" audio options. Note, these options are in the "general" + // stanza! parse_general_audio_options(); if (config.cfg != NULL) { @@ -672,7 +845,8 @@ static int init(int argc, char **argv) { else if (strcasecmp(str, "yes") == 0) config.no_sync = 1; else { - warn("Invalid disable_synchronization option choice \"%s\". It should be \"yes\" or " + warn("Invalid disable_synchronization option choice \"%s\". It should " + "be \"yes\" or " "\"no\". It is set to \"no\"."); config.no_sync = 0; } @@ -687,7 +861,8 @@ static int init(int argc, char **argv) { 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 " + 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; } @@ -700,7 +875,8 @@ static int init(int argc, char **argv) { 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 " + 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; } @@ -723,7 +899,8 @@ static int init(int argc, char **argv) { else if (strcasecmp(str, "S8") == 0) config.output_format = SPS_FORMAT_S8; else { - warn("Invalid output format \"%s\". It should be \"U8\", \"S8\", \"S16\", \"S24\", " + warn("Invalid output format \"%s\". It should be \"U8\", \"S8\", " + "\"S16\", \"S24\", " "\"S24_3LE\", \"S24_3BE\" or " "\"S32\". It is set to \"S16\".", str); @@ -742,7 +919,8 @@ static int init(int argc, char **argv) { config.output_rate = value; break; default: - warn("Invalid output rate \"%d\". It should be a multiple of 44,100 up to 352,800. It is " + warn("Invalid output rate \"%d\". It should be a multiple of 44,100 up " + "to 352,800. It is " "set to 44,100", value); config.output_rate = 44100; @@ -756,7 +934,8 @@ static int init(int argc, char **argv) { 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\". " + warn("Invalid use_mmap_if_available option choice \"%s\". It should be " + "\"yes\" or \"no\". " "It is set to \"yes\"."); config.no_mmap = 0; } @@ -800,12 +979,26 @@ static int init(int argc, char **argv) { } } - // Get the optional "Keep DAC Busy setting" - int kdb; - if (config_set_lookup_bool(config.cfg, "alsa.disable_standby_mode", &kdb)) { - config.keep_dac_busy = kdb; + + /* Get the optional disable_standby_mode setting. */ + config.disable_standby_mode = disable_standby_while_active; + config.keep_dac_busy = 0; + 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, "while active") == 0) + config.disable_standby_mode = disable_standby_while_active; + else { + warn("Invalid disable_standby_mode option choice \"%s\". It should be " + "\"always\", \"while active\" or \"never\". " + "It is set to \"while active\"."); + } } - debug(1, "alsa: disable_standby_mode is %s.", config.keep_dac_busy ? "on" : "off"); + + debug(1, "alsa: disable_standby_mode is %d.", config.disable_standby_mode); } optind = 1; // optind=0 is equivalent to optind=1 plus special behaviour @@ -847,119 +1040,14 @@ static int init(int argc, char **argv) { debug(1, "alsa: output device name is \"%s\".", alsa_out_dev); - if (hardware_mixer) { - 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_mutex, 1000, 1); - if (open_mixer() == 1) { - if (snd_mixer_selem_get_playback_volume_range(alsa_mix_elem, &alsa_mix_minv, &alsa_mix_maxv) < - 0) - debug(1, "Can't read mixer's [linear] min and max volumes."); - else { - if (snd_mixer_selem_get_playback_dB_range(alsa_mix_elem, &alsa_mix_mindb, - &alsa_mix_maxdb) == 0) { - - audio_alsa.volume = &volume; // insert the volume function now we know it can do dB stuff - audio_alsa.parameters = ¶meters; // likewise the parameters stuff - if (alsa_mix_mindb == SND_CTL_TLV_DB_GAIN_MUTE) { - // For instance, the Raspberry Pi does this - debug(1, "Lowest dB value is a mute"); - mixer_volume_setting_gives_mute = 1; - alsa_mix_mute = SND_CTL_TLV_DB_GAIN_MUTE; // this may not be necessary -- it's always - // going to be SND_CTL_TLV_DB_GAIN_MUTE, right? - // debug(1, "Try minimum volume + 1 as lowest true attenuation value"); - if (snd_mixer_selem_ask_playback_vol_dB(alsa_mix_elem, alsa_mix_minv + 1, - &alsa_mix_mindb) != 0) - debug(1, "Can't get dB value corresponding to a minimum volume + 1."); - } - debug(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 - 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 = ¶meters; // 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 = ¶meters; // likewise the parameters stuff - debug(1,"Max and min dB calculated are %d and - %d.",alsa_mix_maxdb,alsa_mix_mindb); - */ - } - } - if (((config.alsa_use_hardware_mute == 1) && - (snd_mixer_selem_has_playback_switch(alsa_mix_elem))) || - mixer_volume_setting_gives_mute) { - audio_alsa.mute = &mute; // insert the mute function now we know it can do muting stuff - // debug(1, "Has mixer and mute ability we will use."); - } else { - // debug(1, "Has mixer but not using hardware mute."); - } - close_mixer(); - } - debug_mutex_unlock(&alsa_mutex, 3); // release the mutex - - pthread_cleanup_pop(0); - pthread_setcancelstate(oldState, NULL); - } else { - debug(1, "alsa: no hardware mixer selected."); - } - alsa_mix_handle = NULL; - - // so, now, if the option to keep the DAC running has been selected, start a thread to monitor the + // 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 desired_sample_rate = config.output_rate; sample_format = config.output_format; - if (response == 0) { - // try opening the device. - int ret = actual_open_alsa_device(); - - if (ret == 0) - actual_close_alsa_device(); - else - die("audio_alsa error %d opening the alsa device. Incorrect settings or device already busy?", - ret); - } 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); @@ -978,8 +1066,45 @@ static void deinit(void) { 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); + } + } 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); + } + } + 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 start(int i_sample_rate, int i_sample_format) { - // debug(2,"audio_alsa start called."); + debug(3, "audio_alsa start called."); if (i_sample_rate == 0) desired_sample_rate = 44100; // default else @@ -995,134 +1120,120 @@ static void start(int i_sample_rate, int i_sample_format) { stall_monitor_start_time = 0; stall_monitor_frame_count = 0; - - 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 + if (alsa_device_initialised == 0) { + debug(2, "alsa: start() calling do_alsa_device_init_if_needed."); + do_alsa_device_init_if_needed(); + } } -// assuming pthread cancellation is disabled -int my_snd_pcm_state_and_delay(snd_pcm_t *pcm, snd_pcm_state_t *state, snd_pcm_sframes_t *delayp) { - *state = SND_PCM_STATE_LAST + 1; // unknown - *delayp = 0; - - int ret; +int delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay) { snd_pcm_status_t *alsa_snd_pcm_status; snd_pcm_status_alloca(&alsa_snd_pcm_status); struct timespec tn; // time now snd_htimestamp_t update_timestamp; // actually a struct timespec - ret = snd_pcm_status(pcm, alsa_snd_pcm_status); - if (ret) { - return ret; + int ret = snd_pcm_status(alsa_handle, alsa_snd_pcm_status); + if (ret == 0) { + *state = snd_pcm_status_get_state(alsa_snd_pcm_status); + + if ((*state == SND_PCM_STATE_RUNNING) || (*state == SND_PCM_STATE_DRAINING)) { + +// 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 + + *delay = snd_pcm_status_get_delay(alsa_snd_pcm_status); + + if (*state == SND_PCM_STATE_DRAINING) + debug(1, "alsa: draining with a delay of %d.", delay); + + clock_gettime(CLOCK_MONOTONIC, &tn); + uint64_t time_now_ns = tn.tv_sec * (uint64_t)1000000000 + tn.tv_nsec; + uint64_t update_timestamp_ns = + update_timestamp.tv_sec * (uint64_t)1000000000 + update_timestamp.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); + 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)desired_sample_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; + } + } else { + debug(1, "alsa: can't get device's status."); } - - *state = snd_pcm_status_get_state(alsa_snd_pcm_status); - - if (*state == SND_PCM_STATE_RUNNING) { - - clock_gettime(CLOCK_MONOTONIC, &tn); - snd_pcm_status_get_htstamp(alsa_snd_pcm_status, &update_timestamp); - - uint64_t t1 = tn.tv_sec * (uint64_t)1000000000 + tn.tv_nsec; - uint64_t t2 = update_timestamp.tv_sec * (uint64_t)1000000000 + update_timestamp.tv_nsec; - uint64_t delta = t1 - t2; - - uint64_t frames_played_since_last_interrupt = - ((uint64_t)desired_sample_rate * delta) / 1000000000; - snd_pcm_sframes_t frames_played_since_last_interrupt_sized = frames_played_since_last_interrupt; - - *delayp = - snd_pcm_status_get_delay(alsa_snd_pcm_status) - frames_played_since_last_interrupt_sized; - } - return 0; + 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; - // snd_pcm_sframes_t is a signed long -- hence the return of a "long" - int reply = 0; - // debug(3,"audio_alsa delay called."); - if (alsa_handle == NULL) { - reply = -ENODEV; - } else { + 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); - int derr; - snd_pcm_state_t dac_state = snd_pcm_state(alsa_handle); - if (dac_state == SND_PCM_STATE_RUNNING) { - reply = snd_pcm_delay(alsa_handle, the_delay); + ret = delay_and_status(&state, &my_delay); - if (reply != 0) { - debug(1, "Error %d in delay(): \"%s\". Delay reported is %d frames.", reply, - snd_strerror(reply), *the_delay); - derr = snd_pcm_recover(alsa_handle, reply, 1); - if (derr < 0) - warn("Error %d -- could not clear an error after attempting delay(): \"%s\".", derr, - snd_strerror(derr)); - - frame_index = 0; - measurement_data_is_valid = 0; - } else { - if (*the_delay == 0) { - // there's nothing in the pipeline, so we can't measure frame rate. - frame_index = 0; // we'll be starting over... - measurement_data_is_valid = 0; - } - } - } else { - reply = -EIO; // shomething is wrong - frame_index = 0; // we'll be starting over... - measurement_data_is_valid = 0; - - if (dac_state == SND_PCM_STATE_PREPARED) { - debug(2, "delay not available -- state is SND_PCM_STATE_PREPARED"); - } else { - if (dac_state == SND_PCM_STATE_XRUN) { - debug(2, "delay not available -- state is SND_PCM_STATE_XRUN"); - } else { - - debug(1, "Error -- ALSA delay(): bad state: %d.", dac_state); - } - if ((derr = snd_pcm_prepare(alsa_handle))) { - debug(1, "Error preparing after delay error: \"%s\".", snd_strerror(derr)); - derr = snd_pcm_recover(alsa_handle, derr, 1); - if (derr < 0) - warn("Error %d -- could not clear an error after attempting to recover following a " - "delay(): \"%s\".", - derr, snd_strerror(derr)); - } - } - } debug_mutex_unlock(&alsa_mutex, 0); pthread_cleanup_pop(0); - // here, occasionally pretend there's a problem with pcm_get_delay() - // if ((random() % 100000) < 3) // keep it pretty rare - // reply = -EIO; // pretend something bad has happened pthread_setcancelstate(oldState, NULL); + + *the_delay = my_delay; // note: snd_pcm_sframes_t is a long } - if ((reply == 0) && (*the_delay != 0)) { - uint64_t stall_monitor_time_now = get_absolute_time_in_fp(); - if ((stall_monitor_start_time != 0) && (stall_monitor_frame_count == *the_delay)) { - // hasn't outputted anything since the last call to delay() - uint64_t time_stalled = stall_monitor_time_now - stall_monitor_start_time; - if (time_stalled > stall_monitor_error_threshold) { - reply = sps_extra_errno_output_stalled; - } - } else { - // is outputting stuff, so restart the monitoring here - stall_monitor_start_time = stall_monitor_time_now; - stall_monitor_frame_count = *the_delay; - } - } else { - // if there is an error or the delay is zero (from which it is assumed there is no output) - 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 reply; + return ret; } int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played) { @@ -1138,96 +1249,49 @@ int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played) { return response; } -int untimed_play(void *buf, int samples) { - +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 - int ret = 0; - if (alsa_handle == NULL) { - pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1); - ret = actual_open_alsa_device(); - if (ret == 0) { - if (audio_alsa.volume) - do_volume(set_volume); - if (audio_alsa.mute) - do_mute(0); - } + snd_pcm_state_t state; + snd_pcm_sframes_t my_delay; + int ret = delay_and_status(&state, &my_delay); - debug_mutex_unlock(&alsa_mutex, 3); - pthread_cleanup_pop(0); // release the mutex - } - if (ret == 0) { - pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 0); - // snd_pcm_sframes_t current_delay = 0; - int err, err2; - if ((snd_pcm_state(alsa_handle) == SND_PCM_STATE_XRUN) || - (snd_pcm_state(alsa_handle) == SND_PCM_STATE_OPEN) || - (snd_pcm_state(alsa_handle) == SND_PCM_STATE_SETUP)) { - if ((err = snd_pcm_prepare(alsa_handle))) { - debug(1, "Error preparing after underrun: \"%s\".", snd_strerror(err)); - err = snd_pcm_recover(alsa_handle, err, 1); - if (err < 0) - warn("Error %d -- could not clear an error after detecting underrun in play(): \"%s\".", - err, snd_strerror(err)); + 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); } - frame_index = 0; // we'll be starting over - measurement_data_is_valid = 0; - } else 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)) { - // debug(3, "write %d frames.", samples); - err = alsa_pcm_write(alsa_handle, buf, samples); + // debug(3, "write %d frames.", samples); + ret = alsa_pcm_write(alsa_handle, buf, samples); + if (ret == samples) { stall_monitor_frame_count += samples; - if (err < 0) { - frame_index = 0; - measurement_data_is_valid = 0; - debug(1, "Error %d writing %d samples in play(): \"%s\".", err, samples, - snd_strerror(err)); - err = snd_pcm_recover(alsa_handle, err, 1); - if (err < 0) - warn("Error %d -- could not clear an error after attempting to write %d samples in " - "play(): \"%s\".", - err, samples, snd_strerror(err)); - } - if (frame_index == 0) { frames_sent_for_playing = samples; } else { frames_sent_for_playing += samples; } + const uint64_t start_measurement_from_this_frame = - (2 * 44100) / 352; // two seconds of frames… + (2 * config.output_rate) / 352; // two seconds of frames + frame_index++; - if ((frame_index == start_measurement_from_this_frame) || (frame_index % 32 == 0)) { - long fl = 0; - err2 = snd_pcm_delay(alsa_handle, &fl); - if (err2 != 0) { - debug(1, "Error %d in delay in play(): \"%s\". Delay reported is %d frames.", err2, - snd_strerror(err2), fl); - err2 = snd_pcm_recover(alsa_handle, err2, 1); - if (err2 < 0) - warn("Error %d -- could not clear an error after checking delay in play(): \"%s\".", - err2, snd_strerror(err2)); - frame_index = 0; - measurement_data_is_valid = 0; - } else { - if (fl == 0) { - // there's nothing in the pipeline, so we can't measure frame rate. - frame_index = 0; // we'll be starting over... - measurement_data_is_valid = 0; - } - } + + 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 - fl; + 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; @@ -1235,74 +1299,153 @@ int untimed_play(void *buf, int samples) { measurement_data_is_valid = 1; } } + } else { + + debug(1, "alsa: error %d writing %d samples to alsa device.", ret, samples); + frame_index = 0; + measurement_data_is_valid = 0; + if (ret == -EPIPE) { /* underrun */ + 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 */ + 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 { - debug(1, "Error -- ALSA device in incorrect state (%d) for play.", - snd_pcm_state(alsa_handle)); - if ((err = snd_pcm_prepare(alsa_handle))) { - debug(1, "Error preparing after play error: \"%s\".", snd_strerror(err)); - err2 = snd_pcm_recover(alsa_handle, err, 1); - if (err2 < 0) - warn("Error %d -- could not clear an error after reporting ALSA device in incorrect " - "state for play: \"%s\".", - err2, snd_strerror(err2)); - } - frame_index = 0; - measurement_data_is_valid = 0; } - debug_mutex_unlock(&alsa_mutex, 0); - pthread_cleanup_pop(0); // release the mutex + } 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 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(); + 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. + } + } else { + debug(1, "alsa: do_open() -- output device already open."); + } + return ret; +} + +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, 1); + + if (alsa_backend_state == abm_disconnected) { + ret = do_open(); + 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; +} + static void flush(void) { // debug(2,"audio_alsa flush called."); - int oldState; - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1); - int derr; - do_mute(1); - - if (alsa_handle) { - stall_monitor_start_time = 0; - if (config.keep_dac_busy == 0) { - 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 - if ((derr = snd_pcm_close(alsa_handle))) - debug(1, "Error %d (\"%s\") closing the output device.", derr, snd_strerror(derr)); - alsa_handle = NULL; + // 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."); } - frame_index = 0; - measurement_data_is_valid = 0; - } + } else + debug(3, "alsa: flush() -- called on a disconnected alsa backend"); debug_mutex_unlock(&alsa_mutex, 3); pthread_cleanup_pop(0); // release the mutex - pthread_setcancelstate(oldState, NULL); -} - -static int play(void *buf, int samples) { - uint64_t time_now = - get_absolute_time_in_fp(); // this is to regulate access by the silence filler thread - uint64_t sample_duration = ((uint64_t)samples) << 32; - sample_duration = sample_duration / desired_sample_rate; - most_recent_write_time = time_now + sample_duration; - return untimed_play(buf, samples); } 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) { @@ -1310,11 +1453,13 @@ 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; + pthread_cleanup_debug_mutex_lock(&alsa_mixer_mutex, 1000, 1); if (volume_set_request && (open_mixer() == 1)) { if (has_softvol) { if (ctl && elem_id) { @@ -1344,15 +1489,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) { - pthread_cleanup_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); - pthread_cleanup_pop(0); // release the mutex } /* @@ -1362,7 +1506,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) @@ -1373,154 +1518,111 @@ static void linear_volume(double vol) { } */ -static void mute(int mute_state_requested) { - // debug(1,"External Mute Request: %d",mute_state_requested); - pthread_cleanup_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); - pthread_cleanup_pop(0); // release the mutex +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 do_mute(int mute_state_requested) { - debug(3, "Setting mute to %d.", mute_state_requested); - int oldState; - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable - - // 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() == 1) { - 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); - } - } 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); - } - } - close_mixer(); - } - } - mute_request_pending = 0; - pthread_setcancelstate(oldState, NULL); -} - -void alsa_buffer_monitor_thread_cleanup_function(__attribute__((unused)) void *arg) { +/* +void alsa_buffer_monitor_thread_cleanup_function(__attribute__((unused)) void +*arg) { debug(1, "alsa: alsa_buffer_monitor_thread_cleanup_function called."); } +*/ -void *alsa_buffer_monitor_thread_code(void *arg) { - // debug(1,"alsa: alsa_buffer_monitor_thread_code called."); - int oldState; - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable - int ret = 0; - if (alsa_handle == NULL) { - - pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1); - ret = actual_open_alsa_device(); - if (ret == 0) { - if (audio_alsa.volume) - do_volume(set_volume); - if (audio_alsa.mute) - do_mute(0); +void *alsa_buffer_monitor_thread_code(__attribute__((unused)) void *arg) { + int okb = -1; + while (1) { + if (okb != config.keep_dac_busy) { + debug(1,"keep_dac_busy is now %d",config.keep_dac_busy); + okb = config.keep_dac_busy; } - - debug_mutex_unlock(&alsa_mutex, 3); - pthread_cleanup_pop(0); // release the mutex - } - pthread_cleanup_push(alsa_buffer_monitor_thread_cleanup_function, arg); - pthread_setcancelstate(oldState, NULL); - - // The thinking is, if the device has a hardware mixer, then - // (if no source transformation is happening), Shairport Sync - // will deliver fill-in silences and the audio material without adding dither. - // So don't insert dither into the silences sent to keep the DAC busy. - - // Also, if the ignore_volume_setting is set, the audio is sent through unaltered, - // so, in that circumstance, don't add dither either. - - int use_dither = 0; - if ((hardware_mixer == 0) && (config.ignore_volume_control == 0) && - (config.airplay_volume != 0.0)) - use_dither = 1; - - debug(1, "alsa: dither will %sbe added to inter-session silence.", use_dither ? "" : "not "); - - int sleep_time_ms = 30; - 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 = (desired_sample_rate * sleep_time_ms * 2) / 1000; - size_t size_of_silence_buffer = frames_of_silence * frame_size; - // debug(1,"Silence buffer length: %u bytes.",size_of_silence_buffer); - void *silence = malloc(size_of_silence_buffer); - if (silence == NULL) { - debug(1, "alsa: failed to allocate memory for a silent frame buffer, thus " - "disable_standby_mode is \"off\"."); - } else { - long buffer_size; - int reply; - while (1) { - if (config.keep_dac_busy != 0) { - uint64_t present_time = get_absolute_time_in_fp(); - - if ((most_recent_write_time == 0) || - ((present_time > most_recent_write_time) && - ((present_time - most_recent_write_time) > (sleep_time_in_fp)))) { - reply = delay(&buffer_size); - 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); - } - if (buffer_size < frames_of_silence) { + 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() == 0) + 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 + if ((alsa_backend_state != abm_disconnected) && (config.keep_dac_busy != 0)) { + 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); + if (reply != 0) { + buffer_size = 0; + char errorstring[1024]; + strerror_r(-reply, (char *)errorstring, sizeof(errorstring)); + debug(1, "alsa: alsa_buffer_monitor_thread_code delay error %d: \"%s\".", reply, + (char *)errorstring); + } + long buffer_size_threshold = + (long)(config.audio_backend_silence_threshold * desired_sample_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 = + // (desired_sample_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; - else - use_dither = 0; - 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); - untimed_play(silence, frames_of_silence); + 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); } } } - usleep(sleep_time_ms * 1000); // has a cancellation point in it - // pthread_testcancel(); } + debug_mutex_unlock(&alsa_mutex, 0); + pthread_cleanup_pop(0); // release the mutex + usleep(sleep_time_ms * 1000); // has a cancellation point in it } - pthread_cleanup_pop(1); pthread_exit(NULL); } diff --git a/audio_dummy.c b/audio_dummy.c index d8dea87f..88c63c94 100644 --- a/audio_dummy.c +++ b/audio_dummy.c @@ -54,10 +54,9 @@ static int play(__attribute__((unused)) void *buf, __attribute__((unused)) int s 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, .start = &start, diff --git a/audio_jack.c b/audio_jack.c index 2f79bee1..c8a8b9ee 100644 --- a/audio_jack.c +++ b/audio_jack.c @@ -27,7 +27,6 @@ #include #include #include -#include #include #include @@ -301,6 +300,9 @@ void *open_client_if_necessary_thread_function(void *arg) { int jack_init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) { config.audio_backend_latency_offset = 0; config.audio_backend_buffer_desired_length = 0.500; + config.audio_backend_buffer_interpolation_threshold_in_seconds = + 0.25; // below this, soxr interpolation will not occur -- it'll be basic interpolation + // instead. config.jack_auto_client_open_interval = 1; // check every second // get settings from settings file first, allow them to be overridden by @@ -332,8 +334,9 @@ int jack_init(__attribute__((unused)) int argc, __attribute__((unused)) char **a * we should try. */ if (config_lookup_int(config.cfg, "jack.auto_client_open_interval", &value)) { if ((value < 0) || (value > 300)) - debug(1, "Invalid jack auto_client_open_interval \"%sd\". It should be between 0 and 300, " - "default is %d.", + debug(1, + "Invalid jack auto_client_open_interval \"%sd\". It should be between 0 and 300, " + "default is %d.", value, config.jack_auto_client_open_interval); else config.jack_auto_client_open_interval = value; @@ -430,4 +433,4 @@ void jack_stop(void) { // debug(1, "jack stop"); if (config.jack_auto_client_disconnect) jack_close(); -} \ No newline at end of file +} diff --git a/audio_pa.c b/audio_pa.c index ca79b4dc..8dd38b8b 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 @@ -296,10 +300,8 @@ 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, .start = &start, diff --git a/audio_pipe.c b/audio_pipe.c index 89a09a0c..e97f6034 100644 --- a/audio_pipe.c +++ b/audio_pipe.c @@ -127,7 +127,7 @@ 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, diff --git a/audio_sndio.c b/audio_sndio.c index d8037edb..f22ed5af 100644 --- a/audio_sndio.c +++ b/audio_sndio.c @@ -99,6 +99,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 diff --git a/audio_soundio.c b/audio_soundio.c index 7d3b48ad..f57f9933 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) { diff --git a/audio_stdout.c b/audio_stdout.c index 0438b1af..ea48e73d 100644 --- a/audio_stdout.c +++ b/audio_stdout.c @@ -74,10 +74,8 @@ 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, .start = &start, diff --git a/common.c b/common.c index aa38007f..d8be92a4 100644 --- a/common.c +++ b/common.c @@ -2,7 +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 (c) Mike Brady 2014 -- 2018 + * Further changes and additions (c) Mike Brady 2014 -- 2019 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -75,10 +75,13 @@ #include #include #include - #endif +#ifdef CONFIG_LIBDAEMON #include +#else +#include +#endif #ifdef CONFIG_ALSA void set_alsa_out_dev(char *); @@ -114,7 +117,8 @@ 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; + 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; @@ -142,13 +146,24 @@ 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); + + #ifdef CONFIG_LIBDAEMON + daemon_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); + daemon_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); + daemon_log(LOG_ERR, "% 20.9f|*fatal error: %s", tss, s); else - daemon_log(LOG_EMERG, "fatal error: %s", s); + daemon_log(LOG_ERR, "fatal error: %s", s); + #else + syslog(LOG_ERR, "|% 20.9f|% 20.9f|*fatal error: %s", tss, tsl, s); + else if ((debuglev) && (config.debugger_show_relative_time)) + syslog(LOG_ERR, "% 20.9f|*fatal error: %s", tsl, s); + else if ((debuglev) && (config.debugger_show_elapsed_time)) + syslog(LOG_ERR, "% 20.9f|*fatal error: %s", tss, s); + else + syslog(LOG_ERR, "fatal error: %s", s); + #endif pthread_setcancelstate(oldState, NULL); exit(1); } @@ -169,7 +184,7 @@ void warn(const char *format, ...) { va_start(args, format); vsnprintf(s, sizeof(s), format, args); va_end(args); - +#ifdef CONFIG_LIBDAEMON 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); else if ((debuglev) && (config.debugger_show_relative_time)) @@ -178,6 +193,16 @@ void warn(const char *format, ...) { daemon_log(LOG_WARNING, "% 20.9f|*warning: %s", tss, s); else daemon_log(LOG_WARNING, "%s", s); +#else + if ((debuglev) && (config.debugger_show_elapsed_time) && (config.debugger_show_relative_time)) + syslog(LOG_WARNING, "|% 20.9f|% 20.9f|*warning: %s", tss, tsl, s); + else if ((debuglev) && (config.debugger_show_relative_time)) + syslog(LOG_WARNING, "% 20.9f|*warning: %s", tsl, s); + else if ((debuglev) && (config.debugger_show_elapsed_time)) + syslog(LOG_WARNING, "% 20.9f|*warning: %s", tss, s); + else + syslog(LOG_WARNING, "%s", s); +#endif pthread_setcancelstate(oldState, NULL); } @@ -199,6 +224,7 @@ void debug(int level, const char *format, ...) { va_start(args, format); vsnprintf(s, sizeof(s), format, args); va_end(args); +#ifdef CONFIG_LIBDAEMON if ((config.debugger_show_elapsed_time) && (config.debugger_show_relative_time)) daemon_log(LOG_DEBUG, "|% 20.9f|% 20.9f|%s", tss, tsl, s); else if (config.debugger_show_relative_time) @@ -207,6 +233,16 @@ void debug(int level, const char *format, ...) { daemon_log(LOG_DEBUG, "% 20.9f|%s", tss, s); else daemon_log(LOG_DEBUG, "%s", s); +#else + if ((config.debugger_show_elapsed_time) && (config.debugger_show_relative_time)) + syslog(LOG_DEBUG, "|% 20.9f|% 20.9f|%s", tss, tsl, s); + else if (config.debugger_show_relative_time) + syslog(LOG_DEBUG, "% 20.9f|%s", tsl, s); + else if (config.debugger_show_elapsed_time) + syslog(LOG_DEBUG, "% 20.9f|%s", tss, s); + else + syslog(LOG_DEBUG, "%s", s); +#endif pthread_setcancelstate(oldState, NULL); } @@ -226,7 +262,7 @@ void inform(const char *format, ...) { va_start(args, format); vsnprintf(s, sizeof(s), format, args); va_end(args); - +#ifdef CONFIG_LIBDAEMON if ((debuglev) && (config.debugger_show_elapsed_time) && (config.debugger_show_relative_time)) daemon_log(LOG_INFO, "|% 20.9f|% 20.9f|%s", tss, tsl, s); else if ((debuglev) && (config.debugger_show_relative_time)) @@ -235,6 +271,17 @@ void inform(const char *format, ...) { daemon_log(LOG_INFO, "% 20.9f|%s", tss, s); else daemon_log(LOG_INFO, "%s", s); +#else + if ((debuglev) && (config.debugger_show_elapsed_time) && (config.debugger_show_relative_time)) + syslog(LOG_INFO, "|% 20.9f|% 20.9f|%s", tss, tsl, s); + else if ((debuglev) && (config.debugger_show_relative_time)) + syslog(LOG_INFO, "% 20.9f|%s", tsl, s); + else if ((debuglev) && (config.debugger_show_elapsed_time)) + syslog(LOG_INFO, "% 20.9f|%s", tss, s); + else + syslog(LOG_INFO, "%s", s); + +#endif pthread_setcancelstate(oldState, NULL); } @@ -394,6 +441,8 @@ uint8_t *base64_dec(char *input, int *outlen) { #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()); @@ -412,12 +461,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); @@ -438,10 +490,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 @@ -473,6 +525,8 @@ static char super_secret_key[] = #ifdef CONFIG_OPENSSL uint8_t *rsa_apply(uint8_t *input, int inlen, int *outlen, int mode) { + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); RSA *rsa = NULL; if (!rsa) { BIO *bmem = BIO_new_mem_buf(super_secret_key, -1); @@ -491,6 +545,8 @@ 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 @@ -716,7 +772,8 @@ void command_start(void) { 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); } @@ -739,7 +796,7 @@ void command_start(void) { } } } -void command_execute(const char *command, const char *extra_argument) { +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]; @@ -767,10 +824,11 @@ void command_execute(const char *command, const char *extra_argument) { exit(127); /* 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) { + 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); } @@ -782,36 +840,8 @@ void command_execute(const char *command, const char *extra_argument) { void command_stop(void) { // this has a cancellation point if waiting is enabled - 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) != - 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"); - else { - // debug(1,"Executing on-stop command %s",config.cmd_stop); - 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 */ - } - } else { - if (config.cmd_blocking) { /* 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); - } - } - // debug(1,"Continue after on-stop command"); - } - } + 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 @@ -846,17 +876,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 @@ -1160,15 +1190,18 @@ 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 ((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); @@ -1256,6 +1289,11 @@ int _debug_mutex_unlock(pthread_mutex_t *mutex, const char *mutexname, const cha 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() { diff --git a/common.h b/common.h index 8d028b3b..6fdf38c1 100644 --- a/common.h +++ b/common.h @@ -30,7 +30,8 @@ enum dbus_session_type { } dbt_type; #endif -#define sps_extra_errno_output_stalled 32768 +#define sps_extra_code_output_stalled 32768 +#define sps_extra_code_output_state_cannot_make_ready 32769 enum endian_type { SS_LITTLE_ENDIAN = 0, @@ -61,6 +62,12 @@ enum decoders_supported_type { decoder_apple_alac, } decoders_supported_type; +enum disable_standby_mode_type { + disable_standby_off = 0, + disable_standby_while_active, + 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 @@ -140,10 +147,13 @@ typedef struct { 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 @@ -151,13 +161,13 @@ typedef struct { int statistics_requested, use_negotiated_latencies; enum playback_mode_type playback_mode; 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 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; @@ -167,11 +177,21 @@ 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. + double active_mode_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 sps_format_t output_format; enum volume_control_profile_type volume_control_profile; int output_rate; @@ -187,6 +207,7 @@ typedef struct { float loudness_reference_volume_db; int alsa_use_hardware_mute; double alsa_maximum_stall_time; + enum disable_standby_mode_type disable_standby_mode; volatile int keep_dac_busy; #if defined(CONFIG_DBUS_INTERFACE) @@ -302,13 +323,12 @@ 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); +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; @@ -341,4 +361,6 @@ void sps_nanosleep(const time_t sec, 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 578dd4b5..f8c72f71 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [3.3d41], [mikebrady@eircom.net]) +AC_INIT([shairport-sync], [3.3d52], [mikebrady@eircom.net]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) @@ -44,14 +44,35 @@ AM_CONDITIONAL([BUILD_FOR_OPENBSD], [test "x${with_os}" = xopenbsd ]) 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 +# Look for libdaemon +AC_ARG_WITH(libdaemon, [ --with-libdaemon = include support for daemonising in older non-systemd systems], + if test "x${with_libdaemon}" = xyes ; then + [AC_MSG_RESULT(>>Including libdaemon support for older systems) + AC_DEFINE([CONFIG_LIBDAEMON], 1, [Include libdaemon]) + REQUESTED_LIBDAEMON=1 + 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, + #default is to include libdaemon + [AC_MSG_RESULT(>>Including libdaemon support for older systems) + AC_DEFINE([CONFIG_LIBDAEMON], 1, [Include libdaemon]) + REQUESTED_LIBDAEMON=1 + 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 + ] + ) +AM_CONDITIONAL([USE_LIBDAEMON], [test "x$REQUESTED_LIBDAEMON" = "x1"]) ##### 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. @@ -73,7 +94,6 @@ AC_ARG_WITH([dummy],[ --with-dummy = include the dummy audio back end ],[AC_MSG 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 +115,9 @@ 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([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( @@ -175,14 +198,15 @@ AC_ARG_WITH(soxr, [ --with-soxr = choose libsoxr for high-quality interpolation AC_DEFINE([CONFIG_SOXR], 1, [Include support for using the SoX Resampler library for interpolation]) PKG_CHECK_MODULES( [SOXR], [soxr], - [LIBS="${SOXR_LIBS} ${LIBS}"]) + [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 requested but libsoxr not found!), [-lavutil]) + 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 requested but libsoxr not found!)) + AC_CHECK_LIB([soxr],[soxr_create], , AC_MSG_ERROR(soxr support requires the libsoxr library -- libsoxr-dev suggested!)) fi fi ], ) diff --git a/dacp.c b/dacp.c index 04821650..4f40e2d0 100644 --- a/dacp.c +++ b/dacp.c @@ -58,6 +58,7 @@ 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; @@ -111,7 +112,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; @@ -127,9 +131,9 @@ void addrinfo_cleanup(void *arg) { } void mutex_lock_cleanup(void *arg) { - // debug(1, "mutex lock cleanup called."); pthread_mutex_t *m = (pthread_mutex_t *)arg; - pthread_mutex_unlock(m); + if (pthread_mutex_unlock(m)) + debug(1, "Error releasing mutex."); } void connect_cleanup(void *arg) { @@ -144,11 +148,6 @@ void http_cleanup(void *arg) { http_free(rt); } -void malloc_cleanup(void *arg) { - // debug(1, "malloc cleanup called."); - free(arg); -} - 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. @@ -193,6 +192,7 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) { // debug(1,"Error %d \"%s\" at getaddrinfo.",ires,gai_strerror(ires)); response.code = 498; // Bad Address information for the DACP server } else { + uint64_t start_time = get_absolute_time_in_fp(); pthread_cleanup_push(addrinfo_cleanup, (void *)&res); // only do this one at a time -- not sure it is necessary, but better safe than sorry @@ -200,7 +200,6 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) { // int mutex_reply = pthread_mutex_lock(&dacp_conversation_lock); if (mutex_reply == 0) { pthread_cleanup_push(mutex_lock_cleanup, (void *)&dacp_conversation_lock); - // debug(1,"dacp_conversation_lock acquired for command \"%s\".",command); // make a socket: sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); @@ -213,8 +212,8 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) { // 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, "dacp_send_command: error %d setting receive timeout.", errno); if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof tv) == -1) @@ -223,7 +222,7 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) { // connect! // debug(1, "DACP socket created."); if (connect(sockfd, res->ai_addr, res->ai_addrlen) < 0) { - // debug(2, "dacp_send_command: 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."); @@ -233,9 +232,21 @@ 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(3,"dacp_send_command: connect message: \"%s\".",message); - if (send(sockfd, message, strlen(message), 0) != (ssize_t)strlen(message)) { - debug(1, "dacp_send_command: 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 { @@ -256,11 +267,23 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) { const char *data = buffer; if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) == -1) debug(1, "dacp_send_command: error %d setting receive timeout.", errno); - int ndata = recv(sockfd, buffer, sizeof(buffer), 0); + 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; @@ -300,14 +323,19 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) { // debug(1,"Sent command\"%s\" with a response body of size %d.",command,response.size); // debug(1,"dacp_conversation_lock released."); } else { - 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?", + 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; @@ -395,8 +423,9 @@ void set_dacp_server_information(rtsp_conn_info *conn) { void dacp_monitor_port_update_callback(char *dacp_id, uint16_t port) { 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.", + 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; @@ -444,8 +473,9 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) { (metadata_store.advanced_dacp_server_active != 0); metadata_store.dacp_server_active = 0; metadata_store.advanced_dacp_server_active = 0; - debug(2, "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) { @@ -829,6 +859,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) @@ -857,14 +889,18 @@ void dacp_monitor_start() { 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() { - debug(1, "dacp_monitor_stop"); - pthread_cancel(dacp_monitor_thread); - pthread_join(dacp_monitor_thread, NULL); - pthread_mutex_destroy(&dacp_server_information_lock); - pthread_mutex_destroy(&dacp_conversation_lock); + if (dacp_monitor_initialised) { // only if it's been started and initialised + debug(1, "dacp_monitor_stop"); + pthread_cancel(dacp_monitor_thread); + pthread_join(dacp_monitor_thread, NULL); + pthread_mutex_destroy(&dacp_server_information_lock); + debug(1, "DACP Conversation Lock Mutex Destroyed"); + pthread_mutex_destroy(&dacp_conversation_lock); + } } uint32_t dacp_tlv_crawl(char **p, int32_t *length) { diff --git a/dbus-service.c b/dbus-service.c index c163ddb9..8656d919 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -15,6 +15,8 @@ #include "dbus-service.h" +int service_is_running = 0; + ShairportSyncDiagnostics *shairportSyncDiagnosticsSkeleton = NULL; ShairportSyncRemoteControl *shairportSyncRemoteControlSkeleton = NULL; ShairportSyncAdvancedRemoteControl *shairportSyncAdvancedRemoteControlSkeleton = NULL; @@ -378,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; @@ -395,10 +410,10 @@ 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; @@ -408,15 +423,45 @@ 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.", dt); + debug(1, ">> setting drift tolerance to %f seconds", dt); config.tolerance = dt; } else { - debug(1, "Invalid drift tolerance: %f. Ignored.", dt); + 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, "while active") == 0) + config.disable_standby_mode = disable_standby_while_active; + 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_while_active: + shairport_sync_set_disable_standby_mode(skeleton, "while active"); + break; + default: + break; + } + } + return TRUE; +} + gboolean notify_alacdecoder_callback(ShairportSync *skeleton, __attribute__((unused)) gpointer user_data) { char *th = (char *)shairport_sync_get_alacdecoder(skeleton); @@ -609,8 +654,12 @@ 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", @@ -691,6 +740,27 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name #endif + 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_while_active: + shairport_sync_set_disable_standby_mode(SHAIRPORT_SYNC(shairportSyncSkeleton), "while active"); + debug(1, ">> disable standby mode set to \"while active\""); + 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"); @@ -710,6 +780,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 { @@ -767,6 +843,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, @@ -813,4 +890,9 @@ void stop_dbus_service() { 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 a00d06e0..5cfc02e2 100644 --- a/dbus-service.h +++ b/dbus-service.h @@ -7,5 +7,6 @@ ShairportSync *shairportSyncSkeleton; int start_dbus_service(); void stop_dbus_service(); +int dbus_service_is_running(); #endif /* #ifndef DBUS_SERVICE_H */ diff --git a/mdns.c b/mdns.c index de703975..8550f520 100644 --- a/mdns.c +++ b/mdns.c @@ -34,7 +34,6 @@ #ifdef CONFIG_AVAHI extern mdns_backend mdns_avahi; -extern mdns_backend mdns_external_avahi; #endif #ifdef CONFIG_DNS_SD extern mdns_backend mdns_dns_sd; @@ -44,10 +43,13 @@ extern mdns_backend mdns_external_dns_sd; extern mdns_backend mdns_tinysvcmdns; #endif +#ifdef CONFIG_EXTERNAL_MDNS +extern mdns_backend mdns_external_avahi; +#endif + static mdns_backend *mdns_backends[] = { #ifdef CONFIG_AVAHI &mdns_avahi, - &mdns_external_avahi, #endif #ifdef CONFIG_DNS_SD &mdns_dns_sd, @@ -55,6 +57,9 @@ static mdns_backend *mdns_backends[] = { #endif #ifdef CONFIG_TINYSVCMDNS &mdns_tinysvcmdns, +#endif +#ifdef CONFIG_EXTERNAL_MDNS + &mdns_external_avahi, #endif NULL}; diff --git a/mdns_dns_sd.c b/mdns_dns_sd.c index 95bae443..cf178f1d 100644 --- a/mdns_dns_sd.c +++ b/mdns_dns_sd.c @@ -24,8 +24,8 @@ * OTHER DEALINGS IN THE SOFTWARE. */ -#include "mdns.h" #include "common.h" +#include "mdns.h" #include #include #include diff --git a/mdns_external.c b/mdns_external.c index d3879ff5..8c0ab262 100644 --- a/mdns_external.c +++ b/mdns_external.c @@ -24,8 +24,8 @@ * OTHER DEALINGS IN THE SOFTWARE. */ -#include "mdns.h" #include "common.h" +#include "mdns.h" #include #include #include diff --git a/metadata_hub.c b/metadata_hub.c index 6938de5f..f116f272 100644 --- a/metadata_hub.c +++ b/metadata_hub.c @@ -46,6 +46,7 @@ #include "metadata_hub.h" #ifdef CONFIG_MBEDTLS +#include #include #endif @@ -57,6 +58,8 @@ #include #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 @@ -99,18 +102,21 @@ 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) { - debug(1, "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; + if (metadata_hub_initialised) { + debug(1, "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; + } } } @@ -217,9 +223,9 @@ 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 CONFIG_SSL +#ifdef CONFIG_OPENSSL MD5_CTX ctx; MD5_Init(&ctx); MD5_Update(&ctx, buf, len); @@ -227,10 +233,17 @@ char *metadata_write_image_file(const char *buf, int len) { #endif #ifdef CONFIG_MBEDTLS - mbedtls_md5_context tctx; - mbedtls_md5_starts(&tctx); - mbedtls_md5_update(&tctx, (const unsigned char *)buf, len); - mbedtls_md5_finish(&tctx, img_md5); + #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 CONFIG_POLARSSL @@ -489,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; @@ -559,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 65e93a23..9406851f 100644 --- a/metadata_hub.h +++ b/metadata_hub.h @@ -12,6 +12,11 @@ enum play_status_type { PS_PLAYING, } play_status_type; +enum active_mode_type { + AM_INACTIVE = 0, + AM_ACTIVE, +} active_mode_type; + enum shuffle_status_type { SS_NOT_AVAILABLE = 0, SS_OFF, @@ -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_mode_type active_state; int speaker_volume; // this is the actual speaker volume, allowing for the main volume and the // speaker volume control diff --git a/org.gnome.ShairportSync.xml b/org.gnome.ShairportSync.xml index 44d90a27..cea574bc 100644 --- a/org.gnome.ShairportSync.xml +++ b/org.gnome.ShairportSync.xml @@ -2,6 +2,9 @@ + + + diff --git a/player.c b/player.c index 1ce5494a..72a79ef8 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 @@ -89,13 +89,15 @@ #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) @@ -473,7 +475,7 @@ void player_put_packet(seq_t seqno, uint32_t actual_timestamp, uint8_t *data, in abuf_t *abuf = 0; if (!conn->ab_synced) { - // if this is the first packetÉ + // if this is the first packet... debug(3, "syncing to seqno %u.", seqno); conn->ab_write = seqno; conn->ab_read = seqno; @@ -495,7 +497,7 @@ void player_put_packet(seq_t seqno, uint32_t actual_timestamp, uint8_t *data, in } if (conn->ab_write == - seqno) { // if this is the expected packet (which could be the first packetÉ) + 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)) { @@ -624,6 +626,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; @@ -846,12 +858,11 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { 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; - } - 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)) { + } 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; } @@ -1017,7 +1028,8 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { // 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.", + 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(); @@ -1084,11 +1096,11 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { 1; // even if we haven't sent silence because it's zero frames long... } } else { - if ((resp == sps_extra_errno_output_stalled) && + 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"); + command_execute(config.cmd_unfixable, "output_device_stalled", 1); } else { warn( "an unrecoverable error, \"output_device_stalled\", has been detected.", @@ -1197,8 +1209,8 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { 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; @@ -1417,6 +1429,9 @@ void player_thread_cleanup_handler(void *arg) { debug(3, "Connection %d: player thread main loop exit via player_thread_cleanup_handler.", conn->connection_number); + if (config.output->stop) + config.output->stop(); + if (config.statistics_requested) { int rawSeconds = (int)difftime(time(NULL), conn->playstart); int elapsedHours = rawSeconds / 3600; @@ -1469,8 +1484,6 @@ void player_thread_cleanup_handler(void *arg) { } free_audio_buffers(conn); terminate_decoders(conn); - if (config.output->stop) - config.output->stop(); clear_reference_timestamp(conn); conn->rtp_running = 0; @@ -1550,6 +1563,11 @@ void *player_thread_func(void *arg) { debug(3, "Output frame bytes is %d.", conn->output_bytes_per_frame); + conn->dac_buffer_queue_minimum_length = (int64_t)( + 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 @@ -1642,6 +1660,7 @@ void *player_thread_func(void *arg) { (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 @@ -1811,8 +1830,9 @@ void *player_thread_func(void *arg) { } } else { - if ((config.output->parameters == NULL) || (conn->input_bit_depth > output_bit_depth) || - (config.playback_mode == ST_mono)) + 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; @@ -1973,8 +1993,8 @@ 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(2, "Underrun of %lld frames reported, but ignored.", current_delay); current_delay = @@ -1984,7 +2004,8 @@ void *player_thread_func(void *arg) { minimum_dac_queue_size = current_delay; // update for display later } } else { - if ((resp == sps_extra_errno_output_stalled) && + current_delay = 0; + if ((resp == sps_extra_code_output_stalled) && (conn->unfixable_error_reported == 0)) { conn->unfixable_error_reported = 1; if (config.cmd_unfixable) { @@ -1992,7 +2013,7 @@ void *player_thread_func(void *arg) { "stalled. Executing the " "\"run_this_if_an_unfixable_error_is_detected\" command.", conn->connection_number); - command_execute(config.cmd_unfixable, "output_device_stalled"); + command_execute(config.cmd_unfixable, "output_device_stalled", 1); } else { warn("Connection %d: An unfixable error has been detected -- output device is " "stalled. \"No " @@ -2001,11 +2022,11 @@ void *player_thread_func(void *arg) { conn->connection_number); } } else - debug(2, "Delay error %d when checking running latency.", resp); + 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); @@ -2075,8 +2096,14 @@ void *player_thread_func(void *arg) { 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; - do_flush(inframe->given_timestamp + frames_to_drop_sized, - conn); // this will reset_input_flow_metrics anyway + + 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(2, "Large negative sync error: %" PRId64 " with should_be_frame_32 of %" PRIu32 @@ -2212,7 +2239,7 @@ void *player_thread_func(void *arg) { } } - if ((current_delay < DAC_BUFFER_QUEUE_MINIMUM_LENGTH) || + if ((current_delay < conn->dac_buffer_queue_minimum_length) || (config.packet_stuffing == ST_basic)) { play_samples = stuff_buffer_basic_32((int32_t *)conn->tbuf, inbuflength, config.output_format, @@ -2247,8 +2274,13 @@ void *player_thread_func(void *arg) { else { if (play_samples == 0) debug(1, "play_samples==0 skipping it (1)."); - else + 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 @@ -2282,8 +2314,13 @@ void *player_thread_func(void *arg) { conn->outbuf, 0, conn->enable_dither, conn); if (conn->outbuf == NULL) debug(1, "NULL outbuf to play -- skipping it."); - else + 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 @@ -2504,236 +2541,216 @@ void *player_thread_func(void *arg) { 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 - // no cancellation points here if we assume that the mute call to the back end has no cancellation - // points + 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. + // 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 - // 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 - - if (config.output->parameters) { // no cancellation points in here + 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; - } 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."); - } - } - } - - 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; - } - 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; - } - } - - /* 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); + 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 value of volume_max_db is invalid. It must be in the range %d to - %d.",max_db,min_db); + warn("The maximum output level is outside the range of the hardware mixer -- ignored"); } } - */ - 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); + // 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); } } } 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); // 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, "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."); - 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); - } - } 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; + // 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.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); + // 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. - conn->fix_volume = temp_fix_volume; - memory_barrier(); // no cancellation points + if (config.ignore_volume_control == 0) { + if (airplay_volume == -144.0) { - if (config.loudness) - loudness_set_volume(software_attenuation / 100); + 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); + } - if (config.logOutputLevel) { - inform("Output Level set to: %.2f dB.", scaled_attenuation / 100.0); - } + } else { + 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; + } + 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"); + + // 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; + 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; + } + + 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; + } + + 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); + + conn->fix_volume = temp_fix_volume; + + 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 - // here, store the volume for possible use in the future + if (config.output->mute) + config.output->mute(0); + conn->software_mute_enabled = 0; + + debug(2, "player_volume_without_notification: volume mode is %d, airplay volume is %f, " + "software_attenuation: %f, hardware_attenuation: %f, muting " + "is disabled.", + 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) { @@ -2774,6 +2791,8 @@ 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(); pthread_t *pt = malloc(sizeof(pthread_t)); if (pt == NULL) @@ -2822,10 +2841,11 @@ int player_stop(rtsp_conn_info *conn) { #endif // debuglev = dl; command_stop(); + activity_monitor_signify_activity(0); // inactive, and should be after command_stop() return 0; } else { debug(3, "Connection %d: player thread already deleted.", conn->connection_number); // debuglev = dl; return -1; } -} \ No newline at end of file +} diff --git a/player.h b/player.h index 7dd1c186..28b0341b 100644 --- a/player.h +++ b/player.h @@ -68,14 +68,15 @@ typedef struct { } 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 - 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 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 @@ -132,7 +133,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 @@ -248,6 +249,7 @@ typedef struct { void *dapo_private_storage; // this is used for compatibility, if dacp stuff isn't enabled. int enable_dither; // needed for filling silences before play actually starts + int64_t dac_buffer_queue_minimum_length; } rtsp_conn_info; uint32_t modulo_32_offset(uint32_t from, uint32_t to); diff --git a/rtp.c b/rtp.c index 07f19169..a1ba6794 100644 --- a/rtp.c +++ b/rtp.c @@ -137,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; @@ -368,9 +369,10 @@ void *rtp_control_receiver(void *arg) { if (la != conn->latency) { conn->latency = la; - debug(3, "New latency detected: %" PRIu32 ", sync latency: %" PRIu32 - ", minimum latency: %" PRIu32 ", maximum " - "latency: %" PRIu32 ", fixed offset: %" PRIu32 ".", + debug(3, + "New latency detected: %" PRIu32 ", sync latency: %" PRIu32 + ", minimum latency: %" PRIu32 ", maximum " + "latency: %" PRIu32 ", fixed offset: %" PRIu32 ".", la, sync_rtp_timestamp - rtp_timestamp_less_latency, conn->minimum_latency, conn->maximum_latency, config.fixedLatencyOffset); } @@ -1098,7 +1100,7 @@ int sanitised_source_rate_information(uint32_t *frames, uint64_t *time, rtsp_con double calculated_frame_rate = ((1.0 * local_frames) / local_time) * one_fp; if (((calculated_frame_rate / conn->input_rate) > 1.002) || ((calculated_frame_rate / conn->input_rate) < 0.998)) { - debug(2, "input frame rate out of bounds at %.2f fps.", calculated_frame_rate); + debug(3, "input frame rate out of bounds at %.2f fps.", calculated_frame_rate); result = 1; } else { *frames = local_frames; @@ -1226,8 +1228,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/rtsp.c b/rtsp.c index bf0ad370..9aff0e65 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 @@ -51,6 +51,7 @@ #endif #ifdef CONFIG_MBEDTLS +#include #include #endif @@ -90,6 +91,8 @@ enum rtsp_read_request_response { // Mike Brady's part... +int metadata_running = 0; + // always lock use this when accessing the playing conn value static pthread_mutex_t playing_conn_lock = PTHREAD_MUTEX_INITIALIZER; @@ -119,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]; @@ -285,15 +291,16 @@ void *player_watchdog_thread_code(void *arg) { if (time_since_last_bark >= ct) { conn->watchdog_barks++; if (conn->watchdog_barks == 1) { - debug(1, "Connection %d: As Yeats almost said, \"Too long a silence / can make a stone " - "of the heart\".", + 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"); + 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); @@ -401,16 +408,17 @@ static char *nextline(char *in, int inbuf) { } 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"); + 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); } } @@ -419,9 +427,11 @@ rtsp_message *msg_init(void) { 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; } @@ -476,12 +486,11 @@ static void debug_print_msg_content(int level, rtsp_message *msg) { } */ -void msg_free(rtsp_message *msg) { - - if (msg) { - debug_mutex_lock(&reference_counter_lock, 1000, 0); +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--; - debug_mutex_unlock(&reference_counter_lock, 0); if (msg->referenceCount == 0) { unsigned int i; for (i = 0; i < msg->nheaders; i++) { @@ -490,14 +499,24 @@ 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); } int msg_handle_line(rtsp_message **pmsg, char *line) { @@ -550,18 +569,23 @@ int msg_handle_line(rtsp_message **pmsg, char *line) { } fail: - *pmsg = NULL; - msg_free(msg); + msg_free(pmsg); return 0; } enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_message **the_packet) { + + *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 - - rtsp_message *msg = NULL; - + 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; @@ -607,9 +631,9 @@ do { 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; @@ -674,7 +698,7 @@ do { 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; @@ -690,21 +714,18 @@ do { 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; } @@ -820,9 +841,10 @@ void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message * 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"); } void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, @@ -1034,8 +1056,8 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, } else #ifdef CONFIG_METADATA if (!strncmp(cp, "progress: ", strlen("progress: "))) { - char *progress = cp + strlen("volume: "); - // debug(2, "progress: \"%s\"\n",progress); // rtpstampstart/rtpstampnow/rtpstampend 44100 per + 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); @@ -1076,6 +1098,8 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, // '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 @@ -1396,7 +1420,7 @@ 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 + msg_free(&pack->carrier); // release the message else if (pack->data) free(pack->data); } @@ -1432,12 +1456,15 @@ void metadata_init(void) { 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) { - debug(1, "metadata_stop called."); - pthread_cancel(metadata_thread); - pthread_join(metadata_thread, NULL); + if (metadata_running) { + debug(1, "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, @@ -1474,14 +1501,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; } @@ -1548,7 +1578,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 @@ -1996,6 +2026,8 @@ static int rtsp_auth(char **nonce, rtsp_message *req, rtsp_message *resp) { #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); @@ -2008,9 +2040,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 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)); @@ -2024,6 +2072,7 @@ static int rtsp_auth(char **nonce, rtsp_message *req, rtsp_message *resp) { mbedtls_md5_update(&tctx, (unsigned char *)":", 1); mbedtls_md5_update(&tctx, (const unsigned char *)uri, strlen(uri)); mbedtls_md5_finish(&tctx, digest_mu); + #endif #endif #ifdef CONFIG_POLARSSL @@ -2048,6 +2097,7 @@ static int rtsp_auth(char **nonce, rtsp_message *req, rtsp_message *resp) { snprintf((char *)buf + 2 * i, 3, "%02x", digest_urp[i]); #ifdef CONFIG_OPENSSL + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); MD5_Init(&ctx); MD5_Update(&ctx, buf, 32); MD5_Update(&ctx, ":", 1); @@ -2057,9 +2107,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 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); @@ -2069,6 +2131,7 @@ static int rtsp_auth(char **nonce, rtsp_message *req, rtsp_message *resp) { snprintf((char *)buf + 2 * i, 3, "%02x", digest_mu[i]); mbedtls_md5_update(&tctx, buf, 32); mbedtls_md5_finish(&tctx, digest_total); + #endif #endif #ifdef CONFIG_POLARSSL @@ -2135,7 +2198,10 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { } // remove flow control and mutexes - int rc = pthread_cond_destroy(&conn->flowcontrol); + 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); @@ -2168,7 +2234,7 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { void msg_cleanup_function(void *arg) { // debug(3, "msg_cleanup_function called."); - msg_free((rtsp_message *)arg); + msg_free((rtsp_message **)arg); } static void *rtsp_conversation_thread_func(void *pconn) { @@ -2198,6 +2264,9 @@ static void *rtsp_conversation_thread_func(void *pconn) { 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); // nothing before this is cancellable pthread_cleanup_push(rtsp_conversation_thread_cleanup_function, (void *)conn); @@ -2214,16 +2283,17 @@ static void *rtsp_conversation_thread_func(void *pconn) { 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); + pthread_cleanup_push(msg_cleanup_function, (void *)&req); resp = msg_init(); - pthread_cleanup_push(msg_cleanup_function, (void *)resp); + 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, "Connection %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); apple_challenge(conn->fd, req, resp); @@ -2282,8 +2352,9 @@ static void *rtsp_conversation_thread_func(void *pconn) { if (conn->stop == 0) { int err = msg_write_response(conn->fd, resp); if (err) { - debug(1, "Connection %d: Unable to write an RTSP message response. Terminating the " - "connection.", + 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" @@ -2576,8 +2647,12 @@ void rtsp_listen_loop(void) { 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); diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 39db8077..4a6ebe29 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -29,6 +29,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_hw_priority = "yes"; // 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 attenuator first, then the 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". @@ -41,6 +42,7 @@ general = // 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_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,15 +50,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" mode starts when play begins and ends when the active_mode_timeout has elapsed after play ends, unless another play session starts before the timeout has fully elapsed. +// run_this_before_entering_active_mode = "/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_mode = "/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_mode_timeout = 10; // wait for this number of seconds after play ends before leaving the active mode, 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. }; diff --git a/shairport.c b/shairport.c index 65c0bd68..2bea884f 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 @@ -45,6 +45,7 @@ #ifdef CONFIG_MBEDTLS #include +#include #endif #ifdef CONFIG_POLARSSL @@ -59,8 +60,8 @@ #include #endif +#include "activity_monitor.h" #include "common.h" -#include "mdns.h" #include "rtp.h" #include "rtsp.h" @@ -84,17 +85,21 @@ #include "mpris-service.h" #endif +#ifdef CONFIG_LIBDAEMON #include #include #include #include #include +#else +#include +#endif #ifdef CONFIG_CONVOLUTION #include #endif -static int shutting_down = 0; +// static int shutting_down = 0; char configuration_file_path[4096 + 1]; char actual_configuration_file_path[4096 + 1]; @@ -103,19 +108,18 @@ static void sig_ignore(__attribute__((unused)) int foo, __attribute__((unused)) static void sig_shutdown(__attribute__((unused)) int foo, __attribute__((unused)) siginfo_t *bar, __attribute__((unused)) void *baz) { debug(1, "shutdown requested..."); - // daemon_log(LOG_NOTICE, "exit..."); +#ifdef CONFIG_LIBDAEMON daemon_retval_send(255); daemon_pid_file_remove(); +#endif exit(0); } 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!"); - } } } @@ -149,12 +153,12 @@ void usage(char *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"); +#endif 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"); printf(" -c, --configfile=FILE read configuration settings from FILE. Default is " "/etc/shairport-sync.conf.\n"); @@ -314,6 +318,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!"); @@ -322,6 +327,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; @@ -330,6 +336,10 @@ 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_mode_timeout = 10.0; + config.volume_range_hw_priority = + 1; // if combining software and hardware volume control, give the hardware priority +// i.e. when reducing volume, reduce the hw first before reducing the software. #ifdef CONFIG_METADATA_HUB config.cover_art_cache_dir = "/tmp/shairport-sync/.cache/coverart"; @@ -363,12 +373,15 @@ int parse_options(int argc, char **argv) { /* Read the file. If there is an error, report it and exit. */ if (config_read_file(&config_file_stuff, config_file_real_path)) { free(config_file_real_path); + config_set_auto_convert(&config_file_stuff, + 1); // allow autoconversion from int/float to int/float // make config.cfg point to it config.cfg = &config_file_stuff; /* Get the Service Name. */ if (config_lookup_string(config.cfg, "general.name", &str)) { raw_service_name = (char *)str; } +#ifdef CONFIG_LIBDAEMON int daemonisewithout = 0; int daemonisewith = 0; /* Get the Daemonize setting. */ @@ -389,7 +402,7 @@ int parse_options(int argc, char **argv) { /* Get the directory path for the pid file created when the program is daemonised. */ if (config_lookup_string(config.cfg, "sessioncontrol.daemon_pid_dir", &str)) config.piddir = (char *)str; - +#endif /* Get the mdns_backend setting. */ if (config_lookup_string(config.cfg, "general.mdns_backend", &str)) config.mdns_name = (char *)str; @@ -605,6 +618,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 */ @@ -721,6 +737,25 @@ int parse_options(int argc, char **argv) { config.cmd_stop = (char *)str; } + if (config_lookup_string(config.cfg, "sessioncontrol.run_this_before_entering_active_mode", + &str)) { + config.cmd_active_start = (char *)str; + } + + if (config_lookup_string(config.cfg, "sessioncontrol.run_this_after_exiting_active_mode", + &str)) { + config.cmd_active_stop = (char *)str; + } + + if (config_lookup_float(config.cfg, "sessioncontrol.active_mode_timeout", &dvalue)) { + if (dvalue < 0.0) + warn("Invalid value \"%f\" for sessioncontrol.active_mode_timeout. It must be positive. " + "The default of %f will be used instead.", + dvalue, config.active_mode_timeout); + else + config.active_mode_timeout = dvalue; + } + if (config_lookup_string(config.cfg, "sessioncontrol.run_this_if_an_unfixable_error_is_detected", &str)) { config.cmd_unfixable = (char *)str; @@ -1022,6 +1057,8 @@ int parse_options(int argc, char **argv) { } #endif +#ifdef CONFIG_LIBDAEMON + // now, check and calculate the pid directory #ifdef DEFINED_CUSTOM_PID_DIR char *use_this_pid_dir = PIDDIR; @@ -1033,12 +1070,12 @@ 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(CONFIG_DBUS_INTERFACE) || defined(CONFIG_MPRIS_INTERFACE) -GMainLoop *g_main_loop; +static GMainLoop *g_main_loop = NULL; pthread_t dbus_thread; void *dbus_thread_func(__attribute__((unused)) void *arg) { @@ -1089,14 +1126,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) { @@ -1105,29 +1135,10 @@ const char *pid_file_proc(void) { // debug(1,"pid_file_path_string \"%s\".",pid_file_path_string); return pid_file_path_string; } - -void exit_function() { - debug(1, "exit function called..."); - // cancel_all_RTSP_threads(); - 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); - if (config.computed_piddir) - free(config.computed_piddir); - if (ranarray) - free((void *)ranarray); - if (config.cfg) - config_destroy(config.cfg); - if (config.appName) - free(config.appName); - // probably should be freeing malloc'ed memory here, including strdup-created strings... -} +#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(1, "main cleanup handler called."); #ifdef CONFIG_MQTT if (config.mqtt_enabled) { @@ -1142,9 +1153,11 @@ void main_cleanup_handler(__attribute__((unused)) void *arg) { #ifdef CONFIG_DBUS_INTERFACE stop_dbus_service(); #endif - debug(1, "Stopping DBUS Loop Thread"); - g_main_loop_quit(g_main_loop); - pthread_join(dbus_thread, NULL); + if (g_main_loop) { + debug(1, "Stopping DBUS Loop Thread"); + g_main_loop_quit(g_main_loop); + pthread_join(dbus_thread, NULL); + } #endif #ifdef CONFIG_DACP_CLIENT @@ -1160,28 +1173,51 @@ void main_cleanup_handler(__attribute__((unused)) void *arg) { #ifdef CONFIG_METADATA metadata_stop(); // close down the metadata pipe #endif - if (config.output->deinit) { + + activity_monitor_stop(0); + + if ((config.output) && (config.output->deinit)) { debug(1, "Deinitialise the audio backend."); config.output->deinit(); } - daemon_log(LOG_NOTICE, "Unexpected exit..."); +#ifdef CONFIG_LIBDAEMON daemon_retval_send(0); daemon_pid_file_remove(); daemon_signal_done(); +#endif + debug(1, "Exit..."); exit(0); } +void exit_function() { + debug(1, "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) + free(config.appName); + // probably should be freeing malloc'ed memory here, including strdup-created strings... +} + int main(int argc, char **argv) { 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); @@ -1189,9 +1225,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]; @@ -1268,6 +1313,8 @@ int main(int argc, char **argv) { exit(1); } +#ifdef CONFIG_LIBDAEMON + pid_t pid; /* Reset signal handlers */ @@ -1287,44 +1334,22 @@ int main(int argc, char **argv) { 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); - } +#endif - /* 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); - } // parse arguments into config -- needed to locate pid_dir int audio_arg = parse_options(argc, argv); + // mDNS supports maximum of 63-character names (we append 13). + if (strlen(config.service_name) > 50) { + warn("Supplied name too long (max 50 characters)"); + config.service_name[50] = '\0'; // truncate it and carry on... + } + /* Check if we are called with -k or --kill parameter */ if (argc >= 2 && ((strcmp(argv[1], "-k") == 0) || (strcmp(argv[1], "--kill") == 0))) { + +#ifdef CONFIG_LIBDAEMON int ret; /* Kill daemon with SIGTERM */ @@ -1334,20 +1359,20 @@ int main(int argc, char **argv) { else daemon_pid_file_remove(); return ret < 0 ? 1 : 0; +#else + syslog(LOG_ERR, "shairport-sync was built without support for the -k or --kill option"); + 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) { @@ -1430,6 +1455,8 @@ int main(int argc, char **argv) { /* end libdaemon stuff */ } +#endif + main_thread_id = pthread_self(); if (!main_thread_id) debug(1, "Main thread is set up to be NULL!"); @@ -1501,17 +1528,23 @@ int main(int argc, char **argv) { "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_mode action is \"%s\".", config.cmd_active_start); + debug(1, "run_this_after_exiting_active_mode action is \"%s\".", config.cmd_active_stop); + debug(1, "active_mode_timeout is %f seconds.", config.active_mode_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); @@ -1525,6 +1558,12 @@ 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, + "combined attenuators (0 -- software is / 1 -- hardware is top of attenuation range) 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); @@ -1538,8 +1577,6 @@ int main(int argc, char **argv) { 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); @@ -1584,11 +1621,18 @@ int main(int argc, char **argv) { #endif #ifdef CONFIG_MBEDTLS +#if MBEDTLS_VERSION_MINOR >= 7 + mbedtls_md5_context tctx; + mbedtls_md5_starts_ret(&tctx); + mbedtls_md5_update_ret(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); + mbedtls_md5_finish_ret(&tctx, ap_md5); +#else mbedtls_md5_context tctx; mbedtls_md5_starts(&tctx); mbedtls_md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); mbedtls_md5_finish(&tctx, ap_md5); #endif +#endif #ifdef CONFIG_POLARSSL md5_context tctx; @@ -1629,6 +1673,8 @@ int main(int argc, char **argv) { } #endif + activity_monitor_start(); + // daemon_log(LOG_INFO, "Successful Startup"); rtsp_listen_loop(); diff --git a/tinysvcmdns.c b/tinysvcmdns.c index ebf6fea7..bdce032c 100644 --- a/tinysvcmdns.c +++ b/tinysvcmdns.c @@ -1713,7 +1713,8 @@ void mdnsd_stop(struct mdnsd *s) { assert(s != NULL); struct timeval tv = { - .tv_sec = 0, .tv_usec = 500 * 1000, + .tv_sec = 0, + .tv_usec = 500 * 1000, }; s->stop_flag = 1;