Skip to content

Instantly share code, notes, and snippets.

@dcooperdalrymple
Created June 20, 2023 19:58
Show Gist options
  • Save dcooperdalrymple/3fe46a3dd48fd7add358111af5bd66ae to your computer and use it in GitHub Desktop.
Save dcooperdalrymple/3fe46a3dd48fd7add358111af5bd66ae to your computer and use it in GitHub Desktop.
CircuitPython synthio Monophonic MIDI Synthesizer
# CircuitPython synthio Monophonic MIDI Synthesizer
# 2023 Cooper Dalrymple - me@dcdalrymple.com
# GPL v3 License
import time
import math
import board
from digitalio import DigitalInOut, Direction, Pull
from busio import UART
import adafruit_midi
from adafruit_midi.note_on import NoteOn
from adafruit_midi.note_off import NoteOff
from adafruit_midi.control_change import ControlChange
from adafruit_midi.program_change import ProgramChange
from adafruit_midi.pitch_bend import PitchBend
from audiobusio import I2SOut
from audiomixer import Mixer
import synthio
import random
import ulab.numpy as numpy
# Program Constants
MIDI_CHANNEL = 1
MIDI_THRU = False
MIDI_TX = board.GP4
MIDI_RX = board.GP5
I2S_CLK = board.GP0
I2S_WS = board.GP1
I2S_DATA = board.GP2
SAMPLE_RATE = 22050
BUFFER_SIZE = 2048
WAVE_SAMPLES = 256
WAVE_AMPLITUDE = 12000 # out of 16384
FILTER_FREQ_MAX = min(SAMPLE_RATE*0.45, 20000)
FILTER_FREQ_MIN = 20
FILTER_RES_MAX = 2.0
FILTER_RES_MIN = 0.7071067811865475
ENVELOPE_TIME_MIN = 0.05
ENVELOPE_TIME_MAX = 2.0
BEND_AMOUNT_MAX = 2.0
BEND_AMOUNT_MIN = 1.0/12.0
# Initialize status LED
led = DigitalInOut(board.LED)
led.direction = Direction.OUTPUT
led.value = True
# Wait for USB to stabilize
time.sleep(0.5)
print("\n:: Initializing Midi ::")
print("UART")
uart = UART(
tx=MIDI_TX,
rx=MIDI_RX,
baudrate=31250,
timeout=0.001
)
print("MIDI Controller")
midi = adafruit_midi.MIDI(
midi_in=uart,
midi_out=uart,
in_channel=MIDI_CHANNEL-1,
out_channel=MIDI_CHANNEL-1,
debug=False
)
print("Channel:", midi.in_channel+1)
print("\n:: Initializing Audio ::")
print("I2S Audio Output")
audio = I2SOut(I2S_CLK, I2S_WS, I2S_DATA)
print("Audio Mixer")
mixer = Mixer(
voice_count=1,
sample_rate=SAMPLE_RATE,
channel_count=2,
bits_per_sample=16,
samples_signed=True,
buffer_size=BUFFER_SIZE
)
audio.play(mixer)
print("\n:: Initializing Synthio ::")
print("Building Waveforms")
waveforms = {
"saw": numpy.linspace(WAVE_AMPLITUDE, -WAVE_AMPLITUDE, num=WAVE_SAMPLES, dtype=numpy.int16),
"reverse_saw": numpy.array(numpy.flip(numpy.linspace(WAVE_AMPLITUDE, -WAVE_AMPLITUDE, num=WAVE_SAMPLES, dtype=numpy.int16)), dtype=numpy.int16),
"square": numpy.concatenate((numpy.ones(WAVE_SAMPLES//2, dtype=numpy.int16)*WAVE_AMPLITUDE,numpy.ones(WAVE_SAMPLES//2, dtype=numpy.int16)*-WAVE_AMPLITUDE)),
"sine": numpy.array(numpy.sin(numpy.linspace(0, 4*numpy.pi, WAVE_SAMPLES, endpoint=False)) * WAVE_AMPLITUDE, dtype=numpy.int16),
"noise": numpy.array([random.randint(-WAVE_AMPLITUDE, WAVE_AMPLITUDE) for i in range(WAVE_SAMPLES)], dtype=numpy.int16)
}
print("Generating Synth")
synth = synthio.Synthesizer(
sample_rate=SAMPLE_RATE,
channel_count=2
)
mixer.voice[0].play(synth)
print("Building Voice")
filter_types = ["lpf", "hpf", "bpf"]
def map_value(value, min_value, max_value):
return min(max((value * (max_value - min_value)) + min_value, min_value), max_value)
def map_array(value, arr):
index = math.floor(max(min(value * len(arr), len(arr) - 1), 0))
return arr[index]
def map_dict(value, dict):
return map_array(value, list(dict))
class Voice:
def __init__(self):
self.notenum = 0
self.velocity = 0.0
self.waveform = "saw"
self._waveform = self.waveform
self.velocity_amount = 1.0
self.attack_time = 0.0
self.decay_time = 0.0
self.release_time = 0.0
self.attack_level = 1.0
self.sustain_level = 0.75
self.filter_type = "lpf"
self._filter_type = self.filter_type
self.filter_frequency = 1.0
self.filter_resonance = 0.0
self.bend = 0.0
self.bend_amount = 0.0
self.note = synthio.Note(
waveform=self.get_waveform(),
frequency=0.0,
envelope=self.build_envelope(),
amplitude=synthio.LFO( # Tremolo
waveform=waveforms.get("sine", None),
rate=1.0,
scale=0.0,
offset=1.0
),
bend=synthio.LFO( # Vibrato
waveform=waveforms.get("sine", None),
rate=1.0,
scale=0.0,
offset=0.0
),
panning=synthio.LFO( # Panning
waveform=waveforms.get("sine", None),
rate=1.0,
scale=0.0,
offset=0.0
),
filter=self.build_filter()
)
synth.blocks.append(self.note.amplitude)
synth.blocks.append(self.note.bend)
synth.blocks.append(self.note.panning)
def press(self, notenum, velocity):
self.velocity = velocity
self.update_envelope()
if notenum != self.notenum:
self.notenum = notenum
self.note.frequency = synthio.midi_to_hz(notenum)
synth.press(self.note)
def release(self):
synth.release(self.note)
self.notenum = 0
def get_velocity_mod(self):
return 1.0 - (1.0 - self.velocity) * self.velocity_amount
def set_waveform(self, value, update=True):
self.waveform = map_dict(value, waveforms)
if update and self.waveform != self._waveform:
self._waveform = self.waveform
self.note.waveform = self.get_waveform()
def get_waveform(self):
return waveforms.get(self.waveform, None)
def build_filter(self):
type = self.get_filter_type()
if type == "lpf":
return synth.low_pass_filter(self.get_filter_frequency(), self.get_filter_resonance())
elif type == "hpf":
return synth.high_pass_filter(self.get_filter_frequency(), self.get_filter_resonance())
else: # "bpf"
return synth.band_pass_filter(self.get_filter_frequency(), self.get_filter_resonance())
def update_filter(self):
self.note.filter = self.build_filter()
def get_filter_type(self):
return self.filter_type
def set_filter_type(self, value, update=True):
self.filter_type = map_array(value, filter_types)
if update and self.filter_type != self._filter_type:
self._filter_type = self.filter_type
self.update_filter()
def set_filter_frequency(self, value, update=True):
self.filter_frequency = value
if update:
self.update_filter()
def get_filter_frequency(self, map=True):
if map:
return map_value(self.filter_frequency, FILTER_FREQ_MIN, FILTER_FREQ_MAX)
else:
return self.filter_frequency
def set_filter_resonance(self, value, update=True):
self.filter_resonance = value
if update:
self.update_filter()
def get_filter_resonance(self, map=True):
if map:
return map_value(self.filter_resonance, FILTER_RES_MIN, FILTER_RES_MAX)
else:
return self.filter_resonance
def build_envelope(self):
return synthio.Envelope(
attack_time=map_value(self.attack_time, ENVELOPE_TIME_MIN, ENVELOPE_TIME_MAX),
decay_time=map_value(self.decay_time, ENVELOPE_TIME_MIN, ENVELOPE_TIME_MAX),
release_time=map_value(self.release_time, ENVELOPE_TIME_MIN, ENVELOPE_TIME_MAX),
attack_level=self.get_velocity_mod() * self.attack_level,
sustain_level=self.get_velocity_mod() * self.sustain_level
)
def update_envelope(self):
self.note.envelope = self.build_envelope()
def set_envelope_attack_time(self, value, update=True):
self.attack_time = value
if update:
self.update_envelope()
def set_envelope_decay_time(self, value, update=True):
self.decay_time = value
if update:
self.update_envelope()
def set_envelope_release_time(self, value, update=True):
self.release_time = value
if update:
self.update_envelope()
def set_envelope_attack_level(self, value, update=True):
self.attack_level = value
if update:
self.update_envelope()
def set_envelope_sustain_level(self, value, update=True):
self.sustain_level = value
if update:
self.update_envelope()
def update_bend(self):
self.note.bend.offset = self.bend * map_value(self.bend_amount, BEND_AMOUNT_MIN, BEND_AMOUNT_MAX)
def set_bend(self, value, update=True):
self.bend = value
if update:
self.update_bend()
def set_bend_amount(self, value, update=True):
self.bend_amount = value
if update:
self.update_bend()
voice = Voice()
print("Configuring Keyboard")
note_types = ["high", "low", "last"]
class Keyboard:
def __init__(self):
self.notes = []
self.type = note_types[0]
def get_type(self):
return self.type
def set_type(self, value):
self.type = map_array(value, note_types)
def _get_low(self):
if not self.notes:
return None
index = 0
notenum = 127
velocity = 1.0
for i in range(len(self.notes)):
if self.notes[i][0] < notenum:
index = i
notenum = self.notes[i][0]
velocity = self.notes[i][1]
return (index, notenum, velocity)
def _get_high(self):
if not self.notes:
return None
index = 0
notenum = 0
velocity = 1.0
for i in range(len(self.notes)):
if self.notes[i][0] > notenum:
index = i
notenum = self.notes[i][0]
velocity = self.notes[i][1]
return (index, notenum, velocity)
def _get_last(self):
if not self.notes:
return None
return (len(self.notes)-1, self.notes[len(self.notes)-1][0], self.notes[len(self.notes)-1][1])
def get(self):
if self.type == "high":
return self._get_high()
elif self.type == "low":
return self._get_low()
else: # "last"
return self._get_last()
def append(self, notenum, velocity, update=True):
for i in range(len(self.notes)):
if self.notes[i][0] == notenum:
self.notes[i][1] = velocity
return
self.notes.append((notenum, velocity))
if update:
self.update()
def remove(self, notenum, update=True):
self.notes = [note for note in self.notes if note[0] != notenum]
if update:
self.update()
def update(self):
note = self.get()
if not note:
voice.release()
else:
voice.press(note[1], note[2])
keyboard = Keyboard()
print("\n:: Initialization Complete ::")
def note_on(notenum, velocity):
keyboard.append(notenum, velocity)
def note_off(notenum):
keyboard.remove(notenum)
def control_change(control, value):
if control == 7: # Volume
mixer.voice[0].level = value
elif control == 70: # Waveform
voice.set_waveform(value)
elif control == 12: # Tremolo Rate
voice.note.amplitude.rate = value
elif control == 92: # Tremolo Depth
voice.note.amplitude.scale = value
elif control == 13: # Tremolo Level Offset
voice.note.amplitude.offset = value
elif control == 76: # Vibrato Rate
voice.note.bend.rate = value
elif control == 77: # Vibrato Depth
voice.note.bend.scale = value
elif control == 78: # Pitch Bend Amount
voice.set_bend_amount(value)
elif control == 16: # Pan Rate
voice.note.panning.rate = value
elif control == 17: # Pan Depth
voice.note.panning.scale = value
elif control == 18: # Pan Offset
voice.note.panning.offset = value
elif control == 71: # Velocity Amount
voice.velocity_amount = value
elif control == 19: # Filter Type
voice.set_filter_type(value)
elif control == 80: # Filter Frequency
voice.set_filter_frequency(value)
elif control == 81: # Filter Resonance
voice.set_filter_resonance(value)
elif control == 73: # Envelope Attack Time
voice.set_envelope_attack_time(value)
elif control == 72: # Envelope Release Time
voice.set_envelope_release_time(value)
elif control == 82: # Envelope Decay Time
voice.set_envelope_decay_time(value)
elif control == 83: # Envelope Attack Level
voice.set_envelope_attack_level(value)
elif control == 79: # Envelope Sustain Level
voice.set_envelope_sustain_level(value)
def pitch_bend(value):
voice.set_bend(value)
while True:
msg = midi.receive()
if msg != None:
if MIDI_THRU:
midi.send(msg)
if isinstance(msg, NoteOn):
#print("Note On:", msg.note, msg.velocity / 127.0)
if msg.velocity > 0.0:
note_on(msg.note, msg.velocity / 127.0)
else:
note_off(msg.note)
elif isinstance(msg, NoteOff):
#print("Note Off:", msg.note)
note_off(msg.note)
elif isinstance(msg, ControlChange):
#print("Control Change:", msg.control, msg.value / 127.0)
control_change(msg.control, msg.value / 127.0)
elif isinstance(msg, PitchBend):
#print("Pitch Bend:", (msg.pitch_bend - 8192) / 8192)
pitch_bend((msg.pitch_bend - 8192) / 8192);
print("\n:: Process Ended ::")
led.value = False
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment