mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-27 23:55:57 -05: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"
|
||||
|
||||
#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
|
||||
static char *card_name;
|
||||
@ -59,6 +66,13 @@ enum alsa_state
|
||||
ALSA_STATE_FAILED = -1,
|
||||
};
|
||||
|
||||
enum alsa_sync_state
|
||||
{
|
||||
ALSA_SYNC_OK,
|
||||
ALSA_SYNC_AHEAD,
|
||||
ALSA_SYNC_BEHIND,
|
||||
};
|
||||
|
||||
struct alsa_session
|
||||
{
|
||||
enum alsa_state state;
|
||||
@ -68,6 +82,9 @@ struct alsa_session
|
||||
uint64_t 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
|
||||
// is prebuf_len (measured in rtp_packets)
|
||||
uint8_t *prebuf;
|
||||
@ -92,7 +109,6 @@ struct alsa_session
|
||||
extern struct event_base *evbase_player;
|
||||
|
||||
static struct alsa_session *sessions;
|
||||
static int sync_counter;
|
||||
|
||||
/* Forwards */
|
||||
static void
|
||||
@ -567,74 +583,33 @@ playback_start(struct alsa_session *as, uint64_t pos, uint64_t start_pos)
|
||||
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;
|
||||
int prebuffering;
|
||||
int prebuf_empty;
|
||||
int npackets;
|
||||
int latency;
|
||||
int diff;
|
||||
snd_pcm_sframes_t nsamp;
|
||||
snd_pcm_sframes_t ret;
|
||||
|
||||
prebuffering = (as->pos < as->start_pos);
|
||||
prebuf_empty = (as->prebuf_head == as->prebuf_tail);
|
||||
nsamp = AIRTUNES_V2_PACKET_SAMPLES;
|
||||
|
||||
as->pos += 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)
|
||||
if (prebuffering || !prebuf_empty || *avail < AIRTUNES_V2_PACKET_SAMPLES)
|
||||
{
|
||||
pkt = &as->prebuf[as->prebuf_head * PACKET_SIZE];
|
||||
|
||||
memcpy(pkt, buf, PACKET_SIZE);
|
||||
|
||||
as->prebuf_head = (as->prebuf_head + 1) % as->prebuf_len;
|
||||
}
|
||||
|
||||
if (prebuffering)
|
||||
return;
|
||||
if (prebuffering || *avail < AIRTUNES_V2_PACKET_SAMPLES)
|
||||
return 0; // No actual writing
|
||||
|
||||
ret = snd_pcm_avail_delay(hdl, &avail, &delay);
|
||||
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)
|
||||
{
|
||||
// We will now set buf so that we will transfer as much as possible to ALSA
|
||||
buf = &as->prebuf[as->prebuf_tail * PACKET_SIZE];
|
||||
|
||||
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;
|
||||
|
||||
nsamp = npackets * AIRTUNES_V2_PACKET_SAMPLES;
|
||||
while (nsamp > avail)
|
||||
while (nsamp > *avail)
|
||||
{
|
||||
npackets -= 1;
|
||||
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;
|
||||
}
|
||||
else
|
||||
nsamp = AIRTUNES_V2_PACKET_SAMPLES;
|
||||
|
||||
ret = snd_pcm_writei(hdl, buf, nsamp);
|
||||
if (ret < 0)
|
||||
goto alsa_error;
|
||||
else if (ret != nsamp)
|
||||
return ret;
|
||||
|
||||
if (ret != nsamp)
|
||||
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;
|
||||
|
||||
alsa_error:
|
||||
|
Loading…
Reference in New Issue
Block a user