#!/usr/bin/env python3 from dataclasses import dataclass from NCO import NCO, output_type from Blocks import Summer, Truncator from FixedPoint import FXfamily, FXnum import wave import struct @dataclass class Patch: sine_shift: int square_shift: int triangle_shift: int sawtooth_shift: int global_gain: int pure_sine = Patch(0, 32, 32, 32, 0) pure_triangle = Patch(32, 32, 0, 32, 0) harmonics = Patch(0, 0, 32, 32, 0) # A base synth that doesn't use the truncator at the end class Synth: def __init__(self, fsamp: float, patch: Patch) -> None: self.nco = NCO(fsamp, interpolate = True) self.summer = Summer(patch.sine_shift, patch.square_shift, patch.triangle_shift, patch.sawtooth_shift) self.truncator = Truncator(patch.global_gain) self.playing_note = False self.note_freq = 0 def start_note(self, freq: float) -> None: self.nco.reset() self.note_freq = freq self.playing_note = True def release_note(self) -> None: self.playing_note = False def next_sample(self) -> int: nco_out = self.nco.next_sample_f(self.note_freq) summer_out = self.summer.next_sample(nco_out) if self.playing_note: return summer_out else: return FXnum(0, output_type) # A monophonic synth that uses the truncator at the output of 1 base synth class MonoSynth(Synth): def next_sample(self) -> int: nco_out = self.nco.next_sample_f(self.note_freq) summer_out = self.summer.next_sample(nco_out) truncator_out = self.truncator.next_sample(summer_out) if self.playing_note: return int(truncator_out) else: return 0 # A polyphonic synth that uses the truncator at the output of N base synths after summing class PolySynth: def __init__(self, fsamp: float, patch: Patch, polyphony: int) -> None: self.synths = [Synth(fsamp, patch) for s in range(polyphony)] self.active = [False]*polyphony self.notes = [0]*polyphony self.truncator = Truncator(patch.global_gain) def start_note(self, freq: float) -> None: for idx,synth in enumerate(self.synths): if not self.active[idx]: synth.start_note(freq) self.active[idx] = True self.notes[idx] = freq break def release_note(self, freq: float) -> None: for idx,note in enumerate(self.notes): if freq == note and self.active[idx]: self.synths[idx].release_note() self.active[idx] = False self.notes[idx] = freq break def next_sample(self) -> int: sample = FXnum(0, output_type) for synth in self.synths: sample = sample + synth.next_sample() return self.truncator.next_sample(sample) if __name__ == "__main__": fsamp = 30e3 s = MonoSynth(fsamp, pure_sine) #s = PolySynth(fsamp, pure_sine, 4) s.start_note(220) samples = [s.next_sample() for x in range(10000)] s.start_note(440) samples.extend([s.next_sample() for x in range(10000)]) s.start_note(880) samples.extend([s.next_sample() for x in range(10000)]) for s in samples: print(s) output_wav = wave.open('synth.wav','w') output_wav.setparams((2,2,int(fsamp),0,'NONE','not compressed')) values = [] for s in samples: result = int(s) packed_value = struct.pack('h',result) values.append(packed_value) values.append(packed_value) value_str = b''.join(values) output_wav.writeframes(value_str) output_wav.close() import matplotlib.pyplot as plt plt.plot(samples) plt.show()