1 /* 2 * Copyright (c) 2009-2010 jMonkeyEngine 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 12 * * Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33 package com.jme3.audio.plugins; 34 35 import com.jme3.asset.AssetInfo; 36 import com.jme3.asset.AssetLoader; 37 import com.jme3.audio.AudioBuffer; 38 import com.jme3.audio.AudioData; 39 import com.jme3.audio.AudioKey; 40 import com.jme3.audio.AudioStream; 41 import com.jme3.audio.SeekableStream; 42 import com.jme3.util.BufferUtils; 43 import de.jarnbjo.ogg.EndOfOggStreamException; 44 import de.jarnbjo.ogg.LogicalOggStream; 45 import de.jarnbjo.ogg.PhysicalOggStream; 46 import de.jarnbjo.vorbis.IdentificationHeader; 47 import de.jarnbjo.vorbis.VorbisStream; 48 import java.io.ByteArrayOutputStream; 49 import java.io.IOException; 50 import java.io.InputStream; 51 import java.nio.ByteBuffer; 52 import java.util.Collection; 53 import java.util.logging.Level; 54 import java.util.logging.Logger; 55 56 public class OGGLoader implements AssetLoader { 57 58 // private static int BLOCK_SIZE = 4096*64; 59 60 private PhysicalOggStream oggStream; 61 private LogicalOggStream loStream; 62 private VorbisStream vorbisStream; 63 64 // private CommentHeader commentHdr; 65 private IdentificationHeader streamHdr; 66 67 private static class JOggInputStream extends InputStream { 68 69 private boolean endOfStream = false; 70 protected final VorbisStream vs; 71 72 public JOggInputStream(VorbisStream vs){ 73 this.vs = vs; 74 } 75 76 @Override 77 public int read() throws IOException { 78 return 0; 79 } 80 81 @Override 82 public int read(byte[] buf) throws IOException{ 83 return read(buf,0,buf.length); 84 } 85 86 @Override 87 public int read(byte[] buf, int offset, int length) throws IOException{ 88 if (endOfStream) 89 return -1; 90 91 int bytesRead = 0, cnt = 0; 92 assert length % 2 == 0; // read buffer should be even 93 94 while (bytesRead <length) { 95 if ((cnt = vs.readPcm(buf, offset + bytesRead,length - bytesRead)) <= 0) { 96 System.out.println("Read "+cnt+" bytes"); 97 System.out.println("offset "+offset); 98 System.out.println("bytesRead "+bytesRead); 99 System.out.println("buf length "+length); 100 for (int i = 0; i < bytesRead; i++) { 101 System.out.print(buf[i]); 102 } 103 System.out.println(""); 104 105 106 System.out.println("EOS"); 107 endOfStream = true; 108 break; 109 } 110 bytesRead += cnt; 111 } 112 113 swapBytes(buf, offset, bytesRead); 114 return bytesRead; 115 116 } 117 118 @Override 119 public void close() throws IOException{ 120 vs.close(); 121 } 122 123 } 124 125 private static class SeekableJOggInputStream extends JOggInputStream implements SeekableStream { 126 127 private LogicalOggStream los; 128 private float duration; 129 130 public SeekableJOggInputStream(VorbisStream vs, LogicalOggStream los, float duration){ 131 super(vs); 132 this.los = los; 133 this.duration = duration; 134 } 135 136 public void setTime(float time) { 137 System.out.println("--setTime--)"); 138 System.out.println("max granule : "+los.getMaximumGranulePosition()); 139 System.out.println("current granule : "+los.getTime()); 140 System.out.println("asked Time : "+time); 141 System.out.println("new granule : "+(time/duration*los.getMaximumGranulePosition())); 142 System.out.println("new granule2 : "+(time*vs.getIdentificationHeader().getSampleRate())); 143 144 145 146 try { 147 los.setTime((long)(time*vs.getIdentificationHeader().getSampleRate())); 148 } catch (IOException ex) { 149 Logger.getLogger(OGGLoader.class.getName()).log(Level.SEVERE, null, ex); 150 } 151 } 152 153 } 154 155 /** 156 * Returns the total of expected OGG bytes. 157 * 158 * @param dataBytesTotal The number of bytes in the input 159 * @return If the computed number of bytes is less than the number 160 * of bytes in the input, it is returned, otherwise the number 161 * of bytes in the input is returned. 162 */ 163 private int getOggTotalBytes(int dataBytesTotal){ 164 // Vorbis stream could have more samples than than the duration of the sound 165 // Must truncate. 166 int numSamples; 167 if (oggStream instanceof CachedOggStream){ 168 CachedOggStream cachedOggStream = (CachedOggStream) oggStream; 169 numSamples = (int) cachedOggStream.getLastOggPage().getAbsoluteGranulePosition(); 170 }else{ 171 UncachedOggStream uncachedOggStream = (UncachedOggStream) oggStream; 172 numSamples = (int) uncachedOggStream.getLastOggPage().getAbsoluteGranulePosition(); 173 } 174 175 // Number of Samples * Number of Channels * Bytes Per Sample 176 int totalBytes = numSamples * streamHdr.getChannels() * 2; 177 178 // System.out.println("Sample Rate: " + streamHdr.getSampleRate()); 179 // System.out.println("Channels: " + streamHdr.getChannels()); 180 // System.out.println("Stream Length: " + numSamples); 181 // System.out.println("Bytes Calculated: " + totalBytes); 182 // System.out.println("Bytes Available: " + dataBytes.length); 183 184 // Take the minimum of the number of bytes available 185 // and the expected duration of the audio. 186 return Math.min(totalBytes, dataBytesTotal); 187 } 188 189 private float computeStreamDuration(){ 190 // for uncached stream sources, the granule position is not known. 191 if (oggStream instanceof UncachedOggStream) 192 return -1; 193 194 // 2 bytes(16bit) * channels * sampleRate 195 int bytesPerSec = 2 * streamHdr.getChannels() * streamHdr.getSampleRate(); 196 197 // Don't know how many bytes are in input, pass MAX_VALUE 198 int totalBytes = getOggTotalBytes(Integer.MAX_VALUE); 199 200 return (float)totalBytes / bytesPerSec; 201 } 202 203 private ByteBuffer readToBuffer() throws IOException{ 204 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 205 206 byte[] buf = new byte[512]; 207 int read = 0; 208 209 try { 210 while ( (read = vorbisStream.readPcm(buf, 0, buf.length)) > 0){ 211 baos.write(buf, 0, read); 212 } 213 } catch (EndOfOggStreamException ex){ 214 } 215 216 217 byte[] dataBytes = baos.toByteArray(); 218 swapBytes(dataBytes, 0, dataBytes.length); 219 220 int bytesToCopy = getOggTotalBytes( dataBytes.length ); 221 222 ByteBuffer data = BufferUtils.createByteBuffer(bytesToCopy); 223 data.put(dataBytes, 0, bytesToCopy).flip(); 224 225 vorbisStream.close(); 226 loStream.close(); 227 oggStream.close(); 228 229 return data; 230 } 231 232 private static void swapBytes(byte[] b, int off, int len) { 233 byte tempByte; 234 for (int i = off; i < (off+len); i+=2) { 235 tempByte = b[i]; 236 b[i] = b[i+1]; 237 b[i+1] = tempByte; 238 } 239 } 240 241 private InputStream readToStream(boolean seekable,float streamDuration){ 242 if(seekable){ 243 return new SeekableJOggInputStream(vorbisStream,loStream,streamDuration); 244 }else{ 245 return new JOggInputStream(vorbisStream); 246 } 247 } 248 249 private AudioData load(InputStream in, boolean readStream, boolean streamCache) throws IOException{ 250 if (readStream && streamCache){ 251 oggStream = new CachedOggStream(in); 252 }else{ 253 oggStream = new UncachedOggStream(in); 254 } 255 256 Collection<LogicalOggStream> streams = oggStream.getLogicalStreams(); 257 loStream = streams.iterator().next(); 258 259 // if (loStream == null){ 260 // throw new IOException("OGG File does not contain vorbis audio stream"); 261 // } 262 263 vorbisStream = new VorbisStream(loStream); 264 streamHdr = vorbisStream.getIdentificationHeader(); 265 // commentHdr = vorbisStream.getCommentHeader(); 266 267 if (!readStream){ 268 AudioBuffer audioBuffer = new AudioBuffer(); 269 audioBuffer.setupFormat(streamHdr.getChannels(), 16, streamHdr.getSampleRate()); 270 audioBuffer.updateData(readToBuffer()); 271 return audioBuffer; 272 }else{ 273 AudioStream audioStream = new AudioStream(); 274 audioStream.setupFormat(streamHdr.getChannels(), 16, streamHdr.getSampleRate()); 275 276 // might return -1 if unknown 277 float streamDuration = computeStreamDuration(); 278 279 audioStream.updateData(readToStream(oggStream.isSeekable(),streamDuration), streamDuration); 280 return audioStream; 281 } 282 } 283 284 public Object load(AssetInfo info) throws IOException { 285 if (!(info.getKey() instanceof AudioKey)){ 286 throw new IllegalArgumentException("Audio assets must be loaded using an AudioKey"); 287 } 288 289 AudioKey key = (AudioKey) info.getKey(); 290 boolean readStream = key.isStream(); 291 boolean streamCache = key.useStreamCache(); 292 293 InputStream in = null; 294 try { 295 in = info.openStream(); 296 AudioData data = load(in, readStream, streamCache); 297 if (data instanceof AudioStream){ 298 // audio streams must remain open 299 in = null; 300 } 301 return data; 302 } finally { 303 if (in != null){ 304 in.close(); 305 } 306 } 307 308 } 309 310 } 311