fix(audio): defer and coalesce role switch notices

This commit is contained in:
chenjintao
2026-10-13 15:56:00 +08:00
parent 109a95497a
commit 23983ccdc8
4 changed files with 214 additions and 11 deletions
@@ -108,6 +108,7 @@ static char payload[MQTT_PAYLOAD_SIZE] = {0};
extern int fota_start;
int app_play_ota_tips_file(void);
int app_role_notice_handle_url(const char *url);
int xiaoice_get_play_counter(void);
typedef struct {
@@ -1510,7 +1511,9 @@ void handle_command(command_t *cmd)
app_mqtt_publish(resp, strlen(resp), 0);
free(resp);
add_file_to_play_player_queue(cmd->params.nfc.url, 0);
if (!app_role_notice_handle_url(cmd->params.nfc.url)) {
add_file_to_play_player_queue(cmd->params.nfc.url, 0);
}
break;
}
@@ -46,6 +46,7 @@ extern ql_sem_t sender_sem;
extern void conversation_no_speech(void);
extern void conversation_record_begin(void);
extern void conversation_cancel_wait(void);
extern int app_role_notice_before_finish(void);
// 配置
static int sample_rate = 16000; // FVAD 支持的采样率:8k/16k/32k/48k
@@ -502,6 +503,7 @@ void vad_audio_process(void) {
case STATE_WAITING_COMMAND:
if(vad_tts_playback_finished_status == 1) {
if (app_role_notice_before_finish()) break;
int leave_message_finished = vad_leave_message_flag != 0;
printf("finished_status:%d, no_speech_timeout:%d\n", vad_tts_playback_finished_status, no_speech_timeout);
set_state(STATE_IDLE);
@@ -65,6 +65,13 @@ static int round_response_received = 0;
static unsigned int active_session_round = 0;
static int active_session_valid = 0;
static uint64_t startup_start_ms = 0;
#define ROLE_NOTICE_TYPE 5
#define ROLE_NOTICE_URL_SIZE 256
static char role_notice_url[ROLE_NOTICE_URL_SIZE];
static int role_notice_pending = 0;
static int role_notice_playing = 0;
static int role_notice_consumed = 0;
static unsigned int role_notice_round = 0;
static void conversation_lock(void)
{
@@ -84,6 +91,9 @@ static void conversation_begin_round(void)
round_response_received = 0;
active_session_valid = 0;
startup_start_ms = hal_os_up_time_ms();
/* Accepted restart cancels the preceding round's deferred notice. */
role_notice_pending = role_notice_playing = role_notice_consumed = 0;
role_notice_url[0] = '\0';
}
void conversation_record_begin(void)
@@ -98,6 +108,8 @@ void conversation_cancel_wait(void)
{
conversation_lock();
conversation_wait = WAIT_NONE;
role_notice_pending = role_notice_playing = role_notice_consumed = 0;
role_notice_url[0] = '\0';
conversation_unlock();
}
@@ -129,6 +141,7 @@ extern int aofei_mp3_player_get_result(void);
extern int vad_leave_message_is_active(void);
extern int vad_cancel_leave_message(void);
extern void vad_reset_conversation_flags(void);
extern system_state_t vad_audio_get_state(void);
void sendAuthentication(void);
void vad_start_func(void);
@@ -139,8 +152,8 @@ int audio_packet_type = 0;
typedef struct ice_work_item_s
{
int type; //0-tts_out 1-session_continue 2-session_end 3-云端超时提示(失败时播放本地兜底) 4-本地超时兜底
unsigned int timeout_round; /* Timeout items (type 3/4) use this cancellation token. */
int type; //0-tts_out 1-session_continue 2-session_end 3-云端超时提示(失败时播放本地兜底) 4-本地超时兜底 5-角色切换提示
unsigned int timeout_round; /* Startup, timeout and role notices use this round token. */
int error;
int del_url;
void *data;
@@ -373,6 +386,99 @@ int add_file_to_play_player_queue(char *path, int type)
}
/* Only this resource is a role-change notice. Other NFC notices keep their
* existing immediate queue path, including binding and card audio. */
static int role_notice_is_url(const char *url)
{
const char *host, *path;
const char resource[] = "/assets/audio/switch_role.mp3";
size_t n = sizeof(resource) - 1;
if (!url) return 0;
host = strstr(url, "://");
if (!host) return 0;
path = strchr(host + 3, '/');
return path && strncmp(path, resource, n) == 0 &&
(path[n] == '\0' || path[n] == '?' || path[n] == '#');
}
/* Caller holds conversation_mutex. A full queue/failed download must not
* prevent the original conversation completion from proceeding. */
static int role_notice_start_locked(void)
{
role_notice_pending = 0;
role_notice_consumed = 1;
role_notice_playing = 1;
role_notice_round = conversation_round;
if (add_file_to_play_player_queue(role_notice_url, ROLE_NOTICE_TYPE) != 0) {
role_notice_playing = 0;
printf("role notice queue failed:round=%u\n", conversation_round);
return 0;
}
printf("role notice queued once:round=%u\n", conversation_round);
return 1;
}
/* Called only after the normal VAD playback-finished signal, or while idle.
* The original finished flag/no-speech flag stays untouched during the notice. */
static int role_notice_poll_locked(int finishing)
{
if (fota_start || power_down_status || get_sleep_mode()) {
role_notice_pending = role_notice_playing = 0;
return 0;
}
if (role_notice_playing) return 1;
if (!role_notice_pending) return 0;
if (role_notice_round != conversation_round) {
role_notice_pending = 0;
return 0;
}
if (vad_audio_is_recording() || g_mic_working || shine_init ||
timeout_action_busy || player_mq_couter > 0 || tts_playing != 0 ||
conversation_wait != WAIT_NONE) return 1;
if (!finishing && vad_audio_get_state() != STATE_IDLE) return 1;
return role_notice_start_locked();
}
int app_role_notice_handle_url(const char *url)
{
if (!role_notice_is_url(url)) return 0;
conversation_lock();
if (strlen(url) >= sizeof(role_notice_url)) {
printf("role notice URL too long\n");
} else if (role_notice_playing ||
(role_notice_consumed && vad_audio_get_state() != STATE_IDLE)) {
printf("role notice coalesced:round=%u\n", conversation_round);
} else {
/* Repeated commands before playback replace one pending slot. */
snprintf(role_notice_url, sizeof(role_notice_url), "%s", url);
role_notice_pending = 1;
role_notice_round = conversation_round;
printf("role notice pending:round=%u\n", conversation_round);
role_notice_poll_locked(0);
}
conversation_unlock();
return 1;
}
int app_role_notice_before_finish(void)
{
int wait;
conversation_lock();
wait = role_notice_poll_locked(1);
conversation_unlock();
return wait;
}
static void role_notice_finished(unsigned int round, int result)
{
conversation_lock();
if (role_notice_round == round && round == conversation_round) {
role_notice_playing = 0;
printf("role notice finished:round=%u result=%d\n", round, result);
}
conversation_unlock();
}
//如果录音后 超过10s 还没有收到信息 提示本轮对话结束
static void onmessage_text_locked(rws_socket socket, const char *text, const unsigned int len)
{
@@ -1104,6 +1210,9 @@ int app_play_tips_file(int index)
int app_play_ota_tips_file(void)
{
conversation_lock();
role_notice_pending = role_notice_playing = role_notice_consumed = 0;
conversation_unlock();
char filename_buff[64] = {0};
uint32_t timeout;
@@ -1170,8 +1279,10 @@ void app_player_thread(void *arvg)
if(player_item!=NULL){
int stale_timeout;
conversation_lock();
stale_timeout = (player_item->type == 2 || player_item->type == 3 || player_item->type == 4) &&
player_item->timeout_round != conversation_round;
stale_timeout = ((player_item->type == 2 || player_item->type == 3 || player_item->type == 4 ||
player_item->type == ROLE_NOTICE_TYPE) &&
player_item->timeout_round != conversation_round) ||
(player_item->type == ROLE_NOTICE_TYPE && !role_notice_playing);
conversation_unlock();
if (stale_timeout) {
if (player_mq_couter > 0) player_mq_couter--;
@@ -1210,7 +1321,7 @@ void app_player_thread(void *arvg)
conversation_unlock();
if(player_mq_couter==0){
if(player_mq_couter==0 && player_item->type != ROLE_NOTICE_TYPE){
printf("player_mq_couter==0\r\n");
if(tts_play_status==2){
@@ -1233,6 +1344,9 @@ void app_player_thread(void *arvg)
vad_leave_message();
}
}
if (player_item->type == ROLE_NOTICE_TYPE) {
role_notice_finished(player_item->timeout_round, play_ret);
}
if (player_item->type == 2) {
conversation_finish_startup_prompt(player_item->timeout_round, play_ret);
}
@@ -1507,6 +1621,9 @@ static void check_connect_thread_entry(void *parameter)
#endif
#if 1
conversation_check_timeout();
conversation_lock();
role_notice_poll_locked(0);
conversation_unlock();
#endif
ql_rtos_task_sleep_ms(3000);
+86 -5
View File
@@ -1,4 +1,4 @@
"""Run firmware timeout/round functions on host with scripted RTOS and transport."""
"""Run timeout, round and role-notice scheduling with scripted RTOS and transport."""
from pathlib import Path
import subprocess
import tempfile
@@ -27,7 +27,13 @@ harness = r'''
#include <string.h>
typedef void *ql_mutex_t;
static int g_mic_working, recording, shine_init, fota_start;
enum { STATE_IDLE, STATE_RECORDING_WAIT_SOUND, STATE_RECORDING_ACTIVE, STATE_WAITING_COMMAND };
static int tts_playing, tts_play_status, audio_packet_type, no_speech_timeout;
static int power_down_status, sleeping, vad_state;
static int queue_failure, last_queue_type;
static char last_queue_url[256];
static int get_sleep_mode(void) { return sleeping; }
static int vad_audio_get_state(void) { return vad_state; }
static int queued, disconnected, flags_reset, mutex_locked, player_mq_couter, vad_starts;
static uint64_t now;
static unsigned int gSequenceNumber;
@@ -42,8 +48,14 @@ static uint64_t hal_os_up_time_ms(void) { return now; }
static void vad_set_speech_timeout_flag(int value) { no_speech_timeout = value; }
static void vad_reset_conversation_flags(void) { no_speech_timeout = 0; flags_reset++; }
static int build_random_tts_error_url(char *p, size_t n) { (void)p; (void)n; return 1; }
static void add_file_to_play_player_queue(char *p, int type) { (void)p; assert(type == 3); queued++; }
static void vad_start_func(void) { vad_starts++; recording = 1; g_mic_working = 0; }
static int add_file_to_play_player_queue(char *p, int type) {
assert(type == 3 || type == 5);
if (queue_failure) return -1;
queued++; player_mq_couter++; last_queue_type = type;
snprintf(last_queue_url, sizeof(last_queue_url), "%s", p);
return 0;
}
static void vad_start_func(void) { vad_starts++; recording = 1; vad_state = STATE_RECORDING_WAIT_SOUND; g_mic_working = 0; }
static void app_play_tips_file(int n) { (void)n; assert(0); }
static int conversation_try_begin(void);
static void talkingq_ws_disconnect(void) {
@@ -54,7 +66,7 @@ static void talkingq_ws_disconnect(void) {
static int sendRecordingAudioPacket(AudioPacket *p) { assert(p->packetType == 2); return end_result; }
'''
harness += source[state_start:state_end]
for name in ['conversation_try_begin', 'sendEndPacket', 'conversation_check_timeout', 'conversation_accept_voice_message', 'conversation_finish_startup_prompt', 'conversation_find_session_id']:
for name in ['conversation_try_begin', 'sendEndPacket', 'conversation_check_timeout', 'conversation_accept_voice_message', 'conversation_finish_startup_prompt', 'conversation_find_session_id', 'role_notice_is_url', 'role_notice_start_locked', 'role_notice_poll_locked', 'app_role_notice_handle_url', 'app_role_notice_before_finish', 'role_notice_finished']:
harness += function(name)
harness += r'''
static void reset(void) {
@@ -62,6 +74,9 @@ static void reset(void) {
conversation_round = wait_round = sender_round = 0;
conversation_wait = WAIT_NONE; timeout_action_busy = round_response_received = 0;
queued = disconnected = flags_reset = no_speech_timeout = mutex_locked = 0;
power_down_status = sleeping = queue_failure = 0; vad_state = STATE_IDLE;
role_notice_pending = role_notice_playing = role_notice_consumed = 0;
role_notice_round = 0; last_queue_type = 0; last_queue_url[0] = 0;
now = 1000; end_result = 0; player_mq_couter = vad_starts = 0; active_session_valid = 0; startup_start_ms = 0; tts_playing = 0;
}
int main(void) {
@@ -126,7 +141,73 @@ int main(void) {
assert(conversation_try_begin() == 0);
now += 30001; player_mq_couter = 1; conversation_check_timeout(); assert(g_mic_working);
player_mq_couter = 0; recording = 1; conversation_check_timeout(); assert(g_mic_working);
puts("PASS: late backend error admission, prompt completion with queued audio, stale prompt, failed playback, stalled-startup recovery; no-speech prompt, new-round cancellation, ignored recording keys, END deadline, stale rounds, early reply, send failure, timeout/key race, auto-listening");
/* The exact resource alone is routed; unrelated notices retain original path. */
reset();
assert(!app_role_notice_handle_url("http://host/assets/audio/card.mp3"));
assert(!app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3.extra"));
assert(!role_notice_is_url("http://host/other/switch_role.mp3"));
assert(!queued);
/* No speech: retain one notice until tts_error + optional local fallback ends. */
reset(); recording = 1; vad_state = STATE_RECORDING_WAIT_SOUND;
assert(app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3"));
assert(!queued && role_notice_pending);
recording = 0; vad_state = STATE_WAITING_COMMAND; conversation_wait = WAIT_NO_SPEECH;
assert(app_role_notice_before_finish() && !queued);
conversation_wait = WAIT_NONE; player_mq_couter = 1; no_speech_timeout = 1;
assert(app_role_notice_before_finish() && !queued);
player_mq_couter = 0;
assert(app_role_notice_before_finish() && queued == 1 && last_queue_type == 5);
assert(no_speech_timeout == 1); /* Error flag never becomes auto-listening. */
assert(app_role_notice_before_finish() && queued == 1);
player_mq_couter = 0; role_notice_finished(conversation_round, 0);
assert(!app_role_notice_before_finish() && no_speech_timeout == 1);
/* Speech: zero insertion while uploading/waiting or between reply segments. */
reset(); recording = 1; vad_state = STATE_RECORDING_ACTIVE;
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
recording = 0; vad_state = STATE_WAITING_COMMAND; conversation_wait = WAIT_REPLY;
assert(app_role_notice_before_finish() && !queued);
conversation_wait = WAIT_NONE; tts_playing = 1;
assert(app_role_notice_before_finish() && !queued);
tts_playing = 0; player_mq_couter = 2;
assert(app_role_notice_before_finish() && !queued);
player_mq_couter = 0;
assert(app_role_notice_before_finish() && queued == 1);
role_notice_finished(conversation_round, 0); player_mq_couter = 0;
assert(!app_role_notice_before_finish() && no_speech_timeout == 0);
/* Repeated switches produce one latest URL at the end, never six entries. */
reset(); vad_state = STATE_WAITING_COMMAND; tts_playing = 1;
for (int n = 0; n < 6; n++) {
char url[128]; snprintf(url, sizeof(url), "http://host/assets/audio/switch_role.mp3?v=%d", n);
assert(app_role_notice_handle_url(url)); assert(!queued);
}
tts_playing = 0;
assert(app_role_notice_before_finish() && queued == 1);
assert(strstr(last_queue_url, "v=5"));
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
assert(queued == 1 && !role_notice_pending);
role_notice_finished(conversation_round, 0); player_mq_couter = 0;
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
assert(queued == 1 && !app_role_notice_before_finish());
/* Idle plays promptly; playback/queue failure releases completion too. */
reset(); app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
assert(queued == 1); player_mq_couter = 0;
role_notice_finished(conversation_round, -1); assert(!app_role_notice_before_finish());
reset(); queue_failure = 1;
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
assert(!queued && !app_role_notice_before_finish());
/* Accepted restart/force-stop cancels notice; no replay into next round. */
reset(); vad_state = STATE_WAITING_COMMAND; tts_playing = 1;
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
assert(conversation_try_begin() == 0 && !role_notice_pending);
reset(); recording = 1; app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
conversation_cancel_wait(); assert(!role_notice_pending && !role_notice_playing);
for (int mode = 0; mode < 3; mode++) {
reset(); vad_state = STATE_WAITING_COMMAND;
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
fota_start = mode == 0; power_down_status = mode == 1; sleeping = mode == 2;
assert(!app_role_notice_before_finish() && !role_notice_pending && !queued);
}
puts("PASS: role notices: silent/speech end, latest-only coalescing, idle, failure, restart/OTA/shutdown/sleep isolation; late backend error admission, prompt completion with queued audio, stale prompt, failed playback, stalled-startup recovery; no-speech prompt, new-round cancellation, ignored recording keys, END deadline, stale rounds, early reply, send failure, timeout/key race, auto-listening");
}
'''
with tempfile.TemporaryDirectory(prefix='banban-timeout-') as directory: