libsndfile库的文件与内存读写操作(C++ Qt)

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 (主要用来看波形和我的处理结果是否一致;以及转换格式)

由于都是用的盗版,就不放链接了

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龚建波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值