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>
The auto-select beam always returns an angle (even for noise), so
VAD was always true. The processed_doa (index 0) is NaN when no
speech is present and a real angle when speech is detected.
Now: angle from auto-select beam, VAD from processed_doa being non-NaN.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DOA_VALUE on GPO resource was sluggish/cached. The beamformer-level
AUDIO_MGR_SELECTED_AZIMUTHS on resource 35 tracks the active speaker
in real time. Falls back to simple DOA_VALUE when both azimuths are NaN.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Response format is [status_byte, angle_lo, angle_hi, vad_lo, vad_hi],
not [angle_lo, angle_hi, vad_lo, vad_hi]. Was reading the status byte
(0x42=66) as the angle, which is why DoA was always stuck at 66.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>