diff options
Diffstat (limited to 'src/main/java')
258 files changed, 26076 insertions, 0 deletions
diff --git a/src/main/java/com/jsyn/JSyn.java b/src/main/java/com/jsyn/JSyn.java new file mode 100644 index 0000000..bbc2891 --- /dev/null +++ b/src/main/java/com/jsyn/JSyn.java @@ -0,0 +1,78 @@ +/* + * 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; + +import java.sql.Date; +import java.util.GregorianCalendar; + +import com.jsyn.devices.AudioDeviceManager; +import com.jsyn.engine.SynthesisEngine; + +/** + * JSyn Synthesizer for Java. Use this factory class to create a synthesizer. This code demonstrates + * how to start playing a sine wave: + * + * <pre><code> + // Create a context for the synthesizer. + synth = JSyn.createSynthesizer(); + + // Start synthesizer using default stereo output at 44100 Hz. + synth.start(); + + // Add a tone generator. + synth.add( osc = new SineOscillator() ); + // Add a stereo audio output unit. + synth.add( lineOut = new LineOut() ); + + // Connect the oscillator to both channels of the output. + osc.output.connect( 0, lineOut.input, 0 ); + osc.output.connect( 0, lineOut.input, 1 ); + + // Set the frequency and amplitude for the sine wave. + osc.frequency.set( 345.0 ); + osc.amplitude.set( 0.6 ); + + // We only need to start the LineOut. It will pull data from the oscillator. + lineOut.start(); +</code> </pre> + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class JSyn { + // Update these for every release. + private final static int VERSION_MAJOR = 16; + private final static int VERSION_MINOR = 8; + private final static int VERSION_REVISION = 1; + public final static int BUILD_NUMBER = 464; + private final static long BUILD_TIME = new GregorianCalendar(2017, + GregorianCalendar.OCTOBER, 16).getTime().getTime(); + + public final static String VERSION = VERSION_MAJOR + "." + VERSION_MINOR + "." + + VERSION_REVISION; + public final static int VERSION_CODE = (VERSION_MAJOR << 16) + (VERSION_MINOR << 8) + + VERSION_REVISION; + public final static String VERSION_TEXT = "V" + VERSION + " (build " + BUILD_NUMBER + ", " + + (new Date(BUILD_TIME)) + ")"; + + public static Synthesizer createSynthesizer() { + return new SynthesisEngine(); + } + + public static Synthesizer createSynthesizer(AudioDeviceManager audioDeviceManager) { + return new SynthesisEngine(audioDeviceManager); + } +} diff --git a/src/main/java/com/jsyn/Synthesizer.java b/src/main/java/com/jsyn/Synthesizer.java new file mode 100644 index 0000000..bfabb4c --- /dev/null +++ b/src/main/java/com/jsyn/Synthesizer.java @@ -0,0 +1,202 @@ +/* + * 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; + +import com.jsyn.devices.AudioDeviceManager; +import com.jsyn.unitgen.UnitGenerator; +import com.softsynth.shared.time.ScheduledCommand; +import com.softsynth.shared.time.TimeStamp; + +/** + * A synthesizer used by JSyn to generate audio. The synthesizer executes a network of unit + * generators to create an audio signal. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public interface Synthesizer { + + public final static int FRAMES_PER_BLOCK = 8; + + /** + * Starts a background thread that generates audio using the default frame rate of 44100 and two + * stereo output channels. + */ + public void start(); + + /** + * Starts a background thread that generates audio using the specified frame rate and two stereo + * output channels. + * + * @param frameRate in Hertz + */ + public void start(int frameRate); + + /** + * Starts the synthesizer using specific audio devices. + * <p> + * Note that using more than 2 channels will probably require the use of JPortAudio because + * JavaSound currently does not support more than two channels. + * JPortAudio is available at + * <a href="http://www.softsynth.com/jsyn/developers/download.php">http://www.softsynth.com/jsyn/developers/download.php</a>. + * <p> + * If you use more than 2 inputs or outputs then you will probably want to use {@link com.jsyn.unitgen.ChannelIn} + * or {@link com.jsyn.unitgen.ChannelOut}, which can be associated with any indexed channel. + * + * @param frameRate in Hertz + * @param inputDeviceID obtained from an {@link AudioDeviceManager} or pass + * AudioDeviceManager.USE_DEFAULT_DEVICE + * @param numInputChannels 0 for no input, 1 for mono, 2 for stereo, etcetera + * @param ouputDeviceID obtained from an AudioDeviceManager or pass + * AudioDeviceManager.USE_DEFAULT_DEVICE + * @param numOutputChannels 0 for no output, 1 for mono, 2 for stereo, etcetera + */ + public void start(int frameRate, int inputDeviceID, int numInputChannels, int ouputDeviceID, + int numOutputChannels); + + /** @return JSyn version as a string */ + public String getVersion(); + + /** @return version as an integer that always increases */ + public int getVersionCode(); + + /** Stops the background thread that generates the audio. */ + public void stop(); + + /** + * An AudioDeviceManager is an interface to audio hardware. It might be implemented using + * JavaSound or a wrapper around PortAudio. + * + * @return audio device manager being used by the synthesizer. + */ + public AudioDeviceManager getAudioDeviceManager(); + + /** @return the frame rate in samples per second */ + public int getFrameRate(); + + /** + * Add a unit generator to the synthesizer so it can be played. This is required before starting + * or connecting a unit generator into a network. + * + * @param ugen a unit generator to be executed by the synthesizer + */ + public void add(UnitGenerator ugen); + + /** Removes a unit generator added using add(). */ + public void remove(UnitGenerator ugen); + + /** @return the current audio time in seconds */ + public double getCurrentTime(); + + /** + * Start a unit generator at the specified time. This is not needed if a unit generator's output + * is connected to other units. Typically you only need to start units that have no outputs, for + * example LineOut or ChannelOut. + */ + public void startUnit(UnitGenerator unit, double time); + + public void startUnit(UnitGenerator unit, TimeStamp timeStamp); + + /** + * The startUnit and stopUnit methods are mainly for internal use. + * Please call unit.start() or unit.stop() instead. + * @param unit + */ + public void startUnit(UnitGenerator unit); + + public void stopUnit(UnitGenerator unit, double time); + + public void stopUnit(UnitGenerator unit, TimeStamp timeStamp); + + /** + * The startUnit and stopUnit methods are mainly for internal use. + * Please call unit.start() or unit.stop() instead. + * @param unit + */ + public void stopUnit(UnitGenerator unit); + + /** + * Sleep until the specified audio time is reached. In non-real-time mode, this will enable the + * synthesizer to run. + */ + public void sleepUntil(double time) throws InterruptedException; + + /** + * Sleep for the specified audio time duration. In non-real-time mode, this will enable the + * synthesizer to run. + */ + public void sleepFor(double duration) throws InterruptedException; + + /** + * If set true then the synthesizer will generate audio in real-time. Set it true for live + * audio. If false then JSyn will run in non-real-time mode. This can be used to generate audio + * to be written to a file. The default is true. + * + * @param realTime + */ + public void setRealTime(boolean realTime); + + /** Is JSyn running in real-time mode? */ + public boolean isRealTime(); + + /** Create a TimeStamp using the current audio time. */ + public TimeStamp createTimeStamp(); + + /** @return the current CPU usage as a fraction between 0.0 and 1.0 */ + public double getUsage(); + + /** @return inverse of frameRate, to avoid expensive divides */ + public double getFramePeriod(); + + /** + * This count is not reset if you stop and restart. + * + * @return number of frames synthesized + */ + public long getFrameCount(); + + /** Queue a command to be processed at a specific time in the background audio thread. */ + public void scheduleCommand(TimeStamp timeStamp, ScheduledCommand command); + + /** Queue a command to be processed at a specific time in the background audio thread. */ + public void scheduleCommand(double time, ScheduledCommand command); + + /** Queue a command to be processed as soon as possible in the background audio thread. */ + public void queueCommand(ScheduledCommand command); + + /** + * Clear all scheduled commands from the queue. + * Commands will be discarded. + */ + public void clearCommandQueue(); + + /** + * @return true if the Synthesizer has been started + */ + public boolean isRunning(); + + /** + * Add a task that will be run repeatedly on the Audio Thread before it generates every new block of Audio. + * This task must be very quick and should not perform any blocking operations. If you are not + * certain that you need an Audio rate task then don't use this. + * + * @param task + */ + public void addAudioTask(Runnable task); + + public void removeAudioTask(Runnable task); + +} diff --git a/src/main/java/com/jsyn/apps/AboutJSyn.java b/src/main/java/com/jsyn/apps/AboutJSyn.java new file mode 100644 index 0000000..0624591 --- /dev/null +++ b/src/main/java/com/jsyn/apps/AboutJSyn.java @@ -0,0 +1,114 @@ +/* + * 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.apps; + +import java.awt.GridLayout; + +import javax.swing.JApplet; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; + +import com.jsyn.JSyn; +import com.jsyn.Synthesizer; +import com.jsyn.swing.JAppletFrame; +import com.jsyn.swing.PortControllerFactory; +import com.jsyn.unitgen.LineOut; +import com.jsyn.unitgen.LinearRamp; +import com.jsyn.unitgen.SineOscillator; +import com.jsyn.unitgen.UnitOscillator; + +/** + * Show the version of JSyn and play some sine waves. This program will be run if you double click + * the JSyn jar file. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public class AboutJSyn extends JApplet { + private static final long serialVersionUID = -2704222221111608377L; + private Synthesizer synth; + private UnitOscillator osc1; + private UnitOscillator osc2; + private LinearRamp lag; + private LineOut lineOut; + + @Override + public void init() { + synth = JSyn.createSynthesizer(); + + // Add a tone generator. + synth.add(osc1 = new SineOscillator()); + synth.add(osc2 = new SineOscillator()); + // Add a lag to smooth out amplitude changes and avoid pops. + synth.add(lag = new LinearRamp()); + // Add an output mixer. + synth.add(lineOut = new LineOut()); + // Connect the oscillator to the output. + osc1.output.connect(0, lineOut.input, 0); + osc2.output.connect(0, lineOut.input, 1); + + // Arrange the faders in a stack. + setLayout(new GridLayout(0, 1)); + + JPanel infoPanel = new JPanel(); + infoPanel.setLayout(new GridLayout(0, 1)); + infoPanel.add(new JLabel("About: " + synth, SwingConstants.CENTER)); + infoPanel.add(new JLabel("From: http://www.softsynth.com/", SwingConstants.CENTER)); + infoPanel.add(new JLabel("(C) 1997-2011 Mobileer Inc", SwingConstants.CENTER)); + add(infoPanel); + + // Set the minimum, current and maximum values for the port. + lag.output.connect(osc1.amplitude); + lag.output.connect(osc2.amplitude); + lag.input.setup(0.001, 0.5, 1.0); + lag.time.set(0.1); + lag.input.setName("Amplitude"); + add(PortControllerFactory.createExponentialPortSlider(lag.input)); + + osc1.frequency.setup(50.0, 300.0, 3000.0); + osc1.frequency.setName("Frequency (Left)"); + add(PortControllerFactory.createExponentialPortSlider(osc1.frequency)); + osc2.frequency.setup(50.0, 302.0, 3000.0); + osc2.frequency.setName("Frequency (Right)"); + add(PortControllerFactory.createExponentialPortSlider(osc2.frequency)); + validate(); + } + + @Override + public void start() { + // Start synthesizer using default stereo output at 44100 Hz. + synth.start(); + // We only need to start the LineOut. It will pull data from the + // oscillator. + lineOut.start(); + } + + @Override + public void stop() { + synth.stop(); + } + + /* Can be run as either an application or as an applet. */ + public static void main(String[] args) { + AboutJSyn applet = new AboutJSyn(); + JAppletFrame frame = new JAppletFrame("About JSyn", applet); + frame.setSize(440, 300); + frame.setVisible(true); + frame.test(); + } + +} diff --git a/src/main/java/com/jsyn/apps/InstrumentTester.java b/src/main/java/com/jsyn/apps/InstrumentTester.java new file mode 100644 index 0000000..2505759 --- /dev/null +++ b/src/main/java/com/jsyn/apps/InstrumentTester.java @@ -0,0 +1,210 @@ +/* + * Copyright 2012 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.apps; + +import java.awt.BorderLayout; +import java.io.IOException; + +import javax.sound.midi.MidiDevice; +import javax.sound.midi.MidiMessage; +import javax.sound.midi.MidiUnavailableException; +import javax.sound.midi.Receiver; +import javax.swing.JApplet; + +import com.jsyn.JSyn; +import com.jsyn.Synthesizer; +import com.jsyn.devices.javasound.MidiDeviceTools; +import com.jsyn.instruments.JSynInstrumentLibrary; +import com.jsyn.midi.MessageParser; +import com.jsyn.swing.InstrumentBrowser; +import com.jsyn.swing.JAppletFrame; +import com.jsyn.swing.PresetSelectionListener; +import com.jsyn.swing.SoundTweaker; +import com.jsyn.unitgen.LineOut; +import com.jsyn.unitgen.UnitSource; +import com.jsyn.unitgen.UnitVoice; +import com.jsyn.util.PolyphonicInstrument; +import com.jsyn.util.VoiceDescription; +import com.softsynth.math.AudioMath; +import com.softsynth.shared.time.TimeStamp; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Let the user select an instrument using the InstrumentBrowser and play + * them using the ASCII keyboard or with MIDI. + * Sound parameters can be tweaked using faders. + * + * @author Phil Burk (C) 2012 Mobileer Inc + */ +public class InstrumentTester extends JApplet { + + private static final Logger LOGGER = LoggerFactory.getLogger(InstrumentTester.class); + private static final long serialVersionUID = -2704222221111608377L; + + private Synthesizer synth; + private LineOut lineOut; + private SoundTweaker tweaker; + protected PolyphonicInstrument instrument; + private MyParser messageParser; + + class MyParser extends MessageParser { + + @Override + public void controlChange(int channel, int index, int value) { + } + + @Override + public void noteOff(int channel, int noteNumber, int velocity) { + instrument.noteOff(noteNumber, synth.createTimeStamp()); + } + + @Override + public void noteOn(int channel, int noteNumber, int velocity) { + double frequency = AudioMath.pitchToFrequency(noteNumber); + double amplitude = velocity / (4 * 128.0); + TimeStamp timeStamp = synth.createTimeStamp(); + instrument.noteOn(noteNumber, frequency, amplitude, timeStamp); + } + + } + + // Write a Receiver to get the messages from a Transmitter. + class CustomReceiver implements Receiver { + @Override + public void close() { + System.out.print("Closed."); + } + + @Override + public void send(MidiMessage message, long timeStamp) { + byte[] bytes = message.getMessage(); + messageParser.parse(bytes); + } + } + + public int setupMidiKeyboard() throws MidiUnavailableException, IOException, InterruptedException { + messageParser = new MyParser(); + + int result = 2; + MidiDevice keyboard = MidiDeviceTools.findKeyboard(); + Receiver receiver = new CustomReceiver(); + // Just use default synthesizer. + if (keyboard != null) { + // If you forget to open them you will hear no sound. + keyboard.open(); + // Put the receiver in the transmitter. + // This gives fairly low latency playing. + keyboard.getTransmitter().setReceiver(receiver); + LOGGER.debug("Play MIDI keyboard: " + keyboard.getDeviceInfo().getDescription()); + result = 0; + } else { + LOGGER.debug("Could not find a keyboard."); + } + return result; + } + + @Override + public void init() { + setLayout(new BorderLayout()); + + synth = JSyn.createSynthesizer(); + synth.add(lineOut = new LineOut()); + + InstrumentBrowser browser = new InstrumentBrowser(new JSynInstrumentLibrary()); + browser.addPresetSelectionListener(new PresetSelectionListener() { + + @Override + public void presetSelected(VoiceDescription voiceDescription, int presetIndex) { + UnitVoice[] voices = new UnitVoice[8]; + for (int i = 0; i < voices.length; i++) { + voices[i] = voiceDescription.createUnitVoice(); + } + instrument = new PolyphonicInstrument(voices); + synth.add(instrument); + instrument.usePreset(presetIndex, synth.createTimeStamp()); + String title = voiceDescription.getVoiceClassName() + ": " + + voiceDescription.getPresetNames()[presetIndex]; + useSource(instrument, title); + } + }); + add(browser, BorderLayout.NORTH); + + try { + setupMidiKeyboard(); + } catch (MidiUnavailableException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + validate(); + } + + private void useSource(UnitSource voice, String title) { + + lineOut.input.disconnectAll(0); + lineOut.input.disconnectAll(1); + + // Connect the source to both left and right output. + voice.getOutput().connect(0, lineOut.input, 0); + voice.getOutput().connect(0, lineOut.input, 1); + + if (tweaker != null) { + remove(tweaker); + } + try { + if (synth.isRunning()) { + synth.sleepFor(0.1); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + tweaker = new SoundTweaker(synth, title, voice); + add(tweaker, BorderLayout.CENTER); + validate(); + } + + @Override + public void start() { + // Start synthesizer using default stereo output at 44100 Hz. + synth.start(); + // We only need to start the LineOut. It will pull data from the + // oscillator. + lineOut.start(); + } + + @Override + public void stop() { + synth.stop(); + } + + /* Can be run as either an application or as an applet. */ + public static void main(String[] args) { + InstrumentTester applet = new InstrumentTester(); + JAppletFrame frame = new JAppletFrame("InstrumentTester", applet); + frame.setSize(600, 800); + frame.setVisible(true); + frame.test(); + } + +} diff --git a/src/main/java/com/jsyn/data/AudioSample.java b/src/main/java/com/jsyn/data/AudioSample.java new file mode 100644 index 0000000..dcbbae5 --- /dev/null +++ b/src/main/java/com/jsyn/data/AudioSample.java @@ -0,0 +1,108 @@ +/* + * 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.data; + +import java.util.ArrayList; + +/** + * Base class for FloatSample and ShortSample. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public abstract class AudioSample extends SequentialDataCommon { + protected int numFrames; + protected int channelsPerFrame = 1; + private double frameRate = 44100.0; + private double pitch; + private ArrayList<SampleMarker> markers; + + public abstract void allocate(int numFrames, int channelsPerFrame); + + @Override + public double getRateScaler(int index, double synthesisRate) { + return 1.0; + } + + public double getFrameRate() { + return frameRate; + } + + public void setFrameRate(double f) { + this.frameRate = f; + } + + @Override + public int getNumFrames() { + return numFrames; + } + + @Override + public int getChannelsPerFrame() { + return channelsPerFrame; + } + + public void setChannelsPerFrame(int channelsPerFrame) { + this.channelsPerFrame = channelsPerFrame; + } + + /** + * Set the recorded pitch as a fractional MIDI semitone value where 60 is Middle C. + * + * @param pitch + */ + public void setPitch(double pitch) { + this.pitch = pitch; + } + + public double getPitch() { + return pitch; + } + + public int getMarkerCount() { + if (markers == null) + return 0; + else + return markers.size(); + } + + public SampleMarker getMarker(int index) { + if (markers == null) + return null; + else + return markers.get(index); + } + + /** + * Add a marker that will be stored sorted by position. This is normally used internally by the + * SampleLoader. + * + * @param marker + */ + public void addMarker(SampleMarker marker) { + if (markers == null) + markers = new ArrayList<SampleMarker>(); + int idx = markers.size(); + for (int k = 0; k < markers.size(); k++) { + SampleMarker cue = markers.get(k); + if (cue.position > marker.position) { + idx = k; + break; + } + } + markers.add(idx, marker); + } +} diff --git a/src/main/java/com/jsyn/data/DoubleTable.java b/src/main/java/com/jsyn/data/DoubleTable.java new file mode 100644 index 0000000..ca64c94 --- /dev/null +++ b/src/main/java/com/jsyn/data/DoubleTable.java @@ -0,0 +1,109 @@ +/* + * 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.data; + +import com.jsyn.exceptions.ChannelMismatchException; + +/** + * Evaluate a Function by interpolating between the values in a table. This can be used for + * wavetable lookup or waveshaping. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class DoubleTable implements Function { + private double[] table; + + public DoubleTable(int numFrames) { + allocate(numFrames); + } + + public DoubleTable(double[] data) { + allocate(data.length); + write(data); + } + + public DoubleTable(ShortSample shortSample) { + if (shortSample.getChannelsPerFrame() != 1) { + throw new ChannelMismatchException("DoubleTable can only be built from mono samples."); + } + short[] buffer = new short[256]; + int framesLeft = shortSample.getNumFrames(); + allocate(framesLeft); + int cursor = 0; + while (framesLeft > 0) { + int numTransfer = framesLeft; + if (numTransfer > buffer.length) { + numTransfer = buffer.length; + } + shortSample.read(cursor, buffer, 0, numTransfer); + write(cursor, buffer, 0, numTransfer); + cursor += numTransfer; + framesLeft -= numTransfer; + } + } + + public void allocate(int numFrames) { + table = new double[numFrames]; + } + + public int length() { + return table.length; + } + + public void write(double[] data) { + write(0, data, 0, data.length); + } + + public void write(int startFrame, short[] data, int startIndex, int numFrames) { + for (int i = 0; i < numFrames; i++) { + table[startFrame + i] = data[startIndex + i] * (1.0 / 32768.0); + } + } + + public void write(int startFrame, double[] data, int startIndex, int numFrames) { + for (int i = 0; i < numFrames; i++) { + table[startFrame + i] = data[startIndex + i]; + } + } + + /** + * Treat the double array as a lookup table with a domain of -1.0 to 1.0. If the input is out of + * range then the output will clip to the end values. + * + * @param input + * @return interpolated value from table + */ + @Override + public double evaluate(double input) { + double interp; + if (input < -1.0) { + interp = table[0]; + } else if (input < 1.0) { + double fractionalIndex = (table.length - 1) * (input - (-1.0)) / 2.0; + // We don't need floor() because fractionalIndex >= 0.0 + int index = (int) fractionalIndex; + double fraction = fractionalIndex - index; + + double s1 = table[index]; + double s2 = table[index + 1]; + interp = ((s2 - s1) * fraction) + s1; + } else { + interp = table[table.length - 1]; + } + return interp; + } +} diff --git a/src/main/java/com/jsyn/data/FloatSample.java b/src/main/java/com/jsyn/data/FloatSample.java new file mode 100644 index 0000000..2d8c973 --- /dev/null +++ b/src/main/java/com/jsyn/data/FloatSample.java @@ -0,0 +1,164 @@ +/* + * 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.data; + +import com.jsyn.unitgen.FixedRateMonoReader; +import com.jsyn.unitgen.FixedRateStereoReader; +import com.jsyn.unitgen.VariableRateMonoReader; +import com.jsyn.unitgen.VariableRateStereoReader; +import com.jsyn.util.SampleLoader; + +/** + * Store multi-channel floating point audio data in an interleaved buffer. The values are stored as + * 32-bit floats. You can play samples using one of the readers, for example VariableRateMonoReader. + * + * @author Phil Burk (C) 2010 Mobileer Inc + * @see SampleLoader + * @see FixedRateMonoReader + * @see FixedRateStereoReader + * @see VariableRateMonoReader + * @see VariableRateStereoReader + */ +public class FloatSample extends AudioSample implements Function { + private float[] buffer; + + public FloatSample() { + } + + /** Constructor for mono samples. */ + public FloatSample(int numFrames) { + this(numFrames, 1); + } + + /** Constructor for mono samples with data. */ + public FloatSample(float[] data) { + this(data.length, 1); + write(data); + } + + /** Constructor for multi-channel samples with data. */ + public FloatSample(float[] data, int channelsPerFrame) { + this(data.length / channelsPerFrame, channelsPerFrame); + write(data); + } + + /** + * Create a silent sample with enough memory to hold the audio data. The number of sample + * numbers in the array will be numFrames*channelsPerFrame. + * + * @param numFrames number of sample groups. A stereo frame contains 2 samples. + * @param channelsPerFrame 1 for mono, 2 for stereo + */ + public FloatSample(int numFrames, int channelsPerFrame) { + allocate(numFrames, channelsPerFrame); + } + + /** + * Allocate memory to hold the audio data. The number of sample numbers in the array will be + * numFrames*channelsPerFrame. + * + * @param numFrames number of sample groups. A stereo frame contains 2 samples. + * @param channelsPerFrame 1 for mono, 2 for stereo + */ + @Override + public void allocate(int numFrames, int channelsPerFrame) { + buffer = new float[numFrames * channelsPerFrame]; + this.numFrames = numFrames; + this.channelsPerFrame = channelsPerFrame; + } + + /** + * Note that in a stereo sample, a frame has two values. + * + * @param startFrame index of frame in the sample + * @param data data to be written + * @param startIndex index of first value in array + * @param numFrames + */ + public void write(int startFrame, float[] data, int startIndex, int numFrames) { + int numSamplesToWrite = numFrames * channelsPerFrame; + int firstSampleIndexToWrite = startFrame * channelsPerFrame; + System.arraycopy(data, startIndex, buffer, firstSampleIndexToWrite, numSamplesToWrite); + } + + /** + * Note that in a stereo sample, a frame has two values. + * + * @param startFrame index of frame in the sample + * @param data array to receive the data from the sample + * @param startIndex index of first location in array to start filling + * @param numFrames + */ + public void read(int startFrame, float[] data, int startIndex, int numFrames) { + int numSamplesToRead = numFrames * channelsPerFrame; + int firstSampleIndexToRead = startFrame * channelsPerFrame; + System.arraycopy(buffer, firstSampleIndexToRead, data, startIndex, numSamplesToRead); + } + + /** + * Write the entire array to the sample. The sample data must have already been allocated with + * enough room to contain the data. + * + * @param data + */ + public void write(float[] data) { + write(0, data, 0, data.length / getChannelsPerFrame()); + } + + public void read(float[] data) { + read(0, data, 0, data.length / getChannelsPerFrame()); + } + + @Override + public double readDouble(int index) { + return buffer[index]; + } + + @Override + public void writeDouble(int index, double value) { + buffer[index] = (float) value; + } + + /** + * Interpolate between two adjacent samples. + * Note that this will only work for mono, single channel samples. + * + * @param fractionalIndex must be >=0 and < (size-1) + */ + public double interpolate(double fractionalIndex) { + int index = (int) fractionalIndex; + float phase = (float) (fractionalIndex - index); + float source = buffer[index]; + float target = buffer[index + 1]; + return ((target - source) * phase) + source; + } + + /** + * Note that this will only work for mono, single channel samples. + */ + @Override + public double evaluate(double input) { + // Input ranges from -1 to +1 + // Map it to range of sample with guard point. + double normalizedInput = (input + 1.0) * 0.5; + // Clip so it does not go out of range of the sample. + if (normalizedInput < 0.0) normalizedInput = 0.0; + else if (normalizedInput > 1.0) normalizedInput = 1.0; + double fractionalIndex = (getNumFrames() - 1.01) * normalizedInput; + return interpolate(fractionalIndex); + } +} diff --git a/src/main/java/com/jsyn/data/Function.java b/src/main/java/com/jsyn/data/Function.java new file mode 100644 index 0000000..c0e6566 --- /dev/null +++ b/src/main/java/com/jsyn/data/Function.java @@ -0,0 +1,35 @@ +/* + * 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.data; + +import com.jsyn.unitgen.FunctionEvaluator; +import com.jsyn.unitgen.FunctionOscillator; + +/** + * @author Phil Burk (C) 2010 Mobileer Inc + * @see FunctionEvaluator + * @see FunctionOscillator + */ +public interface Function { + /** + * Convert an input value to an output value. + * + * @param input + * @return generated value + */ + public double evaluate(double input); +} diff --git a/src/main/java/com/jsyn/data/HammingWindow.java b/src/main/java/com/jsyn/data/HammingWindow.java new file mode 100644 index 0000000..d8e1238 --- /dev/null +++ b/src/main/java/com/jsyn/data/HammingWindow.java @@ -0,0 +1,41 @@ +/* + * 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.data; + +public class HammingWindow implements SpectralWindow { + private double[] data; + + /** Construct a generalized Hamming Window */ + public HammingWindow(int length, double alpha, double beta) { + data = new double[length]; + double scaler = 2.0 * Math.PI / (length - 1); + for (int i = 0; i < length; i++) { + data[i] = alpha - (beta * (Math.cos(i * scaler))); + } + } + + /** Traditional Hamming Window with alpha = 25/46 and beta = 21/46 */ + public HammingWindow(int length) { + this(length, 25.0 / 46.0, 21.0 / 46.0); + } + + @Override + public double get(int index) { + return data[index]; + } + +} diff --git a/src/main/java/com/jsyn/data/HannWindow.java b/src/main/java/com/jsyn/data/HannWindow.java new file mode 100644 index 0000000..878d07c --- /dev/null +++ b/src/main/java/com/jsyn/data/HannWindow.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013 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.data; + +import com.jsyn.unitgen.SpectralFFT; +import com.jsyn.unitgen.SpectralIFFT; + +/** + * A HammingWindow with alpha and beta = 0.5. + * + * @author Phil Burk (C) 2013 Mobileer Inc + * @see SpectralWindow + * @see SpectralFFT + * @see SpectralIFFT + */ +public class HannWindow extends HammingWindow { + + public HannWindow(int length) { + super(length, 0.5, 0.5); + } + +} diff --git a/src/main/java/com/jsyn/data/SampleMarker.java b/src/main/java/com/jsyn/data/SampleMarker.java new file mode 100644 index 0000000..d3db1d4 --- /dev/null +++ b/src/main/java/com/jsyn/data/SampleMarker.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012 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.data; + +/** + * A marker for an audio sample. + * + * @author (C) 2012 Phil Burk, Mobileer Inc + */ + +public class SampleMarker { + /** Sample frame index. */ + public int position; + public String name; + public String comment; +} diff --git a/src/main/java/com/jsyn/data/SegmentedEnvelope.java b/src/main/java/com/jsyn/data/SegmentedEnvelope.java new file mode 100644 index 0000000..efdfd89 --- /dev/null +++ b/src/main/java/com/jsyn/data/SegmentedEnvelope.java @@ -0,0 +1,125 @@ +/* + * 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.data; + +import com.jsyn.unitgen.EnvelopeDAHDSR; +import com.jsyn.unitgen.VariableRateMonoReader; + +/** + * Store an envelope as a series of line segments. Each line is described as a duration and a target + * value. The envelope can be played using a {@link VariableRateMonoReader}. Here is an example that + * generates an envelope that looks like a traditional ADSR envelope. + * + * <pre> + * <code> + * // Create an amplitude envelope and fill it with data. + * double[] ampData = { + * 0.02, 0.9, // duration,value pair 0, "attack" + * 0.10, 0.5, // pair 1, "decay" + * 0.50, 0.0 // pair 2, "release" + * }; + * SegmentedEnvelope ampEnvelope = new SegmentedEnvelope( ampData ); + * + * // Hang at end of decay segment to provide a "sustain" segment. + * ampEnvelope.setSustainBegin( 1 ); + * ampEnvelope.setSustainEnd( 1 ); + * + * // Play the envelope using queueOn so that it uses the sustain and release information. + * synth.add( ampEnv = new VariableRateMonoReader() ); + * ampEnv.dataQueue.queueOn( ampEnvelope ); + * </code> + * </pre> + * + * As an alternative you could use an {@link EnvelopeDAHDSR}. + * + * @author Phil Burk (C) 2010 Mobileer Inc + * @see VariableRateMonoReader + * @see EnvelopeDAHDSR + */ +public class SegmentedEnvelope extends SequentialDataCommon { + private double[] buffer; + + public SegmentedEnvelope(int maxFrames) { + allocate(maxFrames); + } + + public SegmentedEnvelope(double[] pairs) { + this(pairs.length / 2); + write(pairs); + } + + public void allocate(int maxFrames) { + buffer = new double[maxFrames * 2]; + this.maxFrames = maxFrames; + this.numFrames = 0; + } + + /** + * Write frames of envelope data. A frame consists of a duration and a value. + * + * @param startFrame Index of frame in envelope to write to. + * @param data Pairs of duration and value. + * @param startIndex Index of frame in data[] to read from. + * @param numToWrite Number of frames (pairs) to write. + */ + public void write(int startFrame, double[] data, int startIndex, int numToWrite) { + System.arraycopy(data, startIndex * 2, buffer, startFrame * 2, numToWrite * 2); + if ((startFrame + numToWrite) > numFrames) { + numFrames = startFrame + numToWrite; + } + } + + public void read(int startFrame, double[] data, int startIndex, int numToRead) { + System.arraycopy(buffer, startFrame * 2, data, startIndex * 2, numToRead * 2); + } + + public void write(double[] data) { + write(0, data, 0, data.length / 2); + } + + public void read(double[] data) { + read(0, data, 0, data.length / 2); + } + + /** Read the value of an envelope, not the duration. */ + @Override + public double readDouble(int index) { + return buffer[(index * 2) + 1]; + } + + @Override + public void writeDouble(int index, double value) { + buffer[(index * 2) + 1] = value; + if ((index + 1) > numFrames) { + numFrames = index + 1; + } + } + + @Override + public double getRateScaler(int index, double synthesisPeriod) { + double duration = buffer[index * 2]; + if (duration < synthesisPeriod) { + duration = synthesisPeriod; + } + return 1.0 / duration; + } + + @Override + public int getChannelsPerFrame() { + return 1; + } +} diff --git a/src/main/java/com/jsyn/data/SequentialData.java b/src/main/java/com/jsyn/data/SequentialData.java new file mode 100644 index 0000000..f567493 --- /dev/null +++ b/src/main/java/com/jsyn/data/SequentialData.java @@ -0,0 +1,96 @@ +/* + * 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.data; + +import com.jsyn.unitgen.FixedRateMonoReader; +import com.jsyn.unitgen.FixedRateMonoWriter; +import com.jsyn.unitgen.FixedRateStereoReader; +import com.jsyn.unitgen.FixedRateStereoWriter; +import com.jsyn.unitgen.VariableRateMonoReader; +import com.jsyn.unitgen.VariableRateStereoReader; + +/** + * Interface for objects that can be read and/or written by index. The index is not stored + * internally so they can be shared by multiple readers. + * + * @author Phil Burk (C) 2010 Mobileer Inc + * @see FixedRateMonoReader + * @see FixedRateStereoReader + * @see FixedRateMonoWriter + * @see FixedRateStereoWriter + * @see VariableRateMonoReader + * @see VariableRateStereoReader + */ +public interface SequentialData { + /** + * Write a value at the given index. + * + * @param index sample index is ((frameIndex * channelsPerFrame) + channelIndex) + * @param value the value to be written + */ + void writeDouble(int index, double value); + + /** + * Read a value from the sample independently from the internal storage format. + * + * @param index sample index is ((frameIndex * channelsPerFrame) + channelIndex) + */ + double readDouble(int index); + + /*** + * @return Beginning of sustain loop or -1 if no loop. + */ + public int getSustainBegin(); + + /** + * SustainEnd value is the frame index of the frame just past the end of the loop. The number of + * frames included in the loop is (SustainEnd - SustainBegin). + * + * @return End of sustain loop or -1 if no loop. + */ + public int getSustainEnd(); + + /*** + * @return Beginning of release loop or -1 if no loop. + */ + public int getReleaseBegin(); + + /*** + * @return End of release loop or -1 if no loop. + */ + public int getReleaseEnd(); + + /** + * Get rate to play the data. In an envelope this correspond to the inverse of the frame + * duration and would vary frame to frame. For an audio sample it is 1.0. + * + * @param index + * @param synthesisRate + * @return rate to scale the playback speed. + */ + double getRateScaler(int index, double synthesisRate); + + /** + * @return For a stereo sample, return 2. + */ + int getChannelsPerFrame(); + + /** + * @return The number of valid frames that can be read. + */ + int getNumFrames(); +} diff --git a/src/main/java/com/jsyn/data/SequentialDataCommon.java b/src/main/java/com/jsyn/data/SequentialDataCommon.java new file mode 100644 index 0000000..5cc51df --- /dev/null +++ b/src/main/java/com/jsyn/data/SequentialDataCommon.java @@ -0,0 +1,136 @@ +/* + * 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.data; + +/** + * Abstract base class for envelopes and samples that adds sustain and release loops. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public abstract class SequentialDataCommon implements SequentialData { + protected int numFrames; + protected int maxFrames; + private int sustainBegin = -1; + private int sustainEnd = -1; + private int releaseBegin = -1; + private int releaseEnd = -1; + + @Override + public abstract void writeDouble(int index, double value); + + @Override + public abstract double readDouble(int index); + + @Override + public abstract double getRateScaler(int index, double synthesisRate); + + @Override + public abstract int getChannelsPerFrame(); + + /** + * @return Maximum number of frames of data. + */ + public int getMaxFrames() { + return maxFrames; + } + + /** + * Set number of frames of data. Input will be clipped to maxFrames. This is useful when + * changing the contents of a sample or envelope. + */ + public void setNumFrames(int numFrames) { + if (numFrames > maxFrames) + numFrames = maxFrames; + this.numFrames = numFrames; + } + + @Override + public int getNumFrames() { + return numFrames; + } + + // JavaDocs will be copied from SequentialData + + @Override + public int getSustainBegin() { + return this.sustainBegin; + } + + @Override + public int getSustainEnd() { + return this.sustainEnd; + } + + @Override + public int getReleaseBegin() { + return this.releaseBegin; + } + + @Override + public int getReleaseEnd() { + return this.releaseEnd; + } + + /** + * Set beginning of a sustain loop. When UnitDataQueuePort.queueOn() is called, + * if the loop is set then the attack portion will be queued followed by this sustain + * region using queueLoop(). + * The number of frames in the loop will be (SustainEnd - SustainBegin). + * <p> + * For a steady sustain level, like in an ADSR envelope, set SustainBegin and + * SustainEnd to the same frame. + * <p> + * For a sustain that is modulated, include two or more frames in the loop. + * + * @param sustainBegin + */ + public void setSustainBegin(int sustainBegin) { + this.sustainBegin = sustainBegin; + } + + /** + * SustainEnd value is the frame index of the frame just past the end of the loop. + * The number of frames included in the loop is (SustainEnd - SustainBegin). + * + * @param sustainEnd + */ + public void setSustainEnd(int sustainEnd) { + this.sustainEnd = sustainEnd; + } + + /** + * The release loop behaves like the sustain loop but it is triggered + * by UnitDataQueuePort.queueOff(). + * + * @param releaseBegin + */ + public void setReleaseBegin(int releaseBegin) { + this.releaseBegin = releaseBegin; + } + + /** + * ReleaseEnd value is the frame index of the frame just past the end of the loop. + * The number of frames included in the loop is (ReleaseEnd - ReleaseBegin). + * + * @param releaseEnd + */ + + public void setReleaseEnd(int releaseEnd) { + this.releaseEnd = releaseEnd; + } + +} diff --git a/src/main/java/com/jsyn/data/ShortSample.java b/src/main/java/com/jsyn/data/ShortSample.java new file mode 100644 index 0000000..4a4110e --- /dev/null +++ b/src/main/java/com/jsyn/data/ShortSample.java @@ -0,0 +1,123 @@ +/* + * 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.data; + +import com.jsyn.engine.SynthesisEngine; +import com.jsyn.unitgen.FixedRateMonoReader; +import com.jsyn.unitgen.FixedRateStereoReader; +import com.jsyn.unitgen.VariableRateMonoReader; +import com.jsyn.unitgen.VariableRateStereoReader; +import com.jsyn.util.SampleLoader; + +/** + * Store multi-channel short audio data in an interleaved buffer. + * + * @author Phil Burk (C) 2010 Mobileer Inc + * @see SampleLoader + * @see FixedRateMonoReader + * @see FixedRateStereoReader + * @see VariableRateMonoReader + * @see VariableRateStereoReader + */ +public class ShortSample extends AudioSample { + private short[] buffer; + + public ShortSample() { + } + + public ShortSample(int numFrames, int channelsPerFrame) { + allocate(numFrames, channelsPerFrame); + } + + /** Constructor for mono samples with data. */ + public ShortSample(short[] data) { + this(data.length, 1); + write(data); + } + + /** Constructor for multi-channel samples with data. */ + public ShortSample(short[] data, int channelsPerFrame) { + this(data.length / channelsPerFrame, channelsPerFrame); + write(data); + } + + @Override + public void allocate(int numFrames, int channelsPerFrame) { + buffer = new short[numFrames * channelsPerFrame]; + this.numFrames = numFrames; + this.channelsPerFrame = channelsPerFrame; + } + + /** + * Note that in a stereo sample, a frame has two values. + * + * @param startFrame index of frame in the sample + * @param data data to be written + * @param startIndex index of first value in array + * @param numFrames + */ + public void write(int startFrame, short[] data, int startIndex, int numFrames) { + int numSamplesToWrite = numFrames * channelsPerFrame; + int firstSampleIndexToWrite = startFrame * channelsPerFrame; + System.arraycopy(data, startIndex, buffer, firstSampleIndexToWrite, numSamplesToWrite); + } + + /** + * Note that in a stereo sample, a frame has two values. + * + * @param startFrame index of frame in the sample + * @param data array to receive the data from the sample + * @param startIndex index of first location in array to start filling + * @param numFrames + */ + public void read(int startFrame, short[] data, int startIndex, int numFrames) { + int numSamplesToRead = numFrames * channelsPerFrame; + int firstSampleIndexToRead = startFrame * channelsPerFrame; + System.arraycopy(buffer, firstSampleIndexToRead, data, startIndex, numSamplesToRead); + } + + public void write(short[] data) { + write(0, data, 0, data.length); + } + + public void read(short[] data) { + read(0, data, 0, data.length); + } + + public short readShort(int index) { + return buffer[index]; + } + + public void writeShort(int index, short value) { + buffer[index] = value; + } + + /** Read a sample converted to a double in the range -1.0 to almost 1.0. */ + @Override + public double readDouble(int index) { + return SynthesisEngine.convertShortToDouble(buffer[index]); + } + + /** + * Write a double that will be clipped to the range -1.0 to almost 1.0 and converted to a short. + */ + @Override + public void writeDouble(int index, double value) { + buffer[index] = SynthesisEngine.convertDoubleToShort(value); + } + +} diff --git a/src/main/java/com/jsyn/data/SpectralWindow.java b/src/main/java/com/jsyn/data/SpectralWindow.java new file mode 100644 index 0000000..0fcfac4 --- /dev/null +++ b/src/main/java/com/jsyn/data/SpectralWindow.java @@ -0,0 +1,21 @@ +/* + * 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.data; + +public interface SpectralWindow { + public double get(int index); +} diff --git a/src/main/java/com/jsyn/data/SpectralWindowFactory.java b/src/main/java/com/jsyn/data/SpectralWindowFactory.java new file mode 100644 index 0000000..01cced6 --- /dev/null +++ b/src/main/java/com/jsyn/data/SpectralWindowFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013 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.data; + +import com.jsyn.unitgen.SpectralFFT; +import com.jsyn.unitgen.SpectralFilter; +import com.jsyn.unitgen.SpectralIFFT; + +/** + * Create shared windows as needed for use with FFTs and IFFTs. + * + * @author Phil Burk (C) 2013 Mobileer Inc + * @see SpectralWindow + * @see SpectralFFT + * @see SpectralIFFT + * @see SpectralFilter + */ +public class SpectralWindowFactory { + private static final int NUM_WINDOWS = 16; + private static final int MIN_SIZE_LOG_2 = 2; + private static HammingWindow[] hammingWindows = new HammingWindow[NUM_WINDOWS]; + private static HannWindow[] hannWindows = new HannWindow[NUM_WINDOWS]; + + /** @return a shared standard HammingWindow */ + public static HammingWindow getHammingWindow(int sizeLog2) { + int index = sizeLog2 - MIN_SIZE_LOG_2; + if (hammingWindows[index] == null) { + hammingWindows[index] = new HammingWindow(1 << sizeLog2); + } + return hammingWindows[index]; + } + + /** @return a shared HannWindow */ + public static HannWindow getHannWindow(int sizeLog2) { + int index = sizeLog2 - MIN_SIZE_LOG_2; + if (hannWindows[index] == null) { + hannWindows[index] = new HannWindow(1 << sizeLog2); + } + return hannWindows[index]; + } +} diff --git a/src/main/java/com/jsyn/data/Spectrum.java b/src/main/java/com/jsyn/data/Spectrum.java new file mode 100644 index 0000000..66e4ee4 --- /dev/null +++ b/src/main/java/com/jsyn/data/Spectrum.java @@ -0,0 +1,97 @@ +/* + * Copyright 2013 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.data; + +import com.jsyn.unitgen.SpectralFFT; +import com.jsyn.unitgen.SpectralIFFT; +import com.jsyn.unitgen.SpectralProcessor; + +/** + * Complex spectrum with real and imaginary parts. The frequency associated with each bin of the + * spectrum is: + * + * <pre> + * frequency = binIndex * sampleRate / size + * </pre> + * + * Note that the upper half of the spectrum is above the Nyquist frequency. Those frequencies are + * mirrored around the Nyquist frequency. Note that this spectral API is experimental and may change + * at any time. + * + * @author Phil Burk (C) 2013 Mobileer Inc + * @version 016 + * @see SpectralFFT + * @see SpectralIFFT + * @see SpectralProcessor + */ +public class Spectrum { + private double[] real; + private double[] imaginary; + public static final int DEFAULT_SIZE_LOG_2 = 9; + public static final int DEFAULT_SIZE = 1 << DEFAULT_SIZE_LOG_2; + + public Spectrum() { + this(DEFAULT_SIZE); + } + + public Spectrum(int size) { + setSize(size); + } + + public double[] getReal() { + return real; + } + + public double[] getImaginary() { + return imaginary; + } + + /** + * If you change the size of the spectrum then the real and imaginary arrays will be + * reallocated. + * + * @param size + */ + public void setSize(int size) { + if ((real == null) || (real.length != size)) { + real = new double[size]; + imaginary = new double[size]; + } + } + + public int size() { + return real.length; + } + + /** + * Copy this spectrum to another spectrum of the same length. + * + * @param destination + */ + public void copyTo(Spectrum destination) { + assert (size() == destination.size()); + System.arraycopy(real, 0, destination.real, 0, real.length); + System.arraycopy(imaginary, 0, destination.imaginary, 0, imaginary.length); + } + + public void clear() { + for (int i = 0; i < real.length; i++) { + real[i] = 0.0; + imaginary[i] = 0.0; + } + } +} diff --git a/src/main/java/com/jsyn/devices/AudioDeviceFactory.java b/src/main/java/com/jsyn/devices/AudioDeviceFactory.java new file mode 100644 index 0000000..612c81d --- /dev/null +++ b/src/main/java/com/jsyn/devices/AudioDeviceFactory.java @@ -0,0 +1,93 @@ +/* + * 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.devices; + +import com.jsyn.util.JavaTools; + +/** + * Create a device appropriate for the platform. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class AudioDeviceFactory { + private static AudioDeviceManager instance; + + /** + * Use a custom device interface. Overrides the selection of a default device manager. + * + * @param instance + */ + public static void setInstance(AudioDeviceManager instance) { + AudioDeviceFactory.instance = instance; + } + + /** + * Try to load JPortAudio or JavaSound devices. + * + * @return A device supported on this platform. + */ + public static AudioDeviceManager createAudioDeviceManager() { + return createAudioDeviceManager(false); + } + + /** + * Try to load JPortAudio or JavaSound devices. + * + * @param preferJavaSound if true then try to create a JavaSound manager before other types. + * @return A device supported on this platform. + */ + public static AudioDeviceManager createAudioDeviceManager(boolean preferJavaSound) { + if (preferJavaSound) { + tryJavaSound(); + tryJPortAudio(); + } else { + tryJPortAudio(); + tryJavaSound(); + } + return instance; + } + + private static void tryJavaSound() { + if (instance == null) { + try { + @SuppressWarnings("unchecked") + Class<AudioDeviceManager> clazz = JavaTools.loadClass( + "com.jsyn.devices.javasound.JavaSoundAudioDevice", false); + if (clazz != null) { + instance = clazz.newInstance(); + } + } catch (Throwable e) { + System.err.println("Could not load JavaSound device. " + e); + } + } + } + + private static void tryJPortAudio() { + if (instance == null) { + try { + if (JavaTools.loadClass("com.portaudio.PortAudio", false) != null) { + instance = (AudioDeviceManager) JavaTools.loadClass( + "com.jsyn.devices.jportaudio.JPortAudioDevice").newInstance(); + } + + } catch (Throwable e) { + System.err.println("Could not load JPortAudio device. " + e); + } + } + } + +} diff --git a/src/main/java/com/jsyn/devices/AudioDeviceInputStream.java b/src/main/java/com/jsyn/devices/AudioDeviceInputStream.java new file mode 100644 index 0000000..a3d1854 --- /dev/null +++ b/src/main/java/com/jsyn/devices/AudioDeviceInputStream.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.devices; + +import com.jsyn.io.AudioInputStream; + +public interface AudioDeviceInputStream extends AudioInputStream { + /** Start the input device. */ + public void start(); + + public void stop(); + + /** + * @return Estimated latency in seconds. + */ + public double getLatency(); +} diff --git a/src/main/java/com/jsyn/devices/AudioDeviceManager.java b/src/main/java/com/jsyn/devices/AudioDeviceManager.java new file mode 100644 index 0000000..ac8d47c --- /dev/null +++ b/src/main/java/com/jsyn/devices/AudioDeviceManager.java @@ -0,0 +1,120 @@ +/* + * 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.devices; + +/** + * Interface for an audio system. This may be implemented using JavaSound, or a native device + * wrapper. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public interface AudioDeviceManager { + /** + * Pass this value to the start method to request the default device ID. + */ + public final static int USE_DEFAULT_DEVICE = -1; + + /** + * @return The number of devices available. + */ + public int getDeviceCount(); + + /** + * Get the name of an audio device. + * + * @param deviceID An index between 0 to deviceCount-1. + * @return A name that can be shown to the user. + */ + public String getDeviceName(int deviceID); + + /** + * @return A name of the device manager that can be shown to the user. + */ + public String getName(); + + /** + * The user can generally select a default device using a control panel that is part of the + * operating system. + * + * @return The ID for the input device that the user has selected as the default. + */ + public int getDefaultInputDeviceID(); + + /** + * The user can generally select a default device using a control panel that is part of the + * operating system. + * + * @return The ID for the output device that the user has selected as the default. + */ + public int getDefaultOutputDeviceID(); + + /** + * @param deviceID + * @return The maximum number of channels that the device will support. + */ + public int getMaxInputChannels(int deviceID); + + /** + * @param deviceID An index between 0 to numDevices-1. + * @return The maximum number of channels that the device will support. + */ + public int getMaxOutputChannels(int deviceID); + + /** + * This the lowest latency that the device can support reliably. It should be used for + * applications that require low latency such as live processing of guitar signals. + * + * @param deviceID An index between 0 to numDevices-1. + * @return Latency in seconds. + */ + public double getDefaultLowInputLatency(int deviceID); + + /** + * This the highest latency that the device can support. High latency is recommended for + * applications that are not time critical, such as recording. + * + * @param deviceID An index between 0 to numDevices-1. + * @return Latency in seconds. + */ + public double getDefaultHighInputLatency(int deviceID); + + public double getDefaultLowOutputLatency(int deviceID); + + public double getDefaultHighOutputLatency(int deviceID); + + /** + * Set latency in seconds for the audio device. If set to zero then the DefaultLowLatency value + * for the device will be used. This is just a suggestion that will be used when the + * AudioDeviceInputStream is started. + **/ + public int setSuggestedInputLatency(double latency); + + public int setSuggestedOutputLatency(double latency); + + /** + * Create a stream that can be used internally by JSyn for outputting audio data. Applications + * should not call this directly. + */ + AudioDeviceOutputStream createOutputStream(int deviceID, int frameRate, int numOutputChannels); + + /** + * Create a stream that can be used internally by JSyn for acquiring audio input data. + * Applications should not call this directly. + */ + AudioDeviceInputStream createInputStream(int deviceID, int frameRate, int numInputChannels); + +} diff --git a/src/main/java/com/jsyn/devices/AudioDeviceOutputStream.java b/src/main/java/com/jsyn/devices/AudioDeviceOutputStream.java new file mode 100644 index 0000000..5c17efb --- /dev/null +++ b/src/main/java/com/jsyn/devices/AudioDeviceOutputStream.java @@ -0,0 +1,30 @@ +/* + * 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.devices; + +import com.jsyn.io.AudioOutputStream; + +public interface AudioDeviceOutputStream extends AudioOutputStream { + public void start(); + + public void stop(); + + /** + * @return Estimated latency in seconds. + */ + public double getLatency(); +} diff --git a/src/main/java/com/jsyn/devices/javasound/JavaSoundAudioDevice.java b/src/main/java/com/jsyn/devices/javasound/JavaSoundAudioDevice.java new file mode 100644 index 0000000..75c4a8a --- /dev/null +++ b/src/main/java/com/jsyn/devices/javasound/JavaSoundAudioDevice.java @@ -0,0 +1,432 @@ +/* + * 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.devices.javasound; + +import java.util.ArrayList; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.Line; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.Mixer; +import javax.sound.sampled.SourceDataLine; +import javax.sound.sampled.TargetDataLine; + +import com.jsyn.devices.AudioDeviceInputStream; +import com.jsyn.devices.AudioDeviceManager; +import com.jsyn.devices.AudioDeviceOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Use JavaSound to access the audio hardware. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class JavaSoundAudioDevice implements AudioDeviceManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(JavaSoundAudioDevice.class); + + private static final int BYTES_PER_SAMPLE = 2; + private static final boolean USE_BIG_ENDIAN = false; + + ArrayList<DeviceInfo> deviceRecords; + private double suggestedOutputLatency = 0.040; + private double suggestedInputLatency = 0.100; + private int defaultInputDeviceID = -1; + private int defaultOutputDeviceID = -1; + + public JavaSoundAudioDevice() { + String osName = System.getProperty("os.name"); + if (osName.contains("Windows")) { + suggestedOutputLatency = 0.08; + LOGGER.info("JSyn: default output latency set to " + + ((int) (suggestedOutputLatency * 1000)) + " msec for " + osName); + } + deviceRecords = new ArrayList<DeviceInfo>(); + sniffAvailableMixers(); + dumpAvailableMixers(); + } + + private void dumpAvailableMixers() { + for (DeviceInfo deviceInfo : deviceRecords) { + LOGGER.debug("" + deviceInfo); + } + } + + /** + * Build device info and determine default devices. + */ + private void sniffAvailableMixers() { + Mixer.Info[] mixers = AudioSystem.getMixerInfo(); + for (int i = 0; i < mixers.length; i++) { + DeviceInfo deviceInfo = new DeviceInfo(); + + deviceInfo.name = mixers[i].getName(); + Mixer mixer = AudioSystem.getMixer(mixers[i]); + + Line.Info[] lines = mixer.getTargetLineInfo(); + deviceInfo.maxInputs = scanMaxChannels(lines); + // Remember first device that supports input. + if ((defaultInputDeviceID < 0) && (deviceInfo.maxInputs > 0)) { + defaultInputDeviceID = i; + } + + lines = mixer.getSourceLineInfo(); + deviceInfo.maxOutputs = scanMaxChannels(lines); + // Remember first device that supports output. + if ((defaultOutputDeviceID < 0) && (deviceInfo.maxOutputs > 0)) { + defaultOutputDeviceID = i; + } + + deviceRecords.add(deviceInfo); + } + } + + private int scanMaxChannels(Line.Info[] lines) { + int maxChannels = 0; + for (Line.Info line : lines) { + if (line instanceof DataLine.Info) { + int numChannels = scanMaxChannels(((DataLine.Info) line)); + if (numChannels > maxChannels) { + maxChannels = numChannels; + } + } + } + return maxChannels; + } + + private int scanMaxChannels(DataLine.Info info) { + int maxChannels = 0; + for (AudioFormat format : info.getFormats()) { + int numChannels = format.getChannels(); + if (numChannels > maxChannels) { + maxChannels = numChannels; + } + } + return maxChannels; + } + + static class DeviceInfo { + String name; + int maxInputs; + int maxOutputs; + + @Override + public String toString() { + return "AudioDevice: " + name + ", max in = " + maxInputs + ", max out = " + maxOutputs; + } + } + + private static class JavaSoundStream { + AudioFormat format; + byte[] bytes; + int frameRate; + int deviceID; + int samplesPerFrame; + + public JavaSoundStream(int deviceID, int frameRate, int samplesPerFrame) { + this.deviceID = deviceID; + this.frameRate = frameRate; + this.samplesPerFrame = samplesPerFrame; + format = new AudioFormat(frameRate, 16, samplesPerFrame, true, USE_BIG_ENDIAN); + } + + Line getDataLine(DataLine.Info info) throws LineUnavailableException { + Line dataLine; + if (deviceID >= 0) { + Mixer.Info[] mixers = AudioSystem.getMixerInfo(); + Mixer mixer = AudioSystem.getMixer(mixers[deviceID]); + dataLine = mixer.getLine(info); + } else { + dataLine = AudioSystem.getLine(info); + } + return dataLine; + } + + int calculateBufferSize(double suggestedOutputLatency) { + int numFrames = (int) (suggestedOutputLatency * frameRate); + return numFrames * samplesPerFrame * BYTES_PER_SAMPLE; + } + + } + + private class JavaSoundOutputStream extends JavaSoundStream implements AudioDeviceOutputStream { + SourceDataLine line; + + public JavaSoundOutputStream(int deviceID, int frameRate, int samplesPerFrame) { + super(deviceID, frameRate, samplesPerFrame); + } + + @Override + public void start() { + DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); + if (!AudioSystem.isLineSupported(info)) { + // Handle the error. + LOGGER.error("JavaSoundOutputStream - not supported." + format); + } else { + try { + line = (SourceDataLine) getDataLine(info); + int bufferSize = calculateBufferSize(suggestedOutputLatency); + line.open(format, bufferSize); + LOGGER.debug("Output buffer size = " + bufferSize + " bytes."); + line.start(); + + } catch (Exception e) { + e.printStackTrace(); + line = null; + } + } + } + + /** Grossly inefficient. Call the array version instead. */ + @Override + public void write(double value) { + double[] buffer = new double[1]; + buffer[0] = value; + write(buffer, 0, 1); + } + + @Override + public void write(double[] buffer) { + write(buffer, 0, buffer.length); + } + + @Override + public void write(double[] buffer, int start, int count) { + // Allocate byte buffer if needed. + if ((bytes == null) || ((bytes.length * 2) < count)) { + bytes = new byte[count * 2]; + } + + // Convert float samples to LittleEndian bytes. + int byteIndex = 0; + for (int i = 0; i < count; i++) { + // 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 = (32767.0 * buffer[i + start]) + 32768.5; + int sample = ((int) temp) - 32768; + if (sample > Short.MAX_VALUE) { + sample = Short.MAX_VALUE; + } else if (sample < Short.MIN_VALUE) { + sample = Short.MIN_VALUE; + } + bytes[byteIndex++] = (byte) sample; // little end + bytes[byteIndex++] = (byte) (sample >> 8); // big end + } + + line.write(bytes, 0, byteIndex); + } + + @Override + public void stop() { + if (line != null) { + line.stop(); + line.flush(); + line.close(); + line = null; + } else { + new RuntimeException("AudioOutput stop attempted when no line created.") + .printStackTrace(); + } + } + + @Override + public double getLatency() { + if (line == null) { + return 0.0; + } + int numBytes = line.getBufferSize(); + int numFrames = numBytes / (BYTES_PER_SAMPLE * samplesPerFrame); + return ((double) numFrames) / frameRate; + } + + @Override + public void close() { + } + + } + + private class JavaSoundInputStream extends JavaSoundStream implements AudioDeviceInputStream { + TargetDataLine line; + + public JavaSoundInputStream(int deviceID, int frameRate, int samplesPerFrame) { + super(deviceID, frameRate, samplesPerFrame); + } + + @Override + public void start() { + DataLine.Info info = new DataLine.Info(TargetDataLine.class, format); + if (!AudioSystem.isLineSupported(info)) { + // Handle the error. + LOGGER.error("JavaSoundInputStream - not supported." + format); + } else { + try { + line = (TargetDataLine) getDataLine(info); + int bufferSize = calculateBufferSize(suggestedInputLatency); + line.open(format, bufferSize); + LOGGER.debug("Input buffer size = " + bufferSize + " bytes."); + line.start(); + } catch (Exception e) { + e.printStackTrace(); + line = null; + } + } + } + + @Override + public double read() { + double[] buffer = new double[1]; + read(buffer, 0, 1); + return buffer[0]; + } + + @Override + public int read(double[] buffer) { + return read(buffer, 0, buffer.length); + } + + @Override + public int read(double[] buffer, int start, int count) { + // Allocate byte buffer if needed. + if ((bytes == null) || ((bytes.length * 2) < count)) { + bytes = new byte[count * 2]; + } + int bytesRead = line.read(bytes, 0, bytes.length); + + // Convert BigEndian bytes to float samples + int bi = 0; + for (int i = 0; i < count; i++) { + int sample = bytes[bi++] & 0x00FF; // little end + sample = sample + (bytes[bi++] << 8); // big end + buffer[i + start] = sample * (1.0 / 32767.0); + } + return bytesRead / 4; + } + + @Override + public void stop() { + if (line != null) { + line.drain(); + line.close(); + } else { + new RuntimeException("AudioInput stop attempted when no line created.") + .printStackTrace(); + } + } + + @Override + public double getLatency() { + if (line == null) { + return 0.0; + } + int numBytes = line.getBufferSize(); + int numFrames = numBytes / (BYTES_PER_SAMPLE * samplesPerFrame); + return ((double) numFrames) / frameRate; + } + + @Override + public int available() { + return line.available() / BYTES_PER_SAMPLE; + } + + @Override + public void close() { + } + + } + + @Override + public AudioDeviceOutputStream createOutputStream(int deviceID, int frameRate, + int samplesPerFrame) { + return new JavaSoundOutputStream(deviceID, frameRate, samplesPerFrame); + } + + @Override + public AudioDeviceInputStream createInputStream(int deviceID, int frameRate, int samplesPerFrame) { + return new JavaSoundInputStream(deviceID, frameRate, samplesPerFrame); + } + + @Override + public double getDefaultHighInputLatency(int deviceID) { + return 0.300; + } + + @Override + public double getDefaultHighOutputLatency(int deviceID) { + return 0.300; + } + + @Override + public int getDefaultInputDeviceID() { + return defaultInputDeviceID; + } + + @Override + public int getDefaultOutputDeviceID() { + return defaultOutputDeviceID; + } + + @Override + public double getDefaultLowInputLatency(int deviceID) { + return 0.100; + } + + @Override + public double getDefaultLowOutputLatency(int deviceID) { + return 0.100; + } + + @Override + public int getDeviceCount() { + return deviceRecords.size(); + } + + @Override + public String getDeviceName(int deviceID) { + return deviceRecords.get(deviceID).name; + } + + @Override + public int getMaxInputChannels(int deviceID) { + return deviceRecords.get(deviceID).maxInputs; + } + + @Override + public int getMaxOutputChannels(int deviceID) { + return deviceRecords.get(deviceID).maxOutputs; + } + + @Override + public int setSuggestedOutputLatency(double latency) { + suggestedOutputLatency = latency; + return 0; + } + + @Override + public int setSuggestedInputLatency(double latency) { + suggestedInputLatency = latency; + return 0; + } + + @Override + public String getName() { + return "JavaSound"; + } + +} diff --git a/src/main/java/com/jsyn/devices/javasound/MidiDeviceTools.java b/src/main/java/com/jsyn/devices/javasound/MidiDeviceTools.java new file mode 100644 index 0000000..9cff095 --- /dev/null +++ b/src/main/java/com/jsyn/devices/javasound/MidiDeviceTools.java @@ -0,0 +1,86 @@ +/* + * 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.devices.javasound; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sound.midi.MidiDevice; +import javax.sound.midi.MidiSystem; +import javax.sound.midi.MidiUnavailableException; +import javax.sound.midi.Sequencer; +import javax.sound.midi.Synthesizer; + +public class MidiDeviceTools { + + private static final Logger LOGGER = LoggerFactory.getLogger(MidiDeviceTools.class); + + /** Print the available MIDI Devices. */ + public static void listDevices() { + // Ask the MidiSystem what is available. + MidiDevice.Info[] infos = MidiSystem.getMidiDeviceInfo(); + // Print info about each device. + for (MidiDevice.Info info : infos) { + LOGGER.debug("MIDI Info: " + info.getDescription() + ", " + info.getName() + ", " + + info.getVendor() + ", " + info.getVersion()); + // Get the device for more information. + try { + MidiDevice device = MidiSystem.getMidiDevice(info); + LOGGER.debug(" Device: " + ", #recv = " + device.getMaxReceivers() + + ", #xmit = " + device.getMaxTransmitters() + ", open = " + + device.isOpen() + ", " + device); + } catch (MidiUnavailableException e) { + e.printStackTrace(); + } + } + } + + /** Find a MIDI transmitter that contains text in the name. */ + public static MidiDevice findKeyboard(String text) { + MidiDevice keyboard = null; + // Ask the MidiSystem what is available. + MidiDevice.Info[] infos = MidiSystem.getMidiDeviceInfo(); + // Print info about each device. + for (MidiDevice.Info info : infos) { + try { + MidiDevice device = MidiSystem.getMidiDevice(info); + // Hardware devices are not Synthesizers or Sequencers. + if (!(device instanceof Synthesizer) && !(device instanceof Sequencer)) { + // Is this a transmitter? + // Might be -1 if unlimited. + if (device.getMaxTransmitters() != 0) { + if ((text == null) + || (info.getDescription().toLowerCase() + .contains(text.toLowerCase()))) { + keyboard = device; + LOGGER.debug("Chose: " + info.getDescription()); + break; + } + } + } + } catch (MidiUnavailableException e) { + e.printStackTrace(); + } + } + return keyboard; + } + + public static MidiDevice findKeyboard() { + return findKeyboard(null); + } + +} diff --git a/src/main/java/com/jsyn/devices/jportaudio/JPortAudioDevice.java b/src/main/java/com/jsyn/devices/jportaudio/JPortAudioDevice.java new file mode 100644 index 0000000..15ab9ed --- /dev/null +++ b/src/main/java/com/jsyn/devices/jportaudio/JPortAudioDevice.java @@ -0,0 +1,264 @@ +/* + * 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.devices.jportaudio; + +import com.jsyn.devices.AudioDeviceInputStream; +import com.jsyn.devices.AudioDeviceManager; +import com.jsyn.devices.AudioDeviceOutputStream; +import com.portaudio.BlockingStream; +import com.portaudio.DeviceInfo; +import com.portaudio.HostApiInfo; +import com.portaudio.PortAudio; +import com.portaudio.StreamParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JPortAudioDevice implements AudioDeviceManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(JPortAudioDevice.class); + + private double suggestedOutputLatency = 0.030; + private double suggestedInputLatency = 0.050; + private static final int FRAMES_PER_BUFFER = 128; + + // static Logger logger = Logger.getLogger( JPortAudioDevice.class.getName() ); + + public JPortAudioDevice() { + PortAudio.initialize(); + } + + @Override + public int getDeviceCount() { + return PortAudio.getDeviceCount(); + } + + @Override + public String getDeviceName(int deviceID) { + DeviceInfo deviceInfo = PortAudio.getDeviceInfo(deviceID); + HostApiInfo hostInfo = PortAudio.getHostApiInfo(deviceInfo.hostApi); + return deviceInfo.name + " - " + hostInfo.name; + } + + @Override + public int getDefaultInputDeviceID() { + return PortAudio.getDefaultInputDevice(); + } + + @Override + public int getDefaultOutputDeviceID() { + return PortAudio.getDefaultOutputDevice(); + } + + @Override + public int getMaxInputChannels(int deviceID) { + if (deviceID < 0) { + deviceID = PortAudio.getDefaultInputDevice(); + } + return PortAudio.getDeviceInfo(deviceID).maxInputChannels; + } + + @Override + public int getMaxOutputChannels(int deviceID) { + if (deviceID < 0) { + deviceID = PortAudio.getDefaultOutputDevice(); + } + return PortAudio.getDeviceInfo(deviceID).maxOutputChannels; + } + + @Override + public double getDefaultLowInputLatency(int deviceID) { + if (deviceID < 0) { + deviceID = PortAudio.getDefaultInputDevice(); + } + return PortAudio.getDeviceInfo(deviceID).defaultLowInputLatency; + } + + @Override + public double getDefaultHighInputLatency(int deviceID) { + if (deviceID < 0) { + deviceID = PortAudio.getDefaultInputDevice(); + } + return PortAudio.getDeviceInfo(deviceID).defaultHighInputLatency; + } + + @Override + public double getDefaultLowOutputLatency(int deviceID) { + if (deviceID < 0) { + deviceID = PortAudio.getDefaultOutputDevice(); + } + return PortAudio.getDeviceInfo(deviceID).defaultLowOutputLatency; + } + + @Override + public double getDefaultHighOutputLatency(int deviceID) { + if (deviceID < 0) { + deviceID = PortAudio.getDefaultOutputDevice(); + } + return PortAudio.getDeviceInfo(deviceID).defaultHighOutputLatency; + } + + @Override + public int setSuggestedOutputLatency(double latency) { + suggestedOutputLatency = latency; + return 0; + } + + @Override + public int setSuggestedInputLatency(double latency) { + suggestedInputLatency = latency; + return 0; + } + + @Override + public AudioDeviceOutputStream createOutputStream(int deviceID, int frameRate, + int samplesPerFrame) { + return new JPAOutputStream(deviceID, frameRate, samplesPerFrame); + } + + @Override + public AudioDeviceInputStream createInputStream(int deviceID, int frameRate, int samplesPerFrame) { + return new JPAInputStream(deviceID, frameRate, samplesPerFrame); + } + + private static class JPAStream { + BlockingStream blockingStream; + float[] floatBuffer = null; + int samplesPerFrame; + + public void close() { + blockingStream.close(); + } + + public void start() { + blockingStream.start(); + } + + public void stop() { + blockingStream.stop(); + } + + } + + private class JPAOutputStream extends JPAStream implements AudioDeviceOutputStream { + + private JPAOutputStream(int deviceID, int frameRate, int samplesPerFrame) { + this.samplesPerFrame = samplesPerFrame; + StreamParameters streamParameters = new StreamParameters(); + streamParameters.channelCount = samplesPerFrame; + if (deviceID < 0) { + deviceID = PortAudio.getDefaultOutputDevice(); + } + streamParameters.device = deviceID; + streamParameters.suggestedLatency = suggestedOutputLatency; + int flags = 0; + LOGGER.debug("Audio output on " + getDeviceName(deviceID)); + blockingStream = PortAudio.openStream(null, streamParameters, frameRate, + FRAMES_PER_BUFFER, flags); + } + + /** Grossly inefficient. Call the array version instead. */ + @Override + public void write(double value) { + double[] buffer = new double[1]; + buffer[0] = value; + write(buffer, 0, 1); + } + + @Override + public void write(double[] buffer) { + write(buffer, 0, buffer.length); + } + + @Override + public void write(double[] buffer, int start, int count) { + // Allocate float buffer if needed. + if ((floatBuffer == null) || (floatBuffer.length < count)) { + floatBuffer = new float[count]; + } + for (int i = 0; i < count; i++) { + + floatBuffer[i] = (float) buffer[i + start]; + } + blockingStream.write(floatBuffer, count / samplesPerFrame); + } + + @Override + public double getLatency() { + return blockingStream.getInfo().outputLatency; + } + } + + private class JPAInputStream extends JPAStream implements AudioDeviceInputStream { + private JPAInputStream(int deviceID, int frameRate, int samplesPerFrame) { + this.samplesPerFrame = samplesPerFrame; + StreamParameters streamParameters = new StreamParameters(); + streamParameters.channelCount = samplesPerFrame; + if (deviceID < 0) { + deviceID = PortAudio.getDefaultInputDevice(); + } + streamParameters.device = deviceID; + streamParameters.suggestedLatency = suggestedInputLatency; + int flags = 0; + LOGGER.debug("Audio input from " + getDeviceName(deviceID)); + blockingStream = PortAudio.openStream(streamParameters, null, frameRate, + FRAMES_PER_BUFFER, flags); + } + + @Override + public double read() { + double[] buffer = new double[1]; + read(buffer, 0, 1); + return buffer[0]; + } + + @Override + public int read(double[] buffer) { + return read(buffer, 0, buffer.length); + } + + @Override + public int read(double[] buffer, int start, int count) { + // Allocate float buffer if needed. + if ((floatBuffer == null) || (floatBuffer.length < count)) { + floatBuffer = new float[count]; + } + blockingStream.read(floatBuffer, count / samplesPerFrame); + + for (int i = 0; i < count; i++) { + + buffer[i + start] = floatBuffer[i]; + } + return count; + } + + @Override + public double getLatency() { + return blockingStream.getInfo().inputLatency; + } + + @Override + public int available() { + return blockingStream.getReadAvailable() * samplesPerFrame; + } + + } + + @Override + public String getName() { + return "JPortAudio"; + } +} diff --git a/src/main/java/com/jsyn/engine/LoadAnalyzer.java b/src/main/java/com/jsyn/engine/LoadAnalyzer.java new file mode 100644 index 0000000..cbf7ed5 --- /dev/null +++ b/src/main/java/com/jsyn/engine/LoadAnalyzer.java @@ -0,0 +1,61 @@ +/* + * 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.engine; + +/** Measure CPU load. */ +public class LoadAnalyzer { + private long stopTime; + private long previousStopTime; + private long startTime; + private double averageTotalTime; + private double averageOnTime; + + protected LoadAnalyzer() { + stopTime = System.nanoTime(); + } + + /** + * Call this when you stop doing something. Ideally all of the time since start() was spent on + * doing something without interruption. + */ + public void stop() { + previousStopTime = stopTime; + stopTime = System.nanoTime(); + long onTime = stopTime - startTime; + long totalTime = stopTime - previousStopTime; + if (totalTime > 0) { + // Recursive averaging filter. + double rate = 0.01; + averageOnTime = (averageOnTime * (1.0 - rate)) + (onTime * rate); + averageTotalTime = (averageTotalTime * (1.0 - rate)) + (totalTime * rate); + } + } + + /** Call this when you start doing something. */ + public void start() { + startTime = System.nanoTime(); + } + + /** Calculate, on average, how much of the time was spent doing something. */ + public double getAverageLoad() { + if (averageTotalTime > 0.0) { + return averageOnTime / averageTotalTime; + } else { + return 0.0; + } + } +} diff --git a/src/main/java/com/jsyn/engine/MultiTable.java b/src/main/java/com/jsyn/engine/MultiTable.java new file mode 100644 index 0000000..6606639 --- /dev/null +++ b/src/main/java/com/jsyn/engine/MultiTable.java @@ -0,0 +1,230 @@ +/* + * 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.engine; + +/* + * Multiple tables of sawtooth data. + * organized by octaves below the Nyquist Rate. + * used to generate band-limited Sawtooth, Impulse, Pulse, Square and Triangle BL waveforms + * + <pre> + Analysis of octave requirements for tables. + + OctavesIndex Frequency Partials + 0 N/2 11025 1 + 1 N/4 5512 2 + 2 N/8 2756 4 + 3 N/16 1378 8 + 4 N/32 689 16 + 5 N/64 344 32 + 6 N/128 172 64 + 7 N/256 86 128 + </pre> + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class MultiTable { + + public final static int NUM_TABLES = 8; + public final static int CYCLE_SIZE = (1 << 10); + + private static MultiTable instance = new MultiTable(NUM_TABLES, CYCLE_SIZE); + private double phaseScalar; + private float[][] tables; // array of array of tables + + /************************************************************************** + * Initialize sawtooth wavetables. Table[0] should contain a pure sine wave. Succeeding tables + * should have increasing numbers of partials. + */ + public MultiTable(int numTables, int cycleSize) { + int tableSize = cycleSize + 1; + + // Allocate array of arrays. + tables = new float[numTables][tableSize]; + + float[] sineTable = tables[0]; + + phaseScalar = (float) (cycleSize * 0.5); + + /* Fill initial sine table with values for -PI to PI. */ + for (int j = 0; j < tableSize; j++) { + sineTable[j] = (float) Math.sin(((((double) j) / (double) cycleSize) * Math.PI * 2.0) + - Math.PI); + } + + /* + * Build each table from scratch and scale partials by raised cosine* to eliminate Gibbs + * effect. + */ + for (int i = 1; i < numTables; i++) { + int numPartials; + double kGibbs; + float[] table = tables[i]; + + /* Add together partials for this table. */ + numPartials = 1 << i; + kGibbs = Math.PI / (2 * numPartials); + for (int k = 0; k < numPartials; k++) { + double ampl, cGibbs; + int sineIndex = 0; + int partial = k + 1; + cGibbs = Math.cos(k * kGibbs); + /* Calculate amplitude for Nth partial */ + ampl = cGibbs * cGibbs / partial; + + for (int j = 0; j < tableSize; j++) { + table[j] += (float) ampl * sineTable[sineIndex]; + sineIndex += partial; + /* Wrap index at end of table.. */ + if (sineIndex >= cycleSize) { + sineIndex -= cycleSize; + } + } + } + } + + /* Normalize after */ + for (int i = 1; i < numTables; i++) { + normalizeArray(tables[i]); + } + } + + /**************************************************************************/ + public static float normalizeArray(float[] fdata) { + float max, val, gain; + int i; + + // determine maximum value. + max = 0.0f; + for (i = 0; i < fdata.length; i++) { + val = Math.abs(fdata[i]); + if (val > max) + max = val; + } + if (max < 0.0000001f) + max = 0.0000001f; + // scale array + gain = 1.0f / max; + for (i = 0; i < fdata.length; i++) + fdata[i] *= gain; + return gain; + } + + /***************************************************************************** + * When the phaseInc maps to the highest level table, then we start interpolating between the + * highest table and the raw sawtooth value (phase). When phaseInc points to highest table: + * flevel = NUM_TABLES - 1 = -1 - log2(pInc); log2(pInc) = - NUM_TABLES pInc = 2**(-NUM_TABLES) + */ + private final static double LOWEST_PHASE_INC_INV = (1 << NUM_TABLES); + + /**************************************************************************/ + /* Phase ranges from -1.0 to +1.0 */ + public double calculateSawtooth(double currentPhase, double positivePhaseIncrement, + double flevel) { + float[] tableBase; + double val; + double hiSam; /* Use when verticalFraction is 1.0 */ + double loSam; /* Use when verticalFraction is 0.0 */ + double sam1, sam2; + + /* Use Phase to determine sampleIndex into table. */ + double findex = ((phaseScalar * currentPhase) + phaseScalar); + // findex is > 0 so we do not need to call floor(). + int sampleIndex = (int) findex; + double horizontalFraction = findex - sampleIndex; + int tableIndex = (int) flevel; + + if (tableIndex > (NUM_TABLES - 2)) { + /* + * Just use top table and mix with arithmetic sawtooth if below lowest frequency. + * Generate new fraction for interpolating between 0.0 and lowest table frequency. + */ + double fraction = positivePhaseIncrement * LOWEST_PHASE_INC_INV; + tableBase = tables[(NUM_TABLES - 1)]; + + /* Get adjacent samples. Assume guard point present. */ + sam1 = tableBase[sampleIndex]; + sam2 = tableBase[sampleIndex + 1]; + /* Interpolate between adjacent samples. */ + loSam = sam1 + (horizontalFraction * (sam2 - sam1)); + + /* Use arithmetic version for low frequencies. */ + /* fraction is 0.0 at 0 Hz */ + val = currentPhase + (fraction * (loSam - currentPhase)); + } else { + + double verticalFraction = flevel - tableIndex; + + if (tableIndex < 0) { + if (tableIndex < -1) // above Nyquist! + { + val = 0.0; + } else { + /* + * At top of supported range, interpolate between 0.0 and first partial. + */ + tableBase = tables[0]; /* Sine wave table. */ + + /* Get adjacent samples. Assume guard point present. */ + sam1 = tableBase[sampleIndex]; + sam2 = tableBase[sampleIndex + 1]; + + /* Interpolate between adjacent samples. */ + hiSam = sam1 + (horizontalFraction * (sam2 - sam1)); + /* loSam = 0.0 */ + // verticalFraction is 0.0 at Nyquist + val = verticalFraction * hiSam; + } + } else { + /* + * Interpolate between adjacent levels to prevent harmonics from popping. + */ + tableBase = tables[tableIndex + 1]; + + /* Get adjacent samples. Assume guard point present. */ + sam1 = tableBase[sampleIndex]; + sam2 = tableBase[sampleIndex + 1]; + + /* Interpolate between adjacent samples. */ + hiSam = sam1 + (horizontalFraction * (sam2 - sam1)); + + /* Get adjacent samples. Assume guard point present. */ + tableBase = tables[tableIndex]; + sam1 = tableBase[sampleIndex]; + sam2 = tableBase[sampleIndex + 1]; + + /* Interpolate between adjacent samples. */ + loSam = sam1 + (horizontalFraction * (sam2 - sam1)); + + val = loSam + (verticalFraction * (hiSam - loSam)); + } + } + return val; + } + + public double convertPhaseIncrementToLevel(double positivePhaseIncrement) { + if (positivePhaseIncrement < 1.0e-30) { + positivePhaseIncrement = 1.0e-30; + } + return -1.0 - (Math.log(positivePhaseIncrement) / Math.log(2.0)); + } + + public static MultiTable getInstance() { + return instance; + } + +} diff --git a/src/main/java/com/jsyn/engine/SynthesisEngine.java b/src/main/java/com/jsyn/engine/SynthesisEngine.java new file mode 100644 index 0000000..30872a8 --- /dev/null +++ b/src/main/java/com/jsyn/engine/SynthesisEngine.java @@ -0,0 +1,700 @@ +/* + * 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.engine; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.jsyn.JSyn; +import com.jsyn.Synthesizer; +import com.jsyn.devices.AudioDeviceFactory; +import com.jsyn.devices.AudioDeviceInputStream; +import com.jsyn.devices.AudioDeviceManager; +import com.jsyn.devices.AudioDeviceOutputStream; +import com.jsyn.unitgen.UnitGenerator; +import com.softsynth.shared.time.ScheduledCommand; +import com.softsynth.shared.time.ScheduledQueue; +import com.softsynth.shared.time.TimeStamp; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +//TODO Resolve problem with HearDAHDSR where "Rate" port.set is not reflected in knob. Engine not running. +//TODO new tutorial and docs on website +//TODO AutoStop on DAHDSR +//TODO Test/example SequentialData queueOn and queueOff + +//TODO Abstract device interface. File device! +//TODO Measure thread switching sync, performance for multi-core synthesis. Use 4 core pro. +//TODO Optimize SineOscillatorPhaseModulated +//TODO More circuits. +//TODO DC blocker +//TODO Swing scope probe UIs, auto ranging + +/** + * Internal implementation of JSyn Synthesizer. The public API is in the Synthesizer interface. This + * class might be used directly internally. + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @see Synthesizer + */ +public class SynthesisEngine implements Synthesizer { + + private static final Logger LOGGER = LoggerFactory.getLogger(SynthesisEngine.class); + + private final static int BLOCKS_PER_BUFFER = 8; + private final static int FRAMES_PER_BUFFER = Synthesizer.FRAMES_PER_BLOCK * BLOCKS_PER_BUFFER; + // I have measured JavaSound taking 1200 msec to close devices. + private static final int MAX_THREAD_STOP_TIME = 2000; + + public static final int DEFAULT_FRAME_RATE = 44100; + + private final AudioDeviceManager audioDeviceManager; + private EngineThread engineThread; + private final ScheduledQueue<ScheduledCommand> commandQueue = new ScheduledQueue<ScheduledCommand>(); + + private InterleavingBuffer inputBuffer; + private InterleavingBuffer outputBuffer; + private double inverseNyquist; + private long frameCount; + private boolean pullDataEnabled = true; + private boolean useRealTime = true; + private boolean started; + private int frameRate = DEFAULT_FRAME_RATE; + private double framePeriod = 1.0 / frameRate; + + // List of all units added to the synth. + private final ArrayList<UnitGenerator> allUnitList = new ArrayList<UnitGenerator>(); + // List of running units. + private final ArrayList<UnitGenerator> runningUnitList = new ArrayList<UnitGenerator>(); + // List of units stopping because of autoStop. + private final ArrayList<UnitGenerator> stoppingUnitList = new ArrayList<UnitGenerator>(); + + private LoadAnalyzer loadAnalyzer; + // private int numOutputChannels; + // private int numInputChannels; + private final CopyOnWriteArrayList<Runnable> audioTasks = new CopyOnWriteArrayList<Runnable>(); + private double mOutputLatency; + private double mInputLatency; + /** A fraction corresponding to exactly -96 dB. */ + public static final double DB96 = (1.0 / 63095.73444801943); + /** A fraction that is approximately -90.3 dB. Defined as 1 bit of an S16. */ + public static final double DB90 = (1.0 / (1 << 15)); + + public SynthesisEngine(AudioDeviceManager audioDeviceManager) { + this.audioDeviceManager = audioDeviceManager; + } + + public SynthesisEngine() { + this(AudioDeviceFactory.createAudioDeviceManager()); + } + + @Override + public String getVersion() { + return JSyn.VERSION; + } + + @Override + public int getVersionCode() { + return JSyn.VERSION_CODE; + } + + @Override + public String toString() { + return "JSyn " + JSyn.VERSION_TEXT; + } + + public boolean isPullDataEnabled() { + return pullDataEnabled; + } + + /** + * If set true then audio data will be pulled from the output ports of connected unit + * generators. The final unit in a tree of units needs to be start()ed. + * + * @param pullDataEnabled + */ + public void setPullDataEnabled(boolean pullDataEnabled) { + this.pullDataEnabled = pullDataEnabled; + } + + private void setupAudioBuffers(int numInputChannels, int numOutputChannels) { + inputBuffer = new InterleavingBuffer(FRAMES_PER_BUFFER, Synthesizer.FRAMES_PER_BLOCK, + numInputChannels); + outputBuffer = new InterleavingBuffer(FRAMES_PER_BUFFER, Synthesizer.FRAMES_PER_BLOCK, + numOutputChannels); + } + + public void terminate() { + } + + class InterleavingBuffer { + private final double[] interleavedBuffer; + ChannelBlockBuffer[] blockBuffers; + + InterleavingBuffer(int framesPerBuffer, int framesPerBlock, int samplesPerFrame) { + interleavedBuffer = new double[framesPerBuffer * samplesPerFrame]; + // Allocate buffers for each channel of synthesis output. + blockBuffers = new ChannelBlockBuffer[samplesPerFrame]; + for (int i = 0; i < blockBuffers.length; i++) { + blockBuffers[i] = new ChannelBlockBuffer(framesPerBlock); + } + } + + int deinterleave(int inIndex) { + for (int jf = 0; jf < Synthesizer.FRAMES_PER_BLOCK; jf++) { + for (int iob = 0; iob < blockBuffers.length; iob++) { + ChannelBlockBuffer buffer = blockBuffers[iob]; + buffer.values[jf] = interleavedBuffer[inIndex++]; + } + } + return inIndex; + } + + int interleave(int outIndex) { + for (int jf = 0; jf < Synthesizer.FRAMES_PER_BLOCK; jf++) { + for (int iob = 0; iob < blockBuffers.length; iob++) { + ChannelBlockBuffer buffer = blockBuffers[iob]; + interleavedBuffer[outIndex++] = buffer.values[jf]; + } + } + return outIndex; + } + + public double[] getChannelBuffer(int i) { + return blockBuffers[i].values; + } + + public void clear() { + for (int i = 0; i < blockBuffers.length; i++) { + blockBuffers[i].clear(); + } + } + } + + static class ChannelBlockBuffer { + private final double[] values; + + ChannelBlockBuffer(int framesPerBlock) { + values = new double[framesPerBlock]; + } + + void clear() { + for (int i = 0; i < values.length; i++) { + values[i] = 0.0f; + } + } + } + + @Override + public void start() { + // TODO Use constants. + start(DEFAULT_FRAME_RATE, -1, 0, -1, 2); + } + + @Override + public void start(int frameRate) { + // TODO Use constants. + start(frameRate, -1, 0, -1, 2); + } + + @Override + public synchronized void start(int frameRate, int inputDeviceID, int numInputChannels, + int outputDeviceID, int numOutputChannels) { + if (started) { + LOGGER.info("JSyn already started."); + return; + } + + this.frameRate = frameRate; + this.framePeriod = 1.0 / frameRate; + + setupAudioBuffers(numInputChannels, numOutputChannels); + + LOGGER.info("Pure Java JSyn from www.softsynth.com, rate = " + frameRate + ", " + + (useRealTime ? "RT" : "NON-RealTime") + ", " + JSyn.VERSION_TEXT); + + inverseNyquist = 2.0 / frameRate; + + if (useRealTime) { + engineThread = new EngineThread(inputDeviceID, numInputChannels, + outputDeviceID, numOutputChannels); + LOGGER.debug("Synth thread old priority = " + engineThread.getPriority()); + int engineThreadPriority = engineThread.getPriority() + 2 > Thread.MAX_PRIORITY ? + Thread.MAX_PRIORITY : engineThread.getPriority() + 2; + engineThread.setPriority(engineThreadPriority); + LOGGER.debug("Synth thread new priority = " + engineThread.getPriority()); + engineThread.start(); + } + + started = true; + } + + @Override + public boolean isRunning() { + Thread thread = engineThread; + return (thread != null) && thread.isAlive(); + } + + @Override + public synchronized void stop() { + if (!started) { + LOGGER.info("JSyn already stopped."); + return; + } + + if (useRealTime) { + // Stop audio synthesis and all units. + if (engineThread != null) { + try { + // Interrupt now, otherwise audio thread will wait for audio I/O. + engineThread.requestStop(); + engineThread.join(MAX_THREAD_STOP_TIME); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + synchronized (runningUnitList) { + runningUnitList.clear(); + } + started = false; + } + + private class EngineThread extends Thread + { + private AudioDeviceOutputStream audioOutputStream; + private AudioDeviceInputStream audioInputStream; + private volatile boolean go = true; + + EngineThread(int inputDeviceID, int numInputChannels, + int outputDeviceID, int numOutputChannels) { + if (numInputChannels > 0) { + audioInputStream = audioDeviceManager.createInputStream(inputDeviceID, frameRate, + numInputChannels); + } + if (numOutputChannels > 0) { + audioOutputStream = audioDeviceManager.createOutputStream(outputDeviceID, + frameRate, numOutputChannels); + } + } + + public void requestStop() { + go = false; + interrupt(); + } + + @Override + public void run() { + LOGGER.debug("JSyn synthesis thread starting."); + try { + if (audioInputStream != null) { + LOGGER.debug("JSyn synthesis thread trying to start audio INPUT!"); + audioInputStream.start(); + mInputLatency = audioInputStream.getLatency(); + String msg = String.format("Input Latency in = %5.1f msec", + 1000 * mInputLatency); + LOGGER.debug(msg); + } + if (audioOutputStream != null) { + LOGGER.debug("JSyn synthesis thread trying to start audio OUTPUT!"); + audioOutputStream.start(); + mOutputLatency = audioOutputStream.getLatency(); + String msg = String.format("Output Latency = %5.1f msec", + 1000 * mOutputLatency); + LOGGER.debug(msg); + // Buy some time while we fill the buffer. + audioOutputStream.write(outputBuffer.interleavedBuffer); + } + loadAnalyzer = new LoadAnalyzer(); + while (go) { + boolean throttled = false; + if (audioInputStream != null) { + // This call will block when the input is empty. + audioInputStream.read(inputBuffer.interleavedBuffer); + throttled = true; + } + + loadAnalyzer.start(); + runAudioTasks(); + generateNextBuffer(); + loadAnalyzer.stop(); + + if (audioOutputStream != null) { + // This call will block when the output is full. + audioOutputStream.write(outputBuffer.interleavedBuffer); + throttled = true; + } + if (!throttled && isRealTime()) { + Thread.sleep(2); // avoid spinning and eating up CPU + } + } + + } catch (Throwable e) { + e.printStackTrace(); + go = false; + + } finally { + LOGGER.info("JSyn synthesis thread in finally code."); + // Stop audio system. + if (audioInputStream != null) { + audioInputStream.stop(); + } + if (audioOutputStream != null) { + audioOutputStream.stop(); + } + } + LOGGER.debug("JSyn synthesis thread exiting."); + } + } + + private void runAudioTasks() { + for (Runnable task : audioTasks) { + task.run(); + } + } + + // TODO We need to implement a sharedSleeper like we use in JSyn1. + public void generateNextBuffer() { + int outIndex = 0; + int inIndex = 0; + for (int i = 0; i < BLOCKS_PER_BUFFER; i++) { + if (inputBuffer != null) { + inIndex = inputBuffer.deinterleave(inIndex); + } + + TimeStamp timeStamp = createTimeStamp(); + // Try putting this up here so incoming time-stamped events will get + // scheduled later. + processScheduledCommands(timeStamp); + clearBlockBuffers(); + synthesizeBuffer(); + + if (outputBuffer != null) { + outIndex = outputBuffer.interleave(outIndex); + } + frameCount += Synthesizer.FRAMES_PER_BLOCK; + } + } + + @Override + public double getCurrentTime() { + return frameCount * framePeriod; + } + + @Override + public TimeStamp createTimeStamp() { + return new TimeStamp(getCurrentTime()); + } + + private void processScheduledCommands(TimeStamp timeStamp) { + List<ScheduledCommand> timeList = commandQueue.removeNextList(timeStamp); + + while (timeList != null) { + while (!timeList.isEmpty()) { + ScheduledCommand command = timeList.remove(0); + LOGGER.debug("processing " + command + ", at time " + timeStamp.getTime()); + command.run(); + } + // Get next list of commands at the given time. + timeList = commandQueue.removeNextList(timeStamp); + } + } + + @Override + public void scheduleCommand(TimeStamp timeStamp, ScheduledCommand command) { + if ((Thread.currentThread() == engineThread) && (timeStamp.getTime() <= getCurrentTime())) { + command.run(); + } else { + LOGGER.debug("scheduling " + command + ", at time " + timeStamp.getTime()); + commandQueue.add(timeStamp, command); + } + } + + @Override + public void scheduleCommand(double time, ScheduledCommand command) { + TimeStamp timeStamp = new TimeStamp(time); + scheduleCommand(timeStamp, command); + } + + @Override + public void queueCommand(ScheduledCommand command) { + TimeStamp timeStamp = createTimeStamp(); + scheduleCommand(timeStamp, command); + } + + @Override + public void clearCommandQueue() { + commandQueue.clear(); + } + + private void clearBlockBuffers() { + outputBuffer.clear(); + } + + private void synthesizeBuffer() { + synchronized (runningUnitList) { + ListIterator<UnitGenerator> iterator = runningUnitList.listIterator(); + while (iterator.hasNext()) { + UnitGenerator unit = iterator.next(); + if (pullDataEnabled) { + unit.pullData(getFrameCount(), 0, Synthesizer.FRAMES_PER_BLOCK); + } else { + unit.generate(0, Synthesizer.FRAMES_PER_BLOCK); + } + } + // Remove any units that got auto stopped. + for (UnitGenerator ugen : stoppingUnitList) { + runningUnitList.remove(ugen); + ugen.flattenOutputs(); + } + } + stoppingUnitList.clear(); + } + + public double[] getInputBuffer(int i) { + try { + return inputBuffer.getChannelBuffer(i); + } catch (ArrayIndexOutOfBoundsException e) { + throw new RuntimeException("Audio Input not configured in start() method."); + } + } + + public double[] getOutputBuffer(int i) { + try { + return outputBuffer.getChannelBuffer(i); + } catch (ArrayIndexOutOfBoundsException e) { + throw new RuntimeException("Audio Output not configured in start() method."); + } + } + + private void internalStopUnit(UnitGenerator unit) { + synchronized (runningUnitList) { + runningUnitList.remove(unit); + } + unit.flattenOutputs(); + } + + public void autoStopUnit(UnitGenerator unitGenerator) { + synchronized (stoppingUnitList) { + stoppingUnitList.add(unitGenerator); + } + } + + @Override + public void startUnit(UnitGenerator unit, double time) { + startUnit(unit, new TimeStamp(time)); + } + + @Override + public void stopUnit(UnitGenerator unit, double time) { + stopUnit(unit, new TimeStamp(time)); + } + + @Override + public void startUnit(final UnitGenerator unit, TimeStamp timeStamp) { + // Don't start if it is a component in a circuit because it will be + // executed by the circuit. + if (unit.getCircuit() == null) { + scheduleCommand(timeStamp, new ScheduledCommand() { + @Override + public void run() { + internalStartUnit(unit); + } + }); + } + } + + @Override + public void stopUnit(final UnitGenerator unit, TimeStamp timeStamp) { + scheduleCommand(timeStamp, new ScheduledCommand() { + @Override + public void run() { + internalStopUnit(unit); + } + }); + } + + @Override + public void startUnit(UnitGenerator unit) { + startUnit(unit, createTimeStamp()); + } + + @Override + public void stopUnit(UnitGenerator unit) { + stopUnit(unit, createTimeStamp()); + } + + private void internalStartUnit(UnitGenerator unit) { + // LOGGER.info( "internalStartUnit " + unit + " with circuit " + + // unit.getCircuit() ); + if (unit.getCircuit() == null) { + synchronized (runningUnitList) { + if (!runningUnitList.contains(unit)) { + runningUnitList.add(unit); + } + } + } + // else + // { + // LOGGER.info( + // "internalStartUnit detected race condition !!!! from old JSyn" + unit + // + " with circuit " + unit.getCircuit() ); + // } + } + + public double getInverseNyquist() { + return inverseNyquist; + } + + public double convertTimeToExponentialScaler(double duration) { + // Calculate scaler so that scaler^frames = target/source + double numFrames = duration * getFrameRate(); + return Math.pow(DB90, (1.0 / numFrames)); + } + + @Override + public long getFrameCount() { + return frameCount; + } + + /** + * @return the frameRate + */ + @Override + public int getFrameRate() { + return frameRate; + } + + /** + * @return the inverse of the frameRate for efficiency + */ + @Override + public double getFramePeriod() { + return framePeriod; + } + + /** Convert a short value to a double in the range -1.0 to almost 1.0. */ + public static double convertShortToDouble(short sdata) { + return (sdata * (1.0 / Short.MAX_VALUE)); + } + + /** + * Convert a double value in the range -1.0 to almost 1.0 to a short. Double value is clipped + * before converting. + */ + public static short convertDoubleToShort(double d) { + final double maxValue = ((double) (Short.MAX_VALUE - 1)) / Short.MAX_VALUE; + if (d > maxValue) { + d = maxValue; + } else if (d < -1.0) { + d = -1.0; + } + return (short) (d * Short.MAX_VALUE); + } + + @Override + public void addAudioTask(Runnable blockTask) { + audioTasks.add(blockTask); + } + + @Override + public void removeAudioTask(Runnable blockTask) { + audioTasks.remove(blockTask); + } + + @Override + public double getUsage() { + // use temp so we don't have to synchronize + LoadAnalyzer temp = loadAnalyzer; + if (temp != null) { + return temp.getAverageLoad(); + } else { + return 0.0; + } + } + + @Override + public AudioDeviceManager getAudioDeviceManager() { + return audioDeviceManager; + } + + @Override + public void setRealTime(boolean realTime) { + useRealTime = realTime; + } + + @Override + public boolean isRealTime() { + return useRealTime; + } + + public double getOutputLatency() { + return mOutputLatency; + } + + public double getInputLatency() { + return mInputLatency; + } + + @Override + public void add(UnitGenerator ugen) { + ugen.setSynthesisEngine(this); + allUnitList.add(ugen); + } + + @Override + public void remove(UnitGenerator ugen) { + allUnitList.remove(ugen); + } + + @Override + public void sleepUntil(double time) throws InterruptedException { + double timeToSleep = time - getCurrentTime(); + while (timeToSleep > 0.0) { + if (useRealTime) { + long msecToSleep = (long) (1000 * timeToSleep); + if (msecToSleep <= 0) { + msecToSleep = 1; + } + Thread.sleep(msecToSleep); + } else { + + generateNextBuffer(); + } + timeToSleep = time - getCurrentTime(); + } + } + + @Override + public void sleepFor(double duration) throws InterruptedException { + sleepUntil(getCurrentTime() + duration); + } + + public void printConnections() { + if (pullDataEnabled) { + ListIterator<UnitGenerator> iterator = runningUnitList.listIterator(); + while (iterator.hasNext()) { + UnitGenerator unit = iterator.next(); + unit.printConnections(); + } + } + + } + +} diff --git a/src/main/java/com/jsyn/exceptions/ChannelMismatchException.java b/src/main/java/com/jsyn/exceptions/ChannelMismatchException.java new file mode 100644 index 0000000..a1554cd --- /dev/null +++ b/src/main/java/com/jsyn/exceptions/ChannelMismatchException.java @@ -0,0 +1,35 @@ +/* + * 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.exceptions; + +/** + * This will get thrown if, for example, stereo data is queued to a mono player. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class ChannelMismatchException extends RuntimeException { + + public ChannelMismatchException(String message) { + super(message); + } + + /** + * + */ + private static final long serialVersionUID = -5345224363387498119L; + +} diff --git a/src/main/java/com/jsyn/instruments/DrumWoodFM.java b/src/main/java/com/jsyn/instruments/DrumWoodFM.java new file mode 100644 index 0000000..ba6cd1b --- /dev/null +++ b/src/main/java/com/jsyn/instruments/DrumWoodFM.java @@ -0,0 +1,159 @@ +/* + * 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.instruments; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.unitgen.Add; +import com.jsyn.unitgen.Circuit; +import com.jsyn.unitgen.EnvelopeAttackDecay; +import com.jsyn.unitgen.Multiply; +import com.jsyn.unitgen.PassThrough; +import com.jsyn.unitgen.SineOscillator; +import com.jsyn.unitgen.SineOscillatorPhaseModulated; +import com.jsyn.unitgen.UnitVoice; +import com.jsyn.util.VoiceDescription; +import com.softsynth.shared.time.TimeStamp; + +/** + * Drum instruments using 2 Operator FM. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public class DrumWoodFM extends Circuit implements UnitVoice { + private static final int NUM_PRESETS = 3; + // Declare units and ports. + EnvelopeAttackDecay ampEnv; + SineOscillatorPhaseModulated carrierOsc; + EnvelopeAttackDecay modEnv; + SineOscillator modOsc; + PassThrough freqDistributor; + Add modSummer; + Multiply frequencyMultiplier; + + public UnitInputPort mcratio; + public UnitInputPort index; + public UnitInputPort modRange; + public UnitInputPort frequency; + + public DrumWoodFM() { + // Create unit generators. + add(carrierOsc = new SineOscillatorPhaseModulated()); + add(freqDistributor = new PassThrough()); + add(modSummer = new Add()); + add(ampEnv = new EnvelopeAttackDecay()); + add(modEnv = new EnvelopeAttackDecay()); + add(modOsc = new SineOscillator()); + add(frequencyMultiplier = new Multiply()); + + addPort(mcratio = frequencyMultiplier.inputB, "MCRatio"); + addPort(index = modSummer.inputA, "Index"); + addPort(modRange = modEnv.amplitude, "ModRange"); + addPort(frequency = freqDistributor.input, "Frequency"); + + ampEnv.export(this, "Amp"); + modEnv.export(this, "Mod"); + + freqDistributor.output.connect(carrierOsc.frequency); + freqDistributor.output.connect(frequencyMultiplier.inputA); + + carrierOsc.output.connect(ampEnv.amplitude); + modEnv.output.connect(modSummer.inputB); + modSummer.output.connect(modOsc.amplitude); + modOsc.output.connect(carrierOsc.modulation); + frequencyMultiplier.output.connect(modOsc.frequency); + + // Make the circuit turn off when the envelope finishes to reduce CPU load. + ampEnv.setupAutoDisable(this); + + usePreset(0); + } + + @Override + public void noteOff(TimeStamp timeStamp) { + } + + @Override + public void noteOn(double freq, double ampl, TimeStamp timeStamp) { + carrierOsc.amplitude.set(ampl, timeStamp); + ampEnv.input.trigger(timeStamp); + modEnv.input.trigger(timeStamp); + } + + @Override + public UnitOutputPort getOutput() { + return ampEnv.output; + } + + @Override + public void usePreset(int presetIndex) { + mcratio.setup(0.001, 0.6875, 20.0); + ampEnv.attack.setup(0.001, 0.005, 8.0); + modEnv.attack.setup(0.001, 0.005, 8.0); + + int n = presetIndex % NUM_PRESETS; + switch (n) { + case 0: + ampEnv.decay.setup(0.001, 0.293, 8.0); + modEnv.decay.setup(0.001, 0.07, 8.0); + frequency.setup(0.0, 349.0, 3000.0); + index.setup(0.001, 0.05, 10.0); + modRange.setup(0.001, 0.4, 10.0); + break; + case 1: + default: + ampEnv.decay.setup(0.001, 0.12, 8.0); + modEnv.decay.setup(0.001, 0.06, 8.0); + frequency.setup(0.0, 1400.0, 3000.0); + index.setup(0.001, 0.16, 10.0); + modRange.setup(0.001, 0.17, 10.0); + break; + } + } + + static class MyVoiceDescription extends VoiceDescription { + static String[] presetNames = { + "WoodBlockFM", "ClaveFM" + }; + static String[] tags = { + "electronic", "drum" + }; + + public MyVoiceDescription() { + super("DrumWoodFM", presetNames); + } + + @Override + public UnitVoice createUnitVoice() { + return new DrumWoodFM(); + } + + @Override + public String[] getTags(int presetIndex) { + return tags; + } + + @Override + public String getVoiceClassName() { + return DrumWoodFM.class.getName(); + } + } + + public static VoiceDescription getVoiceDescription() { + return new MyVoiceDescription(); + } +} diff --git a/src/main/java/com/jsyn/instruments/DualOscillatorSynthVoice.java b/src/main/java/com/jsyn/instruments/DualOscillatorSynthVoice.java new file mode 100644 index 0000000..c81041f --- /dev/null +++ b/src/main/java/com/jsyn/instruments/DualOscillatorSynthVoice.java @@ -0,0 +1,301 @@ +/* + * 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.instruments; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.unitgen.Add; +import com.jsyn.unitgen.Circuit; +import com.jsyn.unitgen.EnvelopeDAHDSR; +import com.jsyn.unitgen.FilterFourPoles; +import com.jsyn.unitgen.MorphingOscillatorBL; +import com.jsyn.unitgen.Multiply; +import com.jsyn.unitgen.UnitVoice; +import com.jsyn.util.VoiceDescription; +import com.softsynth.math.AudioMath; +import com.softsynth.shared.time.TimeStamp; + +/** + * Synthesizer voice with two morphing oscillators and a four-pole resonant filter. + * Modulate the amplitude and filter using DAHDSR envelopes. + */ +public class DualOscillatorSynthVoice extends Circuit implements UnitVoice { + private Multiply frequencyMultiplier; + private Multiply amplitudeMultiplier; + private Multiply detuneScaler1; + private Multiply detuneScaler2; + private Multiply amplitudeBoost; + private MorphingOscillatorBL osc1; + private MorphingOscillatorBL osc2; + private FilterFourPoles filter; + private EnvelopeDAHDSR ampEnv; + private EnvelopeDAHDSR filterEnv; + private Add cutoffAdder; + + private static MyVoiceDescription voiceDescription; + + public UnitInputPort amplitude; + public UnitInputPort frequency; + /** + * This scales the frequency value. You can use this to modulate a group of instruments using a + * shared LFO and they will stay in tune. Set to 1.0 for no modulation. + */ + public UnitInputPort frequencyScaler; + public UnitInputPort oscShape1; + public UnitInputPort oscShape2; +// public UnitInputPort oscDetune1; +// public UnitInputPort oscDetune2; + public UnitInputPort cutoff; + public UnitInputPort filterEnvDepth; + public UnitInputPort Q; + + public DualOscillatorSynthVoice() { + add(frequencyMultiplier = new Multiply()); + add(amplitudeMultiplier = new Multiply()); + add(amplitudeBoost = new Multiply()); + add(detuneScaler1 = new Multiply()); + add(detuneScaler2 = new Multiply()); + // Add tone generators. + add(osc1 = new MorphingOscillatorBL()); + add(osc2 = new MorphingOscillatorBL()); + + // Use an envelope to control the amplitude. + add(ampEnv = new EnvelopeDAHDSR()); + + // Use an envelope to control the filter cutoff. + add(filterEnv = new EnvelopeDAHDSR()); + add(filter = new FilterFourPoles()); + add(cutoffAdder = new Add()); + + filterEnv.output.connect(cutoffAdder.inputA); + cutoffAdder.output.connect(filter.frequency); + frequencyMultiplier.output.connect(detuneScaler1.inputA); + frequencyMultiplier.output.connect(detuneScaler2.inputA); + detuneScaler1.output.connect(osc1.frequency); + detuneScaler2.output.connect(osc2.frequency); + osc1.output.connect(amplitudeMultiplier.inputA); // mix oscillators + osc2.output.connect(amplitudeMultiplier.inputA); + amplitudeMultiplier.output.connect(filter.input); + filter.output.connect(amplitudeBoost.inputA); + amplitudeBoost.output.connect(ampEnv.amplitude); + + addPort(amplitude = amplitudeMultiplier.inputB, PORT_NAME_AMPLITUDE); + addPort(frequency = frequencyMultiplier.inputA, PORT_NAME_FREQUENCY); + addPort(oscShape1 = osc1.shape, "OscShape1"); + addPort(oscShape2 = osc2.shape, "OscShape2"); +// addPort(oscDetune1 = osc1.shape, "OscDetune1"); +// addPort(oscDetune2 = osc2.shape, "OscDetune2"); + addPort(cutoff = cutoffAdder.inputB, PORT_NAME_CUTOFF); + addPortAlias(cutoff, PORT_NAME_TIMBRE); + addPort(Q = filter.Q); + addPort(frequencyScaler = frequencyMultiplier.inputB, PORT_NAME_FREQUENCY_SCALER); + addPort(filterEnvDepth = filterEnv.amplitude, "FilterEnvDepth"); + + filterEnv.export(this, "Filter"); + ampEnv.export(this, "Amp"); + + frequency.setup(osc1.frequency); + frequencyScaler.setup(0.2, 1.0, 4.0); + cutoff.setup(filter.frequency); + // Allow negative filter sweeps + filterEnvDepth.setup(-4000.0, 2000.0, 4000.0); + + // set amplitudes slightly different so that they never entirely cancel + osc1.amplitude.set(0.5); + osc2.amplitude.set(0.4); + // Make the circuit turn off when the envelope finishes to reduce CPU load. + ampEnv.setupAutoDisable(this); + // Add named port for mapping pressure. + amplitudeBoost.inputB.setup(1.0, 1.0, 4.0); + addPortAlias(amplitudeBoost.inputB, PORT_NAME_PRESSURE); + + usePreset(0); + } + + /** + * The first oscillator will be tuned UP by semitoneOffset/2. + * The second oscillator will be tuned DOWN by semitoneOffset/2. + * @param semitoneOffset + */ + private void setDetunePitch(double semitoneOffset) { + double halfOffset = semitoneOffset * 0.5; + setDetunePitch1(halfOffset); + setDetunePitch2(-halfOffset); + } + + /** + * Set the detuning for osc1 in semitones. + * @param semitoneOffset + */ + private void setDetunePitch1(double semitoneOffset) { + double scale = AudioMath.semitonesToFrequencyScaler(semitoneOffset); + detuneScaler1.inputB.set(scale); + } + + /** + * Set the detuning for osc2 in semitones. + * @param semitoneOffset + */ + private void setDetunePitch2(double semitoneOffset) { + double scale = AudioMath.semitonesToFrequencyScaler(semitoneOffset); + detuneScaler2.inputB.set(scale); + } + + @Override + public void noteOff(TimeStamp timeStamp) { + ampEnv.input.off(timeStamp); + filterEnv.input.off(timeStamp); + } + + @Override + public void noteOn(double freq, double ampl, TimeStamp timeStamp) { + frequency.set(freq, timeStamp); + amplitude.set(ampl, timeStamp); + ampEnv.input.on(timeStamp); + filterEnv.input.on(timeStamp); + } + + @Override + public UnitOutputPort getOutput() { + return ampEnv.output; + } + + // Reset to basic voice. + public void reset() { + osc1.shape.set(0.0); + osc2.shape.set(0.0); + ampEnv.attack.set(0.005); + ampEnv.decay.set(0.2); + ampEnv.sustain.set(0.5); + ampEnv.release.set(1.0); + filterEnv.attack.set(0.01); + filterEnv.decay.set(0.6); + filterEnv.sustain.set(0.4); + filterEnv.release.set(1.0); + cutoff.set(500.0); + filterEnvDepth.set(3000.0); + filter.reset(); + filter.Q.set(3.9); + setDetunePitch(0.02); + } + + @Override + public void usePreset(int presetIndex) { + reset(); // start from known configuration + int n = presetIndex % presetNames.length; + switch (n) { + case 0: + break; + case 1: + ampEnv.attack.set(0.1); + ampEnv.decay.set(0.9); + ampEnv.sustain.set(0.1); + ampEnv.release.set(0.1); + cutoff.set(500.0); + filterEnvDepth.set(500.0); + filter.Q.set(3.0); + break; + case 2: + ampEnv.attack.set(0.1); + ampEnv.decay.set(0.3); + ampEnv.release.set(0.5); + cutoff.set(2000.0); + filterEnvDepth.set(500.0); + filter.Q.set(2.0); + break; + case 3: + osc1.shape.set(-0.9); + osc2.shape.set(-0.8); + ampEnv.attack.set(0.3); + ampEnv.decay.set(0.8); + ampEnv.release.set(0.2); + filterEnv.sustain.set(0.7); + cutoff.set(500.0); + filterEnvDepth.set(500.0); + filter.Q.set(3.0); + break; + case 4: + osc1.shape.set(1.0); + osc2.shape.set(0.0); + break; + case 5: + osc1.shape.set(1.0); + setDetunePitch1(0.0); + osc2.shape.set(0.9); + setDetunePitch1(7.0); + break; + case 6: + osc1.shape.set(0.6); + osc2.shape.set(-0.2); + setDetunePitch1(0.01); + ampEnv.attack.set(0.005); + ampEnv.decay.set(0.09); + ampEnv.sustain.set(0.0); + ampEnv.release.set(1.0); + filterEnv.attack.set(0.005); + filterEnv.decay.set(0.1); + filterEnv.sustain.set(0.4); + filterEnv.release.set(1.0); + cutoff.set(2000.0); + filterEnvDepth.set(5000.0); + filter.Q.set(7.02); + break; + default: + break; + } + } + + private static final String[] presetNames = { + "FastSaw", "SlowSaw", "BrightSaw", + "SoftSine", "SquareSaw", "SquareFifth", + "Blip" + }; + + static class MyVoiceDescription extends VoiceDescription { + String[] tags = { + "electronic", "filter", "analog", "subtractive" + }; + + public MyVoiceDescription() { + super(DualOscillatorSynthVoice.class.getName(), presetNames); + } + + @Override + public UnitVoice createUnitVoice() { + return new DualOscillatorSynthVoice(); + } + + @Override + public String[] getTags(int presetIndex) { + return tags; + } + + @Override + public String getVoiceClassName() { + return DualOscillatorSynthVoice.class.getName(); + } + } + + public static VoiceDescription getVoiceDescription() { + if (voiceDescription == null) { + voiceDescription = new MyVoiceDescription(); + } + return voiceDescription; + } + + +} diff --git a/src/main/java/com/jsyn/instruments/JSynInstrumentLibrary.java b/src/main/java/com/jsyn/instruments/JSynInstrumentLibrary.java new file mode 100644 index 0000000..9f111c3 --- /dev/null +++ b/src/main/java/com/jsyn/instruments/JSynInstrumentLibrary.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.instruments; + +import com.jsyn.swing.InstrumentBrowser; +import com.jsyn.util.InstrumentLibrary; +import com.jsyn.util.VoiceDescription; + +/** + * Stock instruments provided with the JSyn distribution. + * + * @author Phil Burk (C) 2011 Mobileer Inc + * @see InstrumentBrowser + */ + +public class JSynInstrumentLibrary implements InstrumentLibrary { + static VoiceDescription[] descriptions = { + WaveShapingVoice.getVoiceDescription(), + SubtractiveSynthVoice.getVoiceDescription(), + DualOscillatorSynthVoice.getVoiceDescription(), + NoiseHit.getVoiceDescription(), + DrumWoodFM.getVoiceDescription() + }; + + @Override + public VoiceDescription[] getVoiceDescriptions() { + return descriptions; + } + + @Override + public String getName() { + return "JSynInstruments"; + } +} diff --git a/src/main/java/com/jsyn/instruments/NoiseHit.java b/src/main/java/com/jsyn/instruments/NoiseHit.java new file mode 100644 index 0000000..b8714fc --- /dev/null +++ b/src/main/java/com/jsyn/instruments/NoiseHit.java @@ -0,0 +1,114 @@ +/* + * 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.instruments; + +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.unitgen.Circuit; +import com.jsyn.unitgen.EnvelopeAttackDecay; +import com.jsyn.unitgen.PinkNoise; +import com.jsyn.unitgen.UnitVoice; +import com.jsyn.util.VoiceDescription; +import com.softsynth.shared.time.TimeStamp; + +/** + * Cheap synthetic cymbal sound. + */ +public class NoiseHit extends Circuit implements UnitVoice { + EnvelopeAttackDecay ampEnv; + PinkNoise noise; + private static final int NUM_PRESETS = 3; + + public NoiseHit() { + // Create unit generators. + add(noise = new PinkNoise()); + add(ampEnv = new EnvelopeAttackDecay()); + noise.output.connect(ampEnv.amplitude); + + ampEnv.export(this, "Amp"); + + // Make the circuit turn off when the envelope finishes to reduce CPU load. + ampEnv.setupAutoDisable(this); + + usePreset(0); + } + + @Override + public void noteOff(TimeStamp timeStamp) { + } + + @Override + public void noteOn(double freq, double ampl, TimeStamp timeStamp) { + noise.amplitude.set(ampl, timeStamp); + ampEnv.input.trigger(); + } + + @Override + public UnitOutputPort getOutput() { + return ampEnv.output; + } + + @Override + public void usePreset(int presetIndex) { + int n = presetIndex % NUM_PRESETS; + switch (n) { + case 0: + ampEnv.attack.set(0.001); + ampEnv.decay.set(0.1); + break; + case 1: + ampEnv.attack.set(0.03); + ampEnv.decay.set(1.4); + break; + default: + ampEnv.attack.set(0.9); + ampEnv.decay.set(0.3); + break; + } + } + + static class MyVoiceDescription extends VoiceDescription { + static String[] presetNames = { + "ShortNoiseHit", "LongNoiseHit", "SlowNoiseHit" + }; + static String[] tags = { + "electronic", "noise" + }; + + public MyVoiceDescription() { + super("NoiseHit", presetNames); + } + + @Override + public UnitVoice createUnitVoice() { + return new NoiseHit(); + } + + @Override + public String[] getTags(int presetIndex) { + return tags; + } + + @Override + public String getVoiceClassName() { + return NoiseHit.class.getName(); + } + } + + public static VoiceDescription getVoiceDescription() { + return new MyVoiceDescription(); + } +} diff --git a/src/main/java/com/jsyn/instruments/SubtractiveSynthVoice.java b/src/main/java/com/jsyn/instruments/SubtractiveSynthVoice.java new file mode 100644 index 0000000..5cfc4b9 --- /dev/null +++ b/src/main/java/com/jsyn/instruments/SubtractiveSynthVoice.java @@ -0,0 +1,182 @@ +/* + * 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.instruments; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.unitgen.Add; +import com.jsyn.unitgen.Circuit; +import com.jsyn.unitgen.EnvelopeDAHDSR; +import com.jsyn.unitgen.FilterLowPass; +import com.jsyn.unitgen.Multiply; +import com.jsyn.unitgen.SawtoothOscillatorBL; +import com.jsyn.unitgen.UnitOscillator; +import com.jsyn.unitgen.UnitVoice; +import com.jsyn.util.VoiceDescription; +import com.softsynth.shared.time.TimeStamp; + +/** + * Typical synthesizer voice with one oscillator and a biquad resonant filter. Modulate the amplitude and + * filter using DAHDSR envelopes. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class SubtractiveSynthVoice extends Circuit implements UnitVoice { + private UnitOscillator osc; + private FilterLowPass filter; + private EnvelopeDAHDSR ampEnv; + private EnvelopeDAHDSR filterEnv; + private Add cutoffAdder; + private Multiply frequencyScaler; + + public UnitInputPort amplitude; + public UnitInputPort frequency; + /** + * This scales the frequency value. You can use this to modulate a group of instruments using a + * shared LFO and they will stay in tune. + */ + public UnitInputPort pitchModulation; + public UnitInputPort cutoff; + public UnitInputPort cutoffRange; + public UnitInputPort Q; + + public SubtractiveSynthVoice() { + add(frequencyScaler = new Multiply()); + // Add a tone generator. + add(osc = new SawtoothOscillatorBL()); + + // Use an envelope to control the amplitude. + add(ampEnv = new EnvelopeDAHDSR()); + + // Use an envelope to control the filter cutoff. + add(filterEnv = new EnvelopeDAHDSR()); + add(filter = new FilterLowPass()); + add(cutoffAdder = new Add()); + + filterEnv.output.connect(cutoffAdder.inputA); + cutoffAdder.output.connect(filter.frequency); + frequencyScaler.output.connect(osc.frequency); + osc.output.connect(filter.input); + filter.output.connect(ampEnv.amplitude); + + addPort(amplitude = osc.amplitude, "Amplitude"); + addPort(frequency = frequencyScaler.inputA, "Frequency"); + addPort(pitchModulation = frequencyScaler.inputB, "PitchMod"); + addPort(cutoff = cutoffAdder.inputB, "Cutoff"); + addPort(cutoffRange = filterEnv.amplitude, "CutoffRange"); + addPort(Q = filter.Q); + + ampEnv.export(this, "Amp"); + filterEnv.export(this, "Filter"); + + frequency.setup(osc.frequency); + pitchModulation.setup(0.2, 1.0, 4.0); + cutoff.setup(filter.frequency); + cutoffRange.setup(filter.frequency); + + // Make the circuit turn off when the envelope finishes to reduce CPU load. + ampEnv.setupAutoDisable(this); + + usePreset(0); + } + + @Override + public void noteOff(TimeStamp timeStamp) { + ampEnv.input.off(timeStamp); + filterEnv.input.off(timeStamp); + } + + @Override + public void noteOn(double freq, double ampl, TimeStamp timeStamp) { + frequency.set(freq, timeStamp); + amplitude.set(ampl, timeStamp); + + ampEnv.input.on(timeStamp); + filterEnv.input.on(timeStamp); + } + + @Override + public UnitOutputPort getOutput() { + return ampEnv.output; + } + + @Override + public void usePreset(int presetIndex) { + int n = presetIndex % presetNames.length; + switch (n) { + case 0: + ampEnv.attack.set(0.01); + ampEnv.decay.set(0.2); + ampEnv.release.set(1.0); + cutoff.set(500.0); + cutoffRange.set(500.0); + filter.Q.set(1.0); + break; + case 1: + ampEnv.attack.set(0.5); + ampEnv.decay.set(0.3); + ampEnv.release.set(0.2); + cutoff.set(500.0); + cutoffRange.set(500.0); + filter.Q.set(3.0); + break; + case 2: + default: + ampEnv.attack.set(0.1); + ampEnv.decay.set(0.3); + ampEnv.release.set(0.5); + cutoff.set(2000.0); + cutoffRange.set(500.0); + filter.Q.set(2.0); + break; + } + } + + static String[] presetNames = { + "FastSaw", "SlowSaw", "BrightSaw" + }; + + static class MyVoiceDescription extends VoiceDescription { + String[] tags = { + "electronic", "filter", "clean" + }; + + public MyVoiceDescription() { + super("SubtractiveSynth", presetNames); + } + + @Override + public UnitVoice createUnitVoice() { + return new SubtractiveSynthVoice(); + } + + @Override + public String[] getTags(int presetIndex) { + return tags; + } + + @Override + public String getVoiceClassName() { + return SubtractiveSynthVoice.class.getName(); + } + } + + public static VoiceDescription getVoiceDescription() { + return new MyVoiceDescription(); + } + +} diff --git a/src/main/java/com/jsyn/instruments/WaveShapingVoice.java b/src/main/java/com/jsyn/instruments/WaveShapingVoice.java new file mode 100644 index 0000000..5044f21 --- /dev/null +++ b/src/main/java/com/jsyn/instruments/WaveShapingVoice.java @@ -0,0 +1,187 @@ +/* + * 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.instruments; + +import com.jsyn.data.DoubleTable; +import com.jsyn.ports.UnitFunctionPort; +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.unitgen.Circuit; +import com.jsyn.unitgen.EnvelopeDAHDSR; +import com.jsyn.unitgen.FunctionEvaluator; +import com.jsyn.unitgen.Multiply; +import com.jsyn.unitgen.SineOscillator; +import com.jsyn.unitgen.UnitOscillator; +import com.jsyn.unitgen.UnitVoice; +import com.jsyn.util.VoiceDescription; +import com.softsynth.math.ChebyshevPolynomial; +import com.softsynth.math.PolynomialTableData; +import com.softsynth.shared.time.TimeStamp; + +/** + * Waveshaping oscillator with envelopes. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public class WaveShapingVoice extends Circuit implements UnitVoice { + private static final long serialVersionUID = -2704222221111608377L; + private static final int NUM_PRESETS = 3; + private UnitOscillator osc; + private FunctionEvaluator waveShaper; + private EnvelopeDAHDSR ampEnv; + private EnvelopeDAHDSR rangeEnv; + private Multiply frequencyScaler; + + public UnitInputPort range; + public UnitInputPort frequency; + public UnitInputPort amplitude; + public UnitFunctionPort function; + public UnitInputPort pitchModulation; + + // default Chebyshev polynomial table to share. + private static DoubleTable chebyshevTable; + private final static int CHEBYSHEV_ORDER = 11; + + static { + // Make table with Chebyshev polynomial to share among voices + PolynomialTableData chebData = new PolynomialTableData( + ChebyshevPolynomial.T(CHEBYSHEV_ORDER), 1024); + chebyshevTable = new DoubleTable(chebData.getData()); + } + + public WaveShapingVoice() { + add(frequencyScaler = new Multiply()); + add(osc = new SineOscillator()); + add(waveShaper = new FunctionEvaluator()); + add(rangeEnv = new EnvelopeDAHDSR()); + add(ampEnv = new EnvelopeDAHDSR()); + + addPort(amplitude = ampEnv.amplitude); + addPort(range = osc.amplitude, "Range"); + addPort(function = waveShaper.function); + addPort(frequency = frequencyScaler.inputA, "Frequency"); + addPort(pitchModulation = frequencyScaler.inputB, "PitchMod"); + + ampEnv.export(this, "Amp"); + rangeEnv.export(this, "Range"); + + function.set(chebyshevTable); + + // Connect units. + osc.output.connect(rangeEnv.amplitude); + rangeEnv.output.connect(waveShaper.input); + ampEnv.output.connect(waveShaper.amplitude); + frequencyScaler.output.connect(osc.frequency); + + // Set reasonable defaults for the ports. + pitchModulation.setup(0.1, 1.0, 10.0); + range.setup(0.1, 0.8, 1.0); + frequency.setup(osc.frequency); + amplitude.setup(0.0, 0.5, 1.0); + + // Make the circuit turn off when the envelope finishes to reduce CPU load. + ampEnv.setupAutoDisable(this); + + usePreset(2); + } + + @Override + public UnitOutputPort getOutput() { + return waveShaper.output; + } + + @Override + public void noteOn(double freq, double amp, TimeStamp timeStamp) { + frequency.set(freq, timeStamp); + amplitude.set(amp, timeStamp); + ampEnv.input.on(timeStamp); + rangeEnv.input.on(timeStamp); + } + + @Override + public void noteOff(TimeStamp timeStamp) { + ampEnv.input.off(timeStamp); + rangeEnv.input.off(timeStamp); + } + + @Override + public void usePreset(int presetIndex) { + int n = presetIndex % NUM_PRESETS; + switch (n) { + case 0: + ampEnv.attack.set(0.01); + ampEnv.decay.set(0.2); + ampEnv.release.set(1.0); + rangeEnv.attack.set(0.01); + rangeEnv.decay.set(0.2); + rangeEnv.sustain.set(0.4); + rangeEnv.release.set(1.0); + break; + case 1: + ampEnv.attack.set(0.5); + ampEnv.decay.set(0.3); + ampEnv.release.set(0.2); + rangeEnv.attack.set(0.03); + rangeEnv.decay.set(0.2); + rangeEnv.sustain.set(0.5); + rangeEnv.release.set(1.0); + break; + default: + ampEnv.attack.set(0.1); + ampEnv.decay.set(0.3); + ampEnv.release.set(0.5); + rangeEnv.attack.set(0.01); + rangeEnv.decay.set(0.2); + rangeEnv.sustain.set(0.9); + rangeEnv.release.set(1.0); + break; + } + } + + static class MyVoiceDescription extends VoiceDescription { + static String[] presetNames = { + "FastChebyshev", "SlowChebyshev", "BrightChebyshev" + }; + static String[] tags = { + "electronic", "waveshaping", "clean" + }; + + public MyVoiceDescription() { + super("Waveshaping", presetNames); + } + + @Override + public UnitVoice createUnitVoice() { + return new WaveShapingVoice(); + } + + @Override + public String[] getTags(int presetIndex) { + return tags; + } + + @Override + public String getVoiceClassName() { + return WaveShapingVoice.class.getName(); + } + } + + public static VoiceDescription getVoiceDescription() { + return new MyVoiceDescription(); + } + +} diff --git a/src/main/java/com/jsyn/io/AudioFifo.java b/src/main/java/com/jsyn/io/AudioFifo.java new file mode 100644 index 0000000..0c563e4 --- /dev/null +++ b/src/main/java/com/jsyn/io/AudioFifo.java @@ -0,0 +1,204 @@ +/* + * 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.io; + +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * FIFO that implements AudioInputStream, AudioOutputStream interfaces. This can be used to send + * audio data between different threads. The reads or writes may or may not wait based on flags. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class AudioFifo implements AudioInputStream, AudioOutputStream { + // These indices run double the FIFO size so that we can tell empty from full. + private volatile int readIndex; + private volatile int writeIndex; + private volatile double[] buffer; + // Used to mask the index into range when accessing the buffer array. + private int accessMask; + // Used to mask the index so it wraps around. + private int sizeMask; + private boolean writeWaitEnabled = true; + private boolean readWaitEnabled = true; + final Lock lock = new ReentrantLock(); + final Condition notFull = lock.newCondition(); + final Condition notEmpty = lock.newCondition(); + + /** + * @param size Number of doubles in the FIFO. Must be a power of 2. Eg. 1024. + */ + public void allocate(int size) { + if (!isPowerOfTwo(size)) { + throw new IllegalArgumentException("Size must be a power of two."); + } + buffer = new double[size]; + accessMask = size - 1; + sizeMask = (size * 2) - 1; + } + + public int size() { + return buffer.length; + } + + public static boolean isPowerOfTwo(int size) { + return ((size & (size - 1)) == 0); + } + + /** How many samples are available for reading without blocking? */ + @Override + public int available() { + return (writeIndex - readIndex) & sizeMask; + } + + @Override + public void close() { + // TODO Maybe we should tell any thread that is waiting that the FIFO is closed. + } + + @Override + public double read() { + double value = Double.NaN; + if (readWaitEnabled) { + lock.lock(); + try { + while (available() < 1) { + try { + notEmpty.await(); + } catch (InterruptedException e) { + return Double.NaN; + } + } + value = readOneInternal(); + } finally { + lock.unlock(); + } + + } else { + if (readIndex != writeIndex) { + value = readOneInternal(); + } + } + + if (writeWaitEnabled) { + lock.lock(); + notFull.signal(); + lock.unlock(); + } + + return value; + } + + private double readOneInternal() { + double value = buffer[readIndex & accessMask]; + readIndex = (readIndex + 1) & sizeMask; + return value; + } + + @Override + public void write(double value) { + if (writeWaitEnabled) { + lock.lock(); + try { + while (available() == buffer.length) + { + try { + notFull.await(); + } catch (InterruptedException e) { + return; // Silently fail + } + } + writeOneInternal(value); + } finally { + lock.unlock(); + } + + } else { + if (available() != buffer.length) { + writeOneInternal(value); + } + } + + if (readWaitEnabled) { + lock.lock(); + notEmpty.signal(); + lock.unlock(); + } + } + + private void writeOneInternal(double value) { + buffer[writeIndex & accessMask] = value; + writeIndex = (writeIndex + 1) & sizeMask; + } + + @Override + public int read(double[] buffer) { + return read(buffer, 0, buffer.length); + } + + @Override + public int read(double[] buffer, int start, int count) { + if (readWaitEnabled) { + for (int i = 0; i < count; i++) { + buffer[i + start] = read(); + } + } else { + if (available() < count) { + count = available(); + } else { + for (int i = 0; i < count; i++) { + buffer[i + start] = read(); + } + } + } + return count; + } + + @Override + public void write(double[] buffer) { + write(buffer, 0, buffer.length); + } + + @Override + public void write(double[] buffer, int start, int count) { + for (int i = 0; i < count; i++) { + write(buffer[i + start]); + } + } + + /** If true then a subsequent write call will wait if there is no room to write. */ + public void setWriteWaitEnabled(boolean enabled) { + writeWaitEnabled = enabled; + + } + + /** If true then a subsequent read call will wait if there is no data to read. */ + public void setReadWaitEnabled(boolean enabled) { + readWaitEnabled = enabled; + + } + + public boolean isWriteWaitEnabled() { + return writeWaitEnabled; + } + + public boolean isReadWaitEnabled() { + return readWaitEnabled; + } +} diff --git a/src/main/java/com/jsyn/io/AudioInputStream.java b/src/main/java/com/jsyn/io/AudioInputStream.java new file mode 100644 index 0000000..f233ff1 --- /dev/null +++ b/src/main/java/com/jsyn/io/AudioInputStream.java @@ -0,0 +1,46 @@ +/* + * 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.io; + +public interface AudioInputStream { + public double read(); + + /** + * Try to fill the entire buffer. + * + * @param buffer + * @return number of samples read + */ + public int read(double[] buffer); + + /** + * Read from the stream. Block until some data is available. + * + * @param buffer + * @param start index of first sample in buffer + * @param count number of samples to read, for example count=8 for 4 stereo frames + * @return number of samples read + */ + public int read(double[] buffer, int start, int count); + + public void close(); + + /** + * @return number of samples currently available to read without blocking + */ + public int available(); +} diff --git a/src/main/java/com/jsyn/io/AudioOutputStream.java b/src/main/java/com/jsyn/io/AudioOutputStream.java new file mode 100644 index 0000000..dada577 --- /dev/null +++ b/src/main/java/com/jsyn/io/AudioOutputStream.java @@ -0,0 +1,29 @@ +/* + * 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.io; + +import java.io.IOException; + +public interface AudioOutputStream { + public void write(double value) throws IOException; + + public void write(double[] buffer) throws IOException; + + public void write(double[] buffer, int start, int count) throws IOException; + + public void close() throws IOException; +} diff --git a/src/main/java/com/jsyn/midi/MessageParser.java b/src/main/java/com/jsyn/midi/MessageParser.java new file mode 100644 index 0000000..d0f5d4d --- /dev/null +++ b/src/main/java/com/jsyn/midi/MessageParser.java @@ -0,0 +1,147 @@ +/* + * 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.midi; + +/** + * Parse the message and call the appropriate method to handle it. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class MessageParser { + private int[] parameterIndices = new int[MidiConstants.MAX_CHANNELS]; + private int[] parameterValues = new int[MidiConstants.MAX_CHANNELS]; + private int BIT_NON_RPM = 1 << 14; + private int MASK_14BIT = (1 << 14) - 1; + + public void parse(byte[] message) { + int status = message[0]; + int command = status & 0xF0; + int channel = status & 0x0F; + + switch (command) { + case MidiConstants.NOTE_ON: + int velocity = message[2]; + if (velocity == 0) { + noteOff(channel, message[1], velocity); + } else { + noteOn(channel, message[1], velocity); + } + break; + + case MidiConstants.NOTE_OFF: + noteOff(channel, message[1], message[2]); + break; + + case MidiConstants.POLYPHONIC_AFTERTOUCH: + polyphonicAftertouch(channel, message[1], message[2]); + break; + + case MidiConstants.CHANNEL_PRESSURE: + channelPressure(channel, message[1]); + break; + + case MidiConstants.CONTROL_CHANGE: + rawControlChange(channel, message[1], message[2]); + break; + + case MidiConstants.PROGRAM_CHANGE: + programChange(channel, message[1]); + break; + + case MidiConstants.PITCH_BEND: + int bend = (message[2] << 7) + message[1]; + pitchBend(channel, bend); + break; + } + + } + + public void rawControlChange(int channel, int index, int value) { + int paramIndex; + int paramValue; + switch(index) { + case MidiConstants.CONTROLLER_DATA_ENTRY: + parameterValues[channel] = value << 7; + fireParameterChange(channel); + break; + case MidiConstants.CONTROLLER_DATA_ENTRY_LSB: + paramValue = parameterValues[channel] & ~0x7F; + paramValue |= value; + parameterValues[channel] = paramValue; + fireParameterChange(channel); + break; + case MidiConstants.CONTROLLER_NRPN_LSB: + paramIndex = parameterIndices[channel] & ~0x7F; + paramIndex |= value | BIT_NON_RPM; + parameterIndices[channel] = paramIndex; + break; + case MidiConstants.CONTROLLER_NRPN_MSB: + parameterIndices[channel] = (value << 7) | BIT_NON_RPM;; + break; + case MidiConstants.CONTROLLER_RPN_LSB: + paramIndex = parameterIndices[channel] & ~0x7F; + paramIndex |= value; + parameterIndices[channel] = paramIndex; + break; + case MidiConstants.CONTROLLER_RPN_MSB: + parameterIndices[channel] = value << 7; + break; + default: + controlChange(channel, index, value); + break; + + } + } + + private void fireParameterChange(int channel) { + int paramIndex; + paramIndex = parameterIndices[channel]; + if ((paramIndex & BIT_NON_RPM) == 0) { + registeredParameter(channel, paramIndex, parameterValues[channel]); + } else { + nonRegisteredParameter(channel, paramIndex & MASK_14BIT, parameterValues[channel]); + } + } + + public void nonRegisteredParameter(int channel, int index14, int value14) { + } + + public void registeredParameter(int channel, int index14, int value14) { + } + + public void pitchBend(int channel, int bend) { + } + + public void programChange(int channel, int program) { + } + + public void polyphonicAftertouch(int channel, int pitch, int pressure) { + } + + public void channelPressure(int channel, int pressure) { + } + + public void controlChange(int channel, int index, int value) { + } + + public void noteOn(int channel, int pitch, int velocity) { + } + + // If a NOTE_ON with zero velocity is received then noteOff will be called. + public void noteOff(int channel, int pitch, int velocity) { + } +} diff --git a/src/main/java/com/jsyn/midi/MidiConstants.java b/src/main/java/com/jsyn/midi/MidiConstants.java new file mode 100644 index 0000000..8c92119 --- /dev/null +++ b/src/main/java/com/jsyn/midi/MidiConstants.java @@ -0,0 +1,84 @@ +/* + * 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.midi; + +/** + * Constants that define the MIDI standard. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class MidiConstants { + + public static final int MAX_CHANNELS = 16; + // Basic commands. + public static final int NOTE_OFF = 0x80; + public static final int NOTE_ON = 0x90; + public static final int POLYPHONIC_AFTERTOUCH = 0xA0; + public static final int CONTROL_CHANGE = 0xB0; + public static final int PROGRAM_CHANGE = 0xC0; + public static final int CHANNEL_AFTERTOUCH = 0xD0; + public static final int CHANNEL_PRESSURE = CHANNEL_AFTERTOUCH; + public static final int PITCH_BEND = 0xE0; + public static final int SYSTEM_COMMON = 0xF0; + + public static final int PITCH_BEND_CENTER = 0x2000; + + public static final int CONTROLLER_BANK_SELECT = 0; + public static final int CONTROLLER_MOD_WHEEL = 1; + public static final int CONTROLLER_BREATH = 2; + public static final int CONTROLLER_DATA_ENTRY = 6; + public static final int CONTROLLER_VOLUME = 7; + public static final int CONTROLLER_PAN = 10; + + public static final int CONTROLLER_LSB_OFFSET = 32; + public static final int CONTROLLER_DATA_ENTRY_LSB = CONTROLLER_DATA_ENTRY + CONTROLLER_LSB_OFFSET; + + public static final int CONTROLLER_TIMBRE = 74; // Often used by MPE for Y axis control. + + public static final int CONTROLLER_DATA_INCREMENT = 96; + public static final int CONTROLLER_DATA_DECREMENT = 97; + public static final int CONTROLLER_NRPN_LSB = 98; + public static final int CONTROLLER_NRPN_MSB = 99; + public static final int CONTROLLER_RPN_LSB = 100; + public static final int CONTROLLER_RPN_MSB = 101; + + public static final int RPN_BEND_RANGE = 0; + public static final int RPN_FINE_TUNING = 1; + + public static final String PITCH_NAMES[] = { + "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" + }; + + /** + * Calculate frequency in Hertz based on MIDI pitch. Middle C is 60.0. You can use fractional + * pitches so 60.5 would give you a pitch half way between C and C#. + */ + static final double CONCERT_A_FREQUENCY = 440.0; + static final double CONCERT_A_PITCH = 69.0; + + public static double convertPitchToFrequency(double pitch) { + return CONCERT_A_FREQUENCY * Math.pow(2.0, ((pitch - CONCERT_A_PITCH) / 12.0)); + } + + /** + * Calculate MIDI pitch based on frequency in Hertz. Middle C is 60.0. + */ + public static double convertFrequencyToPitch(double frequency) { + return CONCERT_A_PITCH + (12 * Math.log(frequency / CONCERT_A_FREQUENCY) / Math.log(2.0)); + } + +} diff --git a/src/main/java/com/jsyn/midi/MidiSynthesizer.java b/src/main/java/com/jsyn/midi/MidiSynthesizer.java new file mode 100644 index 0000000..e5dbae7 --- /dev/null +++ b/src/main/java/com/jsyn/midi/MidiSynthesizer.java @@ -0,0 +1,121 @@ +/* + * 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.midi; + +import com.jsyn.util.MultiChannelSynthesizer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Map MIDI messages into calls to a MultiChannelSynthesizer. + * Handles CONTROLLER_MOD_WHEEL, TIMBRE, VOLUME and PAN. + * Handles Bend Range RPN. + * + * <pre><code> + voiceDescription = DualOscillatorSynthVoice.getVoiceDescription(); + multiSynth = new MultiChannelSynthesizer(); + final int startChannel = 0; + multiSynth.setup(synth, startChannel, NUM_CHANNELS, VOICES_PER_CHANNEL, voiceDescription); + midiSynthesizer = new MidiSynthesizer(multiSynth); + // pass MIDI bytes + midiSynthesizer.onReceive(bytes, 0, bytes.length); + </code></pre> + * + * See the example UseMidiKeyboard.java + * + * @author Phil Burk (C) 2016 Mobileer Inc + */ +public class MidiSynthesizer extends MessageParser { + + private static final Logger LOGGER = LoggerFactory.getLogger(MidiSynthesizer.class); + + private MultiChannelSynthesizer multiSynth; + + public MidiSynthesizer(MultiChannelSynthesizer multiSynth) { + this.multiSynth = multiSynth; + } + + @Override + public void controlChange(int channel, int index, int value) { + //LOGGER.debug("controlChange(" + channel + ", " + index + ", " + value + ")"); + double normalized = value * (1.0 / 127.0); + switch (index) { + case MidiConstants.CONTROLLER_MOD_WHEEL: + double vibratoDepth = 0.1 * normalized; + LOGGER.debug( "vibratoDepth = " + vibratoDepth ); + multiSynth.setVibratoDepth(channel, vibratoDepth); + break; + case MidiConstants.CONTROLLER_TIMBRE: + multiSynth.setTimbre(channel, normalized); + break; + case MidiConstants.CONTROLLER_VOLUME: + multiSynth.setVolume(channel, normalized); + break; + case MidiConstants.CONTROLLER_PAN: + // convert to -1 to +1 range + multiSynth.setPan(channel, (normalized * 2.0) - 1.0); + break; + } + } + + @Override + public void registeredParameter(int channel, int index14, int value14) { + switch(index14) { + case MidiConstants.RPN_BEND_RANGE: + int semitones = value14 >> 7; + int cents = value14 & 0x7F; + double bendRange = semitones + (cents * 0.01); + multiSynth.setBendRange(channel, bendRange); + break; + default: + break; + } + } + + @Override + public void programChange(int channel, int program) { + multiSynth.programChange(channel, program); + } + + @Override + public void channelPressure(int channel, int value) { + double normalized = value * (1.0 / 127.0); + multiSynth.setPressure(channel, normalized); + } + + @Override + public void noteOff(int channel, int noteNumber, int velocity) { + multiSynth.noteOff(channel, noteNumber, velocity); + } + + @Override + public void noteOn(int channel, int noteNumber, int velocity) { + multiSynth.noteOn(channel, noteNumber, velocity); + } + + @Override + public void pitchBend(int channel, int bend) { + double offset = (bend - MidiConstants.PITCH_BEND_CENTER) + * (1.0 / (MidiConstants.PITCH_BEND_CENTER)); + multiSynth.setPitchBend(channel, offset); + } + + public void onReceive(byte[] bytes, int i, int length) { + parse(bytes); // TODO + } + +} diff --git a/src/main/java/com/jsyn/package.html b/src/main/java/com/jsyn/package.html new file mode 100644 index 0000000..cd73832 --- /dev/null +++ b/src/main/java/com/jsyn/package.html @@ -0,0 +1,17 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> +<html> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> +<title>JSyn Package</title> +</head> +<body> +JSyn is a music and audio synthesis API for Java. The basic sequence of operations is: +<ul> +<li>Use the JSyn class to create a synthesizer.</li> +<li>Create unit generators and add them to the synthesizer.</li> +<li>Connect unit generators so that audio signals can flow between them.</li> +<li>Start an output generator. It will pull data from the connected units.</li> +<li>Set port values and queue sample and envelope data to change the sound.</li> +</ul> +</body> +</html>
\ No newline at end of file diff --git a/src/main/java/com/jsyn/ports/ConnectableInput.java b/src/main/java/com/jsyn/ports/ConnectableInput.java new file mode 100644 index 0000000..3dae876 --- /dev/null +++ b/src/main/java/com/jsyn/ports/ConnectableInput.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.ports; + +/** + * This interface lets you pass either an input port, or a single part of an input port. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public interface ConnectableInput { + public void connect(ConnectableOutput other); + + public void disconnect(ConnectableOutput other); + + /** + * This is used internally by PortBlockPart to make a connection between specific parts of a + * port. + * + * @return + */ + public PortBlockPart getPortBlockPart(); + + public void pullData(long frameCount, int start, int limit); +} diff --git a/src/main/java/com/jsyn/ports/ConnectableOutput.java b/src/main/java/com/jsyn/ports/ConnectableOutput.java new file mode 100644 index 0000000..f42a799 --- /dev/null +++ b/src/main/java/com/jsyn/ports/ConnectableOutput.java @@ -0,0 +1,23 @@ +/* + * 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.ports; + +public interface ConnectableOutput { + public void connect(ConnectableInput other); + + public void disconnect(ConnectableInput other); +} diff --git a/src/main/java/com/jsyn/ports/GettablePort.java b/src/main/java/com/jsyn/ports/GettablePort.java new file mode 100644 index 0000000..aabf5ca --- /dev/null +++ b/src/main/java/com/jsyn/ports/GettablePort.java @@ -0,0 +1,27 @@ +/* + * 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.ports; + +public interface GettablePort { + String getName(); + + int getNumParts(); + + double getValue(int partNum); + + Object getUnitGenerator(); +} diff --git a/src/main/java/com/jsyn/ports/InputMixingBlockPart.java b/src/main/java/com/jsyn/ports/InputMixingBlockPart.java new file mode 100644 index 0000000..2d28888 --- /dev/null +++ b/src/main/java/com/jsyn/ports/InputMixingBlockPart.java @@ -0,0 +1,112 @@ +/* + * 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.ports; + +import java.io.PrintStream; + +import com.jsyn.Synthesizer; +import com.jsyn.unitgen.UnitGenerator; + +/** + * A UnitInputPort has an array of these, one for each part. + * + * @author Phil Burk 2009 Mobileer Inc + */ + +public class InputMixingBlockPart extends PortBlockPart { + private double[] mixer = new double[Synthesizer.FRAMES_PER_BLOCK]; + private double current; + private UnitInputPort unitInputPort; + + InputMixingBlockPart(UnitInputPort unitInputPort, double defaultValue) { + super(unitInputPort, defaultValue); + this.unitInputPort = unitInputPort; + } + + @Override + public double getValue() { + return current; + } + + @Override + protected void setValue(double value) { + current = value; + super.setValue(value); + } + + @Override + public double[] getValues() { + double[] result; + int numConnections = getConnectionCount(); + // LOGGER.debug("numConnection = " + numConnections + " for " + + // this ); + if (numConnections == 0) { + // No connection so just use our own data. + result = super.getValues(); + } else { + // Mix all of the connected ports. + double[] inputs; + int jCon = 0; + PortBlockPart otherPart; + // Choose value to initialize the mixer array. + if (unitInputPort.isValueAdded()) { + inputs = super.getValues(); // prime mixer with the set() values + jCon = 0; + } else { + otherPart = getConnection(jCon); + inputs = otherPart.getValues(); // prime mixer with first connected + jCon = 1; + } + for (int i = 0; i < mixer.length; i++) { + mixer[i] = inputs[i]; + } + // Now mix in the remaining inputs. + for (; jCon < numConnections; jCon++) { + otherPart = getConnection(jCon); + inputs = otherPart.getValues(); + for (int i = 0; i < mixer.length; i++) { + mixer[i] += inputs[i]; + } + } + result = mixer; + } + current = result[0]; + return result; + } + + private void printIndentation(PrintStream out, int level) { + for (int i = 0; i < level; i++) { + out.print(" "); + } + } + + private String portToString(UnitBlockPort port) { + UnitGenerator ugen = port.getUnitGenerator(); + return ugen.getClass().getSimpleName() + "." + port.getName(); + } + + public void printConnections(PrintStream out, int level) { + for (int i = 0; i < getConnectionCount(); i++) { + PortBlockPart part = getConnection(i); + + printIndentation(out, level); + out.println(portToString(getPort()) + " <--- " + portToString(part.getPort())); + + part.getPort().getUnitGenerator().printConnections(out, level + 1); + } + } +} diff --git a/src/main/java/com/jsyn/ports/PortBlockPart.java b/src/main/java/com/jsyn/ports/PortBlockPart.java new file mode 100644 index 0000000..b1ced32 --- /dev/null +++ b/src/main/java/com/jsyn/ports/PortBlockPart.java @@ -0,0 +1,210 @@ +/* + * 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.ports; + +import java.util.ArrayList; + +import com.jsyn.Synthesizer; +import com.jsyn.engine.SynthesisEngine; +import com.softsynth.shared.time.ScheduledCommand; +import com.softsynth.shared.time.TimeStamp; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Part of a multi-part port, for example, the left side of a stereo port. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class PortBlockPart implements ConnectableOutput, ConnectableInput { + + private static final Logger LOGGER = LoggerFactory.getLogger(PortBlockPart.class); + + private double[] values = new double[Synthesizer.FRAMES_PER_BLOCK]; + private ArrayList<PortBlockPart> connections = new ArrayList<PortBlockPart>(); + private UnitBlockPort unitBlockPort; + + protected PortBlockPart(UnitBlockPort unitBlockPort, double defaultValue) { + this.unitBlockPort = unitBlockPort; + setValue(defaultValue); + } + + public double[] getValues() { + return values; + } + + public double getValue() { + return values[0]; + } + + public double get() { + return values[0]; + } + + protected void setValue(double value) { + for (int i = 0; i < values.length; i++) { + values[i] = value; + } + } + + protected boolean isConnected() { + return (connections.size() > 0); + } + + private void addConnection(PortBlockPart otherPart) { + // LOGGER.debug("addConnection from " + this + " to " + otherPart + // ); + if (connections.contains(otherPart)) { + LOGGER.debug("addConnection already had connection from " + this + " to " + + otherPart); + } else { + connections.add(otherPart); + } + } + + private void removeConnection(PortBlockPart otherPart) { + // LOGGER.debug("removeConnection from " + this + " to " + + // otherPart ); + connections.remove(otherPart); + } + + private void connectNow(PortBlockPart otherPart) { + addConnection(otherPart); + otherPart.addConnection(this); + } + + private void disconnectNow(PortBlockPart otherPart) { + removeConnection(otherPart); + otherPart.removeConnection(this); + } + + private void disconnectAllNow() { + for (PortBlockPart part : connections) { + part.removeConnection(this); + } + connections.clear(); + } + + public PortBlockPart getConnection(int i) { + return connections.get(i); + } + + public int getConnectionCount() { + return connections.size(); + } + + /** Set all values to the last value. */ + protected void flatten() { + double lastValue = values[values.length - 1]; + for (int i = 0; i < values.length - 1; i++) { + values[i] = lastValue; + } + } + + protected UnitBlockPort getPort() { + return unitBlockPort; + } + + private void checkConnection(PortBlockPart destination) { + SynthesisEngine sourceSynth = unitBlockPort.getSynthesisEngine(); + SynthesisEngine destSynth = destination.unitBlockPort.getSynthesisEngine(); + if ((sourceSynth != destSynth) && (sourceSynth != null) && (destSynth != null)) { + throw new RuntimeException("Connection between units on different synths."); + } + } + + protected void connect(final PortBlockPart destination) { + checkConnection(destination); + unitBlockPort.queueCommand(new ScheduledCommand() { + @Override + public void run() { + connectNow(destination); + } + }); + } + + protected void connect(final PortBlockPart destination, TimeStamp timeStamp) { + unitBlockPort.scheduleCommand(timeStamp, new ScheduledCommand() { + @Override + public void run() { + connectNow(destination); + } + }); + } + + protected void disconnect(final PortBlockPart destination) { + unitBlockPort.queueCommand(new ScheduledCommand() { + @Override + public void run() { + disconnectNow(destination); + } + }); + } + + protected void disconnect(final PortBlockPart destination, TimeStamp timeStamp) { + unitBlockPort.scheduleCommand(timeStamp, new ScheduledCommand() { + @Override + public void run() { + disconnectNow(destination); + } + }); + } + + protected void disconnectAll() { + unitBlockPort.queueCommand(new ScheduledCommand() { + @Override + public void run() { + disconnectAllNow(); + } + }); + } + + @Override + public void connect(ConnectableInput other) { + connect(other.getPortBlockPart()); + } + + @Override + public void connect(ConnectableOutput other) { + other.connect(this); + } + + @Override + public void disconnect(ConnectableOutput other) { + other.disconnect(this); + } + + @Override + public void disconnect(ConnectableInput other) { + disconnect(other.getPortBlockPart()); + } + + /** To implement ConnectableInput */ + @Override + public PortBlockPart getPortBlockPart() { + return this; + } + + @Override + public void pullData(long frameCount, int start, int limit) { + for (int i = 0; i < getConnectionCount(); i++) { + PortBlockPart part = getConnection(i); + part.getPort().getUnitGenerator().pullData(frameCount, start, limit); + } + } + +} diff --git a/src/main/java/com/jsyn/ports/QueueDataCommand.java b/src/main/java/com/jsyn/ports/QueueDataCommand.java new file mode 100644 index 0000000..0ef36e2 --- /dev/null +++ b/src/main/java/com/jsyn/ports/QueueDataCommand.java @@ -0,0 +1,170 @@ +/* + * 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.ports; + +import com.jsyn.data.SequentialData; +import com.softsynth.shared.time.ScheduledCommand; + +/** + * A command that can be used to queue SequentialData to a UnitDataQueuePort. Here is an example of + * queuing data with a callback using this command. + * + * <pre> + * <code> + * // Queue an envelope with a completion callback. + * QueueDataCommand command = envelopePlayer.dataQueue.createQueueDataCommand( envelope, 0, + * envelope.getNumFrames() ); + * // Create an object to be called when the queued data is done. + * TestQueueCallback callback = new TestQueueCallback(); + * command.setCallback( callback ); + * command.setNumLoops( 2 ); + * envelopePlayer.rate.set( 0.2 ); + * synth.queueCommand( command ); + * </code> + * </pre> + * + * The callback will be passed QueueDataEvents. + * + * <pre> + * <code> + * class TestQueueCallback implements UnitDataQueueCallback + * { + * public void started( QueueDataEvent event ) + * { + * LOGGER.debug("CALLBACK: Envelope started."); + * } + * + * public void looped( QueueDataEvent event ) + * { + * LOGGER.debug("CALLBACK: Envelope looped."); + * } + * + * public void finished( QueueDataEvent event ) + * { + * LOGGER.debug("CALLBACK: Envelope finished."); + * } + * } + * </code> + * </pre> + * + * @author Phil Burk 2009 Mobileer Inc + */ +public abstract class QueueDataCommand extends QueueDataEvent implements ScheduledCommand { + + protected SequentialDataCrossfade crossfadeData; + protected SequentialData currentData; + + private static final long serialVersionUID = -1185274459972359536L; + private UnitDataQueueCallback callback; + + public QueueDataCommand(UnitDataQueuePort port, SequentialData sequentialData, int startFrame, + int numFrames) { + super(port); + + if ((startFrame + numFrames) > sequentialData.getNumFrames()) { + throw new IllegalArgumentException("tried to queue past end of data, " + (startFrame + numFrames)); + } else if (startFrame < 0) { + throw new IllegalArgumentException("tried to queue before start of data, " + startFrame); + } + this.sequentialData = sequentialData; + this.currentData = sequentialData; + crossfadeData = new SequentialDataCrossfade(); + this.startFrame = startFrame; + this.numFrames = numFrames; + } + + @Override + public abstract void run(); + + /** + * If true then this item will be skipped if other items are queued after it. This flag allows + * you to queue lots of small pieces of sound without making the queue very long. + * + * @param skipIfOthers + */ + public void setSkipIfOthers(boolean skipIfOthers) { + this.skipIfOthers = skipIfOthers; + } + + /** + * If true then the queue will be cleared and this item will be started immediately. It is + * better to use this flag than to clear the queue from the application because there could be a + * gap before the next item is available. This is most useful when combined with + * setCrossFadeIn(). + * + * @param immediate + */ + public void setImmediate(boolean immediate) { + this.immediate = immediate; + } + + public UnitDataQueueCallback getCallback() { + return callback; + } + + public void setCallback(UnitDataQueueCallback callback) { + this.callback = callback; + } + + public SequentialDataCrossfade getCrossfadeData() { + return crossfadeData; + } + + public void setCrossfadeData(SequentialDataCrossfade crossfadeData) { + this.crossfadeData = crossfadeData; + } + + public SequentialData getCurrentData() { + return currentData; + } + + public void setCurrentData(SequentialData currentData) { + this.currentData = currentData; + } + + /** + * Stop the unit that contains this port after this command has finished. + * + * @param autoStop + */ + public void setAutoStop(boolean autoStop) { + this.autoStop = autoStop; + } + + /** + * Set how many time the block should be repeated after the first time. For example, if you set + * numLoops to zero the block will only be played once. If you set numLoops to one the block + * will be played twice. + * + * @param numLoops number of times to loop back + */ + public void setNumLoops(int numLoops) { + this.numLoops = numLoops; + } + + /** + * Number of frames to cross fade from the previous block to this block. This can be used to + * avoid pops when making abrupt transitions. There must be frames available after the end of + * the previous block to use for crossfading. The crossfade is linear. + * + * @param size + */ + public void setCrossFadeIn(int size) { + this.crossFadeIn = size; + } + +} diff --git a/src/main/java/com/jsyn/ports/QueueDataEvent.java b/src/main/java/com/jsyn/ports/QueueDataEvent.java new file mode 100644 index 0000000..2b93fab --- /dev/null +++ b/src/main/java/com/jsyn/ports/QueueDataEvent.java @@ -0,0 +1,80 @@ +/* + * 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.ports; + +import java.util.EventObject; + +import com.jsyn.data.SequentialData; + +/** + * An event that is passed to a UnitDataQueueCallback when the element in the queue is played.. + * + * @author Phil Burk 2009 Mobileer Inc + */ +public class QueueDataEvent extends EventObject { + private static final long serialVersionUID = 176846633064538053L; + protected SequentialData sequentialData; + protected int startFrame; + protected int numFrames; + protected int numLoops; + protected int loopsLeft; + protected int crossFadeIn; + protected boolean skipIfOthers; + protected boolean autoStop; + protected boolean immediate; + + public QueueDataEvent(Object arg0) { + super(arg0); + } + + public boolean isSkipIfOthers() { + return skipIfOthers; + } + + public boolean isImmediate() { + return immediate; + } + + public SequentialData getSequentialData() { + return sequentialData; + } + + public int getCrossFadeIn() { + return crossFadeIn; + } + + public int getStartFrame() { + return startFrame; + } + + public int getNumFrames() { + return numFrames; + } + + public int getNumLoops() { + return numLoops; + } + + public int getLoopsLeft() { + return loopsLeft; + } + + public boolean isAutoStop() { + return autoStop; + } + +} diff --git a/src/main/java/com/jsyn/ports/SequentialDataCrossfade.java b/src/main/java/com/jsyn/ports/SequentialDataCrossfade.java new file mode 100644 index 0000000..0c3d3b2 --- /dev/null +++ b/src/main/java/com/jsyn/ports/SequentialDataCrossfade.java @@ -0,0 +1,139 @@ +/* + * 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.ports; + +import com.jsyn.data.SequentialData; +import com.jsyn.data.SequentialDataCommon; + +/** + * A SequentialData object that will crossfade between two other SequentialData objects. The + * crossfade is linear. This could, for example, be used to create a smooth transition between two + * samples, or between two arbitrary regions in one sample. As an example, consider a sample that + * has a length of 200000 frames. You could specify a sample loop that started arbitrarily at frame + * 50000 and with a size of 30000 frames. Unless you got lucky with the zero crossings, it is likely + * that you will hear a pop when this sample loops. To prevent the pop you could crossfade the + * beginning of the loop with the region immediately after the end of the loop. To crossfade with + * 5000 samples after the loop: + * + * <pre> + * SequentialDataCrossfade xfade = new SequentialDataCrossfade(sample, (50000 + 30000), 5000, sample, + * 50000, 30000); + * </pre> + * + * After the crossfade you will hear the rest of the target at full volume. There are two regions + * that determine what is returned from readDouble() + * <ol> + * <li>Crossfade region with size crossFadeFrames. It fades smoothly from source to target.</li> + * <li>Steady region that is simply the target values with size (numFrames-crossFadeFrames).</li> + * </ol> + * + * <pre> + * "Crossfade Region" "Steady Region" + * |-- source fading out --| + * |-- target fading in --|-- remainder of target at original volume --| + * </pre> + * + * @author Phil Burk + */ +class SequentialDataCrossfade extends SequentialDataCommon { + private SequentialData source; + private int sourceStartIndex; + + private SequentialData target; + private int targetStartIndex; + + private int crossFadeFrames; + private double frameScaler; + + /** + * @param source SequentialData that will be at full volume at the beginning of the crossfade + * region. + * @param sourceStartFrame Frame in source to begin the crossfade. + * @param crossFadeFrames Number of frames in the crossfaded region. + * @param target SequentialData that will be at full volume at the end of the crossfade region. + * @param targetStartFrame Frame in target to begin the crossfade. + * @param numFrames total number of frames in this data object. + */ + public void setup(SequentialData source, int sourceStartFrame, int crossFadeFrames, + SequentialData target, int targetStartFrame, int numFrames) { + + assert ((sourceStartFrame + crossFadeFrames) <= source.getNumFrames()); + assert ((targetStartFrame + numFrames) <= target.getNumFrames()); + + // LOGGER.debug( "WARNING! sourceStartFrame = " + sourceStartFrame + // + ", crossFadeFrames = " + crossFadeFrames + ", maxFrame = " + // + source.getNumFrames() + ", source = " + source ); + // LOGGER.debug( " targetStartFrame = " + targetStartFrame + // + ", numFrames = " + numFrames + ", maxFrame = " + // + target.getNumFrames() + ", target = " + target ); + + // There is a danger that we might nest SequentialDataCrossfades deeply + // as source. If past crossfade region then pull out the target. + if (source instanceof SequentialDataCrossfade) { + SequentialDataCrossfade crossfade = (SequentialDataCrossfade) source; + // If we are starting past the crossfade region then just use the + // target. + if (sourceStartFrame >= crossfade.crossFadeFrames) { + source = crossfade.target; + sourceStartFrame += crossfade.targetStartIndex / source.getChannelsPerFrame(); + } + } + + if (target instanceof SequentialDataCrossfade) { + SequentialDataCrossfade crossfade = (SequentialDataCrossfade) target; + target = crossfade.target; + targetStartFrame += crossfade.targetStartIndex / target.getChannelsPerFrame(); + } + + this.source = source; + this.target = target; + this.sourceStartIndex = sourceStartFrame * source.getChannelsPerFrame(); + this.crossFadeFrames = crossFadeFrames; + this.targetStartIndex = targetStartFrame * target.getChannelsPerFrame(); + + frameScaler = (crossFadeFrames == 0) ? 1.0 : (1.0 / crossFadeFrames); + this.numFrames = numFrames; + } + + @Override + public void writeDouble(int index, double value) { + } + + @Override + public double readDouble(int index) { + int frame = index / source.getChannelsPerFrame(); + if (frame < crossFadeFrames) { + double factor = frame * frameScaler; + double value = (1.0 - factor) * source.readDouble(index + sourceStartIndex); + value += (factor * target.readDouble(index + targetStartIndex)); + return value; + } else { + return target.readDouble(index + targetStartIndex); + } + } + + @Override + public double getRateScaler(int index, double synthesisRate) { + return target.getRateScaler(index, synthesisRate); + } + + @Override + public int getChannelsPerFrame() { + return target.getChannelsPerFrame(); + } + +} diff --git a/src/main/java/com/jsyn/ports/SettablePort.java b/src/main/java/com/jsyn/ports/SettablePort.java new file mode 100644 index 0000000..e0db05c --- /dev/null +++ b/src/main/java/com/jsyn/ports/SettablePort.java @@ -0,0 +1,28 @@ +/* + * 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.ports; + +import com.softsynth.shared.time.TimeStamp; + +/** + * Port whose parts can be set. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public interface SettablePort extends GettablePort { + void set(int partNum, double value, TimeStamp timeStamp); +} diff --git a/src/main/java/com/jsyn/ports/UnitBlockPort.java b/src/main/java/com/jsyn/ports/UnitBlockPort.java new file mode 100644 index 0000000..d7fc82f --- /dev/null +++ b/src/main/java/com/jsyn/ports/UnitBlockPort.java @@ -0,0 +1,110 @@ +/* + * 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.ports; + +/** + * A port that contains multiple parts with blocks of data. + * + * @author Phil Burk 2009 Mobileer Inc + */ +public class UnitBlockPort extends UnitPort { + PortBlockPart[] parts; + + public UnitBlockPort(int numParts, String name, double defaultValue) { + super(name); + makeParts(numParts, defaultValue); + } + + public UnitBlockPort(String name) { + this(1, name, 0.0); + } + + protected void makeParts(int numParts, double defaultValue) { + parts = new PortBlockPart[numParts]; + for (int i = 0; i < numParts; i++) { + parts[i] = new PortBlockPart(this, defaultValue); + } + } + + @Override + public int getNumParts() { + return parts.length; + } + + /** + * Convenience call to get(0). + * + * @return value of 0th part as set + */ + public double get() { + return get(0); + } + + public double getValue() { + return getValue(0); + } + + /** + * This is used inside UnitGenerators to get the current values for a port. It works regardless + * of whether the port is connected or not. + * + * @return + */ + public double[] getValues() { + return parts[0].getValues(); + } + + /** Only for use in the audio thread when implementing UnitGenerators. */ + public double[] getValues(int partNum) { + return parts[partNum].getValues(); + } + + /** Get the immediate current value of the port. */ + public double getValue(int partNum) { + return parts[partNum].getValue(); + } + + public double get(int partNum) { + return parts[partNum].get(); + } + + /** Only for use in the audio thread when implementing UnitGenerators. */ + protected void setValueInternal(int partNum, double value) { + parts[partNum].setValue(value); + } + + /** Only for use in the audio thread when implementing UnitGenerators. */ + public void setValueInternal(double value) { + setValueInternal(0, value); + } + + public boolean isConnected() { + return isConnected(0); + } + + public boolean isConnected(int partNum) { + return parts[partNum].isConnected(); + } + + public void disconnectAll(int partNum) { + parts[partNum].disconnectAll(); + } + + public void disconnectAll() { + disconnectAll(0); + } +} diff --git a/src/main/java/com/jsyn/ports/UnitDataQueueCallback.java b/src/main/java/com/jsyn/ports/UnitDataQueueCallback.java new file mode 100644 index 0000000..dca4adc --- /dev/null +++ b/src/main/java/com/jsyn/ports/UnitDataQueueCallback.java @@ -0,0 +1,31 @@ +/* + * 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.ports; + +/** + * This is called when a block of data that is queued to a UnitDataQueuePort starts, loops, or + * finishes. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public interface UnitDataQueueCallback { + public void started(QueueDataEvent event); + + public void looped(QueueDataEvent event); + + public void finished(QueueDataEvent event); +} diff --git a/src/main/java/com/jsyn/ports/UnitDataQueuePort.java b/src/main/java/com/jsyn/ports/UnitDataQueuePort.java new file mode 100644 index 0000000..13b2e2a --- /dev/null +++ b/src/main/java/com/jsyn/ports/UnitDataQueuePort.java @@ -0,0 +1,466 @@ +/* + * 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.ports; + +import java.util.LinkedList; + +import com.jsyn.data.SequentialData; +import com.jsyn.exceptions.ChannelMismatchException; +import com.softsynth.shared.time.ScheduledCommand; +import com.softsynth.shared.time.TimeStamp; + +/** + * Queue for SequentialData, samples or envelopes + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class UnitDataQueuePort extends UnitPort { + private final LinkedList<QueuedBlock> blocks = new LinkedList<QueuedBlock>(); + private QueueDataCommand currentBlock; + private int frameIndex; + private int numChannels = 1; + private double normalizedRate; + private long framesMoved; + private boolean autoStopPending; + private boolean targetValid; + private QueueDataCommand finishingBlock; + private QueueDataCommand loopingBlock; + public static final int LOOP_IF_LAST = -1; + + public UnitDataQueuePort(String name) { + super(name); + } + + /** Hold a reference to part of a sample. */ + @SuppressWarnings("serial") + private class QueuedBlock extends QueueDataCommand { + + public QueuedBlock(SequentialData queueableData, int startFrame, int numFrames) { + super(UnitDataQueuePort.this, queueableData, startFrame, numFrames); + } + + @Override + public void run() { + synchronized (blocks) { + // Remove last block if it can be skipped. + if (blocks.size() > 0) { + QueueDataEvent lastBlock = blocks.getLast(); + if (lastBlock.isSkipIfOthers()) { + blocks.removeLast(); + } + } + + // If we are crossfading then figure out where to crossfade + // from. + if (getCrossFadeIn() > 0) { + if (isImmediate()) { + // Queue will be cleared so fade in from current. + if (currentBlock != null) { + setupCrossFade(currentBlock, frameIndex, this); + } + // else nothing is playing so don't crossfade. + } else { + QueueDataCommand endBlock = getEndBlock(); + if (endBlock != null) { + setupCrossFade(endBlock, + endBlock.getStartFrame() + endBlock.getNumFrames(), this); + } + } + } + + if (isImmediate()) { + clearQueue(); + } + + blocks.add(this); + } + } + } + + // FIXME - determine crossfade on any transition between blocks or when looping back. + + protected void setupCrossFade(QueueDataCommand sourceCommand, int sourceStartIndex, + QueueDataCommand targetCommand) { + int crossFrames = targetCommand.getCrossFadeIn(); + SequentialData sourceData = sourceCommand.getCurrentData(); + SequentialData targetData = targetCommand.getCurrentData(); + int remainingSource = sourceData.getNumFrames() - sourceStartIndex; + // clip to end of source + if (crossFrames > remainingSource) + crossFrames = remainingSource; + if (crossFrames > 0) { + // The SequentialDataCrossfade should continue to the end of the target + // so that we can crossfade from it to the target. + int remainingTarget = targetData.getNumFrames() - targetCommand.getStartFrame(); + targetCommand.crossfadeData.setup(sourceData, sourceStartIndex, crossFrames, + targetData, targetCommand.getStartFrame(), remainingTarget); + targetCommand.currentData = targetCommand.crossfadeData; + targetCommand.startFrame = 0; + } + } + + public QueueDataCommand createQueueDataCommand(SequentialData queueableData) { + return createQueueDataCommand(queueableData, 0, queueableData.getNumFrames()); + } + + public QueueDataCommand createQueueDataCommand(SequentialData queueableData, int startFrame, + int numFrames) { + if (queueableData.getChannelsPerFrame() != UnitDataQueuePort.this.numChannels) { + throw new ChannelMismatchException("Tried to queue " + + queueableData.getChannelsPerFrame() + " channel data to a " + numChannels + + " channel port."); + } + return new QueuedBlock(queueableData, startFrame, numFrames); + } + + public QueueDataCommand getEndBlock() { + if (blocks.size() > 0) { + return blocks.getLast(); + } else if (currentBlock != null) { + return currentBlock; + } else { + return null; + } + } + + public void setCurrentBlock(QueueDataCommand currentBlock) { + this.currentBlock = currentBlock; + } + + public void firePendingCallbacks() { + if (loopingBlock != null) { + if (loopingBlock.getCallback() != null) { + loopingBlock.getCallback().looped(currentBlock); + } + loopingBlock = null; + } + if (finishingBlock != null) { + if (finishingBlock.getCallback() != null) { + finishingBlock.getCallback().finished(currentBlock); // FIXME - Should this pass + // finishingBlock?! + } + finishingBlock = null; + } + } + + public boolean hasMore() { + return (currentBlock != null) || (blocks.size() > 0); + } + + private void checkBlock() { + if (currentBlock == null) { + synchronized (blocks) { + setCurrentBlock(blocks.remove()); + frameIndex = currentBlock.getStartFrame(); + currentBlock.loopsLeft = currentBlock.getNumLoops(); + if (currentBlock.getCallback() != null) { + currentBlock.getCallback().started(currentBlock); + } + } + } + } + + private void advanceFrameIndex() { + frameIndex += 1; + framesMoved += 1; + // Are we done with this block? + if (frameIndex >= (currentBlock.getStartFrame() + currentBlock.getNumFrames())) { + // Should we loop on this block based on a counter? + if (currentBlock.loopsLeft > 0) { + currentBlock.loopsLeft -= 1; + loopToStart(); + } + // Should we loop forever on this block? + else if ((blocks.size() == 0) && (currentBlock.loopsLeft < 0)) { + loopToStart(); + } + // We are done. + else { + if (currentBlock.isAutoStop()) { + autoStopPending = true; + } + finishingBlock = currentBlock; + setCurrentBlock(null); + // LOGGER.debug("advanceFrameIndex: currentBlock set null"); + } + } + } + + private void loopToStart() { + if (currentBlock.getCrossFadeIn() > 0) { + setupCrossFade(currentBlock, frameIndex, currentBlock); + } + frameIndex = currentBlock.getStartFrame(); + loopingBlock = currentBlock; + } + + public double getNormalizedRate() { + return normalizedRate; + } + + public double readCurrentChannelDouble(int channelIndex) { + return currentBlock.currentData.readDouble((frameIndex * numChannels) + channelIndex); + } + + public void writeCurrentChannelDouble(int channelIndex, double value) { + currentBlock.currentData.writeDouble((frameIndex * numChannels) + channelIndex, value); + } + + public void beginFrame(double synthesisPeriod) { + checkBlock(); + normalizedRate = currentBlock.currentData.getRateScaler(frameIndex, synthesisPeriod); + } + + public void endFrame() { + advanceFrameIndex(); + targetValid = true; + } + + public double readNextMonoDouble(double synthesisPeriod) { + beginFrame(synthesisPeriod); + double value = currentBlock.currentData.readDouble(frameIndex); + endFrame(); + return value; + } + + /** Write directly to the port queue. This is only called by unit tests! */ + protected void addQueuedBlock(QueueDataEvent block) { + blocks.add((QueuedBlock) block); + } + + /** Clear the queue. Internal use only. */ + protected void clearQueue() { + synchronized (blocks) { + blocks.clear(); + setCurrentBlock(null); + targetValid = false; + autoStopPending = false; + } + } + + class ClearQueueCommand implements ScheduledCommand { + @Override + public void run() { + clearQueue(); + } + } + + /** Queue the data to the port at a future time. */ + public void queue(SequentialData queueableData, int startFrame, int numFrames, + TimeStamp timeStamp) { + QueueDataCommand command = createQueueDataCommand(queueableData, startFrame, numFrames); + scheduleCommand(timeStamp, command); + } + + /** + * Queue the data to the port at a future time. Command will clear the queue before executing. + */ + public void queueImmediate(SequentialData queueableData, int startFrame, int numFrames, + TimeStamp timeStamp) { + QueueDataCommand command = createQueueDataCommand(queueableData, startFrame, numFrames); + command.setImmediate(true); + scheduleCommand(timeStamp, command); + } + + /** Queue the data to the port at a future time. */ + public void queueLoop(SequentialData queueableData, int startFrame, int numFrames, + TimeStamp timeStamp) { + queueLoop(queueableData, startFrame, numFrames, LOOP_IF_LAST, timeStamp); + } + + /** + * Queue the data to the port at a future time with a specified number of loops. + */ + public void queueLoop(SequentialData queueableData, int startFrame, int numFrames, + int numLoops, TimeStamp timeStamp) { + QueueDataCommand command = createQueueDataCommand(queueableData, startFrame, numFrames); + command.setNumLoops(numLoops); + scheduleCommand(timeStamp, command); + } + + /** Queue the entire data object for looping. */ + public void queueLoop(SequentialData queueableData) { + queueLoop(queueableData, 0, queueableData.getNumFrames()); + } + + /** Queue the data to the port for immediate use. */ + public void queueLoop(SequentialData queueableData, int startFrame, int numFrames) { + queueLoop(queueableData, startFrame, numFrames, LOOP_IF_LAST); + } + + /** + * Queue the data to the port for immediate use with a specified number of loops. + */ + public void queueLoop(SequentialData queueableData, int startFrame, int numFrames, int numLoops) { + QueueDataCommand command = createQueueDataCommand(queueableData, startFrame, numFrames); + command.setNumLoops(numLoops); + queueCommand(command); + } + + /** + * Queue the data to the port at a future time. Request that the unit stop when this block is + * finished. + */ + public void queueStop(SequentialData queueableData, int startFrame, int numFrames, + TimeStamp timeStamp) { + QueueDataCommand command = createQueueDataCommand(queueableData, startFrame, numFrames); + command.setAutoStop(true); + scheduleCommand(timeStamp, command); + } + + /** Queue the data to the port through the command queue ASAP. */ + public void queue(SequentialData queueableData, int startFrame, int numFrames) { + QueueDataCommand command = createQueueDataCommand(queueableData, startFrame, numFrames); + queueCommand(command); + } + + /** + * Queue entire amount of data with no options. + * + * @param queueableData + */ + public void queue(SequentialData queueableData) { + queue(queueableData, 0, queueableData.getNumFrames()); + } + + /** Schedule queueOn now! */ + public void queueOn(SequentialData queueableData) { + queueOn(queueableData, getSynthesisEngine().createTimeStamp()); + } + + /** Schedule queueOff now! */ + public void queueOff(SequentialData queueableData) { + queueOff(queueableData, false); + } + + /** Schedule queueOff now! */ + public void queueOff(SequentialData queueableData, boolean ifStop) { + queueOff(queueableData, ifStop, getSynthesisEngine().createTimeStamp()); + } + + /** + * Convenience method that will queue the attack portion of a channelData and the sustain loop + * if it exists. This could be used to implement a NoteOn method. + */ + public void queueOn(SequentialData queueableData, TimeStamp timeStamp) { + + if (queueableData.getSustainBegin() < 0) { + // no sustain loop, handle release + if (queueableData.getReleaseBegin() < 0) { + // No loops + queueImmediate(queueableData, 0, queueableData.getNumFrames(), timeStamp); + } else { + queueImmediate(queueableData, 0, queueableData.getReleaseEnd(), timeStamp); + int size = queueableData.getReleaseEnd() - queueableData.getReleaseBegin(); + queueLoop(queueableData, queueableData.getReleaseBegin(), size, timeStamp); + } + } else { + // yes sustain loop + if (queueableData.getSustainEnd() > 0) { + int frontSize = queueableData.getSustainBegin(); + int loopSize = queueableData.getSustainEnd() - queueableData.getSustainBegin(); + // Is there an initial portion before the sustain loop? + if (frontSize > 0) { + queueImmediate(queueableData, 0, frontSize, timeStamp); + } + loopSize = queueableData.getSustainEnd() - queueableData.getSustainBegin(); + if (loopSize > 0) { + queueLoop(queueableData, queueableData.getSustainBegin(), loopSize, timeStamp); + } + } + + } + } + + /** + * Convenience method that will queue the decay portion of a SequentialData object, or the gap + * and release loop portions if they exist. This could be used to implement a NoteOff method. + * + * @param ifStop Will setAutostop(true) if release portion queued without a release loop. This will + * stop execution of the unit. + */ + public void queueOff(SequentialData queueableData, boolean ifStop, TimeStamp timeStamp) { + if (queueableData.getSustainBegin() >= 0) /* Sustain loop? */ + { + int relSize = queueableData.getReleaseEnd() - queueableData.getReleaseBegin(); + if (queueableData.getReleaseBegin() < 0) { /* Sustain loop, no release loop. */ + int susEnd = queueableData.getSustainEnd(); + int size = queueableData.getNumFrames() - susEnd; + // LOGGER.debug("queueOff: size = " + size ); + if (size <= 0) { + // always queue something so that we can stop the loop + // 20001117 + size = 1; + susEnd = queueableData.getNumFrames() - 1; + } + if (ifStop) { + queueStop(queueableData, susEnd, size, timeStamp); + } else { + queue(queueableData, susEnd, size, timeStamp); + } + } else if (queueableData.getReleaseBegin() > queueableData.getSustainEnd()) { + // Queue gap between sustain and release loop. + queue(queueableData, queueableData.getSustainEnd(), queueableData.getReleaseEnd() + - queueableData.getSustainEnd(), timeStamp); + if (relSize > 0) + queueLoop(queueableData, queueableData.getReleaseBegin(), relSize, timeStamp); + } else if (relSize > 0) { + // No gap between sustain and release. + queueLoop(queueableData, queueableData.getReleaseBegin(), relSize, timeStamp); + } + } + /* If no sustain loop, then nothing to do. */ + } + + public void clear(TimeStamp timeStamp) { + ScheduledCommand command = new ClearQueueCommand(); + scheduleCommand(timeStamp, command); + } + + public void clear() { + ScheduledCommand command = new ClearQueueCommand(); + queueCommand(command); + } + + public void writeNextDouble(double value) { + checkBlock(); + currentBlock.currentData.writeDouble(frameIndex, value); + advanceFrameIndex(); + } + + public long getFrameCount() { + return framesMoved; + } + + public boolean testAndClearAutoStop() { + boolean temp = autoStopPending; + autoStopPending = false; + return temp; + } + + public boolean isTargetValid() { + return targetValid; + } + + public void setNumChannels(int numChannels) { + this.numChannels = numChannels; + } + + public int getNumChannels() { + return numChannels; + } +} diff --git a/src/main/java/com/jsyn/ports/UnitFunctionPort.java b/src/main/java/com/jsyn/ports/UnitFunctionPort.java new file mode 100644 index 0000000..e45241a --- /dev/null +++ b/src/main/java/com/jsyn/ports/UnitFunctionPort.java @@ -0,0 +1,48 @@ +/* + * 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.ports; + +import com.jsyn.data.Function; + +/** + * Port for holding a Function object. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class UnitFunctionPort extends UnitPort { + private static NullFunction nullFunction = new NullFunction(); + private Function function = nullFunction; + + private static class NullFunction implements Function { + @Override + public double evaluate(double input) { + return 0.0; + } + } + + public UnitFunctionPort(String name) { + super(name); + } + + public void set(Function function) { + this.function = function; + } + + public Function get() { + return function; + } +} diff --git a/src/main/java/com/jsyn/ports/UnitGatePort.java b/src/main/java/com/jsyn/ports/UnitGatePort.java new file mode 100644 index 0000000..700aef8 --- /dev/null +++ b/src/main/java/com/jsyn/ports/UnitGatePort.java @@ -0,0 +1,158 @@ +/* + * 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.ports; + +import com.jsyn.unitgen.UnitGenerator; +import com.softsynth.shared.time.ScheduledCommand; +import com.softsynth.shared.time.TimeStamp; + +public class UnitGatePort extends UnitInputPort { + private boolean autoDisableEnabled = false; + private boolean triggered = false; + private boolean off = true; + private UnitGenerator gatedUnit; + public static final double THRESHOLD = 0.01; + + public UnitGatePort(String name) { + super(name); + } + + public void on() { + setOn(true); + } + + public void off() { + setOn(false); + } + + public void off(TimeStamp timeStamp) { + setOn(false, timeStamp); + } + + public void on(TimeStamp timeStamp) { + setOn(true, timeStamp); + } + + private void setOn(final boolean on) { + queueCommand(new ScheduledCommand() { + @Override + public void run() { + setOnInternal(on); + } + }); + } + + private void setOn(final boolean on, TimeStamp timeStamp) { + scheduleCommand(timeStamp, new ScheduledCommand() { + @Override + public void run() { + setOnInternal(on); + } + }); + } + + private void setOnInternal(boolean on) { + if (on) { + triggerInternal(); + } + setValueInternal(on ? 1.0 : 0.0); + } + + private void triggerInternal() { + getGatedUnit().setEnabled(true); + triggered = true; + } + + public void trigger() { + queueCommand(new ScheduledCommand() { + @Override + public void run() { + triggerInternal(); + } + }); + } + + public void trigger(TimeStamp timeStamp) { + scheduleCommand(timeStamp, new ScheduledCommand() { + @Override + public void run() { + triggerInternal(); + } + }); + } + + /** + * This is called by UnitGenerators. It sets the off value that can be tested using isOff(). + * + * @param i + * @return true if triggered by a positive edge. + */ + public boolean checkGate(int i) { + double[] inputs = getValues(); + boolean result = triggered; + triggered = false; + if (off) { + if (inputs[i] >= THRESHOLD) { + result = true; + off = false; + } + } else { + if (inputs[i] < THRESHOLD) { + off = true; + } + } + return result; + } + + public boolean isOff() { + return off; + } + + public boolean isAutoDisableEnabled() { + return autoDisableEnabled; + } + + /** + * Request the containing UnitGenerator be disabled when checkAutoDisabled() is called. This can + * be used to reduce CPU load. + * + * @param autoDisableEnabled + */ + public void setAutoDisableEnabled(boolean autoDisableEnabled) { + this.autoDisableEnabled = autoDisableEnabled; + } + + /** + * Called by UnitGenerator when an envelope reaches the end of its contour. + */ + public void checkAutoDisable() { + if (autoDisableEnabled) { + getGatedUnit().setEnabled(false); + } + } + + private UnitGenerator getGatedUnit() { + return (gatedUnit == null) ? getUnitGenerator() : gatedUnit; + } + + public void setupAutoDisable(UnitGenerator unit) { + gatedUnit = unit; + setAutoDisableEnabled(true); + // Start off disabled so we don't immediately swamp the CPU. + gatedUnit.setEnabled(false); + } +} diff --git a/src/main/java/com/jsyn/ports/UnitInputPort.java b/src/main/java/com/jsyn/ports/UnitInputPort.java new file mode 100644 index 0000000..3eda1f6 --- /dev/null +++ b/src/main/java/com/jsyn/ports/UnitInputPort.java @@ -0,0 +1,254 @@ +/* + * 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.ports; + +import java.io.PrintStream; + +import com.softsynth.shared.time.ScheduledCommand; +import com.softsynth.shared.time.TimeStamp; + +/** + * A port that is used to pass values into a UnitGenerator. + * + * @author Phil Burk 2009 Mobileer Inc + */ +public class UnitInputPort extends UnitBlockPort implements ConnectableInput, SettablePort { + private double minimum = 0.0; + private double maximum = 1.0; + private double defaultValue = 0.0; + private double[] setValues; + private boolean valueAdded = false; + + /** + * @param numParts typically 1, use 2 for stereo ports + * @param name name that may be used in GUIs + * @param defaultValue + */ + public UnitInputPort(int numParts, String name, double defaultValue) { + super(numParts, name, defaultValue); + setDefault(defaultValue); + setValues = new double[numParts]; + for (int i = 0; i < numParts; i++) { + setValues[i] = defaultValue; + } + } + + public UnitInputPort(String name, double defaultValue) { + this(1, name, defaultValue); + } + + public UnitInputPort(String name) { + this(1, name, 0.0); + } + + public UnitInputPort(int numParts, String name) { + this(numParts, name, 0.0); + } + + @Override + protected void makeParts(int numParts, double defaultValue) { + parts = new InputMixingBlockPart[numParts]; + for (int i = 0; i < numParts; i++) { + parts[i] = new InputMixingBlockPart(this, defaultValue); + } + } + + /** + * This is used internally by the SynthesisEngine to execute units based on their connections. + * + * @param frameCount + * @param start + * @param limit + */ + @Override + public void pullData(long frameCount, int start, int limit) { + for (PortBlockPart part : parts) { + ((InputMixingBlockPart) part).pullData(frameCount, start, limit); + } + } + + @Override + protected void setValueInternal(int partNum, double value) { + super.setValueInternal(partNum, value); + setValues[partNum] = value; + } + + public void set(double value) { + set(0, value); + } + + public void set(final int partNum, final double value) { + // Trigger exception now if out of range. + setValues[partNum] = value; + queueCommand(new ScheduledCommand() { + @Override + public void run() { + setValueInternal(partNum, value); + } + }); + } + + public void set(double value, TimeStamp time) { + set(0, value, time); + } + + public void set(double value, double time) { + set(0, value, time); + } + + public void set(int partNum, double value, double time) { + set(partNum, value, new TimeStamp(time)); + } + + @Override + public void set(final int partNum, final double value, TimeStamp timeStamp) { + // Trigger exception now if out of range. + getValue(partNum); + scheduleCommand(timeStamp, new ScheduledCommand() { + @Override + public void run() { + setValueInternal(partNum, value); + } + }); + } + + /** + * Value of a port based on the set() calls. Not affected by connected ports. + * + * @param partNum + * @return value as set + */ + @Override + public double get(int partNum) { + return setValues[partNum]; + } + + public double getMaximum() { + return maximum; + } + + /** + * The minimum and maximum are only used when setting up knobs or other control systems. The + * internal values are not clipped to this range. + * + * @param maximum + */ + public void setMaximum(double maximum) { + this.maximum = maximum; + } + + public double getMinimum() { + return minimum; + } + + public void setMinimum(double minimum) { + this.minimum = minimum; + } + + public double getDefault() { + return defaultValue; + } + + public void setDefault(double defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * Convenience function for setting limits on a port. These limits are recommended values when + * setting up a GUI. It is possible to set a port to a value outside these limits. + * + * @param minimum + * @param value default value, will be clipped to min/max + * @param maximum + */ + public void setup(double minimum, double value, double maximum) { + setMinimum(minimum); + setMaximum(maximum); + setDefault(value); + set(value); + } + + // Grab min, max, default from another port. + public void setup(UnitInputPort other) { + setup(other.getMinimum(), other.getDefault(), other.getMaximum()); + } + + public boolean isValueAdded() { + return valueAdded; + } + + /** + * If set false then the set() value will be ignored when other ports are connected to this port. + * The sum of the connected port values will be used instead. + * + * If set true then the set() value will be added to the sum of the connected port values. + * This is useful when you want to modulate the set value. + * + * The default is false. + * + * @param valueAdded + */ + public void setValueAdded(boolean valueAdded) { + this.valueAdded = valueAdded; + } + + public void connect(int thisPartNum, UnitOutputPort otherPort, int otherPartNum, + TimeStamp timeStamp) { + otherPort.connect(otherPartNum, this, thisPartNum, timeStamp); + } + + /** Connect an input to an output port. */ + public void connect(int thisPartNum, UnitOutputPort otherPort, int otherPartNum) { + // Typically connections are made from output to input because it is + // more intuitive. + otherPort.connect(otherPartNum, this, thisPartNum); + } + + public void connect(UnitOutputPort otherPort) { + connect(0, otherPort, 0); + } + + @Override + public void connect(ConnectableOutput other) { + other.connect(this); + } + + public void disconnect(int thisPartNum, UnitOutputPort otherPort, int otherPartNum) { + otherPort.disconnect(otherPartNum, this, thisPartNum); + } + + @Override + public PortBlockPart getPortBlockPart() { + return parts[0]; + } + + public ConnectableInput getConnectablePart(int i) { + return parts[i]; + } + + @Override + public void disconnect(ConnectableOutput other) { + other.disconnect(this); + } + + public void printConnections(PrintStream out, int level) { + for (PortBlockPart part : parts) { + ((InputMixingBlockPart) part).printConnections(out, level); + } + } + +} diff --git a/src/main/java/com/jsyn/ports/UnitOutputPort.java b/src/main/java/com/jsyn/ports/UnitOutputPort.java new file mode 100644 index 0000000..6fcd758 --- /dev/null +++ b/src/main/java/com/jsyn/ports/UnitOutputPort.java @@ -0,0 +1,103 @@ +/* + * 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.ports; + +import com.jsyn.unitgen.UnitSink; +import com.softsynth.shared.time.TimeStamp; + +/** + * Units write to their output port blocks. Other multiple connected input ports read from them. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ + +public class UnitOutputPort extends UnitBlockPort implements ConnectableOutput, GettablePort { + public UnitOutputPort() { + this("Output"); + } + + public UnitOutputPort(String name) { + this(1, name, 0.0); + } + + public UnitOutputPort(int numParts, String name) { + this(numParts, name, 0.0); + } + + public UnitOutputPort(int numParts, String name, double defaultValue) { + super(numParts, name, defaultValue); + } + + public void flatten() { + for (PortBlockPart part : parts) { + part.flatten(); + } + } + + public void connect(int thisPartNum, UnitInputPort otherPort, int otherPartNum) { + PortBlockPart source = parts[thisPartNum]; + PortBlockPart destination = otherPort.parts[otherPartNum]; + source.connect(destination); + } + + public void connect(int thisPartNum, UnitInputPort otherPort, int otherPartNum, + TimeStamp timeStamp) { + PortBlockPart source = parts[thisPartNum]; + PortBlockPart destination = otherPort.parts[otherPartNum]; + source.connect(destination, timeStamp); + } + + public void connect(UnitInputPort input) { + connect(0, input, 0); + } + + @Override + public void connect(ConnectableInput input) { + parts[0].connect(input); + } + + public void connect(UnitSink sink) { + connect(0, sink.getInput(), 0); + } + + public void disconnect(int thisPartNum, UnitInputPort otherPort, int otherPartNum) { + PortBlockPart source = parts[thisPartNum]; + PortBlockPart destination = otherPort.parts[otherPartNum]; + source.disconnect(destination); + } + + public void disconnect(int thisPartNum, UnitInputPort otherPort, int otherPartNum, + TimeStamp timeStamp) { + PortBlockPart source = parts[thisPartNum]; + PortBlockPart destination = otherPort.parts[otherPartNum]; + source.disconnect(destination, timeStamp); + } + + public void disconnect(UnitInputPort otherPort) { + disconnect(0, otherPort, 0); + } + + @Override + public void disconnect(ConnectableInput input) { + parts[0].disconnect(input); + } + + public ConnectableOutput getConnectablePart(int i) { + return parts[i]; + } + +} diff --git a/src/main/java/com/jsyn/ports/UnitPort.java b/src/main/java/com/jsyn/ports/UnitPort.java new file mode 100644 index 0000000..a652e68 --- /dev/null +++ b/src/main/java/com/jsyn/ports/UnitPort.java @@ -0,0 +1,85 @@ +/* + * 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.ports; + +import com.jsyn.engine.SynthesisEngine; +import com.jsyn.unitgen.UnitGenerator; +import com.softsynth.shared.time.ScheduledCommand; +import com.softsynth.shared.time.TimeStamp; + +/** + * Basic audio port for JSyn unit generators. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class UnitPort { + private String name; + private UnitGenerator unit; + + public UnitPort(String name) { + this.name = name; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setUnitGenerator(UnitGenerator unit) { + // If a port is in a circuit then we want to just use the lower level + // unit that instantiated the circuit. + if (this.unit == null) { + this.unit = unit; + } + } + + public UnitGenerator getUnitGenerator() { + return unit; + } + + SynthesisEngine getSynthesisEngine() { + if (unit == null) { + return null; + } + return unit.getSynthesisEngine(); + } + + public int getNumParts() { + return 1; + } + + public void scheduleCommand(TimeStamp timeStamp, ScheduledCommand scheduledCommand) { + if (getSynthesisEngine() == null) { + scheduledCommand.run(); + } else { + getSynthesisEngine().scheduleCommand(timeStamp, scheduledCommand); + } + } + + public void queueCommand(ScheduledCommand scheduledCommand) { + if (getSynthesisEngine() == null) { + scheduledCommand.run(); + } else { + getSynthesisEngine().scheduleCommand(getSynthesisEngine().createTimeStamp(), + scheduledCommand); + } + } + +} diff --git a/src/main/java/com/jsyn/ports/UnitSpectralInputPort.java b/src/main/java/com/jsyn/ports/UnitSpectralInputPort.java new file mode 100644 index 0000000..bdf0ff5 --- /dev/null +++ b/src/main/java/com/jsyn/ports/UnitSpectralInputPort.java @@ -0,0 +1,83 @@ +/* + * 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.ports; + +import com.jsyn.data.Spectrum; + +public class UnitSpectralInputPort extends UnitPort implements ConnectableInput { + private UnitSpectralOutputPort other; + + private Spectrum spectrum; + + public UnitSpectralInputPort() { + this("Output"); + } + + public UnitSpectralInputPort(String name) { + super(name); + } + + public void setSpectrum(Spectrum spectrum) { + this.spectrum = spectrum; + } + + public Spectrum getSpectrum() { + if (other == null) { + return spectrum; + } else { + return other.getSpectrum(); + } + } + + @Override + public void connect(ConnectableOutput other) { + if (other instanceof UnitSpectralOutputPort) { + this.other = (UnitSpectralOutputPort) other; + } else { + throw new RuntimeException( + "Can only connect UnitSpectralOutputPort to UnitSpectralInputPort!"); + } + } + + @Override + public void disconnect(ConnectableOutput other) { + if (this.other == other) { + this.other = null; + } + } + + @Override + public PortBlockPart getPortBlockPart() { + return null; + } + + @Override + public void pullData(long frameCount, int start, int limit) { + if (other != null) { + other.getUnitGenerator().pullData(frameCount, start, limit); + } + } + + public boolean isAvailable() { + if (other != null) { + return other.isAvailable(); + } else { + return (spectrum != null); + } + } + +} diff --git a/src/main/java/com/jsyn/ports/UnitSpectralOutputPort.java b/src/main/java/com/jsyn/ports/UnitSpectralOutputPort.java new file mode 100644 index 0000000..51633ce --- /dev/null +++ b/src/main/java/com/jsyn/ports/UnitSpectralOutputPort.java @@ -0,0 +1,69 @@ +/* + * 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.ports; + +import com.jsyn.data.Spectrum; + +public class UnitSpectralOutputPort extends UnitPort implements ConnectableOutput { + private Spectrum spectrum; + private boolean available; + + public UnitSpectralOutputPort() { + this("Output"); + } + + public UnitSpectralOutputPort(int size) { + this("Output", size); + } + + public UnitSpectralOutputPort(String name) { + super(name); + spectrum = new Spectrum(); + } + + public UnitSpectralOutputPort(String name, int size) { + super(name); + spectrum = new Spectrum(size); + } + + public void setSize(int size) { + spectrum.setSize(size); + } + + public Spectrum getSpectrum() { + return spectrum; + } + + public void advance() { + available = true; + } + + @Override + public void connect(ConnectableInput other) { + other.connect(this); + } + + @Override + public void disconnect(ConnectableInput other) { + other.disconnect(this); + } + + public boolean isAvailable() { + return available; + } + +} diff --git a/src/main/java/com/jsyn/ports/UnitVariablePort.java b/src/main/java/com/jsyn/ports/UnitVariablePort.java new file mode 100644 index 0000000..60b64fd --- /dev/null +++ b/src/main/java/com/jsyn/ports/UnitVariablePort.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.ports; + +import com.softsynth.shared.time.ScheduledCommand; +import com.softsynth.shared.time.TimeStamp; + +public class UnitVariablePort extends UnitPort implements SettablePort { + private double value; + + public UnitVariablePort(String name, double defaultValue) { + super(name); + value = defaultValue; + } + + public UnitVariablePort(String name) { + super(name); + } + + public void setValue(double value) { + this.value = value; + } + + public void set(double value) { + this.value = value; + } + + public double get() { + return value; + } + + public double getValue() { + return value; + } + + @Override + public double getValue(int partNum) { + return value; + } + + @Override + public void set(int partNum, final double value, TimeStamp timeStamp) { + scheduleCommand(timeStamp, new ScheduledCommand() { + @Override + public void run() { + set(value); + } + }); + } +} diff --git a/src/main/java/com/jsyn/ports/package.html b/src/main/java/com/jsyn/ports/package.html new file mode 100644 index 0000000..3547618 --- /dev/null +++ b/src/main/java/com/jsyn/ports/package.html @@ -0,0 +1,13 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> +<html> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> +<title>JSyn Ports</title> +</head> +<body> +<p>Ports are used to pass audio data in and out of UnitGenerators. +They can also be used to connect UnitGenerators together so that signals can flow between them. +The UnitDataQueuePort contains a FIFO that will accept envelope and sample data. +</p> +</body> +</html>
\ No newline at end of file diff --git a/src/main/java/com/jsyn/scope/AudioScope.java b/src/main/java/com/jsyn/scope/AudioScope.java new file mode 100644 index 0000000..7b2a98c --- /dev/null +++ b/src/main/java/com/jsyn/scope/AudioScope.java @@ -0,0 +1,101 @@ +/* + * 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.scope; + +import com.jsyn.Synthesizer; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.scope.swing.AudioScopeView; + +// TODO Auto and Manual triggers. +// TODO Auto scaling of vertical. +// TODO Fixed size Y scale knobs. +// TODO Pan back and forth around trigger. +// TODO Continuous capture +/** + * Digital oscilloscope for JSyn. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class AudioScope { + public enum TriggerMode { + AUTO, NORMAL // , MANUAL + } + + public enum ViewMode { + WAVEFORM, SPECTRUM + } + + private AudioScopeView audioScopeView = null; + private AudioScopeModel audioScopeModel; + + public AudioScope(Synthesizer synth) { + audioScopeModel = new AudioScopeModel(synth); + } + + public AudioScopeProbe addProbe(UnitOutputPort output) { + return addProbe(output, 0); + } + + public AudioScopeProbe addProbe(UnitOutputPort output, int partIndex) { + return audioScopeModel.addProbe(output, partIndex); + } + + public void start() { + audioScopeModel.start(); + } + + public void stop() { + audioScopeModel.stop(); + } + + public AudioScopeModel getModel() { + return audioScopeModel; + } + + public AudioScopeView getView() { + if (audioScopeView == null) { + audioScopeView = new AudioScopeView(); + audioScopeView.setModel(audioScopeModel); + } + return audioScopeView; + } + + public void setTriggerMode(TriggerMode triggerMode) { + audioScopeModel.setTriggerMode(triggerMode); + } + + public void setTriggerSource(AudioScopeProbe probe) { + audioScopeModel.setTriggerSource(probe); + } + + public void setTriggerLevel(double level) { + getModel().getTriggerModel().getLevelModel().setDoubleValue(level); + } + + public double getTriggerLevel() { + return getModel().getTriggerModel().getLevelModel().getDoubleValue(); + } + + /** + * Not yet implemented. + * @param viewMode + */ + public void setViewMode(ViewMode viewMode) { + // TODO Auto-generated method stub + } + +} diff --git a/src/main/java/com/jsyn/scope/AudioScopeModel.java b/src/main/java/com/jsyn/scope/AudioScopeModel.java new file mode 100644 index 0000000..85c4413 --- /dev/null +++ b/src/main/java/com/jsyn/scope/AudioScopeModel.java @@ -0,0 +1,157 @@ +/* + * 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.scope; + +import java.util.ArrayList; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.jsyn.Synthesizer; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.scope.AudioScope.TriggerMode; + +public class AudioScopeModel implements Runnable { + private static final int PRE_TRIGGER_SIZE = 32; + private Synthesizer synthesisEngine; + private ArrayList<AudioScopeProbe> probes = new ArrayList<AudioScopeProbe>(); + private CopyOnWriteArrayList<ChangeListener> changeListeners = new CopyOnWriteArrayList<ChangeListener>(); + private MultiChannelScopeProbeUnit probeUnit; + private double timeToArm; + private double period = 0.2; + private TriggerModel triggerModel; + + public AudioScopeModel(Synthesizer synth) { + this.synthesisEngine = synth; + triggerModel = new TriggerModel(); + } + + public AudioScopeProbe addProbe(UnitOutputPort output, int partIndex) { + AudioScopeProbe probe = new AudioScopeProbe(this, output, partIndex); + DefaultWaveTraceModel waveTraceModel = new DefaultWaveTraceModel(this, probes.size()); + probe.setWaveTraceModel(waveTraceModel); + probes.add(probe); + if (triggerModel.getSource() == null) { + triggerModel.setSource(probe); + } + return probe; + } + + public void start() { + stop(); + probeUnit = new MultiChannelScopeProbeUnit(probes.size(), triggerModel); + synthesisEngine.add(probeUnit); + for (int i = 0; i < probes.size(); i++) { + AudioScopeProbe probe = probes.get(i); + probe.getSource().connect(probe.getPartIndex(), probeUnit.input, i); + } + // Connect trigger signal to input of probe. + triggerModel.getSource().getSource() + .connect(triggerModel.getSource().getPartIndex(), probeUnit.trigger, 0); + probeUnit.start(); + + // Get synthesizer time in seconds. + timeToArm = synthesisEngine.getCurrentTime(); + probeUnit.arm(timeToArm, this); + } + + public void stop() { + if (probeUnit != null) { + for (int i = 0; i < probes.size(); i++) { + probeUnit.input.disconnectAll(i); + } + probeUnit.trigger.disconnectAll(); + probeUnit.stop(); + synthesisEngine.remove(probeUnit); + probeUnit = null; + } + } + + public AudioScopeProbe[] getProbes() { + return probes.toArray(new AudioScopeProbe[0]); + } + + public Synthesizer getSynthesizer() { + return synthesisEngine; + } + + @Override + public void run() { + fireChangeListeners(); + timeToArm = synthesisEngine.getCurrentTime(); + timeToArm += period; + probeUnit.arm(timeToArm, this); + } + + private void fireChangeListeners() { + ChangeEvent changeEvent = new ChangeEvent(this); + for (ChangeListener listener : changeListeners) { + listener.stateChanged(changeEvent); + } + // debug(); + } + + public void addChangeListener(ChangeListener changeListener) { + changeListeners.add(changeListener); + } + + public void removeChangeListener(ChangeListener changeListener) { + changeListeners.remove(changeListener); + } + + public void setTriggerMode(TriggerMode triggerMode) { + triggerModel.getModeModel().setSelectedItem(triggerMode); + } + + public void setTriggerSource(AudioScopeProbe probe) { + triggerModel.setSource(probe); + } + + public double getSample(int bufferIndex, int i) { + return probeUnit.getSample(bufferIndex, i); + } + + public int getFramesPerBuffer() { + return probeUnit.getFramesPerBuffer(); + } + + public int getFramesCaptured() { + return probeUnit.getFramesCaptured(); + } + + public int getVisibleSize() { + int size = 0; + if (probeUnit != null) { + size = probeUnit.getPostTriggerSize() + PRE_TRIGGER_SIZE; + if (size > getFramesCaptured()) { + size = getFramesCaptured(); + } + } + return size; + } + + public int getStartIndex() { + // TODO Add pan support here. + return getFramesCaptured() - getVisibleSize(); + } + + public TriggerModel getTriggerModel() { + return triggerModel; + } + +} diff --git a/src/main/java/com/jsyn/scope/AudioScopeProbe.java b/src/main/java/com/jsyn/scope/AudioScopeProbe.java new file mode 100644 index 0000000..f1aad65 --- /dev/null +++ b/src/main/java/com/jsyn/scope/AudioScopeProbe.java @@ -0,0 +1,94 @@ +/* + * 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.scope; + +import java.awt.Color; + +import javax.swing.JToggleButton.ToggleButtonModel; + +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.swing.ExponentialRangeModel; + +/** + * Collect data from the source and make it available to the scope. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class AudioScopeProbe { + // private UnitOutputPort output; + private WaveTraceModel waveTraceModel; + private AudioScopeModel audioScopeModel; + private UnitOutputPort source; + private int partIndex; + private Color color; + private ExponentialRangeModel verticalScaleModel; + private ToggleButtonModel autoScaleButtonModel; + private double MIN_RANGE = 0.01; + private double MAX_RANGE = 100.0; + + public AudioScopeProbe(AudioScopeModel audioScopeModel, UnitOutputPort source, int partIndex) { + this.audioScopeModel = audioScopeModel; + this.source = source; + this.partIndex = partIndex; + + verticalScaleModel = new ExponentialRangeModel("VScale", 1000, MIN_RANGE, MAX_RANGE, + MIN_RANGE); + autoScaleButtonModel = new ToggleButtonModel(); + autoScaleButtonModel.setSelected(true); + } + + public WaveTraceModel getWaveTraceModel() { + return waveTraceModel; + } + + public void setWaveTraceModel(WaveTraceModel waveTraceModel) { + this.waveTraceModel = waveTraceModel; + } + + public UnitOutputPort getSource() { + return source; + } + + public int getPartIndex() { + return partIndex; + } + + public Color getColor() { + return color; + } + + public void setColor(Color color) { + this.color = color; + } + + public void setAutoScaleEnabled(boolean enabled) { + autoScaleButtonModel.setSelected(enabled); + } + + public void setVerticalScale(double max) { + verticalScaleModel.setDoubleValue(max); + } + + public ExponentialRangeModel getVerticalScaleModel() { + return verticalScaleModel; + } + + public ToggleButtonModel getAutoScaleButtonModel() { + return autoScaleButtonModel; + } + +} diff --git a/src/main/java/com/jsyn/scope/DefaultWaveTraceModel.java b/src/main/java/com/jsyn/scope/DefaultWaveTraceModel.java new file mode 100644 index 0000000..a123c0b --- /dev/null +++ b/src/main/java/com/jsyn/scope/DefaultWaveTraceModel.java @@ -0,0 +1,48 @@ +/* + * 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.scope; + +public class DefaultWaveTraceModel implements WaveTraceModel { + private AudioScopeModel audioScopeModel; + private int bufferIndex; + + public DefaultWaveTraceModel(AudioScopeModel audioScopeModel, int bufferIndex) { + this.audioScopeModel = audioScopeModel; + this.bufferIndex = bufferIndex; + } + + @Override + public double getSample(int i) { + return audioScopeModel.getSample(bufferIndex, i); + } + + @Override + public int getSize() { + return audioScopeModel.getFramesCaptured(); + } + + @Override + public int getStartIndex() { + return audioScopeModel.getStartIndex(); + } + + @Override + public int getVisibleSize() { + return audioScopeModel.getVisibleSize(); + } + +} diff --git a/src/main/java/com/jsyn/scope/MultiChannelScopeProbeUnit.java b/src/main/java/com/jsyn/scope/MultiChannelScopeProbeUnit.java new file mode 100644 index 0000000..59bb635 --- /dev/null +++ b/src/main/java/com/jsyn/scope/MultiChannelScopeProbeUnit.java @@ -0,0 +1,246 @@ +/* + * 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.scope; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.scope.AudioScope.TriggerMode; +import com.jsyn.unitgen.UnitGenerator; +import com.softsynth.shared.time.ScheduledCommand; + +/** + * Multi-channel scope probe with an independent trigger input. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class MultiChannelScopeProbeUnit extends UnitGenerator { + // Signal that is captured. + public UnitInputPort input; + // Signal that triggers the probe. + public UnitInputPort trigger; + + // I am using ints instead of an enum for performance reasons. + private static final int STATE_IDLE = 0; + private static final int STATE_ARMED = 1; + private static final int STATE_READY = 2; + private static final int STATE_TRIGGERED = 3; + private int state = STATE_IDLE; + + private int numChannels; + private double[][] inputValues; + private static final int FRAMES_PER_BUFFER = 4096; // must be power of two + private static final int FRAMES_PER_BUFFER_MASK = FRAMES_PER_BUFFER - 1; + private Runnable callback; + + private TriggerModel triggerModel; + private int autoCountdown; + private int countdown; + private int postTriggerSize = 512; + SignalBuffer captureBuffer; + SignalBuffer displayBuffer; + + // Use double buffers. One for capture, one for display. + static class SignalBuffer { + float[][] buffers; + private int writeCursor; + private int triggerIndex; + private int framesCaptured; + + SignalBuffer(int numChannels) { + buffers = new float[numChannels][]; + for (int j = 0; j < numChannels; j++) { + buffers[j] = new float[FRAMES_PER_BUFFER]; + } + } + + void reset() { + writeCursor = 0; + triggerIndex = 0; + framesCaptured = 0; + } + + public void saveChannelValue(int j, float value) { + buffers[j][writeCursor] = value; + } + + public void markTrigger() { + triggerIndex = writeCursor; + } + + public void bumpCursor() { + writeCursor = (writeCursor + 1) & FRAMES_PER_BUFFER_MASK; + if (writeCursor >= FRAMES_PER_BUFFER) { + writeCursor = 0; + } + if (framesCaptured < FRAMES_PER_BUFFER) { + framesCaptured += 1; + } + } + + private int convertInternalToExternalIndex(int internalIndex) { + if (framesCaptured < FRAMES_PER_BUFFER) { + return internalIndex; + } else { + return (internalIndex - writeCursor) & (FRAMES_PER_BUFFER_MASK); + } + } + + private int convertExternalToInternalIndex(int externalIndex) { + if (framesCaptured < FRAMES_PER_BUFFER) { + return externalIndex; + } else { + return (externalIndex + writeCursor) & (FRAMES_PER_BUFFER_MASK); + } + } + + public int getTriggerIndex() { + return convertInternalToExternalIndex(triggerIndex); + } + + public int getFramesCaptured() { + return framesCaptured; + } + + public float getSample(int bufferIndex, int sampleIndex) { + int index = convertExternalToInternalIndex(sampleIndex); + return buffers[bufferIndex][index]; + } + } + + public MultiChannelScopeProbeUnit(int numChannels, TriggerModel triggerModel) { + this.numChannels = numChannels; + captureBuffer = new SignalBuffer(numChannels); + displayBuffer = new SignalBuffer(numChannels); + this.triggerModel = triggerModel; + addPort(trigger = new UnitInputPort(numChannels, "Trigger")); + addPort(input = new UnitInputPort(numChannels, "Input")); + inputValues = new double[numChannels][]; + } + + private synchronized void switchBuffers() { + SignalBuffer temp = captureBuffer; + captureBuffer = displayBuffer; + displayBuffer = temp; + } + + private void internalArm(Runnable callback) { + this.callback = callback; + state = STATE_ARMED; + captureBuffer.reset(); + } + + class ScheduledArm implements ScheduledCommand { + private Runnable callback; + + ScheduledArm(Runnable callback) { + this.callback = callback; + } + + @Override + public void run() { + internalArm(this.callback); + } + } + + /** Arm the probe at a future time. */ + public void arm(double time, Runnable callback) { + ScheduledArm command = new ScheduledArm(callback); + getSynthesisEngine().scheduleCommand(time, command); + } + + @Override + public void generate(int start, int limit) { + if (state != STATE_IDLE) { + TriggerMode triggerMode = triggerModel.getMode(); + double triggerLevel = triggerModel.getTriggerLevel(); + double[] triggerValues = trigger.getValues(); + + for (int j = 0; j < numChannels; j++) { + inputValues[j] = input.getValues(j); + } + + for (int i = start; i < limit; i++) { + // Capture one sample from each channel. + for (int j = 0; j < numChannels; j++) { + captureBuffer.saveChannelValue(j, (float) inputValues[j][i]); + } + captureBuffer.bumpCursor(); + + switch (state) { + case STATE_ARMED: + if (triggerValues[i] <= triggerLevel) { + state = STATE_READY; + autoCountdown = 44100; + } + break; + + case STATE_READY: { + boolean triggered = false; + if (triggerValues[i] > triggerLevel) { + triggered = true; + } else if (triggerMode.equals(TriggerMode.AUTO)) { + if (--autoCountdown == 0) { + triggered = true; + } + } + if (triggered) { + captureBuffer.markTrigger(); + state = STATE_TRIGGERED; + countdown = postTriggerSize; + } + } + break; + + case STATE_TRIGGERED: + countdown -= 1; + if (countdown <= 0) { + state = STATE_IDLE; + switchBuffers(); + fireCallback(); + } + break; + } + } + } + } + + private void fireCallback() { + if (callback != null) { + callback.run(); + } + } + + public float getSample(int bufferIndex, int sampleIndex) { + return displayBuffer.getSample(bufferIndex, sampleIndex); + } + + public int getTriggerIndex() { + return displayBuffer.getTriggerIndex(); + } + + public int getFramesCaptured() { + return displayBuffer.getFramesCaptured(); + } + + public int getFramesPerBuffer() { + return FRAMES_PER_BUFFER; + } + + public int getPostTriggerSize() { + return postTriggerSize; + } + +} diff --git a/src/main/java/com/jsyn/scope/TriggerModel.java b/src/main/java/com/jsyn/scope/TriggerModel.java new file mode 100644 index 0000000..0367d71 --- /dev/null +++ b/src/main/java/com/jsyn/scope/TriggerModel.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.scope; + +import javax.swing.DefaultComboBoxModel; + +import com.jsyn.scope.AudioScope.TriggerMode; +import com.jsyn.swing.ExponentialRangeModel; + +public class TriggerModel { + private ExponentialRangeModel levelModel; + private DefaultComboBoxModel<AudioScope.TriggerMode> modeModel; + private AudioScopeProbe source; + + public TriggerModel() { + modeModel = new DefaultComboBoxModel<AudioScope.TriggerMode>(); + modeModel.addElement(TriggerMode.AUTO); + modeModel.addElement(TriggerMode.NORMAL); + levelModel = new ExponentialRangeModel("TriggerLevel", 1000, 0.01, 2.0, 0.04); + } + + public AudioScopeProbe getSource() { + return source; + } + + public void setSource(AudioScopeProbe source) { + this.source = source; + } + + public ExponentialRangeModel getLevelModel() { + return levelModel; + } + + public void setLevelModel(ExponentialRangeModel levelModel) { + this.levelModel = levelModel; + } + + public DefaultComboBoxModel<TriggerMode> getModeModel() { + return modeModel; + } + + public void setModeModel(DefaultComboBoxModel<TriggerMode> modeModel) { + this.modeModel = modeModel; + } + + public double getTriggerLevel() { + return levelModel.getDoubleValue(); + } + + public TriggerMode getMode() { + return (TriggerMode) modeModel.getSelectedItem(); + } +} diff --git a/src/main/java/com/jsyn/scope/WaveTraceModel.java b/src/main/java/com/jsyn/scope/WaveTraceModel.java new file mode 100644 index 0000000..e9d8bf9 --- /dev/null +++ b/src/main/java/com/jsyn/scope/WaveTraceModel.java @@ -0,0 +1,27 @@ +/* + * 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.scope; + +public interface WaveTraceModel { + int getSize(); + + int getVisibleSize(); + + int getStartIndex(); + + double getSample(int i); +} diff --git a/src/main/java/com/jsyn/scope/swing/AudioScopeProbeView.java b/src/main/java/com/jsyn/scope/swing/AudioScopeProbeView.java new file mode 100644 index 0000000..59526e1 --- /dev/null +++ b/src/main/java/com/jsyn/scope/swing/AudioScopeProbeView.java @@ -0,0 +1,45 @@ +/* + * 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.scope.swing; + +import com.jsyn.scope.AudioScopeProbe; + +/** + * Wave display associated with a probe. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class AudioScopeProbeView { + private AudioScopeProbe probeModel; + private WaveTraceView waveTrace; + + public AudioScopeProbeView(AudioScopeProbe probeModel) { + this.probeModel = probeModel; + waveTrace = new WaveTraceView(probeModel.getAutoScaleButtonModel(), + probeModel.getVerticalScaleModel()); + waveTrace.setModel(probeModel.getWaveTraceModel()); + } + + public WaveTraceView getWaveTraceView() { + return waveTrace; + } + + public AudioScopeProbe getModel() { + return probeModel; + } + +} diff --git a/src/main/java/com/jsyn/scope/swing/AudioScopeView.java b/src/main/java/com/jsyn/scope/swing/AudioScopeView.java new file mode 100644 index 0000000..ec1afa3 --- /dev/null +++ b/src/main/java/com/jsyn/scope/swing/AudioScopeView.java @@ -0,0 +1,112 @@ +/* + * 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.scope.swing; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.util.ArrayList; + +import javax.swing.JPanel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.jsyn.scope.AudioScopeModel; +import com.jsyn.scope.AudioScopeProbe; + +public class AudioScopeView extends JPanel { + private static final long serialVersionUID = -7507986850757860853L; + private AudioScopeModel audioScopeModel; + private ArrayList<AudioScopeProbeView> probeViews = new ArrayList<AudioScopeProbeView>(); + private MultipleWaveDisplay multipleWaveDisplay; + private boolean showControls = false; + private ScopeControlPanel controlPanel = null; + + public AudioScopeView() { + setBackground(Color.GREEN); + } + + public void setModel(AudioScopeModel audioScopeModel) { + this.audioScopeModel = audioScopeModel; + // Create a view for each probe. + probeViews.clear(); + for (AudioScopeProbe probeModel : audioScopeModel.getProbes()) { + AudioScopeProbeView audioScopeProbeView = new AudioScopeProbeView(probeModel); + probeViews.add(audioScopeProbeView); + } + setupGUI(); + + // Listener for signal change events. + audioScopeModel.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + multipleWaveDisplay.repaint(); + } + }); + + } + + private void setupGUI() { + removeAll(); + setLayout(new BorderLayout()); + multipleWaveDisplay = new MultipleWaveDisplay(); + + for (AudioScopeProbeView probeView : probeViews) { + multipleWaveDisplay.addWaveTrace(probeView.getWaveTraceView()); + probeView.getModel().setColor(probeView.getWaveTraceView().getColor()); + } + + add(multipleWaveDisplay, BorderLayout.CENTER); + + setMinimumSize(new Dimension(400, 200)); + setPreferredSize(new Dimension(600, 250)); + setMaximumSize(new Dimension(1200, 300)); + } + + /** @deprecated Use setControlsVisible() instead. */ + @Deprecated + public void setShowControls(boolean show) { + setControlsVisible(show); + } + + public void setControlsVisible(boolean show) { + if (this.showControls) { + if (!show && (controlPanel != null)) { + remove(controlPanel); + } + } else { + if (show) { + if (controlPanel == null) { + controlPanel = new ScopeControlPanel(this); + } + add(controlPanel, BorderLayout.EAST); + validate(); + } + } + + this.showControls = show; + } + + public AudioScopeModel getModel() { + return audioScopeModel; + } + + public AudioScopeProbeView[] getProbeViews() { + return probeViews.toArray(new AudioScopeProbeView[0]); + } + +} diff --git a/src/main/java/com/jsyn/scope/swing/MultipleWaveDisplay.java b/src/main/java/com/jsyn/scope/swing/MultipleWaveDisplay.java new file mode 100644 index 0000000..0259850 --- /dev/null +++ b/src/main/java/com/jsyn/scope/swing/MultipleWaveDisplay.java @@ -0,0 +1,58 @@ +/* + * 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.scope.swing; + +import java.awt.Color; +import java.awt.Graphics; +import java.util.ArrayList; + +import javax.swing.JPanel; + +/** + * Display multiple waveforms together in different colors. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public class MultipleWaveDisplay extends JPanel { + private static final long serialVersionUID = -5157397030540800373L; + + private ArrayList<WaveTraceView> waveTraceViews = new ArrayList<WaveTraceView>(); + private Color[] defaultColors = { + Color.BLUE, Color.RED, Color.BLACK, Color.MAGENTA, Color.GREEN, Color.ORANGE + }; + + public MultipleWaveDisplay() { + setBackground(Color.WHITE); + } + + public void addWaveTrace(WaveTraceView waveTraceView) { + if (waveTraceView.getColor() == null) { + waveTraceView.setColor(defaultColors[waveTraceViews.size() % defaultColors.length]); + } + waveTraceViews.add(waveTraceView); + } + + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + int width = getWidth(); + int height = getHeight(); + for (WaveTraceView waveTraceView : waveTraceViews.toArray(new WaveTraceView[0])) { + waveTraceView.drawWave(g, width, height); + } + } +} diff --git a/src/main/java/com/jsyn/scope/swing/ScopeControlPanel.java b/src/main/java/com/jsyn/scope/swing/ScopeControlPanel.java new file mode 100644 index 0000000..7f3a026 --- /dev/null +++ b/src/main/java/com/jsyn/scope/swing/ScopeControlPanel.java @@ -0,0 +1,46 @@ +/* + * 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.scope.swing; + +import java.awt.GridLayout; + +import javax.swing.JPanel; + +import com.jsyn.scope.AudioScopeModel; + +public class ScopeControlPanel extends JPanel { + private static final long serialVersionUID = 7738305116057614812L; + private AudioScopeModel audioScopeModel; + private ScopeTriggerPanel triggerPanel; + private JPanel probeRows; + + public ScopeControlPanel(AudioScopeView audioScopeView) { + setLayout(new GridLayout(0, 1)); + this.audioScopeModel = audioScopeView.getModel(); + triggerPanel = new ScopeTriggerPanel(audioScopeModel); + add(triggerPanel); + + probeRows = new JPanel(); + probeRows.setLayout(new GridLayout(1, 0)); + add(probeRows); + for (AudioScopeProbeView probeView : audioScopeView.getProbeViews()) { + ScopeProbePanel probePanel = new ScopeProbePanel(probeView); + probeRows.add(probePanel); + } + } + +} diff --git a/src/main/java/com/jsyn/scope/swing/ScopeProbePanel.java b/src/main/java/com/jsyn/scope/swing/ScopeProbePanel.java new file mode 100644 index 0000000..a0dec91 --- /dev/null +++ b/src/main/java/com/jsyn/scope/swing/ScopeProbePanel.java @@ -0,0 +1,87 @@ +/* + * 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.scope.swing; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JCheckBox; +import javax.swing.JPanel; +import javax.swing.JToggleButton.ToggleButtonModel; + +import com.jsyn.scope.AudioScopeProbe; +import com.jsyn.swing.RotaryTextController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ScopeProbePanel extends JPanel { + + private static final Logger LOGGER = LoggerFactory.getLogger(ScopeProbePanel.class); + private static final long serialVersionUID = 4511589171299298548L; + + private AudioScopeProbeView audioScopeProbeView; + private AudioScopeProbe audioScopeProbe; + private RotaryTextController verticalScaleKnob; + private JCheckBox autoBox; + private ToggleButtonModel autoScaleModel; + + public ScopeProbePanel(AudioScopeProbeView probeView) { + this.audioScopeProbeView = probeView; + setLayout(new BorderLayout()); + + setBorder(BorderFactory.createLineBorder(Color.GRAY, 3)); + + // Add a colored box to match the waveform color. + JPanel colorPanel = new JPanel(); + colorPanel.setMinimumSize(new Dimension(40, 40)); + audioScopeProbe = probeView.getModel(); + colorPanel.setBackground(audioScopeProbe.getColor()); + add(colorPanel, BorderLayout.NORTH); + + // Knob for tweaking vertical range. + verticalScaleKnob = new RotaryTextController(audioScopeProbeView.getWaveTraceView() + .getVerticalRangeModel(), 5); + add(verticalScaleKnob, BorderLayout.CENTER); + verticalScaleKnob.setTitle("YScale"); + + // Auto ranging checkbox. + autoBox = new JCheckBox("Auto"); + autoScaleModel = audioScopeProbeView.getWaveTraceView().getAutoButtonModel(); + autoScaleModel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ToggleButtonModel model = (ToggleButtonModel) e.getSource(); + boolean enabled = !model.isSelected(); + LOGGER.debug("Knob enabled = " + enabled); + verticalScaleKnob.setEnabled(!model.isSelected()); + } + }); + autoBox.setModel(autoScaleModel); + add(autoBox, BorderLayout.SOUTH); + + verticalScaleKnob.setEnabled(!autoScaleModel.isSelected()); + + setMinimumSize(new Dimension(80, 100)); + setPreferredSize(new Dimension(80, 150)); + setMaximumSize(new Dimension(120, 200)); + } + +} diff --git a/src/main/java/com/jsyn/scope/swing/ScopeTriggerPanel.java b/src/main/java/com/jsyn/scope/swing/ScopeTriggerPanel.java new file mode 100644 index 0000000..9c22aa1 --- /dev/null +++ b/src/main/java/com/jsyn/scope/swing/ScopeTriggerPanel.java @@ -0,0 +1,47 @@ +/* + * 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.scope.swing; + +import java.awt.BorderLayout; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.JPanel; + +import com.jsyn.scope.AudioScopeModel; +import com.jsyn.scope.TriggerModel; +import com.jsyn.scope.AudioScope.TriggerMode; +import com.jsyn.swing.RotaryTextController; + +public class ScopeTriggerPanel extends JPanel { + private static final long serialVersionUID = 4511589171299298548L; + private JComboBox<DefaultComboBoxModel<TriggerMode>> triggerModeComboBox; + private RotaryTextController triggerLevelKnob; + + public ScopeTriggerPanel(AudioScopeModel audioScopeModel) { + setLayout(new BorderLayout()); + TriggerModel triggerModel = audioScopeModel.getTriggerModel(); + triggerModeComboBox = new JComboBox(triggerModel.getModeModel()); + add(triggerModeComboBox, BorderLayout.NORTH); + + triggerLevelKnob = new RotaryTextController(triggerModel.getLevelModel(), 5); + + add(triggerLevelKnob, BorderLayout.CENTER); + triggerLevelKnob.setTitle("Trigger Level"); + } + +} diff --git a/src/main/java/com/jsyn/scope/swing/WaveTraceView.java b/src/main/java/com/jsyn/scope/swing/WaveTraceView.java new file mode 100644 index 0000000..849a6f4 --- /dev/null +++ b/src/main/java/com/jsyn/scope/swing/WaveTraceView.java @@ -0,0 +1,122 @@ +/* + * 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.scope.swing; + +import java.awt.Color; +import java.awt.Graphics; + +import javax.swing.JToggleButton.ToggleButtonModel; + +import com.jsyn.scope.WaveTraceModel; +import com.jsyn.swing.ExponentialRangeModel; + +public class WaveTraceView { + private static final double AUTO_DECAY = 0.95; + private WaveTraceModel waveTraceModel; + private Color color; + private ExponentialRangeModel verticalScaleModel; + private ToggleButtonModel autoScaleButtonModel; + + private double xScaler; + private double yScalar; + private int centerY; + + public WaveTraceView(ToggleButtonModel autoButtonModel, ExponentialRangeModel verticalRangeModel) { + this.verticalScaleModel = verticalRangeModel; + this.autoScaleButtonModel = autoButtonModel; + } + + public Color getColor() { + return color; + } + + public void setColor(Color color) { + this.color = color; + } + + public ExponentialRangeModel getVerticalRangeModel() { + return verticalScaleModel; + } + + public ToggleButtonModel getAutoButtonModel() { + return autoScaleButtonModel; + } + + public void setModel(WaveTraceModel waveTraceModel) { + this.waveTraceModel = waveTraceModel; + } + + public int convertRealToY(double r) { + return centerY - (int) (yScalar * r); + } + + public void drawWave(Graphics g, int width, int height) { + double sampleMax = 0.0; + double sampleMin = 0.0; + g.setColor(color); + int numSamples = waveTraceModel.getVisibleSize(); + if (numSamples > 0) { + xScaler = (double) width / numSamples; + // Scale by 0.5 because it is bipolar. + yScalar = 0.5 * height / verticalScaleModel.getDoubleValue(); + centerY = height / 2; + + // Calculate position of first point. + int x1 = 0; + int offset = waveTraceModel.getStartIndex(); + double value = waveTraceModel.getSample(offset); + int y1 = convertRealToY(value); + + // Draw lines to remaining points. + for (int i = 1; i < numSamples; i++) { + int x2 = (int) (i * xScaler); + value = waveTraceModel.getSample(offset + i); + int y2 = convertRealToY(value); + g.drawLine(x1, y1, x2, y2); + x1 = x2; + y1 = y2; + // measure min and max for auto + if (value > sampleMax) { + sampleMax = value; + } else if (value < sampleMin) { + sampleMin = value; + } + } + + autoScaleRange(sampleMax); + } + } + + // Autoscale the vertical range. + private void autoScaleRange(double sampleMax) { + if (autoScaleButtonModel.isSelected()) { + double scaledMax = sampleMax * 1.1; + double current = verticalScaleModel.getDoubleValue(); + if (scaledMax > current) { + verticalScaleModel.setDoubleValue(scaledMax); + } else { + double decayed = current * AUTO_DECAY; + if (decayed > verticalScaleModel.getMinimum()) { + if (scaledMax < decayed) { + verticalScaleModel.setDoubleValue(decayed); + } + } + } + } + } + +} diff --git a/src/main/java/com/jsyn/swing/ASCIIMusicKeyboard.java b/src/main/java/com/jsyn/swing/ASCIIMusicKeyboard.java new file mode 100644 index 0000000..dc02259 --- /dev/null +++ b/src/main/java/com/jsyn/swing/ASCIIMusicKeyboard.java @@ -0,0 +1,199 @@ +/* + * Copyright 2012 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.swing; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.HashSet; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +/** + * Support for playing musical scales on the ASCII keyboard of a computer. Has a Sustain checkbox + * that simulates a sustain pedal. Auto-repeat keys are detected and suppressed. + * + * @author Phil Burk (C) 2012 Mobileer Inc + */ +@SuppressWarnings("serial") +public abstract class ASCIIMusicKeyboard extends JPanel { + + private static final Logger LOGGER = LoggerFactory.getLogger(ASCIIMusicKeyboard.class); + + private final JCheckBox sustainBox; + private final JButton focusButton; + public static final String PENTATONIC_KEYS = "zxcvbasdfgqwert12345"; + public static final String SEPTATONIC_KEYS = "zxcvbnmasdfghjqwertyu1234567890"; + private String keyboardLayout = SEPTATONIC_KEYS; /* default music keyboard layout */ + private int basePitch = 48; + private final KeyListener keyListener; + private final JLabel countLabel; + private int onCount; + private int offCount; + private int pressedCount; + private int releasedCount; + private final HashSet<Integer> pressedKeys = new HashSet<Integer>(); + private final HashSet<Integer> onKeys = new HashSet<Integer>(); + + public ASCIIMusicKeyboard() { + focusButton = new JButton("Click here to play ASCII keys."); + focusButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + } + }); + keyListener = new KeyListener() { + + @Override + public void keyPressed(KeyEvent e) { + int key = e.getKeyChar(); + int idx = keyboardLayout.indexOf(key); + LOGGER.debug("keyPressed " + idx); + if (idx >= 0) { + if (!pressedKeys.contains(idx)) { + keyOn(convertIndexToPitch(idx)); + onCount++; + pressedKeys.add(idx); + onKeys.add(idx); + } + } + pressedCount++; + updateCountLabel(); + } + + @Override + public void keyReleased(KeyEvent e) { + int key = e.getKeyChar(); + int idx = keyboardLayout.indexOf(key); + LOGGER.debug("keyReleased " + idx); + if (idx >= 0) { + if (!sustainBox.isSelected()) { + noteOffInternal(idx); + onKeys.remove(idx); + } + pressedKeys.remove(idx); + } + releasedCount++; + updateCountLabel(); + } + + @Override + public void keyTyped(KeyEvent arg0) { + } + }; + focusButton.addKeyListener(keyListener); + add(focusButton); + + sustainBox = new JCheckBox("sustain"); + sustainBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + if (!sustainBox.isSelected()) { + for (Integer noteIndex : onKeys) { + noteOffInternal(noteIndex); + } + onKeys.clear(); + } + } + }); + add(sustainBox); + sustainBox.addKeyListener(keyListener); + + countLabel = new JLabel("0"); + add(countLabel); + } + + private void noteOffInternal(int idx) { + keyOff(convertIndexToPitch(idx)); + offCount++; + } + + protected void updateCountLabel() { + countLabel.setText(onCount + "/" + offCount + ", " + pressedCount + "/" + releasedCount); + } + + /** + * Convert index to a MIDI noteNumber in a major scale. Result will be offset by the basePitch. + */ + public int convertIndexToPitch(int keyIndex) { + int scale[] = { + 0, 2, 4, 5, 7, 9, 11 + }; + int octave = keyIndex / scale.length; + int idx = keyIndex % scale.length; + int pitch = (octave * 12) + scale[idx]; + return pitch + basePitch; + } + + /** + * This will be called when a key is released. It may also be called for sustaining notes when + * the Sustain check box is turned off. + * + * @param keyIndex + */ + public abstract void keyOff(int keyIndex); + + /** + * This will be called when a key is pressed. + * + * @param keyIndex + */ + public abstract void keyOn(int keyIndex); + + public String getKeyboardLayout() { + return keyboardLayout; + } + + /** + * Specify the keys that will be active for music. + * For example "qwertyui". + * If the first character in the layout is + * pressed then keyOn() will be called with 0. Default is SEPTATONIC_KEYS. + * + * @param keyboardLayout defines order of playable keys + */ + public void setKeyboardLayout(String keyboardLayout) { + this.keyboardLayout = keyboardLayout; + } + + public int getBasePitch() { + return basePitch; + } + + /** + * Define offset used by convertIndexToPitch(). + * + * @param basePitch + */ + public void setBasePitch(int basePitch) { + this.basePitch = basePitch; + } + + /** + * @return + */ + public KeyListener getKeyListener() { + return keyListener; + } +} diff --git a/src/main/java/com/jsyn/swing/DoubleBoundedRangeModel.java b/src/main/java/com/jsyn/swing/DoubleBoundedRangeModel.java new file mode 100644 index 0000000..647e8da --- /dev/null +++ b/src/main/java/com/jsyn/swing/DoubleBoundedRangeModel.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002 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.swing; + +import javax.swing.DefaultBoundedRangeModel; + +/** + * Double precision data model for sliders and knobs. Maps integer range info to a double value. + * + * @author Phil Burk, (C) 2002 SoftSynth.com, PROPRIETARY and CONFIDENTIAL + */ +public class DoubleBoundedRangeModel extends DefaultBoundedRangeModel { + private static final long serialVersionUID = 284361767102120148L; + protected String name; + private double dmin; + private double dmax; + + public DoubleBoundedRangeModel(String name, int resolution, double dmin, double dmax, + double dval) { + this.name = name; + this.dmin = dmin; + this.dmax = dmax; + setMinimum(0); + setMaximum(resolution); + setDoubleValue(dval); + } + + public boolean equivalentTo(Object other) { + if (!(other instanceof DoubleBoundedRangeModel)) + return false; + DoubleBoundedRangeModel otherModel = (DoubleBoundedRangeModel) other; + return (getValue() == otherModel.getValue()); + } + + /** Set name of value. This may be used in labels or when saving the value. */ + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public double getDoubleMinimum() { + return dmin; + } + + public double getDoubleMaximum() { + return dmax; + } + + public double sliderToDouble(int sliderValue) { + double doubleMin = getDoubleMinimum(); + return doubleMin + ((getDoubleMaximum() - doubleMin) * sliderValue / getMaximum()); + } + + public int doubleToSlider(double dval) { + double doubleMin = getDoubleMinimum(); + // TODO consider using Math.floor() instead of (int) if not too slow. + return (int) Math.round(getMaximum() * (dval - doubleMin) + / (getDoubleMaximum() - doubleMin)); + } + + public double getDoubleValue() { + return sliderToDouble(getValue()); + } + + public void setDoubleValue(double dval) { + setValue(doubleToSlider(dval)); + } + +} diff --git a/src/main/java/com/jsyn/swing/DoubleBoundedRangeSlider.java b/src/main/java/com/jsyn/swing/DoubleBoundedRangeSlider.java new file mode 100644 index 0000000..81b67df --- /dev/null +++ b/src/main/java/com/jsyn/swing/DoubleBoundedRangeSlider.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002 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.swing; + +import java.util.Hashtable; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JSlider; +import javax.swing.border.TitledBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.jsyn.util.NumericOutput; + +/** + * Slider that takes a DoubleBoundedRangeModel. It displays the current value in a titled border. + * + * @author Phil Burk, (C) 2002 SoftSynth.com, PROPRIETARY and CONFIDENTIAL + */ + +public class DoubleBoundedRangeSlider extends JSlider { + /** + * + */ + private static final long serialVersionUID = -440390322602838998L; + /** Places after decimal point for display. */ + private int places; + + public DoubleBoundedRangeSlider(DoubleBoundedRangeModel model) { + this(model, 5); + } + + public DoubleBoundedRangeSlider(DoubleBoundedRangeModel model, int places) { + super(model); + this.places = places; + setBorder(BorderFactory.createTitledBorder(generateTitleText())); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + updateTitle(); + } + }); + } + + protected void updateTitle() { + TitledBorder border = (TitledBorder) getBorder(); + if (border != null) { + border.setTitle(generateTitleText()); + repaint(); + } + } + + String generateTitleText() { + DoubleBoundedRangeModel model = (DoubleBoundedRangeModel) getModel(); + double val = model.getDoubleValue(); + String valText = NumericOutput.doubleToString(val, 0, places); + return model.getName() + " = " + valText; + } + + public void makeStandardLabels(int labelSpacing) { + setMajorTickSpacing(labelSpacing / 2); + setLabelTable(createStandardLabels(labelSpacing)); + setPaintTicks(true); + setPaintLabels(true); + } + + public double nextLabelValue(double current, double delta) { + return current + delta; + } + + public void makeLabels(double start, double delta, int places) { + DoubleBoundedRangeModel model = (DoubleBoundedRangeModel) getModel(); + // Create the label table + Hashtable<Integer, JLabel> labelTable = new Hashtable<Integer, JLabel>(); + double dval = start; + while (dval <= model.getDoubleMaximum()) { + int sliderValue = model.doubleToSlider(dval); + String text = NumericOutput.doubleToString(dval, 0, places); + labelTable.put(sliderValue, new JLabel(text)); + dval = nextLabelValue(dval, delta); + } + setLabelTable(labelTable); + setPaintLabels(true); + } + +} diff --git a/src/main/java/com/jsyn/swing/DoubleBoundedTextField.java b/src/main/java/com/jsyn/swing/DoubleBoundedTextField.java new file mode 100644 index 0000000..3301bb1 --- /dev/null +++ b/src/main/java/com/jsyn/swing/DoubleBoundedTextField.java @@ -0,0 +1,94 @@ +/* + * Copyright 2000 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.swing; + +import java.awt.Color; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; + +import javax.swing.JTextField; +import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * TextField that turns pink when modified, and white when the value is entered. + * + * @author (C) 2000-2010 Phil Burk, Mobileer Inc + * @version 16 + */ + +public class DoubleBoundedTextField extends JTextField { + private static final long serialVersionUID = 6882779668177620812L; + boolean modified = false; + int numCharacters; + private DoubleBoundedRangeModel model; + + public DoubleBoundedTextField(DoubleBoundedRangeModel pModel, int numCharacters) { + super(numCharacters); + this.model = pModel; + this.numCharacters = numCharacters; + setHorizontalAlignment(SwingConstants.LEADING); + setValue(model.getDoubleValue()); + addKeyListener(new KeyAdapter() { + @Override + public void keyTyped(KeyEvent e) { + if (e.getKeyChar() == '\n') { + model.setDoubleValue(getValue()); + } else { + markDirty(); + } + } + }); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + setValue(model.getDoubleValue()); + } + }); + } + + private void markDirty() { + modified = true; + setBackground(Color.pink); + repaint(); + } + + private void markClean() { + modified = false; + setBackground(Color.white); + setCaretPosition(0); + repaint(); + } + + @Override + public void setText(String text) { + markDirty(); + super.setText(text); + } + + private double getValue() throws NumberFormatException { + double val = Double.valueOf(getText()).doubleValue(); + markClean(); + return val; + } + + private void setValue(double value) { + super.setText(String.format("%6.4f", value)); + markClean(); + } +} diff --git a/src/main/java/com/jsyn/swing/EnvelopeEditorBox.java b/src/main/java/com/jsyn/swing/EnvelopeEditorBox.java new file mode 100644 index 0000000..2db4c29 --- /dev/null +++ b/src/main/java/com/jsyn/swing/EnvelopeEditorBox.java @@ -0,0 +1,573 @@ +/* + * 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.swing; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.util.ArrayList; + +import com.jsyn.data.SegmentedEnvelope; +import com.jsyn.unitgen.VariableRateDataReader; + +/** + * Edit a list of ordered duration,value pairs suitable for use with a SegmentedEnvelope. + * + * @author (C) 1997-2013 Phil Burk, SoftSynth.com + * @see EnvelopePoints + * @see SegmentedEnvelope + * @see VariableRateDataReader + */ + +/* ========================================================================== */ +public class EnvelopeEditorBox extends XYController implements MouseListener, MouseMotionListener { + EnvelopePoints points; + ArrayList<EditListener> listeners = new ArrayList<EditListener>(); + int dragIndex = -1; + double dragLowLimit; + double dragHighLimit; + double draggedPoint[]; + double xBefore; // WX value before point + double xPicked; // WX value of picked point + double dragWX; + double dragWY; + int maxPoints = Integer.MAX_VALUE; + int radius = 4; + double verticalBarSpacing = 1.0; + boolean verticalBarsEnabled = false; + double maximumXRange = Double.MAX_VALUE; + double minimumXRange = 0.1; + int rangeStart = -1; // gx coordinates + int rangeEnd = -1; + int mode = EDIT_POINTS; + public final static int EDIT_POINTS = 0; + public final static int SELECT_SUSTAIN = 1; + public final static int SELECT_RELEASE = 2; + + Color rangeColor = Color.RED; + Color sustainColor = Color.BLUE; + Color releaseColor = Color.YELLOW; + Color overlapColor = Color.GREEN; + Color firstLineColor = Color.GRAY; + + public interface EditListener { + public void objectEdited(Object editor, Object edited); + } + + public EnvelopeEditorBox() { + addMouseListener(this); + addMouseMotionListener(this); + } + + public void setMaximumXRange(double maxXRange) { + maximumXRange = maxXRange; + } + + public double getMaximumXRange() { + return maximumXRange; + } + + public void setMinimumXRange(double minXRange) { + minimumXRange = minXRange; + } + + public double getMinimumXRange() { + return minimumXRange; + } + + public void setSelection(int start, int end) { + switch (mode) { + case SELECT_SUSTAIN: + points.setSustainLoop(start, end); + break; + case SELECT_RELEASE: + points.setReleaseLoop(start, end); + break; + } + // LOGGER.debug("start = " + start + ", end = " + end ); + } + + /** Set mode to either EDIT_POINTS or SELECT_SUSTAIN, SELECT_RELEASE; */ + public void setMode(int mode) { + this.mode = mode; + } + + public int getMode() { + return mode; + } + + /** + * Add a listener to receive edit events. Listener will be passed the editor object and the + * edited object. + */ + public void addEditListener(EditListener listener) { + listeners.add(listener); + } + + public void removeEditListener(EditListener listener) { + listeners.remove(listener); + } + + /** Send event to every subscribed listener. */ + public void fireObjectEdited() { + for (EditListener listener : listeners) { + listener.objectEdited(this, points); + } + } + + public void setMaxPoints(int maxPoints) { + this.maxPoints = maxPoints; + } + + public int getMaxPoints() { + return maxPoints; + } + + public int getNumPoints() { + return points.size(); + } + + public void setPoints(EnvelopePoints points) { + this.points = points; + setMaxWorldY(points.getMaximumValue()); + } + + public EnvelopePoints getPoints() { + return points; + } + + /** + * Return index of point before this X position. + */ + private int findPointBefore(double wx) { + int pnt = -1; + double px = 0.0; + xBefore = 0.0; + for (int i = 0; i < points.size(); i++) { + px += points.getDuration(i); + if (px > wx) + break; + pnt = i; + xBefore = px; + } + return pnt; + } + + private int pickPoint(double wx, double wxAperture, double wy, double wyAperture) { + double px = 0.0; + double wxLow = wx - wxAperture; + double wxHigh = wx + wxAperture; + // LOGGER.debug("wxLow = " + wxLow + ", wxHigh = " + wxHigh ); + double wyLow = wy - wyAperture; + double wyHigh = wy + wyAperture; + // LOGGER.debug("wyLow = " + wyLow + ", wyHigh = " + wyHigh ); + double wxScale = 1.0 / wxAperture; // only divide once, then multiply + double wyScale = 1.0 / wyAperture; + int bestPoint = -1; + double bestDistance = Double.MAX_VALUE; + for (int i = 0; i < points.size(); i++) { + double dar[] = points.getPoint(i); + px += dar[0]; + double py = dar[1]; + // LOGGER.debug("px = " + px + ", py = " + py ); + if ((px > wxLow) && (px < wxHigh) && (py > wyLow) && (py < wyHigh)) { + /* Inside pick range. Calculate distance squared. */ + double ndx = (px - wx) * wxScale; + double ndy = (py - wy) * wyScale; + double dist = (ndx * ndx) + (ndy * ndy); + // LOGGER.debug("dist = " + dist ); + if (dist < bestDistance) { + bestPoint = i; + bestDistance = dist; + xPicked = px; + } + } + } + return bestPoint; + } + + private void clickDownRange(boolean shiftDown, int gx, int gy) { + setSelection(-1, -1); + rangeStart = rangeEnd = gx; + repaint(); + } + + private void dragRange(int gx, int gy) { + rangeEnd = gx; + repaint(); + } + + private void clickUpRange(int gx, int gy) { + dragRange(gx, gy); + if (rangeEnd < rangeStart) { + int temp = rangeEnd; + rangeEnd = rangeStart; + rangeStart = temp; + } + // LOGGER.debug("clickUpRange: gx = " + gx + ", rangeStart = " + + // rangeStart ); + double wx = convertGXtoWX(rangeStart); + int i0 = findPointBefore(wx); + wx = convertGXtoWX(rangeEnd); + int i1 = findPointBefore(wx); + + if (i1 == i0) { + // set single point at zero so there is nothing played for queueOn() + if (gx < 0) { + setSelection(0, 0); + } + // else clear any existing loop + } else if (i1 == (i0 + 1)) { + setSelection(i1 + 1, i1 + 1); // set to a single point + } else if (i1 > (i0 + 1)) { + setSelection(i0 + 1, i1 + 1); // set to a range of two or more + } + + rangeStart = -1; + rangeEnd = -1; + fireObjectEdited(); + } + + private void clickDownPoints(boolean shiftDown, int gx, int gy) { + dragIndex = -1; + double wx = convertGXtoWX(gx); + double wy = convertGYtoWY(gy); + // calculate world values for aperture + double wxAp = convertGXtoWX(radius + 2) - convertGXtoWX(0); + // LOGGER.debug("wxAp = " + wxAp ); + double wyAp = convertGYtoWY(0) - convertGYtoWY(radius + 2); + // LOGGER.debug("wyAp = " + wyAp ); + int pnt = pickPoint(wx, wxAp, wy, wyAp); + // LOGGER.debug("pickPoint = " + pnt); + if (shiftDown) { + if (pnt >= 0) { + points.removePoint(pnt); + repaint(); + } + } else { + if (pnt < 0) // didn't hit one so look for point to left of click + { + if (points.size() < maxPoints) // add if room + { + pnt = findPointBefore(wx); + // LOGGER.debug("pointBefore = " + pnt); + dragIndex = pnt + 1; + if (pnt == (points.size() - 1)) { + points.add(wx - xBefore, wy); + } else { + points.insert(dragIndex, wx - xBefore, wy); + } + dragLowLimit = xBefore; + dragHighLimit = wx + (maximumXRange - points.getTotalDuration()); + repaint(); + } + } else + // hit one so drag it + { + dragIndex = pnt; + if (dragIndex <= 0) + dragLowLimit = 0.0; // FIXME envelope drag limit + else + dragLowLimit = xPicked - points.getPoint(dragIndex)[0]; + dragHighLimit = xPicked + (maximumXRange - points.getTotalDuration()); + // LOGGER.debug("dragLowLimit = " + dragLowLimit ); + } + } + // Set up drag point if we are dragging. + if (dragIndex >= 0) { + draggedPoint = points.getPoint(dragIndex); + } + + } + + private void dragPoint(int gx, int gy) { + if (dragIndex < 0) + return; + + double wx = convertGXtoWX(gx); + if (wx < dragLowLimit) + wx = dragLowLimit; + else if (wx > dragHighLimit) + wx = dragHighLimit; + draggedPoint[0] = wx - dragLowLimit; // duration + + double wy = convertGYtoWY(gy); + wy = clipWorldY(wy); + draggedPoint[1] = wy; + dragWY = wy; + dragWX = wx; + points.setDirty(true); + repaint(); + } + + private void clickUpPoints(int gx, int gy) { + dragPoint(gx, gy); + fireObjectEdited(); + dragIndex = -1; + } + + // Implement the MouseMotionListener interface for AWT 1.1 + @Override + public void mouseDragged(MouseEvent e) { + int x = e.getX(); + int y = e.getY(); + if (points == null) + return; + if (mode == EDIT_POINTS) { + dragPoint(x, y); + } else { + dragRange(x, y); + } + } + + @Override + public void mouseMoved(MouseEvent e) { + } + + // Implement the MouseListener interface for AWT 1.1 + @Override + public void mousePressed(MouseEvent e) { + int x = e.getX(); + int y = e.getY(); + if (points == null) + return; + if (mode == EDIT_POINTS) { + clickDownPoints(e.isShiftDown(), x, y); + } else { + clickDownRange(e.isShiftDown(), x, y); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + } + + @Override + public void mouseReleased(MouseEvent e) { + int x = e.getX(); + int y = e.getY(); + if (points == null) + return; + if (mode == EDIT_POINTS) { + clickUpPoints(x, y); + } else { + clickUpRange(x, y); + } + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } + + /** + * Draw selected range. + */ + private void drawRange(Graphics g) { + if (rangeStart >= 0) { + int height = getHeight(); + int gx0 = 0, gx1 = 0; + + if (rangeEnd < rangeStart) { + gx0 = rangeEnd; + gx1 = rangeStart; + } else { + gx0 = rangeStart; + gx1 = rangeEnd; + } + g.setColor(rangeColor); + g.fillRect(gx0, 0, gx1 - gx0, height); + } + } + + private void drawUnderSelection(Graphics g, int start, int end) { + if (start >= 0) { + int height = getHeight(); + int gx0 = 0, gx1 = radius; + double wx = 0.0; + for (int i = 0; i <= (end - 1); i++) { + double dar[] = (double[]) points.elementAt(i); + wx += dar[0]; + if (start == (i + 1)) { + gx0 = convertWXtoGX(wx) + radius; + } + if (end == (i + 1)) { + gx1 = convertWXtoGX(wx) + radius; + } + } + if (gx0 == gx1) + gx0 = gx0 - radius; + g.fillRect(gx0, 0, gx1 - gx0, height); + } + } + + private void drawSelections(Graphics g) { + int sus0 = points.getSustainBegin(); + int sus1 = points.getSustainEnd(); + int rel0 = points.getReleaseBegin(); + int rel1 = points.getReleaseEnd(); + + g.setColor(sustainColor); + drawUnderSelection(g, sus0, sus1); + g.setColor(releaseColor); + drawUnderSelection(g, rel0, rel1); + // draw overlapping sustain and release region + if (sus1 >= rel0) { + int sel1 = (rel1 < sus1) ? rel1 : sus1; + g.setColor(overlapColor); + drawUnderSelection(g, rel0, sel1); + } + } + + /** + * Override this to draw a grid or other stuff under the envelope. + */ + public void drawUnderlay(Graphics g) { + if (dragIndex < 0) { + drawSelections(g); + drawRange(g); + } + if (verticalBarsEnabled) + drawVerticalBars(g); + } + + public void setVerticalBarsEnabled(boolean flag) { + verticalBarsEnabled = flag; + } + + public boolean areVerticalBarsEnabled() { + return verticalBarsEnabled; + } + + /** + * Set spacing in world coordinates. + */ + public void setVerticalBarSpacing(double spacing) { + verticalBarSpacing = spacing; + } + + public double getVerticalBarSpacing() { + return verticalBarSpacing; + } + + /** + * Draw vertical lines. + */ + private void drawVerticalBars(Graphics g) { + int width = getWidth(); + int height = getHeight(); + double wx = verticalBarSpacing; + int gx; + + // g.setColor( getBackground().darker() ); + g.setColor(Color.lightGray); + while (true) { + gx = convertWXtoGX(wx); + if (gx > width) + break; + g.drawLine(gx, 0, gx, height); + wx += verticalBarSpacing; + } + } + + public void drawPoints(Graphics g, Color lineColor) { + double wx = 0.0; + int gx1 = 0; + int gy1 = getHeight(); + for (int i = 0; i < points.size(); i++) { + double dar[] = (double[]) points.elementAt(i); + wx += dar[0]; + double wy = dar[1]; + int gx2 = convertWXtoGX(wx); + int gy2 = convertWYtoGY(wy); + if (i == 0) { + g.setColor(isEnabled() ? firstLineColor : firstLineColor.darker()); + g.drawLine(gx1, gy1, gx2, gy2); + g.setColor(isEnabled() ? lineColor : lineColor.darker()); + } else if (i > 0) { + g.drawLine(gx1, gy1, gx2, gy2); + } + int diameter = (2 * radius) + 1; + g.fillOval(gx2 - radius, gy2 - radius, diameter, diameter); + gx1 = gx2; + gy1 = gy2; + } + } + + public void drawAllPoints(Graphics g) { + drawPoints(g, getForeground()); + } + + /* Override default paint action. */ + @Override + public void paint(Graphics g) { + double wx = 0.0; + int width = getWidth(); + int height = getHeight(); + + // draw background and erase all values + g.setColor(isEnabled() ? getBackground() : getBackground().darker()); + g.fillRect(0, 0, width, height); + + if (points == null) { + g.setColor(getForeground()); + g.drawString("No EnvelopePoints", 10, 30); + return; + } + + // Determine total duration. + if (points.size() > 0) { + wx = points.getTotalDuration(); + // Adjust max X so that we see entire circle of last point. + double radiusWX = this.convertGXtoWX(radius) - this.getMinWorldX(); + double wxFar = wx + radiusWX; + if (wxFar > getMaxWorldX()) { + if (wx > maximumXRange) + wxFar = maximumXRange; + setMaxWorldX(wxFar); + } else if (wx < (getMaxWorldX() * 0.7)) { + double newMax = wx / 0.7001; // make slightly larger to prevent + // endless jitter, FIXME - still + // needed after repaint() + // removed from setMaxWorldX? + // LOGGER.debug("newMax = " + newMax ); + if (newMax < minimumXRange) + newMax = minimumXRange; + setMaxWorldX(newMax); + } + } + // LOGGER.debug("total X = " + wx ); + + drawUnderlay(g); + + drawAllPoints(g); + + /* Show X,Y,TotalX as text. */ + g.drawString(points.getName() + ", len=" + String.format("%7.3f", wx), 5, 15); + if ((draggedPoint != null) && (dragIndex >= 0)) { + String s = "i=" + dragIndex + ", dur=" + + String.format("%7.3f", draggedPoint[0]) + ", y = " + + String.format("%8.4f", draggedPoint[1]); + g.drawString(s, 5, 30); + } + } +} diff --git a/src/main/java/com/jsyn/swing/EnvelopeEditorPanel.java b/src/main/java/com/jsyn/swing/EnvelopeEditorPanel.java new file mode 100644 index 0000000..dc9f2cd --- /dev/null +++ b/src/main/java/com/jsyn/swing/EnvelopeEditorPanel.java @@ -0,0 +1,164 @@ +/* + * 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.swing; + +import java.awt.BorderLayout; +import java.awt.Button; +import java.awt.Checkbox; +import java.awt.CheckboxGroup; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Label; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; + +import javax.swing.JPanel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +public class EnvelopeEditorPanel extends JPanel { + EnvelopeEditorBox editor; + Checkbox pointsBox; + Checkbox sustainBox; + Checkbox releaseBox; + Checkbox autoBox; + Button onButton; + Button offButton; + Button clearButton; + Button yUpButton; + Button yDownButton; + DoubleBoundedTextField zoomField; + + public EnvelopeEditorPanel(EnvelopePoints points, int maxFrames) { + setSize(600, 300); + + setLayout(new BorderLayout()); + editor = new EnvelopeEditorBox(); + editor.setMaxPoints(maxFrames); + editor.setBackground(Color.cyan); + editor.setPoints(points); + editor.setMinimumSize(new Dimension(500, 300)); + + add(editor, "Center"); + + JPanel buttonPanel = new JPanel(); + add(buttonPanel, "South"); + + CheckboxGroup cbg = new CheckboxGroup(); + pointsBox = new Checkbox("points", cbg, true); + pointsBox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + editor.setMode(EnvelopeEditorBox.EDIT_POINTS); + } + }); + buttonPanel.add(pointsBox); + + sustainBox = new Checkbox("onLoop", cbg, false); + sustainBox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + editor.setMode(EnvelopeEditorBox.SELECT_SUSTAIN); + } + }); + buttonPanel.add(sustainBox); + + releaseBox = new Checkbox("offLoop", cbg, false); + releaseBox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + editor.setMode(EnvelopeEditorBox.SELECT_RELEASE); + } + }); + buttonPanel.add(releaseBox); + + autoBox = new Checkbox("AutoStop", false); + /* + * buttonPanel.add( onButton = new Button( "On" ) ); onButton.addActionListener( module ); + * buttonPanel.add( offButton = new Button( "Off" ) ); offButton.addActionListener( module + * ); buttonPanel.add( clearButton = new Button( "Clear" ) ); clearButton.addActionListener( + * module ); + */ + buttonPanel.add(yUpButton = new Button("Y*2")); + yUpButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + scaleEnvelopeValues(2.0); + } + }); + + buttonPanel.add(yDownButton = new Button("Y/2")); + yDownButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + scaleEnvelopeValues(0.5); + } + }); + + /* Add a TextField for setting the Y scale. */ + double max = getMaxEnvelopeValue(editor.getPoints()); + editor.setMaxWorldY(max); + buttonPanel.add(new Label("YMax =")); + final DoubleBoundedRangeModel model = new DoubleBoundedRangeModel("YMax", 100000, 1.0, + 100001.0, 1.0); + buttonPanel.add(zoomField = new DoubleBoundedTextField(model, 8)); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + try { + double val = model.getDoubleValue(); + editor.setMaxWorldY(val); + editor.repaint(); + } catch (NumberFormatException exp) { + zoomField.setText("ERROR"); + zoomField.selectAll(); + } + } + }); + + validate(); + } + + /** + * Multiply all the values in the envelope by scalar. + */ + double getMaxEnvelopeValue(EnvelopePoints points) { + double max = 1.0; + for (int i = 0; i < points.size(); i++) { + double value = points.getValue(i); + if (value > max) { + max = value; + } + } + return max; + } + + /** + * Multiply all the values in the envelope by scalar. + */ + void scaleEnvelopeValues(double scalar) { + EnvelopePoints points = editor.getPoints(); + for (int i = 0; i < points.size(); i++) { + double[] dar = points.getPoint(i); + dar[1] = dar[1] * scalar; // scale value + } + points.setDirty(true); + editor.repaint(); + } +} diff --git a/src/main/java/com/jsyn/swing/EnvelopePoints.java b/src/main/java/com/jsyn/swing/EnvelopePoints.java new file mode 100644 index 0000000..ab4ed03 --- /dev/null +++ b/src/main/java/com/jsyn/swing/EnvelopePoints.java @@ -0,0 +1,234 @@ +/* + * 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.swing; + +import java.util.Vector; + +import com.jsyn.data.SegmentedEnvelope; + +/** + * Vector that contains duration,value pairs. Used by EnvelopeEditor + * + * @author (C) 1997 Phil Burk, SoftSynth.com + */ + +/* ========================================================================== */ +public class EnvelopePoints extends Vector { + private String name = ""; + private double maximumValue = 1.0; + private int sustainBegin = -1; + private int sustainEnd = -1; + private int releaseBegin = -1; + private int releaseEnd = -1; + private boolean dirty = false; + + /** + * Update only if points or loops were modified. + */ + public void updateEnvelopeIfDirty(SegmentedEnvelope envelope) { + if (dirty) { + updateEnvelope(envelope); + } + } + + /** + * The editor works on a vector of points, not a real envelope. The data must be written to a + * real SynthEnvelope in order to use it. + */ + public void updateEnvelope(SegmentedEnvelope envelope) { + int numFrames = size(); + for (int i = 0; i < numFrames; i++) { + envelope.write(i, getPoint(i), 0, 1); + } + envelope.setSustainBegin(getSustainBegin()); + envelope.setSustainEnd(getSustainEnd()); + envelope.setReleaseBegin(getReleaseBegin()); + envelope.setReleaseEnd(getReleaseEnd()); + envelope.setNumFrames(numFrames); + dirty = false; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setMaximumValue(double maximumValue) { + this.maximumValue = maximumValue; + } + + public double getMaximumValue() { + return maximumValue; + } + + public void add(double dur, double value) { + double dar[] = { + dur, value + }; + addElement(dar); + dirty = true; + } + + /** + * Insert point without changing total duration by reducing next points duration. + */ + public void insert(int index, double dur, double y) { + double dar[] = { + dur, y + }; + if (index < size()) { + ((double[]) elementAt(index))[0] -= dur; + } + insertElementAt(dar, index); + + if (index <= sustainBegin) + sustainBegin += 1; + if (index <= sustainEnd) + sustainEnd += 1; + if (index <= releaseBegin) + releaseBegin += 1; + if (index <= releaseEnd) + releaseEnd += 1; + dirty = true; + } + + /** + * Remove indexed point and update sustain and release loops if necessary. Did not name this + * "remove()" because of conflicts with new JDK 1.3 method with the same name. + */ + public void removePoint(int index) { + super.removeElementAt(index); + // move down loop if points below or inside loop removed + if (index < sustainBegin) + sustainBegin -= 1; + if (index <= sustainEnd) + sustainEnd -= 1; + if (index < releaseBegin) + releaseBegin -= 1; + if (index <= releaseEnd) + releaseEnd -= 1; + + // was entire loop removed? + if (sustainBegin > sustainEnd) { + sustainBegin = -1; + sustainEnd = -1; + } + // was entire loop removed? + if (releaseBegin > releaseEnd) { + releaseBegin = -1; + releaseEnd = -1; + } + dirty = true; + } + + public double getDuration(int index) { + return ((double[]) elementAt(index))[0]; + } + + public double getValue(int index) { + return ((double[]) elementAt(index))[1]; + } + + public double[] getPoint(int index) { + return (double[]) elementAt(index); + } + + public double getTotalDuration() { + double sum = 0.0; + for (int i = 0; i < size(); i++) { + double dar[] = (double[]) elementAt(i); + sum += dar[0]; + } + return sum; + } + + /** + * Set location of Sustain Loop in units of Frames. Set SustainBegin to -1 if no Sustain Loop. + * SustainEnd value is the frame index of the frame just past the end of the loop. The number of + * frames included in the loop is (SustainEnd - SustainBegin). + */ + public void setSustainLoop(int startFrame, int endFrame) { + this.sustainBegin = startFrame; + this.sustainEnd = endFrame; + dirty = true; + } + + /*** + * @return Beginning of sustain loop or -1 if no loop. + */ + public int getSustainBegin() { + return this.sustainBegin; + } + + /*** + * @return End of sustain loop or -1 if no loop. + */ + public int getSustainEnd() { + return this.sustainEnd; + } + + /*** + * @return Size of sustain loop in frames, 0 if no loop. + */ + public int getSustainSize() { + return (this.sustainEnd - this.sustainBegin); + } + + /** + * Set location of Release Loop in units of Frames. Set ReleaseBegin to -1 if no ReleaseLoop. + * ReleaseEnd value is the frame index of the frame just past the end of the loop. The number of + * frames included in the loop is (ReleaseEnd - ReleaseBegin). + */ + public void setReleaseLoop(int startFrame, int endFrame) { + this.releaseBegin = startFrame; + this.releaseEnd = endFrame; + dirty = true; + } + + /*** + * @return Beginning of release loop or -1 if no loop. + */ + public int getReleaseBegin() { + return this.releaseBegin; + } + + /*** + * @return End of release loop or -1 if no loop. + */ + public int getReleaseEnd() { + return this.releaseEnd; + } + + /*** + * @return Size of release loop in frames, 0 if no loop. + */ + public int getReleaseSize() { + return (this.releaseEnd - this.releaseBegin); + } + + public boolean isDirty() { + return dirty; + } + + public void setDirty(boolean b) { + dirty = b; + } + +} diff --git a/src/main/java/com/jsyn/swing/ExponentialRangeModel.java b/src/main/java/com/jsyn/swing/ExponentialRangeModel.java new file mode 100644 index 0000000..c807000 --- /dev/null +++ b/src/main/java/com/jsyn/swing/ExponentialRangeModel.java @@ -0,0 +1,110 @@ +/* + * 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.swing; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Maps integer range info to a double value along an exponential scale. + * + * <pre> + * + * x = ival / resolution + * f(x) = a*(rootˆcx) + b + * f(0.0) = dmin + * f(1.0) = dmax + * b = dmin - a + * a = (dmax - dmin) / (rootˆc - 1) + * + * Inverse function: + * x = log( (y-b)/a ) / log(root) + * + * </pre> + * + * @author Phil Burk, (C) 2011 Mobileer Inc + */ +public class ExponentialRangeModel extends DoubleBoundedRangeModel { + + private static final Logger LOGGER = LoggerFactory.getLogger(ExponentialRangeModel.class); + private static final long serialVersionUID = -142785624892302160L; + + double a = 1.0; + double b = -1.0; + double span = 1.0; + double root = 10.0; + + /** Use default root of 10.0 and span of 1.0. */ + public ExponentialRangeModel(String name, int resolution, double dmin, double dmax, double dval) { + this(name, resolution, dmin, dmax, dval, 1.0); + } + + /** Set span before setting double value so it is translated correctly. */ + ExponentialRangeModel(String name, int resolution, double dmin, double dmax, double dval, + double span) { + super(name, resolution, dmin, dmax, dval); + setRoot(10.0); + setSpan(span); + /* Set again after coefficients setup. */ + setDoubleValue(dval); + } + + private void updateCoefficients() { + a = (getDoubleMaximum() - getDoubleMinimum()) / (Math.pow(root, span) - 1.0); + b = getDoubleMinimum() - a; + } + + private void setRoot(double w) { + root = w; + updateCoefficients(); + } + + public double getRoot() { + return root; + } + + public void setSpan(double c) { + this.span = c; + updateCoefficients(); + } + + public double getSpan() { + return span; + } + + @Override + public double sliderToDouble(int sliderValue) { + updateCoefficients(); // TODO optimize when we call this + double x = (double) sliderValue / getMaximum(); + return (a * Math.pow(root, span * x)) + b; + } + + @Override + public int doubleToSlider(double dval) { + updateCoefficients(); // TODO optimize when we call this + double z = (dval - b) / a; + double x = Math.log(z) / (span * Math.log(root)); + return (int) Math.round(x * getMaximum()); + } + + public void test(int sliderValue) { + double dval = sliderToDouble(sliderValue); + int ival = doubleToSlider(dval); + LOGGER.debug(sliderValue + " => " + dval + " => " + ival); + } + +} diff --git a/src/main/java/com/jsyn/swing/InstrumentBrowser.java b/src/main/java/com/jsyn/swing/InstrumentBrowser.java new file mode 100644 index 0000000..8e74660 --- /dev/null +++ b/src/main/java/com/jsyn/swing/InstrumentBrowser.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012 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.swing; + +import java.awt.Dimension; +import java.awt.GridLayout; +import java.util.ArrayList; + +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import com.jsyn.util.InstrumentLibrary; +import com.jsyn.util.VoiceDescription; + +/** + * Display a list of VoiceDescriptions and their associated presets. Notify PresetSelectionListeners + * when a preset is selected. + * + * @author Phil Burk (C) 2012 Mobileer Inc + */ +@SuppressWarnings("serial") +public class InstrumentBrowser extends JPanel { + private InstrumentLibrary library; + private JScrollPane listScroller2; + private VoiceDescription voiceDescription; + private ArrayList<PresetSelectionListener> listeners = new ArrayList<>(); + + public InstrumentBrowser(InstrumentLibrary library) { + this.library = library; + JPanel horizontalPanel = new JPanel(); + horizontalPanel.setLayout(new GridLayout(1, 2)); + + final JList<VoiceDescription> instrumentList = new JList<VoiceDescription>(library.getVoiceDescriptions()); + setupList(instrumentList); + instrumentList.addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + if (!e.getValueIsAdjusting()) { + int n = instrumentList.getSelectedIndex(); + if (n >= 0) { + showPresetList(n); + } + } + } + }); + + JScrollPane listScroller1 = new JScrollPane(instrumentList); + listScroller1.setPreferredSize(new Dimension(250, 120)); + add(listScroller1); + + instrumentList.setSelectedIndex(0); + } + + public void addPresetSelectionListener(PresetSelectionListener listener) { + listeners.add(listener); + } + + public void removePresetSelectionListener(PresetSelectionListener listener) { + listeners.remove(listener); + } + + private void firePresetSelectionListeners(VoiceDescription voiceDescription, int presetIndex) { + for (PresetSelectionListener listener : listeners) { + listener.presetSelected(voiceDescription, presetIndex); + } + } + + private void showPresetList(int n) { + if (listScroller2 != null) { + remove(listScroller2); + } + voiceDescription = library.getVoiceDescriptions()[n]; + final JList<String> presetList = new JList<String>(voiceDescription.getPresetNames()); + setupList(presetList); + presetList.addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getValueIsAdjusting() == false) { + int n = presetList.getSelectedIndex(); + if (n >= 0) { + firePresetSelectionListeners(voiceDescription, n); + } + } + } + }); + + listScroller2 = new JScrollPane(presetList); + listScroller2.setPreferredSize(new Dimension(250, 120)); + add(listScroller2); + presetList.setSelectedIndex(0); + validate(); + } + + private void setupList(@SuppressWarnings("rawtypes") JList list) { + list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); + list.setLayoutOrientation(JList.VERTICAL); + list.setVisibleRowCount(-1); + } +} diff --git a/src/main/java/com/jsyn/swing/JAppletFrame.java b/src/main/java/com/jsyn/swing/JAppletFrame.java new file mode 100644 index 0000000..53bd65b --- /dev/null +++ b/src/main/java/com/jsyn/swing/JAppletFrame.java @@ -0,0 +1,65 @@ +/* + * 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.swing; + +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.JApplet; +import javax.swing.JFrame; + +/** + * Frame that allows a program to be run as either an Application or an Applet. Used by JSyn example + * programs. + * + * @author (C) 1997 Phil Burk, SoftSynth.com + */ + +public class JAppletFrame extends JFrame { + private static final long serialVersionUID = -6047247494856379114L; + JApplet applet; + + public JAppletFrame(String frameTitle, final JApplet pApplet) { + super(frameTitle); + this.applet = pApplet; + getContentPane().add(applet); + repaint(); + + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + applet.stop(); + applet.destroy(); + try { + System.exit(0); + } catch (SecurityException exc) { + System.err.println("System.exit(0) not allowed by Java VM."); + } + } + + @Override + public void windowClosed(WindowEvent e) { + } + }); + } + + public void test() { + applet.init(); + applet.start(); + } + +} diff --git a/src/main/java/com/jsyn/swing/PortBoundedRangeModel.java b/src/main/java/com/jsyn/swing/PortBoundedRangeModel.java new file mode 100644 index 0000000..a5cf841 --- /dev/null +++ b/src/main/java/com/jsyn/swing/PortBoundedRangeModel.java @@ -0,0 +1,45 @@ +/* + * 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.swing; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.jsyn.ports.UnitInputPort; + +/** + * A bounded range model that drives a UnitInputPort. The range of the model is set based on the min + * and max of the port. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public class PortBoundedRangeModel extends DoubleBoundedRangeModel { + private static final long serialVersionUID = -8011867146560305808L; + private UnitInputPort port; + + public PortBoundedRangeModel(UnitInputPort pPort) { + super(pPort.getName(), 10000, pPort.getMinimum(), pPort.getMaximum(), pPort.getValue()); + this.port = pPort; + addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + port.set(getDoubleValue()); + } + }); + } + +} diff --git a/src/main/java/com/jsyn/swing/PortControllerFactory.java b/src/main/java/com/jsyn/swing/PortControllerFactory.java new file mode 100644 index 0000000..a73d047 --- /dev/null +++ b/src/main/java/com/jsyn/swing/PortControllerFactory.java @@ -0,0 +1,60 @@ +/* + * 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.swing; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.jsyn.ports.UnitInputPort; + +/** + * Factory class for making various controllers for JSyn ports. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class PortControllerFactory { + private static final int RESOLUTION = 100000; + + public static DoubleBoundedRangeSlider createPortSlider(final UnitInputPort port) { + DoubleBoundedRangeModel rangeModel = new DoubleBoundedRangeModel(port.getName(), + RESOLUTION, port.getMinimum(), port.getMaximum(), port.get()); + rangeModel.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + DoubleBoundedRangeModel model = (DoubleBoundedRangeModel) e.getSource(); + double value = model.getDoubleValue(); + port.set(value); + } + }); + return new DoubleBoundedRangeSlider(rangeModel, 4); + } + + public static DoubleBoundedRangeSlider createExponentialPortSlider(final UnitInputPort port) { + ExponentialRangeModel rangeModel = new ExponentialRangeModel(port.getName(), RESOLUTION, + port.getMinimum(), port.getMaximum(), port.get()); + rangeModel.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + ExponentialRangeModel model = (ExponentialRangeModel) e.getSource(); + double value = model.getDoubleValue(); + port.set(value); + } + }); + return new DoubleBoundedRangeSlider(rangeModel, 4); + } + +} diff --git a/src/main/java/com/jsyn/swing/PortModelFactory.java b/src/main/java/com/jsyn/swing/PortModelFactory.java new file mode 100644 index 0000000..8bec76a --- /dev/null +++ b/src/main/java/com/jsyn/swing/PortModelFactory.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.swing; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.jsyn.ports.UnitInputPort; + +public class PortModelFactory { + private static final int RESOLUTION = 1000000; + + public static DoubleBoundedRangeModel createLinearModel(final UnitInputPort pPort) { + final DoubleBoundedRangeModel model = new DoubleBoundedRangeModel(pPort.getName(), + RESOLUTION, pPort.getMinimum(), pPort.getMaximum(), pPort.get()); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + pPort.set(model.getDoubleValue()); + } + }); + return model; + } + + public static ExponentialRangeModel createExponentialModel(final UnitInputPort pPort) { + final ExponentialRangeModel model = new ExponentialRangeModel(pPort.getName(), RESOLUTION, + pPort.getMinimum(), pPort.getMaximum(), pPort.get()); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + pPort.set(model.getDoubleValue()); + } + }); + return model; + } + + public static ExponentialRangeModel createExponentialModel(final int partNum, + final UnitInputPort pPort) { + final ExponentialRangeModel model = new ExponentialRangeModel(pPort.getName(), RESOLUTION, + pPort.getMinimum(), pPort.getMaximum(), pPort.get()); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + pPort.set(partNum, model.getDoubleValue()); + } + }); + return model; + } + +} diff --git a/src/main/java/com/jsyn/swing/PresetSelectionListener.java b/src/main/java/com/jsyn/swing/PresetSelectionListener.java new file mode 100644 index 0000000..daf0310 --- /dev/null +++ b/src/main/java/com/jsyn/swing/PresetSelectionListener.java @@ -0,0 +1,23 @@ +/* + * 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.swing; + +import com.jsyn.util.VoiceDescription; + +public interface PresetSelectionListener { + public void presetSelected(VoiceDescription voiceDescription, int presetIndex); +} diff --git a/src/main/java/com/jsyn/swing/RotaryController.java b/src/main/java/com/jsyn/swing/RotaryController.java new file mode 100644 index 0000000..c26c37f --- /dev/null +++ b/src/main/java/com/jsyn/swing/RotaryController.java @@ -0,0 +1,335 @@ +/* + * 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.swing; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; + +import javax.swing.BoundedRangeModel; +import javax.swing.JPanel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * Rotary controller looks like a knob on a synthesizer. You control this knob by clicking on it and + * dragging <b>up</b> or <b>down</b>. If you move the mouse to the <b>left</b> of the knob then you + * will have <b>coarse</b> control. If you move the mouse to the <b>right</b> of the knob then you + * will have <b>fine</b> control. + * <P> + * + * @author (C) 2010 Phil Burk, Mobileer Inc + * @version 16.1 + */ +public class RotaryController extends JPanel { + private static final long serialVersionUID = 6681532871556659546L; + private static final double SENSITIVITY = 0.01; + private final BoundedRangeModel model; + + private final double minAngle = 1.4 * Math.PI; + private final double maxAngle = -0.4 * Math.PI; + private final double unitIncrement = 0.01; + private int lastY; + private int startX; + private Color knobColor = Color.LIGHT_GRAY; + private Color lineColor = Color.RED; + private double baseValue; + + public enum Style { + LINE, LINEDOT, ARROW, ARC + }; + + private Style style = Style.ARC; + + public RotaryController(BoundedRangeModel model) { + this.model = model; + setMinimumSize(new Dimension(50, 50)); + setPreferredSize(new Dimension(50, 50)); + addMouseListener(new MouseHandler()); + addMouseMotionListener(new MouseMotionHandler()); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + safeRepaint(); + } + + }); + } + + // This can be overridden in subclasses to workaround OpenJDK bugs. + public void safeRepaint() { + repaint(); + } + + public BoundedRangeModel getModel() { + return model; + } + + private class MouseHandler extends MouseAdapter { + + @Override + public void mousePressed(MouseEvent e) { + lastY = e.getY(); + startX = e.getX(); + } + + @Override + public void mouseReleased(MouseEvent e) { + if (isEnabled()) { + setKnobByXY(e.getX(), e.getY()); + } + } + } + + private class MouseMotionHandler extends MouseMotionAdapter { + @Override + public void mouseDragged(MouseEvent e) { + if (isEnabled()) { + setKnobByXY(e.getX(), e.getY()); + } + } + } + + private int getModelRange() { + return (((model.getMaximum() - model.getExtent()) - model.getMinimum())); + } + + /** + * A fractional value is useful for drawing. + * + * @return model value as a normalized fraction between 0.0 and 1.0 + */ + public double getFractionFromModel() { + double value = model.getValue(); + return convertValueToFraction(value); + } + + private double convertValueToFraction(double value) { + return (value - model.getMinimum()) / getModelRange(); + } + + private void setKnobByXY(int x, int y) { + // Scale increment by X position. + int xdiff = startX - x; // More to left causes bigger increments. + double power = xdiff * SENSITIVITY; + double perPixel = unitIncrement * Math.pow(2.0, power); + + int ydiff = lastY - y; + double fractionalDelta = ydiff * perPixel; + // Only update the model if we actually change values. + // This is needed in case the range is small. + int valueDelta = (int) Math.round(fractionalDelta * getModelRange()); + if (valueDelta != 0) { + model.setValue(model.getValue() + valueDelta); + lastY = y; + } + } + + private double fractionToAngle(double fraction) { + return (fraction * (maxAngle - minAngle)) + minAngle; + } + + private void drawLineIndicator(Graphics g, int x, int y, int radius, double angle, + boolean drawDot) { + double arrowSize = radius * 0.95; + int arrowX = (int) (arrowSize * Math.sin(angle)); + int arrowY = (int) (arrowSize * Math.cos(angle)); + g.setColor(lineColor); + g.drawLine(x, y, x + arrowX, y - arrowY); + if (drawDot) { + // draw little dot at end + double dotScale = 0.1; + int dotRadius = (int) (dotScale * arrowSize); + if (dotRadius > 1) { + int dotX = x + (int) ((0.99 - dotScale) * arrowX) - dotRadius; + int dotY = y - (int) ((0.99 - dotScale) * arrowY) - dotRadius; + g.fillOval(dotX, dotY, dotRadius * 2, dotRadius * 2); + } + } + } + + private void drawArrowIndicator(Graphics g, int x0, int y0, int radius, double angle) { + int arrowSize = (int) (radius * 0.95); + int arrowWidth = (int) (radius * 0.2); + int xp[] = { + 0, arrowWidth, 0, -arrowWidth + }; + int yp[] = { + arrowSize, -arrowSize / 2, 0, -arrowSize / 2 + }; + double sa = Math.sin(angle); + double ca = Math.cos(angle); + for (int i = 0; i < xp.length; i++) { + int x = xp[i]; + int y = yp[i]; + xp[i] = x0 - (int) ((x * ca) - (y * sa)); + yp[i] = y0 - (int) ((x * sa) + (y * ca)); + } + g.fillPolygon(xp, yp, xp.length); + } + + private void drawArcIndicator(Graphics g, int x, int y, int radius, double angle) { + final double DEGREES_PER_RADIAN = 180.0 / Math.PI; + final int minAngleDegrees = (int) (minAngle * DEGREES_PER_RADIAN); + final int maxAngleDegrees = (int) (maxAngle * DEGREES_PER_RADIAN); + + int zeroAngleDegrees = (int) (fractionToAngle(baseValue) * DEGREES_PER_RADIAN); + + double arrowSize = radius * 0.95; + int arcX = x - radius; + int arcY = y - radius; + int arcAngle = (int) (angle * DEGREES_PER_RADIAN); + int arrowX = (int) (arrowSize * Math.cos(angle)); + int arrowY = (int) (arrowSize * Math.sin(angle)); + + g.setColor(knobColor.darker().darker()); + g.fillArc(arcX, arcY, 2 * radius, 2 * radius, minAngleDegrees, maxAngleDegrees + - minAngleDegrees); + g.setColor(Color.ORANGE); + g.fillArc(arcX, arcY, 2 * radius, 2 * radius, zeroAngleDegrees, arcAngle - zeroAngleDegrees); + + // fill in middle + int arcWidth = radius / 4; + int diameter = ((radius - arcWidth) * 2); + g.setColor(knobColor); + g.fillOval(arcWidth + x - radius, arcWidth + y - radius, diameter, diameter); + + g.setColor(lineColor); + g.drawLine(x, y, x + arrowX, y - arrowY); + + } + + /** + * Override this method if you want to draw your own line or dot on the knob. + */ + public void drawIndicator(Graphics g, int x, int y, int radius, double angle) { + g.setColor(isEnabled() ? lineColor : lineColor.darker()); + switch (style) { + case LINE: + drawLineIndicator(g, x, y, radius, angle, false); + break; + case LINEDOT: + drawLineIndicator(g, x, y, radius, angle, true); + break; + case ARROW: + drawArrowIndicator(g, x, y, radius, angle); + break; + case ARC: + drawArcIndicator(g, x, y, radius, angle); + break; + } + } + + /** + * Override this method if you want to draw your own knob. + * + * @param g graphics context + * @param x position of center of knob + * @param y position of center of knob + * @param radius of knob in pixels + * @param angle in radians. Zero is straight up. + */ + public void drawKnob(Graphics g, int x, int y, int radius, double angle) { + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + int diameter = radius * 2; + // Draw shaded side. + g.setColor(knobColor.darker()); + g.fillOval(x - radius + 2, y - radius + 2, diameter, diameter); + g.setColor(knobColor); + g.fillOval(x - radius, y - radius, diameter, diameter); + + // Draw line or other indicator of knob position. + drawIndicator(g, x, y, radius, angle); + } + + // Draw the round knob based on the current size and model value. + // This used to have a bug where the scope would draw in this components background. + // Then I changed it from overriding paint() to overriding paintComponent() and it worked. + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + + int width = getWidth(); + int height = getHeight(); + int x = width / 2; + int y = height / 2; + + // Calculate radius from size of component. + int diameter = (width < height) ? width : height; + diameter -= 4; + int radius = diameter / 2; + + double angle = fractionToAngle(getFractionFromModel()); + drawKnob(g, x, y, radius, angle); + } + + public Color getKnobColor() { + return knobColor; + } + + /** + * @param knobColor color of body of knob + */ + public void setKnobColor(Color knobColor) { + this.knobColor = knobColor; + } + + public Color getLineColor() { + return lineColor; + } + + /** + * @param lineColor color of indicator on knob like a line or arrow + */ + public void setLineColor(Color lineColor) { + this.lineColor = lineColor; + } + + public void setStyle(Style style) { + this.style = style; + } + + public Style getStyle() { + return style; + } + + public double getBaseValue() { + return baseValue; + } + + /* + * Specify where the orange arc originates. For example a pan knob with a centered arc would + * have a baseValue of 0.5. + * @param baseValue a fraction between 0.0 and 1.0. + */ + public void setBaseValue(double baseValue) { + if (baseValue < 0.0) { + baseValue = 0.0; + } else if (baseValue > 1.0) { + baseValue = 1.0; + } + this.baseValue = baseValue; + } + +} diff --git a/src/main/java/com/jsyn/swing/RotaryTextController.java b/src/main/java/com/jsyn/swing/RotaryTextController.java new file mode 100644 index 0000000..81d6614 --- /dev/null +++ b/src/main/java/com/jsyn/swing/RotaryTextController.java @@ -0,0 +1,53 @@ +/* + * 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.swing; + +import java.awt.BorderLayout; + +import javax.swing.BorderFactory; +import javax.swing.JPanel; + +/** + * Combine a RotaryController and a DoubleBoundedTextField into a convenient package. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class RotaryTextController extends JPanel { + private static final long serialVersionUID = -2931828326251895375L; + private RotaryController rotary; + private DoubleBoundedTextField textField; + + public RotaryTextController(DoubleBoundedRangeModel pModel, int numDigits) { + rotary = new RotaryController(pModel); + textField = new DoubleBoundedTextField(pModel, numDigits); + setLayout(new BorderLayout()); + add(rotary, BorderLayout.CENTER); + add(textField, BorderLayout.SOUTH); + } + + /** Display the title in a border. */ + public void setTitle(String label) { + setBorder(BorderFactory.createTitledBorder(label)); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + rotary.setEnabled(enabled); + textField.setEnabled(enabled); + } +} diff --git a/src/main/java/com/jsyn/swing/SoundTweaker.java b/src/main/java/com/jsyn/swing/SoundTweaker.java new file mode 100644 index 0000000..dc48c8f --- /dev/null +++ b/src/main/java/com/jsyn/swing/SoundTweaker.java @@ -0,0 +1,120 @@ +/* + * 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.swing; + +import java.awt.Component; +import java.awt.GridLayout; +import java.util.ArrayList; +import java.util.logging.Logger; + +import javax.swing.JLabel; +import javax.swing.JPanel; + +import com.jsyn.Synthesizer; +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitPort; +import com.jsyn.unitgen.UnitGenerator; +import com.jsyn.unitgen.UnitSource; +import com.jsyn.unitgen.UnitVoice; +import com.jsyn.util.Instrument; +import com.softsynth.math.AudioMath; + +@SuppressWarnings("serial") +public class SoundTweaker extends JPanel { + private UnitSource source; + private ASCIIMusicKeyboard keyboard; + private Synthesizer synth; + + static Logger logger = Logger.getLogger(SoundTweaker.class.getName()); + + public SoundTweaker(Synthesizer synth, String title, UnitSource source) { + this.synth = synth; + this.source = source; + + setLayout(new GridLayout(0, 2)); + + UnitGenerator ugen = source.getUnitGenerator(); + ArrayList<Component> sliders = new ArrayList<Component>(); + + add(new JLabel(title)); + + if (source instanceof Instrument) { + add(keyboard = createPolyphonicKeyboard()); + } else if (source instanceof UnitVoice) { + add(keyboard = createMonophonicKeyboard()); + } + + // Arrange the faders in a stack. + // Iterate through the ports. + for (UnitPort port : ugen.getPorts()) { + if (port instanceof UnitInputPort) { + UnitInputPort inputPort = (UnitInputPort) port; + Component slider; + // Use an exponential slider if it seems appropriate. + if ((inputPort.getMinimum() > 0.0) + && ((inputPort.getMaximum() / inputPort.getMinimum()) > 4.0)) { + slider = PortControllerFactory.createExponentialPortSlider(inputPort); + } else { + slider = PortControllerFactory.createPortSlider(inputPort); + + } + add(slider); + sliders.add(slider); + } + } + + if (keyboard != null) { + for (Component slider : sliders) { + slider.addKeyListener(keyboard.getKeyListener()); + } + } + validate(); + } + + @SuppressWarnings("serial") + private ASCIIMusicKeyboard createPolyphonicKeyboard() { + return new ASCIIMusicKeyboard() { + @Override + public void keyOff(int pitch) { + ((Instrument) source).noteOff(pitch, synth.createTimeStamp()); + } + + @Override + public void keyOn(int pitch) { + double freq = AudioMath.pitchToFrequency(pitch); + ((Instrument) source).noteOn(pitch, freq, 0.5, synth.createTimeStamp()); + } + }; + } + + @SuppressWarnings("serial") + private ASCIIMusicKeyboard createMonophonicKeyboard() { + return new ASCIIMusicKeyboard() { + @Override + public void keyOff(int pitch) { + ((UnitVoice) source).noteOff(synth.createTimeStamp()); + } + + @Override + public void keyOn(int pitch) { + double freq = AudioMath.pitchToFrequency(pitch); + ((UnitVoice) source).noteOn(freq, 0.5, synth.createTimeStamp()); + } + }; + } + +} diff --git a/src/main/java/com/jsyn/swing/XYController.java b/src/main/java/com/jsyn/swing/XYController.java new file mode 100644 index 0000000..0d97c62 --- /dev/null +++ b/src/main/java/com/jsyn/swing/XYController.java @@ -0,0 +1,132 @@ +/* + * 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.swing; + +import javax.swing.JPanel; + +/** + * Root class for 2 dimensional X,Y controller for wave editors, Theremins, etc. Maps pixel + * coordinates into "world" coordinates. + * + * @author (C) 1997 Phil Burk, SoftSynth.com + */ + +public class XYController extends JPanel { + double minWorldX = 0.0; + double maxWorldX = 1.0; + double minWorldY = 0.0; + double maxWorldY = 1.0; + + public XYController() { + } + + public XYController(double minWX, double minWY, double maxWX, double maxWY) { + setMinWorldX(minWX); + setMaxWorldX(maxWX); + setMinWorldY(minWY); + setMaxWorldY(maxWY); + } + + /** + * Set minimum World coordinate value for the horizontal X dimension. The minimum value + * corresponds to the left of the component. + */ + public void setMinWorldX(double minWX) { + minWorldX = minWX; + } + + public double getMinWorldX() { + return minWorldX; + } + + /** + * Set maximum World coordinate value for the horizontal X dimension. The minimum value + * corresponds to the right of the component. + */ + public void setMaxWorldX(double maxWX) { + maxWorldX = maxWX; + } + + public double getMaxWorldX() { + return maxWorldX; + } + + /** + * Set minimum World coordinate value for the vertical Y dimension. The minimum value + * corresponds to the bottom of the component. + */ + public void setMinWorldY(double minWY) { + minWorldY = minWY; + } + + public double getMinWorldY() { + return minWorldY; + } + + /** + * Set maximum World coordinate value for the vertical Y dimension. The maximum value + * corresponds to the top of the component. + */ + public void setMaxWorldY(double maxWY) { + maxWorldY = maxWY; + } + + public double getMaxWorldY() { + return maxWorldY; + } + + /** Convert from graphics coordinates (pixels) to world coordinates. */ + public double convertGXtoWX(int gx) { + int width = getWidth(); + return minWorldX + ((maxWorldX - minWorldX) * gx) / width; + } + + public double convertGYtoWY(int gy) { + int height = getHeight(); + return minWorldY + ((maxWorldY - minWorldY) * (height - gy)) / height; + } + + /** Convert from world coordinates to graphics coordinates (pixels). */ + public int convertWXtoGX(double wx) { + int width = getWidth(); + return (int) (((wx - minWorldX) * width) / (maxWorldX - minWorldX)); + } + + public int convertWYtoGY(double wy) { + int height = getHeight(); + return height - (int) (((wy - minWorldY) * height) / (maxWorldY - minWorldY)); + } + + /** Clip wx to the min and max World X values. */ + public double clipWorldX(double wx) { + if (wx < minWorldX) + wx = minWorldX; + else if (wx > maxWorldX) + wx = maxWorldX; + return wx; + } + + /** Clip wy to the min and max World Y values. */ + public double clipWorldY(double wy) { + if (wy < minWorldY) + wy = minWorldY; + else if (wy > maxWorldY) + wy = maxWorldY; + return wy; + } + +} diff --git a/src/main/java/com/jsyn/unitgen/Add.java b/src/main/java/com/jsyn/unitgen/Add.java new file mode 100644 index 0000000..c91fc85 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Add.java @@ -0,0 +1,50 @@ +/* + * 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.unitgen; + +/** + * This unit performs a signed addition on its two inputs. <br> + * + * <pre> + * output = inputA + inputB + * </pre> + * + * <br> + * Note that signals connected to an InputPort are automatically added together so you may not need + * this unit. + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @version 016 + * @see MultiplyAdd + * @see Subtract + */ +public class Add extends UnitBinaryOperator { + + @Override + public void generate(int start, int limit) { + double[] aValues = inputA.getValues(); + double[] bValues = inputB.getValues(); + double[] outputs = output.getValues(); + + // LOGGER.debug("adder = " + this); + // LOGGER.debug("A = " + aValues[0]); + for (int i = start; i < limit; i++) { + outputs[i] = aValues[i] + bValues[i]; + } + // LOGGER.debug("add out = " + outputs[0]); + } +} diff --git a/src/main/java/com/jsyn/unitgen/AsymptoticRamp.java b/src/main/java/com/jsyn/unitgen/AsymptoticRamp.java new file mode 100644 index 0000000..8b51294 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/AsymptoticRamp.java @@ -0,0 +1,81 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitVariablePort; + +/** + * Output approaches Input exponentially. This unit provides a slowly changing value that approaches + * its Input value exponentially. The equation is: + * + * <PRE> + * Output = Output + Rate * (Input - Output); + * </PRE> + * + * Note that the output may never reach the value of the input. It approaches the input + * asymptotically. The Rate is calculated internally based on the value on the halfLife port. Rate + * is generally just slightly less than 1.0. + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @version 016 + * @see LinearRamp + * @see ExponentialRamp + * @see ContinuousRamp + */ +public class AsymptoticRamp extends UnitFilter { + public UnitVariablePort current; + public UnitInputPort halfLife; + private double previousHalfLife = -1.0; + private double decayScalar = 0.99; + + /* Define Unit Ports used by connect() and set(). */ + public AsymptoticRamp() { + addPort(halfLife = new UnitInputPort(1, "HalfLife", 0.1)); + addPort(current = new UnitVariablePort("Current")); + } + + @Override + public void generate(int start, int limit) { + double[] outputs = output.getValues(); + double[] inputs = input.getValues(); + double currentHalfLife = halfLife.getValues()[0]; + double currentValue = current.getValue(); + double inputValue = currentValue; + + if (currentHalfLife != previousHalfLife) { + decayScalar = this.convertHalfLifeToMultiplier(currentHalfLife); + previousHalfLife = currentHalfLife; + } + + for (int i = start; i < limit; i++) { + inputValue = inputs[i]; + currentValue = currentValue + decayScalar * (inputValue - currentValue); + outputs[i] = currentValue; + } + + /* + * When current gets close to input, set current to input to prevent FP underflow, which can + * cause a severe performance degradation in 'C'. + */ + if (Math.abs(inputValue - currentValue) < VERY_SMALL_FLOAT) { + currentValue = inputValue; + } + + current.setValue(currentValue); + } +} diff --git a/src/main/java/com/jsyn/unitgen/BrownNoise.java b/src/main/java/com/jsyn/unitgen/BrownNoise.java new file mode 100644 index 0000000..e70b7f4 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/BrownNoise.java @@ -0,0 +1,75 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.util.PseudoRandom; + +/** + * BrownNoise unit. This unit uses a pseudo-random number generator to produce a noise related to + * Brownian Motion. A DC blocker is used to prevent runaway drift. + * + * <pre> + * <code> + * output = (previous * (1.0 - damping)) + (random * amplitude) + * </code> + * </pre> + * + * The output drifts quite a bit and will generally exceed the range of +/1 amplitude. + * + * @author (C) 1997-2011 Phil Burk, Mobileer Inc + * @see WhiteNoise + * @see RedNoise + * @see PinkNoise + */ +public class BrownNoise extends UnitGenerator implements UnitSource { + private PseudoRandom randomNum; + /** Increasing the damping will effectively increase the cutoff + * frequency of a high pass filter that is used to block DC bias. + * Warning: setting this too close to zero can result in very large output values. + */ + public UnitInputPort damping; + public UnitInputPort amplitude; + public UnitOutputPort output; + private double previous; + + public BrownNoise() { + randomNum = new PseudoRandom(); + addPort(damping = new UnitInputPort("Damping")); + damping.setup(0.0001, 0.01, 0.1); + addPort(amplitude = new UnitInputPort("Amplitude", UnitOscillator.DEFAULT_AMPLITUDE)); + addPort(output = new UnitOutputPort("Output")); + } + + @Override + public void generate(int start, int limit) { + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + double damper = 1.0 - damping.getValues()[0]; + + for (int i = start; i < limit; i++) { + double r = randomNum.nextRandomDouble() * amplitudes[i]; + outputs[i] = previous = (damper * previous) + r; + } + } + + @Override + public UnitOutputPort getOutput() { + return output; + } +} diff --git a/src/main/java/com/jsyn/unitgen/ChannelIn.java b/src/main/java/com/jsyn/unitgen/ChannelIn.java new file mode 100644 index 0000000..c440b4f --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/ChannelIn.java @@ -0,0 +1,59 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitOutputPort; + +/** + * Provides access to one specific channel of the audio input. For ChannelIn to work you must call + * the {@link com.jsyn.Synthesizer} start() method with numInputChannels > 0. + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @see ChannelOut + * @see LineIn + */ +public class ChannelIn extends UnitGenerator { + public UnitOutputPort output; + private int channelIndex; + + public ChannelIn() { + this(0); + } + + public ChannelIn(int channelIndex) { + addPort(output = new UnitOutputPort()); + setChannelIndex(channelIndex); + } + + public int getChannelIndex() { + return channelIndex; + } + + public void setChannelIndex(int channelIndex) { + this.channelIndex = channelIndex; + } + + @Override + public void generate(int start, int limit) { + double[] outputs = output.getValues(0); + double[] buffer = synthesisEngine.getInputBuffer(channelIndex); + for (int i = start; i < limit; i++) { + outputs[i] = buffer[i]; + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/ChannelOut.java b/src/main/java/com/jsyn/unitgen/ChannelOut.java new file mode 100644 index 0000000..8ef0677 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/ChannelOut.java @@ -0,0 +1,62 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * Provides access to one channel of the audio output. + * For more than two channels you must call + * the {@link com.jsyn.Synthesizer} start() method with numOutputChannels > 2. + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @see ChannelIn + */ +public class ChannelOut extends UnitGenerator { + public UnitInputPort input; + private int channelIndex; + + public ChannelOut() { + addPort(input = new UnitInputPort("Input")); + } + + public int getChannelIndex() { + return channelIndex; + } + + public void setChannelIndex(int channelIndex) { + this.channelIndex = channelIndex; + } + + /** + * This unit won't do anything unless you start() it. + */ + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(0); + double[] buffer = synthesisEngine.getOutputBuffer(channelIndex); + for (int i = start; i < limit; i++) { + buffer[i] += inputs[i]; + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/Circuit.java b/src/main/java/com/jsyn/unitgen/Circuit.java new file mode 100644 index 0000000..01cb860 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Circuit.java @@ -0,0 +1,122 @@ +/* + * 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.unitgen; + +import java.util.ArrayList; +import java.util.LinkedHashMap; + +import com.jsyn.engine.SynthesisEngine; +import com.jsyn.ports.UnitPort; + +/** + * Contains a list of units that are executed together. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class Circuit extends UnitGenerator { + private ArrayList<UnitGenerator> units = new ArrayList<UnitGenerator>(); + + private final LinkedHashMap<String, UnitPort> portAliases = new LinkedHashMap<String, UnitPort>(); + + @Override + public void generate(int start, int limit) { + for (UnitGenerator unit : units) { + unit.generate(start, limit); + } + } + + /** + * Call flattenOutputs on subunits. Flatten output ports so we don't output a changing signal + * when stopped. + */ + @Override + public void flattenOutputs() { + for (UnitGenerator unit : units) { + unit.flattenOutputs(); + } + } + + /** + * Call setEnabled on subunits. + */ + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + for (UnitGenerator unit : units) { + unit.setEnabled(enabled); + } + } + + /** + * @deprecated ignored, frameRate comes from the SynthesisEngine + * @param frameRate + */ + @Deprecated + @Override + public void setFrameRate(int frameRate) { + super.setFrameRate(frameRate); + for (UnitGenerator unit : units) { + unit.setFrameRate(frameRate); + } + } + + @Override + public void setSynthesisEngine(SynthesisEngine engine) { + super.setSynthesisEngine(engine); + for (UnitGenerator unit : units) { + unit.setSynthesisEngine(engine); + } + } + + /** Add a unit to the circuit. */ + public void add(UnitGenerator unit) { + units.add(unit); + unit.setCircuit(this); + // Propagate circuit properties down into subunits. + unit.setEnabled(isEnabled()); + } + + public void usePreset(int presetIndex) { + } + + + /** + * Add an alternate name for looking up a port. + * @param port + * @param alias + */ + public void addPortAlias(UnitPort port, String alias) { + // Store in a hash table by an alternate name. + portAliases.put(alias.toLowerCase(), port); + } + + + /** + * Case-insensitive search for a port by its name or alias. + * @param portName + * @return matching port or null + */ + @Override + public UnitPort getPortByName(String portName) { + UnitPort port = super.getPortByName(portName); + if (port == null) { + port = portAliases.get(portName.toLowerCase()); + } + return port; + } + +} diff --git a/src/main/java/com/jsyn/unitgen/Compare.java b/src/main/java/com/jsyn/unitgen/Compare.java new file mode 100644 index 0000000..7de2e53 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Compare.java @@ -0,0 +1,38 @@ +/* + * 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.unitgen; + +/** + * + Output 1.0 if inputA > inputB. Otherwise output 0.0. + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @version 016 + * @see Maximum + */ +public class Compare extends UnitBinaryOperator { + @Override + public void generate(int start, int limit) { + double[] aValues = inputA.getValues(); + double[] bValues = inputB.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + outputs[i] = (aValues[i] > bValues[i]) ? 1.0 : 0.0; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/ContinuousRamp.java b/src/main/java/com/jsyn/unitgen/ContinuousRamp.java new file mode 100644 index 0000000..dd90445 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/ContinuousRamp.java @@ -0,0 +1,91 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitVariablePort; + +/** + * A ramp whose function over time is continuous in value and in slope. Also called an "S curve". + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @version 016 + * @see LinearRamp + * @see ExponentialRamp + * @see AsymptoticRamp + */ +public class ContinuousRamp extends UnitFilter { + public UnitVariablePort current; + /** + * Time it takes to get from current value to input value when input is changed. Default value + * is 1.0 seconds. + */ + public UnitInputPort time; + private double previousInput = Double.MIN_VALUE; + // Coefficients for cubic polynomial. + private double a; + private double b; + private double d; + private int framesLeft; + + /* Define Unit Ports used by connect() and set(). */ + public ContinuousRamp() { + addPort(time = new UnitInputPort(1, "Time", 1.0)); + addPort(current = new UnitVariablePort("Current")); + } + + @Override + public void generate(int start, int limit) { + double[] outputs = output.getValues(); + double[] inputs = input.getValues(); + double currentTime = time.getValues()[0]; + double currentValue = current.getValue(); + double inputValue = currentValue; + + for (int i = start; i < limit; i++) { + inputValue = inputs[i]; + double x; + if (inputValue != previousInput) { + x = framesLeft; + // Calculate coefficients. + double currentSlope = x * ((3 * a * x) + (2 * b)); + + framesLeft = (int) (getSynthesisEngine().getFrameRate() * currentTime); + if (framesLeft < 1) { + framesLeft = 1; + } + x = framesLeft; + // Calculate coefficients. + d = inputValue; + double xsq = x * x; + b = ((3 * currentValue) - (currentSlope * x) - (3 * d)) / xsq; + a = (currentSlope - (2 * b * x)) / (3 * xsq); + previousInput = inputValue; + } + + if (framesLeft > 0) { + x = --framesLeft; + // Cubic polynomial. c==0 + currentValue = (x * (x * ((x * a) + b))) + d; + } + + outputs[i] = currentValue; + } + + current.setValue(currentValue); + } +} diff --git a/src/main/java/com/jsyn/unitgen/CrossFade.java b/src/main/java/com/jsyn/unitgen/CrossFade.java new file mode 100644 index 0000000..4375fa6 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/CrossFade.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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * Linear CrossFade between parts of the input. + * <P> + * Mix input[0] and input[1] based on the value of "fade". When fade is -1, output is all input[0]. + * When fade is 0, output is half input[0] and half input[1]. When fade is +1, output is all + * input[1]. + * <P> + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @version 016 + * @see Pan + */ +public class CrossFade extends UnitGenerator { + public UnitInputPort input; + public UnitInputPort fade; + public UnitOutputPort output; + + /* Define Unit Ports used by connect() and set(). */ + public CrossFade() { + addPort(input = new UnitInputPort(2, "Input")); + addPort(fade = new UnitInputPort("Fade")); + fade.setup(-1.0, 0.0, 1.0); + addPort(output = new UnitOutputPort()); + } + + @Override + public void generate(int start, int limit) { + double[] input0s = input.getValues(0); + double[] input1s = input.getValues(1); + double[] fades = fade.getValues(); + double[] outputs = output.getValues(); + for (int i = start; i < limit; i++) { + // Scale and offset to 0.0 to 1.0 range. + double gain = (fades[i] * 0.5) + 0.5; + outputs[i] = (input0s[i] * (1.0 - gain)) + (input1s[i] * gain); + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/Delay.java b/src/main/java/com/jsyn/unitgen/Delay.java new file mode 100644 index 0000000..aa450a9 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Delay.java @@ -0,0 +1,57 @@ +/* + * 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.unitgen; + +/** + * Simple non-interpolating delay. The delay line must be allocated by calling allocate(n). + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @version 016 + * @see InterpolatingDelay + */ + +public class Delay extends UnitFilter { + private float[] buffer; + private int cursor; + private int numSamples; + + /** + * Allocate an internal array for the delay line. + * + * @param numSamples + */ + public void allocate(int numSamples) { + this.numSamples = numSamples; + buffer = new float[numSamples]; + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + outputs[i] = buffer[cursor]; + buffer[cursor] = (float) inputs[i]; + cursor += 1; + if (cursor >= numSamples) { + cursor = 0; + } + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/Divide.java b/src/main/java/com/jsyn/unitgen/Divide.java new file mode 100644 index 0000000..cddcd7c --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Divide.java @@ -0,0 +1,53 @@ +/* + * 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.unitgen; + +/** + * This unit divides its two inputs. <br> + * + * <pre> + * output = inputA / inputB + * </pre> + * + * <br> + * Note that this unit is protected from dividing by zero. But you can still get some very big + * outputs. + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @version 016 + * @see Multiply + * @see Subtract + */ +public class Divide extends UnitBinaryOperator { + + @Override + public void generate(int start, int limit) { + double[] aValues = inputA.getValues(); + double[] bValues = inputB.getValues(); + double[] outputs = output.getValues(); + for (int i = start; i < limit; i++) { + /* Prevent divide by zero crash. */ + double b = bValues[i]; + if (b == 0.0) { + b = 0.0000001; + } + + outputs[i] = aValues[i] / b; + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/DualInTwoOut.java b/src/main/java/com/jsyn/unitgen/DualInTwoOut.java new file mode 100644 index 0000000..ec7dff5 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/DualInTwoOut.java @@ -0,0 +1,59 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * This unit splits a dual (stereo) input to two discrete outputs. <br> + * + * <pre> + * outputA = input[0]; + * outputB = input[1]; + * </pre> + * + * <br> + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @version 016 + */ + +public class DualInTwoOut extends UnitGenerator { + public UnitInputPort input; + public UnitOutputPort outputA; + public UnitOutputPort outputB; + + public DualInTwoOut() { + addPort(input = new UnitInputPort(2, "Input")); + addPort(outputA = new UnitOutputPort("OutputA")); + addPort(outputB = new UnitOutputPort("OutputB")); + } + + @Override + public void generate(int start, int limit) { + double[] input0s = input.getValues(0); + double[] input1s = input.getValues(1); + double[] outputAs = outputA.getValues(); + double[] outputBs = outputB.getValues(); + + for (int i = start; i < limit; i++) { + outputAs[i] = input0s[i]; + outputBs[i] = input1s[i]; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/EdgeDetector.java b/src/main/java/com/jsyn/unitgen/EdgeDetector.java new file mode 100644 index 0000000..e314f7d --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/EdgeDetector.java @@ -0,0 +1,44 @@ +/* + * 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.unitgen; + +/** + * Output 1.0 if the input crosses from zero while rising. Otherwise output zero. The output is a + * single sample wide impulse. This can be used with a Latch to implement a "sample and hold" + * circuit. + * + * @author (C) 1997-2010 Phil Burk, Mobileer Inc + * @see Latch + */ +public class EdgeDetector extends UnitFilter { + private double previous = 0.0; + + public EdgeDetector() { + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + double in = inputs[i]; + outputs[i] = ((previous <= 0.0) && (in > 0.0)) ? 1.0 : 0.0; + previous = in; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/EnvelopeAttackDecay.java b/src/main/java/com/jsyn/unitgen/EnvelopeAttackDecay.java new file mode 100644 index 0000000..db3ecaa --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/EnvelopeAttackDecay.java @@ -0,0 +1,145 @@ +/* + * 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.unitgen; + +import com.jsyn.engine.SynthesisEngine; +import com.jsyn.ports.UnitInputPort; + +/** + * Two stage Attack/Decay envelope that is triggered by an input level greater than THRESHOLD. This + * does not sustain. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public class EnvelopeAttackDecay extends UnitGate { + public static final double THRESHOLD = 0.01; + private static final double MIN_DURATION = (1.0 / 100000.0); + + /** + * Time in seconds for the rising stage of the envelope to go from 0.0 to 1.0. The attack is a + * linear ramp. + */ + public UnitInputPort attack; + /** + * Time in seconds for the falling stage to go from 0 dB to -90 dB. + */ + public UnitInputPort decay; + + public UnitInputPort amplitude; + + private enum State { + IDLE, ATTACKING, DECAYING + } + + private State state = State.IDLE; + private double scaler = 1.0; + private double level; + private double increment; + + public EnvelopeAttackDecay() { + super(); + addPort(attack = new UnitInputPort("Attack")); + attack.setup(0.001, 0.05, 8.0); + addPort(decay = new UnitInputPort("Decay")); + decay.setup(0.001, 0.2, 8.0); + addPort(amplitude = new UnitInputPort("Amplitude", 1.0)); + startIdle(); + } + + public void export(Circuit circuit, String prefix) { + circuit.addPort(attack, prefix + attack.getName()); + circuit.addPort(decay, prefix + decay.getName()); + } + + @Override + public void generate(int start, int limit) { + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit;) { + boolean triggered = input.checkGate(i); + switch (state) { + case IDLE: + for (; i < limit; i++) { + outputs[i] = level; + if (triggered) { + startAttack(i); + break; + } + } + break; + case ATTACKING: + for (; i < limit; i++) { + // Increment first so we can render fast attacks. + level += increment; + if (level >= 1.0) { + level = 1.0; + outputs[i] = level * amplitudes[i]; + startDecay(i); + break; + } + outputs[i] = level * amplitudes[i]; + } + break; + case DECAYING: + for (; i < limit; i++) { + outputs[i] = level * amplitudes[i]; + level *= scaler; + if (triggered) { + startAttack(i); + break; + } else if (level < SynthesisEngine.DB90) { + input.checkAutoDisable(); + startIdle(); + break; + } + } + break; + } + } + } + + private void startIdle() { + state = State.IDLE; + level = 0.0; + } + + private void startAttack(int i) { + double[] attacks = attack.getValues(); + double duration = attacks[i]; + if (duration < MIN_DURATION) { + level = 1.0; + startDecay(i); + } else { + // assume going from 0.0 to 1.0 even if retriggering + increment = getFramePeriod() / duration; + state = State.ATTACKING; + } + } + + private void startDecay(int i) { + double[] decays = decay.getValues(); + double duration = decays[i]; + if (duration < MIN_DURATION) { + startIdle(); + } else { + scaler = getSynthesisEngine().convertTimeToExponentialScaler(duration); + state = State.DECAYING; + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/EnvelopeDAHDSR.java b/src/main/java/com/jsyn/unitgen/EnvelopeDAHDSR.java new file mode 100644 index 0000000..c5ebe83 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/EnvelopeDAHDSR.java @@ -0,0 +1,294 @@ +/* + * 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.unitgen; + +import com.jsyn.data.SegmentedEnvelope; +import com.jsyn.engine.SynthesisEngine; +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * Six stage envelope similar to an ADSR. DAHDSR is like an ADSR but with an additional Delay stage + * before the attack, and a Hold stage after the Attack. If Delay and Hold are both set to zero then + * it will act like an ADSR. The envelope is triggered when the input goes above THRESHOLD. The + * envelope is released when the input goes below THRESHOLD. The THRESHOLD is currently 0.01 but may + * change so it would be best to use an input signal that went from 0 to 1. Mathematically an + * exponential Release will never reach 0.0. But when it reaches -96 dB the DAHDSR just sets its + * output to 0.0 and stops. There is an example program in the ZIP archive called HearDAHDSR. It + * drives a DAHDSR with a square wave. + * + * @author Phil Burk (C) 2010 Mobileer Inc + * @see SegmentedEnvelope + */ +public class EnvelopeDAHDSR extends UnitGate implements UnitSource { + private static final double MIN_DURATION = (1.0 / 100000.0); + + /** + * Time in seconds for first stage of the envelope, before the attack. Typically zero. + */ + public UnitInputPort delay; + /** + * Time in seconds for the rising stage of the envelope to go from 0.0 to 1.0. The attack is a + * linear ramp. + */ + public UnitInputPort attack; + /** Time in seconds for the plateau between the attack and decay stages. */ + public UnitInputPort hold; + /** + * Time in seconds for the falling stage to go from 0 dB to -90 dB. The decay stage will stop at + * the sustain level. But we calculate the time to fall to -90 dB so that the decay + * <em>rate</em> will be unaffected by the sustain level. + */ + public UnitInputPort decay; + /** + * Level for the sustain stage. The envelope will hold here until the input goes to zero or + * less. This should be set between 0.0 and 1.0. + */ + public UnitInputPort sustain; + /** + * Time in seconds to go from 0 dB to -90 dB. This stage is triggered when the input goes to + * zero or less. The release stage will start from the sustain level. But we calculate the time + * to fall from full amplitude so that the release <em>rate</em> will be unaffected by the + * sustain level. + */ + public UnitInputPort release; + public UnitInputPort amplitude; + + enum State { + IDLE, DELAYING, ATTACKING, HOLDING, DECAYING, SUSTAINING, RELEASING + } + + private State state = State.IDLE; + private double countdown; + private double scaler = 1.0; + private double level; + private double increment; + + public EnvelopeDAHDSR() { + super(); + addPort(delay = new UnitInputPort("Delay", 0.0)); + delay.setup(0.0, 0.0, 2.0); + addPort(attack = new UnitInputPort("Attack", 0.1)); + attack.setup(0.01, 0.1, 8.0); + addPort(hold = new UnitInputPort("Hold", 0.0)); + hold.setup(0.0, 0.0, 2.0); + addPort(decay = new UnitInputPort("Decay", 0.2)); + decay.setup(0.01, 0.2, 8.0); + addPort(sustain = new UnitInputPort("Sustain", 0.5)); + sustain.setup(0.0, 0.5, 1.0); + addPort(release = new UnitInputPort("Release", 0.3)); + release.setup(0.01, 0.3, 8.0); + addPort(amplitude = new UnitInputPort("Amplitude", 1.0)); + } + + @Override + public void generate(int start, int limit) { + double[] sustains = sustain.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit;) { + boolean triggered = input.checkGate(i); + switch (state) { + case IDLE: + for (; i < limit; i++) { + outputs[i] = level * amplitudes[i]; + if (triggered) { + startDelay(i); + break; + } + } + break; + + case DELAYING: + for (; i < limit; i++) { + outputs[i] = level * amplitudes[i]; + if (input.isOff()) { + startRelease(i); + break; + } else { + countdown -= 1; + if (countdown <= 0) { + startAttack(i); + break; + } + } + } + break; + + case ATTACKING: + for (; i < limit; i++) { + // Increment first so we can render fast attacks. + level += increment; + if (level >= 1.0) { + level = 1.0; + outputs[i] = level * amplitudes[i]; + startHold(i); + break; + } else { + outputs[i] = level * amplitudes[i]; + if (input.isOff()) { + startRelease(i); + break; + } + } + } + break; + + case HOLDING: + for (; i < limit; i++) { + outputs[i] = amplitudes[i]; // level is 1.0 + countdown -= 1; + if (countdown <= 0) { + startDecay(i); + break; + } else if (input.isOff()) { + startRelease(i); + break; + } + } + break; + + case DECAYING: + for (; i < limit; i++) { + outputs[i] = level * amplitudes[i]; + level *= scaler; // exponential decay + if (triggered) { + startDelay(i); + break; + } else if (level < sustains[i]) { + level = sustains[i]; + startSustain(i); + break; + } else if (level < SynthesisEngine.DB96) { + input.checkAutoDisable(); + startIdle(); + break; + } else if (input.isOff()) { + startRelease(i); + break; + } + } + break; + + case SUSTAINING: + for (; i < limit; i++) { + level = sustains[i]; + outputs[i] = level * amplitudes[i]; + if (triggered) { + startDelay(i); + break; + } else if (input.isOff()) { + startRelease(i); + break; + } + } + break; + + case RELEASING: + for (; i < limit; i++) { + outputs[i] = level * amplitudes[i]; + level *= scaler; // exponential decay + if (triggered) { + startDelay(i); + break; + } else if (level < SynthesisEngine.DB96) { + input.checkAutoDisable(); + startIdle(); + break; + } + } + break; + } + } + } + + private void startIdle() { + state = State.IDLE; + level = 0.0; + } + + private void startDelay(int i) { + double[] delays = delay.getValues(); + if (delays[i] <= 0.0) { + startAttack(i); + } else { + countdown = (int) (delays[i] * getFrameRate()); + state = State.DELAYING; + } + } + + private void startAttack(int i) { + double[] attacks = attack.getValues(); + double duration = attacks[i]; + if (duration < MIN_DURATION) { + level = 1.0; + startHold(i); + } else { + increment = getFramePeriod() / duration; + state = State.ATTACKING; + } + } + + private void startHold(int i) { + double[] holds = hold.getValues(); + if (holds[i] <= 0.0) { + startDecay(i); + } else { + countdown = (int) (holds[i] * getFrameRate()); + state = State.HOLDING; + } + } + + private void startDecay(int i) { + double[] decays = decay.getValues(); + double duration = decays[i]; + if (duration < MIN_DURATION) { + startSustain(i); + } else { + scaler = getSynthesisEngine().convertTimeToExponentialScaler(duration); + state = State.DECAYING; + } + } + + private void startSustain(int i) { + state = State.SUSTAINING; + } + + private void startRelease(int i) { + double[] releases = release.getValues(); + double duration = releases[i]; + if (duration < MIN_DURATION) { + duration = MIN_DURATION; + } + scaler = getSynthesisEngine().convertTimeToExponentialScaler(duration); + state = State.RELEASING; + } + + public void export(Circuit circuit, String prefix) { + circuit.addPort(attack, prefix + attack.getName()); + circuit.addPort(decay, prefix + decay.getName()); + circuit.addPort(sustain, prefix + sustain.getName()); + circuit.addPort(release, prefix + release.getName()); + } + + @Override + public UnitOutputPort getOutput() { + return output; + } + +} diff --git a/src/main/java/com/jsyn/unitgen/ExponentialRamp.java b/src/main/java/com/jsyn/unitgen/ExponentialRamp.java new file mode 100644 index 0000000..36159b4 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/ExponentialRamp.java @@ -0,0 +1,104 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitVariablePort; + +/** + * Output approaches Input exponentially and will reach it in the specified time. + * + * @author Phil Burk (C) 2010 Mobileer Inc + * @version 016 + * @see LinearRamp + * @see AsymptoticRamp + * @see ContinuousRamp + */ +public class ExponentialRamp extends UnitFilter { + public UnitInputPort time; + public UnitVariablePort current; + + private double target; + private double timeHeld = 0.0; + private double scaler = 1.0; + + public ExponentialRamp() { + addPort(time = new UnitInputPort("Time")); + input.setup(0.0001, 1.0, 1.0); + addPort(current = new UnitVariablePort("Current", 1.0)); + } + + @Override + public void generate(int start, int limit) { + double[] outputs = output.getValues(); + double currentInput = input.getValues()[0]; + double currentTime = time.getValues()[0]; + double currentValue = current.getValue(); + + if (currentTime != timeHeld) { + scaler = convertTimeToExponentialScaler(currentTime, currentValue, currentInput); + timeHeld = currentTime; + } + + // If input has changed, start new segment. + // Equality check is OK because we set them exactly equal below. + if (currentInput != target) { + scaler = convertTimeToExponentialScaler(currentTime, currentValue, currentInput); + target = currentInput; + } + + if (currentValue < target) { + // Going up. + for (int i = start; i < limit; i++) { + currentValue = currentValue * scaler; + if (currentValue > target) { + currentValue = target; + scaler = 1.0; + } + outputs[i] = currentValue; + } + } else if (currentValue > target) { + // Going down. + for (int i = start; i < limit; i++) { + currentValue = currentValue * scaler; + if (currentValue < target) { + currentValue = target; + scaler = 1.0; + } + outputs[i] = currentValue; + } + + } else if (currentValue == target) { + for (int i = start; i < limit; i++) { + outputs[i] = target; + } + } + + current.setValue(currentValue); + } + + private double convertTimeToExponentialScaler(double duration, double source, double target) { + double product = source * target; + if (product <= 0.0000001) { + throw new IllegalArgumentException( + "Exponential ramp crosses zero or gets too close to zero."); + } + // Calculate scaler so that scaler^frames = target/source + double numFrames = duration * getFrameRate(); + return Math.pow((target / source), (1.0 / numFrames)); + } +} diff --git a/src/main/java/com/jsyn/unitgen/FFT.java b/src/main/java/com/jsyn/unitgen/FFT.java new file mode 100644 index 0000000..63fce50 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FFT.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013 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.unitgen; + +/** + * Periodically transform the complex input signal using an FFT to a complex spectral stream. This + * is probably not as useful as the SpectralFFT, which outputs complete spectra. + * + * @author Phil Burk (C) 2013 Mobileer Inc + * @see IFFT + * @see SpectralFFT + */ +public class FFT extends FFTBase { + public FFT() { + super(); + } + + @Override + protected int getSign() { + return 1; // 1 for FFT + } +} diff --git a/src/main/java/com/jsyn/unitgen/FFTBase.java b/src/main/java/com/jsyn/unitgen/FFTBase.java new file mode 100644 index 0000000..055c04b --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FFTBase.java @@ -0,0 +1,86 @@ +/* + * Copyright 2013 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.unitgen; + +import com.jsyn.data.Spectrum; +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; +import com.softsynth.math.FourierMath; + +/** + * Periodically transform the complex input signal using an FFT to a complex spectral stream. + * + * @author Phil Burk (C) 2013 Mobileer Inc + * @version 016 + * @see IFFT + */ +public abstract class FFTBase extends UnitGenerator { + public UnitInputPort inputReal; + public UnitInputPort inputImaginary; + public UnitOutputPort outputReal; + public UnitOutputPort outputImaginary; + protected double[] realInput; + protected double[] realOutput; + protected double[] imaginaryInput; + protected double[] imaginaryOutput; + protected int cursor; + + protected FFTBase() { + addPort(inputReal = new UnitInputPort("InputReal")); + addPort(inputImaginary = new UnitInputPort("InputImaginary")); + addPort(outputReal = new UnitOutputPort("OutputReal")); + addPort(outputImaginary = new UnitOutputPort("OutputImaginary")); + setSize(Spectrum.DEFAULT_SIZE); + } + + public void setSize(int size) { + realInput = new double[size]; + realOutput = new double[size]; + imaginaryInput = new double[size]; + imaginaryOutput = new double[size]; + cursor = 0; + } + + public int getSize() { + return realInput.length; + } + + @Override + public void generate(int start, int limit) { + double[] inputRs = inputReal.getValues(); + double[] inputIs = inputImaginary.getValues(); + double[] outputRs = outputReal.getValues(); + double[] outputIs = outputImaginary.getValues(); + for (int i = start; i < limit; i++) { + realInput[cursor] = inputRs[i]; + imaginaryInput[cursor] = inputIs[i]; + outputRs[i] = realOutput[cursor]; + outputIs[i] = imaginaryOutput[cursor]; + cursor += 1; + // When it is full, do the FFT. + if (cursor == realInput.length) { + // Copy to output buffer so we can do the FFT in place. + System.arraycopy(realInput, 0, realOutput, 0, realInput.length); + System.arraycopy(imaginaryInput, 0, imaginaryOutput, 0, imaginaryInput.length); + FourierMath.transform(getSign(), realOutput.length, realOutput, imaginaryOutput); + cursor = 0; + } + } + } + + protected abstract int getSign(); +} diff --git a/src/main/java/com/jsyn/unitgen/FilterAllPass.java b/src/main/java/com/jsyn/unitgen/FilterAllPass.java new file mode 100644 index 0000000..749b2d6 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterAllPass.java @@ -0,0 +1,62 @@ +/* + * Copyright 2014 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * AllPass filter using the following formula: + * + * <pre> + * y(n) = -gain * x(n) + x(n - 1) + gain * y(n - 1) + * </pre> + * + * where y(n) is Output, x(n) is Input, x(n-1) is a delayed copy of the input, and y(n-1) is a + * delayed copy of the output. An all-pass filter will pass all frequencies with equal amplitude. + * But it changes the phase relationships of the partials by delaying them by an amount proportional + * to their wavelength,. + * + * @author (C) 2014 Phil Burk, SoftSynth.com + * @see FilterLowPass + */ + +public class FilterAllPass extends UnitFilter { + /** Feedback gain. Should be less than 1.0. Default is 0.8. */ + public UnitInputPort gain; + + private double x1; + private double y1; + + public FilterAllPass() { + addPort(gain = new UnitInputPort("Gain", 0.8)); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + double g = gain.getValue(); + + for (int i = start; i < limit; i++) { + double x0 = inputs[i]; + y1 = (g * (y1 - x0)) + x1; + x1 = x0; + outputs[i] = y1; + } + + } +} diff --git a/src/main/java/com/jsyn/unitgen/FilterBandPass.java b/src/main/java/com/jsyn/unitgen/FilterBandPass.java new file mode 100644 index 0000000..b103400 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterBandPass.java @@ -0,0 +1,44 @@ +/* + * 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. + */ +/** + * Aug 21, 2009 + * com.jsyn.engine.units.Filter_HighPass.java + */ + +package com.jsyn.unitgen; + +/** + * Filter that allows frequencies around the center frequency to pass and blocks others. This filter + * is based on the BiQuad filter. Coefficients are updated whenever the frequency or Q changes. + * + * @author Phil Burk (C) 2009 Mobileer Inc Translated from 'C' to Java by Lisa + * Tolentino. + */ +public class FilterBandPass extends FilterBiquadCommon { + /** + * This method is by Filter_Biquad to update coefficients for the Filter_BandPass filter. + */ + @Override + public void updateCoefficients() { + double scalar = 1.0 / (1.0 + alpha); + + a0 = alpha * scalar; + a1 = 0.0; + a2 = -a0; + b1 = -2.0 * cos_omega * scalar; + b2 = (1.0 - alpha) * scalar; + } +} diff --git a/src/main/java/com/jsyn/unitgen/FilterBandStop.java b/src/main/java/com/jsyn/unitgen/FilterBandStop.java new file mode 100644 index 0000000..d4f5249 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterBandStop.java @@ -0,0 +1,49 @@ +/* + * 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.unitgen; + +/** + * Filter that blocks frequencies around the center frequency. This filter is based on the BiQuad + * filter. Coefficients are updated whenever the frequency or Q changes. + * + * @author Phil Burk (C) 2009 Mobileer Inc Translated from 'C' to Java by Lisa + * Tolentino. + */ +public class FilterBandStop extends FilterBiquadCommon { + + @Override + public void updateCoefficients() { + + // scalar = 1.0f / (1.0f + BQCM.alpha); + // A1_B1_Value = -2.0f * BQCM.cos_omega * scalar; + // + // csFilter->csFBQ_A0 = scalar; + // csFilter->csFBQ_A1 = A1_B1_Value; + // csFilter->csFBQ_A2 = scalar; + // csFilter->csFBQ_B1 = A1_B1_Value; + // csFilter->csFBQ_B2 = (1.0f - BQCM.alpha) * scalar; + + double scalar = 1.0 / (1.0 + alpha); + double a1_b1_value = -2.0 * cos_omega * scalar; + + this.a0 = scalar; + this.a1 = a1_b1_value; + this.a2 = scalar; + this.b1 = a1_b1_value; + this.b2 = (1.0 - alpha) * scalar; + } +} diff --git a/src/main/java/com/jsyn/unitgen/FilterBiquad.java b/src/main/java/com/jsyn/unitgen/FilterBiquad.java new file mode 100644 index 0000000..f9b792f --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterBiquad.java @@ -0,0 +1,156 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * Base class for a set of IIR filters. + * + * @author Phil Burk (C) 2011 Mobileer Inc + * @see FilterBandStop + * @see FilterBandPass + * @see FilterLowPass + * @see FilterHighPass + * @see FilterTwoPolesTwoZeros + */ +public abstract class FilterBiquad extends TunableFilter { + public UnitInputPort amplitude; + + protected static final double MINIMUM_FREQUENCY = 0.00001; + protected static final double MINIMUM_GAIN = 0.00001; + protected static final double RATIO_MINIMUM = 0.499; + protected double a0; + protected double a1; + protected double a2; + protected double b1; + protected double b2; + private double x1; + private double x2; + private double y1; + private double y2; + protected double previousFrequency; + protected double omega; + protected double sin_omega; + protected double cos_omega; + + public FilterBiquad() { + addPort(amplitude = new UnitInputPort("Amplitude", 1.0)); + } + + /** + * Generic generate(int start, int limit) method calls this filter's recalculate() and + * performBiquadFilter(int, int) methods. + */ + @Override + public void generate(int start, int limit) { + recalculate(); + performBiquadFilter(start, limit); + } + + protected abstract void recalculate(); + + /** + * Each filter calls performBiquadFilter() through the generate(int, int) method. This method + * has converted Robert Bristow-Johnson's coefficients for the Direct I form in this way: Here + * is the equation that JSyn uses for this filter: + * + * <pre> + * y(n) = A0*x(n) + A1*x(n-1) + A2*x(n-2) -vB1*y(n-1) - B2*y(n-2) + * </pre> + * + * Here is the equation that Robert Bristow-Johnson uses: + * + * <pre> + * y[n] = (b0/a0)*x[n] + (b1/a0)*x[n-1] + (b2/a0)*x[n-2] - (a1/a0)*y[n-1] - (a2/a0)*y[n-2] + * </pre> + * + * So to translate between JSyn coefficients and RBJ coefficients: + * + * <pre> + * JSyn => RBJ + * A0 => b0/a0 + * A1 => b1/a0 + * A2 => b2/a0 + * B1 => a1/a0 + * B2 => a2/a0 + * </pre> + * + * @param start + * @param limit + */ + public void performBiquadFilter(int start, int limit) { + double[] inputs = input.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + double a0_jsyn, a1_jsyn, a2_jsyn, b1_jsyn, b2_jsyn; + double x0_jsyn, x1_jsyn, x2_jsyn, y1_jsyn, y2_jsyn; + + x1_jsyn = this.x1; + x2_jsyn = this.x2; + + y1_jsyn = this.y1; + y2_jsyn = this.y2; + + a0_jsyn = this.a0; + a1_jsyn = this.a1; + a2_jsyn = this.a2; + + b1_jsyn = this.b1; + b2_jsyn = this.b2; + + // Permute filter operations to reduce data movement. + for (int i = start; i < limit; i += 2) + + { + x0_jsyn = inputs[i]; + y2_jsyn = (a0_jsyn * x0_jsyn) + (a1_jsyn * x1_jsyn) + (a2_jsyn * x2_jsyn) + - (b1_jsyn * y1_jsyn) - (b2_jsyn * y2_jsyn); + + outputs[i] = amplitudes[i] * y2_jsyn; + + x2_jsyn = inputs[i + 1]; + y1_jsyn = (a0_jsyn * x2_jsyn) + (a1_jsyn * x0_jsyn) + (a2_jsyn * x1_jsyn) + - (b1_jsyn * y2_jsyn) - (b2_jsyn * y1_jsyn); + + outputs[i + 1] = amplitudes[i + 1] * y1_jsyn; + + x1_jsyn = x2_jsyn; + x2_jsyn = x0_jsyn; + } + + this.x1 = x1_jsyn; // save filter state for next time + this.x2 = x2_jsyn; + + // apply small bipolar impulse to prevent arithmetic underflow + this.y1 = y1_jsyn + VERY_SMALL_FLOAT; + this.y2 = y2_jsyn - VERY_SMALL_FLOAT; + } + + protected void calculateOmega(double ratio) { + if (ratio >= FilterBiquad.RATIO_MINIMUM) // keep a minimum + // distance from Nyquist + { + ratio = FilterBiquad.RATIO_MINIMUM; + } + + omega = 2.0 * Math.PI * ratio; + cos_omega = Math.cos(omega); // compute cosine + sin_omega = Math.sin(omega); // compute sine + } + +} diff --git a/src/main/java/com/jsyn/unitgen/FilterBiquadCommon.java b/src/main/java/com/jsyn/unitgen/FilterBiquadCommon.java new file mode 100644 index 0000000..d84b39a --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterBiquadCommon.java @@ -0,0 +1,99 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * Extend this class to create a filter that implements a Biquad filter with a Q port. + * + * @author Phil Burk (C) 2009 Mobileer Inc Translated from 'C' to Java by Lisa + * Tolentino. + */ +public abstract class FilterBiquadCommon extends FilterBiquad { + public UnitInputPort Q; + + protected final static double MINIMUM_Q = 0.00001; + private double previousQ; + protected double alpha; + + /** + * No-argument constructor instantiates the Biquad common and adds a Q port to this filter. + */ + public FilterBiquadCommon() { + addPort(Q = new UnitInputPort("Q")); + Q.setup(0.1, 1.0, 10.0); + } + + /** + * Calculate coefficients based on the filter type, eg. LowPass. + */ + public abstract void updateCoefficients(); + + public void computeBiquadCommon(double ratio, double Q) { + if (ratio >= FilterBiquad.RATIO_MINIMUM) // keep a minimum distance + // from Nyquist + { + ratio = FilterBiquad.RATIO_MINIMUM; + } + + omega = 2.0 * Math.PI * ratio; + cos_omega = Math.cos(omega); // compute cosine + sin_omega = Math.sin(omega); // compute sine + alpha = sin_omega / (2.0 * Q); // set alpha + // LOGGER.debug("Q = " + Q + ", omega = " + omega + + // ", cos(omega) = " + cos_omega + ", alpha = " + alpha ); + } + + /** + * The recalculate() method checks and ensures that the frequency and Q values are at a minimum. + * It also only updates the Biquad coefficients if either frequency or Q have changed. + */ + @Override + public void recalculate() { + double frequencyValue = frequency.getValues()[0]; // grab frequency + // element (we'll + // only use + // element[0]) + double qValue = Q.getValues()[0]; // grab Q element (we'll only use + // element[0]) + + if (frequencyValue < MINIMUM_FREQUENCY) // ensure a minimum frequency + { + frequencyValue = MINIMUM_FREQUENCY; + } + + if (qValue < MINIMUM_Q) // ensure a minimum Q + { + qValue = MINIMUM_Q; + } + // only update changed values + if (isRecalculationNeeded(frequencyValue, qValue)) { + previousFrequency = frequencyValue; // hold previous frequency + previousQ = qValue; // hold previous Q + + double ratio = frequencyValue * getFramePeriod(); + computeBiquadCommon(ratio, qValue); + updateCoefficients(); + } + } + + protected boolean isRecalculationNeeded(double frequencyValue, double qValue) { + return (frequencyValue != previousFrequency) || (qValue != previousQ); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/FilterBiquadShelf.java b/src/main/java/com/jsyn/unitgen/FilterBiquadShelf.java new file mode 100644 index 0000000..737d18d --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterBiquadShelf.java @@ -0,0 +1,111 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * This filter is based on the BiQuad filter and is used as a base class for FilterLowShelf and + * FilterHighShelf. Coefficients are updated whenever the frequency, gain or slope changes. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public abstract class FilterBiquadShelf extends FilterBiquad { + protected final static double MINIMUM_SLOPE = 0.00001; + + /** + * Gain of peak. Use 1.0 for flat response. + */ + public UnitInputPort gain; + + /** + * Shelf Slope parameter. When S = 1, the shelf slope is as steep as you can get it and remain + * monotonically increasing or decreasing gain with frequency. + */ + public UnitInputPort slope; + + private double prevGain; + private double prevSlope; + + private double beta; + protected double alpha; + protected double factorA; + protected double AP1; + protected double AM1; + protected double beta_sn; + protected double AP1cs; + protected double AM1cs; + + public FilterBiquadShelf() { + addPort(gain = new UnitInputPort("Gain", 1.0)); + addPort(slope = new UnitInputPort("Slope", 1.0)); + } + + /** + * Abstract method. Each filter must implement its update of coefficients. + */ + public abstract void updateCoefficients(); + + /** + * Compute coefficients for shelf filter if frequency, gain or slope have changed. + */ + @Override + public void recalculate() { + // Just look at first value to save time. + double frequencyValue = frequency.getValues()[0]; + if (frequencyValue < MINIMUM_FREQUENCY) { + frequencyValue = MINIMUM_FREQUENCY; + } + + double gainValue = gain.getValues()[0]; + if (gainValue < MINIMUM_GAIN) { + gainValue = MINIMUM_GAIN; + } + + double slopeValue = slope.getValues()[0]; + if (slopeValue < MINIMUM_SLOPE) { + slopeValue = MINIMUM_SLOPE; + } + + // Only do complex calculations if input changed. + if ((frequencyValue != previousFrequency) || (gainValue != prevGain) + || (slopeValue != prevSlope)) { + previousFrequency = frequencyValue; // hold previous frequency + prevGain = gainValue; + prevSlope = slopeValue; + + double ratio = frequencyValue * getFramePeriod(); + calculateOmega(ratio); + + factorA = Math.sqrt(gainValue); + + AP1 = factorA + 1.0; + AM1 = factorA - 1.0; + + /* Avoid sqrt(r<0) which hangs filter. */ + double beta2 = ((gainValue + 1.0) / slopeValue) - (AM1 * AM1); + beta = (beta2 < 0.0) ? 0.0 : Math.sqrt(beta2); + + beta_sn = beta * sin_omega; + AP1cs = AP1 * cos_omega; + AM1cs = AM1 * cos_omega; + + updateCoefficients(); + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/FilterFourPoles.java b/src/main/java/com/jsyn/unitgen/FilterFourPoles.java new file mode 100644 index 0000000..39a47c7 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterFourPoles.java @@ -0,0 +1,185 @@ +/* + * Copyright 2014 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * Resonant filter in the style of the Moog ladder filter. This implementation is loosely based on: + * http://www.musicdsp.org/archive.php?classid=3#26 + * More interesting reading: + * http://dafx04.na.infn.it/WebProc/Proc/P_061.pdf + * http://www.acoustics.ed.ac.uk/wp-content/uploads/AMT_MSc_FinalProjects + * /2012__Daly__AMT_MSc_FinalProject_MoogVCF.pdf + * http://www.music.mcgill.ca/~ich/research/misc/papers/cr1071.pdf + * + * @author Phil Burk (C) 2014 Mobileer Inc + * @see FilterLowPass + */ +public class FilterFourPoles extends TunableFilter { + public UnitInputPort Q; + public UnitInputPort gain; + + private static final double MINIMUM_FREQUENCY = 1.0; // blows up if near 0.01 + private static final double MINIMUM_Q = 0.00001; + + //private static final double SATURATION_COEFFICIENT = 0.1666667; + private static final double SATURATION_COEFFICIENT = 0.2; + // Inflection point where slope is zero. + private static final double SATURATION_UPPER_INPUT = 1.0 / Math.sqrt(3.0 * SATURATION_COEFFICIENT); + private static final double SATURATION_LOWER_INPUT = 0.0 - SATURATION_UPPER_INPUT; + private static final double SATURATION_UPPER_OUTPUT = cubicPolynomial(SATURATION_UPPER_INPUT); + private static final double SATURATION_LOWER_OUTPUT = cubicPolynomial(SATURATION_LOWER_INPUT); + + private double x1; + private double x2; + private double x3; + private double x4; + private double y1; + private double y2; + private double y3; + private double y4; + + private double previousFrequency; + private double previousQ; + // filter coefficients + private double f; + private double fTo4th; + private double feedback; + + private boolean oversampled = true; + + public FilterFourPoles() { + addPort(Q = new UnitInputPort("Q")); + frequency.setup(40.0, DEFAULT_FREQUENCY, 4000.0); + Q.setup(0.1, 2.0, 10.0); + } + + /** + * The recalculate() method checks and ensures that the frequency and Q values are at a minimum. + * It also only updates the coefficients if either frequency or Q have changed. + */ + public void recalculate() { + double frequencyValue = frequency.getValues()[0]; + double qValue = Q.getValues()[0]; + + if (frequencyValue < MINIMUM_FREQUENCY) // ensure a minimum frequency + { + frequencyValue = MINIMUM_FREQUENCY; + } + if (qValue < MINIMUM_Q) // ensure a minimum Q + { + qValue = MINIMUM_Q; + } + + // Only recompute coefficients if changed. + if ((frequencyValue != previousFrequency) || (qValue != previousQ)) { + previousFrequency = frequencyValue; + previousQ = qValue; + computeCoefficients(); + } + } + + private void computeCoefficients() { + double normalizedFrequency = previousFrequency * getFramePeriod(); + double fudge = 4.9 - 0.27 * previousQ; + if (fudge < 3.0) + fudge = 3.0; + f = normalizedFrequency * (oversampled ? 1.0 : 2.0) * fudge; + + double fSquared = f * f; + fTo4th = fSquared * fSquared; + feedback = 0.5 * previousQ * (1.0 - 0.15 * fSquared); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + + recalculate(); + + for (int i = start; i < limit; i++) { + double x0 = inputs[i]; + + if (oversampled) { + oneSample(0.0); + } + oneSample(x0); + outputs[i] = y4; + } + + // apply small bipolar impulse to prevent arithmetic underflow + y1 += VERY_SMALL_FLOAT; + y2 -= VERY_SMALL_FLOAT; + } + + private void oneSample(double x0) { + final double coeff = 0.3; + x0 -= y4 * feedback; // feedback + x0 *= 0.35013 * fTo4th; + y1 = x0 + coeff * x1 + (1 - f) * y1; // pole 1 + x1 = x0; + y2 = y1 + coeff * x2 + (1 - f) * y2; // pole 2 + x2 = y1; + y3 = y2 + coeff * x3 + (1 - f) * y3; // pole 3 + x3 = y2; + y4 = y3 + coeff * x4 + (1 - f) * y4; // pole 4 + y4 = clip(y4); + x4 = y3; + } + + public boolean isOversampled() { + return oversampled; + } + + public void setOversampled(boolean oversampled) { + this.oversampled = oversampled; + } + + // Soft saturation. This used to blow up the filter! + private static double cubicPolynomial(double x) { + return x - (x * x * x * SATURATION_COEFFICIENT); + } + + private static double clip(double x) { + if (x > SATURATION_UPPER_INPUT) { + return SATURATION_UPPER_OUTPUT; + } else if (x < SATURATION_LOWER_INPUT) { + return SATURATION_LOWER_OUTPUT; + } else { + return cubicPolynomial(x); + } + } + + public void reset() { + x1 = 0.0; + x2 = 0.0; + x3 = 0.0; + x4 = 0.0; + y1 = 0.0; + y2 = 0.0; + y3 = 0.0; + y4 = 0.0; + + previousFrequency = 0.0; + previousQ = 0.0; + f = 0.0; + fTo4th = 0.0; + feedback = 0.0; + } +} diff --git a/src/main/java/com/jsyn/unitgen/FilterHighPass.java b/src/main/java/com/jsyn/unitgen/FilterHighPass.java new file mode 100644 index 0000000..76ad6b9 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterHighPass.java @@ -0,0 +1,46 @@ +/* + * 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. + */ +/** + * Aug 21, 2009 + * com.jsyn.engine.units.Filter_HighPass.java + */ + +package com.jsyn.unitgen; + +/** + * Filter that allows frequencies above the center frequency to pass. This filter is based on the + * BiQuad filter. Coefficients are updated whenever the frequency or Q changes. + * + * @author Phil Burk (C) 2009 Mobileer Inc Translated from 'C' to Java by Lisa + * Tolentino. + */ +public class FilterHighPass extends FilterBiquadCommon { + /** + * This method is used by Filter_Biquad to update coefficients for the Filter_HighPass filter. + */ + @Override + public void updateCoefficients() { + double scalar = 1.0 / (1.0 + alpha); + double onePlusCosine = 1.0 + cos_omega; + double a0_a2_value = onePlusCosine * 0.5 * scalar; + + this.a0 = a0_a2_value; + this.a1 = -onePlusCosine * scalar; + this.a2 = a0_a2_value; + this.b1 = -2.0 * cos_omega * scalar; + this.b2 = (1.0 - alpha) * scalar; + } +} diff --git a/src/main/java/com/jsyn/unitgen/FilterHighShelf.java b/src/main/java/com/jsyn/unitgen/FilterHighShelf.java new file mode 100644 index 0000000..449090a --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterHighShelf.java @@ -0,0 +1,38 @@ +/* + * 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.unitgen; + +/** + * HighShelf Filter. This creates a flat response above the cutoff frequency. This filter is + * sometimes used at the end of a bank of EQ filters. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class FilterHighShelf extends FilterBiquadShelf { + /** + * This method is called by by Filter_BiquadShelf to update coefficients. + */ + @Override + public void updateCoefficients() { + double scalar = 1.0 / (AP1 - AM1cs + beta_sn); + a0 = factorA * (AP1 + AM1cs + beta_sn) * scalar; + a1 = -2.0 * factorA * (AM1 + AP1cs) * scalar; + a2 = factorA * (AP1 + AM1cs - beta_sn) * scalar; + b1 = 2.0 * (AM1 - AP1cs) * scalar; + b2 = (AP1 - AM1cs - beta_sn) * scalar; + } +} diff --git a/src/main/java/com/jsyn/unitgen/FilterLowPass.java b/src/main/java/com/jsyn/unitgen/FilterLowPass.java new file mode 100644 index 0000000..1557367 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterLowPass.java @@ -0,0 +1,65 @@ +/* + * 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. + */ +/** + * Aug 21, 2009 + * com.jsyn.engine.units.Filter_HighPass.java + */ + +package com.jsyn.unitgen; + +/** + * Filter that allows frequencies below the center frequency to pass. This filter is based on the + * BiQuad filter. Coefficients are updated whenever the frequency or Q changes. + * + * @author Phil Burk (C) 2009 Mobileer Inc Translated from 'C' to Java by Lisa + * Tolentino. + * + * @see FilterFourPoles + */ +public class FilterLowPass extends FilterBiquadCommon { + + /** + * This method is by FilterBiquad to update coefficients for the lowpass filter. + */ + @Override + public void updateCoefficients() { + + // scalar = 1.0f / (1.0f + BQCM.alpha); + // omc = (1.0f - BQCM.cos_omega); + // A0_A2_Value = omc * 0.5f * scalar; + // // translating from RBJ coefficients + // // A0 = (b0/(2*a0) + // // = ((1 - cos_omega)/2) / (1 + alpha) + // // = (omc*0.5) / (1 + alpha) + // // = (omc*0.5) * (1.0/(1 + alpha)) + // // = omc * 0.5 * scalar + // csFilter->csFBQ_A0 = A0_A2_Value; + // csFilter->csFBQ_A1 = omc * scalar; + // csFilter->csFBQ_A2 = A0_A2_Value; + // csFilter->csFBQ_B1 = -2.0f * BQCM.cos_omega * scalar; + // csFilter->csFBQ_B2 = (1.0f - BQCM.alpha) * scalar; + + double scalar = 1.0 / (1.0 + alpha); + double oneMinusCosine = 1.0 - cos_omega; + double a0_a2_value = oneMinusCosine * 0.5 * scalar; + + this.a0 = a0_a2_value; + this.a1 = oneMinusCosine * scalar; + this.a2 = a0_a2_value; + this.b1 = -2.0 * cos_omega * scalar; + this.b2 = (1.0 - alpha) * scalar; + } +} diff --git a/src/main/java/com/jsyn/unitgen/FilterLowShelf.java b/src/main/java/com/jsyn/unitgen/FilterLowShelf.java new file mode 100644 index 0000000..cf41f45 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterLowShelf.java @@ -0,0 +1,40 @@ +/* + * 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.unitgen; + +/** + * LowShelf Filter. This creates a flat response below the cutoff frequency. This filter is + * sometimes used at the end of a bank of EQ filters. This filter is based on the BiQuad filter. + * Coefficients are updated whenever the frequency or Q changes. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class FilterLowShelf extends FilterBiquadShelf { + + /** + * This method is called by Filter_BiquadShelf to update coefficients. + */ + @Override + public void updateCoefficients() { + double scalar = 1.0 / (AP1 + AM1cs + beta_sn); + a0 = factorA * (AP1 - AM1cs + beta_sn) * scalar; + a1 = 2.0 * factorA * (AM1 - AP1cs) * scalar; + a2 = factorA * (AP1 - AM1cs - beta_sn) * scalar; + b1 = -2.0 * (AM1 + AP1cs) * scalar; + b2 = (AP1 + AM1cs - beta_sn) * scalar; + } +} diff --git a/src/main/java/com/jsyn/unitgen/FilterOnePole.java b/src/main/java/com/jsyn/unitgen/FilterOnePole.java new file mode 100644 index 0000000..090e42b --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterOnePole.java @@ -0,0 +1,62 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitVariablePort; + +/** + * First Order, One Pole filter using the following formula: + * + * <pre> + * y(n) = A0 * x(n) - B1 * y(n - 1) + * </pre> + * + * where y(n) is Output, x(n) is Input and y(n-1) is a delayed copy of the output. This filter is a + * recursive IIR or Infinite Impulse Response filter. It can be unstable depending on the values of + * the coefficients. This can be useful as a low-pass filter, or a "leaky integrator". A thorough + * description of the digital filter theory needed to fully describe this filter is beyond the scope + * of this document. Calculating coefficients is non-intuitive; the interested user is referred to + * one of the standard texts on filter theory (e.g., Moore, "Elements of Computer Music", section + * 2.4). + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @see FilterLowPass + */ +public class FilterOnePole extends UnitFilter { + public UnitVariablePort a0; + public UnitVariablePort b1; + private double y1; + + public FilterOnePole() { + addPort(a0 = new UnitVariablePort("A0", 0.6)); + addPort(b1 = new UnitVariablePort("B1", -0.3)); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + double a0v = a0.getValue(); + double b1v = b1.getValue(); + + for (int i = start; i < limit; i++) { + double x0 = inputs[i]; + outputs[i] = y1 = (a0v * x0) - (b1v * y1); + } + + } +} diff --git a/src/main/java/com/jsyn/unitgen/FilterOnePoleOneZero.java b/src/main/java/com/jsyn/unitgen/FilterOnePoleOneZero.java new file mode 100644 index 0000000..ed1868c --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterOnePoleOneZero.java @@ -0,0 +1,68 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitVariablePort; + +/** + * First Order, One Pole, One Zero filter using the following formula: + * + * <pre> + * y(n) = A0 * x(n) + A1 * x(n - 1) - B1 * y(n - 1) + * </pre> + * + * where y(n) is Output, x(n) is Input, x(n-1) is a delayed copy of the input, and y(n-1) is a + * delayed copy of the output. This filter is a recursive IIR or Infinite Impulse Response filter. + * it can be unstable depending on the values of the coefficients. A thorough description of the + * digital filter theory needed to fully describe this filter is beyond the scope of this document. + * Calculating coefficients is non-intuitive; the interested user is referred to one of the standard + * texts on filter theory (e.g., Moore, "Elements of Computer Music", section 2.4). + * + * @author (C) 1997-2009 Phil Burk, SoftSynth.com + * @see FilterLowPass + */ + +public class FilterOnePoleOneZero extends UnitFilter { + public UnitVariablePort a0; + public UnitVariablePort a1; + public UnitVariablePort b1; + + private double x1; + private double y1; + + public FilterOnePoleOneZero() { + addPort(a0 = new UnitVariablePort("A0")); + addPort(a1 = new UnitVariablePort("A1")); + addPort(b1 = new UnitVariablePort("B1")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + double a0v = a0.getValue(); + double a1v = a1.getValue(); + double b1v = b1.getValue(); + + for (int i = start; i < limit; i++) { + double x0 = inputs[i]; + outputs[i] = y1 = (a0v * x0) + (a1v * x1) + (b1v * y1); + x1 = x0; + } + + } +} diff --git a/src/main/java/com/jsyn/unitgen/FilterOneZero.java b/src/main/java/com/jsyn/unitgen/FilterOneZero.java new file mode 100644 index 0000000..2a07a16 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterOneZero.java @@ -0,0 +1,65 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitVariablePort; + +/** + * First Order, One Zero filter using the following formula: + * + * <pre> + * y(n) = A0 * x(n) + A1 * x(n - 1) + * </pre> + * + * where y(n) is Output, x(n) is Input and x(n-1) is Input at the prior sample tick. Setting A1 + * positive gives a low-pass response; setting A1 negative gives a high-pass response. The bandwidth + * of this filter is fairly high, so it often serves a building block by being cascaded with other + * filters. If A0 and A1 are both 0.5, then this filter is a simple averaging lowpass filter, with a + * zero at SR/2 = 22050 Hz. If A0 is 0.5 and A1 is -0.5, then this filter is a high pass filter, + * with a zero at 0.0 Hz. A thorough description of the digital filter theory needed to fully + * describe this filter is beyond the scope of this document. Calculating coefficients is + * non-intuitive; the interested user is referred to one of the standard texts on filter theory + * (e.g., Moore, "Elements of Computer Music", section 2.4). + * + * @author Phil Burk (C) 2011 Mobileer Inc + * @see FilterLowPass + */ +public class FilterOneZero extends UnitFilter { + public UnitVariablePort a0; + public UnitVariablePort a1; + private double x1; + + public FilterOneZero() { + addPort(a0 = new UnitVariablePort("A0", 0.5)); + addPort(a1 = new UnitVariablePort("A1", 0.5)); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + double a0v = a0.getValue(); + double a1v = a1.getValue(); + + for (int i = start; i < limit; i++) { + double x0 = inputs[i]; + outputs[i] = (a0v * x0) + (a1v * x1); + x1 = x0; + } + + } +} diff --git a/src/main/java/com/jsyn/unitgen/FilterPeakingEQ.java b/src/main/java/com/jsyn/unitgen/FilterPeakingEQ.java new file mode 100644 index 0000000..bec7096 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterPeakingEQ.java @@ -0,0 +1,68 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * PeakingEQ Filter. This can be used to raise or lower the gain around the cutoff frequency. This + * filter is sometimes used in the middle of a bank of EQ filters. This filter is based on the + * BiQuad filter. Coefficients are updated whenever the frequency or Q changes. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class FilterPeakingEQ extends FilterBiquadCommon { + public UnitInputPort gain; + + private double previousGain; + + public FilterPeakingEQ() { + addPort(gain = new UnitInputPort("Gain", 1.0)); + } + + @Override + protected boolean isRecalculationNeeded(double frequencyValue, double qValue) { + double currentGain = gain.getValues()[0]; + if (currentGain < MINIMUM_GAIN) { + currentGain = MINIMUM_GAIN; + } + + boolean needed = super.isRecalculationNeeded(frequencyValue, qValue); + needed |= (previousGain != currentGain); + + previousGain = currentGain; + return needed; + } + + @Override + public void updateCoefficients() { + double factorA = Math.sqrt(previousGain); + double alphaTimesA = alpha * factorA; + double alphaOverA = alpha / factorA; + // Note this is not the normal scalar! + double scalar = 1.0 / (1.0 + alphaOverA); + double a1_b1_value = -2.0 * cos_omega * scalar; + + this.a0 = (1.0 + alphaTimesA) * scalar; + + this.a1 = a1_b1_value; + this.a2 = (1.0 - alphaTimesA) * scalar; + + this.b1 = a1_b1_value; + this.b2 = (1.0 - alphaOverA) * scalar; + } +} diff --git a/src/main/java/com/jsyn/unitgen/FilterStateVariable.java b/src/main/java/com/jsyn/unitgen/FilterStateVariable.java new file mode 100644 index 0000000..3a0f05c --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterStateVariable.java @@ -0,0 +1,120 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * A versatile filter described in Hal Chamberlain's "Musical Applications of MicroProcessors". It + * is convenient because its frequency and resonance can each be controlled by a single value. The + * "output" port of this filter is the "lowPass" output multiplied by the "amplitude" + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @see FilterLowPass + * @see FilterHighPass + * @see FilterFourPoles + */ +public class FilterStateVariable extends TunableFilter { + /** + * Amplitude of Output in the range of 0.0 to 1.0. SIGNAL_TYPE_RAW_SIGNED Defaults to 1.0 + * <P> + * Note that the amplitude only affects the "output" port and not the lowPass, bandPass or + * highPass signals. Use a Multiply unit if you need to scale those signals. + */ + public UnitInputPort amplitude; + + /** + * Controls feedback that causes self oscillation. Actually 1/Q - SIGNAL_TYPE_RAW_SIGNED in the + * range of 0.0 to 1.0. Defaults to 0.125. + */ + public UnitInputPort resonance; + /** + * Low pass filtered signal. + * <P> + * Note that this signal is not affected by the amplitude port. + */ + public UnitOutputPort lowPass; + /** + * Band pass filtered signal. + * <P> + * Note that this signal is not affected by the amplitude port. + */ + public UnitOutputPort bandPass; + /** + * High pass filtered signal. + * <P> + * Note that this signal is not affected by the amplitude port. + */ + public UnitOutputPort highPass; + + private double freqInternal; + private double previousFrequency = Double.MAX_VALUE; // So we trigger an immediate update. + private double lowPassValue; + private double bandPassValue; + + /** + * No-argument constructor instantiates the Biquad common and adds an amplitude port to this + * filter. + */ + public FilterStateVariable() { + frequency.set(440.0); + addPort(resonance = new UnitInputPort("Resonance", 0.2)); + addPort(amplitude = new UnitInputPort("Amplitude", 1.0)); + addPort(lowPass = new UnitOutputPort("LowPass")); + addPort(bandPass = new UnitOutputPort("BandPass")); + addPort(highPass = new UnitOutputPort("HighPass")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + double[] frequencies = frequency.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] reses = resonance.getValues(); + double[] lows = lowPass.getValues(); + double[] highs = highPass.getValues(); + double[] bands = bandPass.getValues(); + + double newFreq = frequencies[0]; + if (newFreq != previousFrequency) { + previousFrequency = newFreq; + freqInternal = 2.0 * Math.sin(Math.PI * newFreq * getFramePeriod()); + } + + for (int i = start; i < limit; i++) { + lowPassValue = (freqInternal * bandPassValue) + lowPassValue; + // Clip between -1 and +1 to prevent blowup. + lowPassValue = (lowPassValue < -1.0) ? -1.0 : ((lowPassValue > 1.0) ? 1.0 + : lowPassValue); + lows[i] = lowPassValue; + + outputs[i] = lowPassValue * (amplitudes[i]); + double highPassValue = inputs[i] - (reses[i] * bandPassValue) - lowPassValue; + // LOGGER.debug("low = " + lowPassValue + ", band = " + bandPassValue + + // ", high = " + highPassValue ); + highs[i] = highPassValue; + + bandPassValue = (freqInternal * highPassValue) + bandPassValue; + bands[i] = bandPassValue; + // LOGGER.debug("low = " + lowPassValue + ", band = " + bandPassValue + + // ", high = " + highPassValue ); + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/FilterTwoPoles.java b/src/main/java/com/jsyn/unitgen/FilterTwoPoles.java new file mode 100644 index 0000000..0c68a64 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterTwoPoles.java @@ -0,0 +1,66 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitVariablePort; + +/** + * Second Order, Two Pole filter using the following formula: + * + * <pre> + * y(n) = A0 * x(n) - B1 * y(n - 1) - B2 * y(n - 2) + * </pre> + * + * where y(n) is Output, x(n) is Input, and y(n-1) is a delayed copy of the output. This filter is a + * recursive IIR or Infinite Impulse Response filter. it can be unstable depending on the values of + * the coefficients. A thorough description of the digital filter theory needed to fully describe + * this filter is beyond the scope of this document. Calculating coefficients is non-intuitive; the + * interested user is referred to one of the standard texts on filter theory (e.g., Moore, + * "Elements of Computer Music", section 2.4). + * + * @author (C) 1997-2009 Phil Burk, Mobileer Inc + */ + +public class FilterTwoPoles extends UnitFilter { + public UnitVariablePort a0; + public UnitVariablePort b1; + public UnitVariablePort b2; + private double y1; + private double y2; + + public FilterTwoPoles() { + addPort(a0 = new UnitVariablePort("A0")); + addPort(b1 = new UnitVariablePort("B1")); + addPort(b2 = new UnitVariablePort("B2")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + double a0v = a0.getValue(); + double b1v = b1.getValue(); + double b2v = b2.getValue(); + + for (int i = start; i < limit; i++) { + double x0 = inputs[i]; + outputs[i] = y1 = 2.0 * ((a0v * x0) + (b1v * y1) + (b2v * y2)); + y2 = y1; + } + + } +} diff --git a/src/main/java/com/jsyn/unitgen/FilterTwoPolesTwoZeros.java b/src/main/java/com/jsyn/unitgen/FilterTwoPolesTwoZeros.java new file mode 100644 index 0000000..cde279f --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FilterTwoPolesTwoZeros.java @@ -0,0 +1,79 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitVariablePort; + +/** + * Second Order, Two Pole, Two Zero filter using the following formula: + * + * <pre> + * y(n) = 2.0 * (A0 * x(n) + A1 * x(n - 1) + A2 * x(n - 2) - B1 * y(n - 1) - B2 * y(n - 2)) + * </pre> + * + * where y(n) is Output, x(n) is Input, x(n-1) is a delayed copy of the input, and y(n-1) is a + * delayed copy of the output. This filter is a recursive IIR or Infinite Impulse Response filter. + * It can be unstable depending on the values of the coefficients. This filter is basically the same + * as the FilterBiquad with different ports. A thorough description of the digital filter theory + * needed to fully describe this filter is beyond the scope of this document. Calculating + * coefficients is non-intuitive; the interested user is referred to one of the standard texts on + * filter theory (e.g., Moore, "Elements of Computer Music", section 2.4). Special thanks to Robert + * Bristow-Johnson for contributing his filter equations to the music-dsp list. They were used for + * calculating the coefficients for the lowPass, highPass, and other parametric filter calculations. + * + * @author (C) 1997-2009 Phil Burk, SoftSynth.com + */ + +public class FilterTwoPolesTwoZeros extends UnitFilter { + public UnitVariablePort a0; + public UnitVariablePort a1; + public UnitVariablePort a2; + public UnitVariablePort b1; + public UnitVariablePort b2; + private double x1; + private double y1; + private double x2; + private double y2; + + public FilterTwoPolesTwoZeros() { + addPort(a0 = new UnitVariablePort("A0")); + addPort(a1 = new UnitVariablePort("A1")); + addPort(a2 = new UnitVariablePort("A2")); + addPort(b1 = new UnitVariablePort("B1")); + addPort(b2 = new UnitVariablePort("B2")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + double a0v = a0.getValue(); + double a1v = a1.getValue(); + double a2v = a2.getValue(); + double b1v = b1.getValue(); + double b2v = b2.getValue(); + + for (int i = start; i < limit; i++) { + double x0 = inputs[i]; + outputs[i] = y1 = 2.0 * ((a0v * x0) + (a1v * x1) + (a2v * x2) + (b1v * y1) + (b2v * y2)); + x2 = x1; + x1 = x0; + y2 = y1; + } + + } +} diff --git a/src/main/java/com/jsyn/unitgen/FixedRateMonoReader.java b/src/main/java/com/jsyn/unitgen/FixedRateMonoReader.java new file mode 100644 index 0000000..c6edc23 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FixedRateMonoReader.java @@ -0,0 +1,52 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitOutputPort; + +/** + * Simple sample player. Play one sample per audio frame with no interpolation. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class FixedRateMonoReader extends SequentialDataReader { + + public FixedRateMonoReader() { + addPort(output = new UnitOutputPort()); + } + + @Override + public void generate(int start, int limit) { + + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + if (dataQueue.hasMore()) { + double fdata = dataQueue.readNextMonoDouble(getFramePeriod()); + outputs[i] = fdata * amplitudes[i]; + } else { + outputs[i] = 0.0; + if (dataQueue.testAndClearAutoStop()) { + autoStop(); + } + } + dataQueue.firePendingCallbacks(); + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/FixedRateMonoWriter.java b/src/main/java/com/jsyn/unitgen/FixedRateMonoWriter.java new file mode 100644 index 0000000..c215c55 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FixedRateMonoWriter.java @@ -0,0 +1,54 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * Simple sample writer. Write one sample per audio frame with no interpolation. This can be used to + * record audio or to build delay lines. + * + * Note that you must call start() on this unit because it does not have an output for pulling data. + * + * @see FixedRateStereoWriter + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class FixedRateMonoWriter extends SequentialDataWriter { + + public FixedRateMonoWriter() { + addPort(input = new UnitInputPort("Input")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + + for (int i = start; i < limit; i++) { + if (dataQueue.hasMore()) { + double value = inputs[i]; + dataQueue.writeNextDouble(value); + } else { + if (dataQueue.testAndClearAutoStop()) { + autoStop(); + } + } + dataQueue.firePendingCallbacks(); + } + + } + +} diff --git a/src/main/java/com/jsyn/unitgen/FixedRateStereoReader.java b/src/main/java/com/jsyn/unitgen/FixedRateStereoReader.java new file mode 100644 index 0000000..6dcf72c --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FixedRateStereoReader.java @@ -0,0 +1,59 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitOutputPort; + +/** + * Simple stereo sample player. Play one sample per audio frame with no interpolation. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class FixedRateStereoReader extends SequentialDataReader { + public FixedRateStereoReader() { + addPort(output = new UnitOutputPort(2, "Output")); + dataQueue.setNumChannels(2); + } + + @Override + public void generate(int start, int limit) { + double[] amplitudes = amplitude.getValues(); + double[] output0s = output.getValues(0); + double[] output1s = output.getValues(1); + + for (int i = start; i < limit; i++) { + if (dataQueue.hasMore()) { + dataQueue.beginFrame(getFramePeriod()); + double fdata = dataQueue.readCurrentChannelDouble(0); + // LOGGER.debug("SampleReader_16F2: left = " + fdata ); + double amp = amplitudes[i]; + output0s[i] = fdata * amp; + fdata = dataQueue.readCurrentChannelDouble(1); + // LOGGER.debug("SampleReader_16F2: right = " + fdata ); + output1s[i] = fdata * amp; + dataQueue.endFrame(); + } else { + output0s[i] = 0.0; + output1s[i] = 0.0; + if (dataQueue.testAndClearAutoStop()) { + autoStop(); + } + } + dataQueue.firePendingCallbacks(); + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/FixedRateStereoWriter.java b/src/main/java/com/jsyn/unitgen/FixedRateStereoWriter.java new file mode 100644 index 0000000..e4502f9 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FixedRateStereoWriter.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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * Simple stereo sample writer. Write two samples per audio frame with no interpolation. This can be + * used to record audio or to build delay lines. + * + * Note that you must call start() on this unit because it does not have an output for pulling data. + * + * @see FixedRateMonoWriter + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class FixedRateStereoWriter extends SequentialDataWriter { + + public FixedRateStereoWriter() { + addPort(input = new UnitInputPort(2, "Input")); + dataQueue.setNumChannels(2); + } + + @Override + public void generate(int start, int limit) { + double[] input0s = input.getValues(0); + double[] input1s = input.getValues(1); + + for (int i = start; i < limit; i++) { + if (dataQueue.hasMore()) { + dataQueue.beginFrame(getFramePeriod()); + double value = input0s[i]; + dataQueue.writeCurrentChannelDouble(0, value); + value = input1s[i]; + dataQueue.writeCurrentChannelDouble(1, value); + dataQueue.endFrame(); + } else { + if (dataQueue.testAndClearAutoStop()) { + autoStop(); + } + } + dataQueue.firePendingCallbacks(); + } + + } + +} diff --git a/src/main/java/com/jsyn/unitgen/FourWayFade.java b/src/main/java/com/jsyn/unitgen/FourWayFade.java new file mode 100644 index 0000000..8f88965 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FourWayFade.java @@ -0,0 +1,94 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * FourWayFade unit. + * <P> + * Mix inputs 0-3 based on the value of two fade ports. You can think of the four inputs arranged + * clockwise as follows. + * </P> + * + * <PRE> + * input[0] ---- input[1] + * | | + * | | + * | | + * input[3] ---- input[2] + * </PRE> + * + * The "fade" port has two parts. Fade[0] fades between the pair of inputs (0,3) and the pair of + * inputs (1,2). Fade[1] fades between the pair of inputs (0,1) and the pair of inputs (3,2). + * + * <PRE> + * Fade[0] Fade[1] Output + * -1 -1 Input[3] + * -1 +1 Input[0] + * +1 -1 Input[2] + * +1 +1 Input[1] + * + * + * -----Fade[0]-----> + * + * A + * | + * | + * Fade[1] + * | + * | + * </PRE> + * <P> + * + * @author (C) 1997-2009 Phil Burk, Mobileer Inc + */ +public class FourWayFade extends UnitGenerator { + public UnitInputPort input; + public UnitInputPort fade; + public UnitOutputPort output; + + /* Define Unit Ports used by connect() and set(). */ + public FourWayFade() { + addPort(input = new UnitInputPort(4, "Input")); + addPort(fade = new UnitInputPort(2, "Fade")); + addPort(output = new UnitOutputPort()); + } + + @Override + public void generate(int start, int limit) { + double[] inputAs = input.getValues(0); + double[] inputBs = input.getValues(1); + double[] inputCs = input.getValues(2); + double[] inputDs = input.getValues(3); + double[] fadeLRs = fade.getValues(0); + double[] fadeFBs = fade.getValues(1); + double[] outputs = output.getValues(0); + + for (int i = start; i < limit; i++) { + // Scale and offset to 0.0 to 1.0 range. + double gainLR = (fadeLRs[i] * 0.5) + 0.5; + double temp = 1.0 - gainLR; + double mixFront = (inputAs[i] * temp) + (inputBs[i] * gainLR); + double mixBack = (inputDs[i] * temp) + (inputCs[i] * gainLR); + + double gainFB = (fadeFBs[i] * 0.5) + 0.5; + outputs[i] = (mixBack * (1.0 - gainFB)) + (mixFront * gainFB); + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/FunctionEvaluator.java b/src/main/java/com/jsyn/unitgen/FunctionEvaluator.java new file mode 100644 index 0000000..0cc0c83 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FunctionEvaluator.java @@ -0,0 +1,76 @@ +/* + * 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. + */ +/** + * Aug 26, 2009 + * com.jsyn.engine.units.TunableFilter.java + */ + +package com.jsyn.unitgen; + +import com.jsyn.data.Function; +import com.jsyn.ports.UnitFunctionPort; +import com.jsyn.ports.UnitInputPort; + +/** + * Convert an input value to an output value. The Function is typically implemented by looking up a + * value in a DoubleTable. But other implementations of Function can be used. Input typically ranges + * from -1.0 to +1.0. + * + * <pre> + * <code> + * // A unit that will lookup the function. + * FunctionEvaluator shaper = new FunctionEvaluator(); + * synth.add( shaper ); + * shaper.start(); + * // Define a custom function. + * Function cuber = new Function() + * { + * public double evaluate( double x ) + * { + * return x * x * x; + * } + * }; + * shaper.function.set(cuber); + * + * shaper.input.set( 0.5 ); + * </code> + * </pre> + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @see Function + */ +public class FunctionEvaluator extends UnitFilter { + public UnitInputPort amplitude; + public UnitFunctionPort function; + + public FunctionEvaluator() { + addPort(amplitude = new UnitInputPort("Amplitude", 1.0)); + addPort(function = new UnitFunctionPort("Function")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + Function functionObject = function.get(); + + for (int i = start; i < limit; i++) { + outputs[i] = functionObject.evaluate(inputs[i]) * amplitudes[i]; + } + + } +} diff --git a/src/main/java/com/jsyn/unitgen/FunctionOscillator.java b/src/main/java/com/jsyn/unitgen/FunctionOscillator.java new file mode 100644 index 0000000..30d32d5 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/FunctionOscillator.java @@ -0,0 +1,58 @@ +/* + * 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.unitgen; + +import com.jsyn.data.Function; +import com.jsyn.ports.UnitFunctionPort; + +/** + * Oscillator that uses a Function object to define the waveform. Note that a DoubleTable can be + * used as the Function. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class FunctionOscillator extends UnitOscillator { + public UnitFunctionPort function; + + public FunctionOscillator() { + addPort(function = new UnitFunctionPort("Function")); + } + + @Override + public void generate(int start, int limit) { + double[] frequencies = frequency.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + + Function functionObject = function.get(); + + // Variables have a single value. + double currentPhase = phase.getValue(); + + for (int i = start; i < limit; i++) { + // Generate sawtooth phasor to provide phase for function lookup. + double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]); + currentPhase = incrementWrapPhase(currentPhase, phaseIncrement); + double value = functionObject.evaluate(currentPhase); + outputs[i] = value * amplitudes[i]; + } + + // Value needs to be saved for next time. + phase.setValue(currentPhase); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/Grain.java b/src/main/java/com/jsyn/unitgen/Grain.java new file mode 100644 index 0000000..812061c --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Grain.java @@ -0,0 +1,89 @@ +/* + * 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.unitgen; + +/** + * A single Grain that is normally created and controlled by a GrainFarm. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public class Grain implements GrainEnvelope { + private double frameRate; + private double amplitude = 1.0; + + private GrainSource source; + private GrainEnvelope envelope; + + public Grain(GrainSource source, GrainEnvelope envelope) { + this.source = source; + this.envelope = envelope; + } + + @Override + public double next() { + if (envelope.hasMoreValues()) { + double env = envelope.next(); + return source.next() * env * amplitude; + } else { + return 0.0; + } + } + + @Override + public boolean hasMoreValues() { + return envelope.hasMoreValues(); + } + + @Override + public void reset() { + source.reset(); + envelope.reset(); + } + + public void setRate(double rate) { + source.setRate(rate); + } + + @Override + public void setDuration(double duration) { + envelope.setDuration(duration); + } + + @Override + public double getFrameRate() { + return frameRate; + } + + @Override + public void setFrameRate(double frameRate) { + this.frameRate = frameRate; + source.setFrameRate(frameRate); + envelope.setFrameRate(frameRate); + } + + public double getAmplitude() { + return amplitude; + } + + public void setAmplitude(double amplitude) { + this.amplitude = amplitude; + } + + public GrainSource getSource() { + return source; + } +} diff --git a/src/main/java/com/jsyn/unitgen/GrainCommon.java b/src/main/java/com/jsyn/unitgen/GrainCommon.java new file mode 100644 index 0000000..a7a04fc --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/GrainCommon.java @@ -0,0 +1,32 @@ +/* + * 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.unitgen; + +public class GrainCommon { + protected double frameRate; + + public double getFrameRate() { + return frameRate; + } + + public void setFrameRate(double frameRate) { + this.frameRate = frameRate; + } + + public void reset() { + } +} diff --git a/src/main/java/com/jsyn/unitgen/GrainEnvelope.java b/src/main/java/com/jsyn/unitgen/GrainEnvelope.java new file mode 100644 index 0000000..e6ff24c --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/GrainEnvelope.java @@ -0,0 +1,52 @@ +/* + * 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.unitgen; + +/** + * This envelope should start at 0.0, go up to 1.0 and then return to 0.0 in duration time. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public interface GrainEnvelope { + + double getFrameRate(); + + void setFrameRate(double frameRate); + + /** + * @return next amplitude value of envelope + */ + double next(); + + /** + * Are there any more values to be generated in the envelope? + * + * @return true if more + */ + boolean hasMoreValues(); + + /** + * Prepare to start a new envelope. + */ + void reset(); + + /** + * @param duration in seconds. + */ + void setDuration(double duration); + +} diff --git a/src/main/java/com/jsyn/unitgen/GrainFarm.java b/src/main/java/com/jsyn/unitgen/GrainFarm.java new file mode 100644 index 0000000..78179bc --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/GrainFarm.java @@ -0,0 +1,178 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.util.PseudoRandom; + +/** + * A unit generator that generates a cloud of sound using multiple Grains. Special thanks to my + * friend Ross Bencina for his excellent article on Granular Synthesis. Several of his ideas are + * reflected in this architecture. "Implementing Real-Time Granular Synthesis" by Ross Bencina, + * Audio Anecdotes III, 2001. + * + * <pre><code> + synth.add( sampleGrainFarm = new GrainFarm() ); + grainFarm.allocate( NUM_GRAINS ); +</code></pre> + * + * @author Phil Burk (C) 2011 Mobileer Inc + * @see Grain + * @see GrainSourceSine + * @see RaisedCosineEnvelope + */ +public class GrainFarm extends UnitGenerator implements UnitSource { + /** A scaler for playback rate. Nominally 1.0. */ + public UnitInputPort rate; + public UnitInputPort rateRange; + public UnitInputPort amplitude; + public UnitInputPort amplitudeRange; + public UnitInputPort density; + public UnitInputPort duration; + public UnitInputPort durationRange; + public UnitOutputPort output; + + PseudoRandom randomizer; + private GrainState[] states; + private double countScaler = 1.0; + private final GrainScheduler scheduler = new StochasticGrainScheduler(); + + public GrainFarm() { + randomizer = new PseudoRandom(); + addPort(rate = new UnitInputPort("Rate", 1.0)); + addPort(amplitude = new UnitInputPort("Amplitude", 1.0)); + addPort(duration = new UnitInputPort("Duration", 0.01)); + addPort(rateRange = new UnitInputPort("RateRange", 0.0)); + addPort(amplitudeRange = new UnitInputPort("AmplitudeRange", 0.0)); + addPort(durationRange = new UnitInputPort("DurationRange", 0.0)); + addPort(density = new UnitInputPort("Density", 0.1)); + addPort(output = new UnitOutputPort()); + } + + private class GrainState { + Grain grain; + int countdown; + double lastDuration; + final static int STATE_IDLE = 0; + final static int STATE_GAP = 1; + final static int STATE_RUNNING = 2; + int state = STATE_IDLE; + private double gapError; + + public double next(int i) { + double output = 0.0; + if (state == STATE_RUNNING) { + if (grain.hasMoreValues()) { + output = grain.next(); + } else { + startGap(i); + } + } else if (state == STATE_GAP) { + if (countdown > 0) { + countdown -= 1; + } else if (countdown == 0) { + state = STATE_RUNNING; + grain.reset(); + + setupGrain(grain, i); + + double dur = nextDuration(i); + grain.setDuration(dur); + } + } else if (state == STATE_IDLE) { + nextDuration(i); // initialize lastDuration + startGap(i); + } + return output; + } + + private double nextDuration(int i) { + double dur = duration.getValues()[i]; + dur = scheduler.nextDuration(dur); + lastDuration = dur; + return dur; + } + + private void startGap(int i) { + state = STATE_GAP; + double dens = density.getValues()[i]; + double gap = scheduler.nextGap(lastDuration, dens) * getFrameRate(); + gap += gapError; + countdown = (int) gap; + gapError = gap - countdown; + } + } + + public void setGrainArray(Grain[] grains) { + countScaler = 1.0 / grains.length; + states = new GrainState[grains.length]; + for (int i = 0; i < states.length; i++) { + states[i] = new GrainState(); + states[i].grain = grains[i]; + grains[i].setFrameRate(getSynthesisEngine().getFrameRate()); + } + } + + public void setupGrain(Grain grain, int i) { + double temp = rate.getValues()[i] * calculateOctaveScaler(rateRange.getValues()[i]); + grain.setRate(temp); + + // Scale the amplitude range so that we never go above + // original amplitude. + double base = amplitude.getValues()[i]; + double offset = base * Math.random() * amplitudeRange.getValues()[i]; + grain.setAmplitude(base - offset); + } + + public void allocate(int numGrains) { + Grain[] grainArray = new Grain[numGrains]; + for (int i = 0; i < numGrains; i++) { + Grain grain = new Grain(new GrainSourceSine(), new RaisedCosineEnvelope()); + grainArray[i] = grain; + } + setGrainArray(grainArray); + } + + @Override + public UnitOutputPort getOutput() { + return output; + } + + private double calculateOctaveScaler(double rangeValue) { + double octaveRange = 0.5 * randomizer.nextRandomDouble() * rangeValue; + return Math.pow(2.0, octaveRange); + } + + @Override + public void generate(int start, int limit) { + double[] outputs = output.getValues(); + double[] amplitudes = amplitude.getValues(); + // double frp = getSynthesisEngine().getFramePeriod(); + for (int i = start; i < limit; i++) { + double result = 0.0; + + // Mix the grains together. + for (GrainState grainState : states) { + result += grainState.next(i); + } + + outputs[i] = result * amplitudes[i] * countScaler; + } + + } +} diff --git a/src/main/java/com/jsyn/unitgen/GrainScheduler.java b/src/main/java/com/jsyn/unitgen/GrainScheduler.java new file mode 100644 index 0000000..df9c25e --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/GrainScheduler.java @@ -0,0 +1,44 @@ +/* + * 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.unitgen; + +/** + * Defines a class that can schedule the execution of Grains in a GrainFarm. This is mostly for + * internal use. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public interface GrainScheduler { + + /** + * Calculate time in seconds for the next gap between grains. + * + * @param duration + * @param density + * @return seconds before next grain + */ + double nextGap(double duration, double density); + + /** + * Calculate duration in seconds for the next grains. + * + * @param suggestedDuration + * @return duration of grain seconds + */ + double nextDuration(double suggestedDuration); + +} diff --git a/src/main/java/com/jsyn/unitgen/GrainSource.java b/src/main/java/com/jsyn/unitgen/GrainSource.java new file mode 100644 index 0000000..1d5c522 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/GrainSource.java @@ -0,0 +1,36 @@ +/* + * 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.unitgen; + +/** + * Defines classes that can provide the signal inside a Grain. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public interface GrainSource { + double getFrameRate(); + + void setFrameRate(double frameRate); + + /** Generate one more value or the source signal. */ + double next(); + + /** Reset the internal phase of the grain. */ + void reset(); + + void setRate(double rate); +} diff --git a/src/main/java/com/jsyn/unitgen/GrainSourceSine.java b/src/main/java/com/jsyn/unitgen/GrainSourceSine.java new file mode 100644 index 0000000..0af9cbd --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/GrainSourceSine.java @@ -0,0 +1,51 @@ +/* + * 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.unitgen; + +/** + * A simple sine wave generator for a Grain. This uses the same fast Taylor expansion that the + * SineOscillator uses. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public class GrainSourceSine extends GrainCommon implements GrainSource { + protected double phase; + protected double phaseIncrement; + + public GrainSourceSine() { + setRate(1.0); + } + + public void setPhaseIncrement(double phaseIncrement) { + this.phaseIncrement = phaseIncrement; + } + + @Override + public double next() { + phase += phaseIncrement; + if (phase > 1.0) { + phase -= 2.0; + } + return SineOscillator.fastSin(phase); + } + + @Override + public void setRate(double rate) { + setPhaseIncrement(rate * 0.1 / Math.PI); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/IFFT.java b/src/main/java/com/jsyn/unitgen/IFFT.java new file mode 100644 index 0000000..307acd2 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/IFFT.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013 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.unitgen; + +/** + * Periodically transform the complex input spectrum using an IFFT to a complex signal stream. + * + * @author Phil Burk (C) 2013 Mobileer Inc + * @version 016 + * @see FFT + */ +public class IFFT extends FFTBase { + + public IFFT() { + super(); + } + + @Override + protected int getSign() { + return -1; // -1 for IFFT + } +} diff --git a/src/main/java/com/jsyn/unitgen/ImpulseOscillator.java b/src/main/java/com/jsyn/unitgen/ImpulseOscillator.java new file mode 100644 index 0000000..8c676f3 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/ImpulseOscillator.java @@ -0,0 +1,59 @@ +/* + * 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.unitgen; + +/** + * Narrow impulse oscillator. An impulse is only one sample wide. It is useful for pinging filters + * or generating an "impulse response". + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class ImpulseOscillator extends UnitOscillator { + + @Override + public void generate(int start, int limit) { + double[] frequencies = frequency.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + + // Variables have a single value. + double currentPhase = phase.getValue(); + + double inverseNyquist = synthesisEngine.getInverseNyquist(); + + for (int i = start; i < limit; i++) { + /* Generate sawtooth phasor to provide phase for impulse generation. */ + double phaseIncrement = frequencies[i] * inverseNyquist; + currentPhase += phaseIncrement; + + double ampl = amplitudes[i]; + double result = 0.0; + if (currentPhase >= 1.0) { + currentPhase -= 2.0; + result = ampl; + } else if (currentPhase < -1.0) { + currentPhase += 2.0; + result = ampl; + } + outputs[i] = result; + } + + // Value needs to be saved for next time. + phase.setValue(currentPhase); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/ImpulseOscillatorBL.java b/src/main/java/com/jsyn/unitgen/ImpulseOscillatorBL.java new file mode 100644 index 0000000..23686b8 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/ImpulseOscillatorBL.java @@ -0,0 +1,39 @@ +/* + * 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.unitgen; + +import com.jsyn.engine.MultiTable; + +/** + * Impulse oscillator created by differentiating a sawtoothBL. A band limited impulse is very narrow + * but is slightly wider than one sample. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class ImpulseOscillatorBL extends SawtoothOscillatorBL { + private double previous = 0.0; + + @Override + protected double generateBL(MultiTable multiTable, double currentPhase, + double positivePhaseIncrement, double flevel, int i) { + double saw = multiTable.calculateSawtooth(currentPhase, positivePhaseIncrement, flevel); + double result = previous - saw; + previous = saw; + return result; + } + +} diff --git a/src/main/java/com/jsyn/unitgen/Integrate.java b/src/main/java/com/jsyn/unitgen/Integrate.java new file mode 100644 index 0000000..50831d2 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Integrate.java @@ -0,0 +1,82 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * IntegrateUnit unit. + * <P> + * Output accumulated sum of the input signal. This can be used to transform one signal into + * another, or to generate ramps between the limits by setting the input signal positive or + * negative. For a "leaky integrator" use a FilterOnePoleOneZero. + * </P> + * + * <pre> + * output = output + input; + * if (output < lowerLimit) + * output = lowerLimit; + * else if (output > upperLimit) + * output = upperLimit; + * </pre> + * + * @author (C) 1997-2011 Phil Burk, Mobileer Inc + * @see FilterOnePoleOneZero + */ +public class Integrate extends UnitGenerator { + public UnitInputPort input; + /** + * Output will be stopped internally from going below this value. Default is -1.0. + */ + public UnitInputPort lowerLimit; + /** + * Output will be stopped internally from going above this value. Default is +1.0. + */ + public UnitInputPort upperLimit; + public UnitOutputPort output; + + private double accum; + + /* Define Unit Ports used by connect() and set(). */ + public Integrate() { + addPort(input = new UnitInputPort("Input")); + addPort(lowerLimit = new UnitInputPort("LowerLimit", -1.0)); + addPort(upperLimit = new UnitInputPort("UpperLimit", 1.0)); + addPort(output = new UnitOutputPort("Output")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] lowerLimits = lowerLimit.getValues(); + double[] upperLimits = upperLimit.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + accum += inputs[i]; // INTEGRATE + + // clip to limits + if (accum > upperLimits[i]) + accum = upperLimits[i]; + else if (accum < lowerLimits[i]) + accum = lowerLimits[i]; + + outputs[i] = accum; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/InterpolatingDelay.java b/src/main/java/com/jsyn/unitgen/InterpolatingDelay.java new file mode 100644 index 0000000..24de4f9 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/InterpolatingDelay.java @@ -0,0 +1,117 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * InterpolatingDelay + * <P> + * InterpolatingDelay provides a variable time delay with an input and output. The internal data + * format is 32-bit floating point. The amount of delay can be varied from 0.0 to a time in seconds + * corresponding to the numFrames allocated. The fractional delay values are calculated by linearly + * interpolating between adjacent values in the delay line. + * <P> + * This unit can be used to implement time varying delay effects such as a flanger or a chorus. It + * can also be used to implement physical models of acoustic instruments, or other tunable delay + * based resonant systems. + * <P> + * + * @author (C) 1997-2011 Phil Burk, Mobileer Inc + * @see Delay + */ + +public class InterpolatingDelay extends UnitFilter { + /** + * Delay time in seconds. This value will converted to frames and clipped between zero and the + * numFrames value passed to allocate(). The minimum and default delay time is 0.0. + */ + public UnitInputPort delay; + + private float[] buffer; + private int cursor; + private int numFrames; + + public InterpolatingDelay() { + addPort(delay = new UnitInputPort("Delay")); + } + + /** + * Allocate memory for the delay buffer. For a 2 second delay at 44100 Hz sample rate you will + * need at least 88200 samples. + * + * @param numFrames size of the float array to hold the delayed samples + */ + public void allocate(int numFrames) { + this.numFrames = numFrames; + // Allocate extra frame for guard point to speed up interpolation. + buffer = new float[numFrames + 1]; + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + double[] delays = delay.getValues(); + + for (int i = start; i < limit; i++) { + // This should be at the beginning of the loop + // because the guard point should == buffer[0]. + if (cursor == numFrames) { + // Write guard point! Must allocate one extra sample. + buffer[numFrames] = (float) inputs[i]; + cursor = 0; + } + + buffer[cursor] = (float) inputs[i]; + + /* Convert delay time to a clipped frame offset. */ + double delayFrames = delays[i] * getFrameRate(); + + // Clip to zero delay. + if (delayFrames <= 0.0) { + outputs[i] = buffer[cursor]; + } else { + // Clip to maximum delay. + if (delayFrames >= numFrames) { + delayFrames = numFrames - 1; + } + + // Calculate fractional index into delay buffer. + double readIndex = cursor - delayFrames; + if (readIndex < 0.0) { + readIndex += numFrames; + } + // setup for interpolation. + // We know readIndex is > 0 so we do not need to call floor(). + int iReadIndex = (int) readIndex; + double frac = readIndex - iReadIndex; + + // Get adjacent values relying on guard point to prevent overflow. + double val0 = buffer[iReadIndex]; + double val1 = buffer[iReadIndex + 1]; + + // Interpolate new value. + outputs[i] = val0 + (frac * (val1 - val0)); + } + + cursor += 1; + } + + } + +} diff --git a/src/main/java/com/jsyn/unitgen/Latch.java b/src/main/java/com/jsyn/unitgen/Latch.java new file mode 100644 index 0000000..0518f69 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Latch.java @@ -0,0 +1,53 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * Latch or hold an input value. + * <p> + * Pass a value unchanged if gate true, otherwise output held value. + * <p> + * output = ( gate > 0.0 ) ? input : previous_output; + * + * @author (C) 1997-2010 Phil Burk, Mobileer Inc + * @see EdgeDetector + */ +public class Latch extends UnitFilter { + public UnitInputPort gate; + private double held; + + /* Define Unit Ports used by connect() and set(). */ + public Latch() { + addPort(gate = new UnitInputPort("Gate")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] gates = gate.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + if (gates[i] > 0.0) { + held = inputs[i]; + } + outputs[i] = held; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/LatchZeroCrossing.java b/src/main/java/com/jsyn/unitgen/LatchZeroCrossing.java new file mode 100644 index 0000000..9e6c011 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/LatchZeroCrossing.java @@ -0,0 +1,72 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * Latches when input crosses zero. + * <P> + * Pass a value unchanged if gate true, otherwise pass input unchanged until input crosses zero then + * output zero. This can be used to turn off a sound at a zero crossing so there is no pop. + * <P> + * + * @author (C) 2010 Phil Burk, Mobileer Inc + * @see Latch + * @see Minimum + */ +public class LatchZeroCrossing extends UnitGenerator { + public UnitInputPort input; + public UnitInputPort gate; + public UnitOutputPort output; + private double held; + private boolean crossed; + + /* Define Unit Ports used by connect() and set(). */ + public LatchZeroCrossing() { + addPort(input = new UnitInputPort("Input")); + addPort(gate = new UnitInputPort("Gate", 1.0)); + addPort(output = new UnitOutputPort("Output")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] gates = gate.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + double current = inputs[i]; + if (gates[i] > 0.0) { + held = current; + crossed = false; + } else { + // If we haven't already seen a zero crossing then look for one. + if (!crossed) { + if ((held * current) <= 0.0) { + held = 0.0; + crossed = true; + } else { + held = current; + } + } + } + outputs[i] = held; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/LineIn.java b/src/main/java/com/jsyn/unitgen/LineIn.java new file mode 100644 index 0000000..aeef965 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/LineIn.java @@ -0,0 +1,51 @@ +/* + * 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.unitgen; + +import com.jsyn.Synthesizer; +import com.jsyn.ports.UnitOutputPort; + +/** + * External audio input is sent to the output of this unit. The LineIn provides a stereo signal + * containing channels 0 and 1. For LineIn to work you must call the Synthesizer start() method with + * numInputChannels > 0. + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @see Synthesizer + * @see ChannelIn + * @see LineOut + */ +public class LineIn extends UnitGenerator { + public UnitOutputPort output; + + public LineIn() { + addPort(output = new UnitOutputPort(2, "Output")); + } + + @Override + public void generate(int start, int limit) { + double[] outputs0 = output.getValues(0); + double[] outputs1 = output.getValues(1); + double[] buffer0 = synthesisEngine.getInputBuffer(0); + double[] buffer1 = synthesisEngine.getInputBuffer(1); + for (int i = start; i < limit; i++) { + outputs0[i] = buffer0[i]; + outputs1[i] = buffer1[i]; + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/LineOut.java b/src/main/java/com/jsyn/unitgen/LineOut.java new file mode 100644 index 0000000..29c8ce7 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/LineOut.java @@ -0,0 +1,57 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * Input audio is sent to the external audio output device. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class LineOut extends UnitGenerator implements UnitSink { + public UnitInputPort input; + + public LineOut() { + addPort(input = new UnitInputPort(2, "Input")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs0 = input.getValues(0); + double[] inputs1 = input.getValues(1); + double[] buffer0 = synthesisEngine.getOutputBuffer(0); + double[] buffer1 = synthesisEngine.getOutputBuffer(1); + for (int i = start; i < limit; i++) { + buffer0[i] += inputs0[i]; + buffer1[i] += inputs1[i]; + } + } + + /** + * This unit won't do anything unless you start() it. + */ + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public UnitInputPort getInput() { + return input; + } +} diff --git a/src/main/java/com/jsyn/unitgen/LinearRamp.java b/src/main/java/com/jsyn/unitgen/LinearRamp.java new file mode 100644 index 0000000..cad53d5 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/LinearRamp.java @@ -0,0 +1,94 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitVariablePort; + +/** + * Output approaches Input linearly. + * <P> + * When you change the value of the input port, the ramp will start changing from its current output + * value toward the value of input. An internal phase value will go from 0.0 to 1.0 at a rate + * controlled by time. When the internal phase reaches 1.0, the output will equal input. + * <P> + * + * @author (C) 1997 Phil Burk, SoftSynth.com + * @see ExponentialRamp + * @see AsymptoticRamp + * @see ContinuousRamp + */ +public class LinearRamp extends UnitFilter { + /** Time in seconds to get to the input value. */ + public UnitInputPort time; + public UnitVariablePort current; + + private double source; + private double phase; + private double target; + private double timeHeld = 0.0; + private double rate = 1.0; + + public LinearRamp() { + addPort(time = new UnitInputPort("Time")); + addPort(current = new UnitVariablePort("Current")); + } + + @Override + public void generate(int start, int limit) { + double[] outputs = output.getValues(); + double currentInput = input.getValues()[0]; + double currentValue = current.getValue(); + + // If input has changed, start new segment. + // Equality check is OK because we set them exactly equal below. + if (currentInput != target) + { + source = currentValue; + phase = 0.0; + target = currentInput; + } + + if (currentValue == target) { + // at end of ramp + for (int i = start; i < limit; i++) { + outputs[i] = currentValue; + } + } else { + // in middle of ramp + double currentTime = time.getValues()[0]; + // Has time changed? + if (currentTime != timeHeld) { + rate = convertTimeToRate(currentTime); + timeHeld = currentTime; + } + + for (int i = start; i < limit; i++) { + if (phase < 1.0) { + /* Interpolate current. */ + currentValue = source + (phase * (target - source)); + phase += rate; + } else { + currentValue = target; + } + outputs[i] = currentValue; + } + } + + current.setValue(currentValue); + } +} diff --git a/src/main/java/com/jsyn/unitgen/Maximum.java b/src/main/java/com/jsyn/unitgen/Maximum.java new file mode 100644 index 0000000..296e5da --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Maximum.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.unitgen; + +/** + * + Output largest of inputA or inputB. + * + * <pre> + * output = (inputA > InputB) ? inputA : InputB; + * </pre> + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @version 016 + * @see Minimum + */ +public class Maximum extends UnitBinaryOperator { + @Override + public void generate(int start, int limit) { + double[] aValues = inputA.getValues(); + double[] bValues = inputB.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + outputs[i] = (aValues[i] > bValues[i]) ? aValues[i] : bValues[i]; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/Minimum.java b/src/main/java/com/jsyn/unitgen/Minimum.java new file mode 100644 index 0000000..046387e --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Minimum.java @@ -0,0 +1,43 @@ +/* + * 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.unitgen; + +/** + * + Output smallest of inputA or inputB. + * + * <pre> + * output = (inputA < InputB) ? inputA : InputB; + * </pre> + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @version 016 + * @see Maximum + */ +public class Minimum extends UnitBinaryOperator { + + @Override + public void generate(int start, int limit) { + double[] aValues = inputA.getValues(); + double[] bValues = inputB.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + outputs[i] = (aValues[i] < bValues[i]) ? aValues[i] : bValues[i]; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/MixerMono.java b/src/main/java/com/jsyn/unitgen/MixerMono.java new file mode 100644 index 0000000..f4c7d7d --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/MixerMono.java @@ -0,0 +1,77 @@ +/* + * Copyright 2014 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * Multi-channel mixer with mono output and master amplitude. + * + * @author Phil Burk (C) 2014 Mobileer Inc + * @see MixerMonoRamped + * @see MixerStereo + */ +public class MixerMono extends UnitGenerator implements UnitSink, UnitSource { + public UnitInputPort input; + /** + * Linear gain for the corresponding input. + */ + public UnitInputPort gain; + /** + * Master gain control. + */ + public UnitInputPort amplitude; + public UnitOutputPort output; + + public MixerMono(int numInputs) { + addPort(input = new UnitInputPort(numInputs, "Input")); + addPort(gain = new UnitInputPort(numInputs, "Gain", 1.0)); + addPort(amplitude = new UnitInputPort("Amplitude", 1.0)); + addPort(output = new UnitOutputPort(getNumOutputs(), "Output")); + } + + public int getNumOutputs() { + return 1; + } + + @Override + public void generate(int start, int limit) { + double[] amplitudes = amplitude.getValues(0); + double[] outputs = output.getValues(0); + for (int i = start; i < limit; i++) { + double sum = 0; + for (int n = 0; n < input.getNumParts(); n++) { + double[] inputs = input.getValues(n); + double[] gains = gain.getValues(n); + sum += inputs[i] * gains[i]; + } + outputs[i] = sum * amplitudes[i]; + } + } + + @Override + public UnitInputPort getInput() { + return input; + } + + @Override + public UnitOutputPort getOutput() { + return output; + } + +} diff --git a/src/main/java/com/jsyn/unitgen/MixerMonoRamped.java b/src/main/java/com/jsyn/unitgen/MixerMonoRamped.java new file mode 100644 index 0000000..30f5342 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/MixerMonoRamped.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014 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.unitgen; + +/** + * Similar to MixerMono but the gain and amplitude ports are smoothed using short linear ramps. So + * you can control them with knobs and not hear any zipper noise. + * + * @author Phil Burk (C) 2014 Mobileer Inc + */ +public class MixerMonoRamped extends MixerMono { + private Unzipper[] unzippers; + private Unzipper amplitudeUnzipper; + + public MixerMonoRamped(int numInputs) { + super(numInputs); + unzippers = new Unzipper[numInputs]; + for (int i = 0; i < numInputs; i++) { + unzippers[i] = new Unzipper(); + } + amplitudeUnzipper = new Unzipper(); + } + + @Override + public void generate(int start, int limit) { + double[] amplitudes = amplitude.getValues(0); + double[] outputs = output.getValues(0); + for (int i = start; i < limit; i++) { + double sum = 0; + for (int n = 0; n < input.getNumParts(); n++) { + double[] inputs = input.getValues(n); + double[] gains = gain.getValues(n); + double smoothGain = unzippers[n].smooth(gains[i]); + sum += inputs[i] * smoothGain; + } + outputs[i] = sum * amplitudeUnzipper.smooth(amplitudes[i]); + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/MixerStereo.java b/src/main/java/com/jsyn/unitgen/MixerStereo.java new file mode 100644 index 0000000..218546e --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/MixerStereo.java @@ -0,0 +1,94 @@ +/* + * Copyright 2014 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * Mixer with monophonic inputs and two channels of output. Each signal can be panned left or right + * using an equal power curve. The "left" signal will be on output part zero. The "right" signal + * will be on output part one. + * + * @author Phil Burk (C) 2014 Mobileer Inc + * @see MixerMono + * @see MixerStereoRamped + */ +public class MixerStereo extends MixerMono { + /** + * Set to -1.0 for all left channel, 0.0 for center, or +1.0 for all right. Or anywhere in + * between. + */ + public UnitInputPort pan; + protected PanTracker[] panTrackers; + + static class PanTracker { + double previousPan = Double.MAX_VALUE; // so we update immediately + double leftGain; + double rightGain; + + public void update(double pan) { + if (pan != previousPan) { + // fastSine range is -1.0 to +1.0 for full cycle. + // We want a quarter cycle. So map -1.0 to +1.0 into 0.0 to 0.5 + double phase = pan * 0.25 + 0.25; + leftGain = SineOscillator.fastSin(0.5 - phase); + rightGain = SineOscillator.fastSin(phase); + previousPan = pan; + } + } + } + + public MixerStereo(int numInputs) { + super(numInputs); + addPort(pan = new UnitInputPort(numInputs, "Pan")); + pan.setup(-1.0, 0.0, 1.0); + panTrackers = new PanTracker[numInputs]; + for (int i = 0; i < numInputs; i++) { + panTrackers[i] = new PanTracker(); + } + } + + @Override + public int getNumOutputs() { + return 2; + } + + @Override + public void generate(int start, int limit) { + double[] amplitudes = amplitude.getValues(0); + double[] outputs0 = output.getValues(0); + double[] outputs1 = output.getValues(1); + for (int i = start; i < limit; i++) { + double sum0 = 0.0; + double sum1 = 0.0; + for (int n = 0; n < input.getNumParts(); n++) { + double[] inputs = input.getValues(n); + double[] gains = gain.getValues(n); + double[] pans = pan.getValues(n); + PanTracker panTracker = panTrackers[n]; + panTracker.update(pans[i]); + double scaledInput = inputs[i] * gains[i]; + sum0 += scaledInput * panTracker.leftGain; + sum1 += scaledInput * panTracker.rightGain; + } + double amp = amplitudes[i]; + outputs0[i] = sum0 * amp; + outputs1[i] = sum1 * amp; + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/MixerStereoRamped.java b/src/main/java/com/jsyn/unitgen/MixerStereoRamped.java new file mode 100644 index 0000000..6f3bfcc --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/MixerStereoRamped.java @@ -0,0 +1,71 @@ +/* + * Copyright 2014 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.unitgen; + +/** + * Similar to MixerStereo but the gain, pan and amplitude ports are smoothed using short linear + * ramps. So you can control them with knobs and not hear any zipper noise. + * + * @author Phil Burk (C) 2014 Mobileer Inc + */ +public class MixerStereoRamped extends MixerStereo { + private Unzipper[] gainUnzippers; + private Unzipper[] panUnzippers; + private Unzipper amplitudeUnzipper; + + public MixerStereoRamped(int numInputs) { + super(numInputs); + gainUnzippers = new Unzipper[numInputs]; + for (int i = 0; i < numInputs; i++) { + gainUnzippers[i] = new Unzipper(); + } + panUnzippers = new Unzipper[numInputs]; + for (int i = 0; i < numInputs; i++) { + panUnzippers[i] = new Unzipper(); + } + amplitudeUnzipper = new Unzipper(); + } + + @Override + public void generate(int start, int limit) { + double[] amplitudes = amplitude.getValues(0); + double[] outputs0 = output.getValues(0); + double[] outputs1 = output.getValues(1); + for (int i = start; i < limit; i++) { + double sum0 = 0; + double sum1 = 0; + for (int n = 0; n < input.getNumParts(); n++) { + double[] inputs = input.getValues(n); + double[] gains = gain.getValues(n); + double[] pans = pan.getValues(n); + + PanTracker panTracker = panTrackers[n]; + double smoothPan = panUnzippers[n].smooth(pans[i]); + panTracker.update(smoothPan); + + double smoothGain = gainUnzippers[n].smooth(gains[i]); + double scaledInput = inputs[i] * smoothGain; + sum0 += scaledInput * panTracker.leftGain; + sum1 += scaledInput * panTracker.rightGain; + } + double amp = amplitudeUnzipper.smooth(amplitudes[i]); + outputs0[i] = sum0 * amp; + outputs1[i] = sum1 * amp; + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/MonoStreamWriter.java b/src/main/java/com/jsyn/unitgen/MonoStreamWriter.java new file mode 100644 index 0000000..0fb6f40 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/MonoStreamWriter.java @@ -0,0 +1,49 @@ +/* + * 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.unitgen; + +import java.io.IOException; + +import com.jsyn.io.AudioOutputStream; +import com.jsyn.ports.UnitInputPort; + +/** + * Write one sample per audio frame to an AudioOutputStream with no interpolation. + * + * Note that you must call start() on this unit because it does not have an output for pulling data. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class MonoStreamWriter extends UnitStreamWriter { + public MonoStreamWriter() { + addPort(input = new UnitInputPort("Input")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + AudioOutputStream output = outputStream; + if (output != null) { + int count = limit - start; + try { + output.write(inputs, start, count); + } catch (IOException ignored) { + } + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/MorphingOscillatorBL.java b/src/main/java/com/jsyn/unitgen/MorphingOscillatorBL.java new file mode 100644 index 0000000..7ca440d --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/MorphingOscillatorBL.java @@ -0,0 +1,72 @@ +/* + * 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.unitgen; + +import com.jsyn.engine.MultiTable; +import com.jsyn.ports.UnitInputPort; + +/** + * Oscillator that can change its shape from sine to sawtooth to pulse. + * + * @author Phil Burk (C) 2016 Mobileer Inc + */ +public class MorphingOscillatorBL extends PulseOscillatorBL { + /** + * Controls the shape of the waveform. + * The shape varies continuously from a sine wave at -1.0, + * to a sawtooth at 0.0 to a pulse wave at 1.0. + */ + public UnitInputPort shape; + + public MorphingOscillatorBL() { + addPort(shape = new UnitInputPort("Shape")); + shape.setMinimum(-1.0); + shape.setMaximum(1.0); + } + + @Override + protected double generateBL(MultiTable multiTable, double currentPhase, + double positivePhaseIncrement, double flevel, int i) { + double[] shapes = shape.getValues(); + double shape = shapes[i]; + + if (shape < 0.0) { + // Squeeze flevel towards the pure sine table. + flevel += flevel * shape; + return multiTable.calculateSawtooth(currentPhase, positivePhaseIncrement, flevel); + } else { + double[] widths = width.getValues(); + double width = widths[i]; + width = (width > 0.999) ? 0.999 : ((width < -0.999) ? -0.999 : width); + + double val1 = multiTable.calculateSawtooth(currentPhase, positivePhaseIncrement, flevel); + // Generate second sawtooth so we can add them together. + double phase2 = currentPhase + 1.0 - width; // 180 degrees out of phase + if (phase2 >= 1.0) { + phase2 -= 2.0; + } + double val2 = multiTable.calculateSawtooth(phase2, positivePhaseIncrement, flevel); + + /* + * Need to adjust amplitude based on positive phaseInc. little less than half at + * Nyquist/2.0! + */ + double scale = 1.0 - positivePhaseIncrement; + return scale * (val1 - ((val2 + width) * shape)); // apply shape morphing + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/MultiPassThrough.java b/src/main/java/com/jsyn/unitgen/MultiPassThrough.java new file mode 100644 index 0000000..9125fc3 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/MultiPassThrough.java @@ -0,0 +1,70 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * Pass the input through to the output unchanged. This is often used for distributing a signal to + * multiple ports inside a circuit. It can also be used as a summing node, in other words, a mixer. + * + * This is just like PassThrough except the input and output ports have multiple parts. + * The default is two parts, ie. stereo. + * + * @author Phil Burk (C) 2016 Mobileer Inc + * @see Circuit + * @see PassThrough + */ +public class MultiPassThrough extends UnitGenerator implements UnitSink, UnitSource { + public UnitInputPort input; + public UnitOutputPort output; + private final int mNumParts; + + /* Define Unit Ports used by connect() and set(). */ + public MultiPassThrough(int numParts) { + mNumParts = numParts; + addPort(input = new UnitInputPort(numParts, "Input")); + addPort(output = new UnitOutputPort(numParts, "Output")); + } + + public MultiPassThrough() { + this(2); // stereo + } + + @Override + public UnitInputPort getInput() { + return input; + } + + @Override + public UnitOutputPort getOutput() { + return output; + } + + @Override + public void generate(int start, int limit) { + for (int partIndex = 0; partIndex < mNumParts; partIndex++) { + double[] inputs = input.getValues(partIndex); + double[] outputs = output.getValues(partIndex); + + for (int i = start; i < limit; i++) { + outputs[i] = inputs[i]; + } + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/Multiply.java b/src/main/java/com/jsyn/unitgen/Multiply.java new file mode 100644 index 0000000..ded7646 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Multiply.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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * This unit multiplies its two inputs. <br> + * + * <pre> + * output = inputA * inputB + * </pre> + * + * <br> + * Note that some units have an amplitude port, which controls an internal multiply. So you may not + * need this unit. + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @version 016 + * @see MultiplyAdd + * @see Subtract + */ +public class Multiply extends UnitBinaryOperator { + public Multiply() { + } + + /** Connect a to inputA and b to inputB. */ + public Multiply(UnitOutputPort a, UnitOutputPort b) { + a.connect(inputA); + b.connect(inputB); + } + + /** Connect a to inputA and b to inputB and connect output to c. */ + public Multiply(UnitOutputPort a, UnitOutputPort b, UnitInputPort c) { + this(a, b); + output.connect(c); + } + + @Override + public void generate(int start, int limit) { + double[] aValues = inputA.getValues(); + double[] bValues = inputB.getValues(); + double[] outputs = output.getValues(); + for (int i = start; i < limit; i++) { + outputs[i] = aValues[i] * bValues[i]; + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/MultiplyAdd.java b/src/main/java/com/jsyn/unitgen/MultiplyAdd.java new file mode 100644 index 0000000..adbee6c --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/MultiplyAdd.java @@ -0,0 +1,57 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * <pre> + * output = (inputA * inputB) + inputC + * </pre> + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @version 016 + * @see Multiply + * @see Add + */ +public class MultiplyAdd extends UnitGenerator { + public UnitInputPort inputA; + public UnitInputPort inputB; + public UnitInputPort inputC; + public UnitOutputPort output; + + /* Define Unit Ports used by connect() and set(). */ + public MultiplyAdd() { + addPort(inputA = new UnitInputPort("InputA")); + addPort(inputB = new UnitInputPort("InputB")); + addPort(inputC = new UnitInputPort("InputC")); + addPort(output = new UnitOutputPort("Output")); + } + + @Override + public void generate(int start, int limit) { + double[] aValues = inputA.getValues(); + double[] bValues = inputB.getValues(); + double[] cValues = inputC.getValues(); + double[] outputs = output.getValues(); + for (int i = start; i < limit; i++) { + outputs[i] = (aValues[i] * bValues[i]) + cValues[i]; + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/Pan.java b/src/main/java/com/jsyn/unitgen/Pan.java new file mode 100644 index 0000000..bc90984 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Pan.java @@ -0,0 +1,64 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * Pan unit. The profile is constant amplitude and not constant energy. + * <P> + * Takes an input and pans it between two outputs based on value of pan. When pan is -1, output[0] + * is input, and output[1] is zero. When pan is 0, output[0] and output[1] are both input/2. When + * pan is +1, output[0] is zero, and output[1] is input. + * <P> + * + * @author (C) 1997 Phil Burk, SoftSynth.com + * @see Select + */ +public class Pan extends UnitGenerator { + public UnitInputPort input; + /** + * Pan control varies from -1.0 for full left to +1.0 for full right. Set to 0.0 for center. + */ + public UnitInputPort pan; + public UnitOutputPort output; + + public Pan() { + addPort(input = new UnitInputPort("Input")); + addPort(pan = new UnitInputPort("Pan")); + addPort(output = new UnitOutputPort(2, "Output")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] panPtr = pan.getValues(); + double[] outputs_0 = output.getValues(0); + double[] outputs_1 = output.getValues(1); + + for (int i = start; i < limit; i++) { + double gainB = (panPtr[i] * 0.5) + 0.5; /* + * Scale and offset to 0.0 to 1.0 + */ + double gainA = 1.0 - gainB; + double inVal = inputs[i]; + outputs_0[i] = inVal * gainA; + outputs_1[i] = inVal * gainB; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/PanControl.java b/src/main/java/com/jsyn/unitgen/PanControl.java new file mode 100644 index 0000000..63bddd8 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/PanControl.java @@ -0,0 +1,61 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * PanControl unit. + * <P> + * Generates control signals that can be used to control a mixer or the amplitude ports of two + * units. + * + * <PRE> + * temp = (pan * 0.5) + 0.5; + * output[0] = temp; + * output[1] = 1.0 - temp; + * </PRE> + * <P> + * + * @author (C) 1997-2009 Phil Burk, SoftSynth.com + */ +public class PanControl extends UnitGenerator { + public UnitInputPort pan; + public UnitOutputPort output; + + /* Define Unit Ports used by connect() and set(). */ + public PanControl() { + addPort(pan = new UnitInputPort("Pan")); + addPort(output = new UnitOutputPort(2, "Output", 0.0)); + } + + @Override + public void generate(int start, int limit) { + double[] panPtr = pan.getValues(); + double[] output0s = output.getValues(0); + double[] output1s = output.getValues(1); + + for (int i = start; i < limit; i++) { + double gainB = (panPtr[i] * 0.5) + 0.5; /* + * Scale and offset to 0.0 to 1.0 + */ + output0s[i] = 1.0 - gainB; + output1s[i] = gainB; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/ParabolicEnvelope.java b/src/main/java/com/jsyn/unitgen/ParabolicEnvelope.java new file mode 100644 index 0000000..6de97d9 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/ParabolicEnvelope.java @@ -0,0 +1,110 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * ParabolicEnvelope unit. Output goes from zero to amplitude then back to zero in a parabolic arc. + * <P> + * Generate a short parabolic envelope that could be used for granular synthesis. The output starts + * at zero, peaks at the value of amplitude then returns to zero. This unit has two states, IDLE and + * RUNNING. If a trigger is received when IDLE, the envelope is started and another trigger is sent + * out the triggerOutput port. This triggerOutput can be used to latch values for the synthesis of a + * grain. If a trigger is received when RUNNING, then it is ignored and passed out the triggerPass + * port. The triggerPass can be connected to the triggerInput of another ParabolicEnvelope. Thus you + * can implement a simple grain allocation scheme by daisy chaining the triggers of + * ParabolicEnvelopes. + * <P> + * The envelope is generated by a double integrator method so it uses relatively little CPU time. + * + * @author (C) 1997 Phil Burk, SoftSynth.com + * @see EnvelopeDAHDSR + */ +public class ParabolicEnvelope extends UnitGenerator { + + /** Fastest repeat rate of envelope if it were continually retriggered in Hertz. */ + public UnitInputPort frequency; + /** True value triggers envelope when in resting state. */ + public UnitInputPort triggerInput; + public UnitInputPort amplitude; + + /** Trigger output when envelope started. */ + public UnitOutputPort triggerOutput; + /** Input trigger passed out if ignored for daisy chaining. */ + public UnitOutputPort triggerPass; + public UnitOutputPort output; + + private double slope; + private double curve; + private double level; + private boolean running; + + /* Define Unit Ports used by connect() and set(). */ + public ParabolicEnvelope() { + addPort(triggerInput = new UnitInputPort("Input")); + addPort(frequency = new UnitInputPort("Frequency", UnitOscillator.DEFAULT_FREQUENCY)); + addPort(amplitude = new UnitInputPort("Amplitude", UnitOscillator.DEFAULT_AMPLITUDE)); + + addPort(output = new UnitOutputPort("Output")); + addPort(triggerOutput = new UnitOutputPort("TriggerOutput")); + addPort(triggerPass = new UnitOutputPort("TriggerPass")); + } + + @Override + public void generate(int start, int limit) { + double[] frequencies = frequency.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] triggerInputs = triggerInput.getValues(); + double[] outputs = output.getValues(); + double[] triggerPasses = triggerPass.getValues(); + double[] triggerOutputs = triggerOutput.getValues(); + + for (int i = start; i < limit; i++) { + if (!running) { + if (triggerInputs[i] > 0) { + double freq = frequencies[i] * synthesisEngine.getInverseNyquist(); + freq = (freq > 1.0) ? 1.0 : ((freq < -1.0) ? -1.0 : freq); + double ampl = amplitudes[i]; + double freq2 = freq * freq; /* Square frequency. */ + slope = 4.0 * ampl * (freq - freq2); + curve = -8.0 * ampl * freq2; + level = 0.0; + triggerOutputs[i] = UnitGenerator.TRUE; + running = true; + } else { + triggerOutputs[i] = UnitGenerator.FALSE; + } + triggerPasses[i] = UnitGenerator.FALSE; + } else /* RUNNING */ + { + level += slope; + slope += curve; + if (level <= 0.0) { + level = 0.0; + running = false; + /* Autostop? - FIXME */ + } + + triggerOutputs[i] = UnitGenerator.FALSE; + triggerPasses[i] = triggerInputs[i]; + } + outputs[i] = level; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/PassThrough.java b/src/main/java/com/jsyn/unitgen/PassThrough.java new file mode 100644 index 0000000..8ac0b93 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/PassThrough.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.unitgen; + +/** + * Pass the input through to the output unchanged. This is often used for distributing a signal to + * multiple ports inside a circuit. It can also be used as a summing node, in other words, a mixer. + * + * @author Phil Burk (C) 2011 Mobileer Inc + * @see Circuit + * @see MultiPassThrough + */ +public class PassThrough extends UnitFilter { + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + outputs[i] = inputs[i]; + } + + } +} diff --git a/src/main/java/com/jsyn/unitgen/PeakFollower.java b/src/main/java/com/jsyn/unitgen/PeakFollower.java new file mode 100644 index 0000000..7bf0508 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/PeakFollower.java @@ -0,0 +1,87 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.ports.UnitVariablePort; + +/** + * Tracks the peaks of an input signal. This can be used to monitor the overall amplitude of a + * signal. The output can be used to drive color organs, vocoders, VUmeters, etc. Output drops + * exponentially when the input drops below the current output level. The output approaches zero + * based on the value on the halfLife port. + * + * @author (C) 1997-2009 Phil Burk, SoftSynth.com + */ +public class PeakFollower extends UnitGenerator { + public UnitInputPort input; + public UnitVariablePort current; + public UnitInputPort halfLife; + public UnitOutputPort output; + + private double previousHalfLife = -1.0; + private double decayScalar = 0.99; + + /* Define Unit Ports used by connect() and set(). */ + public PeakFollower() { + addPort(input = new UnitInputPort("Input")); + addPort(halfLife = new UnitInputPort(1, "HalfLife", 0.1)); + addPort(current = new UnitVariablePort("Current")); + addPort(output = new UnitOutputPort("Output")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + double currentHalfLife = halfLife.getValues()[0]; + double currentValue = current.getValue(); + + if (currentHalfLife != previousHalfLife) { + decayScalar = this.convertHalfLifeToMultiplier(currentHalfLife); + previousHalfLife = currentHalfLife; + } + + double scalar = 1.0 - decayScalar; + + for (int i = start; i < limit; i++) { + double inputValue = inputs[i]; + if (inputValue < 0.0) { + inputValue = -inputValue; // absolute value + } + + if (inputValue >= currentValue) { + currentValue = inputValue; + } else { + currentValue = currentValue * scalar; + } + + outputs[i] = currentValue; + } + + /* + * When current gets close to zero, set current to zero to prevent FP underflow, which can + * cause a severe performance degradation in 'C'. + */ + if (currentValue < VERY_SMALL_FLOAT) { + currentValue = 0.0; + } + + current.setValue(currentValue); + } +} diff --git a/src/main/java/com/jsyn/unitgen/PhaseShifter.java b/src/main/java/com/jsyn/unitgen/PhaseShifter.java new file mode 100644 index 0000000..4b17245 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/PhaseShifter.java @@ -0,0 +1,90 @@ +/* + * Copyright 2014 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * PhaseShifter effects processor. This unit emulates a common guitar pedal effect but without the + * LFO modulation. You can use your own modulation source connected to the "offset" port. Different + * frequencies are phase shifted varying amounts using a series of AllPass filters. By feeding the + * output back to the input we can get varying phase cancellation. This implementation was based on + * code posted to the music-dsp archive by Ross Bencina. http://www.musicdsp.org/files/phaser.cpp + * + * @author (C) 2014 Phil Burk, Mobileer Inc + * @see FilterLowPass + * @see FilterAllPass + * @see RangeConverter + */ + +public class PhaseShifter extends UnitFilter { + /** + * Connect an oscillator to this port to sweep the phase. A range of 0.05 to 0.4 is a good + * start. + */ + public UnitInputPort offset; + public UnitInputPort feedback; + public UnitInputPort depth; + + private double zm1; + private double[] xs; + private double[] ys; + + public PhaseShifter() { + this(6); + } + + public PhaseShifter(int numStages) { + addPort(offset = new UnitInputPort("Offset", 0.1)); + addPort(feedback = new UnitInputPort("Feedback", 0.7)); + addPort(depth = new UnitInputPort("Depth", 1.0)); + + xs = new double[numStages]; + ys = new double[numStages]; + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + double[] feedbacks = feedback.getValues(); + double[] depths = depth.getValues(); + double[] offsets = offset.getValues(); + double gain; + + for (int i = start; i < limit; i++) { + // Support audio rate modulation. + double currentOffset = offsets[i]; + + // Prevent gain from exceeding 1.0. + gain = 1.0 - (currentOffset * currentOffset); + if (gain < -1.0) { + gain = -1.0; + } + + double x = inputs[i] + (zm1 * feedbacks[i]); + // Cascaded all-pass filters. + for (int stage = 0; stage < xs.length; stage++) { + double temp = ys[stage] = (gain * (ys[stage] - x)) + xs[stage]; + xs[stage] = x; + x = temp; + } + zm1 = x; + outputs[i] = inputs[i] + (x * depths[i]); + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/PinkNoise.java b/src/main/java/com/jsyn/unitgen/PinkNoise.java new file mode 100644 index 0000000..84aa2f2 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/PinkNoise.java @@ -0,0 +1,128 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.util.PseudoRandom; + +/** + * Random output with 3dB per octave rolloff providing a soft natural noise sound. Generated using + * Gardner method. Optimization suggested by James McCartney uses a tree to select which random + * value to replace. + * + * <pre> + * x x x x x x x x x x x x x x x x + * x x x x x x x x + * x x x x + * x x + * x + * </pre> + * + * Tree is generated by counting trailing zeros in an increasing index. When the index is zero, no + * random number is selected. Author: Phil Burk (C) 1996 SoftSynth.com. + */ + +public class PinkNoise extends UnitGenerator implements UnitSource { + + public UnitInputPort amplitude; + public UnitOutputPort output; + + private final int NUM_ROWS = 16; + private final int RANDOM_BITS = 24; + private final int RANDOM_SHIFT = 32 - RANDOM_BITS; + + private PseudoRandom randomNum; + protected double prevNoise, currNoise; + + private long[] rows = new long[NUM_ROWS]; // NEXT RANDOM UNSIGNED 32 + private double scalar; // used to scale within range of -1.0 to +1.0 + private int runningSum; // used to optimize summing of generators + private int index; // incremented with each sample + private int indexMask; // index wrapped and ANDing with this mask + + /* Define Unit Ports used by connect() and set(). */ + public PinkNoise() { + addPort(amplitude = new UnitInputPort("Amplitude", UnitOscillator.DEFAULT_AMPLITUDE)); + addPort(output = new UnitOutputPort("Output")); + + randomNum = new PseudoRandom(); + + // set up for N rows of generators + index = 0; + indexMask = (1 << NUM_ROWS) - 1; + + // Calculate maximum possible signed random value. Extra 1 for white + // noise always added. + int pmax = (NUM_ROWS + 1) * (1 << (RANDOM_BITS - 1)); + scalar = 1.0 / pmax; + + // initialize rows + for (int i = 0; i < NUM_ROWS; i++) { + rows[i] = 0; + } + + runningSum = 0; + } + + @Override + public void generate(int start, int limit) { + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + outputs[i] = generatePinkNoise() * amplitudes[i]; + } + } + + public double generatePinkNoise() { + index = (index + 1) & indexMask; + + // If index is zero, don't update any random values. + if (index != 0) { + // Determine how many trailing zeros in PinkIndex. + // This algorithm will hang of n==0 so test first + int numZeros = 0; + int n = index; + + while ((n & 1) == 0) { + n = n >> 1; + numZeros++; + } + + // Replace the indexed ROWS random value. + // Subtract and add back to RunningSum instead of adding all the + // random values together. Only one changes each time. + runningSum -= rows[numZeros]; + int newRandom = randomNum.nextRandomInteger() >> RANDOM_SHIFT; + runningSum += newRandom; + rows[numZeros] = newRandom; + } + + // Add extra white noise value. + int newRandom = randomNum.nextRandomInteger() >> RANDOM_SHIFT; + int sum = runningSum + newRandom; + + // Scale to range of -1.0 to 0.9999. + return scalar * sum; + } + + @Override + public UnitOutputPort getOutput() { + return output; + } +} diff --git a/src/main/java/com/jsyn/unitgen/PitchDetector.java b/src/main/java/com/jsyn/unitgen/PitchDetector.java new file mode 100644 index 0000000..ff44c93 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/PitchDetector.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.util.AutoCorrelator; +import com.jsyn.util.SignalCorrelator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Estimate the fundamental frequency of a monophonic signal. Analyzes an input signal and outputs + * an estimated period in frames and a frequency in Hertz. The frequency is frameRate/period. The + * confidence tells you how accurate the estimate is. When the confidence is low, you should ignore + * the period. You can use a CompareUnit and a LatchUnit to hold values that you are confident of. + * <P> + * Note that a stable monophonic signal is required for accurate pitch tracking. + * + * @author (C) 2012 Phil Burk, Mobileer Inc + */ +public class PitchDetector extends UnitGenerator { + + private static final Logger LOGGER = LoggerFactory.getLogger(PitchDetector.class); + + public UnitInputPort input; + + public UnitOutputPort period; + public UnitOutputPort confidence; + public UnitOutputPort frequency; + public UnitOutputPort updated; + + protected SignalCorrelator signalCorrelator; + + private double lastFrequency = 440.0; + private double lastPeriod = 44100.0 / lastFrequency; // result of analysis TODO update for 48000 + private double lastConfidence = 0.0; // Measure of confidence in the result. + + private static final int LOWEST_FREQUENCY = 40; + private static final int HIGHEST_RATE = 48000; + private static final int CYCLES_NEEDED = 2; + + public PitchDetector() { + super(); + addPort(input = new UnitInputPort("Input")); + + addPort(period = new UnitOutputPort("Period")); + addPort(confidence = new UnitOutputPort("Confidence")); + addPort(frequency = new UnitOutputPort("Frequency")); + addPort(updated = new UnitOutputPort("Updated")); + signalCorrelator = createSignalCorrelator(); + } + + public SignalCorrelator createSignalCorrelator() { + int framesNeeded = HIGHEST_RATE * CYCLES_NEEDED / LOWEST_FREQUENCY; + return new AutoCorrelator(framesNeeded); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] periods = period.getValues(); + double[] confidences = confidence.getValues(); + double[] frequencies = frequency.getValues(); + double[] updateds = updated.getValues(); + + for (int i = start; i < limit; i++) { + double current = inputs[i]; + if (signalCorrelator.addSample(current)) { + lastPeriod = signalCorrelator.getPeriod(); + if (lastPeriod < 0.1) { + LOGGER.debug("ILLEGAL PERIOD"); + } + double currentFrequency = getFrameRate() / (lastPeriod + 0); + double confidence = signalCorrelator.getConfidence(); + if (confidence > 0.1) { + if (true) { + double coefficient = confidence * 0.2; + // Take weighted average with previous frequency. + lastFrequency = ((lastFrequency * (1.0 - coefficient)) + (currentFrequency * coefficient)); + } else { + lastFrequency = ((lastFrequency * lastConfidence) + (currentFrequency * confidence)) + / (lastConfidence + confidence); + } + } + lastConfidence = confidence; + updateds[i] = 1.0; + } else { + updateds[i] = 0.0; + } + periods[i] = lastPeriod; + confidences[i] = lastConfidence; + frequencies[i] = lastFrequency; + } + } + + /** + * For debugging only. + * + * @return internal array of correlation results. + */ + public float[] getDiffs() { + return signalCorrelator.getDiffs(); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/PitchToFrequency.java b/src/main/java/com/jsyn/unitgen/PitchToFrequency.java new file mode 100644 index 0000000..9086749 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/PitchToFrequency.java @@ -0,0 +1,26 @@ +package com.jsyn.unitgen; + +import com.softsynth.math.AudioMath; + +public class PitchToFrequency extends PowerOfTwo { + + public PitchToFrequency() { + input.setup(0.0, 60.0, 127.0); + } + + /** + * Convert from MIDI pitch to an octave offset from Concert A. + */ + @Override + public double adjustInput(double in) { + return (in - AudioMath.CONCERT_A_PITCH) * (1.0/12.0); + } + + /** + * Convert scaler to a frequency relative to Concert A. + */ + @Override + public double adjustOutput(double out) { + return out * AudioMath.getConcertAFrequency(); + } +} diff --git a/src/main/java/com/jsyn/unitgen/PowerOfTwo.java b/src/main/java/com/jsyn/unitgen/PowerOfTwo.java new file mode 100644 index 0000000..5916860 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/PowerOfTwo.java @@ -0,0 +1,108 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * output = (2.0^input) This is useful for converting a pitch modulation value into a frequency + * scaler. An input value of +1.0 will output 2.0 for an octave increase. An input value of -1.0 + * will output 0.5 for an octave decrease. + * + * This implementation uses a table lookup to optimize for + * speed. It is accurate enough for tuning. It also checks to see if the current input value is the + * same as the previous input value. If so then it reuses the previous computed value. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class PowerOfTwo extends UnitGenerator { + /** + * Offset in octaves. + */ + public UnitInputPort input; + public UnitOutputPort output; + + private static double[] table; + private static final int NUM_VALUES = 2048; + // Cached computation. + private double lastInput = 0.0; + private double lastOutput = 1.0; + + static { + // Add guard point for faster interpolation. + // Add another point to handle inputs like -1.5308084989341915E-17, + // which generate indices above range. + table = new double[NUM_VALUES + 2]; + // Fill one octave of the table. + for (int i = 0; i < table.length; i++) { + double value = Math.pow(2.0, ((double) i) / NUM_VALUES); + table[i] = value; + } + } + + public PowerOfTwo() { + addPort(input = new UnitInputPort("Input")); + input.setup(-8.0, 0.0, 8.0); + addPort(output = new UnitOutputPort("Output")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + double in = inputs[i]; + // Can we reuse a previously computed value? + if (in == lastInput) { + outputs[i] = lastOutput; + } else { + lastInput = in; + double adjustedInput = adjustInput(in); + int octave = (int) Math.floor(adjustedInput); + double normal = adjustedInput - octave; + // Do table lookup. + double findex = normal * NUM_VALUES; + int index = (int) findex; + double fraction = findex - index; + double value = table[index] + (fraction * (table[index + 1] - table[index])); + + // Adjust for octave. + while (octave > 0) { + octave -= 1; + value *= 2.0; + } + while (octave < 0) { + octave += 1; + value *= 0.5; + } + double adjustedOutput = adjustOutput(value); + outputs[i] = adjustedOutput; + lastOutput = adjustedOutput; + } + } + } + + public double adjustInput(double in) { + return in; + } + + public double adjustOutput(double out) { + return out; + } +} diff --git a/src/main/java/com/jsyn/unitgen/PulseOscillator.java b/src/main/java/com/jsyn/unitgen/PulseOscillator.java new file mode 100644 index 0000000..5ac7352 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/PulseOscillator.java @@ -0,0 +1,59 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * Simple pulse wave oscillator. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class PulseOscillator extends UnitOscillator { + /** + * Pulse width varies from -1.0 to +1.0. At 0.0 the pulse is actually a square wave. + */ + public UnitInputPort width; + + public PulseOscillator() { + addPort(width = new UnitInputPort("Width")); + } + + @Override + public void generate(int start, int limit) { + double[] frequencies = frequency.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] widths = width.getValues(); + double[] outputs = output.getValues(); + + // Variables have a single value. + double currentPhase = phase.getValue(); + + for (int i = start; i < limit; i++) { + // Generate sawtooth phaser to provide phase for pulse generation. + double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]); + currentPhase = incrementWrapPhase(currentPhase, phaseIncrement); + double ampl = amplitudes[i]; + // Either full negative or positive amplitude. + outputs[i] = (currentPhase < widths[i]) ? -ampl : ampl; + } + + // Value needs to be saved for next time. + phase.setValue(currentPhase); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/PulseOscillatorBL.java b/src/main/java/com/jsyn/unitgen/PulseOscillatorBL.java new file mode 100644 index 0000000..c0e234c --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/PulseOscillatorBL.java @@ -0,0 +1,61 @@ +/* + * 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.unitgen; + +import com.jsyn.engine.MultiTable; +import com.jsyn.ports.UnitInputPort; + +/** + * Pulse oscillator that uses two band limited sawtooth waveforms. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class PulseOscillatorBL extends SawtoothOscillatorBL { + /** Controls the duty cycle of the pulse waveform. + * The width varies from -1.0 to +1.0. + * When width is zero the output is a square wave. + */ + public UnitInputPort width; + + public PulseOscillatorBL() { + addPort(width = new UnitInputPort("Width")); + } + + @Override + protected double generateBL(MultiTable multiTable, double currentPhase, + double positivePhaseIncrement, double flevel, int i) { + double[] widths = width.getValues(); + double width = widths[i]; + width = (width > 0.999) ? 0.999 : ((width < -0.999) ? -0.999 : width); + + double val1 = multiTable.calculateSawtooth(currentPhase, positivePhaseIncrement, flevel); + + // Generate second sawtooth so we can add them together. + double phase2 = currentPhase + 1.0 - width; // 180 degrees out of phase + if (phase2 >= 1.0) { + phase2 -= 2.0; + } + double val2 = multiTable.calculateSawtooth(phase2, positivePhaseIncrement, flevel); + + /* + * Need to adjust amplitude based on positive phaseInc and width. little less than half at + * Nyquist/2.0! + */ + double scale = 1.0 - positivePhaseIncrement; + return scale * (val1 - val2 - width); + } +} diff --git a/src/main/java/com/jsyn/unitgen/RaisedCosineEnvelope.java b/src/main/java/com/jsyn/unitgen/RaisedCosineEnvelope.java new file mode 100644 index 0000000..c32417c --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/RaisedCosineEnvelope.java @@ -0,0 +1,73 @@ +/* + * 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.unitgen; + +/** + * An envelope that can be used in a GrainFarm to shape the amplitude of a Grain. The envelope + * starts at 0.0, rises to 1.0, then returns to 0.0 following a cosine curve. + * + * <pre> + * output = 0.5 - (0.5 * cos(phase)) + * </pre> + * + * @author Phil Burk (C) 2011 Mobileer Inc + * @see GrainFarm + */ +public class RaisedCosineEnvelope extends GrainCommon implements GrainEnvelope { + protected double phase; + protected double phaseIncrement; + + public RaisedCosineEnvelope() { + setFrameRate(44100); + setDuration(0.1); + } + + /** + * @return next value of the envelope. + */ + @Override + public double next() { + phase += phaseIncrement; + if (phase > (2.0 * Math.PI)) { + return 0.0; + } else { + return 0.5 - (0.5 * Math.cos(phase)); // TODO optimize using Taylor expansion + } + } + + /** + * @return true if there are more envelope values left. + */ + @Override + public boolean hasMoreValues() { + return (phase < (2.0 * Math.PI)); + } + + /** + * Reset the envelope back to the beginning. + */ + @Override + public void reset() { + phase = 0.0; + } + + @Override + public void setDuration(double duration) { + phaseIncrement = 2.0 * Math.PI / (getFrameRate() * duration); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/RangeConverter.java b/src/main/java/com/jsyn/unitgen/RangeConverter.java new file mode 100644 index 0000000..ae94b0f --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/RangeConverter.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * Convert an input signal between -1.0 and +1.0 to the range min to max. This is handy when using + * an oscillator as a modulator. + * + * @author (C) 2014 Phil Burk, Mobileer Inc + * @see EdgeDetector + */ +public class RangeConverter extends UnitFilter { + public UnitInputPort min; + public UnitInputPort max; + + /* Define Unit Ports used by connect() and set(). */ + public RangeConverter() { + addPort(min = new UnitInputPort("Min", 40.0)); + addPort(max = new UnitInputPort("Max", 2000.0)); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] mins = min.getValues(); + double[] maxs = max.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + double low = mins[i]; + outputs[i] = low + ((maxs[i] - low) * (inputs[i] + 1) * 0.5); + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/RectangularWindow.java b/src/main/java/com/jsyn/unitgen/RectangularWindow.java new file mode 100644 index 0000000..d61f763 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/RectangularWindow.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013 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.unitgen; + +import com.jsyn.data.SpectralWindow; + +/** + * Window that is just 1.0. Flat like a rectangle. + * + * @author Phil Burk (C) 2013 Mobileer Inc + * @see SpectralFFT + */ +public class RectangularWindow implements SpectralWindow { + static RectangularWindow instance = new RectangularWindow(); + + @Override + /** This always returns 1.0. Do not pass indices outside the window range. */ + public double get(int index) { + return 1.0; // impressive, eh? + } + + public static RectangularWindow getInstance() { + return instance; + } +} diff --git a/src/main/java/com/jsyn/unitgen/RedNoise.java b/src/main/java/com/jsyn/unitgen/RedNoise.java new file mode 100644 index 0000000..d3e4321 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/RedNoise.java @@ -0,0 +1,80 @@ +/* + * 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.unitgen; + +import com.jsyn.util.PseudoRandom; + +/** + * RedNoise unit. This unit interpolates straight line segments between pseudo-random numbers to + * produce "red" noise. It is a grittier alternative to the white generator WhiteNoise. It is also + * useful as a slowly changing random control generator for natural sounds. Frequency port controls + * the number of times per second that a new random number is chosen. + * + * @author (C) 1997 Phil Burk, SoftSynth.com + * @see WhiteNoise + */ +public class RedNoise extends UnitOscillator { + private PseudoRandom randomNum; + protected double prevNoise, currNoise; + + /* Define Unit Ports used by connect() and set(). */ + public RedNoise() { + super(); + randomNum = new PseudoRandom(); + } + + @Override + public void generate(int start, int limit) { + double[] amplitudes = amplitude.getValues(); + double[] frequencies = frequency.getValues(); + double[] outputs = output.getValues(); + double currPhase = phase.getValue(); + double phaseIncrement, currOutput; + + double framePeriod = getFramePeriod(); + + for (int i = start; i < limit; i++) { + // compute phase + phaseIncrement = frequencies[i] * framePeriod; + + // verify that phase is within minimums and is not negative + if (phaseIncrement < 0.0) { + phaseIncrement = 0.0 - phaseIncrement; + } + if (phaseIncrement > 1.0) { + phaseIncrement = 1.0; + } + + currPhase += phaseIncrement; + + // calculate new random whenever phase passes 1.0 + if (currPhase > 1.0) { + prevNoise = currNoise; + currNoise = randomNum.nextRandomDouble(); + // reset phase for interpolation + currPhase -= 1.0; + } + + // interpolate current + currOutput = prevNoise + (currPhase * (currNoise - prevNoise)); + outputs[i] = currOutput * amplitudes[i]; + } + + // store new phase + phase.setValue(currPhase); + } +} diff --git a/src/main/java/com/jsyn/unitgen/SampleGrainFarm.java b/src/main/java/com/jsyn/unitgen/SampleGrainFarm.java new file mode 100644 index 0000000..3f908d6 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SampleGrainFarm.java @@ -0,0 +1,71 @@ +/* + * 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.unitgen; + +import com.jsyn.data.FloatSample; +import com.jsyn.ports.UnitInputPort; + +/** + * A GrainFarm that uses a FloatSample as source material. In this example we load a FloatSample for + * use as a source material. + * + * <pre><code> + synth.add(sampleGrainFarm = new SampleGrainFarm()); + // Load a sample that we want to "granulate" from a file. + sample = SampleLoader.loadFloatSample(sampleFile); + sampleGrainFarm.setSample(sample); + // Use a ramp to move smoothly within the file. + synth.add(ramp = new ContinuousRamp()); + ramp.output.connect(sampleGrainFarm.position); +</code></pre> + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public class SampleGrainFarm extends GrainFarm { + private FloatSample sample; + public UnitInputPort position; + public UnitInputPort positionRange; + + public SampleGrainFarm() { + super(); + addPort(position = new UnitInputPort("Position", 0.0)); + addPort(positionRange = new UnitInputPort("PositionRange", 0.0)); + } + + @Override + public void allocate(int numGrains) { + Grain[] grainArray = new Grain[numGrains]; + for (int i = 0; i < numGrains; i++) { + Grain grain = new Grain(new SampleGrainSource(), new RaisedCosineEnvelope()); + grainArray[i] = grain; + } + setGrainArray(grainArray); + } + + @Override + public void setupGrain(Grain grain, int i) { + SampleGrainSource sampleGrainSource = (SampleGrainSource) grain.getSource(); + sampleGrainSource.setSample(sample); + sampleGrainSource.setPosition(position.getValues()[i]); + sampleGrainSource.setPositionRange(positionRange.getValues()[i]); + super.setupGrain(grain, i); + } + + public void setSample(FloatSample sample) { + this.sample = sample; + } +} diff --git a/src/main/java/com/jsyn/unitgen/SampleGrainSource.java b/src/main/java/com/jsyn/unitgen/SampleGrainSource.java new file mode 100644 index 0000000..f33817f --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SampleGrainSource.java @@ -0,0 +1,69 @@ +/* + * 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.unitgen; + +import com.jsyn.data.FloatSample; + +public class SampleGrainSource extends GrainCommon implements GrainSource { + private FloatSample sample; + private double position; // ranges from -1.0 to 1.0 + private double positionRange; + private double phase; // ranges from 0.0 to 1.0 + private double phaseIncrement; + private int numFramesGuarded; + private static final double MAX_PHASE = 0.9999999999; + + @Override + public double next() { + phase += phaseIncrement; + if (phase > MAX_PHASE) { + phase = MAX_PHASE; + } + double fractionalIndex = phase * numFramesGuarded; + return sample.interpolate(fractionalIndex); + } + + @Override + public void setRate(double rate) { + phaseIncrement = rate * sample.getFrameRate() / (getFrameRate() * numFramesGuarded); + } + + public void setSample(FloatSample sample) { + this.sample = sample; + numFramesGuarded = sample.getNumFrames() - 1; + } + + public void setPosition(double position) { + this.position = position; + } + + @Override + public void reset() { + double randomPosition = position + (positionRange * (Math.random() - 0.5)); + phase = (randomPosition * 0.5) + 0.5; + if (phase < 0.0) { + phase = 0.0; + } else if (phase > MAX_PHASE) { + phase = MAX_PHASE; + } + } + + public void setPositionRange(double positionRange) { + this.positionRange = positionRange; + } + +} diff --git a/src/main/java/com/jsyn/unitgen/SawtoothOscillator.java b/src/main/java/com/jsyn/unitgen/SawtoothOscillator.java new file mode 100644 index 0000000..1b3dead --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SawtoothOscillator.java @@ -0,0 +1,47 @@ +/* + * 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.unitgen; + +/** + * Simple sawtooth oscillator. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class SawtoothOscillator extends UnitOscillator { + + @Override + public void generate(int start, int limit) { + + double[] frequencies = frequency.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + + // Variables have a single value. + double currentPhase = phase.getValue(); + + for (int i = start; i < limit; i++) { + /* Generate sawtooth phasor to provide phase for sine generation. */ + double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]); + currentPhase = incrementWrapPhase(currentPhase, phaseIncrement); + outputs[i] = currentPhase * amplitudes[i]; + } + + // Value needs to be saved for next time. + phase.setValue(currentPhase); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/SawtoothOscillatorBL.java b/src/main/java/com/jsyn/unitgen/SawtoothOscillatorBL.java new file mode 100644 index 0000000..8b58f6c --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SawtoothOscillatorBL.java @@ -0,0 +1,65 @@ +/* + * 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.unitgen; + +import com.jsyn.engine.MultiTable; + +/** + * Sawtooth oscillator that uses multiple wave tables for band limiting. This requires more CPU than + * a plain SawtoothOscillator but has less aliasing at high frequencies. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class SawtoothOscillatorBL extends UnitOscillator { + @Override + public void generate(int start, int limit) { + MultiTable multiTable = MultiTable.getInstance(); + + double[] frequencies = frequency.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + // Variables have a single value. + double currentPhase = phase.getValue(); + + double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[0]); + double positivePhaseIncrement = Math.abs(phaseIncrement); + // This is very expensive so we moved it outside the loop. + // Try to optimize it with a table lookup. + double flevel = multiTable.convertPhaseIncrementToLevel(positivePhaseIncrement); + + for (int i = start; i < limit; i++) { + /* Generate sawtooth phasor to provide phase for sine generation. */ + phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]); + currentPhase = incrementWrapPhase(currentPhase, phaseIncrement); + positivePhaseIncrement = Math.abs(phaseIncrement); + + double val = generateBL(multiTable, currentPhase, positivePhaseIncrement, flevel, i); + + outputs[i] = val * amplitudes[i]; + } + + // Value needs to be saved for next time. + phase.setValue(currentPhase); + } + + protected double generateBL(MultiTable multiTable, double currentPhase, + double positivePhaseIncrement, double flevel, int i) { + /* Calculate table level then use it for lookup. */ + return multiTable.calculateSawtooth(currentPhase, positivePhaseIncrement, flevel); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/SawtoothOscillatorDPW.java b/src/main/java/com/jsyn/unitgen/SawtoothOscillatorDPW.java new file mode 100644 index 0000000..27d0c5a --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SawtoothOscillatorDPW.java @@ -0,0 +1,76 @@ +/* + * 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.unitgen; + +/** + * Sawtooth DPW oscillator (a sawtooth with reduced aliasing). + * Based on a paper by Antti Huovilainen and Vesa Valimaki: + * http://www.scribd.com/doc/33863143/New-Approaches-to-Digital-Subtractive-Synthesis + * + * @author Phil Burk and Lisa Tolentino (C) 2009 Mobileer Inc + */ +public class SawtoothOscillatorDPW extends UnitOscillator { + // At a very low frequency, switch from DPW to raw sawtooth. + private static final double VERY_LOW_FREQUENCY = 2.0 * 0.1 / 44100.0; + private double z1; + private double z2; + + @Override + public void generate(int start, int limit) { + double[] frequencies = frequency.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + + // Variables have a single value. + double currentPhase = phase.getValue(); + + for (int i = start; i < limit; i++) { + /* Generate raw sawtooth phaser. */ + double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]); + currentPhase = incrementWrapPhase(currentPhase, phaseIncrement); + + /* Square the raw sawtooth. */ + double squared = currentPhase * currentPhase; + // Differentiate using a delayed value. + double diffed = squared - z2; + z2 = z1; + z1 = squared; + + /* Calculate scaling based on phaseIncrement */ + double pinc = phaseIncrement; + // Absolute value. + if (pinc < 0.0) { + pinc = 0.0 - pinc; + } + + double dpw; + // If the frequency is very low then just use the raw sawtooth. + // This avoids divide by zero problems and scaling problems. + if (pinc < VERY_LOW_FREQUENCY) { + dpw = currentPhase; + } else { + dpw = diffed * 0.25 / pinc; + } + + outputs[i] = amplitudes[i] * dpw; + } + + // Value needs to be saved for next time. + phase.setValue(currentPhase); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/SchmidtTrigger.java b/src/main/java/com/jsyn/unitgen/SchmidtTrigger.java new file mode 100644 index 0000000..64129ff --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SchmidtTrigger.java @@ -0,0 +1,83 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * SchmidtTrigger unit. + * <P> + * Output logic level value with hysteresis. Transition high when input exceeds setLevel. Only go + * low when input is below resetLevel. This can be used to reject low level noise on the input + * signal. The default values for setLevel and resetLevel are both 0.0. Setting setLevel to 0.1 and + * resetLevel to -0.1 will give some hysteresis. The outputPulse is a single sample wide pulse set + * when the output transitions from low to high. + * + * <PRE> + * if (output == 0.0) + * output = (input > setLevel) ? 1.0 : 0.0; + * else if (output > 0.0) + * output = (input <= resetLevel) ? 0.0 : 1.0; + * else + * output = previous_output; + * </PRE> + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @see Compare + */ +public class SchmidtTrigger extends UnitFilter { + public UnitInputPort setLevel; + public UnitInputPort resetLevel; + public UnitOutputPort outputPulse; + + /* Define Unit Ports used by connect() and set(). */ + public SchmidtTrigger() { + addPort(setLevel = new UnitInputPort("SetLevel")); + addPort(resetLevel = new UnitInputPort("ResetLevel")); + addPort(input = new UnitInputPort("Input")); + addPort(outputPulse = new UnitOutputPort("OutputPulse")); + } + + @Override + public void generate(int start, int limit) { + double[] inPtr = input.getValues(); + double[] pulsePtr = outputPulse.getValues(); + double[] outPtr = output.getValues(); + double[] setPtr = setLevel.getValues(); + double[] resetPtr = resetLevel.getValues(); + + double outputValue = outPtr[0]; + boolean state = (outputValue > UnitGenerator.FALSE); + for (int i = start; i < limit; i++) { + pulsePtr[i] = UnitGenerator.FALSE; + if (state) { + if (inPtr[i] <= resetPtr[i]) { + state = false; + outputValue = UnitGenerator.FALSE; + } + } else { + if (inPtr[i] > setPtr[i]) { + state = true; + outputValue = UnitGenerator.TRUE; + pulsePtr[i] = UnitGenerator.TRUE; /* Single impulse. */ + } + } + outPtr[i] = outputValue; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/Select.java b/src/main/java/com/jsyn/unitgen/Select.java new file mode 100644 index 0000000..6d8792e --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Select.java @@ -0,0 +1,56 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * SelectUnit unit. Select InputA or InputB based on value on Select port. + * + *<pre> <code> + output = ( select > 0.0 ) ? inputB : inputA; +</code> </pre> + * + * @author (C) 2004-2009 Phil Burk, SoftSynth.com + */ + +public class Select extends UnitGenerator { + public UnitInputPort inputA; + public UnitInputPort inputB; + public UnitInputPort select; + public UnitOutputPort output; + + public Select() { + addPort(inputA = new UnitInputPort("InputA")); + addPort(inputB = new UnitInputPort("InputB")); + addPort(select = new UnitInputPort("Select")); + addPort(output = new UnitOutputPort("Output")); + } + + @Override + public void generate(int start, int limit) { + double[] inputAs = inputA.getValues(); + double[] inputBs = inputB.getValues(); + double[] selects = select.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + outputs[i] = (selects[i] > UnitGenerator.FALSE) ? inputBs[i] : inputAs[i]; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/SequentialDataReader.java b/src/main/java/com/jsyn/unitgen/SequentialDataReader.java new file mode 100644 index 0000000..901767b --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SequentialDataReader.java @@ -0,0 +1,38 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitDataQueuePort; +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * Base class for reading a sample or envelope. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public abstract class SequentialDataReader extends UnitGenerator { + public UnitDataQueuePort dataQueue; + public UnitInputPort amplitude; + public UnitOutputPort output; + + /* Define Unit Ports used by connect() and set(). */ + public SequentialDataReader() { + addPort(dataQueue = new UnitDataQueuePort("Data")); + addPort(amplitude = new UnitInputPort("Amplitude", UnitOscillator.DEFAULT_AMPLITUDE)); + } +} diff --git a/src/main/java/com/jsyn/unitgen/SequentialDataWriter.java b/src/main/java/com/jsyn/unitgen/SequentialDataWriter.java new file mode 100644 index 0000000..cb3bb11 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SequentialDataWriter.java @@ -0,0 +1,44 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitDataQueuePort; +import com.jsyn.ports.UnitInputPort; + +/** + * Base class for writing to a sample. + * + * Note that you must call start() on subclasses of this unit because it does not have an output for pulling data. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public abstract class SequentialDataWriter extends UnitGenerator { + public UnitDataQueuePort dataQueue; + public UnitInputPort input; + + public SequentialDataWriter() { + addPort(dataQueue = new UnitDataQueuePort("Data")); + } + + /** + * This unit won't do anything unless you start() it. + */ + @Override + public boolean isStartRequired() { + return true; + } +} diff --git a/src/main/java/com/jsyn/unitgen/SineOscillator.java b/src/main/java/com/jsyn/unitgen/SineOscillator.java new file mode 100644 index 0000000..8112e46 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SineOscillator.java @@ -0,0 +1,84 @@ +/* + * 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.unitgen; + +/** + * Sine oscillator generates a frequency controlled sine wave. It is implemented using a fast Taylor + * expansion. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class SineOscillator extends UnitOscillator { + public SineOscillator() { + } + + public SineOscillator(double freq) { + frequency.set(freq); + } + + public SineOscillator(double freq, double amp) { + frequency.set(freq); + amplitude.set(amp); + } + + @Override + public void generate(int start, int limit) { + double[] frequencies = frequency.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + double currentPhase = phase.getValue(); + + for (int i = start; i < limit; i++) { + /* Generate sawtooth phasor to provide phase for sine generation. */ + double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]); + currentPhase = incrementWrapPhase(currentPhase, phaseIncrement); + if (true) { + double value = fastSin(currentPhase); + outputs[i] = value * amplitudes[i]; + } else { + // Slower but more accurate implementation. + outputs[i] = Math.sin(currentPhase * Math.PI) * amplitudes[i]; + } + } + + phase.setValue(currentPhase); + } + + /** + * Calculate sine using Taylor expansion. Do not use values outside the range. + * + * @param currentPhase in the range of -1.0 to +1.0 for one cycle + */ + public static double fastSin(double currentPhase) { + // Factorial constants so code is easier to read. + final double IF3 = 1.0 / (2 * 3); + final double IF5 = IF3 / (4 * 5); + final double IF7 = IF5 / (6 * 7); + final double IF9 = IF7 / (8 * 9); + final double IF11 = IF9 / (10 * 11); + + /* Wrap phase back into region where results are more accurate. */ + double yp = (currentPhase > 0.5) ? 1.0 - currentPhase : ((currentPhase < (-0.5)) ? (-1.0) + - currentPhase : currentPhase); + + double x = yp * Math.PI; + double x2 = (x * x); + /* Taylor expansion out to x**11/11! factored into multiply-adds */ + return x + * (x2 * (x2 * (x2 * (x2 * ((x2 * (-IF11)) + IF9) - IF7) + IF5) - IF3) + 1); + } +} diff --git a/src/main/java/com/jsyn/unitgen/SineOscillatorPhaseModulated.java b/src/main/java/com/jsyn/unitgen/SineOscillatorPhaseModulated.java new file mode 100644 index 0000000..7631dff --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SineOscillatorPhaseModulated.java @@ -0,0 +1,74 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * Sine oscillator with a phase modulation input. Phase modulation is similar to frequency + * modulation but is easier to use in some ways. + * + * <pre> + * output = sin(PI * (phase + modulation)) + * </pre> + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class SineOscillatorPhaseModulated extends SineOscillator { + public UnitInputPort modulation; + + /* Define Unit Ports used by connect() and set(). */ + public SineOscillatorPhaseModulated() { + super(); + addPort(modulation = new UnitInputPort("Modulation")); + } + + @Override + public void generate(int start, int limit) { + double[] frequencies = frequency.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + double[] modulations = modulation.getValues(); + double currentPhase = phase.getValue(); + + for (int i = start; i < limit; i++) { + /* Generate sawtooth phaser to provide phase for sine generation. */ + double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]); + currentPhase = incrementWrapPhase(currentPhase, phaseIncrement); + double modulatedPhase = currentPhase + modulations[i]; + double value; + if (false) { + // TODO Compare benchmarks. + while (modulatedPhase >= 1.0) { + modulatedPhase -= 2.0; + } + while (modulatedPhase < -1.0) { + modulatedPhase += 2.0; + } + value = fastSin(modulatedPhase); + } else { + value = Math.sin(modulatedPhase * Math.PI); + } + outputs[i] = value * amplitudes[i]; + // System.out.format("Sine: freq = %10.4f , amp = %8.5f, out = %8.5f, phase = %8.5f, frame = %8d\n", + // frequencies[i], amplitudes[i],outputs[i],currentPhase,frame++ ); + } + + phase.setValue(currentPhase); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/SpectralFFT.java b/src/main/java/com/jsyn/unitgen/SpectralFFT.java new file mode 100644 index 0000000..f3e881a --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SpectralFFT.java @@ -0,0 +1,130 @@ +/* + * Copyright 2013 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.unitgen; + +import java.util.Arrays; + +import com.jsyn.data.SpectralWindow; +import com.jsyn.data.Spectrum; +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitSpectralOutputPort; +import com.softsynth.math.FourierMath; + +/** + * Periodically transform the input signal using an FFT. Output complete spectra. + * + * @author Phil Burk (C) 2013 Mobileer Inc + * @version 016 + * @see SpectralIFFT + * @see Spectrum + * @see SpectralFilter + */ +public class SpectralFFT extends UnitGenerator { + public UnitInputPort input; + /** + * Provides complete complex spectra when the FFT completes. + */ + public UnitSpectralOutputPort output; + private double[] buffer; + private int cursor; + private SpectralWindow window = RectangularWindow.getInstance(); + private int sizeLog2; + private int offset; + private boolean running; + + /* Define Unit Ports used by connect() and set(). */ + public SpectralFFT() { + this(Spectrum.DEFAULT_SIZE_LOG_2); + } + + /** + * @param sizeLog2 for example, pass 10 to get a 1024 bin FFT + */ + public SpectralFFT(int sizeLog2) { + addPort(input = new UnitInputPort("Input")); + addPort(output = new UnitSpectralOutputPort("Output", 1 << sizeLog2)); + setSizeLog2(sizeLog2); + } + + /** + * Please do not change the size of the FFT while JSyn is running. + * + * @param sizeLog2 for example, pass 9 to get a 512 bin FFT + */ + public void setSizeLog2(int sizeLog2) { + this.sizeLog2 = sizeLog2; + output.setSize(1 << sizeLog2); + buffer = output.getSpectrum().getReal(); + cursor = 0; + } + + public int getSizeLog2() { + return sizeLog2; + } + + @Override + public void generate(int start, int limit) { + if (!running) { + int mask = (1 << sizeLog2) - 1; + if (((getSynthesisEngine().getFrameCount() - offset) & mask) == 0) { + running = true; + cursor = 0; + } + } + // Don't use "else" because "running" may have changed in above block. + if (running) { + double[] inputs = input.getValues(); + for (int i = start; i < limit; i++) { + buffer[cursor] = inputs[i] * window.get(cursor); + ++cursor; + // When it is full, do the FFT. + if (cursor == buffer.length) { + Spectrum spectrum = output.getSpectrum(); + Arrays.fill(spectrum.getImaginary(), 0.0); + FourierMath.fft(buffer.length, spectrum.getReal(), spectrum.getImaginary()); + output.advance(); + cursor = 0; + } + } + } + } + + public SpectralWindow getWindow() { + return window; + } + + /** + * Multiply input data by this window before doing the FFT. The default is a RectangularWindow. + */ + public void setWindow(SpectralWindow window) { + this.window = window; + } + + /** + * The FFT will be performed on a frame that is a multiple of the size plus this offset. + * + * @param offset + */ + public void setOffset(int offset) { + this.offset = offset; + } + + public int getOffset() { + return offset; + } + +} diff --git a/src/main/java/com/jsyn/unitgen/SpectralFilter.java b/src/main/java/com/jsyn/unitgen/SpectralFilter.java new file mode 100644 index 0000000..758c8e7 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SpectralFilter.java @@ -0,0 +1,130 @@ +/* + * Copyright 2014 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.unitgen; + +import com.jsyn.data.SpectralWindow; +import com.jsyn.data.SpectralWindowFactory; +import com.jsyn.data.Spectrum; +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.ports.UnitSpectralInputPort; +import com.jsyn.ports.UnitSpectralOutputPort; + +/** + * Process a signal using multiple overlapping FFT and IFFT pairs. For passthrough, you can connect + * the spectral outputs to the spectral inputs. Or you can connect one or more SpectralProcessors + * between them. + * + * <pre> + * for (int i = 0; i < numFFTs; i++) { + * filter.getSpectralOutput(i).connect(processors[i].input); + * processors[i].output.connect(filter.getSpectralInput(i)); + * } + * </pre> + * + * See the example program "HearSpectralFilter.java". Note that this spectral API is experimental + * and may change at any time. + * + * @author Phil Burk (C) 2014 Mobileer Inc + * @see SpectralProcessor + */ +public class SpectralFilter extends Circuit implements UnitSink, UnitSource { + public UnitInputPort input; + public UnitOutputPort output; + + private SpectralFFT[] ffts; + private SpectralIFFT[] iffts; + private PassThrough inlet; // fan out to FFTs + private PassThrough sum; // mix output of IFFTs + + /** + * Create a default sized filter with 2 FFT/IFFT pairs and a sizeLog2 of + * Spectrum.DEFAULT_SIZE_LOG_2. + */ + public SpectralFilter() { + this(2, Spectrum.DEFAULT_SIZE_LOG_2); + } + + /** + * @param numFFTs number of FFT/IFFT pairs for the overlap and add + * @param sizeLog2 for example, use 10 to get a 1024 bin FFT, 12 for 4096 + */ + public SpectralFilter(int numFFTs, int sizeLog2) { + add(inlet = new PassThrough()); + add(sum = new PassThrough()); + ffts = new SpectralFFT[numFFTs]; + iffts = new SpectralIFFT[numFFTs]; + int offset = (1 << sizeLog2) / numFFTs; + for (int i = 0; i < numFFTs; i++) { + add(ffts[i] = new SpectralFFT(sizeLog2)); + inlet.output.connect(ffts[i].input); + ffts[i].setOffset(i * offset); + + add(iffts[i] = new SpectralIFFT()); + iffts[i].output.connect(sum.input); + } + setWindow(SpectralWindowFactory.getHammingWindow(sizeLog2)); + + addPort(input = inlet.input); + addPort(output = sum.output); + } + + public SpectralWindow getWindow() { + return ffts[0].getWindow(); + } + + /** + * Specify one window to be used for all FFTs and IFFTs. The window should be the same size as + * the FFTs. + * + * @param window default is HammingWindow + * @see SpectralWindowFactory + */ + public void setWindow(SpectralWindow window) { + // Use the same window everywhere. + for (int i = 0; i < ffts.length; i++) { + ffts[i].setWindow(window); // TODO review, both sides or just one + iffts[i].setWindow(window); + } + } + + @Override + public UnitOutputPort getOutput() { + return output; + } + + @Override + public UnitInputPort getInput() { + return input; + } + + /** + * @param i + * @return the output of the indexed FFT + */ + public UnitSpectralOutputPort getSpectralOutput(int i) { + return ffts[i].output; + } + + /** + * @param i + * @return the input of the indexed IFFT + */ + public UnitSpectralInputPort getSpectralInput(int i) { + return iffts[i].input; + } +} diff --git a/src/main/java/com/jsyn/unitgen/SpectralIFFT.java b/src/main/java/com/jsyn/unitgen/SpectralIFFT.java new file mode 100644 index 0000000..c040e52 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SpectralIFFT.java @@ -0,0 +1,92 @@ +/* + * Copyright 2013 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.unitgen; + +import com.jsyn.data.SpectralWindow; +import com.jsyn.data.Spectrum; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.ports.UnitSpectralInputPort; +import com.softsynth.math.FourierMath; + +/** + * Periodically transform the input signal using an Inverse FFT. + * + * @author Phil Burk (C) 2013 Mobileer Inc + * @version 016 + * @see SpectralFFT + */ +public class SpectralIFFT extends UnitGenerator { + public UnitSpectralInputPort input; + public UnitOutputPort output; + private Spectrum localSpectrum; + private double[] buffer; + private int cursor; + private SpectralWindow window = RectangularWindow.getInstance(); + + /* Define Unit Ports used by connect() and set(). */ + public SpectralIFFT() { + addPort(output = new UnitOutputPort()); + addPort(input = new UnitSpectralInputPort("Input")); + } + + @Override + public void generate(int start, int limit) { + double[] outputs = output.getValues(); + + if (buffer == null) { + if (input.isAvailable()) { + Spectrum spectrum = input.getSpectrum(); + int size = spectrum.size(); + localSpectrum = new Spectrum(size); + buffer = localSpectrum.getReal(); + cursor = 0; + } else { + for (int i = start; i < limit; i++) { + outputs[i] = 0.0; + } + } + } + + if (buffer != null) { + for (int i = start; i < limit; i++) { + if (cursor == 0) { + Spectrum spectrum = input.getSpectrum(); + spectrum.copyTo(localSpectrum); + FourierMath.ifft(buffer.length, localSpectrum.getReal(), + localSpectrum.getImaginary()); + } + + outputs[i] = buffer[cursor] * window.get(cursor); + cursor += 1; + if (cursor == buffer.length) { + cursor = 0; + } + } + } + } + + public SpectralWindow getWindow() { + return window; + } + + /** + * Multiply output data by this window after doing the FFT. The default is a RectangularWindow. + */ + public void setWindow(SpectralWindow window) { + this.window = window; + } +} diff --git a/src/main/java/com/jsyn/unitgen/SpectralProcessor.java b/src/main/java/com/jsyn/unitgen/SpectralProcessor.java new file mode 100644 index 0000000..de96877 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SpectralProcessor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2014 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.unitgen; + +import com.jsyn.data.Spectrum; +import com.jsyn.ports.UnitSpectralInputPort; +import com.jsyn.ports.UnitSpectralOutputPort; + +/** + * This is a base class for implementing your own spectral processing units. You need to implement + * the processSpectrum() method. + * + * @author Phil Burk (C) 2014 Mobileer Inc + * @see Spectrum + */ +public abstract class SpectralProcessor extends UnitGenerator { + public UnitSpectralInputPort input; + public UnitSpectralOutputPort output; + private int counter; + + /* Define Unit Ports used by connect() and set(). */ + public SpectralProcessor() { + addPort(output = new UnitSpectralOutputPort()); + addPort(input = new UnitSpectralInputPort()); + } + + /* Define Unit Ports used by connect() and set(). */ + public SpectralProcessor(int size) { + addPort(output = new UnitSpectralOutputPort(size)); + addPort(input = new UnitSpectralInputPort()); + } + + @Override + public void generate(int start, int limit) { + for (int i = start; i < limit; i++) { + if (counter == 0) { + if (input.isAvailable()) { + Spectrum inputSpectrum = input.getSpectrum(); + Spectrum outputSpectrum = output.getSpectrum(); + processSpectrum(inputSpectrum, outputSpectrum); + + output.advance(); + counter = inputSpectrum.size() - 1; + } + } else { + counter--; + } + } + } + + /** + * Define this method to implement your own processor. + * + * @param inputSpectrum + * @param outputSpectrum + */ + public abstract void processSpectrum(Spectrum inputSpectrum, Spectrum outputSpectrum); + +} diff --git a/src/main/java/com/jsyn/unitgen/SquareOscillator.java b/src/main/java/com/jsyn/unitgen/SquareOscillator.java new file mode 100644 index 0000000..aaca2d0 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SquareOscillator.java @@ -0,0 +1,49 @@ +/* + * 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.unitgen; + +/** + * Simple square wave oscillator. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class SquareOscillator extends UnitOscillator { + + @Override + public void generate(int start, int limit) { + double[] frequencies = frequency.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + + // Variables have a single value. + double currentPhase = phase.getValue(); + + for (int i = start; i < limit; i++) { + /* Generate sawtooth phasor to provide phase for square generation. */ + double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]); + currentPhase = incrementWrapPhase(currentPhase, phaseIncrement); + + double ampl = amplitudes[i]; + // Either full negative or positive amplitude. + outputs[i] = (currentPhase < 0.0) ? -ampl : ampl; + } + + // Value needs to be saved for next time. + phase.setValue(currentPhase); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/SquareOscillatorBL.java b/src/main/java/com/jsyn/unitgen/SquareOscillatorBL.java new file mode 100644 index 0000000..cb9e141 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/SquareOscillatorBL.java @@ -0,0 +1,48 @@ +/* + * 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.unitgen; + +import com.jsyn.engine.MultiTable; + +/** + * Band-limited square wave oscillator. This requires more CPU than a SquareOscillator but is less + * noisy at high frequencies. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class SquareOscillatorBL extends SawtoothOscillatorBL { + @Override + protected double generateBL(MultiTable multiTable, double currentPhase, + double positivePhaseIncrement, double flevel, int i) { + double val1 = multiTable.calculateSawtooth(currentPhase, positivePhaseIncrement, flevel); + + /* Generate second sawtooth so we can add them together. */ + double phase2 = currentPhase + 1.0; /* 180 degrees out of phase. */ + if (phase2 >= 1.0) { + phase2 -= 2.0; + } + double val2 = multiTable.calculateSawtooth(phase2, positivePhaseIncrement, flevel); + + /* + * Need to adjust amplitude based on positive phaseInc. little less than half at + * Nyquist/2.0! + */ + final double STARTAMP = 0.92; /* Derived by viewing waveforms with TJ_SEEOSC */ + double scale = STARTAMP - positivePhaseIncrement; + return scale * (val1 - val2); + } +} diff --git a/src/main/java/com/jsyn/unitgen/StereoStreamWriter.java b/src/main/java/com/jsyn/unitgen/StereoStreamWriter.java new file mode 100644 index 0000000..b387836 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/StereoStreamWriter.java @@ -0,0 +1,53 @@ +/* + * 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.unitgen; + +import java.io.IOException; + +import com.jsyn.io.AudioOutputStream; +import com.jsyn.ports.UnitInputPort; + +/** + * Write two samples per audio frame to an AudioOutputStream as interleaved samples. + * + * Note that you must call start() on this unit because it does not have an output for pulling data. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class StereoStreamWriter extends UnitStreamWriter { + public StereoStreamWriter() { + addPort(input = new UnitInputPort(2, "Input")); + } + + @Override + public void generate(int start, int limit) { + double[] leftInputs = input.getValues(0); + double[] rightInputs = input.getValues(1); + AudioOutputStream output = outputStream; + if (output != null) { + try { + for (int i = start; i < limit; i++) { + output.write(leftInputs[i]); + output.write(rightInputs[i]); + } + } catch (IOException e) { + e.printStackTrace(); + output = null; + } + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/StochasticGrainScheduler.java b/src/main/java/com/jsyn/unitgen/StochasticGrainScheduler.java new file mode 100644 index 0000000..1f79877 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/StochasticGrainScheduler.java @@ -0,0 +1,43 @@ +/* + * 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.unitgen; + +import com.jsyn.util.PseudoRandom; + +/** + * Use a random function to schedule grains. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public class StochasticGrainScheduler implements GrainScheduler { + PseudoRandom pseudoRandom = new PseudoRandom(); + + @Override + public double nextDuration(double duration) { + return duration; + } + + @Override + public double nextGap(double duration, double density) { + if (density < 0.00000001) { + density = 0.00000001; + } + double gapRange = duration * (1.0 - density) / density; + return pseudoRandom.random() * gapRange; + } + +} diff --git a/src/main/java/com/jsyn/unitgen/Subtract.java b/src/main/java/com/jsyn/unitgen/Subtract.java new file mode 100644 index 0000000..d9ca035 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Subtract.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.unitgen; + +/** + * This unit performs a signed subtraction on its two inputs. + * + * <pre> + * output = inputA - inputB + * </pre> + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @version 016 + * @see MultiplyAdd + * @see Subtract + */ +public class Subtract extends UnitBinaryOperator { + @Override + public void generate(int start, int limit) { + double[] aValues = inputA.getValues(); + double[] bValues = inputB.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + outputs[i] = aValues[i] - bValues[i]; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/TriangleOscillator.java b/src/main/java/com/jsyn/unitgen/TriangleOscillator.java new file mode 100644 index 0000000..ada2d6e --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/TriangleOscillator.java @@ -0,0 +1,59 @@ +/* + * 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.unitgen; + +/** + * Simple triangle wave oscillator. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class TriangleOscillator extends UnitOscillator { + int frame; + + public TriangleOscillator() { + super(); + phase.setValue(-0.5); + } + + @Override + public void generate(int start, int limit) { + + double[] frequencies = frequency.getValues(); + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + + // Variables have a single value. + double currentPhase = phase.getValue(); + + for (int i = start; i < limit; i++) { + /* Generate sawtooth phasor to provide phase for triangle generation. */ + double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]); + currentPhase = incrementWrapPhase(currentPhase, phaseIncrement); + + /* Map phase to triangle waveform. */ + /* 0 - 0.999 => 0.5-p => +0.5 - -0.5 */ + /* -1.0 - 0.0 => 0.5+p => -0.5 - +0.5 */ + double triangle = (currentPhase >= 0.0) ? (0.5 - currentPhase) : (0.5 + currentPhase); + + outputs[i] = triangle * 2.0 * amplitudes[i]; + } + + // Value needs to be saved for next time. + phase.setValue(currentPhase); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/TunableFilter.java b/src/main/java/com/jsyn/unitgen/TunableFilter.java new file mode 100644 index 0000000..d2c9f66 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/TunableFilter.java @@ -0,0 +1,41 @@ +/* + * 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. + */ +/** + * Aug 26, 2009 + * com.jsyn.engine.units.TunableFilter.java + */ + +package com.jsyn.unitgen; + +import com.jsyn.ports.UnitInputPort; + +/** + * A UnitFilter with a frequency port. + * + * @author Phil Burk (C) 2009 Mobileer Inc Translated from 'C' to Java by Lisa + * Tolentino. + */ +public abstract class TunableFilter extends UnitFilter { + + static final double DEFAULT_FREQUENCY = 400; + public UnitInputPort frequency; + + public TunableFilter() { + addPort(frequency = new UnitInputPort("Frequency")); + frequency.setup(40.0, DEFAULT_FREQUENCY, 6000.0); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/TwoInDualOut.java b/src/main/java/com/jsyn/unitgen/TwoInDualOut.java new file mode 100644 index 0000000..a8fea48 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/TwoInDualOut.java @@ -0,0 +1,56 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * This unit combines two discrete inputs into a dual (stereo) output. + * + * <pre> + * output[0] = inputA + * output[1] = inputB + * </pre> + * + * @author (C) 2004-2009 Phil Burk, SoftSynth.com + */ + +public class TwoInDualOut extends UnitGenerator { + public UnitInputPort inputA; + public UnitInputPort inputB; + public UnitOutputPort output; + + public TwoInDualOut() { + addPort(inputA = new UnitInputPort("InputA")); + addPort(inputB = new UnitInputPort("InputB")); + addPort(output = new UnitOutputPort(2, "OutputB")); + } + + @Override + public void generate(int start, int limit) { + double[] inputAs = inputA.getValues(); + double[] inputBs = inputB.getValues(); + double[] output0s = output.getValues(0); + double[] output1s = output.getValues(1); + + for (int i = start; i < limit; i++) { + output0s[i] = inputAs[i]; + output1s[i] = inputBs[i]; + } + } +} diff --git a/src/main/java/com/jsyn/unitgen/UnitBinaryOperator.java b/src/main/java/com/jsyn/unitgen/UnitBinaryOperator.java new file mode 100644 index 0000000..c5675ff --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/UnitBinaryOperator.java @@ -0,0 +1,41 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * Base class for binary arithmetic operators like Add and Compare. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public abstract class UnitBinaryOperator extends UnitGenerator { + public UnitInputPort inputA; + public UnitInputPort inputB; + public UnitOutputPort output; + + /* Define Unit Ports used by connect() and set(). */ + public UnitBinaryOperator() { + addPort(inputA = new UnitInputPort("InputA")); + addPort(inputB = new UnitInputPort("InputB")); + addPort(output = new UnitOutputPort("Output")); + } + + @Override + public abstract void generate(int start, int limit); +} diff --git a/src/main/java/com/jsyn/unitgen/UnitFilter.java b/src/main/java/com/jsyn/unitgen/UnitFilter.java new file mode 100644 index 0000000..49976ba --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/UnitFilter.java @@ -0,0 +1,47 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * Base class for all filters. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public abstract class UnitFilter extends UnitGenerator implements UnitSink, UnitSource { + public UnitInputPort input; + public UnitOutputPort output; + + /* Define Unit Ports used by connect() and set(). */ + public UnitFilter() { + addPort(input = new UnitInputPort("Input")); + addPort(output = new UnitOutputPort("Output")); + } + + @Override + public UnitInputPort getInput() { + return input; + } + + @Override + public UnitOutputPort getOutput() { + return output; + } + +} diff --git a/src/main/java/com/jsyn/unitgen/UnitGate.java b/src/main/java/com/jsyn/unitgen/UnitGate.java new file mode 100644 index 0000000..59144c2 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/UnitGate.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012 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.unitgen; + +import com.jsyn.ports.UnitGatePort; +import com.jsyn.ports.UnitOutputPort; + +/** + * Base class for other envelopes. + * + * @author Phil Burk (C) 2012 Mobileer Inc + */ +public abstract class UnitGate extends UnitGenerator implements UnitSource { + /** + * Input that triggers the envelope. Use amplitude port if you want to connect a signal to be + * modulated by the envelope. + */ + public UnitGatePort input; + public UnitOutputPort output; + + public UnitGate() { + addPort(input = new UnitGatePort("Input")); + addPort(output = new UnitOutputPort("Output")); + } + + @Override + public UnitOutputPort getOutput() { + return output; + } + + /** + * Specify a unit to be disabled when the envelope finishes. + * + * @param unit + */ + public void setupAutoDisable(UnitGenerator unit) { + input.setupAutoDisable(unit); + } + +} diff --git a/src/main/java/com/jsyn/unitgen/UnitGenerator.java b/src/main/java/com/jsyn/unitgen/UnitGenerator.java new file mode 100644 index 0000000..7aacaf1 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/UnitGenerator.java @@ -0,0 +1,357 @@ +/* + * 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.unitgen; + +import java.io.PrintStream; +import java.util.Collection; +import java.util.LinkedHashMap; + +import com.jsyn.Synthesizer; +import com.jsyn.engine.SynthesisEngine; +import com.jsyn.ports.ConnectableInput; +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.ports.UnitPort; +import com.softsynth.shared.time.TimeStamp; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for all unit generators. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public abstract class UnitGenerator { + + private static final Logger LOGGER = LoggerFactory.getLogger(UnitGenerator.class); + + protected static final double VERY_SMALL_FLOAT = 1.0e-26; + + // Some common port names. + public static final String PORT_NAME_INPUT = "Input"; + public static final String PORT_NAME_OUTPUT = "Output"; + public static final String PORT_NAME_PHASE = "Phase"; + public static final String PORT_NAME_FREQUENCY = "Frequency"; + public static final String PORT_NAME_FREQUENCY_SCALER = "FreqScaler"; + public static final String PORT_NAME_AMPLITUDE = "Amplitude"; + public static final String PORT_NAME_PAN = "Pan"; + public static final String PORT_NAME_TIME = "Time"; + public static final String PORT_NAME_CUTOFF = "Cutoff"; + public static final String PORT_NAME_PRESSURE = "Pressure"; + public static final String PORT_NAME_TIMBRE = "Timbre"; + + public static final double FALSE = 0.0; + public static final double TRUE = 1.0; + protected SynthesisEngine synthesisEngine; + private final LinkedHashMap<String, UnitPort> ports = new LinkedHashMap<String, UnitPort>(); + private Circuit circuit; + private long lastFrameCount; + private boolean enabled = true; + private static int nextId; + private final int id = nextId++; + + public int getId() { + return id; + } + + public int getFrameRate() { + // return frameRate; + return synthesisEngine.getFrameRate(); + } + + public double getFramePeriod() { + // return framePeriod; // TODO - Why does OldJSynTestSuite fail if I use this! + return synthesisEngine.getFramePeriod(); + } + + public void addPort(UnitPort port) { + port.setUnitGenerator(this); + // Store in a hash table by name. + ports.put(port.getName().toLowerCase(), port); + } + + public void addPort(UnitPort port, String name) { + port.setName(name); + addPort(port); + } + + /** + * Case-insensitive search for a port by name. + * @param portName + * @return matching port or null + */ + public UnitPort getPortByName(String portName) { + return ports.get(portName.toLowerCase()); + } + + public Collection<UnitPort> getPorts() { + return ports.values(); + } + + /** + * Perform essential synthesis function. + * + * @param start offset into port buffers + * @param limit limit offset into port buffers for loop + */ + public abstract void generate(int start, int limit); + + /** + * Generate a full block. + */ + public void generate() { + generate(0, Synthesizer.FRAMES_PER_BLOCK); + } + + /** + * @return the synthesisEngine + */ + public SynthesisEngine getSynthesisEngine() { + return synthesisEngine; + } + + /** + * @return the Synthesizer + */ + public Synthesizer getSynthesizer() { + return synthesisEngine; + } + + /** + * @param synthesisEngine the synthesisEngine to set + */ + public void setSynthesisEngine(SynthesisEngine synthesisEngine) { + if ((this.synthesisEngine != null) && (this.synthesisEngine != synthesisEngine)) { + throw new RuntimeException("Unit synthesisEngine already set."); + } + this.synthesisEngine = synthesisEngine; + } + + public UnitGenerator getTopUnit() { + UnitGenerator unit = this; + // Climb to top of circuit hierarchy. + while (unit.circuit != null) { + unit = unit.circuit; + } + LOGGER.debug("getTopUnit " + this + " => " + unit); + return unit; + } + + protected void autoStop() { + synthesisEngine.autoStopUnit(getTopUnit()); + } + + /** Calculate signal based on halflife of an exponential decay. */ + public double convertHalfLifeToMultiplier(double halfLife) { + if (halfLife < (2.0 * getFramePeriod())) { + return 1.0; + } else { + // Oddly enough, this code is valid for both PeakFollower and AsymptoticRamp. + return 1.0 - Math.pow(0.5, 1.0 / (halfLife * getSynthesisEngine().getFrameRate())); + } + } + + protected double incrementWrapPhase(double currentPhase, double phaseIncrement) { + currentPhase += phaseIncrement; + + if (currentPhase >= 1.0) { + currentPhase -= 2.0; + } else if (currentPhase < -1.0) { + currentPhase += 2.0; + } + return currentPhase; + } + + /** Calculate rate based on phase going from 0.0 to 1.0 in time. */ + protected double convertTimeToRate(double time) { + double period2X = synthesisEngine.getInverseNyquist(); + if (time < period2X) { + return 1.0; + } else { + return getFramePeriod() / time; + } + } + + /** Flatten output ports so we don't output a changing signal when stopped. */ + public void flattenOutputs() { + for (UnitPort port : ports.values()) { + if (port instanceof UnitOutputPort) { + ((UnitOutputPort) port).flatten(); + } + } + } + + public void setCircuit(Circuit circuit) { + if ((this.circuit != null) && (circuit != null)) { + throw new RuntimeException("Unit is already in a circuit."); + } + // LOGGER.info( "setCircuit in unit " + this + " with circuit " + circuit ); + this.circuit = circuit; + } + + public Circuit getCircuit() { + return circuit; + } + + public void pullData(long frameCount, int start, int limit) { + // Don't generate twice in case the paths merge. + if (enabled && (frameCount > lastFrameCount)) { + // Do this first to block recursion when there is a feedback loop. + lastFrameCount = frameCount; + // Then pull from all the units that are upstream. + for (UnitPort port : ports.values()) { + if (port instanceof ConnectableInput) { + ((ConnectableInput) port).pullData(frameCount, start, limit); + } + } + // Finally generate using outputs of the upstream units. + generate(start, limit); + } + } + + public boolean isEnabled() { + return enabled; + } + + /** + * If enabled, then a unit will execute if its output is connected to another unit that is + * executed. If not enabled then it will not execute and will not pull data from units that are + * connected to its inputs. Disabling a unit at the output of a tree of units can be used to + * turn off the entire tree, thus saving CPU cycles. + * + * @param enabled + * @see UnitGate#setupAutoDisable(UnitGenerator) + * @see #start() + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + if (!enabled) { + flattenOutputs(); + } + } + + /** + * Some units, for example LineOut and FixedRateMonoWriter, will only work if + * started explicitly. Other units will run when downstream units are started. + * + * @return true if you should call start() for this unit + */ + public boolean isStartRequired() { + return false; + } + + /** + * Start executing this unit directly by adding it to a "run list" of units in the synthesis + * engine. This method is normally only called for the final unit in a chain, for example a + * LineOut. When that final unit executes it will "pull" data from any units connected to its + * inputs. Those units will then pull data their inputs until the entire chain is executed. If + * units are connected in a circle then this will be detected and the infinite recursion will be + * blocked. + * + * @see #setEnabled(boolean) + */ + public void start() { + if (getSynthesisEngine() == null) { + throw new RuntimeException("This " + this.getClass().getName() + + " was not add()ed to a Synthesizer."); + } + getSynthesisEngine().startUnit(this); + } + + /** + * Start a unit at the specified time. + * + * @param time + * @see #start() + */ + public void start(double time) { + start(new TimeStamp(time)); + } + + /** + * Start a unit at the specified time. + * + * @param timeStamp + * @see #start() + */ + public void start(TimeStamp timeStamp) { + if (getSynthesisEngine() == null) { + throw new RuntimeException("This " + this.getClass().getName() + + " was not add()ed to a Synthesizer."); + } + getSynthesisEngine().startUnit(this, timeStamp); + } + + /** + * Stop a unit at the specified time. + * + * @param time + * @see #start() + */ + public void stop(double time) { + stop(new TimeStamp(time)); + } + + public void stop() { + getSynthesisEngine().stopUnit(this); + } + + public void stop(TimeStamp timeStamp) { + getSynthesisEngine().stopUnit(this, timeStamp); + } + + /** + * @deprecated ignored, frameRate comes from the SynthesisEngine + * @param rate + */ + @Deprecated + public void setFrameRate(int rate) { + } + + /** Needed by UnitSink */ + public UnitGenerator getUnitGenerator() { + return this; + } + + /** Needed by UnitVoice */ + public void setPort(String portName, double value, TimeStamp timeStamp) { + UnitInputPort port = (UnitInputPort) getPortByName(portName); + // LOGGER.debug("setPort " + port ); + if (port == null) { + LOGGER.warn("port was null for name " + portName + ", " + this.getClass().getName()); + } else { + port.set(value, timeStamp); + } + } + + public void printConnections() { + printConnections(System.out); + } + + public void printConnections(PrintStream out) { + printConnections(out, 0); + } + + public void printConnections(PrintStream out, int level) { + for (UnitPort port : getPorts()) { + if (port instanceof UnitInputPort) { + ((UnitInputPort) port).printConnections(out, level); + } + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/UnitOscillator.java b/src/main/java/com/jsyn/unitgen/UnitOscillator.java new file mode 100644 index 0000000..5d4c6fa --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/UnitOscillator.java @@ -0,0 +1,93 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.ports.UnitVariablePort; +import com.softsynth.shared.time.TimeStamp; + +/** + * Base class for all oscillators. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public abstract class UnitOscillator extends UnitGenerator implements UnitVoice { + /** Frequency in Hertz. */ + public UnitInputPort frequency; + public UnitInputPort amplitude; + public UnitVariablePort phase; + public UnitOutputPort output; + + public static final double DEFAULT_FREQUENCY = 440.0; + public static final double DEFAULT_AMPLITUDE = 1.0; + + /* Define Unit Ports used by connect() and set(). */ + public UnitOscillator() { + addPort(frequency = new UnitInputPort(PORT_NAME_FREQUENCY)); + frequency.setup(40.0, DEFAULT_FREQUENCY, 8000.0); + addPort(amplitude = new UnitInputPort(PORT_NAME_AMPLITUDE, DEFAULT_AMPLITUDE)); + addPort(phase = new UnitVariablePort(PORT_NAME_PHASE)); + addPort(output = new UnitOutputPort(PORT_NAME_OUTPUT)); + } + + /** + * Convert a frequency in Hertz to a phaseIncrement in the range -1.0 to +1.0 + */ + public double convertFrequencyToPhaseIncrement(double freq) { + double phaseIncrement; + try { + phaseIncrement = freq * synthesisEngine.getInverseNyquist(); + } catch (NullPointerException e) { + throw new NullPointerException( + "Null Synth! You probably forgot to add this unit to the Synthesizer!"); + } + // Clip to range. + phaseIncrement = (phaseIncrement > 1.0) ? 1.0 : ((phaseIncrement < -1.0) ? -1.0 + : phaseIncrement); + return phaseIncrement; + } + + @Override + public UnitOutputPort getOutput() { + return output; + } + + public void noteOn(double freq, double ampl) { + frequency.set(freq); + amplitude.set(ampl); + } + + public void noteOff() { + amplitude.set(0.0); + } + + @Override + public void noteOff(TimeStamp timeStamp) { + amplitude.set(0.0, timeStamp); + } + + @Override + public void noteOn(double freq, double ampl, TimeStamp timeStamp) { + frequency.set(freq, timeStamp); + amplitude.set(ampl, timeStamp); + } + + @Override + public void usePreset(int presetIndex) { + } +} diff --git a/src/main/java/com/jsyn/unitgen/UnitSink.java b/src/main/java/com/jsyn/unitgen/UnitSink.java new file mode 100644 index 0000000..3e0f55e --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/UnitSink.java @@ -0,0 +1,43 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.softsynth.shared.time.TimeStamp; + +/** + * Interface for unit generators that have an input. + * + * @author Phil Burk, (C) 2009 Mobileer Inc + */ +public interface UnitSink { + public UnitInputPort getInput(); + + /** + * Begin execution of this unit by the Synthesizer. The input will pull data from any output + * port that is connected from it. + */ + public void start(); + + public void start(TimeStamp timeStamp); + + public void stop(); + + public void stop(TimeStamp timeStamp); + + public UnitGenerator getUnitGenerator(); +} diff --git a/src/main/java/com/jsyn/unitgen/UnitSource.java b/src/main/java/com/jsyn/unitgen/UnitSource.java new file mode 100644 index 0000000..5ee8134 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/UnitSource.java @@ -0,0 +1,30 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitOutputPort; + +/** + * Interface for things that have that have an output and an associated UnitGenerator. + * + * @author Phil Burk, (C) 2009 Mobileer Inc + */ +public interface UnitSource { + public UnitOutputPort getOutput(); + + public UnitGenerator getUnitGenerator(); +} diff --git a/src/main/java/com/jsyn/unitgen/UnitStreamWriter.java b/src/main/java/com/jsyn/unitgen/UnitStreamWriter.java new file mode 100644 index 0000000..0c5bd8b --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/UnitStreamWriter.java @@ -0,0 +1,53 @@ +/* + * 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.unitgen; + +import com.jsyn.io.AudioOutputStream; +import com.jsyn.ports.UnitInputPort; + +/** + * Base class for writing to an AudioOutputStream. + * + * Note that you must call start() on subclasses of this unit because it does not have an output for pulling data. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public abstract class UnitStreamWriter extends UnitGenerator implements UnitSink { + protected AudioOutputStream outputStream; + public UnitInputPort input; + + public AudioOutputStream getOutputStream() { + return outputStream; + } + + public void setOutputStream(AudioOutputStream outputStream) { + this.outputStream = outputStream; + } + + /** + * This unit won't do anything unless you start() it. + */ + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public UnitInputPort getInput() { + return input; + } +} diff --git a/src/main/java/com/jsyn/unitgen/UnitVoice.java b/src/main/java/com/jsyn/unitgen/UnitVoice.java new file mode 100644 index 0000000..3f5e6ef --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/UnitVoice.java @@ -0,0 +1,59 @@ +/* + * 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.unitgen; + +import com.jsyn.util.Instrument; +import com.jsyn.util.VoiceDescription; +import com.softsynth.shared.time.TimeStamp; + +/** + * A voice that can be allocated and played by the VoiceAllocator. + * + * @author Phil Burk (C) 2011 Mobileer Inc + * @see VoiceDescription + * @see Instrument + */ +public interface UnitVoice extends UnitSource { + /** + * Play whatever you consider to be a note on this voice. Do not be constrained by traditional + * definitions of notes or music. + * + * @param frequency in Hz related to the perceived pitch of the note. + * @param amplitude generally between 0.0 and 1.0 + * @param timeStamp when to play the note + */ + void noteOn(double frequency, double amplitude, TimeStamp timeStamp); + + void noteOff(TimeStamp timeStamp); + + /** + * Typically a UnitVoice will be a subclass of UnitGenerator, which just returns "this". + */ + @Override + public UnitGenerator getUnitGenerator(); + + /** + * Looks up a port using its name and sets the value. + * + * @param portName + * @param value + * @param timeStamp + */ + void setPort(String portName, double value, TimeStamp timeStamp); + + void usePreset(int presetIndex); +} diff --git a/src/main/java/com/jsyn/unitgen/Unzipper.java b/src/main/java/com/jsyn/unitgen/Unzipper.java new file mode 100644 index 0000000..c776ffb --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/Unzipper.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014 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.unitgen; + +/** + * Used inside UnitGenerators for fast smoothing of inputs. + * + * @author Phil Burk (C) 2014 Mobileer Inc + */ +public class Unzipper { + private double target; + private double delta; + private double current; + private int counter; + // About 30 msec. Power of 2 so divide should be faster. + private static final int NUM_STEPS = 1024; + + public double smooth(double input) { + if (input != target) { + target = input; + delta = (target - current) / NUM_STEPS; + counter = NUM_STEPS; + } + if (counter > 0) { + if (--counter == 0) { + current = target; + } else { + current += delta; + } + } + return current; + } +} diff --git a/src/main/java/com/jsyn/unitgen/VariableRateDataReader.java b/src/main/java/com/jsyn/unitgen/VariableRateDataReader.java new file mode 100644 index 0000000..2ef163c --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/VariableRateDataReader.java @@ -0,0 +1,29 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; + +public abstract class VariableRateDataReader extends SequentialDataReader { + /** A scaler for playback rate. Nominally 1.0. */ + public UnitInputPort rate; + + public VariableRateDataReader() { + super(); + addPort(rate = new UnitInputPort("Rate", 1.0)); + } +} diff --git a/src/main/java/com/jsyn/unitgen/VariableRateMonoReader.java b/src/main/java/com/jsyn/unitgen/VariableRateMonoReader.java new file mode 100644 index 0000000..52b7f1e --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/VariableRateMonoReader.java @@ -0,0 +1,115 @@ +/* + * 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.unitgen; + +import com.jsyn.data.FloatSample; +import com.jsyn.data.SegmentedEnvelope; +import com.jsyn.data.ShortSample; +import com.jsyn.ports.UnitOutputPort; + +/** + * This reader can play any SequentialData and will interpolate between adjacent values. It can play + * both {@link SegmentedEnvelope envelopes} and {@link FloatSample samples}. + * + * <pre><code> + // Queue an envelope to the dataQueue port. + ampEnv.dataQueue.queue(ampEnvelope); +</code></pre> + * + * @author Phil Burk (C) 2009 Mobileer Inc + * @see FloatSample + * @see ShortSample + * @see SegmentedEnvelope + */ +public class VariableRateMonoReader extends VariableRateDataReader { + private double phase; // ranges from 0.0 to 1.0 + private double baseIncrement; + private double source; + private double current; + private double target; + private boolean starved; + private boolean ranout; + + public VariableRateMonoReader() { + super(); + addPort(output = new UnitOutputPort("Output")); + starved = true; + baseIncrement = 1.0; + } + + @Override + public void generate(int start, int limit) { + double[] amplitudes = amplitude.getValues(); + double[] rates = rate.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + // Decrement phase and advance through queued data until phase back + // in range. + if (phase >= 1.0) { + while (phase >= 1.0) { + source = target; + phase -= 1.0; + baseIncrement = advanceToNextFrame(); + } + } else if ((i == 0) && (starved || !dataQueue.isTargetValid())) { + // A starved condition can only be cured at the beginning of a + // block. + source = target = current; + phase = 0.0; + baseIncrement = advanceToNextFrame(); + } + + // Interpolate along line segment. + current = ((target - source) * phase) + source; + outputs[i] = current * amplitudes[i]; + + double phaseIncrement = baseIncrement * rates[i]; + phase += limitPhaseIncrement(phaseIncrement); + } + + if (ranout) { + ranout = false; + if (dataQueue.testAndClearAutoStop()) { + autoStop(); + } + } + } + + public double limitPhaseIncrement(double phaseIncrement) { + return phaseIncrement; + } + + private double advanceToNextFrame() { + // Fire callbacks before getting next value because we already got the + // target value previously. + dataQueue.firePendingCallbacks(); + if (dataQueue.hasMore()) { + starved = false; + target = dataQueue.readNextMonoDouble(getFramePeriod()); + + // calculate phase increment; + return getFramePeriod() * dataQueue.getNormalizedRate(); + } else { + starved = true; + ranout = true; + phase = 0.0; + return 0.0; + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/VariableRateStereoReader.java b/src/main/java/com/jsyn/unitgen/VariableRateStereoReader.java new file mode 100644 index 0000000..0f9fce8 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/VariableRateStereoReader.java @@ -0,0 +1,113 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitOutputPort; + +/** + * This reader can play any SequentialData and will interpolate between adjacent values. It can play + * both envelopes and samples. + * + * @author Phil Burk (C) 2009 Mobileer Inc + */ +public class VariableRateStereoReader extends VariableRateDataReader { + private double phase; + private double baseIncrement; + private double source0; + private double current0; + private double target0; + private double source1; + private double current1; + private double target1; + private boolean starved; + private boolean ranout; + + public VariableRateStereoReader() { + dataQueue.setNumChannels(2); + addPort(output = new UnitOutputPort(2, "Output")); + starved = true; + baseIncrement = 1.0; + } + + @Override + public void generate(int start, int limit) { + double[] amplitudes = amplitude.getValues(); + double[] rates = rate.getValues(); + double[] output0s = output.getValues(0); + double[] output1s = output.getValues(1); + + for (int i = start; i < limit; i++) { + // Decrement phase and advance through queued data until phase back + // in range. + if (phase >= 1.0) { + while (phase >= 1.0) { + source0 = target0; + source1 = target1; + phase -= 1.0; + baseIncrement = advanceToNextFrame(); + } + } else if ((i == 0) && (starved || !dataQueue.isTargetValid())) { + // A starved condition can only be cured at the beginning of a block. + source0 = target0 = current0; + source1 = target1 = current1; + phase = 0.0; + baseIncrement = advanceToNextFrame(); + } + + // Interpolate along line segment. + current0 = ((target0 - source0) * phase) + source0; + output0s[i] = current0 * amplitudes[i]; + current1 = ((target1 - source1) * phase) + source1; + output1s[i] = current1 * amplitudes[i]; + + double phaseIncrement = baseIncrement * rates[i]; + phase += limitPhaseIncrement(phaseIncrement); + } + + if (ranout) { + ranout = false; + if (dataQueue.testAndClearAutoStop()) { + autoStop(); + } + } + } + + public double limitPhaseIncrement(double phaseIncrement) { + return phaseIncrement; + } + + private double advanceToNextFrame() { + dataQueue.firePendingCallbacks(); + if (dataQueue.hasMore()) { + starved = false; + + dataQueue.beginFrame(getFramePeriod()); + target0 = dataQueue.readCurrentChannelDouble(0); + target1 = dataQueue.readCurrentChannelDouble(1); + dataQueue.endFrame(); + + // calculate phase increment; + return synthesisEngine.getFramePeriod() * dataQueue.getNormalizedRate(); + } else { + starved = true; + ranout = true; + phase = 0.0; + return 0.0; + } + } + +} diff --git a/src/main/java/com/jsyn/unitgen/WhiteNoise.java b/src/main/java/com/jsyn/unitgen/WhiteNoise.java new file mode 100644 index 0000000..b708e92 --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/WhiteNoise.java @@ -0,0 +1,56 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; +import com.jsyn.util.PseudoRandom; + +/** + * WhiteNoise unit. This unit uses a pseudo-random number generator to produce white noise. The + * energy in a white noise signal is distributed evenly across the spectrum. A new random number is + * generated every frame. + * + * @author (C) 1997-2011 Phil Burk, Mobileer Inc + * @see RedNoise + */ +public class WhiteNoise extends UnitGenerator implements UnitSource { + private PseudoRandom randomNum; + public UnitInputPort amplitude; + public UnitOutputPort output; + + public WhiteNoise() { + randomNum = new PseudoRandom(); + addPort(amplitude = new UnitInputPort("Amplitude", UnitOscillator.DEFAULT_AMPLITUDE)); + addPort(output = new UnitOutputPort("Output")); + } + + @Override + public void generate(int start, int limit) { + double[] amplitudes = amplitude.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + outputs[i] = randomNum.nextRandomDouble() * amplitudes[i]; + } + } + + @Override + public UnitOutputPort getOutput() { + return output; + } +} diff --git a/src/main/java/com/jsyn/unitgen/ZeroCrossingCounter.java b/src/main/java/com/jsyn/unitgen/ZeroCrossingCounter.java new file mode 100644 index 0000000..6cd36ea --- /dev/null +++ b/src/main/java/com/jsyn/unitgen/ZeroCrossingCounter.java @@ -0,0 +1,61 @@ +/* + * 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.unitgen; + +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitOutputPort; + +/** + * Count zero crossings. Handy for unit tests. + * + * @author (C) 1997-2011 Phil Burk, Mobileer Inc + */ +public class ZeroCrossingCounter extends UnitGenerator { + private static final double THRESHOLD = 0.0001; + public UnitInputPort input; + public UnitOutputPort output; + + private long count; + private boolean armed; + + /* Define Unit Ports used by connect() and set(). */ + public ZeroCrossingCounter() { + addPort(input = new UnitInputPort("Input")); + addPort(output = new UnitOutputPort("Output")); + } + + @Override + public void generate(int start, int limit) { + double[] inputs = input.getValues(); + double[] outputs = output.getValues(); + + for (int i = start; i < limit; i++) { + double value = inputs[i]; + if (value < -THRESHOLD) { + armed = true; + } else if (armed & (value > THRESHOLD)) { + ++count; + armed = false; + } + outputs[i] = value; + } + } + + public long getCount() { + return count; + } +} 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; + } + +} diff --git a/src/main/java/com/softsynth/math/AudioMath.java b/src/main/java/com/softsynth/math/AudioMath.java new file mode 100644 index 0000000..06eb45b --- /dev/null +++ b/src/main/java/com/softsynth/math/AudioMath.java @@ -0,0 +1,82 @@ +/* + * Copyright 1998 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.softsynth.math; + +/** + * Miscellaneous math functions useful in Audio + * + * @author (C) 1998 Phil Burk + */ +public class AudioMath { + // use scalar to convert natural log to log_base_10 + private final static double a2dScalar = 20.0 / Math.log(10.0); + public static final int CONCERT_A_PITCH = 69; + public static final double CONCERT_A_FREQUENCY = 440.0; + private static double mConcertAFrequency = CONCERT_A_FREQUENCY; + + /** + * Convert amplitude to decibels. 1.0 is zero dB. 0.5 is -6.02 dB. + */ + public static double amplitudeToDecibels(double amplitude) { + return Math.log(amplitude) * a2dScalar; + } + + /** + * Convert decibels to amplitude. Zero dB is 1.0 and -6.02 dB is 0.5. + */ + public static double decibelsToAmplitude(double decibels) { + return Math.pow(10.0, decibels / 20.0); + } + + /** + * Calculate MIDI pitch based on frequency in Hertz. Middle C is 60.0. + */ + public static double frequencyToPitch(double frequency) { + return CONCERT_A_PITCH + 12 * Math.log(frequency / mConcertAFrequency) / Math.log(2.0); + } + + /** + * Calculate frequency in Hertz based on MIDI pitch. Middle C is 60.0. You can use fractional + * pitches so 60.5 would give you a pitch half way between C and C#. + */ + public static double pitchToFrequency(double pitch) { + return mConcertAFrequency * Math.pow(2.0, ((pitch - CONCERT_A_PITCH) * (1.0 / 12.0))); + } + + /** + * This can be used to globally adjust the tuning in JSyn from Concert A at 440.0 Hz to + * a slightly different frequency. Some orchestras use a higher frequency, eg. 441.0. + * This value will be used by pitchToFrequency() and frequencyToPitch(). + * + * @param concertAFrequency + */ + public static void setConcertAFrequency(double concertAFrequency) { + mConcertAFrequency = concertAFrequency; + } + + public static double getConcertAFrequency() { + return mConcertAFrequency; + } + + /** Convert a delta value in semitones to a frequency multiplier. + * @param semitones + * @return scaler For example 2.0 for an input of 12.0 semitones. + */ + public static double semitonesToFrequencyScaler(double semitones) { + return Math.pow(2.0, semitones / 12.0); + } +} diff --git a/src/main/java/com/softsynth/math/ChebyshevPolynomial.java b/src/main/java/com/softsynth/math/ChebyshevPolynomial.java new file mode 100644 index 0000000..bc0e854 --- /dev/null +++ b/src/main/java/com/softsynth/math/ChebyshevPolynomial.java @@ -0,0 +1,45 @@ +/* + * 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.softsynth.math; + +/** + * ChebyshevPolynomial<br> + * Used to generate data for waveshaping table oscillators. + * + * @author Nick Didkovsky (C) 1997 Phil Burk and Nick Didkovsky + */ + +public class ChebyshevPolynomial { + static final Polynomial twoX = new Polynomial(2, 0); + static final Polynomial one = new Polynomial(1); + static final Polynomial oneX = new Polynomial(1, 0); + + /** + * Calculates Chebyshev polynomial of specified integer order. Recursively generated using + * relation Tk+1(x) = 2xTk(x) - Tk-1(x) + * + * @return Chebyshev polynomial of specified order + */ + public static Polynomial T(int order) { + if (order == 0) + return one; + else if (order == 1) + return oneX; + else + return Polynomial.minus(Polynomial.mult(T(order - 1), (twoX)), T(order - 2)); + } +} diff --git a/src/main/java/com/softsynth/math/FourierMath.java b/src/main/java/com/softsynth/math/FourierMath.java new file mode 100644 index 0000000..d133d7f --- /dev/null +++ b/src/main/java/com/softsynth/math/FourierMath.java @@ -0,0 +1,254 @@ +/* + * 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.softsynth.math; + +//Simple Fast Fourier Transform. +public class FourierMath { + static private final int MAX_SIZE_LOG_2 = 16; + static BitReverseTable[] reverseTables = new BitReverseTable[MAX_SIZE_LOG_2]; + static DoubleSineTable[] sineTables = new DoubleSineTable[MAX_SIZE_LOG_2]; + static FloatSineTable[] floatSineTables = new FloatSineTable[MAX_SIZE_LOG_2]; + + private static class DoubleSineTable { + double[] sineValues; + + DoubleSineTable(int numBits) { + int len = 1 << numBits; + sineValues = new double[1 << numBits]; + for (int i = 0; i < len; i++) { + sineValues[i] = Math.sin((i * Math.PI * 2.0) / len); + } + } + } + + private static double[] getDoubleSineTable(int n) { + DoubleSineTable sineTable = sineTables[n]; + if (sineTable == null) { + sineTable = new DoubleSineTable(n); + sineTables[n] = sineTable; + } + return sineTable.sineValues; + } + + private static class FloatSineTable { + float[] sineValues; + + FloatSineTable(int numBits) { + int len = 1 << numBits; + sineValues = new float[1 << numBits]; + for (int i = 0; i < len; i++) { + sineValues[i] = (float) Math.sin((i * Math.PI * 2.0) / len); + } + } + } + + private static float[] getFloatSineTable(int n) { + FloatSineTable sineTable = floatSineTables[n]; + if (sineTable == null) { + sineTable = new FloatSineTable(n); + floatSineTables[n] = sineTable; + } + return sineTable.sineValues; + } + + private static class BitReverseTable { + int[] reversedBits; + + BitReverseTable(int numBits) { + reversedBits = new int[1 << numBits]; + for (int i = 0; i < reversedBits.length; i++) { + reversedBits[i] = reverseBits(i, numBits); + } + } + + static int reverseBits(int index, int numBits) { + int i, rev; + + for (i = rev = 0; i < numBits; i++) { + rev = (rev << 1) | (index & 1); + index >>= 1; + } + + return rev; + } + } + + private static int[] getReverseTable(int n) { + BitReverseTable reverseTable = reverseTables[n]; + if (reverseTable == null) { + reverseTable = new BitReverseTable(n); + reverseTables[n] = reverseTable; + } + return reverseTable.reversedBits; + } + + /** + * Calculate the amplitude of the sine wave associated with each bin of a complex FFT result. + * + * @param ar + * @param ai + * @param magnitudes + */ + public static void calculateMagnitudes(double ar[], double ai[], double[] magnitudes) { + for (int i = 0; i < magnitudes.length; ++i) { + magnitudes[i] = Math.sqrt((ar[i] * ar[i]) + (ai[i] * ai[i])); + } + } + + /** + * Calculate the amplitude of the sine wave associated with each bin of a complex FFT result. + * + * @param ar + * @param ai + * @param magnitudes + */ + public static void calculateMagnitudes(float ar[], float ai[], float[] magnitudes) { + for (int i = 0; i < magnitudes.length; ++i) { + magnitudes[i] = (float) Math.sqrt((ar[i] * ar[i]) + (ai[i] * ai[i])); + } + } + + public static void transform(int sign, int n, double ar[], double ai[]) { + double scale = (sign > 0) ? (2.0 / n) : (0.5); + + int numBits = FourierMath.numBits(n); + int[] reverseTable = getReverseTable(numBits); + double[] sineTable = getDoubleSineTable(numBits); + int mask = n - 1; + int cosineOffset = n / 4; // phase offset between cos and sin + + int i, j; + for (i = 0; i < n; i++) { + j = reverseTable[i]; + if (j >= i) { + double tempr = ar[j] * scale; + double tempi = ai[j] * scale; + ar[j] = ar[i] * scale; + ai[j] = ai[i] * scale; + ar[i] = tempr; + ai[i] = tempi; + } + } + + int mmax, stride; + int numerator = sign * n; + for (mmax = 1, stride = 2 * mmax; mmax < n; mmax = stride, stride = 2 * mmax) { + int phase = 0; + int phaseIncrement = numerator / (2 * mmax); + for (int m = 0; m < mmax; ++m) { + double wr = sineTable[(phase + cosineOffset) & mask]; // cosine + double wi = sineTable[phase]; + + for (i = m; i < n; i += stride) { + j = i + mmax; + double tr = (wr * ar[j]) - (wi * ai[j]); + double ti = (wr * ai[j]) + (wi * ar[j]); + ar[j] = ar[i] - tr; + ai[j] = ai[i] - ti; + ar[i] += tr; + ai[i] += ti; + } + + phase = (phase + phaseIncrement) & mask; + } + mmax = stride; + } + } + + public static void transform(int sign, int n, float ar[], float ai[]) { + float scale = (sign > 0) ? (2.0f / n) : (0.5f); + + int numBits = FourierMath.numBits(n); + int[] reverseTable = getReverseTable(numBits); + float[] sineTable = getFloatSineTable(numBits); + int mask = n - 1; + int cosineOffset = n / 4; // phase offset between cos and sin + + int i, j; + for (i = 0; i < n; i++) { + j = reverseTable[i]; + if (j >= i) { + float tempr = ar[j] * scale; + float tempi = ai[j] * scale; + ar[j] = ar[i] * scale; + ai[j] = ai[i] * scale; + ar[i] = tempr; + ai[i] = tempi; + } + } + + int mmax, stride; + int numerator = sign * n; + for (mmax = 1, stride = 2 * mmax; mmax < n; mmax = stride, stride = 2 * mmax) { + int phase = 0; + int phaseIncrement = numerator / (2 * mmax); + for (int m = 0; m < mmax; ++m) { + float wr = sineTable[(phase + cosineOffset) & mask]; // cosine + float wi = sineTable[phase]; + + for (i = m; i < n; i += stride) { + j = i + mmax; + float tr = (wr * ar[j]) - (wi * ai[j]); + float ti = (wr * ai[j]) + (wi * ar[j]); + ar[j] = ar[i] - tr; + ai[j] = ai[i] - ti; + ar[i] += tr; + ai[i] += ti; + } + + phase = (phase + phaseIncrement) & mask; + } + mmax = stride; + } + } + + /** + * Calculate log2(n) + * + * @param powerOf2 must be a power of two, for example 512 or 1024 + * @return for example, 9 for an input value of 512 + */ + public static int numBits(int powerOf2) { + int i; + assert ((powerOf2 & (powerOf2 - 1)) == 0); // is it a power of 2? + for (i = -1; powerOf2 > 0; powerOf2 = powerOf2 >> 1, i++) + ; + return i; + } + + /** + * Calculate an FFT in place, modifying the input arrays. + * + * @param n + * @param ar + * @param ai + */ + public static void fft(int n, double ar[], double ai[]) { + transform(1, n, ar, ai); // TODO -1 or 1 + } + + /** + * Calculate an inverse FFT in place, modifying the input arrays. + * + * @param n + * @param ar + * @param ai + */ + public static void ifft(int n, double ar[], double ai[]) { + transform(-1, n, ar, ai); // TODO -1 or 1 + } +} diff --git a/src/main/java/com/softsynth/math/JustRatio.java b/src/main/java/com/softsynth/math/JustRatio.java new file mode 100644 index 0000000..f4070b4 --- /dev/null +++ b/src/main/java/com/softsynth/math/JustRatio.java @@ -0,0 +1,47 @@ +/* + * 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.softsynth.math; + +public class JustRatio { + public long numerator; + public long denominator; + + public JustRatio(long numerator, long denominator) { + this.numerator = numerator; + this.denominator = denominator; + } + + public JustRatio(int numerator, int denominator) { + this.numerator = numerator; + this.denominator = denominator; + } + + public double getValue() { + return (double) numerator / denominator; + } + + public void invert() { + long temp = denominator; + denominator = numerator; + numerator = temp; + } + + @Override + public String toString() { + return numerator + "/" + denominator; + } +} diff --git a/src/main/java/com/softsynth/math/Polynomial.java b/src/main/java/com/softsynth/math/Polynomial.java new file mode 100644 index 0000000..6c6f96a --- /dev/null +++ b/src/main/java/com/softsynth/math/Polynomial.java @@ -0,0 +1,259 @@ +/* + * 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.softsynth.math; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Vector; + +/** + * Polynomial<br> + * Implement polynomial using Vector as coefficient holder. Element index is power of X, value at a + * given index is coefficient.<br> + * <br> + * + * @author Nick Didkovsky, (C) 1997 Phil Burk and Nick Didkovsky + */ + +public class Polynomial { + + private static final Logger LOGGER = LoggerFactory.getLogger(Polynomial.class); + + private final Vector terms; + + // TODO: Does this need to exist? + static class DoubleHolder { + double value; + + public DoubleHolder(double val) { + value = val; + } + + public double get() { + return value; + } + + public void set(double val) { + value = val; + } + } + + /** create a polynomial with no terms */ + public Polynomial() { + terms = new Vector(); + } + + /** create a polynomial with one term of specified constant */ + public Polynomial(double c0) { + this(); + appendTerm(c0); + } + + /** create a polynomial with two terms with specified coefficients */ + public Polynomial(double c1, double c0) { + this(c0); + appendTerm(c1); + } + + /** create a polynomial with specified coefficients */ + public Polynomial(double c2, double c1, double c0) { + this(c1, c0); + appendTerm(c2); + } + + /** create a polynomial with specified coefficients */ + public Polynomial(double c3, double c2, double c1, double c0) { + this(c2, c1, c0); + appendTerm(c3); + } + + /** create a polynomial with specified coefficients */ + public Polynomial(double c4, double c3, double c2, double c1, double c0) { + this(c3, c2, c1, c0); + appendTerm(c4); + } + + /** + * Append a term with specified coefficient. Power will be next available order (ie if the + * polynomial is of order 2, appendTerm will supply the coefficient for x^3 + */ + public void appendTerm(double coefficient) { + terms.addElement(new DoubleHolder(coefficient)); + } + + /** Set the coefficient of given term */ + public void setTerm(double coefficient, int power) { + // If setting a term greater than the current order of the polynomial, pad with zero terms + int size = terms.size(); + if (power >= size) { + for (int i = 0; i < (power - size + 1); i++) { + appendTerm(0); + } + } + ((DoubleHolder) terms.elementAt(power)).set(coefficient); + } + + /** + * Add the coefficient of given term to the specified coefficient. ex. addTerm(3, 1) add 3x to a + * polynomial, addTerm(4, 3) adds 4x^3 + */ + public void addTerm(double coefficient, int power) { + setTerm(coefficient + get(power), power); + } + + /** @return coefficient of nth term (first term=0) */ + public double get(int power) { + if (power >= terms.size()) + return 0.0; + else + return ((DoubleHolder) terms.elementAt(power)).get(); + } + + /** @return number of terms in this polynomial */ + public int size() { + return terms.size(); + } + + /** + * Add two polynomials together + * + * @return new Polynomial that is the sum of p1 and p2 + */ + public static Polynomial plus(Polynomial p1, Polynomial p2) { + Polynomial sum = new Polynomial(); + for (int i = 0; i < Math.max(p1.size(), p2.size()); i++) { + sum.appendTerm(p1.get(i) + p2.get(i)); + } + return sum; + } + + /** + * Subtract polynomial from another. (First arg - Second arg) + * + * @return new Polynomial p1 - p2 + */ + public static Polynomial minus(Polynomial p1, Polynomial p2) { + Polynomial sum = new Polynomial(); + for (int i = 0; i < Math.max(p1.size(), p2.size()); i++) { + sum.appendTerm(p1.get(i) - p2.get(i)); + } + return sum; + } + + /** + * Multiply two Polynomials + * + * @return new Polynomial that is the product p1 * p2 + */ + + public static Polynomial mult(Polynomial p1, Polynomial p2) { + Polynomial product = new Polynomial(); + for (int i = 0; i < p1.size(); i++) { + for (int j = 0; j < p2.size(); j++) { + product.addTerm(p1.get(i) * p2.get(j), i + j); + } + } + return product; + } + + /** + * Multiply a Polynomial by a scaler + * + * @return new Polynomial that is the product p1 * p2 + */ + + public static Polynomial mult(double scaler, Polynomial p1) { + Polynomial product = new Polynomial(); + for (int i = 0; i < p1.size(); i++) { + product.appendTerm(p1.get(i) * scaler); + } + return product; + } + + /** Evaluate this polynomial for x */ + public double evaluate(double x) { + double result = 0.0; + for (int i = 0; i < terms.size(); i++) { + result += get(i) * Math.pow(x, i); + } + return result; + } + + @Override + public String toString() { + String s = ""; + if (size() == 0) + s = "empty polynomial"; + boolean somethingPrinted = false; + for (int i = size() - 1; i >= 0; i--) { + if (get(i) != 0.0) { + if (somethingPrinted) + s += " + "; + String coeff = ""; + // if (get(i) == (int)(get(i))) + // coeff = (int)(get(i)) + ""; + if ((get(i) != 1.0) || (i == 0)) + coeff += get(i); + if (i == 0) + s += coeff; + else { + String power = ""; + if (i != 1) + power = "^" + i; + s += coeff + "x" + power; + } + somethingPrinted = true; + } + } + return s; + } + + public static void main(String[] args) { + Polynomial p1 = new Polynomial(); + LOGGER.debug("p1=" + p1); + Polynomial p2 = new Polynomial(3); + LOGGER.debug("p2=" + p2); + Polynomial p3 = new Polynomial(2, 3); + LOGGER.debug("p3=" + p3); + Polynomial p4 = new Polynomial(1, 2, 3); + LOGGER.debug("p4=" + p4); + LOGGER.debug("p4*5=" + Polynomial.mult(5.0, p4)); + + LOGGER.debug("{}", p4.evaluate(10)); + + LOGGER.debug("{}", Polynomial.plus(p4, p1)); + LOGGER.debug("{}", Polynomial.minus(p4, p3)); + p4.setTerm(12.2, 5); + LOGGER.debug("{}", p4); + p4.addTerm(0.8, 5); + LOGGER.debug("{}", p4); + p4.addTerm(0.8, 7); + LOGGER.debug("{}", p4); + LOGGER.debug("{}", Polynomial.mult(p3, p2)); + LOGGER.debug("{}", Polynomial.mult(p3, p3)); + LOGGER.debug("{}", Polynomial.mult(p2, p2)); + + Polynomial t2 = new Polynomial(2, 0, -1); // 2x^2-1, Chebyshev Polynomial of order 2 + Polynomial t3 = new Polynomial(4, 0, -3, 0); // 4x^3-3x, Chebyshev Polynomial of order 3 + // Calculate Chebyshev Polynomial of order 4 from relation Tk+1(x) = 2xTk(x) - Tk-1(x) + Polynomial t4 = Polynomial.minus(Polynomial.mult(t3, (new Polynomial(2, 0))), t2); + LOGGER.debug(t2 + "\n" + t3 + "\n" + t4); + // com.softsynth.jmsl.util + + } +} diff --git a/src/main/java/com/softsynth/math/PolynomialTableData.java b/src/main/java/com/softsynth/math/PolynomialTableData.java new file mode 100644 index 0000000..8110151 --- /dev/null +++ b/src/main/java/com/softsynth/math/PolynomialTableData.java @@ -0,0 +1,64 @@ +/* + * 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.softsynth.math; + +/** + * PolynomialTableData<br> + * Provides an array of double[] containing data generated by a polynomial.<br> + * This is typically used with ChebyshevPolynomial. Input to Polynomial is -1..1, output is -1..1. + * + * @author Nick Didkovsky (C) 1997 Phil Burk and Nick Didkovsky + * @see ChebyshevPolynomial + * @see Polynomial + */ + +public class PolynomialTableData { + + double[] data; + Polynomial polynomial; + + /** + * Constructor which fills double[numFrames] with Polynomial data -1..1<br> + * Note that any Polynomial can plug in here, just make sure output is -1..1 when input ranges + * from -1..1 + */ + public PolynomialTableData(Polynomial polynomial, int numFrames) { + data = new double[numFrames]; + this.polynomial = polynomial; + buildData(); + } + + public double[] getData() { + return data; + } + + void buildData() { + double xInterval = 2.0 / (data.length - 1); // FIXED, added "- 1" + double x; + for (int i = 0; i < data.length; i++) { + x = i * xInterval - 1.0; + data[i] = polynomial.evaluate(x); + // LOGGER.debug("x = " + x + ", p(x) = " + data[i] ); + } + + } + + public static void main(String[] args) { + PolynomialTableData chebData = new PolynomialTableData(ChebyshevPolynomial.T(2), 8); + } + +} diff --git a/src/main/java/com/softsynth/math/PrimeFactors.java b/src/main/java/com/softsynth/math/PrimeFactors.java new file mode 100644 index 0000000..06c0d55 --- /dev/null +++ b/src/main/java/com/softsynth/math/PrimeFactors.java @@ -0,0 +1,244 @@ +/* + * 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.softsynth.math; + +import java.util.ArrayList; + +/** + * Tool for factoring primes and prime ratios. This class contains a static array of primes + * generated using the Sieve of Eratosthenes. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public class PrimeFactors { + private static final int SIEVE_SIZE = 1000; + private static int[] primes; + private final int[] factors; + + static { + // Use Sieve of Eratosthenes to fill Prime table + boolean[] sieve = new boolean[SIEVE_SIZE]; + ArrayList<Integer> primeList = new ArrayList<Integer>(); + int i = 2; + while (i < (SIEVE_SIZE / 2)) { + if (!sieve[i]) { + primeList.add(i); + int multiple = 2 * i; + while (multiple < SIEVE_SIZE) { + sieve[multiple] = true; + multiple += i; + } + } + i += 1; + } + primes = primeListToArray(primeList); + } + + private static int[] primeListToArray(ArrayList<Integer> primeList) { + int[] primes = new int[primeList.size()]; + for (int i = 0; i < primes.length; i++) { + primes[i] = primeList.get(i); + } + return primes; + } + + public PrimeFactors(int[] factors) { + this.factors = factors; + } + + public PrimeFactors(int numerator, int denominator) { + int[] topFactors = factor(numerator); + int[] bottomFactors = factor(denominator); + factors = subtract(topFactors, bottomFactors); + } + + public PrimeFactors subtract(PrimeFactors pf) { + return new PrimeFactors(subtract(factors, pf.factors)); + } + + public PrimeFactors add(PrimeFactors pf) { + return new PrimeFactors(add(factors, pf.factors)); + } + + public static int[] subtract(int[] factorsA, int[] factorsB) { + int max; + int min; + if (factorsA.length > factorsB.length) { + max = factorsA.length; + min = factorsB.length; + } else { + + min = factorsA.length; + max = factorsB.length; + } + ArrayList<Integer> primeList = new ArrayList<Integer>(); + int i; + for (i = 0; i < min; i++) { + primeList.add(factorsA[i] - factorsB[i]); + } + if (factorsA.length > factorsB.length) { + for (; i < max; i++) { + primeList.add(factorsA[i]); + } + } else { + for (; i < max; i++) { + primeList.add(0 - factorsB[i]); + } + } + trimPrimeList(primeList); + return primeListToArray(primeList); + } + + public static int[] add(int[] factorsA, int[] factorsB) { + int max; + int min; + if (factorsA.length > factorsB.length) { + max = factorsA.length; + min = factorsB.length; + } else { + min = factorsA.length; + max = factorsB.length; + } + ArrayList<Integer> primeList = new ArrayList<Integer>(); + int i; + for (i = 0; i < min; i++) { + primeList.add(factorsA[i] + factorsB[i]); + } + if (factorsA.length > factorsB.length) { + for (; i < max; i++) { + primeList.add(factorsA[i]); + } + } else if (factorsB.length > factorsA.length) { + for (; i < max; i++) { + primeList.add(factorsB[i]); + } + } + trimPrimeList(primeList); + return primeListToArray(primeList); + } + + private static void trimPrimeList(ArrayList<Integer> primeList) { + int i; + // trim zero factors off end. + for (i = primeList.size() - 1; i >= 0; i--) { + if (primeList.get(i) == 0) { + primeList.remove(i); + } else { + break; + } + } + } + + public static int[] factor(int n) { + ArrayList<Integer> primeList = new ArrayList<Integer>(); + int i = 0; + int p = primes[i]; + int exponent = 0; + while (n > 1) { + // does the prime number divide evenly into n? + int d = n / p; + int m = d * p; + if (m == n) { + n = d; + exponent += 1; + } else { + primeList.add(exponent); + exponent = 0; + i += 1; + p = primes[i]; + } + } + if (exponent > 0) { + primeList.add(exponent); + } + return primeListToArray(primeList); + } + + /** + * Get prime from table. + * + * + * @param n Warning: Do not exceed getPrimeCount()-1. + * @return Nth prime number, the 0th prime is 2 + */ + public static int getPrime(int n) { + return primes[n]; + } + + /** + * @return the number of primes stored in the table + */ + public static int getPrimeCount() { + return primes.length; + } + + public JustRatio getJustRatio() { + long n = 1; + long d = 1; + for (int i = 0; i < factors.length; i++) { + int exponent = factors[i]; + int p = primes[i]; + if (exponent > 0) { + for (int k = 0; k < exponent; k++) { + n = n * p; + } + } else if (exponent < 0) { + exponent = 0 - exponent; + for (int k = 0; k < exponent; k++) { + d = d * p; + } + } + } + return new JustRatio(n, d); + } + + public int[] getFactors() { + return factors.clone(); + } + + @Override + public String toString() { + StringBuffer buffer = new StringBuffer(); + printFactors(buffer, 1); + buffer.append("/"); + printFactors(buffer, -1); + return buffer.toString(); + } + + private void printFactors(StringBuffer buffer, int sign) { + boolean gotSome = false; + for (int i = 0; i < factors.length; i++) { + int pf = factors[i] * sign; + if (pf > 0) { + if (gotSome) + buffer.append('*'); + int prime = primes[i]; + if (pf == 1) { + buffer.append("" + prime); + } else if (pf == 2) { + buffer.append(prime + "*" + prime); + } else if (pf > 2) { + buffer.append("(" + prime + "^" + pf + ")"); + } + gotSome = true; + } + } + if (!gotSome) { + buffer.append("1"); + } + } +} diff --git a/src/main/java/com/softsynth/shared/time/ScheduledCommand.java b/src/main/java/com/softsynth/shared/time/ScheduledCommand.java new file mode 100644 index 0000000..5b600a7 --- /dev/null +++ b/src/main/java/com/softsynth/shared/time/ScheduledCommand.java @@ -0,0 +1,21 @@ +/* + * 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.softsynth.shared.time; + +public interface ScheduledCommand { + public void run(); +} diff --git a/src/main/java/com/softsynth/shared/time/ScheduledQueue.java b/src/main/java/com/softsynth/shared/time/ScheduledQueue.java new file mode 100644 index 0000000..367e4f8 --- /dev/null +++ b/src/main/java/com/softsynth/shared/time/ScheduledQueue.java @@ -0,0 +1,85 @@ +/* + * 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.softsynth.shared.time; + +import java.util.LinkedList; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Store objects in time sorted order. + */ +public class ScheduledQueue<T> { + private final SortedMap<TimeStamp, List<T>> timeNodes; + + public ScheduledQueue() { + timeNodes = new TreeMap<TimeStamp, List<T>>(); + } + + public boolean isEmpty() { + return timeNodes.isEmpty(); + } + + public synchronized void add(TimeStamp time, T obj) { + List<T> timeList = timeNodes.get(time); + if (timeList == null) { + timeList = new LinkedList<T>(); + timeNodes.put(time, timeList); + } + timeList.add(obj); + } + + public synchronized List<T> removeNextList(TimeStamp time) { + List<T> timeList = null; + if (!timeNodes.isEmpty()) { + TimeStamp lowestTime = timeNodes.firstKey(); + // Is the lowest time before or equal to the specified time. + if (lowestTime.compareTo(time) <= 0) { + timeList = timeNodes.remove(lowestTime); + } + } + return timeList; + } + + public synchronized Object removeNext(TimeStamp time) { + Object next = null; + if (!timeNodes.isEmpty()) { + TimeStamp lowestTime = timeNodes.firstKey(); + // Is the lowest time before or equal to the specified time. + if (lowestTime.compareTo(time) <= 0) { + List<T> timeList = timeNodes.get(lowestTime); + if (timeList != null) { + next = timeList.remove(0); + if (timeList.isEmpty()) { + timeNodes.remove(lowestTime); + } + } + } + } + return next; + } + + public synchronized void clear() { + timeNodes.clear(); + } + + public TimeStamp getNextTime() { + return timeNodes.firstKey(); + } + +} diff --git a/src/main/java/com/softsynth/shared/time/TimeStamp.java b/src/main/java/com/softsynth/shared/time/TimeStamp.java new file mode 100644 index 0000000..6d243ed --- /dev/null +++ b/src/main/java/com/softsynth/shared/time/TimeStamp.java @@ -0,0 +1,56 @@ +/* + * 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.softsynth.shared.time; + +/** + * @author Phil Burk, (C) 2009 Mobileer Inc + */ +public class TimeStamp implements Comparable<TimeStamp> { + private final double time; + + public TimeStamp(double time) { + this.time = time; + } + + public double getTime() { + return time; + } + + /** + * @return -1 if (this < t2), 0 if equal, or +1 + */ + @Override + public int compareTo(TimeStamp t2) { + if (time < t2.time) + return -1; + else if (time == t2.time) + return 0; + else + return 1; + } + + /** + * Create a new TimeStamp at a relative offset in seconds. + * + * @param delta + * @return earlier or later TimeStamp + */ + public TimeStamp makeRelative(double delta) { + return new TimeStamp(time + delta); + } + +} |