Skip to content

fix: stabilize CoreS3 streamed audio playback - #650

Draft
meganetaaan wants to merge 5 commits into
developfrom
agent/stabilize-streaming-audio
Draft

fix: stabilize CoreS3 streamed audio playback#650
meganetaaan wants to merge 5 commits into
developfrom
agent/stabilize-streaming-audio

Conversation

@meganetaaan

@meganetaaan meganetaaan commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Disable ESP-IDF Wi-Fi modem sleep on M5StackChan CoreS3 after ECMA-419 Wi-Fi initialization.
  • Hold streamed PCM in the existing shared ring buffer until one second (48,000 bytes at 24 kHz PCM16 mono) is available.
  • Pause autonomous face motion during assistant playback while retaining output-level mouth animation.
  • Release impact: patch; .changeset/calm-streaming-audio.md is included.

Root Cause

Receive-only device tests showed that the audio data already arrived in bursts before AudioOut: with default power save the Worker notification gap reached 1,117 ms, while WIFI_PS_NONE reduced gaps over 100 ms by about 86% and gaps over 200 ms by about 95%. The remaining jitter is absorbed by the one-second prebuffer without allocating or copying another audio buffer.

ESP-IDF documents WIFI_PS_MIN_MODEM as the default. Moddable 8.3.1 and 9.0.0 both initialize and start Wi-Fi without overriding that default, so the missing override predates Moddable 9.0.0 / ESP-IDF 6.0.2. ESP-IDF 6.0.2 updates Wi-Fi/PHY binary submodules, so the combination may have changed how visible the latency is, but the exact version-dependent severity has not been isolated to a single upstream commit.

What Changed

  • Add a CoreS3-only stack-chan native hook for esp_wifi_set_ps(WIFI_PS_NONE); no Moddable source change is required.
  • Delay only the main-thread playback-head notification. The existing 512 KiB shared output ring remains the buffer, so there is no additional PCM copy.
  • Flush responses shorter than one second before the existing terminal silence marker.
  • Keep the existing ChatService half-duplex state transitions and status-bar microphone/speaker icons. The host FaceView interprets onChatState and suppresses autonomous face motion during LISTENING and buffered drain (WAITING); lip sync remains driven by onOutputLevelChanged.

Power consumption increases while Wi-Fi is active on CoreS3. Other targets retain their current behavior.

Verification

  • cd firmware && npm run format
  • cd firmware && npm run lint (passes; one pre-existing informational warning in connectivity/__tests__/fakes/crypt.ts)
  • cd firmware && npm run test:unit (387 passed)
  • cd firmware && npm run check:legacy-names
  • Moddable XS test: server-realtime-model on lin
  • Moddable XS tests: chat-status-bar and face-view-state on lin/m5stack
  • CoreS3 debug build with Moddable 9.0.0 and recursively updated ESP-IDF v6.0.2
  • Verified the final build has no esp_wifi_set_ps reference in Moddable's wifi-esp32.c.o; the reference comes from stack-chan's wifi-power-save.c.o
  • /dev/ttyACM0 M5StackChan, static 24 kHz PCM16 1 kHz stream, volume 0.2, 90 seconds × 2
    • run 1: 90.01 s audible, total gap 0 ms, maximum gap 0 ms
    • run 2: 90.01 s audible, total gap 0 ms, maximum gap 0 ms
    • analysis: PC microphone recording, 10 ms windows, 1 kHz amplitude threshold 500

Affected Areas

  • firmware
  • web
  • schematics
  • case
  • docs
  • ci/github-actions

Breaking Changes

  • none
  • yes, described below

Related Issues

Summary by CodeRabbit

  • Improvements

    • Improved CoreS3 Wi‑Fi connectivity by disabling power-saving behavior.
    • Added one-second audio buffering for smoother realtime playback, with short responses played when complete.
    • Updated facial animation behavior so autonomous motion pauses while listening or waiting, while lip-sync remains active during speech.
  • Tests

    • Expanded coverage for Wi‑Fi handling, audio buffering, playback completion, and chat-driven facial motion.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Wi-Fi power-save control

Layer / File(s) Summary
Wi-Fi power-save integration
firmware/host/modules/connectivity/..., .changeset/calm-streaming-audio.md
CoreS3 network initialization loads an ESP32 module that sets WIFI_PS_NONE. Tests verify one invocation. The changeset records the connectivity update.

Realtime audio prebuffering

Layer / File(s) Summary
Audio prebuffer and flush
firmware/host/modules/conversation/chat-audioio/server-openai-realtime-model.js
PCM16 audio waits for 48,000 bytes before playback. Remaining audio flushes on response.done.
Audio playback validation
firmware/host/modules/conversation/__tests__/server-realtime-model/*
Tests cover threshold playback and short-response flushing.

Chat-state face motion

Layer / File(s) Summary
Chat-state motion behavior
firmware/host/modules/ui/views/main/face-view.ts, firmware/host/modules/ui/views/main/__tests__/face-view-state/*
Autonomous face motion stops during LISTENING and WAITING and resumes during SPEAKING.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NetworkService
  participant Modules
  participant wifi_power_save
  participant ESP32Native
  NetworkService->>Modules: load wifi-power-save
  Modules->>wifi_power_save: invoke disableWiFiPowerSave
  wifi_power_save->>ESP32Native: disable Wi-Fi power saving
Loading
sequenceDiagram
  participant ServerChatWebSocketWorker
  participant ServerOpenAIRealtimeModel
  participant AudioConsumer
  ServerChatWebSocketWorker->>ServerOpenAIRealtimeModel: provide audio offset and size
  ServerOpenAIRealtimeModel->>ServerOpenAIRealtimeModel: buffer until 48000 bytes
  ServerOpenAIRealtimeModel->>AudioConsumer: deliver audio
  ServerOpenAIRealtimeModel->>AudioConsumer: flush on response.done
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: stabilizing streamed audio playback on CoreS3.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/stabilize-streaming-audio

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Cloudflare PR preview

Open the latest preview for commit 2003fd33e9fa.

Immutable deployment: https://c04c91b1.stack-chan-pr-preview.pages.dev

Warning

Pull request previews contain untrusted web and firmware code. Review the changes before granting WebSerial/Bluetooth permissions or flashing a device.

@meganetaaan

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
firmware/host/modules/conversation/__tests__/server-realtime-model/server-realtime-model.test.ts (1)

112-128: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert PCM coverage and terminal ordering, not only message count.

The threshold assertion passes when the implementation drops the first buffered chunks and forwards only the last chunk. A correct implementation may emit several queued ranges or one contiguous range, so receivedAudioCount() === 1 is not a valid correctness check.

Assert that emitted ranges cover 0..48000 in order for the threshold case and 48000..60000 for the short response. Also assert that receiveAudio appears before speak after response.done.

As per path instructions, tests must verify observable behavior or relational invariants; message count alone does not verify audio coverage or event ordering.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@firmware/host/modules/conversation/__tests__/server-realtime-model/server-realtime-model.test.ts`
around lines 112 - 128, Strengthen the test around the response.created/onBase64
threshold flow by inspecting each receiveAudio payload’s PCM range and asserting
ordered coverage of 0..48000, rather than only counting messages. For the short
response after response.done, assert coverage of 48000..60000 and verify the
receiveAudio event precedes the speak event; update the test fixtures or helpers
as needed to observe payload ranges and event order.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/calm-streaming-audio.md:
- Line 5: Update the changeset entry to state that disabling CoreS3 Wi-Fi power
saving increases CoreS3 power consumption, and document the tested target and
playback verification, including the one-second audio prebuffer and preserved
lip-sync/autonomous-motion behavior.

In
`@firmware/host/modules/conversation/chat-audioio/server-openai-realtime-model.js`:
- Around line 195-200: Update the prebuffering logic in the response event
handlers, including response.done and the threshold path around
outputPrebufferOffset/outputPrebufferSize, to queue every { offset, size } range
rather than overwrite prior chunks. When the threshold is reached or
response.done flushes the buffer, replay all queued ranges in arrival order,
preserving one receiveAudio delivery per range; only merge ranges when the ring
buffer guarantees they are contiguous and retained.

---

Nitpick comments:
In
`@firmware/host/modules/conversation/__tests__/server-realtime-model/server-realtime-model.test.ts`:
- Around line 112-128: Strengthen the test around the response.created/onBase64
threshold flow by inspecting each receiveAudio payload’s PCM range and asserting
ordered coverage of 0..48000, rather than only counting messages. For the short
response after response.done, assert coverage of 48000..60000 and verify the
receiveAudio event precedes the speak event; update the test fixtures or helpers
as needed to observe payload ranges and event order.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ce11e26-655b-47bc-b601-645ca86b75f0

📥 Commits

Reviewing files that changed from the base of the PR and between f16e56a and 2003fd3.

📒 Files selected for processing (13)
  • .changeset/calm-streaming-audio.md
  • firmware/host/modules/connectivity/__tests__/network-manager.test.ts
  • firmware/host/modules/connectivity/__tests__/network-service.test.ts
  • firmware/host/modules/connectivity/__tests__/network-service/manifest.json
  • firmware/host/modules/connectivity/esp32/wifi-power-save.c
  • firmware/host/modules/connectivity/esp32/wifi-power-save.js
  • firmware/host/modules/connectivity/manifest.json
  • firmware/host/modules/connectivity/network-service.ts
  • firmware/host/modules/conversation/__tests__/server-realtime-model/server-chat-websocket-worker.js
  • firmware/host/modules/conversation/__tests__/server-realtime-model/server-realtime-model.test.ts
  • firmware/host/modules/conversation/chat-audioio/server-openai-realtime-model.js
  • firmware/host/modules/ui/views/main/__tests__/face-view-state/face-view-state.test.ts
  • firmware/host/modules/ui/views/main/face-view.ts

"stack-chan": patch
---

CoreS3のWi-Fi省電力を無効化し、stack-chan-aiの音声を1秒プリバッファします。再生中は口パクを維持したまま自律顔モーションを抑制します。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the tested target and CoreS3 power impact.

The patch classification is present, but Line 5 does not state that disabling Wi-Fi power saving increases CoreS3 power consumption or identify the tested target and playback verification. Add those details.

Suggested release-note update
 CoreS3のWi-Fi省電力を無効化し、stack-chan-aiの音声を1秒プリバッファします。再生中は口パクを維持したまま自律顔モーションを抑制します。
+CoreS3ではWi-Fi消費電力が増加します。検証対象はCoreS3で、debug buildと90秒連続再生試験を2回実施しました。

As per path instructions: “Since this PR changes user-visible firmware behavior, retain the changeset and document tested targets and hardware-specific CoreS3 impact.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CoreS3のWi-Fi省電力を無効化し、stack-chan-aiの音声を1秒プリバッファします。再生中は口パクを維持したまま自律顔モーションを抑制します。
CoreS3のWi-Fi省電力を無効化し、stack-chan-aiの音声を1秒プリバッファします。再生中は口パクを維持したまま自律顔モーションを抑制します。
CoreS3ではWi-Fi消費電力が増加します。検証対象はCoreS3で、debug buildと90秒連続再生試験を2回実施しました。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/calm-streaming-audio.md at line 5, Update the changeset entry to
state that disabling CoreS3 Wi-Fi power saving increases CoreS3 power
consumption, and document the tested target and playback verification, including
the one-second audio prebuffer and preserved lip-sync/autonomous-motion
behavior.

Source: Path instructions

Comment on lines 195 to +200
'response.done'() {
if (this.outputPrebuffering) {
this.outputPrebuffering = false
if (this.outputPrebufferSize) {
super.onBase64(this.outputPrebufferOffset, this.outputPrebufferSize)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Preserve every prebuffered audio range.

When multiple chunks arrive before the threshold, Lines 212-214 overwrite the previous range. Line 217 then forwards only the last chunk. With the supplied test inputs, the first 36,000 bytes are lost. Lines 198-200 have the same defect for short responses.

Queue each { offset, size } range and replay all queued ranges in order when the threshold is reached or response.done flushes the buffer. Combine ranges only when the shared ring buffer guarantees contiguous, retained data.

The delivery contract in firmware/host/modules/conversation/__tests__/server-realtime-model/server-chat-websocket-worker.js forwards one receiveAudio message per range, so downstream code cannot recover omitted PCM.

Proposed fix
-    this.outputPrebufferOffset = 0
-    this.outputPrebufferSize = 0
+    this.outputPrebuffer = []

-      if (this.outputPrebufferSize) {
-        super.onBase64(this.outputPrebufferOffset, this.outputPrebufferSize)
+      for (const { offset, size } of this.outputPrebuffer) {
+        super.onBase64(offset, size)
       }
+      this.outputPrebuffer = []

-    this.outputPrebufferOffset = offset
-    this.outputPrebufferSize = size
+    this.outputPrebuffer.push({ offset, size })

-    super.onBase64(offset, size)
+    for (const chunk of this.outputPrebuffer) {
+      super.onBase64(chunk.offset, chunk.size)
+    }
+    this.outputPrebuffer = []

Also applies to: 212-217

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@firmware/host/modules/conversation/chat-audioio/server-openai-realtime-model.js`
around lines 195 - 200, Update the prebuffering logic in the response event
handlers, including response.done and the threshold path around
outputPrebufferOffset/outputPrebufferSize, to queue every { offset, size } range
rather than overwrite prior chunks. When the threshold is reached or
response.done flushes the buffer, replay all queued ranges in arrival order,
preserving one receiveAudio delivery per range; only merge ranges when the ring
buffer guarantees they are contiguous and retained.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant