mirror of
				https://github.com/owntone/owntone-server.git
				synced 2025-10-29 15:55:02 -04:00 
			
		
		
		
	[alsa] Add low-tech sync with the player (and AirPlay)
This commit is contained in:
		
							parent
							
								
									eca41e306e
								
							
						
					
					
						commit
						632bfd9a33
					
				| @ -38,6 +38,13 @@ | |||||||
| #include "outputs.h" | #include "outputs.h" | ||||||
| 
 | 
 | ||||||
| #define PACKET_SIZE STOB(AIRTUNES_V2_PACKET_SAMPLES) | #define PACKET_SIZE STOB(AIRTUNES_V2_PACKET_SAMPLES) | ||||||
|  | // The maximum number of samples that the output is allowed to get behind (or
 | ||||||
|  | // ahead) of the player position, before compensation is attempted
 | ||||||
|  | #define ALSA_MAX_LATENCY 352 | ||||||
|  | // If latency is jumping up and down we don't do compensation since we probably
 | ||||||
|  | // wouldn't do a good job. This sets the maximum the latency is allowed to vary
 | ||||||
|  | // within the 10 seconds where we measure latency each second.
 | ||||||
|  | #define ALSA_MAX_LATENCY_VARIANCE 100 | ||||||
| 
 | 
 | ||||||
| // TODO Unglobalise these and add support for multiple sound cards
 | // TODO Unglobalise these and add support for multiple sound cards
 | ||||||
| static char *card_name; | static char *card_name; | ||||||
| @ -59,6 +66,13 @@ enum alsa_state | |||||||
|   ALSA_STATE_FAILED    = -1, |   ALSA_STATE_FAILED    = -1, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | enum alsa_sync_state | ||||||
|  | { | ||||||
|  |   ALSA_SYNC_OK, | ||||||
|  |   ALSA_SYNC_AHEAD, | ||||||
|  |   ALSA_SYNC_BEHIND, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| struct alsa_session | struct alsa_session | ||||||
| { | { | ||||||
|   enum alsa_state state; |   enum alsa_state state; | ||||||
| @ -68,6 +82,9 @@ struct alsa_session | |||||||
|   uint64_t pos; |   uint64_t pos; | ||||||
|   uint64_t start_pos; |   uint64_t start_pos; | ||||||
| 
 | 
 | ||||||
|  |   int32_t last_latency; | ||||||
|  |   int sync_counter; | ||||||
|  | 
 | ||||||
|   // An array that will hold the packets we prebuffer. The length of the array
 |   // An array that will hold the packets we prebuffer. The length of the array
 | ||||||
|   // is prebuf_len (measured in rtp_packets)
 |   // is prebuf_len (measured in rtp_packets)
 | ||||||
|   uint8_t *prebuf; |   uint8_t *prebuf; | ||||||
| @ -92,7 +109,6 @@ struct alsa_session | |||||||
| extern struct event_base *evbase_player; | extern struct event_base *evbase_player; | ||||||
| 
 | 
 | ||||||
| static struct alsa_session *sessions; | static struct alsa_session *sessions; | ||||||
| static int sync_counter; |  | ||||||
| 
 | 
 | ||||||
| /* Forwards */ | /* Forwards */ | ||||||
| static void | static void | ||||||
| @ -567,74 +583,33 @@ playback_start(struct alsa_session *as, uint64_t pos, uint64_t start_pos) | |||||||
|   as->state = ALSA_STATE_STREAMING; |   as->state = ALSA_STATE_STREAMING; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void | 
 | ||||||
| playback_write(struct alsa_session *as, uint8_t *buf, uint64_t rtptime) | // This function writes the sample buf into either the prebuffer or directly to
 | ||||||
|  | // ALSA, depending on how much room there is in ALSA, and whether we are
 | ||||||
|  | // prebuffering or not. It also transfers from the the prebuffer to ALSA, if
 | ||||||
|  | // needed. Returns 0 on success, negative on error.
 | ||||||
|  | static int | ||||||
|  | buffer_write(struct alsa_session *as, uint8_t *buf, snd_pcm_sframes_t *avail, int prebuffering, int prebuf_empty) | ||||||
| { | { | ||||||
|   snd_pcm_sframes_t ret; |  | ||||||
|   snd_pcm_sframes_t avail; |  | ||||||
|   snd_pcm_sframes_t delay; |  | ||||||
|   snd_pcm_sframes_t nsamp; |  | ||||||
|   struct timespec now; |  | ||||||
|   uint64_t pb_pos; |  | ||||||
|   uint64_t cur_pos; |  | ||||||
|   uint8_t *pkt; |   uint8_t *pkt; | ||||||
|   int prebuffering; |  | ||||||
|   int prebuf_empty; |  | ||||||
|   int npackets; |   int npackets; | ||||||
|   int latency; |   snd_pcm_sframes_t nsamp; | ||||||
|   int diff; |   snd_pcm_sframes_t ret; | ||||||
| 
 | 
 | ||||||
|   prebuffering = (as->pos < as->start_pos); |   nsamp = AIRTUNES_V2_PACKET_SAMPLES; | ||||||
|   prebuf_empty = (as->prebuf_head == as->prebuf_tail); |  | ||||||
| 
 | 
 | ||||||
|   as->pos += AIRTUNES_V2_PACKET_SAMPLES; |   if (prebuffering || !prebuf_empty || *avail < AIRTUNES_V2_PACKET_SAMPLES) | ||||||
| 
 |  | ||||||
|   // We need to copy to the prebuffer if we are prebuffering OR if the
 |  | ||||||
|   // prebuffer has not been emptied yet
 |  | ||||||
|   if (prebuffering || !prebuf_empty) |  | ||||||
|     { |     { | ||||||
|       pkt = &as->prebuf[as->prebuf_head * PACKET_SIZE]; |       pkt = &as->prebuf[as->prebuf_head * PACKET_SIZE]; | ||||||
| 
 | 
 | ||||||
|       memcpy(pkt, buf, PACKET_SIZE); |       memcpy(pkt, buf, PACKET_SIZE); | ||||||
| 
 | 
 | ||||||
|       as->prebuf_head = (as->prebuf_head + 1) % as->prebuf_len; |       as->prebuf_head = (as->prebuf_head + 1) % as->prebuf_len; | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|   if (prebuffering) |       if (prebuffering || *avail < AIRTUNES_V2_PACKET_SAMPLES) | ||||||
|     return; | 	return 0; // No actual writing
 | ||||||
| 
 | 
 | ||||||
|   ret = snd_pcm_avail_delay(hdl, &avail, &delay); |       // We will now set buf so that we will transfer as much as possible to ALSA
 | ||||||
|   if (ret < 0) |  | ||||||
|     goto alsa_error; |  | ||||||
| 
 |  | ||||||
|   if (avail < AIRTUNES_V2_PACKET_SAMPLES) |  | ||||||
|     return; |  | ||||||
| 
 |  | ||||||
|   sync_counter++; |  | ||||||
|   if (sync_counter >= 126) |  | ||||||
|     { |  | ||||||
|       sync_counter = 0; |  | ||||||
| 
 |  | ||||||
|       if (!prebuf_empty) |  | ||||||
| 	npackets = (as->prebuf_head - (as->prebuf_tail + 1) + as->prebuf_len) % as->prebuf_len + 1; |  | ||||||
|       else |  | ||||||
| 	npackets = 0; |  | ||||||
| 
 |  | ||||||
|       pb_pos = rtptime - delay - AIRTUNES_V2_PACKET_SAMPLES * npackets; |  | ||||||
|       ret = player_get_current_pos(&cur_pos, &now, 0); // TODO commit?
 |  | ||||||
|       if (ret == 0) |  | ||||||
| 	latency = cur_pos - pb_pos; |  | ||||||
|       else |  | ||||||
| 	latency = 0; |  | ||||||
| 
 |  | ||||||
|       diff = cur_pos - as->pos; |  | ||||||
|       if (latency) |  | ||||||
| 	DPRINTF(E_DBG, L_LAUDIO, "Sync to cur_pos %" PRIu64 ", pb_pos %" PRIu64 " (diff %d, delay %li), pos %" PRIu64 " (diff %d)\n", cur_pos, pb_pos, latency, delay, as->pos, diff); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|   // If we have data in prebuf we send as much as we can
 |  | ||||||
|   if (!prebuf_empty) |  | ||||||
|     { |  | ||||||
|       buf = &as->prebuf[as->prebuf_tail * PACKET_SIZE]; |       buf = &as->prebuf[as->prebuf_tail * PACKET_SIZE]; | ||||||
| 
 | 
 | ||||||
|       if (as->prebuf_head > as->prebuf_tail) |       if (as->prebuf_head > as->prebuf_tail) | ||||||
| @ -643,7 +618,7 @@ playback_write(struct alsa_session *as, uint8_t *buf, uint64_t rtptime) | |||||||
| 	npackets = as->prebuf_len - as->prebuf_tail; | 	npackets = as->prebuf_len - as->prebuf_tail; | ||||||
| 
 | 
 | ||||||
|       nsamp = npackets * AIRTUNES_V2_PACKET_SAMPLES; |       nsamp = npackets * AIRTUNES_V2_PACKET_SAMPLES; | ||||||
|       while (nsamp > avail) |       while (nsamp > *avail) | ||||||
| 	{ | 	{ | ||||||
| 	  npackets -= 1; | 	  npackets -= 1; | ||||||
| 	  nsamp -= AIRTUNES_V2_PACKET_SAMPLES; | 	  nsamp -= AIRTUNES_V2_PACKET_SAMPLES; | ||||||
| @ -651,15 +626,111 @@ playback_write(struct alsa_session *as, uint8_t *buf, uint64_t rtptime) | |||||||
| 
 | 
 | ||||||
|       as->prebuf_tail = (as->prebuf_tail + npackets) % as->prebuf_len; |       as->prebuf_tail = (as->prebuf_tail + npackets) % as->prebuf_len; | ||||||
|     } |     } | ||||||
|   else |  | ||||||
|     nsamp = AIRTUNES_V2_PACKET_SAMPLES; |  | ||||||
| 
 | 
 | ||||||
|   ret = snd_pcm_writei(hdl, buf, nsamp); |   ret = snd_pcm_writei(hdl, buf, nsamp); | ||||||
|   if (ret < 0) |   if (ret < 0) | ||||||
|     goto alsa_error; |     return ret; | ||||||
|   else if (ret != nsamp) | 
 | ||||||
|  |   if (ret != nsamp) | ||||||
|     DPRINTF(E_WARN, L_LAUDIO, "ALSA partial write detected\n"); |     DPRINTF(E_WARN, L_LAUDIO, "ALSA partial write detected\n"); | ||||||
| 
 | 
 | ||||||
|  |   *avail -= ret; | ||||||
|  | 
 | ||||||
|  |   return 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Checks if ALSA's playback position is ahead or behind the player's
 | ||||||
|  | enum alsa_sync_state | ||||||
|  | sync_check(struct alsa_session *as, uint64_t rtptime, snd_pcm_sframes_t delay, int prebuf_empty) | ||||||
|  | { | ||||||
|  |   enum alsa_sync_state sync; | ||||||
|  |   struct timespec now; | ||||||
|  |   uint64_t cur_pos; | ||||||
|  |   uint64_t pb_pos; | ||||||
|  |   int32_t latency; | ||||||
|  |   int npackets; | ||||||
|  | 
 | ||||||
|  |   sync = ALSA_SYNC_OK; | ||||||
|  | 
 | ||||||
|  |   if (player_get_current_pos(&cur_pos, &now, 0) != 0) | ||||||
|  |     return sync; | ||||||
|  | 
 | ||||||
|  |   if (!prebuf_empty) | ||||||
|  |     npackets = (as->prebuf_head - (as->prebuf_tail + 1) + as->prebuf_len) % as->prebuf_len + 1; | ||||||
|  |   else | ||||||
|  |     npackets = 0; | ||||||
|  | 
 | ||||||
|  |   pb_pos = rtptime - delay - AIRTUNES_V2_PACKET_SAMPLES * npackets; | ||||||
|  |   latency = cur_pos - pb_pos; | ||||||
|  | 
 | ||||||
|  |   // If the latency is low or very different from our last measurement, we reset the sync_counter
 | ||||||
|  |   if (abs(latency) < ALSA_MAX_LATENCY || abs(as->last_latency - latency) > ALSA_MAX_LATENCY_VARIANCE) | ||||||
|  |     { | ||||||
|  |       as->sync_counter = 0; | ||||||
|  |       sync = ALSA_SYNC_OK; | ||||||
|  |     } | ||||||
|  |   // If we have measured a consistent latency for 10 seconds, then we take action
 | ||||||
|  |   else if (as->sync_counter >= 10 * 126) | ||||||
|  |     { | ||||||
|  |       DPRINTF(E_INFO, L_LAUDIO, "Taking action to compensate for ALSA latency of %d samples\n", latency); | ||||||
|  | 
 | ||||||
|  |       as->sync_counter = 0; | ||||||
|  |       if (latency > 0) | ||||||
|  | 	sync = ALSA_SYNC_BEHIND; | ||||||
|  |       else | ||||||
|  | 	sync = ALSA_SYNC_AHEAD; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |   as->last_latency = latency; | ||||||
|  | 
 | ||||||
|  |   if (latency) | ||||||
|  |     DPRINTF(E_DBG, L_LAUDIO, "Sync %d cur_pos %" PRIu64 ", pb_pos %" PRIu64 " (diff %d, delay %li), pos %" PRIu64 "\n", sync, cur_pos, pb_pos, latency, delay, as->pos); | ||||||
|  | 
 | ||||||
|  |   return sync; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | playback_write(struct alsa_session *as, uint8_t *buf, uint64_t rtptime) | ||||||
|  | { | ||||||
|  |   snd_pcm_sframes_t ret; | ||||||
|  |   snd_pcm_sframes_t avail; | ||||||
|  |   snd_pcm_sframes_t delay; | ||||||
|  |   enum alsa_sync_state sync; | ||||||
|  |   int prebuffering; | ||||||
|  |   int prebuf_empty; | ||||||
|  | 
 | ||||||
|  |   prebuffering = (as->pos < as->start_pos); | ||||||
|  |   prebuf_empty = (as->prebuf_head == as->prebuf_tail); | ||||||
|  | 
 | ||||||
|  |   as->pos += AIRTUNES_V2_PACKET_SAMPLES; | ||||||
|  | 
 | ||||||
|  |   if (prebuffering) | ||||||
|  |     { | ||||||
|  |       buffer_write(as, buf, NULL, prebuffering, prebuf_empty); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |   ret = snd_pcm_avail_delay(hdl, &avail, &delay); | ||||||
|  |   if (ret < 0) | ||||||
|  |     goto alsa_error; | ||||||
|  | 
 | ||||||
|  |   // Every second we do a sync check
 | ||||||
|  |   sync = ALSA_SYNC_OK; | ||||||
|  |   as->sync_counter++; | ||||||
|  |   if (as->sync_counter % 126 == 0) | ||||||
|  |     sync = sync_check(as, rtptime, delay, prebuf_empty); | ||||||
|  | 
 | ||||||
|  |   // Skip write -> reduce the delay
 | ||||||
|  |   if (sync == ALSA_SYNC_BEHIND) | ||||||
|  |     return; | ||||||
|  | 
 | ||||||
|  |   ret = buffer_write(as, buf, &avail, prebuffering, prebuf_empty); | ||||||
|  |   // Double write -> increase the delay
 | ||||||
|  |   if (sync == ALSA_SYNC_AHEAD && (ret == 0)) | ||||||
|  |     ret = buffer_write(as, buf, &avail, prebuffering, prebuf_empty); | ||||||
|  |   if (ret < 0) | ||||||
|  |     goto alsa_error; | ||||||
|  | 
 | ||||||
|   return; |   return; | ||||||
| 
 | 
 | ||||||
|  alsa_error: |  alsa_error: | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user