1、前言

因为项目需要,做了个小工具来做前期准备。

这个需求实现两步:播放和显示波形。

播放方面,一开始选择FMod,小工具快做好的时候偶然发现FMod需要商业授权,所以只能放弃。试了试ffmpeg+SDL又觉得大材小用过于复杂(主要是对编解码这一块有点畏惧)。最后才发现QT自带播放类其实已经满足需求,最后播放就交给QMediaPlayer。

由于显示波形需要放大平移等操作,自己实现起来很费时间,所以选择现有的图表工具。目前可选的有QWT和QCustomPlot。由于QWT配置起来比较麻烦,所以显示方面选择QCustomPlot。

显示波形的数据源在使用FMod的时候,是从FMod函数中获取的,后来发现Wav文件就是文件头+PCM数据,直接解出来就可以了。自己对音视频这一块真的一窍不通。

2、播放Wav文件

使用QMediaPlayer播放非常简单,设置文件路径,然后播放就可以了。

m_mediaPlayer.setMedia(QUrl::fromLocalFile(ui->editPath->text()));
m_mediaPlayer.play();

由于需要对Wav文件循环播放,但使用QMediaPlaylist的setPlaybackMode方法并不成功,最终也没有得到解决。所以在QMediaPlayer::stateChanged信号中加里循环控制。 

connect(&m_mediaPlayer, &QMediaPlayer::stateChanged, [=](QMediaPlayer::State state) {
        if(state == QMediaPlayer::StoppedState)
        {
            ui->btnPlay->setText("播放");
            if(m_isBtnStop)
            {
                m_isBtnStop = false;
                return ;
            }

            if(ui->checkLoop->isChecked())
            {
                m_mediaPlayer.setPosition(0);
                m_mediaPlayer.play();
            }
        }
        else if(state == QMediaPlayer::PlayingState)
        {
            ui->btnPlay->setText("暂停");
        }
        else if(state == QMediaPlayer::PausedState)
        {
            ui->btnPlay->setText("播放");
        }

        qDebug()<<"stateChanged"<<state;
    });

其中m_isBtnStop用于区分是按键的停止还是文件播放结束的停止。在停止按键按下时,m_isBtnStop先设置为true。

在播放类的选择中,选择QMediaPlayer而不是QSound,是因为QMediaPlayer有更多的播放控制。对音量和进度的控制是QSound没有的。

播放时长和播放进度获取

connect(&m_mediaPlayer, &QMediaPlayer::durationChanged, [=](qint64 duration) {
        ui->sliderPosition->setRange(0,duration);
        ui->labelCurPosition->setText(formatTime(0));
        ui->labelDuration->setText(formatTime(duration));
        qDebug()<<"durationChanged"<<duration;
      });
    connect(&m_mediaPlayer, &QMediaPlayer::positionChanged, [=](qint64 position) {
        ui->sliderPosition->setValue(position);
        ui->labelCurPosition->setText(formatTime(position));
        qDebug()<<"positionChanged"<<position;
      });

时间显示部分调用的是()中的函数。因为实际项目中并不会出现时间,就懒得自己写了,小工具里借用一下。

进度信号默认1秒发送一次,对于一些只有几秒的文件,进度条滑动就很不顺畅。可以把发送间隔调整一下,这里是调整为100毫秒。

m_mediaPlayer.setNotifyInterval(100);

播放/暂停、停止、后退一秒、前进一秒、静音、音量调节、进度调节

void Dialog::on_btnPlay_clicked()
{
    switch (m_mediaPlayer.state())
    {
    case QMediaPlayer::PlayingState:
    {
        m_mediaPlayer.pause();
        break;
    }

    case QMediaPlayer::StoppedState:
    {
        m_mediaPlayer.setMedia(QUrl::fromLocalFile(ui->editPath->text()));
    }
    case QMediaPlayer::PausedState:
    {
        m_mediaPlayer.play();
        break;
    }
    default:break;
    }
}

void Dialog::on_btnStop_clicked()
{
    m_isBtnStop = true;
    m_mediaPlayer.stop();
}

void Dialog::on_btnPre_clicked()
{
    int newPosition = m_mediaPlayer.position() - 1000;
    m_mediaPlayer.setPosition(newPosition < 0 ? 0 : newPosition);
}

void Dialog::on_btnNext_clicked()
{
    int newPosition = m_mediaPlayer.position() + 1000;
    int duration = m_mediaPlayer.duration();
    m_mediaPlayer.setPosition(newPosition > duration ? duration : newPosition);
}

void Dialog::on_btnMute_clicked(bool checked)
{
    m_mediaPlayer.setMuted(checked);
}

void Dialog::on_sliderVolume_valueChanged(int value)
{
    m_mediaPlayer.setVolume(value);
}

void Dialog::on_sliderPosition_sliderReleased()
{
    m_mediaPlayer.setPosition(ui->sliderPosition->value());
}

 需要注意的是,由于sliderPosition同时受QMediaPlayer::positionChanged的影响,所有不能用valueChanged,否在会因为反复控制间的延时,造成杂音。

3、获取波形数据

QFile fileInfo(ui->editPath->text());
    if (!fileInfo.open(QIODevice::ReadOnly))
    {
        return ;
    }
    fileInfo.read(m_wavFileHeader.RiffName, sizeof(m_wavFileHeader.RiffName));
    fileInfo.read((char*)&m_wavFileHeader.nRiffLength, sizeof(m_wavFileHeader.nRiffLength));
    fileInfo.read(m_wavFileHeader.WavName, sizeof(m_wavFileHeader.WavName));
    fileInfo.read(m_wavFileHeader.FmtName, sizeof(m_wavFileHeader.FmtName));
    fileInfo.read((char*)&m_wavFileHeader.nFmtLength, sizeof(m_wavFileHeader.nFmtLength));
    fileInfo.read((char*)&m_wavFileHeader.nAudioFormat, sizeof(m_wavFileHeader.nAudioFormat));
    fileInfo.read((char*)&m_wavFileHeader.nChannleNumber, sizeof(m_wavFileHeader.nChannleNumber));
    fileInfo.read((char*)&m_wavFileHeader.nSampleRate, sizeof(m_wavFileHeader.nSampleRate));
    fileInfo.read((char*)&m_wavFileHeader.nBytesPerSecond, sizeof(m_wavFileHeader.nBytesPerSecond));
    fileInfo.read((char*)&m_wavFileHeader.nBytesPerSample, sizeof(m_wavFileHeader.nBytesPerSample));
    fileInfo.read((char*)&m_wavFileHeader.nBitsPerSample, sizeof(m_wavFileHeader.nBitsPerSample));

    QString strAppendMessageData; 
    if (m_wavFileHeader.nFmtLength >= 18)
    {
        fileInfo.read((char*)&m_wavFileHeader.nAppendMessage, sizeof(m_wavFileHeader.nAppendMessage));

        int appendMessageLength = m_wavFileHeader.nFmtLength - 18;
        m_wavFileHeader.AppendMessageData = new char[appendMessageLength];
        fileInfo.read(m_wavFileHeader.AppendMessageData, appendMessageLength);       
        strAppendMessageData = QString(m_wavFileHeader.AppendMessageData);
    }
    char chunkName[5];
    fileInfo.read(chunkName, sizeof(chunkName) - 1);
    chunkName[4] = '\0';
    QString strChunkName(chunkName);
    if (strChunkName.compare("fact") == 0)
    {
        strcpy(m_wavFileHeader.FactName, chunkName);
        fileInfo.read((char*)&m_wavFileHeader.nFactLength, sizeof(m_wavFileHeader.nFactLength));
        fileInfo.read(m_wavFileHeader.FactData, sizeof(m_wavFileHeader.FactData));
        fileInfo.read(m_wavFileHeader.DATANAME, sizeof(m_wavFileHeader.DATANAME));
    }
    else
    {
        strcpy(m_wavFileHeader.DATANAME, chunkName);
    }

    fileInfo.read((char*)&m_wavFileHeader.nDataLength, sizeof(m_wavFileHeader.nDataLength));

    QByteArray pcmData;
    pcmData = fileInfo.readAll();
    m_wavFileHeader.fileDataSize = pcmData.size();
    m_wavFileHeader.fileTotalSize = m_wavFileHeader.nRiffLength + 8;
    m_wavFileHeader.fileHeaderSize = m_wavFileHeader.fileTotalSize - m_wavFileHeader.fileDataSize;

    fileInfo.close();

由于篇幅关系删掉了注释,可以到原博客里看更具体的解释,以及WAVFILEHEADER的定义。

此处pcmData就是用于显示波形的数据。

4、显示波形-QCustomPlot

QCustomPlot官网:https://www.qcustomplot.com/index.php/download

android 实时音频波形实现 显示音频波形的软件_qt

 下载后解压,将qcustomplot.h/.cpp两个文件复制到项目,在UI中添加一个Widget窗体,并提升为QCustomPlot。(也可以examples-plots看到各种Demo)

QVector<double> waveData;
    uint len = m_wavFileHeader.fileDataSize/m_wavFileHeader.nBytesPerSample;
    qDebug()<<__FUNCTION__<<pcmData.size()<<len;
    if(m_wavFileHeader.nBytesPerSample == 1)//8位
    {
        char *data = (char *)pcmData.data();
        for (uint i = 0; i < len; i++)
        {
            waveData.append(data[i]);
        }
    }
    else//16位
    {
        short *data = (short *)pcmData.data();
        for (uint i = 0; i < len; i++)
        {
            waveData.append(data[i]);
        }
    }
    ui->customPlot->addGraph();
    ui->customPlot->graph(0)->setPen(QPen(Qt::blue)); 

    ui->customPlot->addGraph();
    ui->customPlot->graph(1)->setPen(QPen(Qt::red));

    QVector<double> x(len);
    for (uint i=0; i<len; ++i)
    {
        x[i] = i;
    }
    QVector<double> lineX(2),lineY(2);
    lineX[0]=lineX[1]=0;
    lineY[0]=-100000;
    lineY[1]=100000;
    ui->customPlot->graph(0)->setData(x, waveData);
    ui->customPlot->graph(1)->setData(lineX,lineY);
    ui->customPlot->graph(0)->rescaleAxes();

由于数据位数不同,8位的PCM数据使用1字节表示一个采样数据,而16位的PCM数据使用2字节表示一个采样数据,此处做了区分。由于实际项目上不会有双通道的音频文件,所以没有对通道数作出处理。

graph(0)用于显示波形,graph(1)用于显示进度线。因此在QMediaPlayer::positionChanged时需要实时刷新线的位置。

QVector<double> lineX(2),lineY(2);
        lineX[0]=lineX[1]=(double)position*m_wavFileHeader.nSampleRate/1000;
        lineY[0]=-100000;
        lineY[1]=100000;
        ui->customPlot->graph(1)->setData(lineX,lineY);

线的Y坐标±100000只是设置了比测试音频最大值更大的数值,使这线能贯穿整个波形。

画线的时候考虑过另外画一条无限延长的直线QCPItemStraightLine,但是测试x坐标逐一累加时发现效果不如graph(1)的这种,会有轻微的闪烁,不如graph(1)走得顺滑,所以放弃了。

该工具还在继续增加功能中,后期会加上音频截取,以及拖动图上得线控制播放进度,所以在做完之前暂时不会放代码。这些功能写完后,也会更新到这里。

这个工具目前为止就几百行代码非常简单,甚至想过是否值得特地写一篇来记录,最后写下来一个是因为音频方面前面弯路走得太多,一个是自己本身对音视频这方面非常欠缺就记录一下,还有就是这个工具是独立的功能,后期上传代码囤点积分,现在下代码积分要求极高吗,不多存点以后遇到难题实在下不起大神们的代码了。