From e1b5e3ab76ee83fcb5a3ccd7c4c5aa04b9bd635d Mon Sep 17 00:00:00 2001 From: Lucca Pirovano Date: Thu, 16 Jul 2026 17:05:29 -0400 Subject: [PATCH] added V for voice chat, it outputs my laptop mic out the nao speaker --- naowalk.py | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 140 insertions(+), 5 deletions(-) diff --git a/naowalk.py b/naowalk.py index 65b8adc..d8921f8 100644 --- a/naowalk.py +++ b/naowalk.py @@ -14,6 +14,7 @@ import subprocess import os import json import re +import socket # Import the new music module from naomusic import NaoMusicPlayer @@ -25,6 +26,13 @@ try: except ImportError: HAS_AUDIO = False +# --- SSH for live mic relay (push-to-talk) --- +try: + import paramiko + HAS_SSH = True +except ImportError: + HAS_SSH = False + # ========================================== # NAO MIC STREAM # ========================================== @@ -131,11 +139,113 @@ class SoundReceiver: self.p.terminate() except: pass +# ========================================== +# LIVE MIC RELAY (push-to-talk over SSH) +# ========================================== +class LiveMicRelay: + """ + Push-to-talk: streams the local mic straight to NAO's own speaker. + On start(), SSHes into the robot and launches `nc -l | aplay` there, + then opens a plain TCP socket to that listener and pipes raw mic + audio into it for as long as the key is held. Closing the socket on + stop() ends the remote nc process, which ends aplay with it. + """ + def __init__(self, nao_ip, ssh_user="nao", ssh_password="nao", + port=5555, rate=16000, input_device_index=None): + self.nao_ip = nao_ip + self.ssh_user = ssh_user + self.ssh_password = ssh_password + self.port = port + self.rate = rate + self.input_device_index = input_device_index + + self.active = False + self._ssh_client = None + self._ssh_channel = None + self._sock = None + self._stream = None + self._thread = None + self._p = pyaudio.PyAudio() if HAS_AUDIO else None + + def start(self): + if self.active: + return + if not HAS_AUDIO: + print("šŸŽ™ļø Live mic needs pyaudio installed") + return + if not HAS_SSH: + print("šŸŽ™ļø Live mic needs paramiko: pip install paramiko") + return + self.active = True + self._thread = threading.Thread(target=self._run, daemon=True) + self._thread.start() + + def stop(self): + self.active = False + if self._sock: + try: self._sock.close() + except: pass + self._sock = None + if self._stream: + try: + self._stream.stop_stream() + self._stream.close() + except: pass + self._stream = None + if self._ssh_channel: + try: self._ssh_channel.close() + except: pass + self._ssh_channel = None + if self._ssh_client: + try: self._ssh_client.close() + except: pass + self._ssh_client = None + + def _run(self): + try: + self._ssh_client = paramiko.SSHClient() + self._ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + self._ssh_client.connect(self.nao_ip, username=self.ssh_user, + password=self.ssh_password, timeout=5) + + # Raw 16-bit mono PCM in, straight to ALSA out. If pitch/speed + # sounds off, the robot's ALSA default rate doesn't match + # self.rate - pass --relay-rate to match it (try 48000). + remote_cmd = ( + f"nc -l -p {self.port} | " + f"aplay -q -r {self.rate} -f S16_LE -c 1 -t raw -" + ) + print("šŸŽ™ļø Talk relay: starting remote listener...") + self._ssh_channel = self._ssh_client.get_transport().open_session() + self._ssh_channel.exec_command(remote_cmd) + time.sleep(0.4) # give nc a moment to bind before we connect + + self._sock = socket.create_connection((self.nao_ip, self.port), timeout=5) + + self._stream = self._p.open(format=pyaudio.paInt16, channels=1, + rate=self.rate, input=True, + input_device_index=self.input_device_index, + frames_per_buffer=1024) + print("šŸŽ™ļø Talk relay: live") + + while self.active: + try: + chunk = self._stream.read(1024, exception_on_overflow=False) + self._sock.sendall(chunk) + except Exception: + break + except Exception as e: + print(f"šŸŽ™ļø Talk relay error: {e}") + finally: + self.stop() + print("šŸŽ™ļø Talk relay: stopped") + # ========================================== # MAIN TELEOP # ========================================== class NaoTeleop: - def __init__(self, session, nao_ip, audio_device_index=None, mic_channel=1, mic_gain=8.0, video_scale=2.0): + def __init__(self, session, nao_ip, audio_device_index=None, mic_channel=1, mic_gain=8.0, video_scale=2.0, + nao_ssh_user="nao", nao_ssh_password="nao", relay_port=5555, relay_rate=16000): self.session = session self.nao_ip = nao_ip self.motion = session.service("ALMotion") @@ -156,6 +266,10 @@ class NaoTeleop: self.mic_channel = mic_channel self.mic_gain = mic_gain + self.mic_relay = LiveMicRelay(nao_ip, ssh_user=nao_ssh_user, ssh_password=nao_ssh_password, + port=relay_port, rate=relay_rate) + self.ptt_active = False + self.running = True self.head_yaw = 0.0 self.head_pitch = 0.0 @@ -284,7 +398,7 @@ class NaoTeleop: if self.typing_mode: return print("šŸ–ļø SIX... SEVEN...") try: - self.tts.say("Six, seven") + threading.Thread(target=self.tts.say, args=("six seven",), daemon=True).start() self.motion.setStiffnesses(["RArm", "LArm"], 1.0) @@ -387,7 +501,7 @@ class NaoTeleop: self._safe("moveInit", self.motion.moveInit) print("🦶 Walk engine ready") - print("\nšŸŽ® Controls: WASD/Arrows=Walk | [/]=Speed | IJKL=Head | SPACE=Reset head | 1=Wave | 2=Cena | 3=6-7 | M=Music search | X=Stop | -/==Volume | T=TTS | ESC=Quit") + print("\nšŸŽ® Controls: WASD/Arrows=Walk | [/]=Speed | IJKL=Head | SPACE=Reset head | 1=Wave | 2=Cena | 3=6-7 | M=Music search | X=Stop | -/==Volume | T=TTS | V=Push-to-talk (hold) | ESC=Quit") try: while self.running: @@ -487,7 +601,14 @@ class NaoTeleop: if not self.typing_mode and not self.music_search_mode: keys = pygame.key.get_pressed() - + + if keys[pygame.K_v] and not self.ptt_active: + self.ptt_active = True + self.mic_relay.start() + elif not keys[pygame.K_v] and self.ptt_active: + self.ptt_active = False + self.mic_relay.stop() + speed_changed = False if keys[pygame.K_LEFTBRACKET]: self.walk_speed = max(0.1, self.walk_speed - 0.01) @@ -592,6 +713,8 @@ class NaoTeleop: print("šŸ›‘ Shutting down...") self._safe("stopMove", self.motion.stopMove) self._safe("music stop", self.music.stop) + if getattr(self, "mic_relay", None): + self._safe("mic relay stop", self.mic_relay.stop) if self.audio_device: self._safe("audio unsubscribe", lambda: self.audio_device.unsubscribe(self.audio_service_name)) if getattr(self, "sound_receiver", None): @@ -612,6 +735,14 @@ if __name__ == "__main__": parser.add_argument("--mic-channel", choices=MIC_CHANNELS.keys(), default="left") parser.add_argument("--mic-gain", type=float, default=8.0) parser.add_argument("--video-scale", type=float, default=2.0) + parser.add_argument("--nao-ssh-user", type=str, default="nao", + help="SSH login for the live mic relay (push-to-talk)") + parser.add_argument("--nao-ssh-password", type=str, default="nao", + help="SSH password for the live mic relay (push-to-talk)") + parser.add_argument("--relay-port", type=int, default=5555, + help="TCP port used to stream mic audio to the robot's nc listener") + parser.add_argument("--relay-rate", type=int, default=16000, + help="Sample rate for the live mic relay - try 48000 if audio sounds sped up/slow") args = parser.parse_args() session = qi.Session() @@ -626,4 +757,8 @@ if __name__ == "__main__": audio_device_index=args.audio_device_index, mic_channel=MIC_CHANNELS[args.mic_channel], mic_gain=args.mic_gain, - video_scale=args.video_scale).run() + video_scale=args.video_scale, + nao_ssh_user=args.nao_ssh_user, + nao_ssh_password=args.nao_ssh_password, + relay_port=args.relay_port, + relay_rate=args.relay_rate).run()