Looper Instrument & Metronome inside Theremin Synthesizer is a musical instrument that combines the functionality of a synthesizer, an optical-sensor-based theremin, and a loop station, all processed on an ESP32-S3 microcontroller.
The project is structured around the ESP32-S3-WROOM-1 (N16R8) microcontroller, which runs the audio synthesis on one processor core and the user interface/metronome on the other core, ensuring zero-latency performance.
System Architecture (Main Blocks):
Bill of Materials (BoM):
104 Ceramic Capacitor (0.1µF)475 Ceramic Capacitor (4.7µF) - For power supply stabilization on the breadboard.To ensure stability and prevent conflicts with the ESP32-S3's internal memory and boot processes, several pins were explicitly avoided:
| Pin Constant / Name | GPIO Pin | Component / Module | Notes & Justification |
|---|---|---|---|
PIN_I2C_SDA | 15 | I2C Bus Data (OLED + VL53L1X) | SAFE — Not used by Octal PSRAM. Shares the bus for display and sensor. |
PIN_I2C_SCL | 16 | I2C Bus Clock (OLED + VL53L1X) | SAFE — Not used by Octal PSRAM. |
PIN_ENC_A | 17 | EC11 Rotary Encoder | Standard digital input. |
PIN_ENC_B | 18 | EC11 Rotary Encoder | Standard digital input. |
PIN_ENC_PUSH | 21 | EC11 Rotary Encoder | Push button for menu selection. |
PIN_ENC_KEY | 47 | EC11 Rotary Encoder | Additional encoder key/switch input. |
KEY_PINS | 1, 2, 4, 5, 6, 7, 8, 9, 10, 11 | Touch Keyboard (10 Keys) | Uses native S3 touch-capable GPIOs. Consciously avoids strapping pins. |
PIN_BPM_POT | 12 | Analog Potentiometer (BPM) | ADC channel — safe and stable for adc_oneshot_read(). |
PIN_VOL_POT | 13 | Analog Potentiometer (Volume) | ADC channel — safe and stable for adc_oneshot_read(). |
PIN_MASTER_VOL | 14 | Analog Potentiometer (Master) | ADC channel — safe and stable for adc_oneshot_read(). |
PIN_AUDIO_PDM | 40 | Audio Output (PDM via I2S) | SAFE — Placed outside of the reserved PSRAM range. |
PIN_BUZZER | 41 | Metronome Buzzer | Independent passive buzzer driven via LEDC PWM. |
PIN_CH2_PLAY | 42 | Looper Control (CH2 Play) | Re-mapped from 48 (which was colliding with the onboard RGB LED). Safe MTMS pin after GPIO reset. |
PIN_CH2_REC | 38 | Looper Control (CH2 Record) | Tactile button (active-LOW with internal pull-up). |
PIN_CH1_PLAY | 20 | Looper Control (CH1 Play) | Tactile button (active-LOW with internal pull-up). |
PIN_CH1_REC | 19 | Looper Control (CH1 Record) | Tactile button (active-LOW with internal pull-up). |
The firmware is written in ESP-IDF (v6.x) using the C programming language. The architecture heavily utilizes FreeRTOS to divide the immense computational load of real-time audio synthesis from user interaction and sensor polling.
NOTE_ON, NOTE_OFF, and VOL_CHANGE events relative to the metronome.shared_i2c.c wrapper uses FreeRTOS mutexes to allow both the OLED display (UI Task) and the VL53L1X sensor (Theremin Task) to safely communicate on the exact same physical I2C pins without corrupting data.
The project utilizes the ESP32-S3's dual-core processor to guarantee that audio never stutters:
synth_task. This task runs at a very high priority, rapidly filling the I2S audio buffer to ensure zero-latency sound generation.metronome_task: Polls the potentiometers and drives the passive buzzer precisely on beat.looper_task: Manages state machines (LOOPER_RECORDING, LOOPER_PLAYING) and feeds synthetic MIDI-like events back into the synthesizer.theremin_task: Polls the I2C Time-of-Flight sensor.ui_task: Draws menus and dynamic overlays to the SSD1306 OLED based on rotary encoder inputs.Because audio instruments require instantaneous feedback, all buttons and keys use hardware interrupts.
touch_pad_isr.gpio_isr_handler.
To prevent Kernel Panics during heavy system load, the Interrupt Service Routines (ISRs) never use standard logging (ESP_LOGI) or blocking functions. They only update minimal state variables and trigger events instantly using esp_rom_printf for thread-safe debug logging.
Dynamic BPM Playhead Scaling (looper.c):
uint32_t cur_bpm = metronome_get_bpm(); int64_t scaled_delta = (delta_us * cur_bpm) / s_channels[ch].recording_bpm; s_channels[ch].playhead_us += scaled_delta;
Instead of recording raw audio (which takes massive memory and locks the tempo), the Looper records “events”. During playback, the time delta is multiplied by the ratio of the current BPM to the recorded BPM. This allows time-stretching loops in real-time.
Hardware Noise Gating (metronome.c):
/* Hardware deadzones to guarantee true 0% and 100% and prevent ADC boundary jitter */ if (s_master_vol <= 4) s_master_vol = 0; if (s_current_vol <= 4) s_current_vol = 0;
Analog potentiometers inherently experience electrical noise. When turned almost all the way down, “ADC boundary jitter” would cause the metronome to randomly fluctuate between 0% and 5% volume, resulting in phantom/random beeps. This simple hardware deadzone explicitly suppresses the noise floor.
Collision-Free Polyphony (synth.c / looper.c): To ensure the live instruments and the two looper channels never steal each other's audio channels, the synthesizer allocates a massive pool of 15 voices.
0-4: Live Instruments (Theremin, Touch Keyboard).5-9: Looper Channel 1 playback.10-14: Looper Channel 2 playback.This guarantees robust sound even during complex, multi-layered jamming.
The final result is a fully functional, standalone digital instrument that requires no PC to operate. The hardware RC filter successfully converts high-frequency PDM digital signals into a smooth analog waveform that drives standard headphones. The capacitive keyboard and optical Theremin successfully mimic traditional musical input, and the Dual-Channel Looper operates flawlessly in sync with the hardware metronome.
Validation was performed iteratively:
mixed_sample *= 0.33f) and hard clipping limits in the synth engine.The project successfully proves that advanced DSP (Digital Signal Processing) and multi-layered audio sequencing can be achieved entirely on a low-cost microcontroller. The most difficult challenge was managing the ESP32's memory constraints and routing hardware interrupts without crashing the RTOS scheduler.
If I were to build a v2.0, I would add an external I2S DAC (like a PCM5102a) for true high-fidelity 24-bit audio output, and I would design a custom PCB to eliminate the inherent electrical noise caused by breadboard jumper wires.
The complete project source code is available on GitLab.
ESP-IDF Documentation (FreeRTOS, I2C, I2S, LEDC, Touch Sensor)ESP32-S3 Technical Reference ManualVL53L1X / VL53L0X Time-of-Flight DatasheetSSD1306 OLED Display Controller DatasheetLM386 Audio Amplifier Datasheet