AVAudioEngine: одновременный запуск набора узлов AVAudioPlayerNode
Я работаю над приложением, которое запускает несколько циклов одновременно и должно поддерживать их синхронизацию.
Используя предыдущие, наивные подходы к проблеме (не используя AVAudioEngine), я обнаружил, что программный запуск ряда аудиоплееров в последовательности дает достаточную задержку между вызовами, что делает результаты бесполезными; удары были слышны не синхронно.
Могу ли я достичь такой функциональности с помощью AVAudioEngine?
В настоящее время я подключаю AVAudioPlayerNodes к микшеру, и я прикрепил к ним буферы и управляю вводом оттуда. Но могу ли я их все запустить одновременно?
Кажется, что узлы не начинают воспроизводить звук до тех пор, пока я не вызову play, а этого нельзя сделать до запуска двигателя...
3 ответа
Для более подробного объяснения взгляните на мой ответ в
AVAudioEngine несколько AVAudioInputNodes не воспроизводятся в идеальной синхронизации
Вот вкратце:
Если ваш движок уже запущен, вы получили @property lastRenderTime в AVAudioNode - суперклассе вашего плеера - это ваш билет на 100% точную синхронизацию кадров сэмплов...
AVAudioFormat *outputFormat = [playerA outputFormatForBus:0];
const float kStartDelayTime = 0.0; // seconds - in case you wanna delay the start
AVAudioFramePosition startSampleTime = playerA.lastRenderTime.sampleTime;
AVAudioTime *startTime = [AVAudioTime timeWithSampleTime:(startSampleTime + (kStartDelayTime * outputFormat.sampleRate)) atRate:outputFormat.sampleRate];
[playerA playAtTime: startTime];
[playerB playAtTime: startTime];
[playerC playAtTime: startTime];
[playerD playAtTime: startTime];
[player...
Между прочим - вы можете достичь того же 100% точного результата сэмплированного кадра с классом AVAudioPlayer...
NSTimeInterval startDelayTime = 0.0; // seconds - in case you wanna delay the start
NSTimeInterval now = playerA.deviceCurrentTime;
NSTimeIntervall startTime = now + startDelayTime;
[playerA playAtTime: startTime];
[playerB playAtTime: startTime];
[playerC playAtTime: startTime];
[playerD playAtTime: startTime];
[player...
Без startDelayTime первые 100-200 мсек всех игроков будут обрезаны, потому что команда запуска фактически отводит время циклу выполнения, хотя игроки уже начали (ну, по расписанию) на 100% в синхронизации в настоящее время. Но с startDelayTime = 0,25 вы готовы. И никогда не забывайте заранее готовить проигрыватели к своим игрокам, чтобы в начальный момент не требовалось выполнять дополнительную буферизацию или настройку - просто запустите их, ребята;-)
Правильный способ сделать это - использовать аудиоустройство микшера. Минимально у вас будет график с микшером и удаленным интерфейсом.
Создайте микшер с двумя входами. Вытягивающая архитектура аудиосистемы iOS будет одновременно воспроизводить два ваших аудио буфера.
Я добился нужного эффекта, просматривая экземпляры AVAudioPlayerNode, которые должны быть синхронизированы, и для каждого из них:
- Называть "играть"
- Планирование запуска соответствующего буфера в будущем
Код выглядит примерно так:
for (AVAudioPlayerNode *playerNode in playerNodes) {
[playerNode play];
[playerNode scheduleBuffer:loopBufferForThisPlayer atTime:startTime options:AVAudioPlayerNodeBufferLoops completionHandler:nil];
}
Время, указанное в startTime, - это время в ближайшем будущем (300 мс или около того), чтобы все игровые узлы были готовы к игре. startTime должно основываться на времени хоста; основывайте время на вашем образце, и все будет не синхронизировано.
Все эти узлы были присоединены к AVAudioMixerNode, как описано в вопросе.
Я не заинтересован в использовании задержки таким образом; Я бы предпочел, чтобы мои узлы сообщали мне, когда они были готовы к работе... но это работает.