Skip to content

Commit

Permalink
Simplify clock estimation
Browse files Browse the repository at this point in the history
The slope encodes the drift between the device clock and the computer
clock. Its real value is expected very close to 1.

To estimate it, just assume it is exactly 1.

Since the clock is used to estimate very close points in the future, the
error caused by clock drift is totally negligible, and in practice it is
way lower than the slope estimation error.

Therefore, only estimate the offset.
  • Loading branch information
rom1v committed Mar 30, 2023
1 parent 0ebb3df commit 2f9396e
Show file tree
Hide file tree
Showing 5 changed files with 23 additions and 213 deletions.
4 changes: 0 additions & 4 deletions app/meson.build
Expand Up @@ -277,10 +277,6 @@ if get_option('buildtype') == 'debug'
'src/util/strbuf.c',
'src/util/term.c',
]],
['test_clock', [
'tests/test_clock.c',
'src/clock.c',
]],
['test_control_msg_serialize', [
'tests/test_control_msg_serialize.c',
'src/control_msg.c',
Expand Down
108 changes: 14 additions & 94 deletions app/src/clock.c
@@ -1,116 +1,36 @@
#include "clock.h"

#include <assert.h>

#include "util/log.h"

#define SC_CLOCK_NDEBUG // comment to debug

#define SC_CLOCK_RANGE 32

void
sc_clock_init(struct sc_clock *clock) {
clock->count = 0;
clock->head = 0;
clock->left_sum.system = 0;
clock->left_sum.stream = 0;
clock->right_sum.system = 0;
clock->right_sum.stream = 0;
}

// Estimate the affine function f(stream) = slope * stream + offset
static void
sc_clock_estimate(struct sc_clock *clock,
double *out_slope, sc_tick *out_offset) {
assert(clock->count);

if (clock->count == 1) {
// If there is only 1 point, we can't compute a slope. Assume it is 1.
struct sc_clock_point *single_point = &clock->right_sum;
*out_slope = 1;
*out_offset = single_point->system - single_point->stream;
return;
}

struct sc_clock_point left_avg = {
.system = clock->left_sum.system / (clock->count / 2),
.stream = clock->left_sum.stream / (clock->count / 2),
};
struct sc_clock_point right_avg = {
.system = clock->right_sum.system / ((clock->count + 1) / 2),
.stream = clock->right_sum.stream / ((clock->count + 1) / 2),
};

double slope = (double) (right_avg.system - left_avg.system)
/ (right_avg.stream - left_avg.stream);

if (clock->count < SC_CLOCK_RANGE) {
/* The first frames are typically received and decoded with more delay
* than the others, causing a wrong slope estimation on start. To
* compensate, assume an initial slope of 1, then progressively use the
* estimated slope. */
slope = (clock->count * slope + (SC_CLOCK_RANGE - clock->count))
/ SC_CLOCK_RANGE;
}

struct sc_clock_point global_avg = {
.system = (clock->left_sum.system + clock->right_sum.system)
/ clock->count,
.stream = (clock->left_sum.stream + clock->right_sum.stream)
/ clock->count,
};

sc_tick offset = global_avg.system - (sc_tick) (global_avg.stream * slope);

*out_slope = slope;
*out_offset = offset;
clock->range = 0;
clock->offset = 0;
}

void
sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
struct sc_clock_point *point = &clock->points[clock->head];

if (clock->count == SC_CLOCK_RANGE || clock->count & 1) {
// One point passes from the right sum to the left sum

unsigned mid;
if (clock->count == SC_CLOCK_RANGE) {
mid = (clock->head + SC_CLOCK_RANGE / 2) % SC_CLOCK_RANGE;
} else {
// Only for the first frames
mid = clock->count / 2;
}

struct sc_clock_point *mid_point = &clock->points[mid];
clock->left_sum.system += mid_point->system;
clock->left_sum.stream += mid_point->stream;
clock->right_sum.system -= mid_point->system;
clock->right_sum.stream -= mid_point->stream;
if (clock->range < SC_CLOCK_RANGE) {
++clock->range;
}

if (clock->count == SC_CLOCK_RANGE) {
// The current point overwrites the previous value in the circular
// array, update the left sum accordingly
clock->left_sum.system -= point->system;
clock->left_sum.stream -= point->stream;
} else {
++clock->count;
}

point->system = system;
point->stream = stream;

clock->right_sum.system += system;
clock->right_sum.stream += stream;

clock->head = (clock->head + 1) % SC_CLOCK_RANGE;

// Update estimation
sc_clock_estimate(clock, &clock->slope, &clock->offset);
sc_tick offset = system - stream;
clock->offset = ((clock->range - 1) * clock->offset + offset)
/ clock->range;

#ifndef SC_CLOCK_NDEBUG
LOGD("Clock estimation: %f * pts + %" PRItick, clock->slope, clock->offset);
LOGD("Clock estimation: pts + %" PRItick, clock->offset);
#endif
}

sc_tick
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
assert(clock->count); // sc_clock_update() must have been called
return (sc_tick) (stream * clock->slope) + clock->offset;
assert(clock->range); // sc_clock_update() must have been called
return stream + clock->offset;
}
43 changes: 8 additions & 35 deletions app/src/clock.h
Expand Up @@ -3,13 +3,8 @@

#include "common.h"

#include <assert.h>

#include "util/tick.h"

#define SC_CLOCK_RANGE 32
static_assert(!(SC_CLOCK_RANGE & 1), "SC_CLOCK_RANGE must be even");

struct sc_clock_point {
sc_tick system;
sc_tick stream;
Expand All @@ -21,40 +16,18 @@ struct sc_clock_point {
*
* f(stream) = slope * stream + offset
*
* To that end, it stores the SC_CLOCK_RANGE last clock points (the timestamps
* of a frame expressed both in stream time and system time) in a circular
* array.
* Theoretically, the slope encodes the drift between the device clock and the
* computer clock. It is expected to be very close to 1.
*
* To estimate the slope, it splits the last SC_CLOCK_RANGE points into two
* sets of SC_CLOCK_RANGE/2 points, and computes their centroid ("average
* point"). The slope of the estimated affine function is that of the line
* passing through these two points.
* Since the clock is used to estimate very close points in the future (which
* are reestimated on every clock update, see delay_buffer), the error caused
* by clock drift is totally negligible, so it is better to assume that the
* slope is 1 than to estimate it (the estimation error would be larger).
*
* To estimate the offset, it computes the centroid of all the SC_CLOCK_RANGE
* points. The resulting affine function passes by this centroid.
*
* With a circular array, the rolling sums (and average) are quick to compute.
* In practice, the estimation is stable and the evolution is smooth.
* Therefore, only the offset is estimated.
*/
struct sc_clock {
// Circular array
struct sc_clock_point points[SC_CLOCK_RANGE];

// Number of points in the array (count <= SC_CLOCK_RANGE)
unsigned count;

// Index of the next point to write
unsigned head;

// Sum of the first count/2 points
struct sc_clock_point left_sum;

// Sum of the last (count+1)/2 points
struct sc_clock_point right_sum;

// Estimated slope and offset
// (computed on sc_clock_update(), used by sc_clock_to_system_time())
double slope;
unsigned range;
sc_tick offset;
};

Expand Down
2 changes: 1 addition & 1 deletion app/src/delay_buffer.c
Expand Up @@ -194,7 +194,7 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink,
sc_clock_update(&db->clock, sc_tick_now(), pts);
sc_cond_signal(&db->wait_cond);

if (db->first_frame_asap && db->clock.count == 1) {
if (db->first_frame_asap && db->clock.range == 1) {
sc_mutex_unlock(&db->mutex);
return sc_frame_source_sinks_push(&db->frame_source, frame);
}
Expand Down
79 changes: 0 additions & 79 deletions app/tests/test_clock.c

This file was deleted.

0 comments on commit 2f9396e

Please sign in to comment.