Skip to content

Instantly share code, notes, and snippets.

@greydoubt
Forked from todbot/synthio_midi_synth.py
Created May 12, 2023 23:24
Show Gist options
  • Select an option

  • Save greydoubt/f94a8bb5a3f7bf87ba23a9a4b03bfea6 to your computer and use it in GitHub Desktop.

Select an option

Save greydoubt/f94a8bb5a3f7bf87ba23a9a4b03bfea6 to your computer and use it in GitHub Desktop.

Revisions

  1. @todbot todbot revised this gist May 12, 2023. 1 changed file with 13 additions and 9 deletions.
    22 changes: 13 additions & 9 deletions synthio_midi_synth.py
    Original file line number Diff line number Diff line change
    @@ -1,13 +1,13 @@
    # synthio_midi_synth.py - pretty usable MIDI-controlled synth using synthio in CircuitPython
    # 11 May 2023 - @todbot / Tod Kurt
    # Uses cheapie PCM5102 DAC on QTPY RP2040
    # Features:
    # Features:
    # - midi velocity controls attack rate (gentle press = slow, hard press = fast)
    # - notes have small random detune on all oscillators to reduce phase stacking
    # - adjustable number of detuned oscillators per note 1-5 (midi controller 83)
    # - five selectable waveforms: saw, squ, sin, noisy sin, noise (midi controller 82)
    # - vibrato depth on mod wheel (midi controller 1)
    #
    # - five selectable waveforms: saw, squ, sin, noisy sin, noise (midi controller 82)
    # - vibrato depth on mod wheel (midi controller 1)
    #
    import time,random
    import board, analogio
    import audiobusio, audiomixer
    @@ -41,7 +41,7 @@
    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 ) # buffer_size=4096 )
    audio.play(mixer) # attach mixer to DAC
    audio.play(mixer) # attach mixer to DAC
    mixer.voice[0].play(synth) # start synth engine playing

    wave_i = 0 # which waveform to play
    @@ -50,6 +50,7 @@
    osc_detune = 0.01 # how much detune (fatness)

    notes_pressed = {} # which notes are currently being pressed, and their note objects (so we can unpress them)
    mod_val = 0 # ranges 0-1

    def note_on(notenum, vel):
    at_time = max(0, 2 * (127-(vel*1.2)) / 127) # velocity controls attack time
    @@ -63,7 +64,8 @@ def note_on(notenum, vel):
    fr = f * (1 + (osc_detune*i) + (random.random()/1000) )
    print("fr:",fr)
    notes.append( synthio.Note( frequency=fr, envelope=amp_env, waveform=waveform,
    vibrato_depth=0.0, vibrato_rate=3 ) )
    bend_mode=synthio.BendMode.VIBRATO,
    bend_depth = 0.5 * mod_val, bend_rate = 20 * mod_val ) )
    notes_pressed[notenum] = notes
    synth.press(notes)

    @@ -91,13 +93,15 @@ def note_off(notenum,vel):
    elif isinstance(msg,ControlChange):
    print("controlChange", msg.control, "=", msg.value)
    if msg.control == 1: # mod wheel
    mod_val = msg.value / 127
    for notes in notes_pressed.values():
    for n in notes:
    #n.vibrato_rate = 20 * (msg.value/127) # this perhaps does not work?
    n.vibrato_depth = (msg.value/127)
    for n in notes: # adjust vibrato depth & rate for all notes
    n.bend_depth = 0.5 * mod_val
    n.bend_rate = 20 * mod_val
    elif msg.control == 82: # leftmost slider on minilab3
    num_oscs = int( 1 + (msg.value/127) * max_oscs )
    print("num_oscs:",num_oscs)
    elif msg.control == 83: # leftmost+1 slider on minilab3
    wave_i = int( (msg.value/127) * (len(waveforms)-1) )
    print("wave_i:",wave_i)

  2. @todbot todbot revised this gist May 12, 2023. 1 changed file with 4 additions and 2 deletions.
    6 changes: 4 additions & 2 deletions synthio_midi_synth.py
    Original file line number Diff line number Diff line change
    @@ -2,8 +2,10 @@
    # 11 May 2023 - @todbot / Tod Kurt
    # Uses cheapie PCM5102 DAC on QTPY RP2040
    # Features:
    # - adjustable number of oscillators per note 1-5 (midi controller 83)
    # - three selectable waveforms: saw, squ, sin (midi controller 82)
    # - midi velocity controls attack rate (gentle press = slow, hard press = fast)
    # - notes have small random detune on all oscillators to reduce phase stacking
    # - adjustable number of detuned oscillators per note 1-5 (midi controller 83)
    # - five selectable waveforms: saw, squ, sin, noisy sin, noise (midi controller 82)
    # - vibrato depth on mod wheel (midi controller 1)
    #
    import time,random
  3. @todbot todbot revised this gist May 12, 2023. 1 changed file with 6 additions and 1 deletion.
    7 changes: 6 additions & 1 deletion synthio_midi_synth.py
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,11 @@
    # synthio_midi_synth.py
    # synthio_midi_synth.py - pretty usable MIDI-controlled synth using synthio in CircuitPython
    # 11 May 2023 - @todbot / Tod Kurt
    # Uses cheapie PCM5102 DAC on QTPY RP2040
    # Features:
    # - adjustable number of oscillators per note 1-5 (midi controller 83)
    # - three selectable waveforms: saw, squ, sin (midi controller 82)
    # - vibrato depth on mod wheel (midi controller 1)
    #
    import time,random
    import board, analogio
    import audiobusio, audiomixer
  4. @todbot todbot revised this gist May 12, 2023. 1 changed file with 6 additions and 9 deletions.
    15 changes: 6 additions & 9 deletions synthio_midi_synth.py
    Original file line number Diff line number Diff line change
    @@ -1,11 +1,6 @@
    # synthio_midi_synth.py -- pretty usable MIDI-controlled synth using synthio
    # synthio_midi_synth.py
    # 11 May 2023 - @todbot / Tod Kurt
    # Uses cheapie PCM5102 DAC on QTPY RP2040
    # Features:
    # - adjustable number of oscillators per note 1-5 (midi controller 83)
    # - three selectable waveforms: saw, squ, sin (midi controller 82)
    # - vibrato depth on mod wheel (midi controller 1)
    #
    import time,random
    import board, analogio
    import audiobusio, audiomixer
    @@ -31,7 +26,9 @@
    wave_saw = np.linspace(VOLUME, -VOLUME, num=SAMPLE_SIZE, dtype=np.int16)
    wave_squ = np.concatenate((np.ones(SAMPLE_SIZE//2, dtype=np.int16)*VOLUME,np.ones(SAMPLE_SIZE//2, dtype=np.int16)*-VOLUME))
    wave_sin = np.array(np.sin(np.linspace(0, 4*np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME, dtype=np.int16)
    waveforms = (wave_saw, wave_squ, wave_sin)
    wave_noise = np.array([random.randint(-VOLUME, VOLUME) for i in range(SAMPLE_SIZE)], dtype=np.int16)
    wave_sin_dirty = np.array( wave_sin + (wave_noise/4), dtype=np.int16)
    waveforms = (wave_saw, wave_squ, wave_sin, wave_sin_dirty, wave_noise)

    synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE) # note no envelope or waveform, we do that in Note now!
    audio = audiobusio.I2SOut(bit_clock=bck_pin, word_select=lck_pin, data=dat_pin)
    @@ -48,7 +45,8 @@
    notes_pressed = {} # which notes are currently being pressed, and their note objects (so we can unpress them)

    def note_on(notenum, vel):
    amp_env = synthio.Envelope(attack_time=0.1, decay_time=0.05, release_time=0.8,
    at_time = max(0, 2 * (127-(vel*1.2)) / 127) # velocity controls attack time
    amp_env = synthio.Envelope(attack_time=at_time, decay_time=0.05, release_time=0.8,
    attack_level=1, sustain_level=0.8)
    waveform = waveforms[wave_i]
    notes = []
    @@ -96,4 +94,3 @@ def note_off(notenum,vel):
    elif msg.control == 83: # leftmost+1 slider on minilab3
    wave_i = int( (msg.value/127) * (len(waveforms)-1) )
    print("wave_i:",wave_i)

  5. @todbot todbot created this gist May 12, 2023.
    99 changes: 99 additions & 0 deletions synthio_midi_synth.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,99 @@
    # synthio_midi_synth.py -- pretty usable MIDI-controlled synth using synthio
    # 11 May 2023 - @todbot / Tod Kurt
    # Uses cheapie PCM5102 DAC on QTPY RP2040
    # Features:
    # - adjustable number of oscillators per note 1-5 (midi controller 83)
    # - three selectable waveforms: saw, squ, sin (midi controller 82)
    # - vibrato depth on mod wheel (midi controller 1)
    #
    import time,random
    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

    led = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2)
    midi = adafruit_midi.MIDI(midi_in=usb_midi.ports[0], in_channel=0 )

    # qtpy rp2040 SPI pins, be sure PCM5102 SCK is tied to Gnd
    lck_pin, bck_pin, dat_pin = board.MISO, board.MOSI, board.SCK

    SAMPLE_RATE = 28000 # clicks @ 36kHz & 48kHz on rp2040
    SAMPLE_SIZE = 256 # we like powers of two
    VOLUME = 12000 # 16384 is max volume I think
    wave_saw = np.linspace(VOLUME, -VOLUME, num=SAMPLE_SIZE, dtype=np.int16)
    wave_squ = np.concatenate((np.ones(SAMPLE_SIZE//2, dtype=np.int16)*VOLUME,np.ones(SAMPLE_SIZE//2, dtype=np.int16)*-VOLUME))
    wave_sin = np.array(np.sin(np.linspace(0, 4*np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME, dtype=np.int16)
    waveforms = (wave_saw, wave_squ, wave_sin)

    synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE) # note no envelope or waveform, we do that in Note now!
    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 ) # buffer_size=4096 )
    audio.play(mixer) # attach mixer to DAC
    mixer.voice[0].play(synth) # start synth engine playing

    wave_i = 0 # which waveform to play
    num_oscs = 1 # how many oscillators per note
    max_oscs = 5
    osc_detune = 0.01 # how much detune (fatness)

    notes_pressed = {} # which notes are currently being pressed, and their note objects (so we can unpress them)

    def note_on(notenum, vel):
    amp_env = synthio.Envelope(attack_time=0.1, decay_time=0.05, release_time=0.8,
    attack_level=1, sustain_level=0.8)
    waveform = waveforms[wave_i]
    notes = []
    f = synthio.midi_to_hz(notenum)
    for i in range(num_oscs):
    # add detuning to oscillators + a bit of random so phases w/ other notes don't perfectly align
    fr = f * (1 + (osc_detune*i) + (random.random()/1000) )
    print("fr:",fr)
    notes.append( synthio.Note( frequency=fr, envelope=amp_env, waveform=waveform,
    vibrato_depth=0.0, vibrato_rate=3 ) )
    notes_pressed[notenum] = notes
    synth.press(notes)

    def note_off(notenum,vel):
    notes = notes_pressed.get(notenum, None)
    if notes:
    synth.release(notes)
    del notes_pressed[notenum]

    debug_notes = False

    print("synthio_midi_synth ready")
    while True:
    msg = midi.receive()
    if isinstance(msg, NoteOn) and msg.velocity != 0:
    print("noteOn: ", msg.note, "vel=", msg.velocity)
    led.fill(0xff00ff)
    note_on( msg.note, msg.velocity)
    if debug_notes: print("notes_pressed:", notes_pressed)
    elif isinstance(msg,NoteOff) or isinstance(msg,NoteOn) and msg.velocity==0:
    print("noteOff:", msg.note, "vel=", msg.velocity)
    led.fill(0x00000)
    note_off( msg.note, msg.velocity)
    if debug_notes: print("notes_pressed:", notes_pressed)
    elif isinstance(msg,ControlChange):
    print("controlChange", msg.control, "=", msg.value)
    if msg.control == 1: # mod wheel
    for notes in notes_pressed.values():
    for n in notes:
    #n.vibrato_rate = 20 * (msg.value/127) # this perhaps does not work?
    n.vibrato_depth = (msg.value/127)
    elif msg.control == 82: # leftmost slider on minilab3
    num_oscs = int( 1 + (msg.value/127) * max_oscs )
    print("num_oscs:",num_oscs)
    elif msg.control == 83: # leftmost+1 slider on minilab3
    wave_i = int( (msg.value/127) * (len(waveforms)-1) )
    print("wave_i:",wave_i)