mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-29 00:23:23 -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"
|
#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…
Reference in New Issue
Block a user