为什么我的音频声音没有按时播放?

发布于 2024-11-26 20:14:54 字数 3074 浏览 3 评论 0原文

我的一个应用程序有一个简单的节拍器风格的功能,可以每分钟 (bpm) 播放指定次数的点击声。我通过启动一个 NSTimer 来做到这一点,并根据指定的 bpm 计算出一个时间间隔,调用一个播放声音的方法。

如果我将 NSLog 行放入 play 方法中,我可以看到 NSTimer 精确触发到大约 1 毫秒。但是,如果我将声音输出记录到音频编辑器中,然后测量点击之间的间隔,我可以看到它们的间隔不均匀。例如,如果速度为 150 bpm,则计时器每 400 毫秒触发一次。但大多数声音在 395 毫秒后播放,每第三个或第四个声音在 418 毫秒后播放。

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

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

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

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

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

One of my apps has a simple metronome-style feature that plays a click sound a specified number of times per minute (bpm). I'm doing this by starting an NSTimer, with an interval calculated from the specified bpm, that calls a method that plays the sound.

If I put an NSLog line into the play method, I can see that NSTimer is firing accurately to about 1 millisecond. However, if I record the sound output into an audio editor and then measure the interval between clicks, I can see that they are not evenly spaced. For example, with 150 bpm, the timer fires every 400 milliseconds. But most of the sounds play after 395 milliseconds, with every third or fourth sound playing after 418 milliseconds.

So the sounds are not uniformly delayed, but rather, they follow a pattern of shorter and longer intervals. It seems as if the iOS has a lower resolution for timing of sounds, and is rounding each sound event to the nearest available point, rounding up or down as needed to keep on track overall.

I have tried this with system sounds, AVAudioPlayer and OpenAL and have gotten the exact same results with all three methods. With each method, I'm doing all the setup when the view loads, so each time I play the sound all I have to do is play it. With AVAudioPlayer, I tried calling prepareToPlay using a second timer after each time the sound plays, so it is initialized and ready to go next time, but got the same results.

Here's the code for setting up the OpenAL sound in viewDidLoad (adapted from 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);

And then in the play method I just call:

alSourcePlay(self.tempoSoundSource);

Can anyone explain what is happening here, and how I can work around it?

UPDATE 1:

I have another project that plays brief sounds with audio units, so as a quick test I added a timer to that project to play my click sound every 400 milliseconds. In that case, the timing is nearly perfect. So, it seems that NSTimer is fine but system sounds, AVAudioPlayer and OpenAL are less accurate in their playback than audio units.

UPDATE 2:

I just reworked my project to use audio units and now the audio is playing back much more accurately. It still occasionally drifts by up to four milliseconds in either direction, but this is better than the other audio methods. I'm still curious why the other methods all show a pattern of short, short, short, long intervals -- it's like the audio playback times are being rounded up or down to map to some kind of frame rate -- so I'll leave this question open for anyone who can explain that and/or offer a workaround for the other audio methods.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

秋意浓 2024-12-03 20:14:54

NSTimer 不保证你的方法何时真正被触发。

更多信息请参见:如何对iPhone 上的实时准确音频音序器?

关于您的编辑:

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

使用 OpenAL,加载缓冲区后,将其附加到源并播放应该不会造成任何延迟。

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

NSTimer does not guarantee when your method will actually get fired.

More info here: How to program a real-time accurate audio sequencer on the iphone?

Regarding your edits:

AVAudioPlayer takes some time to initialize itself. If you call prepareToPlay, it will initialize itself such that it can play the currently loaded sound immediately upon calling play. Once playback stops, it uninitializes itself, so you'd need to call prepareToPlay again to reinitialize. It's best to use this class for stream-y playback rather than discrete sound playback.

With OpenAL, once you've loaded the buffer, attaching it to a source and playing it should cause no delay at all.

You can encapsulate your audio units code into a .mm file and then call that from .m modules without having to compile those as C++.

久夏青 2024-12-03 20:14:54

好吧,我已经弄清楚了。音频单元比其他音频方法工作得更好的真正原因是我的音频单元类(我从另一个项目改编而来)正在音频会话中设置缓冲区持续时间属性,如下所示:

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

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

当然,我是在花了一整天的时间调整我的项目以使用音频单元之后才发现这一点的——调整它以在 C++ 下编译、测试中断处理程序等。我希望这可以让其他人免于同样的麻烦。

Okay, I've figured it out. The real reason audio units worked better than the other audio methods is that my audio unit class, which I was adapting from another project, was setting a buffer duration property in the audio session, like this:

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

When I added this code to the OpenAL version, or even to the AVAudioPlayer version, I got accuracy to within a few milliseconds, the same as with audio units. (System Sounds, however, were still not very accurate.) I can verify the connection by increasing the buffer size and watching the playback intervals get less accurate.

Of course I only figured this out after spending an entire day adapting my project to use audio units -- tweaking it to compile under C++, testing the interruption handlers, etc. I hope this can save someone else from the same trouble.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文