Skip to content

Instantly share code, notes, and snippets.

@thorwhalen
Created July 7, 2020 17:41
Show Gist options
  • Save thorwhalen/6745b24f3966d6015b8fc4bba5aed6c7 to your computer and use it in GitHub Desktop.
Save thorwhalen/6745b24f3966d6015b8fc4bba5aed6c7 to your computer and use it in GitHub Desktop.
Looking into the musical notes and color correspondence
"""
I've often seen that “color of musical notes” thing,
and I always wondered about it (especially because I’d like to use it as educational support when I teach my (reluctant)
daughter music).
The mappings I see are not always consistent, but one comes up most often;
the one where the red-to-violet range is mapped to the C-to-B range.
Now this could just so happen to be correct, but the choice that C as the canonical root note of a scale seems
to be arbitrary, whereas red as the first color is a physical reality, leading me to think that:
- The choice of C as the root note was purposely based on the red-to-violet correspondence
- It’s a crock of s**t
Here's look into the numbers…
The conclusion is:
It’s a lie. At least the C -> red, D -> Orange, ..., B -> Violet is.
But… It does seem to correspond to an A-based scale!
That is A -> Red (on the orange side), B -> Yellow, C -> Green, D -> Blue, E -> Blue-Violet, F -> Red-Violet, G -> Red
Which now makes me think that it’s the reason why we call it “A” in the first place...
The following script prints:
abs_note log2_base_freq Color color_freq_THz
C 0.03 Green 566.0
C# 0.11 Cyan 600.0
D 0.20 Blue 638.0
D# 0.28 Blue 638.0
E 0.36 Violet 714.0
F 0.45 Violet 714.0
F# 0.53 Violet 714.0
G 0.61 Red 428.0
G# 0.70 Red 428.0
A 0.78 Red 428.0
A# 0.86 Orange 484.0
B 0.95 Yellow 517.0
"""
import requests
import pandas as pd
import numpy as np
import re
from collections import Counter
note_to_abs_note = lambda note: re.compile('[#A-G]+').match(note).group(0)
def get_notes_base_freq():
url = 'https://pages.mtu.edu/~suits/notefreqs.html'
r = requests.get(url)
html = r.content
_, df = pd.read_html(html)
df.columns = ['Note', 'Frequency', 'Wavelength']
df['abs_note'] = list(map(note_to_abs_note, df['Note']))
df['log2_freq'] = np.log2(df['Frequency'])
df['log2_base_freq'] = np.mod(df['log2_freq'], 1) # to get a representative frequency number for a note
# the decimals=2 is what leads to a consistent note->log2_base_freq mapping:
df['log2_base_freq'] = np.round(df['log2_base_freq'], decimals=2)
assert len(Counter(df['log2_base_freq'])) == len(Counter(df['abs_note'])) # assert that we're good!
t = df[['abs_note', 'log2_base_freq']].groupby('abs_note').mean()
return t.reset_index()
def get_color_base_freq():
url = 'https://en.wikipedia.org/wiki/Color'
r = requests.get(url)
html = r.content
tables = pd.read_html(html)
d = tables[2] # chose number 2 manually (see "the way I got the color data" section)
d = d[['Color', '(THz)']]
d = d.iloc[1:8]
d['(THz)'] = d['(THz)'].astype(float)
d['Color'] = d['Color'].replace('Violet (visible)', 'Violet')
t = d['(THz)'] * 1e12 # Tera means 10^12 (10 ** 12 == 1e12)
d['log2_base_freq'] = np.mod(np.log2(t), 1)
d['color_freq_THz'] = d['(THz)']
del d['(THz)']
return d
notes = get_notes_base_freq()
color = get_color_base_freq()
the_truth_about_it = pd.merge_asof(notes.sort_values('log2_base_freq'),
color.sort_values('log2_base_freq'),
on='log2_base_freq')
print(the_truth_about_it.to_string(index=False))
"""
Prints:
abs_note log2_base_freq Color color_freq_THz
C 0.03 Green 566.0
C# 0.11 Cyan 600.0
D 0.20 Blue 638.0
D# 0.28 Blue 638.0
E 0.36 Violet 714.0
F 0.45 Violet 714.0
F# 0.53 Violet 714.0
G 0.61 Red 428.0
G# 0.70 Red 428.0
A 0.78 Red 428.0
A# 0.86 Orange 484.0
B 0.95 Yellow 517.0
"""
@thorwhalen
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment