You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何同步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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 23:55:08