From 07b60e7112ee38eb69731a1b279bf887edb58881 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sun, 14 Dec 2025 10:17:38 +0000 Subject: [PATCH] =?UTF-8?q?1.=20Add=20the=20ability=20to=20work=20back=20t?= =?UTF-8?q?hrough=20queued=20buffers=20to=20find=20the=20first=20buffer=20?= =?UTF-8?q?that=E2=80=99s=20older=20than=20the=20given=20one.=20This=20is?= =?UTF-8?q?=20to=20enable=20the=20replacement=20of=20deciphered=20buffers?= =?UTF-8?q?=20with=20buffers=20that=20arrive=20later=20but=20are=20timed?= =?UTF-8?q?=20earlier.=E2=80=A8Added=20functions=20that=20relate=20to=20th?= =?UTF-8?q?is=20are:=20get=5Frevised=5Fseqno()=20to=20wind=20back=20throug?= =?UTF-8?q?h=20the=20queued=20buffers=20and=20clear=5Fbuffers=5Ffrom()=20t?= =?UTF-8?q?o=20free=20buffers=20that=20are=20already=20occupied.=202.=20Cl?= =?UTF-8?q?ean=20up=20get=5Faudio=5Fbuffer=5Fsize=5Fand=5Foccupancy()=20an?= =?UTF-8?q?d=20replace=20it=20with=20get=5Faudio=5Fbuffer=5Foccupancy().?= =?UTF-8?q?=203.=20Add=20a=20new=20general=20setting:=20audio=5Fdecoded=5F?= =?UTF-8?q?buffer=5Fdesired=5Flength=5Fin=5Fseconds=20for=20managing=20the?= =?UTF-8?q?=20size=20of=20the=20player=20buffer=20queue=20when=20the=20sou?= =?UTF-8?q?rce=20is=20buffered=20audio.=20Default=20is=200.75=20seconds=20?= =?UTF-8?q?so=20that=20we=20can=20step=20back=20up=20to=20that=20time=20if?= =?UTF-8?q?=20older=20buffers=20appear.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ap2_buffered_audio_processor.c | 34 +++++++++++++++---- ap2_event_receiver.c | 6 ++-- ap2_rc_event_receiver.c | 2 +- audio.c | 20 +++++++++-- common.h | 2 ++ player.c | 61 +++++++++++++++++++++++++++++----- player.h | 10 +++--- rtsp.c | 10 +++--- scripts/shairport-sync.conf | 5 +-- shairport.c | 1 + 10 files changed, 119 insertions(+), 32 deletions(-) diff --git a/ap2_buffered_audio_processor.c b/ap2_buffered_audio_processor.c index 10dd9155..1a0fe835 100644 --- a/ap2_buffered_audio_processor.c +++ b/ap2_buffered_audio_processor.c @@ -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)); diff --git a/ap2_event_receiver.c b/ap2_event_receiver.c index 75371cbd..770c6ca1 100644 --- a/ap2_event_receiver.c +++ b/ap2_event_receiver.c @@ -30,12 +30,12 @@ #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) { diff --git a/ap2_rc_event_receiver.c b/ap2_rc_event_receiver.c index 74d03327..0fba8562 100644 --- a/ap2_rc_event_receiver.c +++ b/ap2_rc_event_receiver.c @@ -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 b188ac73..af517510 100644 --- 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", diff --git a/common.h b/common.h index 531f7f78..b887dbe2 100644 --- 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 diff --git a/player.c b/player.c index b2242b18..20366387 100644 --- 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: } diff --git a/player.h b/player.h index 767cd0e5..2f993c24 100644 --- 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 eff9fdf7..42e06b87 100644 --- 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); } diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 57a1fd66..8d2e2ad2 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -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. diff --git a/shairport.c b/shairport.c index 9964ce14..d3d8421a 100644 --- a/shairport.c +++ b/shairport.c @@ -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;