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}