Improve the recording time with a monotonic clock to avoid timer drift on the RecordingViewModel view model in the Recording package target.
This commit is contained in:
@@ -9,6 +9,7 @@ xcuserdata/
|
||||
## Build generated
|
||||
.build/
|
||||
DerivedData/
|
||||
Products/
|
||||
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
|
||||
@@ -51,13 +51,7 @@ extension ContentView {
|
||||
/// supported equivalent of its current value so the locale picker starts with a valid selection.
|
||||
func load() async {
|
||||
locales = await SpeechTranscriber.supportedLocales.sorted {
|
||||
name(for: $0) < name(for: $1)
|
||||
}
|
||||
|
||||
if let equivalent = await SpeechTranscriber.supportedLocale(
|
||||
equivalentTo: locale
|
||||
) {
|
||||
locale = equivalent
|
||||
name(for: $0).localizedStandardCompare(name(for: $1)) == .orderedAscending
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,18 @@ extension RecordingView {
|
||||
/// The transcription of the last processed recording, or `nil` when none has been processed yet.
|
||||
private(set) var transcription: Transcription?
|
||||
|
||||
/// The recording time accumulated by earlier recording stretches, up to the last pause.
|
||||
@ObservationIgnored
|
||||
private var accumulated: Duration = .zero
|
||||
|
||||
/// The instant the current recording stretch started, or `nil` while not recording.
|
||||
@ObservationIgnored
|
||||
private var anchor: ContinuousClock.Instant?
|
||||
|
||||
/// The clock that measures the recording time.
|
||||
@ObservationIgnored
|
||||
private let clock = ContinuousClock()
|
||||
|
||||
/// The binding to the locale of the spoken language to transcribe.
|
||||
@ObservationIgnored
|
||||
private let locale: Binding<Locale>
|
||||
@@ -31,7 +43,7 @@ extension RecordingView {
|
||||
@ObservationIgnored
|
||||
private let recorder: any RecordingService
|
||||
|
||||
/// The task that increments ``elapsedSeconds`` every second while recording.
|
||||
/// The task that updates ``elapsedSeconds`` from the measured recording time once per second while recording.
|
||||
@ObservationIgnored
|
||||
private var taskTimer: Task<Void, Never>?
|
||||
|
||||
@@ -180,6 +192,17 @@ extension RecordingView {
|
||||
|
||||
private extension RecordingView.Model {
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// The time spent recording so far, excluding any time spent paused, as measured by the clock.
|
||||
var elapsed: Duration {
|
||||
guard let anchor else {
|
||||
return accumulated
|
||||
}
|
||||
|
||||
return accumulated + anchor.duration(to: clock.now)
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Processes the recorded input: it stops the audio capture, transcribes the captured audio, and publishes the result in ``transcription``;
|
||||
@@ -200,6 +223,7 @@ private extension RecordingView.Model {
|
||||
transcription = nil
|
||||
}
|
||||
|
||||
accumulated = .zero
|
||||
elapsedSeconds = 0
|
||||
state = .notRecording
|
||||
}
|
||||
@@ -223,34 +247,48 @@ private extension RecordingView.Model {
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the timer task, which increments ``elapsedSeconds`` once per second until it is cancelled. Any previously running timer task is cancelled first.
|
||||
/// Starts the timer task, which updates ``elapsedSeconds`` from the time measured by the clock on every whole second until it is
|
||||
/// cancelled, so scheduling latency never accumulates as drift. Any previously running timer task is cancelled first.
|
||||
///
|
||||
/// - Parameter shouldRestartTimer: Whether ``elapsedSeconds`` and ``transcription`` should be cleared before the timer starts,
|
||||
/// which is the case for a new recording as opposed to one resuming from a pause.
|
||||
/// - Parameter shouldRestartTimer: Whether the measured recording time, ``elapsedSeconds``, and ``transcription`` should be
|
||||
/// cleared before the timer starts, which is the case for a new recording as opposed to one resuming from a pause.
|
||||
func startTimer(
|
||||
_ shouldRestartTimer: Bool
|
||||
) {
|
||||
if shouldRestartTimer {
|
||||
accumulated = .zero
|
||||
elapsedSeconds = 0
|
||||
transcription = nil
|
||||
}
|
||||
|
||||
anchor = clock.now
|
||||
|
||||
taskTimer?.cancel()
|
||||
taskTimer = Task { [weak self] in
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
|
||||
guard let self, !Task.isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
self.elapsedSeconds += 1
|
||||
let elapsed = self.elapsed
|
||||
let seconds = elapsed.components.seconds
|
||||
|
||||
self.elapsedSeconds = Int(seconds)
|
||||
|
||||
try? await Task.sleep(for: .seconds(seconds + 1) - elapsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops the timer task, if any, keeping ``elapsedSeconds`` at its current value.
|
||||
/// Stops the timer task, if any, folding the current recording stretch into the accumulated recording time so ``elapsedSeconds``
|
||||
/// keeps its value.
|
||||
func stopTimer() {
|
||||
if let anchor {
|
||||
accumulated += anchor.duration(to: clock.now)
|
||||
|
||||
self.anchor = nil
|
||||
}
|
||||
|
||||
taskTimer?.cancel()
|
||||
taskTimer = nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user