Polyphonic touch-based synthesizer running bare-metal on STM32. 4 track looper, 43 instrument, 5 FX chain, WAV export to SD card. Inspired by KORG KAOSSILATOR PRO.
EECS 373 Final Project, University of Michigan, Winter 2025.
- Kai Kin Sou
- Tong Sing Wu
- Linglong Qian
- Ethan Kocheril
Short preview above (no sound). Full demo video with sound: docs/demo.mp4 (stored with Git LFS, about 340 MB, use "View raw" to download).
- Quick Start
- Features
- How It Works
- Touch Input
- Effect Rack
- Hardware (full pin map -> docs/HARDWARE.md)
- Build & Flash
- Controls (full table -> docs/CONTROLS.md)
- Tools
- Memory & CPU
- DSP Deep Dive
- Gallery
- Known Issue
- License
- Acknowledgments
- Power on, hear silence (or VU meter idle on the 8 digit SSD)
- R1 turn: pick instrument (43 to choose from)
- Touch screen: x = pitch, y = filter cutoff
- R2 turn for BPM, or tap TEMPO 4 time to set
- Press
REC, play some note, pressRECagain to stop. UseTRK 1-4to switch track and layer Shift + REC: export current loop to SD asexpNN.wav(48 kHz, 16 bit mono)
If something weird happen, see docs/CONTROLS.md for what every button do in each mode.
- 4 track looper, each track up to 384 note, per-track loop length, gain, mute
- 43 instrument (lead, drum, bass, string, chord)
- 6 waveform oscillator (sine, saw, square, soft saw, piano, pulse) from a 65536 entry wavetable
- 5 effect chain: bitcrush, tremolo, ringmod, delay, compressor, plus a separate pitch shift (a distortion stage is in the code but left off in the final build)
- 103 preset song (game, anime, classical, pop), browse with Shift+FX+R1
- 20 metronome pattern (rock, jazz, bossa, dubstep, etc.)
- Touch screen with x = pitch, y = filter cutoff, with octave shift
- Tap tempo, BPM 30 to 400
- WAV export to SD card (48 kHz, 16 bit mono)
- 8 digit 7-seg display with VU meter when idle
- Per-note "shift" snapshot for slide / glide effect inside a single note
The STM32 run at 120 MHz, and a separate PLLSAI1 clock make the 48 kHz audio rate so the sample rate stay stable no matter the CPU load. Audio go out over I2S to a MAX98357A class D amp.
- The SAI DMA use a double buffer (ping-pong). Every 2667 us it fire an interrupt and
DMA_fill()runmixer_next()once per sample to sum all active voice. While the DMA play one half, the CPU fill the other half, so SD card write do not cause glitch. - TIM7 fire at the BPM tick rate and drive the looper. When a track pointer reach a
Record, the looper spawn aSoundinto a fixed pool of 8 voice. - A
Recordis a small note descriptor (pitch, start tick, duration, and up to 8Shiftsnapshot for the x/y slide). A liveSoundis the heavy DSP state, around 20 KB with the Karplus-Strong ring buffer. Hundreds of Record can sit in RAM but only 8 Sound exist at once, so the pool work like a priority scheduler: when it is full, the lowest priority voice get evicted. - Writing a new voice into the pool is wrapped in a
__disable_irq()critical section, so the 48 kHz DMA interrupt never read a half-builtSound.
Full math and code walk through is in docs/DSP.md.
LCD touch grid is 12 col x 8 row. The x axis map to pitch (1 octave per 480 px), y axis map to filter cutoff (200 Hz to 8000 Hz, top = bright).
R4 turn (Shift held) shift the octave by 1.
5 effect in series, each can be toggle on/off independently. Order in the chain (after mixer):
bitcrush -> tremolo -> ringmod -> delay -> compressor
Pitch FX is separate, apply pre osc (multiply the freq by the pitch ratio when create new sound). A distortion stage also exist in the code but is left off in the final build.
| FX | Param 1 (R3) | Param 2 (R4) | Param 3 (R2) |
|---|---|---|---|
| Delay | feedback | time | wet mix |
| Tremolo | depth | rate | n/a |
| RingMod | freq | depth | n/a |
| Bitcrush | bits | level | n/a |
| Compressor | threshold | ratio | makeup |
| Pitch | semitone shift (-24 to +24) | n/a | n/a |
Hold FX and turn R1 to pick which effect to edit, hold FX+REC to toggle that effect on/off.
Quick summary, full pin map and CubeMX config in docs/HARDWARE.md.
| Part | Connection |
|---|---|
| MCU | STM32 Nucleo-L4R5ZI-P, 120 MHz, 2 MB Flash, 640 KB SRAM |
| LCD | 4" TFT 480x320 ILI9488, SPI2 |
| Touch | XPT2046 resistive ADC, SPI1 |
| Amp | Adafruit MAX98357A I2S Class D, SAI1 |
| SD card | Adafruit Micro SD SPI breakout, SPI3, FATFS |
| SSD | HT16K33 8 digit 7-seg, I2C1 |
| Encoder x4 | quadrature on GPIO |
| Button x9 | active low on GPIO (+ 1 Nucleo debug button) |
| ADC | master gain pot (single channel) |
- Open
eecs373_project.iocin STM32CubeIDE (the.iocregenerate the HAL init code if needed) - Target:
STM32L4R5ZITxP - Project -> Build All
- Run -> Run / Debug, ST-Link auto detect via the on board programmer
Note: regenerating from the .ioc needs you to sign in to an ST account inside the IDE. Without it CubeMX will quietly skip the codegen and you keep running the old pin map (this cost us two days, see Known Issue).
Five mode (PLAY, REC, EDIT, SHIFT_EDIT, EXPORT), each with own button mapping. Quick summary:
| Mode | How to enter | What it do |
|---|---|---|
| PLAY | default | play loop, touch screen, knob to change instr/BPM/octave |
| REC | press REC | record touched note into selected track |
| EXPORT | Shift+REC | render all track to wav on SD |
| EDIT | Shift+TEMPO (track has note) | edit individual note pitch / start / end |
| SHIFT_EDIT | R1 click in EDIT | edit per-shift x / y / gain inside the note |
Full reference for every control combo in docs/CONTROLS.md.
eecs373_project/
├── Core/
│ ├── Inc/ # header
│ │ └── songs/ # 103 preset song + metro.h
│ ├── Src/ # source (30 file)
│ └── Startup/
├── Drivers/ # STM32 HAL + CMSIS (vendor)
├── FATFS/ # FATFS middleware (vendor)
├── Middlewares/ # ST middleware (vendor)
├── tools/ # python helper script (midi2record, wav2lut, ...)
├── docs/ # extended doc (DSP, CONTROLS, HARDWARE) + demo video
├── img/ # README image
├── eecs373_project.ioc # CubeMX config
└── README.md
Python script in tools/ for adding song / instrument / lut. See tools/README.md for detail.
| Script | What it do |
|---|---|
midi2record.py |
convert .mid to song header |
song_index.py |
scan songs folder, print #include block for pattern.h |
instrument_creator.py |
print template C code for new instrument |
pattern_visualize.py |
ASCII piano roll for a song |
wav2lut.py |
bake wav into int16 LUT |
envelope_tune.py |
matplotlib slider for ADSR tuning |
memory_report.py |
top RAM / Flash consumer from .map |
Run python3 tools/memory_report.py after a build for live number. Approx from final firmware:
| Resource | Used | Total | % |
|---|---|---|---|
| Flash | ~580 KB | 2 MB | 28% |
| RAM | ~145 KB | 640 KB | 22% |
Polyphony / CPU at 120 MHz, 48 kHz, budget 2667 us per fill:
| Voice | FX off | FX on (all) |
|---|---|---|
| 1 | ~150us | ~280us |
| 4 | ~600us | ~750us |
| 8 | ~1200us | ~1500us |
Stay under budget at max polyphony (8 voice) with all FX on, with margin for SD card export inline. These number are approximate and a bit optimistic, the real cost per voice depends on the instrument (Karplus-Strong string and heavy filter sweep cost more than a plain oscillator), the point is it stay real time at 8 voice with FX on.
Left to right: the 3D printed enclosure, demo day at the expo, and the in-house PCB attempt (we ended up on a breadboard, see Known Issue).
- WAV export to SD play back faster than real time and have some noise. The inline capture pipeline work but the playback rate is off, this was not fix before the deadline
- LED pin assignment in
control.ccontrol_set_ledis placeholder (commented out) to avoid pin conflict with current setup - Power: when many peripheral run at full draw (LCD + touch + SSD + SD), USB 5V can sag and cause SD export to drop chunk. Use external 5V supply for stable export
MIT License. Educational use, EECS 373 final project.
- KORG KAOSSILATOR PRO for the original design idea
- Hal Chamberlin, Musical Applications of Microprocessors, for the SVF topology
- Karplus & Strong (1983), Digital synthesis of plucked-string and drum timbres
- music-dsp.org and EarLevel Engineering for SVF reference
- Julius O. Smith III, Physical Audio Signal Processing (CCRMA Stanford)
- Udo Zölzer, DAFX: Digital Audio Effects, for FX algorithm
- ChaN's FatFs SD card file system
- ST Microelectronics for STM32 HAL / CMSIS
- EECS 373 course staff at University of Michigan





