diff options
author | RubbaBoy <[email protected]> | 2020-07-06 02:33:28 -0400 |
---|---|---|
committer | Phil Burk <[email protected]> | 2020-10-30 11:19:34 -0700 |
commit | 46888fae6eb7b1dd386f7af7d101ead99ae61981 (patch) | |
tree | 8969bbfd68d2fb5c0d8b86da49ec2eca230a72ab /src/main/java/com/jsyn/util | |
parent | c51e92e813dd481603de078f0778e1f75db2ab05 (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')
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 -> pitchToLinear -> [VOICES] -> volume* -> 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; + } + +} |