Skip to content

Instantly share code, notes, and snippets.

@TrebledJ
Last active April 7, 2023 18:31
Show Gist options
  • Select an option

  • Save TrebledJ/f42f9030d1bee0ece8af7fc0db5d0151 to your computer and use it in GitHub Desktop.

Select an option

Save TrebledJ/f42f9030d1bee0ece8af7fc0db5d0151 to your computer and use it in GitHub Desktop.

Revisions

  1. TrebledJ revised this gist Apr 7, 2023. No changes.
  2. TrebledJ revised this gist Apr 7, 2023. No changes.
  3. TrebledJ revised this gist Apr 7, 2023. No changes.
  4. TrebledJ revised this gist Mar 8, 2023. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion wavetable_synthesis.py
    Original file line number Diff line number Diff line change
    @@ -141,6 +141,6 @@ def update(frame):
    ani = FuncAnimation(fig, update, gen, interval=interval)

    if save_animation:
    ani.save('wavetable_synthesis.gif', writer='imagemagick', fps=1000 // interval)
    ani.save('wavetable-synthesis.gif', writer='imagemagick', fps=1000 // interval)
    else:
    plt.show()
  5. TrebledJ created this gist Mar 8, 2023.
    146 changes: 146 additions & 0 deletions wavetable_synthesis.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,146 @@
    # 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()