0.前言
本文内容接上一篇安装:https://blog.csdn.net/gongjianbo1992/article/details/98995983
这个库用起来还算简单。由于专利原因该库还不支持MP3格式(文档显示目前仅支持wav、aiff、au),之后我准备换ffmpeg来处理我的数据。由于我的音频知识匮乏,所以本文只贴了一些操作的代码段,作为学习记录。
使用方式可以参考clone下来的代码目录examples和programs:https://github.com/erikd/libsndfile.git
此外,作者还提供了文档,路径为/libsndfile/doc/api.html。
(2019-8-26)修正:之前我read时第二个参数超过了缓冲区大小,导致异常,本以为是库的问题,还好库作者纠正了我代码里的错误。如果各位发现我代码还有错误,请指正,谢谢。
1.音频文件读写
如果使用C++的话,可以用作者封装的SndfileHandle类(sndfile.hh头文件)来完成大部分操作。
对于音频数据,可以从该网站获取:https://samples.mplayerhq.hu/A-codecs/libsndfile-samples/
下面是一个简单的读取音频文件代码(使用前先配置好include和lib),功能为获取音频信息即读取所有PCM帧(别的格式如GSM610也会先解析成PCM):
#include <iostream>
#include "sndfile.hh"
using namespace std;
int main()
{
SndfileHandle snd(R"(C:\Users\zhaozhao\Desktop\test\wav-pcm16.wav)");
//【1】获取文件信息
cout << "帧长:" << snd.frames() << endl;//帧数为每个通道单独的帧数之和
cout << "通道数:" << snd.channels() << endl;
cout << "格式信息:" << snd.format() << endl; //格式的枚举分量参见文档api部分
//【2】读取PCM全部数据
//pcm-16每个通道每帧为一个short的长度
short *buffer = new short[snd.frames()*snd.channels()]; //帧数为每个通道单独的帧数之和
int read_count = 0;
while (read_count < snd.frames()*snd.channels()) {
//注意读取的长度不要超过缓冲区大小,避免越界
const int read_limit = (snd.frames()*snd.channels() - read_count) > 1024
? 1024
: (snd.frames()*snd.channels() - read_count);
const int read_len = snd.read(buffer+read_count, read_limit);
read_count += read_len;
}
cout << "read count(short):" << read_count;
system("pause");
return 0;
}
在原工程programs目录有个 sndfile-info的程序,通过命令行传入文件路径可以得到音频文件的信息(我是通过MSVC2017打开CMake文件来生成的)。
下面是一个简单的写文件的代码(感觉只是加了个头?)。
#include <iostream>
#include "sndfile.hh"
using namespace std;
static const int Buffer_Len = 1024 * 10+100;
int main()
{
const char *filepath = R"(C:\Users\zhaozhao\Desktop\test\create_pcm16.wav)";
const int format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
const int channels = 2;
const int samplerate = 11025;
SndfileHandle snd(filepath, SFM_WRITE, format,channels,samplerate);
const short buffer[Buffer_Len*channels] = {0};
int write_count = 0;
while (write_count < Buffer_Len*channels) {
//计算每次写入的长度,避免越界
const int write_limit = (Buffer_Len*channels - write_count) > 1024
? 1024
: (Buffer_Len*channels - write_count);
const int write_len = snd.write(buffer, write_limit);
write_count += write_len;
}
system("pause");
return 0;
}
2.内存读写,通过构造SF_VIRTUAL_IO
相对于对文件进行操作,我对内存数据的读写更关心,该库可以通过构造SF_VIRTUAL_IO结构体来完成,结构体定义如下:
/* Virtual I/O functionality. */
typedef sf_count_t (*sf_vio_get_filelen) (void *user_data) ;
typedef sf_count_t (*sf_vio_seek) (sf_count_t offset, int whence, void *user_data) ;
typedef sf_count_t (*sf_vio_read) (void *ptr, sf_count_t count, void *user_data) ;
typedef sf_count_t (*sf_vio_write) (const void *ptr, sf_count_t count, void *user_data) ;
typedef sf_count_t (*sf_vio_tell) (void *user_data) ;
struct SF_VIRTUAL_IO
{ sf_vio_get_filelen get_filelen ;
sf_vio_seek seek ;
sf_vio_read read ;
sf_vio_write write ;
sf_vio_tell tell ;
} ;
typedef struct SF_VIRTUAL_IO SF_VIRTUAL_IO ;
其实就是定义几个回调函数接口来完成读写操作,但是回调函数需要我们自己去实现,其他方面和读写文件基本一样。
这一节的代码我是在Qt中写的。首先,构造一个自定义的结构体,作为操作的对象:
struct AudioPacket
{
qint64 offset; //为了seek
QByteArray data; //保存数据,count()返回字节长度
};
接下来构造我们的回调函数:
//typedef long long sf_count_t
//所以用qint64来表示更清楚点
qint64 vioFileLength(void *user_data)
{
AudioPacket* packet=(AudioPacket*)user_data;
qDebug()<<"vioFileLength"<<packet->data.count();
return packet->data.count();
}
qint64 vioFileTell(void *user_data); //声明,下面要用
qint64 vioFileSeek(qint64 offset, int whence, void *user_data)
{
qDebug()<<"vioFileSeek offset"<<offset<<whence;
AudioPacket* packet=(AudioPacket*)user_data;
//qDebug()<<"packet/offset"<<packet->offset;
qint64 position = 0;
switch (whence)
{
case SEEK_SET :
position = offset;
break;
case SEEK_CUR :
position = vioFileTell(user_data) + offset;
break;
case SEEK_END :
position = packet->data.count() + offset - 1;
break;
default :
return (-1);
break;
}
if (position >= packet->data.count())
position = packet->data.count() - 1;
else if (position < 0)
position = 0;
packet->offset=position;
//qDebug()<<"seek/position"<<position;
return position;
}
qint64 vioFileRead(void *ptr, qint64 count, void *user_data)
{
qDebug()<<"vioFileRead count"<<count;
AudioPacket* packet=(AudioPacket*)user_data;
//qDebug()<<"packet/offset"<<packet->offset;
//qint64 offset = vioFileTell(user_data);
if(packet->offset>=packet->data.count()){
return 0;
}
if(packet->offset+count>packet->data.count()){
count = packet->data.count()-packet->offset;
}
memcpy((char*)ptr,packet->data.data()+packet->offset,count);
//qDebug()<<"read count"<<count;//<<QByteArray(packet->data.data()+packet->offset,count).toHex(' ');
packet->offset+=count;
return count;
}
qint64 vioFileWrite(const void *ptr, qint64 count, void *user_data)
{
AudioPacket* packet=(AudioPacket*)user_data;
qDebug()<<"vioFileWrite"<<count<<packet->offset;
memcpy(packet->data.data()+packet->offset,(char*)ptr,count);
//if(packet->offset==0) qDebug()<<"head:"<<packet->data.left(60).toHex(' ');
packet->offset+=count;
return count;
}
qint64 vioFileTell(void *user_data)
{
AudioPacket* packet=(AudioPacket*)user_data;
//return ftell(cpfInfos->filePtr)-cpfInfos->StartOffset;
return packet->offset;
}
因为没用充分测试,所以 可能有bug。下面开始进行读取操作(因为没有音频流来测试,我先读文件到内存再进行处理)。
QString fileName = "filepath/wav-pcm16.wav";
QByteArray rawdata;
QFile rawfile(fileName);
if(rawfile.open(QIODevice::ReadOnly)){
rawdata=rawfile.readAll();
rawfile.close();
qDebug()<<"read close"<<rawdata.count();
}
SF_VIRTUAL_IO vio; //构建SF_VIRTUAL_IO结构并将回调函数赋值给他
vio.get_filelen=&vioFileLength;
vio.seek=&vioFileSeek;
vio.read=&vioFileRead;
vio.write=&vioFileWrite;
vio.tell=&vioFileTell;
AudioPacket packet{
0,rawdata //rawdata为内存中我们要处理的数据
};
SndfileHandle snd(vio,&packet);
qDebug()<<"sample:"<<snd.samplerate()<<"channel:"<<snd.channels()<<"format:"<<snd.format();
qDebug()<<snd.strError();
short *buffer = new short[snd.frames()*snd.channels()]; //帧数为每个通道单独的帧数之和
int read_count = 0;
while (read_count < snd.frames()*snd.channels()) {
//注意读取的长度不要超过缓冲区大小,避免越界
const int read_limit = (snd.frames()*snd.channels() - read_count) > 10240
? 10240
: (snd.frames()*snd.channels() - read_count);
const int read_len = snd.read(buffer+read_count, read_limit);
read_count += read_len;
}
qDebug()<<"read_count:"<<read_count;
如果需要播放解出来的PCM数据 ,可以借助Qt的QAudioFormat和QAudioOutput来完成,下面是参考代码段:
QBuffer outbuffer;
outbuffer.setData((char*)buffer,read_count*2);
outbuffer.open(QIODevice::ReadWrite);
QAudioFormat audioFormat;
audioFormat.setCodec("audio/pcm");
audioFormat.setByteOrder(QAudioFormat::LittleEndian);
audioFormat.setSampleRate(snd.samplerate());
audioFormat.setChannelCount(snd.channels());
audioFormat.setSampleSize(16); //这个根据format设置
audioFormat.setSampleType(QAudioFormat::SignedInt); //根据format设置
QAudioOutput *audio = new QAudioOutput(QAudioDeviceInfo::defaultOutputDevice(), audioFormat );
audio->start(&outbuffer);
目前连续音频流的播放我还没解决,不过取到PCM数据之后就可以绘制波形了。感觉Qt绘制还是挺快的,两万条线还没卡。
3. 推荐工具
查看二进制数据:Beyond Compare
音频处理软件:Adobe Audition (主要用来看波形和我的处理结果是否一致;以及转换格式)
由于都是用的盗版,就不放链接了