001package jmri.jmrit.audio; 002 003import java.io.File; 004import java.io.IOException; 005import java.io.InputStream; 006import java.nio.ByteBuffer; 007import java.nio.ByteOrder; 008import java.nio.ShortBuffer; 009import javax.sound.sampled.AudioFormat; 010import javax.sound.sampled.AudioInputStream; 011import javax.sound.sampled.AudioSystem; 012import javax.sound.sampled.UnsupportedAudioFileException; 013import jmri.util.FileUtil; 014import org.slf4j.Logger; 015import org.slf4j.LoggerFactory; 016 017/** 018 * JavaSound implementation of the Audio Buffer sub-class. 019 * <p> 020 * For now, no system-specific implementations are forseen - this will remain 021 * internal-only 022 * <p> 023 * For more information about the JavaSound API, visit 024 * <a href="http://java.sun.com/products/java-media/sound/">http://java.sun.com/products/java-media/sound/</a> 025 * 026 * <hr> 027 * This file is part of JMRI. 028 * <p> 029 * JMRI is free software; you can redistribute it and/or modify it under the 030 * terms of version 2 of the GNU General Public License as published by the Free 031 * Software Foundation. See the "COPYING" file for a copy of this license. 032 * <p> 033 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 034 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 035 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 036 * 037 * @author Matthew Harris copyright (c) 2009, 2011 038 */ 039public class JavaSoundAudioBuffer extends AbstractAudioBuffer { 040 041 /** 042 * Holds the AudioFormat of this buffer 043 */ 044 private transient AudioFormat audioFormat; 045 046 /** 047 * Byte array used to store the actual data read from the file 048 */ 049 private byte[] dataStorageBuffer; 050 051 /** 052 * Frequency of this AudioBuffer. Used to calculate pitch changes 053 */ 054 private int freq; 055 056 private long size; 057 058 /** 059 * Reference to the AudioInputStream used to read sound data from the file 060 */ 061 private transient AudioInputStream audioInputStream; 062 063 /** 064 * Holds the initialised status of this AudioBuffer 065 */ 066 private boolean initialised = false; 067 068 /** 069 * Constructor for new JavaSoundAudioBuffer with system name 070 * 071 * @param systemName AudioBuffer object system name (e.g. IAB4) 072 */ 073 public JavaSoundAudioBuffer(String systemName) { 074 super(systemName); 075 if (log.isDebugEnabled()) { 076 log.debug("New JavaSoundAudioBuffer: {}", systemName); 077 } 078 initialised = init(); 079 } 080 081 /** 082 * Constructor for new JavaSoundAudioBuffer with system name and user name 083 * 084 * @param systemName AudioBuffer object system name (e.g. IAB4) 085 * @param userName AudioBuffer object user name 086 */ 087 public JavaSoundAudioBuffer(String systemName, String userName) { 088 super(systemName, userName); 089 if (log.isDebugEnabled()) { 090 log.debug("New JavaSoundAudioBuffer: {} ({})", userName, systemName); 091 } 092 initialised = init(); 093 } 094 095 /** 096 * Performs any necessary initialisation of this AudioBuffer 097 * 098 * @return True if successful 099 */ 100 private boolean init() { 101 this.audioFormat = null; 102 dataStorageBuffer = null; 103 this.freq = 0; 104 this.size = 0; 105 this.setStartLoopPoint(0, false); 106 this.setEndLoopPoint(0, false); 107 this.setState(STATE_EMPTY); 108 return true; 109 } 110 111 /** 112 * Return reference to the DataStorageBuffer byte array 113 * <p> 114 * Applies only to sub-types: 115 * <ul> 116 * <li>Buffer 117 * </ul> 118 * 119 * @return buffer[] reference to DataStorageBuffer 120 */ 121 protected byte[] getDataStorageBuffer() { 122 return dataStorageBuffer; 123 } 124 125 /** 126 * Retrieves the format of the sound sample stored in this buffer as an 127 * AudioFormat object 128 * 129 * @return audio format as an AudioFormat object 130 */ 131 protected AudioFormat getAudioFormat() { 132 return audioFormat; 133 } 134 135 @Override 136 protected boolean loadBuffer(InputStream stream) { 137 if (!initialised) { 138 return false; 139 } 140 141 // Reinitialise 142 init(); 143 144 // Create the input stream for the audio file 145 try { 146 audioInputStream = AudioSystem.getAudioInputStream(stream); 147 } catch (UnsupportedAudioFileException ex) { 148 log.error("Unsupported audio file format when loading buffer", ex); 149 return false; 150 } catch (IOException ex) { 151 log.error("Error loading buffer", ex); 152 return false; 153 } 154 155 return (this.processBuffer()); 156 } 157 158 @Override 159 protected boolean loadBuffer() { 160 if (!initialised) { 161 return false; 162 } 163 164 // Reinitialise 165 init(); 166 167 // Retrieve filename of specified .wav file 168 File file = new File(FileUtil.getExternalFilename(this.getURL())); 169 170 // Create the input stream for the audio file 171 try { 172 audioInputStream = AudioSystem.getAudioInputStream(file); 173 } catch (UnsupportedAudioFileException ex) { 174 log.error("Unsupported audio file format when loading buffer", ex); 175 return false; 176 } catch (IOException ex) { 177 log.error("Error loading buffer", ex); 178 return false; 179 } 180 181 return (this.processBuffer()); 182 } 183 184 private boolean processBuffer() { 185 186 // Temporary storage buffer 187 byte[] buffer; 188 189 // Get the AudioFormat 190 audioFormat = audioInputStream.getFormat(); 191 this.freq = (int) audioFormat.getSampleRate(); 192 193 // Determine the required buffer size in bytes 194 // number of channels * length in frames * sample size in bits / 8 bits in a byte 195 int dataSize = audioFormat.getChannels() 196 * (int) audioInputStream.getFrameLength() 197 * audioFormat.getSampleSizeInBits() / 8; 198 if (log.isDebugEnabled()) { 199 log.debug("Size of JavaSoundAudioBuffer ({}) = {}", this.getSystemName(), dataSize); 200 } 201 if (dataSize > 0) { 202 // Allocate buffer space 203 buffer = new byte[dataSize]; 204 205 // Load into data buffer 206 int bytesRead; 207 int totalBytesRead = 0; 208 try { 209 // Read until end of audioInputStream reached 210 log.debug("Start to load JavaSoundBuffer..."); 211 while ((bytesRead 212 = audioInputStream.read(buffer, 213 totalBytesRead, 214 buffer.length - totalBytesRead)) 215 != -1 && totalBytesRead < buffer.length) { 216 log.debug("read {} bytes of total {}", bytesRead, dataSize); 217 totalBytesRead += bytesRead; 218 } 219 } catch (IOException ex) { 220 log.error("Error when reading JavaSoundAudioBuffer ({})", this.getSystemName(), ex); 221 return false; 222 } 223 224 // Done. All OK. 225 log.debug("...finished loading JavaSoundBuffer"); 226 } else { 227 // Not loaded anything 228 log.warn("Unable to determine length of JavaSoundAudioBuffer ({})", this.getSystemName()); 229 log.warn(" - buffer has not been loaded."); 230 return false; 231 } 232 233 // Done loading - need to convert byte endian order 234 this.dataStorageBuffer = convertAudioEndianness(buffer, audioFormat.getSampleSizeInBits() == 16); 235 236 // Set initial loop points 237 this.setStartLoopPoint(0, false); 238 this.setEndLoopPoint(audioInputStream.getFrameLength(), false); 239 this.generateLoopBuffers(LOOP_POINT_BOTH); 240 241 // Store length of sample 242 this.size = audioInputStream.getFrameLength(); 243 244 this.setState(STATE_LOADED); 245 if (log.isDebugEnabled()) { 246 log.debug("Loaded buffer: {}", this.getSystemName()); 247 log.debug(" from file: {}", this.getURL()); 248 log.debug(" format: {}, {} Hz", parseFormat(), freq); 249 log.debug(" length: {}", audioInputStream.getFrameLength()); 250 } 251 return true; 252 253 } 254 255 @Override 256 protected void generateLoopBuffers(int which) { 257 // TODO: Actually write this bit 258 //if ((which==LOOP_POINT_START)||(which==LOOP_POINT_BOTH)) { 259 //} 260 //if ((which==LOOP_POINT_END)||(which==LOOP_POINT_BOTH)) { 261 //} 262 if (log.isDebugEnabled()) { 263 log.debug("Method generateLoopBuffers() called for JavaSoundAudioBuffer {}", this.getSystemName()); 264 } 265 } 266 267 @Override 268 protected boolean generateStreamingBuffers() { 269 // TODO: Actually write this bit 270 if (log.isDebugEnabled()) { 271 log.debug("Method generateStreamingBuffers() called for JavaSoundAudioBuffer {}", this.getSystemName()); 272 } 273 return true; 274 } 275 276 @Override 277 protected void removeStreamingBuffers() { 278 // TODO: Actually write this bit 279 if (log.isDebugEnabled()) { 280 log.debug("Method removeStreamingBuffers() called for JavaSoundAudioBuffer {}", this.getSystemName()); 281 } 282 } 283 284 @Override 285 public int getFormat() { 286 if (audioFormat != null) { 287 if (audioFormat.getChannels() == 1 && audioFormat.getSampleSizeInBits() == 8) { 288 return FORMAT_8BIT_MONO; 289 } else if (audioFormat.getChannels() == 1 && audioFormat.getSampleSizeInBits() == 16) { 290 return FORMAT_16BIT_MONO; 291 } else if (audioFormat.getChannels() == 2 && audioFormat.getSampleSizeInBits() == 8) { 292 return FORMAT_8BIT_STEREO; 293 } else if (audioFormat.getChannels() == 2 && audioFormat.getSampleSizeInBits() == 16) { 294 return FORMAT_16BIT_STEREO; 295 } else { 296 return FORMAT_UNKNOWN; 297 } 298 } 299 return FORMAT_UNKNOWN; 300 } 301 302 @Override 303 public long getLength() { 304 return this.size; 305 } 306 307 @Override 308 public int getFrequency() { 309 return this.freq; 310 } 311 312 /** 313 * Internal method to return a string representation of the audio format 314 * 315 * @return string representation 316 */ 317 private String parseFormat() { 318 switch (this.getFormat()) { 319 case FORMAT_8BIT_MONO: 320 return "8-bit mono"; 321 case FORMAT_16BIT_MONO: 322 return "16-bit mono"; 323 case FORMAT_8BIT_STEREO: 324 return "8-bit stereo"; 325 case FORMAT_16BIT_STEREO: 326 return "16-bit stereo"; 327 default: 328 return "unknown format"; 329 } 330 } 331 332 /** 333 * Converts the endianness of an AudioBuffer to the format required by the 334 * JRE. 335 * 336 * @param audioData byte array containing the read PCM data 337 * @param twoByteSamples true if 16-bits per sample 338 * @return byte array containing converted PCM data 339 */ 340 private static byte[] convertAudioEndianness(byte[] audioData, boolean twoByteSamples) { 341 342 // Create ByteBuffer for output and set endianness 343 ByteBuffer out = ByteBuffer.allocate(audioData.length); 344 out.order(ByteOrder.nativeOrder()); 345 346 // Wrap the audioData into a ByteBuffer for input and set endianness 347 // (always Little Endian for a WAV file) 348 ByteBuffer in = ByteBuffer.wrap(audioData); 349 in.order(ByteOrder.LITTLE_ENDIAN); 350 351 // Check if we have double-byte samples (i.e. 16-bit) 352 if (twoByteSamples) { 353 // If so, create ShortBuffer views of the in and out ByteBuffers 354 // for further processing 355 ShortBuffer outShort = out.asShortBuffer(); 356 ShortBuffer inShort = in.asShortBuffer(); 357 358 // Loop through appending data to the output buffer 359 while (inShort.hasRemaining()) { 360 outShort.put(inShort.get()); 361 } 362 363 } else { 364 // Otherwise, just loop through appending data to the output buffer 365 while (in.hasRemaining()) { 366 out.put(in.get()); 367 } 368 } 369 370 // Rewind the ByteBuffer 371 out.rewind(); 372 373 // Convert output to an array if necessary 374 if (!out.hasArray()) { 375 // Allocate space 376 byte[] array = new byte[out.capacity()]; 377 // fill the array 378 out.get(array); 379 // clear the ByteBuffer 380 out.clear(); 381 382 return array; 383 } 384 385 return out.array(); 386 } 387 388 @Override 389 protected void cleanup() { 390 if (log.isDebugEnabled()) { 391 log.debug("Cleanup JavaSoundAudioBuffer ({})", this.getSystemName()); 392 } 393 } 394 395 private static final Logger log = LoggerFactory.getLogger(JavaSoundAudioBuffer.class); 396 397}