ESP8266上的String()函数影响模数采样率



我使用ESP8266 NodeMCU 12-E开发板从预放大驻极体麦克风捕获音频,然后将其上传到网络,在那里它将被转换为wav文件。我的第一个想法是将ESP8266上的analogRead(A0)的整数值强制转换为String类型,然后将它们连接到一个更长的字符串有效负载中,我可以将其发布到MQTT代理。

我的MQTT客户端订阅者似乎没有得到合适的声音文件,因为我听到的只是一系列有节奏的爆裂声。

我决定调查我在ESP8266板上的代码是否正确地捕捉到了东西。我把代码精简到这几行,这几行似乎会引起问题:

#include <ESP8266WiFi.h>
const char *ssid =  "____";  // Change it
const char *pass =  "____";  // Change it
void setup()
{
Serial.begin(115200);
Serial.println(0);      //start
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, pass);
}

void loop()
{
int analog = analogRead(A0);
if (analog > 255) {
analog = 255;
}
else if (analog < 0){
analog = 0;
}
Serial.print(String(analog));
Serial.print(" ");
}

以下是我如何使用上面的代码生成一个wav文件来检查声音是否符合我的期望:

- I start up the ESP8266 development board
- I turn on the Serial Monitor and clear all previous output
- I power up my electret microphone and speak into it
- I power down my electret microphone
- I copy the contents of the Serial Monitor (which is a series of integers) into a text file called `audio.raw`
- I copy `audio.raw` to a linux machine that has ffmpeg installed
- I issue the command `ffmpeg -f u8 -ar 11111 -ac 1 -i audio.raw -y audio.wav` on the linux machine

当我听audio.raw文件时,我会听到我的声音,但速度可能比正常情况快5-10倍。(我也有很多噪音和失真,但这可能是输入信号质量的一个单独问题。)

然后我尝试将这一行代码Serial.print(String(analog))更改为Serial.print(analog)。然后我重复了上面的步骤。但这一次,我的声音听起来比平时快了2倍。

为什么将这一行从Serial.print(String(analog))更改为Serial.print(analog)会产生如此大的差异?

是因为String()函数是一个非常昂贵、占用大量时间的操作吗?当脚本需要更多的时间来处理每一行代码时,那么脚本捕获足够的analogRead(A0)数据点的时间就更少了?如果我使用所有相同的标志运行相同的ffmpeg命令,那么ffmpeg将试图通过加快音频播放来满足-ar 11111的要求?这意味着我的采样率取决于脚本的执行速度?这意味着,由于制造精度、环境温度等的可变性,我必须考虑同一型号的其他板的执行速度可变。。。?

您的采样率与循环实现相耦合(正如您所发现的)。这也会导致采样率的抖动,因为不同的代码路径将花费不同的时间,中断服务例程也将窃取CPU周期。

这种抖动将是导致输出失真的原因之一。

当我听audio.raw文件时,我会听到我的声音,但速度可能比正常速度快5-10倍。

ESP8266有一个硬件UART,因此代码可能会以比输出更快的速度加载UART的FIFO缓冲区。这将是感知到的更快采样率的来源,但也会在缓冲器充满时导致抖动或数据丢失。根据实现方式,当缓冲区填充时,它将丢弃数据或阻塞(导致抖动)。

为什么将这一行从Serial.print(字符串(模拟))更改为Serial.print?

是因为String()函数是一个非常昂贵且占用大量时间的操作吗?当脚本需要更多的时间来处理每一行代码时,那么脚本捕获足够的analog Read(A0)数据点的时间就更少了?

是,是,是。

性能差异的原因之一是String()涉及到在堆上分配和管理内存来存储字符。

Serial.print(analog)在堆栈上使用固定大小的缓冲区,因为代码知道显示int所需的最大字符数

如果我使用所有相同的标志运行相同的ffmpeg命令,那么ffmpeg将试图通过加快音频播放来满足-ar 11111的要求?

是。ffmpeg假设样本具有固定的采样率,但这与正在打印的样本不匹配。

这意味着我的采样率取决于脚本的执行速度?

是的!

这意味着,由于制造精度、环境温度等的可变性,我必须考虑同一型号的其他板的可变执行速度。。。?

是。将会有许多变量影响执行速度。

你能做什么

将数据采样与代码执行解耦。

这可以通过实现中断服务例程来实现。将ISR与硬件计时器连接,使其以固定的采样率执行,避免抖动。

ISR可以写入loop()中的代码通过串行连接传输的缓冲器。ISR和串行传输代码需要管理缓冲区,以确保两者都不超过对方。这样做的一种方法是使用ISR和传输代码使用的备用缓冲区。

由于您使用Serial.boot(115200)ESP8266微控制器将通过串行端口每秒传输115200位。即115200/8=14400字节每秒,这意味着由于您对音频使用u8(无符号8位)格式,每个样本由一个字节组成。只需将ffmpeg-ar参数更改为14400即可。

我没有麦克风可以连接到MCU进行测试,但它应该可以正常工作。另一个-ac参数是正确的,因为它是单声道音频。

编辑:打印到Serial时也不要使用String()构造函数。

使用Serial()构造函数时,由于String将1字节的值转换为3字节,因此声音速度提高了约5倍;byte:255->String:"2","5","五",你不必考虑微控制器的执行速度,它会像你定义的那样每秒输出115200位。你只需要考虑它的输出。

最后删除行

Serial.print(");

同时更改

int analog=analog Read(A0);

字节模拟=(字节)模拟读取(A0);

由于int由4个字节组成,您不希望向串行发送额外的3个字节。

在将int更改为byte之后,您可以清除此代码块

if (analog > 255) {
analog = 255;
}
else if (analog < 0){
analog = 0;
}

如果您通过带有ffmpeg的usb将ESP8266连接到linux设备,则可以使用

ttylog-b 115200-d/dev/ttyUSB0|ffmpeg-f u8-ar 14400-ac 1-i-y音频.wav

从ESP8266实时捕获音频数据。

最新更新