feat: lower deployment target to macOS 15 with macOS 26 API fallback - #17
feat: lower deployment target to macOS 15 with macOS 26 API fallback#17matyle wants to merge 2 commits into
Conversation
matyle
commented
Jun 10, 2026
- Change deployment target from macOS 26 to macOS 15 (Sequoia)
- Add SUPPORTS_SPEECH_TRANSCRIBER compile-time flag for macOS 26 APIs
- Wrap SpeechTranscriber, SpeechAnalyzer, AnalyzerInput in conditional compilation blocks with @available(macOS 26.0, *) runtime guards
- Use SFSpeechRecognizer as fallback for speech recognition on macOS 15
- Update README with tiered system requirements
- Change deployment target from macOS 26 to macOS 15 (Sequoia) - Add SUPPORTS_SPEECH_TRANSCRIBER compile-time flag for macOS 26 APIs - Wrap SpeechTranscriber, SpeechAnalyzer, AnalyzerInput in conditional compilation blocks with @available(macOS 26.0, *) runtime guards - Use SFSpeechRecognizer as fallback for speech recognition on macOS 15 - Update README with tiered system requirements
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 32 |
| Duplication | 8 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
- Add 'System Audio' option to capture all system audio (not just single app) - Reduce silence commit timers for faster subtitle response: - ASR inactivity: 600-690ms → 400-530ms - VAD silence: 280ms → 150ms - Reduce ModeConfig thresholds for Follow mode - Change default subtitle mode from Balanced to Follow - Add AnyAudioCapture protocol for unified audio capture interface
franklioxygen
left a comment
There was a problem hiding this comment.
Review of the deployment-target lowering + System Audio / timing changes. The macOS 15 fallback work (commit 1) looks solid. The main blocker is that SystemAudioCapture (commit 2) never installs a process tap, so it will capture silence. Detailed inline comments below. Consider splitting commit 2 (System Audio + timing retune) into its own PR.
| } | ||
|
|
||
| let outputUID = try outputDevice.uid | ||
| let aggregateDescription: [String: Any] = [ |
There was a problem hiding this comment.
Critical: this will capture silence. This aggregate device is built from the default output device but never creates a process tap and never sets kAudioAggregateDeviceTapListKey. Compare to ApplicationAudioCapture above, which creates a CATapDescription via system.makeProcessTap(...) and wires it into the aggregate's tap list — that's what actually routes playback audio to the IOProc. Without a tap, the aggregate input stream has no playback signal, so handleCapturedAudio receives empty/silent buffers. For system-wide capture, use a global tap (e.g. CATapDescription(stereoGlobalTapButExcludeProcesses:)) and add it to kAudioAggregateDeviceTapListKey.
| self.aggregateDevice = aggregateDevice | ||
|
|
||
| // Use a 16-bit stereo format as fallback (44.1kHz, 2ch) | ||
| guard let tapFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 2, interleaved: false) else { |
There was a problem hiding this comment.
Hardcoded tap format. Sample rate / channels / format are hardcoded (44100 / 2ch / float32) instead of querying the real device/tap StreamBasicDescription the way ApplicationAudioCapture does. If the device actually runs at 48 kHz, the downstream converter interprets the data at the wrong rate → wrong pitch/speed → garbage ASR. Also the comment above says 16-bit stereo but the format is pcmFormatFloat32 — the comment is wrong.
| tapFormat = nil | ||
| } | ||
|
|
||
| private func handleCapturedAudio(_ ioData: UnsafeMutableAudioBufferListPointer) { |
There was a problem hiding this comment.
Fragile channel/buffer handling. This only copies ioData.first into floatChannelData[0] of a buffer declared as 2-channel, leaving channel 1 uninitialized, and it assumes a non-interleaved single-buffer layout. If the device delivers interleaved or multi-buffer audio, this produces noise or reads only half the frames. Derive channel count and layout from the actual stream format rather than assuming.
| ) { | ||
| self.queue = queue | ||
| self.audioHandler = audioHandler | ||
| self.errorHandler = errorHandler |
There was a problem hiding this comment.
errorHandler is stored but never invoked anywhere in SystemAudioCapture — all failures are thrown from start(). Either wire it up for async IOProc-time errors or drop it.
| .copy("Resources/silero_vad.onnx"), | ||
| ], | ||
| swiftSettings: [ | ||
| .define("SUPPORTS_SPEECH_TRANSCRIBER"), |
There was a problem hiding this comment.
SUPPORTS_SPEECH_TRANSCRIBER is defined unconditionally, so the #else (macOS 15 SDK) branches are dead code in the default build and never compiled or tested — you still need the macOS 26 SDK to compile; the deployment-target lowering only helps at runtime. That's a fine design (the flag is meant to be toggled off when building on an older toolchain), but please add a comment here explaining when to disable it, since no CI build exercises the #if !SUPPORTS_SPEECH_TRANSCRIBER path.
| @@ -1,4 +1,4 @@ | |||
| // swift-tools-version: 5.10 | |||
| // swift-tools-version: 5.9 | |||
There was a problem hiding this comment.
swift-tools-version downgrade 5.10 → 5.9 is unexplained — presumably to support an older Xcode. Worth confirming this is intentional and noting why.
| case .microphone: | ||
| return AppLocalization.string(.microphone, languageID: languageID) | ||
| case .systemAudio: | ||
| return "System Audio" |
There was a problem hiding this comment.
Hardcoded English string bypasses AppLocalization — every other source uses AppLocalization.string(...). Same for InputSource.systemAudio's name/detail below. The zh-CN UI will show English here.
| minSilenceCommitMs: 200 | ||
| ) | ||
|
|
||
| static let follow = ModeConfig( |
There was a problem hiding this comment.
These timing retunes (plus the default mode change balanced → follow in AppSettings) are a behavior change for all users, unrelated to the deployment-target goal of this PR. Recommend splitting into a separate PR with rationale/validation — the commit message asserts the new numbers but there's no evidence they were tested.