XIAO MG24 sense to detect a beep of a specific frequency

Hello,
I would like to detect a beep of a specific frequency using the MSM381ACT001 microphone on the Seeed Studio XIAO MG24. Do you think this is technically possible with the component?

Regards

interesting project

yes
 I believe the hardware would be capable of this task
 the proof is in the software

Hi there,

I would say yes, but only given certain parameters. What is the Frequency you want to detect? WIll it be a constant amplitude and close up or FAR away (these mic’s don’t do long range vs, fidelity :grin:

Given a constant tone or even a BEEP pulse, and constant distance to the source at a fixed volume level. VERY doable.

There is an Arduino Example MIC , with a TFFT display. look at that example and others to get a feel on how the mic performs in YOUR situation. to get the most reliable and repeatable outcome.

There is a algorithm ro filter called a “Goertzel” it’s what you use for this type of detection. O(N) per block, tiny RAM/CPU, Tunable: just change
𝑓0 sample rate, or frame length, it is very Robust under noise with proper thresholds. it is a "a digital signal processing technique for efficiently calculating a specific frequency component of a Discrete Fourier Transform (DFT) "

https://docs.arduino.cc/libraries/goertzel/

check the basics here;

and here is a basic starting point that compiles AOK :+1:

// ============================================================
// XIAO MG24: Beep Detector (Goertzel) for analog MEMS mic
// Revision: 1.0 (2025-09-29)
// Target tone: 2000 Hz @ 8 kHz sample rate
// ============================================================

#include <Arduino.h>

// ---- User config ----
static const uint8_t  MIC_PIN        = A0;     // Your ADC pin
static const float    FS             = 8000.0; // Sample rate (Hz)
static const uint16_t N              = 205;    // Samples per frame (~25.6 ms)
static const float    F0             = 2000.0; // Target frequency (Hz)
static const uint8_t  FRAMES_HYST    = 3;      // Require this many consecutive "hits"
static const float    DETECT_MULT    = 8.0;    // Threshold = noise_floor * DETECT_MULT


// ---- Derived Goertzel coeff ----
static float coeff;
static float omega;
static float sine_;
static float cosine_;

// ---- State ----
static float noise_floor = 0.0f;
static uint8_t consecutive_hits = 0;

// Optional: simple DC removal (high-pass) parameters
static const float HP_ALPHA = 0.995f; // closer to 1.0 => more aggressive DC tracking
static float dc_est = 0.0f;

inline float goertzel_run(const int16_t *x, uint16_t len) {
  float s_prev = 0.0f;
  float s_prev2 = 0.0f;

  for (uint16_t i = 0; i < len; i++) {
    float s = x[i] + coeff * s_prev - s_prev2;
    s_prev2 = s_prev;
    s_prev  = s;
  }

  float real = s_prev - s_prev2 * cosine_;
  float imag = s_prev2 * sine_;
  return real * real + imag * imag; // power at F0
}

void setup_goertzel() {
  // k = round(N * f0 / fs)
  float k = roundf((N * F0) / FS);
  omega   = (2.0f * PI * k) / N;
  sine_   = sinf(omega);
  cosine_ = cosf(omega);
  coeff   = 2.0f * cosine_;
}

void calibrate_noise_floor(uint16_t frames = 30) {
  Serial.println(F("[Cal] Measuring noise floor..."));
  int16_t buf[N];

  float acc = 0.0f;
  for (uint16_t f = 0; f < frames; f++) {
    // collect a frame
    uint32_t t0 = micros();
    for (uint16_t i = 0; i < N; i++) {
      // hold precise sampling interval
      uint32_t target = t0 + (uint32_t)((i * (1000000.0f / FS)));
      while ((int32_t)(micros() - target) < 0) { /* wait */ }

      int raw = analogRead(MIC_PIN); // 12-bit typical
      // map to signed centered around mid-scale (assume ~2048 mid)
      // You can tweak midpoint if you know your bias exactly.
      int16_t s = (int16_t)raw - 2048;

      // DC removal (simple one-pole)
      dc_est = HP_ALPHA * dc_est + (1.0f - HP_ALPHA) * s;
      float s_hp = s - dc_est;

      buf[i] = (int16_t)s_hp; // store filtered
    }
    float p = goertzel_run(buf, N);
    acc += p;
  }
  noise_floor = (acc / frames);
  Serial.print(F("[Cal] Noise floor (power): "));
  Serial.println(noise_floor, 2);
}

void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println(F("\n=== XIAO MG24 Beep Detector (Goertzel) ==="));
  Serial.println(F("Mic: MSM381ACT001 -> A0, fs=8kHz, N=205, f0=2kHz"));

  analogReadResolution(12); // If supported by MG24 core (usually 12 bits)
  // analogReference(...) // set if your core supports and you need a known Vref

  setup_goertzel();
  calibrate_noise_floor(30);
}

void loop() {
  static int16_t buf[N];

  // Collect one frame at ~exact FS using busy-wait timing
  uint32_t t0 = micros();
  for (uint16_t i = 0; i < N; i++) {
    uint32_t target = t0 + (uint32_t)((i * (1000000.0f / FS)));
    while ((int32_t)(micros() - target) < 0) { /* wait */ }

    int raw = analogRead(MIC_PIN);
    int16_t s = (int16_t)raw - 2048;

    // DC removal
    dc_est = HP_ALPHA * dc_est + (1.0f - HP_ALPHA) * s;
    float s_hp = s - dc_est;

    buf[i] = (int16_t)s_hp;
  }

  // Compute power at F0
  float power_f0 = goertzel_run(buf, N);

  // Simple neighbor guard (optional): compute at F0+Δf to ensure narrowband tone
  // Δf ~ FS/N; here ~39 Hz. We can skip for first pass to keep CPU low.

  // Detection
  float threshold = max(noise_floor * DETECT_MULT, noise_floor + 1.0f);
  bool hit = (power_f0 > threshold);

  // Debounce with consecutive frames
  if (hit) {
    consecutive_hits++;
  } else if (consecutive_hits > 0) {
    consecutive_hits--;
  }

  bool detected = (consecutive_hits >= FRAMES_HYST);

  // Print a light debug stream
  static uint32_t lastPrint = 0;
  if (millis() - lastPrint > 200) {
    lastPrint = millis();
    Serial.print(F("P:"));
    Serial.print(power_f0, 1);
    Serial.print(F("  Th:"));
    Serial.print(threshold, 1);
    Serial.print(F("  NF:"));
    Serial.print(noise_floor, 1);
    Serial.print(F("  Hits:"));
    Serial.print(consecutive_hits);
    Serial.print(F("  "));
    if (detected) Serial.print(F("DETECTED"));
    Serial.println();
  }

  // Optional: periodically refresh noise floor when not detecting
  static uint32_t lastRecal = 0;
  if (!detected && (millis() - lastRecal > 5000)) {
    lastRecal = millis();
    // compute quick running noise estimate from last power
    // (conservative: exponential average)
    noise_floor = 0.95f * noise_floor + 0.05f * power_f0;
  }
}

here is a basic starting point with some suggestions as well.

This will:

  • Sample A0 at ~8 kHz using a tight timing loop (good enough to start).

  • Detect a 2 kHz beep.

  • Print rolling magnitudes and a clean “DETECTED” line when the tone is present.

  • Includes a quick noise-floor calibration at boot.

  • If the mic is very quiet or your tone is far away, you’ll need gain and a clean bias. Don’t expect miracles from a raw high-impedance feed.

  • Timing with a busy-wait loop is “good enough” to prove it works; for production, move sampling to a hardware timer + ADC DMA to get rock-solid FS and free CPU.

  • If the beep can drift ± a few percent, two choices

  • Widen the detection by testing two or three close bins (F0−Δ, F0, F0+Δ) and summing power

  • lower level and loose a little f S/N

The hardware can do it. you can use the serial plotter to see if it’s working also and use your cell phone to generate the tone burst.

HTH

GL :slight_smile: PJ :v:

2 Likes

I would like to detect the frequency of 4 kHz.
Thank you, I’ll look into all that.

1 Like

Hi there,

Ok, So that is well in the range, change the F0 to 4000, you tweak it depending on the waveform your going after, frames and samples per frame are the fine tuning dials
 :grin: :+1:

HTH
GL :slight_smile: PJ :v:

2 Likes

When I was working on voice detection, I used fft to make frequency buckets from audio samples.
You can assign frequency buckets value as a triggering point in code to take actions when that tone has sufficient amplitude.

1 Like