Skip to main content

Music Synthesis Wave Creation With Python

In my previous post, Music Synthesis Wave Analysis With Python, I talked about harmonics in sound waves and how they can be synthezised from electrical signals. Knowing that there are harmonics present in the most used wave forms in music synthesis (triangle, sawtooth and square shapes), I showed the results of the fourier transform analysis to understand the harmonic components of each wave. We now understand that each wave is the result of summing different signals with different frequencies and amplitudes. This can be observed in the Fast Fourier Transform (FFT) graphs shown in the previous post.

It is awesome to know all of this, but you may wonder if it is possible to start from a simple sine wave and try to construct each wave by summing other sine waves to the fundamental. The short answer is: YES. Thanks to the FFT we can visualize each frequency component of a given wave, from there we can try and cook up each one according to the recipe. This is the basic principle of the Fourier Series. In this post, I will try to get as close as I can since the precise relationship of amplitudes and frequencies of each harmonic component for a given wave is hard to replicate. It will be an exercise of aproximations in order to illustrate how powerful the FFT is for understanding waveforms.

Importing Necessary Modules

 

As before, we will start by importing our needed modules and functions.

In [1]:
from typing import List
import pandas as pd
import numpy as np
from scipy.fftpack import fft, fftfreq
import IPython.display as ipd
import matplotlib.pyplot as plt
from scipy import signal

Audio and Plot

 

I did some modifications to the play_and_plot_wave function and added the sum_harmonics in order to do the Fourier sum of each wave related to the provided harmonics list. The amplitude of each harmonic component will be decreased exponentially as we advance the harmonic value. I decided to do this since the results of each previously analyzed waves ressembled an exponential decay in their amplitudes.

 

Note that only sine waves will be necessary in the following functions.

In [2]:
sr = 44100 # Sample Rate in Hz
duration = 2
frequency = 440 # Musical note A
plot_args = {"figsize": (6, 4), "legend": False} # Plot arguments

def sum_harmonics(harmonics: List[int], amplitude: float, bias: float, tau: float) -> np.array:
    waves = []
    # Fourier sum with exponential decay of harmonic amplitudes.
    for i in sorted(set([1] + harmonics)):
        harmonic = i * frequency
        phases = np.cumsum(2.0 * np.pi * harmonic / sr * np.ones(int(sr * float(duration))))
        waves.append(np.sin(phases) * (bias + amplitude * np.exp(-tau * (i - 1))))
    
    return sum(waves, start=np.empty(shape=(1,)))

def play_and_plot_wave(
    harmonics: List[int] = [1],   
    amplitude: float = 1,
    bias: float = 0.0,
    tau: float = 1
) -> None:
    wave = sum_harmonics(harmonics, amplitude, bias, tau)
    yf = fft(wave)
    xf = fftfreq(len(wave)) * sr
    
    fig, (axis1, axis2) = plt.subplots(2, 1, constrained_layout=True)
    axis1.set_title('Signal', fontsize=12)
    axis1.set_yticks([])
    axis2.set_title('FFT', fontsize=12)
    axis2.set_yticks([])
    axis2.set_xticks(np.arange(1, 11) * frequency)
    
    fft_df = pd.DataFrame({"Frequency": xf, "Magnitude": np.abs(yf)})
    wave_df = pd.DataFrame({"Time": np.arange(5, len(wave)+ 5) * (1000 / sr), "Amplitude": wave})
    wave_df.plot(x="Time", y="Amplitude", xlabel="Time (ms)", xlim=(0, 20), ax=axis1, **plot_args)
    xlim2 = (frequency / 2, frequency * 10)
    fft_df.plot(x="Frequency", y="Magnitude", xlabel="Frequency (Hz)", xlim=xlim2, ax=axis2, **plot_args)

    display(ipd.Audio(wave, rate=sr, normalize=True))

Creating The Waves

 

We will start with the fundamental wave, our sine at 440Hz frequency, just to have a comparison on how this will morph into our aproximated wave shapes. For every result in the following graphs, we will be observing up to the 10th harmonic.

 

WARNING

 

Be careful when listening to the audio. If you are wearing headphones, turned down the volume or take them off.

Sine Wave

 

No surprises here, we see a clean sine wave with the expected harmonic only at the fundamental 440Hz frequency. Let the wave shaping begin.

In [3]:
play_and_plot_wave()

Square(ish) Wave

 

As we observed in the square wave analysis of our previous post, this wave shape only had odd harmonic components and it seemed like an exponential decay between them. We will try to approximate the result by just adding sine waves up to the 9th odd harmonic and see what we get. We will have to tweak a bit the decay bias and damping variable tau.

In [4]:
play_and_plot_wave(
    harmonics=[1, 3, 5, 7, 9], 
    amplitude=2, 
    bias=0.05, 
    tau=0.8
)

Pretty close, right? We se can observe that we made the edges steep and the peaks a bit flattened. We also see some curly lines at the peaks, product of our harmonic amplitudes aproximations. Turns out Fourier was right, and he figured this out without computers. You can compare the FFT results with the graph in the previous post.

Lets not stop here and keep synthesizing waves!  

Sawtooth(ish) Wave

 

The sawtooth wave analysis showed us that this waveform has even and odd harmonic components present. Based on that, we proceed to sum all of the sine waves up to the 10th harmonic.

In [5]:
play_and_plot_wave(harmonics=list(range(1,11)), amplitude=2, bias=0.08, tau=0.8)

Again, close enough. The difference is that our frankenstein version is tilted to the left in comparison to the original which is tilted to the right. Again we see some curly peaks and slopes due to the way we calculated the amplitude relationship of each harmonic component. I followed the recipe and we see an exponential decay between each component up to the 10th harmonic. If you play the sound, you'll hear that it resembles pretty well the original sawtooth.  

Triangle(ish) Wave

 

This one was the hardest one to get and the aproximation is not that good in comparison to the previous ones. If you recall the triangle wave analysis, this wave had also only odd harmonic components but the amplitudes of each were smaller compared to the square wave. I had to sum only the odd harmonics up to the 5th and tweak a bit the tau parameter.

In [6]:
play_and_plot_wave(
    harmonics=[1, 3, 5], 
    amplitude=2, 
    bias=0.15, 
    tau=3
)

As you can see, I obtained some sharp peaks and straight slopes. It still resembles a sine wave but if you look the FFT graph you'll see that the harmonic components are present. Not as similar as the ones from the previous post but I am happy with the results.

Wrapping Up

 

Working with the knowledge acquired by understanding the FFT, we were able to see how we can build any waveform by just summing sine waves with different frequencies and amplitudes. We can easily obtain the usual syntheziser wave shapes by either using a function in any programming language or by designing a circuit that generates the electrical waves but... now you know how the fundamental principle works by visualizing the results of applying it.

As a sound synthesist, you work with whatever wave shape is given to you and mix, filter or distort them to make new sounds. Another approach is to use the basic building block, the sine wave, and add harmonics, subharmonics, fold or multiply the waves in order to create your sounds. These are the two perspectives that this series of posts are trying to give you and which opens up the discussion of Additive vs Subtractive Synthesis (or well known as West Coast and East Coast Synthesis). This could be a subject for a future post, since it is very broad.

I hope this is of use to someone, somewhere. I'm thinking about implementing a wavefolder, that could be the next post in this series.