169 lines
5.1 KiB
Python
169 lines
5.1 KiB
Python
#!/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 <wav_file>
|
|
python3 g1_play_wav.py <wav_file> --volume 80
|
|
python3 g1_play_wav.py --list <directory>
|
|
python3 g1_play_wav.py --all <directory>
|
|
|
|
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()
|