diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..60aac425 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + day: sunday + time: "10:00" + open-pull-requests-limit: 10 + target-branch: development diff --git a/.github/workflows/check_ap2_systemd_basic.yml b/.github/workflows/check_ap2_systemd_basic.yml index 3359bace..cb1ecdc8 100644 --- a/.github/workflows/check_ap2_systemd_basic.yml +++ b/.github/workflows/check_ap2_systemd_basic.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.5.2 - name: Install Dependencies run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev xxd - name: Configure diff --git a/.github/workflows/check_ap2_systemd_full.yml b/.github/workflows/check_ap2_systemd_full.yml index 858b2af5..94239fe0 100644 --- a/.github/workflows/check_ap2_systemd_full.yml +++ b/.github/workflows/check_ap2_systemd_full.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.5.2 - name: Install Dependencies run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libpulse-dev libsoundio-dev libsndio-dev libglib2.0-dev libmosquitto-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev xxd - name: Configure diff --git a/.github/workflows/check_ap2_systemd_full_build_folder.yml b/.github/workflows/check_ap2_systemd_full_build_folder.yml index f3b743b5..cad5bbe4 100644 --- a/.github/workflows/check_ap2_systemd_full_build_folder.yml +++ b/.github/workflows/check_ap2_systemd_full_build_folder.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.5.2 - name: Install Dependencies run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libpulse-dev libsoundio-dev libsndio-dev libglib2.0-dev libmosquitto-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev xxd - name: Configure diff --git a/.github/workflows/check_ap2_systemv_full.yml b/.github/workflows/check_ap2_systemv_full.yml index d3ff26f8..4e514430 100644 --- a/.github/workflows/check_ap2_systemv_full.yml +++ b/.github/workflows/check_ap2_systemv_full.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.5.2 - name: Install Dependencies run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libdaemon-dev libconfig-dev libasound2-dev libao-dev libjack-dev libpulse-dev libsoundio-dev libsndio-dev libglib2.0-dev libmosquitto-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev xxd - name: Configure diff --git a/.github/workflows/check_classic_mac_basic.yml b/.github/workflows/check_classic_mac_basic.yml index 2dc4e715..f53de4a2 100644 --- a/.github/workflows/check_classic_mac_basic.yml +++ b/.github/workflows/check_classic_mac_basic.yml @@ -2,7 +2,7 @@ name: Basic libao configuration for macOS with BREW -- classic only, because mac on: push: - branches: [ "development" ] + branches: [ "development", "danger" ] jobs: build: @@ -10,10 +10,9 @@ jobs: runs-on: macos-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.5.2 - name: Install Dependencies run: | - brew update brew install automake brew install openssl brew install popt diff --git a/.github/workflows/check_classic_systemd_basic.yml b/.github/workflows/check_classic_systemd_basic.yml index 5988e965..82525484 100644 --- a/.github/workflows/check_classic_systemd_basic.yml +++ b/.github/workflows/check_classic_systemd_basic.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.5.2 - name: Install Dependencies run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev - name: Configure diff --git a/.github/workflows/check_classic_systemd_full.yml b/.github/workflows/check_classic_systemd_full.yml index 06744a03..57bb7ac5 100644 --- a/.github/workflows/check_classic_systemd_full.yml +++ b/.github/workflows/check_classic_systemd_full.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3.5.2 - name: Install Dependencies run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libpulse-dev libsoundio-dev libsndio-dev libglib2.0-dev libmosquitto-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev - name: Configure diff --git a/.github/workflows/docker-build-on-push.yaml b/.github/workflows/docker-build-on-push.yaml index a5f1d9b3..79ac8eac 100644 --- a/.github/workflows/docker-build-on-push.yaml +++ b/.github/workflows/docker-build-on-push.yaml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3.5.2 with: fetch-depth: 0 @@ -40,20 +40,20 @@ jobs: echo "IMAGE_TAG_BASE=development" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2.5.0 - name: Login to Docker Registry - uses: docker/login-action@v1 + uses: docker/login-action@v2.1.0 with: registry: ${{ secrets.DOCKER_REGISTRY }} username: ${{ secrets.DOCKER_REGISTRY_USER }} password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} - name: Build and push (classic) - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4.0.0 with: context: ./ file: ./docker/classic/Dockerfile @@ -64,7 +64,7 @@ jobs: SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4.0.0 with: context: ./ file: ./docker/Dockerfile diff --git a/.github/workflows/docker-build-on-tag.yaml b/.github/workflows/docker-build-on-tag.yaml index 3ce06306..a83882bc 100644 --- a/.github/workflows/docker-build-on-tag.yaml +++ b/.github/workflows/docker-build-on-tag.yaml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3.5.2 with: fetch-depth: 0 @@ -46,20 +46,20 @@ jobs: echo "NQPTP_BRANCH=development" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v2.5.0 - name: Login to Docker Registry - uses: docker/login-action@v2 + uses: docker/login-action@v2.1.0 with: registry: ${{ secrets.DOCKER_REGISTRY }} username: ${{ secrets.DOCKER_REGISTRY_USER }} password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} - name: Build and push (classic) - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4.0.0 with: context: ./ file: ./docker/classic/Dockerfile @@ -72,7 +72,7 @@ jobs: SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }} - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4.0.0 with: context: ./ file: ./docker/Dockerfile diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index d938bd71..ff36ea20 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -11,7 +11,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v4 + - uses: actions/stale@v8.0.0 with: stale-issue-message: 'This issue has been inactive for 45 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' stale-pr-message: 'This PR has been inactive for 45 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.' diff --git a/ADDINGTOHOME.md b/ADDINGTOHOME.md index 3d3ed5dc..df5f4d5a 100644 --- a/ADDINGTOHOME.md +++ b/ADDINGTOHOME.md @@ -2,6 +2,11 @@ Here is how to add a Shairport Sync device to the Apple Home application on iOS *** +Step 0: Speaker & TV access. +--- + +In the Home app > Home settings "Allow Speaker & TV access", select the option "Anyone On the Same Network". + Step 1: Open the "Home" app --- diff --git a/ADVANCED TOPICS/PulseAudioAndPipeWire.md b/ADVANCED TOPICS/PulseAudioAndPipeWire.md new file mode 100644 index 00000000..520c9607 --- /dev/null +++ b/ADVANCED TOPICS/PulseAudioAndPipeWire.md @@ -0,0 +1,33 @@ +# Working with PulseAudio or PipeWire +Many Linux systems have [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/) or [PipeWire](https://pipewire.org) installed as [sound servers](https://en.wikipedia.org/wiki/Sound_server), typically providing +audio facilities for the system's GUI. Unfortunately, they cause problems for Shairport Sync. As you'll see, these problems can be worked around. + +The following remarks apply to PulseAudio and PipeWire, so, for simplicity, let's refer to PulseAudio only. + +To understand the problems, first consider a Linux system that _does not_ have PulseAudio installed. +1. Sound is managed by the Advanced Linux Sound Architecture ("[ALSA](https://www.alsa-project.org/wiki/Main_Page)") subsystem. +2. The ALSA subsystem is loaded and becomes available at system startup. +3. The ALSA `"default"` output device is mapped to the system's audio output DAC. + +Shairport Sync loads when system startup is complete and provides its service, routing audio to the ALSA default device. + +Now, consider a Linux system with a Graphical User Interface (GUI) that has PulseAudio installed. +1. As before, sound is managed by the ALSA subsystem. +2. As before, The ALSA subsystem is loaded and becomes available at system startup. +3. PulseAudio loads and becomes a client of ALSA. +4. The PulseAudio service becomes available after a user has logged in through the GUI. Importantly, if you don't log in through the GUI, you won't have a PulseAudio service. +5. The ALSA `"default"` device no longer connects to a real output device. Instead audio sent to the `"default"` device is routed into PulseAudio. Importantly, if you have no PulseAudio service, then the ALSA default device either doesn't exist at all or goes nowhere. + +# The Problem +When Shairport Sync is installed as a system service, it starts after the system has booted up. +It runs under a low-priviliged user called `shairport-sync`. +Unfortunately, per (4) above, since it was not logged in through the GUI, it won't have a PulseAudio service. If you are using the `"pa"` backend, Shairport Sync may well crash. +Per (5) above, the ALSA `"default"` device either won't exist or -- if it does exist -- won't work. Hence, using the `"alsa"` backend, Shairport Sync will either terminate or remain silent. + +# The Fixes +There are three ways to address this problem: +1. Stop using Shairport Sync as a system service. Instead, launch it from a terminal window after log-in through the GUI. +Alternatively, create a user service startup script so that it will be launched automatically after the user has logged in. This means that the system can not run without user intervention. +2. If you are using the `"alsa"` backend, set the output device to a real ALSA hardware output device. Use `$ shairport-sync -h` (or, better, [`sps-alsa-explore`](https://github.com/mikebrady/sps-alsa-explore)) to get a list of output devices, and use the configuration file to set the device. There is a possibility this might prevent PulseAudio from working properly. +# The Best Fix +3. The best of all fixes is, if possible, to avoid using a system containing PulseAudio altogether. Linux operating systems typically have a "server", "lite" or "headless" version that has no GUI and no PulseAudio. diff --git a/ADVANCED TOPICS/README.md b/ADVANCED TOPICS/README.md index 19db0266..014fc13b 100644 --- a/ADVANCED TOPICS/README.md +++ b/ADVANCED TOPICS/README.md @@ -1,6 +1,7 @@ # Advanced Topics Here you will find links to some advanced features and things you can do with Shairport Sync. * [Finish Setting Up](InitialConfiguration.md). +* [Working with PulseAudio or PipeWire](https://github.com/mikebrady/shairport-sync/blob/development/ADVANCED%20TOPICS/PulseAudioAndPipeWire.md). * [Adjusting Sync](AdjustingSync.md) – advance or delay the timing of the output from Shairport Sync to compensate for amplifier delays. * [Get The Best](GetTheBest.md) from your system. * [Metadata](Metadata.md). diff --git a/BUILD.md b/BUILD.md index bf666bb9..5a3a6e3d 100644 --- a/BUILD.md +++ b/BUILD.md @@ -154,7 +154,8 @@ If you have problems, please check the items in Final Notes below, or in the [TR Note: Shairport Sync will run indefinitely -- use Control-C it to stop it. ## 5. Enable and Start Service -Once you are happy that Shairport Sync runs from the command line, you should Control-C out of it and enable and start the `shairport-sync` service. This will launch Shairport Sync automatically as a background "daemon" service when the system powers up: +If your system has a Graphical User Interface (GUI) it probably uses PulseAudio or PipeWire for audio services. If that is the case, please review [Working with PulseAudio or PipeWire](https://github.com/mikebrady/shairport-sync/blob/development/ADVANCED%20TOPICS/PulseAudioAndPipeWire.md). +Otherwise, once you are happy that Shairport Sync runs from the command line, you should enable and start the `shairport-sync` service. This will launch Shairport Sync automatically as a background "daemon" service when the system powers up: ### Linux ``` diff --git a/MQTT.md b/MQTT.md index 95779c76..c5be761c 100644 --- a/MQTT.md +++ b/MQTT.md @@ -150,7 +150,7 @@ The MQTT service can parse the above raw messages into a subset of human-readabl * `title` -- text of song title * `volume` -- The volume is sent as a string -- "airplay_volume,volume,lowest_volume,highest_volume", where "volume", "lowest_volume" and "highest_volume" are given in dB. (see above) -and empty messages at the following topics are published. +and empty messages (`--`) at the following topics are published. * `play_start` -- fired at the begining of every song * `play_end` -- fired at the end of every song @@ -168,7 +168,7 @@ Users will find examples on how to consume the MQTT data in various home automat ### Home Assistant Variable Templates Examples of consuming "parsed" MQTT data in [Home Assistant](https://www.home-assistant.io/) -The `active_start` and `active_end` have good potential use as triggers to turn on and off various connect receivers/zones. The messages published are empty and therefor no "payload_on" is set, "payload_off" however is set to prevent accidental triggering. +The `active_start` and `active_end` have good potential use as triggers to turn on and off various connect receivers/zones. "payload_off" is set to prevent accidental triggering. ```yml binary_sensor: @@ -176,14 +176,14 @@ binary_sensor: - platform: mqtt name: "shairport active start" state_topic: "shairport/active_start" - payload_on: "" + payload_on: "--" payload_off: "OFF" off_delay: 300 - platform: mqtt name: "shairport active end" state_topic: "shairport/active_end" - payload_on: "" + payload_on: "--" payload_off: "OFF" off_delay: 300 ``` @@ -191,34 +191,29 @@ binary_sensor: Below parsed data is saved into the Home Assistant database as sensor data. Please note the conversion of the volume from dB to percentage. ```yml -sensor: - - platform: mqtt - name: "shairport album" - state_topic: "shairport/album" - expire_after: 600 - - - platform: mqtt - name: "shairport artist" - state_topic: "shairport/artist" - expire_after: 600 - - - platform: mqtt - name: "shairport title" - state_topic: "shairport/title" - expire_after: 600 - - - platform: mqtt - name: "shairport genre" - state_topic: "shairport/genre" - expire_after: 600 - - - platform: mqtt - name: "shairport volume (dB)" - state_topic: "shairport/volume" - - - platform: mqtt - name: "shairport volume (PCT)" - state_topic: "shairport/volume" - value_template: "{{ value | regex_findall_index(find='^(.+?),', index=0, ignorecase=False) | float / 30 + 1 }}" - unit_of_measurement: 'percent' +mqtt: + sensor: + - name: "shairport album" + state_topic: "shairport/album" + expire_after: 600 + + - name: "shairport artist" + state_topic: "shairport/artist" + expire_after: 600 + + - name: "shairport title" + state_topic: "shairport/title" + expire_after: 600 + + - name: "shairport genre" + state_topic: "shairport/genre" + expire_after: 600 + + - name: "shairport volume (dB)" + state_topic: "shairport/volume" + + - name: "shairport volume (PCT)" + state_topic: "shairport/volume" + value_template: "{{ value | regex_findall_index(find='^(.+?),', index=0, ignorecase=False) | float / 30 + 1 }}" + unit_of_measurement: 'percent' ``` diff --git a/README.md b/README.md index c25890c3..4a2e068e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Shairport Sync does not support AirPlay video or photo streaming. # Quick Start * A building guide is available [here](BUILD.md). -* A Docker image is available on the [Docker Hub](https://hub.docker.com/r/mikebrady/shairport-sync). +* A Docker image is available on the [Docker Hub](https://hub.docker.com/r/mikebrady/shairport-sync). Also see [docker/README.md](docker/README.md). * Next Steps and Advanced Topics are [here](ADVANCED%20TOPICS/README.md). * Runtime settings are documented [here](scripts/shairport-sync.conf). * Build configuration options are detailed in [CONFIGURATION FLAGS.md](CONFIGURATION%20FLAGS.md). diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 174a16fc..b20d898f 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -66,7 +66,10 @@ There are some more details in some the closed issues on this repository. ### VPNs -To see the AirPlay service Shairport Sync provides, your devices must be on the same subnet as Shairport Sync. If, say, a device such as an iPhone is on a VPN and Shairport Sync is not, or vice-versa, then even though the devices might be physically on the same network, they are effectively on separate networks due to the VPN and the AirPlay service will not be accessible to the device. So, when you are troubleshooting, look out for VPN issues. +To see the AirPlay service Shairport Sync provides, your devices must be on the same subnet as Shairport Sync. If, say, a device such as an iPhone is on a VPN and Shairport Sync is not, or vice-versa, then even though the devices might be physically on the same network, they are effectively on separate networks due to the VPN and the AirPlay service will not be accessible to the device. So, when you are troubleshooting, look out for VPN issues. + +### Ubiquiti Routers +Ubiquiti router settings occasionally cause services that use Boujour/Zeroconf -- including Shairport Sync -- not to appear on the network. (AirPrint is another example of a service that can be affected by this problem.) [This link](https://community.ui.com/questions/Issues-with-Bonjour-on-UDM-Pro/6db37e3f-73cf-4aaf-8dba-8a196fd0e2b1) to a Ubiquiti community discussion might be helpful. ### Faulty WiFi For an example of what it can take to track down a bad WiFi situation – in this case, a faulty WiFi adapter – please look at [this report](https://github.com/mikebrady/shairport-sync/issues/689). diff --git a/audio_alsa.c b/audio_alsa.c index 9a37f3f6..bf8d6ada 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -166,6 +166,22 @@ int volume_based_mute_is_active = // use this to allow the use of snd_pcm_writei or snd_pcm_mmap_writei snd_pcm_sframes_t (*alsa_pcm_write)(snd_pcm_t *, const void *, snd_pcm_uframes_t) = snd_pcm_writei; +void handle_unfixable_error(int errorCode) { + if (config.unfixable_error_reported == 0) { + config.unfixable_error_reported = 1; + char messageString[1024]; + messageString[0] = '\0'; + snprintf(messageString, sizeof(messageString), "output_device_error_%d", errorCode); + if (config.cmd_unfixable) { + command_execute(config.cmd_unfixable, messageString, 1); + } else { + die("An unrecoverable error, \"output_device_error_%d\", has been " + "detected. Doing an emergency exit, as no run_this_if_an_unfixable_error_is_detected program.", + errorCode); + } + } +} + static int precision_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, yndk_type *using_update_timestamps); static int standard_delay_and_status(snd_pcm_state_t *state, snd_pcm_sframes_t *delay, @@ -277,40 +293,33 @@ static void help(void) { void set_alsa_out_dev(char *dev) { alsa_out_dev = dev; } // ugh -- not static! // assuming pthread cancellation is disabled +// returns zero of all is okay, a Unx error code if there's a problem static int open_mixer() { int response = 0; if (alsa_mix_ctrl != NULL) { debug(3, "Open Mixer"); - int ret = 0; snd_mixer_selem_id_alloca(&alsa_mix_sid); snd_mixer_selem_id_set_index(alsa_mix_sid, alsa_mix_index); snd_mixer_selem_id_set_name(alsa_mix_sid, alsa_mix_ctrl); - if ((snd_mixer_open(&alsa_mix_handle, 0)) < 0) { + if ((response = snd_mixer_open(&alsa_mix_handle, 0)) < 0) { debug(1, "Failed to open mixer"); - response = -1; } else { debug(3, "Mixer device name is \"%s\".", alsa_mix_dev); - if ((snd_mixer_attach(alsa_mix_handle, alsa_mix_dev)) < 0) { + if ((response = snd_mixer_attach(alsa_mix_handle, alsa_mix_dev)) < 0) { debug(1, "Failed to attach mixer"); - response = -2; } else { - if ((snd_mixer_selem_register(alsa_mix_handle, NULL, NULL)) < 0) { + if ((response = snd_mixer_selem_register(alsa_mix_handle, NULL, NULL)) < 0) { debug(1, "Failed to register mixer element"); - response = -3; } else { - ret = snd_mixer_load(alsa_mix_handle); - if (ret < 0) { + if ((response = snd_mixer_load(alsa_mix_handle)) < 0) { debug(1, "Failed to load mixer element"); - response = -4; } else { debug(3, "Mixer control is \"%s\",%d.", alsa_mix_ctrl, alsa_mix_index); alsa_mix_elem = snd_mixer_find_selem(alsa_mix_handle, alsa_mix_sid); if (!alsa_mix_elem) { warn("failed to find mixer control \"%s\",%d.", alsa_mix_ctrl, alsa_mix_index); - response = -5; - } else { - response = 1; // we found a hardware mixer and successfully opened it + response = -ENXIO; // don't let this be ENODEV! } } } @@ -321,21 +330,25 @@ static int open_mixer() { } // assuming pthread cancellation is disabled -static void close_mixer() { +static int close_mixer() { + int ret = 0; if (alsa_mix_handle) { - snd_mixer_close(alsa_mix_handle); + ret = snd_mixer_close(alsa_mix_handle); alsa_mix_handle = NULL; } + return ret; } // assuming pthread cancellation is disabled -static void do_snd_mixer_selem_set_playback_dB_all(snd_mixer_elem_t *mix_elem, double vol) { - if (snd_mixer_selem_set_playback_dB_all(mix_elem, vol, 0) != 0) { +static int do_snd_mixer_selem_set_playback_dB_all(snd_mixer_elem_t *mix_elem, double vol) { + int response = 0; + if ((response = snd_mixer_selem_set_playback_dB_all(mix_elem, vol, 0)) != 0) { debug(1, "Can't set playback volume accurately to %f dB.", vol); - if (snd_mixer_selem_set_playback_dB_all(mix_elem, vol, -1) != 0) - if (snd_mixer_selem_set_playback_dB_all(mix_elem, vol, 1) != 0) + if ((response = snd_mixer_selem_set_playback_dB_all(mix_elem, vol, -1)) != 0) + if ((response = snd_mixer_selem_set_playback_dB_all(mix_elem, vol, 1)) != 0) debug(1, "Could not set playback dB volume on the mixer."); } + return response; } // This array is a sequence of the output rates to be tried if automatic speed selection is @@ -444,13 +457,6 @@ static int actual_open_alsa_device(int do_auto_setup) { warn("The output device \"%s\" is busy and can't be used by Shairport Sync at present.", alsa_out_dev); debug(2, "the alsa output_device \"%s\" is busy.", alsa_out_dev); - } else if (ret == -ENOENT) { - die("the alsa output_device \"%s\" can not be found.", alsa_out_dev); - } else { - char errorstring[1024]; - strerror_r(-ret, (char *)errorstring, sizeof(errorstring)); - die("alsa: error %d (\"%s\") opening alsa device \"%s\".", ret, (char *)errorstring, - alsa_out_dev); } alsa_handle_status = ret; frames_sent_break_occurred = 1; @@ -900,11 +906,11 @@ static int prepare_mixer() { // 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 (open_mixer() == 0) { if (snd_mixer_selem_get_playback_volume_range(alsa_mix_elem, &alsa_mix_minv, &alsa_mix_maxv) < - 0) + 0) { debug(1, "Can't read mixer's [linear] min and max volumes."); - else { + } else { if (snd_mixer_selem_get_playback_dB_range(alsa_mix_elem, &alsa_mix_mindb, &alsa_mix_maxdb) == 0) { @@ -934,14 +940,12 @@ static int prepare_mixer() { "a dB volume scale.", alsa_mix_ctrl); - if (snd_ctl_open(&ctl, alsa_mix_dev, 0) < 0) { + if ((response = 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) { + if ((response = 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); @@ -959,22 +963,6 @@ static int prepare_mixer() { 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) && @@ -986,7 +974,8 @@ static int prepare_mixer() { } else { // debug(1, "Has mixer but not using hardware mute."); } - close_mixer(); + if (response == 0) + response = close_mixer(); } debug_mutex_unlock(&alsa_mixer_mutex, 3); // release the mutex pthread_cleanup_pop(0); @@ -1393,7 +1382,7 @@ static int set_mute_state() { 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)) { + (open_mixer() == 0)) { response = 0; // okay if actually using the mute facility debug(2, "alsa: actually set_mute_state"); int mute = 0; @@ -1712,36 +1701,20 @@ static int stats(uint64_t *raw_measurement_time, uint64_t *corrected_measurement *the_delay = hd; return ret; } -/* -static int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played) { - // elapsed_time is in nanoseconds - int response = 0; // zero means okay - if (measurement_data_is_valid) { - *elapsed_time = measurement_time - measurement_start_time; - *frames_played = frames_played_at_measurement_time - frames_played_at_measurement_start_time; - } else { - *elapsed_time = 0; - *frames_played = 0; - response = -1; - } - return response; -} -*/ static 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 ((samples != 0) && (buf != NULL)) { - snd_pcm_state_t state; - snd_pcm_sframes_t my_delay; - int ret = delay_and_status(&state, &my_delay, NULL); + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable - if (ret == 0) { // will be non-zero if an error or a stall - - if ((samples != 0) && (buf != NULL)) { + snd_pcm_state_t state; + snd_pcm_sframes_t my_delay; + ret = delay_and_status(&state, &my_delay, NULL); + if (ret == 0) { // will be non-zero if an error or a stall // just check the state of the DAC if ((state != SND_PCM_STATE_PREPARED) && (state != SND_PCM_STATE_RUNNING) && @@ -1778,38 +1751,27 @@ static int do_play(void *buf, int samples) { debug(1, "alsa: recovering from a previous underrun."); else debug(1, "alsa: underrun while writing %d samples to alsa device.", samples); - int tret = snd_pcm_recover(alsa_handle, ret, 1); - if (tret < 0) { - warn("alsa: can't recover from SND_PCM_STATE_XRUN: %s.", snd_strerror(tret)); - } + ret = snd_pcm_recover(alsa_handle, ret, 1); } else if (ret == -ESTRPIPE) { /* suspended */ - debug(1, "alsa: suspended while writing %d samples to alsa device.", samples); - int tret; - while ((tret = snd_pcm_resume(alsa_handle)) == -EAGAIN) { - sleep(1); /* wait until the suspend flag is released */ - if (tret < 0) { - warn("alsa: can't recover from SND_PCM_STATE_SUSPENDED state, " - "snd_pcm_prepare() " - "failed: %s.", - snd_strerror(tret)); - } - } - } else { - char errorstring[1024]; - strerror_r(-ret, (char *)errorstring, sizeof(errorstring)); - debug(1, "alsa: error %d (\"%s\") writing %d samples to alsa device.", ret, - (char *)errorstring, samples); + if (state != prior_state) + debug(1, "alsa: suspended while writing %d samples to alsa device.", samples); + if ((ret = snd_pcm_resume(alsa_handle)) == -ENOSYS) + ret = snd_pcm_prepare(alsa_handle); + } else if (ret >= 0) { + debug(1, "alsa: only %d of %d samples output.", ret, samples); } } } - } else { - debug(1, - "alsa: device status returns fault status %d and SND_PCM_STATE_* " - "%d for play.", - ret, state); + pthread_setcancelstate(oldState, NULL); + if (ret < 0) { + char errorstring[1024]; + strerror_r(-ret, (char *)errorstring, sizeof(errorstring)); + debug(1, "alsa: SND_PCM_STATE_* %d, error %d (\"%s\") writing %d samples to alsa device.", + state, ret, (char *)errorstring, samples); + } + if ((ret == -ENOENT) || (ret == -ENODEV)) // if the device isn't there... + handle_unfixable_error(-ret); } - - pthread_setcancelstate(oldState, NULL); return ret; } @@ -1835,6 +1797,9 @@ static int do_open(int do_auto_setup) { // any previously-reported frame count frames_sent_for_playing = 0; alsa_backend_state = abm_connected; // only do this if it really opened it. + } else { + if ((ret == -ENOENT) || (ret == -ENODEV)) // if the device isn't there... + handle_unfixable_error(-ret); } } else { debug(1, "alsa: do_open() -- output device already open."); @@ -1987,7 +1952,7 @@ static void do_volume(double vol) { // caller is assumed to have the alsa_mutex 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 (volume_set_request && (open_mixer() == 0)) { if (has_softvol) { if (ctl && elem_id) { snd_ctl_elem_value_t *value; diff --git a/audio_pa.c b/audio_pa.c index 8f92ad39..eef771db 100644 --- a/audio_pa.c +++ b/audio_pa.c @@ -1,6 +1,6 @@ /* * Asynchronous PulseAudio Backend. This file is part of Shairport Sync. - * Copyright (c) Mike Brady 2017 + * Copyright (c) Mike Brady 2017-2023 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -45,14 +45,6 @@ static pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER; -/* -static struct { - char *server; - char *sink; - char *service_name; -} pulse_options = {.server = NULL, .sink = NULL, .service_name = NULL}; -*/ - pa_threaded_mainloop *mainloop; pa_mainloop_api *mainloop_api; pa_context *context; @@ -66,8 +58,102 @@ void stream_state_cb(pa_stream *s, void *mainloop); void stream_success_cb(pa_stream *stream, int success, void *userdata); void stream_write_cb(pa_stream *stream, size_t requested_bytes, void *userdata); -static int init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) { +int status_error_notifications = 0; +static void check_pa_stream_status(const pa_stream *p, const char *message) { + if (status_error_notifications < 10) { + if (p == NULL) { + warn("%s No pulseaudio stream!", message); + status_error_notifications++; + } else { + status_error_notifications++; // assume an error + switch (pa_stream_get_state(p)) { + case PA_STREAM_UNCONNECTED: + warn("%s Pulseaudio stream unconnected!", message); + break; + case PA_STREAM_CREATING: + warn("%s Pulseaudio stream being created!", message); + break; + case PA_STREAM_READY: + status_error_notifications--; // no error + break; + case PA_STREAM_FAILED: + warn("%s Pulseaudio stream failed!", message); + break; + case PA_STREAM_TERMINATED: + warn("%s Pulseaudio stream unexpectedly terminated!", message); + break; + default: + warn("%s Pulseaudio stream in unexpected state %d!", message, pa_stream_get_state(p)); + break; + } + } + } +} +static void connect_stream() { + // debug(1, "connect_stream"); + uint32_t buffer_size_in_bytes = (uint32_t)2 * 2 * RATE * 0.1; // hard wired in here + // debug(1, "pa_buffer size is %u bytes.", buffer_size_in_bytes); + + pa_threaded_mainloop_lock(mainloop); + // Create a playback stream + pa_sample_spec sample_specifications; + sample_specifications.format = FORMAT; + sample_specifications.rate = RATE; + sample_specifications.channels = 2; + + pa_channel_map map; + pa_channel_map_init_stereo(&map); + + stream = pa_stream_new(context, "Playback", &sample_specifications, &map); + pa_stream_set_state_callback(stream, stream_state_cb, mainloop); + pa_stream_set_write_callback(stream, stream_write_cb, mainloop); + // pa_stream_set_latency_update_callback(stream, stream_latency_cb, mainloop); + + // recommended settings, i.e. server uses sensible values + pa_buffer_attr buffer_attr; + buffer_attr.maxlength = (uint32_t)-1; + buffer_attr.tlength = buffer_size_in_bytes; + buffer_attr.prebuf = (uint32_t)0; + buffer_attr.minreq = (uint32_t)-1; + + pa_stream_flags_t stream_flags; + stream_flags = PA_STREAM_START_CORKED | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_NOT_MONOTONIC | + PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY; + + int connect_result; + + if (config.pa_sink) { + // Connect stream to the sink specified in the config + connect_result = + pa_stream_connect_playback(stream, config.pa_sink, &buffer_attr, stream_flags, NULL, NULL); + } else { + // Connect stream to the default audio output sink + connect_result = + pa_stream_connect_playback(stream, NULL, &buffer_attr, stream_flags, NULL, NULL); + } + + if (connect_result != 0) + die("could not connect to the pulseaudio playback stream -- the error message is \"%s\".", + pa_strerror(pa_context_errno(context))); + + // Wait for the stream to be ready + for (;;) { + pa_stream_state_t stream_state = pa_stream_get_state(stream); + if (!PA_STREAM_IS_GOOD(stream_state)) + die("stream state is no longer good while waiting for stream to become ready -- the error " + "message is \"%s\".", + pa_strerror(pa_context_errno(context))); + if (stream_state == PA_STREAM_READY) + break; + pa_threaded_mainloop_wait(mainloop); + } + + pa_threaded_mainloop_unlock(mainloop); +} + +static int init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) { + // debug(1, "pa_init"); // set up default values first config.audio_backend_buffer_desired_length = 0.35; config.audio_backend_buffer_interpolation_threshold_in_seconds = @@ -148,202 +234,107 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char * } pa_threaded_mainloop_unlock(mainloop); - + connect_stream(); + check_pa_stream_status(stream, "audio_pa initialisation."); return 0; } static void deinit(void) { + check_pa_stream_status(stream, "audio_pa deinitialisation."); + pa_stream_disconnect(stream); pa_threaded_mainloop_stop(mainloop); pa_threaded_mainloop_free(mainloop); - debug(1, "pa deinit done"); -} - -static void do_start() { - // debug(1, "pa_start"); - uint32_t buffer_size_in_bytes = (uint32_t)2 * 2 * RATE * 0.1; // hard wired in here - // debug(1, "pa_buffer size is %u bytes.", buffer_size_in_bytes); - - pa_threaded_mainloop_lock(mainloop); - // Create a playback stream - pa_sample_spec sample_specifications; - sample_specifications.format = FORMAT; - sample_specifications.rate = RATE; - sample_specifications.channels = 2; - - pa_channel_map map; - pa_channel_map_init_stereo(&map); - - stream = pa_stream_new(context, "Playback", &sample_specifications, &map); - pa_stream_set_state_callback(stream, stream_state_cb, mainloop); - pa_stream_set_write_callback(stream, stream_write_cb, mainloop); - // pa_stream_set_latency_update_callback(stream, stream_latency_cb, mainloop); - - // recommended settings, i.e. server uses sensible values - pa_buffer_attr buffer_attr; - buffer_attr.maxlength = (uint32_t)-1; - buffer_attr.tlength = buffer_size_in_bytes; - buffer_attr.prebuf = (uint32_t)0; - buffer_attr.minreq = (uint32_t)-1; - - // Settings copied as per the chromium browser source - pa_stream_flags_t stream_flags; - stream_flags = PA_STREAM_START_CORKED | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_NOT_MONOTONIC | - // PA_STREAM_AUTO_TIMING_UPDATE; - PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY; - - int connect_result; - - if (config.pa_sink) { - // Connect stream to the sink specified in the config - connect_result = - pa_stream_connect_playback(stream, config.pa_sink, &buffer_attr, stream_flags, NULL, NULL); - } else { - // Connect stream to the default audio output sink - connect_result = - pa_stream_connect_playback(stream, NULL, &buffer_attr, stream_flags, NULL, NULL); - } - - if (connect_result != 0) - die("could not connect to the pulseaudio playback stream -- the error message is \"%s\".", - pa_strerror(pa_context_errno(context))); - - // Wait for the stream to be ready - for (;;) { - pa_stream_state_t stream_state = pa_stream_get_state(stream); - if (!PA_STREAM_IS_GOOD(stream_state)) - die("stream state is no longer good while waiting for stream to become ready -- the error " - "message is \"%s\".", - pa_strerror(pa_context_errno(context))); - if (stream_state == PA_STREAM_READY) - break; - pa_threaded_mainloop_wait(mainloop); - } - - pa_threaded_mainloop_unlock(mainloop); + // debug(1, "pa deinit done"); } static void start(__attribute__((unused)) int sample_rate, __attribute__((unused)) int sample_format) { - do_start(); -} - -static int stream_is_open() { - int response = 0; // default to no - pa_usec_t latency; - int negative; - pa_threaded_mainloop_lock(mainloop); - int gl = pa_stream_get_latency(stream, &latency, &negative); - pa_threaded_mainloop_unlock(mainloop); - if (gl >= 0) - response = 1; - return response; + check_pa_stream_status(stream, "audio_pa start."); } static int play(void *buf, int samples, __attribute__((unused)) int sample_type, __attribute__((unused)) uint32_t timestamp, __attribute__((unused)) uint64_t playtime) { // debug(1,"pa_play of %d samples.",samples); - if (stream_is_open() == 0) { - // debug(1,"pa open stream before play."); - do_start(); - } - if (stream_is_open() != 0) { - // copy the samples into the queue - size_t bytes_to_transfer = samples * 2 * 2; - size_t space_to_end_of_buffer = audio_umb - audio_eoq; - if (space_to_end_of_buffer >= bytes_to_transfer) { - memcpy(audio_eoq, buf, bytes_to_transfer); - audio_occupancy += bytes_to_transfer; - pthread_mutex_lock(&buffer_mutex); - audio_eoq += bytes_to_transfer; - pthread_mutex_unlock(&buffer_mutex); - } else { - memcpy(audio_eoq, buf, space_to_end_of_buffer); - buf += space_to_end_of_buffer; - memcpy(audio_lmb, buf, bytes_to_transfer - space_to_end_of_buffer); - pthread_mutex_lock(&buffer_mutex); - audio_occupancy += bytes_to_transfer; - pthread_mutex_unlock(&buffer_mutex); - audio_eoq = audio_lmb + bytes_to_transfer - space_to_end_of_buffer; - } - if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) { - // debug(1,"Uncorked"); - pa_threaded_mainloop_lock(mainloop); - pa_stream_cork(stream, 0, stream_success_cb, mainloop); - pa_threaded_mainloop_unlock(mainloop); - } + // copy the samples into the queue + check_pa_stream_status(stream, "audio_pa play."); + size_t bytes_to_transfer = samples * 2 * 2; + size_t space_to_end_of_buffer = audio_umb - audio_eoq; + if (space_to_end_of_buffer >= bytes_to_transfer) { + memcpy(audio_eoq, buf, bytes_to_transfer); + audio_occupancy += bytes_to_transfer; + pthread_mutex_lock(&buffer_mutex); + audio_eoq += bytes_to_transfer; + pthread_mutex_unlock(&buffer_mutex); } else { - // debug(2, "could not open pa stream for play"); + memcpy(audio_eoq, buf, space_to_end_of_buffer); + buf += space_to_end_of_buffer; + memcpy(audio_lmb, buf, bytes_to_transfer - space_to_end_of_buffer); + pthread_mutex_lock(&buffer_mutex); + audio_occupancy += bytes_to_transfer; + pthread_mutex_unlock(&buffer_mutex); + audio_eoq = audio_lmb + bytes_to_transfer - space_to_end_of_buffer; + } + if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) { + // debug(1,"Uncorked"); + pa_threaded_mainloop_lock(mainloop); + pa_stream_cork(stream, 0, stream_success_cb, mainloop); + pa_threaded_mainloop_unlock(mainloop); } return 0; } int pa_delay(long *the_delay) { + check_pa_stream_status(stream, "audio_pa delay."); // debug(1,"pa_delay"); - int reply = -ENODEV; long result = 0; - if (stream_is_open() == 0) { - // debug(1,"pa open stream before delay."); - do_start(); - } - if (stream_is_open() != 0) { - pa_usec_t latency; - int negative; - pa_threaded_mainloop_lock(mainloop); - int gl = pa_stream_get_latency(stream, &latency, &negative); - pa_threaded_mainloop_unlock(mainloop); - if (gl == PA_ERR_NODATA) { - // debug(1, "No latency data yet."); - reply = -ENODEV; - } else if (gl != 0) { - // debug(1,"Error %d getting latency.",gl); - reply = -EIO; - } else { - result = (audio_occupancy / (2 * 2)) + (latency * 44100) / 1000000; - reply = 0; - } + int reply = 0; + pa_usec_t latency; + int negative; + pa_threaded_mainloop_lock(mainloop); + int gl = pa_stream_get_latency(stream, &latency, &negative); + pa_threaded_mainloop_unlock(mainloop); + if (gl == PA_ERR_NODATA) { + // debug(1, "No latency data yet."); + reply = -ENODEV; + } else if (gl != 0) { + // debug(1,"Error %d getting latency.",gl); + reply = -EIO; } else { - // debug(2, "could not open pa stream for delay"); + result = (audio_occupancy / (2 * 2)) + (latency * 44100) / 1000000; + reply = 0; } *the_delay = result; return reply; } void flush(void) { - // debug(1,"Flush."); - if (stream_is_open() != 0) { - // Cork the stream so it will stop playing - pa_threaded_mainloop_lock(mainloop); - if (pa_stream_is_corked(stream) == 0) { - // debug(1,"Flush and cork for flush."); - pa_stream_flush(stream, stream_success_cb, NULL); - pa_stream_cork(stream, 1, stream_success_cb, mainloop); - } - pa_threaded_mainloop_unlock(mainloop); - audio_toq = audio_eoq = audio_lmb; - audio_umb = audio_lmb + audio_size; - audio_occupancy = 0; + check_pa_stream_status(stream, "audio_pa flush."); + pa_threaded_mainloop_lock(mainloop); + if (pa_stream_is_corked(stream) == 0) { + // debug(1,"Flush and cork for flush."); + pa_stream_flush(stream, stream_success_cb, NULL); + pa_stream_cork(stream, 1, stream_success_cb, mainloop); } + audio_toq = audio_eoq = audio_lmb; + audio_umb = audio_lmb + audio_size; + audio_occupancy = 0; + pa_threaded_mainloop_unlock(mainloop); } static void stop(void) { - // debug(1,"Stop."); - if (stream_is_open() != 0) { - // Cork the stream so it will stop playing - pa_threaded_mainloop_lock(mainloop); - if (pa_stream_is_corked(stream) == 0) { - // debug(1,"Flush and cork for stop."); - pa_stream_flush(stream, stream_success_cb, NULL); - pa_stream_cork(stream, 1, stream_success_cb, mainloop); - } - pa_threaded_mainloop_unlock(mainloop); - audio_toq = audio_eoq = audio_lmb; - audio_umb = audio_lmb + audio_size; - audio_occupancy = 0; - - // debug(1,"pa stop"); - pa_stream_disconnect(stream); + check_pa_stream_status(stream, "audio_pa stop."); + // Cork the stream so it will stop playing + pa_threaded_mainloop_lock(mainloop); + if (pa_stream_is_corked(stream) == 0) { + // debug(1,"Flush and cork for stop."); + pa_stream_flush(stream, stream_success_cb, NULL); + pa_stream_cork(stream, 1, stream_success_cb, mainloop); } + audio_toq = audio_eoq = audio_lmb; + audio_umb = audio_lmb + audio_size; + audio_occupancy = 0; + pa_threaded_mainloop_unlock(mainloop); } audio_output audio_pa = {.name = "pa", @@ -363,54 +354,29 @@ audio_output audio_pa = {.name = "pa", .mute = NULL}; void context_state_cb(__attribute__((unused)) pa_context *context, void *mainloop) { + // debug(1,"context_state_cb called."); pa_threaded_mainloop_signal(mainloop, 0); } void stream_state_cb(__attribute__((unused)) pa_stream *s, void *mainloop) { + // debug(1,"stream_state_cb called."); pa_threaded_mainloop_signal(mainloop, 0); } void stream_write_cb(pa_stream *stream, size_t requested_bytes, __attribute__((unused)) void *userdata) { - - /* - // play with timing information - const struct pa_timing_info *ti = pa_stream_get_timing_info(stream); - if ((ti == NULL) || (ti->write_index_corrupt)) { - debug(2, "Timing info invalid"); - } else { - struct timeval time_now; - - pa_gettimeofday(&time_now); - - uint64_t time_now_fp = ((uint64_t)time_now.tv_sec << 32) + - ((uint64_t)time_now.tv_usec << 32) / 1000000; // types okay - uint64_t time_of_ti_fp = ((uint64_t)(ti->timestamp.tv_sec) << 32) + - ((uint64_t)(ti->timestamp.tv_usec) << 32) / 1000000; // types okay - - if (time_now_fp >= time_of_ti_fp) { - uint64_t estimate_age = ((time_now_fp - time_of_ti_fp) * 1000000) >> 32; - uint64_t bytes_in_buffer = ti->write_index - ti->read_index; - pa_usec_t microseconds_to_write_buffer = (bytes_in_buffer * 1000000) / (44100 * 2 * 2); - pa_usec_t ea = (pa_usec_t)estimate_age; - pa_usec_t pa_latency = ti->sink_usec + ti->transport_usec + microseconds_to_write_buffer; - pa_usec_t estimated_latency = pa_latency - estimate_age; - // debug(1,"Estimated latency is %d microseconds.",estimated_latency); - - // } else { - // debug(1, "Time now is earlier than time of timing information"); - } - } - */ + check_pa_stream_status(stream, "audio_pa stream_write_cb."); int bytes_to_transfer = requested_bytes; int bytes_transferred = 0; uint8_t *buffer = NULL; - - while ((bytes_to_transfer > 0) && (audio_occupancy > 0)) { + int ret = 0; + while ((bytes_to_transfer > 0) && (audio_occupancy > 0) && (ret == 0)) { + if (pa_stream_is_suspended(stream)) + debug(1, "stream is suspended"); size_t bytes_we_can_transfer = bytes_to_transfer; if (audio_occupancy < bytes_we_can_transfer) { // debug(1, "Underflow? We have %d bytes but we are asked for %d bytes", audio_occupancy, - // bytes_we_can_transfer); + // bytes_we_can_transfer); pa_stream_cork(stream, 1, stream_success_cb, mainloop); // debug(1, "Corked"); bytes_we_can_transfer = audio_occupancy; @@ -418,70 +384,44 @@ void stream_write_cb(pa_stream *stream, size_t requested_bytes, // bytes we can transfer will never be greater than the bytes available - pa_stream_begin_write(stream, (void **)&buffer, &bytes_we_can_transfer); - if (bytes_we_can_transfer <= (size_t)(audio_umb - audio_toq)) { - // the bytes are all in a row in the audo buffer - memcpy(buffer, audio_toq, bytes_we_can_transfer); - audio_toq += bytes_we_can_transfer; - // lock - pthread_mutex_lock(&buffer_mutex); - audio_occupancy -= bytes_we_can_transfer; - pthread_mutex_unlock(&buffer_mutex); - // unlock - pa_stream_write(stream, buffer, bytes_we_can_transfer, NULL, 0LL, PA_SEEK_RELATIVE); - bytes_transferred += bytes_we_can_transfer; - } else { - // the bytes are in two places in the audio buffer - size_t first_portion_to_write = audio_umb - audio_toq; - if (first_portion_to_write != 0) - memcpy(buffer, audio_toq, first_portion_to_write); - uint8_t *new_buffer = buffer + first_portion_to_write; - memcpy(new_buffer, audio_lmb, bytes_we_can_transfer - first_portion_to_write); - pa_stream_write(stream, buffer, bytes_we_can_transfer, NULL, 0LL, PA_SEEK_RELATIVE); - bytes_transferred += bytes_we_can_transfer; - audio_toq = audio_lmb + bytes_we_can_transfer - first_portion_to_write; - // lock - pthread_mutex_lock(&buffer_mutex); - audio_occupancy -= bytes_we_can_transfer; - pthread_mutex_unlock(&buffer_mutex); - // unlock + ret = pa_stream_begin_write(stream, (void **)&buffer, &bytes_we_can_transfer); + if ((ret == 0) && (buffer != NULL)) { + if (bytes_we_can_transfer <= (size_t)(audio_umb - audio_toq)) { + // the bytes are all in a row in the audo buffer + memcpy(buffer, audio_toq, bytes_we_can_transfer); + audio_toq += bytes_we_can_transfer; + // lock + pthread_mutex_lock(&buffer_mutex); + audio_occupancy -= bytes_we_can_transfer; + pthread_mutex_unlock(&buffer_mutex); + // unlock + ret = pa_stream_write(stream, buffer, bytes_we_can_transfer, NULL, 0LL, PA_SEEK_RELATIVE); + bytes_transferred += bytes_we_can_transfer; + } else { + // the bytes are in two places in the audio buffer + size_t first_portion_to_write = audio_umb - audio_toq; + if (first_portion_to_write != 0) + memcpy(buffer, audio_toq, first_portion_to_write); + uint8_t *new_buffer = buffer + first_portion_to_write; + memcpy(new_buffer, audio_lmb, bytes_we_can_transfer - first_portion_to_write); + ret = pa_stream_write(stream, buffer, bytes_we_can_transfer, NULL, 0LL, PA_SEEK_RELATIVE); + bytes_transferred += bytes_we_can_transfer; + audio_toq = audio_lmb + bytes_we_can_transfer - first_portion_to_write; + // lock + pthread_mutex_lock(&buffer_mutex); + audio_occupancy -= bytes_we_can_transfer; + pthread_mutex_unlock(&buffer_mutex); + // unlock + } + bytes_to_transfer -= bytes_we_can_transfer; } - bytes_to_transfer -= bytes_we_can_transfer; - // debug(1,"audio_toq is %llx",audio_toq); } - + if (ret != 0) + debug(1, "error writing to pa buffer"); // debug(1,"<< 0) { - uint8_t *buffer = NULL; - size_t bytes_to_fill = 44100; - size_t i; - - if (bytes_to_fill > bytes_remaining) - bytes_to_fill = bytes_remaining; - - pa_stream_begin_write(stream, (void **)&buffer, &bytes_to_fill); - if (buffer) { - for (i = 0; i < bytes_to_fill; i += 2) { - buffer[i] = (i % 100) * 40 / 100 + 44; - buffer[i + 1] = (i % 100) * 40 / 100 + 44; - } - } else { - die("buffer not allocated in alt_stream_write_cb."); - } - - pa_stream_write(stream, buffer, bytes_to_fill, NULL, 0LL, PA_SEEK_RELATIVE); - - bytes_remaining -= bytes_to_fill; - } -} - void stream_success_cb(__attribute__((unused)) pa_stream *stream, __attribute__((unused)) int success, __attribute__((unused)) void *userdata) { diff --git a/audio_stdout.c b/audio_stdout.c index 61d91bfa..c31595d9 100644 --- a/audio_stdout.c +++ b/audio_stdout.c @@ -37,21 +37,22 @@ #include static int fd = -1; +static int warned = 0; static void start(__attribute__((unused)) int sample_rate, __attribute__((unused)) int sample_format) { fd = STDOUT_FILENO; + warned = 0; } static int play(void *buf, int samples, __attribute__((unused)) int sample_type, __attribute__((unused)) uint32_t timestamp, __attribute__((unused)) uint64_t playtime) { char errorstring[1024]; - int warned = 0; int rc = write(fd, buf, samples * 4); if ((rc < 0) && (warned == 0)) { strerror_r(errno, (char *)errorstring, 1024); - warn("Error %d writing to stdout: \"%s\".", errno, errorstring); + warn("Error %d writing to stdout (fd: %d): \"%s\".", errno, fd, errorstring); warned = 1; } return rc; diff --git a/common.c b/common.c index 63ea1cd4..f60f1ea5 100644 --- a/common.c +++ b/common.c @@ -110,6 +110,10 @@ void set_alsa_out_dev(char *); #endif +#ifdef CONFIG_AIRPLAY_2 +#include "nqptp-shm-structures.h" +#endif + config_t config_file_stuff; int type_of_exit_cleanup; uint64_t ns_time_at_startup, ns_time_at_last_debug_message; @@ -240,21 +244,6 @@ void log_to_syslog() { shairport_cfg config; -// accessors for multi-thread-access fields in the conn structure - -double get_config_airplay_volume() { - config_lock; - double v = config.airplay_volume; - config_unlock; - return v; -} - -void set_config_airplay_volume(double v) { - config_lock; - config.airplay_volume = v; - config_unlock; -} - volatile int debuglev = 0; sigset_t pselect_sigset; @@ -1627,6 +1616,9 @@ char *get_version_string() { strcpy(version_string, PACKAGE_VERSION); #ifdef CONFIG_AIRPLAY_2 strcat(version_string, "-AirPlay2"); + char smiv[1024]; + snprintf(smiv, 1024, "-smi%u", NQPTP_SHM_STRUCTURES_VERSION); + strcat(version_string, smiv); #endif #ifdef CONFIG_APPLE_ALAC strcat(version_string, "-alac"); @@ -1952,48 +1944,65 @@ int32_t mod32Difference(uint32_t a, uint32_t b) { } int get_device_id(uint8_t *id, int int_length) { - int response = 0; + + uint64_t wait_time = 10000000000L; // wait up to this (ns) long to get a MAC address + + int response = -1; struct ifaddrs *ifaddr = NULL; struct ifaddrs *ifa = NULL; + int i = 0; uint8_t *t = id; for (i = 0; i < int_length; i++) { *t++ = 0; } - if (getifaddrs(&ifaddr) == -1) { - response = -1; - } else { - t = id; - int found = 0; + uint64_t wait_until = get_absolute_time_in_ns(); + wait_until = wait_until + wait_time; - for (ifa = ifaddr; ((ifa != NULL) && (found == 0)); ifa = ifa->ifa_next) { + int64_t time_to_wait; + do { + if (getifaddrs(&ifaddr) == 0) { + t = id; + int found = 0; + + for (ifa = ifaddr; ((ifa != NULL) && (found == 0)); ifa = ifa->ifa_next) { #ifdef AF_PACKET - if ((ifa->ifa_addr) && (ifa->ifa_addr->sa_family == AF_PACKET)) { - struct sockaddr_ll *s = (struct sockaddr_ll *)ifa->ifa_addr; - if ((strcmp(ifa->ifa_name, "lo") != 0)) { - found = 1; - for (i = 0; ((i < s->sll_halen) && (i < int_length)); i++) { - *t++ = s->sll_addr[i]; + if ((ifa->ifa_addr) && (ifa->ifa_addr->sa_family == AF_PACKET)) { + struct sockaddr_ll *s = (struct sockaddr_ll *)ifa->ifa_addr; + if ((strcmp(ifa->ifa_name, "lo") != 0)) { + found = 1; + response = 0; + for (i = 0; ((i < s->sll_halen) && (i < int_length)); i++) { + *t++ = s->sll_addr[i]; + } } } - } #else #ifdef AF_LINK - struct sockaddr_dl *sdl = (struct sockaddr_dl *)ifa->ifa_addr; - if ((sdl) && (sdl->sdl_family == AF_LINK)) { - if (sdl->sdl_type == IFT_ETHER) { - found = 1; - uint8_t *s = (uint8_t *)LLADDR(sdl); - for (i = 0; ((i < sdl->sdl_alen) && (i < int_length)); i++) { - *t++ = *s++; + struct sockaddr_dl *sdl = (struct sockaddr_dl *)ifa->ifa_addr; + if ((sdl) && (sdl->sdl_family == AF_LINK)) { + if (sdl->sdl_type == IFT_ETHER) { + found = 1; + response = 0; + uint8_t *s = (uint8_t *)LLADDR(sdl); + for (i = 0; ((i < sdl->sdl_alen) && (i < int_length)); i++) { + *t++ = *s++; + } } } +#endif +#endif } -#endif -#endif + freeifaddrs(ifaddr); } - freeifaddrs(ifaddr); - } + // wait a little time if we haven't got a response + if (response != 0) { + usleep(100000); + } + time_to_wait = wait_until - get_absolute_time_in_ns(); + } while ((response != 0) && (time_to_wait > 0)); + if (response != 0) + warn("Can't create a device ID -- no valid MAC address can be found."); return response; } diff --git a/common.h b/common.h index 639c1c0f..e152db68 100644 --- a/common.h +++ b/common.h @@ -37,7 +37,8 @@ typedef enum { typedef enum { TOE_normal, TOE_emergency, - TOE_dbus // a request was made on a D-Bus interface (the native D-Bus or MPRIS interfaces)-- don't wait for the dbus thread to exit + TOE_dbus // a request was made on a D-Bus interface (the native D-Bus or MPRIS interfaces)-- don't + // wait for the dbus thread to exit } type_of_exit_type; #define sps_extra_code_output_stalled 32768 @@ -121,7 +122,14 @@ typedef struct { config_t *cfg; int endianness; double airplay_volume; // stored here for reloading when necessary - char *appName; // normally the app is called shairport-syn, but it may be symlinked + double default_airplay_volume; + double high_threshold_airplay_volume; + uint64_t last_access_to_volume_info_time; + int limit_to_high_volume_threshold_time_in_minutes; // revert to the high threshold volume level + // if the existing volume level exceeds this + // and hasn't been used for this amount of + // time (0 means never revert) + char *appName; // normally the app is called shairport-syn, but it may be symlinked char *password; char *service_name; // the name for the shairport service, e.g. "Shairport Sync Version %v running // on host %h" @@ -160,7 +168,7 @@ typedef struct { int mqtt_enable_remote; char *mqtt_empty_payload_substitute; #endif - uint8_t ap1_prefix[6]; + uint8_t ap1_prefix[6]; uint8_t hw_addr[8]; // only needs 6 but 8 is handy when converting this to a number int port; int udp_port_base; @@ -168,11 +176,12 @@ typedef struct { int ignore_volume_control; int volume_max_db_set; // set to 1 if a maximum volume db has been set int volume_max_db; - int no_sync; // disable synchronisation, even if it's available - int no_mmap; // disable use of mmap-based output, even if it's available - double resyncthreshold; // if it get's out of whack my more than this number of seconds, resync. - // Zero means never - // resync. + int no_sync; // disable synchronisation, even if it's available + int no_mmap; // disable use of mmap-based output, even if it's available + double resync_threshold; // if it gets out of whack by more than this number of seconds, do a + // resync. if zero, never do a resync. + double resync_recovery_time; // if sync is late, drop the delay but also drop the following frames + // up to the resync_recovery_time int allow_session_interruption; int timeout; // while in play mode, exit if no packets of audio come in for more than this number // of seconds . Zero means never exit. @@ -313,12 +322,9 @@ typedef struct { char *airplay_pi; // UUID in the Bonjour advertisement and the GETINFO Plist char *nqptp_shared_memory_interface_name; // client name for nqptp service #endif + int unfixable_error_reported; // only report once. } shairport_cfg; -// accessors to config for multi-thread access -double get_config_airplay_volume(); -void set_config_airplay_volume(double v); - uint32_t nctohl(const uint8_t *p); // read 4 characters from *p and do ntohl on them uint16_t nctohs(const uint8_t *p); // read 2 characters from *p and do ntohs on them uint64_t nctoh64(const uint8_t *p); // read 8 characters from *p to a uint64_t diff --git a/configure.ac b/configure.ac index 8d3a1563..1e58765f 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], [4.1.1], [4265913+mikebrady@users.noreply.github.com]) +AC_INIT([shairport-sync], [4.2rc0], [4265913+mikebrady@users.noreply.github.com]) AM_INIT_AUTOMAKE([subdir-objects]) AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) @@ -408,27 +408,72 @@ if test "x$with_airplay_2" = "xyes" ; then AC_DEFINE([CONFIG_AIRPLAY_2], 1, [Build for AirPlay 2]) AC_CHECK_PROG(XXD_CHECK,xxd,yes) AS_IF([test x"$XXD_CHECK" != x"yes"], [AC_MSG_ERROR([xxd can not be found. Please install xxd for building for AirPlay 2.])]) - PKG_CHECK_MODULES([libplist], [libplist >= 2.0.0],[CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}"],[ - PKG_CHECK_MODULES([libplist], [libplist-2.0 >= 2.0.0],[CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}"],[ - AC_MSG_ERROR(AirPlay 2 support requires libplist 2.0.0 or later -- search for pkg libplist-dev on Debian or libplist-2.2.0 or later on FreeBSD!) - ]) + LIBPLIST_PACKAGE=libplist + PKG_CHECK_EXISTS(libplist-2.0, LIBPLIST_PACKAGE=libplist-2.0) + PKG_CHECK_MODULES( + [libplist], [${LIBPLIST_PACKAGE} >= 2.3.0], + [CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}" AC_DEFINE([HAVE_LIBPLIST_GE_2_3_0], 1, [libplist >= 2.3.0])], + [PKG_CHECK_MODULES( + [libplist], [${LIBPLIST_PACKAGE} >= 2.0.0], + [CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}"], + [AC_MSG_ERROR(AirPlay 2 support requires libplist 2.0.0 or later -- search for pkg libplist-dev on Debian or libplist-2.2.0 or later on FreeBSD!)] + )] + ) + PKG_CHECK_MODULES( + [libsodium], [libsodium], + [CFLAGS="${libsodium_CFLAGS} ${CFLAGS}" LIBS="${libsodium_LIBS} ${LIBS}"], + [AC_MSG_ERROR(AirPlay 2 support requires libsodium -- libsodium-dev suggested)] + ) + PKG_CHECK_MODULES( + [libgcrypt], [libgcrypt], + [CFLAGS="${libgcrypt_CFLAGS} ${CFLAGS}" LIBS="${libgcrypt_LIBS} ${LIBS}"], + [AC_CHECK_LIB( + [gcrypt], [gcry_control], + [], + [AC_MSG_ERROR([Airplay 2 support requires libgcrypt -- libgcrypt-dev suggested])] + ) ]) - PKG_CHECK_MODULES([libsodium], [libsodium],[CFLAGS="${libsodium_CFLAGS} ${CFLAGS}" LIBS="${libsodium_LIBS} ${LIBS}"],[AC_MSG_ERROR(AirPlay 2 support requires libsodium -- libsodium-dev suggested)]) - PKG_CHECK_MODULES([libgcrypt], [libgcrypt],[CFLAGS="${libgcrypt_CFLAGS} ${CFLAGS}" LIBS="${libgcrypt_LIBS} ${LIBS}"],[ - AC_CHECK_LIB([gcrypt], [gcry_control], [], [ - AC_MSG_ERROR([Airplay 2 support requires libgcrypt -- libgcrypt-dev suggested]) - ]) - ]) - PKG_CHECK_MODULES([libavutil], [libavutil],[CFLAGS="${libavutil_CFLAGS} ${CFLAGS}" LIBS="${libavutil_LIBS} ${LIBS}"],[ - AC_MSG_ERROR(AirPlay 2 support requires libavutil -- libavutil-dev suggested)]) - PKG_CHECK_MODULES([libavcodec], [libavcodec],[CFLAGS="${libavcodec_CFLAGS} ${CFLAGS}" LIBS="${libavcodec_LIBS} ${LIBS}"],[ - AC_CHECK_LIB([avcodec], [avcodec_find_decoder], [], [ - AC_MSG_ERROR(AirPlay 2 support requires libavcodec -- libavcodec-dev suggested) - ]) - ]) - AC_CHECK_LIB([avformat],[avformat_new_stream], [], [AC_MSG_ERROR([AirPlay 2 support requires libavformat -- libavformat-dev suggested])]) - AC_CHECK_LIB([swresample],[swr_convert], [], [AC_MSG_ERROR([AirPlay 2 support requires libswresample -- libswresample-dev suggested])]) - AC_CHECK_LIB([uuid],[uuid_generate], [], [AC_MSG_ERROR([AirPlay 2 support requires the uuid library -- uuid-dev suggested])]) + PKG_CHECK_MODULES( + [libavutil], [libavutil], + [CFLAGS="${libavutil_CFLAGS} ${CFLAGS}" LIBS="${libavutil_LIBS} ${LIBS}"], + [AC_MSG_ERROR(AirPlay 2 support requires libavutil -- libavutil-dev suggested)] + ) + PKG_CHECK_MODULES( + [libavcodec], [libavcodec], + [CFLAGS="${libavcodec_CFLAGS} ${CFLAGS}" LIBS="${libavcodec_LIBS} ${LIBS}"], + [AC_CHECK_LIB( + [avcodec], [avcodec_find_decoder], + [], + [AC_MSG_ERROR(AirPlay 2 support requires libavcodec -- libavcodec-dev suggested)] + )] + ) + PKG_CHECK_MODULES( + [libavformat], [libavformat], + [CFLAGS="${libavformat_CFLAGS} ${CFLAGS}" LIBS="${libavformat_LIBS} ${LIBS}"], + [AC_CHECK_LIB( + [avformat], [avformat_new_stream], + [], + [AC_MSG_ERROR([AirPlay 2 support requires libavformat -- libavformat-dev suggested])] + )] + ) + PKG_CHECK_MODULES( + [libswresample], [libswresample], + [CFLAGS="${libswresample_CFLAGS} ${CFLAGS}" LIBS="${libswresample_LIBS} ${LIBS}"], + [AC_CHECK_LIB( + [swresample], [swr_convert], + [], + [AC_MSG_ERROR([AirPlay 2 support requires libswresample -- libswresample-dev suggested])] + )] + ) + PKG_CHECK_MODULES( + [libuuid], [uuid], + [CFLAGS="${libuuid_CFLAGS} ${CFLAGS}" LIBS="${libuuid_LIBS} ${LIBS}"], + [AC_CHECK_LIB( + [uuid], [uuid_generate], + [], + [AC_MSG_ERROR([AirPlay 2 support requires the uuid library -- uuid-dev suggested])] + )] + ) fi AM_CONDITIONAL([USE_AIRPLAY_2], [test "x$with_airplay_2" = "xyes"]) diff --git a/dbus-service.c b/dbus-service.c index 4cfc8254..6a3b544f 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -111,7 +111,8 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) th = shairport_sync_get_first_frame_position(shairportSyncSkeleton); if ((th == NULL) || (strcasecmp(th, argc->first_frame_position_string) != 0)) { // debug(1, "First frame position string should be changed"); - shairport_sync_set_first_frame_position(shairportSyncSkeleton, argc->first_frame_position_string); + shairport_sync_set_first_frame_position(shairportSyncSkeleton, + argc->first_frame_position_string); } } @@ -635,11 +636,14 @@ gboolean notify_volume_callback(ShairportSync *skeleton, debug(2, ">> setting volume to %7.4f.", iv); lock_player(); - config.airplay_volume = iv; - if (playing_conn != NULL) + if (playing_conn != NULL) { player_volume(iv, playing_conn); + playing_conn->own_airplay_volume = iv; + playing_conn->own_airplay_volume_set = 1; + } unlock_player(); - + config.airplay_volume = iv; + config.last_access_to_volume_info_time = get_absolute_time_in_ns(); } else { debug(1, ">> invalid volume: %f. Ignored.", iv); shairport_sync_set_volume(skeleton, config.airplay_volume); diff --git a/docker/Dockerfile b/docker/Dockerfile index 5a6bc678..1dc34141 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.12 AS builder +FROM alpine:3.17 AS builder # Check required arguments exist. These will be provided by the Github Action # Workflow and are required to ensure the correct branches are being used. @@ -8,26 +8,26 @@ ARG NQPTP_BRANCH RUN test -n "$NQPTP_BRANCH" RUN apk -U add \ - git \ - build-base \ + alsa-lib-dev \ autoconf \ automake \ - libtool \ - dbus \ - alsa-lib-dev \ - popt-dev \ - soxr-dev \ avahi-dev \ - libconfig-dev \ - libsndfile-dev \ - mosquitto-dev \ - libsodium-dev \ - libgcrypt-dev \ + build-base \ + dbus \ ffmpeg-dev \ - xxd \ + git \ + libconfig-dev \ + libgcrypt-dev \ + libplist-dev \ libressl-dev \ - openssl-dev \ - libplist-dev + libsndfile-dev \ + libsodium-dev \ + libtool \ + mosquitto-dev \ + popt-dev \ + pulseaudio-dev \ + soxr-dev \ + xxd ##### ALAC ##### RUN git clone https://github.com/mikebrady/alac @@ -56,8 +56,8 @@ COPY . . RUN git checkout "$SHAIRPORT_SYNC_BRANCH" WORKDIR /shairport-sync/build RUN autoreconf -i ../ -RUN ../configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-airplay-2 \ - --with-metadata --with-dummy --with-pipe --with-dbus-interface \ +RUN ../configure --sysconfdir=/etc --with-alsa --with-pa --with-soxr --with-avahi --with-ssl=openssl \ + --with-airplay-2 --with-metadata --with-dummy --with-pipe --with-dbus-interface \ --with-stdout --with-mpris-interface --with-mqtt-client \ --with-apple-alac --with-convolution RUN make -j $(nproc) @@ -66,28 +66,33 @@ WORKDIR / ##### SPS END ##### # Shairport Sync Runtime System -FROM crazymax/alpine-s6:3.12-3.1.1.2 +FROM crazymax/alpine-s6:3.17-3.1.1.2 + +ENV S6_CMD_WAIT_FOR_SERVICES=1 +ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 RUN apk -U add \ alsa-lib \ - dbus \ - popt \ - glib \ - soxr \ avahi \ avahi-tools \ - libconfig \ - libsndfile \ - mosquitto \ - libuuid \ + dbus \ ffmpeg \ - libsodium \ + glib \ + less \ + less-doc \ + libconfig \ libgcrypt \ libplist \ - mandoc \ + libpulse \ + libressl3.6-libcrypto \ + libsndfile \ + libsodium \ + libuuid \ man-pages \ - less \ - less-doc + mandoc \ + mosquitto \ + popt \ + soxr # Copy build files. COPY --from=builder /shairport-sync/build/install/usr/local/bin/shairport-sync /usr/local/bin/shairport-sync @@ -100,7 +105,8 @@ COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport- COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ COPY ./docker/etc/s6-overlay/s6-rc.d /etc/s6-overlay/s6-rc.d -RUN chmod +x /etc/s6-overlay/s6-rc.d/startup/script.sh +COPY ./docker/etc/pulse /etc/pulse +RUN chmod +x /etc/s6-overlay/s6-rc.d/01-startup/script.sh # Create non-root user for running the container -- running as the user 'shairport-sync' also allows # Shairport Sync to provide the D-Bus and MPRIS interfaces within the container @@ -114,4 +120,8 @@ RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgr # Remove anything we don't need. RUN rm -rf /lib/apk/db/* -ENTRYPOINT [ "/init", "/usr/local/bin/shairport-sync" ] +# Add run script that will start SPS +COPY ./docker/run.sh ./run.sh +RUN chmod +x /run.sh + +Entrypoint ["/init","./run.sh"] diff --git a/docker/README.md b/docker/README.md index 295d2170..6caf1c4d 100644 --- a/docker/README.md +++ b/docker/README.md @@ -32,6 +32,7 @@ $ docker run -d --restart unless-stopped --net host --device /dev/snd \ ### Options Command line options will be passed to Shairport Sync. Here is an example: + ``` $ docker run -d --restart unless-stopped --net host --device /dev/snd \ mikebrady/shairport-sync:latest \ @@ -39,6 +40,8 @@ $ docker run -d --restart unless-stopped --net host --device /dev/snd \ ``` This will send audio to alsa hardware device `hw:0` and make use of the that device's mixer control called `PCM`. The service will be visible as `DenSystem` on the network. +The image is built with PulseAudio backend support. To use it, refer to [`docker-compose.yaml`](docker-compose.yaml) for required environment variables and mounts. You might need to adjust authentication on your PulseAudio server ([PA documentation](https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/Modules/#module-native-protocol-unixtcp)) and set default backend to `pa` via either command line option `-o` or `general.output_backend` field in config file. + ## Configuration File To get access to the full range of configuration options, pass the configuration file to `/etc/shairport-sync.conf` in the container using the `-v` option or docker compose. diff --git a/docker/classic/Dockerfile b/docker/classic/Dockerfile index 03baac5e..9f5c0fe6 100644 --- a/docker/classic/Dockerfile +++ b/docker/classic/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.12 AS builder +FROM alpine:3.17 AS builder # Classic (aka AirPlay 1) Build @@ -8,20 +8,21 @@ ARG SHAIRPORT_SYNC_BRANCH RUN test -n "$SHAIRPORT_SYNC_BRANCH" RUN apk -U add \ - git \ - build-base \ + alsa-lib-dev \ autoconf \ automake \ - libtool \ - dbus \ - alsa-lib-dev \ - popt-dev \ - mbedtls-dev \ - soxr-dev \ avahi-dev \ + build-base \ + dbus \ + git \ libconfig-dev \ + libsndfile-dev \ + libtool \ + mbedtls-dev \ mosquitto-dev \ - libsndfile-dev + popt-dev \ + pulseaudio-dev \ + soxr-dev ##### ALAC ##### RUN git clone https://github.com/mikebrady/alac @@ -39,7 +40,7 @@ COPY . . RUN git checkout "$SHAIRPORT_SYNC_BRANCH" WORKDIR /shairport-sync/build RUN autoreconf -i ../ -RUN ../configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=mbedtls \ +RUN ../configure --sysconfdir=/etc --with-alsa --with-pa --with-soxr --with-avahi --with-ssl=mbedtls \ --with-metadata --with-dummy --with-pipe --with-dbus-interface \ --with-stdout --with-mpris-interface --with-mqtt-client \ --with-apple-alac --with-convolution @@ -49,24 +50,28 @@ WORKDIR / ##### SPS END ##### # Shairport Sync Runtime System -FROM crazymax/alpine-s6:3.12-3.1.1.2 +FROM crazymax/alpine-s6:3.17-3.1.1.2 + +ENV S6_CMD_WAIT_FOR_SERVICES=1 +ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 RUN apk -U add \ alsa-lib \ - dbus \ - popt \ - glib \ - mbedtls \ - soxr \ avahi \ avahi-tools \ - libconfig \ - mosquitto \ - libsndfile-dev \ - mandoc \ - man-pages \ + dbus \ + glib \ less \ - less-doc + less-doc \ + libconfig \ + libsndfile-dev \ + libpulse \ + man-pages \ + mandoc \ + mbedtls \ + mosquitto \ + popt \ + soxr # Copy build files. COPY --from=builder /shairport-sync/build/install/usr/local/bin/shairport-sync /usr/local/bin/shairport-sync @@ -78,7 +83,7 @@ COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport- COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ COPY ./docker/classic/etc/s6-overlay/s6-rc.d /etc/s6-overlay/s6-rc.d -RUN chmod +x /etc/s6-overlay/s6-rc.d/startup/script.sh +RUN chmod +x /etc/s6-overlay/s6-rc.d/01-startup/script.sh # Create non-root user for running the container -- running as the user 'shairport-sync' also allows # Shairport Sync to provide the D-Bus and MPRIS interfaces within the container @@ -92,4 +97,8 @@ RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgr # Remove anything we don't need. RUN rm -rf /lib/apk/db/* -ENTRYPOINT [ "/init", "/usr/local/bin/shairport-sync" ] +# Add run script that will start SPS +COPY ./docker/run.sh ./run.sh +RUN chmod +x /run.sh + +Entrypoint ["/init","./run.sh"] diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/01-dbus/run b/docker/classic/etc/s6-overlay/s6-rc.d/01-dbus/run deleted file mode 100644 index bf1cb501..00000000 --- a/docker/classic/etc/s6-overlay/s6-rc.d/01-dbus/run +++ /dev/null @@ -1,4 +0,0 @@ -#!/command/with-contenv sh -rm -rf /var/run/dbus.pid -echo "Starting dbus" -exec dbus-daemon --system --nofork diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/dependencies b/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/dependencies new file mode 100644 index 00000000..df967b96 --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/dependencies @@ -0,0 +1 @@ +base diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/startup/script.sh b/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/script.sh similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/startup/script.sh rename to docker/classic/etc/s6-overlay/s6-rc.d/01-startup/script.sh diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/startup/type b/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/type similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/startup/type rename to docker/classic/etc/s6-overlay/s6-rc.d/01-startup/type diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/up b/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/up new file mode 100644 index 00000000..0521bff8 --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/01-startup/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/01-startup/script.sh diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/run b/docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/run deleted file mode 100644 index 2f6be7b4..00000000 --- a/docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/run +++ /dev/null @@ -1,8 +0,0 @@ -#!/command/with-contenv sh -sleep 5 -while [ ! -f /var/run/dbus.pid ]; do - "dbus is not running, sleeping for 5 seconds before trying to start avahi" - sleep 5 -done -echo "Starting avahi" -exec avahi-daemon --no-chroot diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/data/check b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/data/check new file mode 100755 index 00000000..39eeac11 --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/data/check @@ -0,0 +1,2 @@ +#!/bin/sh +exec dbus-send --system / org.freedesktop.DBus.Peer.Ping > /dev/null 2> /dev/null diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/dependencies b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/dependencies new file mode 100644 index 00000000..f04b211f --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/dependencies @@ -0,0 +1 @@ +01-startup diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/01-dbus/finish b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/finish similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/01-dbus/finish rename to docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/finish diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd new file mode 100644 index 00000000..00750edc --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd @@ -0,0 +1 @@ +3 diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/run b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/run new file mode 100644 index 00000000..15018b7e --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/run @@ -0,0 +1,3 @@ +#!/command/with-contenv sh +echo "Starting dbus" +exec s6-notifyoncheck dbus-daemon --system --nofork --nopidfile diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/01-dbus/type b/docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/type similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/01-dbus/type rename to docker/classic/etc/s6-overlay/s6-rc.d/02-dbus/type diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/data/check b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/data/check new file mode 100755 index 00000000..ccb833b6 --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/data/check @@ -0,0 +1,10 @@ +#!/bin/sh +state="$(dbus-send --system --dest=org.freedesktop.Avahi --print-reply / org.freedesktop.Avahi.Server.GetState | grep int32 | awk '{printf $2}')" + +# Avahi will return 'state=2' when 'Server startup complete' +if [ "$state" = 2 ]; then + exit 0 +else + exit 1 +fi + diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/dependencies b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/dependencies new file mode 100644 index 00000000..497f42ec --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/dependencies @@ -0,0 +1 @@ +02-dbus diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/finish b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/finish similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/finish rename to docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/finish diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd new file mode 100644 index 00000000..00750edc --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd @@ -0,0 +1 @@ +3 diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run new file mode 100644 index 00000000..edb5e74b --- /dev/null +++ b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/run @@ -0,0 +1,3 @@ +#!/command/with-contenv sh +echo "Starting avahi" +exec s6-notifyoncheck avahi-daemon --no-chroot diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/type b/docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/type similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/02-avahi/type rename to docker/classic/etc/s6-overlay/s6-rc.d/03-avahi/type diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/startup/up b/docker/classic/etc/s6-overlay/s6-rc.d/startup/up deleted file mode 100644 index 06df8350..00000000 --- a/docker/classic/etc/s6-overlay/s6-rc.d/startup/up +++ /dev/null @@ -1 +0,0 @@ -/etc/s6-overlay/s6-rc.d/startup/script.sh diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/01-dbus b/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/01-startup similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/01-dbus rename to docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/01-startup diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/02-avahi b/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/02-dbus similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/02-avahi rename to docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/02-dbus diff --git a/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/startup b/docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/03-avahi similarity index 100% rename from docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/startup rename to docker/classic/etc/s6-overlay/s6-rc.d/user/contents.d/03-avahi diff --git a/docker/classic/run.sh b/docker/classic/run.sh new file mode 100644 index 00000000..f901817d --- /dev/null +++ b/docker/classic/run.sh @@ -0,0 +1,8 @@ +#!/bin/sh +while [ ! -f /var/run/avahi-daemon/pid ]; do + echo "Warning: avahi is not running, sleeping for 1 second before trying to start shairport-sync" + sleep 1 +done +echo "Starting shairport-sync" +# pass all commandline options to shairport-sync +/usr/local/bin/shairport-sync "$@" diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 88831298..563622d6 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -5,10 +5,14 @@ services: image: mikebrady/shairport-sync:latest network_mode: host restart: unless-stopped + # environment: + # PULSE_SERVER: unix:/tmp/pulseaudio.socket # Path for PulseAudio socket + # PULSE_COOKIE: /tmp/pulseaudio.cookie # Path for PulseAudio cookie devices: - - "/dev/snd" + - "/dev/snd" # ALSA device # volumes: # - ./volumes/shairport-sync/shairport-sync.conf:/etc/shairport-sync.conf # Customised Shairport Sync configuration file. + # - /run/user/1000/pulse/native:/tmp/pulseaudio.socket # PulseAudio socket when using that backend logging: options: max-size: "200k" diff --git a/docker/etc/pulse/client.conf b/docker/etc/pulse/client.conf new file mode 100644 index 00000000..5fbb34d1 --- /dev/null +++ b/docker/etc/pulse/client.conf @@ -0,0 +1,6 @@ +default-server = unix:/tmp/pulseaudio.socket +# Prevent a server running in the container +autospawn = no +daemon-binary = /bin/true +# Prevent the use of shared memory +enable-shm = false diff --git a/docker/etc/s6-overlay/s6-rc.d/01-dbus/run b/docker/etc/s6-overlay/s6-rc.d/01-dbus/run deleted file mode 100644 index bf1cb501..00000000 --- a/docker/etc/s6-overlay/s6-rc.d/01-dbus/run +++ /dev/null @@ -1,4 +0,0 @@ -#!/command/with-contenv sh -rm -rf /var/run/dbus.pid -echo "Starting dbus" -exec dbus-daemon --system --nofork diff --git a/docker/etc/s6-overlay/s6-rc.d/01-startup/dependencies b/docker/etc/s6-overlay/s6-rc.d/01-startup/dependencies new file mode 100644 index 00000000..df967b96 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/01-startup/dependencies @@ -0,0 +1 @@ +base diff --git a/docker/etc/s6-overlay/s6-rc.d/startup/script.sh b/docker/etc/s6-overlay/s6-rc.d/01-startup/script.sh similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/startup/script.sh rename to docker/etc/s6-overlay/s6-rc.d/01-startup/script.sh diff --git a/docker/etc/s6-overlay/s6-rc.d/startup/type b/docker/etc/s6-overlay/s6-rc.d/01-startup/type similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/startup/type rename to docker/etc/s6-overlay/s6-rc.d/01-startup/type diff --git a/docker/etc/s6-overlay/s6-rc.d/01-startup/up b/docker/etc/s6-overlay/s6-rc.d/01-startup/up new file mode 100644 index 00000000..0521bff8 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/01-startup/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/01-startup/script.sh diff --git a/docker/etc/s6-overlay/s6-rc.d/02-avahi/run b/docker/etc/s6-overlay/s6-rc.d/02-avahi/run deleted file mode 100644 index 2f6be7b4..00000000 --- a/docker/etc/s6-overlay/s6-rc.d/02-avahi/run +++ /dev/null @@ -1,8 +0,0 @@ -#!/command/with-contenv sh -sleep 5 -while [ ! -f /var/run/dbus.pid ]; do - "dbus is not running, sleeping for 5 seconds before trying to start avahi" - sleep 5 -done -echo "Starting avahi" -exec avahi-daemon --no-chroot diff --git a/docker/etc/s6-overlay/s6-rc.d/02-dbus/data/check b/docker/etc/s6-overlay/s6-rc.d/02-dbus/data/check new file mode 100755 index 00000000..39eeac11 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/02-dbus/data/check @@ -0,0 +1,2 @@ +#!/bin/sh +exec dbus-send --system / org.freedesktop.DBus.Peer.Ping > /dev/null 2> /dev/null diff --git a/docker/etc/s6-overlay/s6-rc.d/02-dbus/dependencies b/docker/etc/s6-overlay/s6-rc.d/02-dbus/dependencies new file mode 100644 index 00000000..f04b211f --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/02-dbus/dependencies @@ -0,0 +1 @@ +01-startup diff --git a/docker/etc/s6-overlay/s6-rc.d/01-dbus/finish b/docker/etc/s6-overlay/s6-rc.d/02-dbus/finish similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/01-dbus/finish rename to docker/etc/s6-overlay/s6-rc.d/02-dbus/finish diff --git a/docker/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd b/docker/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd new file mode 100644 index 00000000..00750edc --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/02-dbus/notification-fd @@ -0,0 +1 @@ +3 diff --git a/docker/etc/s6-overlay/s6-rc.d/02-dbus/run b/docker/etc/s6-overlay/s6-rc.d/02-dbus/run new file mode 100644 index 00000000..15018b7e --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/02-dbus/run @@ -0,0 +1,3 @@ +#!/command/with-contenv sh +echo "Starting dbus" +exec s6-notifyoncheck dbus-daemon --system --nofork --nopidfile diff --git a/docker/etc/s6-overlay/s6-rc.d/01-dbus/type b/docker/etc/s6-overlay/s6-rc.d/02-dbus/type similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/01-dbus/type rename to docker/etc/s6-overlay/s6-rc.d/02-dbus/type diff --git a/docker/etc/s6-overlay/s6-rc.d/03-avahi/data/check b/docker/etc/s6-overlay/s6-rc.d/03-avahi/data/check new file mode 100755 index 00000000..ccb833b6 --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/03-avahi/data/check @@ -0,0 +1,10 @@ +#!/bin/sh +state="$(dbus-send --system --dest=org.freedesktop.Avahi --print-reply / org.freedesktop.Avahi.Server.GetState | grep int32 | awk '{printf $2}')" + +# Avahi will return 'state=2' when 'Server startup complete' +if [ "$state" = 2 ]; then + exit 0 +else + exit 1 +fi + diff --git a/docker/etc/s6-overlay/s6-rc.d/03-avahi/dependencies b/docker/etc/s6-overlay/s6-rc.d/03-avahi/dependencies new file mode 100644 index 00000000..497f42ec --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/03-avahi/dependencies @@ -0,0 +1 @@ +02-dbus diff --git a/docker/etc/s6-overlay/s6-rc.d/02-avahi/finish b/docker/etc/s6-overlay/s6-rc.d/03-avahi/finish similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/02-avahi/finish rename to docker/etc/s6-overlay/s6-rc.d/03-avahi/finish diff --git a/docker/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd b/docker/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd new file mode 100644 index 00000000..00750edc --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/03-avahi/notification-fd @@ -0,0 +1 @@ +3 diff --git a/docker/etc/s6-overlay/s6-rc.d/03-avahi/run b/docker/etc/s6-overlay/s6-rc.d/03-avahi/run new file mode 100644 index 00000000..edb5e74b --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/03-avahi/run @@ -0,0 +1,3 @@ +#!/command/with-contenv sh +echo "Starting avahi" +exec s6-notifyoncheck avahi-daemon --no-chroot diff --git a/docker/etc/s6-overlay/s6-rc.d/02-avahi/type b/docker/etc/s6-overlay/s6-rc.d/03-avahi/type similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/02-avahi/type rename to docker/etc/s6-overlay/s6-rc.d/03-avahi/type diff --git a/docker/etc/s6-overlay/s6-rc.d/04-nqptp/dependencies b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/dependencies new file mode 100644 index 00000000..f04b211f --- /dev/null +++ b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/dependencies @@ -0,0 +1 @@ +01-startup diff --git a/docker/etc/s6-overlay/s6-rc.d/03-nqptp/finish b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/finish similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/03-nqptp/finish rename to docker/etc/s6-overlay/s6-rc.d/04-nqptp/finish diff --git a/docker/etc/s6-overlay/s6-rc.d/03-nqptp/run b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/run similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/03-nqptp/run rename to docker/etc/s6-overlay/s6-rc.d/04-nqptp/run diff --git a/docker/etc/s6-overlay/s6-rc.d/03-nqptp/type b/docker/etc/s6-overlay/s6-rc.d/04-nqptp/type similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/03-nqptp/type rename to docker/etc/s6-overlay/s6-rc.d/04-nqptp/type diff --git a/docker/etc/s6-overlay/s6-rc.d/startup/up b/docker/etc/s6-overlay/s6-rc.d/startup/up deleted file mode 100644 index 06df8350..00000000 --- a/docker/etc/s6-overlay/s6-rc.d/startup/up +++ /dev/null @@ -1 +0,0 @@ -/etc/s6-overlay/s6-rc.d/startup/script.sh diff --git a/docker/etc/s6-overlay/s6-rc.d/user/contents.d/01-dbus b/docker/etc/s6-overlay/s6-rc.d/user/contents.d/01-startup similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/user/contents.d/01-dbus rename to docker/etc/s6-overlay/s6-rc.d/user/contents.d/01-startup diff --git a/docker/etc/s6-overlay/s6-rc.d/user/contents.d/02-avahi b/docker/etc/s6-overlay/s6-rc.d/user/contents.d/02-dbus similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/user/contents.d/02-avahi rename to docker/etc/s6-overlay/s6-rc.d/user/contents.d/02-dbus diff --git a/docker/etc/s6-overlay/s6-rc.d/user/contents.d/03-nqptp b/docker/etc/s6-overlay/s6-rc.d/user/contents.d/03-avahi similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/user/contents.d/03-nqptp rename to docker/etc/s6-overlay/s6-rc.d/user/contents.d/03-avahi diff --git a/docker/etc/s6-overlay/s6-rc.d/user/contents.d/startup b/docker/etc/s6-overlay/s6-rc.d/user/contents.d/04-nqptp similarity index 100% rename from docker/etc/s6-overlay/s6-rc.d/user/contents.d/startup rename to docker/etc/s6-overlay/s6-rc.d/user/contents.d/04-nqptp diff --git a/docker/run.sh b/docker/run.sh new file mode 100644 index 00000000..f901817d --- /dev/null +++ b/docker/run.sh @@ -0,0 +1,8 @@ +#!/bin/sh +while [ ! -f /var/run/avahi-daemon/pid ]; do + echo "Warning: avahi is not running, sleeping for 1 second before trying to start shairport-sync" + sleep 1 +done +echo "Starting shairport-sync" +# pass all commandline options to shairport-sync +/usr/local/bin/shairport-sync "$@" diff --git a/metadata_hub.c b/metadata_hub.c index e39752fe..796e0927 100644 --- a/metadata_hub.c +++ b/metadata_hub.c @@ -598,7 +598,8 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin if (string_update(&metadata_store.first_frame_position_string, &metadata_store.first_frame_position_string_changed, cs)) { changed = 1; - debug(2, "MH First Frame Position String set to: \"%s\"", metadata_store.first_frame_position_string); + debug(2, "MH First Frame Position String set to: \"%s\"", + metadata_store.first_frame_position_string); } free(cs); break; diff --git a/metadata_hub.h b/metadata_hub.h index 8235c6e4..94b3acaa 100644 --- a/metadata_hub.h +++ b/metadata_hub.h @@ -48,7 +48,7 @@ typedef struct metadata_bundle { char *server_ip; // IP number used by Shairport Sync int server_ip_changed; - + char *stream_type; // Realtime or Buffered int stream_type_changed; @@ -79,7 +79,7 @@ typedef struct metadata_bundle { char *cover_art_pathname; int cover_art_pathname_changed; - + uint64_t item_id; // seems to be a track ID -- see itemid in DACP.c int item_id_changed; int item_id_is_valid; @@ -89,11 +89,10 @@ typedef struct metadata_bundle { int item_composite_id_changed; int item_composite_id_is_valid; - int song_data_kind; int song_data_kind_changed; int song_data_kind_is_valid; - + char *track_name; int track_name_changed; @@ -139,7 +138,6 @@ typedef struct metadata_bundle { uint32_t songtime_in_milliseconds; int songtime_in_milliseconds_changed; int songtime_in_milliseconds_is_valid; - // end diff --git a/nqptp-shm-structures.h b/nqptp-shm-structures.h index 3dbe0c90..251d06c1 100644 --- a/nqptp-shm-structures.h +++ b/nqptp-shm-structures.h @@ -22,26 +22,36 @@ #define NQPTP_INTERFACE_NAME "/nqptp" -#define NQPTP_SHM_STRUCTURES_VERSION 8 +#define NQPTP_SHM_STRUCTURES_VERSION 9 #define NQPTP_CONTROL_PORT 9000 -// The control port expects a UDP packet with the first space-delimited string -// being the name of the shared memory interface (SMI) to be used. -// This allows client applications to have a dedicated named SMI interface with -// a timing peer list independent of other clients. -// The name given must be a valid SMI name and must contain no spaces. -// If the named SMI interface doesn't exist it will be created by NQPTP. -// The SMI name should be delimited by a space and followed by a command letter. -// At present, the only command is "T", which must followed by nothing or by -// a space and a space-delimited list of IPv4 or IPv6 numbers, -// the whole not to exceed 4096 characters in total. +// The control port expects a UDP packet with the first character being a command letter +// and the rest being any arguments, the whole not to exceed 4096 characters. +// The "T" command, must followed by nothing or by +// a space and a space-delimited list of IPv4 or IPv6 numbers. // The IPs, if provided, will become the new list of timing peers, replacing any -// previous list. If the master clock of the new list is the same as that of the old list, -// the master clock is retained without resynchronisation; this means that non-master -// devices can be added and removed without disturbing the SMI's existing master clock. +// previous list. The first IP number is the clock that NQPTP will listen to. +// The remaining IP address are the addresses of all the timing peers. The timing peers +// are not used in this version of NQPTP. // If no timing list is provided, the existing timing list is deleted. -// (In future version of NQPTP the SMI interface may also be deleted at this point.) -// SMI interfaces are not currently deleted or garbage collected. +// The "B" command is a message that the client -- which generates the clock -- +// is about to start playing. +// NQPTP uses it to determine that the clock is active and will not sleep. +// The "E" command signifies that the client has stopped playing and that +// the clock may shortly sleep. +// The "P" command signifies that SPS has paused play (buffered audio only). +// The clock seems to stay running in this state. + +// When the clock is active, it is assumed that any decreases in the offset +// between the local and remote clocks are due to delays in the network. +// NQPTP smooths the offset by clamping any decreases to a small value. +// In this way, it can follow clock drift but ignore network delays. + +// When the clock is inactive, it can stop running. This causes the offset to decrease. +// NQPTP clock smoothing would treat this as a network delay, causing true sync to be lost. +// To avoid this, when the clock goes from inactive to active, +// NQPTP resets clock smoothing to the new offset. + #include #include diff --git a/player.c b/player.c index 61b4e5ff..9d52c6ec 100644 --- a/player.c +++ b/player.c @@ -4,7 +4,7 @@ * All rights reserved. * * Modifications for audio synchronisation, AirPlay 2 - * and related work, copyright (c) Mike Brady 2014 -- 2022 + * and related work, copyright (c) Mike Brady 2014 -- 2023 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -106,7 +106,7 @@ int64_t first_frame_early_bias = 8; // default buffer size // needs to be a power of 2 because of the way BUFIDX(seqno) works -//#define BUFFER_FRAMES 512 +// #define BUFFER_FRAMES 512 #define MAX_PACKET 2048 // DAC buffer occupancy stuff @@ -1271,14 +1271,13 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) { } else { if (resp == sps_extra_code_output_stalled) { - if (conn->unfixable_error_reported == 0) { - conn->unfixable_error_reported = 1; + if (config.unfixable_error_reported == 0) { + config.unfixable_error_reported = 1; if (config.cmd_unfixable) { command_execute(config.cmd_unfixable, "output_device_stalled", 1); } else { die("an unrecoverable error, \"output_device_stalled\", has been " - "detected.", - conn->connection_number); + "detected."); } } } else { @@ -1703,6 +1702,39 @@ void statistics_item(const char *heading, const char *format, ...) { statistics_column++; } +double suggested_volume(rtsp_conn_info *conn) { + double response = config.airplay_volume; + if ((conn != NULL) && (conn->own_airplay_volume_set != 0)) { + response = conn->own_airplay_volume; + } else if (config.airplay_volume > config.high_threshold_airplay_volume) { + int64_t volume_validity_time = config.limit_to_high_volume_threshold_time_in_minutes; + // zero means never check the volume + if (volume_validity_time != 0) { + // If the volume is higher than the high volume threshold + // and enough time has gone past, suggest the default volume. + uint64_t time_now = get_absolute_time_in_ns(); + int64_t time_since_last_access_to_volume_info = + time_now - config.last_access_to_volume_info_time; + + volume_validity_time = volume_validity_time * 60; // to seconds + volume_validity_time = volume_validity_time * 1000000000; // to nanoseconds + + if ((config.airplay_volume > config.high_threshold_airplay_volume) && + ((config.last_access_to_volume_info_time == 0) || + (time_since_last_access_to_volume_info > volume_validity_time))) { + + debug(2, + "the current volume %.6f is higher than the high volume threshold %.6f, so the " + "default volume %.6f is suggested.", + config.airplay_volume, config.high_threshold_airplay_volume, + config.default_airplay_volume); + response = config.default_airplay_volume; + } + } + } + return response; +} + void player_thread_cleanup_handler(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; if (pthread_mutex_trylock(&playing_conn_lock) == 0) { @@ -1838,7 +1870,8 @@ void player_thread_cleanup_handler(void *arg) { void *player_thread_func(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; #ifdef CONFIG_METADATA - uint64_t time_of_last_metadata_progress_update = 0; // the assignment is to stop a compiler warning... + uint64_t time_of_last_metadata_progress_update = + 0; // the assignment is to stop a compiler warning... #endif uint64_t previous_frames_played = 0; // initialised to avoid a "possibly uninitialised" warning uint64_t previous_raw_measurement_time = @@ -1990,7 +2023,7 @@ void *player_thread_func(void *arg) { conn->session_corrections = 0; conn->connection_state_to_output = get_requested_connection_state_to_output(); // this is about half a minute -//#define trend_interval 3758 +// #define trend_interval 3758 // this is about 8 seconds #define trend_interval 1003 @@ -2192,9 +2225,11 @@ void *player_thread_func(void *arg) { pthread_setcancelstate(oldState, NULL); - double initial_volume = config.airplay_volume; // default - // set the default volume to whatever it was before, as stored in the config airplay_volume - debug(2, "Set initial volume to %f.", initial_volume); + // if not already set, set the volume to the pending_airplay_volume, if any, or otherwise to the + // suggested volume. + + double initial_volume = suggested_volume(conn); + debug(2, "Set initial volume to %.6f.", initial_volume); player_volume(initial_volume, conn); // will contain a cancellation point if asked to wait debug(2, "Play begin"); @@ -2216,6 +2251,9 @@ void *player_thread_func(void *arg) { abuf_t *inframe = buffer_get_frame(conn); // this has cancellation point(s), but it's not // guaranteed that they'll always be executed uint64_t local_time_now = get_absolute_time_in_ns(); // types okay + config.last_access_to_volume_info_time = + local_time_now; // ensure volume info remains seen as valid + if (inframe) { inbuf = inframe->data; inbuflength = inframe->length; @@ -2629,8 +2667,8 @@ void *player_thread_func(void *arg) { } else { current_delay = 0; if ((resp == sps_extra_code_output_stalled) && - (conn->unfixable_error_reported == 0)) { - conn->unfixable_error_reported = 1; + (config.unfixable_error_reported == 0)) { + config.unfixable_error_reported = 1; if (config.cmd_unfixable) { warn("Connection %d: An unfixable error has been detected -- output device is " "stalled. Executing the " @@ -2645,8 +2683,9 @@ void *player_thread_func(void *arg) { conn->connection_number); } } else { - if (resp != -EBUSY) // delay errors can be reported if the device is (hopefully - // temporarily) busy + if ((resp != -EBUSY) && + (resp != -ENODEV)) // delay and not-there errors can be reported if the device + // is (hopefully temporarily) busy or unavailable debug(1, "Delay error %d when checking running latency.", resp); } } @@ -2780,7 +2819,8 @@ void *player_thread_func(void *arg) { send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); #endif if (config.statistics_requested) - inform("Connection %d: Playback started at frame %" PRId64 " -- Classic AirPlay (\"AirPlay 1\").", + inform("Connection %d: Playback started at frame %" PRId64 + " -- Classic AirPlay (\"AirPlay 1\").", conn->connection_number, inframe->given_timestamp); #endif } @@ -2790,8 +2830,8 @@ void *player_thread_func(void *arg) { abs_sync_error = -abs_sync_error; if ((config.no_sync == 0) && (inframe->given_timestamp != 0) && - (config.resyncthreshold > 0.0) && - (abs_sync_error > config.resyncthreshold * config.output_rate)) { + (config.resync_threshold > 0.0) && + (abs_sync_error > config.resync_threshold * config.output_rate)) { sync_error_out_of_bounds++; } else { sync_error_out_of_bounds = 0; @@ -2814,7 +2854,7 @@ void *player_thread_func(void *arg) { } int64_t filler_length = - (int64_t)(config.resyncthreshold * config.output_rate); // number of samples + (int64_t)(config.resync_threshold * config.output_rate); // number of samples if ((sync_error > 0) && (sync_error > filler_length)) { debug(1, "Large positive (i.e. late) sync error of %" PRId64 @@ -2830,9 +2870,9 @@ void *player_thread_func(void *arg) { int64_t source_frames_to_drop = sync_error; source_frames_to_drop = source_frames_to_drop / conn->output_sample_ratio; - // add some time to give the pipeline a chance to recover -- a bit hacky - double extra_time_to_drop = 0.1; // seconds - int64_t extra_frames_to_drop = (int64_t)(conn->input_rate * extra_time_to_drop); + // drop some extra frames to give the pipeline a chance to recover + int64_t extra_frames_to_drop = + (int64_t)(conn->input_rate * config.resync_recovery_time); source_frames_to_drop += extra_frames_to_drop; uint32_t frames_to_drop = source_frames_to_drop; @@ -3085,8 +3125,8 @@ void *player_thread_func(void *arg) { // timestamp of zero means an inserted silent frame in place of a missing frame /* if ((config.no_sync == 0) && (inframe->timestamp != 0) && - && (config.resyncthreshold > 0.0) && - (abs_sync_error > config.resyncthreshold * config.output_rate)) { + && (config.resync_threshold > 0.0) && + (abs_sync_error > config.resync_threshold * config.output_rate)) { sync_error_out_of_bounds++; // debug(1,"Sync error out of bounds: Error: %lld; previous error: %lld; DAC: %lld; // timestamp: %llx, time now @@ -3460,6 +3500,7 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c } // here, store the volume for possible use in the future config.airplay_volume = airplay_volume; + conn->own_airplay_volume = airplay_volume; debug_mutex_unlock(&conn->volume_control_mutex, 3); } @@ -3565,6 +3606,9 @@ int player_stop(rtsp_conn_info *conn) { // debuglev = 3; debug(3, "player_stop"); if (conn->player_thread) { +#ifdef CONFIG_AIRPLAY_2 + ptp_send_control_message_string("E"); // signify play is "E"nding +#endif debug(3, "player_thread cancel..."); pthread_cancel(*conn->player_thread); debug(3, "player_thread join..."); diff --git a/player.h b/player.h index 1c18567a..a35b54e6 100644 --- a/player.h +++ b/player.h @@ -168,8 +168,7 @@ typedef struct { volatile int stop; volatile int running; volatile uint64_t watchdog_bark_time; - volatile int watchdog_barks; // number of times the watchdog has timed out and done something - int unfixable_error_reported; // set when an unfixable error command has been executed. + volatile int watchdog_barks; // number of times the watchdog has timed out and done something uint64_t playstart; uint64_t connection_start_time; // the time the device is selected, which could be a long time @@ -225,8 +224,8 @@ typedef struct { pthread_mutex_t ab_mutex, flush_mutex, volume_control_mutex; int fix_volume; - double initial_airplay_volume; - int initial_airplay_volume_set; + double own_airplay_volume; + int own_airplay_volume_set; uint32_t timestamp_epoch, last_timestamp, maximum_timestamp_interval; // timestamp_epoch of zero means not initialised, could start at 2 @@ -449,4 +448,6 @@ int64_t monotonic_timestamp(uint32_t timestamp, // assumes, without checking, that successive timestamps in a series always span an interval of less // than one minute. +double suggested_volume(rtsp_conn_info *conn); // volume suggested for the connection + #endif //_PLAYER_H diff --git a/rtp.c b/rtp.c index e61d521c..7f9ebe83 100644 --- a/rtp.c +++ b/rtp.c @@ -1575,7 +1575,7 @@ void *rtp_event_receiver(void *arg) { debug(2, "Connection %d: AP2 Event Receiver started", conn->connection_number); pthread_cleanup_push(rtp_event_receiver_cleanup_handler, arg); - listen(conn->event_socket, 5); + // listen(conn->event_socket, 5); // this is now done in the handle_setup_2 code uint8_t packet[4096]; ssize_t nread; @@ -2017,7 +2017,8 @@ void *buffered_tcp_reader(void *arg) { pthread_cleanup_push(buffered_tcp_reader_cleanup_handler, NULL); buffered_tcp_desc *descriptor = (buffered_tcp_desc *)arg; - listen(descriptor->sock_fd, 5); + // listen(descriptor->sock_fd, 5); // this is done in the handle_setup_2 code to ensure it's open + // when the client hears about it... ssize_t nread; SOCKADDR remote_addr; memset(&remote_addr, 0, sizeof(remote_addr)); @@ -2059,8 +2060,7 @@ void *buffered_tcp_reader(void *arg) { // do the read // debug(1, "Request buffered read of up to %d bytes.", bytes_to_request); nread = recv(fd, descriptor->eoq, bytes_to_request, 0); - // debug(1, "Received %d bytes for a buffer size of %d bytes.",nread, - // descriptor->buffer_occupancy + nread); + // debug(1, "Received %d bytes for a buffer size of %d bytes.",nread, descriptor->buffer_occupancy + nread); if (pthread_mutex_lock(&descriptor->mutex) != 0) debug(1, "problem with not empty mutex"); pthread_cleanup_push(mutex_unlock, (void *)&descriptor->mutex); diff --git a/rtsp.c b/rtsp.c index c68feb43..02c63cce 100644 --- a/rtsp.c +++ b/rtsp.c @@ -3,7 +3,7 @@ * Copyright (c) James Laird 2013 * Modifications associated with audio synchronization, multithreading and - * metadata handling copyright (c) Mike Brady 2014-2022 + * metadata handling copyright (c) Mike Brady 2014-2023 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -47,7 +48,6 @@ #include #include #include -#include #include @@ -91,6 +91,10 @@ #include "plist/plist.h" #include "plist_xml_strings.h" #include "ptp-utilities.h" + +#ifdef HAVE_LIBPLIST_GE_2_3_0 +#define plist_from_memory(plist_data, length, plist) plist_from_memory((plist_data), (length), (plist), NULL) +#endif #endif #ifdef CONFIG_DBUS_INTERFACE @@ -665,8 +669,8 @@ void *player_watchdog_thread_code(void *arg) { 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; + if ((config.cmd_unfixable) && (config.unfixable_error_reported == 0)) { + config.unfixable_error_reported = 1; command_execute(config.cmd_unfixable, "unable_to_cancel_play_session", 1); } else { die("an unrecoverable error, \"unable_to_cancel_play_session\", has been detected.", @@ -739,10 +743,13 @@ void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_o debug_mutex_unlock(&conns_lock, 3); } +int old_connection_count = -1; + void cleanup_threads(void) { void *retval; int i; + int connection_count = 0; // debug(2, "culling threads."); debug_mutex_lock(&conns_lock, 1000000, 3); for (i = 0; i < nconns; i++) { @@ -754,8 +761,24 @@ void cleanup_threads(void) { free(conns[i]); conns[i] = NULL; } + if (conns[i] != NULL) { + debug(2, "Airplay Volume for connection %d is %.6f.", conns[i]->connection_number, + suggested_volume(conns[i])); + connection_count++; + } } debug_mutex_unlock(&conns_lock, 3); + + if (old_connection_count != connection_count) { + if (connection_count == 0) { + debug(2, "No active connections."); + } else if (connection_count == 1) + debug(2, "One active connection."); + else + debug(2, "%d active connections.", connection_count); + old_connection_count = connection_count; + } + debug(2, "Airplay Volume for new connections is %.6f.", suggested_volume(NULL)); } // park a null at the line ending, and return the next line pointer @@ -1223,10 +1246,10 @@ ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time uint64_t time_to_wait_to = get_absolute_time_in_ns(); ; time_to_wait_to = time_to_wait_to + wait_time; - + int flags = 1; if (setsockopt(conn->fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&flags, sizeof(flags))) { - debug(1,"can't enable keepalive checking on the RTSP socket"); + debug(1, "can't enable keepalive checking on the RTSP socket"); } // remaining_time will be zero if wait_time is zero @@ -1355,7 +1378,7 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes // Note: the socket will be closed when the thread exits goto shutdown; } - + // An ETIMEDOUT error usually means keepalive has failed. if (nread < 0) { @@ -1366,6 +1389,12 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes conn->connection_number); continue; } + if (errno == ETIMEDOUT) { + debug(1, "Connection %d: ETIMEDOUT -- keepalive timeout.", conn->connection_number); + reply = rtsp_read_request_response_channel_closed; + // Note: the socket will be closed when the thread exits + goto shutdown; + } if (errno != ECONNRESET) { char errorstring[1024]; strerror_r(errno, (char *)errorstring, sizeof(errorstring)); @@ -2003,6 +2032,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message pthread_cleanup_push(mutex_unlock, &conn->flush_mutex); conn->ap2_rate = rate; if ((rate & 1) != 0) { + ptp_send_control_message_string("B"); // signify clock dependability period is "B"eginning (or resuming) debug(2, "Connection %d: Start playing, with anchor clock %" PRIx64 ".", conn->connection_number, conn->networkTimeTimelineID); activity_monitor_signify_activity(1); @@ -2012,7 +2042,8 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message #endif conn->ap2_play_enabled = 1; } else { - debug(2, "Connection %d: Stop playing.", conn->connection_number); + ptp_send_control_message_string("P"); // signify play is "P"ausing + debug(2, "Connection %d: Pause playing.", conn->connection_number); conn->ap2_play_enabled = 0; activity_monitor_signify_activity(0); reset_anchor_info(conn); @@ -3051,6 +3082,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // debug(1,"initial timing peer command: \"%s\".", timing_list_message); // ptp_send_control_message_string(timing_list_message); set_client_as_ptp_clock(conn); + ptp_send_control_message_string("B"); // signify clock dependability period is "B"eginning (or continuing) plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); plist_dict_set_item(setupResponsePlist, "timingPeerInfo", timingPeerInfoPlist); @@ -3066,6 +3098,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, err); } + listen(conn->event_socket, 128); // ensure socket is open before telling client + debug(2, "Connection %d: TCP PTP event port opened: %u.", conn->connection_number, conn->local_event_port); @@ -3088,7 +3122,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) debug(2, "Connection %d: SETUP mdns_update on %s.", conn->connection_number, get_category_string(conn->airplay_stream_category)); mdns_update(NULL, secondary_txt_records); - resp->respcode = 200; } else { debug(1, "SETUP on Connection %d: PTP setup -- no timingPeerInfo plist.", @@ -3120,6 +3153,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, err); } + listen(conn->event_socket, 128); // ensure socket is open before telling client + debug(2, "Connection %d SETUP (RC): TCP Remote Control event port opened: %u.", conn->connection_number, conn->local_event_port); if (conn->rtp_event_thread != NULL) @@ -3162,6 +3197,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, get_category_string(conn->airplay_stream_category)); if (conn->airplay_stream_category == ptp_stream) { // get stream[0] + ptp_send_control_message_string("B"); // signify clock dependability period is "B"eginning (or continuing) plist_t stream0 = plist_array_get_item(streams, 0); plist_t streams_array = plist_new_array(); // to hold the ports and stuff @@ -3308,6 +3344,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) conn->connection_number, err); } + listen(conn->buffered_audio_socket, 128); // ensure it's open before telling the client + debug(2, "Connection %d: TCP Buffered Audio port opened: %u.", conn->connection_number, conn->local_buffered_audio_port); @@ -3334,7 +3372,6 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // call in the SETRATEANCHORI handler, which should come up right away activity_monitor_signify_activity(0); player_play(conn); - conn->rtp_running = 1; // hack! } break; default: @@ -3572,9 +3609,15 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, } else { #endif lock_player(); - if (playing_conn == conn) + if (playing_conn == conn) { player_volume(volume, conn); + } + if (conn != NULL) { + conn->own_airplay_volume = volume; + conn->own_airplay_volume_set = 1; + } unlock_player(); + config.last_access_to_volume_info_time = get_absolute_time_in_ns(); #ifdef CONFIG_DBUS_INTERFACE } #endif @@ -4208,7 +4251,7 @@ int send_metadata_to_queue(pc_queue *queue, uint32_t type, uint32_t code, char * // pointer to data or NULL, // length of data or NULL, // the rtsp_message or NULL, - + // the rtsp_message is sent for 'core' messages, because it contains the data // and must not be freed until the data has been read. // So, it is passed to send_metadata to be retained, sent to the thread where metadata @@ -4226,7 +4269,7 @@ int send_metadata_to_queue(pc_queue *queue, uint32_t type, uint32_t code, char * // If the rtsp_message is NULL, then if the pointer is non-null then the data it // points to, of the length specified, is memcpy'd and passed to the metadata // handler. The handler should free it when done. - + // If the rtsp_message is NULL and the pointer is also NULL, nothing further // is done. // clang-format on @@ -4327,12 +4370,13 @@ static void handle_get_parameter(__attribute__((unused)) rtsp_conn_info *conn, r if ((req->content) && (req->contentlength == strlen("volume\r\n")) && strstr(req->content, "volume") == req->content) { - debug(1, "Connection %d: Current volume (%.6f) requested", conn->connection_number, - config.airplay_volume); + debug(2, "Connection %d: current volume (%.6f) requested", conn->connection_number, + suggested_volume(conn)); + char *p = malloc(128); // will be automatically deallocated with the response is deleted if (p) { resp->content = p; - resp->contentlength = snprintf(p, 128, "\r\nvolume: %.6f\r\n", config.airplay_volume); + resp->contentlength = snprintf(p, 128, "\r\nvolume: %.6f\r\n", suggested_volume(conn)); } else { debug(1, "Couldn't allocate space for a response."); } @@ -5456,6 +5500,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { FD_SET(sockfd[i], &fds); ret = select(maxfd + 1, &fds, 0, 0, &tv); + if (ret < 0) { if (errno == EINTR) continue; @@ -5497,39 +5542,40 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { if (getsockname(conn->fd, (struct sockaddr *)&conn->local, &size_of_reply) == 0) { // Thanks to https://holmeshe.me/network-essentials-setsockopt-SO_KEEPALIVE/ for this. - - // turn on keepalive stuff -- wait for keepidle + (keepcnt * keepinttvl time) seconds before giving up - // an ETIMEOUT error is returned if the keepalive check fails + + // turn on keepalive stuff -- wait for keepidle + (keepcnt * keepinttvl time) seconds + // before giving up an ETIMEOUT error is returned if the keepalive check fails int keepAliveIdleTime = 35; // wait this many seconds before checking for a dropped client - int keepAliveCount = 5; // check this many times - int keepAliveInterval = 5; // wait this many seconds between checks - + int keepAliveCount = 5; // check this many times + int keepAliveInterval = 5; // wait this many seconds between checks #if defined COMPILE_FOR_BSD || defined COMPILE_FOR_OSX - #define SOL_OPTION IPPROTO_TCP +#define SOL_OPTION IPPROTO_TCP #else - #define SOL_OPTION SOL_TCP +#define SOL_OPTION SOL_TCP #endif #ifdef COMPILE_FOR_OSX - #define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPALIVE +#define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPALIVE #else - #define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPIDLE +#define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPIDLE #endif - - if (setsockopt(conn->fd, SOL_OPTION, KEEP_ALIVE_OR_IDLE_OPTION, (void *)&keepAliveIdleTime, sizeof(keepAliveIdleTime))) { - debug(1,"can't set the keepidle wait time"); + if (setsockopt(conn->fd, SOL_OPTION, KEEP_ALIVE_OR_IDLE_OPTION, + (void *)&keepAliveIdleTime, sizeof(keepAliveIdleTime))) { + debug(1, "can't set the keepidle wait time"); } - if (setsockopt(conn->fd, SOL_OPTION, TCP_KEEPCNT, (void *)&keepAliveCount, sizeof(keepAliveCount))) { - debug(1,"can't set the keepidle missing count"); + if (setsockopt(conn->fd, SOL_OPTION, TCP_KEEPCNT, (void *)&keepAliveCount, + sizeof(keepAliveCount))) { + debug(1, "can't set the keepidle missing count"); } - if (setsockopt(conn->fd, SOL_OPTION , TCP_KEEPINTVL, (void *)&keepAliveInterval, sizeof(keepAliveInterval))) { - debug(1,"can't set the keepidle missing count interval"); + if (setsockopt(conn->fd, SOL_OPTION, TCP_KEEPINTVL, (void *)&keepAliveInterval, + sizeof(keepAliveInterval))) { + debug(1, "can't set the keepidle missing count interval"); }; - + // initialise the connection info void *client_addr = NULL, *self_addr = NULL; conn->connection_ip_family = conn->local.SAFAMILY; diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 660d2b2a..03cb4339 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -28,7 +28,7 @@ general = // drift_tolerance_in_seconds = 0.002; // allow a timing error of this number of seconds of drift away from exact synchronisation before attempting to correct it // resync_threshold_in_seconds = 0.050; // a synchronisation error greater than this number of seconds will cause resynchronisation; 0 disables it - +// resync_recovery_time_in_seconds = 0.100; // allow this extra time to recover after a late resync. Increase the value, possibly to 0.5, in a virtual machine. // playback_mode = "stereo"; // This can be "stereo", "mono", "reverse stereo", "both left" or "both right". Default is "stereo". // alac_decoder = "hammerton"; // This can be "hammerton" or "apple". This advanced setting allows you to choose // the original Shairport decoder by David Hammerton or the Apple Lossless Audio Codec (ALAC) decoder written by Apple. @@ -43,9 +43,24 @@ general = // "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_control_combined_hardware_priority = "no"; // when extending the volume range by combining the built-in software attenuator with the hardware mixer attenuator, set this to "yes" to reduce volume by using the hardware mixer first, then the built-in software attenuator. + +// default_airplay_volume = -24.0; // this is the suggested volume after a reset or after the high_volume_threshold has been exceed and the high_volume_idle_timeout_in_minutes has passed + +// The following settings are for dealing with potentially surprising high ("very loud") volume levels. +// When a new play session starts, it usually requests a suggested volume level from Shairport Sync. This is normally the volume level of the last session. +// This can cause unpleasant surprises if the last session was (a) very loud and (b) a long time ago. +// Thus, the user could be unpleasantly surprised by the volume level of the new session. + +// To deal with this, when the last session volume is "very loud", the following two settings will lower the suggested volume after a period of idleness: + +// high_threshold_airplay_volume = -16.0; // airplay volume greater or equal to this is "very loud" +// high_volume_idle_timeout_in_minutes = 0; // if the current volume is "very loud" and the device is not playing for more than this time, suggest the default volume for new connections instead of the current volume. +// Note 1: This timeout is set to 0 by default to disable this feature. Set it to some positive number, e.g. 180 to activate the feature. +// Note 2: Not all applications use the suggested volume: MacOS Music and Mac OS System Sounds use their own settings. + // 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". +// AirPlay volume goes from 0.0 to -30.0 and -144.0 means "mute". // audio_backend_latency_offset_in_seconds = 0.0; // This is added to the latency requested by the player to delay or advance the output by a fixed amount. // Use it, for example, to compensate for a fixed delay in the audio back end. @@ -81,7 +96,8 @@ 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 -// 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. +// 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 diff --git a/shairport.c b/shairport.c index 96fdcbec..db54c8c6 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 and additions (c) Mike Brady 2014--2022 + * Modifications and additions (c) Mike Brady 2014--2023 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation @@ -343,8 +343,8 @@ int parse_options(int argc, char **argv) { char *stuffing = NULL; /* used for picking up the stuffing option */ signed char c; /* used for argument parsing */ // int i = 0; /* used for tracking options */ - int fResyncthreshold = (int)(config.resyncthreshold * 44100); - int fTolerance = (int)(config.tolerance * 44100); + int resync_threshold_in_frames = 0; + int tolerance_in_frames = 0; poptContext optCon; /* context for parsing command-line options */ struct poptOption optionsTable[] = { {"verbose", 'v', POPT_ARG_NONE, NULL, 'v', NULL, NULL}, @@ -365,10 +365,10 @@ int parse_options(int argc, char **argv) { {"mdns", 'm', POPT_ARG_STRING, &config.mdns_name, 0, NULL, NULL}, {"latency", 'L', POPT_ARG_INT, &config.userSuppliedLatency, 0, NULL, NULL}, {"stuffing", 'S', POPT_ARG_STRING, &stuffing, 'S', NULL, NULL}, - {"resync", 'r', POPT_ARG_INT, &fResyncthreshold, 0, NULL, NULL}, + {"resync", 'r', POPT_ARG_INT, &resync_threshold_in_frames, 'r', NULL, NULL}, {"timeout", 't', POPT_ARG_INT, &config.timeout, 't', NULL, NULL}, {"password", 0, POPT_ARG_STRING, &config.password, 0, NULL, NULL}, - {"tolerance", 'z', POPT_ARG_INT, &fTolerance, 0, NULL, NULL}, + {"tolerance", 'z', POPT_ARG_INT, &tolerance_in_frames, 'z', NULL, NULL}, {"use-stderr", 'u', POPT_ARG_NONE, NULL, 'u', NULL, NULL}, {"log-to-syslog", 0, POPT_ARG_NONE, &log_to_syslog_selected, 0, NULL, NULL}, #ifdef CONFIG_METADATA @@ -424,10 +424,12 @@ int parse_options(int argc, char **argv) { "automatically received from forkedDaapd"); break; case 'r': + config.resync_threshold = (resync_threshold_in_frames * 1.0) / 44100; inform("Warning: the option -r or --resync is deprecated. Please use the " "\"resync_threshold_in_seconds\" setting in the config file instead."); break; case 'z': + config.tolerance = (tolerance_in_frames * 1.0) / 44100; inform("Warning: the option --tolerance is deprecated. Please use the " "\"drift_tolerance_in_seconds\" setting in the config file instead."); break; @@ -462,12 +464,15 @@ int parse_options(int argc, char **argv) { }; #endif - config.resyncthreshold = 1.0 * fResyncthreshold / 44100; - config.tolerance = 1.0 * fTolerance / 44100; config.audio_backend_silent_lead_in_time_auto = - 1; // start outputting silence as soon as packets start arriving - config.airplay_volume = -24.0; // if no volume is ever set, default to initial default value if - // nothing else comes in first. + 1; // start outputting silence as soon as packets start arriving + config.default_airplay_volume = -24.0; + config.high_threshold_airplay_volume = + -16.0; // if the volume exceeds this, reset to the default volume if idle for the + // limit_to_high_volume_threshold_time_in_minutes time + config.limit_to_high_volume_threshold_time_in_minutes = + 0; // after this time in minutes, if the volume is higher, use the default_airplay_volume + // volume for new play sessions. config.fixedLatencyOffset = 11025; // this sounds like it works properly. config.diagnostic_drop_packet_fraction = 0.0; config.active_state_timeout = 10.0; @@ -692,7 +697,7 @@ int parse_options(int argc, char **argv) { if (config_lookup_int(config.cfg, "general.resync_threshold", &value)) { inform("The resync_threshold setting is deprecated. Use " "resync_threshold_in_seconds instead"); - config.resyncthreshold = 1.0 * value / 44100; + config.resync_threshold = 1.0 * value / 44100; } /* Get the drift tolerance setting. */ @@ -701,7 +706,11 @@ int parse_options(int argc, char **argv) { /* Get the resync setting. */ if (config_lookup_float(config.cfg, "general.resync_threshold_in_seconds", &dvalue)) - config.resyncthreshold = dvalue; + config.resync_threshold = dvalue; + + /* Get the resync recovery time setting. */ + if (config_lookup_float(config.cfg, "general.resync_recovery_time_in_seconds", &dvalue)) + config.resync_recovery_time = dvalue; /* Get the verbosity setting. */ if (config_lookup_int(config.cfg, "general.log_verbosity", &value)) { @@ -836,6 +845,37 @@ int parse_options(int argc, char **argv) { config.volume_max_db_set = 1; } + /* Get the optional default_volume setting. */ + if (config_lookup_float(config.cfg, "general.default_airplay_volume", &dvalue)) { + // debug(1, "Default airplay volume setting of %f on the -30.0 to 0 scale", dvalue); + if ((dvalue >= -30.0) && (dvalue <= 0.0)) { + config.default_airplay_volume = dvalue; + } else { + warn("The default airplay volume setting must be between -30.0 and 0.0."); + } + } + + /* Get the optional high_volume_threshold setting. */ + if (config_lookup_float(config.cfg, "general.high_threshold_airplay_volume", &dvalue)) { + // debug(1, "High threshold airplay volume setting of %f on the -30.0 to 0 scale", dvalue); + if ((dvalue >= -30.0) && (dvalue <= 0.0)) { + config.high_threshold_airplay_volume = dvalue; + } else { + warn("The high threshold airplay volume setting must be between -30.0 and 0.0."); + } + } + + /* Get the optional high volume idle tiomeout setting. */ + if (config_lookup_float(config.cfg, "general.high_volume_idle_timeout_in_minutes", &dvalue)) { + // debug(1, "High high_volume_idle_timeout_in_minutes setting of %f", dvalue); + if (dvalue >= 0.0) { + config.limit_to_high_volume_threshold_time_in_minutes = dvalue; + } else { + warn("The high volume idle timeout in minutes setting must be 0.0 or greater. A setting " + "of 0.0 disables the high volume check."); + } + } + if (config_lookup_string(config.cfg, "general.run_this_when_volume_is_set", &str)) { config.cmd_set_volume = (char *)str; } @@ -1371,6 +1411,9 @@ int parse_options(int argc, char **argv) { int i; char hexchar[] = "0123456789abcdef"; for (i = 5; i >= 0; i--) { + // In AirPlay 2 mode, the AP1 name prefix must be + // the same as the AirPlay 2 device id less the colons. + config.ap1_prefix[i] = temporary_airplay_id & 0xFF; apids[i * 3 + 1] = hexchar[temporary_airplay_id & 0xF]; temporary_airplay_id = temporary_airplay_id >> 4; apids[i * 3] = hexchar[temporary_airplay_id & 0xF]; @@ -1436,6 +1479,10 @@ int parse_options(int argc, char **argv) { if (tdebuglev != 0) debuglev = tdebuglev; + // now set the initial volume to the default volume + config.airplay_volume = + config.default_airplay_volume; // if no volume is ever set or requested, default to initial + // default value if nothing else comes in first. // now, do the substitutions in the service name char hostname[100]; gethostname(hostname, 100); @@ -1982,13 +2029,14 @@ int main(int argc, char **argv) { config.debugger_show_file_and_line = 1; // by default, log the file and line of the originating message config.debugger_show_relative_time = - 1; // by default, log the time back to the previous debug message - config.resyncthreshold = 0.05; // 50 ms + 1; // by default, log the time back to the previous debug message config.timeout = 120; // this number of seconds to wait for [more] audio before switching to idle. - config.tolerance = - 0.002; // this number of seconds of timing error before attempting to correct it. config.buffer_start_fill = 220; + config.resync_threshold = 0.050; // default + config.resync_recovery_time = 0.1; // drop this amount of frames following the resync delay. + config.tolerance = 0.002; + #ifdef CONFIG_AIRPLAY_2 config.timeout = 0; // disable watchdog config.port = 7000; @@ -2262,7 +2310,7 @@ int main(int argc, char **argv) { if (sodium_init() < 0) { debug(1, "Can't initialise libsodium!"); } else { - debug(1, "libsodium initialised."); + debug(2, "libsodium initialised."); } // this code is based on @@ -2287,7 +2335,7 @@ int main(int argc, char **argv) { /* Tell Libgcrypt that initialization has completed. */ gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); - debug(1, "libgcrypt initialised."); + debug(2, "libgcrypt initialised."); #endif @@ -2372,11 +2420,21 @@ int main(int argc, char **argv) { : config.packet_stuffing == ST_soxr ? "soxr" : "auto"); debug(1, "interpolation soxr_delay_threshold is %d.", config.soxr_delay_threshold); - debug(1, "resync time is %f seconds.", config.resyncthreshold); + debug(1, "resync time is %f seconds.", config.resync_threshold); + debug(1, "resync recovery time is %f seconds.", config.resync_recovery_time); debug(1, "allow a session to be interrupted: %d.", config.allow_session_interruption); debug(1, "busy timeout time is %d.", config.timeout); debug(1, "drift tolerance is %f seconds.", config.tolerance); debug(1, "password is \"%s\".", strnull(config.password)); + debug(1, "default airplay volume is: %.6f.", config.default_airplay_volume); + debug(1, "high threshold airplay volume is: %.6f.", config.high_threshold_airplay_volume); + if (config.limit_to_high_volume_threshold_time_in_minutes == 0) + debug(1, "check for higher-than-threshold volume for new play session is disabled."); + else + debug(1, + "suggest default airplay volume for new play sessions instead of higher-than-threshold " + "airplay volume after: %d minutes.", + config.limit_to_high_volume_threshold_time_in_minutes); debug(1, "ignore_volume_control is %d.", config.ignore_volume_control); if (config.volume_max_db_set) debug(1, "volume_max_db is %d.", config.volume_max_db); @@ -2458,10 +2516,14 @@ int main(int argc, char **argv) { soxr_time_check_thread_started = 1; #endif - // calculate the 12-hex-digit prefix by hashing the service name. + + // In AirPlay 2 mode, the AP1 prefix is the same as the device ID less the colons + // In AirPlay 1 mode, the AP1 prefix is calculated by hashing the service name. +#ifndef CONFIG_AIRPLAY_2 + uint8_t ap_md5[16]; - debug(1, "size of hw_addr is %u.", sizeof(config.hw_addr)); + // debug(1, "size of hw_addr is %u.", sizeof(config.hw_addr)); #ifdef CONFIG_OPENSSL MD5_CTX ctx; MD5_Init(&ctx); @@ -2495,6 +2557,7 @@ int main(int argc, char **argv) { #endif memcpy(config.ap1_prefix, ap_md5, sizeof(config.ap1_prefix)); +#endif #ifdef CONFIG_METADATA metadata_init(); // create the metadata pipe if necessary