Code
FS = 22050
LEAD = 0.5 # silence before every sound [s]
FADE = 0.03 # fade in and out of every note [s]
PARTIALS = np.arange(1, 11)
BELL_CENTER, BELL_WIDTH = 500.0, 1.0 # the Shepard bell: center [Hz] and standard deviation [octaves]
def bell(t, f):
"""The level of a Shepard tone's partial at frequency ``f``: a bell over log frequency."""
return np.exp(-0.5 * (np.log2(f / BELL_CENTER) / BELL_WIDTH) ** 2)
def harmonic_tone(f0, duration, fade=FADE, partials=PARTIALS):
"""An ordinary note: ten harmonics (or ``partials``) falling as 1/n, faded in and out over
``fade`` seconds."""
return so.harmonic_complex(duration, FS, f0, harmonics=partials, amplitudes=1 / partials).ramp(fade)
def shepard_tone(freq, duration):
"""A Shepard tone of the pitch class of ``freq``: that pitch class in every octave from
below 8 Hz up to Nyquist, each partial at the bell's level."""
lowest = freq * 2.0 ** -np.ceil(np.log2(freq / 8)) # the same pitch class, between 4 and 8 Hz
harmonics = 2 ** np.arange(int(np.floor(np.log2(FS / 2 / lowest))) + 1)
return so.harmonic_complex(duration, FS, lowest, harmonics=harmonics, amplitudes=bell).ramp(FADE)
def sequence(notes, step):
"""Notes one after another, each ``step`` seconds after the last (they may overlap), after
LEAD seconds of silence. ``step`` is one number, or one per note for notes of different
lengths."""
steps = np.broadcast_to(step, len(notes))
onsets = LEAD + np.concatenate([[0], np.cumsum(steps[:-1])])
length = max(int(round(onset * FS)) + note.n_samples for onset, note in zip(onsets, notes, strict=True))
data = np.zeros(length)
for onset, note in zip(onsets, notes, strict=True):
first = int(round(onset * FS))
data[first : first + note.n_samples] += note.data[:, 0]
return so.Sound(data, FS)
def draw(snd, ax_spectrogram, ax_chroma, fmin=50, fmax=8000):
"""A spectrogram on a log frequency axis, and the sound's chroma under it."""
stft = so.STFT(snd, win_dur=80e-3, hop_dur=10e-3)
keep = (stft.f >= fmin) & (stft.f <= fmax)
level = stft.db[0][keep]
ax_spectrogram.pcolormesh(
stft.t, stft.f[keep], level, vmin=level.max() - 70, vmax=level.max(), cmap="magma", rasterized=True
)
ax_spectrogram.set(yscale="log", ylabel="Frequency [Hz]", title="Spectrogram (Hann 80 ms)")
so.chroma(snd).plot(ax_chroma)
ax_chroma.set(xlim=(0, snd.duration))
ax_spectrogram.set(xlim=(0, snd.duration))
def show(snd):
"""Spectrogram and chroma of one sound. Returns the figure and the panels the playhead follows."""
fig, (ax_spectrogram, ax_chroma) = plt.subplots(2, 1, figsize=(10, 5.6), layout="constrained")
draw(snd, ax_spectrogram, ax_chroma)
return fig, [ax_spectrogram, ax_chroma]