Skip to content

Instantly share code, notes, and snippets.

@TrebledJ
Last active April 7, 2023 18:31
Show Gist options
  • Save TrebledJ/f42f9030d1bee0ece8af7fc0db5d0151 to your computer and use it in GitHub Desktop.
Save TrebledJ/f42f9030d1bee0ece8af7fc0db5d0151 to your computer and use it in GitHub Desktop.
Demonstration of wavetable synthesis using numpy and matplotlib. See it in action: https://trebledj.github.io/posts/digital-audio-synthesis-for-dummies-part-2/#wavetable-synthesis
# Wavetable Synthesis Demo
# by @TrebledJ
from matplotlib.patches import ConnectionPatch
import matplotlib.ticker as mticker
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np
# Wavetable Parameters
table_size = 32 # Size of the wavetable (lookup table).
# Generation Parameters
freq = 8 # Frequency of the sine wave to generate (in Hz).
sample_rate = 100 # Sampling rate of the generated signal.
nsamples = 25 # Number of samples to generate.
# Animation Parameters
# Save the animation (True) or play the animation using Matplotlib's rendering (False).
save_animation = True
# Number of milliseconds between frames.
interval = 500
def generate_samples(wavetable, freq, sample_rate, *, once=False, nsamples=20):
"""
Generate samples from a wavetable.
Note that `sample_rate` should be at least `2 * freq`, and the
### Parameters
1. wavetable : np.array[float]
- The wavetable containing all the pregenerated samples.
2. freq : float
- The frequency of the waveform to generate.
3. sample_rate : float
- The sampling rate of the waveform to generate.
4. once : bool
- True to make at most one pass through the wavetable.
- False to run for `nsamples` times.
5. nsamples : int
- Number of samples to generate.
### Returns
- A generator yielding (phases, samples), i.e. (x, y) values along the wavetable.
"""
phases = [] # Stores phases (x).
samples = [] # Stores samples (y).
# The phase is a float with two components: the integer part and the decimal part.
# The integer part indicates the sample along the wavetable which we are at, modulus
# the wavetable's length. The decimal part indicates the fraction between the left and
# right samples.
phase = 0
for _ in range(nsamples):
if once and phase > table_size:
break
idx = int(phase) # Integer part.
frac = phase - idx # Decimal part.
samp0 = wavetable[idx]
samp1 = wavetable[(idx + 1) % table_size]
samp = samp0 + (samp1 - samp0) * frac # Interpolate!
phases.append(phase)
samples.append(samp)
yield phases, samples
phase += table_size * freq / sample_rate
phase %= table_size
yield None
t = np.linspace(0, 1, table_size + 1)
wavetable = np.sin(2 * np.pi * t)
fig, ax = plt.subplots(2, 1)
ax[0].plot(t * table_size, wavetable, 'bo-')
ax[0].set_title("Wavetable (Lookup Table)")
ax[0].axis('off')
ax[1].set_title('Samples')
ax[1].set_xlim(-0.5, nsamples + 0.5)
ax[1].set_ylim(-1.1, 1.1)
ax[1].xaxis.set_major_locator(mticker.MultipleLocator(5))
# We'll store drawn points and connections (to be removed on subsequent iterations).
points = []
connections = []
def update(frame):
"""
Update the animation frame with the next sample.
"""
if frame is None:
# Finished!
try:
points[-1].remove()
connections[-1].remove()
except:
pass
# Freeze!
ani.pause()
return fig,
ph, samples = frame
wt_x = ph[-1] % table_size
wt_y = samples[-1]
p = ax[0].plot([wt_x], [wt_y], 'ro', ms=4)
ax[1].plot(np.arange(len(samples)), samples, 'ro')
# Magic line between axs.
con = ConnectionPatch(xyA=(wt_x, wt_y), xyB=(len(samples) - 1, samples[-1]), coordsA=ax[0].transData, coordsB=ax[1].transData,
color="red")
fig.add_artist(con)
try:
points[-1].remove()
connections[-1].remove()
except:
pass
points.append(p[0])
connections.append(con)
return fig,
gen = generate_samples(wavetable, freq, sample_rate, nsamples=nsamples)
ani = FuncAnimation(fig, update, gen, interval=interval)
if save_animation:
ani.save('wavetable-synthesis.gif', writer='imagemagick', fps=1000 // interval)
else:
plt.show()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment