(注:这是我在编写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, &amp;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
 - (void)playSound:(NSString*)soundKey
 {
     NSNumber * numVal = [soundDictionary objectForKey:soundKey];
     if (numVal == nil) return;
     NSUInteger sourceID = [numVal unsignedIntValue];
     alSourcePlay(sourceID);
 }


但是由于以前我们是为每个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,比如最远的声音等)。


(译者注:实际上音效处理的算法是非常复杂,完全看你程序的具体情况。这里的代码只是简单的示例而已。还是那句话:播放声音很容易,但要正确地关闭声音,一定要仔细考虑!)


原文见:Lots and Lots of sounds in openAL