#!/usr/bin/env python3 """ G1 WAV Player — plays WAV files through the G1 built-in speaker. Runs ON the robot via AudioClient._CallRequestWithParamAndBin. (PlayStream chunked mode is unreliable — see voice_note.txt.) Usage: python3 g1_play_wav.py python3 g1_play_wav.py --volume 80 python3 g1_play_wav.py --list python3 g1_play_wav.py --all Examples: python3 g1_play_wav.py "/home/unitree/SanadVoice/recorded voices/welcome_single.wav" python3 g1_play_wav.py --list "/home/unitree/SanadVoice/recorded voices" python3 g1_play_wav.py --all "/home/unitree/SanadVoice/recorded voices" """ import sys import time import wave import json import argparse import numpy as np from pathlib import Path from unitree_sdk2py.core.channel import ChannelFactoryInitialize from unitree_sdk2py.g1.audio.g1_audio_client import AudioClient from unitree_sdk2py.g1.audio.g1_audio_api import ( ROBOT_API_ID_AUDIO_START_PLAY, ROBOT_API_ID_AUDIO_STOP_PLAY, ) TARGET_RATE = 16000 APP_NAME = "g1_player" def init_client(interface: str = "eth0", volume: int = 100): ChannelFactoryInitialize(0, interface) client = AudioClient() client.SetTimeout(10.0) client.Init() client.SetVolume(volume) return client def load_wav(path: str): """Load WAV and convert to 16kHz mono 16-bit PCM.""" with wave.open(path, "rb") as wf: rate = wf.getframerate() nch = wf.getnchannels() audio = np.frombuffer(wf.readframes(wf.getnframes()), dtype=np.int16) if nch == 2: audio = audio.reshape(-1, 2).mean(axis=1).astype(np.int16) if rate != TARGET_RATE: target_len = int(len(audio) * TARGET_RATE / rate) audio = np.interp( np.linspace(0, len(audio), target_len, endpoint=False), np.arange(len(audio)), audio.astype(np.float64), ).astype(np.int16) return audio def play_wav(client, pcm: np.ndarray, label: str = ""): """Send PCM to G1 speaker in ONE call (the working method).""" pcm_bytes = pcm.tobytes() duration = len(pcm) / TARGET_RATE if label: print(f" {label} ({duration:.1f}s)...", end=" ", flush=True) else: print(f" Playing {duration:.1f}s...", end=" ", flush=True) client._Call(ROBOT_API_ID_AUDIO_STOP_PLAY, json.dumps({"app_name": APP_NAME})) time.sleep(0.2) sid = f"s_{int(time.time() * 1000)}" param = json.dumps({ "app_name": APP_NAME, "stream_id": sid, "sample_rate": TARGET_RATE, "channels": 1, "bits_per_sample": 16, }) start = time.time() client._CallRequestWithParamAndBin( ROBOT_API_ID_AUDIO_START_PLAY, param, list(pcm_bytes) ) time.sleep(duration + 0.5) client._Call(ROBOT_API_ID_AUDIO_STOP_PLAY, json.dumps({"app_name": APP_NAME})) print(f"done ({time.time() - start:.1f}s)") def list_wavs(directory: str): """List all WAV files in a directory.""" wavs = sorted(Path(directory).glob("*.wav")) if not wavs: print(f"No WAV files in {directory}") return [] print(f"\n{'#':<4} {'Name':<40} {'Duration':>10}") print("-" * 56) for i, p in enumerate(wavs): try: with wave.open(str(p), "rb") as wf: dur = wf.getnframes() / wf.getframerate() print(f"{i:<4} {p.stem:<40} {dur:>8.1f}s") except Exception as e: print(f"{i:<4} {p.stem:<40} {'ERROR':>10}") print() return wavs def main(): parser = argparse.ArgumentParser(description="G1 WAV Player (runs on robot)") parser.add_argument("path", nargs="?", help="WAV file or directory path") parser.add_argument("--list", metavar="DIR", help="List WAV files in directory") parser.add_argument("--all", metavar="DIR", help="Play all WAV files in directory") parser.add_argument("--volume", "-v", type=int, default=100, help="Volume 0-100") parser.add_argument("--interface", "-i", default="eth0", help="Network interface") parser.add_argument("--pause", "-p", type=float, default=1.0, help="Pause between files in --all mode (seconds)") args = parser.parse_args() if args.list: list_wavs(args.list) return if args.all: wavs = sorted(Path(args.all).glob("*.wav")) if not wavs: print(f"No WAV files in {args.all}") return client = init_client(args.interface, args.volume) print(f"\nPlaying {len(wavs)} files from {args.all}\n") for i, wav_path in enumerate(wavs): pcm = load_wav(str(wav_path)) play_wav(client, pcm, label=f"[{i+1}/{len(wavs)}] {wav_path.stem}") if i < len(wavs) - 1: time.sleep(args.pause) print(f"\nAll done.") return if not args.path: parser.print_help() return path = Path(args.path) if not path.exists(): print(f"File not found: {path}") sys.exit(1) client = init_client(args.interface, args.volume) pcm = load_wav(str(path)) play_wav(client, pcm, label=path.stem) if __name__ == "__main__": main()