package org.geogebra.desktop.sound; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.LineEvent; import javax.sound.sampled.LineListener; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; import org.geogebra.common.kernel.geos.GeoFunction; import org.geogebra.common.sound.FunctionSound; import org.geogebra.common.util.debug.Log; /** * Class for playing function-generated sounds. * * @author G. Sturr * */ public final class FunctionSoundD extends FunctionSound implements LineListener { // threaded class to play function private SoundThread soundThread; // streaming audio fields private AudioFormat af; private SourceDataLine sdl; /** * Constructs instance of FunctionSound * * @throws Exception */ public FunctionSoundD() throws Exception { super(); if (!initStreamingAudio(getSampleRate(), getBitDepth())) { throw new Exception("Cannot initialize streaming audio"); } } /** * Initializes instances of AudioFormat and SourceDataLine * * @param sampleRate * = 8000, 16000, 11025, 16000, 22050, or 44100 * @param bitDepth * = 8 or 16 * @return */ @Override protected boolean initStreamingAudio(int sampleRate, int bitDepth) { if (!super.initStreamingAudio(sampleRate, bitDepth)) { return false; } boolean success = true; af = new AudioFormat(sampleRate, bitDepth, 1, true, true); try { sdl = AudioSystem.getSourceDataLine(af); // add listener when debugging // sdl.addLineListener(this); } catch (LineUnavailableException e) { e.printStackTrace(); success = false; } return success; } /** * Plays a sound generated by the time valued GeoFunction f(t), from t = min * to t = max in seconds. The function is assumed to have range [-1,1] and * will be clipped to this range otherwise. * * @param geoFunction * @param min * @param max * @param sampleRate * @param bitDepth */ @Override public void playFunction(final GeoFunction geoFunction, final double min, final double max, final int sampleRate, final int bitDepth) { if (!checkFunction(geoFunction, min, max, sampleRate, bitDepth)) { return; } // close current sound thread and prepare sdl if (soundThread != null) { soundThread.interrupt(); sdl.flush(); sdl.close(); } // spawn a new SoundThread to play the function sound soundThread = new SoundThread(); soundThread.start(); } /** * Pauses/resumes sound generation * * @param doPause */ @Override public void pause(boolean doPause) { if (doPause) { setMin(getT()); soundThread.stopSound(); } else { playFunction(getF(), getMin(), getMax(), getSampleRate(), getBitDepth()); } } /** * Listener for line events, used for debugging. */ @Override public void update(LineEvent le) { LineEvent.Type type = le.getType(); if (type == LineEvent.Type.OPEN) { Log.debug("OPEN"); } else if (type == LineEvent.Type.CLOSE) { Log.debug("CLOSE"); } else if (type == LineEvent.Type.START) { Log.debug("START"); } else if (type == LineEvent.Type.STOP) { Log.debug("STOP"); } } /********************************************************** * Class SoundThread * * Plays sounds from time-valued functions. *********************************************************/ private class SoundThread extends Thread { private volatile boolean stopped = false; protected SoundThread() { } @Override public void run() { generateFunctionSound(); } private void generateFunctionSound() { stopped = false; // time between samples setSamplePeriod(1.0 / getSampleRate()); // create internal buffer for mathematically generated sound data // a small buffer minimizes latency when the function changes // dynamically // TODO: find optimal buffer size int frameSetSize = getSampleRate() / 50; // 20ms ok? if (getBitDepth() == 8) { setBuf(new byte[frameSetSize]); } else { setBuf(new byte[2 * frameSetSize]); } // generate the function sound try { // open the sourceDataLine // TODO: the sdl buffer size is relative to our internal buffer // need to experiment for best sizing factor sdl.open(af, 10 * getBufLength()); sdl.start(); if (getBitDepth() == 16) { setT(getMin()); loadBuffer16(getT()); doFade(getBuf()[0], false); sdl.write(getBuf(), 0, getBufLength()); do { setT(getT() + getSamplePeriod() * frameSetSize); loadBuffer16(getT()); sdl.write(getBuf(), 0, getBufLength()); } while (getT() < getMax() && !stopped); doFade(getBuf()[getBufLength() - 1], true); } else { // use 8-bit samples setT(getMin()); loadBuffer8(getT()); doFade(getBuf()[0], false); sdl.write(getBuf(), 0, getBufLength()); do { setT(getT() + getSamplePeriod() * frameSetSize); loadBuffer8(getT()); sdl.write(getBuf(), 0, getBufLength()); } while (getT() < getMax() && !stopped); if (!stopped) { doFade(getBuf()[getBufLength() - 1], true); } } // finish transfer of bytes from internal buffer to the sdl // buffer sdl.drain(); // stop and close the sourceDataLine sdl.stop(); sdl.close(); } catch (LineUnavailableException e) { e.printStackTrace(); } } /** * Shapes ends of waveform to fade sound data TODO: is this actually * working? * * @param peakValue * @param isFadeOut */ private void doFade(short peakValue, boolean isFadeOut) { byte[] fadeBuf = getFadeBuffer(peakValue, isFadeOut); sdl.write(fadeBuf, 0, fadeBuf.length); } /** * Stops function sound */ public void stopSound() { stopped = true; } } // ================================ // END SoundThread class }