音乐信号处理基础
作者:互联网
目录
使用雅马哈 VST 3 SDK 自行调用音色库
获取雅马哈旗下steinberg公司vst(2/3)插件开发工具包:(c++)
https://www.steinberg.net/en/company/developers.html
https://sdk.steinberg.net/index.php?sid=4e40f7ce3449dd801cbefa6dcb02a2e6
由于很少有人用Linux制作音乐,大部分音源插件不支持Linux,只能用Windows版本。SDK框架归纳为处理器和编辑器两部分,两者完全分离。这给编曲中自动化编写、远程控制提供方便。
类Steinberg::Vst::IComponent 和 Steinberg::Vst::IEditController 均从基类Steinberg::IPluginBase继承而来。…:IPluginBase传入参数Steinberg::Vst::IHostApplication。例如
tresult result = factory->createInstance(ID, Vst::IComponent::iid, (void**)&processorComponent);
if (processorComponent && (result == kResultOk))
{
// 初始化
res = (processorComponent->initialize(gStandardPluginContext) == kResultOk);
// 若没有成功分离出编辑控制器
if (processorComponent->queryInterface(Vst::IEditController::iid, (void**)&editController) != kResultTrue)
{
// 释放编辑器
FUID controllerCID;
// 寻找关联classID
if (processorComponent->getControllerClassId(controllerCID) == kResultTrue && controllerCID.isValid())
{
// 创建编辑器部分
result = factory->createInstance(controllerCID, Vst::IEditController::iid, (void**)&editController);
if (editController && (result == kResultOk))
{
// 初始化
res = (editController->initialize(gStandardPluginContext) == kResultOk);
// 成功
}
}
}
}
处理器构架如图所示:
使用VST插件协议调用juicy音源,如图所示:(代码过多,不贴)
用C语言导入\生成WAV格式音频文件
在python中十分简单直接导入pyaudio或pygame就完了,但在C中就不这么容易。
导入二进制音频文件,不仅需要提供采样率、采样类型、轨道数,还要提供样本自己的排列顺序(读取顺序)。WAV,AIFF,MP3等格式在文件头描述,程序以此获得文件读取方式。对没有任何标识的音频文件,设计程序来读取它是十分困难的。C语言程序库中已有向原始二进制音频文件写入读取信息的代码包,可直接使用。下面的程序写出简单的波形文件。
该外部文件暂时使用仅能读取WAV AIFF文件格式的文件。在GitHub中寻找ieee80.c,并根据提示更正几个指针错误直至VS不报错,如图所示。
在portsf.h文件中,使用enum定义音频声道的若干属性:
typedef enum { STDWAVE, MC_STD, MC_MONO, MC_STEREO, MC_QUAD,
MC_LCRS, MC_BFMT, MC_DOLBY_5_1, MC_WAVE_EX }
psf_channelformat;
定义文件读取格式的若干方式和采样类型:
typedef enum {
PSF_FMT_UNKNOWN = 0,
PSF_STDWAVE,
PSF_WAVE_EX,
PSF_AIFF,
PSF_AIFC
} psf_format;
typedef enum {
PSF_SAMP_UNKNOWN = 0,
PSF_SAMP_8, //还没支持
PSF_SAMP_16,
PSF_SAMP_24,
PSF_SAMP_32,
PSF_SAMP_IEEE_FLOAT
} psf_stype;
以上若干属性被添加到portsf.h头文件中,定义为PSF属性(把上面的合起来)。
typedef struct psf_props
{
long srate;
long chans;
psf_stype samptype;
psf_format format;
psf_channelformat chformat;
} PSF_PROPS;
虽然面向过程,但也可认为这是封装了一个API。在PSF_PROPS中只有5个元素。不需要知道音频文件的很多信息,就能编辑它。
上述枚举类信息归纳如下:
对于PSF_PROPS中部分元素的解释:
srate(Sample Rate),采样率:1秒中的样本点数。大多数音频系统使用22050(广播),44100(日常大多数),48000,96000,192000(专业)Hz的采样率。
chans(channels),声道数:1叫单声道(听力收音机),2个叫立体声(绝大多数),6个叫杜比(Dolby)5.1(部分电影院)。在多声道系统中,定义FrameRate,比如上图就是2。
samptype:(sample type),采样类型:理解成每个样本点所占的字节空间。8 bits现在使用yue;16 bits对应C中的short类型;32 bits float对应float类型,32-bit integer对应C中长整型。此外还有64 bits,64 bits float等,在此不支持,所以枚举里没有写。而WAVEFORMATENSIBLE用来指示每个存储单位是否有空余空间,例如24 bits是在4个字节里存储3个字节的数据,另一个空着。这样的现在几乎没有了。第一个变量PSF_SAMP_UNKNOWN指示音频文件的采样类型是否已知。
chformat:(channel format,声道格式)上面代码中给出了几个常用的声道格式。仅当psf_format是PSF_WAVE_EX(WAVEFORMATEXTENSIBLE)(波形可扩展)时,chformat才有选择;否则,chformat将采用STDWAVE(standard wave)格式。在chformat中,MC_BFMT是新近流行的高级编码格式,用于虚拟现实设备的声音定位仿真, 采用Ambisonic的B格式信号,不采用一个喇叭给一组样本,而是给每个声音成分一个三维坐标(使用特殊的麦克风收音获得),创建球状声场,从柱坐标看包括水平定位、距离和高度信息。一个喇叭一组样本可以直接传送给麦克风,而这种需要解码再传。至少8个喇叭才能回放此编码声音定位的全部信息。
初始化portsf库
这个库有一个初始化函数。portsf对读入的声音文件是按块读的,它内部维持着一个音频块,块的末尾规定了一个固定的读取长度,默认是64.psf_init保证这个块在初始化时是空的。类似于C++的构造和析构,在程序的最后应该调用一个psf_finish函数。psf_finish关闭portsf打开的所有声音文件,返回初始时的空状态。
#include "ieee80.h"
#include "portsf.h"
int psf_init(void);
int psf_finish(void);
#include "ieee80.h"
#include "portsf.h"
#include<stdio.h>
#include<iostream>
#define _CRT_SECURE_NO_WARNINGS
int main(void)
{
PSF_PROPS props;
int sf;
sf = psf_sndOpen("original.wav", &props, 0);
if (sf < 0) {
printf("错误:不能打开文件\n");
system("pause");
return 1;
}
printf("采样率:%d\n", props.srate);
printf("通道数:%d\n", props.chans);
system("pause");
return 0;
}
其中PSF是 Portable Sound Format,可移植声音格式。http://www.vgmpf.com/Wiki/index.php?title=PSF
如图,先后出现许多错误。一个个纠正:
修正到可以运行时,发现显示
摘取psf_sndOpen上半段:
int psf_sndOpen(const char *path, PSF_PROPS *props, int rescale)
{
int i, rc = 0;
PSFFILE *sfdat;
psf_format fmt;
char *fname = NULL;
/* RWD interesting syntax issue: I need the curlies, or break doesn't work properly */
for (i = 0; i < psf_maxfiles; i++) {
if (psf_files[i] == NULL)
break;
}
if (i == psf_maxfiles) {
return PSF_E_TOOMANYFILES;
}
sfdat = psf_newFile(NULL);
if (sfdat == NULL) {
return PSF_E_NOMEM;
}
sfdat->rescale = rescale;
sfdat->is_little_endian = byte_order();
fmt = psf_getFormatExt(path);
if (!(fmt == PSF_STDWAVE || fmt == PSF_WAVE_EX || fmt == PSF_AIFF || fmt == PSF_AIFC))
return PSF_E_BADARG;
if ((sfdat->file = fopen(path, "rb")) == NULL) {
DBGFPRINTF((stderr, "psf_sndOpen: cannot open '%s'\n", path));
return PSF_E_CANT_OPEN;
}
sfdat->filename = (char *)malloc(strlen(path) + 1);
if (sfdat->filename == NULL) {
return PSF_E_NOMEM;
}
strcpy(sfdat->filename, path);
sfdat->isRead = 1;
sfdat->nFrames = 0;
/* no need to calc header sizes */
switch (fmt) {
case(PSF_STDWAVE):
case(PSF_WAVE_EX):
rc = wavReadHeader(sfdat);
break;
case(PSF_AIFF):
/* some .aiff files may actually be aifc - esp if floats! */
case(PSF_AIFC):
rc = aiffReadHeader(sfdat);
/* try AIFC if AIFF fails */
if (rc < PSF_E_NOERROR) {
rewind(sfdat->file);
rc = aifcReadHeader(sfdat);
}
break;
default:
DBGFPRINTF((stderr, "psf_sndOpen: unsupported file format\n"));
rc = PSF_E_UNSUPPORTED;
}
可以看出,返回的错误标识符是PSF_E_CANT_OPEN,((sfdat->file = fopen(path, “rb”)) == NULL),是路径找不出来,fopen不允许用外部指针链接。添加音频路径就解决了问题。
同理地,可以获取采样方式(samptype)等。props就是指properties。
if (props.samptype == PSF_SAMP_8)
printf("sample type: 8bit\n");
else if (props.samptype == PSF_SAMP_16)
printf("sample type: 16bit\n");
写入新文件
函数原型是int psf_sndCreate(const char * path, const PSF_PROPS * props,int clip_floats, int minheader, int mode
const char *path, const PSF_PROPS props与sndOpen中的用法相同,只不过这里sndOpen加了一个const声明,表示音频总体属性写入文件中不可改变。
clip_floats用于确定浮点数按照何种方式写入文件,是不是要同时放大/缩小一倍数再写,与CSS中clip属性类似。有些样本点可能超出读取范围,这就要归一化。音频头文件中要有峰值(peak chunk)信息,但很多应用中的音频模块很可能没有这方面信息(读取头文件的时候就掠过去了),把clip_floats设成1可能比较常见,当然对于峰值小的文件也可以设高一点。
minheader:通常设为0,对源自UNIX系统的一些无法读取概要信息的文件,把值设为1,就只生成按要求格式的数据块。一般的应用设成0.
mode:提供一些读写控制,在portsf.h的psf_create_mode中定义成这样:
typedef enum {PSF_CREATE_RDWR,PSF_CREATE_TEMPORARY,PSF_CREATE_WRONLY}psf_create_mode
目前这个portsf库mode只支持PSF_CREATE_RDWR。其他modes以备特殊情况扩展postsf的功能用。
#include "ieee80.h"
#include "portsf.h"
#include<stdio.h>
#include<iostream>
#define _CRT_SECURE_NO_WARNINGS
int readfile(void)
{
PSF_PROPS props;
int sf;
sf = psf_sndOpen("example.wav", &props, 0);
if (sf < 0) {
printf("错误:不能打开文件\n");
system("pause");
return 1;
}
printf("采样率:%d\n", props.srate);
printf("通道数:%d\n", props.chans);
if (props.samptype == PSF_SAMP_8)
printf("sample type: 8bit\n");
else if (props.samptype == PSF_SAMP_16)
printf("sample type: 16bit\n");
system("pause");
return 0;
}
这就成功创建了一文件。
设置文件格式
例如,将Windows或安卓系统音频文件转移到Mac上,需要把格式从wav转化成aiff。通过命令
psf_format format;
format=psf_getFormatExt("soundtrack.wav")
getFormatExt就是提取字符串并分类的程序。
执行这个函数就可以了。它返回psf_format类型文件,可以直接将原始二进制音频文件分配到合适的PSF_PROPS结构。
props.format=format;
如过.wav头信息无法辨认,则返回PSF_PMT_UNKNOWN类型。
关闭声音文件(并通过peak chunk功能获取样本峰值)
用int psf_sndClose(int afd)
,返回值是读取失败对应的错误类型。
int psf_sndClose(int sfd);
typedef struct psf_chpeak {
float val;
unsigned long pos;
} PSF_CHPEAK;//portable sound format channel peak
long psf_sndReadPeaks(int sfd, PSF_CHPEAK peakdata[], long *peaktime);
使用python实现音乐播放、可视化和下载
播放
python用于音乐处理的librosa库已经给好了大多数函数。
import librosa
import sklearn
audio_path = 'D:\大二下\jupyter\WANNABE_ITZY.wav'
x,sr = librosa.load(audio_path, sr=44100)
import IPython.display as ipd
ipd.Audio(audio_path)
用上述图标可直接播放。
可视化
谱质心
谱质心是基于能量分布的频率一阶矩,理解成某时刻频率重心。
SC=∑1NE(n)∑n=1Nf(n)⋅E(n)=n=1∑Nf(n)⋅P(E(n))f(n)是短时傅里叶变换所得频率,P(E(n))是所得频率的倒数。
# 寻找谱质心
spectral_centroids = librosa.feature.spectral_centroid(x, sr=sr)[0]
spectral_centroids.shape
(775,)
# 时间变量
frames = range(len(spectral_centroids))
t = librosa.frames_to_time(frames)
# 归一化谱质心
def normalize(x, axis=0):
return sklearn.preprocessing.minmax_scale(x, axis=axis)
# 绘制图像
librosa.display.waveplot(x, sr=sr, alpha=0.4)
plt.plot(t, normalize(spectral_centroids), color='r')
时域波形
import librosa.display
import matplotlib.pyplot as plt
x,sr = librosa.load('D:\大二下学习文件\jupyter_deepLearning\example.wav')
#plot the signal
plt.figure(figsize = (14,5))
librosa.display.waveplot(x,sr = sr)
截取一部分并求过零点
n0= 57000
n1 = 57100
plt.figure(figsize = (14,5))
plt.plot(x[n0:n1])
plt.grid()
zero_crossings = librosa.zero_crossings(x[n0:n1],pad = False)
print(sum(zero_crossings))
滚降(系数)
python已有封装。
α=fNfΔ分子是超出奈奎斯特采样频率的带宽扩展量,分母是奈奎斯特带宽(从下图可见,鼓点处、清音处的滚降大,动态平稳、频率均衡处小)
spectral_rolloff = librosa.feature.spectral_rolloff(x+0.01,sr = sr)[0]
librosa.display.waveplot(x,sr = sr, alpha = 0.4)
plt.plot(t,normalize(spectral_rolloff),color = 'r')
梅尔倒频谱系数
之前已有讲述。cx[n]=M1m=1∑MY[m]cos(Mπn(m−1/2))
x,fs = librosa.load('D:\大二下学习文件\jupyter_deepLearning\example.wav')
librosa.display.waveplot(x,sr = sr)
mfccs = librosa.feature.mfcc(x,sr = fs)
print (mfccs.shape)
librosa.display.specshow(mfccs,sr = sr, x_axis = 'time')
使用tkinter构建网易云下载小程序
网易云非付费内容爬取器:驱动Edge浏览器(自己写驱动会更高端)进入网易云搜索相应界面,爬取列表中第一个音频地址、载入音频并存入相应文件夹中。这里给出一个最简单的爬虫程序和一个简单的tkinter GUI编程。
注意,要先在网易云音乐网页中将第一个对应音频链接的位置定位:
以上定位可通过如下方式获得(定位器):
req = driver.find_element_by_id('m-search')
a_id = req.find_element_by_xpath('.//div[@class = "item f-cb h-flag "]/div[2]//a').get_attribute("href")
在XML语言中寻找链接路径的方法可参见find_element_by_xpath
创建目录参见makedirs
这里的GUI需要tkinter添加文本。用text控件insert(插入文本)、see(滚动)、update(更新)等方法显示正在下载和已下载图样;在get_music_name函数中,首先从输入窗口获取名称,然后调用Edge驱动访问网易云音乐主页,通过'http://music.163.com/song/media/outer/url?id={}.mp3'.format(song_id)
搜到歌曲,通过上述定位器找到歌曲地址和歌名。注意到第一个函数传入的应该是字典类型(有了这种语句:song_id = item['song_id']
),那就创建一个字典后在函数体内调用song_load实现下载。在这之前,驱动就完成了任务,所以可以关闭驱动。
至于Tkinter的控件内容,应该根据实际情况试错和设计,界面编程相对还是比较简单的。(分别创建标签控件、输入框、列表框、按钮,并依次确定它们在主界面中的位置)
from tkinter import *
from selenium import webdriver
global entry
import os
from urllib.request import urlretrieve
#2.下载歌曲
def song_load(item):
song_id = item['song_id']
song_name = item['song_name']
song_url = 'http://music.163.com/song/media/outer/url?id={}.mp3'.format(song_id)
#创建文件夹
os.makedirs('music_netease',exist_ok=True)
path = 'music_netease\{}.mp3'.format(song_name)
#显示数据到文本框
text.insert(END,'歌曲:{},正在下载...'.format(song_name))
#文本框滚动
text.see(END)
#更新
text.update()
#下载
urlretrieve(song_url,path)
#显示数据到文本框
text.insert(END,'歌曲:{},下载完毕'.format(song_name))
#文本框滚动
text.see(END)
#更新
text.update()
#搜索
def get_music_name():
#获取输入的内容
name = entry.get()
url = 'https://music.163.com/#/search/m/?s={}&type=1'.format(name)
#搜索页面
option = webdriver.EdgeOptions()
option.add_argument('--headless')
#driver = webdriver.Edge(edge_options=option)
driver = webdriver.Edge('D:\\python\\msedgedriver')
driver.get(url)
driver.switch_to.frame('g_iframe')
#获取歌曲的id
req = driver.find_element_by_id('m-search')
a_id = req.find_element_by_xpath('.//div[@class = "item f-cb h-flag "]/div[2]//a').get_attribute("href")
song_id = a_id.split('=')[-1]
print(song_id)
song_name = req.find_element_by_xpath('.//div[@class="item f-cb h-flag "]/div[2]//b').get_attribute("title")
print(song_name)
#构造字典
item = {}
item['song_id'] = song_id
item['song_name'] = song_name
driver.quit()
#下载歌曲
song_load(item)
#get_music_name()
#形象工程
# 搭建界面
#创建画板
root = Tk()
#标题
root.title('网易云下载器')
#设置窗口大小
root.geometry('560x450+400+200')
#标签控件
label = Label(root,text = '输入要下载的歌曲:',font = ('华文行楷',20))
#标签定位
label.grid()
#输入框
entry = Entry(root,font = ('楷书',20))
#定位
entry.grid(row = 0,column = 1)
#列表框
text = Listbox(root,font = ('隶书',16),width = 50, heigh = 15)
text.grid(row = 1,columnspan = 2)
#点击按钮
button = Button(root,text = '开始下载',font = ('楷书',15),command=get_music_name)
button.grid(row=2, column=0,sticky=W)
button1 = Button(root,text = '退出程序',font = ('楷书',15),command=root.quit)
button1.grid(row=2, column=1,sticky=E)
#显示当前的界面内容
root.mainloop()
运行效果
进入music_netease文件夹,说明下载成功。
使用matlab分析参数
过零点
首先读入文件并画图。读取前8000个样本点。
>> a=fread(fp,8000);
>> plot(a)
>> fp=fopen('春天的脚步近了.m4a');
fseek(fp,60244,-1);
>> a=fread(fp,8000);
>> plot(a)
像刚才在python中做的一样,计算零交叉点的数目。8000稍多,这里不妨取第5000到第7000的样本点处理。(已事先在音频软件中观测到5000到7000的样本是在清音阶段)
figure;
for i=5000:5199
if (a(i)>128) && (a(i+1)<128)
x=x+1;
else x=x;
end
end
disp('number of zero crossing='); disp(x);
再在音频软件中定位到春天an处,(浊音)作出这附近的曲线:
fseek(fp,65042,-1);
b=fread(fp,2000);
plot(b);
用相同的方法,得到零交叉点数目,得到只有上段的一半。
再在音频软件中定位到脚步的j处(清音),作出这附近的曲线:
fseek(fp,6724,-1);
c=fread(fp,200);
plot(c);
可以看出,它的振幅变化幅度较浊音更大。
全(频)带低(频)带能量比
这个不用解释也比较清楚。显然浊音部分这个比大点,而清音部分这个比小点。计算低频带的界限是1000Hz。下面的程序按照惯常做法,取帧,(100个样本点一帧),共800个,
clear all;
fp=fopen('狗血.wav');
fseek(fp,44,-1);
a=fread(fp);
a=a-128;
for j =1:800
fseek(fp,44+100*j,-1);
a=fread(fp,100);
a=a-128;
for i =1:50
b=abs(fft(a));
end
sum(j)=0;
for i = 1:12
sum(j)=sum(j)+b(i);
end
sum1(j)=0;
for i =1:50
if sum1(j)==0
sum1(j)=1;
end
end
s(j)=sum(j)/sum1(j);
end
plot(s);
title('低频和全频带的比');
xlabel('片段序号');
ylabel('比值');
这几个字是“别堆砌怀念让剧情变得狗血”。这几个字发音较典型,有“情”“砌””血’“别”“变”等典型字,往后这几个字可以用来验证更多的分析,这里用它分析清音和浊音的分辨也比较好的选择。片段8000左右的高峰是情字,片段12000左右的高峰是血字,200,1500一带堆着“别堆砌”三个字。
归一化自协方差
C1=∑n−1NS(n)∑n−0N−1S2(n)∑n−1NS(n)S(n−1)
这个量规定了相邻音乐/语音片段的相关性。讲人话,C1指这时求的相邻单位时间延迟的自协方差。对于浊音,有大量低频信号能量,在浊音附近是的信号高度自相关的,这个值很大(讲人话,低频信号说明信号的乱起八糟的高频谐波占比比较少,信号比较“单纯”,相互的依赖性比较强);而在清音区域自协方差就很小,在寂静区域会更小(讲人话,就是噪声基本上无法预测、是独立产生的)。与之类似,在语音信号中通常再引入一个叫谱倾斜的东西,定义成
∑i=1s2(i)∑i=1Ns(i)s(i−1)
可以用matlab对“春天来了”做差分,求出它的谱倾斜度(spectrum tilt)(当然,需要事先录制"春天来了"4字)
>> fp=fopen('录音 (5).m4a');
%fseek(fp,800,-1);
a=fread(fp,400,'short');
a=a-128;
subplot(2,1,1);plot(a);
xlabel('sample no.');ylabel('amplitude');
for j=1:200,
fseek(fp,4*j,-1);
a=fread(fp,100);
a=a-128;
sum(j)=0;
for i=2:100,
sum(j)=sum(j)+(a(i)*a(i-1));
end
sum1(j)=0;
for i=2:100,
if a(i)==0,
a(i)=0.1;
end
sum1(j)=sum1(j)+a(i)*a(i);
end
s(j)=sum(j)/sum1(j);
end
subplot(2,1,2);plot(s);
得到的是下图。
可以看出,由于我说的比较波澜不惊,波形很规整的了。音频信号的自相关系数(谱倾斜度和自相关函数实际上差不多是同一种量度)在浊音处是十分高的;由于春和天是连在一起的,所以得到的相关性都比较大(接近1);而后来说来字的时候,音调和共振峰都发生了比较强烈的改变,所以它的相关性急剧下降。
预相位能量比
可以求归一化的预相位能量比区分浊音和清音片段。对于浊音,相邻样本之间的变化值很小,而对于清音部分这个分子很大。Pr=∑i=1s2(i)∑i=1N∣s(i)−s(i−1)∣
求所谓预相位能量的程序如下:
clear all;
fp = fopen('狗血.wav');
[p,fs]=audioread('狗血.wav');
fseek(fp,44,-1);
a=fread(fp);
a=a-128;
subplot(2,1,1);
plot(p);
title('plot of 狗血');
xlabel('sample no.');
ylabel('amplitude');
for j =1:12250
fseek(fp,44+100*j,-1);
a=fread(fp,100);
a=a-128;
sum(j)=0;
for i=2:100
sum(j)=sum(j)+abs(a(i)-a(i-1));
end
sum1(j)=0;
for i =2:100
if a(i)==0
a(i)=0.1;
end
sum1(j)=sum1(j)+a(i)*a(i);
end
s(j)=sum(j)/sum1(j);
end
subplot(2,1,2);
plot(s);
title('预相位能量比');
xlabel('segment number');
ylabel('预相位能量比');
上面不怎么好看,下面直接把节奏整出来比对一下
样本点过多,这样对应着看验证得不是很成功,只在静默时这个比有明显下降,在有声阶段并不能很好表征清音和浊音的分化。
标签:fp,song,PSF,信号处理,音乐,psf,基础,format,sum 来源: https://blog.csdn.net/Wang_Runlin/article/details/105924212