163 lines
5.8 KiB
Python
163 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
TEMPLATE — G1 Audio Script
|
|
|
|
Copy-paste starter demonstrating every reliable primitive for G1 audio:
|
|
|
|
1. Auto-detect and prep the Hollyland mic (g1_audio_devices helpers)
|
|
2. Record N seconds via parec (the method that actually works)
|
|
3. Load / generate PCM (16 kHz mono int16)
|
|
4. Play on the G1 speaker in ONE call (_CallRequestWithParamAndBin)
|
|
5. Always STOP the stream at the end (mandatory reset)
|
|
|
|
Do NOT use:
|
|
- PlayStream (chunked — unreliable, see voice_note.txt)
|
|
- PyAudio input against the pulse device on the robot (gives silence)
|
|
- aplay / paplay to the built-in speaker (no physical path)
|
|
|
|
Run on the robot, gemini conda env (needs _CallRequestWithParamAndBin):
|
|
/home/unitree/miniconda3/envs/gemini/bin/python3 template_audio_script.py
|
|
|
|
Run on the workstation (edit MODE below to "tone" — no robot needed).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
# ─── Constants ──────────────────────────────────────────────────────────────
|
|
TARGET_RATE = 16000
|
|
BIT_DEPTH = 16
|
|
CHANNELS = 1
|
|
APP_NAME = "my_app" # pick any unique label for YOUR script
|
|
DDS_IFACE = "eth0" # on workstation use "enp3s0"
|
|
VOLUME = 100
|
|
|
|
|
|
# ─── 1. Mic prep (optional — skip if recording is handled elsewhere) ────────
|
|
def prep_mic(keywords=("hollyland", "wireless_microphone")):
|
|
"""Auto-detect and unmute the first PulseAudio source matching keywords."""
|
|
rc = subprocess.run(["pactl", "list", "short", "sources"], capture_output=True, text=True)
|
|
for line in rc.stdout.splitlines():
|
|
parts = line.split("\t")
|
|
if len(parts) < 2:
|
|
continue
|
|
idx, name = parts[0], parts[1]
|
|
if any(k in name.lower() for k in keywords):
|
|
subprocess.run(["pactl", "set-default-source", name])
|
|
subprocess.run(["pactl", "set-source-mute", idx, "0"])
|
|
subprocess.run(["pactl", "set-source-volume", idx, "100%"])
|
|
return int(idx), name
|
|
return None, None
|
|
|
|
|
|
# ─── 2. Record via parec (the method that works on the robot) ───────────────
|
|
def record(seconds: float, source_index: int) -> bytes:
|
|
proc = subprocess.Popen(
|
|
["parec", "-d", str(source_index),
|
|
"--format=s16le", "--rate=16000", "--channels=1", "--raw"],
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
time.sleep(seconds)
|
|
proc.terminate()
|
|
return proc.stdout.read() if proc.stdout else b""
|
|
|
|
|
|
# ─── 3. Generate a demo PCM (used when MODE=tone) ───────────────────────────
|
|
def sine_tone(freq_hz: float, seconds: float) -> bytes:
|
|
n = int(TARGET_RATE * seconds)
|
|
out = bytearray()
|
|
for i in range(n):
|
|
val = int(0.3 * 32767 * math.sin(2 * math.pi * freq_hz * i / TARGET_RATE))
|
|
out += struct.pack("<h", val)
|
|
return bytes(out)
|
|
|
|
|
|
# ─── 4. Play PCM on the G1 speaker in ONE call ──────────────────────────────
|
|
def play_on_g1(pcm: bytes):
|
|
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,
|
|
)
|
|
|
|
ChannelFactoryInitialize(0, DDS_IFACE)
|
|
c = AudioClient()
|
|
c.SetTimeout(10.0)
|
|
c.Init()
|
|
c.SetVolume(VOLUME)
|
|
|
|
# 4a. Reset any previous stream first.
|
|
c._Call(ROBOT_API_ID_AUDIO_STOP_PLAY, json.dumps({"app_name": APP_NAME}))
|
|
time.sleep(0.3)
|
|
|
|
# 4b. Unique stream_id per play.
|
|
sid = f"s_{int(time.time() * 1000)}"
|
|
param = json.dumps({
|
|
"app_name": APP_NAME,
|
|
"stream_id": sid,
|
|
"sample_rate": TARGET_RATE,
|
|
"channels": CHANNELS,
|
|
"bits_per_sample": BIT_DEPTH,
|
|
})
|
|
|
|
# 4c. Send ALL audio in ONE call.
|
|
duration = len(pcm) / (TARGET_RATE * BIT_DEPTH // 8)
|
|
print(f"Playing {duration:.1f}s ...")
|
|
c._CallRequestWithParamAndBin(ROBOT_API_ID_AUDIO_START_PLAY, param, list(pcm))
|
|
time.sleep(duration + 0.5)
|
|
|
|
# 4d. Stop = mandatory cleanup.
|
|
c._Call(ROBOT_API_ID_AUDIO_STOP_PLAY, json.dumps({"app_name": APP_NAME}))
|
|
|
|
|
|
# ─── Main: pick a mode ──────────────────────────────────────────────────────
|
|
def main() -> int:
|
|
p = argparse.ArgumentParser(description="G1 audio template")
|
|
p.add_argument("mode", choices=["tone", "echo", "prep-mic"],
|
|
help="tone=440Hz beep; echo=record+play; prep-mic=auto-detect mic only")
|
|
p.add_argument("--seconds", "-s", type=float, default=3.0,
|
|
help="Recording / tone duration (default 3)")
|
|
args = p.parse_args()
|
|
|
|
if args.mode == "prep-mic":
|
|
idx, name = prep_mic()
|
|
if idx is None:
|
|
print("No wireless mic found.")
|
|
return 1
|
|
print(f"Prepped: [{idx}] {name}")
|
|
return 0
|
|
|
|
if args.mode == "tone":
|
|
pcm = sine_tone(440.0, args.seconds)
|
|
play_on_g1(pcm)
|
|
return 0
|
|
|
|
if args.mode == "echo":
|
|
idx, name = prep_mic()
|
|
if idx is None:
|
|
print("No wireless mic found.")
|
|
return 1
|
|
print(f"Mic: [{idx}] {name}")
|
|
print(f"Recording {args.seconds:.1f}s ... speak now")
|
|
pcm = record(args.seconds, idx)
|
|
if not pcm:
|
|
print("No audio captured.")
|
|
return 2
|
|
print(f"Captured {len(pcm):,} bytes. Playing back on G1 ...")
|
|
play_on_g1(pcm)
|
|
return 0
|
|
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|