[译文]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;
‘BUFFER_SIZE’定义了一个区块,它用于容纳每次由流中读取的数据。你会发现(有点经验性质的)较大的缓冲区总能使音频质量提升,因为读取数据的次数会相应降低,这避免了播放的突然中断以及声音的失真。当然,缓冲区过大会消耗更多的内存,这样流的存在就失去意义了。我认为4096是可能的最小大小,因此并不建议使用更小的缓冲区,我试了一下,将会产生很多杂音。[关于缓冲区大小与读取次数的问题,在学习笔记中有更详细的讨论——译注]
那么我们为什么要这么纠结于流呢?为什么不将整个文件装入缓冲区播放呢?这是个好问题,简单的说,是因为音频数据太多了,即使真正的Ogg文件不会很大(大部分在1~3MB之间)但你必须知道它是个压缩音频数据,无法直接用于播放,在装入缓冲区前必须经过解压并格式化成OpenAL能够识别的格式,这便是我们使用流的原因。
/** * 初始化并播放流的主循环 */ 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; }
简言之,这里就是队列的工作方式:有一个缓冲区表,当你将缓冲区由队列移出时,它从表头离开。当你将缓冲区加入队列时,它被压入表的末尾。就是这样,很容易不是吗?
这是本类中两个最重要的方法之一,我们这里所作的,就是检测是否有缓冲区已经播放完毕。如果有,我们将其移出队列,并使用流中的数据重新装填它们,最后将其压入队列末尾来使它们再次进行播放。理想情况下听众不会察觉到我们所做的事,它听起来像是连续不断的音频链,stream方法告诉我们流是否播放完毕,对应标记将会在函数结束时返回。
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库负责完成流的解码工作,我们不必去担心这里。这个方法将比特数组作为其参数并依据其大小进行对应的解码。
oggDecoder.read方法的返回值表达了很多东西。如果对应于正数则表明实际读取的数据量,这很重要因为read方法也许不会读取整个缓冲区所大小的数据量(这在遇到文件末尾时经常发生),大部分情况下结果正好是缓冲区大小BUFFER_SIZE。如果read返回值为负数,则表明比特流中遇到了错误。如果返回值恰好为0则说明文件中没有更多需要播放的部分了。
这里最后一部分是对alBufferData的调用,它将我们使用read方法从Ogg文件中读取的数据填充到指定的缓冲区id所对应的缓冲区中,此处使用了我们初始化时得到的格式与比特率参数。
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..."); }
这里对错误检测进行了简化。
制作自己的OggVorbis格式播放器
如果你跟着我学到了这里,你一定对将所学用于自己实际的工作抱有期望。不要担心,我们就要完成了。我们所要做的工作就是使用最近设计的类来播放Ogg文件。从现在开始过程就变得相对简单了,我们已经把最难的部分完成了。我并不认为你会把这个用到游戏的循环当中,但我会在设计时回想起这些东西。
下面这些谁都会的[原文中使用了no-brainer直译为“无脑之人”,意思是说没脑子的人都会,此处衍伸为简单、任何人都会——译注]
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所作文章的奇怪关系。