|
openal教程(8)
OggVorbis流用声源队列
大家好,由于我现在的脑袋很晕,翻译的文章难免有误,请大家批评指正,谢谢。antking@gmail.cn!!!
Hello again fellow coders. I'm back after a fairly long hiatus with another tutorial on the OpenAL api. And I think this will be a beefy one. I would first like to thank the community for their support thus far in the series. I want to put out some special thanks to DevMaster.net who is hosting the series on their website. This really got the ball rolling on the series which has now been ported to Visual C++ 6 by TheCell, and to Java by Athomas Goldberg for JOAL (Java Bindings for OpenAL). I have also heard they have been translated to Portuguese for the Brazilian GameDev. I would also like to give special thanks to Jorge Bernal for sending me some sample code which gave me enough of a kick in the pants to get me writing again. That was a big help (even though translating the code from Spanish was a chore .
OggVorbis简介
大家是否听说过Ogg?没有比他更滑稽的声音名字了。他是音频压缩有关,诸如
MP3。有希望的是,他将替代MP3成为主流的音频压缩格式。他和MP3谁好?
那是一个很难回答的问题。还有很强的争论。有不同的关于压缩比例对声音质量
的争论,我个人还没有关于谁是最好的观点。但是事实上Ogg是免费的,而MP3
不是,因此ogg得到了很多的支持。
设计你OggVorbis流 API
请看下面的代码:
#include <string>
#include <iostream>
using namespace std;
#include <al/al.h>
#include <ogg/ogg.h>
#include <vorbis/codec.h>
#include <vorbis/vorbisenc.h>
#include <vorbis/vorbisfile.h>
#define BUFFER_SIZE (4096 * 8)
这篇教程全用C++代码书写,因此我们将一开始就包含C++标准头文件。当然还有OPENAL API,
并且我们也将包含4个新的头。这些新的头是我们设计的OggVorbis流函数库
的一部分。总数上有4个文件:‘ogg.dll'(格式和解码器),’vorbis.dll'(编码规划),‘
vorbisenc.dll'’(编码的工具),和‘vorbisfile.dll'(流和搜寻的工具)。
我们不会用’vorbisenc‘,但我们把他包含到我们用的文件中。用这些函数库将
实现99%的工作量。 在这里我们将不会用他。我怀疑我们能写出比多媒体数字信号编解码器的设计者
更好的东西。这些函数库将使我们不用额外的工作而自动更新格式进化。但是,
用这些函数库的最大原因是他的一致性。如果所有的OGG文件重新用这些库编码,那么
所有的OGG文件将能够在所有的OGG播放器中播放。
我们建立了宏'BUFFER_SIZE' 来确定我们从流中读到的块有多大。你将发现
大的缓冲区将产生高质量的声音,而不会产生突然的暂停和声音变形。当然
如果缓冲区太大也回占用你大量的内存。如果要使用很长的流,我相信4096的缓冲区就够用了。
我并不提倡用小的缓冲区,因为他会有卡嗒声。
为什么我们要截断流?为什么不直接导入整个文件,然后播放他?这是个好问题。
而答案是有太多的音频数据。实际的OGG文件十分小(通常是1-3MB),你必须记
住他们是压缩的音频数据。你不能直接播放这些压缩的音频数据。在用于缓冲区以前,
你必须解压并转换成OPENAL能识别的格式。
class ogg_stream
{
public:
void open(string path); // obtain a handle to the file void release(); // release the file handle void display(); // display some info on the Ogg bool playback(); // play the Ogg stream bool playing(); // check if the source is playing bool update(); // update the stream if necessary protected: bool stream(ALuint buffer); // reloads a buffer void empty(); // empties the queue void check(); // checks OpenAL error state string errorString(int code); // stringify an error code
这是我们的OGG流API类。公共方法是先得到我们需要的OGG,然后播放他。
保护方法为一些更为内部的处理(比如错误检测)
private: FILE* oggFile; // file handle OggVorbis_File oggStream; // stream handle vorbis_info* vorbisInfo; // some formatting data vorbis_comment* vorbisComment; // user comments ALuint buffers[2]; // front and back buffers ALuint source; // audio source ALenum format; // internal format};
首先,我必须指出我们用了2个缓冲区而不是1个,我们总是在WAV文件中这样
运用。然后来明白双缓冲区在OPENAL/DIRECTX中是怎样工作的。有一个前面
的缓冲区对应于当前屏幕,而后面的内存在后台工作。然后,他们就交换。
后台缓冲区变成前台缓冲区。一个缓冲区正在播放而另一个缓冲区在等待播放。
当一个完成后,另一个开始。
当你看见'FILE*' ,你也许会想,为什么我们在C++代码中用C文件处理。因为
在设计vorbisfile时,使用的C文件系统,而不是C++。
void ogg_stream: pen(string path){ int result; if(!(oggFile = fopen(path.c_str(), "rb"))) throw string("Could not open Ogg file."); if((result = ov_open(oggFile, &oggStream, NULL, 0)) < 0) { fclose(oggFile); throw string("Could not open Ogg stream. ") + errorString(result); }
如果我们用fstream ,我们不得不建立几个新的函数,并用'ov_open_callbacks'
注册他们。如果你需要支持实际的文件系统,他将是非常有用的。函数的'ov_open'
是用于捆绑OGG流的文件句柄。现在流拥有了这个文件的句柄。
vorbisInfo = ov_info(&oggStream, -1); vorbisComment = ov_comment(&oggStream, -1); if(vorbisInfo->channels == 1) format = AL_FORMAT_MONO16; else format = AL_FORMAT_STEREO16;
这段代码用于获取文件中的一些信息。我们检测OPENAL格式计数器中有多少
通道在OGG中。
alGenBuffers(2, buffers); check(); alGenSources(1, &source); check(); alSource3f(source, AL_POSITION, 0.0, 0.0, 0.0); alSource3f(source, AL_VELOCITY, 0.0, 0.0, 0.0); alSource3f(source, AL_DIRECTION, 0.0, 0.0, 0.0); alSourcef (source, AL_ROLLOFF_FACTOR, 0.0 ); alSourcei (source, AL_SOURCE_RELATIVE, AL_TRUE );}
我们设置了一系列默认的值,位置,速度,方向......但是rolloff 因子是什么?rolloff 因子判断
变弱的力量覆盖距离。如设置为0,将关掉他。这意味着不管听众与声源多远,
都将听到声音。
void ogg_stream::release(){ alSourceStop(source); empty(); alDeleteSources(1, &source); check(); alDeleteBuffers(1, buffers); check(); ov_clear(&oggStream);}
用完后,我们将释放他们。我们停止声源,清空队列中的内存,删除我们的对象。
'ov_clear' 意味着他将释放他占有的文件流和关闭处理。
void ogg_stream::display(){ cout << "version " << vorbisInfo->version << "\n" << "channels " << vorbisInfo->channels << "\n" << "rate (hz) " << vorbisInfo->rate << "\n" << "bitrate upper " << vorbisInfo->bitrate_upper << "\n" << "bitrate nominal " << vorbisInfo->bitrate_nominal << "\n" << "bitrate lower " << vorbisInfo->bitrate_lower << "\n" << "bitrate window " << vorbisInfo->bitrate_window << "\n" << "\n" << "vendor " << vorbisComment->vendor << "\n"; for(int i = 0; i < vorbisComment->comments; i++) cout << " " << vorbisComment->user_comments << "\n"; cout << endl;}
显示一些附加信息。
bool ogg_stream::playback(){ if(playing()) return true; if(!stream(buffers[0])) return false; if(!stream(buffers[1])) return false; alSourceQueueBuffers(source, 2, buffers); alSourcePlay(source); return true;}
用于播放OGG。如果OGG已经播放,如没有其他原因,他将循环。我们必须用他们设置的数据初始化
缓冲区。然后把他们排队,播放。在这里,我们用到了'alSourceQueueBuffers'。
他的作用是个声源多个缓冲区。这些缓冲区将顺序的播放。注:如果你在声源时,没有
用'alSourcei'捆绑到缓冲区,那么总是坚持用'alSourceQueueBuffers'。
bool ogg_stream::playing(){ ALenum state; alGetSourcei(source, AL_SOURCE_STATE, &state); return (state == AL_PLAYING);}
用于检测声源的状态。
bool ogg_stream::update(){ int processed; bool active = true; alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed); while(processed--) { ALuint buffer; alSourceUnqueueBuffers(source, 1, &buffer); check(); active = stream(buffer); alSourceQueueBuffers(source, 1, &buffer); check(); } return active;}
队列是怎样工作的呢?有一列队列,当你推出一个缓冲区时,他将弹出前面的一个,当你进队时,他将弹出
后面的一个。
在这个类中,有两个重要的方法。我们用BIT的形式检测缓冲区是否播放完。如果
有,我们将弹出队列中的最后一个,然后从流中重填数据到缓冲区中。我们做这些
,听众将没有任何注意。声音象一条很长的音乐链子。'stream'函数也将告诉我们
流是否完成播放。
bool ogg_stream::stream(ALuint buffer){ char data[BUFFER_SIZE]; int size = 0; int section; int result; while(size < BUFFER_SIZE) { result = ov_read(&oggStream, data + size, BUFFER_SIZE - size, 0, 2, 1, & section); if(result > 0) size += result; else if(result < 0) throw oggString(result); else break; } if(size == 0) return false; alBufferData(buffer, format, data, size, vorbisInfo->rate); check(); return false;}
这是另一个重要的方法。他用于把OGG BIT流填充到缓冲区。你也许要想'ov_read'
是做什么的?他是从OGG BIT流中读去数据的。vorbisfile做所有BIT流的译码
工作,以使我们不用担心他。这个函数拿走了我们定义的'oggStream'结构,
用于写译码音频的数据缓冲区和你想译码的块的大小。
返回值'ov_read' 代表几件事。如果结果值是确定的,那么他代表读了多少
数据。这是重要的,因为'ov_read' 不能读到被要求的那么多数据。(通常,
是因为已到了文件末尾并且没数据可读)在任何情况用'ov_read' 覆盖'BUFFER_SIZE'。
如果'ov_read' 被否定,那么表示有错误在BIT流中。在这种情况下,结果值是
错误代码。如果结果值等于0,那么文件中无数据播放。
在整个循环中使代码复杂化。这种方法被设计为模块化并且更容易修改。你
可以通过改变'BUFFER_SIZE' 来使他一直播放。但这要求我们必须确定我们
填充到缓冲区中的数据必须带'ov_read' 复合调用并且确定什么都对准了。
这种方法的最后部分是调用'alBufferData',但必须确定我们从OGG文件中用
'ov_read'读到的数据的ID在缓冲区中。我们用'format'和'vorbisInfo' 数据。
void ogg_stream::empty(){ int queued; alGetSourcei(source, AL_BUFFERS_QUEUED, &queued); while(queued--) { ALuint buffer; alSourceUnqueueBuffers(source, 1, &buffer); check(); }}
这种方法将与声源无关的缓冲区退队。
void ogg_stream::check(){ int error = alGetError(); if(error != AL_NO_ERROR) throw string("OpenAL error was raised.");}
这种方法用于保存我们错误检测的类型。
string ogg_stream::errorString(int code){ switch(code) { case OV_EREAD: return string("Read from media."); case OV_ENOTVORBIS: return string("Not Vorbis data."); case OV_EVERSION: return string("Vorbis version mismatch."); case OV_EBADHEADER: return string("Invalid Vorbis header."); case OV_EFAULT: return string("Internal logic fault (bug or heap/stack corruption."); default: return string("Unknown Ogg error."); }}
'stringify'是一条错误信息,用于你控制信息框的检测。
制作OGGVORBIS 播放器
现在,我们用我们自己设计的类来播放OGG文件。他是非常简单的。
int main(int argc, char* argv[]){ ogg_stream ogg; alutInit(&argc, argv);
这里简直不用思考。
try { if(argc < 2) throw string("oggplayer *.ogg"); ogg.open(argv[1]); ogg.display();
在我们用C++时,同样要用try/catch/throw 进行异常处理。你可能已注意到
我在程序中对STRINGS进行了异常处理。
第一件事是确定文件路径是否正确。如果不正确,我们不能做任何事情,因此,我们
必须向用户展示OGG扩展的一些信息;如果正确,我们就能打开一个文件。我们也
将展示关于OGG文件的信息。
if(!ogg.playback()) throw string("Ogg refused to play."); while(ogg.update()) { if(!ogg.playing()) { if(!ogg.playback()) throw string("Ogg abruptly stopped."); else cout << "Ogg stream was interrupted.\n"; } } cout << " rogram normal termination."; cin.get(); }
我们开始播放OGG文件。如果没有数据初始两个缓冲区或者不能读文件,就不能
播放OGG文件。
程序将连续不断的循环,直到'update' 方法被调用并返回真,还要能读和播放声音流。
在循环里,我们将确定OGG在播放。这可以看他是否服务相同的'update',但他也
要和系统解决其他的问题。
如果无事可做,程序将正常退出。
catch(string error) { cout << error; cin.get(); }
在程序不得不停止是显示的信息时,能抓住错误语句。
ogg.release(); alutExit(); return 0;}
主程序的结尾。
回答你可能的问题
我能为流开辟多于一个的缓冲区吗?
是的。你能同时开辟许多缓冲区,这样做将得到更好的结果。就像我前面说的
两个缓冲区不会占用太多的CPU时间,一个缓冲区完成播放,另一个完成更新
数据。当然,3,4个缓冲区将有更好的表现。
怎样经常调用OGG。UPDATA?
有下面几点。如果你想得到一个迅速的答案随便你,但那不是真正的必要。
应该在声源完成播放队列中最后一个后更新。那最大的因子将影响缓冲区大小和
代表队列的缓冲区的个数。显然,如果你有太多的数据要播放,应减少更新。
同时流超过一个OGG安全吗?
应该安全。我虽然没做过,但我想他为什么不能。通常你不会有许多流。你
可以用一条播放背景音乐,和游戏的对白,但是太多的声音将干扰流。太多的
声源将只有一个缓冲区给他们。
关于名字
‘OGG’是Xiph.org的包容器,包含了音频,视频和元数据。‘VORBIS’为OGG
设计的一个音频压缩计划。
(全文完) |
|