Skip to content

Instantly share code, notes, and snippets.

@alefnull
Last active November 10, 2021 21:43
Show Gist options
  • Save alefnull/ea81e75d797db71b22689673900d99f5 to your computer and use it in GitHub Desktop.
Save alefnull/ea81e75d797db71b22689673900d99f5 to your computer and use it in GitHub Desktop.
WaveGenerator
from wave_generator import WaveGenerator
import numpy as np
NOTES = {
'C': 16.35,
'C#': 17.32,
'D': 18.35,
'D#': 19.45,
'E': 20.60,
'F': 21.83,
'F#': 23.12,
'G': 24.50,
'G#': 25.96,
'A': 27.50,
'A#': 29.14,
'B': 30.87
}
def gen_wavs(waveforms=['sine'], octaves=[4], amp=0.5, dur=1, sr=44100):
for waveform in waveforms:
if waveform == 'noise' or waveform == 'pink':
continue
index = 0
for octave in octaves:
for note in NOTES:
freq = NOTES[note] * 2 ** octave
wav = WaveGenerator(freq=freq, waveform=waveform, amp=amp,
dur=dur, sr=sr)
wav.write_to_file('./wavs/{}/{}_{}_{}{}.wav'.format(
waveform, index, waveform, note, octave))
index += 1
wav = WaveGenerator(waveform='noise', amp=amp, dur=dur, sr=sr)
wav.write_to_file('./wavs/noise/noise.wav')
wav = WaveGenerator(waveform='pink', amp=amp, dur=dur, sr=sr)
wav.write_to_file('./wavs/noise/pink.wav')
def main():
import matplotlib.pyplot as chart
# create the basic waveforms
sine = WaveGenerator(
freq=440, amp=0.2, dur=1, phase=0, waveform='sine')
square = WaveGenerator(
freq=440, amp=0.2, dur=1, phase=0, waveform='square')
sawtooth = WaveGenerator(
freq=440, amp=0.2, dur=1, phase=0, waveform='sawtooth')
triangle = WaveGenerator(
freq=440, amp=0.2, dur=1, phase=0, waveform='triangle')
noise = WaveGenerator(
freq=440, amp=0.2, dur=1, phase=0, waveform='noise')
pink = WaveGenerator(
freq=440, amp=0.2, dur=1, phase=0, waveform='pink')
# add the basic waveforms, creating every combination
# sine_square = sine.add(square)
# sine_sawtooth = sine.add(sawtooth)
# sine_triangle = sine.add(triangle)
# sine_noise = sine.add(noise)
# sine_pink = sine.add(pink)
# square_sawtooth = square.add(sawtooth)
# square_triangle = square.add(triangle)
# square_noise = square.add(noise)
# square_pink = square.add(pink)
# sawtooth_triangle = sawtooth.add(triangle)
# sawtooth_noise = sawtooth.add(noise)
# sawtooth_pink = sawtooth.add(pink)
# triangle_noise = triangle.add(noise)
# triangle_pink = triangle.add(pink)
# noise_pink = noise.add(pink)
# all_waveforms = sine_square.add(sawtooth_triangle).add(noise_pink)
# play the basic waveforms
# sine.play()
# square.play()
# sawtooth.play()
# triangle.play()
# noise.play()
# pink.play()
# play the additive waveforms
# sine_square.play()
# sine_sawtooth.play()
# sine_triangle.play()
# sine_noise.play()
# sine_pink.play()
# square_sawtooth.play()
# square_triangle.play()
# square_noise.play()
# square_pink.play()
# sawtooth_triangle.play()
# sawtooth_noise.play()
# sawtooth_pink.play()
# triangle_noise.play()
# triangle_pink.play()
# noise_pink.play()
# all_waveforms.play()
# chart.plot(all_waveforms.samples[:100])
# chart.show()
# save WAV files of given waveforms
# (one file per note in each given octave)
# octaves = [4]
# waveforms = ['sine', 'square', 'sawtooth', 'triangle', 'noise', 'pink']
# gen_wavs(waveforms=waveforms, octaves=octaves)
# plot the basic waveforms
chart.plot(sine.samples[:100], label='sine')
chart.plot(square.samples[:100], label='square')
chart.plot(sawtooth.samples[:100], label='sawtooth')
chart.plot(triangle.samples[:100], label='triangle')
chart.plot(noise.samples[:100], label='noise')
chart.plot(pink.samples[:100], label='pink')
chart.xlabel('Samples')
chart.ylabel('Amplitude')
chart.legend()
chart.title('Basic Waveforms')
chart.show()
# plot the pink noise's power spectrum
pink_samples = pink.samples
pink_fft = np.fft.fft(pink_samples)
pink_fft_db = 20 * np.log10(pink_fft)
pink_fft_db_norm = pink_fft_db / max(pink_fft_db)
chart.plot(pink_fft_db_norm[:200], label='pink')
chart.xlabel('Frequency')
chart.ylabel('Amplitude')
chart.legend()
chart.title('Pink Noise Power Spectrum')
chart.show()
if __name__ == '__main__':
main()
import numpy as np
import pandas as pd
import sounddevice as sd
import soundfile as sf
# wave generator class:
# - generates a waveform with a given frequency,
# amplitude, duration, and phase
# - uses nothing but numpy and the python standard library
# (plus sounddevice & soundfile for sound output)
# - supports a variety of waveforms
# (sine, square, sawtooth, triangle, noise[white, pink])
# - can save the waveform to a WAV file with soundfile
# - can play the waveform with sounddevice
class WaveGenerator:
def __init__(self, freq=440, amp=0.5, dur=0.5, phase=0,
waveform='sine', sr=44100):
self.freq = freq
self.amplitude = amp
self.duration = dur
self.phase = phase
self.waveform = waveform
self.sample_rate = sr
self.samples = self.generate_samples()
def generate_samples(self):
# generate samples
samples = []
for i in range(int(self.duration * 44100)):
samples.append(self.get_sample(i / 44100))
if self.waveform == 'pink':
samples = self.white_to_pink(samples)
return np.array(samples)
def get_sample(self, t):
# get sample at time t
if self.waveform == 'sine':
return self.get_sine_sample(t)
elif self.waveform == 'square':
return self.get_square_sample(t)
elif self.waveform == 'sawtooth':
return self.get_sawtooth_sample(t)
elif self.waveform == 'triangle':
return self.get_triangle_sample(t)
elif self.waveform == 'noise':
return self.get_noise_sample(t)
elif self.waveform == 'pink':
return self.get_noise_sample(t)
else:
raise ValueError("Invalid waveform: " + self.waveform)
def get_sine_sample(self, t):
# get sine sample at time t
x = np.sin(2 * np.pi * self.freq * t + self.phase)
return self.amplitude * x / 2 + self.amplitude / 2
def get_square_sample(self, t):
# get square sample at time t
return self.amplitude * (np.sign(np.sin(
2 * np.pi * self.freq * t + self.phase)) + 1) / 2
def get_sawtooth_sample(self, t):
# get sawtooth sample at time t
return self.amplitude * (t % (1 / self.freq) / (1 / self.freq))
def get_triangle_sample(self, t):
# get triangle sample at time t
per = 1 / self.freq
x = np.floor(t / per + 0.5)
return self.amplitude * (2 * np.abs((t / per) - x))
def get_noise_sample(self, t):
# get noise sample at time t
# white noise is characterized by a series of random impulses,
# with a constant amplitude and a power spectrum that is
# uniformly distributed.
return self.amplitude * np.random.random()
def white_to_pink(self, samples):
# convert white noise to pink noise
# see: http://www.dsprelated.com/showarticle/838.php
# convert to numpy array
samples = np.array(samples).astype(np.float32)
nrows = len(samples)
ncols = 16
# initialize the matrix
matrix = np.zeros((nrows, ncols))
matrix.fill(np.nan)
matrix[0, :] = np.random.random(ncols)
matrix[:, 0] = np.random.random(nrows)
# the total number of changes is nrows
cols = np.random.geometric(0.5, nrows)
cols[cols >= ncols] = 0
rows = np.random.randint(nrows, size=nrows)
matrix[rows, cols] = np.random.random(nrows)
matrix = matrix * self.amplitude
df = pd.DataFrame(matrix)
df = df.fillna(method='ffill', axis=0)
total = df.sum(axis=1)
# normalize the total?
total = total / total.max() * self.amplitude
return total.values
def write_to_file(self, filename):
# write waveform to file
sf.write(filename, self.samples, 44100)
def play(self):
# play waveform
sd.play(self.samples, 44100)
sd.wait()
def add(self, other):
# add two waveforms
new = WaveGenerator(
freq=self.freq,
amp=self.amplitude,
dur=self.duration,
phase=self.phase,
waveform=self.waveform
)
new.samples = self.samples + other.samples
return new
def power_spectrum(self):
# convert samples to power spectrum, and return as numpy array
return np.abs(np.fft.fft(self.samples))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment