001/* A buffer that supports the writing of audio samples to a sound file.
002
003 Copyright (c) 2000-2014 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028package ptolemy.media.javasound;
029
030import java.io.ByteArrayInputStream;
031import java.io.File;
032import java.io.IOException;
033import java.util.ArrayList;
034import java.util.StringTokenizer;
035
036import javax.sound.sampled.AudioFileFormat;
037import javax.sound.sampled.AudioFormat;
038import javax.sound.sampled.AudioInputStream;
039import javax.sound.sampled.AudioSystem;
040
041///////////////////////////////////////////////////////////////////
042//// SoundWriter
043
044/**
045 This class is a buffer that supports the writing of audio samples
046 to a sound file. Specifically, this buffer supports the writing of
047 double-valued audio samples. The maximum valid range of sample
048 values is from -1.0 to 1.0. Any values outside of this range will
049 be hard-clipped to fall within this range.
050 <p>
051 <b>Supported file types</b>
052 <p>
053 Valid sound file formats are WAVE (.wav), AIFF (.aif, .aiff),
054 AU (.au). Valid sample rates are 8000, 11025, 22050, 44100, and
055 48000 Hz. Both 8 bit and 16 bit audio are supported. Mono and
056 stereo files are supported.
057 <p>
058 <b>Usage</b>
059 <p>
060 The path to the sound file to write is given as a constructor
061 parameter, along with parameters that specify the desired
062 audio format. The constructor also takes an array length
063 parameter, which is explained below.
064 <p>
065 After invoking the constructor, the putSamples() method should
066 be repeatedly invoked to write samples to the specified sound
067 file. This method is blocking, so it will not return until the
068 samples have been written. The putSamples() method takes a
069 multidimensional array as a parameter. The first index
070 represents the channel number (0 for first channel, 1 for  second
071 channel, etc.). The second index represents the sample index within
072 a channel. For each channel i, the size of the array,
073 putSamplesArray[i].length, must be equal to the constructor
074 parameter putSamplesArraySize. Otherwise an exception will
075 occur. Thus, each call to putSamples() writes putSamplesArraySize
076 on each channel. It should be noted that the putSamples() method
077 does not write samples directly to a sound file, but instead
078 writes samples to an internal array. The internal array of
079 samples is written to the sound file by the closeFile() method.
080 <p>
081 The closeFile() method should be invoked when no more samples
082 need to be written. This method will write the internal array
083 of samples to the output file and close the file. It is not
084 possible to write any more samples after closeFile() has been
085 called. An exception will occur if putSamples() is invoked
086 at any point after closeFile() is invoked.
087 <p>
088 <b>Security issues</b>
089 <p>
090 Applications have no restrictions on the
091 capturing or playback of audio. Applet code is not allowed to
092 write native files by default. The .java.policy file must be
093 modified to grant applets more privileges.
094 <p>
095 Note: Requires Java 2 v1.3.0 or later.
096
097 @author Brian K. Vogel
098 @version $Id$
099 @since Ptolemy II 1.0
100 @Pt.ProposedRating Red (vogel)
101 @Pt.AcceptedRating Red (cxh)
102 @see ptolemy.media.javasound.SoundCapture
103 */
104public class SoundWriter {
105    /** Construct a sound writer object with the specified name.
106     *  Valid sound file formats are WAVE (.wav), AIFF (.aif,
107     *  .aiff), AU (.au). The file format is automatically
108     *  determined from the file extension.
109     *
110     *  @param fileName The file name to create. If the file already
111     *  exists, overwrite it. Valid sound file formats are WAVE (.wav),
112     *  AIFF (.aif, .aiff), AU (.au). The file format to write is
113     *  determined automatically from the file extension.
114     *  @param sampleRate Sample rate in Hz. Must be in the range: 8000
115     *  to 48000.
116     *  @param bitsPerSample Number of bits per sample (valid choices are
117     *  8 or 16).
118     *  @param channels Number of audio channels. 1 for mono, 2 for
119     *  stereo.
120     *  @param putSamplesArraySize Size of the array parameter of
121     *   putSamples(). There is no restriction on the value of
122     *   this parameter, but typical values are 64-2024.
123     */
124    public SoundWriter(String fileName, float sampleRate, int bitsPerSample,
125            int channels, int putSamplesArraySize) {
126        _isAudioWriterActive = false;
127        this._fileName = fileName;
128        this._bitsPerSample = bitsPerSample;
129        this._sampleRate = sampleRate;
130        this._channels = channels;
131        _initializeAudio();
132        _isAudioWriterActive = true;
133
134        if (_debug) {
135            System.out.println(
136                    "SoundWriter: constructor : fileName = " + fileName);
137            System.out.println("SoundWriter: constructor : bitsPerSample = "
138                    + bitsPerSample);
139            System.out.println(
140                    "SoundWriter: constructor : sampleRate = " + sampleRate);
141            System.out.println(
142                    "SoundWriter: constructor : channels = " + channels);
143            System.out
144                    .println("SoundWriter: constructor : putSamplesArraySize = "
145                            + putSamplesArraySize);
146        }
147    }
148
149    ///////////////////////////////////////////////////////////////////
150    ///  Public Methods                                         ///
151
152    /** Append the audio data contained in <i>putSamplesArray</i>
153     *  to an internal array. The audio samples in this array will
154     *  be written to the sound file specified in the constructor
155     *  when the closeFile() method is invoked.
156     *  <p>
157     *  The samples should be in the range (-1, 1). Samples that are
158     *  outside this range will be hard-clipped so that they fall
159     *  within this range.
160     *  @param putSamplesArray A two dimensional array containing
161     *  the samples to play or write to a file. The first index
162     *  represents the channel number (0 for first channel, 1 for
163     *  second channel, etc.). The second index represents the
164     *  sample index within a channel. For example,
165     *  putSamplesArray[n][m] contains the (m+1)th sample
166     *  of the (n+1)th channel. putSamplesArray should be a
167     *  rectangular array such that putSamplesArray.length() gives
168     *  the number of channels and putSamplesArray[n].length() is
169     *  equal to <i>putSamplesSize</i>, for all channels n. An exception
170     *  will occur if this is not the case.
171     *
172     *  @exception IllegalStateException If closeFile() has already
173     *   been called.
174     */
175    public void putSamples(double[][] putSamplesArray)
176            throws IllegalStateException {
177        if (_isAudioWriterActive == true) {
178            // Convert array of double valued samples into
179            // the proper byte array format.
180            _data = _doubleArrayToByteArray(putSamplesArray, _bytesPerSample,
181                    _channels);
182
183            // Add new audio data to the file buffer array.
184            for (byte element : _data) {
185                _toFileBuffer.add(Byte.valueOf(element));
186            }
187        } else {
188            throw new IllegalStateException("SoundWriter: "
189                    + "putSamples() was called while audio playback was"
190                    + " inactive (startPlayback() was never called or "
191                    + "stopPlayback has already been called).");
192        }
193    }
194
195    /** Open a the file specified in the constructor for writing,
196     *  write the accumulated audio samples (obtained via
197     *  putSamples()) to the file specified in the constructor,
198     *  and close the file. This method should be called when
199     *  no more calls to putSamples() are required. An exception
200     *  will occur if putSamples() is called after this method is invoked.
201     *
202     *  @exception IOException If there is a problem closing the
203     *   audio resources, or if the "write audio data
204     *   to file" constructor was used  and the sound file has an
205     *   unsupported format.
206     */
207    public void closeFile() throws IOException {
208        // IMPLEMENTATION NOTE: It is probably better to open the
209        // file for writing in the constructor. putSamples() should
210        // probably write samples to the file on each invocation.
211        // closeFile() should only close the file. This would result in
212        // more efficient operation and reduced memory usage. There
213        // does not appear to be an easy way to do this in javasound,
214        // however. This is because in javasound, there is an
215        // AudioInputStream but no AudioOutputStream class.
216        if (_debug) {
217            System.out.println("SoundWriter: stopPlayback(): invoked");
218        }
219
220        if (_isAudioWriterActive == true) {
221            // Record data to sound file.
222            _stopPlaybackToFile();
223        }
224
225        _isAudioWriterActive = false;
226    }
227
228    ///////////////////////////////////////////////////////////////////
229    ////                         private methods                   ////
230
231    /** Perform initialization that must be done before putSamples()
232     *  can be invoked.
233     */
234    private void _initializeAudio() {
235        // FIXME: Performance is not great when the incoming audio
236        // samples are being captured in real-time, possibly
237        // due to resizing of the ArrayList.
238        //
239        // Array to hold all data to be saved to file. Grows
240        // as new data are added (via putSamples()).
241        // Each element is a byte of audio data.
242        _toFileBuffer = new ArrayList();
243
244        boolean signed = true;
245        boolean bigEndian = true;
246
247        _playToFileFormat = new AudioFormat(_sampleRate, _bitsPerSample,
248                _channels, signed, bigEndian);
249
250        _frameSizeInBytes = _playToFileFormat.getFrameSize();
251        _bytesPerSample = _bitsPerSample / 8;
252    }
253
254    /** Open a the file specified in the constructor for writing,
255     *  write the accumulated audio samples (obtained via
256     *  putSamples()) to the file specified in the constructor,
257     *  and close the file. This method should be called when
258     *  no more calls to putSamples() are required.
259     */
260    private void _stopPlaybackToFile() throws IOException {
261        int size = _toFileBuffer.size();
262        byte[] audioBytes = new byte[size];
263
264        for (int i = 0; i < size; i++) {
265            Byte j = (Byte) _toFileBuffer.get(i);
266            audioBytes[i] = j.byteValue();
267        }
268
269        ByteArrayInputStream byteInputArrayStream = new ByteArrayInputStream(
270                audioBytes);
271
272        AudioInputStream audioInputStream = new AudioInputStream(
273                byteInputArrayStream, _playToFileFormat,
274                audioBytes.length / _frameSizeInBytes);
275
276        outFile = new File(_fileName);
277
278        try {
279            StringTokenizer st = new StringTokenizer(_fileName, ".");
280
281            // Do error checking:
282            if (st.countTokens() != 2) {
283                throw new IOException("Error: Incorrect " + "file name format. "
284                        + "Format: filename.extension");
285            }
286
287            st.nextToken(); // Advance to the file extension.
288
289            String fileExtension = st.nextToken();
290
291            if (fileExtension.equalsIgnoreCase("au")) {
292                // Save the file.
293                AudioSystem.write(audioInputStream, AudioFileFormat.Type.AU,
294                        outFile);
295            } else if (fileExtension.equalsIgnoreCase("aiff")) {
296                // Save the file.
297                AudioSystem.write(audioInputStream, AudioFileFormat.Type.AIFF,
298                        outFile);
299            } else if (fileExtension.equalsIgnoreCase("wave")) {
300                // Save the file.
301                AudioSystem.write(audioInputStream, AudioFileFormat.Type.WAVE,
302                        outFile);
303            } else if (fileExtension.equalsIgnoreCase("wav")) {
304                // Save the file.
305                AudioSystem.write(audioInputStream, AudioFileFormat.Type.WAVE,
306                        outFile);
307            } else if (fileExtension.equalsIgnoreCase("aifc")) {
308                // Save the file.
309                AudioSystem.write(audioInputStream, AudioFileFormat.Type.AIFC,
310                        outFile);
311            } else {
312                throw new IOException("Error saving "
313                        + "file: Unknown file format: " + fileExtension);
314            }
315        } catch (IOException e) {
316            throw new IOException("SoundWriter: error saving" + " file: " + e);
317        }
318    }
319
320    /* Convert a double array of audio samples into a byte array of
321     * audio samples in linear signed pcm big endian format. The
322     * samples contained in <i>doubleArray</i> should be in the
323     * range (-1, 1). Samples outside this range will be hard clipped
324     * to the range (-1, 1).
325     * @param doubleArray Two dimensional array holding audio samples.
326     * For each channel, m, doubleArray[m] is a single dimensional
327     * array containing samples for channel m.
328     * @param bytesPerSample Number of bytes per sample. Supported
329     * bytes per sample by this method are 8, 16, 24, 32.
330     * @param channels Number of audio channels.
331     * @return The linear signed pcm big endian byte array formatted
332     * array representation of <i>doubleArray</i>. The length of
333     * the returned array is (doubleArray.length*bytesPerSample*channels).
334     */
335    private byte[] _doubleArrayToByteArray(double[][] doubleArray,
336            int bytesPerSample, int channels) {
337        // All channels had better have the same number
338        // of samples! This is not checked!
339        int lengthInSamples = doubleArray[0].length;
340
341        //double  maxSample = Math.pow(2, 8 * bytesPerSample - 1);
342        // Could use above line, but hopefully, code below will
343        // be faster.
344        double maxSample;
345        double maxDoubleValuedSample;
346
347        if (bytesPerSample == 2) {
348            maxSample = 32768;
349        } else if (bytesPerSample == 1) {
350            maxSample = 128;
351        } else if (bytesPerSample == 3) {
352            maxSample = 8388608;
353        } else if (bytesPerSample == 4) {
354            maxSample = 147483648e9;
355        } else {
356            // Should not happen.
357            maxSample = 0;
358        }
359
360        maxDoubleValuedSample = (maxSample - 2) / maxSample;
361
362        byte[] byteArray = new byte[lengthInSamples * bytesPerSample
363                * channels];
364        byte[] b = new byte[bytesPerSample];
365
366        for (int currSamp = 0; currSamp < lengthInSamples; currSamp++) {
367            int l;
368
369            // For each channel,
370            for (int currChannel = 0; currChannel < channels; currChannel++) {
371                // Perform clipping, if necessary.
372                if (doubleArray[currChannel][currSamp] >= maxDoubleValuedSample) {
373                    l = (int) maxSample - 2;
374                } else if (doubleArray[currChannel][currSamp] <= -maxDoubleValuedSample) {
375                    l = (int) -maxSample + 2;
376                } else {
377                    // signed integer representation of current sample of the
378                    // current channel.
379                    l = (int) (doubleArray[currChannel][currSamp] * maxSample);
380                }
381
382                // Create byte representation of current sample.
383                for (int i = 0; i < bytesPerSample; i += 1, l >>= 8) {
384                    b[bytesPerSample - i - 1] = (byte) l;
385                }
386
387                // Copy the byte representation of current sample to
388                // the linear signed pcm big endian formatted byte array.
389                for (int i = 0; i < bytesPerSample; i += 1) {
390                    byteArray[currSamp * bytesPerSample * channels
391                            + bytesPerSample * currChannel + i] = b[i];
392                }
393            }
394        }
395
396        return byteArray;
397    }
398
399    ///////////////////////////////////////////////////////////////////
400    ////                         private variables                 ////
401    // The output file to write to.
402    private File outFile;
403
404    private String _fileName;
405
406    private int _bitsPerSample;
407
408    private float _sampleRate;
409
410    private int _channels;
411
412    // Array of audio samples in byte format.
413    private byte[] _data;
414
415    private int _frameSizeInBytes;
416
417    private ArrayList _toFileBuffer;
418
419    // This is the format of _toFileBuffer.
420    private AudioFormat _playToFileFormat;
421
422    private int _bytesPerSample;
423
424    private boolean _isAudioWriterActive;
425
426    /////////////// For debugging: ///////////////////////////////
427    // Set this variable to "true" to enable debugging information.
428    //private boolean _debug = true;
429    private boolean _debug = false;
430
431    ///////////////////////////////////////////////////////////////////
432}