本人是音乐爱好者,从小就特别喜欢那个随着音乐跳动的方框效果,就是这个:
arduino上一大把
对,我忍你很久了,我就想用mpy做,全网没有,行我自己研究。
果然兴趣是最好的老师,我之前有篇博客专门讲音频,有兴趣的可以回顾一下。
提到可视化频谱,必然绕不开fft,大学学过这玩意,当时一心玩,老师讲的一个字都么听进去,网上教程简略扫了一下, 大该就是把时域转频域的工具,我大mpy居然没有fft函数,奶奶的,先放着。
音频信息如何收集?第一种傻瓜式的ADC,模拟转数字,原始粗暴,第二种,I2S库,我之前博客有讲过,数据是PCM编码。
然后又去学PCM编码,一学豁然开朗,舒服,以代码为例:
audio_in = I2S(0,
sck=bck_pin, ws=ws_pin, sd=sdin_pin,
mode=I2S.RX,
bits=16,
format=I2S.MONO,
rate=8000,
ibuf=64000)
这里初始化了一个I2S对象,应用题来了,已知:bits=16 rate=8000 buf=64000 单声道,请问可以录几秒?之前的博客实践告诉我,4秒,为啥是4秒呢?能懂这个PCM编码已经懂了一大半了,基本原理还是得自己啃,我不直接喂你,我尽量人话解释一下,bits=16:16位采样,也就是有65536中大小的数据,存一个数据需要2bytes,rate=8000hz,一秒存8000个数据,单声道,不用双声道,简化了,然后就算吧,2bytes80004=64000bytes,芜湖,刚好对上,这不就很明了了么,就是逐个bytes存的呀,至于具体咋编的码,我还要看下。
micropython的I2S有几种模式的,阻塞,非阻塞,即时,翻文档:
这一看阻塞就没戏,试试剩下的两个喽。
0905更新:
目前非阻塞的可以正常用了,存完树蕨后吗,取出后出发回调函数继续存数据。
资料太少,完整学FFT又太费精力,奋斗一天脑子快炸了。
找了一个半成品的mpy项目,只给固件不给源码,最关键的两步还都打成c语言方法了,废了,可算终于能跑了,就是效果不太好。
白嫖到了一个mpy的fft算法,效率位置,与numpy库的fft.fft比了一下,结果相同的。
总体项目思路:
fft必学必用,一句话就是时域转化成频域的方法,各种波皆能分解成N个正弦波之和。
取音频部分,有两个方法,一是直接ADC麦,一个线,精度差噪声高,貌似需要用dma能达到较高的采样,官方mpy无此方法,居然在4年前的一个什么第三方固件里有这个方法,官方是真懒,可能觉得用得少,就一直没加。二是I2S协议麦克,精度高,三根线,往上说的用I2S采集ADC是个特么什么方法还是没搞懂,至少MPY不行,哎学会加C的功能块多么重要。
有个什么定理 大致就是说采样频率必需比解析频率高两倍才能进行fft分析
采样频率直接交给i2了,设置多少都行,就是体积爆炸,还有关键一点就是buf的编码翻了半天没有,只能凭感觉蒙,我认为是2bytes 小端的有符号编码,根据实际大小猜的,完整的python to_bytes有个signed=True的参数,mpy没有,草,手动转好了。
最最最核心的fft变换到现在还是一脸懵,不过我肯定算法是输入数字列表,输出虚数列表,进行128长度的fft,单片机就很卡了,推荐64以下,不过这个fft的长度和采样率什么关系还是没搞懂,我采样了10000个数据,随便找连续的64个分析就行????
分析完,取模,也会是abs代表各频率大小,那么多少分频又是咋算的,还有归一化怎么弄,我每个采样点大小是0-32768,这个咋算呢。
9月5晚更新,通过用电脑端完整python的库,验证了我数据计算的想法全是对的,初步有点效果,目前就是杂音大,识别的频率不太shoul受控制,其他的基本都ok了。
此项目学习的东西:
1.mpy fft (仅代码实现和概念,原理公式压根没学)
2.wav pcm的编码方式
差的:具体采样控制 频谱识别控制
9.6更新:
哎,就怕专研啊,今天是研究fft的第三天,终于可以说看懂皮毛辣
涉及的点多知识杂,网上的教程都是一块一块的,经过三天的灌输,脑子中的理念终于连城串了,不容易,现在我不实际上代码都敢肯定自己掌握的没错,这次真的懂了。
1.采样率的选择,必须大于实际频率的2倍,常用音乐基本在3k以下,虽然说音乐是20k,也就是说那些超高频其实用的不多,为了让频谱更直观,这里直接采样率拉到6400,或者12800,至于为啥,是为了方便整除呀,不能整除的话,涉及到一个什么词来的,反正不好,影响效果。
2.fft采样点数,或者叫分频数,这个概念是给我添麻烦最多的,一会儿叫这个一会儿叫那个,什么鬼,现在我脑子里就认定他是fft数,就完了,一般取2的倍数,也是方便运算,esp32实测128比较慢,不流畅,所以我只能用64了,用64表示fft运算数组长度为64,然后频率也是分成64份呀,这个非常非常关键,6400hz/64 也就是说横轴每个单位代表100hz,然后由于什么对称,所以我是0-3200hz 间隔100hz的结果,也就是选64个点,由于对称,只能用32个点。
3 幅度计算,这个也头大好久,fft变换后,对每个复数取模,就完事儿啦,有个教程取对数是为了计算db,给我搞蒙了好久,还有个概念是直流分量,就是第一个数不能用,肯定很大,用来表示基础的,画图是直接删去,后面的要归一化,归一化就是除以fft数的一半
糙,就这么些东西搞了整整三天终于把代码层弄通啦,写代码我有强迫症,不放源码的,不懂具体实现的一概视为没掌握,老想搞懂。肯定还是有很多细节需要打磨的,这个有精力再弄吧。
最终上代码留念,有兴趣的可以看看,很乱,懒得整理了:
import array, time, gc
from math import *
from cmath import *
from machine import Pin, SoftI2C,I2S
from ssd1306 import SSD1306_I2C
i2c = SoftI2C(sda=Pin(13), scl=Pin(14))
oled = SSD1306_I2C(128, 64, i2c, addr=0x3c)
oled.show()
def FFT(P): #mpy比较精简的fft,需要math,cmath
n = len(P)
if n == 1: return P
ye = FFT(P[0::2])
yo = FFT(P[1::2])
y = [0]*n
w = exp(-1j*2*pi/n)
n2 = n//2
for j in range(n//2):
wj = w ** j
yow = [a*wj for a in yo]
y[j] = ye[j] + yow[j]
y[j+n2] = ye[j] - yow[j]
return y
bck_pin = Pin(23)
ws_pin = Pin(5)
sdin_pin = Pin(18)
audio_in = I2S(0,
sck=bck_pin, ws=ws_pin, sd=sdin_pin,
mode=I2S.RX,
bits=16,
format=I2S.MONO,
rate=6400,
ibuf=6400)
mic_samples = bytearray(64)
top, downstep = [0.]*32, 1 # 用于存放最上面的点
mark = 4
def i2s_callback(t):
cyd=64 #多少个采样点
start = time.ticks_ms()
oled.fill(0)
global num_read
num_read = audio_in.readinto(mic_samples)
fdlist=[]
#print(int.from_bytes(mic_samples[0:2],'little'))
for i in range(0,cyd):
twobits=mic_samples[2*i:2*i+2]
#print(twobits)
wa=int.from_bytes(twobits,'little')
if wa>32000:
wa=wa-65536
fdlist.append(wa)
#print(fdlist[0],fdlist[1])
y=FFT(fdlist)[0:32]
x = [abs(yy)/32 for yy in y] #归一化
ruler = 64 / (max(x[1:32]) + 0.001)
ruler = mark if ruler > mark else ruler
#print(x)
for i in range(0,30):
top[i] = top[i] - downstep / ruler if top[i] > x[i + 2] else x[i + 2]
if top[i] < 0: top[i] = 0
oled.fill_rect((i+1)*4, 63 - int(x[i+2] * ruler), 3, int(x[i+2] * ruler) + 1 , 1)
#oled.fill_rect((i+2)*4, 63 - int(top[i] * ruler), 2, 2, 1)
gc.collect()
end = time.ticks_ms()
fps = 1000/(end - start);
#oled.text("fps=" + str(round(fps,1)), 60, 0)
#print(fps)
oled.show()
audio_in.irq(i2s_callback)
num_read = audio_in.readinto(mic_samples)