Кнопочное пианино на Raspberry Pi Pico (5 кнопок + зуммер + запись мелодии)
Мини-пианино на пять клавиш, которое запоминает твою игру! Raspberry Pi Pico играет ДО, РЕ, МИ, ФА и СОЛЬ на пассивном зуммере по пяти кнопкам, и нота звучит, пока удерживается её клавиша. Удержи ДО секунду, сыграй мелодию и снова удержи ДО: Pico проиграет её, сокращая паузы между нотами длиннее 1,5 секунды до 1,5 секунды. Индикатор записи — встроенный светодиод платы, лишние детали не нужны. Ноты через PWM, подавление дребезга и неблокирующие задержки в одном проекте.

Видео
Схема

Исходный код
1# ─────────────────────────────────────────
2# BlueGrays · bluegrays.com
3# Кнопочное пианино на Raspberry Pi Pico (5 кнопок + зуммер + запись мелодии)
4# ─────────────────────────────────────────
5# Мини-пианино на пять кнопок: DO, RE, MI, FA, SOL. Нота звучит, пока её
6# клавиша нажата. Удерживай DO 1 с: начинается запись, и ноты сохраняются
7# со своим временем. Снова удерживай DO 1 с: запись останавливается, и
8# мелодия проигрывается в том же ритме.
9#
10# Пины:
11# GP7 -> Кнопка DO (другая ножка к GND)
12# GP8 -> Кнопка RE (другая ножка к GND)
13# GP9 -> Кнопка MI (другая ножка к GND)
14# GP10 -> Кнопка FA (другая ножка к GND)
15# GP11 -> Кнопка SOL (другая ножка к GND)
16# GP17 -> Зуммер (+), другая ножка к GND
17# LED -> Встроенный светодиод: запись / воспроизведение
18
19from machine import Pin, PWM
20import time
21
22# ============ НАСТРОЙКИ ============
23NOTE_PINS = (7, 8, 9, 10, 11)
24NOTE_NAMES = ("DO", "RE", "MI", "FA", "SOL")
25NOTE_HZ = (523, 587, 659, 698, 784) # C5 D5 E5 F5 G5 (Hz)
26VOLUME = 32768 # Заполнение PWM, 32768 = 50%
27
28REC_KEY = 0 # Клавиша записи: DO
29LONG_PRESS_MS = 1000 # Время удержания для записи
30DEBOUNCE_MS = 20 # Антидребезг кнопок
31MAX_NOTES = 64 # Макс. нот в записи
32MAX_GAP_MS = 1500 # Самая длинная тишина при воспроизведении
33PLAY_DELAY_MS = 500 # Пауза перед воспроизведением
34REC_BLINK_MS = 500 # Мигание светодиода во время записи
35LOOP_MS = 2 # Шаг главного цикла
36
37FREE, REC, PLAY = 0, 1, 2 # Режимы
38
39# ============ ЖЕЛЕЗО ============
40buzzer = PWM(Pin(17))
41buzzer.duty_u16(0)
42try:
43 led = Pin("LED", Pin.OUT) # встроенный светодиод
44except (TypeError, ValueError):
45 led = Pin(25, Pin.OUT) # Старая прошивка
46led.value(0)
47
48
49class Key:
50 # Кнопка с антидребезгом; update() возвращает +1 при нажатии, -1 при отпускании
51 def __init__(self, gpio):
52 self.pin = Pin(gpio, Pin.IN, Pin.PULL_UP)
53 self.raw = 1
54 self.stable = 1
55 self.changed = time.ticks_ms()
56 self.down_at = 0
57 self.used = False # Использовано командой
58
59 def update(self, now):
60 raw = self.pin.value()
61 if raw != self.raw:
62 self.raw = raw
63 self.changed = now
64 if raw != self.stable and time.ticks_diff(now, self.changed) >= DEBOUNCE_MS:
65 self.stable = raw
66 if raw == 0:
67 self.down_at = now
68 self.used = False
69 return 1
70 return -1
71 return 0
72
73 def held_ms(self, now):
74 return time.ticks_diff(now, self.down_at) if self.stable == 0 else 0
75
76
77keys = [Key(p) for p in NOTE_PINS]
78
79
80# ============ ЗВУК ============
81def tone(hz):
82 buzzer.freq(hz)
83 buzzer.duty_u16(VOLUME)
84
85
86def silence():
87 buzzer.duty_u16(0)
88
89
90def chirp(*steps):
91 # Короткий сигнал: пары (Hz, мс); 0 Hz = тишина
92 for hz, ms in steps:
93 if hz:
94 tone(hz)
95 else:
96 silence()
97 time.sleep_ms(ms)
98 silence()
99
100
101# ============ СОСТОЯНИЕ ============
102mode = FREE
103held = [] # Удерживаемые ноты, последняя в конце
104rec_notes = [] # Запись: [клавиша, начало в мс, длительность в мс]
105rec_t0 = 0
106play_list = [] # Воспроизведение: (время в мс, клавиша, длительность в мс)
107play_index = 0
108play_start = 0
109play_end = 0
110play_sounding = False
111
112
113def update_sound():
114 # Зуммер играет одну ноту: побеждает последняя нажатая клавиша
115 if held:
116 tone(NOTE_HZ[held[-1]])
117 else:
118 silence()
119
120
121def start_recording(now):
122 global mode, rec_notes, rec_t0
123 mode = REC
124 rec_notes = []
125 rec_t0 = now
126 chirp((880, 70), (0, 40), (1319, 110)) # Восходящий сигнал
127 print("Kayit basladi / Recording started (en fazla / max %d nota / notes)" % MAX_NOTES)
128
129
130def build_play_list():
131 # Отсортировать ноты по началу, сдвинуть первую на 0, укоротить длинные паузы
132 notes = sorted(rec_notes, key=lambda n: n[1])
133 result = []
134 t = 0
135 for i, (k, start, length) in enumerate(notes):
136 if i > 0:
137 prev_k, prev_start, prev_len = notes[i - 1]
138 t += min(start - prev_start, prev_len + MAX_GAP_MS)
139 result.append((t, k, length))
140 return result
141
142
143def stop_recording(now):
144 global mode, play_list, play_index, play_start, play_end, play_sounding
145 led.value(0)
146 chirp((1319, 70), (0, 40), (880, 110)) # Нисходящий сигнал
147 if not rec_notes:
148 mode = FREE
149 print("Kayit bos / Recording is empty")
150 chirp((220, 300))
151 return
152 play_list = build_play_list()
153 names = " ".join(NOTE_NAMES[n[1]] for n in play_list)
154 print("Kayit bitti / Recording stopped: %d nota / notes" % len(play_list))
155 print("Melodi / Melody:", names)
156 mode = PLAY
157 play_index = 0
158 play_end = 0
159 play_sounding = False
160 play_start = time.ticks_add(time.ticks_ms(), PLAY_DELAY_MS)
161 print("Geri calma / Playback...")
162
163
164def finish_playback(message):
165 global mode
166 mode = FREE
167 silence()
168 led.value(0)
169 print(message)
170
171
172def update_playback(now):
173 # Неблокирующее воспроизведение: запускать ноты вовремя, глушить их по окончании
174 global play_index, play_end, play_sounding
175 elapsed = time.ticks_diff(now, play_start)
176 if elapsed < 0:
177 return
178 while play_index < len(play_list) and elapsed >= play_list[play_index][0]:
179 t, k, length = play_list[play_index]
180 tone(NOTE_HZ[k])
181 led.value(1) # Светодиод горит вместе с нотой
182 play_end = t + length
183 play_sounding = True
184 play_index += 1
185 if play_sounding and elapsed >= play_end:
186 silence()
187 led.value(0)
188 play_sounding = False
189 if not play_sounding and play_index >= len(play_list):
190 finish_playback("Bitti / Done. DO 1 sn = yeni kayit / DO 1 s = new recording")
191
192
193def on_press(k, now):
194 if mode == PLAY:
195 # Любая клавиша останавливает воспроизведение
196 keys[k].used = True
197 finish_playback("Durduruldu / Stopped")
198 return
199 held.append(k)
200 update_sound()
201 tag = "[REC] " if mode == REC else ""
202 print("%sNota / Note: %s (%d Hz)" % (tag, NOTE_NAMES[k], NOTE_HZ[k]))
203
204
205def on_release(k, now):
206 if k in held:
207 held.remove(k)
208 update_sound()
209 if keys[k].used or mode != REC:
210 return
211 start = time.ticks_diff(keys[k].down_at, rec_t0)
212 length = time.ticks_diff(now, keys[k].down_at)
213 rec_notes.append([k, start, length])
214 if len(rec_notes) >= MAX_NOTES:
215 print("Kayit dolu / Recording full")
216 stop_recording(now)
217
218
219def on_long_press(k, now):
220 # Долгое нажатие на DO: начать или остановить запись. Удерживаемая нота DO не записывается.
221 keys[k].used = True
222 if k in held:
223 held.remove(k)
224 update_sound()
225 if mode == FREE:
226 start_recording(now)
227 elif mode == REC:
228 stop_recording(now)
229
230
231# ============ ГЛАВНЫЙ ЦИКЛ ============
232print("== BlueGrays · bluegrays.com ==")
233print("Pico Buton Piyano / Button Piano")
234print("Tuslar / Keys: DO RE MI FA SOL")
235print("DO 1 sn basili = kayit baslat / bitir + geri cal")
236print("Hold DO 1 s = start / stop recording + play back")
237chirp((NOTE_HZ[0], 90), (NOTE_HZ[2], 90), (NOTE_HZ[4], 90), (1047, 180)) # Запуск
238
239while True:
240 now = time.ticks_ms()
241 for i, key in enumerate(keys):
242 event = key.update(now)
243 if event == 1:
244 on_press(i, now)
245 elif event == -1:
246 on_release(i, now)
247
248 rec_key = keys[REC_KEY]
249 if (mode != PLAY and not rec_key.used
250 and rec_key.held_ms(now) >= LONG_PRESS_MS):
251 on_long_press(REC_KEY, now)
252
253 if mode == PLAY:
254 update_playback(now)
255 elif mode == REC:
256 # Во время записи светодиод мигает
257 led.value(1 if (time.ticks_diff(now, rec_t0) // REC_BLINK_MS) % 2 == 0 else 0)
258
259 time.sleep_ms(LOOP_MS)
260Частые вопросы
Как записать мелодию и проиграть её?
Удержи ДО одну секунду: два восходящих сигнала означают, что запись началась. Сыграй мелодию, затем снова удержи ДО одну секунду. После двух нисходящих сигналов мелодия проиграется; паузы между нотами длиннее 1,5 секунды сокращаются до 1,5 секунды. Нажатие любой клавиши во время воспроизведения останавливает его.
Нужно ли добавлять отдельный светодиод для индикатора записи?
Нет. Код управляет светодиодом на плате через `Pin("LED")`; на Pico это GP25, и так же работает на Pico W. Во время записи он мигает каждые полсекунды, а при воспроизведении загорается с каждой нотой.
Как изменить ноты или октаву?
Частоты хранятся в `NOTE_HZ`: 523, 587, 659, 698 и 784 Hz. Раздели их все пополам, чтобы опуститься на октаву, или удвой, чтобы подняться. Чтобы добавить шестую клавишу, допиши новый пин, название и частоту (880 для ЛЯ) в `NOTE_PINS`, `NOTE_NAMES` и `NOTE_HZ`.
Как играть на пианино в Wokwi?
Запусти симуляцию и один раз щёлкни по схеме. Клавиши с 1 по 5 на клавиатуре играют от ДО до СОЛЬ, а удержание клавиши делает ноту длиннее. Для записи удерживай 1 около секунды; сыгранные ноты и записанная мелодия появляются в мониторе порта.
