Create a virtual piano using the Arduino UNO Q and play sounds via the Arduino Modulino Buzzer. Click the keys (or use your computer keyboard) and hear each note played by your board's piezo buzzer.
Devices and components
Modulino™ Ringtone
Arduino® UNO™ Q 2 GB
Software and tools
Arduino Applications Lab
Project description
Materials required
Arduino UNO Q: Runs sketch and Python application side by side
Modulino Buzzer: piezo buzzer connected via Qwiic / I2C cable
How it works
Sketch Arduino (sketch/sketch.ino): initializes the buzzer and exposes a play_note command that the Python application can call by name, transmitting a frequency in Hz.
Python application (python/main.py): receives play_note messages from the browser and transmits them to the sketch via the RPC Bridge.
Browser UI (ui/): Renders a piano keyboard. When a key is pressed, the frequency of the note is transmitted to the Python application and the key is briefly highlighted.
Features
Piano 2 octaves (C4 – B5)
Computer keyboard support
A S D F G H J White keys — C4 D4 E4 F4 G4 A4 B4
WE . T Y U Black keys — C#4 D#4 · F#4 G#4 A#4
KL; White keys — C5 D5 E5
O P Black keys — C#5 D#5
Visual feedback
Javascript file
1var socket = io();
2
3var statusEl = document.getElementById('status');
4
5socket.on('connect', function () { statusEl.className = 'status connected'; statusEl.textContent = '● Connected'; });
6socket.on('disconnect', function () { statusEl.className = 'status disconnected'; statusEl.textContent = '● Disconnected'; });
7socket.on('connect_error', function () { statusEl.className = 'status connecting'; statusEl.textContent = 'Connecting…'; });
8
9// ── Note definitions ──────────────────────────────────────
10// key: keyboard shortcut (lowercase), null = mouse only
11var NOTES = [
12 // Octave 4
13 { name: 'C4', freq: 262, key: 'a', type: 'white' },
14 { name: 'C#4', freq: 277, key: 'w', type: 'black' },
15 { name: 'D4', freq: 294, key: 's', type: 'white' },
16 { name: 'D#4', freq: 311, key: 'e', type: 'black' },
17 { name: 'E4', freq: 330, key: 'd', type: 'white' },
18 { name: 'F4', freq: 349, key: 'f', type: 'white' },
19 { name: 'F#4', freq: 370, key: 't', type: 'black' },
20 { name: 'G4', freq: 392, key: 'g', type: 'white' },
21 { name: 'G#4', freq: 415, key: 'y', type: 'black' },
22 { name: 'A4', freq: 440, key: 'h', type: 'white' },
23 { name: 'A#4', freq: 466, key: 'u', type: 'black' },
24 { name: 'B4', freq: 494, key: 'j', type: 'white' },
25 // Octave 5
26 { name: 'C5', freq: 523, key: 'k', type: 'white' },
27 { name: 'C#5', freq: 554, key: 'o', type: 'black' },
28 { name: 'D5', freq: 587, key: 'l', type: 'white' },
29 { name: 'D#5', freq: 622, key: 'p', type: 'black' },
30 { name: 'E5', freq: 659, key: ';', type: 'white' },
31 { name: 'F5', freq: 698, key: null, type: 'white' },
32 { name: 'F#5', freq: 740, key: null, type: 'black' },
33 { name: 'G5', freq: 784, key: null, type: 'white' },
34 { name: 'G#5', freq: 831, key: null, type: 'black' },
35 { name: 'A5', freq: 880, key: null, type: 'white' },
36 { name: 'A#5', freq: 932, key: null, type: 'black' },
37 { name: 'B5', freq: 988, key: null, type: 'white' },
38];
39
40// Build a shortcut → note lookup
41var keyMap = {};
42NOTES.forEach(function (note) {
43 if (note.key) keyMap[note.key] = note;
44});
45
46// ── Build piano DOM ────────────────────────────────────────
47var WHITE_KEY_W = 48; // must match --white-key-w in CSS
48var BLACK_KEY_W = 30; // must match --black-key-w in CSS
49
50var pianoEl = document.getElementById('piano');
51var keyElements = {}; // name → DOM element
52
53var whiteCount = 0;
54
55NOTES.forEach(function (note) {
56 var el = document.createElement('div');
57 el.dataset.freq = note.freq;
58 el.dataset.name = note.name;
59
60 var labelHtml = '<span class="note-label">' + note.name + '</span>';
61 var shortcutHtml = note.key
62 ? '<span class="key-shortcut">' + note.key.toUpperCase() + '</span>'
63 : '';
64
65 if (note.type === 'white') {
66 el.className = 'white-key';
67 el.innerHTML = labelHtml + shortcutHtml;
68 pianoEl.appendChild(el);
69 whiteCount++;
70 } else {
71 el.className = 'black-key';
72 el.innerHTML = labelHtml + shortcutHtml;
73 // Position: centered on the boundary between the two surrounding white keys
74 var leftPx = whiteCount * WHITE_KEY_W - BLACK_KEY_W / 2;
75 el.style.left = leftPx + 'px';
76 pianoEl.appendChild(el);
77 }
78
79 keyElements[note.name] = el;
80});
81
82// ── Note playback ──────────────────────────────────────────
83var HIGHLIGHT_MS = 300; // matches buzzer duration in sketch
84var highlightTimers = {};
85
86function playNote(note) {
87 socket.emit('play_note', { frequency: note.freq });
88
89 // Visual highlight
90 var el = keyElements[note.name];
91 if (!el) return;
92
93 el.classList.add('active');
94
95 clearTimeout(highlightTimers[note.name]);
96 highlightTimers[note.name] = setTimeout(function () {
97 el.classList.remove('active');
98 }, HIGHLIGHT_MS);
99}
100
101// ── Mouse / touch events ───────────────────────────────────
102pianoEl.addEventListener('mousedown', function (e) {
103 var el = e.target.closest('[data-freq]');
104 if (!el) return;
105 var note = NOTES.find(function (n) { return n.name === el.dataset.name; });
106 if (note) playNote(note);
107});
108
109// ── Keyboard events ────────────────────────────────────────
110var pressedKeys = {};
111
112document.addEventListener('keydown', function (e) {
113 if (e.repeat) return; // prevent key-repeat spam
114 var key = e.key === ';' ? ';' : e.key.toLowerCase();
115 if (pressedKeys[key]) return;
116 var note = keyMap[key];
117 if (!note) return;
118 pressedKeys[key] = true;
119 playNote(note);
120});
121
122document.addEventListener('keyup', function (e) {
123 var key = e.key === ';' ? ';' : e.key.toLowerCase();
124 delete pressedKeys[key];
125});
python
python file
1import json
2import os
3
4from arduino.app_utils import App, Bridge
5from arduino.app_bricks.web_ui import WebUI
6
7_ui_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "ui")
8ui = WebUI(assets_dir_path=_ui_dir)
9
10
11def _call_mcu(method, *args):
12 try:
13 return json.loads(Bridge.call(method, *args))
14 except Exception as exc:
15 print(f"[bridge] {method} error: {exc}")
16 return None
17
18
19def on_play_note(sid, data):
20 frequency = int(data.get("frequency", 440))
21 _call_mcu("play_note", frequency)
22
23
24ui.on_message("play_note", on_play_note)
25
26App.run()
sketch
Arduino sketch
1#include <Arduino_RouterBridge.h>
2#include <Arduino_Modulino.h>
3
4ModulinoBuzzer buzzer;
5
6String rpc_play_note(int frequency) {
7 buzzer.tone(frequency, 300);
8 return "{\"status\":\"ok\"}";
9}
10
11void setup() {
12 Bridge.begin();
13 Modulino.begin();
14 buzzer.begin();
15 Bridge.provide("play_note", rpc_play_note);
16}
17
18void loop() {
19 // Bridge handles RPC on a background thread; nothing needed here.
20}
styles
Style file
1:root {
2 --bg: #0f1117;
3 --surface: #1c1f2a;
4 --border: #2e3244;
5 --accent: #4fa3e0;
6 --text: #e2e8f0;
7 --muted: #8892a4;
8 --green: #22c55e;
9 --red: #ef4444;
10 --radius: 10px;
11
12 --white-key-w: 48px;
13 --white-key-h: 160px;
14 --black-key-w: 30px;
15 --black-key-h: 100px;
16}
17
18* { box-sizing: border-box; margin: 0; padding: 0; }
19
20body {
21 font-family: 'Segoe UI', system-ui, sans-serif;
22 background: var(--bg);
23 color: var(--text);
24 min-height: 100vh;
25 padding: 1rem;
26}
27
28/* ── Header ─────────────────────────────────────────────── */
29header {
30 display: flex;
31 flex-direction: column;
32 align-items: center;
33 gap: 0.5rem;
34 margin-bottom: 1.5rem;
35 text-align: center;
36}
37
38header h1 { font-size: 1.35rem; letter-spacing: -0.02em; }
39
40.status {
41 font-size: 0.8rem;
42 font-weight: 600;
43 padding: 0.25rem 0.75rem;
44 border-radius: 999px;
45 letter-spacing: 0.04em;
46 text-transform: uppercase;
47}
48
49.status.connected { background: var(--green); color: #fff; }
50.status.disconnected { background: var(--red); color: #fff; }
51.status.connecting { background: var(--muted); color: #fff; }
52
53/* ── Layout ─────────────────────────────────────────────── */
54main { display: flex; flex-direction: column; gap: 1.25rem; }
55
56.card {
57 background: var(--surface);
58 border: 1px solid var(--border);
59 border-radius: var(--radius);
60 padding: 1.25rem 1.5rem;
61}
62
63.card h2 {
64 font-size: 0.95rem;
65 font-weight: 600;
66 color: var(--muted);
67 text-transform: uppercase;
68 letter-spacing: 0.06em;
69 margin-bottom: 1rem;
70}
71
72/* ── Shortcuts ───────────────────────────────────────────── */
73.shortcut-grid { display: flex; flex-direction: column; gap: 0.4rem; }
74
75.shortcut-row {
76 display: flex;
77 align-items: center;
78 gap: 1rem;
79 font-size: 0.85rem;
80}
81
82.shortcut-label { color: var(--muted); min-width: 210px; }
83
84.shortcut-keys {
85 font-family: 'Courier New', monospace;
86 color: var(--accent);
87 letter-spacing: 0.05em;
88}
89
90/* ── Piano scroll wrapper ────────────────────────────────── */
91.piano-scroll {
92 overflow-x: auto;
93 padding-bottom: 0.5rem;
94}
95
96/* ── Piano container ─────────────────────────────────────── */
97#piano {
98 position: relative;
99 display: flex;
100 height: var(--white-key-h);
101 user-select: none;
102}
103
104/* ── White keys ──────────────────────────────────────────── */
105.white-key {
106 position: relative;
107 width: var(--white-key-w);
108 height: var(--white-key-h);
109 background: #f0f0f0;
110 border: 1px solid #999;
111 border-radius: 0 0 6px 6px;
112 cursor: pointer;
113 display: flex;
114 flex-direction: column;
115 justify-content: flex-end;
116 align-items: center;
117 padding-bottom: 8px;
118 gap: 2px;
119 transition: background 0.05s;
120 flex-shrink: 0;
121 z-index: 1;
122}
123
124.white-key:hover { background: #e0eeff; }
125.white-key.active { background: var(--accent); }
126
127/* ── Black keys ──────────────────────────────────────────── */
128.black-key {
129 position: absolute;
130 width: var(--black-key-w);
131 height: var(--black-key-h);
132 background: #222;
133 border: 1px solid #111;
134 border-radius: 0 0 4px 4px;
135 cursor: pointer;
136 z-index: 2;
137 display: flex;
138 flex-direction: column;
139 justify-content: flex-end;
140 align-items: center;
141 padding-bottom: 6px;
142 gap: 2px;
143 transition: background 0.05s;
144 top: 0;
145}
146
147.black-key:hover { background: #3a3a3a; }
148.black-key.active { background: #1a5fa0; }
149
150/* ── Key labels ──────────────────────────────────────────── */
151.note-label {
152 font-size: 0.55rem;
153 font-weight: 600;
154 color: #666;
155 text-align: center;
156 line-height: 1;
157}
158
159.white-key.active .note-label { color: #fff; }
160
161.black-key .note-label {
162 color: #aaa;
163 font-size: 0.5rem;
164}
165
166.black-key.active .note-label { color: #fff; }
167
168.key-shortcut {
169 font-size: 0.6rem;
170 font-weight: 700;
171 color: var(--accent);
172 text-transform: uppercase;
173 line-height: 1;
174}
175
176.black-key .key-shortcut {
177 font-size: 0.5rem;
178 color: #7ab8e0;
179}
180
181.white-key.active .key-shortcut,
182.black-key.active .key-shortcut { color: #fff; }
183
184/* ── Responsive ──────────────────────────────────────────── */
185@media (min-width: 600px) {
186 main { max-width: 900px; margin: 0 auto; }
187}
188
189/* ── Centering ───────────────────────────────────────────── */
190body { display: flex; flex-direction: column; align-items: center; }
191
192header { width: 100%; max-width: 900px; }
193
194main { width: 100%; align-items: center; }
195
196.piano-scroll { display: flex; justify-content: center; }
197
198.shortcut-grid { align-items: center; }
markup
HTML file
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>🎹 Virtual Piano</title>
7 <link rel="stylesheet" href="styles.css" />
8</head>
9<body>
10 <header>
11 <h1>🎹 Modulino Buzzer - Virtual Piano</h1>
12 <span id="status" class="status connecting">Connecting…</span>
13 </header>
14
15 <main>
16 <section class="card">
17 <h2>Keyboard Shortcuts</h2>
18 <div class="shortcut-grid">
19 <div class="shortcut-row">
20 <span class="shortcut-label">White keys (Octave 4):</span>
21 <span class="shortcut-keys">A S D F G H J</span>
22 </div>
23 <div class="shortcut-row">
24 <span class="shortcut-label">Black keys (Octave 4):</span>
25 <span class="shortcut-keys">W E T Y U</span>
26 </div>
27 <div class="shortcut-row">
28 <span class="shortcut-label">White keys (Octave 5, partial):</span>
29 <span class="shortcut-keys">K L ;</span>
30 </div>
31 <div class="shortcut-row">
32 <span class="shortcut-label">Black keys (Octave 5, partial):</span>
33 <span class="shortcut-keys">O P</span>
34 </div>
35 </div>
36 </section>
37
38 <section class="card piano-card">
39 <h2>Piano</h2>
40 <div class="piano-scroll">
41 <div id="piano"></div>
42 </div>
43 </section>
44 </main>
45
46 <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
47 <script src="app.js"></script>
48</body>
49</html>
Note: Content and images are from: https://projecthub.arduino.cc/, with some modifications.
If you want it removed due to copyright reasons, please leave a comment. Thank you.
I want to share this article more widely so that everyone knows about Arduino and your project.