4.3.5 master version

This commit is contained in:
Mike Brady
2024-12-02 12:41:11 +00:00
26 changed files with 399 additions and 107 deletions
@@ -13,9 +13,9 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3.5.2
- uses: actions/checkout@v4
- name: Install Dependencies
run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev xxd
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
- name: Configure
run: |
mkdir build
+3 -3
View File
@@ -13,12 +13,12 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3.5.2
- uses: actions/checkout@v4
- name: Install Dependencies
run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libglib2.0-dev libmosquitto-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev xxd
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
- name: Configure
run: |
autoreconf -i
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
- name: Make
run: |
@@ -15,7 +15,7 @@ jobs:
steps:
- uses: actions/checkout@v3.5.2
- name: Install Dependencies
run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libglib2.0-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 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
- name: Configure
run: |
mkdir build
+3 -3
View File
@@ -13,13 +13,13 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3.5.2
- uses: actions/checkout@v4
- name: Install Dependencies
run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libdaemon-dev libconfig-dev libasound2-dev libao-dev libjack-dev libsndio-dev libglib2.0-dev libmosquitto-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libplist-dev libsodium-dev libavutil-dev libavcodec-dev libavformat-dev uuid-dev libgcrypt-dev xxd
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
- name: Configure
run: |
autoreconf -i
./configure --sysconfdir=/etc --with-alsa --with-ao --with-dummy --with-libdaemon --with-jack --with-pipe --with-sndio --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-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
- name: Make
run: |
make -j
@@ -3,7 +3,7 @@ name: Basic libao configuration for macOS with BREW -- classic only, because mac
on:
workflow_dispatch:
push:
branches: [ "development", "danger" ]
branches: [ "development" ]
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
@@ -13,9 +13,9 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3.5.2
- uses: actions/checkout@v4
- name: Install Dependencies
run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev
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
run: |
mkdir build
@@ -3,7 +3,7 @@ name: Classic (without pa, soundio, apple-alac) for systemd, using a build folde
on:
workflow_dispatch:
push:
branches: [ "development", "danger" ]
branches: [ "development" ]
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
@@ -13,9 +13,9 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3.5.2
- uses: actions/checkout@v4
- name: Install Dependencies
run: sudo apt-get -y --no-install-recommends install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libsndio-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 libpopt-dev libconfig-dev libasound2-dev libao-dev libjack-dev libglib2.0-dev libmosquitto-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev
- name: Configure
run: |
mkdir build
+2 -2
View File
@@ -107,12 +107,12 @@ Reboot for these changes to take effect.
Next, install the packages that are needed for Shairport Sync and NQPTP:
```
# pkg install git autotools pkgconf popt libconfig openssl alsa-utils \
# pkg install git autotools pkgconf popt libconfig openssl alsa-utils libsoxr \
libplist libsodium ffmpeg e2fsprogs-libuuid vim
```
If you are building classic Shairport Sync, the list of packages is shorter:
```
# pkg install git autotools pkgconf popt libconfig openssl alsa-utils
# pkg install git autotools pkgconf popt libconfig openssl alsa-utils libsoxr
```
## 3. Build
### NQPTP
+1 -1
View File
@@ -1,6 +1,6 @@
Minor Release Notes
====
Minor release notes are attached to the release themselves.
Minor release notes are attached to the releases themselves.
Version 4.3 -- Security Updates, Bug Fixes and Enhancements
====
+1
View File
@@ -31,6 +31,7 @@ typedef struct {
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
+16 -15
View File
@@ -1,6 +1,6 @@
/*
* jack output driver. This file is part of Shairport Sync.
* Copyright (c) 2019 -- 2022 Mike Brady <4265913+mikebrady@users.noreply.github.com>,
* Copyright (c) 2019 -- 2024 Mike Brady <4265913+mikebrady@users.noreply.github.com>,
* Jörn Nettingsmeier <nettings@luchtbeweging.nl>
*
* All rights reserved.
@@ -50,8 +50,11 @@ 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;
jack_nframes_t sample_rate;
jack_nframes_t jack_sample_rate;
jack_nframes_t jack_latency;
jack_ringbuffer_t *jackbuf;
@@ -85,12 +88,7 @@ soxr_io_spec_t io_spec;
#endif
static inline sample_t sample_conv(short sample) {
// It sounds correct, but I don't understand it.
// Zero int needs to be zero float. Check.
// Plus 32767 int is 1.0. Check.
// Minus 32767 int is -0.99997. And here my brain shuts down.
// In my head, it should be 1.0, and we should tolerate an overflow
// at minus 32768. But I'm sure there's a textbook explanation somewhere.
// signed 16-bit int to float
return ((sample < 0) ? (-1.0 * sample / SHRT_MIN) : (1.0 * sample / SHRT_MAX));
}
@@ -235,17 +233,17 @@ static int jack_init(__attribute__((unused)) int argc, __attribute__((unused)) c
if (!client) {
die("Could not start JACK server. JackStatus is %x", status);
}
sample_rate = jack_get_sample_rate(client);
jack_sample_rate = jack_get_sample_rate(client);
#ifdef CONFIG_SOXR
if (config.jack_soxr_resample_quality >= SOXR_QQ) {
quality_spec = soxr_quality_spec(config.jack_soxr_resample_quality, 0);
io_spec = soxr_io_spec(SOXR_INT16_I, SOXR_FLOAT32_I);
} else
#endif
if (sample_rate != 44100) {
if (jack_sample_rate != 44100) {
die("The JACK server is running at the wrong sample rate (%d) for Shairport Sync."
" Must be 44100 Hz.",
sample_rate);
jack_sample_rate);
}
jack_set_process_callback(client, &process, NULL);
jack_set_graph_order_callback(client, &graph, NULL);
@@ -329,6 +327,7 @@ static void jack_start(int i_sample_rate, __attribute__((unused)) int i_sample_f
// Nothing to do, JACK client has already been set up at jack_init().
// Also, we have no say over the sample rate or sample format of JACK,
// We convert the 16bit samples to float, and die if the sample rate is != 44k1 without soxr.
sps_sample_rate = i_sample_rate;
#ifdef CONFIG_SOXR
if (config.jack_soxr_resample_quality >= SOXR_QQ) {
// we might improve a bit with soxr_clear if the sample_rate doesn't change
@@ -336,7 +335,7 @@ 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(i_sample_rate, 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);
}
@@ -366,13 +365,15 @@ static int jack_delay(long *the_delay) {
debug(2, "audio_occupancy_now is %d.", audio_occupancy_now);
pthread_mutex_unlock(&buffer_mutex);
int64_t frames_processed_since_latest_latency_check = (delta * sample_rate) / 1000000000;
int64_t frames_processed_since_latest_latency_check = (delta * jack_sample_rate) / 1000000000;
// debug(1,"delta: %" PRId64 " frames.",frames_processed_since_latest_latency_check);
// 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:
*the_delay = jack_latency + audio_occupancy_now - frames_processed_since_latest_latency_check;
// debug(1,"reporting a delay of %d frames",*the_delay);
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);
return 0;
}
+30 -3
View File
@@ -56,6 +56,7 @@ static char *audio_lmb, *audio_umb, *audio_toq, *audio_eoq;
static size_t audio_size = buffer_allocation;
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
@@ -268,9 +269,9 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char *
// 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_RT_PROCESS | PW_STREAM_FLAG_INACTIVE,
params, 1);
stream_is_active = 0;
pw_thread_loop_unlock(data.loop);
return 0;
}
@@ -279,9 +280,27 @@ static void start(__attribute__((unused)) int sample_rate,
__attribute__((unused)) int sample_format) {
}
static void prepare_to_play() {
// debug(1, "prepare to play");
if (stream_is_active == 0) {
pw_thread_loop_lock(data.loop);
pw_stream_set_active(data.stream, true);
pw_thread_loop_unlock(data.loop);
stream_is_active = 1;
debug(3, "prepare to play activating stream");
}
}
static int play(__attribute__((unused)) void *buf, int samples,
__attribute__((unused)) int sample_type, __attribute__((unused)) uint32_t timestamp,
__attribute__((unused)) uint64_t playtime) {
if (stream_is_active == 0) {
pw_thread_loop_lock(data.loop);
pw_stream_set_active(data.stream, true);
pw_thread_loop_unlock(data.loop);
stream_is_active = 1;
debug(3, "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;
@@ -382,8 +401,15 @@ static void stop(void) {
// if (enable_fill == 0) {
// debug(1, "stop enable_fill");
// }
enable_fill = 1;
pthread_mutex_unlock(&buffer_mutex);
if (stream_is_active == 1) {
pw_thread_loop_lock(data.loop);
// pw_stream_flush(data.stream, true);
pw_stream_set_active(data.stream, false);
pw_thread_loop_unlock(data.loop);
stream_is_active = 0;
debug(3, "set stream inactive");
}
}
audio_output audio_pw = {.name = "pw",
@@ -398,6 +424,7 @@ audio_output audio_pw = {.name = "pw",
.delay = &delay,
.stats = NULL,
.play = &play,
.prepare_to_play = &prepare_to_play,
.volume = NULL,
.parameters = NULL,
.mute = NULL};
+60 -34
View File
@@ -4,7 +4,7 @@
* Copyright (c) 2017 Tobias Kortkamp <t@tobik.me>
*
* Modifications for audio synchronisation
* and related work, copyright (c) Mike Brady 2014 -- 2022
* and related work, copyright (c) Mike Brady 2014 -- 2024
* All rights reserved.
*
* Permission to use, copy, modify, and distribute this software for any
@@ -30,6 +30,7 @@
static pthread_mutex_t sndio_mutex = PTHREAD_MUTEX_INITIALIZER;
static struct sio_hdl *hdl;
static int is_running;
static int framesize;
static size_t played;
static size_t written;
@@ -58,7 +59,9 @@ static struct sndio_formats formats[] = {{"S8", SPS_FORMAT_S8, 44100, 8, 1, 1, S
{"S24_3BE", SPS_FORMAT_S24_3BE, 44100, 24, 3, 1, 0},
{"S32", SPS_FORMAT_S32, 44100, 32, 4, 1, SIO_LE_NATIVE}};
static void help() { printf(" -d output-device set the output device [default*|...]\n"); }
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) {
time_of_last_onmove_cb = get_absolute_time_in_ns();
@@ -155,6 +158,7 @@ static int init(int argc, char **argv) {
debug(1, "sndio: rate: %u.", par.rate);
debug(1, "sndio: bits: %u.", par.bits);
is_running = 0;
hdl = sio_open(devname, SIO_PLAY, 0);
if (!hdl)
die("sndio: cannot open audio device");
@@ -206,26 +210,15 @@ static int init(int argc, char **argv) {
}
static void deinit() {
// pthread_mutex_lock(&sndio_mutex);
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
sio_close(hdl);
// pthread_mutex_unlock(&sndio_mutex);
pthread_cleanup_pop(1); // unlock the mutex
}
static void start(__attribute__((unused)) int sample_rate,
__attribute__((unused)) int sample_format) {
// pthread_mutex_lock(&sndio_mutex);
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
at_least_one_onmove_cb_seen = 0;
// any previously-reported frame count
if (!sio_start(hdl))
die("sndio: unable to start");
written = played = 0;
time_of_last_onmove_cb = 0;
at_least_one_onmove_cb_seen = 0;
// pthread_mutex_unlock(&sndio_mutex);
if (hdl != NULL) {
if (is_running != 0) {
sio_flush(hdl);
is_running = 0;
}
sio_close(hdl);
hdl = NULL;
}
pthread_cleanup_pop(1); // unlock the mutex
}
@@ -233,10 +226,20 @@ 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_mutex_lock(&sndio_mutex);
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
if (is_running == 0) {
if (hdl != NULL) {
if (sio_start(hdl) != 1)
debug(1, "sndio: unable to start");
is_running = 1;
written = played = 0;
time_of_last_onmove_cb = 0;
at_least_one_onmove_cb_seen = 0;
} else {
debug(1, "sndio: output device is not open for play!");
}
}
written += sio_write(hdl, buf, frames * framesize);
// pthread_mutex_unlock(&sndio_mutex);
pthread_cleanup_pop(1); // unlock the mutex
}
return 0;
@@ -244,10 +247,17 @@ 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 (!sio_stop(hdl))
die("sndio: unable to stop");
written = played = 0;
if (hdl != NULL) {
if (is_running != 0) {
if (sio_flush(hdl) != 1)
debug(1, "sndio: unable to stop");
written = played = is_running = 0;
} else {
debug(1, "sndio: stop: not running.");
}
} else {
debug(1, "sndio: output device is not open for stop!");
}
pthread_cleanup_pop(1); // unlock the mutex
}
@@ -276,18 +286,34 @@ int get_delay(long *delay) {
static int delay(long *delay) {
int result = 0;
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
result = get_delay(delay);
if (hdl != NULL) {
if (is_running != 0) {
result = get_delay(delay);
} else {
debug(1, "sndio: output device is not open for delay!");
if (delay != NULL)
*delay = 0;
}
} else {
debug(1, "sndio: output device is not open for delay!");
}
pthread_cleanup_pop(1); // unlock the mutex
return result;
}
static void flush() {
// pthread_mutex_lock(&sndio_mutex);
pthread_cleanup_debug_mutex_lock(&sndio_mutex, 1000, 1);
if (!sio_stop(hdl) || !sio_start(hdl))
die("sndio: unable to flush");
written = played = 0;
// pthread_mutex_unlock(&sndio_mutex);
if (hdl != NULL) {
if (is_running != 0) {
if (sio_flush(hdl) != 1)
debug(1, "sndio: unable to flush");
written = played = is_running = 0;
} else {
debug(1, "sndio: flush: not running.");
}
} else {
debug(1, "sndio: output device is not open for flush!");
}
pthread_cleanup_pop(1); // unlock the mutex
}
@@ -296,7 +322,7 @@ audio_output audio_sndio = {.name = "sndio",
.init = &init,
.deinit = &deinit,
.prepare = NULL,
.start = &start,
.start = NULL,
.stop = &stop,
.is_running = NULL,
.flush = &flush,
+32
View File
@@ -574,6 +574,38 @@ void _inform(const char *thefilename, const int linenumber, const char *format,
pthread_setcancelstate(oldState, NULL);
}
void _debug_print_buffer(const char *thefilename, const int linenumber, int level, void *vbuf,
size_t buf_len) {
if (level > debuglev)
return;
char *buf = (char *)vbuf;
char *obf =
malloc(buf_len * 4 + 1); // to be on the safe side -- 4 characters on average for each byte
if (obf != NULL) {
char *obfp = obf;
unsigned int obfc;
for (obfc = 0; obfc < buf_len; obfc++) {
snprintf(obfp, 3, "%02X", buf[obfc]);
obfp += 2;
if (obfc != buf_len - 1) {
if (obfc % 32 == 31) {
snprintf(obfp, 5, " || ");
obfp += 4;
} else if (obfc % 16 == 15) {
snprintf(obfp, 4, " | ");
obfp += 3;
} else if (obfc % 4 == 3) {
snprintf(obfp, 2, " ");
obfp += 1;
}
}
};
*obfp = 0;
_debug(thefilename, linenumber, level, "%s", obf);
free(obf);
}
}
// The following two functions are adapted slightly and with thanks from Jonathan Leffler's sample
// code at
// https://stackoverflow.com/questions/675039/how-can-i-create-directory-tree-in-c-linux
+5
View File
@@ -175,6 +175,8 @@ typedef struct {
int mqtt_publish_parsed;
int mqtt_publish_cover;
int mqtt_enable_remote;
int mqtt_enable_autodiscovery;
char *mqtt_autodiscovery_prefix;
char *mqtt_empty_payload_substitute;
#endif
uint8_t ap1_prefix[6];
@@ -382,11 +384,14 @@ void _die(const char *filename, const int linenumber, const char *format, ...);
void _warn(const char *filename, const int linenumber, const char *format, ...);
void _inform(const char *filename, const int linenumber, const char *format, ...);
void _debug(const char *filename, const int linenumber, int level, const char *format, ...);
void _debug_print_buffer(const char *thefilename, const int linenumber, int level, void *buf,
size_t buf_len);
#define die(...) _die(__FILE__, __LINE__, __VA_ARGS__)
#define debug(...) _debug(__FILE__, __LINE__, __VA_ARGS__)
#define warn(...) _warn(__FILE__, __LINE__, __VA_ARGS__)
#define inform(...) _inform(__FILE__, __LINE__, __VA_ARGS__)
#define debug_print_buffer(...) _debug_print_buffer(__FILE__, __LINE__, __VA_ARGS__)
uint8_t *base64_dec(char *input, int *outlen);
char *base64_enc(uint8_t *input, int length);
+4 -3
View File
@@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.50])
AC_INIT([shairport-sync], [4.3.4], [4265913+mikebrady@users.noreply.github.com])
AC_INIT([shairport-sync], [4.3.5], [4265913+mikebrady@users.noreply.github.com])
AM_INIT_AUTOMAKE([subdir-objects])
AC_CONFIG_SRCDIR([shairport.c])
AC_CONFIG_HEADERS([config.h])
@@ -300,8 +300,9 @@ AM_CONDITIONAL([USE_AO], [test "x$with_ao" = "xyes"])
# Look for Soundio flag
AC_ARG_WITH(soundio, [AS_HELP_STRING([--with-soundio],[choose soundio API support.])])
if test "x$with_soundio" = "xyes" ; then
AC_DEFINE([CONFIG_SOUNDIO], 1, [Include SoundIO Support.])
AC_CHECK_LIB([soundio], [soundio_create], , AC_MSG_ERROR(soundio support requires the soundio library -- libsoundio-dev suggested))
AC_DEFINE([CONFIG_SOUNDIO], 1, [Include the SoundIO (libsoundio) backend.])
AC_CHECK_LIB([soundio], [soundio_create], , AC_MSG_ERROR(soundio support requires the libsoundio library -- libsoundio-dev suggested (note: the soundio backend is deprecated and will be removed in a future update)))
AC_MSG_WARN([The soundio (libsoundio) backend is deprecated and will be removed in a future update.])
fi
AM_CONDITIONAL([USE_SOUNDIO], [test "x$with_soundio" = "xyes"])
+7 -2
View File
@@ -23,6 +23,7 @@ RUN apk -U add \
libsndfile-dev \
libsodium-dev \
libtool \
pipewire-dev \
mosquitto-dev \
popt-dev \
pulseaudio-dev \
@@ -58,7 +59,7 @@ RUN autoreconf -i ../
RUN CFLAGS="-O3" CXXFLAGS="-O3" ../configure --sysconfdir=/etc --with-alsa --with-pa --with-soxr --with-avahi --with-ssl=openssl \
--with-airplay-2 --with-metadata --with-dummy --with-pipe --with-dbus-interface \
--with-stdout --with-mpris-interface --with-mqtt-client \
--with-apple-alac --with-convolution
--with-apple-alac --with-convolution --with-pw
RUN make -j $(nproc)
RUN DESTDIR=install make install
WORKDIR /
@@ -87,6 +88,7 @@ RUN apk -U add \
libsndfile \
libsodium \
libuuid \
pipewire \
man-pages \
mandoc \
mosquitto \
@@ -119,8 +121,11 @@ RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgr
# 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
Entrypoint ["/init","./run.sh"]
ENTRYPOINT ["/init","./run.sh"]
+4 -1
View File
@@ -97,8 +97,11 @@ RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgr
# 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
Entrypoint ["/init","./run.sh"]
ENTRYPOINT ["/init","./run.sh"]
+3
View File
@@ -9,11 +9,14 @@ services:
# 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
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
# command: -o pw # You can specify the desired output with command:
logging:
options:
max-size: "200k"
+134
View File
@@ -90,6 +90,135 @@ void on_connect(struct mosquitto *mosq, __attribute__((unused)) void *userdata,
snprintf(remotetopic, strlen(config.mqtt_topic) + 8, "%s/remote", config.mqtt_topic);
mosquitto_subscribe(mosq, NULL, remotetopic, 0);
}
// send autodiscovery messages if enabled
if (config.mqtt_enable_autodiscovery && config.mqtt_publish_parsed) {
send_autodiscovery_messages(mosq);
}
}
// function to send autodiscovery messages for Home Assistant
void send_autodiscovery_messages(struct mosquitto *mosq) {
const char *device_name = config.service_name;
#ifdef CONFIG_AIRPLAY_2
const char *device_id = config.airplay_device_id ? config.airplay_device_id : config.service_name;
#else
const char *device_id = config.service_name;
#endif
const char *device_id_no_colons = str_replace(device_id, ":", "");
const char *sw_version = get_version_string();
const char *model = "shairport-sync";
const char *model_friendly = "Shairport Sync";
const char *manufacturer = "Mike Brady";
const char *autodiscovery_prefix = (config.mqtt_autodiscovery_prefix != NULL) ?
config.mqtt_autodiscovery_prefix : "homeassistant";
char topic[512];
char payload[1280];
char device_payload[512];
char id_string[128];
snprintf(device_payload, sizeof(device_payload),
"\"device\": {"
"\"identifiers\": [\"%s\"],"
"\"name\": \"%s\","
"\"model\": \"%s\","
"\"sw_version\": \"%s\","
"\"manufacturer\": \"%s\""
"}",
device_id, device_name, model_friendly, sw_version, manufacturer);
// when adding sensors here, be sure to also update sensor_names and icons below!
const char *sensors[] = {
"artist",
"album",
"title",
"genre",
"format",
"output_format",
"output_frame_rate",
"track_id",
"client_ip",
"client_mac_address",
"client_name",
"client_model",
"client_device_id",
"server_ip",
"volume",
"active",
"playing",
NULL
};
const char *sensor_names[] = {
"Artist",
"Album",
"Title",
"Genre",
"Format",
"Output Format",
"Output Frame Rate",
"Track ID",
"Client IP",
"Client MAC Address",
"Client Name",
"Client Model",
"Client Device ID",
"Server IP",
"Volume",
"Active Session",
"Playing"
};
const char *icons[] = {
"mdi:account-music", // artist
"mdi:album", // album
"mdi:music", // title
"mdi:music-box-multiple", // genre
"mdi:file", // format
"mdi:file", // output format
"mdi:file-chart", // output frame rate
"mdi:identifier", // track ID
"mdi:ip", // client IP
"mdi:hexadecimal", // client MAC address
"mdi:cellphone-text", // client name
"mdi:cellphone-text", // client model
"mdi:hexadecimal", // client device ID
"mdi:ip-network", // server IP
"mdi:volume-high", // volume
"mdi:play-box-multiple", // active
"mdi:play-box-multiple-outline" // playing
};
for (int i = 0; sensors[i] != NULL; i++) {
bool is_binary_sensor = (strcmp(sensors[i], "active") == 0 || strcmp(sensors[i], "playing") == 0);
bool is_volume_sensor = strcmp(sensors[i], "volume") == 0;
snprintf(topic, sizeof(topic), "%s/%ssensor/%s_%s/%s/config",
autodiscovery_prefix, is_binary_sensor ? "binary_" : "",
model, device_id_no_colons, sensors[i]);
snprintf(id_string, sizeof(id_string), "%s_%s_%s", model, device_name, sensors[i]);
snprintf(payload, sizeof(payload),
"{"
"\"name\": \"%s\","
"\"state_topic\": \"%s/%s\","
"\"icon\": \"%s\","
"\"unique_id\": \"%s\","
"\"object_id\": \"%s\","
"%s%s%s"
"}",
sensor_names[i], config.mqtt_topic, sensors[i], icons[i], id_string, id_string,
is_binary_sensor ? "\"payload_on\": \"1\",\"payload_off\": \"0\"," : "",
is_volume_sensor ? "\"value_template\": \"{{ ((value | regex_findall_index("
"find='^(.+?),', index=0, ignorecase=False) | float / 30 + 1) * 100) | round(0) }}\","
"\"unit_of_measurement\": \"%\"," : "",
device_payload);
mosquitto_publish(mosq, NULL, topic, strlen(payload), payload, 0, true);
debug(2, "[MQTT]: published autodiscovery for %s", id_string);
}
}
// helper function to publish under a topic and automatically append the main topic
@@ -167,12 +296,14 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le
} else if (type == 'ssnc') {
switch (code) {
case 'abeg':
mqtt_publish("active", "1", 1);
mqtt_publish("active_start", data, length);
break;
case 'acre':
mqtt_publish("active_remote_id", data, length);
break;
case 'aend':
mqtt_publish("active", "0", 1);
mqtt_publish("active_end", data, length);
break;
case 'asal':
@@ -210,9 +341,11 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le
mqtt_publish("output_frame_rate", data, length);
break;
case 'pbeg':
mqtt_publish("playing", "1", 1);
mqtt_publish("play_start", data, length);
break;
case 'pend':
mqtt_publish("playing", "0", 1);
mqtt_publish("play_end", data, length);
break;
case 'pfls':
@@ -224,6 +357,7 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le
}
break;
case 'prsm':
mqtt_publish("playing", "1", 1);
mqtt_publish("play_resume", data, length);
break;
case 'pvol':
+1
View File
@@ -7,6 +7,7 @@ int initialise_mqtt();
void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t length);
void mqtt_publish(char *topic, char *data, uint32_t length);
void mqtt_setup();
void send_autodiscovery_messages(struct mosquitto *mosq);
void on_connect(struct mosquitto *mosq, void *userdata, int rc);
void on_disconnect(struct mosquitto *mosq, void *userdata, int rc);
void on_message(struct mosquitto *mosq, void *userdata, const struct mosquitto_message *msg);
+44 -23
View File
@@ -1179,6 +1179,10 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn) {
if (conn->ab_buffering) { // if we are getting packets but not yet forwarding them to the
// player
if (conn->first_packet_timestamp == 0) { // if this is the very first packet
if (config.output->prepare_to_play) // tell the player to get ready
config.output->prepare_to_play(); // there could be more than one of these sent
conn->first_packet_timestamp =
curframe->given_timestamp; // we will keep buffering until we are
// supposed to start playing this
@@ -2141,9 +2145,10 @@ void *player_thread_func(void *arg) {
if ((config.output->parameters == NULL) || (conn->input_bit_depth > output_bit_depth) ||
(config.playback_mode == ST_mono))
conn->enable_dither = 1;
// remember, the output device may never have been initialised prior to this call
config.output->start(config.output_rate, config.output_format); // will need a corresponding stop
// call the backend's start() function if it exists.
if (config.output->start != NULL)
config.output->start(config.output_rate, config.output_format);
// we need an intermediate "transition" buffer
@@ -3291,6 +3296,37 @@ void *player_thread_func(void *arg) {
pthread_exit(NULL);
}
static void player_send_volume_metadata(uint8_t vol_mode_both, double airplay_volume, double scaled_attenuation, int32_t max_db, int32_t min_db, int32_t hw_max_db)
{
#ifdef CONFIG_METADATA
// here, send the 'pvol' metadata message when the airplay volume information
// is being used by shairport sync to control the output volume
char dv[128];
memset(dv, 0, 128);
if (config.ignore_volume_control == 0) {
if (vol_mode_both == 1) {
// normalise the maximum output to the hardware device's max output
snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume,
(scaled_attenuation - max_db + hw_max_db) / 100.0,
(min_db - max_db + hw_max_db) / 100.0, (max_db - max_db + hw_max_db) / 100.0);
} else {
snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, scaled_attenuation / 100.0,
min_db / 100.0, max_db / 100.0);
}
} else {
snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, 0.0, 0.0, 0.0);
}
send_ssnc_metadata('pvol', dv, strlen(dv), 1);
#else
(void)vol_mode_both;
(void)airplay_volume;
(void)scaled_attenuation;
(void)max_db;
(void)min_db;
(void)hw_max_db;
#endif
}
void player_volume_without_notification(double airplay_volume, rtsp_conn_info *conn) {
debug_mutex_lock(&conn->volume_control_mutex, 5000, 1);
// first, see if we are hw only, sw only, both with hw attenuation on the top or both with sw
@@ -3379,6 +3415,9 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c
volume_mode, airplay_volume);
}
}
uint8_t vol_mode_both = (volume_mode == vol_both) ? 1 : 0;
player_send_volume_metadata(vol_mode_both, airplay_volume, 0, 0, 0, 0);
} else {
int32_t max_db = 0, min_db = 0;
switch (volume_mode) {
@@ -3489,26 +3528,8 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c
inform("Output Level set to: %.2f dB.", scaled_attenuation / 100.0);
}
#ifdef CONFIG_METADATA
// here, send the 'pvol' metadata message when the airplay volume information
// is being used by shairport sync to control the output volume
char dv[128];
memset(dv, 0, 128);
if (config.ignore_volume_control == 0) {
if (volume_mode == vol_both) {
// normalise the maximum output to the hardware device's max output
snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume,
(scaled_attenuation - max_db + hw_max_db) / 100.0,
(min_db - max_db + hw_max_db) / 100.0, (max_db - max_db + hw_max_db) / 100.0);
} else {
snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, scaled_attenuation / 100.0,
min_db / 100.0, max_db / 100.0);
}
} else {
snprintf(dv, 127, "%.2f,%.2f,%.2f,%.2f", airplay_volume, 0.0, 0.0, 0.0);
}
send_ssnc_metadata('pvol', dv, strlen(dv), 1);
#endif
uint8_t vol_mode_both = (volume_mode == vol_both) ? 1 : 0;
player_send_volume_metadata(vol_mode_both, airplay_volume, scaled_attenuation, max_db, min_db, hw_max_db);
if (config.output->mute)
config.output->mute(0);
+16 -5
View File
@@ -2094,10 +2094,10 @@ void avcodec_alloc_context3_cleanup_handler(void *arg) {
av_free(codec_context);
}
void avcodec_open2_cleanup_handler(void *arg) {
debug(3, "avcodec_open2_cleanup_handler");
AVCodecContext *codec_context = arg;
avcodec_close(codec_context);
void avcodec_open2_cleanup_handler(__attribute__((unused)) void *arg) {
debug(3, "avcodec_open2_cleanup_handler -- does nothing right now");
// AVCodecContext *codec_context = arg;
// avcodec_free_context(&codec_context);
}
void av_parser_init_cleanup_handler(void *arg) {
@@ -2313,8 +2313,15 @@ void *rtp_buffered_audio_processor(void *arg) {
// push a deallocator -- av_packet_free(pkt);
pthread_cleanup_push(swr_alloc_cleanup_handler, &swr);
// FFmpeg 5.1 or later...
#if LIBAVUTIL_VERSION_MAJOR >= 57
av_opt_set_chlayout(swr, "in_chlayout", &(AVChannelLayout)AV_CHANNEL_LAYOUT_STEREO, 0);
av_opt_set_chlayout(swr, "out_chlayout", &(AVChannelLayout)AV_CHANNEL_LAYOUT_STEREO, 0);
#else
av_opt_set_int(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0);
av_opt_set_int(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0);
#endif
av_opt_set_int(swr, "in_sample_rate", conn->input_rate, 0);
av_opt_set_int(swr, "out_sample_rate", conn->input_rate,
0); // must match or the timing will be wrong`
@@ -2357,7 +2364,11 @@ void *rtp_buffered_audio_processor(void *arg) {
};
av_opt_set_sample_fmt(swr, "out_sample_fmt", av_format, 0);
swr_init(swr);
int swr_err = swr_init(swr);
if (swr_err !=0){
die("FFMpeg swr_init() failed Error %d (%s)",
swr_err, av_err2str(swr_err));
}
uint8_t packet[16 * 1024];
unsigned char m[16 * 1024]; // leave the first 7 bytes blank to make room for the ADTS
+16 -2
View File
@@ -1179,15 +1179,29 @@ ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time
read_encrypted(conn->fd, &conn->ap2_pairing_context.control_cipher_bundle, buf, count);
} else {
result = read(conn->fd, buf, count);
if (result == 0) {
debug(3, "AP2 read result 0, for a request count of %u.", count);
}
}
#else
result = read(conn->fd, buf, count);
if (result == 0) {
debug(3, "AP1 read result 0, for a request count of %u.", count);
}
#endif
if ((result == 0) && (errno != 0)) {
char errorstring[1024];
strerror_r(errno, (char *)errorstring, sizeof(errorstring));
debug(2, "Connection %d: read result 0, error %d: \"%s\".",
conn->connection_number, errno, (char *)errorstring);
}
if (wait_time != 0)
remaining_time = time_to_wait_to - get_absolute_time_in_ns();
if (((result == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) && (remaining_time > 0))
if ((((result == -1) || (result == 0)) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) && (remaining_time > 0))
debug(1, "remaining time on a timed read is %" PRId64 " ns.", remaining_time);
} while (((result == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) &&
} while ((((result == -1) || (result == 0)) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) &&
(remaining_time > 0));
} else {
+3 -1
View File
@@ -157,7 +157,7 @@ pw =
// --with-sndio
sndio =
{
// device = "snd/0"; // optional setting to set the name of the output device. Default is the sndio system default.
// device = "default"; // optional setting to set the name of the output device, e.g. "rsnd/0", "rsnd/1", etc.
// rate = 44100; // optional setting which can be 44100, 88200, 176400 or 352800, but the device must have the capability. Default is 44100.
// format = "S16"; // optional setting which can be "U8", "S8", "S16", "S24", "S24_3LE", "S24_3BE" or "S32", but the device must have the capability. Except where stated using (*LE or *BE), endianness matches that of the processor.
// round = <number>; // advanced optional setting to set the period size near to this value
@@ -285,6 +285,8 @@ mqtt =
// Currently published topics:artist,album,title,genre,format,songalbum,volume,client_ip,
// Additionally, messages at the topics play_start,play_end,play_flush,play_resume are published
// publish_cover = "no"; //whether to publish the cover over mqtt in binary form. This may lead to a bit of load on the broker
// enable_autodiscovery = "no"; //whether to publish an autodiscovery message to automatically appear in Home Assistant
// autodiscovery_prefix = "homeassistant"; //string to prepend to autodiscovery topic
// enable_remote = "no"; //whether to remote control via MQTT. RC is available under `topic`/remote.
// Available commands are "command", "beginff", "beginrew", "mutetoggle", "nextitem", "previtem", "pause", "playpause", "play", "stop", "playresume", "shuffle_songs", "volumedown", "volumeup"
};
+5
View File
@@ -1300,6 +1300,10 @@ int parse_options(int argc, char **argv) {
if (config.mqtt_publish_cover && !config.get_coverart) {
die("You need to have metadata.include_cover_art enabled in order to use mqtt.publish_cover");
}
config_set_lookup_bool(config.cfg, "mqtt.enable_autodiscovery", &config.mqtt_enable_autodiscovery);
if (config_lookup_string(config.cfg, "mqtt.autodiscovery_prefix", &str)) {
config.mqtt_autodiscovery_prefix = (char *)str;
}
config_set_lookup_bool(config.cfg, "mqtt.enable_remote", &config.mqtt_enable_remote);
if (config_lookup_string(config.cfg, "mqtt.empty_payload_substitute", &str)) {
if (strlen(str) == 0)
@@ -2550,6 +2554,7 @@ int main(int argc, char **argv) {
debug(1, "mqtt will%s publish parsed metadata.", config.mqtt_publish_parsed ? "" : " not");
debug(1, "mqtt will%s publish cover Art.", config.mqtt_publish_cover ? "" : " not");
debug(1, "mqtt remote control is %sabled.", config.mqtt_enable_remote ? "en" : "dis");
debug(1, "mqtt autodiscovery is %sabled.", config.mqtt_enable_autodiscovery ? "en" : "dis");
#endif
#ifdef CONFIG_CONVOLUTION