G1_Lootah/Audio_Recorder/g1_interactive_player.py

185 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""
G1 Interactive Voice Player
----------------------------
Lists WAV files and lets you pick which ones to play on the G1 built-in speaker.
Uses _CallRequestWithParamAndBin with format params (the working method).
Usage (run ON the robot):
python3 g1_interactive_player.py "/home/unitree/SanadVoice/recorded voices"
python3 g1_interactive_player.py "/home/unitree/SanadVoice/recorded voices" --volume 80
"""
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 = "p"
def load_wav(path: str):
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:
tl = int(len(audio) * TARGET_RATE / rate)
audio = np.interp(
np.linspace(0, len(audio), tl, endpoint=False),
np.arange(len(audio)),
audio.astype(np.float64),
).astype(np.int16)
return audio
def play_pcm(client, pcm: np.ndarray):
# Single call with a unique stream_id — chunked repeats with the same sid
# are dropped by the audio service, per voice_note.txt.
client._Call(ROBOT_API_ID_AUDIO_STOP_PLAY, json.dumps({"app_name": APP_NAME}))
time.sleep(0.2)
pcm_bytes = pcm.tobytes()
duration = len(pcm) / TARGET_RATE
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.3)
client._Call(ROBOT_API_ID_AUDIO_STOP_PLAY, json.dumps({"app_name": APP_NAME}))
return time.time() - start
def show_menu(wavs):
print(f"\n{'#':<4} {'Name':<45} {'Duration':>8}")
print("-" * 59)
for i, (p, dur) in enumerate(wavs):
print(f"{i:<4} {p.stem:<45} {dur:>6.1f}s")
print("-" * 59)
print(f"{'a':<4} {'Play ALL':<45}")
print(f"{'m':<4} {'Show menu':<45}")
print(f"{'q':<4} {'Quit':<45}")
print()
def main():
parser = argparse.ArgumentParser(description="G1 Interactive Voice Player")
parser.add_argument("directory", help="Directory containing WAV files")
parser.add_argument("--volume", "-v", type=int, default=100, help="Volume 0-100")
parser.add_argument("--interface", "-i", default="eth0", help="Network interface")
args = parser.parse_args()
wav_dir = Path(args.directory)
if not wav_dir.is_dir():
print(f"Not a directory: {wav_dir}")
sys.exit(1)
wav_files = []
for p in sorted(wav_dir.glob("*.wav")):
try:
with wave.open(str(p), "rb") as wf:
dur = wf.getnframes() / wf.getframerate()
wav_files.append((p, dur))
except Exception:
pass
if not wav_files:
print(f"No WAV files found in {wav_dir}")
sys.exit(1)
print(f"Connecting to G1 speaker...")
ChannelFactoryInitialize(0, args.interface)
client = AudioClient()
client.SetTimeout(10.0)
client.Init()
client.SetVolume(args.volume)
print(f"Ready. Volume: {args.volume}%")
show_menu(wav_files)
while True:
try:
choice = input("Pick a number (or a/m/q): ").strip().lower()
except (KeyboardInterrupt, EOFError):
print("\nBye.")
break
if choice == "q":
print("Bye.")
break
if choice == "m":
show_menu(wav_files)
continue
if choice == "a":
print(f"\nPlaying all {len(wav_files)} files...\n")
for i, (p, dur) in enumerate(wav_files):
print(f" [{i+1}/{len(wav_files)}] {p.stem} ({dur:.1f}s)...", end=" ", flush=True)
pcm = load_wav(str(p))
elapsed = play_pcm(client, pcm)
print(f"done ({elapsed:.1f}s)")
time.sleep(0.5)
print("\nAll done.\n")
continue
indices = []
for part in choice.replace(" ", "").split(","):
if "-" in part:
try:
a, b = part.split("-", 1)
indices.extend(range(int(a), int(b) + 1))
except ValueError:
pass
else:
try:
indices.append(int(part))
except ValueError:
pass
if not indices:
print("Invalid. Enter number, range (2-5), list (1,3,5), a, m, or q.")
continue
for idx in indices:
if idx < 0 or idx >= len(wav_files):
print(f" #{idx} out of range, skipping.")
continue
p, dur = wav_files[idx]
print(f" Playing: {p.stem} ({dur:.1f}s)...", end=" ", flush=True)
pcm = load_wav(str(p))
elapsed = play_pcm(client, pcm)
print(f"done ({elapsed:.1f}s)")
print()
if __name__ == "__main__":
main()