]> git.ipfire.org Git - thirdparty/shairport-sync.git/commitdiff
1. Add the ability to work back through queued buffers to find the first buffer that...
authorMike Brady <4265913+mikebrady@users.noreply.github.com>
Sun, 14 Dec 2025 10:17:38 +0000 (10:17 +0000)
committerMike Brady <4265913+mikebrady@users.noreply.github.com>
Sun, 14 Dec 2025 10:17:38 +0000 (10:17 +0000)
2. Clean up get_audio_buffer_size_and_occupancy() and replace it with get_audio_buffer_occupancy().
3. Add a new general setting: audio_decoded_buffer_desired_length_in_seconds for managing the size of the player buffer queue when the source is buffered audio. Default is 0.75 seconds so that we can step back up to that time if older buffers appear.

ap2_buffered_audio_processor.c
ap2_event_receiver.c
ap2_rc_event_receiver.c
audio.c
common.h
player.c
player.h
rtsp.c
scripts/shairport-sync.conf
shairport.c

index 10dd9155a9cd530d0a76e431b500b6bd4af015a8..1a0fe835cca9fb1d8d4c1b4f7450941b84e04108 100644 (file)
@@ -286,6 +286,8 @@ void *rtp_buffered_audio_processor(void *arg) {
               if (ssrc_is_recognised(payload_ssrc) == 0) {
                 debug(2, "Unrecognised SSRC: %u.", payload_ssrc);
               } else {
+                debug(1, "Connection %d: incoming audio switching to \"%s\".",
+                      conn->connection_number, get_ssrc_name(payload_ssrc));
                 debug(2,
                       "Reading a block: new encoding: %s, old encoding: %s. Preparing a new "
                       "decoding chain.",
@@ -420,16 +422,15 @@ void *rtp_buffered_audio_processor(void *arg) {
       // to decode it and pass it to the player
       if (new_audio_block_needed == 0) {
         // is there space in the player thread's buffer system?
-        unsigned int player_buffer_size, player_buffer_occupancy;
-        get_audio_buffer_size_and_occupancy(&player_buffer_size, &player_buffer_occupancy, conn);
+        size_t player_buffer_occupancy = get_audio_buffer_occupancy(conn);
         // debug(1,"player buffer size and occupancy: %u and %u", player_buffer_size,
         // player_buffer_occupancy);
 
         // If we are playing and there is room in the player buffer, go ahead and decode the block
         // and send it to the player. Otherwise, keep the block and sleep for a while.
         if ((play_enabled != 0) &&
-            (player_buffer_occupancy <= 2 * ((config.audio_backend_buffer_desired_length) *
-                                             conn->input_rate / conn->frames_per_packet))) {
+            (((1.0 * player_buffer_occupancy * conn->frames_per_packet) / conn->input_rate) <=
+             config.audio_decoded_buffer_desired_length)) {
           uint64_t buffer_should_be_time;
           frame_to_local_time(timestamp, &buffer_should_be_time, conn);
 
@@ -517,7 +518,7 @@ void *rtp_buffered_audio_processor(void *arg) {
                   } else {
                     timestamp_difference = timestamp - expected_timestamp;
                     if (timestamp_difference != 0) {
-                      debug(1,
+                      debug(2,
                             "Connection %d: "
                             "unexpected timestamp in block %u. Actual: %u, expected: %u "
                             "difference: %d, "
@@ -539,10 +540,31 @@ void *rtp_buffered_audio_processor(void *arg) {
                   }
                   int skip_this_block = 0;
                   if (timestamp_difference < 0) {
+
+                    // uncomment this to work back to replace buffers that have been already decoded
+                    // and placed in the player queue with the incoming new buffers this is a bit
+                    // trickier, but maybe the new buffers are better than the previous ones they
+                    // will replace (?)
+                    /*
+                    seq_t revised_seqno = get_revised_seqno(conn, timestamp);
+                    if (revised_seqno != sequence_number_for_player) {
+                      debug(1, "revised seqno calculated: conn->ab_read: %u, revised_seqno: %u,
+                    conn->ab_write: %u.", conn->ab_read, revised_seqno, conn->ab_write);
+                      clear_buffers_from(conn, revised_seqno);
+                      sequence_number_for_player = revised_seqno;
+                      timestamp_difference = 0;
+                    }
+                    */
+
+                    // uncomment this to drop incoming new buffers that are too old and for whose
+                    // timings buffers have already been decoded and placed in the player queue this
+                    // is easier, but maybe the new late buffers are better than the previous ones
+                    // (?)
+
                     int32_t abs_timestamp_difference = -timestamp_difference;
                     if ((size_t)abs_timestamp_difference > get_ssrc_block_length(payload_ssrc)) {
                       skip_this_block = 1;
-                      debug(1,
+                      debug(2,
                             "skipping block %u because it is too old. Timestamp "
                             "difference: %d, length of block: %u.",
                             seq_no, timestamp_difference, get_ssrc_block_length(payload_ssrc));
index 75371cbde58d7ff8ba266b3a68fc84d084efa649..770c6ca10d756438fc8212a86d2d74d0e62d504d 100644 (file)
 #include "player.h"
 #include "ptp-utilities.h"
 #include "rtsp.h"
-#include "utilities/structured_buffer.h"
 #include "utilities/network_utilities.h"
+#include "utilities/structured_buffer.h"
 
 void ap2_event_receiver_cleanup_handler(void *arg) {
   rtsp_conn_info *conn = (rtsp_conn_info *)arg;
-  debug(1, "Connection %d: AP2 Event Receiver Cleanup start.", conn->connection_number);
+  // debug(1, "Connection %d: AP2 Event Receiver Cleanup start.", conn->connection_number);
   // only update these things if you're (still) the principal conn
 
 #ifdef CONFIG_METADATA
@@ -73,7 +73,7 @@ void ap2_event_receiver_cleanup_handler(void *arg) {
   }
   pthread_cleanup_pop(1); // release the principal_conn lock
   */
-  debug(1, "Connection %d: AP2 Event Receiver Cleanup exit.", conn->connection_number);
+  debug(2, "Connection %d: AP2 Event Receiver Cleanup is complete.", conn->connection_number);
 }
 
 void *ap2_event_receiver(void *arg) {
index 74d03327c75dbb5f4f7edb52ef5294d909b81db5..0fba8562a3ab7f23cfc9453b62bd48d10b884490 100644 (file)
@@ -28,8 +28,8 @@
 #include "common.h"
 #include "player.h"
 #include "rtsp.h"
-#include "utilities/structured_buffer.h"
 #include "utilities/network_utilities.h"
+#include "utilities/structured_buffer.h"
 
 void ap2_rc_event_receiver_cleanup_handler(void *arg) {
   rtsp_conn_info *conn = (rtsp_conn_info *)arg;
diff --git a/audio.c b/audio.c
index b188ac734714beb61788e662cd199b6891b9540d..af51751024ad9eb1ce140b01367198400489999d 100644 (file)
--- a/audio.c
+++ b/audio.c
@@ -140,10 +140,11 @@ void parse_audio_options(const char *named_stanza, uint32_t default_format_set,
     /* Get the desired buffer size setting (deprecated). */
     if (config_lookup_int(config.cfg, "general.audio_backend_buffer_desired_length", &value)) {
       inform("The setting general.audio_backend_buffer_desired_length is no longer supported. "
-             "Please use alsa.audio_backend_buffer_desired_length_in_seconds instead.");
+             "Please use general.audio_backend_buffer_desired_length_in_seconds instead.");
     }
 
-    /* Get the desired buffer size setting in seconds. */
+    /* Get the desired backend buffer size setting in seconds. */
+    /* This is the size of the buffer in the output system, e.g. in the DAC itself in ALSA */
     if (config_lookup_float(config.cfg, "general.audio_backend_buffer_desired_length_in_seconds",
                             &dvalue)) {
       if (dvalue < 0) {
@@ -156,6 +157,21 @@ void parse_audio_options(const char *named_stanza, uint32_t default_format_set,
       }
     }
 
+    /* Get the desired decoded buffer size setting in seconds. */
+    /* This is the size of the buffer of decoded and deciphered audio held in the player's output
+     * queue prior to sending it to the output system */
+    if (config_lookup_float(config.cfg, "general.audio_decoded_buffer_desired_length_in_seconds",
+                            &dvalue)) {
+      if (dvalue < 0) {
+        die("Invalid audio_decoded_buffer_desired_length_in_seconds value: \"%f\". It "
+            "should be 0.0 or greater."
+            " The default is %.3f seconds",
+            dvalue, config.audio_decoded_buffer_desired_length);
+      } else {
+        config.audio_decoded_buffer_desired_length = dvalue;
+      }
+    }
+
     /* Get the minimum buffer size for fancy interpolation setting in seconds. */
     if (config_lookup_float(config.cfg,
                             "general.audio_backend_buffer_interpolation_threshold_in_seconds",
index 531f7f78f7e14d452250c35e3becbd5e3979e06d..b887dbe2bb70aa5589b777aa56a0e38f82f8233c 100644 (file)
--- a/common.h
+++ b/common.h
@@ -301,6 +301,8 @@ typedef struct {
   double audio_backend_buffer_interpolation_threshold_in_seconds; // below this, soxr interpolation
                                                                   // will not occur -- it'll be
                                                                   // basic interpolation instead.
+  double audio_decoded_buffer_desired_length;    // the length of the buffer of fully decoded audio
+                                                 // prior to being sent to the output device
   double disable_standby_mode_silence_threshold; // below this, silence will be added to the output
                                                  // buffer
   double disable_standby_mode_silence_scan_interval; // check the threshold this often
index b2242b18e6529d0191bd1222cf21f34ee14a2d68..20366387ddf2e685b5f5d1255daad38dcf4b0aa0 100644 (file)
--- a/player.c
+++ b/player.c
@@ -343,19 +343,17 @@ void reset_buffer(rtsp_conn_info *conn) {
 // returns the total number of blocks and the number occupied, but not their size,
 // because the size is determined by the block size sent
 
-void get_audio_buffer_size_and_occupancy(unsigned int *size, unsigned int *occupancy,
-                                         rtsp_conn_info *conn) {
+size_t get_audio_buffer_occupancy(rtsp_conn_info *conn) {
+  size_t response = 0;
   pthread_cleanup_debug_mutex_lock(&conn->ab_mutex, 30000, 0);
-  *size = BUFFER_FRAMES;
   if (conn->ab_synced) {
     int16_t occ =
         conn->ab_write - conn->ab_read; // will be zero or positive if read and write are within
                                         // 2^15 of each other and write is at or after read
-    *occupancy = occ;
-  } else {
-    *occupancy = 0;
+    response = occ;
   }
   pthread_cleanup_pop(1);
+  return response;
 }
 
 const char *get_category_string(airplay_stream_c cat) {
@@ -1034,7 +1032,7 @@ void prepare_decoding_chain(rtsp_conn_info *conn, ssrc_t ssrc) {
 
     if ((config.statistics_requested != 0) && (ssrc != SSRC_NONE) &&
         (conn->incoming_ssrc != SSRC_NONE)) {
-      debug(3, "Connection %d: incoming audio switching to \"%s\".", conn->connection_number,
+      debug(2, "Connection %d: incoming audio switching to \"%s\".", conn->connection_number,
             get_ssrc_name(ssrc));
 #ifdef CONFIG_METADATA
       send_ssnc_metadata('sdsc', get_ssrc_name(ssrc), strlen(get_ssrc_name(ssrc)), 1);
@@ -1372,6 +1370,51 @@ int openssl_aes_decrypt_cbc(unsigned char *ciphertext, int ciphertext_len, unsig
 }
 #endif
 
+#ifdef CONFIG_AIRPLAY_2
+
+#ifdef CONFIG_AIRPLAY_2
+// This is a big dirty hack to try to accommodate packets that come in in sequence but are timed to
+// be earlier that what went before them. This happens when the feed is switching from AAC to ALAC.
+// So basically we will look back through the buffers in the queue until we find the last buffer
+// that predates the incoming one. We will make the subsequent buffer the revised_seqno. If we can't
+// find an older buffer, that means we can't go back far enough to find an older buffer and then the
+// ab_read buffer becomes the revised_seqno.
+seq_t get_revised_seqno(rtsp_conn_info *conn, uint32_t timestamp) {
+  // go back through the buffers to find the first buffer following a buffer that predates
+  // the given timestamp, if any.
+  seq_t revised_seqno = conn->ab_write;
+  pthread_cleanup_debug_mutex_lock(&conn->ab_mutex, 30000, 0);
+  int older_seqno_found = 0;
+  while ((older_seqno_found == 0) && (revised_seqno != conn->ab_read)) {
+    revised_seqno--;
+    abuf_t *tbuf = conn->audio_buffer + BUFIDX(revised_seqno);
+    if (tbuf->ready != 0) {
+      int32_t timestamp_difference = timestamp - tbuf->timestamp;
+      if (timestamp_difference >= 0) {
+        older_seqno_found = 1;
+      }
+    }
+  }
+  if (older_seqno_found)
+    revised_seqno++;
+
+  pthread_cleanup_pop(1);
+  return revised_seqno;
+}
+
+void clear_buffers_from(rtsp_conn_info *conn, seq_t from_here) {
+  seq_t bi = from_here;
+  while (bi != conn->ab_write) {
+    abuf_t *tbuf = conn->audio_buffer + BUFIDX(bi);
+    free_audio_buffer_payload(tbuf);
+    bi++;
+  }
+}
+
+#endif
+
+#endif
+
 #ifdef CONFIG_FFMPEG
 uint32_t player_put_packet(uint32_t ssrc, seq_t seqno, uint32_t actual_timestamp, uint8_t *data,
                            size_t len, int mute, int32_t timestamp_gap, rtsp_conn_info *conn) {
@@ -1386,7 +1429,7 @@ uint32_t player_put_packet(uint32_t ssrc, seq_t seqno, uint32_t actual_timestamp
   // The timestamp_gap is the difference between the timestamp and the expected timestamp.
   // It should normally be zero.
 
-  // It can be decoded by the Hammerton or Apple ALAC decoders, or my the FFmpeg decoder.
+  // It can be decoded by the Hammerton or Apple ALAC decoders, or by the FFmpeg decoder.
 
   // The SSRC signifies the encoding used for that block of audio.
   // It is used to select the type of decoding to be done by the FFMPEG-based
@@ -2729,7 +2772,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info *conn, int resync_requested) {
                 get_ssrc_name(curframe->ssrc));
         } else {
           debug(1, "Connection %d: incoming audio switching to \"%s\".", conn->connection_number,
-            get_ssrc_name(curframe->ssrc));
+                get_ssrc_name(curframe->ssrc));
           clear_software_resampler(conn);
           // ask the backend if it can give us its best choice for an ffmpeg configuration:
         }
index 767cd0e5a100000fe8312f84eb7c0023a7babaef..2f993c24a25060b1e139d98a2888bd055ae3dbea 100644 (file)
--- a/player.h
+++ b/player.h
@@ -466,8 +466,8 @@ typedef struct {
   unsigned char *session_key; // needs to be free'd at the end
   char *ap2_client_name;      // needs to be free'd at teardown phase 2
   uint64_t frames_packet;
-  uint64_t type; // 96 (Realtime Audio), 103 (Buffered Audio), 130 (Remote Control)
-  uint64_t networkTimeTimelineID;   // the clock ID used by the player
+  uint64_t type;                  // 96 (Realtime Audio), 103 (Buffered Audio), 130 (Remote Control)
+  uint64_t networkTimeTimelineID; // the clock ID used by the player
   uint8_t groupContainsGroupLeader; // information coming from the SETUP
   uint64_t compressionType;
 #endif
@@ -586,8 +586,7 @@ extern int statistics_row; // will be reset to zero when debug level changes or
 
 void reset_buffer(rtsp_conn_info *conn);
 
-void get_audio_buffer_size_and_occupancy(unsigned int *size, unsigned int *occupancy,
-                                         rtsp_conn_info *conn);
+size_t get_audio_buffer_occupancy(rtsp_conn_info *conn);
 
 int32_t modulo_32_offset(uint32_t from, uint32_t to);
 
@@ -600,6 +599,9 @@ void player_volume(double f, rtsp_conn_info *conn);
 void player_volume_without_notification(double f, rtsp_conn_info *conn);
 void player_flush(uint32_t timestamp, rtsp_conn_info *conn);
 // void player_full_flush(rtsp_conn_info *conn);
+
+seq_t get_revised_seqno(rtsp_conn_info *conn, uint32_t timestamp);
+void clear_buffers_from(rtsp_conn_info *conn, seq_t from_here);
 uint32_t player_put_packet(uint32_t ssrc, seq_t seqno, uint32_t actual_timestamp, uint8_t *data,
                            size_t len, int mute, int32_t timestamp_gap, rtsp_conn_info *conn);
 int64_t monotonic_timestamp(uint32_t timestamp,
diff --git a/rtsp.c b/rtsp.c
index eff9fdf764bf93ebba2921ab12153f22af65f0ee..42e06b8755c6aeb0808ff6b2f5146d26c87e68b3 100644 (file)
--- a/rtsp.c
+++ b/rtsp.c
@@ -1929,7 +1929,7 @@ struct pairings {
   uint8_t public_key[32];
 
   struct pairings *next;
-} *pairings;
+} * pairings;
 
 static struct pairings *pairing_find(const char *device_id) {
   for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) {
@@ -4347,9 +4347,9 @@ static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_messag
 
 #ifdef CONFIG_AIRPLAY_2
     // In AirPlay 2, an ANNOUNCE signifies the start of an AirPlay 1 session.
-    debug(1, "Connection %d: %s connection from %s:%u to self at %s:%u.",
-          conn->connection_number, get_category_string(conn->airplay_stream_category), conn->client_ip_string, conn->client_rtsp_port,
-          conn->self_ip_string, conn->self_rtsp_port);
+    debug(1, "Connection %d: %s connection from %s:%u to self at %s:%u.", conn->connection_number,
+          get_category_string(conn->airplay_stream_category), conn->client_ip_string,
+          conn->client_rtsp_port, conn->self_ip_string, conn->self_rtsp_port);
     conn->airplay_type = ap_1;
     conn->timing_type = ts_ntp;
     conn->type = 96; // this is the AirPlay 2 code for Realtime Audio -- not sure it's right
@@ -4911,7 +4911,7 @@ void rtsp_conversation_thread_cleanup_function(void *arg) {
       } while ((conn->ap2_event_receiver_exited == 0) && (event_receiver_wait_time < 2000000000L));
 
       if (conn->ap2_event_receiver_exited == 0) {
-        debug(1, "Connection %d: %s event receiver has not exited, so cancelling it.",
+        debug(2, "Connection %d: %s event receiver has not exited, so cancelling it.",
               conn->connection_number, get_category_string(conn->airplay_stream_category));
         pthread_cancel(*conn->rtp_event_thread);
       }
index 57a1fd66dfc9927451240a886867e0565a8cc20f..8d2e2ad2ee2e3378bed3d60eb511aba7424a9ae2 100644 (file)
@@ -72,9 +72,10 @@ general =
 //             Use it, for example, to compensate for a fixed delay in the audio back end.
 //             E.g. if the output device, e.g. a soundbar, takes 100 ms to process audio, set this to -0.1 to deliver the audio
 //             to the output device 100 ms early, allowing it time to process the audio and output it perfectly in sync.
-//     audio_backend_buffer_desired_length_in_seconds = 0.2; // If set too small, buffer underflow occurs on low-powered machines.
+//     audio_backend_buffer_desired_length_in_seconds = 0.2; // This is the desired size of the buffer to be maintained in the external output system, e.g. the DAC in ALSA. If set too small, buffer underflow occurs on low-powered machines.
 //             Too long and the response time to volume changes becomes annoying.
-//             Default is 0.2 seconds in the alsa backend, 0.35 seconds in the pa backend and 1.0 seconds otherwise.
+//     audio_decoded_buffer_desired_length_in_seconds = 1.0; // Advanced feature. This is the desired size of the buffer of fully deciphered and decoded audio maintained within Shairport Sync prior to sending it to the external output system , e.g. the DAC in ALSA.
+//             Valid for AirPlay 2 Buffered Audio streams only.
 //     audio_backend_buffer_interpolation_threshold_in_seconds = 0.075; // Advanced feature. If the buffer size drops below this, stop using time-consuming interpolation like soxr to avoid dropouts due to underrun.
 //     audio_backend_silent_lead_in_time = "auto"; // This optional advanced setting, either "auto" or a positive number, sets the length of the period of silence that precedes the start of the audio.
 //             The default is "auto" -- the silent lead-in starts as soon as the player starts sending packets.
index 9964ce14edf8484c5a3e441da69c0852c5d3ef7e..d3d8421ab1c464fc9183946d6b915bbae840e7a2 100644 (file)
@@ -2407,6 +2407,7 @@ int main(int argc, char **argv) {
   set_requested_connection_state_to_output(
       1); // we expect to be able to connect to the output device
   config.audio_backend_buffer_desired_length = 0.15; // seconds
+  config.audio_decoded_buffer_desired_length = 0.75; // seconds
   config.udp_port_base = 6001;
   config.udp_port_range = 10;