Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Solo iOS Growth Diary — Vol. 7: Wiring SpeechAn...

Solo iOS Growth Diary — Vol. 7: Wiring SpeechAnalyzer to a Live Mic

iOS 26 replaces SFSpeechRecognizer with SpeechAnalyzer + composable modules. A practitioner's walkthrough of wiring SpeechTranscriber to a live AVAudioEngine mic in SwiftUI, on-device — plus the traps the docs skip: you must convert the audio buffer, the language model downloads on first use, volatile vs final need different handling, there is no custom vocabulary, and SpeechAnalyzer isn't on watchOS. Latency on a warm start (iPhone 16e, iOS 26.5, time-to-first-volatile-result): ~0.3–0.5s — a first-party measurement, not a controlled benchmark.

Sample code (MIT): https://github.com/simplememofast/ios26-speechanalyzer-live-mic
Full write-up: https://simplememofast.com/en/blog/ios26-speechanalyzer-live-mic
The pipeline ships in Simple Memo's on-device voice input: https://simplememofast.com/voice-input/

— 株式会社ユリカ / Simple Memo team

Avatar for SimpleMemo

SimpleMemo

June 23, 2026

More Decks by SimpleMemo

Other Decks in Programming

Transcript

  1. S O L O i O S G R O

    W T H D I A R Y · V O L . 7 Wiring iOS 26's SpeechAnalyzer to a Live Mic What the docs don't tell you Minimal SwiftUI sample · on-device · builds & runs on a shipping iOS 26 device (Xcode 26, Swift 6) 株式会社ユリカ / Simple Memo team
  2. T L ; D R iOS 26 swaps one object

    for an orchestrator + modules Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 1 • iOS 26 replaces SFSpeechRecognizer with SpeechAnalyzer + composable modules. • This deck: wire SpeechTranscriber to a live AVAudioEngine mic in SwiftUI, on-device. • Five traps the docs skip: buffer conversion · first-use model download · volatile vs final · no custom vocabulary · not on watchOS. • Full sample on GitHub (MIT) — builds & runs on a shipping iOS 26 device.
  3. A U D I E N C E Who this

    is for Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 2 • Developers migrating from SFSpeechRecognizer. • Anyone who followed the WWDC sample and ended up with code that compiles but produces no text. If you've lost an afternoon to “it builds, nothing happens” — the buffer-conversion slide is why.
  4. W H Y Why SpeechAnalyzer replaces SFSpeechRecognizer Solo iOS Growth

    Diary — Vol. 7 株式会社ユリカ · 3 • Old: one object bundled session + language + recognition. • New: an orchestrator you attach modules to — compose only what you need. • Optimized for longer, conversational audio. • Fully on-device for supported locales. • No “enable dictation / Siri in Settings” step required.
  5. M E N TA L M O D E L

    Analyzer + Modules Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 4 mic --> AVAudioEngine.installTap --> AVAudioConverter --> AnalyzerInput | SpeechAnalyzer([ SpeechTranscriber ]) | for try await result in transcriber.results result.text (AttributedString) / result.isFinal SpeechTranscriber = speech-to-text. SpeechDetector = voice-activity. DictationTranscriber = short keyboard-style dictation.
  6. M I N I M A L I M P

    L · 1 / 3 Permissions + build the transcriber & analyzer Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 5 // Info.plist: NSMicrophoneUsageDescription / NSSpeechRecognitionUsageDescription guard let locale = await SpeechTranscriber .supportedLocale(equivalentTo: .current) else { throw Failure.localeNotSupported } let transcriber = SpeechTranscriber( locale: locale, transcriptionOptions: [], reportingOptions: [.volatileResults], // partials while speaking attributeOptions: []) // + .audioTimeRange = per-word timing let analyzer = SpeechAnalyzer(modules: [transcriber]) let analyzerFormat = await SpeechAnalyzer .bestAvailableAudioFormat(compatibleWith: [transcriber])
  7. M I N I M A L I M P

    L · 2 / 3 Results loop, then start the analyzer Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 6 resultsTask = Task { for try await result in transcriber.results { let piece = String(result.text.characters) // AttributedString if result.isFinal { finalizedText += piece; volatileText = "" } else { volatileText = piece } } } let (sequence, builder) = AsyncStream<AnalyzerInput>.makeStream() self.inputBuilder = builder try await analyzer.start(inputSequence: sequence)
  8. M I N I M A L I M P

    L · 3 / 3 The mic tap Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 7 let converter = AudioBufferConverter() // capture locals only let input = audioEngine.inputNode let micFormat = input.outputFormat(forBus: 0) input.installTap(onBus: 0, bufferSize: 4096, format: micFormat) { buffer, _ in // convert to the format SpeechAnalyzer asked for, THEN yield guard let c = try? converter.convert(buffer, to: analyzerFormat) else { return } builder.yield(AnalyzerInput(buffer: c)) } audioEngine.prepare(); try audioEngine.start() Full SpeechSession / AudioBufferConverter / SwiftUI view are in the repo.
  9. G O T C H A 0 1 You MUST

    convert the audio buffer Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 8 • Input-node format (often 48 kHz, hardware-dependent) usually does NOT match SpeechAnalyzer.bestAvailableAudioFormat(compatibleWith:). • Mismatch → clean compile, zero transcription. • The single most common reason for “it builds but nothing happens.” • Fix: run every buffer through AVAudioConverter first.
  10. G O T C H A 0 2 The model

    downloads on first use — handle offline Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 9 let installed = await Set( SpeechTranscriber.installedLocales.map { $0.identifier(.bcp47) }) if !installed.contains(locale.identifier(.bcp47)) { if let request = try await AssetInventory .assetInstallationRequest(supporting: [transcriber]) { try await request.downloadAndInstall() // request.progress for UI } } • Language model = system-shared asset (does not inflate your app bundle). • First run with no network can't download it → surface .assetUnavailable, don't fail silently.
  11. U X H I N G E Volatile vs. finalized

    results Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 10 • reportingOptions: [.volatileResults] → fast partials while the user is still speaking. • result.isFinal → committed text. • Idiomatic UI: volatile dimmed, replaced when a final arrives; persist only finals. • result.text is an AttributedString → attributeOptions: [.audioTimeRange] gives per-word timing for highlight / seek.
  12. M E A S U R E D O N

    S H I P P I N G I O S 2 6 Latency — report it with its conditions Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 11 BETA-ERA REPORT 14s+ first result · iPhone 16 Pro · iOS 26.0 beta · Xcode beta 5 (even after allocate / preheat) SHIPPING iOS 26.5 ~0.3–0.5s first volatile result · iPhone 16e (non-Pro A18) · warm start (model installed, locale allocated) First-party measurement (time-to-first-volatile-result), not a controlled head-to-head. First-ever launch downloads the model once — budget separately, show progress. Neural Engine = same 16-core across the A18 family.
  13. G O T C H A 0 3 There is

    no Custom Vocabulary Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 12 • SFSpeechRecognizer had contextualStrings to bias recognition toward known terms. • SpeechAnalyzer, as of iOS 26.0, exposes no equivalent. • Domain full of proper nouns or jargon? Budget for that gap now.
  14. G O T C H A 0 4 watchOS —

    SpeechAnalyzer isn't there, but voice input is Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 13 • SpeechAnalyzer: iOS / iPadOS / macOS / visionOS / tvOS 26 — not watchOS. • Fallback: watchOS system dictation → returns finished text (lose volatile / time-ranges / your own tap; voice capture still works). // watchOS — system handles dictation and returns text: TextFieldLink(prompt: Text("Speak or type")) { Image(systemName: "mic.fill") } onSubmit: { text in send(text) } The split that ships: SpeechAnalyzer on iPhone, TextFieldLink on the Watch.
  15. G O T C H A 0 5 Swift 6

    concurrency + availability gating Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 14 • The mic-tap closure runs on a real-time audio thread. • Capture only locals — the AsyncStream.Continuation, the target AVAudioFormat, a fresh converter — never touch a @MainActor object inside the tap. • Compiles under complete strict concurrency with no @unchecked Sendable escape hatches. • Deploying below iOS 26? @available(iOS 26.0, *) + gate with if #available(iOS 26.0, *).
  16. M I G R AT I O N SFSpeechRecognizer →

    SpeechAnalyzer (iOS 26) Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 15 SFSpeechRecognizer (old) SpeechAnalyzer (iOS 26) One object: session + recognition SpeechAnalyzer + composable modules append(_:) audio buffers AnalyzerInput(buffer:) into an AsyncStream partialResults flag reportingOptions: [.volatileResults] delegate / result handler for try await result in transcriber.results bestTranscription.formattedString String(result.text.characters) contextualStrings (custom vocab) no equivalent user enables dictation in Settings not required works on watchOS not on watchOS (system dictation)
  17. R E P O + W H E R E

    T H I S S H I P S Take the sample, read the write-up Solo iOS Growth Diary — Vol. 7 株式会社ユリカ · 16 Sample (MIT) github.com/simplememofast/ios26-speechanalyzer-live-mic SpeechSession + AudioBufferConverter + SwiftUI view. Builds on shipping iOS 26 (Xcode 26, Swift 6). Full write-up simplememofast.com/en/blog/ios26-speechanalyzer-live-mic Where it ships This pipeline (minus the email/send parts) powers Simple Memo's on-device voice input: simplememofast.com/voice-input/ © 2026 株式会社ユリカ · sample under MIT · corrections welcome via PR