swift - 如何使用 swift 同步核心音频的输入和播放

标签 swift audio synchronization fft core-audio

我创建了一个应用程序,用于进行声学测量。该应用程序生成对数正弦扫描刺激,当用户按下“开始”时,应用程序会同时播放刺激声音,并记录麦克风输入。

所有相当标准的东西。我正在使用核心音频,因为我想真正深入研究不同的功能,并可能使用多个接口(interface),所以必须从某个地方开始学习。

这是针对 iOS 的,因此我正在创建一个带有用于输入和输出的 RemoteIO 音频单元的 AUGraph。我已经声明了音频格式,它们是正确的,因为没有显示错误,并且 AUGraph 初始化、启动、播放声音和记录。

我在混音器的输入 1 的输入范围上有一个渲染回调。 (即,每次需要更多音频时,都会调用渲染回调,并将一些样本从我的浮点刺激数组中读取到缓冲区中)。

let genContext = Unmanaged.passRetained(self).toOpaque()
var genCallbackStruct = AURenderCallbackStruct(inputProc: genCallback,
                                                      inputProcRefCon: genContext)
    AudioUnitSetProperty(mixerUnit!, kAudioUnitProperty_SetRenderCallback,
                         kAudioUnitScope_Input, 1, &genCallbackStruct,
                         UInt32(MemoryLayout<AURenderCallbackStruct>.size))

然后,我有一个输入回调,每次远程IO输入的输出范围上的缓冲区已满时都会调用该回调。此回调将样本保存到数组中。

var inputCallbackStruct = AURenderCallbackStruct(inputProc: recordingCallback,
                                                      inputProcRefCon: context)
    AudioUnitSetProperty(remoteIOUnit!, kAudioOutputUnitProperty_SetInputCallback,
                                  kAudioUnitScope_Global, 0, &inputCallbackStruct,
                                  UInt32(MemoryLayout<AURenderCallbackStruct>.size))

一旦刺激到达最后一个样本,AUGraph 就会停止,然后我将刺激和记录的数组写入单独的 WAV 文件,以便我可以检查数据。我发现目前记录的输入和刺激之间存在大约 3000 个样本延迟。

enter image description here

虽然很难看到波形的开始(扬声器和麦克风都可能检测不到那么低),但刺激的结束(底部 WAV)和录制的内容应该大致对齐。

音频会有传播时间,我意识到这一点,但在 44100Hz 采样率下,这是 68 毫秒。核心音频旨在降低延迟。

所以我的问题是,任何人都可以解释这个看起来相当高的额外延迟

我的inputCallback如下:

let recordingCallback: AURenderCallback = { (
    inRefCon,
    ioActionFlags,
    inTimeStamp,
    inBusNumber,
    frameCount,
    ioData ) -> OSStatus in

    let audioObject = unsafeBitCast(inRefCon, to: AudioEngine.self)

    var err: OSStatus = noErr

    var bufferList = AudioBufferList(
        mNumberBuffers: 1,
        mBuffers: AudioBuffer(
            mNumberChannels: UInt32(1),
            mDataByteSize: 512,
            mData: nil))

    if let au: AudioUnit = audioObject.remoteIOUnit! {
        err = AudioUnitRender(au,
                              ioActionFlags,
                              inTimeStamp,
                              inBusNumber,
                              frameCount,
                              &bufferList)
    }

    let data = Data(bytes: bufferList.mBuffers.mData!, count: Int(bufferList.mBuffers.mDataByteSize))
    let samples = data.withUnsafeBytes {
        UnsafeBufferPointer<Int16>(start: $0, count: data.count / MemoryLayout<Int16>.size)
    }
    let factor = Float(Int16.max)
    var floats: [Float] = Array(repeating: 0.0, count: samples.count)
    for i in 0..<samples.count {
        floats[i] = (Float(samples[i]) /  factor)
    }

    var j = audioObject.in1BufIndex
    let m = audioObject.in1BufSize
    for i in 0..<(floats.count) {
        audioObject.in1Buf[j] = Float(floats[I])

    j += 1 ; if j >= m { j = 0 }   
    }
    audioObject.in1BufIndex = j
    audioObject.inputCallbackFrameSize = Int(frameCount)        
    audioObject.callbackcount += 1        
    var WindowSize = totalRecordSize / Int(frameCount)                  
    if audioObject.callbackcount == WindowSize {

        audioObject.running = false

    }

    return 0
}

因此,从引擎启动时起,应该在从remoteIO收集到第一组数据后调用此回调。 512 个样本,这是默认分配的缓冲区大小。它所做的就是从有符号整数转换为浮点型,并保存到缓冲区。 in1BufIndex 的值是对写入的数组中最后一个索引的引用,每个回调都会引用并写入该索引,以确保数组中的数据对齐。

目前,在听到捕获的扫频之前,记录的数组中似乎有大约 3000 个静音样本。通过在 Xcode 中调试来检查记录的数组,所有样本都有值(是的,前 3000 个非常安静),但不知怎的,这并没有加起来。

下面是用于播放我的刺激的生成器回调

let genCallback: AURenderCallback = { (
inRefCon,
ioActionFlags,
inTimeStamp,
inBusNumber,
frameCount,
ioData) -> OSStatus in

let audioObject = unsafeBitCast(inRefCon, to: AudioEngine.self)
for buffer in UnsafeMutableAudioBufferListPointer(ioData!) {
    var frames = buffer.mData!.assumingMemoryBound(to: Float.self)
    var j = 0
    if audioObject.stimulusReadIndex < (audioObject.Stimulus.count - Int(frameCount)){
        for i in stride(from: 0, to: Int(frameCount), by: 1) {

            frames[i] = Float((audioObject.Stimulus[j + audioObject.stimulusReadIndex]))

            j += 1

            audioObject.in2Buf[j + audioObject.stimulusReadIndex] = Float((audioObject.Stimulus[j + audioObject.stimulusReadIndex]))
        }

        audioObject.stimulusReadIndex += Int(frameCount)      
    }
}
return noErr;
}

最佳答案

至少有 4 个因素会导致往返延迟。

512 个样本或 11 毫秒是在 RemoteIO 调用回调之前收集足够样本所需的时间。

声音的传播速度约为每毫秒 1 英尺,是往返的两倍。

DAC 有输出延迟。

多个 ADC(您的 iOS 设备上有超过 1 个麦克风)需要时间来采样和后处理音频(用于 sigma-delta、波束形成、均衡等)。后处理可能以 block 为单位完成,因此会产生为一个 block 收集足够样本(未记录的数字)的延迟。

在 ADC 和系统内存之间移动数据(某些未知 block 大小的硬件 D​​MA?)时可能还会增加开销延迟,以及驱动程序和操作系统上下文切换开销。

启动音频硬件子系统(放大器等)时也存在启动延迟,因此最好在输出声音(频率扫描)之前开始播放和录制音频。

关于swift - 如何使用 swift 同步核心音频的输入和播放,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56383990/

相关文章:

ios - 如何在iPhone中快速从文件路径url获取视频或图像?

swift Eureka : Update Eureka tableView's style

ios - 如何在 UITableView 底部添加 UIRefreshControl?

actionscript - 事件监听器调用了错误的函数

c# - 我可以从用户应用程序访问由 Windows 服务创建的互斥量吗?

swift - 如何将 AVPlayerLayer 显示为全屏?

ios - 为什么我的 UILocalNotification 不播放任何声音?

c - ALSA:防止扬声器欠载的方法

android-studio - 无法解析 '...' : Could not resolve project :react-native-navigation 的依赖关系

c# - 使用 MS Sync Framework 2.0,如何最好地处理相关表?