feat(voice): add local VAD speech endpoint detection
This commit is contained in:
@@ -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, ¬ify_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)
|
||||
|
||||
Reference in New Issue
Block a user