使用AVAudioEngine为低延迟节拍器调度声音

13

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

代码概述:

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

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

我的问题是:是否可以使用AVAudioEngine安排一系列声音,并具有低延迟,然后循环该序列?如果不行,那么在使用Swift编写代码时,哪个框架/方法最适合安排声音?


不确定是否有帮助,但是可以尝试使用TheAmazingAudioEngine。它是用Objective-C编写的,但可以作为Swift框架使用。 - Sega-Zero
我已经简要地查看了TAAE,它可能是最好的选择,但我希望有一种更本地化的方法。 - blwinters
3个回答

6
我能够制作出一个包含文件声音和所需长度静默的缓冲区。希望这能帮助到你:
// 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)使用不同的音频文件。真正的诀窍是创建一个包含整个节拍和小节的一整个小节的缓冲区,然后循环整个缓冲区/小节。上面的答案对于基本的节拍器或点击轨迹非常完美,但它无法支持至少需要两种不同声音的“真正”节拍器。 - blwinters
我认为你可以尝试使用两个AVAudioPlayerNode实例构建图形(每种类型一个)。第一个作为“基本”节拍器播放,另一个仅在强拍上播放。 - 5hrp
最终,这是我采用的方法,我已经让单击功能正常工作。将来,我可能会尝试多个节点,并使用atTime参数创建所需的偏移量。 - blwinters

5
我认为实现最小误差播放声音的可能途径之一就是通过回调直接提供音频样本。在iOS中,您可以使用AudioUnit来实现此目的。
在这个回调中,您可以跟踪样本计数,并知道当前所在的样本数。通过样本计数,您可以转换为时间值(使用采样率),并将其用于高级任务,例如节拍器。如果您发现是播放节拍器声音的时候,您只需要从该声音开始复制音频样本到缓冲区即可。
这是一个理论部分,没有任何代码,但您可以找到许多关于AudioUnit和回调技术的示例。

谢谢,我需要更深入地研究一下。 - blwinters

3

进一步解释5hrp的回答:

假设有两个节拍,一个上升拍(音调1)和一个下降拍(音调2),你想让它们互相不同步,这样音频会按照某个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个节点。

在我的使用案例中,这非常有帮助。我想知道如何使其更具可扩展性,以避免在一个栏中创建与注释数量相同的节点。 - mikro098

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接