0
$\begingroup$

I'm trying to understand how to program cascading FM synthesis operations. I can find a good deal of information out there on how to do it on various pieces of hardware, but unfortunately, there isn't enough information to be able to replicate these things in software.

I know that some details may be hardware specific, and that "replicating results" may be complex as there could be specific numbers of bits being used for calculations, or perhaps hardware that alters the results (tubes), but I'm just trying to figure out what is generally considered to be the correct way to do cascading FM synthesis operations.

Let's say that I have the concept of a free spinning oscillator object in my program which stores phase, and has a function to advance phase based on the frequency passed in:

struct SFreeSpinningOscillator {
    SFreeSpinningOscillator () : m_phase(0.0f) { }

    // advances the oscillator and returns the current value of the sine wave
    float AdvanceOscillator (float frequency, float sampleRate)
    {
        float ret = std::sinf(m_phase * c_pi * 2.0f);
        m_phase += frequency / sampleRate;
        return ret;
    }

    // note that phase isn't a true angle, but is just a percent 0..1
    float m_phase;  
};

With the above I know that i can get vibrato easily enough with something like this:

SFreeSpinningOscillator a, b;

....

float modulator = a.AdvanceOscillator(10.0f, sampleRate) * 5.0f;
float carrier = b.AdvanceOscillator(noteFrequency + modulator, sampleRate);
return carrier;

The above will make it so the carrier value is a sine wave at frequency "noteFrequency" which varies it's frequency +/- 5hz on a sine wave, 10 times a second.

When talking about FM synthesis, it's more likely to be something like the below where the modulator's frequency and amplitude is based on the note's frequency:

float modulator = a.AdvanceOscillator(noteFrequency * 0.5, sampleRate) * noteFrequency;
float carrier = b.AdvanceOscillator(noteFrequency + modulator, sampleRate);
return carrier;

This is where I start to get confused though. What if I have two or more modulators? Are their frequencies and amplitudes ALSO based on the basic note frequency? Or are they based on the frequency of the last modulator?

If it's based on the basic note frequency, how do the amplitudes of the modulator waves fit in? If it's based on the modulator frequency, why even calculate the wave forms?

In other words, is the below correct? or is there some other way that it is usually or commonly done?

SFreeSpinningOscillator a, b, c, d;

....

float modulator3 = a.AdvanceOscillator(noteFrequency * 0.5, sampleRate) * noteFrequency;
float modulator2 = b.AdvanceOscillator(noteFrequency * modulator3 * 0.2, sampleRate) * noteFrequency;
float modulator = c.AdvanceOscillator(noteFrequency * modulator2 * 2.8, sampleRate) * noteFrequency;
float carrier = d.AdvanceOscillator(noteFrequency + modulator, sampleRate);
return carrier;

Here are some of the sources I've been reading and have found most helpful:

http://the-all.org/tx81z/fm_overview.html

http://www.soundonsound.com/sos/apr00/articles/synthsecrets.htm

https://www.soundonsound.com/sos/may00/articles/synth.htm

$\endgroup$
2
$\begingroup$

the code on the bottom looks like a triple-nested FM oscillator if i understand how the AdvanceOscillator() method works. likely the SFreeSpinningOscillator class has a method of setting the initial phase of the oscillator. you may get wildly different tone timbres with exactly the same code and different phases for the 4 oscillators.

if you can get your hands on a DX7 alg chart

enter image description here

enter image description here

you can see that most of their algs are sum of two chains. alg 2 has a triple nested oscillator chain where your d is the DX7 osc #3. but i can't tell you what the initial phases of their oscillators are. there are so many parameters that affect the timbre, sometimes in unpredictable ways. so doing software to emulate a DX7 will involve some tweeking of knobs

$\endgroup$
  • $\begingroup$ I've seen charts like that, but I'm stuck understanding the specific details enough to turn it into code. For instance, am I combining the modulations correctly? Also I'm unsure what those "self loops" I indicate. Instead of trying to emulate any specific piece of hardware, I'm just trying to understand a decent "straight and narrow path" vanilla cascading fm synth. I totally get that many things will work, and variations might find interesting things, but trying to understand the "usual case" or a common case at the moment (: $\endgroup$ – Alan Wolfe May 30 '16 at 23:26
  • $\begingroup$ I see the diagram for the operator that you added. Thank you!! I'll see if I can turn that into code (: $\endgroup$ – Alan Wolfe May 30 '16 at 23:37
  • $\begingroup$ i added a pic of what the "Operator" is. usually the "Pitch frequency data" is set to a constant: $$ \frac{noteFrequency}{f_\text{s}} = \frac{440\text{Hz} \cdot 2^{(noteMIDIpitch - 69)/12}}{f_\text{s}} $$ or some integer multiple thereof. but in Alan's code it appears that this is set to 0 and only the "Modulation data" input is active. $\endgroup$ – robert bristow-johnson May 30 '16 at 23:46
  • $\begingroup$ What is $f_s$ in that equation? $\endgroup$ – Alan Wolfe May 30 '16 at 23:48
  • $\begingroup$ sample rate. so that $ \frac{noteFrequency}{f_\text{s}} $ is the phase increment units of OSC table length per output sample. if $$ \frac{noteFrequency}{f_\text{s}} = 0.01 $$ that means that the OSC increments one hundredth of one circular table length in one output sample. $\endgroup$ – robert bristow-johnson May 31 '16 at 4:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.