import math
from scipy.io import wavfile
import numpy as np

A little bit on the physics of the piano

The base tone is the “A4” key with a frequency of \(440 \text{Hz}\). A4 happens to be the 49th key on the piano. You can calculate the frequency of any of the other keys on the piano, say the nth key, as a function of the frequency of A4 as \(2^{\frac{n-49}{12}}\times 440 \text{Hz}\).

We’ll calculate the frequency from the “middle C” (C4) of the piano up to B4, i.e., keys 40 to 51. Below we’re going to create a Piano class that allows us to produce individual notes at will.

Some of the code in this notebook is inspired by and draws from this excellent blog post by Katie He.

class Piano:
    
    __slots__ = ["_noteFreqs", "_sampleRate"]

    def __init__(self, sampleRate=44100):
        """
        Construct a simple piano with one octave from 
        C4 (middle C) to B4
        """
        
        notes = ["C", "C#", "D", "D#", "E", "F", "F#", \
                 "G", "G#", "A", "A#", "B"]
        self._noteFreqs = {}
        
        # calculate frequencies of desired notes
        for i in range(len(notes)):
            noteNum = 40 + i
            self._noteFreqs[notes[i]] = 2**((noteNum-49)/12) * 440
            
        self._sampleRate = sampleRate
        
    def getSampleRate(self):
        """
        Getter method for the sample rate
        """
        return self._sampleRate
    
    def produceNote(self, note, duration, amplitude=4096):
        """
        Produce a pure sine wave corresponding to the note
        """
        
        # We're going to produce a pure sine wave for the duration
        # of the note. The frequency of the sine wave is decided by
        # the note itself, and we need to produce readings of this 
        # sine wave at regular intervals of the duration given 
        # by the sampling rate.
        
        time = 0; increment = 1/self._sampleRate
        result = []
        
        # generate a list of readings based on frequencies of notes
        while time < duration:
            result.append(amplitude*math.sin(2*math.pi*self._noteFreqs[note]*time))
            time += increment
            
        return result
        
    
    def __str__(self):
        """
        Produce a string representation of our piano
        """

        result = ""
        for note in self._noteFreqs:
            result += note + " : " + str(round(self._noteFreqs[note], 2)) + " Hz\n"
        return result
piano = Piano()
print(piano)
C : 261.63 Hz
C# : 277.18 Hz
D : 293.66 Hz
D# : 311.13 Hz
E : 329.63 Hz
F : 349.23 Hz
F# : 369.99 Hz
G : 392.0 Hz
G# : 415.3 Hz
A : 440.0 Hz
A# : 466.16 Hz
B : 493.88 Hz
middleC = piano.produceNote("C", 2)
middleC = np.array(middleC) # convert to array for writing to sound file
wavfile.write('middleC.wav', rate=piano.getSampleRate(), data=middleC.astype(np.int16))
# what does middle C look like?
import matplotlib.pyplot as plt

plt.figure()
plt.plot(middleC[500:2500])
plt.xlabel('Time')
plt.ylabel('Amplitude')
plt.title('Sound Wave of Middle C on Piano')
plt.grid()
../../_images/wrap-up_5_0.png
def composeSong(instrument, notes, durations):
    """
    Compose a song on the given instrument using the given notes
    and durations.
    """
    
    song = []

    for i in range(len(notes)):
        newNote = instrument.produceNote(notes[i], durations[i])
        song += newNote
        
        # uncomment this after first try
        # need to add this slight emptiness because our notes don't fade
        # like a normal piano
        song += [0]*10 
    
    return song
    
notes = ['C', 'C', 'G', 'G',
         'A', 'A', 'G',
         'F', 'F', 'E', 'E',
         'D', 'D', 'C',
         'G', 'G', 'F', 'F',
         'E', 'E', 'D',
         'G', 'G', 'F', 'F',
         'E', 'E', 'D',
         'C', 'C', 'G', 'G',
         'A', 'A', 'G',
         'F', 'F', 'E', 'E',
         'D', 'D', 'C',]



durations = [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1]*6


mysterySong = composeSong(piano, notes, durations)
mysterySong = np.array(mysterySong)
wavfile.write('mysterySong.wav', piano.getSampleRate(), data=mysterySong.astype(np.int16))

What do proteins in our body sound like?

You may be thinking.. “I’m not a musician, this isn’t of much use to me if I don’t already know the notes to the song.” But the beauty is.. you don’t have to be a classically trained musician to create music – you just need to appreciate the music around you :)

Here’s a quote from Joi (an AI) in Blade Runner 2049:

“Mere data makes a man — A and C and T and G. The alphabet of you — all from four symbols. I am only two — 1 and 0.”

A, C, T, and G. Three of those are already notes on a musical instrument. Let’s just translate T to an F (that choice isn’t totally random, there is some musical theory justification for it), and hear what proteins in our body sound like. You could say that this is a really simple algorithm for “sonification” of proteins. We’ll only show you this simple algorithm, but the sky’s the limit really – check out this sonification of the coronavirus spike protein.

Here’s the raw DNA sequence for hemoglobin:

\(GTG/CAC/CTG/ACT/CCT/GAG\)

We’ll also then sonify sickle cell hemoglobin (which results in sickle cell anemia):

\(GTG/CAC/CTG/ACT/CCT/GTG\)

The difference is in that last triplet where \(GTG\) is now \(GAG\) – such a minor difference in our DNA, can lead to such adverse effects on our body.

hbNotes = ["G", "F", "G",
           "C", "A", "C",
           "C", "F", "G",
           "A", "C", "F",
           "C", "C", "F",
           "G", "A", "G"]

shbNotes = ["G", "F", "G",
           "C", "A", "C",
           "C", "F", "G",
           "A", "C", "F",
           "C", "C", "F",
           "G", "F", "G"]

durations = [0.5, 0.5, 1]*6

hbSong = composeSong(piano, hbNotes, durations)
hbSong = np.array(hbSong)
wavfile.write('hbSong.wav', piano.getSampleRate(), data=hbSong.astype(np.int16))

shbSong = composeSong(piano, shbNotes, durations)
shbSong = np.array(shbSong)
wavfile.write('shbSong.wav', piano.getSampleRate(), data=shbSong.astype(np.int16))