fix(audio): defer and coalesce role switch notices
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user