This shows you the differences between two versions of the page.
Both sides previous revision Previous revision Next revision | Previous revision | ||
the_synthesizer [2017/08/30 21:22] shane [SynthVoice] |
the_synthesizer [2017/08/30 23:49] (current) shane [SynthOscillator] |
||
---|---|---|---|
Line 138: | Line 138: | ||
===== SynthOscillator ===== | ===== SynthOscillator ===== | ||
+ | VanillaJuce' | ||
+ | <code cpp> | ||
+ | class SynthOscillator | ||
+ | { | ||
+ | private: | ||
+ | SynthOscillatorWaveform waveForm; | ||
+ | double phase; | ||
+ | double phaseDelta; | ||
+ | |||
+ | public: | ||
+ | SynthOscillator(); | ||
+ | | ||
+ | void setWaveform(SynthOscillatorWaveform wf) { waveForm = wf; } | ||
+ | void setFrequency(double cyclesPerSample); | ||
+ | |||
+ | float getSample (); | ||
+ | }; | ||
+ | |||
+ | SynthOscillator:: | ||
+ | : waveForm(kSawtooth) | ||
+ | , phase(0) | ||
+ | , phaseDelta(0) | ||
+ | { | ||
+ | } | ||
+ | |||
+ | void SynthOscillator:: | ||
+ | { | ||
+ | phaseDelta = cyclesPerSample; | ||
+ | } | ||
+ | |||
+ | float SynthOscillator:: | ||
+ | { | ||
+ | float sample = 0.0f; | ||
+ | switch (waveForm) | ||
+ | { | ||
+ | case kSine: | ||
+ | sample = (float)(std:: | ||
+ | break; | ||
+ | case kSquare: | ||
+ | sample = (phase <= 0.5) ? 1.0f : -1.0f; | ||
+ | break; | ||
+ | case kTriangle: | ||
+ | sample = (float)(2.0 * (0.5 - std:: | ||
+ | break; | ||
+ | case kSawtooth: | ||
+ | sample = (float)(2.0 * phase - 1.0); | ||
+ | break; | ||
+ | } | ||
+ | |||
+ | phase += phaseDelta; | ||
+ | while (phase > 1.0) phase -= 1.0; | ||
+ | |||
+ | return sample; | ||
+ | } | ||
+ | </ | ||
+ | Every time // | ||
+ | |||
+ | This simplistic code is acceptable for low-frequency oscillators (LFOs), but it's not good enough for audio-frequency oscillators, | ||
+ | |||
+ | VanillaJuce is essentially an early iteration of a project which was eventually renamed [[sarah|SARAH]] (// | ||
===== SynthEnvelopeGenerator ===== | ===== SynthEnvelopeGenerator ===== | ||
+ | The // | ||
+ | * 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 // | ||
+ | <code cpp> | ||
+ | typedef enum | ||
+ | { | ||
+ | kIdle, | ||
+ | kAttack, | ||
+ | kDecay, | ||
+ | kSustain, | ||
+ | kRelease | ||
+ | } EG_Segment; | ||
+ | |||
+ | class SynthEnvelopeGenerator | ||
+ | { | ||
+ | private: | ||
+ | double sampleRateHz; | ||
+ | LinearSmoothedValue< | ||
+ | EG_Segment segment; | ||
+ | |||
+ | public: | ||
+ | double attackSeconds, | ||
+ | double sustainLevel; | ||
+ | |||
+ | public: | ||
+ | SynthEnvelopeGenerator(); | ||
+ | |||
+ | void start(double _sampleRateHz); | ||
+ | void release(); | ||
+ | bool isRunning() { return segment != kIdle; } | ||
+ | float getSample (); | ||
+ | }; | ||
+ | </ | ||
+ | // | ||
+ | It has several member functions: the following four of which are used in // | ||
+ | * // | ||
+ | * //reset()// takes a sampling rate in Hz and a ramp time in seconds, and prepares the interpolator. | ||
+ | * // | ||
+ | * // | ||
+ | |||
+ | Unfortunately, | ||
+ | <code cpp> | ||
+ | SynthEnvelopeGenerator:: | ||
+ | : sampleRateHz(44100) | ||
+ | , attackSeconds(0.01) | ||
+ | , decaySeconds(0.1) | ||
+ | , releaseSeconds(0.5) | ||
+ | , sustainLevel(0.5) | ||
+ | , segment(kIdle) | ||
+ | { | ||
+ | interpolator.setValue(0.0); | ||
+ | interpolator.reset(sampleRateHz, | ||
+ | } | ||
+ | |||
+ | void SynthEnvelopeGenerator:: | ||
+ | { | ||
+ | sampleRateHz = _sampleRateHz; | ||
+ | |||
+ | if (segment == kIdle) | ||
+ | { | ||
+ | // start new attack segment from zero | ||
+ | interpolator.setValue(0.0); | ||
+ | interpolator.reset(sampleRateHz, | ||
+ | } | ||
+ | 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, | ||
+ | } | ||
+ | |||
+ | segment = kAttack; | ||
+ | interpolator.setValue(1.0); | ||
+ | } | ||
+ | |||
+ | void SynthEnvelopeGenerator:: | ||
+ | { | ||
+ | segment = kRelease; | ||
+ | interpolator.setValue(interpolator.getNextValue()); | ||
+ | interpolator.reset(sampleRateHz, | ||
+ | interpolator.setValue(0.0); | ||
+ | } | ||
+ | |||
+ | float SynthEnvelopeGenerator:: | ||
+ | { | ||
+ | if (segment == kSustain) return float(sustainLevel); | ||
+ | |||
+ | if (interpolator.isSmoothing()) return float(interpolator.getNextValue()); | ||
+ | |||
+ | if (segment == kAttack) | ||
+ | { | ||
+ | if (decaySeconds > 0.0) | ||
+ | { | ||
+ | // there is a decay segment | ||
+ | segment = kDecay; | ||
+ | interpolator.reset(sampleRateHz, | ||
+ | interpolator.setValue(sustainLevel); | ||
+ | return 1.0; | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | // no decay segment; go straight to sustain | ||
+ | segment = kSustain; | ||
+ | return float(sustainLevel); | ||
+ | } | ||
+ | } | ||
+ | else if (segment == kDecay) | ||
+ | { | ||
+ | segment = kSustain; | ||
+ | return float(sustainLevel); | ||
+ | } | ||
+ | else if (segment == kRelease) | ||
+ | { | ||
+ | segment = kIdle; | ||
+ | } | ||
+ | |||
+ | // after end of release segment | ||
+ | return 0.0f; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Remember where I talked about how // | ||
===== 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 // | ||
+ | - The // | ||
+ | - The // | ||
+ | - Because the // | ||
+ | - 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: | ||
+ | - // | ||
+ | - The // | ||
+ | - The // | ||
+ | - Eventually, the test of // | ||
+ | |||
+ | There are two special voice-assignment scenarios you should be aware of. The first one, //note reassignment// | ||
+ | |||
+ | 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 // | ||