I’ve always liked the modal resonator mode of Rings, and thought I’d build it from scratch as a learning process. Reading up on this, it uses a bank of 60 tuned resonant filters which is split down for the polyphony setting where notes overlap (so for example the maximum of four notes uses 15 bands per note). I’m making a monophonic version first, pinging a filter bandpass filter with a short burst of noise and using VCV’s polyphony to get 16 different frequencies. I can add more filters later, but this is enough for a rough approximation and makes some interesting glassy, digital tones similar to additive synthesis (because that’s basically what it is, pinging the filter produces a sine wave and you’re adding partials at different frequencies).
The question is, how I do I tune the frequencies of the filter to be the same as the modal resonator model? I think it uses a mix of odd and even harmonics, and changes them as you increase the structure parameter, but I don’t know the relationship between them. I’m guessing it should be possible to get this from the source code, but I’m useless at C. I’ve tried the harmonic series and a few other approaches but it’s not the same.
The functions of the knobs seem to work like this, based on the code, and how the VCV version behaves:
Knob functions
Damping
Controls the resonance of all the partials as a group, functions like controlling the decay time on a VCA’s envelope.
Brightness
Controls how fast the partials higher than the fundamental decay, basically acting like adjusting the decay time of a filter’s cutoff frequency envelope. (It does this by decreasing the resonance of the filters slightly as the partial’s “number” increases)
Structure
Adds inharmonicity to the partials, analogous to stiffness in a string. It scales the partials’ frequencies by a “stretch factor” that increases by the “stiffness” amount with each partial (this “stiffness” amount itself also decreases with each partial). When the knob is at 12 o’ clock, the harmonics are “perfect”, lower values bring them closer together, higher spreads them further apart.
(Worth nothing, the range of the brightness knob gets scaled down a bit in inverse proportion to the structure knob to avoid clipping. The code for stiffness also does some stuff to prevent partials from folding into negative frequencies)
Position
Controls the amplitudes of the partials. Seems to be a comb filter, which is a function I’ve seen in other additive oscillators I’ve messed with (e.g., van Ties’ Ad and docB’s OscA1 here in VCV)
The knob’s direct function is controlling the comb filter’s frequency. At 12 o’ clock, the frequency is set up exactly so that all even frequencies have 0 amplitude. At minimum or maximum, all partials are at full amplitude.
(The knob seems to be symmetrical around 12 o’ clock, so turning it clockwise or clockwise from 12 o’ clock behaves the same)
In terms of implementation, it uses a cosine oscillator to calculate the amplitudes.
The code that calculates the frequencies and resonances of the filters is here, it’s conceptually fairly simple, it’s mostly just maths and a function call to set the filter’s frequency and resonance.
I hope that’s useful for understanding how it works. Unfortunately, I don’t know how to explain the calculations for the frequencies without just resorting to code.
The code for the filters is rather straightforward math:
The parts that may look strange are the ones with “lut” prefixes; “lut” is shorthand for “look up table”; Rings and its descendants achieve good performance by using them.
The whole code for the resonator is rather short; but you will find the filter setup is just going half-way… the rest is a cosine oscillator, here:
The code has some of the C++ template fun; but for what you are trying to achieve, just be aware Rings always uses the COSINE_OSCILLATOR_APPROXIMATE versions.
Edit: oh, right, I forgot: what is “kSampleRate”??
Rings always uses 48,000. Versions for Rack sample rate convert the input and the output, if needed.
Another edit: if you want to examine the source and see what goes where and why, I recommend you grab the Audible Instruments version: Anuli’s is a tad harder to read and more modular; also… well, it has to deal with channel polyphony and does some stuff the Rack version doesn’t (but that does not impact the Modal Resonator… it’s mostly stuff missing from Disastrous Peace, and the strumming light and some other stuff).
A decent IDE that can properly analyze the collected sources and send you where you want to go with a right click and menu selection goes a long way to making the process easier (much to my chagrin, the best one I’ve found so far for doing that is Visual Studio Code… not even Visual Studio finds everything all the time)… you can always, also, develop some grep profiency.
Ah, thanks for the link to the code @phantombeta and thanks @Bloodbat - I nearly tagged you, but I know you’re busy developing the amazing mutants!
That was really helpful, and the code does seem well documented. Any reason it uses a cosine - isn’t that the same as a sine but 180 degrees out of phase? I’m badly rusty on this stuff and never got on with the syntax in C. I think I should probably just start learning it at some point.
I have a book somewhere titled “Learn C++ for Linux in 28 Days”….it’s been in the loft for about 20 years so you can guess how that went previously!
The C part: the only stuff that could throw off a first time reader I can see in that code, where math and flow are concerned, is:
+=, -=, *=, /= those are shorthands to apply the operators behind the equals to an existing variable value, an example:
x = 2;
x += 5; // x is now 7
If we write that on paper, it is equivalent to
x = 2
x = x + 5 // x is now 7
The “for” parts, in their simplest form, do things over and over until a certain value is reached, for example, every decent monster has 6 fingers, a monster (let’s call him Bert), is about to go terrorize the village; but he knows he has to polish his claws so that he looks his best while doing it (he’s trying to impress a lovely she-monster… let’s call her Helga). Bert knows he has to polish each talon at least once, so… he has to do the polishing task 6 times.
In Bert’s brain:
I have to polish 6 claws!
In something we call pseudocode, a non-existent programming language to help understand what is happening with code (this version is really close to Pascal ):
ClawCount = 6
Claw = 0
for Claw to ClawCount do
begin
PolishClaw(Claw)
end
Now in C++:
const int kClawCount = 6;
for (int claw = 0; claw < kClawCount; ++claw) {
polishClaw(claw);
}
All three versions make Bert look his best… if he had just one hand; but, of course Bert has four! Like any cool monster (and Goro, the Shokan prince) does, so he has to “loop the loop” four times; but loops can be nested, so…
const int kHandCount = 4;
const int kClawCount = 6;
for (int hand = 0; hand < kHandCount; ++hand) {
for (int claw = 0; claw < kClawCount; ++claw) {
raiseHand(hand);
polishClaw(claw);
}
}
Yes, there are better ways to do that; but I hope it illustrates the “for” stuff… you may also notice 2 things: Bert does not start from 1 when counting in code: programmers begin counting from 0! That is something really important to always keep in mind; also the C++ version has some weird “++” stuff… what is that??
Another shorthand: this time to add “1” to an existing number.
For example:
y = 11;
++y; // y is now 12.
If we write this on paper, the equivalent is:
y = 11;
y = y + 1; // y is now 12.
The “++” operator has some additional quirks; but, right now, just know it adds “1” to an existing number.
I hope some of this helps (and let’s hope Helga falls in love with Bert)
Thanks @Bloodbat that helps a lot. And best of luck to Bert with pulling Helga
I’ve learnt some really basic Python, so I’m vaguely familiar with variables and loops, just never got on with C. I think I need to just bite the bullet and learn it at some point (now would be good!).
Embarrassingly, I do work in IT but I don’t code (I do infrastructure provisioning). Things like starting from zero when counting does make sense, you see that in the identifiers for network adapters in Linux (In Red Hat at least, if you have a four-port onboard network card, the NIC identifiers are eno0 to eno3 for 1 to 4 for example. Windows is the same with eth0, eth1 etc). So this stuff isn’t completely alien, it’s just joining the dots and getting something usable out of it.
That’s a great book you linked to by the way (Pd is something else I’ve started learning a bit of). Glad it’s online too, the hardback copy is nearly a hundred quid on Amazon!