diff --git a/.github/workflows/check_ap2_systemd_basic.yml b/.github/workflows/check_ap2_systemd_basic.yml index 367495646..381d39a13 100644 --- a/.github/workflows/check_ap2_systemd_basic.yml +++ b/.github/workflows/check_ap2_systemd_basic.yml @@ -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 diff --git a/.github/workflows/check_ap2_systemd_full.yml b/.github/workflows/check_ap2_systemd_full.yml index 7aafc1f1d..549b904b3 100644 --- a/.github/workflows/check_ap2_systemd_full.yml +++ b/.github/workflows/check_ap2_systemd_full.yml @@ -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: | diff --git a/.github/workflows/check_ap2_systemd_full_build_folder.yml b/.github/workflows/check_ap2_systemd_full_build_folder.yml index 916d28bd9..4e82666c4 100644 --- a/.github/workflows/check_ap2_systemd_full_build_folder.yml +++ b/.github/workflows/check_ap2_systemd_full_build_folder.yml @@ -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 diff --git a/.github/workflows/check_ap2_systemv_full.yml b/.github/workflows/check_ap2_systemv_full.yml index 3f46c0d62..0665e836a 100644 --- a/.github/workflows/check_ap2_systemv_full.yml +++ b/.github/workflows/check_ap2_systemv_full.yml @@ -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 diff --git a/.github/workflows/check_classic_mac_basic.yml b/.github/workflows/check_classic_mac_basic.yml index 1496be9a0..038ded2e1 100644 --- a/.github/workflows/check_classic_mac_basic.yml +++ b/.github/workflows/check_classic_mac_basic.yml @@ -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] diff --git a/.github/workflows/check_classic_systemd_basic.yml b/.github/workflows/check_classic_systemd_basic.yml index ead6de279..c28cd6f76 100644 --- a/.github/workflows/check_classic_systemd_basic.yml +++ b/.github/workflows/check_classic_systemd_basic.yml @@ -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 diff --git a/.github/workflows/check_classic_systemd_full.yml b/.github/workflows/check_classic_systemd_full.yml index 4d289c532..ea153e967 100644 --- a/.github/workflows/check_classic_systemd_full.yml +++ b/.github/workflows/check_classic_systemd_full.yml @@ -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 diff --git a/BUILD.md b/BUILD.md index ecdfc0fc9..66185af6d 100644 --- a/BUILD.md +++ b/BUILD.md @@ -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 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8f98d4340..7d33f3393 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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 ==== diff --git a/audio.h b/audio.h index 583b2bbbb..82b5409d5 100644 --- a/audio.h +++ b/audio.h @@ -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 diff --git a/audio_jack.c b/audio_jack.c index 911fada79..c1e921042 100644 --- a/audio_jack.c +++ b/audio_jack.c @@ -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 * * 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; } diff --git a/audio_pw.c b/audio_pw.c index 76764d2f3..c2fd132cd 100644 --- a/audio_pw.c +++ b/audio_pw.c @@ -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}; diff --git a/audio_sndio.c b/audio_sndio.c index a369bfb58..9fa1ea986 100644 --- a/audio_sndio.c +++ b/audio_sndio.c @@ -4,7 +4,7 @@ * Copyright (c) 2017 Tobias Kortkamp * * 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, diff --git a/common.c b/common.c index 0e2a07243..42411c5fe 100644 --- a/common.c +++ b/common.c @@ -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 diff --git a/common.h b/common.h index a1d50d448..6a341f7be 100644 --- a/common.h +++ b/common.h @@ -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); diff --git a/configure.ac b/configure.ac index 0bd9727ad..e6613faba 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [4.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"]) diff --git a/docker/Dockerfile b/docker/Dockerfile index e47dc5d7f..84ba6dcf3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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"] diff --git a/docker/classic/Dockerfile b/docker/classic/Dockerfile index d41cac4fb..283442bd7 100644 --- a/docker/classic/Dockerfile +++ b/docker/classic/Dockerfile @@ -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"] diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 82f5632fe..8a435bba8 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -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" diff --git a/mqtt.c b/mqtt.c index f662009d6..105928fd0 100644 --- a/mqtt.c +++ b/mqtt.c @@ -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': diff --git a/mqtt.h b/mqtt.h index 4bf680edd..69ad75bd5 100644 --- a/mqtt.h +++ b/mqtt.h @@ -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); diff --git a/player.c b/player.c index 46ef100f6..ccfa09f56 100644 --- a/player.c +++ b/player.c @@ -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); diff --git a/rtp.c b/rtp.c index 0f35f0341..8a7f0e0ed 100644 --- a/rtp.c +++ b/rtp.c @@ -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 diff --git a/rtsp.c b/rtsp.c index 1712e4805..3c2e0b0e1 100644 --- a/rtsp.c +++ b/rtsp.c @@ -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 { diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 78a79c7e1..8423b807b 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -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 = ; // 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" }; diff --git a/shairport.c b/shairport.c index 1060e0b7d..87ac0ea75 100644 --- a/shairport.c +++ b/shairport.c @@ -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