feat(control_plane): play prompt before light standby

This commit is contained in:
chenjintao
2026-08-21 15:13:23 +08:00
parent bba99a09ec
commit cb203a74f4
7 changed files with 235 additions and 12 deletions
@@ -51,6 +51,9 @@ esp_err_t multimodal_audio_unregister_pcm_observer(
esp_err_t multimodal_audio_handle_downstream_opus(const uint8_t *data, size_t len);
esp_err_t multimodal_audio_begin_downstream_response(uint32_t *out_playback_generation);
esp_err_t multimodal_audio_finish_downstream_response(void);
esp_err_t multimodal_audio_wait_downstream_playback_complete(
uint32_t playback_generation,
int timeout_ms);
esp_err_t multimodal_audio_stop_downstream_playback(void);
esp_err_t multimodal_audio_register_downstream_playback_callback(
multimodal_audio_downstream_playback_callback_t cb,
@@ -25,6 +25,7 @@
#include "freertos/FreeRTOS.h"
#include "freertos/idf_additions.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "soc/soc_caps.h"
#include "sdkconfig.h"
@@ -356,6 +357,16 @@ 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 SemaphoreHandle_t s_downstream_playback_completion_sem;
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
static StaticSemaphore_t s_downstream_playback_completion_sem_storage;
#endif
static bool s_downstream_playback_wait_registered;
static uint32_t s_downstream_playback_wait_generation;
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;
@@ -1212,10 +1212,29 @@ esp_err_t multimodal_audio_init(void)
ESP_LOGW(TAG, "Codec path disabled by configuration");
return ESP_ERR_NOT_SUPPORTED;
#else
if (s_downstream_playback_completion_sem == NULL) {
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
s_downstream_playback_completion_sem = xSemaphoreCreateBinaryStatic(
&s_downstream_playback_completion_sem_storage);
#else
s_downstream_playback_completion_sem = xSemaphoreCreateBinary();
#endif
if (s_downstream_playback_completion_sem == NULL) {
return ESP_ERR_NO_MEM;
}
}
if (s_audio.initialized) {
return ESP_OK;
}
(void)xSemaphoreTake(s_downstream_playback_completion_sem, 0U);
portENTER_CRITICAL(&s_audio_runtime_lock);
s_downstream_playback_wait_registered = false;
s_downstream_playback_wait_generation = 0U;
s_downstream_last_terminal_valid = false;
s_downstream_last_terminal_generation = 0U;
portEXIT_CRITICAL(&s_audio_runtime_lock);
int retry_count = get_audio_init_retry_count();
esp_err_t last_err = ESP_FAIL;
@@ -1677,6 +1696,26 @@ static void dispatch_downstream_playback_event(
multimodal_audio_downstream_playback_event_type_t type,
uint32_t playback_generation)
{
bool terminal = type == MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_COMPLETED ||
type == MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STOPPED ||
type == MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STALLED;
bool notify_waiter = false;
if (terminal) {
portENTER_CRITICAL(&s_audio_runtime_lock);
s_downstream_last_terminal_valid = true;
s_downstream_last_terminal_generation = playback_generation;
s_downstream_last_terminal_type = type;
if (s_downstream_playback_wait_registered &&
s_downstream_playback_wait_generation == playback_generation) {
s_downstream_playback_wait_registered = false;
notify_waiter = true;
}
portEXIT_CRITICAL(&s_audio_runtime_lock);
if (notify_waiter && s_downstream_playback_completion_sem != NULL) {
(void)xSemaphoreGive(s_downstream_playback_completion_sem);
}
}
multimodal_audio_downstream_playback_callback_t cb = NULL;
void *cb_ctx = NULL;
snapshot_downstream_playback_callback(&cb, &cb_ctx);
@@ -2974,7 +3013,19 @@ esp_err_t multimodal_audio_begin_downstream_response(
}
TickType_t begin_tick = xTaskGetTickCount();
bool notify_interrupted_waiter = false;
portENTER_CRITICAL(&s_audio_runtime_lock);
if (s_downstream_playback_wait_registered &&
s_downstream_playback_wait_generation ==
ctx->downstream_playback_generation) {
s_downstream_last_terminal_valid = true;
s_downstream_last_terminal_generation =
ctx->downstream_playback_generation;
s_downstream_last_terminal_type =
MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STOPPED;
s_downstream_playback_wait_registered = false;
notify_interrupted_waiter = true;
}
uint32_t playback_generation = advance_downstream_playback_generation_locked();
ctx->downstream_response_active = true;
ctx->downstream_response_end_received = false;
@@ -2983,6 +3034,10 @@ esp_err_t multimodal_audio_begin_downstream_response(
ctx->downstream_last_audio_tick = begin_tick;
ctx->downstream_last_response_activity_tick = begin_tick;
portEXIT_CRITICAL(&s_audio_runtime_lock);
if (notify_interrupted_waiter &&
s_downstream_playback_completion_sem != NULL) {
(void)xSemaphoreGive(s_downstream_playback_completion_sem);
}
*out_playback_generation = playback_generation;
s_downstream_queued_packet_total = 0U;
@@ -3055,3 +3110,72 @@ esp_err_t multimodal_audio_finish_downstream_response(void)
}
return ESP_OK;
}
static esp_err_t downstream_terminal_type_to_error(
multimodal_audio_downstream_playback_event_type_t type)
{
switch (type) {
case MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_COMPLETED:
return ESP_OK;
case MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STALLED:
return ESP_ERR_TIMEOUT;
case MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STOPPED:
default:
return ESP_ERR_INVALID_STATE;
}
}
esp_err_t multimodal_audio_wait_downstream_playback_complete(
uint32_t playback_generation,
int timeout_ms)
{
if (playback_generation == 0U || timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
if (s_downstream_playback_completion_sem == NULL) {
return ESP_ERR_INVALID_STATE;
}
/* Remove a signal left by a waiter that timed out at a terminal-event race. */
(void)xSemaphoreTake(s_downstream_playback_completion_sem, 0U);
esp_err_t result = ESP_ERR_TIMEOUT;
portENTER_CRITICAL(&s_audio_runtime_lock);
if (s_downstream_last_terminal_valid &&
s_downstream_last_terminal_generation == playback_generation) {
result = downstream_terminal_type_to_error(
s_downstream_last_terminal_type);
portEXIT_CRITICAL(&s_audio_runtime_lock);
return result;
}
if (s_audio.downstream_playback_generation != playback_generation) {
portEXIT_CRITICAL(&s_audio_runtime_lock);
return ESP_ERR_INVALID_STATE;
}
if (s_downstream_playback_wait_registered) {
portEXIT_CRITICAL(&s_audio_runtime_lock);
return ESP_ERR_INVALID_STATE;
}
s_downstream_playback_wait_registered = true;
s_downstream_playback_wait_generation = playback_generation;
portEXIT_CRITICAL(&s_audio_runtime_lock);
TickType_t wait_ticks = pdMS_TO_TICKS((uint32_t)timeout_ms);
if (timeout_ms > 0 && wait_ticks == 0U) {
wait_ticks = 1U;
}
(void)xSemaphoreTake(s_downstream_playback_completion_sem, wait_ticks);
portENTER_CRITICAL(&s_audio_runtime_lock);
if (s_downstream_last_terminal_valid &&
s_downstream_last_terminal_generation == playback_generation) {
result = downstream_terminal_type_to_error(
s_downstream_last_terminal_type);
}
if (s_downstream_playback_wait_registered &&
s_downstream_playback_wait_generation == playback_generation) {
s_downstream_playback_wait_registered = false;
}
portEXIT_CRITICAL(&s_audio_runtime_lock);
return result;
}
@@ -38,7 +38,7 @@
#define VOICE_PROMPT_FRAME_HEADER_BYTES 4U
#define VOICE_PROMPT_MAX_OPUS_PACKET_BYTES 2048U
#define VOICE_PROMPT_OPUS_FRAME_MS 60U
#define VOICE_PROMPT_DRAIN_MARGIN_MS 120U
#define VOICE_PROMPT_COMPLETION_TIMEOUT_MARGIN_MS 3000U
static const char *TAG = "power_manager";
static const char *VOICE_TAG = "voice_prompt";
@@ -308,11 +308,20 @@ esp_err_t voice_prompt_play_file(const char *path)
}
if (err == ESP_OK) {
uint32_t drain_ms = packet_count * VOICE_PROMPT_OPUS_FRAME_MS +
VOICE_PROMPT_DRAIN_MARGIN_MS;
vTaskDelay(pdMS_TO_TICKS(drain_ms));
uint64_t timeout_ms =
(uint64_t)packet_count * VOICE_PROMPT_OPUS_FRAME_MS +
VOICE_PROMPT_COMPLETION_TIMEOUT_MARGIN_MS;
if (timeout_ms > INT32_MAX) {
timeout_ms = INT32_MAX;
}
err = multimodal_audio_wait_downstream_playback_complete(
playback_generation,
(int)timeout_ms);
}
if (err == ESP_OK) {
ESP_LOGI(VOICE_TAG,
"Prompt queued: %s packets=%u bytes=%u",
"Prompt completed: %s packets=%u bytes=%u",
resolved_path,
(unsigned)packet_count,
(unsigned)total_bytes);