使用 AVAudioEngine 为低延迟节拍器安排声音

Posted

技术标签:

【中文标题】使用 AVAudioEngine 为低延迟节拍器安排声音【英文标题】:Using AVAudioEngine to schedule sounds for low-latency metronome 【发布时间】:2015-12-15 00:15:57 【问题描述】:

我正在创建一个节拍器作为大型应用程序的一部分,并且我有一些非常短的 wav 文件可用作单独的声音。我想使用 AVAudioEngine 因为 NSTimer 存在严重的延迟问题,而且 Core Audio 在 Swift 中实现似乎相当令人生畏。我正在尝试以下方法,但我目前无法实现前 3 个步骤,我想知道是否有更好的方法。

代码大纲:

    根据节拍器的当前设置(每小节的节拍数和每节拍的细分;文件 A 用于节拍,文件 B 用于细分)创建文件 URL 数组 根据文件的速度和长度以编程方式创建具有适当数量的静音帧的 wav 文件,并将其插入到每个声音之间的数组中 将这些文件读入单个 AudioBuffer 或 AudioBufferList audioPlayer.scheduleBuffer(buffer, atTime:nil, options:.Loops, completionHandler:nil)

到目前为止,我已经能够播放单个声音文件的循环缓冲区(第 4 步),但我无法从文件数组构造缓冲区或以编程方式创建静音,也没有找到任何*** 上的答案解决了这个问题。所以我猜这不是最好的方法。

我的问题是:是否可以使用 AVAudioEngine 安排一系列低延迟的声音,然后循环该序列?如果不是,在 Swift 中编码时,哪种框架/方法最适合调度声音?

【问题讨论】:

不确定这是否有帮助,但请尝试TheAmazingAudioEngine。它是用objective c编写的,但可以在swift中用作框架 我简要地研究了 TAAE,它可能是最好的选择,但我希望有一种更原生的方法。 【参考方案1】:

我能够制作一个缓冲区,其中包含来自文件的声音和所需长度的静音。希望这会有所帮助:

// audioFile here – an instance of AVAudioFile initialized with wav-file
func tickBuffer(forBpm bpm: Int) -> AVAudioPCMBuffer 
    audioFile.framePosition = 0 // position in file from where to read, required if you're read several times from one AVAudioFile
    let periodLength = AVAudioFrameCount(audioFile.processingFormat.sampleRate * 60 / Double(bpm)) // tick's length for given bpm (sound length + silence length)
    let buffer = AVAudioPCMBuffer(PCMFormat: audioFile.processingFormat, frameCapacity: periodLength)
    try! audioFile.readIntoBuffer(buffer) // sorry for forcing try
    buffer.frameLength = periodLength // key to success. This will append silcence to sound
    return buffer


// player – instance of AVAudioPlayerNode within your AVAudioEngine
func startLoop() 
    player.stop()
    let buffer = tickBuffer(forBpm: bpm)
    player.scheduleBuffer(buffer, atTime: nil, options: .Loops, completionHandler: nil)
    player.play()

【讨论】:

这很有帮助,尤其是使用 buffer.frameLength 来静音,但仍然不允许为每种类型的节拍使用不同的音频文件(即声音 A 代表强拍,声音 B 代表节拍,而声音C 用于细分)。真正的技巧是创建一个包含一整条节拍和细分的缓冲区,然后循环整个缓冲区/小节。上面的答案非常适合基本的节拍器或点击音轨,但它不能支持需要至少两种不同声音的“真实”节拍器。 我认为您可以尝试使用 AVAudioPlayerNode 的两个实例(每种类型的“tick”一个)来构建图形。第一个用作“基本”节拍器,而另一个仅用于强节拍。 最后,这就是我采用的方法,并且单击即可工作。将来我可能会尝试多个节点并使用 atTime 参数来创建所需的偏移量。【参考方案2】:

我认为以尽可能低的时间错误播放声音的一种可能方法是直接通过回调提供音频样本。在 ios 中,您可以使用 AudioUnit 执行此操作。

在此回调中,您可以跟踪样本计数并了解您现在的样本。从样本计数器中,您可以获取时间值(使用采样率)并将其用于节拍器等高级任务。如果您发现是时候播放节拍器声音了,那么您就开始将音频样本从该声音复制到缓冲区。

这是一个没有任何代码的理论部分,但是您可以找到许多AudioUnit 和回调技术的示例。

【讨论】:

谢谢,我将不得不进一步研究。【参考方案3】:

扩展 5hrp 的回答:

举一个简单的例子,你有两个节拍,一个强拍(tone1)和一个弱拍(tone2),你希望它们彼此异相,因此音频将(上、下、上、下)一定的 bpm。

您将需要两个 AVAudioPlayerNode 实例(每个节拍一个),我们称它们为 audioNode1audioNode2

您想要同相的第一个节拍,所以正常设置:

let buffer = tickBuffer(forBpm: bpm)
audioNode1player.scheduleBuffer(buffer, atTime: nil, options: .loops, completionHandler: nil)

那么对于第二个节拍,您希望它完全异相,或者从 t=bpm/2 开始。为此,您可以使用 AVAudioTime 变量:

audioTime2 = AVAudioTime(sampleTime: AVAudioFramePosition(AVAudioFrameCount(audioFile2.processingFormat.sampleRate * 60 / Double(bpm) * 0.5)), atRate: Double(1))

你可以像这样在缓冲区中使用这个变量:

audioNode2player.scheduleBuffer(buffer, atTime: audioTime2, options: .loops, completionHandler: nil)

这将循环播放你的两个节拍,bpm/2 彼此异相!

很容易看出如何将其推广到更多节拍,以创建一个完整的小节。虽然这不是最优雅的解决方案,因为如果你想说做 16 分音符,你必须创建 16 个节点。

【讨论】:

这对我的用例很有帮助。我想知道如何使它更具可扩展性,以避免在一个栏中创建尽可能多的节点。

以上是关于使用 AVAudioEngine 为低延迟节拍器安排声音的主要内容,如果未能解决你的问题,请参考以下文章

STM32F429第15章 ThreadX系统时钟节拍和时间管理(绝对延迟和相对延迟)

使用远程 IO 针对设定长度的节拍器录制

如何使用连接到 AVAudioEngine 混音器的节点同时播放缓冲区中的多个声音

FreeRTOS 系统时钟节拍和时间管理

iOS: 零误差或极小误差的定时执行或延迟执行?

在 iOS 上使用 avaudioengine 会破坏 avplayer