[译文]JOAL教程
原文地址:http://jogamp.org/joal-demos/www/devmaster/lesson6.html
原文作者:Athomas Goldberg
译文:三向板砖
转载请保留以上信息。
本次课程对应的学习笔记:http://blog.csdn.net/shuzhe66/article/details/40303739
第六课 高级加载方式与错误处理
本文是DevMaster.net(http://devmaster.net/)的OpenAL教程对应的JOAL版本。C语言版原文作者为JesseMaurais
直到现在,我们一直在做很简单的事情,它们不需要以精确的方式来完成。这样编写代码的原因是为了让大家学起来更简单而不必考虑程序的健壮性。然而,我们很快会接触一些高级的东西,所以需要点时间来学习更为完善的程序编写方法。我们即将学习一个重要的部分:以更先进的方式处理错误。之后,还会加以提及加载音频数据的更多方式。特别说明一下,之前的方法并没有错误,我们只是需要一个更为有组织性和灵活性的编写流程。
我们先来编写一些函数,它们将在后续课程中对我们提供极大的帮助。
/** * 1) 识别AL错误标记. * 2) 返回该标记代表的AL错误字符串. */ public static String getALErrorString(int err); /** * 1) 识别ALC错误标记. * 2) 返回该标记代表的ALC错误字符串. */ public static String getALCErrorString(int err); /** * 1) 创建缓冲区. * 2) 将wav文件装入缓冲区. * 3) 返回缓冲区id. */ public static int loadALBuffer(String path) throws IOException; /** * 1) 检查该文件是否已经被装载. * 2) 若被装载则返回其缓冲区id. * 3) 否则,将其装载,并返回缓冲区id. */ public static int getLoadedALBuffer(String path) throws IOException; /** * 1) 创建声源. * 2)使用path参数调用GetLoadedALBuffer方法,并将其返回的缓冲区id绑定到声源上。 * 3) 返回声源id */ public static int loadALSample(String path, boolean loop) throws IOException; /** * 1) 释放临时加载的数据段 */ public static void killALLoadedData(); /** * 1) 为应用装载所有声源与缓冲区. */ public static boolean loadALData(); /** * 1) 释放所有缓冲区. * 2) 释放所有声源. */ public static void killALData();
下面进行以上方法的实现:
private static Vector loadedFiles = new Vector(); //临时储存加载的文件路径 private static Vector buffers = new Vector(); // 储存所有被加载的缓冲区 private static Vector sources = new Vector(); // 储存所有合法的声源
仔细观察这些方法并尽可能的想一想我们接下来可能会干什么。大体上讲,我们试图创建一个系统以使我们不在关注缓冲区与声源的关系。我们可以调用由文件创建声源的方法,之后系统自己将会处理缓冲区的创建过程以保证不会有重复的缓冲区(即两个内容相同的缓冲区)。这个系统将缓冲区视为一个有限的资源,并有效的管理它们。
public String getALErrorString(int err) { switch(err) { case AL.AL_NO_ERROR: return "AL_NO_ERROR"; case AL.AL_INVALID_NAME: return "AL_INVALID_NAME"; case AL.AL_INVALID_ENUM: return "AL_INVALID_ENUM"; case AL.AL_INVALID_VALUE: return "AL_INVALID_VALUE"; case AL.AL_INVALID_OPERATION: return "AL_INVALID_OPERATION"; case AL.AL_OUT_OF_MEMORY: return "AL_OUT_OF_MEMORY"; default: return null; } }
这个方法将OpenAL错误变量转为String类型以使控制台(或其他硬件设备)可以显示出来。目前版本的OpenAL文档生成唯一一个需要注意的错误标记是AL_OUT_OF_MEMORY,然而,我们将所有错误标记加以转换来适应未来版本的OpenAL类库。
public String getALCErrorString(int err) { switch(err) { case ALC.ALC_NO_ERROR: return "ALC_NO_ERROR"; case ALC.ALC_INVALID_DEVICE: return "ALC_INVALID_DEVICE"; case ALC.ALC_INVALID_CONTEXT: return "ALC_INVALID_CONTEXT"; case ALC.ALC_INVALID_ENUM: return "ALC_INVALID_ENUM"; case ALC.ALC_INVALID_VALUE: return "ALC_INVALID_VALUE"; case ALC.ALC_OUT_OF_MEMORY: return "ALC_OUT_OF_MEMORY"; default: return null; } }
该函数作用与上一个类似,只不过它翻译的是Alc错误。OpenAL与Alc彼此间通用着标识id,但两者的相同点还不足以通用一个错误检测函数。
另一个关于alGetError函数的事情:OpenAL文档中定义了其在相同时刻仅保留一个错误标记(即没有堆栈机制),调用函数时它仅返回之前所出现的第一个错误,之后将自己置为AL_NO_ERROR。换言之,错误被存于错误标记位当且仅当错误标记未存储其他错误信息。
public int loadALBuffer(String path) throws IOException { //定义缓冲区的数据变量 int[] format = new int[1]; int[] size = new int[1]; ByteBuffer[] data = new ByteBuffer[1]; int[] freq = new int[1]; int[] loop = new int[1]; //缓冲区id及错误检测变量 int[] buffer = new int[1]; int result; //创建一个缓冲区,并检测是否出错 al.alGenBuffers(1, buffer, 0); if ((result = al.alGetError()) != AL.AL_NO_ERROR) throw new IOException(getALErrorString(result)); // 从文件中读入音频数据,检测加载是否正确 ALut.alutLoadWAVFile(path, format, data, size, freq, loop); if ((result = al.alGetError()) != AL.AL_NO_ERROR) throw new IOException(getALErrorString(result)); // 将音频数据装入缓冲区,检测缓冲区是否收到 al.alBufferData(buffer[1], format[0], data[0], size[0], freq[0]); if ((result = al.alGetError()) != AL.AL_NO_ERROR) throw new IOException(getALErrorString(result)); if ((result = al.alGetError()) != AL.AL_NO_ERROR) throw new IOException(getALErrorString(result)); // 返回缓冲区id return buffer[0]; }
正如你所见到的这样,在可能出问题的每一步我们都设置了错误检测,会有多种不同情况的事情导致错误的抛出,例如内存不足无法创建缓冲区或装在数据,wav文件也许不存在,非法数值被送入OpenAL方法等等。
public int getLoadedALBuffer(String path) throws IOException { int count = 0; // 'count' 是缓冲区列表的索引变量 int buffer; // 装在完成的缓冲区id // 迭代器负责遍历文件列表中的所有路径。 Iterator iter = loadedFiles.iterator(); int i = 0; while(iter.hasNext()) { String str = (String)iter.next(); if(str.equals(path)) { return ((Integer)buffers.get(i)).intValue(); } i++; } // 如果执行到了这里,说明文件是新的,我们必须对其创建新的缓冲区 buffer = loadALBuffer(path); // 之后将其加入列表中,这样就将其加载依据注册给系统。 buffers.add(new Integer(buffer)); loadedFiles.add(path); return buffer; }
上面的代码片也许对很多人来讲是个问题,不过它真的没有那么复杂。我们遍历装有文件路径的列表来确定目前已经加载的wav文件。如果里面有我们现在加载的路径,那么仅仅返回它之前装载对应的缓冲区id即可。如果我们不断使用这个方法来加载音频文件,缓冲区也不会由于重复的文件加载请求而浪费。任何以此方式被载入的文件同样需要保留自身列表的引用。缓冲区列表与已加载文件列表是平行的,我的意思是缓冲区在缓冲区列表的索引与对应文件在文件列表的索引值是相同的。[作者使用的应该是JDK1.2之前的版本,显然这里使用Map更为合适一些——译注]
public static int loadALSample(String path, boolean loop) throws IOException { int[] source = new int[1]; int buffer; int result; // 获得文件对应的缓冲区id (加载它如果必要的话). buffer = getLoadedALBuffer(path); //创建一个声源 al.alGenSources(1, source, 0); if ((result = al.alGetError()) != AL.AL_NO_ERROR) throw new IOException(getALErrorString(result)); // 设置生源属性 al.alSourcei (source[0], AL.AL_BUFFER, buffer ); al.alSourcef (source[0], AL.AL_PITCH, 1.0f ); al.alSourcef (source[0], AL.AL_GAIN, 1.0f ); al.alSourcefv(source[0], AL.AL_POSITION, sourcePos, 0); al.alSourcefv(source[0], AL.AL_VELOCITY, sourceVel, 0); al.alSourcei (source[0], AL.AL_LOOPING, loop ? AL.AL_TRUE : AL.AL_FALSE ); // 保存声源 sources.add(new Integer(source[0])); // 返回声源id return source[0]; }
之前的课程编写了一个系统模块来为我们处理缓冲区,我们只需对其稍加拓展即可获得声源。在代码中我们先根据对文件的检索来获得装入文件的对应缓冲区id,将其绑定到一个新的声源上,之后我们将这个声源id保存起来并将其返回。
public static void killALLoadedData() { loadedFiles.clear(); }
全局向量loadedFiles储存着每一个被装入缓冲区的文件路径。在我们将全部文件装入后还保存着它们没什么道理,因此我们将其处理掉。
// 声源id int phaser1; int phaser2; public static void loadALData() throws IOException { // 你应用程序需要做的全部内容,无需担心缓冲区。 phaser1 = loadALSample("wavdata/phaser.wav", false); phaser2 = loadALSample("wavdata/phaser.wav", true); killLoadedALData(); }
在之前的教程中我们见过这个方法,它是装入全部应用所需wav文件的部分。这里可以看出为什么之前的系统模块很有用了,即使我们对同一个文件发出两次加载请求,对于phaser.wav的缓冲区也只有一个,而phaser1、phaser2两个声源会共用这个缓冲区来播放音频。根本不用关系缓冲区的问题因为系统已经自动为我们做好了。
public static void killALData() { // 释放所有缓冲区数据 Iterator iter = buffers.iterator(); while(iter.hasNext()) { al.alDeleteBuffers(1, new int[] { ((Integer)iter.next()).intValue()); } // 释放所有声源 iter = sources.iterator(); while(iter.hasNext()) { al.alDeleteSources(1, new int[] { ((Integer)iter.next()).intValue()); } // 销毁列表项 buffers.clear(); sources.clear(); }
我们使用Collection集合储存全部缓冲区与声源的id。[原文为使用stl库,这里直译为java对应的Collection——译注]我们通过遍历列表来逐一释放它们,并在最后将列表销毁,剩下需要做的工作就是错误检测了。
try { initOpenAL(); loadALData(); } catch(IOException err){ err.printStackTrace(); }
如果加载过程中除了差错我们也能通过正确的渠道注意到问题。当我们捕获到错误时它将会显示在控制台上,我们使用这个功能用于调试或报告错误。
就是这么多了,一个更加高级的错误报告方式,以及更高强度的文件加载方式就介绍到这里。也许以后还要对其进行一些修改来增加可塑性,但我们将会在今后的教程里使用本次实例中的资源加载方法,期待今后教程对其的拓展吧。