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;
}
}