Project 3: FM Synthesis, Spectral Centroid

Project 3 due Sunday, February 15, 2026 by 11:59PM Eastern

Peer grading opens: Monday, February 16, 2026 at 11:59PM Eastern

Peer grading due: Sunday, February 22, 2026 by 11:59PM Eastern

Download

First, download p3.zip which you will need for this project.

Introduction

In FM Synthesis, the depth of modulation ($D$) and the frequency of modulation ($M$) control the number of generated significant (audible) sideband pairs together, using the following formula: $I = D / M$. We can think of depth of modulation in Hz, which means how many Hz by which the carrier frequency deviates. Thus, the result of $I = D / M$ is a unit-less number, called the Index of Modulation. When $D = 0$, there are no sidebands generated (see FM Synthesis (Chapter 4) for more detail.)

Part 1: Create an FM Instrument

Create an FM instrument, a function named fminstr, in the file proj3.sal that takes the following keyword parameters:

The default behavior should be a tone with a carrier / fundamental frequency controlled by pitch, and where higher im produces more and stronger harmonics.

To achieve this, first recall the conventional formula for FM synthesis: $f(t) = \sin(\int_{0}^{t} \omega(\tau)d\tau)$, where $\omega(\tau) = 2 \pi \left[ C + m(\tau))\right]$, and $m(\tau) = D \sin(2\pi M \tau)$. Intuitively, this is just a basic sinusoid with a time-varying frequency $\omega(\tau)$ that is modulated by $m(\tau)$ around a center frequency of $C$. Once integrated, this simplifies to a perceptually equivalent formulation: $\sin(2 \pi C t + I \sin(2 \pi M t))$, where $I = D / M$.

In Nyquist, fmosc implements a more general formulation, taking as input two arguments that together define the time-varying frequency $\omega(\tau)$. Firstly, the carrier frequency $C$ (in steps, conversion to Hz handled for you). Secondly, the time-varying modulation term $m(\tau)$ in units of Hz (not steps), which can be any SOUND, not just a basic sinusoid as in the above definition. fmosc handles the integration for you internally.

fmosc is overly general given our goals here, so we want to restrict things in two ways: (1) we only want our instrument to take in $I = D/M$ as input, and (2) we want to implement conventional FM. Accordingly, you will need to work through the arithmetic to figure out how to implement the conventional $m(\tau) = D \sin(2\pi M \tau)$ given im and pass that in to the second argument of fmosc. By default, you should set the $C$:$M$ ratio to be 1:1, though we encourage you to experiment with other ratios.

You may add additional parameters to fminstr to control loudness (vel:), vibrato, C:M ratio, envelope parameters, etc. The stretch factor should allow you to control the duration of the instrument sound.

Be sure that your instrument uses an envelope to control overall amplitude. If you run play osc(g4) in Nyquist, you will hear an example of what your instrument should not sound like! In fact, if your envelope has just a quick fade-in, a constant hold, and then a quick fade-out, we will not consider that to be an envelope. Be musical. Be creative.

An example that plays the instrument is:

play fminstr(pitch: g4, im: const(0, 1)) ~ 2

In this example, the im parameter is the constant zero with a duration of 1, so this is expected to play a boring sine tone at pitch G4.

Create a function named part1 with no parameters, in file proj3.sal. Running this should instantiate a single instance of your fminstr and return the sound, which should last about as long as the stretch factor. So the command play part1() ~ 3 should play about 3s and will demonstrate that you completed Part 1. Your part1 function can pass in and demonstrate optional keyword parameters you have added.

Hint: Be sure to test part1 with different durations (stretch factors).

Part 2: Time-Varying Index of Modulation

Demonstrate the instrument by using PWL to create an interesting Index of Modulation. In other words, replace const(0, 1) in the previous example with an interesting, time-varying Index of Modulation. You can use the envelope editor in the Nyquist IDE to draw an envelope if you like, and you can apply scale factors and offsets to the PWL function to fine tune your sonic result.

Keep in mind that if you put in a long PWL envelope (or any other envelope), your fminstr() code will not magically deduce you want all of the other components to stretch to the duration of the envelope.

Create a function named part2 with no parameters, in the file proj3.sal. Running this should instantiate a single instance of your fminstr with your PWL modulation and return the sound, which should be 3 to 5s long and contain obvious changes in brightness due to a time-varying im. So the command play part2() will demonstrate that you completed Part 2.

Part 3: Composition using Spectral Centroid Analysis

For a (hopefully) more interesting composition, we are going to analyze a real sound, extract the time-varying spectral centroid, and use that to control the Index of Modulation of one or more FM sounds.

Read the documentation on spectral centroids in the accompanying files (look in the downloaded directory), and try out the project3-demos.sal that is provided. To summarize, you call spectral-centroid() on an input sound (or filename), and it returns an envelope that tracks the spectral centroid of the input sound. (We will discuss spectral centroid in class as well.)

The idea here is that when the input sound gets brighter, the spectral centroid goes up. If we use spectral centroid to control the Index of Modulation, an increasing spectral centroid should cause the Index of Modulation to increase, and therefore the FM sound should get brighter. Thus, there should be a connection between spectral variation of the source sound analyzed with spectral-centroid() and the spectral variation of your synthesized sound.

Your technical task is to make this connection between input sound and output sound. This is a rare case where we are going to suggest assigning the spectral centroid (sound) to a global variable. If you do that, then any note can reference the spectral centroid. For example:

set *sc* = 0.003 * spectral-centroid(...)
play seq(fminstr(pitch: c4, im: *sc*),
         fminstr(pitch: c4, im: *sc*) ~ 2)

This plays two notes. The first runs nominally from 0s to 1s, and it will use the first second of the spectral centroid sound to control its Index of Modulation. The second note runs nominally from 1 to 3s (the duration is 2 because of the stretch operator ~), and this note will use the spectral centroid from 1 to 3s. It is important to note that the second note does not begin “reading” the *sc* variable from the beginning. This is consistent with the idea that, in Nyquist, sounds have an absolute start time.

PROGRAMMING TASK: Your musical task is to create something interesting, with a duration of 30 to 60 seconds. Store your code for part 3 in proj3comp.sal. We do not want to box you in to a specific procedure, but we are looking for a result that shows interesting spectral variation driven by the spectral centroid of a source sound. Some possibilities include:

Grading Criteria

Grading will be based on meeting the technical specifications of the assignment:

Turning In Your Work

Note that the following must be strictly adhered to for the convenience of machine and human graders (including you as a peer grader):

All of these files must be in the top level of the zip file, not in a subfolder within the zip file. Remember to keep your submission anonymous for peer grading.