AVAudioEngine согласовывает / синхронизирует временные метки ввода / вывода в macOS / iOS

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

Поскольку sampleTime различается между узлами вывода и ввода AVAudioEngine, я использую hostTime для определения смещения исходного звука и входных буферов.

В iOS я бы предположил, что мне придется использовать различные свойства задержки AVAudioSession (inputLatency, outputLatency, ioBufferDuration), чтобы согласовать треки, а также смещение времени хоста, но я не придумал волшебную комбинацию, чтобы заставить их работать . То же самое касается различных свойств AVAudioEngine и Node, таких как latency и presentationLatency..

В macOS AVAudioSession не существует (за пределами Catalyst), что означает, что у меня нет доступа к этим номерам. Между тем, свойства _15 _ / _ 16_ в AVAudioNodes сообщают 0.0 в большинстве случаев. В macOS у меня действительно есть доступ к AudioObjectGetPropertyData и я могу спросить систему о kAudioDevicePropertyLatency, _21 _, _ 22_ и т. Д., Но я снова немного не понимаю, какова формула для согласования всего этого.

У меня есть образец проекта на https://github.com/jnpdx/AudioEngineLoopbackLatencyTest, который запускает простой тест с обратной связью. (в macOS, iOS или Mac Catalyst) и показывает результат. На моем Mac смещение между дорожками составляет ~ 720 сэмплов. На других Mac я видел смещение до 1500 сэмплов.

На моем iPhone я могу сделать его почти идеальным, используя AVAudioSession outputLatency + inputLatency. Однако из-за той же формулы все на моем iPad не выровнено.

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

Вот образец моего текущего кода (полную логику синхронизации можно найти по адресу https://github.com/jnpdx/AudioEngineLoopbackLatencyTest/blob/main/AudioEngineLoopbackLatencyTest/AudioManager.swift):

//Schedule playback of original audio during initial playback
let delay = 0.33 * state.secondsToTicks
let audioTime = AVAudioTime(hostTime: mach_absolute_time() + UInt64(delay))
state.audioBuffersScheduledAtHost = audioTime.hostTime

...

//in the inputNode's inputTap, store the first timestamp
audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (pcmBuffer, timestamp) in
            if self.state.inputNodeTapBeganAtHost == 0 {
                self.state.inputNodeTapBeganAtHost = timestamp.hostTime
            }
}

...

//after playback, attempt to reconcile/sync the timestamps recorded above

let timestampToSyncTo = state.audioBuffersScheduledAtHost
let inputNodeHostTimeDiff = Int64(state.inputNodeTapBeganAtHost) - Int64(timestampToSyncTo)
let inputNodeDiffInSamples = Double(inputNodeHostTimeDiff) / state.secondsToTicks * inputFileBuffer.format.sampleRate //secondsToTicks is calculated using mach_timebase_info

//play the original metronome audio at sample position 0 and try to sync everything else up to it
let originalAudioTime = AVAudioTime(sampleTime: 0, atRate: renderingEngine.mainMixerNode.outputFormat(forBus: 0).sampleRate)
originalAudioPlayerNode.scheduleBuffer(metronomeFileBuffer, at: originalAudioTime, options: []) {
  print("Played original audio")
}

//play the tap of the input node at its determined sync time -- this _does not_ appear to line up in the result file
let inputAudioTime = AVAudioTime(sampleTime: AVAudioFramePosition(inputNodeDiffInSamples), atRate: renderingEngine.mainMixerNode.outputFormat(forBus: 0).sampleRate)
recordedInputNodePlayer.scheduleBuffer(inputFileBuffer, at: inputAudioTime, options: []) {
  print("Input buffer played")
}


При запуске примера приложения я получаю следующий результат:

«Результат


person jnpdx    schedule 06.01.2021    source источник


Ответы (2)


Этот ответ применим только к родной macOS

Общее определение задержки

Вывод

В общем случае задержка вывода для потока на устройстве определяется суммой следующих свойств:

  1. kAudioDevicePropertySafetyOffset
  2. kAudioStreamPropertyLatency
  3. kAudioDevicePropertyLatency
  4. kAudioDevicePropertyBufferFrameSize

Значения смещения безопасности устройства, потока и задержки устройства должны быть получены для kAudioObjectPropertyScopeOutput.

На моем Mac для аудиоустройства MacBook Pro Speakers при 44,1 кГц это равно 71 + 424 + 11 + 512 = 1018 кадров.

Вход

Точно так же задержка ввода определяется суммой следующих свойств:

  1. kAudioDevicePropertySafetyOffset
  2. kAudioStreamPropertyLatency
  3. kAudioDevicePropertyLatency
  4. kAudioDevicePropertyBufferFrameSize

Значения смещения безопасности устройства, потока и задержки устройства должны быть получены для kAudioObjectPropertyScopeInput.

На моем Mac для аудиоустройства MacBook Pro Microphone при 44,1 кГц это равно 114 + 2404 + 40 + 512 = 3070 кадров.

AVAudioEngine

Как указанная выше информация относится к AVAudioEngine, не сразу понятно. Внутри AVAudioEngine создается частное совокупное устройство, а Core Audio по сути автоматически обрабатывает компенсацию задержки для совокупных устройств.

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

Я смог получить довольно точную синхронизацию, используя встроенный звук моего Mac, используя следующие настройки:

// Some non-zero value to get AVAudioEngine running
let startDelay = 0.1

// The original audio file start time
let originalStartingFrame: AVAudioFramePosition = AVAudioFramePosition(playerNode.outputFormat(forBus: 0).sampleRate * startDelay)

// The output tap's first sample is delivered to the device after the buffer is filled once
// A number of zero samples equal to the buffer size is produced initially
let outputStartingFrame: AVAudioFramePosition = Int64(state.outputBufferSizeFrames)

// The first output sample makes it way back into the input tap after accounting for all the latencies
let inputStartingFrame: AVAudioFramePosition = outputStartingFrame - Int64(state.outputLatency + state.outputStreamLatency + state.outputSafetyOffset + state.inputSafetyOffset + state.inputLatency + state.inputStreamLatency)

На моем Mac агрегатное устройство AVAudioEngine сообщило следующие значения:

// Output:
// kAudioDevicePropertySafetyOffset:    144
// kAudioDevicePropertyLatency:          11
// kAudioStreamPropertyLatency:         424
// kAudioDevicePropertyBufferFrameSize: 512

// Input:
// kAudioDevicePropertySafetyOffset:     154
// kAudioDevicePropertyLatency:            0
// kAudioStreamPropertyLatency:         2404
// kAudioDevicePropertyBufferFrameSize:  512

что приравнивается к следующим смещениям:

originalStartingFrame =  4410
outputStartingFrame   =   512
inputStartingFrame    = -2625
person sbooth    schedule 15.01.2021
comment
Интересно, что на моей машине (тоже MBP) мои числа схожи, но похоже, что смещение составляет ~ 300 сэмплов (при условии, что я правильно делаю расчеты). Не ужасно, но, конечно, не так близко, как хотелось бы. Заставить кого-нибудь запустить его на своем, чтобы я мог видеть. Мой kAudioStreamPropertyLatency сообщает о 0 на моей машине, что я считаю подозрительным. Прокомментирую еще раз, как только услышу цифры моего тестировщика. - person jnpdx; 15.01.2021
comment
Кстати, я обновил свое репо, чтобы включить эти числа в функцию ветки / printLowLevelLatencies (github.com/jnpdx/AudioEngineLoopbackLoopback) - person jnpdx; 15.01.2021
comment
Цифры моего тестировщика аналогичны вашим (1596 выходных, 150 входных) на MBA. На его машине это, кажется, привело к еще большему смещению, чем у меня, ~ 500 образцов. Вы случайно не знаете, почему задержку потока и размер кадра буфера следует учитывать на стороне вывода, а не на стороне ввода? - person jnpdx; 15.01.2021
comment
Мне потребовалось несколько прочтений, но я думаю, что понимаю, о чем вы говорите. Числа в моих отчетах Mac аналогичны вашим (-70 скорректированный ввод против 66 kAudioDevicePropertySafetyOffset и 1112 скорректированный вывод против 1117 для inBuffer + outBuffer + out security). Часть, которую мне не хватает, и я не понимаю из вашего сообщения, заключается в том, можно ли как-то использовать эти числа для выравнивания звука обратной петли - мой тест (без учета задержки) показывает около 750 кадров. Кажется, я не могу подогнать эти числа к этому числу. Думаешь, это возможно? Удалось выровнять звук? - person jnpdx; 18.01.2021
comment
P.S. Большое вам спасибо за работу, которую вы вложили в это - потрясающие детали и исследования. Рад дать вам награду, даже если это только сторона Mac, но я хотел бы попытаться прояснить свои последние вопросы о выравнивании. Также был бы очень рад возможности быстро поговорить об этом, если бы вы были готовы. - person jnpdx; 18.01.2021
comment
Для тех, кто столкнется с этим в будущем, вероятно, стоит отметить, что решение здесь зависит от машины и по-прежнему приводит примерно к ~ 300-700 смещениям отсчетов. - person jnpdx; 18.01.2021
comment
@jn_pdx Просмотрите правки и посмотрите, подходит ли вам этот метод. Я потерял веру в значения задержки устройства, которое я использовал перед тем, как раздать, и использовал встроенный звук моего Mac с, казалось бы, лучшими результатами. - person sbooth; 20.01.2021

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

Я работал только над уровнями HAL / AUHAL (никогда AVAudioEngine), но при обсуждении вычисления общих задержек всплывают некоторые свойства аудиоустройства / потока: kAudioDevicePropertyLatency и kAudioStreamPropertyLatency.

Немного покопавшись, я вижу те свойства, упомянутые в документации для свойства AVAudioIONode presentationLatency (https://developer.apple.com/documentation/avfoundation/avaudioionode/1385631-presentationlatency). Я ожидаю, что аппаратная задержка, о которой сообщает драйвер, будет там. (Я подозреваю, что стандартное свойство latency сообщает о задержке для входного образца, появляющегося на выходе обычного узла, и случай ввода-вывода является особым)

Это не в контексте AVAudioEngine, но вот одно сообщение из списка рассылки CoreAudio, в котором немного говорится об использовании свойств низкого уровня, которые могут предоставить дополнительную информацию: https://lists.apple.com/archives/coreaudio-api/2017/Jul/msg00035.html

person user1325158    schedule 08.01.2021
comment
presentationLatency представляет 0,0 для узлов ввода и вывода в Catalyst. На Mac он сообщает те же 399 образцов, что и AVAudioSession.sharedInstance().outputLatency (а также mainMixerNode.outputPresentationLatency). Итак, полезно знать, что эти свойства совпадают. Все обычные latency свойства сообщают 0,0 (что заставляет меня задуматься, почему они вообще существуют). Таким образом, у меня остается около 300 + образцов, которые все еще находятся на моей машине ... Теперь заглянем в ссылку на список рассылки ... - person jnpdx; 09.01.2021
comment
Ваша ссылка в конечном итоге указала мне на ветку в январе 2020 года, в которой люди обсуждали эти проблемы на iOS. По общему мнению, пользователю придется откалибровать свою систему, чтобы приблизиться к идеальной выборке. Это кажется удивительным, учитывая, что программное обеспечение для многодорожечной записи всегда должно было это делать. lists.apple.com/archives/coreaudio-api/2020/ Янв / index.html - person jnpdx; 09.01.2021