What Is A Note?

Chris Tralie, CS 372: Digital Music Processing

In [1]:
import numpy as np
import matplotlib.pyplot as plt
import IPython.display as ipd
import pandas as pd
from collections import OrderedDict

Part 1: Harmonicity

We saw at the third module that a vibrating string supports different frequencies that are integer multiples of a base frequency, known as "harmonics" or "overtones." Let's look at a few examples of base frequencies and their harmonics. We'll start with a base frequency of 220hz, and then we'll also look at a base frequency of 440z, which is known as its "octave" (the space in between these two frequencies is also referred to as an "octave"). We perceive these notes to be the same pitch, because we perceive pitch logarithmically in frequency. In other words, multiplying a sequence of frequencies by a constant amount will lead to a constant additive shift in our perception.

Finally, we'll look at frequencies at 330hz and 275hz, which are in 3/2 ratios and 5/4 ratios of 220hz, and which are known as fifths and third, respectively. And we look at another frequency, 313, which forms a "tri tone" with respect to 220hz. The plots below show notes with these base frequencies, along with their first 16 harmonics. Listen and notice that every harmonic of the octave 440hz is contained in the harmonics of 220hz, every other harmonic of 330 is contained in the harmonics of 220, and every fourth harmonic of 275 is contained in the harmonics of 220.

In [2]:
def make_html_audio(ys, sr, width=100):
    clips = []
    for y in ys:
        audio = ipd.Audio(y, rate=sr)
        audio_html = audio._repr_html_().replace('\n', '').strip()
        audio_html = audio_html.replace('<audio ', '<audio style="width: {}px; "'.format(width))
        clips.append(audio_html)
    return clips
In [3]:
sr = 44100
t = np.linspace(0, 1, sr)
pd.set_option('display.max_colwidth', None)  
tuples = []
summed = {}
for f0 in [220, 440, 330, 275, 313]:
    ys = []
    fs = (f0*np.arange(1, 17)).tolist()
    all_together = np.zeros_like(t)
    for f in fs:
        y = np.cos(2*np.pi*f*t)
        ys.append(y)
        all_together += y/f # Put in less of the high frequencies
    ys = [all_together] + ys
    fs = ["All Together"] + fs
    summed[f0] = all_together
    clips = make_html_audio(ys, sr, width=50)
    tuples += [("{} hz Harmonics".format(f0), fs), ("{} hz sinusoids".format(f0), clips)]
df = pd.DataFrame(OrderedDict(tuples))
ipd.HTML(df.to_html(escape=False, float_format='%.2f'))
Out[3]:
220 hz Harmonics 220 hz sinusoids 440 hz Harmonics 440 hz sinusoids 330 hz Harmonics 330 hz sinusoids 275 hz Harmonics 275 hz sinusoids 313 hz Harmonics 313 hz sinusoids
0 All Together All Together All Together All Together All Together
1 220 440 330 275 313
2 440 880 660 550 626
3 660 1320 990 825 939
4 880 1760 1320 1100 1252
5 1100 2200 1650 1375 1565
6 1320 2640 1980 1650 1878
7 1540 3080 2310 1925 2191
8 1760 3520 2640 2200 2504
9 1980 3960 2970 2475 2817
10 2200 4400 3300 2750 3130
11 2420 4840 3630 3025 3443
12 2640 5280 3960 3300 3756
13 2860 5720 4290 3575 4069
14 3080 6160 4620 3850 4382
15 3300 6600 4950 4125 4695
16 3520 7040 5280 4400 5008

If we add together a note and its octave, it sounds quite nice, because all of the harmonics of the octave are contained in the original note

In [4]:
y = summed[220] + 2*summed[440]
ipd.Audio(y, rate=sr)
Out[4]:

The same is true about adding 220hz, 330hz, and 275 together, because they sall hare many harmonics. This is known as a major triad

In [5]:
y = summed[220] + summed[275] + summed[330]
ipd.Audio(y, rate=sr)
Out[5]:

By contrast, if we add 220hz to 313hz, it sounds much less pleasing, because none of the harmonics line up perfectly, but many of them are close, so it creates a lot of beat frequencies that sound "dissonant"

In [6]:
y = summed[220] + summed[313]
ipd.Audio(y, rate=sr)
Out[6]:

Part 2: Circle of Fifths

Now, you should construct 13 notes going in 3/2 intervals, starting at 440hz, but keep them in the interval between 440 and 880. So if the frequency goes above 880, simply halve it to go an octave down. This is known as the "circle of fifths"

In [7]:
f = 440
ys = []
freqs = []
for i in range(13):
    freqs.append(f)
    ys.append(np.cos(2*np.pi*f*t))
    f = f*3/2
    if f > 880:
        f /= 2

tuples = [("Frequencies", freqs), ("Sinusoids", make_html_audio(ys, sr))]
df = pd.DataFrame(OrderedDict(tuples))
ipd.HTML(df.to_html(escape=False, float_format='%.2f'))
Out[7]:
Frequencies Sinusoids
0 440.00
1 660.00
2 495.00
3 742.50
4 556.88
5 835.31
6 626.48
7 469.86
8 704.79
9 528.60
10 792.89
11 594.67
12 446.00

Part 3: Inconsistency of The Circle of Fifths

The circle of fifths is supposed to cycle through the 12 unique notes in an octave. But there is a discrepancy when we cycle all around, which is referred to as the Pythagorean comma.

There's another issue that arises using these notes when we want to "transpose" a tune, or move it up or down by a constant amount of notes. To see this, let's first sort the above notes so they are in frequency order.

In [8]:
idx = np.argsort(np.array(freqs[0:-1]))
freqs_sorted = np.array([freqs[i] for i in idx])
ys_sorted = [ys[i] for i in idx]
tuples = [("Frequencies", freqs_sorted), ("Sinusoids", make_html_audio(ys_sorted, sr))]
df = pd.DataFrame(OrderedDict(tuples))
ipd.HTML(df.to_html(escape=False, float_format='%.2f'))
Out[8]:
Frequencies Sinusoids
0 440.00
1 469.86
2 495.00
3 528.60
4 556.88
5 594.67
6 626.48
7 660.00
8 704.79
9 742.50
10 792.89
11 835.31

Now let's construct the beginning of the happy birthday tune, starting at an A. So putting together the sinusoids at index 0, 2, 0, 5, 4

In [9]:
y = np.concatenate((ys_sorted[0], ys_sorted[2], ys_sorted[0], ys_sorted[5], ys_sorted[4]))
ipd.Audio(y, rate=sr)
Out[9]:

If we want to go up by two notes, then we can use indices 2, 4, 2, 7, 6

In [10]:
y = np.concatenate((ys_sorted[2], ys_sorted[4], ys_sorted[2], ys_sorted[7], ys_sorted[6]))
ipd.Audio(y, rate=sr)
Out[10]:

But there is a problem. If we look at the ratios of the corresponding frequencies from the first tune from the second tune, they are not all the same!

In [11]:
freqs_sorted[[2, 4, 2, 7, 6]] / freqs_sorted[[0, 2, 0, 5, 4]]
Out[11]:
array([1.125     , 1.125     , 1.125     , 1.10985791, 1.125     ])

That fourth note is "flat" in the second tune with respect to the others! So they are technically not the same tune, even up to transposition. What all of this means is that it's impossible to construct all of the notes in a physically harmonic way while also being consistent! So we will have to settle for something close.

Let's now go back to the formula that we saw earlier for constructing notes on top of a base frequency. This time, we'll use 440 as our base frequency to be consistent with the above example. Let $p$ be how many halfsteps we are above the base frequency $f_0$. Then the formula is:

$ f = f_0*2^{\frac{p}{12}} $

Notice now that if we move up by a constant amount of $k$, then the ratio between the notes is a constant $2^{k/12}$. So they really would be the same tune, because they are multiplied by a constant frequency for each note, and, as we said in the beginning, a constant multiple leads to a constant shift in our perception. For this reason, this way of constructing notes is known as "equal tempered." Let's follow this formula to compare the notes we constructed

In [12]:
freqs_formula = 440*(2**(np.arange(12)/12))
ys_formula = [np.cos(2*np.pi*f*t) for f in freqs_formula]
tuples = [("Our Frequencies", freqs_sorted), ("Our Sinusoids", make_html_audio(ys_sorted, sr))]
tuples += [("Formula Frequencies", freqs_formula), ("Formula Sinusoids", make_html_audio(ys_formula, sr))]
df = pd.DataFrame(OrderedDict(tuples))
ipd.HTML(df.to_html(escape=False, float_format='%.2f'))
Out[12]:
Our Frequencies Our Sinusoids Formula Frequencies Formula Sinusoids
0 440.00 440.00
1 469.86 466.16
2 495.00 493.88
3 528.60 523.25
4 556.88 554.37
5 594.67 587.33
6 626.48 622.25
7 660.00 659.26
8 704.79 698.46
9 742.50 739.99
10 792.89 783.99
11 835.31 830.61

Finally, it's worth it to take another look at the circle of fifths just using note numbers. What we see in the above table is that the fifth corresponds to note number 7. So we're jumping by 7 notes each time and wrapping around for octaves. This corresponds to using the operator mod 12. So the question is, do we return back to 0 after adding 7 repeatedly mod 12? Let's see:

In [13]:
notes = np.mod(np.arange(25)*7, 12)
print(notes)
[ 0  7  2  9  4 11  6  1  8  3 10  5  0  7  2  9  4 11  6  1  8  3 10  5
  0]

Yes we do! Let's hear them all together as sinusoids one after the other using the formula for equal tempered frequencies

In [14]:
t = np.arange(int(sr/2))/sr
y = np.array([])
for p in notes:
    f = 440*2**(p/12)
    y = np.concatenate((y, np.cos(2*np.pi*f*t)))
ipd.Audio(y, rate=sr)
Out[14]:
In [ ]: