已合并
fix echo #112
海思-陈哲宇创建于 9月8日
fix echo #112
已合并
共 2 个文件变更+60-5
| @@ -72,6 +72,7 @@ extern "C" int32_t VoiceCloudLlmAudioPlayerStop(void); | |||
| 72 | extern "C" int32_t VoiceCloudLlmAudioPlayerFeed(const uint8_t *data, uint32_t len); | 72 | extern "C" int32_t VoiceCloudLlmAudioPlayerFeed(const uint8_t *data, uint32_t len); |
| 73 | extern "C" int32_t VoiceCloudLlmAudioPlayerFlush(void); | 73 | extern "C" int32_t VoiceCloudLlmAudioPlayerFlush(void); |
| 74 | extern "C" bool VoiceCloudLlmAudioPlayerIsRunning(void); | 74 | extern "C" bool VoiceCloudLlmAudioPlayerIsRunning(void); |
| 75 | +extern "C" bool VoiceCloudLlmAudioPlayerIsPlaying(void); | ||
| 75 | 76 | ||
| 76 | namespace { | 77 | namespace { |
| 77 | 78 | ||
| @@ -1268,6 +1269,18 @@ static void VoiceCloudLlmHandleChatCompletedEvent(VoiceCloudLlmContext *ctx) | |||
| 1268 | VoiceCloudLlmGetState(ctx) != VOICE_CLOUD_LLM_STATE_AWAITING_CHAT_COMPLETION) { | 1269 | VoiceCloudLlmGetState(ctx) != VOICE_CLOUD_LLM_STATE_AWAITING_CHAT_COMPLETION) { |
| 1269 | return; | 1270 | return; |
| 1270 | } | 1271 | } |
| 1272 | + /* | ||
| 1273 | + * Drain the reply tail first: feeding the player returns before the | ||
| 1274 | + * audio is heard, so when CHAT_COMPLETED arrives the tail may still | ||
| 1275 | + * span the player queue, an in-flight write and the DAC ring. Poll | ||
| 1276 | + * until it has actually finished sounding, then restart capture into | ||
| 1277 | + * a quiet downlink. Stops/flushes (call interrupt, player stop) make | ||
| 1278 | + * IsPlaying() false, so this cannot hang on a dead player. | ||
| 1279 | + */ | ||
| 1280 | + const uint32_t kDrainPollMs = 10; | ||
| 1281 | + while (VoiceCloudLlmAudioPlayerIsPlaying()) { | ||
| 1282 | + osDelay(kDrainPollMs); | ||
| 1283 | + } | ||
| 1271 | VoiceCloudLlmStopWakeupRecognition(); | 1284 | VoiceCloudLlmStopWakeupRecognition(); |
| 1272 | if (VoiceCloudLlmAudioCaptureStart(VoiceCloudLlmGetConfig()) != 0) { | 1285 | if (VoiceCloudLlmAudioCaptureStart(VoiceCloudLlmGetConfig()) != 0) { |
| 1273 | WEARABLE_LOGE(WEARABLE_LOG_MODULE_SMART_VOICE, | 1286 | WEARABLE_LOGE(WEARABLE_LOG_MODULE_SMART_VOICE, |
| @@ -102,9 +102,15 @@ struct VoiceCloudLlmPlayerContext { | |||
| 102 | * thread answers with flushAck. */ | 102 | * thread answers with flushAck. */ |
| 103 | std::atomic<bool> flushRequested; | 103 | std::atomic<bool> flushRequested; |
| 104 | std::atomic<bool> flushAck; | 104 | std::atomic<bool> flushAck; |
| 105 | - /* Tick by which audio written before the current flush has finished | 105 | + /* Tick by which everything written so far has finished sounding |
| 106 | - * sounding (last write tick + ring estimate). */ | 106 | + * (last write tick + ring estimate); published after every real chunk |
| 107 | + * write — see VoiceCloudLlmAudioPlayerIsPlaying(). */ | ||
| 107 | std::atomic<uint32_t> ringEmptyTick; | 108 | std::atomic<uint32_t> ringEmptyTick; |
| 109 | + /* Real frames queued but not yet fully written: incremented on feed, | ||
| 110 | + * decremented when the player thread (or a flush) frees the frame. | ||
| 111 | + * Counted here instead of querying the queue so the dequeue window is | ||
| 112 | + * covered too. */ | ||
| 113 | + std::atomic<uint32_t> pendingFrames; | ||
| 108 | osMessageQueueId_t queue; | 114 | osMessageQueueId_t queue; |
| 109 | MediaThreadIdHandle thread; | 115 | MediaThreadIdHandle thread; |
| 110 | std::unique_ptr<Audio::AudioStreamOut> streamOut; | 116 | std::unique_ptr<Audio::AudioStreamOut> streamOut; |
| @@ -117,7 +123,7 @@ struct VoiceCloudLlmPlayerContext { | |||
| 117 | 123 | ||
| 118 | VoiceCloudLlmPlayerContext() | 124 | VoiceCloudLlmPlayerContext() |
| 119 | : started(false), stopRequested(false), flushRequested(false), flushAck(false), | 125 | : started(false), stopRequested(false), flushRequested(false), flushAck(false), |
| 120 | - ringEmptyTick(0), queue(nullptr), thread(nullptr), | 126 | + ringEmptyTick(0), pendingFrames(0), queue(nullptr), thread(nullptr), |
| 121 | streamOut(nullptr), sessionId(AUDIO_SESSION_ID_NONE), interruptActive(false), chunkBytes(0), volume(0) | 127 | streamOut(nullptr), sessionId(AUDIO_SESSION_ID_NONE), interruptActive(false), chunkBytes(0), volume(0) |
| 122 | { | 128 | { |
| 123 | (void)memset_s(&renderConfig, sizeof(renderConfig), 0, sizeof(renderConfig)); | 129 | (void)memset_s(&renderConfig, sizeof(renderConfig), 0, sizeof(renderConfig)); |
| @@ -157,12 +163,17 @@ bool VoiceCloudLlmQueueFrame(const uint8_t *data, uint32_t len) | |||
| 157 | } | 163 | } |
| 158 | frame.len = len; | 164 | frame.len = len; |
| 159 | 165 | ||
| 166 | + /* Count before enqueue (rolled back on failure): the player thread | ||
| 167 | + * decrements when it frees the frame, so the add must be visible | ||
| 168 | + * before the frame is. */ | ||
| 169 | + g_playerCtx.pendingFrames.fetch_add(1); | ||
| 160 | uint32_t try_times = 0; | 170 | uint32_t try_times = 0; |
| 161 | while (osMessageQueuePut(g_playerCtx.queue, &frame, 0, 0) != osOK) { | 171 | while (osMessageQueuePut(g_playerCtx.queue, &frame, 0, 0) != osOK) { |
| 162 | try_times++; | 172 | try_times++; |
| 163 | if (try_times >= VOICE_CLOUD_LLM_PLAYER_PUT_MESSAGE_MAX_TIMES) { | 173 | if (try_times >= VOICE_CLOUD_LLM_PLAYER_PUT_MESSAGE_MAX_TIMES) { |
| 164 | WEARABLE_LOGW(WEARABLE_LOG_MODULE_SMART_VOICE, "voice cloud llm player queue put fail\r\n"); | 174 | WEARABLE_LOGW(WEARABLE_LOG_MODULE_SMART_VOICE, "voice cloud llm player queue put fail\r\n"); |
| 165 | VoiceCloudLlmFreeFrame(&frame); | 175 | VoiceCloudLlmFreeFrame(&frame); |
| 176 | + g_playerCtx.pendingFrames.fetch_sub(1); | ||
| 166 | return false; | 177 | return false; |
| 167 | } | 178 | } |
| 168 | osDelay(VOICE_CLOUD_LLM_PLAYER_PUT_MESSAGE_DELAY_MS); | 179 | osDelay(VOICE_CLOUD_LLM_PLAYER_PUT_MESSAGE_DELAY_MS); |
| @@ -179,6 +190,7 @@ void VoiceCloudLlmFlushPlayerQueue() | |||
| 179 | VoiceCloudLlmPcmFrame frame = {}; | 190 | VoiceCloudLlmPcmFrame frame = {}; |
| 180 | while (osMessageQueueGet(g_playerCtx.queue, &frame, nullptr, 0) == osOK) { | 191 | while (osMessageQueueGet(g_playerCtx.queue, &frame, nullptr, 0) == osOK) { |
| 181 | VoiceCloudLlmFreeFrame(&frame); | 192 | VoiceCloudLlmFreeFrame(&frame); |
| 193 | + g_playerCtx.pendingFrames.fetch_sub(1); | ||
| 182 | } | 194 | } |
| 183 | } | 195 | } |
| 184 | 196 | ||
| @@ -361,6 +373,11 @@ static int16_t VoiceCloudLlmPlayerWriteFrameChunks( | |||
| 361 | wCtx.pacing.lastWriteCompleteTick = osKernelGetTickCount(); | 373 | wCtx.pacing.lastWriteCompleteTick = osKernelGetTickCount(); |
| 362 | wCtx.pacing.armed = true; | 374 | wCtx.pacing.armed = true; |
| 363 | wCtx.ringEst.update(wCtx.pacing.lastWriteCompleteTick, cfg.ms); | 375 | wCtx.ringEst.update(wCtx.pacing.lastWriteCompleteTick, cfg.ms); |
| 376 | + /* Publishing the drain deadline here is what makes IsPlaying() see | ||
| 377 | + * the write→sound-out latency: bytes handed to StreamWrite keep | ||
| 378 | + * sounding for ringEst.ms after the call returns. */ | ||
| 379 | + g_playerCtx.ringEmptyTick.store(wCtx.pacing.lastWriteCompleteTick + | ||
| 380 | + static_cast<uint32_t>(wCtx.ringEst.estMs)); | ||
| 364 | 381 | ||
| 365 | if (writeLen >= 2U) { | 382 | if (writeLen >= 2U) { |
| 366 | const int16_t *samples = reinterpret_cast<const int16_t *>(data + offset); | 383 | const int16_t *samples = reinterpret_cast<const int16_t *>(data + offset); |
| @@ -473,8 +490,8 @@ void *VoiceCloudLlmAudioPlayerThread(void *argument) | |||
| 473 | const uint32_t queueCount = (g_playerCtx.queue != nullptr) | 490 | const uint32_t queueCount = (g_playerCtx.queue != nullptr) |
| 474 | ? osMessageQueueGetCount(g_playerCtx.queue) : 0U; | 491 | ? osMessageQueueGetCount(g_playerCtx.queue) : 0U; |
| 475 | if (g_playerCtx.flushRequested.load() && queueCount == 0U) { | 492 | if (g_playerCtx.flushRequested.load() && queueCount == 0U) { |
| 476 | - g_playerCtx.ringEmptyTick.store(wCtx.pacing.lastWriteCompleteTick + | 493 | + /* ringEmptyTick is already current from the last real write — |
| 477 | - static_cast<uint32_t>(wCtx.ringEst.estMs)); | 494 | + * no new data can arrive while flushRequested is set. */ |
| 478 | g_playerCtx.flushRequested.store(false); | 495 | g_playerCtx.flushRequested.store(false); |
| 479 | g_playerCtx.flushAck.store(true); | 496 | g_playerCtx.flushAck.store(true); |
| 480 | } | 497 | } |
| @@ -498,6 +515,7 @@ void *VoiceCloudLlmAudioPlayerThread(void *argument) | |||
| 498 | 515 | ||
| 499 | if (g_playerCtx.stopRequested.load()) { | 516 | if (g_playerCtx.stopRequested.load()) { |
| 500 | VoiceCloudLlmFreeFrame(&frame); | 517 | VoiceCloudLlmFreeFrame(&frame); |
| 518 | + g_playerCtx.pendingFrames.fetch_sub(1); | ||
| 501 | break; | 519 | break; |
| 502 | } | 520 | } |
| 503 | 521 | ||
| @@ -505,6 +523,7 @@ void *VoiceCloudLlmAudioPlayerThread(void *argument) | |||
| 505 | VoiceCloudLlmPlayerProcessFrame(frame, gap, fState, cfg, wCtx); | 523 | VoiceCloudLlmPlayerProcessFrame(frame, gap, fState, cfg, wCtx); |
| 506 | } | 524 | } |
| 507 | VoiceCloudLlmFreeFrame(&frame); | 525 | VoiceCloudLlmFreeFrame(&frame); |
| 526 | + g_playerCtx.pendingFrames.fetch_sub(1); | ||
| 508 | } | 527 | } |
| 509 | return nullptr; | 528 | return nullptr; |
| 510 | } | 529 | } |
| @@ -536,6 +555,7 @@ void VoiceCloudLlmPlayerCleanup() | |||
| 536 | g_playerCtx.flushRequested.store(false); | 555 | g_playerCtx.flushRequested.store(false); |
| 537 | g_playerCtx.flushAck.store(false); | 556 | g_playerCtx.flushAck.store(false); |
| 538 | g_playerCtx.ringEmptyTick.store(0); | 557 | g_playerCtx.ringEmptyTick.store(0); |
| 558 | + g_playerCtx.pendingFrames.store(0); | ||
| 539 | g_playerCtx.started.store(false); | 559 | g_playerCtx.started.store(false); |
| 540 | g_playerCtx.chunkBytes = 0; | 560 | g_playerCtx.chunkBytes = 0; |
| 541 | g_playerCtx.thread = nullptr; | 561 | g_playerCtx.thread = nullptr; |
| @@ -584,6 +604,7 @@ static int32_t VoiceCloudLlmPlayerCreateStream(const VoiceCloudLlmConfig *config | |||
| 584 | g_playerCtx.flushRequested.store(false); | 604 | g_playerCtx.flushRequested.store(false); |
| 585 | g_playerCtx.flushAck.store(false); | 605 | g_playerCtx.flushAck.store(false); |
| 586 | g_playerCtx.ringEmptyTick.store(0); | 606 | g_playerCtx.ringEmptyTick.store(0); |
| 607 | + g_playerCtx.pendingFrames.store(0); | ||
| 587 | MediaThreadattr attr = {"voice_cloud_llm_play", VOICE_CLOUD_LLM_PLAYER_STACK_SIZE, | 608 | MediaThreadattr attr = {"voice_cloud_llm_play", VOICE_CLOUD_LLM_PLAYER_STACK_SIZE, |
| 588 | THREAD_SCHED_INVALID, VOICE_CLOUD_LLM_PLAYER_PRIORITY, false}; | 609 | THREAD_SCHED_INVALID, VOICE_CLOUD_LLM_PLAYER_PRIORITY, false}; |
| 589 | g_playerCtx.thread = MediaThreadCreate(VoiceCloudLlmAudioPlayerThread, nullptr, &attr); | 610 | g_playerCtx.thread = MediaThreadCreate(VoiceCloudLlmAudioPlayerThread, nullptr, &attr); |
| @@ -722,6 +743,27 @@ extern "C" bool VoiceCloudLlmAudioPlayerIsRunning(void) | |||
| 722 | return g_playerCtx.started.load() && !g_playerCtx.stopRequested.load(); | 743 | return g_playerCtx.started.load() && !g_playerCtx.stopRequested.load(); |
| 723 | } | 744 | } |
| 724 | 745 | ||
| 746 | +/* | ||
| 747 | + * True while real audio is still sounding. Feeding data returns long before | ||
| 748 | + * it is heard: frames may still sit in the player queue or be mid-write, and | ||
| 749 | + * bytes already handed to StreamWrite keep draining from the DAC ring for up | ||
| 750 | + * to ~192 ms afterwards. Covers all three stages, so a caller that loops on | ||
| 751 | + * this until false waits out the send-to-sound-out latency. Gap-bridging | ||
| 752 | + * silence does not count as playing. | ||
| 753 | + */ | ||
| 754 | +extern "C" bool VoiceCloudLlmAudioPlayerIsPlaying(void) | ||
| 755 | +{ | ||
| 756 | + if (!VoiceCloudLlmAudioPlayerIsRunning()) { | ||
| 757 | + return false; | ||
| 758 | + } | ||
| 759 | + if (g_playerCtx.pendingFrames.load() > 0U) { | ||
| 760 | + return true; | ||
| 761 | + } | ||
| 762 | + const int32_t ringRemainMs = static_cast<int32_t>(g_playerCtx.ringEmptyTick.load() + | ||
| 763 | + VOICE_CLOUD_LLM_PLAYER_RING_MARGIN_MS - osKernelGetTickCount()); | ||
| 764 | + return ringRemainMs > 0; | ||
| 765 | +} | ||
| 766 | + | ||
| 725 | extern "C" int32_t VoiceCloudLlmAudioPlayerStop(void) | 767 | extern "C" int32_t VoiceCloudLlmAudioPlayerStop(void) |
| 726 | { | 768 | { |
| 727 | if (!g_playerCtx.started.load() && g_playerCtx.thread == nullptr) { | 769 | if (!g_playerCtx.started.load() && g_playerCtx.thread == nullptr) { |