qt录音的实现

简述

在上一篇 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格式的音频文件。

仔细看代码有点多,其实实现很简单,这里我添加的一些代码是用来实现一个简单完整的小录音机功能。
MyAudioInput.cpp

#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 ");

WavFileHeader.nFmtLength = 16;  //  表示 FMT 的长度  
WavFileHeader.nAudioFormat = 1; //这个表示 PCM 编码; 

qstrcpy(WavFileHeader.DATANAME, "data");

WavFileHeader.nBitsPerSample = 16;
WavFileHeader.nBytesPerSample = 2;  
WavFileHeader.nSampleRate = 8000;
WavFileHeader.nBytesPerSecond = 16000;
WavFileHeader.nChannleNumber = 1;

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文件头信息后的数据截图:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值