Как синхронизировать ввод и воспроизведение для основного звука, используя swift - PullRequest
1 голос
/ 30 мая 2019

Я создал приложение, которое использую для акустических измерений.Приложение генерирует лог-синусоидальный стимул, и когда пользователь нажимает кнопку «пуск», приложение одновременно воспроизводит звук стимула и записывает вход микрофона.

Все довольно стандартные вещи.Я использую базовое аудио, так как я хочу по-настоящему углубиться в различные функциональные возможности и потенциально использовать несколько интерфейсов, поэтому мне нужно начать обучение где-нибудь.

Это для iOS, поэтому я создаю 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))

Затем у меня есть входной обратный вызов, который называетсякаждый раз, когда буфер заполнен в области вывода входа удаленного ввода-вывода.Этот обратный вызов сохраняет образцы в массив.

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) и записанные должны примерно выстроиться в линию.

Будет время распространения звука, я это понимаю, но при частоте дискретизации 44100 Гц, это 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 выборок, так как это размер выделенного буфера по умолчанию.Все, что он делает, это конвертирует целое число со знаком в Float и сохраняет в буфер.Значение in1BufIndex является ссылкой на последний индекс в массиве, в который производится запись, и на него ссылаются и записывают каждый обратный вызов, чтобы убедиться, что данные в массиве выстроены в линию.

В настоящее время кажется, что около 3000 сэмплов тишины находятся в записанном массиве, прежде чем будет слышен захваченный сигнал.Осматривая записанный массив путем отладки в Xcode, все сэмплы имеют значения (и да, первые 3000 очень тихие), но почему-то это не складывается.

Ниже приведен генератор Callback, используемый для воспроизведения моего стимула

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;
}

1 Ответ

2 голосов
/ 14 июня 2019

Может быть как минимум 4 вещи, способствующие задержке в оба конца.

512 выборок, или 11 мс, - это время, необходимое для сбора достаточного количества выборок, прежде чем remoteIO сможет вызвать ваш обратный вызов.

Звук распространяется со скоростью около 1 фута в миллисекунду, вдвое больше, чем в оба конца.

ЦАП имеет задержку на выходе.

Требуется время, необходимое для того, чтобы несколько АЦП (на вашем устройстве iOS было более 1 микрофона) сэмплировали и обрабатывали аудио (для сигма-дельта, формирования луча, выравнивания и т. Д.). Постобработка может выполняться в блоках, что приводит к задержке для сбора достаточного количества выборок (недокументированного числа) для одного блока.

Возможно также добавлена ​​задержка при перемещении данных (аппаратный DMA некоторого неизвестного размера блока?) Между АЦП и системной памятью, а также накладные расходы на переключение контекста драйвера и ОС.

Существует также задержка запуска для включения звуковых аппаратных подсистем (усилителей и т. Д.), Поэтому может быть лучше начать воспроизведение и запись звука задолго до вывода звука (развертка по частоте).

...