This squashed commit comprises a long series of improvements, bug fixes, optimizations, and new feature additions:
- 48,000 frames per second operation, - 48k lossless stereo, 5.1 and 7.1 surround sound - Multichannel and multi rate operation on ALSA, PipeWire, PulseAudio, FreeBSD, stdout and unix pipe output backends. Automatic, flexible and controllable output format (rate, sample format and channel count) selection. Full FFmpeg integration to support transcoding, resampling, and new audio formats. Better operation on lower powered devices down to e.g. Raspberry Pi B. Reduced Docker image sizes with a slimmed-down FFmpeg library. Enhanced timestamp handling for better synchronization. Improved the sync error calculation. A new "vernier" resampling and interpolation method for low-power CPUs. Bug fixes and minor enhancements. Note -- there are many breaking changes from previous versions of Shairport Sync!
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
name: Basic ALSA configuration for systemd, using a build folder.
|
||||
name: systemd, alsa, pipewire, build folder.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -10,18 +10,18 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Install Dependencies
|
||||
run: sudo apt-get -y --no-install-recommends install xmltoman libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev
|
||||
run: sudo apt-get -y --no-install-recommends install libpipewire-0.3-dev libplist-utils xmltoman libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev libgcrypt-dev uuid-dev
|
||||
- name: Configure
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
autoreconf -i ..
|
||||
../configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-airplay-2
|
||||
../configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd-startup --with-airplay-2 --with-pipewire
|
||||
- name: Make
|
||||
run: |
|
||||
cd build
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Configuration (without pa, soundio or apple-alac) for systemd.
|
||||
name: systemd, alsa, ao, dummy, jack, pipe, stdout
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -10,16 +10,16 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Install Dependencies
|
||||
run: sudo apt-get -y --no-install-recommends install xmltoman libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libmosquitto-dev avahi-daemon libavahi-client-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev
|
||||
run: sudo apt-get -y --no-install-recommends install libglib2.0-dev libplist-utils xmltoman libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libmosquitto-dev avahi-daemon libavahi-client-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev libgcrypt-dev
|
||||
- name: Configure
|
||||
run: |
|
||||
autoreconf -fi
|
||||
./configure --sysconfdir=/etc --with-alsa --with-ao --with-dummy --with-jack --with-pipe --with-stdout --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-dbus-interface --with-mpris-interface --with-mqtt-client --with-airplay-2
|
||||
./configure --sysconfdir=/etc --with-alsa --with-ao --with-dummy --with-jack --with-pipe --with-stdout --with-soxr --with-avahi --with-ssl=openssl --with-systemd-startup --with-dbus-interface --with-mpris-interface --with-mqtt-client --with-airplay-2
|
||||
- name: Make
|
||||
run: |
|
||||
make -j
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Configuration (without pa, soundio or apple-alac) for systemd, using a build folder.
|
||||
name: systemd, alsa, pipewire, ao, dummy, jack, pipe, stdout,using a build folder.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -10,18 +10,18 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Install Dependencies
|
||||
run: sudo apt-get -y --no-install-recommends install xmltoman libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-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
|
||||
run: sudo apt-get -y --no-install-recommends install libpipewire-0.3-dev libplist-utils xmltoman libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-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 libgcrypt-dev
|
||||
- name: Configure
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
autoreconf -i ..
|
||||
../configure --sysconfdir=/etc --with-alsa --with-ao --with-dummy --with-jack --with-pipe --with-stdout --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-dbus-interface --with-mpris-interface --with-mqtt-client --with-airplay-2
|
||||
../configure --sysconfdir=/etc --with-alsa --with-pipewire --with-ao --with-dummy --with-jack --with-pipe --with-stdout --with-soxr --with-avahi --with-ssl=openssl --with-systemd-startup --with-dbus-interface --with-mpris-interface --with-mqtt-client --with-airplay-2
|
||||
- name: Make
|
||||
run: |
|
||||
cd build
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Configuration (but without pa, soundio, apple-alac) for a System V system.
|
||||
name: systemv, alsa, pipewire, ao, dummy, jack, pipe, stdout
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -10,16 +10,16 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Install Dependencies
|
||||
run: sudo apt-get -y --no-install-recommends install xmltoman libpopt-dev libdaemon-dev libconfig-dev libasound2-dev libao-dev libjack-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
|
||||
run: sudo apt-get -y --no-install-recommends install libpipewire-0.3-dev libplist-utils xmltoman libpopt-dev libdaemon-dev libconfig-dev libasound2-dev libao-dev libjack-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
|
||||
- name: Configure
|
||||
run: |
|
||||
autoreconf -i
|
||||
./configure --sysconfdir=/etc --with-alsa --with-ao --with-dummy --with-libdaemon --with-jack --with-pipe --with-stdout --with-soxr --with-avahi --with-ssl=openssl --with-systemv --with-dbus-interface --with-mpris-interface --with-mqtt-client --with-airplay-2
|
||||
./configure --sysconfdir=/etc --with-alsa --with-pipewire --with-ao --with-dummy --with-libdaemon --with-jack --with-pipe --with-stdout --with-soxr --with-avahi --with-ssl=openssl --with-systemv-startup --with-dbus-interface --with-mpris-interface --with-mqtt-client --with-airplay-2
|
||||
- name: Make
|
||||
run: |
|
||||
make -j
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Basic libao configuration for macOS with BREW -- classic only, because macOS can't host NQPTP.
|
||||
name: macos classic, libao, BREW -- classic only, because macOS can't host NQPTP.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Basic ALSA classic configuration for systemd, using a build folder.
|
||||
name: systemd classic, alsa, using a build folder.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
mkdir build
|
||||
cd build
|
||||
autoreconf -i ..
|
||||
../configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd
|
||||
../configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd-startup
|
||||
- name: Make
|
||||
run: |
|
||||
cd build
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Classic (without pa, soundio, apple-alac) for systemd, using a build folder.
|
||||
name: systemd classic, ffmpeg, alsa, pipewire, build folder.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -10,18 +10,18 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Install Dependencies
|
||||
run: sudo apt-get -y --no-install-recommends install xmltoman libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libglib2.0-dev libmosquitto-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev
|
||||
run: sudo apt-get -y --no-install-recommends install xmltoman libpipewire-0.3-dev libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libglib2.0-dev libmosquitto-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libavutil-dev libavcodec-dev libavformat-dev
|
||||
- name: Configure
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
autoreconf -i ..
|
||||
../configure --sysconfdir=/etc --with-alsa --with-ao --with-dummy --with-pipe --with-stdout --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-dbus-interface --with-mpris-interface --with-mqtt-client
|
||||
../configure --sysconfdir=/etc --with-ffmpeg --with-alsa --with-pipewire --with-ao --with-dummy --with-pipe --with-stdout --with-soxr --with-avahi --with-ssl=openssl --with-dbus-interface --with-mpris-interface --with-mqtt-client --with-systemd-startup
|
||||
- name: Make
|
||||
run: |
|
||||
cd build
|
||||
|
||||
+4
-46
@@ -20,48 +20,6 @@ env:
|
||||
NQPTP_BRANCH: main
|
||||
|
||||
jobs:
|
||||
test-build-on-pull-request:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Set SHAIRPORT_SYNC_BRANCH env
|
||||
run: |
|
||||
SHAIRPORT_SYNC_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
echo "Current SHAIRPORT_SYNC_BRANCH set to ${SHAIRPORT_SYNC_BRANCH}"
|
||||
echo "SHAIRPORT_SYNC_BRANCH=${SHAIRPORT_SYNC_BRANCH}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
- name: Build (classic)
|
||||
uses: docker/build-push-action@v6.14.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./docker/classic/Dockerfile
|
||||
push: false
|
||||
build-args: |
|
||||
SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }}
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6.14.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./docker/Dockerfile
|
||||
push: false
|
||||
build-args: |
|
||||
SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }}
|
||||
NQPTP_BRANCH=${{ env.NQPTP_BRANCH }}
|
||||
|
||||
build-and-publish:
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -86,10 +44,10 @@ jobs:
|
||||
echo "IMAGE_TAG_BASE=development" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
uses: docker/setup-qemu-action@v3.4.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@v3.9.0
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
@@ -98,7 +56,7 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_REGISTRY_USER }}
|
||||
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build and push (classic)
|
||||
- name: Build and Push (Classic)
|
||||
uses: docker/build-push-action@v6.14.0
|
||||
with:
|
||||
context: ./
|
||||
@@ -109,7 +67,7 @@ jobs:
|
||||
build-args: |
|
||||
SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }}
|
||||
|
||||
- name: Build and push
|
||||
- name: Build and Push (AirPlay 2)
|
||||
uses: docker/build-push-action@v6.14.0
|
||||
with:
|
||||
context: ./
|
||||
@@ -5,7 +5,7 @@
|
||||
# Only pushes the tag when it matches one of the following patterns:
|
||||
# X, X.Y or X.Y.Z
|
||||
|
||||
name: Build and push docker (tag)
|
||||
name: Build and push Docker images...
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -16,8 +16,9 @@ on:
|
||||
- '[0-9]+\.[0-9]+\.[0-9]+' # X.Y.Z
|
||||
|
||||
env:
|
||||
DOCKER_PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7
|
||||
DOCKER_PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
NQPTP_BRANCH: main
|
||||
LATEST_TAG: false
|
||||
|
||||
jobs:
|
||||
main:
|
||||
@@ -37,25 +38,26 @@ jobs:
|
||||
- name: Set tag env
|
||||
run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Is branch "master"?
|
||||
if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'master' }}
|
||||
run: echo "LATEST_TAG=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Is branch "development"?
|
||||
if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }}
|
||||
run: |
|
||||
echo "NQPTP_BRANCH=development" >> $GITHUB_ENV
|
||||
|
||||
|
||||
- name: Is branch "danger-2301"?
|
||||
if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'danger-2301' }}
|
||||
run: |
|
||||
echo "NQPTP_BRANCH=development" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
uses: docker/setup-qemu-action@v3.4.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@v3.9.0
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ${{ secrets.DOCKER_REGISTRY }}
|
||||
# registry: docker.io # default
|
||||
username: ${{ secrets.DOCKER_REGISTRY_USER }}
|
||||
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
|
||||
|
||||
@@ -72,7 +74,7 @@ jobs:
|
||||
build-args: |
|
||||
SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }}
|
||||
|
||||
- name: Build and push
|
||||
- name: Build and Push AirPlay 2 Version
|
||||
uses: docker/build-push-action@v6.14.0
|
||||
with:
|
||||
context: ./
|
||||
@@ -80,8 +82,8 @@ jobs:
|
||||
platforms: ${{ env.DOCKER_PLATFORMS }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.GIT_TAG }}
|
||||
${{ env.LATEST_TAG == 'true' && format('{0}:latest', secrets.DOCKER_IMAGE_NAME) || '' }}
|
||||
mikebrady/sps-private:${{ env.GIT_TAG }}
|
||||
mikebrady/sps-private:latest
|
||||
build-args: |
|
||||
SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }}
|
||||
NQPTP_BRANCH=${{ env.NQPTP_BRANCH }}
|
||||
+7
-3
@@ -28,8 +28,11 @@ scripts/shairport-sync.service
|
||||
scripts/shairport-sync.service-avahi
|
||||
scripts/shairport-sync
|
||||
shairport-sync.core
|
||||
gitversion.*
|
||||
plist_xml_strings.*
|
||||
gitversion-stamp
|
||||
gitversion.h
|
||||
plists/*.c
|
||||
plists/*.h
|
||||
plists/*.x
|
||||
|
||||
#Some dbus files that are automatically generated
|
||||
/org.gnome.ShairportSync.service
|
||||
@@ -45,8 +48,9 @@ plist_xml_strings.*
|
||||
.DS_Store
|
||||
shairport-sync.xcodeproj
|
||||
|
||||
# separate build directory
|
||||
# separate build directories
|
||||
build
|
||||
classic
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Adjusting Synchronisation on Shairport Sync ("SPS")
|
||||
# Adjusting Synchronisation with Shairport Sync
|
||||
|
||||
Sometimes, a timing difference can be heard, where the audio coming from the SPS-powered device is slightly ahead or slightly behind another device playing in synchrony. This can sometimes be heard as an irritating "echo".
|
||||
Sometimes, a timing difference can be heard, where the audio coming from the Shairport-Sync-powered device is slightly ahead or slightly behind another device playing in synchrony. This can sometimes be heard as an irritating "echo".
|
||||
|
||||
This is usually due to audio amplifier delays:
|
||||
|
||||
@@ -8,17 +8,17 @@ This is usually due to audio amplifier delays:
|
||||
|
||||
* If your output device is a HDMI-connected device such as a TV or an AV Receiver (AVR), it will almost certainly delay audio by anything up to several hundred milliseconds.
|
||||
|
||||
In these circumstances, if the output from the SPS device is amplified by a conventional analog-only HiFi amplifier – which has almost no delay – it will be early by comparison with audio coming from the other device.
|
||||
In these circumstances, if the output from the Shairport Sync device is amplified by a conventional analog-only HiFi amplifier – which has almost no delay – it will be early by comparison with audio coming from the other device.
|
||||
|
||||
Conversely, if the output from the SPS device is passed through an AVR, then it could be late by comparison with audio amplified by a conventional audio amplifier.
|
||||
Conversely, if the output from the Shairport Sync device is passed through an AVR, then it could be late by comparison with audio amplified by a conventional audio amplifier.
|
||||
|
||||
The fix for this is to get Shairport Sync to compensate for delays by providing audio to the output device _slightly late_ or _slightly early_, so that when audio emerges from the amplifier, it is in exact synchrony with audio from the other devices.
|
||||
|
||||
The setting to look for is in the `general` section of the Shairport Sync configuration file and is called `audio_backend_latency_offset_in_seconds`. By default it is `0.0` seconds.
|
||||
|
||||
For example, to delay the output from the SPS device by 100 milliseconds (0.1 seconds), set the `audio_backend_latency_offset_in_seconds` to `0.1`, so that audio is provided to your output device 100 milliseconds later than nominal synchronisation time.
|
||||
For example, to delay the output from the Shairport Sync device by 100 milliseconds (0.1 seconds), set the `audio_backend_latency_offset_in_seconds` to `0.1`, so that audio is provided to your output device 100 milliseconds later than nominal synchronisation time.
|
||||
|
||||
Similarly, to get the output from the SPS device 50 milliseconds (0.05 seconds) early, set the `audio_backend_latency_offset_in_seconds` to `-0.05`, so that audio is provided to your output device 50 milliseconds earlier than nominal synchronisation time.
|
||||
Similarly, to get the output from the Shairport Sync device 50 milliseconds (0.05 seconds) early, set the `audio_backend_latency_offset_in_seconds` to `-0.05`, so that audio is provided to your output device 50 milliseconds earlier than nominal synchronisation time.
|
||||
|
||||
Latency adjustments should be small, not more than about ± 250 milliseconds.
|
||||
|
||||
|
||||
@@ -3,13 +3,11 @@ Shairport Sync was designed to run best on dedicated stand-alone low-power "head
|
||||
and with a decent CD-quality Digital to Analog Converter (DAC).
|
||||
|
||||
## CPU Power and Memory
|
||||
Computer power and memory requirements are modest – a Raspberry Pi 2 or better, including the Pi Zero 2 W, is fine.
|
||||
Unfortunately, while the original Raspberry Pi and the Pi Zero are powerful enough for AirPlay operation,
|
||||
they are not really suitable for AirPlay 2 operation.
|
||||
Computer power and memory requirements are modest – a Raspberry Pi B or better, including the Pi Zero W, is fine.
|
||||
|
||||
## CPU Clock
|
||||
For best performance, Shairport Sync requires a stable and accurate system clock.
|
||||
This is because the output DAC's output rate is normally determined by the system clock (exceptionally, some high-end USB streamers use their own built-in clocks).
|
||||
This is because the output DAC's output rate is normally determined by the system clock (exceptionally, some very high-end USB streamers use their own built-in clocks).
|
||||
If the clock drifts, or if its actual frequency is far from its nominal frequency, Shairport Sync will have to do more interpolation,
|
||||
which inevitably must degrade the audio fidelity, even if it is very hard to hear.
|
||||
Some very old laptops are known to have inaccurate clocks and and some embedded systems can suffer from temperature-related clock drift.
|
||||
@@ -17,32 +15,27 @@ Some very old laptops are known to have inaccurate clocks and and some embedded
|
||||
Recent Raspberry Pis seem to have very accurate clocks with low drift.
|
||||
|
||||
## Linux
|
||||
The best kind of Linux for Shairport Sync is a "bare" Linux.
|
||||
Raspberry Pi OS Lite, Debian Minimal Server, Ubuntu Server, Fedora Server and Arch Linux (Minimal Configuration) are good examples.
|
||||
The best kind of Linux for Shairport Sync is a "bare" or "headless" Linux, that is, a Linux without a graphical user interface (GUI). Raspberry Pi OS Lite, Debian Minimal Server, Ubuntu Server, Fedora Server and Arch Linux (Minimal Configuration) are good examples of suitable Linux distributions.
|
||||
|
||||
Shairport Sync also runs on "desktop" Linuxes such as Raspberry Pi OS with desktop, Debian with a desktop environment,
|
||||
Ubuntu Desktop and Fedora Workstation.
|
||||
Desktop Linuxes are less suitable because they almost always use a sound server like PulseAudio or PipeWire.
|
||||
These can interfere with Shairport Sync, which needs direct and exclusive access to the audio hardware.
|
||||
Shairport Sync will also runs on "desktop" Linuxes such as Ubuntu Desktop, Fedora Workstation, Raspberry Pi OS with desktop and Debian. However, Linuxes with a GUI are less suitable because they almost always use a sound server like PipeWire or PulseAudio. These can interfere with Shairport Sync, which prefers direct and exclusive access to the audio hardware.
|
||||
|
||||
## DAC
|
||||
A good Digital to Analog Converter (DAC) will have a huge influence on the quality of the audio.
|
||||
|
||||
Shairport Sync runs at 44,100 frames per second (FPS), each frame consisting of a pair of signed 16 bit linear samples, one for left and one for right.
|
||||
Shairport Sync will take advantage to 24- or 32-bit DACs if available, and will run at 44,100 or 88,200, 176,400 or 352,800 FPS if necessary,
|
||||
though there is no advantage to the higher rates. The 44,100 FPS rate was chosen to match the rate of AirPlay.
|
||||
Shairport Sync runs at the frame rate of the input audio 44,100 or 48,000 frames per second (44,100 only if built for classic AirPlay without FFmpeg support.). It will automatically output in a wide range of formats and will try to match the output rate and channel count to that of the input. If not, it can transcode and mixdown automatically. Output rates, formats and channel counts can be restricted if necessary.
|
||||
|
||||
Good DACs are available at a very wide range of prices, from low-cost USB "Sound Cards" to very high-end HiFi streaming DACs.
|
||||
|
||||
In the Raspberry Pi world, many very good low-cost I2S DACs, some with integrated amplifiers, are available. The DAC powering the Pi's built-in audio jack is not great, however. While it may be good enough for trying out Shairport Sync or for casual listening, it has a very limited frequency response and can generate very large transients when it starts up. A separate DAC will transform the output quality.
|
||||
|
||||
**Note**
|
||||
Make sure that the DAC is capable of 44,100 FPS operation – this is really mandatory.
|
||||
Most recent DACs are okay, but some older DACs will only run at 48,000 FPS or multiples of it, and Shairport Sync can not use them.
|
||||
If you are building for classic AirPlay and do not wish to use the FFmpeg library, input and output is restricted to 44,100 frames per second. Make sure that the DAC is capable of 44,100 FPS operation.
|
||||
Most recent DACs are okay, but some older DACs will only run at 48,000 FPS or multiples of it.
|
||||
|
||||
## Maximum Output Level
|
||||
The `volume_max_db` setting allows you to reduce the maximum level of DAC output to prevent possible overloading of the amplifier or premplifier it feeds.
|
||||
## Volume Range
|
||||
The volume range is the difference (technically the ratio, expressed in dB) between the highest and lowest level of the volume control. Ideally, this should give the highest volume at the high end and a barely audible sound at the lowest level. Typical volume ranges are 60 dB to 75dB. If the range is much less than this, the difference between high and low volume won't seem large enough to the listener. If the range is much more, much of the low end of the volume control range will be inaudible. (The built-in DAC of the Raspberry Pi has this problem.) Use the `volume_range_db` setting to set the volume range. If the range you request is greater than the range available in the hardware mixer, the built-in attenuator will be used to make up the difference.
|
||||
The volume range is the difference (technically the ratio, expressed in dB) between the highest and lowest level of the volume control. Ideally, this should give the highest volume at the high end and a barely audible sound at the lowest level. Typical volume ranges are 60 dB to 85dB. If the range is much less than this, the difference between high and low volume won't seem large enough to the listener. If the range is much more, much of the low end of the volume control range will be inaudible. (The built-in DAC of the Raspberry Pi has this problem.) Use the `volume_range_db` setting to set the volume range. If the range you request is greater than the range available in the hardware mixer, the built-in attenuator will be used to make up the difference.
|
||||
## Volume Control
|
||||
Audio is sent at full volume in AirPlay and AirPlay 2 with separate information being sent to set the actual volume. This volume information can be used in four ways:
|
||||
* It can be used to control a built-in attenuator. This is the default.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
When you complete the instructions in [BUILD.md](../BUILD.md), you have a basic functioning Shairport Sync installation. If you want more control – for example, if you want to use a specific DAC, or if you want AirPlay to control the DAC's volume control – you can use settings in the configuration file (recommended) or you can use command-line options.
|
||||
|
||||
## The Configuration File
|
||||
Shairport Sync reads settings from a configuration file at `/etc/shairport-sync.conf` (note that in FreeBSD it will be at `/usr/local/etc/shairport-sync.conf`). When you run `$sudo make install`, a sample configuration file called `shairport-sync.conf.sample` is always installed or updated. This contains all the setting groups and all the settings available, but they all are commented out (comments begin with `//`) so that default values are used. The file contains explanations of the settings, useful hints and suggestions.
|
||||
Shairport Sync reads settings from a configuration file at `/etc/shairport-sync.conf` (in FreeBSD it will be at `/usr/local/etc/shairport-sync.conf`). When you run `$sudo make install`, a sample configuration file called `shairport-sync.conf.sample` is always installed or updated. This contains all the setting groups and all the settings available, but they all are commented out (comments begin with `//`) so that default values are used. The file contains explanations of the settings, useful hints and suggestions.
|
||||
|
||||
## Specifying the Output Device and Mixer Control
|
||||
If you have followed the [BUILD.md](../BUILD.md) instructions, audio received by Shairport Sync will be sent to the `default` device. Depending on the configuration of your system, you may be able to specify a specific hardware output DAC and use its built-in mixer to control volume levels. This would be desirable because (1) the `default` device may be doing further processing on the audio before sending it to the hardware output device, degrading its fidelity, and (2) using the real device's hardware mixer to control volume would give Shairport Sync complete control of the volume range.
|
||||
@@ -29,7 +29,7 @@ Settings and options for the audio backend "alsa":
|
||||
```
|
||||
In this system, you can see that there are three hardware output devices, `"hw:Headphones"`, `"hw:sndrpihifiberry"` and `"hw:vc4hdmi"`.
|
||||
|
||||
Using a tool like `alsamixer`, an output device can be checked to find the name of the volume control mixer. For the first device, the name of the mixer is `"Headphone"`.
|
||||
Using a tool like [`dacquery`](https://github.com/mikebrady/dacquery) or `alsamixer`, an output device can be checked to find the name of the volume control mixer. For the first device, the name of the mixer is `"Headphone"`.
|
||||
|
||||
These setting can be entered into the configuration file, in the `alsa` section, as follows:
|
||||
```
|
||||
@@ -37,7 +37,7 @@ These setting can be entered into the configuration file, in the `alsa` section,
|
||||
alsa =
|
||||
{
|
||||
output_device = "hw:Headphones"; // the name of the alsa output device. Use "shairport-sync -h" to discover the names of ALSA hardware devices. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc.
|
||||
mixer_control_name = "Headphone"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software.
|
||||
mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software.
|
||||
...
|
||||
}
|
||||
```
|
||||
@@ -45,9 +45,7 @@ Make sure to uncomment entries you wish to make active, and restart Shairport Sy
|
||||
|
||||
If you make a syntax error, Shairport Sync will not start and will leave a message in the log. See more details below and in the comments in the configuration file.
|
||||
|
||||
Please note that if your system has a sound server such as PulseAudio or PipeWire (most desktop linuxes have one of these), you may not be able to access the sound hardware directly, so you may only be able to use the `default` output.
|
||||
|
||||
|
||||
Please note that if your system has a sound server such as PipeWire or PulseAudio (most desktop Linuxes have one of these), you may not be able to access the sound hardware directly, so you may only be able to use the `default` output.
|
||||
|
||||
## More about Configuration Settings
|
||||
Settings in the configuration file are grouped. For instance, there is a `general` group within which you can use the `name` tag to set the service name. Suppose you wanted to set the name of the service to `Front Room` and give the service the password `secret`, then you should do the following:
|
||||
@@ -67,9 +65,9 @@ The password setting is only valid for classic Shairport Sync.
|
||||
|
||||
**Important:** You should *never* use an important password as the AirPlay password for a Shairport Sync player – the password is stored in Shairport Sync's configuration file in plain text and is thus completely vulnerable.
|
||||
|
||||
No backend is specified here, so it will default to the `alsa` backend if more than one back end has been compiled. To route the output to PulseAudio, set:
|
||||
No backend is specified here, so it will default to the `alsa` backend if more than one back end has been compiled. To route the output to PipeWire, set:
|
||||
```
|
||||
output_backend = "pa";
|
||||
output_backend = "pipewire";
|
||||
```
|
||||
in the `general` group.
|
||||
|
||||
@@ -89,9 +87,9 @@ alsa =
|
||||
};
|
||||
```
|
||||
|
||||
The `pa` group is used to specify settings relevant to the PulseAudio backend. You can set the "Application Name" that will appear in the "Sound" control panel.
|
||||
The `pipewire` group is used to specify settings relevant to the PipeWire backend. You can set the "Application Name" that will appear in the "Sound" control panel.
|
||||
|
||||
Note: Shairport Sync can take configuration settings from command line options. This is mainly for backward compatibility, but sometimes still useful. For normal use, it is recommended that you use the configuration file method.
|
||||
Note: Shairport Sync can take configuration settings from command line options. This is mainly for backward compatibility, but sometimes still useful. Where possible, it is recommended that you use the configuration file method.
|
||||
|
||||
### Raspberry Pi
|
||||
|
||||
@@ -100,7 +98,7 @@ To make Shairport Sync output to the built-in audio DAC and use its hardware mix
|
||||
alsa =
|
||||
{
|
||||
output_device = "hw:Headphones"; // the name of the alsa output device. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc.
|
||||
mixer_control_name = "Headphone"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software.
|
||||
mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software.
|
||||
// ... other alsa settings
|
||||
```
|
||||
(Remember to uncomment the lines by removing the `//` at the start of each.) When these changes have been made, restart Shairport Sync or simply reboot the system.
|
||||
@@ -125,7 +123,7 @@ alsa = {
|
||||
|
||||
This gives the service the name "Joe's Stereo" and specifies that audio device `hw:0` be used.
|
||||
|
||||
For best results with the `alsa` backend — including getting true mute and instant response to volume control and pause commands — you should access the hardware volume controls. Use `amixer` or `alsamixer` or similar to discover the name of the mixer control to be used as the `mixer_control_name`.
|
||||
For best results with the `alsa` backend — including getting true mute and instant response to volume control and pause commands — you should access the hardware volume controls. Use [`dacquery`](https://github.com/mikebrady/dacquery)`amixer` or `alsamixer` or similar to discover the name of the mixer control to be used as the `mixer_control_name`.
|
||||
|
||||
Here is an example for for a Raspberry Pi using its internal soundcard — device hw:0 — that drives the headphone jack:
|
||||
```
|
||||
@@ -221,7 +219,7 @@ If synchronisation is lost — say due to a busy source or a congested network
|
||||
* You can vary the resync threshold, or turn resync off completely, with the `general` `resync_threshold_in_seconds` setting.
|
||||
|
||||
### Tolerance
|
||||
Playback synchronisation is allowed to wander — to "drift" — a small amount before attempting to correct it. The default is 0.002 seconds, i.e. 2 ms. The smaller the tolerance, the more likely it is that overcorrection will occur. Overcorrection is when more corrections (insertions and deletions) are made than are strictly necessary to keep the stream in sync. Use the `statistics` setting to monitor correction levels. Corrections should not greatly exceed net corrections.
|
||||
Playback synchronisation is allowed to wander — to "drift" — a small amount before attempting to correct it. The default is 0.002 seconds, i.e. 2 ms. The smaller the tolerance, the more likely it is that overcorrection will occur. Overcorrection is when more corrections (insertions and deletions) are made than are strictly necessary to keep the stream in sync. Use the `statistics` setting to monitor correction levels. Corrections should not greatly exceed net corrections.
|
||||
* You can vary the tolerance with the `general` `drift_tolerance_in_seconds` setting.
|
||||
|
||||
## Command Line Arguments
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
# 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.
|
||||
Many Linux systems, especially desktop Linuxes with a GUI, have [PipeWire](https://pipewire.org) or [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/) installed as [sound servers](https://en.wikipedia.org/wiki/Sound_server).
|
||||
PipeWire and PulseAudio are widely used and have the great advantage of being easily able to mix audio from multiple sources. The slight downside is that audio may be further processed (e.g. transcoded) on its way to the output device.
|
||||
|
||||
The following remarks apply to PulseAudio and PipeWire, so, for simplicity, let's refer to PulseAudio only.
|
||||
To use PipeWire or PulseAudio-based systems, Shairport Sync must be set up as a user service.
|
||||
|
||||
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.
|
||||
### Considerations
|
||||
1. Shairport Sync will work without modification in a PipeWire- or PulseAudio-based system if built with the default ALSA backend. This is because PipeWire and PulseAudio both provide a default ALSA pseudo-device to receive and play audio from ALSA-compatible programs.
|
||||
2. Shairport Sync can be built with "native" PipeWire or PulseAudio backends by adding the `--with-pipewire` or `--with-pulseaudio` configuration flags when it is being built. This has the advantage of bypassing the ALSA compatability layer.
|
||||
3. To check if PipeWire support is built into Shairport Sync, check that the string `PipeWire` is included in the version string. (Enter `$ shairport-sync -V` to get the version string.) Similarly, the version string will include `PulseAudio` if the PulseAudio backend is built in.
|
||||
4. Remember to specify which backend Shairport Sync should use in the configuration file or on the command line.
|
||||
|
||||
Shairport Sync loads when system startup is complete and provides its service, routing audio to the ALSA default device.
|
||||
## Automatic Startup of Shairport Sync
|
||||
|
||||
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 main thing to remember about PipeWire and PulseAudio sound servers is that the services they offer only become available when a user logs in -- that is, they are set up as _user services_.
|
||||
Shairport Sync relies on them, so it must also be set up as a user service; it can not be set up as a system service because the PipeWire or PulseAudio services needed by Shairport Sync are not available when system services are launched just after system startup.
|
||||
|
||||
# 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.
|
||||
### Starting Shairport Sync as a User Service
|
||||
To make Shairport Sync start as a user service, (assuming you built and installed Shairport Sync using the BUILD.md guide), ensure you are logged in as the appropriate user and enter:
|
||||
```
|
||||
$ systemctl --user enable shairport-sync
|
||||
```
|
||||
Make sure it is _not enabled_ as a system service:
|
||||
```
|
||||
# systemctl disable shairport-sync
|
||||
```
|
||||
and then reboot.
|
||||
|
||||
# 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.
|
||||
#### Problems with User Service
|
||||
Shairport Sync will function perfectly well as a user service, but there are a number of things to bear in mind:
|
||||
|
||||
1. The AirPlay service will only be available when the user is logged in. When the user logs out, Shairport Sync will terminate and the AirPlay service will disappear.
|
||||
2. If the Linux system is a desktop Linux with a GUI, audio will be sent to the default output only when the user is logged in through the GUI.
|
||||
|
||||
Automatic user login may help address these problems.
|
||||
|
||||
@@ -1,9 +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).
|
||||
* [Working with PulseAudio or PipeWire](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).
|
||||
* [Events](Events.md).
|
||||
* [Statistics](Statistics.md).
|
||||
|
||||
@@ -19,6 +19,8 @@ Average playback synchronisation error in milliseconds in the last interval. By
|
||||
This is the net amount of interpolation done by Shairport Sync to keep the audio stream in sync in the last interval, i.e. the number of frames added **minus** the number of frames removed from the audio stream, relative to the total number of frames output, expressed in parts per million (PPM). For reference, adding or removing one frame per second into a 44,100 frames per second stream is 22.68 ppm.
|
||||
##### All Sync PPM
|
||||
This is the total amount of interpolation done by Shairport Sync to keep the audio stream in sync in the last interval, i.e. the number of frames added **plus** the number of frames removed from the audio stream, relative to the total number of frames output, expressed in parts per million (PPM). The magnitude of this should be the same as the `Net Sync PPM`. If it is much larger it means that Shairport Sync is overcorrecting for sync errors – try increasing the drift tolerance to reduce it.
|
||||
##### Av Sync Window (ms)
|
||||
This is an estimate, in milliseconds, of how often the audio system updates the latency data it provides.
|
||||
##### Packets
|
||||
This is the number of packets of audio frames received since the start of the session. A packet normally contains 352 ± 1 audio frames.
|
||||
##### Missing
|
||||
|
||||
+60
-21
@@ -1,43 +1,82 @@
|
||||
# AirPlay 2
|
||||
**Shairport Sync** (as of [v4.1](https://github.com/mikebrady/shairport-sync/releases/tag/4.1) and newer) offers **AirPlay 2** support for audio sources on:
|
||||
Please be aware that an official specification of AirPlay 2 has not been made public. What follows is based on what has been discovered so far.
|
||||
|
||||
### Streams
|
||||
AirPlay 2 supports two stream types -- "Realtime Audio" and "Buffered Audio".
|
||||
- Realtime audio is played after a short delay. The delay is usually around two seconds. (This is similar to the older Classic AirPlay format.)
|
||||
- Buffered audio starts playing after a very short delay. Audio can arrive faster than it is played and is buffered locally in the Shairport Sync device.
|
||||
|
||||
### Formats
|
||||
AirPlay 2 supports a variety of audio formats. This is a summary of what is known to work so far.
|
||||
- Compression
|
||||
- ALAC (Apple Lossless Advanced Codec) encoding is used for lossless streaming,
|
||||
- AAC (Advanced Audio Coding) is used for lossy streaming.
|
||||
- (BTW you can check your perception of lossy and lossless formats [here](http://abx.digitalfeed.net).)
|
||||
- Encoding
|
||||
- Signed 16- and 24-bit signed linear PCM samples ("S16" and "S24"),
|
||||
- 24-bit floating point PCM samples ("F24").
|
||||
- Rates
|
||||
- Sample rates of 44,100 and 48,000 frames per second (fps).
|
||||
- Channels
|
||||
- Stereo, 5.1 and 7.1 Surround Sound.
|
||||
|
||||
# Shairport Sync
|
||||
This information relates to Shairport Sync Version 5.0 onwards.
|
||||
|
||||
Shairport Sync offers AirPlay 2 support for audio sources on:
|
||||
- iOS devices,
|
||||
- Macs from macOS 10.15 (Catalina) onwards,
|
||||
- HomePod minis,
|
||||
- and Apple TVs.
|
||||
- Apple TVs.
|
||||
|
||||
## What Works
|
||||
- AirPlay 2 audio for iOS, HomePod mini, AppleTV and Mac players.
|
||||
* Audio is synchronised with other AirPlay 2 devices.
|
||||
* Two types of audio are received by Shairport Sync – "Realtime" streams of CD quality ALAC (like "classic" AirPlay) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second.
|
||||
* The selection of stream type is made by the player.
|
||||
* Realtime streams generally have a latency of about two seconds. Buffered Audio streams typically have a latency of half a second or less.
|
||||
* In AirPlay 2 mode, Shairport Sync reverts to "classic" AirPlay when iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay only.
|
||||
|
||||
- Devices running Shairport Sync in AirPlay 2 mode can be [added](https://github.com/mikebrady/shairport-sync/blob/development/ADDINGTOHOME.md) to the Home app.
|
||||
- Audio synchronised with other AirPlay 2 devices.
|
||||
- AirPlay 2 audio playback:
|
||||
- Basic
|
||||
- ALAC/S16/44100/2 realtime audio (stereo),
|
||||
- AAC/F24/44100/2 buffered audio (stereo).
|
||||
- Better
|
||||
- AAC/F24/48000/2 buffered audio (stereo).
|
||||
- Lossless
|
||||
- ALAC/S24/48000/2 buffered audio (stereo).
|
||||
- Surround
|
||||
- AAC/F24/48000/5.1 and AAC/F24/48000/7.1 buffered audio (surround sound).
|
||||
- Transcoding: the output will be transcoded if necessary (e.g. from 44,100 to 48,000 fps) to match the output device's rate.
|
||||
- Mixdown: mixdown to fewer output channels (e.g. 5.1 to stereo) is automatic but can be controlled.
|
||||
- Shairport Sync can revert to "classic" AirPlay if necessary.
|
||||
- Devices running Shairport Sync in AirPlay 2 mode can be [added](https://github.com/mikebrady/shairport-sync/blob/development/ADDINGTOHOME.md) to the Home app.
|
||||
- Shairport Sync can be built to support classic AirPlay (aka "AirPlay 1") only. Classic Airplay offers only one format, ALAC/S16/44100/2, but output transcoding is available to, for example, 48000 fps.
|
||||
|
||||
## What Does Not Work
|
||||
- No AirPlay 2 for Windows iTunes.
|
||||
- High-Definition Lossless -- 96,000 and 192,000 fps material -- is not supported.
|
||||
- Dolby Atmos is not supported.
|
||||
- AirPlay 2 for Windows iTunes is not supported.
|
||||
- Remote control facilities are not implemented.
|
||||
- AirPlay 2 from macOS prior to 10.15 (Catalina) is not supported.
|
||||
- Multiple instances of the AirPlay 2 version of Shairport Sync can not be hosted on the same system. It seems that AirPlay 2 clients are confused by having multiple AirPlay 2 players at the same IP addresses.
|
||||
|
||||
|
||||
## General
|
||||
Shairport Sync uses a companion application called [NQPTP](https://github.com/mikebrady/nqptp) ("Not Quite PTP")
|
||||
for timing and synchronisation in AirPlay 2. NQPTP must have exclusive access to ports `319` and `320`.
|
||||
|
||||
Lossless and High Definition Lossless material is transcoded to AAC before it reaches Shairport Sync.
|
||||
|
||||
## What You Need
|
||||
AirPlay 2 support needs a slightly more powerful CPU for decoding and synchronisation and more memory for bigger buffers and larger libraries. A system with the power of a Raspberry Pi 2 or Raspberry Pi Zero 2 W, or better, is recommended.
|
||||
For AirPlay 2, a system with the power of a Raspberry Pi B, or better, is recommended.
|
||||
|
||||
Here are some guidelines:
|
||||
* Full access, including network capabilities or `root` privileges, to a system at least as powerful as a Raspberry Pi 2 or a Raspberry Pi Zero 2 W.
|
||||
* Full access, including low-port-number network access or `root` privileges, to a system at least as powerful as a Raspberry Pi B.
|
||||
* Ports 319 and 320 must be free to use (i.e. they must not be in use by another service such as a PTP service) and must not be blocked by a firewall.
|
||||
* An up-to-date system. This is important, as some of the libraries must be the latest available.
|
||||
* Shairport Sync will not run in AirPlay 2 mode on a Mac because NQPTP, on which it relies, needs ports 319 and 320, which are already used by macOS.
|
||||
* A version of the [FFmpeg](https://www.ffmpeg.org) library with an AAC decoder capable of decoding Floating Planar -- `fltp` -- material. There is a guide [here](TROUBLESHOOTING.md#aac-decoder-issues-airplay-2-only) to help you find out if your system has it.
|
||||
* An audio output. The device must be capable of running at 44,100 frames per second. You can use [`sps-alsa-explore`](https://github.com/mikebrady/sps-alsa-explore) to test the suitability of hardware ALSA audio devices on your device.
|
||||
Other backends continue to work as with "classic" Shairport Sync.
|
||||
- Multiple instances of the AirPlay 2 version of Shairport Sync can not be hosted on the same system. It seems that AirPlay 2 clients are confused by having multiple AirPlay 2 players at the same IP addresses.
|
||||
* An up-to-date Linux, FreeBSD or OpenBSD system. This is important, as some of the libraries must be the latest available.
|
||||
|
||||
* Due to realtime timing requirements, Shairport Sync does not work well on virtual machines outputting to ALSA, PipeWire, PulseAudio or Jack Audio. For the same reason, Shairport Sync does not work very well with with Bluetooth. YMMV of course, and you can have success where timing is not crucial, such as outputting to `stdout` or to a unix pipe.
|
||||
* Shairport Sync can not run in AirPlay 2 mode on a Mac because NQPTP, on which it relies, needs ports 319 and 320, which are already used by macOS.
|
||||
* A version of the [FFmpeg](https://www.ffmpeg.org) library with an AAC decoder capable of decoding Floating Planar -- `fltp` -- material must be in your system. There is a guide [here](TROUBLESHOOTING.md#aac-decoder-issues-airplay-2-only) to help you find out if your system has it.
|
||||
* An audio output. For preference, the output device should be capable of accepting stereo or multichannel at 44,100 and 48,000 frames per second. With FFmpeg support, audio will be transcoded and mixed to match output device capabilities as necessary.
|
||||
* You can use [`dacquery`](https://github.com/mikebrady/dacquery) to test the suitability of hardware ALSA audio devices on your system.
|
||||
#### An Ideal System
|
||||
For the highest quality audio with the highest fidelty and a minimum of audio processing, an ideal system would be a bare Linux system without a GUI and without PipeWire or PulseAudio, such as Raspberry Pi OS (Lite) or similar. In this case, Shairport Sync connects directly to the ALSA output DAC. For testing, the build-in DAC or a low-cost USB DAC is usually sufficient.
|
||||
|
||||
A problem with this setup is that Shairport Sync expects exclusive access to the audio device. If you have other audio sources, this can be problematic. In such a situation, an otherwise-bare system as described above, but with PipeWire added, can be used, so that all audio sources output to the PipeWire system, which takes care of mixing. (At the time of writing, PipeWire does not work well on the 32-bit version of Raspberry Pi OS)
|
||||
|
||||
## Guides
|
||||
* A building guide is available at [BUILD.md](BUILD.md).
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# Build and Install Shairport Sync
|
||||
This guide is for a basic installation of Shairport Sync in a recent (2018 onwards) Linux or FreeBSD.
|
||||
|
||||
Shairport Sync can be built as an AirPlay 2 player (with [some limitations](AIRPLAY2.md#features-and-limitations)) or as "classic" Shairport Sync – a player for the older, but still supported, AirPlay (aka "AirPlay 1") protocol. Check ["What You Need"](AIRPLAY2.md#what-you-need) for some basic system requirements.
|
||||
Shairport Sync can be built as an AirPlay 2 player (with [some limitations](AIRPLAY2.md#features-and-limitations)) or as classic Shairport Sync – a player for the older, but still supported, "classic" AirPlay (aka "AirPlay 1") protocol. Check ["What You Need"](AIRPLAY2.md#what-you-need) for some basic system requirements.
|
||||
|
||||
Note that Shairport Sync does not work well in virtual machines -- YMMV.
|
||||
|
||||
Overall, you'll be building and installing two programs – Shairport Sync itself and [NQPTP](https://github.com/mikebrady/nqptp), a companion app that Shairport Sync uses for AirPlay 2 timing. If you are building classic Shairport Sync, NQPTP is unnecessary and can be omitted.
|
||||
|
||||
@@ -26,8 +28,11 @@ You should also remove any of the following service files that may be present:
|
||||
* `/lib/systemd/system/shairport-sync.service`
|
||||
* `/lib/systemd/user/shairport-sync.service`
|
||||
* `/etc/init.d/shairport-sync`
|
||||
|
||||
* `~/.config/systemd/user/shairport-sync.service`
|
||||
|
||||
New service files will be installed if necessary at the `# make install` stage.
|
||||
|
||||
(In FreeBSD, there is no need to remove the file at `/usr/local/etc/rc.d/shairport-sync` – it's always replaced in the `make install` step.)
|
||||
#### Reboot after Cleaning Up
|
||||
If you removed any installations of Shairport Sync or any of its service files in the last two steps, you should reboot.
|
||||
|
||||
@@ -40,15 +45,23 @@ Okay, now let's get the tools and libraries for building and installing Shairpor
|
||||
# apt upgrade # this is optional but recommended
|
||||
# apt install --no-install-recommends build-essential git 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
|
||||
libplist-dev libsodium-dev uuid-dev libgcrypt-dev xxd libplist-utils \
|
||||
libavutil-dev libavcodec-dev libavformat-dev
|
||||
```
|
||||
If you are building classic Shairport Sync, the list of packages is shorter:
|
||||
```
|
||||
# apt update
|
||||
# apt upgrade # this is optional but recommended
|
||||
# apt-get install --no-install-recommends build-essential git autoconf automake libtool \
|
||||
libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev
|
||||
libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev \
|
||||
libavutil-dev libavcodec-dev libavformat-dev
|
||||
```
|
||||
|
||||
Building on Ubuntu 24.10, and possibly other distributions, requires `systemd-dev`. It does no harm to attempt to install it -- it will simply fail if the package doesn't exist:
|
||||
```
|
||||
# apt install --no-install-recommends systemd-dev # it's okay if this fails because the package doesn't exist
|
||||
```
|
||||
|
||||
### Fedora (Fedora 40)
|
||||
For AirPlay 2 operation, _before you install the libraries_, please ensure the you have [enabled](https://docs.fedoraproject.org/en-US/quick-docs/rpmfusion-setup) RPM Fusion software repositories to the "Nonfree" level. If this is not done, the FFmpeg libraries will lack a suitable AAC decoder, preventing Shairport Sync from working in AirPlay 2 mode.
|
||||
```
|
||||
@@ -63,7 +76,7 @@ If you are building classic Shairport Sync, the list of packages is shorter:
|
||||
# yum update
|
||||
# yum install make automake gcc gcc-c++ \
|
||||
git autoconf automake avahi-devel libconfig-devel openssl-devel popt-devel soxr-devel \
|
||||
alsa-lib-devel
|
||||
ffmpeg ffmpeg-devel alsa-lib-devel
|
||||
```
|
||||
### Arch Linux
|
||||
After you have installed the libraries, note that you should enable and start the `avahi-daemon` service.
|
||||
@@ -75,7 +88,7 @@ After you have installed the libraries, note that you should enable and start th
|
||||
If you are building classic Shairport Sync, the list of packages is shorter:
|
||||
```
|
||||
# pacman -Syu
|
||||
# pacman -Sy git base-devel alsa-lib popt libsoxr avahi libconfig
|
||||
# pacman -Sy git base-devel alsa-lib popt libsoxr avahi libconfig ffmpeg
|
||||
```
|
||||
Enable and start the `avahi-daemon` service.
|
||||
```
|
||||
@@ -105,14 +118,17 @@ hosts: files dns mdns
|
||||
```
|
||||
Reboot for these changes to take effect.
|
||||
|
||||
Next, install the packages that are needed for Shairport Sync and NQPTP:
|
||||
Next, install the packages that are needed for Shairport Sync and NQPTP. If you will be using using `--with-sndio` instead of `--with-alsa` -- see below -- you can omit the `alsa-utils` package.
|
||||
|
||||
If you are building Shairport Sync for AirPlay 2, install the following packages:
|
||||
```
|
||||
# pkg install git autotools pkgconf popt libconfig openssl alsa-utils libsoxr \
|
||||
libplist libsodium ffmpeg e2fsprogs-libuuid vim
|
||||
```
|
||||
|
||||
If you are building classic Shairport Sync, the list of packages is shorter:
|
||||
```
|
||||
# pkg install git autotools pkgconf popt libconfig openssl alsa-utils libsoxr
|
||||
# pkg install git autotools pkgconf popt libconfig openssl alsa-utils ffmpeg
|
||||
```
|
||||
## 3. Build
|
||||
### NQPTP
|
||||
@@ -122,56 +138,101 @@ Download, install, enable and start NQPTP from [here](https://github.com/mikebra
|
||||
|
||||
### Shairport Sync
|
||||
#### Build and Install
|
||||
Download Shairport Sync, branch and configure, compile and install it. Before executing the commands, please note the following:
|
||||
Download Shairport Sync and configure, compile and install it. Before executing the commands, please note the following:
|
||||
|
||||
* If building for FreeBSD, replace `--with-systemd` with `--with-os=freebsd --with-freebsd-service`.
|
||||
* Omit the `--with-airplay-2` from the `./configure` options if you are building classic Shairport Sync.
|
||||
* If you wish to add extra features, for example an extra audio backend, take a look at the [configuration flags](CONFIGURATION%20FLAGS.md). For this walkthrough, though, please do not remove the `--with-alsa` flag.
|
||||
##### Build Options
|
||||
* **FreeBSD:** For FreeBSD, replace `--with-systemd-startup` with `--with-os=freebsd --with-freebsd-startup`.
|
||||
* Optionally, replace `--with-alsa` with the backend for FreeBSD's native audio system `--with-sndio`.
|
||||
* If you omit the `--sysconfdir=/etc` entry, `/usr/local/etc` will be used as the `sysconfdir`, which is conventionally used in FreeBSD.
|
||||
* **Classic Shairport Sync:** For classic Shairport Sync, replace `--with-airplay-2` with `--with-ffmpeg`.
|
||||
* You can actually omit `--with-ffmpeg` when building classic Shairport Sync, but it is not recommended. While you'll save space (you can omit the FFMpeg libraries in the build and run-time environments), transcoding, e.g. from 44,100 to 48,000 frames per second, will not be available and less well-maintained and less secure decoders will be used.
|
||||
* **Extra Features:** If you wish to add extra features, for example an extra audio backend, take a look at the [configuration flags](CONFIGURATION%20FLAGS.md). For this walkthrough, though, please do not change too much!
|
||||
|
||||
```
|
||||
$ git clone https://github.com/mikebrady/shairport-sync.git
|
||||
$ cd shairport-sync
|
||||
$ git checkout development
|
||||
$ autoreconf -fi
|
||||
$ autoreconf -fi # about 1.5 minutes on a Raspberry Pi B
|
||||
$ ./configure --sysconfdir=/etc --with-alsa \
|
||||
--with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-airplay-2
|
||||
$ make
|
||||
--with-soxr --with-avahi --with-ssl=openssl --with-systemd-startup --with-airplay-2
|
||||
$ make # just over 7 minutes on a Raspberry Pi B
|
||||
# make install
|
||||
```
|
||||
By the way, the `autoreconf` step may take quite a while – please be patient!
|
||||
|
||||
## 4. Test
|
||||
At this point, Shairport Sync should be built and installed but not running. If the user you are logged in as is a member of the unix `audio` group, Shairport Sync should run from the command line:
|
||||
At this point, Shairport Sync should be built and installed but not running. Now you can test it out from the command line. Before you start testing, though, if you have built Shairport Sync for AirPlay 2 operation, ensure `NQPTP` is running.
|
||||
|
||||
To check the installation, enter:
|
||||
```
|
||||
$ shairport-sync
|
||||
```
|
||||
* Add the `-v` command line option to get some diagnostics.
|
||||
* Add the `--statistics` option to get some infomation about the audio received.
|
||||
|
||||
The AirPlay service should appear on the network and the audio you play should come through to the default ALSA device. (Use `alsamixer` or similar to adjust levels.)
|
||||
|
||||
If you have problems, please check the items in Final Notes below, or in the [TROUBLESHOOTING.md](TROUBLESHOOTING.md) guide.
|
||||
The AirPlay service should appear on the network and the audio you play should come through to the default audio output device. You can use the AirPlay volume control or the system's volume controls or a command-line tool like `alsamixer` to adjust levels.
|
||||
|
||||
Note: Shairport Sync will run indefinitely -- use Control-C it to stop it.
|
||||
If you have problems, please check the hints in Final Notes below, or in the [TROUBLESHOOTING.md](TROUBLESHOOTING.md) guide.
|
||||
|
||||
When you are finished running Shairport Sync, use Control-C it to stop it.
|
||||
|
||||
## 5. Enable and Start Service
|
||||
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/master/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
|
||||
If your system uses either PipeWire or PulseAudio as sound servers (most "desktop" Linuxes use one or the other), Shairport Sync must be started as a user service. This is because the PipeWire or PulseAudio services -- needed by Shairport Sync -- are user services themselves, and they must be running before Shairport Sync starts. That implies that Shairport Sync must be started as a user service.
|
||||
|
||||
#### Checking for PipeWire or PulseAudio
|
||||
If PipeWire is installed and running, the following command should return status information, as follows:
|
||||
```
|
||||
$ systemctl --user status pipewire
|
||||
● pipewire.service - PipeWire Multimedia Service
|
||||
Loaded: loaded (/usr/lib/systemd/user/pipewire.service; enabled; preset: enabled)
|
||||
...
|
||||
```
|
||||
If not, it will return something like this:
|
||||
```
|
||||
$ systemctl --user status pipewire
|
||||
Unit pipewire.service could not be found.
|
||||
```
|
||||
Similarly, if PulseAudio is installed, the following command return status information:
|
||||
```
|
||||
$ systemctl --user status pulseaudio
|
||||
```
|
||||
If PipeWire or PulseAudio is installed, you must enable Shairport Sync as a _user service_ only.
|
||||
|
||||
#### Enable Shairport Sync as a User Service
|
||||
|
||||
To enable Shairport Sync as a user service that starts automatically when the user logs in, ensure you are logged in as that user and enter:
|
||||
```
|
||||
$ systemctl --user enable shairport-sync
|
||||
```
|
||||
#### Enable Shairport Sync as a System Service
|
||||
To enable Shairport Sync as a system service that starts automatically when the system starts up, enter:
|
||||
```
|
||||
# systemctl enable shairport-sync
|
||||
```
|
||||
#### User Service Limitations.
|
||||
1. If Shairport Sync is installed as a user service, it is activated when that user logs in and deactivated when the user logs out.
|
||||
On an unattended system, this difficulty can be overcome by using automatic user login.
|
||||
2. If your system has a GUI (that is, if it's a "desktop Linux"), then audio will only be routed to the speakers when the user is logged in through the GUI.
|
||||
|
||||
|
||||
### FreeBSD
|
||||
To make the `shairport-sync` daemon load at startup, add the following line to `/etc/rc.conf`:
|
||||
```
|
||||
shairport_sync_enable="YES"
|
||||
```
|
||||
## 6. Check
|
||||
Reboot the machine. The AirPlay service should once again be visible on the network and audio will be sent to the default ALSA device.
|
||||
Reboot the machine. The AirPlay service should once again be visible on the network and audio will be sent to the default audio output.
|
||||
|
||||
## 7. Final Notes
|
||||
A number of system settings can affect Shairport Sync. Please review them as follows:
|
||||
### ALSA Output Devices
|
||||
If your system is _not_ using PipeWire or PulseAudio (see [above](#checking-for-pipewire-or-pulseaudio)), it probably uses ALSA directly. The following hints might be useful:
|
||||
1. To access ALSA output devices, a user must be in the `audio` group. To add the current user to the `audio` group, enter:
|
||||
```
|
||||
$ sudo usermod -aG audio $USER
|
||||
```
|
||||
2. Ensure that the ALSA default output device is not muted and has the volume turned up -- `alsamixer` is very useful for this.
|
||||
3. To explore the ALSA output devices on your system, consider using the [dacquery](https://github.com/mikebrady/dacquery) tool.
|
||||
|
||||
### Power Saving
|
||||
If your computer has an `Automatic Suspend` Power Saving Option, you should experiment with disabling it, because your computer has to be available for AirPlay service at all times.
|
||||
@@ -194,6 +255,10 @@ If a firewall is running (some systems, e.g. Fedora, run a firewall by default),
|
||||
### Add to Home
|
||||
With AirPlay 2, you can follow the steps in [ADDINGTOHOME.md](ADDINGTOHOME.md) to add your device to the Apple Home system.
|
||||
### Wait, there's more...
|
||||
Instead of using default values for everything, you can use the configuration file to get finer control over the setup, particularly the output device and mixer control -- see [Finish Setting Up](ADVANCED%20TOPICS/InitialConfiguration.md).
|
||||
|
||||
At this point, you should have a basic functioning Shairport Sync installation. If you want more control – for example, using the ALSA backend, if you want to use a specific DAC, or if you want AirPlay to control the DAC's volume control – you can use settings in the configuration file or you can use command-line options.
|
||||
|
||||
#### Configuration File
|
||||
When you run `# make install`, a configuration file is installed if one doesn't already exist. Additionally, a sample configuration file called `shairport-sync.conf.sample` is _always_ installed. This contains all the setting groups and all the settings available, commented out so that default values are used. The file contains explanations of the settings, useful hints and suggestions. The configuration file and the sample configuration file are installed in the `sysconfdir` you specified at the `./configure...` step above.
|
||||
|
||||
Please take a look at [Advanced Topics](ADVANCED%20TOPICS/README.md) for some ideas about what else you can do to enhance the operation of Shairport Sync. For example, you can adjust synchronisation to compensate for delays in your system.
|
||||
|
||||
+18
-16
@@ -11,7 +11,7 @@ Please note that Android phones and tablets can not, so far, do this trick of us
|
||||
|
||||
If you are updating an existing installation, please refer to the [updating](#updating) section below.
|
||||
|
||||
In this example, a Raspberry Pi Zero 2 W and a Pimoroni PHAT DAC are used. Shairport Sync will be built for AirPlay 2 operation, but you can build it for "classic" AirPlay (aka AirPlay 1) operation if you prefer. A Pi Zero W is powerful enough for classic AirPlay.
|
||||
In this example, a Raspberry Pi Zero with WiFi and a Pimoroni PHAT DAC are used. Shairport Sync will be built for AirPlay 2 operation, but you can build it for "classic" AirPlay (aka AirPlay 1) operation if you prefer.
|
||||
|
||||
Please note that some of the details of setting up networks are specific to the version of Linux used.
|
||||
|
||||
@@ -20,7 +20,7 @@ Please note that some of the details of setting up networks are specific to the
|
||||
* Before writing the image to the card, use the Settings control on `Raspberry Pi Imager` to set hostname, enable SSH and provide a username and password to use while building the system. Similarly, you can specify a wireless network the Pi will connect to while building the system. Later on, the Pi will be configured to start its own isolated network.
|
||||
* The next few steps are to add the overlay needed for the sound card. This may not be necessary in your case, but in this example a Pimoroni PHAT is being used. If you do not need to add an overlay, skip these steps.
|
||||
* Mount the card on a Linux machine. Two drives should appear – a `boot` drive and a `rootfs` drive.
|
||||
* `cd` to the `boot` drive (since my username is `mike`, it will be `$ cd /media/mike/boot`).
|
||||
* `cd` to the `firmware` director on the `boot` drive (since my username is `mike`, it will be `$ cd /media/mike/boot/firmware`).
|
||||
* Edit the `config.txt` file to add the overlay needed for the sound card. This may not be necessary in your case, but in this example a Pimoroni PHAT is being used and it needs the following entry to be added:
|
||||
```
|
||||
dtoverlay=hifiberry-dac
|
||||
@@ -41,16 +41,17 @@ The first thing to do on a Pi would be to use the `raspi-config` tool to expand
|
||||
Let's get the tools and libraries for building and installing Shairport Sync (and NQPTP).
|
||||
|
||||
```
|
||||
# apt install --no-install-recommends build-essential git xmltoman autoconf automake libtool \
|
||||
# apt install --no-install-recommends build-essential git 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
|
||||
libplist-dev libsodium-dev uuid-dev libgcrypt-dev xxd libplist-utils \
|
||||
libavutil-dev libavcodec-dev libavformat-dev
|
||||
```
|
||||
If you are building classic Shairport Sync, the list of packages is shorter:
|
||||
```
|
||||
# apt-get install --no-install-recommends build-essential git xmltoman autoconf automake libtool \
|
||||
libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev
|
||||
# apt-get install --no-install-recommends build-essential git autoconf automake libtool \
|
||||
libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev \
|
||||
libavutil-dev libavcodec-dev libavformat-dev
|
||||
```
|
||||
|
||||
#### NQPTP
|
||||
Skip this section if you are building classic Shairport Sync – NQPTP is not needed for classic Shairport Sync.
|
||||
|
||||
@@ -59,19 +60,18 @@ Download, install, enable and start NQPTP from [here](https://github.com/mikebra
|
||||
#### Shairport Sync
|
||||
Download Shairport Sync, configure, compile and install it.
|
||||
|
||||
* Omit the `--with-airplay-2` from the `./configure` options if you are building classic Shairport Sync.
|
||||
* Replace the `--with-airplay-2` with `--with-ffmpeg` in the `./configure` options if you are building classic Shairport Sync.
|
||||
|
||||
```
|
||||
$ git clone https://github.com/mikebrady/shairport-sync.git
|
||||
$ cd shairport-sync
|
||||
$ autoreconf -fi
|
||||
$ autoreconf -fi # about 1.5 minutes on a Raspberry Pi B
|
||||
$ ./configure --sysconfdir=/etc --with-alsa \
|
||||
--with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-airplay-2
|
||||
$ make
|
||||
$ make # about 6 minutes on a Raspberry Pi B
|
||||
# make install
|
||||
# systemctl enable shairport-sync
|
||||
```
|
||||
The `autoreconf` step may take quite a while – please be patient!
|
||||
|
||||
### Configure Shairport Sync
|
||||
Here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`:
|
||||
@@ -160,7 +160,9 @@ INTERFACESv4="wlan0"
|
||||
INTERFACESv6=""
|
||||
```
|
||||
### Set up the Startup Sequence
|
||||
Configure the startup sequence by adding commands to `/etc/rc.local` to start `hostapd` and the `dhcp` automatically after startup. Its contents should look like this:
|
||||
Firstly, if the file `/etc/rc.local` doesn't already exist, create it, ensure it is owned by `root` and set its permissions are `755`; otherwise, it won't be executed at startup.
|
||||
|
||||
Now, configure the startup sequence by adding commands to `/etc/rc.local` to start `hostapd` and the `dhcp` automatically after startup. Its contents should look like this:
|
||||
```
|
||||
#!/bin/sh -e
|
||||
#
|
||||
@@ -211,7 +213,7 @@ Some services are not necessary for this setup and can be disabled as follows:
|
||||
|
||||
|
||||
#### Disable Unused Services - Mandatory
|
||||
You now need to disable some services; that is, you need to stop them starting automatically on power-up. This is because they either interfere with the system's operation in WiFi Access Point mode, or because they won't work when the system isn't connected to the Internet. Only one of the `NetworkManager` and the `dhcpcd` service will be present in your system, but it's no harm to try to disable both.
|
||||
You now need to disable some services; that is, you need to stop them starting automatically on power-up. This is because they either interfere with the system's operation in WiFi Access Point mode, or because they won't work when the system isn't connected to the Internet. Only one of the `NetworkManager` and the `dhcpcd` services will be present in your system, but it's no harm to try to disable both.
|
||||
```
|
||||
# systemctl disable dhcpcd
|
||||
# systemctl disable NetworkManager
|
||||
@@ -240,13 +242,13 @@ When the power source is switched on -- typically when you start the car -- it w
|
||||
## Updating
|
||||
From time to time, you may wish to update this installation. Assuming you haven't deleted your original WiFi network credentials, the easiest thing is to temporarily reconnect to the network you used when you created the system. You can then update the operating system and libraries in the normal way and then update Shairport Sync.
|
||||
|
||||
However, if you're *upgrading* the operating system to e.g. from Bullseye to Bookworm, the names and index numbers of the output devices may change, and the names of the mixer controls may also change. You can use [`sps-alsa-explore`](https://github.com/mikebrady/sps-alsa-explore) to discover device names and mixer names.
|
||||
However, if you're *upgrading* the operating system to e.g. from Bullseye to Bookworm, the names and index numbers of the output devices may change, and the names of the mixer controls may also change. You can use [`dacquery`](https://github.com/mikebrady/dacquery) to discover device names and mixer names.
|
||||
|
||||
#### Exit Raspberry Pi Read-Only Mode
|
||||
If it's a Raspberry Pi and you have optionally enabled the read-only mode, you must take the device out of Read-only mode:
|
||||
Run `sudo raspi-config` and then choose `Performance Options` > `Overlay Filesystem` and choose to disable the overlay filesystem and to set the boot partition not to be write-protected. This is so that changes can be written to the file system; you can make the filesystem read-only again later. Save the changes and reboot the system.
|
||||
#### Undo Optimisations
|
||||
If you have disabled any of the services listed in the [Disable Unused Services - Optional](#disable-unused-services---optional) section, you should re-enable them. (But *do not* re-eneable `NetworkManager`, `dhcpcd`, `wpa_supplicant` or `systemd-timesyncd` -- they are handled specially by the startup script.)
|
||||
If you have disabled any of the services listed in the [Disable Unused Services - Optional](#disable-unused-services---optional) section, you should re-enable them. (But *do not* re-enable `NetworkManager`, `dhcpcd`, `wpa_supplicant` or `systemd-timesyncd` -- they are handled specially by the startup script.)
|
||||
#### Perform Legacy Updates
|
||||
Over time, the arrangements by which the system is prepared for operation has changed to make it easier to revert to normal operation when necessary for maintenance, updates, etc. A small number of the old settings need to be changed to bring them up to date with the present arrangements. Once the required changes have been made, your system will be ready for the update process detailed below. Here are those legacy changes you need to make, just once:
|
||||
|
||||
@@ -260,7 +262,7 @@ Over time, the arrangements by which the system is prepared for operation has ch
|
||||
|
||||
2. Replace the contents of the file `/etc/rc.local` with the new contents given [above](#set-up-the-startup-sequence).
|
||||
|
||||
3. Disable a number of services as follows. Only one of the `NetworkManager` and the `dhcpcd` service will be present in your system, but it's no harm to try to disable both.
|
||||
3. Disable a number of services as follows. Only one of the `NetworkManager` and the `dhcpcd` services will be present in your system, but it's no harm to try to disable both.
|
||||
```
|
||||
# systemctl disable dhcpcd
|
||||
# systemctl disable NetworkManager
|
||||
|
||||
+24
-26
@@ -11,22 +11,21 @@ In the following sections, Shairport Sync's compilation configuration options ar
|
||||
| ---- |
|
||||
| `--with-airplay-2` |
|
||||
|
||||
AirPlay 2 is the current version of the AirPlay protocol. It offers multi-room operation and integration with the Home application. However, the Shairport Sync implementation is doesn't support iTunes on Windows, and its integration with the Home app and support for remote control is incomplete. Additionally, it requires a somewhat more powerful computer system (Raspberry Pi 2 equivalent or better) and a recent (2018 or later) version of a Debian-like Linux, Alpine Linux or FreeBSD. It has not been tested on other Linux distributions such as OpenWrt. Finally, AirPlay 2 can be lossy – in one mode of operation, audio is encoded in 256kbps AAC.
|
||||
_AirPlay 2_ is the current version of the AirPlay protocol. It offers multi-room operation and integration with the Home application. However, the Shairport Sync implementation is doesn't support iTunes on Windows, and its integration with the Home app and support for remote control is incomplete. Additionally, it requires a fairly recent (2018 or later) version of a Debian-like Linux, Alpine Linux or FreeBSD. It has not been tested on other Linux distributions such as OpenWrt. Finally, AirPlay 2 can be lossy – in one mode of operation, audio is encoded in 256kbps AAC.
|
||||
|
||||
AirPlay (aka AirPlay 1) is an older version of the AirPlay protocol. If offers multi-room operation to iTunes on macOS or Windows, the Music app on macOS and some third-party computer applications such as [OwnTone](https://owntone.github.io/owntone-server/). It will run on lower powered machines, e.g. the original Raspberry Pi and many embedded devices. This version of AirPlay is lossless – audio is received in 44,100 frames per second 16-bit interleaved stereo ALAC format. It is compatible with a wider range of Linux distributions, back to around 2012. However, support for this version of AirPlay seems to be gradually waning. It does not offer multi-room operation to iOS, iPadOS or AppleTV and is incompatible with HomePod. It is not integrated with the Home app.
|
||||
_AirPlay_ (without the "2", aka "classic AirPlay" or "AirPlay 1") is an older version of the AirPlay protocol. If offers multi-room operation to iTunes on macOS or Windows, the Music app on macOS and some third-party computer applications such as [OwnTone](https://owntone.github.io/owntone-server/). It will run on lower powered machines, including many embedded devices. This version of AirPlay is lossless – audio is received in 44,100 frames per second 16-bit interleaved stereo ALAC format. It is compatible with a wider range of Linux distributions, back to around 2012. However, support for this version of AirPlay seems to be gradually waning. It does not offer multi-room operation to iOS, iPadOS or AppleTV and is incompatible with HomePod. It is not integrated with the Home app.
|
||||
|
||||
To build Shairport Sync for AirPlay 2, include the `--with-airplay-2` option in the `./configure ...` options. You will also have to include extra libraries. Omitting this option will cause Shairport Sync to be built for the older AirPlay protocol.
|
||||
To build Shairport Sync for AirPlay 2, include the `--with-airplay-2` option in the `./configure ...` options. You will also have to include extra libraries. Omitting this option will cause Shairport Sync to be built for the older AirPlay protocol. Include the `--with-ffmpeg` to build for classic AirPlay but using the FFmpeg libraries for decoding and for transcoding. This is recommended for classic AirPlay because it is better maintained and more flexible. Unfortunately, the FFmpeg library is very bulky and so many embedded devices simply don't have space for it.
|
||||
|
||||
## Audio Output
|
||||
| Flags |
|
||||
| ----- |
|
||||
| `--with-alsa` |
|
||||
| `--with-sndio` |
|
||||
| `--with-pa` |
|
||||
| `--with-pw` |
|
||||
| `--with-ao` |
|
||||
| `--with-pipewire` |
|
||||
| `--with-pulseaudio` |
|
||||
| `--with-jack` |
|
||||
| `--with-soundio` |
|
||||
| `--with-ao` |
|
||||
| `--with-stdout` |
|
||||
| `--with-pipe` |
|
||||
|
||||
@@ -36,20 +35,17 @@ Here are the audio backend configuration options:
|
||||
|
||||
- `--with-alsa` Output to the Advanced Linux Sound Architecture ([ALSA](https://www.alsa-project.org/wiki/Main_Page)) system. This is recommended for highest quality.
|
||||
- `--with-sndio` Output to the FreeBSD-native [sndio](https://sndio.org) system.
|
||||
- `--with-pa` Include the PulseAudio audio back end.
|
||||
- `--with-pw` Output to the [PipeWire](https://pipewire.org) system.
|
||||
- `--with-ao` Output to the [libao](https://xiph.org/ao/) system. No synchronisation.
|
||||
- `--with-pipewire` Output to the [PipeWire](https://pipewire.org) sound server.
|
||||
- `--with-pulseaudio` Include the [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio) sound server.
|
||||
- `--with-jack` Output to the [Jack Audio](https://jackaudio.org) system.
|
||||
- `--with-soundio` Include an optional backend module for audio to be output through the [`soundio`](http://libsound.io) system. No synchronisation.
|
||||
- `--with-ao` Output to the [libao](https://xiph.org/ao/) system. No synchronisation.
|
||||
- `--with-stdout` Include an optional backend module to enable raw audio to be output through standard output (`STDOUT`).
|
||||
- `--with-pipe` Include an optional backend module to enable raw audio to be output through a unix pipe.
|
||||
|
||||
### PulseAudio and PipeWire
|
||||
Many recent Linux distributions with a GUI -- "desktop" Linuxes -- use PulseAudio or PipeWire to handle sound. There are two things to consider with these sound servers:
|
||||
1. They may not always be available: a sound server generally becomes available when a user logs in via the GUI and disappears when the user logs out; it is not available when the system starts up and it is not available to non-GUI users. This means that Shairport Sync can not run as a daemon (see "Daemonisation" below) using a sound server unless the sound server is configured as a system-wide service.
|
||||
2. The fidelity of the audio is unknown: once audio is delivered to the sound server, it is unknown what happens to it as it is processed through PulseAudio to arrives eventually at the loudspeakers.
|
||||
If your system uses either PipeWire or PulseAudio as sound servers, Shairport Sync must be started as a user service. This is because the PipeWire or PulseAudio services -- needed by Shairport Sync -- are user services themselves, and they must be running before Shairport Sync starts. That implies that Shairport Sync must be started as a user service.
|
||||
|
||||
It should be noted that both PulseAudio and PipeWire provide a default ALSA pseudo device that enables ALSA-compatible programs to send audio. Shairport Sync can therefore use the ALSA backend with PulseAudio- or PipeWire-based systems.
|
||||
PipeWire and PulseAudio provide a default ALSA pseudo device, so that Shairport Sync can therefore use the ALSA backend with PipeWire- or PulseAudio-based systems.
|
||||
|
||||
## Audio Options
|
||||
| Flags |
|
||||
@@ -57,10 +53,12 @@ It should be noted that both PulseAudio and PipeWire provide a default ALSA pseu
|
||||
| `--with-soxr` |
|
||||
| `--with-apple-alac` |
|
||||
| `--with-convolution` |
|
||||
| `--with-ffmpeg` |
|
||||
|
||||
- `--with-soxr` Allows Shairport Sync to use [libsoxr](https://sourceforge.net/p/soxr/wiki/Home/)-based resampling for improved interpolation. Recommended.
|
||||
- `--with-apple-alac` Allows Shairport Sync to use the Apple ALAC Decoder. Requires [`libalac`](https://github.com/mikebrady/alac).
|
||||
- `--with-convolution` Includes a convolution filter that can be used to apply effects such as frequency and phase correction, and a loudness filter that compensates for the non-linearity of the human auditory system. Requires `libsndfile`.
|
||||
- `--with-soxr` Enables Shairport Sync to use [libsoxr](https://sourceforge.net/p/soxr/wiki/Home/)-based resampling for improved interpolation. Recommended.
|
||||
- `--with-apple-alac` Enables Shairport Sync to use the Apple ALAC Decoder. Requires [`libalac`](https://github.com/mikebrady/alac). Deprecated due to security issues.
|
||||
- `--with-convolution` Includes a convolution filter that can be used to apply effects such as frequency and phase correction, and a loudness filter that compensates for the non-linearity of the human auditory system. Requires `libsndfile`. Note that this is only available with audio at 44100 frames per second at present.
|
||||
- `--with-ffmpeg` Enables classic Shairport Sync to use the [FFmpeg](https://ffmpeg.org) ALAC decoder and the FFmpeg software resampler to transcode, e.g. from 44100 to 48000 frames per second. Requires FFmpeg libraries. (Note: this is for Classic AirPlay only -- `--with-ffmpeg` is automatically enabled for AirPlay 2.)
|
||||
|
||||
## Metadata
|
||||
| Flags |
|
||||
@@ -120,18 +118,18 @@ FreeBSD and most recent Linux distributions can run an application as a daemon w
|
||||
### Automatic Start
|
||||
| Flags |
|
||||
| ----- |
|
||||
| `--with-systemd` |
|
||||
| `--with-systemd-startup` |
|
||||
| `--with-systemdsystemunitdir=<dir>` |
|
||||
| `--with-systemv` |
|
||||
| `--with-freebsd-service` |
|
||||
| `--with-sygwin-service` |
|
||||
| `--with-systemv-startup` |
|
||||
| `--with-freebsd-startup` |
|
||||
| `--with-cygwin-service` |
|
||||
|
||||
Daemon programs such as Shairport Sync need to be started automatically, so that the service they provide becomes available without further intervention. Typically this is done using startup scripts. Four options are provided – two for Linux, one for FreeBSD and one for CYGWIN. In Linux, the choice depends on whether [systemd](https://en.wikipedia.org/wiki/Systemd) is used or not. If `systemd` is installed, then the `--with-systemd` option is suggested. If not, the `--with-systemv` option is suggested.
|
||||
Daemon programs such as Shairport Sync should be started automatically so that the service they provide becomes available without further intervention. Typically this is done using startup scripts. Four options are provided – two for Linux, one for FreeBSD and one for CYGWIN. In Linux, the choice depends on whether [systemd](https://en.wikipedia.org/wiki/Systemd) is used or not. If `systemd` is installed, then the `--with-systemd-startup` option is suggested. If not, the `--with-systemv-startup` option is suggested.
|
||||
|
||||
- `--with-systemd` Includes a script to create a Shairport Sync service that can optionally launch automatically at startup on `systemd`-based Linuxes. Default is not to to install. Note: an associated special-purpose option allows you to specify where the `systemd` service file will be placed:
|
||||
- `--with-systemd-startup` Includes scripts to create a Shairport Sync service that can optionally launch automatically at startup or at user login on `systemd`-based Linuxes. Default is not to to install. Note: an associated special-purpose option allows you to specify where the `systemd` system startup file will be placed:
|
||||
- `--with-systemdsystemunitdir=<dir>` Specifies the directory for `systemd` service files.
|
||||
- `--with-systemv` Includes a script to create a Shairport Sync service that can optionally launch automatically at startup on System V based Linuxes. Default is not to to install.
|
||||
- `--with-freebsd-service` Includes a script to create a Shairport Sync service that can optionally launch automatically at startup on FreeBSD. Default is not to to install.
|
||||
- `--with-systemv-startup` Includes a script to create a Shairport Sync service that can optionally launch automatically at startup on System V based Linuxes. Default is not to to install.
|
||||
- `--with-freebsd-startup` Includes a script to create a Shairport Sync service that can optionally launch automatically at startup on FreeBSD. Default is not to to install.
|
||||
- `--with-cygwin-service` Includes a script to create a Shairport Sync service that can optionally launch automatically at startup on CYGWIN. Default is not to to install.
|
||||
|
||||
### Cryptography
|
||||
|
||||
+65
-63
@@ -1,24 +1,33 @@
|
||||
ARFLAGS = cr
|
||||
|
||||
man_MANS = $(top_srcdir)/man/shairport-sync.1
|
||||
# To be able to ref to the user's own home directory:
|
||||
homedir = @HOME@
|
||||
|
||||
lib_pair_ap_a_CFLAGS = -Wall -g -DCONFIG_GCRYPT -pthread
|
||||
# Note that xmlmantohtml isn't producing correct HTML.
|
||||
# Uncomment the if/else and SUBDIR lines in this stanza to make xmltoman build shairport-sync.1
|
||||
# if USE_XMLTOMAN
|
||||
# SUBDIRS = man
|
||||
# else
|
||||
man_MANS = $(top_srcdir)/man/shairport-sync.1
|
||||
# endif
|
||||
|
||||
lib_pair_ap_a_CFLAGS = -Wall -g -DCONFIG_GCRYPT -pthread
|
||||
lib_tinyhttp_a_CFLAGS = -pthread
|
||||
lib_dbus_interface_a_CFLAGS = -pthread
|
||||
lib_mpris_interface_a_CFLAGS = -pthread
|
||||
|
||||
|
||||
bin_PROGRAMS = shairport-sync
|
||||
|
||||
# BUILT_SOURCES get built before anything else (?)
|
||||
|
||||
BUILT_SOURCES =
|
||||
noinst_HEADERS =
|
||||
CLEANFILES =
|
||||
shairport_sync_LDADD =
|
||||
noinst_LIBRARIES =
|
||||
|
||||
# See below for the flags for the test client program
|
||||
|
||||
shairport_sync_SOURCES = shairport.c rtsp.c mdns.c common.c rtp.c player.c alac.c audio.c loudness.c activity_monitor.c
|
||||
shairport_sync_SOURCES = shairport.c rtsp.c mdns.c common.c rtp.c player.c audio.c loudness.c activity_monitor.c
|
||||
|
||||
if BUILD_FOR_DARWIN
|
||||
AM_CXXFLAGS = -I/usr/local/include -Wno-multichar -Wall -Wextra -Wno-deprecated-declarations -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
@@ -32,28 +41,25 @@ if BUILD_FOR_OPENBSD
|
||||
AM_CXXFLAGS = -I/usr/local/include -Wno-multichar -Wall -Wextra -Wno-clobbered -Wno-psabi -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
AM_CFLAGS = -Wno-multichar -Wall -Wextra -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
else
|
||||
AM_CXXFLAGS = -fno-common -Wno-multichar -Wall -Wextra -Wno-clobbered -Wno-psabi -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
AM_CFLAGS = -fno-common -Wno-multichar -Wall -Wextra -Wno-clobbered -Wno-psabi -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
shairport_sync_SOURCES += scripts/shairport-sync.service scripts/shairport-sync.service-avahi scripts/shairport-sync
|
||||
AM_CXXFLAGS = -Wshadow -fno-common -Wno-multichar -Wall -Wextra -Wno-clobbered -Wno-psabi -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
AM_CFLAGS = -Wshadow -fno-common -Wno-multichar -Wall -Wextra -Wno-clobbered -Wno-psabi -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
# include information generated by 'git describe --tags --dirty --broken' if requested
|
||||
# include version information from git if available
|
||||
if USE_GIT_VERSION
|
||||
shairport_sync_SOURCES += gitversion.c
|
||||
gitversion.h: .git/index
|
||||
printf "// Do not edit!\n" > gitversion.h
|
||||
printf "// This file is automatically generated by 'git describe --tags --dirty --broken', if available.\n" >> gitversion.h
|
||||
printf " char git_version_string[] = \"" >> gitversion.h
|
||||
git describe --tags --dirty --broken | tr -d '[[:space:]]' >> gitversion.h
|
||||
printf "\";\n" >> gitversion.h
|
||||
gitversion.c: gitversion.h
|
||||
touch gitversion.c
|
||||
BUILT_SOURCES += gitversion.c gitversion.h
|
||||
noinst_HEADERS += $(BUILT_SOURCES)
|
||||
# Correctly clean the generated headers, but keep the xml description
|
||||
CLEANFILES += $(BUILT_SOURCES)
|
||||
## Check if the git version information has changed and rebuild gitversion.h if so
|
||||
.PHONY: gitversion-check
|
||||
gitversion-check:
|
||||
$(top_srcdir)/verify-gitversion
|
||||
|
||||
BUILT_SOURCES += gitversion-check
|
||||
CLEANFILES += gitversion-stamp gitversion.h
|
||||
endif
|
||||
|
||||
if USE_HAMMERTON
|
||||
shairport_sync_SOURCES += alac.c
|
||||
endif
|
||||
|
||||
if USE_APPLE_ALAC
|
||||
@@ -109,11 +115,11 @@ if USE_SOUNDIO
|
||||
shairport_sync_SOURCES += audio_soundio.c
|
||||
endif
|
||||
|
||||
if USE_PA
|
||||
if USE_PULSEAUDIO
|
||||
shairport_sync_SOURCES += audio_pa.c
|
||||
endif
|
||||
|
||||
if USE_PW
|
||||
if USE_PIPEWIRE
|
||||
shairport_sync_SOURCES += audio_pw.c
|
||||
endif
|
||||
|
||||
@@ -142,22 +148,19 @@ lib_tinyhttp_a_SOURCES = tinyhttp/chunk.c tinyhttp/header.c tinyhttp/http.c
|
||||
endif
|
||||
|
||||
if USE_AIRPLAY_2
|
||||
shairport_sync_SOURCES += ptp-utilities.c plist_xml_strings.c
|
||||
shairport_sync_SOURCES += ptp-utilities.c plists/get_info_response.c
|
||||
shairport_sync_LDADD += lib_pair_ap.a
|
||||
lib_pair_ap_a_SOURCES = pair_ap/pair.c pair_ap/pair_fruit.c pair_ap/pair_homekit.c pair_ap/pair-tlv.c
|
||||
noinst_LIBRARIES += lib_pair_ap.a
|
||||
plist_xml_strings.h: plists/get_info_response.xml
|
||||
printf "// Do not edit!\n" > plist_xml_strings.h
|
||||
printf "// This file is automatically generated from files in the plists folder.\n\n" >> plist_xml_strings.h
|
||||
xxd -i $(top_srcdir)/plists/get_info_response.xml - | sed -e 's/[^ ]*plists_get_info_response_xml/plists_get_info_response_xml/g' >> plist_xml_strings.h
|
||||
printf "\n" >> plist_xml_strings.h
|
||||
plist_xml_strings.c: plist_xml_strings.h
|
||||
touch plist_xml_strings.c
|
||||
|
||||
BUILT_SOURCES += plist_xml_strings.c plist_xml_strings.h
|
||||
noinst_HEADERS += $(BUILT_SOURCES)
|
||||
# Correctly clean the generated headers, but keep the xml description
|
||||
CLEANFILES += $(BUILT_SOURCES)
|
||||
plists/get_info_response.h: plists/get_info_response.xml
|
||||
sh $(top_srcdir)/xml_plist_codegen.sh $(top_srcdir)/plists/get_info_response.xml $(abs_builddir)/plists
|
||||
plists/get_info_response.c: plists/get_info_response.h
|
||||
touch plists/get_info_response.c
|
||||
|
||||
BUILT_SOURCES += plists/get_info_response.h plists/get_info_response.c
|
||||
CLEANFILES +=plists/get_info_response.h plists/get_info_response.c
|
||||
|
||||
endif
|
||||
|
||||
if USE_DBUS
|
||||
@@ -166,12 +169,7 @@ noinst_LIBRARIES += lib_dbus_interface.a
|
||||
lib_dbus_interface_a_SOURCES = dbus-interface.c
|
||||
shairport_sync_SOURCES += dbus-service.c
|
||||
BUILT_SOURCES += dbus-interface.h dbus-interface.c
|
||||
# We don't want to install this header
|
||||
noinst_HEADERS += $(BUILT_SOURCES)
|
||||
# Correctly clean the generated headers, but keep the xml description
|
||||
CLEANFILES += $(BUILT_SOURCES)
|
||||
|
||||
|
||||
CLEANFILES += dbus-interface.h dbus-interface.c
|
||||
|
||||
dbus-interface.c: org.gnome.ShairportSync.xml
|
||||
gdbus-codegen --interface-prefix org.gnome --generate-c-code dbus-interface $(top_srcdir)/org.gnome.ShairportSync.xml
|
||||
@@ -185,10 +183,7 @@ noinst_LIBRARIES += lib_mpris_interface.a
|
||||
lib_mpris_interface_a_SOURCES = mpris-interface.c
|
||||
shairport_sync_SOURCES += mpris-service.c
|
||||
BUILT_SOURCES += mpris-interface.h mpris-interface.c
|
||||
# We don't want to install this header
|
||||
noinst_HEADERS += $(BUILT_SOURCES)
|
||||
# Correctly clean the generated headers, but keep the xml description
|
||||
CLEANFILES += $(BUILT_SOURCES)
|
||||
CLEANFILES += mpris-interface.h mpris-interface.c
|
||||
|
||||
mpris-interface.c: org.mpris.MediaPlayer2.xml
|
||||
gdbus-codegen --interface-prefix org.mpris --generate-c-code mpris-interface $(top_srcdir)/org.mpris.MediaPlayer2.xml
|
||||
@@ -226,11 +221,11 @@ endif
|
||||
|
||||
if USE_DBUS
|
||||
|
||||
if INSTALL_CYGWIN_SERVICE
|
||||
if INSTALL_CYGWIN_STARTUP
|
||||
DBUS_POLICY_FILE = scripts/shairport-sync-dbus-policy-cygwin.conf
|
||||
else
|
||||
DBUS_POLICY_FILE = scripts/shairport-sync-dbus-policy.conf
|
||||
endif # INSTALL_CYGWIN_SERVICE
|
||||
endif # INSTALL_CYGWIN_STARTUP
|
||||
|
||||
DBUS_POLICY_INSTALL_TARGET = dbus-policy-install-local
|
||||
|
||||
@@ -242,11 +237,11 @@ endif # USE_DBUS
|
||||
|
||||
if USE_MPRIS
|
||||
|
||||
if INSTALL_CYGWIN_SERVICE
|
||||
if INSTALL_CYGWIN_STARTUP
|
||||
MPRIS_POLICY_FILE = scripts/shairport-sync-mpris-policy-cygwin.conf
|
||||
else
|
||||
MPRIS_POLICY_FILE = scripts/shairport-sync-mpris-policy.conf
|
||||
endif # INSTALL_CYGWIN_SERVICE
|
||||
endif # INSTALL_CYGWIN_STARTUP
|
||||
|
||||
MPRIS_POLICY_INSTALL_TARGET = mpris-policy-install-local
|
||||
|
||||
@@ -261,16 +256,12 @@ INSTALL_GROUP_TARGET = install-group-local
|
||||
$(INSTALL_GROUP_TARGET):
|
||||
getent group shairport-sync &>/dev/null || groupadd -r shairport-sync &>/dev/null
|
||||
|
||||
if INSTALL_CREATE_USER_GROUP
|
||||
INSTALL_USER_TARGET = install-user-local
|
||||
else
|
||||
INSTALL_USER_TARGET =
|
||||
endif
|
||||
|
||||
$(INSTALL_USER_TARGET): $(INSTALL_GROUP_TARGET)
|
||||
getent passwd shairport-sync &>/dev/null || useradd -r -M -g shairport-sync -s /usr/sbin/nologin -G audio shairport-sync &>/dev/null
|
||||
getent passwd shairport-sync &> /dev/null || useradd -r -M -g shairport-sync -s /usr/sbin/nologin -G audio shairport-sync &>/dev/null
|
||||
|
||||
if INSTALL_SYSTEMV
|
||||
if INSTALL_SYSTEMV_STARTUP
|
||||
|
||||
INSTALL_SYSTEMV_TARGET = install-systemv-local
|
||||
|
||||
@@ -282,9 +273,9 @@ $(INSTALL_SYSTEMV_TARGET): scripts/shairport-sync $(INSTALL_USER_TARGET)
|
||||
install -d $(DESTDIR)$(sysconfdir)/init.d
|
||||
[ -e $(DESTDIR)$(sysconfdir)/init.d/shairport-sync ] || install -m 0755 scripts/shairport-sync $(DESTDIR)$(sysconfdir)/init.d
|
||||
|
||||
endif # INSTALL_SYSTEMV
|
||||
endif # INSTALL_SYSTEMV_STARTUP
|
||||
|
||||
if INSTALL_SYSTEMD
|
||||
if INSTALL_SYSTEMD_STARTUP
|
||||
|
||||
if USE_AVAHI
|
||||
SYSTEMD_SERVICE = shairport-sync.service-avahi
|
||||
@@ -298,13 +289,23 @@ INSTALL_SYSTEMD_TARGET = install-systemd-local
|
||||
# will be stored in a scripts folder in the _build_ folder
|
||||
# which will be the source folder if you're not using a separate build folder
|
||||
|
||||
# Install a system service script, but don't replace an existing script
|
||||
$(INSTALL_SYSTEMD_TARGET): scripts/$(SYSTEMD_SERVICE) $(INSTALL_USER_TARGET)
|
||||
install -d $(DESTDIR)$(systemdsystemunitdir)
|
||||
[ -e $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service ] || install -m 0644 scripts/$(SYSTEMD_SERVICE) $(DESTDIR)$(systemdsystemunitdir)/shairport-sync.service
|
||||
|
||||
endif # INSTALL_SYSTEMD
|
||||
SYSTEMD_USER_SERVICE = shairport-sync.user.service
|
||||
|
||||
if INSTALL_FREEBSD_SERVICE
|
||||
INSTALL_SYSTEMD_USER_TARGET = install-systemd-user-local
|
||||
|
||||
# Install a user service script into the current user's ~/.config/systemd/user folder, but don't replace an existing script
|
||||
$(INSTALL_SYSTEMD_USER_TARGET): scripts/$(SYSTEMD_USER_SERVICE) $(INSTALL_USER_TARGET)
|
||||
install -d $(DESTDIR)$(homedir)/.config/systemd/user
|
||||
[ -e $(DESTDIR)$(homedir)/.config/systemd/user/shairport-sync.service ] || install -m 0644 $(top_srcdir)/scripts/$(SYSTEMD_USER_SERVICE) $(DESTDIR)$(homedir)/.config/systemd/user/shairport-sync.service
|
||||
|
||||
endif # INSTALL_SYSTEMD_STARTUP
|
||||
|
||||
if INSTALL_FREEBSD_STARTUP
|
||||
|
||||
# Choose a uid and gid of 801 completely arbitrarity, except that it should be below 1000. FreeBSD doesn't seem to allow you to say "an ID in the range of..."
|
||||
install-freebsd-user-local:
|
||||
@@ -318,9 +319,9 @@ $(INSTALL_FREEBSD_TARGET): scripts/shairport-sync.freebsd install-freebsd-user-l
|
||||
install -d $(DESTDIR)/usr/local/etc/rc.d/
|
||||
install -m 0555 $(top_srcdir)/scripts/shairport-sync.freebsd $(DESTDIR)/usr/local/etc/rc.d/shairport_sync
|
||||
|
||||
endif # INSTALL_FREEBSD_SERVICE
|
||||
endif # INSTALL_FREEBSD_STARTUP
|
||||
|
||||
if INSTALL_CYGWIN_SERVICE
|
||||
if INSTALL_CYGWIN_STARTUP
|
||||
|
||||
INSTALL_CYGWIN_TARGET = install-cygwin-local
|
||||
|
||||
@@ -328,13 +329,14 @@ $(INSTALL_CYGWIN_TARGET): scripts/shairport-sync-config
|
||||
install -d $(DESTDIR)/usr/local/bin
|
||||
[ -e $(DESTDIR)/usr/local/bin/shairport-sync-config ] || install -m 0755 $(top_srcdir)/scripts/shairport-sync-config $(DESTDIR)/usr/local/bin/
|
||||
|
||||
endif # INSTALL_CYGWIN_SERVICE
|
||||
endif # INSTALL_CYGWIN_STARTUP
|
||||
|
||||
install-config-files: $(CONFIG_FILE_INSTALL_TARGET) \
|
||||
$(DBUS_POLICY_INSTALL_TARGET) \
|
||||
$(MPRIS_POLICY_INSTALL_TARGET) \
|
||||
$(INSTALL_SYSTEMV_TARGET) \
|
||||
$(INSTALL_SYSTEMD_TARGET) \
|
||||
$(INSTALL_SYSTEMD_USER_TARGET) \
|
||||
$(INSTALL_FREEBSD_TARGET) \
|
||||
$(INSTALL_CYGWIN_TARGET)
|
||||
|
||||
|
||||
@@ -13,27 +13,25 @@ Shairport Sync does not support AirPlay video or photo streaming.
|
||||
* 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).
|
||||
* The `man` page, detailing command line options, is [here](https://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/master/man/shairport-sync.html).
|
||||
* The `man` page, detailing command line options, is [here](https://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/development/man/shairport-sync.html).
|
||||
* Some advanced topics and developed in [ADVANCED TOPICS](https://github.com/mikebrady/shairport-sync/tree/master/ADVANCED%20TOPICS).
|
||||
|
||||
# Features
|
||||
* Outputs AirPlay audio to [ALSA](https://www.alsa-project.org/wiki/Main_Page), [sndio](http://www.sndio.org), [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/), [Jack Audio](http://jackaudio.org), to a unix pipe or to `STDOUT`. It also has experimental support for [PipeWire](https://pipewire.org) and limited support for [libao](https://xiph.org/ao/) and for [libsoundio](http://libsound.io).
|
||||
* Outputs AirPlay audio to [ALSA](https://www.alsa-project.org/wiki/Main_Page), [sndio](http://www.sndio.org), [PipeWire](https://pipewire.org), [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/), [Jack Audio](http://jackaudio.org), to a unix pipe or to `STDOUT`. It also has limited support for [libao](https://xiph.org/ao/).
|
||||
* Metadata — Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in macOS and iOS.
|
||||
* An interface to [MQTT](https://en.wikipedia.org/wiki/MQTT), a popular protocol for Inter Process Communication, Machine-to-Machine, Internet of Things and Home Automation projects. The interface provides access to metadata and artwork, and has limited remote control.
|
||||
* Digital Signal Processing facilities – please see the [DSP Wiki Page Guide](https://github.com/mikebrady/shairport-sync/wiki/Digital-Signal-Processing-with-Shairport-Sync). (Thanks to [Yann Pomarède](https://github.com/yannpom) for the code and to [Paul Wieland](https://github.com/PaulWieland) for the guide.)
|
||||
* An [MPRIS](https://specifications.freedesktop.org/mpris-spec/2.2/)-like interface, partially complete and very functional, including access to metadata and artwork, and limited remote control.
|
||||
* A native D-Bus interface, including access to metadata and artwork, limited remote control and system settings.
|
||||
* Better Volume Control — Shairport Sync offers finer control at very top and very bottom of the volume range. See http://tangentsoft.net/audio/atten.html for a good discussion of audio "attenuators", upon which volume control in Shairport Sync is modelled. See also the diagram of the volume transfer function in the documents folder. In addition, Shairport Sync can offer an extended volume control range on devices with a restricted range.
|
||||
* Support for the [Apple ALAC decoder](https://macosforge.github.io/alac/) (library available [here](https://github.com/mikebrady/alac)).
|
||||
* Output bit depths of 8, 16, 24 and 32 bits, rather than the standard 16 bits.
|
||||
* Output frame rates of 44,100, 88,200, 176,000 or 352,000 frames per second.
|
||||
* Flexible output rates, formats and channels with built-in transcoding.
|
||||
|
||||
Some features require configuration at build time – see [CONFIGURATION FLAGS.md](CONFIGURATION%20FLAGS.md).
|
||||
|
||||
# Status
|
||||
Shairport Sync was designed to [run best](ADVANCED%20TOPICS/GetTheBest.md) on stable, dedicated, stand-alone low-power "headless" systems with ALSA as the audio system and with a decent CD-quality Digital to Analog Converter (DAC).
|
||||
|
||||
Shairport Sync runs on recent (2018 onwards) Linux systems, FreeBSD from 12.1 onwards and OpenBSD. It requires a system with the power of a Raspberry Pi 2 or a Pi Zero 2 or better.
|
||||
Shairport Sync runs on recent (2018 onwards) Linux systems, FreeBSD from 12.1 onwards and OpenBSD. It requires a system with the power of a Raspberry Pi B or better.
|
||||
|
||||
Classic Shairport Sync runs on a wider variety of Linux sytems, including OpenWrt and Cygwin and it also runs on OpenBSD. Many embedded devices are powerful enough to power classic Shairport Sync.
|
||||
|
||||
@@ -42,7 +40,7 @@ The functionality offered by Shairport Sync is the result of study and analysis
|
||||
|
||||
Shairport Sync is a substantial rewrite of the fantastic work done in Shairport 1.0 by James Wah (aka [abrasive](https://github.com/abrasive)), James Laird and others — please see [this list](https://github.com/abrasive/shairport/blob/master/README.md#contributors-to-version-1x) of the contributors to Shairport 1.x and Shairport 0.x. From a "heritage" point of view, Shairport Sync is a fork of Shairport 1.0.
|
||||
|
||||
For the development of AirPlay 2 support, special thanks are due to:
|
||||
For the development of AirPlay 2 support in Version 4.x, special thanks are due to:
|
||||
* [JD Smith](https://github.com/jdtsmith) for really thorough testing, support and encouragement.
|
||||
* [ejurgensen](https://github.com/ejurgensen) for advice and [code to deal with pairing and encryption](https://github.com/ejurgensen/pair_ap).
|
||||
* [ckdo](https://github.com/ckdo) for pointing the way, particularly with pairing and encryption protocols, with a [functional Python implementation](https://github.com/ckdo/airplay2-receiver) of AirPlay 2.
|
||||
@@ -54,20 +52,20 @@ Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at
|
||||
Thanks to everyone who has supported and improved Shairport Sync over the years.
|
||||
|
||||
# More about Shairport Sync
|
||||
The audio that Shairport Sync receives is sent to the computer's sound system, to a named unix pipe or to `STDOUT`. By far the best sound system to use is ALSA. This is because ALSA can give direct access to the Digital to Analog Converter (DAC) hardware of the machine. Audio samples can be sent through ALSA directly to the DAC, maximising fidelity, and accurate timing information can be obtained from the DAC, maximising synchronisation. Direct access to hardware is given through ALSA devices with names beginning with `hw:`.
|
||||
The audio that Shairport Sync receives is sent to the computer's sound system, to a named unix pipe or to `STDOUT`. By far the best sound system to use is ALSA. This is because ALSA can give direct access to the Digital to Analog Converter (DAC) hardware of the machine. Audio samples can be sent through ALSA directly to the DAC, maximising fidelity, and accurate timing information can be obtained from the DAC, maximising synchronisation.
|
||||
|
||||
## Synchronised Audio
|
||||
Shairport Sync offers *full audio synchronisation*. Full audio synchronisation means that audio is played on the output device at exactly the time specified by the audio source. To accomplish this, Shairport Sync needs access to audio systems – such as ALSA on Linux and `sndio` on FreeBSD and OpenBSD – that provide very accurate timing information about audio being streamed to output devices. Ideally, Shairport Sync should have direct access to the output device used, which should be a real sound card capable of working with 44,100, 88,200 or 176,400 samples per second, interleaved PCM stereo of 8, 16, 24 or 32 bits. Using the ALSA sound system, Shairport Sync will choose the greatest bit depth available at 44,100 samples per second, resorting to multiples of 44,100 if it is not available. You'll get a message in the log if there's a problem. With all other sound systems, a sample rate of 44,100 is chosen with a bit depth of 16 bit.
|
||||
Shairport Sync offers *full audio synchronisation*. Full audio synchronisation means that audio is played on the output device at exactly the time specified by the audio source. To accomplish this, Shairport Sync needs access to audio systems – such as ALSA on Linux and `sndio` on FreeBSD and OpenBSD – that provide very accurate timing information about audio being streamed to output devices. Ideally, Shairport Sync should have direct access to the output device used, which should be a real sound card capable of working with 44,100 or 48,000 samples ("frames") per second, interleaved PCM stereo of 8, 16, 24 or 32 bits. Shairport Sync will choose a suitable output rate, format and channel count. This can be done manually or automatically.
|
||||
|
||||
Shairport Sync works well with PulseAudio, a widely used sound server found on many desktop Linuxes. While the timing information is not as accurate as that of ALSA or `sndio`, it is often impractical to remove or disable PulseAudio.
|
||||
Shairport Sync works well with sound servers such as PipeWire and PulseAudio, widely used sound servers found on many desktop Linuxes. While the timing information is not as accurate as that of ALSA or `sndio`, it is often impractical to bypass these systems.
|
||||
|
||||
For other use cases, Shairport Sync can provide synchronised audio output to a unix pipe or to `STDOUT`, or to audio systems that do not provide timing information. This could perhaps be described as *partial audio synchronisation*, where synchronised audio is provided by Shairport Sync, but what happens to it in the subsequent processing chain, before it reaches the listener's ear, is outside the control of Shairport Sync.
|
||||
|
||||
## Latency, "Stuffing", Timing
|
||||
AirPlay protocols use an agreed *latency* – a time lag or delay – between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). Latency gives players time to compensate for network delays, processing time variations and so on. The latency is specified by the audio source when it negotiates with Shairport Sync. AirPlay sources set a latency of around 2.0 to 2.25 seconds. AirPlay 2 can use shorter latencies, around half a second.
|
||||
|
||||
As mentioned previously, Shairport Sync implements full audio synchronisation when used with ALSA, `sndio` or PulseAudio audio systems. This is done by monitoring the timestamps present in data coming from the audio source and the timing information coming back from the audio system itself. To maintain the latency required for exact synchronisation, if the output device is running slow relative to the source, Shairport Sync will delete frames of audio to allow the device to keep up. If the output device is running fast, Shairport Sync will insert ("stuff") extra frames to keep time. The number of frames inserted or deleted is so small as to be almost inaudible on normal audio material. Frames are inserted or deleted as necessary at pseudorandom intervals. Alternatively, with `libsoxr` support, Shairport Sync can resample the audio feed to ensure the output device can keep up. This is less obtrusive than insertion and deletion but requires a good deal of processing power — most embedded devices probably can't support it. If your computer is fast enough, Shairport Sync will, by default, automatically choose this method.
|
||||
As mentioned previously, Shairport Sync implements full audio synchronisation when used with ALSA, `sndio`, PipeWire or PulseAudio audio systems. This is done by monitoring the timestamps present in data coming from the audio source and the timing information coming back from the audio system itself. To maintain the latency required for exact synchronisation, Shairport Sync will perform interpolation -- effectively shortening or lengthening the stream of audio to exactly match the output rate to the input rate. If the output device is running too slow or too fast relative to the source, Shairport Sync will resample sequences of audio frames to add or remove frames as needed. Higher quality resampling can be achieved with `libsoxr` support, but this requires a good deal of processing power — most embedded devices probably can't support it. If your computer is fast enough, Shairport Sync will automatically choose this method.
|
||||
|
||||
Stuffing is not done for partial audio synchronisation – the audio samples are simply presented at exactly the right time to the next stage in the processing chain.
|
||||
Interpolation is not done for partial audio synchronisation – the audio samples are simply presented at exactly the right time to the next stage in the processing chain.
|
||||
|
||||
Timestamps are referenced relative to the source computer's clock – the "source clock", but timing must be done relative to the clock of the computer running Shairport Sync – the "local clock". So, Shairport Sync synchronises the source clock and the local clock, usually to within a fraction of a millisecond. In AirPlay 2, this is done with the assistance of a companion application called [NQPTP](https://github.com/mikebrady/nqptp) using a [PTP](https://en.wikipedia.org/wiki/Precision_Time_Protocol)-based timing protocol. In classic AirPlay, a variant of [NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol) synchronisation protocols is used.
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
Version 5.0-dev
|
||||
==
|
||||
This is a major update adding exciting new features and improvements to Shairport Sync:
|
||||
* 48k operation;
|
||||
* 48k lossless stereo from Apple devices e.g. Homepod mini, Apple TV 4K;
|
||||
* 48k 5.1 and 7.1 surround sound from Apple devices e.g. Homepod mini, Apple TV 4K;
|
||||
* Full FFmpeg integration for flexibility and security;
|
||||
* Multichannel ALSA, PipeWire, PulseAudio, FreeBSD, stdout and unix pipe output backends;
|
||||
* Flexible, controllable, automatic matching of output format to input format;
|
||||
* Improved synchronization;
|
||||
* A new "vernier" resampling and interpolation method for low-power CPUs;
|
||||
* Lower powered devices operation, including the original Raspberry Pi B;
|
||||
* Reduced Docker image sizes;
|
||||
* A `systemd` user startup script.
|
||||
* Many bug fixes.
|
||||
|
||||
44.1k operation is retained.
|
||||
|
||||
**Warning:** This is a breaking update. That means you probably can not simply update an existing installation of Shairport Sync without a little bit of work. To update correctly you must:
|
||||
* Remove the Shairport Sync device from Apple Home -- you can add it back later;
|
||||
* Review the list of libraries needed for building Shairport Sync -- there are some extras;
|
||||
* Review configuration flags when building Shairport Sync -- some names have changed, e.g. `--with-systemd` becomes `--with-systemd-startup`;
|
||||
* Fully delete and reinstall Shairport Sync and NQPTP, along with all their service files;
|
||||
* Review and update settings:
|
||||
* Some of the names of the setting sections have changed, e.g. the `pa` section becomes the `pulseaudio` section;
|
||||
* The behaviour of Shairport Sync backends has changed, e.g. rates and channel counts can change dynamically.
|
||||
|
||||
Version 4.3.7-dev-24-gbc5001eb
|
||||
==
|
||||
|
||||
+3
-2
@@ -7,7 +7,7 @@
|
||||
*
|
||||
*
|
||||
* This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2019
|
||||
* Copyright (c) Mike Brady 2019--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -234,7 +234,8 @@ enum am_state activity_status() { return (state); }
|
||||
|
||||
void activity_monitor_start() {
|
||||
// debug(1,"activity_monitor_start");
|
||||
pthread_create(&activity_monitor_thread, NULL, activity_monitor_thread_code, NULL);
|
||||
named_pthread_create(&activity_monitor_thread, NULL, activity_monitor_thread_code, NULL,
|
||||
"activity_mon");
|
||||
activity_monitor_running = 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,13 +44,13 @@ static const int host_bigendian = 0;
|
||||
|
||||
#define _Swap32(v) \
|
||||
do { \
|
||||
v = (((v)&0x000000FF) << 0x18) | (((v)&0x0000FF00) << 0x08) | (((v)&0x00FF0000) >> 0x08) | \
|
||||
(((v)&0xFF000000) >> 0x18); \
|
||||
v = (((v) & 0x000000FF) << 0x18) | (((v) & 0x0000FF00) << 0x08) | \
|
||||
(((v) & 0x00FF0000) >> 0x08) | (((v) & 0xFF000000) >> 0x18); \
|
||||
} while (0)
|
||||
|
||||
#define _Swap16(v) \
|
||||
do { \
|
||||
v = (((v)&0x00FF) << 0x08) | (((v)&0xFF00) >> 0x08); \
|
||||
v = (((v) & 0x00FF) << 0x08) | (((v) & 0xFF00) >> 0x08); \
|
||||
} while (0)
|
||||
|
||||
struct {
|
||||
@@ -436,15 +436,15 @@ static void predictor_decompress_fir_adapt(int32_t *error_buffer, int32_t *buffe
|
||||
|
||||
/* read warm-up samples */
|
||||
if (predictor_coef_num > 0) {
|
||||
int i;
|
||||
for (i = 0; i < predictor_coef_num; i++) {
|
||||
int li;
|
||||
for (li = 0; li < predictor_coef_num; li++) {
|
||||
int32_t val;
|
||||
|
||||
val = buffer_out[i] + error_buffer[i + 1];
|
||||
val = buffer_out[li] + error_buffer[li + 1];
|
||||
|
||||
val = SIGN_EXTENDED32(val, readsamplesize);
|
||||
|
||||
buffer_out[i + 1] = val;
|
||||
buffer_out[li + 1] = val;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,11 +603,11 @@ static void deinterlace_24(int32_t *buffer_a, int32_t *buffer_b, int uncompresse
|
||||
right |= uncompressed_bytes_buffer_b[i] & mask;
|
||||
}
|
||||
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3] = (left)&0xFF;
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3] = (left) & 0xFF;
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3 + 1] = (left >> 8) & 0xFF;
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3 + 2] = (left >> 16) & 0xFF;
|
||||
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3 + 3] = (right)&0xFF;
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3 + 3] = (right) & 0xFF;
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3 + 4] = (right >> 8) & 0xFF;
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3 + 5] = (right >> 16) & 0xFF;
|
||||
}
|
||||
@@ -631,11 +631,11 @@ static void deinterlace_24(int32_t *buffer_a, int32_t *buffer_b, int uncompresse
|
||||
right |= uncompressed_bytes_buffer_b[i] & mask;
|
||||
}
|
||||
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3] = (left)&0xFF;
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3] = (left) & 0xFF;
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3 + 1] = (left >> 8) & 0xFF;
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3 + 2] = (left >> 16) & 0xFF;
|
||||
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3 + 3] = (right)&0xFF;
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3 + 3] = (right) & 0xFF;
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3 + 4] = (right >> 8) & 0xFF;
|
||||
((uint8_t *)buffer_out)[i * numchannels * 3 + 5] = (right >> 16) & 0xFF;
|
||||
}
|
||||
@@ -721,9 +721,9 @@ void alac_decode_frame(alac_file *alac, unsigned char *inbuffer, void *outbuffer
|
||||
}
|
||||
|
||||
if (uncompressed_bytes) {
|
||||
int i;
|
||||
for (i = 0; i < outputsamples; i++) {
|
||||
alac->uncompressed_bytes_buffer_a[i] = readbits(alac, uncompressed_bytes * 8);
|
||||
int li;
|
||||
for (li = 0; li < outputsamples; li++) {
|
||||
alac->uncompressed_bytes_buffer_a[li] = readbits(alac, uncompressed_bytes * 8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -798,7 +798,7 @@ void alac_decode_frame(alac_file *alac, unsigned char *inbuffer, void *outbuffer
|
||||
sample |= alac->uncompressed_bytes_buffer_a[i] & mask;
|
||||
}
|
||||
|
||||
((uint8_t *)outbuffer)[i * alac->numchannels * 3] = (sample)&0xFF;
|
||||
((uint8_t *)outbuffer)[i * alac->numchannels * 3] = (sample) & 0xFF;
|
||||
((uint8_t *)outbuffer)[i * alac->numchannels * 3 + 1] = (sample >> 8) & 0xFF;
|
||||
((uint8_t *)outbuffer)[i * alac->numchannels * 3 + 2] = (sample >> 16) & 0xFF;
|
||||
}
|
||||
@@ -896,10 +896,10 @@ void alac_decode_frame(alac_file *alac, unsigned char *inbuffer, void *outbuffer
|
||||
|
||||
/*********************/
|
||||
if (uncompressed_bytes) { /* see mono case */
|
||||
int i;
|
||||
for (i = 0; i < outputsamples; i++) {
|
||||
alac->uncompressed_bytes_buffer_a[i] = readbits(alac, uncompressed_bytes * 8);
|
||||
alac->uncompressed_bytes_buffer_b[i] = readbits(alac, uncompressed_bytes * 8);
|
||||
int li;
|
||||
for (li = 0; li < outputsamples; li++) {
|
||||
alac->uncompressed_bytes_buffer_a[li] = readbits(alac, uncompressed_bytes * 8);
|
||||
alac->uncompressed_bytes_buffer_b[li] = readbits(alac, uncompressed_bytes * 8);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2019
|
||||
* Copyright (c) Mike Brady 2019--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2019
|
||||
* Copyright (c) Mike Brady 2019--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Audio driver handler. This file is part of Shairport.
|
||||
* Copyright (c) James Laird 2013
|
||||
* Modifications (c) Mike Brady 2014 -- 2019
|
||||
* Modifications (c) Mike Brady 2014--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "common.h"
|
||||
#include "config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef CONFIG_JACK
|
||||
@@ -43,10 +44,10 @@ extern audio_output audio_ao;
|
||||
#ifdef CONFIG_SOUNDIO
|
||||
extern audio_output audio_soundio;
|
||||
#endif
|
||||
#ifdef CONFIG_PW
|
||||
#ifdef CONFIG_PIPEWIRE
|
||||
extern audio_output audio_pw;
|
||||
#endif
|
||||
#ifdef CONFIG_PA
|
||||
#ifdef CONFIG_PULSEAUDIO
|
||||
extern audio_output audio_pa;
|
||||
#endif
|
||||
#ifdef CONFIG_ALSA
|
||||
@@ -69,10 +70,10 @@ static audio_output *outputs[] = {
|
||||
#ifdef CONFIG_SNDIO
|
||||
&audio_sndio,
|
||||
#endif
|
||||
#ifdef CONFIG_PW
|
||||
#ifdef CONFIG_PIPEWIRE
|
||||
&audio_pw,
|
||||
#endif
|
||||
#ifdef CONFIG_PA
|
||||
#ifdef CONFIG_PULSEAUDIO
|
||||
&audio_pa,
|
||||
#endif
|
||||
#ifdef CONFIG_JACK
|
||||
@@ -127,7 +128,8 @@ void audio_ls_outputs(void) {
|
||||
}
|
||||
}
|
||||
|
||||
void parse_general_audio_options(void) {
|
||||
void parse_audio_options(const char *named_stanza, uint32_t default_format_set,
|
||||
uint32_t default_rate_set, uint32_t default_channel_set) {
|
||||
/* this must be called after the output device has been initialised, so that the default values
|
||||
* are set before any options are chosen */
|
||||
int value;
|
||||
@@ -137,18 +139,8 @@ void parse_general_audio_options(void) {
|
||||
|
||||
/* Get the desired buffer size setting (deprecated). */
|
||||
if (config_lookup_int(config.cfg, "general.audio_backend_buffer_desired_length", &value)) {
|
||||
if ((value < 0) || (value > 66150)) {
|
||||
inform("The setting general.audio_backend_buffer_desired_length is deprecated. "
|
||||
"Use alsa.audio_backend_buffer_desired_length_in_seconds instead.");
|
||||
die("Invalid audio_backend_buffer_desired_length value: \"%d\". It "
|
||||
"should be between 0 and "
|
||||
"66150, default is %d",
|
||||
value, (int)(config.audio_backend_buffer_desired_length * 44100));
|
||||
} else {
|
||||
inform("The setting general.audio_backend_buffer_desired_length is deprecated. "
|
||||
"Use general.audio_backend_buffer_desired_length_in_seconds instead.");
|
||||
config.audio_backend_buffer_desired_length = 1.0 * value / 44100;
|
||||
}
|
||||
inform("The setting general.audio_backend_buffer_desired_length is no longer supported. "
|
||||
"Please use alsa.audio_backend_buffer_desired_length_in_seconds instead.");
|
||||
}
|
||||
|
||||
/* Get the desired buffer size setting in seconds. */
|
||||
@@ -181,17 +173,9 @@ void parse_general_audio_options(void) {
|
||||
|
||||
/* Get the latency offset (deprecated). */
|
||||
if (config_lookup_int(config.cfg, "general.audio_backend_latency_offset", &value)) {
|
||||
if ((value < -66150) || (value > 66150)) {
|
||||
inform("The setting general.audio_backend_latency_offset is deprecated. "
|
||||
"Use general.audio_backend_latency_offset_in_seconds instead.");
|
||||
die("Invalid audio_backend_latency_offset value: \"%d\". It "
|
||||
"should be between -66150 and +66150, default is 0",
|
||||
value);
|
||||
} else {
|
||||
inform("The setting general.audio_backend_latency_offset is deprecated. "
|
||||
"Use general.audio_backend_latency_offset_in_seconds instead.");
|
||||
config.audio_backend_latency_offset = 1.0 * value / 44100;
|
||||
}
|
||||
|
||||
inform("The setting general.audio_backend_latency_offset is no longer supported. "
|
||||
"Please use general.audio_backend_latency_offset_in_seconds instead.");
|
||||
}
|
||||
|
||||
/* Get the latency offset in seconds. */
|
||||
@@ -236,4 +220,812 @@ void parse_general_audio_options(void) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (named_stanza != NULL) {
|
||||
config.format_set = get_format_settings(named_stanza, "output_format");
|
||||
config.rate_set = get_rate_settings(named_stanza, "output_rate");
|
||||
config.channel_set = get_channel_settings(named_stanza, "output_channels");
|
||||
}
|
||||
// use the supplied defaults if no settings were given
|
||||
if (config.format_set == 0)
|
||||
config.format_set = default_format_set;
|
||||
if (config.rate_set == 0)
|
||||
config.rate_set = default_rate_set;
|
||||
if (config.channel_set == 0)
|
||||
config.channel_set = default_channel_set;
|
||||
}
|
||||
|
||||
uint32_t get_format_settings(const char *stanza_name, const char *setting_name) {
|
||||
uint32_t format_set = 0;
|
||||
if (config.cfg != NULL) {
|
||||
char setting_path[256];
|
||||
snprintf(setting_path, sizeof(setting_path) - 1, "%s.%s", stanza_name, setting_name);
|
||||
// get any format settings -- can be "auto", a single format e.g. "S16" or a list of formats
|
||||
config_setting_t *format_setting = config_lookup(config.cfg, setting_path);
|
||||
if (format_setting != NULL) {
|
||||
const char **format_settings;
|
||||
int format_settings_count =
|
||||
config_get_string_settings_as_string_array(format_setting, &format_settings);
|
||||
if (format_settings_count > 0) {
|
||||
int i;
|
||||
for (i = 0; i < format_settings_count; i++) {
|
||||
debug(3, "format setting %u: \"%s\".", i, format_settings[i]);
|
||||
if (strcmp(format_settings[i], "auto") == 0) {
|
||||
if (format_settings_count != 1)
|
||||
warn("in the \"%s\" setting in the \"%s\" section of the configuration file, "
|
||||
"multiple formats, including \"auto\", "
|
||||
"are specified, but \"auto\" includes all "
|
||||
"formats anyway...",
|
||||
setting_name, stanza_name);
|
||||
format_set = SPS_FORMAT_SET; // all valid formats
|
||||
} else if (strcmp(format_settings[i], "S16") == 0) {
|
||||
format_set |= (1 << SPS_FORMAT_S16_LE) | (1 << SPS_FORMAT_S16_BE);
|
||||
} else if (strcmp(format_settings[i], "S24") == 0) {
|
||||
format_set |= (1 << SPS_FORMAT_S24_LE) | (1 << SPS_FORMAT_S24_BE) |
|
||||
(1 << SPS_FORMAT_S24_3LE) | (1 << SPS_FORMAT_S24_3BE);
|
||||
} else if (strcmp(format_settings[i], "S32") == 0) {
|
||||
format_set |= (1 << SPS_FORMAT_S32_LE) | (1 << SPS_FORMAT_S32_BE);
|
||||
} else {
|
||||
sps_format_t f;
|
||||
int valid = 0;
|
||||
for (f = SPS_FORMAT_LOWEST; f <= SPS_FORMAT_HIGHEST_NATIVE; f++) {
|
||||
if (strcmp(format_settings[i], sps_format_description_string(f)) == 0) {
|
||||
format_set |= (1 << f);
|
||||
valid = 1;
|
||||
}
|
||||
}
|
||||
if (valid == 0) {
|
||||
warn("in the \"%s\" setting in the \"%s\" section of the configuration file, an "
|
||||
"invalid format: \"%s\" has been detected.",
|
||||
setting_name, stanza_name, format_settings[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
free(format_settings);
|
||||
} else {
|
||||
debug(1,
|
||||
"in the \"%s\" setting in the \"%s\" section of the configuration file, an error has "
|
||||
"been detected at argument %d.",
|
||||
setting_name, stanza_name, -format_settings_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
sps_format_t f;
|
||||
uint32_t t_format_set = format_set;
|
||||
char buf[256];
|
||||
char *p = buf;
|
||||
for (f = SPS_FORMAT_UNKNOWN; f <= SPS_FORMAT_S32_BE; f++) {
|
||||
if ((t_format_set & (1 << f)) != 0) {
|
||||
snprintf(p, sizeof(buf) - (p - buf) - 1, "%s", sps_format_description_string(f));
|
||||
p = p + strlen(sps_format_description_string(f));
|
||||
t_format_set -= (1 << f);
|
||||
if (t_format_set != 0) {
|
||||
snprintf(p, sizeof(buf) - (p - buf) - 1, ", ");
|
||||
p = p + strlen(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (p != buf) {
|
||||
*p = '.';
|
||||
p++;
|
||||
}
|
||||
*p = '\0';
|
||||
if (strlen(buf) == 0)
|
||||
debug(3, "No \"%s\" output format settings.", stanza_name, buf);
|
||||
else
|
||||
debug(3, "The \"%s\" output format settings are: \"%s\".", stanza_name, buf);
|
||||
return format_set;
|
||||
}
|
||||
|
||||
uint32_t get_rate_settings(const char *stanza_name, const char *setting_name) {
|
||||
uint32_t rate_set = 0;
|
||||
if (config.cfg != NULL) {
|
||||
char setting_path[256];
|
||||
snprintf(setting_path, sizeof(setting_path) - 1, "%s.%s", stanza_name, setting_name);
|
||||
config_setting_t *rate_setting = config_lookup(config.cfg, setting_path);
|
||||
if (rate_setting != NULL) {
|
||||
if (config_setting_type(rate_setting) == CONFIG_TYPE_STRING) {
|
||||
// see if it is "auto"
|
||||
if (strcmp(config_setting_get_string(rate_setting), "auto") == 0) {
|
||||
#ifdef CONFIG_FFMPEG
|
||||
rate_set = SPS_RATE_SET; // all valid rates
|
||||
#else
|
||||
rate_set = SPS_RATE_NON_FFMPEG_SET;
|
||||
#endif
|
||||
} else {
|
||||
warn("in the \"%s\" setting in the \"%s\" section of the configuration file, an invalid "
|
||||
"setting: \"%s\" has been detected.",
|
||||
setting_name, stanza_name, config_setting_get_string(rate_setting));
|
||||
}
|
||||
} else {
|
||||
int *rates;
|
||||
int rate_settings_count = config_get_int_settings_as_int_array(rate_setting, &rates);
|
||||
if (rate_settings_count > 0) {
|
||||
debug(3, "%d rate settings found.", rate_settings_count);
|
||||
int i;
|
||||
for (i = 0; i < rate_settings_count; i++) {
|
||||
debug(3, "rate setting %d: %d.", i, rates[i]);
|
||||
sps_rate_t r;
|
||||
int valid = 0;
|
||||
for (r = SPS_RATE_5512; r <= SPS_RATE_384000; r++) {
|
||||
if ((unsigned int)rates[i] == sps_rate_actual_rate(r)) {
|
||||
valid = 1;
|
||||
|
||||
#ifdef CONFIG_FFMPEG
|
||||
rate_set |= (1 << r);
|
||||
#else
|
||||
if (((1 << r) & SPS_RATE_NON_FFMPEG_SET) != 0) {
|
||||
rate_set |= (1 << r);
|
||||
} else {
|
||||
warn("in the \"%s\" setting in the \"%s\" section of the configuration file, "
|
||||
"the rate selected -- %d -- can not be used because Shairport Sync has been "
|
||||
"built without FFmpeg support.",
|
||||
setting_name, stanza_name, rates[i]);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (valid == 0) {
|
||||
warn("in the \"%s\" setting in the \"%s\" section of the configuration file, an "
|
||||
"invalid rate: %d has been detected.",
|
||||
setting_name, stanza_name, rates[i]);
|
||||
}
|
||||
}
|
||||
free(rates);
|
||||
} else {
|
||||
debug(1,
|
||||
"in the \"%s\" setting in the \"%s\" section of the configuration file, an error "
|
||||
"has been detected at argument %d.",
|
||||
setting_name, stanza_name, -rate_settings_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sps_rate_t r;
|
||||
char buf[256];
|
||||
char *p = buf;
|
||||
uint32_t t_rate_set = rate_set;
|
||||
char numbuf[32];
|
||||
for (r = SPS_RATE_UNKNOWN; r <= SPS_RATE_384000; r++) {
|
||||
if ((t_rate_set & (1 << r)) != 0) {
|
||||
snprintf(numbuf, sizeof(numbuf) - 1, "%u", sps_rate_actual_rate(r));
|
||||
snprintf(p, sizeof(buf) - (p - buf) - 1, "%s", numbuf);
|
||||
p = p + strlen(numbuf);
|
||||
t_rate_set -= (1 << r);
|
||||
if (t_rate_set != 0) {
|
||||
snprintf(p, sizeof(buf) - (p - buf) - 1, ", ");
|
||||
p = p + strlen(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (p != buf) {
|
||||
*p = '.';
|
||||
p++;
|
||||
}
|
||||
*p = '\0';
|
||||
if (strlen(buf) == 0)
|
||||
debug(3, "No \"%s\" output rate settings.", stanza_name, buf);
|
||||
else
|
||||
debug(3, "The \"%s\" output rate settings are: \"%s\".", stanza_name, buf);
|
||||
return rate_set;
|
||||
}
|
||||
|
||||
uint32_t get_channel_settings(const char *stanza_name, const char *setting_name) {
|
||||
uint32_t channel_set = 0;
|
||||
if (config.cfg != NULL) {
|
||||
char setting_path[256];
|
||||
snprintf(setting_path, sizeof(setting_path) - 1, "%s.%s", stanza_name, setting_name);
|
||||
// now get the channels count set
|
||||
config_setting_t *channels_setting = config_lookup(config.cfg, setting_path);
|
||||
if (channels_setting != NULL) {
|
||||
if (config_setting_type(channels_setting) == CONFIG_TYPE_STRING) {
|
||||
// see if it is "auto"
|
||||
if (strcmp(config_setting_get_string(channels_setting), "auto") == 0) {
|
||||
#ifdef CONFIG_FFMPEG
|
||||
channel_set = SPS_CHANNEL_SET; // all valid channels
|
||||
#else
|
||||
channel_set = SPS_CHANNNEL_NON_FFMPEG_SET; // just two channels
|
||||
#endif
|
||||
} else {
|
||||
warn("in the \"%s\" setting in the \"%s\" section of the configuration file, an invalid "
|
||||
"setting: \"%s\" has been detected.",
|
||||
setting_name, stanza_name, config_setting_get_string(channels_setting));
|
||||
}
|
||||
} else {
|
||||
int *channel_counts;
|
||||
int channel_counts_count =
|
||||
config_get_int_settings_as_int_array(channels_setting, &channel_counts);
|
||||
if (channel_counts_count > 0) {
|
||||
debug(3, "%d channel count settings found.", channel_counts_count);
|
||||
int i;
|
||||
for (i = 0; i < channel_counts_count; i++) {
|
||||
debug(3, "channel count setting %d: %d.", i, channel_counts[i]);
|
||||
|
||||
if ((channel_counts[i] >= 1) && (channel_counts[i] <= 8)) {
|
||||
#ifdef CONFIG_FFMPEG
|
||||
channel_set |= (1 << channel_counts[i]);
|
||||
#else
|
||||
if (((1 << channel_counts[i]) & SPS_CHANNNEL_NON_FFMPEG_SET) != 0) {
|
||||
channel_set |= (1 << channel_counts[i]);
|
||||
} else {
|
||||
warn("in the \"%s\" setting in the \"%s\" section of the configuration file, "
|
||||
"the channel count selected -- %d -- can not be used because Shairport Sync "
|
||||
"has been built without FFmpeg support.",
|
||||
setting_name, stanza_name, channel_counts[i]);
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
warn("in the \"%s\" setting in the \"%s\" section of the configuration file, an "
|
||||
"invalid channel count: %d has been detected.",
|
||||
setting_name, stanza_name, channel_counts[i]);
|
||||
}
|
||||
}
|
||||
free(channel_counts);
|
||||
} else {
|
||||
debug(1,
|
||||
"in the \"%s\" setting in the \"%s\" section of the configuration file, an error "
|
||||
"has been detected at argument %d.",
|
||||
setting_name, stanza_name, -channel_counts_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
char buf[256];
|
||||
char *p = buf;
|
||||
char numbuf[32];
|
||||
unsigned int c;
|
||||
uint32_t t_channel_set = channel_set;
|
||||
for (c = 0; c <= 8; c++) {
|
||||
if ((t_channel_set & (1 << c)) != 0) {
|
||||
snprintf(numbuf, sizeof(numbuf) - 1, "%u", c);
|
||||
snprintf(p, sizeof(buf) - (p - buf) - 1, "%s", numbuf);
|
||||
p = p + strlen(numbuf);
|
||||
t_channel_set -= (1 << c);
|
||||
if (t_channel_set != 0) {
|
||||
snprintf(p, sizeof(buf) - (p - buf) - 1, ", ");
|
||||
p = p + strlen(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (p != buf) {
|
||||
*p = '.';
|
||||
p++;
|
||||
}
|
||||
*p = '\0';
|
||||
if (strlen(buf) == 0)
|
||||
debug(3, "No \"%s\" output channel settings.", stanza_name, buf);
|
||||
else
|
||||
debug(3, "The \"%s\" output channel settings are: \"%s\".", stanza_name, buf);
|
||||
return channel_set;
|
||||
}
|
||||
|
||||
sps_format_t check_configuration_with_formats(
|
||||
unsigned int channels, unsigned int rate, sps_format_t format,
|
||||
int (*check_configuration)(unsigned int channels, unsigned int rate, unsigned int format)) {
|
||||
sps_format_t response = SPS_FORMAT_UNKNOWN; // this is encoded as zero
|
||||
|
||||
// first, check that the channels are permissible...
|
||||
if ((config.channel_set & (1 << channels)) != 0) {
|
||||
// now check the rate...
|
||||
sps_rate_t r = SPS_RATE_LOWEST;
|
||||
int rate_is_permissible = 0;
|
||||
while ((rate_is_permissible == 0) && (r <= SPS_RATE_HIGHEST)) {
|
||||
if (((config.rate_set & (1 << r)) != 0) && (rate == sps_rate_actual_rate(r)))
|
||||
rate_is_permissible = 1;
|
||||
else
|
||||
r++;
|
||||
}
|
||||
if (rate_is_permissible != 0) {
|
||||
// check the actual requested format first with the initial_search array
|
||||
// and if it fails, then check in the subsequent_search array.
|
||||
// the choice of subsequent search array is determined by the machine's endianness
|
||||
|
||||
// clang-format off
|
||||
sps_format_t ordered_initial_search_U8[] = {SPS_FORMAT_U8, SPS_FORMAT_S8};
|
||||
sps_format_t ordered_initial_search_S8[] = {SPS_FORMAT_S8, SPS_FORMAT_U8};
|
||||
sps_format_t ordered_subsequent_search_8_LE[] = {SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE, SPS_FORMAT_S24_LE, SPS_FORMAT_S24_BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE};
|
||||
sps_format_t ordered_subsequent_search_8_BE[] = {SPS_FORMAT_S16_BE, SPS_FORMAT_S16_LE, SPS_FORMAT_S24_BE, SPS_FORMAT_S24_LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S32_LE};
|
||||
|
||||
sps_format_t ordered_initial_search_S16_LE[] = {SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE};
|
||||
sps_format_t ordered_initial_search_S16_BE[] = {SPS_FORMAT_S16_BE, SPS_FORMAT_S16_LE};
|
||||
sps_format_t ordered_subsequent_search_S16_LE[] = {SPS_FORMAT_S24_LE, SPS_FORMAT_S24_BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S8, SPS_FORMAT_U8};
|
||||
sps_format_t ordered_subsequent_search_S16_BE[] = {SPS_FORMAT_S24_BE, SPS_FORMAT_S24_LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S8, SPS_FORMAT_U8};
|
||||
|
||||
sps_format_t ordered_initial_search_S24_LE[] = {SPS_FORMAT_S24_LE, SPS_FORMAT_S24_BE};
|
||||
sps_format_t ordered_initial_search_S24_BE[] = {SPS_FORMAT_S24_BE, SPS_FORMAT_S24_LE};
|
||||
sps_format_t ordered_subsequent_search_S24_LE[] = {SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE, SPS_FORMAT_S8, SPS_FORMAT_U8};
|
||||
sps_format_t ordered_subsequent_search_S24_BE[] = {SPS_FORMAT_S24_3BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S16_BE, SPS_FORMAT_S16_LE, SPS_FORMAT_S8, SPS_FORMAT_U8};
|
||||
|
||||
sps_format_t ordered_initial_search_S24_3le[] = {SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE};
|
||||
sps_format_t ordered_initial_search_S24_3be[] = {SPS_FORMAT_S24_3BE, SPS_FORMAT_S24_3LE};
|
||||
sps_format_t ordered_subsequent_search_S24_3le[] = {SPS_FORMAT_S24_LE, SPS_FORMAT_S24_BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE, SPS_FORMAT_S8, SPS_FORMAT_U8};
|
||||
sps_format_t ordered_subsequent_search_S24_3be[] = {SPS_FORMAT_S24_BE, SPS_FORMAT_S24_LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S16_BE, SPS_FORMAT_S16_LE, SPS_FORMAT_S8, SPS_FORMAT_U8};
|
||||
|
||||
sps_format_t ordered_initial_search_S32_LE[] = {SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE};
|
||||
sps_format_t ordered_initial_search_S32_BE[] = {SPS_FORMAT_S32_BE, SPS_FORMAT_S32_LE};
|
||||
sps_format_t ordered_subsequent_search_S32_LE[] = {SPS_FORMAT_S24_LE, SPS_FORMAT_S24_BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE, SPS_FORMAT_S8, SPS_FORMAT_U8};
|
||||
sps_format_t ordered_subsequent_search_S32_BE[] = {SPS_FORMAT_S24_BE, SPS_FORMAT_S24_LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S16_BE, SPS_FORMAT_S16_LE, SPS_FORMAT_S8, SPS_FORMAT_U8};
|
||||
// clang-format on
|
||||
|
||||
// set up the initial_search pointer
|
||||
sps_format_t *initial_formats_to_check = NULL;
|
||||
unsigned int number_of_initial_formats_to_check = 0;
|
||||
|
||||
// set up the subsequent_search pointer
|
||||
sps_format_t *subsequent_formats_to_check = NULL;
|
||||
unsigned int number_of_subsequent_formats_to_check = 0;
|
||||
|
||||
// Set up the initial and subsequent search pointers.
|
||||
|
||||
// Searching is done with an initial search and, if necessary, a subsequent search.
|
||||
// Searching stops when a suitable format is confirmed as being acceptable.
|
||||
|
||||
// The initial search checks the requested format first, followed by its equivalents of the
|
||||
// same bit size. The subsequent search puts the formats to check in order of suitability but
|
||||
// with the native-endian format first. SPS_FORMAT_S16, SPS_FORMAT_S24 and SPS_FORMAT_S24 are
|
||||
// treated the same as their equivalent with native endianness. AUTO is treated as
|
||||
// SPS_FORMAT_S24 with native endianness.
|
||||
|
||||
switch (format) {
|
||||
case SPS_FORMAT_U8:
|
||||
case SPS_FORMAT_S8:
|
||||
if (config.endianness == SS_LITTLE_ENDIAN) {
|
||||
subsequent_formats_to_check = ordered_subsequent_search_8_LE;
|
||||
number_of_subsequent_formats_to_check =
|
||||
sizeof(ordered_subsequent_search_8_LE) / sizeof(sps_format_t);
|
||||
} else {
|
||||
subsequent_formats_to_check = ordered_subsequent_search_8_BE;
|
||||
number_of_subsequent_formats_to_check =
|
||||
sizeof(ordered_subsequent_search_8_BE) / sizeof(sps_format_t);
|
||||
}
|
||||
if (format == SPS_FORMAT_S8) {
|
||||
initial_formats_to_check = ordered_initial_search_S8;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S8) / sizeof(sps_format_t);
|
||||
} else {
|
||||
initial_formats_to_check = ordered_initial_search_U8;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_U8) / sizeof(sps_format_t);
|
||||
}
|
||||
break;
|
||||
case SPS_FORMAT_S16:
|
||||
case SPS_FORMAT_S16_LE:
|
||||
case SPS_FORMAT_S16_BE:
|
||||
if (config.endianness == SS_LITTLE_ENDIAN) {
|
||||
subsequent_formats_to_check = ordered_subsequent_search_S16_LE;
|
||||
number_of_subsequent_formats_to_check =
|
||||
sizeof(ordered_subsequent_search_S16_LE) / sizeof(sps_format_t);
|
||||
if (format == SPS_FORMAT_S16) {
|
||||
initial_formats_to_check = ordered_initial_search_S16_LE;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S16_LE) / sizeof(sps_format_t);
|
||||
}
|
||||
} else {
|
||||
subsequent_formats_to_check = ordered_subsequent_search_S16_BE;
|
||||
number_of_subsequent_formats_to_check =
|
||||
sizeof(ordered_subsequent_search_S16_BE) / sizeof(sps_format_t);
|
||||
if (format == SPS_FORMAT_S16) {
|
||||
initial_formats_to_check = ordered_initial_search_S16_BE;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S16_BE) / sizeof(sps_format_t);
|
||||
}
|
||||
}
|
||||
if (format == SPS_FORMAT_S16_LE) {
|
||||
initial_formats_to_check = ordered_initial_search_S16_LE;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S16_LE) / sizeof(sps_format_t);
|
||||
} else if (format == SPS_FORMAT_S16_BE) {
|
||||
initial_formats_to_check = ordered_initial_search_S16_BE;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S16_BE) / sizeof(sps_format_t);
|
||||
}
|
||||
break;
|
||||
case SPS_FORMAT_S24:
|
||||
case SPS_FORMAT_S24_LE:
|
||||
case SPS_FORMAT_S24_BE:
|
||||
if (config.endianness == SS_LITTLE_ENDIAN) {
|
||||
subsequent_formats_to_check = ordered_subsequent_search_S24_LE;
|
||||
number_of_subsequent_formats_to_check =
|
||||
sizeof(ordered_subsequent_search_S24_LE) / sizeof(sps_format_t);
|
||||
if (format == SPS_FORMAT_S24) {
|
||||
initial_formats_to_check = ordered_initial_search_S24_LE;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S24_LE) / sizeof(sps_format_t);
|
||||
}
|
||||
} else {
|
||||
subsequent_formats_to_check = ordered_subsequent_search_S24_BE;
|
||||
number_of_subsequent_formats_to_check =
|
||||
sizeof(ordered_subsequent_search_S24_BE) / sizeof(sps_format_t);
|
||||
if (format == SPS_FORMAT_S24) {
|
||||
initial_formats_to_check = ordered_initial_search_S24_BE;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S24_BE) / sizeof(sps_format_t);
|
||||
}
|
||||
}
|
||||
if (format == SPS_FORMAT_S24_LE) {
|
||||
initial_formats_to_check = ordered_initial_search_S24_LE;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S24_LE) / sizeof(sps_format_t);
|
||||
} else if (format == SPS_FORMAT_S24_BE) {
|
||||
initial_formats_to_check = ordered_initial_search_S24_BE;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S24_BE) / sizeof(sps_format_t);
|
||||
}
|
||||
break;
|
||||
case SPS_FORMAT_S32:
|
||||
case SPS_FORMAT_S32_LE:
|
||||
case SPS_FORMAT_S32_BE:
|
||||
// case SPS_FORMAT_AUTO:
|
||||
if (config.endianness == SS_LITTLE_ENDIAN) {
|
||||
subsequent_formats_to_check = ordered_subsequent_search_S32_LE;
|
||||
number_of_subsequent_formats_to_check =
|
||||
sizeof(ordered_subsequent_search_S32_LE) / sizeof(sps_format_t);
|
||||
if ((format == SPS_FORMAT_S32) || (format == SPS_FORMAT_AUTO)) {
|
||||
initial_formats_to_check = ordered_initial_search_S32_LE;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S32_LE) / sizeof(sps_format_t);
|
||||
}
|
||||
} else {
|
||||
subsequent_formats_to_check = ordered_subsequent_search_S32_BE;
|
||||
number_of_subsequent_formats_to_check =
|
||||
sizeof(ordered_subsequent_search_S32_BE) / sizeof(sps_format_t);
|
||||
if ((format == SPS_FORMAT_S32) || (format == SPS_FORMAT_AUTO)) {
|
||||
initial_formats_to_check = ordered_initial_search_S32_BE;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S32_BE) / sizeof(sps_format_t);
|
||||
}
|
||||
}
|
||||
if (format == SPS_FORMAT_S32_LE) {
|
||||
initial_formats_to_check = ordered_initial_search_S32_LE;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S32_LE) / sizeof(sps_format_t);
|
||||
} else if (format == SPS_FORMAT_S32_BE) {
|
||||
initial_formats_to_check = ordered_initial_search_S32_BE;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S32_BE) / sizeof(sps_format_t);
|
||||
}
|
||||
break;
|
||||
case SPS_FORMAT_S24_3LE:
|
||||
case SPS_FORMAT_S24_3BE:
|
||||
if (config.endianness == SS_LITTLE_ENDIAN) {
|
||||
subsequent_formats_to_check = ordered_subsequent_search_S24_3le;
|
||||
number_of_subsequent_formats_to_check =
|
||||
sizeof(ordered_subsequent_search_S24_3le) / sizeof(sps_format_t);
|
||||
} else {
|
||||
subsequent_formats_to_check = ordered_subsequent_search_S24_3be;
|
||||
number_of_subsequent_formats_to_check =
|
||||
sizeof(ordered_subsequent_search_S24_3be) / sizeof(sps_format_t);
|
||||
}
|
||||
if (format == SPS_FORMAT_S24_3LE) {
|
||||
initial_formats_to_check = ordered_initial_search_S24_3le;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S24_3le) / sizeof(sps_format_t);
|
||||
} else {
|
||||
initial_formats_to_check = ordered_initial_search_S24_3be;
|
||||
number_of_initial_formats_to_check =
|
||||
sizeof(ordered_initial_search_S24_3be) / sizeof(sps_format_t);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
debug(1, "unknown format %u to check.", format);
|
||||
break;
|
||||
}
|
||||
|
||||
if ((initial_formats_to_check == NULL) || (subsequent_formats_to_check == NULL)) {
|
||||
debug(1, "error initialising for format %s.", sps_format_description_string(format));
|
||||
}
|
||||
|
||||
// here, we know the channel and rate are permissible according to the settings,
|
||||
// but we don't yet know about each of the formats
|
||||
// do initial search
|
||||
unsigned int i = 0;
|
||||
while ((i < number_of_initial_formats_to_check) && (response == SPS_FORMAT_UNKNOWN)) {
|
||||
// only call check_configuration() if the format is permissible
|
||||
if ((((1 << initial_formats_to_check[i]) & config.format_set) != 0) &&
|
||||
((check_configuration == NULL) ||
|
||||
(check_configuration(channels, rate, initial_formats_to_check[i]) == 0)))
|
||||
response = initial_formats_to_check[i];
|
||||
else
|
||||
i++;
|
||||
}
|
||||
// if no joy, do subsequent search
|
||||
if (response == 0) {
|
||||
i = 0;
|
||||
while ((i < number_of_subsequent_formats_to_check) && (response == SPS_FORMAT_UNKNOWN)) {
|
||||
// only call check_configuration() if the format is permissible
|
||||
if ((((1 << subsequent_formats_to_check[i]) & config.format_set) != 0) &&
|
||||
((check_configuration == NULL) ||
|
||||
(check_configuration(channels, rate, subsequent_formats_to_check[i]) == 0)))
|
||||
response = subsequent_formats_to_check[i];
|
||||
else
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
sps_format_t all_formats_to_check_le[] = {SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S24_LE,
|
||||
SPS_FORMAT_S24_BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE,
|
||||
SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE};
|
||||
// look at little-endian formats first
|
||||
sps_format_t all_formats_to_check_le[] = {SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S24_LE,
|
||||
SPS_FORMAT_S24_BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE,
|
||||
SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE};
|
||||
// look at big-endian formats first
|
||||
sps_format_t all_formats_to_check_be[] = {SPS_FORMAT_S32_BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S24_BE,
|
||||
SPS_FORMAT_S24_LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S24_3LE,
|
||||
SPS_FORMAT_S16_BE, SPS_FORMAT_S16_LE};
|
||||
// look at little-endian formats first
|
||||
sps_format_t s32_formats_to_check_le[] = {SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE};
|
||||
// look at big-endian formats first
|
||||
sps_format_t s32_formats_to_check_be[] = {SPS_FORMAT_S32_BE, SPS_FORMAT_S32_LE};
|
||||
// look at little-endian formats first
|
||||
sps_format_t s24_formats_to_check_le[] = {
|
||||
SPS_FORMAT_S24_LE,
|
||||
SPS_FORMAT_S24_BE,
|
||||
SPS_FORMAT_S24_3LE,
|
||||
SPS_FORMAT_S24_3BE,
|
||||
};
|
||||
// look at big-endian formats first
|
||||
sps_format_t s24_formats_to_check_be[] = {
|
||||
SPS_FORMAT_S24_BE,
|
||||
SPS_FORMAT_S24_LE,
|
||||
SPS_FORMAT_S24_3BE,
|
||||
SPS_FORMAT_S24_3LE,
|
||||
};
|
||||
// look at little-endian formats first
|
||||
sps_format_t s16_formats_to_check_le[] = {SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE};
|
||||
// look at big-endian formats first
|
||||
sps_format_t s16_formats_to_check_be[] = {SPS_FORMAT_S16_BE, SPS_FORMAT_S16_LE};
|
||||
|
||||
sps_format_t single_format_to_check[1];
|
||||
|
||||
sps_format_t check_configuration_with_formats(
|
||||
unsigned int channels, unsigned int rate, sps_format_t format,
|
||||
int (*check_configuration)(unsigned int channels, unsigned int rate, unsigned int format)) {
|
||||
sps_format_t response = SPS_FORMAT_UNKNOWN; // this is encoded as zero
|
||||
|
||||
// assume there is one single format to check
|
||||
unsigned int number_of_formats_to_check = 1;
|
||||
sps_format_t single_format_to_check[] = {format};
|
||||
sps_format_t *formats_to_check = single_format_to_check;
|
||||
|
||||
// now, check to see if there are multiple formats to check, e.g. if the format is given as AUTO
|
||||
// or if it doesn't specify _LE or _BE. Try to favour the host endianness:
|
||||
|
||||
if (config.endianness == SS_LITTLE_ENDIAN) {
|
||||
switch (format) {
|
||||
case SPS_FORMAT_AUTO:
|
||||
case SPS_FORMAT_S32:
|
||||
case SPS_FORMAT_32_LE:
|
||||
case SPS_FORMAT_32_BE:
|
||||
formats_to_check = all_formats_to_check_le;
|
||||
number_of_formats_to_check = sizeof(all_formats_to_check_le) / sizeof(sps_format_t);
|
||||
break;
|
||||
case SPS_FORMAT_S24:
|
||||
case SPS_FORMAT_S24_LE:
|
||||
case SPS_FORMAT_S24_BE:
|
||||
formats_to_check = s32_formats_to_check_le;
|
||||
number_of_formats_to_check = sizeof(s32_formats_to_check_le) / sizeof(sps_format_t);
|
||||
break;
|
||||
case SPS_FORMAT_S24:
|
||||
formats_to_check = s24_formats_to_check_le;
|
||||
number_of_formats_to_check = sizeof(s24_formats_to_check_le) / sizeof(sps_format_t);
|
||||
break;
|
||||
case SPS_FORMAT_S16:
|
||||
formats_to_check = s16_formats_to_check_le;
|
||||
number_of_formats_to_check = sizeof(s16_formats_to_check_le) / sizeof(sps_format_t);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
} else { // big-endian
|
||||
switch (format) {
|
||||
case SPS_FORMAT_AUTO:
|
||||
formats_to_check = all_formats_to_check_be;
|
||||
number_of_formats_to_check = sizeof(all_formats_to_check_be) / sizeof(sps_format_t);
|
||||
break;
|
||||
case SPS_FORMAT_S32:
|
||||
formats_to_check = s32_formats_to_check_be;
|
||||
number_of_formats_to_check = sizeof(s32_formats_to_check_be) / sizeof(sps_format_t);
|
||||
break;
|
||||
case SPS_FORMAT_S24:
|
||||
formats_to_check = s24_formats_to_check_be;
|
||||
number_of_formats_to_check = sizeof(s24_formats_to_check_be) / sizeof(sps_format_t);
|
||||
break;
|
||||
case SPS_FORMAT_S16:
|
||||
formats_to_check = s16_formats_to_check_be;
|
||||
number_of_formats_to_check = sizeof(s16_formats_to_check_be) / sizeof(sps_format_t);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
unsigned int i = 0;
|
||||
while ((i < number_of_formats_to_check) && (response == SPS_FORMAT_UNKNOWN)) {
|
||||
if (check_configuration(channels, rate, formats_to_check[i]) == 0)
|
||||
response = formats_to_check[i];
|
||||
else
|
||||
i++;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
// This looks for a suitable configuration in the right order and checks possible
|
||||
// configurations using the check_configuration method passed in.
|
||||
|
||||
// If it's the same as the most recent request, it sends back the cached reply.
|
||||
// The cache is keyed to the address of the checker method in order to distinguish
|
||||
// between requests from different backends. It's moot, as only one back end can be
|
||||
// active, but anyway... ya never just know.
|
||||
|
||||
// but it does mean that it only remembers the last response, whichever backend it was for
|
||||
|
||||
typedef struct {
|
||||
int32_t encoded_request; // this will have the channels and the rate requested but not the format
|
||||
int32_t encoded_response; // this will have the channels, rate and format suggested
|
||||
int (*check_configuration)(unsigned int channels, unsigned int rate, unsigned int format);
|
||||
} configuration_request_and_response_t;
|
||||
|
||||
static configuration_request_and_response_t search_for_suitable_configuration_last_response = {
|
||||
0, 0, NULL};
|
||||
|
||||
// First it looks for an exact match of channels, rate or with a rate that is an even multiple.
|
||||
// If that fails it will look for an exact match of channels with the next highest rate
|
||||
// If that fails it will look for an exact match of channels with the next lowest rate
|
||||
// If that fails it increases the number channels, if available, and checks all over again
|
||||
// Finally, if that fails it reduces the number channels, if possible, and checks all over again.
|
||||
|
||||
// Regarding the requested format: ask for the exact format only if no audio processing will be
|
||||
// done in the software or (as far as can be known) in the hardware mixer.
|
||||
// So, if ignore_volume_control is set and volume_max_db is
|
||||
// not set, and the number of output channels is greater than or equal to those requested,
|
||||
// and mixing to mono is not requested and the rate being checked is the rate requested,
|
||||
// then look for the exact rate format.
|
||||
// Otherwise ask for the deepest format, which could be useful
|
||||
// for attenuation, mixing, transcoding or other audio processing.
|
||||
// (Haven't included checking for convolution here.)
|
||||
|
||||
int32_t search_for_suitable_configuration(unsigned int channels, unsigned int rate,
|
||||
unsigned int format,
|
||||
int (*check_configuration)(unsigned int in_channels,
|
||||
unsigned int in_rate,
|
||||
unsigned int in_format)) {
|
||||
int32_t reply = 0; // means nothing was found
|
||||
if ((channels == 0) || (rate == 0))
|
||||
debug(1, "invalid channel %u or rate %u parameters to search_for_suitable_configuration",
|
||||
channels, rate);
|
||||
int32_t encoded_request = CHANNELS_TO_ENCODED_FORMAT(channels) | RATE_TO_ENCODED_FORMAT(rate) |
|
||||
FORMAT_TO_ENCODED_FORMAT(format);
|
||||
|
||||
if ((search_for_suitable_configuration_last_response.check_configuration ==
|
||||
check_configuration) && // same method (implies same backend)
|
||||
(search_for_suitable_configuration_last_response.encoded_request ==
|
||||
encoded_request)) { // same request
|
||||
reply = search_for_suitable_configuration_last_response
|
||||
.encoded_response; // provide cached response...
|
||||
}
|
||||
if (reply == 0) { // no luck with the last response generated, if any...
|
||||
// check for native number of channels or more... and then, if not successful, with fewer
|
||||
// channels
|
||||
#ifdef CONFIG_FFMPEG
|
||||
unsigned int channel_count_check_sequence[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1};
|
||||
unsigned int rates[] = {44100, 48000, 88200, 96000, 176400, 192000, 352800, 384000,
|
||||
64000, 32000, 22050, 16000, 11025, 8000, 5512};
|
||||
#else
|
||||
unsigned int channel_count_check_sequence[] = {0, 1, 2};
|
||||
// unsigned int rates[] = {44100};
|
||||
#endif
|
||||
|
||||
unsigned int channel_count_index = channels;
|
||||
// start looking with the required number of channels
|
||||
unsigned int local_channels;
|
||||
unsigned int local_rate;
|
||||
sps_format_t local_format = SPS_FORMAT_UNKNOWN;
|
||||
uint32_t channel_check_bit_field = 0; // set (1 << channel_count) when check done.
|
||||
// Use it to stop checking the same channel count on the way down as well as on the way up...
|
||||
while ((local_format == SPS_FORMAT_UNKNOWN) &&
|
||||
(channel_count_index < (sizeof(channel_count_check_sequence) / sizeof(unsigned int)))) {
|
||||
local_channels = channel_count_check_sequence[channel_count_index];
|
||||
// if we haven't already checked....
|
||||
if ((channel_check_bit_field & (1 << local_channels)) == 0) {
|
||||
channel_check_bit_field |= (1 << local_channels); // flag that we have checked it
|
||||
int rate_multiplier = 2;
|
||||
local_rate = rate; // begin with the requested rate
|
||||
debug(3, "check for an exact multiple of %u with %u channels.", rate, local_channels);
|
||||
while ((local_rate <= 384000) && (local_format == SPS_FORMAT_UNKNOWN)) {
|
||||
if (
|
||||
// clang-format off
|
||||
// check for the exact format only under these conditions, otherwise look for the best
|
||||
(config.ignore_volume_control != 0) &&
|
||||
(config.volume_max_db_set == 0) &&
|
||||
(local_rate == rate) &&
|
||||
(local_channels >= channels) &&
|
||||
(config.playback_mode != ST_mono)
|
||||
// clang-format on
|
||||
) {
|
||||
// debug(1, "check exact");
|
||||
local_format = check_configuration_with_formats(
|
||||
local_channels, local_rate, (sps_format_t)format, check_configuration);
|
||||
} else {
|
||||
// debug(1, "check best");
|
||||
local_format = check_configuration_with_formats(local_channels, local_rate,
|
||||
SPS_FORMAT_S32, check_configuration);
|
||||
}
|
||||
|
||||
if (local_format == SPS_FORMAT_UNKNOWN) {
|
||||
local_rate = rate * rate_multiplier;
|
||||
rate_multiplier = rate_multiplier * 2;
|
||||
}
|
||||
}
|
||||
#ifdef CONFIG_FFMPEG
|
||||
if (local_format == SPS_FORMAT_UNKNOWN) {
|
||||
debug(3, "check for an next highest rate above %u with %u channels.", rate,
|
||||
local_channels);
|
||||
unsigned int rate_pointer = 0;
|
||||
while ((rate_pointer < sizeof(rates) / sizeof(unsigned int)) &&
|
||||
(local_format == SPS_FORMAT_UNKNOWN)) {
|
||||
local_rate = rates[rate_pointer];
|
||||
if (local_rate > rate) {
|
||||
local_format = check_configuration_with_formats(
|
||||
local_channels, local_rate, (sps_format_t)format, check_configuration);
|
||||
}
|
||||
if (local_format == SPS_FORMAT_UNKNOWN) {
|
||||
rate_pointer++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (local_format == SPS_FORMAT_UNKNOWN) {
|
||||
int rate_pointer = (int)(sizeof(rates) / sizeof(unsigned int) - 1);
|
||||
debug(3, "check for an next lowest rate below %u with %u channels.", rate,
|
||||
local_channels);
|
||||
while ((rate_pointer >= 0) && (local_format == SPS_FORMAT_UNKNOWN)) {
|
||||
local_rate = rates[rate_pointer];
|
||||
if (local_rate < rate) {
|
||||
local_format = check_configuration_with_formats(
|
||||
local_channels, local_rate, (sps_format_t)format, check_configuration);
|
||||
}
|
||||
if (local_format == SPS_FORMAT_UNKNOWN) {
|
||||
rate_pointer--;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
// if unsuccessful, try with, firstly, more channels, and then, later, with fewer channels
|
||||
if (local_format == SPS_FORMAT_UNKNOWN)
|
||||
channel_count_index++;
|
||||
}
|
||||
if (local_format != SPS_FORMAT_UNKNOWN) {
|
||||
reply = CHANNELS_TO_ENCODED_FORMAT(local_channels) | RATE_TO_ENCODED_FORMAT(local_rate) |
|
||||
FORMAT_TO_ENCODED_FORMAT(local_format);
|
||||
|
||||
// cache the response
|
||||
search_for_suitable_configuration_last_response.check_configuration = check_configuration;
|
||||
search_for_suitable_configuration_last_response.encoded_request = encoded_request;
|
||||
search_for_suitable_configuration_last_response.encoded_response = reply;
|
||||
|
||||
debug(3,
|
||||
"output configuration search for request: %s/%u/%u, succeeded with response: %u/%s/%u.",
|
||||
sps_format_description_string(format), rate, channels,
|
||||
RATE_FROM_ENCODED_FORMAT(reply), // rate
|
||||
sps_format_description_string(FORMAT_FROM_ENCODED_FORMAT(reply)), // format
|
||||
CHANNELS_FROM_ENCODED_FORMAT(reply) // channels
|
||||
);
|
||||
|
||||
} else {
|
||||
debug(1, "output configuration search for request: %s/%u/%u failed.",
|
||||
sps_format_description_string(format), rate, channels);
|
||||
}
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
@@ -18,22 +18,52 @@ typedef enum {
|
||||
} play_samples_type;
|
||||
|
||||
typedef struct {
|
||||
double current_volume_dB;
|
||||
int32_t minimum_volume_dB;
|
||||
int32_t maximum_volume_dB;
|
||||
} audio_parameters;
|
||||
unsigned int channels;
|
||||
unsigned int rate;
|
||||
unsigned int format; // this contains an sps_format_t enum but "common.h" is not imported here.
|
||||
char short_description[32]; // e.g. S32/44100/8.
|
||||
char channel_map[128];
|
||||
} output_configuration_t;
|
||||
|
||||
typedef struct {
|
||||
int32_t minimum_volume_dB;
|
||||
int32_t maximum_volume_dB;
|
||||
} volume_range_t;
|
||||
|
||||
typedef struct {
|
||||
volume_range_t *volume_range;
|
||||
} output_parameters_t;
|
||||
|
||||
// backend interface
|
||||
typedef struct {
|
||||
// may be NULL if no implemented
|
||||
void (*help)(void);
|
||||
char *name;
|
||||
|
||||
// start of program
|
||||
int (*init)(int argc, char **argv);
|
||||
// at end of program
|
||||
void (*deinit)(void);
|
||||
void (*prepare_to_play)(void); // sent when audio is received for the first time -- advance warning.
|
||||
|
||||
int (*prepare)(void); // looks and sets stuff in the config data structure
|
||||
int (*prepare)(void); // send when a play session is about to begin
|
||||
|
||||
// Get an encoded configuration for the given channels, rate and depth
|
||||
// The response may have a different rate, format and channel count!
|
||||
// See FROM_ENCODED_FORMAL macros in common.h for the encoding scheme
|
||||
// An encoded configuration will always be positive.
|
||||
// 0 if not successful
|
||||
// may be NULL if not implemented
|
||||
// sps_format is not typed because the definition is in common.h, which can't be included here,
|
||||
// sigh.
|
||||
int32_t (*get_configuration)(unsigned int channels, unsigned int rate, unsigned int sps_format);
|
||||
|
||||
// Set the output configuration
|
||||
// Returns 0 and a channel map (if available) if successful
|
||||
// may be NULL if not implemented.
|
||||
// Set channel_map to NULL if you don't want it.
|
||||
// Otherwise, a space-separated channel map string will be returned
|
||||
// and you are responsible for freeing it.
|
||||
// If there isn't a channel map a NULL will be returned.
|
||||
int (*configure)(int32_t encoded_output_format, char **channel_map);
|
||||
|
||||
void (*start)(int sample_rate, int sample_format);
|
||||
|
||||
@@ -41,18 +71,18 @@ typedef struct {
|
||||
int (*play)(void *buf, int samples, int sample_type, uint32_t timestamp, uint64_t playtime);
|
||||
void (*stop)(void);
|
||||
|
||||
// may be null if no implemented
|
||||
// may be NULL if no implemented
|
||||
int (*is_running)(
|
||||
void); // if implemented, will return 0 if everything is okay, non-zero otherwise
|
||||
|
||||
// may be null if not implemented
|
||||
// may be NULL if no implemented
|
||||
void (*flush)(void);
|
||||
|
||||
// returns the delay before the next frame to be sent to the device would actually be audible.
|
||||
// almost certainly wrong if the buffer is empty, so put silent buffers into it to make it busy.
|
||||
// will change dynamically, so keep watching it. Implemented in ALSA only.
|
||||
// almost certainly wrong if the buffer is empty, so play silent buffers to make it busy.
|
||||
// will change dynamically, so keep watching it.
|
||||
// returns a negative error code if there's a problem
|
||||
int (*delay)(long *the_delay); // snd_pcm_sframes_t is a signed long
|
||||
int (*delay)(long *the_delay); // snd_pcm_sframes_t is a long
|
||||
int (*stats)(uint64_t *raw_measurement_time, uint64_t *corrected_measurement_time,
|
||||
uint64_t *delay,
|
||||
uint64_t *frames_sent_to_dac); // use this to get the true rate of the DAC
|
||||
@@ -60,8 +90,8 @@ typedef struct {
|
||||
// may be NULL, in which case soft volume is applied
|
||||
void (*volume)(double vol);
|
||||
|
||||
// may be NULL, in which case soft volume parameters are used
|
||||
void (*parameters)(audio_parameters *info);
|
||||
// may be NULL, in which case defaults are used
|
||||
output_parameters_t *(*parameters)();
|
||||
|
||||
// may be NULL, in which case software muting is used.
|
||||
// also, will return a 1 if it is actually using the mute facility, 0 otherwise
|
||||
@@ -69,8 +99,19 @@ typedef struct {
|
||||
|
||||
} audio_output;
|
||||
|
||||
// this looks for a suitable configuration in the right order and checks possible
|
||||
// configurations using the check_configuration method passed in.
|
||||
int32_t search_for_suitable_configuration(
|
||||
unsigned int channels, unsigned int rate, unsigned int format,
|
||||
int (*check_configuration)(unsigned int channels, unsigned int rate, unsigned int format));
|
||||
|
||||
audio_output *audio_get_output(const char *name);
|
||||
void audio_ls_outputs(void);
|
||||
void parse_general_audio_options(void);
|
||||
void parse_audio_options(const char *named_stanza, uint32_t default_format_set,
|
||||
uint32_t default_rate_set,
|
||||
uint32_t default_channel_set); // look in "general" and in the named stanza
|
||||
uint32_t get_format_settings(const char *stanza_name, const char *setting_name);
|
||||
uint32_t get_rate_settings(const char *stanza_name, const char *setting_name);
|
||||
uint32_t get_channel_settings(const char *stanza_name, const char *setting_name);
|
||||
|
||||
#endif //_AUDIO_H
|
||||
|
||||
+1032
-792
File diff suppressed because it is too large
Load Diff
+176
-35
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* libao output driver. This file is part of Shairport.
|
||||
* Copyright (c) James Laird 2013
|
||||
* Copyright (c) Mike Brady 2014 -- 2022
|
||||
* Copyright (c) Mike Brady 2014--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -32,10 +32,161 @@
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef struct {
|
||||
sps_format_t sps_format;
|
||||
int bits_per_sample;
|
||||
int byte_format;
|
||||
} sps_ao_t;
|
||||
|
||||
// these are the only formats that audio_pw will ever allow itself to be configured with
|
||||
static sps_ao_t format_lookup[] = {{SPS_FORMAT_S16_LE, 16, AO_FMT_LITTLE},
|
||||
{SPS_FORMAT_S16_BE, 16, AO_FMT_BIG},
|
||||
{SPS_FORMAT_S32_LE, 32, AO_FMT_LITTLE},
|
||||
{SPS_FORMAT_S32_BE, 32, AO_FMT_BIG}};
|
||||
|
||||
// use an SPS_FORMAT_... to find an entry in the format_lookup table or return NULL
|
||||
static sps_ao_t *sps_format_lookup(sps_format_t to_find) {
|
||||
sps_ao_t *response = NULL;
|
||||
unsigned int i = 0;
|
||||
while ((response == NULL) && (i < sizeof(format_lookup) / sizeof(sps_ao_t))) {
|
||||
if (format_lookup[i].sps_format == to_find)
|
||||
response = &format_lookup[i];
|
||||
else
|
||||
i++;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
ao_device *dev = NULL;
|
||||
ao_option *ao_opts = NULL;
|
||||
ao_sample_format fmt;
|
||||
ao_sample_format ao_output_format;
|
||||
int driver = 0;
|
||||
static int32_t current_encoded_output_format = 0;
|
||||
|
||||
static int check_settings(sps_format_t sample_format, unsigned int sample_rate,
|
||||
unsigned int channel_count) {
|
||||
|
||||
// debug(1, "ao check_settings: configuration: %u/%s/%u.", sample_rate,
|
||||
// sps_format_description_string(sample_format), channel_count);
|
||||
|
||||
int response = EINVAL;
|
||||
|
||||
sps_ao_t *format_info = sps_format_lookup(sample_format);
|
||||
if (format_info != NULL) {
|
||||
|
||||
ao_sample_format check_fmt;
|
||||
memset(&check_fmt, 0, sizeof(check_fmt));
|
||||
check_fmt.bits = format_info->bits_per_sample;
|
||||
check_fmt.rate = sample_rate;
|
||||
check_fmt.channels = channel_count;
|
||||
check_fmt.byte_format = format_info->byte_format;
|
||||
// fmt.matrix = strdup("L,R");
|
||||
ao_device *check_dev = ao_open_live(driver, &check_fmt, ao_opts);
|
||||
if (check_dev != NULL) {
|
||||
debug(3, "ao: check_settings %u/%s/%u works!", sample_rate,
|
||||
sps_format_description_string(sample_format), channel_count);
|
||||
if (ao_close(check_dev) == 0)
|
||||
debug(1, "ao check_settings: error at ao_close");
|
||||
response = 0;
|
||||
} else {
|
||||
debug(3, "ao: check_settings %u/%s/%u is not available!", sample_rate,
|
||||
sps_format_description_string(sample_format), channel_count);
|
||||
}
|
||||
|
||||
// response = 0;
|
||||
}
|
||||
|
||||
// debug(1, "pa check_settings: configuration: %u/%s/%u %s.", sample_rate,
|
||||
// sps_format_description_string(sample_format), channel_count,
|
||||
// response == 0 ? "is okay" : "can not be configured");
|
||||
return response;
|
||||
}
|
||||
|
||||
static int check_configuration(unsigned int channels, unsigned int rate, unsigned int format) {
|
||||
return check_settings(format, rate, channels);
|
||||
}
|
||||
|
||||
static int32_t get_configuration(unsigned int channels, unsigned int rate, unsigned int format) {
|
||||
uint64_t start_time = get_absolute_time_in_ns();
|
||||
int32_t response =
|
||||
search_for_suitable_configuration(channels, rate, format, &check_configuration);
|
||||
int64_t elapsed_time = get_absolute_time_in_ns() - start_time;
|
||||
debug(1, "ao: get_configuration took %0.3f mS.", elapsed_time * 0.000001);
|
||||
return response;
|
||||
}
|
||||
|
||||
static int configure(int32_t requested_encoded_format, char **channel_map) {
|
||||
debug(3, "a0: configure %s.", short_format_description(requested_encoded_format));
|
||||
int response = EINVAL;
|
||||
if (current_encoded_output_format != requested_encoded_format) {
|
||||
uint64_t start_time = get_absolute_time_in_ns();
|
||||
if (current_encoded_output_format == 0)
|
||||
debug(1, "a0: setting output configuration to %s.",
|
||||
short_format_description(requested_encoded_format));
|
||||
else
|
||||
// note -- can't use short_format_description twice in one call because it returns the same
|
||||
// string buffer each time
|
||||
debug(1, "a0: changing output configuration to %s.",
|
||||
short_format_description(requested_encoded_format));
|
||||
current_encoded_output_format = requested_encoded_format;
|
||||
sps_ao_t *format_info =
|
||||
sps_format_lookup(FORMAT_FROM_ENCODED_FORMAT(current_encoded_output_format));
|
||||
|
||||
if (format_info == NULL)
|
||||
die("ao: can't find format information!");
|
||||
|
||||
if (dev != NULL) {
|
||||
if (ao_close(dev) == 0) {
|
||||
debug(1, "ao configure: error closing existing device ao_close");
|
||||
}
|
||||
}
|
||||
dev = NULL;
|
||||
memset(&ao_output_format, 0, sizeof(ao_output_format));
|
||||
ao_output_format.bits = format_info->bits_per_sample;
|
||||
ao_output_format.rate = RATE_FROM_ENCODED_FORMAT(current_encoded_output_format);
|
||||
ao_output_format.channels = CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format);
|
||||
ao_output_format.byte_format = format_info->byte_format;
|
||||
switch (CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format)) {
|
||||
case 2:
|
||||
ao_output_format.matrix = strdup("L,R");
|
||||
break;
|
||||
case 6:
|
||||
ao_output_format.matrix = strdup("L,R,C,LFE,BR,BL");
|
||||
break;
|
||||
case 8:
|
||||
ao_output_format.matrix = strdup("L,R,C,LFE,BR,BL,SL,SR");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// fmt.matrix = strdup("L,R");
|
||||
/*
|
||||
dev = ao_open_live(driver, &ao_output_format, ao_opts);
|
||||
if (dev != NULL) {
|
||||
debug(
|
||||
1, "ao: configure %u/%s/%u opened!",
|
||||
RATE_FROM_ENCODED_FORMAT(current_encoded_output_format),
|
||||
sps_format_description_string(FORMAT_FROM_ENCODED_FORMAT(current_encoded_output_format)),
|
||||
CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format));
|
||||
response = 0;
|
||||
} else {
|
||||
debug(
|
||||
1, "ao: configure %u/%s/%u is not available!",
|
||||
RATE_FROM_ENCODED_FORMAT(current_encoded_output_format),
|
||||
sps_format_description_string(FORMAT_FROM_ENCODED_FORMAT(current_encoded_output_format)),
|
||||
CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format));
|
||||
}
|
||||
*/
|
||||
int64_t elapsed_time = get_absolute_time_in_ns() - start_time;
|
||||
debug(3, "pa: configure took %0.3f mS.", elapsed_time * 0.000001);
|
||||
} else {
|
||||
debug(3, "pa: setting output configuration -- configuration unchanged, so nothing done.");
|
||||
}
|
||||
if ((response == 0) && (channel_map != NULL)) {
|
||||
*channel_map = NULL; // nothing back here
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
static void help(void) {
|
||||
printf(" -d driver set the output driver\n"
|
||||
@@ -69,12 +220,10 @@ static void help(void) {
|
||||
}
|
||||
|
||||
static int init(int argc, char **argv) {
|
||||
int oldState;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
|
||||
ao_initialize();
|
||||
driver = ao_default_driver_id();
|
||||
if (driver == -1) {
|
||||
warn("libao can not find a usable driver!");
|
||||
warn("the ao backend can not find a usable output device with the default driver!");
|
||||
} else {
|
||||
|
||||
// set up default values first
|
||||
@@ -85,8 +234,14 @@ static int init(int argc, char **argv) {
|
||||
// get settings from settings file first, allow them to be overridden by
|
||||
// command line options
|
||||
|
||||
// do the "general" audio options. Note, these options are in the "general" stanza!
|
||||
parse_general_audio_options();
|
||||
// get settings from settings file, passing in defaults for format_set, rate_set and channel_set
|
||||
// Note, these options may be in the "general" stanza or the named stanza
|
||||
#ifdef CONFIG_FFMPEG
|
||||
parse_audio_options("ao", SPS_FORMAT_SET, SPS_RATE_SET, SPS_CHANNEL_SET);
|
||||
#else
|
||||
parse_audio_options("ao", SPS_FORMAT_NON_FFMPEG_SET, SPS_RATE_NON_FFMPEG_SET,
|
||||
SPS_CHANNNEL_NON_FFMPEG_SET);
|
||||
#endif
|
||||
|
||||
optind = 1; // optind=0 is equivalent to optind=1 plus special behaviour
|
||||
argv--; // so we shift the arguments to satisfy getopt()
|
||||
@@ -127,66 +282,52 @@ static int init(int argc, char **argv) {
|
||||
if (optind < argc)
|
||||
die("Invalid audio argument: %s", argv[optind]);
|
||||
|
||||
memset(&fmt, 0, sizeof(fmt));
|
||||
|
||||
fmt.bits = 16;
|
||||
fmt.rate = 44100;
|
||||
fmt.channels = 2;
|
||||
fmt.byte_format = AO_FMT_NATIVE;
|
||||
fmt.matrix = strdup("L,R");
|
||||
// get_permissible_configuration_settings();
|
||||
}
|
||||
pthread_setcancelstate(oldState, NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void deinit(void) {
|
||||
int oldState;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
|
||||
if (dev != NULL)
|
||||
ao_close(dev);
|
||||
dev = NULL;
|
||||
ao_shutdown();
|
||||
pthread_setcancelstate(oldState, NULL);
|
||||
}
|
||||
|
||||
static void start(__attribute__((unused)) int sample_rate,
|
||||
__attribute__((unused)) int sample_format) {
|
||||
// debug(1,"libao start");
|
||||
}
|
||||
|
||||
static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
__attribute__((unused)) uint32_t timestamp,
|
||||
__attribute__((unused)) uint64_t playtime) {
|
||||
int response = 0;
|
||||
|
||||
if (dev == NULL) {
|
||||
debug(1, "ao play(): ao_open_live to play %d samples, bytes per sample: %u, channels: %u",
|
||||
samples, ao_output_format.bits / 8, ao_output_format.channels);
|
||||
dev = ao_open_live(driver, &ao_output_format, ao_opts);
|
||||
}
|
||||
// debug(1,"ao play(): play %d samples, bytes per sample: %u, channels: %u", samples,
|
||||
// ao_output_format.bits/8, ao_output_format.channels);
|
||||
int oldState;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
|
||||
if (driver != -1) {
|
||||
if (dev == NULL)
|
||||
dev = ao_open_live(driver, &fmt, ao_opts);
|
||||
if (dev != NULL)
|
||||
response = ao_play(dev, buf, samples * 4);
|
||||
}
|
||||
response = ao_play(dev, buf, samples * ao_output_format.bits / 8 * ao_output_format.channels);
|
||||
pthread_setcancelstate(oldState, NULL);
|
||||
return response;
|
||||
}
|
||||
|
||||
static void stop(void) {
|
||||
// debug(1,"libao stop");
|
||||
int oldState;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
|
||||
if (dev != NULL) {
|
||||
debug(1, "ao stop(): ao_close");
|
||||
ao_close(dev);
|
||||
dev = NULL;
|
||||
}
|
||||
pthread_setcancelstate(oldState, NULL);
|
||||
}
|
||||
|
||||
audio_output audio_ao = {.name = "ao",
|
||||
.help = &help,
|
||||
.init = &init,
|
||||
.deinit = &deinit,
|
||||
.prepare = NULL,
|
||||
.start = &start,
|
||||
.configure = &configure,
|
||||
.get_configuration = &get_configuration,
|
||||
// .start = &start,
|
||||
.stop = &stop,
|
||||
.is_running = NULL,
|
||||
.flush = NULL,
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
* All rights reserved.
|
||||
*
|
||||
* Modifications for audio synchronisation
|
||||
* and related work, copyright (c) Mike Brady 2014 -- 2022
|
||||
* and related work, copyright (c) Mike Brady 2014--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -71,7 +71,7 @@ audio_output audio_dummy = {.name = "dummy",
|
||||
.help = NULL,
|
||||
.init = &init,
|
||||
.deinit = &deinit,
|
||||
.prepare = NULL,
|
||||
.configure = NULL,
|
||||
.start = &start,
|
||||
.stop = &stop,
|
||||
.is_running = NULL,
|
||||
|
||||
+12
-9
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* jack output driver. This file is part of Shairport Sync.
|
||||
* Copyright (c) 2019 -- 2024 Mike Brady <4265913+mikebrady@users.noreply.github.com>,
|
||||
* Jörn Nettingsmeier <nettings@luchtbeweging.nl>
|
||||
* Copyright (c) 2019--2025 Mike Brady <4265913+mikebrady@users.noreply.github.com>,
|
||||
* Jörn Nettingsmeier <nettings@luchtbeweging.nl>
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -50,7 +50,6 @@ pthread_mutex_t client_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
jack_port_t *port[NPORTS];
|
||||
const char *port_name[NPORTS] = {"out_L", "out_R"};
|
||||
|
||||
|
||||
int sps_sample_rate;
|
||||
|
||||
jack_client_t *client;
|
||||
@@ -187,8 +186,9 @@ static int jack_init(__attribute__((unused)) int argc, __attribute__((unused)) c
|
||||
// instead.
|
||||
config.audio_backend_buffer_interpolation_threshold_in_seconds = 0.25;
|
||||
|
||||
// Do the "general" audio options. Note, these options are in the "general" stanza!
|
||||
parse_general_audio_options();
|
||||
// this ensures that the Shairport Sync system will provide only 44100/S16/2 to this backend.
|
||||
parse_audio_options(NULL, (1 << SPS_FORMAT_S16_LE), (1 << SPS_RATE_44100), (1 << 2));
|
||||
|
||||
#ifdef CONFIG_SOXR
|
||||
config.jack_soxr_resample_quality = -1; // don't resample by default
|
||||
#endif
|
||||
@@ -335,7 +335,8 @@ static void jack_start(int i_sample_rate, __attribute__((unused)) int i_sample_f
|
||||
soxr_delete(soxr);
|
||||
}
|
||||
soxr_error_t e = NULL;
|
||||
soxr = soxr_create(sps_sample_rate, jack_sample_rate, NPORTS, &e, &io_spec, &quality_spec, NULL);
|
||||
soxr =
|
||||
soxr_create(sps_sample_rate, jack_sample_rate, NPORTS, &e, &io_spec, &quality_spec, NULL);
|
||||
if (!soxr) {
|
||||
die("Unable to create soxr resampler for JACK: %s", e);
|
||||
}
|
||||
@@ -370,10 +371,12 @@ static int jack_delay(long *the_delay) {
|
||||
// jack_latency is set by the graph() callback, it's the average of the maximum
|
||||
// latencies of all our output ports. Adjust this constant baseline delay according
|
||||
// to the buffer fill level:
|
||||
int64_t the_delay_in_jack_frames = jack_latency + audio_occupancy_now - frames_processed_since_latest_latency_check;
|
||||
int64_t the_delay_in_jack_frames =
|
||||
jack_latency + audio_occupancy_now - frames_processed_since_latest_latency_check;
|
||||
int64_t the_delay_in_sps_frames = (the_delay_in_jack_frames * sps_sample_rate) / jack_sample_rate;
|
||||
*the_delay = the_delay_in_sps_frames;
|
||||
// debug(2, "reporting a delay of %ld frames at Shairport Sync's rate of %d FPS.",*the_delay, sps_sample_rate);
|
||||
// debug(2, "reporting a delay of %ld frames at Shairport Sync's rate of %d FPS.",*the_delay,
|
||||
// sps_sample_rate);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -430,7 +433,7 @@ audio_output audio_jack = {.name = "jack",
|
||||
.help = NULL,
|
||||
.init = &jack_init,
|
||||
.deinit = &jack_deinit,
|
||||
.prepare = NULL,
|
||||
.configure = NULL,
|
||||
.start = &jack_start,
|
||||
.stop = NULL,
|
||||
.is_running = NULL,
|
||||
|
||||
+534
-171
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Asynchronous PulseAudio Backend. This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2017-2023
|
||||
* PulseAudio Backend. This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2017--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -36,12 +36,20 @@
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// note -- these are hacked and hardwired into this code.
|
||||
#define FORMAT PA_SAMPLE_S16NE
|
||||
#define RATE 44100
|
||||
typedef struct {
|
||||
pa_sample_format_t pa_format;
|
||||
sps_format_t sps_format;
|
||||
unsigned int bytes_per_sample;
|
||||
} pa_sps_t;
|
||||
|
||||
// Four seconds buffer -- should be plenty
|
||||
#define buffer_allocation RATE * 4 * 2 * 2
|
||||
// these are the only formats that audio_pw will ever allow itself to be configured with
|
||||
static pa_sps_t format_lookup[] = {{PA_SAMPLE_S16LE, SPS_FORMAT_S16_LE, 2},
|
||||
{PA_SAMPLE_S16BE, SPS_FORMAT_S16_BE, 2},
|
||||
{PA_SAMPLE_S32LE, SPS_FORMAT_S32_LE, 4},
|
||||
{PA_SAMPLE_S32BE, SPS_FORMAT_S32_BE, 4}};
|
||||
|
||||
#define CHANNEL_MAP_SIZE 1024
|
||||
static char channel_map[CHANNEL_MAP_SIZE + 1];
|
||||
|
||||
static pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
@@ -49,9 +57,24 @@ pa_threaded_mainloop *mainloop;
|
||||
pa_mainloop_api *mainloop_api;
|
||||
pa_context *context;
|
||||
pa_stream *stream;
|
||||
|
||||
static int32_t current_encoded_output_format = 0;
|
||||
const char *default_channel_layouts = NULL;
|
||||
static char *audio_lmb, *audio_umb, *audio_toq, *audio_eoq;
|
||||
static size_t audio_size = buffer_allocation;
|
||||
static size_t audio_occupancy;
|
||||
static size_t audio_size, audio_occupancy;
|
||||
|
||||
// use an SPS_FORMAT_... to find an entry in the format_lookup table or return NULL
|
||||
static pa_sps_t *sps_format_lookup(sps_format_t to_find) {
|
||||
pa_sps_t *response = NULL;
|
||||
unsigned int i = 0;
|
||||
while ((response == NULL) && (i < sizeof(format_lookup) / sizeof(pa_sps_t))) {
|
||||
if (format_lookup[i].sps_format == to_find)
|
||||
response = &format_lookup[i];
|
||||
else
|
||||
i++;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
void context_state_cb(pa_context *context, void *mainloop);
|
||||
void stream_state_cb(pa_stream *s, void *mainloop);
|
||||
@@ -90,66 +113,367 @@ static void check_pa_stream_status(pa_stream *p, const char *message) {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
static int check_settings(sps_format_t sample_format, unsigned int sample_rate,
|
||||
unsigned int channel_count) {
|
||||
|
||||
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;
|
||||
// debug(1, "pa check_settings: configuration: %u/%s/%u.", sample_rate,
|
||||
// sps_format_description_string(sample_format), channel_count);
|
||||
|
||||
pa_channel_map map;
|
||||
pa_channel_map_init_stereo(&map);
|
||||
int response = EINVAL;
|
||||
|
||||
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);
|
||||
pa_sps_t *format_info = sps_format_lookup(sample_format);
|
||||
if (format_info != NULL) {
|
||||
// here, try to create a stream of the given format.
|
||||
|
||||
// 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_threaded_mainloop_lock(mainloop);
|
||||
// Create a playback stream
|
||||
pa_sample_spec sample_specifications;
|
||||
sample_specifications.format = format_info->pa_format;
|
||||
sample_specifications.rate = sample_rate;
|
||||
sample_specifications.channels = channel_count;
|
||||
|
||||
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;
|
||||
pa_channel_map map;
|
||||
pa_channel_map_init_auto(&map, sample_specifications.channels, PA_CHANNEL_MAP_DEFAULT);
|
||||
|
||||
int connect_result;
|
||||
pa_stream *check_settings_stream =
|
||||
pa_stream_new(context, "Playback", &sample_specifications, &map);
|
||||
|
||||
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 (check_settings_stream != NULL) {
|
||||
response = 0; // success
|
||||
pa_stream_unref(check_settings_stream);
|
||||
}
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
|
||||
// response = 0;
|
||||
}
|
||||
|
||||
if (connect_result != 0)
|
||||
die("could not connect to the pulseaudio playback stream -- the error message is \"%s\".",
|
||||
pa_strerror(pa_context_errno(context)));
|
||||
// debug(1, "pa check_settings: configuration: %u/%s/%u %s.", sample_rate,
|
||||
// sps_format_description_string(sample_format), channel_count,
|
||||
// response == 0 ? "is okay" : "can not be configured");
|
||||
return response;
|
||||
}
|
||||
|
||||
// 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\".",
|
||||
static int check_configuration(unsigned int channels, unsigned int rate, unsigned int format) {
|
||||
return check_settings(format, rate, channels);
|
||||
}
|
||||
|
||||
static int32_t get_configuration(unsigned int channels, unsigned int rate, unsigned int format) {
|
||||
uint64_t start_time = get_absolute_time_in_ns();
|
||||
int32_t response =
|
||||
search_for_suitable_configuration(channels, rate, format, &check_configuration);
|
||||
int64_t elapsed_time = get_absolute_time_in_ns() - start_time;
|
||||
debug(3, "pa: get_configuration took %0.3f mS.", elapsed_time * 0.000001);
|
||||
return response;
|
||||
}
|
||||
|
||||
static int configure(int32_t requested_encoded_format, char **resulting_channel_map) {
|
||||
debug(3, "pa: configure %s.", short_format_description(requested_encoded_format));
|
||||
int response = 0;
|
||||
if (current_encoded_output_format != requested_encoded_format) {
|
||||
uint64_t start_time = get_absolute_time_in_ns();
|
||||
if (current_encoded_output_format == 0)
|
||||
debug(3, "pa: setting output configuration to %s.",
|
||||
short_format_description(requested_encoded_format));
|
||||
else
|
||||
// note -- can't use short_format_description twice in one call because it returns the same
|
||||
// string buffer each time
|
||||
debug(3, "pa: changing output configuration to %s.",
|
||||
short_format_description(requested_encoded_format));
|
||||
current_encoded_output_format = requested_encoded_format;
|
||||
pa_sps_t *format_info =
|
||||
sps_format_lookup(FORMAT_FROM_ENCODED_FORMAT(current_encoded_output_format));
|
||||
|
||||
if (format_info == NULL)
|
||||
die("pa: can't find format information!");
|
||||
|
||||
if (audio_lmb != NULL) {
|
||||
free(audio_lmb); // release previous buffer
|
||||
}
|
||||
|
||||
if (stream != NULL) {
|
||||
// debug(1, "pa: stopping and releasing the current stream...");
|
||||
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_stream_disconnect(stream);
|
||||
pa_stream_unref(stream);
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
stream = NULL;
|
||||
}
|
||||
|
||||
audio_size = RATE_FROM_ENCODED_FORMAT(current_encoded_output_format) *
|
||||
format_info->bytes_per_sample *
|
||||
CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format) * 1; // one seconds
|
||||
audio_lmb = malloc(audio_size);
|
||||
if (audio_lmb == NULL)
|
||||
die("Can't allocate %d bytes for pulseaudio buffer.", audio_size);
|
||||
audio_toq = audio_eoq = audio_lmb;
|
||||
audio_umb = audio_lmb + audio_size;
|
||||
audio_occupancy = 0;
|
||||
|
||||
pa_threaded_mainloop_lock(mainloop);
|
||||
// Create a playback stream
|
||||
pa_sample_spec sample_specifications;
|
||||
sample_specifications.format = format_info->pa_format;
|
||||
sample_specifications.rate = RATE_FROM_ENCODED_FORMAT(current_encoded_output_format);
|
||||
sample_specifications.channels = CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format);
|
||||
|
||||
uint32_t buffer_size_in_bytes =
|
||||
(uint32_t)CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format) *
|
||||
format_info->bytes_per_sample * RATE_FROM_ENCODED_FORMAT(current_encoded_output_format) /
|
||||
10;
|
||||
|
||||
pa_channel_map map;
|
||||
|
||||
pa_channel_map *pacm = NULL;
|
||||
|
||||
// if we've not asked specifically for native formats, ask for the alsa format...
|
||||
if ((default_channel_layouts == NULL) || (strcasecmp(default_channel_layouts, "alsa") == 0)) {
|
||||
pacm = pa_channel_map_init_auto(&map, sample_specifications.channels, PA_CHANNEL_MAP_ALSA);
|
||||
}
|
||||
|
||||
// ask for the native format...
|
||||
if (pacm == NULL) {
|
||||
pacm = pa_channel_map_init_auto(&map, sample_specifications.channels, PA_CHANNEL_MAP_DEFAULT);
|
||||
}
|
||||
|
||||
// ask for some format...
|
||||
if (pacm == NULL) {
|
||||
pacm =
|
||||
pa_channel_map_init_extend(&map, sample_specifications.channels, PA_CHANNEL_MAP_DEFAULT);
|
||||
}
|
||||
|
||||
if (resulting_channel_map != NULL) { // if needed...
|
||||
// PA_CHANNEL_MAP_ALSA gives default channel maps that correspond to the FFmpeg defaults.
|
||||
// make up a channel map
|
||||
channel_map[0] = '\0';
|
||||
int c;
|
||||
for (c = 0; c < map.channels; c++) {
|
||||
switch (map.map[c]) {
|
||||
|
||||
case PA_CHANNEL_POSITION_MONO:
|
||||
strncat(channel_map, "FC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_FRONT_LEFT:
|
||||
strncat(channel_map, "FL", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_FRONT_RIGHT:
|
||||
strncat(channel_map, "FR", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_FRONT_CENTER:
|
||||
strncat(channel_map, "FC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_REAR_CENTER:
|
||||
strncat(channel_map, "BC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_REAR_LEFT:
|
||||
strncat(channel_map, "BL", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_REAR_RIGHT:
|
||||
strncat(channel_map, "BR", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_LFE:
|
||||
strncat(channel_map, "LFE", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER:
|
||||
strncat(channel_map, "FLC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER:
|
||||
strncat(channel_map, "FRC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_SIDE_LEFT:
|
||||
strncat(channel_map, "SL", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_SIDE_RIGHT:
|
||||
strncat(channel_map, "SR", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX0:
|
||||
strncat(channel_map, "AUX0", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX1:
|
||||
strncat(channel_map, "AUX1", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX2:
|
||||
strncat(channel_map, "AUX2", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX3:
|
||||
strncat(channel_map, "AUX3", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX4:
|
||||
strncat(channel_map, "AUX4", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX5:
|
||||
strncat(channel_map, "AUX5", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX6:
|
||||
strncat(channel_map, "AUX6", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX7:
|
||||
strncat(channel_map, "AUX7", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX8:
|
||||
strncat(channel_map, "AUX8", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX9:
|
||||
strncat(channel_map, "AUX9", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX10:
|
||||
strncat(channel_map, "AUX10", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX11:
|
||||
strncat(channel_map, "AUX11", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX12:
|
||||
strncat(channel_map, "AUX12", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX13:
|
||||
strncat(channel_map, "AUX13", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX14:
|
||||
strncat(channel_map, "AUX14", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX15:
|
||||
strncat(channel_map, "AUX15", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX16:
|
||||
strncat(channel_map, "AUX16", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX17:
|
||||
strncat(channel_map, "AUX17", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX18:
|
||||
strncat(channel_map, "AUX18", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX19:
|
||||
strncat(channel_map, "AUX19", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX20:
|
||||
strncat(channel_map, "AUX20", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX21:
|
||||
strncat(channel_map, "AUX21", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX22:
|
||||
strncat(channel_map, "AUX22", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX23:
|
||||
strncat(channel_map, "AUX23", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX24:
|
||||
strncat(channel_map, "AUX24", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX25:
|
||||
strncat(channel_map, "AUX25", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX26:
|
||||
strncat(channel_map, "AUX26", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX27:
|
||||
strncat(channel_map, "AUX27", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX28:
|
||||
strncat(channel_map, "AUX28", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX29:
|
||||
strncat(channel_map, "AUX29", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX30:
|
||||
strncat(channel_map, "AUX30", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX31:
|
||||
strncat(channel_map, "AUX31", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_TOP_CENTER:
|
||||
strncat(channel_map, "TC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_TOP_FRONT_LEFT:
|
||||
strncat(channel_map, "TFL", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_TOP_FRONT_RIGHT:
|
||||
strncat(channel_map, "TFR", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_TOP_FRONT_CENTER:
|
||||
strncat(channel_map, "TFC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_TOP_REAR_LEFT:
|
||||
strncat(channel_map, "TBL", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_TOP_REAR_RIGHT:
|
||||
strncat(channel_map, "TBR", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_TOP_REAR_CENTER:
|
||||
strncat(channel_map, "TBC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (c != (map.channels - 1))
|
||||
strncat(channel_map, " ", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
}
|
||||
|
||||
debug(1, "audio_pa: channel map for %d channels is \"%s\".", sample_specifications.channels,
|
||||
channel_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);
|
||||
|
||||
// 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)));
|
||||
if (stream_state == PA_STREAM_READY)
|
||||
break;
|
||||
pa_threaded_mainloop_wait(mainloop);
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
// 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);
|
||||
|
||||
// to here
|
||||
|
||||
int64_t elapsed_time = get_absolute_time_in_ns() - start_time;
|
||||
debug(3, "pa: configuration took %0.3f mS.", elapsed_time * 0.000001);
|
||||
} else {
|
||||
debug(3, "pa: setting output configuration -- configuration unchanged, so nothing done.");
|
||||
}
|
||||
if ((response == 0) && (resulting_channel_map != NULL)) {
|
||||
*resulting_channel_map = channel_map;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
static int init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) {
|
||||
@@ -162,42 +486,55 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char *
|
||||
|
||||
config.audio_backend_latency_offset = 0;
|
||||
|
||||
// get settings from settings file
|
||||
|
||||
// do the "general" audio options. Note, these options are in the "general" stanza!
|
||||
parse_general_audio_options();
|
||||
// get settings from settings file, passing in defaults for format_set, rate_set and channel_set
|
||||
// Note, these options may be in the "general" stanza or the named stanza
|
||||
#ifdef CONFIG_FFMPEG
|
||||
parse_audio_options("pulseaudio", SPS_FORMAT_SET, SPS_RATE_SET, SPS_CHANNEL_SET);
|
||||
#else
|
||||
parse_audio_options("pulseaudio", SPS_FORMAT_NON_FFMPEG_SET, SPS_RATE_NON_FFMPEG_SET,
|
||||
SPS_CHANNNEL_NON_FFMPEG_SET);
|
||||
#endif
|
||||
|
||||
// now the specific options
|
||||
if (config.cfg != NULL) {
|
||||
const char *str;
|
||||
|
||||
/* Get the PulseAudio server name. */
|
||||
if (config_lookup_string(config.cfg, "pa.server", &str)) {
|
||||
if (config_lookup_string(config.cfg, "pulseaudio.server", &str)) {
|
||||
config.pa_server = (char *)str;
|
||||
}
|
||||
|
||||
// get the default channel mapping setting basis -- native ("no") or pa ("yes").
|
||||
|
||||
if (config_lookup_string(config.cfg, "pulseaudio", &default_channel_layouts)) {
|
||||
if ((strcasecmp(default_channel_layouts, "alsa") == 0) ||
|
||||
(strcasecmp(default_channel_layouts, "pulseaudio") == 0)) {
|
||||
debug(1, "pulseaudio default_channel_layouts setting: \"%s\".", default_channel_layouts);
|
||||
} else {
|
||||
debug(1, "Invalid pulseaudio default_channel_layouts setting. Must be \"alsa\" or "
|
||||
"\"pulseaudio\".");
|
||||
default_channel_layouts = NULL;
|
||||
}
|
||||
};
|
||||
|
||||
/* Get the Application Name. */
|
||||
if (config_lookup_string(config.cfg, "pa.application_name", &str)) {
|
||||
if (config_lookup_string(config.cfg, "pulseaudio.application_name", &str)) {
|
||||
config.pa_application_name = (char *)str;
|
||||
}
|
||||
|
||||
/* Get the PulseAudio sink name. */
|
||||
if (config_lookup_string(config.cfg, "pa.sink", &str)) {
|
||||
if (config_lookup_string(config.cfg, "pulseaudio.sink", &str)) {
|
||||
config.pa_sink = (char *)str;
|
||||
}
|
||||
}
|
||||
|
||||
// finish collecting settings
|
||||
|
||||
// allocate space for the audio buffer
|
||||
audio_lmb = malloc(audio_size);
|
||||
if (audio_lmb == NULL)
|
||||
die("Can't allocate %d bytes for pulseaudio buffer.", audio_size);
|
||||
audio_toq = audio_eoq = audio_lmb;
|
||||
audio_umb = audio_lmb + audio_size;
|
||||
audio_occupancy = 0;
|
||||
stream = NULL; // no stream
|
||||
audio_lmb = NULL; // no buffer
|
||||
|
||||
// Get a mainloop and its context
|
||||
|
||||
mainloop = pa_threaded_mainloop_new();
|
||||
if (mainloop == NULL)
|
||||
die("could not create a pa_threaded_mainloop.");
|
||||
@@ -232,25 +569,27 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char *
|
||||
break;
|
||||
pa_threaded_mainloop_wait(mainloop);
|
||||
}
|
||||
|
||||
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");
|
||||
// debug(1, "pa deinit");
|
||||
if (stream != NULL) {
|
||||
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 start(__attribute__((unused)) int sample_rate,
|
||||
__attribute__((unused)) int sample_format) {
|
||||
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,
|
||||
@@ -258,13 +597,17 @@ static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
// debug(1,"pa_play of %d samples.",samples);
|
||||
// copy the samples into the queue
|
||||
check_pa_stream_status(stream, "audio_pa play.");
|
||||
size_t bytes_to_transfer = samples * 2 * 2;
|
||||
|
||||
pa_sps_t *format_info =
|
||||
sps_format_lookup(FORMAT_FROM_ENCODED_FORMAT(current_encoded_output_format));
|
||||
size_t bytes_to_transfer = samples * format_info->bytes_per_sample *
|
||||
CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format);
|
||||
|
||||
pthread_mutex_lock(&buffer_mutex);
|
||||
size_t bytes_available = audio_size - audio_occupancy;
|
||||
if (bytes_available < bytes_to_transfer)
|
||||
bytes_to_transfer = bytes_available;
|
||||
if (bytes_to_transfer > 0) {
|
||||
if ((bytes_to_transfer > 0) && (audio_lmb != NULL)) {
|
||||
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);
|
||||
@@ -278,7 +621,11 @@ static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
audio_occupancy += bytes_to_transfer;
|
||||
}
|
||||
|
||||
if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) {
|
||||
if ((audio_occupancy >=
|
||||
(RATE_FROM_ENCODED_FORMAT(current_encoded_output_format) * format_info->bytes_per_sample *
|
||||
CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format)) /
|
||||
4) &&
|
||||
(pa_stream_is_corked(stream))) {
|
||||
// debug(1,"Uncorked");
|
||||
pthread_mutex_unlock(&buffer_mutex);
|
||||
pa_threaded_mainloop_lock(mainloop);
|
||||
@@ -291,6 +638,7 @@ static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
}
|
||||
|
||||
int pa_delay(long *the_delay) {
|
||||
// debug(1, "pa delay");
|
||||
check_pa_stream_status(stream, "audio_pa delay.");
|
||||
// debug(1,"pa_delay");
|
||||
long result = 0;
|
||||
@@ -301,13 +649,16 @@ int pa_delay(long *the_delay) {
|
||||
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;
|
||||
pa_sps_t *format_info =
|
||||
sps_format_lookup(FORMAT_FROM_ENCODED_FORMAT(current_encoded_output_format));
|
||||
// convert audio_occupancy bytes to frames and latency microseconds into frames
|
||||
result = (audio_occupancy / (format_info->bytes_per_sample *
|
||||
CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format))) +
|
||||
(latency * RATE_FROM_ENCODED_FORMAT(current_encoded_output_format)) / 1000000;
|
||||
reply = 0;
|
||||
}
|
||||
*the_delay = result;
|
||||
@@ -315,14 +666,17 @@ int pa_delay(long *the_delay) {
|
||||
}
|
||||
|
||||
static void flush(void) {
|
||||
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);
|
||||
// debug(1, "pa flush");
|
||||
if (stream != NULL) {
|
||||
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);
|
||||
}
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
}
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
pthread_mutex_lock(&buffer_mutex);
|
||||
audio_toq = audio_eoq = audio_lmb;
|
||||
audio_umb = audio_lmb + audio_size;
|
||||
@@ -331,15 +685,18 @@ static void flush(void) {
|
||||
}
|
||||
|
||||
static void stop(void) {
|
||||
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);
|
||||
// debug(1, "pa stop");
|
||||
if (stream != NULL) {
|
||||
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);
|
||||
}
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
}
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
pthread_mutex_lock(&buffer_mutex);
|
||||
audio_toq = audio_eoq = audio_lmb;
|
||||
audio_umb = audio_lmb + audio_size;
|
||||
@@ -347,12 +704,86 @@ static void stop(void) {
|
||||
pthread_mutex_unlock(&buffer_mutex);
|
||||
}
|
||||
|
||||
audio_output audio_pa = {.name = "pa",
|
||||
void context_state_cb(__attribute__((unused)) pa_context *local_context, void *local_mainloop) {
|
||||
// debug(1,"context_state_cb called.");
|
||||
pa_threaded_mainloop_signal(local_mainloop, 0);
|
||||
}
|
||||
|
||||
void stream_state_cb(__attribute__((unused)) pa_stream *s, void *local_mainloop) {
|
||||
// debug(1,"stream_state_cb called.");
|
||||
pa_threaded_mainloop_signal(local_mainloop, 0);
|
||||
}
|
||||
|
||||
void stream_write_cb(pa_stream *local_stream, size_t requested_bytes,
|
||||
__attribute__((unused)) void *userdata) {
|
||||
// debug(1, "pa stream_write_cb");
|
||||
check_pa_stream_status(local_stream, "audio_pa stream_write_cb.");
|
||||
int bytes_to_transfer = requested_bytes;
|
||||
int bytes_transferred = 0;
|
||||
uint8_t *buffer = NULL;
|
||||
int ret = 0;
|
||||
pthread_mutex_lock(&buffer_mutex);
|
||||
pthread_cleanup_push(mutex_unlock, (void *)&buffer_mutex);
|
||||
while ((bytes_to_transfer > 0) && (audio_occupancy > 0) && (ret == 0)) {
|
||||
if (pa_stream_is_suspended(local_stream))
|
||||
debug(1, "local_stream is suspended");
|
||||
size_t bytes_we_can_transfer = bytes_to_transfer;
|
||||
if (audio_occupancy == 0) {
|
||||
pa_stream_cork(local_stream, 1, stream_success_cb, mainloop);
|
||||
debug(1, "stream_write_cb: corked");
|
||||
}
|
||||
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 = audio_occupancy;
|
||||
}
|
||||
|
||||
// bytes we can transfer will never be greater than the bytes available
|
||||
|
||||
ret = pa_stream_begin_write(local_stream, (void **)&buffer, &bytes_we_can_transfer);
|
||||
if ((ret == 0) && (buffer != NULL) && (audio_lmb != 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;
|
||||
ret = pa_stream_write(local_stream, buffer, bytes_we_can_transfer, NULL, 0LL,
|
||||
PA_SEEK_RELATIVE);
|
||||
} 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(local_stream, buffer, bytes_we_can_transfer, NULL, 0LL,
|
||||
PA_SEEK_RELATIVE);
|
||||
audio_toq = audio_lmb + bytes_we_can_transfer - first_portion_to_write;
|
||||
}
|
||||
bytes_transferred += bytes_we_can_transfer;
|
||||
audio_occupancy -= bytes_we_can_transfer;
|
||||
bytes_to_transfer -= bytes_we_can_transfer;
|
||||
}
|
||||
}
|
||||
pthread_cleanup_pop(1); // release the mutex
|
||||
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(local_stream));
|
||||
}
|
||||
|
||||
void stream_success_cb(__attribute__((unused)) pa_stream *local_stream,
|
||||
__attribute__((unused)) int success,
|
||||
__attribute__((unused)) void *userdata) {
|
||||
return;
|
||||
}
|
||||
|
||||
audio_output audio_pa = {.name = "pulseaudio",
|
||||
.help = NULL,
|
||||
.init = &init,
|
||||
.deinit = &deinit,
|
||||
.prepare = NULL,
|
||||
.start = &start,
|
||||
.start = NULL,
|
||||
.configure = &configure,
|
||||
.get_configuration = &get_configuration,
|
||||
.stop = &stop,
|
||||
.is_running = NULL,
|
||||
.flush = &flush,
|
||||
@@ -362,71 +793,3 @@ audio_output audio_pa = {.name = "pa",
|
||||
.volume = NULL,
|
||||
.parameters = NULL,
|
||||
.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) {
|
||||
check_pa_stream_status(stream, "audio_pa stream_write_cb.");
|
||||
int bytes_to_transfer = requested_bytes;
|
||||
// int bytes_transferred = 0;
|
||||
uint8_t *buffer = NULL;
|
||||
int ret = 0;
|
||||
pthread_mutex_lock(&buffer_mutex);
|
||||
pthread_cleanup_push(mutex_unlock, (void *)&buffer_mutex);
|
||||
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);
|
||||
pa_stream_cork(stream, 1, stream_success_cb, mainloop);
|
||||
// debug(1, "Corked");
|
||||
bytes_we_can_transfer = audio_occupancy;
|
||||
}
|
||||
|
||||
// bytes we can transfer will never be greater than the bytes available
|
||||
|
||||
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 audio buffer
|
||||
memcpy(buffer, audio_toq, bytes_we_can_transfer);
|
||||
audio_toq += bytes_we_can_transfer;
|
||||
ret = pa_stream_write(stream, buffer, bytes_we_can_transfer, NULL, 0LL, PA_SEEK_RELATIVE);
|
||||
} 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);
|
||||
audio_toq = audio_lmb + bytes_we_can_transfer - first_portion_to_write;
|
||||
}
|
||||
// bytes_transferred += bytes_we_can_transfer;
|
||||
audio_occupancy -= bytes_we_can_transfer;
|
||||
bytes_to_transfer -= bytes_we_can_transfer;
|
||||
}
|
||||
}
|
||||
pthread_cleanup_pop(1); // release the mutex
|
||||
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 stream_success_cb(__attribute__((unused)) pa_stream *stream,
|
||||
__attribute__((unused)) int success,
|
||||
__attribute__((unused)) void *userdata) {
|
||||
return;
|
||||
}
|
||||
|
||||
+38
-9
@@ -4,7 +4,7 @@
|
||||
* All rights reserved.
|
||||
*
|
||||
* Modifications for audio synchronisation
|
||||
* and related work, copyright (c) Mike Brady 2014
|
||||
* and related work, copyright (c) Mike Brady 2014--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -40,6 +40,8 @@
|
||||
#include <unistd.h>
|
||||
|
||||
static int fd = -1;
|
||||
static int warned = 0;
|
||||
static unsigned int bytes_per_frame = 0;
|
||||
|
||||
char *pipename = NULL;
|
||||
char *default_pipe_name = "/tmp/shairport-sync-audio";
|
||||
@@ -77,11 +79,13 @@ static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
// if it's got a reader, write to it.
|
||||
if (fd > 0) {
|
||||
// int rc = non_blocking_write(fd, buf, samples * 4);
|
||||
int rc = write(fd, buf, samples * 4);
|
||||
if ((rc < 0) && (errno != EPIPE)) {
|
||||
if (bytes_per_frame == 0)
|
||||
debug(1, "pipe: bytes per frame not initialised before play()!");
|
||||
int rc = write(fd, buf, samples * bytes_per_frame);
|
||||
if ((rc < 0) && (errno != EPIPE) && (warned == 0)) {
|
||||
strerror_r(errno, (char *)errorstring, 1024);
|
||||
debug(1, "audio_pip play: error %d writing to the pipe named \"%s\": \"%s\".", errno,
|
||||
pipename, errorstring);
|
||||
debug(1, "error %d writing to the pipe named \"%s\": \"%s\".", errno, pipename, errorstring);
|
||||
warned = 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
@@ -102,9 +106,13 @@ static int init(int argc, char **argv) {
|
||||
config.audio_backend_buffer_desired_length = 1.0;
|
||||
config.audio_backend_latency_offset = 0;
|
||||
|
||||
// do the "general" audio options. Note, these options are in the "general" stanza!
|
||||
parse_general_audio_options();
|
||||
|
||||
// get settings from settings file, passing in defaults for format_set, rate_set and channel_set
|
||||
// Note, these options may be in the "general" stanza or the named stanza
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
parse_audio_options("pipe", (1 << SPS_FORMAT_S32_LE), (1 << SPS_RATE_48000), (1 << 2));
|
||||
#else
|
||||
parse_audio_options("pipe", (1 << SPS_FORMAT_S16_LE), (1 << SPS_RATE_44100), (1 << 2));
|
||||
#endif
|
||||
if (config.cfg != NULL) {
|
||||
/* Get the Output Pipename. */
|
||||
const char *str;
|
||||
@@ -145,11 +153,32 @@ static void help(void) {
|
||||
printf(" Provide the pipe's pathname. The default is \"%s\".\n", default_pipe_name);
|
||||
}
|
||||
|
||||
static int32_t get_configuration(unsigned int channels, unsigned int rate, unsigned int format) {
|
||||
// use the standard format/rate/channel search to get a suitable configuration. No
|
||||
// check_configuration() method needs to be passed to search_for_suitable_configuration() because
|
||||
// it will always return a valid choice based on any settings and the defaults
|
||||
return search_for_suitable_configuration(channels, rate, format, NULL);
|
||||
}
|
||||
|
||||
static int configure(int32_t requested_encoded_format, __attribute__((unused)) char **channel_map) {
|
||||
int response = 0;
|
||||
unsigned int bytes_per_sample =
|
||||
sps_format_sample_size(FORMAT_FROM_ENCODED_FORMAT(requested_encoded_format));
|
||||
if (bytes_per_sample == 0) {
|
||||
debug(1, "pipe: unknown output format.");
|
||||
bytes_per_sample = 4; // emergency hack
|
||||
response = EINVAL;
|
||||
}
|
||||
bytes_per_frame = bytes_per_sample * CHANNELS_FROM_ENCODED_FORMAT(requested_encoded_format);
|
||||
return response;
|
||||
}
|
||||
|
||||
audio_output audio_pipe = {.name = "pipe",
|
||||
.help = &help,
|
||||
.init = &init,
|
||||
.deinit = &deinit,
|
||||
.prepare = NULL,
|
||||
.get_configuration = &get_configuration,
|
||||
.configure = &configure,
|
||||
.start = &start,
|
||||
.stop = &stop,
|
||||
.is_running = NULL,
|
||||
|
||||
+360
-169
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Asynchronous PipeWire Backend. This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2023
|
||||
* Copyright (c) Mike Brady 2024--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -39,65 +39,81 @@
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <spa/param/audio/format-utils.h>
|
||||
|
||||
// note -- these are hardwired into this code.
|
||||
#define DEFAULT_FORMAT SPA_AUDIO_FORMAT_S16_LE
|
||||
#define DEFAULT_BYTES_PER_SAMPLE 2
|
||||
static char channel_map_mono[] = "FC";
|
||||
static char channel_map_stereo[] = "FL FR";
|
||||
static char channel_map_2p1[] = "FL FR LFE";
|
||||
static char channel_map_4p0[] = "FL FR FC BC";
|
||||
static char channel_map_5p0[] = "FL FR FC BL BR";
|
||||
static char channel_map_5p1[] = "FL FR FC LFE BL BR";
|
||||
static char channel_map_6p1[] = "FL FR FC LFE BC SL SR";
|
||||
static char channel_map_7p1[] = "FL FR FC LFE BL BR SL SR";
|
||||
|
||||
#define DEFAULT_RATE 44100
|
||||
#define DEFAULT_CHANNELS 2
|
||||
#define DEFAULT_BUFFER_SIZE_IN_SECONDS 4
|
||||
typedef struct {
|
||||
enum spa_audio_format spa_format;
|
||||
sps_format_t sps_format;
|
||||
unsigned int bytes_per_sample;
|
||||
} spa_sps_t;
|
||||
|
||||
// Four seconds buffer -- should be plenty
|
||||
#define buffer_allocation DEFAULT_RATE * DEFAULT_BUFFER_SIZE_IN_SECONDS * DEFAULT_BYTES_PER_SAMPLE * DEFAULT_CHANNELS
|
||||
// these are the only formats that audio_pw will ever allow itself to be configured with
|
||||
static spa_sps_t format_lookup[] = {{SPA_AUDIO_FORMAT_S16_LE, SPS_FORMAT_S16_LE, 2},
|
||||
{SPA_AUDIO_FORMAT_S16_BE, SPS_FORMAT_S16_BE, 2},
|
||||
{SPA_AUDIO_FORMAT_S32_LE, SPS_FORMAT_S32_LE, 4},
|
||||
{SPA_AUDIO_FORMAT_S32_BE, SPS_FORMAT_S32_BE, 4}};
|
||||
|
||||
#define BUFFER_SIZE_IN_SECONDS 1
|
||||
|
||||
static uint8_t buffer[1024];
|
||||
static pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static int32_t current_encoded_output_format = 0;
|
||||
static char *audio_lmb, *audio_umb, *audio_toq, *audio_eoq;
|
||||
static size_t audio_size = buffer_allocation;
|
||||
static size_t audio_size = 0;
|
||||
static size_t audio_occupancy;
|
||||
static int enable_fill;
|
||||
static int stream_is_active;
|
||||
|
||||
struct timing_data {
|
||||
int pw_time_is_valid; // set when the pw_time has been set
|
||||
struct pw_time time_info; // information about the last time a process callback occurred
|
||||
size_t frames; // the number of frames sent at that time
|
||||
};
|
||||
|
||||
// to avoid using a mutex, write the same data twice and check they are the same
|
||||
// to ensure they are consistent. Make sure the first is written strictly before the second
|
||||
// using __sync_synchronize();
|
||||
struct timing_data timing_data_1, timing_data_2;
|
||||
static int on_process_is_running = 0;
|
||||
|
||||
struct data {
|
||||
struct pw_thread_loop *loop;
|
||||
struct pw_stream *stream;
|
||||
unsigned int rate;
|
||||
unsigned int bytes_per_sample;
|
||||
unsigned int channels;
|
||||
};
|
||||
|
||||
// the pipewire global data structure
|
||||
struct data data = {NULL, NULL};
|
||||
struct data data = {NULL, NULL, 0, 0, 0};
|
||||
|
||||
/*
|
||||
static void on_state_changed(__attribute__((unused)) void *userdata, enum pw_stream_state old,
|
||||
enum pw_stream_state state,
|
||||
__attribute__((unused)) const char *error) {
|
||||
// struct pw_data *pw = userdata;
|
||||
debug(3, "pw: stream state changed %s -> %s", pw_stream_state_as_string(old),
|
||||
pw_stream_state_as_string(state));
|
||||
// use an SPS_FORMAT_... to find an entry in the format_lookup table or return NULL
|
||||
static spa_sps_t *sps_format_lookup(sps_format_t to_find) {
|
||||
spa_sps_t *response = NULL;
|
||||
unsigned int i = 0;
|
||||
while ((response == NULL) && (i < sizeof(format_lookup) / sizeof(spa_sps_t))) {
|
||||
if (format_lookup[i].sps_format == to_find)
|
||||
response = &format_lookup[i];
|
||||
else
|
||||
i++;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
*/
|
||||
|
||||
static void on_process(void *userdata) {
|
||||
|
||||
struct data *data = userdata;
|
||||
struct data *local_data = userdata;
|
||||
int n_frames = 0;
|
||||
|
||||
pthread_mutex_lock(&buffer_mutex);
|
||||
|
||||
// debug(1, "on_process called.");
|
||||
|
||||
if (stream_is_active == 0)
|
||||
debug(1, "on_process called while stream inactive!");
|
||||
|
||||
on_process_is_running = 1;
|
||||
if ((audio_occupancy > 0) || (enable_fill)) {
|
||||
|
||||
// get a buffer to see how big it can be
|
||||
struct pw_buffer *b = pw_stream_dequeue_buffer(data->stream);
|
||||
struct pw_buffer *b = pw_stream_dequeue_buffer(local_data->stream);
|
||||
if (b == NULL) {
|
||||
pw_log_warn("out of buffers: %m");
|
||||
die("PipeWire failure -- out of buffers!");
|
||||
@@ -105,8 +121,8 @@ static void on_process(void *userdata) {
|
||||
struct spa_buffer *buf = b->buffer;
|
||||
uint8_t *dest = buf->datas[0].data;
|
||||
if (dest != NULL) {
|
||||
int stride = DEFAULT_BYTES_PER_SAMPLE * DEFAULT_CHANNELS;
|
||||
|
||||
int stride = local_data->bytes_per_sample * local_data->channels;
|
||||
|
||||
// note: the requested field is the number of frames, not bytes, requested
|
||||
int max_possible_frames = SPA_MIN(b->requested, buf->datas[0].maxsize / stride);
|
||||
|
||||
@@ -145,89 +161,76 @@ static void on_process(void *userdata) {
|
||||
memset(dest, 0, bytes_we_can_transfer);
|
||||
n_frames = max_possible_frames;
|
||||
}
|
||||
|
||||
buf->datas[0].chunk->offset = 0;
|
||||
buf->datas[0].chunk->stride = stride;
|
||||
buf->datas[0].chunk->size = n_frames * stride;
|
||||
pw_stream_queue_buffer(data->stream, b);
|
||||
debug(3, "Queueing %d frames for output.", n_frames);
|
||||
pw_stream_queue_buffer(local_data->stream, b);
|
||||
|
||||
} // (else the first data block does not contain a data pointer)
|
||||
}
|
||||
pthread_mutex_unlock(&buffer_mutex);
|
||||
|
||||
timing_data_1.frames = n_frames;
|
||||
if (pw_stream_get_time_n(data->stream, &timing_data_1.time_info, sizeof(struct timing_data)) == 0)
|
||||
timing_data_1.pw_time_is_valid = 1;
|
||||
else
|
||||
timing_data_1.pw_time_is_valid = 0;
|
||||
__sync_synchronize();
|
||||
memcpy((char *)&timing_data_2, (char *)&timing_data_1, sizeof(struct timing_data));
|
||||
__sync_synchronize();
|
||||
}
|
||||
|
||||
static const struct pw_stream_events stream_events = {PW_VERSION_STREAM_EVENTS,
|
||||
.process = on_process};
|
||||
// PW_VERSION_STREAM_EVENTS, .process = on_process, .state_changed = on_state_changed};
|
||||
|
||||
static void deinit(void) {
|
||||
pw_thread_loop_stop(data.loop);
|
||||
pw_stream_destroy(data.stream);
|
||||
if (data.stream != NULL)
|
||||
pw_stream_destroy(data.stream);
|
||||
pw_thread_loop_destroy(data.loop);
|
||||
pw_deinit();
|
||||
free(audio_lmb); // deallocate that buffer
|
||||
on_process_is_running = 0;
|
||||
if (audio_lmb != NULL)
|
||||
free(audio_lmb); // deallocate that buffer
|
||||
}
|
||||
|
||||
static int init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) {
|
||||
// set up default values first
|
||||
memset(&timing_data_1, 0, sizeof(struct timing_data));
|
||||
memset(&timing_data_2, 0, sizeof(struct timing_data));
|
||||
config.audio_backend_buffer_desired_length = 0.35;
|
||||
config.audio_backend_buffer_desired_length = 0.5;
|
||||
config.audio_backend_buffer_interpolation_threshold_in_seconds =
|
||||
0.02; // below this, soxr interpolation will not occur -- it'll be basic interpolation
|
||||
// instead.
|
||||
|
||||
config.audio_backend_latency_offset = 0;
|
||||
|
||||
// get settings from settings file
|
||||
// do the "general" audio options. Note, these options are in the "general" stanza!
|
||||
parse_general_audio_options();
|
||||
|
||||
// now any PipeWire-specific options
|
||||
if (config.cfg != NULL) {
|
||||
const char *str;
|
||||
// get settings from settings file, passing in defaults for format_set, rate_set and channel_set
|
||||
// Note, these options may be in the "general" stanza or the named stanza
|
||||
#ifdef CONFIG_FFMPEG
|
||||
parse_audio_options("pipewire", SPS_FORMAT_SET, SPS_RATE_SET, SPS_CHANNEL_SET);
|
||||
#else
|
||||
parse_audio_options("pipewire", SPS_FORMAT_NON_FFMPEG_SET, SPS_RATE_NON_FFMPEG_SET,
|
||||
SPS_CHANNNEL_NON_FFMPEG_SET);
|
||||
#endif
|
||||
|
||||
// Get the optional Application Name, if provided.
|
||||
if (config_lookup_string(config.cfg, "pw.application_name", &str)) {
|
||||
config.pw_application_name = (char *)str;
|
||||
}
|
||||
// now any PipeWire-specific options
|
||||
if (config.cfg != NULL) {
|
||||
const char *str;
|
||||
|
||||
// Get the optional PipeWire node name, if provided.
|
||||
if (config_lookup_string(config.cfg, "pw.node_name", &str)) {
|
||||
config.pw_node_name = (char *)str;
|
||||
}
|
||||
|
||||
// Get the optional PipeWire sink target name, if provided.
|
||||
if (config_lookup_string(config.cfg, "pw.sink_target", &str)) {
|
||||
config.pw_sink_target = (char *)str;
|
||||
}
|
||||
// Get the optional Application Name, if provided.
|
||||
if (config_lookup_string(config.cfg, "pipewire.application_name", &str)) {
|
||||
config.pw_application_name = (char *)str;
|
||||
}
|
||||
|
||||
|
||||
// Get the optional PipeWire node name, if provided.
|
||||
if (config_lookup_string(config.cfg, "pipewire.node_name", &str)) {
|
||||
config.pw_node_name = (char *)str;
|
||||
}
|
||||
|
||||
// Get the optional PipeWire sink target name, if provided.
|
||||
if (config_lookup_string(config.cfg, "pipewire.sink_target", &str)) {
|
||||
config.pw_sink_target = (char *)str;
|
||||
}
|
||||
}
|
||||
|
||||
// finished collecting settings
|
||||
|
||||
// allocate space for the audio buffer
|
||||
audio_lmb = malloc(audio_size);
|
||||
if (audio_lmb == NULL)
|
||||
die("Can't allocate %d bytes for PipeWire buffer.", audio_size);
|
||||
audio_toq = audio_eoq = audio_lmb;
|
||||
audio_umb = audio_lmb + audio_size;
|
||||
audio_occupancy = 0;
|
||||
// debug(1, "init enable_fill");
|
||||
audio_lmb = NULL;
|
||||
audio_size = 0;
|
||||
current_encoded_output_format = 0;
|
||||
enable_fill = 1;
|
||||
|
||||
const struct spa_pod *params[1];
|
||||
uint8_t buffer[1024];
|
||||
struct pw_properties *props;
|
||||
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
||||
|
||||
int largc = 0;
|
||||
pw_init(&largc, NULL);
|
||||
|
||||
@@ -236,20 +239,18 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char *
|
||||
|
||||
pw_thread_loop_lock(data.loop);
|
||||
|
||||
pw_thread_loop_start(data.loop);
|
||||
|
||||
char* appname = config.pw_application_name;
|
||||
char *appname = config.pw_application_name;
|
||||
if (appname == NULL)
|
||||
appname = "Shairport Sync";
|
||||
|
||||
char* nodename = config.pw_node_name;
|
||||
|
||||
char *nodename = config.pw_node_name;
|
||||
if (nodename == NULL)
|
||||
nodename = "Shairport Sync";
|
||||
|
||||
props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, "Playback",
|
||||
PW_KEY_MEDIA_ROLE, "Music", PW_KEY_APP_NAME, appname,
|
||||
PW_KEY_NODE_NAME, nodename, NULL);
|
||||
|
||||
struct pw_properties *props = pw_properties_new(
|
||||
PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, "Playback", PW_KEY_MEDIA_ROLE, "Music",
|
||||
PW_KEY_APP_NAME, appname, PW_KEY_NODE_NAME, nodename, NULL);
|
||||
|
||||
if (config.pw_sink_target != NULL) {
|
||||
debug(3, "setting sink target to \"%s\".", config.pw_sink_target);
|
||||
pw_properties_set(props, PW_KEY_TARGET_OBJECT, config.pw_sink_target);
|
||||
@@ -257,38 +258,208 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char *
|
||||
|
||||
data.stream = pw_stream_new_simple(pw_thread_loop_get_loop(data.loop), config.appName, props,
|
||||
&stream_events, &data);
|
||||
pw_thread_loop_start(data.loop);
|
||||
|
||||
// Make one parameter with the supported formats. The SPA_PARAM_EnumFormat
|
||||
// id means that this is a format enumeration (of 1 value).
|
||||
params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
|
||||
&SPA_AUDIO_INFO_RAW_INIT(.format = DEFAULT_FORMAT,
|
||||
.channels = DEFAULT_CHANNELS,
|
||||
.rate = DEFAULT_RATE));
|
||||
on_process_is_running = 0;
|
||||
|
||||
// Now connect this stream. We ask that our process function is
|
||||
// called in a realtime thread.
|
||||
pw_stream_connect(data.stream, PW_DIRECTION_OUTPUT, PW_ID_ANY,
|
||||
PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS |
|
||||
PW_STREAM_FLAG_RT_PROCESS | PW_STREAM_FLAG_INACTIVE,
|
||||
params, 1);
|
||||
stream_is_active = 0;
|
||||
pw_thread_loop_unlock(data.loop);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void start(__attribute__((unused)) int sample_rate,
|
||||
__attribute__((unused)) int sample_format) {
|
||||
static int check_settings(sps_format_t sample_format, unsigned int sample_rate,
|
||||
unsigned int channel_count) {
|
||||
// we know the formats with be big- or little-ended.
|
||||
// we will accept only S32_..., S16_...
|
||||
|
||||
int response = EINVAL;
|
||||
|
||||
if (sps_format_lookup(sample_format) != NULL)
|
||||
response = 0;
|
||||
|
||||
debug(3, "pw: configuration: %u/%s/%u %s.", sample_rate,
|
||||
sps_format_description_string(sample_format), channel_count,
|
||||
response == 0 ? "is okay" : "can not be configured");
|
||||
return response;
|
||||
}
|
||||
|
||||
static void prepare_to_play() {
|
||||
// debug(1, "prepare to play");
|
||||
if (stream_is_active == 0) {
|
||||
static int check_configuration(unsigned int channels, unsigned int rate, unsigned int format) {
|
||||
return check_settings(format, rate, channels);
|
||||
}
|
||||
|
||||
static int32_t get_configuration(unsigned int channels, unsigned int rate, unsigned int format) {
|
||||
return search_for_suitable_configuration(channels, rate, format, &check_configuration);
|
||||
}
|
||||
|
||||
static int configure(int32_t requested_encoded_format, char **resulting_channel_map) {
|
||||
// debug(2, "pw: configure %s.", short_format_description(requested_encoded_format));
|
||||
int response = 0;
|
||||
char *channel_map = NULL;
|
||||
// if (1) {
|
||||
if (current_encoded_output_format != requested_encoded_format) {
|
||||
uint64_t start_time = get_absolute_time_in_ns();
|
||||
if (current_encoded_output_format == 0)
|
||||
debug(2, "pw: setting output configuration to %s.",
|
||||
short_format_description(requested_encoded_format));
|
||||
else
|
||||
// note -- can't use short_format_description twice in one call because it returns the same
|
||||
// string buffer each time
|
||||
debug(2, "pw: changing output configuration to %s.",
|
||||
short_format_description(requested_encoded_format));
|
||||
current_encoded_output_format = requested_encoded_format;
|
||||
spa_sps_t *format_info =
|
||||
sps_format_lookup(FORMAT_FROM_ENCODED_FORMAT(current_encoded_output_format));
|
||||
|
||||
if (format_info == NULL)
|
||||
die("Can't find format information!");
|
||||
// enum spa_audio_format spa_format = format_info->spa_format;
|
||||
data.bytes_per_sample = format_info->bytes_per_sample;
|
||||
data.channels = CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format);
|
||||
data.rate = RATE_FROM_ENCODED_FORMAT(current_encoded_output_format);
|
||||
|
||||
pw_thread_loop_lock(data.loop);
|
||||
pw_stream_set_active(data.stream, true);
|
||||
enable_fill = 0;
|
||||
|
||||
if (pw_stream_get_state(data.stream, NULL) != PW_STREAM_STATE_UNCONNECTED) {
|
||||
response = pw_stream_disconnect(data.stream);
|
||||
if (response != 0) {
|
||||
debug(1, "error %d disconnecting stream.", response);
|
||||
}
|
||||
}
|
||||
|
||||
if (audio_lmb != NULL) {
|
||||
// debug(3, "deallocating existing audio_pw.c buffer.");
|
||||
free(audio_lmb);
|
||||
}
|
||||
|
||||
audio_size = data.rate * BUFFER_SIZE_IN_SECONDS * data.bytes_per_sample * data.channels;
|
||||
// allocate space for the audio buffer
|
||||
audio_lmb = malloc(audio_size);
|
||||
if (audio_lmb == NULL)
|
||||
die("Can't allocate %d bytes for PipeWire buffer.", audio_size);
|
||||
audio_toq = audio_eoq = audio_lmb;
|
||||
audio_umb = audio_lmb + audio_size;
|
||||
audio_occupancy = 0;
|
||||
|
||||
// Make one parameter with the supported formats. The SPA_PARAM_EnumFormat
|
||||
// id means that this is a format enumeration (of 1 value).
|
||||
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
||||
|
||||
const struct spa_pod *params[1];
|
||||
// create a stream with the default channel layout corresponding to
|
||||
// the number of channels
|
||||
switch (CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format)) {
|
||||
case 1:
|
||||
channel_map = channel_map_mono;
|
||||
params[0] = spa_format_audio_raw_build(
|
||||
&b, SPA_PARAM_EnumFormat,
|
||||
// we are giving the position of 8 channels here, even if we need less than that.
|
||||
&SPA_AUDIO_INFO_RAW_INIT(.format = format_info->spa_format, .channels = data.channels,
|
||||
.rate = data.rate, .position = {SPA_AUDIO_CHANNEL_FC}));
|
||||
break;
|
||||
case 2:
|
||||
channel_map = channel_map_stereo;
|
||||
params[0] = spa_format_audio_raw_build(
|
||||
&b, SPA_PARAM_EnumFormat,
|
||||
// we are giving the position of 8 channels here, even if we need less than that.
|
||||
&SPA_AUDIO_INFO_RAW_INIT(.format = format_info->spa_format, .channels = data.channels,
|
||||
.rate = data.rate,
|
||||
.position = {SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR}));
|
||||
break;
|
||||
case 3:
|
||||
channel_map = channel_map_2p1;
|
||||
params[0] = spa_format_audio_raw_build(
|
||||
&b, SPA_PARAM_EnumFormat,
|
||||
// we are giving the position of 8 channels here, even if we need less than that.
|
||||
&SPA_AUDIO_INFO_RAW_INIT(.format = format_info->spa_format, .channels = data.channels,
|
||||
.rate = data.rate,
|
||||
.position = {SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR,
|
||||
SPA_AUDIO_CHANNEL_LFE}));
|
||||
break;
|
||||
case 4:
|
||||
channel_map = channel_map_4p0;
|
||||
params[0] = spa_format_audio_raw_build(
|
||||
&b, SPA_PARAM_EnumFormat,
|
||||
// we are giving the position of 8 channels here, even if we need less than that.
|
||||
&SPA_AUDIO_INFO_RAW_INIT(.format = format_info->spa_format, .channels = data.channels,
|
||||
.rate = data.rate,
|
||||
.position = {SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR,
|
||||
SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_BC}));
|
||||
break;
|
||||
case 5:
|
||||
channel_map = channel_map_5p0;
|
||||
params[0] = spa_format_audio_raw_build(
|
||||
&b, SPA_PARAM_EnumFormat,
|
||||
// we are giving the position of 8 channels here, even if we need less than that.
|
||||
&SPA_AUDIO_INFO_RAW_INIT(.format = format_info->spa_format, .channels = data.channels,
|
||||
.rate = data.rate,
|
||||
.position = {SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR,
|
||||
SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_RL,
|
||||
SPA_AUDIO_CHANNEL_RR}));
|
||||
break;
|
||||
case 6:
|
||||
channel_map = channel_map_5p1;
|
||||
params[0] = spa_format_audio_raw_build(
|
||||
&b, SPA_PARAM_EnumFormat,
|
||||
&SPA_AUDIO_INFO_RAW_INIT(.format = format_info->spa_format, .channels = data.channels,
|
||||
.rate = data.rate,
|
||||
.position = {SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR,
|
||||
SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
|
||||
SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR}));
|
||||
break;
|
||||
case 7:
|
||||
channel_map = channel_map_6p1;
|
||||
params[0] = spa_format_audio_raw_build(
|
||||
&b, SPA_PARAM_EnumFormat,
|
||||
&SPA_AUDIO_INFO_RAW_INIT(.format = format_info->spa_format, .channels = data.channels,
|
||||
.rate = data.rate,
|
||||
.position = {SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR,
|
||||
SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
|
||||
SPA_AUDIO_CHANNEL_BC, SPA_AUDIO_CHANNEL_SL,
|
||||
SPA_AUDIO_CHANNEL_SR}));
|
||||
break;
|
||||
case 8:
|
||||
channel_map = channel_map_7p1;
|
||||
params[0] = spa_format_audio_raw_build(
|
||||
&b, SPA_PARAM_EnumFormat,
|
||||
&SPA_AUDIO_INFO_RAW_INIT(.format = format_info->spa_format, .channels = data.channels,
|
||||
.rate = data.rate,
|
||||
.position = {SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR,
|
||||
SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
|
||||
SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR,
|
||||
SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR}));
|
||||
break;
|
||||
default:
|
||||
channel_map = NULL;
|
||||
params[0] = spa_format_audio_raw_build(
|
||||
&b, SPA_PARAM_EnumFormat,
|
||||
// we are giving the position of 8 channels here, even if we need less than that.
|
||||
&SPA_AUDIO_INFO_RAW_INIT(.format = format_info->spa_format, .channels = data.channels,
|
||||
.rate = data.rate,
|
||||
.position = {SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR,
|
||||
SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
|
||||
SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR,
|
||||
SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR}));
|
||||
break;
|
||||
}
|
||||
|
||||
// Now connect this stream. We ask that our process function is
|
||||
// called in a realtime thread.
|
||||
pw_stream_connect(data.stream, PW_DIRECTION_OUTPUT, PW_ID_ANY,
|
||||
PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS |
|
||||
PW_STREAM_FLAG_RT_PROCESS,
|
||||
params, 1);
|
||||
stream_is_active = 0;
|
||||
enable_fill = 1;
|
||||
pw_thread_loop_unlock(data.loop);
|
||||
stream_is_active = 1;
|
||||
debug(3, "prepare to play activating stream");
|
||||
int64_t elapsed_time = get_absolute_time_in_ns() - start_time;
|
||||
debug(3, "pw: configuration took %0.3f mS.", elapsed_time * 0.000001);
|
||||
} else {
|
||||
debug(2, "pw: setting output configuration -- configuration unchanged, so nothing done.");
|
||||
}
|
||||
if ((response == 0) && (resulting_channel_map != NULL)) {
|
||||
*resulting_channel_map = channel_map;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
static int play(__attribute__((unused)) void *buf, int samples,
|
||||
@@ -296,14 +467,16 @@ static int play(__attribute__((unused)) void *buf, int samples,
|
||||
__attribute__((unused)) uint64_t playtime) {
|
||||
if (stream_is_active == 0) {
|
||||
pw_thread_loop_lock(data.loop);
|
||||
on_process_is_running = 0;
|
||||
pw_stream_set_active(data.stream, true);
|
||||
pw_thread_loop_unlock(data.loop);
|
||||
stream_is_active = 1;
|
||||
debug(3, "set stream active");
|
||||
// debug(1, "set stream active");
|
||||
}
|
||||
// copy the samples into the queue
|
||||
debug(3, "play %u samples; %u bytes already in the buffer.", samples, audio_occupancy);
|
||||
size_t bytes_to_transfer = samples * DEFAULT_CHANNELS * DEFAULT_BYTES_PER_SAMPLE;
|
||||
// debug(3, "play %u samples; %u samples already in the buffer.", samples, audio_occupancy /
|
||||
// (data.bytes_per_sample * data.channels));
|
||||
size_t bytes_to_transfer = samples * data.channels * data.bytes_per_sample;
|
||||
pthread_mutex_lock(&buffer_mutex);
|
||||
size_t bytes_available = audio_size - audio_occupancy;
|
||||
if (bytes_available < bytes_to_transfer)
|
||||
@@ -325,58 +498,76 @@ static int play(__attribute__((unused)) void *buf, int samples,
|
||||
return 0;
|
||||
}
|
||||
|
||||
int delay(long *the_delay) {
|
||||
static int delay(long *the_delay) {
|
||||
long result = 0;
|
||||
int reply = 0;
|
||||
// find out what's already in the PipeWire system and when
|
||||
struct timing_data timing_data;
|
||||
int loop_count = 1;
|
||||
do {
|
||||
memcpy(&timing_data, (char *)&timing_data_1, sizeof(struct timing_data));
|
||||
__sync_synchronize();
|
||||
if (memcmp(&timing_data, (char *)&timing_data_2, sizeof(struct timing_data)) != 0) {
|
||||
usleep(2); // microseconds
|
||||
loop_count++;
|
||||
__sync_synchronize();
|
||||
}
|
||||
} while ((memcmp(&timing_data, (char *)&timing_data_2, sizeof(struct timing_data)) != 0) &&
|
||||
(loop_count < 10));
|
||||
long total_delay_now_frames_long = 0;
|
||||
if ((loop_count < 10) && (timing_data.pw_time_is_valid != 0)) {
|
||||
struct timespec time_now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &time_now);
|
||||
int64_t interval_from_process_time_to_now =
|
||||
SPA_TIMESPEC_TO_NSEC(&time_now) - timing_data.time_info.now;
|
||||
int64_t delay_in_ns = timing_data.time_info.delay + timing_data.time_info.buffered;
|
||||
delay_in_ns = delay_in_ns * 1000000000;
|
||||
delay_in_ns = delay_in_ns * timing_data.time_info.rate.num;
|
||||
delay_in_ns = delay_in_ns / timing_data.time_info.rate.denom;
|
||||
int reply = -ENODEV; // ENODATA is not defined in FreeBSD
|
||||
|
||||
int64_t total_delay_now_ns = delay_in_ns - interval_from_process_time_to_now;
|
||||
int64_t total_delay_now_frames = (total_delay_now_ns * DEFAULT_RATE) / 1000000000 + timing_data.frames;
|
||||
total_delay_now_frames_long = total_delay_now_frames;
|
||||
debug(3, "total delay in frames: %ld.", total_delay_now_frames_long);
|
||||
if (on_process_is_running == 0) {
|
||||
debug(3, "pw_processor not running");
|
||||
}
|
||||
|
||||
if (timing_data.time_info.queued != 0) {
|
||||
debug(1, "buffers queued: %d", timing_data.time_info.queued);
|
||||
if (stream_is_active == 0) {
|
||||
debug(1, "stream not active but on_process_is_running is 1!");
|
||||
}
|
||||
if (on_process_is_running != 0) {
|
||||
|
||||
struct pw_time stream_time_info_1, stream_time_info_2;
|
||||
ssize_t audio_occupancy_now;
|
||||
|
||||
// get stable pw_time info to ensure we get an audio occupancy figure
|
||||
// that relates to the pw_time we have.
|
||||
// we do this by getting a pw_time before and after getting the occupancy
|
||||
// and accepting the information if they are both the same
|
||||
|
||||
int loop_count = 1;
|
||||
int non_matching;
|
||||
int stream_time_valid_if_zero;
|
||||
do {
|
||||
stream_time_valid_if_zero =
|
||||
pw_stream_get_time_n(data.stream, &stream_time_info_1, sizeof(struct pw_time));
|
||||
audio_occupancy_now = audio_occupancy;
|
||||
pw_stream_get_time_n(data.stream, &stream_time_info_2, sizeof(struct pw_time));
|
||||
|
||||
non_matching = memcmp(&stream_time_info_1, &stream_time_info_2, sizeof(struct pw_time));
|
||||
if (non_matching != 0) {
|
||||
loop_count++;
|
||||
}
|
||||
} while (((non_matching != 0) || (stream_time_valid_if_zero != 0)) && (loop_count < 10));
|
||||
|
||||
if (non_matching != 0) {
|
||||
debug(1, "can't get a stable pw_time!");
|
||||
}
|
||||
if (stream_time_valid_if_zero != 0) {
|
||||
debug(1, "can't get valid stream info");
|
||||
}
|
||||
if (stream_time_info_1.rate.denom == 0) {
|
||||
debug(2, "non valid stream_time_info_1");
|
||||
}
|
||||
|
||||
if ((non_matching == 0) && (stream_time_valid_if_zero == 0) &&
|
||||
(stream_time_info_1.rate.denom != 0)) {
|
||||
int64_t interval_from_pw_time_to_now_ns =
|
||||
pw_stream_get_nsec(data.stream) - stream_time_info_1.now;
|
||||
|
||||
uint64_t frames_possibly_played_since_measurement =
|
||||
((interval_from_pw_time_to_now_ns * data.rate) + 500000000L) / 1000000000L;
|
||||
|
||||
uint64_t net_delay_in_frames = stream_time_info_1.queued + stream_time_info_1.buffered;
|
||||
|
||||
uint64_t fixed_delay_ns =
|
||||
(stream_time_info_1.delay * stream_time_info_1.rate.num * 1000000000L) /
|
||||
stream_time_info_1.rate.denom; // ns;
|
||||
uint64_t fixed_delay_in_frames = ((fixed_delay_ns * data.rate) + 500000000L) / 1000000000L;
|
||||
|
||||
net_delay_in_frames = net_delay_in_frames + fixed_delay_in_frames +
|
||||
audio_occupancy_now / (data.bytes_per_sample * data.channels) -
|
||||
frames_possibly_played_since_measurement;
|
||||
|
||||
result = net_delay_in_frames;
|
||||
reply = 0;
|
||||
}
|
||||
/*
|
||||
debug(3,
|
||||
"interval_from_process_time_to_now: %" PRId64 " ns, "
|
||||
"delay_in_ns: %" PRId64 ", queued: %" PRId64 ", buffered: %" PRId64 ".",
|
||||
// delay_timing_data.time_info.rate.num, delay_timing_data.time_info.rate.denom,
|
||||
interval_from_process_time_to_now, delay_in_ns,
|
||||
timing_data.time_info.queued, timing_data.time_info.buffered);
|
||||
*/
|
||||
|
||||
} else {
|
||||
warn("Shairport Sync's PipeWire backend can not get timing information from the PipeWire "
|
||||
"system. Is PipeWire running?");
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&buffer_mutex);
|
||||
result = total_delay_now_frames_long + audio_occupancy / (DEFAULT_BYTES_PER_SAMPLE * DEFAULT_CHANNELS);
|
||||
pthread_mutex_unlock(&buffer_mutex);
|
||||
*the_delay = result;
|
||||
return reply;
|
||||
}
|
||||
@@ -408,23 +599,23 @@ static void stop(void) {
|
||||
pw_stream_set_active(data.stream, false);
|
||||
pw_thread_loop_unlock(data.loop);
|
||||
stream_is_active = 0;
|
||||
debug(3, "set stream inactive");
|
||||
// debug(1, "set stream inactive");
|
||||
}
|
||||
}
|
||||
|
||||
audio_output audio_pw = {.name = "pw",
|
||||
audio_output audio_pw = {.name = "pipewire",
|
||||
.help = NULL,
|
||||
.init = &init,
|
||||
.deinit = &deinit,
|
||||
.prepare = NULL,
|
||||
.start = &start,
|
||||
.start = NULL,
|
||||
.get_configuration = &get_configuration,
|
||||
.configure = &configure,
|
||||
.stop = &stop,
|
||||
.is_running = NULL,
|
||||
.flush = &flush,
|
||||
.delay = &delay,
|
||||
.stats = NULL,
|
||||
.play = &play,
|
||||
.prepare_to_play = &prepare_to_play,
|
||||
.volume = NULL,
|
||||
.parameters = NULL,
|
||||
.mute = NULL};
|
||||
|
||||
+366
-124
@@ -2,9 +2,7 @@
|
||||
* sndio output driver. This file is part of Shairport Sync.
|
||||
* Copyright (c) 2013 Dimitri Sokolyuk <demon@dim13.org>
|
||||
* Copyright (c) 2017 Tobias Kortkamp <t@tobik.me>
|
||||
*
|
||||
* Modifications for audio synchronisation
|
||||
* and related work, copyright (c) Mike Brady 2014 -- 2024
|
||||
* Copyright (c) 2014--2025 Mike Brady
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
@@ -22,15 +20,31 @@
|
||||
|
||||
#include "audio.h"
|
||||
#include "common.h"
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <sndio.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// see https://sndio.org/tips.html#section_2_1, section 11 Glossary
|
||||
// don't have any information for 7 and 8 channels
|
||||
static char channel_map_1[] = "FL";
|
||||
static char channel_map_2[] = "FL FR";
|
||||
static char channel_map_3[] = "FL FR BL";
|
||||
static char channel_map_4[] = "FL FR BL BR";
|
||||
static char channel_map_5[] = "FL FR BL BR FC";
|
||||
static char channel_map_6[] = "FL FR BL BR FC LFE";
|
||||
|
||||
static pthread_mutex_t sndio_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static int current_encoded_output_format;
|
||||
static struct sio_hdl *hdl;
|
||||
static const char *output_device_name;
|
||||
static unsigned int output_device_driver_bufsiz; // parameters for opening the output device
|
||||
static unsigned int output_device_driver_round; // parameters for opening the output device
|
||||
static int is_running;
|
||||
|
||||
static int framesize;
|
||||
static size_t played;
|
||||
static size_t written;
|
||||
@@ -40,110 +54,376 @@ int at_least_one_onmove_cb_seen;
|
||||
struct sio_par par;
|
||||
|
||||
struct sndio_formats {
|
||||
const char *name;
|
||||
sps_format_t fmt;
|
||||
unsigned int rate;
|
||||
unsigned int bits;
|
||||
unsigned int bps;
|
||||
unsigned int sig;
|
||||
unsigned int le;
|
||||
sps_format_t sps_format;
|
||||
unsigned int bits; // bits per sample
|
||||
unsigned int sig; // signed = 1,
|
||||
unsigned int le; // is little endian
|
||||
};
|
||||
|
||||
static struct sndio_formats formats[] = {{"S8", SPS_FORMAT_S8, 44100, 8, 1, 1, SIO_LE_NATIVE},
|
||||
{"U8", SPS_FORMAT_U8, 44100, 8, 1, 0, SIO_LE_NATIVE},
|
||||
{"S16", SPS_FORMAT_S16, 44100, 16, 2, 1, SIO_LE_NATIVE},
|
||||
{"AUTOMATIC", SPS_FORMAT_S16, 44100, 16, 2, 1,
|
||||
SIO_LE_NATIVE}, // TODO: make this really automatic?
|
||||
{"S24", SPS_FORMAT_S24, 44100, 24, 4, 1, SIO_LE_NATIVE},
|
||||
{"S24_3LE", SPS_FORMAT_S24_3LE, 44100, 24, 3, 1, 1},
|
||||
{"S24_3BE", SPS_FORMAT_S24_3BE, 44100, 24, 3, 1, 0},
|
||||
{"S32", SPS_FORMAT_S32, 44100, 32, 4, 1, SIO_LE_NATIVE}};
|
||||
static struct sndio_formats format_lookup[] = {{SPS_FORMAT_S16_LE, 16, 1, 1},
|
||||
{SPS_FORMAT_S16_BE, 16, 1, 0},
|
||||
{SPS_FORMAT_S32_LE, 32, 1, 1},
|
||||
{SPS_FORMAT_S32_BE, 32, 1, 0}};
|
||||
|
||||
// use an SPS_FORMAT_... to find an entry in the format_lookup table or return NULL
|
||||
static struct sndio_formats *sps_format_lookup(sps_format_t to_find) {
|
||||
struct sndio_formats *response = NULL;
|
||||
unsigned int i = 0;
|
||||
while ((response == NULL) && (i < sizeof(format_lookup) / sizeof(struct sndio_formats))) {
|
||||
if (format_lookup[i].sps_format == to_find)
|
||||
response = &format_lookup[i];
|
||||
else
|
||||
i++;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
static uint16_t permissible_configurations[SPS_RATE_HIGHEST + 1][SPS_FORMAT_HIGHEST_NATIVE + 1]
|
||||
[8 + 1];
|
||||
|
||||
static int get_permissible_configuration_settings() {
|
||||
int ret = 0;
|
||||
uint64_t hto = get_absolute_time_in_ns();
|
||||
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
|
||||
struct sio_hdl *hdl = sio_open(output_device_name, SIO_PLAY, 0);
|
||||
if (hdl != NULL) {
|
||||
struct sio_cap cap;
|
||||
// memset(&cap, 0, sizeof(struct sio_cap));
|
||||
ret = sio_getcap(hdl, &cap);
|
||||
if (ret == 1) {
|
||||
sps_format_t f;
|
||||
sps_rate_t r;
|
||||
unsigned int c;
|
||||
// check what numbers of channels the device can provide...
|
||||
for (c = 1; c <= 8; c++) {
|
||||
// if it's in the channel set -- either due to a setting in the configuration file or by
|
||||
// default, check it...
|
||||
if ((config.channel_set & (1 << c)) != 0) {
|
||||
|
||||
unsigned int s = 0;
|
||||
int f = 0;
|
||||
while ((s < SIO_NCHAN) && (f == 0)) {
|
||||
if (cap.pchan[s] == c)
|
||||
f = 1;
|
||||
else
|
||||
s++;
|
||||
}
|
||||
if (f == 0) {
|
||||
debug(3, "sndio: output device can't deal with %u channels.", c);
|
||||
config.channel_set &= ~(1 << c); // remove this channel count
|
||||
} else {
|
||||
debug(3, "sndio: output device can have %u channels.", c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check what speeds the device can handle
|
||||
for (r = SPS_RATE_LOWEST; r <= SPS_RATE_HIGHEST; r++) {
|
||||
// if it's in the rate set -- either due to a setting in the configuration file or by
|
||||
// default, check it...
|
||||
if ((config.rate_set & (1 << r)) != 0) {
|
||||
unsigned int s = 0;
|
||||
int f = 0;
|
||||
while ((s < SIO_NRATE) && (f == 0)) {
|
||||
if (cap.rate[s] == sps_rate_actual_rate(r))
|
||||
f = 1;
|
||||
else
|
||||
s++;
|
||||
}
|
||||
if (f == 0) {
|
||||
debug(3, "sndio: output device can't be set to %u fps.", sps_rate_actual_rate(r));
|
||||
config.rate_set &= ~(1 << r); // remove this rate
|
||||
} else {
|
||||
debug(3, "sndio: output device can be set to %u fps.", sps_rate_actual_rate(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check what formats the device can handle
|
||||
for (f = SPS_FORMAT_LOWEST; f <= SPS_FORMAT_HIGHEST_NATIVE; f++) {
|
||||
// if it's in the format set -- either due to a setting in the configuration file or by
|
||||
// default, check it...
|
||||
if ((config.format_set & (1 << f)) != 0) {
|
||||
struct sndio_formats *format = sps_format_lookup(f);
|
||||
unsigned int s = 0;
|
||||
int found = 0;
|
||||
if (format != NULL) {
|
||||
while ((s < SIO_NENC) && (found == 0)) {
|
||||
if ((cap.enc[s].bits == format->bits) && (cap.enc[s].le == format->le) &&
|
||||
(cap.enc[s].sig == format->sig)) {
|
||||
// debug(1, "bits: %u, %u. le: %u, %u. sig: %u, %u.", cap.enc[s].bits, format->bits,
|
||||
// cap.enc[s].le, format->le, cap.enc[s].sig, format->sig);
|
||||
found = 1;
|
||||
} else {
|
||||
s++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug(3, "sndio: no entry for format %s.", sps_format_description_string(f));
|
||||
}
|
||||
if (found == 0) {
|
||||
if (format != 0)
|
||||
debug(3, "sndio: output device can't be set to format %s.",
|
||||
sps_format_description_string(f));
|
||||
config.format_set &= ~(1 << f); // remove this format
|
||||
} else {
|
||||
debug(3, "sndio: output device can be set to format %s.",
|
||||
sps_format_description_string(f));
|
||||
}
|
||||
}
|
||||
}
|
||||
// for (f = SPS_FORMAT_LOWEST; f <= SPS_FORMAT_HIGHEST_NATIVE; f++) {
|
||||
// if ((config.format_set & (1 << f)) != 0)
|
||||
// debug(1, "format %s okay.", sps_format_description_string(f));
|
||||
// }
|
||||
// now we have the channels, rates and formats, but we need to check each combination
|
||||
// set the permissible_configurations array (r/f/c) to EINVAL
|
||||
for (r = SPS_RATE_LOWEST; r <= SPS_RATE_HIGHEST; r++)
|
||||
for (f = SPS_FORMAT_LOWEST; f <= SPS_FORMAT_HIGHEST_NATIVE; f++)
|
||||
for (c = 0; c <= 8; c++) {
|
||||
permissible_configurations[r][f][c] = EINVAL;
|
||||
}
|
||||
// now check each combination of permitted rate/format/channel and see if it's really allowed
|
||||
for (r = SPS_RATE_LOWEST; r <= SPS_RATE_HIGHEST; r++) {
|
||||
if ((config.rate_set & (1 << r)) != 0) {
|
||||
for (f = SPS_FORMAT_LOWEST; f <= SPS_FORMAT_HIGHEST_NATIVE; f++) {
|
||||
if ((config.format_set & (1 << f)) != 0) {
|
||||
for (c = 0; c <= 8; c++) {
|
||||
if ((config.channel_set & (1 << c)) != 0) {
|
||||
// debug(1, "check %u/%s/%u.", sps_rate_actual_rate(r),
|
||||
// sps_format_description_string(f), c);
|
||||
struct sio_par proposed_par, actual_par;
|
||||
struct sndio_formats *format_info = sps_format_lookup(f);
|
||||
sio_initpar(&proposed_par);
|
||||
proposed_par.rate = sps_rate_actual_rate(r);
|
||||
proposed_par.pchan = c;
|
||||
proposed_par.bits = format_info->bits;
|
||||
proposed_par.bps = SIO_BPS(par.bits);
|
||||
proposed_par.le = format_info->le;
|
||||
proposed_par.sig = format_info->sig;
|
||||
if (sio_setpar(hdl, &proposed_par) == 1) {
|
||||
if (sio_getpar(hdl, &actual_par) == 1) {
|
||||
if ((actual_par.rate == proposed_par.rate) &&
|
||||
(actual_par.pchan == proposed_par.pchan) &&
|
||||
(actual_par.bits == proposed_par.bits) &&
|
||||
(actual_par.le == proposed_par.le) &&
|
||||
(actual_par.sig == proposed_par.sig)) {
|
||||
permissible_configurations[r][f][c] =
|
||||
0; // i.e. no error, so remove the EINVAL
|
||||
} else {
|
||||
debug(3, "sndio: check_setting: could not set format exactly");
|
||||
}
|
||||
} else {
|
||||
debug(1,
|
||||
"sndio: check_setting: could not get response for a proposed format");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pthread_cleanup_pop(1); // let the handle go...
|
||||
ret = 0; // all good here, even if the last ret was an error
|
||||
int64_t hot = get_absolute_time_in_ns() - hto;
|
||||
debug(2, "sndio: get_permissible_configuration_settings took %f ms.", 0.000001 * hot);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int check_configuration(unsigned int channels, unsigned int rate, unsigned int format) {
|
||||
// we know that the format/rate/channel count are legitimate but the combination
|
||||
// may not be permitted.
|
||||
// now see if the individual formats, rates and channel count are permissible
|
||||
sps_rate_t r = SPS_RATE_LOWEST;
|
||||
int found = 0;
|
||||
// we know the rate is there, we just have to find it.
|
||||
while ((r <= SPS_RATE_HIGHEST) && (found == 0)) {
|
||||
if ((sps_rate_actual_rate(r) == rate) && ((config.rate_set & (1 << r)) != 0))
|
||||
found = 1;
|
||||
else
|
||||
r++;
|
||||
}
|
||||
|
||||
int response = permissible_configurations[r][format][channels];
|
||||
|
||||
if (response != 0)
|
||||
debug(3, "check %u/%s/%u returns %d.", rate, sps_format_description_string(format), channels,
|
||||
response);
|
||||
return response;
|
||||
}
|
||||
|
||||
static int32_t get_configuration(unsigned int channels, unsigned int rate, unsigned int format) {
|
||||
return search_for_suitable_configuration(channels, rate, format, &check_configuration);
|
||||
}
|
||||
|
||||
static int configure(int32_t requested_encoded_format, char **channel_map) {
|
||||
int response = 0;
|
||||
debug(3, "sndio: configure %s.", short_format_description(requested_encoded_format));
|
||||
|
||||
// if (1) {
|
||||
if (current_encoded_output_format != requested_encoded_format) {
|
||||
if (current_encoded_output_format == 0)
|
||||
debug(2, "sndio: setting output configuration to %s.",
|
||||
short_format_description(requested_encoded_format));
|
||||
else
|
||||
// note -- can't use short_format_description twice in one call because it returns the same
|
||||
// string buffer each time
|
||||
debug(2, "sndio: changing output configuration to %s.",
|
||||
short_format_description(requested_encoded_format));
|
||||
current_encoded_output_format = requested_encoded_format;
|
||||
|
||||
struct sndio_formats *format_info =
|
||||
sps_format_lookup(FORMAT_FROM_ENCODED_FORMAT(requested_encoded_format));
|
||||
|
||||
if (format_info == NULL)
|
||||
die("sndio: can't find format information!");
|
||||
|
||||
if (is_running != 0) {
|
||||
debug(1, "sndio: the output device is running while changing configuration");
|
||||
if (sio_flush(hdl) != 1)
|
||||
debug(1, "sndio: unable to flush");
|
||||
written = played = is_running = 0;
|
||||
time_of_last_onmove_cb = 0;
|
||||
at_least_one_onmove_cb_seen = 0;
|
||||
}
|
||||
|
||||
struct sio_par par;
|
||||
memset(&par, 0, sizeof(struct sio_par));
|
||||
sio_initpar(&par);
|
||||
par.rate = RATE_FROM_ENCODED_FORMAT(requested_encoded_format);
|
||||
par.pchan = CHANNELS_FROM_ENCODED_FORMAT(requested_encoded_format);
|
||||
par.bits = format_info->bits;
|
||||
par.bps = SIO_BPS(par.bits);
|
||||
par.le = format_info->le;
|
||||
par.sig = format_info->sig;
|
||||
debug(3, "Requested %u/%u/%u/%u/%u (rate/bits/signed/le/channels)", par.rate, par.bits, par.sig,
|
||||
par.le, par.pchan);
|
||||
if (sio_setpar(hdl, &par) == 1) {
|
||||
struct sio_par apar;
|
||||
if (sio_getpar(hdl, &apar) == 1) {
|
||||
debug(3, "Got %u/%u/%u/%u/%u (rate/bits/signed/le/channels)", apar.rate, apar.bits,
|
||||
apar.sig, apar.le, apar.pchan);
|
||||
if ((apar.rate == par.rate) && (apar.pchan == par.pchan) && (apar.bits == par.bits) &&
|
||||
(apar.le == par.le) && (apar.sig == par.sig)) {
|
||||
framesize = apar.bps * apar.pchan;
|
||||
// config.audio_backend_buffer_desired_length = 1.0 * par.bufsz / par.rate;
|
||||
debug(
|
||||
2,
|
||||
"bufsiz is %u, rate is %u: computed buffer length is %f, actual buffer length is %f.",
|
||||
apar.bufsz, apar.rate, (1.0 * apar.bufsz) / apar.rate,
|
||||
config.audio_backend_buffer_desired_length);
|
||||
} else {
|
||||
debug(1, "sndio: configure: could not set format exactly");
|
||||
response = 1;
|
||||
}
|
||||
} else {
|
||||
debug(1, "sndio: configure: could not set format");
|
||||
response = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((response == 0) && (channel_map != NULL)) {
|
||||
switch (CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format)) {
|
||||
case 1:
|
||||
*channel_map = channel_map_1;
|
||||
break;
|
||||
case 2:
|
||||
*channel_map = channel_map_2;
|
||||
break;
|
||||
case 3:
|
||||
*channel_map = channel_map_3;
|
||||
break;
|
||||
case 4:
|
||||
*channel_map = channel_map_4;
|
||||
break;
|
||||
case 5:
|
||||
*channel_map = channel_map_5;
|
||||
break;
|
||||
case 6:
|
||||
*channel_map = channel_map_6;
|
||||
break;
|
||||
case 7:
|
||||
*channel_map = channel_map_6;
|
||||
break;
|
||||
case 8:
|
||||
*channel_map = channel_map_6;
|
||||
break;
|
||||
default:
|
||||
*channel_map = NULL;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
static void help() {
|
||||
printf(" -d output-device set the output device [default|rsnd/0|rsnd/1...]\n");
|
||||
}
|
||||
|
||||
void onmove_cb(__attribute__((unused)) void *arg, int delta) {
|
||||
static void onmove_cb(__attribute__((unused)) void *arg, int delta) {
|
||||
time_of_last_onmove_cb = get_absolute_time_in_ns();
|
||||
at_least_one_onmove_cb_seen = 1;
|
||||
played += delta;
|
||||
}
|
||||
|
||||
static int init(int argc, char **argv) {
|
||||
int found, opt, round, rate, bufsz;
|
||||
unsigned int i;
|
||||
const char *devname, *tmp;
|
||||
|
||||
// set up default values first
|
||||
|
||||
sio_initpar(&par);
|
||||
par.rate = 44100;
|
||||
par.pchan = 2;
|
||||
par.bits = 16;
|
||||
par.bps = SIO_BPS(par.bits);
|
||||
par.le = 1;
|
||||
par.sig = 1;
|
||||
devname = SIO_DEVANY;
|
||||
// debug(1, "sndio: init");
|
||||
current_encoded_output_format = 0;
|
||||
is_running = 0;
|
||||
|
||||
// defaults
|
||||
config.audio_backend_buffer_desired_length = 1.0;
|
||||
config.audio_backend_buffer_interpolation_threshold_in_seconds =
|
||||
0.25; // below this, soxr interpolation will not occur -- it'll be basic interpolation
|
||||
// instead.
|
||||
config.audio_backend_latency_offset = 0;
|
||||
output_device_name = SIO_DEVANY;
|
||||
output_device_driver_bufsiz = 0; // hmm, not sure about this...
|
||||
output_device_driver_round = 0;
|
||||
int opt;
|
||||
|
||||
// get settings from settings file
|
||||
|
||||
// do the "general" audio options. Note, these options are in the "general" stanza!
|
||||
parse_general_audio_options();
|
||||
|
||||
// get the specific settings
|
||||
// get settings from settings file, passing in defaults for format_set, rate_set and channel_set
|
||||
// Note, these options may be in the "general" stanza or the named stanza
|
||||
#ifdef CONFIG_FFMPEG
|
||||
parse_audio_options("sndio", SPS_FORMAT_SET, SPS_RATE_SET, SPS_CHANNEL_SET);
|
||||
#else
|
||||
parse_audio_options("sndio", SPS_FORMAT_NON_FFMPEG_SET, SPS_RATE_NON_FFMPEG_SET,
|
||||
SPS_CHANNNEL_NON_FFMPEG_SET);
|
||||
#endif
|
||||
|
||||
// get specific settings
|
||||
int value;
|
||||
if (config.cfg != NULL) {
|
||||
if (!config_lookup_string(config.cfg, "sndio.device", &devname))
|
||||
devname = SIO_DEVANY;
|
||||
if (config_lookup_int(config.cfg, "sndio.rate", &rate)) {
|
||||
if (rate % 44100 == 0 && rate >= 44100 && rate <= 352800) {
|
||||
par.rate = rate;
|
||||
} else {
|
||||
die("sndio: output rate must be a multiple of 44100 and 44100 <= rate <= "
|
||||
"352800");
|
||||
}
|
||||
}
|
||||
if (config_lookup_int(config.cfg, "sndio.bufsz", &bufsz)) {
|
||||
if (bufsz > 0) {
|
||||
par.appbufsz = bufsz;
|
||||
if (!config_lookup_string(config.cfg, "sndio.device", &output_device_name))
|
||||
output_device_name = SIO_DEVANY;
|
||||
|
||||
if (config_lookup_int(config.cfg, "sndio.bufsz", &value)) {
|
||||
if (value > 0) {
|
||||
output_device_driver_bufsiz = value;
|
||||
} else {
|
||||
die("sndio: bufsz must be > 0");
|
||||
}
|
||||
}
|
||||
if (config_lookup_int(config.cfg, "sndio.round", &round)) {
|
||||
if (round > 0) {
|
||||
par.round = round;
|
||||
|
||||
if (config_lookup_int(config.cfg, "sndio.round", &value)) {
|
||||
if (value > 0) {
|
||||
output_device_driver_round = value;
|
||||
} else {
|
||||
die("sndio: round must be > 0");
|
||||
}
|
||||
}
|
||||
if (config_lookup_string(config.cfg, "sndio.format", &tmp)) {
|
||||
for (i = 0, found = 0; i < sizeof(formats) / sizeof(formats[0]); i++) {
|
||||
if (strcasecmp(formats[i].name, tmp) == 0) {
|
||||
config.output_format = formats[i].fmt;
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
die("Invalid output format \"%s\". Should be one of: S8, U8, S16, S24, "
|
||||
"S24_3LE, S24_3BE, S32, Automatic",
|
||||
tmp);
|
||||
}
|
||||
}
|
||||
|
||||
optind = 1; // optind=0 is equivalent to optind=1 plus special behaviour
|
||||
argv--; // so we shift the arguments to satisfy getopt()
|
||||
argc++;
|
||||
while ((opt = getopt(argc, argv, "d:")) > 0) {
|
||||
switch (opt) {
|
||||
case 'd':
|
||||
devname = optarg;
|
||||
output_device_name = optarg;
|
||||
break;
|
||||
default:
|
||||
help();
|
||||
@@ -152,60 +432,18 @@ static int init(int argc, char **argv) {
|
||||
}
|
||||
if (optind < argc)
|
||||
die("Invalid audio argument: %s", argv[optind]);
|
||||
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
|
||||
// pthread_mutex_lock(&sndio_mutex);
|
||||
debug(1, "sndio: output device name is \"%s\".", devname);
|
||||
debug(1, "sndio: rate: %u.", par.rate);
|
||||
debug(1, "sndio: bits: %u.", par.bits);
|
||||
|
||||
is_running = 0;
|
||||
hdl = sio_open(devname, SIO_PLAY, 0);
|
||||
/*
|
||||
written = played = 0;
|
||||
time_of_last_onmove_cb = 0;
|
||||
at_least_one_onmove_cb_seen = 0;
|
||||
*/
|
||||
get_permissible_configuration_settings();
|
||||
debug(2, "sndio: output device name is \"%s\".", output_device_name);
|
||||
hdl = sio_open(output_device_name, SIO_PLAY, 0);
|
||||
if (!hdl)
|
||||
die("sndio: cannot open audio device");
|
||||
|
||||
written = played = 0;
|
||||
time_of_last_onmove_cb = 0;
|
||||
at_least_one_onmove_cb_seen = 0;
|
||||
|
||||
for (i = 0; i < sizeof(formats) / sizeof(formats[0]); i++) {
|
||||
if (formats[i].fmt == config.output_format) {
|
||||
par.bits = formats[i].bits;
|
||||
par.bps = formats[i].bps;
|
||||
par.sig = formats[i].sig;
|
||||
par.le = formats[i].le;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sio_setpar(hdl, &par) || !sio_getpar(hdl, &par))
|
||||
die("sndio: failed to set audio parameters");
|
||||
for (i = 0, found = 0; i < sizeof(formats) / sizeof(formats[0]); i++) {
|
||||
if (formats[i].bits == par.bits && formats[i].bps == par.bps && formats[i].sig == par.sig &&
|
||||
formats[i].le == par.le && formats[i].rate == par.rate) {
|
||||
config.output_format = formats[i].fmt;
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
die("sndio: could not set output device to the required format and rate.");
|
||||
|
||||
framesize = par.bps * par.pchan;
|
||||
config.output_rate = par.rate;
|
||||
if (par.rate == 0) {
|
||||
die("sndio: par.rate set to zero.");
|
||||
}
|
||||
|
||||
config.audio_backend_buffer_desired_length = 1.0 * par.bufsz / par.rate;
|
||||
config.audio_backend_latency_offset = 0;
|
||||
|
||||
sio_onmove(hdl, onmove_cb, NULL);
|
||||
|
||||
// pthread_mutex_unlock(&sndio_mutex);
|
||||
pthread_cleanup_pop(1); // unlock the mutex
|
||||
if (framesize == 0) {
|
||||
die("sndio: framesize set to zero.");
|
||||
}
|
||||
// debug(1, "sndio: init done");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -225,6 +463,7 @@ static void deinit() {
|
||||
static int play(void *buf, int frames, __attribute__((unused)) int sample_type,
|
||||
__attribute__((unused)) uint32_t timestamp,
|
||||
__attribute__((unused)) uint64_t playtime) {
|
||||
|
||||
if (frames > 0) {
|
||||
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
|
||||
if (is_running == 0) {
|
||||
@@ -246,6 +485,7 @@ static int play(void *buf, int frames, __attribute__((unused)) int sample_type,
|
||||
}
|
||||
|
||||
static void stop() {
|
||||
|
||||
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
|
||||
if (hdl != NULL) {
|
||||
if (is_running != 0) {
|
||||
@@ -263,6 +503,7 @@ static void stop() {
|
||||
|
||||
int get_delay(long *delay) {
|
||||
int response = 0;
|
||||
|
||||
size_t estimated_extra_frames_output = 0;
|
||||
if (at_least_one_onmove_cb_seen) { // when output starts, the onmove_cb callback will be made
|
||||
// calculate the difference in time between now and when the last callback occurred,
|
||||
@@ -290,7 +531,7 @@ static int delay(long *delay) {
|
||||
if (is_running != 0) {
|
||||
result = get_delay(delay);
|
||||
} else {
|
||||
debug(1, "sndio: output device is not open for delay!");
|
||||
debug(2, "sndio: output device is not open for delay!");
|
||||
if (delay != NULL)
|
||||
*delay = 0;
|
||||
}
|
||||
@@ -309,7 +550,7 @@ static void flush() {
|
||||
debug(1, "sndio: unable to flush");
|
||||
written = played = is_running = 0;
|
||||
} else {
|
||||
debug(1, "sndio: flush: not running.");
|
||||
debug(2, "sndio: flush: not running.");
|
||||
}
|
||||
} else {
|
||||
debug(1, "sndio: output device is not open for flush!");
|
||||
@@ -321,7 +562,8 @@ audio_output audio_sndio = {.name = "sndio",
|
||||
.help = &help,
|
||||
.init = &init,
|
||||
.deinit = &deinit,
|
||||
.prepare = NULL,
|
||||
.configure = &configure,
|
||||
.get_configuration = &get_configuration,
|
||||
.start = NULL,
|
||||
.stop = &stop,
|
||||
.is_running = NULL,
|
||||
|
||||
+21
-32
@@ -34,7 +34,7 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m
|
||||
if (frame_count_min > fill_count) {
|
||||
int frame_count = frame_count_min;
|
||||
if ((err = soundio_outstream_begin_write(outstream, &areas, &frame_count))) {
|
||||
debug(0, "[--->>] begin write error: %s", soundio_strerror(err));
|
||||
debug(1, "[--->>] begin write error: %s", soundio_strerror(err));
|
||||
}
|
||||
for (int frame = 0; frame < frame_count; frame += 1) {
|
||||
for (int ch = 0; ch < outstream->layout.channel_count; ch += 1) {
|
||||
@@ -43,7 +43,7 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m
|
||||
}
|
||||
}
|
||||
if ((err = soundio_outstream_end_write(outstream)))
|
||||
debug(0, "[--->>] end write error: %s", soundio_strerror(err));
|
||||
debug(1, "[--->>] end write error: %s", soundio_strerror(err));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m
|
||||
int frame_count = frames_left;
|
||||
|
||||
if ((err = soundio_outstream_begin_write(outstream, &areas, &frame_count)))
|
||||
debug(0, "[--->>] begin write error: %s", soundio_strerror(err));
|
||||
debug(1, "[--->>] begin write error: %s", soundio_strerror(err));
|
||||
|
||||
if (frame_count <= 0)
|
||||
break;
|
||||
@@ -68,7 +68,7 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m
|
||||
}
|
||||
|
||||
if ((err = soundio_outstream_end_write(outstream)))
|
||||
debug(0, "[--->>] end write error: %s", soundio_strerror(err));
|
||||
debug(1, "[--->>] end write error: %s", soundio_strerror(err));
|
||||
|
||||
frames_left -= frame_count;
|
||||
}
|
||||
@@ -79,7 +79,7 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m
|
||||
|
||||
static void underflow_callback(__attribute__((unused)) struct SoundIoOutStream *outstream) {
|
||||
static int count = 0;
|
||||
debug(0, "underflow %d\n", ++count);
|
||||
debug(1, "underflow %d\n", ++count);
|
||||
}
|
||||
|
||||
static int init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) {
|
||||
@@ -88,36 +88,34 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char *
|
||||
config.audio_backend_buffer_desired_length = 2.0;
|
||||
config.audio_backend_latency_offset = 0;
|
||||
|
||||
// get settings from settings file
|
||||
|
||||
// do the "general" audio options. Note, these options are in the "general" stanza!
|
||||
parse_general_audio_options();
|
||||
// this ensures that the Shairport Sync system will provide only 44100/S16/2 to this backend.
|
||||
parse_audio_options(NULL, (1 << SPS_FORMAT_S16_LE), (1 << SPS_RATE_44100), (1 << 2));
|
||||
|
||||
// get the specific settings
|
||||
|
||||
soundio = soundio_create();
|
||||
if (!soundio) {
|
||||
debug(0, "out of memory\n");
|
||||
debug(1, "out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
if ((err = soundio_connect_backend(soundio, SoundIoBackendCoreAudio))) {
|
||||
debug(0, "error connecting: %s", soundio_strerror(err));
|
||||
if ((err = soundio_connect_backend(soundio, SoundIoBackendAlsa))) {
|
||||
debug(1, "error connecting: %s", soundio_strerror(err));
|
||||
return 1;
|
||||
}
|
||||
soundio_flush_events(soundio);
|
||||
|
||||
int default_out_device_index = soundio_default_output_device_index(soundio);
|
||||
if (default_out_device_index < 0) {
|
||||
debug(0, "no output device found");
|
||||
debug(1, "no output device found");
|
||||
return 1;
|
||||
}
|
||||
|
||||
device = soundio_get_output_device(soundio, default_out_device_index);
|
||||
if (!device) {
|
||||
debug(0, "out of memory");
|
||||
debug(1, "out of memory");
|
||||
return 1;
|
||||
}
|
||||
debug(0, "Output device: %s\n", device->name);
|
||||
debug(1, "Output device: %s\n", device->name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -125,7 +123,7 @@ static void deinit(void) {
|
||||
soundio_ring_buffer_destroy(ring_buffer);
|
||||
soundio_device_unref(device);
|
||||
soundio_destroy(soundio);
|
||||
debug(0, "soundio audio deinit\n");
|
||||
debug(1, "soundio audio deinit\n");
|
||||
}
|
||||
|
||||
static void start(int sample_rate, int sample_format) {
|
||||
@@ -141,24 +139,24 @@ static void start(int sample_rate, int sample_format) {
|
||||
outstream->layout.channel_count = 2;
|
||||
outstream->write_callback = write_callback;
|
||||
outstream->underflow_callback = underflow_callback;
|
||||
// outstream->software_latency = 0;
|
||||
outstream->software_latency = 0;
|
||||
|
||||
if ((err = soundio_outstream_open(outstream))) {
|
||||
debug(0, "unable to open device: %s", soundio_strerror(err));
|
||||
debug(1, "unable to open device: %s", soundio_strerror(err));
|
||||
}
|
||||
if (outstream->layout_error)
|
||||
debug(0, "unable to set channel layout: %s\n", soundio_strerror(outstream->layout_error));
|
||||
debug(1, "unable to set channel layout: %s\n", soundio_strerror(outstream->layout_error));
|
||||
|
||||
int capacity = outstream->sample_rate * outstream->bytes_per_frame;
|
||||
ring_buffer = soundio_ring_buffer_create(soundio, capacity);
|
||||
if (!ring_buffer)
|
||||
debug(0, "unable to create ring buffer: out of memory");
|
||||
debug(1, "unable to create ring buffer: out of memory");
|
||||
char *buf = soundio_ring_buffer_write_ptr(ring_buffer);
|
||||
memset(buf, 0, capacity);
|
||||
soundio_ring_buffer_advance_write_ptr(ring_buffer, capacity);
|
||||
|
||||
if ((err = soundio_outstream_start(outstream))) {
|
||||
debug(0, "unable to start outstream: %s", soundio_strerror(err));
|
||||
debug(1, "unable to start outstream: %s", soundio_strerror(err));
|
||||
}
|
||||
|
||||
debug(1, "libsoundio output started\n");
|
||||
@@ -188,15 +186,6 @@ static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void parameters(audio_parameters *info) {
|
||||
info->minimum_volume_dB = -30.0;
|
||||
info->maximum_volume_dB = 0.0;
|
||||
debug(2, "Parameters\n");
|
||||
debug(2, "Current Volume dB: %f\n", info->current_volume_dB);
|
||||
debug(2, "Minimum Volume dB: %d\n", info->minimum_volume_dB);
|
||||
debug(2, "Maximum Volume dB: %d\n", info->maximum_volume_dB);
|
||||
}
|
||||
|
||||
static void stop(void) {
|
||||
soundio_outstream_destroy(outstream);
|
||||
soundio_ring_buffer_clear(ring_buffer);
|
||||
@@ -212,7 +201,7 @@ audio_output audio_soundio = {.name = "soundio",
|
||||
.help = NULL,
|
||||
.init = &init,
|
||||
.deinit = &deinit,
|
||||
.prepare = NULL,
|
||||
.configure = NULL,
|
||||
.start = &start,
|
||||
.stop = &stop,
|
||||
.is_running = NULL,
|
||||
@@ -221,5 +210,5 @@ audio_output audio_soundio = {.name = "soundio",
|
||||
.stats = NULL,
|
||||
.play = &play,
|
||||
.volume = NULL,
|
||||
.parameters = ¶meters,
|
||||
.parameters = NULL,
|
||||
.mute = NULL};
|
||||
|
||||
+37
-23
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* stdout output driver. This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2015
|
||||
* Copyright (c) Mike Brady 2015--2025
|
||||
*
|
||||
* Based on pipe output driver
|
||||
* Copyright (c) James Laird 2013
|
||||
@@ -36,54 +36,68 @@
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int fd = -1;
|
||||
static int fd = STDOUT_FILENO;
|
||||
static int warned = 0;
|
||||
|
||||
static void start(__attribute__((unused)) int sample_rate,
|
||||
__attribute__((unused)) int sample_format) {
|
||||
fd = STDOUT_FILENO;
|
||||
warned = 0;
|
||||
}
|
||||
static unsigned int bytes_per_frame = 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 rc = write(fd, buf, samples * 4);
|
||||
if (bytes_per_frame == 0)
|
||||
debug(1, "stdout: bytes per frame not initialised before play()!");
|
||||
int rc = write(fd, buf, samples * bytes_per_frame);
|
||||
if ((rc < 0) && (warned == 0)) {
|
||||
strerror_r(errno, (char *)errorstring, 1024);
|
||||
warn("Error %d writing to stdout (fd: %d): \"%s\".", errno, fd, errorstring);
|
||||
warn("error %d writing to stdout (fd: %d): \"%s\".", errno, fd, errorstring);
|
||||
warned = 1;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void stop(void) {
|
||||
// Do nothing when play stops
|
||||
}
|
||||
|
||||
static int init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) {
|
||||
// set up default values first
|
||||
config.audio_backend_buffer_desired_length = 1.0;
|
||||
config.audio_backend_latency_offset = 0;
|
||||
|
||||
// get settings from settings file
|
||||
// do the "general" audio options. Note, these options are in the "general" stanza!
|
||||
parse_general_audio_options();
|
||||
// get settings from settings file, passing in defaults for format_set, rate_set and channel_set
|
||||
// Note, these options may be in the "general" stanza or the named stanza
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
parse_audio_options("stdout", (1 << SPS_FORMAT_S32_LE), (1 << SPS_RATE_48000), (1 << 2));
|
||||
#else
|
||||
parse_audio_options("stdout", (1 << SPS_FORMAT_S16_LE), (1 << SPS_RATE_44100), (1 << 2));
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void deinit(void) {
|
||||
// don't close stdout
|
||||
static int32_t get_configuration(unsigned int channels, unsigned int rate, unsigned int format) {
|
||||
// use the standard format/rate/channel search to get a suitable configuration. No
|
||||
// check_configuration() method needs to be passed to search_for_suitable_configuration() because
|
||||
// it will always return a valid choice based on any settings and the defaults
|
||||
return search_for_suitable_configuration(channels, rate, format, NULL);
|
||||
}
|
||||
|
||||
static int configure(int32_t requested_encoded_format, __attribute__((unused)) char **channel_map) {
|
||||
int response = 0;
|
||||
unsigned int bytes_per_sample =
|
||||
sps_format_sample_size(FORMAT_FROM_ENCODED_FORMAT(requested_encoded_format));
|
||||
if (bytes_per_sample == 0) {
|
||||
debug(1, "stdout: unknown output format.");
|
||||
bytes_per_sample = 4; // emergency hack
|
||||
response = EINVAL;
|
||||
}
|
||||
bytes_per_frame = bytes_per_sample * CHANNELS_FROM_ENCODED_FORMAT(requested_encoded_format);
|
||||
return response;
|
||||
}
|
||||
|
||||
audio_output audio_stdout = {.name = "stdout",
|
||||
.help = NULL,
|
||||
.init = &init,
|
||||
.deinit = &deinit,
|
||||
.prepare = NULL,
|
||||
.start = &start,
|
||||
.stop = &stop,
|
||||
.deinit = NULL,
|
||||
.get_configuration = &get_configuration,
|
||||
.configure = &configure,
|
||||
.start = NULL,
|
||||
.stop = NULL,
|
||||
.is_running = NULL,
|
||||
.flush = NULL,
|
||||
.delay = NULL,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Utility routines. This file is part of Shairport.
|
||||
* Copyright (c) James Laird 2013
|
||||
* The volume to attenuation function vol2attn copyright (c) Mike Brady 2014
|
||||
* Further changes and additions (c) Mike Brady 2014 -- 2021
|
||||
* Further changes and additions (c) Mike Brady 2014--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -45,6 +45,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
@@ -125,15 +126,34 @@ void set_alsa_out_dev(char *);
|
||||
config_t config_file_stuff;
|
||||
int type_of_exit_cleanup;
|
||||
uint64_t ns_time_at_startup, ns_time_at_last_debug_message;
|
||||
uint64_t minimum_dac_queue_size;
|
||||
|
||||
// always lock use this when accessing the ns_time_at_last_debug_message
|
||||
static pthread_mutex_t debug_timing_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
pthread_mutex_t the_conn_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
unsigned int sps_format_sample_size_array[] = {
|
||||
0, // unknown
|
||||
1, 1, // S8, U8
|
||||
2, 2, // S16_LE, S16_BE,
|
||||
4, 4, // S24_LE, S24_BE,
|
||||
3, 3, // S24_3LE, S24_3BE,
|
||||
4, 4, // S32_LE, S32_BE,
|
||||
2, 4, 4, // S16, S24, S32
|
||||
0, 0 // Auto, Invalid
|
||||
};
|
||||
|
||||
unsigned int sps_format_sample_size(sps_format_t format) {
|
||||
unsigned int response = 0;
|
||||
if (format <= SPS_FORMAT_AUTO)
|
||||
response = sps_format_sample_size_array[format];
|
||||
return response;
|
||||
}
|
||||
|
||||
const char *sps_format_description_string_array[] = {
|
||||
"unknown", "S8", "U8", "S16", "S16_LE", "S16_BE", "S24", "S24_LE",
|
||||
"S24_BE", "S24_3LE", "S24_3BE", "S32", "S32_LE", "S32_BE", "auto", "invalid"};
|
||||
"unknown", "S8", "U8", "S16_LE", "S16_BE", "S24_LE", "S24_BE", "S24_3LE",
|
||||
"S24_3BE", "S32_LE", "S32_BE", "S16", "S24", "S32", "auto", "invalid"};
|
||||
|
||||
const char *sps_format_description_string(sps_format_t format) {
|
||||
if (format <= SPS_FORMAT_AUTO)
|
||||
@@ -142,8 +162,74 @@ const char *sps_format_description_string(sps_format_t format) {
|
||||
return sps_format_description_string_array[SPS_FORMAT_INVALID];
|
||||
}
|
||||
|
||||
// true if Shairport Sync is supposed to be sending output to the output device, false otherwise
|
||||
unsigned int sps_rate_actual_rate(sps_rate_t rate) {
|
||||
unsigned int response = 0;
|
||||
switch (rate) {
|
||||
case SPS_RATE_5512:
|
||||
response = 5512;
|
||||
break;
|
||||
case SPS_RATE_8000:
|
||||
response = 8000;
|
||||
break;
|
||||
case SPS_RATE_11025:
|
||||
response = 11025;
|
||||
break;
|
||||
case SPS_RATE_16000:
|
||||
response = 16000;
|
||||
break;
|
||||
case SPS_RATE_22050:
|
||||
response = 22050;
|
||||
break;
|
||||
case SPS_RATE_32000:
|
||||
response = 32000;
|
||||
break;
|
||||
case SPS_RATE_44100:
|
||||
response = 44100;
|
||||
break;
|
||||
case SPS_RATE_48000:
|
||||
response = 48000;
|
||||
break;
|
||||
case SPS_RATE_64000:
|
||||
response = 64000;
|
||||
break;
|
||||
case SPS_RATE_88200:
|
||||
response = 88200;
|
||||
break;
|
||||
case SPS_RATE_96000:
|
||||
response = 96000;
|
||||
break;
|
||||
case SPS_RATE_176400:
|
||||
response = 176400;
|
||||
break;
|
||||
case SPS_RATE_192000:
|
||||
response = 192000;
|
||||
break;
|
||||
case SPS_RATE_352800:
|
||||
response = 352800;
|
||||
break;
|
||||
case SPS_RATE_384000:
|
||||
response = 384000;
|
||||
break;
|
||||
default:
|
||||
debug(1, "unrecognised SPS_RATE_: %u.", rate);
|
||||
break;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
char sfd[32];
|
||||
const char *short_format_description(int32_t encoded_format) {
|
||||
if (encoded_format < 0)
|
||||
snprintf(sfd, sizeof(sfd) - 1, "error %d", encoded_format);
|
||||
else
|
||||
snprintf(
|
||||
sfd, sizeof(sfd) - 1, "%u/%s/%u", RATE_FROM_ENCODED_FORMAT(encoded_format),
|
||||
sps_format_description_string((sps_format_t)(FORMAT_FROM_ENCODED_FORMAT(encoded_format))),
|
||||
CHANNELS_FROM_ENCODED_FORMAT(encoded_format));
|
||||
return (const char *)sfd;
|
||||
}
|
||||
|
||||
// true if Shairport Sync is supposed to be sending output to the output device, false otherwise
|
||||
static volatile int requested_connection_state_to_output = 1;
|
||||
|
||||
// this stuff is to direct logging to syslog via libdaemon or directly
|
||||
@@ -256,6 +342,18 @@ volatile int debuglev = 0;
|
||||
|
||||
sigset_t pselect_sigset;
|
||||
|
||||
// note -- don't use this to shutdown from dbus -- see its own code in dbus-service.c
|
||||
void sps_shutdown(type_of_exit_type shutdown_type) { // TOE_normal, TOE_emergency
|
||||
type_of_exit_cleanup = shutdown_type;
|
||||
if (type_of_exit_cleanup == TOE_emergency) {
|
||||
debug(1, "emergency shutdown requested");
|
||||
exit(EXIT_FAILURE);
|
||||
} else {
|
||||
debug(1, "normal shutdown requested");
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
int usleep_uncancellable(useconds_t usec) {
|
||||
int response;
|
||||
int oldState;
|
||||
@@ -315,7 +413,7 @@ int bind_socket_and_port(int type, int ip_family, const char *self_ip_address, u
|
||||
ret = errno;
|
||||
close(local_socket);
|
||||
char errorstring[1024];
|
||||
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
|
||||
getErrorText((char *)errorstring, sizeof(errorstring));
|
||||
warn("error %d: \"%s\". Could not bind a port!", errno, errorstring);
|
||||
} else {
|
||||
uint16_t sport;
|
||||
@@ -326,7 +424,7 @@ int bind_socket_and_port(int type, int ip_family, const char *self_ip_address, u
|
||||
ret = errno;
|
||||
close(local_socket);
|
||||
char errorstring[1024];
|
||||
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
|
||||
getErrorText((char *)errorstring, sizeof(errorstring));
|
||||
warn("error %d: \"%s\". Could not retrieve socket's port!", errno, errorstring);
|
||||
} else {
|
||||
#ifdef AF_INET6
|
||||
@@ -398,7 +496,7 @@ uint16_t bind_UDP_port(int ip_family, const char *self_ip_address, uint32_t scop
|
||||
if (ret < 0) {
|
||||
close(local_socket);
|
||||
char errorstring[1024];
|
||||
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
|
||||
getErrorText((char *)errorstring, sizeof(errorstring));
|
||||
die("error %d: \"%s\". Could not bind a UDP port! Check the udp_port_range is large enough -- "
|
||||
"it must be "
|
||||
"at least 3, and 10 or more is suggested -- or "
|
||||
@@ -516,6 +614,13 @@ void _warn(const char *thefilename, const int linenumber, const char *format, ..
|
||||
pthread_setcancelstate(oldState, NULL);
|
||||
}
|
||||
|
||||
void getErrorText(char *destinationString, size_t destinationStringLength) {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wunused-result"
|
||||
strerror_r(errno, destinationString, destinationStringLength);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
void _debug(const char *thefilename, const int linenumber, int level, const char *format, ...) {
|
||||
if (level > debuglev)
|
||||
return;
|
||||
@@ -654,6 +759,61 @@ int mkpath(const char *path, mode_t mode) {
|
||||
return (status);
|
||||
}
|
||||
|
||||
// including a simple base64 encoder to minimise malloc/free activity
|
||||
|
||||
// From Stack Overflow, with thanks:
|
||||
// http://stackoverflow.com/questions/342409/how-do-i-base64-encode-decode-in-c
|
||||
// minor mods to make independent of C99.
|
||||
// more significant changes make it not malloc memory
|
||||
// needs to initialise the encoding table first
|
||||
|
||||
// add _so to end of name to avoid confusion with polarssl's implementation
|
||||
|
||||
static char encoding_table[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
||||
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
|
||||
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
|
||||
|
||||
static size_t mod_table[] = {0, 2, 1};
|
||||
|
||||
// pass in a pointer to the data, its length, a pointer to the output buffer and
|
||||
// a pointer to an int
|
||||
// containing its maximum length
|
||||
// the actual length will be returned.
|
||||
|
||||
char *base64_encode_so(const unsigned char *data, size_t input_length, char *encoded_data,
|
||||
size_t *output_length) {
|
||||
|
||||
size_t calculated_output_length = 4 * ((input_length + 2) / 3);
|
||||
if (calculated_output_length > *output_length)
|
||||
return (NULL);
|
||||
*output_length = calculated_output_length;
|
||||
|
||||
size_t i, j;
|
||||
for (i = 0, j = 0; i < input_length;) {
|
||||
|
||||
uint32_t octet_a = i < input_length ? (unsigned char)data[i++] : 0;
|
||||
uint32_t octet_b = i < input_length ? (unsigned char)data[i++] : 0;
|
||||
uint32_t octet_c = i < input_length ? (unsigned char)data[i++] : 0;
|
||||
|
||||
uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
|
||||
|
||||
encoded_data[j++] = encoding_table[(triple >> 3 * 6) & 0x3F];
|
||||
encoded_data[j++] = encoding_table[(triple >> 2 * 6) & 0x3F];
|
||||
encoded_data[j++] = encoding_table[(triple >> 1 * 6) & 0x3F];
|
||||
encoded_data[j++] = encoding_table[(triple >> 0 * 6) & 0x3F];
|
||||
}
|
||||
|
||||
for (i = 0; i < mod_table[input_length % 3]; i++)
|
||||
encoded_data[*output_length - 1 - i] = '=';
|
||||
|
||||
return encoded_data;
|
||||
}
|
||||
|
||||
// with thanks!
|
||||
//
|
||||
|
||||
#ifdef CONFIG_MBEDTLS
|
||||
char *base64_enc(uint8_t *input, int length) {
|
||||
char *buf = NULL;
|
||||
@@ -952,7 +1112,7 @@ uint8_t *rsa_apply(uint8_t *input, int inlen, int *outlen, int mode) {
|
||||
rc = mbedtls_pk_parse_key(&pkctx, (unsigned char *)super_secret_key, sizeof(super_secret_key),
|
||||
NULL, 0, mbedtls_ctr_drbg_random, &ctr_drbg);
|
||||
#else
|
||||
rc = mbedtls_pk_parse_key(&pkctx, (unsigned char *)super_secret_key, sizeof(super_secret_key),
|
||||
rc = mbedtls_pk_parse_key(&pkctx, (unsigned char *)super_secret_key, sizeof(super_secret_key),
|
||||
NULL, 0);
|
||||
|
||||
#endif
|
||||
@@ -964,11 +1124,10 @@ uint8_t *rsa_apply(uint8_t *input, int inlen, int *outlen, int mode) {
|
||||
|
||||
switch (mode) {
|
||||
case RSA_MODE_AUTH:
|
||||
mbedtls_rsa_set_padding(trsa, MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_NONE);
|
||||
mbedtls_rsa_set_padding(trsa, MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_NONE);
|
||||
outbuf = malloc(trsa->MBEDTLS_PRIVATE_V3_ONLY(len));
|
||||
#if MBEDTLS_VERSION_MAJOR == 3
|
||||
rc = mbedtls_rsa_pkcs1_encrypt(trsa, mbedtls_ctr_drbg_random, &ctr_drbg,
|
||||
inlen, input, outbuf);
|
||||
rc = mbedtls_rsa_pkcs1_encrypt(trsa, mbedtls_ctr_drbg_random, &ctr_drbg, inlen, input, outbuf);
|
||||
#else
|
||||
rc = mbedtls_rsa_pkcs1_encrypt(trsa, mbedtls_ctr_drbg_random, &ctr_drbg, MBEDTLS_RSA_PRIVATE,
|
||||
inlen, input, outbuf);
|
||||
@@ -981,8 +1140,8 @@ uint8_t *rsa_apply(uint8_t *input, int inlen, int *outlen, int mode) {
|
||||
mbedtls_rsa_set_padding(trsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA1);
|
||||
outbuf = malloc(trsa->MBEDTLS_PRIVATE_V3_ONLY(len));
|
||||
#if MBEDTLS_VERSION_MAJOR == 3
|
||||
rc = mbedtls_rsa_pkcs1_decrypt(trsa, mbedtls_ctr_drbg_random, &ctr_drbg,
|
||||
&olen, input, outbuf, trsa->MBEDTLS_PRIVATE_V3_ONLY(len));
|
||||
rc = mbedtls_rsa_pkcs1_decrypt(trsa, mbedtls_ctr_drbg_random, &ctr_drbg, &olen, input, outbuf,
|
||||
trsa->MBEDTLS_PRIVATE_V3_ONLY(len));
|
||||
#else
|
||||
rc = mbedtls_rsa_pkcs1_decrypt(trsa, mbedtls_ctr_drbg_random, &ctr_drbg, MBEDTLS_RSA_PRIVATE,
|
||||
&olen, input, outbuf, trsa->len);
|
||||
@@ -1075,6 +1234,165 @@ int config_set_lookup_bool(config_t *cfg, char *where, int *dst) {
|
||||
}
|
||||
}
|
||||
|
||||
// remember to free the returned array of strings.
|
||||
// you don't need to free the strings themselves -- they belong to libconfig.
|
||||
unsigned int config_get_string_settings_as_string_array(config_setting_t *setting,
|
||||
const char ***result) {
|
||||
unsigned int count = 0;
|
||||
int error = 0;
|
||||
*result = NULL;
|
||||
const char **arr = NULL;
|
||||
if (setting != NULL) { // definitely a setting
|
||||
const char *str = config_setting_get_string(setting);
|
||||
if (str != NULL) { // definitely a string
|
||||
arr = malloc(sizeof(const char *));
|
||||
arr[0] = str;
|
||||
count = 1;
|
||||
} else { // it might be a list, an array or a group
|
||||
count = config_setting_length(setting);
|
||||
if (count != 0) {
|
||||
arr = malloc(sizeof(const char *) * count);
|
||||
unsigned int i;
|
||||
for (i = 0; i < count; i++) {
|
||||
config_setting_t *item = config_setting_get_elem(setting, i);
|
||||
if (config_setting_type(item) == CONFIG_TYPE_STRING)
|
||||
arr[i] = config_setting_get_string(item);
|
||||
else
|
||||
error = i + 1;
|
||||
}
|
||||
} else {
|
||||
error = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (error != 0) {
|
||||
if (arr != NULL) {
|
||||
free(arr);
|
||||
}
|
||||
count = -error; // signify an error
|
||||
} else {
|
||||
*result = arr;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// remember to free the returned array of ints.
|
||||
unsigned int config_get_int_settings_as_int_array(config_setting_t *setting, int **result) {
|
||||
int error = 0;
|
||||
unsigned int count = 0;
|
||||
*result = NULL;
|
||||
int *arr = NULL;
|
||||
if (setting != NULL) { // definitely a setting there
|
||||
if (config_setting_type(setting) == CONFIG_TYPE_INT) {
|
||||
arr = malloc(sizeof(int));
|
||||
arr[0] = config_setting_get_int(setting);
|
||||
count = 1;
|
||||
} else if (config_setting_is_aggregate(setting) == CONFIG_TRUE) {
|
||||
count = config_setting_length(setting);
|
||||
if (count != 0) {
|
||||
arr = malloc(sizeof(int) * count);
|
||||
unsigned int i;
|
||||
for (i = 0; i < count; i++) {
|
||||
config_setting_t *item = config_setting_get_elem(setting, i);
|
||||
if (config_setting_type(item) == CONFIG_TYPE_INT)
|
||||
arr[i] = config_setting_get_int(item);
|
||||
else
|
||||
error = i + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error = 1; // subtract 1 from the error number to get the element number
|
||||
}
|
||||
}
|
||||
if (error != 0) {
|
||||
if (arr != NULL) {
|
||||
free(arr);
|
||||
}
|
||||
count = -error; // signify an error
|
||||
} else {
|
||||
*result = arr;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Look for the item in the setting which could be either a string or an array or list or group of
|
||||
// strings. Result: 0 means there is a setting but no match, 1 means there's no setting, 2 means
|
||||
// "auto" was found, 3 means a match.
|
||||
int check_string_or_list_setting(config_setting_t *setting, const char *item) {
|
||||
int result = 1; // means there is no setting at all (so the caller should implement the default)
|
||||
if (setting != NULL) { // definitely a setting
|
||||
const char *str = config_setting_get_string(setting);
|
||||
debug(3, "check \"%s\" against \"%s\"", str, item);
|
||||
if (str != NULL) { // definitely a string
|
||||
if (strcasecmp(str, item) == 0) {
|
||||
result = 3; // an exact match
|
||||
} else if (strcasecmp(str, "auto") == 0) {
|
||||
result = 2; // auto
|
||||
} else {
|
||||
result = 0; // a string that is not a match
|
||||
}
|
||||
} else { // it might be a list, an array or a group
|
||||
int i = 0;
|
||||
result = 0; // presume there is no match
|
||||
// keep looking, even if "auto" has been found, to see if the exact match (preferred) is there
|
||||
// too.
|
||||
while (((result == 0) || (result == 2)) && (i < config_setting_length(setting))) {
|
||||
const char *str2 = config_setting_get_string_elem(setting, i);
|
||||
if (str2 != NULL) { // definitely a string
|
||||
if (strcasecmp(str2, "auto") == 0) {
|
||||
result = 2; // auto
|
||||
} else if (strcasecmp(str2, item) == 0) {
|
||||
result = 3; // an exact match
|
||||
}
|
||||
}
|
||||
i++; // will point to 1 past the found item or last item.
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Look for the item in the setting which could be either an int or an array or list or group of
|
||||
// ints. Result: 0 means there is a setting but no match, 1 means there's no setting, 2 means "auto"
|
||||
// was found, 3 means a match.
|
||||
int check_int_or_list_setting(config_setting_t *setting, const int item) {
|
||||
int result = 1; // means there is no setting at all
|
||||
if (setting != NULL) { // definitely a setting
|
||||
int setting_type = config_setting_type(setting);
|
||||
if (setting_type == CONFIG_TYPE_STRING) {
|
||||
if (strcasecmp(config_setting_get_string(setting), "auto") == 0) {
|
||||
result = 2; // auto
|
||||
} else {
|
||||
result = 0; // a string that can not be a match
|
||||
}
|
||||
} else if (setting_type == CONFIG_TYPE_INT) {
|
||||
if (item == config_setting_get_int(setting))
|
||||
result = 3; // an exact match
|
||||
else
|
||||
result = 0; // a setting but not a match
|
||||
} else { // it might be a list, an array or a group
|
||||
int i = 0;
|
||||
result = 0; // presume there is no match (there is a setting)
|
||||
// keep looking, even if "auto" has been found, to see if the exact match (preferred) is there
|
||||
// too.
|
||||
while (((result == 0) || (result == 2)) && (i < config_setting_length(setting))) {
|
||||
config_setting_t *sub_setting = config_setting_get_elem(setting, i);
|
||||
int sub_setting_type = config_setting_type(sub_setting);
|
||||
if (sub_setting_type == CONFIG_TYPE_STRING) {
|
||||
if (strcasecmp(config_setting_get_string_elem(sub_setting, i), "auto") == 0) {
|
||||
result = 2; // auto
|
||||
}
|
||||
} else if (sub_setting_type == CONFIG_TYPE_INT) {
|
||||
if (item == config_setting_get_int(sub_setting))
|
||||
result = 3; // an exact match
|
||||
}
|
||||
i++; // will point to 1 past the found item or last item.
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void command_set_volume(double volume) {
|
||||
// this has a cancellation point if waiting is enabled
|
||||
if (config.cmd_set_volume) {
|
||||
@@ -1519,14 +1837,14 @@ int try_to_open_pipe_for_writing(const char *pathname) {
|
||||
int flags = fcntl(fdis, F_GETFL);
|
||||
if (flags == -1) {
|
||||
char errorstring[1024];
|
||||
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
|
||||
getErrorText((char *)errorstring, sizeof(errorstring));
|
||||
debug(1, "try_to_open_pipe -- error %d (\"%s\") getting flags of pipe: \"%s\".", errno,
|
||||
(char *)errorstring, pathname);
|
||||
} else {
|
||||
flags = fcntl(fdis, F_SETFL, flags & ~O_NONBLOCK);
|
||||
if (flags == -1) {
|
||||
char errorstring[1024];
|
||||
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
|
||||
getErrorText((char *)errorstring, sizeof(errorstring));
|
||||
debug(1, "try_to_open_pipe -- error %d (\"%s\") unsetting NONBLOCK of pipe: \"%s\".", errno,
|
||||
(char *)errorstring, pathname);
|
||||
}
|
||||
@@ -1748,8 +2066,12 @@ int _debug_mutex_unlock(pthread_mutex_t *mutex, const char *mutexname, const cha
|
||||
}
|
||||
|
||||
void malloc_cleanup(void *arg) {
|
||||
// debug(1, "malloc cleanup freeing %" PRIxPTR ".", arg);
|
||||
free(arg);
|
||||
// the address of the malloc variable is passed in case a realloc is done as some time
|
||||
// debug(1, "malloc cleanup called.");
|
||||
void **allocation = arg;
|
||||
void *ref = *allocation;
|
||||
if (ref != NULL)
|
||||
free(ref);
|
||||
}
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
@@ -1761,7 +2083,8 @@ void plist_cleanup(void *arg) {
|
||||
|
||||
void socket_cleanup(void *arg) {
|
||||
intptr_t fdp = (intptr_t)arg;
|
||||
debug(3, "socket_cleanup called for socket: %" PRIdPTR ".", fdp);
|
||||
int soc = fdp;
|
||||
debug(3, "socket_cleanup called for socket: %" PRIdPTR ".", soc);
|
||||
close(fdp);
|
||||
}
|
||||
|
||||
@@ -1777,10 +2100,10 @@ void mutex_cleanup(void *arg) {
|
||||
pthread_mutex_destroy(mutex);
|
||||
}
|
||||
|
||||
void mutex_unlock(void *arg) { pthread_mutex_unlock((pthread_mutex_t *)arg); }
|
||||
|
||||
void rwlock_unlock(void *arg) { pthread_rwlock_unlock((pthread_rwlock_t *)arg); }
|
||||
|
||||
void mutex_unlock(void *arg) { pthread_mutex_unlock((pthread_mutex_t *)arg); }
|
||||
|
||||
void thread_cleanup(void *arg) {
|
||||
debug(3, "thread_cleanup called.");
|
||||
pthread_t *thread = (pthread_t *)arg;
|
||||
@@ -1812,6 +2135,11 @@ char *get_version_string() {
|
||||
#ifdef CONFIG_APPLE_ALAC
|
||||
strcat(version_string, "-alac");
|
||||
#endif
|
||||
#ifndef CONFIG_AIRPLAY_2
|
||||
#ifdef CONFIG_FFMPEG
|
||||
strcat(version_string, "-FFmpeg");
|
||||
#endif
|
||||
#endif
|
||||
#ifdef CONFIG_LIBDAEMON
|
||||
strcat(version_string, "-libdaemon");
|
||||
#endif
|
||||
@@ -1848,11 +2176,11 @@ char *get_version_string() {
|
||||
#ifdef CONFIG_AO
|
||||
strcat(version_string, "-ao");
|
||||
#endif
|
||||
#ifdef CONFIG_PA
|
||||
strcat(version_string, "-pa");
|
||||
#ifdef CONFIG_PULSEAUDIO
|
||||
strcat(version_string, "-PulseAudio");
|
||||
#endif
|
||||
#ifdef CONFIG_PW
|
||||
strcat(version_string, "-pw");
|
||||
#ifdef CONFIG_PIPEWIRE
|
||||
strcat(version_string, "-PipeWire");
|
||||
#endif
|
||||
#ifdef CONFIG_SOUNDIO
|
||||
strcat(version_string, "-soundio");
|
||||
@@ -1890,161 +2218,176 @@ char *get_version_string() {
|
||||
return version_string;
|
||||
}
|
||||
|
||||
int64_t generate_zero_frames(char *outp, size_t number_of_frames, sps_format_t format,
|
||||
int with_dither, int64_t random_number_in) {
|
||||
// return the last random number used
|
||||
// assuming the buffer has been assigned
|
||||
|
||||
// add a TPDF dither -- see
|
||||
// http://educypedia.karadimov.info/library/DitherExplained.pdf
|
||||
// and the discussion around https://www.hydrogenaud.io/forums/index.php?showtopic=16963&st=25
|
||||
|
||||
// I think, for a 32 --> 16 bits, the range of
|
||||
// random numbers needs to be from -2^16 to 2^16, i.e. from -65536 to 65536 inclusive, not from
|
||||
// -32768 to +32767
|
||||
|
||||
// Actually, what would be generated here is from -65535 to 65535, i.e. one less on the limits.
|
||||
|
||||
// See the original paper at
|
||||
// http://www.ece.rochester.edu/courses/ECE472/resources/Papers/Lipshitz_1992.pdf
|
||||
// by Lipshitz, Wannamaker and Vanderkooy, 1992.
|
||||
|
||||
int64_t dither_mask = 0;
|
||||
switch (format) {
|
||||
case SPS_FORMAT_S32:
|
||||
case SPS_FORMAT_S32_LE:
|
||||
case SPS_FORMAT_S32_BE:
|
||||
dither_mask = (int64_t)1 << (64 - 32);
|
||||
break;
|
||||
case SPS_FORMAT_S24:
|
||||
case SPS_FORMAT_S24_LE:
|
||||
case SPS_FORMAT_S24_BE:
|
||||
case SPS_FORMAT_S24_3LE:
|
||||
case SPS_FORMAT_S24_3BE:
|
||||
dither_mask = (int64_t)1 << (64 - 24);
|
||||
break;
|
||||
case SPS_FORMAT_S16:
|
||||
case SPS_FORMAT_S16_LE:
|
||||
case SPS_FORMAT_S16_BE:
|
||||
dither_mask = (int64_t)1 << (64 - 16);
|
||||
break;
|
||||
case SPS_FORMAT_S8:
|
||||
case SPS_FORMAT_U8:
|
||||
dither_mask = (int64_t)1 << (64 - 8);
|
||||
break;
|
||||
case SPS_FORMAT_UNKNOWN:
|
||||
die("Unexpected SPS_FORMAT_UNKNOWN while calculating dither mask.");
|
||||
break;
|
||||
case SPS_FORMAT_AUTO:
|
||||
die("Unexpected SPS_FORMAT_AUTO while calculating dither mask.");
|
||||
break;
|
||||
case SPS_FORMAT_INVALID:
|
||||
die("Unexpected SPS_FORMAT_INVALID while calculating dither mask.");
|
||||
break;
|
||||
}
|
||||
dither_mask -= 1;
|
||||
|
||||
int64_t generate_zero_frames(char *outp, size_t number_of_frames, int with_dither,
|
||||
int64_t random_number_in, uint32_t encoded_output_format) {
|
||||
int64_t previous_random_number = random_number_in;
|
||||
char *p = outp;
|
||||
size_t sample_number;
|
||||
r64_lock; // the random number generator is not thread safe, so we need to lock it while using it
|
||||
for (sample_number = 0; sample_number < number_of_frames * 2; sample_number++) {
|
||||
if (encoded_output_format != 0) {
|
||||
unsigned int channels = CHANNELS_FROM_ENCODED_FORMAT(encoded_output_format);
|
||||
sps_format_t format = (sps_format_t)FORMAT_FROM_ENCODED_FORMAT(encoded_output_format);
|
||||
// return the last random number used
|
||||
// assuming the buffer has been assigned
|
||||
|
||||
int64_t hyper_sample = 0;
|
||||
int64_t r = r64i();
|
||||
// add a TPDF dither -- see
|
||||
// http://educypedia.karadimov.info/library/DitherExplained.pdf
|
||||
// and the discussion around https://www.hydrogenaud.io/forums/index.php?showtopic=16963&st=25
|
||||
|
||||
int64_t tpdf = (r & dither_mask) - (previous_random_number & dither_mask);
|
||||
// I think, for a 32 --> 16 bits, the range of
|
||||
// random numbers needs to be from -2^16 to 2^16, i.e. from -65536 to 65536 inclusive, not from
|
||||
// -32768 to +32767
|
||||
|
||||
// add dither if permitted -- no need to check for clipping, as the sample is, uh, zero
|
||||
// Actually, what would be generated here is from -65535 to 65535, i.e. one less on the limits.
|
||||
|
||||
if (with_dither != 0)
|
||||
hyper_sample += tpdf;
|
||||
|
||||
// move the result to the desired position in the int64_t
|
||||
char *op = p;
|
||||
int sample_length; // this is the length of the sample
|
||||
// See the original paper at
|
||||
// http://www.ece.rochester.edu/courses/ECE472/resources/Papers/Lipshitz_1992.pdf
|
||||
// by Lipshitz, Wannamaker and Vanderkooy, 1992.
|
||||
|
||||
int64_t dither_mask = 0;
|
||||
switch (format) {
|
||||
case SPS_FORMAT_S32:
|
||||
hyper_sample >>= (64 - 32);
|
||||
*(int32_t *)op = hyper_sample;
|
||||
sample_length = 4;
|
||||
break;
|
||||
case SPS_FORMAT_S32_LE:
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 32)); // 32 bits, ls byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 32 + 8)); // 32 bits, less significant middle byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 32 + 16)); // 32 bits, more significant middle byte
|
||||
*op = (uint8_t)(hyper_sample >> (64 - 32 + 24)); // 32 bits, ms byte
|
||||
sample_length = 4;
|
||||
break;
|
||||
case SPS_FORMAT_S32_BE:
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 32 + 24)); // 32 bits, ms byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 32 + 16)); // 32 bits, more significant middle byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 32 + 8)); // 32 bits, less significant middle byte
|
||||
*op = (uint8_t)(hyper_sample >> (64 - 32)); // 32 bits, ls byte
|
||||
sample_length = 4;
|
||||
break;
|
||||
case SPS_FORMAT_S24_3LE:
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24)); // 24 bits, ls byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24 + 8)); // 24 bits, middle byte
|
||||
*op = (uint8_t)(hyper_sample >> (64 - 24 + 16)); // 24 bits, ms byte
|
||||
sample_length = 3;
|
||||
break;
|
||||
case SPS_FORMAT_S24_3BE:
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24 + 16)); // 24 bits, ms byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24 + 8)); // 24 bits, middle byte
|
||||
*op = (uint8_t)(hyper_sample >> (64 - 24)); // 24 bits, ls byte
|
||||
sample_length = 3;
|
||||
dither_mask = (int64_t)1 << (64 - 32);
|
||||
break;
|
||||
case SPS_FORMAT_S24:
|
||||
hyper_sample >>= (64 - 24);
|
||||
*(int32_t *)op = hyper_sample;
|
||||
sample_length = 4;
|
||||
break;
|
||||
case SPS_FORMAT_S24_LE:
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24)); // 24 bits, ls byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24 + 8)); // 24 bits, middle byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24 + 16)); // 24 bits, ms byte
|
||||
*op = 0;
|
||||
sample_length = 4;
|
||||
break;
|
||||
case SPS_FORMAT_S24_BE:
|
||||
*op++ = 0;
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24 + 16)); // 24 bits, ms byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24 + 8)); // 24 bits, middle byte
|
||||
*op = (uint8_t)(hyper_sample >> (64 - 24)); // 24 bits, ls byte
|
||||
sample_length = 4;
|
||||
break;
|
||||
case SPS_FORMAT_S16_LE:
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 16));
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 16 + 8)); // 16 bits, ms byte
|
||||
sample_length = 2;
|
||||
break;
|
||||
case SPS_FORMAT_S16_BE:
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 16 + 8)); // 16 bits, ms byte
|
||||
*op = (uint8_t)(hyper_sample >> (64 - 16));
|
||||
sample_length = 2;
|
||||
case SPS_FORMAT_S24_3LE:
|
||||
case SPS_FORMAT_S24_3BE:
|
||||
dither_mask = (int64_t)1 << (64 - 24);
|
||||
break;
|
||||
case SPS_FORMAT_S16:
|
||||
*(int16_t *)op = (int16_t)(hyper_sample >> (64 - 16));
|
||||
sample_length = 2;
|
||||
case SPS_FORMAT_S16_LE:
|
||||
case SPS_FORMAT_S16_BE:
|
||||
dither_mask = (int64_t)1 << (64 - 16);
|
||||
break;
|
||||
case SPS_FORMAT_S8:
|
||||
*op = (int8_t)(hyper_sample >> (64 - 8));
|
||||
sample_length = 1;
|
||||
break;
|
||||
case SPS_FORMAT_U8:
|
||||
*op = 128 + (uint8_t)(hyper_sample >> (64 - 8));
|
||||
sample_length = 1;
|
||||
dither_mask = (int64_t)1 << (64 - 8);
|
||||
break;
|
||||
case SPS_FORMAT_UNKNOWN:
|
||||
die("Unexpected SPS_FORMAT_UNKNOWN while calculating dither mask.");
|
||||
break;
|
||||
case SPS_FORMAT_AUTO:
|
||||
die("Unexpected SPS_FORMAT_AUTO while calculating dither mask.");
|
||||
break;
|
||||
case SPS_FORMAT_INVALID:
|
||||
die("Unexpected SPS_FORMAT_INVALID while calculating dither mask.");
|
||||
break;
|
||||
default:
|
||||
sample_length = 0; // stop a compiler warning
|
||||
die("Unexpected SPS_FORMAT_* with index %d while outputting silence", format);
|
||||
}
|
||||
p += sample_length;
|
||||
previous_random_number = r;
|
||||
dither_mask -= 1;
|
||||
|
||||
char *p = outp;
|
||||
size_t sample_number;
|
||||
r64_lock; // the random number generator is not thread safe, so we need to lock it while using
|
||||
// it
|
||||
for (sample_number = 0; sample_number < number_of_frames * channels; sample_number++) {
|
||||
|
||||
int64_t hyper_sample = 0;
|
||||
int64_t r = r64i();
|
||||
|
||||
int64_t tpdf = (r & dither_mask) - (previous_random_number & dither_mask);
|
||||
|
||||
// add dither if permitted -- no need to check for clipping, as the sample is, uh, zero
|
||||
|
||||
if (with_dither != 0)
|
||||
hyper_sample += tpdf;
|
||||
|
||||
/*
|
||||
{
|
||||
// hack to generate low level white noise instead of adding dither
|
||||
hyper_sample = r;
|
||||
hyper_sample = hyper_sample / (1 << 8); // keep the sign
|
||||
}
|
||||
*/
|
||||
|
||||
// move the result to the desired position in the int64_t
|
||||
char *op = p;
|
||||
int sample_length; // this is the length of the sample
|
||||
|
||||
switch (format) {
|
||||
case SPS_FORMAT_S32:
|
||||
hyper_sample >>= (64 - 32);
|
||||
*(int32_t *)op = hyper_sample;
|
||||
sample_length = 4;
|
||||
break;
|
||||
case SPS_FORMAT_S32_LE:
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 32)); // 32 bits, ls byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 32 + 8)); // 32 bits, less significant middle byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 32 + 16)); // 32 bits, more significant middle byte
|
||||
*op = (uint8_t)(hyper_sample >> (64 - 32 + 24)); // 32 bits, ms byte
|
||||
sample_length = 4;
|
||||
break;
|
||||
case SPS_FORMAT_S32_BE:
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 32 + 24)); // 32 bits, ms byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 32 + 16)); // 32 bits, more significant middle byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 32 + 8)); // 32 bits, less significant middle byte
|
||||
*op = (uint8_t)(hyper_sample >> (64 - 32)); // 32 bits, ls byte
|
||||
sample_length = 4;
|
||||
break;
|
||||
case SPS_FORMAT_S24_3LE:
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24)); // 24 bits, ls byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24 + 8)); // 24 bits, middle byte
|
||||
*op = (uint8_t)(hyper_sample >> (64 - 24 + 16)); // 24 bits, ms byte
|
||||
sample_length = 3;
|
||||
break;
|
||||
case SPS_FORMAT_S24_3BE:
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24 + 16)); // 24 bits, ms byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24 + 8)); // 24 bits, middle byte
|
||||
*op = (uint8_t)(hyper_sample >> (64 - 24)); // 24 bits, ls byte
|
||||
sample_length = 3;
|
||||
break;
|
||||
case SPS_FORMAT_S24:
|
||||
hyper_sample >>= (64 - 24);
|
||||
*(int32_t *)op = hyper_sample;
|
||||
sample_length = 4;
|
||||
break;
|
||||
case SPS_FORMAT_S24_LE:
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24)); // 24 bits, ls byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24 + 8)); // 24 bits, middle byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24 + 16)); // 24 bits, ms byte
|
||||
*op = 0;
|
||||
sample_length = 4;
|
||||
break;
|
||||
case SPS_FORMAT_S24_BE:
|
||||
*op++ = 0;
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24 + 16)); // 24 bits, ms byte
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 24 + 8)); // 24 bits, middle byte
|
||||
*op = (uint8_t)(hyper_sample >> (64 - 24)); // 24 bits, ls byte
|
||||
sample_length = 4;
|
||||
break;
|
||||
case SPS_FORMAT_S16_LE:
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 16));
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 16 + 8)); // 16 bits, ms byte
|
||||
sample_length = 2;
|
||||
break;
|
||||
case SPS_FORMAT_S16_BE:
|
||||
*op++ = (uint8_t)(hyper_sample >> (64 - 16 + 8)); // 16 bits, ms byte
|
||||
*op = (uint8_t)(hyper_sample >> (64 - 16));
|
||||
sample_length = 2;
|
||||
break;
|
||||
case SPS_FORMAT_S16:
|
||||
*(int16_t *)op = (int16_t)(hyper_sample >> (64 - 16));
|
||||
sample_length = 2;
|
||||
break;
|
||||
case SPS_FORMAT_S8:
|
||||
*op = (int8_t)(hyper_sample >> (64 - 8));
|
||||
sample_length = 1;
|
||||
break;
|
||||
case SPS_FORMAT_U8:
|
||||
*op = 128 + (uint8_t)(hyper_sample >> (64 - 8));
|
||||
sample_length = 1;
|
||||
break;
|
||||
default:
|
||||
sample_length = 0; // stop a compiler warning
|
||||
die("Unexpected SPS_FORMAT_* with index %d while outputting silence", format);
|
||||
}
|
||||
p += sample_length;
|
||||
previous_random_number = r;
|
||||
}
|
||||
r64_unlock;
|
||||
} else {
|
||||
debug(1, "No output configuration!");
|
||||
}
|
||||
r64_unlock;
|
||||
return previous_random_number;
|
||||
}
|
||||
|
||||
@@ -2121,17 +2464,6 @@ char *debug_malloc_hex_cstring(void *packet, size_t nread) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// the difference between two unsigned 32-bit modulo values as a signed 32-bit result
|
||||
// now, if the two numbers are constrained to be within 2^(n-1)-1 of one another,
|
||||
// we can use their as a signed 2^n bit number which will be positive
|
||||
// if the first number is the same or "after" the second, and
|
||||
// negative otherwise
|
||||
|
||||
int32_t mod32Difference(uint32_t a, uint32_t b) {
|
||||
int32_t result = a - b;
|
||||
return result;
|
||||
}
|
||||
|
||||
int get_device_id(uint8_t *id, int int_length) {
|
||||
|
||||
uint64_t wait_time = 10000000000L; // wait up to this (ns) long to get a MAC address
|
||||
@@ -2195,3 +2527,114 @@ int get_device_id(uint8_t *id, int int_length) {
|
||||
warn("Can't create a device ID -- no valid MAC address can be found.");
|
||||
return response;
|
||||
}
|
||||
|
||||
char *bnprintf(char *buffer, ssize_t max_bytes, const char *format, ...) {
|
||||
int oldState;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(buffer, max_bytes, format, args);
|
||||
va_end(args);
|
||||
pthread_setcancelstate(oldState, NULL);
|
||||
// debug(1,"bnprintf string is: \"%s\"", buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
int do_pthread_setname(pthread_t *restrict thread, const char *format, ...) {
|
||||
#ifdef COMPILE_FOR_OSX
|
||||
return 0;
|
||||
#else
|
||||
// pthread_setname_np/2 not defined in macOS
|
||||
char actual_name[16];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(actual_name, sizeof(actual_name), format, args);
|
||||
va_end(args);
|
||||
return pthread_setname_np(*thread, actual_name);
|
||||
#endif
|
||||
}
|
||||
|
||||
int named_pthread_create(pthread_t *thread, const pthread_attr_t *attr,
|
||||
void *(*start_routine)(void *), void *arg, const char *format, ...) {
|
||||
char actual_name[16];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(actual_name, sizeof(actual_name), format, args);
|
||||
va_end(args);
|
||||
int response = pthread_create(thread, attr, start_routine, arg);
|
||||
if (response != 0) {
|
||||
debug(1, "error creating thread \"%s\"", actual_name);
|
||||
}
|
||||
#ifndef COMPILE_FOR_OSX
|
||||
else {
|
||||
pthread_setname_np(*thread, actual_name);
|
||||
}
|
||||
#endif
|
||||
return response;
|
||||
}
|
||||
|
||||
int named_pthread_create_with_priority(pthread_t *thread, int priority,
|
||||
void *(*start_routine)(void *), void *arg,
|
||||
const char *format, ...) {
|
||||
|
||||
// if this gets a permissions error, it'll try to create a thread without any special
|
||||
// priority or scheduling
|
||||
|
||||
static int failed_to_set_rt = 0;
|
||||
|
||||
struct sched_param param;
|
||||
pthread_attr_t attr;
|
||||
int ret = 0;
|
||||
|
||||
char actual_name[16];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(actual_name, sizeof(actual_name), format, args);
|
||||
va_end(args);
|
||||
|
||||
/* Initialize pthread attributes (default values) */
|
||||
ret = pthread_attr_init(&attr);
|
||||
if (ret == 0) {
|
||||
/* Set scheduler policy and priority of pthread */
|
||||
ret = pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
|
||||
if (ret == 0) {
|
||||
param.sched_priority = priority;
|
||||
ret = pthread_attr_setschedparam(&attr, ¶m);
|
||||
if (ret == 0) {
|
||||
/* Use scheduling parameters of attr */
|
||||
ret = pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
|
||||
if (ret != 0) {
|
||||
debug(1, "pthread setinheritsched failed");
|
||||
}
|
||||
} else {
|
||||
debug(1, "pthread setschedparam failed");
|
||||
}
|
||||
} else {
|
||||
debug(1, "pthread setschedpolicy failed");
|
||||
}
|
||||
} else {
|
||||
debug(1, "init pthread attributes failed");
|
||||
}
|
||||
// ret == 0 if creating and setting up the attributes was successful
|
||||
if (ret == 0) {
|
||||
ret = pthread_create(thread, &attr, start_routine, arg);
|
||||
pthread_attr_destroy(&attr);
|
||||
}
|
||||
// ret will be non-zero if there was a problem creating the attribute or creating the prioritized
|
||||
// thread
|
||||
if (ret != 0) {
|
||||
ret = pthread_create(thread, NULL, start_routine, arg);
|
||||
if (failed_to_set_rt == 0) {
|
||||
inform("Can not set realtime properties of a thread.");
|
||||
failed_to_set_rt = 1;
|
||||
}
|
||||
}
|
||||
#ifndef COMPILE_FOR_OSX
|
||||
if (ret == 0) {
|
||||
pthread_setname_np(*thread, actual_name);
|
||||
} else {
|
||||
die("named_pthread_create_with_priority failed with error %d", ret);
|
||||
}
|
||||
#endif
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -5,15 +5,11 @@ extern "C" {
|
||||
#ifndef _COMMON_H
|
||||
#define _COMMON_H
|
||||
|
||||
#include <libconfig.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <sys/types.h> // for mode_t
|
||||
#include <unistd.h> // for useconds_t
|
||||
|
||||
#include "audio.h"
|
||||
#include "config.h"
|
||||
#include "definitions.h"
|
||||
#include "mdns.h"
|
||||
@@ -28,6 +24,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_DBUS_INTERFACE) || defined(CONFIG_MPRIS_INTERFACE)
|
||||
#include <glib.h>
|
||||
typedef enum {
|
||||
DBT_system = 0, // use the session bus
|
||||
DBT_session, // use the system bus
|
||||
@@ -37,8 +34,6 @@ 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
|
||||
} type_of_exit_type;
|
||||
|
||||
#define sps_extra_code_output_stalled 32768
|
||||
@@ -58,6 +53,7 @@ typedef enum {
|
||||
|
||||
typedef enum {
|
||||
ST_basic = 0, // straight deletion or insertion of a frame in a 352-frame packet
|
||||
ST_vernier, // interpolate from 352/1024 samples to 353/1025 or 351/1023
|
||||
ST_soxr, // use libsoxr to make a 352 frame packet one frame longer or shorter
|
||||
ST_auto, // use soxr if compiled for it and if the soxr_index is low enough
|
||||
} stuffing_type;
|
||||
@@ -79,6 +75,7 @@ typedef enum {
|
||||
typedef enum {
|
||||
decoder_hammerton = 0,
|
||||
decoder_apple_alac,
|
||||
decoder_ffmpeg_alac,
|
||||
} decoders_supported_type;
|
||||
|
||||
typedef enum {
|
||||
@@ -90,27 +87,81 @@ typedef enum {
|
||||
// the following enum is for the formats recognised -- currently only S16LE is recognised for input,
|
||||
// so these are output only for the present
|
||||
|
||||
// ensure sps_format_sample_size_array and sps_format_description_string_array are in sync with
|
||||
// this!
|
||||
typedef enum {
|
||||
SPS_FORMAT_UNKNOWN = 0,
|
||||
SPS_FORMAT_S8,
|
||||
SPS_FORMAT_LOWEST = SPS_FORMAT_S8,
|
||||
SPS_FORMAT_U8,
|
||||
SPS_FORMAT_S16,
|
||||
SPS_FORMAT_S16_LE,
|
||||
SPS_FORMAT_S16_BE,
|
||||
SPS_FORMAT_S24,
|
||||
SPS_FORMAT_S24_LE,
|
||||
SPS_FORMAT_S24_BE,
|
||||
SPS_FORMAT_S24_3LE,
|
||||
SPS_FORMAT_S24_3BE,
|
||||
SPS_FORMAT_S32,
|
||||
SPS_FORMAT_S32_LE,
|
||||
SPS_FORMAT_S32_BE,
|
||||
SPS_FORMAT_HIGHEST_NATIVE = SPS_FORMAT_S32_BE,
|
||||
SPS_FORMAT_S16,
|
||||
SPS_FORMAT_S24,
|
||||
SPS_FORMAT_S32,
|
||||
SPS_FORMAT_AUTO,
|
||||
SPS_FORMAT_INVALID,
|
||||
} sps_format_t;
|
||||
|
||||
typedef enum {
|
||||
SPS_RATE_UNKNOWN = 0,
|
||||
SPS_RATE_5512,
|
||||
SPS_RATE_LOWEST = SPS_RATE_5512,
|
||||
SPS_RATE_8000,
|
||||
SPS_RATE_11025,
|
||||
SPS_RATE_16000,
|
||||
SPS_RATE_22050,
|
||||
SPS_RATE_32000,
|
||||
SPS_RATE_44100,
|
||||
SPS_RATE_48000,
|
||||
SPS_RATE_64000,
|
||||
SPS_RATE_88200,
|
||||
SPS_RATE_96000,
|
||||
SPS_RATE_176400,
|
||||
SPS_RATE_192000,
|
||||
SPS_RATE_352800,
|
||||
SPS_RATE_384000,
|
||||
SPS_RATE_HIGHEST = SPS_RATE_384000,
|
||||
SPS_RATE_ILLEGAL,
|
||||
} sps_rate_t;
|
||||
|
||||
// these sets omit the _UNKNOWN, _AUTO and _ILLEGAL values
|
||||
#define SPS_FORMAT_SET (((1 << (SPS_FORMAT_HIGHEST_NATIVE + 1)) - 1) - (1 << SPS_FORMAT_UNKNOWN))
|
||||
#define SPS_RATE_SET (((1 << (SPS_RATE_HIGHEST + 1)) - 1) - (1 << SPS_RATE_UNKNOWN))
|
||||
#define SPS_CHANNEL_SET (((1 << (8 + 1)) - 1) - (1 << 0)) // channels 1 to 8, not 0-based!
|
||||
|
||||
#ifndef CONFIG_AIRPLAY_2
|
||||
#define SPS_FORMAT_NON_FFMPEG_SET SPS_FORMAT_SET
|
||||
#define SPS_RATE_NON_FFMPEG_SET \
|
||||
((1 << SPS_RATE_44100) | (1 << SPS_RATE_88200) | (1 << SPS_RATE_176400) | (1 << SPS_RATE_352800))
|
||||
#define SPS_CHANNNEL_NON_FFMPEG_SET (1 << 2)
|
||||
#endif
|
||||
|
||||
// up to 1048576 fps, but must be an even number
|
||||
#define RATE_FROM_ENCODED_FORMAT(encoded_format) (((encoded_format >> 6) & 0x7FFFF) * 2)
|
||||
#define RATE_TO_ENCODED_FORMAT(rate) (((rate / 2) & 0x7FFFF) << 6)
|
||||
|
||||
// up to 127 channels
|
||||
#define CHANNELS_FROM_ENCODED_FORMAT(encoded_format) ((encoded_format >> 25) & 0x7F)
|
||||
#define CHANNELS_TO_ENCODED_FORMAT(channels) ((channels & 0x7F) << 25)
|
||||
|
||||
// up to 64 different SPS_FORMATs
|
||||
#define FORMAT_FROM_ENCODED_FORMAT(encoded_format) (encoded_format & 0x3F)
|
||||
#define FORMAT_TO_ENCODED_FORMAT(format) (format & 0x3F)
|
||||
|
||||
const char *short_format_description(int32_t encoded_format);
|
||||
const char *sps_format_description_string(sps_format_t format);
|
||||
|
||||
unsigned int sps_format_sample_size(sps_format_t format);
|
||||
unsigned int sps_rate_actual_rate(sps_rate_t rate);
|
||||
|
||||
typedef struct {
|
||||
double missing_port_dacp_scan_interval_seconds; // if no DACP port number can be found, check at
|
||||
// these intervals
|
||||
@@ -124,31 +175,25 @@ typedef struct {
|
||||
int endianness;
|
||||
double airplay_volume; // stored here for reloading when necessary
|
||||
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"
|
||||
|
||||
#ifdef CONFIG_PA
|
||||
#ifdef CONFIG_PULSEAUDIO
|
||||
char *pa_server; // the pulseaudio server address that Shairport Sync will play on.
|
||||
char *pa_application_name; // the name under which Shairport Sync shows up as an "Application" in
|
||||
// the Sound Preferences in most desktop Linuxes.
|
||||
// Defaults to "Shairport Sync".
|
||||
// Defaults to "Shairport Sync". Shairport Sync must be playing to see it.
|
||||
|
||||
char *pa_sink; // the name (or id) of the sink that Shairport Sync will play on.
|
||||
#endif
|
||||
#ifdef CONFIG_PW
|
||||
char *pw_application_name; // the name under which Shairport Sync shows up as an "Application" in
|
||||
// the Sound Preferences in most desktop Linuxes.
|
||||
// Defaults to "Shairport Sync".
|
||||
#ifdef CONFIG_PIPEWIRE
|
||||
char *pw_application_name; // the name under which Shairport Sync shows up as an "Application" in
|
||||
// the Sound Preferences in most desktop Linuxes.
|
||||
// Defaults to "Shairport Sync".
|
||||
|
||||
char *pw_node_name; // defaults to the application's name, usually "shairport-sync".
|
||||
char *pw_node_name; // defaults to the application's name, usually "shairport-sync".
|
||||
char *pw_sink_target; // leave this unset if you don't want to change the sink_target.
|
||||
#endif
|
||||
#ifdef CONFIG_METADATA
|
||||
@@ -187,12 +232,10 @@ 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 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 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.
|
||||
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.
|
||||
@@ -232,8 +275,7 @@ typedef struct {
|
||||
int soxr_delay_threshold; // the soxr delay must be less or equal to this for soxr interpolation
|
||||
// to be enabled under the auto setting
|
||||
int decoders_supported;
|
||||
int use_apple_decoder; // set to 1 if you want to use the apple decoder instead of the original by
|
||||
// David Hammerton
|
||||
int decoder_in_use;
|
||||
// char *logfile;
|
||||
// char *errfile;
|
||||
char *configfile;
|
||||
@@ -268,11 +310,13 @@ typedef struct {
|
||||
// before using
|
||||
// sw attenuation
|
||||
volume_control_profile_type volume_control_profile;
|
||||
|
||||
int output_format_auto_requested; // true if the configuration requests auto configuration
|
||||
sps_format_t output_format;
|
||||
int output_rate_auto_requested; // true if the configuration requests auto configuration
|
||||
unsigned int output_rate;
|
||||
int output_rate_auto_requested; // true if the configuration requests auto configuration
|
||||
uint32_t current_output_configuration;
|
||||
// these are the formats/rate and channel configurations permitted by the settings or defaults
|
||||
uint32_t format_set;
|
||||
uint32_t rate_set;
|
||||
uint32_t channel_set;
|
||||
|
||||
#ifdef CONFIG_CONVOLUTION
|
||||
int convolution;
|
||||
@@ -290,13 +334,6 @@ typedef struct {
|
||||
volatile int keep_dac_busy;
|
||||
yna_type use_precision_timing; // defaults to no
|
||||
|
||||
#if defined(CONFIG_DBUS_INTERFACE)
|
||||
dbus_session_type dbus_service_bus_type;
|
||||
#endif
|
||||
#if defined(CONFIG_MPRIS_INTERFACE)
|
||||
dbus_session_type mpris_service_bus_type;
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_METADATA_HUB
|
||||
char *cover_art_cache_dir;
|
||||
int retain_coverart;
|
||||
@@ -324,16 +361,52 @@ typedef struct {
|
||||
// can't use IP numbers as they might be given to different devices
|
||||
// can't get hold of MAC addresses.
|
||||
// can't define the nvll linked list struct here
|
||||
char *firmware_version;
|
||||
// use these in information requests
|
||||
char *model;
|
||||
char *srcvers;
|
||||
char *osvers;
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
uint64_t airplay_features;
|
||||
uint32_t airplay_statusflags;
|
||||
char *airplay_fex; // a base64-encoded version of the airplay_features in little-endian form
|
||||
char *airplay_device_id; // for the Bonjour advertisement and the GETINFO PList
|
||||
char *airplay_pin; // non-NULL, 4 char PIN, if required for pairing
|
||||
char *airplay_pi; // UUID in the Bonjour advertisement and the GETINFO Plist
|
||||
char *airplay_pgid; // UUID in the txtAirPlay data sent on the event channel
|
||||
char *airplay_psi; // type 4 fixed UUID
|
||||
uint8_t airplay_pk[32]; // public key
|
||||
char *pk_string;
|
||||
char *nqptp_shared_memory_interface_name; // client name for nqptp service
|
||||
#endif
|
||||
int unfixable_error_reported; // only report once.
|
||||
|
||||
uint64_t eight_channel_layout; // non-zero means enabled and is a channel layout
|
||||
uint64_t six_channel_layout; // non-zero means enabled and is a channel layout
|
||||
|
||||
int mixdown_enable;
|
||||
uint64_t mixdown_channel_layout; // if mixdown_enable is true, 0 signifies auto, based on number
|
||||
// of channels
|
||||
int output_channel_mapping_enable; // 0 means off, non-zero means on. If on and
|
||||
// output_channel_map_size is 0, use the device's channel map
|
||||
const char *output_channel_map[8]; // names of the output channels
|
||||
unsigned int output_channel_map_size; // number of output channels
|
||||
|
||||
#if defined(CONFIG_DBUS_INTERFACE) || defined(CONFIG_MPRIS_INTERFACE)
|
||||
GMainLoop *glib_worker_loop;
|
||||
// for clean quitting from a dbus interface quit request (from the DBus or MPRIS interfaces)
|
||||
int quit_requested_from_glib_mainloop; // remember that it initialised to zero.
|
||||
|
||||
#if defined(CONFIG_DBUS_INTERFACE)
|
||||
dbus_session_type dbus_service_bus_type;
|
||||
#endif
|
||||
#if defined(CONFIG_MPRIS_INTERFACE)
|
||||
dbus_session_type mpris_service_bus_type;
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
} shairport_cfg;
|
||||
|
||||
uint32_t nctohl(const uint8_t *p); // read 4 characters from *p and do ntohl on them
|
||||
@@ -393,9 +466,34 @@ void _debug_print_buffer(const char *thefilename, const int linenumber, int leve
|
||||
#define inform(...) _inform(__FILE__, __LINE__, __VA_ARGS__)
|
||||
#define debug_print_buffer(...) _debug_print_buffer(__FILE__, __LINE__, __VA_ARGS__)
|
||||
|
||||
// Thanks to https://stackoverflow.com/a/1597129 for the inspiration for this identifier generation
|
||||
#define MAKEUNIQUEID2(x, y) x##y
|
||||
#define MADEID(x, y) MAKEUNIQUEID2(x, y)
|
||||
|
||||
// do X once, and never again until the app is restarted
|
||||
#define once(X) \
|
||||
static int MADEID(once_flag_, __LINE__) = 0; \
|
||||
if (MADEID(once_flag_, __LINE__) == 0) { \
|
||||
X; \
|
||||
MADEID(once_flag_, __LINE__) = 1; \
|
||||
}
|
||||
|
||||
// do X once, and then ignore repeated calls until they stop for more than one second
|
||||
#define once_per_1_second_burst(X) \
|
||||
static uint64_t MADEID(time_, __LINE__) = 0; \
|
||||
int64_t MADEID(interval_, __LINE__) = get_absolute_time_in_ns() - MADEID(time_, __LINE__); \
|
||||
if ((MADEID(time_, __LINE__) == 0) || (MADEID(interval_, __LINE__) > 1000000000L)) \
|
||||
X; \
|
||||
MADEID(time_, __LINE__) = get_absolute_time_in_ns()
|
||||
|
||||
void getErrorText(char *destinationString, size_t destinationStringLength);
|
||||
|
||||
uint8_t *base64_dec(char *input, int *outlen);
|
||||
char *base64_enc(uint8_t *input, int length);
|
||||
|
||||
char *base64_encode_so(const unsigned char *data, size_t input_length, char *encoded_data,
|
||||
size_t *output_length);
|
||||
|
||||
#define RSA_MODE_AUTH (0)
|
||||
#define RSA_MODE_KEY (1)
|
||||
uint8_t *rsa_apply(uint8_t *input, int inlen, int *outlen, int mode);
|
||||
@@ -435,7 +533,15 @@ extern shairport_cfg config;
|
||||
extern config_t config_file_stuff;
|
||||
extern int type_of_exit_cleanup; // normal, emergency, dbus requested...
|
||||
|
||||
extern uint64_t minimum_dac_queue_size;
|
||||
|
||||
int config_set_lookup_bool(config_t *cfg, char *where, int *dst);
|
||||
int check_string_or_list_setting(config_setting_t *setting, const char *item);
|
||||
int check_int_or_list_setting(config_setting_t *setting, const int item);
|
||||
|
||||
unsigned int config_get_string_settings_as_string_array(config_setting_t *setting,
|
||||
const char ***result);
|
||||
unsigned int config_get_int_settings_as_int_array(config_setting_t *setting, int **result);
|
||||
|
||||
void command_start(void);
|
||||
void command_stop(void);
|
||||
@@ -444,7 +550,7 @@ void command_set_volume(double volume);
|
||||
|
||||
int mkpath(const char *path, mode_t mode);
|
||||
|
||||
void shairport_shutdown();
|
||||
void sps_shutdown(type_of_exit_type shutdown_type); // TOE_normal, TOE_emergency, TOE_dbus
|
||||
|
||||
extern sigset_t pselect_sigset;
|
||||
|
||||
@@ -483,6 +589,15 @@ void pthread_cleanup_debug_mutex_unlock(void *arg);
|
||||
|
||||
#define config_unlock pthread_mutex_unlock(&config.lock)
|
||||
|
||||
int do_pthread_setname(pthread_t *restrict thread, const char *format, ...);
|
||||
|
||||
int named_pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr,
|
||||
void *(*start_routine)(void *), void *restrict arg, const char *format,
|
||||
...);
|
||||
int named_pthread_create_with_priority(pthread_t *thread, int priority,
|
||||
void *(*start_routine)(void *), void *arg,
|
||||
const char *format, ...);
|
||||
|
||||
extern pthread_mutex_t r64_mutex;
|
||||
|
||||
#define r64_lock pthread_mutex_lock(&r64_mutex)
|
||||
@@ -491,8 +606,8 @@ extern pthread_mutex_t r64_mutex;
|
||||
|
||||
char *get_version_string(); // mallocs a string space -- remember to free it afterwards
|
||||
|
||||
int64_t generate_zero_frames(char *outp, size_t number_of_frames, sps_format_t format,
|
||||
int with_dither, int64_t random_number_in);
|
||||
int64_t generate_zero_frames(char *outp, size_t number_of_frames, int with_dither,
|
||||
int64_t random_number_in, uint32_t encoded_output_format);
|
||||
|
||||
void malloc_cleanup(void *arg);
|
||||
|
||||
@@ -523,16 +638,10 @@ char *debug_malloc_hex_cstring(void *packet, size_t nread);
|
||||
// analogous to strndup;
|
||||
void *memdup(const void *mem, size_t size);
|
||||
|
||||
// the difference between two unsigned 32-bit modulo values as a signed 32-bit result
|
||||
// now, if the two numbers are constrained to be within 2^(n-1)-1 of one another,
|
||||
// we can use their as a signed 2^n bit number which will be positive
|
||||
// if the first number is the same or "after" the second, and
|
||||
// negative otherwise
|
||||
|
||||
int32_t mod32Difference(uint32_t a, uint32_t b);
|
||||
|
||||
int get_device_id(uint8_t *id, int int_length);
|
||||
|
||||
char *bnprintf(char *buffer, ssize_t max_bytes, const char *format, ...);
|
||||
|
||||
#ifdef CONFIG_USE_GIT_VERSION_STRING
|
||||
extern char git_version_string[];
|
||||
#endif
|
||||
|
||||
+83
-50
@@ -1,18 +1,14 @@
|
||||
# -*- Autoconf -*-
|
||||
# Process this file with autoconf to produce a configure script.
|
||||
|
||||
AC_PREREQ([2.50])
|
||||
AC_INIT([shairport-sync], [4.3.7-dev], [4265913+mikebrady@users.noreply.github.com])
|
||||
AC_INIT([shairport-sync], [5.0-dev], [4265913+mikebrady@users.noreply.github.com])
|
||||
: ${CFLAGS="-O3"}
|
||||
: ${CXXFLAGS="-O3"}
|
||||
AM_INIT_AUTOMAKE([subdir-objects])
|
||||
AC_CONFIG_SRCDIR([shairport.c])
|
||||
AC_CONFIG_HEADERS([config.h])
|
||||
AC_PROG_RANLIB
|
||||
|
||||
AC_CHECK_PROGS([GIT], [git])
|
||||
if test -n "$GIT" && test -e "$srcdir/.git/index" ; then
|
||||
AC_DEFINE([CONFIG_USE_GIT_VERSION_STRING], 1, [Use the version string produced by running 'git describe --dirty'.])
|
||||
fi
|
||||
AM_CONDITIONAL([USE_GIT_VERSION], [test -n "$GIT" && test -e "$srcdir/.git/index" ])
|
||||
AC_SUBST([HOME], [$HOME])
|
||||
|
||||
# Derived from the Avahi configure.ac file
|
||||
# Specifying the OS type, defaulting to linux.
|
||||
@@ -36,6 +32,17 @@ AC_PROG_CXX
|
||||
AC_CHECK_TOOL(AR, ar, :)
|
||||
AC_PROG_INSTALL
|
||||
|
||||
# see if we have git -- not fatal if we don't
|
||||
AC_CHECK_PROGS([GIT], [git])
|
||||
if test -n "$GIT" && test -e "$srcdir/.git/index" ; then
|
||||
AC_DEFINE([CONFIG_USE_GIT_VERSION_STRING], 1, [Use the version string produced by running 'git describe --dirty'.])
|
||||
fi
|
||||
AM_CONDITIONAL([USE_GIT_VERSION], [test -n "$GIT" && test -e "$srcdir/.git/index" ])
|
||||
|
||||
# see if we have xmltoman -- not fatal if we don't
|
||||
AC_CHECK_PROGS([XMLTOMAN], [xmltoman])
|
||||
AM_CONDITIONAL([USE_XMLTOMAN], [ test -n "$XMLTOMAN" ])
|
||||
|
||||
PKG_PROG_PKG_CONFIG([0.9.0])
|
||||
|
||||
# Checks for libraries.
|
||||
@@ -85,7 +92,7 @@ if test "x$with_stdout" = "xyes" ; then
|
||||
fi
|
||||
AM_CONDITIONAL([USE_STDOUT], [test "x$with_stdout" = "xyes"])
|
||||
|
||||
AC_ARG_WITH([pipe],[AS_HELP_STRING([--with-pipe],[include the pipe audio back end])])
|
||||
AC_ARG_WITH([pipe],[AS_HELP_STRING([--with-pipe],[build the unix pipe backend (not the PipeWire backend!) into Shairport Sync])])
|
||||
if test "x$with_pipe" = "xyes" ; then
|
||||
AC_DEFINE([CONFIG_PIPE], 1, [Include an audio backend to output to a unix pipe.])
|
||||
fi
|
||||
@@ -93,24 +100,24 @@ AM_CONDITIONAL([USE_PIPE], [test "x$with_pipe" = "xyes" ])
|
||||
|
||||
# Check to see if we should include the System V initscript
|
||||
|
||||
AC_ARG_WITH([systemv],[AS_HELP_STRING([--with-systemv],[install a System V startup script during a make install])])
|
||||
AM_CONDITIONAL([INSTALL_SYSTEMV], [test "x$with_systemv" = "xyes"])
|
||||
AC_ARG_WITH([systemv-startup],[AS_HELP_STRING([--with-systemv-startup],[install a System V startup script during a make install])])
|
||||
AM_CONDITIONAL([INSTALL_SYSTEMV_STARTUP], [test "x$with_systemv_startup" = "xyes"])
|
||||
|
||||
# Check to see if we should include the systemd stuff to define it as a service
|
||||
|
||||
AC_ARG_WITH([systemd],[AS_HELP_STRING([--with-systemd],[install a systemd startup script during a make install])])
|
||||
AM_CONDITIONAL([INSTALL_SYSTEMD], [test "x$with_systemd" = "xyes"])
|
||||
AC_ARG_WITH([systemd-startup],[AS_HELP_STRING([--with-systemd-startup],[install systemd startup scripts to start Shairport Sync after system startup or after user login])])
|
||||
AM_CONDITIONAL([INSTALL_SYSTEMD_STARTUP], [test "x$with_systemd_startup" = "xyes"])
|
||||
|
||||
AC_ARG_WITH([freebsd-service],[AS_HELP_STRING([--with-freebsd-service],[install a FreeBSD startup script during a make install])])
|
||||
AM_CONDITIONAL([INSTALL_FREEBSD_SERVICE], [test "x$with_freebsd_service" = "xyes"])
|
||||
AC_ARG_WITH([freebsd-startup],[AS_HELP_STRING([--with-freebsd-startup],[install a FreeBSD startup script to start Shairport Sync after system startup])])
|
||||
AM_CONDITIONAL([INSTALL_FREEBSD_STARTUP], [test "x$with_freebsd_startup" = "xyes"])
|
||||
|
||||
AC_ARG_WITH([cygwin-service],[AS_HELP_STRING([--with-cygwin-service],[install a CYGWIN config script during a make install])])
|
||||
AM_CONDITIONAL([INSTALL_CYGWIN_SERVICE], [test "x$with_cygwin_service" = "xyes"])
|
||||
AC_ARG_WITH([cygwin-startup],[AS_HELP_STRING([--with-cygwin-startup],[install a CYGWIN startup script to start Shairport Sync after startup])])
|
||||
AM_CONDITIONAL([INSTALL_CYGWIN_STARTUP], [test "x$with_cygwin_startup" = "xyes"])
|
||||
|
||||
AC_ARG_WITH([external-mdns],[AS_HELP_STRING([--with-external-mdns],[support the use of 'avahi-publish-service' or 'mDNSPublish' to advertise the service on Bonjour/ZeroConf])])
|
||||
if test "x$with_external_mdns" = xyes ; then
|
||||
AC_MSG_RESULT(include external mdns support)
|
||||
AC_DEFINE([CONFIG_EXTERNAL_MDNS], 1, [Use 'avahi-publish-service' or 'mDNSPublish' to advertise.])
|
||||
AC_DEFINE([CONFIG_EXTERNAL_MDNS], 1, [Use 'avahi-publish-service' or 'mDNSPublish' to advertise the service on Bonjour/ZeroConf])
|
||||
fi
|
||||
AM_CONDITIONAL([USE_EXTERNAL_MDNS], [test "x$with_external_mdns" = "xyes" ])
|
||||
|
||||
@@ -131,18 +138,6 @@ fi
|
||||
AC_ARG_WITH([configfiles],[AS_HELP_STRING([--with-configfiles],[install configuration files during a make install])], ,[with_configfiles=yes])
|
||||
AM_CONDITIONAL([INSTALL_CONFIG_FILES], [test "x$with_configfiles" = "xyes"])
|
||||
|
||||
# Look for Apple ALAC flag
|
||||
AC_ARG_WITH(apple-alac,[AS_HELP_STRING([--with-apple-alac],[include support for the Apple ALAC decoder])])
|
||||
if test "x${with_apple_alac}" = "xyes" ; then
|
||||
AC_DEFINE([CONFIG_APPLE_ALAC], 1, [Include support for using the Apple ALAC Decoder])
|
||||
if test "x${with_pkg_config}" = xyes ; then
|
||||
PKG_CHECK_MODULES([alac], [alac], [LIBS="${alac_LIBS} ${LIBS}"], AC_MSG_ERROR(Apple ALAC Decoder support requires the ALAC library. See https://github.com/mikebrady/alac.))
|
||||
else
|
||||
AC_CHECK_LIB([alac], [BitBufferInit], , AC_MSG_ERROR(Apple ALAC Decoder support requires the ALAC library. See https://github.com/mikebrady/alac.))
|
||||
fi
|
||||
fi
|
||||
AM_CONDITIONAL([USE_APPLE_ALAC], [test "x${with_apple_alac}" = "xyes"])
|
||||
|
||||
# Look for piddir flag
|
||||
AC_ARG_WITH(piddir, [AS_HELP_STRING([--with-piddir=<pathname>],[Specify a pathname to a directory in which to write the PID file.])])
|
||||
if test "x${with_piddir}" != "x" ; then
|
||||
@@ -279,7 +274,7 @@ AM_CONDITIONAL([USE_JACK], [test "x$with_jack" = "xyes"])
|
||||
AC_ARG_WITH(sndio, [AS_HELP_STRING([--with-sndio],[choose SNDIO API support])])
|
||||
if test "x$with_sndio" = "xyes" ; then
|
||||
AC_DEFINE([CONFIG_SNDIO], 1, [Include a sndio-compatible audio backend.])
|
||||
AC_CHECK_LIB([sndio], [sio_open], , AC_MSG_ERROR(SNDIO support requires the sndio library -- libsndio-dev suggested))
|
||||
AC_CHECK_LIB([sndio], [sio_open], , AC_MSG_ERROR(SNDIO support requires the sndio library -- sndio suggested for FreeBSD and libsndio-dev for Debian))
|
||||
fi
|
||||
AM_CONDITIONAL([USE_SNDIO], [test "x$with_sndio" = "xyes"])
|
||||
|
||||
@@ -307,34 +302,34 @@ fi
|
||||
AM_CONDITIONAL([USE_SOUNDIO], [test "x$with_soundio" = "xyes"])
|
||||
|
||||
# Look for pulseaudio flag
|
||||
AC_ARG_WITH(pa, [AS_HELP_STRING([--with-pa],[choose PulseAudio support.])])
|
||||
if test "x$with_pa" = "xyes" ; then
|
||||
AC_DEFINE([CONFIG_PA], 1, [Include PulseAudio support.])
|
||||
AC_ARG_WITH(pulseaudio, [AS_HELP_STRING([--with-pulseaudio],[build the PulseAudio backend into Shairport Sync])])
|
||||
if test "x$with_pulseaudio" = "xyes" ; then
|
||||
AC_DEFINE([CONFIG_PULSEAUDIO], 1, [Include PulseAudio support.])
|
||||
if test "x${with_pkg_config}" = xyes ; then
|
||||
PKG_CHECK_MODULES(
|
||||
[PULSEAUDIO], [libpulse >= 0.9.2],
|
||||
[LIBS="${PULSEAUDIO_LIBS} ${LIBS}"],[AC_MSG_ERROR(PulseAudio support requires the libpulse-dev library!)])
|
||||
[LIBS="${PULSEAUDIO_LIBS} ${LIBS}"],[AC_MSG_ERROR(PulseAudio support requires the libpulse library -- libpulse-dev suggested!)])
|
||||
else
|
||||
AC_CHECK_LIB([pulse-simple], [pa_simple_new], , AC_MSG_ERROR(PulseAudio support requires the libpulse library!))
|
||||
AC_CHECK_LIB([pulse], [pa_stream_peek], , AC_MSG_ERROR(PulseAudio support requires the libpulse-dev library.))
|
||||
AC_CHECK_LIB([pulse-simple], [pa_simple_new], , AC_MSG_ERROR(PulseAudio support requires the libpulse library -- libpulse-dev suggested!))
|
||||
AC_CHECK_LIB([pulse], [pa_stream_peek], , AC_MSG_ERROR(PulseAudio support requires the libpulse library -- libpulse-dev suggested!))
|
||||
fi
|
||||
fi
|
||||
AM_CONDITIONAL([USE_PA], [test "x$with_pa" = "xyes"])
|
||||
AM_CONDITIONAL([USE_PULSEAUDIO], [test "x$with_pulseaudio" = "xyes"])
|
||||
|
||||
# Look for pipewire flag
|
||||
AC_ARG_WITH(pw, [AS_HELP_STRING([--with-pw],[choose Pipewire support.])])
|
||||
if test "x$with_pw" = "xyes" ; then
|
||||
AC_DEFINE([CONFIG_PW], 1, [Include Pipewire support.])
|
||||
AC_ARG_WITH(pipewire, [AS_HELP_STRING([--with-pipewire],[build the Pipewire backend into Shairport Sync.])])
|
||||
if test "x$with_pipewire" = "xyes" ; then
|
||||
AC_DEFINE([CONFIG_PIPEWIRE], 1, [Include Pipewire support.])
|
||||
if test "x${with_pkg_config}" = xyes ; then
|
||||
PKG_CHECK_MODULES(
|
||||
[PIPEWIRE], [libpipewire-0.3 >= 0.3.24],
|
||||
PIPEWIRE, libpipewire-0.3 >= 1.0.5,
|
||||
[CFLAGS="${PIPEWIRE_CFLAGS} ${CFLAGS} -Wno-missing-field-initializers" LIBS="${PIPEWIRE_LIBS} ${LIBS}"],
|
||||
[AC_MSG_ERROR(Pipewire support requires the libpipewire library -- libpipewire-0.3-dev suggested!)])
|
||||
[AC_MSG_ERROR(Pipewire support requires libpipewire-0.3 version 1.0.5 or later -- libpipewire-0.3-dev suggested!)])
|
||||
else
|
||||
AC_CHECK_LIB([pipewire], [pw_stream_queue_buffer], , AC_MSG_ERROR(Pipewire support requires the libpipewire library -- libpipewire-0.3-dev suggested!))
|
||||
AC_CHECK_LIB([pipewire], [pw_stream_queue_buffer], , AC_MSG_ERROR(Pipewire support requires libpipewire-0.3 version 1.1.0 or later -- libpipewire-0.3-dev suggested!))
|
||||
fi
|
||||
fi
|
||||
AM_CONDITIONAL([USE_PW], [test "x$with_pw" = "xyes"])
|
||||
AM_CONDITIONAL([USE_PIPEWIRE], [test "x$with_pipewire" = "xyes"])
|
||||
|
||||
# Look for Convolution flag
|
||||
AC_ARG_WITH(convolution, [AS_HELP_STRING([--with-convolution],[choose audio DSP convolution support])])
|
||||
@@ -418,6 +413,8 @@ AM_CONDITIONAL([USE_METADATA], [test "x$with_metadata" = "xyes"])
|
||||
AC_ARG_WITH(airplay-2, [AS_HELP_STRING([--with-airplay-2],[Build for AirPlay 2])])
|
||||
if test "x$with_airplay_2" = "xyes" ; then
|
||||
AC_DEFINE([CONFIG_AIRPLAY_2], 1, [Build for AirPlay 2])
|
||||
AC_CHECK_PROG(PLISTUTIL_CHECK,plistutil,yes)
|
||||
AS_IF([test x"$PLISTUTIL_CHECK" != x"yes"], [AC_MSG_ERROR([plistutil can not be found. Please install plistutil for building for AirPlay 2. Maybe it's in the apt package libplist-utils.])])
|
||||
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.])])
|
||||
LIBPLIST_PACKAGE=libplist
|
||||
@@ -445,6 +442,16 @@ if test "x$with_airplay_2" = "xyes" ; then
|
||||
[AC_MSG_ERROR([Airplay 2 support requires libgcrypt -- libgcrypt-dev suggested])]
|
||||
)
|
||||
])
|
||||
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"])
|
||||
|
||||
# Look for ffmpeg flag
|
||||
|
||||
AC_ARG_WITH(ffmpeg, [AS_HELP_STRING([--with-ffmpeg],[Build with FFMPEG support (recommended)])])
|
||||
if test "x$with_ffmpeg" = "xyes" -o "x$with_airplay_2" = "xyes" ; then
|
||||
using_ffmpeg=true
|
||||
AC_DEFINE([CONFIG_FFMPEG], 1, [Include support for FFmpeg decoders, resampler and utilities.])
|
||||
PKG_CHECK_MODULES(
|
||||
[libavutil], [libavutil],
|
||||
[CFLAGS="${libavutil_CFLAGS} ${CFLAGS}" LIBS="${libavutil_LIBS} ${LIBS}"],
|
||||
@@ -486,10 +493,31 @@ if test "x$with_airplay_2" = "xyes" ; then
|
||||
[AC_MSG_ERROR([AirPlay 2 support requires the uuid library -- uuid-dev suggested])]
|
||||
)]
|
||||
)
|
||||
else
|
||||
using_ffmpeg=false
|
||||
fi
|
||||
AM_CONDITIONAL([USE_AIRPLAY_2], [test "x$with_airplay_2" = "xyes"])
|
||||
|
||||
if test "x${with_systemd}" = xyes ; then
|
||||
if test $using_ffmpeg = false ; then
|
||||
AC_DEFINE([CONFIG_HAMMERTON], 1, [Include support for using the ALAC Decoder by David Hammerton (deprecated)])
|
||||
fi
|
||||
AM_CONDITIONAL([USE_HAMMERTON], [test $using_ffmpeg = false])
|
||||
|
||||
# Look for Apple ALAC flag
|
||||
AC_ARG_WITH(apple-alac,[AS_HELP_STRING([--with-apple-alac],[include support for the Apple ALAC decoder (deprecated)])])
|
||||
if test "x${with_apple_alac}" = "xyes" ; then
|
||||
if test $using_ffmpeg = true ; then
|
||||
AC_MSG_RESULT([Note: the FFMPEG decoder, included in the build, supercedes the Apple ALAC decoder, which is deprecated. Consider omitting the Apple ALAC decoder.])
|
||||
fi
|
||||
AC_DEFINE([CONFIG_APPLE_ALAC], 1, [Include support for using the Apple ALAC Decoder (deprecated)])
|
||||
if test "x${with_pkg_config}" = xyes ; then
|
||||
PKG_CHECK_MODULES([alac], [alac], [LIBS="${alac_LIBS} ${LIBS}"], AC_MSG_ERROR(Apple ALAC Decoder support requires the ALAC library. See https://github.com/mikebrady/alac.))
|
||||
else
|
||||
AC_CHECK_LIB([alac], [BitBufferInit], , AC_MSG_ERROR(Apple ALAC Decoder support requires the ALAC library. See https://github.com/mikebrady/alac.))
|
||||
fi
|
||||
fi
|
||||
AM_CONDITIONAL([USE_APPLE_ALAC], [test "x${with_apple_alac}" = "xyes"])
|
||||
|
||||
if test "x${with_systemd_startup}" = xyes ; then
|
||||
# Find systemd unit dir
|
||||
AC_ARG_WITH([systemdsystemunitdir],
|
||||
[AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd service files])],,
|
||||
@@ -498,8 +526,8 @@ if test "x${with_systemd}" = xyes ; then
|
||||
def_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd)
|
||||
|
||||
AS_IF([test "x$def_systemdsystemunitdir" = "x"],
|
||||
[AS_IF([test "x$with_systemdsystemunitdir" = "xyes"],
|
||||
[AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])])
|
||||
[AS_IF([test "x$with_systemdsystemunitdir" = "xyes" -o "x$with_systemdsystemunitdir" = "xauto"],
|
||||
[AC_MSG_ERROR([pkg-config unable to query systemd package -- install systemd-dev package suggested])])
|
||||
with_systemdsystemunitdir=no],
|
||||
[with_systemdsystemunitdir="$def_systemdsystemunitdir"])])
|
||||
AS_IF([test "x$with_systemdsystemunitdir" != "xno"],
|
||||
@@ -530,6 +558,11 @@ AC_FUNC_FORK
|
||||
AC_CHECK_FUNCS([atexit clock_gettime gethostname inet_ntoa memchr memmove memset mkfifo pow select socket stpcpy strcasecmp strchr strdup strerror strstr strtol strtoul])
|
||||
|
||||
# Note -- there are AC_CONFIG_FILES directives further back, conditional on Avahi
|
||||
AC_CONFIG_FILES([Makefile])
|
||||
|
||||
# The man directory will not be recursively made unless you set the SUBDIR in Makefile.am.
|
||||
# But leave this as is to enable a distclean to be done.
|
||||
|
||||
AC_CONFIG_FILES([Makefile man/Makefile])
|
||||
|
||||
AC_CONFIG_FILES([scripts/shairport-sync],[chmod +x scripts/shairport-sync])
|
||||
AC_OUTPUT
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DACP protocol handler. This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2017 -- 2020
|
||||
* Copyright (c) Mike Brady 2017--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -74,9 +74,22 @@ struct HttpResponse {
|
||||
};
|
||||
|
||||
void *response_realloc(__attribute__((unused)) void *opaque, void *ptr, int size) {
|
||||
void *t = realloc(ptr, size);
|
||||
if ((t == NULL) && (size != 0))
|
||||
debug(1, "Response realloc of size %d failed!", size);
|
||||
void *t = NULL;
|
||||
if (size == 0) {
|
||||
// debug(1, "response_realloc of size 0 to ptr %" PRIxPTR " requested, and a pointer of NULL was
|
||||
// returned.", size, ptr);
|
||||
free(ptr);
|
||||
} else {
|
||||
if (ptr == NULL) {
|
||||
t = malloc(size);
|
||||
// debug(1, "response_realloc of size %d to a NULL pointer was requested, and a pointer of %"
|
||||
// PRIxPTR " was returned.", size, t);
|
||||
} else {
|
||||
t = realloc(ptr, size);
|
||||
}
|
||||
if (t == NULL)
|
||||
debug(1, "response_realloc of size %d to ptr %" PRIxPTR " failed!", size, ptr);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
@@ -104,8 +117,7 @@ void response_body(void *opaque, const char *data, int size) {
|
||||
static void
|
||||
response_header(__attribute__((unused)) void *opaque, __attribute__((unused)) const char *ckey,
|
||||
__attribute__((unused)) int nkey, __attribute__((unused)) const char *cvalue,
|
||||
__attribute__((unused)) int nvalue) { /* example doesn't care about headers */
|
||||
}
|
||||
__attribute__((unused)) int nvalue) { /* example doesn't care about headers */ }
|
||||
|
||||
static void response_code(void *opaque, int code) {
|
||||
struct HttpResponse *response = (struct HttpResponse *)opaque;
|
||||
@@ -255,7 +267,7 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) {
|
||||
ssize_t wresp = send(sockfd, message, strlen(message), 0);
|
||||
if (wresp == -1) {
|
||||
char errorstring[1024];
|
||||
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
|
||||
getErrorText((char *)errorstring, sizeof(errorstring));
|
||||
debug(2, "dacp_send_command: write error %d: \"%s\".", errno, (char *)errorstring);
|
||||
struct linger so_linger;
|
||||
so_linger.l_onoff = 1; // "true"
|
||||
@@ -272,7 +284,7 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) {
|
||||
|
||||
response.body = malloc(2048); // it can resize this if necessary
|
||||
response.malloced_size = 2048;
|
||||
pthread_cleanup_push(malloc_cleanup, response.body);
|
||||
pthread_cleanup_push(malloc_cleanup, &response.body);
|
||||
|
||||
struct http_roundtripper rt;
|
||||
http_init(&rt, responseFuncs, &response);
|
||||
@@ -291,9 +303,11 @@ int dacp_send_command(const char *command, char **body, ssize_t *bodysize) {
|
||||
if (ndata <= 0) {
|
||||
if (ndata == -1) {
|
||||
char errorstring[1024];
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wunused-result"
|
||||
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
|
||||
debug(2, "dacp_send_command: receiving error %d: \"%s\".", errno,
|
||||
(char *)errorstring);
|
||||
#pragma GCC diagnostic pop
|
||||
debug(2, "dacp_send_command: receiving error %d: \"%s\".", errno, errorstring);
|
||||
struct linger so_linger;
|
||||
so_linger.l_onoff = 1; // "true"
|
||||
so_linger.l_linger = 0;
|
||||
@@ -461,7 +475,7 @@ void set_dacp_server_information(rtsp_conn_info *conn) {
|
||||
|
||||
void dacp_monitor_port_update_callback(char *dacp_id, uint16_t port) {
|
||||
debug_mutex_lock(&dacp_server_information_lock, 500000, 2);
|
||||
debug(2,
|
||||
debug(3,
|
||||
"dacp_monitor_port_update_callback with Remote ID \"%s\", target ID \"%s\" and port "
|
||||
"number %d.",
|
||||
dacp_id, dacp_server.dacp_id, port);
|
||||
@@ -489,6 +503,8 @@ void dacp_monitor_port_update_callback(char *dacp_id, uint16_t port) {
|
||||
}
|
||||
|
||||
void *dacp_monitor_thread_code(__attribute__((unused)) void *na) {
|
||||
// #include <syscall.h>
|
||||
// debug(1, "dacp_monitor_thread_code PID %d", syscall(SYS_gettid));
|
||||
int scan_index = 0;
|
||||
int always_use_revision_number_1 = 0;
|
||||
// char server_reply[10000];
|
||||
@@ -982,7 +998,7 @@ void dacp_monitor_start() {
|
||||
|
||||
memset(&dacp_server, 0, sizeof(dacp_server_record));
|
||||
|
||||
pthread_create(&dacp_monitor_thread, NULL, dacp_monitor_thread_code, NULL);
|
||||
named_pthread_create(&dacp_monitor_thread, NULL, dacp_monitor_thread_code, NULL, "dacp");
|
||||
dacp_monitor_initialised = 1;
|
||||
}
|
||||
|
||||
|
||||
+105
-124
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2018 -- 2022
|
||||
* Copyright (c) Mike Brady 2018--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -54,7 +54,7 @@ ShairportSyncDiagnostics *shairportSyncDiagnosticsSkeleton = NULL;
|
||||
ShairportSyncRemoteControl *shairportSyncRemoteControlSkeleton = NULL;
|
||||
ShairportSyncAdvancedRemoteControl *shairportSyncAdvancedRemoteControlSkeleton = NULL;
|
||||
|
||||
guint ownerID = 0;
|
||||
static guint ownerID = 0;
|
||||
|
||||
void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) void *userdata) {
|
||||
char response[100];
|
||||
@@ -126,6 +126,24 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused))
|
||||
}
|
||||
}
|
||||
|
||||
if (argc->output_format) {
|
||||
// debug(1, "Check output format");
|
||||
th = shairport_sync_get_output_format(shairportSyncSkeleton);
|
||||
if ((th == NULL) || (strcasecmp(th, argc->output_format) != 0)) {
|
||||
// debug(1, "Output format string should be changed");
|
||||
shairport_sync_set_output_format(shairportSyncSkeleton, argc->output_format);
|
||||
}
|
||||
}
|
||||
|
||||
if (argc->source_format) {
|
||||
// debug(1, "Check source format");
|
||||
th = shairport_sync_get_source_format(shairportSyncSkeleton);
|
||||
if ((th == NULL) || (strcasecmp(th, argc->source_format) != 0)) {
|
||||
// debug(1, "Source format string should be changed");
|
||||
shairport_sync_set_source_format(shairportSyncSkeleton, argc->source_format);
|
||||
}
|
||||
}
|
||||
|
||||
switch (argc->player_state) {
|
||||
case PS_NOT_AVAILABLE:
|
||||
shairport_sync_remote_control_set_player_state(shairportSyncRemoteControlSkeleton,
|
||||
@@ -223,7 +241,7 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused))
|
||||
}
|
||||
|
||||
// Build the metadata array
|
||||
debug(2, "Build metadata");
|
||||
// debug(2, "Build metadata");
|
||||
GVariantBuilder *dict_builder = g_variant_builder_new(G_VARIANT_TYPE("a{sv}"));
|
||||
|
||||
// Add in the artwork URI if it exists.
|
||||
@@ -442,10 +460,10 @@ gboolean notify_elapsed_time_callback(ShairportSyncDiagnostics *skeleton,
|
||||
// debug(1, "\"notify_elapsed_time_callback\" called.");
|
||||
if (shairport_sync_diagnostics_get_elapsed_time(skeleton)) {
|
||||
config.debugger_show_elapsed_time = 1;
|
||||
debug(1, ">> start including elapsed time in logs");
|
||||
debug(1, ">> include elapsed time in logs");
|
||||
} else {
|
||||
config.debugger_show_elapsed_time = 0;
|
||||
debug(1, ">> stop including elapsed time in logs");
|
||||
debug(1, ">> do not include elapsed time in logs");
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
@@ -455,10 +473,10 @@ gboolean notify_delta_time_callback(ShairportSyncDiagnostics *skeleton,
|
||||
// debug(1, "\"notify_delta_time_callback\" called.");
|
||||
if (shairport_sync_diagnostics_get_delta_time(skeleton)) {
|
||||
config.debugger_show_relative_time = 1;
|
||||
debug(1, ">> start including delta time in logs");
|
||||
debug(1, ">> include delta time in logs");
|
||||
} else {
|
||||
config.debugger_show_relative_time = 0;
|
||||
debug(1, ">> stop including delta time in logs");
|
||||
debug(1, ">> do not include delta time in logs");
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
@@ -468,10 +486,10 @@ gboolean notify_file_and_line_callback(ShairportSyncDiagnostics *skeleton,
|
||||
// debug(1, "\"notify_file_and_line_callback\" called.");
|
||||
if (shairport_sync_diagnostics_get_file_and_line(skeleton)) {
|
||||
config.debugger_show_file_and_line = 1;
|
||||
debug(1, ">> start including file and line in logs");
|
||||
debug(1, ">> include file and line in logs");
|
||||
} else {
|
||||
config.debugger_show_file_and_line = 0;
|
||||
debug(1, ">> stop including file and line in logs");
|
||||
debug(1, ">> do not include file and line in logs");
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
@@ -480,12 +498,12 @@ gboolean notify_statistics_callback(ShairportSyncDiagnostics *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
// debug(1, "\"notify_statistics_callback\" called.");
|
||||
if (shairport_sync_diagnostics_get_statistics(skeleton)) {
|
||||
debug(1, ">> start logging statistics");
|
||||
debug(1, ">> log statistics");
|
||||
if (config.statistics_requested == 0)
|
||||
statistics_row = 0; // redraw the header line
|
||||
config.statistics_requested = 1;
|
||||
} else {
|
||||
debug(1, ">> stop logging statistics");
|
||||
debug(1, ">> do not log statistics");
|
||||
config.statistics_requested = 0;
|
||||
}
|
||||
return TRUE;
|
||||
@@ -496,11 +514,11 @@ gboolean notify_verbosity_callback(ShairportSyncDiagnostics *skeleton,
|
||||
gint th = shairport_sync_diagnostics_get_verbosity(skeleton);
|
||||
if ((th >= 0) && (th <= 3)) {
|
||||
if (th == 0)
|
||||
debug(1, ">> log verbosity set to %d.", th);
|
||||
debug(1, ">> set log verbosity to %d.", th);
|
||||
if (((debuglev == 0) && (th != 0)) || ((debuglev != 0) && (th == 0)))
|
||||
statistics_row = 0; // if the debug level changes, redraw the header line
|
||||
debuglev = th;
|
||||
debug(1, ">> log verbosity set to %d.", th);
|
||||
debug(1, ">> set log verbosity to %d.", th);
|
||||
} else {
|
||||
debug(1, ">> invalid log verbosity: %d. Ignored.", th);
|
||||
shairport_sync_diagnostics_set_verbosity(skeleton, debuglev);
|
||||
@@ -512,10 +530,10 @@ gboolean notify_disable_standby_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
// debug(1, "\"notify_disable_standby_callback\" called.");
|
||||
if (shairport_sync_get_disable_standby(skeleton)) {
|
||||
debug(1, ">> activating disable standby");
|
||||
debug(1, ">> disable standby mode");
|
||||
config.keep_dac_busy = 1;
|
||||
} else {
|
||||
debug(1, ">> deactivating disable standby");
|
||||
debug(1, ">> do not disable standby mode");
|
||||
config.keep_dac_busy = 0;
|
||||
}
|
||||
return TRUE;
|
||||
@@ -526,12 +544,12 @@ gboolean notify_convolution_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
// debug(1, "\"notify_convolution_callback\" called.");
|
||||
if (shairport_sync_get_convolution(skeleton)) {
|
||||
debug(1, ">> activating convolution");
|
||||
debug(1, ">> activate convolution filter");
|
||||
config.convolution = 1;
|
||||
config.convolver_valid =
|
||||
convolver_init(config.convolution_ir_file, config.convolution_max_length);
|
||||
} else {
|
||||
debug(1, ">> deactivating convolution");
|
||||
debug(1, ">> deactivate convolution filter");
|
||||
config.convolution = 0;
|
||||
}
|
||||
return TRUE;
|
||||
@@ -550,7 +568,7 @@ gboolean notify_convolution_gain_callback(ShairportSync *skeleton,
|
||||
|
||||
gdouble th = shairport_sync_get_convolution_gain(skeleton);
|
||||
if ((th <= 0.0) && (th >= -100.0)) {
|
||||
debug(1, ">> setting convolution gain to %f.", th);
|
||||
debug(1, ">> set convolution gain to %f.", th);
|
||||
config.convolution_gain = th;
|
||||
} else {
|
||||
debug(1, ">> invalid convolution gain: %f. Ignored.", th);
|
||||
@@ -566,24 +584,23 @@ gboolean notify_convolution_gain_callback(__attribute__((unused)) ShairportSync
|
||||
}
|
||||
#endif
|
||||
#ifdef CONFIG_CONVOLUTION
|
||||
gboolean notify_convolution_impulse_response_file_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused))
|
||||
gpointer user_data) {
|
||||
gboolean
|
||||
notify_convolution_impulse_response_file_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
char *th = (char *)shairport_sync_get_convolution_impulse_response_file(skeleton);
|
||||
if (config.convolution_ir_file)
|
||||
free(config.convolution_ir_file);
|
||||
config.convolution_ir_file = strdup(th);
|
||||
debug(1, ">> setting configuration impulse response filter file to \"%s\".",
|
||||
debug(1, ">> set configuration impulse response filter file to \"%s\".",
|
||||
config.convolution_ir_file);
|
||||
config.convolver_valid =
|
||||
convolver_init(config.convolution_ir_file, config.convolution_max_length);
|
||||
return TRUE;
|
||||
}
|
||||
#else
|
||||
gboolean notify_convolution_impulse_response_file_callback(__attribute__((unused))
|
||||
ShairportSync *skeleton,
|
||||
__attribute__((unused))
|
||||
gpointer user_data) {
|
||||
gboolean
|
||||
notify_convolution_impulse_response_file_callback(__attribute__((unused)) ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
__attribute__((unused)) char *th =
|
||||
(char *)shairport_sync_get_convolution_impulse_response_file(skeleton);
|
||||
return TRUE;
|
||||
@@ -594,10 +611,10 @@ gboolean notify_loudness_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
// debug(1, "\"notify_loudness_callback\" called.");
|
||||
if (shairport_sync_get_loudness(skeleton)) {
|
||||
debug(1, ">> activating loudness");
|
||||
debug(1, ">> activate loudness filter");
|
||||
config.loudness = 1;
|
||||
} else {
|
||||
debug(1, ">> deactivating loudness");
|
||||
debug(1, ">> deactivate loudness filter");
|
||||
config.loudness = 0;
|
||||
}
|
||||
return TRUE;
|
||||
@@ -607,7 +624,7 @@ gboolean notify_loudness_threshold_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
gdouble th = shairport_sync_get_loudness_threshold(skeleton);
|
||||
if ((th <= 0.0) && (th >= -100.0)) {
|
||||
debug(1, ">> setting loudness threshold to %f.", th);
|
||||
debug(1, ">> set loudness threshold to %f.", th);
|
||||
config.loudness_reference_volume_db = th;
|
||||
} else {
|
||||
debug(1, ">> invalid loudness threshold: %f. Ignored.", th);
|
||||
@@ -620,7 +637,7 @@ gboolean notify_drift_tolerance_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
gdouble dt = shairport_sync_get_drift_tolerance(skeleton);
|
||||
if ((dt >= 0.0) && (dt <= 2.0)) {
|
||||
debug(1, ">> setting drift tolerance to %f seconds.", dt);
|
||||
debug(1, ">> set drift tolerance to %f seconds.", dt);
|
||||
config.tolerance = dt;
|
||||
} else {
|
||||
debug(1, ">> invalid drift tolerance: %f seconds. Ignored.", dt);
|
||||
@@ -633,7 +650,7 @@ gboolean notify_volume_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
gdouble iv = shairport_sync_get_volume(skeleton);
|
||||
if (((iv >= -30.0) && (iv <= 0.0)) || (iv == -144.0)) {
|
||||
debug(2, ">> setting volume to %7.4f.", iv);
|
||||
debug(2, ">> set volume to %7.4f.", iv);
|
||||
|
||||
pthread_rwlock_rdlock(&principal_conn_lock); // don't let the principal_conn be changed
|
||||
pthread_cleanup_push(rwlock_unlock, (void *)&principal_conn_lock);
|
||||
@@ -645,7 +662,6 @@ gboolean notify_volume_callback(ShairportSync *skeleton,
|
||||
}
|
||||
pthread_cleanup_pop(1); // release the principal_conn lock
|
||||
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);
|
||||
@@ -688,46 +704,52 @@ gboolean notify_disable_standby_mode_callback(ShairportSync *skeleton,
|
||||
gboolean notify_alacdecoder_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
char *th = (char *)shairport_sync_get_alacdecoder(skeleton);
|
||||
#ifdef CONFIG_APPLE_ALAC
|
||||
if (strcasecmp(th, "hammerton") == 0)
|
||||
config.use_apple_decoder = 0;
|
||||
else if (strcasecmp(th, "apple") == 0)
|
||||
config.use_apple_decoder = 1;
|
||||
|
||||
if ((strcasecmp(th, "hammerton") == 0) &&
|
||||
((config.decoders_supported & (1 << decoder_hammerton)) != 0))
|
||||
config.decoder_in_use = 1 << decoder_hammerton;
|
||||
else if ((strcasecmp(th, "apple") == 0) &&
|
||||
((config.decoders_supported & (1 << decoder_apple_alac)) != 0))
|
||||
config.decoder_in_use = 1 << decoder_apple_alac;
|
||||
else if ((strcasecmp(th, "ffmpeg") == 0) &&
|
||||
((config.decoders_supported & (1 << decoder_ffmpeg_alac)) != 0))
|
||||
config.decoder_in_use = 1 << decoder_ffmpeg_alac;
|
||||
else {
|
||||
warn("An unrecognised ALAC decoder: \"%s\" was requested via D-Bus interface.", th);
|
||||
if (config.use_apple_decoder == 0)
|
||||
shairport_sync_set_alacdecoder(skeleton, "hammerton");
|
||||
else
|
||||
shairport_sync_set_alacdecoder(skeleton, "apple");
|
||||
}
|
||||
// debug(1,"Using the %s ALAC decoder.", ((config.use_apple_decoder==0) ? "Hammerton" : "Apple"));
|
||||
#else
|
||||
if (strcasecmp(th, "hammerton") == 0) {
|
||||
config.use_apple_decoder = 0;
|
||||
// debug(1,"Using the Hammerton ALAC decoder.");
|
||||
} else {
|
||||
warn("An unrecognised ALAC decoder: \"%s\" was requested via D-Bus interface. (Possibly "
|
||||
warn("An unrecognised or unsupported ALAC decoder: \"%s\" was requested via D-Bus interface. "
|
||||
"(Possibly "
|
||||
"support for this decoder was not compiled "
|
||||
"into this version of Shairport Sync.)",
|
||||
th);
|
||||
shairport_sync_set_alacdecoder(skeleton, "hammerton");
|
||||
}
|
||||
#endif
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean notify_interpolation_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
char *th = (char *)shairport_sync_get_interpolation(skeleton);
|
||||
#ifdef CONFIG_SOXR
|
||||
// #ifdef CONFIG_SOXR
|
||||
if (strcasecmp(th, "basic") == 0)
|
||||
config.packet_stuffing = ST_basic;
|
||||
#ifdef CONFIG_SOXR
|
||||
else if (strcasecmp(th, "soxr") == 0)
|
||||
config.packet_stuffing = ST_soxr;
|
||||
#endif
|
||||
else if (strcasecmp(th, "auto") == 0)
|
||||
config.packet_stuffing = ST_auto;
|
||||
else if (strcasecmp(th, "vernier") == 0)
|
||||
config.packet_stuffing = ST_vernier;
|
||||
else {
|
||||
#ifdef CONFIG_SOXR
|
||||
warn("An unrecognised interpolation method: \"%s\" was requested via the D-Bus interface.", th);
|
||||
#else
|
||||
if (strcasecmp(th, "soxr") == 0) {
|
||||
warn("Soxr interpolation is not supported in this edition of Shairport Sync.");
|
||||
} else {
|
||||
warn("An unrecognised interpolation method: \"%s\" was requested via the D-Bus interface.", th);
|
||||
}
|
||||
#endif
|
||||
// set the shairport_sync_set_interpolation on the D-Bus interface back to what it is in the setting.
|
||||
switch (config.packet_stuffing) {
|
||||
case ST_basic:
|
||||
shairport_sync_set_interpolation(skeleton, "basic");
|
||||
@@ -735,26 +757,18 @@ gboolean notify_interpolation_callback(ShairportSync *skeleton,
|
||||
case ST_soxr:
|
||||
shairport_sync_set_interpolation(skeleton, "soxr");
|
||||
break;
|
||||
case ST_vernier:
|
||||
shairport_sync_set_interpolation(skeleton, "vernier");
|
||||
break;
|
||||
case ST_auto:
|
||||
shairport_sync_set_interpolation(skeleton, "auto");
|
||||
break;
|
||||
default:
|
||||
debug(1, "This should never happen!");
|
||||
shairport_sync_set_interpolation(skeleton, "basic");
|
||||
debug(1, "This should never happen, but defaulting to \"vernier\" interpolation!");
|
||||
shairport_sync_set_interpolation(skeleton, "vernier");
|
||||
break;
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (strcasecmp(th, "basic") == 0)
|
||||
config.packet_stuffing = ST_basic;
|
||||
else {
|
||||
warn("An unrecognised interpolation method: \"%s\" was requested via the D-Bus interface. "
|
||||
"(Possibly support for this method was not compiled "
|
||||
"into this version of Shairport Sync.)",
|
||||
th);
|
||||
shairport_sync_set_interpolation(skeleton, "basic");
|
||||
}
|
||||
#endif
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -840,9 +854,9 @@ gboolean notify_loop_status_callback(ShairportSyncAdvancedRemoteControl *skeleto
|
||||
static gboolean on_handle_quit(ShairportSync *skeleton, GDBusMethodInvocation *invocation,
|
||||
__attribute__((unused)) const gchar *command,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
debug(1, ">> quit requested");
|
||||
type_of_exit_cleanup = TOE_dbus; // request an exit cleanup that is compatible with dbus
|
||||
exit(EXIT_SUCCESS);
|
||||
debug(1, ">> quit request...");
|
||||
config.quit_requested_from_glib_mainloop = 1;
|
||||
g_main_loop_quit(config.glib_worker_loop);
|
||||
shairport_sync_complete_quit(skeleton, invocation);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -880,11 +894,9 @@ static gboolean on_handle_drop_session(ShairportSync *skeleton, GDBusMethodInvoc
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean on_handle_set_frame_position_update_interval(ShairportSync *skeleton,
|
||||
GDBusMethodInvocation *invocation,
|
||||
const gdouble seconds,
|
||||
__attribute__((unused))
|
||||
gpointer user_data) {
|
||||
static gboolean on_handle_set_frame_position_update_interval(
|
||||
ShairportSync *skeleton, GDBusMethodInvocation *invocation, const gdouble seconds,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
debug(1, ">> set frame position update interval to %.6f.", seconds);
|
||||
config.metadata_progress_interval = seconds;
|
||||
shairport_sync_complete_set_frame_position_update_interval(skeleton, invocation);
|
||||
@@ -894,8 +906,7 @@ static gboolean on_handle_set_frame_position_update_interval(ShairportSync *skel
|
||||
static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
|
||||
// debug(1, "Shairport Sync native D-Bus interface \"%s\" acquired on the %s bus.", name,
|
||||
// (config.dbus_service_bus_type == DBT_session) ? "session" : "system");
|
||||
debug(2, "Shairport Sync native D-Bus interface \"%s\" acquired on the %s bus.", name, (config.dbus_service_bus_type == DBT_session) ? "session" : "system");
|
||||
|
||||
shairportSyncSkeleton = shairport_sync_skeleton_new();
|
||||
g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(shairportSyncSkeleton), connection,
|
||||
@@ -1012,19 +1023,16 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
|
||||
shairport_sync_set_drift_tolerance(SHAIRPORT_SYNC(shairportSyncSkeleton), config.tolerance);
|
||||
shairport_sync_set_volume(SHAIRPORT_SYNC(shairportSyncSkeleton), config.airplay_volume);
|
||||
|
||||
#ifdef CONFIG_APPLE_ALAC
|
||||
if (config.use_apple_decoder == 0) {
|
||||
if ((config.decoder_in_use & (1 << decoder_hammerton)) != 0) {
|
||||
shairport_sync_set_alacdecoder(SHAIRPORT_SYNC(shairportSyncSkeleton), "hammerton");
|
||||
debug(1, ">> ALACDecoder set to \"hammerton\"");
|
||||
} else {
|
||||
} else if ((config.decoder_in_use & (1 << decoder_apple_alac)) != 0) {
|
||||
shairport_sync_set_alacdecoder(SHAIRPORT_SYNC(shairportSyncSkeleton), "apple");
|
||||
debug(1, ">> ALACDecoder set to \"apple\"");
|
||||
} else if ((config.decoder_in_use & (1 << decoder_ffmpeg_alac)) != 0) {
|
||||
shairport_sync_set_alacdecoder(SHAIRPORT_SYNC(shairportSyncSkeleton), "ffmpeg");
|
||||
debug(1, ">> ALACDecoder set to \"ffmpeg\"");
|
||||
}
|
||||
#else
|
||||
shairport_sync_set_alacdecoder(SHAIRPORT_SYNC(shairportSyncSkeleton), "hammerton");
|
||||
debug(1, ">> ALACDecoder set to \"hammerton\"");
|
||||
|
||||
#endif
|
||||
|
||||
shairport_sync_set_active(SHAIRPORT_SYNC(shairportSyncSkeleton), FALSE);
|
||||
debug(1, ">> Active set to \"false\"");
|
||||
@@ -1046,28 +1054,19 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
|
||||
debug(1, "invalid disable_standby mode!");
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_SOXR
|
||||
if (config.packet_stuffing == ST_basic) {
|
||||
shairport_sync_set_interpolation(SHAIRPORT_SYNC(shairportSyncSkeleton), "basic");
|
||||
debug(1, ">> interpolation set to \"basic\" (soxr support built in)");
|
||||
debug(1, ">> interpolation set to \"basic\"");
|
||||
} else if (config.packet_stuffing == ST_auto) {
|
||||
shairport_sync_set_interpolation(SHAIRPORT_SYNC(shairportSyncSkeleton), "auto");
|
||||
debug(1, ">> interpolation set to \"auto\" (soxr support built in)");
|
||||
debug(1, ">> interpolation set to \"auto\"");
|
||||
} else if (config.packet_stuffing == ST_vernier) {
|
||||
shairport_sync_set_interpolation(SHAIRPORT_SYNC(shairportSyncSkeleton), "vernier");
|
||||
debug(1, ">> interpolation set to \"vernier\"");
|
||||
} else {
|
||||
shairport_sync_set_interpolation(SHAIRPORT_SYNC(shairportSyncSkeleton), "soxr");
|
||||
debug(1, ">> interpolation set to \"soxr\"");
|
||||
}
|
||||
#else
|
||||
if (config.packet_stuffing == ST_basic) {
|
||||
shairport_sync_set_interpolation(SHAIRPORT_SYNC(shairportSyncSkeleton), "basic");
|
||||
debug(1, ">> interpolation set to \"basic\" (no soxr support)");
|
||||
} else if (config.packet_stuffing == ST_auto) {
|
||||
shairport_sync_set_interpolation(SHAIRPORT_SYNC(shairportSyncSkeleton), "auto");
|
||||
debug(1, ">> interpolation set to \"auto\" (no soxr support)");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (config.volume_control_profile == VCP_standard)
|
||||
shairport_sync_set_volume_control_profile(SHAIRPORT_SYNC(shairportSyncSkeleton), "standard");
|
||||
else if (config.volume_control_profile == VCP_dasl_tapered)
|
||||
@@ -1103,9 +1102,6 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
|
||||
#endif
|
||||
|
||||
shairport_sync_set_service_name(SHAIRPORT_SYNC(shairportSyncSkeleton), config.service_name);
|
||||
shairport_sync_set_output_rate(SHAIRPORT_SYNC(shairportSyncSkeleton), config.output_rate);
|
||||
shairport_sync_set_output_format(SHAIRPORT_SYNC(shairportSyncSkeleton),
|
||||
sps_format_description_string(config.output_format));
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
shairport_sync_set_protocol(SHAIRPORT_SYNC(shairportSyncSkeleton), "AirPlay 2");
|
||||
@@ -1177,30 +1173,12 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
|
||||
service_is_running = 1;
|
||||
}
|
||||
|
||||
static void on_dbus_name_lost_again(__attribute__((unused)) GDBusConnection *connection,
|
||||
static void on_dbus_name_lost(__attribute__((unused)) GDBusConnection *connection,
|
||||
__attribute__((unused)) const gchar *name,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
warn("could not acquire a Shairport Sync native D-Bus interface \"%s\" on the %s bus.", name,
|
||||
(config.dbus_service_bus_type == DBT_session) ? "session" : "system");
|
||||
}
|
||||
|
||||
static void on_dbus_name_lost(__attribute__((unused)) GDBusConnection *connection,
|
||||
__attribute__((unused)) const gchar *name,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
// debug(1, "could not acquire a Shairport Sync native D-Bus interface \"%s\" on the %s bus --
|
||||
// will try adding the process "
|
||||
// "number to the end of it.",
|
||||
// name, (config.dbus_service_bus_type == DBT_session) ? "session" : "system");
|
||||
pid_t pid = getpid();
|
||||
char interface_name[256] = "";
|
||||
snprintf(interface_name, sizeof(interface_name), "org.gnome.ShairportSync.i%d", pid);
|
||||
GBusType dbus_bus_type = G_BUS_TYPE_SYSTEM;
|
||||
if (config.dbus_service_bus_type == DBT_session)
|
||||
dbus_bus_type = G_BUS_TYPE_SESSION;
|
||||
// debug(1, "Looking for a Shairport Sync native D-Bus interface \"%s\" on the %s bus.",
|
||||
// interface_name,(config.dbus_service_bus_type == DBT_session) ? "session" : "system");
|
||||
g_bus_own_name(dbus_bus_type, interface_name, G_BUS_NAME_OWNER_FLAGS_NONE, NULL,
|
||||
on_dbus_name_acquired, on_dbus_name_lost_again, NULL, NULL);
|
||||
ownerID = 0;
|
||||
}
|
||||
|
||||
int start_dbus_service() {
|
||||
@@ -1208,16 +1186,19 @@ int start_dbus_service() {
|
||||
GBusType dbus_bus_type = G_BUS_TYPE_SYSTEM;
|
||||
if (config.dbus_service_bus_type == DBT_session)
|
||||
dbus_bus_type = G_BUS_TYPE_SESSION;
|
||||
// debug(1, "Looking for a Shairport Sync native D-Bus interface \"org.gnome.ShairportSync\" on
|
||||
// the %s bus.",(config.dbus_service_bus_type == DBT_session) ? "session" : "system");
|
||||
debug(1,
|
||||
"Looking for a Shairport Sync native D-Bus interface \"org.gnome.ShairportSync\" on the %s "
|
||||
"bus.",
|
||||
(config.dbus_service_bus_type == DBT_session) ? "session" : "system");
|
||||
ownerID = g_bus_own_name(dbus_bus_type, "org.gnome.ShairportSync", G_BUS_NAME_OWNER_FLAGS_NONE,
|
||||
NULL, on_dbus_name_acquired, on_dbus_name_lost, NULL, NULL);
|
||||
debug(2, "ownerID: %d.", ownerID);
|
||||
return 0; // this is just to quieten a compiler warning
|
||||
}
|
||||
|
||||
void stop_dbus_service() {
|
||||
debug(2, "stopping dbus service");
|
||||
if (ownerID) {
|
||||
debug(2, "stopping dbus service -- unowning ownerID %d.", ownerID);
|
||||
g_bus_unown_name(ownerID);
|
||||
} else if (service_is_running != 0) {
|
||||
debug(1, "Zero OwnerID for running \"org.gnome.ShairportSync\" dbus service.");
|
||||
|
||||
+100
-73
@@ -1,44 +1,51 @@
|
||||
ARG NQPTP_BRANCH=main
|
||||
ARG FFMPEG_BRANCH=release/7.1
|
||||
ARG SHAIRPORT_SYNC_BRANCH=.
|
||||
ARG ALPINE_VERSION=20250108
|
||||
|
||||
FROM alpine:3.20 AS builder
|
||||
|
||||
RUN apk -U add \
|
||||
alsa-lib-dev \
|
||||
autoconf \
|
||||
automake \
|
||||
avahi-dev \
|
||||
build-base \
|
||||
dbus \
|
||||
ffmpeg-dev \
|
||||
git \
|
||||
libconfig-dev \
|
||||
libgcrypt-dev \
|
||||
libplist-dev \
|
||||
libressl-dev \
|
||||
libsndfile-dev \
|
||||
libsodium-dev \
|
||||
libtool \
|
||||
pipewire-dev \
|
||||
mosquitto-dev \
|
||||
popt-dev \
|
||||
pulseaudio-dev \
|
||||
soxr-dev \
|
||||
xxd
|
||||
|
||||
##### ALAC #####
|
||||
FROM builder AS alac
|
||||
RUN git clone --depth=1 https://github.com/mikebrady/alac
|
||||
WORKDIR /alac
|
||||
RUN autoreconf -i
|
||||
RUN ./configure
|
||||
##### FFMPEGLITE #####
|
||||
FROM alpine:$ALPINE_VERSION AS ffmpeglite
|
||||
RUN apk -U add build-base git nasm pkgconf
|
||||
RUN mkdir -p /ffmpeg_sources /ffmpeg_build
|
||||
WORKDIR /ffmpeg_sources
|
||||
ARG FFMPEG_BRANCH
|
||||
RUN git clone --depth=1 -b "$FFMPEG_BRANCH" https://git.ffmpeg.org/ffmpeg.git
|
||||
WORKDIR /ffmpeg_sources/ffmpeg
|
||||
RUN ./configure \
|
||||
--prefix="/ffmpeg_build" \
|
||||
--extra-libs="-lpthread -lm" \
|
||||
--ld="g++" \
|
||||
--disable-static \
|
||||
--enable-shared \
|
||||
--enable-gpl \
|
||||
--disable-programs \
|
||||
--disable-everything \
|
||||
--disable-doc \
|
||||
--disable-iconv \
|
||||
--disable-avdevice \
|
||||
--disable-avfilter \
|
||||
--disable-swscale \
|
||||
--disable-network \
|
||||
--disable-iamf \
|
||||
--disable-pixelutils \
|
||||
--disable-postproc \
|
||||
--enable-decoder=alac \
|
||||
--enable-decoder=aac
|
||||
RUN make -j $(nproc)
|
||||
RUN make install
|
||||
WORKDIR /
|
||||
##### ALAC END #####
|
||||
##### FFMPEGLITE END #####
|
||||
|
||||
|
||||
##### NQPTP #####
|
||||
FROM builder AS nqptp
|
||||
FROM alpine:$ALPINE_VERSION AS nqptp
|
||||
RUN apk -U add \
|
||||
musl-dev \
|
||||
autoconf \
|
||||
automake \
|
||||
build-base \
|
||||
git \
|
||||
linux-headers
|
||||
|
||||
ARG NQPTP_BRANCH
|
||||
RUN git clone --depth=1 -b "$NQPTP_BRANCH" https://github.com/mikebrady/nqptp
|
||||
WORKDIR /nqptp
|
||||
@@ -49,19 +56,52 @@ WORKDIR /
|
||||
##### NQPTP END #####
|
||||
|
||||
##### SPS #####
|
||||
# Note: apple-alac requires alac build first.
|
||||
FROM alac AS shairport-sync
|
||||
ARG SHAIRPORT_SYNC_BRANCH
|
||||
FROM alpine:$ALPINE_VERSION AS shairport-sync
|
||||
RUN apk -U add \
|
||||
pkgconf \
|
||||
alsa-lib-dev \
|
||||
autoconf \
|
||||
automake \
|
||||
avahi-dev \
|
||||
build-base \
|
||||
dbus \
|
||||
git \
|
||||
libconfig-dev \
|
||||
libgcrypt-dev \
|
||||
libplist-dev \
|
||||
openssl-dev \
|
||||
libsndfile-dev \
|
||||
libsodium-dev \
|
||||
libtool \
|
||||
pipewire-dev \
|
||||
mosquitto-dev \
|
||||
popt-dev \
|
||||
pulseaudio-dev \
|
||||
soxr-dev \
|
||||
libplist-util \
|
||||
xxd
|
||||
|
||||
ARG SHAIRPORT_SYNC_BRANCH
|
||||
WORKDIR /shairport-sync
|
||||
COPY . .
|
||||
RUN git checkout "$SHAIRPORT_SYNC_BRANCH"
|
||||
RUN rm -rf build
|
||||
# the above three lines or the following...
|
||||
# RUN git clone --depth=1 -b "SHAIRPORT_SYNC_BRANCH" https://github.com/mikebrady/sps-private shairport-sync
|
||||
|
||||
COPY --from=ffmpeglite /ffmpeg_build/lib/ /usr/local/lib
|
||||
COPY --from=ffmpeglite /ffmpeg_build/lib/pkgconfig/*.pc /usr/local/lib/pkgconfig/
|
||||
COPY --from=ffmpeglite /ffmpeg_build/include/ /usr/local/include
|
||||
|
||||
# RUN git checkout "$SHAIRPORT_SYNC_BRANCH"
|
||||
# the following is to remove any build directory that might be there
|
||||
# not needed if the source is a clone of the repository
|
||||
# RUN rm -rf build
|
||||
WORKDIR /shairport-sync/build
|
||||
RUN autoreconf -i ../
|
||||
RUN CFLAGS="-O3" CXXFLAGS="-O3" ../configure --sysconfdir=/etc --with-alsa --with-pa --with-soxr --with-avahi --with-ssl=openssl \
|
||||
RUN ../configure --sysconfdir=/etc --with-alsa --with-pulseaudio --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 --with-pw
|
||||
--with-convolution --with-pipewire
|
||||
RUN make -j $(nproc)
|
||||
RUN DESTDIR=install make install
|
||||
WORKDIR /
|
||||
@@ -72,17 +112,15 @@ FROM scratch AS files
|
||||
|
||||
# Add run script that will start SPS
|
||||
COPY --chmod=755 ./docker/run.sh ./run.sh
|
||||
COPY ./docker/etc/s6-overlay/s6-rc.d /etc/s6-overlay/s6-rc.d
|
||||
COPY ./docker/etc/pulse /etc/pulse
|
||||
##### END STATIC FILES #####
|
||||
|
||||
##### BUILD FILES #####
|
||||
FROM scratch AS build-files
|
||||
|
||||
COPY --from=ffmpeglite /ffmpeg_build/lib/ /usr/local/lib
|
||||
COPY --from=shairport-sync /shairport-sync/build/install/usr/local/bin/shairport-sync /usr/local/bin/shairport-sync
|
||||
COPY --from=shairport-sync /shairport-sync/build/install/usr/local/share/man/man1 /usr/share/man/man1
|
||||
COPY --from=nqptp /nqptp/nqptp /usr/local/bin/nqptp
|
||||
COPY --from=alac /usr/local/lib/libalac.* /usr/local/lib/
|
||||
COPY --from=shairport-sync /shairport-sync/build/install/etc/shairport-sync.conf /etc/
|
||||
COPY --from=shairport-sync /shairport-sync/build/install/etc/shairport-sync.conf.sample /etc/
|
||||
COPY --from=shairport-sync /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/
|
||||
@@ -90,53 +128,42 @@ COPY --from=shairport-sync /shairport-sync/build/install/etc/dbus-1/system.d/sha
|
||||
##### END BUILD FILES #####
|
||||
|
||||
# Shairport Sync Runtime System
|
||||
FROM crazymax/alpine-s6:3.20-3.2.0.2
|
||||
|
||||
ENV S6_CMD_WAIT_FOR_SERVICES=1
|
||||
ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0
|
||||
|
||||
FROM alpine:$ALPINE_VERSION
|
||||
COPY --from=build-files / /
|
||||
RUN apk -U add \
|
||||
alsa-lib \
|
||||
avahi \
|
||||
avahi-tools \
|
||||
dbus \
|
||||
ffmpeg \
|
||||
glib \
|
||||
less \
|
||||
less-doc \
|
||||
libconfig \
|
||||
libgcrypt \
|
||||
libplist \
|
||||
pipewire \
|
||||
libpulse \
|
||||
libressl3.8-libcrypto \
|
||||
libsndfile \
|
||||
libcrypto3 \
|
||||
libsodium \
|
||||
libuuid \
|
||||
pipewire \
|
||||
man-pages \
|
||||
mandoc \
|
||||
mosquitto \
|
||||
popt \
|
||||
soxr \
|
||||
curl
|
||||
curl && \
|
||||
rm -rfv /lib/apk/db/* && \
|
||||
rm -rfv /etc/avahi/services/*.service && \
|
||||
rm -rf /usr/share/man && \
|
||||
addgroup shairport-sync && \
|
||||
adduser -D shairport-sync -G shairport-sync && \
|
||||
addgroup -g 29 docker_audio && \
|
||||
addgroup shairport-sync docker_audio && \
|
||||
addgroup shairport-sync audio && \
|
||||
mkdir -p /run/dbus
|
||||
|
||||
RUN rm -rfv /lib/apk/db/* && \
|
||||
rm -rfv /etc/avahi/services/*.service && \
|
||||
addgroup shairport-sync && \
|
||||
adduser -D shairport-sync -G shairport-sync && \
|
||||
addgroup -g 29 docker_audio && \
|
||||
addgroup shairport-sync docker_audio && \
|
||||
addgroup shairport-sync audio && \
|
||||
mkdir -p /run/dbus
|
||||
# Removed anything we don't need.
|
||||
# Removed any statically-defined Avahi services, e.g. SSH and SFTP
|
||||
|
||||
# Remove anything we don't need.
|
||||
# Remove any statically-defined Avahi services, e.g. SSH and SFTP
|
||||
|
||||
# Create non-root user for running the container -- running as the user 'shairport-sync' also allows
|
||||
# Created 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
|
||||
# Add the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff
|
||||
# Added the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff
|
||||
|
||||
COPY --from=files / /
|
||||
COPY --from=build-files / /
|
||||
|
||||
ENTRYPOINT ["/init","./run.sh"]
|
||||
ENTRYPOINT ["./run.sh"]
|
||||
|
||||
+9
-4
@@ -19,13 +19,13 @@ See the `docker-compose.yaml` file in this folder for an example.
|
||||
To run the latest release of Shairport Sync, which provides AirPlay 2 service:
|
||||
|
||||
```
|
||||
$ docker run -d --restart unless-stopped --net host --device /dev/snd \
|
||||
$ docker run -d --cap-add=SYS_NICE --restart unless-stopped --net host --device /dev/snd \
|
||||
mikebrady/shairport-sync:latest
|
||||
```
|
||||
To run the classic version:
|
||||
|
||||
```
|
||||
$ docker run -d --restart unless-stopped --net host --device /dev/snd \
|
||||
$ docker run -d --cap-add=SYS_NICE --restart unless-stopped --net host --device /dev/snd \
|
||||
mikebrady/shairport-sync:latest-classic
|
||||
```
|
||||
|
||||
@@ -34,13 +34,18 @@ $ docker run -d --restart unless-stopped --net host --device /dev/snd \
|
||||
Command line options will be passed to Shairport Sync. Here is an example:
|
||||
|
||||
```
|
||||
$ docker run -d --restart unless-stopped --net host --device /dev/snd \
|
||||
$ docker run -d --cap-add=SYS_NICE --restart unless-stopped --net host --device /dev/snd \
|
||||
mikebrady/shairport-sync:latest \
|
||||
-v --statistics -a DenSystem -- -d hw:0 -c PCM
|
||||
```
|
||||
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.
|
||||
The image is built with PipeWire and PulseAudio backend support. To use it, refer to [`docker-compose.yaml`](docker-compose.yaml) for required environment variables and mounts.
|
||||
|
||||
To use the PipeWire backend, set the backend to `pipewire` via either command line option `-o pipewire ` or the `output_backend` field in the `general` section of the configuration file.
|
||||
|
||||
Similarly, to use the PulseAudio backend, set the backend to `pulseaudio` via either command line option `-o pulseaudio ` or the `output_backend` field in the `general` section of the configuration file.
|
||||
For use with PulseAudio, 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)).
|
||||
|
||||
## Configuration File
|
||||
|
||||
|
||||
+98
-68
@@ -1,15 +1,45 @@
|
||||
# Classic Shairport-Sync build. Note: the only decoder in FFmpeg is ALAC.
|
||||
|
||||
ARG SHAIRPORT_SYNC_BRANCH=.
|
||||
ARG FFMPEG_BRANCH=release/7.1
|
||||
ARG ALPINE_VERSION=20250108
|
||||
|
||||
FROM alpine:3.20 AS builder
|
||||
##### FFMPEGLITE #####
|
||||
FROM alpine:$ALPINE_VERSION AS ffmpeglite
|
||||
RUN apk -U add build-base git nasm pkgconf
|
||||
RUN mkdir -p /ffmpeg_sources /ffmpeg_build
|
||||
WORKDIR /ffmpeg_sources
|
||||
ARG FFMPEG_BRANCH
|
||||
RUN git clone --depth=1 -b "$FFMPEG_BRANCH" https://git.ffmpeg.org/ffmpeg.git
|
||||
WORKDIR /ffmpeg_sources/ffmpeg
|
||||
RUN ./configure \
|
||||
--prefix="/ffmpeg_build" \
|
||||
--extra-libs="-lpthread -lm" \
|
||||
--ld="g++" \
|
||||
--disable-static \
|
||||
--enable-shared \
|
||||
--enable-gpl \
|
||||
--disable-programs \
|
||||
--disable-everything \
|
||||
--disable-doc \
|
||||
--disable-iconv \
|
||||
--disable-avdevice \
|
||||
--disable-avfilter \
|
||||
--disable-swscale \
|
||||
--disable-network \
|
||||
--disable-iamf \
|
||||
--disable-pixelutils \
|
||||
--disable-postproc \
|
||||
--enable-decoder=alac
|
||||
RUN make
|
||||
RUN make install
|
||||
##### FFMPEGLITE END #####
|
||||
|
||||
# Classic (aka AirPlay 1) Build
|
||||
|
||||
# Check required arguments exist. These will be provided by the Github Action
|
||||
# Workflow and are required to ensure the correct branches are being used.
|
||||
ARG SHAIRPORT_SYNC_BRANCH
|
||||
RUN test -n "$SHAIRPORT_SYNC_BRANCH"
|
||||
##### SPS #####
|
||||
FROM alpine:$ALPINE_VERSION AS shairport-sync
|
||||
|
||||
RUN apk -U add \
|
||||
pkgconf \
|
||||
alsa-lib-dev \
|
||||
autoconf \
|
||||
automake \
|
||||
@@ -18,96 +48,96 @@ RUN apk -U add \
|
||||
dbus \
|
||||
git \
|
||||
libconfig-dev \
|
||||
openssl-dev \
|
||||
libsndfile-dev \
|
||||
libtool \
|
||||
mbedtls-dev \
|
||||
pipewire-dev \
|
||||
mosquitto-dev \
|
||||
popt-dev \
|
||||
pulseaudio-dev \
|
||||
soxr-dev
|
||||
soxr-dev \
|
||||
xxd
|
||||
|
||||
##### ALAC #####
|
||||
RUN git clone https://github.com/mikebrady/alac
|
||||
WORKDIR /alac
|
||||
RUN autoreconf -i
|
||||
RUN ./configure
|
||||
RUN make -j $(nproc)
|
||||
RUN make install
|
||||
WORKDIR /
|
||||
##### ALAC END #####
|
||||
|
||||
##### SPS #####
|
||||
ARG SHAIRPORT_SYNC_BRANCH
|
||||
WORKDIR /shairport-sync
|
||||
COPY . .
|
||||
RUN git checkout "$SHAIRPORT_SYNC_BRANCH"
|
||||
RUN rm -rf build
|
||||
# the above three lines or the following...
|
||||
# RUN git clone --depth=1 -b "SHAIRPORT_SYNC_BRANCH" https://github.com/mikebrady/sps-private shairport-sync
|
||||
|
||||
COPY --from=ffmpeglite /ffmpeg_build/lib/ /usr/local/lib
|
||||
COPY --from=ffmpeglite /ffmpeg_build/lib/pkgconfig/*.pc /usr/local/lib/pkgconfig/
|
||||
COPY --from=ffmpeglite /ffmpeg_build/include/ /usr/local/include
|
||||
|
||||
# RUN git checkout "$SHAIRPORT_SYNC_BRANCH"
|
||||
# the following is to remove any build directory that might be there
|
||||
# not needed if the source is a clone of the repository
|
||||
# RUN rm -rf build
|
||||
WORKDIR /shairport-sync/build
|
||||
RUN autoreconf -i ../
|
||||
RUN CFLAGS="-O3" CXXFLAGS="-O3" ../configure --sysconfdir=/etc --with-alsa --with-pa --with-soxr --with-avahi --with-ssl=mbedtls \
|
||||
--with-metadata --with-dummy --with-pipe --with-dbus-interface \
|
||||
RUN ../configure --sysconfdir=/etc --with-alsa --with-pulseaudio --with-soxr --with-avahi --with-ssl=openssl \
|
||||
--with-ffmpeg --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)
|
||||
--with-convolution --with-pipewire
|
||||
RUN make
|
||||
RUN DESTDIR=install make install
|
||||
WORKDIR /
|
||||
##### SPS END #####
|
||||
|
||||
##### STATIC FILES #####
|
||||
FROM scratch AS files
|
||||
|
||||
# Add run script that will start SPS
|
||||
COPY --chmod=755 ./docker/classic/run.sh ./run.sh
|
||||
COPY ./docker/etc/pulse /etc/pulse
|
||||
##### END STATIC FILES #####
|
||||
|
||||
##### BUILD FILES #####
|
||||
FROM scratch AS build-files
|
||||
COPY --from=ffmpeglite /ffmpeg_build/lib/ /usr/local/lib
|
||||
COPY --from=shairport-sync /shairport-sync/build/install/usr/local/bin/shairport-sync /usr/local/bin/shairport-sync
|
||||
COPY --from=shairport-sync /shairport-sync/build/install/usr/local/share/man/man1 /usr/share/man/man1
|
||||
COPY --from=shairport-sync /shairport-sync/build/install/etc/shairport-sync.conf /etc/
|
||||
COPY --from=shairport-sync /shairport-sync/build/install/etc/shairport-sync.conf.sample /etc/
|
||||
COPY --from=shairport-sync /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/
|
||||
COPY --from=shairport-sync /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/
|
||||
##### END BUILD FILES #####
|
||||
|
||||
# Shairport Sync Runtime System
|
||||
FROM crazymax/alpine-s6:3.20-3.2.0.2
|
||||
FROM alpine:$ALPINE_VERSION
|
||||
|
||||
ENV S6_CMD_WAIT_FOR_SERVICES=1
|
||||
ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0
|
||||
COPY --from=build-files / /
|
||||
|
||||
RUN apk -U add \
|
||||
RUN apk --no-cache add \
|
||||
alsa-lib \
|
||||
avahi \
|
||||
avahi-tools \
|
||||
dbus \
|
||||
glib \
|
||||
less \
|
||||
less-doc \
|
||||
libconfig \
|
||||
libsndfile-dev \
|
||||
pipewire \
|
||||
libpulse \
|
||||
man-pages \
|
||||
mandoc \
|
||||
mbedtls \
|
||||
libcrypto3 \
|
||||
mosquitto \
|
||||
popt \
|
||||
soxr \
|
||||
curl
|
||||
curl && \
|
||||
rm -rfv /lib/apk/db/* && \
|
||||
rm -rfv /etc/avahi/services/*.service && \
|
||||
rm -rf /usr/share/man && \
|
||||
addgroup shairport-sync && \
|
||||
adduser -D shairport-sync -G shairport-sync && \
|
||||
addgroup -g 29 docker_audio && \
|
||||
addgroup shairport-sync docker_audio && \
|
||||
addgroup shairport-sync audio && \
|
||||
mkdir -p /run/dbus
|
||||
|
||||
# Copy build files.
|
||||
COPY --from=builder /shairport-sync/build/install/usr/local/bin/shairport-sync /usr/local/bin/shairport-sync
|
||||
COPY --from=builder /shairport-sync/build/install/usr/local/share/man/man1 /usr/share/man/man1
|
||||
COPY --from=builder /usr/local/lib/libalac.* /usr/local/lib/
|
||||
COPY --from=builder /shairport-sync/build/install/etc/shairport-sync.conf /etc/
|
||||
COPY --from=builder /shairport-sync/build/install/etc/shairport-sync.conf.sample /etc/
|
||||
COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/
|
||||
COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/
|
||||
# Removed anything we don't need.
|
||||
# Removed any statically-defined Avahi services, e.g. SSH and SFTP
|
||||
|
||||
COPY ./docker/classic/etc/s6-overlay/s6-rc.d /etc/s6-overlay/s6-rc.d
|
||||
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
|
||||
# Created 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
|
||||
# Added the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff
|
||||
|
||||
RUN addgroup shairport-sync
|
||||
RUN adduser -D shairport-sync -G shairport-sync
|
||||
COPY --from=files / /
|
||||
|
||||
# Add the shairport-sync user to the pre-existing audio group, which has ID 29, for access to the ALSA stuff
|
||||
RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgroup shairport-sync audio
|
||||
|
||||
# Remove anything we don't need.
|
||||
RUN rm -rf /lib/apk/db/*
|
||||
|
||||
# Remove any statically-defined Avahi services, e.g. SSH and SFTP
|
||||
RUN rm -rf /etc/avahi/services/*.service
|
||||
|
||||
# Add run script that will start SPS
|
||||
COPY ./docker/run.sh ./run.sh
|
||||
RUN chmod +x /run.sh
|
||||
|
||||
# D-Bus might need this directory
|
||||
RUN mkdir -p /run/dbus
|
||||
|
||||
ENTRYPOINT ["/init","./run.sh"]
|
||||
ENTRYPOINT ["./run.sh"]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
base
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
echo "STARTING - $(date)"
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/01-startup/script.sh
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
exec dbus-send --system / org.freedesktop.DBus.Peer.Ping > /dev/null 2> /dev/null
|
||||
@@ -1 +0,0 @@
|
||||
01-startup
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/command/execlineb -S0
|
||||
/run/s6/basedir/bin/halt
|
||||
@@ -1 +0,0 @@
|
||||
3
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/command/with-contenv sh
|
||||
|
||||
# Set the limit to the same value Docker has been using in earlier version.
|
||||
ulimit -n 1048576
|
||||
|
||||
echo "Starting dbus"
|
||||
exec s6-notifyoncheck dbus-daemon --system --nofork --nopidfile
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
02-dbus
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/command/execlineb -S0
|
||||
/run/s6/basedir/bin/halt
|
||||
@@ -1 +0,0 @@
|
||||
3
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/command/with-contenv sh
|
||||
echo "Starting avahi"
|
||||
exec s6-notifyoncheck avahi-daemon --no-chroot
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
Regular → Executable
+20
-3
@@ -1,8 +1,25 @@
|
||||
#!/bin/sh
|
||||
|
||||
# exist if any command returns a non-zero result
|
||||
set -e
|
||||
|
||||
rm -rf /var/run/dbus.pid
|
||||
|
||||
dbus-uuidgen --ensure
|
||||
dbus-daemon --system
|
||||
|
||||
avahi-daemon --daemonize --no-chroot
|
||||
|
||||
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 "$@"
|
||||
|
||||
# for PipeWire
|
||||
export XDG_RUNTIME_DIR=/tmp
|
||||
|
||||
# for PulseAudio
|
||||
export PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
export PULSE_COOKIE=/tmp/pulseaudio.cookie
|
||||
|
||||
exec /usr/local/bin/shairport-sync "$@"
|
||||
|
||||
+22
-3
@@ -1,8 +1,27 @@
|
||||
#!/bin/sh
|
||||
|
||||
# exist if any command returns a non-zero result
|
||||
set -e
|
||||
|
||||
rm -rf /var/run/dbus.pid
|
||||
|
||||
dbus-uuidgen --ensure
|
||||
dbus-daemon --system
|
||||
|
||||
(/usr/local/bin/nqptp > /dev/null 2>&1) &
|
||||
|
||||
avahi-daemon --daemonize --no-chroot
|
||||
|
||||
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 "$@"
|
||||
|
||||
# for PipeWire
|
||||
export XDG_RUNTIME_DIR=/tmp
|
||||
|
||||
# for PulseAudio
|
||||
export PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
export PULSE_COOKIE=/tmp/pulseaudio.cookie
|
||||
|
||||
exec /usr/local/bin/shairport-sync "$@"
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ void loudness_set_volume(float volume) {
|
||||
if (gain < 0)
|
||||
gain = 0;
|
||||
|
||||
debug(2, "Volume: %.1f dB - Loudness gain @10Hz: %.1f dB", volume, gain);
|
||||
debug(3, "Volume: %.1f dB - Loudness gain @10Hz: %.1f dB", volume, gain);
|
||||
_loudness_set_volume(&loudness_l, volume);
|
||||
_loudness_set_volume(&loudness_r, volume);
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
all: shairport-sync.1 shairport-sync.html
|
||||
|
||||
%.1: %.1.xml
|
||||
xmltoman $*.1.xml > $*.tmp && mv $*.tmp $*.1
|
||||
|
||||
%.html: %.1.xml
|
||||
xsltproc xmltoman.xsl $*.1.xml > $*.tmp && mv $*.tmp $*.html
|
||||
|
||||
clean:
|
||||
rm shairport-sync.1
|
||||
rm shairport-sync.html
|
||||
@@ -0,0 +1,17 @@
|
||||
man_MANS = shairport-sync.1
|
||||
|
||||
if USE_XMLTOMAN
|
||||
shairport-sync.1: shairport-sync.1.xml
|
||||
xmltoman $< > $@
|
||||
|
||||
# It seems that xmlmantohtml is not working anymore (20240905)
|
||||
# all-local: shairport-sync.html
|
||||
|
||||
|
||||
# shairport-sync.html: shairport-sync.1.xml
|
||||
# xmlmantohtml $< > $@
|
||||
|
||||
clean:
|
||||
rm -f shairport-sync.1
|
||||
# rm -f shairport-sync.html
|
||||
endif
|
||||
@@ -1,482 +0,0 @@
|
||||
<?xml version="1.0" encoding="iso-8859-15"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-15" />
|
||||
<title>shairport-sync(7)</title>
|
||||
<style type="text/css">
|
||||
body { color: black; background-color: white; }
|
||||
a:link, a:visited { color: #900000; }
|
||||
h1 { text-transform:uppercase; font-size: 18pt; }
|
||||
p { margin-left:1cm; margin-right:1cm; }
|
||||
.cmd { font-family:monospace; }
|
||||
.file { font-family:monospace; }
|
||||
.arg { font-family:monospace; font-style: italic; }
|
||||
.opt { font-family:monospace; font-weight: bold; }
|
||||
.manref { font-family:monospace; }
|
||||
.option .optdesc { margin-left:2cm; }
|
||||
</style>
|
||||
</head>
|
||||
<body><h1>Name</h1><p>shairport-sync - AirPlay and AirPlay 2 Audio Player</p>
|
||||
|
||||
<h1>Synopsis</h1>
|
||||
|
||||
<p class="cmd">shairport-sync <span class="opt">[-djvw]</span>
|
||||
<span class="opt">[-a </span><span class="arg">service-name</span><span class="opt"> | --name=</span><span class="arg">service-name</span><span class="opt">]</span>
|
||||
<span class="opt">[-B </span><span class="arg">command</span><span class="opt"> | --onstart=</span><span class="arg">command</span><span class="opt">]</span>
|
||||
<span class="opt">[-c </span><span class="arg">configurationfile</span><span class="opt"> | --configfile=</span><span class="arg">configurationfile</span><span class="opt">]</span>
|
||||
<span class="opt">[-d | --daemon]</span>
|
||||
<span class="opt">[-E </span><span class="arg">command</span><span class="opt"> | --onstop=</span><span class="arg">command</span><span class="opt">]</span>
|
||||
<span class="opt">[-g | --get-cover-art]</span>
|
||||
<span class="opt">[-j | --justDaemoniseNoPIDFile]</span>
|
||||
<span class="opt">[--logOutputLevel]</span>
|
||||
<span class="opt">[--log-to-syslog]</span>
|
||||
<span class="opt">[-L </span><span class="arg">latency</span><span class="opt"> | --latency=</span><span class="arg">latency</span><span class="opt">]</span>
|
||||
<span class="opt">[-m </span><span class="arg">backend</span><span class="opt"> | --mdns=</span><span class="arg">backend</span><span class="opt">]</span>
|
||||
<span class="opt">[-M | --metadata-enable]</span>
|
||||
<span class="opt">[-o </span><span class="arg">backend</span><span class="opt"> | --output=</span><span class="arg">backend</span><span class="opt">]</span>
|
||||
<span class="opt">[-p </span><span class="arg">port</span><span class="opt"> | --port=</span><span class="arg">port</span><span class="opt">]</span>
|
||||
<span class="opt">[--password=</span><span class="arg">secret</span><span class="opt">]</span>
|
||||
<span class="opt">[-r </span><span class="arg">threshold</span><span class="opt"> | --resync=</span><span class="arg">threshold</span><span class="opt">]</span>
|
||||
<span class="opt">[--statistics]</span>
|
||||
<span class="opt">[-S </span><span class="arg">mode</span><span class="opt"> | --stuffing=</span><span class="arg">mode</span><span class="opt">]</span>
|
||||
<span class="opt">[-t </span><span class="arg">timeout</span><span class="opt"> | --timeout=</span><span class="arg">timeout</span><span class="opt">]</span>
|
||||
<span class="opt">[--tolerance=</span><span class="arg">frames</span><span class="opt">]</span>
|
||||
<span class="opt">[-v | --verbose]</span>
|
||||
<span class="opt">[-w | --wait-cmd]</span>
|
||||
<span class="opt">[-- </span><span class="arg">audio_backend_options</span><span class="opt">]</span>
|
||||
</p>
|
||||
<p class="cmd">shairport-sync <span class="opt">-X | --displayConfig</span></p>
|
||||
<p class="cmd">shairport-sync <span class="opt">-h</span></p>
|
||||
<p class="cmd">shairport-sync <span class="opt">-k</span></p>
|
||||
<p class="cmd">shairport-sync <span class="opt">-V</span></p>
|
||||
|
||||
|
||||
<h1>Description</h1>
|
||||
<p>Shairport Sync plays AirPlay audio.
|
||||
It can be built to stream either from "classic" AirPlay (aka "AirPlay 1")
|
||||
or from AirPlay 2 devices.</p>
|
||||
|
||||
<p>AirPlay 2 support is limited, and AirPlay 2 from iTunes for Windows is not supported.
|
||||
For AirPlay 2 operation, a companion program called <span class="opt">nqptp</span> must be installed.</p>
|
||||
|
||||
<p>Please see <a class="url" href="https://github.com/mikebrady/shairport-sync">https://github.com/mikebrady/shairport-sync</a> for details.</p>
|
||||
|
||||
<p>The name of the Shairport Sync executable is <span class="opt">shairport-sync</span>.</p>
|
||||
|
||||
|
||||
|
||||
|
||||
<h1>Configuration File Settings</h1>
|
||||
<p>You should use the configuration file for setting up Shairport Sync because --
|
||||
apart from a few special-purpose commands -- it has a much richer set of options
|
||||
than are available on the command line.
|
||||
This file is usually <span class="file">shairport-sync.conf</span> and is generally located in the
|
||||
System Configuration Directory, which is normally the <span class="file">/etc</span> directory in
|
||||
Linux or the <span class="file">/usr/local/etc</span> directory in BSD unixes.
|
||||
You may need to have root privileges to modify it.</p>
|
||||
|
||||
<p>(Note: Shairport Sync may have been compiled to use a different configuration
|
||||
directory. You can determine which by performing the command <span class="file">$ shairport-sync
|
||||
-V</span>. The last item in the output string is the value of the
|
||||
<span class="opt">sysconfdir</span>, i.e. the System Configuration Directory.)</p>
|
||||
|
||||
<p>Within the configuration file, settings are organised into groups, for
|
||||
example, there is a <span class="opt">general</span> group of
|
||||
standard settings, and there is an <span class="opt">alsa</span> group with settings
|
||||
that pertain to the <span class="opt">ALSA</span> back end.
|
||||
Here is an example of a typical configuration file:</p>
|
||||
|
||||
<p><span class="opt">general = {</span></p>
|
||||
<p><p><span class="opt">name = "Mike's Boombox";</span></p></p>
|
||||
<p><span class="opt">};</span></p>
|
||||
<p><span class="opt"></span></p>
|
||||
<p><span class="opt">alsa = {</span></p>
|
||||
<p><p><span class="opt">output_device = "hw:0";</span></p></p>
|
||||
<p><p><span class="opt">mixer_control_name = "PCM";</span></p></p>
|
||||
<p><span class="opt">};</span></p>
|
||||
|
||||
<p>Users generally only need to set (1) the service name and
|
||||
(2) the output device.
|
||||
|
||||
If the <span class="opt">name</span> setting is omitted, the service name is derived from the system's hostname.
|
||||
|
||||
By default, the <span class="opt">ALSA</span> backend will be chosen if included in the build.
|
||||
|
||||
If the (alsa) output device has a mixer that can be used for volume
|
||||
control, then (3) the mixer name should be specified. It is important
|
||||
to do this if the mixer exists. Otherwise, the
|
||||
maximum output from the output device will be whatever setting the mixer happens to
|
||||
have, which will be a matter of chance and which could be very low or even silent.</p>
|
||||
|
||||
<p>A sample configuration file with all possible settings, but with all of them
|
||||
commented out, is installed at <span class="file">shairport-sync.conf.sample</span>, within the
|
||||
System Configuration Directory -- <span class="file">/etc</span> in Linux,
|
||||
<span class="file">/usr/local/etc</span> in BSD unixes.</p>
|
||||
|
||||
<p>The sample configuration file includes extensive documentation of the settings.
|
||||
and is also available at
|
||||
<a class="url" href="https://github.com/mikebrady/shairport-sync/blob/master/scripts/shairport-sync.conf">https://github.com/mikebrady/shairport-sync/blob/master/scripts/shairport-sync.conf</a>.
|
||||
Please refer to it for the most up-to-date information on configuration file settings.</p>
|
||||
|
||||
|
||||
|
||||
<h1>Options</h1>
|
||||
|
||||
<p>There are two kinds of command-line options for shairport-sync:
|
||||
regular <span class="opt">program options</span> and <span class="opt">audio backend options</span>.
|
||||
Program options are
|
||||
always listed first, followed by any audio backend options, preceded by
|
||||
a <span class="opt">--</span> symbol.</p>
|
||||
|
||||
<p>See the EXAMPLES section for sample usages.</p>
|
||||
|
||||
<h1>Program Options</h1>
|
||||
<p>Program Options are used by shairport-sync itself.</p>
|
||||
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-a </span><span class="arg">service name</span><span class="opt"> | --name=</span><span class="arg">service
|
||||
name</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Use this <span class="arg">service name</span> to identify this player in iTunes, etc.</p>
|
||||
|
||||
<p>The following substitutions are allowed:
|
||||
<span class="opt">%h</span> for the computer's hostname,
|
||||
<span class="opt">%H</span> for the computer's hostname with the first letter capitalised (ASCII
|
||||
only),
|
||||
<span class="opt">%v</span> for the shairport-sync version number, e.g. "3.0.1" and
|
||||
<span class="opt">%V</span> for the shairport-sync version string, e.g.
|
||||
"3.0.1-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc".</p>
|
||||
<p>The default is "%H", which is replaced by the hostname with the first letter
|
||||
capitalised.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-B </span><span class="arg">program</span><span class="opt"> | --on-start=</span><span class="arg">program</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Execute <span class="arg">program</span> when playback is about to begin. Specify the
|
||||
full path to the program, e.g. <span class="file">/usr/bin/logger</span>.
|
||||
Executable scripts can be used, but they must have the appropriate shebang
|
||||
(<span class="file">#!/bin/sh</span>) in the headline.</p>
|
||||
|
||||
<p>If you want shairport-sync to wait until the command has
|
||||
completed before starting to play, select the <span class="opt">-w</span> option as well.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-c </span><span class="arg">filename</span><span class="opt"> | --configfile=</span><span class="arg">filename</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Read configuration settings from <span class="arg">filename</span>. The default is to read them from
|
||||
the <span class="file">shairport-sync.conf</span> in the System Configuration Directory --
|
||||
<span class="file">/etc</span> in Linux, <span class="file">/usr/local/etc</span> in BSD unixes.
|
||||
For information about configuration settings, see the "Configuration File Settings"
|
||||
section above.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-d | --daemon</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Instruct shairport-sync to demonise itself. It will write its
|
||||
Process ID (PID) to a file, usually at
|
||||
<span class="file">/var/run/shairport-sync/shairport-sync.pid</span>, which is used by the
|
||||
<span class="opt">-k</span>, <span class="opt">-D</span> and <span class="opt">-R</span> options to locate
|
||||
the daemon at a later time. See also the <span class="opt">-j</span> option. Only available if
|
||||
shairport-sync has been compiled with libdaemon support.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-E </span><span class="arg">program</span><span class="opt"> | --on-stop=</span><span class="arg">program</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Execute <span class="arg">program</span> when playback has ended. Specify the
|
||||
full path to the program, e.g. <span class="file">/usr/bin/logger</span>.
|
||||
Executable scripts can be used, but they must have the appropriate shebang
|
||||
(<span class="file">#!/bin/sh</span>) in the headline.</p>
|
||||
<p>If you want shairport-sync to wait until the command has
|
||||
completed before continuing, select the <span class="opt">-w</span> option as well.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-g | --get-coverart</span></p>
|
||||
<div class="optdesc"><p>
|
||||
This option requires the <span class="opt">-M | --metadata-enable</span> option to be set, and enables
|
||||
shairport-sync to request cover art from the source and to process it as metadata.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-h | --help</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Print brief help message and exit.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-j | justDaemoniseNoPIDFile</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Instruct shairport-sync to demonise itself. Unlike the <span class="opt">-d</span> option, it will
|
||||
not write a Process ID (PID) to a file -- it will just (hence the "j") demonise
|
||||
itself. Only available if shairport-sync has been compiled with libdaemon support.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-k | --kill</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Kill the shairport-sync daemon and exit. (Requires that the daemon has
|
||||
written its PID to an agreed file -- see the <span class="opt">-d</span> option. Only available if
|
||||
shairport-sync has been compiled with libdaemon support.)
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">--logOutputLevel</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Use this to log the volume level when the volume is changed. It may be useful if you
|
||||
are trying to determine a suitable value for the maximum volume level. Not available
|
||||
as a configuration file setting.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">--log-to-syslog</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Warnings, error messages and messages are sent, by default, to <span class="file">STDERR</span>.
|
||||
Use this option to route these messages to the <span class="opt">syslog</span> instead.
|
||||
This is intended for use when Shairport Sync is operating as a daemon.
|
||||
</p><p>See also <span class="opt">--displayConfig</span>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-L | --latency=</span><span class="arg">latency</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Use this to set the <span class="arg">default latency</span>, in frames, for audio coming from an
|
||||
unidentified source or from an iTunes Version 9 or earlier source. The standard value
|
||||
for the <span class="arg">default latency</span> is 88,200 frames, where there are 44,100
|
||||
frames to the second.
|
||||
</p>
|
||||
<p>Please note that this feature is deprecated and will be removed in a future version
|
||||
of shairport-sync.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-M | --metadata-enable</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Ask the client to send metadata. It will be sent, along with metadata generated
|
||||
by shairport-sync itself, to a pipe and will also be
|
||||
sent as UDP packets.
|
||||
If you add the <span class="opt">-g | --get-cover-art</span>
|
||||
then cover art included, where available. See <a class="url" href="https://github.com/mikebrady/shairport-sync-metadata-reader">https://github.com/mikebrady/shairport-sync-metadata-reader</a>
|
||||
for a sample metadata reader.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">--metadata-pipename=</span><span class="arg">pathname</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Specify the path name for the metadata pipe.
|
||||
Note that <span class="opt">shairport-sync</span> will need write permission on that directory and pipe.
|
||||
The default is <span class="file">/tmp/shairport-sync-metadata</span>.
|
||||
If you rename the <span class="opt">shairport-sync</span> executable, the default pipe name will change accordingly.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-m </span><span class="arg">mdnsbackend</span><span class="opt"> | --mdns=</span><span class="arg">mdnsbackend</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Force the use of the specified mDNS backend to advertise the
|
||||
player on the network. The default is to try all mDNS backends in order until one
|
||||
works.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-o </span><span class="arg">outputbackend</span><span class="opt"> |
|
||||
--output=</span><span class="arg">outputbackend</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Force the use of the specified output backend to play the audio.
|
||||
The default is to try the first one.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-p </span><span class="arg">port</span><span class="opt"> | --port=</span><span class="arg">port</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Listen for play requests on <span class="arg">port</span>. The default is to use port
|
||||
5000 for AirPlay and 7000 for AirPlay 2.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">--password=</span><span class="arg">secret</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Require the password <span class="arg">secret</span> to be able to connect and stream to the
|
||||
service. (This only works for AirPlay and not for AirPlay 2.)
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-r </span><span class="arg">threshold</span><span class="opt"> | --resync=</span><span class="arg">threshold</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Resynchronise if timings differ by more than <span class="arg">threshold</span> frames.
|
||||
If the output timing differs from the source timing by more than
|
||||
the threshold, output will be muted and a full resynchronisation
|
||||
will occur. The default threshold is 2,205 frames, i.e. 50
|
||||
milliseconds. Specify <span class="opt">0</span> to disable resynchronisation. This setting is
|
||||
deprecated and will be removed in a future version of shairport-sync.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">--statistics</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Print some performance information to <span class="file">STDERR</span>, or to <span class="opt">syslog</span> if the <span class="opt">-log-to-syslog</span> command line option is also chosen.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-S </span><span class="arg">mode</span><span class="opt"> | --stuffing=</span><span class="arg">mode</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Interpolate ("stuff") the audio stream using the <span class="arg">mode</span>.
|
||||
"Stuffing" refers to the
|
||||
process of adding or removing frames of audio to or from the
|
||||
stream sent to the output device in order to keep it synchronised
|
||||
with the player.
|
||||
The <span class="opt">basic</span> mode is normally almost completely inaudible.
|
||||
The alternative mode, <span class="opt">soxr</span>, is even less obtrusive but
|
||||
requires much more processing power. For this mode, support for
|
||||
<span class="opt">libsoxr</span>, the SoX Resampler Library, must be selected when
|
||||
<span class="opt">shairport-sync</span> is built.
|
||||
|
||||
The default setting, <span class="opt">auto</span>, allows Shairport Sync to choose
|
||||
<span class="opt">soxr</span> mode if the system is powerful enough.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-t </span><span class="arg">timeout</span><span class="opt"> | --timeout=</span><span class="arg">timeout</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Exit play mode if the stream disappears for more than <span class="arg">timeout</span>
|
||||
seconds.</p>
|
||||
<p>When shairport-sync plays an audio stream, it starts a play
|
||||
session and will return a busy signal to any other sources that
|
||||
attempt to use it. If the audio stream disappears for longer
|
||||
than <span class="arg">timeout</span> seconds, the play session will be terminated.
|
||||
If you specify a timeout time of <span class="opt">0</span>,
|
||||
shairport-sync will never signal that
|
||||
it is busy and will not prevent other sources from "barging in"
|
||||
on an existing play session. The default value is 120 seconds.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">--tolerance=</span><span class="arg">frames</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Allow playback to be up to <span class="arg">frames</span> out of exact synchronization before
|
||||
attempting to correct it.
|
||||
The default is 88 frames, i.e. 2 ms. The smaller the tolerance, the more likely it is
|
||||
that overcorrection will occur.
|
||||
Overcorrection is when more corrections (insertions and deletions) are made than are
|
||||
strictly necessary to keep the stream in sync. Use the <span class="opt">--statistics</span> option
|
||||
to monitor correction levels. Corrections should not greatly exceed net corrections.
|
||||
This setting is deprecated and will be removed in a future version of shairport-sync.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-V | --version</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Print version information and exit.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-v | --verbose</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Print debug information to the <span class="file">STDERR</span>, or to <span class="opt">syslog</span> if the <span class="opt">-log-to-syslog</span> command line option is also chosen.
|
||||
Repeat up to three times (i.e. <span class="opt">-vv</span> or <span class="opt">-vvv</span>) for more detail. You should use <span class="opt">-vvv</span> very sparingly -- it is really noisy.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-w | --wait-cmd</span></p>
|
||||
<div class="optdesc"><p>
|
||||
Wait for commands specified using <span class="opt">-B</span> or <span class="opt">-E</span> to complete before
|
||||
continuing execution.
|
||||
</p></div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p><span class="opt">-X | --displayConfig</span></p>
|
||||
<div class="optdesc"><p>
|
||||
This logs information relating to the configuration of Shairport Sync.
|
||||
It can be very useful for debugging. The information logged is
|
||||
some host OS information, the Shairport Sync version string
|
||||
(which indicates the build options used when <span class="opt">shairport-sync</span> was built),
|
||||
the contents of the command line that invoked Shairport Sync,
|
||||
the name of the configuration file and the active settings therein.</p>
|
||||
<p>If this is the only option on the command line, <span class="opt">shairport-sync</span> will
|
||||
terminate after displaying the information.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>Audio Backend Options</h1>
|
||||
<p>Audio Backend Options are command-line options that are passed to the chosen audio backend.
|
||||
They are always preceded by the <span class="opt">--</span> symbol to introduce them and to separate them from
|
||||
any preceding program options. In this way, option letters can be used as program
|
||||
options and reused as audio backend options without ambiguity.</p>
|
||||
|
||||
<p>Audio backends are listed with their corresponding Audio Backend Options in the help text provided by the help (<span class="opt">-h</span> or <span class="opt">--help</span>) option.</p>
|
||||
|
||||
|
||||
|
||||
<h1>Examples</h1>
|
||||
<p>Here is a slightly contrived example:</p>
|
||||
<p class="cmd">shairport-sync
|
||||
<span class="opt">-a "Joe's Stereo"</span>
|
||||
<span class="opt">-o alsa</span>
|
||||
<span class="opt">--</span>
|
||||
<span class="opt">-d hw:1,0</span>
|
||||
<span class="opt">-m hw:1</span>
|
||||
<span class="opt">-c PCM</span>
|
||||
</p>
|
||||
<p>The program will be visible as
|
||||
"Joe's Stereo" ( <span class="opt">-a "Joe's Stereo"</span> ).
|
||||
The program option <span class="opt">-o alsa</span> specifies that the <span class="opt">alsa</span> backend be used, thus that audio should be output into the <span class="opt">ALSA</span> audio subsystem.
|
||||
The audio backend options following the <span class="opt">--</span> separator are passed to the <span class="opt">alsa</span> backend and specify
|
||||
that the audio will be output on subdevice 0 of soundcard 1
|
||||
( <span class="opt">-d hw:1,0</span> ) and will take advantage of the same sound card's mixer
|
||||
( <span class="opt">-m hw:1</span> ) using the level control named "PCM" ( <span class="opt">-c "PCM"</span> ).
|
||||
</p>
|
||||
<p>The example above is slightly contrived:
|
||||
Firstly, if the <span class="opt">alsa</span> backend has been included in the build, it will be the default, so it doesn't need to be specified and the <span class="opt">-o alsa</span> option could be omitted.
|
||||
Secondly, subdevice 0 is the default for a soundcard, so the output device could simply be written <span class="opt">-d hw:1</span>.
|
||||
Thirdly, when a mixer name is given ( <span class="opt">-c "PCM"</span> ), the default is that the mixer is on the output device, so the <span class="opt">-m hw:1</span> is unnecessary here.
|
||||
Using these defaults and simplifications gives the following command:</p>
|
||||
<p class="cmd">shairport-sync
|
||||
<span class="opt">-a "Joe's Stereo"</span>
|
||||
<span class="opt">--</span>
|
||||
<span class="opt">-d hw:1</span>
|
||||
<span class="opt">-c PCM</span>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
<h1>Credits</h1>
|
||||
<p>Mike Brady (<a class="url" href="https://github.com/mikebrady">https://github.com/mikebrady</a>) developed Shairport Sync from Shairport by James Wah (<a class="url" href="https://github.com/abrasive">https://github.com/abrasive</a>).</p>
|
||||
|
||||
|
||||
<h1>Comments</h1>
|
||||
<p>This man page was written using <a class="manref" href="http://masqmail.cx/xml2man/">xml2man(1)</a> by Oliver Kurth.</p>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* mDNS registration handler. This file is part of Shairport.
|
||||
* Copyright (c) James Laird 2013
|
||||
* Modifications, updates and additions (c) Mike Brady 2014 -- 2020
|
||||
* Modifications, updates and additions (c) Mike Brady 2014--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
|
||||
@@ -33,13 +33,13 @@ typedef struct {
|
||||
|
||||
/*
|
||||
#define MDNS_RECORD_WITH_METADATA \
|
||||
"tp=UDP", "sm=false", "ek=1", "et=0,1", "cn=0,1", "ch=2", METADATA_EXPRESSION, "ss=16", \
|
||||
"tp=UDP", "sm=false", "ek=1", "et=0,1", "cn=0,1", "ch=2", METADATA_EXPRESSION, "ss=16", \
|
||||
"sr=44100", "vn=3", "txtvers=1", config.password ? "pw=true" : "pw=false"
|
||||
*/
|
||||
#define MDNS_RECORD_WITH_METADATA \
|
||||
"sf=0x4", "fv=76400.10", "am=ShairportSync", "vs=105.1", "tp=TCP,UDP", "vn=65537", \
|
||||
METADATA_EXPRESSION, "ss=16", "sr=44100", "da=true", "sv=false", "et=0,1", "ek=1", \
|
||||
"cn=0,1", "ch=2", "txtvers=1", config.password ? "pw=true" : "pw=false"
|
||||
METADATA_EXPRESSION, "ss=16", "sr=44100", "da=true", "sv=false", "et=0,1", "ek=1", "cn=0,1", \
|
||||
"ch=2", "txtvers=1", config.password ? "pw=true" : "pw=false"
|
||||
|
||||
#endif
|
||||
|
||||
@@ -52,7 +52,7 @@ typedef struct {
|
||||
|
||||
#define MDNS_RECORD_WITHOUT_METADATA \
|
||||
"sf=0x4", "fv=76400.10", "am=ShairportSync", "vs=105.1", "tp=TCP,UDP", "vn=65537", "ss=16", \
|
||||
"sr=44100", "da=true", "sv=false", "et=0,1", "ek=1", "cn=0,1", "ch=2", "txtvers=1", \
|
||||
"sr=44100", "da=true", "sv=false", "et=0,1", "ek=1", "cn=0,1", "ch=2", "txtvers=1", \
|
||||
config.password ? "pw=true" : "pw=false"
|
||||
|
||||
#endif // _MDNS_H
|
||||
|
||||
+17
-9
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Embedded Avahi client. This file is part of Shairport.
|
||||
* Copyright (c) James Laird 2013
|
||||
* Additions for metadata and for detecting IPv6 Copyright (c) Mike Brady 2015--2019
|
||||
* Additions for maintenance, metadata, AirPlay 2 and IPv6 Copyright (c) Mike Brady 2015--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -86,7 +86,12 @@ static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIn
|
||||
AVAHI_GCC_UNUSED AvahiProtocol protocol, AvahiResolverEvent event,
|
||||
const char *name, const char *type, const char *domain,
|
||||
__attribute__((unused)) const char *host_name,
|
||||
__attribute__((unused)) const AvahiAddress *address, uint16_t port,
|
||||
#ifdef CONFIG_METADATA
|
||||
__attribute__((unused)) const AvahiAddress *address, uint16_t lport,
|
||||
#else
|
||||
__attribute__((unused)) const AvahiAddress *address,
|
||||
__attribute__((unused)) uint16_t lport,
|
||||
#endif
|
||||
__attribute__((unused)) AvahiStringList *txt,
|
||||
__attribute__((unused)) AvahiLookupResultFlags flags, void *userdata) {
|
||||
// debug(1,"resolve_callback, event %d.", event);
|
||||
@@ -97,7 +102,7 @@ static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIn
|
||||
/* Called whenever a service has been resolved successfully or timed out */
|
||||
switch (event) {
|
||||
case AVAHI_RESOLVER_FAILURE:
|
||||
debug(2, "(Resolver) Failed to resolve service '%s' of type '%s' in domain '%s': %s.", name,
|
||||
debug(3, "(Resolver) Failed to resolve service '%s' of type '%s' in domain '%s': %s.", name,
|
||||
type, domain, avahi_strerror(avahi_client_errno(avahi_service_resolver_get_client(r))));
|
||||
break;
|
||||
case AVAHI_RESOLVER_FOUND: {
|
||||
@@ -115,10 +120,9 @@ static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIn
|
||||
dacp_monitor_port_update_callback(dacpid, port);
|
||||
#endif
|
||||
#ifdef CONFIG_METADATA
|
||||
char portstring[20];
|
||||
memset(portstring, 0, sizeof(portstring));
|
||||
snprintf(portstring, sizeof(portstring), "%u", port);
|
||||
send_ssnc_metadata('dapo', portstring, strlen(portstring), 0);
|
||||
uint32_t nport = lport;
|
||||
nport = htonl(nport);
|
||||
send_ssnc_metadata('dapo', (const char *)&nport, sizeof(nport), 0);
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
@@ -157,7 +161,7 @@ static void browse_callback(AvahiServiceBrowser *b, AvahiIfIndex interface, Avah
|
||||
avahi_strerror(avahi_client_errno(client)));
|
||||
break;
|
||||
case AVAHI_BROWSER_REMOVE:
|
||||
debug(2, "(Browser) REMOVE: service '%s' of type '%s' in domain '%s'.", name, type, domain);
|
||||
debug(3, "(Browser) REMOVE: service '%s' of type '%s' in domain '%s'.", name, type, domain);
|
||||
#ifdef CONFIG_DACP_CLIENT
|
||||
dacp_browser_struct *dbs = (dacp_browser_struct *)userdata;
|
||||
char *dacpid = strstr(name, "iTunes_Ctrl_");
|
||||
@@ -196,7 +200,11 @@ static void egroup_callback(AvahiEntryGroup *g, AvahiEntryGroupState state,
|
||||
|
||||
/* A service name collision with a remote service
|
||||
* happened. Let's pick a new name */
|
||||
debug(1, "avahi name collision -- look for another");
|
||||
if (service_name)
|
||||
debug(1, "avahi name collision with \"%s\" -- look for another", service_name);
|
||||
else
|
||||
debug(1, "avahi name collision (name not available) -- look for another");
|
||||
|
||||
n = avahi_alternative_service_name(service_name);
|
||||
if (service_name)
|
||||
avahi_free(service_name);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Embedded dns-sd client. This file is part of Shairport.
|
||||
* Copyright (c) Paul Lietar 2013
|
||||
* Copyright (c) Mike Brady 2014--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* mDNS registration handler. This file is part of Shairport.
|
||||
* Copyright (c) Paul Lietar 2013
|
||||
* Amendments and updates copyright (c) Mike Brady 2014 -- 2019
|
||||
* Amendments and updates copyright (c) Mike Brady 2014--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* mDNS registration handler. This file is part of Shairport.
|
||||
* Copyright (c) Paul Lietar 2013
|
||||
* Copyright (c) Mike Brady 2014--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
|
||||
+49
-33
@@ -5,7 +5,7 @@
|
||||
* then you need a metadata hub,
|
||||
* where everything is stored
|
||||
* This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2017--2022
|
||||
* Copyright (c) Mike Brady 2017--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -138,14 +138,14 @@ void _metadata_hub_modify_prolog(const char *filename, const int linenumber) {
|
||||
// debug(1, "locking metadata hub for writing");
|
||||
if (pthread_rwlock_trywrlock(&metadata_hub_re_lock) != 0) {
|
||||
if (last_metadata_hub_modify_prolog_file)
|
||||
debug(2, "Metadata_hub write lock at \"%s:%d\" is already taken at \"%s:%d\" -- must wait.",
|
||||
debug(3, "Metadata_hub write lock at \"%s:%d\" is already taken at \"%s:%d\" -- must wait.",
|
||||
filename, linenumber, last_metadata_hub_modify_prolog_file,
|
||||
last_metadata_hub_modify_prolog_line);
|
||||
else
|
||||
debug(2, "Metadata_hub write lock is already taken by unknown -- must wait.");
|
||||
metadata_hub_re_lock_access_is_delayed = 0;
|
||||
pthread_rwlock_wrlock(&metadata_hub_re_lock);
|
||||
debug(2, "Okay -- acquired the metadata_hub write lock at \"%s:%d\".", filename, linenumber);
|
||||
debug(3, "Okay -- acquired the metadata_hub write lock at \"%s:%d\".", filename, linenumber);
|
||||
} else {
|
||||
if (last_metadata_hub_modify_prolog_file) {
|
||||
free(last_metadata_hub_modify_prolog_file);
|
||||
@@ -357,13 +357,13 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
// get the one-byte number as an unsigned number
|
||||
int song_data_kind = data[0]; // one byte
|
||||
song_data_kind = song_data_kind & 0xFF; // unsigned
|
||||
debug(2, "MH Song Data Kind seen: \"%d\" of length %u.", song_data_kind, length);
|
||||
debug(3, "MH Song Data Kind seen: \"%d\" of length %u.", song_data_kind, length);
|
||||
if ((song_data_kind != metadata_store.song_data_kind) ||
|
||||
(metadata_store.song_data_kind_is_valid == 0)) {
|
||||
metadata_store.song_data_kind = song_data_kind;
|
||||
metadata_store.song_data_kind_changed = 1;
|
||||
metadata_store.song_data_kind_is_valid = 1;
|
||||
debug(2, "MH Song Data Kind set to: \"%d\"", metadata_store.song_data_kind);
|
||||
debug(3, "MH Song Data Kind set to: \"%d\"", metadata_store.song_data_kind);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
} break;
|
||||
@@ -373,31 +373,31 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
vl = vl << 32; // shift them into the correct location
|
||||
uint64_t ul = ntohl(*(uint32_t *)(data + sizeof(uint32_t))); // and the low order 32 bits
|
||||
vl = vl + ul;
|
||||
debug(2, "MH Item ID seen: \"%" PRIx64 "\" of length %u.", vl, length);
|
||||
debug(3, "MH Item ID seen: \"%" PRIx64 "\" of length %u.", vl, length);
|
||||
if ((vl != metadata_store.item_id) || (metadata_store.item_id_is_valid == 0)) {
|
||||
metadata_store.item_id = vl;
|
||||
metadata_store.item_id_changed = 1;
|
||||
metadata_store.item_id_is_valid = 1;
|
||||
debug(2, "MH Item ID set to: \"%" PRIx64 "\"", metadata_store.item_id);
|
||||
debug(3, "MH Item ID set to: \"%" PRIx64 "\"", metadata_store.item_id);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
} break;
|
||||
case 'astm': {
|
||||
uint32_t ui = ntohl(*(uint32_t *)data);
|
||||
debug(2, "MH Song Time seen: \"%u\" of length %u.", ui, length);
|
||||
debug(3, "MH Song Time seen: \"%u\" of length %u.", ui, length);
|
||||
if ((ui != metadata_store.songtime_in_milliseconds) ||
|
||||
(metadata_store.songtime_in_milliseconds_is_valid == 0)) {
|
||||
metadata_store.songtime_in_milliseconds = ui;
|
||||
metadata_store.songtime_in_milliseconds_changed = 1;
|
||||
metadata_store.songtime_in_milliseconds_is_valid = 1;
|
||||
debug(2, "MH Song Time set to: \"%u\"", metadata_store.songtime_in_milliseconds);
|
||||
debug(3, "MH Song Time set to: \"%u\"", metadata_store.songtime_in_milliseconds);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
} break;
|
||||
case 'asal':
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.album_name, &metadata_store.album_name_changed, cs)) {
|
||||
debug(2, "MH Album name set to: \"%s\"", metadata_store.album_name);
|
||||
debug(3, "MH Album name set to: \"%s\"", metadata_store.album_name);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
free(cs);
|
||||
@@ -405,7 +405,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
case 'asar':
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.artist_name, &metadata_store.artist_name_changed, cs)) {
|
||||
debug(2, "MH Artist name set to: \"%s\"", metadata_store.artist_name);
|
||||
debug(3, "MH Artist name set to: \"%s\"", metadata_store.artist_name);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
free(cs);
|
||||
@@ -414,7 +414,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.album_artist_name,
|
||||
&metadata_store.album_artist_name_changed, cs)) {
|
||||
debug(2, "MH Album Artist name set to: \"%s\"", metadata_store.album_artist_name);
|
||||
debug(3, "MH Album Artist name set to: \"%s\"", metadata_store.album_artist_name);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
free(cs);
|
||||
@@ -422,7 +422,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
case 'ascm':
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.comment, &metadata_store.comment_changed, cs)) {
|
||||
debug(2, "MH Comment set to: \"%s\"", metadata_store.comment);
|
||||
debug(3, "MH Comment set to: \"%s\"", metadata_store.comment);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
free(cs);
|
||||
@@ -430,7 +430,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
case 'asgn':
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.genre, &metadata_store.genre_changed, cs)) {
|
||||
debug(2, "MH Genre set to: \"%s\"", metadata_store.genre);
|
||||
debug(3, "MH Genre set to: \"%s\"", metadata_store.genre);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
free(cs);
|
||||
@@ -438,7 +438,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
case 'minm':
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.track_name, &metadata_store.track_name_changed, cs)) {
|
||||
debug(2, "MH Track Name set to: \"%s\"", metadata_store.track_name);
|
||||
debug(3, "MH Track Name set to: \"%s\"", metadata_store.track_name);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
free(cs);
|
||||
@@ -446,7 +446,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
case 'ascp':
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.composer, &metadata_store.composer_changed, cs)) {
|
||||
debug(2, "MH Composer set to: \"%s\"", metadata_store.composer);
|
||||
debug(3, "MH Composer set to: \"%s\"", metadata_store.composer);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
free(cs);
|
||||
@@ -455,7 +455,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.song_description, &metadata_store.song_description_changed,
|
||||
cs)) {
|
||||
debug(2, "MH Song Description set to: \"%s\"", metadata_store.song_description);
|
||||
debug(3, "MH Song Description set to: \"%s\"", metadata_store.song_description);
|
||||
}
|
||||
free(cs);
|
||||
break;
|
||||
@@ -463,7 +463,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.song_album_artist,
|
||||
&metadata_store.song_album_artist_changed, cs)) {
|
||||
debug(2, "MH Song Album Artist set to: \"%s\"", metadata_store.song_album_artist);
|
||||
debug(3, "MH Song Album Artist set to: \"%s\"", metadata_store.song_album_artist);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
free(cs);
|
||||
@@ -471,7 +471,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
case 'assn':
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.sort_name, &metadata_store.sort_name_changed, cs)) {
|
||||
debug(2, "MH Sort Name set to: \"%s\"", metadata_store.sort_name);
|
||||
debug(3, "MH Sort Name set to: \"%s\"", metadata_store.sort_name);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
free(cs);
|
||||
@@ -479,7 +479,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
case 'assa':
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.sort_artist, &metadata_store.sort_artist_changed, cs)) {
|
||||
debug(2, "MH Sort Artist set to: \"%s\"", metadata_store.sort_artist);
|
||||
debug(3, "MH Sort Artist set to: \"%s\"", metadata_store.sort_artist);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
free(cs);
|
||||
@@ -487,7 +487,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
case 'assu':
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.sort_album, &metadata_store.sort_album_changed, cs)) {
|
||||
debug(2, "MH Sort Album set to: \"%s\"", metadata_store.sort_album);
|
||||
debug(3, "MH Sort Album set to: \"%s\"", metadata_store.sort_album);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
free(cs);
|
||||
@@ -495,7 +495,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
case 'assc':
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.sort_composer, &metadata_store.sort_composer_changed, cs)) {
|
||||
debug(2, "MH Sort Composer set to: \"%s\"", metadata_store.sort_composer);
|
||||
debug(3, "MH Sort Composer set to: \"%s\"", metadata_store.sort_composer);
|
||||
metadata_packet_item_changed = 1;
|
||||
}
|
||||
free(cs);
|
||||
@@ -528,18 +528,18 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
case 'pcen':
|
||||
break;
|
||||
case 'mdst':
|
||||
debug(2, "MH Metadata stream processing start.");
|
||||
debug(3, "MH Metadata stream processing start.");
|
||||
metadata_packet_item_changed = 0;
|
||||
break;
|
||||
case 'mden':
|
||||
if (metadata_packet_item_changed != 0)
|
||||
debug(2, "MH Metadata stream processing end with changes.");
|
||||
debug(3, "MH Metadata stream processing end with changes.");
|
||||
else
|
||||
debug(2, "MH Metadata stream processing end without changes.");
|
||||
debug(3, "MH Metadata stream processing end without changes.");
|
||||
changed = metadata_packet_item_changed;
|
||||
break;
|
||||
case 'PICT':
|
||||
debug(2, "MH Picture received, length %u bytes.", length);
|
||||
debug(3, "MH Picture received, length %u bytes.", length);
|
||||
|
||||
char uri[2048];
|
||||
if ((length > 16) &&
|
||||
@@ -567,7 +567,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.client_ip, &metadata_store.client_ip_changed, cs)) {
|
||||
changed = 1;
|
||||
debug(2, "MH Client IP set to: \"%s\"", metadata_store.client_ip);
|
||||
debug(3, "MH Client IP set to: \"%s\"", metadata_store.client_ip);
|
||||
}
|
||||
free(cs);
|
||||
break;
|
||||
@@ -575,7 +575,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.client_name, &metadata_store.client_name_changed, cs)) {
|
||||
changed = 1;
|
||||
debug(2, "MH Client Name set to: \"%s\"", metadata_store.client_name);
|
||||
debug(3, "MH Client Name set to: \"%s\"", metadata_store.client_name);
|
||||
}
|
||||
free(cs);
|
||||
break;
|
||||
@@ -584,7 +584,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
if (string_update(&metadata_store.progress_string, &metadata_store.progress_string_changed,
|
||||
cs)) {
|
||||
changed = 1;
|
||||
debug(2, "MH Progress String set to: \"%s\"", metadata_store.progress_string);
|
||||
debug(3, "MH Progress String set to: \"%s\"", metadata_store.progress_string);
|
||||
}
|
||||
free(cs);
|
||||
break;
|
||||
@@ -593,7 +593,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
if (string_update(&metadata_store.frame_position_string,
|
||||
&metadata_store.frame_position_string_changed, cs)) {
|
||||
changed = 1;
|
||||
debug(2, "MH Frame Position String set to: \"%s\"", metadata_store.frame_position_string);
|
||||
debug(3, "MH Frame Position String set to: \"%s\"", metadata_store.frame_position_string);
|
||||
}
|
||||
free(cs);
|
||||
break;
|
||||
@@ -602,7 +602,7 @@ 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\"",
|
||||
debug(3, "MH First Frame Position String set to: \"%s\"",
|
||||
metadata_store.first_frame_position_string);
|
||||
}
|
||||
free(cs);
|
||||
@@ -611,7 +611,23 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.stream_type, &metadata_store.stream_type_changed, cs)) {
|
||||
changed = 1;
|
||||
debug(2, "MH Stream Type set to: \"%s\"", metadata_store.stream_type);
|
||||
debug(3, "MH Stream Type set to: \"%s\"", metadata_store.stream_type);
|
||||
}
|
||||
free(cs);
|
||||
break;
|
||||
case 'sdsc':
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.source_format, &metadata_store.source_format_changed, cs)) {
|
||||
changed = 1;
|
||||
debug(3, "MH Source Format set to: \"%s\"", metadata_store.source_format);
|
||||
}
|
||||
free(cs);
|
||||
break;
|
||||
case 'odsc':
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.output_format, &metadata_store.output_format_changed, cs)) {
|
||||
changed = 1;
|
||||
debug(3, "MH Output Format set to: \"%s\"", metadata_store.output_format);
|
||||
}
|
||||
free(cs);
|
||||
break;
|
||||
@@ -619,7 +635,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin
|
||||
cs = strndup(data, length);
|
||||
if (string_update(&metadata_store.server_ip, &metadata_store.server_ip_changed, cs)) {
|
||||
changed = 1;
|
||||
debug(2, "MH Server IP set to: \"%s\"", metadata_store.server_ip);
|
||||
debug(3, "MH Server IP set to: \"%s\"", metadata_store.server_ip);
|
||||
}
|
||||
free(cs);
|
||||
break;
|
||||
|
||||
@@ -52,6 +52,12 @@ typedef struct metadata_bundle {
|
||||
char *stream_type; // Realtime or Buffered
|
||||
int stream_type_changed;
|
||||
|
||||
char *source_format; // Format of incoming audio, e.g. AAC/44100/S16_LE/2
|
||||
int source_format_changed;
|
||||
|
||||
char *output_format; // Format of outgoing audio, e.g. 44100/S32_LE/2 (always PCM)
|
||||
int output_format_changed;
|
||||
|
||||
char *progress_string; // progress string, emitted by the source from time to time
|
||||
int progress_string_changed;
|
||||
|
||||
|
||||
+19
-29
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2018 -- 2020
|
||||
* Copyright (c) Mike Brady 2018--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -40,6 +40,8 @@
|
||||
#include "metadata_hub.h"
|
||||
#include "mpris-service.h"
|
||||
|
||||
static guint ownerID = 0;
|
||||
|
||||
MediaPlayer2 *mprisPlayerSkeleton;
|
||||
MediaPlayer2Player *mprisPlayerPlayerSkeleton;
|
||||
|
||||
@@ -228,9 +230,9 @@ void mpris_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)
|
||||
|
||||
static gboolean on_handle_quit(MediaPlayer2 *skeleton, GDBusMethodInvocation *invocation,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
debug(1, "quit requested (MPRIS interface).");
|
||||
type_of_exit_cleanup = TOE_dbus; // request an exit cleanup that is compatible with dbus
|
||||
exit(EXIT_SUCCESS);
|
||||
debug(1, ">> quit request...");
|
||||
config.quit_requested_from_glib_mainloop = 1;
|
||||
g_main_loop_quit(config.glib_worker_loop);
|
||||
media_player2_complete_quit(skeleton, invocation);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -295,8 +297,7 @@ static void on_mpris_name_acquired(GDBusConnection *connection, const gchar *nam
|
||||
|
||||
const char *empty_string_array[] = {NULL};
|
||||
|
||||
// debug(1, "MPRIS well-known interface name \"%s\" acquired on the %s bus.", name,
|
||||
// (config.mpris_service_bus_type == DBT_session) ? "session" : "system");
|
||||
debug(2, "MPRIS well-known interface name \"%s\" acquired on the %s bus.", name, (config.mpris_service_bus_type == DBT_session) ? "session" : "system");
|
||||
mprisPlayerSkeleton = media_player2_skeleton_new();
|
||||
mprisPlayerPlayerSkeleton = media_player2_player_skeleton_new();
|
||||
|
||||
@@ -343,30 +344,12 @@ static void on_mpris_name_acquired(GDBusConnection *connection, const gchar *nam
|
||||
(config.mpris_service_bus_type == DBT_session) ? "session" : "system");
|
||||
}
|
||||
|
||||
static void on_mpris_name_lost_again(__attribute__((unused)) GDBusConnection *connection,
|
||||
static void on_mpris_name_lost(__attribute__((unused)) GDBusConnection *connection,
|
||||
const gchar *name,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
warn("could not acquire an MPRIS interface named \"%s\" on the %s bus.", name,
|
||||
(config.mpris_service_bus_type == DBT_session) ? "session" : "system");
|
||||
}
|
||||
|
||||
static void on_mpris_name_lost(__attribute__((unused)) GDBusConnection *connection,
|
||||
__attribute__((unused)) const gchar *name,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
// debug(1, "Could not acquire MPRIS interface \"%s\" on the %s bus -- will try adding the process
|
||||
// "
|
||||
// "number to the end of it.",
|
||||
// name,(mpris_bus_type==G_BUS_TYPE_SESSION) ? "session" : "system");
|
||||
pid_t pid = getpid();
|
||||
char interface_name[256] = "";
|
||||
snprintf(interface_name, sizeof(interface_name), "org.mpris.MediaPlayer2.ShairportSync.i%d", pid);
|
||||
GBusType mpris_bus_type = G_BUS_TYPE_SYSTEM;
|
||||
if (config.mpris_service_bus_type == DBT_session)
|
||||
mpris_bus_type = G_BUS_TYPE_SESSION;
|
||||
// debug(1, "Looking for an MPRIS interface \"%s\" on the %s bus.",interface_name,
|
||||
// (mpris_bus_type==G_BUS_TYPE_SESSION) ? "session" : "system");
|
||||
g_bus_own_name(mpris_bus_type, interface_name, G_BUS_NAME_OWNER_FLAGS_NONE, NULL,
|
||||
on_mpris_name_acquired, on_mpris_name_lost_again, NULL, NULL);
|
||||
ownerID = 0;
|
||||
}
|
||||
|
||||
int start_mpris_service() {
|
||||
@@ -377,8 +360,15 @@ int start_mpris_service() {
|
||||
mpris_bus_type = G_BUS_TYPE_SESSION;
|
||||
// debug(1, "Looking for an MPRIS interface \"org.mpris.MediaPlayer2.ShairportSync\" on the %s
|
||||
// bus.",(mpris_bus_type==G_BUS_TYPE_SESSION) ? "session" : "system");
|
||||
g_bus_own_name(mpris_bus_type, "org.mpris.MediaPlayer2.ShairportSync",
|
||||
G_BUS_NAME_OWNER_FLAGS_NONE, NULL, on_mpris_name_acquired, on_mpris_name_lost,
|
||||
NULL, NULL);
|
||||
ownerID = g_bus_own_name(mpris_bus_type, "org.mpris.MediaPlayer2.ShairportSync",
|
||||
G_BUS_NAME_OWNER_FLAGS_NONE, NULL, on_mpris_name_acquired,
|
||||
on_mpris_name_lost, NULL, NULL);
|
||||
return 0; // this is just to quieten a compiler warning
|
||||
}
|
||||
|
||||
void stop_mpris_service() {
|
||||
if (ownerID) {
|
||||
debug(2, "stopping MPRIS service -- unowning ownerID %d.", ownerID);
|
||||
g_bus_unown_name(ownerID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,6 @@ extern MediaPlayer2 *mprisPlayerSkeleton;
|
||||
extern MediaPlayer2Player *mprisPlayerPlayerSkeleton;
|
||||
|
||||
int start_mpris_service();
|
||||
void stop_mpris_service();
|
||||
|
||||
#endif /* #ifndef MPRIS_SERVICE_H */
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
// this holds the mosquitto client
|
||||
struct mosquitto *global_mosq = NULL;
|
||||
char *topic = NULL;
|
||||
int connected = 0;
|
||||
|
||||
// mosquitto logging
|
||||
@@ -99,126 +98,126 @@ void on_connect(struct mosquitto *mosq, __attribute__((unused)) void *userdata,
|
||||
|
||||
// function to send autodiscovery messages for Home Assistant
|
||||
void send_autodiscovery_messages(struct mosquitto *mosq) {
|
||||
const char *device_name = config.service_name;
|
||||
const char *device_name = config.service_name;
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
const char *device_id = config.airplay_device_id ? config.airplay_device_id : config.service_name;
|
||||
const char *device_id = config.airplay_device_id ? config.airplay_device_id : config.service_name;
|
||||
#else
|
||||
const char *device_id = config.service_name;
|
||||
const char *device_id = config.service_name;
|
||||
#endif
|
||||
const char *device_id_no_colons = str_replace(device_id, ":", "");
|
||||
const char *sw_version = get_version_string();
|
||||
const char *model = "shairport-sync";
|
||||
const char *model_friendly = "Shairport Sync";
|
||||
const char *manufacturer = "Mike Brady";
|
||||
const char *autodiscovery_prefix = (config.mqtt_autodiscovery_prefix != NULL) ?
|
||||
config.mqtt_autodiscovery_prefix : "homeassistant";
|
||||
const char *device_id_no_colons = str_replace(device_id, ":", "");
|
||||
const char *sw_version = get_version_string();
|
||||
const char *model = "shairport-sync";
|
||||
const char *model_friendly = "Shairport Sync";
|
||||
const char *manufacturer = "Mike Brady";
|
||||
const char *autodiscovery_prefix = (config.mqtt_autodiscovery_prefix != NULL)
|
||||
? config.mqtt_autodiscovery_prefix
|
||||
: "homeassistant";
|
||||
|
||||
char topic[512];
|
||||
char payload[1280];
|
||||
char device_payload[512];
|
||||
char id_string[128];
|
||||
char topic[512];
|
||||
char payload[1280];
|
||||
char device_payload[512];
|
||||
char id_string[128];
|
||||
|
||||
snprintf(device_payload, sizeof(device_payload),
|
||||
"\"device\": {"
|
||||
"\"identifiers\": [\"%s\"],"
|
||||
"\"name\": \"%s\","
|
||||
"\"model\": \"%s\","
|
||||
"\"sw_version\": \"%s\","
|
||||
"\"manufacturer\": \"%s\""
|
||||
snprintf(device_payload, sizeof(device_payload),
|
||||
"\"device\": {"
|
||||
"\"identifiers\": [\"%s\"],"
|
||||
"\"name\": \"%s\","
|
||||
"\"model\": \"%s\","
|
||||
"\"sw_version\": \"%s\","
|
||||
"\"manufacturer\": \"%s\""
|
||||
"}",
|
||||
device_id, device_name, model_friendly, sw_version, manufacturer);
|
||||
|
||||
// when adding sensors here, be sure to also update sensor_names and icons below!
|
||||
const char *sensors[] = {"artist",
|
||||
"album",
|
||||
"title",
|
||||
"genre",
|
||||
"format",
|
||||
"output_format",
|
||||
"output_frame_rate",
|
||||
"track_id",
|
||||
"client_ip",
|
||||
"client_mac_address",
|
||||
"client_name",
|
||||
"client_model",
|
||||
"client_device_id",
|
||||
"server_ip",
|
||||
"volume",
|
||||
"active",
|
||||
"playing",
|
||||
NULL};
|
||||
|
||||
const char *sensor_names[] = {"Artist",
|
||||
"Album",
|
||||
"Title",
|
||||
"Genre",
|
||||
"Format",
|
||||
"Output Format",
|
||||
"Output Frame Rate",
|
||||
"Track ID",
|
||||
"Client IP",
|
||||
"Client MAC Address",
|
||||
"Client Name",
|
||||
"Client Model",
|
||||
"Client Device ID",
|
||||
"Server IP",
|
||||
"Volume",
|
||||
"Active Session",
|
||||
"Playing"};
|
||||
|
||||
const char *icons[] = {
|
||||
"mdi:account-music", // artist
|
||||
"mdi:album", // album
|
||||
"mdi:music", // title
|
||||
"mdi:music-box-multiple", // genre
|
||||
"mdi:file", // format
|
||||
"mdi:file", // output format
|
||||
"mdi:file-chart", // output frame rate
|
||||
"mdi:identifier", // track ID
|
||||
"mdi:ip", // client IP
|
||||
"mdi:hexadecimal", // client MAC address
|
||||
"mdi:cellphone-text", // client name
|
||||
"mdi:cellphone-text", // client model
|
||||
"mdi:hexadecimal", // client device ID
|
||||
"mdi:ip-network", // server IP
|
||||
"mdi:volume-high", // volume
|
||||
"mdi:play-box-multiple", // active
|
||||
"mdi:play-box-multiple-outline" // playing
|
||||
};
|
||||
|
||||
for (int i = 0; sensors[i] != NULL; i++) {
|
||||
bool is_binary_sensor =
|
||||
(strcmp(sensors[i], "active") == 0 || strcmp(sensors[i], "playing") == 0);
|
||||
bool is_volume_sensor = strcmp(sensors[i], "volume") == 0;
|
||||
|
||||
snprintf(topic, sizeof(topic), "%s/%ssensor/%s_%s/%s/config", autodiscovery_prefix,
|
||||
is_binary_sensor ? "binary_" : "", model, device_id_no_colons, sensors[i]);
|
||||
|
||||
snprintf(id_string, sizeof(id_string), "%s_%s_%s", model, device_name, sensors[i]);
|
||||
|
||||
snprintf(
|
||||
payload, sizeof(payload),
|
||||
"{"
|
||||
"\"name\": \"%s\","
|
||||
"\"state_topic\": \"%s/%s\","
|
||||
"\"icon\": \"%s\","
|
||||
"\"unique_id\": \"%s\","
|
||||
"\"object_id\": \"%s\","
|
||||
"%s%s%s"
|
||||
"}",
|
||||
device_id, device_name, model_friendly, sw_version, manufacturer);
|
||||
sensor_names[i], config.mqtt_topic, sensors[i], icons[i], id_string, id_string,
|
||||
is_binary_sensor ? "\"payload_on\": \"1\",\"payload_off\": \"0\"," : "",
|
||||
is_volume_sensor
|
||||
? "\"value_template\": \"{{ ((value | regex_findall_index("
|
||||
"find='^(.+?),', index=0, ignorecase=False) | float / 30 + 1) * 100) | round(0) }}\","
|
||||
"\"unit_of_measurement\": \"%\","
|
||||
: "",
|
||||
device_payload);
|
||||
|
||||
// when adding sensors here, be sure to also update sensor_names and icons below!
|
||||
const char *sensors[] = {
|
||||
"artist",
|
||||
"album",
|
||||
"title",
|
||||
"genre",
|
||||
"format",
|
||||
"output_format",
|
||||
"output_frame_rate",
|
||||
"track_id",
|
||||
"client_ip",
|
||||
"client_mac_address",
|
||||
"client_name",
|
||||
"client_model",
|
||||
"client_device_id",
|
||||
"server_ip",
|
||||
"volume",
|
||||
"active",
|
||||
"playing",
|
||||
NULL
|
||||
};
|
||||
|
||||
const char *sensor_names[] = {
|
||||
"Artist",
|
||||
"Album",
|
||||
"Title",
|
||||
"Genre",
|
||||
"Format",
|
||||
"Output Format",
|
||||
"Output Frame Rate",
|
||||
"Track ID",
|
||||
"Client IP",
|
||||
"Client MAC Address",
|
||||
"Client Name",
|
||||
"Client Model",
|
||||
"Client Device ID",
|
||||
"Server IP",
|
||||
"Volume",
|
||||
"Active Session",
|
||||
"Playing"
|
||||
};
|
||||
|
||||
const char *icons[] = {
|
||||
"mdi:account-music", // artist
|
||||
"mdi:album", // album
|
||||
"mdi:music", // title
|
||||
"mdi:music-box-multiple", // genre
|
||||
"mdi:file", // format
|
||||
"mdi:file", // output format
|
||||
"mdi:file-chart", // output frame rate
|
||||
"mdi:identifier", // track ID
|
||||
"mdi:ip", // client IP
|
||||
"mdi:hexadecimal", // client MAC address
|
||||
"mdi:cellphone-text", // client name
|
||||
"mdi:cellphone-text", // client model
|
||||
"mdi:hexadecimal", // client device ID
|
||||
"mdi:ip-network", // server IP
|
||||
"mdi:volume-high", // volume
|
||||
"mdi:play-box-multiple", // active
|
||||
"mdi:play-box-multiple-outline" // playing
|
||||
};
|
||||
|
||||
for (int i = 0; sensors[i] != NULL; i++) {
|
||||
bool is_binary_sensor = (strcmp(sensors[i], "active") == 0 || strcmp(sensors[i], "playing") == 0);
|
||||
bool is_volume_sensor = strcmp(sensors[i], "volume") == 0;
|
||||
|
||||
snprintf(topic, sizeof(topic), "%s/%ssensor/%s_%s/%s/config",
|
||||
autodiscovery_prefix, is_binary_sensor ? "binary_" : "",
|
||||
model, device_id_no_colons, sensors[i]);
|
||||
|
||||
snprintf(id_string, sizeof(id_string), "%s_%s_%s", model, device_name, sensors[i]);
|
||||
|
||||
snprintf(payload, sizeof(payload),
|
||||
"{"
|
||||
"\"name\": \"%s\","
|
||||
"\"state_topic\": \"%s/%s\","
|
||||
"\"icon\": \"%s\","
|
||||
"\"unique_id\": \"%s\","
|
||||
"\"object_id\": \"%s\","
|
||||
"%s%s%s"
|
||||
"}",
|
||||
sensor_names[i], config.mqtt_topic, sensors[i], icons[i], id_string, id_string,
|
||||
is_binary_sensor ? "\"payload_on\": \"1\",\"payload_off\": \"0\"," : "",
|
||||
is_volume_sensor ? "\"value_template\": \"{{ ((value | regex_findall_index("
|
||||
"find='^(.+?),', index=0, ignorecase=False) | float / 30 + 1) * 100) | round(0) }}\","
|
||||
"\"unit_of_measurement\": \"%\"," : "",
|
||||
device_payload);
|
||||
|
||||
mosquitto_publish(mosq, NULL, topic, strlen(payload), payload, 0, true);
|
||||
debug(2, "[MQTT]: published autodiscovery for %s", id_string);
|
||||
}
|
||||
mosquitto_publish(mosq, NULL, topic, strlen(payload), payload, 0, true);
|
||||
debug(2, "[MQTT]: published autodiscovery for %s", id_string);
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to publish under a topic and automatically append the main topic
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of the nqptp distribution (https://github.com/mikebrady/nqptp).
|
||||
* Copyright (c) 2021--2023 Mike Brady.
|
||||
* Copyright (c) 2021--2025 Mike Brady.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<property name="Protocol" type="s" access="read" />
|
||||
<property name="ServiceName" type="s" access="read" />
|
||||
<property name="OutputFormat" type="s" access="read" />
|
||||
<property name="OutputRate" type="i" access="read" />
|
||||
<property name="SourceFormat" type="s" access="read" />
|
||||
<property name="FirstFramePosition" type="s" access="read" />
|
||||
<property name="FramePosition" type="s" access="read" />
|
||||
<method name="SetFramePositionUpdateInterval">
|
||||
|
||||
@@ -214,7 +214,7 @@ struct pair_definition
|
||||
int (*pair_remove)(uint8_t **out, size_t *out_len, pair_cb cb, void *cb_arg, const uint8_t *in, size_t in_len);
|
||||
int (*pair_list)(uint8_t **out, size_t *out_len, pair_list_cb cb, void *cb_arg, const uint8_t *in, size_t in_len);
|
||||
|
||||
struct pair_cipher_context *(*pair_cipher_new)(struct pair_definition *type, int channel, const uint8_t *shared_secret, size_t shared_secret_len);
|
||||
struct pair_cipher_context *(*pair_cipher_new)(struct pair_definition *type, int channel, const uint8_t *shared_secret, size_t shared_secret_len, const char *dynamic_salt_suffix);
|
||||
void (*pair_cipher_free)(struct pair_cipher_context *cctx);
|
||||
|
||||
ssize_t (*pair_encrypt)(uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pair_cipher_context *cctx);
|
||||
|
||||
+2
-2
@@ -687,12 +687,12 @@ pair_verify_result(struct pair_result **result, struct pair_verify_context *vctx
|
||||
}
|
||||
|
||||
struct pair_cipher_context *
|
||||
pair_cipher_new(enum pair_type type, int channel, const uint8_t *shared_secret, size_t shared_secret_len)
|
||||
pair_cipher_new(enum pair_type type, int channel, const uint8_t *shared_secret, size_t shared_secret_len, const char *dynamic_salt_suffix)
|
||||
{
|
||||
if (!pair[type]->pair_cipher_new)
|
||||
return NULL;
|
||||
|
||||
return pair[type]->pair_cipher_new(pair[type], channel, shared_secret, shared_secret_len);
|
||||
return pair[type]->pair_cipher_new(pair[type], channel, shared_secret, shared_secret_len, dynamic_salt_suffix);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
+1
-1
@@ -204,7 +204,7 @@ pair_verify_response2(struct pair_verify_context *vctx, const uint8_t *in, size_
|
||||
* create a ciphering context.
|
||||
*/
|
||||
struct pair_cipher_context *
|
||||
pair_cipher_new(enum pair_type type, int channel, const uint8_t *shared_secret, size_t shared_secret_len);
|
||||
pair_cipher_new(enum pair_type type, int channel, const uint8_t *shared_secret, size_t shared_secret_len, const char *dynamic_salt_suffix);
|
||||
void
|
||||
pair_cipher_free(struct pair_cipher_context *cctx);
|
||||
|
||||
|
||||
+82
-34
@@ -75,6 +75,8 @@ enum pair_keys
|
||||
PAIR_CONTROL_READ,
|
||||
PAIR_EVENTS_WRITE,
|
||||
PAIR_EVENTS_READ,
|
||||
PAIR_DATA_WRITE,
|
||||
PAIR_DATA_READ,
|
||||
};
|
||||
|
||||
struct pair_keys_map
|
||||
@@ -110,6 +112,14 @@ static struct pair_keys_map pair_keys_map[] =
|
||||
// Encryption/decryption of event channel
|
||||
{ 0, "Events-Salt", "Events-Write-Encryption-Key", "" },
|
||||
{ 0, "Events-Salt", "Events-Read-Encryption-Key", "" },
|
||||
|
||||
// Encryption/decryption of data channel
|
||||
// The salt is dynamic -- the 64-bit `seed` provided during SETUP, treated as an unsigned unmber, is appended:
|
||||
// e.g. DataStream-Salt15014746994705656022.
|
||||
// Sincere thanks to Pierre Stahl, (latex: St\aahl) https://github.com/postlund.
|
||||
// Please see https://pyatv.dev/documentation/protocols/#remote-control, AirPlay 2 / Remote Control / Data Channel / Encryption
|
||||
{ 0, "DataStream-Salt", "DataStream-Output-Encryption-Key", "" },
|
||||
{ 0, "DataStream-Salt", "DataStream-Input-Encryption-Key", "" },
|
||||
};
|
||||
|
||||
enum pair_method {
|
||||
@@ -857,7 +867,7 @@ message_process(const uint8_t *data, size_t data_len, const char **errmsg)
|
||||
else if (error->value[0] == TLVError_MaxPeers)
|
||||
*errmsg = "Max peers trying to connect to device\n";
|
||||
else if (error->value[0] == TLVError_MaxTries)
|
||||
*errmsg = "Max pairing attemps reached\n";
|
||||
*errmsg = "Max pairing attempts reached\n";
|
||||
else if (error->value[0] == TLVError_Unavailable)
|
||||
*errmsg = "Device is unuavailble at this time\n";
|
||||
else
|
||||
@@ -877,9 +887,11 @@ message_process(const uint8_t *data, size_t data_len, const char **errmsg)
|
||||
|
||||
hkdfExtract(SHA512, salt, salt_len, ikm, ikm_len, prk);
|
||||
hkdfExpand(SHA512, prk, SHA512_LEN, info, info_len, okm, okm_len);
|
||||
|
||||
add an explicit salt value to allow for a unique salt to be given at initialisation
|
||||
*/
|
||||
static int
|
||||
hkdf_extract_expand(uint8_t *okm, size_t okm_len, const uint8_t *ikm, size_t ikm_len, enum pair_keys pair_key)
|
||||
hkdf_extract_expand_with_salt_and_info(uint8_t *okm, size_t okm_len, const uint8_t *ikm, size_t ikm_len, const char *salt, const char *info)
|
||||
{
|
||||
#ifdef CONFIG_OPENSSL
|
||||
#include <openssl/kdf.h>
|
||||
@@ -893,11 +905,11 @@ hkdf_extract_expand(uint8_t *okm, size_t okm_len, const uint8_t *ikm, size_t ikm
|
||||
goto error;
|
||||
if (EVP_PKEY_CTX_set_hkdf_md(pctx, EVP_sha512()) <= 0)
|
||||
goto error;
|
||||
if (EVP_PKEY_CTX_set1_hkdf_salt(pctx, (const unsigned char *)pair_keys_map[pair_key].salt, strlen(pair_keys_map[pair_key].salt)) <= 0)
|
||||
if (EVP_PKEY_CTX_set1_hkdf_salt(pctx, salt, strlen(salt)) <= 0)
|
||||
goto error;
|
||||
if (EVP_PKEY_CTX_set1_hkdf_key(pctx, ikm, ikm_len) <= 0)
|
||||
goto error;
|
||||
if (EVP_PKEY_CTX_add1_hkdf_info(pctx, (const unsigned char *)pair_keys_map[pair_key].info, strlen(pair_keys_map[pair_key].info)) <= 0)
|
||||
if (EVP_PKEY_CTX_add1_hkdf_info(pctx, info, strlen(info)) <= 0)
|
||||
goto error;
|
||||
if (EVP_PKEY_derive(pctx, okm, &okm_len) <= 0)
|
||||
goto error;
|
||||
@@ -916,7 +928,7 @@ hkdf_extract_expand(uint8_t *okm, size_t okm_len, const uint8_t *ikm, size_t ikm
|
||||
return -1; // Below calculation not valid if output is larger than hash size
|
||||
if (gcry_md_open(&hmac_handle, GCRY_MD_SHA512, GCRY_MD_FLAG_HMAC) != GPG_ERR_NO_ERROR)
|
||||
return -1;
|
||||
if (gcry_md_setkey(hmac_handle, (const unsigned char *)pair_keys_map[pair_key].salt, strlen(pair_keys_map[pair_key].salt)) != GPG_ERR_NO_ERROR)
|
||||
if (gcry_md_setkey(hmac_handle, salt, strlen(salt)) != GPG_ERR_NO_ERROR)
|
||||
goto error;
|
||||
gcry_md_write(hmac_handle, ikm, ikm_len);
|
||||
memcpy(prk, gcry_md_read(hmac_handle, 0), sizeof(prk));
|
||||
@@ -925,7 +937,7 @@ hkdf_extract_expand(uint8_t *okm, size_t okm_len, const uint8_t *ikm, size_t ikm
|
||||
|
||||
if (gcry_md_setkey(hmac_handle, prk, sizeof(prk)) != GPG_ERR_NO_ERROR)
|
||||
goto error;
|
||||
gcry_md_write(hmac_handle, (const unsigned char *)pair_keys_map[pair_key].info, strlen(pair_keys_map[pair_key].info));
|
||||
gcry_md_write(hmac_handle, info, strlen(info));
|
||||
gcry_md_putc(hmac_handle, 1);
|
||||
|
||||
memcpy(okm, gcry_md_read(hmac_handle, 0), okm_len);
|
||||
@@ -941,6 +953,17 @@ hkdf_extract_expand(uint8_t *okm, size_t okm_len, const uint8_t *ikm, size_t ikm
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Executes SHA512 RFC 5869 extract + expand, writing a derived key to okm
|
||||
|
||||
hkdfExtract(SHA512, salt, salt_len, ikm, ikm_len, prk);
|
||||
hkdfExpand(SHA512, prk, SHA512_LEN, info, info_len, okm, okm_len);
|
||||
*/
|
||||
static int
|
||||
hkdf_extract_expand(uint8_t *okm, size_t okm_len, const uint8_t *ikm, size_t ikm_len, enum pair_keys pair_key) {
|
||||
// pass in the salt that is in the pair_key_maps table
|
||||
return hkdf_extract_expand_with_salt_and_info(okm, okm_len, ikm, ikm_len, pair_keys_map[pair_key].salt, pair_keys_map[pair_key].info);
|
||||
}
|
||||
|
||||
static int
|
||||
encrypt_chacha(uint8_t *cipher, const uint8_t *plain, size_t plain_len, const uint8_t *key, size_t key_len, const void *ad, size_t ad_len, uint8_t *tag, size_t tag_len, const uint8_t nonce[NONCE_LENGTH])
|
||||
{
|
||||
@@ -2890,47 +2913,22 @@ cipher_free(struct pair_cipher_context *cctx)
|
||||
}
|
||||
|
||||
static struct pair_cipher_context *
|
||||
cipher_new(struct pair_definition *type, int channel, const uint8_t *shared_secret, size_t shared_secret_len)
|
||||
cipher_new_with_salt_and_info(struct pair_definition *type, const uint8_t *shared_secret, size_t shared_secret_len, const char *write_salt, const char *write_info, const char *read_salt, const char *read_info)
|
||||
{
|
||||
struct pair_cipher_context *cctx;
|
||||
enum pair_keys write_key;
|
||||
enum pair_keys read_key;
|
||||
int ret;
|
||||
|
||||
// Note that events is opposite, probably because it is a reverse connection
|
||||
switch (channel)
|
||||
{
|
||||
case 0:
|
||||
write_key = PAIR_CONTROL_WRITE;
|
||||
read_key = PAIR_CONTROL_READ;
|
||||
break;
|
||||
case 1:
|
||||
write_key = PAIR_EVENTS_READ;
|
||||
read_key = PAIR_EVENTS_WRITE;
|
||||
break;
|
||||
case 2:
|
||||
write_key = PAIR_CONTROL_READ;
|
||||
read_key = PAIR_CONTROL_WRITE;
|
||||
break;
|
||||
case 3:
|
||||
write_key = PAIR_EVENTS_WRITE;
|
||||
read_key = PAIR_EVENTS_READ;
|
||||
break;
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cctx = calloc(1, sizeof(struct pair_cipher_context));
|
||||
if (!cctx)
|
||||
goto error;
|
||||
|
||||
cctx->type = type;
|
||||
|
||||
ret = hkdf_extract_expand(cctx->encryption_key, sizeof(cctx->encryption_key), shared_secret, shared_secret_len, write_key);
|
||||
ret = hkdf_extract_expand_with_salt_and_info(cctx->encryption_key, sizeof(cctx->encryption_key), shared_secret, shared_secret_len, write_salt, write_info);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
ret = hkdf_extract_expand(cctx->decryption_key, sizeof(cctx->decryption_key), shared_secret, shared_secret_len, read_key);
|
||||
ret = hkdf_extract_expand_with_salt_and_info(cctx->decryption_key, sizeof(cctx->decryption_key), shared_secret, shared_secret_len, read_salt, read_info);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
@@ -2941,6 +2939,56 @@ cipher_new(struct pair_definition *type, int channel, const uint8_t *shared_secr
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct pair_cipher_context *
|
||||
cipher_new(struct pair_definition *type, int channel, const uint8_t *shared_secret, size_t shared_secret_len, const char *dynamic_salt_suffix)
|
||||
{
|
||||
|
||||
enum pair_keys write_key;
|
||||
enum pair_keys read_key;
|
||||
|
||||
// Note that events is opposite, probably because it is a reverse connection
|
||||
switch (channel)
|
||||
{
|
||||
case 0: // control
|
||||
write_key = PAIR_CONTROL_WRITE;
|
||||
read_key = PAIR_CONTROL_READ;
|
||||
break;
|
||||
case 1: // events
|
||||
write_key = PAIR_EVENTS_READ;
|
||||
read_key = PAIR_EVENTS_WRITE;
|
||||
break;
|
||||
case 2: // data
|
||||
write_key = PAIR_DATA_WRITE;
|
||||
read_key = PAIR_DATA_READ;
|
||||
break;
|
||||
case 3: // control
|
||||
write_key = PAIR_CONTROL_READ;
|
||||
read_key = PAIR_CONTROL_WRITE;
|
||||
break;
|
||||
case 4: // events
|
||||
write_key = PAIR_EVENTS_WRITE;
|
||||
read_key = PAIR_EVENTS_READ;
|
||||
break;
|
||||
case 5: // data
|
||||
write_key = PAIR_DATA_READ;
|
||||
read_key = PAIR_DATA_WRITE;
|
||||
break;
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
//if ((dynamic_salt_suffix == NULL) || (dynamic_salt_suffix[0] == '\0')) {
|
||||
// return cipher_new_with_salt_and_info(type, shared_secret, shared_secret_len, pair_keys_map[write_key].salt, pair_keys_map[write_key].info, pair_keys_map[read_key].salt, pair_keys_map[read_key].info);
|
||||
//} else {
|
||||
char write_salt[256] = "";
|
||||
char read_salt[256] = "";
|
||||
snprintf(write_salt, sizeof(write_salt), "%s%s", pair_keys_map[write_key].salt, dynamic_salt_suffix);
|
||||
//printf("channel %d, dynamic_salt_suffix: \"%s\", write_salt: \"%s\"\n", channel, dynamic_salt_suffix, write_salt);
|
||||
snprintf(read_salt, sizeof(read_salt), "%s%s", pair_keys_map[read_key].salt, dynamic_salt_suffix);
|
||||
//printf("channel %d, dynamic_salt_suffix: \"%s\", read_salt: \"%s\"\n", channel, dynamic_salt_suffix, read_salt);
|
||||
return cipher_new_with_salt_and_info(type, shared_secret, shared_secret_len, write_salt, pair_keys_map[write_key].info, read_salt, pair_keys_map[read_key].info);
|
||||
//}
|
||||
}
|
||||
|
||||
static ssize_t
|
||||
encrypt(uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pair_cipher_context *cctx)
|
||||
{
|
||||
|
||||
@@ -16,18 +16,98 @@
|
||||
#include <polarssl/havege.h>
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_OPENSSL
|
||||
#include <openssl/aes.h>
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
#include "pair_ap/pair.h"
|
||||
#include <plist/plist.h>
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_FFMPEG
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/channel_layout.h>
|
||||
#include <libavutil/opt.h>
|
||||
#include <libswresample/swresample.h>
|
||||
#endif
|
||||
|
||||
#include "alac.h"
|
||||
#include "audio.h"
|
||||
|
||||
// clang-format off
|
||||
|
||||
/*
|
||||
__________________________________________________________________________________________________________________________________
|
||||
* ALAC Specific Info (24 bytes) (mandatory)
|
||||
__________________________________________________________________________________________________________________________________
|
||||
|
||||
The Apple Lossless codec stores specific information about the encoded stream in the ALACSpecificConfig. This
|
||||
info is vended by the encoder and is used to setup the decoder for a given encoded bitstream.
|
||||
|
||||
When read from and written to a file, the fields of this struct must be in big-endian order.
|
||||
When vended by the encoder (and received by the decoder) the struct values will be in big-endian order.
|
||||
|
||||
struct ALACSpecificConfig (defined in ALACAudioTypes.h)
|
||||
abstract This struct is used to describe codec provided information about the encoded Apple Lossless bitstream.
|
||||
It must accompany the encoded stream in the containing audio file and be provided to the decoder.
|
||||
|
||||
field frameLength uint32_t indicating the frames per packet when no explicit frames per packet setting is
|
||||
present in the packet header. The encoder frames per packet can be explicitly set
|
||||
but for maximum compatibility, the default encoder setting of 4096 should be used.
|
||||
|
||||
field compatibleVersion uint8_t indicating compatible version,
|
||||
value must be set to 0
|
||||
|
||||
field bitDepth uint8_t describes the bit depth of the source PCM data (maximum value = 32)
|
||||
|
||||
field pb uint8_t currently unused tuning parameter.
|
||||
value should be set to 40
|
||||
|
||||
field mb uint8_t currently unused tuning parameter.
|
||||
value should be set to 10
|
||||
|
||||
field kb uint8_t currently unused tuning parameter.
|
||||
value should be set to 14
|
||||
|
||||
field numChannels uint8_t describes the channel count (1 = mono, 2 = stereo, etc...)
|
||||
when channel layout info is not provided in the 'magic cookie', a channel count > 2
|
||||
describes a set of discreet channels with no specific ordering
|
||||
|
||||
field maxRun uint16_t currently unused.
|
||||
value should be set to 255
|
||||
|
||||
field maxFrameBytes uint32_t the maximum size of an Apple Lossless packet within the encoded stream.
|
||||
value of 0 indicates unknown
|
||||
|
||||
field avgBitRate uint32_t the average bit rate in bits per second of the Apple Lossless stream.
|
||||
value of 0 indicates unknown
|
||||
|
||||
field sampleRate uint32_t sample rate of the encoded stream
|
||||
*/
|
||||
|
||||
// clang-format on
|
||||
|
||||
typedef struct __attribute__((__packed__)) ALACSpecificConfig {
|
||||
uint32_t frameLength;
|
||||
uint8_t compatibleVersion;
|
||||
uint8_t bitDepth;
|
||||
uint8_t pb;
|
||||
uint8_t mb;
|
||||
uint8_t kb;
|
||||
uint8_t numChannels;
|
||||
uint16_t maxRun;
|
||||
uint32_t maxFrameBytes;
|
||||
uint32_t avgBitRate;
|
||||
uint32_t sampleRate;
|
||||
|
||||
} ALACSpecificConfig;
|
||||
|
||||
// everything in here is big-endian, i.e. network byte order
|
||||
typedef struct __attribute__((__packed__)) alac_ffmpeg_magic_cookie {
|
||||
uint32_t cookie_size; // 36 bytes
|
||||
uint32_t cookie_tag; // 'alac'
|
||||
uint32_t cookie_version; // 0
|
||||
ALACSpecificConfig alac_config;
|
||||
} alac_ffmpeg_magic_cookie;
|
||||
|
||||
#define time_ping_history_power_of_two 7
|
||||
// this must now be zero, otherwise bad things will happen
|
||||
#define time_ping_history \
|
||||
@@ -57,6 +137,19 @@ typedef enum {
|
||||
|
||||
typedef uint16_t seq_t;
|
||||
|
||||
// these are the values coming in on a buffered audio RTP packet's SSRC field
|
||||
// the apparent significances are as indicated.
|
||||
// Dolby Atmos seems to be 7P1
|
||||
typedef enum {
|
||||
SSRC_NONE = 0,
|
||||
ALAC_44100_S16_2 = 0x0000FACE, // this is made up
|
||||
ALAC_48000_S24_2 = 0x15000000,
|
||||
AAC_44100_F24_2 = 0x16000000,
|
||||
AAC_48000_F24_2 = 0x17000000,
|
||||
AAC_48000_F24_5P1 = 0x27000000,
|
||||
AAC_48000_F24_7P1 = 0x28000000,
|
||||
} ssrc_t;
|
||||
|
||||
typedef struct audio_buffer_entry { // decoded audio packets
|
||||
uint8_t ready;
|
||||
uint8_t status; // flags
|
||||
@@ -66,11 +159,19 @@ typedef struct audio_buffer_entry { // decoded audio packets
|
||||
uint64_t initialisation_time; // the time the packet was added or the time it was noticed the
|
||||
// packet was missing
|
||||
uint64_t resend_time; // time of last resend request or zero
|
||||
uint32_t given_timestamp; // for debugging and checking
|
||||
int length; // the length of the decoded data
|
||||
uint32_t timestamp; // for timing
|
||||
int32_t timestamp_gap; // the difference between the timestamp and the expected timestamp.
|
||||
size_t length; // the length of the decoded data (or silence requested) in input frames
|
||||
#ifdef CONFIG_FFMPEG
|
||||
ssrc_t ssrc; // this is the type of this specific frame.
|
||||
AVFrame *avframe; // In AP2 and optionally in AP1, an AVFrame will be
|
||||
// used to carry audio rather than just a malloced memory space.
|
||||
#endif
|
||||
} abuf_t;
|
||||
|
||||
typedef struct stats { // statistics for running averages
|
||||
uint32_t timestamp; // timestamp (denominated in input frames)
|
||||
size_t frames; // number of audio frames in the block (denominated in output frames)
|
||||
int64_t sync_error, correction, drift;
|
||||
} stats_t;
|
||||
|
||||
@@ -88,6 +189,9 @@ typedef struct stats { // statistics for running averages
|
||||
|
||||
#define BUFFER_FRAMES 1024
|
||||
|
||||
// maximum number of frames that can be added or removed from a packet_count
|
||||
#define INTERPOLATION_LIMIT 20
|
||||
|
||||
typedef enum {
|
||||
ast_unknown,
|
||||
ast_uncompressed, // L16/44100/2
|
||||
@@ -126,12 +230,16 @@ typedef struct {
|
||||
sized_buffer encrypted_read_buffer;
|
||||
sized_buffer plaintext_read_buffer;
|
||||
int is_encrypted;
|
||||
char *description;
|
||||
} pair_cipher_bundle; // cipher context and buffers
|
||||
|
||||
typedef struct {
|
||||
struct pair_setup_context *setup_ctx;
|
||||
struct pair_verify_context *verify_ctx;
|
||||
pair_cipher_bundle control_cipher_bundle;
|
||||
pair_cipher_bundle event_cipher_bundle;
|
||||
pair_cipher_bundle data_cipher_bundle;
|
||||
char *data_cipher_salt;
|
||||
} ap2_pairing;
|
||||
|
||||
// flush requests are stored in order of flushFromSeq
|
||||
@@ -148,11 +256,15 @@ typedef struct flush_request_t {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
int connection_number; // for debug ID purposes, nothing else...
|
||||
int resend_interval; // this is really just for debugging
|
||||
int rtsp_link_is_idle; // if true, this indicates if the client asleep
|
||||
char *UserAgent; // free this on teardown
|
||||
int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency
|
||||
int connection_number; // for debug ID purposes, nothing else...
|
||||
int is_playing; // set true by player_play, set false by player_stop
|
||||
unsigned int sync_samples_index; // for estimating the gap between the highest and lowest timing
|
||||
// error over the past n samples
|
||||
unsigned int sync_samples_count; // the array of samples is defined locally
|
||||
int at_least_one_frame_seen_this_session; // set when the first frame is output
|
||||
int resend_interval; // this is really just for debugging
|
||||
char *UserAgent; // free this on teardown
|
||||
int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency
|
||||
int latency_warning_issued;
|
||||
uint32_t latency; // the actual latency used for this play session
|
||||
uint32_t minimum_latency; // set if an a=min-latency: line appears in the ANNOUNCE message; zero
|
||||
@@ -175,12 +287,11 @@ typedef struct {
|
||||
|
||||
// buffers to delete on exit
|
||||
int32_t *tbuf;
|
||||
int32_t *sbuf;
|
||||
char *outbuf;
|
||||
|
||||
// for generating running statistics...
|
||||
|
||||
stats_t *statistics;
|
||||
// stats_t *statistics;
|
||||
|
||||
// for holding the output rate information until printed out at the end of a session
|
||||
double raw_frame_rate;
|
||||
@@ -201,9 +312,11 @@ typedef struct {
|
||||
// other stuff...
|
||||
pthread_t *player_thread;
|
||||
abuf_t audio_buffer[BUFFER_FRAMES];
|
||||
unsigned int max_frames_per_packet, input_num_channels, input_bit_depth, input_rate;
|
||||
int input_bytes_per_frame, output_bytes_per_frame, output_sample_ratio;
|
||||
int max_frame_size_change;
|
||||
unsigned int frames_per_packet, input_num_channels, input_bit_depth, input_effective_bit_depth,
|
||||
input_rate;
|
||||
int input_bytes_per_frame;
|
||||
unsigned int output_sample_ratio;
|
||||
unsigned int output_bit_depth;
|
||||
int64_t previous_random_number;
|
||||
alac_file *decoder_info;
|
||||
uint64_t packet_count;
|
||||
@@ -215,7 +328,8 @@ typedef struct {
|
||||
uint64_t missing_packets, late_packets, too_late_packets, resend_requests;
|
||||
int decoder_in_use;
|
||||
// debug variables
|
||||
int32_t last_seqno_read;
|
||||
int last_seqno_valid;
|
||||
seq_t last_seqno_read;
|
||||
// mutexes and condition variables
|
||||
pthread_cond_t flowcontrol;
|
||||
pthread_mutex_t ab_mutex, flush_mutex, volume_control_mutex, player_create_delete_mutex;
|
||||
@@ -224,11 +338,8 @@ typedef struct {
|
||||
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
|
||||
// or 1.
|
||||
int ab_buffering, ab_synced;
|
||||
int64_t first_packet_timestamp;
|
||||
uint32_t first_packet_timestamp;
|
||||
int flush_requested;
|
||||
int flush_output_flushed; // true if the output device has been flushed.
|
||||
uint32_t flush_rtp_timestamp;
|
||||
@@ -243,8 +354,6 @@ typedef struct {
|
||||
aes_context dctx;
|
||||
#endif
|
||||
|
||||
int amountStuffed;
|
||||
|
||||
int32_t framesProcessedInThisEpoch;
|
||||
int32_t framesGeneratedInThisEpoch;
|
||||
int32_t correctionsRequestedInThisEpoch;
|
||||
@@ -333,6 +442,7 @@ typedef struct {
|
||||
int ap2_play_enabled; // protect with flush mutex
|
||||
|
||||
ap2_pairing ap2_pairing_context;
|
||||
struct pair_result *pair_setup_result; // need to keep the shared secret
|
||||
|
||||
int event_socket;
|
||||
int data_socket;
|
||||
@@ -351,10 +461,70 @@ typedef struct {
|
||||
uint64_t audio_format;
|
||||
uint64_t compression;
|
||||
unsigned char *session_key; // needs to be free'd at the end
|
||||
char *ap2_client_name; // needs to be free'd at teardown phase 2
|
||||
uint64_t frames_packet;
|
||||
uint64_t type;
|
||||
uint64_t networkTimeTimelineID; // the clock ID used by the player
|
||||
uint8_t groupContainsGroupLeader; // information coming from the SETUP
|
||||
uint64_t compressionType;
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_FFMPEG
|
||||
ssrc_t incoming_ssrc; // The SSRC of incoming packets. In AirPlay 2, the RTP SSRC seems to encode
|
||||
// something about the contents of the packet -- Atmos/etc. We use it also
|
||||
// even in AP1 as a code
|
||||
ssrc_t resampler_ssrc; // the SSRC of packets for which the software resampler has been set up.
|
||||
// normally it's the same as that of incoming packets, but if the encoding of incoming packets
|
||||
// changes dynamically and the decoding chain hasn't been reset, the resampler will have to deal
|
||||
// with queued AVFrames encoded according to the previous SSRC.
|
||||
const AVCodec *codec;
|
||||
AVCodecContext *codec_context;
|
||||
// the swr can't be used just after the incoming packet has been decoded as explained below
|
||||
|
||||
// The reasons that resampling can not occur when the packet initially arrives are twofold.
|
||||
// Resampling requires input samples from before and after the resampling instant.
|
||||
// So, at the end of a block, since the subsequent samples aren't in the block, resampling
|
||||
// is deferred until the next block is loaded. From this, the two reasons follow:
|
||||
// First, the "next" block to be provided in player_put_packet is not guaranteed to be
|
||||
// the next block in sequence -- packets can arrive out of sequence in UDP transmission.
|
||||
// Second, not all the frames that should be generated for a block will be generated
|
||||
// by a call to swr_convert. The frames that can't be calculated will not be provided, and
|
||||
// will be held back and provided to the subsequent call.
|
||||
// Tht means that the first frame output by swr_convert will not in general,
|
||||
// not correspond to the first frame provided to it, throwing
|
||||
// timing calculations off.
|
||||
|
||||
// In summary, we have to wait until (1) we have all the blocks in order,
|
||||
// and (2) we have to track the number of resampler output frames to
|
||||
// keep the correspondence between them and the input frames.
|
||||
|
||||
// We can calculate the "deficit" between the number of frames that should be generated
|
||||
// versus the number of frames actually generated.
|
||||
|
||||
// For example, converting 352 frames at 44,100 to 48,000 should result
|
||||
// in 352 * 48000 / 44100, or 383.129252 frames.
|
||||
|
||||
// Say only 360 frames are actually produced, then the deficit is 23.129252.
|
||||
|
||||
// If those 360 frames are sent to the output device, then the timing of the next
|
||||
// block of 352 frames will be ahead by 23.129252 frames at 48,000 fps -- about 0.48 ms.
|
||||
|
||||
// We need to add the delay corresponding to the frames that should have been sent to
|
||||
// keep timing correct. I.e. when calculating the buffer delay at the start of the following
|
||||
// block, those 23.129252 frames the were not actually sent should be added to it.
|
||||
|
||||
// The "deficit" can readily be kept up to date and can be always added to the
|
||||
// DAC buffer delay to exactly compensate for the
|
||||
|
||||
SwrContext *swr; // this will do transcoding anf resampling, if necessary, just prior to output
|
||||
int ffmpeg_decoding_chain_initialised;
|
||||
int64_t resampler_output_channels;
|
||||
int resampler_output_bytes_per_sample;
|
||||
int64_t frames_retained_in_the_resampler; // swr will retain frames it hasn't finished processing
|
||||
// they'll come out before the frames corresponding to the start of next block passed to
|
||||
// swrconvert so we need to compensate for their absence in sync timing
|
||||
unsigned int output_channel_to_resampler_channel_map[8];
|
||||
unsigned int output_channel_map_size;
|
||||
#endif
|
||||
|
||||
// used as the initials values for calculating the rate at which the source thinks it's sending
|
||||
@@ -407,7 +577,6 @@ typedef struct {
|
||||
void *dapo_private_storage; // this is used for compatibility, if dacp stuff isn't enabled.
|
||||
|
||||
int enable_dither; // needed for filling silences before play actually starts
|
||||
uint64_t dac_buffer_queue_minimum_length;
|
||||
} rtsp_conn_info;
|
||||
|
||||
extern int statistics_row; // will be reset to zero when debug level changes or statistics enabled
|
||||
@@ -421,7 +590,6 @@ int32_t modulo_32_offset(uint32_t from, uint32_t to);
|
||||
|
||||
void ab_resync(rtsp_conn_info *conn);
|
||||
|
||||
int player_prepare_to_play(rtsp_conn_info *conn);
|
||||
int player_play(rtsp_conn_info *conn);
|
||||
int player_stop(rtsp_conn_info *conn);
|
||||
|
||||
@@ -429,8 +597,8 @@ void player_volume(double f, rtsp_conn_info *conn);
|
||||
void player_volume_without_notification(double f, rtsp_conn_info *conn);
|
||||
void player_flush(uint32_t timestamp, rtsp_conn_info *conn);
|
||||
// void player_full_flush(rtsp_conn_info *conn);
|
||||
void player_put_packet(int original_format, seq_t seqno, uint32_t actual_timestamp, uint8_t *data,
|
||||
int len, rtsp_conn_info *conn);
|
||||
uint32_t player_put_packet(uint32_t ssrc, seq_t seqno, uint32_t actual_timestamp, uint8_t *data,
|
||||
size_t len, int mute, int32_t timestamp_gap, rtsp_conn_info *conn);
|
||||
int64_t monotonic_timestamp(uint32_t timestamp,
|
||||
rtsp_conn_info *conn); // add an epoch to the timestamp. The monotonic
|
||||
// timestamp guaranteed to start between 2^32 2^33
|
||||
@@ -441,4 +609,10 @@ int64_t monotonic_timestamp(uint32_t timestamp,
|
||||
|
||||
double suggested_volume(rtsp_conn_info *conn); // volume suggested for the connection
|
||||
|
||||
const char *get_ssrc_name(ssrc_t ssrc);
|
||||
int ssrc_is_recognised(ssrc_t ssrc);
|
||||
int ssrc_is_aac(ssrc_t ssrc); // used to decide if a mute might be needed (AAC only)
|
||||
void prepare_decoding_chain(rtsp_conn_info *conn, ssrc_t ssrc); // also sets up timing stuff
|
||||
void clear_decoding_chain(rtsp_conn_info *conn); // tear down the decoding chain
|
||||
|
||||
#endif //_PLAYER_H
|
||||
|
||||
@@ -1,67 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Comment -->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>audioLatencies</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>inputLatencyMicros</key>
|
||||
<integer>0</integer>
|
||||
<key>outputLatencyMicros</key>
|
||||
<integer>400000</integer>
|
||||
<key>type</key>
|
||||
<integer>100</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>audioType</key>
|
||||
<string>default</string>
|
||||
<key>inputLatencyMicros</key>
|
||||
<integer>0</integer>
|
||||
<key>outputLatencyMicros</key>
|
||||
<integer>400000</integer>
|
||||
<key>type</key>
|
||||
<integer>100</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>audioType</key>
|
||||
<string>media</string>
|
||||
<key>inputLatencyMicros</key>
|
||||
<integer>0</integer>
|
||||
<key>outputLatencyMicros</key>
|
||||
<integer>400000</integer>
|
||||
<key>type</key>
|
||||
<integer>100</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>audioType</key>
|
||||
<string>media</string>
|
||||
<key>inputLatencyMicros</key>
|
||||
<integer>0</integer>
|
||||
<key>outputLatencyMicros</key>
|
||||
<integer>400000</integer>
|
||||
<key>type</key>
|
||||
<integer>102</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>keepAliveLowPower</key>
|
||||
<true/>
|
||||
<key>vv</key>
|
||||
<integer>2</integer>
|
||||
<key>playbackCapabilities</key>
|
||||
<dict>
|
||||
<key>supportsInterstitials</key>
|
||||
<false/>
|
||||
<key>supportsFPSSecureStop</key>
|
||||
<false/>
|
||||
<key>supportsUIForAudioOnlyContent</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>canRecordScreenStream</key>
|
||||
<false/>
|
||||
<key>keepAliveSendStatsAsBody</key>
|
||||
<true/>
|
||||
<key>manufacturer</key>
|
||||
<string>Shairport Sync</string>
|
||||
<key>nameIsFactoryDefault</key>
|
||||
<false/>
|
||||
<key>protocolVersion</key>
|
||||
<string>1.1</string>
|
||||
<key>sdk</key>
|
||||
<string>AirPlay;2.0.2</string>
|
||||
<key>sourceVersion</key>
|
||||
<string>366.0</string>
|
||||
<key>statusFlags</key>
|
||||
<integer>4</integer>
|
||||
<!-- key values for deviceID, features, pi, name, model and a txtAirPlay data field are added programatically -->
|
||||
<key>volumeControlType</key>
|
||||
<integer>3</integer>
|
||||
<!-- <key>senderAddress</key> --> <!-- hoping we don't need this -->
|
||||
<!-- <string>192.168.50.63:45008</string> -->
|
||||
<key>screenDemoMode</key>
|
||||
<false/>
|
||||
<!-- <key>initialVolume</key> --> <!-- added programmatically -->
|
||||
<!-- <real>-18.612697601318359</real> -->
|
||||
<!-- <key>featuresEx</key> -->
|
||||
<!-- <string>AMp/StBLNbw</string> --> <!-- added programmatically -->
|
||||
<!-- <key>supportedFormats</key> --> <!-- added programmatically -->
|
||||
<!-- <dict> -->
|
||||
<!-- <key>audioStream</key> -->
|
||||
<!--<integer>21235712</integer> --> <!-- looks like: 0x1440800 -->
|
||||
<!--<key>bufferStream</key> -->
|
||||
<!-- This seems to tell the client what formats it supports. -->
|
||||
<!-- See https://emanuelecozzi.net/docs/airplay2/audio for an incomplete list -->
|
||||
<!-- <integer>4017212516896604160></integer> --> <!-- looks like: 0x37C0018000E00000, from ATV4K: lots of unknown + AAC-LC/48K/F24/7.1, AAC-LC/48K/F24/5.1, ALAC/48000/S24/2, AAC-LC/48000/F24/2 and AAC-LC/44100/F24/2 -->
|
||||
<!-- <integer>2190448001024</integer> --> <!-- looks like: 0x1FE00E00000, lots of guesses of unknowns + AAC-LC/48K/F24/7.1, AAC-LC/48K/F24/5.1, ALAC/48000/S24/2, AAC-LC/48000/F24/2 and AAC-LC/44100/F24/2 -->
|
||||
<!-- <integer>1649282121728</integer> --> <!-- looks like: 0x18000E00000, AAC-LC/48K/F24/7.1, AAC-LC/48K/F24/5.1, ALAC/48000/S24/2, AAC-LC/48000/F24/2 and AAC-LC/44100/F24/2 -->
|
||||
<!-- <integer>-577021992845180928</integer> --> <!-- looks like: 0xF7FE 018E 00E0 0000, from HPm 16.3.2 -->
|
||||
<!-- 0x10000000000 is AAC-LC/48K/F24/7.1 -->
|
||||
<!-- 0x08000000000 is AAC-LC/48K/F24/5.1 -->
|
||||
<!-- 0x00000800000 is AAC-LC/48K/F24/2 -->
|
||||
<!-- 0x00000400000 is AAC-LC/44.1K/F24/2 -->
|
||||
<!-- 0x00000200000 is ALAC/48K/F24/2 -->
|
||||
|
||||
</dict>
|
||||
<!-- <key>sourceVersion</key> --> <!-- added programmatically -->
|
||||
<!-- <string>620.8.2</string> -->
|
||||
<!-- <key>macAddress</key> --> <!-- hoping we don't need this -->
|
||||
<!-- <string>b8:27:eb:82:d0:87</string> -->
|
||||
<key>receiverHDRCapability</key>
|
||||
<string>4k60</string>
|
||||
<!-- key values for initialVolume, sourceVersion, deviceID, features, pi, name, model, featuresEx and a txtAirPlay data field are added programatically -->
|
||||
<!-- key values for firmwareBuildDate (string), build (string) and PTPInfo (string)
|
||||
should be added programatically -->
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
+7
-8
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2020 -- 2023
|
||||
* Copyright (c) Mike Brady 2020--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -36,9 +36,9 @@
|
||||
#ifdef COMPILE_FOR_FREEBSD
|
||||
#include <netinet/in.h>
|
||||
#endif
|
||||
#include <netdb.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <netdb.h>
|
||||
#define __STDC_FORMAT_MACROS
|
||||
#include "common.h"
|
||||
#include "ptp-utilities.h"
|
||||
@@ -162,7 +162,7 @@ int ptp_get_clock_info(uint64_t *actual_clock_id, uint64_t *time_of_sample, uint
|
||||
|
||||
int ptp_shm_interface_open() {
|
||||
int response = 0;
|
||||
debug(2, "ptp_shm_interface_open with mapped_addr = %" PRIuPTR "", mapped_addr);
|
||||
debug(3, "ptp_shm_interface_open with mapped_addr = %" PRIuPTR "", mapped_addr);
|
||||
if ((mapped_addr == NULL) || (mapped_addr == MAP_FAILED)) {
|
||||
response = -1;
|
||||
if (mapped_addr == NULL)
|
||||
@@ -193,11 +193,11 @@ int ptp_shm_interface_open() {
|
||||
debug(1, "No config.nqptp_shared_memory_interface_name");
|
||||
}
|
||||
if (response == 0)
|
||||
debug(2, "ptp_shm_interface_open -- success!");
|
||||
debug(3, "ptp_shm_interface_open -- success!");
|
||||
else
|
||||
debug(2, "ptp_shm_interface_open -- fail!");
|
||||
debug(3, "ptp_shm_interface_open -- fail!");
|
||||
} else {
|
||||
debug(2, "ptp_shm_interface_open -- already open!");
|
||||
debug(3, "ptp_shm_interface_open -- already open!");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@@ -249,8 +249,7 @@ void ptp_send_control_message_string(const char *msg) {
|
||||
}
|
||||
|
||||
/* Send the message in buf to the server */
|
||||
if (sendto(s, full_message, full_message_size, 0, info->ai_addr, info->ai_addrlen) <
|
||||
0) {
|
||||
if (sendto(s, full_message, full_message_size, 0, info->ai_addr, info->ai_addrlen) < 0) {
|
||||
die("error sending timing_peer_list to NQPTP");
|
||||
}
|
||||
/* Deallocate the socket */
|
||||
|
||||
@@ -1,28 +1,3 @@
|
||||
/*
|
||||
* This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2020 -- 2023
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
#ifndef __PTP_UTILITIES_H
|
||||
#define __PTP_UTILITIES_H
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ int frame_to_local_time(uint32_t timestamp, uint64_t *time, rtsp_conn_info *conn
|
||||
int local_time_to_frame(uint64_t time, uint32_t *frame, rtsp_conn_info *conn);
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
void *rtp_data_receiver(void *arg);
|
||||
void *rtp_event_receiver(void *arg);
|
||||
void *rtp_ap2_control_receiver(void *arg);
|
||||
void *rtp_realtime_audio_receiver(void *arg);
|
||||
|
||||
@@ -26,6 +26,16 @@ void metadata_stop(void);
|
||||
// e.g. if it's malloced, to free it, etc.
|
||||
// nothing is done automatically
|
||||
|
||||
int send_ssnc_metadata(uint32_t code, char *data, uint32_t length, int block);
|
||||
int send_ssnc_metadata(const uint32_t code, const char *data, const uint32_t length,
|
||||
const int block);
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
ssize_t read_encrypted(int fd, pair_cipher_bundle *ctx, void *buf, size_t count);
|
||||
ssize_t write_encrypted(int fd, pair_cipher_bundle *ctx, const void *buf, size_t count);
|
||||
|
||||
void generateTxtDataValueInfo(rtsp_conn_info *conn, void **response, size_t *responseLength);
|
||||
plist_t generateInfoPlist(rtsp_conn_info *conn);
|
||||
char *plist_as_xml_text(plist_t the_plist); // caller must free the returned NUL-terminated string
|
||||
#endif
|
||||
|
||||
#endif // _RTSP_H
|
||||
|
||||
+125
-47
@@ -15,7 +15,12 @@ general =
|
||||
// %V for the full version string, e.g. 3.3-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc
|
||||
// Overall length can not exceed 50 characters. Example: "Shairport Sync %v on %H".
|
||||
// password = "secret"; // (AirPlay 1 only) leave this commented out if you don't want to require a password
|
||||
// interpolation = "auto"; // aka "stuffing". Default is "auto". Alternatives are "basic" or "soxr". Choose "soxr" only if you have a reasonably fast processor and Shairport Sync has been built with "soxr" support.
|
||||
// The interpolation setting below controls how Shairport Sync adds or removes frames of audio to keep in sync.
|
||||
// "auto" (default) measures the processor's floating point speed and chooses "soxr" if available and it is fast enough. Otherwise, "vernier" is selected.
|
||||
// "soxr" uses the SoX library to recode a packet of frames to a new packet containing more or fewer frames. This needs a processor with fast floating point capability.
|
||||
// "vernier" recodes a packet of frames to a new packet containing more or fewer frames. This is recommended for low powered devices.
|
||||
// "basic" causes the simple removal or insertion of frames in a packet of frames. Not recommended.
|
||||
// interpolation = "auto"; // aka "stuffing". Default is "auto". Alternatives are "vernier", "basic" or "soxr". Choose "soxr" only if you have a reasonably fast processor and Shairport Sync has been built with "soxr" support.
|
||||
// output_backend = "alsa"; // Run "shairport-sync -h" to get a list of all output_backends, e.g. "alsa", "pipe", "stdout". The default is the first one.
|
||||
// mdns_backend = "avahi"; // Run "shairport-sync -h" to get a list of all mdns_backends. The default is the first one.
|
||||
// interface = "name"; // Use this advanced setting to specify the interface on which Shairport Sync should provide its service. Leave it commented out to get the default, which is to select the interface(s) automatically.
|
||||
@@ -25,15 +30,25 @@ general =
|
||||
// airplay_device_id_offset = 0; // (AirPlay 2 only) add this to the default airplay_device_id calculated from one of the device's MAC address
|
||||
// airplay_device_id = 0x<six-digit_hexadecimal_number>L; // (AirPlay 2 only) use this as the airplay_device_id e.g. 0xDCA632D4E8F3L -- remember the "L" at the end as it's a 64-bit quantity!
|
||||
// regtype = "<string>"; // Use this advanced setting to set the service type and transport to be advertised by Zeroconf/Bonjour. Default is "_raop._tcp" for AirPlay 1, "_airplay._tcp" for AirPlay 2.
|
||||
|
||||
// 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.
|
||||
// If you build Shairport Sync with the flag --with-apple-alac, the Apple ALAC decoder will be chosen by default.
|
||||
// For FFmpeg channel and layout names, (e.g. "7.1", "FL", "3.0(back)", etc.), please see channel_names and channel_layout_map at https://ffmpeg.org/doxygen/trunk/channel__layout_8c_source.html
|
||||
// eight_channel_mode = "on"; // Enable reception of eight channel audio. Can be "off", "on" or an eight-channel FFmpeg channel layout. If "on", the channel layout used is: "7.1".
|
||||
// six_channel_mode = "on"; // Enable reception of six channel audio. Can be "off", "on" or a six-channel FFmpeg channel layout. If "on", the channel layout used is: "5.1".
|
||||
// mixdown = "auto"; // Enable mixdown. Can be "auto", "off" or an FFmpeg channel layout, e.g. "quad". If "auto", mixdown will occur, if needed, to the default channel layout for the output channels available.
|
||||
// output_channel_mapping = "auto"; // Specify how audio channels are mapped to the output device's channels:
|
||||
// Shairport Sync uses standard FFmpeg channel names for each channel in the audio output. The names are, in order, "FL", "FR", "FC", "LFE", "BL", "BR", "SL", "SR".
|
||||
// If "auto", the audio channels are matched, where possible, to the channels in the device's channel map. Any leftover output channels are mapped, in order, to leftover device channels.
|
||||
// If "off", or if there is no device channel map, audio channels are output to the device channels in order.
|
||||
// If a list of audio channels is given, e.g. ( "FL", "FR", "LFE", "FC", "BL", "BR", "SL", "SR" ), they are mapped in the order given to the device channels from 1 upwards.
|
||||
// The audio channel list can include the same channel more than once and can include the silent channel "--".
|
||||
|
||||
// alac_decoder = <setting>; //This advanced setting is for Classic Airplay only. It can be "ffmpeg" (default on systems with FFmpeg support), "hammerton" (default on systems without FFmpeg support, deprecated) or "apple" (deprecated).
|
||||
// The original Shairport decoder is by David Hammerton. This is deprecated for security reasons as it is no longer maintained. However, it is compact and may be useful in storage-constrained devices. It is included by default on systems without FFmpeg support.
|
||||
// If you build Shairport Sync with the flag --with-apple-alac, the Apple ALAC decoder will be chosen by default unless FFmpeg support is included. The Apple ALAC decoder is deprecated for security reasons.
|
||||
// If you build Shairport Sync with the flag --with-ffmpeg, the FFmpeg ALAC decoder will be chosen by default. This is recommended except where there is insufficient storage on the device. With this setting, the Hammerton decoder will not be built.
|
||||
// In AirPlay 2 operation, the FFmpeg ALAC decoder is always used.
|
||||
// ignore_volume_control = "no"; // set this to "yes" if you want the volume to be at 100% no matter what the source's volume control is set to.
|
||||
// volume_range_db = 60 ; // use this advanced setting to set the range, in dB, you want between the maximum volume and the minimum volume. Range is 30 to 150 dB. Leave it commented out to use mixer's native range.
|
||||
// volume_max_db = 0.0 ; // use this advanced setting, which must have a decimal point in it, to set the maximum volume, in dB, you wish to use.
|
||||
@@ -43,32 +58,16 @@ 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.
|
||||
// "dasl_tapered" is similar to "standard" - it makes the volume change more quickly at lower volumes and slower at higher volumes.
|
||||
// The intention behind dasl_tapered is that a given percentage change in volume should result in the same percentage change in
|
||||
// The basic idea behind dasl_tapered is that a given percentage change in volume should result in the same percentage change in
|
||||
// perceived loudness. For instance, doubling the volume level should result in doubling the perceived loudness.
|
||||
// With the range of AirPlay volume being from -30 to 0, doubling the volume from -22.5 to -15 results in an increase of 10 dB.
|
||||
// Similarly, doubling the volume from -15 to 0 results in an increase of 10 dB.
|
||||
// For compatibility with mixers having a restricted attenuation range (e.g. 30 dB), "dasl_tapered" will switch to a flat profile at low AirPlay 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.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.
|
||||
// E.g. if the output device, e.g. a soundbar, takes 100 ms to process audio, set this to -0.1 to deliver the audio
|
||||
@@ -80,12 +79,10 @@ general =
|
||||
// audio_backend_silent_lead_in_time = "auto"; // This optional advanced setting, either "auto" or a positive number, sets the length of the period of silence that precedes the start of the audio.
|
||||
// The default is "auto" -- the silent lead-in starts as soon as the player starts sending packets.
|
||||
// Values greater than the latency are ignored. Values that are too low will affect initial synchronisation.
|
||||
|
||||
// dbus_service_bus = "system"; // The Shairport Sync dbus interface, if selected at compilation, will appear
|
||||
// as "org.gnome.ShairportSync" on the whichever bus you specify here: "system" (default) or "session".
|
||||
// mpris_service_bus = "system"; // The Shairport Sync mpris interface, if selected at compilation, will appear
|
||||
// as "org.gnome.ShairportSync" on the whichever bus you specify here: "system" (default) or "session".
|
||||
|
||||
// resend_control_first_check_time = 0.10; // Use this optional advanced setting to set the wait time in seconds before deciding a packet is missing.
|
||||
// resend_control_check_interval_time = 0.25; // Use this optional advanced setting to set the time in seconds between requests for a missing packet.
|
||||
// resend_control_last_check_time = 0.10; // Use this optional advanced setting to set the latest time, in seconds, by which the last check should be done before the estimated time of a missing packet's transfer to the output buffer.
|
||||
@@ -110,11 +107,52 @@ sessioncontrol =
|
||||
// wait_for_completion = "no"; // set to "yes" to get Shairport Sync to wait until the "run_this..." applications have terminated before continuing
|
||||
|
||||
// allow_session_interruption = "no"; // set to "yes" to allow another device to interrupt Shairport Sync while it's playing from an existing audio source
|
||||
// session_timeout = 120; // wait for this number of seconds after a source disappears before terminating the session and becoming available again.
|
||||
// session_timeout = 60; // wait for this number of seconds after a source disappears before terminating the session and becoming available again.
|
||||
};
|
||||
|
||||
// Back End Settings
|
||||
|
||||
// Rates, Formats and Channels
|
||||
// Shairport Sync can handle a wide range of wide range of output rates, formats and channels.
|
||||
|
||||
// If it has been built for AirPlay 2 or with FFmpeg support, Shairport Sync can output in a wide range settings, including 48,000 and 44,100 frames per second, 32- and 24-bit sample sizes, 1 to 8 channels.
|
||||
// Otherwise, if Shairport Sync has been built (1) for classic AirPlay only and (2) without FFmpeg support, it is restricted to output rates of 44,100 frames per second or exact multiples and two channels only.
|
||||
|
||||
// You can check Shairport Sync's support for FFmpeg by entering the command: shairport-sync -V
|
||||
// If the resulting string contains "FFmpeg" or "AirPlay2", then it has FFmpeg support built in.
|
||||
|
||||
// With FFmpeg support:
|
||||
// Possible output rates are: 5512, 8000, 11025, 16000, 22050, 32000, 44100, 48000, 64000, 88200, 96000, 176400, 192000, 352800 and 384000 frames per seconds.
|
||||
// Possible output formats are: "S8", "U8", "S16_LE", "S16_BE", "S24_LE", "S24_BE", "S24_3LE", "S24_3BE", "S32_LE" and "S32_BE".
|
||||
// Possible output channel counts are: 1 to 8.
|
||||
|
||||
// Without FFmpeg support:
|
||||
// Possible rates are: 44100, 88200, 176400 and 352800 frames per seconds.
|
||||
// Possible formats are: "S8", "U8", "S16_LE", "S16_BE", "S24_LE", "S24_BE", "S24_3LE", "S24_3BE", "S32_LE" and "S32_BE".
|
||||
// Possible channel counts are: 2.
|
||||
|
||||
// Automatic settings
|
||||
// Shairport Sync will dynamically select output formats, attempting to match input and output rates, formats and channel counts, picking the best alternatives otherwise.
|
||||
// (Settings are static by default on the STDOUT and pipe backends, as there is no obvious way to signal a downstream consumer of the data when the format has changed.)
|
||||
|
||||
// Rate Selection:
|
||||
// Shairport Sync checks the rates the output system can accept and those that have been specified in the configuration file.
|
||||
// From that set of possibilities, Shairport Sync will attempt to match the rate at which the audio is being received and will switch the output to that rate if necessary.
|
||||
// If the exact rate is not available, an exact multiple will be selected if available. Finally, and only with FFmpeg support, a higher rate or a lower rate will be chosen.
|
||||
// To avoid output rate switching, specify just one rate in the configuration file.
|
||||
|
||||
// Format Selection:
|
||||
// Shairport Sync checks the formats the output system can accept and those that have been specified in the configuration file.
|
||||
// From that set of possibilities, Shairport Sync will use the deepest format unless ignore_volume_control is true and maximum_volume is not used, if which case it will try to switch the output to the exact format of the incoming audio.
|
||||
// To avoid output format switching, specify just one format in the configuration file.
|
||||
|
||||
// Channel Count Selection:
|
||||
// Shairport Sync checks the channels counts the output system can accept and those that have been specified in the configuration file.
|
||||
// From that set of possibilities, Shairport Sync will attempt to match the output channels to the number of channels in the audio and will switch the output to that number of channels if necessary.
|
||||
// If the exact number of output channels is not available, a greater output channel count will be selected if available. Failing that, a lower channel count will be chosen.
|
||||
// To avoid channel count switching, specify just one channel count in the configuration file.
|
||||
|
||||
|
||||
// These are parameters for the "alsa" audio back end.
|
||||
// For this section to be operative, Shairport Sync must be built with the following configuration flag:
|
||||
// --with-alsa
|
||||
@@ -125,31 +163,40 @@ alsa =
|
||||
// mixer_control_index = 0; // the index of the mixer to use to adjust output volume. Default is 0. The mixer is fully identified by the combination of the mixer_control_name and the mixer_control_index, e.g. "PCM",0 would be such a specification.
|
||||
// mixer_device = "default"; // the mixer_device default is whatever the output_device is. Normally you wouldn't have to use this.
|
||||
|
||||
// output_rate = "auto"; // can be "auto", 44100, 88200, 176400 or 352800, but the device must have the capability.
|
||||
// output_format = "auto"; // can be "auto", "U8", "S8", "S16", "S16_LE", "S16_BE", "S24", "S24_LE", "S24_BE", "S24_3LE", "S24_3BE", "S32", "S32_LE" or "S32_BE" but the device must have the capability. Except where stated using (*LE or *BE), endianness matches that of the processor.
|
||||
// Note: if you specify settings here, the output device must be capable of them. Otherwise, Shairport Sync will quit and leave a message in the system log.
|
||||
// output_rate = "auto"; // Specify "auto", or a single rate, e.g. 48000, or a bracketed comma-separated list of rates, e.g. (44100, 48000, 64000). Default is "auto" -- try to match the input. See the "Rates, Formats and Channels" discussion above.
|
||||
// output_format = "auto"; // Specify "auto", or a single format, e.g. "S32_LE", or a bracketed comma-separated list of formats, e.g. ("S32_LE", "S16_LE"). Default is "auto". See the "Rates, Formats and Channels" discussion above.
|
||||
// output_channels = "auto"; // Specify "auto", or a specific number of channels, e.g. 2, or a bracketed comma-separated list of numbers of channels, e.g. (2, 6). Default is "auto" -- try to match the input. See the "Rates, Formats and Channels" discussion above.
|
||||
|
||||
// disable_synchronization = "no"; // Set to "yes" to disable synchronization. Default is "no" This is really meant for troubleshooting.
|
||||
// disable_synchronization = "no"; // Set to "yes" to disable synchronization.
|
||||
|
||||
// period_size = <number>; // Use this optional advanced setting to set the alsa period size near to this value
|
||||
// buffer_size = <number>; // Use this optional advanced setting to set the alsa buffer size near to this value
|
||||
// use_mmap_if_available = "yes"; // Use this optional advanced setting to control whether MMAP-based output is used to communicate with the DAC. Default is "yes"
|
||||
// use_mmap_if_available = "no"; // Use this optional advanced setting to control whether MMAP-based output is used to communicate with the DAC. Default is "no".
|
||||
// use_hardware_mute_if_available = "no"; // Use this optional advanced setting to control whether the hardware in the DAC is used for muting. Default is "no", for compatibility with other audio players.
|
||||
// maximum_stall_time = 0.200; // Use this optional advanced setting to control how long to wait for data to be consumed by the output device before considering it an error. It should never approach 200 ms.
|
||||
// use_precision_timing = "auto"; // Use this optional advanced setting to control how Shairport Sync gathers timing information. When set to "auto", if the output device is a real hardware device, precision timing will be used. Choose "no" for more compatible standard timing, choose "yes" to force the use of precision timing, which may cause problems.
|
||||
|
||||
// disable_standby_mode = "never"; // This setting prevents the DAC from entering the standby mode. Some DACs make small "popping" noises when they go in and out of standby mode. Settings can be: "always", "auto" or "never". Default is "never", but only for backwards compatibility. The "auto" setting prevents entry to standby mode while Shairport Sync is in the "active" mode. You can use "yes" instead of "always" and "no" instead of "never".
|
||||
// disable_standby_mode_silence_threshold = 0.040; // Use this optional advanced setting to control how little audio should remain in the output buffer before the disable_standby code should start sending silence to the output device.
|
||||
// disable_standby_mode_silence_scan_interval = 0.004; // Use this optional advanced setting to control how often the amount of audio remaining in the output buffer should be checked.
|
||||
// disable_standby_mode_silence_scan_interval = 0.030; // Use this optional advanced setting to control how often the amount of audio remaining in the output buffer should be checked.
|
||||
// disable_standby_mode_default_channels = 2; // Use this optional advanced setting to set the initial channel setting when disable_standby_mode is "always" or "yes". After a track has been played, the track's output channel setting will be used.
|
||||
// disable_standby_mode_default_rate = <rate>; // Use this optional advanced setting to set the initial rate, in frames per second, when disable_standby_mode is "always" or "yes". Default is 44100 for classic AirPlay, 48000 for AirPlay 2. After a track has been played, the track's output rate setting will be used.
|
||||
};
|
||||
|
||||
// Parameters for the "pw" PipeWire backend.
|
||||
// Parameters for the PipeWire backend.
|
||||
// For this section to be operative, Shairport Sync must be built with the following configuration flag:
|
||||
// --with-pw
|
||||
pw =
|
||||
// --with-pipewire
|
||||
pipewire =
|
||||
{
|
||||
// application_name = "Shairport Sync"; // Set this to the name that should appear in the Sounds "Applications" or "Volume Levels".
|
||||
// node_name = "Shairport Sync"; // This appears in some PipeWire CLI tool outputs.
|
||||
// sink_target = "<sink target name>"; // Leave this commented out to get the sink target already chosen by the PipeWire system.
|
||||
|
||||
// Note: if you specify settings here, the the PipeWire system must be capable of giving effect to them. Otherwise, Shairport Sync will quit and leave a message in the system log.
|
||||
// output_rate = "auto"; // Specify "auto", or a single rate, e.g. 48000, or a bracketed comma-separated list of rates, e.g. (44100, 48000, 64000). Default is "auto" -- try to match the input. See the "Rates, Formats and Channels" discussion above.
|
||||
// output_format = "auto"; // Specify "auto", or a single format, e.g. "S32_LE", or a bracketed comma-separated list of formats, e.g. ("S32_LE", "S16_LE"). Default is "auto". See the "Rates, Formats and Channels" discussion above.
|
||||
// output_channels = "auto"; // Specify "auto", or a specific number of channels, e.g. 2, or a bracketed comma-separated list of numbers of channels, e.g. (2, 6). Default is "auto" -- try to match the input. See the "Rates, Formats and Channels" discussion above.
|
||||
};
|
||||
|
||||
// Parameters for the "sndio" audio back end. All are optional.
|
||||
@@ -158,23 +205,32 @@ pw =
|
||||
sndio =
|
||||
{
|
||||
// device = "default"; // optional setting to set the name of the output device, e.g. "rsnd/0", "rsnd/1", etc.
|
||||
// rate = 44100; // optional setting which can be 44100, 88200, 176400 or 352800, but the device must have the capability. Default is 44100.
|
||||
// format = "S16"; // optional setting which can be "U8", "S8", "S16", "S24", "S24_3LE", "S24_3BE" or "S32", but the device must have the capability. Except where stated using (*LE or *BE), endianness matches that of the processor.
|
||||
// round = <number>; // advanced optional setting to set the period size near to this value
|
||||
// bufsz = <number>; // advanced optional setting to set the buffer size near to this value
|
||||
|
||||
// Note: if you specify settings here, the sndio system must be capable of giving effect to them. Otherwise, Shairport Sync will quit and leave a message in the system log.
|
||||
// output_rate = "auto"; // Specify "auto", or a single rate, e.g. 48000, or a bracketed comma-separated list of rates, e.g. (44100, 48000, 64000). Default is "auto" -- try to match the input. See the "Rates, Formats and Channels" discussion above.
|
||||
// output_format = "auto"; // Specify "auto", or a single format, e.g. "S32_LE", or a bracketed comma-separated list of formats, e.g. ("S32_LE", "S16_LE") from the list: "S32_LE", "S32_BE", "S16_LE" or "S16_BE". Default is "auto". See the "Rates, Formats and Channels" discussion above.
|
||||
// output_channels = "auto"; // Specify "auto", or a specific number of channels, e.g. 2, or a bracketed comma-separated list of numbers of channels, e.g. (2, 6). Default is "auto" -- try to match the input. See the "Rates, Formats and Channels" discussion above.
|
||||
};
|
||||
|
||||
// Parameters for the "pa" PulseAudio backend.
|
||||
// Parameters for the PulseAudio backend.
|
||||
// For this section to be operative, Shairport Sync must be built with the following configuration flag:
|
||||
// --with-pa
|
||||
pa =
|
||||
// --with-pulseaudio
|
||||
pulseaudio =
|
||||
{
|
||||
// server = "host"; // Set this to override the default pulseaudio server that should be used.
|
||||
// sink = "Sink Name"; // Set this to override the default pulseaudio sink that should be used. (Untested)
|
||||
// application_name = "Shairport Sync"; //Set this to the name that should appear in the Sounds "Applications" tab when Shairport Sync is active.
|
||||
|
||||
// Note: if you specify settings here, the PulseAudio system must be capable of giving effect to them. Otherwise, Shairport Sync will quit and leave a message in the system log.
|
||||
// output_rate = "auto"; // Specify "auto", or a single rate, e.g. 48000, or a bracketed comma-separated list of rates, e.g. (44100, 48000, 64000). Default is "auto" -- try to match the input. See the "Rates, Formats and Channels" discussion above.
|
||||
// output_format = "auto"; // Specify "auto", or a single format, e.g. "S32_LE", or a bracketed comma-separated list of formats, e.g. ("S32_LE", "S16_LE"). Default is "auto". See the "Rates, Formats and Channels" discussion above.
|
||||
// output_channels = "auto"; // Specify "auto", or a specific number of channels, e.g. 2, or a bracketed comma-separated list of numbers of channels, e.g. (2, 6). Default is "auto" -- try to match the input. See the "Rates, Formats and Channels" discussion above.
|
||||
// default_channel_layouts = "alsa"; // Set to "alsa" (default) for the alsa-compatible channel layouts (see "PA_CHANNEL_MAP_ALSA"), or set to "pulseaudio" if you want PulseAudio's own channel layouts ("PA_CHANNEL_MAP_DEFAULT") to be used instead.
|
||||
};
|
||||
|
||||
// Parameters for the "jack" JACK Audio Connection Kit backend.
|
||||
// Parameters for the JACK Audio Connection Kit backend.
|
||||
// For this section to be operative, Shairport Sync must be built with the following configuration flag:
|
||||
// --with-jack
|
||||
jack =
|
||||
@@ -191,27 +247,49 @@ jack =
|
||||
// bufsz = <number>; // advanced optional setting to set the buffer size to this value
|
||||
};
|
||||
|
||||
// Parameters for the "pipe" audio back end, a back end that directs raw CD-format audio output to a pipe. No interpolation is done.
|
||||
// Parameters for the "pipe" audio back end, a back end that directs raw PCM audio output to a unix pipe. No interpolation is done.
|
||||
// For this section to be operative, Shairport Sync must have been built with the following configuration flag:
|
||||
// --with-pipe
|
||||
pipe =
|
||||
{
|
||||
// name = "/tmp/shairport-sync-audio"; // this is the default
|
||||
|
||||
// Note: if you specify "auto" or multiple settings here. Shairport Sync may switch between them to match the input, but there will be no notification in the pipe as changes occur. To avoid this, consider setting one just rate/format/channel count. If built with AirPlay 2 or FFmpeg support, Shairport Sync will automatically transcode and mixdown as necessary.
|
||||
// output_rate = <rate>; // Specify a single rate, e.g. 44100, or a bracketed comma-separated list of rates, e.g. (44100, 48000, 64000) or "auto" -- try to match the input. Default is 44100 for classic AirPlay, 48000 for AirPlay 2. See the "Rates, Formats and Channels" discussion above.
|
||||
// output_format = <format>; // Specify a format, e.g. "S16_LE", or a bracketed comma-separated list of formats, e.g. ("S32_LE", "S16_LE") or "auto". Default is "S16_LE" for classic AirPlay, "S32_LE" for AirPlay 2. See the "Rates, Formats and Channels" discussion above.
|
||||
// output_channels = 2; // Specify a specific number of channels, e.g. 2, or a bracketed comma-separated list of numbers of channels, e.g. (2, 6) or "auto" -- try to match the input. Default is 2. See the "Rates, Formats and Channels" discussion above.
|
||||
};
|
||||
|
||||
// There are no configuration file parameters for the "stdout" audio back end. No interpolation is done.
|
||||
// Parameters for the "stdout" audio back end, a back end that directs raw PCM audio output to STDOUT. No interpolation is done.
|
||||
// To include support for the "stdout" backend, Shairport Sync must be built with the following configuration flag:
|
||||
// --with-stdout
|
||||
stdout =
|
||||
{
|
||||
// Note: if you specify "auto" or multiple settings here. Shairport Sync may switch between them to match the input, but there will be no notification in STDOUT as changes occur. To avoid this, consider setting one just rate/format/channel count. If built with AirPlay 2 or FFmpeg support, Shairport Sync will automatically transcode and mixdown as necessary.
|
||||
// output_rate = <rate>; // Specify a single rate, e.g. 44100, or a bracketed comma-separated list of rates, e.g. (44100, 48000, 64000) or "auto" -- try to match the input. Default is 44100 for classic AirPlay, 48000 for AirPlay 2. See the "Rates, Formats and Channels" discussion above.
|
||||
// output_format = <format>; // Specify a format, e.g. "S16_LE", or a bracketed comma-separated list of formats, e.g. ("S32_LE", "S16_LE") or "auto". Default is "S16_LE" for classic AirPlay, "S32_LE" for AirPlay 2. See the "Rates, Formats and Channels" discussion above.
|
||||
// output_channels = 2; // Specify a specific number of channels, e.g. 2, or a bracketed comma-separated list of numbers of channels, e.g. (2, 6) or "auto" -- try to match the input. Default is 2. See the "Rates, Formats and Channels" discussion above.
|
||||
};
|
||||
|
||||
// There are no configuration file parameters for the "ao" audio back end. No interpolation is done.
|
||||
// Parameters for the "ao" (also known as "libao") backend.
|
||||
// To include support for the "ao" backend, Shairport Sync must be built with the following configuration flag:
|
||||
// --with-ao
|
||||
ao =
|
||||
{
|
||||
// Note: if you specify settings here, the ao (aka "libao") system must be capable of giving effect to them. Otherwise, Shairport Sync will quit and leave a message in the system log.
|
||||
// output_rate = "auto"; // Specify "auto", or a single rate, e.g. 48000, or a bracketed comma-separated list of rates, e.g. (44100, 48000, 64000). Default is "auto" -- try to match the input. See the "Rates, Formats and Channels" discussion above.
|
||||
// output_format = "auto"; // Specify "auto", or a single format, e.g. "S32_LE", or a bracketed comma-separated list of formats, e.g. ("S32_LE", "S16_LE"). Default is "auto". See the "Rates, Formats and Channels" discussion above.
|
||||
// output_channels = "auto"; // Specify "auto", or a specific number of channels, e.g. 2, or a bracketed comma-separated list of numbers of channels, e.g. (2, 6). Default is "auto" -- try to match the input. See the "Rates, Formats and Channels" discussion above.
|
||||
};
|
||||
|
||||
|
||||
// For this section to be operative, Shairport Sync must be built with the following configuration flag:
|
||||
// --with-convolution
|
||||
dsp =
|
||||
{
|
||||
|
||||
// NOTE: convolution and loudness filters currently work only for 44100 input and are disabled for other frame rates.
|
||||
|
||||
//////////////////////////////////////////
|
||||
// This convolution filter can be used to apply almost any correction to the audio signal, like frequency and phase correction.
|
||||
// For example you could measure (with a good microphone and a sweep-sine) the frequency response of your speakers + room,
|
||||
@@ -251,7 +329,7 @@ metadata =
|
||||
// pipe_timeout = 5000; // wait for this number of milliseconds for a blocked pipe to unblock before giving up
|
||||
// progress_interval = 0.0; // if non-zero, progress 'phbt' messages will be sent at the interval specified in seconds. A 'phb0' message will also be sent when the first audio frame of a play session is about to be played.
|
||||
// Each message consists of the RTPtime of a a frame of audio and the exact system time when it is to be played. The system time, in nanoseconds, is based the CLOCK_MONOTONIC_RAW of the machine -- if available -- or CLOCK_MONOTONIC otherwise.
|
||||
// Messages are sent when the frame is placed in the output device's buffer, thus, they will be _approximately_ 'audio_backend_buffer_desired_length_in_seconds' (default 0.2 seconds) ahead of time.
|
||||
// Messages are sent when the frame is placed in the output device's buffer, thus, they will be _approximately_ 'audio_backend_buffer_desired_length_in_seconds' ahead of time.
|
||||
// socket_address = "226.0.0.1"; // if set to a host name or IP address, UDP packets containing metadata will be sent to this address. May be a multicast address. "socket-port" must be non-zero and "enabled" must be set to yes"
|
||||
// socket_port = 5555; // if socket_address is set, the port to send UDP packets to
|
||||
// socket_msglength = 65000; // the maximum packet size for any UDP metadata. This will be clipped to be between 500 or 65000. The default is 500.
|
||||
@@ -281,12 +359,12 @@ mqtt =
|
||||
// publish_raw = "no"; //whether to publish all available metadata under the codes given in the 'metadata' docs.
|
||||
// publish_parsed = "no"; //whether to publish a small (but useful) subset of metadata under human-understandable topics
|
||||
// empty_payload_substitute = "--"; // MQTT messages with empty payloads often are invisible or have special significance to MQTT brokers and readers.
|
||||
// To avoid empty payload problems, the string here is used instead of any empty payload. Set it to the empty string -- "" -- to leave the payload empty.
|
||||
// To avoid empty payload problems, this string is used instead of any empty payload. Set it to the empty string -- "" -- to leave the payload empty.
|
||||
// Currently published topics:artist,album,title,genre,format,songalbum,volume,client_ip,
|
||||
// Additionally, messages at the topics play_start,play_end,play_flush,play_resume are published
|
||||
// publish_cover = "no"; //whether to publish the cover over mqtt in binary form. This may lead to a bit of load on the broker
|
||||
// enable_autodiscovery = "no"; //whether to publish an autodiscovery message to automatically appear in Home Assistant
|
||||
// autodiscovery_prefix = "homeassistant"; //string to prepend to autodiscovery topic
|
||||
// enable_autodiscovery = "no"; //whether to publish an autodiscovery message to automatically appear in Home Assistant
|
||||
// autodiscovery_prefix = "homeassistant"; //string to prepend to autodiscovery topic
|
||||
// enable_remote = "no"; //whether to remote control via MQTT. RC is available under `topic`/remote.
|
||||
// Available commands are "command", "beginff", "beginrew", "mutetoggle", "nextitem", "previtem", "pause", "playpause", "play", "stop", "playresume", "shuffle_songs", "volumedown", "volumeup"
|
||||
};
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
# Required-Stop: $remote_fs $syslog
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Shairport Synchronous AirPlay
|
||||
# Short-Description: Shairport Sync -- AirPlay Receiver
|
||||
# Description: Implements a synchronous (multi-room-capable) AirPlay receiver
|
||||
### END INIT INFO
|
||||
|
||||
# Author: Mike Brady <mikebrady@eircom.net>
|
||||
# Author: Mike Brady <4265913+mikebrady@users.noreply.github.com>
|
||||
#
|
||||
# Do NOT "set -e"
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user