M5Cardputer icon indicating copy to clipboard operation
M5Cardputer copied to clipboard

No microphone examples work on the M5Cardputer

Open brandoncarl opened this issue 5 months ago • 4 comments

Issue

  • None of the provided examples capture any audio via the microphone.
  • The code compiles and uploads successfully, but no waveforms are detected nor played back.
  • The device is working: the M5Cardputer demo successfully captures audio via the microphone and plays it back.

Expected Behavior

The microphone should capture audio, which should display via the waveform.

Details

Device

  • M5Cardputer v1.1 with M5Stamp S3A
  • Arduino IDE v2.3.6

Libraries

  • M5Cardputer v1.1
  • M5Unified v0.2.8
  • M5GFX v0.2.11

Demos

  • M5Cardputer/Basic/mic
  • M5Cardputer/Basic/mic_wav_rec
  • M5Unified/Basic/Microphone
  • M5Unified/Basic/Mic_FFT

brandoncarl avatar Sep 09 '25 11:09 brandoncarl

Linked to https://github.com/m5stack/M5Unified/issues/184

brandoncarl avatar Sep 09 '25 11:09 brandoncarl

https://docs.m5stack.com/en/arduino/m5cardputer/mic

Please try this example in M5Stack docs.

Harrison-Xu avatar Sep 10 '25 01:09 Harrison-Xu

Hi Harrison. I tried that example. It compiles, uploads and displays on the machine.

However, no audio is recorded nor played back. During "🔴 REC" there is a constant flat line. During "▶️ PLAY" nothing comes out.

brandoncarl avatar Sep 10 '25 11:09 brandoncarl

I was not able to get the demos to work, but was successful in creating this MicStream class. Up to you if you'd like to close.

// MicStream.h
#pragma once
#include <Arduino.h>
#include <M5Cardputer.h>
#include <esp_heap_caps.h>

class MicStream : public Stream {
public:

  /* ──────────────────────────────────────────────────────────────────────────────
    Helpers
  ────────────────────────────────────────────────────────────────────────────── */
  using mic_config_t = decltype(M5.Mic.config());
  void debug(size_t frameSize = 128, int step = 20);

  /* ──────────────────────────────────────────────────────────────────────────────
    Lifecycle
  ────────────────────────────────────────────────────────────────────────────── */
  MicStream();
  ~MicStream();
  bool begin();
  void update();
  void end();
  void configure(const mic_config_t* config = nullptr); 
  void stop();  // Lifecycle safe: stops microphone recording but keep buffer available for consumers.

  /* ──────────────────────────────────────────────────────────────────────────────
    Stream API
  ────────────────────────────────────────────────────────────────────────────── */
  int     available() override;                                // bytes available to read
  int     read() override;                                     // byte-wise read
  int     peek() override;                                     // non-consuming peek
  void    flush() override {}                                  // no-op for input stream
  size_t  readBytes(uint8_t* dst, size_t len) override;        // bulk read
  size_t  write(uint8_t) override { return 0; }                // read-only

private:

  /* ──────────────────────────────────────────────────────────────────────────────
    Audio Constants
  ────────────────────────────────────────────────────────────────────────────── */
  static constexpr uint32_t SAMPLE_RATE           = 16000;
  static constexpr uint8_t  BITS_PER_SAMPLE       = 16;
  static constexpr uint8_t  NUM_CHANNELS          = 1;
  static constexpr uint8_t  SAMPLE_BYTES          = (BITS_PER_SAMPLE / 8) * NUM_CHANNELS;
  static constexpr const size_t SAMPLES_PER_CHUNK = 320;
  static constexpr const size_t TOTAL_CHUNKS      = 100;
  static constexpr const size_t TOTAL_SAMPLES     = TOTAL_CHUNKS * SAMPLES_PER_CHUNK;

  /* ──────────────────────────────────────────────────────────────────────────────
    Buffer
  ────────────────────────────────────────────────────────────────────────────── */
  int16_t* buffer;                 // ring buffer of TOTAL_SAMPLES samples
  inline int16_t* ptr(size_t idx) { return &buffer[idx * SAMPLES_PER_CHUNK]; }
  void allocate();
  void free(bool force = false);
  
  // Ring buffer state (chunk granularity)
  size_t head = 0;         // chunk index to read from
  size_t tail = 0;         // chunk index to write to
  size_t filledChunks = 0; // number of completed chunks available to read
  size_t readOffset = 0;   // byte offset into head chunk for next read

  // Returns true when there is no more recorded data (filledChunks==0) and mic is not recording.
  bool isDrained() const;

};
// MicStream.cpp
#include "MicStream.h"

/* ──────────────────────────────────────────────────────────────────────────────
   Lifecycle
────────────────────────────────────────────────────────────────────────────── */

MicStream::MicStream() {
  buffer = nullptr;
  head = tail = filledChunks = 0;
  readOffset = 0;
}

MicStream::~MicStream() {
  // Force free in destructor to ensure buffer memory is released.
  free(true);
}

bool MicStream::begin() {
  allocate();
  configure();

  // Start mic
  if (!M5.Mic.begin()) {
    Serial.println("[MicStream] Mic Error");
    return false;
  }

  // Prime the first recording into tail
  if (!M5.Mic.record(ptr(tail), SAMPLES_PER_CHUNK, SAMPLE_RATE, NUM_CHANNELS == 2)) {
    Serial.println("[MicStream] Failed to start initial recording");
  }

  return true;
}

void MicStream::end() {
  stop();
  // Default behavior: do not force free to avoid destroying buffer while consumer may be reading.
  free(false);
}

void MicStream::stop() {
  // Stop the hardware recording; the last chunk may still be being written
  if (M5.Mic.isRecording()) {
    while (M5.Mic.isRecording()) delay(1);
    M5.Mic.end();
  }
}

void MicStream::configure(const mic_config_t* config) {
  if (!config) {
    // Default config
    auto cfg = M5.Mic.config();
    M5.Mic.config(cfg);
  } else {
    M5.Mic.config(*config);
  }
}

void MicStream::update() {

  // Use hardware recording state only. Detect falling edge of isRecording()
  static bool wasRecording = false;
  bool isRecording = M5.Mic.isRecording();

  // On falling edge (chunk finished) advance tail and mark filled
  if (wasRecording && !isRecording) {
    filledChunks = min(filledChunks + 1, TOTAL_CHUNKS);
    tail = (tail + 1) % TOTAL_CHUNKS;

    // Do not overwrite unread data
    if (filledChunks < TOTAL_CHUNKS) {
      if (!M5.Mic.record(ptr(tail), SAMPLES_PER_CHUNK, SAMPLE_RATE, NUM_CHANNELS == 2)) {
        Serial.println("[MicStream] Failed to start recording");
      }
    } else {
      // Buffer full: skip starting new recording until consumer frees chunks
      Serial.println("[MicStream] Buffer full, dropping incoming audio");
    }
  }

  wasRecording = isRecording;

}


/* ──────────────────────────────────────────────────────────────────────────────
   Buffer Management
────────────────────────────────────────────────────────────────────────────── */

// free(bool): if force==false then only free when drained; otherwise free immediately
void MicStream::free(bool force) {
  if (!force) {
    if (!isDrained()) return;
  }
  if (buffer) {
    heap_caps_free(buffer);
    buffer = nullptr;
  }
}

bool MicStream::isDrained() const {
  return (filledChunks == 0) && (!M5.Mic.isRecording());
}


/* ──────────────────────────────────────────────────────────────────────────────
   Stream API
────────────────────────────────────────────────────────────────────────────── */

int MicStream::available() {
  // Return available bytes (filled chunks * samples per chunk - readOffset)
  size_t availableSamples = filledChunks * SAMPLES_PER_CHUNK;
  size_t availableBytes = availableSamples * SAMPLE_BYTES;
  if (readOffset >= availableBytes) return 0;
  return (int)(availableBytes - readOffset);
}

int MicStream::read() {
  uint8_t byte = 0;
  uint8_t* base = (uint8_t*)buffer;
  if (filledChunks == 0) return -1;

  size_t absoluteReadIndex = (head * SAMPLES_PER_CHUNK * SAMPLE_BYTES) + readOffset;
  byte = base[absoluteReadIndex % (TOTAL_SAMPLES * SAMPLE_BYTES)];
  readOffset += 1;

  // If we've consumed a whole chunk, advance head and decrease filledChunks
  if (readOffset >= SAMPLES_PER_CHUNK * SAMPLE_BYTES) {
    readOffset = 0;
    head = (head + 1) % TOTAL_CHUNKS;
    if (filledChunks > 0) filledChunks -= 1;
  }

  return byte;
}

int MicStream::peek() {
  if (filledChunks == 0) return -1;
  uint8_t* base = (uint8_t*)buffer;
  size_t absoluteReadIndex = (head * SAMPLES_PER_CHUNK * SAMPLE_BYTES) + readOffset;
  return base[absoluteReadIndex % (TOTAL_SAMPLES * SAMPLE_BYTES)];
}

size_t MicStream::readBytes(uint8_t* dst, size_t len) {
  size_t bytesRead = 0;
  uint8_t* base = (uint8_t*)buffer;
  size_t totalBytesAvailable = filledChunks * SAMPLES_PER_CHUNK * SAMPLE_BYTES;
  size_t toRead = min(len, totalBytesAvailable - readOffset);

  while (bytesRead < toRead) {
    size_t absoluteReadIndex = (head * SAMPLES_PER_CHUNK * SAMPLE_BYTES) + readOffset;
    size_t chunkPos = absoluteReadIndex % (TOTAL_SAMPLES * SAMPLE_BYTES);
    size_t canCopy = min(toRead - bytesRead, (size_t)(TOTAL_SAMPLES * SAMPLE_BYTES - chunkPos));
    memcpy(dst + bytesRead, base + chunkPos, canCopy);
    bytesRead += canCopy;
    readOffset += canCopy;

    if (readOffset >= SAMPLES_PER_CHUNK * SAMPLE_BYTES) {
      readOffset = 0;
      head = (head + 1) % TOTAL_CHUNKS;
      if (filledChunks > 0) filledChunks -= 1;
    }
  }

  return bytesRead;
}


/* ──────────────────────────────────────────────────────────────────────────────
  Buffer
────────────────────────────────────────────────────────────────────────────── */

void MicStream::allocate() {
  if (!buffer) {
    buffer = (int16_t*)heap_caps_malloc(TOTAL_SAMPLES * SAMPLE_BYTES, MALLOC_CAP_8BIT);
    if (!buffer) {
      Serial.println("[ERROR] MicStream: Buffer allocation failed");
    }
  }
}

void MicStream::debug(size_t frameSize, int step) {

  // Static variables to maintain state across calls
  static size_t _frameSize = -1;
  static uint8_t* buf = nullptr;

  // Check if frameSize is being set for the first time
  if (_frameSize == -1) {
    _frameSize = frameSize;
    buf = new uint8_t[frameSize]; // Allocate buffer dynamically
  } else if (_frameSize != frameSize) {
    Serial.println("[ERROR] MicStream::debug: frameSize mismatch");
    return;
  }

  size_t got = 0;
  while (got < frameSize) {
    M5Cardputer.update();
    update();
    int avail = available();
    if (avail <= 0) {
      // No data yet — yield a tiny amount to avoid busy spinning
      delay(1);
      continue;
    }
    size_t need = frameSize - got;
    size_t toRead = min((size_t)avail, need);
    size_t r = readBytes(buf + got, toRead);
    got += r;
  }

  static int loopCounter = 0;
  loopCounter++;

  if (loopCounter >= step) {
    loopCounter = 0;

    // Compute simple diagnostics: RMS, peak, dBFS and a running noise floor.
    int16_t* samples = (int16_t*)buf;
    int sampleCount = frameSize / 2;

    double sumSq = 0.0;
    int32_t peak = 0;
    for (int i = 0; i < sampleCount; ++i) {
      int32_t v = samples[i];
      int32_t av = abs(v);
      if (av > peak) peak = av;
      sumSq += (double)v * (double)v;
    }
    double meanSq = sumSq / (double)sampleCount;
    double rms = sqrt(meanSq);

    // dB relative to full scale
    double dbfs = 20.0 * log10(max(rms, 1e-12) / 32767.0);

    // Smooth an estimate of the noise floor (RMS) and convert to dB
    static double noiseRms = 0.0;
    const double noiseAlpha = 0.01; // smoothing factor (lower = slower)
    if (noiseRms == 0.0) noiseRms = rms;
    noiseRms = (1.0 - noiseAlpha) * noiseRms + noiseAlpha * rms;
    double noiseDb = 20.0 * log10(max(noiseRms, 1e-12) / 32767.0);

    // Hysteresis-based detection: require louder threshold to enter signal state,
    // lower threshold to stay in it (reduces chatter).
    static bool inSignal = false;
    const double enterDb = 6.0; // dB above noise to consider "signal"
    const double stayDb = 3.0;  // dB above noise to remain in signal
    if (!inSignal) {
      inSignal = (dbfs > (noiseDb + enterDb));
    } else {
      inSignal = (dbfs > (noiseDb + stayDb));
    }

    // Small VU bar
    int vu = (int)round((dbfs + 90.0) / 90.0 * 30.0);
    if (vu < 0) vu = 0; if (vu > 30) vu = 30;

    for (int i = 0; i < vu; ++i) Serial.print('#');
    for (int i = vu; i < 30; ++i) Serial.print(' ');
    Serial.print(" ");
    Serial.print(inSignal ? "SIGNAL " : "silence");
    Serial.print(" ");

    Serial.print("RMS="); Serial.print(rms, 1);
    Serial.print(" Peak="); Serial.print(peak);
    Serial.print(" dBFS="); Serial.print(dbfs, 1);
    Serial.print(" noiseDb="); Serial.print(noiseDb, 1);

    // Show first few raw bytes (hex) for low-level tests
    Serial.print("  Bytes=");
    int showBytes = min((int)frameSize, 6);
    for (int i = 0; i < showBytes; ++i) {
      uint8_t b = buf[i];
      if (b < 16) Serial.print('0');
      Serial.print(b, HEX);
      if (i < showBytes - 1) Serial.print(' ');
    }

    // Print every 10th sample (up to 8 samples) for a quick sanity check
    int maxPrint = 8;
    int availableStride = sampleCount / 10;
    int toShow = min(maxPrint, max(1, availableStride));
    Serial.print("  Samples=");
    for (int i = 0; i < toShow; ++i) {
      int idx = i * 10;
      if (idx >= sampleCount) break;
      Serial.print(' ');
      Serial.print(samples[idx]);
    }

    Serial.println();
  }
}

brandoncarl avatar Sep 14 '25 13:09 brandoncarl