001package stdlib;
002/******************************************************************************
003 *  Compilation:  javac StdAudio.java
004 *  Execution:    java StdAudio
005 *  Dependencies: none
006 *  
007 *  Simple library for reading, writing, and manipulating .wav files.
008 *
009 *
010 *  Limitations
011 *  -----------
012 *    - Assumes the audio is monaural, little endian, with sampling rate
013 *      of 44,100
014 *    - check when reading .wav files from a .jar file ?
015 *
016 ******************************************************************************/
017
018import javax.sound.sampled.Clip;
019
020import java.io.File;
021import java.io.ByteArrayInputStream;
022import java.io.InputStream;
023import java.io.IOException;
024
025import java.net.URL;
026
027import javax.sound.sampled.AudioFileFormat;
028import javax.sound.sampled.AudioFormat;
029import javax.sound.sampled.AudioInputStream;
030import javax.sound.sampled.AudioSystem;
031import javax.sound.sampled.DataLine;
032import javax.sound.sampled.LineUnavailableException;
033import javax.sound.sampled.SourceDataLine;
034import javax.sound.sampled.UnsupportedAudioFileException;
035
036/**
037 *  <i>Standard audio</i>. This class provides a basic capability for
038 *  creating, reading, and saving audio. 
039 *  <p>
040 *  The audio format uses a sampling rate of 44,100 Hz, 16-bit, monaural.
041 *
042 *  <p>
043 *  For additional documentation, see <a href="https://introcs.cs.princeton.edu/15inout">Section 1.5</a> of
044 *  <i>Computer Science: An Interdisciplinary Approach</i> by Robert Sedgewick and Kevin Wayne.
045 *
046 *  @author Robert Sedgewick
047 *  @author Kevin Wayne
048 */
049public final class StdAudio {
050
051    /**
052     *  The sample rate: 44,100 Hz for CD quality audio.
053     */
054    public static final int SAMPLE_RATE = 44100;
055
056    private static final int BYTES_PER_SAMPLE = 2;       // 16-bit audio
057    private static final int BITS_PER_SAMPLE = 16;       // 16-bit audio
058    private static final int MAX_16_BIT = 32768;
059    private static final int SAMPLE_BUFFER_SIZE = 4096;
060
061    private static final int MONO   = 1;
062    private static final int STEREO = 2;
063    private static final boolean LITTLE_ENDIAN = false;
064    private static final boolean BIG_ENDIAN    = true;
065    private static final boolean SIGNED        = true;
066    private static final boolean UNSIGNED      = false;
067
068
069    private static SourceDataLine line;   // to play the sound
070    private static byte[] buffer;         // our internal buffer
071    private static int bufferSize = 0;    // number of samples currently in internal buffer
072
073    private StdAudio() {
074        // can not instantiate
075    }
076   
077    // static initializer
078    static {
079        init();
080    }
081
082    // open up an audio stream
083    private static void init() {
084        try {
085            // 44,100 Hz, 16-bit audio, mono, signed PCM, little endian
086            AudioFormat format = new AudioFormat((float) SAMPLE_RATE, BITS_PER_SAMPLE, MONO, SIGNED, LITTLE_ENDIAN);
087            DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
088
089            line = (SourceDataLine) AudioSystem.getLine(info);
090            line.open(format, SAMPLE_BUFFER_SIZE * BYTES_PER_SAMPLE);
091            
092            // the internal buffer is a fraction of the actual buffer size, this choice is arbitrary
093            // it gets divided because we can't expect the buffered data to line up exactly with when
094            // the sound card decides to push out its samples.
095            buffer = new byte[SAMPLE_BUFFER_SIZE * BYTES_PER_SAMPLE/3];
096        }
097        catch (LineUnavailableException e) {
098            System.out.println(e.getMessage());
099        }
100
101        // no sound gets made before this call
102        line.start();
103    }
104
105    // get an AudioInputStream object from a file
106    @SuppressWarnings("unused")
107        private static AudioInputStream getAudioInputStreamFromFile(String filename) {
108        if (filename == null) {
109            throw new IllegalArgumentException("filename is null");
110        }
111
112        try {
113            // first try to read file from local file system
114            File file = new File(filename);
115            if (file.exists()) {
116                return AudioSystem.getAudioInputStream(file);
117            }
118
119            // resource relative to .class file
120            InputStream is1 = StdAudio.class.getResourceAsStream(filename);
121            if (is1 != null) {
122                return AudioSystem.getAudioInputStream(is1);
123            }
124
125            // resource relative to classloader root
126            InputStream is2 = StdAudio.class.getClassLoader().getResourceAsStream(filename);
127            if (is2 != null) {
128                return AudioSystem.getAudioInputStream(is2);
129            }
130
131            // from URL (including jar file)
132            URL url = new URL(filename);
133            if (url != null) {
134                return AudioSystem.getAudioInputStream(url);
135            }
136
137            // give up
138            throw new IllegalArgumentException("could not read '" + filename + "'");            
139        }
140        catch (IOException e) {
141            throw new IllegalArgumentException("could not read '" + filename + "'", e);
142        }
143        catch (UnsupportedAudioFileException e) {
144            throw new IllegalArgumentException("file of unsupported audio format: '" + filename + "'", e);
145        }
146    }
147
148    /**
149     * Closes standard audio.
150     */
151    public static void close() {
152        line.drain();
153        line.stop();
154    }
155    
156    /**
157     * Writes one sample (between -1.0 and +1.0) to standard audio.
158     * If the sample is outside the range, it will be clipped.
159     *
160     * @param  sample the sample to play
161     * @throws IllegalArgumentException if the sample is {@code Double.NaN}
162     */
163    public static void play(double sample) {
164        if (Double.isNaN(sample)) throw new IllegalArgumentException("sample is NaN");
165
166        // clip if outside [-1, +1]
167        if (sample < -1.0) sample = -1.0;
168        if (sample > +1.0) sample = +1.0;
169
170        // convert to bytes
171        short s = (short) (MAX_16_BIT * sample);
172        if (sample == 1.0) s = Short.MAX_VALUE;   // special case since 32768 not a short
173        buffer[bufferSize++] = (byte) s;
174        buffer[bufferSize++] = (byte) (s >> 8);   // little endian
175
176        // send to sound card if buffer is full        
177        if (bufferSize >= buffer.length) {
178            line.write(buffer, 0, buffer.length);
179            bufferSize = 0;
180        }
181    }
182
183    /**
184     * Writes the array of samples (between -1.0 and +1.0) to standard audio.
185     * If a sample is outside the range, it will be clipped.
186     *
187     * @param  samples the array of samples to play
188     * @throws IllegalArgumentException if any sample is {@code Double.NaN}
189     * @throws IllegalArgumentException if {@code samples} is {@code null}
190     */
191    public static void play(double[] samples) {
192        if (samples == null) throw new IllegalArgumentException("argument to play() is null");
193        for (int i = 0; i < samples.length; i++) {
194            play(samples[i]);
195        }
196    }
197
198    /**
199     * Reads audio samples from a file (in .wav or .au format) and returns
200     * them as a double array with values between -1.0 and +1.0.
201     * The audio file must be 16-bit with a sampling rate of 44,100.
202     * It can be mono or stereo.
203     *
204     * @param  filename the name of the audio file
205     * @return the array of samples
206     */
207    public static double[] read(String filename) {
208
209        // make sure that AudioFormat is 16-bit, 44,100 Hz, little endian
210        final AudioInputStream ais = getAudioInputStreamFromFile(filename);
211        AudioFormat audioFormat = ais.getFormat();
212
213        // require sampling rate = 44,100 Hz
214        if (audioFormat.getSampleRate() != SAMPLE_RATE) {
215            throw new IllegalArgumentException("StdAudio.read() currently supports only a sample rate of " + SAMPLE_RATE + " Hz\n"
216                                             + "audio format: " + audioFormat);
217        }
218
219        // require 16-bit audio
220        if (audioFormat.getSampleSizeInBits() != BITS_PER_SAMPLE) {
221            throw new IllegalArgumentException("StdAudio.read() currently supports only " + BITS_PER_SAMPLE + "-bit audio\n"
222                                             + "audio format: " + audioFormat);
223        }
224
225        // require little endian
226        if (audioFormat.isBigEndian()) {
227            throw new IllegalArgumentException("StdAudio.read() currently supports only audio stored using little endian\n"
228                                             + "audio format: " + audioFormat);
229        }
230
231        byte[] bytes = null;
232        try {
233            int bytesToRead = ais.available();
234            bytes = new byte[bytesToRead];
235            int bytesRead = ais.read(bytes);
236            if (bytesToRead != bytesRead) {
237                throw new IllegalStateException("read only " + bytesRead + " of " + bytesToRead + " bytes"); 
238            }
239        }
240        catch (IOException ioe) {
241            throw new IllegalArgumentException("could not read '" + filename + "'", ioe);
242        }
243
244        int n = bytes.length;
245
246        // little endian, mono
247        if (audioFormat.getChannels() == MONO) {
248            double[] data = new double[n/2];
249            for (int i = 0; i < n/2; i++) {
250                // little endian, mono
251                data[i] = ((short) (((bytes[2*i+1] & 0xFF) << 8) | (bytes[2*i] & 0xFF))) / ((double) MAX_16_BIT);
252            }
253            return data;
254        }
255
256        // little endian, stereo
257        else if (audioFormat.getChannels() == STEREO) {
258            double[] data = new double[n/4];
259            for (int i = 0; i < n/4; i++) {
260                double left  = ((short) (((bytes[4*i+1] & 0xFF) << 8) | (bytes[4*i + 0] & 0xFF))) / ((double) MAX_16_BIT);
261                double right = ((short) (((bytes[4*i+3] & 0xFF) << 8) | (bytes[4*i + 2] & 0xFF))) / ((double) MAX_16_BIT);
262                data[i] = (left + right) / 2.0;
263            }
264            return data;
265        }
266
267        // TODO: handle big endian (or other formats)
268        else throw new IllegalStateException("audio format is neither mono or stereo");
269    }
270
271    /**
272     * Saves the double array as an audio file (using .wav or .au format).
273     *
274     * @param  filename the name of the audio file
275     * @param  samples the array of samples
276     * @throws IllegalArgumentException if unable to save {@code filename}
277     * @throws IllegalArgumentException if {@code samples} is {@code null}
278     * @throws IllegalArgumentException if {@code filename} is {@code null}
279     * @throws IllegalArgumentException if {@code filename} extension is not {@code .wav}
280     *         or {@code .au}
281     */
282    public static void save(String filename, double[] samples) {
283        if (filename == null) {
284            throw new IllegalArgumentException("filenameis null");
285        }
286        if (samples == null) {
287            throw new IllegalArgumentException("samples[] is null");
288        }
289
290        // assumes 16-bit samples with sample rate = 44,100 Hz
291        // use 16-bit audio, mono, signed PCM, little Endian
292        AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, MONO, SIGNED, LITTLE_ENDIAN);
293        byte[] data = new byte[2 * samples.length];
294        for (int i = 0; i < samples.length; i++) {
295            int temp = (short) (samples[i] * MAX_16_BIT);
296            if (samples[i] == 1.0) temp = Short.MAX_VALUE;   // special case since 32768 not a short
297            data[2*i + 0] = (byte) temp;
298            data[2*i + 1] = (byte) (temp >> 8);   // little endian
299        }
300
301        // now save the file
302        try {
303            ByteArrayInputStream bais = new ByteArrayInputStream(data);
304            AudioInputStream ais = new AudioInputStream(bais, format, samples.length);
305            if (filename.endsWith(".wav") || filename.endsWith(".WAV")) {
306                AudioSystem.write(ais, AudioFileFormat.Type.WAVE, new File(filename));
307            }
308            else if (filename.endsWith(".au") || filename.endsWith(".AU")) {
309                AudioSystem.write(ais, AudioFileFormat.Type.AU, new File(filename));
310            }
311            else {
312                throw new IllegalArgumentException("file type for saving must be .wav or .au");
313            }
314        }
315        catch (IOException ioe) {
316            throw new IllegalArgumentException("unable to save file '" + filename + "'", ioe);
317        }
318    }
319
320    /**
321     * Plays an audio file (in .wav, .mid, or .au format) in a background thread.
322     *
323     * @param filename the name of the audio file
324     * @throws IllegalArgumentException if unable to play {@code filename}
325     * @throws IllegalArgumentException if {@code filename} is {@code null}
326     * @deprecated replaced by {@link #playInBackground(String filename)}
327     */
328    @Deprecated
329    public static synchronized void play(String filename) {
330        playInBackground(filename);
331    }
332
333    /**
334     * Plays an audio file (in .wav, .mid, or .au format) in a background thread.
335     *
336     * @param filename the name of the audio file
337     * @throws IllegalArgumentException if unable to play {@code filename}
338     * @throws IllegalArgumentException if {@code filename} is {@code null}
339     */
340    public static synchronized void playInBackground(final String filename) {
341        new Thread(new Runnable() {
342            public void run() {
343                AudioInputStream ais = getAudioInputStreamFromFile(filename);
344                stream(ais);
345            }
346        }).start();
347    }
348
349
350    // https://www3.ntu.edu.sg/home/ehchua/programming/java/J8c_PlayingSound.html
351    // play a wav or aif file
352    // javax.sound.sampled.Clip fails for long clips (on some systems), perhaps because
353    // JVM closes (see remedy in loop)
354    private static void stream(AudioInputStream ais) {
355        SourceDataLine line = null;
356        int BUFFER_SIZE = 4096; // 4K buffer
357
358        try {
359            AudioFormat audioFormat = ais.getFormat();
360            DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
361            line = (SourceDataLine) AudioSystem.getLine(info);
362            line.open(audioFormat);
363            line.start();
364            byte[] samples = new byte[BUFFER_SIZE];
365            int count = 0;
366            while ((count = ais.read(samples, 0, BUFFER_SIZE)) != -1) {
367                line.write(samples, 0, count);
368            }
369        }
370        catch (IOException e) {
371            e.printStackTrace();
372        }
373        catch (LineUnavailableException e) {
374            e.printStackTrace();
375        }
376        finally {
377            if (line != null) {
378                line.drain();
379                line.close();
380            }
381        }
382    }
383
384    /**
385     * Loops an audio file (in .wav, .mid, or .au format) in a background thread.
386     *
387     * @param filename the name of the audio file
388     * @throws IllegalArgumentException if {@code filename} is {@code null}
389     * @deprecated replaced by {@link #loopInBackground(String filename)}
390     */
391    @Deprecated
392    public static synchronized void loop(String filename) {
393        loopInBackground(filename);
394    }
395
396    /**
397     * Loops an audio file (in .wav, .mid, or .au format) in a background thread.
398     *
399     * @param filename the name of the audio file
400     * @throws IllegalArgumentException if {@code filename} is {@code null}
401     */
402    public static synchronized void loopInBackground(String filename) {
403        if (filename == null) throw new IllegalArgumentException();
404
405        final AudioInputStream ais = getAudioInputStreamFromFile(filename);
406
407        try {
408            Clip clip = AudioSystem.getClip();
409            // Clip clip = (Clip) AudioSystem.getLine(new Line.Info(Clip.class));
410            clip.open(ais);
411            clip.loop(Clip.LOOP_CONTINUOUSLY);
412        }
413        catch (LineUnavailableException e) {
414            e.printStackTrace();
415        }
416        catch (IOException e) {
417            e.printStackTrace();
418        }
419
420        // keep JVM open
421        new Thread(new Runnable() {
422            public void run() {
423                while (true) {
424                    try {
425                       Thread.sleep(1000);
426                    }
427                    catch (InterruptedException e) {
428                        e.printStackTrace();
429                    }
430                }
431            }
432        }).start();
433    }
434
435
436   /***************************************************************************
437    * Unit tests {@code StdAudio}.
438    ***************************************************************************/
439
440    // create a note (sine wave) of the given frequency (Hz), for the given
441    // duration (seconds) scaled to the given volume (amplitude)
442    private static double[] note(double hz, double duration, double amplitude) {
443        int n = (int) (StdAudio.SAMPLE_RATE * duration);
444        double[] a = new double[n+1];
445        for (int i = 0; i <= n; i++)
446            a[i] = amplitude * Math.sin(2 * Math.PI * i * hz / StdAudio.SAMPLE_RATE);
447        return a;
448    }
449
450    /**
451     * Test client - play an A major scale to standard audio.
452     *
453     * @param args the command-line arguments
454     */
455    /**
456     * Test client - play an A major scale to standard audio.
457     *
458     * @param args the command-line arguments
459     */
460    public static void main(String[] args) {
461        
462        // 440 Hz for 1 sec
463        double freq = 440.0;
464        for (int i = 0; i <= StdAudio.SAMPLE_RATE; i++) {
465            StdAudio.play(0.5 * Math.sin(2*Math.PI * freq * i / StdAudio.SAMPLE_RATE));
466        }
467        
468        // scale increments
469        int[] steps = { 0, 2, 4, 5, 7, 9, 11, 12 };
470        for (int i = 0; i < steps.length; i++) {
471            double hz = 440.0 * Math.pow(2, steps[i] / 12.0);
472            StdAudio.play(note(hz, 1.0, 0.5));
473        }
474
475
476        // need to call this in non-interactive stuff so the program doesn't terminate
477        // until all the sound leaves the speaker.
478        StdAudio.close(); 
479    }
480}
481
482/******************************************************************************
483 *  Copyright 2002-2020, Robert Sedgewick and Kevin Wayne.
484 *
485 *  This file is part of algs4.jar, which accompanies the textbook
486 *
487 *      Algorithms, 4th edition by Robert Sedgewick and Kevin Wayne,
488 *      Addison-Wesley Professional, 2011, ISBN 0-321-57351-X.
489 *      http://algs4.cs.princeton.edu
490 *
491 *
492 *  algs4.jar is free software: you can redistribute it and/or modify
493 *  it under the terms of the GNU General Public License as published by
494 *  the Free Software Foundation, either version 3 of the License, or
495 *  (at your option) any later version.
496 *
497 *  algs4.jar is distributed in the hope that it will be useful,
498 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
499 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
500 *  GNU General Public License for more details.
501 *
502 *  You should have received a copy of the GNU General Public License
503 *  along with algs4.jar.  If not, see http://www.gnu.org/licenses.
504 ******************************************************************************/