diff --git a/mic_test.wav b/mic_test.wav new file mode 100644 index 0000000..7aecaeb Binary files /dev/null and b/mic_test.wav differ diff --git a/naomictest.py b/naomictest.py new file mode 100644 index 0000000..61ca6da --- /dev/null +++ b/naomictest.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# -*- encoding: UTF-8 -*- +""" +Standalone NAO audio diagnostic - deliberately bypasses naowalk.py entirely. + +Run this FIRST when troubleshooting "I can't hear the robot's mic". It tests +each link in the chain independently, so a failure tells you exactly where +the problem is instead of "somewhere in 300 lines of PyAudio/threading code": + + TEST 1 - Front mic hardware energy. + Uses ALAudioDevice's built-in energy meter. No PyAudio, no + network audio streaming, no laptop audio stack involved at all. + This is the lowest-level check of "does the mic itself work". + + TEST 2 - Local on-robot recording. + Records straight to a .wav file on the robot's own disk, then + you pull it off and play it normally. This bypasses the live + network stream, the SoundReceiver/queue/threading code, and + your laptop's PyAudio output path - all at once. + + TEST 3 - Robot's own speaker. + Plays a tone through the ROBOT's speaker (not your laptop), to + confirm robot-side audio output is fine, independent of any of + the mic-listening code. + +If TEST 1 shows no energy change when you make noise: mic hardware/mute/ +environment issue, upstream of any code in this project. +If TEST 1 works but TEST 2's file is silent: something is wrong with the +recording/robot-audio-stack itself, still nothing to do with naowalk.py. +If TEST 2's file has clear audio: the mic and robot are fine, and the bug +is specifically in naowalk.py's live-streaming path (network delivery, +PyAudio device selection, etc.) - go back to the diagnostics already +built into naowalk.py (device list / test beep / peak level) to isolate it +further. + +Usage: + python3 nao_audio_diagnostic.py --ip spike.local --port 9559 +""" + +import qi +import argparse +import time + +parser = argparse.ArgumentParser() +parser.add_argument("--ip", type=str, default="127.0.0.1") +parser.add_argument("--port", type=int, default=9559) +args = parser.parse_args() + +session = qi.Session() +session.connect(f"tcp://{args.ip}:{args.port}") +print(f"✅ Connected to {args.ip}") +audio = session.service("ALAudioDevice") + +# --- TEST 1: mic hardware energy, all four channels ------------------------ +print("\n=== TEST 1: Mic hardware energy (all 4 mics) ===") +print("Make noise near the robot's head (clap, talk right up close to it) for 10s...") +audio.enableEnergyComputation() +front_readings, rear_readings, left_readings, right_readings = [], [], [], [] +for i in range(10): + f, r, l, rt = (audio.getFrontMicEnergy(), audio.getRearMicEnergy(), + audio.getLeftMicEnergy(), audio.getRightMicEnergy()) + front_readings.append(f); rear_readings.append(r); left_readings.append(l); right_readings.append(rt) + print(f" t={i}s front={f:.2f} rear={r:.2f} left={l:.2f} right={rt:.2f}") + time.sleep(1) +audio.disableEnergyComputation() + +results = {"front": front_readings, "rear": rear_readings, "left": left_readings, "right": right_readings} +flat = [name for name, vals in results.items() if max(vals) - min(vals) < 2] +if len(flat) == 4: + print("⚠️ ALL FOUR mics are flat. This points to something systemic - e.g. audio " + "inputs closed (closeAudioInputs() called somewhere and never reopened), " + "or a low-level hardware/driver fault - rather than one damaged mic.") +elif flat: + print(f"⚠️ Flat mic(s): {', '.join(flat)}. The others responded normally, which " + f"points to a fault specific to {', '.join(flat)} rather than the audio " + f"pipeline as a whole (that pipeline is clearly working for the others).") +else: + print("✅ All four mics responded to noise - hardware capture is fine across the board.") + +# --- TEST 2: local on-robot recording --------------------------------------- +print("\n=== TEST 2: Local on-robot recording ===") +remote_path = "/home/nao/mic_test.wav" +print(f"Recording 5s directly to {remote_path} on the robot itself " + f"(this bypasses the network stream and your laptop's audio stack entirely)...") +audio.startMicrophonesRecording(remote_path) +time.sleep(5) +audio.stopMicrophonesRecording() +print(f"Done. Pull it off and play it with:") +print(f" scp nao@{args.ip}:{remote_path} .") +print(f" (then open mic_test.wav in any media player - it's 4-channel, so check all channels)") +print("If that file has clearly audible sound, the mic and robot are fine and the") +print("bug is specifically in naowalk.py's live-streaming path.") + +# --- TEST 3: robot's own speaker, via TTS (version-agnostic) --------------- +print("\n=== TEST 3: Robot's own speaker ===") +input("Press Enter to have the robot say a test phrase out loud...") +session.service("ALTextToSpeech").say("Testing 1 2 3, can you hear me.") +print("If you heard the robot speak just now, its speaker output is fine " + "(this is unrelated to hearing its mic on your laptop, but rules out a robot-wide audio fault).") + +print("\nDone. Report back which test(s) failed and we'll know exactly where to look.") diff --git a/naowalk.py b/naowalk.py index 0c7ffff..f0f1518 100644 --- a/naowalk.py +++ b/naowalk.py @@ -178,7 +178,7 @@ class SoundReceiver: # MAIN TELEOP CLASS # ========================================== class NaoTeleop: - def __init__(self, session, audio_device_index=None): + def __init__(self, session, audio_device_index=None, mic_channel=1): self.session = session self.motion = session.service("ALMotion") self.posture = session.service("ALRobotPosture") @@ -196,6 +196,7 @@ class NaoTeleop: self.audio_device = None self.audio_service_name = "SoundReceiver" self.audio_device_index = audio_device_index # which laptop output device to play the mic feed on + self.mic_channel = mic_channel # which NAO mic to stream: LEFTCHANNEL(1)/RIGHTCHANNEL(2)/FRONTCHANNEL(3)/REARCHANNEL(4) self.running = True self.head_yaw = 0.0 @@ -261,10 +262,13 @@ class NaoTeleop: # 2. Add a tiny delay to let the network handshake complete time.sleep(0.5) - # 3. Use the IP of the machine running the code - # We explicitly tell the robot the IP of your laptop (replace with your laptop IP if still failing) - # '0' tells it to auto-discover, but sometimes we need to be explicit. - self.audio_device.setClientPreferences(self.audio_service_name, 16000, 3, 0) + # 3. Which physical mic to stream. FRONTCHANNEL(3) was the + # original default, but on this robot the front and rear mics + # test dead (flat energy readings even up close) while left + # and right respond normally - see nao_audio_diagnostic.py. + # Defaulting to LEFTCHANNEL(1) accordingly; override with + # --mic-channel if you want to compare. + self.audio_device.setClientPreferences(self.audio_service_name, 16000, self.mic_channel, 0) self.audio_device.subscribe(self.audio_service_name) print("✅ Live Audio Stream ready") @@ -479,6 +483,8 @@ class NaoTeleop: pygame.quit() if __name__ == "__main__": + MIC_CHANNELS = {"left": 1, "right": 2, "front": 3, "rear": 4} + parser = argparse.ArgumentParser() parser.add_argument("--ip", type=str, default="127.0.0.1") parser.add_argument("--port", type=int, default=9559) @@ -486,6 +492,10 @@ if __name__ == "__main__": help="Force a specific PyAudio output device index for the robot's " "mic feed (see the '🔈 Available output devices' list printed " "at startup) if the system default isn't your speakers.") + parser.add_argument("--mic-channel", choices=MIC_CHANNELS.keys(), default="left", + help="Which NAO mic to stream. Defaults to 'left' since " + "nao_audio_diagnostic.py confirmed front/rear are dead on " + "this unit while left/right respond normally.") args = parser.parse_args() session = qi.Session() @@ -496,4 +506,5 @@ if __name__ == "__main__": print(f"❌ Connection failed: {e}") sys.exit(1) - NaoTeleop(session, audio_device_index=args.audio_device_index).run() + NaoTeleop(session, audio_device_index=args.audio_device_index, + mic_channel=MIC_CHANNELS[args.mic_channel]).run()