Last active
November 10, 2021 21:43
-
-
Save alefnull/ea81e75d797db71b22689673900d99f5 to your computer and use it in GitHub Desktop.
WaveGenerator
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
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() |
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
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