在上一篇 Qt 之 WAV文件解析 中详细地分析了wav格式文件的文件头信息。通过QAudioInput实现录音功能,但是录音生成的文件并不能用播放器打开,就算更改后缀名也无法识别(有时候下载的一些音频文件通过修改文件名可以播放)。在Qt助手中将录音生成的文件保存为.raw格式,那么这个raw到底是什么格式呢?
其实看raw字面的意思是原始的、未处理的、未加工的,从此看来QAudioInput 生成的音频文件未经过处理,即文件保存的数据全部为音频数据,没有文件头,播放器识别不了。好了,既然知道这个原因导致播放器播不了,那么我们就手动给.raw文件添加上头信息,转为wav格式,这样不仅可以通过QAudioOutput来播放,同时播放器也能够播放该音频文件。
这里主要是通过QAudioInput来生成音频文件,录音结束后,将.raw音频文件转为.wav格式文件,在 Qt 之 WAV文件解析 中介绍了wav文件头的数据结构,这里我们只要将这个结构的数据加在.raw文件的头部即可,代码中通过addWavHeader方法将.raw文件转成.wav文件。播放音频文件利用QAudioOutput类即可,既可播放.raw文件也可以播放.wav文件。我们这里就直接播放重新生成的.wav格式的音频文件。
仔细看代码有点多,其实实现很简单,这里我添加的一些代码是用来实现一个简单完整的小录音机功能。
#include "myaudioinput.h"
#include
#include
#include
#define RAW_RECORD_FILENAME "F:/audio/test.raw" // 录音文件名;
#define WAV_RECORD_FILENAME "F:/audio/test.wav" // 录音文件转wav格式文件名;
const qint64 TIME_TRANSFORM = 1000 * 1000; // 微妙转秒;
struct WAVFILEHEADER
{
// RIFF 头
char RiffName[4];
unsigned long nRiffLength;
// 数据类型标识符
char WavName[4];
// 格式块中的块头
char FmtName[4];
unsigned long nFmtLength;
// 格式块中的块数据
unsigned short nAudioFormat;
unsigned short nChannleNumber;
unsigned long nSampleRate;
unsigned long nBytesPerSecond;
unsigned short nBytesPerSample;
unsigned short nBitsPerSample;
// 数据块中的块头
char DATANAME[4];
unsigned long nDataLength;
};
MyAudioInput::MyAudioInput(QWidget *parent)
: QWidget(parent)
, m_isRecord(false)
, m_isPlay(false)
, m_RecordTimerId(0)
, m_RecordTime(0)
{
ui.setupUi(this);
// 录音,播放 等按钮 信号槽;
connect(ui.pButtonRecord, SIGNAL(clicked()), this, SLOT(onStartRecord()));
connect(ui.pButtonStopRecord, SIGNAL(clicked()), this, SLOT(onStopRecording()));
connect(ui.pButtonPlay, SIGNAL(clicked()), this, SLOT(onPlay()));
connect(ui.pButtonStopPlay, SIGNAL(clicked()), this, SLOT(onStopPlay()));
// 输出当前设备支持的音频编码格式;
QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice());
qDebug()<< "AudioDevice supportedCodecs : " << info.supportedCodecs();
}
MyAudioInput::~MyAudioInput()
{
}
void MyAudioInput::onStartRecord()
{
// 如果正在播放则停止播放;
if (m_isPlay)
{
onStopPlay();
}
// 如果当前没有开始录音则允许录音;
if (!m_isRecord)
{
// 判断本地设备是否支持该格式
QAudioDeviceInfo audioDeviceInfo = QAudioDeviceInfo::defaultInputDevice();
// 判断本地是否有录音设备;
if (!audioDeviceInfo.isNull())
{
m_isRecord = true;
destinationFile.setFileName(RAW_RECORD_FILENAME);
destinationFile.open(QIODevice::WriteOnly | QIODevice::Truncate);
// 设置音频文件格式;
QAudioFormat format;
// 设置采样频率;
format.setSampleRate(8000);
// 设置通道数;
format.setChannelCount(1);
// 设置每次采样得到的样本数据位值;
format.setSampleSize(16);
// 设置编码方法;
format.setCodec("audio/pcm");
// 设置采样字节存储顺序;
format.setByteOrder(QAudioFormat::LittleEndian);
// 设置采样类型;
format.setSampleType(QAudioFormat::UnSignedInt);
// 判断当前设备设置是否支持该音频格式;
if (!audioDeviceInfo.isFormatSupported(format))
{
qDebug() << "Default format not supported, trying to use the nearest.";
format = audioDeviceInfo.nearestFormat(format);
}
// 开始录音;
m_audioInput = new QAudioInput(format, this);
m_audioInput->start(&destinationFile);
// 开启时钟,用于更新当前录音时间;
if (m_RecordTimerId == 0)
{
m_RecordTimerId = startTimer(100);
}
}
else
{
// 没有录音设备;
QMessageBox::information(NULL, tr("Record"), tr("Current No Record Device"));
}
}
else
{
// 当前正在录音;
QMessageBox::information(NULL, tr("Record"), tr("Current is Recording"));
}
}
void MyAudioInput::onStopRecording()
{
// 当前正在录音时,停止录音;
if (m_isRecord)
{
// 关闭时钟,并初始化数据;
killTimer(m_RecordTimerId);
m_RecordTime = 0;
m_RecordTimerId = 0;
m_isRecord = false;
ui.labelTime->setText(QString("Idle : %1/S").arg(m_RecordTime));
if (m_audioInput != NULL)
{
m_audioInput->stop();
destinationFile.close();
delete m_audioInput;
m_audioInput = NULL;
}
// 将生成的.raw文件转成.wav格式文件;
if (addWavHeader(RAW_RECORD_FILENAME, WAV_RECORD_FILENAME) > 0)
QMessageBox::information(NULL, tr("Save"), tr("RecordFile Save Success"));
}
}
void MyAudioInput::onPlay()
{
// 当前没有录音才播放;
if (!m_isRecord)
{
// 如果正在播放则关闭播放,准备重新播放;
if (m_isPlay)
{
onStopPlay();
}
m_isPlay = true;
sourceFile.setFileName(WAV_RECORD_FILENAME);
sourceFile.open(QIODevice::ReadOnly);
// 设置播放音频格式;
QAudioFormat format;
format.setSampleRate(8000);
format.setChannelCount(1);
format.setSampleSize(16);
format.setCodec("audio/pcm");
format.setByteOrder(QAudioFormat::LittleEndian);
format.setSampleType(QAudioFormat::UnSignedInt);
QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice());
qDebug() << info.supportedCodecs();
if (!info.isFormatSupported(format))
{
qWarning() << "Raw audio format not supported by backend, cannot play audio.";
return;
}
m_audioOutput = new QAudioOutput(format, this);
connect(m_audioOutput, SIGNAL(stateChanged(QAudio::State)), this, SLOT(handleStateChanged(QAudio::State)));
m_audioOutput->start(&sourceFile);
// 开启时钟,用于更新当前播放时间;
if (m_RecordTimerId == 0)
{
m_RecordTimerId = startTimer(100);
}
}
else
{
// 当前正在录音;
QMessageBox::information(NULL, tr("Record"), tr("Current is Recording"));
}
}
void MyAudioInput::onStopPlay()
{
// 如果当前在播放则停止播放;
if (m_isPlay)
{
// 关闭时钟,并初始化数据;
killTimer(m_RecordTimerId);
m_RecordTime = 0;
m_RecordTimerId = 0;
m_isPlay = false;
ui.labelTime->setText(QString("Idle : %1/S").arg(m_RecordTime));
if (m_audioOutput != NULL)
{
m_audioOutput->stop();
sourceFile.close();
delete m_audioOutput;
m_audioOutput = NULL;
}
}
}
// 播放状态更新;
void MyAudioInput::handleStateChanged(QAudio::State state)
{
switch (state) {
case QAudio::IdleState:
// Finished playing (no more data)
onStopPlay();
break;
case QAudio::StoppedState:
// Stopped for other reasons
if (m_audioOutput->error() != QAudio::NoError) {
// Error handling
}
break;
default:
// ... other cases as appropriate
break;
}
}
// 时钟事件;
void MyAudioInput::timerEvent(QTimerEvent *event)
{
if (event->timerId() == m_RecordTimerId)
{
QString strState;
if (m_isRecord)
{
strState = "Recording";
m_RecordTime = m_audioInput->elapsedUSecs() / TIME_TRANSFORM;
}
else if (m_isPlay)
{
strState = "Playing";
m_RecordTime = m_audioOutput->elapsedUSecs() / TIME_TRANSFORM;
}
ui.labelTime->setText(QString("%1 : %2/S").arg(strState).arg(m_RecordTime));
}
}
// 将生成的.raw文件转成.wav格式文件;
qint64 MyAudioInput::addWavHeader(QString catheFileName , QString wavFileName)
{
// 开始设置WAV的文件头
// 这里具体的数据代表什么含义请看上一篇文章(Qt之WAV文件解析)中对wav文件头的介绍
WAVFILEHEADER WavFileHeader;
qstrcpy(WavFileHeader.RiffName, "RIFF");
qstrcpy(WavFileHeader.WavName, "WAVE");
qstrcpy(WavFileHeader.FmtName, "fmt ");
qstrcpy(WavFileHeader.DATANAME, "data");
// 表示 FMT块 的长度
WavFileHeader.nFmtLength = 16;
// 表示 按照PCM 编码;
WavFileHeader.nAudioFormat = 1;
// 声道数目;
WavFileHeader.nChannleNumber = 1;
// 采样频率;
WavFileHeader.nSampleRate = 8000;
// nBytesPerSample 和 nBytesPerSecond这两个值通过设置的参数计算得到;
// 数据块对齐单位(每个采样需要的字节数 = 通道数 × 每次采样得到的样本数据位数 / 8 )
WavFileHeader.nBytesPerSample = 2;
// 波形数据传输速率
// (每秒平均字节数 = 采样频率 × 通道数 × 每次采样得到的样本数据位数 / 8 = 采样频率 × 每个采样需要的字节数 )
WavFileHeader.nBytesPerSecond = 16000;
// 每次采样得到的样本数据位数;
WavFileHeader.nBitsPerSample = 16;
QFile cacheFile(catheFileName);
QFile wavFile(wavFileName);
if (!cacheFile.open(QIODevice::ReadWrite))
{
return -1;
}
if (!wavFile.open(QIODevice::WriteOnly))
{
return -2;
}
int nSize = sizeof(WavFileHeader);
qint64 nFileLen = cacheFile.bytesAvailable();
WavFileHeader.nRiffLength = nFileLen - 8 + nSize;
WavFileHeader.nDataLength = nFileLen;
// 先将wav文件头信息写入,再将音频数据写入;
wavFile.write((char *)&WavFileHeader, nSize);
wavFile.write(cacheFile.readAll());
cacheFile.close();
wavFile.close();
return nFileLen;
}
点击录音开始录音,再次点击会弹出提示,当前正在录音,这样防止多次点击录音,导致重复录音。点击停止录音,结束录音,并将生成的.raw文件转成.wav文件,点击播放,则播放.wav音频文件。多次点击播放会重新播放,点击停止播放,则停止当前播放。
这里做了一些简单的逻辑判断,避免多次点击按钮导致生成的文件错误,同时在时钟事件中不断更新当前状态。
仔细对比两个文件,二者大小相差44个字节,这44个字节即为手动添加的wav文件头信息,而前者是不能用播放器打开,后者可以直接用播放器播放。
从上图中可以通过文件头来判断是否是一个wav文件,该文件的前4位为 “52 49 46 46” ,即为 “RIFF”。后面的数据也是按照wav文件头数据结构依次存储。有兴趣可以对照上一篇文章对wav文件头的介绍,将生成的wav文件头信息解析出来。
以下是对wav文件解析后的数据:
具体如何解析可以参考 Qt 之 解析wav文件的头信息(详细分析、对比不同wav文件的数据)。
在利用QAudioInput生成音频时需要设置音频的格式(通过QAudioFormat来设置),这里设置的格式要与 转为wav文件时设置的一系列参数 以及 在用 QAudioOutput 进行播放时设置的格式要完全一致,否则会导致声音文件识别不了,或者播放声音不清楚或者就只能听见嗡嗡的声音,所以一定要保持格式的一致性。至于在格式中参数的取值,到底对生成的音频文件有什么影响,将在下篇中进行解答。
下一篇将继续介绍用Qt直接生成wav格式的文件,不需要手动来添加wav文件头,同时也会用代码来解析一个wav文件的头信息,以及在生成时设置的一些格式参数对音频文件的影响等,下次见 。
Qt之实现录音播放及raw(pcm)转wav格式
Qt 之 WAV文件解析
Qt 之 解析wav文件的头信息(详细分析、对比不同wav文件的数据)。