12

我正在创建一个节拍器作为更大应用程序的一部分,并且我有一些非常短的 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 中编码时,哪种框架/方法最适合调度声音?

4

3 回答 3

4

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

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

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

于 2015-09-21T20:45:11.970 回答
4

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

// 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()
}
于 2015-09-28T07:53:11.970 回答
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 个节点。

于 2017-12-09T10:27:34.453 回答