为什么我的音频听不到按时播放?

时间:2011-07-26 19:22:41

标签: iphone audio nstimer openal

我的一个应用程序具有简单的节拍器风格功能,可以每分钟(bpm)播放指定次数的咔嗒声。我这样做是通过启动NSTimer,从指定的bpm计算的间隔,调用播放声音的方法。

如果我将一条NSLog线放入播放方法,我可以看到NSTimer准确地发射到大约1毫秒。但是,如果我将声音输出录制到音频编辑器然后测量点击之间的间隔,我可以看到它们的间距不均匀。例如,150 bpm时,计时器每400毫秒触发一次。但大多数声音在395毫秒后播放,每隔三,四声音播放418毫秒。

因此声音没有均匀延迟,而是遵循较短和较长间隔的模式。似乎iOS的声音定时分辨率较低,并且将每个声音事件四舍五入到最近的可用点,根据需要向上或向下舍入以保持整体轨道。

我用系统声音,AVAudioPlayer和OpenAL尝试了这个,并且用这三种方法得到了完全相同的结果。使用每种方法,我在视图加载时进行所有设置,因此每次播放声音时,我所要做的就是播放它。使用AVAudioPlayer,我尝试在每次声音播放后使用第二个计时器调用prepareToPlay,因此它已初始化并准备好下次播放,但结果相同。

以下是在viewDidLoad中设置OpenAL声音的代码(改编自this tutorial):

// 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:

我只是改造了我的项目以使用音频单元,现在音频播放得更加准确。它在任何一个方向上偶尔漂移最多4毫秒,但这比其他音频方法更好。我仍然很好奇为什么其他方法都显示短,短,短,长间隔的模式 - 就像音频播放时间被向上或向下舍入以映射到某种帧速率 - 所以我会将这个问题留给任何可以解释和/或为其他音频方法提供解决方法的人。

2 个答案:

答案 0 :(得分:1)

NSTimer不保证您的方法何时会被解雇。

此处有更多信息:How to program a real-time accurate audio sequencer on the iphone?

关于您的修改:

AVAudioPlayer需要一些时间来初始化自己。如果您调用prepareToPlay,它将初始化自身,以便它可以在调用播放时立即播放当前加载的声音。一旦播放停止,它就会取消初始化,因此您需要再次调用prepareToPlay来重新初始化。最好使用此类进行流式播放而不是离散声音播放。

使用OpenAL,一旦你加载了缓冲区,将它附加到源并播放它应该不会造成任何延迟。

您可以将音频单元代码封装到.mm文件中,然后从.m模块调用它,而不必将它们编译为C ++。

答案 1 :(得分:1)

好的,我已经明白了。音频单元比其他音频方法工作得更好的真正原因是我的音频单元类,我正在调整另一个项目,在音频会话中设置缓冲区持续时间属性,如下所示:

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

当我将此代码添加到OpenAL版本,甚至添加到AVAudioPlayer版本时,我的精确度在几毫秒内,与音频单元相同。 (系统声音仍然不太准确。)我可以通过增加缓冲区大小来验证连接,并观察播放间隔变得不准确。

当然,我只花了一整天的时间来调整我的项目以使用音频单元 - 将其调整为在C ++下编译,测试中断处理程序等等。我希望这可以为其他人避免同样的麻烦。