SOFTWARE

REPOSITORY

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).

LIBRARIES

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.

  • FastLED (source, docs): allows to control WS2812B LEDs
  • SSD1306Ascii (source, docs): used instead of Adafruit libs, because no complex graphics was needed and less resource hungry
  • arduinoFFT (source, docs): the core of the project, a platform optimized lib to perform Fast Fourirer Transform
  • ezButton (source, docs): the easy way to debounce buttons

Make sure to install them before compiling the code.

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.

loop()

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:

  • a short press advances to the next guitar string
  • a long press triggers the playback of the reference tone for the currently selected string

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:

  1. when sampling is true, the function getSamples() collects audio data from the microphone and fills the signal vector;
  2. isSound() checks if the input exceeds a predefined threshold, filtering out ambient noise or silence. If a valid sound is detected, the next cycle will perform FFT instead of resampling;
  3. when sampling is false, it calculates FFT and finds the peak frequency;
  4. isPlaying() confirms the frequency is stable enough, then calculateTuning() determines the deviation from the target note.

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();
  }

isPlaying()

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:

  • the same frequency (±5%);
  • twice the frequency (an octave above ±5%);
  • half the frequency (an octave below ±5%).

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;
    }
  }

calculateTuning()

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);

    [...]
  }

updateLeds()

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:

  • if not tuning (tuning_status = -5): all LEDs are turned off;
  • if perfectly tuned (tuning_status = 0): a static green cross is shown;
  • if too low (tuning_status = -1): the LEDs lit red and rotate clockwise;
  • if too high (tuning_status = 1): the LEDs lit red and rotate counterclockwise.

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;
  }

LICENSE

The code is licensed under the MIT License. Copyright (c) 2025 Emanuele Pines.

See LICENSE for more details.