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

Ios 使用AVAudioEngine为低延迟节拍器安排声音,ios,swift,avaudioengine,avaudioplayernode,Ios,Swift,Avaudioengine,Avaudioplayernode,我正在创建一个节拍器作为一个更大的应用程序的一部分,我有一些非常短的wav文件作为单独的声音使用。我想使用AVAudioEngine,因为NSTimer存在严重的延迟问题,而且在Swift中实现核心音频似乎相当困难。我正在尝试以下步骤,但我目前无法实现前3个步骤,我想知道是否有更好的方法 代码大纲: 根据节拍器的当前设置创建文件URL数组(每个小节的节拍数和每个节拍的细分数;文件A表示节拍,文件B表示细分) 根据文件的节奏和长度,以编程方式创建具有适当静默帧数的wav文件,并将其插入到每个声音之

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

代码大纲:

  • 根据节拍器的当前设置创建文件URL数组(每个小节的节拍数和每个节拍的细分数;文件A表示节拍,文件B表示细分)
  • 根据文件的节奏和长度,以编程方式创建具有适当静默帧数的wav文件,并将其插入到每个声音之间的数组中
  • 将这些文件读入单个AudioBuffer或AudioBufferList
  • audioPlayer.scheduleBuffer(缓冲区,时间:nil,选项:。循环,completionHandler:nil)
  • 到目前为止,我已经能够播放单个声音文件的循环缓冲区(步骤4),但我还无法从文件数组构造缓冲区或以编程方式创建静默,也没有找到任何关于StackOverflow的解决方案。所以我猜这不是最好的方法


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

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

    在这个回调中,您可以跟踪样本数并知道您现在的样本数。从样本计数器,您可以转到时间值(使用样本率),并将其用于metronome等高级任务。若你们看到是时候播放节拍器的声音了,那个么你们就开始把音频样本从那个声音复制到缓冲区


    这是一个没有任何代码的理论部分,但是你可以找到许多音频单元和回调技术的例子。

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

    // 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的答案:

    举一个简单的例子,你有两个节拍,一个上行(音调1)和一个下行(音调2),你希望它们彼此不同步,这样音频将(向上、向下、向上、向下)达到某个bpm

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

    您希望处于相位的第一拍,因此设置为正常:

    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个节点

    不确定这是否有帮助,但试试看。它是用objective c编写的,但可能在swiftI中用作框架。我简要介绍了TAAE,它可能是最好的选择,尽管我希望有一种更为自然的方法。这很有帮助,特别是使用
    buffer.frameLength
    实现静音,但仍然不允许针对每种节拍使用不同的音频文件(即声音A代表下拍,声音B代表下拍,声音C代表细分)。真正的诀窍是创建一个缓冲区,包含一整条的下拍和细分,然后循环整个缓冲区/条。上述答案对于基本节拍器或点击音轨来说是完美的,但它不支持“真实”metronome至少需要两种不同的声音。我认为您可以尝试使用两个AVAudioPlayerNode实例(每种类型的“滴答声”对应一个实例)构建图形。第一个作为“基本”播放metronome和另一个仅在强节拍上使用。最后,这是我采用的方法,我实现了单击操作。将来,我可能会尝试多个节点并使用atTime参数来创建所需的偏移量。谢谢,我将不得不进一步研究。这在我的用例中非常有用。我想知道如何使其更具scala我们可以避免在一个栏中创建尽可能多的节点。