├── play.fzz ├── requirements.txt ├── README.md └── play.py /play.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqez/play21323/HEAD/play.fzz -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gpiozero==1.5.1 2 | pygame==1.9.6 3 | RPi.GPIO==0.7.0 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | play21323 2 | ========= 3 | 4 | Make your Lego Grand Piano (21323) as a playable piano. 5 | 6 | [![YouTube Video](http://img.youtube.com/vi/ES2W4QlB7xM/0.jpg)](https://youtu.be/ES2W4QlB7xM?t=0s) 7 | -------------------------------------------------------------------------------- /play.py: -------------------------------------------------------------------------------- 1 | from gpiozero import DigitalOutputDevice, DigitalInputDevice 2 | from gpiozero import Button, LED 3 | from time import sleep 4 | import pygame.mixer 5 | from pygame.mixer import Sound 6 | from enum import Enum 7 | 8 | 9 | PRESS_DELAY = 8 10 | PRESS_THRESHOLD = 2 11 | 12 | class STATE(Enum): 13 | NORMAL = 0 14 | RECORDING = 1 15 | PLAYING = 2 16 | 17 | state = STATE.NORMAL 18 | tick = 0 19 | note = 0 20 | record = [] 21 | 22 | # low buffer for shorter latency 23 | pygame.mixer.init(44100, -8, 1, 256) 24 | 25 | # Mux selector 26 | signals = [ 27 | LED('BOARD15'), 28 | LED('BOARD13'), 29 | LED('BOARD11'), 30 | ] 31 | 32 | # Muxes are connected to each touch sensor 33 | muxes = [ 34 | Button('BOARD29', pull_up=False), 35 | Button('BOARD31', pull_up=False), 36 | Button('BOARD33', pull_up=False), 37 | Button('BOARD35', pull_up=False), 38 | ] 39 | 40 | buttons = { 41 | "play": Button('BOARD38', pull_up=False), 42 | "sound": Button('BOARD36', pull_up=False), 43 | "record": Button('BOARD32', pull_up=False), 44 | } 45 | 46 | leds = { 47 | "playing": LED('BOARD37'), 48 | "recording": LED('BOARD40'), 49 | } 50 | 51 | voices = { 52 | "piano": Sound("assets/piano.wav"), 53 | "vibraphone": Sound("assets/vibraphone.wav"), 54 | "organ": Sound("assets/organ.wav"), 55 | "rec_start": Sound("assets/rec_start.wav"), 56 | "rec_end": Sound("assets/rec_end.wav"), 57 | } 58 | 59 | instruments = ["piano", "vibraphone", "organ"] 60 | ins_current = 0 61 | 62 | notemap = { 63 | "": None, 64 | 65 | "C2": "c2", "C#2": "c2s", "D2": "d2", "D#2": "d2s", "E2": "e2", 66 | "F2": "f2", "F#2": "f2s", "G2": "g2", "G#2": "g2s", "A2": "a2", "A#2": "a2s", "B2": "b2", 67 | 68 | "C3": "c3", "C#3": "c3s", "D3": "d3", "D#3": "d3s", "E3": "e3", 69 | "F3": "f3", "F#3": "f3s", "G3": "g3", "G#3": "g3s", "A3": "a3", "A#3": "a3s", "B3": "b3", 70 | 71 | "C4": "c4", 72 | } 73 | 74 | muxmap = [ 75 | # 0 1 2 3 4 5 6 7 76 | "F2", "C2", "G2", "C#2", "F#2", "D#2", "E2", "D2", # MUX 0 77 | "", "G#2", "C#3", "A2", "C3", "B2", "", "A#2", # MUX 1 78 | "F#3", "D3", "G#3", "D#3", "G3", "", "F3", "E3", # MUX 2 79 | "", "A3", "", "A#3", "", "C4", "", "B3", # MUX 2 80 | ] 81 | 82 | print("Loading sounds...") 83 | 84 | sounds = { 85 | ins: {k: Sound(f"assets/{ins}/{v}.wav") if v else None for (k, v) in notemap.items()} 86 | for ins in instruments 87 | } 88 | 89 | 90 | def get_instrument(ins): 91 | voices[instruments[ins]].play() 92 | return [sounds[instruments[ins]][_] for _ in muxmap] 93 | 94 | def reset(): 95 | # turn off leds 96 | [led.off() for (name, led) in leds.items()] 97 | 98 | # reset mux signal 99 | for s in range(8): 100 | b = '{0:03b}'.format(s) 101 | for i in range(3): 102 | if b[i] == '1': 103 | signals[i].on() 104 | else: 105 | signals[i].off() 106 | 107 | for i in range(4): 108 | if muxes[i].is_pressed: 109 | print("mux", i, "pressed at", b) 110 | 111 | def btn_sound_pressed(): 112 | global sndmuxmap, ins_current 113 | ins_current = (ins_current + 1) % len(instruments) 114 | print("Change sound to", instruments[ins_current]) 115 | sndmuxmap = get_instrument(ins_current) 116 | 117 | def btn_play_pressed(): 118 | if state == STATE.PLAYING: 119 | stop_playing() 120 | else: 121 | start_playing() 122 | 123 | def btn_record_pressed(): 124 | if state == STATE.RECORDING: 125 | stop_recording() 126 | else: 127 | start_recording() 128 | 129 | def start_recording(): 130 | global state 131 | if state == STATE.PLAYING: 132 | stop_playing() 133 | 134 | voices["rec_start"].play() 135 | print("> start recording") 136 | state = STATE.RECORDING 137 | leds['recording'].on() 138 | 139 | global record, tick 140 | record = [] 141 | tick = 0 142 | 143 | def stop_recording(): 144 | global state 145 | voices["rec_end"].play() 146 | print("> stop recording") 147 | if len(record) > 0: 148 | print(">", len(record), "notes were recorded") 149 | state = STATE.NORMAL 150 | leds['recording'].off() 151 | 152 | def start_playing(): 153 | global state 154 | if state == STATE.RECORDING: 155 | stop_recording() 156 | 157 | print("> start playing") 158 | state = STATE.PLAYING 159 | leds['playing'].on() 160 | 161 | global tick, note 162 | tick = note = 0 163 | # move to the first note tick with a bit delay 164 | if len(record) > 0: 165 | tick = record[0][0] - 24 166 | 167 | def stop_playing(): 168 | global state, vs 169 | print("> stop playing") 170 | state = STATE.NORMAL 171 | leds['playing'].off() 172 | vs = [0] * 32 173 | 174 | 175 | # Prepare 176 | sndmuxmap = get_instrument(ins_current) 177 | reset() 178 | buttons["sound"].when_pressed = btn_sound_pressed 179 | buttons["play"].when_pressed = btn_play_pressed 180 | buttons["record"].when_pressed = btn_record_pressed 181 | 182 | print("Piano is ready.") 183 | 184 | # Main loop 185 | bs = [0] * 32 186 | vs = [0] * 32 187 | 188 | while True: 189 | on = [] 190 | off = [] 191 | 192 | # piano keys 193 | for s in range(8): 194 | b = '{0:03b}'.format(s) 195 | for i in range(3): 196 | if b[i] == '1': 197 | signals[i].on() 198 | else: 199 | signals[i].off() 200 | 201 | for i in range(4): 202 | idx = i * 8 + s 203 | sound = sndmuxmap[idx] 204 | 205 | if not sound: 206 | continue 207 | 208 | if muxes[i].is_pressed or vs[idx] == 1: 209 | if bs[idx] < PRESS_THRESHOLD: 210 | bs[idx] = PRESS_DELAY 211 | sound.stop() 212 | sound.play() 213 | on.append(idx) 214 | else: 215 | if bs[idx] > 0: 216 | bs[idx] -= 1 217 | if bs[idx] == 0: 218 | sound.fadeout(250) # stop() sounds weird, so fading them out... 219 | off.append(idx) 220 | 221 | print(tick, bs, end='\r') 222 | 223 | if state == STATE.RECORDING: 224 | if len(on) + len(off) > 0: 225 | record.append([tick, on, off]) 226 | tick += 1 227 | elif state == STATE.PLAYING: 228 | if len(record) <= note: 229 | stop_playing() 230 | else: 231 | if record[note][0] <= tick: 232 | # on 233 | for idx in record[note][1]: 234 | vs[idx] = 1 235 | # off 236 | for idx in record[note][2]: 237 | vs[idx] = 0 238 | note += 1 239 | tick += 1 240 | --------------------------------------------------------------------------------