From 580fea450ec0982d0bd8be589f00566267e7b0d1 Mon Sep 17 00:00:00 2001 From: Phil Burk Date: Tue, 2 Aug 2016 07:52:17 -0700 Subject: Instruments: add better synth, pitch control --- .classpath | 2 +- src/com/jsyn/apps/InstrumentTester.java | 93 ++++++- src/com/jsyn/engine/MultiTable.java | 6 +- src/com/jsyn/engine/SynthesisEngine.java | 10 - .../jsyn/instruments/DualOscillatorSynthVoice.java | 301 ++++++++++++++++++++ .../jsyn/instruments/JSynInstrumentLibrary.java | 13 +- .../jsyn/instruments/SubtractiveSynthVoice.java | 9 +- src/com/jsyn/midi/MessageParser.java | 90 +++++- src/com/jsyn/midi/MidiConstants.java | 33 ++- src/com/jsyn/midi/MidiSynthesizer.java | 98 +++++++ src/com/jsyn/ports/InputMixingBlockPart.java | 48 ++-- src/com/jsyn/ports/UnitGatePort.java | 8 +- src/com/jsyn/ports/UnitInputPort.java | 35 ++- src/com/jsyn/scope/AudioScope.java | 12 +- src/com/jsyn/scope/TriggerModel.java | 5 +- src/com/jsyn/scope/swing/AudioScopeView.java | 6 +- src/com/jsyn/scope/swing/ScopeTriggerPanel.java | 5 +- src/com/jsyn/swing/EnvelopeEditorBox.java | 2 +- src/com/jsyn/swing/SoundTweaker.java | 10 +- src/com/jsyn/unitgen/Circuit.java | 42 ++- src/com/jsyn/unitgen/EnvelopeDAHDSR.java | 6 +- src/com/jsyn/unitgen/FilterFourPoles.java | 49 +++- src/com/jsyn/unitgen/LineOut.java | 6 +- src/com/jsyn/unitgen/LinearRamp.java | 50 ++-- src/com/jsyn/unitgen/MorphingOscillatorBL.java | 72 +++++ src/com/jsyn/unitgen/PitchToFrequency.java | 26 ++ src/com/jsyn/unitgen/PowerOfTwo.java | 79 +++--- src/com/jsyn/unitgen/PulseOscillatorBL.java | 14 +- src/com/jsyn/unitgen/SawtoothOscillatorDPW.java | 6 +- src/com/jsyn/unitgen/SquareOscillatorBL.java | 9 +- src/com/jsyn/unitgen/TunableFilter.java | 10 +- src/com/jsyn/unitgen/UnitGenerator.java | 32 ++- src/com/jsyn/unitgen/UnitOscillator.java | 16 +- src/com/jsyn/util/MultiChannelSynthesizer.java | 303 +++++++++++++++++++++ src/com/jsyn/util/PolyphonicInstrument.java | 12 +- src/com/jsyn/util/VoiceAllocator.java | 20 +- src/com/softsynth/math/AudioMath.java | 34 ++- tests/com/jsyn/examples/CircuitTester.java | 13 +- tests/com/jsyn/examples/HearMoogFilter.java | 32 ++- tests/com/jsyn/examples/PlayMIDI.java | 241 ++++++++++++++++ tests/com/jsyn/examples/SeeOscillators.java | 51 +++- tests/com/jsyn/examples/UseMidiKeyboard.java | 127 ++------- tests/com/jsyn/research/lambdas/LambdaUnits.java | 22 ++ tests/com/jsyn/unitgen/TestMath.java | 25 +- 44 files changed, 1745 insertions(+), 338 deletions(-) create mode 100644 src/com/jsyn/instruments/DualOscillatorSynthVoice.java create mode 100644 src/com/jsyn/midi/MidiSynthesizer.java create mode 100644 src/com/jsyn/unitgen/MorphingOscillatorBL.java create mode 100644 src/com/jsyn/unitgen/PitchToFrequency.java create mode 100644 src/com/jsyn/util/MultiChannelSynthesizer.java create mode 100644 tests/com/jsyn/examples/PlayMIDI.java create mode 100644 tests/com/jsyn/research/lambdas/LambdaUnits.java diff --git a/.classpath b/.classpath index cccb45a..bb47eaa 100644 --- a/.classpath +++ b/.classpath @@ -2,8 +2,8 @@ - + diff --git a/src/com/jsyn/apps/InstrumentTester.java b/src/com/jsyn/apps/InstrumentTester.java index 4186703..6e347cd 100644 --- a/src/com/jsyn/apps/InstrumentTester.java +++ b/src/com/jsyn/apps/InstrumentTester.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,12 +17,19 @@ 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; @@ -32,11 +39,14 @@ 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; /** - * Let the user select an instrument using the InstrumentBrowser and play them using the ASCII - * keyboard. Sound parameters can be tweaked using faders. - * + * 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 { @@ -44,6 +54,64 @@ public class InstrumentTester extends JApplet { 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); + System.out.println("Play MIDI keyboard: " + keyboard.getDeviceInfo().getDescription()); + result = 0; + } else { + System.out.println("Could not find a keyboard."); + } + return result; + } @Override public void init() { @@ -61,7 +129,7 @@ public class InstrumentTester extends JApplet { for (int i = 0; i < voices.length; i++) { voices[i] = voiceDescription.createUnitVoice(); } - PolyphonicInstrument instrument = new PolyphonicInstrument(voices); + instrument = new PolyphonicInstrument(voices); synth.add(instrument); instrument.usePreset(presetIndex, synth.createTimeStamp()); String title = voiceDescription.getVoiceClassName() + ": " @@ -71,6 +139,19 @@ public class InstrumentTester extends JApplet { }); 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(); } diff --git a/src/com/jsyn/engine/MultiTable.java b/src/com/jsyn/engine/MultiTable.java index 48b03cd..6606639 100644 --- a/src/com/jsyn/engine/MultiTable.java +++ b/src/com/jsyn/engine/MultiTable.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,7 +20,7 @@ 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 - * + *
  Analysis of octave requirements for tables.
 
diff --git a/src/com/jsyn/engine/SynthesisEngine.java b/src/com/jsyn/engine/SynthesisEngine.java
index e966b30..ae16405 100644
--- a/src/com/jsyn/engine/SynthesisEngine.java
+++ b/src/com/jsyn/engine/SynthesisEngine.java
@@ -222,13 +222,6 @@ public class SynthesisEngine implements Synthesizer {
         this.frameRate = frameRate;
         this.framePeriod = 1.0 / frameRate;
 
-        // Set rate for any units that have already been added.
-        for (UnitGenerator ugen : allUnitList) {
-            ugen.setFrameRate(frameRate);
-        }
-
-        // this.numInputChannels = numInputChannels;
-        // this.numOutputChannels = numOutputChannels;
         setupAudioBuffers(numInputChannels, numOutputChannels);
 
         logger.info("Pure Java JSyn from www.softsynth.com, rate = " + frameRate + ", "
@@ -652,9 +645,6 @@ public class SynthesisEngine implements Synthesizer {
     public void add(UnitGenerator ugen) {
         ugen.setSynthesisEngine(this);
         allUnitList.add(ugen);
-        if (frameRate > 0) {
-            ugen.setFrameRate(frameRate);
-        }
     }
 
     @Override
diff --git a/src/com/jsyn/instruments/DualOscillatorSynthVoice.java b/src/com/jsyn/instruments/DualOscillatorSynthVoice.java
new file mode 100644
index 0000000..c81041f
--- /dev/null
+++ b/src/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/com/jsyn/instruments/JSynInstrumentLibrary.java b/src/com/jsyn/instruments/JSynInstrumentLibrary.java
index c5ed91e..9f111c3 100644
--- a/src/com/jsyn/instruments/JSynInstrumentLibrary.java
+++ b/src/com/jsyn/instruments/JSynInstrumentLibrary.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -22,15 +22,18 @@ 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(),
-            NoiseHit.getVoiceDescription(), DrumWoodFM.getVoiceDescription()
+            WaveShapingVoice.getVoiceDescription(),
+            SubtractiveSynthVoice.getVoiceDescription(),
+            DualOscillatorSynthVoice.getVoiceDescription(),
+            NoiseHit.getVoiceDescription(),
+            DrumWoodFM.getVoiceDescription()
     };
 
     @Override
diff --git a/src/com/jsyn/instruments/SubtractiveSynthVoice.java b/src/com/jsyn/instruments/SubtractiveSynthVoice.java
index af3329e..5cfc4b9 100644
--- a/src/com/jsyn/instruments/SubtractiveSynthVoice.java
+++ b/src/com/jsyn/instruments/SubtractiveSynthVoice.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -30,13 +30,12 @@ import com.jsyn.util.VoiceDescription;
 import com.softsynth.shared.time.TimeStamp;
 
 /**
- * Typical synthesizer voice with an oscillator and resonant filter. Modulate the amplitude and
+ * 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 static final long serialVersionUID = -2704222221111608377L;
     private UnitOscillator osc;
     private FilterLowPass filter;
     private EnvelopeDAHDSR ampEnv;
diff --git a/src/com/jsyn/midi/MessageParser.java b/src/com/jsyn/midi/MessageParser.java
index 43d10c8..d0f5d4d 100644
--- a/src/com/jsyn/midi/MessageParser.java
+++ b/src/com/jsyn/midi/MessageParser.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -18,10 +18,15 @@ 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;
@@ -41,27 +46,102 @@ public class MessageParser {
                 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:
-                controlChange(channel, message[1], message[2]);
+                rawControlChange(channel, message[1], message[2]);
+                break;
+
+            case MidiConstants.PROGRAM_CHANGE:
+                programChange(channel, message[1]);
                 break;
 
             case MidiConstants.PITCH_BEND:
-                int bend = (((message[2]) & 0x007F) << 7) + ((message[1]) & 0x007F);
+                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/com/jsyn/midi/MidiConstants.java b/src/com/jsyn/midi/MidiConstants.java
index dae9390..8c92119 100644
--- a/src/com/jsyn/midi/MidiConstants.java
+++ b/src/com/jsyn/midi/MidiConstants.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -18,10 +18,12 @@ 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;
@@ -29,10 +31,33 @@ public class MidiConstants {
     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 = 8192;
+    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"
diff --git a/src/com/jsyn/midi/MidiSynthesizer.java b/src/com/jsyn/midi/MidiSynthesizer.java
new file mode 100644
index 0000000..e011430
--- /dev/null
+++ b/src/com/jsyn/midi/MidiSynthesizer.java
@@ -0,0 +1,98 @@
+/*
+ * 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;
+
+public class MidiSynthesizer extends MessageParser {
+
+    private MultiChannelSynthesizer multiSynth;
+
+    public MidiSynthesizer(MultiChannelSynthesizer multiSynth) {
+        this.multiSynth = multiSynth;
+    }
+
+    @Override
+    public void controlChange(int channel, int index, int value) {
+        //System.out.println("controlChange(" + channel + ", " + index + ", " + value + ")");
+        double normalized = value * (1.0 / 127.0);
+        switch (index) {
+            case MidiConstants.CONTROLLER_MOD_WHEEL:
+                double vibratoDepth = 0.1 * normalized;
+                System.out.println( "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/com/jsyn/ports/InputMixingBlockPart.java b/src/com/jsyn/ports/InputMixingBlockPart.java
index 3211342..5b54b99 100644
--- a/src/com/jsyn/ports/InputMixingBlockPart.java
+++ b/src/com/jsyn/ports/InputMixingBlockPart.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -23,16 +23,18 @@ 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(UnitBlockPort unitBlockPort, double defaultValue) {
-        super(unitBlockPort, defaultValue);
+    InputMixingBlockPart(UnitInputPort unitInputPort, double defaultValue) {
+        super(unitInputPort, defaultValue);
+        this.unitInputPort = unitInputPort;
     }
 
     @Override
@@ -52,30 +54,32 @@ public class InputMixingBlockPart extends PortBlockPart {
         int numConnections = getConnectionCount();
         // System.out.println("numConnection = " + numConnections + " for " +
         // this );
-        if (numConnections == 0)
-        // No connection so just use our own data.
-        {
+        if (numConnections == 0) {
+            // No connection so just use our own data.
             result = super.getValues();
-        } else if (numConnections == 1)
-        // Grab values from one connected port.
-        {
-            PortBlockPart otherPart = getConnection(0);
-            result = otherPart.getValues();
-        } else
-        // Mix all of the inputs.
-        {
-            PortBlockPart otherPart = getConnection(0);
-            double[] inputs = otherPart.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]; // set directly instead of zeroing first
+                mixer[i] = inputs[i];
             }
             // Now mix in the remaining inputs.
-            for (int jCon = 1; jCon < numConnections; jCon++) {
+            for (; jCon < numConnections; jCon++) {
                 otherPart = getConnection(jCon);
-
                 inputs = otherPart.getValues();
                 for (int i = 0; i < mixer.length; i++) {
-                    mixer[i] += inputs[i]; // mix with previous inputs
+                    mixer[i] += inputs[i];
                 }
             }
             result = mixer;
diff --git a/src/com/jsyn/ports/UnitGatePort.java b/src/com/jsyn/ports/UnitGatePort.java
index 43d5e7f..700aef8 100644
--- a/src/com/jsyn/ports/UnitGatePort.java
+++ b/src/com/jsyn/ports/UnitGatePort.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -97,7 +97,7 @@ public class UnitGatePort extends UnitInputPort {
 
     /**
      * 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.
      */
@@ -129,7 +129,7 @@ public class UnitGatePort extends UnitInputPort {
     /**
      * 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) {
diff --git a/src/com/jsyn/ports/UnitInputPort.java b/src/com/jsyn/ports/UnitInputPort.java
index 93a7f7a..3eda1f6 100644
--- a/src/com/jsyn/ports/UnitInputPort.java
+++ b/src/com/jsyn/ports/UnitInputPort.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -23,15 +23,15 @@ 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
@@ -69,7 +69,7 @@ public class UnitInputPort extends UnitBlockPort implements ConnectableInput, Se
 
     /**
      * This is used internally by the SynthesisEngine to execute units based on their connections.
-     * 
+     *
      * @param frameCount
      * @param start
      * @param limit
@@ -128,7 +128,7 @@ public class UnitInputPort extends UnitBlockPort implements ConnectableInput, Se
 
     /**
      * Value of a port based on the set() calls. Not affected by connected ports.
-     * 
+     *
      * @param partNum
      * @return value as set
      */
@@ -144,7 +144,7 @@ public class UnitInputPort extends UnitBlockPort implements ConnectableInput, Se
     /**
      * 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) {
@@ -170,7 +170,7 @@ public class UnitInputPort extends UnitBlockPort implements ConnectableInput, Se
     /**
      * 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
@@ -187,6 +187,25 @@ public class UnitInputPort extends UnitBlockPort implements ConnectableInput, Se
         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);
diff --git a/src/com/jsyn/scope/AudioScope.java b/src/com/jsyn/scope/AudioScope.java
index 32268cd..9ab4a73 100644
--- a/src/com/jsyn/scope/AudioScope.java
+++ b/src/com/jsyn/scope/AudioScope.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -27,7 +27,7 @@ import com.jsyn.scope.swing.AudioScopeView;
 // TODO Continuous capture
 /**
  * Digital oscilloscope for JSyn.
- * 
+ *
  * @author Phil Burk (C) 2010 Mobileer Inc
  */
 public class AudioScope {
@@ -90,7 +90,11 @@ public class AudioScope {
         return getModel().getTriggerModel().getLevelModel().getDoubleValue();
     }
 
-    public void setViewMode(ViewMode waveform) {
+    /**
+     * Not yet implemented.
+     * @param waveform
+     */
+    public void setViewMode(ViewMode viewMode) {
         // TODO Auto-generated method stub
 
     }
diff --git a/src/com/jsyn/scope/TriggerModel.java b/src/com/jsyn/scope/TriggerModel.java
index 7081b2c..0367d71 100644
--- a/src/com/jsyn/scope/TriggerModel.java
+++ b/src/com/jsyn/scope/TriggerModel.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -16,7 +16,6 @@
 
 package com.jsyn.scope;
 
-import javax.swing.ComboBoxModel;
 import javax.swing.DefaultComboBoxModel;
 
 import com.jsyn.scope.AudioScope.TriggerMode;
diff --git a/src/com/jsyn/scope/swing/AudioScopeView.java b/src/com/jsyn/scope/swing/AudioScopeView.java
index 31f1264..ec1afa3 100644
--- a/src/com/jsyn/scope/swing/AudioScopeView.java
+++ b/src/com/jsyn/scope/swing/AudioScopeView.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -77,7 +77,7 @@ public class AudioScopeView extends JPanel {
         setMaximumSize(new Dimension(1200, 300));
     }
 
-    /** @deprecated */
+    /** @deprecated Use setControlsVisible() instead. */
     @Deprecated
     public void setShowControls(boolean show) {
         setControlsVisible(show);
diff --git a/src/com/jsyn/scope/swing/ScopeTriggerPanel.java b/src/com/jsyn/scope/swing/ScopeTriggerPanel.java
index a5f1541..9c22aa1 100644
--- a/src/com/jsyn/scope/swing/ScopeTriggerPanel.java
+++ b/src/com/jsyn/scope/swing/ScopeTriggerPanel.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -18,7 +18,6 @@ package com.jsyn.scope.swing;
 
 import java.awt.BorderLayout;
 
-import javax.swing.ComboBoxModel;
 import javax.swing.DefaultComboBoxModel;
 import javax.swing.JComboBox;
 import javax.swing.JPanel;
diff --git a/src/com/jsyn/swing/EnvelopeEditorBox.java b/src/com/jsyn/swing/EnvelopeEditorBox.java
index 44fe53c..aab5762 100644
--- a/src/com/jsyn/swing/EnvelopeEditorBox.java
+++ b/src/com/jsyn/swing/EnvelopeEditorBox.java
@@ -282,7 +282,7 @@ public class EnvelopeEditorBox extends XYController implements MouseListener, Mo
             {
                 dragIndex = pnt;
                 if (dragIndex <= 0)
-                    dragLowLimit = 0.0; // FIXME
+                    dragLowLimit = 0.0; // FIXME envelope drag limit
                 else
                     dragLowLimit = xPicked - points.getPoint(dragIndex)[0];
                 dragHighLimit = xPicked + (maximumXRange - points.getTotalDuration());
diff --git a/src/com/jsyn/swing/SoundTweaker.java b/src/com/jsyn/swing/SoundTweaker.java
index d41946d..043677e 100644
--- a/src/com/jsyn/swing/SoundTweaker.java
+++ b/src/com/jsyn/swing/SoundTweaker.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -45,13 +45,12 @@ public class SoundTweaker extends JPanel {
         this.synth = synth;
         this.source = source;
 
-        setLayout(new GridLayout(0, 1));
+        setLayout(new GridLayout(0, 2));
 
         UnitGenerator ugen = source.getUnitGenerator();
         ArrayList sliders = new ArrayList();
 
         add(new JLabel(title));
-        // Arrange the faders in a stack.
 
         if (source instanceof Instrument) {
             add(keyboard = createPolyphonicKeyboard());
@@ -59,6 +58,7 @@ public class SoundTweaker extends JPanel {
             add(keyboard = createMonophonicKeyboard());
         }
 
+        // Arrange the faders in a stack.
         // Iterate through the ports.
         for (UnitPort port : ugen.getPorts()) {
             if (port instanceof UnitInputPort) {
@@ -90,13 +90,11 @@ public class SoundTweaker extends JPanel {
         ASCIIMusicKeyboard keyboard = new ASCIIMusicKeyboard() {
             @Override
             public void keyOff(int pitch) {
-                logger.info("-------------- keyOff " + pitch);
                 ((Instrument) source).noteOff(pitch, synth.createTimeStamp());
             }
 
             @Override
             public void keyOn(int pitch) {
-                logger.info("-------------- keyOn " + pitch);
                 double freq = AudioMath.pitchToFrequency(pitch);
                 ((Instrument) source).noteOn(pitch, freq, 0.5, synth.createTimeStamp());
             }
diff --git a/src/com/jsyn/unitgen/Circuit.java b/src/com/jsyn/unitgen/Circuit.java
index a501600..c5a1dcf 100644
--- a/src/com/jsyn/unitgen/Circuit.java
+++ b/src/com/jsyn/unitgen/Circuit.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -17,17 +17,21 @@
 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 units = new ArrayList();
 
+    private final LinkedHashMap portAliases = new LinkedHashMap();
+
     @Override
     public void generate(int start, int limit) {
         for (UnitGenerator unit : units) {
@@ -57,6 +61,11 @@ public class Circuit extends UnitGenerator {
         }
     }
 
+    /**
+     * @deprecated ignored, frameRate comes from the SynthesisEngine
+     * @param rate
+     */
+    @Deprecated
     @Override
     public void setFrameRate(int frameRate) {
         super.setFrameRate(frameRate);
@@ -83,4 +92,31 @@ public class Circuit extends UnitGenerator {
 
     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/com/jsyn/unitgen/EnvelopeDAHDSR.java b/src/com/jsyn/unitgen/EnvelopeDAHDSR.java
index 6acd763..c5ebe83 100644
--- a/src/com/jsyn/unitgen/EnvelopeDAHDSR.java
+++ b/src/com/jsyn/unitgen/EnvelopeDAHDSR.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -30,7 +30,7 @@ import com.jsyn.ports.UnitOutputPort;
  * 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
  */
diff --git a/src/com/jsyn/unitgen/FilterFourPoles.java b/src/com/jsyn/unitgen/FilterFourPoles.java
index d4f80f4..39a47c7 100644
--- a/src/com/jsyn/unitgen/FilterFourPoles.java
+++ b/src/com/jsyn/unitgen/FilterFourPoles.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,13 +20,13 @@ 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 
+ * 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
  */
@@ -37,6 +37,14 @@ public class FilterFourPoles extends TunableFilter {
     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;
@@ -57,6 +65,7 @@ public class FilterFourPoles extends TunableFilter {
 
     public FilterFourPoles() {
         addPort(Q = new UnitInputPort("Q"));
+        frequency.setup(40.0, DEFAULT_FREQUENCY, 4000.0);
         Q.setup(0.1, 2.0, 10.0);
     }
 
@@ -111,7 +120,6 @@ public class FilterFourPoles extends TunableFilter {
                 oneSample(0.0);
             }
             oneSample(x0);
-
             outputs[i] = y4;
         }
 
@@ -143,8 +151,35 @@ public class FilterFourPoles extends TunableFilter {
         this.oversampled = oversampled;
     }
 
-    private double clip(double x) {
-        return x - (x * x * x * 0.1666667);
+    // 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/com/jsyn/unitgen/LineOut.java b/src/com/jsyn/unitgen/LineOut.java
index 489033e..d58f211 100644
--- a/src/com/jsyn/unitgen/LineOut.java
+++ b/src/com/jsyn/unitgen/LineOut.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,7 +20,7 @@ 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 {
diff --git a/src/com/jsyn/unitgen/LinearRamp.java b/src/com/jsyn/unitgen/LinearRamp.java
index 438adf6..cad53d5 100644
--- a/src/com/jsyn/unitgen/LinearRamp.java
+++ b/src/com/jsyn/unitgen/LinearRamp.java
@@ -4,9 +4,9 @@
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -26,7 +26,7 @@ import com.jsyn.ports.UnitVariablePort;
  * 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.
  * 

- * + * * @author (C) 1997 Phil Burk, SoftSynth.com * @see ExponentialRamp * @see AsymptoticRamp @@ -52,33 +52,41 @@ public class LinearRamp extends UnitFilter { 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) { - rate = convertTimeToRate(currentTime); - timeHeld = currentTime; - } - - /* If input has changed, start new segment */ - if (currentInput != target) /* - * Equality check is OK because we set them exactly equal below. - */ + // 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; } - for (int i = start; i < limit; i++) { - if (phase < 1.0) { - /* Interpolate current. */ - currentValue = source + (phase * (target - source)); - phase += rate; - } else { - currentValue = target; + 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; } - outputs[i] = currentValue; } current.setValue(currentValue); diff --git a/src/com/jsyn/unitgen/MorphingOscillatorBL.java b/src/com/jsyn/unitgen/MorphingOscillatorBL.java new file mode 100644 index 0000000..7ca440d --- /dev/null +++ b/src/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/com/jsyn/unitgen/PitchToFrequency.java b/src/com/jsyn/unitgen/PitchToFrequency.java new file mode 100644 index 0000000..9086749 --- /dev/null +++ b/src/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/com/jsyn/unitgen/PowerOfTwo.java b/src/com/jsyn/unitgen/PowerOfTwo.java index 5f1b4cd..5916860 100644 --- a/src/com/jsyn/unitgen/PowerOfTwo.java +++ b/src/com/jsyn/unitgen/PowerOfTwo.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,18 +22,23 @@ 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 + * 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 = 1024; + private static final int NUM_VALUES = 2048; // Cached computation. private double lastInput = 0.0; private double lastOutput = 1.0; @@ -61,39 +66,43 @@ public class PowerOfTwo extends UnitGenerator { double[] inputs = input.getValues(); double[] outputs = output.getValues(); - if (true) { - 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 { - int octave = (int) Math.floor(in); - double normal = in - 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])); + 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; - } - outputs[i] = value; - lastInput = in; - lastOutput = value; + // Adjust for octave. + while (octave > 0) { + octave -= 1; + value *= 2.0; } - } - } else { - for (int i = start; i < limit; i++) { - outputs[i] = Math.pow(2.0, inputs[i]); + 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/com/jsyn/unitgen/PulseOscillatorBL.java b/src/com/jsyn/unitgen/PulseOscillatorBL.java index 43fe27b..c0e234c 100644 --- a/src/com/jsyn/unitgen/PulseOscillatorBL.java +++ b/src/com/jsyn/unitgen/PulseOscillatorBL.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,11 +20,15 @@ import com.jsyn.engine.MultiTable; import com.jsyn.ports.UnitInputPort; /** - * Pulse oscillator that uses two band limited sawtooth oscillators. - * + * 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() { @@ -48,7 +52,7 @@ public class PulseOscillatorBL extends SawtoothOscillatorBL { double val2 = multiTable.calculateSawtooth(phase2, positivePhaseIncrement, flevel); /* - * Need to adjust amplitude based on positive phaseInc. little less than half at + * Need to adjust amplitude based on positive phaseInc and width. little less than half at * Nyquist/2.0! */ double scale = 1.0 - positivePhaseIncrement; diff --git a/src/com/jsyn/unitgen/SawtoothOscillatorDPW.java b/src/com/jsyn/unitgen/SawtoothOscillatorDPW.java index 6868c15..27d0c5a 100644 --- a/src/com/jsyn/unitgen/SawtoothOscillatorDPW.java +++ b/src/com/jsyn/unitgen/SawtoothOscillatorDPW.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,7 +20,7 @@ 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 { diff --git a/src/com/jsyn/unitgen/SquareOscillatorBL.java b/src/com/jsyn/unitgen/SquareOscillatorBL.java index cfe8541..cb9e141 100644 --- a/src/com/jsyn/unitgen/SquareOscillatorBL.java +++ b/src/com/jsyn/unitgen/SquareOscillatorBL.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -21,7 +21,7 @@ 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 { @@ -32,8 +32,9 @@ public class SquareOscillatorBL extends SawtoothOscillatorBL { /* Generate second sawtooth so we can add them together. */ double phase2 = currentPhase + 1.0; /* 180 degrees out of phase. */ - if (phase2 >= 1.0) + if (phase2 >= 1.0) { phase2 -= 2.0; + } double val2 = multiTable.calculateSawtooth(phase2, positivePhaseIncrement, flevel); /* diff --git a/src/com/jsyn/unitgen/TunableFilter.java b/src/com/jsyn/unitgen/TunableFilter.java index 1724ec1..31f6631 100644 --- a/src/com/jsyn/unitgen/TunableFilter.java +++ b/src/com/jsyn/unitgen/TunableFilter.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,7 +15,7 @@ */ /** * Aug 26, 2009 - * com.jsyn.engine.units.TunableFilter.java + * com.jsyn.engine.units.TunableFilter.java */ package com.jsyn.unitgen; @@ -24,13 +24,13 @@ 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 * Tolenti. */ public abstract class TunableFilter extends UnitFilter { - private static final double DEFAULT_FREQUENCY = 400; + static final double DEFAULT_FREQUENCY = 400; public UnitInputPort frequency; public TunableFilter() { diff --git a/src/com/jsyn/unitgen/UnitGenerator.java b/src/com/jsyn/unitgen/UnitGenerator.java index a9a7459..f8278ae 100644 --- a/src/com/jsyn/unitgen/UnitGenerator.java +++ b/src/com/jsyn/unitgen/UnitGenerator.java @@ -36,6 +36,20 @@ import com.softsynth.shared.time.TimeStamp; */ public abstract class UnitGenerator { 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; @@ -75,6 +89,11 @@ public abstract class UnitGenerator { 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()); } @@ -141,8 +160,7 @@ public abstract class UnitGenerator { if (halfLife < (2.0 * getFramePeriod())) { return 1.0; } else { - // Strangely enough, this code is valid for both PeakFollower - // and AsymptoticRamp. + // Oddly enough, this code is valid for both PeakFollower and AsymptoticRamp. return 1.0 - Math.pow(0.5, 1.0 / (halfLife * getSynthesisEngine().getFrameRate())); } } @@ -150,10 +168,11 @@ public abstract class UnitGenerator { protected double incrementWrapPhase(double currentPhase, double phaseIncrement) { currentPhase += phaseIncrement; - if (currentPhase >= 1.0) + if (currentPhase >= 1.0) { currentPhase -= 2.0; - else if (currentPhase < -1.0) + } else if (currentPhase < -1.0) { currentPhase += 2.0; + } return currentPhase; } @@ -285,6 +304,11 @@ public abstract class UnitGenerator { getSynthesisEngine().stopUnit(this, timeStamp); } + /** + * @deprecated ignored, frameRate comes from the SynthesisEngine + * @param rate + */ + @Deprecated public void setFrameRate(int rate) { this.frameRate = rate; this.framePeriod = 1.0 / rate; diff --git a/src/com/jsyn/unitgen/UnitOscillator.java b/src/com/jsyn/unitgen/UnitOscillator.java index 4c02f09..5d4c6fa 100644 --- a/src/com/jsyn/unitgen/UnitOscillator.java +++ b/src/com/jsyn/unitgen/UnitOscillator.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,7 +23,7 @@ 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 { @@ -34,15 +34,15 @@ public abstract class UnitOscillator extends UnitGenerator implements UnitVoice public UnitOutputPort output; public static final double DEFAULT_FREQUENCY = 440.0; - public static final double DEFAULT_AMPLITUDE = 0x7FFF / (double) 0x8000; + public static final double DEFAULT_AMPLITUDE = 1.0; /* Define Unit Ports used by connect() and set(). */ public UnitOscillator() { - addPort(frequency = new UnitInputPort("Frequency")); + addPort(frequency = new UnitInputPort(PORT_NAME_FREQUENCY)); frequency.setup(40.0, DEFAULT_FREQUENCY, 8000.0); - addPort(amplitude = new UnitInputPort("Amplitude", DEFAULT_AMPLITUDE)); - addPort(phase = new UnitVariablePort("Phase")); - addPort(output = new UnitOutputPort("Output")); + addPort(amplitude = new UnitInputPort(PORT_NAME_AMPLITUDE, DEFAULT_AMPLITUDE)); + addPort(phase = new UnitVariablePort(PORT_NAME_PHASE)); + addPort(output = new UnitOutputPort(PORT_NAME_OUTPUT)); } /** diff --git a/src/com/jsyn/util/MultiChannelSynthesizer.java b/src/com/jsyn/util/MultiChannelSynthesizer.java new file mode 100644 index 0000000..c2d0e86 --- /dev/null +++ b/src/com/jsyn/util/MultiChannelSynthesizer.java @@ -0,0 +1,303 @@ +/* + * 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.Multiply; +import com.jsyn.unitgen.Pan; +import com.jsyn.unitgen.LinearRamp; +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: + *


+ * lfo -> pitchToLinear -> [VOICES] -> volume* -> panner
+ * bend --/
+ * 
+ * + * 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 class ChannelContext { + private VoiceDescription voiceDescription; + private UnitVoice[] voices; + private VoiceAllocator allocator; + 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 = 24.0 / 12.0; +// private double bendRangeOctaves = 0.0 / 12.0; + private int presetIndex; + + void setup(int numVoices, VoiceDescription voiceDescription) { + this.voiceDescription = voiceDescription; + 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); + + voices = new UnitVoice[numVoices]; + for (int i = 0; i < numVoices; i++) { + UnitVoice voice = voiceDescription.createUnitVoice(); + UnitGenerator ugen = voice.getUnitGenerator(); + synth.add(ugen); + + // Hook up some channel controllers to standard ports on the voice. + UnitInputPort freqMod = (UnitInputPort) ugen + .getPortByName(UnitGenerator.PORT_NAME_FREQUENCY_SCALER); + if (freqMod != null) { + pitchToLinear.output.connect(freqMod); + } + UnitInputPort timbrePort = (UnitInputPort) ugen + .getPortByName(UnitGenerator.PORT_NAME_TIMBRE); + if (timbrePort != null) { + timbreRamp.output.connect(timbrePort); + timbreRamp.input.setup(timbrePort); + } + UnitInputPort pressurePort = (UnitInputPort) ugen + .getPortByName(UnitGenerator.PORT_NAME_PRESSURE); + if (pressurePort != null) { + pressureRamp.output.connect(pressurePort); + pressureRamp.input.setup(pressurePort); + } + voice.getOutput().connect(volumeMultiplier.inputA); // mono mix all the voices + voices[i] = voice; + } + + 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); + + allocator = new VoiceAllocator(voices); + } + + void programChange(int program) { + int programWrapped = program % voiceDescription.getPresetCount(); + String name = voiceDescription.getPresetNames()[programWrapped]; + System.out.println("Preset[" + program + "] = " + name); + presetIndex = programWrapped; + } + + void noteOff(int noteNumber, int velocity) { + allocator.noteOff(noteNumber, synth.createTimeStamp()); + } + + void noteOn(int noteNumber, int velocity) { + double frequency = AudioMath.pitchToFrequency(noteNumber); + double amplitude = velocity / (4 * 128.0); + TimeStamp timeStamp = synth.createTimeStamp(); + allocator.usePreset(presetIndex, timeStamp); + // System.out.println("noteOn(noteNumber) -> " + frequency + " Hz"); + allocator.noteOn(noteNumber, frequency, amplitude, 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()); + } + for (int i = 0; i < numChannels; i++) { + channels[startChannel + i].setup(voicesPerChannel, voiceDescription); + } + } + + public void programChange(int channel, int program) { + ChannelContext channelContext = channels[channel]; + channelContext.programChange(program); + } + + public void noteOff(int channel, int noteNumber, int velocity) { + ChannelContext channelContext = channels[channel]; + channelContext.noteOff(noteNumber, velocity); + } + + public void noteOn(int channel, int noteNumber, int velocity) { + ChannelContext channelContext = channels[channel]; + channelContext.noteOn(noteNumber, velocity); + } + + /** + * 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) { + 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 offset ranges from -1.0 to +1.0 + */ + public void setPan(int channel, double pan) { + ChannelContext channelContext = channels[channel]; + channelContext.setPan(pan); + } + + public UnitOutputPort getOutput() { + return outputUnit.output; + } + +} diff --git a/src/com/jsyn/util/PolyphonicInstrument.java b/src/com/jsyn/util/PolyphonicInstrument.java index 8501554..2cba78f 100644 --- a/src/com/jsyn/util/PolyphonicInstrument.java +++ b/src/com/jsyn/util/PolyphonicInstrument.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,7 +29,7 @@ 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 */ @@ -56,6 +56,7 @@ public class PolyphonicInstrument extends Circuit implements UnitSource, Instrum addPort(amplitude = mixer.inputB, "Amplitude"); amplitude.setup(0.0001, 0.4, 2.0); + exportAllInputPorts(); } /** @@ -81,11 +82,11 @@ public class PolyphonicInstrument extends Circuit implements UnitSource, Instrum /** * 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) { + void exportNamedInputPort(String portName) { UnitInputPort voicePort = null; PassThrough fanout = new PassThrough(); for (UnitVoice voice : voices) { @@ -103,7 +104,6 @@ public class PolyphonicInstrument extends Circuit implements UnitSource, Instrum return mixer.output; } - // FIXME - no timestamp on UnitVoice @Override public void usePreset(int presetIndex) { usePreset(presetIndex, getSynthesisEngine().createTimeStamp()); diff --git a/src/com/jsyn/util/VoiceAllocator.java b/src/com/jsyn/util/VoiceAllocator.java index af37b91..3310b52 100644 --- a/src/com/jsyn/util/VoiceAllocator.java +++ b/src/com/jsyn/util/VoiceAllocator.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -25,7 +25,7 @@ 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 { @@ -38,7 +38,7 @@ public class VoiceAllocator implements Instrument { /** * 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) { @@ -121,7 +121,7 @@ public class VoiceAllocator implements Instrument { * 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. */ @@ -158,7 +158,7 @@ public class VoiceAllocator implements Instrument { return null; } - /** Turn off all the note currently on. */ + /** Turn off all the notes currently on. */ @Override public void allNotesOff(TimeStamp timeStamp) { getSynthesizer().scheduleCommand(timeStamp, new ScheduledCommand() { @@ -235,4 +235,12 @@ public class VoiceAllocator implements Instrument { }); } + public int getPresetIndex() { + return presetIndex; + } + + public void setPresetIndex(int presetIndex) { + this.presetIndex = presetIndex; + } + } diff --git a/src/com/softsynth/math/AudioMath.java b/src/com/softsynth/math/AudioMath.java index 64f064f..6d5ab07 100644 --- a/src/com/softsynth/math/AudioMath.java +++ b/src/com/softsynth/math/AudioMath.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -18,7 +18,7 @@ package com.softsynth.math; /** * Miscellaneous math functions useful in Audio - * + * * @author (C) 1998 Phil Burk */ public class AudioMath { @@ -26,6 +26,7 @@ public class AudioMath { 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. @@ -47,7 +48,7 @@ public class AudioMath { * 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 / CONCERT_A_FREQUENCY) / Math.log(2.0); + return CONCERT_A_PITCH + 12 * Math.log(frequency / mConcertAFrequency) / Math.log(2.0); } /** @@ -55,6 +56,29 @@ public class AudioMath { * pitches so 60.5 would give you a pitch half way between C and C#. */ public static double pitchToFrequency(double pitch) { - return CONCERT_A_FREQUENCY * Math.pow(2.0, ((pitch - CONCERT_A_PITCH) * (1.0 / 12.0))); + 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/tests/com/jsyn/examples/CircuitTester.java b/tests/com/jsyn/examples/CircuitTester.java index 1a307f2..948e8a0 100644 --- a/tests/com/jsyn/examples/CircuitTester.java +++ b/tests/com/jsyn/examples/CircuitTester.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,6 +22,7 @@ import javax.swing.JApplet; import com.jsyn.JSyn; import com.jsyn.Synthesizer; +import com.jsyn.instruments.DualOscillatorSynthVoice; import com.jsyn.instruments.SubtractiveSynthVoice; import com.jsyn.scope.AudioScope; import com.jsyn.swing.JAppletFrame; @@ -31,7 +32,7 @@ import com.jsyn.unitgen.UnitSource; /** * Listen to a circuit while tweaking it knobs. Show output in a scope. - * + * * @author Phil Burk (C) 2012 Mobileer Inc */ public class CircuitTester extends JApplet { @@ -63,7 +64,7 @@ public class CircuitTester extends JApplet { // Use a scope to see the output. scope = new AudioScope(synth); scope.addProbe(unitSource.getOutput()); - scope.setTriggerMode(AudioScope.TriggerMode.NORMAL); + scope.setTriggerMode(AudioScope.TriggerMode.AUTO); scope.getView().setControlsVisible(false); add(BorderLayout.SOUTH, scope.getView()); @@ -72,13 +73,13 @@ public class CircuitTester extends JApplet { /** * Override this to test your own circuits. - * + * * @return */ public UnitSource createUnitSource() { //return new SampleHoldNoteBlaster(); //return new com.syntona.exported.FMVoice(); - return new SubtractiveSynthVoice(); + return new DualOscillatorSynthVoice(); //return new WindCircuit(); //return new WhiteNoise(); //return new BrownNoise(); diff --git a/tests/com/jsyn/examples/HearMoogFilter.java b/tests/com/jsyn/examples/HearMoogFilter.java index dfe3bec..4ec4811 100644 --- a/tests/com/jsyn/examples/HearMoogFilter.java +++ b/tests/com/jsyn/examples/HearMoogFilter.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -48,7 +48,7 @@ import com.jsyn.unitgen.UnitOscillator; /** * Play a sawtooth through a 4-pole filter. - * + * * @author Phil Burk (C) 2010 Mobileer Inc */ public class HearMoogFilter extends JApplet { @@ -58,11 +58,12 @@ public class HearMoogFilter extends JApplet { private FilterLowPass filterBiquad; private LinearRamp rampCutoff; private PassThrough tieQ; + private PassThrough tieCutoff; private PassThrough mixer; private LineOut lineOut; private AudioScope scope; - private AudioScopeProbe moogProbe; + private boolean useCutoffRamp = false; @Override public void init() { @@ -70,6 +71,7 @@ public class HearMoogFilter extends JApplet { synth.add(oscillator = new SawtoothOscillatorBL()); synth.add(rampCutoff = new LinearRamp()); synth.add(tieQ = new PassThrough()); + synth.add(tieCutoff = new PassThrough()); synth.add(filterMoog = new FilterFourPoles()); synth.add(filterBiquad = new FilterLowPass()); synth.add(mixer = new PassThrough()); @@ -77,9 +79,14 @@ public class HearMoogFilter extends JApplet { oscillator.output.connect(filterMoog.input); oscillator.output.connect(filterBiquad.input); - rampCutoff.output.connect(filterMoog.frequency); - rampCutoff.output.connect(filterBiquad.frequency); - rampCutoff.time.set(0.050); + if (useCutoffRamp) { + rampCutoff.output.connect(filterMoog.frequency); + rampCutoff.output.connect(filterBiquad.frequency); + rampCutoff.time.set(0.000); + } else { + tieCutoff.output.connect(filterMoog.frequency); + tieCutoff.output.connect(filterBiquad.frequency); + } tieQ.output.connect(filterMoog.Q); tieQ.output.connect(filterBiquad.Q); filterMoog.output.connect(mixer.input); @@ -89,7 +96,8 @@ public class HearMoogFilter extends JApplet { filterBiquad.amplitude.set(0.1); oscillator.frequency.setup(50.0, 130.0, 3000.0); oscillator.amplitude.setup(0.0, 0.336, 1.0); - rampCutoff.input.setup(50.0, 400.0, 4000.0); + rampCutoff.input.setup(filterMoog.frequency); + tieCutoff.input.setup(filterMoog.frequency); tieQ.input.setup(0.1, 0.7, 10.0); setupGUI(); } @@ -144,14 +152,18 @@ public class HearMoogFilter extends JApplet { knobPanel.add(setupPortKnob(oscillator.frequency, "OscFreq")); knobPanel.add(setupPortKnob(oscillator.amplitude, "OscAmp")); - knobPanel.add(setupPortKnob(rampCutoff.input, "Cutoff")); + if (useCutoffRamp) { + knobPanel.add(setupPortKnob(rampCutoff.input, "Cutoff")); + } else { + knobPanel.add(setupPortKnob(tieCutoff.input, "Cutoff")); + } knobPanel.add(setupPortKnob(tieQ.input, "Q")); rackPanel.add(knobPanel); add(rackPanel, BorderLayout.SOUTH); scope = new AudioScope(synth); scope.addProbe(oscillator.output); - moogProbe = scope.addProbe(filterMoog.output); + scope.addProbe(filterMoog.output); scope.addProbe(filterBiquad.output); scope.setTriggerMode(AudioScope.TriggerMode.NORMAL); scope.getView().setControlsVisible(false); diff --git a/tests/com/jsyn/examples/PlayMIDI.java b/tests/com/jsyn/examples/PlayMIDI.java new file mode 100644 index 0000000..04c6b9b --- /dev/null +++ b/tests/com/jsyn/examples/PlayMIDI.java @@ -0,0 +1,241 @@ +/* + * 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.examples; + +import java.io.IOException; + +import com.jsyn.JSyn; +import com.jsyn.Synthesizer; +import com.jsyn.devices.javasound.MidiDeviceTools; +import com.jsyn.instruments.DualOscillatorSynthVoice; +import com.jsyn.instruments.SubtractiveSynthVoice; +import com.jsyn.midi.MessageParser; +import com.jsyn.midi.MidiConstants; +import com.jsyn.midi.MidiSynthesizer; +import com.jsyn.unitgen.LineOut; +import com.jsyn.unitgen.PowerOfTwo; +import com.jsyn.unitgen.SineOscillator; +import com.jsyn.unitgen.UnitOscillator; +import com.jsyn.unitgen.UnitVoice; +import com.jsyn.util.MultiChannelSynthesizer; +import com.jsyn.util.VoiceAllocator; +import com.jsyn.util.VoiceDescription; +import com.softsynth.math.AudioMath; +import com.softsynth.shared.time.TimeStamp; + +/** + * Send MIDI messages to JSyn based MIDI synthesizer. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class PlayMIDI { + private static final int NUM_CHANNELS = 16; + private static final int VOICES_PER_CHANNEL = 6; + private Synthesizer synth; + private MidiSynthesizer midiSynthesizer; + private LineOut lineOut; + + private VoiceDescription voiceDescription; + private MultiChannelSynthesizer multiSynth; + + public static void main(String[] args) { + PlayMIDI app = new PlayMIDI(); + try { + VoiceDescription description = DualOscillatorSynthVoice.getVoiceDescription(); + app.test(description); + System.out.println("Test complete"); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.exit(0); + } + + public void sendMidiMessage(byte[] bytes) { + midiSynthesizer.onReceive(bytes, 0, bytes.length); + } + + public void sendNoteOff(int channel, int pitch, int velocity) { + midiCommand(MidiConstants.NOTE_OFF + channel, pitch, velocity); + } + + public void sendNoteOn(int channel, int pitch, int velocity) { + midiCommand(MidiConstants.NOTE_ON + channel, pitch, velocity); + } + + public void sendControlChange(int channel, int index, int value) { + midiCommand(MidiConstants.CONTROL_CHANGE + channel, index, value); + } + + /** + * @param channel + * @param program starts at zero + */ + private void sendProgramChange(int channel, int program) { + midiCommand(MidiConstants.PROGRAM_CHANGE + channel, program); + + } + + /** + * Send either RPN or NRPN. + */ + public void sendParameter(int channel, int index14, int value14, int controllerXPN) { + int indexLsb = index14 & 0x07F; + int indexMsb = (index14 >> 7) & 0x07F; + int valueLsb = value14 & 0x07F; + int valueMsb = (value14 >> 7) & 0x07F; + sendControlChange(channel, controllerXPN + 1, indexMsb); + sendControlChange(channel, controllerXPN, indexLsb); + sendControlChange(channel, MidiConstants.CONTROLLER_DATA_ENTRY, valueMsb); + sendControlChange(channel, MidiConstants.CONTROLLER_DATA_ENTRY_LSB, valueLsb); + sendControlChange(channel, controllerXPN + 1, 0x7F); // NULL RPN index + sendControlChange(channel, controllerXPN, 0x7F); // to deactivate RPN + } + + public void sendRPN(int channel, int index14, int value14) { + sendParameter(channel, index14, value14, MidiConstants.CONTROLLER_RPN_LSB); + } + + public void sendNRPN(int channel, int index14, int value14) { + sendParameter(channel, index14, value14, MidiConstants.CONTROLLER_NRPN_LSB); + } + + private void midiCommand(int status, int data1, int data2) { + byte[] buffer = new byte[3]; + buffer[0] = (byte) status; + buffer[1] = (byte) data1; + buffer[2] = (byte) data2; + sendMidiMessage(buffer); + } + + private void midiCommand(int status, int data1) { + byte[] buffer = new byte[2]; + buffer[0] = (byte) status; + buffer[1] = (byte) data1; + sendMidiMessage(buffer); + } + + public int test(VoiceDescription description) throws IOException, InterruptedException { + setupSynth(description); + + //playOctaveUsingBend(); + playSameNotesBent(); + + // Setup all the channels. + int maxChannels = 8; + for (int channel = 0; channel < maxChannels; channel++) { + int program = channel; + sendProgramChange(channel, program); + } + playNotePerChannel(maxChannels); + + return 0; + } + + private void playOctaveUsingBend() throws InterruptedException { + sendProgramChange(0, 0); + float range0 = 12.0f; + sendPitchBendRange(0, range0); + for(int i = 0; i < 13; i++) { + System.out.println("Bend to pitch " + i); + sendPitchBend(0, i / range0); + sendNoteOn(0, 60, 100); + synth.sleepFor(0.5); + sendNoteOff(0, 60, 100); + synth.sleepFor(0.5); + } + } + + private void playSameNotesBent() throws InterruptedException { + sendProgramChange(0, 0); + sendProgramChange(1, 0); + float range0 = 2.3f; + float range1 = 6.8f; + sendPitchBendRange(0, range0); + sendPitchBendRange(1, range1); + sendPitchBend(0, 0.0f / range0); // bend by 0 semitones + sendPitchBend(1, 1.0f / range1); // bend by 1 semitones + + System.out.println("These two notes should play at the same pitch."); + sendNoteOn(0, 61, 100); + synth.sleepFor(0.5); + sendNoteOff(0, 61, 100); + + sendNoteOn(1, 60, 100); + synth.sleepFor(0.5); + sendNoteOff(1, 60, 100); + + synth.sleepFor(2.0); + System.out.println("------ done ---------------"); + } + + /** + * + * @param channel + * @param normalizedBend between -1 and +1 + */ + private void sendPitchBend(int channel, float normalizedBend) { + final int BEND_MIN = 0x0000; + final int BEND_CENTER = 0x2000; + final int BEND_MAX = 0x3FFF; + int bend = BEND_CENTER + (int)(BEND_CENTER * normalizedBend); + if (bend < BEND_MIN) bend = BEND_MIN; + else if (bend > BEND_MAX) bend = BEND_MAX; + int lsb = bend & 0x07F; + int msb = (bend >> 7) & 0x07F; + midiCommand(MidiConstants.PITCH_BEND + channel, lsb, msb); + } + + private void sendPitchBendRange(int channel, float range0) { + int semitones = (int)range0; + int cents = (int) (100 * (range0 - semitones)); + int value = (semitones << 7) + cents; + sendRPN(channel, MidiConstants.RPN_BEND_RANGE, value); + } + + private void playNotePerChannel(int maxChannels) throws InterruptedException { + // Play notes on those channels. + for (int channel = 0; channel < maxChannels; channel++) { + sendNoteOn(channel, 60 + channel, 100); + synth.sleepFor(0.5); + sendNoteOff(channel, 60 + channel, 100); + synth.sleepFor(0.5); + } + } + + private void setupSynth(VoiceDescription description) { + synth = JSyn.createSynthesizer(); + + // Add an output. + synth.add(lineOut = new LineOut()); + + voiceDescription = description; + multiSynth = new MultiChannelSynthesizer(); + final int startChannel = 0; + multiSynth.setup(synth, startChannel, NUM_CHANNELS, VOICES_PER_CHANNEL, voiceDescription); + midiSynthesizer = new MidiSynthesizer(multiSynth); + + multiSynth.getOutput().connect(0,lineOut.input, 0); + multiSynth.getOutput().connect(1,lineOut.input, 1); + + // Start synthesizer using default stereo output at 44100 Hz. + synth.start(); + lineOut.start(); + } + +} diff --git a/tests/com/jsyn/examples/SeeOscillators.java b/tests/com/jsyn/examples/SeeOscillators.java index b01e3a9..b8088c4 100644 --- a/tests/com/jsyn/examples/SeeOscillators.java +++ b/tests/com/jsyn/examples/SeeOscillators.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -31,12 +31,15 @@ import javax.swing.JRadioButton; import com.jsyn.JSyn; import com.jsyn.Synthesizer; import com.jsyn.scope.AudioScope; +import com.jsyn.scope.AudioScopeProbe; +import com.jsyn.swing.DoubleBoundedRangeSlider; import com.jsyn.swing.JAppletFrame; import com.jsyn.swing.PortControllerFactory; import com.jsyn.unitgen.ImpulseOscillator; import com.jsyn.unitgen.ImpulseOscillatorBL; import com.jsyn.unitgen.LineOut; import com.jsyn.unitgen.LinearRamp; +import com.jsyn.unitgen.MorphingOscillatorBL; import com.jsyn.unitgen.Multiply; import com.jsyn.unitgen.PulseOscillator; import com.jsyn.unitgen.PulseOscillatorBL; @@ -51,9 +54,9 @@ import com.jsyn.unitgen.TriangleOscillator; import com.jsyn.unitgen.UnitOscillator; /** - * Display each oscillators waveform using the AudioScope. This is a reimplementation of the + * Display each oscillator's waveform using the AudioScope. This is a re-implementation of the * TJ_SeeOsc Applet from the old API. - * + * * @author Phil Burk (C) 2010 Mobileer Inc */ public class SeeOscillators extends JApplet { @@ -66,6 +69,10 @@ public class SeeOscillators extends JApplet { private Multiply oscGain; private ButtonGroup buttonGroup; private LinearRamp freqRamp; + private LinearRamp widthRamp; + private LinearRamp shapeRamp; + private DoubleBoundedRangeSlider widthSlider; + private DoubleBoundedRangeSlider shapeSlider; /* Can be run as either an application or as an applet. */ public static void main(String args[]) { @@ -83,11 +90,13 @@ public class SeeOscillators extends JApplet { add(BorderLayout.NORTH, new JLabel("Show Oscillators in an AudioScope")); scope = new AudioScope(synth); - scope.addProbe(oscGain.output); + AudioScopeProbe probe = scope.addProbe(oscGain.output); + probe.setAutoScaleEnabled(false); + probe.setVerticalScale(1.1); scope.setTriggerMode(AudioScope.TriggerMode.NORMAL); // scope.getModel().getTriggerModel().getLevelModel().setDoubleValue( 0.0001 ); // Turn off the gain and trigger control GUI. - scope.getView().setShowControls(false); + scope.getView().setControlsVisible(false); scope.start(); add(BorderLayout.CENTER, scope.getView()); @@ -101,6 +110,10 @@ public class SeeOscillators extends JApplet { southPanel.add(PortControllerFactory.createExponentialPortSlider(freqRamp.input)); southPanel.add(PortControllerFactory.createExponentialPortSlider(oscGain.inputB)); + southPanel.add(widthSlider = PortControllerFactory.createPortSlider(widthRamp.input)); + widthSlider.setEnabled(false); + southPanel.add(shapeSlider = PortControllerFactory.createPortSlider(shapeRamp.input)); + shapeSlider.setEnabled(false); oscPanel.validate(); validate(); @@ -121,10 +134,21 @@ public class SeeOscillators extends JApplet { freqRamp.input.setName("Frequency"); freqRamp.time.set(0.1); + synth.add(widthRamp = new LinearRamp()); + widthRamp.input.setup(-1.0, 0.0, 1.0); + widthRamp.input.setName("Width"); + widthRamp.time.set(0.1); + + synth.add(shapeRamp = new LinearRamp()); + shapeRamp.input.setup(-1.0, 0.0, 1.0); + shapeRamp.input.setName("Shape"); + shapeRamp.time.set(0.1); + // Add an output so we can hear the oscillators. synth.add(lineOut = new LineOut()); - oscGain.output.connect(lineOut.input); + oscGain.output.connect(0, lineOut.input, 0); + oscGain.output.connect(0, lineOut.input, 1); setupGUI(); @@ -141,6 +165,7 @@ public class SeeOscillators extends JApplet { addOscillator(new SquareOscillatorBL(), "SquareBL"); addOscillator(new PulseOscillator(), "Pulse"); addOscillator(new PulseOscillatorBL(), "PulseBL"); + addOscillator(new MorphingOscillatorBL(), "MorphBL"); addOscillator(new ImpulseOscillator(), "Impulse"); addOscillator(new ImpulseOscillatorBL(), "ImpulseBL"); @@ -159,6 +184,15 @@ public class SeeOscillators extends JApplet { oscillators.add(osc); synth.add(osc); freqRamp.output.connect(osc.frequency); + if (osc instanceof PulseOscillatorBL) { + widthRamp.output.connect(((PulseOscillatorBL)osc).width); + } + if (osc instanceof PulseOscillator) { + widthRamp.output.connect(((PulseOscillator)osc).width); + } + if (osc instanceof MorphingOscillatorBL) { + shapeRamp.output.connect(((MorphingOscillatorBL)osc).shape); + } osc.amplitude.set(1.0); JRadioButton checkBox = new JRadioButton(label); buttonGroup.add(checkBox); @@ -169,6 +203,9 @@ public class SeeOscillators extends JApplet { oscGain.inputA.disconnectAll(0); // Connect this one. osc.output.connect(oscGain.inputA); + widthSlider.setEnabled(osc instanceof PulseOscillator + || osc instanceof PulseOscillatorBL); + shapeSlider.setEnabled(osc instanceof MorphingOscillatorBL); } }); oscPanel.add(checkBox); diff --git a/tests/com/jsyn/examples/UseMidiKeyboard.java b/tests/com/jsyn/examples/UseMidiKeyboard.java index 196f13c..0efa039 100644 --- a/tests/com/jsyn/examples/UseMidiKeyboard.java +++ b/tests/com/jsyn/examples/UseMidiKeyboard.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -26,33 +26,26 @@ import javax.sound.midi.Receiver; import com.jsyn.JSyn; import com.jsyn.Synthesizer; import com.jsyn.devices.javasound.MidiDeviceTools; -import com.jsyn.instruments.SubtractiveSynthVoice; -import com.jsyn.midi.MessageParser; -import com.jsyn.midi.MidiConstants; +import com.jsyn.instruments.DualOscillatorSynthVoice; +import com.jsyn.midi.MidiSynthesizer; import com.jsyn.unitgen.LineOut; -import com.jsyn.unitgen.PowerOfTwo; -import com.jsyn.unitgen.SineOscillator; -import com.jsyn.unitgen.UnitOscillator; -import com.jsyn.util.VoiceAllocator; -import com.softsynth.shared.time.TimeStamp; +import com.jsyn.util.MultiChannelSynthesizer; +import com.jsyn.util.VoiceDescription; /** * Connect a USB MIDI Keyboard to the internal MIDI Synthesizer using JavaSound. - * + * * @author Phil Burk (C) 2010 Mobileer Inc */ public class UseMidiKeyboard { - private static final int MAX_VOICES = 8; + private static final int NUM_CHANNELS = 16; + private static final int VOICES_PER_CHANNEL = 3; + private Synthesizer synth; - private VoiceAllocator allocator; private LineOut lineOut; - private double vibratoRate = 5.0; - private double vibratoDepth = 0.0; - - private UnitOscillator lfo; - private PowerOfTwo powerOfTwo; - private MessageParser messageParser; - private SubtractiveSynthVoice[] voices; + private MidiSynthesizer midiSynthesizer; + private VoiceDescription voiceDescription; + private MultiChannelSynthesizer multiSynth; public static void main(String[] args) { UseMidiKeyboard app = new UseMidiKeyboard(); @@ -77,15 +70,13 @@ public class UseMidiKeyboard { @Override public void send(MidiMessage message, long timeStamp) { byte[] bytes = message.getMessage(); - messageParser.parse(bytes); + midiSynthesizer.onReceive(bytes, 0, bytes.length); } } public int test() throws MidiUnavailableException, IOException, InterruptedException { setupSynth(); - messageParser = new MyParser(); - int result = 2; MidiDevice keyboard = MidiDeviceTools.findKeyboard(); Receiver receiver = new CustomReceiver(); @@ -104,95 +95,27 @@ public class UseMidiKeyboard { return result; } - class MyParser extends MessageParser { - @Override - public void controlChange(int channel, int index, int value) { - // Mod Wheel - if (index == 1) { - vibratoDepth = 0.1 * value / 128.0; - // System.out.println( "vibratoDepth = " + vibratoDepth ); - lfo.amplitude.set(vibratoDepth); - } - // 102 is the index of the first knob on my Axiom 25 - else if (index == 102) { - final double bump = 0.95; - if (value < 64) { - vibratoRate *= bump; - } else { - vibratoRate *= 1.0 / bump; - } - System.out.println("vibratoRate = " + vibratoRate); - lfo.frequency.set(vibratoRate); - } - - } - - @Override - public void noteOff(int channel, int noteNumber, int velocity) { - allocator.noteOff(noteNumber, synth.createTimeStamp()); - } - - @Override - public void noteOn(int channel, int noteNumber, int velocity) { - double frequency = convertPitchToFrequency(noteNumber); - double amplitude = velocity / (4 * 128.0); - TimeStamp timeStamp = synth.createTimeStamp(); - allocator.noteOn(noteNumber, frequency, amplitude, timeStamp); - } - - @Override - public void pitchBend(int channel, int bend) { - double fraction = (bend - MidiConstants.PITCH_BEND_CENTER) - / ((double) MidiConstants.PITCH_BEND_CENTER); - System.out.println("bend = " + bend + ", fraction = " + fraction); - } - } - - /** - * 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#. - */ - double convertPitchToFrequency(double pitch) { - final double concertA = 440.0; - return concertA * Math.pow(2.0, ((pitch - 69) * (1.0 / 12.0))); - } private void setupSynth() { synth = JSyn.createSynthesizer(); - // Add an output. - synth.add(lineOut = new LineOut()); + voiceDescription = DualOscillatorSynthVoice.getVoiceDescription(); +// voiceDescription = SubtractiveSynthVoice.getVoiceDescription(); - synth.add(powerOfTwo = new PowerOfTwo()); - synth.add(lfo = new SineOscillator()); - // Sums pitch modulation. - lfo.output.connect(powerOfTwo.input); - lfo.amplitude.set(vibratoDepth); - lfo.frequency.set(vibratoRate); - - voices = new SubtractiveSynthVoice[MAX_VOICES]; - for (int i = 0; i < MAX_VOICES; i++) { - SubtractiveSynthVoice voice = new SubtractiveSynthVoice(); - synth.add(voice); - powerOfTwo.output.connect(voice.pitchModulation); - voice.getOutput().connect(0, lineOut.input, 0); - voice.getOutput().connect(0, lineOut.input, 1); - voices[i] = voice; - } - allocator = new VoiceAllocator(voices); + multiSynth = new MultiChannelSynthesizer(); + final int startChannel = 0; + multiSynth.setup(synth, startChannel, NUM_CHANNELS, VOICES_PER_CHANNEL, voiceDescription); + midiSynthesizer = new MidiSynthesizer(multiSynth); + + // Create a LineOut for the entire synthesizer. + synth.add(lineOut = new LineOut()); + multiSynth.getOutput().connect(0,lineOut.input, 0); + multiSynth.getOutput().connect(1,lineOut.input, 1); // 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(); - // Get synthesizer time in seconds. - double timeNow = synth.getCurrentTime(); - - // Advance to a near future time so we have a clean start. - double time = timeNow + 0.5; - } } diff --git a/tests/com/jsyn/research/lambdas/LambdaUnits.java b/tests/com/jsyn/research/lambdas/LambdaUnits.java new file mode 100644 index 0000000..42807ac --- /dev/null +++ b/tests/com/jsyn/research/lambdas/LambdaUnits.java @@ -0,0 +1,22 @@ +package com.jsyn.research.lambdas; + +import java.util.function.BinaryOperator; + +public class LambdaUnits { + + + public static void main(String[] args) { + test(); + } + + void tryLambda(BinaryOperator op) { + double result = op.apply(3.0, 4.0); + System.out.println("result = " + result); + } + + private static void test() { + System.out.println("Test Lambdas"); + // Need Java 8! tryLambda((x, y) -> (x * y)); + } + +} diff --git a/tests/com/jsyn/unitgen/TestMath.java b/tests/com/jsyn/unitgen/TestMath.java index 0fde9b5..cae1dea 100644 --- a/tests/com/jsyn/unitgen/TestMath.java +++ b/tests/com/jsyn/unitgen/TestMath.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,6 +19,7 @@ package com.jsyn.unitgen; import junit.framework.TestCase; import com.jsyn.engine.SynthesisEngine; +import com.softsynth.math.AudioMath; /** * @author Phil Burk, (C) 2009 Mobileer Inc @@ -388,5 +389,25 @@ public class TestMath extends TestCase { assertEquals("PowerOfTwo", Math.pow(2.0, in), powerOfTwo.output.getValue(), 0.001); } } + public void testPitchToFrequency() { + PitchToFrequency ugen = new PitchToFrequency(); + ugen.setSynthesisEngine(synthesisEngine); + final double smallValue = -1.5308084989341915E-17; + double values[] = { + 49.0, 49.5, 50.0 + smallValue, + 60.0 -smallValue, + 79.2, 12.9, 118.973 + }; + // Sanity check AudioMath + assertEquals("PitchToFrequency", 440.0, AudioMath.pitchToFrequency(69), 0.001); + assertEquals("PitchToFrequency", 660.0, AudioMath.pitchToFrequency(69+7.02), 0.1); + + for (double pitch : values) { + ugen.input.setValueInternal(pitch); + ugen.generate(); + assertEquals("PitchToFrequency", AudioMath.pitchToFrequency(pitch), + ugen.output.getValue(), 0.001); + } + } } -- cgit v1.2.3