Merge branch 'mikebrady:development' into development

This commit is contained in:
Charles
2022-01-28 21:52:51 +00:00
committed by GitHub
8 changed files with 154 additions and 100 deletions
+13 -22
View File
@@ -1,17 +1,6 @@
AirPlay 2
===
Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices, Macs, HomePod minis and Apple TVs. It does not work for iTunes on Windows.
The focus of the development effort has been on getting good audio performance for iOS and Mac. Features outside this focus may be missing or broken. So, for example, remote control doesn't work.
For AirPlay 2, Shairport Sync uses another application called [`nqptp`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. `nqptp` must run as `root` and must have exclusive access to ports `319` and `320`.
AirPlay 2 functionality is available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported.
The AirPlay 2 version of Shairport Sync will not work if installed on macOS. This is because [`nqptp`](https://github.com/mikebrady/nqptp) uses ports `319` and `320` but these ports are unavailable because there are used by macOS to support its implementation of AirPlay 2.
The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM.
Shairport Sync offers AirPlay 2 support for audio sources on iOS devices, Macs, HomePod minis and Apple TVs.
### What Works
- AirPlay 2 for iOS, HomePod mini, AppleTV and Mac players.
@@ -20,21 +9,19 @@ The AirPlay 2 build requires a good deal of extra library support and may not fi
- No Remote Control
- No AirPlay 2 for Windows iTunes
### What Partly Works
- Lossless or High Definition Lossless material over AirPlay 2 is automatically transcoded to ALAC or AAC before sending it via AirPlay 2 to Shairport Sync.
### General
Shairport Sync uses another application called [`nqptp`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation in AirPlay 2. `nqptp` must run as `root` and must have exclusive access to ports `319` and `320`.
### Limitations
- Only a single instance of Shairport Sync can run on a device.
- The AirPlay 2 version of Shairport Sync will not work if installed on macOS, since `nqptp` can not be installed on macOS.
Shairport Sync does not support lossless playback of Lossless or High Definition Lossless material. Instead, this material is automatically transcoded to AAC before sending it via AirPlay 2 to Shairport Sync.
AirPlay 2 -- What You Need
---
AirPlay 2 support needs a more powerful CPU for decoding and synchronisation and more memory for bigger buffers and larger libraries. Raspberry Pi OS, Ubuntu 20.04 on a VM and Ubuntu 20.04.2 64-bit Server Edition have been used extensively in development, with Alpine Linux and FreeBSD 12.2 being used to a lesser extent.
AirPlay 2 support needs a slightly more powerful CPU for decoding and synchronisation and more memory for bigger buffers and larger libraries. A Raspberry Pi 2 or Raspberry Pi Zero 2 W or better is required.
So, here are some guideline requirements:
* Full access, including `root` privileges, to a system at least as powerful as a Raspberry Pi 2.
Here are some guidelines:
* Full access, including `root` privileges, to a system at least as powerful as a Raspberry Pi 2 or a Raspberry Pi Zero 2 W.
* A fully up-to-date Linux. This is important, as some of the libraries must be the latest available.
* An audio output, for example an `alsa` device (or `sndio` in FreeBSD). The `stdout` and `pipe` backends continue to work as before. Other backends have not been tested.
* An audio output, for example an `alsa` device (or `sndio` in FreeBSD). You can use an application called [`sps-alsa-explore`](https://github.com/mikebrady/sps-alsa-explore) to test the suitability of hardware `alsa` audio devices on your device. Other backends continue to work as with "classic" Shairport Sync.
Guides
---
@@ -47,6 +34,10 @@ AirPlay 2 -- More About What Works
* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks.
* Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only.
Note
----
The functionality offered by Shairport Sync is the result of lots of study and analysis of the AirPlay 1 and AirPlay 2 protocols by many people over the years. These protocols have not been officially published, and there is no assurance that Shairport Sync will continue to work with AirPlay in future.
Acknowledgements
----
Huge thanks are due to a number of individuals who made direct and valuable contributions to Shairport Sync:
@@ -59,4 +50,4 @@ Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at
Finally
----
Did we mention AirPlay 2 support is experimental?
AirPlay 2 support is experimental.
+16
View File
@@ -1,3 +1,19 @@
Version-4.1-dev-173-gdeb11654
====
#### Enhancements
* Add some sanity checking of the latency offset and buffer size to check if they can be accommodated.
* Discard non-sentinel UDP timing packets that are received just when a timing thread starts -- they may relate to a prior session.
Version-4.1-dev-171-g703717a5
====
#### Bugfix
* Clear PTP clock information when a Realtime stream stops playing. This _may_ address one of the issues reported in [#1404](https://github.com/mikebrady/shairport-sync/issues/1404).
Version-4.1-dev-169-g6ced6bc6
====
#### Enhancement
* Add a new `mixer_control_index` setting to the `alsa` section of the configuration file. A mixer is fully identified by a name and index. The index defaults to zero, and, until now, it could only be set using the `alsa` command-line argument `-i`. Thanks to [flipoidix](https://github.com/flipoidix) for bringing this issue to notice.
Version-4.1-dev-164-gf79222f7
====
#### Bugfix
+7 -2
View File
@@ -298,10 +298,10 @@ int open_mixer() {
debug(1, "Failed to load mixer element");
response = -4;
} else {
debug(3, "Mixer Control name is \"%s\".", alsa_mix_ctrl);
debug(3, "Mixer control is \"%s\",%d.", alsa_mix_ctrl, alsa_mix_index);
alsa_mix_elem = snd_mixer_find_selem(alsa_mix_handle, alsa_mix_sid);
if (!alsa_mix_elem) {
warn("failed to find mixer control \"%s\".", alsa_mix_ctrl);
warn("failed to find mixer control \"%s\",%d.", alsa_mix_ctrl, alsa_mix_index);
response = -5;
} else {
response = 1; // we found a hardware mixer and successfully opened it
@@ -1057,6 +1057,11 @@ static int init(int argc, char **argv) {
alsa_mix_ctrl = (char *)str;
}
// Get the Mixer Control Index
if (config_lookup_int(config.cfg, "alsa.mixer_control_index", &value)) {
alsa_mix_index = value;
}
/* Get the disable_synchronization setting. */
if (config_lookup_string(config.cfg, "alsa.disable_synchronization", &str)) {
if (strcasecmp(str, "no") == 0)
+3
View File
@@ -1703,6 +1703,7 @@ void player_thread_cleanup_handler(void *arg) {
#ifdef CONFIG_AIRPLAY_2
}
// ptp_send_control_message_string("T"); // remove all timing peers to force the master to 0
reset_anchor_info(conn);
#endif
if (conn->outbuf) {
@@ -1742,6 +1743,8 @@ void *player_thread_func(void *arg) {
int previous_frames_played_valid = 0;
// pthread_cleanup_push(player_thread_initial_cleanup_handler, arg);
conn->latency_warning_issued =
0; // be permitted to generate a warning each time a play is attempted
conn->packet_count = 0;
conn->packet_count_since_flush = 0;
conn->previous_random_number = 0;
+5 -4
View File
@@ -145,10 +145,11 @@ typedef struct flush_request_t {
#endif
typedef struct {
int connection_number; // for debug ID purposes, nothing else...
int resend_interval; // this is really just for debugging
char *UserAgent; // free this on teardown
int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency
int connection_number; // for debug ID purposes, nothing else...
int resend_interval; // this is really just for debugging
char *UserAgent; // free this on teardown
int AirPlayVersion; // zero if not an AirPlay session. Used to help calculate latency
int latency_warning_issued;
uint32_t latency; // the actual latency used for this play session
uint32_t minimum_latency; // set if an a=min-latency: line appears in the ANNOUNCE message; zero
// otherwise
+107 -70
View File
@@ -1213,7 +1213,7 @@ void rtp_request_resend(seq_t first, uint32_t count, rtsp_conn_info *conn) {
void set_ptp_anchor_info(rtsp_conn_info *conn, uint64_t clock_id, uint32_t rtptime,
uint64_t networktime) {
if (conn->anchor_clock != clock_id) {
debug(2, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id);
debug(1, "Connection %d: Set Anchor Clock: %" PRIx64 ".", conn->connection_number, clock_id);
conn->anchor_clock_is_new = 1;
}
// debug(1,"set anchor info clock: %" PRIx64", rtptime: %u, networktime: %" PRIx64 ".", clock_id,
@@ -1426,7 +1426,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP,
debug(2, "Connection %d: No NQPTP master clock.", conn->connection_number);
break;
case clock_no_anchor_info:
debug(1, "Connection %d: No Clock Anchor.", conn->connection_number);
debug(1, "Connection %d: No clock anchor information.", conn->connection_number);
break;
case clock_version_mismatch:
debug(1, "Connection %d: NQPTP clock interface mismatch.", conn->connection_number);
@@ -1453,7 +1453,7 @@ int get_ptp_anchor_local_time_info(rtsp_conn_info *conn, uint32_t *anchorRTP,
if (anchorLocalTime != NULL)
*anchorLocalTime = conn->last_anchor_local_time;
}
return response;
}
@@ -1651,6 +1651,8 @@ void *rtp_ap2_control_receiver(void *arg) {
uint8_t packet[4096];
ssize_t nread;
int keep_going = 1;
uint64_t start_time = get_absolute_time_in_ns();
uint64_t packet_number = 0;
while (keep_going) {
SOCKADDR from_sock_addr;
socklen_t from_sock_addr_length = sizeof(SOCKADDR);
@@ -1658,87 +1660,122 @@ void *rtp_ap2_control_receiver(void *arg) {
nread = recvfrom(conn->ap2_control_socket, packet, sizeof(packet), 0,
(struct sockaddr *)&from_sock_addr, &from_sock_addr_length);
uint64_t time_now = get_absolute_time_in_ns();
int64_t time_since_start = time_now - start_time;
if (nread > 0) {
// debug(1,"rtp_ap2_control_receiver coded: %u, %u", packet[0], packet[1]);
if ((time_since_start < 2000000) && ((packet[0] & 0x10) == 0)) {
debug(1,
"Dropping what looks like a (non-sentinel) packet left over from a previous session "
"at %f ms.",
0.000001 * time_since_start);
} else {
packet_number++;
if ((config.diagnostic_drop_packet_fraction == 0.0) ||
(drand48() > config.diagnostic_drop_packet_fraction)) {
// store the from_sock_addr if we haven't already done so
// v remember to zero this when you're finished!
if (conn->ap2_remote_control_socket_addr_length == 0) {
memcpy(&conn->ap2_remote_control_socket_addr, &from_sock_addr, from_sock_addr_length);
conn->ap2_remote_control_socket_addr_length = from_sock_addr_length;
if (packet_number == 1) {
if ((packet[0] & 0x10) != 0) {
debug(2, "First packet is a sentinel packet.");
} else {
debug(1, "First packet is a not a sentinel packet!");
}
}
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);
// debug(1,"rtp_ap2_control_receiver coded: %u, %u", packet[0], packet[1]);
/*
char obf[4096];
char *obfp = obf;
int obfc;
for (obfc=0;obfc<nread;obfc++) {
snprintf(obfp, 3, "%02X", packet[obfc]);
obfp+=2;
};
*obfp=0;
debug(1,"AP2 Timing Control Received: \"%s\"",obf);
*/
if ((config.diagnostic_drop_packet_fraction == 0.0) ||
(drand48() > config.diagnostic_drop_packet_fraction)) {
// store the from_sock_addr if we haven't already done so
// v remember to zero this when you're finished!
if (conn->ap2_remote_control_socket_addr_length == 0) {
memcpy(&conn->ap2_remote_control_socket_addr, &from_sock_addr, from_sock_addr_length);
conn->ap2_remote_control_socket_addr_length = from_sock_addr_length;
}
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);
uint64_t remote_packet_time_ns = nctoh64(packet + 8);
uint64_t clock_id = nctoh64(packet + 20);
/*
char obf[4096];
char *obfp = obf;
int obfc;
for (obfc=0;obfc<nread;obfc++) {
snprintf(obfp, 3, "%02X", packet[obfc]);
obfp+=2;
};
*obfp=0;
debug(1,"AP2 Timing Control Received: \"%s\"",obf);
*/
// debug(1, "we have clock_id: %" PRIx64 ".", clock_id);
// debug(1,"remote_packet_time_ns: %" PRIx64 ", local_realtime_now_ns: %" PRIx64 ".",
// remote_packet_time_ns, local_realtime_now);
uint32_t frame_1 =
nctohl(packet + 4); // this seems to be the frame with latency of 77165 included
uint32_t frame_2 = nctohl(packet + 16); // this seems to be the frame the time refers to
// this just updates the anchor information contained in the packet
// the frame and its remote time
// add in the audio_backend_latency_offset;
int32_t notified_latency = frame_2 - frame_1;
int32_t added_latency = (int32_t)(config.audio_backend_latency_offset * conn->input_rate);
// the actual latency is the notified latency plus the fixed latency + the added latency
uint64_t remote_packet_time_ns = nctoh64(packet + 8);
uint64_t clock_id = nctoh64(packet + 20);
if (added_latency < (-(notified_latency + 11035)))
debug(1, "the audio_backend_latency_offset is causing a negative latency!");
// debug(1, "we have clock_id: %" PRIx64 ".", clock_id);
// debug(1,"remote_packet_time_ns: %" PRIx64 ", local_realtime_now_ns: %" PRIx64 ".",
// remote_packet_time_ns, local_realtime_now);
uint32_t frame_1 =
nctohl(packet + 4); // this seems to be the frame with latency of 77165 included
uint32_t frame_2 = nctohl(packet + 16); // this seems to be the frame the time refers to
// this just updates the anchor information contained in the packet
// the frame and its remote time
// add in the audio_backend_latency_offset;
int32_t notified_latency = frame_2 - frame_1;
int32_t added_latency =
(int32_t)(config.audio_backend_latency_offset * conn->input_rate);
// the actual latency is the notified latency plus the fixed latency + the added latency
/*
debug_mutex_lock(&conn->reference_time_mutex, 1000, 0);
conn->remote_reference_timestamp_time = remote_packet_time_ns;
conn->reference_timestamp =
frame_1 - 11035 - added_latency; // add the latency in to the anchortime
debug_mutex_unlock(&conn->reference_time_mutex, 0);
*/
// this is now only used for calculating when to ask for resends
conn->latency = notified_latency + 11035 + added_latency;
// debug(1,"conn->latency is %d.", conn->latency);
set_ptp_anchor_info(conn, clock_id, frame_1 - 11035 - added_latency,
remote_packet_time_ns);
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 * conn->input_rate);
// debug(1, "Net latency is %d frames.", net_latency);
} break;
case 0xd6:
// six bytes in is the sequence number at the start of the encrypted audio packet
// returns the sequence number but we're not really interested
decipher_player_put_packet(packet + 6, nread - 6, conn);
break;
default: {
char *packet_in_hex_cstring =
debug_malloc_hex_cstring(packet, nread); // remember to free this afterwards
debug(1,
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, 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);
conn->latency_warning_issued = 1;
}
conn->latency = notified_latency + 11035;
} else {
conn->latency = notified_latency + 11035 + added_latency;
}
/*
debug_mutex_lock(&conn->reference_time_mutex, 1000, 0);
conn->remote_reference_timestamp_time = remote_packet_time_ns;
conn->reference_timestamp =
frame_1 - 11035 - added_latency; // add the latency in to the anchortime
debug_mutex_unlock(&conn->reference_time_mutex, 0);
*/
// this is now only used for calculating when to ask for resends
// debug(1, "conn->latency is %d.", conn->latency);
set_ptp_anchor_info(conn, clock_id, frame_1 - 11035 - added_latency,
remote_packet_time_ns);
} break;
case 0xd6:
// six bytes in is the sequence number at the start of the encrypted audio packet
// returns the sequence number but we're not really interested
decipher_player_put_packet(packet + 6, nread - 6, conn);
break;
default: {
char *packet_in_hex_cstring =
debug_malloc_hex_cstring(packet, nread); // remember to free this afterwards
debug(
1,
"AP2 Control Receiver Packet of first byte 0x%02X, type 0x%02X length %d received: "
"\"%s\".",
packet[0], packet[1], nread, packet_in_hex_cstring);
free(packet_in_hex_cstring);
} break;
free(packet_in_hex_cstring);
} break;
}
} else {
debug(1, "AP2 Control Receiver -- dropping a packet.");
}
} else {
debug(1, "AP2 Control Receiver -- dropping a packet.");
}
} else if (nread == 0) {
debug(1, "AP2 Control Receiver -- connection closed.");
+2 -1
View File
@@ -98,7 +98,8 @@ sessioncontrol =
alsa =
{
// output_device = "default"; // the name of the alsa output device. Use "shairport-sync -h" to discover the names of ALSA hardware devices. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc.
// mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software.
// mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. No default. If not specified, no mixer is used and volume in adjusted in software.
// mixer_control_index = 0; // the index of the mixer to use to adjust output volume. Default is 0. The mixer is fully identified by the combination of the mixer_control_name and the mixer_control_index, e.g. "PCM",0 would be such a specification.
// mixer_device = "default"; // the mixer_device default is whatever the output_device is. Normally you wouldn't have to use this.
// output_rate = "auto"; // can be "auto", 44100, 88200, 176400 or 352800, but the device must have the capability.
+1 -1
View File
@@ -416,7 +416,7 @@ int parse_options(int argc, char **argv) {
config.tolerance = 1.0 * fTolerance / 44100;
config.audio_backend_silent_lead_in_time_auto =
1; // start outputting silence as soon as packets start arriving
config.airplay_volume = -18.0; // if no volume is ever set, default to initial default value if
config.airplay_volume = -24.0; // if no volume is ever set, default to initial default value if
// nothing else comes in first.
config.fixedLatencyOffset = 11025; // this sounds like it works properly.
config.diagnostic_drop_packet_fraction = 0.0;