#include "audio.h" #include "app_state.h" #include "config.h" #include #include #include #include // --------------------------------------------------------------------------- // ES7210 4-channel mic ADC. Register sequence follows Espressif's es7210 // driver (esp-bsp/components/es7210), configured as plain I2S slave, // 16-bit, TDM off, so SDOUT1 carries MIC1 (left) and MIC2 (right). // --------------------------------------------------------------------------- static bool es7210Write(uint8_t reg, uint8_t val) { Wire.beginTransmission(ES7210_ADDR); Wire.write(reg); Wire.write(val); return Wire.endTransmission() == 0; } static bool es7210Init() { // MCLK = 256 x 16 kHz = 4.096 MHz. Clock coefficients from Espressif's table: // {4096000, 16000, ss_ds 0, adc_div 1, dll 1, doubler 1, osr 0x20, lrck 0x0100} const uint8_t gain = 0x10 | MIC_GAIN_STEP; const uint8_t seq[][2] = { {0x00, 0xFF}, {0x00, 0x32}, // software reset {0x09, 0x30}, {0x0A, 0x30}, // power-up timing {0x23, 0x2A}, {0x22, 0x0A}, // ADC1/2 high-pass filter {0x21, 0x2A}, {0x20, 0x0A}, // ADC3/4 high-pass filter {0x11, 0x60}, {0x12, 0x00}, // I2S format, 16-bit, TDM off {0x40, 0xC3}, // analog power, VMID {0x41, 0x70}, {0x42, 0x70}, // mic bias 2.87 V {0x43, gain}, {0x44, gain}, {0x45, gain}, {0x46, gain}, {0x47, 0x08}, {0x48, 0x08}, {0x49, 0x08}, {0x4A, 0x08}, // mic power {0x07, 0x20}, // OSR {0x02, 0xC1}, // adc_div=1 | doubler<<6 | dll<<7 {0x04, 0x01}, {0x05, 0x00}, // LRCK divider = 256 {0x06, 0x04}, // power down DLL {0x4B, 0x0F}, {0x4C, 0x0F}, // mic bias, ADC, PGA on {0x00, 0x71}, {0x00, 0x41}, // enable }; for (auto &rw : seq) { if (!es7210Write(rw[0], rw[1])) { Serial.printf("ES7210: I2C write 0x%02X failed\n", rw[0]); return false; } } return true; } // --------------------------------------------------------------------------- // Capture task: I2S -> mono PCM -> stream buffer. Runs at high priority and // never touches SPI, so a slow SD write can't make it miss samples. The // 64 KB stream buffer absorbs ~2 s of SD card latency. // --------------------------------------------------------------------------- static I2SClass i2s; static StreamBufferHandle_t pcmBuffer; static volatile bool capturing = false; static const size_t PCM_BUFFER_BYTES = 64 * 1024; // Per-microphone level statistics for audioLevelReport(). static volatile bool statsOn = false; static uint64_t statSumSq[3]; // MIC1, MIC2, recorded output static int32_t statPeak[3]; static uint32_t statFrames; static void audioTask(void *) { static int16_t stereo[512]; // 256 frames, L/R interleaved static int16_t mono[256]; float level = 0; const float gain = powf(10.0f, DIGITAL_GAIN_DB / 20.0f); for (;;) { size_t got = i2s.readBytes((char *)stereo, sizeof(stereo)); size_t frames = got / 4; if (frames == 0) continue; if (statsOn) { for (size_t i = 0; i < frames; i++) { for (int ch = 0; ch < 2; ch++) { int32_t v = stereo[2 * i + ch]; statSumSq[ch] += (int64_t)v * v; statPeak[ch] = max(statPeak[ch], abs(v)); } } statFrames += frames; } uint64_t sumSq = 0; for (size_t i = 0; i < frames; i++) { // Mix both mics, apply the digital gain, then a tanh soft limiter: nearly // linear for normal speech, rounding off peaks instead of hard clipping. float x = ((int32_t)stereo[2 * i] + (int32_t)stereo[2 * i + 1]) * 0.5f * gain; int32_t s = DIGITAL_GAIN_DB > 0 ? (int32_t)(32767.0f * tanhf(x / 32767.0f)) : (int32_t)x; mono[i] = (int16_t)s; if (statsOn) { statSumSq[2] += (int64_t)s * s; statPeak[2] = max(statPeak[2], abs(s)); } sumSq += (int64_t)s * s; } // Level meter: RMS mapped from -60..0 dBFS to 0..1, fast attack / slow decay. float rms = sqrtf((float)sumSq / frames); float db = 20.0f * log10f(rms / 32768.0f + 1e-9f); float target = constrain((db + 60.0f) / 60.0f, 0.0f, 1.0f); level = target > level ? target : level * 0.9f + target * 0.1f; app.micLevel = level; if (capturing) { size_t bytes = frames * 2; if (xStreamBufferSpacesAvailable(pcmBuffer) >= bytes) { xStreamBufferSend(pcmBuffer, mono, bytes, 0); } else { app.droppedBytes += bytes; } } } } void audioLevelReport(uint32_t ms) { for (int ch = 0; ch < 3; ch++) { statSumSq[ch] = 0; statPeak[ch] = 0; } statFrames = 0; statsOn = true; delay(ms); statsOn = false; delay(20); static const char *names[] = {"MIC1", "MIC2", "recorded"}; for (int ch = 0; ch < 3; ch++) { float rms = sqrtf((float)statSumSq[ch] / max(statFrames, (uint32_t)1)); Serial.printf("[audio] %-8s RMS %6.1f dBFS, peak %6.1f dBFS\n", names[ch], 20 * log10f(rms / 32768 + 1e-9f), 20 * log10f(statPeak[ch] / 32768.0f + 1e-9f)); } } bool audioBegin() { pinMode(PIN_PA_CTRL, OUTPUT); digitalWrite(PIN_PA_CTRL, LOW); // speaker amp off; we only record // Start I2S first: the ES7210 needs MCLK running while it powers up. i2s.setPins(PIN_I2S_BCLK, PIN_I2S_WS, -1 /* dout */, PIN_I2S_DIN, PIN_I2S_MCLK); if (!i2s.begin(I2S_MODE_STD, SAMPLE_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO)) { Serial.println("I2S begin failed"); return false; } Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL, 400000); if (!es7210Init()) return false; pcmBuffer = xStreamBufferCreate(PCM_BUFFER_BYTES, 1); if (!pcmBuffer) return false; xTaskCreate(audioTask, "audio", 4096, nullptr, configMAX_PRIORITIES - 2, nullptr); return true; } // --------------------------------------------------------------------------- // WAV recorder // --------------------------------------------------------------------------- static File wavFile; static uint32_t recStartMs = 0; static uint32_t lastFlushMs = 0; void wavHeader(uint8_t h[44], uint32_t dataBytes) { const uint32_t byteRate = SAMPLE_RATE * 2; auto u32 = [&](int o, uint32_t v) { h[o] = v; h[o + 1] = v >> 8; h[o + 2] = v >> 16; h[o + 3] = v >> 24; }; auto u16 = [&](int o, uint16_t v) { h[o] = v; h[o + 1] = v >> 8; }; memcpy(h, "RIFF", 4); u32(4, 36 + dataBytes); memcpy(h + 8, "WAVEfmt ", 8); u32(16, 16); // fmt chunk size u16(20, 1); // PCM u16(22, 1); // mono u32(24, SAMPLE_RATE); u32(28, byteRate); u16(32, 2); // block align u16(34, 16); // bits per sample memcpy(h + 36, "data", 4); u32(40, dataBytes); } static void writeWavHeader(File &f, uint32_t dataBytes) { uint8_t h[44]; wavHeader(h, dataBytes); f.seek(0); f.write(h, sizeof(h)); } bool recorderStart(const char *path) { { SpiLock lock; wavFile = SD.open(path, FILE_WRITE); if (!wavFile) return false; writeWavHeader(wavFile, 0); } xStreamBufferReset(pcmBuffer); app.recordedBytes = 0; app.droppedBytes = 0; recStartMs = lastFlushMs = millis(); capturing = true; return true; } bool recorderActive() { return capturing; } uint32_t recorderSeconds() { return app.recordedBytes / (SAMPLE_RATE * 2); } static void drain(bool all) { static uint8_t chunk[4096]; // While recording, write only full chunks (SD cards like 512-byte multiples) // and cap the work per call so the UI stays responsive. for (int i = 0; all || i < 8; i++) { size_t avail = xStreamBufferBytesAvailable(pcmBuffer); if (avail == 0 || (!all && avail < sizeof(chunk))) break; size_t n = xStreamBufferReceive(pcmBuffer, chunk, sizeof(chunk), 0); if (n == 0) break; SpiLock lock; wavFile.write(chunk, n); app.recordedBytes += n; } } void recorderService() { if (!capturing) return; drain(false); // Every 10 s, update the header's length fields and flush, so a long // recording interrupted by a power cut is still a valid, playable WAV. if (millis() - lastFlushMs > 10000) { SpiLock lock; writeWavHeader(wavFile, app.recordedBytes); wavFile.seek(44 + app.recordedBytes); wavFile.flush(); lastFlushMs = millis(); } } bool recorderStop() { if (!capturing) return false; capturing = false; delay(40); // let the capture task finish its current block drain(true); SpiLock lock; writeWavHeader(wavFile, app.recordedBytes); wavFile.close(); return true; }