feat(voice): add on-device Miao Zhang Gui wake word

Add an optional local wake response prompt configuration. The prompt asset is intentionally not included yet.
This commit is contained in:
chenjintao
2026-08-10 16:54:42 +08:00
parent d9660164a6
commit e5c1564130
21 changed files with 2010 additions and 56 deletions
+1
View File
@@ -6,6 +6,7 @@ idf_component_register(
"src/multimodal/control_plane_model_command_extractor.c"
"src/multimodal/control_plane_multimodal_protocol.c"
"src/multimodal/control_plane_multimodal_runtime.c"
"src/multimodal/control_plane_multimodal_local_wake.c"
"src/multimodal/control_plane_multimodal_device_auth.c"
"src/multimodal/control_plane_multimodal_auth_service.c"
"src/multimodal/control_plane_multimodal_session_service.c"
@@ -133,6 +133,11 @@ esp_err_t control_plane_multimodal_play_downstream_audio(const uint8_t *data, si
esp_err_t control_plane_multimodal_handle_ptt_press_step(void);
esp_err_t control_plane_multimodal_handle_ptt_release(void);
void control_plane_multimodal_reset_uplink_failure_window(void);
esp_err_t control_plane_local_wake_start(void);
esp_err_t control_plane_local_wake_stop(int timeout_ms);
void control_plane_local_wake_pause_for_foreground(void);
void control_plane_local_wake_resume_after_foreground(void);
bool control_plane_voice_turn_is_idle(void);
esp_err_t control_plane_start_system_event_task(void);
void control_plane_stop_system_event_task(void);
@@ -0,0 +1,384 @@
#include "control_plane_internal.h"
#include <stdint.h>
#include "domain.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#ifndef CONFIG_TQ_LOCAL_WAKE_ENABLE
#define CONFIG_TQ_LOCAL_WAKE_ENABLE 0
#endif
#ifndef CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE
#define CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE 0
#endif
#ifndef CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE
#define CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE \
"/littlefs/audio_prompts/local_wake_response.opus"
#endif
#ifndef CONFIG_TQ_LOCAL_WAKE_RETRY_MS
#define CONFIG_TQ_LOCAL_WAKE_RETRY_MS 5000
#endif
#ifndef CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS
#define CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS 1000
#endif
#ifndef CONFIG_TQ_LOCAL_WAKE_TASK_STACK_BYTES
#define CONFIG_TQ_LOCAL_WAKE_TASK_STACK_BYTES 6144
#endif
#ifndef CONFIG_TQ_LOCAL_WAKE_TASK_PRIORITY
#define CONFIG_TQ_LOCAL_WAKE_TASK_PRIORITY 4
#endif
#ifndef CONFIG_TQ_LOCAL_WAKE_TASK_CORE
#define CONFIG_TQ_LOCAL_WAKE_TASK_CORE 0
#endif
#define CONTROL_PLANE_LOCAL_WAKE_QUEUE_LEN 4U
#define CONTROL_PLANE_LOCAL_WAKE_POLL_MS 100U
#if CONFIG_TQ_LOCAL_WAKE_TASK_CORE < 0
#define CONTROL_PLANE_LOCAL_WAKE_TASK_CORE tskNO_AFFINITY
#else
#define CONTROL_PLANE_LOCAL_WAKE_TASK_CORE CONFIG_TQ_LOCAL_WAKE_TASK_CORE
#endif
typedef struct {
TaskHandle_t task;
QueueHandle_t detection_queue;
bool stop_requested;
bool started;
bool paused_for_foreground;
bool capture_owned;
} local_wake_state_t;
static const char *TAG = "cp_local_wake";
static local_wake_state_t s_local_wake;
static portMUX_TYPE s_local_wake_lock = portMUX_INITIALIZER_UNLOCKED;
static local_wake_state_t local_wake_snapshot(void)
{
local_wake_state_t snapshot;
portENTER_CRITICAL(&s_local_wake_lock);
snapshot = s_local_wake;
portEXIT_CRITICAL(&s_local_wake_lock);
return snapshot;
}
static void local_wake_set_capture_owned(bool owned)
{
portENTER_CRITICAL(&s_local_wake_lock);
s_local_wake.capture_owned = owned;
portEXIT_CRITICAL(&s_local_wake_lock);
}
static void local_wake_domain_event(
const domain_local_command_event_t *event,
void *user_data)
{
(void)user_data;
local_wake_state_t state = local_wake_snapshot();
if (event == NULL || state.stop_requested || state.detection_queue == NULL) {
return;
}
if (xQueueSend(state.detection_queue, event, 0U) != pdTRUE) {
ESP_EARLY_LOGW(TAG, "Drop local wake detection: queue full");
}
}
static void local_wake_stop_owned_capture(void)
{
local_wake_state_t state = local_wake_snapshot();
if (!state.capture_owned) {
return;
}
local_wake_set_capture_owned(false);
esp_err_t err = domain_runtime_multimodal_audio_stop_uplink(
CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS);
if (err != ESP_OK && err != ESP_ERR_TIMEOUT) {
ESP_LOGW(TAG, "Stop local wake capture failed: %s", esp_err_to_name(err));
}
}
static esp_err_t local_wake_start_listening(void)
{
local_wake_state_t state = local_wake_snapshot();
if (state.stop_requested || state.paused_for_foreground ||
!control_plane_voice_turn_is_idle()) {
return ESP_ERR_INVALID_STATE;
}
if (!domain_runtime_multimodal_audio_is_initialized()) {
esp_err_t init_err = domain_runtime_multimodal_audio_init();
if (init_err != ESP_OK) {
return init_err;
}
}
domain_local_command_snapshot_t command_state = {0};
esp_err_t snapshot_err =
domain_runtime_local_command_get_snapshot(&command_state);
if (snapshot_err != ESP_OK || !command_state.started) {
esp_err_t start_err = domain_runtime_local_command_start(
local_wake_domain_event,
NULL);
if (start_err != ESP_OK) {
return start_err;
}
} else if (!command_state.detection_running) {
esp_err_t resume_err = domain_runtime_local_command_resume(
CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS);
if (resume_err != ESP_OK) {
return resume_err;
}
}
if (!domain_runtime_multimodal_audio_is_uplink_running()) {
esp_err_t capture_err =
domain_runtime_multimodal_audio_start_uplink(NULL, NULL);
if (capture_err != ESP_OK) {
return capture_err;
}
}
local_wake_set_capture_owned(true);
return ESP_OK;
}
static void local_wake_handle_detection(
const domain_local_command_event_t *event)
{
if (event == NULL ||
event->type != DOMAIN_LOCAL_COMMAND_EVENT_DETECTED ||
event->command_id != DOMAIN_LOCAL_COMMAND_MIAO_ZHANG_GUI) {
return;
}
portENTER_CRITICAL(&s_local_wake_lock);
if (s_local_wake.stop_requested || s_local_wake.paused_for_foreground) {
portEXIT_CRITICAL(&s_local_wake_lock);
return;
}
s_local_wake.paused_for_foreground = true;
portEXIT_CRITICAL(&s_local_wake_lock);
local_wake_stop_owned_capture();
(void)domain_runtime_local_command_suspend(
CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS);
ESP_LOGI(TAG,
"Local wake accepted: phrase=喵掌柜 confidence_x10000=%u generation=%u",
(unsigned)event->confidence_x10000,
(unsigned)event->detection_generation);
#if CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE
if (CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE[0] != '\0') {
esp_err_t prompt_err = domain_runtime_behavior_play_prompt_file(
CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE);
if (prompt_err != ESP_OK) {
ESP_LOGW(TAG,
"Local wake response prompt failed: %s",
esp_err_to_name(prompt_err));
}
}
#endif
control_plane_set_ptt_active(true);
}
static void local_wake_task(void *arg)
{
(void)arg;
TickType_t retry_not_before = 0U;
bool listening_logged = false;
ESP_LOGI(TAG, "Local wake worker started: phrase=喵掌柜");
while (!local_wake_snapshot().stop_requested) {
local_wake_state_t state = local_wake_snapshot();
TickType_t now = xTaskGetTickCount();
if (!state.paused_for_foreground &&
control_plane_voice_turn_is_idle() &&
(retry_not_before == 0U || now >= retry_not_before)) {
esp_err_t listen_err = local_wake_start_listening();
if (listen_err == ESP_OK) {
retry_not_before = 0U;
if (!listening_logged) {
ESP_LOGI(TAG, "Local wake listening ready: phrase=喵掌柜");
listening_logged = true;
}
} else if (listen_err != ESP_ERR_INVALID_STATE) {
ESP_LOGW(TAG,
"Local wake listener unavailable: %s; retry in %ums",
esp_err_to_name(listen_err),
(unsigned)CONFIG_TQ_LOCAL_WAKE_RETRY_MS);
retry_not_before =
now + pdMS_TO_TICKS(CONFIG_TQ_LOCAL_WAKE_RETRY_MS);
listening_logged = false;
}
}
domain_local_command_event_t event = {0};
if (state.detection_queue != NULL &&
xQueueReceive(state.detection_queue,
&event,
pdMS_TO_TICKS(CONTROL_PLANE_LOCAL_WAKE_POLL_MS)) ==
pdTRUE) {
local_wake_handle_detection(&event);
} else {
ulTaskNotifyTake(pdTRUE, 0U);
}
}
local_wake_stop_owned_capture();
(void)domain_runtime_local_command_stop(
CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS);
portENTER_CRITICAL(&s_local_wake_lock);
s_local_wake.task = NULL;
s_local_wake.started = false;
portEXIT_CRITICAL(&s_local_wake_lock);
ESP_LOGI(TAG, "Local wake worker stopped");
vTaskDelete(NULL);
}
esp_err_t control_plane_local_wake_start(void)
{
#if !CONFIG_TQ_LOCAL_WAKE_ENABLE
return ESP_OK;
#else
portENTER_CRITICAL(&s_local_wake_lock);
if (s_local_wake.task != NULL || s_local_wake.started) {
portEXIT_CRITICAL(&s_local_wake_lock);
return ESP_OK;
}
portEXIT_CRITICAL(&s_local_wake_lock);
if (s_local_wake.detection_queue == NULL) {
QueueHandle_t queue = xQueueCreate(
CONTROL_PLANE_LOCAL_WAKE_QUEUE_LEN,
sizeof(domain_local_command_event_t));
if (queue == NULL) {
return ESP_ERR_NO_MEM;
}
portENTER_CRITICAL(&s_local_wake_lock);
if (s_local_wake.detection_queue == NULL) {
s_local_wake.detection_queue = queue;
queue = NULL;
}
portEXIT_CRITICAL(&s_local_wake_lock);
if (queue != NULL) {
vQueueDelete(queue);
}
}
portENTER_CRITICAL(&s_local_wake_lock);
s_local_wake.stop_requested = false;
s_local_wake.paused_for_foreground = false;
s_local_wake.capture_owned = false;
s_local_wake.started = true;
portEXIT_CRITICAL(&s_local_wake_lock);
TaskHandle_t task = NULL;
BaseType_t created = xTaskCreatePinnedToCore(
local_wake_task,
"cp_local_wake",
CONFIG_TQ_LOCAL_WAKE_TASK_STACK_BYTES,
NULL,
CONFIG_TQ_LOCAL_WAKE_TASK_PRIORITY,
&task,
CONTROL_PLANE_LOCAL_WAKE_TASK_CORE);
if (created != pdPASS) {
portENTER_CRITICAL(&s_local_wake_lock);
s_local_wake.started = false;
portEXIT_CRITICAL(&s_local_wake_lock);
return ESP_ERR_NO_MEM;
}
portENTER_CRITICAL(&s_local_wake_lock);
s_local_wake.task = task;
portEXIT_CRITICAL(&s_local_wake_lock);
return ESP_OK;
#endif
}
esp_err_t control_plane_local_wake_stop(int timeout_ms)
{
#if !CONFIG_TQ_LOCAL_WAKE_ENABLE
(void)timeout_ms;
return ESP_OK;
#else
if (timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
local_wake_state_t state = local_wake_snapshot();
if (!state.started && state.task == NULL) {
return ESP_OK;
}
portENTER_CRITICAL(&s_local_wake_lock);
s_local_wake.stop_requested = true;
TaskHandle_t task = s_local_wake.task;
portEXIT_CRITICAL(&s_local_wake_lock);
if (task != NULL) {
xTaskNotifyGive(task);
}
TickType_t started = xTaskGetTickCount();
TickType_t timeout_ticks = pdMS_TO_TICKS((uint32_t)timeout_ms);
if (timeout_ms > 0 && timeout_ticks == 0U) {
timeout_ticks = 1U;
}
while (local_wake_snapshot().task != NULL &&
timeout_ms > 0 &&
(xTaskGetTickCount() - started) < timeout_ticks) {
vTaskDelay(pdMS_TO_TICKS(20));
}
state = local_wake_snapshot();
if (state.task != NULL) {
vTaskDelete(state.task);
local_wake_stop_owned_capture();
(void)domain_runtime_local_command_stop(
CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS);
portENTER_CRITICAL(&s_local_wake_lock);
s_local_wake.task = NULL;
s_local_wake.started = false;
portEXIT_CRITICAL(&s_local_wake_lock);
return ESP_ERR_TIMEOUT;
}
return ESP_OK;
#endif
}
void control_plane_local_wake_pause_for_foreground(void)
{
#if CONFIG_TQ_LOCAL_WAKE_ENABLE
portENTER_CRITICAL(&s_local_wake_lock);
s_local_wake.paused_for_foreground = true;
TaskHandle_t task = s_local_wake.task;
portEXIT_CRITICAL(&s_local_wake_lock);
(void)domain_runtime_local_command_suspend(
CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS);
local_wake_stop_owned_capture();
if (task != NULL) {
xTaskNotifyGive(task);
}
#endif
}
void control_plane_local_wake_resume_after_foreground(void)
{
#if CONFIG_TQ_LOCAL_WAKE_ENABLE
portENTER_CRITICAL(&s_local_wake_lock);
s_local_wake.paused_for_foreground = false;
TaskHandle_t task = s_local_wake.task;
portEXIT_CRITICAL(&s_local_wake_lock);
if (task != NULL) {
xTaskNotifyGive(task);
}
#endif
}
@@ -575,6 +575,16 @@ static void control_plane_multimodal_event_bridge(control_plane_multimodal_event
ESP_LOGI(TAG, "Pause uplink during TTS responding");
control_plane_multimodal_stop_uplink();
}
#if CONFIG_TQ_PTT_MODE_CLICK_TO_TALK
if (!control_plane_voice_turn_is_idle()) {
/*
* The server response closes this one-shot voice turn. Once
* teardown reaches IDLE, the local listener can resume with
* AEC-clean PCM and accept 喵掌柜 while TTS is playing.
*/
control_plane_set_ptt_active(false);
}
#endif
break;
}
@@ -67,12 +67,14 @@
#define CONFIG_MULTIMODAL_OPUS_BITRATE_KBPS 24
#endif
#define CONTROL_PLANE_MULTIMODAL_OPUS_CHANNELS 1
#if CONFIG_MULTIMODAL_AUDIO_CODEC_SAMPLE_RATE != 16000
#error "Multimodal voice requires 16 kHz audio"
#endif
#if CONFIG_MULTIMODAL_AUDIO_CODEC_CHANNELS != 1
#error "Multimodal voice requires mono audio"
#if CONFIG_MULTIMODAL_AUDIO_CODEC_CHANNELS != 2
#error "Multimodal AEC requires stereo codec capture"
#endif
#if CONFIG_MULTIMODAL_OPUS_FRAME_DURATION_MS != 60
@@ -1943,7 +1945,7 @@ esp_err_t control_plane_multimodal_send_start(void)
(unsigned)sample_rate,
(unsigned)opus_frame_duration_ms,
(unsigned)CONFIG_MULTIMODAL_OPUS_BITRATE_KBPS,
CONFIG_MULTIMODAL_AUDIO_CODEC_CHANNELS,
CONTROL_PLANE_MULTIMODAL_OPUS_CHANNELS,
CONFIG_MULTIMODAL_AUDIO_PA_GPIO);
return ESP_OK;
@@ -88,6 +88,10 @@
#define CONFIG_TQ_PTT_CLICK_CAPTURE_TIMEOUT_MS 8000
#endif
#ifndef CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS
#define CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS 1000
#endif
#ifndef CONFIG_TQ_NETWORK_TASK_CORE
#define CONFIG_TQ_NETWORK_TASK_CORE 0
#endif
@@ -303,6 +307,9 @@ static void control_plane_notify_wifi_state_task(void)
void control_plane_set_ptt_active(bool active)
{
if (active) {
control_plane_local_wake_pause_for_foreground();
}
bool notify_worker = false;
ptt_state_t old_state = PTT_STATE_IDLE;
ptt_state_t new_state = PTT_STATE_IDLE;
@@ -356,6 +363,20 @@ void control_plane_set_ptt_active(bool active)
if (notify_worker) {
control_plane_notify_wifi_state_task();
}
if (!active) {
control_plane_local_wake_resume_after_foreground();
}
}
bool control_plane_voice_turn_is_idle(void)
{
bool idle = false;
portENTER_CRITICAL(&s_runtime_lock);
idle = s_ptt_runtime.state == PTT_STATE_IDLE &&
!s_ptt_runtime.active &&
!s_ptt_runtime.pending_press_after_teardown;
portEXIT_CRITICAL(&s_runtime_lock);
return idle;
}
static bool control_plane_get_ptt_active(void)
@@ -1880,6 +1901,15 @@ esp_err_t control_plane_start_runtime_workers(void)
return key_input_err;
}
esp_err_t local_wake_err = control_plane_local_wake_start();
if (local_wake_err != ESP_OK) {
ESP_LOGE(TAG,
"Start local wake worker failed: %s",
esp_err_to_name(local_wake_err));
control_plane_stop_runtime_workers();
return local_wake_err;
}
esp_err_t remote_log_err = control_plane_remote_log_start();
if (remote_log_err != ESP_OK && remote_log_err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGW(TAG, "Remote log start failed: %s", esp_err_to_name(remote_log_err));
@@ -1946,6 +1976,7 @@ void control_plane_stop_runtime_workers(void)
{
control_plane_set_runtime_stop_requested(true);
control_plane_stop_user_config_server();
(void)control_plane_local_wake_stop(CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS);
#if CONFIG_TQ_FACTORY_CLI_ENABLE
control_plane_factory_cli_stop();
+2
View File
@@ -8,12 +8,14 @@ idf_component_register(
"src/fortune/domain_fortune_store.c"
"src/fortune/domain_user_config_server.c"
"src/runtime/domain_runtime_adapters.c"
"src/runtime/domain_local_command.c"
INCLUDE_DIRS
"include"
PRIV_INCLUDE_DIRS
"internal"
PRIV_REQUIRES
platform
esp-sr
log
json
EMBED_TXTFILES
+42
View File
@@ -66,6 +66,34 @@ typedef esp_err_t (*domain_multimodal_audio_uplink_cb_t)(
size_t len,
void *user_data);
typedef enum {
DOMAIN_LOCAL_COMMAND_MIAO_ZHANG_GUI = 1,
} domain_local_command_id_t;
typedef enum {
DOMAIN_LOCAL_COMMAND_EVENT_DETECTED = 0,
} domain_local_command_event_type_t;
typedef struct {
domain_local_command_event_type_t type;
domain_local_command_id_t command_id;
uint32_t confidence_x10000;
uint32_t detection_generation;
} domain_local_command_event_t;
typedef void (*domain_local_command_event_callback_t)(
const domain_local_command_event_t *event,
void *user_data);
typedef struct {
bool started;
bool detection_running;
uint32_t detection_generation;
uint32_t detection_count;
uint32_t dropped_pcm_frames;
uint32_t task_stack_high_water_bytes;
} domain_local_command_snapshot_t;
typedef enum {
DOMAIN_MULTIMODAL_DOWNSTREAM_PLAYBACK_STARTED = 0,
DOMAIN_MULTIMODAL_DOWNSTREAM_PLAYBACK_COMPLETED,
@@ -180,6 +208,20 @@ esp_err_t domain_runtime_multimodal_audio_start_uplink(
esp_err_t domain_runtime_multimodal_audio_stop_uplink(int timeout_ms);
bool domain_runtime_multimodal_audio_is_uplink_running(void);
bool domain_runtime_multimodal_audio_is_initialized(void);
/**
* @brief Start local MultiNet detection for the Chinese phrase "喵掌柜".
*
* The callback runs in the domain inference task and must be bounded and
* non-blocking. Repeated start with the same callback is idempotent.
*/
esp_err_t domain_runtime_local_command_start(
domain_local_command_event_callback_t callback,
void *user_data);
esp_err_t domain_runtime_local_command_suspend(int timeout_ms);
esp_err_t domain_runtime_local_command_resume(int timeout_ms);
esp_err_t domain_runtime_local_command_stop(int timeout_ms);
esp_err_t domain_runtime_local_command_get_snapshot(
domain_local_command_snapshot_t *out_snapshot);
esp_err_t domain_runtime_multimodal_audio_play_downstream_opus(const uint8_t *data, size_t len);
esp_err_t domain_runtime_multimodal_audio_begin_downstream_response(void);
esp_err_t domain_runtime_multimodal_audio_finish_downstream_response(void);
@@ -0,0 +1,976 @@
#include "domain.h"
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_mn_iface.h"
#include "esp_mn_models.h"
#include "esp_mn_speech_commands.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/idf_additions.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "model_path.h"
#include "platform.h"
#include "sdkconfig.h"
#ifndef CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES
#define CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES 8192
#endif
#ifndef CONFIG_TQ_LOCAL_COMMAND_TASK_PRIORITY
#define CONFIG_TQ_LOCAL_COMMAND_TASK_PRIORITY 7
#endif
#ifndef CONFIG_TQ_LOCAL_COMMAND_TASK_CORE
#define CONFIG_TQ_LOCAL_COMMAND_TASK_CORE 0
#endif
#ifndef CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS
#define CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS 1000
#endif
#ifndef CONFIG_TQ_LOCAL_COMMAND_DETECTION_TIMEOUT_MS
#define CONFIG_TQ_LOCAL_COMMAND_DETECTION_TIMEOUT_MS 6000
#endif
#ifndef CONFIG_TQ_LOCAL_COMMAND_DETECTION_THRESHOLD_X10000
#define CONFIG_TQ_LOCAL_COMMAND_DETECTION_THRESHOLD_X10000 1200
#endif
#define DOMAIN_LOCAL_COMMAND_MODEL_PARTITION "model"
#define DOMAIN_LOCAL_COMMAND_MODEL_NAME "mn7_cn"
#define DOMAIN_LOCAL_COMMAND_TEXT "喵掌柜"
#define DOMAIN_LOCAL_COMMAND_GRAPH_TEXT "miao zhang gui"
#define DOMAIN_LOCAL_COMMAND_PCM_QUEUE_LEN 8U
#define DOMAIN_LOCAL_COMMAND_PCM_MAX_SAMPLES 512U
#define DOMAIN_LOCAL_COMMAND_QUEUE_WAIT_MS 100U
#define DOMAIN_LOCAL_COMMAND_LOCK_TIMEOUT_MS 100U
#define DOMAIN_LOCAL_COMMAND_STACK_MIN_BYTES 2048U
#define DOMAIN_LOCAL_COMMAND_STATS_INTERVAL_MS 2000U
#define DOMAIN_LOCAL_COMMAND_STOP_BIT BIT0
#define DOMAIN_LOCAL_COMMAND_STOPPED_BIT BIT1
#define DOMAIN_LOCAL_COMMAND_RUNNING_BIT BIT2
#define DOMAIN_LOCAL_COMMAND_SUSPENDED_BIT BIT3
#if CONFIG_TQ_LOCAL_COMMAND_TASK_CORE < 0
#define DOMAIN_LOCAL_COMMAND_TASK_CORE tskNO_AFFINITY
#else
#define DOMAIN_LOCAL_COMMAND_TASK_CORE CONFIG_TQ_LOCAL_COMMAND_TASK_CORE
#endif
typedef struct {
uint16_t sample_count;
int16_t samples[DOMAIN_LOCAL_COMMAND_PCM_MAX_SAMPLES];
} domain_local_command_pcm_frame_t;
typedef struct {
uint32_t received_frames;
uint32_t processed_chunks;
uint32_t dropped_frames;
uint32_t timeout_count;
uint32_t queue_high_water;
uint32_t max_inference_us;
} domain_local_command_pipeline_stats_t;
typedef struct {
SemaphoreHandle_t lock;
EventGroupHandle_t events;
QueueHandle_t pcm_queue;
TaskHandle_t task;
srmodel_list_t *models;
const esp_mn_iface_t *multinet_iface;
model_iface_data_t *multinet_data;
bool commands_allocated;
int multinet_chunk_samples;
int16_t *inference_pcm;
domain_local_command_event_callback_t callback;
void *callback_user_data;
bool started;
bool stop_requested;
bool suspended;
bool detection_running;
bool reset_pending;
uint32_t control_generation;
uint32_t applied_generation;
uint32_t detection_generation;
uint32_t detection_count;
uint32_t task_stack_high_water_bytes;
} domain_local_command_state_t;
static const char *TAG = "domain_local_cmd";
static domain_local_command_state_t s_local_command;
static StaticTask_t s_local_command_task_buffer;
static StackType_t *s_local_command_task_stack;
static portMUX_TYPE s_pcm_ingress_lock = portMUX_INITIALIZER_UNLOCKED;
static bool s_pcm_ingress_enabled;
static domain_local_command_pipeline_stats_t s_pipeline_stats;
static bool local_command_lock(void)
{
if (s_local_command.lock == NULL) {
return false;
}
TickType_t ticks = pdMS_TO_TICKS(DOMAIN_LOCAL_COMMAND_LOCK_TIMEOUT_MS);
return xSemaphoreTake(s_local_command.lock, ticks > 0U ? ticks : 1U) == pdTRUE;
}
static void local_command_unlock(void)
{
xSemaphoreGive(s_local_command.lock);
}
static uint32_t local_command_default_timeout(int timeout_ms)
{
return timeout_ms == 0 ? CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS : (uint32_t)timeout_ms;
}
static uint32_t local_command_remaining_ms(TickType_t started, uint32_t timeout_ms)
{
const uint32_t elapsed_ms = (uint32_t)((xTaskGetTickCount() - started) * portTICK_PERIOD_MS);
return elapsed_ms >= timeout_ms ? 0U : timeout_ms - elapsed_ms;
}
static void local_command_set_pcm_admission(bool enabled)
{
portENTER_CRITICAL(&s_pcm_ingress_lock);
s_pcm_ingress_enabled = enabled;
portEXIT_CRITICAL(&s_pcm_ingress_lock);
}
static uint32_t local_command_dropped_pcm_frames(void)
{
uint32_t dropped = 0U;
portENTER_CRITICAL(&s_pcm_ingress_lock);
dropped = s_pipeline_stats.dropped_frames;
portEXIT_CRITICAL(&s_pcm_ingress_lock);
return dropped;
}
static void local_command_note_dropped_pcm(void)
{
portENTER_CRITICAL(&s_pcm_ingress_lock);
++s_pipeline_stats.dropped_frames;
portEXIT_CRITICAL(&s_pcm_ingress_lock);
}
static void local_command_note_queued_pcm(uint32_t queue_depth)
{
portENTER_CRITICAL(&s_pcm_ingress_lock);
++s_pipeline_stats.received_frames;
if (queue_depth > s_pipeline_stats.queue_high_water) {
s_pipeline_stats.queue_high_water = queue_depth;
}
portEXIT_CRITICAL(&s_pcm_ingress_lock);
}
static void local_command_note_processed_chunk(uint32_t inference_us, bool timed_out)
{
portENTER_CRITICAL(&s_pcm_ingress_lock);
++s_pipeline_stats.processed_chunks;
if (timed_out) {
++s_pipeline_stats.timeout_count;
}
if (inference_us > s_pipeline_stats.max_inference_us) {
s_pipeline_stats.max_inference_us = inference_us;
}
portEXIT_CRITICAL(&s_pcm_ingress_lock);
}
static domain_local_command_pipeline_stats_t local_command_pipeline_stats(void)
{
domain_local_command_pipeline_stats_t stats;
portENTER_CRITICAL(&s_pcm_ingress_lock);
stats = s_pipeline_stats;
portEXIT_CRITICAL(&s_pcm_ingress_lock);
return stats;
}
static void local_command_reset_pipeline_stats(void)
{
portENTER_CRITICAL(&s_pcm_ingress_lock);
memset(&s_pipeline_stats, 0, sizeof(s_pipeline_stats));
portEXIT_CRITICAL(&s_pcm_ingress_lock);
}
static void local_command_pcm_observer(const int16_t *pcm,
size_t samples,
uint32_t sample_rate_hz,
void *user_data)
{
(void)user_data;
bool admitted = false;
portENTER_CRITICAL(&s_pcm_ingress_lock);
admitted = s_pcm_ingress_enabled;
portEXIT_CRITICAL(&s_pcm_ingress_lock);
if (!admitted || pcm == NULL || samples == 0U) {
return;
}
if (sample_rate_hz != 16000U || samples > DOMAIN_LOCAL_COMMAND_PCM_MAX_SAMPLES ||
s_local_command.pcm_queue == NULL) {
local_command_note_dropped_pcm();
return;
}
domain_local_command_pcm_frame_t frame = {
.sample_count = (uint16_t)samples,
};
memcpy(frame.samples, pcm, samples * sizeof(int16_t));
if (xQueueSend(s_local_command.pcm_queue, &frame, 0U) != pdTRUE) {
local_command_note_dropped_pcm();
} else {
local_command_note_queued_pcm(
(uint32_t)uxQueueMessagesWaiting(s_local_command.pcm_queue));
}
}
static void local_command_drain_pcm_queue(void)
{
domain_local_command_pcm_frame_t frame;
if (s_local_command.pcm_queue == NULL) {
return;
}
while (xQueueReceive(s_local_command.pcm_queue, &frame, 0U) == pdTRUE) {
}
}
static esp_err_t local_command_configure_model(void)
{
s_local_command.models = esp_srmodel_init(DOMAIN_LOCAL_COMMAND_MODEL_PARTITION);
if (s_local_command.models == NULL || s_local_command.models->num <= 0) {
ESP_LOGE(TAG,
"ESP-SR model partition unavailable: partition=%s action=full_flash_required",
DOMAIN_LOCAL_COMMAND_MODEL_PARTITION);
return ESP_ERR_NOT_FOUND;
}
if (esp_srmodel_exists(s_local_command.models,
(char *)DOMAIN_LOCAL_COMMAND_MODEL_NAME) < 0) {
ESP_LOGE(TAG, "Chinese MultiNet model unavailable: model=%s",
DOMAIN_LOCAL_COMMAND_MODEL_NAME);
return ESP_ERR_NOT_FOUND;
}
s_local_command.multinet_iface = esp_mn_handle_from_name(
(char *)DOMAIN_LOCAL_COMMAND_MODEL_NAME);
const esp_mn_iface_t *iface = s_local_command.multinet_iface;
if (iface == NULL || iface->create == NULL || iface->destroy == NULL ||
iface->get_samp_rate == NULL || iface->get_samp_chunksize == NULL ||
iface->detect == NULL || iface->get_results == NULL || iface->clean == NULL ||
iface->check_speech_command == NULL || iface->set_det_threshold == NULL) {
return ESP_ERR_NOT_SUPPORTED;
}
s_local_command.multinet_data = iface->create(
DOMAIN_LOCAL_COMMAND_MODEL_NAME,
CONFIG_TQ_LOCAL_COMMAND_DETECTION_TIMEOUT_MS);
if (s_local_command.multinet_data == NULL) {
return ESP_ERR_NO_MEM;
}
if (iface->get_samp_rate(s_local_command.multinet_data) != 16000) {
return ESP_ERR_NOT_SUPPORTED;
}
s_local_command.multinet_chunk_samples =
iface->get_samp_chunksize(s_local_command.multinet_data);
if (s_local_command.multinet_chunk_samples <= 0 ||
s_local_command.multinet_chunk_samples > INT16_MAX) {
return ESP_ERR_INVALID_SIZE;
}
if (iface->check_speech_command(s_local_command.multinet_data,
DOMAIN_LOCAL_COMMAND_GRAPH_TEXT) == 0) {
ESP_LOGE(TAG, "MultiNet command pronunciation is not tokenizable: %s",
DOMAIN_LOCAL_COMMAND_GRAPH_TEXT);
return ESP_ERR_INVALID_ARG;
}
esp_err_t err = esp_mn_commands_alloc(iface, s_local_command.multinet_data);
if (err != ESP_OK) {
return err;
}
s_local_command.commands_allocated = true;
err = esp_mn_commands_clear();
if (err == ESP_OK) {
err = esp_mn_commands_phoneme_add(
DOMAIN_LOCAL_COMMAND_MIAO_ZHANG_GUI,
DOMAIN_LOCAL_COMMAND_GRAPH_TEXT,
DOMAIN_LOCAL_COMMAND_GRAPH_TEXT);
}
if (err != ESP_OK) {
return err;
}
esp_mn_error_t *command_errors = esp_mn_commands_update();
if (command_errors != NULL) {
ESP_LOGE(TAG, "MultiNet rejected local command graph: invalid_count=%d",
command_errors->num);
return ESP_ERR_INVALID_ARG;
}
const float detection_threshold =
(float)CONFIG_TQ_LOCAL_COMMAND_DETECTION_THRESHOLD_X10000 / 10000.0f;
(void)iface->set_det_threshold(s_local_command.multinet_data,
detection_threshold);
const esp_mn_phrase_t *command = esp_mn_commands_get_from_index(0);
if (command == NULL || command->command_id != DOMAIN_LOCAL_COMMAND_MIAO_ZHANG_GUI ||
command->string == NULL || command->phonemes == NULL ||
strcmp(command->string, DOMAIN_LOCAL_COMMAND_GRAPH_TEXT) != 0 ||
strcmp(command->phonemes, DOMAIN_LOCAL_COMMAND_GRAPH_TEXT) != 0 ||
esp_mn_commands_get_from_index(1) != NULL) {
return ESP_ERR_INVALID_RESPONSE;
}
s_local_command.inference_pcm = calloc(
(size_t)s_local_command.multinet_chunk_samples,
sizeof(int16_t));
if (s_local_command.inference_pcm == NULL) {
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG,
"Local command model ready: model=%s phrase=%s command=%s samples=%d "
"rate=16000 threshold_x10000=%u",
DOMAIN_LOCAL_COMMAND_MODEL_NAME,
DOMAIN_LOCAL_COMMAND_TEXT,
DOMAIN_LOCAL_COMMAND_GRAPH_TEXT,
s_local_command.multinet_chunk_samples,
(unsigned)CONFIG_TQ_LOCAL_COMMAND_DETECTION_THRESHOLD_X10000);
return ESP_OK;
}
static void local_command_release_model(void)
{
if (s_local_command.commands_allocated) {
(void)esp_mn_commands_free();
s_local_command.commands_allocated = false;
}
if (s_local_command.multinet_iface != NULL &&
s_local_command.multinet_data != NULL) {
s_local_command.multinet_iface->destroy(s_local_command.multinet_data);
}
s_local_command.multinet_data = NULL;
s_local_command.multinet_iface = NULL;
if (s_local_command.models != NULL) {
esp_srmodel_deinit(s_local_command.models);
}
s_local_command.models = NULL;
free(s_local_command.inference_pcm);
s_local_command.inference_pcm = NULL;
s_local_command.multinet_chunk_samples = 0;
}
static bool local_command_apply_control(size_t *fill_samples)
{
bool stop_requested = true;
bool suspended = true;
bool changed = false;
bool reset_pending = false;
bool applied = false;
uint32_t generation = 0U;
if (local_command_lock()) {
stop_requested = s_local_command.stop_requested;
suspended = s_local_command.suspended;
generation = s_local_command.control_generation;
changed = s_local_command.applied_generation != generation;
reset_pending = s_local_command.reset_pending;
local_command_unlock();
}
if (stop_requested) {
return false;
}
if (!changed) {
return !suspended;
}
if (reset_pending) {
s_local_command.multinet_iface->clean(s_local_command.multinet_data);
}
local_command_drain_pcm_queue();
*fill_samples = 0U;
if (!local_command_lock()) {
return false;
}
if (s_local_command.stop_requested) {
local_command_unlock();
return false;
}
if (generation == s_local_command.control_generation) {
applied = true;
s_local_command.applied_generation = generation;
s_local_command.detection_running = !s_local_command.suspended;
s_local_command.reset_pending = false;
suspended = s_local_command.suspended;
xEventGroupClearBits(s_local_command.events,
DOMAIN_LOCAL_COMMAND_RUNNING_BIT |
DOMAIN_LOCAL_COMMAND_SUSPENDED_BIT);
xEventGroupSetBits(s_local_command.events,
suspended ? DOMAIN_LOCAL_COMMAND_SUSPENDED_BIT :
DOMAIN_LOCAL_COMMAND_RUNNING_BIT);
}
local_command_unlock();
if (applied) {
ESP_LOGI(TAG,
"Local command detection %s: control_generation=%u queue_depth=%u dropped_total=%u",
suspended ? "suspended" : "running",
(unsigned)generation,
s_local_command.pcm_queue != NULL ?
(unsigned)uxQueueMessagesWaiting(s_local_command.pcm_queue) : 0U,
(unsigned)local_command_dropped_pcm_frames());
}
return !suspended;
}
static void local_command_publish_detection(float probability)
{
domain_local_command_event_callback_t callback = NULL;
void *callback_user_data = NULL;
domain_local_command_event_t event = {
.type = DOMAIN_LOCAL_COMMAND_EVENT_DETECTED,
.command_id = DOMAIN_LOCAL_COMMAND_MIAO_ZHANG_GUI,
};
if (probability > 0.0f) {
event.confidence_x10000 = probability >= 0.9999f ?
9999U : (uint32_t)(probability * 10000.0f + 0.5f);
}
if (!local_command_lock()) {
return;
}
if (!s_local_command.started || s_local_command.stop_requested ||
s_local_command.suspended) {
local_command_unlock();
return;
}
s_local_command.suspended = true;
s_local_command.detection_running = false;
s_local_command.reset_pending = false;
local_command_set_pcm_admission(false);
++s_local_command.control_generation;
s_local_command.applied_generation = s_local_command.control_generation;
event.detection_generation = ++s_local_command.detection_generation;
++s_local_command.detection_count;
callback = s_local_command.callback;
callback_user_data = s_local_command.callback_user_data;
xEventGroupClearBits(s_local_command.events, DOMAIN_LOCAL_COMMAND_RUNNING_BIT);
xEventGroupSetBits(s_local_command.events, DOMAIN_LOCAL_COMMAND_SUSPENDED_BIT);
local_command_unlock();
const domain_local_command_pipeline_stats_t stats = local_command_pipeline_stats();
ESP_LOGI(TAG,
"Local command detected: phrase=%s confidence_x10000=%u generation=%u "
"received=%u processed=%u drops=%u queue_high=%u inference_max_us=%u",
DOMAIN_LOCAL_COMMAND_TEXT,
(unsigned)event.confidence_x10000,
(unsigned)event.detection_generation,
(unsigned)stats.received_frames,
(unsigned)stats.processed_chunks,
(unsigned)stats.dropped_frames,
(unsigned)stats.queue_high_water,
(unsigned)stats.max_inference_us);
if (callback != NULL) {
callback(&event, callback_user_data);
}
}
static void local_command_task(void *arg)
{
(void)arg;
size_t fill_samples = 0U;
uint32_t frame_count = 0U;
bool stack_reported = false;
bool stack_warned = false;
TickType_t stats_last_log_tick = xTaskGetTickCount();
uint32_t previous_received_frames = 0U;
uint32_t previous_processed_chunks = 0U;
uint32_t previous_dropped_frames = 0U;
uint32_t previous_timeout_count = 0U;
for (;;) {
const bool detection_running = local_command_apply_control(&fill_samples);
bool stop_requested = true;
if (local_command_lock()) {
stop_requested = s_local_command.stop_requested;
local_command_unlock();
}
if (stop_requested) {
break;
}
if (!detection_running) {
(void)ulTaskNotifyTake(pdTRUE,
pdMS_TO_TICKS(DOMAIN_LOCAL_COMMAND_QUEUE_WAIT_MS));
continue;
}
domain_local_command_pcm_frame_t frame;
if (xQueueReceive(s_local_command.pcm_queue,
&frame,
pdMS_TO_TICKS(DOMAIN_LOCAL_COMMAND_QUEUE_WAIT_MS)) != pdTRUE) {
continue;
}
if (!local_command_apply_control(&fill_samples)) {
continue;
}
size_t offset = 0U;
while (offset < frame.sample_count) {
const size_t needed = (size_t)s_local_command.multinet_chunk_samples - fill_samples;
const size_t available = (size_t)frame.sample_count - offset;
const size_t copy_samples = available < needed ? available : needed;
memcpy(s_local_command.inference_pcm + fill_samples,
frame.samples + offset,
copy_samples * sizeof(int16_t));
fill_samples += copy_samples;
offset += copy_samples;
if (fill_samples != (size_t)s_local_command.multinet_chunk_samples) {
continue;
}
const TickType_t inference_started_tick = xTaskGetTickCount();
const esp_mn_state_t state = s_local_command.multinet_iface->detect(
s_local_command.multinet_data,
s_local_command.inference_pcm);
const uint64_t inference_elapsed_us =
(uint64_t)(xTaskGetTickCount() - inference_started_tick) *
(uint64_t)portTICK_PERIOD_MS * 1000U;
const uint32_t inference_us = inference_elapsed_us > UINT32_MAX ?
UINT32_MAX : (uint32_t)inference_elapsed_us;
fill_samples = 0U;
++frame_count;
local_command_note_processed_chunk(
inference_us, state == ESP_MN_STATE_TIMEOUT);
/* MultiNet can consume a full core while the PCM queue is nonempty.
* Give the pinned core's idle task a bounded scheduling point so
* continuous playback-time inference cannot trip the task WDT. */
vTaskDelay(1);
if (state == ESP_MN_STATE_TIMEOUT) {
s_local_command.multinet_iface->clean(s_local_command.multinet_data);
} else if (state == ESP_MN_STATE_DETECTED) {
esp_mn_results_t *results = s_local_command.multinet_iface->get_results(
s_local_command.multinet_data);
const bool accepted = results != NULL && results->num > 0 &&
results->command_id[0] == DOMAIN_LOCAL_COMMAND_MIAO_ZHANG_GUI;
const float probability = accepted ? results->prob[0] : 0.0f;
s_local_command.multinet_iface->clean(s_local_command.multinet_data);
if (accepted) {
local_command_publish_detection(probability);
fill_samples = 0U;
break;
}
ESP_LOGW(TAG, "Ignore unexpected MultiNet result");
} else if (state != ESP_MN_STATE_DETECTING) {
ESP_LOGE(TAG, "Invalid MultiNet state: %d", (int)state);
if (local_command_lock()) {
s_local_command.stop_requested = true;
local_command_unlock();
}
break;
}
const TickType_t now = xTaskGetTickCount();
if ((now - stats_last_log_tick) >=
pdMS_TO_TICKS(DOMAIN_LOCAL_COMMAND_STATS_INTERVAL_MS)) {
const domain_local_command_pipeline_stats_t stats =
local_command_pipeline_stats();
const uint32_t queue_depth = s_local_command.pcm_queue != NULL ?
(uint32_t)uxQueueMessagesWaiting(s_local_command.pcm_queue) : 0U;
ESP_LOGI(TAG,
"Local command pipeline: received=%u processed=%u drops=%u timeouts=%u "
"queue=%u high=%u inference_max_us=%u",
(unsigned)(stats.received_frames - previous_received_frames),
(unsigned)(stats.processed_chunks - previous_processed_chunks),
(unsigned)(stats.dropped_frames - previous_dropped_frames),
(unsigned)(stats.timeout_count - previous_timeout_count),
(unsigned)queue_depth,
(unsigned)stats.queue_high_water,
(unsigned)stats.max_inference_us);
previous_received_frames = stats.received_frames;
previous_processed_chunks = stats.processed_chunks;
previous_dropped_frames = stats.dropped_frames;
previous_timeout_count = stats.timeout_count;
stats_last_log_tick = now;
}
if ((frame_count % 100U) == 0U) {
const uint32_t high_water = (uint32_t)uxTaskGetStackHighWaterMark(NULL);
if (local_command_lock()) {
s_local_command.task_stack_high_water_bytes = high_water;
local_command_unlock();
}
const uint32_t required =
CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES / 4U >
DOMAIN_LOCAL_COMMAND_STACK_MIN_BYTES ?
CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES / 4U :
DOMAIN_LOCAL_COMMAND_STACK_MIN_BYTES;
if (!stack_reported) {
stack_reported = true;
ESP_LOGI(TAG,
"Task stack headroom=%u required=%u configured=%u bytes",
(unsigned)high_water,
(unsigned)required,
(unsigned)CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES);
}
if (!stack_warned && high_water < required) {
stack_warned = true;
ESP_LOGW(TAG,
"Local command task stack margin low: high_water=%u required=%u configured=%u",
(unsigned)high_water,
(unsigned)required,
(unsigned)CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES);
}
}
}
}
if (local_command_lock()) {
s_local_command.task_stack_high_water_bytes =
(uint32_t)uxTaskGetStackHighWaterMark(NULL);
s_local_command.detection_running = false;
s_local_command.reset_pending = false;
local_command_unlock();
}
xEventGroupClearBits(s_local_command.events, DOMAIN_LOCAL_COMMAND_RUNNING_BIT);
xEventGroupSetBits(s_local_command.events,
DOMAIN_LOCAL_COMMAND_SUSPENDED_BIT |
DOMAIN_LOCAL_COMMAND_STOPPED_BIT);
vTaskSuspend(NULL);
abort();
}
static esp_err_t local_command_reserve_task_stack(void)
{
if (s_local_command_task_stack != NULL) {
return ESP_OK;
}
#if (configSUPPORT_STATIC_ALLOCATION == 1) && \
((defined(CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) && CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) || \
(defined(CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY) && CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY))
s_local_command_task_stack = (StackType_t *)heap_caps_malloc(
CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (s_local_command_task_stack != NULL) {
ESP_LOGI(TAG,
"Task stack reserved in PSRAM (%uB)",
(unsigned)CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES);
return ESP_OK;
}
#endif
ESP_LOGE(TAG,
"PSRAM task stack reservation failed bytes=%u internal_free=%u "
"internal_largest=%u psram_free=%u psram_largest=%u",
(unsigned)CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES,
(unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT),
(unsigned)heap_caps_get_largest_free_block(
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT),
(unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT),
(unsigned)heap_caps_get_largest_free_block(
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
return ESP_ERR_NO_MEM;
}
static void local_command_cleanup_after_stop(void)
{
local_command_set_pcm_admission(false);
local_command_release_model();
if (local_command_lock()) {
s_local_command.callback = NULL;
s_local_command.callback_user_data = NULL;
s_local_command.started = false;
s_local_command.stop_requested = false;
s_local_command.suspended = true;
s_local_command.detection_running = false;
s_local_command.reset_pending = false;
s_local_command.control_generation = 0U;
s_local_command.applied_generation = 0U;
local_command_unlock();
}
}
esp_err_t domain_runtime_local_command_start(
domain_local_command_event_callback_t callback,
void *user_data)
{
if (callback == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (s_local_command.lock == NULL) {
s_local_command.lock = xSemaphoreCreateMutex();
}
if (s_local_command.events == NULL) {
s_local_command.events = xEventGroupCreate();
}
if (s_local_command.lock == NULL || s_local_command.events == NULL) {
return ESP_ERR_NO_MEM;
}
if (!local_command_lock()) {
return ESP_ERR_TIMEOUT;
}
if (s_local_command.started) {
const bool same = s_local_command.callback == callback &&
s_local_command.callback_user_data == user_data;
local_command_unlock();
return same ? ESP_OK : ESP_ERR_INVALID_STATE;
}
s_local_command.callback = callback;
s_local_command.callback_user_data = user_data;
s_local_command.stop_requested = false;
s_local_command.suspended = false;
s_local_command.detection_running = false;
s_local_command.reset_pending = false;
s_local_command.control_generation = 1U;
s_local_command.applied_generation = 0U;
local_command_unlock();
local_command_reset_pipeline_stats();
esp_err_t err = local_command_reserve_task_stack();
if (err != ESP_OK) {
local_command_cleanup_after_stop();
return err;
}
if (s_local_command.pcm_queue == NULL) {
s_local_command.pcm_queue = xQueueCreateWithCaps(
DOMAIN_LOCAL_COMMAND_PCM_QUEUE_LEN,
sizeof(domain_local_command_pcm_frame_t),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
}
if (s_local_command.pcm_queue == NULL) {
local_command_cleanup_after_stop();
return ESP_ERR_NO_MEM;
}
(void)xQueueReset(s_local_command.pcm_queue);
err = local_command_configure_model();
if (err != ESP_OK) {
local_command_cleanup_after_stop();
return err;
}
err = platform_multimodal_audio_register_pcm_observer(
local_command_pcm_observer,
NULL);
if (err != ESP_OK) {
local_command_cleanup_after_stop();
return err;
}
local_command_set_pcm_admission(true);
xEventGroupClearBits(s_local_command.events,
DOMAIN_LOCAL_COMMAND_STOP_BIT |
DOMAIN_LOCAL_COMMAND_STOPPED_BIT |
DOMAIN_LOCAL_COMMAND_RUNNING_BIT |
DOMAIN_LOCAL_COMMAND_SUSPENDED_BIT);
if (!local_command_lock()) {
(void)platform_multimodal_audio_unregister_pcm_observer(
local_command_pcm_observer, NULL,
CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS);
local_command_cleanup_after_stop();
return ESP_ERR_TIMEOUT;
}
s_local_command.started = true;
local_command_unlock();
/* PSRAM-backed static stack. This inference-only task never initiates flash
* writes; OTA stops local wake before image installation. Its worst case is
* a 1026-byte PCM frame, MultiNet inference, and one bounded typed callback. */
TaskHandle_t created = xTaskCreateStaticPinnedToCore(
local_command_task,
"domain_cmd",
CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES,
NULL,
CONFIG_TQ_LOCAL_COMMAND_TASK_PRIORITY,
s_local_command_task_stack,
&s_local_command_task_buffer,
DOMAIN_LOCAL_COMMAND_TASK_CORE);
if (created == NULL) {
(void)platform_multimodal_audio_unregister_pcm_observer(
local_command_pcm_observer, NULL,
CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS);
local_command_cleanup_after_stop();
return ESP_ERR_NO_MEM;
}
if (local_command_lock()) {
s_local_command.task = created;
local_command_unlock();
} else {
vTaskDelete(created);
(void)platform_multimodal_audio_unregister_pcm_observer(
local_command_pcm_observer, NULL,
CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS);
local_command_cleanup_after_stop();
return ESP_ERR_TIMEOUT;
}
EventBits_t bits = xEventGroupWaitBits(
s_local_command.events,
DOMAIN_LOCAL_COMMAND_RUNNING_BIT | DOMAIN_LOCAL_COMMAND_STOPPED_BIT,
pdFALSE,
pdFALSE,
pdMS_TO_TICKS(CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS));
if ((bits & DOMAIN_LOCAL_COMMAND_RUNNING_BIT) == 0U) {
(void)domain_runtime_local_command_stop(CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS);
return ESP_ERR_TIMEOUT;
}
return ESP_OK;
}
static esp_err_t local_command_request_detection_state(bool suspended, int timeout_ms)
{
if (timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
const uint32_t bounded_ms = local_command_default_timeout(timeout_ms);
if (suspended) {
local_command_set_pcm_admission(false);
}
if (!local_command_lock()) {
return ESP_ERR_TIMEOUT;
}
if (!s_local_command.started || s_local_command.stop_requested ||
s_local_command.task == NULL) {
local_command_unlock();
return ESP_ERR_INVALID_STATE;
}
if (s_local_command.suspended == suspended &&
s_local_command.applied_generation == s_local_command.control_generation) {
local_command_unlock();
return ESP_OK;
}
const bool resume_from_clean_stop = !suspended &&
s_local_command.suspended &&
!s_local_command.reset_pending &&
!s_local_command.detection_running &&
s_local_command.applied_generation == s_local_command.control_generation;
s_local_command.suspended = suspended;
if (suspended || !resume_from_clean_stop) {
s_local_command.reset_pending = true;
}
const uint32_t generation = ++s_local_command.control_generation;
const EventBits_t expected = suspended ?
DOMAIN_LOCAL_COMMAND_SUSPENDED_BIT : DOMAIN_LOCAL_COMMAND_RUNNING_BIT;
xEventGroupClearBits(s_local_command.events, expected);
TaskHandle_t task = s_local_command.task;
local_command_unlock();
xTaskNotifyGive(task);
EventBits_t bits = xEventGroupWaitBits(
s_local_command.events,
expected | DOMAIN_LOCAL_COMMAND_STOPPED_BIT,
pdFALSE,
pdFALSE,
pdMS_TO_TICKS(bounded_ms));
if ((bits & expected) == 0U) {
return (bits & DOMAIN_LOCAL_COMMAND_STOPPED_BIT) != 0U ?
ESP_ERR_INVALID_STATE : ESP_ERR_TIMEOUT;
}
if (!local_command_lock()) {
return ESP_ERR_TIMEOUT;
}
const bool applied = s_local_command.started &&
!s_local_command.stop_requested &&
s_local_command.suspended == suspended &&
s_local_command.applied_generation == generation;
local_command_unlock();
if (applied && !suspended) {
local_command_set_pcm_admission(true);
}
return applied ? ESP_OK : ESP_ERR_INVALID_STATE;
}
esp_err_t domain_runtime_local_command_suspend(int timeout_ms)
{
return local_command_request_detection_state(true, timeout_ms);
}
esp_err_t domain_runtime_local_command_resume(int timeout_ms)
{
return local_command_request_detection_state(false, timeout_ms);
}
esp_err_t domain_runtime_local_command_stop(int timeout_ms)
{
if (timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
const uint32_t bounded_ms = local_command_default_timeout(timeout_ms);
const TickType_t started = xTaskGetTickCount();
if (s_local_command.lock == NULL) {
return ESP_OK;
}
local_command_set_pcm_admission(false);
esp_err_t result = ESP_OK;
esp_err_t observer_err = platform_multimodal_audio_unregister_pcm_observer(
local_command_pcm_observer,
NULL,
(int)local_command_remaining_ms(started, bounded_ms));
if (observer_err != ESP_OK && observer_err != ESP_ERR_INVALID_STATE) {
result = observer_err;
}
if (!local_command_lock()) {
return result != ESP_OK ? result : ESP_ERR_TIMEOUT;
}
if (!s_local_command.started) {
local_command_unlock();
return result;
}
s_local_command.stop_requested = true;
s_local_command.detection_running = false;
TaskHandle_t task = s_local_command.task;
local_command_unlock();
xEventGroupSetBits(s_local_command.events, DOMAIN_LOCAL_COMMAND_STOP_BIT);
if (task != NULL) {
xTaskNotifyGive(task);
}
const uint32_t remaining_ms = local_command_remaining_ms(started, bounded_ms);
EventBits_t bits = xEventGroupWaitBits(
s_local_command.events,
DOMAIN_LOCAL_COMMAND_STOPPED_BIT,
pdFALSE,
pdTRUE,
pdMS_TO_TICKS(remaining_ms));
if ((bits & DOMAIN_LOCAL_COMMAND_STOPPED_BIT) == 0U && result == ESP_OK) {
result = ESP_ERR_TIMEOUT;
}
TaskHandle_t stopped_task = task;
if (local_command_lock()) {
if (s_local_command.task == stopped_task) {
s_local_command.task = NULL;
}
local_command_unlock();
}
if (stopped_task != NULL) {
vTaskDelete(stopped_task);
}
if ((bits & DOMAIN_LOCAL_COMMAND_STOPPED_BIT) == 0U) {
ESP_LOGE(TAG, "Local command worker forced down after stop timeout");
}
local_command_cleanup_after_stop();
return result;
}
esp_err_t domain_runtime_local_command_get_snapshot(
domain_local_command_snapshot_t *out_snapshot)
{
if (out_snapshot == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (s_local_command.lock == NULL) {
memset(out_snapshot, 0, sizeof(*out_snapshot));
return ESP_OK;
}
if (!local_command_lock()) {
return ESP_ERR_TIMEOUT;
}
*out_snapshot = (domain_local_command_snapshot_t) {
.started = s_local_command.started,
.detection_running = s_local_command.detection_running,
.detection_generation = s_local_command.detection_generation,
.detection_count = s_local_command.detection_count,
.dropped_pcm_frames = local_command_dropped_pcm_frames(),
.task_stack_high_water_bytes = s_local_command.task_stack_high_water_bytes,
};
local_command_unlock();
return ESP_OK;
}
+1
View File
@@ -4,5 +4,6 @@ dependencies:
espressif/esp_websocket_client: ^1.6.1
espressif/esp_codec_dev: ^1.5.4
espressif/esp_audio_codec: ^2.4.1
espressif/esp-sr: ^2.4.7
espressif/mdns: ^1.11.0
78/esp-wifi-connect: ^3.1.4
+24
View File
@@ -49,6 +49,12 @@ typedef struct {
void *user_data;
} platform_multimodal_downstream_playback_callbacks_t;
typedef void (*platform_multimodal_audio_pcm_observer_t)(
const int16_t *pcm,
size_t samples,
uint32_t sample_rate_hz,
void *user_data);
#define PLATFORM_LAN_HTTP_PATH_MAX_LEN 96
#define PLATFORM_LAN_HTTP_HOSTNAME_MAX_LEN 32
#define PLATFORM_LAN_HTTP_REQUEST_BODY_MAX_LEN 1024
@@ -321,6 +327,24 @@ esp_err_t platform_multimodal_audio_start_uplink(
esp_err_t platform_multimodal_audio_stop_uplink(int timeout_ms);
bool platform_multimodal_audio_is_uplink_running(void);
bool platform_multimodal_audio_is_initialized(void);
/**
* @brief Register the single mono PCM observer for the active capture path.
*
* The callback runs synchronously in the platform capture task, outside the
* registration lock, and must remain bounded and non-blocking.
*/
esp_err_t platform_multimodal_audio_register_pcm_observer(
platform_multimodal_audio_pcm_observer_t observer,
void *user_data);
/**
* @brief Remove a matching PCM observer and wait for in-flight callbacks.
*
* Negative timeout is invalid. Zero performs a non-blocking drain check.
*/
esp_err_t platform_multimodal_audio_unregister_pcm_observer(
platform_multimodal_audio_pcm_observer_t observer,
void *user_data,
int timeout_ms);
esp_err_t platform_multimodal_audio_play_downstream_opus(const uint8_t *data, size_t len);
esp_err_t platform_multimodal_audio_begin_downstream_response(void);
esp_err_t platform_multimodal_audio_finish_downstream_response(void);
@@ -11,6 +11,10 @@ extern "C" {
#endif
typedef esp_err_t (*multimodal_audio_uplink_cb_t)(const uint8_t *data, size_t len, void *ctx);
typedef void (*multimodal_audio_pcm_observer_t)(const int16_t *pcm,
size_t samples,
uint32_t sample_rate_hz,
void *ctx);
typedef enum {
MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STARTED = 0,
@@ -33,6 +37,13 @@ esp_err_t multimodal_audio_start_uplink(multimodal_audio_uplink_cb_t cb, void *c
esp_err_t multimodal_audio_stop_uplink(int timeout_ms);
bool multimodal_audio_is_uplink_running(void);
bool multimodal_audio_is_initialized(void);
esp_err_t multimodal_audio_register_pcm_observer(
multimodal_audio_pcm_observer_t observer,
void *observer_ctx);
esp_err_t multimodal_audio_unregister_pcm_observer(
multimodal_audio_pcm_observer_t observer,
void *observer_ctx,
int timeout_ms);
esp_err_t multimodal_audio_handle_downstream_opus(const uint8_t *data, size_t len);
esp_err_t multimodal_audio_begin_downstream_response(void);
@@ -15,6 +15,7 @@
#include "esp_codec_dev_defaults.h"
#include "esp_audio_dec.h"
#include "esp_audio_enc.h"
#include "esp_afe_aec.h"
#include "esp_err.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
@@ -114,6 +115,14 @@
#define CONFIG_MULTIMODAL_AUDIO_MIC_CHANNEL 0
#endif
#ifndef CONFIG_MULTIMODAL_AUDIO_AEC_REFERENCE_CHANNEL
#define CONFIG_MULTIMODAL_AUDIO_AEC_REFERENCE_CHANNEL 1
#endif
#ifndef CONFIG_MULTIMODAL_AUDIO_AEC_FILTER_LENGTH
#define CONFIG_MULTIMODAL_AUDIO_AEC_FILTER_LENGTH 4
#endif
#ifndef CONFIG_MULTIMODAL_AUDIO_LOG_UPLINK_LEVELS
#define CONFIG_MULTIMODAL_AUDIO_LOG_UPLINK_LEVELS 1
#endif
@@ -208,8 +217,16 @@
#error "Multimodal voice requires 16 kHz audio"
#endif
#if CONFIG_MULTIMODAL_AUDIO_CODEC_CHANNELS != 1
#error "Multimodal voice requires mono audio"
#if CONFIG_MULTIMODAL_AUDIO_CODEC_CHANNELS != 2
#error "ES8311 local wake AEC requires two physical codec slots"
#endif
#if CONFIG_MULTIMODAL_AUDIO_CODEC_BITS_PER_SAMPLE != 16
#error "ESP AEC requires signed 16-bit PCM"
#endif
#if CONFIG_MULTIMODAL_AUDIO_MIC_CHANNEL == CONFIG_MULTIMODAL_AUDIO_AEC_REFERENCE_CHANNEL
#error "Microphone and AEC reference must use distinct codec slots"
#endif
#if CONFIG_MULTIMODAL_OPUS_FRAME_DURATION_MS != 60
@@ -235,6 +252,9 @@
#define MULTIMODAL_DECODER_BUFFER_GUARD_BYTES 512U
#define MULTIMODAL_AUDIO_DOWNSTREAM_TX_BOUNCE_BYTES 3200U
#define MULTIMODAL_AUDIO_DEFAULT_MIC_CHANNEL ((uint8_t)CONFIG_MULTIMODAL_AUDIO_MIC_CHANNEL)
#define MULTIMODAL_AUDIO_DEFAULT_AEC_REFERENCE_CHANNEL \
((uint8_t)CONFIG_MULTIMODAL_AUDIO_AEC_REFERENCE_CHANNEL)
#define MULTIMODAL_AUDIO_AEC_BUFFER_ALIGNMENT 16U
static const char *TAG = "multimodal_audio";
static portMUX_TYPE s_audio_runtime_lock = portMUX_INITIALIZER_UNLOCKED;
@@ -253,6 +273,7 @@ typedef struct {
uint8_t i2c_ctrl_addr_8bit;
esp_audio_enc_handle_t opus_enc;
esp_audio_dec_handle_t opus_dec;
afe_aec_handle_t *afe_aec_handle;
int opus_enc_in_bytes;
int opus_enc_out_bytes;
uint32_t opus_sample_rate;
@@ -260,8 +281,11 @@ typedef struct {
uint32_t opus_bitrate_bps;
uint8_t *uplink_codec_buf;
int16_t *uplink_mono_buf;
int16_t *uplink_aec_clean_buf;
uint8_t *uplink_opus_buf;
size_t uplink_codec_read_bytes;
size_t uplink_aec_frame_bytes;
size_t uplink_aec_frame_samples;
size_t uplink_mono_bytes;
size_t uplink_mono_buf_capacity_bytes;
size_t uplink_mono_fill_bytes;
@@ -290,6 +314,7 @@ typedef struct {
bool downstream_playback_keep_stopping_on_exit;
bool audio_deinit_in_progress;
uint8_t mic_capture_channel;
uint8_t aec_reference_channel;
QueueHandle_t uplink_send_queue;
bool uplink_send_queue_with_caps;
TaskHandle_t uplink_task;
@@ -306,6 +331,10 @@ typedef struct {
static multimodal_audio_ctx_t s_audio;
static multimodal_audio_downstream_playback_callback_t s_downstream_playback_cb;
static void *s_downstream_playback_cb_ctx;
static multimodal_audio_pcm_observer_t s_pcm_observer;
static void *s_pcm_observer_ctx;
static uint32_t s_pcm_observer_in_flight;
static portMUX_TYPE s_pcm_observer_lock = portMUX_INITIALIZER_UNLOCKED;
static bool s_uplink_stack_warned;
static bool s_uplink_send_stack_warned;
static bool s_uplink_send_queue_warned;
@@ -50,10 +50,17 @@ static void cleanup_opus_path(multimodal_audio_ctx_t *ctx)
ctx->opus_dec = NULL;
}
if (ctx->afe_aec_handle != NULL) {
afe_aec_destroy(ctx->afe_aec_handle);
ctx->afe_aec_handle = NULL;
}
free(ctx->uplink_codec_buf);
ctx->uplink_codec_buf = NULL;
free(ctx->uplink_mono_buf);
ctx->uplink_mono_buf = NULL;
free(ctx->uplink_aec_clean_buf);
ctx->uplink_aec_clean_buf = NULL;
free(ctx->uplink_opus_buf);
ctx->uplink_opus_buf = NULL;
free(ctx->downstream_pcm_buf);
@@ -64,6 +71,8 @@ static void cleanup_opus_path(multimodal_audio_ctx_t *ctx)
ctx->downstream_tx_bounce_buf = NULL;
ctx->uplink_codec_read_bytes = 0;
ctx->uplink_aec_frame_bytes = 0;
ctx->uplink_aec_frame_samples = 0;
ctx->uplink_mono_bytes = 0;
ctx->uplink_mono_buf_capacity_bytes = 0;
ctx->uplink_mono_fill_bytes = 0;
@@ -566,15 +575,54 @@ static esp_err_t init_opus_codec(multimodal_audio_ctx_t *ctx)
return audio_err_to_esp_err(audio_err);
}
const char *aec_input_format = NULL;
if (ctx->mic_capture_channel == 0U &&
ctx->aec_reference_channel == 1U) {
aec_input_format = "MR";
} else if (ctx->mic_capture_channel == 1U &&
ctx->aec_reference_channel == 0U) {
aec_input_format = "RM";
} else {
ESP_LOGE(TAG,
"Unsupported AEC slot map: mic=%u ref=%u",
(unsigned)ctx->mic_capture_channel,
(unsigned)ctx->aec_reference_channel);
return ESP_ERR_NOT_SUPPORTED;
}
ctx->afe_aec_handle = afe_aec_create(
aec_input_format,
CONFIG_MULTIMODAL_AUDIO_AEC_FILTER_LENGTH,
AFE_TYPE_FD,
AFE_MODE_LOW_COST);
if (ctx->afe_aec_handle == NULL) {
ESP_LOGE(TAG, "Create ESP-SR AEC failed");
return ESP_ERR_NO_MEM;
}
if (ctx->afe_aec_handle->handle != NULL) {
(void)aec_set_nlp_level(
ctx->afe_aec_handle->handle,
AEC_NLP_LEVEL_AGGR);
}
int aec_frame_samples = afe_aec_get_chunksize(ctx->afe_aec_handle);
if (aec_frame_samples <= 0) {
ESP_LOGE(TAG, "Invalid ESP-SR AEC frame size: %d", aec_frame_samples);
return ESP_ERR_INVALID_SIZE;
}
ctx->uplink_aec_frame_samples = (size_t)aec_frame_samples;
ctx->uplink_aec_frame_bytes =
ctx->uplink_aec_frame_samples * sizeof(int16_t);
ctx->uplink_mono_bytes = (size_t)ctx->opus_enc_in_bytes;
if (ctx->uplink_mono_bytes == 0 || ctx->fs.channel == 0U) {
return ESP_ERR_INVALID_SIZE;
}
if (ctx->uplink_mono_bytes > (SIZE_MAX / (size_t)ctx->fs.channel)) {
if (ctx->uplink_aec_frame_bytes >
(SIZE_MAX / (size_t)ctx->fs.channel)) {
return ESP_ERR_INVALID_SIZE;
}
size_t codec_read_bytes = ctx->uplink_mono_bytes * (size_t)ctx->fs.channel;
size_t codec_read_bytes =
ctx->uplink_aec_frame_bytes * (size_t)ctx->fs.channel;
ctx->uplink_codec_read_bytes = codec_read_bytes;
ctx->uplink_opus_buf_bytes = (size_t)ctx->opus_enc_out_bytes;
if (ctx->uplink_opus_buf_bytes < 256U) {
@@ -586,8 +634,14 @@ static esp_err_t init_opus_codec(multimodal_audio_ctx_t *ctx)
ctx->uplink_codec_buf = (uint8_t *)audio_psram_first_calloc(1, ctx->uplink_codec_read_bytes);
ctx->uplink_mono_buf = (int16_t *)audio_psram_first_calloc(1, ctx->uplink_mono_buf_capacity_bytes);
ctx->uplink_aec_clean_buf = (int16_t *)heap_caps_aligned_calloc(
MULTIMODAL_AUDIO_AEC_BUFFER_ALIGNMENT,
1,
ctx->uplink_aec_frame_bytes,
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
ctx->uplink_opus_buf = (uint8_t *)audio_psram_first_calloc(1, ctx->uplink_opus_buf_bytes);
if (!ctx->uplink_codec_buf || !ctx->uplink_mono_buf || !ctx->uplink_opus_buf) {
if (!ctx->uplink_codec_buf || !ctx->uplink_mono_buf ||
!ctx->uplink_aec_clean_buf || !ctx->uplink_opus_buf) {
return ESP_ERR_NO_MEM;
}
@@ -601,38 +655,20 @@ static esp_err_t init_opus_codec(multimodal_audio_ctx_t *ctx)
}
ESP_LOGI(TAG,
"Opus ready: %uHz frame=%ums bitrate=%ukbps enc(in=%d out=%d)",
"Opus/AEC ready: %uHz opus_frame=%ums bitrate=%ukbps enc(in=%d out=%d) aec_frame=%u input=%s",
(unsigned)ctx->opus_sample_rate,
(unsigned)ctx->opus_frame_duration_ms,
(unsigned)bitrate_kbps,
ctx->opus_enc_in_bytes,
ctx->opus_enc_out_bytes);
ctx->opus_enc_out_bytes,
(unsigned)ctx->uplink_aec_frame_samples,
aec_input_format);
ctx->opus_ready = true;
log_heap_snapshot("after_opus_init");
return ESP_OK;
}
static void extract_mic_channel_16(const int16_t *input,
uint8_t channels,
uint8_t mic_channel,
int16_t *output,
size_t frames)
{
if (!input || !output || channels == 0 || mic_channel >= channels) {
return;
}
if (channels == 1) {
memcpy(output, input, frames * sizeof(int16_t));
return;
}
for (size_t frame = 0; frame < frames; ++frame) {
output[frame] = input[frame * channels + mic_channel];
}
}
static void expand_mono_to_multichannel_16(const int16_t *input,
uint8_t channels,
int16_t *output,
@@ -207,6 +207,8 @@ static void reset_capture_channel_map(multimodal_audio_ctx_t *ctx)
uint8_t mic_channel = MULTIMODAL_AUDIO_DEFAULT_MIC_CHANNEL > 0U ? 1U : 0U;
ctx->mic_capture_channel = mic_channel;
ctx->aec_reference_channel =
MULTIMODAL_AUDIO_DEFAULT_AEC_REFERENCE_CHANNEL > 0U ? 1U : 0U;
}
static void fill_codec_sample_info(multimodal_audio_ctx_t *ctx)
@@ -220,9 +222,14 @@ static void fill_codec_sample_info(multimodal_audio_ctx_t *ctx)
ctx->fs.channel_mask = 0;
ctx->fs.mclk_multiple = 0;
reset_capture_channel_map(ctx);
if (ctx->fs.channel == 1U || ctx->mic_capture_channel >= ctx->fs.channel) {
if (ctx->mic_capture_channel >= ctx->fs.channel) {
ctx->mic_capture_channel = 0U;
}
if (ctx->aec_reference_channel >= ctx->fs.channel ||
ctx->aec_reference_channel == ctx->mic_capture_channel) {
ctx->aec_reference_channel =
ctx->mic_capture_channel == 0U ? 1U : 0U;
}
}
static bool is_supported_opus_sample_rate(uint32_t sample_rate)
@@ -157,6 +157,34 @@ static void audio_set_uplink_callback(multimodal_audio_ctx_t *ctx,
portEXIT_CRITICAL(&s_audio_runtime_lock);
}
static void notify_pcm_observer(const int16_t *pcm, size_t samples)
{
multimodal_audio_pcm_observer_t observer = NULL;
void *observer_ctx = NULL;
portENTER_CRITICAL(&s_pcm_observer_lock);
observer = s_pcm_observer;
observer_ctx = s_pcm_observer_ctx;
if (observer != NULL) {
++s_pcm_observer_in_flight;
}
portEXIT_CRITICAL(&s_pcm_observer_lock);
if (observer == NULL) {
return;
}
observer(pcm,
samples,
(uint32_t)CONFIG_MULTIMODAL_AUDIO_CODEC_SAMPLE_RATE,
observer_ctx);
portENTER_CRITICAL(&s_pcm_observer_lock);
if (s_pcm_observer_in_flight > 0U) {
--s_pcm_observer_in_flight;
}
portEXIT_CRITICAL(&s_pcm_observer_lock);
}
static void audio_snapshot_uplink_tasks(const multimodal_audio_ctx_t *ctx,
TaskHandle_t *out_capture_task,
TaskHandle_t *out_send_task)
@@ -610,6 +638,47 @@ static esp_err_t encode_and_queue_mono_pcm(multimodal_audio_ctx_t *ctx,
return ESP_OK;
}
static esp_err_t append_and_queue_mono_pcm(
multimodal_audio_ctx_t *ctx,
const int16_t *pcm,
size_t pcm_bytes)
{
if (ctx == NULL || pcm == NULL || pcm_bytes == 0U ||
ctx->uplink_mono_buf == NULL ||
ctx->uplink_mono_buf_capacity_bytes != ctx->uplink_mono_bytes) {
return ESP_ERR_INVALID_ARG;
}
const uint8_t *input = (const uint8_t *)pcm;
size_t remaining = pcm_bytes;
while (remaining > 0U) {
size_t available =
ctx->uplink_mono_buf_capacity_bytes -
ctx->uplink_mono_fill_bytes;
size_t copy_bytes = remaining < available ? remaining : available;
memcpy((uint8_t *)ctx->uplink_mono_buf +
ctx->uplink_mono_fill_bytes,
input,
copy_bytes);
ctx->uplink_mono_fill_bytes += copy_bytes;
input += copy_bytes;
remaining -= copy_bytes;
if (ctx->uplink_mono_fill_bytes ==
ctx->uplink_mono_buf_capacity_bytes) {
esp_err_t err = encode_and_queue_mono_pcm(
ctx,
ctx->uplink_mono_buf,
ctx->uplink_mono_bytes);
ctx->uplink_mono_fill_bytes = 0U;
if (err != ESP_OK) {
return err;
}
}
}
return ESP_OK;
}
static void uplink_send_task(void *arg)
{
(void)arg;
@@ -666,9 +735,13 @@ static void uplink_capture_task(void *arg)
(void)arg;
multimodal_audio_ctx_t *ctx = &s_audio;
if (!ctx->uplink_codec_buf || !ctx->uplink_mono_buf || !ctx->uplink_opus_buf ||
!ctx->opus_enc || ctx->uplink_mono_bytes == 0U || ctx->uplink_codec_read_bytes == 0U) {
ESP_LOGE(TAG, "Uplink Opus buffers not ready");
if (!ctx->uplink_codec_buf || !ctx->uplink_mono_buf ||
!ctx->uplink_aec_clean_buf || !ctx->uplink_opus_buf ||
!ctx->opus_enc || !ctx->afe_aec_handle ||
ctx->uplink_mono_bytes == 0U ||
ctx->uplink_codec_read_bytes == 0U ||
ctx->uplink_aec_frame_samples == 0U) {
ESP_LOGE(TAG, "Uplink Opus/AEC buffers not ready");
audio_set_uplink_capture_task(ctx, NULL);
delete_audio_task_with_caps(NULL);
return;
@@ -681,13 +754,14 @@ static void uplink_capture_task(void *arg)
}
ESP_LOGI(TAG,
"Mic uplink capture task started (stack=%uB opus in=%uB out=%uB read=%uB channels=%u mic_ch=%u)",
"Mic uplink capture task started (stack=%uB opus in=%uB out=%uB read=%uB channels=%u mic_ch=%u ref_ch=%u)",
(unsigned)get_uplink_task_stack_bytes(),
(unsigned)ctx->uplink_mono_bytes,
(unsigned)ctx->uplink_opus_buf_bytes,
(unsigned)ctx->uplink_codec_read_bytes,
(unsigned)ctx->fs.channel,
(unsigned)ctx->mic_capture_channel);
(unsigned)ctx->mic_capture_channel,
(unsigned)ctx->aec_reference_channel);
while (!audio_get_uplink_task_exit_requested(ctx)) {
if (!audio_get_uplink_recording_active(ctx)) {
@@ -707,19 +781,37 @@ static void uplink_capture_task(void *arg)
continue;
}
size_t uplink_frames = ctx->uplink_mono_bytes / sizeof(int16_t);
extract_mic_channel_16((const int16_t *)ctx->uplink_codec_buf,
ctx->fs.channel,
ctx->mic_capture_channel,
ctx->uplink_mono_buf,
uplink_frames);
audio_debug_dump_pcm(ctx->uplink_mono_buf, ctx->uplink_mono_bytes);
esp_err_t err = encode_and_queue_mono_pcm(ctx, ctx->uplink_mono_buf, ctx->uplink_mono_bytes);
size_t clean_bytes = afe_aec_process(
ctx->afe_aec_handle,
(const int16_t *)ctx->uplink_codec_buf,
ctx->uplink_aec_clean_buf);
if (clean_bytes != ctx->uplink_aec_frame_bytes) {
ESP_LOGW(TAG,
"AEC output size=%u expected=%u",
(unsigned)clean_bytes,
(unsigned)ctx->uplink_aec_frame_bytes);
ctx->uplink_mono_fill_bytes = 0U;
continue;
}
size_t uplink_frames = ctx->uplink_aec_frame_samples;
notify_pcm_observer(ctx->uplink_aec_clean_buf, uplink_frames);
audio_debug_dump_pcm(ctx->uplink_aec_clean_buf, clean_bytes);
multimodal_audio_uplink_cb_t uplink_cb = NULL;
audio_snapshot_uplink_callback(ctx, &uplink_cb, NULL);
esp_err_t err = ESP_OK;
if (uplink_cb != NULL) {
err = append_and_queue_mono_pcm(
ctx,
ctx->uplink_aec_clean_buf,
clean_bytes);
} else {
ctx->uplink_mono_fill_bytes = 0U;
}
maybe_log_uplink_levels(ctx,
(const int16_t *)ctx->uplink_codec_buf,
uplink_frames,
ctx->uplink_mono_buf,
ctx->uplink_mono_bytes);
ctx->uplink_aec_clean_buf,
clean_bytes);
if (err != ESP_OK && err != ESP_ERR_INVALID_SIZE && err != ESP_ERR_TIMEOUT) {
ESP_LOGW(TAG, "Encode uplink PCM failed: %s", esp_err_to_name(err));
vTaskDelay(pdMS_TO_TICKS(10));
@@ -739,7 +831,9 @@ static esp_err_t ensure_uplink_worker_tasks(multimodal_audio_ctx_t *ctx)
return ESP_ERR_INVALID_ARG;
}
if (!ctx->initialized || !ctx->codec_dev || !ctx->opus_enc ||
!ctx->uplink_codec_buf || !ctx->uplink_mono_buf || !ctx->uplink_opus_buf) {
!ctx->afe_aec_handle || !ctx->uplink_codec_buf ||
!ctx->uplink_mono_buf || !ctx->uplink_aec_clean_buf ||
!ctx->uplink_opus_buf) {
return ESP_ERR_INVALID_STATE;
}
@@ -970,18 +1064,82 @@ esp_err_t multimodal_audio_register_downstream_playback_callback(
return ESP_OK;
}
esp_err_t multimodal_audio_start_uplink(multimodal_audio_uplink_cb_t cb, void *cb_ctx)
esp_err_t multimodal_audio_register_pcm_observer(
multimodal_audio_pcm_observer_t observer,
void *observer_ctx)
{
if (!cb) {
if (observer == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t result = ESP_OK;
portENTER_CRITICAL(&s_pcm_observer_lock);
if (s_pcm_observer != NULL &&
(s_pcm_observer != observer || s_pcm_observer_ctx != observer_ctx)) {
result = ESP_ERR_INVALID_STATE;
} else {
s_pcm_observer = observer;
s_pcm_observer_ctx = observer_ctx;
}
portEXIT_CRITICAL(&s_pcm_observer_lock);
return result;
}
esp_err_t multimodal_audio_unregister_pcm_observer(
multimodal_audio_pcm_observer_t observer,
void *observer_ctx,
int timeout_ms)
{
if (observer == NULL || timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
portENTER_CRITICAL(&s_pcm_observer_lock);
if (s_pcm_observer == NULL) {
portEXIT_CRITICAL(&s_pcm_observer_lock);
return ESP_OK;
}
if (s_pcm_observer != observer || s_pcm_observer_ctx != observer_ctx) {
portEXIT_CRITICAL(&s_pcm_observer_lock);
return ESP_ERR_INVALID_STATE;
}
s_pcm_observer = NULL;
s_pcm_observer_ctx = NULL;
portEXIT_CRITICAL(&s_pcm_observer_lock);
TickType_t started = xTaskGetTickCount();
TickType_t timeout_ticks = pdMS_TO_TICKS((uint32_t)timeout_ms);
if (timeout_ms > 0 && timeout_ticks == 0U) {
timeout_ticks = 1U;
}
for (;;) {
uint32_t in_flight = 0U;
portENTER_CRITICAL(&s_pcm_observer_lock);
in_flight = s_pcm_observer_in_flight;
portEXIT_CRITICAL(&s_pcm_observer_lock);
if (in_flight == 0U) {
return ESP_OK;
}
if (timeout_ms == 0 ||
(xTaskGetTickCount() - started) >= timeout_ticks) {
return ESP_ERR_TIMEOUT;
}
vTaskDelay(1U);
}
}
esp_err_t multimodal_audio_start_uplink(multimodal_audio_uplink_cb_t cb, void *cb_ctx)
{
if (!s_audio.initialized || !s_audio.codec_dev || !s_audio.opus_enc ||
!s_audio.uplink_codec_buf || !s_audio.uplink_mono_buf || !s_audio.uplink_opus_buf) {
return ESP_ERR_INVALID_STATE;
}
bool already_active = audio_get_uplink_recording_active(&s_audio);
audio_set_uplink_callback(&s_audio, cb, cb_ctx);
if (already_active) {
return ESP_OK;
}
s_audio.uplink_mono_fill_bytes = 0U;
memset(s_audio.uplink_mono_buf, 0, s_audio.uplink_mono_buf_capacity_bytes);
reset_uplink_debug_stats();
@@ -177,6 +177,24 @@ bool platform_multimodal_audio_is_initialized(void)
return multimodal_audio_is_initialized();
}
esp_err_t platform_multimodal_audio_register_pcm_observer(
platform_multimodal_audio_pcm_observer_t observer,
void *user_data)
{
return multimodal_audio_register_pcm_observer(observer, user_data);
}
esp_err_t platform_multimodal_audio_unregister_pcm_observer(
platform_multimodal_audio_pcm_observer_t observer,
void *user_data,
int timeout_ms)
{
return multimodal_audio_unregister_pcm_observer(
observer,
user_data,
timeout_ms);
}
esp_err_t platform_multimodal_audio_play_downstream_opus(const uint8_t *data, size_t len)
{
return multimodal_audio_handle_downstream_opus(data, len);
+54 -1
View File
@@ -15,6 +15,58 @@ dependencies:
registry_url: https://components.espressif.com/
type: service
version: 3.1.4
espressif/cjson:
component_hash: e788323270d90738662d66fffa910bfe1fba019bba087f01557e70c40485b469
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com
type: service
version: 1.7.19~2
espressif/dl_fft:
component_hash: a955ca29581de2177ddf25759273bd83f082b3c805e735fe6ee105858643989b
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com
type: service
version: 0.6.0
espressif/esp-dsp:
component_hash: 12e44db246517a627bc0fe2b255b8335410c0215c98bfba2df5a8e3edea839ef
dependencies:
- name: idf
require: private
version: '>=4.2'
source:
registry_url: https://components.espressif.com
type: service
version: 1.8.0
espressif/esp-sr:
component_hash: 809d0041cdddd98a278f0d5afef7bb60a451290577b98cf718dfffc91bdcbd9b
dependencies:
- name: espressif/cjson
registry_url: https://components.espressif.com
require: private
version: ^1.7.19
- name: espressif/dl_fft
registry_url: https://components.espressif.com
require: private
version: '>=0.6.0'
- name: espressif/esp-dsp
registry_url: https://components.espressif.com
require: private
version: 1.8.0
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 2.4.7
espressif/esp_audio_codec:
component_hash: af4663830bc73eca58febcc34a976d003465e78ed57458aa00a14b231e392ca1
dependencies:
@@ -71,12 +123,13 @@ dependencies:
version: 1.20.4
direct_dependencies:
- 78/esp-wifi-connect
- espressif/esp-sr
- espressif/esp_audio_codec
- espressif/esp_codec_dev
- espressif/esp_websocket_client
- espressif/mdns
- idf
- joltwallet/littlefs
manifest_hash: b7c0e21c22598b04ad72173217dbd4f7c984674b1b7df2f688b54eb19cff8a12
manifest_hash: c05cc39447d3758499f6a9a2b947c3fe21cf4fd47e0e9905cde8d79dc68d0472
target: esp32s3
version: 2.0.0
+113 -3
View File
@@ -879,6 +879,99 @@ menu "Example Configuration"
help
Leave disabled for the default active-low Button 2 wiring.
config TQ_LOCAL_WAKE_ENABLE
bool "Enable local wake-word listener"
default y
help
Use the on-device Chinese MultiNet model to detect 喵掌柜.
The physical PTT button remains enabled as a parallel entry.
config TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE
bool "Enable local wake response prompt"
depends on TQ_LOCAL_WAKE_ENABLE
default n
help
Play a short acknowledgement after 喵掌柜 is detected and
before the normal single-turn dialog starts. Keep disabled
until the placeholder Opus asset has been supplied.
config TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE
string "Local wake response prompt file"
depends on TQ_LOCAL_WAKE_ENABLE
default "/littlefs/audio_prompts/local_wake_response.opus"
help
Reserved LittleFS path for the future wake acknowledgement.
Add local_wake_response.opus under
components/platform/assets/audio_prompts before enabling it.
config TQ_LOCAL_WAKE_RETRY_MS
int "Local wake startup retry delay (ms)"
depends on TQ_LOCAL_WAKE_ENABLE
default 5000
range 500 60000
config TQ_LOCAL_WAKE_STOP_WAIT_MS
int "Local wake lifecycle wait (ms)"
depends on TQ_LOCAL_WAKE_ENABLE
default 1000
range 100 5000
config TQ_LOCAL_WAKE_TASK_STACK_BYTES
int "Local wake control task stack (bytes)"
depends on TQ_LOCAL_WAKE_ENABLE
default 6144
range 4096 32768
config TQ_LOCAL_WAKE_TASK_PRIORITY
int "Local wake control task priority"
depends on TQ_LOCAL_WAKE_ENABLE
default 4
range 1 10
config TQ_LOCAL_WAKE_TASK_CORE
int "Local wake control task core (-1 for no affinity)"
depends on TQ_LOCAL_WAKE_ENABLE
default 0
range -1 1
config TQ_LOCAL_COMMAND_TASK_STACK_BYTES
int "MultiNet inference task stack (bytes)"
depends on TQ_LOCAL_WAKE_ENABLE
default 8192
range 6144 32768
config TQ_LOCAL_COMMAND_TASK_PRIORITY
int "MultiNet inference task priority"
depends on TQ_LOCAL_WAKE_ENABLE
default 7
range 1 10
config TQ_LOCAL_COMMAND_TASK_CORE
int "MultiNet inference task core (-1 for no affinity)"
depends on TQ_LOCAL_WAKE_ENABLE
default 0
range -1 1
config TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS
int "MultiNet control acknowledgement timeout (ms)"
depends on TQ_LOCAL_WAKE_ENABLE
default 1000
range 100 5000
config TQ_LOCAL_COMMAND_DETECTION_TIMEOUT_MS
int "MultiNet utterance detection timeout (ms)"
depends on TQ_LOCAL_WAKE_ENABLE
default 6000
range 1000 15000
config TQ_LOCAL_COMMAND_DETECTION_THRESHOLD_X10000
int "MultiNet detection threshold (x10000)"
depends on TQ_LOCAL_WAKE_ENABLE
default 1200
range 1 9999
help
Initial threshold is 0.12 and must be tuned with device audio.
config MULTIMODAL_HEARTBEAT_INTERVAL_MS
int "Heartbeat interval (ms)"
default 50000
@@ -966,10 +1059,11 @@ menu "Example Configuration"
config MULTIMODAL_AUDIO_CODEC_CHANNELS
int "Codec channels"
default 1
range 1 1
default 2
range 2 2
help
Fixed at 1 for the raw-opus 16kHz mono voice contract.
Two physical ES8311 slots are captured: microphone and DAC
playback reference. The external Opus contract remains mono.
config MULTIMODAL_AUDIO_CODEC_BITS_PER_SAMPLE
int "Codec bits per sample"
@@ -1051,6 +1145,22 @@ menu "Example Configuration"
speech. Use 0 for the default left/first channel, or 1 if
level logs show speech energy on raw_ch1 instead of raw_ch0.
config MULTIMODAL_AUDIO_AEC_REFERENCE_CHANNEL
int "AEC playback reference channel"
default 1
range 0 1
help
Interleaved ES8311 slot carrying the DAC playback reference.
It must differ from the microphone channel.
config MULTIMODAL_AUDIO_AEC_FILTER_LENGTH
int "AEC filter length"
default 4
range 1 8
help
ESP-SR synchronous AEC filter length. Four is the recommended
starting point for ESP32-S3.
config MULTIMODAL_AUDIO_LOG_UPLINK_LEVELS
bool "Log microphone uplink audio levels"
default n
+19 -1
View File
@@ -59,6 +59,22 @@ CONFIG_TQ_KEY_POST_DOUBLE_CLICK_SUPPRESS_MS=450
CONFIG_TQ_PTT_MODE_CLICK_TO_TALK=y
# CONFIG_TQ_PTT_MODE_HOLD_TO_TALK is not set
CONFIG_TQ_PTT_CLICK_CAPTURE_TIMEOUT_MS=8000
CONFIG_TQ_LOCAL_WAKE_ENABLE=y
# CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE is not set
CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE="/littlefs/audio_prompts/local_wake_response.opus"
CONFIG_TQ_LOCAL_WAKE_RETRY_MS=5000
CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS=1000
CONFIG_TQ_LOCAL_WAKE_TASK_STACK_BYTES=6144
CONFIG_TQ_LOCAL_WAKE_TASK_PRIORITY=4
CONFIG_TQ_LOCAL_WAKE_TASK_CORE=0
CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES=8192
CONFIG_TQ_LOCAL_COMMAND_TASK_PRIORITY=7
CONFIG_TQ_LOCAL_COMMAND_TASK_CORE=0
CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS=1000
CONFIG_TQ_LOCAL_COMMAND_DETECTION_TIMEOUT_MS=6000
CONFIG_TQ_LOCAL_COMMAND_DETECTION_THRESHOLD_X10000=1200
CONFIG_SR_MN_CN_MULTINET7_QUANT=y
CONFIG_SR_MN_EN_NONE=y
# Internal DMA-capable SRAM budget. TLS/auth/fortune flows, hardware AES DMA
# bounce buffers, LCD DMA, and worker task stacks all need internal-only memory.
@@ -150,7 +166,9 @@ CONFIG_MULTIMODAL_AUDIO_UPLINK_TASK_STACK=32768
CONFIG_MULTIMODAL_AUDIO_UPLINK_SEND_TASK_STACK=12288
CONFIG_MULTIMODAL_AUDIO_UPLINK_SEND_QUEUE_LEN=24
CONFIG_MULTIMODAL_AUDIO_UPLINK_SEND_TIMEOUT_MS=5
CONFIG_MULTIMODAL_AUDIO_CODEC_CHANNELS=1
CONFIG_MULTIMODAL_AUDIO_CODEC_CHANNELS=2
CONFIG_MULTIMODAL_AUDIO_AEC_REFERENCE_CHANNEL=1
CONFIG_MULTIMODAL_AUDIO_AEC_FILTER_LENGTH=4
CONFIG_MULTIMODAL_AUDIO_UPLINK_MAX_PACKET_BYTES=1024
CONFIG_MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_TASK_STACK=12288
CONFIG_MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_QUEUE_LEN=1024
+38 -2
View File
@@ -31,8 +31,44 @@ fi
if rg -n "platform_multimodal_downstream_playback_event|PLATFORM_MULTIMODAL_DOWNSTREAM_PLAYBACK" components/platform/include/platform.h >/dev/null 2>&1; then
fail "platform facade must not duplicate domain-owned multimodal semantic events"
fi
if rg -n "esp-sr|esp_sr|esp_afe|afe_config|afe_sr|model_path\\.h|srmodels|CONFIG_SR_|SR_WN|MULTIMODAL_WAKE_WORD|wake_word|Wake word|Wake-word|wn9_" components main sdkconfig.defaults dependencies.lock >/dev/null 2>&1; then
fail "ESP-SR and wake-word code/dependencies must stay removed"
if rg -n "esp_afe_sr|afe_config_init|afe_config_check|esp_afe_handle_from_config|SR_WN|MULTIMODAL_WAKE_WORD|wake_word|Wake word|Wake-word|wn9_" \
components/platform/{include,internal,src} \
components/control_plane/{include,internal,src} \
components/app_composition/{include,internal,src} >/dev/null 2>&1 ||
rg -n 'model_path\\.h|esp_srmodel_|esp_mn_' components/domain/{include,internal,src} \
--glob '!domain_local_command.c' >/dev/null 2>&1; then
fail "Local speech models must stay domain-owned and WakeNet/full AFE SR must stay absent"
fi
if ! rg -n 'espressif/esp-sr: \^2\.4\.7' components/platform/idf_component.yml >/dev/null 2>&1 ||
! rg -n 'DOMAIN_LOCAL_COMMAND_MIAO_ZHANG_GUI' \
components/domain/include/domain.h \
components/domain/src/runtime/domain_local_command.c >/dev/null 2>&1 ||
! rg -n 'miao zhang gui' \
components/domain/src/runtime/domain_local_command.c >/dev/null 2>&1; then
fail "Local command detection must keep the ESP-SR dependency and 喵掌柜 domain contract"
fi
if ! rg -n 'TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE' \
main/Kconfig.projbuild \
components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c >/dev/null 2>&1 ||
! rg -n '/littlefs/audio_prompts/local_wake_response\.opus' \
main/Kconfig.projbuild \
sdkconfig.defaults \
components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c >/dev/null 2>&1; then
fail "Local wake response prompt must keep its disabled asset placeholder contract"
fi
if ! rg -n '#include "esp_afe_aec.h"' components/platform/src/multimodal/audio_pipeline.c >/dev/null 2>&1 ||
! rg -n 'afe_aec_create\(' components/platform/src/multimodal/audio_pipeline/audio_pipeline_codec_impl.c >/dev/null 2>&1 ||
! rg -n 'afe_aec_process\(' components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c >/dev/null 2>&1; then
fail "Platform must own the synchronous ESP-SR AEC implementation"
fi
if rg -n 'esp_afe_aec\.h|afe_aec_process\(' \
components/domain/{include,internal,src} \
components/control_plane/{include,internal,src} \
components/app_composition/{include,internal,src} >/dev/null 2>&1; then
fail "AEC execution must remain a platform primitive"
fi
if ! rg -n '^CONFIG_MULTIMODAL_AUDIO_CODEC_CHANNELS=2\r?$' sdkconfig.defaults >/dev/null 2>&1; then
fail "ES8311 microphone and DAC reference capture requires two physical codec slots"
fi
if rg -n "platform_lcd_trigger_slot_animation|lcd_trigger_slot_animation" components/platform/include components/platform/internal components/platform/src/runtime >/dev/null 2>&1; then
fail "platform must expose LCD presentation primitives, not one-off expression APIs"