一:前言
刚毕业不久,接到的第一个项目就是音频采集。
要求在树莓派Linux系统上用QT进行音频采集,然后实时编码发送给window上的一个程序。
完成项目期间搜集了许多相关资料,苦于网上信息太过零散,大多都是存成文件传输,几乎没有实时性相关的完整的流程代码。FAAC编码器相关的资料也寥寥无几,在使用期间踩了不少坑。
因此在这里分享一下自己项目中的一些要点和部分代码,以及一些注意事项。可能代码比较稚嫩,有什么问题希望大佬指点。
二:音频采集的流程图如下:
三:具体代码
因为本次只讲采集,所以自己写了一个QT采集的代码,为了更好理解,文件不包含FAAC编码。编码将在下次博客中加入。
下面程序能实现实时采集PCM的数据,并存到文件中。
定义了一个DataPool内存空间,用于存放音频数据。如果存满了,从首地址开始将新数据覆盖原来数据。在槽函数中,一边从设备读取数据存到DataPool,一边从DataPool拿来写入文件。
其实想写文件完全没必要这么复杂,也可以这么操作:
QFile inputFile; //创建QFile文件句柄
inputFile.setFileName("/home/pi/DeskTop/New.Record/PCM 16000 16.PCM"); //设置文件存储位置
inputFile.open(QIODevice::WriteOnly | QIODevice::Truncate); //以只写的形式打开。
audioInput->start(&inputFile);
然后运行,就能开始启动录音并直接写到文件里,连槽函数都用不到,和下面程序功能完全一样!
下面这么做的意义,是因为后续音频编码要从DataPool不断取数据,干脆在这里提前贴上,让大家更好地理解槽函数内的操作。
.h 头文件
/*本程序实现简单的QT音频录音并实时写入文件*/
/*通过学习本例可以学习QT音频实时采集的流程*/
/* [email protected] 姓值钱的金三岁 */
#ifndef MYRECORD_H
#define MYRECORD_H
#include "myserver.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class myRecord : public QMainWindow
{
Q_OBJECT
public:
explicit myRecord(QWidget *parent = 0);
void StartRecord(); //开始录音
int temp;
int end; //标志位 尾部 用于标志DataPool数据尾部
int offset; //标志位 头部
int PoolSize = 4096*10; //开辟的内存大小
char *DataPool = new char[PoolSize]; //创建内存空间 用于暂时存放音频原始数据
int DataReadAll; //读到的数据总量
int DataWrite; //已写入文件的数据量
int nBytesRead; //从设备中一次读到的数据长度
signals:
public slots:
void captureDataFromDevice(); //槽函数 数据处理函数(包括后期 采集 编码 传输 等都在此函数中实现)
void on_Record_stop(); //停止录音 连接了一个ui按钮
void Start_Record(); //开始录音 连接了一个ui按钮
private:
QAudioInput *audioInput; //音频输入
QIODevice *device; //音频IO设备
QAudioFormat format; //音频采集设置
};
#endif // MYRECORD_H
.cpp 源文件
#include "myrecord.h"
myRecord::myRecord(QWidget *parent) :
QMainWindow(parent)
{
/*音频采集设置*/
format.setSampleRate(16000);
format.setChannelCount(1);
format.setSampleSize(16);
format.setCodec("audio/pcm");
format.setByteOrder(QAudioFormat::LittleEndian);
format.setSampleType(QAudioFormat::SignedInt);
QAudioDeviceInfo info(QAudioDeviceInfo::defaultInputDevice());
if (!info.isFormatSupported(format))
{
qWarning() << "Error";
format = info.nearestFormat(format);
}
DataReadAll = 0;
nBytesRead = 0;
temp = 0;
end = 0;
offset = 0;
DataWrite = 0;
}
void myRecord::Start_Record()
{
StartRecord();
}
void myRecord::StartRecord()
{
fout = fopen("/home/pi/Desktop/New/Record/_pcm 16000 16.pcm", "wb"); //打开文件位置
audioInput = new QAudioInput(format, this); //创建音频
device = audioInput->start(); //开始录音
qDebug() << "Start record !";
connect(device, SIGNAL(readyRead()), this, SLOT(captureDataFromDevice())); //连接信号和槽
QTimer::singleShot(10000, this, SLOT(on_Record_stop())); //设置定时器,十秒后结束录音
}
void myRecord::on_Record_stop() //停止录音 关闭文件 删除内存空间 关闭音频
{
audioInput->stop();
fclose(fout);
delete DataPool;
delete audioInput;
std::cout << "Stop Record" << std::endl;
}
void myRecord::captureDataFromDevice()
{
temp = end - offset;
int Readable = audioInput->bytesReady(); //bytesReady()用于查看可以麦克风中可读的数据量
std::cout << "Readable :" << Readable << std::endl;
if (end <= PoolSize)
{
if (end + 4096read(DataPool + end, 4096);
offset += temp;
end += nBytesRead;
fwrite(DataPool + offset, 1, (end - offset), fout);
DataWrite += (end - offset);
}
/*DataPool内存大小有限,如果出现末尾空间不够读入的情况,先创建一个内存fin暂存数据,
把fin中一部分数据写入DataPool尾部剩余空间,剩下数据覆盖DataPool头部的原数据*/
else
{
char* fin = new char[4096];
nBytesRead = device->read(fin, 4096);
int remain = PoolSize - end;
if (remain < nBytesRead)
{
int i = end;
memcpy(DataPool + end, fin, remain);
memcpy(DataPool, fin + remain, nBytesRead - remain);
offset = 0;
end = nBytesRead - remain;
fwrite(DataPool + i, 1, remain, fout);
fwrite(DataPool + offset, 1, end, fout);
DataWrite += (remain + end);
}
else
{
memcpy(DataPool + end, fin, nBytesRead);
offset += temp;
end += nBytesRead;
fwrite(DataPool + offset, 1, end - offset, fout);
DataWrite += (end - offset);
}
delete fin;
}
DataReadAll += nBytesRead;
std::cout << "DataReadAll :" << DataReadAll << std::endl;
std::cout << "DataWrite :" << DataWrite << std::endl;
std::cout << "nBytesRead :" << nBytesRead << std::endl;
std::cout << "Temp :" << temp << std::endl;
}
}
四:注意事项
(1)麦克风有它自带的缓存空间。数据没读的话会存在那里。但是空间非常小,如果满了,新数据会覆盖原数据。
(2) .pro文件中 QT+=multimedia 记得写好,否则一些头识别不了。
(3) 想把数据存到文件,文件的打开方式必须是二进制方式打开(b),否则录下来的音频都是噪音。
(4) readyRead()信号,解释为“每次麦克风有新数据传入,便触发一次”,因此槽函数是不断刷新的(而不是只执行一次)。在新的信号来之前,槽函数内的代码必须运行完。我在树莓派上,槽函数内采集+写文件+编码+传输,完全来得及。所以大部分情况下不用担心来不及处理。
(5)audioInput->bytesReady()可以看麦克风缓存中可读多少数据。但是有一个很奇怪的现象,我在槽函数外开启录音,另外一个线程调用bytesReady(),返回值为0。但是如果这个线程读一点数据,返回值就有了。
(6)device->read()中第二个参数,是设置读数据的长度。但是不知道是我QT版本问题还是其他问题,这个参数并不有效。比如我设置为2048,bytesReady()中表明麦克风中有4096个数据,最后读来的数据只有1367而不是2048。
这个问题困扰我很久,本来只要读够一帧4096长度数据直接送到编码器就行了,现在这个办法无效。这也是为什么我开DataPool存数据的原因。