Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 242 additions & 0 deletions moneo/Recorder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
#include "Recorder.h"
#include <time.h>

Recorder::Recorder()
: _psramBuf(nullptr), _psramWritten(0),
_dataLength(0), _recording(false),
_toggleRequested(false), _stopWriter(false),
_lastToggleTime(0), _segmentStartMs(0),
_captureTask_h(nullptr), _writerTask_h(nullptr),
_bufMutex(nullptr)
{}

bool Recorder::begin() {
if (!SD.begin(SD_CARD_PIN)) {
DLOG("[Recorder] SD card mount failed!");
return false;
}
DLOG("[Recorder] SD card mounted.");

_psramBuf = (uint8_t*)ps_malloc(PSRAM_BUFFER_SIZE);
if (!_psramBuf) {
DLOG("[Recorder] PSRAM allocation failed!");
return false;
}
DLOG("[Recorder] PSRAM buffer allocated.");

_bufMutex = xSemaphoreCreateMutex();
if (!_bufMutex) {
DLOG("[Recorder] Mutex creation failed!");
return false;
}

_i2s.setPinsPdmRx(I2S_BCLK_PIN, I2S_LRCLK_PIN);
if (!_i2s.begin(I2S_MODE_PDM_RX, SAMPLE_RATE,
I2S_DATA_BIT_WIDTH_8BIT, I2S_SLOT_MODE_MONO)) {
DLOG("[Recorder] I2S init failed!");
return false;
}
DLOG("[Recorder] I2S initialized (16kHz, 8-bit).");

DLOG("[Recorder] Ready. Touch pin to start.");
return true;
}

void Recorder::loop() {
if (!_toggleRequested) return;
_toggleRequested = false;

unsigned long now = millis();
if (now - _lastToggleTime < DEBOUNCE_DELAY) return;
_lastToggleTime = now;

if (!_recording) {
_startRecording();
} else {
_stopRecording();
}
}

void Recorder::requestToggle() {
_toggleRequested = true;
}

// ── Generate datetime filename ─────────────────────────────
String Recorder::_generateFilename() {
// Try to get real time from NTP if available
// Fall back to millis-based name if no time sync
struct tm timeinfo;
if (getLocalTime(&timeinfo, 1000)) {
char buf[32];
strftime(buf, sizeof(buf), "/rec_%Y%m%d_%H%M%S.wav", &timeinfo);
return String(buf);
}

// Fallback: use millis
unsigned long ms = millis();
unsigned long secs = ms / 1000;
char buf[32];
snprintf(buf, sizeof(buf), "/rec_%05lu.wav", secs);
return String(buf);
}

// ── WAV header ─────────────────────────────────────────────
void Recorder::_writeWavHeader(File& f, uint32_t dataLen) {
uint32_t sampleRate = SAMPLE_RATE;
uint16_t bitsPerSamp = BITS_PER_SAMPLE;
uint16_t numChan = NUM_CHANNELS;
uint32_t byteRate = sampleRate * numChan * bitsPerSamp / 8;
uint16_t blockAlign = numChan * bitsPerSamp / 8;
uint32_t chunkSize = dataLen + 36;
uint32_t subChunk1 = 16;
uint16_t audioFormat = 1;

f.seek(0);
f.write((const uint8_t*)"RIFF", 4);
f.write((uint8_t*)&chunkSize, 4);
f.write((const uint8_t*)"WAVE", 4);
f.write((const uint8_t*)"fmt ", 4);
f.write((uint8_t*)&subChunk1, 4);
f.write((uint8_t*)&audioFormat, 2);
f.write((uint8_t*)&numChan, 2);
f.write((uint8_t*)&sampleRate, 4);
f.write((uint8_t*)&byteRate, 4);
f.write((uint8_t*)&blockAlign, 2);
f.write((uint8_t*)&bitsPerSamp, 2);
f.write((const uint8_t*)"data", 4);
f.write((uint8_t*)&dataLen, 4);
}

// ── Start ──────────────────────────────────────────────────
void Recorder::_startRecording() {
DLOG("[Recorder] Starting...");
_recording = true;
_stopWriter = false;
_dataLength = 0;
_psramWritten = 0;

// Try NTP time sync
configTime(19800, 0, "pool.ntp.org"); // UTC+5:30 for IST
delay(500);

_wavPath = _generateFilename();
DLOGF("[Recorder] File: %s\n", _wavPath.c_str());

_wavFile = SD.open(_wavPath.c_str(), FILE_WRITE);
if (!_wavFile) {
DLOG("[Recorder] Cannot create WAV file!");
_recording = false;
return;
}

// Write placeholder header (updated on stop)
_writeWavHeader(_wavFile, 0);

_segmentStartMs = millis();
digitalWrite(LED_BUILTIN, HIGH);

xTaskCreatePinnedToCore(_captureTaskEntry, "Capture", 4096,
this, 5, &_captureTask_h, 1);
xTaskCreatePinnedToCore(_writerTaskEntry, "Writer", 8192,
this, 3, &_writerTask_h, 1);

DLOG("[Recorder] Recording started.");
}

// ── Stop ───────────────────────────────────────────────────
void Recorder::_stopRecording() {
DLOG("[Recorder] Stopping...");
_recording = false;
_stopWriter = true;

digitalWrite(LED_BUILTIN, LOW);

// Give tasks time to finish naturally
vTaskDelay(pdMS_TO_TICKS(3000));

_captureTask_h = nullptr;
_writerTask_h = nullptr;

// Flush remaining PSRAM buffer to SD
if (_psramWritten > 0 && _wavFile) {
_wavFile.write(_psramBuf, _psramWritten);
_dataLength += _psramWritten;
_psramWritten = 0;
DLOGF("[Recorder] Final flush: %u bytes\n", _dataLength);
}

// Finalize WAV header
if (_wavFile) {
_wavFile.seek(0);
_writeWavHeader(_wavFile, _dataLength);
_wavFile.close();
DLOGF("[Recorder] WAV saved: %s (%u bytes)\n",
_wavPath.c_str(), _dataLength);
}

DLOG("[Recorder] Stopped.");
}

// ── Capture task ───────────────────────────────────────────
void Recorder::_captureTaskEntry(void* arg) {
((Recorder*)arg)->_captureTask();
}

void Recorder::_captureTask() {
uint8_t buf[I2S_BUFFER_SIZE];
DLOG("[Capture] Task started.");

while (_recording) {
int avail = _i2s.available();
if (avail > 0) {
int n = _i2s.readBytes((char*)buf, min(avail, I2S_BUFFER_SIZE));
if (n > 0) {
xSemaphoreTake(_bufMutex, portMAX_DELAY);
size_t space = PSRAM_BUFFER_SIZE - _psramWritten;
size_t toWrite = min((size_t)n, space);
if (toWrite > 0) {
memcpy(_psramBuf + _psramWritten, buf, toWrite);
_psramWritten += toWrite;
}
xSemaphoreGive(_bufMutex);
}
}
taskYIELD();
}

DLOG("[Capture] Task finished.");
vTaskDelete(nullptr);
}

// ── Writer task ────────────────────────────────────────────
void Recorder::_writerTaskEntry(void* arg) {
((Recorder*)arg)->_writerTask();
}

void Recorder::_writerTask() {
DLOG("[Writer] Task started.");

while (!_stopWriter) {
// Every SEGMENT_DURATION_MS, flush PSRAM buffer to SD
if (millis() - _segmentStartMs >= SEGMENT_DURATION_MS) {
xSemaphoreTake(_bufMutex, portMAX_DELAY);

if (_psramWritten > 0 && _wavFile) {
size_t written = _psramWritten;
_wavFile.write(_psramBuf, written);
_dataLength += written;
_psramWritten = 0;
DLOGF("[Writer] Flushed %u bytes to WAV (total: %u)\n",
written, _dataLength);
}

xSemaphoreGive(_bufMutex);
_segmentStartMs = millis();
}

vTaskDelay(pdMS_TO_TICKS(100));
}

DLOG("[Writer] Task finished.");
vTaskDelete(nullptr);
}
68 changes: 68 additions & 0 deletions moneo/Recorder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#ifndef Recorder_h
#define Recorder_h

#include <Arduino.h>
#include <ESP_I2S.h>
#include <SD.h>
#include <FS.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "Config.h"

// ============================================================
// Recorder — Records audio into a SINGLE WAV file.
//
// Flow:
// Touch start → create dated WAV file
// Every 10s: flush PSRAM buffer segment to WAV file
// Touch stop → finalize WAV header → send to AI
//
// The WAV file grows continuously — one file per session.
// No chunk files. No gateway. File sent directly to AI when done.
// ============================================================

class Recorder {
public:
Recorder();
bool begin();
void loop();
void requestToggle();

bool isRecording() const { return _recording; }
String lastRecordingPath() const { return _wavPath; }

private:
void _startRecording();
void _stopRecording();
void _writeWavHeader(File& f, uint32_t dataLen);
String _generateFilename();

static void _captureTaskEntry(void* arg);
static void _writerTaskEntry(void* arg);
void _captureTask();
void _writerTask();

I2SClass _i2s;

// PSRAM capture buffer: filled by the capture task, drained by the
// writer task every segment. Access is guarded by _bufMutex.
uint8_t* _psramBuf;
size_t _psramWritten;

File _wavFile;
String _wavPath;
uint32_t _dataLength;

volatile bool _recording;
volatile bool _toggleRequested;
volatile bool _stopWriter;
unsigned long _lastToggleTime;
unsigned long _segmentStartMs;

TaskHandle_t _captureTask_h;
TaskHandle_t _writerTask_h;

SemaphoreHandle_t _bufMutex;
};

#endif