diff --git a/.github/workflows/docker-on-push-tag-or-pr.yaml b/.github/workflows/docker-on-push-tag-or-pr.yaml index 4e25a428..34011ee5 100644 --- a/.github/workflows/docker-on-push-tag-or-pr.yaml +++ b/.github/workflows/docker-on-push-tag-or-pr.yaml @@ -59,7 +59,7 @@ jobs: fi - name: Login to Docker Registry - uses: docker/login-action@v4.5.1 + uses: docker/login-action@v4.6.0 with: registry: ${{ secrets.DOCKER_REGISTRY }} username: ${{ secrets.DOCKER_REGISTRY_USER }} diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 2b329ed4..04495c77 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -11,7 +11,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v10.4.0 + - uses: actions/stale@v11.0.0 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.' diff --git a/README.md b/README.md index 521f56ca..e6b8dd77 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Shairport Sync does not support AirPlay video or photo streaming. * 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, with remote control for Classic AirPlay clients. * 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 partial remote control for Classic AirPlay clients. +* An [MPRIS](https://specifications.freedesktop.org/mpris/latest/)-like interface, partially complete and very functional, including access to metadata and artwork, and partial remote control for Classic AirPlay clients. * A native D-Bus interface, including access to metadata, artwork and system settings. Remote control is also available for Classic AirPlay clients. * 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. * Flexible output rates, formats and channels with built-in transcoding. diff --git a/audio.c b/audio.c index 90ca8fac..c1d27348 100644 --- a/audio.c +++ b/audio.c @@ -959,21 +959,30 @@ int32_t search_for_suitable_configuration(unsigned int channels, unsigned int ra 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)) { + // first, if we in a circumstance where no further audio processing, + // e.g. volume adjustment or convolution is required, check_ + // for an exact rate and bit depth match with the same or more channels if ( - // clang-format off + // 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) && +#ifdef CONFIG_CONVOLUTION + (config.convolution_enabled == 0) && +#endif + (config.loudness_enabled == 0) && (local_rate == rate) && (local_channels >= channels) && (config.playback_mode != ST_mono) - // clang-format on + // clang-format on ) { - // debug(1, "check exact"); + // debug(1, "first, check exact rate and format"); local_format = check_configuration_with_formats( local_channels, local_rate, (sps_format_t)format, check_configuration); - } else { - // debug(1, "check best"); + } + + if (local_format == SPS_FORMAT_UNKNOWN) { + // debug(1, "check best, with any format"); local_format = check_configuration_with_formats(local_channels, local_rate, SPS_FORMAT_S32, check_configuration); } @@ -985,7 +994,7 @@ int32_t search_for_suitable_configuration(unsigned int channels, unsigned int ra } #ifdef CONFIG_FFMPEG if (local_format == SPS_FORMAT_UNKNOWN) { - debug(3, "check for the next highest rate above %u with %u channels.", rate, + debug(3, "check for the next highest rate above %u with %u channels and the best format.", rate, local_channels); unsigned int rate_pointer = 0; while ((rate_pointer < sizeof(rates) / sizeof(unsigned int)) && @@ -993,7 +1002,7 @@ int32_t search_for_suitable_configuration(unsigned int channels, unsigned int ra 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); + local_channels, local_rate, SPS_FORMAT_S32, check_configuration); } if (local_format == SPS_FORMAT_UNKNOWN) { rate_pointer++; @@ -1003,13 +1012,13 @@ int32_t search_for_suitable_configuration(unsigned int channels, unsigned int ra if (local_format == SPS_FORMAT_UNKNOWN) { int rate_pointer = (int)(sizeof(rates) / sizeof(unsigned int) - 1); - debug(3, "check for the next lowest rate below %u with %u channels.", rate, + debug(3, "check for the next lowest rate below %u with %u channels and the best format.", 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); + local_channels, local_rate, SPS_FORMAT_S32, check_configuration); } if (local_format == SPS_FORMAT_UNKNOWN) { rate_pointer--; diff --git a/common.h b/common.h index 772da08a..11a10fc0 100644 --- a/common.h +++ b/common.h @@ -213,6 +213,7 @@ typedef struct { char *pa_sink; // the name (or id) of the sink that Shairport Sync will play on. #endif + #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. @@ -221,6 +222,7 @@ typedef struct { 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 int metadata_enabled; char *metadata_pipename; @@ -230,6 +232,7 @@ typedef struct { int get_coverart; double metadata_progress_interval; // 0 means no progress reports #endif + #ifdef CONFIG_MQTT int mqtt_enabled; char *mqtt_hostname; @@ -250,6 +253,7 @@ typedef struct { char *mqtt_autodiscovery_prefix; char *mqtt_empty_payload_substitute; #endif + uint8_t ap1_prefix[6]; uint8_t hw_addr[8]; // only needs 6 but 8 is handy when converting this to a number int port; @@ -302,8 +306,6 @@ typedef struct { // to be enabled under the auto setting int decoders_supported; int decoder_in_use; - // char *logfile; - // char *errfile; char *configfile; char *regtype; // The regtype is the service type followed by the protocol, separated by a dot, by // default “_raop._tcp.” for AirPlay 1. @@ -348,17 +350,11 @@ typedef struct { #ifdef CONFIG_CONVOLUTION 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; double convolution_max_length_in_seconds; @@ -385,6 +381,7 @@ typedef struct { int scan_max_inactive_count; // number of scans to do before stopping if not made active again // (about 15 minutes worth) #endif + int disable_resend_requests; // set this to stop resend request being made for missing packets double diagnostic_drop_packet_fraction; // pseudo randomly drop this fraction of packets, for // debugging. Currently audio packets only... @@ -395,10 +392,11 @@ typedef struct { int jack_soxr_resample_quality; #endif #endif + void *gradients; // a linked list of the clock gradients discovered for all DACP IDs // 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 + // can't define the null linked list struct here char *firmware_version; // use these in information requests char *model; @@ -440,6 +438,7 @@ typedef struct { #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 @@ -452,21 +451,6 @@ uint32_t nctohl(const uint8_t *p); // read 4 characters from *p and do ntohl on uint16_t nctohs(const uint8_t *p); // read 2 characters from *p and do ntohs on them uint64_t nctoh64(const uint8_t *p); // read 8 characters from *p to a uint64_t -// void memory_barrier(); - -/* -void log_to_stderr(); // call this to direct logging to stderr; -void log_to_stdout(); // call this to direct logging to stdout; -void log_to_syslog(); // call this to direct logging to the system log; -void log_to_file(); // call this to direct logging to a file or (pre-existing) pipe; -*/ - -// true if Shairport Sync is supposed to be sending output to the output device, false otherwise - -//int get_requested_connection_state_to_output(); - -// void set_requested_connection_state_to_output(int v); - int try_to_open_pipe_for_writing( const char *pathname); // open it without blocking if it's not hooked up diff --git a/configure.ac b/configure.ac index b3d2dbb8..010c1946 100644 --- a/configure.ac +++ b/configure.ac @@ -1,7 +1,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [5.2.1], [4265913+mikebrady@users.noreply.github.com]) +AC_INIT([shairport-sync], [5.2.2], [4265913+mikebrady@users.noreply.github.com]) : ${CFLAGS="-O3"} : ${CXXFLAGS="-O3"} AM_INIT_AUTOMAKE([subdir-objects]) diff --git a/rtp.c b/rtp.c index 7b7ee774..ec718964 100644 --- a/rtp.c +++ b/rtp.c @@ -1488,8 +1488,11 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP, } if (conn->last_anchor_info_is_valid != 0) { - if (anchorRTP != NULL) - *anchorRTP = conn->last_anchor_rtptime; + if (anchorRTP != NULL) { + // Use the current rate in case the stream format has changed. + int32_t added_latency = (int32_t)(config.audio_backend_latency_offset * conn->input_rate); + *anchorRTP = conn->last_anchor_rtptime - added_latency; + } if (anchorLocalTime != NULL) *anchorLocalTime = conn->last_anchor_local_time; } @@ -1616,6 +1619,7 @@ int32_t decipher_player_put_packet(uint8_t *ciphered_audio_alt, ssize_t nread, } void *rtp_ap2_control_receiver(void *arg) { + const int32_t ap2_realttime_stream_latency_fudge_factor = 11025; // seems to bring everything into sync // #include // debug(1, "rtp_ap2_control_receiver PID %d", syscall(SYS_gettid)); pthread_cleanup_push(rtp_ap2_control_handler_cleanup_handler, arg); @@ -1674,22 +1678,6 @@ void *rtp_ap2_control_receiver(void *arg) { switch (packet[1]) { case 215: // code 215, effectively an anchoring announcement { - // struct timespec tnr; - // clock_gettime(CLOCK_REALTIME, &tnr); - // uint64_t local_realtime_now = timespec_to_ns(&tnr); - - /* - char obf[4096]; - char *obfp = obf; - int obfc; - for (obfc=0;obfcinput_rate); - // the actual latency is the notified latency plus the fixed latency + the added - // latency + // this updates the anchor information contained in the packet + int32_t stream_specified_latency = frame_2 - frame_1; // this is the latency expected + if (stream_specified_latency != 77175) + debug(1, "Stream-specified latency is %d frames. Normally it is 77175.", stream_specified_latency); + int32_t net_source_latency = stream_specified_latency + ap2_realttime_stream_latency_fudge_factor; - int32_t net_latency = - notified_latency + 11035 + - added_latency; // this is the latency between incoming frames and the DAC - net_latency = net_latency - (int32_t)(config.audio_backend_buffer_desired_length * + // Now to accommodate a backend buffer of the desired length. + // Note that it's in input-rate frames, not output-rate frames! + + net_source_latency = net_source_latency - (int32_t)(config.audio_backend_buffer_desired_length * conn->input_rate); - // debug(1, "Net latency is %d frames.", net_latency); + + // Now we want to check the user-specified latency offset. + // We want to warn the user if they have asked for a negative latency that is too great -- + // one that would require packets to arrive before they actually do, + // (which is about two seconds before they are to be played). + + int32_t net_latency = net_source_latency + (int32_t)(config.audio_backend_latency_offset * conn->input_rate); + if (net_latency <= 0) { if (conn->latency_warning_issued == 0) { - warn("The stream latency (%f seconds) it too short to accommodate an offset of " - "%f " - "seconds and a backend buffer of %f seconds.", - ((notified_latency + 11035) * 1.0) / conn->input_rate, + warn("The stream latency (%g seconds) is too short to accommodate an audio backend latency offset of " + "%g seconds and a backend buffer of %g seconds. The audio_backend_latency_offset has been set to zero.", + ((stream_specified_latency + ap2_realttime_stream_latency_fudge_factor) * 1.0) / conn->input_rate, config.audio_backend_latency_offset, config.audio_backend_buffer_desired_length); - warn("(FYI the stream latency needed would be %f seconds.)", - config.audio_backend_buffer_desired_length - - config.audio_backend_latency_offset); + config.audio_backend_latency_offset = 0.0; + net_latency = net_source_latency; conn->latency_warning_issued = 1; } - conn->latency = notified_latency + 11035; - } else { - conn->latency = notified_latency + 11035 + added_latency; } - - set_ptp_anchor_info(conn, clock_id, frame_1 - 11035 - added_latency, - remote_packet_time_ns); + conn->latency = net_latency; // this is the time window within which packets can be accepted without being too late + set_ptp_anchor_info(conn, clock_id, frame_1 - ap2_realttime_stream_latency_fudge_factor, remote_packet_time_ns); if (conn->anchor_clock != clock_id) { debug(2, "Connection %d: Change Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id); diff --git a/rtsp.c b/rtsp.c index 60a9929c..91ea19ef 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1453,11 +1453,8 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message // debug(1, "anchor rtpTime is %" PRId64 ".", rtpTime); uint32_t anchorRTPTime = rtpTime; - int32_t added_latency = (int32_t)(config.audio_backend_latency_offset * conn->input_rate); - // debug(1,"anchorRTPTime: %" PRIu32 ", added latency: %" PRId32 ".", anchorRTPTime, - // added_latency); - set_ptp_anchor_info(conn, conn->networkTimeTimelineID, anchorRTPTime - added_latency, - anchorTimeNanoseconds); + // Store the raw anchor; apply the latency offset when it is used. + set_ptp_anchor_info(conn, conn->networkTimeTimelineID, anchorRTPTime, anchorTimeNanoseconds); } item = plist_dict_get_item(messagePlist, "rate"); diff --git a/shairport.c b/shairport.c index d64538dd..8489832c 100644 --- a/shairport.c +++ b/shairport.c @@ -1955,7 +1955,7 @@ void exit_function() { } if (glib_worker_thread != NULL) { g_thread_join(glib_worker_thread); - debug(1, "GLib worker thread joined"); + debug(2, "GLib worker thread joined"); } #endif diff --git a/xml_plist_codegen.sh b/xml_plist_codegen.sh index 18ffdfb7..3c8f5ad6 100644 --- a/xml_plist_codegen.sh +++ b/xml_plist_codegen.sh @@ -23,6 +23,6 @@ printf "// Please do not edit this file!\n" > $2/$BASE_FILENAME.c printf "// This file was automatically generated from $INPUT_FILE_NAME.\n\n" >> $2/$BASE_FILENAME.c printf "#include \"$BASE_FILENAME.h\"\n\n" >> $2/$BASE_FILENAME.c printf "unsigned char ${BASE_FILENAME}_plist[] = {\n" >> $2/$BASE_FILENAME.c -plistutil -i $1 | xxd -i - >> $2/$BASE_FILENAME.c +plistutil -i $1 | xxd -i >> $2/$BASE_FILENAME.c printf " };\n\n" >> $2/$BASE_FILENAME.c printf "size_t ${BASE_FILENAME}_plist_len = sizeof(${BASE_FILENAME}_plist);\n" >> $2/$BASE_FILENAME.c