feat(voice): add local VAD speech endpoint detection

This commit is contained in:
chenjintao
2026-10-23 10:59:26 +08:00
parent ee32b4d63f
commit f17ce857ff
17 changed files with 1183 additions and 141 deletions
+11 -10
View File
@@ -548,15 +548,13 @@ esp_err_t platform_multimodal_audio_start_uplink(
void *user_data);
/**
* @brief 以有界方式停止多模态上行采集与流式发送任务。
* Stop multimodal uplink capture/stream worker with bounded teardown.
* @brief 暂停上行、补齐尾帧并限时排空发送;常驻 worker 留待 deinit 回收。
* Pause capture and drain the padded final frame; deinit owns workers.
*
* 负超时无效;零只请求协作停止;正超时结束时强制清理所属任务;重复调用幂等。
* Semantics:
* - `timeout_ms < 0`: invalid argument.
* - `timeout_ms == 0`: request cooperative stop and return immediately.
* - `timeout_ms > 0`: wait up to the bound, then force-delete owned workers.
* - Repeated calls are idempotent.
* 负超时无效;零只尝试立即完成,未完成返回 ESP_ERR_TIMEOUT。
* 正数预算包含等待当前采集迭代和尾包发送;成功后没有在途采集/发送。
* 超时保留队列、回调和缓冲以便重试,不后台清理;不可据此发送输入结束指令。
* 重复调用幂等;不可在 PCM observer / uplink sender 回调中调用。
*/
esp_err_t platform_multimodal_audio_stop_uplink(int timeout_ms);
bool platform_multimodal_audio_is_uplink_running(void);
@@ -571,12 +569,14 @@ bool platform_multimodal_audio_is_initialized(void);
*/
esp_err_t platform_multimodal_audio_set_speaker_output_enabled(bool enabled);
/**
* @brief 为当前采集路径注册唯一的单声道 PCM 观察者。
* Register the single mono PCM observer for the active capture path.
* @brief 为当前采集路径注册单声道 PCM 观察者(固定两个独立槽)。
* Register a mono PCM observer in one of two fixed slots.
*
* 回调在 platform 采集任务中同步执行,但不持有注册锁;回调必须有界且不可阻塞。
* The callback runs synchronously in the platform capture task, outside the
* registration lock, and must remain bounded and non-blocking.
* 相同 cb/user_data 重复注册幂等;槽位满返回 ESP_ERR_NO_MEM。
* 不得在观察者回调中注册/注销或调用音频 start/stop。
*/
esp_err_t platform_multimodal_audio_register_pcm_observer(
platform_multimodal_audio_pcm_observer_t observer,
@@ -587,6 +587,7 @@ esp_err_t platform_multimodal_audio_register_pcm_observer(
*
* 负超时无效,零只执行非阻塞的排空检查。
* Negative timeout is invalid. Zero performs a non-blocking drain check.
* 超时后槽位保持禁用且保留在途计数;必须重试成功后才能释放 user_data。
*/
esp_err_t platform_multimodal_audio_unregister_pcm_observer(
platform_multimodal_audio_pcm_observer_t observer,
@@ -388,7 +388,10 @@ typedef struct {
TaskHandle_t uplink_send_task;
volatile bool uplink_recording_active;
volatile bool uplink_task_exit_requested;
volatile bool uplink_send_in_flight;
bool uplink_send_in_flight;
bool uplink_tail_flush_requested;
bool uplink_stop_incomplete;
esp_err_t uplink_tail_flush_error;
bool uplink_first_network_pcm_pending;
bool uplink_first_network_packet_pending;
TickType_t uplink_network_start_tick;
@@ -412,9 +415,21 @@ static bool s_downstream_last_terminal_valid;
static uint32_t s_downstream_last_terminal_generation;
static multimodal_audio_downstream_playback_event_type_t
s_downstream_last_terminal_type;
static multimodal_audio_pcm_observer_t s_pcm_observer;
static void *s_pcm_observer_ctx;
static uint32_t s_pcm_observer_in_flight;
#define MULTIMODAL_AUDIO_PCM_OBSERVER_SLOTS 2U
typedef struct {
multimodal_audio_pcm_observer_t callback;
void *ctx;
uint32_t in_flight;
uint32_t registration;
bool enabled;
} audio_pcm_observer_slot_t;
static audio_pcm_observer_slot_t s_pcm_observers[MULTIMODAL_AUDIO_PCM_OBSERVER_SLOTS];
static uint32_t s_pcm_observer_registration;
static StaticSemaphore_t s_uplink_capture_mutex_storage;
static SemaphoreHandle_t s_uplink_capture_mutex;
static StaticSemaphore_t s_uplink_lifecycle_mutex_storage;
static SemaphoreHandle_t s_uplink_lifecycle_mutex;
static bool s_uplink_capture_mutex_initializing;
static portMUX_TYPE s_pcm_observer_lock = portMUX_INITIALIZER_UNLOCKED;
static bool s_uplink_stack_warned;
static bool s_uplink_send_stack_warned;
@@ -206,31 +206,73 @@ static bool audio_consume_first_network_packet(multimodal_audio_ctx_t *ctx,
static void notify_pcm_observer(const int16_t *pcm, size_t samples)
{
/* in_flight 计数让 unregister 能等待正在执行的观察者,同时回调在锁外运行。 */
multimodal_audio_pcm_observer_t observer = NULL;
void *observer_ctx = NULL;
/* 固定两个订阅,无分配;先计入在途,再在锁外回调,支持独立有界注销。 */
audio_pcm_observer_slot_t observers[MULTIMODAL_AUDIO_PCM_OBSERVER_SLOTS] = {0};
portENTER_CRITICAL(&s_pcm_observer_lock);
observer = s_pcm_observer;
observer_ctx = s_pcm_observer_ctx;
if (observer != NULL) {
++s_pcm_observer_in_flight;
for (size_t i = 0; i < MULTIMODAL_AUDIO_PCM_OBSERVER_SLOTS; ++i) {
if (s_pcm_observers[i].enabled) {
observers[i] = s_pcm_observers[i];
++s_pcm_observers[i].in_flight;
}
}
portEXIT_CRITICAL(&s_pcm_observer_lock);
if (observer == NULL) {
return;
for (size_t i = 0; i < MULTIMODAL_AUDIO_PCM_OBSERVER_SLOTS; ++i) {
if (observers[i].callback == NULL) {
continue;
}
observers[i].callback(pcm, samples,
(uint32_t)CONFIG_MULTIMODAL_AUDIO_CODEC_SAMPLE_RATE,
observers[i].ctx);
portENTER_CRITICAL(&s_pcm_observer_lock);
--s_pcm_observers[i].in_flight;
portEXIT_CRITICAL(&s_pcm_observer_lock);
}
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;
static bool ensure_uplink_mutexes(void)
{
portENTER_CRITICAL(&s_audio_runtime_lock);
SemaphoreHandle_t mutex = s_uplink_capture_mutex;
bool create = mutex == NULL && !s_uplink_capture_mutex_initializing;
if (create) {
s_uplink_capture_mutex_initializing = true;
}
portEXIT_CRITICAL(&s_pcm_observer_lock);
portEXIT_CRITICAL(&s_audio_runtime_lock);
if (create) {
mutex = xSemaphoreCreateMutexStatic(&s_uplink_capture_mutex_storage);
SemaphoreHandle_t lifecycle = xSemaphoreCreateMutexStatic(&s_uplink_lifecycle_mutex_storage);
portENTER_CRITICAL(&s_audio_runtime_lock);
s_uplink_capture_mutex = mutex;
s_uplink_lifecycle_mutex = lifecycle;
s_uplink_capture_mutex_initializing = false;
portEXIT_CRITICAL(&s_audio_runtime_lock);
}
return mutex != NULL;
}
static bool lock_uplink_capture(TickType_t ticks)
{
return ensure_uplink_mutexes() &&
xSemaphoreTake(s_uplink_capture_mutex, ticks) == pdTRUE;
}
static void audio_set_uplink_send_in_flight(multimodal_audio_ctx_t *ctx, bool active)
{
portENTER_CRITICAL(&s_audio_runtime_lock);
ctx->uplink_send_in_flight = active;
portEXIT_CRITICAL(&s_audio_runtime_lock);
}
static bool audio_uplink_send_pending(multimodal_audio_ctx_t *ctx)
{
/* 必须先读队列,再读 busy;发送者先置 busy 再出队,不能漏掉两者交界。 */
if (ctx->uplink_send_queue != NULL && uxQueueMessagesWaiting(ctx->uplink_send_queue) > 0U) {
return true;
}
portENTER_CRITICAL(&s_audio_runtime_lock);
bool in_flight = ctx->uplink_send_in_flight;
portEXIT_CRITICAL(&s_audio_runtime_lock);
return in_flight;
}
static void audio_snapshot_uplink_tasks(const multimodal_audio_ctx_t *ctx,
@@ -950,6 +992,11 @@ static esp_err_t encode_and_queue_mono_pcm(multimodal_audio_ctx_t *ctx,
}
track_uplink_packet_queued(packet.len);
TaskHandle_t send_task = NULL;
audio_snapshot_uplink_tasks(ctx, NULL, &send_task);
if (send_task != NULL) {
xTaskNotifyGive(send_task);
}
return ESP_OK;
}
@@ -1013,11 +1060,15 @@ static void uplink_send_task(void *arg)
continue;
}
audio_set_uplink_send_in_flight(ctx, true);
uplink_packet_t packet = {0};
if (xQueueReceive(ctx->uplink_send_queue, &packet, pdMS_TO_TICKS(100)) != pdTRUE) {
if (xQueueReceive(ctx->uplink_send_queue, &packet, 0U) != pdTRUE) {
audio_set_uplink_send_in_flight(ctx, false);
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(20));
continue;
}
if (packet.len == 0U) {
audio_set_uplink_send_in_flight(ctx, false);
continue;
}
@@ -1025,12 +1076,12 @@ static void uplink_send_task(void *arg)
void *cb_ctx = NULL;
audio_snapshot_uplink_callback(ctx, &cb, &cb_ctx);
if (cb == NULL) {
audio_set_uplink_send_in_flight(ctx, false);
continue;
}
ctx->uplink_send_in_flight = true;
esp_err_t send_err = cb(packet.data, packet.len, cb_ctx);
ctx->uplink_send_in_flight = false;
audio_set_uplink_send_in_flight(ctx, false);
uint32_t first_packet_elapsed_ms = 0U;
if (send_err == ESP_OK &&
audio_consume_first_network_packet(ctx, &first_packet_elapsed_ms)) {
@@ -1055,6 +1106,38 @@ static void uplink_send_task(void *arg)
delete_audio_task_with_caps(NULL);
}
static bool audio_uplink_tail_pending(multimodal_audio_ctx_t *ctx, esp_err_t *out_error)
{
portENTER_CRITICAL(&s_audio_runtime_lock);
bool pending = ctx->uplink_tail_flush_requested;
if (out_error != NULL) {
*out_error = ctx->uplink_tail_flush_error;
}
portEXIT_CRITICAL(&s_audio_runtime_lock);
return pending;
}
static void audio_flush_uplink_tail(multimodal_audio_ctx_t *ctx)
{
if (!audio_uplink_tail_pending(ctx, NULL) || !lock_uplink_capture(pdMS_TO_TICKS(20))) {
return;
}
esp_err_t err = ESP_OK;
if (ctx->uplink_mono_fill_bytes > 0U) {
memset((uint8_t *)ctx->uplink_mono_buf + ctx->uplink_mono_fill_bytes, 0,
ctx->uplink_mono_bytes - ctx->uplink_mono_fill_bytes);
err = encode_and_queue_mono_pcm(ctx, ctx->uplink_mono_buf, ctx->uplink_mono_bytes);
if (err == ESP_OK) {
ctx->uplink_mono_fill_bytes = 0U;
}
}
portENTER_CRITICAL(&s_audio_runtime_lock);
ctx->uplink_tail_flush_error = err;
ctx->uplink_tail_flush_requested = false;
portEXIT_CRITICAL(&s_audio_runtime_lock);
xSemaphoreGive(s_uplink_capture_mutex);
}
static void uplink_capture_task(void *arg)
{
/* 采集循环先完成声道映射/AEC,再通知 PCM 观察者并编码网络音频。 */
@@ -1091,6 +1174,8 @@ static void uplink_capture_task(void *arg)
while (!audio_get_uplink_task_exit_requested(ctx)) {
if (!audio_get_uplink_recording_active(ctx)) {
/* Opus 编码必须留在原 32 KB 采集栈,不能挪到控制线程的小栈。 */
audio_flush_uplink_tail(ctx);
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(100));
continue;
}
@@ -1100,9 +1185,17 @@ static void uplink_capture_task(void *arg)
continue;
}
if (!lock_uplink_capture(pdMS_TO_TICKS(20))) {
continue;
}
if (!audio_get_uplink_recording_active(ctx)) {
xSemaphoreGive(s_uplink_capture_mutex);
continue;
}
int ret = esp_codec_dev_read(ctx->codec_dev, ctx->uplink_codec_buf, (int)ctx->uplink_codec_read_bytes);
if (ret != ESP_CODEC_DEV_OK) {
ESP_LOGW(TAG, "Mic read failed: 0x%x", ret);
xSemaphoreGive(s_uplink_capture_mutex);
vTaskDelay(pdMS_TO_TICKS(20));
continue;
}
@@ -1117,6 +1210,7 @@ static void uplink_capture_task(void *arg)
(unsigned)clean_bytes,
(unsigned)ctx->uplink_aec_frame_bytes);
ctx->uplink_mono_fill_bytes = 0U;
xSemaphoreGive(s_uplink_capture_mutex);
continue;
}
size_t uplink_frames = ctx->uplink_aec_frame_samples;
@@ -1145,6 +1239,7 @@ static void uplink_capture_task(void *arg)
uplink_frames,
ctx->uplink_aec_clean_buf,
clean_bytes);
xSemaphoreGive(s_uplink_capture_mutex);
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));
@@ -1368,6 +1463,11 @@ void multimodal_audio_deinit(void)
audio_set_downstream_playback_stopping(&s_audio, true);
audio_debug_dump_deinit();
(void)multimodal_audio_stop_uplink(2000);
if (s_uplink_lifecycle_mutex == NULL ||
xSemaphoreTake(s_uplink_lifecycle_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) {
ESP_LOGE(TAG, "Uplink lifecycle busy during deinit; retain resources for retry");
return;
}
audio_set_uplink_task_exit_requested(&s_audio, true);
TaskHandle_t notify_capture_task = NULL;
audio_snapshot_uplink_tasks(&s_audio, &notify_capture_task, NULL);
@@ -1401,6 +1501,13 @@ void multimodal_audio_deinit(void)
cleanup_codec_path(&s_audio);
deinit_buses(&s_audio);
memset(&s_audio, 0, sizeof(s_audio));
/* 两个 worker 已退出;force-delete 可能遗留被持有的 capture mutex。 */
vSemaphoreDelete(s_uplink_capture_mutex);
SemaphoreHandle_t capture_mutex = xSemaphoreCreateMutexStatic(&s_uplink_capture_mutex_storage);
portENTER_CRITICAL(&s_audio_runtime_lock);
s_uplink_capture_mutex = capture_mutex;
portEXIT_CRITICAL(&s_audio_runtime_lock);
xSemaphoreGive(s_uplink_lifecycle_mutex);
}
bool multimodal_audio_is_initialized(void)
@@ -1457,15 +1564,29 @@ esp_err_t multimodal_audio_register_pcm_observer(
if (observer == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t result = ESP_OK;
esp_err_t result = ESP_ERR_NO_MEM;
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;
size_t free_slot = MULTIMODAL_AUDIO_PCM_OBSERVER_SLOTS;
for (size_t i = 0; i < MULTIMODAL_AUDIO_PCM_OBSERVER_SLOTS; ++i) {
audio_pcm_observer_slot_t *slot = &s_pcm_observers[i];
if (slot->callback == observer && slot->ctx == observer_ctx) {
result = slot->enabled ? ESP_OK : ESP_ERR_INVALID_STATE;
portEXIT_CRITICAL(&s_pcm_observer_lock);
return result;
}
if (slot->callback == NULL && free_slot == MULTIMODAL_AUDIO_PCM_OBSERVER_SLOTS) {
free_slot = i;
}
}
if (free_slot < MULTIMODAL_AUDIO_PCM_OBSERVER_SLOTS) {
if (++s_pcm_observer_registration == 0U) {
++s_pcm_observer_registration;
}
s_pcm_observers[free_slot] = (audio_pcm_observer_slot_t) {
.callback = observer, .ctx = observer_ctx, .enabled = true,
.registration = s_pcm_observer_registration,
};
result = ESP_OK;
}
portEXIT_CRITICAL(&s_pcm_observer_lock);
return result;
@@ -1479,42 +1600,44 @@ esp_err_t multimodal_audio_unregister_pcm_observer(
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;
}
uint32_t registration = 0U;
for (;;) {
uint32_t in_flight = 0U;
bool pending = false;
portENTER_CRITICAL(&s_pcm_observer_lock);
in_flight = s_pcm_observer_in_flight;
for (size_t i = 0; i < MULTIMODAL_AUDIO_PCM_OBSERVER_SLOTS; ++i) {
audio_pcm_observer_slot_t *slot = &s_pcm_observers[i];
if (slot->callback != observer || slot->ctx != observer_ctx) {
continue;
}
if (registration != 0U && slot->registration != registration) {
break; /* 另一注销者已完成;不能移除其后注册的新订阅。 */
}
registration = slot->registration;
slot->enabled = false;
pending = slot->in_flight != 0U;
if (!pending) {
memset(slot, 0, sizeof(*slot));
}
break;
}
portEXIT_CRITICAL(&s_pcm_observer_lock);
if (in_flight == 0U) {
if (!pending) {
return ESP_OK;
}
if (timeout_ms == 0 ||
(xTaskGetTickCount() - started) >= timeout_ticks) {
if (timeout_ms == 0 || (xTaskGetTickCount() - started) >= timeout_ticks) {
/* 保留禁用的槽位与在途计数,重试前不能复用或释放 user_data。 */
return ESP_ERR_TIMEOUT;
}
vTaskDelay(1U);
}
}
esp_err_t multimodal_audio_start_uplink(multimodal_audio_uplink_cb_t cb, void *cb_ctx)
static esp_err_t audio_start_uplink_locked(multimodal_audio_uplink_cb_t cb, void *cb_ctx)
{
/* 回调在任务启动前发布;任一启动失败都会撤销回调,避免半启动状态。 */
if (!s_audio.initialized || !s_audio.codec_dev || !s_audio.opus_enc ||
@@ -1559,42 +1682,101 @@ esp_err_t multimodal_audio_start_uplink(multimodal_audio_uplink_cb_t cb, void *c
return ESP_OK;
}
esp_err_t multimodal_audio_start_uplink(multimodal_audio_uplink_cb_t cb, void *cb_ctx)
{
/* 初始化两个 mutex;锁序始终 lifecycle -> capture,采集任务只拿 capture。 */
if (!ensure_uplink_mutexes()) {
return ESP_ERR_NO_MEM;
}
if (xSemaphoreTake(s_uplink_lifecycle_mutex, pdMS_TO_TICKS(100)) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
if (!lock_uplink_capture(pdMS_TO_TICKS(100))) {
xSemaphoreGive(s_uplink_lifecycle_mutex);
return ESP_ERR_TIMEOUT;
}
bool unfinished = audio_get_deinit_in_progress(&s_audio) || s_audio.uplink_stop_incomplete ||
audio_uplink_tail_pending(&s_audio, NULL) ||
(!audio_get_uplink_recording_active(&s_audio) && audio_uplink_send_pending(&s_audio));
esp_err_t err = unfinished ? ESP_ERR_INVALID_STATE : audio_start_uplink_locked(cb, cb_ctx);
xSemaphoreGive(s_uplink_capture_mutex);
xSemaphoreGive(s_uplink_lifecycle_mutex);
return err;
}
esp_err_t multimodal_audio_stop_uplink(int timeout_ms)
{
/* 先协作请求退出并等待两个任务;超时后保留明确错误而不伪装成已停止。 */
if (timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
TickType_t started = xTaskGetTickCount();
TickType_t budget = pdMS_TO_TICKS((uint32_t)timeout_ms);
if (timeout_ms > 0 && budget == 0U) {
budget = 1U;
}
/* 未初始化时先安全建立锁;此处不操作任何 PCM。 */
if (!ensure_uplink_mutexes()) {
return ESP_ERR_NO_MEM;
}
TickType_t elapsed = xTaskGetTickCount() - started;
if (xSemaphoreTake(s_uplink_lifecycle_mutex, elapsed < budget ? budget - elapsed : 0U) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
s_audio.uplink_stop_incomplete = true;
audio_set_uplink_recording_active(&s_audio, false);
portENTER_CRITICAL(&s_audio_runtime_lock);
s_audio.uplink_first_network_pcm_pending = false;
s_audio.uplink_first_network_packet_pending = false;
portEXIT_CRITICAL(&s_audio_runtime_lock);
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS((uint32_t)timeout_ms);
while (((s_audio.uplink_send_queue != NULL &&
uxQueueMessagesWaiting(s_audio.uplink_send_queue) > 0U) ||
s_audio.uplink_send_in_flight) &&
xTaskGetTickCount() < deadline) {
vTaskDelay(pdMS_TO_TICKS(20));
elapsed = xTaskGetTickCount() - started;
if (!lock_uplink_capture(elapsed < budget ? budget - elapsed : 0U)) {
xSemaphoreGive(s_uplink_lifecycle_mutex);
return ESP_ERR_TIMEOUT;
}
if ((s_audio.uplink_send_queue != NULL &&
uxQueueMessagesWaiting(s_audio.uplink_send_queue) > 0U) ||
s_audio.uplink_send_in_flight) {
ESP_LOGW(TAG,
"Mic uplink stop timed out with %u queued packet(s), in_flight=%d",
(unsigned)((s_audio.uplink_send_queue != NULL) ?
uxQueueMessagesWaiting(s_audio.uplink_send_queue) : 0U),
s_audio.uplink_send_in_flight ? 1 : 0);
reset_uplink_send_queue(&s_audio);
multimodal_audio_uplink_cb_t cb = NULL;
audio_snapshot_uplink_callback(&s_audio, &cb, NULL);
esp_err_t err = ESP_OK;
if (cb != NULL && s_audio.uplink_mono_fill_bytes > 0U &&
!audio_uplink_tail_pending(&s_audio, NULL)) {
if (timeout_ms == 0) {
err = ESP_ERR_TIMEOUT;
} else {
portENTER_CRITICAL(&s_audio_runtime_lock);
s_audio.uplink_tail_flush_error = ESP_OK;
s_audio.uplink_tail_flush_requested = true;
portEXIT_CRITICAL(&s_audio_runtime_lock);
}
}
audio_set_uplink_callback(&s_audio, NULL, NULL);
s_audio.uplink_mono_fill_bytes = 0U;
log_heap_snapshot("after_uplink_stop");
return ESP_OK;
xSemaphoreGive(s_uplink_capture_mutex);
if (audio_uplink_tail_pending(&s_audio, NULL)) {
TaskHandle_t capture_task = NULL;
audio_snapshot_uplink_tasks(&s_audio, &capture_task, NULL);
if (capture_task != NULL) {
xTaskNotifyGive(capture_task);
}
}
while (err == ESP_OK) {
bool tail_pending = audio_uplink_tail_pending(&s_audio, &err);
if (err != ESP_OK || (!tail_pending && !audio_uplink_send_pending(&s_audio))) {
break;
}
if ((xTaskGetTickCount() - started) >= budget) {
err = ESP_ERR_TIMEOUT;
break;
}
vTaskDelay(1U);
}
if (err == ESP_OK) {
s_audio.uplink_stop_incomplete = false;
audio_set_uplink_callback(&s_audio, NULL, NULL);
/* 非网络采集的残帧也已被 capture 锁确认静止。 */
s_audio.uplink_mono_fill_bytes = 0U;
portENTER_CRITICAL(&s_audio_runtime_lock);
s_audio.uplink_first_network_pcm_pending = false;
s_audio.uplink_first_network_packet_pending = false;
portEXIT_CRITICAL(&s_audio_runtime_lock);
}
xSemaphoreGive(s_uplink_lifecycle_mutex);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Mic uplink stop incomplete: %s", esp_err_to_name(err));
}
return err;
}
bool multimodal_audio_is_uplink_running(void)