box3: fix gamebook audio, accents, and crashes
The BOX-3 touch gamebook had several blocking bugs that made the voiced+foley story packs unusable. This fixes them end to end. - Stack overflow on story start: gb_play_task kept a 2 KB buffer on its 4 KB stack and rebooted the board. Buffers are now static (the task is a singleton) and the stack is 6 KB. - No sound at all: the ES8311 codec was never initialised — main.c wrote raw I2S to a muted DAC. Output (and the mic) now go through the BSP esp_codec_dev (bsp_audio_codec_speaker/microphone_init, esp_codec_dev_write/read). The power amplifier (GPIO46) is also forced on and volume set to 100, as the es8311 pa_pin handling did not drive it in practice. - First passage silent: gb_play re-mounted the SD that the gamebook had already mounted; the failed second bsp_sdcard_mount tore down the VFS so the first fopen failed. Now we open the file first and only mount if that fails. A short silence lead-in also primes the codec/PA ramp. - No accents: the gamebook used ASCII-only lv_font_montserrat_14/24. Added custom Montserrat fonts (font_fr_14/24, Latin-1 + œŒŸ « » ’ …, uncompressed so they render without LV_USE_FONT_COMPRESSED). - Mic/voice streaming gated behind CONFIG_BOX3_VOICE_STREAMING (default off): as a gamebook the BOX-3 needs no mic, and the constant codec reads + bridge reconnects interfered with playback and flooded the log. plip_virtual + cmd_exec handle types switched from i2s_chan_handle_t to esp_codec_dev_handle_t accordingly.
This commit was merged in pull request #47.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "main.c" "voice_ws_client.c" "scenario_server.c" "plip_virtual.c" "plip_ui.c" "stimulus.c" "cmd_exec.c" "gamebook.c"
|
||||
SRCS "main.c" "voice_ws_client.c" "scenario_server.c" "plip_virtual.c" "plip_ui.c" "stimulus.c" "cmd_exec.c" "gamebook.c" "font_fr_14.c" "font_fr_24.c"
|
||||
INCLUDE_DIRS "."
|
||||
PRIV_REQUIRES
|
||||
driver
|
||||
@@ -13,5 +13,6 @@ idf_component_register(
|
||||
spiffs
|
||||
scenario_mesh
|
||||
espressif__esp-box-3
|
||||
espressif__esp_codec_dev
|
||||
lvgl__lvgl
|
||||
)
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
menu "Zacus BOX-3 Voice Configuration"
|
||||
|
||||
config BOX3_VOICE_STREAMING
|
||||
bool "Enable microphone + voice-bridge streaming"
|
||||
default n
|
||||
help
|
||||
Start the mic capture task and the WebSocket voice-bridge client.
|
||||
Off by default: as a touch gamebook the BOX-3 needs no microphone,
|
||||
and the continuous codec reads + bridge reconnect attempts interfere
|
||||
with speaker playback (the first narration clip came out silent) and
|
||||
flood the log. Enable only when using the BOX-3 as a voice device.
|
||||
|
||||
config ZACUS_WIFI_SSID
|
||||
string "WiFi SSID"
|
||||
default "zacus-net"
|
||||
|
||||
+39
-22
@@ -10,7 +10,7 @@
|
||||
//
|
||||
// Objective 2 — play: real WAV streaming to I2S
|
||||
// 1. Mount SD card (best-effort, /sdcard). Parse WAV header (PCM-16).
|
||||
// 2. Stream PCM samples via s_spk_handle (extern, same I2S channel as TTS).
|
||||
// 2. Stream PCM samples via the ES8311 codec (esp_codec_dev), same path as TTS.
|
||||
// 3. If SD absent / file not found → fallback bip + log.
|
||||
// 4. Embedded test cue always available at virtual path "embedded://" or
|
||||
// when the requested path starts with "embedded:" — proves WAV decode + I2S.
|
||||
@@ -33,16 +33,16 @@
|
||||
|
||||
#include "embedded_wav.h" // s_embedded_wav[], EMBEDDED_WAV_SIZE
|
||||
|
||||
// audio_play_tone and s_spk_handle are defined in main.c
|
||||
// audio_play_tone is defined in main.c (writes through the ES8311 codec).
|
||||
extern void audio_play_tone(float frequency, int duration_ms);
|
||||
|
||||
// Speaker I2S handle — obtained from main.c via a weak accessor.
|
||||
// We declare a weak symbol here; main.c must define cmd_exec_set_spk_handle()
|
||||
// and call it after speaker_init(). Alternatively we use the driver directly.
|
||||
#include "driver/i2s_std.h"
|
||||
static i2s_chan_handle_t s_spk_handle_local = NULL;
|
||||
// Speaker codec (ES8311) handle — obtained from main.c after speaker_init().
|
||||
// Audio is streamed through esp_codec_dev so the ES8311 DAC is actually driven
|
||||
// (a raw I2S write leaves the codec muted).
|
||||
#include "esp_codec_dev.h"
|
||||
static esp_codec_dev_handle_t s_spk_handle_local = NULL;
|
||||
|
||||
void cmd_exec_set_spk_handle(i2s_chan_handle_t h)
|
||||
void cmd_exec_set_spk_handle(esp_codec_dev_handle_t h)
|
||||
{
|
||||
s_spk_handle_local = h;
|
||||
}
|
||||
@@ -344,13 +344,11 @@ static esp_err_t stream_pcm16(const uint8_t *pcm, size_t byte_len,
|
||||
size_t offset = 0;
|
||||
while (offset < byte_len) {
|
||||
size_t chunk = (byte_len - offset < 1024) ? (byte_len - offset) : 1024;
|
||||
size_t written = 0;
|
||||
esp_err_t ret = i2s_channel_write(s_spk_handle_local,
|
||||
pcm + offset, chunk,
|
||||
&written, pdMS_TO_TICKS(500));
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "I2S write error: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
int ret = esp_codec_dev_write(s_spk_handle_local,
|
||||
(void *)(pcm + offset), (int)chunk);
|
||||
if (ret != ESP_CODEC_DEV_OK) {
|
||||
ESP_LOGW(TAG, "codec write error: %d", ret);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
offset += chunk;
|
||||
}
|
||||
@@ -501,8 +499,12 @@ static volatile bool s_gb_stop = false;
|
||||
static void gb_play_task(void *arg)
|
||||
{
|
||||
(void) arg;
|
||||
uint8_t hdr[64];
|
||||
uint8_t buf[2048];
|
||||
/* The task is a singleton (guarded by s_gb_task), so these large buffers
|
||||
* are kept static — on the stack they ate ~2.1 KB and, together with the
|
||||
* FATFS fread internals + i2s + logging, overflowed the 4 KB stack and
|
||||
* rebooted the board when a story started. */
|
||||
static uint8_t hdr[64];
|
||||
static uint8_t buf[2048];
|
||||
for (;;) {
|
||||
if (!s_gb_new) { vTaskDelay(pdMS_TO_TICKS(20)); continue; }
|
||||
char path[160];
|
||||
@@ -511,8 +513,15 @@ static void gb_play_task(void *arg)
|
||||
s_gb_new = false;
|
||||
s_gb_stop = false;
|
||||
|
||||
ensure_sd_mounted();
|
||||
/* The SD is normally already mounted (the gamebook mounts it at init).
|
||||
* Try the file directly first — re-mounting an already-mounted card via
|
||||
* bsp_sdcard_mount() FAILS and tears down the existing VFS, which made
|
||||
* the very first narration clip silent. Only mount if the open fails. */
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
ensure_sd_mounted();
|
||||
f = fopen(path, "rb");
|
||||
}
|
||||
if (!f) { ESP_LOGW(TAG, "gb: open %s failed", path); continue; }
|
||||
size_t hn = fread(hdr, 1, sizeof(hdr), f);
|
||||
wav_info_t wi;
|
||||
@@ -522,15 +531,23 @@ static void gb_play_task(void *arg)
|
||||
continue;
|
||||
}
|
||||
fseek(f, wi.data_offset, SEEK_SET);
|
||||
/* Wake the codec/PA before the real audio: after idle the ES8311 mutes
|
||||
* and ramps up on the first write, swallowing the start of the clip.
|
||||
* A short silent lead-in lets the unmute settle so the narration plays
|
||||
* from the very first word. */
|
||||
memset(buf, 0, sizeof(buf));
|
||||
for (int i = 0; i < 4 && s_spk_handle_local && !s_gb_stop && !s_gb_new; i++) {
|
||||
esp_codec_dev_write(s_spk_handle_local, buf, (int)sizeof(buf));
|
||||
}
|
||||
size_t remaining = wi.data_size;
|
||||
ESP_LOGI(TAG, "gb: play %s (%u B, %u Hz)", path,
|
||||
(unsigned)wi.data_size, (unsigned)wi.sample_rate);
|
||||
while (remaining > 0 && !s_gb_stop && !s_gb_new) {
|
||||
size_t want = remaining < sizeof(buf) ? remaining : sizeof(buf);
|
||||
size_t got = fread(buf, 1, want, f);
|
||||
if (got == 0) break;
|
||||
size_t written = 0;
|
||||
if (s_spk_handle_local) {
|
||||
i2s_channel_write(s_spk_handle_local, buf, got, &written,
|
||||
pdMS_TO_TICKS(500));
|
||||
esp_codec_dev_write(s_spk_handle_local, buf, (int)got);
|
||||
}
|
||||
remaining -= got;
|
||||
}
|
||||
@@ -546,7 +563,7 @@ void cmd_exec_play_file_async(const char *path)
|
||||
s_gb_stop = true; // interrupt whatever is playing
|
||||
s_gb_new = true; // … and pick up the new clip
|
||||
if (!s_gb_task) {
|
||||
xTaskCreate(gb_play_task, "gb_play", 4096, NULL, 4, &s_gb_task);
|
||||
xTaskCreate(gb_play_task, "gb_play", 6144, NULL, 4, &s_gb_task);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
#include <stddef.h>
|
||||
#include "esp_err.h"
|
||||
#include "driver/i2s_std.h"
|
||||
#include "esp_codec_dev.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -30,9 +30,9 @@ extern "C" {
|
||||
// ESP_ERR_NOT_SUPPORTED for unknown ops (all logged, no crash).
|
||||
esp_err_t cmd_exec_handle(const char *data, size_t len);
|
||||
|
||||
// Provide the speaker I2S channel handle so exec_play() can stream audio.
|
||||
// Provide the speaker codec (ES8311) handle so exec_play() can stream audio.
|
||||
// Must be called from main.c after speaker_init(), before any CMD arrives.
|
||||
void cmd_exec_set_spk_handle(i2s_chan_handle_t h);
|
||||
void cmd_exec_set_spk_handle(esp_codec_dev_handle_t h);
|
||||
|
||||
// Play a 16 kHz mono 16-bit WAV from the SD asynchronously, streamed in chunks
|
||||
// (RAM-safe for long narration) and INTERRUPTIBLE: a new call stops the current
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,10 @@
|
||||
|
||||
static const char *TAG = "gamebook";
|
||||
|
||||
// Custom fonts with French glyphs (ASCII + Latin-1 + ’ + …). See main/font_fr_*.c.
|
||||
LV_FONT_DECLARE(font_fr_14);
|
||||
LV_FONT_DECLARE(font_fr_24);
|
||||
|
||||
#define GB_DIR "/sdcard/gamebook"
|
||||
#define GB_LIBRARY GB_DIR "/library.json"
|
||||
#define GB_JSON_MAX (256 * 1024) // expanded books are ~80 KB of JSON
|
||||
@@ -80,7 +84,7 @@ static void ensure_root(void)
|
||||
static lv_obj_t *add_title(const char *txt)
|
||||
{
|
||||
lv_obj_t *l = lv_label_create(s_root);
|
||||
lv_obj_set_style_text_font(l, &lv_font_montserrat_24, 0);
|
||||
lv_obj_set_style_text_font(l, &font_fr_24, 0);
|
||||
lv_obj_set_style_text_color(l, lv_color_hex(COL_TITLE), 0);
|
||||
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(l, LV_PCT(100));
|
||||
@@ -91,7 +95,7 @@ static lv_obj_t *add_title(const char *txt)
|
||||
static lv_obj_t *add_text(const char *txt)
|
||||
{
|
||||
lv_obj_t *l = lv_label_create(s_root);
|
||||
lv_obj_set_style_text_font(l, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_font(l, &font_fr_14, 0);
|
||||
lv_obj_set_style_text_color(l, lv_color_hex(COL_TEXT), 0);
|
||||
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(l, LV_PCT(100));
|
||||
@@ -107,7 +111,7 @@ static lv_obj_t *add_button(const char *txt, lv_event_cb_t cb, void *user)
|
||||
lv_obj_set_style_pad_all(b, 10, 0);
|
||||
lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, user);
|
||||
lv_obj_t *l = lv_label_create(b);
|
||||
lv_obj_set_style_text_font(l, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_font(l, &font_fr_14, 0);
|
||||
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(l, LV_PCT(100));
|
||||
lv_label_set_text(l, txt);
|
||||
|
||||
+88
-72
@@ -40,8 +40,12 @@ static const char *TAG = "zacus-voice";
|
||||
|
||||
/* --------------- Shared state --------------- */
|
||||
|
||||
static i2s_chan_handle_t s_spk_handle = NULL; /* Speaker TX channel */
|
||||
static i2s_chan_handle_t s_mic_handle = NULL; /* Microphone RX channel */
|
||||
/* Audio now goes through the ES8311 (speaker) / ES7210 (mic) codecs via the
|
||||
* BSP + esp_codec_dev. bsp_audio_init() owns the single shared I2S port; we
|
||||
* must NOT create our own raw I2S channels (that left the ES8311 DAC unpowered
|
||||
* and the speaker silent). */
|
||||
static esp_codec_dev_handle_t s_spk_codec = NULL; /* ES8311 output (DAC) */
|
||||
static esp_codec_dev_handle_t s_mic_codec = NULL; /* ES7210 input (ADC) */
|
||||
static volatile bool s_wifi_connected = false;
|
||||
static volatile bool s_voice_streaming = false;
|
||||
|
||||
@@ -114,11 +118,10 @@ static esp_err_t wifi_init_sta(void)
|
||||
// generator can sequence a melody for the master's microphone.
|
||||
void audio_play_tone(float frequency, int duration_ms)
|
||||
{
|
||||
if (!s_spk_handle || duration_ms <= 0) return;
|
||||
if (!s_spk_codec || duration_ms <= 0) return;
|
||||
const int total_samples = AUDIO_SAMPLE_RATE * duration_ms / 1000;
|
||||
const float amplitude = 16000.0f;
|
||||
int16_t buffer[256];
|
||||
size_t bytes_written = 0;
|
||||
int sample_idx = 0;
|
||||
while (sample_idx < total_samples) {
|
||||
int chunk = (total_samples - sample_idx < 256)
|
||||
@@ -127,29 +130,27 @@ void audio_play_tone(float frequency, int duration_ms)
|
||||
float t = (float)(sample_idx + i) / (float)AUDIO_SAMPLE_RATE;
|
||||
buffer[i] = (int16_t)(amplitude * sinf(2.0f * M_PI * frequency * t));
|
||||
}
|
||||
i2s_channel_write(s_spk_handle, buffer, chunk * sizeof(int16_t),
|
||||
&bytes_written, portMAX_DELAY);
|
||||
esp_codec_dev_write(s_spk_codec, buffer, chunk * sizeof(int16_t));
|
||||
sample_idx += chunk;
|
||||
}
|
||||
}
|
||||
|
||||
static void audio_test_tone(void)
|
||||
{
|
||||
if (!s_spk_handle) {
|
||||
if (!s_spk_codec) {
|
||||
ESP_LOGW(TAG, "Speaker not initialized, skipping test tone");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Playing 440 Hz test tone (1 second)...");
|
||||
|
||||
/* Generate 1 second of 440 Hz sine wave using persistent speaker handle */
|
||||
/* Generate 1 second of 440 Hz sine wave through the ES8311 codec */
|
||||
const int duration_ms = 1000;
|
||||
const int total_samples = AUDIO_SAMPLE_RATE * duration_ms / 1000;
|
||||
const float frequency = 440.0f;
|
||||
const float amplitude = 16000.0f;
|
||||
|
||||
int16_t buffer[256];
|
||||
size_t bytes_written = 0;
|
||||
int sample_idx = 0;
|
||||
|
||||
while (sample_idx < total_samples) {
|
||||
@@ -158,7 +159,7 @@ static void audio_test_tone(void)
|
||||
float t = (float)(sample_idx + i) / (float)AUDIO_SAMPLE_RATE;
|
||||
buffer[i] = (int16_t)(amplitude * sinf(2.0f * M_PI * frequency * t));
|
||||
}
|
||||
i2s_channel_write(s_spk_handle, buffer, chunk * sizeof(int16_t), &bytes_written, portMAX_DELAY);
|
||||
esp_codec_dev_write(s_spk_codec, buffer, chunk * sizeof(int16_t));
|
||||
sample_idx += chunk;
|
||||
}
|
||||
|
||||
@@ -174,15 +175,13 @@ static void tts_audio_callback(const uint8_t *data, size_t len)
|
||||
* Called from WebSocket event context with incoming TTS PCM16 data.
|
||||
* Write directly to the speaker I2S channel.
|
||||
*/
|
||||
if (!s_spk_handle || len == 0) {
|
||||
if (!s_spk_codec || len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t bytes_written = 0;
|
||||
esp_err_t ret = i2s_channel_write(s_spk_handle, data, len,
|
||||
&bytes_written, pdMS_TO_TICKS(200));
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "TTS write to speaker failed: %s", esp_err_to_name(ret));
|
||||
int ret = esp_codec_dev_write(s_spk_codec, (void *)data, (int)len);
|
||||
if (ret != ESP_CODEC_DEV_OK) {
|
||||
ESP_LOGW(TAG, "TTS write to speaker failed: %d", ret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,34 +189,42 @@ static void tts_audio_callback(const uint8_t *data, size_t len)
|
||||
|
||||
static esp_err_t speaker_init(void)
|
||||
{
|
||||
/* Enable power amplifier */
|
||||
/* Init the ES8311 output codec via the BSP. This also runs bsp_audio_init()
|
||||
* (shared duplex I2S port) + bsp_i2c_init() and configures the power
|
||||
* amplifier (BSP_POWER_AMP_IO = GPIO46). The ES8311 DAC is configured over
|
||||
* I2C here — without this the speaker stays muted no matter what we push on
|
||||
* I2S. */
|
||||
s_spk_codec = bsp_audio_codec_speaker_init();
|
||||
if (!s_spk_codec) {
|
||||
ESP_LOGE(TAG, "bsp_audio_codec_speaker_init failed — speaker unavailable");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* All speaker audio in this app is 16 kHz / 16-bit / mono PCM. */
|
||||
esp_codec_dev_sample_info_t fs = {
|
||||
.bits_per_sample = AUDIO_BITS,
|
||||
.channel = AUDIO_CHANNELS,
|
||||
.channel_mask = 0,
|
||||
.sample_rate = AUDIO_SAMPLE_RATE,
|
||||
.mclk_multiple = 0,
|
||||
};
|
||||
int ret = esp_codec_dev_open(s_spk_codec, &fs);
|
||||
if (ret != ESP_CODEC_DEV_OK) {
|
||||
ESP_LOGE(TAG, "esp_codec_dev_open (speaker) failed: %d", ret);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Output volume 0..100. */
|
||||
esp_codec_dev_set_out_vol(s_spk_codec, 100);
|
||||
|
||||
/* Belt-and-suspenders: the es8311 driver is configured with pa_pin=GPIO46
|
||||
* (pa_reverted=false) and should raise it on open, but the speaker stays
|
||||
* silent in practice — force the power amplifier on explicitly. */
|
||||
gpio_set_direction(BOX3_PA_ENABLE, GPIO_MODE_OUTPUT);
|
||||
gpio_set_level(BOX3_PA_ENABLE, 1);
|
||||
|
||||
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
|
||||
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &s_spk_handle, NULL));
|
||||
|
||||
i2s_std_config_t std_cfg = {
|
||||
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(AUDIO_SAMPLE_RATE),
|
||||
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
|
||||
.gpio_cfg = {
|
||||
.mclk = BOX3_I2S_MCLK,
|
||||
.bclk = BOX3_I2S_BCLK,
|
||||
.ws = BOX3_I2S_WS,
|
||||
.dout = BOX3_I2S_DOUT,
|
||||
.din = I2S_GPIO_UNUSED,
|
||||
.invert_flags = {
|
||||
.mclk_inv = false,
|
||||
.bclk_inv = false,
|
||||
.ws_inv = false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(i2s_channel_init_std_mode(s_spk_handle, &std_cfg));
|
||||
ESP_ERROR_CHECK(i2s_channel_enable(s_spk_handle));
|
||||
|
||||
ESP_LOGI(TAG, "Speaker I2S initialized (I2S_NUM_0)");
|
||||
ESP_LOGI(TAG, "Speaker codec (ES8311) initialized @ %d Hz, vol=100, PA on",
|
||||
AUDIO_SAMPLE_RATE);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -227,43 +234,45 @@ static void mic_monitor_task(void *arg)
|
||||
{
|
||||
ESP_LOGI(TAG, "Starting mic capture task...");
|
||||
|
||||
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_1, I2S_ROLE_MASTER);
|
||||
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, NULL, &s_mic_handle));
|
||||
/* Init the ES7210 input codec via the BSP. It shares the same I2S port
|
||||
* (already created by speaker_init -> bsp_audio_init); creating a separate
|
||||
* raw I2S channel here would conflict with that shared port. */
|
||||
s_mic_codec = bsp_audio_codec_microphone_init();
|
||||
if (!s_mic_codec) {
|
||||
ESP_LOGE(TAG, "bsp_audio_codec_microphone_init failed — mic unavailable");
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
i2s_std_config_t std_cfg = {
|
||||
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(AUDIO_SAMPLE_RATE),
|
||||
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
|
||||
.gpio_cfg = {
|
||||
.mclk = I2S_GPIO_UNUSED,
|
||||
.bclk = BOX3_I2S_BCLK,
|
||||
.ws = BOX3_I2S_WS,
|
||||
.dout = I2S_GPIO_UNUSED,
|
||||
.din = BOX3_I2S_DIN,
|
||||
.invert_flags = {
|
||||
.mclk_inv = false,
|
||||
.bclk_inv = false,
|
||||
.ws_inv = false,
|
||||
},
|
||||
},
|
||||
esp_codec_dev_sample_info_t fs = {
|
||||
.bits_per_sample = AUDIO_BITS,
|
||||
.channel = AUDIO_CHANNELS,
|
||||
.channel_mask = 0,
|
||||
.sample_rate = AUDIO_SAMPLE_RATE,
|
||||
.mclk_multiple = 0,
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(i2s_channel_init_std_mode(s_mic_handle, &std_cfg));
|
||||
ESP_ERROR_CHECK(i2s_channel_enable(s_mic_handle));
|
||||
int oret = esp_codec_dev_open(s_mic_codec, &fs);
|
||||
if (oret != ESP_CODEC_DEV_OK) {
|
||||
ESP_LOGE(TAG, "esp_codec_dev_open (mic) failed: %d", oret);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
/* ES7210 input gain (dB). 30 dB is a sane default for the BOX-3 mic array. */
|
||||
esp_codec_dev_set_in_gain(s_mic_codec, 30.0f);
|
||||
|
||||
int16_t buffer[AUDIO_FRAME_SAMPLES];
|
||||
size_t bytes_read = 0;
|
||||
int rms_log_counter = 0;
|
||||
|
||||
while (1) {
|
||||
esp_err_t ret = i2s_channel_read(s_mic_handle, buffer,
|
||||
AUDIO_FRAME_SAMPLES * sizeof(int16_t),
|
||||
&bytes_read, pdMS_TO_TICKS(1000));
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Mic read error: %s", esp_err_to_name(ret));
|
||||
int ret = esp_codec_dev_read(s_mic_codec, buffer,
|
||||
AUDIO_FRAME_SAMPLES * sizeof(int16_t));
|
||||
if (ret != ESP_CODEC_DEV_OK) {
|
||||
ESP_LOGW(TAG, "Mic read error: %d", ret);
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
continue;
|
||||
}
|
||||
|
||||
int samples = bytes_read / sizeof(int16_t);
|
||||
int samples = AUDIO_FRAME_SAMPLES;
|
||||
|
||||
/* If voice streaming is active, send PCM to bridge */
|
||||
if (s_voice_streaming) {
|
||||
@@ -423,14 +432,19 @@ void app_main(void)
|
||||
/* Initialize persistent speaker output (used for test tone + TTS playback) */
|
||||
speaker_init();
|
||||
|
||||
/* Pass speaker handle to CMD executor for real WAV playback */
|
||||
cmd_exec_set_spk_handle(s_spk_handle);
|
||||
/* Pass speaker codec handle to CMD executor for real WAV playback */
|
||||
cmd_exec_set_spk_handle(s_spk_codec);
|
||||
|
||||
/* Play test tone to verify audio output */
|
||||
audio_test_tone();
|
||||
|
||||
/* Start mic capture task (reads I2S mic, streams to bridge when active) */
|
||||
#if CONFIG_BOX3_VOICE_STREAMING
|
||||
/* Start mic capture task (reads I2S mic, streams to bridge when active).
|
||||
* Off by default: as a touch gamebook the BOX-3 needs no mic, and the
|
||||
* continuous codec reads + bridge reconnects interfered with the speaker
|
||||
* (the first narration clip came out silent) and spammed the log. */
|
||||
xTaskCreatePinnedToCore(mic_monitor_task, "mic_capture", 4096, NULL, 5, NULL, 1);
|
||||
#endif
|
||||
|
||||
/* Start button handler (BOOT button toggles voice streaming) */
|
||||
xTaskCreate(button_task, "button", 2048, NULL, 4, NULL);
|
||||
@@ -439,8 +453,10 @@ void app_main(void)
|
||||
ESP_LOGI(TAG, "Connecting to WiFi...");
|
||||
wifi_init_sta();
|
||||
|
||||
#if CONFIG_BOX3_VOICE_STREAMING
|
||||
/* Start voice bridge connection task (waits for WiFi, then connects WS) */
|
||||
xTaskCreate(voice_bridge_task, "voice_bridge", 6144, NULL, 5, NULL);
|
||||
#endif
|
||||
|
||||
/* Start the scenario hot-load HTTP server (POST /game/scenario).
|
||||
* httpd_start binds to all netifs — works as soon as the WiFi STA has an IP. */
|
||||
@@ -450,7 +466,7 @@ void app_main(void)
|
||||
/* Phone-less PLIP annex: same REST contract as PLIP_FIRMWARE
|
||||
* (POST /ring /stop /play, GET /status), ring on the speaker,
|
||||
* BOOT button as the virtual hook switch. */
|
||||
if (plip_virtual_init(scenario_server_handle(), s_spk_handle) != ESP_OK) {
|
||||
if (plip_virtual_init(scenario_server_handle(), s_spk_codec) != ESP_OK) {
|
||||
ESP_LOGW(TAG, "plip_virtual_init failed — virtual phone unavailable");
|
||||
} else if (plip_ui_init() != ESP_OK) {
|
||||
/* REST/ESP-NOW phone still works headless if the UI fails. */
|
||||
|
||||
@@ -38,7 +38,7 @@ typedef struct {
|
||||
char reason[32]; /* "pickup" | "hangup" | "ring_timeout" | "dial:<n>" */
|
||||
} hook_event_t;
|
||||
|
||||
static i2s_chan_handle_t s_spk;
|
||||
static esp_codec_dev_handle_t s_spk;
|
||||
static volatile plip_hook_state_t s_state = PLIP_HOOK_ON;
|
||||
static volatile bool s_ring_stop = false;
|
||||
static TaskHandle_t s_ring_task = NULL;
|
||||
@@ -107,7 +107,6 @@ static void ring_burst(int duration_ms)
|
||||
{
|
||||
const int total = AUDIO_SAMPLE_RATE * duration_ms / 1000;
|
||||
int16_t buffer[256];
|
||||
size_t written = 0;
|
||||
int idx = 0;
|
||||
while (idx < total && !s_ring_stop) {
|
||||
int chunk = (total - idx < 256) ? (total - idx) : 256;
|
||||
@@ -116,8 +115,9 @@ static void ring_burst(int duration_ms)
|
||||
buffer[i] = (int16_t) (RING_AMPLITUDE *
|
||||
sinf(2.0f * (float) M_PI * RING_FREQ_HZ * t));
|
||||
}
|
||||
i2s_channel_write(s_spk, buffer, chunk * sizeof(int16_t), &written,
|
||||
pdMS_TO_TICKS(500));
|
||||
if (s_spk) {
|
||||
esp_codec_dev_write(s_spk, buffer, chunk * sizeof(int16_t));
|
||||
}
|
||||
idx += chunk;
|
||||
}
|
||||
}
|
||||
@@ -279,7 +279,7 @@ plip_hook_state_t plip_virtual_state(void)
|
||||
return s_state;
|
||||
}
|
||||
|
||||
esp_err_t plip_virtual_init(httpd_handle_t server, i2s_chan_handle_t spk)
|
||||
esp_err_t plip_virtual_init(httpd_handle_t server, esp_codec_dev_handle_t spk)
|
||||
{
|
||||
if (!server || !spk) return ESP_ERR_INVALID_ARG;
|
||||
s_spk = spk;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "driver/i2s_std.h"
|
||||
#include "esp_codec_dev.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_http_server.h"
|
||||
|
||||
@@ -34,7 +34,7 @@ typedef enum {
|
||||
// Register the REST handlers on `server` and keep `spk` for the ring tone.
|
||||
// Call once after scenario_server_start(); the speaker channel must be
|
||||
// enabled (speaker_init() in main.c).
|
||||
esp_err_t plip_virtual_init(httpd_handle_t server, i2s_chan_handle_t spk);
|
||||
esp_err_t plip_virtual_init(httpd_handle_t server, esp_codec_dev_handle_t spk);
|
||||
|
||||
// Forward a BOOT-button press. Returns true when the press was consumed as
|
||||
// a hook transition (pickup/hangup) — the caller should then skip its own
|
||||
|
||||
Reference in New Issue
Block a user