package org.geogebra.common.sound; import org.geogebra.common.kernel.geos.GeoFunction; import org.geogebra.common.util.debug.Log; /** * Class for playing function-generated sounds. * * @author Laszlo Gal * */ public abstract class FunctionSound { protected static final int DEFAULT_SAMPLE_RATE = 8000; protected static final int DEFAULT_BIT_RATE = 8; private int bitDepth; private int sampleRate; // set maximum volume to 100% of external volume setting private int maxVolume = 100; // sound function fields private volatile GeoFunction f; private double min; private double max; private double t; // records current time, used with pause/resume /** * Constructs instance of FunctionSound * * @throws Exception */ public FunctionSound() { bitDepth = DEFAULT_BIT_RATE; sampleRate = DEFAULT_SAMPLE_RATE; } /** * Initializes instances of AudioFormat and SourceDataLine * * @param sampleRate * = 8000, 16000, 11025, 16000, 22050, or 44100 * @param bitDepth * = 8 or 16 * @return */ protected boolean initStreamingAudio(int sampleRate, int bitDepth) { if (sampleRate != 8000 && sampleRate != 16000 && sampleRate != 11025 && sampleRate != 22050 && sampleRate != 44100) { return false; } if (bitDepth != 8 && bitDepth != 16) { return false; } this.sampleRate = sampleRate; this.bitDepth = bitDepth; return true; } /** * 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 */ public void playFunction(GeoFunction geoFunction, double min, double max) { playFunction(geoFunction, min, max, DEFAULT_SAMPLE_RATE, DEFAULT_BIT_RATE); } /** * 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 */ public abstract void playFunction(final GeoFunction geoFunction, final double min, final double max, final int sampleRate, final int bitDepth); public boolean checkFunction(final GeoFunction geoFunction, final double min, final double max, final int sampleRate, final int bitDepth) { f = geoFunction; this.min = min; this.max = max; if ((sampleRate != DEFAULT_SAMPLE_RATE || bitDepth != DEFAULT_BIT_RATE) && !initStreamingAudio(sampleRate, bitDepth)) { return false; } return true; } /** * Pauses/resumes sound generation * * @param doPause */ public abstract void pause(boolean doPause); public int getBitDepth() { return bitDepth; } public void setBitDepth(int bitDepth) { this.bitDepth = bitDepth; } public int getSampleRate() { return sampleRate; } public void setSampleRate(int sampleRate) { this.sampleRate = sampleRate; } private double samplePeriod; private byte[] buf; /** * Fills the internal buffer with sound data generated by time-valued * GeoFunction f(t) starting at time t. Uses 8-bit mono samples. * * @param t */ protected void loadBuffer8(double t) { double value; for (int k = 0; k < getBuf().length; k++) { value = getF().value(t + 1.0 * k * getSamplePeriod()); // clip sound data if (value > 1.0) { value = 1.0; } if (value < -1.0) { value = -1.0; } value = value * getMaxVolume(); // make sure rounding works when truncated to short/byte if (value > 0) { value += 0.5; } else if (value < 0) { value -= 0.5; } getBuf()[k] = (byte) (value); } } /** * Fills the internal buffer with sound data generated by time-valued * GeoFunction f(t) starting at time t. Uses 16-bit mono, signed, big-endian * samples. * * @param t */ protected void loadBuffer16(double t) { double value; // Log.debug((byte)(-10.7)); // System.out.print("\nstart: "); for (int k = 0; k < getBuf().length / 2; k++) { if (k < 5 || k > getBuf().length / 2 - 6) { Log.debug(k + " " + (t + 1.0 * k * getSamplePeriod())); } value = getF().value(t + 1.0 * k * getSamplePeriod()); // System.out.print(value+","); // clip sound data if (value > 1.0) { value = 1.0; } if (value < -1.0) { value = -1.0; } value = value * getMaxVolume(); // make sure rounding works when truncated to short/byte if (value > 0) { value += 0.5; } else if (value < 0) { value -= 0.5; } short sample = (short) (value); getBuf()[2 * k] = (byte) (sample & 0xff); getBuf()[2 * k + 1] = (byte) ((sample >> 8) & 0xff); } } /** * Shapes ends of waveform to fade sound data TODO: is this actually * working? * * @param peakValue * @param isFadeOut */ protected byte[] getFadeBuffer(short peakValue, boolean isFadeOut) { int numSamples = getSampleRate() / 100; byte[] fadeBuf = new byte[getBitDepth() == 8 ? numSamples : 2 * numSamples]; double delta = 1.0 * peakValue / numSamples; if (isFadeOut) { delta = -delta; } short value = isFadeOut ? peakValue : 0; // System.out.println("peak: " + peakValue); // System.out.println("delta: " + delta); for (int k = 0; k < numSamples; k++) { if (getBitDepth() == 8) { fadeBuf[k] = (byte) (value); } else { fadeBuf[2 * k] = (byte) (value & 0xff); fadeBuf[2 * k + 1] = (byte) ((value >> 8) & 0xff); } value += delta; // System.out.println(value); } return fadeBuf; } public GeoFunction getF() { return f; } public void setF(GeoFunction f) { this.f = f; } public double getMin() { return min; } public void setMin(double min) { this.min = min; } public double getMax() { return max; } public void setMax(double max) { this.max = max; } public double getT() { return t; } public void setT(double t) { this.t = t; } public double getSamplePeriod() { return samplePeriod; } public void setSamplePeriod(double samplePeriod) { this.samplePeriod = samplePeriod; } public byte[] getBuf() { return buf; } public void setBuf(byte[] buf) { this.buf = buf; } public int getBufLength() { return buf.length; } public int getMaxVolume() { return maxVolume; } public void setMaxVolume(int maxVolume) { this.maxVolume = maxVolume; } }