Merge pull request 'feat(plip): I2S RX mic capture + POST /voice/capture (STT input leg) + ES8388 ADC register fix' (#17) from feat/plip-mic-capture into main
CI / platformio (push) Failing after 4m20s
CI / platformio (push) Failing after 4m20s
This commit was merged in pull request #17.
This commit is contained in:
+312
-7
@@ -18,11 +18,13 @@
|
||||
#include "board_config.h"
|
||||
#include "es8388.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_spiffs.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
@@ -63,6 +65,7 @@ typedef struct {
|
||||
|
||||
static QueueHandle_t s_queue;
|
||||
static i2s_chan_handle_t s_spk_handle = NULL;
|
||||
static i2s_chan_handle_t s_mic_handle = NULL;
|
||||
static volatile bool s_stop_req = false;
|
||||
static bool s_sd_mounted = false;
|
||||
static bool s_spiffs_mounted = false;
|
||||
@@ -359,17 +362,17 @@ esp_err_t audio_init(void)
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* 2. Create I2S channels (TX = speaker, RX = mic). */
|
||||
/* 2. Create I2S channels (TX = speaker, RX = mic — full-duplex pair). */
|
||||
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(PLIP_I2S_NUM,
|
||||
I2S_ROLE_MASTER);
|
||||
chan_cfg.auto_clear = true;
|
||||
ret = i2s_new_channel(&chan_cfg, &s_spk_handle, NULL);
|
||||
ret = i2s_new_channel(&chan_cfg, &s_spk_handle, &s_mic_handle);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "i2s_new_channel: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* 3. Std mode: Philips 16-bit stereo @ 16 kHz. */
|
||||
/* 3a. TX (speaker): Philips 16-bit stereo @ 16 kHz. */
|
||||
i2s_std_config_t std_cfg = {
|
||||
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
|
||||
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(
|
||||
@@ -379,7 +382,7 @@ esp_err_t audio_init(void)
|
||||
.bclk = PLIP_I2S_BCLK,
|
||||
.ws = PLIP_I2S_WS,
|
||||
.dout = PLIP_I2S_DOUT,
|
||||
.din = I2S_GPIO_UNUSED, /* mic handled separately if needed */
|
||||
.din = I2S_GPIO_UNUSED,
|
||||
.invert_flags = {
|
||||
.mclk_inv = false,
|
||||
.bclk_inv = false,
|
||||
@@ -389,14 +392,78 @@ esp_err_t audio_init(void)
|
||||
};
|
||||
ret = i2s_channel_init_std_mode(s_spk_handle, &std_cfg);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "i2s_channel_init_std_mode: %s", esp_err_to_name(ret));
|
||||
ESP_LOGE(TAG, "i2s_channel_init_std_mode (TX): %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* 3b. RX (mic): same GPIO config as TX except DIN=GPIO35, DOUT=UNUSED.
|
||||
* In full-duplex std mode, IDF requires the same BCLK/WS GPIOs to be
|
||||
* specified in both TX and RX configs — it deduplicates GPIO matrix
|
||||
* routing internally. Using UNUSED for BCLK/WS causes zero-data RX. */
|
||||
i2s_std_config_t rx_cfg = {
|
||||
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
|
||||
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(
|
||||
I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
|
||||
.gpio_cfg = {
|
||||
.mclk = PLIP_I2S_MCLK,
|
||||
.bclk = PLIP_I2S_BCLK,
|
||||
.ws = PLIP_I2S_WS,
|
||||
.dout = I2S_GPIO_UNUSED,
|
||||
.din = PLIP_I2S_DIN,
|
||||
.invert_flags = {
|
||||
.mclk_inv = false,
|
||||
.bclk_inv = false,
|
||||
.ws_inv = false,
|
||||
},
|
||||
},
|
||||
};
|
||||
ret = i2s_channel_init_std_mode(s_mic_handle, &rx_cfg);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "i2s_channel_init_std_mode (RX): %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Enable both TX and RX at boot for full-duplex operation.
|
||||
* Both must be enabled for the I2S hardware to route ASDOUT→DIN correctly.
|
||||
* RX data will be read only during capture; TX stays active for clock/DAC. */
|
||||
ret = i2s_channel_enable(s_spk_handle);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "i2s_channel_enable: %s", esp_err_to_name(ret));
|
||||
ESP_LOGE(TAG, "i2s_channel_enable (TX): %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
ret = i2s_channel_enable(s_mic_handle);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "i2s_channel_enable (RX): %s", esp_err_to_name(ret));
|
||||
/* Non-fatal: TX still works for playback; capture will fail gracefully. */
|
||||
ESP_LOGW(TAG, "RX enable failed — capture disabled, playback unaffected");
|
||||
s_mic_handle = NULL;
|
||||
}
|
||||
|
||||
/* 3c. Quick RX sanity: read multiple frames to check DMA and signal quality.
|
||||
* Do this before the 440Hz boot tone to see baseline ADC output. */
|
||||
if (s_mic_handle) {
|
||||
int16_t test_buf[320 * 2];
|
||||
size_t test_read = 0;
|
||||
/* Drain any startup garbage (first few frames may be codec settling). */
|
||||
for (int flush = 0; flush < 5; flush++) {
|
||||
i2s_channel_read(s_mic_handle, test_buf, sizeof(test_buf), &test_read, pdMS_TO_TICKS(50));
|
||||
}
|
||||
/* Read a real frame and report. */
|
||||
esp_err_t rx_ret = i2s_channel_read(s_mic_handle, test_buf,
|
||||
sizeof(test_buf), &test_read,
|
||||
pdMS_TO_TICKS(200));
|
||||
int32_t max_val = 0, rms_sq = 0;
|
||||
int n = (int)(test_read / 2);
|
||||
for (int i = 0; i < n; i++) {
|
||||
int32_t v = test_buf[i] < 0 ? -test_buf[i] : test_buf[i];
|
||||
if (v > max_val) max_val = v;
|
||||
rms_sq += (int32_t)test_buf[i] * test_buf[i];
|
||||
}
|
||||
rms_sq /= (n > 0 ? n : 1);
|
||||
ESP_LOGI(TAG, "RX sanity: ret=%s read=%zu max=%"PRId32" rms²=%"PRId32" s[0..3]=(%d,%d,%d,%d)",
|
||||
esp_err_to_name(rx_ret), test_read, max_val, rms_sq,
|
||||
n>0?test_buf[0]:0, n>0?test_buf[1]:0, n>1?test_buf[2]:0, n>1?test_buf[3]:0);
|
||||
}
|
||||
|
||||
/* 4. Create async command queue + worker. */
|
||||
s_queue = xQueueCreate(8, sizeof(audio_cmd_t));
|
||||
@@ -406,10 +473,248 @@ esp_err_t audio_init(void)
|
||||
8192, NULL, 5, NULL, 0);
|
||||
if (ok != pdPASS) return ESP_ERR_NO_MEM;
|
||||
|
||||
ESP_LOGI(TAG, "audio init OK (I2S TX ready, ES8388 live)");
|
||||
ESP_LOGI(TAG, "audio init OK (I2S TX ready, RX handle allocated, ES8388 live)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ── Mic capture ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* audio_capture_wav() — capture microphone audio and return a WAV buffer.
|
||||
*
|
||||
* Strategy: half-duplex. Disable TX, enable RX, read until VAD silence timeout
|
||||
* or hard cap, then disable RX, re-enable TX.
|
||||
*
|
||||
* ES8388 delivers stereo I2S frames (L+R int16) even though the mic is mono.
|
||||
* We average L+R into a single mono sample for the output WAV.
|
||||
*
|
||||
* Returns number of bytes written to out (header + PCM), or -1 on error.
|
||||
*/
|
||||
int audio_capture_wav(uint8_t *out, size_t out_max, int max_ms, int silence_ms)
|
||||
{
|
||||
if (!s_mic_handle || !s_spk_handle) {
|
||||
ESP_LOGE(TAG, "capture: I2S handles not ready");
|
||||
return -1;
|
||||
}
|
||||
if (!out || out_max < 44 + 320) {
|
||||
ESP_LOGE(TAG, "capture: output buffer too small");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* WAV parameters for output: 16 kHz, mono, S16-LE. */
|
||||
const uint32_t cap_rate = SAMPLE_RATE; /* 16000 */
|
||||
const uint16_t cap_ch = 1; /* mono output */
|
||||
const uint16_t cap_bits = 16;
|
||||
const size_t hdr_bytes = 44;
|
||||
|
||||
/* Per-frame: 20 ms at 16kHz = 320 mono samples = 640 bytes PCM out.
|
||||
* ES8388 stereo frame = 640 bytes input (320 samples × 2 ch × 2 bytes). */
|
||||
const int frame_samples = 320; /* mono samples per frame */
|
||||
const size_t frame_in_bytes = (size_t)frame_samples * 2 * sizeof(int16_t); /* stereo */
|
||||
const size_t frame_out_bytes = (size_t)frame_samples * sizeof(int16_t); /* mono */
|
||||
|
||||
/* Allocate scratch buffer in SPIRAM if available, else internal heap. */
|
||||
int16_t *rx_buf = (int16_t *)heap_caps_malloc(frame_in_bytes,
|
||||
MALLOC_CAP_SPIRAM);
|
||||
if (!rx_buf) {
|
||||
rx_buf = (int16_t *)malloc(frame_in_bytes);
|
||||
}
|
||||
if (!rx_buf) {
|
||||
ESP_LOGE(TAG, "capture: OOM for rx_buf");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Enable RX while keeping TX running (TX drives MCLK/BCLK/WS for the codec). */
|
||||
esp_err_t ret = i2s_channel_enable(s_mic_handle);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "capture: i2s_channel_enable(RX): %s", esp_err_to_name(ret));
|
||||
free(rx_buf);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Reserve space for WAV header at start of out buffer. */
|
||||
uint8_t *pcm_out = out + hdr_bytes;
|
||||
size_t pcm_max = out_max - hdr_bytes;
|
||||
size_t pcm_written = 0;
|
||||
|
||||
const int max_frames = (max_ms / 20);
|
||||
const int silence_frames = (silence_ms / 20);
|
||||
|
||||
/* VAD thresholds (raw S16: ~1% FS = ~328 for speech onset, ~0.3% for end) */
|
||||
const int32_t vad_onset_rms_sq = 328 * 328; /* ~1% FS² */
|
||||
const int32_t vad_silence_rms_sq = 100 * 100; /* ~0.3% FS² */
|
||||
|
||||
bool voice_started = false;
|
||||
int silent_frames = 0;
|
||||
int total_frames = 0;
|
||||
|
||||
ESP_LOGI(TAG, "capture: RX enabled, max_ms=%d silence_ms=%d", max_ms, silence_ms);
|
||||
|
||||
for (int f = 0; f < max_frames && pcm_written + frame_out_bytes <= pcm_max; f++) {
|
||||
size_t bytes_read = 0;
|
||||
ret = i2s_channel_read(s_mic_handle, rx_buf, frame_in_bytes,
|
||||
&bytes_read, pdMS_TO_TICKS(100));
|
||||
if (ret != ESP_OK || bytes_read == 0) {
|
||||
ESP_LOGW(TAG, "capture: read error f=%d: %s", f, esp_err_to_name(ret));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Downmix stereo → mono, compute RMS². */
|
||||
int16_t *out_frame = (int16_t *)(pcm_out + pcm_written);
|
||||
int64_t rms_sq = 0;
|
||||
int n_stereo = (int)(bytes_read / (2 * sizeof(int16_t)));
|
||||
for (int i = 0; i < n_stereo; i++) {
|
||||
int32_t l = rx_buf[i * 2];
|
||||
int32_t r = rx_buf[i * 2 + 1];
|
||||
int16_t mono = (int16_t)((l + r) / 2);
|
||||
out_frame[i] = mono;
|
||||
rms_sq += (int64_t)mono * mono;
|
||||
}
|
||||
rms_sq /= (n_stereo > 0 ? n_stereo : 1);
|
||||
|
||||
/* VAD logic. */
|
||||
if (!voice_started) {
|
||||
if (rms_sq >= vad_onset_rms_sq) {
|
||||
voice_started = true;
|
||||
silent_frames = 0;
|
||||
ESP_LOGI(TAG, "capture: voice onset at frame %d (rms²=%"PRId64")", f, rms_sq);
|
||||
}
|
||||
/* Before voice onset: still accumulate (to avoid clipping onset).
|
||||
* But don't count towards silence timeout yet. */
|
||||
} else {
|
||||
if (rms_sq < vad_silence_rms_sq) {
|
||||
silent_frames++;
|
||||
if (silent_frames >= silence_frames) {
|
||||
total_frames = f + 1;
|
||||
ESP_LOGI(TAG, "capture: VAD end (silence %d frames at f=%d)", silent_frames, f);
|
||||
pcm_written += (size_t)n_stereo * sizeof(int16_t);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
silent_frames = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pcm_written += (size_t)n_stereo * sizeof(int16_t);
|
||||
total_frames = f + 1;
|
||||
}
|
||||
|
||||
/* Disable RX; TX was never stopped. */
|
||||
i2s_channel_disable(s_mic_handle);
|
||||
free(rx_buf);
|
||||
|
||||
if (pcm_written == 0) {
|
||||
ESP_LOGW(TAG, "capture: no audio captured (silence or timeout)");
|
||||
/* Return a valid but minimal WAV with 0 PCM bytes so caller can inspect. */
|
||||
}
|
||||
|
||||
uint32_t data_size = (uint32_t)pcm_written;
|
||||
uint32_t byte_rate = cap_rate * cap_ch * (cap_bits / 8);
|
||||
uint16_t block_align = (uint16_t)(cap_ch * (cap_bits / 8));
|
||||
uint32_t riff_size = 36 + data_size;
|
||||
|
||||
/* Write WAV header (44 bytes, little-endian). */
|
||||
memcpy(out + 0, "RIFF", 4);
|
||||
memcpy(out + 4, &riff_size, 4);
|
||||
memcpy(out + 8, "WAVE", 4);
|
||||
memcpy(out + 12, "fmt ", 4);
|
||||
uint32_t fmt_size = 16;
|
||||
uint16_t fmt_pcm = 1;
|
||||
memcpy(out + 16, &fmt_size, 4);
|
||||
memcpy(out + 20, &fmt_pcm, 2);
|
||||
memcpy(out + 22, &cap_ch, 2);
|
||||
memcpy(out + 24, &cap_rate, 4);
|
||||
memcpy(out + 28, &byte_rate, 4);
|
||||
memcpy(out + 32, &block_align, 2);
|
||||
memcpy(out + 34, &cap_bits, 2);
|
||||
memcpy(out + 36, "data", 4);
|
||||
memcpy(out + 40, &data_size, 4);
|
||||
|
||||
int total_bytes = (int)(hdr_bytes + pcm_written);
|
||||
float duration_s = (float)pcm_written / (float)(cap_rate * (cap_bits / 8) * cap_ch);
|
||||
ESP_LOGI(TAG, "capture done: %d frames, %.2fs, %zu PCM bytes, %d total bytes",
|
||||
total_frames, duration_s, pcm_written, total_bytes);
|
||||
return total_bytes;
|
||||
}
|
||||
|
||||
/* ── Streaming capture API ───────────────────────────────────────────────── */
|
||||
|
||||
/* Scratch buffer reused across frames (lives for the duration of a capture). */
|
||||
static int16_t *s_rx_scratch = NULL; /* stereo frame: 320*2 int16_t = 1280 bytes */
|
||||
static int s_frame_debug_count = 0;
|
||||
#define CAP_FRAME_SAMPLES 320 /* 20 ms @ 16kHz */
|
||||
#define CAP_FRAME_IN_BYTES (CAP_FRAME_SAMPLES * 2 * sizeof(int16_t))
|
||||
|
||||
int audio_capture_begin(int max_ms, int silence_ms)
|
||||
{
|
||||
(void)max_ms; (void)silence_ms; /* caller drives VAD/timeout */
|
||||
if (!s_mic_handle) {
|
||||
ESP_LOGE(TAG, "capture_begin: RX handle not available");
|
||||
return -1;
|
||||
}
|
||||
s_rx_scratch = (int16_t *)heap_caps_malloc(CAP_FRAME_IN_BYTES, MALLOC_CAP_SPIRAM);
|
||||
if (!s_rx_scratch) s_rx_scratch = (int16_t *)malloc(CAP_FRAME_IN_BYTES);
|
||||
if (!s_rx_scratch) {
|
||||
ESP_LOGE(TAG, "capture_begin: OOM for scratch buf");
|
||||
return -1;
|
||||
}
|
||||
s_frame_debug_count = 0;
|
||||
|
||||
/* Full-duplex: keep TX running (provides MCLK/BCLK/WS clocks to codec).
|
||||
* RX is already enabled at boot. */
|
||||
ESP_LOGI(TAG, "capture_begin: ready (full-duplex, TX running)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Read one 20 ms frame from the mic, downmix stereo→mono.
|
||||
* mono_out must hold n_samples (320) int16_t values.
|
||||
* Sets *rms_sq_out to mean-squared energy of the frame.
|
||||
* Returns number of mono samples written, 0 on read timeout, -1 on error.
|
||||
*/
|
||||
int audio_capture_read_frame(int16_t *mono_out, int n_samples, int64_t *rms_sq_out)
|
||||
{
|
||||
if (!s_rx_scratch) return -1;
|
||||
size_t bytes_read = 0;
|
||||
esp_err_t ret = i2s_channel_read(s_mic_handle, s_rx_scratch,
|
||||
CAP_FRAME_IN_BYTES, &bytes_read,
|
||||
pdMS_TO_TICKS(100));
|
||||
if (ret != ESP_OK || bytes_read == 0) return 0;
|
||||
|
||||
int n = (int)(bytes_read / (2 * sizeof(int16_t)));
|
||||
if (n > n_samples) n = n_samples;
|
||||
|
||||
/* Debug: log first 3 frames raw stereo values to diagnose zero data. */
|
||||
if (s_frame_debug_count < 3) {
|
||||
int16_t r0 = (n > 0) ? s_rx_scratch[0] : 0;
|
||||
int16_t r1 = (n > 0) ? s_rx_scratch[1] : 0;
|
||||
int16_t r2 = (n > 1) ? s_rx_scratch[2] : 0;
|
||||
int16_t r3 = (n > 1) ? s_rx_scratch[3] : 0;
|
||||
ESP_LOGI(TAG, "RX frame %d: bytes_read=%zu n=%d raw[0..3]=(%d,%d,%d,%d)",
|
||||
s_frame_debug_count, bytes_read, n, r0, r1, r2, r3);
|
||||
s_frame_debug_count++;
|
||||
}
|
||||
|
||||
int64_t rms_sq = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int32_t l = s_rx_scratch[i * 2];
|
||||
int32_t r = s_rx_scratch[i * 2 + 1];
|
||||
int16_t mono = (int16_t)((l + r) / 2);
|
||||
mono_out[i] = mono;
|
||||
rms_sq += (int64_t)mono * mono;
|
||||
}
|
||||
if (rms_sq_out) *rms_sq_out = (n > 0) ? rms_sq / n : 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
void audio_capture_end(void)
|
||||
{
|
||||
if (s_rx_scratch) { free(s_rx_scratch); s_rx_scratch = NULL; }
|
||||
ESP_LOGI(TAG, "capture_end: done");
|
||||
}
|
||||
|
||||
i2s_chan_handle_t audio_spk_handle(void)
|
||||
{
|
||||
return s_spk_handle;
|
||||
|
||||
@@ -43,6 +43,40 @@ esp_err_t audio_stop(void);
|
||||
* audio_stop() is called. Non-blocking — spawns an internal task. */
|
||||
esp_err_t audio_ring_start(void);
|
||||
|
||||
/*
|
||||
* Capture microphone audio and encode as WAV (16 kHz, mono, S16-LE).
|
||||
*
|
||||
* out : caller-provided buffer (must be >= 44 + PCM bytes)
|
||||
* out_max : size of out in bytes
|
||||
* max_ms : hard cap on capture duration in milliseconds
|
||||
* silence_ms: milliseconds of silence after voice onset to trigger stop
|
||||
*
|
||||
* Returns total bytes written (44-byte WAV header + PCM), or -1 on error.
|
||||
* Uses half-duplex: disables TX speaker during capture and re-enables it
|
||||
* on return. Safe to call from any task; not reentrant.
|
||||
*/
|
||||
int audio_capture_wav(uint8_t *out, size_t out_max, int max_ms, int silence_ms);
|
||||
|
||||
/*
|
||||
* Streaming capture API — avoids a large output buffer by letting the caller
|
||||
* write chunks incrementally (e.g. directly into a TCP socket).
|
||||
*
|
||||
* Usage:
|
||||
* audio_capture_begin(max_ms, silence_ms) — enable RX, returns 0 on OK
|
||||
* audio_capture_read_frame(mono_out, n_samples, rms_sq_out)
|
||||
* — read one 20 ms frame (mono S16)
|
||||
* fills n_samples mono samples,
|
||||
* sets *rms_sq_out (energy metric)
|
||||
* returns actual samples read, 0=timeout, -1=error
|
||||
* audio_capture_end() — disable RX, re-enable TX
|
||||
*
|
||||
* n_samples must be 320 (= SAMPLE_RATE/1000*20, one 20 ms frame).
|
||||
* The caller is responsible for VAD and stop logic.
|
||||
*/
|
||||
int audio_capture_begin(int max_ms, int silence_ms);
|
||||
int audio_capture_read_frame(int16_t *mono_out, int n_samples, int64_t *rms_sq_out);
|
||||
void audio_capture_end(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
#define PLIP_I2S_BCLK 27
|
||||
#define PLIP_I2S_WS 25 /* LRCK */
|
||||
#define PLIP_I2S_DOUT 26 /* DAC → speaker */
|
||||
#define PLIP_I2S_DIN 35 /* ADC ← mic (input-only) */
|
||||
#define PLIP_I2S_DIN 35 /* ADC ← mic (input-only, ES8388 ASDOUT) */
|
||||
|
||||
/* ---------- Power Amplifier ---------- */
|
||||
#define PLIP_PA_ENABLE 21 /* Active HIGH, drives NS4150 amp */
|
||||
|
||||
+80
-41
@@ -34,13 +34,14 @@
|
||||
#define ES8388_DAC_POWER 0x04 /* DACPOWER */
|
||||
#define ES8388_CHIP_LP 0x05 /* CHIP_LP */
|
||||
#define ES8388_CHIP_CTL3 0x06 /* CHIP_CTL3 */
|
||||
#define ES8388_ADC_CTL 0x09 /* ADCCONTROL1 — PGA gain */
|
||||
#define ES8388_ADC_CTL3 0x0A /* ADCCONTROL3 — input select */
|
||||
#define ES8388_ADC_CTL4 0x0B /* ADCCONTROL4 — I2S word len / format */
|
||||
#define ES8388_ADC_CTL5 0x0C /* ADCCONTROL5 — MCLK divider */
|
||||
#define ES8388_ADC_CTL7 0x0E /* ADCCONTROL7 — HPF */
|
||||
#define ES8388_ADC_CTL8 0x0F /* ADCCONTROL8 — L volume */
|
||||
#define ES8388_ADC_CTL9 0x10 /* ADCCONTROL9 — R volume */
|
||||
#define ES8388_ADC_CTL1 0x09 /* ADCCONTROL1 — PGA gain */
|
||||
#define ES8388_ADC_CTL2 0x0A /* ADCCONTROL2 — input select (LINSEL/RINSEL) */
|
||||
#define ES8388_ADC_CTL3 0x0B /* ADCCONTROL3 — DS/DS filter select */
|
||||
#define ES8388_ADC_CTL4 0x0C /* ADCCONTROL4 — I2S format / word length */
|
||||
#define ES8388_ADC_CTL5 0x0D /* ADCCONTROL5 — MCLK divider (RATIO=256) */
|
||||
#define ES8388_ADC_CTL7 0x0F /* ADCCONTROL7 — HPF enable/config */
|
||||
#define ES8388_ADC_CTL8 0x10 /* ADCCONTROL8 — ADC L volume */
|
||||
#define ES8388_ADC_CTL9 0x11 /* ADCCONTROL9 — ADC R volume */
|
||||
#define ES8388_DAC_CTL1 0x17 /* DACCONTROL1 — I2S word len / format */
|
||||
#define ES8388_DAC_CTL2 0x18 /* DACCONTROL2 — MCLK divider */
|
||||
#define ES8388_DAC_CTL3 0x19 /* DACCONTROL3 — mute */
|
||||
@@ -49,12 +50,12 @@
|
||||
#define ES8388_DAC_CTL16 0x26 /* DACCONTROL16 — L/R mixer config */
|
||||
#define ES8388_DAC_CTL17 0x27 /* DACCONTROL17 — L mixer gain */
|
||||
#define ES8388_DAC_CTL20 0x2A /* DACCONTROL20 — R mixer gain */
|
||||
#define ES8388_DAC_CTL21 0x2B /* DACCONTROL21 — out1 vol L */
|
||||
#define ES8388_DAC_CTL22 0x2C /* DACCONTROL22 — out1 vol R */
|
||||
#define ES8388_DAC_CTL23 0x2D /* DACCONTROL23 — out2 vol L (speaker) */
|
||||
#define ES8388_DAC_CTL24 0x2E /* DACCONTROL24 — out2 vol R */
|
||||
#define ES8388_DAC_CTL25 0x2F /* DACCONTROL25 — out1 enable */
|
||||
#define ES8388_DAC_CTL26 0x30 /* DACCONTROL26 — out2 enable */
|
||||
#define ES8388_DAC_CTL21 0x2B /* DACCONTROL21 — ADC/DAC LRCK sync (bit7=1 for full-duplex) */
|
||||
#define ES8388_DAC_CTL22 0x2C /* DACCONTROL22 — (unused in this config) */
|
||||
#define ES8388_DAC_CTL23 0x2D /* DACCONTROL23 — (unused in this config) */
|
||||
#define ES8388_DAC_CTL24 0x2E /* DACCONTROL24 — LOUT1VOL (OUT1 left volume) */
|
||||
#define ES8388_DAC_CTL25 0x2F /* DACCONTROL25 — ROUT1VOL (OUT1 right volume) */
|
||||
#define ES8388_DAC_CTL26 0x30 /* DACCONTROL26 — LOUT2VOL (OUT2 left volume) */
|
||||
|
||||
/* Volume register: 0x00=0dB (max), 0x21=-33dB step per 1.5dB, 0x24=mute. */
|
||||
#define ES8388_VOL_MAX 0x00
|
||||
@@ -128,22 +129,35 @@ esp_err_t es8388_init(void)
|
||||
/* 2. Power down all blocks first (prevents pop on line-up). */
|
||||
if (i2c_write_reg(ES8388_CHIP_POWER, 0xFF) != ESP_OK) return ESP_FAIL;
|
||||
|
||||
/* 3. Master mode off, serial port SLAVE (I2S clock from ESP32). */
|
||||
if (i2c_write_reg(ES8388_CHIP_CTL1, 0x05) != ESP_OK) return ESP_FAIL;
|
||||
/* 3. CONTROL1: WORK_MODE = record+playback (0x10), VMIDSEL=10 (0x02).
|
||||
* 0x12 = 0b00010010 — enables both ADC and DAC paths.
|
||||
* Previous value 0x05 only enabled playback (WORK_MODE=00). */
|
||||
if (i2c_write_reg(ES8388_CHIP_CTL1, 0x12) != ESP_OK) return ESP_FAIL;
|
||||
|
||||
/* 3b. CONTROL2 (0x01): VROI=0, LPVrefBuf=0, normal operation. */
|
||||
if (i2c_write_reg(ES8388_CHIP_CTL2, 0x50) != ESP_OK) return ESP_FAIL;
|
||||
|
||||
/* 4. Clock: MCLK divider = 256*Fs, DAC SRC = MCLK. */
|
||||
if (i2c_write_reg(0x08, 0x00) != ESP_OK) return ESP_FAIL; /* MASTERMODE = slave */
|
||||
|
||||
/* 5. ADC power: LINN1 × RINP1 on; keep ADC PGA/HPF off for now. */
|
||||
if (i2c_write_reg(ES8388_ADC_POWER, 0x00) != ESP_OK) return ESP_FAIL;
|
||||
/* 5. ADC power: power down to allow clean register writes. */
|
||||
if (i2c_write_reg(ES8388_ADC_POWER, 0xFF) != ESP_OK) return ESP_FAIL;
|
||||
|
||||
/* 6. ADC control: differential input on LIN1/RIN1, PGA gain +24dB. */
|
||||
if (i2c_write_reg(ES8388_ADC_CTL, 0x88) != ESP_OK) return ESP_FAIL;
|
||||
if (i2c_write_reg(ES8388_ADC_CTL3, 0x02) != ESP_OK) return ESP_FAIL; /* LINSEL=LIN2(mic) */
|
||||
|
||||
/* 7. ADC I2S: 16-bit I2S Philips. MCLK/256 = 16kHz. */
|
||||
if (i2c_write_reg(ES8388_ADC_CTL4, 0x0C) != ESP_OK) return ESP_FAIL;
|
||||
if (i2c_write_reg(ES8388_ADC_CTL5, 0x02) != ESP_OK) return ESP_FAIL; /* ADCLRCKDIV=256 */
|
||||
/* 6. ADC config (follows Espressif esp-codec-dev reference sequence):
|
||||
* - ADCCONTROL1 (0x09) = 0xBB: MIC PGA +24dB L and R
|
||||
* - ADCCONTROL2 (0x0A) = 0x50: LINSEL=10 LIN2/RIN2 (telephone handset mic)
|
||||
* Value 0x00 = LIN1 differential, 0x50 = LIN2 single-ended (A1S combiné)
|
||||
* - ADCCONTROL3 (0x0B) = 0x02: DS filter select (required by reference)
|
||||
* - ADCCONTROL4 (0x0C) = 0x0C: I2S Philips 16-bit word length
|
||||
* - ADCCONTROL5 (0x0D) = 0x02: ADCFsMode SINGLE SPEED RATIO=256 (16kHz@MCLK 4.096MHz)
|
||||
* - ADCCONTROL8/9 (0x10/0x11) = 0x00: ADC digital volume 0dB */
|
||||
if (i2c_write_reg(ES8388_ADC_CTL1, 0xBB) != ESP_OK) return ESP_FAIL; /* PGA +24dB L+R */
|
||||
if (i2c_write_reg(ES8388_ADC_CTL2, 0x00) != ESP_OK) return ESP_FAIL; /* LIN1/RIN1 differential (onboard mic) */
|
||||
if (i2c_write_reg(ES8388_ADC_CTL3, 0x02) != ESP_OK) return ESP_FAIL; /* DS filter sel */
|
||||
if (i2c_write_reg(ES8388_ADC_CTL4, 0x0C) != ESP_OK) return ESP_FAIL; /* I2S 16-bit */
|
||||
if (i2c_write_reg(ES8388_ADC_CTL5, 0x02) != ESP_OK) return ESP_FAIL; /* RATIO=256 */
|
||||
if (i2c_write_reg(ES8388_ADC_CTL8, 0x00) != ESP_OK) return ESP_FAIL; /* ADC vol L 0dB */
|
||||
if (i2c_write_reg(ES8388_ADC_CTL9, 0x00) != ESP_OK) return ESP_FAIL; /* ADC vol R 0dB */
|
||||
|
||||
/* 8. DAC I2S: 16-bit I2S Philips, MCLK/256, no softmute. */
|
||||
if (i2c_write_reg(ES8388_DAC_CTL1, 0x18) != ESP_OK) return ESP_FAIL;
|
||||
@@ -159,40 +173,65 @@ esp_err_t es8388_init(void)
|
||||
if (i2c_write_reg(ES8388_DAC_CTL17, 0x90) != ESP_OK) return ESP_FAIL;
|
||||
if (i2c_write_reg(ES8388_DAC_CTL20, 0x90) != ESP_OK) return ESP_FAIL;
|
||||
|
||||
/* 11. Output attenuation — OUT1 + OUT2 at 0 dB (register 0x00). */
|
||||
if (i2c_write_reg(ES8388_DAC_CTL21, 0x00) != ESP_OK) return ESP_FAIL;
|
||||
if (i2c_write_reg(ES8388_DAC_CTL22, 0x00) != ESP_OK) return ESP_FAIL;
|
||||
if (i2c_write_reg(ES8388_DAC_CTL23, 0x00) != ESP_OK) return ESP_FAIL;
|
||||
if (i2c_write_reg(ES8388_DAC_CTL24, 0x00) != ESP_OK) return ESP_FAIL;
|
||||
/* 11. DACCONTROL21 (0x2B): ADC+DAC LRCK sync = 0xC0 (both ADC and DAC share LRCK).
|
||||
* Espressif reference: 0xC0 when both ADC and DAC active (record+playback mode).
|
||||
* 0x80 when DAC only.
|
||||
* This register is a clock routing register, NOT a volume register. */
|
||||
if (i2c_write_reg(ES8388_DAC_CTL21, 0xC0) != ESP_OK) return ESP_FAIL;
|
||||
|
||||
/* 12. Enable OUT1 (headphone) and OUT2 (speaker line). */
|
||||
if (i2c_write_reg(ES8388_DAC_CTL25, 0x35) != ESP_OK) return ESP_FAIL;
|
||||
if (i2c_write_reg(ES8388_DAC_CTL26, 0x35) != ESP_OK) return ESP_FAIL;
|
||||
/* 11b. Restart internal state machine (required after DACCONTROL21 change).
|
||||
* Without this pulse, the ADC clock domain may not synchronise properly.
|
||||
* Espressif reference sequence: CHIPPOWER=0xF0 then 0x00 to restart FSM. */
|
||||
if (i2c_write_reg(ES8388_CHIP_POWER, 0xF0) != ESP_OK) return ESP_FAIL;
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
if (i2c_write_reg(ES8388_CHIP_POWER, 0x00) != ESP_OK) return ESP_FAIL;
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
|
||||
/* DACCONTROL23 (0x2D): VROI=0, normal output. */
|
||||
if (i2c_write_reg(ES8388_DAC_CTL23, 0x00) != ESP_OK) return ESP_FAIL;
|
||||
|
||||
/* 12. Volume: OUT1L/R = 0x1E (0dB), OUT2L = 0 (speaker vol set by PA separately). */
|
||||
if (i2c_write_reg(ES8388_DAC_CTL24, 0x1E) != ESP_OK) return ESP_FAIL; /* LOUT1VOL */
|
||||
if (i2c_write_reg(ES8388_DAC_CTL25, 0x1E) != ESP_OK) return ESP_FAIL; /* ROUT1VOL */
|
||||
if (i2c_write_reg(ES8388_DAC_CTL26, 0x00) != ESP_OK) return ESP_FAIL; /* LOUT2VOL */
|
||||
|
||||
/* 13. DAC power: power up DAC L+R. */
|
||||
if (i2c_write_reg(ES8388_DAC_POWER, 0x3C) != ESP_OK) return ESP_FAIL;
|
||||
|
||||
/* 14. Chip power: release global power-down (ADC+DAC active). */
|
||||
if (i2c_write_reg(ES8388_CHIP_POWER, 0x00) != ESP_OK) return ESP_FAIL;
|
||||
/* 14. ADC power: power up ADC (0x09 per Espressif reference).
|
||||
* Must come AFTER DACCONTROL21 state machine restart and all ADC register writes. */
|
||||
if (i2c_write_reg(ES8388_ADC_POWER, 0x09) != ESP_OK) return ESP_FAIL;
|
||||
|
||||
/* 15. Enable PA (power amplifier for speaker). */
|
||||
gpio_set_direction(PLIP_PA_ENABLE, GPIO_MODE_OUTPUT);
|
||||
gpio_set_level(PLIP_PA_ENABLE, 1);
|
||||
|
||||
ESP_LOGI(TAG, "ES8388 init OK — PA enabled, DAC @ 0dB, ADC PGA +24dB");
|
||||
/* Verify key register values to confirm ADC path is configured correctly. */
|
||||
uint8_t ctl1=0, adcpwr=0, adcinsel=0, dacctl21=0, chippower=0, adcctl3=0;
|
||||
es8388_read_reg(ES8388_CHIP_CTL1, &ctl1);
|
||||
es8388_read_reg(ES8388_ADC_POWER, &adcpwr);
|
||||
es8388_read_reg(ES8388_ADC_CTL2, &adcinsel); /* ADCCONTROL2 = input sel (0x50=LIN2) */
|
||||
es8388_read_reg(ES8388_ADC_CTL3, &adcctl3); /* ADCCONTROL3 = DS filter (0x02) */
|
||||
es8388_read_reg(ES8388_DAC_CTL21, &dacctl21); /* DACCONTROL21 = ADC+DAC LRCK sync (0xC0) */
|
||||
es8388_read_reg(ES8388_CHIP_POWER, &chippower);
|
||||
ESP_LOGI(TAG, "ES8388 regs: CTL1=0x%02X ADCPWR=0x%02X ADCINSEL=0x%02X ADCCTL3=0x%02X DACCTL21=0x%02X CHIPPOWER=0x%02X",
|
||||
ctl1, adcpwr, adcinsel, adcctl3, dacctl21, chippower);
|
||||
ESP_LOGI(TAG, "ES8388 init OK — PA enabled, DAC @ 0dB, ADC PGA +24dB, input=LIN1, DACCTL21=0xC0");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t es8388_set_volume(uint8_t vol)
|
||||
{
|
||||
/* Map 0..100 to 0x00 (0dB) .. 0x24 (mute); register is attenuation. */
|
||||
/* Map 0..100 to 0x00 (0dB) .. 0x24 (mute); register is attenuation.
|
||||
* Volume registers: DACCONTROL24 (0x2E) = OUT1L, DACCONTROL25 (0x2F) = OUT1R,
|
||||
* DACCONTROL26 (0x30) = OUT2L, DACCONTROL27 (0x31) = OUT2R.
|
||||
* DACCONTROL21 (0x2B) is the ADC/DAC LRCK sync register — DO NOT touch here. */
|
||||
uint8_t reg_val = (uint8_t)((100 - (int)vol) * 0x24 / 100);
|
||||
ESP_LOGI(TAG, "set_volume: %d%% → reg=0x%02X", vol, reg_val);
|
||||
ESP_LOGI(TAG, "set_volume: %d%% -> reg=0x%02X", vol, reg_val);
|
||||
esp_err_t r = ESP_OK;
|
||||
r |= i2c_write_reg(ES8388_DAC_CTL21, reg_val);
|
||||
r |= i2c_write_reg(ES8388_DAC_CTL22, reg_val);
|
||||
r |= i2c_write_reg(ES8388_DAC_CTL23, reg_val);
|
||||
r |= i2c_write_reg(ES8388_DAC_CTL24, reg_val);
|
||||
r |= i2c_write_reg(ES8388_DAC_CTL24, reg_val); /* OUT1 L volume */
|
||||
r |= i2c_write_reg(ES8388_DAC_CTL25, reg_val); /* OUT1 R volume */
|
||||
r |= i2c_write_reg(ES8388_DAC_CTL26, reg_val); /* OUT2 L volume */
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
+178
-1
@@ -16,12 +16,16 @@
|
||||
|
||||
#include "net.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "audio.h"
|
||||
#include "cmd_exec.h"
|
||||
#include "es8388.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_http_server.h"
|
||||
@@ -222,6 +226,169 @@ static esp_err_t handle_file_post(httpd_req_t *req)
|
||||
return send_json(req, "200 OK", resp);
|
||||
}
|
||||
|
||||
/* ── POST /voice/capture ──────────────────────────────────────────────────── */
|
||||
|
||||
#define CAP_MAX_MS_DEFAULT 5000
|
||||
#define CAP_SILENCE_MS_DEFAULT 800
|
||||
/* Max capture duration: 10 s (no large buffer needed — streaming approach). */
|
||||
#define CAP_MAX_ALLOWED_MS 10000
|
||||
|
||||
/*
|
||||
* Streaming WAV capture handler.
|
||||
*
|
||||
* Memory budget: only two small buffers on stack:
|
||||
* - WAV header : 44 bytes
|
||||
* - mono frame : 320 × 2 = 640 bytes
|
||||
* The audio driver uses an additional 1280-byte scratch buf for the stereo I2S read.
|
||||
* Total heap usage during capture: ~2 KB (vs 128+ KB for buffer-at-once approach).
|
||||
*
|
||||
* The response uses chunked transfer encoding via httpd_resp_send_chunk().
|
||||
* We send the WAV header first (with data_size = 0xFFFFFFFF as placeholder since
|
||||
* length is unknown), then PCM frames, then finalize with httpd_resp_send_chunk(NULL,0).
|
||||
* Most WAV players / ffmpeg handle the placeholder length gracefully.
|
||||
*/
|
||||
static esp_err_t handle_voice_capture(httpd_req_t *req)
|
||||
{
|
||||
/* Parse optional query params: max_ms, silence_ms */
|
||||
char query[128] = {0};
|
||||
httpd_req_get_url_query_str(req, query, sizeof(query));
|
||||
|
||||
char val[16] = {0};
|
||||
int max_ms = CAP_MAX_MS_DEFAULT;
|
||||
int silence_ms = CAP_SILENCE_MS_DEFAULT;
|
||||
if (httpd_query_key_value(query, "max_ms", val, sizeof(val)) == ESP_OK) {
|
||||
int v = atoi(val);
|
||||
if (v > 0 && v <= CAP_MAX_ALLOWED_MS) max_ms = v;
|
||||
else if (v > CAP_MAX_ALLOWED_MS) max_ms = CAP_MAX_ALLOWED_MS;
|
||||
}
|
||||
memset(val, 0, sizeof(val));
|
||||
if (httpd_query_key_value(query, "silence_ms", val, sizeof(val)) == ESP_OK) {
|
||||
int v = atoi(val);
|
||||
if (v > 0 && v <= 5000) silence_ms = v;
|
||||
}
|
||||
ESP_LOGI(TAG, "POST /voice/capture max_ms=%d silence_ms=%d (streaming)", max_ms, silence_ms);
|
||||
|
||||
/* Start capture (enables I2S RX, disables TX). */
|
||||
if (audio_capture_begin(max_ms, silence_ms) != 0) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "capture init failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Send HTTP headers. */
|
||||
httpd_resp_set_type(req, "audio/wav");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
|
||||
/* WAV header with placeholder data_size (0xFFFFFFFF = unknown length). */
|
||||
const uint32_t cap_rate = 16000;
|
||||
const uint16_t cap_ch = 1;
|
||||
const uint16_t cap_bits = 16;
|
||||
const uint32_t byte_rate = cap_rate * cap_ch * (cap_bits / 8);
|
||||
const uint16_t block_align = (uint16_t)(cap_ch * (cap_bits / 8));
|
||||
const uint32_t data_size = 0xFFFFFFFFu; /* placeholder, updated below if possible */
|
||||
const uint32_t riff_size = 0xFFFFFFFFu;
|
||||
uint16_t fmt_pcm = 1;
|
||||
uint32_t fmt_size = 16;
|
||||
|
||||
uint8_t hdr[44];
|
||||
memcpy(hdr + 0, "RIFF", 4); memcpy(hdr + 4, &riff_size, 4);
|
||||
memcpy(hdr + 8, "WAVE", 4); memcpy(hdr + 12, "fmt ", 4);
|
||||
memcpy(hdr + 16, &fmt_size, 4); memcpy(hdr + 20, &fmt_pcm, 2);
|
||||
memcpy(hdr + 22, &cap_ch, 2); memcpy(hdr + 24, &cap_rate, 4);
|
||||
memcpy(hdr + 28, &byte_rate, 4); memcpy(hdr + 32, &block_align, 2);
|
||||
memcpy(hdr + 34, &cap_bits, 2);
|
||||
memcpy(hdr + 36, "data", 4); memcpy(hdr + 40, &data_size, 4);
|
||||
|
||||
esp_err_t ret = httpd_resp_send_chunk(req, (const char *)hdr, sizeof(hdr));
|
||||
if (ret != ESP_OK) { audio_capture_end(); return ESP_FAIL; }
|
||||
|
||||
/* VAD state (mirrors audio_capture_wav logic). */
|
||||
const int64_t vad_onset_sq = (int64_t)328 * 328;
|
||||
const int64_t vad_silence_sq = (int64_t)100 * 100;
|
||||
const int max_frames = max_ms / 20;
|
||||
const int silence_frames = silence_ms / 20;
|
||||
bool voice_started = false;
|
||||
int silent_count = 0;
|
||||
int total_frames = 0;
|
||||
size_t total_pcm_bytes = 0;
|
||||
|
||||
/* Per-frame mono buffer: 320 × int16_t = 640 bytes on stack. */
|
||||
int16_t mono_frame[320];
|
||||
|
||||
for (int f = 0; f < max_frames; f++) {
|
||||
int64_t rms_sq = 0;
|
||||
int n = audio_capture_read_frame(mono_frame, 320, &rms_sq);
|
||||
if (n < 0) { ESP_LOGW(TAG, "capture: read error at frame %d", f); break; }
|
||||
if (n == 0) continue; /* timeout, retry */
|
||||
|
||||
/* Send PCM chunk. */
|
||||
size_t frame_bytes = (size_t)n * sizeof(int16_t);
|
||||
ret = httpd_resp_send_chunk(req, (const char *)mono_frame, (ssize_t)frame_bytes);
|
||||
if (ret != ESP_OK) { ESP_LOGW(TAG, "capture: send_chunk failed"); break; }
|
||||
total_pcm_bytes += frame_bytes;
|
||||
total_frames = f + 1;
|
||||
|
||||
/* VAD. */
|
||||
if (!voice_started) {
|
||||
if (rms_sq >= vad_onset_sq) {
|
||||
voice_started = true;
|
||||
silent_count = 0;
|
||||
ESP_LOGI(TAG, "capture: voice onset frame %d rms²=%"PRId64, f, rms_sq);
|
||||
}
|
||||
} else {
|
||||
if (rms_sq < vad_silence_sq) {
|
||||
if (++silent_count >= silence_frames) {
|
||||
ESP_LOGI(TAG, "capture: VAD end frame %d", f);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
silent_count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audio_capture_end();
|
||||
|
||||
/* Finalize chunked transfer. */
|
||||
httpd_resp_send_chunk(req, NULL, 0);
|
||||
|
||||
float dur = (float)total_pcm_bytes / (float)(cap_rate * (cap_bits / 8) * cap_ch);
|
||||
ESP_LOGI(TAG, "capture done: %d frames %.2fs %zu PCM bytes sent", total_frames, dur, total_pcm_bytes);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ── GET /debug/regs (ES8388 register dump for ADC diagnosis) ─────────────── */
|
||||
|
||||
static esp_err_t handle_debug_regs(httpd_req_t *req)
|
||||
{
|
||||
/* Read key ES8388 registers to diagnose ADC path. */
|
||||
uint8_t regs[0x40] = {0};
|
||||
for (int i = 0; i < 0x40; i++) {
|
||||
es8388_read_reg((uint8_t)i, ®s[i]);
|
||||
}
|
||||
char buf[1024];
|
||||
/* All register names use Espressif official numbering (ADCCONTROL1=reg0x09, etc.) */
|
||||
int n = snprintf(buf, sizeof(buf),
|
||||
"{\"CTL1\":\"0x%02X\",\"CTL2\":\"0x%02X\",\"CHIPPOWER\":\"0x%02X\","
|
||||
"\"ADCPOWER\":\"0x%02X\",\"DACPOWER\":\"0x%02X\","
|
||||
"\"ADCCONTROL1\":\"0x%02X\",\"ADCCONTROL2\":\"0x%02X\",\"ADCCONTROL3\":\"0x%02X\","
|
||||
"\"ADCCONTROL4\":\"0x%02X\",\"ADCCONTROL5\":\"0x%02X\",\"ADCCONTROL6\":\"0x%02X\","
|
||||
"\"ADCCONTROL7\":\"0x%02X\",\"ADCCONTROL8\":\"0x%02X\",\"ADCCONTROL9\":\"0x%02X\","
|
||||
"\"DACCTL1\":\"0x%02X\",\"DACCTL2\":\"0x%02X\",\"DACCTL3\":\"0x%02X\","
|
||||
"\"DACCTL21\":\"0x%02X\",\"DACCTL24\":\"0x%02X\",\"DACCTL25\":\"0x%02X\","
|
||||
"\"MASTERMODE\":\"0x%02X\",\"CHIPCTL3\":\"0x%02X\",\"CHIPLP\":\"0x%02X\"}",
|
||||
regs[0x00], regs[0x01], regs[0x02],
|
||||
regs[0x03], regs[0x04],
|
||||
regs[0x09], regs[0x0A], regs[0x0B],
|
||||
regs[0x0C], regs[0x0D], regs[0x0E],
|
||||
regs[0x0F], regs[0x10], regs[0x11],
|
||||
regs[0x17], regs[0x18], regs[0x19],
|
||||
regs[0x2B], regs[0x2E], regs[0x2F],
|
||||
regs[0x08], regs[0x06], regs[0x05]);
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
return httpd_resp_send(req, buf, n);
|
||||
}
|
||||
|
||||
/* ── POST /game/cmd (WiFi-direct CMD endpoint) ───────────────────────────── */
|
||||
|
||||
#define MAX_CMD_BYTES 512
|
||||
@@ -347,11 +514,21 @@ esp_err_t net_init(void)
|
||||
.uri = "/game/cmd", .method = HTTP_POST,
|
||||
.handler = handle_cmd_post, .user_ctx = NULL,
|
||||
};
|
||||
static const httpd_uri_t uri_capture = {
|
||||
.uri = "/voice/capture", .method = HTTP_POST,
|
||||
.handler = handle_voice_capture, .user_ctx = NULL,
|
||||
};
|
||||
static const httpd_uri_t uri_debug_regs = {
|
||||
.uri = "/debug/regs", .method = HTTP_GET,
|
||||
.handler = handle_debug_regs, .user_ctx = NULL,
|
||||
};
|
||||
httpd_register_uri_handler(s_httpd, &uri_status);
|
||||
httpd_register_uri_handler(s_httpd, &uri_scenario);
|
||||
httpd_register_uri_handler(s_httpd, &uri_file);
|
||||
httpd_register_uri_handler(s_httpd, &uri_cmd);
|
||||
httpd_register_uri_handler(s_httpd, &uri_capture);
|
||||
httpd_register_uri_handler(s_httpd, &uri_debug_regs);
|
||||
|
||||
ESP_LOGI(TAG, "httpd up on :80 (GET /status, POST /game/scenario, POST /game/file, POST /game/cmd)");
|
||||
ESP_LOGI(TAG, "httpd up on :80 (GET /status, GET /debug/regs, POST /game/scenario, POST /game/file, POST /game/cmd, POST /voice/capture)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user