Wave Table Oscillator Implementation¶
The PPG Wave Synthesizer Block Diagram¶
import numpy as np
import matplotlib.pyplot as plt
from scipy.io import wavfile
Step By Step Implementation¶
To get a better understanding of table look up oscillators, we will use design less performative but comprehensive program. This exercise provides the structure and implementation of a table look up oscillator which can then be integrated with modulators and envelope generators to build a wavetable synthesizer in python. In this exercise there are some coding gaps needed to be filled. These gaps are marked with the '[CODE]' label.
We will implement 4 classes today
- The table look up class to store one cycle of waveforms
- The phasor class that implements a phase accumulator that increments at a specific rate (phase_increment)
- The voice generator that uses the table look up class and phasor to generate our specified waveforms
- A simple interpolator
Table Look Up Class¶
We create an object which stores a dictionary of single cycles of waveforms [sine, square, saw, triangle and noise].
class waveTable:
def __init__(self, table_size, duty_cycle = 0.5):
# Constructor to Initialize all the parameters for our wavetable
self.table_size = table_size
self.duty_cycle = duty_cycle
# Dictionary to store one cycle of the waveforms
self.waveTableDictionary = {
'sine': self.generate_sine(),
'square': self.generate_square(),
'saw': self.generate_saw(),
'triangle': self.generate_triangle(),
'noise': self.generate_noise()
}
# Methods to generate one cycle of our waves given the table_size
def generate_sine(self):
# We need an array of length `table_size'
'[CODE]'
return sine_wave
def generate_square(self):
# We need an array of length `table_size'
'[CODE]'
return square_wave
def generate_saw(self):
# We need an array of length `table_size'
'[CODE]'
return saw_wave
def generate_triangle(self):
increment_array = 2*np.arange(self.table_size//2) / (self.table_size//2) - 1
decrement_array = increment_array[::-1]
# How would you handle odd entries for table_size
triangle_wave = np.concatenate((increment_array, decrement_array))
return triangle_wave
def generate_noise(self):
return np.random.uniform(-1, 1, self.table_size)
Phasor Class¶
For a sine wave at $f_0$ Hz with a table size of $N$ samples and a sampling rate of $f_s$ kHz, the sample increment is given by:
$$ \Delta s = \frac{f_0 \times N}{f_s} $$
class phaseGenerator:
def __init__(self, frequency, duration, sample_rate = 48000):
self.frequency = frequency
self.duration = duration
self.sample_rate = sample_rate
self.num_samples = '[Code]'
def generate_phasor(self, table_size):
phasor = np.zeros(self.num_samples)
phase = 0.0
N = table_size
# calculate the phase sample increment here:
'[Code]'
for i in range(self.num_samples):
phasor[i] = phase
# Increment phase
phase += phase_increment
# Wrap phase using modulus
phase %= N
return phasor
The history saving thread hit an unexpected error (OperationalError('attempt to write a readonly database')).History will not be written to the database.
Interpolator¶
# Can be improved to implement different interpolation methods
class linearInterpolator():
#Implementing the call method in a class allows its instances to be invoked as functions, returning a value upon being called.
def __call__(self, values, index):
# Calculate lower index (pl) and fractional part (pf)
lower_index = int(index)
fractional_part = index - lower_index
# Calculate upper index with wrapping (pu)
upper_index = (lower_index + 1) % len(values)
# Handle exact integer case for efficiency
if fractional_part == 0:
return values[lower_index]
# Calculate weighted sum: (1 - pf)T[pl] + pf·T[pu]
'[CODE]'
return interpolated_value
Voice Generator¶
class voiceGenerator:
def __init__(self, table_size, duty_cycle=0.5, sample_rate = 48000):
self.sample_rate = sample_rate
self.table_size = table_size
self.wave_table = waveTable(table_size, duty_cycle=duty_cycle)
self.interpolator = linearInterpolator()
def waveform(self, wave_type, frequency, duration, duty_cycle=0.5):
phase = phaseGenerator(frequency, duration,self.sample_rate)
phasor = phase.generate_phasor(self.table_size)
# Call the appropriate method here
'[CODE]'
output = np.zeros(len(phasor))
for i in range(len(phasor)):
output[i] = self.interpolator(wavetable, phasor[i])
return output
Simple Test¶
from scipy.io import wavfile
#vg = voiceGenerator(table_size=1024)
#waveforms = ['sine', 'square', 'saw', 'triangle']
#for wf in waveforms:
# audio = vg.waveform(wf, 145, 2.0)
# wavfile.write(f"{wf}_145hz.wav", 48000, (audio * 32767).astype(np.int16))
#print("Waveforms generated!")