Skip to content

Jitreq/CCC

Repository files navigation

Buzzer – 8‑Voice Polyphonic Synthesizer for Arduino Uno

PlatformIO Mozzi Arduino Python

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.


Architecture

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
Loading
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
Loading

Features

  • 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

Sound Quality Notes

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.


Hardware

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.

Audio Output Circuit

The audio path has three stages: HIFI resistor network + RC filter → volume pot → PN2222 amplifier → passive buzzer.

Signal Chain Overview

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
Loading

Stage 1: HIFI Resistor Network + 2‑Pole RC Filter

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
Loading

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.

Stage 2: Volume Control (10 kΩ Pot)

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
Loading

Stage 3: PN2222 Common‑Emitter Amplifier

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
Loading
  • 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).

Bill of Materials (Audio Circuit)

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

How the Audio Path Works

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.


Getting Started

Build & Upload (PlatformIO)

pio run --target upload        # Build and flash
pio device monitor -b 2000000  # Serial monitor at 2 Mbaud

Python GUI

cd tools
pip install pyserial           # Required
pip install mido python-rtmidi # Optional: MIDI file support
python piano_gui.py --port /dev/ttyACM0

The 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

Serial Protocol

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>.


File Structure

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

Key Optimisations

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

Limitations

  • 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.

License

MIT License. See LICENSE.


Last updated: 2026‑04‑02

About

Cluster Circuit Cartel

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors