Использование AVAudioEngine для планирования звуков для метронома с низкой задержкой

Я создаю метроном как часть более крупного приложения, и у меня есть несколько очень коротких wav файлов для использования в качестве отдельных звуков. Я хотел бы использовать AVAudioEngine, потому что NSTimer имеет значительные проблемы с задержкой, и Core Audio кажется довольно сложным для реализации в Swift. Я пытаюсь сделать следующее, но в настоящее время я не могу выполнить первые 3 шага, и мне интересно, есть ли лучший способ.

Схема кода:

  • Создайте массив URL-адресов файлов в соответствии с текущими настройками метронома (количество ударов на панель и разбиение на бит, файл A для бит, файл B для подразделений)
  • Программно создайте wav файл с соответствующим количеством кадров молчания на основе темпа и длины файлов и вставьте его в массив между каждым звуком
  • Прочитайте эти файлы в одном AudioBuffer или AudioBufferList
  • audioPlayer.scheduleBuffer(buffer, atTime:nil, options:.Loops, completionHandler:nil)

До сих пор мне удалось воспроизвести циклный буфер (шаг 4) одного звукового файла, но мне не удалось создать буфер из массива файлов или создать тишину программно, и я не нашел никаких ответы на StackOverflow, которые обращаются к этому. Поэтому я предполагаю, что это не лучший подход.

Мой вопрос: Возможно ли запланировать последовательность звуков с низкой задержкой, используя AVAudioEngine, а затем закодировать эту последовательность? Если нет, какая структура/подход лучше всего подходит для планирования звуков при кодировании в Swift?

Ответ 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 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()
}

Ответ 2

Я думаю, что одним из возможных способов воспроизведения звуков с минимально возможной временной ошибкой является предоставление аудиокассет непосредственно через обратный вызов. В iOS вы можете сделать это с помощью AudioUnit.

В этом обратном вызове вы можете отслеживать количество отсчетов и знать, на каком образце вы сейчас находитесь. С помощью счетчика образцов вы можете перейти к значению времени (с использованием частоты дискретизации) и использовать его для своих задач высокого уровня, таких как метроном. Если вы видите, что пришло время воспроизвести звук метронома, вы просто начинаете копировать звуковые фрагменты из этого звука в буфер.

Это теоретическая часть без какого-либо кода, но вы можете найти много примеров AudioUnit и метода обратного вызова.

Ответ 3

Чтобы расширить ответ на 5hrp:

Возьмите простой случай, когда у вас есть два бита, оптимистичный (тон1) и приглушенный (тон 2), и вы хотите, чтобы они были не в фазе друг с другом, поэтому звук будет (вверх, вниз, вверх, вниз) до определенный уд/мин.

Вам понадобятся два экземпляра 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 узлов.