oscbank in genexpr

jvkr's icon

This came up in our course today, thought it would be good to share it here.

Max Patch
Copy patch and select New From Clipboard in Max.

bertrandfraysse's icon

this was very handy to synthesise real world spanish guitar sounds.
ok, I couldn't help but make a stupid joke.
this is great !
it would be nice to make functions to fill those tables.
could be nice with FM synthesis too !
thanks for sharing.

jvkr's icon

By the way, the reason I shared it, was mostly that I thought it was quite impressive how little code is needed to do this. Instead of fm, a version with pm. And a separation over two channels.

Max Patch
Copy patch and select New From Clipboard in Max.

Graham Wakefield's icon

Neat -- PM oscbank is an interesting thing! Thanks for sharing!

BTW this example also shows how much cheaper a lot of [cycle @index phase] are than [sin] or even [fastsin]. (Try swapping them out to see the CPU usage go up!) For the curious: The cycle() operator uses a large wavetable (with linear interpolation by default), fastsin() uses a Taylor series approximation, and sin() is just the C math library's sin().

Just a couple of tips:

The PM amplitude could be relabeled "modulation index" for the usual phase modulation terminology.

The phase wrapping assumes that accumval is positive only (only positive frequencies). Though, as it happens, since the result goes into a sine shaper (the cycle), a large negative phase wouldn't necessarily be a problem unless it ran for a very long time! Changing the phase wrapping to wrap(newphase, 0, 1) doesn't seem to have any impact on the CPU measurement, and that would handle negative frequencies properly.

Maybe `channel` shouldn't be a History. If you had an odd number of active oscillators, you'd end up alternating each oscillator between L and R on each sample, which makes for a pretty drastic filter/RM effect. Just declare `channel = 0;` before the for loop, and remove `History channel;`.

Lines like this:

currentphase1 = peek(pdata, i, 0);
currentphase2 = peek(pdata, i, 1);

might be slightly faster like this:

currentphase1, currentphase2 = peek(pdata, i, channels=2);

Because the math for dealing with bounds checking of `i` only happens once. Though it's possible the compiler might be smart enough to figure that out anyway! Actually l can't see much change in the CPU performance... but maybe since you were interested in 'how little code it needs' it might be helpful?

jvkr's icon

@Graham Thank you for the input, that is really valuable, especially reading values from multiple channels with peek. Yes, I first tried the wrapping, and didn't see much of a difference cpu wise compared with the if statement as you mention. Maybe wrap is smart enough to do a check before applying the math. I like the if statement because it allows for additional controls, like randomizing the increment values. As soon as I've a bit of time, I'll work further on it.