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