mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-26 07:05:57 -05:00
[alsa] New resample-based sync correction
This commit is contained in:
parent
781a3c16ed
commit
02cd65a992
@ -223,11 +223,6 @@ audio {
|
||||
# ahead, positive correspond to delaying it. The unit is milliseconds.
|
||||
# The offset must be between -1000 and 1000 (+/- 1 sec).
|
||||
# offset_ms = 0
|
||||
|
||||
# How often to check and correct for drift between ALSA and AirPlay.
|
||||
# The value is an integer expressed in seconds.
|
||||
# Clamped to the range 1..20.
|
||||
# adjust_period_seconds = 10
|
||||
}
|
||||
|
||||
# Pipe output
|
||||
|
@ -117,7 +117,6 @@ static cfg_opt_t sec_audio[] =
|
||||
CFG_STR("mixer_device", NULL, CFGF_NONE),
|
||||
CFG_INT("offset", 0, CFGF_NONE), // deprecated
|
||||
CFG_INT("offset_ms", 0, CFGF_NONE),
|
||||
CFG_INT("adjust_period_seconds", 10, CFGF_NONE),
|
||||
CFG_END()
|
||||
};
|
||||
|
||||
|
35
src/misc.c
35
src/misc.c
@ -1039,6 +1039,41 @@ murmur_hash64(const void *key, int len, uint32_t seed)
|
||||
# error Platform not supported
|
||||
#endif
|
||||
|
||||
|
||||
int
|
||||
linear_regression(double *m, double *b, double *r2, const double *x, const double *y, int n)
|
||||
{
|
||||
double x_val;
|
||||
double sum_x = 0;
|
||||
double sum_x2 = 0;
|
||||
double sum_y = 0;
|
||||
double sum_y2 = 0;
|
||||
double sum_xy = 0;
|
||||
double denom;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < n; i++)
|
||||
{
|
||||
x_val = x ? x[i] : (double)i;
|
||||
sum_x += x_val;
|
||||
sum_x2 += x_val * x_val;
|
||||
sum_y += y[i];
|
||||
sum_y2 += y[i] * y[i];
|
||||
sum_xy += x_val * y[i];
|
||||
}
|
||||
|
||||
denom = (n * sum_x2 - sum_x * sum_x);
|
||||
if (denom == 0)
|
||||
return -1;
|
||||
|
||||
*m = (n * sum_xy - sum_x * sum_y) / denom;
|
||||
*b = (sum_y * sum_x2 - sum_x * sum_xy) / denom;
|
||||
if (r2)
|
||||
*r2 = (sum_xy - (sum_x * sum_y)/n) * (sum_xy - (sum_x * sum_y)/n) / ((sum_x2 - (sum_x * sum_x)/n) * (sum_y2 - (sum_y * sum_y)/n));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool
|
||||
quality_is_equal(struct media_quality *a, struct media_quality *b)
|
||||
{
|
||||
|
@ -139,6 +139,9 @@ b64_encode(const uint8_t *in, size_t len);
|
||||
uint64_t
|
||||
murmur_hash64(const void *key, int len, uint32_t seed);
|
||||
|
||||
int
|
||||
linear_regression(double *m, double *b, double *r, const double *x, const double *y, int n);
|
||||
|
||||
bool
|
||||
quality_is_equal(struct media_quality *a, struct media_quality *b);
|
||||
|
||||
|
@ -37,13 +37,28 @@
|
||||
#include "player.h"
|
||||
#include "outputs.h"
|
||||
|
||||
// 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 480
|
||||
// We measure latency each second, and after a number of measurements determined
|
||||
// by adjust_period_seconds we try to determine drift and latency. If both are
|
||||
// below the two thresholds set by the below, we don't do anything. Otherwise we
|
||||
// may attempt compensation by resampling. Latency is measured in samples, and
|
||||
// drift is change of latency per second. Both are floats.
|
||||
#define ALSA_MAX_LATENCY 480.0
|
||||
#define ALSA_MAX_DRIFT 16.0
|
||||
// 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 480
|
||||
// wouldn't do a good job. We use linear regression to determine the trend, but
|
||||
// if r2 is below this value we won't attempt to correct sync.
|
||||
#define ALSA_MAX_VARIANCE 0.2
|
||||
|
||||
// How many latency calculations we keep in the latency_history buffer
|
||||
#define ALSA_LATENCY_HISTORY_SIZE 100
|
||||
|
||||
// We correct latency by adjusting the sample rate in steps. However, if the
|
||||
// latency keeps drifting we give up after reaching this step.
|
||||
#define ALSA_RESAMPLE_STEP_MAX 8
|
||||
// The sample rate gets adjusted by a multiple of this number. The number of
|
||||
// multiples depends on the sample rate, i.e. a low sample rate may get stepped
|
||||
// by 16, while high one would get stepped by 4 x 16
|
||||
#define ALSA_RESAMPLE_STEP_MULTIPLE 2
|
||||
|
||||
#define ALSA_F_STARTED (1 << 15)
|
||||
|
||||
@ -77,17 +92,23 @@ struct alsa_session
|
||||
uint32_t last_pos;
|
||||
uint32_t last_buflen;
|
||||
|
||||
struct timespec start_pts;
|
||||
struct timespec last_pts;
|
||||
|
||||
int last_latency;
|
||||
int sync_counter;
|
||||
// Used for syncing with the clock
|
||||
struct timespec stamp_pts;
|
||||
uint64_t stamp_pos;
|
||||
|
||||
// Array of latency calculations, where latency_counter tells how many are
|
||||
// currently in the array
|
||||
double latency_history[ALSA_LATENCY_HISTORY_SIZE];
|
||||
int latency_counter;
|
||||
|
||||
int sync_resample_step;
|
||||
|
||||
// Here we buffer samples during startup
|
||||
struct ringbuffer prebuf;
|
||||
|
||||
int offset_ms;
|
||||
int adjust_period_seconds;
|
||||
|
||||
int volume;
|
||||
long vol_min;
|
||||
@ -511,7 +532,7 @@ playback_restart(struct alsa_session *as, struct output_buffer *obuf)
|
||||
// Time stamps used for syncing, here we set when playback should start
|
||||
ts.tv_sec = OUTPUTS_BUFFER_DURATION;
|
||||
ts.tv_nsec = (uint64_t)as->offset_ms * 1000000UL;
|
||||
as->start_pts = timespec_add(obuf->pts, ts);
|
||||
as->stamp_pts = timespec_add(obuf->pts, ts);
|
||||
|
||||
// The difference between pos and start pos should match the 2 second buffer
|
||||
// that AirPlay uses (OUTPUTS_BUFFER_DURATION) + user configured offset_ms. We
|
||||
@ -587,71 +608,117 @@ buffer_write(struct alsa_session *as, struct output_data *odata, snd_pcm_sframes
|
||||
}
|
||||
|
||||
static enum alsa_sync_state
|
||||
sync_check(struct alsa_session *as)
|
||||
sync_check(double *drift, double *latency, struct alsa_session *as, snd_pcm_sframes_t delay)
|
||||
{
|
||||
enum alsa_sync_state sync;
|
||||
snd_pcm_sframes_t delay;
|
||||
struct timespec ts;
|
||||
int elapsed;
|
||||
uint64_t cur_pos;
|
||||
uint64_t exp_pos;
|
||||
int32_t latency;
|
||||
int32_t diff;
|
||||
double r2;
|
||||
int ret;
|
||||
|
||||
as->sync_counter++;
|
||||
|
||||
ret = snd_pcm_delay(as->hdl, &delay);
|
||||
if (ret < 0)
|
||||
return ALSA_SYNC_OK;
|
||||
|
||||
// Would be nice to use snd_pcm_status_get_audio_htstamp here, but it doesn't
|
||||
// seem to be supported on my computer
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
|
||||
// Here we calculate elapsed time since playback was supposed to start, taking
|
||||
// into account buffer time and configuration of offset_ms. We then calculate
|
||||
// our expected position based on elapsed time, and if it is different from
|
||||
// where we are + what is the buffers, then ALSA is out of sync.
|
||||
elapsed = (ts.tv_sec - as->start_pts.tv_sec) * 1000L + (ts.tv_nsec - as->start_pts.tv_nsec) / 1000000;
|
||||
// Here we calculate elapsed time since last reference position (which is
|
||||
// equal to playback start time, unless we have reset due to sync correction),
|
||||
// taking into account buffer time and configuration of offset_ms. We then
|
||||
// calculate our expected position based on elapsed time, and if different
|
||||
// from where we are + what is in the buffers then ALSA is out of sync.
|
||||
elapsed = (ts.tv_sec - as->stamp_pts.tv_sec) * 1000L + (ts.tv_nsec - as->stamp_pts.tv_nsec) / 1000000;
|
||||
if (elapsed < 0)
|
||||
return ALSA_SYNC_OK;
|
||||
|
||||
cur_pos = (uint64_t)as->pos - (delay + BTOS(as->prebuf.read_avail, as->quality.bits_per_sample, as->quality.channels));
|
||||
cur_pos = (uint64_t)as->pos - as->stamp_pos - (delay + BTOS(as->prebuf.read_avail, as->quality.bits_per_sample, as->quality.channels));
|
||||
exp_pos = (uint64_t)elapsed * as->quality.sample_rate / 1000;
|
||||
latency = cur_pos - exp_pos;
|
||||
diff = cur_pos - exp_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)
|
||||
DPRINTF(E_DBG, L_LAUDIO, "counter %d/%d, stamp %lu:%lu, now %lu:%lu, elapsed is %d ms, cur_pos=%" PRIu64 ", exp_pos=%" PRIu64 ", diff=%d\n",
|
||||
as->latency_counter, ALSA_LATENCY_HISTORY_SIZE, as->stamp_pts.tv_sec, as->stamp_pts.tv_nsec / 1000000, ts.tv_sec, ts.tv_nsec / 1000000, elapsed, cur_pos, exp_pos, diff);
|
||||
|
||||
// Add the latency to our measurement history
|
||||
as->latency_history[as->latency_counter] = (double)diff;
|
||||
as->latency_counter++;
|
||||
|
||||
// Haven't collected enough samples for sync evaluation yet, so just return
|
||||
if (as->latency_counter < ALSA_LATENCY_HISTORY_SIZE)
|
||||
return ALSA_SYNC_OK;
|
||||
|
||||
as->latency_counter = 0;
|
||||
|
||||
ret = linear_regression(drift, latency, &r2, NULL, as->latency_history, ALSA_LATENCY_HISTORY_SIZE);
|
||||
if (ret < 0)
|
||||
{
|
||||
as->sync_counter = 0;
|
||||
DPRINTF(E_WARN, L_LAUDIO, "Linear regression of collected latency samples failed\n");
|
||||
return ALSA_SYNC_OK;
|
||||
}
|
||||
|
||||
// Set *latency to the "average" within the period
|
||||
*latency = (*drift) * ALSA_LATENCY_HISTORY_SIZE / 2 + (*latency);
|
||||
|
||||
if (abs(*latency) < ALSA_MAX_LATENCY && abs(*drift) < ALSA_MAX_DRIFT)
|
||||
sync = ALSA_SYNC_OK; // If both latency and drift are within thresholds -> no action
|
||||
else if (*latency > 0 && *drift > 0)
|
||||
sync = ALSA_SYNC_AHEAD;
|
||||
else if (*latency < 0 && *drift < 0)
|
||||
sync = ALSA_SYNC_BEHIND;
|
||||
else
|
||||
sync = ALSA_SYNC_OK; // Drift is counteracting latency -> no action
|
||||
|
||||
if (sync != ALSA_SYNC_OK && r2 < ALSA_MAX_VARIANCE)
|
||||
{
|
||||
DPRINTF(E_DBG, L_LAUDIO, "Too much variance in latency measurements (r2=%f/%f), won't try to compensate\n", r2, ALSA_MAX_VARIANCE);
|
||||
sync = ALSA_SYNC_OK;
|
||||
}
|
||||
// If we have measured a consistent latency for configured period, then we take action
|
||||
else if (as->sync_counter >= as->adjust_period_seconds)
|
||||
{
|
||||
as->sync_counter = 0;
|
||||
if (latency < 0)
|
||||
sync = ALSA_SYNC_BEHIND;
|
||||
else
|
||||
sync = ALSA_SYNC_AHEAD;
|
||||
}
|
||||
else
|
||||
sync = ALSA_SYNC_OK;
|
||||
|
||||
// The will be used by sync_correct, so it knows how much we are out of sync
|
||||
as->last_latency = latency;
|
||||
|
||||
DPRINTF(E_DBG, L_LAUDIO, "start %lu:%lu, now %lu:%lu, elapsed is %d ms, cur_pos=%" PRIu64 ", exp_pos=%" PRIu64 ", latency=%d\n",
|
||||
as->start_pts.tv_sec, as->start_pts.tv_nsec / 1000000, ts.tv_sec, ts.tv_nsec / 1000000, elapsed, cur_pos, exp_pos, latency);
|
||||
DPRINTF(E_DBG, L_LAUDIO, "Sync check result: drift=%f, latency=%f, r2=%f, sync=%d\n", *drift, *latency, r2, sync);
|
||||
|
||||
return sync;
|
||||
}
|
||||
|
||||
static void
|
||||
sync_correct(struct alsa_session *as)
|
||||
sync_correct(struct alsa_session *as, double drift, double latency, struct timespec pts, snd_pcm_sframes_t delay)
|
||||
{
|
||||
DPRINTF(E_INFO, L_LAUDIO, "Here we should take action to compensate for ALSA latency of %d samples\n", as->last_latency);
|
||||
// Not implemented yet
|
||||
int step;
|
||||
int sign;
|
||||
|
||||
// We change the sample_rate in steps that are a multiple of 50. So we might
|
||||
// step 44100 -> 44000 -> 40900 -> 44000 -> 44100. If we used percentages to
|
||||
// to step, we would have to deal with rounding; we don't want to step 44100
|
||||
// -> 39996 -> 44099.
|
||||
step = ALSA_RESAMPLE_STEP_MULTIPLE * (as->quality.sample_rate / 20000);
|
||||
|
||||
sign = (drift < 0) ? -1 : 1;
|
||||
|
||||
if (abs(as->sync_resample_step) == ALSA_RESAMPLE_STEP_MAX)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "The sync of ALSA device '%s' cannot be corrected (drift=%f, latency=%f)\n", as->devname, drift, latency);
|
||||
as->sync_resample_step += sign;
|
||||
return;
|
||||
}
|
||||
else if (abs(as->sync_resample_step) > ALSA_RESAMPLE_STEP_MAX)
|
||||
return; // Don't do anything, we have given up
|
||||
|
||||
// Step 0 is the original audio quality (or the fallback quality), which we
|
||||
// will just keep receiving
|
||||
if (as->sync_resample_step != 0)
|
||||
outputs_quality_unsubscribe(&as->quality);
|
||||
|
||||
as->sync_resample_step += sign;
|
||||
as->quality.sample_rate += sign * step;
|
||||
|
||||
if (as->sync_resample_step != 0)
|
||||
outputs_quality_subscribe(&as->quality);
|
||||
|
||||
// Reset position so next sync_correct latency correction is only based on
|
||||
// what has elapsed since our correction
|
||||
as->stamp_pos = (uint64_t)as->pos - (delay + BTOS(as->prebuf.read_avail, as->quality.bits_per_sample, as->quality.channels));;
|
||||
as->stamp_pts = pts;
|
||||
|
||||
DPRINTF(E_INFO, L_LAUDIO, "Adjusted sample rate to %d to sync ALSA device '%s' (drift=%f, latency=%f)\n", as->quality.sample_rate, as->devname, drift, latency);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -659,7 +726,10 @@ playback_write(struct alsa_session *as, struct output_buffer *obuf)
|
||||
{
|
||||
snd_pcm_sframes_t ret;
|
||||
snd_pcm_sframes_t avail;
|
||||
snd_pcm_sframes_t delay;
|
||||
enum alsa_sync_state sync;
|
||||
double drift;
|
||||
double latency;
|
||||
bool prebuffering;
|
||||
int i;
|
||||
|
||||
@ -689,9 +759,13 @@ playback_write(struct alsa_session *as, struct output_buffer *obuf)
|
||||
// Check sync each second (or if this is first write where last_pts is zero)
|
||||
if (obuf->pts.tv_sec != as->last_pts.tv_sec)
|
||||
{
|
||||
sync = sync_check(as);
|
||||
if (sync != ALSA_SYNC_OK)
|
||||
sync_correct(as);
|
||||
ret = snd_pcm_delay(as->hdl, &delay);
|
||||
if (ret == 0)
|
||||
{
|
||||
sync = sync_check(&drift, &latency, as, delay);
|
||||
if (sync != ALSA_SYNC_OK)
|
||||
sync_correct(as, drift, latency, obuf->pts, delay);
|
||||
}
|
||||
|
||||
as->last_pts = obuf->pts;
|
||||
}
|
||||
@ -777,7 +851,6 @@ alsa_session_make(struct output_device *device, int callback_id)
|
||||
struct alsa_session *as;
|
||||
cfg_t *cfg_audio;
|
||||
char *errmsg;
|
||||
int original_adjust;
|
||||
int ret;
|
||||
|
||||
CHECK_NULL(L_LAUDIO, as = calloc(1, sizeof(struct alsa_session)));
|
||||
@ -801,16 +874,6 @@ alsa_session_make(struct output_device *device, int callback_id)
|
||||
as->offset_ms = 1000 * (as->offset_ms/abs(as->offset_ms));
|
||||
}
|
||||
|
||||
original_adjust = cfg_getint(cfg_audio, "adjust_period_seconds");
|
||||
if (original_adjust < 1)
|
||||
as->adjust_period_seconds = 1;
|
||||
else if (original_adjust > 20)
|
||||
as->adjust_period_seconds = 20;
|
||||
else
|
||||
as->adjust_period_seconds = original_adjust;
|
||||
if (as->adjust_period_seconds != original_adjust)
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Clamped ALSA adjust_period_seconds to %d\n", as->adjust_period_seconds);
|
||||
|
||||
snd_pcm_status_malloc(&as->pcm_status);
|
||||
|
||||
ret = device_open(as);
|
||||
|
Loading…
Reference in New Issue
Block a user