Skip to content

Commit 0e04f03

Browse files
feat(audio): let moves carry non-wav sound files (#1220)
GStreamer playbin already sniffs content, so non-wav audio plays fine; the only wav assumptions were in naming/lookup, not playback. - recorded_move: dataset sidecar lookup tries all containers the daemon can play (wav/mp3/ogg/oga/opus/flac/m4a/aac), wav winning ties, instead of only .wav. This is what actually unblocks bundled non-wav moves. - protocol: generalize UploadAudioStartCmd.encoding from wav-base64 to any "<container>-base64"; wav-base64 stays the default so older clients are unaffected. - daemon: _handle_audio_finish derives the written extension from the declared encoding instead of hard-coding .wav. The TS SDK still sends encoding='wav-base64', so live uploads stay wav until it's updated; this only widens what the daemon accepts. Assisted-by: Claude:claude-opus-4-8
1 parent a0fcf67 commit 0e04f03

3 files changed

Lines changed: 28 additions & 12 deletions

File tree

src/reachy_mini/daemon/backend/abstract.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1633,7 +1633,9 @@ def _handle_audio_finish(self, cmd: UploadAudioFinishCmd) -> None:
16331633
import base64
16341634
raw = base64.b64decode(payload, validate=False)
16351635
os.makedirs(self._audio_temp_dir, exist_ok=True)
1636-
path = os.path.join(self._audio_temp_dir, f"{cmd.upload_id}.wav")
1636+
# encoding "<container>-base64" → file extension; wav for legacy clients.
1637+
ext = str(meta.get("encoding", "wav-base64")).split("-")[0] or "wav"
1638+
path = os.path.join(self._audio_temp_dir, f"{cmd.upload_id}.{ext}")
16371639
with open(path, "wb") as f:
16381640
f.write(raw)
16391641
except Exception as e:

src/reachy_mini/io/protocol.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -526,11 +526,10 @@ class UploadAudioStartCmd(BaseModel):
526526
matching move is held until either a matching move arrives or
527527
the TTL expires.
528528
529-
``encoding`` is currently always ``"wav-base64"``: raw PCM WAV
530-
bytes (any container the GStreamer playbin can decode) sliced
531-
into chunks and base64-encoded. Defined as an enum so a future
532-
encoding (raw binary frames, opus, ...) can be added without
533-
breaking older clients.
529+
``encoding`` is ``"<container>-base64"``: transport is always
530+
base64, the prefix just sets the written file's extension (playbin
531+
sniffs content, so playback works regardless). Defaults to
532+
``"wav-base64"`` for older clients.
534533
"""
535534

536535
type: Literal["upload_audio_start"] = "upload_audio_start"
@@ -541,7 +540,16 @@ class UploadAudioStartCmd(BaseModel):
541540
# exposing the CM4 to multi-GB allocations on a misbehaving
542541
# client.
543542
total_chunks: int = Field(..., ge=1, le=16384)
544-
encoding: Literal["wav-base64"] = "wav-base64"
543+
encoding: Literal[
544+
"wav-base64",
545+
"mp3-base64",
546+
"ogg-base64",
547+
"oga-base64",
548+
"opus-base64",
549+
"flac-base64",
550+
"m4a-base64",
551+
"aac-base64",
552+
] = "wav-base64"
545553
description: str = ""
546554

547555

src/reachy_mini/motion/recorded_move.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
logger = logging.getLogger(__name__)
1818

19+
# Sidecar audio containers; mirrors media.py ALLOWED_SOUND_EXTENSIONS. .wav wins ties.
20+
SOUND_EXTENSIONS = (".wav", ".mp3", ".ogg", ".oga", ".opus", ".flac", ".m4a", ".aac")
21+
1922
# Default datasets to preload at daemon startup
2023
DEFAULT_DATASETS = [
2124
"pollen-robotics/reachy-mini-emotions-library",
@@ -200,11 +203,14 @@ def process(self) -> None:
200203
move = json.load(open(move_path, "r"))
201204
self.moves[move_name] = move
202205

203-
sound_path = move_path.with_suffix(".wav")
204-
self.sounds[move_name] = None
205-
206-
if os.path.exists(sound_path):
207-
self.sounds[move_name] = sound_path
206+
self.sounds[move_name] = next(
207+
(
208+
p
209+
for ext in SOUND_EXTENSIONS
210+
if (p := move_path.with_suffix(ext)).exists()
211+
),
212+
None,
213+
)
208214

209215
def get(self, move_name: str) -> RecordedMove:
210216
"""Get a recorded move by name."""

0 commit comments

Comments
 (0)