/* * Copyright 2016 Phil Burk, Mobileer Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jsyn.util; import com.jsyn.Synthesizer; import com.jsyn.engine.SynthesisEngine; import com.jsyn.midi.MidiConstants; import com.jsyn.ports.UnitInputPort; import com.jsyn.ports.UnitOutputPort; import com.jsyn.unitgen.ExponentialRamp; import com.jsyn.unitgen.LinearRamp; import com.jsyn.unitgen.Multiply; import com.jsyn.unitgen.Pan; import com.jsyn.unitgen.PowerOfTwo; import com.jsyn.unitgen.SineOscillator; import com.jsyn.unitgen.TwoInDualOut; import com.jsyn.unitgen.UnitGenerator; import com.jsyn.unitgen.UnitOscillator; import com.jsyn.unitgen.UnitVoice; import com.softsynth.math.AudioMath; import com.softsynth.shared.time.TimeStamp; /** * General purpose synthesizer with "channels" that could be used to implement a MIDI synthesizer. * Each channel has: * *
 * 
 * lfo -> pitchToLinear -> [VOICES] -> volume* -> panner
 * bend --/
 * 
 * 
* * Note: this class is experimental and subject to change. * * @author Phil Burk (C) 2016 Mobileer Inc */ public class MultiChannelSynthesizer { private Synthesizer synth; private TwoInDualOut outputUnit; private ChannelContext[] channels; private final static int MAX_VELOCITY = 127; private final static int DEFAULT_RHYTHM_CHANNEL = 9; // known as channel "10" by musicians // Use preset 128 as a special code to indicate that a voice is being used for rhythm. private final static int RHYTHM_PRESET = 128; private double mMasterAmplitude = 0.25; private class ChannelGroupContext { private VoiceDescription voiceDescription; private UnitVoice[] voices; private VoiceAllocator allocator; ChannelGroupContext(int numVoices, VoiceDescription voiceDescription) { this.voiceDescription = voiceDescription; voices = new UnitVoice[numVoices]; for (int i = 0; i < numVoices; i++) { UnitVoice voice = voiceDescription.createUnitVoice(); UnitGenerator ugen = voice.getUnitGenerator(); synth.add(ugen); voices[i] = voice; } allocator = new VoiceAllocator(voices); } } private class ChannelContext { private UnitOscillator lfo; private PowerOfTwo pitchToLinear; private LinearRamp timbreRamp; private LinearRamp pressureRamp; private ExponentialRamp volumeRamp; private Multiply volumeMultiplier; private Pan panner; private double vibratoRate = 5.0; private double bendRangeOctaves = 2.0 / 12.0; private int presetIndex; private ChannelGroupContext groupContext; private int channelIndex; private boolean rhythm; // Is this channel a drum channel or a melodic channel? ChannelContext(int channelIndex) { this.channelIndex = channelIndex; if (channelIndex == DEFAULT_RHYTHM_CHANNEL) { rhythm = true; presetIndex = RHYTHM_PRESET; } } VoiceOperation voiceOperation = new VoiceOperation() { @Override public void operate(UnitVoice voice) { voice.usePreset(presetIndex); connectVoice(voice); } }; void setup(ChannelGroupContext groupContext) { this.groupContext = groupContext; synth.add(pitchToLinear = new PowerOfTwo()); synth.add(lfo = new SineOscillator()); // TODO use a MorphingOscillator or switch between S&H etc. // Use a ramp to smooth out the timbre changes. // This helps reduce pops from changing filter cutoff too abruptly. synth.add(timbreRamp = new LinearRamp()); timbreRamp.time.set(0.02); synth.add(pressureRamp = new LinearRamp()); pressureRamp.time.set(0.02); synth.add(volumeRamp = new ExponentialRamp()); volumeRamp.input.set(1.0); volumeRamp.time.set(0.02); synth.add(volumeMultiplier = new Multiply()); synth.add(panner = new Pan()); pitchToLinear.input.setValueAdded(true); // so we can sum pitch bend lfo.output.connect(pitchToLinear.input); lfo.amplitude.set(0.0); lfo.frequency.set(vibratoRate); volumeRamp.output.connect(volumeMultiplier.inputB); volumeMultiplier.output.connect(panner.input); panner.output.connect(0, outputUnit.inputA, 0); // Use MultiPassthrough panner.output.connect(1, outputUnit.inputB, 0); } private void connectVoice(UnitVoice voice) { UnitGenerator ugen = voice.getUnitGenerator(); // Hook up some channel controllers to standard ports on the voice. UnitInputPort freqMod = (UnitInputPort) ugen .getPortByName(UnitGenerator.PORT_NAME_FREQUENCY_SCALER); if (freqMod != null) { freqMod.disconnectAll(); pitchToLinear.output.connect(freqMod); } UnitInputPort timbrePort = (UnitInputPort) ugen .getPortByName(UnitGenerator.PORT_NAME_TIMBRE); if (timbrePort != null) { timbrePort.disconnectAll(); timbreRamp.output.connect(timbrePort); timbreRamp.input.setup(timbrePort); } UnitInputPort pressurePort = (UnitInputPort) ugen .getPortByName(UnitGenerator.PORT_NAME_PRESSURE); if (pressurePort != null) { pressurePort.disconnectAll(); pressureRamp.output.connect(pressurePort); pressureRamp.input.setup(pressurePort); } voice.getOutput().disconnectAll(); voice.getOutput().connect(volumeMultiplier.inputA); // mono mix all the voices } void programChange(int program) { if (!rhythm) { int programWrapped = program % groupContext.voiceDescription.getPresetCount(); String name = groupContext.voiceDescription.getPresetNames()[programWrapped]; // LOGGER.debug("Preset[" + program + "] = " + name); presetIndex = programWrapped; } } /** * Combine channel and noteNumber in case we are sharing a VoiceAllocator across multiple * channels. * * @param noteNumber * @return a tag that is unique per channel and note */ private int makeNoteTag(int noteNumber) { return (channelIndex << 8) + noteNumber; } void noteOff(int noteNumber, double amplitude) { noteOff(noteNumber, amplitude, synth.createTimeStamp()); } void noteOff(int noteNumber, double amplitude, TimeStamp timeStamp) { groupContext.allocator.noteOff(makeNoteTag(noteNumber), timeStamp); } void noteOn(int noteNumber, double amplitude) { noteOn(noteNumber, amplitude, synth.createTimeStamp()); } void noteOn(int noteNumber, double amplitude, TimeStamp timeStamp) { double frequency = AudioMath.pitchToFrequency(noteNumber); groupContext.allocator.noteOn(makeNoteTag(noteNumber), frequency, amplitude, voiceOperation, timeStamp); } public void setPitchBend(double offset) { pitchToLinear.input.set(bendRangeOctaves * offset); } public void setBendRange(double semitones) { bendRangeOctaves = semitones / 12.0; } public void setVibratoDepth(double semitones) { lfo.amplitude.set(semitones); } public void setVolume(double volume) { double min = SynthesisEngine.DB96; double max = 1.0; double ratio = max / min; double value = min * Math.pow(ratio, volume); volumeRamp.input.set(value); } public void setPan(double pan) { panner.pan.set(pan); } /* * @param timbre normalized 0 to 1 */ public void setTimbre(double timbre) { double min = timbreRamp.input.getMinimum(); double max = timbreRamp.input.getMaximum(); double value = min + (timbre * (max - min)); timbreRamp.input.set(value); } /* * @param pressure normalized 0 to 1 */ public void setPressure(double pressure) { double min = pressureRamp.input.getMinimum(); double max = pressureRamp.input.getMaximum(); double ratio = max / min; double value = min * Math.pow(ratio, pressure); pressureRamp.input.set(value); } } /** * Construct a synthesizer with a maximum of 16 channels like MIDI. */ public MultiChannelSynthesizer() { this(MidiConstants.MAX_CHANNELS); } public MultiChannelSynthesizer(int maxChannels) { channels = new ChannelContext[maxChannels]; for (int i = 0; i < channels.length; i++) { channels[i] = new ChannelContext(i); } } /** * 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()); } int voicesPerChannelGroupContext = numChannels * voicesPerChannel; ChannelGroupContext groupContext = new ChannelGroupContext(voicesPerChannelGroupContext, voiceDescription); for (int i = 0; i < numChannels; i++) { channels[startChannel + i].setup(groupContext); } } public void programChange(int channel, int program) { ChannelContext channelContext = channels[channel]; channelContext.programChange(program); } /** * Turn off a note. * * @param channel * @param noteNumber * @param velocity between 0 and 127, will be scaled by masterAmplitude */ public void noteOff(int channel, int noteNumber, int velocity) { double amplitude = velocity * (1.0 / MAX_VELOCITY); noteOff(channel, noteNumber, amplitude); } /** * Turn off a note. * * @param channel * @param noteNumber * @param amplitude between 0 and 1.0, will be scaled by masterAmplitude */ public void noteOff(int channel, int noteNumber, double amplitude) { ChannelContext channelContext = channels[channel]; channelContext.noteOff(noteNumber, amplitude * mMasterAmplitude); } /** * Turn off a note. * * @param channel * @param noteNumber * @param amplitude between 0 and 1.0, will be scaled by masterAmplitude */ public void noteOff(int channel, int noteNumber, double amplitude, TimeStamp timeStamp) { ChannelContext channelContext = channels[channel]; channelContext.noteOff(noteNumber, amplitude * mMasterAmplitude, timeStamp); } /** * Turn on a note. * * @param channel * @param noteNumber * @param velocity between 0 and 127, will be scaled by masterAmplitude */ public void noteOn(int channel, int noteNumber, int velocity) { double amplitude = velocity * (1.0 / MAX_VELOCITY); noteOn(channel, noteNumber, amplitude); } /** * Turn on a note. * * @param channel * @param noteNumber * @param amplitude between 0 and 1.0, will be scaled by masterAmplitude */ public void noteOn(int channel, int noteNumber, double amplitude, TimeStamp timeStamp) { ChannelContext channelContext = channels[channel]; channelContext.noteOn(noteNumber, amplitude * mMasterAmplitude, timeStamp); } /** * Turn on a note. * * @param channel * @param noteNumber * @param amplitude between 0 and 1.0, will be scaled by masterAmplitude */ public void noteOn(int channel, int noteNumber, double amplitude) { ChannelContext channelContext = channels[channel]; channelContext.noteOn(noteNumber, amplitude * mMasterAmplitude); } /** * Set a pitch offset that will be scaled by the range for the channel. * * @param channel * @param offset ranges from -1.0 to +1.0 */ public void setPitchBend(int channel, double offset) { // LOGGER.debug("setPitchBend[" + channel + "] = " + offset); ChannelContext channelContext = channels[channel]; channelContext.setPitchBend(offset); } public void setBendRange(int channel, double semitones) { ChannelContext channelContext = channels[channel]; channelContext.setBendRange(semitones); } public void setPressure(int channel, double pressure) { ChannelContext channelContext = channels[channel]; channelContext.setPressure(pressure); } public void setVibratoDepth(int channel, double semitones) { ChannelContext channelContext = channels[channel]; channelContext.setVibratoDepth(semitones); } public void setTimbre(int channel, double timbre) { ChannelContext channelContext = channels[channel]; channelContext.setTimbre(timbre); } /** * Set volume for entire channel. * * @param channel * @param volume normalized between 0.0 and 1.0 */ public void setVolume(int channel, double volume) { ChannelContext channelContext = channels[channel]; channelContext.setVolume(volume); } /** * Pan from left to right. * * @param channel * @param pan ranges from -1.0 to +1.0 */ public void setPan(int channel, double pan) { ChannelContext channelContext = channels[channel]; channelContext.setPan(pan); } /** * @return stereo output port */ public UnitOutputPort getOutput() { return outputUnit.output; } /** * Set amplitude for a single voice when the velocity is 127. * * @param masterAmplitude */ public void setMasterAmplitude(double masterAmplitude) { mMasterAmplitude = masterAmplitude; } public double getMasterAmplitude() { return mMasterAmplitude; } }