Add multi-speaker tracking with beam steering (#5)
multi_speaker.py: Tracks up to 2 speakers simultaneously. When 2 distinct DoA angles are detected (30°+ apart) for >1s, locks the XVF3800's fixed beams onto each speaker. Releases back to auto mode when only 1 speaker remains (3s timeout). Manages beam gating so only the speaking beam is active. xvf3800.py: Added beam steering commands — enable_fixed_beams(), set_beam_azimuths(), enable_beam_gating(), read_all_beams(). Manager gets steer_beams() and release_beams() convenience methods. headmic.py: Wire multi-speaker tracker into DoA loop. New endpoint: GET /speakers/tracked — current speaker positions, beam mode, lock state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
56
xvf3800.py
56
xvf3800.py
@@ -40,8 +40,15 @@ LED_EFFECT_CMD = 12 # 0=off, 1=breath, 2=rainbow, 3=solid, 4=doa, 5=ring
|
||||
AEC_RESID = 33
|
||||
AEC_AZIMUTH_CMD = 75 # 4 floats: beam1, beam2, free-running, auto-select (radians)
|
||||
AEC_SPENERGY_CMD = 80 # 4 floats: speech energy per beam (>0 = speech)
|
||||
# Beam steering (resid=33)
|
||||
AEC_FIXED_BEAMS_ON_CMD = 37 # rw uint8: 0=off, 1=on — enable fixed beam mode
|
||||
AEC_FIXED_BEAMS_AZ_CMD = 81 # rw 2 floats (radians): beam 1 azimuth, beam 2 azimuth
|
||||
AEC_FIXED_BEAMS_ELEV_CMD = 82 # rw 2 floats (radians): beam 1 elevation, beam 2 elevation
|
||||
AEC_FIXED_BEAMS_GATING_CMD = 83 # rw uint8: 0=off, 1=on — silence beams without speech
|
||||
|
||||
AUDIO_MGR_RESID = 35
|
||||
AUDIO_MGR_SELECTED_AZ_CMD = 11 # 2 floats: processed DoA, auto-select DoA (radians)
|
||||
AUDIO_MGR_SELECTED_CH_CMD = 12 # rw 2 uint8: which beam goes to L/R output channel
|
||||
LED_BRIGHTNESS_CMD = 14
|
||||
LED_COLOR_CMD = 16 # single uint32 color (confirmed: xvf_host LED_COLOR cmdid=16)
|
||||
LED_DOA_COLOR_CMD = 17 # two uint32 values: base + indicator
|
||||
@@ -129,6 +136,41 @@ class XVF3800:
|
||||
|
||||
return int(angle_deg) % 360, vad
|
||||
|
||||
def read_all_beams(self) -> dict:
|
||||
"""Read all 4 beam azimuths: beam1, beam2, free-running, auto-select."""
|
||||
import math
|
||||
data = self._read_float(AEC_RESID, AEC_AZIMUTH_CMD, 4)
|
||||
if len(data) < 17: # 1 status + 4*4 bytes
|
||||
return {}
|
||||
beams = struct.unpack_from("<ffff", data, 1)
|
||||
return {
|
||||
"beam1_deg": round(math.degrees(beams[0]) % 360, 1),
|
||||
"beam2_deg": round(math.degrees(beams[1]) % 360, 1),
|
||||
"free_running_deg": round(math.degrees(beams[2]) % 360, 1),
|
||||
"auto_select_deg": round(math.degrees(beams[3]) % 360, 1),
|
||||
}
|
||||
|
||||
# --- Beam steering ---
|
||||
|
||||
def enable_fixed_beams(self, on: bool = True):
|
||||
"""Enable/disable fixed beam mode. When on, beams lock to set azimuths."""
|
||||
self._write(AEC_RESID, AEC_FIXED_BEAMS_ON_CMD, bytes([1 if on else 0]))
|
||||
|
||||
def set_beam_azimuths(self, beam1_deg: float, beam2_deg: float):
|
||||
"""Set fixed beam directions in degrees (0=front, 90=right, 180=back, 270=left)."""
|
||||
import math
|
||||
b1_rad = math.radians(beam1_deg)
|
||||
b2_rad = math.radians(beam2_deg)
|
||||
self._write(AEC_RESID, AEC_FIXED_BEAMS_AZ_CMD, struct.pack("<ff", b1_rad, b2_rad))
|
||||
|
||||
def enable_beam_gating(self, on: bool = True):
|
||||
"""Enable/disable beam gating. When on, only the beam with speech is active."""
|
||||
self._write(AEC_RESID, AEC_FIXED_BEAMS_GATING_CMD, bytes([1 if on else 0]))
|
||||
|
||||
def disable_fixed_beams(self):
|
||||
"""Return to auto beam mode."""
|
||||
self.enable_fixed_beams(False)
|
||||
|
||||
# --- LEDs ---
|
||||
|
||||
def led_off(self):
|
||||
@@ -280,6 +322,20 @@ class XVF3800Manager:
|
||||
if dev:
|
||||
dev.led_doa()
|
||||
|
||||
def steer_beams(self, beam1_deg: float, beam2_deg: float):
|
||||
"""Steer both arrays' fixed beams to the same directions."""
|
||||
for dev in [self.left, self.right]:
|
||||
if dev:
|
||||
dev.enable_fixed_beams(True)
|
||||
dev.set_beam_azimuths(beam1_deg, beam2_deg)
|
||||
dev.enable_beam_gating(True)
|
||||
|
||||
def release_beams(self):
|
||||
"""Return both arrays to auto beam mode."""
|
||||
for dev in [self.left, self.right]:
|
||||
if dev:
|
||||
dev.disable_fixed_beams()
|
||||
|
||||
def read_both_doa(self) -> dict:
|
||||
"""Read DoA from both arrays."""
|
||||
result = {}
|
||||
|
||||
Reference in New Issue
Block a user