diag_ws_record.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. from __future__ import annotations
  2. import argparse
  3. import json
  4. import time
  5. import wave
  6. from pathlib import Path
  7. import numpy as np
  8. import socketio
  9. def wav_stats(path: Path) -> dict[str, float | int | str]:
  10. with wave.open(str(path), "rb") as wf:
  11. channels = wf.getnchannels()
  12. sample_rate = wf.getframerate()
  13. frames = wf.getnframes()
  14. raw = wf.readframes(frames)
  15. if not raw:
  16. return {
  17. "file": path.name,
  18. "channels": channels,
  19. "sample_rate": sample_rate,
  20. "frames": frames,
  21. "duration_sec": 0.0,
  22. "rms": 0.0,
  23. "peak": 0.0,
  24. }
  25. pcm = np.frombuffer(raw, dtype=np.int16)
  26. if channels > 1:
  27. pcm = pcm.reshape(-1, channels)[:, 0]
  28. norm = pcm.astype(np.float32) / 32768.0
  29. rms = float(np.sqrt(np.mean(norm * norm, dtype=np.float64)))
  30. peak = float(np.max(np.abs(norm)))
  31. duration = float(frames / sample_rate) if sample_rate > 0 else 0.0
  32. return {
  33. "file": path.name,
  34. "channels": channels,
  35. "sample_rate": sample_rate,
  36. "frames": frames,
  37. "duration_sec": duration,
  38. "rms": rms,
  39. "peak": peak,
  40. }
  41. def main() -> int:
  42. parser = argparse.ArgumentParser()
  43. parser.add_argument("--url", default="http://127.0.0.1:5000")
  44. parser.add_argument("--duration", type=float, default=10.0)
  45. parser.add_argument("--source", default="compare_all")
  46. parser.add_argument("--agc", action="store_true")
  47. parser.add_argument("--hifi", action="store_true")
  48. parser.add_argument("--hifi-mic", default="mic1", choices=["mic1", "mic2"])
  49. parser.add_argument("--sample-rate", type=int, default=16000)
  50. parser.add_argument("--timeout", type=float, default=60.0)
  51. parser.add_argument("--recordings-dir", default="/home/pch/mic_system/recordings")
  52. args = parser.parse_args()
  53. sio = socketio.Client(reconnection=False, logger=False, engineio_logger=False)
  54. state: dict[str, object] = {
  55. "record_started": False,
  56. "recording_now": False,
  57. "filenames": [],
  58. "last_rec_duration": 0.0,
  59. "max_rec_duration": 0.0,
  60. "first_rec_wall": None,
  61. "last_rec_wall": None,
  62. "events": [],
  63. }
  64. @sio.on("audio_data")
  65. def on_audio_data(payload: dict) -> None: # type: ignore[no-redef]
  66. now = time.monotonic()
  67. recording = bool(payload.get("recording", False))
  68. rec_duration = float(payload.get("rec_duration", 0.0))
  69. state["recording_now"] = recording
  70. state["last_rec_duration"] = rec_duration
  71. if rec_duration > float(state["max_rec_duration"]):
  72. state["max_rec_duration"] = rec_duration
  73. if recording:
  74. if state["first_rec_wall"] is None:
  75. state["first_rec_wall"] = now
  76. state["last_rec_wall"] = now
  77. @sio.on("server_ack")
  78. def on_server_ack(payload: dict) -> None: # type: ignore[no-redef]
  79. events = state["events"]
  80. assert isinstance(events, list)
  81. events.append(payload)
  82. if payload.get("type") == "record_started":
  83. state["record_started"] = True
  84. state["filenames"] = list(payload.get("filenames", []))
  85. if payload.get("type") == "record_stopped":
  86. state["recording_now"] = False
  87. sio.connect(args.url)
  88. sio.emit(
  89. "client_message",
  90. {
  91. "type": "settings",
  92. "mode": "beamforming",
  93. "auto_beam": True,
  94. "sample_rate": int(args.sample_rate),
  95. "gain_db": 0.0,
  96. "agc": bool(args.agc),
  97. "attack_ms": 5.0,
  98. "release_ms": 300.0,
  99. "hifi_mode": bool(args.hifi),
  100. "hifi_mic": str(args.hifi_mic),
  101. "monitor_on": False,
  102. },
  103. )
  104. time.sleep(0.4)
  105. wall_start = time.monotonic()
  106. sio.emit(
  107. "client_message",
  108. {
  109. "type": "record_start",
  110. "source": args.source,
  111. "duration_sec": float(args.duration),
  112. },
  113. )
  114. deadline = wall_start + float(args.timeout)
  115. saw_recording = False
  116. quiet_ticks = 0
  117. while time.monotonic() < deadline:
  118. time.sleep(0.2)
  119. recording_now = bool(state["recording_now"])
  120. if recording_now:
  121. saw_recording = True
  122. quiet_ticks = 0
  123. continue
  124. if saw_recording:
  125. quiet_ticks += 1
  126. if quiet_ticks >= 8:
  127. break
  128. if bool(state["recording_now"]):
  129. sio.emit("client_message", {"type": "record_stop"})
  130. time.sleep(0.5)
  131. wall_end = time.monotonic()
  132. sio.disconnect()
  133. filenames = [str(x) for x in state["filenames"] if isinstance(x, str) and x]
  134. rec_dir = Path(args.recordings_dir)
  135. stats = []
  136. for name in filenames:
  137. path = rec_dir / name
  138. wait_deadline = time.monotonic() + 5.0
  139. while (not path.exists()) and time.monotonic() < wait_deadline:
  140. time.sleep(0.1)
  141. if path.exists():
  142. stats.append(wav_stats(path))
  143. else:
  144. stats.append({"file": name, "error": "missing"})
  145. first_rec_wall = state["first_rec_wall"]
  146. last_rec_wall = state["last_rec_wall"]
  147. rec_wall = 0.0
  148. if isinstance(first_rec_wall, float) and isinstance(last_rec_wall, float) and last_rec_wall >= first_rec_wall:
  149. rec_wall = last_rec_wall - first_rec_wall
  150. out = {
  151. "agc": bool(args.agc),
  152. "hifi": bool(args.hifi),
  153. "hifi_mic": str(args.hifi_mic),
  154. "source": args.source,
  155. "target_duration_sec": float(args.duration),
  156. "wall_elapsed_sec": wall_end - wall_start,
  157. "recording_wall_sec": rec_wall,
  158. "reported_rec_duration_sec": float(state["last_rec_duration"]),
  159. "reported_max_rec_duration_sec": float(state["max_rec_duration"]),
  160. "record_started": bool(state["record_started"]),
  161. "recordings": stats,
  162. }
  163. print(json.dumps(out, ensure_ascii=True))
  164. return 0
  165. if __name__ == "__main__":
  166. raise SystemExit(main())