/* * File : JavaSoundAudioBuffer.java * Created : 29-may-2002 16:24 * By : fbusquets * * JClic - Authoring and playing system for educational activities * * Copyright (C) 2000 - 2005 Francesc Busquets & Departament * d'Educacio de la Generalitat de Catalunya * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details (see the LICENSE file). */ package edu.xtec.jclic.media; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Timer; import java.util.TimerTask; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.Line.Info; import javax.sound.sampled.SourceDataLine; import javax.sound.sampled.TargetDataLine; /** * This class extends {@link AudioBuffer} using the javax.sound.sampled package. * @author Francesc Busquets (fbusquets@xtec.cat) * @version 13.09.10 */ public class JavaSoundAudioBuffer extends AudioBuffer{ /** * Default sample rate used in recordings */ public static final float RATE=11025.0F; /** * Default resolution used in recordings */ public static final int BITS=16; /** * Default number of channels used in recordings */ public static final int CHANNELS=1; /** * Default buffer size */ public static final int LINE_BUFFER=10000; /** * Default size for small buffer used in read loops */ public static final int STEP_BUFFER=5000; /** * Line used for recording audio */ private static TargetDataLine m_targetLine; /** * Line used for playing recorded audio */ private static SourceDataLine m_sourceLine; /** * Thread used to record audio. Static because only one instance can stay making * use of the sound hardware. */ private static RecordThread recordThread; /** * Thread used to play audio. Static because only one instance can stay making * use of the audio hardware. */ private static PlayThread playThread; /** * Timer used to stop the record thread when the maximum time is achieved */ private static Timer recordTimer; /** * <CODE>true</CODE> only when all {@link Line} members are build */ private static boolean initialized; /** * Array that holds the recorded audio data */ private byte[] m_buffer; /** * Creates new JavaSoundAudioBuffer * @param seconds Maximum amount of seconds allowed for recording * @throws Exception If something goes wrong... */ public JavaSoundAudioBuffer(int seconds) throws Exception{ super(seconds); //m_buffer=null; initialize(); } public static void initialize() throws Exception{ if(!initialized) buildLines(); } /** * Looks for available formats and lines, and builds <CODE>m_sourceLine</CODE> and <CODE>m_targetLine</CODE> members * @throws Exception If it was unable to build the lines */ protected static void buildLines() throws Exception{ // Get lists of all and optimal audio formats ArrayList<AudioFormat> vOptimal=new ArrayList<AudioFormat>(); ArrayList<AudioFormat> vAll=new ArrayList<AudioFormat>(); for(Info info : AudioSystem.getTargetLineInfo(new Info(TargetDataLine.class))){ if(info instanceof javax.sound.sampled.DataLine.Info){ for(AudioFormat format : ((javax.sound.sampled.DataLine.Info)info).getFormats()){ if(format.getSampleRate()==AudioSystem.NOT_SPECIFIED || format.getSampleRate()>=8000.0F) vAll.add(format); if(format.getChannels()==CHANNELS && format.getSampleSizeInBits()==BITS && (format.getSampleRate()==AudioSystem.NOT_SPECIFIED || format.getSampleRate()==RATE)) vOptimal.add(format); } } } if(vAll.isEmpty()){ throw new Exception("Unable to find any available TargetDataLine for recording"); } // Build m_targetLine javax.sound.sampled.DataLine.Info dli; AudioFormat[] af; // try with optimal formats if(!vOptimal.isEmpty()){ af=vOptimal.toArray(new AudioFormat[vOptimal.size()]); dli=new javax.sound.sampled.DataLine.Info(TargetDataLine.class, af, LINE_BUFFER, LINE_BUFFER+1000); try{ m_targetLine=(TargetDataLine)AudioSystem.getLine(dli); m_targetLine.open(); } catch(Exception ex){ m_targetLine=null; } } // optimal failed: try with all formats if(m_targetLine==null){ af=vAll.toArray(new AudioFormat[vAll.size()]); dli=new javax.sound.sampled.DataLine.Info(TargetDataLine.class, af, LINE_BUFFER, LINE_BUFFER+1000); m_targetLine=(TargetDataLine)AudioSystem.getLine(dli); m_targetLine.open(); } // build m_sourceLine dli=new javax.sound.sampled.DataLine.Info(SourceDataLine.class, m_targetLine.getFormat()); m_sourceLine=(SourceDataLine)AudioSystem.getLine(dli); m_sourceLine.open(); initialized=true; } /** * Used for recording data. The thread stops itself when <CODE>running</CODE> * is set to <CODE>false</CODE>. */ class RecordThread extends Thread { /** * When <CODE>false</CODE>, the thread must be stopped as soon as possible. */ public boolean running; /** * Creates new RecordThread */ public RecordThread(){ running=false; } /** * Thread main method */ @Override public void run(){ AudioBuffer.activeAudioBuffer=JavaSoundAudioBuffer.this; running=true; m_buffer=null; ByteArrayOutputStream out=new ByteArrayOutputStream(LINE_BUFFER); try{ int avail, numBytesRead; byte[] data=new byte[STEP_BUFFER]; m_targetLine.start(); do{ avail=m_targetLine.available(); if(avail>0){ numBytesRead=m_targetLine.read(data, 0, Math.min(avail, data.length)); out.write(data, 0, numBytesRead); } Thread.yield(); } while(running); if(recordTimer!=null){ recordTimer.cancel(); recordTimer=null; } } catch(Exception ex){ System.err.println("JavaSound recording error:\n"+ex); } m_targetLine.stop(); m_targetLine.flush(); //m_targetLine.close(); m_buffer=out.toByteArray(); AudioBuffer.hideRecordingCursor(); AudioBuffer.activeAudioBuffer=null; recordThread=null; running=false; } } /** * Used for playing data. The thread stops itself when <CODE>running</CODE> * is set to <CODE>false</CODE>, or when all audio data was played. */ class PlayThread extends Thread { /** * When <CODE>false</CODE>, the thread must be stopped as soon as possible. */ public boolean running; /** * In some cases, the data stored in <CODE>m_buffer</CODE> was corrupted after * calling to <CODE>m_sourceLine.write</CODE>. A workaround to this JavaSond * bug is to make a duplicate of the data and discard it after used. */ byte[] buf; /** * Creates new PlayThread */ public PlayThread(){ running=false; buf=new byte[m_buffer.length]; // make a copy of the audio data System.arraycopy(m_buffer, 0, buf, 0, m_buffer.length); } /** * Thread main method */ @Override public void run(){ running=true; try{ m_sourceLine.start(); int l=m_sourceLine.getBufferSize()/2; int p=0; int remainingData=buf.length; while(running && remainingData>0){ int k=m_sourceLine.write(buf, p, Math.min(l, remainingData)); p+=k; remainingData-=k; Thread.yield(); } } catch(Exception ex){ System.err.println("JavaSound playing error:\n"+ex); } m_sourceLine.drain(); m_sourceLine.stop(); //m_sourceLine.close(); playThread=null; running=false; } } /** * Starts recording * @throws Exception If something goes wrong. */ public void record() throws Exception{ if(!initialized){ AudioBuffer.hideRecordingCursor(); AudioBuffer.activeAudioBuffer=null; return; } stop(); recordThread=new RecordThread(); recordThread.start(); recordTimer=new Timer(); recordTimer.schedule(new TimerTask(){ public void run(){ recordTimer=null; if(recordThread!=null){ recordThread.running=false; } } }, m_seconds*1000); } /** * Plays recorded audio data, if any. * @throws Exception If something goes wrong */ public void play() throws Exception{ if(!initialized) return; stop(); if(m_buffer!=null && m_buffer.length>0){ playThread=new PlayThread(); playThread.start(); } } /** * Checks if the AudioBuffer is currently playing or recording sound. * @return <CODE>true</CODE> if playing or recording, <CODE>false</CODE> otherwise */ private boolean isRunning(){ return (playThread!=null || recordThread!=null); } /** * If running, gently stops play and record threads */ public void stop(){ if(recordThread!=null) recordThread.running=false; if(playThread!=null) playThread.running=false; while(recordThread!=null || playThread!=null){ Thread.yield(); } } /** * Stops playing or recording and clears all recorded data */ protected void clear(){ stop(); m_buffer=null; } }