Fun with FM synthesis

MarkT

Well-known member
I've played with some simple FM synthesis, and reworked the PlaySynthMusic example to use it,
as well as trying to declutter all the toplevel boilerplate so it can be parameterized by a number of voices.

The way I've addressed this is to bundle all the apparatus for a voice into a class, 3 oscillators and an
envelope, like this:
Code:
// Implement an FM voice, 3 stages in the algorithm in sequence, plus the envelope
class FMVoice
{
public:
  FMVoice () 
  {
    new AudioConnection (modulator, 0, middle, 0) ; // on heap so destructors not called when this 
    new AudioConnection (middle, 0, finalosc, 0) ; // constructor exits!
    new AudioConnection (finalosc, 0, env, 0) ;
  }

  AudioEffectEnvelope & output (void)  // need this to connect up and to set envelope parameters
  {
    return env ;
  }

 
  // depth1 is modulation depth applied to middle,
  // depth2 is modulation depth applied to final
  // f2 and f3 are multipliers for fundamental used in middle and modulator respectively, small integers
  void noteOn (float amp, float freq, float depth1, float depth2, int f2, int f3)
  {
    AudioNoInterrupts() ;
    modulator.frequency (f3 * freq) ;  // modulator is lowly sine wave generator, no begin method...
    modulator.amplitude (depth1) ;
    middle.phaseModulation (180) ; // mid range so no discontinuities
    middle.begin (depth2, f2 * freq, WAVEFORM_SINE) ;
    finalosc.phaseModulation (180) ; // mid range so no discontinuities
    finalosc.begin (amp, freq, WAVEFORM_SINE) ;
    env.noteOn () ;
    AudioInterrupts () ;
  }
  
  void noteOff (void)
  {
    env.noteOff() ;
  }

  bool isActive (void)
  {
    return env.isActive() ;
  }

  void silence ()  // save cycles when the envelope has finished releasing
  {
    finalosc.amplitude (0) ;
    middle.amplitude (0) ;
    modulator.amplitude (0) ;
  }
  
protected:
  AudioSynthWaveformSine modulator ;
  AudioSynthWaveformModulated middle ;
  AudioSynthWaveformModulated finalosc ;
  AudioEffectEnvelope env ;
};

The whole reworked example is attached as a zip - I'd recommend finding something less annoying as the example
data than the William Tell overture (I found a midi file of Bach's Tocatta / Fugue in D-minor, which can be listened too
repeatedly without going crazy! - and put it through miditones, but it was copyright so didn't include it here alas).

The FM parameters I found after a little tinkering sound fairly organ-like and are pretty funky on the 'scope!

fm_synth.png

Comments welcome - the topic of how to structure audio components hierarchically is relevant, found a few comments
about this in a search on teensy FM synthesis.

BTW this is on Teensy 4.0 with 150MHz clock, about 22% audio CPU for 16 voices which is pretty reasonable...

It was quite fun to play with FM synthesis for the first time :)

[ I was partly inspired by reading this https://cs.gmu.edu/~sean/book/synthesis/Synthesis.pdf ]
 

Attachments

  • fmsynth.zip
    19.2 KB · Views: 124
This time I've made a library AudioStream object to implement 4-operator FM synthesis with 8 algorithms, like the DX9.

The 4 operators have individual amplitude settings (which for modulators are the modulation depth), and the
single input stream is used for overall modulation depth control which scales all the modulation depths. Operator
4 has configurable feedback.

The synth example code uses 16-fold polyphony and makes a Teensy 4.0 work pretty hard, note, I'm not sure a
slower board will handle this.

Code:
class AudioSynthFM : public AudioStream
{
public:
  AudioSynthFM(void) : AudioStream(1, inputQueueArray) { ... }

  void algorithm (int alg_num) ;
  void frequencies (float freq1, float freq2, float freq3, float freq4) ;
  void amplitudes (float amp1, float amp2, float amp3, float amp4, float feedb) ;
  void enable (bool on) ;
  ...
};
algorithm() takes an integer in range 0 to 7 to set the FM algorithm, standard DX9 set.

frequencies() takes Hz values for the four operators.

amplitudes() takes 5 float values for the output amplitudes of the oscillators and operator 4's feedback amount.

enable(false) is a convenience for shutting down the unit totally to save cycles, to be called when an output
envelope drops from active to idle, and on noteOn.

The example routes DC -> expression envelope -> AudioSynthFM -> note envelope -> mixers,
and triggers both envelopes on noteOn.

Code:
zip fm2.zip FM2PlaySynth/* libraries/FM/*
  adding: FM2PlaySynth/FM2PlaySynth.ino (deflated 61%)
  adding: FM2PlaySynth/FMPlaySynth.h (deflated 47%)
  adding: FM2PlaySynth/william_tell_overture.c (deflated 83%)
  adding: libraries/FM/FM.cpp (deflated 62%)
  adding: libraries/FM/FM.h (deflated 49%)
 

Attachments

  • fm2.zip
    23.1 KB · Views: 119
This looks very cool. Thanks for posting.

I've been looking at the Korg Opsix - seems they've really thought out the interface well. It's quite difficult to make FM synthesis immediate in the way subtractive synthesis is. Makes me interested to whip something similar up with the Teensy.

Ken
 
https://codeberg.org/dcoredump/MicroDexed

This is a 6-OP-FM synth for the Teensy, compatible with a famous japanese synth from the 80's ;-)

I have just returned to this and your code looks very promising, so I had a go with the Audio Library object version.

I am using a Teensy 4.1 and the problems I describe occur with both the audio shield and I2S into a PCM5102A. I have also built with the CPU speed changing from 600MHz to 816MHz, and with the Optimise level at Fastest and at Debug.

The first problem is with the SimplePlay example. It mostly worked, but the first note of one of the loops does not sound.
Listen here: SimplePlay original
If I add a small delay before the missing note it does sound reliably, but I'd like to understand what's going on and find a proper way to avoid it.

More worrisome though, I made a slightly different demo and it is sounds very crackly and noisy.
The code is below, and you can listen here: SimplePlay alternative.

***Edit: Found the stupid mistake in my code! I was retriggering the note repeatedly. Fixed and it plays as clean as a whistle. :)

<deleted broken code>

Thanks,

Ken
 
Last edited:
***Edit: Found the stupid mistake in my code! I was retriggering the note repeatedly. Fixed and it plays as clean as a whistle. :)

Great that you were able to fix the error yourself!

BTW, the Dexed audio object is still used in another project: https://github.com/probonopd/MiniDexed
This is a bare-metal Dexed for the Raspi (based on the circle SDK, so there is no OS underneath). If you have a Raspi (preferably a Raspi >2), you can easily turn your Raspi into a 6-OP FM synth by creating an SD card and boot - only a USB MIDI keyboard is needed.

The project is currently still in the startup phase, but already very promising. I'm curious what will come out of it in the next weeks/months.

Regards, Holger
 
I put together a Eurorack 4-operator FM voice using parts of the Dexed audio library object.

demo.jpg

Because I wasn't trying to recreate a DX7, I dropped various things (like the LFO, pitch envelope, detune, effects), changed others (coarse frequency down to 1/128 like the OpSix, ADSR envelopes instead of the 4-level/4-rate DX7 ones), and it's CV driven monophonic rather than MIDI.

Most of the code was pretty useful, though the highly interactive nature of my device meant I had to make changes to minimise digital artifacts and have the envelopes behave properly.

Here's a short overview and demo.

Thanks for the inspiration! :)
 
Back
Top