Building a Toy Synth with Teensy 4.1: Audio Adapter, FastTouch, and Taming Potentiometer Noise
Why the Teensy 4.1 works so well for audio
The 600 MHz ARM Cortex-M7 inside the Teensy 4.1 can handle hundreds of simultaneous audio processing objects at 44.1 kHz without complaint. Stack oscillators, filters, reverb, and chorus in a single sketch and the CPU still has headroom. For a project where every physical control maps to a live sound parameter, that processing power is what keeps the whole thing viable.
The Audio Adapter Card
PJRC’s Audio Adapter Board for Teensy 4.x plugs directly onto the Teensy and adds a 16-bit, 44.1 kHz stereo codec: headphone out, stereo line out, and stereo line in or mono mic input. The adapter uses the SGTL5000 chip. In practice, you declare AudioControlSGTL5000 audioShield; in your sketch and the Audio Library handles all the low-level I2S communication underneath — no register-fiddling required.
Start with the Audio System Design Tool
Before writing a line of code, open the browser-based Audio System Design Tool at pjrc.com/teensy/gui. You drag audio objects from the left panel — oscillators, filters, mixers, envelopes, effects — wire them up with virtual patch cables, then click Export. Out comes Arduino C++ that handles all the object declarations and AudioConnection wiring for you.
A waveform generator through a filter, into a mixer, out to the adapter takes maybe a minute of clicking. The generated boilerplate is the part that takes longest to write by hand, so starting here saves real time. It also makes the signal path readable at a glance, which matters when you’re debugging noise at midnight.
Potentiometers and joysticks as synth controls
Mapping a pot to a synth parameter is conceptually simple: read analogRead(pin), scale to the parameter range, call a method like filter.frequency() or waveform.frequency(). The complication is ADC noise.
The Teensy 4.1’s ADC has jitter at the least-significant bits, and on audio parameters you hear it. Filter cutoff crackles faintly even when the knob is sitting still. Pitch wanders by a few cents. Exponential smoothing fixes it. Keep a running average:
float smoothed = 0;
void loop() {
int raw = analogRead(POT_PIN);
smoothed = 0.9 * smoothed + 0.1 * raw;
filter.frequency(smoothed * 20000.0 / 1023.0);
}
The alpha value (0.9 above) controls how much noise is filtered versus how quickly the control responds. Higher alpha means smoother but slightly sluggish. For filter cutoff, 0.9 to 0.95 works well. For pitch, go higher — around 0.97 — to avoid audible stepping as the value ticks between integers.
Joysticks are just two potentiometers on perpendicular axes. Wire each axis to its own analog pin and treat them as independent controls. No special handling needed.
Capacitive touch with FastTouch
The FastTouch library by Adrian Freed gives you capacitive sensing on any digital I/O pin that has an internal pull-up resistor. It measures how long a pin takes to rise to HIGH after the pull-up is enabled. A finger resting on a pad adds capacitance, slows that rise time, and the library returns a higher number.
Basic usage:
#include <FastTouch.h>
void loop() {
int reading = fastTouchRead(TOUCH_PIN);
if (reading > THRESHOLD) {
// finger detected
}
}
The right threshold depends on your pad’s geometry. A larger surface gives a bigger reading differential between touched and not touched, so there’s more room to position the threshold safely. A small piece of copper tape is enough for a reliable button. The FastTouch GitHub page has guidance on calibrating for different pad sizes.
FastTouch is intentionally fast and low-fidelity — on/off detection, no pressure gradations. For button-style controls on a toy synth, that’s exactly what you need.
Wiring it all together in the sketch
The Audio Library runs its processing in the background via DMA and interrupts. Your loop() doesn’t render audio; it just reads controls and calls methods on audio objects. That keeps the structure clean and predictable.
Polling all your pots and touch pins every single loop iteration isn’t necessary and eats cycles. Rate-limiting the control reads to around 100 Hz is clean:
unsigned long lastRead = 0;
void loop() {
if (millis() - lastRead > 10) {
lastRead = millis();
updateControls();
}
}
100 Hz is imperceptible as lag. The audio buffer period at 44.1 kHz with 128-sample blocks is about 2.9 ms, so control reads at 10 ms intervals compete with nothing time-critical.
One mechanical note worth making: hot-glue the wire ends right where they connect to the Teensy headers. Wires that flex repeatedly at a solder joint fail eventually — a small dab of glue costs five seconds and keeps a build alive for years rather than months.
Sources
