麦斯科技 · 2021年09月26日

面向数据科学家的定点DSP

了解如何在 Python 中创建 DSP 管道,并将其转换为使用 C/C++ 和 Arm 的 CMSIS-DSP 库在基于Arm的 Cortex-M 上运行。

作者:Sandeep Mistry, Henri Woodcock (代表Arm软件开发者团队)

介绍

当机器学习(ML)应用于音频系统时,通常使用数字信号处理(DSP)技术将输入音频信号转换为2D“图像”,以便可以使用计算机视觉技术对音频进行分类。现实世界中的例子包括音频或语音识别,以及关键词识别。

本指南将使用Python和NumPy代码示例向您介绍这些系统中使用的DSP技术。一旦您熟悉了这些DSP技术,我们将介绍如何在Python中使用Arm的CMSIS-DSP库,使用定点数学执行相同的操作。

最后,我们将演示如何在C中创建等效管道,以便将其部署到基于Arm Cortex-M的微控制器(MCU)。在基于Arm Cortex-M的系统上使用定点数学可以大大减少内存使用,同时提高运行时性能!

本指南的Jupyter笔记本版本也可以在GitHub上找到(https://github.com/ArmDeveloperEcosystem/fixed-point-dsp-for-data-scientists/blob/main/fixed_point_dsp_for_data_scientists.ipynb),也可以使用Google Colab访问(https://colab.research.google.com/github/ArmDeveloperEcosystem/fixed-point-dsp-for-data-scientists/blob/main/fixed_point_dsp_for_data_scientists.ipynb)。

目录

  • 音频频谱图
  • Python中的音频频谱图
  • 基于CMSIS-DSP的音频频谱图
  • 不同音频声音的频谱图示例
  • 将CMSIS-DSP代码从Python移植到C
  • 结论

音频频谱图

在本笔记本中,我们将在NumPy和CMSIS-DSP中开发音频频谱图管道。这是一个用于音频数据的通用预处理管道。原始音频样本被转换为音频信号的视觉表示,该音频信号显示频率随时间的变化。它通常表示为二维图像。

要更深入地解释频谱图,可以查看TensorFlow音频识别教程。

为了将音频信号转换为频域,使用快速傅里叶变换(FFT)。这会将振幅随时间变化表示的音频信号转换为频域,因此您可以了解信号中包含的频率。然而,当对整个音频信号执行FFT时,您将丢失该音频信号的时域信息,为了抵消这一点,我们将音频分解为单独的段。

频谱图步骤

创建频谱图可分为以下步骤:

  1. 音频信号被分解成称为“窗口”的较短段。这意味着当我们以后应用FFT时,我们仍然保留一些时间信息,因为我们知道每个窗口发生在什么时间点。将音频信号拆分为窗口时,窗口之间通常会有一些重叠,以帮助保持输出更连续。
  2. 然后对每个窗口应用窗口功能。将信号分割成重叠的窗口将使音频信号出现不连续性,而这将无法准确表示真实世界的音频信号。为了减少这种影响,窗口功能将通过将窗口乘以0和1之间的平滑对称曲线来平滑每个窗口振幅的边缘。
  3. 然后使用FFT将每个窗口转换为频域。由于FFT产生复数输出,因此取该输出的绝对值以保留振幅信息,但去除复数。
  4. 最后创建一个称为频谱图的二维数组,其大小(窗口数、FFT箱数)。

我们在下面为您选择了一些音频输入示例及其音频频谱图表示:

微信图片_20210926233204.png

Python中的音频频谱图

现在您已经大致了解了音频频谱图是什么,我们将介绍如何使用Python和Numpy从预先录制的波形文件创建一个音频频谱图。

安装Python模块

您需要使用pip安装NumPy、Matplotlib、SciPy和requests Python模块

pip install numpy scipy matplotlib requests

下载示例音频文件

我们将使用“ESC-50:环境声音分类数据集”GitHub repo(https://github.com/karolpiczak/ESC-50)中的“door knocking“.wav文件作为笔记本中DSP管道的输入信号。您可以使用Python中的请求库从GitHub下载.wav文件:

import requests

wave_file = "1-103995-A-30.wav"
response = requests.get(
  f"https://github.com/karolpiczak/ESC-50/raw/master/audio/{wave_file}"
)

with open(wave_file, "wb") as f:
  f.write(response.content)
download_wav_file.py hosted with ❤ by GitHub

从示例音频文件中读取数据

SciPy的SciPy.io.wavfile.read(…)API现在可以用来读取.wav文件中的音频样本。

.wav文件的采样率为44.1 kHz,这意味着每1秒的音频由44100个样本表示。由于44.1 kHz采样率包含的分辨率比我们需要的分辨率高,我们将使用SciPy的SciPy.signal.resample(…)API将音频数据的采样率从44.1 kHz降低到16 kHz,以减少分析中使用的采样数。


import numpy as np
from scipy.io import wavfile
from scipy import signal

def read_wave_file(file, sample_rate):
  # read sample rate and data from wave file
  original_sample_rate, orginal_audio_data = wavfile.read(file)

  # normalize input to floating-point value, if needed
  if orginal_audio_data.dtype is np.float32:
    audio_data_float32 = orginal_audio_data
  else:
    audio_data_float32 = orginal_audio_data / np.iinfo(orginal_audio_data.dtype).max

  # downsample to desired sample rate
  audio_data = signal.resample(audio_data_float32, (len(audio_data_float32) // original_sample_rate) * sample_rate)

  return audio_data

audio_sample_rate = 16000

audio_samples = read_wave_file(wave_file, audio_sample_rate)

print(f"Successfully read {len(audio_samples)} samples from '{wave_file} with sample rate of {audio_sample_rate}")

read_wav_file.py hosted with ❤ by GitHub

现在我们已经从.wav文件中读取了音频样本,让我们使用Matplotlib绘制音频信号随时间的变化:

1_gXX9H-9CAbCz3NBbkWUilw (1).png

您还可以在计算机上下载并收听1–103995-A-30.wav文件。

根据整个音频信号的图表,播放声音时,我们知道在开始时大约有1秒的声音,然后是静音。让我们使用Matplotlib放大并仅绘制音频信号随时间变化的前1秒,以便仔细查看:

1_0dmfgyusX25-Y1yFNyVjXQ (1).png

窗口化

现在我们可以将音频样本分割为重叠窗口。通常选择2的幂,例如16、32、64、128、256、512、1024或2048作为窗口大小。我们将使用256的窗口大小,同时步进音频信号128个样本。

1_jn-qyQGTfeoUpaGK1nEwgw (1).png

当前窗口以绿色突出显示在整个信号的上方,以蓝色突出显示。您可以看到第一个窗口从0到256,第二个窗口从128到384(128+256),第三个窗口从256到512,第四个窗口从384到640。

.wav文件的窗口总数可按如下方式计算:

number_of_windows = (len(audio_samples) - window_size) // step_size

窗口功能和汉宁窗口

在处理连续信号(如音频)时,在应用FFT变换之前应用窗口函数,以便音频片段的边缘更平滑。窗口功能是一种(通常)范围为(0–1)的功能,可按如下方式应用于窗口:

𝑦 = 𝑤𝑖𝑛𝑑𝑜𝑤_𝑓𝑢𝑛𝑐𝑡𝑖𝑜𝑛 × 𝑤𝑖𝑛𝑑𝑜𝑤

最常见的窗口功能之一(本例中使用的功能)是Hanning窗口。NumPy有一个内置的方法来计算任意窗长的Hanning窗:

import numpy as np
hanning_window = np.hanning(window_size)

让我们使用Matplotlib绘制汉宁窗口:

1_UEFGkEfv4Yt8fU0RCPPCGw (1).png

现在,可以使用以下方法将汉宁窗口应用于第一个音频信号窗口:

window_1 = audio_samples[0:window_size]
processed_window_1 = hanning_window * window_1

然后,我们可以使用Matplotlib绘制第一个窗口和已处理的窗口

1_TyQu3pNEQRu3bcXVZ_02wQ (1).png

您可以看到,在整个窗口周期内,处理窗口的振幅从未超过汉宁窗口的值。

让我们扩展我们的图,在前几个窗口中循环。

1_wbZQ3qlV4wTghAyXYD1sFA (1).gif

FFT

对于每个处理过的窗口,可以使用NumPy的np.FFT.rfft(…)API计算FFT,并使用np.absolute(…)获取FFT的绝对值:

# Calculate the FFT.
fft_1 = np.fft.rfft(processed_window_1)
# Take the absolute value.
fft_bins_1 = np.absolute(fft_1)

然后,我们可以使用Matplotlib绘制FFT bins 。

1_VOXjlY5Kq91zkCEikzaGOg (1).png

让我们绘制前4个窗口随时间变化的FFT,以便我们可以看到音频信号的频率内容随时间的变化:

1_RZ2W7QGfpjjIny3BqY7TSg (1).png

程序

我们可以创建一个音频信号频域随时间变化的2D图,作为热图,也称为谱图,而不是为每个窗口周期创建单独的FFT图!该频谱图显示x轴上的时间,y轴上的频率,最后是带有颜色的振幅。

让我们以重叠的步长和窗口大小遍历所有样本,以计算随时间变化的信号频率,并显示为频谱图。这里我们计算1秒音频中的窗口数。


# Putting this all together
window_size = 256
step_size = 128

# Calculate the number of windows
number_of_windows = int(1 + (audio_sample_rate - window_size) // step_size)

# Calculate the FFT Output size
num_fft_bins = int(window_size // 2 + 1)

# Create an empty array to hold the Spectrogram
spectrogram = np.empty((number_of_windows, num_fft_bins))

# Apply hanning window and apply fft
for index in range(number_of_windows):
  window_start = index * step_size
  window_end = window_start + window_size

  # Take the window from the waveform.
  window = audio_samples[window_start:window_end]

  # Apply the Hanning Window.
  processed_window = window * hanning_window

  # Calculate the FFT
  fft = np.fft.rfft(processed_window)

  # Take the absolute value of the FFT and add to the Spectrogram.
  spectrogram[index] = np.abs(fft)

create_spectrum.py hosted with ❤ by GitHub

现在我们已经计算了音频信号的频谱图,让我们绘制它:

1_pf1eMngFr9IZ0AfKTEu3nA (1).png

从频谱图中,我们可以清楚地看到每个knock的 5个感兴趣的区域,频率低于4000 Hz的区域更为普遍。

让我们放大频谱图的下半部分,仔细观察:

1_eUvkn-SwR5Wq_kum2gLhvQ (1).png

让我们再画一个图,但将时域中的信号添加到频谱图上方。这样,我们可以看到信号的振幅和频率计数与时间的相关性。

1_Y8W5T2G3d12Hcmbvut34AA (1).png

回顾

让我们回顾一下我们迄今为止所取得的成就:

  1. 使用请求库从GitHub下载敲门声的.wav文件。
  2. 使用SciPy库以44.1 kHz的频率从.wav文件中读取音频样本,并对其重新采样,以获得16 kHz的采样率。
  3. 了解什么是窗口以及为什么需要窗口功能,如汉宁窗口。
  4. 学习了如何在特定窗口周期内创建音频信号的FFT。
  5. 通过跨越音频信号,应用汉宁窗口,并对每个窗口周期使用FFT函数,创建音频信号的频谱图表示。

基于CMSIS-DSP的音频频谱图

现在您已经了解了如何使用NumPy创建音频频谱图,我们可以使用带有定点数学的CMSIS-DSP Python包装器来做同样的事情。

定点数学和CMSIS-DSP简介

与浮点运算不同,定点数学数表示精度较低的实数,因为它们有固定位数来表示小数点前后的数字。例如,如果我们有1.15定点格式的16位数字,16位值可以表示-1和1之间的值,固定精度为1/2^15=0.000030517578125。

Arm Cortex-M处理器在基于整数的计算方面比浮点运算更快。Arm Cortex-M4、Cortex-M7和后者处理器支持单指令多数据(SIMD)指令,允许它们一次对多个值执行相同的指令。CMSIS-DSP库利用这些SIMD指令进行更高性能的DSP处理。

Arm的CMSIS-DSP库包含针对Arm Cortex-M处理器的常见DSP操作的高度优化实现。

CMSIS-DSP Python包装器

通常,在ARM CORTEX-M设备上使用C或C++的CMIS-DSP库。然而,CMSIS-DSP团队已经创建了一个Python包装器来利用Python中的这些DSP函数。

该库包含针对Arm Cortex-M处理器的高度优化的DSP函数实现和针对非Arm处理器的通用C实现。如果您在PC上使用Arm CMSIS-DSP Python包装器(可能基于x86),则从输入和输出的角度来看,这些函数的行为将与在Arm处理器上的行为相同,但是您可能看不到运行时性能的提高。

让我们继续安装CMSIS-DSP Python包装器:

pip install git+https://github.com/ARM-software/CMSIS_5.git@5.8.0#egg=CMSISDSP\&subdirectory=CMSIS/DSP/PythonWrapper

量化输入信号

我们可以使用CMSIS-DSP的arm_float_to_q15(..)函数将浮点值转换为16位定点(q15)值:

from cmsisdsp import arm_float_to_q15
audio_samples_q15 = arm_float_to_q15(audio_samples)

现在,让我们沿着原始NumPy值绘制定点值:

1_XUHHQpyP38aCQUzHml-yXQ (1).png

您可以看到信号的形状与使用NumPy创建的信号相似,但是Y轴上的值范围为-32768到32767,而不是0到1。

CMSIS-DSP中的汉宁窗

CMSIS-DSP没有为特定窗口大小创建汉宁窗口的内置功能。但是,我们可以利用内置的arm_cos_f32(…)和arm_float_to_q15(…)API来创建一个定点汉宁窗口。让我们使用Hann函数的公式:

1_OniVxcmKoYgyj7kwODmFIQ (1).png

# Generate the Hanning Window
from numpy import pi as PI
from cmsisdsp import arm_cos_f32, arm_float_to_q15

hanning_window_f32 = np.zeros(window_size)

for i in range(window_size):
    hanning_window_f32[i] = 0.5 * (1 - arm_cos_f32(2 * PI * i / window_size ))
    

hanning_window_q15 = arm_float_to_q15(hanning_window_f32)

create_hanning_window_cmsisdsp.py hosted with ❤ by GitHub

现在,让我们使用Matplotlib沿着NumPy Hanning窗口绘制Q15 Hanning窗口:

1_MrvOE_0AcWyunmd6kR_1zw (1).png

您可以看到它的形状与使用NumPy创建的形状相似,但是值的范围是0到32767,而不是0到1。

要像在NumPy中一样应用汉宁窗口,我们可以使用arm_mult_q15(…)乘法函数将两个长度相等的向量相乘在一起。


from cmsisdsp import arm_mult_q15

window_1_q15 = audio_samples_q15[:window_size]

# Multiply the signal by the window
processed_window_1_q15 = arm_mult_q15(window_1_q15, hanning_window_q15)

apply_hanning_window_cmsisdsp.py hosted with ❤ by GitHub

现在让我们看一下应用q15格式的汉宁窗口后的第一个窗口:

1_m1kvManlqgAZ9MIs97F_kg (1).png

让我们将其与NumPy值并排绘制:

1_EqigP4pjYRTz2GiOBl-jRg (1).png

用CMSIS-DSP实现FFT

CMSIS-DSP为各种数据类型提供了许多FFT功能:q15、q31和f32。在本例中,我们将使用实FFT函数。要使用CMSIS-DSP的Q15 RFFT函数,我们首先需要创建一个arm_RFFT_实例_Q15实例,并使用arm_RFFT_init_Q15(…)函数对其进行初始化。之后,该过程类似于NumPy,您可以使用arm_rfft_q15(…)函数来计算FFT,使用arm_cmplx_mag_q15(…)来计算FFT的魔力。


from cmsisdsp import arm_rfft_instance_q15, arm_rfft_init_q15, arm_rfft_q15, arm_cmplx_mag_q15

# Initialize the FFT
rfft_instance_q15 = arm_rfft_instance_q15()
status = arm_rfft_init_q15(rfft_instance_q15, window_size, 0, 1)

# Apply the FFT to the audio
rfft_1_q15 = arm_rfft_q15(rfft_instance_q15, processed_window_1_q15)

# Take the absolute value
fft_bins_1_q15 = arm_cmplx_mag_q15(rfft_1_q15)[:window_size // 2 + 1]

xf = np.fft.rfftfreq(len(processed_window_1_q15), d=1./audio_sample_rate)

calculate_fft_cmsis_dsp.py hosted with ❤ by GitHub

现在,我们可以使用Matplotlib绘制Q15定点值:

1_eckkXMGnbTaxxxHNGCKv9Q (1).png

现在,让我们将fft_bins_1_q15 q15值转换为浮点值,以便我们可以将该值与NumPy计算值进行比较。

使用256 rfft长度时,arm_rfft_q15(…)函数的输出格式将为9.7(而不是1.15)。arm_cmplx_mag_q15(…)功能的文档说明输入值为1.15格式,输出为2.14格式。因此,如果我们将一个9.7格式的数字传递到arm_cmplx_mag_q15(…),输出的格式将为10.6,这意味着它的值将在-512到511之间。

我们可以使用arm_Q15_to_float(…)函数将10.6数字从Q15转换为浮点值,然后将其乘以512(2^9):


from cmsisdsp import arm_q15_to_float
# Let's rescale them and compare the two
fft_bins_1_q15_scaled = arm_q15_to_float(fft_bins_1_q15) * 512

我们可以覆盖它们,看看它们有多近:

1_9wHuQlKpSs5vr8HWS5Atsw (1).png

从上面我们可以看到,这两条管道在视觉上产生了非常相似的特征。

虽然使用定点函数会丢失一些信息,但我们设法计算出一个相似的形状,并在相同的频率下显示峰值。定点函数无法计算的频率(2000年的右边)都具有非常低的振幅,表明信息损失最小。

基于CMSIS-DSP的频谱图

最后,我们需要把所有这些放在一起,就像我们对NumPy做的那样。


# Putting this all together
window_size = 256
step_size = 128

# Convert the audio to q15
audio_samples_q15 = arm_float_to_q15(audio_samples)

# Calculate the number of windows
number_of_windows = int(1 + (audio_sample_rate - window_size) // step_size)

# Calculate the FFT Output size
fft_size = int(window_size // 2 + 1)

# Create an empty array to hold the Spectrogram
spectrogram_q15 = np.empty((number_of_windows, fft_size))

start_index = 0
# Apply hanning window and apply fft
for index in range(number_of_windows):
  # Take the window from the waveform.
  audio_window_q15 = audio_samples_q15[start_index:start_index + window_size]

  # Apply the Hanning Window.
  processed_audio_q15 = arm_mult_q15(audio_window_q15, hanning_window_q15)

  # Calculate the FFT
  rfft_q15 = arm_rfft_q15(rfft_instance_q15, processed_audio_q15)

  # Take the absolute value of the FFT and add to the Spectrogram.
  rfft_mag_q15 = arm_cmplx_mag_q15(rfft_q15)[:fft_size]

  spectrogram_q15[index] = rfft_mag_q15

  # Increase the start index of the window by the overlap amount.
  start_index += step_size

create_spectrogram_cmsisdsp.py hosted with ❤ by GitHub

现在,让我们用Matplotlib绘制输入信号和Q15频谱图:

1_qiUvjjDcVvOgbraxIz6M2A (1).png

最后将Q15频谱图与NumPy版本进行比较:

1_0KIpL-2ubEyda92DoIHmZA (1).png

回顾

正如我们所看到的,CMSIS-DSP管道与NumPy管道类似。让我们回顾一下我们为创建它所做的工作:

  1. 使用arm_float_to_Q15功能将音频样本转换为Q15格式。
  2. 使用arm_cos_F32和arm_float_to_q15函数,使用Hann函数公式计算Hanning窗口。
  3. 学习了如何使用arm_mult_q15函数乘以相同长度的向量。
  4. 学习了如何使用arm_rfft_实例_Q15、arm_rfft_初始_Q15和arm_rfft_Q15函数在CMSIS-DSP中计算Q15 FFT。
  5. 通过跨越音频信号,应用汉宁窗口,并对每个窗口周期使用FFT函数,创建音频信号的频谱图表示。

不同音频声音的频谱图示例

现在让我们看一下ESC-50代码库(https://github.com/karolpiczak/ESC-50)中不同音频文件的几个不同的频谱图。

1_SnlkFObW0-LA3OMEGHzF0Q (1).png

将CMSIS-DSP代码从Python移植到C

现在,我们已经大致了解了如何在上一节中使用Python中的各种CMSIS-DSP API,我们可以开始将它们映射到C,以便在基于Cortex-M的系统上运行。

Includes

第一步是在.c文件的顶部添加一个#include<arm_math.h>:

#include <arm_math.h>

常数

然后我们可以为窗口和步长定义一些常量变量:

const int WINDOW_SIZE = 256;
const int STEP_SIZE   = 128;

输入信号

可以声明一个全局数组变量来存储输入信号:

q15_t input_q15[WINDOW_SIZE];

用于创建频谱的输入信号可存储在输入_q15变量中。

出于测试目的,我们将其设置为固定正弦波,频率为440 Hz,采样频率为16 kHz。

for (int i = 0; i < WINDOW_SIZE; i++) {
    float32_t f = sin((2 * PI * 440) / 16000 * i);
    
    arm_float_to_q15(&f, &input_q15[i], 1);
}

汉宁窗

对于Hanning窗口函数,必须声明一个全局数组变量来存储窗口函数值:

q15_t hanning_window_q15[WINDOW_SIZE];

然后我们可以创建一个C函数,在运行时初始化hanning_window_q15数组:

void hanning_window_init_q15(q15_t* hanning_window_q15, size_t size) {
    for (size_t i = 0; i < size; i++) {
        // calculate the Hanning Window value for i as a float32_t
        float32_t f = 0.5 * (1.0 - arm_cos_f32(2 * PI * i / size ));
        
        // convert value for index i from float32_t to q15_t and
        // store in window at position i
        arm_float_to_q15(&f, &hanning_window_q15[i], 1);
    }
}

我们需要另一个变量来存储输入信号上应用的汉宁窗口的值:

q15_t processed_window_q15[WINDOW_SIZE];

现在我们可以在输入信号上应用汉宁窗口:

// equivalent to:
//   processed_window_q15 = input_q15 * hanning_window_q15
arm_mult_q15(input_q15, hanning_window_q15, processed_window_q15,
             WINDOW_SIZE);

FFT

变量

我们需要FFT实例和输出的全局变量:

arm_rfft_instance_q15 S_q15;
// this is twice the size because each FFT output has a real and
// imaginary part
q15_t fft_q15[WINDOW_SIZE * 2];
// this is half the size of WINDOW_SIZE becase we just need the
// magnitude from the first half of the FFT output
q15_t fft_mag_q15[WINDOW_SIZE / 2];

初始化

我们需要调用arm_rfft_init_q15来初始化q15 rfft实例:

arm_rfft_init_q15(&S_q15, WINDOW_SIZE, 0, 1);

执行FFT

执行FFT和计算FFT的输出幅度现在可以通过以下方式完成:

arm_rfft_q15(&S_q15, processed_window_q15, fft_q15);
arm_cmplx_mag_q15(fft_q15, fft_mag_q15, WINDOW_SIZE / 2);

把所有的东西都集成在一起做一个Arduino Sketch


#include <arm_math.h>

// constants
const int WINDOW_SIZE = 256;
const int STEP_SIZE   = 128;

// global variables
q15_t input_q15[WINDOW_SIZE];
q15_t hanning_window_q15[WINDOW_SIZE];
q15_t processed_window_q15[WINDOW_SIZE];

arm_rfft_instance_q15 S_q15;

// this is twice the size because each FFT output has a real and imaginary part
q15_t fft_q15[WINDOW_SIZE * 2];

// this is half the size of WINDOW_SIZE becase we just need the magnitude from
// the first half of the FFT output
q15_t fft_mag_q15[WINDOW_SIZE / 2];


void setup() {
  Serial.begin(9600);
  while (!Serial);

  Serial.println("Hello from CMSIS DSP Q15 FFT example");
  
  // initialize input with a 400 Hz sine wave
  for (int i = 0; i < WINDOW_SIZE; i++) {
    float32_t f = sin((2 * PI * 440) / 16000 * i);
    
    arm_float_to_q15(&f, &input_q15[i], 1);
  }

  // pipeline initialization
  hanning_window_init_q15(hanning_window_q15, WINDOW_SIZE);
  arm_rfft_init_q15(&S_q15, WINDOW_SIZE, 0, 1);
}

void loop() {
  unsigned long pipeline_start_us = micros();
  
  // equivalent to: processed_window_q15 = input_q15 * hanning_window_q15
  arm_mult_q15(input_q15, hanning_window_q15, processed_window_q15, WINDOW_SIZE);

  // calculate the FFT and FFT magnitude
  arm_rfft_q15(&S_q15, processed_window_q15, fft_q15);
  arm_cmplx_mag_q15(fft_q15, fft_mag_q15, WINDOW_SIZE / 2);

  unsigned long pipeline_end_us = micros();

  Serial.print("Pipeline run time = ");
  Serial.print(pipeline_end_us - pipeline_start_us);
  Serial.println(" microseconds");


  // all done loop forever ...
  while (1);
}


void hanning_window_init_q15(q15_t* hanning_window_q15, size_t size) {
  for (size_t i = 0; i < size; i++) {
    // calculate the Hanning Window value for i as a float32_t
    float32_t f = 0.5 * (1.0 - arm_cos_f32(2 * PI * i / size ));

    // convert value for index i from float32_t to q15_t and store
    // in window at position i
    arm_float_to_q15(&f, &hanning_window_q15[i], 1);
  }
}

cmsisdsp_fft.ino hosted with ❤ by GitHub

性能基准测试

现在,让我们在以下Arduino或Arduino兼容板上对上面的Arduino sketch进行基准测试:

  • Arduino Nano 33 IoT
  • Arduino Nano 33 BLE
  • Teensy 4.0

我们已经创建了一个等效的草图,它使用32位浮点而不是定点来与上面的定点草图进行比较。这两个草图都可以在GitHub上找到:

  • cmsis_dsp_fft_q15.ino
  • cmsis_dsp_fft_f32.ino

内存使用

当我们使用16位Q15而不是32位浮点时,我们应该期望看到DSP操作所需的缓冲区占用一半的空间。

我们目前的缓冲区包括:

微信图片_20210926235409.png

因此,对于窗口大小=256,我们期望:

  • F32: 22 x 256 = 5632 bytes
  • Q15: 11 x 256 = 2816 bytes

这说明,与F32使用浮点相比,Q15使用定点需要一半的RAM。

如果我们编译草图,我们会得到以下“全局变量使用”指标。

微信图片_20210926235431.png

来自Arduino的值与我们希望保存的RAM量完全匹配。

运行时速度

现在,让我们将两个基准测试草图上传到电路板,以查看计算单个输入窗口的FFT bin需要多长时间:

微信图片_20210926235459.png

Arduino Nano 33 IoT板基于Arm Cortex-M0+CPU,未配备FPU(浮点单元),使用16位定点DSP管道而不是32位浮点版本,完成管道的速度快11倍。Arduino Nano 33 BLE基于Arm Cortex-M4F CPU并配备FPU,其性能提高了37%。最后,Teensy 4.0基于配备FPU的Arm Cortex-M7 CPU,提高了26%。

结论

本指南概述了将音频信号转换为频谱图的DSP管道,然后可以将频谱图与计算机视觉技术结合起来对音频信号进行分类。我们已经解释了什么是频谱图,以及如何使用以下DSP操作从Python和NumPy中的.wav文件创建频谱图:加窗、汉宁窗口和FFT。在此之后,您将了解定点数学,以及如何使用CMSIS-DSP Python包装器使用16位定点操作创建等效的频谱图,并将精度损失降至最低。然后,我们介绍了如何将CMSIS-DSP管道从Python移植到C,以便在基于Arm Cortex-M的设备上运行。

在我们的基准测试中,当使用定点16位操作而不是32位浮点操作时,DSP管道所需的全局缓冲区需要的内存减少50%。当使用定点16位操作而不是浮点操作时,Cortex-M0+(没有FPU)的速度提高了11倍。Cortex-M4F和Cortex-M7F系统内置FPU,当使用定点16位操作而不是浮点操作时,速度分别提高了37%和26%。这些都是令人印象深刻的改进。使用定点还节省了转换步骤,从模拟话筒(使用ADC)或数字话筒读取音频输入值时,音频输入值(通常)存储为16位值。

有关Arm软件开发团队的更多博客、指南、资源、视频和笔记本,请访问:

@ArmSoftwareDev on Twitter

The Arm Software Developers YouTube channel

@ArmDeveloperEcosystem on GitHub

了解更多

在即将到来的Arm DevSummit(10月19日至21日为期3天的虚拟活动)上,提高技能并使用tinyML获得实践经验。该活动包括tinyML计算机视觉真实世界嵌入式设备研讨会,以及使用基于Arm Cortex-M的MCU构建大词汇量语音控制。我们希望在那里见到你!

https://devsummit.arm.com

推荐阅读
关注数
5822
内容数
525
定期发布Arm相关软件信息,微信公众号 ArmSWDevs,欢迎关注~
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息