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 generated
|
||||||
.build/
|
.build/
|
||||||
DerivedData/
|
DerivedData/
|
||||||
|
Products/
|
||||||
|
|
||||||
## Various settings
|
## Various settings
|
||||||
*.pbxuser
|
*.pbxuser
|
||||||
|
|||||||
@@ -51,13 +51,7 @@ extension ContentView {
|
|||||||
/// supported equivalent of its current value so the locale picker starts with a valid selection.
|
/// supported equivalent of its current value so the locale picker starts with a valid selection.
|
||||||
func load() async {
|
func load() async {
|
||||||
locales = await SpeechTranscriber.supportedLocales.sorted {
|
locales = await SpeechTranscriber.supportedLocales.sorted {
|
||||||
name(for: $0) < name(for: $1)
|
name(for: $0).localizedStandardCompare(name(for: $1)) == .orderedAscending
|
||||||
}
|
|
||||||
|
|
||||||
if let equivalent = await SpeechTranscriber.supportedLocale(
|
|
||||||
equivalentTo: locale
|
|
||||||
) {
|
|
||||||
locale = equivalent
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,18 @@ extension RecordingView {
|
|||||||
/// The transcription of the last processed recording, or `nil` when none has been processed yet.
|
/// The transcription of the last processed recording, or `nil` when none has been processed yet.
|
||||||
private(set) var transcription: Transcription?
|
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.
|
/// The binding to the locale of the spoken language to transcribe.
|
||||||
@ObservationIgnored
|
@ObservationIgnored
|
||||||
private let locale: Binding<Locale>
|
private let locale: Binding<Locale>
|
||||||
@@ -31,7 +43,7 @@ extension RecordingView {
|
|||||||
@ObservationIgnored
|
@ObservationIgnored
|
||||||
private let recorder: any RecordingService
|
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
|
@ObservationIgnored
|
||||||
private var taskTimer: Task<Void, Never>?
|
private var taskTimer: Task<Void, Never>?
|
||||||
|
|
||||||
@@ -180,6 +192,17 @@ extension RecordingView {
|
|||||||
|
|
||||||
private extension RecordingView.Model {
|
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
|
// MARK: Methods
|
||||||
|
|
||||||
/// Processes the recorded input: it stops the audio capture, transcribes the captured audio, and publishes the result in ``transcription``;
|
/// 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
|
transcription = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accumulated = .zero
|
||||||
elapsedSeconds = 0
|
elapsedSeconds = 0
|
||||||
state = .notRecording
|
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,
|
/// - Parameter shouldRestartTimer: Whether the measured recording time, ``elapsedSeconds``, and ``transcription`` should be
|
||||||
/// which is the case for a new recording as opposed to one resuming from a pause.
|
/// cleared before the timer starts, which is the case for a new recording as opposed to one resuming from a pause.
|
||||||
func startTimer(
|
func startTimer(
|
||||||
_ shouldRestartTimer: Bool
|
_ shouldRestartTimer: Bool
|
||||||
) {
|
) {
|
||||||
if shouldRestartTimer {
|
if shouldRestartTimer {
|
||||||
|
accumulated = .zero
|
||||||
elapsedSeconds = 0
|
elapsedSeconds = 0
|
||||||
transcription = nil
|
transcription = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
anchor = clock.now
|
||||||
|
|
||||||
taskTimer?.cancel()
|
taskTimer?.cancel()
|
||||||
taskTimer = Task { [weak self] in
|
taskTimer = Task { [weak self] in
|
||||||
while !Task.isCancelled {
|
while !Task.isCancelled {
|
||||||
try? await Task.sleep(for: .seconds(1))
|
|
||||||
|
|
||||||
guard let self, !Task.isCancelled else {
|
guard let self, !Task.isCancelled else {
|
||||||
return
|
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() {
|
func stopTimer() {
|
||||||
|
if let anchor {
|
||||||
|
accumulated += anchor.duration(to: clock.now)
|
||||||
|
|
||||||
|
self.anchor = nil
|
||||||
|
}
|
||||||
|
|
||||||
taskTimer?.cancel()
|
taskTimer?.cancel()
|
||||||
taskTimer = nil
|
taskTimer = nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user