Version 5.0 Major Release.
New Features:
Multi-Channel and High-Resolution Audio Support
48,000 frames per second ("48k") operation.
48k lossless stereo support.
5.1 and 7.1 surround sound support.
Multi-channel and multi-rate operation on ALSA, PipeWire, PulseAudio, FreeBSD, stdout and Unix pipe output backends.
Automatic Audio Format Selection
Flexible and controllable output format selection.
Automatic rate, sample format, and channel count selection.
Full FFmpeg Integration
Support for transcoding.
Advanced resampling capabilities.
New audio format support.
Enhanced Resampling
New vernier resampling and interpolation method optimized for low-power CPUs.
Better performance on resource-constrained devices.
Convolution and Loudness Enhancements:
Convolution system is now multithreaded and works on stereo and multichannel audio at 48k and 44.1k.
Multiple impulse response (IR) files can now be provided via convolution_ir_files setting.
New convolution_thread_pool_size setting for multithreaded processing (defaults to 1).
Loudness processing now works with stereo and multichannel audio at 48k and 44.1k.
Updated to the most recent HiFi-LoFi FFT convolver.
MQTT Enhancements:
Added new publish_retain boolean option. When enabled, published MQTT messages have the retain flag set, so the MQTT broker stores the last message per topic and new subscribers receive the most recent value immediately. Thanks to lululombard for PR #2142.
D-Bus Enhancements:
Added new dbus_default_message_bus command-line argument (can be system or session) to set the default message bus for both D-Bus native service and MPRIS service.
Performance Improvements:
Enhanced compatibility with AirPlay 2 AutoMix and Smart Tracklists resulting in less unexplained track skipping.
Better operation on low-power devices down to Raspberry Pi B.
Improved efficiency on embedded systems.
Enhanced timestamp handling for better synchronization.
Improved sync error calculation.
Rebuilt buffered audio processor for cleaner handling of immediate and deferred flush requests.
Docker Enhancements:
Reduced Docker image sizes with slimmed-down FFmpeg library.
Removed dhclient from Docker images for smaller footprint.
Bug Fixes:
Fixed MQTT warning on service startup: "Could not establish a mqtt connection". The startup script now correctly states that the mosquitto service is required. Thanks to Hugo Villeneuve for PR #2137.
Fixed compatibility with mbedtls library version 3.4+ (present on recent Linux versions). Thanks to Christian Beier for finding and fixing the bug.
Fixed PulseAudio backend so that PA_ERR_NODATA returns "No latency data yet". Thanks to Vladimir Shakov for the report and fix.
Ensured old flush requests are deleted when a new play session starts. Thanks to saujanyashah for the report.
Fixed format warnings on 64-bit and 32-bit systems
Removed compilation warnings on 32-bit builds
Improved argument checking for debug(), inform(), warn() and die() functions
Fixed "daemon" typos throughout codebase. Thanks to Chris Boot for PR #1981.
Added warning if a convolution impulse response file cannot be read due to bad path or permissions
Build System Improvements:
Unified service file with variable substitution for Avahi support, making it easier to add future service dependencies. Thanks to Hugo Villeneuve.
Network interface selection now only considers interfaces that are up, running and not loopback interfaces. Thanks to Carl Johnson for the suggestion.
Configuration File Changes and Deprecations
New settings: convolution_ir_files (replaces convolution_ir_file), convolution_enabled (replaces convolution), convolution_max_length_in_seconds (replaces convolution_max_length), loudness_enabled (replaces loudness).
New convolution_thread_pool_size setting (defaults to 1).
Deprecated settings: convolution_ir_file, convolution, convolution_max_length, loudness.
Corresponding D-Bus methods and properties have been updated.
Deprecation Notice:
The Jack Audio and soundio backends are deprecated and will be removed in a future release. Consider using the updated PipeWire backend instead.
Documentation Updates
Updated BUILD.md with latest build instructions.
Updated AIRPLAY2.md with feature information.
Enhanced convolution and loudness documentation.
Maintenance:
Fixed FFmpeg deprecation warnings.
Bumped actions/checkout from 6.0.1 to 6.0.2.
Bumped docker/login-action from 3.6.0 to 3.7.0.
Bumped docker/build-push-action from 6.13.0 to 6.15.0.
Bumped docker/setup-qemu-action from 3.4.0 to 3.6.0.
Bumped docker/setup-buildx-action from 3.9.0 to 3.10.0.
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
|
||||
- uses: actions/checkout@v6.0.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
|
||||
- uses: actions/checkout@v6.0.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
|
||||
- uses: actions/checkout@v6.0.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
|
||||
- uses: actions/checkout@v6.0.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:
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: macos-13
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: actions/checkout@v6.0.2
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
brew install automake
|
||||
|
||||
@@ -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,10 +10,10 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: actions/checkout@v6.0.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 libssl-dev libsoxr-dev
|
||||
- name: Configure
|
||||
@@ -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
|
||||
- uses: actions/checkout@v6.0.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
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
# Builds a docker image when a commit is made. Also pushes the build if the branch is 'master' or 'development'.
|
||||
|
||||
# Tag pattern
|
||||
# 'master' - rolling, rolling-classic
|
||||
# 'development' - development, development-classic
|
||||
|
||||
name: Build and push docker (push/pull request)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- development
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
env:
|
||||
DOCKER_PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
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.13.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.13.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
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set SHAIRPORT_SYNC_BRANCH env.
|
||||
run: echo "SHAIRPORT_SYNC_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Is branch "master"?
|
||||
if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'master' }}
|
||||
run: |
|
||||
echo "IMAGE_TAG_BASE=rolling" >> $GITHUB_ENV
|
||||
|
||||
- name: Is branch "development"?
|
||||
if: ${{ env.SHAIRPORT_SYNC_BRANCH == 'development' }}
|
||||
run: |
|
||||
echo "NQPTP_BRANCH=development" >> $GITHUB_ENV
|
||||
echo "IMAGE_TAG_BASE=development" >> $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: Login to Docker Registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ${{ secrets.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_REGISTRY_USER }}
|
||||
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build and push (classic)
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./docker/classic/Dockerfile
|
||||
platforms: ${{ env.DOCKER_PLATFORMS }}
|
||||
push: ${{ env.IMAGE_TAG_BASE != '' }}
|
||||
tags: ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.IMAGE_TAG_BASE }}-classic
|
||||
build-args: |
|
||||
SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./docker/Dockerfile
|
||||
platforms: ${{ env.DOCKER_PLATFORMS }}
|
||||
push: ${{ env.IMAGE_TAG_BASE != '' }}
|
||||
tags: ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.IMAGE_TAG_BASE }}
|
||||
build-args: |
|
||||
SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }}
|
||||
NQPTP_BRANCH=${{ env.NQPTP_BRANCH }}
|
||||
@@ -1,87 +0,0 @@
|
||||
# Builds & pushes a docker image when a tag is created.
|
||||
# Tag pattern: '[tag]' & '[tag]-classic'
|
||||
# 'latest' & 'classic' also, when master tagged.
|
||||
|
||||
# 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)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+' # X
|
||||
- '[0-9]+\.[0-9]+' # X.Y
|
||||
- '[0-9]+\.[0-9]+\.[0-9]+' # X.Y.Z
|
||||
|
||||
env:
|
||||
DOCKER_PLATFORMS: linux/386,linux/amd64,linux/arm/v6,linux/arm64,linux/arm/v7
|
||||
NQPTP_BRANCH: main
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set SHAIRPORT_SYNC_BRANCH env.
|
||||
run: |
|
||||
raw=$(git branch -r --contains ${{ github.ref }})
|
||||
branch=${raw##*/}
|
||||
echo "SHAIRPORT_SYNC_BRANCH=${branch}" >> $GITHUB_ENV
|
||||
|
||||
- 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: 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: Login to Docker Registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ${{ secrets.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_REGISTRY_USER }}
|
||||
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build and push (classic)
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./docker/classic/Dockerfile
|
||||
platforms: ${{ env.DOCKER_PLATFORMS }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.GIT_TAG }}-classic
|
||||
${{ env.LATEST_TAG == 'true' && format('{0}:classic', secrets.DOCKER_IMAGE_NAME) || '' }}
|
||||
build-args: |
|
||||
SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./docker/Dockerfile
|
||||
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) || '' }}
|
||||
build-args: |
|
||||
SHAIRPORT_SYNC_BRANCH=${{ env.SHAIRPORT_SYNC_BRANCH }}
|
||||
NQPTP_BRANCH=${{ env.NQPTP_BRANCH }}
|
||||
@@ -0,0 +1,95 @@
|
||||
# Builds and pushes a docker image in the following scenarios:
|
||||
# When a commit is made to 'master' or 'development'.
|
||||
# Tag pattern
|
||||
# 'master' - rolling, rolling-classic
|
||||
# 'development' - development, development-classic
|
||||
|
||||
# When a tag is created.
|
||||
# Tag pattern: '[tag]' & '[tag]-classic'
|
||||
|
||||
# TODO: Does not currently push 'latest' or 'classic' tags.
|
||||
|
||||
# Builds but does not push a docker image for PRs.
|
||||
|
||||
name: Build and conditionally push docker image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- development
|
||||
tags:
|
||||
- "*"
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
docker-vars:
|
||||
uses: ./.github/workflows/docker-vars.yaml
|
||||
|
||||
build-docker-image-and-publish:
|
||||
name: Build and conditionally push docker image
|
||||
needs:
|
||||
- docker-vars
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: classic
|
||||
dockerfile: ./docker/classic/Dockerfile
|
||||
tag_suffix: "-classic-rc"
|
||||
build_args: |
|
||||
SHAIRPORT_SYNC_BRANCH=.
|
||||
- name: main
|
||||
dockerfile: ./docker/Dockerfile
|
||||
tag_suffix: "-rc"
|
||||
build_args: |
|
||||
SHAIRPORT_SYNC_BRANCH=.
|
||||
NQPTP_BRANCH=${{ needs.docker-vars.outputs.nqptp_branch }}
|
||||
|
||||
steps:
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3.7.0
|
||||
with:
|
||||
registry: ${{ secrets.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_REGISTRY_USER }}
|
||||
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
|
||||
if: needs.docker-vars.outputs.push_docker_image == 'true'
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.7.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.12.0
|
||||
|
||||
- name: Checkout shairport sync repo
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build and push ${{ matrix.name }}
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
env:
|
||||
registry_and_name: ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_IMAGE_NAME }}
|
||||
with:
|
||||
context: ./
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: ${{ needs.docker-vars.outputs.docker_platforms }}
|
||||
push: ${{ needs.docker-vars.outputs.push_docker_image == 'true' }}
|
||||
# Assign tags based on branch or tag type for clarity
|
||||
tags: |
|
||||
${{ github.ref_type == 'branch' && github.ref_name == 'development'
|
||||
&& format('{0}:development{1}', env.registry_and_name, matrix.tag_suffix) || '' }}
|
||||
|
||||
${{ github.ref_type == 'branch' && github.ref_name == 'master'
|
||||
&& format('{0}:rolling{1}', env.registry_and_name, matrix.tag_suffix) || '' }}
|
||||
|
||||
${{ github.ref_type == 'tag'
|
||||
&& format('{0}:{1}{2}', env.registry_and_name, github.ref_name, matrix.tag_suffix) || '' }}
|
||||
build-args: |
|
||||
${{ matrix.build_args }}
|
||||
|
||||
# TODO: Fix pushing of 'latest' and 'classic' tags.
|
||||
# env.is_tag == 'true' && env.branch_name == 'master' && format('{0}:latest{1}', env.registry_and_name, matrix.tag_suffix) || ''
|
||||
@@ -0,0 +1,46 @@
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
docker_platforms:
|
||||
description: "The docker platforms to build for."
|
||||
value: ${{ jobs.docker-vars.outputs.docker_platforms }}
|
||||
nqptp_branch:
|
||||
description: "The NQPTP branch."
|
||||
value: ${{ jobs.docker-vars.outputs.nqptp_branch }}
|
||||
push_docker_image:
|
||||
description: "Whether to push the docker image."
|
||||
value: ${{ jobs.docker-vars.outputs.push_docker_image }}
|
||||
|
||||
jobs:
|
||||
docker-vars:
|
||||
name: Set variables required for docker build.
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
NQPTP_BRANCH: main
|
||||
PUSH_DOCKER_IMAGE: "false"
|
||||
outputs:
|
||||
nqptp_branch: ${{ env.NQPTP_BRANCH }}
|
||||
push_docker_image: ${{ env.PUSH_DOCKER_IMAGE }}
|
||||
docker_platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
steps:
|
||||
- name: Push docker image if this is a tag.
|
||||
if: github.ref_type == 'tag'
|
||||
run: |
|
||||
echo "PUSH_DOCKER_IMAGE=true" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Push docker image if this is the "master" or "development" branch.
|
||||
if: |
|
||||
github.ref_type == 'branch' &&
|
||||
(
|
||||
github.ref_name == 'master' ||
|
||||
github.ref_name == 'development'
|
||||
)
|
||||
run: |
|
||||
echo "PUSH_DOCKER_IMAGE=true" >> "$GITHUB_ENV"
|
||||
|
||||
- name: If 'development' branch, set NQPTP_BRANCH to 'development'.
|
||||
if: |
|
||||
github.ref_type == 'branch' &&
|
||||
github.ref_name == 'development'
|
||||
run: |
|
||||
echo "NQPTP_BRANCH=development" >> "$GITHUB_ENV"
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9.1.0
|
||||
- uses: actions/stale@v10.1.1
|
||||
with:
|
||||
stale-issue-message: 'This issue has been inactive for 28 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.'
|
||||
stale-pr-message: 'This PR has been inactive for 28 days so will be closed 7 days from now. To prevent this, please remove the "stale" label or post a comment.'
|
||||
|
||||
+8
-2
@@ -25,11 +25,14 @@ stamp-h1
|
||||
man/Makefile
|
||||
man/Makefile.in
|
||||
scripts/shairport-sync.service
|
||||
scripts/shairport-sync.service-avahi
|
||||
scripts/shairport-sync
|
||||
shairport-sync.core
|
||||
gitversion-stamp
|
||||
gitversion.*
|
||||
plist_xml_strings.*
|
||||
plists/*.c
|
||||
plists/*.h
|
||||
plists/*.x
|
||||
|
||||
#Some dbus files that are automatically generated
|
||||
/org.gnome.ShairportSync.service
|
||||
@@ -45,8 +48,11 @@ plist_xml_strings.*
|
||||
.DS_Store
|
||||
shairport-sync.xcodeproj
|
||||
|
||||
# separate build directory
|
||||
# separate build directories
|
||||
build
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
|
||||
# IntelliJ/JetBrains
|
||||
.idea/
|
||||
|
||||
@@ -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/master/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 or PulseAudio. 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. Take care to ensure that the Unix users running the audio applications that use PipeWire are members of the `pipewire` group.
|
||||
|
||||
## Guides
|
||||
* A building guide is available at [BUILD.md](BUILD.md).
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
# 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.
|
||||
## Important Note -- Upgrading to Version 5!
|
||||
|
||||
If you have been using Shairport Sync prior to Version 5.0 and are rebuilding or reinstalling Shairport Sync, be aware that a few important things have changed.
|
||||
While the overall operation of Shairport Sync has not changed much, it is really important to fully remove existing startup scripts and to check and update configuration files. Some important changes are highlighted here:
|
||||
|
||||
1. The default sample rate has changed from 44,100 to 48,000 for buffered audio. Real-time audio streams remain at 44,100.
|
||||
2. Shairport Sync will also play surround sound (5.1 and 7.1) and lossless (48k) audio.
|
||||
3. Shairport Sync will automatically switch output rates and formats to correspond to input rates and formats. This can be controlled.
|
||||
4. Many build flags have changed: for example `--with-systemd` is now `with-systemd-startup`.
|
||||
5. Many configuration settings names and facilities have changed. For example `convolution` is now `convolution_enabled`. Another example is that convolution is now multi-threaded, so a new `convolution_thread_pool_size` setting is available.
|
||||
7. Installation has changed: when Shairport Sync and NQPTP are installed, their startup scripts have changed to provide them with more suitable privileges. You must remove any existing startup scripts.
|
||||
8. Jack Audio is deprecated and will be removed in a future update. Consider using PipeWire instead.
|
||||
|
||||
## 0. General
|
||||
|
||||
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.
|
||||
|
||||
@@ -18,18 +35,31 @@ Remove it as follows:
|
||||
```
|
||||
# rm /usr/local/bin/shairport-sync
|
||||
```
|
||||
Do this until no more copies of `shairport-sync` are found.
|
||||
Do this until no more copies of `shairport-sync` are found. Shairport Sync could also be found in `/usr/bin/shairport-sync` or elsewhere.
|
||||
#### Remove Old Configuration Files
|
||||
If you want to preserve any configuration settings you have made, you should make a note of them and then delete the configuration file.
|
||||
This is suggested because there may be new configuration options available, which will be present but disabled in the
|
||||
updated configuration file that will be installed.
|
||||
You can then apply your previous settings to the updated configuration file.
|
||||
|
||||
The configuration file is typically at `/etc/shairport-sync.conf` or `/usr/local/etc/shairport-sync.conf`.
|
||||
|
||||
#### Remove Old Service Files
|
||||
You should also remove any of the following service files that may be present:
|
||||
* `/etc/systemd/system/shairport-sync.service`
|
||||
* `/etc/systemd/user/shairport-sync.service`
|
||||
* `/lib/systemd/system/shairport-sync.service`
|
||||
* `/lib/systemd/user/shairport-sync.service`
|
||||
* `/etc/dbus-1/system.d/shairport-sync-dbus.conf`
|
||||
* `/etc/dbus-1/system.d/shairport-sync-mpris.conf`
|
||||
* `/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.
|
||||
If you removed any installations of Shairport Sync or any of its service or configuration files in the last three steps, you should reboot.
|
||||
|
||||
## 2. Get Tools and Libraries
|
||||
Okay, now let's get the tools and libraries for building and installing Shairport Sync (and NQPTP).
|
||||
@@ -40,22 +70,25 @@ 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 or Debian 13 ("Trixie") and later -- and possibly on other distributions -- requires `systemd-dev`. It does no harm to attempt to install it -- the install 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.
|
||||
Important: to get the correct version of FFmpeg, _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.
|
||||
```
|
||||
# yum update
|
||||
# yum install --allowerasing make automake gcc gcc-c++ \
|
||||
@@ -68,7 +101,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.
|
||||
@@ -80,7 +113,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.
|
||||
```
|
||||
@@ -88,14 +121,16 @@ Enable and start the `avahi-daemon` service.
|
||||
# systemctl start avahi-daemon
|
||||
```
|
||||
### FreeBSD
|
||||
First, update everything:
|
||||
This is for FreeBSD 14.3. First, update everything:
|
||||
```
|
||||
# freebsd-update fetch
|
||||
# freebsd-update install
|
||||
# pkg
|
||||
# pkg update
|
||||
# pkg upgrade
|
||||
```
|
||||
Next, install the Avahi subsystem. FYI, `avahi-app` is chosen because it doesn’t require X11. `nss_mdns` is included to allow FreeBSD to resolve mDNS-originated addresses – it's not actually needed by Shairport Sync. Thanks to [reidransom](https://gist.github.com/reidransom/6033227) for this.
|
||||
Next, install the Avahi subsystem.
|
||||
FYI, `avahi-app` is chosen because it doesn’t require X11. If you are using a GUI with your FreeBSD system, select `avahi` rather than `avahi-app`.
|
||||
`nss_mdns` is included to allow FreeBSD to resolve mDNS-originated addresses – it's not actually needed by Shairport Sync. Thanks to [reidransom](https://gist.github.com/reidransom/6033227) for this.
|
||||
```
|
||||
# pkg install avahi-app nss_mdns
|
||||
```
|
||||
@@ -110,14 +145,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
|
||||
libplist libsodium ffmpeg 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
|
||||
@@ -127,60 +165,111 @@ 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
|
||||
$ 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, reboot, ensure you are logged in as that user and then run the following convenience script:
|
||||
```
|
||||
$ sh user-service-install.sh
|
||||
```
|
||||
This will run a few checks, install a user startup script and start Shairport Sync immediately. (Run `$ sh user-service-install.sh --dry-run` initially if you prefer...)
|
||||
|
||||
##### 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.
|
||||
|
||||
#### Enable Shairport Sync as a System Service
|
||||
|
||||
If your system does not have either PipeWire or PulseAudio installed (see how to check above), then you can enable Shairport Sync as a system service that starts automatically when the system boots up. To do so -- assuming you have followed the build guide successfully -- enter the following command:
|
||||
```
|
||||
# systemctl enable shairport-sync
|
||||
```
|
||||
This enables Shairport Sync to start automatically when the system boots up. Please remember, this will not work if PipeWire or PulseAudio are installed on your system.
|
||||
|
||||
You should not enable Shairport Sync as a user service and a system service at the same time!
|
||||
|
||||
### 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.
|
||||
### WiFi Power Management – Linux
|
||||
If you are using WiFi, you should turn off WiFi Power Management:
|
||||
If you are using WiFi, you should turn off WiFi Power Management. On a Raspberry Pi, for example, you can use the following commands:
|
||||
```
|
||||
# iwconfig wlan0 power off
|
||||
```
|
||||
@@ -198,6 +287,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 Sample 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.
|
||||
|
||||
+42
-29
@@ -3,24 +3,26 @@ If your car audio has an AUX input, you can get AirPlay in your car using Shairp
|
||||
|
||||
## The Basic Idea
|
||||
|
||||
The basic idea is to use a small Linux computer to create an isolated WiFi network (a "car network") and run Shairport Sync on it to provide an AirPlay service. An iPhone or an iPad with cellular capability can simultaneously connect to internet radio, YouTube, Apple Music, Spotify, etc. over the cellular network and send AirPlay audio through the car network to the AirPlay service provided by Shairport Sync. This sends the audio to the computer's DAC which is connected to the AUX input of your car audio.
|
||||
The basic idea is to use a small Linux computer to create an isolated WiFi network (a "Car WiFi Network") and run Shairport Sync on it to provide an AirPlay service. An iPhone or an iPad with cellular capability can simultaneously connect to internet radio, YouTube, Apple Music, Spotify, etc. over its cellular network connection and send AirPlay audio through the Car WiFi Network to the AirPlay service provided by Shairport Sync. This sends the audio to the computer's sound card or DAC, which is connected to the AUX input of your car audio.
|
||||
|
||||
Please note that Android phones and tablets can not, so far, do this trick of using the two networks simultaneously.
|
||||
|
||||
## Example
|
||||
|
||||
This example is based on Raspberry Pi OS Lite (Trixie). Note that some of the details will vary if you are using a different version of Linux.
|
||||
|
||||
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 W and a Pimoroni PHAT DAC are used. Shairport Sync will be built for AirPlay 2 operation. You can build classic Shairport Sync to support only the original "classic" AirPlay (aka AirPlay 1) if you prefer.
|
||||
|
||||
Please note that some of the details of setting up networks are specific to the version of Linux used.
|
||||
|
||||
### Prepare the initial SD Image
|
||||
* Download Raspberry Pi OS (Lite) and install it onto an SD Card using `Raspberry Pi Imager`. The Lite version is preferable to the Desktop version as it doesn't include a sound server like PulseAudio or PipeWire that can prevent direct access to the audio output device.
|
||||
* Download Raspberry Pi OS Lite (Trixie) and install it onto an SD Card using `Raspberry Pi Imager`. The Lite version is preferable to the Desktop version as it doesn't include a sound server like PulseAudio or PipeWire that is not needed in this case.
|
||||
* 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`).
|
||||
* From there, cd to the `firmware` subdirectory, i.e. `/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
|
||||
@@ -31,7 +33,7 @@ Please note that some of the details of setting up networks are specific to the
|
||||
After a short time, the Pi should appear on your network – it may take a couple of minutes. To check, try to `ping` it at the `<hostname>.local`, e.g. if the hostname is `bmw` then use `$ ping bmw.local`. Once it has appeared, you can SSH into it and configure it.
|
||||
|
||||
### Boot, Configure, Update
|
||||
The first thing to do on a Pi would be to use the `raspi-config` tool to expand the file system to use the entire card. Next, do the usual update and upgrade:
|
||||
The first thing to do on a Pi would be to do the usual update and upgrade:
|
||||
```
|
||||
# apt-get update
|
||||
# apt-get upgrade
|
||||
@@ -41,32 +43,40 @@ 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 update
|
||||
# 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 systemd-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 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 \
|
||||
libavutil-dev libavcodec-dev libavformat-dev systemd-dev
|
||||
```
|
||||
Note: older versions of the Raspberry Pi OS don't have -- and don't need -- the `systemd-dev` package. If it is reported as unknown, omit if from the installation.
|
||||
|
||||
#### NQPTP
|
||||
Skip this section if you are building classic Shairport Sync – NQPTP is not needed for classic Shairport Sync.
|
||||
Skip this step if you are building classic Shairport Sync – NQPTP is not needed for classic Shairport Sync.
|
||||
|
||||
Download, install, enable and start NQPTP from [here](https://github.com/mikebrady/nqptp) following the guide for Linux.
|
||||
Download, install, enable and start NQPTP from [here](https://github.com/mikebrady/nqptp/tree/development) following the guide for Linux.
|
||||
|
||||
#### 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 `--with-airplay-2` from the `./configure` options with `--with-ffmpeg` if you are building classic Shairport Sync.
|
||||
|
||||
```
|
||||
$ git clone https://github.com/mikebrady/shairport-sync.git
|
||||
$ cd shairport-sync
|
||||
$ git checkout development
|
||||
$ autoreconf -fi
|
||||
$ ./configure --sysconfdir=/etc --with-alsa \
|
||||
--with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-airplay-2
|
||||
--with-soxr --with-avahi --with-ssl=openssl --with-systemd-startup --with-airplay-2
|
||||
$ make
|
||||
# make install
|
||||
# systemctl enable shairport-sync
|
||||
@@ -160,7 +170,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:
|
||||
Configure the startup sequence by adding commands to `/etc/rc.local`. If `/etc/rc.local` does not already exist, create it with owner and group `root` and permissons `755`. The commands will start the necessary services automatically after startup, depending on the `MODE` -- `DEC` for develoment mode, and `RUN` for normal operating mode.
|
||||
|
||||
The contents of `/etc/rc.local` should look like this:
|
||||
```
|
||||
#!/bin/sh -e
|
||||
#
|
||||
@@ -200,29 +212,30 @@ fi
|
||||
exit 0 # normal exit here
|
||||
```
|
||||
|
||||
#### Disable Unused Services - Optional
|
||||
These optional steps have been tested on a Raspberry Pi only -- they have not been tested on other systems.
|
||||
Some services are not necessary for this setup and can be disabled as follows:
|
||||
```
|
||||
# systemctl disable keyboard-setup
|
||||
# systemctl disable triggerhappy
|
||||
# systemctl disable dphys-swapfile
|
||||
```
|
||||
|
||||
|
||||
#### 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.
|
||||
#### Disable 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 interfere with the system's operation in WiFi Access Point mode, or because they don't work when the system isn't connected to the Internet. A further possibility if that they will be started by the script in `/etc/rc.local` when appropriate. 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.
|
||||
```
|
||||
# systemctl disable dhcpcd
|
||||
# systemctl disable NetworkManager
|
||||
# systemctl disable wpa_supplicant
|
||||
# systemctl disable systemd-timesyncd
|
||||
```
|
||||
Lastly, note that the WiFi credentials you used initially to connect to your network (e.g. your home network) will have been stored in the system in plain text. This is convenient for when you want to reconnect to update (see later), but if you prefer to delete them, they will be in `/etc/wpa_supplicant/wpa_supplicant.conf`.
|
||||
|
||||
#### Disable Unused Services - Optional
|
||||
These optional steps have been tested on a Raspberry Pi Lite (Trixie) only -- they have not been tested on other systems.
|
||||
Some services are not necessary for this setup and can be disabled as follows:
|
||||
```
|
||||
# systemctl disable keyboard-setup
|
||||
```
|
||||
#### Security Note
|
||||
The WiFi credentials you used initially to connect to your network (e.g. your home network) will have been stored in the system.
|
||||
This is convenient for when you want to reconnect to update (see later), but if you prefer to delete them, use `nmtui` or `nmcli` to remove them.
|
||||
|
||||
#### Optional: Read-only mode – Raspberry Pi Specific
|
||||
This optional step is applicable to a Raspberry Pi only. Run `sudo raspi-config` and then choose `Performance Options` > `Overlay Filesystem` and choose to enable the overlay filesystem, and to set the boot partition to be write-protected. (The idea here is that this offers more protection against files being corrupted by the sudden removal of power.)
|
||||
|
||||
Note that some packages may be installed to enable read-only mode, so the Pi needs to be connected to the internet for this step. Thus, if may be necessary to temporarily enable and disable read-only mode while connected to your network before re-enabling it after restarting in its final `RUN` mode as a stand-alone AirPlay receiver.
|
||||
|
||||
### Final Step
|
||||
When you are finished, carefully power down the machine before unplugging it from power:
|
||||
```
|
||||
@@ -233,14 +246,14 @@ Note: doing a `reboot` here doesn't seem to work properly -- it really does seem
|
||||
### Ready
|
||||
Install the Raspberry Pi in your car. It should be powered from a source that is switched off when you leave the car, otherwise the slight current drain will eventually flatten the car's battery.
|
||||
|
||||
When the power source is switched on -- typically when you start the car -- it will take around 35 seconds for the system to become available (timing based on a Raspberry Pi Zero 2 W running Bookworm).
|
||||
When the power source is switched on -- typically when you start the car -- it will take around 75 seconds for the system to become available using a Raspberry Pi Zero W, about 35 seconds with a Pi Zero 2 W.
|
||||
|
||||
### Enjoy!
|
||||
---
|
||||
## 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:
|
||||
|
||||
+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
|
||||
|
||||
+1041
-1018
File diff suppressed because it is too large
Load Diff
+168
-176
@@ -1,176 +1,168 @@
|
||||
// ==================================================================================
|
||||
// Copyright (c) 2016 HiFi-LoFi
|
||||
//
|
||||
// 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 _AUDIOFFT_H
|
||||
#define _AUDIOFFT_H
|
||||
|
||||
|
||||
/**
|
||||
* AudioFFT provides real-to-complex/complex-to-real FFT routines.
|
||||
*
|
||||
* Features:
|
||||
*
|
||||
* - Real-complex FFT and complex-real inverse FFT for power-of-2-sized real data.
|
||||
*
|
||||
* - Uniform interface to different FFT implementations (currently Ooura, FFTW3 and Apple Accelerate).
|
||||
*
|
||||
* - Complex data is handled in "split-complex" format, i.e. there are separate
|
||||
* arrays for the real and imaginary parts which can be useful for SIMD optimizations
|
||||
* (split-complex arrays have to be of length (size/2+1) representing bins from DC
|
||||
* to Nyquist frequency).
|
||||
*
|
||||
* - Output is "ready to use" (all scaling etc. is already handled internally).
|
||||
*
|
||||
* - No allocations/deallocations after the initialization which makes it usable
|
||||
* for real-time audio applications (that's what I wrote it for and using it).
|
||||
*
|
||||
*
|
||||
* How to use it in your project:
|
||||
*
|
||||
* - Add the .h and .cpp file to your project - that's all.
|
||||
*
|
||||
* - To get extra speed, you can link FFTW3 to your project and define
|
||||
* AUDIOFFT_FFTW3 (however, please check whether your project suits the
|
||||
* according license).
|
||||
*
|
||||
* - To get the best speed on Apple platforms, you can link the Apple
|
||||
* Accelerate framework to your project and define
|
||||
* AUDIOFFT_APPLE_ACCELERATE (however, please check whether your
|
||||
* project suits the according license).
|
||||
*
|
||||
*
|
||||
* Remarks:
|
||||
*
|
||||
* - AudioFFT is not intended to be the fastest FFT, but to be a fast-enough
|
||||
* FFT suitable for most audio applications.
|
||||
*
|
||||
* - AudioFFT uses the quite liberal MIT license.
|
||||
*
|
||||
*
|
||||
* Example usage:
|
||||
* @code
|
||||
* #include "AudioFFT.h"
|
||||
*
|
||||
* void Example()
|
||||
* {
|
||||
* const size_t fftSize = 1024; // Needs to be power of 2!
|
||||
*
|
||||
* std::vector<float> input(fftSize, 0.0f);
|
||||
* std::vector<float> re(audiofft::AudioFFT::ComplexSize(fftSize));
|
||||
* std::vector<float> im(audiofft::AudioFFT::ComplexSize(fftSize));
|
||||
* std::vector<float> output(fftSize);
|
||||
*
|
||||
* audiofft::AudioFFT fft;
|
||||
* fft.init(1024);
|
||||
* fft.fft(input.data(), re.data(), im.data());
|
||||
* fft.ifft(output.data(), re.data(), im.data());
|
||||
* }
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
|
||||
|
||||
namespace audiofft
|
||||
{
|
||||
|
||||
namespace details
|
||||
{
|
||||
|
||||
class AudioFFTImpl
|
||||
{
|
||||
public:
|
||||
AudioFFTImpl() = default;
|
||||
virtual ~AudioFFTImpl() = default;
|
||||
virtual void init(size_t size) = 0;
|
||||
virtual void fft(const float* data, float* re, float* im) = 0;
|
||||
virtual void ifft(float* data, const float* re, const float* im) = 0;
|
||||
|
||||
private:
|
||||
AudioFFTImpl(const AudioFFTImpl&) = delete;
|
||||
AudioFFTImpl& operator=(const AudioFFTImpl&) = delete;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// ======================================================
|
||||
|
||||
|
||||
/**
|
||||
* @class AudioFFT
|
||||
* @brief Performs 1D FFTs
|
||||
*/
|
||||
class AudioFFT
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor
|
||||
*/
|
||||
AudioFFT();
|
||||
|
||||
/**
|
||||
* @brief Initializes the FFT object
|
||||
* @param size Size of the real input (must be power 2)
|
||||
*/
|
||||
void init(size_t size);
|
||||
|
||||
/**
|
||||
* @brief Performs the forward FFT
|
||||
* @param data The real input data (has to be of the length as specified in init())
|
||||
* @param re The real part of the complex output (has to be of length as returned by ComplexSize())
|
||||
* @param im The imaginary part of the complex output (has to be of length as returned by ComplexSize())
|
||||
*/
|
||||
void fft(const float* data, float* re, float* im);
|
||||
|
||||
/**
|
||||
* @brief Performs the inverse FFT
|
||||
* @param data The real output data (has to be of the length as specified in init())
|
||||
* @param re The real part of the complex input (has to be of length as returned by ComplexSize())
|
||||
* @param im The imaginary part of the complex input (has to be of length as returned by ComplexSize())
|
||||
*/
|
||||
void ifft(float* data, const float* re, const float* im);
|
||||
|
||||
/**
|
||||
* @brief Calculates the necessary size of the real/imaginary complex arrays
|
||||
* @param size The size of the real data
|
||||
* @return The size of the real/imaginary complex arrays
|
||||
*/
|
||||
static size_t ComplexSize(size_t size);
|
||||
|
||||
private:
|
||||
std::unique_ptr<details::AudioFFTImpl> _impl;
|
||||
|
||||
AudioFFT(const AudioFFT&) = delete;
|
||||
AudioFFT& operator=(const AudioFFT&) = delete;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @brief Let's keep an AudioFFTBase type around for now because it has been here already in the 1st version in order to avoid breaking existing code.
|
||||
*/
|
||||
typedef AudioFFT AudioFFTBase;
|
||||
|
||||
} // End of namespace
|
||||
|
||||
#endif // Header guard
|
||||
// ==================================================================================
|
||||
// Copyright (c) 2017 HiFi-LoFi
|
||||
//
|
||||
// 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 _AUDIOFFT_H
|
||||
#define _AUDIOFFT_H
|
||||
|
||||
|
||||
/**
|
||||
* AudioFFT provides real-to-complex/complex-to-real FFT routines.
|
||||
*
|
||||
* Features:
|
||||
*
|
||||
* - Real-complex FFT and complex-real inverse FFT for power-of-2-sized real data.
|
||||
*
|
||||
* - Uniform interface to different FFT implementations (currently Ooura, FFTW3 and Apple Accelerate).
|
||||
*
|
||||
* - Complex data is handled in "split-complex" format, i.e. there are separate
|
||||
* arrays for the real and imaginary parts which can be useful for SIMD optimizations
|
||||
* (split-complex arrays have to be of length (size/2+1) representing bins from DC
|
||||
* to Nyquist frequency).
|
||||
*
|
||||
* - Output is "ready to use" (all scaling etc. is already handled internally).
|
||||
*
|
||||
* - No allocations/deallocations after the initialization which makes it usable
|
||||
* for real-time audio applications (that's what I wrote it for and using it).
|
||||
*
|
||||
*
|
||||
* How to use it in your project:
|
||||
*
|
||||
* - Add the .h and .cpp file to your project - that's all.
|
||||
*
|
||||
* - To get extra speed, you can link FFTW3 to your project and define
|
||||
* AUDIOFFT_FFTW3 (however, please check whether your project suits the
|
||||
* according license).
|
||||
*
|
||||
* - To get the best speed on Apple platforms, you can link the Apple
|
||||
* Accelerate framework to your project and define
|
||||
* AUDIOFFT_APPLE_ACCELERATE (however, please check whether your
|
||||
* project suits the according license).
|
||||
*
|
||||
*
|
||||
* Remarks:
|
||||
*
|
||||
* - AudioFFT is not intended to be the fastest FFT, but to be a fast-enough
|
||||
* FFT suitable for most audio applications.
|
||||
*
|
||||
* - AudioFFT uses the quite liberal MIT license.
|
||||
*
|
||||
*
|
||||
* Example usage:
|
||||
* @code
|
||||
* #include "AudioFFT.h"
|
||||
*
|
||||
* void Example()
|
||||
* {
|
||||
* const size_t fftSize = 1024; // Needs to be power of 2!
|
||||
*
|
||||
* std::vector<float> input(fftSize, 0.0f);
|
||||
* std::vector<float> re(audiofft::AudioFFT::ComplexSize(fftSize));
|
||||
* std::vector<float> im(audiofft::AudioFFT::ComplexSize(fftSize));
|
||||
* std::vector<float> output(fftSize);
|
||||
*
|
||||
* audiofft::AudioFFT fft;
|
||||
* fft.init(1024);
|
||||
* fft.fft(input.data(), re.data(), im.data());
|
||||
* fft.ifft(output.data(), re.data(), im.data());
|
||||
* }
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
|
||||
|
||||
namespace audiofft
|
||||
{
|
||||
|
||||
namespace detail
|
||||
{
|
||||
class AudioFFTImpl;
|
||||
}
|
||||
|
||||
|
||||
// =============================================================
|
||||
|
||||
|
||||
/**
|
||||
* @class AudioFFT
|
||||
* @brief Performs 1D FFTs
|
||||
*/
|
||||
class AudioFFT
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor
|
||||
*/
|
||||
AudioFFT();
|
||||
|
||||
AudioFFT(const AudioFFT&) = delete;
|
||||
AudioFFT& operator=(const AudioFFT&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Destructor
|
||||
*/
|
||||
~AudioFFT();
|
||||
|
||||
/**
|
||||
* @brief Initializes the FFT object
|
||||
* @param size Size of the real input (must be power 2)
|
||||
*/
|
||||
void init(size_t size);
|
||||
|
||||
/**
|
||||
* @brief Performs the forward FFT
|
||||
* @param data The real input data (has to be of the length as specified in init())
|
||||
* @param re The real part of the complex output (has to be of length as returned by ComplexSize())
|
||||
* @param im The imaginary part of the complex output (has to be of length as returned by ComplexSize())
|
||||
*/
|
||||
void fft(const float* data, float* re, float* im);
|
||||
|
||||
/**
|
||||
* @brief Performs the inverse FFT
|
||||
* @param data The real output data (has to be of the length as specified in init())
|
||||
* @param re The real part of the complex input (has to be of length as returned by ComplexSize())
|
||||
* @param im The imaginary part of the complex input (has to be of length as returned by ComplexSize())
|
||||
*/
|
||||
void ifft(float* data, const float* re, const float* im);
|
||||
|
||||
/**
|
||||
* @brief Calculates the necessary size of the real/imaginary complex arrays
|
||||
* @param size The size of the real data
|
||||
* @return The size of the real/imaginary complex arrays
|
||||
*/
|
||||
static size_t ComplexSize(size_t size);
|
||||
|
||||
private:
|
||||
std::unique_ptr<detail::AudioFFTImpl> _impl;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @brief Let's keep an AudioFFTBase type around for now because it has been here already in the 1st version in order to avoid breaking existing code.
|
||||
*/
|
||||
typedef AudioFFT AudioFFTBase;
|
||||
|
||||
} // End of namespace
|
||||
|
||||
#endif // Header guard
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
#include "ConvolverThreadPool.h"
|
||||
#include "FFTConvolver.h"
|
||||
#include "config.h"
|
||||
|
||||
ConvolverThreadPool::ConvolverThreadPool()
|
||||
: _convolvers(), _threads(), _taskQueue(), _queueMutex(), _condition(), _completionCV(),
|
||||
_stop(false), _activeTasks(0) {}
|
||||
|
||||
ConvolverThreadPool::~ConvolverThreadPool() {
|
||||
shutdown();
|
||||
|
||||
// Delete all convolver instances
|
||||
for (size_t i = 0; i < _convolvers.size(); ++i) {
|
||||
delete _convolvers[i];
|
||||
}
|
||||
_convolvers.clear();
|
||||
}
|
||||
|
||||
bool ConvolverThreadPool::init(size_t numThreads, size_t numConvolvers) {
|
||||
// Clean up any existing threads first
|
||||
shutdown();
|
||||
|
||||
// Validate parameters
|
||||
if (numThreads == 0 || numConvolvers == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Delete any existing convolvers
|
||||
for (size_t i = 0; i < _convolvers.size(); ++i) {
|
||||
delete _convolvers[i];
|
||||
}
|
||||
_convolvers.clear();
|
||||
|
||||
// Reset state
|
||||
_stop = false;
|
||||
_activeTasks = 0;
|
||||
|
||||
// Create convolver instances (but don't initialize them yet)
|
||||
_convolvers.resize(numConvolvers);
|
||||
for (size_t i = 0; i < numConvolvers; ++i) {
|
||||
_convolvers[i] = new FFTConvolver();
|
||||
}
|
||||
|
||||
// Create worker threads
|
||||
_threads.reserve(numThreads);
|
||||
for (size_t i = 0; i < numThreads; i++) {
|
||||
#ifndef COMPILE_FOR_OSX
|
||||
pthread_setname_np(pthread_self(), "convolver");
|
||||
#endif
|
||||
_threads.emplace_back([this]() { workerThread(); });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mutex to protect FFTW plan creation (required)
|
||||
std::mutex fftwMutex;
|
||||
|
||||
bool ConvolverThreadPool::initConvolver(size_t convolverId, size_t blockSize, const Sample *ir,
|
||||
size_t irLen) {
|
||||
if (convolverId >= _convolvers.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure no tasks are using this convolver
|
||||
waitForAll();
|
||||
std::lock_guard<std::mutex> lock(fftwMutex);
|
||||
return _convolvers[convolverId]->init(blockSize, ir, irLen);
|
||||
}
|
||||
|
||||
bool ConvolverThreadPool::initAllConvolvers(size_t blockSize, const Sample *ir, size_t irLen) {
|
||||
// Make sure no tasks are running
|
||||
waitForAll();
|
||||
|
||||
for (size_t i = 0; i < _convolvers.size(); ++i) {
|
||||
if (!_convolvers[i]->init(blockSize, ir, irLen)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ConvolverThreadPool::processAsync(size_t convolverId, const Sample *input, Sample *output,
|
||||
size_t len) {
|
||||
assert(convolverId < _convolvers.size());
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_queueMutex);
|
||||
|
||||
// Create the task
|
||||
_taskQueue.push([this, convolverId, input, output, len]() {
|
||||
_convolvers[convolverId]->process(input, output, len);
|
||||
});
|
||||
|
||||
++_activeTasks;
|
||||
}
|
||||
|
||||
// Wake up one worker thread
|
||||
_condition.notify_one();
|
||||
}
|
||||
|
||||
void ConvolverThreadPool::waitForAll() {
|
||||
std::unique_lock<std::mutex> lock(_queueMutex);
|
||||
_completionCV.wait(lock, [this]() { return _taskQueue.empty() && _activeTasks == 0; });
|
||||
}
|
||||
|
||||
void ConvolverThreadPool::clearState(size_t convolverId) {
|
||||
assert(convolverId < _convolvers.size());
|
||||
|
||||
// Make sure no tasks are running before clearing state
|
||||
waitForAll();
|
||||
|
||||
// _convolvers[convolverId]->clearState();
|
||||
}
|
||||
|
||||
void ConvolverThreadPool::clearAllStates() {
|
||||
// Make sure no tasks are running before clearing state
|
||||
waitForAll();
|
||||
|
||||
for (size_t i = 0; i < _convolvers.size(); ++i) {
|
||||
_convolvers[i]->clearState();
|
||||
}
|
||||
}
|
||||
|
||||
void ConvolverThreadPool::workerThread() {
|
||||
while (true) {
|
||||
std::function<void()> task;
|
||||
|
||||
// Wait for a task or stop signal
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_queueMutex);
|
||||
_condition.wait(lock, [this]() { return _stop || !_taskQueue.empty(); });
|
||||
|
||||
// Exit if stopping and no more tasks
|
||||
if (_stop && _taskQueue.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the next task
|
||||
task = std::move(_taskQueue.front());
|
||||
_taskQueue.pop();
|
||||
}
|
||||
|
||||
// Execute the task (outside the lock for better parallelism)
|
||||
task();
|
||||
|
||||
// Mark task as complete
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_queueMutex);
|
||||
--_activeTasks;
|
||||
_completionCV.notify_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConvolverThreadPool::shutdown() {
|
||||
// Signal threads to stop
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_queueMutex);
|
||||
_stop = true;
|
||||
}
|
||||
_condition.notify_all();
|
||||
|
||||
// Wait for all threads to finish
|
||||
for (auto &thread : _threads) {
|
||||
if (thread.joinable()) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear thread vector
|
||||
_threads.clear();
|
||||
|
||||
// Clear remaining tasks - properly this time
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_queueMutex);
|
||||
while (!_taskQueue.empty()) {
|
||||
_taskQueue.pop();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset state
|
||||
_activeTasks = 0;
|
||||
_stop = false;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#ifndef CONVOLVER_THREAD_POOL_H
|
||||
#define CONVOLVER_THREAD_POOL_H
|
||||
|
||||
#include "FFTConvolver.h"
|
||||
#include <cassert>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
// Use the fftconvolver namespace
|
||||
using fftconvolver::FFTConvolver;
|
||||
using fftconvolver::Sample;
|
||||
|
||||
class ConvolverThreadPool {
|
||||
private:
|
||||
std::vector<FFTConvolver *> _convolvers;
|
||||
std::vector<std::thread> _threads;
|
||||
std::queue<std::function<void()>> _taskQueue;
|
||||
std::mutex _queueMutex;
|
||||
std::condition_variable _condition;
|
||||
std::condition_variable _completionCV;
|
||||
bool _stop;
|
||||
size_t _activeTasks;
|
||||
|
||||
public:
|
||||
ConvolverThreadPool();
|
||||
~ConvolverThreadPool();
|
||||
|
||||
// Initialize the thread pool (creates convolvers but doesn't initialize them)
|
||||
// numThreads: number of worker threads (level of parallelism)
|
||||
// numConvolvers: total number of convolver instances
|
||||
bool init(size_t numThreads, size_t numConvolvers);
|
||||
|
||||
// Initialize a specific convolver with its IR
|
||||
// convolverId: which convolver to initialize
|
||||
// blockSize: block size for convolution
|
||||
// ir: impulse response data
|
||||
// irLen: length of impulse response
|
||||
bool initConvolver(size_t convolverId, size_t blockSize, const Sample *ir, size_t irLen);
|
||||
|
||||
// Initialize all convolvers with the same IR
|
||||
bool initAllConvolvers(size_t blockSize, const Sample *ir, size_t irLen);
|
||||
|
||||
// Queue a convolution task (non-blocking)
|
||||
void processAsync(size_t convolverId, const Sample *input, Sample *output, size_t len);
|
||||
|
||||
// Wait for all queued tasks to complete (blocking)
|
||||
void waitForAll();
|
||||
|
||||
// Clear the state of a specific convolver
|
||||
void clearState(size_t convolverId);
|
||||
|
||||
// Clear the state of all convolvers
|
||||
void clearAllStates();
|
||||
|
||||
// Get the number of convolvers
|
||||
size_t getNumConvolvers() const { return _convolvers.size(); }
|
||||
|
||||
// Get the number of threads
|
||||
size_t getNumThreads() const { return _threads.size(); }
|
||||
|
||||
void shutdown();
|
||||
|
||||
private:
|
||||
void workerThread();
|
||||
// void shutdown();
|
||||
};
|
||||
|
||||
#endif // CONVOLVER_THREAD_POOL_H
|
||||
@@ -77,6 +77,26 @@ void FFTConvolver::reset()
|
||||
_inputBufferFill = 0;
|
||||
}
|
||||
|
||||
void FFTConvolver::clearState()
|
||||
{
|
||||
if (_segCount == 0)
|
||||
{
|
||||
return; // Not initialized
|
||||
}
|
||||
|
||||
_inputBuffer.setZero();
|
||||
_inputBufferFill = 0;
|
||||
_overlap.setZero();
|
||||
|
||||
for (size_t i = 0; i < _segCount; ++i)
|
||||
{
|
||||
_segments[i]->setZero();
|
||||
}
|
||||
|
||||
_preMultiplied.setZero();
|
||||
_conv.setZero();
|
||||
_current = 0;
|
||||
}
|
||||
|
||||
bool FFTConvolver::init(size_t blockSize, const Sample* ir, size_t irLen)
|
||||
{
|
||||
|
||||
+21
-11
@@ -1,18 +1,22 @@
|
||||
// ==================================================================================
|
||||
// Copyright (c) 2012 HiFi-LoFi
|
||||
// Copyright (c) 2017 HiFi-LoFi
|
||||
//
|
||||
// This is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// 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:
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// 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 _FFTCONVOLVER_FFTCONVOLVER_H
|
||||
@@ -74,6 +78,12 @@ public:
|
||||
*/
|
||||
void reset();
|
||||
|
||||
/**
|
||||
* @brief Clears audio history
|
||||
*/
|
||||
|
||||
void clearState();
|
||||
|
||||
private:
|
||||
size_t _blockSize;
|
||||
size_t _segSize;
|
||||
|
||||
+15
-11
@@ -1,18 +1,22 @@
|
||||
// ==================================================================================
|
||||
// Copyright (c) 2012 HiFi-LoFi
|
||||
// Copyright (c) 2017 HiFi-LoFi
|
||||
//
|
||||
// This is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// 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:
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
// ==================================================================================
|
||||
|
||||
#include "Utilities.h"
|
||||
|
||||
+16
-13
@@ -1,18 +1,22 @@
|
||||
// ==================================================================================
|
||||
// Copyright (c) 2012 HiFi-LoFi
|
||||
// Copyright (c) 2017 HiFi-LoFi
|
||||
//
|
||||
// This is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// 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:
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// 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 _FFTCONVOLVER_UTILITIES_H
|
||||
@@ -94,8 +98,7 @@ public:
|
||||
_size = size;
|
||||
}
|
||||
}
|
||||
if (_data)
|
||||
setZero();
|
||||
setZero();
|
||||
}
|
||||
|
||||
size_t size() const
|
||||
|
||||
+171
-43
@@ -1,85 +1,213 @@
|
||||
|
||||
|
||||
#include <pthread.h>
|
||||
#include <sndfile.h>
|
||||
#include "convolver.h"
|
||||
#include "ConvolverThreadPool.h"
|
||||
#include "FFTConvolver.h"
|
||||
#include "Utilities.h"
|
||||
#include <pthread.h>
|
||||
#include <sndfile.h>
|
||||
#include <unistd.h>
|
||||
|
||||
extern "C" void _warn(const char *filename, const int linenumber, const char *format, ...);
|
||||
extern "C" void _debug(const char *filename, const int linenumber, int level, const char *format, ...);
|
||||
extern "C" void _debug(const char *filename, const int linenumber, int level, const char *format,
|
||||
...);
|
||||
|
||||
#define warn(...) _warn(__FILE__, __LINE__, __VA_ARGS__)
|
||||
#define debug(...) _debug(__FILE__, __LINE__, __VA_ARGS__)
|
||||
|
||||
fftconvolver::FFTConvolver convolver_l;
|
||||
fftconvolver::FFTConvolver convolver_r;
|
||||
// Create and initialize the thread pool
|
||||
ConvolverThreadPool pool;
|
||||
|
||||
// always lock use this when accessing the playing conn value
|
||||
pthread_mutex_t convolver_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
void convolver_pool_init(size_t numThreads, size_t numConvolvers) {
|
||||
if (!pool.init(numThreads, numConvolvers)) {
|
||||
debug(1, "failed to initialize thread pool!");
|
||||
} else {
|
||||
debug(1, "thread pool initialized with %u threads and %u convolvers.", numThreads,
|
||||
numConvolvers);
|
||||
}
|
||||
}
|
||||
|
||||
void convolver_pool_closedown() {
|
||||
pool.shutdown(); // Just shutdown, don't delete
|
||||
debug(3, "thread pool shut down");
|
||||
}
|
||||
|
||||
int convolver_init(const char* filename, int max_length) {
|
||||
int convolver_init(const char *filename, unsigned char channel_count,
|
||||
double max_length_in_seconds, size_t block_size) {
|
||||
debug(3, "convolver_init");
|
||||
int success = 0;
|
||||
SF_INFO info;
|
||||
SF_INFO info = {}; // Zero everything, including format
|
||||
if (filename) {
|
||||
SNDFILE* file = sf_open(filename, SFM_READ, &info);
|
||||
SNDFILE *file = sf_open(filename, SFM_READ, &info);
|
||||
if (file) {
|
||||
|
||||
if (info.samplerate == 44100) {
|
||||
if ((info.channels == 1) || (info.channels == 2)) {
|
||||
const size_t size = info.frames > max_length ? max_length : info.frames;
|
||||
float buffer[size*info.channels];
|
||||
|
||||
size_t max_length = (size_t)(max_length_in_seconds * info.samplerate);
|
||||
const size_t size =
|
||||
(unsigned int)info.frames > max_length ? max_length : (unsigned int)info.frames;
|
||||
float *buffer = (float*)malloc(sizeof(float) * size * info.channels);
|
||||
if (buffer != NULL) {
|
||||
// float buffer[size * info.channels];
|
||||
float *abuffer = (float*)malloc(sizeof(float) * size);
|
||||
if (abuffer != NULL) {
|
||||
size_t l = sf_readf_float(file, buffer, size);
|
||||
if (l != 0) {
|
||||
pthread_mutex_lock(&convolver_lock);
|
||||
convolver_l.reset(); // it is possible that init could be called more than once
|
||||
convolver_r.reset(); // so it could be necessary to remove all previous settings
|
||||
|
||||
unsigned int cc;
|
||||
if (info.channels == 1) {
|
||||
convolver_l.init(352, buffer, size);
|
||||
convolver_r.init(352, buffer, size);
|
||||
} else {
|
||||
// deinterleave
|
||||
float buffer_l[size];
|
||||
float buffer_r[size];
|
||||
|
||||
unsigned int i;
|
||||
for (i=0; i<size; ++i)
|
||||
{
|
||||
buffer_l[i] = buffer[2*i+0];
|
||||
buffer_r[i] = buffer[2*i+1];
|
||||
for (cc = 0; cc < channel_count; cc++) {
|
||||
if (!pool.initConvolver(cc, block_size, buffer, size)) {
|
||||
debug(1, "new convolver failed to initialize convolver %u ", cc);
|
||||
}
|
||||
}
|
||||
} else if (info.channels == channel_count) {
|
||||
// we have to deinterleave the ir file channels for each convolver
|
||||
// float abuffer[size];
|
||||
for (cc = 0; cc < channel_count; cc++) {
|
||||
unsigned int i;
|
||||
for (i = 0; i < size; ++i) {
|
||||
abuffer[i] = buffer[channel_count * i + cc];
|
||||
}
|
||||
if (!pool.initConvolver(cc, block_size, abuffer, size)) {
|
||||
debug(1, "new convolver failed to initialize convolver %u ", cc);
|
||||
}
|
||||
}
|
||||
|
||||
convolver_l.init(352, buffer_l, size);
|
||||
convolver_r.init(352, buffer_r, size);
|
||||
|
||||
}
|
||||
pthread_mutex_unlock(&convolver_lock);
|
||||
success = 1;
|
||||
}
|
||||
debug(1, "IR initialized from \"%s\" with %d channels and %d samples", filename, info.channels, size);
|
||||
debug(2,
|
||||
"convolution impulse response filter initialized from \"%s\" with %d channel%s and "
|
||||
"%d samples",
|
||||
filename, info.channels, info.channels == 1 ? "" : "s", size);
|
||||
sf_close(file);
|
||||
free((void*)abuffer);
|
||||
} else {
|
||||
warn("Impulse file \"%s\" contains %d channels. Only 1 or 2 is supported.", filename, info.channels);
|
||||
debug(1, "failed to init convolvers because insufficient memory was available");
|
||||
}
|
||||
free((void*)buffer);
|
||||
} else {
|
||||
warn("Impulse file \"%s\" sample rate is %d Hz. Only 44100 Hz is supported", filename, info.samplerate);
|
||||
warn("failed to init convolvers because insufficient memory was available");
|
||||
}
|
||||
sf_close(file);
|
||||
} else {
|
||||
warn("Convolution impulse response filter file \"%s\" can not be opened. Please check that "
|
||||
"it exists, is a valid sound file and has appropriate access permissions.",
|
||||
filename);
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
void convolver_process_l(float* data, int length) {
|
||||
void convolver_process(unsigned int channel, float *data, int length) {
|
||||
pool.processAsync(channel, data, data, length);
|
||||
}
|
||||
|
||||
void convolver_wait_for_all() { pool.waitForAll(); }
|
||||
|
||||
void convolver_clear_state() {
|
||||
pool.clearAllStates();
|
||||
}
|
||||
|
||||
const unsigned int max_channels = 8;
|
||||
fftconvolver::FFTConvolver convolvers[max_channels];
|
||||
|
||||
// fftconvolver::FFTConvolver convolver_l;
|
||||
// fftconvolver::FFTConvolver convolver_r;
|
||||
|
||||
// always lock use this when accessing the playing conn value
|
||||
/*
|
||||
|
||||
pthread_mutex_t convolver_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
int convolver_init(const char *filename, unsigned char channel_count, double max_length_in_seconds,
|
||||
size_t block_size) {
|
||||
debug(1, "convolver_init");
|
||||
int success = 0;
|
||||
SF_INFO info;
|
||||
if (filename) {
|
||||
SNDFILE *file = sf_open(filename, SFM_READ, &info);
|
||||
if (file) {
|
||||
size_t max_length = (size_t)(max_length_in_seconds * info.samplerate);
|
||||
const size_t size =
|
||||
(unsigned int)info.frames > max_length ? max_length : (unsigned int)info.frames;
|
||||
float buffer[size * info.channels];
|
||||
|
||||
size_t l = sf_readf_float(file, buffer, size);
|
||||
if (l != 0) {
|
||||
pthread_mutex_lock(&convolver_lock);
|
||||
|
||||
unsigned int cc;
|
||||
for (cc = 0; cc < channel_count; cc++) {
|
||||
convolvers[cc].reset();
|
||||
}
|
||||
|
||||
if (info.channels == 1) {
|
||||
for (cc = 0; cc < channel_count; cc++) {
|
||||
convolvers[cc].init(block_size, buffer, size);
|
||||
}
|
||||
} else if (info.channels == channel_count) {
|
||||
// we have to deinterleave the ir file channels for each convolver
|
||||
for (cc = 0; cc < channel_count; cc++) {
|
||||
float abuffer[size];
|
||||
unsigned int i;
|
||||
for (i = 0; i < size; ++i) {
|
||||
abuffer[i] = buffer[channel_count * i + cc];
|
||||
}
|
||||
convolvers[cc].init(block_size, abuffer, size);
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&convolver_lock);
|
||||
success = 1;
|
||||
}
|
||||
debug(2,
|
||||
"convolution impulse response filter initialized from \"%s\" with %d channel%s and "
|
||||
"%d samples",
|
||||
filename, info.channels, info.channels == 1 ? "" : "s", size);
|
||||
sf_close(file);
|
||||
} else {
|
||||
warn("Convolution impulse response filter file \"%s\" can not be opened. Please check that "
|
||||
"it exists, is a valid sound file and has appropriate access permissions.",
|
||||
filename);
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
void convolver_reset() {
|
||||
debug(1, "convolver_reset");
|
||||
pthread_mutex_lock(&convolver_lock);
|
||||
unsigned int cc;
|
||||
for (cc = 0; cc < max_channels; cc++) {
|
||||
convolvers[cc].reset();
|
||||
}
|
||||
// convolver_l.reset(); // it is possible that init could be called more than once
|
||||
// convolver_r.reset(); // so it could be necessary to remove all previous settings
|
||||
pthread_mutex_unlock(&convolver_lock);
|
||||
}
|
||||
|
||||
void convolver_clear_state() {
|
||||
debug(1, "convolver_clear_state");
|
||||
pthread_mutex_lock(&convolver_lock);
|
||||
unsigned int cc;
|
||||
for (cc = 0; cc < max_channels; cc++) {
|
||||
convolvers[cc].clearState();
|
||||
}
|
||||
// convolver_l.reset(); // it is possible that init could be called more than once
|
||||
// convolver_r.reset(); // so it could be necessary to remove all previous settings
|
||||
pthread_mutex_unlock(&convolver_lock);
|
||||
}
|
||||
|
||||
void convolver_process(unsigned int channel, float *data, int length) {
|
||||
pthread_mutex_lock(&convolver_lock);
|
||||
convolvers[channel].process(data, data, length);
|
||||
pthread_mutex_unlock(&convolver_lock);
|
||||
usleep(100);
|
||||
}
|
||||
|
||||
void convolver_process_l(float *data, int length) {
|
||||
pthread_mutex_lock(&convolver_lock);
|
||||
convolver_l.process(data, data, length);
|
||||
pthread_mutex_unlock(&convolver_lock);
|
||||
}
|
||||
|
||||
void convolver_process_r(float* data, int length) {
|
||||
void convolver_process_r(float *data, int length) {
|
||||
pthread_mutex_lock(&convolver_lock);
|
||||
convolver_r.process(data, data, length);
|
||||
pthread_mutex_unlock(&convolver_lock);
|
||||
}
|
||||
*/
|
||||
@@ -4,10 +4,22 @@
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
int convolver_init(const char* file, int max_length);
|
||||
void convolver_process_l(float* data, int length);
|
||||
void convolver_process_r(float* data, int length);
|
||||
// int convolver_init(const char* file, unsigned char channel_count, double max_length_in_seconds, size_t block_size);
|
||||
void convolver_reset();
|
||||
//void convolver_clear_state();
|
||||
// void convolver_process(unsigned int channel, float *data, int length);
|
||||
// void convolver_process_l(float* data, int length);
|
||||
// void convolver_process_r(float* data, int length);
|
||||
|
||||
void convolver_pool_init(size_t numThreads, size_t numConvolvers);
|
||||
void convolver_pool_closedown();
|
||||
int convolver_init(const char* file, unsigned char channel_count, double max_length_in_seconds, size_t block_size);
|
||||
void convolver_process(unsigned int channel, float *data, int length);
|
||||
void convolver_clear_state();
|
||||
void convolver_wait_for_all();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ mqtt =
|
||||
// publish_raw = "no"; // Whether to publish all available metadata under the codes given in the 'metadata' docs.
|
||||
publish_parsed = "yes"; // Whether to publish a small (but useful) subset of metadata under human-understandable topics.
|
||||
publish_cover = "yes"; // Whether to publish the cover over MQTT in binary form. This may lead to a bit of load on the broker.
|
||||
// publish_retain = "no"; // Whether to set the retain flag on published MQTT messages. When enabled, the broker stores the last message for each topic so new subscribers receive the most recent value immediately.
|
||||
// enable_remote = "no"; // Whether to remote control via MQTT. RC is available under `topic`/remote.
|
||||
};
|
||||
```
|
||||
|
||||
+60
-71
@@ -1,59 +1,65 @@
|
||||
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 bonjour_strings.c rtsp.c mdns.c common.c rtp.c player.c audio.c loudness.c activity_monitor.c utilities/debug.c utilities/network_utilities.c
|
||||
|
||||
if BUILD_FOR_DARWIN
|
||||
AM_CXXFLAGS = -I/usr/local/include -Wno-multichar -Wall -Wextra -Wno-deprecated-declarations -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
AM_CFLAGS = -Wno-multichar -Wall -Wextra -Wno-deprecated-declarations -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
AM_CFLAGS = --include=utilities/debug.h -Wno-multichar -Wall -Wextra -Wno-deprecated-declarations -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
else
|
||||
if BUILD_FOR_FREEBSD
|
||||
AM_CXXFLAGS = -I/usr/local/include -Wno-multichar -Wall -Wextra -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
AM_CFLAGS = -Wno-multichar -Wall -Wextra -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
AM_CFLAGS = --include=utilities/debug.h -Wno-multichar -Wall -Wextra -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
else
|
||||
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)\"
|
||||
AM_CFLAGS = --include=utilities/debug.h -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 -Wformat -Wformat=2 -Wno-clobbered -Wno-psabi -pthread -DSYSCONFDIR=\"$(sysconfdir)\"
|
||||
AM_CFLAGS = --include=utilities/debug.h -Wshadow -fno-common -Wno-multichar -Wall -Wextra -Wformat -Wformat=2 -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,16 +115,16 @@ 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
|
||||
|
||||
if USE_CONVOLUTION
|
||||
shairport_sync_SOURCES += FFTConvolver/AudioFFT.cpp FFTConvolver/FFTConvolver.cpp FFTConvolver/Utilities.cpp FFTConvolver/convolver.cpp
|
||||
shairport_sync_SOURCES += FFTConvolver/AudioFFT.cpp FFTConvolver/FFTConvolver.cpp FFTConvolver/Utilities.cpp FFTConvolver/convolver.cpp FFTConvolver/ConvolverThreadPool.cpp
|
||||
AM_CXXFLAGS += -std=c++11
|
||||
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 += ap2_buffered_audio_processor.c ap2_event_receiver.c ap2_rc_event_receiver.c ptp-utilities.c utilities/buffered_read.c utilities/structured_buffer.c utilities/mod23.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,15 +273,11 @@ $(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
|
||||
else
|
||||
SYSTEMD_SERVICE = shairport-sync.service
|
||||
endif # USE_AVAHI
|
||||
|
||||
INSTALL_SYSTEMD_TARGET = install-systemd-local
|
||||
|
||||
@@ -298,13 +285,14 @@ 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
|
||||
endif # INSTALL_SYSTEMD_STARTUP
|
||||
|
||||
if INSTALL_FREEBSD_SERVICE
|
||||
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 +306,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 +316,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)
|
||||
|
||||
|
||||
@@ -17,23 +17,21 @@ Shairport Sync does not support AirPlay video or photo streaming.
|
||||
* 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.
|
||||
|
||||
+4
-3
@@ -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
|
||||
@@ -146,7 +146,7 @@ void *activity_monitor_thread_code(void *arg) {
|
||||
|
||||
rc = pthread_cond_init(&activity_monitor_cv, NULL);
|
||||
if (rc)
|
||||
die("activity_monitor: error %d initialising activity_monitor_cv.");
|
||||
die("activity_monitor: error %d initialising activity_monitor_cv.", rc);
|
||||
pthread_cleanup_push(activity_thread_cleanup_handler, arg);
|
||||
|
||||
uint64_t sec;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,641 @@
|
||||
/*
|
||||
* AirPlay 2 Buffered Audio Processor. This file is part of Shairport Sync
|
||||
* Copyright (c) Mike Brady 2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "ap2_buffered_audio_processor.h"
|
||||
#include "common.h"
|
||||
#include "player.h"
|
||||
#include "rtp.h"
|
||||
#include "utilities/buffered_read.h"
|
||||
#include "utilities/mod23.h"
|
||||
#include <sodium.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef CONFIG_CONVOLUTION
|
||||
#include "FFTConvolver/convolver.h"
|
||||
#endif
|
||||
|
||||
void addADTStoPacket(uint8_t *packet, int packetLen, int rate, int channel_configuration) {
|
||||
// https://stackoverflow.com/questions/18862715/how-to-generate-the-aac-adts-elementary-stream-with-android-mediacodec
|
||||
// with thanks!
|
||||
|
||||
// See https://wiki.multimedia.cx/index.php/Understanding_AAC
|
||||
// see also https://wiki.multimedia.cx/index.php/ADTS for the ADTS layout
|
||||
// see https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Sampling_Frequencies for sampling
|
||||
// frequencies
|
||||
|
||||
/**
|
||||
* Add ADTS header at the beginning of each and every AAC packet.
|
||||
* This is needed as the packet is raw AAC data.
|
||||
*
|
||||
* Note the packetLen must count in the ADTS header itself.
|
||||
**/
|
||||
|
||||
int profile = 2;
|
||||
int freqIdx = 4;
|
||||
if (rate == 44100)
|
||||
freqIdx = 4;
|
||||
else if (rate == 48000)
|
||||
freqIdx = 3;
|
||||
else
|
||||
debug(1, "Unsupported AAC sample rate %d.", rate);
|
||||
|
||||
// Channel Configuration
|
||||
// https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Channel_Configurations
|
||||
// clang-format off
|
||||
// 0: Defined in AOT Specifc Config
|
||||
// 1: 1 channel: front-center
|
||||
// 2: 2 channels: front-left, front-right
|
||||
// 3: 3 channels: front-center, front-left, front-right
|
||||
// 4: 4 channels: front-center, front-left, front-right, back-center
|
||||
// 5: 5 channels: front-center, front-left, front-right, back-left, back-right
|
||||
// 6: 6 channels: front-center, front-left, front-right, back-left, back-right, LFE-channel
|
||||
// 7: 8 channels: front-center, front-left, front-right, side-left, side-right, back-left, back-right, LFE-channel
|
||||
// 8-15: Reserved
|
||||
// clang-format on
|
||||
|
||||
int chanCfg = channel_configuration; // CPE
|
||||
|
||||
// fill in ADTS data
|
||||
packet[0] = 0xFF;
|
||||
packet[1] = 0xF9;
|
||||
packet[2] = ((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2);
|
||||
packet[3] = ((chanCfg & 3) << 6) + (packetLen >> 11);
|
||||
packet[4] = (packetLen & 0x7FF) >> 3;
|
||||
packet[5] = ((packetLen & 7) << 5) + 0x1F;
|
||||
packet[6] = 0xFC;
|
||||
}
|
||||
|
||||
void rtp_buffered_audio_cleanup_handler(__attribute__((unused)) void *arg) {
|
||||
debug(2, "Buffered Audio Receiver Cleanup Start.");
|
||||
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
|
||||
close(conn->buffered_audio_socket);
|
||||
debug(3, "Connection %d: closing TCP Buffered Audio port: %u.", conn->connection_number,
|
||||
conn->local_buffered_audio_port);
|
||||
conn->buffered_audio_socket = 0;
|
||||
debug(2, "Connection %d: rtp_buffered_audio_processor exit.", conn->connection_number);
|
||||
}
|
||||
|
||||
void *rtp_buffered_audio_processor(void *arg) {
|
||||
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
|
||||
// #include <syscall.h>
|
||||
// debug(1, "Connection %d: rtp_buffered_audio_processor PID %d start", conn->connection_number,
|
||||
// syscall(SYS_gettid));
|
||||
conn->incoming_ssrc = 0; // reset
|
||||
conn->resampler_ssrc = 0;
|
||||
|
||||
// turn off all flush requests that might have been pending in the connection. Not sure if this is
|
||||
// right...
|
||||
unsigned int fr = 0;
|
||||
for (fr = 0; fr < MAX_DEFERRED_FLUSH_REQUESTS; fr++) {
|
||||
conn->ap2_deferred_flush_requests[fr].inUse = 0;
|
||||
conn->ap2_deferred_flush_requests[fr].active = 0;
|
||||
}
|
||||
conn->ap2_immediate_flush_requested = 0;
|
||||
|
||||
pthread_cleanup_push(rtp_buffered_audio_cleanup_handler, arg);
|
||||
|
||||
pthread_t *buffered_reader_thread = malloc(sizeof(pthread_t));
|
||||
if (buffered_reader_thread == NULL)
|
||||
debug(1, "cannot allocate a buffered_reader_thread!");
|
||||
memset(buffered_reader_thread, 0, sizeof(pthread_t));
|
||||
pthread_cleanup_push(malloc_cleanup, &buffered_reader_thread);
|
||||
|
||||
buffered_tcp_desc *buffered_audio = malloc(sizeof(buffered_tcp_desc));
|
||||
if (buffered_audio == NULL)
|
||||
debug(1, "cannot allocate a buffered_tcp_desc!");
|
||||
// initialise the
|
||||
|
||||
memset(buffered_audio, 0, sizeof(buffered_tcp_desc));
|
||||
pthread_cleanup_push(malloc_cleanup, &buffered_audio);
|
||||
|
||||
if (pthread_mutex_init(&buffered_audio->mutex, NULL))
|
||||
debug(1, "Connection %d: error %d initialising buffered_audio mutex.", conn->connection_number,
|
||||
errno);
|
||||
pthread_cleanup_push(mutex_cleanup, &buffered_audio->mutex);
|
||||
|
||||
if (pthread_cond_init(&buffered_audio->not_empty_cv, NULL))
|
||||
die("Connection %d: error %d initialising not_empty cv.", conn->connection_number, errno);
|
||||
pthread_cleanup_push(cv_cleanup, &buffered_audio->not_empty_cv);
|
||||
|
||||
if (pthread_cond_init(&buffered_audio->not_full_cv, NULL))
|
||||
die("Connection %d: error %d initialising not_full cv.", conn->connection_number, errno);
|
||||
pthread_cleanup_push(cv_cleanup, &buffered_audio->not_full_cv);
|
||||
|
||||
// initialise the buffer data structure
|
||||
buffered_audio->buffer_max_size = conn->ap2_audio_buffer_size;
|
||||
buffered_audio->buffer = malloc(conn->ap2_audio_buffer_size);
|
||||
if (buffered_audio->buffer == NULL)
|
||||
debug(1, "cannot allocate an audio buffer of %zu bytes!", buffered_audio->buffer_max_size);
|
||||
pthread_cleanup_push(malloc_cleanup, &buffered_audio->buffer);
|
||||
|
||||
// pthread_mutex_lock(&conn->buffered_audio_mutex);
|
||||
buffered_audio->toq = buffered_audio->buffer;
|
||||
buffered_audio->eoq = buffered_audio->buffer;
|
||||
|
||||
buffered_audio->sock_fd = conn->buffered_audio_socket;
|
||||
|
||||
named_pthread_create(buffered_reader_thread, NULL, &buffered_tcp_reader, buffered_audio,
|
||||
"ap2_buf_rdr_%d", conn->connection_number);
|
||||
pthread_cleanup_push(thread_cleanup, buffered_reader_thread);
|
||||
|
||||
const size_t leading_free_space_length =
|
||||
256; // leave this many bytes free to make room for prefixes that might be added later
|
||||
uint8_t packet[32 * 1024];
|
||||
unsigned char m[32 * 1024 + leading_free_space_length];
|
||||
|
||||
unsigned char *payload_pointer = NULL;
|
||||
unsigned long long payload_length = 0;
|
||||
uint32_t payload_ssrc =
|
||||
SSRC_NONE; // this is the SSRC of the payload, needed to decide if it should be muted
|
||||
uint32_t previous_ssrc = SSRC_NONE;
|
||||
|
||||
uint32_t seq_no =
|
||||
0; // audio packet number. Initialised to avoid a "possibly uninitialised" warning.
|
||||
uint32_t previous_seqno = 0;
|
||||
uint16_t sequence_number_for_player = 0;
|
||||
|
||||
uint32_t timestamp = 0; // initialised to avoid a "possibly uninitialised" warning.
|
||||
uint32_t previous_timestamp = 0;
|
||||
|
||||
uint32_t expected_timestamp = 0;
|
||||
uint64_t previous_buffer_should_be_time = 0;
|
||||
|
||||
ssize_t nread;
|
||||
|
||||
int new_audio_block_needed = 0; // goes true when a block is needed, false one is read in, but
|
||||
// will be made true by flushing or by playing the block
|
||||
int finished = 0;
|
||||
|
||||
uint64_t blocks_read_since_play_began = 0;
|
||||
uint64_t blocks_read = 0;
|
||||
|
||||
int ap2_immediate_flush_requested = 0; // for diagnostics, probably
|
||||
|
||||
uint32_t first_timestamp_in_this_sequence = 0;
|
||||
int packets_played_in_this_sequence = 0;
|
||||
|
||||
int play_enabled = 0;
|
||||
// double requested_lead_time = 0.0; // normal lead time minimum -- maybe it should be about 0.1
|
||||
|
||||
// wait until our timing information is valid
|
||||
while (have_ptp_timing_information(conn) == 0)
|
||||
usleep(1000);
|
||||
|
||||
reset_buffer(conn); // in case there is any garbage in the player
|
||||
|
||||
do {
|
||||
|
||||
if ((play_enabled == 0) && (conn->ap2_play_enabled != 0)) {
|
||||
// play newly started
|
||||
debug(2, "Play started.");
|
||||
new_audio_block_needed = 1;
|
||||
blocks_read_since_play_began = 0;
|
||||
}
|
||||
|
||||
if ((play_enabled != 0) && (conn->ap2_play_enabled == 0)) {
|
||||
debug(2, "Play stopped.");
|
||||
packets_played_in_this_sequence = 0; // not all blocks read are played...
|
||||
#ifdef CONFIG_CONVOLUTION
|
||||
convolver_clear_state();
|
||||
#endif
|
||||
reset_buffer(conn); // stop play ASAP
|
||||
}
|
||||
|
||||
play_enabled = conn->ap2_play_enabled;
|
||||
|
||||
// now, if get_next_block is non-zero, read a block. We may flush or use it
|
||||
|
||||
if (new_audio_block_needed != 0) {
|
||||
// a block is preceded by its length in a uint16_t
|
||||
uint16_t data_len;
|
||||
// here we read from the buffer that our thread has been reading
|
||||
|
||||
size_t bytes_remaining_in_buffer;
|
||||
nread =
|
||||
read_sized_block(buffered_audio, &data_len, sizeof(data_len), &bytes_remaining_in_buffer);
|
||||
data_len = ntohs(data_len);
|
||||
|
||||
// diagnostic
|
||||
if ((conn->ap2_audio_buffer_minimum_size < 0) ||
|
||||
(bytes_remaining_in_buffer < (size_t)conn->ap2_audio_buffer_minimum_size))
|
||||
conn->ap2_audio_buffer_minimum_size = bytes_remaining_in_buffer;
|
||||
|
||||
if (nread > 0) {
|
||||
// get the block itself
|
||||
// debug(1,"buffered audio packet of size %u detected.", data_len - 2);
|
||||
nread = read_sized_block(buffered_audio, packet, data_len - 2, &bytes_remaining_in_buffer);
|
||||
|
||||
// diagnostic
|
||||
if ((conn->ap2_audio_buffer_minimum_size < 0) ||
|
||||
(bytes_remaining_in_buffer < (size_t)conn->ap2_audio_buffer_minimum_size))
|
||||
conn->ap2_audio_buffer_minimum_size = bytes_remaining_in_buffer;
|
||||
// debug(1, "buffered audio packet of size %u received.", nread);
|
||||
|
||||
if (nread > 0) {
|
||||
// got the block
|
||||
blocks_read++; // note, this doesn't mean they are valid audio blocks
|
||||
blocks_read_since_play_began++; // 1 means previous seq_no and timestamps are invalid
|
||||
|
||||
// get the sequence number
|
||||
// see https://en.wikipedia.org/wiki/Real-time_Transport_Protocol#Packet_header
|
||||
// the Marker bit is always set, and it and the remaining 23 bits form the sequence number
|
||||
|
||||
previous_seqno = seq_no;
|
||||
seq_no = nctohl(&packet[0]) & 0x7FFFFF;
|
||||
|
||||
previous_timestamp = timestamp;
|
||||
timestamp = nctohl(&packet[4]);
|
||||
|
||||
if (payload_ssrc != SSRC_NONE)
|
||||
previous_ssrc = payload_ssrc;
|
||||
payload_ssrc = nctohl(&packet[8]);
|
||||
|
||||
|
||||
|
||||
if ((payload_ssrc != previous_ssrc) && (payload_ssrc != SSRC_NONE)) {
|
||||
if (ssrc_is_recognised(payload_ssrc) == 0) {
|
||||
debug(2, "Unrecognised SSRC: %u.", payload_ssrc);
|
||||
} else {
|
||||
debug(2, "Connection %d: incoming audio encoding is%s \"%s\".",
|
||||
conn->connection_number, previous_ssrc == SSRC_NONE ? "" : " switching to", get_ssrc_name(payload_ssrc));
|
||||
}
|
||||
}
|
||||
|
||||
if ((payload_ssrc != previous_ssrc) && (ssrc_is_recognised(payload_ssrc) == 0)) {
|
||||
debug(2, "Unrecognised SSRC: %u.", payload_ssrc);
|
||||
}
|
||||
|
||||
if (blocks_read_since_play_began == 1) {
|
||||
debug(2, "Preparing initial decoding chain for %s.", get_ssrc_name(payload_ssrc));
|
||||
prepare_decoding_chain(conn, payload_ssrc); // needed to set the input rate...
|
||||
sequence_number_for_player =
|
||||
seq_no & 0xffff; // this is arbitrary -- the sequence_number_for_player numbers will
|
||||
// be sequential irrespective of seq_no jumps...
|
||||
}
|
||||
|
||||
if (blocks_read_since_play_began > 1) {
|
||||
|
||||
uint32_t t_expected_seqno = (previous_seqno + 1) & 0x7fffff;
|
||||
if (t_expected_seqno != seq_no) {
|
||||
debug(2,
|
||||
"reading block %u, the sequence number differs from the expected sequence "
|
||||
"number %u. The previous sequence number was %u",
|
||||
seq_no, t_expected_seqno, previous_seqno);
|
||||
}
|
||||
uint32_t t_expected_timestamp =
|
||||
previous_timestamp + get_ssrc_block_length(previous_ssrc);
|
||||
int32_t diff = timestamp - t_expected_timestamp;
|
||||
if (diff != 0) {
|
||||
debug(2, "reading block %u, the timestamp %u differs from expected_timestamp %u.",
|
||||
seq_no, timestamp, t_expected_timestamp);
|
||||
}
|
||||
}
|
||||
new_audio_block_needed = 0; // block has been read.
|
||||
}
|
||||
}
|
||||
|
||||
if (nread == 0) {
|
||||
// nread is 0 -- the port has been closed
|
||||
debug(2, "Connection %d: buffered audio port closed!", conn->connection_number);
|
||||
finished = 1;
|
||||
} else if (nread < 0) {
|
||||
char errorstring[1024];
|
||||
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
|
||||
debug(1, "error in rtp_buffered_audio_processor %d: \"%s\". Could not recv a data_len .",
|
||||
errno, errorstring);
|
||||
finished = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (finished == 0) {
|
||||
pthread_cleanup_debug_mutex_lock(&conn->flush_mutex, 25000,
|
||||
1); // 25 ms is a long time to wait!
|
||||
if (blocks_read != 0) {
|
||||
if (conn->ap2_immediate_flush_requested != 0) {
|
||||
if (ap2_immediate_flush_requested == 0) {
|
||||
debug(2, "immediate flush started at sequence number %u until sequence number of %u.",
|
||||
seq_no, conn->ap2_immediate_flush_until_sequence_number);
|
||||
}
|
||||
if ((blocks_read != 0) && ((a_minus_b_mod23(seq_no, conn->ap2_immediate_flush_until_sequence_number) > 0))) {
|
||||
debug(1, "immediate flush may have escaped its endpoint! Seq_no is %u, conn->ap2_immediate_flush_until_sequence_number is %u.", seq_no, conn->ap2_immediate_flush_until_sequence_number);
|
||||
}
|
||||
|
||||
if ((blocks_read != 0) && ((a_minus_b_mod23(seq_no, conn->ap2_immediate_flush_until_sequence_number) >= 0))) {
|
||||
debug(2, "immediate flush completed at seq_no: %u, conn->ap2_immediate_flush_until_sequence_number: %u.", seq_no, conn->ap2_immediate_flush_until_sequence_number);
|
||||
|
||||
conn->ap2_immediate_flush_requested = 0;
|
||||
ap2_immediate_flush_requested = 0;
|
||||
|
||||
|
||||
// turn off all deferred requests. Not sure if this is right...
|
||||
unsigned int f = 0;
|
||||
for (f = 0; f < MAX_DEFERRED_FLUSH_REQUESTS; f++) {
|
||||
if ((conn->ap2_deferred_flush_requests[f].inUse != 0) && (conn->ap2_deferred_flush_requests[f].active = 0)) {
|
||||
debug(1,
|
||||
"deferred flush cancelled by an immediate flush: flushFromTS: %12u, flushFromSeq: %12u, "
|
||||
"flushUntilTS: %12u, flushUntilSeq: %12u, timestamp: %12u.",
|
||||
conn->ap2_deferred_flush_requests[f].flushFromTS,
|
||||
conn->ap2_deferred_flush_requests[f].flushFromSeq,
|
||||
conn->ap2_deferred_flush_requests[f].flushUntilTS,
|
||||
conn->ap2_deferred_flush_requests[f].flushUntilSeq, timestamp);
|
||||
}
|
||||
conn->ap2_deferred_flush_requests[f].inUse = 0;
|
||||
conn->ap2_deferred_flush_requests[f].active = 0;
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
debug(3, "immediate flush of block %u until block %u", seq_no,
|
||||
conn->ap2_immediate_flush_until_sequence_number);
|
||||
ap2_immediate_flush_requested = 1;
|
||||
new_audio_block_needed = 1; //
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now, even if an immediate flush has been requested and is active, we still need to process
|
||||
// deferred flush requests as they may refer to sequences that are going to be purged anyway
|
||||
|
||||
unsigned int f = 0;
|
||||
for (f = 0; f < MAX_DEFERRED_FLUSH_REQUESTS; f++) {
|
||||
if (conn->ap2_deferred_flush_requests[f].inUse != 0) {
|
||||
if ((conn->ap2_deferred_flush_requests[f].flushFromSeq == seq_no) &&
|
||||
(conn->ap2_deferred_flush_requests[f].flushUntilSeq != seq_no)) {
|
||||
debug(2,
|
||||
"deferred flush activated: flushFromTS: %12u, flushFromSeq: %12u, "
|
||||
"flushUntilTS: %12u, flushUntilSeq: %12u, timestamp: %12u.",
|
||||
conn->ap2_deferred_flush_requests[f].flushFromTS,
|
||||
conn->ap2_deferred_flush_requests[f].flushFromSeq,
|
||||
conn->ap2_deferred_flush_requests[f].flushUntilTS,
|
||||
conn->ap2_deferred_flush_requests[f].flushUntilSeq, timestamp);
|
||||
conn->ap2_deferred_flush_requests[f].active = 1;
|
||||
new_audio_block_needed = 1;
|
||||
}
|
||||
if (conn->ap2_deferred_flush_requests[f].flushUntilSeq == seq_no) {
|
||||
debug(2,
|
||||
"deferred flush terminated: flushFromTS: %12u, flushFromSeq: %12u, "
|
||||
"flushUntilTS: %12u, flushUntilSeq: %12u, timestamp: %12u.",
|
||||
conn->ap2_deferred_flush_requests[f].flushFromTS,
|
||||
conn->ap2_deferred_flush_requests[f].flushFromSeq,
|
||||
conn->ap2_deferred_flush_requests[f].flushUntilTS,
|
||||
conn->ap2_deferred_flush_requests[f].flushUntilSeq, timestamp);
|
||||
conn->ap2_deferred_flush_requests[f].active = 0;
|
||||
conn->ap2_deferred_flush_requests[f].inUse = 0;
|
||||
} else if (a_minus_b_mod23(seq_no, conn->ap2_deferred_flush_requests[f].flushUntilSeq) >
|
||||
0) {
|
||||
// now, do a modulo 2^23 unsigned int calculation to see if we may have overshot the
|
||||
// flushUntilSeq
|
||||
debug(2,
|
||||
"deferred flush terminated due to overshoot at block %u: flushFromTS: %12u, "
|
||||
"flushFromSeq: %12u, "
|
||||
"flushUntilTS: %12u, flushUntilSeq: %12u, timestamp: %12u.",
|
||||
seq_no, conn->ap2_deferred_flush_requests[f].flushFromTS,
|
||||
conn->ap2_deferred_flush_requests[f].flushFromSeq,
|
||||
conn->ap2_deferred_flush_requests[f].flushUntilTS,
|
||||
conn->ap2_deferred_flush_requests[f].flushUntilSeq, timestamp);
|
||||
conn->ap2_deferred_flush_requests[f].active = 0;
|
||||
conn->ap2_deferred_flush_requests[f].inUse = 0;
|
||||
debug(2, "immediate flush was %s.", ap2_immediate_flush_requested == 0 ? "off" : "on");
|
||||
} else if (conn->ap2_deferred_flush_requests[f].active != 0) {
|
||||
new_audio_block_needed = 1;
|
||||
debug(3,
|
||||
"deferred flush of block: %u. flushFromTS: %12u, flushFromSeq: %12u, "
|
||||
"flushUntilTS: %12u, flushUntilSeq: %12u, timestamp: %12u.",
|
||||
seq_no, conn->ap2_deferred_flush_requests[f].flushFromTS,
|
||||
conn->ap2_deferred_flush_requests[f].flushFromSeq,
|
||||
conn->ap2_deferred_flush_requests[f].flushUntilTS,
|
||||
conn->ap2_deferred_flush_requests[f].flushUntilSeq, timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
pthread_cleanup_pop(1); // the mutex
|
||||
|
||||
// now, if the block is not invalidated by the flush code, see if we need
|
||||
// to decode it and pass it to the player
|
||||
if (new_audio_block_needed == 0) {
|
||||
// is there space in the player thread's buffer system?
|
||||
size_t player_buffer_occupancy = get_audio_buffer_occupancy(conn);
|
||||
// debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size,
|
||||
// player_buffer_occupancy);
|
||||
|
||||
// If we are playing and there is room in the player buffer, go ahead and decode the block
|
||||
// and send it to the player. Otherwise, keep the block and sleep for a while.
|
||||
if ((play_enabled != 0) &&
|
||||
(((1.0 * player_buffer_occupancy * conn->frames_per_packet) / conn->input_rate) <=
|
||||
config.audio_decoded_buffer_desired_length)) {
|
||||
uint64_t buffer_should_be_time;
|
||||
frame_to_local_time(timestamp, &buffer_should_be_time, conn);
|
||||
|
||||
// try to identify blocks that are timed to before the last buffer, and drop 'em
|
||||
int64_t time_from_last_buffer_time =
|
||||
buffer_should_be_time - previous_buffer_should_be_time;
|
||||
|
||||
if ((packets_played_in_this_sequence == 0) || (time_from_last_buffer_time > 0)) {
|
||||
int64_t lead_time = buffer_should_be_time - get_absolute_time_in_ns();
|
||||
payload_length = 0;
|
||||
if (ssrc_is_recognised(payload_ssrc) != 0) {
|
||||
// prepare_decoding_chain(conn, payload_ssrc);
|
||||
unsigned long long new_payload_length = 0;
|
||||
payload_pointer = m + leading_free_space_length;
|
||||
if ((lead_time < (int64_t)30000000000L) &&
|
||||
(lead_time >= 0)) { // only decipher the packet if it's not too late or too early
|
||||
int response = -1; // guess that there is a problem
|
||||
if (conn->session_key != NULL) {
|
||||
unsigned char nonce[12];
|
||||
memset(nonce, 0, sizeof(nonce));
|
||||
memcpy(
|
||||
nonce + 4, packet + nread - 8,
|
||||
8); // front-pad the 8-byte nonce received to get the 12-byte nonce expected
|
||||
|
||||
// https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/ietf_chacha20-poly1305_construction
|
||||
// Note: the eight-byte nonce must be front-padded out to 12 bytes.
|
||||
|
||||
// Leave leading_free_space_length bytes at the start for possible headers like an
|
||||
// ADTS header (7 bytes)
|
||||
memset(m, 0, leading_free_space_length);
|
||||
response = crypto_aead_chacha20poly1305_ietf_decrypt(
|
||||
payload_pointer, // where the decrypted payload will start
|
||||
&new_payload_length, // mlen_p
|
||||
NULL, // nsec,
|
||||
packet +
|
||||
12, // the ciphertext starts 12 bytes in and is followed by the MAC tag,
|
||||
nread - (8 + 12), // clen -- the last 8 bytes are the nonce
|
||||
packet + 4, // authenticated additional data
|
||||
8, // authenticated additional data length
|
||||
nonce,
|
||||
conn->session_key); // *k
|
||||
if (response != 0)
|
||||
debug(1, "Error decrypting audio packet %u -- packet length %zd.", seq_no,
|
||||
nread);
|
||||
} else {
|
||||
debug(2, "No session key, so the audio packet can not be deciphered -- skipped.");
|
||||
}
|
||||
|
||||
if ((response == 0) && (new_payload_length > 0)) {
|
||||
// now we have the deciphered block, so send it to the player if we can
|
||||
payload_length = new_payload_length;
|
||||
|
||||
if (ssrc_is_aac(payload_ssrc)) {
|
||||
payload_pointer =
|
||||
payload_pointer - 7; // including the 7-byte leader for the ADTS
|
||||
payload_length = payload_length + 7;
|
||||
|
||||
// now, fill in the 7-byte ADTS information, which seems to be needed by the
|
||||
// decoder we made room for it in the front of the buffer by filling from m + 7.
|
||||
int channelConfiguration = 2; // 2: 2 channels: front-left, front-right
|
||||
if (payload_ssrc == AAC_48000_F24_5P1)
|
||||
channelConfiguration = 6; // 6: 6 channels: front-center, front-left,
|
||||
// front-right, back-left, back-right, LFE-channel
|
||||
else if (payload_ssrc == AAC_48000_F24_7P1)
|
||||
channelConfiguration =
|
||||
7; // 7: 8 channels: front-center, front-left, front-right,
|
||||
// side-left, side-right, back-left, back-right, LFE-channel
|
||||
addADTStoPacket(payload_pointer, payload_length, conn->input_rate,
|
||||
channelConfiguration);
|
||||
}
|
||||
int mute =
|
||||
((packets_played_in_this_sequence == 0) && (ssrc_is_aac(payload_ssrc)));
|
||||
if (mute) {
|
||||
debug(2, "Connection %d: muting first AAC block -- block %u -- timestamp %u.",
|
||||
conn->connection_number, seq_no, timestamp);
|
||||
}
|
||||
int32_t timestamp_difference = 0;
|
||||
if (packets_played_in_this_sequence == 0) {
|
||||
// first_block_in_this_sequence = seq_no;
|
||||
first_timestamp_in_this_sequence = timestamp;
|
||||
debug(2,
|
||||
"Connection %d: "
|
||||
"first block %u, first timestamp %u.",
|
||||
conn->connection_number, seq_no, timestamp);
|
||||
} else {
|
||||
timestamp_difference = timestamp - expected_timestamp;
|
||||
if (timestamp_difference != 0) {
|
||||
debug(2,
|
||||
"Connection %d: "
|
||||
"unexpected timestamp in block %u. Actual: %u, expected: %u "
|
||||
"difference: %d, "
|
||||
"%f ms. "
|
||||
"Positive means later, i.e. a gap. First timestamp was %u, payload "
|
||||
"type: \"%s\".",
|
||||
conn->connection_number, seq_no, timestamp, expected_timestamp,
|
||||
timestamp_difference, 1000.0 * timestamp_difference / conn->input_rate,
|
||||
first_timestamp_in_this_sequence, get_ssrc_name(payload_ssrc));
|
||||
// mute the first packet after a discontinuity
|
||||
if (ssrc_is_aac(payload_ssrc)) {
|
||||
debug(2,
|
||||
"Connection %d: muting first AAC block -- block %u -- following a "
|
||||
"timestamp discontinuity, timestamp %u.",
|
||||
conn->connection_number, seq_no, timestamp);
|
||||
mute = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
int skip_this_block = 0;
|
||||
if (timestamp_difference < 0) {
|
||||
|
||||
// uncomment this to work back to replace buffers that have been already decoded
|
||||
// and placed in the player queue with the incoming new buffers this is a bit
|
||||
// trickier, but maybe the new buffers are better than the previous ones they
|
||||
// will replace (?)
|
||||
/*
|
||||
seq_t revised_seqno = get_revised_seqno(conn, timestamp);
|
||||
if (revised_seqno != sequence_number_for_player) {
|
||||
debug(1, "revised seqno calculated: conn->ab_read: %u, revised_seqno: %u,
|
||||
conn->ab_write: %u.", conn->ab_read, revised_seqno, conn->ab_write);
|
||||
clear_buffers_from(conn, revised_seqno);
|
||||
sequence_number_for_player = revised_seqno;
|
||||
timestamp_difference = 0;
|
||||
}
|
||||
*/
|
||||
|
||||
// uncomment this to drop incoming new buffers that are too old and for whose
|
||||
// timings buffers have already been decoded and placed in the player queue this
|
||||
// is easier, but maybe the new late buffers are better than the previous ones
|
||||
// (?)
|
||||
|
||||
int32_t abs_timestamp_difference = -timestamp_difference;
|
||||
if ((size_t)abs_timestamp_difference > get_ssrc_block_length(payload_ssrc)) {
|
||||
skip_this_block = 1;
|
||||
debug(2,
|
||||
"skipping block %u because it is too old. Timestamp "
|
||||
"difference: %d, length of block: %zu.",
|
||||
seq_no, timestamp_difference, get_ssrc_block_length(payload_ssrc));
|
||||
}
|
||||
}
|
||||
if (skip_this_block == 0) {
|
||||
uint32_t packet_size = player_put_packet(
|
||||
payload_ssrc, sequence_number_for_player, timestamp, payload_pointer,
|
||||
payload_length, mute, timestamp_difference, conn);
|
||||
debug(3, "block %u, timestamp %u, length %u sent to the player.", seq_no,
|
||||
timestamp, packet_size);
|
||||
sequence_number_for_player++; // simply increment
|
||||
expected_timestamp = timestamp + packet_size; // for the next time
|
||||
packets_played_in_this_sequence++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug(3,
|
||||
"skipped deciphering block %u with timestamp %u because its lead time is "
|
||||
"out of range at %f "
|
||||
"seconds.",
|
||||
seq_no, timestamp, lead_time * 1.0E-9);
|
||||
uint32_t currentAnchorRTP = 0;
|
||||
uint64_t currentAnchorLocalTime = 0;
|
||||
if (get_ptp_anchor_local_time_info(conn, ¤tAnchorRTP,
|
||||
¤tAnchorLocalTime) == clock_ok) {
|
||||
debug(3, "anchorRTP: %u, anchorLocalTime: %" PRIu64 ".", currentAnchorRTP,
|
||||
currentAnchorLocalTime);
|
||||
} else {
|
||||
debug(3, "Clock not okay");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug(2, "Unrecognised or invalid ssrc: %s.", get_ssrc_name(payload_ssrc));
|
||||
}
|
||||
} else {
|
||||
debug(1, "dropping buffer that should have played before the last actually played.");
|
||||
}
|
||||
new_audio_block_needed = 1; // the block has been used up and is no longer current
|
||||
} else {
|
||||
usleep(20000); // wait for a while
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (finished == 0);
|
||||
// debug(1, "Connection %d: rtp_buffered_audio_processor PID %d exiting", conn->connection_number,
|
||||
// syscall(SYS_gettid));
|
||||
pthread_cleanup_pop(1); // buffered_tcp_reader thread creation
|
||||
pthread_cleanup_pop(1); // buffer malloc
|
||||
pthread_cleanup_pop(1); // not_full_cv
|
||||
pthread_cleanup_pop(1); // not_empty_cv
|
||||
pthread_cleanup_pop(1); // mutex
|
||||
pthread_cleanup_pop(1); // descriptor malloc
|
||||
pthread_cleanup_pop(1); // pthread_t malloc
|
||||
pthread_cleanup_pop(1); // do the cleanup.
|
||||
// debug(1, "Connection %d: rtp_buffered_audio_processor PID %d finish", conn->connection_number,
|
||||
// syscall(SYS_gettid));
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef _AP2_BUFFERED_AUDIO_PROCESSOR_H
|
||||
#define _AP2_BUFFERED_AUDIO_PROCESSOR_H
|
||||
|
||||
void *rtp_buffered_audio_processor(void *arg);
|
||||
|
||||
#endif // _AP2_BUFFERED_AUDIO_PROCESSOR_H
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Apple AirPlay 2 Event Receiver. This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2014--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "ap2_event_receiver.h"
|
||||
#include "bonjour_strings.h"
|
||||
#include "common.h"
|
||||
#include "player.h"
|
||||
#include "ptp-utilities.h"
|
||||
#include "rtsp.h"
|
||||
#include "utilities/network_utilities.h"
|
||||
#include "utilities/structured_buffer.h"
|
||||
|
||||
void ap2_event_receiver_cleanup_handler(void *arg) {
|
||||
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
|
||||
// debug(1, "Connection %d: AP2 Event Receiver Cleanup start.", conn->connection_number);
|
||||
// only update these things if you're (still) the principal conn
|
||||
|
||||
#ifdef CONFIG_METADATA
|
||||
// this is here to ensure it's only performed once during a teardown of a ptp stream
|
||||
send_ssnc_metadata('disc', conn->client_ip_string, strlen(conn->client_ip_string), 1);
|
||||
#endif
|
||||
|
||||
if (conn->airplay_gid != NULL) {
|
||||
free(conn->airplay_gid);
|
||||
conn->airplay_gid = NULL;
|
||||
}
|
||||
conn->groupContainsGroupLeader = 0;
|
||||
if (conn->dacp_active_remote != NULL) {
|
||||
free(conn->dacp_active_remote);
|
||||
conn->dacp_active_remote = NULL;
|
||||
}
|
||||
if (conn->ap2_client_name) {
|
||||
free(conn->ap2_client_name);
|
||||
conn->ap2_client_name = NULL;
|
||||
}
|
||||
/*
|
||||
pthread_rwlock_wrlock(&principal_conn_lock); // don't let the principal_conn be changed
|
||||
pthread_cleanup_push(rwlock_unlock, (void *)&principal_conn_lock);
|
||||
if (principal_conn)
|
||||
debug(1, "principal_conn: %d.", principal_conn->connection_number);
|
||||
else
|
||||
debug(1, "principal_conn: is NULL.");
|
||||
if (principal_conn == conn) {
|
||||
config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay
|
||||
build_bonjour_strings(conn);
|
||||
debug(1, "Connection %d: SETUP mdns_update on %s.", conn->connection_number,
|
||||
get_category_string(conn->airplay_stream_category));
|
||||
mdns_update(NULL, secondary_txt_records);
|
||||
principal_conn = NULL;
|
||||
}
|
||||
pthread_cleanup_pop(1); // release the principal_conn lock
|
||||
*/
|
||||
debug(2, "Connection %d: AP2 Event Receiver Cleanup is complete.", conn->connection_number);
|
||||
}
|
||||
|
||||
void *ap2_event_receiver(void *arg) {
|
||||
// #include <syscall.h>
|
||||
// debug(1, "rtp_event_receiver PID %d", syscall(SYS_gettid));
|
||||
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
|
||||
debug(2, "Connection %d: AP2 Event Receiver started", conn->connection_number);
|
||||
structured_buffer *sbuf = sbuf_new(4096);
|
||||
if (sbuf != NULL) {
|
||||
pthread_cleanup_push(sbuf_cleanup, sbuf);
|
||||
|
||||
/*
|
||||
// only update these things if you're (still) the principal conn
|
||||
pthread_rwlock_wrlock(&principal_conn_lock); // don't let the principal_conn be changed
|
||||
pthread_cleanup_push(rwlock_unlock, (void *)&principal_conn_lock);
|
||||
if (principal_conn == conn) {
|
||||
config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay
|
||||
// config.airplay_statusflags |= 1 << 17; // ReceiverSessionIsActive
|
||||
build_bonjour_strings(conn);
|
||||
debug(2, "Connection %d: SETUP mdns_update on %s.", conn->connection_number,
|
||||
get_category_string(conn->airplay_stream_category));
|
||||
mdns_update(NULL, secondary_txt_records);
|
||||
}
|
||||
pthread_cleanup_pop(1); // release the principal_conn lock
|
||||
*/
|
||||
pthread_cleanup_push(ap2_event_receiver_cleanup_handler, arg);
|
||||
|
||||
// listen(conn->event_socket, 5); // this is now done in the handle_setup_2 code
|
||||
|
||||
uint8_t packet[4096];
|
||||
ssize_t nread;
|
||||
SOCKADDR remote_addr;
|
||||
memset(&remote_addr, 0, sizeof(remote_addr));
|
||||
socklen_t addr_size = sizeof(remote_addr);
|
||||
|
||||
int fd = eintr_checked_accept(conn->event_socket, (struct sockaddr *)&remote_addr, &addr_size);
|
||||
debug(2,
|
||||
"Connection %d: ap2_event_receiver accepted a connection on socket %d and moved to a new "
|
||||
"socket %d.",
|
||||
conn->connection_number, conn->event_socket, fd);
|
||||
intptr_t pfd = fd;
|
||||
pthread_cleanup_push(socket_cleanup, (void *)pfd);
|
||||
int finished = 0;
|
||||
do {
|
||||
|
||||
plist_t value_plist = generateInfoPlist(conn);
|
||||
if (value_plist != NULL) {
|
||||
void *txtData = NULL;
|
||||
size_t txtDataLength = 0;
|
||||
generateTxtDataValueInfo(conn, &txtData, &txtDataLength);
|
||||
plist_dict_set_item(value_plist, "txtAirPlay", plist_new_data(txtData, txtDataLength));
|
||||
free(txtData);
|
||||
plist_t update_info_plist = plist_new_dict();
|
||||
if (update_info_plist != NULL) {
|
||||
plist_dict_set_item(update_info_plist, "type", plist_new_string("updateInfo"));
|
||||
plist_dict_set_item(update_info_plist, "value", value_plist);
|
||||
char *plistString = NULL;
|
||||
uint32_t plistStringLength = 0;
|
||||
plist_to_bin(update_info_plist, &plistString, &plistStringLength);
|
||||
if (plistString != NULL) {
|
||||
char *plist_as_string = plist_as_xml_text(update_info_plist);
|
||||
if (plist_as_string != NULL) {
|
||||
debug(3, "Plist is: \"%s\".", plist_as_string);
|
||||
free(plist_as_string);
|
||||
}
|
||||
sbuf_printf(sbuf, "POST /command RTSP/1.0\r\nContent-Length: %u\r\n",
|
||||
plistStringLength);
|
||||
sbuf_printf(sbuf, "Content-Type: application/x-apple-binary-plist\r\n\r\n");
|
||||
sbuf_append(sbuf, plistString, plistStringLength);
|
||||
|
||||
free(plistString); // should be plist_to_bin_free, but it's not defined in older
|
||||
// libraries
|
||||
char *b = 0;
|
||||
size_t l = 0;
|
||||
sbuf_buf_and_length(sbuf, &b, &l);
|
||||
ssize_t wres =
|
||||
write_encrypted(fd, &conn->ap2_pairing_context.event_cipher_bundle, b, l);
|
||||
if ((wres == -1) || ((size_t)wres != l))
|
||||
debug(1, "Encrypted write error");
|
||||
|
||||
sbuf_clear(sbuf);
|
||||
} else {
|
||||
debug(1, "plist string not created!");
|
||||
}
|
||||
plist_free(update_info_plist);
|
||||
} else {
|
||||
debug(1, "Could not build an updateInfo plist");
|
||||
}
|
||||
// plist_free(value_plist);
|
||||
} else {
|
||||
debug(1, "Could not build an value plist");
|
||||
}
|
||||
|
||||
while (finished == 0) {
|
||||
nread = read_encrypted(fd, &conn->ap2_pairing_context.event_cipher_bundle, packet,
|
||||
sizeof(packet));
|
||||
|
||||
// nread = recv(fd, packet, sizeof(packet), 0);
|
||||
|
||||
if (nread < 0) {
|
||||
char errorstring[1024];
|
||||
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
|
||||
debug(1,
|
||||
"Connection %d: error in ap2_event_receiver %d: \"%s\". Could not recv a packet.",
|
||||
conn->connection_number, errno, errorstring);
|
||||
// if ((config.diagnostic_drop_packet_fraction == 0.0) ||
|
||||
// (drand48() > config.diagnostic_drop_packet_fraction)) {
|
||||
} else if (nread > 0) {
|
||||
|
||||
// ssize_t plen = nread;
|
||||
packet[nread] = '\0';
|
||||
debug(3, "Connection %d: Packet Received on Event Port with contents: \"%s\".",
|
||||
conn->connection_number, packet);
|
||||
} else {
|
||||
debug(2, "Connection %d: Event Port connection closed by client",
|
||||
conn->connection_number);
|
||||
finished = 1;
|
||||
}
|
||||
}
|
||||
|
||||
} while (finished == 0);
|
||||
|
||||
debug(3, "Connection %d: AP2 Event Receiver RTP thread starting \"normal\" exit.",
|
||||
conn->connection_number);
|
||||
pthread_cleanup_pop(1); // close the socket
|
||||
|
||||
pthread_cleanup_pop(1); // do the cleanup
|
||||
pthread_cleanup_pop(1); // delete the structured buffer
|
||||
debug(2, "Connection %d: AP2 Event Receiver RTP thread \"normal\" exit.",
|
||||
conn->connection_number);
|
||||
} else {
|
||||
debug(1, "Could not allocate a structured buffer!");
|
||||
}
|
||||
conn->ap2_event_receiver_exited = 1;
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef _AP2_EVENT_RECEIVER_H
|
||||
#define _AP2_EVENT_RECEIVER_H
|
||||
|
||||
void *ap2_event_receiver(void *arg);
|
||||
|
||||
#endif // _AP2_EVENT_RECEIVER_H
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Apple AirPlay 2 Remote Control (RC) Event Receiver. This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2014--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "ap2_rc_event_receiver.h"
|
||||
#include "common.h"
|
||||
#include "player.h"
|
||||
#include "rtsp.h"
|
||||
#include "utilities/network_utilities.h"
|
||||
#include "utilities/structured_buffer.h"
|
||||
|
||||
void ap2_rc_event_receiver_cleanup_handler(void *arg) {
|
||||
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
|
||||
debug(2, "Connection %d: AP2 Event Receiver Cleanup.", conn->connection_number);
|
||||
}
|
||||
|
||||
void *ap2_rc_event_receiver(void *arg) {
|
||||
// #include <syscall.h>
|
||||
// debug(1, "rtp_event_receiver PID %d", syscall(SYS_gettid));
|
||||
rtsp_conn_info *conn = (rtsp_conn_info *)arg;
|
||||
if (conn->airplay_stream_category == remote_control_stream)
|
||||
debug(2, "Connection %d (RC): AP2 RC Event Receiver started", conn->connection_number);
|
||||
else
|
||||
debug(2, "Connection %d: AP2 RC Event Receiver started", conn->connection_number);
|
||||
|
||||
structured_buffer *sbuf = sbuf_new(4096);
|
||||
if (sbuf != NULL) {
|
||||
pthread_cleanup_push(sbuf_cleanup, sbuf);
|
||||
|
||||
pthread_cleanup_push(ap2_rc_event_receiver_cleanup_handler, arg);
|
||||
|
||||
// listen(conn->event_socket, 5); // this is now done in the handle_setup_2 code
|
||||
|
||||
uint8_t packet[4096];
|
||||
ssize_t nread;
|
||||
SOCKADDR remote_addr;
|
||||
memset(&remote_addr, 0, sizeof(remote_addr));
|
||||
socklen_t addr_size = sizeof(remote_addr);
|
||||
|
||||
int fd = eintr_checked_accept(conn->event_socket, (struct sockaddr *)&remote_addr, &addr_size);
|
||||
debug(2,
|
||||
"Connection %d: ap2_rc_event_receiver accepted a connection on socket %d and moved to a "
|
||||
"new "
|
||||
"socket %d.",
|
||||
conn->connection_number, conn->event_socket, fd);
|
||||
intptr_t pfd = fd;
|
||||
pthread_cleanup_push(socket_cleanup, (void *)pfd);
|
||||
int finished = 0;
|
||||
do {
|
||||
|
||||
plist_t value_plist = generateInfoPlist(conn);
|
||||
if (value_plist != NULL) {
|
||||
void *txtData = NULL;
|
||||
size_t txtDataLength = 0;
|
||||
generateTxtDataValueInfo(conn, &txtData, &txtDataLength);
|
||||
plist_dict_set_item(value_plist, "txtAirPlay", plist_new_data(txtData, txtDataLength));
|
||||
free(txtData);
|
||||
plist_t update_info_plist = plist_new_dict();
|
||||
if (update_info_plist != NULL) {
|
||||
plist_dict_set_item(update_info_plist, "type", plist_new_string("updateInfo"));
|
||||
plist_dict_set_item(update_info_plist, "value", value_plist);
|
||||
char *plistString = NULL;
|
||||
uint32_t plistStringLength = 0;
|
||||
plist_to_bin(update_info_plist, &plistString, &plistStringLength);
|
||||
if (plistString != NULL) {
|
||||
char *plist_as_string = plist_as_xml_text(update_info_plist);
|
||||
if (plist_as_string != NULL) {
|
||||
debug(3, "Plist is: \"%s\".", plist_as_string);
|
||||
free(plist_as_string);
|
||||
}
|
||||
sbuf_printf(sbuf, "POST /command RTSP/1.0\r\nContent-Length: %u\r\n",
|
||||
plistStringLength);
|
||||
sbuf_printf(sbuf, "Content-Type: application/x-apple-binary-plist\r\n\r\n");
|
||||
sbuf_append(sbuf, plistString, plistStringLength);
|
||||
|
||||
free(plistString); // should be plist_to_bin_free, but it's not defined in older
|
||||
// libraries
|
||||
char *b = 0;
|
||||
size_t l = 0;
|
||||
sbuf_buf_and_length(sbuf, &b, &l);
|
||||
ssize_t wres =
|
||||
write_encrypted(fd, &conn->ap2_pairing_context.event_cipher_bundle, b, l);
|
||||
if ((wres == -1) || ((size_t)wres != l))
|
||||
debug(1, "Encrypted write error");
|
||||
|
||||
sbuf_clear(sbuf);
|
||||
} else {
|
||||
debug(1, "plist string not created!");
|
||||
}
|
||||
plist_free(update_info_plist);
|
||||
} else {
|
||||
debug(1, "Could not build an updateInfo plist");
|
||||
}
|
||||
// plist_free(value_plist);
|
||||
} else {
|
||||
debug(1, "Could not build an value plist");
|
||||
}
|
||||
|
||||
while (finished == 0) {
|
||||
nread = read_encrypted(fd, &conn->ap2_pairing_context.event_cipher_bundle, packet,
|
||||
sizeof(packet));
|
||||
|
||||
// nread = recv(fd, packet, sizeof(packet), 0);
|
||||
|
||||
if (nread < 0) {
|
||||
char errorstring[1024];
|
||||
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
|
||||
debug(
|
||||
1,
|
||||
"Connection %d: error in ap2_rc_event_receiver %d: \"%s\". Could not recv a packet.",
|
||||
conn->connection_number, errno, errorstring);
|
||||
// if ((config.diagnostic_drop_packet_fraction == 0.0) ||
|
||||
// (drand48() > config.diagnostic_drop_packet_fraction)) {
|
||||
} else if (nread > 0) {
|
||||
|
||||
// ssize_t plen = nread;
|
||||
packet[nread] = '\0';
|
||||
debug(2,
|
||||
"Connection %d: ap2_rc_event_receiver Packet Received on Event Port with contents: "
|
||||
"\"%s\".",
|
||||
conn->connection_number, packet);
|
||||
} else {
|
||||
debug(2, "Connection %d: ap2_rc_event_receiver Event Port connection closed by client",
|
||||
conn->connection_number);
|
||||
finished = 1;
|
||||
}
|
||||
}
|
||||
|
||||
} while (finished == 0);
|
||||
pthread_cleanup_pop(1); // close the socket
|
||||
pthread_cleanup_pop(1); // do the cleanup
|
||||
pthread_cleanup_pop(1); // delete the structured buffer
|
||||
debug(2, "Connection %d: AP2 ap2_rc_event_receiver \"normal\" exit.", conn->connection_number);
|
||||
} else {
|
||||
debug(1, "Could not allocate a structured buffer!");
|
||||
}
|
||||
conn->ap2_event_receiver_exited = 1;
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// RC means Remote Control
|
||||
|
||||
#ifndef _AP2_RC_EVENT_RECEIVER_H
|
||||
#define _AP2_RC_EVENT_RECEIVER_H
|
||||
|
||||
void *ap2_rc_event_receiver(void *arg);
|
||||
|
||||
#endif // _AP2_RC_EVENT_RECEIVER_H
|
||||
+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,21 +139,12 @@ 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 general.audio_backend_buffer_desired_length_in_seconds instead.");
|
||||
}
|
||||
|
||||
/* Get the desired buffer size setting in seconds. */
|
||||
/* Get the desired backend buffer size setting in seconds. */
|
||||
/* This is the size of the buffer in the output system, e.g. in the DAC itself in ALSA */
|
||||
if (config_lookup_float(config.cfg, "general.audio_backend_buffer_desired_length_in_seconds",
|
||||
&dvalue)) {
|
||||
if (dvalue < 0) {
|
||||
@@ -164,6 +157,21 @@ void parse_general_audio_options(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Get the desired decoded buffer size setting in seconds. */
|
||||
/* This is the size of the buffer of decoded and deciphered audio held in the player's output
|
||||
* queue prior to sending it to the output system */
|
||||
if (config_lookup_float(config.cfg, "general.audio_decoded_buffer_desired_length_in_seconds",
|
||||
&dvalue)) {
|
||||
if (dvalue < 0) {
|
||||
die("Invalid audio_decoded_buffer_desired_length_in_seconds value: \"%f\". It "
|
||||
"should be 0.0 or greater."
|
||||
" The default is %.3f seconds",
|
||||
dvalue, config.audio_decoded_buffer_desired_length);
|
||||
} else {
|
||||
config.audio_decoded_buffer_desired_length = dvalue;
|
||||
}
|
||||
}
|
||||
|
||||
/* Get the minimum buffer size for fancy interpolation setting in seconds. */
|
||||
if (config_lookup_float(config.cfg,
|
||||
"general.audio_backend_buffer_interpolation_threshold_in_seconds",
|
||||
@@ -181,17 +189,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 +236,813 @@ 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);
|
||||
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 "
|
||||
"character string -- \"%s\" -- has been detected. (Note that numbers must not be "
|
||||
"enclosed in quotes.)",
|
||||
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 {
|
||||
warn("in the \"%s\" setting in the \"%s\" section of the configuration file, an error "
|
||||
"has been detected at argument %d. (Note that numbers must not be enclosed in "
|
||||
"quotes.)",
|
||||
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);
|
||||
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);
|
||||
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
|
||||
|
||||
+1041
-801
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,
|
||||
|
||||
+15
-12
@@ -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
|
||||
@@ -196,14 +196,14 @@ static int jack_init(__attribute__((unused)) int argc, __attribute__((unused)) c
|
||||
// Now the options specific to the backend, from the "jack" stanza:
|
||||
if (config.cfg != NULL) {
|
||||
const char *str;
|
||||
if (config_lookup_string(config.cfg, "jack.client_name", &str)) {
|
||||
if (config_lookup_non_empty_string(config.cfg, "jack.client_name", &str)) {
|
||||
config.jack_client_name = (char *)str;
|
||||
}
|
||||
if (config_lookup_string(config.cfg, "jack.autoconnect_pattern", &str)) {
|
||||
if (config_lookup_non_empty_string(config.cfg, "jack.autoconnect_pattern", &str)) {
|
||||
config.jack_autoconnect_pattern = (char *)str;
|
||||
}
|
||||
#ifdef CONFIG_SOXR
|
||||
if (config_lookup_string(config.cfg, "jack.soxr_resample_quality", &str)) {
|
||||
if (config_lookup_non_empty_string(config.cfg, "jack.soxr_resample_quality", &str)) {
|
||||
debug(1, "SOXR quality %s", str);
|
||||
config.jack_soxr_resample_quality = parse_soxr_quality_name(str);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
+535
-172
@@ -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 %zd 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_non_empty_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_non_empty_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_non_empty_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_non_empty_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;
|
||||
@@ -300,14 +648,17 @@ int pa_delay(long *the_delay) {
|
||||
pa_threaded_mainloop_lock(mainloop);
|
||||
int gl = pa_stream_get_latency(stream, &latency, &negative);
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
if (gl == PA_ERR_NODATA) {
|
||||
// debug(1, "No latency data yet.");
|
||||
if (gl == -PA_ERR_NODATA) {
|
||||
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;
|
||||
}
|
||||
|
||||
+41
-10
@@ -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,14 +106,20 @@ 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;
|
||||
if (config_lookup_string(config.cfg, "pipe.name", &str)) {
|
||||
if (config_lookup_non_empty_string(config.cfg, "pipe.name", &str)) {
|
||||
pipename = (char *)str;
|
||||
} else {
|
||||
die("pipename needed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,11 +155,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_non_empty_string(config.cfg, "pipewire.application_name", &str)) {
|
||||
config.pw_application_name = (char *)str;
|
||||
}
|
||||
|
||||
|
||||
// Get the optional PipeWire node name, if provided.
|
||||
if (config_lookup_non_empty_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_non_empty_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 %zd 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) && (on_process_is_running != 0)) {
|
||||
debug(3, "stream not active but on_process_is_running is true.");
|
||||
}
|
||||
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 fo = 0;
|
||||
while ((s < SIO_NCHAN) && (fo == 0)) {
|
||||
if (cap.pchan[s] == c)
|
||||
fo = 1;
|
||||
else
|
||||
s++;
|
||||
}
|
||||
if (fo == 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 fo = 0;
|
||||
while ((s < SIO_NRATE) && (fo == 0)) {
|
||||
if (cap.rate[s] == sps_rate_actual_rate(r))
|
||||
fo = 1;
|
||||
else
|
||||
s++;
|
||||
}
|
||||
if (fo == 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_non_empty_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,
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Bonjour strings manager. This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2014--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "bonjour_strings.h"
|
||||
#include "common.h"
|
||||
|
||||
char *txt_records[128];
|
||||
char *secondary_txt_records[128];
|
||||
|
||||
// mDNS advertisement strings
|
||||
|
||||
// Create these strings and then keep them updated.
|
||||
// When necessary, update the mDNS service records, using e.g. Avahi
|
||||
// from these sources.
|
||||
|
||||
char fwString[128];
|
||||
char ap1_featuresString[128];
|
||||
char ap1StatusFlagsString[128];
|
||||
char ap1ModelString[128];
|
||||
char ap1SrcversString[128];
|
||||
char pkString[128];
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
char deviceIdString[128];
|
||||
char featuresString[128];
|
||||
char statusflagsString[128];
|
||||
char piString[128];
|
||||
char gidString[128];
|
||||
char psiString[128];
|
||||
char fexString[128];
|
||||
char modelString[128];
|
||||
char srcversString[128];
|
||||
char osversString[128];
|
||||
char ap1OsversString[128];
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
void build_bonjour_strings(rtsp_conn_info *conn) {
|
||||
#else
|
||||
void build_bonjour_strings(__attribute((unused)) rtsp_conn_info *conn) {
|
||||
#endif
|
||||
|
||||
// Watch out here, the strings that form each entry
|
||||
// need to be permanent, because we don't know
|
||||
// when avahi will look at them.
|
||||
// bnprintf is (should be) the same as snprintf except that it returns a pointer to the resulting
|
||||
// character string. so this rather odd arranement below allows you to use a snprintf for
|
||||
// convenience but get the character string as a result, both as a store for the item so that
|
||||
// Avahi can see it in future and as a pointer
|
||||
int entry_number = 0;
|
||||
|
||||
// the txt_records entries are for the _raop._tcp characteristics
|
||||
// the secondary_txt_records are for the _airplay._tcp items.
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
txt_records[entry_number++] = "cn=0,1";
|
||||
txt_records[entry_number++] = "da=true";
|
||||
txt_records[entry_number++] = "et=0,1";
|
||||
|
||||
uint64_t features_hi = config.airplay_features;
|
||||
features_hi = (features_hi >> 32) & 0xffffffff;
|
||||
uint64_t features_lo = config.airplay_features;
|
||||
features_lo = features_lo & 0xffffffff;
|
||||
|
||||
txt_records[entry_number++] =
|
||||
bnprintf(ap1_featuresString, sizeof(ap1_featuresString), "ft=0x%" PRIX64 ",0x%" PRIX64 "",
|
||||
features_lo, features_hi);
|
||||
|
||||
txt_records[entry_number++] =
|
||||
bnprintf(fwString, sizeof(fwString), "fv=%s", config.firmware_version);
|
||||
txt_records[entry_number++] = bnprintf(ap1StatusFlagsString, sizeof(ap1StatusFlagsString),
|
||||
"sf=0x%" PRIX32, config.airplay_statusflags);
|
||||
#ifdef CONFIG_METADATA
|
||||
if (config.get_coverart == 0)
|
||||
txt_records[entry_number++] = "md=0,2";
|
||||
else
|
||||
txt_records[entry_number++] = "md=0,1,2";
|
||||
#endif
|
||||
txt_records[entry_number++] =
|
||||
bnprintf(ap1ModelString, sizeof(ap1ModelString), "am=%s", config.model);
|
||||
txt_records[entry_number++] = bnprintf(pkString, sizeof(pkString), "pk=%s", config.pk_string);
|
||||
txt_records[entry_number++] = "tp=UDP";
|
||||
txt_records[entry_number++] = "vn=65537";
|
||||
txt_records[entry_number++] =
|
||||
bnprintf(ap1SrcversString, sizeof(ap1SrcversString), "vs=%s", config.srcvers);
|
||||
txt_records[entry_number++] =
|
||||
bnprintf(ap1OsversString, sizeof(ap1OsversString), "ov=%s", config.osvers);
|
||||
txt_records[entry_number++] = NULL;
|
||||
|
||||
#else
|
||||
// here, just replicate what happens in mdns.h when using those #defines
|
||||
txt_records[entry_number++] =
|
||||
bnprintf(ap1StatusFlagsString, sizeof(ap1StatusFlagsString), "sf=0x4");
|
||||
txt_records[entry_number++] =
|
||||
bnprintf(fwString, sizeof(fwString), "fv=%s", config.firmware_version);
|
||||
txt_records[entry_number++] =
|
||||
bnprintf(ap1ModelString, sizeof(ap1ModelString), "am=%s", config.model);
|
||||
txt_records[entry_number++] = bnprintf(ap1SrcversString, sizeof(ap1SrcversString), "vs=105.1");
|
||||
txt_records[entry_number++] = "tp=TCP,UDP";
|
||||
txt_records[entry_number++] = "vn=65537";
|
||||
#ifdef CONFIG_METADATA
|
||||
if (config.get_coverart == 0)
|
||||
txt_records[entry_number++] = "md=0,2";
|
||||
else
|
||||
txt_records[entry_number++] = "md=0,1,2";
|
||||
#endif
|
||||
txt_records[entry_number++] = "ss=16";
|
||||
txt_records[entry_number++] = "sr=44100";
|
||||
txt_records[entry_number++] = "da=true";
|
||||
txt_records[entry_number++] = "sv=false";
|
||||
txt_records[entry_number++] = "et=0,1";
|
||||
txt_records[entry_number++] = "ek=1";
|
||||
txt_records[entry_number++] = "cn=0,1";
|
||||
txt_records[entry_number++] = "ch=2";
|
||||
txt_records[entry_number++] = "txtvers=1";
|
||||
if (config.password == 0)
|
||||
txt_records[entry_number++] = "pw=false";
|
||||
else
|
||||
txt_records[entry_number++] = "pw=true";
|
||||
txt_records[entry_number++] = NULL;
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
// make up a secondary set of text records
|
||||
entry_number = 0;
|
||||
|
||||
secondary_txt_records[entry_number++] = "acl=0";
|
||||
secondary_txt_records[entry_number++] = "btaddr=00:00:00:00:00:00";
|
||||
secondary_txt_records[entry_number++] =
|
||||
bnprintf(deviceIdString, sizeof(deviceIdString), "deviceid=%s", config.airplay_device_id);
|
||||
secondary_txt_records[entry_number++] =
|
||||
bnprintf(fexString, sizeof(fexString), "fex=%s", config.airplay_fex);
|
||||
secondary_txt_records[entry_number++] =
|
||||
bnprintf(featuresString, sizeof(featuresString), "features=0x%" PRIX64 ",0x%" PRIX64 "",
|
||||
features_lo, features_hi); // features_hi and features_lo already calculated.
|
||||
secondary_txt_records[entry_number++] = bnprintf(statusflagsString, sizeof(statusflagsString),
|
||||
"flags=0x%" PRIX32, config.airplay_statusflags);
|
||||
if ((conn != NULL) && (conn->airplay_gid != 0)) {
|
||||
snprintf(gidString, sizeof(gidString), "gid=%s", conn->airplay_gid);
|
||||
} else {
|
||||
snprintf(gidString, sizeof(gidString), "gid=%s", config.airplay_pi);
|
||||
}
|
||||
secondary_txt_records[entry_number++] = gidString;
|
||||
|
||||
if ((conn != NULL) && (conn->groupContainsGroupLeader != 0)) {
|
||||
secondary_txt_records[entry_number++] = "igl=0";
|
||||
secondary_txt_records[entry_number++] = "gcgl=1";
|
||||
} else {
|
||||
secondary_txt_records[entry_number++] = "igl=0";
|
||||
secondary_txt_records[entry_number++] = "gcgl=0";
|
||||
}
|
||||
// if ((conn != NULL) && (conn->airplay_gid != 0)) // if it's in a group
|
||||
// secondary_txt_records[entry_number++] = "isGroupLeader=0";
|
||||
secondary_txt_records[entry_number++] =
|
||||
bnprintf(modelString, sizeof(modelString), "model=%s", config.model);
|
||||
secondary_txt_records[entry_number++] = "protovers=1.1";
|
||||
secondary_txt_records[entry_number++] =
|
||||
bnprintf(piString, sizeof(piString), "pi=%s", config.airplay_pi);
|
||||
secondary_txt_records[entry_number++] =
|
||||
bnprintf(psiString, sizeof(psiString), "psi=%s", config.airplay_psi);
|
||||
secondary_txt_records[entry_number++] = pkString; // already calculated
|
||||
secondary_txt_records[entry_number++] =
|
||||
bnprintf(srcversString, sizeof(srcversString), "srcvers=%s", config.srcvers);
|
||||
secondary_txt_records[entry_number++] =
|
||||
bnprintf(osversString, sizeof(osversString), "osvers=%s", config.osvers);
|
||||
secondary_txt_records[entry_number++] = "vv=2";
|
||||
secondary_txt_records[entry_number++] = fwString; // already calculated
|
||||
secondary_txt_records[entry_number++] = NULL;
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef _BONJOUR_STRINGS_H
|
||||
#define _BONJOUR_STRINGS_H
|
||||
|
||||
#include "player.h"
|
||||
|
||||
extern char *txt_records[128];
|
||||
extern char *secondary_txt_records[128];
|
||||
|
||||
#ifdef CONFIG_AIRPLAY_2
|
||||
void build_bonjour_strings(rtsp_conn_info *conn);
|
||||
#else
|
||||
void build_bonjour_strings(__attribute((unused)) rtsp_conn_info *conn);
|
||||
#endif
|
||||
|
||||
#endif // _BONJOUR_STRINGS_H
|
||||
@@ -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"
|
||||
@@ -27,18 +23,28 @@ extern "C" {
|
||||
#define SAFAMILY sa_family
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_CONVOLUTION)
|
||||
typedef enum { ev_unchecked, ev_okay, ev_invalid } ir_file_evaluation;
|
||||
|
||||
typedef struct {
|
||||
unsigned int samplerate; // initialized to 0, will be filter frame rate
|
||||
unsigned int channels;
|
||||
char *filename; // the parsed filename
|
||||
} ir_file_info_t;
|
||||
#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
|
||||
} dbus_session_type;
|
||||
DBT_default = 0,
|
||||
DBT_system, // use the system bus
|
||||
DBT_session, // use the session bus
|
||||
} dbus_message_bus_t;
|
||||
#endif
|
||||
|
||||
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 +64,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 +86,7 @@ typedef enum {
|
||||
typedef enum {
|
||||
decoder_hammerton = 0,
|
||||
decoder_apple_alac,
|
||||
decoder_ffmpeg_alac,
|
||||
} decoders_supported_type;
|
||||
|
||||
typedef enum {
|
||||
@@ -90,27 +98,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 +186,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
|
||||
@@ -174,6 +230,7 @@ typedef struct {
|
||||
int mqtt_publish_raw;
|
||||
int mqtt_publish_parsed;
|
||||
int mqtt_publish_cover;
|
||||
int mqtt_publish_retain;
|
||||
int mqtt_enable_remote;
|
||||
int mqtt_enable_autodiscovery;
|
||||
char *mqtt_autodiscovery_prefix;
|
||||
@@ -187,12 +244,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 +287,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;
|
||||
@@ -248,6 +302,8 @@ typedef struct {
|
||||
double audio_backend_buffer_interpolation_threshold_in_seconds; // below this, soxr interpolation
|
||||
// will not occur -- it'll be
|
||||
// basic interpolation instead.
|
||||
double audio_decoded_buffer_desired_length; // the length of the buffer of fully decoded audio
|
||||
// prior to being sent to the output device
|
||||
double disable_standby_mode_silence_threshold; // below this, silence will be added to the output
|
||||
// buffer
|
||||
double disable_standby_mode_silence_scan_interval; // check the threshold this often
|
||||
@@ -268,21 +324,33 @@ 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;
|
||||
int convolver_valid;
|
||||
char *convolution_ir_file;
|
||||
int convolution_enabled;
|
||||
unsigned int convolution_rate; // 0 means the convolver has never been initialised, so ignore
|
||||
// convolver_valid.
|
||||
// but if this is the same as the current rate and convolver_valid is false, it means that an
|
||||
// attempt to initialise the convolver has failed.
|
||||
size_t convolution_block_size;
|
||||
unsigned int convolution_ir_file_count;
|
||||
ir_file_info_t *convolution_ir_files; // NULL or an array of information about all the impulse
|
||||
// response files loaded
|
||||
int convolution_ir_files_updated; // set to true if the convolution_ir_files are changed. Cleared
|
||||
// when the convolver has been initialised
|
||||
int convolver_valid; // set to true if the convolver can be initialised
|
||||
unsigned int convolution_threads; // number of threads in the convolver thread pool
|
||||
float convolution_gain;
|
||||
int convolution_max_length;
|
||||
double convolution_max_length_in_seconds;
|
||||
#endif
|
||||
|
||||
int loudness;
|
||||
int loudness_enabled;
|
||||
float loudness_reference_volume_db;
|
||||
int alsa_use_hardware_mute;
|
||||
double alsa_maximum_stall_time;
|
||||
@@ -290,13 +358,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 +385,53 @@ 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.
|
||||
dbus_message_bus_t dbus_default_message_bus;
|
||||
|
||||
#if defined(CONFIG_DBUS_INTERFACE)
|
||||
dbus_message_bus_t dbus_service_bus_type;
|
||||
#endif
|
||||
#if defined(CONFIG_MPRIS_INTERFACE)
|
||||
dbus_message_bus_t 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 +491,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);
|
||||
@@ -425,7 +548,7 @@ uint64_t get_absolute_time_in_ns(void); // monotonic_raw or monotonic
|
||||
uint64_t get_monotonic_time_in_ns(void); // NTP-disciplined
|
||||
|
||||
// time at startup for debugging timing
|
||||
extern uint64_t ns_time_at_startup, ns_time_at_last_debug_message;
|
||||
// extern uint64_t ns_time_at_startup, ns_time_at_last_debug_message;
|
||||
|
||||
// this is for reading an unsigned 32 bit number, such as an RTP timestamp
|
||||
|
||||
@@ -435,7 +558,16 @@ 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_lookup_non_empty_string(const config_t *cfg, const char *path, const char **value);
|
||||
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 +576,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 +615,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 +632,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 +664,28 @@ 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_CONVOLUTION
|
||||
|
||||
/* Parse comma-separated filenames with optional quotes from the input string
|
||||
* Returns array of ir_file_info_t structs (caller must free both array and filenames)
|
||||
* count is set to number of filenames found
|
||||
* Returns NULL on error
|
||||
*/
|
||||
ir_file_info_t *parse_ir_filenames(const char *input, unsigned int *file_count);
|
||||
// Access: files[i].filename, files[i].rate, files[i].evaluation
|
||||
|
||||
/* Do a quick sanity check on the files -- see if they can be opened as sound files */
|
||||
void sanity_check_ir_files(const int option_print_level, ir_file_info_t *files, unsigned int count);
|
||||
|
||||
/* Free the array returned by parse_filenames */
|
||||
void free_ir_filenames(ir_file_info_t *files, unsigned int file_count);
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_USE_GIT_VERSION_STRING
|
||||
extern char git_version_string[];
|
||||
#endif
|
||||
|
||||
+99
-54
@@ -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], [4265913+mikebrady@users.noreply.github.com])
|
||||
AC_INIT([shairport-sync], [5.0], [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
|
||||
@@ -233,9 +228,8 @@ if test "x$with_avahi" = "xyes" ; then
|
||||
AC_DEFINE([CONFIG_AVAHI], 1, [Include Avahi-based mDNS support.])
|
||||
AC_CHECK_LIB([avahi-client], [avahi_client_new], , AC_MSG_ERROR(Avahi support requires the avahi-client library!))
|
||||
AC_CHECK_LIB([avahi-common],[avahi_strerror], , AC_MSG_ERROR(Avahi support requires the avahi-common library!))
|
||||
AC_CONFIG_FILES([scripts/shairport-sync.service-avahi])
|
||||
else
|
||||
AC_CONFIG_FILES([scripts/shairport-sync.service])
|
||||
systemd_after_args="${systemd_after_args} avahi-daemon.service"
|
||||
systemd_requires_args="${systemd_requires_args} avahi-daemon.service"
|
||||
fi
|
||||
AM_CONDITIONAL([USE_AVAHI], [test "x$with_avahi" = "xyes"])
|
||||
|
||||
@@ -279,7 +273,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 +301,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])])
|
||||
@@ -348,6 +342,7 @@ if test "x$with_convolution" = "xyes" ; then
|
||||
else
|
||||
AC_CHECK_LIB([sndfile], [sf_open], , AC_MSG_ERROR(Convolution support requires the sndfile library -- libsndfile1-dev suggested!))
|
||||
fi
|
||||
|
||||
fi
|
||||
AM_CONDITIONAL([USE_CONVOLUTION], [test "x$with_convolution" = "xyes"])
|
||||
|
||||
@@ -394,6 +389,8 @@ AC_ARG_WITH(mqtt-client, [AS_HELP_STRING([--with-mqtt-client],[include a client
|
||||
if test "x$with_mqtt_client" = "xyes" ; then
|
||||
AC_DEFINE([CONFIG_MQTT], 1, [Include a client for MQTT, the Message Queuing Telemetry Transport protocol])
|
||||
AC_CHECK_LIB([mosquitto], [mosquitto_lib_init], , AC_MSG_ERROR(MQTT support requires the mosquitto library -- libmosquitto-dev suggested!))
|
||||
systemd_after_args="${systemd_after_args} mosquitto.service"
|
||||
systemd_requires_args="${systemd_requires_args} mosquitto.service"
|
||||
fi
|
||||
AM_CONDITIONAL([USE_MQTT], [test "x$with_mqtt_client" = "xyes"])
|
||||
|
||||
@@ -418,6 +415,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 +444,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 +495,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 +528,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"],
|
||||
@@ -529,7 +559,22 @@ AC_FUNC_ERROR_AT_LINE
|
||||
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])
|
||||
if test "x${systemd_after_args}" != x ; then
|
||||
systemd_after_args="After=${systemd_after_args}"
|
||||
fi
|
||||
|
||||
if test "x${systemd_requires_args}" != x ; then
|
||||
systemd_requires_args="Requires=${systemd_requires_args}"
|
||||
fi
|
||||
|
||||
AC_SUBST([SYSTEMD_AFTER_ARGS], [${systemd_after_args}])
|
||||
AC_SUBST([SYSTEMD_REQUIRES_ARGS], [${systemd_requires_args}])
|
||||
|
||||
# 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.service])
|
||||
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, (uintptr_t) ptr);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
@@ -255,7 +268,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 +285,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 +304,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 +476,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,7 +504,9 @@ void dacp_monitor_port_update_callback(char *dacp_id, uint16_t port) {
|
||||
}
|
||||
|
||||
void *dacp_monitor_thread_code(__attribute__((unused)) void *na) {
|
||||
int scan_index = 0;
|
||||
// #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];
|
||||
// debug(1, "DACP monitor thread started.");
|
||||
@@ -566,7 +583,7 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) {
|
||||
mdns_dacp_monitor_set_id(dacp_server.dacp_id);
|
||||
}
|
||||
} else {
|
||||
scan_index++;
|
||||
// scan_index++;
|
||||
// debug(1,"DACP Scan Result: %d.", result);
|
||||
|
||||
if ((result == 200) || (result == 400)) {
|
||||
@@ -982,7 +999,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;
|
||||
}
|
||||
|
||||
@@ -1285,7 +1302,7 @@ int dacp_set_volume(int32_t vo) {
|
||||
int32_t active_speakers = 0;
|
||||
for (i = 0; i < speaker_count; i++) {
|
||||
if (speaker_info[i].speaker_number == machine_number) {
|
||||
debug(2, "Our speaker number found: %ld with relative volume.", machine_number,
|
||||
debug(2, "Our speaker number found: %" PRId64 " with relative volume %" PRId32 ".", machine_number,
|
||||
speaker_info[i].volume);
|
||||
}
|
||||
if (speaker_info[i].active == 1) {
|
||||
|
||||
+207
-171
@@ -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
|
||||
@@ -48,13 +48,14 @@
|
||||
|
||||
ShairportSync *shairportSyncSkeleton;
|
||||
|
||||
static GBusType dbus_bus_type = G_BUS_TYPE_SYSTEM; // default is the dbus system message bus
|
||||
int service_is_running = 0;
|
||||
|
||||
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 +127,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 +242,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 +461,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 +474,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 +487,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 +499,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,14 +515,14 @@ 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);
|
||||
if (((debuglev == 0) && (th != 0)) || ((debuglev != 0) && (th == 0)))
|
||||
debug(1, ">> set log verbosity to %d.", th);
|
||||
if (((debug_level() == 0) && (th != 0)) || ((debug_level() != 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);
|
||||
set_debug_level(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);
|
||||
shairport_sync_diagnostics_set_verbosity(skeleton, debug_level());
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
@@ -512,33 +531,58 @@ 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;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_CONVOLUTION
|
||||
gboolean notify_convolution_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
gboolean notify_convolution_enabled_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
// debug(1, "\"notify_convolution_callback\" called.");
|
||||
if (shairport_sync_get_convolution(skeleton)) {
|
||||
debug(1, ">> activating convolution");
|
||||
config.convolution = 1;
|
||||
config.convolver_valid =
|
||||
convolver_init(config.convolution_ir_file, config.convolution_max_length);
|
||||
if (shairport_sync_get_convolution_enabled(skeleton)) {
|
||||
debug(1, ">> activate convolution impulse response filter");
|
||||
config.convolution_enabled = 1;
|
||||
} else {
|
||||
debug(1, ">> deactivating convolution");
|
||||
config.convolution = 0;
|
||||
debug(1, ">> deactivate convolution impulse response filter");
|
||||
config.convolution_enabled = 0;
|
||||
convolver_clear_state();
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
#else
|
||||
gboolean notify_convolution_callback(__attribute__((unused)) ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
gboolean notify_convolution_enabled_callback(__attribute__((unused)) ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
warn(">> Convolution support is not built in to this build of Shairport Sync.");
|
||||
return TRUE;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_CONVOLUTION
|
||||
gboolean notify_convolution_maximum_length_in_seconds_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused))
|
||||
gpointer user_data) {
|
||||
|
||||
gdouble th = shairport_sync_get_convolution_maximum_length_in_seconds(skeleton);
|
||||
if ((th >= 0.0) && (th <= 15.0)) {
|
||||
debug(1, ">> set convolution maximum length in seconds to %f.", th);
|
||||
config.convolution_max_length_in_seconds = th;
|
||||
} else {
|
||||
debug(1, ">> invalid convolution gain: %f. Ignored.", th);
|
||||
shairport_sync_set_convolution_maximum_length_in_seconds(
|
||||
skeleton, config.convolution_max_length_in_seconds);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
#else
|
||||
gboolean notify_convolution_maximum_length_in_seconds_callback(__attribute__((unused))
|
||||
ShairportSync *skeleton,
|
||||
__attribute__((unused))
|
||||
gpointer user_data) {
|
||||
warn(">> Convolution support is not built in to this build of Shairport Sync.");
|
||||
return TRUE;
|
||||
}
|
||||
@@ -549,8 +593,8 @@ gboolean notify_convolution_gain_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
|
||||
gdouble th = shairport_sync_get_convolution_gain(skeleton);
|
||||
if ((th <= 0.0) && (th >= -100.0)) {
|
||||
debug(1, ">> setting convolution gain to %f.", th);
|
||||
if ((th <= 18.0) && (th >= -60.0)) {
|
||||
debug(1, ">> set convolution gain to %f.", th);
|
||||
config.convolution_gain = th;
|
||||
} else {
|
||||
debug(1, ">> invalid convolution gain: %f. Ignored.", th);
|
||||
@@ -566,39 +610,44 @@ 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) {
|
||||
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\".",
|
||||
config.convolution_ir_file);
|
||||
config.convolver_valid =
|
||||
convolver_init(config.convolution_ir_file, config.convolution_max_length);
|
||||
gboolean notify_convolution_impulse_response_files_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused))
|
||||
gpointer user_data) {
|
||||
char *th = (char *)shairport_sync_get_convolution_impulse_response_files(skeleton);
|
||||
if (th != NULL) {
|
||||
debug(1, ">> freeing current configuration impulse response filter files.");
|
||||
free_ir_filenames(config.convolution_ir_files, config.convolution_ir_file_count);
|
||||
config.convolution_ir_files = NULL;
|
||||
config.convolution_ir_file_count = 0;
|
||||
|
||||
config.convolution_ir_files = parse_ir_filenames(th, &config.convolution_ir_file_count);
|
||||
sanity_check_ir_files(1, config.convolution_ir_files, config.convolution_ir_file_count);
|
||||
debug(1, ">> setting %d configuration impulse response filter%s",
|
||||
config.convolution_ir_file_count, config.convolution_ir_file_count == 1 ? "" : "s");
|
||||
config.convolution_ir_files_updated = 1;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
#else
|
||||
gboolean notify_convolution_impulse_response_file_callback(__attribute__((unused))
|
||||
ShairportSync *skeleton,
|
||||
__attribute__((unused))
|
||||
gpointer user_data) {
|
||||
gboolean notify_convolution_impulse_response_files_callback(__attribute__((unused))
|
||||
ShairportSync *skeleton,
|
||||
__attribute__((unused))
|
||||
gpointer user_data) {
|
||||
__attribute__((unused)) char *th =
|
||||
(char *)shairport_sync_get_convolution_impulse_response_file(skeleton);
|
||||
(char *)shairport_sync_get_convolution_impulse_response_files(skeleton);
|
||||
return TRUE;
|
||||
}
|
||||
#endif
|
||||
|
||||
gboolean notify_loudness_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
gboolean notify_loudness_enabled_callback(ShairportSync *skeleton,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
// debug(1, "\"notify_loudness_callback\" called.");
|
||||
if (shairport_sync_get_loudness(skeleton)) {
|
||||
debug(1, ">> activating loudness");
|
||||
config.loudness = 1;
|
||||
if (shairport_sync_get_loudness_enabled(skeleton)) {
|
||||
debug(1, ">> activate loudness filter");
|
||||
config.loudness_enabled = 1;
|
||||
} else {
|
||||
debug(1, ">> deactivating loudness");
|
||||
config.loudness = 0;
|
||||
debug(1, ">> deactivate loudness filter");
|
||||
config.loudness_enabled = 0;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
@@ -607,7 +656,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 +669,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 +682,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 +694,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 +736,54 @@ 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 +791,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 +888,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;
|
||||
}
|
||||
@@ -875,7 +923,7 @@ static gboolean on_handle_remote_command(ShairportSync *skeleton, GDBusMethodInv
|
||||
|
||||
static gboolean on_handle_drop_session(ShairportSync *skeleton, GDBusMethodInvocation *invocation,
|
||||
__attribute__((unused)) gpointer user_data) {
|
||||
get_play_lock(NULL, 1); // stop any current session and don't replace it
|
||||
release_play_lock(NULL); // stop any current session and don't replace it
|
||||
shairport_sync_complete_drop_session(skeleton, invocation);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -893,9 +941,8 @@ 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,
|
||||
(dbus_bus_type == G_BUS_TYPE_SESSION) ? "session" : "system");
|
||||
|
||||
shairportSyncSkeleton = shairport_sync_skeleton_new();
|
||||
g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(shairportSyncSkeleton), connection,
|
||||
@@ -926,14 +973,16 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
|
||||
G_CALLBACK(notify_volume_control_profile_callback), NULL);
|
||||
g_signal_connect(shairportSyncSkeleton, "notify::disable-standby",
|
||||
G_CALLBACK(notify_disable_standby_callback), NULL);
|
||||
g_signal_connect(shairportSyncSkeleton, "notify::convolution",
|
||||
G_CALLBACK(notify_convolution_callback), NULL);
|
||||
g_signal_connect(shairportSyncSkeleton, "notify::convolution-enabled",
|
||||
G_CALLBACK(notify_convolution_enabled_callback), NULL);
|
||||
g_signal_connect(shairportSyncSkeleton, "notify::convolution-gain",
|
||||
G_CALLBACK(notify_convolution_gain_callback), NULL);
|
||||
g_signal_connect(shairportSyncSkeleton, "notify::convolution-impulse-response-file",
|
||||
G_CALLBACK(notify_convolution_impulse_response_file_callback), NULL);
|
||||
g_signal_connect(shairportSyncSkeleton, "notify::loudness", G_CALLBACK(notify_loudness_callback),
|
||||
NULL);
|
||||
g_signal_connect(shairportSyncSkeleton, "notify::convolution-maximum-length-in-seconds",
|
||||
G_CALLBACK(notify_convolution_maximum_length_in_seconds_callback), NULL);
|
||||
g_signal_connect(shairportSyncSkeleton, "notify::convolution-impulse-response-files",
|
||||
G_CALLBACK(notify_convolution_impulse_response_files_callback), NULL);
|
||||
g_signal_connect(shairportSyncSkeleton, "notify::loudness-enabled",
|
||||
G_CALLBACK(notify_loudness_enabled_callback), NULL);
|
||||
g_signal_connect(shairportSyncSkeleton, "notify::loudness-threshold",
|
||||
G_CALLBACK(notify_loudness_threshold_callback), NULL);
|
||||
g_signal_connect(shairportSyncSkeleton, "notify::drift-tolerance",
|
||||
@@ -1012,19 +1061,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 +1092,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)
|
||||
@@ -1082,30 +1119,37 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
|
||||
shairport_sync_set_disable_standby(SHAIRPORT_SYNC(shairportSyncSkeleton), TRUE);
|
||||
}
|
||||
|
||||
if (config.loudness == 0) {
|
||||
shairport_sync_set_loudness(SHAIRPORT_SYNC(shairportSyncSkeleton), FALSE);
|
||||
if (config.loudness_enabled == 0) {
|
||||
debug(1, ">> loudness_enabled is false");
|
||||
shairport_sync_set_loudness_enabled(SHAIRPORT_SYNC(shairportSyncSkeleton), FALSE);
|
||||
} else {
|
||||
shairport_sync_set_loudness(SHAIRPORT_SYNC(shairportSyncSkeleton), TRUE);
|
||||
debug(1, ">> loudness_enabled is true");
|
||||
shairport_sync_set_loudness_enabled(SHAIRPORT_SYNC(shairportSyncSkeleton), TRUE);
|
||||
}
|
||||
|
||||
#ifdef CONFIG_CONVOLUTION
|
||||
if (config.convolution == 0) {
|
||||
shairport_sync_set_convolution(SHAIRPORT_SYNC(shairportSyncSkeleton), FALSE);
|
||||
if (config.convolution_enabled == 0) {
|
||||
debug(1, ">> convolution_enabled is false");
|
||||
shairport_sync_set_convolution_enabled(SHAIRPORT_SYNC(shairportSyncSkeleton), FALSE);
|
||||
} else {
|
||||
shairport_sync_set_convolution(SHAIRPORT_SYNC(shairportSyncSkeleton), TRUE);
|
||||
debug(1, ">> convolution_enabled is true");
|
||||
shairport_sync_set_convolution_enabled(SHAIRPORT_SYNC(shairportSyncSkeleton), TRUE);
|
||||
}
|
||||
if (config.convolution_ir_file)
|
||||
shairport_sync_set_convolution_impulse_response_file(SHAIRPORT_SYNC(shairportSyncSkeleton),
|
||||
config.convolution_ir_file);
|
||||
// else
|
||||
// shairport_sync_set_convolution_impulse_response_file(SHAIRPORT_SYNC(shairportSyncSkeleton),
|
||||
// NULL);
|
||||
|
||||
const char *str = NULL;
|
||||
if ((config.cfg != NULL) &&
|
||||
(config_lookup_non_empty_string(config.cfg, "dsp.convolution_ir_files", &str))) {
|
||||
shairport_sync_set_convolution_impulse_response_files(SHAIRPORT_SYNC(shairportSyncSkeleton),
|
||||
str);
|
||||
} else {
|
||||
shairport_sync_set_convolution_impulse_response_files(SHAIRPORT_SYNC(shairportSyncSkeleton),
|
||||
NULL);
|
||||
}
|
||||
shairport_sync_set_convolution_maximum_length_in_seconds(
|
||||
SHAIRPORT_SYNC(shairportSyncSkeleton), config.convolution_max_length_in_seconds);
|
||||
#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");
|
||||
@@ -1120,7 +1164,7 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
|
||||
free(vs);
|
||||
|
||||
shairport_sync_diagnostics_set_verbosity(
|
||||
SHAIRPORT_SYNC_DIAGNOSTICS(shairportSyncDiagnosticsSkeleton), debuglev);
|
||||
SHAIRPORT_SYNC_DIAGNOSTICS(shairportSyncDiagnosticsSkeleton), debug_level());
|
||||
|
||||
// debug(2,">> log verbosity is %d.",debuglev);
|
||||
|
||||
@@ -1173,51 +1217,43 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name
|
||||
"Not Available");
|
||||
|
||||
debug(1, "Shairport Sync native D-Bus service started at \"%s\" on the %s bus.", name,
|
||||
(config.dbus_service_bus_type == DBT_session) ? "session" : "system");
|
||||
(dbus_bus_type == G_BUS_TYPE_SESSION) ? "session" : "system");
|
||||
service_is_running = 1;
|
||||
}
|
||||
|
||||
static void on_dbus_name_lost_again(__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);
|
||||
warn("could not acquire a Shairport Sync native D-Bus interface \"%s\" on the %s bus.", name,
|
||||
(dbus_bus_type == G_BUS_TYPE_SESSION) ? "session" : "system");
|
||||
ownerID = 0;
|
||||
}
|
||||
|
||||
int start_dbus_service() {
|
||||
// shairportSyncSkeleton = NULL;
|
||||
GBusType dbus_bus_type = G_BUS_TYPE_SYSTEM;
|
||||
if (config.dbus_service_bus_type == DBT_session)
|
||||
|
||||
// set up default message bus
|
||||
if (config.dbus_default_message_bus == 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");
|
||||
|
||||
// look for explicit overrides
|
||||
if (config.dbus_service_bus_type == DBT_system)
|
||||
dbus_bus_type = G_BUS_TYPE_SYSTEM;
|
||||
else 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.",
|
||||
(dbus_bus_type == G_BUS_TYPE_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.");
|
||||
|
||||
+99
-71
@@ -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://github.com/FFmpeg/FFmpeg
|
||||
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,43 @@ 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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
# Remove anything we don't need.
|
||||
# Remove any statically-defined Avahi services, e.g. SSH and SFTP
|
||||
# Removed anything we don't need.
|
||||
# Removed 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"]
|
||||
ENV ENABLE_AVAHI=1
|
||||
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://github.com/FFmpeg/FFmpeg
|
||||
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 \
|
||||
openssl-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=openssl \
|
||||
--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 \
|
||||
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
|
||||
|
||||
# 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"]
|
||||
COPY --from=files / /
|
||||
ENV ENABLE_AVAHI=1
|
||||
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
+29
-5
@@ -1,8 +1,32 @@
|
||||
#!/bin/sh
|
||||
|
||||
# exist if any command returns a non-zero result
|
||||
set -e
|
||||
|
||||
echo "Shairport Sync Startup ($(date))"
|
||||
|
||||
if [ -z ${ENABLE_AVAHI+x} ] || [ $ENABLE_AVAHI -eq 1 ]; then
|
||||
rm -rf /run/dbus/dbus.pid
|
||||
rm -rf /run/avahi-daemon/pid
|
||||
|
||||
dbus-uuidgen --ensure
|
||||
dbus-daemon --system
|
||||
|
||||
avahi-daemon --daemonize --no-chroot
|
||||
fi
|
||||
|
||||
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
|
||||
echo "Warning: avahi is not running, sleeping for 5 seconds before trying to start shairport-sync"
|
||||
sleep 5
|
||||
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
|
||||
|
||||
echo "Finished startup tasks ($(date)), starting Shairport Sync."
|
||||
|
||||
exec /usr/local/bin/shairport-sync "$@"
|
||||
|
||||
@@ -3,19 +3,21 @@
|
||||
services:
|
||||
shairport-sync:
|
||||
image: mikebrady/shairport-sync:latest
|
||||
network_mode: host
|
||||
network_mode: host # Required for AirPlay 2
|
||||
restart: unless-stopped
|
||||
# environment:
|
||||
# S6_KEEP_ENV: 1 # Allow S6 to pass environment variables from compose file
|
||||
# PULSE_SERVER: unix:/tmp/pulseaudio.socket # Path for PulseAudio socket
|
||||
# PULSE_COOKIE: /tmp/pulseaudio.cookie # Path for PulseAudio cookie
|
||||
# XDG_RUNTIME_DIR: /tmp # Path for pipewire
|
||||
# ENABLE_AVAHI: 0 # Disable DBus and Avahi daemon inside the container
|
||||
devices:
|
||||
- "/dev/snd" # ALSA device, omit if using PulseAudio
|
||||
# volumes:
|
||||
# - ./volumes/shairport-sync/shairport-sync.conf:/etc/shairport-sync.conf # Customised Shairport Sync configuration file.
|
||||
# - /run/user/1000/pulse/native:/tmp/pulseaudio.socket # PulseAudio socket when using that backend
|
||||
# - /run/user/1000/pipewire-0:/tmp/pipewire-0 # Pipewire socket when using pipewire
|
||||
# - /var/run/dbus:/var/run/dbus # DBus when ENABLE_AVAHI set to 0
|
||||
# - /var/run/avahi-daemon:/var/run/avahi-daemon # Avahi socket when ENABLE_AVAHI set to 0
|
||||
# command: -o pw # You can specify the desired output with command:
|
||||
logging:
|
||||
options:
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user