|
| 1 | +# SPDX-FileCopyrightText: 2023 John Park, Jeff Epler, and Tod Kurt for Adafruit Industries |
| 2 | +# SPDX-License-Identifier: MIT |
| 3 | +# Computer Perfection Synth |
| 4 | +# * 10 numbered buttons play notes |
| 5 | +# * SET button to increase LFO rate, long press to decrease LFO rate |
| 6 | +# * SCORE button to add lower octave |
| 7 | +# * MODE switch changes LFO depth? |
| 8 | +# * SKILL switch toggles sustain |
| 9 | +# * GAME switch must stay in position 1 or it messes with the other switches |
| 10 | + |
| 11 | +import time |
| 12 | +import random |
| 13 | +import board |
| 14 | +import audiobusio |
| 15 | +import audiomixer |
| 16 | +import synthio |
| 17 | +import ulab.numpy as np |
| 18 | +import neopixel |
| 19 | +import keypad |
| 20 | + |
| 21 | + |
| 22 | +# NeoPixel setup |
| 23 | +num_pixels = 34 |
| 24 | +pixels = neopixel.NeoPixel(board.D11, num_pixels, brightness=0.7, auto_write=False) |
| 25 | +pixels.fill(0x0) |
| 26 | +pixels.show() |
| 27 | +time.sleep(0.25) |
| 28 | +pix_map = [26, 23, 19, 16, 13, 10, 7, 4, 32, 29] # map the LEDs to the numbered panel sections 0-9 |
| 29 | +for p in range(len(pix_map)): |
| 30 | + pixels[pix_map[p]] = 0xff0000 |
| 31 | + pixels.show() |
| 32 | + time.sleep(0.1) |
| 33 | + |
| 34 | + |
| 35 | +note_buttons = keypad.Keys( |
| 36 | + (board.D0, board.D1, board.D2, board.D3, board.D4, |
| 37 | + board.D5, board.D6, board.D7, board.D8, board.A5), |
| 38 | + value_when_pressed=False, |
| 39 | + pull=True |
| 40 | +) |
| 41 | +switches = keypad.Keys( |
| 42 | + (board.A1, board.A0), |
| 43 | + value_when_pressed=False, |
| 44 | + pull=True |
| 45 | +) |
| 46 | +octave = 3 # octave multiplier |
| 47 | +note_list = (0, 4, 6, 7, 9, 12, 16, 18, 19, 21) # Lydian scale |
| 48 | + |
| 49 | +mod_buttons = keypad.Keys( |
| 50 | + (board.A4, board.A3), # SET and SCORE buttons |
| 51 | + value_when_pressed=False, |
| 52 | + pull=True |
| 53 | +) |
| 54 | + |
| 55 | +SAMPLE_RATE = 48000 # clicks @ 36kHz & 48kHz on rp2040 |
| 56 | +SAMPLE_SIZE = 200 |
| 57 | +VOLUME = 12000 |
| 58 | + |
| 59 | +# Metro M7 pins for the I2S amp: |
| 60 | +lck_pin, bck_pin, dat_pin = board.D9, board.D10, board.D12 |
| 61 | + |
| 62 | +# synth engine setup |
| 63 | +waveform = np.zeros(SAMPLE_SIZE, dtype=np.int16) # intially all zeros (silence) |
| 64 | + |
| 65 | +amp_env = synthio.Envelope( # default (0.1, 0.05, 0.2, 1, 0.8) |
| 66 | + attack_time=1.0, |
| 67 | + decay_time=0.05, |
| 68 | + release_time=3.0, |
| 69 | + attack_level=1.0, |
| 70 | + sustain_level=0.8 |
| 71 | +) |
| 72 | + |
| 73 | +synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE, waveform=waveform, envelope=amp_env) |
| 74 | +audio = audiobusio.I2SOut(bit_clock=bck_pin, word_select=lck_pin, data=dat_pin) |
| 75 | +mixer = audiomixer.Mixer(voice_count=1, sample_rate=SAMPLE_RATE, channel_count=1, |
| 76 | + bits_per_sample=16, samples_signed=True, buffer_size=8192) |
| 77 | +audio.play(mixer) |
| 78 | +mixer.voice[0].level = 0.55 |
| 79 | +mixer.voice[0].play(synth) |
| 80 | + |
| 81 | +led = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.3) # on board neopixel |
| 82 | + |
| 83 | +# waveforms setup |
| 84 | +wave_sine = np.array(np.sin(np.linspace(0, 2*np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME, |
| 85 | + dtype=np.int16) |
| 86 | +wave_saw = np.linspace(VOLUME, -VOLUME, num=SAMPLE_SIZE, dtype=np.int16) |
| 87 | +wave_weird1 = np.array((198,2776,5441,8031,10454,12653,14609,16333,17824,19130,20260,21227,22043, |
| 88 | + 22721,23269,23699,24019,24243,24385,24461,18630,-26956,-28048,-29175,-30249, |
| 89 | + -31227,-32073,-32631,-32359,-31817,-30941,-29663,-27900,-25596,-22591, |
| 90 | + -18834,-14291,-9016,-3212,2794,8624,13943,18544,22353,25408,27780,29553, |
| 91 | + 30855,31751,32315,32611,32687,32593,32351,31983,31491,30871,30097,28895, |
| 92 | + -28240,-30489,-31343,-31975,-32431,-32697,-32767,-32615,-32217,-31525, |
| 93 | + -30489,-29035,-27090,-24519,-21237,-17178,-12339,-6829,-902,5081,10748, |
| 94 | + 15805,20102,23615,26396,28510,30109,31245,31995,31955,31437,30729,29887, |
| 95 | + 28943,27908,26784,25560,24077,22781,-22207,-22735,-22709,-22471,-22065, |
| 96 | + -21497,-20773,-19896,-18872,-17698,-16361,-14857,-13141,-11206,-9054,-6717, |
| 97 | + -4259,-1796,522,2548,4167,5339,6079,6445,6503,6319,5949,5449,4847,4183, |
| 98 | + 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, |
| 99 | + 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, |
| 100 | + -3336,-4074,-4795,-5487,-6119,-6669,-7095,-7357,-7399,-7157,-6559,-5543, |
| 101 | + -4076,-2132,), dtype=np.int16) |
| 102 | +wave_noise = np.array([random.randint(-VOLUME, VOLUME) for i in range(SAMPLE_SIZE)], dtype=np.int16) |
| 103 | + |
| 104 | +# map s range a1-a2 to b1-b2 |
| 105 | +def map_range(s, a1, a2, b1, b2): |
| 106 | + return b1 + ((s - a1) * (b2 - b1) / (a2 - a1)) |
| 107 | + |
| 108 | +# mix between values a and b, works with numpy arrays too, t ranges 0-1 |
| 109 | +def lerp(a, b, t): |
| 110 | + return (1-t)*a + t*b |
| 111 | + |
| 112 | +waveform[:] = wave_saw |
| 113 | +wave_mix = 0.0 |
| 114 | +lfo_rates = (0.1, 0.5, 0.8, 1.5, 3.0, 6.0, 7.0, 8.0) |
| 115 | +lfo_index = 0 |
| 116 | +lfo1 = synthio.LFO(rate=(lfo_rates[lfo_index]), waveform=wave_sine) # rate is in Hz |
| 117 | +synth.lfos.append(lfo1) |
| 118 | +hold = False # state of note hold |
| 119 | +octaves = False |
| 120 | + |
| 121 | +def light_button_pixels(button_number): |
| 122 | + pixels[pix_map[button_number]+1] = 0xFF0000 |
| 123 | + pixels[pix_map[button_number]-1] = 0xFF0000 |
| 124 | + pixels.show() |
| 125 | + |
| 126 | +def reset_button_pixels(button_number): |
| 127 | + pixels[pix_map[button_number]+1] = 0x000000 |
| 128 | + pixels[pix_map[button_number]-1] = 0x000000 |
| 129 | + pixels.show() |
| 130 | + |
| 131 | +def clamp(v, low, high): |
| 132 | + return min(max(v, low), high) |
| 133 | + |
| 134 | +print("-Computer Perfection Synth-") |
| 135 | + |
| 136 | +note = None |
| 137 | +mod_key = 0 |
| 138 | +last_mod_button_event_time = 0 |
| 139 | +waveset = 0 |
| 140 | + |
| 141 | + |
| 142 | +while True: |
| 143 | + # watch for mod buttons to be pressed |
| 144 | + mod_button_event = mod_buttons.events.get() |
| 145 | + if mod_button_event: |
| 146 | + mod_key = mod_button_event.key_number |
| 147 | + if mod_button_event.pressed: |
| 148 | + if mod_key == 0: # SET switch |
| 149 | + last_mod_button_event_time = time.monotonic() |
| 150 | + |
| 151 | + if mod_key == 1: # enable octaves |
| 152 | + octaves = True |
| 153 | + |
| 154 | + if mod_button_event.released: |
| 155 | + if last_mod_button_event_time and mod_key == 0: # short press-release increase LFO rate |
| 156 | + lfo_index = clamp(lfo_index+1, 0, len(lfo_rates)-1) |
| 157 | + print(lfo_index) |
| 158 | + lfo_rate = lfo_rates[lfo_index] |
| 159 | + lfo1.rate = lfo_rate |
| 160 | + last_mod_button_event_time = 0 |
| 161 | + if mod_key == 1: # disable octaves |
| 162 | + octaves = False |
| 163 | + # long press slows the LFO rate |
| 164 | + if last_mod_button_event_time != 0 and time.monotonic() - last_mod_button_event_time > 1.0: |
| 165 | + last_mod_button_event_time = 0 |
| 166 | + lfo_index = clamp(lfo_index-1, 0, len(lfo_rates)-1) |
| 167 | + lfo_rate = lfo_rates[lfo_index] |
| 168 | + lfo1.rate = lfo_rate |
| 169 | + |
| 170 | + # watch for note buttons to be pressed |
| 171 | + note_button_event = note_buttons.events.get() |
| 172 | + if note_button_event: |
| 173 | + i = note_button_event.key_number |
| 174 | + if note_button_event.pressed: |
| 175 | + if octaves: |
| 176 | + synth.press((note_list[i]+(octave*12), note_list[i]+(octave*12)-12)) |
| 177 | + else: |
| 178 | + synth.press((note_list[i]+(octave*12),)) |
| 179 | + light_button_pixels(i) |
| 180 | + if note_button_event.released: |
| 181 | + if not hold: |
| 182 | + reset_button_pixels(i) |
| 183 | + synth.release((note_list[i]+(octave*12), note_list[i]+(octave*12)-12)) |
| 184 | + reset_button_pixels(i) |
| 185 | + |
| 186 | + # watch for switches to be changed |
| 187 | + switch_event = switches.events.get() |
| 188 | + if switch_event: |
| 189 | + sw = switch_event.key_number |
| 190 | + if switch_event.pressed: |
| 191 | + if sw == 0: # MODE toggle right |
| 192 | + mixer.voice[0].level = 0.45 |
| 193 | + # wave_mix = 0.5 |
| 194 | + waveset = 0 |
| 195 | + if sw == 1: # SKILL toggle center |
| 196 | + hold = True |
| 197 | + |
| 198 | + if switch_event.released: |
| 199 | + if sw == 0: # MODE toggle center |
| 200 | + mixer.voice[0].level = 0.95 |
| 201 | + waveset = 1 |
| 202 | + if sw == 1: # SKILL toggle right or left |
| 203 | + hold = False |
| 204 | + for r in range(len(note_list)): # turn off all notes |
| 205 | + # if octaves: |
| 206 | + synth.release((note_list[r]+(octave*12), note_list[r]+(octave*12)-12)) |
| 207 | + for h in range(len(pix_map)): # turn off held pixels |
| 208 | + reset_button_pixels(h) |
| 209 | + |
| 210 | + lfo_val_for_lerp = map_range(lfo1.value, -1, 1, 0, 1) |
| 211 | + if waveset == 0: |
| 212 | + waveform[:] = lerp(wave_sine, wave_weird1, lfo_val_for_lerp) |
| 213 | + else: |
| 214 | + waveform[:] = lerp(wave_saw, wave_noise, lfo_val_for_lerp) |
0 commit comments