Last active
April 7, 2023 18:31
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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