AVAudioEngine несколько AVAudioInputNodes не воспроизводятся в идеальной синхронизации
Я пытался использовать AVAudioEngine, чтобы запланировать воспроизведение нескольких аудиофайлов в идеальной синхронизации, но при прослушивании выходных данных между входными узлами, кажется, очень небольшая задержка. Аудио движок реализован с использованием следующего графика:
//
//AVAudioPlayerNode1 -->
//AVAudioPlayerNode2 -->
//AVAudioPlayerNode3 --> AVAudioMixerNode --> AVAudioUnitVarispeed ---> AvAudioOutputNode
//AVAudioPlayerNode4 --> |
//AVAudioPlayerNode5 --> AudioTap
// |
//AVAudioPCMBuffers
//
И я использую следующий код для загрузки образцов и планирования их одновременно:
- (void)scheduleInitialAudioBlock:(SBScheduledAudioBlock *)block {
for (int i = 0; i < 5; i++) {
NSString *path = [self assetPathForChannel:i trackItem:block.trackItem]; //this fetches the right audio file path to be played
AVAudioPCMBuffer *buffer = [self bufferFromFile:path];
[block.buffers addObject:buffer];
}
AVAudioTime *time = [[AVAudioTime alloc] initWithSampleTime:0 atRate:1.0];
for (int i = 0; i < 5; i++) {
[inputNodes[i] scheduleBuffer:block.buffers[i]
atTime:time
options:AVAudioPlayerNodeBufferInterrupts
completionHandler:nil];
}
}
- (AVAudioPCMBuffer *)bufferFromFile:(NSString *)filePath {
NSURL *fileURl = [NSURL fileURLWithPath:filePath];
NSError *error;
AVAudioFile *audioFile = [[AVAudioFile alloc] initForReading:fileURl commonFormat:AVAudioPCMFormatFloat32 interleaved:NO error:&error];
if (error) {
return nil;
}
AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:audioFile.processingFormat frameCapacity:audioFile.length];
[audioFile readIntoBuffer:buffer frameCount:audioFile.length error:&error];
if (error) {
return nil;
}
return buffer;
}
Я заметил, что проблема заметна только на устройствах, я тестирую с iPhone5s, но я не могу понять, почему аудио файлы воспроизводятся не синхронизировано, любая помощь будет принята с благодарностью.
** ОТВЕТ **
В итоге мы решили проблему с помощью следующего кода:
AVAudioTime *startTime = nil;
for (AVAudioPlayerNode *node in inputNodes) {
if(startTime == nil) {
const float kStartDelayTime = 0.1; // sec
AVAudioFormat *outputFormat = [node outputFormatForBus:0];
AVAudioFramePosition startSampleTime = node.lastRenderTime.sampleTime + kStartDelayTime * outputFormat.sampleRate;
startTime = [AVAudioTime timeWithSampleTime:startSampleTime atRate:outputFormat.sampleRate];
}
[node playAtTime:startTime];
}
Это дало каждому AVAudioInputNode достаточно времени для загрузки буферов и исправило все проблемы с синхронизацией звука. Надеюсь, что это помогает другим!
2 ответа
Проблема:
Проблема в том, что вы извлекаете ваш player.lastRenderTime при каждом запуске цикла for перед playAt:
Таким образом, вы получите разные времена для каждого игрока!
Таким образом, вы можете запустить всех игроков в цикле с play: или playAtTime: nil!!! Вы получите тот же результат с потерей синхронизации...
По той же причине, по которой ваш проигрыватель по-разному работает на разных устройствах в зависимости от скорости машины;-) Ваши текущие времена - случайные магические числа - поэтому не думайте, что они всегда будут работать, если они Просто так получилось, что вы работаете по вашему сценарию. Даже небольшая задержка из-за загруженного цикла выполнения или процессора снова выбьет вас из синхронизации...
Решение:
Что вам действительно нужно сделать, это получить ОДИН дискретный снимок now = player.lastRenderTime перед циклом и использовать этот самый якорь, чтобы получить пакетный синхронизированный старт для всех ваших игроков.
Таким образом, вам даже не нужно откладывать запуск вашего игрока. По общему признанию, система обрезает некоторые из ведущих кадров - (но, конечно, ту же сумму для каждого игрока;-) - чтобы компенсировать разницу между вашим недавно установленным сейчас (который на самом деле уже в прошлом и ушел) и фактическим playTime (который еще впереди в самом ближайшем будущем), но в конечном итоге запустите весь ваш плеер точно в синхронизации, как если бы вы действительно начали его сейчас в прошлом. Эти обрезанные кадры почти никогда не заметны, и вы будете спокойны за отзывчивость...
Если вам понадобятся эти кадры - из-за слышимых щелчков или артефактов при запуске файла / сегмента / буфера - хорошо, перенесите ваше сейчас в будущее, запустив проигрыватель с задержкой. Но, конечно, вы получите это небольшое отставание после нажатия кнопки запуска - хотя, конечно, все еще в идеальной синхронизации...
Заключение:
Смысл здесь в том, чтобы иметь единственную ссылку сейчас - для всех игроков и вызывать ваши методы playAtTime: сейчас как можно скорее после захвата этой ссылки - теперь. Чем больше разрыв, тем больше будет часть отсеченных начальных кадров - если вы не предоставите разумную задержку запуска и не добавите ее к своему текущему времени, что, конечно, вызывает неотзывчивость в виде отложенного запуска после нажатия кнопки запуска.
И всегда помните о том факте, что - независимо от того, какая задержка на каком-либо устройстве создается механизмами буферизации звука, - она НЕ влияет на синхронность любого количества проигрывателей, если все сделано надлежащим образом, описанным выше! Это не задерживает ваше аудио, либо! Просто окно, которое фактически позволяет вам слышать ваше аудио, открывается в более поздний момент времени...
Имейте в виду, что:
- Если вы выберете вариант запуска без задержки (супер-отзывчивый) и по какой-либо причине произойдут с большой задержкой (между получением текущего момента и фактическим началом вашего игрока), вы отрежете большую ведущую часть (примерно до 300 мс / 0,3 с) вашего аудио. Это означает, что когда вы запускаете проигрыватель, он запускается сразу же, но не возобновляется с позиции, которую вы недавно приостановили, а скорее (до ~300 мс) позже в вашем аудио. Таким образом, акустическое восприятие состоит в том, что воспроизведение паузы отключает часть вашего аудио на ходу, хотя все идеально синхронизировано.
- В качестве задержки запуска, которую вы указываете в вызове метода playAtTime: now + myProvidedDelay, используется фиксированное постоянное значение (которое не изменяется динамически, чтобы приспособиться к задержке буферизации или другим изменяющимся параметрам при большой загрузке системы), даже для параметра Delayed с параметром Delayed. предоставленное время задержки меньше, чем ~300 мс, может вызвать отсечение ведущих аудиосэмплов, если зависящее от устройства время подготовки превышает указанное вами время задержки.
- Максимальное количество отсечения (по замыслу) не становится больше, чем эти ~300 мс. Чтобы получить подтверждение, просто принудительно управляйте (точной выборкой) отсечением ведущих кадров, например, добавив отрицательное время задержки к текущему моменту, и вы увидите растущую отрезанную аудио-часть, увеличивая это отрицательное значение. Каждое отрицательное значение, превышающее ~300 мс, исправляется до ~300 мс. Таким образом, предоставленная отрицательная задержка в 30 секунд приведет к тому же поведению, что и отрицательное значение в 10, 6, 3 или 1 секунду, и, конечно, также будет включать отрицательные 0,8, 0,5 секунды до ~0,3
Этот пример хорошо подходит для демонстрационных целей, но отрицательные значения задержки не должны использоваться в производственном коде.
ВНИМАНИЕ:
Самое главное в многопользовательской настройке - синхронизировать ваш player.pause. По состоянию на июнь 2016 года в AVAudioPlayerNode до сих пор нет стратегии синхронизированного выхода.
Просто небольшой поиск метода или выход из системы чего-либо на консоль между двумя player.pause вызовами может заставить последний выполнить один или несколько кадров / сэмплов позже, чем первый. Так что ваш игрок на самом деле не остановится на той же относительной позиции во времени. И прежде всего - разные устройства дают разное поведение...
Если вы теперь начнете их вышеупомянутым (синхронизированным) образом, эти несинхронизированные текущие позиции игроков вашей последней паузы обязательно будут принудительно синхронизированы с вашей новой позицией сейчас при каждом playAtTime: означает, что вы распространяете потерянные сэмплы / кадры в будущее с каждым новым стартом вашего игрока. Это, конечно, складывается с каждым новым циклом пуска / паузы и увеличивает разрыв. Сделайте это пятьдесят или сто раз, и вы уже получите хороший эффект задержки без использования эффекта-аудио-устройства;-)
Так как мы не имеем (по предоставленной системе) контроля над этим фактором, единственное средство - сделать все вызовы игроку. Делайте паузу один за другим в строгой последовательности, без чего-либо промежуточного между ними, как вы можете видеть в примеры ниже. Не бросайте их в цикл for или что-то подобное - это было бы гарантией несинхронизации при следующей паузе / запуске вашего плеера...
Является ли сохранение этих вызовов вместе идеальным решением на 100%, или цикл выполнения при любой большой загрузке процессора может случайно помешать и принудительно отделить вызовы паузы друг от друга и вызвать пропадание кадров - я не знаю - по крайней мере, в течение нескольких недель возиться с API-интерфейсом AVAudioNode Я никоим образом не мог заставить мой мультиплеерный набор выйти из синхронизации - однако, я все еще не чувствую себя очень удобным или безопасным с этой несинхронизированной паузой со случайным магическим числом решение...
Пример кода и альтернатива:
Если ваш движок уже запущен, вы получили @property lastRenderTime в AVAudioNode - суперклассе вашего плеера - это ваш билет на 100% точную синхронизацию кадров сэмплов...
AVAudioFormat *outputFormat = [playerA outputFormatForBus:0];
const float kStartDelayTime = 0.0; // seconds - in case you wanna delay the start
AVAudioFramePosition now = playerA.lastRenderTime.sampleTime;
AVAudioTime *startTime = [AVAudioTime timeWithSampleTime:(now + (kStartDelayTime * outputFormat.sampleRate)) atRate:outputFormat.sampleRate];
[playerA playAtTime: startTime];
[playerB playAtTime: startTime];
[playerC playAtTime: startTime];
[playerD playAtTime: startTime];
[player...
Между прочим - вы можете добиться того же 100% точного результата сэмплированного кадра с классами AVAudioPlayer/AVAudioRecorder...
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 вы готовы. И никогда не забывайте заранее готовить проигрыватели к своим игрокам, чтобы в начальный момент не требовалось выполнять дополнительную буферизацию или настройку - просто запустите их, ребята;-)
Я использовал билет поддержки разработчиков Apple для своих собственных проблем с AVAudioEngine, в которых одна проблема была (есть) точно такой же, как ваша. Я получил этот код, чтобы попробовать:
AudioTimeStamp myAudioQueueStartTime = {0};
UInt32 theNumberOfSecondsInTheFuture = 5;
Float64 hostTimeFreq = CAHostTimeBase::GetFrequency();
UInt64 startHostTime = CAHostTimeBase::GetCurrentTime()+theNumberOfSecondsInTheFuture*hostTimeFreq;
myAudioQueueStartTime.mFlags = kAudioTimeStampHostTimeValid;
myAudioQueueStartTime.mHostTime = startHostTime;
AVAudioTime *time = [AVAudioTime timeWithAudioTimeStamp:&myAudioQueueStartTime sampleRate:_file.processingFormat.sampleRate];
Помимо планирования воспроизведения в эру Skynet вместо 5 секунд в будущем, он все еще не синхронизировал два узла AVAudioPlayerNode (когда я переключался GetCurrentTime()
для некоторого произвольного значения, чтобы на самом деле удалось воспроизвести узлы).
Поэтому неспособность синхронизировать два и более узла вместе является ошибкой (подтверждено поддержкой Apple). Как правило, если вам не нужно использовать что-то, что было представлено в AVAudioEngine (и вы не знаете, как перевести это в AUGraph), я советую вместо этого использовать AUGraph. Это немного сложнее в реализации, но у вас есть больше контроля над этим.