Lesson 5: Playing tones
Table of Contents
- The ESP32 tone problem
- How does Arduino’s tone work?
- Playing tones on ESP32
- Let’s make stuff
- Resources
- Next Lesson
Video. A video demonstrating the Tone32.hpp class, which supports play durations on the ESP32. The code running on the ESP32 is available here. Make sure your sound is on.
On Arduino, the tone()
function generates a square wave of a specified frequency on a pin and is used to “play” tones on piezo buzzers or speakers; however, it is famously unsupported on the ESP32. In this lesson, we will provide some context about this problem and then show you how to play tones on the ESP32 using the LEDC PWM library, which we also used in our ESP32 LED Fade lesson.
The ESP32 tone problem
The ESP32’s Arduino library called arduino-esp32 attempts to mimic and/or directly replicate the functionality from core Arduino; however, as we saw in our ESP32’s LED Fade lesson, this is not always possible and key functions, like analogWrite
, are different.
Just as analogWrite
is not supported on arduino-esp32 so too is tone()
unavailable. Recall that on Arduino, tone()
generates a square wave of a specified frequency (with fixed 50% duty cycle) on a pin and is used to “play” tones on piezo buzzers or speakers. In our Intro to Arduino series, for example, we used tone()
to create a piano.
However, if you attempt to compile code with tone()
using the ESP32, you will receive a compiler error like this: 'tone' was not declared in this scope
. Thus, even basic tone examples built into the Arduino IDE like Examples -> Digital -> toneMelody
fail, as shown below.
Figure. Example of how even basic tone examples, like toneMelody.ino, which ships as a built-in example with the Arduino IDE, fails with a ESP32 board selected.
The lack of tone()
support has caused much chagrin and confusion in the maker community, including Issue #980 and Issue #1720 in the arduino-esp32 GitHub repo as well as forum and blog posts.
What can we do about this? And why isn’t tone supported anyway? Let’s dive in below.
How does Arduino’s tone work?
Recall that Arduino’s tone provides three key methods:
Both tone methods drive a PWM waveform on the provided pin with the given frequency using timer interrupts. The second version adds in a duration
parameter, which allows you to specify how long (in milliseconds) to play the tone for. Either way, you can call noTone(pin)
to stop outputting the PWM waveform and turn off the tone.
This tone API is simple and well-understood. It’s implemented in core Arduino, including for AVR-based microcontrollers—ArduinoCore-avr (Tone.cpp)—and SAMD-based microcontrollers— ArduinoCore-samd (Tone.cpp). When using Arduino, we expect tone()
to be available!
To generate the PWM waveforms and to track play tone duration, the tone library uses hardware timers (aka timer interrupts). However, these hardware timers and the functionality therein differs widely depending on microcontroller chip. The Atmel AVR microcontrollers like the ATmega328 used on the Arduino Uno and ATmega32u4 used on the Arduino Leonardo handle them one way while the Atmel SAMD21 microcontrollers handle them another. Even for just AVR-based microcontrollers, there is a lot of nuance and differences—see the #ifdef
in Tone.cpp for ArduinoCore-avr.
Most relevantly for us, Expressif decided not to implement tone()
into arduino-esp32. While we’re not sure why, what can we do about it?
Playing tones on ESP32
Fear not, things are not as dire as they seem. As Thomas Countz points out on GitHub Issue #1720, the LEDC PWM library—which we used in our ESP32 LED Fade lesson—actually has tone related methods, including:
where note_t
is defined as the following in esp32-hal-ledc.h:
Code. See esp32-hal-ledc.h and esp32-hal-ledc.c from the arduino-esp32 repo.
To use either ledcWriteTone
and ledcWriteNote
, we can follow a similar approach to what we did for fading an LED. First, let’s build our circuit.
Example circuit
Our circuit is as simple as they come. Simply attach your piezo buzzer to a GPIO pin. In this case, we’re using GPIO Pin 26. In our courses, we often use the TDK PS1240 piezo buzzers (about $0.46 Mouser or $1.35 at Adafruit). These buzzers work with both 3V and 5V square waves. Their resonant frequency (loudest tone) is 4kHz but you can drive them with a far larger range (we’ve tested from 32Hz up to 10Khz, at which point the sound is ear piercing). As non-polarized devices, they can be connected in either orientation (like resistors).
Figure. Circuit diagram to hook up PS1240 piezo buzzer with the ESP32. We’ve wired the buzzer to GPIO Pin 26. Image made in Fritzing and PowerPoint.
Example code
Now, let’s write the code.
First, we need to “attach” our piezo buzzer pin to one of the 16 PWM channels available on the ESP32 using the ledcAttachPin
function. In this case, we’ll use Pin 26 and PWM channel 0. Recall that the ESP32 has 16 PWM channels (0-15) and each can be configured independently to drive different PWM waveforms. In software, we “attach” pins to these PWM channels to receive the waveform.
Great, now we’ve attached Pin 26 on PWM channel 0.
Now, we can simply play a note using ledcWriteNote
or raw frequency using ledcWriteTone
. For example, the code below loops through playing middle C using ledcWriteNote
and then the frequency 800 Hz using ledcWriteTone
with 500ms pauses in between.
That’s it!
Now, those with a keen eye may have noticed that there are no functions that take in a duration
parameter. Yes, this is a small problem. Yes, we will address this!
Let’s get making!
Let’s make stuff
In the activities below, we are going to first play a scale and various raw frequencies before introducing the Tone32.hpp class, which helps abstract some complexity. Generally, we try to embed mp4 videos directly into our lessons. To more easily control playback and sound, we are going to be using YouTube embeds here. So, make sure you have your sound on (and possibly wear headphones, to limit annoyance to others around you).
Playing the C scale
Using the same circuit as before, let’s write code to play a simple C major scale based on Thomas Countz’s comment on GitHub Issue 1720. While we could use an array to step through notes, let’s keep things super simple and just write out each note directly. The full code is:
And a video demo below:
Video. A video demonstration of PlayScale.ino. The actual version shown in the video is PlayScaleWithOLED.ino. Make sure your sound is on.
Reading analog input, outputting raw frequencies
OK, now let’s make a slightly more complicated version that reads in an analog input and translates this value into an output frequency. For our demo, we are using a potentiometer. But, of course, any analog input would work!
Build the circuit
We need to slightly modify our circuit by adding a potentiometer—in this case, a 10K potentiometer.
Figure. Circuit diagram of Huzzah32 with piezo buzzer and potentiometer. Image made in Fritzing and PowerPoint.
Write the code
Now, let’s write code to take in the analog input and use this to set the frequency of the PWM output waveform.
Here’s a video demo.
Video. A video demonstration of AnalogInputTone.ino. The actual version shown in the video is AnalogInputToneWithOLED.ino. Make sure your sound is on.
Introducing the Tone32.hpp class
The examples above demonstrated how to use ledcWriteTone
and ledcWriteNote
from esp32-hal-ledc.c to drive specific PWM frequencies on output pins—these square waves manifest as sound with the piezo buzzers.
However, these methods aren’t as easy to use as the Arduino tone()
library and don’t support play durations. To address these limitations, we created Tone32.hpp. Tone32.hpp is part of the Makeability Lab Arduino Library. Follow the instructions here for installation and use.
Key Tone32 differences with tone library
There are a few key differences with: tone()
library:
-
First, we use an object-oriented approach. To create a Tone32 object, simply call
Tone32 tone32(pin, pwmChannel)
, which creates a Tone32 object with the given output pin and PWM channel. -
Second, while
tone()
uses timer interrupts to track play durations—and automatically stops playing after a duration has expired—we use a “polling” approach. So, you must callupdate()
on eachloop()
. This is essential if you are using the duration parameters. Note: I encourage others to adapt Tone32 to use timer interrupts but polling is fine for our purposes (as long as you consistently call update() with limited time between calls) -
Third, unlike
tone()
, you play tones via eitherplayNote
orplayTone
, both of which are overloaded functions withduration
options.
Primary Tone32 methods
Here are the key Tone32 methods:
There are also some other helpful functions like:
Let’s try building some stuff with Tone32.hpp!
Demonstrating Tone32 tone durations
To demonstrate using durations, we wrote AnalogInputTone32.ino, which walks up and down the C scale using an analog input (we used a potentiometer). When you “land” on a new note, we play the note’s frequency for 500 ms. We will use the same piezo buzzer + potentiometer circuit as before but with new code.
Here’s the whole code:
Here’s a video demonstration. Notice how we’re displaying the duration
remaining for each tone on the OLED—this is to help highlight the duration functionality of the Tone32.hpp library.
Video. A video demonstrating AnalogInputTone32. Note that this video is running a slight variation with OLED output called AnalogInputTone32WithOLED.ino. Make sure your sound is on.
Bonus Ball Bounce Video
Finally, we’ve included a bonus simple ball bounce demo using the Tone32.hpp library, again highlighting the play duration
functionality. Here, we play a brief tone when the ball bounces off the floor or ceiling.
Video. A video demonstrating BallBounceTone32WithOLED.ino. Make sure your sound is on.
Resources
-
Tone was Not Declared in this Scope, arduino-esp32 GitHub Issue #1720
-
Arduino Tone for ESP32, Thomas Countz
-
ESP32Servo, a third-party Servo library for ESP32 that attempts to faithfully mimic the Arduino Servo library but also has tone functionality.
Next Lesson
In the next lesson, we will learn about and use the ESP32’s built-in capacitive touch sensing module.
Previous: Analog input using the ESP32 Next: Capacitive touch sensing with the ESP32