Need help with integration of FFT for Teensy to my feedback loop system with relays code

Hello there! I am new to microcontrollers, so until this point I got help from friends and chatgpt, but got stucked at some point.
I have here Teensy (i am using Teensy 3.2 controller) code with relay modules and analog microphone sensors in order to detect amplitude from loudspeakers (for each relay and mic there is one speaker). And after detecting the amplitude which is above threshold, it would turn on or off the relay of other speaker.

However, instead of detecting amplitude of the sound, i would like to detect pitch (the speakers are playing human speech sounds). So, i thought of using fft, which whenever there is high pitch detected (above the threshold) it would trigger the relay modules.

A friend suggested this parameter: Goertzel frequency. and here is the example of code i tried. But its not what exactly i want to achieve. It only shows the value where the given Hz is found in a strong or low power.

Question: How can I implement the code i have with relays with pitch detection ?

Here is the code of FFT Goertzel frequency:

/*
  Basic frequency detection with Goertzel algorithm
  Reads electret mic on A0 and checks for a target frequency
  No FFT library, only math functions
*/

#include <Arduino.h>

const int micPin = A0;      // microphone analog pin
const float TARGET_FREQ = 2000.0;  // frequency to detect (Hz)
const int SAMPLE_RATE = 9000;     // Hz, how fast we sample
const int N = 128;                // number of samples per block

// Precomputed constants
float coeff;
float Q1, Q2;

void setup() {
  Serial.begin(115200);

  // precompute coefficient
  float omega = (2.0 * PI * TARGET_FREQ) / SAMPLE_RATE;
  coeff = 2.0 * cos(omega);

  Serial.println("Goertzel frequency detector ready.");
}

void loop() {
  float magnitude = detectFrequency();

  Serial.print("Magnitude at ");
  Serial.print(TARGET_FREQ);
  Serial.print(" Hz = ");
  Serial.println(magnitude);

  delay(100); // small pause
}

// Function to detect target frequency using Goertzel
float detectFrequency() {
  Q1 = 0;
  Q2 = 0;

  for (int i = 0; i < N; i++) {
    int sample = analogRead(micPin);   // 0–1023
    float x = sample - 512;            // center to 0

    float Q0 = coeff * Q1 - Q2 + x;
    Q2 = Q1;
    Q1 = Q0;

    delayMicroseconds(1000000 / SAMPLE_RATE); // control sampling rate
  }

  // magnitude calculation
  float magnitude = sqrt(Q1 * Q1 + Q2 * Q2 - Q1 * Q2 * coeff);
  return magnitude;
}

here is the code with relays:

// Teensy 3.x or 4.x sketch: two‑mic → two‑relay trigger

// ————— Pin assignments —————
const int micA_pin    = A0;    // Vibration sensor on speaker A
const int micB_pin    = A1;    // Vibration sensor on speaker B
const int relayA_pin  = 6;     // Relay for speaker A
const int relayB_pin  = 3;     // Relay for speaker B

// ————— Trigger parameters —————
const int  THRESHOLD     = 10;      // analogRead() threshold
const unsigned long COOLDOWN = 800;  // ms before retrigger
const unsigned long PULSE   = 200;   // ms relay stays ON

unsigned long lastA = 0, lastB = 0;       // last trigger times
unsigned long offTimeA = 0, offTimeB = 0; // times to turn relays OFF

// ————— Serial debug parameters —————
const unsigned long DBG_INTERVAL = 200; // ms between prints
unsigned long lastDbg = 0;

void setup() {
  Serial.begin(115200);
  while (!Serial);

  // inputs
  pinMode(micA_pin, INPUT);
  pinMode(micB_pin, INPUT);
  // outputs (assume active HIGH relay modules)
  pinMode(relayA_pin, OUTPUT);
  pinMode(relayB_pin, OUTPUT);
  digitalWrite(relayA_pin, LOW);
  digitalWrite(relayB_pin, LOW);

  Serial.println("=== Relay Trigger System ===");
}

void loop() {
  unsigned long now = millis();
  int lvlA = analogRead(micA_pin);
  int lvlB = analogRead(micB_pin);

  // 1) periodic debug
  if (now - lastDbg >= DBG_INTERVAL) {
    Serial.print("Mic A: "); Serial.print(lvlA);
    Serial.print(" | Mic B: "); Serial.println(lvlB);
    lastDbg = now;
  }

  if (lvlA > 30)
  {
    digitalWrite(relayA_pin, LOW);
    digitalWrite(relayB_pin, HIGH);
  }
  if (lvlB > 30)
  {
    digitalWrite(relayB_pin, LOW);
    digitalWrite(relayA_pin, HIGH);
  }

  delay(100);


  // // 2) turn relays off when pulse time expired
  // if (offTimeA && now >= offTimeA) {
  //   digitalWrite(relayA_pin, LOW);
  //   offTimeA = 0;
  //   Serial.println("Relay A OFF");
  // }
  // if (offTimeB && now >= offTimeB) {
  //   digitalWrite(relayB_pin, LOW);
  //   offTimeB = 0;
  //   Serial.println("Relay B OFF");
  // }

  // // 3) check triggers (with cooldown)
  // //    Mic A → Relay B
        // if (lvlA > THRESHOLD && (now - lastA) >= COOLDOWN) {
        //   Serial.println(">>> Trigger: Mic A → Relay B ON");
        //   digitalWrite(relayB_pin, HIGH);
        //   offTimeB = now + PULSE;
        //   lastA = now;
        // }
        // //    Mic B → Relay A
        // if (lvlB > THRESHOLD && (now - lastB) >= COOLDOWN) {
        //   Serial.println(">>> Trigger: Mic B → Relay A ON");
        //   digitalWrite(relayA_pin, HIGH);
        //   offTimeA = now + PULSE;
        //   lastB = ;
        // }now

  // // no delay() — loop runs freely
}

The FFT produces the amplitudes of tones in frequency bands between 0 and (sample frequency)/2 Hz.

It is possible, but not simple, to add up the amplitudes in frequency bands of your choice, and from the total decide for example whether human speech or certain tones are present.

It is important to make sure that the only frequencies present in the input sample are in the range of 0 to (sample frequency)/2, or the FFT results may be uninterpretable. Either use a low pass filter to restrict the input frequency range, or choose a sufficiently high sample frequency.

On Teensy you normally use the Teensy Audio Library.

It includes ready-made FFT analysis objects such as AudioAnalyzeFFT1024 and AudioAnalyzeFFT256 , which give you real-time spectral magnitudes

You can see their tool @ Audio System Design Tool for Teensy Audio Library

(the Tone module would be the Goertzel algorithm)

How about passing the output of the microphone through a high-pass filter, followed by a peak detector?

Goertzel algorithm only detects if a specific frequency is present in the input signal, if you need all the frequencies above a certain threshold you have to implement the FFT or use a high-pass filter.

If you are looking for the code of one of FFT algorithms, here is a C++ version of Cooley–Tukey FFT algorithm published in Cooley–Tukey FFT algorithm - Wikipedia

    template<typename T, size_t N>
    void fft (complex<T> (&output) [N], const complex<T> (&input) [N]) {
        static_assert((N > 0) && ((N & (N - 1)) == 0), "N must be a power of 2");

        // Calculate the number of stages to perform: 2^S = N
        int S = 0;
        for (int s = 1; s < N; s *= 2)
            S++;

        // Bit-reverse copy: output <- input
        for (size_t i = 0; i < N; i++) {
            size_t iTmp = i;
            size_t iReversed = 0;
            for (int s = 1; s <= S; s++) {
                iReversed = (iReversed << 1) | (iTmp & 0x01);
                iTmp >>= 1;
            }
            output [iReversed] = input [i];
        }

        // Go through all the stages
        for (int s = 1; s <= S; s++) {
            int m = 1 << s; // m = 2^s
            complex<T> omegam = exp (complex<T> (0, -2 * M_PI / m));
            for (size_t k = 0; k < N; k += m) {
                complex<T> omega = { 1.f, 0.f };
                for (size_t j = 0; j < m / 2; j++) {
                    complex<T> t = omega * output [k + j + m / 2];
                    complex<T> u = output [k + j];
                    output [k + j] = u + t;
                    output [k + j + m / 2] = u - t;
                    omega *= omegam;
                }
            }
        }
    }

Since FFT works on array of complex numbers you'll also need a complex class template (which is missing in Arduino):

            template<class T>
            class complex {
                private:
                    T _real_;
                    T _imag_;
                
                public:
                    // Constructor to initialize real and imag to 0
                    complex() : _real_ (0), _imag_ (0) {}
                    complex(T real, T imag) : _real_ (real), _imag_ (imag) {}
                
                    // Real and imag parts
                    inline T real () const __attribute__((always_inline)) { return _real_; }
                    inline T imag () const __attribute__((always_inline)) { return _imag_; }
                
                    // + operator
                    template<typename t>
                    friend complex operator + (const complex<T>& obj1, const complex<t>& obj2) {
                        return {obj1.real () + obj2.real (), obj1.imag () + obj2.imag ()};
                    }
                
                    // += operator
                    complex& operator += (const complex<T>& other) {
                        _real_ += other.real ();
                        _imag_ += other.imag ();
                        return *this;
                    }
                
                    // - operator
                    template<typename t>
                    friend complex operator - (const complex<T>& obj1, const complex<t>& obj2) {
                        return {obj1.real () - obj2.real (), obj1.imag () - obj2.imag ()};
                    }
                
                    // -= operator
                    complex& operator -= (const complex<T>& other) {
                        _real_ -= other.real ();
                        _imag_ -= other.imag ();
                        return *this;
                    }
                
                    // * operator
                    template<typename t>
                    friend complex operator * (const complex<T>& obj1, const complex<t>& obj2) {
                        return {obj1.real () * obj2.real () - obj1.imag () * obj2.imag (), obj1.imag () * obj2.real () + obj1.real () * obj2.imag ()};
                    }
                
                    // *= operator
                    complex& operator *= (const complex<T>& other) {
                        T r = real () * other.real () - imag () * other.imag ();
                        T i = imag () * other.real () + real () * other.imag ();
                        _real_ = r;
                        _imag_ = i;
                        return *this;
                    }
                
                    // / operator
                    template<typename t>
                    friend complex operator / (const complex<T>& obj1, const complex<t>& obj2) {
                        T tmp = obj2.real () * obj2.real () + obj2.imag () * obj2.imag ();
                        return {(obj1.real () * obj2.real () + obj1.imag () * obj2.imag ()) / tmp, (obj1.imag () * obj2.real () - obj1.real () * obj2.imag ()) / tmp};
                    }
                
                    // /= operator
                    complex& operator /= (const complex<T>& other) {
                        T tmp = other.real () * other.real () + other.imag () * other.imag ();
                        T r = (real () * other.real () + imag () * other.imag ()) / tmp;
                        T i = (imag () * other.real () - real () * other.imag ()) / tmp;
                        _real_ = r;
                        _imag_ = i;
                        return *this;
                    }
                
                    // conjugate function
                    constexpr complex conj () const { return {real (), -imag ()}; }
                
                    // print complex number to ostream
                    friend ostream& operator << (ostream& os, const complex& c) {
                        os << c.real () << '+' << c.imag () << 'i';
                        return os;
                    }
            };

            complex<float> exp (complex<float> z) {
                float exp_real = expf (z.real ());
                return { exp_real * cos (z.imag ()), exp_real * sin (z.imag ()) };
            }

            complex<double> exp (complex<double> z) {
                double exp_real = exp (z.real ());
                return { exp_real * cos (z.imag ()), exp_real * sin (z.imag ()) };
            }

            // replace abs #definition with function template that would also handle complex numbers
            #ifdef abs
                #undef abs

                template<typename T>
                T abs (T x) { return x > 0 ? x : -x; }

                float abs (const complex<float>& z) { return sqrt (z.real () * z.real () + z.imag () * z.imag ()); }

                double abs (const complex<double>& z) { return sqrt (z.real () * z.real () + z.imag () * z.imag ()); }
            #endif

The magnitudes are calculated from complex coefficients as magnitude [i] = abs (fftOutputFromFFT [i]);

If you need further instructions on how to do the FFT let me know.

Two Arduino libraries that are easy to use and relieve one of the need to use complex data types are arduinoFFT and the OpenMusicLabs FFT (the latter works only on AVR based Arduinos, like the Uno R3).

Start with the provided examples.

You can also look for a digital high-pass filter if you want. It may be easier to implement and more efficient than FFT.

As suggested by @J-M-L, the Teensy Audio Library is probably the easiest way to implement FFT, LPF, BPF, HPF, etc.