Почему мои звуки не воспроизводятся вовремя? - PullRequest
1 голос
/ 26 июля 2011

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

Если я добавлю строку NSLog в метод воспроизведения, я вижу, что NSTimer срабатывает с точностью до 1 миллисекунды. Однако, если я записываю вывод звука в аудиоредактор, а затем измеряю интервал между щелчками, я вижу, что они не расположены равномерно. Например, при скорости 150 ударов в минуту таймер срабатывает каждые 400 миллисекунд. Но большинство звуков воспроизводится через 395 миллисекунд, а каждый третий или четвертый звук воспроизводится через 418 миллисекунд.

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

Я пробовал это с системными звуками, AVAudioPlayer и OpenAL и получил те же самые результаты со всеми тремя методами. При каждом методе я выполняю все настройки при загрузке представления, поэтому каждый раз, когда я воспроизводю звук, все, что мне нужно сделать, это воспроизвести его. С AVAudioPlayer я пытался вызывать prepareToPlay, используя второй таймер после каждого воспроизведения звука, поэтому он инициализируется и готов к работе в следующий раз, но получил те же результаты.

Вот код для настройки звука OpenAL в viewDidLoad (адаптировано из этого урока ):

// set up the context and device
ALCcontext *context;
ALCdevice *device;
OSStatus result;
device = alcOpenDevice(NULL); // select the "preferred device"
if (device) {
    context = alcCreateContext(device, NULL); // use the device to make a context
    alcMakeContextCurrent(context); // set the context to the currently active one
}

// open the sound file
NSString *soundFilePath = [[NSBundle mainBundle] pathForResource:@"TempoClick" ofType:@"caf"];
NSURL *soundFileURL = [NSURL fileURLWithPath:soundFilePath];
AudioFileID fileID;
result = AudioFileOpenURL((CFURLRef)soundFileURL, kAudioFileReadPermission, 0, &fileID);
if (result != 0) DLog(@"cannot open file %@: %ld", soundFilePath, result);

// get the size of the file data
UInt32 fileSize = 0;
UInt32 propSize = sizeof(UInt64);
result = AudioFileGetProperty(fileID, kAudioFilePropertyAudioDataByteCount, &propSize, &fileSize);
if (result != 0) DLog(@"cannot find file size: %ld", result);
DLog(@"file size: %li", fileSize);

// copy the data into a buffer, then close the file
unsigned char *outData = malloc(fileSize);
AudioFileOpenURL((CFURLRef)soundFileURL, kAudioFileReadPermission, 0, &fileID); // we get a "file is not open" error on the next line if we don't open this again
result = AudioFileReadBytes(fileID, false, 0, &fileSize, outData);
if (result != 0) NSLog(@"cannot load data: %ld", result);
AudioFileClose(fileID);
alGenBuffers(1, &tempoSoundBuffer);
alBufferData(self.tempoSoundBuffer, AL_FORMAT_MONO16, outData, fileSize, 44100);
free(outData);
outData = NULL;

// connect the buffer to the source and set some preferences
alGenSources(1, &tempoSoundSource); 
alSourcei(tempoSoundSource, AL_BUFFER, tempoSoundBuffer);
alSourcef(tempoSoundSource, AL_PITCH, 1.0f);
alSourcef(tempoSoundSource, AL_GAIN, 1.0f);
alSourcei(tempoSoundSource, AL_LOOPING, AL_FALSE);

А потом в методе воспроизведения я просто вызываю:

alSourcePlay(self.tempoSoundSource);

Может кто-нибудь объяснить, что здесь происходит, и как я могу обойти это?

ОБНОВЛЕНИЕ 1:

У меня есть другой проект, который воспроизводит короткие звуки с аудиоустройствами, поэтому в качестве быстрого теста я добавил в этот проект таймер для воспроизведения звука щелчка каждые 400 миллисекунд. В этом случае время почти идеально. Итак, кажется, что NSTimer в порядке, но системные звуки, AVAudioPlayer и OpenAL менее точны в своем воспроизведении, чем аудиоустройства.

ОБНОВЛЕНИЕ 2:

Я только что переработал свой проект, чтобы использовать аудиоустройства, и теперь звук воспроизводится гораздо точнее. Это все еще иногда дрейфует до четырех миллисекунд в любом направлении, но это лучше, чем другие аудио методы. Мне все еще любопытно, почему все другие методы показывают паттерн коротких, коротких, коротких, длинных интервалов - это как время воспроизведения звука округляется в большую или меньшую сторону, чтобы отобразить какую-то частоту кадров - поэтому я оставьте этот вопрос открытым для тех, кто может объяснить это и / или предложить обходной путь для других аудио методов.

Ответы [ 2 ]

1 голос
/ 05 августа 2011

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

Float32 preferredBufferSize = .001;
UInt32 size = sizeof(preferredBufferSize);
AudioSessionSetProperty(kAudioSessionProperty_PreferredHardwareIOBufferDuration, size, &preferredBufferSize);

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

Конечно, я понял это только после того, как потратил целый день на адаптацию моего проекта к использованию аудиоустройств - настройку его для компиляции под C ++, тестирование обработчиков прерываний и т. Д. Я надеюсь, что это может спасти кого-то еще от той же проблемы.

1 голос
/ 27 июля 2011

NSTimer не гарантирует, когда ваш метод будет запущен.

Больше информации здесь: Как запрограммировать точный аудио секвенсор в реальном времени на iphone?

Относительно ваших правок:

AVAudioPlayer требуется некоторое время для инициализации. Если вы вызываете prepareToPlay, он инициализирует себя так, что он может воспроизводить загруженный в данный момент звук сразу после вызова play. Как только воспроизведение останавливается, оно самоинициализируется, поэтому вам потребуется снова вызвать prepareToPlay для повторной инициализации. Лучше всего использовать этот класс для потокового воспроизведения, а не для воспроизведения дискретного звука.

В OpenAL, после того, как вы загрузили буфер, присоедините его к источнику и воспроизведите его, это вообще не вызовет задержки.

Вы можете инкапсулировать код ваших аудиоустройств в файл .mm и затем вызывать его из модулей .m без необходимости компилировать их как C ++.

...