Oscillators, Buffers, Aliasing and CPU
Have I just had the stark realisation that Max isn't great for synth design?
If I use multiples of the stock antialiased for oscillators and phasors for LFOs the CPU is outrageous.
if I use buffer with single cycle waveforms being read from a cycle, the aliasing is outrageous.
I have opened up a lot of synths in Max and it seems every one of them suffers from the same problem, including those for sale on the ableton webpage: If they use antialiased the polyphony is limited to single figures for efficiency, but nearly all use buffers which alias.
Even the gen~ patches alias, or have very few oscillators.
What's the answer?
how many are "multiple" phasors? 3 per voice? or 100?
a bandlimited harmonic waveform should not cause too much aliasing in cycle~. for high freqeuncy stuff, maybe upsample your waveforms. of course that needs even more CPU, but that is what most good hardware synths do and why they seem to sound better than a sampler.
Hi there thanks for the reply, per voice I have:
8 osc ( the 4 standard waveforms into a selector x 2) and two phasors for LFO with the usual waveform choice, this shows 20 % CPU before a key has been pressed.
I'll try your upsampling suggestion, thanks.
Max is amazing for synth design.
It tends to hit the cpu a little harder than if you work in a lower level language, like c++, I think for pretty obvious reasons. Max's benefits vastly outweigh the drawbacks. There are certain esoteric procedures to ensure max is runs efficiently too.
I must say, I was generally not super excited by the insides of the m4L synths i bought as well. So I stopped buying them. They are aimed at an audience who wants out-of-the-box toys to play with in live, not devs building their own stuff.
Oscillators in max will alias as much or as little as they are designed to. You have to keep in mind that making a crystal-clear digital oscillator is a highly non-trivial activity, even if you love math and are working with the well trodden algos.
Any one-size fits all anti-aliasing solution is going to suffer from high cpu usage, poor alias suppression or some combination of the two.
I'm not sure why that simple patch you described would decimate your CPU unless you still need to upgrade from that cutting edge pentium 2 workstation you built back in '99. Maybe share the patch to see if something else is fishy?
when i am not wrong you are using send and receive inside your poly patcher without making them per-voice.
so each of your sends goes to all of the receives, causing millions of useless signal connections.
a plain connection from A to B already eats up more CPU than what most people think.
overall you are very well organised, but there are some other small design imperfections.
for example at [in 9] you are taking a global control message per voice, then interpolate each of them as signal. you could as well pull the transform to a signal incl the line~ out of the poly patcher, and then send it into the poly as signal.
and while this isnt a general rule, at times it can make sense to have a sub-poly in a poly.
your 4 oscillator objects are never used together, but they all always run.
if you would make a simple poly~ patcher for each of these objects (in~, rect~, out~) and switch between them using thispoly~ "mute 0" "mute 1" you could run them only when needed and even save the selector object in addition.
one could also make subcircuits for the FM and filter sections which are turning off when settings are at virtually zero, but IMO this only make sense in practive when "not in use" is the most frequently used state.
same with active voices - in plug-ins automatic voicing make only sense when you bind it to a user-controlled global (i.e. not per-preset) "number of voices" parameter.
Very nice work. I particularly like that you included a custom tuning mechanism.
I think roman nailed pretty much all of your cpu woes.
At any given point there are at least 6 audio rate oscillators chugging away for no reason(and sometimes more if you aren't doing fm). When you add polyphony to the mix you get alot of wasted resources. If the user can turn it on and off, then max should too. Having a [poly~] for each sub process like roman described creates alot of dependencies to manage, but it is the most bullet-proof way keep a tight lid on your DSP.
If you open your .amxd in vanilla max (ie not launched from Live), you can click on the 'show containing project' button on the bottom left of the window to pull up the the list of dependencies where you can easily open/close/modify them and relaod everything to force an update if needed.
Better yet, you can just save the whole thing as a project and 'export to m4l device' when needed to get even better manual control over dependencies and search paths, but there is a bit of a learning curve with this workflow so be warned. It is up to you if you want to work this way, just a suggestion.
You can also do the same to the LFOs (put each osc in a [poly~]), and you can also downsample them to save on CPU. Just be aware that this lowers the nyquist for the lfo. Try by a factor of 4x 8x or 16x and see what yeilds good fidelity at the speeds you want it to run. Unless you want super-smooth control signals, which are cool too, in which case don't downsample.
Now, about that routing system...those [send]s and [receive]s are using a 'global namespace' and they will be sending to and receiving from other instances of your synth (like if you have two or more in the same Live Set). Put --- (three dashes) before the name to keep it local to a device. like this: [s ---localSend]. Max puts a random prefix inplace of the dashes for each instance of the device [s 089localSend]. You can see it in action if you make [s ---localSend] visible in Live.
You can keep the sends out of the [poly~] and use them to broadcast to all the voices inside of it, having them in the [poly~] creates alot of extra messages flying around for no reason. Better yet, use [forward] to specify different receives and ditch the [gate 900000]. See the attached patch for an idea on using the names from [live.menu] to forward messages around.
Now get back to work!! haha just kidding, if you need further elaboration just ask...
A big thank-you to you both for such comprehensive advice ... it's much appreciated, quite a lesson with lots to learn and practice which is great.
It's a privilege to have such great information shared, and now back to work it is then! I'm looking forward to putting your advice into action and many thanks also for the tutorial patch.
Cheers guys
Hey guys, been interesting to read this thread, knocked up a quick patch with the poly > poly > poly tree to see if it is any use or can be improved upon... Top level patch is PCT.maxpat. worth noting that the down 8 on the sah has some spikes on the scope at the turn points on the waveform...
Ah, yes, I usually decide whether to downsamp and LFO on a case by case basis, after some tinkering. This is a good example of why. I think we are witnessing some 'slide' from [poly~]s interpolation filters. You could just downsample the [noise] and [phasor~], leaving the [sah~] outside. You would have to include something to prevent [sah~] from messing up the other lfos with dc bias when it is inactive. Testing would needed to see if it was even worth the trouble vs. the amount of CPU you'd be saving, but I bet it you had *a ton* of [sah~] lfos it would be.