An 8‑voice polyphonic wavetable synthesizer running on an Arduino Uno (ATmega328P), powered by the Mozzi audio library. Uses 14‑bit HIFI audio output (2‑pin PWM on pins 9+10) with a 125 kHz carrier — well above audible range. Features 15 waveforms, full ADSR envelopes, EEPROM recording, 13 built‑in melodies, real‑time pitch transpose via potentiometer, and an optional Python GUI with MIDI file playback.
flowchart LR
GUI["Python GUI\n(tkinter)"] -- "2 Mbaud\nbinary serial" --> UNO["Arduino Uno\n(ATmega328P)"]
UNO --> MOZZI["Mozzi Engine\n(14‑bit HIFI)"]
MOZZI -- "Pins 9+10" --> HIFI["HIFI RC Filter\n(2‑pole, ‑40dB/dec)"]
HIFI --> POT["Volume Pot\n10kΩ"]
POT --> AMP["PN2222 Amp"]
AMP --> BUZ["Passive Buzzer"]
style GUI fill:#3776AB,color:#fff
style UNO fill:#00979D,color:#fff
style MOZZI fill:#4c9f38,color:#fff
style HIFI fill:#ff9f43,color:#fff
style POT fill:#2ed573,color:#fff
style AMP fill:#e056fd,color:#fff
style BUZ fill:#1e90ff,color:#fff
flowchart TD
subgraph Audio Path
OSC["Wavetable Oscillator\n(512 samples)"] --> MUL["× ADSR Envelope\n(0–255)"]
MUL --> MIX["8‑Voice Mixer\n(bitmask iteration)"]
MIX --> GAIN["× Smoothed Gain\n(voice‑count table)"]
GAIN --> CLIP["Hard Clip ±32767"]
CLIP --> PWM["MonoOutput::from16Bit()\n→ 14‑bit HIFI (Pins 9+10)"]
end
style OSC fill:#ff9f43,color:#fff
style MUL fill:#ff9f43,color:#fff
style MIX fill:#ff6b6b,color:#fff
style GAIN fill:#ff6b6b,color:#fff
style CLIP fill:#ff6b6b,color:#fff
style PWM fill:#4c9f38,color:#fff
- 8‑voice polyphony with oldest‑voice stealing
- 15 wavetable waveforms – Triangle, Sawtooth, Sine, Square, Electric Piano, Clarinet, Violin, FM Synth, Guitar, Cello, Flute, NES Pulse, Oboe, Osc Chip, Piano
- ADSR envelopes – per‑voice Attack, Decay, Sustain (level + length), Release
- 8 envelope presets – Piano, Organ, Staccato, Pad, Flute, Bell, Bass, Custom
- Pitch transpose – ±24 semitones via potentiometer (A5), synced to GUI in real‑time
- EEPROM recording – non‑blocking trickle‑write of up to ~170 notes with trailing silence capture
- 13 built‑in melodies – Mario, Tetris, Nokia, Imperial March, Pac‑Man, Happy Birthday, Zelda Secret, Game Over, Alert, Success, Failure, Notify, Jingle Bells
- 4‑segment LED display – shows note name and octave (TM1637)
- Hardware buttons – Record, Stop, Play, Clear with debounce (A0–A3)
- RGB LED visualiser – pitch‑mapped colour with brightness from envelope level
- Python GUI – 61‑key piano with QWERTY bindings, ADSR editors, MIDI file playback, pitch sync
- Binary serial protocol – low‑latency communication at 2 Mbaud
Not all waveform/envelope combinations sound equally good through the piezo buzzer output. The 14‑bit HIFI mode significantly improves dynamic range over standard 8‑bit PWM, but the piezo's frequency response still favours certain timbres. Recommended combos:
| Good | Not great | |
|---|---|---|
| Waveforms | Piano, Violin, Square, Clarinet | Most others sound mediocre through the buzzer |
| Envelopes | Piano, Staccato | Other presets are usable but less polished |
Tip: Piano waveform + Piano envelope is the default and the best all‑round combination.
| Component | Pins | Notes |
|---|---|---|
| Arduino Uno | – | ATmega328P, 2 KB RAM, 32 KB Flash |
| Audio output | Pin 9 + Pin 10 (Timer1 PWM) | Mozzi HIFI 14‑bit 2‑pin PWM |
| HIFI RC filter | Between pins 9/10 and pot | 2‑pole low‑pass (2×0.01 µF ceramic) |
| 4 push buttons | A0, A1, A2, A3 (INPUT_PULLUP) | Record, Stop, Play, Clear |
| Pitch potentiometer | A5 | 10 kΩ linear, ±24 semitones |
| RGB LED | 3, 5, 6 (PWM) | Common cathode |
| Status LEDs | 4, 7, 8 | Feedback, Rec status, Play status |
| 7‑segment display | 11 (CLK), 12 (DIO) | TM1637 4‑digit |
Note: Pins 9 and 10 are both reserved by Mozzi for HIFI audio output (Timer1 + Timer2). Do not connect anything else to them.
The audio path has three stages: HIFI resistor network + RC filter → volume pot → PN2222 amplifier → passive buzzer.
flowchart LR
P9["Pin 9"] --> HIFI["Stage 1\nHIFI Network"]
P10["Pin 10"] --> HIFI
HIFI --> POT["Stage 2\nVolume Pot 10kΩ"]
POT --> AMP["Stage 3\nPN2222 Amp"]
AMP --> BUZ["Buzzer"]
style P9 fill:#4a9eff,color:#fff
style P10 fill:#4a9eff,color:#fff
style HIFI fill:#ff9f43,color:#fff
style POT fill:#2ed573,color:#fff
style AMP fill:#e056fd,color:#fff
style BUZ fill:#1e90ff,color:#fff
Combines pin 9 (high 7 bits) and pin 10 (low 7 bits) into a single analog signal. Target ratio R_low/R_high ≈ 128. Actual: 500 kΩ / 4 kΩ = 125 (2.3% error, <0.05 bit loss).
flowchart LR
P9["Pin 9 high 7 bits"]
P10["Pin 10 low 7 bits"]
R1["2kΩ"]
R2["2kΩ"]
R3["1MΩ"]
R4["1MΩ"]
C1["0.01µF ceramic"]
C2["0.01µF ceramic"]
A(("Node A to Pot"))
G1["GND"]
G2["GND"]
P9 --- R1 --- C1 --- G1
R1 --- R2 --- A
R2 --- C2 --- G2
P10 --- R3 --- A
P10 --- R4 --- A
style P9 fill:#4a9eff,color:#fff
style P10 fill:#4a9eff,color:#fff
style A fill:#ff9f43,color:#fff
style G1 fill:#555,color:#fff
style G2 fill:#555,color:#fff
Parts: 2× 2 kΩ (series = 4 kΩ), 2× 1 MΩ (parallel = 500 kΩ), 2× 0.01 µF ceramic. Filter: 2‑pole RC, −40 dB/decade rolloff. Attenuates 125 kHz PWM carrier by ~36 dB.
flowchart LR
A(("Node A from filter"))
POT_1["Pot Pin 1"]
POT_W["Pot Wiper"]
POT_3["Pot Pin 3"]
GND["GND"]
AMP["To Amp"]
A --> POT_1
POT_1 --- POT_W --- AMP
POT_1 --- POT_3 --- GND
style A fill:#ff9f43,color:#fff
style POT_1 fill:#2ed573,color:#fff
style POT_W fill:#2ed573,color:#fff
style POT_3 fill:#2ed573,color:#fff
style GND fill:#555,color:#fff
style AMP fill:#e056fd,color:#fff
flowchart TD
VCC["+5V"]
WIPER["Pot wiper"]
R1["10kΩ bias top"]
R2["2kΩ bias bottom"]
CIN["10µF coupling cap"]
RC["1kΩ collector load"]
BUZ["Passive Buzzer"]
RE["100Ω emitter"]
CBYP["100µF bypass cap"]
QB(("B"))
QC(("C"))
QE(("E"))
QL["PN2222"]
G1["GND"]
G2["GND"]
G3["GND"]
VCC --- R1 --- QB
QB --- R2 --- G1
WIPER --- CIN --- QB
QB --- QL
QL --- QC
QL --- QE
VCC --- RC --- QC
VCC --- BUZ --- QC
QE --- RE --- G2
RE --- CBYP --- G3
style VCC fill:#ff4757,color:#fff
style QL fill:#ffa502,color:#fff
style QB fill:#ffa502,color:#fff
style QC fill:#ffa502,color:#fff
style QE fill:#ffa502,color:#fff
style WIPER fill:#e056fd,color:#fff
style BUZ fill:#1e90ff,color:#fff
style G1 fill:#555,color:#fff
style G2 fill:#555,color:#fff
style G3 fill:#555,color:#fff
- Use a PASSIVE buzzer (no internal oscillator). Active buzzers only beep at one fixed frequency.
- R_C (1 kΩ) and buzzer are in parallel between +5V and collector.
- Electrolytic cap polarity: + leg toward higher DC voltage (10 µF: + toward pot wiper; 100 µF: + toward emitter).
| Part | Qty | Role |
|---|---|---|
| 2 kΩ resistor | 2 | HIFI R_high (series = 4 kΩ) |
| 1 MΩ resistor | 2 | HIFI R_low (parallel = 500 kΩ) |
| 0.01 µF ceramic cap | 2 | 2‑pole PWM filter |
| 10 kΩ potentiometer | 1 | Volume control |
| 10 kΩ resistor | 1 | Bias divider top |
| 2 kΩ resistor | 1 | Bias divider bottom |
| 1 kΩ resistor | 1 | Collector load |
| 100 Ω resistor | 1 | Emitter degeneration |
| 10 µF electrolytic | 1 | Input coupling |
| 100 µF electrolytic | 1 | Emitter bypass |
| PN2222 transistor | 1 | Amplifier |
| Passive buzzer | 1 | Speaker output |
The circuit has four distinct signal‑conditioning jobs, each handled by a specific component:
1. Low‑Pass Filtering (Stage 1 — ceramic caps)
Mozzi's HIFI PWM carrier runs at 125 kHz — far above audible range, but it carries enormous energy that would distort the amplifier and waste power heating the buzzer coil. The two 0.01 µF ceramic caps form a 2‑pole RC low‑pass filter with a cutoff around 4 kHz (matched to the piezo's useful bandwidth). Each pole contributes −20 dB/decade rolloff, so the pair gives −40 dB/decade. At 125 kHz the carrier is attenuated by ~36 dB — reduced to roughly 1.5% of its original amplitude — while audio‑band content passes through essentially unchanged.
2. Coupling / DC Blocking (10 µF electrolytic)
The pot wiper sits at roughly +2.5 V DC (mid‑rail), while the transistor base is biased at ~0.83 V by the 10 kΩ/2 kΩ divider. Connecting them directly would fight the bias point and shift the transistor out of its linear operating region. The 10 µF coupling capacitor blocks DC, passing only the AC audio signal. It forms a high‑pass filter with the base's input impedance (~1.7 kΩ), giving a −3 dB point around 9 Hz — below the lowest musical note, so no audible bass is lost.
3. Bypass / AC Grounding (100 µF electrolytic)
The 100 Ω emitter resistor (R_E) provides DC stability — it prevents thermal runaway by introducing negative feedback. But for AC signals, this same negative feedback kills gain. The 100 µF bypass capacitor shorts R_E to ground for AC (its impedance at 100 Hz is ~16 Ω, dropping to <1 Ω by 1 kHz), restoring the amplifier's full AC gain of ~90× while keeping DC stability intact.
4. Full DC Isolation
Every stage is DC‑isolated from the next:
| Path | DC blocking element | Purpose |
|---|---|---|
| PWM pins → pot | RC filter caps (0.01 µF) | Low‑pass filters inherently block DC |
| Pot → transistor base | 10 µF coupling cap | Preserves bias point |
| Transistor collector → buzzer | Buzzer in parallel with R_C from +5 V | Passive piezo is inherently AC‑coupled (capacitive load) |
The Arduino's 5 V rail never directly drives the speaker. The transistor amplifier is the only current source, and the bias network sets its own independent operating point. This means PWM duty‑cycle offsets, ground bounce, or supply noise from the digital side cannot push DC through the buzzer.
pio run --target upload # Build and flash
pio device monitor -b 2000000 # Serial monitor at 2 Mbaudcd tools
pip install pyserial # Required
pip install mido python-rtmidi # Optional: MIDI file support
python piano_gui.py --port /dev/ttyACM0The GUI provides:
- 61‑key visual piano (C2–C7) with 2‑row QWERTY keyboard bindings
- Waveform selector, envelope presets, custom ADSR sliders
- Pitch transpose display (synced from hardware pot via UART)
- EEPROM recording controls and melody launcher
- MIDI file loading with speed control and progress bar
- Reset button to return Arduino + GUI to defaults
Binary protocol at 2 Mbaud. System commands are single bytes in the 0x00–0x14 range. Notes use MIDI numbering.
| Byte(s) | Command | Arguments |
|---|---|---|
0x00 |
STOP_ALL | – |
0x01 |
REC_START | – |
0x02 |
REC_STOP | – |
0x03 |
REC_PLAY | – |
0x04 |
REC_CLEAR | – |
0x05 |
SET_ADSR | 10 bytes: A_hi, A_lo, D_hi, D_lo, S, SL_hi, SL_lo, R_hi, R_lo, Oct |
0x07 |
SET_INST | 1 byte: instrument ID (0–7) |
0x08 |
PLAY_MELODY | 1 byte: melody ID (0–12) |
0x09 |
SET_WAVEFORM | 1 byte: waveform ID (0–14) |
0x15–0x7F |
NOTE_OFF | Byte = MIDI note number |
0x95–0xFF |
NOTE_ON | Byte − 128 = MIDI note number |
Arduino→GUI text responses: NOTE:ON <freq>, NOTE:OFF <freq>, SYNC:PI <semitones>, STOP, PLAYING <id>.
Buzzer/
├── src/
│ ├── main.cpp # Mozzi setup, control loop, command dispatch
│ ├── sound.cpp # 8‑voice synth engine, ADSR, pitch transpose
│ ├── recorder.cpp # EEPROM recording with trickle‑write
│ ├── player.cpp # PROGMEM melody player
│ ├── protocol.cpp # Binary serial parser
│ ├── buttons.cpp # Hardware button handler
│ ├── leds.cpp # RGB + status LED control
│ └── display.cpp # TM1637 note display
├── include/
│ ├── config.h # Pin assignments, constants
│ ├── sound.h # Sound API
│ ├── recorder.h # Recorder API, RecordedNote struct
│ ├── player.h / melodies.h # Melody definitions (PROGMEM)
│ ├── protocol.h # Command byte map + types
│ ├── notes.h / freq_utils.h # MIDI-to-frequency helpers
│ └── tables/ # 512‑sample wavetable headers
├── tools/
│ ├── piano_gui.py # GUI entry point
│ └── gui/ # tkinter app, config, serial, MIDI, components
├── AKWF_MOZZI_MODIFIED/ # Source waveform data
├── platformio.ini
└── README.md
| Optimisation | Detail |
|---|---|
| Tick‑based timing | Global controlTicks counter at 64 Hz replaces millis() — avoids Timer0 conflicts with Mozzi |
| O(1) voice allocation | Free‑voice stack (push/pop) instead of linear scan |
| Active voice bitmask | __builtin_popcount for voice counting; bitmask iteration in audio loop |
| Precomputed octave offset | Division by 12 cached at instrument‑change time, not in note‑on hot path |
| Fixed‑point pitch transpose | PROGMEM semitone ratio table (8.8 format), single multiply + shift per voice |
| Smoothed gain control | Integer IIR on gain factor at 64 Hz control rate prevents pops on voice‑count changes |
| Non‑blocking EEPROM | Ring‑buffered trickle‑write, 1 byte per control cycle via raw register access |
| PROGMEM melodies | O(1) melody lookup table; notes read with memcpy_P |
- PWM audio only – 14‑bit HIFI 2‑pin PWM (pins 9+10), not true DAC. The HIFI RC filter network attenuates the 125 kHz carrier, but output is still lo‑fi through a piezo buzzer.
- 2 KB RAM – 92%+ utilisation; 8 Voice structs + ring buffers + Mozzi internals leave very little headroom.
- 1 KB EEPROM – ~170 notes maximum recording capacity (6 bytes each).
- No analog volume control – volume is hardware‑only (external pot/amp). Software gain is for voice mixing balance.
- Timer1 + Timer2 taken – HIFI mode claims both timers. Pins 9, 10, 11 lose
analogWrite(). Pin 11 (TM1637 CLK) uses bit‑bang, not PWM, so is unaffected.
MIT License. See LICENSE.
Last updated: 2026‑04‑02