For-fun experiment with an Arduino UNO, where a passive piezo-buzzer’s pitch is controlled by an ultrasonic sensor (HC-SR04).
- 1 × Arduino UNO R3 board, or equivalent
- 1 × HC-SR04 ultrasonic proximity sensor
- 1 × passive piezo-buzzer
- 1 × four-pin push button
- 1 × 100 Ω or 220 Ω resistor (for button)
- 1 × 10 kΩ resistor (for button debounce)
- 1 × 1 μF capacitor (for button debounce)
The whole idea of this “pseudo-theremin” is to obtain the proximity to the nearest object (like a hand) via an ultrasonic sensor, turn that into a playable frequency, generate a square wave of that frequency, and pass it to a passive buzzer. To measure the proximity to the nearest object, it first sends a trigger pulse (10 μs) and then awaits an echo. Usually, the entire echo (so until the echo is back to logical zero). The distance can be calculated from either the time between the pulse signal and the first echo or just from the echo duration. The latter is more common, especially for cheap sensors, so I went with that.
HC-SR04 is a cheap hobbyist classic (I got it in a Freenove kit). It can measure between 2 and 400 centimetres with a working frequency of 40 Hz. In the worst case, the 400 cm round-trip can take 60 ms rounded up. This is the recommended measurement cycle noted in the datasheet. This project’s loop is thus made to be 60 ms long.
Implementing a simple function to measure the proximity was relatively straightforward, and so was hooking up its return value to a frequency for the buzzer. However, the sensor is quite unreliable.
example-without-filter.mp3.mp4
An ultrasonic sensor is not exactly the best choice for a musical application, as it is inherently slow and prone to noise and fluctuations. The HC-SR04 can never reach professional theremin quality by a long shot – theremins usually operate with ≤ 5 ms response, while the HC-SR04 has a typical latency of 30–90 ms, already making it 6–18× slower.
The first thing I had to accept was the inability to change tones linearly. Empirically-measured jitter of ± 3 cm, when the distance was linearly remapped to one octave, caused a pitch wobble of half a semitone. As much as I wanted a smooth, theremin-like linear effect, the best any ultrasonic sensor can really do are semi-stable discrete tones (relevant research: Raes, 2008). More on mapping distances to tones in the chapter Quantisation.
Another “issue” was the trigger and echo bouncing off all surfaces in my room while not correctly detecting the thing right in front of it (like my hand). While I call it an issue, this is how ultrasonic sensors work. I mostly resolved this issue with two filters:
- a “consecutive stop filter” (called ‘constop filter’ in code) that doesn’t stop the buzzer until a certain number of stop readings (timeout or too far) are read,
- a ‘median filter’ that takes three readings and only returns the median one (inspired byNewPing
ping_medianmethod), and - a ‘hysteresis filter’ that updated the reading only if the new reading was greater than the previous one by a certain threshold.
The readings could potentially be further smoothed with ‘exponential moving average (EMA)’ or a ‘Kalman Filter’ (relevant research: Al-Tahtawi, 2018). However, at this point, the responsivity was being negatively affected, and the readings were somewhat stable, so I left it at that. When played, the music no longer sounds like a cacophony – not quite like a theremin, but it’s a fun toy nonetheless.
The passive buzzer receives a square wave signal from the board’s 16-bit Timer1. This timer is set to ‘Fast PWM’ mode (single-slope operation), and the output comparator (OC1A on pin 9) is set to inverting mode. The internal clock is used (16 MHz) with a prescaler pitches.h, reducing quality). The OC1A frequency follows this equation:
For the tones, I used the pitches.h file available from ArduinoGetStarted.com (public domain), which defines note frequencies as macros. Solving for TOP:
The TOP value (stored in ICR1) is recalculated for every frequency, though fortunately, it didn’t cause performance issues. If it did, optimisation with a LUT should be possible. ICR1 defines the period length. The duty cycle is set to 50% via OCR1A (simple, works and having the multiplier a power of two allows use of a shift in place of expensive division). The combined behaviour in a nutshell: “The output pin and timer both start at zero. The timer counts up until it hits half of the period, setting the output to 1. After that, it continues counting until it hits the whole period length, where the timer resets and the output pin is set back to 0.”
TOP is a 16-bit value. hence, it can be within the range of ICR1 to properly set 50 % duty cycle of 1). Using the above equation, the theoretical maximum and minimum playable frequencies are:
The range is skewed towards higher frequencies. The lower boundary is higher than the minimum hearable frequency by humans (20 Hz), while the higher is much greater (20 kHz). Using a higher prescaler would balance it (e.g.
Lastly, the calculation of TOP can be slightly optimised to avoid expanding and narrowing the integer width with a binary shift (ATmega328P is an 8-bit MCU). The prescaler value
As mentioned at the start of the Ultrasonic Sensor Issues chapter, the ultrasonic sensor readings had too great of a jitter to allow linear tone change. Using a linear remap function was too much of a cacophony, so I had to go for stabler discrete notes rather than a rough continuous pitch. The distance was discretely mapped to 14 cm zones of A-moll (A-minor) pentatonic scale (A3, C4, D4, E4, G4).
Implementing different scales would be quite simple, even having more than one scale and switching between them. For this project, however, it was good enough to have a single scale.
For the sake of completeness, I should mention that the buzzer’s volume is rather low. The input signal comes directly from the output pin and is not amplified. For a more audible sound, using an NPN transistor as a switch to drive the buzzer from a higher voltage source would do the trick (as noted in the Freenove tutorial, page 88). To me personally, the volume is sufficient as is, and the quieter buzzer allowed me to work on this project at night. :)
The final piece of the project is a toggle button that turns it on or off. Although this part of the project is small, it forms the largest of all sub-circuits. The button, naturally, has to be debounced – I chose to do this with a pull-up resistor and an RC subcircuit (CircuitJS) rather than burdening the software. The result is a reliable button with an unnoticeable delay that is active in logical 0.
To turn the button into a switch, I unfortunately had to rely on software due to a lack of ICs at hand. I made a struct that serves as a button wrapper, maintaining its state and detecting a push (a falling edge, as the button is active in logical 0) that toggles it. While I would prefer a hardware solution, it works, it doesn’t cause performance issues, and it has a minimal memory fingerprint. A simple T flip-flop with a negative edge trigger would do the trick (various ICs can be used for that).
SimulIDE simulation of toggle button with 74HC112 used as a T flip-flop (GIF).
The schematic and the breadboard visualisation were made in Fritzing.
This project was made using PlatformIO. The simplest way to run it is
using the PlatformIO IDE extension for Visual Studio Code. The extension will
incorporate everything necessary components from the platformio.ini
file. To build and upload the project, run ‘Build’ and then ‘Upload’ tasks in the
PlatformIO menu tab.
You can also use PlatformIO CLI:
platformio run
platformio run --target uploadA Makefile is present for quick actions with the PlatformIO CLI. Rebuilding, uploading to the board and monitoring UART can be done in one line like this:
make clean; make build; make upload-monitorStatic analysis is done with cppcheck (via platformio check or make check) with MISRA C addon. To see the actual error messages, you’ll need to download MISRA C:2012 rule headlines (rename to misra_c_2012__headlines_for_cppcheck.txt).
Running this project with Arduino IDE has not been tested and would assumably require some adjustments.
This project’s code is free and open-source, licensed under the MIT Licence.
- TL;DR;NAL: Do absolutely whatever you want with the code, just include the LICENCE file if you re-distribute it.
- See
LICENCEfile or tl;drLegal for more details.
- ATmega328P MCU datasheet, parameters and PlatformIO setup
- Arduino UNO R3 datasheet and pinout
- Freenove K0007B tutorial and schematic (repository)
- HC-SR04 ultrasonic distance sensor datasheet; on Random Nerd Tutorials and Components101
- Active piezo-buzzer tutorial on ArduinoGetStarted.com
- Arduino button debouncing (switch debounce in digital circuits theory)





