2015-09-18 3 views
10

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

Код очертание:

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

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

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

+0

Не уверен, что это помогает, но попробуйте [TheAmazingAudioEngine] (https://github.com/TheAmazingAudioEngine/TheAmazingAudioEngine). Он написан в объективе c, но может быть использован как основа в быстром –

+0

Я кратко рассмотрел TAAE, и это может быть лучший вариант, хотя я надеюсь, что есть более привычный подход. – blwinters

ответ

2

Я смог создать буфер, содержащий звук из файла и тишину требуемой длины. Надеюсь, что это поможет:

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

Это полезно, особенно с использованием 'buffer.frameLength' для тишины, но все же не позволяет использовать другой аудиофайл для каждого типа ритма (т. Е. Звук A для downbeats, звук B для ударов и звук C для подразделений). Реальный трюк состоял бы в создании буфера, содержащего всю строку битов и подразделений, а затем петлю весь буфер/бар. Вышеупомянутый ответ идеально подходит для базового метронома или клика, но он не может поддерживать «настоящий» метроном, которому требуется как минимум два разных звука. – blwinters

+0

Я думаю, вы можете попытаться построить график с двумя экземплярами AVAudioPlayerNode (по одному для каждого типа «тика»). Первый играет как «базовый» метроном, а другой - только при сильных ударах. – 5hrp

+0

В конце концов, это подход, с которым я пошел, и я получил один клик. В будущем я могу попробовать несколько узлов и использовать параметр atTime для создания необходимого смещения. – blwinters

3

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

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

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

+0

Спасибо, мне нужно будет изучить это больше. – blwinters

2

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

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

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

Это будет играть на петле ваши два удара, уд/2 из фазы друг от друга!

Легко понять, как обобщить это на большее количество ударов, чтобы создать целую панель. Это не самое элегантное решение, потому что, если вы хотите сказать, что нужно делать 16-ю ноты, вам нужно будет создать 16 узлов.