Additive synthesis in Gen~
Hello !
I have been using Csound for two years and I am trying to learn Max (and Gen~) because Csound is not fun to use. I am then very very new to Max (I started learning one hour ago).
I am trying to make additive synthesis using Gen~ with a loop, but it doesn't seem to be working, I don't really know genexpr syntax since I am used to using Csound.
Could anyone tell me what's wrong ? Thank you :)
Are you aware you can use csound inside Max to achieve the same thing as Gen? As in, you can set the csound object to have whatever ksmps you want to do single sample rendering with feedback.
I ported the csound6~ object to Max and it's available here, for windows, mac, and apple silicon mac.
https://github.com/iainctduncan/csound_max
Demo video here: https://www.youtube.com/watch?v=ZMWpfdoe2fw&ab_channel=MusicwithLisp
I like gen a lot, and use both gen and csound for different things, but I personally find csound much nicer for additive synthesis in particular. Using the csound~ object in max means you can use only the good bits of csound and ignore all the ugly parts. :-)
Hello :)
Yes I already tried it but I liked the idea of using Max for Live which is very close to Max, and Max for Live feels more finished than Cabbage. Using Csound inside Max inside Live looks pretty complicated.....
Thank you for your advice, I will give it one more try ! I am really into "partikkel like" granulation then Csound seems to be a better choice indeed. Thanks a lot :)
Hi Jacques, I use the csound6~ object within Max for live without issue, though I am planning some improvements to it to make it easier to use in Max 4 Live. (like allowing one to change the source file without having to edit the amxd device). If you have suggestions for what would help, do let me know. And I'm happy to answer any questions.
If you have access to it through a university library or anything, the Victor Lazzarini books on csound and computer music instrument design are very good for this.
Csound inside Max for Live sounds like a good option. I'll let you know if anything comes to my mind when I'm used to it :) Thank you for your work ! Csound deserves it for sure
Yes these books helped me a lot, I'll dive into it again
sure, feel free to leave feedback on the github discussion board for it
Hey there,
To answer the original question -- inside your while loop you call phasor(). The phasor has an internal state (the phase), which is updated each time you call it. So, if you loop N times, the phasor() is called N times and the phase updates N times. Maybe you were expecting this to create N phasors, but that's not how it works with genexpr. Putting a stateful operator (like a counter, accum, phasor, cycle, delta, change, latch, etc.) inside a loop is a bit like oversampling it -- you end up calling it N times. So in the end you are summing up N phases of a single phasor.
If you actually want N phasors, you need to write `phasor()` N times in the code. It's just like patching -- you would want N subpatchers each with a phasor inside.
As a more general tip: I recommend approaching gen~ first by patching, not codebox. Patching is actually closer to the semantics of the signal processing in many cases, it lays out flows better (especially feedback, which is always implicit in code), and *it is just as efficient*.
Graham
---
Background explanation if you are curious:
Basically there's two ways of interpreting what the following code means:
for (i=0; i<N; i+=1) {
some_stateful_op();
}
Either this means we want to call one some_stateful_op N times, or it means we want to have N some_stateful_ops and call each one in turn. Both are quite reasonable assumptions to make. For an oscillator bank you probably want N calls. For a series operation (such as a filter cascade or an oversampled operation) you probably want to call one thing N times.
Genexpr uses the "call one thing N times" semantics. The reason has to do with performance & stability. If we used "call N things" we would have to allocate memory for each one, but for a general for or while loop we don't necessarily know how many we might need, so we don't know how much memory to allocate until the loops happens. Depending on unkonwn things this could even be changing thousands of time per second. However for audio processing in general we should never be allocating memory dynamically, as it can cause audio dropouts. This is why genexpr does not use the "call N things" semantics.
... and for the specific case of creating an additive synth / oscillator bank, you might find it better to just store state in a Data or Buffer object. You can then use a for loop over the Data frames to get the amplitude & frequency, and to get and update the current phase.
Something like this:
// first channel is unit frequency, second channel is amplitude
// using a Buffer so that the amps & freqs can be set from the Max patch
Buffer oscbank;
// this stores the current phase of each oscillator
// using Data to keep this internal to the patch
Data phases(256);
// how many partials to synthesize:
Param numpartials(256, min=0, max=256);
sum = 0;
// for each partial
for (i=0; i<numpartials; i+=1) {
// get the amplitude and normalized frequency for this partial
amp, normfreq = peek(oscbank, i, channels=2);
// get the phase of this partial
phase = peek(phases, i);
// update the phase, keeping it in the 0..1 range
phase = wrap(phase + normfreq, 0, 1);
// apply sine curve and amplitude, mix to output
sum += amp * cycle(phase, index="phase");
// update the phase into the Data for this partial:
poke(phases, phase, i);
}
out1 = sum;
In the Max patch, create a [buffer~ oscbank @samps 256 2] and create two [waveform~ @buffername oscbank] objects, set the second one to channel 2, set both to "draw" mode. Draw some curves in and enjoy!
Here's the above with a couple of tweaks to play more nicely with the ears:
Hi ! Alright I get it now :) Thanks a lot !
I am reading Generating Sound and Organizing Time while watching the video you made with Gregory Taylor, very helpful :)
The Wakefield and Tayorl is a great book - I really like the FM and filtering chapters. (Well done Graham!) The 3rd Volume of Cipriani and Giri also goes into gen in depth and is excellent. I have found the process of taking things back and forth between gen and csound (and C++ for that matter) to be a really good way of deeply understanding the algorithms, FWIW. It's also actually quite easy to get them to play well together in one Max patch to make hybrids.
The "call one thing N times" semantics is also something that happens in Csound and is explained well in the 2016 Lazzarini Csound book.
For me, the advantage of csound is that it's very easy to put in multiples of things with precise math (ie a bunch of envelopes, oscillators, etc). It's also *much* easier to add C code to csound than to Max. The big advantage of Gen is the way the patching can closely resemble DSP diagrams (and of course the integration with Max!) I like the bit in the wakefield book about turning filter diagrams sideways to implement them in gen, brilliant!
What I would really like to see (in case Graham reads this!) is a great, hosted repository for open source gen code. If could get a browsable library of all the things people are implementing that would be fantastic! One of the weaknesses of Max compared to other options for the last 10 years or so has been the fact that the stock oscillators and filters aren't great sounding compared to more commercial offerings and a library of top drawer gen implementations could really address that.
I spent a lot of time looking for comparaisons between Max and Csound for a while, you're being very helpful :)
Yes I feel like I have to build everything by myself with Max Gen~ while Csound already has a lot of opcodes that sound great
You're welcome. If you are interested in the kind of synthesis you can do really well with Gen and Csound, (as in, mathematical algorithms with feedback, digital filters, and non-linear elements) another excellent resource is Perry Cook's "Real Sound Synthesis" book. https://www.cs.princeton.edu/~prc/AKPetersBook.htm