/*
* Copyright 2006, United States Government as represented by the Administrator
* for the National Aeronautics and Space Administration. No copyright is
* claimed in the United States under Title 17, U.S. Code. All Other Rights
* Reserved.
*
* class to handle "sonification" of numerical data Gets data and window spec from
* Solver Must not cache anything from Solver because there's no way to know if
* cached info is fresh. Sets up necessary javax.sound.sampled machinery. Starts a
* thread which continually replenishes a sound buffer. Contains methods to "render"
* a single slice of the data through a given point x
*/
package gov.nasa.ial.mde.sound;
import gov.nasa.ial.mde.math.MultiPointXY;
import gov.nasa.ial.mde.math.PointXY;
import gov.nasa.ial.mde.properties.MdeSettings;
import gov.nasa.ial.mde.solver.Solution;
import gov.nasa.ial.mde.solver.Solver;
import gov.nasa.ial.mde.solver.symbolic.AnalyzedData;
/**
* Class to generate the audio used to sonify data or equations in the form of
* AnalyzedItems in a Solver. Currently, we support a maximum of
* MAX_FREQUENCIES=10 simultaneous tones, and if the Solver happens to generate
* more than Sounder.MAX_FREQUENCIES simultaneous tones, the behavior is not
* defined.
*
* @author Dr. Robert Shelton
* @version 1.0
* @since 1.0
*/
public class Sounder extends MultiWavePlayer {
/**
* <code>Solver</code> contains information on all graphs to be sonified.
*/
private Solver solver;
/**
* <code>lastX</code> is used internally to track when the sweep crosses
* the X-axis
*/
private double lastX = Double.POSITIVE_INFINITY;
/**
* <code>lastNumNegative</code> is the number of traces which were below
* the X-axis at the previous value of x. We maintain this quantity in order
* to identify axis crossings which are indicated when the value of
* <code>lastNumNegative</code> changes.
*/
private int lastNumNegative = -1;
/**
* <code>settings</code> encapsulates user settings for the application,
* including choices for how the graph should be sonified.
*/
private MdeSettings settings;
/**
* <code>MAX_FREQUENCIES</code> is the maximum number of tones which can
* be generated simultaneously by this <code>Sounder</code>. largest
* number of y's for a given x
*/
private final static int MAX_FREQUENCIES = 10;
private boolean usingHiss = true;
private boolean usingChirp = true;
private boolean usingAlteredWaveform = true;
private boolean usingDing = true;
private float alteredWaveformAmplitude = 0.2f;
private int whichWaveform = MultiWave.SINE;
private float dingFreq = 4000f; //Hz
private int dingTime = 150; // milliseconds
@SuppressWarnings("unused")
private Sounder() {
throw new RuntimeException("Default constructor not allowed.");
}
/**
* Instantiates a Sounder for a particular Solver and MDE configuration.
*
* @param solver
* The Solver containing the AnalyzedItems to be sonified
* @param settings
* Controls how the AnalyzedItems in the Solver will be rendered
*/
public Sounder(Solver solver, MdeSettings settings) {
super(Sounder.MAX_FREQUENCIES);
if (solver == null) {
throw new NullPointerException("Null solver.");
}
if (settings == null) {
throw new NullPointerException("Null settings.");
}
this.solver = solver;
this.settings = settings;
initLine();
} // end Sounder
/**
* Performs a sonification sweep for all sonifiable data represented in
* <code>this.solver</code>
*
* @param sweepTimeInSeconds
* is the desired duration of the sweep.
*/
public void sweep(double sweepTimeInSeconds) {
if (!isPlaying()) {
return;
}
double sweepTimeInMiliseconds = 1000.0 * sweepTimeInSeconds;
// makes dx just big enough to go 0 to one in the proper number of steps
// where we take one step in each latency interval
double dx = Sounder.LATENCY_IN_MILLISECONDS / sweepTimeInMiliseconds;
for (double x = 0.0; x <= 1.0; x += dx) {
render(x);
try {
Thread.sleep(Sounder.LATENCY_IN_MILLISECONDS);
} // end try
catch (InterruptedException ie) {
// ignore exception
}
} // end for x
} // end sweep
/**
* Updates all sonification settings including waveform and indicators for
* axis crossing and negative values.
*
* @param mdeSettings the sonification settings to be used.
*/
public void updateSettings(MdeSettings mdeSettings) {
if (mdeSettings == null) {
throw new NullPointerException("Null settings.");
}
this.settings = mdeSettings;
setWhichWaveform(settings.getSonificationWaveform());
int negativeYIndicator = settings.getNegativeYValuesIndicator();
int xAxisIndicator = settings.getXAxisIndicator();
int yAxisIndicator = settings.getYAxisIndicator();
setUsingHiss((negativeYIndicator & MdeSettings.HISS) != 0);
setUsingAlteredWaveform((negativeYIndicator & MdeSettings.TAMBOR_CHANGE) != 0);
setUsingChirp((xAxisIndicator & MdeSettings.CHIRP) != 0);
setUsingDing((yAxisIndicator & MdeSettings.DING) != 0);
setXAxisIndicatorDuration(settings.getXAxisIndicatorDuration());
setXAxisIndicatorFrequency(settings.getXAxisIndicatorFrequency());
setYAxisIndicatorDuration(settings.getYAxisIndicatorDuration());
setYAxisIndicatorFrequency(settings.getYAxisIndicatorFrequency());
}
/**
* Sonify the specified single point.
*
* @param x the x value
* @param y the y value
*/
public void render(double x, double y) {
boolean noiseOn = false;
int numNegative = 0;
int freqNumber = 0;
double left = solver.getLeft();
double right = solver.getRight();
double top = solver.getTop();
double bottom = solver.getBottom();
double midY = 0.5 * (top + bottom);
double invHalfHeight = 2.0 / (top - bottom);
// Set pan between -1 and 1, representing the spatial x position of the
// slice we will render, and only update the pan once.
double midX = 0.5 * (right + left);
double invHalfWidth = 2.0 / (right - left);
setPan((float) ((x - midX) * invHalfWidth));
if (usingDing) {
dingAtYAxis(x);
}
if (y < 0.0) {
numNegative++;
}
double octave = 2.0 * (y - midY) * invHalfHeight;
// Toss stuff more than two octaves off fundamental frequency.
if ((octave >= -2.0) && (octave <= 2.0)) {
// The basic conversion from octave = log_2(f) to Hz
float f = (float) (MultiWavePlayer.FUNDAMENTAL * Math.pow(2.0, octave));
// source.activate(freqNumber, f, (y >= 0.0) ? MultiWave.SINE : MultiWave.SAW);
source.activate(freqNumber, f, whichWaveform);
if (y < 0.0) {
if (usingAlteredWaveform)
source.activate(alteredWaveformAmplitude, ++freqNumber, f, MultiWave.SAW);
noiseOn = true;
} // end if
freqNumber++;
}
// Kill off unnecessary/unused frequencies
int na = source.getNumActive();
if (freqNumber < na) {
for (int i = freqNumber; i < na; i++) {
source.deactivate(i);
}
}
if (usingHiss) {
if (noiseOn) {
source.setNoise(0.15);
} else {
source.setNoise(0.0);
}
} // end if
else {
source.setNoise(0.0);
}
if (lastNumNegative >= 0) {
if (numNegative > lastNumNegative) {
if (usingChirp) {
source.chirpDown();
}
}
if (numNegative < lastNumNegative) {
if (usingChirp) {
source.chirpUp();
}
}
} // end if
lastNumNegative = numNegative;
}
/**
* Sonify the relative position [0,1] for all the solutions in the solver.
*
* @param position the relative position [0,1] in the array of points to render.
* Note that this assumes that the data points are sorted and equally spaced in x.
*/
public void render(double position) {
int numNegative = 0;
boolean noiseOn = false;
MultiPointXY point, modelPoint;
Solution solution;
AnalyzedData analyzedData;
PointXY realDataPoint;
int i, n;
float f;
double y, octave;
int freqNumber = 0;
double left = solver.getLeft();
double right = solver.getRight();
double top = solver.getTop();
double bottom = solver.getBottom();
double midY = 0.5 * (top + bottom);
double invHalfHeight = 2.0 / (top - bottom);
// For the given bounds and position, calculate the relative x-value.
double x = left + position * (right - left);
// Set pan between -1 and 1, representing the spatial x position of the
// slice we will render, and only update the pan once.
double midX = 0.5 * (right + left);
double invHalfWidth = 2.0 / (right - left);
// Set the pan value here only if we are not sonifiying anything Polar.
if (solver.getSonifyPolarCount() == 0) {
setPan((float) ((x - midX) * invHalfWidth));
if (usingDing) {
dingAtYAxis(x);
}
}
int numSolutions = solver.size();
for (int solutionIndex = 0; solutionIndex < numSolutions; solutionIndex++) {
solution = solver.get(solutionIndex);
// Just continue if this solution should not be sonified, or if
// there is no point.
if (!solution.isSonifyGraph() || ((point = solution.getPointNear(x)) == null)) {
continue;
}
// If the solution is polar then adjust the pan as needed.
if (solution.isPolar()) {
// Update the pan value if needed.
float p = (float) ((point.x - midX) * invHalfWidth);
if (usingDing) {
dingAtYAxis(point.x);
}
if (p != getPan().getValue()) {
setPan(p);
}
}
// The number of frequences needed to represent this solution.
n = point.yArray.length;
// Sonify the real data point based on the current position and
// sonify the corresponding real data model point based on the
// x-value of the real data point. Therefore sonification of the
// real data points is relative to how many are viewable for the
// current bounds.
if (settings.isDataPointsShown() && (freqNumber < MAX_FREQUENCIES) &&
(solution.getAnalyzedItem() instanceof AnalyzedData)) {
analyzedData = (AnalyzedData) solution.getAnalyzedItem();
realDataPoint = analyzedData.getRealDataPoint(x);
if (realDataPoint != null) {
// Based on the x-value for the real data, search for and
// use the model point that is close to the corresponding
// real data x-value.
modelPoint = solution.getPointNear(realDataPoint.x);
if (modelPoint != null) {
point = modelPoint;
n = point.yArray.length;
}
// Sonify the real data point if there is no corresponding
// model data point or it the real data point is not the
// same as the model data.
if ((n == 0) || (realDataPoint.y != point.yArray[0])) {
y = realDataPoint.y;
octave = 2.0 * (y - midY) * invHalfHeight;
if (y < 0.0) {
numNegative++;
}
// Toss stuff more than two octaves off fundamental
// frequency.
if ((octave >= -2.0) && (octave <= 2.0)) {
// The basic conversion from octave = log_2(f) to Hz
f = (float) (MultiWavePlayer.FUNDAMENTAL * Math
.pow(2.0, octave));
// source.activate(freqNumber, f, (y >= 0.0) ? MultiWave.SINE : MultiWave.SAW);
source.activate(freqNumber, f, whichWaveform);
if (y < 0.0) {
if (usingAlteredWaveform) {
source.activate(alteredWaveformAmplitude, ++freqNumber, f, MultiWave.SAW);
}
noiseOn = true;
} // end if
freqNumber++;
}
}
}
}
// Calculate the frequencies midY to top = one octave up;
// midY to bottom = one octave down
for (i = 0; (i < n) && (freqNumber < MAX_FREQUENCIES); i++) {
y = point.yArray[i];
if (y < 0.0) {
numNegative++;
}
octave = 2.0 * (y - midY) * invHalfHeight;
// Toss stuff more than two octaves off fundamental frequency.
if ((octave >= -2.0) && (octave <= 2.0)) {
// The basic conversion from octave = log_2(f) to Hz
f = (float) (MultiWavePlayer.FUNDAMENTAL * Math.pow(2.0, octave));
// source.activate(freqNumber, f, (y >= 0.0) ?
// MultiWave.SINE : MultiWave.SAW);
source.activate(freqNumber, f, whichWaveform);
if (y < 0.0) {
if (usingAlteredWaveform) {
source.activate(alteredWaveformAmplitude, ++freqNumber, f, MultiWave.SAW);
}
noiseOn = true;
} // end if
freqNumber++;
}
} // end for i
}
// Kill off unnecessary/unused frequencies
int na = source.getNumActive();
if (freqNumber < na) {
for (i = freqNumber; i < na; i++) {
source.deactivate(i);
}
}
if (usingHiss) {
if (noiseOn) {
source.setNoise(0.15);
} else {
source.setNoise(0.0);
}
} // end if
else {
source.setNoise(0.0);
}
if (lastNumNegative >= 0) {
if (numNegative > lastNumNegative) {
if (usingChirp) {
source.chirpDown();
}
}
if (numNegative < lastNumNegative) {
if (usingChirp) {
source.chirpUp();
}
}
} // end if
lastNumNegative = numNegative;
} // end render
/**
* Manages identification and time at which the sonification sweep crosses
* the Y-axis. Updates <code>lastX</code> and declares a crossing if (1)
* <code>x</code> is zero, or (2) the product of <code>x</code> and
* <code>lastX</code> is negative.
*
* @param x current value of the independent variable.
*/
private void dingAtYAxis(double x) {
if (!Double.isInfinite(lastX)) {
if (x == 0.0) {
source.ding(dingFreq, dingTime);
} else if (x * lastX < 0.0) {
source.ding(dingFreq, dingTime);
}
}
lastX = x;
}
/**
* Returns the state of altered (fuzzified) waveform sonification.
*
* @return True whenever sonification uses an altered (fuzzified) waveform
* to indicate negative y values.
*/
public boolean isUsingAlteredWaveform() {
return usingAlteredWaveform;
}
/**
* Enables altered (fuzzed) waveform sonification.
*
* @param usingAlteredWaveform The value indicating whether or not negative
* values of y are to be sonified with an altered (fuzzed) waveform.
*/
public void setUsingAlteredWaveform(boolean usingAlteredWaveform) {
this.usingAlteredWaveform = usingAlteredWaveform;
}
/**
* Returns the state of the X-axis crossing chirp flag.
*
* @return True whenever chirps are present to indicate X-axis crossings.
*/
public boolean isUsingChirp() {
return usingChirp;
}
/**
* Enables the X-axis crossings chirp flag.
*
* @param usingChirp Determines whether X-axis crossings will be indicated
* with chirps.
*/
public void setUsingChirp(boolean usingChirp) {
this.usingChirp = usingChirp;
}
/**
* Returns the state of the Ding flag.
*
* @return True whenever a ding will indicate the trace crossing the Y-axis.
*/
public boolean isUsingDing() {
return usingDing;
}
/**
* Enables the X-axis crosing chirp flag.
* @param usingDing Determines whether the trace crossing the X-axis is
* indicated with a ding.
*/
public void setUsingDing(boolean usingDing) {
this.usingDing = usingDing;
}
/**
* Returns the state of the Hiss flag.
*
* @return True whenever a hiss is used to indicate negative y values.
*/
public boolean isUsingHiss() {
return usingHiss;
}
/**
* Enables the hiss sonification for negative y values.
*
* @param usingHiss Determines whether negative y values will be indicated
* with a hiss.
*/
public void setUsingHiss(boolean usingHiss) {
this.usingHiss = usingHiss;
}
/**
* Determines which waveform is to be used for sonification.
*
* @return a define constant indicating the waveform indicating which
* waveform.
* @see gov.nasa.ial.mde.sound.MultiWave#SINE
* @see gov.nasa.ial.mde.sound.MultiWave#SAW
* @see gov.nasa.ial.mde.sound.MultiWave#TRIANGLE
* @see gov.nasa.ial.mde.sound.MultiWave#SQUARE
* @see gov.nasa.ial.mde.sound.MultiWave#VARIABLE
*/
public int getWhichWaveform() {
return whichWaveform;
}
/**
* Sets which waveform to use for sonification.
*
* @param whichWaveform a define constant indicating the waveform.
* @see gov.nasa.ial.mde.sound.MultiWave#SINE
* @see gov.nasa.ial.mde.sound.MultiWave#SAW
* @see gov.nasa.ial.mde.sound.MultiWave#TRIANGLE
* @see gov.nasa.ial.mde.sound.MultiWave#SQUARE
* @see gov.nasa.ial.mde.sound.MultiWave#VARIABLE
*/
public void setWhichWaveform(int whichWaveform) {
switch (whichWaveform) {
case MdeSettings.SINE:
this.whichWaveform = MultiWave.SINE;
return;
case MdeSettings.TRIANGLE:
this.whichWaveform = MultiWave.TRIANGLE;
return;
case MdeSettings.SAW:
this.whichWaveform = MultiWave.SAW;
return;
case MdeSettings.SQUARE:
this.whichWaveform = MultiWave.SQUARE;
return;
case MdeSettings.VARIABLE:
this.whichWaveform = MultiWave.VARIABLE;
return;
default:
throw new IllegalArgumentException("Unsupported value of whichWaveForm = " + whichWaveform);
} // end switch
}
/**
* Changes the duration of the sound which indicates an X-axis crossing.
*
* @param duration must be one of <code>MdeSettings.SHORT</code>,
* <code>MdeSettings.MEDIUM</code> or
* <code>MdeSettings.LONG</code>
* @see gov.nasa.ial.mde.properties.MdeSettings
*/
public void setXAxisIndicatorDuration(int duration) {
switch (duration) {
case MdeSettings.SHORT:
source.setChirpDuration(50); // milliseconds
return;
case MdeSettings.MEDIUM:
source.setChirpDuration(150); // milliseconds
return;
case MdeSettings.LONG:
source.setChirpDuration(300); // milliseconds
return;
default:
throw new IllegalArgumentException("Unsupported value of duration = " + duration);
} // end switch
}
/**
* Changes the pitch of the sound which indicates an X-axis crossing.
*
* @param frequency must be one of <code>MdeSettings.HIGH</code>,
* <code>MdeSettings.MEDIUM</code> or
* <code>MdeSettings.LOW</code>
* @see gov.nasa.ial.mde.properties.MdeSettings
*/
public void setXAxisIndicatorFrequency(int frequency) {
switch (frequency) {
case MdeSettings.LOW:
source.setChirpFrequency(1000f); // Hz
return;
case MdeSettings.MEDIUM:
source.setChirpFrequency(2000f); // Hz
return;
case MdeSettings.HIGH:
source.setChirpFrequency(4000f); // Hz
return;
default:
throw new IllegalArgumentException("Unsupported value of frequency = " + frequency);
} // end switch
}
/**
* Changes the duration of the sound which indicates a Y-axis crossing.
*
* @param duration must be one of <code>MdeSettings.SHORT</code>,
* <code>MdeSettings.MEDIUM</code> or
* <code>MdeSettings.LONG</code>
* @see gov.nasa.ial.mde.properties.MdeSettings
*/
public void setYAxisIndicatorDuration(int duration) {
switch (duration) {
case MdeSettings.SHORT:
dingTime = 50; // milliseconds
return;
case MdeSettings.MEDIUM:
dingTime = 150; // milliseconds
return;
case MdeSettings.LONG:
dingTime = 300; //Milliseconds
return;
default:
throw new IllegalArgumentException("Unsupported value of duration = " + duration);
} // end switch
}
/**
* Changes the pitch of the sound which indicates a Y-axis crossing.
*
* @param frequency must be one of <code>MdeSettings.HIGH</code>,
* <code>MdeSettings.MEDIUM</code> or
* <code>MdeSettings.LOW</code>
* @see gov.nasa.ial.mde.properties.MdeSettings
*/
public void setYAxisIndicatorFrequency(int frequency) {
switch (frequency) {
case MdeSettings.LOW:
dingFreq = 1000f; // Hz
return;
case MdeSettings.MEDIUM:
dingFreq = 2000f; // Hz
return;
case MdeSettings.HIGH:
dingFreq = 4000f; //Hz
return;
default:
throw new IllegalArgumentException("Unsupported value of frequency = " + frequency);
} // end switch
}
/**
* Can be used to determine the current level of "buzz" inserted to indicate
* negative Y-values.
*
* @return the alteredWaveformAmplitude.
*/
public float getAlteredWaveformAmplitude() {
return alteredWaveformAmplitude;
}
/**
* Used to modify the degree of "buzz" inserted to indicate negative
* Y-values.
*
* @param alteredWaveformAmplitude the alteredWaveformAmplitude to set.
*/
public void setAlteredWaveformAmplitude(float alteredWaveformAmplitude) {
this.alteredWaveformAmplitude = alteredWaveformAmplitude;
}
} // end class Sounder