Sipeed麦克风阵列获取声源角度

Posted on Jul 06, 2025

官方文档:MicArray 麦克风阵列 - Sipeed Wiki

在官方文档中,MicroPython 例程代码如下:

from Maix import MIC_ARRAY as mic
import lcd

lcd.init()
mic.init() #默认配置
#mic.init(i2s_d0=23, i2s_d1=22, i2s_d2=21, i2s_d3=20, i2s_ws=19, i2s_sclk=18, sk9822_dat=24, sk9822_clk=25)#可自定义配置 IO

while True:
    imga = mic.get_map()    # 获取声音源分布图像
    b = mic.get_dir(imga)   # 计算、获取声源方向
    a = mic.set_led(b,(0,0,255)) # 配置 RGB LED 颜色值
    imgb = imga.resize(160,160)
    imgc = imgb.to_rainbow(1) # 将图像转换为彩虹图像
    a = lcd.display(imgc)
mic.deinit()

上述代码能够实现检测声源位置,输出声源彩虹图像,并点亮麦克风阵列上相应的 LED 灯

但是,代码中并没有明确输出声源的位置信息,这就导致了我们无法直接获取到声源的角度、距离等信息,从而无法实现某些功能

通过观察与调试,我们不难发现,mic.get_dir(imga) 获取到的数据是一组元组数据,其包括十二个元素。当麦克风阵列接收到声音时,其在控制台的输出近似如下的数据:

(0, 0, 0, 5, 8, 6, 0, 0, 0, 0, 0, 0)

我们可以初步判断,这组元组数据表示的应该是 12 个 LED 方向上的声音强度

聪明的你马上就想到了,我们可以根据这组数据获取声源角度

我们知道,麦克风阵列上有 12 个 LED ,如果以丝印正位方向为正位,那么它们代表的角度分别是 30° 60° 90° 120° 150° 180° 210° 240° 270° 300° 330°。据此,我们便可以通过计算十二个方向的矢量和来估算声源的角度:

import math

def get_sound_angle(energy_data):
    if not energy_data or sum(energy_data) == 0:
        return None  # 返回None表示无效数据

    # 矢量求和法
    x_sum = 0.0
    y_sum = 0.0

    for i in range(12):
        angle_rad = math.radians(i * 30)
        # 对能量值进行平方增强主声源权重
        weighted_energy = energy_data[i] ** 2
        x_sum += weighted_energy * math.cos(angle_rad)
        y_sum += weighted_energy * math.sin(angle_rad)

    # 计算角度
    angle = math.degrees(math.atan2(y_sum, x_sum))

    # 转换为-180~180范围
    if angle > 180:
        angle -= 360
    elif angle < -180:
        angle += 360

    return int(angle)

将例程中的 b = mic.get_dir(imga) 作为参数传递给该函数,便可以得到声源角度了。

我的任务是使用 K210 Maix Dock 搭配 Sipeed 麦克风阵列获取声源角度,控制舵机将激光照射在声源处。以下是我的程序源码:

from Maix import MIC_ARRAY as mic
from machine import Timer, PWM
import math
import time

# 全局变量
tim = None
pwm = None
angle_buffer = []  # 角度数据缓冲区
FILTER_WINDOW = 15  # 滤波窗口大小
SERVO_SMOOTH_FACTOR = 0.2  # 舵机运动平滑因子

def init():
    global pwm, tim
    tim = Timer(Timer.TIMER0, Timer.CHANNEL0, mode=Timer.MODE_PWM)
    pwm = PWM(tim, freq=50, duty=7.5, pin=0)  # 初始中间位置
    mic.init(i2s_d0=23, i2s_d1=22, i2s_d2=21, i2s_d3=20,
             i2s_ws=19, i2s_sclk=18, sk9822_clk=25, sk9822_dat=24)

def get_sound_angle(energy_data):
    if not energy_data or sum(energy_data) == 0:
        return None  # 返回None表示无效数据

    # 矢量求和法
    x_sum = 0.0
    y_sum = 0.0

    for i in range(12):
        angle_rad = math.radians(i * 30)
        # 对能量值进行平方增强主声源权重
        weighted_energy = energy_data[i] ** 2
        x_sum += weighted_energy * math.cos(angle_rad)
        y_sum += weighted_energy * math.sin(angle_rad)

    # 计算角度
    angle = math.degrees(math.atan2(y_sum, x_sum))

    # 转换为-180~180范围
    if angle > 180:
        angle -= 360
    elif angle < -180:
        angle += 360

    return int(angle)

def apply_filters(raw_angle):
    global angle_buffer

    # 1. 无效数据跳过
    if raw_angle is None:
        return None

    # 2. 更新数据缓冲区
    angle_buffer.append(raw_angle)
    if len(angle_buffer) > FILTER_WINDOW:
        angle_buffer.pop(0)

    # 3. 中值滤波
    sorted_angles = sorted(angle_buffer)
    median_angle = sorted_angles[len(sorted_angles) // 2]

    # 4. 移动平均滤波
    avg_angle = sum(angle_buffer) / len(angle_buffer)

    # 5. 加权组合滤波结果
    filtered_angle = 0.6 * median_angle + 0.4 * avg_angle

    # 6. 角度连续性处理
    if filtered_angle > 180:
        filtered_angle -= 360
    elif filtered_angle < -180:
        filtered_angle += 360

    return int(filtered_angle)

def convert_angle(angle):
    """将声源角度映射到舵机控制信号
    参数: angle - 声源角度(-180~180度)
    返回: (有效角度, duty_val)
    """
    # 将角度转换为0~360范围用于计算
    angle_360 = angle % 360
    if angle_360 < 0:
        angle_360 += 360

    # 检查角度是否在有效范围内 (270°~90° 的半圆)
    if 90 < angle_360 < 270:
        return False, None  # 无效角度

    # 映射角度到舵机控制信号:
    # - 90° -> duty 2.5 (右边)
    # - 0° -> duty 7.5 (正前方)
    # - -90°/270° -> duty 12.5 (左边)

    # 线性映射到duty值
    # 角度范围: -90°到 90°映射到 duty 12.5 到 2.5
    duty_val = 7.5 - (angle / 18.0)

    # 确保duty值在有效范围内
    duty_val = max(2.5, min(12.5, duty_val))
    return True, duty_val

def main():
    global angle_buffer
    last_valid_duty = 7.5   # 最后有效的duty值(初始为中间位置)

    try:
        # 初始化角度缓冲区
        print("初始化角度缓冲区...")
        for i in range(FILTER_WINDOW):
            imga = mic.get_map()
            energy_data = mic.get_dir(imga)
            raw_angle = get_sound_angle(energy_data)
            angle_buffer.append(raw_angle if raw_angle is not None else 0)
            print("填充缓冲区 %d/%d" % (i+1, FILTER_WINDOW))
            time.sleep_ms(50)

        print("开始主循环...")
        while True:
            start_time = time.ticks_ms()

            # 获取原始数据
            imga = mic.get_map()
            energy_data = mic.get_dir(imga)
            mic.set_led(energy_data, (0, 0, 255))

            # 计算原始角度
            raw_angle = get_sound_angle(energy_data)

            # 应用多级滤波
            filtered_angle = apply_filters(raw_angle)

            # 如果本次数据无效,使用上次滤波结果
            if filtered_angle is None:
                filtered_angle = angle_buffer[-1] if angle_buffer else 0

            raw_str = str(raw_angle) if raw_angle is not None else "--"
            info = "原始角度: %s°  ->  滤波角度: %d°" % (raw_str, filtered_angle)
            info += "\n能量数据: " + str(energy_data)
            print(info)
            print('-' * 30)

            # 转换角度到舵机控制信号
            is_valid, duty_val = convert_angle(filtered_angle)

            if is_valid:
                # 有效角度,使用平滑处理
                target_duty = duty_val
                # 平滑处理:混合上次的duty值和本次的目标值
                smoothed_duty = last_valid_duty * (1 - SERVO_SMOOTH_FACTOR) + target_duty * SERVO_SMOOTH_FACTOR
                last_valid_duty = smoothed_duty
                duty_val = smoothed_duty
                print("有效角度,duty_val:", duty_val)
            else:
                # 无效角度范围,保持上次的有效duty值
                duty_val = last_valid_duty
                print("角度 %d° 在无效范围,保持上次duty值: %.2f" % (filtered_angle, duty_val))

            pwm.duty(duty_val)

            loop_time = time.ticks_diff(time.ticks_ms(), start_time)
            delay_time = max(10, 100 - loop_time)
            time.sleep_ms(delay_time)

    except KeyboardInterrupt:
        print('用户退出')
    finally:
        mic.deinit()
        pwm.duty(7.5)  # 回到中间位置
        print("资源已释放")

if __name__ == '__main__':
    init()
    main()