Sound source: University of Iowa Electronic Music Studios

Listen to these three notes on the piano

Image modified from: piano-music-theory.com

A3
A4
B4

import stuff
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import pandas as pd
import aifc
from pandas.plotting import register_matplotlib_converters
register_matplotlib_converters()  # datetime converter for a matplotlib
import seaborn as sns
sns.set(style="ticks", font_scale=1.5)

import plotly.io as pio
pio.renderers.default = "plotly_mimetype+notebook_connected"
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
pio.templates.default = "presentation"
import math
import scipy
from scipy.io import wavfile

# %matplotlib widget
define useful functions
def open_aiff(filename):
    data = aifc.open(filename)
    Nframes = data.getnframes()
    str_signal = data.readframes(Nframes)
    # d = np.fromstring(str_signal, numpy.short).byteswap()
    d = np.frombuffer(str_signal, dtype=np.int16).byteswap()
    # if there are two channels, take only one of them
    if data.getnchannels() == 2:
        d = d[::2]
    N = len(d)
    dt = 1.0 / data.getframerate()
    time = np.arange(N) * dt
    return time, d

def open_wav_return_fft(filename, max_freq=None):
    sample_rate, signal = wavfile.read(filename)
    time = np.arange(len(signal)) / sample_rate
    dt = time[1] - time[0]
    N = len(time)
    signal = normalize(signal)
    fft = scipy.fft.fft(signal) / N
    k = scipy.fft.fftfreq(N, dt)
    fft_abs = np.abs(fft)
    if max_freq == None:
        max_freq = k.max()
    dk = 1 / (time.max() - time.min())
    n = int(max_freq/dk)
    return k[:n], fft_abs[:n]


def normalize(signal):
    return (signal - signal.mean()) / signal.std()

def fft_abs(signal, time, max_freq=None):
    dt = time[1] - time[0]
    N = len(time)
    fft = scipy.fft.fft(signal) / N
    k = scipy.fft.fftfreq(N, dt)
    fft_abs = np.abs(fft)
    if max_freq == None:
        max_freq = k.max()
    dk = 1 / (time.max() - time.min())
    n = int(max_freq/dk)
    return k[:n], fft_abs[:n]
    
load audio files
tmin = 2.00
tmax = 2.20

timeA4, pianoA4 = open_aiff('files/Piano.mf.A4.aiff')
indices = np.where((timeA4 > tmin) & (timeA4 < tmax))
timeA4 = timeA4[indices]
pianoA4 = normalize(pianoA4[indices])

timeA3, pianoA3 = open_aiff('files/Piano.mf.A3.aiff')
indices = np.where((timeA3 > tmin) & (timeA3 < tmax))
timeA3 = timeA3[indices]
pianoA3 = normalize(pianoA3[indices])

timeB4, pianoB4 = open_aiff('files/Piano.mf.B4.aiff')
indices = np.where((timeB4 > tmin) & (timeB4 < tmax))
timeB4 = timeB4[indices]
pianoB4 = normalize(pianoB4[indices])
plot time series for 3 notes
# blue, orange, green, red, purple, brown, pink, gray, olive, cyan = px.colors.qualitative.D3[:]

color_A3 = "#e7c535"
color_A4 = "#ff00c3"
color_B4 = "#327fce"

fig = make_subplots()

fig.add_trace(
    go.Scatter(x=list(timeA3),
               y=list(pianoA3),
               name='piano A3',
               line=dict(color=color_A3),
               visible='legendonly'),
)

fig.add_trace(
    go.Scatter(x=list(timeA4),
               y=list(pianoA4),
               name='piano A4',
               line=dict(color=color_A4),
               ),
)

fig.add_trace(
    go.Scatter(x=list(timeB4),
               y=list(pianoB4),
               name='piano B4',
               line=dict(color=color_B4),
               visible='legendonly'),
)

# Add range slider
fig.update_layout(
    title='3 notes on the piano',
    xaxis=dict(
        rangeslider={"visible":True},
        type="linear"
    ),
    legend={"orientation":"h",           # Horizontal legend
            "yanchor":"top",             # Anchor legend to the top
            "y":1.1,                    # Adjust vertical position
            "xanchor":"center",           # Anchor legend to the right
            "x":0.5,                       # Adjust horizontal position
           },
)

# Set y-axes titles
fig.update_yaxes(
    title_text="signal",
    range=[-3,3])
fig.update_xaxes(
    title_text="time (s)",)