[AAudio] offset position by number of written frames.
Some checks failed
Build / build (macos-13, Debug) (push) Has been cancelled
Build / build (macos-13, Release) (push) Has been cancelled
Build / build (macos-14, Debug) (push) Has been cancelled
Build / build (macos-14, Release) (push) Has been cancelled
Build / build (ubuntu-20.04, Debug) (push) Has been cancelled
Build / build (ubuntu-20.04, Release) (push) Has been cancelled
Build / build (windows-2019, Debug) (push) Has been cancelled
Build / build (windows-2019, Release) (push) Has been cancelled
Build / build-android (arm64-v8a, Debug) (push) Has been cancelled
Build / build-android (arm64-v8a, Release) (push) Has been cancelled
Build / build-android (armeabi-v7a, Debug) (push) Has been cancelled
Build / build-android (armeabi-v7a, Release) (push) Has been cancelled
Build / build-android (x86_64, Debug) (push) Has been cancelled
Build / build-android (x86_64, Release) (push) Has been cancelled
Build / check_format (push) Has been cancelled

When reinitializating, `cubeb_stream::previous_clock` isn't reset
properly but stream gets reset and restarts `AAudioTimingInfo::output_frame_index`
from zero. Therefore, `aaudio_stream_get_position()` always reports
previous clock rather than actual position until it is surpassed later.

To estimate the position correctly, Save the number of frames sent
to stream before it's reset, and use the value as the basis for
future position estimation.
This commit is contained in:
John Lin 2024-07-04 18:47:57 -07:00 committed by Paul Adenot
parent 063a090221
commit bbbe5bb0b2

View file

@ -139,10 +139,14 @@ struct AAudioTimingInfo {
/* To guess the current position of the stream when it's playing, the elapsed /* To guess the current position of the stream when it's playing, the elapsed
* time between the last callback and now is used. However, when the stream was * time between the last callback and now is used. However, when the stream was
* stopped and there was no new callback after playing restarted yet, the time * stopped and there was no new callback after playing restarted yet, the time
* spent in stopped state should be excluded. * spent in stopped state should be excluded. It's also necessary to track the
* number of audio frames written to stream before reinitialization so it can be
* used to offset the position later, because
* `AAudioTimingInfo.output_frame_index` will restart from zero after
* reinitializing.
* This class defines an internal state machine that takes the stream state * This class defines an internal state machine that takes the stream state
* changes and callback emissions as events to changes it own statesm and * changes and callback emissions as events to changes it own states and
* calculates played time accordingly. * estimates played time accordingly.
* *
* A simplified |stream_state| transitions of playing looks like: * A simplified |stream_state| transitions of playing looks like:
* INIT -> [STARTING/STARTED -> callback* -> STOPPING/STOPPED]* -> SHUTDOWN|INIT * INIT -> [STARTING/STARTED -> callback* -> STOPPING/STOPPED]* -> SHUTDOWN|INIT
@ -161,7 +165,7 @@ struct AAudioTimingInfo {
* - Resume -(STARTING)-> Resume * - Resume -(STARTING)-> Resume
* - Pause -(INIT)-> None * - Pause -(INIT)-> None
*/ */
class position_interpolator { class position_estimate {
public: public:
// Called with the current time when stopping the stream. // Called with the current time when stopping the stream.
void stop(uint64_t timestamp) void stop(uint64_t timestamp)
@ -187,7 +191,8 @@ public:
} }
// Calculate how much time the stream bas been playing since last callback. // Calculate how much time the stream bas been playing since last callback.
uint64_t compute(uint64_t now, uint64_t last_callback_timestamp) uint64_t elapsed_time_since_callback(uint64_t now,
uint64_t last_callback_timestamp)
{ {
if (in_state<Play>()) { if (in_state<Play>()) {
if (callback_timestamp != last_callback_timestamp) { if (callback_timestamp != last_callback_timestamp) {
@ -214,6 +219,18 @@ public:
} }
} }
// Called when reinitializing stream. The input parameter is how many frames
// have already been written to AAudio since the first initialization.
void reinit(uint64_t position)
{
init_position = position;
state = None{};
callback_timestamp = 0;
}
// Frame index when last reinitialized.
uint64_t initial_position() { return init_position; }
private: private:
template <typename T> void set_state() { state.emplace<T>(); } template <typename T> void set_state() { state.emplace<T>(); }
@ -254,8 +271,10 @@ private:
uint64_t pause_time; // Elapsed time from stopping to starting stream. uint64_t pause_time; // Elapsed time from stopping to starting stream.
}; };
std::variant<None, Play, Pause, Resume> state; std::variant<None, Play, Pause, Resume> state;
// Keep track input callback timestamp to detect callback emission. // Track input callback timestamp to detect callback emission.
uint64_t callback_timestamp{0}; uint64_t callback_timestamp{0};
// Track number of written frames to adjust position after reinitialization.
uint64_t init_position{0};
}; };
struct cubeb_stream { struct cubeb_stream {
@ -296,7 +315,7 @@ struct cubeb_stream {
bool voice_input{}; bool voice_input{};
bool voice_output{}; bool voice_output{};
uint64_t previous_clock{}; uint64_t previous_clock{};
position_interpolator interpolator; position_estimate pos_estimate;
}; };
struct cubeb { struct cubeb {
@ -1020,12 +1039,18 @@ reinitialize_stream(cubeb_stream * stm)
state == stream_state::STARTING || state == stream_state::STARTING ||
state == stream_state::DRAINING; state == stream_state::DRAINING;
int err = aaudio_stream_stop_locked(stm, lock); int err = aaudio_stream_stop_locked(stm, lock);
// get total number of written frames before destroying the stream.
uint64_t total_frames = stm->pos_estimate.initial_position() +
WRAP(AAudioStream_getFramesWritten)(stm->ostream);
// error ignored. // error ignored.
aaudio_stream_destroy_locked(stm, lock); aaudio_stream_destroy_locked(stm, lock);
err = aaudio_stream_init_impl(stm, lock); err = aaudio_stream_init_impl(stm, lock);
assert(stm->in_use.load()); assert(stm->in_use.load());
// set the new initial position.
stm->pos_estimate.reinit(total_frames);
if (err != CUBEB_OK) { if (err != CUBEB_OK) {
aaudio_stream_destroy_locked(stm, lock); aaudio_stream_destroy_locked(stm, lock);
LOG("aaudio_stream_init_impl error while reiniting: %s", LOG("aaudio_stream_init_impl error while reiniting: %s",
@ -1173,7 +1198,8 @@ aaudio_stream_destroy_locked(cubeb_stream * stm, lock_guard<mutex> & lock)
} }
stm->timing_info.invalidate(); stm->timing_info.invalidate();
stm->interpolator = {}; stm->previous_clock = 0;
stm->pos_estimate = {};
if (stm->resampler) { if (stm->resampler) {
cubeb_resampler_destroy(stm->resampler); cubeb_resampler_destroy(stm->resampler);
@ -1544,7 +1570,7 @@ aaudio_stream_start_locked(cubeb_stream * stm, lock_guard<mutex> & lock)
} }
if (success) { if (success) {
stm->interpolator.start(now_ns()); stm->pos_estimate.start(now_ns());
stm->context->state.waiting.store(true); stm->context->state.waiting.store(true);
stm->context->state.cond.notify_one(); stm->context->state.cond.notify_one();
} }
@ -1653,7 +1679,7 @@ aaudio_stream_stop_locked(cubeb_stream * stm, lock_guard<mutex> & lock)
} }
if (success) { if (success) {
stm->interpolator.stop(now_ns()); stm->pos_estimate.stop(now_ns());
stm->context->state.waiting.store(true); stm->context->state.waiting.store(true);
stm->context->state.cond.notify_one(); stm->context->state.cond.notify_one();
} }
@ -1668,6 +1694,7 @@ aaudio_stream_get_position(cubeb_stream * stm, uint64_t * position)
lock_guard lock(stm->mutex); lock_guard lock(stm->mutex);
stream_state state = stm->state.load(); stream_state state = stm->state.load();
uint64_t init_position = stm->pos_estimate.initial_position();
AAudioStream * stream = stm->ostream ? stm->ostream : stm->istream; AAudioStream * stream = stm->ostream ? stm->ostream : stm->istream;
switch (state) { switch (state) {
case stream_state::ERROR: case stream_state::ERROR:
@ -1678,7 +1705,7 @@ aaudio_stream_get_position(cubeb_stream * stm, uint64_t * position)
case stream_state::STOPPING: case stream_state::STOPPING:
// getTimestamp is only valid when the stream is playing. // getTimestamp is only valid when the stream is playing.
// Simply return the number of frames passed to aaudio // Simply return the number of frames passed to aaudio
*position = WRAP(AAudioStream_getFramesRead)(stream); *position = init_position + WRAP(AAudioStream_getFramesRead)(stream);
if (*position < stm->previous_clock) { if (*position < stm->previous_clock) {
*position = stm->previous_clock; *position = stm->previous_clock;
} else { } else {
@ -1696,7 +1723,7 @@ aaudio_stream_get_position(cubeb_stream * stm, uint64_t * position)
// No callback yet, the stream hasn't really started. // No callback yet, the stream hasn't really started.
if (stm->previous_clock == 0 && !stm->timing_info.updated()) { if (stm->previous_clock == 0 && !stm->timing_info.updated()) {
LOG("Not timing info yet"); LOG("Not timing info yet");
*position = 0; *position = init_position;
return CUBEB_OK; return CUBEB_OK;
} }
@ -1704,10 +1731,12 @@ aaudio_stream_get_position(cubeb_stream * stm, uint64_t * position)
LOGV("AAudioTimingInfo idx:%lu tstamp:%lu latency:%u", LOGV("AAudioTimingInfo idx:%lu tstamp:%lu latency:%u",
info.output_frame_index, info.tstamp, info.output_latency); info.output_frame_index, info.tstamp, info.output_latency);
// Interpolate client side since the last callback. // Interpolate client side since the last callback.
uint64_t interpolation = stm->sample_rate * uint64_t interpolation =
stm->interpolator.compute(now_ns(), info.tstamp) / (stm->sample_rate *
NS_PER_S; stm->pos_estimate.elapsed_time_since_callback(now_ns(), info.tstamp) /
*position = info.output_frame_index + interpolation - info.output_latency; NS_PER_S);
*position = init_position + info.output_frame_index + interpolation -
info.output_latency;
if (*position < stm->previous_clock) { if (*position < stm->previous_clock) {
*position = stm->previous_clock; *position = stm->previous_clock;
} else { } else {