Skip to content

iOS: harden dictation lifecycle (double-stop, dismissal teardown, stale interim, metering perf)#28

Merged
metaforismo merged 1 commit into
mainfrom
fix/ios-dictation-hardening
Jun 11, 2026
Merged

iOS: harden dictation lifecycle (double-stop, dismissal teardown, stale interim, metering perf)#28
metaforismo merged 1 commit into
mainfrom
fix/ios-dictation-hardening

Conversation

@metaforismo

Copy link
Copy Markdown
Owner

Fixes from a correctness review of the recently merged iOS dictation work (PRs #25/#26), verified at build + test time on a simulator.

  • Double-stop transcribed the same audio twice: finishRecording() was not idempotent (two quick stop taps; AirPods removal firing both interruption AND route-change notifications; an interruption racing a user stop) and AudioRecorder.stop() returned pcmSamples without clearing them — the same WAV could be POSTed twice and the transcript inserted twice. Now guarded by a state check at the top of finishRecording() (@mainactor, no suspension point before the state flip) and stop() takes-and-clears the samples under the lock.
  • Keyboard dismissal mid-dictation left the mic hot: no teardown existed, so AVAudioEngine kept capturing (orange mic indicator with no UI) and a late transcript could be inserted into whatever field the proxy pointed at later. viewWillDisappear now cancels via a new generation-counted DictationController.cancel() that also discards in-flight start/transcription results.
  • Stale interim text: a late SFSpeechRecognizer result delivered after stop() could republish old words at the start of the next dictation; the result handler now ignores results from a request that is no longer current.
  • Metering perf: level publishes throttled to ~15Hz via a pure RateLimiter (RMS skipped entirely when throttled, x*x instead of pow), and the waveform is isolated into a child view so the whole keyboard no longer re-renders 25–50×/sec — keyboard extensions live under tight CPU/memory budgets.

Tests: +5 RateLimiter unit tests; full suite 36 green on a booted simulator; xcodebuild build succeeds with CODE_SIGNING_ALLOWED=NO.

🤖 Generated with Claude Code

…etering

- DictationController.finishRecording is now idempotent (guard on
  .recording) so racing stop paths (double tap, interruption + route
  change, interruption vs user stop) can't both finalize; a generation
  counter discards in-flight starts/transcripts after cancellation
- AudioRecorder.stop clears captured samples after reading them so a
  second stop can't return the same audio twice
- KeyboardViewController.viewWillDisappear cancels active dictation via
  new DictationController.cancel(), so a dismissed keyboard stops the
  mic, ends the live preview, and drops the pending transcript
- LiveTranscriber ignores late recognition results from a stale request
  (identity check against the current request) so stopped sessions
  can't republish interim text
- Level metering: publishes throttled to ~15 Hz via a new pure
  RateLimiter helper, RMS computed with x*x instead of pow, and the
  Waveform child now observes the recorder itself so per-tick
  re-renders no longer hit the whole keyboard
- Tests: RateLimiterTests cover the new throttling logic

Co-Authored-By: Claude Fable 5 <[email protected]>
@metaforismo
metaforismo merged commit 567f775 into main Jun 11, 2026
0 of 2 checks passed
@metaforismo
metaforismo deleted the fix/ios-dictation-hardening branch June 11, 2026 19:00
metaforismo added a commit that referenced this pull request Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant