diff --git a/src/sync_task.cpp b/src/sync_task.cpp index ea46cb6..b9ceb51 100644 --- a/src/sync_task.cpp +++ b/src/sync_task.cpp @@ -221,6 +221,18 @@ SyncTaskState SyncTask::handle_synchronize_audio(SyncContext& sync_context) { const int64_t active_threshold = sync_context.hard_syncing ? HARD_SYNC_SETTLE_THRESHOLD_US : HARD_SYNC_THRESHOLD_US; + if ((raw_error > active_threshold) || (raw_error < -active_threshold)) { + // A hard sync is needed. While aligning (initial-sync priming/alignment or post-seek + // re-alignment) hard syncs are expected, so they do not report an error. Otherwise this is + // an unexpected loss of sync (e.g. buffer underrun): report ERROR once and keep filling + // with silence until we re-align, at which point SYNCHRONIZED is reported. + if (!sync_context.aligning && !sync_context.reported_error) { + sync_context.reported_error = true; + this->player_impl_->enqueue_state_update(SendspinClientState::ERROR); + SS_LOGW(TAG, "Lost sync (%" PRId64 "us off), reporting error", raw_error); + } + } + if (raw_error > active_threshold) { // Buffer will run out before this chunk is supposed to play - insert silence to fill the // gap @@ -273,6 +285,15 @@ SyncTaskState SyncTask::handle_synchronize_audio(SyncContext& sync_context) { // corrections sync_context.hard_syncing = false; + // First in-tolerance alignment completes initial-sync/post-seek alignment. If we had + // reported a sync error, we have now recovered: report SYNCHRONIZED. + sync_context.aligning = false; + if (sync_context.reported_error) { + sync_context.reported_error = false; + this->player_impl_->enqueue_state_update(SendspinClientState::SYNCHRONIZED); + SS_LOGI(TAG, "Regained sync, reporting synchronized"); + } + if (raw_error > SOFT_SYNC_THRESHOLD_US) { // Slightly behind - add one interpolated frame between the last two decoded frames // Playtime estimate is advanced by transfer_audio() when the extra frame is sent @@ -616,6 +637,9 @@ void SyncTask::apply_stream_clear(SyncContext& sync_context) { sync_context.silence_remaining = 0; sync_context.release_chunk = false; sync_context.hard_syncing = true; + // Post-seek re-alignment hard syncs are expected, not a loss of sync. Leave reported_error + // as-is so a pre-seek error still recovers to SYNCHRONIZED once we re-align. + sync_context.aligning = true; if (sync_context.decode_buffer != nullptr) { sync_context.decode_buffer->decrease_buffer_length(sync_context.decode_buffer->available()); } @@ -660,6 +684,8 @@ void SyncTask::reset_context(SyncContext& sync_context) { sync_context.release_chunk = false; sync_context.initial_decode = true; sync_context.hard_syncing = true; + sync_context.aligning = true; + sync_context.reported_error = false; sync_context.silence_remaining = 0; // Empty the decode buffer without deallocating diff --git a/src/sync_task.h b/src/sync_task.h index d1b9045..6bf0cb6 100644 --- a/src/sync_task.h +++ b/src/sync_task.h @@ -84,6 +84,11 @@ struct SyncContext { bool hard_syncing{true}; // Starts true so initial sync uses tight settle threshold bool initial_decode{false}; bool release_chunk{false}; + bool aligning{true}; // True during initial-sync alignment (both priming phases) and post-seek + // re-alignment; cleared on first in-tolerance sync. Hard syncs while + // aligning are expected and do not report the ERROR client state. + bool reported_error{false}; // True between reporting ERROR and recovering to SYNCHRONIZED; + // edge-triggers the client/state transitions. }; /// @brief Event flag bits used for sync task lifecycle and command signaling