Files
2026-09-29 22:41:29 +08:00

212 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ESP32 八麦克风 DOA 监视器
本文件夹的程序运行在PC端上位机,是纯python程序 windows/Linux/MaxOS均可运行
从 USB 音频设备采集八路同步音频,使用 Pyroomacoustics 的SRP-PHAT 或 NormMUSIC
算法估计单个远场声源的水平方位,并根据估计方向引导频域 MVDR 波束形成器。
界面会同时显示声源方位、各通道电平和波束形成后的输出电平。
## 阵列与角度约定
- 圆形阵列半径为 56 mm。
- USB 通道必须按照 CH1 至 CH8 的顺序排列。
- 各通道的数学方位角通过 `src/doa_estimator.py` 中的
`MIC_AZIMUTHS_DEG` 配置。
- 界面中的 0° 指向右侧(`+x`),角度沿逆时针方向增大。
- 当前通道布局中 CH1 位于 157.5°,其余通道沿逆时针方向以 45° 间隔均匀分布。
如果实际 USB 通道顺序或阵列参考方向不同,请修改 `MIC_AZIMUTHS_DEG`。
即使所有通道完全同步,错误的通道顺序仍可能造成方位镜像或其他定位错误。
## Pyroomacoustics 源码
`pyroomacoustics/` 是独立的上游仓库,不纳入本项目的 Git 管理。首次检出本项目后,
请从官方仓库克隆(当前项目已验证的上游提交为 `ff7d61f`):
```powershell
git clone https://github.com/LCAV/pyroomacoustics.git pyroomacoustics
git -C pyroomacoustics checkout ff7d61f219e4eb41489963c4bb5f57bea5bc2c69
```
项目要求上游源码位于以下路径:
```text
./pyroomacoustics/pyroomacoustics/doa/srp.py
./pyroomacoustics/pyroomacoustics/doa/music.py
./pyroomacoustics/pyroomacoustics/doa/normmusic.py
```
程序只加载上游纯 Python 实现的 SRP-PHAT 和 NormMUSIC 算法。NormMUSIC
会先对各频率的 MUSIC 伪谱进行归一化,再沿频率轴融合,相比普通 MUSIC 对宽带
信号更加稳健。实时 DOA 估计不需要房间仿真使用的原生 `libroom` 扩展。
## 运行程序
安装或同步依赖,然后列出可用的音频输入设备:
```powershell
uv sync
uv run python src/main.py --list-devices
```
自动选择 USB 音频设备并启动:
```powershell
uv run python src/main.py
```
启动时终端会显示本次实际打开的设备编号、设备名称、驱动接口(Host API)、
使用/可用输入通道数、实际采样率和设备默认采样率。如果无法打开兼容的八通道
输入设备,终端会显示进入模拟预览模式的具体原因。
默认不再强制使用 48 kHz。程序会优先选择能够更准确报告端点格式的 Windows
WASAPI/WDM-KS 设备,读取其 `default_samplerate`,再以音频流实际报告的采样率
统一配置采集、SRP-PHAT、NormMUSIC 和 MVDR。顶部的 `DEVICE RATE` 显示本次音频
流实际采用的采样率。`--list-devices` 的输出同时包含每个端点的默认采样率和
Host API,便于区分同一硬件的 MME、DirectSound、WASAPI 和 WDM-KS 条目。
也可以使用 PortAudio 设备序号或设备名称中不区分大小写的部分文本来选择设备:
```powershell
uv run python src/main.py --device 3
uv run python src/main.py --device "USB Audio"
```
设备编号可能因重启、重新插拔或增加其他音频设备而改变。运行前可随时重新查询
当前可用的设备编号:
```powershell
uv run python src/main.py --list-devices
```
如需覆盖自动检测结果,可以显式指定采样率:
```powershell
uv run python src/main.py --sample-rate 24000
```
PortAudio 能查询的是操作系统音频端点的默认采样率和音频流协商后的实际采样率,
无法对所有驱动直接读取 ADC 内部时钟。Windows 上的旧式 MME/DirectSound
端点可能报告经过共享混音器转换后的 44.1/48 kHz,因此自动选择会优先使用
WASAPI,其次使用 WDM-KS。显式使用数字设备序号时,程序会严格使用指定端点。
估计器使用 500 Hz 至 4 kHz 的频段和 1° 间隔的方位搜索网格。STFT 参数会
根据设备采样率自动缩放:48 kHz 使用 `nfft=512、hop=256、window=8192`,
24 kHz 使用 `nfft=256、hop=128、window=4096`。两套配置的频率间隔均为
93.75 Hz,STFT 帧长均约为 10.67 ms,帧重叠率均为 50%,滚动窗口时长均约
为 171 ms。因此 24 kHz 模式在保持原有时频分辨率和处理延迟的同时,减少了
FFT 与后续矩阵计算量。
当音频低于 -58 dBFS 时,DOA 区域显示 `LOW SIGNAL`,不再用噪声刷新方位。
如果此前已经获得过有效 DOA,MVDR 会冻结并继续使用最后一次有效导向角,同时
使用当前音频窗口更新协方差和流式输出,监听不会因低信号而断流;MVDR 卡片显示
`HOLD` 和实际保持的 `STEER` 角度。程序启动后尚未得到过有效方向时,MVDR 才会
等待首次定位。切换 SRP-PHAT/NormMUSIC 会清除保持角并重新定位。
上述参数均可在 `DoaConfig` 中配置,其中 MVDR 对角加载系数由
`mvdr_diagonal_loading` 设置。程序会检查 4 kHz 分析上限必须低于当前采样率
的奈奎斯特频率。
使用右侧面板中的 `SRP-PHAT / NormMUSIC` 分段开关选择 DOA 算法,也可以按
`M` 键切换。切换算法不会中断当前 USB 音频采集流,但会重新开始方位估计。
如果没有兼容的八通道输入设备,界面会进入带有明确标识的模拟预览模式。
## DOA 引导的 MVDR 波束形成
每个有效的 DOA 结果都会用于引导一个八输入、单输出的频域 MVDR 波束形成器。
在 500 Hz 至 4 kHz 的分析频段内,算法根据当前滚动窗口估计空间协方差矩阵,
执行对角加载,并在检测到的声源方向上施加单位增益的无失真约束。该频段之外
使用更稳定的延迟求和权重。MVDR 与 SRP-PHAT/NormMUSIC 共用麦克风坐标、声速、
STFT 参数和角度约定。每个处理周期只执行一次多通道去直流、加窗和 FFT;DOA
与 MVDR 直接复用同一份 `snapshots × frequencies × channels` 频谱,不再重复
计算 STFT。
界面采用三栏布局:左侧为阵列方向图,中间为 DOA 结果、算法选择和八通道电平,
最右侧为独立的 MVDR 专用栏。该栏顶部的 `MVDR OUTPUT` 卡片在有最新结果时显示
`LIVE` 状态、单通道输出电平、输入电平、引导角度和输出电平条。
`LiveAudioSource.read_beamformed_audio()` 可返回最新单通道窗口的结果副本:
24 kHz 时为 4096 点,48 kHz 时为 8192 点。该数据是滚动窗口快照,相邻结果
之间存在重叠,因此不能直接连续拼接成录音。
卡片中的 `MVDR IN` 不代表某一个麦克风通道,而是当前窗口内八个通道的整体
RMS 输入电平。程序先对所有通道、所有采样点的平方值求平均,再开平方得到
整体 RMS,最后转换为 dBFS:
```text
MVDR IN = 20 × log10(sqrt(sum(x[m,n]²) / (8 × N)))
```
其中 `m` 为通道编号,`n` 为采样点编号,`N` 为每个通道的采样点数。因此它
等价于先对八个通道的线性功率求平均后再转换为 dBFS,并不是八个通道 dBFS
读数的算术平均。若八个通道的电平接近,该数值会接近任意单通道的 RMS 电平。
MVDR 专用栏还提供 `MANUAL STEERING` 角度输入框和开关。输入采用与 DOA 相同的
角度约定:0° 指向右侧,逆时针为正;提交时会归一化到 0°~359.9°。开关显示
`MANUAL ON` 时,MVDR 使用该角度而不改变 DOA 算法的检测结果,即使当前为低信号
也会继续按手动方向处理;输出卡片状态显示 `MANUAL`。阵列方向图使用浅蓝色箭头
显示手动方向。切回 `AUTO DOA` 后,浅蓝色箭头消失,MVDR 恢复使用最新有效 DOA。
## MVDR 实时监听
最右侧 MVDR 专用栏按照 `MANUAL STEERING`、`MVDR OUTPUT`、`MONITOR`、
`OUTPUT DEVICE`、`GAIN` 的顺序纵向排列引导设置、结果与监听控件:
- `MONITOR` 开关用于启动或停止监听,键盘 `L` 键也可以切换。
- `OUTPUT DEVICE` 下拉列表显示支持当前采样率、单通道 Float32 输出的设备,
设备名称后同时标注 Host API。
- `GAIN` 滑动条范围为 -10 dB~+40 dB,默认值为 0 dB。
- 增益按照 `10^(dB/20)` 应用于 MVDR 单通道输出,超过 `[-1, 1]` 的采样会被
限幅,此时监听状态显示 `CLIPPING`。
监听默认关闭,以免扬声器输出重新进入麦克风阵列并产生强声反馈。启用前应优先
选择耳机,或确保监听扬声器与麦克风阵列之间具有足够的声学隔离。
MVDR 完整滚动窗口的左右边缘缺少相邻 STFT 帧的重叠贡献,不能直接用于连续
播放。监听路径使用有状态的流式 WOLA:只处理全局采样序号中尚未合成的新频谱
帧,保存当前帧的后半个 Hann 窗,等下一帧到达后与其前半窗重叠相加,再按窗
平方和归一化。只有获得完整前后帧贡献的 `hop` 才会送往输出设备,因此不会播放
有限窗口中归一化条件很差的末端。24 kHz 配置会增加一个 128 点、约 5.33 ms
的合成等待时间。
空间协方差矩阵同时采用系数 0.70 的跨窗口指数平滑,相邻批次使用不同 MVDR
权重时,Hann 重叠也会对时域输出自然交叉渐变。已完成的 WOLA 音频块进入最多
八项的有界播放队列;监听启动时先等待第一个完整块,再打开 PortAudio 输出,
避免初始欠载。播放使用独立的阻塞输出线程,不占用 Pygame 界面线程和 USB
输入线程。界面中的 `U` 表示输出欠载次数,`D` 表示队列溢出或源采样序号不连续
的次数。
录音模块或网络发送模块仍可单独使用上述快照接口。
Pyroomacoustics 中考虑房间响应的 rake-MVDR 实现需要已知房间脉冲响应以及
声源和干扰源的位置。实时 DOA 链路无法提供这些输入,因此本项目直接根据实测
协方差矩阵实现等效的宽带 MVDR 无失真约束。
## 音频采集架构
USB 输入在独立的普通优先级线程中使用 PortAudio 阻塞读取模式。音频流设置为
`blocksize=0`,由主机音频 API 选择稳定的原生块大小,采集线程每次请求读取
1024 帧。RMS 计算和 DOA 数据提交均不会在 PortAudio 实时回调中执行。
DOA 输入保存在预分配的定长八通道环形缓冲区中,24 kHz 时为 4096×8,
48 kHz 时为 8192×8。新音频会覆盖最旧数据,因此即使估计过程延迟,也不会
造成队列或处理延迟持续增长。声源方向面板中的 `OVF n` 表示 PortAudio 检测到
的输入溢出次数。`AUDIO STALE` 或 `AUDIO ERROR` 表示采集线程已经停止提供
新的音频块。
## 测试
```powershell
uv run pytest -q
```
测试会在多个方位合成远场平面波,验证从 STFT 到 Pyroomacoustics 的两条完整
算法链路,包括 0°/360° 边界、运行时算法切换、有界环形缓冲区和 USB 阻塞采集。
测试还会验证 MVDR 对目标方向的单位响应、对轴外干扰的抑制能力,以及后台线程
能否正确发布波束形成音频,并检查 DOA 与 MVDR 在每个处理周期只执行一次共享
STFT。监听测试还会检查有状态 WOLA 输出、输出设备参数、增益换算和削波保护。
连续合成测试会额外验证 WOLA 块的全局采样区间首尾相接,并确保有限窗口边缘
尖峰不会进入监听音频。
## 实际麦克风增益校准
'calibration/' 中是麦克风增益一致性校准使用的脚本,具体极少查看 'calibration/README.md'