前言

市面上常见的麦克风有很多,可是基本都是usb的,包括领夹麦克风,最多也就是插卡的,看似技术高端,实则菜的一批,音质在设计的时候调整过以后就开始批量生产,只需要在拙劣的电路外面设计一个好看的外壳就行了。而我想做的是,不管人在哪,都可以在服务器上实时听到更接近原声的音频设备。

一、如何采集声音数据

麦克风其实是一种将声音信号转换成电子信号的换能器,在嵌入式领域,依据工作原理可分为多种类型,其中压电式麦克风和MEMS麦克风尤其突出。
微信截图_20220526105355.png
理论上来说压电式麦克风既便宜又丰富,可是它输出的是模拟信号,得使用ADC对他进行转换,ADC精度越高动态范围越精确,价格也就越贵,而esp32内置ADC精度最多只有12位。
c06e911014fd9de4122745e53c1bd1be.jpeg
MEMS麦克风又叫做微基电麦克风,有时也叫数字麦克风输出数字信号。我们可以直接通过i2s协议读取麦克风的数据,这将大大降低我们采集的难度和成本。

二、选型

麦克风:IMP441(协议i2s)
微信截图_20220526140754.png
主控:esp32(ESP-WROOM-32)一核采集处理声音二核发送接收数据

三、esp32固件:

AudioSampler.h:

//
// Created by lightning on 2022/5/23.
//

#ifndef WIFIAUDIOTX_AUDIOSAMPLER_H
#define WIFIAUDIOTX_AUDIOSAMPLER_H

#include "driver/i2s.h"
#include <WiFi.h>

class AudioSampler {
  private:
    // i2s序列
    i2s_port_t i2sPort = I2S_NUM_1;
    // i2s配置
    i2s_config_t i2SConfig;
    // i2s pin设置
    i2s_pin_config_t i2SPinConfig;
    //发送句柄
    TaskHandle_t transmitHandle;
    //两个缓冲buffer8个,2的3次方
    int16_t *currentAudioBuffer;
    //发送缓存
    int16_t *transmitAudioBuffer;
    // i2s队列
    QueueHandle_t i2sQueue;
    //数据指针
    int32_t bufferPointer = 0;
    //缓冲数据大小
    int32_t bufferArraySize;
    //发送数据包大小
    int32_t transmitPackageSize;
    /**
       处理数据方法,用于对剩余进行增益控制
       @param i2sData 原始数据
       @param bytesRead 读取到的数据数
    */
    void processData(int16_t *i2sData, size_t bytesRead);
    //添加单个数据到缓冲中
    void addSingleData(int16_t singleData);

  public:
    void start(i2s_config_t i2SConfig, i2s_pin_config_t i2SPinConfig,
               int32_t transmitPackageSize, TaskHandle_t transmitHandle);
    int16_t *getTransmitBuffer() {
      return transmitAudioBuffer;
    };
    int32_t getTransmitPackageSize() {
      return transmitPackageSize * sizeof(int16_t);
    };
    friend void i2sSamplerTask(void *param);
};
#endif // WIFIAUDIOTX_AUDIOSAMPLER_H

AudioSampler.cpp:

//
// Created by lightning on 2022/5/23.
//

#include <Arduino.h>
#include "driver/i2s.h"
#include "AudioSampler.h"

void AudioSampler::processData(int16_t *i2sData, size_t bytesRead) {
  for (int i = 0; i < bytesRead / 2; i++) {
    addSingleData(i2sData[i]);
  }
}

void AudioSampler::addSingleData(int16_t singleData) {
  this->currentAudioBuffer[this->bufferPointer++] = singleData;
  if (this->bufferPointer == this->transmitPackageSize) {
    //过滤器处理

    //如果采样缓冲池满了就交换
    std::swap(this->currentAudioBuffer, this->transmitAudioBuffer);
    this->bufferPointer = 0;
    xTaskNotify(this->transmitHandle, 1, eIncrement);
  }
}
void i2sSamplerTask(void *param) {
  AudioSampler *audioSampler = (AudioSampler *)param;
  while (true) {
    // 等待队列中有数据再处理
    i2s_event_t event;
    if (xQueueReceive(audioSampler->i2sQueue, &event, portMAX_DELAY) == pdPASS) {
      if (event.type == I2S_EVENT_RX_DONE) {
        size_t bytesRead = 0;
        do {
          // 从i2s中读取数据
          int16_t readData[1024];
          i2s_read(audioSampler->i2sPort, readData, 1024, &bytesRead, 10);
          // 处理原始数据
          audioSampler->processData(readData, bytesRead);
        } while (bytesRead > 0);
      }
    }
  }
}
void AudioSampler::start(i2s_config_t i2SConfig, i2s_pin_config_t i2SPinConfig,
                         int32_t transmitPackageSize, TaskHandle_t transmitHandle) {
  this->i2SConfig = i2SConfig;
  this->i2SPinConfig = i2SPinConfig;
  this->transmitPackageSize = transmitPackageSize / sizeof(int16_t);
  //保存通知句柄
  this->transmitHandle = transmitHandle;
  //分配buffer大小
  this->currentAudioBuffer = (int16_t *)malloc(transmitPackageSize);
  this->transmitAudioBuffer = (int16_t *)malloc(transmitPackageSize);
  //安装i2s驱动
  i2s_driver_install(this->i2sPort, &i2SConfig, 4, &i2sQueue);
  //设置i2spin
  i2s_set_pin(this->i2sPort, &i2SPinConfig);
  //在0核心中读取数据
  xTaskCreatePinnedToCore(i2sSamplerTask, "AudioSampler", 10240, this, 1, NULL,
                          0);
}

wifiaudiotx.ino:

//--------引入库-----------
#include <Arduino.h>
#include <WiFi.h>
#include "AudioSampler.h"
//--------定义常量---------
#define OPEN_DEBUG true //正式运行的时候请设置为false
#define SSID "周嘉浩的iphone"
#define PASSWORD "123456789"
#define SERVER_IP "192.168.101.2"
#define SERVER_PORT 8888
#define uS_TO_S_FACTOR 1000000ULL
#define TIME_TO_SLEEP  600        //睡眠时间10分钟,到时会醒来检查一次,还是没电会继续睡
#define MUTEPIN 12
#define TOUCH_THRESHOLD 40        //触摸灵敏度阈值,越大越灵敏
#define STATUSPIN 14
#define ADCPIN 34
//-------定义变量----------
volatile bool wifiConnected = false;
volatile bool mute = false;
volatile int statusLedState = LOW;
volatile unsigned long sinceLastTouch = 0;
volatile bool inited = false;
// i2s配置
i2s_config_t i2sConfig = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
                           .sample_rate = 44100,
                           .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
                           .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
                           .communication_format =
                             i2s_comm_format_t(I2S_COMM_FORMAT_I2S),
                           .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
                           .dma_buf_count = 4,
                           .dma_buf_len = 1024,
                           .use_apll = false,
                           .tx_desc_auto_clear = false,
                           .fixed_mclk = 0
                         };
// i2s pin配置
i2s_pin_config_t i2SPinConfig = { .bck_io_num = GPIO_NUM_5,
                                  .ws_io_num = GPIO_NUM_19,
                                  .data_out_num = I2S_PIN_NO_CHANGE,
                                  .data_in_num = GPIO_NUM_18
                                };
AudioSampler *audioSampler = NULL;


/**
   静音中断函数
*/
void muteTouch() {
  if (millis() - sinceLastTouch < 1000) return;
  sinceLastTouch = millis();
  mute = !mute;
  if (mute) {
    digitalWrite(MUTEPIN, HIGH);
  } else {
    digitalWrite(MUTEPIN, LOW);
  }
}

/**
   发送缓冲数组到服务器方法
   @param param
*/
void transmitTask(void *param) {
  AudioSampler *audioSampler = (AudioSampler *)param;
  //socket连接服务器
  WiFiClient *wifiClient = new WiFiClient();
  //  while (!wifiClient->connect("192.168.43.121", 8888)) {
  while (!wifiClient->connect(SERVER_IP, SERVER_PORT)) {
    delay(100);
  }
  wifiClient->setNoDelay(true);
  const TickType_t xMaxBlockTime = pdMS_TO_TICKS(100);
  unsigned long startTime;
  unsigned long endTime;
  while (true) {
    // 等待队列通知
    uint32_t ulNotificationValue = ulTaskNotifyTake(pdTRUE, xMaxBlockTime);
    if (ulNotificationValue > 0) {
      //wifi连接上同时未静音才发送数据
      if (wifiConnected && !mute) {
//        Serial.print("start-->");
//        startTime = millis();
//        Serial.print(startTime);
//        Serial.print("---->");
        int sendNum = wifiClient->write((uint8_t *)audioSampler->getTransmitBuffer(), audioSampler->getTransmitPackageSize());
//        Serial.print("end-->");
//        endTime = millis();
//        Serial.print(endTime);
//        Serial.print("---->");
//        Serial.print("total--->");
//        Serial.println(endTime - startTime);
      } else {
        //未连接时情况tcp缓存
        wifiClient->flush();

      }
    }
  }
}
void wifiEvent(WiFiEvent_t event) {
  switch (event) {
    case SYSTEM_EVENT_STA_CONNECTED:
      wifiConnected = true;
      //led亮起
      digitalWrite(STATUSPIN, HIGH);
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      //回调会多次执行,所以要判断一下
      if (inited) {
        wifiConnected = false;
        digitalWrite(STATUSPIN, LOW);
        ESP.restart();
      }
      break;
    default: break;
  }
}
void setup() {
  //状态led初始化
  pinMode(MUTEPIN, OUTPUT);
  pinMode(STATUSPIN, OUTPUT);
  digitalWrite(STATUSPIN, LOW);
  digitalWrite(MUTEPIN, LOW);

  esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
  //读取电压,电池电压低于3.7v就直接睡眠,ADC值大概算了一下在2300未精确测量,睡10分钟然后醒来检测一次
  if (analogRead(ADCPIN) <= 2300) {
    esp_deep_sleep_start();
  }
  //是否开启debug
  if (OPEN_DEBUG) {
    Serial.begin(115200);
  }
  //初始化并连接WiFi
  WiFi.mode(WIFI_STA);
  WiFi.onEvent(wifiEvent);
  WiFi.begin(SSID, PASSWORD);
  //循环等待连接上WiFi
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    delay(500);
    if (statusLedState == LOW) {
      statusLedState = HIGH;
    } else {
      statusLedState = LOW;
    }
    digitalWrite(STATUSPIN, statusLedState);
    Serial.println(statusLedState);
  }
  //创建取样器对象
  audioSampler = new AudioSampler();
  //创建发送句柄
  TaskHandle_t transmitHandle;
  xTaskCreate(transmitTask, "transmitTask", 10240, audioSampler, 1,
              &transmitHandle);
  //启动采样
  audioSampler->start(i2sConfig, i2SPinConfig, 2048, transmitHandle);
  //触摸T0对应的是gpio4
  touchAttachInterrupt(T0, muteTouch, TOUCH_THRESHOLD);
  //初始化完将初始化状态置为true
  inited = true;
}

void loop() {}

四、程序设计及参数说明

微信截图_20220526154948.png
查看数据手册可以得知这个esp32模块为双核,所以物尽其用————双核全部调用,1核用于采集和处理声音,2核用于传输声音和接受命令。
微信截图_20220526155610.png
注:上图为核心0采集声音
微信截图_20220526155758.png
注:上图为核心1调用wifi传输
多核心涉及到任务调用等功能,乐鑫官方自身支持了FreeRTOS系统
最终采样率多次测试,定为44100HZ(cd级别),为了减少wifi传输带来的数据包抖动和延时问题,缓冲区定位1024字节,计算得出采集到缓存共需要11ms时间,发送大约1ms,加上其他aoe延时总延时不超过20ms。
微信截图_20220526160743.png

五、硬件设计

硬件设计本次采用最新学习的开源软件KiCad(进大厂必备)

电路设计:

微信截图_20220526161122.png

PCB设计:

微信截图_20220526161047.png

PCB3D预览

微信截图_20220526161220.png

实物焊接(主要元件)

ec072ecfecd0b365e9f9a96235606da.jpg
BOOM物料地址:BOOM
注:锂电池用小电池包参数S2级别

六、服务器端(暂不开源)

需要:

1.一台自己的服务器或云服务器
2.服务器环境支持java运行

服务器端主要程序:(不含播放程序,只有接受与处理)

AudioServerV2.java:

package pro.dengyi;//创建java包名称pro.dengyi

import javax.sound.sampled.*;    //声明类
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author dengyi (email:dengyi@dengyi.pro)
 * @date 2021-10-06
 */
public class AudioServerV2 {


    public static void main(String[] args) throws IOException, LineUnavailableException {
        ServerSocket serverSocket = new ServerSocket(8888);
        //存储文件,不存在就创建,创建就追加
        File file = new File("C:\\Users\\BLab\\Desktop\\au.raw");
        FileOutputStream fileOutputStream = new FileOutputStream(file, true);
        if (!file.exists()) {
            file.createNewFile();
        }
        //阻塞等待客户端连接
        Socket socket = serverSocket.accept();
        //播放
        AudioFormat audioFormat = new AudioFormat(44100, 16, 1, true, false);
        SourceDataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat, 1024);
        SourceDataLine sourceDataLine = (SourceDataLine) AudioSystem.getLine(info);
        sourceDataLine.open(audioFormat);
        sourceDataLine.start();

        socket.getInetAddress();
        System.out.println("客户端:" + InetAddress.getLocalHost() + "已连接到服务器");
        // 装饰流BufferedReader封装输入流(接收客户端的流)
        BufferedInputStream bis = new BufferedInputStream(
                socket.getInputStream());
        byte[] buffer = new byte[2];

        while (bis.read(buffer) != -1) {
            //保存至文件
            fileOutputStream.write(buffer);
            fileOutputStream.flush();
            //实时播放PCM
            sourceDataLine.write(buffer, 0, 2);


        }


    }

}