Qt之实现录音播放及raw(pcm)转wav格式

简述

在上一篇 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 <QAudioDeviceInfo>
#include <QDebug>
#include <QMessageBox>


#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文件的数据)

  • 5
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
本课程详细、全面地介绍了 Qt 开发中的各个技术细节,并且额外赠送在嵌入式端编写Qt程序的技巧。整个课程涵盖知识点非常多,知识模块囊括 Qt-Core 组件、QWidgets、多媒体、网络、绘图、数据库,超过200个 C++ 类的分析和使用,学完之后将拥有 Qt 图形界面开发的非常坚实的功底。 每个知识点不仅仅会通过视频讲解清楚,并且会配以精心安排的实验和作业,用来保证学习过程中切实掌握核心技术和概念,通过实验来巩固,通过实验来检验,实验与作业的目的是发现问题,发现技术盲点,通过答疑和沟通夯实技术技能。注意:本套视频教程来源于线下的实体班级,因此视频中有少量场景对话和学生问答,对此比较介意的亲们谨慎购买。注意:本套视频教程包含大量课堂源码,包含对应每个知识点的精心编排的作业。由于CSDN官方规定在课程介绍中不能出现作者的联系方式,因此在这里无法直接给出QQ答疑号,视频中的源码、资料和作业文档链接统一在购买后从CSDN平台跟我沟通,我会及时回复跟进。注意:本套视频教程包含全套10套作业题,覆盖所有视频知识点,循序渐进,各个击破,作业总纲如下:下面是部分作业题目展示,每道题都有知识点说明,是检验学习效果的一大利器:(部分作业展示,为了防止盗图盗题对题干做了模糊处理)(部分作业展示,为了防止盗图盗题对题干做了模糊处理)(部分作业展示,为了防止盗图盗题对题干做了模糊处理)(部分作业展示,为了防止盗图盗题对题干做了模糊处理)(部分作业展示,为了防止盗图盗题对题干做了模糊处理)…… ……

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值