blob: 18249e700ecdba6e6ad3e21c030b22a913a1f8a1 [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/services/speech/speech_timestamp_estimator.h"
#include <algorithm>
namespace speech {
namespace {
using MediaTimestamp = SpeechTimestampEstimator::MediaTimestamp;
using SpeechTimestamp = SpeechTimestampEstimator::SpeechTimestamp;
using PlaybackDuration = SpeechTimestampEstimator::PlaybackDuration;
using MediaRanges = SpeechTimestampEstimator::MediaRanges;
using PlaybackChunk = SpeechTimestampEstimator::PlaybackChunk;
PlaybackDuration CalculateDuration(SpeechTimestamp start, SpeechTimestamp end) {
CHECK_LT(start, end);
return PlaybackDuration(end.value() - start.value());
}
MediaTimestamp IncreaseTimestamp(MediaTimestamp timestamp,
PlaybackDuration duration) {
CHECK(duration->is_positive());
return MediaTimestamp(timestamp.value() + duration.value());
}
} // namespace
SpeechTimestampEstimator::SpeechTimestampEstimator() = default;
SpeechTimestampEstimator::~SpeechTimestampEstimator() = default;
SpeechTimestampEstimator::PlaybackChunk::PlaybackChunk(
MediaTimestamp media_start,
SpeechTimestamp current_speech_time)
: media_start(media_start), speech_start(current_speech_time) {}
void SpeechTimestampEstimator::PlaybackChunk::TrimStart(
PlaybackDuration duration) {
CHECK_LE(duration, playback_duration);
*media_start += duration.value();
*speech_start += duration.value();
*playback_duration -= duration.value();
// Note: `playback_duration` might 0 at this point, which is allowed.
}
void SpeechTimestampEstimator::PlaybackChunk::AddDuration(
PlaybackDuration duration) {
*playback_duration += duration.value();
}
MediaTimestamp SpeechTimestampEstimator::PlaybackChunk::MediaEnd() const {
return MediaTimestamp(media_start.value() + playback_duration.value());
}
SpeechTimestamp SpeechTimestampEstimator::PlaybackChunk::SpeechEnd() const {
return SpeechTimestamp(speech_start.value() + playback_duration.value());
}
void SpeechTimestampEstimator::AddPlaybackStart(
MediaTimestamp media_start_pts) {
playback_chunks_.emplace_back(media_start_pts, current_speech_time_);
// Silence is only saved to estimate the current media timestamp. A new
// playback should "drop" the running silence, since no audio was sent to the
// speech service during that time.
running_silence_duration_.reset();
}
void SpeechTimestampEstimator::AppendDuration(PlaybackDuration duration) {
CHECK(duration->is_positive());
// Adjust for silences before moving the current speech time.
if (running_silence_duration_) {
AdjustLastMediaTimestampForSilence(current_speech_time_);
}
*current_speech_time_ += duration.value();
if (playback_chunks_.empty()) {
return;
}
playback_chunks_.back().AddDuration(duration);
}
void SpeechTimestampEstimator::OnSilentMediaDropped(PlaybackDuration duration) {
CHECK(duration->is_positive());
// We've never received a media timestamp to adjust... No need to keep track
// of silence.
if (playback_chunks_.empty()) {
return;
}
if (!running_silence_duration_) {
running_silence_duration_ = duration;
return;
}
*(running_silence_duration_.value()) += duration.value();
}
void SpeechTimestampEstimator::AdjustLastMediaTimestampForSilence(
SpeechTimestamp current_speech_time) {
CHECK(!playback_chunks_.empty());
CHECK(running_silence_duration_.has_value());
// If this method is called, we have received audible frames after a period of
// silence. Calculate this first non-silent media timestamp from the last
// non-silent media timestamp and elapsed silence duration.
MediaTimestamp first_audible_timestamp = IncreaseTimestamp(
playback_chunks_.back().MediaEnd(), running_silence_duration_.value());
// "Forward" the last media timestamp by `running_silence_duration_`.
playback_chunks_.emplace_back(first_audible_timestamp, current_speech_time);
running_silence_duration_.reset();
}
void SpeechTimestampEstimator::PopFrontUntil(
base::circular_deque<PlaybackChunk>& chunks,
SpeechTimestamp end_timestamp) {
CHECK(!chunks.empty());
CHECK_LE(end_timestamp, current_speech_time_);
CHECK_EQ(chunks.back().SpeechEnd(), current_speech_time_);
// Remove all chunks that have ended before `end_timestamp`.
while (chunks.front().SpeechEnd() < end_timestamp) {
chunks.pop_front();
}
// We should always have a leftover chunk, even if `end_timestamp` is equal to
// `current_speech_time_`.
CHECK(!chunks.empty());
// Partially discard the front chunk until `end_timestamp` (exclusively),
// if it starts before `end_timestamp`.
PlaybackChunk& front_chunk = chunks.front();
if (front_chunk.speech_start < end_timestamp) {
PlaybackDuration duration =
CalculateDuration(front_chunk.speech_start, end_timestamp);
front_chunk.TrimStart(duration);
}
// We should always have a leftover chunk, potentially with 0 duration, since
// we popped [0, `end_timestamp`), not [0, `end_timestamp`].
CHECK(!chunks.empty());
}
std::vector<SpeechTimestampEstimator::PlaybackChunk>
SpeechTimestampEstimator::TakeFrontUntil(
base::circular_deque<PlaybackChunk>& chunks,
SpeechTimestamp end_timestamp) {
CHECK(!chunks.empty());
CHECK_LE(end_timestamp, current_speech_time_);
CHECK_EQ(chunks.back().SpeechEnd(), current_speech_time_);
std::vector<PlaybackChunk> results;
// Take all complete chunks before `end_timestamp`.
while (chunks.front().SpeechEnd() < end_timestamp) {
// Don't save chunks with no duration.
if (!chunks.front().playback_duration->is_zero()) {
results.push_back(std::move(chunks.front()));
}
chunks.pop_front();
}
// We should always have a leftover chunk, even if `end_timestamp` is equal to
// `current_speech_time_`.
CHECK(!chunks.empty());
// Split the front chunk at `end_timestamp`, and save the results before
// `end_timestamp`.
PlaybackChunk& front_chunk = chunks.front();
if (front_chunk.speech_start < end_timestamp) {
PlaybackDuration duration =
CalculateDuration(front_chunk.speech_start, end_timestamp);
PlaybackChunk front_copy = front_chunk;
front_copy.playback_duration = duration;
results.push_back(std::move(front_copy));
front_chunk.TrimStart(duration);
}
// We should always have a leftover chunk, potentially with 0 duration, since
// we took [0, `end_timestamp`), not [0, `end_timestamp`].
CHECK(!chunks.empty());
return results;
}
MediaRanges SpeechTimestampEstimator::TakeTimestampsInRange(
SpeechTimestamp start,
SpeechTimestamp end) {
// Verify the timestamps and chunks.
if (start >= end || playback_chunks_.empty()) {
return MediaRanges();
}
// Clamp inputs.
constexpr auto kSpeechTimeZero = SpeechTimestamp(base::Seconds(0));
start = std::clamp(start, kSpeechTimeZero, current_speech_time_);
end = std::clamp(end, kSpeechTimeZero, current_speech_time_);
// Clamping values can collapse them to the same point.
if (start == end) {
return MediaRanges();
}
// Trim the front of the queue, from [0, `start`).
PopFrontUntil(playback_chunks_, start);
// `playback_chunks_` no longer contain any timestamps before `start`.
// Take all playback chunks from [0, `end`), which is now [`start`, `end`).
auto playbacks = TakeFrontUntil(playback_chunks_, end);
// We should always have one chunk leftover (sometimes with 0 duration),
// since we removed [0, `end`), not [0, `end`].
CHECK(!playback_chunks_.empty());
return ConvertToMediaRanges(playbacks);
}
MediaRanges SpeechTimestampEstimator::PeekTimestampsInRange(
SpeechTimestamp start,
SpeechTimestamp end) {
// Verify the timestamps and chunks.
if (start >= end || playback_chunks_.empty()) {
return MediaRanges();
}
// Clamp inputs.
constexpr auto kSpeechTimeZero = SpeechTimestamp(base::Seconds(0));
start = std::clamp(start, kSpeechTimeZero, current_speech_time_);
end = std::clamp(end, kSpeechTimeZero, current_speech_time_);
// Clamping values can collapse them to the same point.
if (start == end) {
return MediaRanges();
}
// Create a copy of the chunks to avoid modifying the original state.
auto chunks_copy = playback_chunks_;
// Operate on the copy.
PopFrontUntil(chunks_copy, start);
auto playbacks = TakeFrontUntil(chunks_copy, end);
return ConvertToMediaRanges(playbacks);
}
MediaRanges SpeechTimestampEstimator::ConvertToMediaRanges(
const std::vector<PlaybackChunk>& playbacks) {
MediaRanges results;
results.reserve(playbacks.size());
// Convert playback chunks into media start/end presentation timestamps.
std::ranges::transform(
playbacks, std::back_inserter(results),
[](const PlaybackChunk& chunk) -> media::MediaTimestampRange {
return {.start = chunk.media_start.value(),
.end = chunk.MediaEnd().value()};
});
return results;
}
} // namespace speech