(注:这是我在编写MozartBrain时,参考过的一篇文章,虽然年代久远,但对于学习OpenAL编程还是具有指导意义。)
今天要讨论的话题是OpenAL同时处理大量的音效。当你使用OpenAL进行声音播放时,能够同时播放的声音有一个上限(换而言之,最大的source数目)。在iPhone上能够通过下面代码获得的最大source数目大约为32:
1 | alGenSources(1, &sourceID); |
当你要求超过最大数目的source时,上面的语句将失败,但它并不产生错误。因此,不要要求超过32个source(注:这是iPhone OS 2.2上的数目,其他版本的OS可能会有些差别)
所以,这意味着什么?它意味着任何时候不管什么原因都不要同时播放超过32个音效。这确实是一个问题,如果你的目标是通过分别播放各种乐器来模拟整个交响乐团的演奏,那么你最好不要在iPhone上尝试这样做。
我以前的文章中提到建议为每个音效(音效缓存)分配一个source并在需要播放时都调用此source。这在大部分情形下都可以正常工作。但是,如果在你的应用程序中有超过32个音效时,你应该怎么做?我记得曾经提到过:你应该将未用的source移交给将要被播放的buffer。
这实际上非常简单,我将展示一下怎样实现!
首先,你需要预先创建并加载source。这并不是必须的,但我希望在我想要播放声音时,能够立刻进行,所以我必须预先准备好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // 注意: MAX_SOURCES 是你需要预先加载的source数目 // 应该小于32 -(void)preloadSources { // lazy init of my data structure if (sources == nil) sources = [[NSMutableArray alloc] init]; // we want to allocate all the sources we will need up front NSUInteger sourceCount = MAX_SOURCES; NSInteger sourceIndex; NSUInteger sourceID; // build a bunch of sources and load them into our array. for (sourceIndex = 0; sourceIndex < sourceCount; sourceIndex++) { alGenSources(1, &sourceID); [sources addObject:[NSNumber numberWithUnsignedInt:sourceID]]; } } |
这实际上很简单,只是让openAL创建一系列 source 并保存它们的 ID。 你可以通过向alGenSources传递所需source数目,让 openAL一次创建所有source,然后将它们存储于 C 数组中,但这里我不想调用诸如malloc/free之类的函数,所有使用的是objc-c的方法。
老的声音播放方法如下:
// 获取声音ID,然后启动声音播放
1 2 3 4 5 6 7 |
但是由于以前我们是为每个buffer都分配一个source。现在我们不再会有预先分配好的source,所以必须在声音播放时进行分配。现在的播放方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | - (NSUInteger)playSound:(NSString*)soundKey gain:(ALfloat)gain pitch:(ALfloat)pitch loops:(BOOL)loops { ALenum err = alGetError(); // clear error code // first, find the buffer we want to play NSNumber * numVal = [soundLibrary objectForKey:soundKey]; if (numVal == nil) return 0; NSUInteger bufferID = [numVal unsignedIntValue]; // now find an available source NSUInteger sourceID = [self _nextAvailableSource]; // make sure it is clean by resetting the source buffer to 0 alSourcei(sourceID, AL_BUFFER, 0); // attach the buffer to the source alSourcei(sourceID, AL_BUFFER, bufferID); // set the pitch and gain of the source alSourcef(sourceID, AL_PITCH, pitch); alSourcef(sourceID, AL_GAIN, gain); // set the looping value if (loops) { alSourcei(sourceID, AL_LOOPING, AL_TRUE); } else { alSourcei(sourceID, AL_LOOPING, AL_FALSE); } // check to see if there are any errors err = alGetError(); if (err != 0) { [self _error:err note:@"Error Playing Sound!"]; return 0; } // now play! alSourcePlay(sourceID); return sourceID; // return the sourceID so I can stop loops easily } |
请注意,这个新方法不再直接从soundLibrary中获取sourceID,所以我们必须保存buffer而不是source。等等,下面一行代码在做什么:
1 2 | // now find an available source NSUInteger sourceID = [self _nextAvailableSource]; |
这实际上是调用我自己的方法,查找未被使用的source,然后返回,请看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | -(NSUInteger)_nextAvailableSource { NSInteger sourceState; // a holder for the state of the current source // first check: find a source that is not being used at the moment. for (NSNumber * sourceNumber in sources) { alGetSourcei([sourceNumber unsignedIntValue], AL_SOURCE_STATE, &sourceState); // great! we found one! return it and shunt if (sourceState != AL_PLAYING) return [sourceNumber unsignedIntValue]; } // in the case that all our sources are being used, we will find the first non-looping source // and return that. // first kick out an error NSLog(@"available source overrun, increase MAX_SOURCES"); NSInteger looping; for (NSNumber * sourceNumber in sources) { alGetSourcei([sourceNumber unsignedIntValue], AL_LOOPING, &looping); if (!looping) { // we found one that is not looping, cut it short and return it NSUInteger sourceID = [sourceNumber unsignedIntValue]; alSourceStop(sourceID); return sourceID; } } // what if they are all loops? arbitrarily grab the first one and cut it short // kick out another error NSLog(@"available source overrun, all used sources looping"); NSUInteger sourceID = [[sources objectAtIndex:0] unsignedIntegerValue]; alSourceStop(sourceID); return sourceID; } |
在 99% 的情况下,它将在第一个循环时找到合适的source并返回。根据我的经验即使在大量使用音效的程序中,你也很少会遇到几个声音同时播放的情形(除非你的音频采样非常长),所以它将很快返回。
(译者注:实际上,并非完全正确,在我编写的一个未发表的吉他乐器游戏中,我的声音引擎经常遇到大量音效同时播放的情况。之所以会有这种情况发生,是由于你必须在适当的时候关闭声音。是的,播放声音非常容易,但要关闭,就有相当的学问。如果你在声音播放的中途直接关闭,那么我可以保证,你将听到爆音。我在App Store下载了许多乐器软件,有几个会产生明显的爆音,就是由于这个原因。要解决这个问题,可能有许多方法。经过我的实验,我的方法是在停止一个声音前,采用定时器逐渐减小音量或将souce移动到很远的地方,确保听不见,但不要停止声音。哪怕声音的音量只有0.00001,如果你关闭它,你仍然会听到爆音。你必须让声音采样自然结束,所以你采用的音效一定不能过长。这就是为什么有许多source会同时存在的原因。为减小source的同时使用的数量,造成source不够用的情况,还必须采用一些其他的技术,比如在将音量减小到接近0(注意不能为0,如果是0,就等同于关闭音量,会产生破音)时,将buffer中的指针指向buffer的结尾处,或采用streaming的方法,另外还可以采用source重用的技术。当然,这些不在本文讨论的范围之内了)
但是,如果所有source还在播放怎么办?有几个选择:容易的方法:增加max_sources直到问题解决;稍难一点的方法(仍然很容易):实时生成新的source。
但是如果你用完了32个source,仍然没有适当的source可用时,最后几行代码将进行处理(译者注,如我上面注释的,这种解决方法并不完美,它很有可能会产生破音。但这只是一个简单的示例,你应根据具体情况进行处理),它假设新声音比旧声音更重要而且循环音比音效更重要:我们采用两个步骤找到需要停止的声音。
首先我们找到一个非循环音,停止它如何返回其source。第二种情况如果有MAX个循环音在同时播放,那么找到第一个source,停止并返回其source (译者注:其实这也是与你程序的具体情况相关的,你可能会需要采用特殊的算法来决定需要停止的source,比如最远的声音等)。
(译者注:实际上音效处理的算法是非常复杂,完全看你程序的具体情况。这里的代码只是简单的示例而已。还是那句话:播放声音很容易,但要正确地关闭声音,一定要仔细考虑!)



