2.1 Synthesis: making sound from math

import matplotlib
if not hasattr(matplotlib.RcParams, "_get"):
    matplotlib.RcParams._get = dict.get

2.1 Synthesis: making sound from math#

So far, we’ve discussed recording an existing analog signal and storing it as digital audio. Rather than measuring some real-world sound, we can alternatively invent a continuous function \(x(t)\). Then, we can perform synthesis by having the computer repeatedly evaluating \(x(t)\) at integer multiples of the sampling period \(1 / f_s\).

Acoustic instruments are bound by the physics of vibrating strings, air columns, and membranes; the sounds they can produce occupy a tiny corner of the space of all possible waveforms. A computer has no such limitations: any \(x(t)\) you can describe in code is fair game, whether inspired by physics or invented from scratch. However, not all \(x(t)\) are musically interesting. Much of this book concerns how to navigate this much larger space of sonic possibilities.

The recipe for synthesis is simple:

  1. Pick a sample rate \(f_s\) and a duration \(T\) in seconds.

  2. Determine the total number of samples, \(N = \lfloor T \cdot f_s \rfloor\).

  3. Allocate a buffer of length \(N\), an array in memory where we can store samples.

  4. For each index \(n \in \{0, 1, \ldots, N-1\}\), compute \(x[n] = x(n / f_s)\).

  5. Hand the resulting array to the audio system to play back at \(f_s\).

Because synthesis involves sampling the value of a function at many points in time, loops are a ubiquitous primitive in computer music programming. Here is an elementary example: \(x(t) = \sin(2 \pi \cdot 440 t)\), a 440 Hz sine wave (concert A) for one second at CD-quality sample rate, written as a plain Python loop.

import math

f_s = 44100            # samples per second
T = 1.0                # duration in seconds
f = 440.0              # Hz, synthesis parameter
N = int(T * f_s)       # total number of samples

samples = [0.0] * N    # sample "buffer" (memory)
for n in range(N):
    samples[n] = math.sin(2.0 * math.pi * f * (n / f_s))
print(len(samples), samples[:4])
44100 [0.0, 0.06264832417874368, 0.1250505236945281, 0.18696144082725336]

A 440 Hz sine tone, one second long, at \(f_s = 44{,}100\) Hz.

Although this is just a for loop over math.sin, you have already done something nontrivial: used the relationship \(x[n] = x(n / f_s)\) to bridge between the continuous mathematical description of a sound (a function of time) and its discrete computer representation (an array of samples).