Я пытаюсь синхронизировать записанный звук (из 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")
}
При запуске примера приложения я получаю следующий результат: