Both sides previous revision
Previous revision
Next revision
|
Previous revision
|
the_synthesizer [2017/08/30 22:19] shane [SynthOscillator] |
the_synthesizer [2017/08/30 23:49] (current) shane [SynthOscillator] |
This simplistic code is acceptable for low-frequency oscillators (LFOs), but it's not good enough for audio-frequency oscillators, because mathematical functions which define the waveforms are not //band-limited// (with the exception of the sine waveform, which is in fact perfectly band-limited). As a result, higher-frequency harmonics will be "aliased" to completely different audio frequencies when you play higher notes. | This simplistic code is acceptable for low-frequency oscillators (LFOs), but it's not good enough for audio-frequency oscillators, because mathematical functions which define the waveforms are not //band-limited// (with the exception of the sine waveform, which is in fact perfectly band-limited). As a result, higher-frequency harmonics will be "aliased" to completely different audio frequencies when you play higher notes. |
| |
VanillaJuce is essentially an early iteration of a project which was eventually renamed //SARAH// (//Synthèse à Rapide Analyse Harmonique//, or "synthesis with fast harmonic analysis"), which I plan to publish soon. | VanillaJuce is essentially an early iteration of a project which was eventually renamed [[sarah|SARAH]] (//Synthèse à Rapide Analyse Harmonique//, or "synthesis with fast harmonic analysis"), which I plan to publish soon. |
| |
===== SynthEnvelopeGenerator ===== | ===== SynthEnvelopeGenerator ===== |
| The //SynthEnvelopeGenerator// class implements a simple "ADSR" envelope function with a linear Attack and Decay ramps, a constant Sustain level, and a linear Release ramp. //juce::LinearSmoothedValue// is used to facilitate generating the linear ramps. To understand the code, it will be helpful to understand that the ADSR envelope always begins and ends at the value 0.0: |
| * The Attack phase always ramps from 0.0 up to 1.0. |
| * The Sustain level is some fraction in the range 0.0 to 1.0. |
| * The Release phase ramps from the Sustain level back to 0.0. |
| |
| Here is the class declaration for //SynthEnvelopeGenerator//: |
| <code cpp> |
| typedef enum |
| { |
| kIdle, |
| kAttack, |
| kDecay, |
| kSustain, |
| kRelease |
| } EG_Segment; |
| |
| class SynthEnvelopeGenerator |
| { |
| private: |
| double sampleRateHz; |
| LinearSmoothedValue<double> interpolator; |
| EG_Segment segment; |
| |
| public: |
| double attackSeconds, decaySeconds, releaseSeconds; |
| double sustainLevel; // [0.0, 1.0] |
| |
| public: |
| SynthEnvelopeGenerator(); |
| |
| void start(double _sampleRateHz); // called for note-on |
| void release(); // called for note-off |
| bool isRunning() { return segment != kIdle; } |
| float getSample (); |
| }; |
| </code> |
| //juce::LinearSmoothedValue// is a template class, which in this case is instantiated with a base type of //double//, to define the member variable //interpolator//. |
| It has several member functions: the following four of which are used in //SynthEnvelopeGenerator//: |
| * //setValue()// is used to set the "target value" that the interpolator will be ramping up (or down) to. |
| * //reset()// takes a sampling rate in Hz and a ramp time in seconds, and prepares the interpolator. |
| * //isSmoothing()// returns //true// if the interpolator's current value has not yet reached the target value, //false// if it has. |
| * //getNextValue()// advances the interpolator by one step (one sample time), and returns the new current value. |
| |
| Unfortunately, the //juce::LinearSmoothedValue// class does //not// provide a function to set the interpolator's current value, so we have to resort to calling //setValue()// to set the target value, followed immediately by a call to //reset()//, which happens to set the current value to the target value (I only know this because I peeked at the //juce::LinearSmoothedValue// source code), followed by a second call to //setValue()// to set the new target value. You'll see this pattern more than once in the //SynthEnvelopeGenerator// code: |
| <code cpp> |
| SynthEnvelopeGenerator::SynthEnvelopeGenerator() |
| : sampleRateHz(44100) |
| , attackSeconds(0.01) |
| , decaySeconds(0.1) |
| , releaseSeconds(0.5) |
| , sustainLevel(0.5) |
| , segment(kIdle) |
| { |
| interpolator.setValue(0.0); |
| interpolator.reset(sampleRateHz, 0.0); |
| } |
| |
| void SynthEnvelopeGenerator::start (double _sampleRateHz) |
| { |
| sampleRateHz = _sampleRateHz; |
| |
| if (segment == kIdle) |
| { |
| // start new attack segment from zero |
| interpolator.setValue(0.0); |
| interpolator.reset(sampleRateHz, attackSeconds); |
| } |
| else |
| { |
| // note is still playing but has been retriggered or stolen |
| // start new attack from where we are |
| double currentValue = interpolator.getNextValue(); |
| interpolator.setValue(currentValue); |
| interpolator.reset(sampleRateHz, attackSeconds * (1.0 - currentValue)); |
| } |
| |
| segment = kAttack; |
| interpolator.setValue(1.0); |
| } |
| |
| void SynthEnvelopeGenerator::release() |
| { |
| segment = kRelease; |
| interpolator.setValue(interpolator.getNextValue()); |
| interpolator.reset(sampleRateHz, releaseSeconds); |
| interpolator.setValue(0.0); |
| } |
| |
| float SynthEnvelopeGenerator::getSample() |
| { |
| if (segment == kSustain) return float(sustainLevel); |
| |
| if (interpolator.isSmoothing()) return float(interpolator.getNextValue()); |
| |
| if (segment == kAttack) // end of attack segment |
| { |
| if (decaySeconds > 0.0) |
| { |
| // there is a decay segment |
| segment = kDecay; |
| interpolator.reset(sampleRateHz, decaySeconds); |
| interpolator.setValue(sustainLevel); |
| return 1.0; |
| } |
| else |
| { |
| // no decay segment; go straight to sustain |
| segment = kSustain; |
| return float(sustainLevel); |
| } |
| } |
| else if (segment == kDecay) // end of decay segment |
| { |
| segment = kSustain; |
| return float(sustainLevel); |
| } |
| else if (segment == kRelease) // end of release |
| { |
| segment = kIdle; |
| } |
| |
| // after end of release segment |
| return 0.0f; |
| } |
| </code> |
| |
| Remember where I talked about how //juce::Synthesizer// will re-trigger a voice back to its attack phase if the same MIDI note goes on, then off, then on again? That requires an even uglier version of the //setValue//, //reset//, //setValue// sequence of function calls, where the argument to the initial //setValue()// call is obtained by calling //getNextValue()//, to ensure that the new ramp begins exactly where the one being truncated leaves off, to avoid another type of "click" transient. |
| |
===== Summary: What happens when you play a note ===== | ===== Summary: What happens when you play a note ===== |
| When the VanillaJuce plugin is compiled and instantiated in a DAW, and the user presses down a note on a MIDI keyboard (or the same sequence of MIDI-events occurs during the playback of a recorded MIDI sequence), the following things happen: |
| - The //juce::Synthesiser::noteOn()// member function is called, with ''this'' pointing to the one and only //Synth// object (member variable //synth// of //VanillaJuceAudioProcessor//) |
| - The //juce::Synthesiser// code assigns (or re-assigns) one of the sixteen available //SynthVoice// objects to play the note, and calls that object's //startNote()// function. |
| - The //SynthVoice::startNote()// code (see above) sets up its two oscillators and envelope-generator according to the current program/patch parameters, and starts the note sounding by calling the envelope-generator's //start()// function. |
| - Because the //SynthVoice// instance is now "active", calls made from the plugin host to //juce::Synthesiser::renderNextBlock()// are passed on to //SynthVoice::renderNextBlock()// (see above), which generates the required number of samples, //adding them in// to the supplied //juce::AudioSampleBuffer// so that all active voices are effectively summed (mixed with equal intensity) to the output. |
| - With each successive sample, the envelope generator is advanced through the ADSR shape. |
| |
| When the MIDI note-off event occurs in the MIDI input sequence: |
| - //juce::Synthesiser::noteOff()// is called. |
| - The //juce::Synthesiser// code uses the MIDI note-number to determine which of the currently-active //SynthVoice// is playing the note, and calls that object's //stopNote()// function, with the //allowTailOff// argument set to //true//. |
| - The //SynthVoice::stopNote()// code (see above) forces the ADSR envelope generator to go straight to the start of its Release phase. |
| - Eventually, the test of //ampEG.isRunning()// in //SynthVoice::renderNextBlock()// returns //false//, because the envelope generator has reached the end of the Release ramp, and //juce::SynthesiserVoice::clearCurrentNote()// gets called; this causes the voice to become inactive, suppressing further calls to //SynthVoice::renderNextBlock()// for that voice. |
| |
| There are two special voice-assignment scenarios you should be aware of. The first one, //note reassignment// was already discussed above. If a MIDI note-on event occurs while there is already an active voice sounding the same MIDI note-number, //juce::Synthesiser::noteOn()// will simply call //startNote()// again on the active //SynthVoice// instance. Care must then be taken to ensure that there is not much of an audible "click" as the note goes back to its Attack phase. |
| |
| The second special case concerns what happens when the synthesizer runs out of voices. That case is actually almost identical to the first one; the only difference is how the //juce::Synthesiser// code selects which active voice to reassign---a process called //note-stealing//. Have a look at the //juce::Synthesiser// source code to learn exactly how its note-stealing algorithm works, and be aware that this is just one of several possible ways to do it. If you wanted a different note-stealing algorithm, you would simply have to override more of the //juce::Synthesiser// member functions. |
| |