FM Synthesis: Pure Data Example

The following Pure Data example shows a 2-operator FM synthesis with two temporal envelopes. This allows the generation of sounds with a dynamic spectrum, for example with a sharp attack and a decrease during the decay, as it is found in many sounds of musical instruments. The patch is derived from the example given by John Chowning in his early FM synthesis publication:

/images/Sound_Synthesis/modulation/fm-chowning-flow-2.png
[Fig.1] Flow chart for dynamic FM with two operators (Chowning, 1973).

The patch fm_example_adsr.pd can be downloaded from the repository. For the temporal envelopes, the patch relies on the abstraction adsr.pd, which needs to be saved to the same directory as the main patch. This ADSR object is a direct copy of the one used in the examples of the PD help browser.

/images/Sound_Synthesis/modulation/pd-fm-envelope.png
[Fig.2] PD FM Patch.

Additive & Spectral: Parabolic Interpolation

Envelopes: Exponential

For percussive, plucked or struck instrument sounds, the envelope needs to model an exponential decay. This is very useful for string-like sounds but most importantly for most electronic musicians, it is the very core of kick drum sounds.

In contrast to the ADSR envelope, the exponential one does not contain a sustain portion for holding a sound. The only parameter is the decay rate, allowing quick adjustment. Alternative to an actual exponential, a modified reciprocal function can be used for easier implementation. The factor $d$ controls the rate of the decay, respectively the decay time:

$$ e = \frac{1}{(1+(d t))} $$


The following example adds a short linear attack before the exponential decay. This minimizes clicks which otherwise occur through the rapid step from $0$ to $1$:

Your browser does not support the HTML5 canvas tag

Attack Time:

Decay Time:

Controlling SC with the Mouse

A quick way of control is often needed when testing and designing synthesis and processing algorithms in SuperCollider. One quick way is to map the mouse position to control rate buses. Combined with a touch display, this can even be an interesting means for expressive control. This example first creates a control bus with two channels. The node ~mouse uses the MouseX and MouseY UGens to influence the two channels of this bus:

// mouse xy controll with busses
~mouse_BUS = Bus.control(s,2);

~mouse   = {
  Out.kr(~mouse_BUS.index,   MouseX.kr(0,1));
  Out.kr(~mouse_BUS.index+1, MouseY.kr(0,1));
}.play;

Exercise

Exercise

Use the mouse example with the previous sawtooth-filter example to control pitch and filter characteristics.

FM Synthesis: Interactive Example

The following example is a minimal FM synthesis with two operators - one modulator and one carrier:

Carrier (Hz):

Modulator (Hz):

Modulation Depth (Hz):

Gain:

Time Domain:

Frequency Domain:

Additive & Spectral: Partial Tracking

Partial tracking is the process of detecting single sinusoidal components in a signal and obtaining their individual amplitude, freuquency and phase trajectories.

Monophonic

  • STFT

    A short term fourier transform segments a signal into frames of equal length.

  • Fundamental Frequency Estimation - YIN (de Cheveign√© et al, 2002) - Swipe (Camacho, 2007)

  • Peak Detection

    For every STFT frame, local maxima are calculated in the range of integer multiples of the fundamental frequency.

/images/Sound_Synthesis/spectral_analysis/amplitudes.png

Trajectories of partial amplitudes for a violin sound.

/images/Sound_Synthesis/spectral_analysis/frequencies.png

Trajectories of partial frequencies for a violin sound.

/images/Sound_Synthesis/spectral_analysis/phases.png

Trajectories of unwrapped partial phases for a violin sound.

Binaural Spatialization with SC-HOA

The SC-HOA library by Florian Grond is a feature-rich toolbox for working with Ambisonics and binaural synthesis in SuperCollider. Once installed, it is well documented inside the SC help files. Additional information and install instructions are part of the Git repository. This section gives a brief introduction into the solution used for the SPRAWL server.

Installing SC-HOA

The SC-HOA library is shipped as a so called Quark and it can be installed from inside SC. Besides a GUI-based way, a single command is enough to install the complete library with all objects and classes in the system's directories:

Quarks.install("https://github.com/florian-grond/SC-HOA")

Make sure to reboot the interpreter after installing the Quark. The external classes need to be compiled.

To find out where SC has installed your external, run:

Platform.userExtensionDir

Encoding a 1st Order Source

The Ambisonics Bus

First thing to create is an audio rate bus for the encoded Ambisonics signal. The bus size depends on the Ambisonics order M, following the formula \(N = (M+1)^2\). For simplicity, this example uses first order:

s.boot;

// create the Ambisonics mix bus

~order     = 1;
~nHOA      = (pow(~order+1,2)).asInteger;
~ambi_BUS  = Bus.audio(s,~nHOA);

The channels of this bus correspond to the spherical harmonics. They encode the overall pressure and the distribution along the three basic dimensions:

/images/nsmi/first-order-harmonics.png

The Encoder

The SC-HOA library includes different encoders. This example uses the HOASphericalHarmonics class. This simple encoder can set the angles of incidence (azimuth, elevation) in spherical coordinates. Angles are controlled in radians:

  • azimuth=0 with elevation=0 is a signal straight ahead
  • azimuth=-pi/2 is hard left
  • azimuth=pi/2 is hard right
  • azimuth=pi is in the back.
  • elevation=pi/2 is on the top
  • elevation=-pi/2 is on the bottom

This example uses a sawtooth signal as mono input and calculates the four Ambisonics channels.

~encoder_A = {arg azim=0, elev=0;
      Out.ar(~ambi_BUS,HOASphericalHarmonics.coefN3D(~order,azim,elev)*Saw.ar(140));
      }.play;

The Ambisonics bus can be monitored and the angles of the source can be set, manually:

~ambi_BUS.scope;

// set parameters
~encoder_A.set(\azim,0)
~encoder_A.set(\elev,0)

Exercise

Change the angles of the encoder and check whether the Ambisonics buses behave as expected. (Use multiples of pi/2.)


The Decoder

The SC-HOA library features default binaural impulse responses, which need to be loaded first:

// load binaural IRs for the decoder
HOABinaural.loadbinauralIRs(s);

Afterwards, a first order HOABinaural decoder is fed with the encoded Ambisonics signal. It needs to be placed after the encoder node to get an audible output to the left and right channels. This output is the actual binaural signal for headphone use.

~decoder = {HOABinaural.ar(~order, In.ar(~ambi_BUS,~nHOA))}.play;
~decoder.moveAfter(~encoder_A);

Exercise

Listen to the decoded signal and change the angles.


Panning Multiple Sources

Working with multiple sources requires a dedicated encoder for each source. All encoded signals are subsequently routed to the same Ambisonics bus and a single decoder is used to create the binaural signal. The angles of all sources can be set, individually.

~encoder_B = {arg azim=0, elev=0;
         Out.ar(~ambi_BUS,HOASphericalHarmonics.coefN3D(~order,azim,elev)*Saw.ar(277))}.play;

~encoder_B.set(\azim,pi/4)
~encoder_B.set(\elev,1)

Exercises

Exercise I

Use the mouse for a continuous control of a source's angles.

Exercise II

Add a control for the source distance to the encoder.

Exercise III

Increase the Ambisonics order and compare the results.

Exercise IV

Use OSC messages to control the positions of multiple sources.

Audio Buffers

Most systems for digital signal processing and music programming process audio in chunks, which are defined by a so called buffer size. These buffer sizes are usually powers of 2, usually ranging from $16$ samples - which can be considered a small buffer size - to $2048$ samples (and more). Most applications, like DAWs and hardware interfaces allow the user to select this parameter. Technically this means that a system collects (or buffers) single samples - for example from an ADC (analog-digital-converter) - until the buffer is filled. This compensates irregularities in the speed of execution for single operations and ensures a jitter-free processing.


Latency

The choice of the buffer size $N$ is usually a trade-off between processor load and system latency. Small buffers require faster processing whereas large buffers keep the user waiting until a buffer has been filled. In combination with the sampling rate $f_s$, the buffer-dependent latency can be calculated as follows:

$$ \tau = \frac{N}{f_s} $$

Round trip latency usually considers both the input and output buffers, thus doubling the latency. For a system running at $48\ \mathrm{kHz}$ with a buffer size of $128$ samples - a typical size for a decent prosumer setup - this results in a round trip latency of $5.5\ \mathrm{ms}$. This value is low enough to allow a perceptually satisfying interaction with the system. When exceeding the $10\ \mathrm{ms}$ threshold it is likely that percussions and other timing-critical instruments experience disrupting latency.


Buffers in Programming

In higher level programming environments like PD, MAX, SuperCollider or Faust (depending on the way it is used), users usually do not need to deal with the buffer size. When programming in C or C++, most frameworks and APIs offer a processing routine which is based on the buffer size. This accounts for solutions like JUCE or the JACK API, but also when programming externals or extensions for the above mentioned higher level environments. These processing routines, also referred to as callback, are called by an interrupt once the the hardware is ready to process the next buffer.



Contents © Henrik von Coler 2021 - Contact