OpenAL(Open Audio Library)是专门负责3D定位音效方面的API,可用来开放地、跨平台地访问声音硬件。与那些今日在游戏中得到普遍应用的较大的面向对象的库相比,OpenAL是一个简单明了的替代方案。OpenAL一直在不断的创新,几乎没有一个API能达到她的全部潜能。一个很大的原因是因为hardware加速建立在特殊的版卡上。然而,Creative Labs是OpenAL的主要支持者,同时也是最大声卡厂商之一。OpenAL的另一个主要支持者是LOKI。
OpenAL不是商业产品,那样做限制了她的发展。OpenAL有许多的潜能,有许多的声音库工作在最底层的硬件上。但OpenAL的设计者经过无数的测试使她成为一个高级的API。她的风格是自由的,不同的编码风格和硬件部件将充分运用她的功能。有OpenGL编程精练的人将很快掌握OpenAL。
现在在PC竞技场中,游戏玩家实际上只有一种声音卡可以选择 – PC声卡制造商创新公司(Creative Labs)的Sound Blaster Live! 从旧的时间个人计算机声音卡片制造业者有创造力的中心. 多年来创新公司已经为DirectX提供了他们的EAX声音扩展,并且他们是发起新的OpenAL(开放音频库Open Audio Library)的创立者。就如同OpenGL是一个图形API一样,OpenAL,像它起来听一样,是一个声音系统的API。OpenAL 被设计为支持大多数通常声卡的许多特征,而且在一个特定的硬件特征不可得时提供一个软件替代。
OpenAL的主页为:http://www.openal.org。在这里可下载到OpenAL在各种操作系统上的实现及其文档。
OpenAL API
这部分将主要介绍OpenAL的接口,从基本的概念到可选的扩展都会做一些说明。
OpenAL从本质上讲是一个音频场景图库(audio scene graph library)。它描述对象之间的一系列关系。大部分的对象体现了离散的概念。其中重要概念有:
大部分的OpenAL条目都和这些类型的创建、销毁或者属性改变有关。
一般而言,对象之间有如下关系:
设备是最终输出PCM(Pulse Code Modulation,脉冲编码调制)数据的硬件。
一个listener属于且仅属于一个context,而每个context也刚好只能有一个listener。因此context就是在场景中聆听声音的对象实例。通常,每个场景中有一个listener,有对应的位置和其他应用程序用户属性。
缓冲器中存储的是原始PCM样本数据,不能直接播放。只有把缓冲器和音源关联起来,并播放该声音,声音才能被渲染出来。一个音源可以和多个缓冲器相关联,此时我们称其拥有一个缓冲器队列(Buffer Queue)。
音源和缓冲器一般通过名字(name)来引用,名字是整形标识符(不同的对象类型具有惟一对应的名字)。例如,没有两个音源名字会相同,介但它们可能与某些缓冲器的数字ID重复。
对象初始化以及名字绑定的语法是alGen{Object}。相应地,销毁对象时调用alDelete{Object}。例如,分别调用函数alGenSource()和alDeleteSource()来创建和销毁音源对象。创建context和device的函数一此不同,稍后会详细讨论。
音源是和context相关的。在一个context内有效的音源名字在其他的context中无效。缓冲器是和context无关的,创建缓冲器无需引用任何当前活动的context。
这些对象是的大部分都具有一些可以直接设定和查询的属性(Attribute)。属性有一个特定的类型,也有默认值。最常用的是音源属性,通过音源属性可以使缓冲器和一些音源相关联,还可以设置某一音源的位置等等。
listener与音源在设定和查询属性方面具有相似的语法,都是al{Object}{n}{if}{v}。大部分属性访问都是以数字(n)或者向量(v)形式来表示,而所传递或接受的参数的类型是这样指定的:i代表整数,f代表浮点数。例如,音源的位置通过函数alSource3f()或alSourcefv()带上AL_POSITION标记来设置。
最重要的缓冲属性,即组成声音的PCM样本集,是通过函数alBufferData()来设定的。
下面是一小段OpenAL程序例子。
//打开设备,创建设备
ALCdevice *dev = alcOpenDevice(NULL);
ALCcontextCurrent *cc = alcCreateContext(dev, NULL);
alcMakeContextCurrent(cc);
//创建音源和缓冲器
ALuint bid, sid;
alGenSources(1, &sid);
alGenBuffers(1, &bid);
//取得pcm数据,用缓冲区来关联它
ALvoid *data;
ALsizei size, bits, freq;
ALenum format;
ALboolean loop;
alutLoadWAVFile("boom.wav", &format, &data, &size, &freq, &loop);
alBufferData(bid, format, data, size, freq);
//用音源关联缓冲器
alSourcei(sid, AL_BUFFER, bid);
//播放音源然后等待直到完成
//然后释放它
alSourcePlay(sid);
ALint state;
do{
alGetSourcei(sid, AL_SOURCE_STATE, &state);
}while(state == AL_PLAYING);
alDeleteSources(1, &sid);
alDeleteBuffers(1, &bid);
alcMakeContextCurrent(NULL);
alcDestroyContext(cc);
alcCloseDevice(dev);
如上述程序所示, 函数alcOpenDevice()用于打开设备, 它带有一个可选的设备指示字符串参数。该字符串参数的语法和含义是与实现相关的。这意味着允许应用程序指定另外的后端或与设备相关的配置参数。在GNU/Linux的参考实现中,该设备字符串参数作为LISP语言风格的标记被解释,能够指定多个后端以及一些属性诸如采样率和后端相关功能。
函数alcCreateContext()用于创建渲染上下文环境,创建时需要指定一个设备用作context中混音的渲染目标。另外这个函数还可以琏可选的context属性表参数,形式是以零终止的整数对。需要由实现支持的context属性包括有ALC_SYNC、ALC_REFRESH及ALC_FREQUENCY。ALC_REFRESH和ALC_FREQUENCY会影响context渲染的性能与保真度, 而ALC_SYNC令context仅在调用函数alcProcessContext()进行更新后才会进行混音。
如前例所示的那样,OpenAL在语法、编码风格和习惯上都有模仿OpenGL。做出这个决定是为了在一定程度上迎合那些已经熟悉OpenGL的开发人员,也是为了仿效OpenAL ARB所代表的切合实际的设计原则。
可通过alSource{n}{if}{v}条目来设定音源的属性。音源属性可以公成三组:第一组影响音源在OpenAL世界中的物理位置,例如AL_POSITION和AL_VELOCITY;第二组表示“旋钮和转盘”如何影响音源的高级别的管理很有用处的状态属性,例如AL_LOOPING和AL_SOURCE_STATE。
使用AL_POSITION属性来设置音源位置是世界坐标系中的位置。只有带了附加属性AL_SOURCE_RELATIVE的时候才例外,这个属性告诉实现程序以渲染上下文环境的听从作为它原点来定位。这个属性在像类似可能从头盔发出头部相关的声音或者像音乐这种“2D”声音很有用处。对于那些无需定位的声音,常常使用internalFormat(多通道)扩展来实现,这会在后面进行介绍。
音源的AL_PITCH属性用于控制某一声音的相对音高。取值为1.0的时候,渲染的音源上无需调高。每减少50%会导致一个八度(-12半音)的音高变化。在GNU/Linux实现下,多普勒频率滤波器计算多普勒效应作为现有的音高属性的放缩因子。在应用程序中生动地使用可以达到非常好的效果。而通过软件实现音源的音高变化的代价是昂贵的,因此使用前合理地判断是必要的。
多普勒效应说明了OpenAL API的一些亮点。假如想使用多普勒效应,则必须设定listener和音源的一些属性。
ALfloat l_pos[] = {0,0,5};
ALfloat s_pos[] = {0,0,5}, s_vel[] = {0,0,1};
ALfloat zeros[] = {0,0,0};
alListenerfv(AL_POSITION, l_pos);
alListenerfv(AL_VELOCITY, zeros);
alSourcefv(sid, AL_POSITION, s_pos);
alSourcefv(sid, AL_VELOCITY, s_vel);
alSourcePlay(sid);
ALint state;
do{
s_vel[2] += 0.001;
s_pos[2] += 0.001;
alSourcefv(sid, AL_VELOCITY, s_vel);
alSourcefv(sid, AL_POSITION, s_pos);
alGetSourcei(sid, AL_SOURCE_STATE, &state);
}while(state != AL_PLAYING);
本例是需要注意的是,音源位置的计算不是推导出来的——而是由应用程序明确设置的。同时假定所有的位置和速度都是即时的。
OpenAL通过缓冲器排队机制支持声音的流式播放。缓冲器排队是多个缓冲器与单一音源相关联的一种机制。当音源播放时,连续对各个缓冲器进行渲染,就好象这些缓冲器组成了一个连续的声音。这可以通过一些特殊函数来控制。
流音源的工作一般是这样的。音源里的一批缓冲器通过alSourceQueueBuffers()函数进行排队,然后播放音源,接下来用属性AL_BUFFERS_PROCESSED来查询。该属性得出已经处理好的缓冲器的数量,从而允许应用程序使用alSourceUnqueueBuffers()函数删除那些已经处理好的缓冲器。alSourceUnqueueBuffers()函数将从队列头部开始依次将处理好的缓冲器删除。最后,其余的缓冲器在音源上排队。当缓冲器正在播放时,试图移去缓冲器会得到一个错误。
//使用排队机制来关联到缓冲器第一个集
alSourceQueueBuffers(sid, NUMBUFFERS, Buffers);
alSourcePlay(sid);
ALuint count = 0;
ALuint buffers_returned = 0;
ALint processed = 0;
ALboolean bFinished = AL_FALSE;
ALuint buffers_in_queue = NUMBUFFERS;
while(!bFinished)
{
//取得状态
alGetSourceiv(sid, AL_BUFFER_PROCESSED, &processed);
//假如播放完毕了一些缓冲器,然后让它们退出队列
//然后装载新的音频,再把它们装入队列
if(processed>0)
{
buffers_returned += processed;
while(processed)
{
ALuint bid;
alSourceUnqueueBuffers(sid, 1, &bid);
if(!bFinished)
{
DataToRead = (DataSize>BSIZE) ? BSIZE : DataSize;
if(DataToRead == DataSize)
bFinish = AL_TRUE;
//.......
//省略从音源读出DataToRead字节的代码
DataSize -= DataToRead;
if(bFinish == AL_TRUE)
memset(data + DataToRead, 0, BSIZE - DataToRead);
alBufferData(bid, format, data, DataToRead, wave.SamplesPerSec);
//对缓冲器排队
alSourceQueueBuffers(sid, 1, &bid);
processed--;
}
else
{
processed--;
if(buffers_in_queue-- == 0)
{
bFinished = AL_TRUE;
break;
}//if
}//else
}//while
}// if process
}
OpenAL的核心是将声音的衰减表现为某一距离函数。OpenAL有一系列的距离模型可以在运行的时候选择。
函数alDistaneceModel()用于在不同的距离模型中进行了选择。默认的距离模型是AL_INVERSE_DISTANCE,遵守下面的公式:
G_db=clamp(GAIN-20*log10(1+Rf*(dist-Rd)/Rd, MinG, MaxG))
此公式中Rf和Ed对应于音源的两个属性:AL_ROLLOFF_FACTOR和AL_RDFERENCE_DISTANCE。
MinG和MaxG分别对应于音源的最小增益属性AL_MIN_GAIN和最大增益属性AL_MAX_GAIN。参考距离dist是listen体验增益(GAIN)的距离。依音源而定的rolloff系数(高低频规律性衰减系数)能够在值变化量的负方向上改变音源的范围。当rolloff系数为0表明对于音源没有衰减。
OpenAL具有和OpenGL相似的可扩展性。应用程序首先调用函数alGetString(AL_EXTENSIONS)来询问实现。此函数返回一个可在其中搜索特定标识的扩展字符串。此外,函数alIsExtensionPresent()可以确定是否存在某个扩展。一旦确定某一扩展的存在,就用程序将能够通过函数alGetProcAddress()和alGetEnumValue()取得特定的函数和枚举标记。
OpenAL核心库中没有用于处理文件格式的函数。该功能由alut辅助库提供实现。函数alutLoadWavFile()和alutLoadWavMemory()可以加载不同版本的WAV文件格式。在提供便于应用程序载入音频文件函数的同时,alut还有简化初始化和结束程序的alutInit和alutExit例程。它们隐藏了context和设备的初始化细节,不过要稍稍损失一些灵活性作为代价。
#include
#include
#include
#include
#include
#include
#include
// 我们需要的最大的数据缓冲.
#define NUM_BUFFERS 3
// 我们需要放三种声音.
#define NUM_SOURCES 3
// 缓冲和源标志.
#define BATTLE 0
#define GUN1 1
#define GUN2 2
// 存储声音数据.
ALuint Buffers[NUM_BUFFERS];
// 用于播放声音.
ALuint Sources[NUM_SOURCES];
// 源声音的位置.
ALfloat SourcesPos[NUM_SOURCES][3];
// 源声音的速度.
ALfloat SourcesVel[NUM_SOURCES][3];
// 听者的位置.
ALfloat ListenerPos[] = { 0.0, 0.0, 0.0 };
// 听者的速度.
ALfloat ListenerVel[] = { 0.0, 0.0, 0.0 };
// 听者的方向 (first 3 elements are "at", second 3 are "up")
ALfloat ListenerOri[] = { 0.0, 0.0, -1.0, 0.0, 1.0, 0.0 };
ALboolean LoadALData()
{
// 载入变量.
ALenum format;
ALsizei size;
ALvoid* data;
ALsizei freq;
ALboolean loop;
// 载入WAV数据.
alGenBuffers(NUM_BUFFERS, Buffers);
if (alGetError() != AL_NO_ERROR)
return AL_FALSE;
alutLoadWAVFile("wavdata/Battle.wav", &format, &data, &size, &freq, &loop);
alBufferData(Buffers[BATTLE], format, data, size, freq);
alutUnloadWAV(format, data, size, freq);
alutLoadWAVFile("wavdata/Gun1.wav", &format, &data, &size, &freq, &loop);
alBufferData(Buffers[GUN1], format, data, size, freq);
alutUnloadWAV(format, data, size, freq);
alutLoadWAVFile("wavdata/Gun2.wav", &format, &data, &size, &freq, &loop);
alBufferData(Buffers[GUN2], format, data, size, freq);
alutUnloadWAV(format, data, size, freq);
// 捆绑源.
alGenSources(NUM_SOURCES, Sources);
if (alGetError() != AL_NO_ERROR)
return AL_FALSE;
alSourcei (Sources[BATTLE], AL_BUFFER, Buffers[BATTLE] );
alSourcef (Sources[BATTLE], AL_PITCH, 1.0 );
alSourcef (Sources[BATTLE], AL_GAIN, 1.0 );
alSourcefv(Sources[BATTLE], AL_POSITION, SourcePos[BATTLE]);
alSourcefv(Sources[BATTLE], AL_VELOCITY, SourceVel[BATTLE]);
alSourcei (Sources[BATTLE], AL_LOOPING, AL_TRUE );
alSourcei (Sources[GUN1], AL_BUFFER, Buffers[GUN1] );
alSourcef (Sources[GUN1], AL_PITCH, 1.0 );
alSourcef (Sources[GUN1], AL_GAIN, 1.0 );
alSourcefv(Sources[GUN1], AL_POSITION, SourcePos[GUN1]);
alSourcefv(Sources[GUN1], AL_VELOCITY, SourceVel[GUN1]);
alSourcei (Sources[GUN1], AL_LOOPING, AL_FALSE );
alSourcei (Sources[GUN2], AL_BUFFER, Buffers[GUN2] );
alSourcef (Sources[GUN2], AL_PITCH, 1.0 );
alSourcef (Sources[GUN2], AL_GAIN, 1.0 );
alSourcefv(Sources[GUN2], AL_POSITION, SourcePos[GUN2]);
alSourcefv(Sources[GUN2], AL_VELOCITY, SourceVel[GUN2]);
alSourcei (Sources[GUN2], AL_LOOPING, AL_FALSE );
// 做错误检测并返回
if( alGetError() != AL_NO_ERROR)
return AL_FALSE;
return AL_TRUE;
}
//首先,我们导入文件数据到3个缓冲区,然后把3个缓冲区和3个源锁在
//一起。唯一的不同是文件“battle.wav”在不停止时循环。
void SetListenervalues()
{
alListenerfv(AL_POSITION, ListenerPos);
alListenerfv(AL_VELOCITY, ListenerVel);
alListenerfv(AL_ORIENTATION, ListenerOri);
}
void KillALData()
{
alDeleteBuffers(NUM_BUFFERS, &Buffers[0]);
alDeleteSources(NUM_SOURCES, &Sources[0]);
alutExit();
}
int main(int argc, char *argv[])
{
// Initialize OpenAL and clear the error bit.
alutInit(NULL, 0);
alGetError();
// Load the wav data.
if (LoadALData() == AL_FALSE)
return 0;
SetListenervalues();
// Setup an exit procedure.
atexit(KillALData);
// Begin the battle sample to play.
alSourcePlay(Sources[BATTLE]);
// Go through all the sources and check that they are playing.
// Skip the first source because it is looping anyway (will always be playing).
ALint play;
while (!kbhit())
{
for (int i = 1; i < NUM_SOURCES; i++)
{
alGetSourcei(Sources[i], AL_SOURCE_STATE, &play);
if (play != AL_PLAYING)
{
// Pick a random position around the listener to play the source.
double theta = (double) (rand() % 360) * 3.14 / 180.0;
SourcePos[i][0] = -float(cos(theta));
SourcePos[i][1] = -float(rand()%2);
SourcePos[i][2] = -float(sin(theta));
alSourcefv(Sources[i], AL_POSITION, SourcePos[i] );
alSourcePlay(Sourcev[i]);
}//if
}//for
}//while
return 0;
}