[译文]JOAL教程
原文地址:http://jogamp.org/joal-demos/www/devmaster/lesson8.html
原文作者:Athomas Goldberg
译文:三向板砖
转载请保留以上信息。
这是JOAL教程系列的最后一节,学习笔记:http://blog.csdn.net/shuzhe66/article/details/40583771
我十分建议您在阅读完本文后参考学习笔记内容,这节的问题非常多。
第八课OggVorbis格式流
本文是DevMaster.net(http://devmaster.net/)的OpenAL教程对应的JOAL版本。C语言版原文作者为JesseMaurais
“本软件基于由http://www.j-ogg.de所开发的J-Ogg库,版权归Tor-EinarJarnbjo所有”
OggVorbis格式简介
听说过Ogg吗?它并不仅仅是一个好玩的声音格式名字。它的出现可以算是自MP3格式(也是一种常用的音乐格式)出现以来音频压缩界中发生的最大事件了。也许,在某一天,它将取代MP3而成为压缩音频的主流标准。它真的比MP3要好吗?这个问题有些难以回答,它在某些社群中引起了巨大争论。关于压缩率与声音质量取舍的争论是如此之多以至于我们无法通篇浏览。我个人不对哪一个更好发表任何见解。我认为对这两种压缩格式的论据都存在争议,不值一提 。但对我来讲,现实就是这样:Ogg是版权免费的(而MP3不是),这一点倍受青睐。MP3版权费对于财大气粗的开发者们来说绝对不多,但对于使用着有限资源并使用闲暇时间独立完成项目的你来讲,花费一大笔开销可不是个好的选择,Ogg也许正是你所祈求的答案。
设计你自己的OggVorbis格式流API
现在不用这么麻烦了,我们先来看看代码吧:
本次教程使用Java编写,它有两个主要的类:OggDecoder与OggStreamer。OggDecoder类是对J-Ogg库的包装,它用来解码OggVorbis流,本次教程不会过多的叙述。OggStreamer是使用OpenAL处理大部分Ogg流的主要类,我把它写在下面了:
// 区块大小是我们每次希望由流中读取的数据数量。 private static int BUFFER_SIZE = 4096*8; // 音频管线中需要使用的缓冲区数量 private static int NUM_BUFFERS = 2;
/** * 初始化并播放流的主循环 */ public boolean playstream() { ... } /** * 打开Ogg流,并依据流的属性初始化OpenAL */ public boolean open() { ... } /** * 清理OpenAL的过程 */ public void release() { ... } /** * 播放Ogg流 */ public boolean playback() { ... } /** * 检测当前是否处于播放当中 */ public boolean playing() { ... } /** * 如果需要,将流的下一部分读入缓冲区 */ public boolean update() { ... } /** * 重新装载缓冲区 (读入下一个区块) */ public boolean stream(int buffer) { ... } /** * 清空队列 */ protected void empty() { ... }以上是我们Ogg格式流API的基础。这部分声明的公共(public)方法就是播放指定的Ogg所需的全部了,而保护(protected)方法更像是内部过程。我不会再对每一个方法进行说明了,我相信我的注释能让你明白它们是干什么的。
// 容纳声音数据的缓冲区.默认两个 (前缓冲区/后缓冲区) private int[] buffers = new int[NUM_BUFFERS]; // 发出声音的声源 private int[] source = new int[1];我想说明的第一个事情是我们为流声明了两个缓冲区而不是像之前播放wav那样声明一个,这很重要,为了理解这一点请先回想一下双缓冲是如何在OpenGL/DirectX下工作的。前缓冲区始终处于屏幕的展示下,而后缓冲区正在被绘制,之后交换两者,后缓冲区变为前缓冲区用以展示内容而后者反之。相同的原理也适用于此处,第一个缓冲区在播放中而另一个等待播放,当第一个播放完毕时后一个开始播放,此时,第一个缓冲区由流中重新装入数据并在播放中的缓冲区播放完毕后接替它。很难理解吗?之后我还会对这些进行进一步解释。
public boolean open() { oggDecoder = new OggDecoder(url); if (!oggDecoder.initialize()) { System.err.println("Error initializing ogg stream..."); return false; } if (oggDecoder.numChannels() == 1) format = AL.AL_FORMAT_MONO16; else format = AL.AL_FORMAT_STEREO16; rate = oggDecoder.sampleRate(); ... }这里为Ogg文件创建了解码器,之后初始化并由文件中获得一些信息。我们基于Ogg之中有多少声道来获得OpenAL中对应的枚举值并对其采样率进行记录。
public boolean open() { ... al.alGenBuffers(NUM_BUFFERS, buffers, 0); check(); al.alGenSources(1, source, 0); check(); al.alSourcefv(source[0], AL.AL_POSITION , sourcePos, 0); al.alSourcefv(source[0], AL.AL_VELOCITY , sourceVel, 0); al.alSourcefv(source[0], AL.AL_DIRECTION, sourceDir, 0); al.alSourcef(source[0], AL.AL_ROLLOFF_FACTOR, 0.0f ); al.alSourcei(source[0], AL.AL_SOURCE_RELATIVE, AL.AL_TRUE); ... }这里的大部分代码你之前都见过,我们设置了一堆默认值,如位置、速度、方向等,但是衰减系数(ROOLOFF_FACTOR)是什么呢?它跟衰减有关系。我会在之后的其他文章中详细讨论衰减的,现在还无需知道太多,但我还是大体上说一下吧。衰减系数决定了声音随距离衰减的大小,将其设置为0则关闭了相应功能,这意味着无论听众与Ogg声源距离有多远,听众都会听得到声音,对于声源也是如此。
public void release() { al.alSourceStop(source[0]); empty(); for (int i = 0; i < NUM_BUFFERS; i++) { al.alDeleteSources(i, source, 0); check(); } }我们使用这个方法进行清理,我们停止声源并将任何处于队列中的缓冲区清空,之后销毁我们的对象。
public boolean playback() { if (playing()) return true; for (int i = 0; i < NUM_BUFFERS; i++) { if (!stream(buffers[i])) return false; } al.alSourceQueueBuffers(source[0], NUM_BUFFERS, buffers, 0); al.alSourcePlay(source[0]); return true; }调用这个函数将开始播放Ogg,如果Ogg处于播放当中,那么就没必要再做一次了。我们也必须使用第一组数据来初始化缓冲区,之后将其加入队列并告诉声源播放它们。这是我们第一次使用alSourceQueueBuffers,大体上讲,它所做的就是给予声源多个缓冲区,这些缓冲区将会按顺序播放。我之后很快就会解释一下这里与声源队列的问题,记住这一点:如果使用声源需要播放流格式,不要用alSourcei来绑定缓冲区,而一定要用alSourceQueueBuffers。
public boolean playing() { int[] state = new int[1]; al.alGetSourcei(source[0], AL.AL_SOURCE_STATE, state, 0); return (state[0] == AL.AL_PLAYING); }这里简化了对声源状态的检测。
public boolean update() { int[] processed = new int[1]; boolean active = true; al.alGetSourcei(source[0], AL.AL_BUFFERS_PROCESSED, processed, 0); while (processed[0] > 0) { int[] buffer = new int[1]; al.alSourceUnqueueBuffers(source[0], 1, buffer, 0); check(); active = stream(buffer[0]); al.alSourceQueueBuffers(source[0], 1, buffer, 0); check(); processed[0]--; } return active; }简言之,这里就是队列的工作方式:有一个缓冲区表,当你将缓冲区由队列移出时,它从表头离开。当你将缓冲区加入队列时,它被压入表的末尾。就是这样,很容易不是吗?
public boolean stream(int buffer) { byte[] pcm = new byte[BUFFER_SIZE]; int size = 0; try { if ((size = oggDecoder.read(pcm)) <= 0) return false; } catch (Exception e) { e.printStackTrace(); return false; } ByteBuffer data = ByteBuffer.wrap(pcm, 0, size); al.alBufferData(buffer, format, data, size, rate); check(); return true; }这是该类的另一个重要方法。这里使用Ogg比特流填充缓冲区。这里很难控制因为它无法用自上而下的方法来阐述。oggDecoder.read与你想的一样,做着它该做的事;它由Ogg比特流中读取数据,j-ogg库负责完成流的解码工作,我们不必去担心这里。这个方法将比特数组作为其参数并依据其大小进行对应的解码。
protected void empty() { int[] queued = new int[1]; al.alGetSourcei(source[0], AL.AL_BUFFERS_QUEUED, queued, 0); while (queued[0] > 0) { int[] buffer = new int[1]; al.alSourceUnqueueBuffers(source[0], 1, buffer, 0); check(); queued[0]--; } oggDecoder = null; }这个方法将会移出任何在声源队列中等待的缓冲区。
protected void check() { if (al.alGetError() != AL.AL_NO_ERROR) throw new ALException("OpenAL error raised..."); }这里对错误检测进行了简化。
public boolean playstream() { if (!open()) return false; oggDecoder.dump(); if (!playback()) return false; while (update()) { if (playing()) continue; if (!playback()) return false; } return true; }上面的程序打开流,获得一些流信息并在update方法返回true时不断循环,而且update仅在其成功读取并播放音频流时才返回true。在循环中我们将会确认Ogg处于播放状态。
你可能会问到的问题
对于流格式,我是否可以创建多于一个的缓冲区呢?
简单的说,可以。在同一时刻,可以有多缓冲区处于声源的队列当中。这样做你也会获得更好的结果。就像我之前说过的一样,如果在队列中只有两个缓冲区而又恰巧遇到CPU不干活了(或是系统当机),声源也许就会在对下一流区块的解码前停止播放。在队列中使用三到四个缓冲区将会在错过update时带来更高的可靠性。
我该多久调用一次update呢?
这取决于很多事,如果想要一个快捷的答复我会告诉你:调用频率应该尽可能的高,但这有时并不是必要的。只是需要在声源播放完队列中的缓冲区前调用即可。对于这个频度取值的最大影响还是缓冲区大小以及队列中允许的缓冲区数量。显然,如果你有很多准备播放的数据,update的调用频率显然不需要那么多。
同时获得多个Ogg流是否安全?
当然没问题,我并没有进行过极端测试但是我也没看出为什么不行,比较一般你也不会有非常多的流。你可能有一个用于播放一些背景乐,或是偶尔出现在游戏中的人物对话,但大部分音效是如此之小以至于我们无需使用流格式。你大部分的声源也仅有一个与之绑定的缓冲区。
Ogg这个名字是什么意思呢?
“Ogg”是Xiph.org对于音频、视频、元数据的容器格式。“Vorbis”是被设计在Ogg中的具体音频压缩计划的名称。那么说这个名字的具体意思呢……这个,很难讲。我觉得它们牵扯到了一些与Terry Pratchett所作文章的奇怪关系。
原文地址:http://blog.csdn.net/shuzhe66/article/details/40583701