在音视频编辑软件中直观的看到一个素材的音频波形后,就可以大致知道该素材音量的变化,这样方便我们各个位置打上关键帧进行音量调节或者添加音频特技。
音频采样数据在时序上具有波形分布的特征,声压距离标准值偏差越大,说明振动越剧烈,响度越大。本文依次介绍音频采样数据的获取,重采样算法,以及用OpenGL展示最终绘制的效果。
一、音频采样数据的获取
音频采样数据通常是通过编码后与视频编码数据一起封装在各种文件容器中,比如MP4,MXF等,也有部分是存储在wav文件中,通常不会直接以二进制文件(pcm)的形式提供。这里介绍一下通过ffmpeg命令行获取各种期望格式的音频采样数据。
../ffmpeg.exe -i "test.mp4" -vn -ar 44100 -ac 2 -f s16le "test.pcm"
//"test.mp4" 原素材
// vn 只要音频数据
// -ar:音频的采样率 44100
// -ac2:双声道
// -f:音频的数据存储格式 s16le, s代表有符号的16位数据
// "test.pcm" 输出文件
从素材中抽离音频并保存成wav格式的命令如下:
../ffmpeg.exe -i ."test.mp4" -f wav -ar 16000 "test.wav"
//-i "test.mp4" 输入的文件路径
//-f wav 输出wav格式的文件
//-ar 48000 采样率为48K
//"test.wav" 输出的文件名
二 、音频采样数据的处理
在各种视音频软件中对音频波形绘制时通常都需要进行重采样,以降低数据量提升效率。重采样的精度和绘制波形的窗口大小相关,简而言之就是试图用多少个像素点来绘制整个波形,比如在100*100的窗口中绘制所需要的数据就比在全屏(1920*1080)中绘制要低,当然对应的精度也要低一些。
在获取音频数据后需要先进行归一化处理,转化到 [-1, 1] 的浮点数区间内,各种精度的归一化计算如下所示:
1.8 bit 采样数据: 除以 128(2^8/2)
2.16 bit 采样数据: 除以 32678(2^16/2)
3.24 bit 采样数据: 除以 8388608(2^24/2)
4.32 bit 采样数据: 除以 2147483648(2^32/2)
对音频采样数据的处理通常有三种方式,这里分别介绍各自的特点和应用场景
(1) 呈现原始数据,将采样数据的每个采样点归一化后直接描绘出来。
这种方式不适合于描绘整个素材的波形,因为数据量太大了,会导致效率低下。比如一段 PCM 音频数据,48kHz 的采样率就会在每秒生成 48000 个采样点,如果我们要绘制这段音频的音量波形图,单声道的情况下1秒就要绘制 48000个点,五分钟的素材则有5x60x48000个数据点。这种方式适合于将波形图放大至某一比例时只绘制某一小段采样点时使用,比如在Audacity软件种放大到某一个比例时音频波形会呈现一个个独立的点。
(2) 根据绘制波形的像素点数求采样点数据的平均值
该方法发直接对每组采样点音频数据的值求平均值。假如我们在一个300x300的窗口中绘制一个三分钟时长(48KHz)音频波形,那么每一个像素点则对应3x60x48000/300个采样点的平均值。代码展示如下:
1.float sum = 0;
2.//求和, nb_samples 为3*60*48000/300 = 2880
3.for(int i = 0 ; i < nb_samples ; i++){
4. if(samples[i] < 0)
5. sum += -samples[i];
6. else
7. sum += samples[i];
8.}
9.//求平均
10.float average_point = sum / nb_samples;
(3)根据绘制波形的像素点数求采样点数据的均方根RMS
首先对每个样本数组元素的值求平方取和,然后计算均值,最后再求均平方根。其实例代码如下:
1.float squaredsum = 0;
2.//求平法和
3.for(int i = 0 ; i < nb_samples ; i++){
4. squaredsum += samples[i] * samples[i];
5.}
6.//求均值
7.float mean = squaredsum / nb_samples;
8.//求平方根
float rms_point = sqrt(mean);
三、使用OpenGL来绘制波形
之前介绍了音频采样数据的获取和处理算法后,这里借助OpenGL来展示各种算法处理后的音频波形图(测试用的pcm数据为16位单声道)。
3.1 准备三种方法的音频采样数据
vector<float> pcm_data_vec;
vector<float> pcm_avrdata_vec;
vector<float> pcm_rmsdata_vec;
void ReadPcmData()
{
FILE* fp = fopen("out.pcm", "rb+");
//从20秒开始获取音频数据
fseek(fp, 48000 * 20, SEEK_SET);
//获取三秒钟额音频数据
int totalsize(48000 * 3 );
//平均值和RMS求和数据初始化
float avrsample_sum(0.f);
float rmssamle_sum(0.f);
short pcm_data(0);
int curreadsize(0);
while (!feof(fp))
{
int size = fread(&pcm_data, 2, 1, fp);
curreadsize += size;
float pcmnormal = static_cast<float>(pcm_data) / 32768.f;
//第一种方式直接将每个采样点绘制出来
pcm_data_vec.push_back(pcmnormal);
//求平均值和RMS方式的采样点
avrsample_sum += pcmnormal;
rmssamle_sum += (pcmnormal*pcmnormal);
//SAMPLE_NUM 对多少个音频点重采样
//SAMPLE_NUM 1 :绘制所有采样点时
//SAMPLE_NUM 4 :平均值法和RMS的采样系数,这里设置设置为每四个原始采样点进行一次重采样
if (curreadsize % SAMPLE_NUM == 0)
{
pcm_avrdata_vec.push_back(avrsample_sum / SAMPLE_NUM);
float signal = avrsample_sum < 0.f ? -1.f : 1.f;
pcm_rmsdata_vec.push_back(signal*sqrt(rmssamle_sum / SAMPLE_NUM));
avrsample_sum = rmssamle_sum = 0.f;
}
if (curreadsize >= totalsize)
{
break;
}
}
fclose(fp);
}
3.2 绘制音频波形
void DrawWaveLine()
{
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
glLineWidth(1);
glColor3f(1.0, 0.0, 0.0);
float start_x = -1.f;
float start_y = 0.f;
float end_x = 0.f;
float end_y = 0.f;
float last_y = 0.f;
//三秒钟共有48000.f * 3.f的音频点, OpenGL的屏幕坐标为[-1,1]
//计算每次描绘点的步长
float step_x = 2.f / (48000.f * 3.f/SAMPLE_NUM);
//设置曲线为蓝色
glColor3f(0.f, 0.f, 1.f);
glBegin(GL_LINES);
//分别取三种数组中的重采样数据来绘制就可以了
//vector<float>::iterator itor = pcm_data_vec.begin();
//vector<float>::iterator avr_itor = pcm_avrdata_vec.begin();
vector<float>::iterator rms_itor = pcm_rmsdata_vec.begin();
for (; rms_itor != pcm_rmsdata_vec.end(); rms_itor++)
{
//确定折线坐标
start_x += step_x;
start_y = last_y;
end_x = start_x + step_x;
end_y = *rms_itor;
//绘制折线
glVertex2f(start_x, start_y);
glVertex2f(end_x, end_y);
last_y = end_y;
}
glEnd();
}
①绘制所有点的采样波形
#define SAMPLE_NUM 1
//vector<float>::iterator itor = pcm_data_vec.begin();
//vector<float>::iterator avr_itor = pcm_avrdata_vec.begin();
//vector<float>::iterator rms_itor = pcm_rmsdata_vec.begin();
在1920*1080的窗口中绘制
②平均值法绘制波形
#define SAMPLE_NUM 4
//vector<float>::iterator itor = pcm_data_vec.begin();
vector<float>::iterator avr_itor = pcm_avrdata_vec.begin();
//vector<float>::iterator rms_itor = pcm_rmsdata_vec.begin();
在540*270的窗口中绘制
③RMS 法绘制波形
#define SAMPLE_NUM 4
//vector<float>::iterator itor = pcm_data_vec.begin();
// vector<float>::iterator avr_itor = pcm_avrdata_vec.begin();
vector<float>::iterator rms_itor = pcm_rmsdata_vec.begin();
在540*270的窗口中绘制