174 lines
6.3 KiB
Python
174 lines
6.3 KiB
Python
# 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)
|