aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/com/jsyn/util
diff options
context:
space:
mode:
authorRubbaBoy <[email protected]>2020-07-06 02:33:28 -0400
committerPhil Burk <[email protected]>2020-10-30 11:19:34 -0700
commit46888fae6eb7b1dd386f7af7d101ead99ae61981 (patch)
tree8969bbfd68d2fb5c0d8b86da49ec2eca230a72ab /src/main/java/com/jsyn/util
parentc51e92e813dd481603de078f0778e1f75db2ab05 (diff)
Restructured project, added gradle, JUnit, logger, and more
Added Gradle (and removed ant), modernized testing via the JUnit framework, moved standalone examples from the tests directory to a separate module, removed sparsely used Java logger and replaced it with SLF4J. More work could be done, however this is a great start to greatly improving the health of the codebase.
Diffstat (limited to 'src/main/java/com/jsyn/util')
-rw-r--r--src/main/java/com/jsyn/util/AudioSampleLoader.java42
-rw-r--r--src/main/java/com/jsyn/util/AudioStreamReader.java85
-rw-r--r--src/main/java/com/jsyn/util/AutoCorrelator.java290
-rw-r--r--src/main/java/com/jsyn/util/Instrument.java38
-rw-r--r--src/main/java/com/jsyn/util/InstrumentLibrary.java32
-rw-r--r--src/main/java/com/jsyn/util/JavaSoundSampleLoader.java149
-rw-r--r--src/main/java/com/jsyn/util/JavaTools.java64
-rw-r--r--src/main/java/com/jsyn/util/MultiChannelSynthesizer.java404
-rw-r--r--src/main/java/com/jsyn/util/NumericOutput.java193
-rw-r--r--src/main/java/com/jsyn/util/PolyphonicInstrument.java155
-rw-r--r--src/main/java/com/jsyn/util/PseudoRandom.java89
-rw-r--r--src/main/java/com/jsyn/util/RecursiveSequenceGenerator.java214
-rw-r--r--src/main/java/com/jsyn/util/SampleLoader.java230
-rw-r--r--src/main/java/com/jsyn/util/SignalCorrelator.java48
-rw-r--r--src/main/java/com/jsyn/util/StreamingThread.java121
-rw-r--r--src/main/java/com/jsyn/util/TransportListener.java31
-rw-r--r--src/main/java/com/jsyn/util/TransportModel.java67
-rw-r--r--src/main/java/com/jsyn/util/VoiceAllocator.java258
-rw-r--r--src/main/java/com/jsyn/util/VoiceDescription.java68
-rw-r--r--src/main/java/com/jsyn/util/VoiceOperation.java7
-rw-r--r--src/main/java/com/jsyn/util/WaveFileWriter.java293
-rw-r--r--src/main/java/com/jsyn/util/WaveRecorder.java134
-rw-r--r--src/main/java/com/jsyn/util/soundfile/AIFFFileParser.java232
-rw-r--r--src/main/java/com/jsyn/util/soundfile/AudioFileParser.java129
-rw-r--r--src/main/java/com/jsyn/util/soundfile/ChunkHandler.java49
-rw-r--r--src/main/java/com/jsyn/util/soundfile/CustomSampleLoader.java60
-rw-r--r--src/main/java/com/jsyn/util/soundfile/IFFParser.java313
-rw-r--r--src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java338
28 files changed, 4133 insertions, 0 deletions
diff --git a/src/main/java/com/jsyn/util/AudioSampleLoader.java b/src/main/java/com/jsyn/util/AudioSampleLoader.java
new file mode 100644
index 0000000..b665933
--- /dev/null
+++ b/src/main/java/com/jsyn/util/AudioSampleLoader.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import com.jsyn.data.FloatSample;
+
+public interface AudioSampleLoader {
+ /**
+ * Load a FloatSample from a File object.
+ */
+ public FloatSample loadFloatSample(File fileIn) throws IOException;
+
+ /**
+ * Load a FloatSample from an InputStream. This is handy when loading Resources from a JAR file.
+ */
+ public FloatSample loadFloatSample(InputStream inputStream) throws IOException;
+
+ /**
+ * Load a FloatSample from a URL.. This is handy when loading Resources from a website.
+ */
+ public FloatSample loadFloatSample(URL url) throws IOException;
+
+}
diff --git a/src/main/java/com/jsyn/util/AudioStreamReader.java b/src/main/java/com/jsyn/util/AudioStreamReader.java
new file mode 100644
index 0000000..5a725c3
--- /dev/null
+++ b/src/main/java/com/jsyn/util/AudioStreamReader.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.io.AudioFifo;
+import com.jsyn.io.AudioInputStream;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.unitgen.MonoStreamWriter;
+import com.jsyn.unitgen.StereoStreamWriter;
+import com.jsyn.unitgen.UnitStreamWriter;
+
+/**
+ * Reads audio signals from the background engine to a foreground application through an AudioFifo.
+ * Connect to the input port returned by getInput().
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class AudioStreamReader implements AudioInputStream {
+ private UnitStreamWriter streamWriter;
+ private AudioFifo fifo;
+
+ public AudioStreamReader(Synthesizer synth, int samplesPerFrame) {
+ if (samplesPerFrame == 1) {
+ streamWriter = new MonoStreamWriter();
+ } else if (samplesPerFrame == 2) {
+ streamWriter = new StereoStreamWriter();
+ } else {
+ throw new IllegalArgumentException("Only 1 or 2 samplesPerFrame supported.");
+ }
+ synth.add(streamWriter);
+
+ fifo = new AudioFifo();
+ fifo.setWriteWaitEnabled(!synth.isRealTime());
+ fifo.setReadWaitEnabled(true);
+ fifo.allocate(32 * 1024);
+ streamWriter.setOutputStream(fifo);
+ streamWriter.start();
+ }
+
+ public UnitInputPort getInput() {
+ return streamWriter.input;
+ }
+
+ /** How many values are available to read without blocking? */
+ @Override
+ public int available() {
+ return fifo.available();
+ }
+
+ @Override
+ public void close() {
+ fifo.close();
+ }
+
+ @Override
+ public double read() {
+ return fifo.read();
+ }
+
+ @Override
+ public int read(double[] buffer) {
+ return fifo.read(buffer);
+ }
+
+ @Override
+ public int read(double[] buffer, int start, int count) {
+ return fifo.read(buffer, start, count);
+ }
+
+}
diff --git a/src/main/java/com/jsyn/util/AutoCorrelator.java b/src/main/java/com/jsyn/util/AutoCorrelator.java
new file mode 100644
index 0000000..944d515
--- /dev/null
+++ b/src/main/java/com/jsyn/util/AutoCorrelator.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright 2004 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+/**
+ * Calculate period of a repeated waveform in an array. This algorithm is based on a normalized
+ * auto-correlation function as dewscribed in: "A Smarter Way to Find Pitch" by Philip McLeod and
+ * Geoff Wyvill
+ *
+ * @author (C) 2004 Mobileer, PROPRIETARY and CONFIDENTIAL
+ */
+public class AutoCorrelator implements SignalCorrelator {
+ // A higher number will reject suboctaves more.
+ private static final float SUB_OCTAVE_REJECTION_FACTOR = 0.0005f;
+ // We can focus our analysis on the maxima
+ private static final int STATE_SEEKING_NEGATIVE = 0;
+ private static final int STATE_SEEKING_POSITIVE = 1;
+ private static final int STATE_SEEKING_MAXIMUM = 2;
+ private static final int[] tauAdvanceByState = {
+ 4, 2, 1
+ };
+ private int state;
+
+ private float[] buffer;
+ // double buffer the diffs so we can view them
+ private float[] diffs;
+ private float[] diffs1;
+ private float[] diffs2;
+ private int cursor = -1;
+ private int tau;
+
+ private float sumProducts;
+ private float sumSquares;
+ private float localMaximum;
+ private int localPosition;
+ private float bestMaximum;
+ private int bestPosition;
+ private int peakCounter;
+ // This factor was found empirically to reduce a systematic offset in the pitch.
+ private float pitchCorrectionFactor = 0.99988f;
+
+ // Results of analysis.
+ private double period;
+ private double confidence;
+ private int minPeriod = 2;
+ private boolean bufferValid;
+ private double previousSample = 0.0;
+ private int maxWindowSize;
+ private float noiseThreshold = 0.001f;
+
+ public AutoCorrelator(int numFrames) {
+ buffer = new float[numFrames];
+ maxWindowSize = buffer.length / 2;
+ diffs1 = new float[2 + numFrames / 2];
+ diffs2 = new float[diffs1.length];
+ diffs = diffs1;
+ period = minPeriod;
+ reset();
+ }
+
+ // Scan assuming we will not wrap around the buffer.
+ private void rawDeltaScan(int last1, int last2, int count, int stride) {
+ for (int k = 0; k < count; k += stride) {
+ float d1 = buffer[last1 - k];
+ float d2 = buffer[last2 - k];
+ sumProducts += d1 * d2;
+ sumSquares += ((d1 * d1) + (d2 * d2));
+ }
+ }
+
+ // Do correlation when we know the splitLast will wrap around.
+ private void splitDeltaScan(int last1, int splitLast, int count, int stride) {
+ rawDeltaScan(last1, splitLast, splitLast, stride);
+ rawDeltaScan(last1 - splitLast, buffer.length - 1, count - splitLast, stride);
+ }
+
+ private void checkDeltaScan(int last1, int last2, int count, int stride) {
+ if (count > last2) {
+ // Use recursion with reverse indexes to handle a double split.
+ checkDeltaScan(last2, last1, last2, stride);
+ checkDeltaScan(buffer.length - 1, last1 - last2, count - last2, stride);
+ } else if (count > last1) {
+ splitDeltaScan(last2, last1, count, stride);
+ } else {
+ rawDeltaScan(last1, last2, count, stride);
+ }
+ }
+
+ // Perform correlation. Handle circular buffer wrap around.
+ // Normalized square difference function between -1.0 and +1.0.
+ private float topScan(int last1, int tau, int count, int stride) {
+ final float minimumResult = 0.00000001f;
+
+ int last2 = last1 - tau;
+ if (last2 < 0) {
+ last2 += buffer.length;
+ }
+ sumProducts = 0.0f;
+ sumSquares = 0.0f;
+ checkDeltaScan(last1, last2, count, stride);
+ // Prevent divide by zero.
+ if (sumSquares < minimumResult) {
+ return minimumResult;
+ }
+ float correction = (float) Math.pow(pitchCorrectionFactor, tau);
+
+ return (float) (2.0 * sumProducts / sumSquares) * correction;
+ }
+
+ // Prepare for a new calculation.
+ private void reset() {
+ switchDiffs();
+ int i = 0;
+ for (; i < minPeriod; i++) {
+ diffs[i] = 1.0f;
+ }
+ for (; i < diffs.length; i++) {
+ diffs[i] = 0.0f;
+ }
+ tau = minPeriod;
+ state = STATE_SEEKING_NEGATIVE;
+ peakCounter = 0;
+ bestMaximum = -1.0f;
+ bestPosition = -1;
+ }
+
+ // Analyze new diff result. Incremental peak detection.
+ private void nextPeakAnalysis(int index) {
+ // Scale low frequency correlation down to reduce suboctave matching.
+ // Note that this has a side effect of reducing confidence value for low frequency sounds.
+ float value = diffs[index] * (1.0f - (index * SUB_OCTAVE_REJECTION_FACTOR));
+ switch (state) {
+ case STATE_SEEKING_NEGATIVE:
+ if (value < -0.01f) {
+ state = STATE_SEEKING_POSITIVE;
+ }
+ break;
+ case STATE_SEEKING_POSITIVE:
+ if (value > 0.2f) {
+ state = STATE_SEEKING_MAXIMUM;
+ localMaximum = value;
+ localPosition = index;
+ }
+ break;
+ case STATE_SEEKING_MAXIMUM:
+ if (value > localMaximum) {
+ localMaximum = value;
+ localPosition = index;
+ } else if (value < -0.1f) {
+ peakCounter += 1;
+ if (localMaximum > bestMaximum) {
+ bestMaximum = localMaximum;
+ bestPosition = localPosition;
+ }
+ state = STATE_SEEKING_POSITIVE;
+ }
+ break;
+ }
+ }
+
+ /**
+ * Generate interpolated maximum from index of absolute maximum using three point analysis.
+ */
+ private double findPreciseMaximum(int indexMax) {
+ if (indexMax < 3) {
+ return 3.0;
+ }
+ if (indexMax == (diffs.length - 1)) {
+ return indexMax;
+ }
+ // Get 3 adjacent values.
+ double d1 = diffs[indexMax - 1];
+ double d2 = diffs[indexMax];
+ double d3 = diffs[indexMax + 1];
+
+ return interpolatePeak(d1, d2, d3) + indexMax;
+ }
+
+ // Use quadratic fit to return offset between -0.5 and +0.5 from center.
+ protected static double interpolatePeak(double d1, double d2, double d3) {
+ return 0.5 * (d1 - d3) / (d1 - (2.0 * d2) + d3);
+ }
+
+ // Calculate a little more for each sample.
+ // This spreads the CPU load out more evenly.
+ private boolean incrementalAnalysis() {
+ boolean updated = false;
+ if (bufferValid) {
+ // int windowSize = maxWindowSize;
+ // Interpolate between tau and maxWindowsSize based on confidence.
+ // If confidence is low then use bigger window.
+ int windowSize = (int) ((tau * confidence) + (maxWindowSize * (1.0 - confidence)));
+
+ int stride = 1;
+ // int stride = (windowSize / 32) + 1;
+
+ diffs[tau] = topScan(cursor, tau, windowSize, stride);
+
+ // Check to see if the signal is strong enough to analyze.
+ // Look at sumPeriods on first correlation.
+ if ((tau == minPeriod) && (sumProducts < noiseThreshold)) {
+ // Update if we are dropping to zero confidence.
+ boolean result = (confidence > 0.0);
+ confidence = 0.0;
+ return result;
+ }
+
+ nextPeakAnalysis(tau);
+
+ // Reuse calculated values if we are not near a peak.
+ tau += 1;
+ int advance = tauAdvanceByState[state] - 1;
+ while ((advance > 0) && (tau < diffs.length)) {
+ diffs[tau] = diffs[tau - 1];
+ tau++;
+ advance--;
+ }
+
+ if ((peakCounter >= 4) || (tau >= maxWindowSize)) {
+ if (bestMaximum > 0.0) {
+ period = findPreciseMaximum(bestPosition);
+ // clip into range 0.0 to 1.0, low values are really bogus
+ confidence = (bestMaximum < 0.0) ? 0.0 : bestMaximum;
+ } else {
+ confidence = 0.0;
+ }
+ updated = true;
+ reset();
+ }
+ }
+ return updated;
+ }
+
+ @Override
+ public float[] getDiffs() {
+ // Return diffs that are not currently being used
+ return (diffs == diffs1) ? diffs2 : diffs1;
+ }
+
+ private void switchDiffs() {
+ diffs = (diffs == diffs1) ? diffs2 : diffs1;
+ }
+
+ @Override
+ public boolean addSample(double value) {
+ double average = (value + previousSample) * 0.5;
+ previousSample = value;
+
+ cursor += 1;
+ if (cursor == buffer.length) {
+ cursor = 0;
+ bufferValid = true;
+ }
+ buffer[cursor] = (float) average;
+
+ return incrementalAnalysis();
+ }
+
+ @Override
+ public double getPeriod() {
+ return period;
+ }
+
+ @Override
+ public double getConfidence() {
+ return confidence;
+ }
+
+ public float getPitchCorrectionFactor() {
+ return pitchCorrectionFactor;
+ }
+
+ public void setPitchCorrectionFactor(float pitchCorrectionFactor) {
+ this.pitchCorrectionFactor = pitchCorrectionFactor;
+ }
+}
diff --git a/src/main/java/com/jsyn/util/Instrument.java b/src/main/java/com/jsyn/util/Instrument.java
new file mode 100644
index 0000000..8a53304
--- /dev/null
+++ b/src/main/java/com/jsyn/util/Instrument.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * A note player that references one or more voices by a noteNumber. This is similar to the MIDI
+ * protocol that references voices by an integer pitch or keyIndex.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public interface Instrument {
+ // This will be applied to the voice when noteOn is called.
+ void usePreset(int presetIndex, TimeStamp timeStamp);
+
+ public void noteOn(int tag, double frequency, double amplitude, TimeStamp timeStamp);
+
+ public void noteOff(int tag, TimeStamp timeStamp);
+
+ public void setPort(int tag, String portName, double value, TimeStamp timeStamp);
+
+ public void allNotesOff(TimeStamp timeStamp);
+}
diff --git a/src/main/java/com/jsyn/util/InstrumentLibrary.java b/src/main/java/com/jsyn/util/InstrumentLibrary.java
new file mode 100644
index 0000000..65113dc
--- /dev/null
+++ b/src/main/java/com/jsyn/util/InstrumentLibrary.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import com.jsyn.swing.InstrumentBrowser;
+
+/**
+ * A library of instruments that can be used to play notes.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ * @see InstrumentBrowser
+ */
+
+public interface InstrumentLibrary {
+ public String getName();
+
+ public VoiceDescription[] getVoiceDescriptions();
+}
diff --git a/src/main/java/com/jsyn/util/JavaSoundSampleLoader.java b/src/main/java/com/jsyn/util/JavaSoundSampleLoader.java
new file mode 100644
index 0000000..56a654e
--- /dev/null
+++ b/src/main/java/com/jsyn/util/JavaSoundSampleLoader.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.UnsupportedAudioFileException;
+
+import com.jsyn.data.FloatSample;
+
+/**
+ * Internal class for loading audio samples. Use SampleLoader instead.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+class JavaSoundSampleLoader implements AudioSampleLoader {
+ /**
+ * Load a FloatSample from a File object.
+ */
+ @Override
+ public FloatSample loadFloatSample(File fileIn) throws IOException {
+ try {
+ return loadFloatSample(AudioSystem.getAudioInputStream(fileIn));
+ } catch (UnsupportedAudioFileException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Load a FloatSample from an InputStream. This is handy when loading Resources from a JAR file.
+ */
+ @Override
+ public FloatSample loadFloatSample(InputStream inputStream) throws IOException {
+ try {
+ return loadFloatSample(AudioSystem.getAudioInputStream(inputStream));
+ } catch (UnsupportedAudioFileException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Load a FloatSample from a URL.. This is handy when loading Resources from a website.
+ */
+ @Override
+ public FloatSample loadFloatSample(URL url) throws IOException {
+ try {
+ return loadFloatSample(AudioSystem.getAudioInputStream(url));
+ } catch (UnsupportedAudioFileException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private FloatSample loadFloatSample(javax.sound.sampled.AudioInputStream audioInputStream)
+ throws IOException, UnsupportedAudioFileException {
+ float[] floatData = null;
+ FloatSample sample = null;
+ int bytesPerFrame = audioInputStream.getFormat().getFrameSize();
+ if (bytesPerFrame == AudioSystem.NOT_SPECIFIED) {
+ // some audio formats may have unspecified frame size
+ // in that case we may read any amount of bytes
+ bytesPerFrame = 1;
+ }
+ AudioFormat format = audioInputStream.getFormat();
+ if (format.getEncoding() == AudioFormat.Encoding.PCM_SIGNED) {
+ floatData = loadSignedPCM(audioInputStream);
+ }
+ sample = new FloatSample(floatData, format.getChannels());
+ sample.setFrameRate(format.getFrameRate());
+ return sample;
+ }
+
+ private float[] loadSignedPCM(AudioInputStream audioInputStream) throws IOException,
+ UnsupportedAudioFileException {
+ int totalSamplesRead = 0;
+ AudioFormat format = audioInputStream.getFormat();
+ int numFrames = (int) audioInputStream.getFrameLength();
+ int numSamples = format.getChannels() * numFrames;
+ float[] data = new float[numSamples];
+ final int bytesPerFrame = format.getFrameSize();
+ // Set an arbitrary buffer size of 1024 frames.
+ int numBytes = 1024 * bytesPerFrame;
+ byte[] audioBytes = new byte[numBytes];
+ int numBytesRead = 0;
+ int numFramesRead = 0;
+ // Try to read numBytes bytes from the file.
+ while ((numBytesRead = audioInputStream.read(audioBytes)) != -1) {
+ int bytesRemainder = numBytesRead % bytesPerFrame;
+ if (bytesRemainder != 0) {
+ // TODO Read until you get enough data.
+ throw new IOException("Read partial block of sample data!");
+ }
+
+ if (audioInputStream.getFormat().getSampleSizeInBits() == 16) {
+ if (format.isBigEndian()) {
+ SampleLoader.decodeBigI16ToF32(audioBytes, 0, numBytesRead, data,
+ totalSamplesRead);
+ } else {
+ SampleLoader.decodeLittleI16ToF32(audioBytes, 0, numBytesRead, data,
+ totalSamplesRead);
+ }
+ } else if (audioInputStream.getFormat().getSampleSizeInBits() == 24) {
+ if (format.isBigEndian()) {
+ SampleLoader.decodeBigI24ToF32(audioBytes, 0, numBytesRead, data,
+ totalSamplesRead);
+ } else {
+ SampleLoader.decodeLittleI24ToF32(audioBytes, 0, numBytesRead, data,
+ totalSamplesRead);
+ }
+ } else if (audioInputStream.getFormat().getSampleSizeInBits() == 32) {
+ if (format.isBigEndian()) {
+ SampleLoader.decodeBigI32ToF32(audioBytes, 0, numBytesRead, data,
+ totalSamplesRead);
+ } else {
+ SampleLoader.decodeLittleI32ToF32(audioBytes, 0, numBytesRead, data,
+ totalSamplesRead);
+ }
+ } else {
+ throw new UnsupportedAudioFileException(
+ "Only 16, 24 or 32 bit PCM samples supported.");
+ }
+
+ // Calculate the number of frames actually read.
+ numFramesRead = numBytesRead / bytesPerFrame;
+ totalSamplesRead += numFramesRead * format.getChannels();
+ }
+ return data;
+ }
+
+}
diff --git a/src/main/java/com/jsyn/util/JavaTools.java b/src/main/java/com/jsyn/util/JavaTools.java
new file mode 100644
index 0000000..570e4c4
--- /dev/null
+++ b/src/main/java/com/jsyn/util/JavaTools.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class JavaTools {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(JavaTools.class);
+
+ @SuppressWarnings("rawtypes")
+ public static Class loadClass(String className, boolean verbose) {
+ Class newClass = null;
+ try {
+ newClass = Class.forName(className);
+ } catch (Throwable e) {
+ if (verbose)
+ LOGGER.debug("Caught " + e);
+ }
+ if (newClass == null) {
+ try {
+ ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
+ newClass = Class.forName(className, true, systemLoader);
+ } catch (Throwable e) {
+ if (verbose)
+ LOGGER.debug("Caught " + e);
+ }
+ }
+ return newClass;
+ }
+
+ /**
+ * First try Class.forName(). If this fails, try Class.forName() using
+ * ClassLoader.getSystemClassLoader().
+ *
+ * @return Class or null
+ */
+ @SuppressWarnings("rawtypes")
+ public static Class loadClass(String className) {
+ /**
+ * First try Class.forName(). If this fails, try Class.forName() using
+ * ClassLoader.getSystemClassLoader().
+ *
+ * @return Class or null
+ */
+ return loadClass(className, true);
+ }
+
+}
diff --git a/src/main/java/com/jsyn/util/MultiChannelSynthesizer.java b/src/main/java/com/jsyn/util/MultiChannelSynthesizer.java
new file mode 100644
index 0000000..da7f6c7
--- /dev/null
+++ b/src/main/java/com/jsyn/util/MultiChannelSynthesizer.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright 2016 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.engine.SynthesisEngine;
+import com.jsyn.midi.MidiConstants;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.unitgen.ExponentialRamp;
+import com.jsyn.unitgen.LinearRamp;
+import com.jsyn.unitgen.Multiply;
+import com.jsyn.unitgen.Pan;
+import com.jsyn.unitgen.PowerOfTwo;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.TwoInDualOut;
+import com.jsyn.unitgen.UnitGenerator;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.unitgen.UnitVoice;
+import com.softsynth.math.AudioMath;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * General purpose synthesizer with "channels"
+ * that could be used to implement a MIDI synthesizer.
+ *
+ * Each channel has:
+ * <pre><code>
+ * lfo -&gt; pitchToLinear -&gt; [VOICES] -&gt; volume* -&gt; panner
+ * bend --/
+ * </code></pre>
+ *
+ * Note: this class is experimental and subject to change.
+ *
+ * @author Phil Burk (C) 2016 Mobileer Inc
+ */
+public class MultiChannelSynthesizer {
+ private Synthesizer synth;
+ private TwoInDualOut outputUnit;
+ private ChannelContext[] channels;
+ private final static int MAX_VELOCITY = 127;
+ private double mMasterAmplitude = 0.25;
+
+ private class ChannelGroupContext {
+ private VoiceDescription voiceDescription;
+ private UnitVoice[] voices;
+ private VoiceAllocator allocator;
+
+ ChannelGroupContext(int numVoices, VoiceDescription voiceDescription) {
+ this.voiceDescription = voiceDescription;
+
+ voices = new UnitVoice[numVoices];
+ for (int i = 0; i < numVoices; i++) {
+ UnitVoice voice = voiceDescription.createUnitVoice();
+ UnitGenerator ugen = voice.getUnitGenerator();
+ synth.add(ugen);
+ voices[i] = voice;
+
+ }
+ allocator = new VoiceAllocator(voices);
+ }
+ }
+
+ private class ChannelContext {
+ private UnitOscillator lfo;
+ private PowerOfTwo pitchToLinear;
+ private LinearRamp timbreRamp;
+ private LinearRamp pressureRamp;
+ private ExponentialRamp volumeRamp;
+ private Multiply volumeMultiplier;
+ private Pan panner;
+ private double vibratoRate = 5.0;
+ private double bendRangeOctaves = 2.0 / 12.0;
+ private int presetIndex;
+ private ChannelGroupContext groupContext;
+ VoiceOperation voiceOperation = new VoiceOperation() {
+ @Override
+ public void operate (UnitVoice voice) {
+ voice.usePreset(presetIndex);
+ connectVoice(voice);
+ }
+ };
+
+ void setup(ChannelGroupContext groupContext) {
+ this.groupContext = groupContext;
+ synth.add(pitchToLinear = new PowerOfTwo());
+ synth.add(lfo = new SineOscillator()); // TODO use a MorphingOscillator or switch
+ // between S&H etc.
+ // Use a ramp to smooth out the timbre changes.
+ // This helps reduce pops from changing filter cutoff too abruptly.
+ synth.add(timbreRamp = new LinearRamp());
+ timbreRamp.time.set(0.02);
+ synth.add(pressureRamp = new LinearRamp());
+ pressureRamp.time.set(0.02);
+ synth.add(volumeRamp = new ExponentialRamp());
+ volumeRamp.input.set(1.0);
+ volumeRamp.time.set(0.02);
+ synth.add(volumeMultiplier = new Multiply());
+ synth.add(panner = new Pan());
+
+ pitchToLinear.input.setValueAdded(true); // so we can sum pitch bend
+ lfo.output.connect(pitchToLinear.input);
+ lfo.amplitude.set(0.0);
+ lfo.frequency.set(vibratoRate);
+
+ volumeRamp.output.connect(volumeMultiplier.inputB);
+ volumeMultiplier.output.connect(panner.input);
+ panner.output.connect(0, outputUnit.inputA, 0); // Use MultiPassthrough
+ panner.output.connect(1, outputUnit.inputB, 0);
+ }
+
+ private void connectVoice(UnitVoice voice) {
+ UnitGenerator ugen = voice.getUnitGenerator();
+ // Hook up some channel controllers to standard ports on the voice.
+ UnitInputPort freqMod = (UnitInputPort) ugen
+ .getPortByName(UnitGenerator.PORT_NAME_FREQUENCY_SCALER);
+ if (freqMod != null) {
+ freqMod.disconnectAll();
+ pitchToLinear.output.connect(freqMod);
+ }
+ UnitInputPort timbrePort = (UnitInputPort) ugen
+ .getPortByName(UnitGenerator.PORT_NAME_TIMBRE);
+ if (timbrePort != null) {
+ timbrePort.disconnectAll();
+ timbreRamp.output.connect(timbrePort);
+ timbreRamp.input.setup(timbrePort);
+ }
+ UnitInputPort pressurePort = (UnitInputPort) ugen
+ .getPortByName(UnitGenerator.PORT_NAME_PRESSURE);
+ if (pressurePort != null) {
+ pressurePort.disconnectAll();
+ pressureRamp.output.connect(pressurePort);
+ pressureRamp.input.setup(pressurePort);
+ }
+ voice.getOutput().disconnectAll();
+ voice.getOutput().connect(volumeMultiplier.inputA); // mono mix all the voices
+ }
+
+ void programChange(int program) {
+ int programWrapped = program % groupContext.voiceDescription.getPresetCount();
+ String name = groupContext.voiceDescription.getPresetNames()[programWrapped];
+ //LOGGER.debug("Preset[" + program + "] = " + name);
+ presetIndex = programWrapped;
+ }
+
+ void noteOff(int noteNumber, double amplitude) {
+ groupContext.allocator.noteOff(noteNumber, synth.createTimeStamp());
+ }
+
+ void noteOff(int noteNumber, double amplitude, TimeStamp timeStamp) {
+ groupContext.allocator.noteOff(noteNumber, timeStamp);
+ }
+
+ void noteOn(int noteNumber, double amplitude) {
+ noteOn(noteNumber, amplitude, synth.createTimeStamp());
+ }
+
+ void noteOn(int noteNumber, double amplitude, TimeStamp timeStamp) {
+ double frequency = AudioMath.pitchToFrequency(noteNumber);
+ //LOGGER.debug("noteOn(noteNumber) -> " + frequency + " Hz");
+ groupContext.allocator.noteOn(noteNumber, frequency, amplitude, voiceOperation, timeStamp);
+ }
+
+ public void setPitchBend(double offset) {
+ pitchToLinear.input.set(bendRangeOctaves * offset);
+ }
+
+ public void setBendRange(double semitones) {
+ bendRangeOctaves = semitones / 12.0;
+ }
+
+ public void setVibratoDepth(double semitones) {
+ lfo.amplitude.set(semitones);
+ }
+
+ public void setVolume(double volume) {
+ double min = SynthesisEngine.DB96;
+ double max = 1.0;
+ double ratio = max / min;
+ double value = min * Math.pow(ratio, volume);
+ volumeRamp.input.set(value);
+ }
+
+ public void setPan(double pan) {
+ panner.pan.set(pan);
+ }
+
+ /*
+ * @param timbre normalized 0 to 1
+ */
+ public void setTimbre(double timbre) {
+ double min = timbreRamp.input.getMinimum();
+ double max = timbreRamp.input.getMaximum();
+ double value = min + (timbre * (max - min));
+ timbreRamp.input.set(value);
+ }
+
+ /*
+ * @param pressure normalized 0 to 1
+ */
+ public void setPressure(double pressure) {
+ double min = pressureRamp.input.getMinimum();
+ double max = pressureRamp.input.getMaximum();
+ double ratio = max / min;
+ double value = min * Math.pow(ratio, pressure);
+ pressureRamp.input.set(value);
+ }
+ }
+
+ /**
+ * Construct a synthesizer with a maximum of 16 channels like MIDI.
+ */
+ public MultiChannelSynthesizer() {
+ this(MidiConstants.MAX_CHANNELS);
+ }
+
+
+ public MultiChannelSynthesizer(int maxChannels) {
+ channels = new ChannelContext[maxChannels];
+ for (int i = 0; i < channels.length; i++) {
+ channels[i] = new ChannelContext();
+ }
+ }
+
+ /**
+ * Specify a VoiceDescription to use with multiple channels.
+ *
+ * @param synth
+ * @param startChannel channel index is zero based
+ * @param numChannels
+ * @param voicesPerChannel
+ * @param voiceDescription
+ */
+ public void setup(Synthesizer synth, int startChannel, int numChannels, int voicesPerChannel,
+ VoiceDescription voiceDescription) {
+ this.synth = synth;
+ if (outputUnit == null) {
+ synth.add(outputUnit = new TwoInDualOut());
+ }
+ ChannelGroupContext groupContext = new ChannelGroupContext(voicesPerChannel,
+ voiceDescription);
+ for (int i = 0; i < numChannels; i++) {
+ channels[startChannel + i].setup(groupContext);
+ }
+ }
+
+ public void programChange(int channel, int program) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.programChange(program);
+ }
+
+
+ /**
+ * Turn off a note.
+ * @param channel
+ * @param noteNumber
+ * @param velocity between 0 and 127, will be scaled by masterAmplitude
+ */
+ public void noteOff(int channel, int noteNumber, int velocity) {
+ double amplitude = velocity * (1.0 / MAX_VELOCITY);
+ noteOff(channel, noteNumber, amplitude);
+ }
+
+ /**
+ * Turn off a note.
+ * @param channel
+ * @param noteNumber
+ * @param amplitude between 0 and 1.0, will be scaled by masterAmplitude
+ */
+ public void noteOff(int channel, int noteNumber, double amplitude) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.noteOff(noteNumber, amplitude * mMasterAmplitude);
+ }
+
+ /**
+ * Turn off a note.
+ * @param channel
+ * @param noteNumber
+ * @param amplitude between 0 and 1.0, will be scaled by masterAmplitude
+ */
+ public void noteOff(int channel, int noteNumber, double amplitude, TimeStamp timeStamp) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.noteOff(noteNumber, amplitude * mMasterAmplitude, timeStamp);
+ }
+
+ /**
+ * Turn on a note.
+ * @param channel
+ * @param noteNumber
+ * @param velocity between 0 and 127, will be scaled by masterAmplitude
+ */
+ public void noteOn(int channel, int noteNumber, int velocity) {
+ double amplitude = velocity * (1.0 / MAX_VELOCITY);
+ noteOn(channel, noteNumber, amplitude);
+ }
+
+ /**
+ * Turn on a note.
+ * @param channel
+ * @param noteNumber
+ * @param amplitude between 0 and 1.0, will be scaled by masterAmplitude
+ */
+ public void noteOn(int channel, int noteNumber, double amplitude, TimeStamp timeStamp) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.noteOn(noteNumber, amplitude * mMasterAmplitude, timeStamp);
+ }
+
+ /**
+ * Turn on a note.
+ * @param channel
+ * @param noteNumber
+ * @param amplitude between 0 and 1.0, will be scaled by masterAmplitude
+ */
+ public void noteOn(int channel, int noteNumber, double amplitude) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.noteOn(noteNumber, amplitude * mMasterAmplitude);
+ }
+
+ /**
+ * Set a pitch offset that will be scaled by the range for the channel.
+ *
+ * @param channel
+ * @param offset ranges from -1.0 to +1.0
+ */
+ public void setPitchBend(int channel, double offset) {
+ //LOGGER.debug("setPitchBend[" + channel + "] = " + offset);
+ ChannelContext channelContext = channels[channel];
+ channelContext.setPitchBend(offset);
+ }
+
+ public void setBendRange(int channel, double semitones) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.setBendRange(semitones);
+ }
+
+ public void setPressure(int channel, double pressure) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.setPressure(pressure);
+ }
+
+ public void setVibratoDepth(int channel, double semitones) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.setVibratoDepth(semitones);
+ }
+
+ public void setTimbre(int channel, double timbre) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.setTimbre(timbre);
+ }
+
+ /**
+ * Set volume for entire channel.
+ *
+ * @param channel
+ * @param volume normalized between 0.0 and 1.0
+ */
+ public void setVolume(int channel, double volume) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.setVolume(volume);
+ }
+
+ /**
+ * Pan from left to right.
+ *
+ * @param channel
+ * @param pan ranges from -1.0 to +1.0
+ */
+ public void setPan(int channel, double pan) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.setPan(pan);
+ }
+
+ /**
+ * @return stereo output port
+ */
+ public UnitOutputPort getOutput() {
+ return outputUnit.output;
+ }
+
+ /**
+ * Set amplitude for a single voice when the velocity is 127.
+ * @param masterAmplitude
+ */
+ public void setMasterAmplitude(double masterAmplitude) {
+ mMasterAmplitude = masterAmplitude;
+ }
+ public double getMasterAmplitude() {
+ return mMasterAmplitude;
+ }
+}
diff --git a/src/main/java/com/jsyn/util/NumericOutput.java b/src/main/java/com/jsyn/util/NumericOutput.java
new file mode 100644
index 0000000..e30975f
--- /dev/null
+++ b/src/main/java/com/jsyn/util/NumericOutput.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 1999 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Formatted numeric output. Convert integers and floats to strings based on field widths and
+ * desired decimal places.
+ *
+ * @author Phil Burk (C) 1999 SoftSynth.com
+ */
+
+public class NumericOutput {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(NumericOutput.class);
+
+ static char digitToChar(int digit) {
+ if (digit > 9) {
+ return (char) ('A' + digit - 10);
+ } else {
+ return (char) ('0' + digit);
+ }
+ }
+
+ public static String integerToString(int n, int width, boolean leadingZeros) {
+ return integerToString(n, width, leadingZeros, 10);
+ }
+
+ public static String integerToString(int n, int width) {
+ return integerToString(n, width, false, 10);
+ }
+
+ public static String integerToString(int n, int width, boolean leadingZeros, int radix) {
+ if (width > 32)
+ width = 32;
+ StringBuffer buf = new StringBuffer();
+ long ln = n;
+ boolean ifNeg = false;
+ // only do sign if decimal
+ if (radix != 10) {
+ // LOGGER.debug("MASK before : ln = " + ln );
+ ln = ln & 0x00000000FFFFFFFFL;
+ // LOGGER.debug("MASK after : ln = " + ln );
+ } else if (ln < 0) {
+ ifNeg = true;
+ ln = -ln;
+ }
+ if (ln == 0) {
+ buf.append('0');
+ } else {
+ // LOGGER.debug(" ln = " + ln );
+ while (ln > 0) {
+ int rem = (int) (ln % radix);
+ buf.append(digitToChar(rem));
+ ln = ln / radix;
+ }
+ }
+ if (leadingZeros) {
+ int pl = width;
+ if (ifNeg)
+ pl -= 1;
+ for (int i = buf.length(); i < pl; i++)
+ buf.append('0');
+ }
+ if (ifNeg)
+ buf.append('-');
+ // leading spaces
+ for (int i = buf.length(); i < width; i++)
+ buf.append(' ');
+ // reverse buffer to put characters in correct order
+ buf.reverse();
+
+ return buf.toString();
+ }
+
+ /**
+ * Convert double to string.
+ *
+ * @param width = minimum width of formatted string
+ * @param places = number of digits displayed after decimal point
+ */
+ public static String doubleToString(double value, int width, int places) {
+ return doubleToString(value, width, places, false);
+ }
+
+ /**
+ * Convert double to string.
+ *
+ * @param width = minimum width of formatted string
+ * @param places = number of digits displayed after decimal point
+ */
+ public static String doubleToString(double value, int width, int places, boolean leadingZeros) {
+ if (width > 32)
+ width = 32;
+ if (places > 16)
+ places = 16;
+
+ boolean ifNeg = false;
+ if (value < 0.0) {
+ ifNeg = true;
+ value = -value;
+ }
+ // round at relevant decimal place
+ value += 0.5 * Math.pow(10.0, 0 - places);
+ int ival = (int) Math.floor(value);
+ // get portion after decimal point as an integer
+ int fval = (int) ((value - Math.floor(value)) * Math.pow(10.0, places));
+ String result = "";
+
+ result += integerToString(ival, 0, false, 10);
+ result += ".";
+ result += integerToString(fval, places, true, 10);
+
+ if (leadingZeros) {
+ // prepend leading zeros and {-}
+ int zw = width;
+ if (ifNeg)
+ zw -= 1;
+ while (result.length() < zw)
+ result = "0" + result;
+ if (ifNeg)
+ result = "-" + result;
+ } else {
+ // prepend {-} and leading spaces
+ if (ifNeg)
+ result = "-" + result;
+ while (result.length() < width)
+ result = " " + result;
+ }
+ return result;
+ }
+
+ static void testInteger(int n) {
+ LOGGER.debug("Test " + n + ", 0x" + Integer.toHexString(n) + ", %"
+ + Integer.toBinaryString(n));
+ LOGGER.debug(" +,8,t,10 = " + integerToString(n, 8, true, 10));
+ LOGGER.debug(" +,8,f,10 = " + integerToString(n, 8, false, 10));
+ LOGGER.debug(" -,8,t,10 = " + integerToString(-n, 8, true, 10));
+ LOGGER.debug(" -,8,f,10 = " + integerToString(-n, 8, false, 10));
+ LOGGER.debug(" +,8,t,16 = " + integerToString(n, 8, true, 16));
+ LOGGER.debug(" +,8,f,16 = " + integerToString(n, 8, false, 16));
+ LOGGER.debug(" -,8,t,16 = " + integerToString(-n, 8, true, 16));
+ LOGGER.debug(" -,8,f,16 = " + integerToString(-n, 8, false, 16));
+ LOGGER.debug(" +,8,t, 2 = " + integerToString(n, 8, true, 2));
+ LOGGER.debug(" +,8,f, 2 = " + integerToString(n, 8, false, 2));
+ }
+
+ static void testDouble(double value) {
+ LOGGER.debug("Test " + value);
+ LOGGER.debug(" +,5,1 = " + doubleToString(value, 5, 1));
+ LOGGER.debug(" -,5,1 = " + doubleToString(-value, 5, 1));
+
+ LOGGER.debug(" +,14,3 = " + doubleToString(value, 14, 3));
+ LOGGER.debug(" -,14,3 = " + doubleToString(-value, 14, 3));
+
+ LOGGER.debug(" +,6,2,true = " + doubleToString(value, 6, 2, true));
+ LOGGER.debug(" -,6,2,true = " + doubleToString(-value, 6, 2, true));
+ }
+
+ public static void main(String argv[]) {
+ LOGGER.debug("Test NumericOutput");
+ testInteger(0);
+ testInteger(1);
+ testInteger(16);
+ testInteger(23456);
+ testInteger(0x23456);
+ testInteger(0x89ABC);
+ testDouble(0.0);
+ testDouble(0.0678);
+ testDouble(0.1234567);
+ testDouble(1.234567);
+ testDouble(12.34567);
+ testDouble(123.4567);
+ testDouble(1234.5678);
+
+ }
+}
diff --git a/src/main/java/com/jsyn/util/PolyphonicInstrument.java b/src/main/java/com/jsyn/util/PolyphonicInstrument.java
new file mode 100644
index 0000000..08460d0
--- /dev/null
+++ b/src/main/java/com/jsyn/util/PolyphonicInstrument.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.ports.UnitPort;
+import com.jsyn.unitgen.Circuit;
+import com.jsyn.unitgen.Multiply;
+import com.jsyn.unitgen.PassThrough;
+import com.jsyn.unitgen.UnitGenerator;
+import com.jsyn.unitgen.UnitSource;
+import com.jsyn.unitgen.UnitVoice;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * The API for this class is likely to change. Please comment on its usefulness.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+
+public class PolyphonicInstrument extends Circuit implements UnitSource, Instrument {
+ private Multiply mixer;
+ private UnitVoice[] voices;
+ private VoiceAllocator voiceAllocator;
+ public UnitInputPort amplitude;
+
+ public PolyphonicInstrument(UnitVoice[] voices) {
+ this.voices = voices;
+ voiceAllocator = new VoiceAllocator(voices);
+ add(mixer = new Multiply());
+ // Mix all the voices to one output.
+ for (UnitVoice voice : voices) {
+ UnitGenerator unit = voice.getUnitGenerator();
+ boolean wasEnabled = unit.isEnabled();
+ // This overrides the enabled property of the voice.
+ add(unit);
+ voice.getOutput().connect(mixer.inputA);
+ // restore
+ unit.setEnabled(wasEnabled);
+ }
+
+ addPort(amplitude = mixer.inputB, "Amplitude");
+ amplitude.setup(0.0001, 0.4, 2.0);
+ exportAllInputPorts();
+ }
+
+ /**
+ * Connect a PassThrough unit to the input ports of the voices so that they can be controlled
+ * together using a single port. Note that this will prevent their individual use. So the
+ * "Frequency" and "Amplitude" ports are excluded. Note that this method is a bit funky and is
+ * likely to change.
+ */
+ public void exportAllInputPorts() {
+ // Iterate through the ports.
+ for (UnitPort port : voices[0].getUnitGenerator().getPorts()) {
+ if (port instanceof UnitInputPort) {
+ UnitInputPort inputPort = (UnitInputPort) port;
+ String voicePortName = inputPort.getName();
+ // FIXME Need better way to identify ports that are per note.
+ if (!voicePortName.equals("Frequency") && !voicePortName.equals("Amplitude")) {
+ exportNamedInputPort(voicePortName);
+ }
+ }
+ }
+ }
+
+ /**
+ * Create a UnitInputPort for the circuit that is connected to the named port on each voice
+ * through a PassThrough unit. This allows you to control all of the voices at once.
+ *
+ * @param portName
+ * @see exportAllInputPorts
+ */
+ public void exportNamedInputPort(String portName) {
+ UnitInputPort voicePort = null;
+ PassThrough fanout = new PassThrough();
+ for (UnitVoice voice : voices) {
+ voicePort = (UnitInputPort) voice.getUnitGenerator().getPortByName(portName);
+ fanout.output.connect(voicePort);
+ }
+ if (voicePort != null) {
+ addPort(fanout.input, portName);
+ fanout.input.setup(voicePort);
+ }
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return mixer.output;
+ }
+
+ @Override
+ public void usePreset(int presetIndex) {
+ usePreset(presetIndex, getSynthesisEngine().createTimeStamp());
+ }
+
+ // FIXME - no timestamp on UnitVoice
+ @Override
+ public void usePreset(int presetIndex, TimeStamp timeStamp) {
+ // Apply preset to all voices.
+ for (UnitVoice voice : voices) {
+ voice.usePreset(presetIndex);
+ }
+ // Then copy values from first voice to instrument.
+ for (UnitPort port : voices[0].getUnitGenerator().getPorts()) {
+ if (port instanceof UnitInputPort) {
+ UnitInputPort inputPort = (UnitInputPort) port;
+ // FIXME Need better way to identify ports that are per note.
+ UnitInputPort fanPort = (UnitInputPort) getPortByName(inputPort.getName());
+ if ((fanPort != null) && (fanPort != amplitude)) {
+ fanPort.set(inputPort.get());
+ }
+ }
+ }
+ }
+
+ @Override
+ public void noteOn(int tag, double frequency, double amplitude, TimeStamp timeStamp) {
+ voiceAllocator.noteOn(tag, frequency, amplitude, timeStamp);
+ }
+
+ @Override
+ public void noteOff(int tag, TimeStamp timeStamp) {
+ voiceAllocator.noteOff(tag, timeStamp);
+ }
+
+ @Override
+ public void setPort(int tag, String portName, double value, TimeStamp timeStamp) {
+ voiceAllocator.setPort(tag, portName, value, timeStamp);
+ }
+
+ @Override
+ public void allNotesOff(TimeStamp timeStamp) {
+ voiceAllocator.allNotesOff(timeStamp);
+ }
+
+ public synchronized boolean isOn(int tag) {
+ return voiceAllocator.isOn(tag);
+ }
+}
diff --git a/src/main/java/com/jsyn/util/PseudoRandom.java b/src/main/java/com/jsyn/util/PseudoRandom.java
new file mode 100644
index 0000000..e92b669
--- /dev/null
+++ b/src/main/java/com/jsyn/util/PseudoRandom.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Sep 9, 2009
+ * com.jsyn.engine.units.SynthRandom.java
+ */
+
+package com.jsyn.util;
+
+import java.util.Random;
+
+/**
+ * Pseudo-random numbers using predictable and fast linear-congruential method.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc Translated from 'C' to Java by Lisa
+ * Tolentino.
+ */
+public class PseudoRandom {
+ // We must shift 1L or else we get a negative number!
+ private static final double INT_TO_DOUBLE = (1.0 / (1L << 31));
+ private long seed = 99887766;
+
+ /**
+ * Create an instance of SynthRandom.
+ */
+ public PseudoRandom() {
+ this(new Random().nextInt());
+ }
+
+ /**
+ * Create an instance of PseudoRandom.
+ */
+ public PseudoRandom(int seed) {
+ setSeed(seed);
+ }
+
+ public void setSeed(int seed) {
+ this.seed = (long) seed;
+ }
+
+ public int getSeed() {
+ return (int) seed;
+ }
+
+ /**
+ * Returns the next random double from 0.0 to 1.0
+ *
+ * @return value from 0.0 to 1.0
+ */
+ public double random() {
+ int positiveInt = nextRandomInteger() & 0x7FFFFFFF;
+ return positiveInt * INT_TO_DOUBLE;
+ }
+
+ /**
+ * Returns the next random double from -1.0 to 1.0
+ *
+ * @return value from -1.0 to 1.0
+ */
+ public double nextRandomDouble() {
+ return nextRandomInteger() * INT_TO_DOUBLE;
+ }
+
+ /** Calculate random 32 bit number using linear-congruential method. */
+ public int nextRandomInteger() {
+ // Use values for 64-bit sequence from MMIX by Donald Knuth.
+ seed = (seed * 6364136223846793005L) + 1442695040888963407L;
+ return (int) (seed >> 32); // The higher bits have a longer sequence.
+ }
+
+ public int choose(int range) {
+ long positiveInt = nextRandomInteger() & 0x7FFFFFFF;
+ long temp = positiveInt * range;
+ return (int) (temp >> 31);
+ }
+}
diff --git a/src/main/java/com/jsyn/util/RecursiveSequenceGenerator.java b/src/main/java/com/jsyn/util/RecursiveSequenceGenerator.java
new file mode 100644
index 0000000..0d6e451
--- /dev/null
+++ b/src/main/java/com/jsyn/util/RecursiveSequenceGenerator.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.util.Random;
+
+/**
+ * Generate a sequence of integers based on a recursive mining of previous material. Notes are
+ * generated by one of the following formula:
+ *
+ * <pre>
+ * <code>
+ * value[n] = value[n-delay] + offset;
+ * </code>
+ * </pre>
+ *
+ * The parameters delay and offset are randomly generated. This algorithm was first developed in
+ * 1977 for a class project in FORTRAN. It was ported to Forth for HMSL in the late 80's. It was
+ * then ported to Java for JSyn in 1997.
+ *
+ * @author Phil Burk (C) 1997,2011 Mobileer Inc
+ */
+public class RecursiveSequenceGenerator {
+ private int delay = 1;
+ private int maxValue;
+ private int maxInterval;
+ private double desiredDensity = 0.5;
+
+ private int offset;
+ private int values[];
+ private boolean enables[];
+ private int cursor;
+ private int countdown = -1;
+ private double actualDensity;
+ private int beatsPerMeasure = 8;
+ private Random random;
+
+ public RecursiveSequenceGenerator() {
+ this(25, 7, 64);
+ }
+
+ public RecursiveSequenceGenerator(int maxValue, int maxInterval, int arraySize) {
+ values = new int[arraySize];
+ enables = new boolean[arraySize];
+ this.maxValue = maxValue;
+ this.maxInterval = maxInterval;
+ for (int i = 0; i < values.length; i++) {
+ values[i] = maxValue / 2;
+ enables[i] = isNextEnabled(false);
+ }
+ }
+
+ /** Set density of notes. 0.0 to 1.0 */
+ public void setDensity(double density) {
+ desiredDensity = density;
+ }
+
+ public double getDensity() {
+ return desiredDensity;
+ }
+
+ /** Set maximum for generated value. */
+ public void setMaxValue(int maxValue) {
+ this.maxValue = maxValue;
+ }
+
+ public int getMaxValue() {
+ return maxValue;
+ }
+
+ /** Set maximum for generated value. */
+ public void setMaxInterval(int maxInterval) {
+ this.maxInterval = maxInterval;
+ }
+
+ public int getMaxInterval() {
+ return maxInterval;
+ }
+
+ /* Determine whether next in sequence should occur. */
+ public boolean isNextEnabled(boolean preferance) {
+ /* Calculate note density using low pass IIR filter. */
+ double newDensity = (actualDensity * 0.9) + (preferance ? 0.1 : 0.0);
+ /* Invert enable to push density towards desired level, with hysteresis. */
+ if (preferance && (newDensity > ((desiredDensity * 0.7) + 0.3)))
+ preferance = false;
+ else if (!preferance && (newDensity < (desiredDensity * 0.7)))
+ preferance = true;
+ actualDensity = (actualDensity * 0.9) + (preferance ? 0.1 : 0.0);
+ return preferance;
+ }
+
+ public int randomPowerOf2(int maxExp) {
+ return (1 << (int) (random.nextDouble() * (maxExp + 1)));
+ }
+
+ /** Random number evenly distributed from -maxInterval to +maxInterval */
+ public int randomEvenInterval() {
+ return (int) (random.nextDouble() * ((maxInterval * 2) + 1)) - maxInterval;
+ }
+
+ void calcNewOffset() {
+ offset = randomEvenInterval();
+ }
+
+ public void randomize() {
+
+ delay = randomPowerOf2(4);
+ calcNewOffset();
+ // LOGGER.debug("NewSeq: delay = " + delay + ", offset = " +
+ // offset );
+ }
+
+ /** Change parameters based on random countdown. */
+ public int next() {
+ // If this sequence is finished, start a new one.
+ if (countdown-- < 0) {
+ randomize();
+ countdown = randomPowerOf2(3);
+ }
+ return nextValue();
+ }
+
+ /** Change parameters using a probability based on beatIndex. */
+ public int next(int beatIndex) {
+ int beatMod = beatIndex % beatsPerMeasure;
+ switch (beatMod) {
+ case 0:
+ if (Math.random() < 0.90)
+ randomize();
+ break;
+ case 2:
+ case 6:
+ if (Math.random() < 0.15)
+ randomize();
+ break;
+ case 4:
+ if (Math.random() < 0.30)
+ randomize();
+ break;
+ default:
+ if (Math.random() < 0.07)
+ randomize();
+ break;
+ }
+ return nextValue();
+ }
+
+ /** Generate nextValue based on current delay and offset */
+ public int nextValue() {
+ // Generate index into circular value buffer.
+ int idx = (cursor - delay);
+ if (idx < 0)
+ idx += values.length;
+
+ // Generate new value. Calculate new offset if too high or low.
+ int nextVal = 0;
+ int timeout = 100;
+ while (timeout > 0) {
+ nextVal = values[idx] + offset;
+ if ((nextVal >= 0) && (nextVal < maxValue))
+ break;
+ // Prevent endless loops when maxValue changes.
+ if (nextVal > (maxValue + maxInterval - 1)) {
+ nextVal = maxValue;
+ break;
+ }
+ calcNewOffset();
+ timeout--;
+ // LOGGER.debug("NextVal = " + nextVal + ", offset = " +
+ // offset );
+ }
+ if (timeout <= 0) {
+ System.err.println("RecursiveSequence: nextValue timed out. offset = " + offset);
+ nextVal = maxValue / 2;
+ offset = 0;
+ }
+
+ // Save new value in circular buffer.
+ values[cursor] = nextVal;
+
+ boolean playIt = enables[cursor] = isNextEnabled(enables[idx]);
+ cursor++;
+ if (cursor >= values.length)
+ cursor = 0;
+
+ // LOGGER.debug("nextVal = " + nextVal );
+
+ return playIt ? nextVal : -1;
+ }
+
+ public Random getRandom() {
+ return random;
+ }
+
+ public void setRandom(Random random) {
+ this.random = random;
+ }
+
+}
diff --git a/src/main/java/com/jsyn/util/SampleLoader.java b/src/main/java/com/jsyn/util/SampleLoader.java
new file mode 100644
index 0000000..170b4cb
--- /dev/null
+++ b/src/main/java/com/jsyn/util/SampleLoader.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import com.jsyn.data.FloatSample;
+import com.jsyn.util.soundfile.CustomSampleLoader;
+
+/**
+ * Load a FloatSample from various sources. The default loader uses custom code to load WAV or AIF
+ * files. Supported data formats are 16, 24 and 32 bit PCM, and 32-bit float. Compressed formats
+ * such as unsigned 8-bit, uLaw, A-Law and MP3 are not support. If you need to load one of those
+ * files try setJavaSoundPreferred(true). Or convert it to a supported format using Audacity or Sox
+ * or some other sample file tool. Here is an example of loading a sample from a file.
+ *
+ * <pre>
+ * <code>
+ * File sampleFile = new File("guitar.wav");
+ * FloatSample sample = SampleLoader.loadFloatSample( sampleFile );
+ * </code>
+ * </pre>
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class SampleLoader {
+ private static boolean javaSoundPreferred = false;
+ private static final String JS_LOADER_NAME = "com.jsyn.util.JavaSoundSampleLoader";
+
+ /**
+ * Try to create an implementation of AudioSampleLoader.
+ *
+ * @return A device supported on this platform.
+ */
+ private static AudioSampleLoader createLoader() {
+ AudioSampleLoader loader = null;
+ try {
+ if (javaSoundPreferred) {
+ loader = (AudioSampleLoader) JavaTools.loadClass(JS_LOADER_NAME).newInstance();
+ } else {
+ loader = new CustomSampleLoader();
+ }
+ } catch (InstantiationException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ return loader;
+ }
+
+ /**
+ * Load a FloatSample from a File object.
+ */
+ public static FloatSample loadFloatSample(File fileIn) throws IOException {
+ AudioSampleLoader loader = SampleLoader.createLoader();
+ return loader.loadFloatSample(fileIn);
+ }
+
+ /**
+ * Load a FloatSample from an InputStream. This is handy when loading Resources from a JAR file.
+ */
+ public static FloatSample loadFloatSample(InputStream inputStream) throws IOException {
+ AudioSampleLoader loader = SampleLoader.createLoader();
+ return loader.loadFloatSample(inputStream);
+ }
+
+ /**
+ * Load a FloatSample from a URL.. This is handy when loading Resources from a website.
+ */
+ public static FloatSample loadFloatSample(URL url) throws IOException {
+ AudioSampleLoader loader = SampleLoader.createLoader();
+ return loader.loadFloatSample(url);
+ }
+
+ public static boolean isJavaSoundPreferred() {
+ return javaSoundPreferred;
+ }
+
+ /**
+ * If set true then the audio file parser from JavaSound will be used. Note that JavaSound
+ * cannot load audio files containing floating point data. But it may be able to load some
+ * compressed data formats such as uLaw.
+ *
+ * Note: JavaSound is not supported on Android.
+ *
+ * @param javaSoundPreferred
+ */
+ public static void setJavaSoundPreferred(boolean javaSoundPreferred) {
+ SampleLoader.javaSoundPreferred = javaSoundPreferred;
+ }
+
+ /**
+ * Decode 24 bit samples from a BigEndian byte array into a float array. The samples will be
+ * normalized into the range -1.0 to +1.0.
+ *
+ * @param audioBytes raw data from an audio file
+ * @param offset first element of byte array
+ * @param numBytes number of bytes to process
+ * @param data array to be filled with floats
+ * @param outputOffset first element of float array to be filled
+ */
+ public static void decodeBigI24ToF32(byte[] audioBytes, int offset, int numBytes, float[] data,
+ int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int hi = ((audioBytes[byteCursor++]) & 0x00FF);
+ int mid = ((audioBytes[byteCursor++]) & 0x00FF);
+ int lo = ((audioBytes[byteCursor++]) & 0x00FF);
+ int value = (hi << 24) | (mid << 16) | (lo << 8);
+ data[floatCursor++] = value * (1.0f / Integer.MAX_VALUE);
+ }
+ }
+
+ public static void decodeBigI16ToF32(byte[] audioBytes, int offset, int numBytes, float[] data,
+ int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int hi = ((audioBytes[byteCursor++]) & 0x00FF);
+ int lo = ((audioBytes[byteCursor++]) & 0x00FF);
+ short value = (short) ((hi << 8) | lo);
+ data[floatCursor++] = value * (1.0f / 32768);
+ }
+ }
+
+ public static void decodeBigF32ToF32(byte[] audioBytes, int offset, int numBytes, float[] data,
+ int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int bits = audioBytes[byteCursor++];
+ bits = (bits << 8) | ((audioBytes[byteCursor++]) & 0x00FF);
+ bits = (bits << 8) | ((audioBytes[byteCursor++]) & 0x00FF);
+ bits = (bits << 8) | ((audioBytes[byteCursor++]) & 0x00FF);
+ data[floatCursor++] = Float.intBitsToFloat(bits);
+ }
+ }
+
+ public static void decodeBigI32ToF32(byte[] audioBytes, int offset, int numBytes, float[] data,
+ int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int value = audioBytes[byteCursor++]; // MSB
+ value = (value << 8) | ((audioBytes[byteCursor++]) & 0x00FF);
+ value = (value << 8) | ((audioBytes[byteCursor++]) & 0x00FF);
+ value = (value << 8) | ((audioBytes[byteCursor++]) & 0x00FF);
+ data[floatCursor++] = value * (1.0f / Integer.MAX_VALUE);
+ }
+ }
+
+ public static void decodeLittleF32ToF32(byte[] audioBytes, int offset, int numBytes,
+ float[] data, int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int bits = ((audioBytes[byteCursor++]) & 0x00FF); // LSB
+ bits += ((audioBytes[byteCursor++]) & 0x00FF) << 8;
+ bits += ((audioBytes[byteCursor++]) & 0x00FF) << 16;
+ bits += (audioBytes[byteCursor++]) << 24;
+ data[floatCursor++] = Float.intBitsToFloat(bits);
+ }
+ }
+
+ public static void decodeLittleI32ToF32(byte[] audioBytes, int offset, int numBytes,
+ float[] data, int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int value = ((audioBytes[byteCursor++]) & 0x00FF);
+ value += ((audioBytes[byteCursor++]) & 0x00FF) << 8;
+ value += ((audioBytes[byteCursor++]) & 0x00FF) << 16;
+ value += (audioBytes[byteCursor++]) << 24;
+ data[floatCursor++] = value * (1.0f / Integer.MAX_VALUE);
+ }
+ }
+
+ public static void decodeLittleI24ToF32(byte[] audioBytes, int offset, int numBytes,
+ float[] data, int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int lo = ((audioBytes[byteCursor++]) & 0x00FF);
+ int mid = ((audioBytes[byteCursor++]) & 0x00FF);
+ int hi = ((audioBytes[byteCursor++]) & 0x00FF);
+ int value = (hi << 24) | (mid << 16) | (lo << 8);
+ data[floatCursor++] = value * (1.0f / Integer.MAX_VALUE);
+ }
+ }
+
+ public static void decodeLittleI16ToF32(byte[] audioBytes, int offset, int numBytes,
+ float[] data, int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int lo = ((audioBytes[byteCursor++]) & 0x00FF);
+ int hi = ((audioBytes[byteCursor++]) & 0x00FF);
+ short value = (short) ((hi << 8) | lo);
+ float sample = value * (1.0f / 32768);
+ data[floatCursor++] = sample;
+ }
+ }
+
+}
diff --git a/src/main/java/com/jsyn/util/SignalCorrelator.java b/src/main/java/com/jsyn/util/SignalCorrelator.java
new file mode 100644
index 0000000..ebdd46b
--- /dev/null
+++ b/src/main/java/com/jsyn/util/SignalCorrelator.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+/**
+ * Interface used to evaluate various algorithms for pitch detection.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public interface SignalCorrelator {
+ /**
+ * Add a sample to be analyzed. The samples will generally be held in a circular buffer.
+ *
+ * @param value
+ * @return true if a new period value has been generated
+ */
+ public boolean addSample(double value);
+
+ /**
+ * @return the estimated period of the waveform in samples
+ */
+ public double getPeriod();
+
+ /**
+ * Measure of how confident the analyzer is of the last result.
+ *
+ * @return quality of the estimate between 0.0 and 1.0
+ */
+ public double getConfidence();
+
+ /** For internal debugging. */
+ public float[] getDiffs();
+
+}
diff --git a/src/main/java/com/jsyn/util/StreamingThread.java b/src/main/java/com/jsyn/util/StreamingThread.java
new file mode 100644
index 0000000..682f476
--- /dev/null
+++ b/src/main/java/com/jsyn/util/StreamingThread.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.io.IOException;
+
+import com.jsyn.io.AudioInputStream;
+import com.jsyn.io.AudioOutputStream;
+
+/**
+ * Read from an AudioInputStream and write to an AudioOutputStream as a background thread.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class StreamingThread extends Thread {
+ private AudioInputStream inputStream;
+ private AudioOutputStream outputStream;
+ private int framesPerBuffer = 1024;
+ private volatile boolean go = true;
+ private TransportModel transportModel;
+ private long framePosition;
+ private long maxFrames;
+ private int samplesPerFrame = 1;
+
+ public StreamingThread(AudioInputStream inputStream, AudioOutputStream outputStream) {
+ this.inputStream = inputStream;
+ this.outputStream = outputStream;
+ }
+
+ @Override
+ public void run() {
+ double[] buffer = new double[framesPerBuffer * samplesPerFrame];
+ try {
+ transportModel.firePositionChanged(framePosition);
+ transportModel.fireStateChanged(TransportModel.STATE_RUNNING);
+ int framesToRead = geteFramesToRead(buffer);
+ while (go && (framesToRead > 0)) {
+ int samplesToRead = framesToRead * samplesPerFrame;
+ while (samplesToRead > 0) {
+ int samplesRead = inputStream.read(buffer, 0, samplesToRead);
+ outputStream.write(buffer, 0, samplesRead);
+ samplesToRead -= samplesRead;
+ }
+ framePosition += framesToRead;
+ transportModel.firePositionChanged(framePosition);
+ framesToRead = geteFramesToRead(buffer);
+ }
+ transportModel.fireStateChanged(TransportModel.STATE_STOPPED);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private int geteFramesToRead(double[] buffer) {
+ if (maxFrames > 0) {
+ long numToRead = maxFrames - framePosition;
+ if (numToRead < 0) {
+ return 0;
+ } else if (numToRead > framesPerBuffer) {
+ numToRead = framesPerBuffer;
+ }
+ return (int) numToRead;
+ } else {
+ return framesPerBuffer;
+ }
+ }
+
+ public int getFramesPerBuffer() {
+ return framesPerBuffer;
+ }
+
+ /**
+ * Only call this before the thread has started.
+ *
+ * @param framesPerBuffer
+ */
+ public void setFramesPerBuffer(int framesPerBuffer) {
+ this.framesPerBuffer = framesPerBuffer;
+ }
+
+ public void requestStop() {
+ go = false;
+ }
+
+ public TransportModel getTransportModel() {
+ return transportModel;
+ }
+
+ public void setTransportModel(TransportModel transportModel) {
+ this.transportModel = transportModel;
+ }
+
+ /**
+ * @param maxFrames
+ */
+ public void setMaxFrames(long maxFrames) {
+ this.maxFrames = maxFrames;
+ }
+
+ public int getSamplesPerFrame() {
+ return samplesPerFrame;
+ }
+
+ public void setSamplesPerFrame(int samplesPerFrame) {
+ this.samplesPerFrame = samplesPerFrame;
+ }
+}
diff --git a/src/main/java/com/jsyn/util/TransportListener.java b/src/main/java/com/jsyn/util/TransportListener.java
new file mode 100644
index 0000000..3c8b048
--- /dev/null
+++ b/src/main/java/com/jsyn/util/TransportListener.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+public interface TransportListener {
+ /**
+ * @param transportModel
+ * @param framePosition position in frames
+ */
+ void positionChanged(TransportModel transportModel, long framePosition);
+
+ /**
+ * @param transportModel
+ * @param state for example TransportModel.STATE_STOPPED
+ */
+ void stateChanged(TransportModel transportModel, int state);
+}
diff --git a/src/main/java/com/jsyn/util/TransportModel.java b/src/main/java/com/jsyn/util/TransportModel.java
new file mode 100644
index 0000000..bcc75be
--- /dev/null
+++ b/src/main/java/com/jsyn/util/TransportModel.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class TransportModel {
+ public static final int STATE_STOPPED = 0;
+ public static final int STATE_PAUSED = 1;
+ public static final int STATE_RUNNING = 2;
+
+ private CopyOnWriteArrayList<TransportListener> listeners = new CopyOnWriteArrayList<TransportListener>();
+ private int state = STATE_STOPPED;
+ private long position;
+
+ public void addTransportListener(TransportListener listener) {
+ listeners.add(listener);
+ }
+
+ public void removeTransportListener(TransportListener listener) {
+ listeners.remove(listener);
+ }
+
+ public void setState(int newState) {
+ state = newState;
+ fireStateChanged(newState);
+ }
+
+ public int getState() {
+ return state;
+ }
+
+ public void setPosition(long newPosition) {
+ position = newPosition;
+ firePositionChanged(newPosition);
+ }
+
+ public long getPosition() {
+ return position;
+ }
+
+ public void fireStateChanged(int newState) {
+ for (TransportListener listener : listeners) {
+ listener.stateChanged(this, newState);
+ }
+ }
+
+ public void firePositionChanged(long newPosition) {
+ for (TransportListener listener : listeners) {
+ listener.positionChanged(this, newPosition);
+ }
+ }
+}
diff --git a/src/main/java/com/jsyn/util/VoiceAllocator.java b/src/main/java/com/jsyn/util/VoiceAllocator.java
new file mode 100644
index 0000000..f20f7a5
--- /dev/null
+++ b/src/main/java/com/jsyn/util/VoiceAllocator.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.UnitVoice;
+import com.softsynth.shared.time.ScheduledCommand;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Allocate voices based on an integer tag. The tag could, for example, be a MIDI note number. Or a
+ * tag could be an int that always increments. Use the same tag to refer to a voice for noteOn() and
+ * noteOff(). If no new voices are available then a voice in use will be stolen.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class VoiceAllocator implements Instrument {
+ private int maxVoices;
+ private VoiceTracker[] trackers;
+ private long tick;
+ private Synthesizer synthesizer;
+ private static final int UNASSIGNED_PRESET = -1;
+ private int mPresetIndex = UNASSIGNED_PRESET;
+
+ /**
+ * Create an allocator for the array of UnitVoices. The array must be full of instantiated
+ * UnitVoices that are connected to some kind of mixer.
+ *
+ * @param voices
+ */
+ public VoiceAllocator(UnitVoice[] voices) {
+ maxVoices = voices.length;
+ trackers = new VoiceTracker[maxVoices];
+ for (int i = 0; i < maxVoices; i++) {
+ trackers[i] = new VoiceTracker();
+ trackers[i].voice = voices[i];
+ }
+ }
+
+ public Synthesizer getSynthesizer() {
+ if (synthesizer == null) {
+ synthesizer = trackers[0].voice.getUnitGenerator().getSynthesizer();
+ }
+ return synthesizer;
+ }
+
+ private class VoiceTracker {
+ UnitVoice voice;
+ int tag = -1;
+ int presetIndex = UNASSIGNED_PRESET;
+ long when;
+ boolean on;
+
+ public void off() {
+ on = false;
+ when = tick++;
+ }
+ }
+
+ /**
+ * @return number of UnitVoices passed to the allocator.
+ */
+ public int getVoiceCount() {
+ return maxVoices;
+ }
+
+ private VoiceTracker findVoice(int tag) {
+ for (VoiceTracker tracker : trackers) {
+ if (tracker.tag == tag) {
+ return tracker;
+ }
+ }
+ return null;
+ }
+
+ private VoiceTracker stealVoice() {
+ VoiceTracker bestOff = null;
+ VoiceTracker bestOn = null;
+ for (VoiceTracker tracker : trackers) {
+ if (tracker.voice == null) {
+ return tracker;
+ }
+ // If we have a bestOff voice then don't even bother with on voices.
+ else if (bestOff != null) {
+ // Older off voice?
+ if (!tracker.on && (tracker.when < bestOff.when)) {
+ bestOff = tracker;
+ }
+ } else if (tracker.on) {
+ if (bestOn == null) {
+ bestOn = tracker;
+ } else if (tracker.when < bestOn.when) {
+ bestOn = tracker;
+ }
+ } else {
+ bestOff = tracker;
+ }
+ }
+ if (bestOff != null) {
+ return bestOff;
+ } else {
+ return bestOn;
+ }
+ }
+
+ /**
+ * Allocate a Voice associated with this tag. It will first pick a voice already assigned to
+ * that tag. Next it will pick the oldest voice that is off. Next it will pick the oldest voice
+ * that is on. If you are using timestamps to play the voice in the future then you should use
+ * the noteOn() noteOff() and setPort() methods.
+ *
+ * @param tag
+ * @return Voice that is most available.
+ */
+ protected synchronized UnitVoice allocate(int tag) {
+ VoiceTracker tracker = allocateTracker(tag);
+ return tracker.voice;
+ }
+
+ private VoiceTracker allocateTracker(int tag) {
+ VoiceTracker tracker = findVoice(tag);
+ if (tracker == null) {
+ tracker = stealVoice();
+ }
+ tracker.tag = tag;
+ tracker.when = tick++;
+ tracker.on = true;
+ return tracker;
+ }
+
+ protected synchronized boolean isOn(int tag) {
+ VoiceTracker tracker = findVoice(tag);
+ if (tracker != null) {
+ return tracker.on;
+ }
+ return false;
+ }
+
+ protected synchronized UnitVoice off(int tag) {
+ VoiceTracker tracker = findVoice(tag);
+ if (tracker != null) {
+ tracker.off();
+ return tracker.voice;
+ }
+ return null;
+ }
+
+ /** Turn off all the note currently on. */
+ @Override
+ public void allNotesOff(TimeStamp timeStamp) {
+ getSynthesizer().scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ for (VoiceTracker tracker : trackers) {
+ if (tracker.on) {
+ tracker.voice.noteOff(getSynthesizer().createTimeStamp());
+ tracker.off();
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Play a note on the voice and associate it with the given tag. if needed a new voice will be
+ * allocated and an old voice may be turned off.
+ */
+ @Override
+ public void noteOn(final int tag, final double frequency, final double amplitude,
+ TimeStamp timeStamp) {
+ getSynthesizer().scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ VoiceTracker voiceTracker = allocateTracker(tag);
+ if (voiceTracker.presetIndex != mPresetIndex) {
+ voiceTracker.voice.usePreset(mPresetIndex);
+ voiceTracker.presetIndex = mPresetIndex;
+ }
+ voiceTracker.voice.noteOn(frequency, amplitude, getSynthesizer().createTimeStamp());
+ }
+ });
+ }
+
+ /**
+ * Play a note on the voice and associate it with the given tag. if needed a new voice will be
+ * allocated and an old voice may be turned off.
+ * Apply an operation to the voice.
+ */
+ public void noteOn(final int tag,
+ final double frequency,
+ final double amplitude,
+ final VoiceOperation operation,
+ TimeStamp timeStamp) {
+ getSynthesizer().scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ VoiceTracker voiceTracker = allocateTracker(tag);
+ operation.operate(voiceTracker.voice);
+ voiceTracker.voice.noteOn(frequency, amplitude, getSynthesizer().createTimeStamp());
+ }
+ });
+ }
+
+ /** Turn off the voice associated with the given tag if allocated. */
+ @Override
+ public void noteOff(final int tag, TimeStamp timeStamp) {
+ getSynthesizer().scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ VoiceTracker voiceTracker = findVoice(tag);
+ if (voiceTracker != null) {
+ voiceTracker.voice.noteOff(getSynthesizer().createTimeStamp());
+ off(tag);
+ }
+ }
+ });
+ }
+
+ /** Set a port on the voice associated with the given tag if allocated. */
+ @Override
+ public void setPort(final int tag, final String portName, final double value,
+ TimeStamp timeStamp) {
+ getSynthesizer().scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ VoiceTracker voiceTracker = findVoice(tag);
+ if (voiceTracker != null) {
+ voiceTracker.voice.setPort(portName, value, getSynthesizer().createTimeStamp());
+ }
+ }
+ });
+ }
+
+ @Override
+ public void usePreset(final int presetIndex, TimeStamp timeStamp) {
+ getSynthesizer().scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ mPresetIndex = presetIndex;
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/com/jsyn/util/VoiceDescription.java b/src/main/java/com/jsyn/util/VoiceDescription.java
new file mode 100644
index 0000000..b7be044
--- /dev/null
+++ b/src/main/java/com/jsyn/util/VoiceDescription.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import com.jsyn.unitgen.UnitVoice;
+
+/**
+ * Describe a voice so that a user can pick it out of an InstrumentLibrary.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ * @see PolyphonicInstrument
+ */
+public abstract class VoiceDescription {
+ private String name;
+ private String[] presetNames;
+
+ public VoiceDescription(String name, String[] presetNames) {
+ this.name = name;
+ this.presetNames = presetNames;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getPresetCount() {
+ return presetNames.length;
+ }
+
+ public String[] getPresetNames() {
+ return presetNames;
+ }
+
+ public abstract String[] getTags(int presetIndex);
+
+ /**
+ * Instantiate one of these voices. You may want to call usePreset(n) on the voice after
+ * instantiating it.
+ *
+ * @return a voice
+ */
+ public abstract UnitVoice createUnitVoice();
+
+ public abstract String getVoiceClassName();
+
+ @Override
+ public String toString() {
+ return name + "[" + getPresetCount() + "]";
+ }
+}
diff --git a/src/main/java/com/jsyn/util/VoiceOperation.java b/src/main/java/com/jsyn/util/VoiceOperation.java
new file mode 100644
index 0000000..cd3b48e
--- /dev/null
+++ b/src/main/java/com/jsyn/util/VoiceOperation.java
@@ -0,0 +1,7 @@
+package com.jsyn.util;
+
+import com.jsyn.unitgen.UnitVoice;
+
+public interface VoiceOperation {
+ public void operate(UnitVoice voice);
+}
diff --git a/src/main/java/com/jsyn/util/WaveFileWriter.java b/src/main/java/com/jsyn/util/WaveFileWriter.java
new file mode 100644
index 0000000..32e9995
--- /dev/null
+++ b/src/main/java/com/jsyn/util/WaveFileWriter.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+
+import com.jsyn.io.AudioOutputStream;
+
+/**
+ * Write audio data to a WAV file.
+ *
+ * <pre>
+ * <code>
+ * WaveFileWriter writer = new WaveFileWriter(file);
+ * writer.setFrameRate(22050);
+ * writer.setBitsPerSample(24);
+ * writer.write(floatArray);
+ * writer.close();
+ * </code>
+ * </pre>
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class WaveFileWriter implements AudioOutputStream {
+ private static final short WAVE_FORMAT_PCM = 1;
+ private OutputStream outputStream;
+ private long riffSizePosition = 0;
+ private long dataSizePosition = 0;
+ private int frameRate = 44100;
+ private int samplesPerFrame = 1;
+ private int bitsPerSample = 16;
+ private int bytesWritten;
+ private File outputFile;
+ private boolean headerWritten = false;
+ private final static int PCM24_MIN = -(1 << 23);
+ private final static int PCM24_MAX = (1 << 23) - 1;
+
+ /**
+ * Create a writer that will write to the specified file.
+ *
+ * @param outputFile
+ * @throws FileNotFoundException
+ */
+ public WaveFileWriter(File outputFile) throws FileNotFoundException {
+ this.outputFile = outputFile;
+ FileOutputStream fileOut = new FileOutputStream(outputFile);
+ outputStream = new BufferedOutputStream(fileOut);
+ }
+
+ /**
+ * @param frameRate default is 44100
+ */
+ public void setFrameRate(int frameRate) {
+ this.frameRate = frameRate;
+ }
+
+ public int getFrameRate() {
+ return frameRate;
+ }
+
+ /** For stereo, set this to 2. Default is 1. */
+ public void setSamplesPerFrame(int samplesPerFrame) {
+ this.samplesPerFrame = samplesPerFrame;
+ }
+
+ public int getSamplesPerFrame() {
+ return samplesPerFrame;
+ }
+
+ /** Only 16 or 24 bit samples supported at the moment. Default is 16. */
+ public void setBitsPerSample(int bits) {
+ if ((bits != 16) && (bits != 24)) {
+ throw new IllegalArgumentException("Only 16 or 24 bits per sample allowed. Not " + bits);
+ }
+ bitsPerSample = bits;
+ }
+
+ public int getBitsPerSample() {
+ return bitsPerSample;
+ }
+
+ @Override
+ public void close() throws IOException {
+ outputStream.close();
+ fixSizes();
+ }
+
+ /** Write entire buffer of audio samples to the WAV file. */
+ @Override
+ public void write(double[] buffer) throws IOException {
+ write(buffer, 0, buffer.length);
+ }
+
+ /** Write audio to the WAV file. */
+ public void write(float[] buffer) throws IOException {
+ write(buffer, 0, buffer.length);
+ }
+
+ /** Write single audio data value to the WAV file. */
+ @Override
+ public void write(double value) throws IOException {
+ if (!headerWritten) {
+ writeHeader();
+ }
+
+ if (bitsPerSample == 24) {
+ writePCM24(value);
+ } else {
+ writePCM16(value);
+ }
+ }
+
+ private void writePCM24(double value) throws IOException {
+ // Offset before casting so that we can avoid using floor().
+ // Also round by adding 0.5 so that very small signals go to zero.
+ double temp = (PCM24_MAX * value) + 0.5 - PCM24_MIN;
+ int sample = ((int) temp) + PCM24_MIN;
+ // clip to 24-bit range
+ if (sample > PCM24_MAX) {
+ sample = PCM24_MAX;
+ } else if (sample < PCM24_MIN) {
+ sample = PCM24_MIN;
+ }
+ // encode as little-endian
+ writeByte(sample); // little end
+ writeByte(sample >> 8); // middle
+ writeByte(sample >> 16); // big end
+ }
+
+ private void writePCM16(double value) throws IOException {
+ // Offset before casting so that we can avoid using floor().
+ // Also round by adding 0.5 so that very small signals go to zero.
+ double temp = (Short.MAX_VALUE * value) + 0.5 - Short.MIN_VALUE;
+ int sample = ((int) temp) + Short.MIN_VALUE;
+ if (sample > Short.MAX_VALUE) {
+ sample = Short.MAX_VALUE;
+ } else if (sample < Short.MIN_VALUE) {
+ sample = Short.MIN_VALUE;
+ }
+ writeByte(sample); // little end
+ writeByte(sample >> 8); // big end
+ }
+
+ /** Write audio to the WAV file. */
+ @Override
+ public void write(double[] buffer, int start, int count) throws IOException {
+ for (int i = 0; i < count; i++) {
+ write(buffer[start + i]);
+ }
+ }
+
+ /** Write audio to the WAV file. */
+ public void write(float[] buffer, int start, int count) throws IOException {
+ for (int i = 0; i < count; i++) {
+ write(buffer[start + i]);
+ }
+ }
+
+ // Write lower 8 bits. Upper bits ignored.
+ private void writeByte(int b) throws IOException {
+ outputStream.write(b);
+ bytesWritten += 1;
+ }
+
+ /**
+ * Write a 32 bit integer to the stream in Little Endian format.
+ */
+ public void writeIntLittle(int n) throws IOException {
+ writeByte(n);
+ writeByte(n >> 8);
+ writeByte(n >> 16);
+ writeByte(n >> 24);
+ }
+
+ /**
+ * Write a 16 bit integer to the stream in Little Endian format.
+ */
+ public void writeShortLittle(short n) throws IOException {
+ writeByte(n);
+ writeByte(n >> 8);
+ }
+
+ /**
+ * Write a simple WAV header for PCM data.
+ */
+ private void writeHeader() throws IOException {
+ writeRiffHeader();
+ writeFormatChunk();
+ writeDataChunkHeader();
+ outputStream.flush();
+ headerWritten = true;
+ }
+
+ /**
+ * Write a 'RIFF' file header and a 'WAVE' ID to the WAV file.
+ */
+ private void writeRiffHeader() throws IOException {
+ writeByte('R');
+ writeByte('I');
+ writeByte('F');
+ writeByte('F');
+ riffSizePosition = bytesWritten;
+ writeIntLittle(Integer.MAX_VALUE);
+ writeByte('W');
+ writeByte('A');
+ writeByte('V');
+ writeByte('E');
+ }
+
+ /**
+ * Write an 'fmt ' chunk to the WAV file containing the given information.
+ */
+ public void writeFormatChunk() throws IOException {
+ int bytesPerSample = (bitsPerSample + 7) / 8;
+
+ writeByte('f');
+ writeByte('m');
+ writeByte('t');
+ writeByte(' ');
+ writeIntLittle(16); // chunk size
+ writeShortLittle(WAVE_FORMAT_PCM);
+ writeShortLittle((short) samplesPerFrame);
+ writeIntLittle(frameRate);
+ // bytes/second
+ writeIntLittle(frameRate * samplesPerFrame * bytesPerSample);
+ // block align
+ writeShortLittle((short) (samplesPerFrame * bytesPerSample));
+ writeShortLittle((short) bitsPerSample);
+ }
+
+ /**
+ * Write a 'data' chunk header to the WAV file. This should be followed by call to
+ * writeShortLittle() to write the data to the chunk.
+ */
+ public void writeDataChunkHeader() throws IOException {
+ writeByte('d');
+ writeByte('a');
+ writeByte('t');
+ writeByte('a');
+ dataSizePosition = bytesWritten;
+ writeIntLittle(Integer.MAX_VALUE); // size
+ }
+
+ /**
+ * Fix RIFF and data chunk sizes based on final size. Assume data chunk is the last chunk.
+ */
+ private void fixSizes() throws IOException {
+ RandomAccessFile randomFile = new RandomAccessFile(outputFile, "rw");
+ try {
+ // adjust RIFF size
+ long end = bytesWritten;
+ int riffSize = (int) (end - riffSizePosition) - 4;
+ randomFile.seek(riffSizePosition);
+ writeRandomIntLittle(randomFile, riffSize);
+ // adjust data size
+ int dataSize = (int) (end - dataSizePosition) - 4;
+ randomFile.seek(dataSizePosition);
+ writeRandomIntLittle(randomFile, dataSize);
+ } finally {
+ randomFile.close();
+ }
+ }
+
+ private void writeRandomIntLittle(RandomAccessFile randomFile, int n) throws IOException {
+ byte[] buffer = new byte[4];
+ buffer[0] = (byte) n;
+ buffer[1] = (byte) (n >> 8);
+ buffer[2] = (byte) (n >> 16);
+ buffer[3] = (byte) (n >> 24);
+ randomFile.write(buffer);
+ }
+
+}
diff --git a/src/main/java/com/jsyn/util/WaveRecorder.java b/src/main/java/com/jsyn/util/WaveRecorder.java
new file mode 100644
index 0000000..8008d1d
--- /dev/null
+++ b/src/main/java/com/jsyn/util/WaveRecorder.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Connect a unit generator to the input. Then start() recording. The signal will be written to a
+ * WAV format file that can be read by other programs.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class WaveRecorder {
+ private AudioStreamReader reader;
+ private WaveFileWriter writer;
+ private StreamingThread thread;
+ private Synthesizer synth;
+ private TransportModel transportModel = new TransportModel();
+ private double maxRecordingTime;
+
+ /**
+ * Create a stereo 16-bit recorder.
+ *
+ * @param synth
+ * @param outputFile
+ * @throws FileNotFoundException
+ */
+ public WaveRecorder(Synthesizer synth, File outputFile) throws FileNotFoundException {
+ this(synth, outputFile, 2, 16);
+ }
+
+ public WaveRecorder(Synthesizer synth, File outputFile, int samplesPerFrame)
+ throws FileNotFoundException {
+ this(synth, outputFile, samplesPerFrame, 16);
+ }
+
+ /**
+ * @param synth
+ * @param outputFile
+ * @param samplesPerFrame 1 for mono, 2 for stereo
+ * @param bitsPerSample 16 or 24
+ * @throws FileNotFoundException
+ */
+ public WaveRecorder(Synthesizer synth, File outputFile, int samplesPerFrame, int bitsPerSample)
+ throws FileNotFoundException {
+ this.synth = synth;
+ reader = new AudioStreamReader(synth, samplesPerFrame);
+
+ writer = new WaveFileWriter(outputFile);
+ writer.setFrameRate(synth.getFrameRate());
+ writer.setSamplesPerFrame(samplesPerFrame);
+ writer.setBitsPerSample(bitsPerSample);
+ }
+
+ public UnitInputPort getInput() {
+ return reader.getInput();
+ }
+
+ public void start() {
+ stop();
+ thread = new StreamingThread(reader, writer);
+ thread.setTransportModel(transportModel);
+ thread.setSamplesPerFrame(writer.getSamplesPerFrame());
+ updateMaxRecordingTime();
+ thread.start();
+ }
+
+ public void stop() {
+ if (thread != null) {
+ thread.requestStop();
+ try {
+ thread.join(500);
+ } catch (InterruptedException ignored) {
+ }
+ thread = null;
+ }
+ }
+
+ /** Close and disconnect any connected inputs. */
+ public void close() throws IOException {
+ stop();
+ if (writer != null) {
+ writer.close();
+ writer = null;
+ }
+ if (reader != null) {
+ reader.close();
+ for (int i = 0; i < reader.getInput().getNumParts(); i++) {
+ reader.getInput().disconnectAll(i);
+ }
+ reader = null;
+ }
+ }
+
+ public void addTransportListener(TransportListener listener) {
+ transportModel.addTransportListener(listener);
+ }
+
+ public void removeTransportListener(TransportListener listener) {
+ transportModel.removeTransportListener(listener);
+ }
+
+ public void setMaxRecordingTime(double maxRecordingTime) {
+ this.maxRecordingTime = maxRecordingTime;
+ updateMaxRecordingTime();
+ }
+
+ private void updateMaxRecordingTime() {
+ StreamingThread streamingThread = thread;
+ if (streamingThread != null) {
+ long maxFrames = (long) (maxRecordingTime * synth.getFrameRate());
+ streamingThread.setMaxFrames(maxFrames);
+ }
+ }
+}
diff --git a/src/main/java/com/jsyn/util/soundfile/AIFFFileParser.java b/src/main/java/com/jsyn/util/soundfile/AIFFFileParser.java
new file mode 100644
index 0000000..89b443c
--- /dev/null
+++ b/src/main/java/com/jsyn/util/soundfile/AIFFFileParser.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util.soundfile;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+import com.jsyn.data.FloatSample;
+import com.jsyn.data.SampleMarker;
+import com.jsyn.util.SampleLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AIFFFileParser extends AudioFileParser {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AIFFFileParser.class);
+
+ private static final String SUPPORTED_FORMATS = "Only 16 and 24 bit PCM or 32-bit float AIF files supported.";
+ static final int AIFF_ID = ('A' << 24) | ('I' << 16) | ('F' << 8) | 'F';
+ static final int AIFC_ID = ('A' << 24) | ('I' << 16) | ('F' << 8) | 'C';
+ static final int COMM_ID = ('C' << 24) | ('O' << 16) | ('M' << 8) | 'M';
+ static final int SSND_ID = ('S' << 24) | ('S' << 16) | ('N' << 8) | 'D';
+ static final int MARK_ID = ('M' << 24) | ('A' << 16) | ('R' << 8) | 'K';
+ static final int INST_ID = ('I' << 24) | ('N' << 16) | ('S' << 8) | 'T';
+ static final int NONE_ID = ('N' << 24) | ('O' << 16) | ('N' << 8) | 'E';
+ static final int FL32_ID = ('F' << 24) | ('L' << 16) | ('3' << 8) | '2';
+ static final int FL32_ID_LC = ('f' << 24) | ('l' << 16) | ('3' << 8) | '2';
+
+ int sustainBeginID = -1;
+ int sustainEndID = -1;
+ int releaseBeginID = -1;
+ int releaseEndID = -1;
+ boolean typeFloat = false;
+
+ @Override
+ FloatSample finish() throws IOException {
+ setLoops();
+
+ if ((byteData == null)) {
+ throw new IOException("No data found in audio sample.");
+ }
+ float[] floatData = new float[numFrames * samplesPerFrame];
+ if (bitsPerSample == 16) {
+ SampleLoader.decodeBigI16ToF32(byteData, 0, byteData.length, floatData, 0);
+ } else if (bitsPerSample == 24) {
+ SampleLoader.decodeBigI24ToF32(byteData, 0, byteData.length, floatData, 0);
+ } else if (bitsPerSample == 32) {
+ if (typeFloat) {
+ SampleLoader.decodeBigF32ToF32(byteData, 0, byteData.length, floatData, 0);
+ } else {
+ SampleLoader.decodeBigI32ToF32(byteData, 0, byteData.length, floatData, 0);
+ }
+ } else {
+ throw new IOException(SUPPORTED_FORMATS + " size = " + bitsPerSample);
+ }
+
+ return makeSample(floatData);
+ }
+
+ double read80BitFloat() throws IOException {
+ /*
+ * This is not a full decoding of the 80 bit number but it should suffice for the range we
+ * expect.
+ */
+ byte[] bytes = new byte[10];
+ parser.read(bytes);
+ int exp = ((bytes[0] & 0x3F) << 8) | (bytes[1] & 0xFF);
+ int mant = ((bytes[2] & 0xFF) << 16) | ((bytes[3] & 0xFF) << 8) | (bytes[4] & 0xFF);
+ // LOGGER.debug( "exp = " + exp + ", mant = " + mant );
+ return mant / (double) (1 << (22 - exp));
+ }
+
+ void parseCOMMChunk(IFFParser parser, int ckSize) throws IOException {
+ samplesPerFrame = parser.readShortBig();
+ numFrames = parser.readIntBig();
+ bitsPerSample = parser.readShortBig();
+ frameRate = read80BitFloat();
+ if (ckSize > 18) {
+ int format = parser.readIntBig();
+ // Validate data format.
+ if ((format == FL32_ID) || (format == FL32_ID_LC)) {
+ typeFloat = true;
+ } else if (format == NONE_ID) {
+ typeFloat = false;
+ } else {
+ throw new IOException(SUPPORTED_FORMATS + " format " + IFFParser.IDToString(format));
+ }
+ }
+
+ bytesPerSample = (bitsPerSample + 7) / 8;
+ bytesPerFrame = bytesPerSample * samplesPerFrame;
+ }
+
+ /* parse tuning and multi-sample info */
+ @SuppressWarnings("unused")
+ void parseINSTChunk(IFFParser parser, int ckSize) throws IOException {
+ int baseNote = parser.readByte();
+ int detune = parser.readByte();
+ originalPitch = baseNote + (0.01 * detune);
+
+ int lowNote = parser.readByte();
+ int highNote = parser.readByte();
+
+ parser.skip(2); /* lo,hi velocity */
+ int gain = parser.readShortBig();
+
+ int playMode = parser.readShortBig(); /* sustain */
+ sustainBeginID = parser.readShortBig();
+ sustainEndID = parser.readShortBig();
+
+ playMode = parser.readShortBig(); /* release */
+ releaseBeginID = parser.readShortBig();
+ releaseEndID = parser.readShortBig();
+ }
+
+ private void setLoops() {
+ SampleMarker cuePoint = cueMap.get(sustainBeginID);
+ if (cuePoint != null) {
+ sustainBegin = cuePoint.position;
+ }
+ cuePoint = cueMap.get(sustainEndID);
+ if (cuePoint != null) {
+ sustainEnd = cuePoint.position;
+ }
+ }
+
+ void parseSSNDChunk(IFFParser parser, int ckSize) throws IOException {
+ long numRead;
+ // LOGGER.debug("parseSSNDChunk()");
+ int offset = parser.readIntBig();
+ parser.readIntBig(); /* blocksize */
+ parser.skip(offset);
+ dataPosition = parser.getOffset();
+ int numBytes = ckSize - 8 - offset;
+ if (ifLoadData) {
+ byteData = new byte[numBytes];
+ numRead = parser.read(byteData);
+ } else {
+ numRead = parser.skip(numBytes);
+ }
+ if (numRead != numBytes)
+ throw new EOFException("AIFF data chunk too short!");
+ }
+
+ void parseMARKChunk(IFFParser parser, int ckSize) throws IOException {
+ long startOffset = parser.getOffset();
+ int numCuePoints = parser.readShortBig();
+ // LOGGER.debug( "parseCueChunk: numCuePoints = " + numCuePoints
+ // );
+ for (int i = 0; i < numCuePoints; i++) {
+ // Some AIF files have a bogus numCuePoints so check to see if we
+ // are at end.
+ long numInMark = parser.getOffset() - startOffset;
+ if (numInMark >= ckSize) {
+ LOGGER.debug("Reached end of MARK chunk with bogus numCuePoints = "
+ + numCuePoints);
+ break;
+ }
+
+ int uniqueID = parser.readShortBig();
+ int position = parser.readIntBig();
+ int len = parser.read();
+ String markerName = parseString(parser, len);
+ if ((len & 1) == 0) {
+ parser.skip(1); /* skip pad byte */
+ }
+
+ SampleMarker cuePoint = findOrCreateCuePoint(uniqueID);
+ cuePoint.position = position;
+ cuePoint.name = markerName;
+
+ if (IFFParser.debug) {
+ LOGGER.debug("AIFF Marker at " + position + ", " + markerName);
+ }
+ }
+ }
+
+ /**
+ * Called by parse() method to handle FORM chunks in an AIFF specific manner.
+ *
+ * @param ckID four byte chunk ID such as 'data'
+ * @param ckSize size of chunk in bytes
+ * @exception IOException If parsing fails, or IO error occurs.
+ */
+ @Override
+ public void handleForm(IFFParser parser, int ckID, int ckSize, int type) throws IOException {
+ if ((ckID == IFFParser.FORM_ID) && (type != AIFF_ID) && (type != AIFC_ID))
+ throw new IOException("Bad AIFF form type = " + IFFParser.IDToString(type));
+ }
+
+ /**
+ * Called by parse() method to handle chunks in an AIFF specific manner.
+ *
+ * @param ckID four byte chunk ID such as 'data'
+ * @param ckSize size of chunk in bytes
+ * @exception IOException If parsing fails, or IO error occurs.
+ */
+ @Override
+ public void handleChunk(IFFParser parser, int ckID, int ckSize) throws IOException {
+ switch (ckID) {
+ case COMM_ID:
+ parseCOMMChunk(parser, ckSize);
+ break;
+ case SSND_ID:
+ parseSSNDChunk(parser, ckSize);
+ break;
+ case MARK_ID:
+ parseMARKChunk(parser, ckSize);
+ break;
+ case INST_ID:
+ parseINSTChunk(parser, ckSize);
+ break;
+ default:
+ break;
+ }
+ }
+
+}
diff --git a/src/main/java/com/jsyn/util/soundfile/AudioFileParser.java b/src/main/java/com/jsyn/util/soundfile/AudioFileParser.java
new file mode 100644
index 0000000..e7bb066
--- /dev/null
+++ b/src/main/java/com/jsyn/util/soundfile/AudioFileParser.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2001 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util.soundfile;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import com.jsyn.data.FloatSample;
+import com.jsyn.data.SampleMarker;
+
+/**
+ * Base class for various types of audio specific file parsers.
+ *
+ * @author (C) 2001 Phil Burk, SoftSynth.com
+ */
+
+abstract class AudioFileParser implements ChunkHandler {
+ IFFParser parser;
+ protected byte[] byteData;
+ boolean ifLoadData = true; /* If true, load sound data into memory. */
+ long dataPosition; /*
+ * Number of bytes from beginning of file where sound data resides.
+ */
+ protected int bitsPerSample;
+ protected int bytesPerFrame; // in the file
+ protected int bytesPerSample; // in the file
+ protected HashMap<Integer, SampleMarker> cueMap = new HashMap<Integer, SampleMarker>();
+ protected short samplesPerFrame;
+ protected double frameRate;
+ protected int numFrames;
+ protected double originalPitch = 60.0;
+ protected int sustainBegin = -1;
+ protected int sustainEnd = -1;
+
+ public AudioFileParser() {
+ }
+
+ /**
+ * @return Number of bytes from beginning of stream where sound data resides.
+ */
+ public long getDataPosition() {
+ return dataPosition;
+ }
+
+ /**
+ * This can be read by another thread when load()ing a sample to determine how many bytes have
+ * been read so far.
+ */
+ public synchronized long getNumBytesRead() {
+ IFFParser p = parser; // prevent race
+ if (p != null)
+ return p.getOffset();
+ else
+ return 0;
+ }
+
+ /**
+ * This can be read by another thread when load()ing a sample to determine how many bytes need
+ * to be read.
+ */
+ public synchronized long getFileSize() {
+ IFFParser p = parser; // prevent race
+ if (p != null)
+ return p.getFileSize();
+ else
+ return 0;
+ }
+
+ protected SampleMarker findOrCreateCuePoint(int uniqueID) {
+ SampleMarker cuePoint = cueMap.get(uniqueID);
+ if (cuePoint == null) {
+ cuePoint = new SampleMarker();
+ cueMap.put(uniqueID, cuePoint);
+ }
+ return cuePoint;
+ }
+
+ public FloatSample load(IFFParser parser) throws IOException {
+ this.parser = parser;
+ parser.parseAfterHead(this);
+ return finish();
+ }
+
+ abstract FloatSample finish() throws IOException;
+
+ FloatSample makeSample(float[] floatData) throws IOException {
+ FloatSample floatSample = new FloatSample(floatData, samplesPerFrame);
+
+ floatSample.setChannelsPerFrame(samplesPerFrame);
+ floatSample.setFrameRate(frameRate);
+ floatSample.setPitch(originalPitch);
+
+ if (sustainBegin >= 0) {
+ floatSample.setSustainBegin(sustainBegin);
+ floatSample.setSustainEnd(sustainEnd);
+ }
+
+ for (SampleMarker marker : cueMap.values()) {
+ floatSample.addMarker(marker);
+ }
+
+ /* Set Sustain Loop by assuming first two markers are loop points. */
+ if (floatSample.getMarkerCount() >= 2) {
+ floatSample.setSustainBegin(floatSample.getMarker(0).position);
+ floatSample.setSustainEnd(floatSample.getMarker(1).position);
+ }
+ return floatSample;
+ }
+
+ protected String parseString(IFFParser parser, int textLength) throws IOException {
+ byte[] bar = new byte[textLength];
+ parser.read(bar);
+ return new String(bar);
+ }
+}
diff --git a/src/main/java/com/jsyn/util/soundfile/ChunkHandler.java b/src/main/java/com/jsyn/util/soundfile/ChunkHandler.java
new file mode 100644
index 0000000..6dfe26d
--- /dev/null
+++ b/src/main/java/com/jsyn/util/soundfile/ChunkHandler.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util.soundfile;
+
+import java.io.IOException;
+
+/**
+ * Handle IFF Chunks as they are parsed from an IFF or RIFF file.
+ *
+ * @see IFFParser
+ * @see AudioSampleAIFF
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ */
+interface ChunkHandler {
+ /**
+ * The parser will call this when it encounters a FORM or LIST chunk that contains other chunks.
+ * This handler can either read the form's chunks, or let the parser find them and call
+ * handleChunk().
+ *
+ * @param ID a 4 byte identifier such as FORM_ID that identifies the IFF chunk type.
+ * @param numBytes number of bytes contained in the FORM, not counting the FORM type.
+ * @param type a 4 byte identifier such as AIFF_ID that identifies the FORM type.
+ */
+ public void handleForm(IFFParser parser, int ID, int numBytes, int type) throws IOException;
+
+ /**
+ * The parser will call this when it encounters a chunk that is not a FORM or LIST. This handler
+ * can either read the chunk's, or ignore it. The parser will skip over any unread data. Do NOT
+ * read past the end of the chunk!
+ *
+ * @param ID a 4 byte identifier such as SSND_ID that identifies the IFF chunk type.
+ * @param numBytes number of bytes contained in the chunk, not counting the ID and size field.
+ */
+ public void handleChunk(IFFParser parser, int ID, int numBytes) throws IOException;
+}
diff --git a/src/main/java/com/jsyn/util/soundfile/CustomSampleLoader.java b/src/main/java/com/jsyn/util/soundfile/CustomSampleLoader.java
new file mode 100644
index 0000000..14efde9
--- /dev/null
+++ b/src/main/java/com/jsyn/util/soundfile/CustomSampleLoader.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util.soundfile;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import com.jsyn.data.FloatSample;
+import com.jsyn.util.AudioSampleLoader;
+
+public class CustomSampleLoader implements AudioSampleLoader {
+
+ @Override
+ public FloatSample loadFloatSample(File fileIn) throws IOException {
+ FileInputStream fileStream = new FileInputStream(fileIn);
+ BufferedInputStream inputStream = new BufferedInputStream(fileStream);
+ return loadFloatSample(inputStream);
+ }
+
+ @Override
+ public FloatSample loadFloatSample(URL url) throws IOException {
+ InputStream rawStream = url.openStream();
+ BufferedInputStream inputStream = new BufferedInputStream(rawStream);
+ return loadFloatSample(inputStream);
+ }
+
+ @Override
+ public FloatSample loadFloatSample(InputStream inputStream) throws IOException {
+ AudioFileParser fileParser;
+ IFFParser parser = new IFFParser(inputStream);
+ parser.readHead();
+ if (parser.isRIFF()) {
+ fileParser = new WAVEFileParser();
+ } else if (parser.isIFF()) {
+ fileParser = new AIFFFileParser();
+ } else {
+ throw new IOException("Unsupported audio file type.");
+ }
+ return fileParser.load(parser);
+ }
+
+}
diff --git a/src/main/java/com/jsyn/util/soundfile/IFFParser.java b/src/main/java/com/jsyn/util/soundfile/IFFParser.java
new file mode 100644
index 0000000..9bb4ec3
--- /dev/null
+++ b/src/main/java/com/jsyn/util/soundfile/IFFParser.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util.soundfile;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.EOFException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parse Electronic Arts style IFF File. IFF is a file format that allows "chunks" of data to be
+ * placed in a hierarchical file. It was designed by Jerry Morrison at Electronic Arts for the Amiga
+ * computer and is now used extensively by Apple Computer and other companies. IFF is an open
+ * standard.
+ *
+ * @see RIFFParser
+ * @see AudioSampleAIFF
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ */
+
+class IFFParser extends FilterInputStream {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(IFFParser.class);
+
+ private long numBytesRead = 0;
+ private long totalSize = 0;
+ private int fileId;
+ static boolean debug = false;
+
+ public static final int RIFF_ID = ('R' << 24) | ('I' << 16) | ('F' << 8) | 'F';
+ public static final int LIST_ID = ('L' << 24) | ('I' << 16) | ('S' << 8) | 'T';
+ public static final int FORM_ID = ('F' << 24) | ('O' << 16) | ('R' << 8) | 'M';
+
+ IFFParser(InputStream stream) {
+ super(stream);
+ numBytesRead = 0;
+ }
+
+ /**
+ * Size of file based on outermost chunk size plus 8. Can be used to report progress when
+ * loading samples.
+ *
+ * @return Number of bytes in outer chunk plus header.
+ */
+ public long getFileSize() {
+ return totalSize;
+ }
+
+ /**
+ * Since IFF files use chunks with explicit size, it is important to keep track of how many
+ * bytes have been read from the file. Can be used to report progress when loading samples.
+ *
+ * @return Number of bytes read from stream, or skipped.
+ */
+ public long getOffset() {
+ return numBytesRead;
+ }
+
+ /** @return Next byte from stream. Increment offset by 1. */
+ @Override
+ public int read() throws IOException {
+ numBytesRead++;
+ return super.read();
+ }
+
+ /** @return Next byte array from stream. Increment offset by len. */
+ @Override
+ public int read(byte[] bar) throws IOException {
+ return read(bar, 0, bar.length);
+ }
+
+ /** @return Next byte array from stream. Increment offset by len. */
+ @Override
+ public int read(byte[] bar, int off, int len) throws IOException {
+ // Reading from a URL can return before all the bytes are available.
+ // So we keep reading until we get the whole thing.
+ int cursor = off;
+ int numLeft = len;
+ // keep reading data until we get it all
+ while (numLeft > 0) {
+ int numRead = super.read(bar, cursor, numLeft);
+ if (numRead < 0)
+ return numRead;
+ cursor += numRead;
+ numBytesRead += numRead;
+ numLeft -= numRead;
+ // LOGGER.debug("read " + numRead + ", cursor = " + cursor +
+ // ", len = " + len);
+ }
+ return cursor - off;
+ }
+
+ /** @return Skip forward in stream and add numBytes to offset. */
+ @Override
+ public long skip(long numBytes) throws IOException {
+ numBytesRead += numBytes;
+ return super.skip(numBytes);
+ }
+
+ /** Read 32 bit signed integer assuming Big Endian byte order. */
+ public int readIntBig() throws IOException {
+ int result = read() & 0xFF;
+ result = (result << 8) | (read() & 0xFF);
+ result = (result << 8) | (read() & 0xFF);
+ int data = read();
+ if (data == -1)
+ throw new EOFException("readIntBig() - EOF in middle of word at offset " + numBytesRead);
+ result = (result << 8) | (data & 0xFF);
+ return result;
+ }
+
+ /** Read 32 bit signed integer assuming Little Endian byte order. */
+ public int readIntLittle() throws IOException {
+ int result = read() & 0xFF; // LSB
+ result |= ((read() & 0xFF) << 8);
+ result |= ((read() & 0xFF) << 16);
+ int data = read();
+ if (data == -1)
+ throw new EOFException("readIntLittle() - EOF in middle of word at offset "
+ + numBytesRead);
+ result |= (data << 24);
+ return result;
+ }
+
+ /** Read 16 bit signed short assuming Big Endian byte order. */
+ public short readShortBig() throws IOException {
+ short result = (short) ((read() << 8)); // MSB
+ int data = read();
+ if (data == -1)
+ throw new EOFException("readShortBig() - EOF in middle of word at offset "
+ + numBytesRead);
+ result |= data & 0xFF;
+ return result;
+ }
+
+ /** Read 16 bit signed short assuming Little Endian byte order. */
+ public short readShortLittle() throws IOException {
+ short result = (short) (read() & 0xFF); // LSB
+ int data = read(); // MSB
+ if (data == -1)
+ throw new EOFException("readShortLittle() - EOF in middle of word at offset "
+ + numBytesRead);
+ result |= data << 8;
+ return result;
+ }
+
+ public int readUShortLittle() throws IOException {
+ return (readShortLittle()) & 0x0000FFFF;
+ }
+
+ /** Read 8 bit signed byte. */
+ public byte readByte() throws IOException {
+ return (byte) read();
+ }
+
+ /** Read 32 bit signed int assuming IFF order. */
+ public int readChunkSize() throws IOException {
+ if (isRIFF()) {
+ return readIntLittle();
+ }
+ {
+ return readIntBig();
+ }
+ }
+
+ /** Convert a 4 character IFF ID to a String */
+ public static String IDToString(int ID) {
+ byte bar[] = new byte[4];
+ bar[0] = (byte) (ID >> 24);
+ bar[1] = (byte) (ID >> 16);
+ bar[2] = (byte) (ID >> 8);
+ bar[3] = (byte) ID;
+ return new String(bar);
+ }
+
+ /**
+ * Parse the stream after reading the first ID and pass the forms and chunks to the ChunkHandler
+ */
+ public void parseAfterHead(ChunkHandler handler) throws IOException {
+ int numBytes = readChunkSize();
+ totalSize = numBytes + 8;
+ parseChunk(handler, fileId, numBytes);
+ if (debug)
+ LOGGER.debug("parse() ------- end");
+ }
+
+ /**
+ * Parse the FORM and pass the chunks to the ChunkHandler The cursor should be positioned right
+ * after the type field.
+ */
+ void parseForm(ChunkHandler handler, int ID, int numBytes, int type) throws IOException {
+ if (debug) {
+ LOGGER.debug("IFF: parseForm >>>>>>>>>>>>>>>>>> BEGIN");
+ }
+ while (numBytes > 8) {
+ int ckid = readIntBig();
+ int size = readChunkSize();
+ numBytes -= 8;
+ if (debug) {
+ LOGGER.debug("chunk( " + IDToString(ckid) + ", " + size + " )");
+ }
+ if (size < 0) {
+ throw new IOException("Bad IFF chunk Size: " + IDToString(ckid) + " = 0x"
+ + Integer.toHexString(ckid) + ", Size = " + size);
+ }
+ parseChunk(handler, ckid, size);
+ if ((size & 1) == 1)
+ size++; // even-up
+ numBytes -= size;
+ if (debug) {
+ LOGGER.debug("parseForm: numBytes left in form = " + numBytes);
+ }
+ }
+ if (debug) {
+ LOGGER.debug("IFF: parseForm <<<<<<<<<<<<<<<<<<<< END");
+ }
+
+ if (numBytes > 0) {
+ LOGGER.debug("IFF Parser detected " + numBytes
+ + " bytes of garbage at end of FORM.");
+ skip(numBytes);
+ }
+ }
+
+ /*
+ * Parse one chunk from IFF file. After calling handler, make sure stream is positioned at end
+ * of chunk.
+ */
+ void parseChunk(ChunkHandler handler, int ckid, int numBytes) throws IOException {
+ long startOffset, endOffset;
+ int numRead;
+ startOffset = getOffset();
+ if (isForm(ckid)) {
+ int type = readIntBig();
+ if (debug)
+ LOGGER.debug("parseChunk: form = " + IDToString(ckid) + ", " + numBytes
+ + ", " + IDToString(type));
+ handler.handleForm(this, ckid, numBytes - 4, type);
+ endOffset = getOffset();
+ numRead = (int) (endOffset - startOffset);
+ if (numRead < numBytes)
+ parseForm(handler, ckid, (numBytes - numRead), type);
+ } else {
+ handler.handleChunk(this, ckid, numBytes);
+ }
+ endOffset = getOffset();
+ numRead = (int) (endOffset - startOffset);
+ if (debug) {
+ LOGGER.debug("parseChunk: endOffset = " + endOffset);
+ LOGGER.debug("parseChunk: numRead = " + numRead);
+ }
+ if ((numBytes & 1) == 1)
+ numBytes++; // even-up
+ if (numRead < numBytes)
+ skip(numBytes - numRead);
+ }
+
+ public void readHead() throws IOException {
+ if (debug)
+ LOGGER.debug("parse() ------- begin");
+ numBytesRead = 0;
+ fileId = readIntBig();
+ }
+
+ public boolean isRIFF() {
+ return (fileId == RIFF_ID);
+ }
+
+ public boolean isIFF() {
+ return (fileId == FORM_ID);
+ }
+
+ /**
+ * Does the following chunk ID correspond to a container type like FORM?
+ */
+ public boolean isForm(int ckid) {
+ if (isRIFF()) {
+ switch (ckid) {
+ case LIST_ID:
+ case RIFF_ID:
+ return true;
+ default:
+ return false;
+ }
+ } else {
+ switch (ckid) {
+ case LIST_ID:
+ case FORM_ID:
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java b/src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java
new file mode 100644
index 0000000..a083961
--- /dev/null
+++ b/src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util.soundfile;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+import com.jsyn.data.FloatSample;
+import com.jsyn.data.SampleMarker;
+import com.jsyn.util.SampleLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class WAVEFileParser extends AudioFileParser implements ChunkHandler {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(WAVEFileParser.class);
+
+ static final short WAVE_FORMAT_PCM = 1;
+ static final short WAVE_FORMAT_IEEE_FLOAT = 3;
+ static final short WAVE_FORMAT_EXTENSIBLE = (short) 0xFFFE;
+
+ static final byte[] KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = {
+ 3, 0, 0, 0, 0, 0, 16, 0, -128, 0, 0, -86, 0, 56, -101, 113
+ };
+ static final byte[] KSDATAFORMAT_SUBTYPE_PCM = {
+ 1, 0, 0, 0, 0, 0, 16, 0, -128, 0, 0, -86, 0, 56, -101, 113
+ };
+
+ static final int WAVE_ID = ('W' << 24) | ('A' << 16) | ('V' << 8) | 'E';
+ static final int FMT_ID = ('f' << 24) | ('m' << 16) | ('t' << 8) | ' ';
+ static final int DATA_ID = ('d' << 24) | ('a' << 16) | ('t' << 8) | 'a';
+ static final int CUE_ID = ('c' << 24) | ('u' << 16) | ('e' << 8) | ' ';
+ static final int FACT_ID = ('f' << 24) | ('a' << 16) | ('c' << 8) | 't';
+ static final int SMPL_ID = ('s' << 24) | ('m' << 16) | ('p' << 8) | 'l';
+ static final int LTXT_ID = ('l' << 24) | ('t' << 16) | ('x' << 8) | 't';
+ static final int LABL_ID = ('l' << 24) | ('a' << 16) | ('b' << 8) | 'l';
+
+ int samplesPerBlock = 0;
+ int blockAlign = 0;
+ private int numFactSamples = 0;
+ private short format;
+
+ WAVEFileParser() {
+ }
+
+ @Override
+ FloatSample finish() throws IOException {
+ if ((byteData == null)) {
+ throw new IOException("No data found in audio sample.");
+ }
+ float[] floatData = new float[numFrames * samplesPerFrame];
+ if (bitsPerSample == 16) {
+ SampleLoader.decodeLittleI16ToF32(byteData, 0, byteData.length, floatData, 0);
+ } else if (bitsPerSample == 24) {
+ SampleLoader.decodeLittleI24ToF32(byteData, 0, byteData.length, floatData, 0);
+ } else if (bitsPerSample == 32) {
+ if (format == WAVE_FORMAT_IEEE_FLOAT) {
+ SampleLoader.decodeLittleF32ToF32(byteData, 0, byteData.length, floatData, 0);
+ } else if (format == WAVE_FORMAT_PCM) {
+ SampleLoader.decodeLittleI32ToF32(byteData, 0, byteData.length, floatData, 0);
+ } else {
+ throw new IOException("WAV: Unsupported format = " + format);
+ }
+ } else {
+ throw new IOException("WAV: Unsupported bitsPerSample = " + bitsPerSample);
+ }
+
+ return makeSample(floatData);
+ }
+
+ // typedef struct {
+ // long dwIdentifier;
+ // long dwPosition;
+ // ID fccChunk;
+ // long dwChunkStart;
+ // long dwBlockStart;
+ // long dwSampleOffset;
+ // } CuePoint;
+
+ /* Parse various chunks encountered in WAV file. */
+ void parseCueChunk(IFFParser parser, int ckSize) throws IOException {
+ int numCuePoints = parser.readIntLittle();
+ if (IFFParser.debug) {
+ LOGGER.debug("WAV: numCuePoints = " + numCuePoints);
+ }
+ if ((ckSize - 4) != (6 * 4 * numCuePoints))
+ throw new EOFException("Cue chunk too short!");
+ for (int i = 0; i < numCuePoints; i++) {
+ int dwName = parser.readIntLittle(); /* dwName */
+ int position = parser.readIntLittle(); // dwPosition
+ parser.skip(3 * 4); // fccChunk, dwChunkStart, dwBlockStart
+ int sampleOffset = parser.readIntLittle(); // dwPosition
+
+ if (IFFParser.debug) {
+ LOGGER.debug("WAV: parseCueChunk: #" + i + ", dwPosition = " + position
+ + ", dwName = " + dwName + ", dwSampleOffset = " + sampleOffset);
+ }
+ SampleMarker cuePoint = findOrCreateCuePoint(dwName);
+ cuePoint.position = position;
+ }
+ }
+
+ void parseLablChunk(IFFParser parser, int ckSize) throws IOException {
+ int dwName = parser.readIntLittle();
+ int textLength = (ckSize - 4) - 1; // don't read NUL terminator
+ String text = parseString(parser, textLength);
+ if (IFFParser.debug) {
+ LOGGER.debug("WAV: label id = " + dwName + ", text = " + text);
+ }
+ SampleMarker cuePoint = findOrCreateCuePoint(dwName);
+ cuePoint.name = text;
+ }
+
+ void parseLtxtChunk(IFFParser parser, int ckSize) throws IOException {
+ int dwName = parser.readIntLittle();
+ int dwSampleLength = parser.readIntLittle();
+ parser.skip(4 + (4 * 2)); // purpose through codepage
+ int textLength = (ckSize - ((4 * 4) + (4 * 2))) - 1; // don't read NUL
+ // terminator
+ if (textLength > 0) {
+ String text = parseString(parser, textLength);
+ if (IFFParser.debug) {
+ LOGGER.debug("WAV: ltxt id = " + dwName + ", dwSampleLength = "
+ + dwSampleLength + ", text = " + text);
+ }
+ SampleMarker cuePoint = findOrCreateCuePoint(dwName);
+ cuePoint.comment = text;
+ }
+ }
+
+ void parseFmtChunk(IFFParser parser, int ckSize) throws IOException {
+ format = parser.readShortLittle();
+ samplesPerFrame = parser.readShortLittle();
+ frameRate = parser.readIntLittle();
+ parser.readIntLittle(); /* skip dwAvgBytesPerSec */
+ blockAlign = parser.readShortLittle();
+ bitsPerSample = parser.readShortLittle();
+
+ if (IFFParser.debug) {
+ LOGGER.debug("WAV: format = 0x" + Integer.toHexString(format));
+ LOGGER.debug("WAV: bitsPerSample = " + bitsPerSample);
+ LOGGER.debug("WAV: samplesPerFrame = " + samplesPerFrame);
+ }
+ bytesPerFrame = blockAlign;
+ bytesPerSample = bytesPerFrame / samplesPerFrame;
+ samplesPerBlock = (8 * blockAlign) / bitsPerSample;
+
+ if (format == WAVE_FORMAT_EXTENSIBLE) {
+ int extraSize = parser.readShortLittle();
+ short validBitsPerSample = parser.readShortLittle();
+ int channelMask = parser.readIntLittle();
+ byte[] guid = new byte[16];
+ parser.read(guid);
+ if (IFFParser.debug) {
+ LOGGER.debug("WAV: extraSize = " + extraSize);
+ LOGGER.debug("WAV: validBitsPerSample = " + validBitsPerSample);
+ LOGGER.debug("WAV: channelMask = " + channelMask);
+ System.out.print("guid = {");
+ for (int i = 0; i < guid.length; i++) {
+ System.out.print(guid[i] + ", ");
+ }
+ LOGGER.debug("}");
+ }
+ if (matchBytes(guid, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) {
+ format = WAVE_FORMAT_IEEE_FLOAT;
+ } else if (matchBytes(guid, KSDATAFORMAT_SUBTYPE_PCM)) {
+ format = WAVE_FORMAT_PCM;
+ }
+ }
+ if ((format != WAVE_FORMAT_PCM) && (format != WAVE_FORMAT_IEEE_FLOAT)) {
+ throw new IOException(
+ "Only WAVE_FORMAT_PCM and WAVE_FORMAT_IEEE_FLOAT supported. format = " + format);
+ }
+ if ((bitsPerSample != 16) && (bitsPerSample != 24) && (bitsPerSample != 32)) {
+ throw new IOException(
+ "Only 16 and 24 bit PCM or 32-bit float WAV files supported. width = "
+ + bitsPerSample);
+ }
+ }
+
+ private boolean matchBytes(byte[] bar1, byte[] bar2) {
+ if (bar1.length != bar2.length)
+ return false;
+ for (int i = 0; i < bar1.length; i++) {
+ if (bar1[i] != bar2[i])
+ return false;
+ }
+ return true;
+ }
+
+ private int convertByteToFrame(int byteOffset) throws IOException {
+ if (blockAlign == 0) {
+ throw new IOException("WAV file has bytesPerBlock = zero");
+ }
+ if (samplesPerFrame == 0) {
+ throw new IOException("WAV file has samplesPerFrame = zero");
+ }
+ return (samplesPerBlock * byteOffset) / (samplesPerFrame * blockAlign);
+ }
+
+ private int calculateNumFrames(int numBytes) throws IOException {
+ int nFrames;
+ if (numFactSamples > 0) {
+ // nFrames = numFactSamples / samplesPerFrame;
+ nFrames = numFactSamples; // FIXME which is right
+ } else {
+ nFrames = convertByteToFrame(numBytes);
+ }
+ return nFrames;
+ }
+
+ // Read fraction in range of 0 to 0xFFFFFFFF and
+ // convert to 0.0 to 1.0 range.
+ private double readFraction(IFFParser parser) throws IOException {
+ // Put L at end or we get -1.
+ long maxFraction = 0x0FFFFFFFFL;
+ // Get unsigned fraction. Have to fit in long.
+ long fraction = (parser.readIntLittle()) & maxFraction;
+ return (double) fraction / (double) maxFraction;
+ }
+
+ void parseSmplChunk(IFFParser parser, int ckSize) throws IOException {
+ parser.readIntLittle(); // Manufacturer
+ parser.readIntLittle(); // Product
+ parser.readIntLittle(); // Sample Period
+ int unityNote = parser.readIntLittle();
+ double pitchFraction = readFraction(parser);
+ originalPitch = unityNote + pitchFraction;
+
+ parser.readIntLittle(); // SMPTE Format
+ parser.readIntLittle(); // SMPTE Offset
+ int numLoops = parser.readIntLittle();
+ parser.readIntLittle(); // Sampler Data
+
+ int lastCueID = Integer.MAX_VALUE;
+ for (int i = 0; i < numLoops; i++) {
+ int cueID = parser.readIntLittle();
+ parser.readIntLittle(); // type
+ int loopStartPosition = parser.readIntLittle();
+ // Point to sample one after.
+ int loopEndPosition = parser.readIntLittle() + 1;
+ // TODO handle fractional loop sizes?
+ double endFraction = readFraction(parser);
+ parser.readIntLittle(); // playCount
+
+ // Use lowest numbered cue.
+ if (cueID < lastCueID) {
+ sustainBegin = loopStartPosition;
+ sustainEnd = loopEndPosition;
+ }
+ }
+ }
+
+ void parseFactChunk(IFFParser parser, int ckSize) throws IOException {
+ numFactSamples = parser.readIntLittle();
+ }
+
+ void parseDataChunk(IFFParser parser, int ckSize) throws IOException {
+ long numRead;
+ dataPosition = parser.getOffset();
+ if (ifLoadData) {
+ byteData = new byte[ckSize];
+ numRead = parser.read(byteData);
+ } else {
+ numRead = parser.skip(ckSize);
+ }
+ if (numRead != ckSize) {
+ throw new EOFException("WAV data chunk too short! Read " + numRead + " instead of "
+ + ckSize);
+ }
+ numFrames = calculateNumFrames(ckSize);
+ }
+
+ @Override
+ public void handleForm(IFFParser parser, int ckID, int ckSize, int type) throws IOException {
+ if ((ckID == IFFParser.RIFF_ID) && (type != WAVE_ID))
+ throw new IOException("Bad WAV form type = " + IFFParser.IDToString(type));
+ }
+
+ /**
+ * Called by parse() method to handle chunks in a WAV specific manner.
+ *
+ * @param ckID four byte chunk ID such as 'data'
+ * @param ckSize size of chunk in bytes
+ * @return number of bytes left in chunk
+ */
+ @Override
+ public void handleChunk(IFFParser parser, int ckID, int ckSize) throws IOException {
+ switch (ckID) {
+ case FMT_ID:
+ parseFmtChunk(parser, ckSize);
+ break;
+ case DATA_ID:
+ parseDataChunk(parser, ckSize);
+ break;
+ case CUE_ID:
+ parseCueChunk(parser, ckSize);
+ break;
+ case FACT_ID:
+ parseFactChunk(parser, ckSize);
+ break;
+ case SMPL_ID:
+ parseSmplChunk(parser, ckSize);
+ break;
+ case LABL_ID:
+ parseLablChunk(parser, ckSize);
+ break;
+ case LTXT_ID:
+ parseLtxtChunk(parser, ckSize);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.softsynth.javasonics.util.AudioSampleLoader#isLittleEndian()
+ */
+ boolean isLittleEndian() {
+ return true;
+ }
+
+}