001/* 002 * $Rev: 1023 $: Revision of last commit 003 * $Author: tgutwin $: Author of last commit 004 * $Date: 2015-10-28 15:59:29 -0700 (Wed, 28 Oct 2015) $: Date of last commit 005 * $URL: svn://svn.webarts.bc.ca/open/trunk/projects/WebARTS/ca/bc/webarts/tools/Recorder.java $ 006 */ 007/* 008 * 009 * Written by Tom Gutwin - WebARTS Design. 010 * Copyright (C) 2015-2019 WebARTS Design, North Vancouver Canada 011 * http://www.webarts.bc.ca 012 * 013 * This program is free software; you can redistribute it and/or modify 014 * it under the terms of the GNU General Public License as published by 015 * the Free Software Foundation; either version 2 of the License, or 016 * (at your option) any later version. 017 * 018 * This program is distributed in the hope that it will be useful, 019 * but WITHOUT ANY WARRANTY; without_ even the implied warranty of 020 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 021 * GNU General Public License for more details. 022 * 023 * You should have received a copy of the GNU General Public License 024 * along with this program; if not, write to the Free Software 025 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 026 */ 027 028package ca.bc.webarts.tools; 029 030import javax.swing.*; 031import javax.swing.border.*; 032// import java.io.*; 033import java.awt.*; 034import java.awt.event.*; 035// import javax.sound.sampled.*; 036 037import java.io.File; 038import java.io.IOException; 039import javax.sound.sampled.AudioFormat; 040import javax.sound.sampled.AudioFileFormat; 041import javax.sound.sampled.AudioInputStream; 042import javax.sound.sampled.AudioSystem; 043import javax.sound.sampled.DataLine; 044import javax.sound.sampled.FloatControl; 045import javax.sound.sampled.Line; 046import javax.sound.sampled.LineUnavailableException; 047import javax.sound.sampled.SourceDataLine; 048import javax.sound.sampled.TargetDataLine; 049import javax.sound.sampled.UnsupportedAudioFileException; 050 051import ca.bc.webarts.widgets.Util; 052 053 054/** Very basic audio Reclorder and Player. It sample the microphone input to a file, and also alows the playback of the last sample file it saved. **/ 055public class Recorder extends JFrame implements Runnable 056{ 057 058 private JLabel filenameLabel; // Displays the current file name 059 private JButton record; // Record/Stop button 060 private JButton playButton; 061 062 private File currentDir; // Current directory 063 private File fileOut; // Current sound file 064 065 private String filename = "javaAudioSample"; // Basic file name 066 private int filenameSuffix = 0; // Name differentiator 067 private String soundFileName; // The complete file name 068 069 final Color recordColor = Color.red; // Recording state color 070 Color stopColor; // Stop state color 071 072 // Our chosen audio format or we could use a different format 073 // as long as there is a line to support it 074 final int MONO = 1; 075 076 private int sampleRate = 8000; 077 078 // Create target PCM wav AudioFormat. In this case, mono sound. 079 private AudioFormat pcmHiQFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, /* 44100, 16, MONO, 2, 44100, true); */ 080 16000, 16, MONO, 2, 16000, true); 081 082 private AudioFormat pcmLowQFormat = new AudioFormat(8000, 16, MONO, true, false); 083 084 private AudioFormat pcmFormat = pcmHiQFormat; 085 086 // Create target Speex AudioFormat. In this case, mono sound. 087 private AudioFormat speexFormat = new AudioFormat(org.xiph.speex.spi.SpeexEncoding.SPEEX, 088 sampleRate, 8, MONO, 2, sampleRate, true); 089 090 private AudioFormat format = pcmFormat; 091 092 private boolean useSpeex = false; 093 094 // Sound file type 095 private AudioFileFormat.Type fileType = AudioFileFormat.Type.WAVE; 096 097 098 private TargetDataLine mic; // The microphone input 099 private Thread thread; // The recording thread 100 101 WaveAudioPlayer wavPlayer;// Player Thread 102 103 104 105 public static void main(String[] args) 106 { 107 Recorder recorder = new Recorder( ((args.length>0) && args[0].equals("-spx"))); 108 } 109 110 111 public Recorder() 112 { 113 if(useSpeex) switchToSpeex(); 114 initGui(); 115 } 116 117 118 public Recorder(boolean useSpeex) 119 { 120 this.useSpeex = useSpeex; 121 if(useSpeex) switchToSpeex(); 122 initGui(); 123 } 124 125 126 private void initGui() 127 { 128 setDefaultCloseOperation(EXIT_ON_CLOSE); 129 setTitle((useSpeex?"SPEEX":"Sound") + " Recorder"); 130 setSize(250,200); 131 132 currentDir = new File(System.getProperty("user.dir")); // Get current directory 133 134 // Create a panel for the file name 135 JPanel filenamePane = new JPanel(new GridLayout(0,1)); 136 CompoundBorder border = BorderFactory.createCompoundBorder (BorderFactory.createEmptyBorder(5,5,5,5), BorderFactory.createRaisedBevelBorder()); 137 filenamePane.setBorder(BorderFactory.createCompoundBorder(border, BorderFactory.createEmptyBorder(5,5,5,5))); 138 139 // Get a new file 140 if ((fileOut = getNewFile()) == null) 141 { 142 System.err.println("Cannot create file"); 143 System.exit(1); 144 } 145 146 filenameLabel = new JLabel(fileOut.getName(), SwingConstants.CENTER); 147 stopColor = filenameLabel.getForeground(); 148 filenamePane.add(filenameLabel); 149 Container content = getContentPane(); 150 content.add(filenamePane, BorderLayout.NORTH); 151 152 // Add the Record/Stop button 153 record = new JButton("RECORD"); 154 record.setBorder(border); 155 record.addActionListener(new ActionListener() 156 { 157 public void actionPerformed(ActionEvent e) 158 { 159 System.out.println("Record Action: "+e.getActionCommand()); 160 if (e.getActionCommand().equals("RECORD")) 161 { 162 record.setText("STOP"); 163 startRecording(); 164 } 165 else 166 { 167 stopRecording(); 168 record.setText("RECORD"); 169 } 170 } 171 } 172 ); 173 content.add(record, BorderLayout.CENTER); 174 playButton = new JButton("Play Last Sample"); 175 playButton.setBorder(border); 176 playButton.addActionListener(new ActionListener() 177 { 178 public void actionPerformed(ActionEvent e) 179 { 180 System.out.println("PLAY Action: "+e.getActionCommand()); 181 if (e.getActionCommand().equals("Play Last Sample")) 182 { 183 playButton.setText("STOP Playing"); 184 startPlaying(); 185 } 186 else 187 { 188 stopPlaying(); 189 playButton.setText("Play Last Sample"); 190 } 191 } 192 } 193 ); 194 195 content.add(playButton, BorderLayout.SOUTH); 196 setVisible(true); 197 } 198 199 200 // Create a new file 201 File getNewFile() 202 { 203 File file = null; 204 try 205 { 206 do 207 { 208 soundFileName = filename + (filenameSuffix++) + '.' + fileType.getExtension(); 209 file = new File(currentDir, soundFileName); 210 } while (!file.createNewFile()); 211 if (!file.isFile()) 212 { 213 System.out.println("File not created: " + file.getName()); 214 return null; 215 } 216 } 217 catch (IOException e) 218 { 219 System.out.println(e); 220 return null; 221 } 222 return file; 223 224 } 225 226 227 public void switchToPCM() 228 { 229 format = pcmHiQFormat; 230 pcmFormat = pcmHiQFormat; 231 useSpeex = false; 232 fileType = AudioFileFormat.Type.WAVE; 233 } 234 235 236 public void switchToSpeex() 237 { 238 format = speexFormat; 239 pcmFormat = pcmLowQFormat; 240 useSpeex = true; 241 fileType = org.xiph.speex.spi.SpeexFileFormatType.SPEEX; 242 } 243 244 245 /* start recording the default wav file. 246 */ 247 public void startRecording() 248 { 249 DataLine.Info info = new DataLine.Info(TargetDataLine.class, pcmFormat); // should always sample from PCM format. speex converts this later on 250 if (!AudioSystem.isLineSupported(info)) 251 { 252 System.out.println("Line not supported" + info); 253 record.setEnabled(false); 254 return; 255 } 256 try 257 { 258 mic = (TargetDataLine) AudioSystem.getLine(info); 259 mic.open(pcmFormat, mic.getBufferSize()); 260 } 261 catch (LineUnavailableException e) 262 { 263 System.out.println("Line not available" + e); 264 record.setEnabled(false); 265 return; 266 } 267 if (fileOut.length() > 0) 268 { 269 fileOut = getNewFile(); 270 filenameLabel.setText(fileOut.getName()); 271 } 272 filenameLabel.setForeground(recordColor); 273 filenameLabel.repaint(); 274 thread = new Thread(this); 275 System.out.println(" starting recording thread..."); 276 thread.start(); 277 278 } 279 280 281 public void stopRecording() 282 { 283 filenameLabel.setForeground(stopColor); 284 filenameLabel.repaint(); 285 mic.stop(); 286 mic.close(); 287 288 } 289 290 291 public void startPlaying() { startPlaying(soundFileName); } 292 public void startPlaying(String wavFileName) 293 { 294 System.out.println("STARTING Playing "+ wavFileName); 295 296 wavPlayer = new WaveAudioPlayer(wavFileName); 297 wavPlayer.start(); 298 PlayerWatchdogThread pt = new PlayerWatchdogThread(wavPlayer, playButton, "Play Last Sample"); 299 pt.start(); 300 301 /* 302 long sleepAmount = 250; 303 long sleepCount = 2*sleepAmount; 304 ca.bc.webarts.widgets.Util.sleep(2*sleepAmount); 305 while (sleepCount<32000 && wavPlayer.isPlaying() ) 306 { 307 ca.bc.webarts.widgets.Util.sleep(sleepAmount); 308 sleepCount+=sleepAmount; 309 System.out.print("."); 310 } 311 playButton.setText("Play Last Sample"); 312 */ 313 } 314 315 316 public void stopPlaying() 317 { 318 System.out.println("Requesting STOP Playing"); 319 wavPlayer.stopPlaying(); 320 } 321 322 323 public void run() 324 { 325 AudioInputStream sound = new AudioInputStream(mic); // Microphone stream 326 327 mic.start(); // Start input 328 try 329 { 330 Line.Info srcLineInfo = mic.getLineInfo(); 331 System.out.print(" Using Microphone :" + srcLineInfo.toString()); 332 System.out.println(" (" + srcLineInfo.getLineClass().getName() + ")"); 333 System.out.println(" Writing to file: " + fileOut); 334 if(useSpeex) 335 { 336 // https://vlad.d2dx.com/using-the-speex-codec-in-java/ 337 AudioInputStream speexStream = AudioSystem.getAudioInputStream(speexFormat, sound); // Convert the stream 338 AudioSystem.write(speexStream, fileType, fileOut); // Write input to file 339 } 340 else 341 AudioSystem.write(sound, fileType, fileOut); // Write input to file 342 343 System.out.println(" DONE!"); 344 } 345 catch (IOException e) 346 { 347 System.out.println(e); 348 } 349 catch (java.lang.IllegalArgumentException iaEx) 350 { 351 System.out.println(iaEx); 352 System.out.println("Supported conversions: "); 353 System.out.println(" "+java.util.Arrays.toString(AudioSystem.getTargetFormats(org.xiph.speex.spi.SpeexEncoding.SPEEX, pcmFormat))); 354 } 355 } 356} 357 358 359class PlayerWatchdogThread extends Thread 360{ 361 /** Class flag for the threadWatchdog method to enable the calling user 362 * to stop the watch. */ 363 public static boolean watchdogReset = false; 364 365 long timeToTerminate = 180000; 366 long timeWatched = 0; 367 int sleepTime = 250; 368 369 WaveAudioPlayer watchedPlayerThread = null; 370 JButton buttonToChange = null; 371 String buttonMessage = ""; 372 373 public PlayerWatchdogThread(WaveAudioPlayer waThread, JButton jb, String msg) 374 { 375 watchedPlayerThread=waThread; 376 buttonToChange = jb; 377 buttonMessage = msg; 378 } 379 380 public void run() 381 { 382 watchdogReset = false; 383 Util.sleep(500); // make sure the player gets a head start! 384 385 // now wait till it finishes 386 while (timeWatched < timeToTerminate && 387 watchedPlayerThread.isPlaying() && 388 !watchdogReset) 389 { 390 Util.sleep(sleepTime); 391 timeWatched += sleepTime; 392 } 393 394 // Now clean up 395 buttonToChange.setText(buttonMessage); 396 397 if (timeWatched >= timeToTerminate) 398 { 399 System.out.println("Player Time MAX'd Out."); 400 } 401 } 402} 403 404 405 406class WaveAudioPlayer extends Thread 407{ 408 409 private String filename; 410 private Position curPosition; 411 private final int EXTERNAL_BUFFER_SIZE = 128; //524288; // 128Kb 412 413 private static enum Position { LEFT, RIGHT, NORMAL }; 414 415 /** Cleanly Stop the Thread Playing **/ 416 private boolean stopPlaying = false; 417 private boolean isPlaying = false; 418 419 420 public WaveAudioPlayer(String wavfile) 421 { 422 filename = wavfile; 423 curPosition = Position.NORMAL; 424 } 425 426 427 public WaveAudioPlayer(String wavfile, Position p) 428 { 429 filename = wavfile; 430 curPosition = p; 431 } 432 433 synchronized void stopPlaying() { stopPlaying = true;} 434 synchronized void setPlaying() { stopPlaying = false;} 435 synchronized boolean isPlaying() { return isPlaying;} 436 synchronized boolean getIsPlaying() { return isPlaying;} 437 synchronized void setIsPlaying(boolean isPlay) { isPlaying = isPlay;} 438 synchronized void setIsPlaying() { setIsPlaying(true);} 439 440 public void run() 441 { 442 if (filename == null) return; 443 File soundFile = new File(filename); 444 if (!soundFile.exists()) 445 { 446 System.err.println("Wave file not found: " + filename); 447 return; 448 } 449 450 AudioInputStream audioInputStream = null; 451 try 452 { 453 audioInputStream = AudioSystem.getAudioInputStream(soundFile); 454 } 455 catch (UnsupportedAudioFileException e1) 456 { 457 e1.printStackTrace(); 458 return; 459 } 460 catch (IOException e1) 461 { 462 e1.printStackTrace(); 463 return; 464 } 465 466 AudioFormat format = audioInputStream.getFormat(); 467 SourceDataLine auline = null; 468 DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); 469 470 try 471 { 472 auline = (SourceDataLine) AudioSystem.getLine(info); 473 auline.open(format); 474 } 475 catch (LineUnavailableException e) 476 { 477 e.printStackTrace(); 478 return; 479 } 480 catch (Exception e) 481 { 482 e.printStackTrace(); 483 return; 484 } 485 486 if (auline.isControlSupported(FloatControl.Type.PAN)) 487 { 488 FloatControl pan = (FloatControl) auline.getControl(FloatControl.Type.PAN); 489 if (curPosition == Position.RIGHT) 490 { 491 pan.setValue(1.0f); 492 } 493 else if (curPosition == Position.LEFT) 494 { 495 pan.setValue(-1.0f); 496 } 497 } 498 499 auline.start(); 500 int nBytesRead = 0; 501 byte[] abData = new byte[EXTERNAL_BUFFER_SIZE]; 502 503 setPlaying(); 504 try 505 { 506 setIsPlaying() ; 507 while (!stopPlaying && nBytesRead != -1) 508 { 509 nBytesRead = audioInputStream.read(abData, 0, abData.length); 510 if (nBytesRead >= 0) 511 { 512 auline.write(abData, 0, nBytesRead); 513 } 514 } 515 setIsPlaying(false); 516 } 517 catch (IOException e) 518 { 519 e.printStackTrace(); 520 return; 521 } 522 finally 523 { 524 auline.drain(); 525 auline.close(); 526 stopPlaying(); 527 } 528 529 } 530} 531