Waveshapes
The default waveshape of a Note
's oscillator in synthio is a square wave. You can use any single-cycle waveshape imaginable, however, by creating a wave shape buffer with sample points in the shape of your wave.
These are then assigned to the Note
's waveform
parameter.
# SPDX-FileCopyrightText: 2023 John Park and @todbot / Tod Kurt # # SPDX-License-Identifier: MIT import time import board import digitalio import audiomixer import synthio import ulab.numpy as np # for PWM audio with an RC filter # import audiopwmio # audio = audiopwmio.PWMAudioOut(board.GP10) # for I2S audio with external I2S DAC board import audiobusio # I2S on Audio BFF or Amp BFF on QT Py: # audio = audiobusio.I2SOut(bit_clock=board.A3, word_select=board.A2, data=board.A1) # I2S audio on PropMaker Feather RP2040 power = digitalio.DigitalInOut(board.EXTERNAL_POWER) power.switch_to_output(value=True) audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA) mixer = audiomixer.Mixer(channel_count=1, sample_rate=44100, buffer_size=4096) amp_env_slow = synthio.Envelope( attack_time=0.15, sustain_level=1.0, release_time=0.8 ) synth = synthio.Synthesizer(channel_count=1, sample_rate=44100, envelope=amp_env_slow) audio.play(mixer) mixer.voice[0].play(synth) mixer.voice[0].level = 0.6 # create sine, tri, saw & square single-cycle waveforms to act as oscillators SAMPLE_SIZE = 512 SAMPLE_VOLUME = 32000 # 0-32767 half_period = SAMPLE_SIZE // 2 wave_sine = np.array(np.sin(np.linspace(0, 2*np.pi, SAMPLE_SIZE, endpoint=False)) * SAMPLE_VOLUME, dtype=np.int16) wave_saw = np.linspace(SAMPLE_VOLUME, -SAMPLE_VOLUME, num=SAMPLE_SIZE, dtype=np.int16) wave_tri = np.concatenate((np.linspace(-SAMPLE_VOLUME, SAMPLE_VOLUME, num=half_period, dtype=np.int16), np.linspace(SAMPLE_VOLUME, -SAMPLE_VOLUME, num=half_period, dtype=np.int16))) wave_square = np.concatenate((np.full(half_period, SAMPLE_VOLUME, dtype=np.int16), np.full(half_period, -SAMPLE_VOLUME, dtype=np.int16))) midi_note = 65 while True: # create notes using those waveforms note1 = synthio.Note(synthio.midi_to_hz(midi_note), waveform=wave_sine, amplitude=1) synth.press(note1) time.sleep(0.75) synth.release(note1) time.sleep(.75) note1 = synthio.Note(synthio.midi_to_hz(midi_note), waveform=wave_tri, amplitude=0.7) synth.press(note1) time.sleep(0.75) synth.release(note1) time.sleep(.75) note1 = synthio.Note(synthio.midi_to_hz(midi_note), waveform=wave_saw, amplitude=0.25) synth.press(note1) time.sleep(0.75) synth.release(note1) time.sleep(.75) note1 = synthio.Note(synthio.midi_to_hz(midi_note), waveform=wave_square, amplitude=0.2) synth.press(note1) time.sleep(0.75) synth.release(note1) time.sleep(.75)
Waveshape Morphing
DIY waveshapes are cool, but even cooler is the ability to morph between waveshapes in real time!
This is done by creating the Note object with an empty waveform buffer, and then instead of replacing that buffer, we copy the new wave into it with with note.waveform[:] = new_wave
# SPDX-FileCopyrightText: 2023 John Park and @todbot / Tod Kurt # # SPDX-License-Identifier: MIT import time import board import audiomixer import digitalio import synthio import ulab.numpy as np # for PWM audio with an RC filter # import audiopwmio # audio = audiopwmio.PWMAudioOut(board.GP10) # for I2S audio with external I2S DAC board import audiobusio # I2S on Audio BFF or Amp BFF on QT Py: # audio = audiobusio.I2SOut(bit_clock=board.A3, word_select=board.A2, data=board.A1) # I2S audio on PropMaker Feather RP2040 power = digitalio.DigitalInOut(board.EXTERNAL_POWER) power.switch_to_output(value=True) audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA) mixer = audiomixer.Mixer(channel_count=1, sample_rate=44100, buffer_size=4096) amp_env_slow = synthio.Envelope( attack_time=0.25, sustain_level=1.0, release_time=0.8 ) synth = synthio.Synthesizer(channel_count=1, sample_rate=44100, envelope=amp_env_slow) audio.play(mixer) mixer.voice[0].play(synth) mixer.voice[0].level = 0.6 # create sine, tri, saw & square single-cycle waveforms to act as oscillators SAMPLE_SIZE = 512 SAMPLE_VOLUME = 32000 # 0-32767 half_period = SAMPLE_SIZE // 2 wave_sine = np.array(np.sin(np.linspace(0, 2*np.pi, SAMPLE_SIZE, endpoint=False)) * SAMPLE_VOLUME, dtype=np.int16) wave_saw = np.linspace(SAMPLE_VOLUME, -SAMPLE_VOLUME, num=SAMPLE_SIZE, dtype=np.int16) wave_tri = np.concatenate((np.linspace(-SAMPLE_VOLUME, SAMPLE_VOLUME, num=half_period, dtype=np.int16), np.linspace(SAMPLE_VOLUME, -SAMPLE_VOLUME, num=half_period, dtype=np.int16))) wave_square = np.concatenate((np.full(half_period, SAMPLE_VOLUME, dtype=np.int16), np.full(half_period, -SAMPLE_VOLUME, dtype=np.int16))) def lerp(a, b, t): # function to morph shapes w linear interpolation return (1-t) * a + t * b wave_empty = np.zeros(SAMPLE_SIZE, dtype=np.int16) # empty buffer we use array slice copy "[:]" on note1 = synthio.Note(frequency=440, waveform=wave_empty, amplitude=0.6) synth.press(note1) pos = 0 my_wave = wave_empty while True: while pos <= 1.0: print(pos) pos += 0.01 my_wave[:] = lerp(wave_sine, wave_saw, pos) note1.waveform = my_wave time.sleep(0.05) while pos >= 0.1: print(pos) pos -= 0.01 my_wave[:] = lerp(wave_sine, wave_saw, pos) note1.waveform = my_wave time.sleep(0.05)
Detuning Oscillators for Fatter Sound
Since we have fine-grained control over a note's frequency with note.frequency
, this means we can do a common technique for getting a "fatter" sound. When a note is played at a specific pitch, a second note object is created with a slightly shifted pitch, which adds organic "movement" and a sort of chorusing effect to the notes.
We can stack up a bunch of these progressively further detuned notes to create a huge wall of synth awesomeness!
# SPDX-FileCopyrightText: 2023 John Park and @todbot / Tod Kurt # # SPDX-License-Identifier: MIT import time import board import audiomixer import digitalio import synthio import ulab.numpy as np # for PWM audio with an RC filter # import audiopwmio # audio = audiopwmio.PWMAudioOut(board.GP10) # for I2S audio with external I2S DAC board import audiobusio # I2S on Audio BFF or Amp BFF on QT Py: # audio = audiobusio.I2SOut(bit_clock=board.A3, word_select=board.A2, data=board.A1) # I2S audio on PropMaker Feather RP2040 power = digitalio.DigitalInOut(board.EXTERNAL_POWER) power.switch_to_output(value=True) audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA) mixer = audiomixer.Mixer(channel_count=1, sample_rate=44100, buffer_size=4096) amp_env_slow = synthio.Envelope( attack_time=0.65, sustain_level=1.0, release_time=0.8 ) synth = synthio.Synthesizer(channel_count=1, sample_rate=44100, envelope=amp_env_slow) audio.play(mixer) mixer.voice[0].play(synth) mixer.voice[0].level = 0.3 # create sine, tri, saw & square single-cycle waveforms to act as oscillators SAMPLE_SIZE = 512 SAMPLE_VOLUME = 32000 # 0-32767 half_period = SAMPLE_SIZE // 2 wave_sine = np.array(np.sin(np.linspace(0, 2*np.pi, SAMPLE_SIZE, endpoint=False)) * SAMPLE_VOLUME, dtype=np.int16) wave_saw = np.linspace(SAMPLE_VOLUME, -SAMPLE_VOLUME, num=SAMPLE_SIZE, dtype=np.int16) wave_tri = np.concatenate((np.linspace(-SAMPLE_VOLUME, SAMPLE_VOLUME, num=half_period, dtype=np.int16), np.linspace(SAMPLE_VOLUME, -SAMPLE_VOLUME, num=half_period, dtype=np.int16))) wave_square = np.concatenate((np.full(half_period, SAMPLE_VOLUME, dtype=np.int16), np.full(half_period, -SAMPLE_VOLUME, dtype=np.int16))) # note1 = synthio.Note( frequency = 220, waveform = wave_sine, amplitude=0.3) detune = 0.003 # how much to detune num_oscs = 1 midi_note = 52 while True: print("num_oscs:", num_oscs) notes = [] # holds note objs being pressed # simple detune, always detunes up for i in range(num_oscs): f = synthio.midi_to_hz(midi_note) * (1 + i*detune) notes.append(synthio.Note(frequency=f, waveform=wave_saw)) synth.press(notes) time.sleep(3.6) synth.release(notes) time.sleep(0.1) # increment number of detuned oscillators num_oscs = num_oscs+1 if num_oscs < 5 else 1
# SPDX-FileCopyrightText: 2023 @todbot / Tod Kurt w mods by John Park # # SPDX-License-Identifier: MIT # wavetable_midisynth_code_i2s.py -- simple wavetable synth that responds to MIDI # 26 Jul 2023 - @todbot / Tod Kurt # Demonstrate using wavetables to make a MIDI synth # Needs WAV files from waveeditonline.com # - BRAIDS01.WAV - http://waveeditonline.com/index-17.html import time import busio import board import audiomixer import synthio import digitalio import audiobusio import ulab.numpy as np import adafruit_wave import usb_midi import adafruit_midi from adafruit_midi.note_on import NoteOn from adafruit_midi.note_off import NoteOff from adafruit_midi.control_change import ControlChange auto_play = False # set to true to have it play its own little song auto_play_notes = [36, 38, 40, 41, 43, 45, 46, 48, 50, 52] auto_play_speed = 0.9 # time in seconds between notes midi_channel = 1 wavetable_fname = "wav/BRAIDS01.WAV" # from http://waveeditonline.com/index-17.html wavetable_sample_size = 256 # number of samples per wave in wavetable (256 is standard) sample_rate = 44100 wave_lfo_min = 0 # which wavetable number to start from 10 wave_lfo_max = 6 # which wavetable number to go up to 25 # for PWM audio with an RC filter # Pins used on QTPY RP2040: # - board.MOSI - Audio PWM output (needs RC filter output) # import audiopwmio # audio = audiopwmio.PWMAudioOut(board.GP10) # for I2S audio with external I2S DAC board # import audiobusio # I2S on Audio BFF or Amp BFF on QT Py: # audio = audiobusio.I2SOut(bit_clock=board.A3, word_select=board.A2, data=board.A1) # I2S audio on PropMaker Feather RP2040 power = digitalio.DigitalInOut(board.EXTERNAL_POWER) power.switch_to_output(value=True) audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA) mixer = audiomixer.Mixer(buffer_size=4096, voice_count=1, sample_rate=sample_rate, channel_count=1, bits_per_sample=16, samples_signed=True) audio.play(mixer) # attach mixer to audio playback synth = synthio.Synthesizer(sample_rate=sample_rate) mixer.voice[0].play(synth) # attach synth to mixer mixer.voice[0].level = 1 uart = busio.UART(tx=board.TX, rx=board.RX, baudrate=31250, timeout=0.001) midi_uart = adafruit_midi.MIDI(midi_in=uart, in_channel=midi_channel-1) midi_usb = adafruit_midi.MIDI(midi_in=usb_midi.ports[0], in_channel=midi_channel-1) # mix between values a and b, works with numpy arrays too, t ranges 0-1 def lerp(a, b, t): return (1-t)*a + t*b class Wavetable: """ A 'waveform' for synthio.Note that uses a wavetable w/ a scannable wave position.""" def __init__(self, filepath, wave_len=256): self.w = adafruit_wave.open(filepath) self.wave_len = wave_len # how many samples in each wave if self.w.getsampwidth() != 2 or self.w.getnchannels() != 1: raise ValueError("unsupported WAV format") self.waveform = np.zeros(wave_len, dtype=np.int16) # empty buffer we'll copy into self.num_waves = self.w.getnframes() // self.wave_len self.set_wave_pos(0) def set_wave_pos(self, pos): """Pick where in wavetable to be, morphing between waves""" pos = min(max(pos, 0), self.num_waves-1) # constrain samp_pos = int(pos) * self.wave_len # get sample position self.w.setpos(samp_pos) waveA = np.frombuffer(self.w.readframes(self.wave_len), dtype=np.int16) self.w.setpos(samp_pos + self.wave_len) # one wave up waveB = np.frombuffer(self.w.readframes(self.wave_len), dtype=np.int16) pos_frac = pos - int(pos) # fractional position between wave A & B self.waveform[:] = lerp(waveA, waveB, pos_frac) # mix waveforms A & B wavetable1 = Wavetable(wavetable_fname, wave_len=wavetable_sample_size) amp_env = synthio.Envelope(attack_level=0.2, sustain_level=0.2, attack_time=0.05, release_time=0.3, decay_time=.5) wave_lfo = synthio.LFO(rate=0.2, waveform=np.array((0, 32767), dtype=np.int16)) lpf = synth.low_pass_filter(4000, 1) # cut some of the annoying harmonics synth.blocks.append(wave_lfo) # attach wavelfo to global lfo runner since cannot attach to note notes_pressed = {} # keys = midi note num, value = synthio.Note, def note_on(notenum): # release old note at this notenum if present if oldnote := notes_pressed.pop(notenum, None): synth.release(oldnote) if not auto_play: wave_lfo.retrigger() f = synthio.midi_to_hz(notenum) vibrato_lfo = synthio.LFO(rate=1, scale=0.01) note = synthio.Note(frequency=f, waveform=wavetable1.waveform, envelope=amp_env, filter=lpf, bend=vibrato_lfo) synth.press(note) notes_pressed[notenum] = note def note_off(notenum): if note := notes_pressed.pop(notenum, None): synth.release(note) def set_wave_lfo_minmax(wmin, wmax): scale = (wmax - wmin) wave_lfo.scale = scale wave_lfo.offset = wmin last_synth_update_time = 0 def update_synth(): # pylint: disable=global-statement global last_synth_update_time # only update 100 times a sec to lighten the load if time.monotonic() - last_synth_update_time > 0.01: # last_update_time = time.monotonic() wavetable1.set_wave_pos( wave_lfo.value ) last_auto_play_time = 0 auto_play_pos = -1 def update_auto_play(): # pylint: disable=global-statement global last_auto_play_time, auto_play_pos if auto_play and time.monotonic() - last_auto_play_time > auto_play_speed: last_auto_play_time = time.monotonic() note_off( auto_play_notes[ auto_play_pos ] ) auto_play_pos = (auto_play_pos + 3) % len(auto_play_notes) note_on( auto_play_notes[ auto_play_pos ] ) set_wave_lfo_minmax(wave_lfo_min, wave_lfo_max) def map_range(s, a1, a2, b1, b2): return b1 + ((s - a1) * (b2 - b1) / (a2 - a1)) print("wavetable midisynth i2s. auto_play:",auto_play) while True: update_synth() update_auto_play() msg = midi_uart.receive() or midi_usb.receive() if isinstance(msg, NoteOn) and msg.velocity != 0: note_on(msg.note) elif isinstance(msg,NoteOff) or isinstance(msg,NoteOn) and msg.velocity==0: note_off(msg.note) elif isinstance(msg,ControlChange): if msg.control == 21: # mod wheel scan_low = map_range(msg.value, 0,127, 0, 64) set_wave_lfo_minmax(scan_low, scan_low)
Tyrell Desktop Synth
This synthio-based creation is an adaptation of Tod Kurt's eighties_dystopia synth. It combines many of the concepts we've covered, including:
- detuned oscillators
- LFO modulation of filter cutoff
- saw wave oscillators
- user pitch input
For more info, check out the full Learn Guide.
Circle of Fifths Euclidean Synth
This synth by Liz Clark celebrates all things circular: the circle of fifths, Euclidean rhythms, and rotary encoders. Four synth voices play random notes in a triad to the beat of a determined Euclidean rhythm animated on the 8x8 matrix. You can scroll through the circle of fifths on each synth voice to change the triad for easy modulation between keys.
For more info, check out the full Learn Guide.
Computer Perfection Synthesizer
This project takes the Computer Perfection and reuses its buttons and switches to trigger a polyphonic, multi-timbral wavetable synthesizer for all your spacey jam sessions. It includes ADSR envelopes and LFO modulation for a beautiful, other-worldly sound.
For more info, check out the full Learn Guide.
Monosynth
Another awesome Tod Kurt creation, this is a monosynth created in synthio that you can control with a MIDI keyboard.
You can plug it into your computer or other USB MIDI Host, or a keyboard via UART (classic DIN-5 or TRS MIDI).
Wavetable Polysynth
Tod's done it again. This is a syntio-based polyphonic synth that uses a Mutable Instruments Plaits wavetable for wave shape morphing source.
Mini Arpeggiator
This "todbot" character sure is prolific! Here's his self-contained arpeggiator synth (with controls for root note, BPM, pattern, and pattern octaves) called eighties_arp
Text editor powered by tinymce.