merge 4.2 updates from the branch

This commit is contained in:
Mike Brady
2023-05-20 17:48:01 +01:00
90 changed files with 966 additions and 699 deletions
+10
View File
@@ -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
@@ -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
+1 -1
View File
@@ -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
@@ -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
+1 -1
View File
@@ -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
@@ -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
@@ -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
@@ -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
+6 -6
View File
@@ -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
+6 -6
View File
@@ -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
+1 -1
View File
@@ -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.'
+5
View File
@@ -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
---
+33
View File
@@ -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.
+1
View File
@@ -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).
+2 -1
View File
@@ -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
```
+29 -34
View File
@@ -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'
```
+1 -1
View File
@@ -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).
+4 -1
View File
@@ -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).
+68 -103
View File
@@ -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 = &parameters; // likewise the parameters
stuff
debug(1,"Max and min dB calculated are %d and
%d.",alsa_mix_maxdb,alsa_mix_mindb);
*/
}
}
if (((config.alsa_use_hardware_mute == 1) &&
@@ -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;
+201 -261
View File
@@ -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,"<<<Frames requested %d, written to pa: %d, corked status:
// %d.",requested_bytes/4,bytes_transferred/4,pa_stream_is_corked(stream));
}
void alt_stream_write_cb(pa_stream *stream, size_t requested_bytes,
__attribute__((unused)) void *userdata) {
// debug(1, "***Bytes requested bytes %d.", requested_bytes);
size_t bytes_remaining = requested_bytes;
while (bytes_remaining > 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) {
+3 -2
View File
@@ -37,21 +37,22 @@
#include <unistd.h>
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;
+49 -40
View File
@@ -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;
}
+18 -12
View File
@@ -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
+66 -21
View File
@@ -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"])
+8 -4
View File
@@ -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);
+43 -33
View File
@@ -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"]
+3
View File
@@ -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.
+34 -25
View File
@@ -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"]
@@ -1,4 +0,0 @@
#!/command/with-contenv sh
rm -rf /var/run/dbus.pid
echo "Starting dbus"
exec dbus-daemon --system --nofork
@@ -0,0 +1 @@
base
@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/01-startup/script.sh
@@ -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
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
exec dbus-send --system / org.freedesktop.DBus.Peer.Ping > /dev/null 2> /dev/null
@@ -0,0 +1 @@
01-startup
@@ -0,0 +1 @@
3
@@ -0,0 +1,3 @@
#!/command/with-contenv sh
echo "Starting dbus"
exec s6-notifyoncheck dbus-daemon --system --nofork --nopidfile
+10
View File
@@ -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
@@ -0,0 +1 @@
02-dbus
@@ -0,0 +1 @@
3
@@ -0,0 +1,3 @@
#!/command/with-contenv sh
echo "Starting avahi"
exec s6-notifyoncheck avahi-daemon --no-chroot
@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/startup/script.sh
+8
View File
@@ -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 "$@"
+5 -1
View File
@@ -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"
+6
View File
@@ -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
@@ -1,4 +0,0 @@
#!/command/with-contenv sh
rm -rf /var/run/dbus.pid
echo "Starting dbus"
exec dbus-daemon --system --nofork
@@ -0,0 +1 @@
base
@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/01-startup/script.sh
@@ -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
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
exec dbus-send --system / org.freedesktop.DBus.Peer.Ping > /dev/null 2> /dev/null
@@ -0,0 +1 @@
01-startup
@@ -0,0 +1 @@
3
@@ -0,0 +1,3 @@
#!/command/with-contenv sh
echo "Starting dbus"
exec s6-notifyoncheck dbus-daemon --system --nofork --nopidfile
+10
View File
@@ -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
@@ -0,0 +1 @@
02-dbus
@@ -0,0 +1 @@
3
@@ -0,0 +1,3 @@
#!/command/with-contenv sh
echo "Starting avahi"
exec s6-notifyoncheck avahi-daemon --no-chroot
@@ -0,0 +1 @@
01-startup
-1
View File
@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/startup/script.sh
+8
View File
@@ -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 "$@"
+2 -1
View File
@@ -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;
+3 -5
View File
@@ -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
+26 -16
View File
@@ -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 <inttypes.h>
#include <pthread.h>
+68 -24
View File
@@ -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...");
+5 -4
View File
@@ -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
+4 -4
View File
@@ -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);
+80 -34
View File
@@ -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 <net/if.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
@@ -47,7 +48,6 @@
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <netinet/tcp.h>
#include <sys/ioctl.h>
@@ -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;
+19 -3
View File
@@ -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
+84 -21
View File
@@ -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