The full source code for the project is available in the repository.
On this page, you'll find explanations of the main parts of the sketch and a list of
the external libraries used.
Remeber, the project is open source and released under the MIT License, which means you are free to use, modify, and distribute it for any purpose, as long as the original license and copyright notice are included (go to license).
Here is a list of the external libraries, each with a link to its source and documentation. They are used to handle FFT processing and hardware components, such as the OLED display, the LED ring and the button.
Make sure to install them before compiling the code.
In this section you can find explanations about the main functions used in the project, the ones that may not be immediately clear. It's not a line-by-line analisys of the full sketch: you can find it well commented in the repo.
This is the central part of the code, which is executed continuously, so it coordinates the various parts and functionalities.
The function begins by calling checkButton(), which manages the state of the push button, including
debouncing and press duration detection.
Depending on how long the button is held:
Then an if clause allows to alternate between audio sampling and FFT calculation, using a boolean
flag.
This structure ensures that both operations — which take roughly the same time (~65 ms) — don’t block other
instructions, allowing the device to maintain responsive LED updates and avoid visible lag.
How it works:
To avoid flickering or false "no tuning" states, a timer is used to keep showing the last valid tuning result for a few moments, even after the sound disappears.
At the end of the loop(), the system updates the visual output (LEDs and display).
void loop() {
checkButton();
if (sampling) {
getSamples(); // takes around 65 ms
if (isSound()) {
sampling = false;
}
} else {
calculateFFT(); // takes around 65 ms
sampling = true;
peak_frequency = fft.majorPeak();
peak_frequency *= 0.985; // peak correction
if (isPlaying()) {
calculateTuning();
lastTuningTime = millis();
}
}
if ((millis() - lastTuningTime) > keepLastTuning) {
tuning_status = -5;
}
updateDisplay();
updateLeds();
}
This function is used to verify the stability of the detected frequency over time, helping to avoid reacting to spurious or short-lived peaks that often occur while tuning.
It first checks whether the detected frequency is in a plausible range (I choose 65 Hz and 740 Hz as
limits, i.e. from about -400 cents from E2 (lowest string) to +200 cents from E4 (highest string), including
possible octave harmonics.
Then if the frequency is inside the valid range, the function checks if it is close to the previous peak,
considering three scenarios:
This check accounts for higher octaves caused by harmonic resonance, which can make the detected
frequency
appear as twice the actual note being played — common in acoustic instruments with resonant bodies.
If the new frequency does not match any of these criteria, it resets the time count.
At the end it returns true only if the frequency has been stable for longer than the debouncing time, meaning it is likely to be a real, sustained note rather than noise or a transient harmonic.
bool isPlaying() {
// freq between 65 Hz (-400c from E2) and 740 ((+200c from E4)*2)
if (peak_frequency < 65 || peak_frequency > 740) {
lastStableTime = millis();
} else {
// reset time if far from old peak
if (!((peak_frequency > (last_peak_frequency * 0.95) && peak_frequency < (last_peak_frequency * 1.05)) ||
(peak_frequency > (last_peak_frequency * 2 * 0.95) && peak_frequency < (last_peak_frequency * 2 * 1.05)) ||
(peak_frequency > (last_peak_frequency / 2 * 0.95) && peak_frequency < (last_peak_frequency / 2 * 1.05)))) {
lastStableTime = millis();
}
}
last_peak_frequency = peak_frequency;
if (((millis()) - lastStableTime) < freqDebouncingTime) {
return false;
} else {
return true;
}
}
This function is where the actual tuning status calculations are done.
The function begins by checking whether the detected frequency is significantly higher than the target string frequency (over 1.6×). If so, it’s likely an octave harmonic due to harmonic resonance (discussed earlier), and the frequency is halved to bring it back to the "correct note".
Then, the tuning deviation is calculated in musical cents using the standard formula (source):
cents = 1200 * log₂(peak_frequency / string_frequency)
This gives a precise measure of how many cents the detected pitch is above or below the target.
The result is then mapped into discrete tuning ranges from 0 (string correctly tuned) to 5 (dev > 50 cents) which are later used to control the speed and direction of LED rotation. This system ensures the rotation remains monotonic with respect to tuning accuracy, providing intuitive feedback to the user.
void calculateTuning() {
// correct frequency if double
if (peak_frequency > (strings_pitch[c_string] * 1.6))
peak_frequency /= 2;
// calculate distance in cents
int dev_cents = 1200.0 * log(peak_frequency / strings_pitch[c_string]) / log(2.0);
[...]
}
This is the function that handles the visual representation of tuning status using the WS2812B LED ring.
Based on the previously calculated tuning status, the behavior is as follows:
To control the rotation speed, a wait counter is used. Since the main loop runs approximately every 65 ms, the speed can be adjusted by changing how many loops must pass before advancing the LED positions.
The actual LED rotation is implemented by fading old lit LEDs and incrementing current LED indices to light up the next two LEDs in the ring using the rotateRight() or rotateLeft() functions. This approach gives a smooth stroboscopic effect that becomes faster as the tuning deviation increases.
void updateLeds() {
if (tuning_status == -1) { // rotation clockwise (right)
if (led_rotation_waits >= (MAX_TUNING_DEVIATION - tuning_deviation)) { // to set speed
fadeAllLeds();
cled_color = CRGB::Red; // red leds
rotateRight();
led_rotation_waits = 0; // reset waits
} else {
led_rotation_waits++;
}
} else if (tuning_status == 1) { // rotation counterclockwise (left)
[...]
} else if (tuning_status == 0) { // green bold cross
offAllLeds();
cled_color = CRGB::Green; // green leds
boldCross();
led_rotation_waits = 0;
} else { // turn off alls
offAllLeds();
led_rotation_waits = 0;
}
FastLED.show();
}
void rotateRight() {
cled_a++; // increment (going right)
cled_b++;
if (cled_a >= LED_NUMBER) cled_a = 0; // check led index
if (cled_b >= LED_NUMBER) cled_b = 0;
leds[cled_a] = cled_color; // set on leds
leds[cled_b] = cled_color;
}
The code is licensed under the MIT License. Copyright (c) 2025 Emanuele Pines.
See LICENSE for more details.