Synth work in progress - 8 Tone Generators/Envelopes/Effects/LFO's/Filters

Last two months I have been working at least 8 hours a day on my first software synthesizer. My C++ and JUCE skills are mediocre at best, but still I have managed to create something I think is quite unique. Note sometimes I got lots of invaluable help from other JUCE users.

Specs so far, but I come up with new interesting (crazy) stuff almost daily!

  • 8 Tone Generators - Up to 16 Unison each, and each with literally a billion+ waveforms to make and use with my waveform creator! When used as a source for AM/RM/FM/PM/PD modulation, Waveshaping, and Distortion, each can be set to 4 frequency modes (Note, Fixed, LFO Note dependent, and LFO Fixed). Pulse width on any type of waveform.
  • 8 Envelopes - Assignable multiple targets, as for example each tone generator can have its own envelope.
  • 8 Effect slots - 9 different effects
  • 8 LFO’s - Assignable to multiple targets, each target with option to override LFO level and depth. About 69000 different LFO waveforms.
  • 8 Filters - Assignable to any tone generator, even in series with different cutoff and/or resonance.
  • 19 Duty Cycle (Pulse With) types
  • 30+ Modulation types and variations
  • 30+ Waveshaping types, with source of tone generator waveshaping can be itself, or another tone generator for some truly interesting (weird) sounds!
  • 8 Tone Generator Morphing types, each with 6 different frequency variations, and each with adjustable speed. Flexible so for example one tone generator can be morphed at one speed to another tone generator, that it self is being morphed at same or a different speed, from a third tone generator!
  • 39 Distortion types, with source of tone generator distortion can be itself, or another tone generator for some truly interesting (weird) sounds!
  • Advanced real-time Waveform Creator which can mix up to 4 of various base waveform types, split same into segments, put into symmetry, adjust level, distortion type, LFO modulation, curve adjustment, cycle size, and related “Change Over Time” adjustment. All-in-all with a “Random” button there is literally a billion+ variations, although admittedly many will sound similar especially on higher frequency notes.
  • Multiple manual waveform editing options, such as set area of waveform to be edited, scroll waveform or part of it up or down.
  • Flexible storage options, that is each module’s, that being a tone generator, envelope, effect, LFO, or filter, parameters can be saved as a preset. Same with an entire “Strip” of before mentioned modules, and off course finally everything can be saved as a “Patch”.
  • Background and Knob “theme” instant variations.

Anyways I am posting this synthesizer information and video clip, where I briefly demonstrate most of the above, here for inspiration to others and maybe some feedback. If anyone has any question on how I did something, please feel free to ask.

7 Likes

Congrats! It always feels great to reach milestones! :slight_smile:

That looks like a great job, there’s a lot of work behind that beast and I like the “route everything” aproach you took even if it has fixed “modules” to choose from, it gives more flexibility. I’m really interested on how you aproached all the modulation routing as it’s one of the things I always think there must be a better/more efficient aproach out there than what I actually do, but it’s not a very treated topic here or in other forums and the few other implementations I saw out there looked too overcomplicated.
Bonus question: is it really CPU hungry when all the tone generators, filters, LFO and modulations in general running at same?

Since my knowledge of JUCE is not that great, I am probably doing many things the “improper” way. Also C++ object oriented language, constructor headers and the advanced OOP stuff still confuses me. No give me some assembly language and I am good to go! Besides this synthesizer, I only made one JUCE program before, a Breakout game. Check it out here if you get a chance - Making an arcade game with JUCE? Absolutely just look here! - #5 by DKDiveDude.

First off I don’t use SynthSound at all, not a single line of code. In SynthVoice I do all 8 tone generators, meaning I use a buffer of 16 float channels, 8 stereo channels, from renderNextBlock in PluginProcessor to SynthVoice, which has its own local set of 8 stereo float buffers.

To minimize if statements in SynthVoice’s buffer sample loop, I do a lot of “status” variable setting in the PluginEditor. Then outside of the main buffer loop, SynthVoice’s local variables are updated.

All the hard LFO matrix work is also done in the PluginEditor and more specifically in my small pop-up “LFO Target” component via an intricate variable system so for example back in SynthVoice for a tone generator’s “Fine Pitch” LFO multiplier used for Vibrato, it is just retrieved once per buffer sample loop, after actually, like the following where “tg” is the tone generator one of eight;

lfoMultiplier = lfoClass[lfoTargetTGPitchModule[tg]].getPolyLFOMultiplier (voiceNumber, lfoDepthTGPitch[tg], lfoDirectionTGPitch[tg]);

Then that multiplier is multiplied with the tone generator’s delta either in the main buffer loop, or I more relaxed also just once per buffer sample loop, as with my buffer size of only 512 the audible chance is good enough for me. Anyways so back in the PluginEditor, whenever I turn on, solo, or turn off an LFO I need to go through all possible LFO target’s, which each has a set of 6 array variables specifying if the LFO the target is using is on or off, what LFO is assigned to “module” (tone generator or effect), current LFO level, current LFO override level, current LFO direction (down, center, up), and LFO override direction. Note the current LFO direction doubles as a LFO status, so if it “0” back my LFO class switch simply returns “1” as multiplier, and there are 4 cases, 1 through 4 being; off (1), down (0-1), center(0 to 2), or up (1 to 2). Similar in my PluginEditor’s “LFO Target” component, I do a lot of matrix array variable setting when I assign, or unassign any active LFO to a target, and/or override the LFO’s level and/or direction.

I use a similar system, setting variables back in PluginEditor, for whether or not SynthVoice main buffer loop needs to process any of the 8 tone generators, as that can be if they are turned on, or even being off if used as a modulation, waveshaping, morphing, or distortion source. Also if SynthVoice needs to get FM modulation modifiers, needs to process effects and where, as in the tone generator’s unison stage and/or once a unison set, that being from 1 to 16 unison voices, has been combined into a tone generator stereo set.

Also anytime I need more than one “If” I always use Switch statements.

So in SynthVoice I have roughly the following flow, where I go through buffer samples in tone generator and unison stages, first only one set of unison voices at a time, which can be a loop of only 1 or up to 16;

Buffer Sample Loop

  • Tone Generator Loop (up to 8), whether on or used as a source

    • Unison Loop (1 to 16)
      • Get samples from wavetable and interpolate linear.
      • Duty Cycle processing - Delta manipulation to simulate pulse width but with any waveform.
  • Active modulation source tone generator loop (up to 8) - Only TG’s used as source

    • Unison Loop (1 to 16) - AM, RM, and get FM/PM modulation modifiers.
  • Active Tone Generator Loop (up to 8), whether on or used as a source

    • Unison Loop (1 to 16). Advance angle by delta which is multiplied with modulation and LFO modifier

At this point I have a unison set of all tone generators, and I continue to do special sample processing, which is waveshaping and effects, only one unison set (1 to 16) a time, and only on tone generators actually on, and only on effect modules on and routed to correct tone generator and TG stage, all predetermined in the PluginEditor. Each processing using a switch to determine which type, and each using its own local unison sample loop. Sure a bit more code, but that way there is no need to Switch during unison samples.

Finally mixing the unison set, voices to a stereo tone generator set and move on to the next buffer sample. And I do this for tone generators, whether they are actually on or just to be used as a source for later on. So loop here is tone generators on or used as source, then a unison set.

Then I am finally done with all work on a unison level, and can move on to any processing on a voice (note) tone generator level, and again only on active tone generators, active effect modules. So here I do;

Active Tone Generator Loop (up to 8), whether on or used as a source

  • Apply gain to try and maintain even level no matter how many unison voices

  • Modulation - AM, RM, PD, and even other forms of PM and FM! Each type/variation using a switch to determine which type, and each using its own local buffer sample loop. Sure a bit more code, but that way there is no need to Switch during buffer samples.

Waveshaping Stage
Tone Generator Loop (up to 8) - Only if actually on!

  • Waveshape type using a switch to determine which type, and each again using its own local buffer sample loop. Several variables used here have been set in PluginEditor, and only updates in SynthVoice once per buffer sample loop.

Effect Stage
Active Tone Generator Loop (up to 8) - Only if actually on!

  • Active Effect Module Loop - Only active modules and if needed, selected via matrix.
    • Using a switch to determine which type, and each again using its own local buffer sample loop.

Wave Morphing Stage

  • Active Tone Generator Loop (up to 8) - Only if actually on!
    • Using a switch to determine which type, and each again using its own local buffer sample loop.

Phew finally done with all buffer samples, with a bit more work before I hand data over the buffer to PluginProcessor.

Active Tone Generator Loop (up to 8) - Only if actually on!

  • Get current ADSR and LFO multipliers
  • If one of the active tone generator’s release stage is done, I reset some variables here including filters, so I don’t have to do it on StartNote.

Active Tone Generator Loop (up to 8) - Only if actually on!

  • Active Filter Module Loop (up to 8) - Only if actually on and needed!

    • Filter processing - Sample loop. And as in my LFO and Effect matrix, I use an intricate array variable system, where all the “If” work is already done in the PluginEditor
  • Hand over each active tone generator stereo set to main buffer, and I do it with a ramp due to me processing ADSR only once per buffer loop.

  • If all active tone generators are done (released), as each tone generator can have a unique ADSR, I call ClearCurrentNote()

  • Otherwise - I update various local variables as needed, if any related chances has been done in PluginEditor such as unison voices, pitch (octave, semi, fine), phase start, end, size chances (yes I can adjust which part of the wavetable to use), sources (modulation, waveshaping, morphing, effects).

Then I am back over in the PluginProcessor, where I do further processing.

Active Tone Generator Loop (up to 8) - Only if actually on!

  • Main level adjustment
  • Tremolo - Get LFO multiplier
  • Pan adjustment
  • DC Filter - Sample loop
  • Effects Module Loop - If on and selected via matrix. Sample loop
  • Filter - If on and selected via matrix - Sample Loop. So obviously here done on a combined tone generator’s notes level, compared to the filtering over in SynthVoice that is done on a voice (note) level with potential ADSR and Key Tracker, as if those are not needed it will save some CPU cycles, off courses dependent on how many notes is played at once.
  • Add tone generator stereo set to main buffer

Global - Only one stereo buffer here!

  • Effect Module Loop - Only those active and needed - Sample loop
    Filter - If selected via matrix - Sample loop. Again no ADSR and Key Tracker, and not on an individual tone generator level, but if you need to filter all tone generators the same and don’t need ADSR or Key Tracker, it will save some CPU cycles, off courses dependent on how tone generator’s is active and how many notes is played at once.

Then I check if my visual buffer is ready to be fed some data, and if so transfer it. Once done I signal my visual component to plot against me!

I hope this helps you or somebody else in some way, and if anyone have any comments or suggestions I am all ears.

2 Likes

Woah dude thanks for taking your time, that’s such a great detailed answer (a really really detailed one, which is amazing so you leave no room for doubts hahah).
I actually like the idea or retreiving the modulations once per block (and ramping), I will try it to see if it sounds good (your video demo sounded great) as I discarded at first that option due to fear of being noticable, but doing it per sample basis as I do now is quite CPU hungry. I know others take a middle term and do it every 16-32 samples sor so, that would mean having an “if” every X samples for each voice.
That’s a clever aproach aswell to update variables in the PluginProcessor once instead of in the startNote for each voice.

I have no more suggestions than saying SIMD will help you a bit more squeezing more perf, but seeing you said you’re more into asm-low level I bet you already did your optimizations. All in all that’s a great job and a nice sounding synth, keep it up!

Well my FM modulation I off course do on each sample. Also to be clear I juggle all sorts of variables in my PluginEditor. Then when I hit a note, on startNote I transfer those to a local set, and also “in-between buffer samples”, that is when SynthVoice is done going through all samples, and right before it is going for another run. I have not yet looked into how SIMD works, as I get so many new synthesize ideas every day. But I optimize slowly over time, and SIMD is on my list to look into before I release it.

1 Like

After several days of optimizing, especially in SynthVoice’s renderNextBlock, but also my output draw plot which off course only take CPU time when the PluginEditor is open and then only when a note(s) are played, I did some non-scientific tests on my Windows 10 64-bit, latest updates Laptop with Intel® Core™ i7-8550U CPU @ 1.80GHz 1.99 GHz, which seems to run at about 4GHz when needed. Installed RAM 8.00 GB (7.89 GB usable).

I’m only running Visual Studio Community 2019, and my plugin in 64-bit release mode. I have Velocity turned off, keep main and all tone generator levels at default, for consistent results. Plugin is running 1920x1080 on my Laptop screen with plugin “real-time” output plot component at its zoomed larger size, and Task Manager maximized on a secondary monitor.

Test results as shown by Task Manager, and note that all remaining background tasks and services takes up about 2% CPU time.

  1. All 8 tone generators on, all with 16 unison voices, random spread, and pan spread. Each with its own ADSR, although all set to the same - About 2.75% CPU time.
  2. As above two notes playing - 4.6% CPU time
  3. As above three notes playing - 5.7%

From here on I turned on modulation from one tone generator to another, waveshaping from one tone generator to another, two LFO’s running, two filters running, filtering each a different tone generator, one LFO is modulating cutoff on filter 1, the other LFO modulating resonance on filter two. So to clarify everything else is as in test 1 through 3!
4-6) So CPU times are at 3% (1 note), 4.8% (2 notes), and 6% (3 notes). The difference in CPU time is actually so small, almost unnoticeable, that for more accurate results I have to test another way.

Now I have not really tested other plugin synths like this, but I consider this speed at 8 oscillators, each with 16 unison voices, pretty decent.

Finally I have still not taken the time to look into SIMD optimizations, but it is high on my to-do list.

1 Like