Skip to content

Instantly share code, notes, and snippets.

@todbot
Last active July 13, 2023 07:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save todbot/9bbbcd93e04c9ce258b0f4ffbe7dc43a to your computer and use it in GitHub Desktop.
Save todbot/9bbbcd93e04c9ce258b0f4ffbe7dc43a to your computer and use it in GitHub Desktop.
Demonstrate cool new CircuitPython synthio.Synthesizer as wavetable MIDI synth, using new synthio.Envelope feature
#
# midi_env_synthio_synth.py -- Demonstrate new synthio.Synthesizer as wavetable MIDI synth
# using new synthio.Envelope feature in https://github.com/adafruit/circuitpython/pull/7862
#
# 30 Apr 2023 - @todbot / Tod Kurt
#
# Hooked up to generic I2S DAC
# - MIDI key velocity controls attack & release times
# - MIDI modwheel (CC1) controls wave mix between saw and a granular AKWF wave
#
import time
import board, analogio
import audiobusio, audiomixer
import synthio
import ulab.numpy as np
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
import neopixel
SAMPLE_RATE = 28000 # clicks @ 36kHz & 48kHz on rp2040
SAMPLE_SIZE = 200
VOLUME = 12000
# qtpy rp2040 SPI pins
lck_pin, bck_pin, dat_pin = board.MISO, board.MOSI, board.SCK
# synth engine setup
waveform = np.zeros(SAMPLE_SIZE, dtype=np.int16) # intially all zeros (silence)
# "amp_env" will be replaced on every key press (and key release?)
amp_env = synthio.Envelope(attack_time=0.1, decay_time = 0.05, release_time=0.2,
attack_level=1, sustain_level=0.8)
synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE, waveform=waveform, envelope=amp_env)
audio = audiobusio.I2SOut(bit_clock=bck_pin, word_select=lck_pin, data=dat_pin)
mixer = audiomixer.Mixer(voice_count=1, sample_rate=SAMPLE_RATE, channel_count=1,
bits_per_sample=16, samples_signed=True, buffer_size=2048 )
audio.play(mixer)
mixer.voice[0].play(synth)
midi = adafruit_midi.MIDI(midi_in=usb_midi.ports[0], in_channel=0 )
led = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2)
# waveforms setup
wave_saw = np.linspace(VOLUME, -VOLUME, num=SAMPLE_SIZE, dtype=np.int16)
# akwf_0018_08 I think
wave_weird1 = np.array((198,2776,5441,8031,10454,12653,14609,16333,17824,19130,20260,21227,22043,22721,23269,23699,24019,24243,24385,24461,18630,-26956,-28048,-29175,-30249,-31227,-32073,-32631,-32359,-31817,-30941,-29663,-27900,-25596,-22591,-18834,-14291,-9016,-3212,2794,8624,13943,18544,22353,25408,27780,29553,30855,31751,32315,32611,32687,32593,32351,31983,31491,30871,30097,28895,-28240,-30489,-31343,-31975,-32431,-32697,-32767,-32615,-32217,-31525,-30489,-29035,-27090,-24519,-21237,-17178,-12339,-6829,-902,5081,10748,15805,20102,23615,26396,28510,30109,31245,31995,31955,31437,30729,29887,28943,27908,26784,25560,24077,22781,-22207,-22735,-22709,-22471,-22065,-21497,-20773,-19896,-18872,-17698,-16361,-14857,-13141,-11206,-9054,-6717,-4259,-1796,522,2548,4167,5339,6079,6445,6503,6319,5949,5449,4847,4183,3480,2756,2028,1304,590,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-478,-1168,-1882,-2596,-3336,-4074,-4795,-5487,-6119,-6669,-7095,-7357,-7399,-7157,-6559,-5543,-4076,-2132,), dtype=np.int16)
wave_weird1 = wave_weird1 * 0.5 # lower volume a bit
# map s range a1-a2 to b1-b2
def map_range(s, a1, a2, b1, b2): return b1 + ((s - a1) * (b2 - b1) / (a2 - a1))
# 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
waveform[:] = wave_saw
wave_mix = 0
last_debug_time = time.monotonic()
print("midi_env_synthio_synth")
while True:
# handle midi
msg = midi.receive()
if isinstance(msg, NoteOn) and msg.velocity != 0:
print("noteOn: ", msg.note, "vel=", msg.velocity)
led.fill(0xff00ff)
# can't do this:
# amp_env.attack_time = map_range( msg.velocity, 1,127, 3, 0.1)
# instead, replace entire synthio.amp_env
# NOTE: this means new notes overwrite envelope of held notes
attack_time = map_range( msg.velocity, 1,127, 3, 0.05)
release_time = map_range( msg.velocity, 1,127, 3, 0.1)
amp_env = synthio.Envelope(attack_time = attack_time,
decay_time = amp_env.decay_time,
release_time = release_time,
attack_level = amp_env.attack_level,
sustain_level = amp_env.sustain_level)
synth.envelope = amp_env
synth.press( (msg.note,) )
elif isinstance(msg,NoteOff) or isinstance(msg,NoteOn) and msg.velocity==0:
print("noteOff:", msg.note, "vel=", msg.velocity)
led.fill(0x00000)
# what about changing release_time based on noteoff velocity?
# yes that's possible, but some keyboards (like Arturia Keystep),
# send fixed noteoff velocity, so we fake it by using noteon velocity
# release_time = map_range( msg.velocity, 1,127, 3, 0.2)
# amp_env = synthio.Envelope(attack_time=amp_env.attack_time,
# decay_time=amp_env.decay_time,
# release_time=release_time,
# attack_level=amp_env.attac`k_level,
# sustain_level=amp_env.sustain_level)
# synth.envelope = amp_env
synth.release( (msg.note,) )
elif isinstance(msg,ControlChange):
print("controlChange", msg.control, "=", msg.value)
if msg.control == 1: # mod wheel
wave_mix = msg.value / 127
waveform[:] = lerp( wave_saw, wave_weird1, wave_mix )
# debug info
if time.monotonic() - last_debug_time > 0.5:
last_debug_time = time.monotonic()
print("wave_mix: %.2f" % wave_mix)
@todbot
Copy link
Author

todbot commented May 1, 2023

Here's a little demo of the above program, using a QTPY RP2040 and PCM5102 cheapie I2S DAC.

synthio_envelopetest2.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment