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
Page last edited January 22, 2025
Text editor powered by tinymce.