Skip to content

No microphone examples work on the M5Cardputer #11

Open

Description

@brandoncarl

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

Activity

  1. brandoncarl commented on Sep 9, 2025

    @brandoncarl
    Author
  2. Harrison-Xu commented on Sep 10, 2025

    @Harrison-Xu
    Member

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

    Please try this example in M5Stack docs.

  3. brandoncarl commented on Sep 10, 2025

    @brandoncarl
    Author

    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.

  4. brandoncarl commented on Sep 14, 2025

    @brandoncarl
    Author

    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();
      }
    }
  5. SCNya commented on Jan 24, 2026

    @SCNya

    This helped me

    #include "driver/gpio.h"  // Required for gpio_reset_pin
    
    // Function to switch to Microphone
    void switchToMic() {
        M5.Speaker.end();
        delay(50);
        
        // Critical: Reset I2S pins before switching
        gpio_reset_pin(GPIO_NUM_43);
        gpio_reset_pin(GPIO_NUM_46);
        
        delay(10);
        M5.Mic.begin();
        delay(100);  // Allow the microphone to stabilize
    }
    
    // Function to switch to Speaker
    void switchToSpeaker() {
        M5.Mic.end();
        delay(50);
        
        // Reset pins for the reverse switch as well
        gpio_reset_pin(GPIO_NUM_43);
        gpio_reset_pin(GPIO_NUM_46);
        
        delay(10);
        M5.Speaker.begin();
        delay(30);
    }
    
  6. dzy-star commented on Apr 25, 2026

    @dzy-star

    The reason why the microphone has no audio input is that ESP-IDF 5.5.x versions are incompatible; only IDF 5.4.x works properly.Correspondingly, the M5Stack board package version in Arduino needs to be downgraded to 3.2.x.

  7. chilang commented on May 14, 2026

    @chilang

    Note on hardware: this issue describes the original M5Cardputer v1.1 (PDM microphone). My findings below are for the Cardputer-Adv (ES8311 I²S codec) — different hardware, almost certainly different root cause. Posting here only because users hitting silent mic on either device often land here first.

    For Cardputer-Adv specifically: silent mic is caused by an ESP-IDF v5.5.1 regression in the I²S driver. Building against IDF v5.4.2 fixes it.

    If anyone here has confirmed silent-mic symptoms on the original Cardputer (PDM mic), that's a separate issue and the IDF-version workaround is unlikely to apply.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions