Skip to main content
Pitfall

AudioClient.PlayStream() will reject audio that looks fine but isn't

Symptom

Audio sent to the G1's onboard speaker via AudioClient.PlayStream() either doesn't play, plays corrupted, or fails silently — even when the source audio sounds completely normal on a computer.

Environment

  • G1 EDU onboard speaker path (RockChip-owned, via AudioClient, not the external USB audio path)
  • TTS source: ElevenLabs (outputs pcm_22050)

Root Cause

PlayStream() has a narrow, undocumented-until-you-hit-it format requirement: 16kHz mono, 16-bit PCM, little-endian (int16). Nothing else works, and there's no helpful error — the API accepts the call and the failure shows up as silence, glitching, or garbage audio rather than a clear rejection.

Two specific gotchas compound this:

  1. Sample rate mismatch. ElevenLabs (and most modern TTS) outputs at 22050Hz or higher — you need to actually downsample, not just relabel the sample rate. A naive resample can introduce artifacts; scipy.signal.resample_poly gives a clean 3:1 downsample from 22050→16000Hz.
  2. Chunk size and pacing. The API expects a specific chunk size — 96000 bytes, which is exactly 3 seconds of audio at 16kHz mono 16-bit — with roughly a 1-second sleep between chunks. Sending audio in arbitrary chunk sizes, or streaming it too fast without pacing, causes playback issues even when the format itself is otherwise correct.

A separate trap worth flagging explicitly: cached/pre-rendered WAV files can go stale silently. A WAV cache generated at 48kHz (correct for a different playback path) will have corrupt frame counts when played through this 16kHz-only path — it's not that the file is broken, it's that it was rendered for the wrong destination. If you maintain a WAV cache across multiple audio-output paths, regenerate it explicitly for each target rather than assuming one WAV works everywhere.

Fix

import scipy.signal

# resample from ElevenLabs' native 22050Hz to the required 16000Hz
audio_16k = scipy.signal.resample_poly(audio_22050, up=16000, down=22050)

# chunk at exactly 96000 bytes (3 seconds @ 16kHz mono 16-bit),
# with ~1s pacing between chunks
CHUNK_SIZE = 96000
for i in range(0, len(audio_bytes), CHUNK_SIZE):
chunk = audio_bytes[i:i + CHUNK_SIZE]
audio_client.PlayStream(chunk)
time.sleep(1.0)

Reference

Format and chunking behavior confirmed against Unitree's own reference example (g1_audio_client_play_wav.py + wav.py in unitree_sdk2_python) — worth diffing your own implementation against that reference directly if you hit playback issues, rather than debugging from format assumptions alone.

Time cost: A few hours, most of it spent on the WAV-cache staleness issue specifically — the format requirement itself was findable from the reference example, but a cache generated for a different sample rate produced a confusing "corrupt frame count" symptom that looked unrelated to sample rate at first.