Repository navigation
Conversation
冷启动录音时 AVAudioEngine 会先按系统默认输入(蓝牙/连续互通麦克风) 实现输入节点,再改绑到目标设备,并在默认输入输出不同时创建聚合设备; 这些都在主线程同步执行,导致合盖外接屏、蓝牙麦克风场景下首次录音 卡顿约 6 秒且常常录到全零音频。 - 新增 Core/Audio/MicrophoneCaptureSession:仅输入 HAL 单元,在初始化前 绑定目标设备,不触碰输出设备;创建/启动/停止/设备变化均在独立控制队列, 交付队列统一转换为单声道目标采样率,并检测数字静音、设备丢失与格式变化 - MLXTranscriber 改用该会话,每次录音新建,冷/热启动走同一路径;移除 AVAudioEngine 启动超时、启动看门狗与改绑设备的旧实现 - 合盖时将笔记本内置麦克风视为不可用(LaptopLidState / MicrophoneAvailabilityPolicy),显示配置变化与唤醒时刷新设备快照 - 录音中检测到数字静音时提示检查麦克风;麦克风启动期间用户停止录音时 直接以空结果结束会话 - 诊断:采集启动日志包含设备连接方式、输入输出声道、采样率、默认输入 输出、合盖状态与分步耗时,另记录首帧延迟、静音与停止汇总;应用元信息 增加合盖状态、默认设备、全部音频设备与可用麦克风 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
将剩余的 AVAudioEngine / AVAudioRecorder 采集路径迁移到仅输入 AUHAL 采集会话,移除为其打的补丁: - 系统听写:异步启动,切换麦克风时识别请求不中断 - 远程 ASR:豆包、阿里 Fun/Qwen、StepFun、Gemini 共用一个采集入口; 文件录音改为从采集样本写出上传 WAV(AVAudioRecorder 无法使用所选麦克风), OpenAI 预览从样本快照生成,删除 WAV 头修补 - 会议与引导页麦克风检测改为非阻塞启动 - 模型调试录音与正式录音走同一采集路径 - 用"所选设备启动失败时用系统默认输入重试一次"替换三处"1.2 秒无回调 即重启"的看门狗(会误伤启动较慢的蓝牙设备) - 删除 captureTapFormat、isAvailableInputDevice 等只服务旧引擎的代码及测试 - 录音中切换麦克风统一由采集会话在后台完成,不再重建引擎 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
打开蓝牙耳机的麦克风会把耳机从媒体模式切到通话模式,正在播放的开始 提示音被截断且音量变小。当输入与当前输出属于同一副蓝牙耳机时,在提示音 结束后再启动采集;其他设备仍立即启动。提示音期间停止录音则以空结果结束。 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- AUHAL 每个 I/O 周期约 10ms,旧的 AVAudioEngine tap 约 100ms 一块; 本地 VAD 帧、远程 ASR 推流与主线程任务都依赖原有节奏。交付层按 chunkDurationSeconds(默认 0.1s)重新分块,停止时交付最后不足一块的尾部 - 会议麦克风改为异步启动后,最终启动失败通过 safetyMessage 提示并记录, 不再静默继续 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AVAudioPCMBuffer.mutableAudioBufferList 每次访问都按 frameLength 重新计算 mDataByteSize。原实现先通过一次访问改写字节数,再传入新的访问结果,且 frameLength 在渲染成功后才设置,于是每次 AudioUnitRender 都拿到 0 字节的 缓冲区并返回 kAudio_ParamError(-50),整段录音收不到任何音频。 同时补上"无音频"可见性:设备启动 2 秒内没有交付第一帧时记录警告并上报 noAudio 事件;MLX 录音在 noAudio、renderFailed 时与数字静音一样提示用户, 不再静默录出空结果。 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
背景
部分用户反馈:首次录音(以及闲置一段时间后的第一次录音)会卡住,速度明显变慢,经常录不到声音。典型环境:
用户确认 1.14.x 同样有这个问题,所以这不是 1.15 引入的回归,而是采集层一直存在的问题。
日志分析结论
Recording started之后,下一条日志总在 +6.1 秒出现;同一次模型加载的 3 个等待方中,第一个报elapsedMs≈6600,另外两个只有 ~500ms;Esc 和停止键都要等到 +6.1 秒才被处理adjusted input tap format … nodeSampleRate=16000,16k 正是 DJI 的采样率audioSpanMs=27300, levelMax=0.000,热启动则全部正常levelMax=0.000,结果为空;手动切到 DJI 后立刻正常根因
所有麦克风采集(9 个 tap 和 1 个
AVAudioRecorder)都用同一套写法:AVAudioEngine.inputNode是第一次访问时才创建的,创建时先按系统默认输入初始化,之后才通过AudioUnitSetProperty(CurrentDevice)改绑到目标设备;CADefaultDeviceAggregate;captureTapFormat采样率修正、4 份重复的applyPreferredInputDeviceIfNeeded、带 6 秒超时的engine.start、3 套"1.2 秒没有回调就重启"的看门狗,都是在给这个写法打补丁。另外,AVAudioRecorder不支持指定设备,所以远程 ASR 文件模式一直在录系统默认输入。修复方案
1. 新的统一采集组件
Core/Audio/MicrophoneCaptureSession采用 Apple TN2091 的标准做法:只开输入的 HAL output unit(AUHAL)。
AudioUnitInitialize之前就绑定目标设备,从头到尾不打开系统默认输入,也不碰输出设备,所以不会产生聚合设备。AVAudioConverter,会做降混),再按 100ms 重新分块,保持和旧 tap 一样的节奏。停止时会先把已经采到的数据(包括最后不足一块的尾部)交付出去,再关闭。DeviceIsAlive)和格式变化(采样率、声道数,比如蓝牙切换模式)。格式变了就在内部重建,交付给上层的格式保持不变。2. 迁移所有采集路径
startRecordingSession()改为异步;切换麦克风时识别请求不中断startMicrophoneCapture入口;PCM16 改为正规重采样,不再是简单抽点RemoteASRPreviewAudio的 WAV 头修补safetyMessage提示并记录删除了只为旧引擎服务的代码:
captureTapFormat、isAvailableInputDevice、makeDoubaoPCM16MonoData、RemoteASRPreviewAudio,以及对应的测试。3. 设备可用性:合盖识别
LaptopLidState读取 IOKit 的AppleClamshellState。MicrophoneAvailabilityPolicy在合盖时把笔记本内置麦克风判为不可用。判断依据是连接方式为内置,并且 UID 是BuiltInMicrophoneDevice或数据源是imic。接在内置耳机孔上的有线耳机麦克风(emic)不受影响。4. 录音中的体验
5. 蓝牙耳机提示音被截断
打开蓝牙耳机的麦克风,会让耳机从媒体模式(A2DP)切到通话模式(HFP),正在播放的开始提示音会被截断,音量也会变小。
BluetoothAudioRoute通过设备地址(UID 前缀)判断输入和当前输出是否属于同一副耳机。是的话就等提示音播完再打开麦克风;其他设备仍然立即启动。6. 诊断日志(供测试版回归排查)
Microphone capture startedstepsMs=resolve,create,bind,configure,initialize,start、totalMsMicrophone capture first bufferlatencyMs、峰值Microphone capture is delivering digital silence/signal recoveredMicrophone capture stoppedMicrophone capture rebuilt/device format changed/device disappearedInternal microphone excluded because the laptop lid is closedmicrophonePriorityUIDs、laptopLid、audioDefaults、audioDevices(所有输入和输出设备,带连接方式)、availableMicrophones提交
30599d5fix: 以仅输入 AUHAL 重建麦克风采集并补充音频诊断ab5a70frefactor: 所有麦克风采集统一走 MicrophoneCaptureSession5d6e69dfix: 蓝牙耳机上先播完开始提示音再打开麦克风91ea442fix: 采集按 100ms 分块交付并提示会议麦克风启动失败ea7194bfix: 渲染前设置 frameLength,修复 AUHAL 采集始终返回 -50(首轮本地测试发现:每次AudioUnitRender都拿到 0 字节缓冲区,录音全程无音频);新增启动 2 秒内无音频的警告与提示预期效果
stepsMs和first buffer latencyMs判断行为变化
风险与已知限制
nonisolated嵌套类型、withTaskCancellationHandler这几处最需要编译器把关。AppleClamshellState,会返回unavailable,等于不做合盖过滤,不会比现在更糟。测试
新增
VoxtTests/MicrophoneCaptureSupportTests,覆盖:imic、emic、外接设备;-0和NaN;删除了
AudioInputDeviceManagerTests中关于captureTapFormat的 3 个用例,因为被测代码已经删除。合并前检查
xcodebuild build -project Voxt.xcodeproj -scheme Voxt -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NOxcodebuild test … -only-testing:VoxtTests/MicrophoneCaptureSupportTests,以及全量测试stepsMs、first buffer latencyMs、是否出现数字静音、合盖过滤是否生效🤖 Generated with Claude Code