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

Using AVAudioEngine to schedule sounds for low-latency metronome

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

代码大纲:

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

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

我的问题是:是否可以使用 AVAudioEngine 安排低延迟的声音序列,然后循环播放该序列?如果不是,哪个 framework/approach 最适合在 Swift 编码时安排声音?

我认为以尽可能低的时间错误播放声音的一种可能方法是直接通过回调提供音频样本。在 iOS 中,您可以使用 AudioUnit 来做到这一点。

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

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

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

// 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()
}

扩展 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 个节点。