Last active
July 13, 2023 07:01
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# 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) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here's a little demo of the above program, using a QTPY RP2040 and PCM5102 cheapie I2S DAC.
synthio_envelopetest2.mp4