You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
AVAudioEngine couples input and output in one I/O unit, so with AirPods the capture depended on the output device. Switching the headset to its microphone profile stopped the engine and lost the start of recordings (measured: 2.9 s / 2.8 s / 3.2 s recordings held 0.17 s / 1.2 s / 2.4 s of audio), and selecting the built-in microphone while AirPods were the output could deliver 0 samples.
HALInputCapture (AudioDeviceCreateIOProcIDWithBlock on the chosen device) replaces the engine, so the output device is irrelevant. It reads the device stream format on every callback (Float32, Int16, Int32; rate changes followed), takes the first channel as mono Float32, and the existing 16 kHz conversion is unchanged. AudioEngine keeps its public API. The ObjCExceptionCatcher target is now unused but left in Package.swift.
2. Start tone and Listening overlay wait for the first audio
With a headset the tone played before the output profile switch (about 107-121 ms after capture start) and was not heard. AudioCapturing.waitForFirstAudio(handle:timeout:) (2 s timeout) now gates the tone and the Listening overlay. The capture already records, so nothing is lost, and releasing the key during the wait still transcribes.
3. Keep Last Dictation / Paste Last Dictation (new)
For long dictations whose target window is gone. A menu bar toggle keeps the last text in memory only (never on disk or in logs; discarded when the setting is turned off or the app quits; kept even if pasting failed). Paste Last Dictation pastes it again into the frontmost app with the same routing as a fresh dictation (typing in Windows App when enabled), even with Auto-Paste off, after a short delay so the menu has closed. Off by default.
Supersedes #129, which did not address the cause. Version bumped to 1.7.4.
Related issue
Maintainer reports: dictation fails with AirPods Max/Pro and with the built-in microphone while they are worn; paste fails when the window is gone. Related to #119 and #114. Does not close them.
Prepared with an AI assistant using Copilot SDK in VS Code.
Validation
swift build -c release -Xswiftc -warnings-as-errors, swiftlint lint --strict, and swift test succeed locally
For audio, hotkey, paste, overlay, or update changes: I ran the built app and checked the affected flow
swift build -c release -Xswiftc -warnings-as-errors: passed. swiftlint lint --strict: 0 violations. swift test: 119 tests passed. Python suite: 23 passed. plutil -lint Info.plist: OK. scripts/verify-apple-intelligence-linked.sh: FoundationModels linked.
New tests: missing-device handling, sample decoding (Float32/Int16/Int32, interleaved, partial frames, unreadable formats), first-audio gating (including a silent capture failing instead of announcing Listening), default-input changes and liveness handling in the capture, and Keep Last Dictation (off by default, paste again with Auto-Paste on and off, kept after a failed paste, discarded when turned off or on shutdown, clipboard-only recordings).
Hardware: the guarded real-capture test passes (OPENWRITR_HARDWARE_TESTS=1). The maintainer ran the build with System Default and explicit selection on AirPods Pro/Max and the built-in microphone with AirPods connected: 22 recordings, none failed, full length; first audio after about 11-65 ms.
Not verified by hand: clicking Paste Last Dictation in the real menu into another app (covered by view-model tests with a fake paster only), and that the start tone is audible on the AirPods after the first-audio change.
Impact
User or operational risk: the capture path used by every dictation changes. Covered by tests and the hardware checks above. Microphone permission works as before.
Security or privacy impact (permissions, network destinations, stored or transmitted data): none new. Kept dictation text is memory-only and never logged; the setting is off by default.
Compatibility or migration impact (macOS versions, saved preferences, update path): none. macOS 14 minimum unchanged; one new preference (keepLastDictationEnabled, default off).
Documentation
README, docs, and changelog are updated where the change is user-visible (README, docs/index.html, AGENTS.md, changelog 1.7.4)
AVAudioEngine couples input and output in one I/O unit. A Bluetooth headset switching profile stopped the engine and lost the start of the recording, and the built-in microphone selected while a headset was the output delivered no samples. Read the input device directly through an IOProc so the output device is irrelevant.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The tone played as soon as the capture was requested, before a Bluetooth headset had switched its output profile, so it was not heard. Wait for the first audio before showing Listening and playing the tone; the capture records meanwhile.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-resolve default input after device changes during recording
Sources/OpenWritr/AudioEngine.swift:605
The running-capture shortcut ignores a default-input change. The default-input listener schedules this method, but activeDeviceID is never re-resolved, so when the old device remains available this branch returns and the rest of the recording continues on the old microphone. Resolve the current System Default before this shortcut and recover when its device ID differs from activeDeviceID, while preserving the existing explicit-device loss behavior.
Avoid claiming unverified AirPods Max dictation support
CHANGELOG.md:11
This release note states that AirPods Max dictation works, but the PR validation explicitly says AirPods Max and the built-in microphone while worn have not been checked and that the change should not ship before those cases are confirmed. Either complete that validation or qualify the note so the published release notes do not claim an unverified fix.
Adds Keep Last Dictation and Paste Last Dictation to the menu bar menu. The
text is held in memory only and discarded when the setting is turned off or
the app quits. Also bumps the version to 1.7.4.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
trsdn
changed the title
fix(audio): capture through an input-only Core Audio callback
feat: Bluetooth-safe capture, audible start tone, paste last dictation (1.7.4)
Oct 13, 2026
Report unsupported audio formats with accurate guidance
Sources/OpenWritr/HALInputCapture.swift:112
Using engineStartFailed here gives this format error the recovery suggestion “Check Microphone access…”. Changing permission cannot make an unsupported 24-bit or compressed device format readable, so users receive misleading remediation. Add a distinct unsupported-format error with guidance to choose another input device or support that format.
Validate AirPods Max support before claiming the fix
CHANGELOG.md:11
This release note states that AirPods Max dictation works, but the PR validation explicitly says AirPods Max has not been checked and requires a manual check before merge. Either complete that hardware validation or avoid claiming this specific device is fixed until it is confirmed.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The HAL callback performs non-real-time-safe work, silently ignores live format failures, and the retention behavior conflicts with its privacy contract.
Avoid blocking Core Audio callback with state queries and allocations
Sources/OpenWritr/HALInputCapture.swift:166
handleInput is the synchronously dispatched HAL I/O callback, but this queries Core Audio state for every buffer; AudioObjectGetPropertyData may lock or block, and the subsequent array/AVAudio allocations add more unbounded work. Stalling this callback can drop microphone samples under load. Cache format changes outside the callback and keep the I/O path to a preallocated copy/ring buffer, with conversion on a consumer queue.
Report unsupported audio format instead of silently dropping input
Sources/OpenWritr/HALInputCapture.swift:168
A live switch to an unsupported or temporarily unreadable format silently drops every callback here. startedFlag remains true and no failure is reported, so after initial audio has arrived the UI can remain in Listening while the recording no longer grows. Report the format failure (or negotiate a supported client format) instead of treating the capture as healthy.
Transcript fields remain retained when privacy toggle is disabled
Sources/OpenWritr/OpenWritrApp.swift:1757
This guard creates a separate lastDictation, but it does not make the setting control in-memory retention: lastTranscription and, for enhanced output, lastRawTranscription are assigned unconditionally immediately above and remain available to Debug Mode after the toggle is turned off. That contradicts the stated guarantee that the text is not kept unless enabled and is discarded when disabled. Either gate/clear the legacy transcript fields too, or describe this toggle as controlling repaste availability rather than retention.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request
Summary
Release 1.7.4: Bluetooth headset capture, a start tone that is actually heard, and a way to paste the last dictation again.
1. Input-only Core Audio capture (Bluetooth headsets)
AVAudioEnginecouples input and output in one I/O unit, so with AirPods the capture depended on the output device. Switching the headset to its microphone profile stopped the engine and lost the start of recordings (measured: 2.9 s / 2.8 s / 3.2 s recordings held 0.17 s / 1.2 s / 2.4 s of audio), and selecting the built-in microphone while AirPods were the output could deliver 0 samples.HALInputCapture(AudioDeviceCreateIOProcIDWithBlockon the chosen device) replaces the engine, so the output device is irrelevant. It reads the device stream format on every callback (Float32, Int16, Int32; rate changes followed), takes the first channel as mono Float32, and the existing 16 kHz conversion is unchanged.AudioEnginekeeps its public API. TheObjCExceptionCatchertarget is now unused but left inPackage.swift.2. Start tone and Listening overlay wait for the first audio
With a headset the tone played before the output profile switch (about 107-121 ms after capture start) and was not heard.
AudioCapturing.waitForFirstAudio(handle:timeout:)(2 s timeout) now gates the tone and the Listening overlay. The capture already records, so nothing is lost, and releasing the key during the wait still transcribes.3. Keep Last Dictation / Paste Last Dictation (new)
For long dictations whose target window is gone. A menu bar toggle keeps the last text in memory only (never on disk or in logs; discarded when the setting is turned off or the app quits; kept even if pasting failed). Paste Last Dictation pastes it again into the frontmost app with the same routing as a fresh dictation (typing in Windows App when enabled), even with Auto-Paste off, after a short delay so the menu has closed. Off by default.
Supersedes #129, which did not address the cause. Version bumped to 1.7.4.
Related issue
Maintainer reports: dictation fails with AirPods Max/Pro and with the built-in microphone while they are worn; paste fails when the window is gone. Related to #119 and #114. Does not close them.
Prepared with an AI assistant using Copilot SDK in VS Code.
Validation
swift build -c release -Xswiftc -warnings-as-errors,swiftlint lint --strict, andswift testsucceed locallyFor audio, hotkey, paste, overlay, or update changes: I ran the built app and checked the affected flow
swift build -c release -Xswiftc -warnings-as-errors: passed.swiftlint lint --strict: 0 violations.swift test: 119 tests passed. Python suite: 23 passed.plutil -lint Info.plist: OK.scripts/verify-apple-intelligence-linked.sh: FoundationModels linked.New tests: missing-device handling, sample decoding (Float32/Int16/Int32, interleaved, partial frames, unreadable formats), first-audio gating (including a silent capture failing instead of announcing Listening), default-input changes and liveness handling in the capture, and Keep Last Dictation (off by default, paste again with Auto-Paste on and off, kept after a failed paste, discarded when turned off or on shutdown, clipboard-only recordings).
Hardware: the guarded real-capture test passes (
OPENWRITR_HARDWARE_TESTS=1). The maintainer ran the build with System Default and explicit selection on AirPods Pro/Max and the built-in microphone with AirPods connected: 22 recordings, none failed, full length; first audio after about 11-65 ms.Not verified by hand: clicking Paste Last Dictation in the real menu into another app (covered by view-model tests with a fake paster only), and that the start tone is audible on the AirPods after the first-audio change.
Impact
keepLastDictationEnabled, default off).Documentation