700 lines · 19.3 KB · Text
RawDownload
#+TITLE: FM Pair Oscillator* IntroductionThis document outlines an implementation of a classicFM oscillator with frequency, C:M ratio, and modulationindex control. There is quite a lot to be said about FM synthesis, but aclassic FM oscillator implementation is poetic tribute tothe technique. Requiring only two oscillators (usually sinewaves), an FM operator pair is capable of generating veryrich and dynamic spectrums. For how little computation isrequired, these suckers can pack a punch! The FM pair is the Bechamel sauce of the FM world. Creatingone from scratch provides great sonic intution into themostly unintuitive world of FM synthesis programming. It canbe the building block for grokking more complex modulationconfigurations, such as those found on the DX7.* Some words on FM synthesis(I can't help but ramble a little bit about the topic ofFM synthesis. Feel free to scroll/skip/page ahead.) FM synthesis, or Frequently Modulation synthesis, was firstimplemented by John Chowning while doing research at theStanford Artificial Intelligence Laboratory (SAIL, whosespritual successor would become CCRMA, or the Center forComputer Research in Music and Acoustics). As the legendgoes, Chowning stumbled upon the technique by accident.Experimenting with different vibrato speeds, he found thatwhen vibrato got fast enough, it started changing timbreand spectrum. The humble beginings of FM synthesis started out with an FMpair configuration similar to this one. But can you imaginethe surprise he got when he first heard that new FM sound? To paint a picture: At the time, computers were about thesize of fridges. They were expensive devices owned by theUniversity, and time spent using the computer was precious.Chowning no doubt had to fight to get his time slot. Beinga composer amongst scientists and engineers, he probablycould only work in late hours. Typing away at a mainframe 3am, Chowning would have to hadto wait at least a day for the computer to run his FMsynthesis program. Assuming it successfully ran without anyerrors, Chowning would then have to ship the output of thatprogram off somewhere to get it converted to magneticreel-to-reel tape. Back then,digital-to-analog converers (DACs) were things that you hadto get in a car and drive to. Now-a-days, audio DACs arethings found on pretty much every electronic device you own. So, if you're keeping track, that's at least a week fromcode to sound. That's an enormous amount of round-triplatency! To go that long, and to finally hear these newsounds must have been such a rewarding experience. Chowing's original FM synthesis algorithm would eventuallybecome one of Stanford University's highest grossing patentsof all time. The royalties of thispatent helped to sustain the CCRMA facilities. Famously, the patent was sold to Yamaha, and that gave usthings like the DX7 synthesizer (and all the cheesy musicthat came with it). Due to the efficiency ofthe algorithm, it also found itself in a whole generation ofsound cards at the time, used in video game consoles andarcade machines. The most famous of these chips was the[[https://en.wikipedia.org/wiki/Yamaha_YM2151][YM2151]], also known as the OPM chip.* Overview of Algorithm and ImplementationAn FM Pair uses two oscillators, sometimes referred to as=operators=. One of these operators synthesizes audble sound.This known as the =carrier=, and can be remembered becauseit *carries* the sound signal to the speaker. The otheroperator *modulates* the frequency of the carrier, and itis known as the =modulator=. If you set the modulator to any frequency that is audio rate(give or take 20hz or higher), you will technically get somekind of FM synthesis. However, depending on the frequency ofthe carrier, the spectrum can range from being very pitchedwith harmonically related overtones, to something quiteclangorous and not at all something you could easily play"Hot Cross Buns" on. So, to make an FM pair easier to use,three unit-less parameters are used to control theFM characteristics of the sound that work evenly acrossthe spectrum: a carrier-to-modulator frequency ratio knownas the C:M ratio, and something known as the modulationindex, which can kind of be thought of as a thing thatcontrols the amount of modulation. The C and M parameters are values that primarily multiplythe frequency value of the carrier and modulator operators,respectively. The modulation index can be defined as having the followingrelationship (taken from the Computer Music Tutorial'schapter on Modulation synthesis, page 229): $$ I = D/M $$ Where $I$ is the modulation index, $M$ is the modulationfrequency (which would be the oscillator frequency multipliedby the modulator ratio), and $D$ is the amount of modulation=depth=. The equation can be reworked in terms of $D$: $$ D = MI $$ This depth value can be thought of as the "amplitude" amountthe modulator operator (in Hz). This will produce a signalthat modulates the carrier frequency up the depth amount,and then down that same amount. In this implemetation, two table-lookup oscillators will beused for the operators, using an algorithm similar to the@!(ref "osc" "table lookup oscillator")!@, which employ afixed-point phasor with linear interpolation. Details forthe gory details of these oscillators are beyond the scopeof this article, but are described in the implementationreferenced above.* Tangled Files=fmpair.c= contains the ANSI C code required for theimplementation. #+NAME: fmpair.c#+BEGIN_SRC c :tangle fmpair.c#include <math.h>#define SK_FMPAIR_PRIV#include "fmpair.h"<<constants>><<funcs>>#+END_SRC =fmpair.h= is the corresponding header file for the C file.It contains all the forward declarations. If=SK_FMPAIR_PRIV= is defined, it exposes the structs. #+NAME: fmpair.h#+BEGIN_SRC c :tangle fmpair.h#ifndef SK_FMPAIR_H#define SK_FMPAIR_H#ifndef SKFLT#define SKFLT float#endif<<typedefs>><<funcdefs>>#ifdef SK_FMPAIR_PRIV<<structs>>#endif#endif#+END_SRC* StructsThe main FM oscillator algorithm is encapsulated in a structcalled =sk_fmpair=.#+NAME: typedefs#+BEGIN_SRC ctypedef struct sk_fmpair sk_fmpair;#+END_SRC #+NAME: structs#+BEGIN_SRC cstruct sk_fmpair {    <<sk_fmpair>>};#+END_SRC* Table-lookup oscillator data and constants** ConstantsThe constant =SK_FMPAIR_MAXLEN= is the maximum length alookup table can be. The constant =SK_FMPAIR_PHASEMASK= is a bitmask used bythe fixed point phasor.#+NAME: constants#+BEGIN_SRC c#define SK_FMPAIR_MAXLEN 0x1000000L#define SK_FMPAIR_PHASEMASK 0x0FFFFFFL#+END_SRC** Struct DataThe FM oscillator implements two independent table-[ookuposcillators. Each one needs their own set of wavetables,incrementors, sizes, and cached phase positions. The convention used will this: any variables used for thecarrier oscillator will begin with 'c': #+NAME: sk_fmpair#+BEGIN_SRC cSKFLT *ctab;int csz;int clphs;#+END_SRC #+NAME: init#+BEGIN_SRC cfmp->ctab = ctab;fmp->csz = msz;#+END_SRC Any variables used for the modulator oscillator will beginwith 'm': #+NAME: sk_fmpair#+BEGIN_SRC cSKFLT *mtab;int msz;int mlphs;#+END_SRC #+NAME: init#+BEGIN_SRC cfmp->mtab = mtab;fmp->msz = msz;#+END_SRC In order to be used by the fixed point phasor,the initial phases need to be rescaled by =SK_FMPAIR_MAXLEN=and then converted to an integer value. These arestored in each respective variable keeping trackof phase. #+NAME: init#+BEGIN_SRC cfmp->clphs = floor(ciphs * SK_FMPAIR_MAXLEN);fmp->mlphs = floor(miphs * SK_FMPAIR_MAXLEN);#+END_SRC A number of constants are used with these tables: =nlb=,=inlb=, =mask=, and =maxlens=. These are beyond the scopeof this document, but are touched upon in @!(ref "osc")!@.Since these are all based on table size, two sets ofconstants are derived for the carrier and modulator. #+NAME: sk_fmpair#+BEGIN_SRC c/* carrier constants */int cnlb;SKFLT cinlb;unsigned long cmask; /* modulator constants */int mnlb;SKFLT minlb;unsigned long mmask; SKFLT maxlens;#+END_SRC #+NAME: init#+BEGIN_SRC c{    int tmp;     /* carrier */    tmp = SK_FMPAIR_MAXLEN / csz;    fmp->cnlb = 0;    while (tmp >>= 1) fmp->cnlb++;     /* modulator */    tmp = SK_FMPAIR_MAXLEN / msz;    fmp->mnlb = 0;    while (tmp >>= 1) fmp->mnlb++;} /* phase mask for dividing lower/upper bits */ fmp->cmask = (1<<fmp->cnlb) - 1;fmp->mmask = (1<<fmp->mnlb) - 1; /* constant used to convert to floating point */ fmp->cinlb = 1.0 / (1<<fmp->cnlb);fmp->minlb = 1.0 / (1<<fmp->mnlb); /* max table length in seconds *//* used to convert cycles-per-second units to cycles */ fmp->maxlens = 1.0 * SK_FMPAIR_MAXLEN / sr;#+END_SRC* InitializationThe FM oscillator is initialize with =sk_fmpair_init=.It needs the sampling rate =sr=, and two sets ofwavetables (=cwt= and =mwt=),their sizes (=csz=) and (=msz=), andinitial phases (=ciphs= and =miphs=) for the carrier (c)and modulator (m) oscillators. In a classic FM pair, these would be identical,and the wavetable would contain a sampled period of asine wave. #+NAME: funcdefs#+BEGIN_SRC cvoid sk_fmpair_init(sk_fmpair *fmp, int sr,                    SKFLT *ctab, int csz, SKFLT ciphs,                    SKFLT *mtab, int msz, SKFLT miphs);#+END_SRC #+NAME: funcs#+BEGIN_SRC cvoid sk_fmpair_init(sk_fmpair *fmp, int sr,                    SKFLT *ctab, int csz, SKFLT ciphs,                    SKFLT *mtab, int msz, SKFLT miphs){    <<init>>}#+END_SRC* Parameter Control** FrequencySet with =sk_fmpair_freq=. #+NAME: funcdefs#+BEGIN_SRC cvoid sk_fmpair_freq(sk_fmpair *fmp, SKFLT freq);#+END_SRC #+NAME: funcs#+BEGIN_SRC cvoid sk_fmpair_freq(sk_fmpair *fmp, SKFLT freq){    fmp->freq = freq;}#+END_SRC #+NAME: sk_fmpair#+BEGIN_SRC cSKFLT freq;#+END_SRC A sensible default of A440. #+NAME: init#+BEGIN_SRC csk_fmpair_freq(fmp, 440);#+END_SRC** Carrier/Modulator RatiosSet with =sk_fmpair_modulator= and =sk_fmpair_carrier=. #+NAME: funcdefs#+BEGIN_SRC cvoid sk_fmpair_modulator(sk_fmpair *fmp, SKFLT mod);void sk_fmpair_carrier(sk_fmpair *fmp, SKFLT car);#+END_SRC #+NAME: funcs#+BEGIN_SRC cvoid sk_fmpair_modulator(sk_fmpair *fmp, SKFLT mod){    fmp->mod = mod;} void sk_fmpair_carrier(sk_fmpair *fmp, SKFLT car){    fmp->car = car;}#+END_SRC #+NAME: sk_fmpair#+BEGIN_SRC cSKFLT car;SKFLT mod;#+END_SRC A 1:1 ratio is a good strong and sensible default. #+NAME: init#+BEGIN_SRC csk_fmpair_carrier(fmp, 1);sk_fmpair_modulator(fmp, 1);#+END_SRC** Modulation IndexSet with =sk_fmpair_modindex=. #+NAME: funcdefs#+BEGIN_SRC cvoid sk_fmpair_modindex(sk_fmpair *fmp, SKFLT index);#+END_SRC #+NAME: funcs#+BEGIN_SRC cvoid sk_fmpair_modindex(sk_fmpair *fmp, SKFLT index){    fmp->index = index;}#+END_SRC #+NAME: sk_fmpair#+BEGIN_SRC cSKFLT index;#+END_SRC A modulation index of 1 on with 1:1 C-to-M ratio producesa very warm and versatile initial sound. #+NAME: init#+BEGIN_SRC csk_fmpair_modindex(fmp, 1);#+END_SRC* Computing a SampleA single sample of audio is computed with =sk_fmpair_tick=. #+NAME: funcdefs#+BEGIN_SRC cSKFLT sk_fmpair_tick(sk_fmpair *fmp);#+END_SRC #+NAME: funcs#+BEGIN_SRC cSKFLT sk_fmpair_tick(sk_fmpair *fmp){    SKFLT out;    SKFLT cfreq, mfreq;    SKFLT modout;    int ipos;    SKFLT frac;    SKFLT x[2];    out = 0;    <<calculate_frequencies>>    <<table_lookup_for_modulator>>    <<scale_modulator_output>>    <<modulate_carrier_frequency>>    <<table_lookup_for_carrier>>    <<update_phase_positions>>    return out;}#+END_SRC The carrier and modulator frequencies are calculated bymultiplying their respective ratio values withthe oscillator frequency. #+NAME: calculate_frequencies#+BEGIN_SRC ccfreq = fmp->freq * fmp->car;mfreq = fmp->freq * fmp->mod;#+END_SRC The modulator oscillator does its table-lookup andlinear interpolation with some bitwise magic. The details ofthis can be found in @!(ref "osc")!@. #+NAME: table_lookup_for_modulator#+BEGIN_SRC cfmp->mlphs &= SK_FMPAIR_PHASEMASK;ipos = fmp->mlphs >> fmp->mnlb;x[0] = fmp->mtab[ipos]; if (ipos == fmp->msz - 1) {    x[1] = fmp->mtab[0];} else {    x[1] = fmp->mtab[ipos + 1];} frac = (fmp->mlphs & fmp->mmask) * fmp->minlb;modout = (x[0] + (x[1] - x[0]) * frac);#+END_SRC Before it can modulate the carrier, the output of themodulator needs to be appropriately scaled. This amplitudeamount is the modulator frequency and modulationindex multiplied together. #+NAME: scale_modulator_output#+BEGIN_SRC cmodout *= mfreq * fmp->index;#+END_SRC The frequency of the carrier oscillator is now ready to bemodulated. Modulation is an addition operation; The outputof the scaled modulator is tacked on the carrier frequency. #+NAME: modulate_carrier_frequency#+BEGIN_SRC ccfreq += modout;#+END_SRC A table-lookup operation computation happens for thecarrier oscillator, similar to what happened with themodulator oscillator. This output is what gets returned. #+NAME: table_lookup_for_carrier#+BEGIN_SRC cfmp->clphs &= SK_FMPAIR_PHASEMASK;ipos = (fmp->clphs) >> fmp->cnlb;x[0] = fmp->ctab[ipos]; if (ipos == fmp->csz - 1) {    x[1] = fmp->ctab[0];} else {    x[1] = fmp->ctab[ipos + 1];} frac = (fmp->clphs & fmp->cmask) * fmp->cinlb;out = (x[0] + (x[1] - x[0]) * frac);#+END_SRC To wrap things up, the phase increments and positionsof both oscillators are updated, based on their respective

Lines 1–500 of 700.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t