diff options
Diffstat (limited to 'src/com/jsyn/util/MultiChannelSynthesizer.java')
-rw-r--r-- | src/com/jsyn/util/MultiChannelSynthesizer.java | 303 |
1 files changed, 303 insertions, 0 deletions
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; + } + +} |