一、项目介绍
很多动物都有两只耳朵,可以通过两个耳朵听到的声音差来判断声源的方向。本项目就是模仿生物的这个特性来实现声源定位的功能。使用两颗麦克风代替动物的耳朵,使用ESP32S3代替大脑。当MCU收集到两颗麦克风的声音信号时,通过声音信号的时间差ITD和声级差ILD,判断出声音的来源方向。
本项目是实现任务列表中的方向五:教育与创意互动。实现了方向五中的自命题,用来展示模拟双耳声音定位功能。
二、硬件介绍
1、主控使用了Seeed Studio XIAO ESP32-S3 Sense。核心处理器为乐鑫ESP32-S3双核Xtensa处理器,主频240MHz,集成Wi-Fi 4和蓝牙5.0,提供可靠的无线连接能力,并内置8MB PSRAM,适合处理图像、语音等复杂数据。
2、扩展板XIAO-Expansion-Board。这个扩展板是专为XIAO系列核心板设计的。板上有OLED等外设。在本项目中利用扩展板连接麦克风,使用OLED展示声源角度信息。

3、麦克风。 这个麦克风扩展板是由一个智能插座上拆下来的。板子上有两颗PCM麦克风,ESP32-S3内部集成了PDM的解码器,可以直接从I2S数据线上接收PDM流,所以可以正常使用这个麦克风的板子,并且接线更加简单,仅仅需要时钟线和数据线两条信号线即可。
4、硬件连接:
类型 | ESP32-S3管脚 | 说明 | |
麦克风 | I2S | D7(44) | CLK |
麦克风 | I2S | D6(43) | DATA |
OLED | I2C | D5(6) | SCL |
OLED | I2C | D4(5) | SDA |
三、系统设计
1、基于时延差的声源定位法原理。对麦克风阵列进行建模之前,先要选择使用近场模型还是远场模型。两者区别在于,离麦克风近则符合近场模型 ,离得远则符合远场模型。公式 d=2L2/λ 作为临界值进行判断。其中:L是两个麦克风的距离,λ是声音波长。
本项目中麦克风之间距离为5.4cm,日常听到的声音都处于100 Hz ~ 3000 Hz之间,音源基本都超过10cm以上,所以选用远场模型。即将音源到两个麦克风之间视作平行线。

因为两个麦克风与音源距离有区别,所以两个麦克风收到的声音就有时间上的区别。将这个时间时延视作τ ,则有:
τ=d*cosθ/c (c:声音在空气中传播速度)。 通过反三角函数:θ=arccos(cτ/d) 即可求出声源的角度。
获得时延即可计算出声源角度,那么如何获得时延呢?这里使用最经典、最常用的方法,计算两个信号x1和x2在不同时延下τ下的相似度,峰值对应的τ就是对应的时延。
a、使用傅里叶(FFT)进行频域变换。将声音的时域信号转换为频域信号。
b、计算互功率谱,获取频域相位差。
c、广义互相关 (GCC) 与 逆变换 (IFFT)。将互功率谱的结果转换回时域,得到互相关函数。互相关函数的峰值位置即为两个信号的时间延迟。

2、按以上原理进行项目开发。开发工具使用Vscode+esp-idf5.5,使用了espressif/esp-dsp开源库(这是一个乐鑫推出的高性能数字信号处理库)。声音信号使用16000采样率、16bit的深度。两个麦克风之间的距离是0.054米,声音速度使用固定值340米/秒。

3、代码编写。麦克风使用I2S进行连接,需要留意,要使用立体声进行初始化。
void init_microphone(void)
{
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, NULL, &rx_handle));
i2s_pdm_rx_config_t pdm_rx_cfg = {
.clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
/* The default mono slot is the left slot (whose 'select pin' of the PDM microphone is pulled down) */
// .slot_cfg = I2S_PDM_RX_SLOT_PCM_FMT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
.slot_cfg = I2S_PDM_RX_SLOT_PCM_FMT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = {
.clk = PDM_CLK_PIN,
.din = PDM_DATA_PIN,
.invert_flags = {
.clk_inv = false,
},
},
};
ESP_ERROR_CHECK(i2s_channel_init_pdm_rx_mode(rx_handle, &pdm_rx_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
}
一个进程负责循环读取麦克风数据。从麦克风读取到的数据是左右声道交替存储的16位的PCM数据,先将左右声道数据进行分离,然后交给傅里叶变换器转换为频域信号。
while (1)
{
/* Read i2s data */
if (i2s_channel_read(rx_handle, (char *)i2s_readraw_buff, sizeof(i2s_readraw_buff), NULL, 1000) == ESP_OK)
{
// 计算总声音强度 RMS
current_volume_db = calculateVolumeRMS(i2s_readraw_buff, I2S_BUFF_SIZE);
for (int i = 0; i < I2S_BUFF_SIZE; i += 2)
{
x1[i] = i2s_readraw_buff[i];
x1[i + 1] = 0.0;
x2[i] = i2s_readraw_buff[i + 1];
x2[i + 1] = 0.0;
}
vol_angle = angleEstimation(x1, x2, INPUT_ARRAY_SIZE, SAMPLE_RATE);
if (vol_angle >= 0 && vol_angle <= 180 && current_volume_db > -25)
{
angle = (int)vol_angle;
printf("angle: %d vol_db=%.2f\n", angle, current_volume_db);
}
}
else
{
printf("Read Task: i2s read failed\n");
}
vTaskDelay(pdMS_TO_TICKS(10));
}
这里添加了个统计声音强度的计算。
float calculateVolumeRMS(int16_t *buffer, int size)
{
// 第一步:计算直流偏置(平均值)
float sum = 0.0f;
for (int i = 0; i < size; i++)
{
sum += buffer[i];
}
float dc_offset = sum / size;
// 第二步:去除 DC 偏置后计算 RMS
float ac_sum = 0.0f;
for (int i = 0; i < size; i++)
{
float sample = buffer[i] - dc_offset;
ac_sum += sample * sample;
}
float rms = sqrtf(ac_sum / size);
// 第三步:转换为分贝(使用更合适的参考值)
if (rms < 1.0f)
return -100.0f;
return 20.0f * log10f(rms / 1000.0f); // 参考值改为 1000,可根据实际调整
}
因为麦克风是时时刻刻都在监听环境的声音信号。当环境比较安静时,依然会有信号传入,此时计算出的角度信息就不可用。所以统计一下环境声音强度信息,只有当声音强度大于阈值时才更新系统的角度信息。这里计算后的声音强度单位是“”dBFS/Pa"。这里选用-25做为阈值,当声音大于-25时,才会重新绘制OLED屏幕。
场景 | 代码输出估算值(dB) |
安静环境 | 从-50至-40 |
普通办公室环境 | 从-40至-30 |
正常谈话环境 | 从-25至-15 |
大声说话争吵 | 从-15至-5 |
将左右声道的时域信号转换为频域信号后,接下来计算互功率谱、广义互相关的计算和寻找峰值与时延的计算都是使用经典开源的算法实现的。
float angleEstimation(float *x1, float *x2, int N, float fs)
{
// 计算互功率谱
computeFFTandCrossPowerSpectrum(x1, x2, N);
// 计算权重并应用
applyWeightAndIFFT(x2, x1, N); // 这个使用临时数组更快,x1内容会被覆盖
// 计算时延
float delay = FindDelay(x2, NULL, N, fs); // 这个不使用临时数组更快, 不知道为什么
return acos(delay * val) * RAD2DEG;
}
最后将获得的角度信号在OLED屏幕上进行绘制。因为只有两颗麦克风,只能获得声源的角度信息。并且角度信息的范围是从0~180度之间。在OLED屏幕上建立了一个极坐标,仅仅显示0~180度的角度范围,使用一个指针来指向声源方向。并在屏幕中间显示当前角度的具体值。
void Needle(SSD1306_t *dev, int x0, int y0, int rn, int deg)
{
float rad = deg * M_PI / 180.0;
int x1 = x0 + (rn * cos(rad));
int y1 = y0 - (rn * sin(rad));
_ssd1306_line(dev, x0, y0, x1, y1, false);
int rc = 8;
unsigned int opt;
opt = OLED_DRAW_UPPER_RIGHT | OLED_DRAW_UPPER_LEFT;
_ssd1306_disc(dev, x0, y0, rc, opt, false);
ssd1306_show_buffer(dev);
_ssd1306_line(dev, x0, y0, x1, y1, true);
}
四、效果展示



测试效果相当不错,反应迅速。不过在180度角时就效果很差了,当声源和两个麦克风处于同一条直线时,感知不出声源位置。
五、心得体会:
能完成这个项目非常开心,这算是小时候的一个梦想变成了现实。成功中还有遗憾,尚不能判断声源的距离,还需要拓展到更多的麦克风矩阵上去实现。这里感谢电子森林、贸泽电子的大力支持。