QC

Modulino Buzzer + UNO Q - Virtual piano

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 &nbsp;S &nbsp;D &nbsp;F &nbsp;G &nbsp;H &nbsp;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 &nbsp;E &nbsp; &nbsp;&nbsp;T &nbsp;Y &nbsp;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 &nbsp;L &nbsp;;</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 &nbsp;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.

Easy remote control

Remotely control an LED with ease. Devices and components Arduino Nano 33 BLE with headers Box 525 Resistors precision 1% - 17 values Breadb...