如何同步ScreenCaptureKit与麦克风音频流?
解决ScreenCaptureKit系统音频与麦克风音频同步问题
核心思路:对齐ScreenCaptureKit同步时钟与AVAudioEngine时间轴
ScreenCaptureKit的synchronizationClock(CMClock类型)是系统音频捕获的时间基准,你需要让AVAudioEngine的所有音频流都对齐这个时钟,而非默认的系统时钟,从根源上解决时间差问题。
1. 绑定PlayerNode到ScreenCaptureKit同步时钟
默认情况下AVPlayerNode使用系统时钟调度,会和ScreenCaptureKit的捕获时钟产生偏差。你可以基于SCStream的同步时钟创建AVAudioClock,并将其设置为PlayerNode的时间源:
// 初始化SCStream后获取同步时钟 guard let syncClock = stream.synchronizationClock else { return } // 创建基于CMClock的AVAudioClock let audioSyncClock = AVAudioClock(cmClock: syncClock) // 将PlayerNode的时间源设置为这个同步时钟 playerNode.setTimeSource(audioSyncClock)
这样PlayerNode调度的系统音频缓冲区会严格遵循ScreenCaptureKit的捕获时间线,和麦克风流的时间基准统一。
2. 补偿麦克风流的处理延迟
麦克风输入经过converterNode转采样会产生固定延迟,你需要计算这个延迟值,并在mixerNode中给麦克风流设置对应的时间偏移,抵消处理差异:
// 计算麦克风转采样的延迟(单位:采样帧) let converterDelayFrames = converterNode.outputPresentationLatency * converterNode.outputFormat(forBus: 0).sampleRate // 创建基于同步时钟的音频时间偏移 let delayTime = AVAudioTime(sampleTime: AVAudioFramePosition(converterDelayFrames), atRate: mixerNode.inputFormat(forBus: 0).sampleRate) // 给麦克风输入总线设置时间偏移 let micInputParams = AVAudioMixInputParameters(node: converterNode) micInputParams.setTimeOffset(delayTime, at: nil) mixerNode.inputParameters[0] = micInputParams
3. 替换PlayerNode为SinkNode(消除调度不确定性)
AVPlayerNode的scheduleBuffer存在调度延迟的不确定性,改用AVAudioSinkNode可以直接将ScreenCaptureKit的PCM数据写入音频引擎,精准控制时间点:
// 创建线程安全的缓冲区缓存队列 let audioBufferCache = DispatchQueue(label: "com.audio.buffer.cache") var cachedBuffers: [AVAudioPCMBuffer] = [] // 创建SinkNode let sinkNode = AVAudioSinkNode { (time, frameCount, inputBusNumber, inputData) -> OSStatus in audioBufferCache.sync { guard let buffer = cachedBuffers.first else { return noErr } // 将缓存的PCM数据写入引擎输入 memcpy(inputData.pointee.mBuffers.mData, buffer.floatChannelData![0], Int(frameCount * buffer.format.streamDescription.pointee.mBytesPerFrame)) cachedBuffers.removeFirst() } return noErr } // 将SinkNode添加到音频引擎图谱 engine.attach(sinkNode) engine.connect(sinkNode, to: mixerNode, format: desiredFormat) // 在SCStream回调中缓存转换后的PCM数据 func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of outputType: SCStreamOutputType) { guard let pcmBuffer = convertToPCM(sampleBuffer) else { return } audioBufferCache.sync { cachedBuffers.append(pcmBuffer) } }
4. 启用回声消除(解决麦克风拾音回声)
同步后仍有回声的话,可在麦克风输入节点启用系统内置的回声消除:
let inputNode = engine.inputNode let voiceProcessingFormat = AVAudioFormat(standardFormatWithSampleRate: 48000, channels: 1) // 启用语音处理IO,自带回声消除功能 try inputNode.setVoiceProcessingEnabled(true) // 连接至转采样节点 engine.connect(inputNode, to: converterNode, format: voiceProcessingFormat)
内容的提问来源于stack exchange,提问作者Hundley
相关产品推荐
相关产品推荐

