package phunky;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import org.osgi.framework.BundleContext;
/**
* SampleStream reads in 3 samples for each of the X, Y and Z axes
* at startup. It then chooses between them at runtime based on
* the accelerometer input given to it via its update() method.
* The accelerometer readings determine which sample will be used
* for each axis and the chosen three are then mixed and returned
* as the sample value for the SampleStream itself.
*
* @author finsprings
*
*/
public class SampleStream extends InputStream {
private int byteCount = 0;
// streams for the samples we'll mix together based on accelerometer input
private final InputStream x1;
private final InputStream x2;
private final InputStream x3;
private final InputStream y1;
private final InputStream y2;
private final InputStream y3;
private final InputStream z1;
private final InputStream z2;
private final InputStream z3;
private InputStream xIn;
private InputStream yIn;
private InputStream zIn;
// most recent accelerometer readings to choose our output from
private float x = 0.0F;
private float y = 0.0F;
private float z = 0.0F;
// thresholds for deciding which sample to choose,
// calculated with audio module in slot 0 and
// BUGmotion in slot 1 (and nothing in slots 2 and 3)
// Values with BUGbase laying on a table were approximately:
// X=-0.28, Y=-0.14, Z=1.08
private final float xLowThreshold = -1.06F; // back raised ~45 deg
private final float xHighThreshold = 0.23F; // front raised ~45 deg
private final float yLowThreshold = -0.80F; // left raised ~45deg
private final float yHighThreshold = 0.53F; // right raised ~45deg
private final float zLowThreshold = -0.5F; // Z seems to track x and y ?
private final float zHighThreshold = 0.8F; // so I just looked at its variation
/// used so we know when to stop
private boolean tearDownRequested = false;
/**
* read in the sample data we'll play with
*
* @param bundleContext we need this to get to our sample resources
* @throws IOException may throw this trying to read in the samples
*/
public SampleStream(final BundleContext bundleContext) throws IOException {
System.out.println("Loading samples data");
x1 = loadSample(bundleContext, "claves.raw");
x2 = loadSample(bundleContext, "cowbell.raw");
x3 = loadSample(bundleContext, "floortom.raw");
y1 = loadSample(bundleContext, "highmidtom.raw");
y2 = loadSample(bundleContext, "vibra.raw");
y3 = loadSample(bundleContext, "conga.raw");
z1 = loadSample(bundleContext, "crashcymbal.raw");
z2 = loadSample(bundleContext, "guiro.raw");
z3 = loadSample(bundleContext, "snare.raw");
System.out.println("All samples loaded");
}
/**
* read in a sample from our resource bundle
*
* @param bundleContext we need this to get to the sample resource
* @param sampleFileName the name of the sample to read in
* @return
* @throws IOException may throw this trying to read in the samples
*/
private Sample loadSample(
final BundleContext bundleContext,
final String sampleFileName) throws IOException {
final URL url = bundleContext.getBundle().getResource(
"resources/" + sampleFileName);
return new Sample(url.openStream());
}
/**
* Our canned WAVE header. IAudioPlayer only accepts 44.1 16-bit stereo
* little-endian samples so our canned header tells it that's what we will
* send. We make the length as large as can be but fortunately IAudioPlayer
* doesn't pay any attention to it. If we didn't make it large then it
* would stop playing before we wanted it to (we want it to play forever,
* as long as this app is running).
*/
private static final char [] riffHeader = {
// RIFF Chunk
'R', 'I', 'F', 'F',
0xFF, 0xFF, 0xFF, 0xFF, // fake file length,
'W', 'A', 'V', 'E',
// FORMAT Chunk
'f', 'm', 't', ' ',
0x10, 0x00, 0x00, 0x00, // FORMAT length
0x01, 0x00, // always 0x0001
0x02, 0x00, // stereo
0x44, 0xAC, 0x00, 0x00, // sample rate (44.1kHz)
0x10, 0xB1, 0x02, 0x00, // bytes per second
0x04, 0x00, // bytes per sample (16-bit, stereo)
0x10, 0x00, // bits per sample
// DATA Chunk
'd', 'a', 't', 'a',
0xFF, 0xFF, 0xFF, 0xFF // fake sample count
// actual sample data would be here if we were a real wave file...
};
/**
* Pick an input stream for a given axis, based on that axis'
* current value and its thresholds.
*
* @param value the current reading for this axis
* @param lowThreshold this axis' low threshold
* @param highThreshold this axis' high threshold
* @param in1 stream to use if value is less than lowThreshold
* @param in2 stream to use if value is between lowThreshold and highThreshold
* @param in3 stream to use if value is higher than highThreshold
* @return the stream chosen to be used
*/
private InputStream chooseInput(
final float value,
final float lowThreshold,
final float highThreshold,
final InputStream in1,
final InputStream in2,
final InputStream in3) {
if (value < lowThreshold) {
return in1;
} else if (value < highThreshold) {
return in2;
} else {
return in3;
}
}
/**
* Accept accelerometer data to base our sample selection on.
*
* @param x X axis reading
* @param y Y axis reading
* @param z Z axis reading
*/
public void update(final float x, final float y, final float z) {
//System.out.println("X="+x+", Y="+y+", Z="+z);
this.x = x;
this.y = y;
this.z = z;
}
/**
* called when we need to stop sending audio
*/
public void stop() {
this.tearDownRequested = true;
}
/**
* Return our chosen mix of sample data. Note that
* we return the canned WAVE header above then move
* onto the sample data after that, which makes us
* appear to be a WAVE file to IAudioPlayer.
*/
public int read() throws IOException {
int sample = 0;
if (tearDownRequested) {
System.out.println("Returning -1 to make audio stream appear done");
sample = -1;
}
else if (byteCount < riffHeader.length) {
// return the next byte of our canned header
sample = riffHeader[byteCount];
}
else {
// return the most recently computed sample based on the update() data
// pick which samples to combine to create out output sample
// based on the accelerometer values we have been given
if ((byteCount % 4) == 0) {
// we return sample data 8 bits at a time, but the samples
// themselves are 16 bits each, and in stereo so there are
// two samples that go together, so we mustn't switch streams
// in the middle of a sample or we'll be out of sync (returning
// half of one sample and half of the next). We can use the same
// byteCount as we used for the header because the header has an
// even number of bytes
xIn = chooseInput(x, xLowThreshold, xHighThreshold, x1, x2, x3);
yIn = chooseInput(y, yLowThreshold, yHighThreshold, y1, y2, y3);
zIn = chooseInput(z, zLowThreshold, zHighThreshold, z1, z2, z3);
}
try {
sample = xIn.read() + yIn.read() + zIn.read();
}
catch (IOException e) {
// ignore it as it shouldn't happen with our InputStreams anyway
}
}
++byteCount;
return sample;
}
}