diff options
author | Phil Burk <[email protected]> | 2016-08-02 07:52:17 -0700 |
---|---|---|
committer | Phil Burk <[email protected]> | 2016-10-24 08:29:20 -0700 |
commit | 580fea450ec0982d0bd8be589f00566267e7b0d1 (patch) | |
tree | 0420f768fc7c63208b1720232c447e17af9017af /src | |
parent | a6583e89166f7477a675cf3094a91b303ba7850a (diff) |
Instruments: add better synth, pitch control
Diffstat (limited to 'src')
36 files changed, 1360 insertions, 210 deletions
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 - * + * <pre> 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<Component> sliders = new ArrayList<Component>(); 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<UnitGenerator> units = new ArrayList<UnitGenerator>(); + private final LinkedHashMap<String, UnitPort> portAliases = new LinkedHashMap<String, UnitPort>(); + @Override public void generate(int start, int limit) { for (UnitGenerator unit : units) { @@ -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. * <P> - * + * * @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: + * <pre><code> + * lfo -> pitchToLinear -> [VOICES] -> volume* -> panner + * bend --/ + * </code></pre> + * + * Note: this class is experimental and subject to change. + * + * @author Phil Burk (C) 2016 Mobileer Inc + */ +public class MultiChannelSynthesizer { + private Synthesizer synth; + private TwoInDualOut outputUnit; + private ChannelContext[] channels; + + private 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); } } |