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 ******************************************************************************/