No microphone examples work on the M5Cardputer
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
Linked to https://github.com/m5stack/M5Unified/issues/184
https://docs.m5stack.com/en/arduino/m5cardputer/mic
Please try this example in M5Stack docs.
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.
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();
}
}