Qt播放PCM音频(裸流)文件的两种方法

        这两种方法都需要在.pro文件中加入multimedia模块。方法一是通过QAudioOutput实现的,先用QFile打开PCM文件,然后进行音频参数设置,最后调用QAudioOutput的start函数进行播放;方法二也用到了QAudioOutput,先将PCM文件读取到内存,然后进行音频参数设置,最后从内存中定时读取部分数据,调用QIODevice的write函数将这部分数据写入到扬声器。与方法一相比,方法二虽然麻烦,但是这种方法是进行语音编解码和语音网络传输的基础。

方法一:

#include <QApplication>
#include <QFile>
#include <QAudioFormat>
#include <QAudioOutput>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QFile inputFile;
    inputFile.setFileName("test.pcm");
    inputFile.open(QIODevice::ReadOnly);

    //设置采样格式
    QAudioFormat audioFormat;
    //设置采样率
    audioFormat.setSampleRate(44100);
    //设置通道数
    audioFormat.setChannelCount(2);
    //设置采样大小,一般为8位或16位
    audioFormat.setSampleSize(16);
    //设置编码方式
    audioFormat.setCodec("audio/pcm");
    //设置字节序
    audioFormat.setByteOrder(QAudioFormat::LittleEndian);
    //设置样本数据类型
    audioFormat.setSampleType(QAudioFormat::UnSignedInt);

    QAudioOutput *audio = new QAudioOutput( audioFormat, 0);
    audio->start(&inputFile);
    return a.exec();
}

方法二:

#ifndef TESTAUDIO_H
#define TESTAUDIO_H

#include <QWidget>
#include <QAudioOutput>
#include <QIODevice>
#include <QTimer>
#include <QByteArray>

class TestAudio : public QWidget
{
    Q_OBJECT

public:
    explicit TestAudio(QWidget *parent = nullptr);
    ~TestAudio();

private:
    int fileLength;
    char *fileBuffer;
    QAudioOutput *audioOutput;
    QIODevice *streamOut;
    QTimer *timer;
    QByteArray tempBuffer;

private slots:
    void slotTimeout();
};

#endif // TESTAUDIO_H
#include "TestAudio.h"

#include <QDebug>
#include <QAudioFormat>
#include <QAudioDeviceInfo>
#include <QApplication>

TestAudio::TestAudio(QWidget *parent) :
    QWidget(parent)
{
    //获取文件的指针.
    FILE *file=fopen(QString(qApp->applicationDirPath()+"/NorwayForest-500.pcm").toStdString().c_str(),"rb");
    if(!file)
    {
        qDebug()<<"Open Failed";
        return;
    }
    //把指针移动到文件的结尾 ,获取文件长度.
    fseek(file,0,SEEK_END);
    //获取文件长度.
    fileLength=ftell(file);
    //定义数组长度.
    fileBuffer=new char[fileLength+1];
    //把指针移动到文件开头 因为我们一开始把指针移动到结尾,如果不移动回来 会出错.
    rewind(file);
    //读文件.
    fread(fileBuffer,1,fileLength,file);
    //把读到的文件最后一位 写为0 要不然系统会一直寻找到0后才结束.
    fileBuffer[fileLength]=0;
    //关闭文件.
    fclose(file);
    qDebug()<<fileLength;

    //设置采样格式.
    QAudioFormat audioFormat;
    //设置采样率.
    audioFormat.setSampleRate(44100);
    //设置通道数.
    audioFormat.setChannelCount(2);
    //设置采样大小,一般为8位或16位.
    audioFormat.setSampleSize(16);
    //设置编码方式.
    audioFormat.setCodec("audio/pcm");
    //设置字节序.
    audioFormat.setByteOrder(QAudioFormat::LittleEndian);
    //设置样本数据类型.
    audioFormat.setSampleType(QAudioFormat::UnSignedInt);
    //音频设备信息.
    QAudioDeviceInfo info = QAudioDeviceInfo::defaultOutputDevice();
    if (!info.isFormatSupported(audioFormat)) {
        qDebug()<<"default format not supported try to use nearest";
        audioFormat = info.nearestFormat(audioFormat);
    }
    audioOutput = new QAudioOutput(audioFormat, this);
    streamOut = audioOutput->start();
    //定时器.
    timer=new QTimer(this);
    connect(timer, SIGNAL(timeout()), SLOT(slotTimeout()));
    timer->start(10);
}

TestAudio::~TestAudio()
{

}

void TestAudio::slotTimeout()
{
    static int i=0;
    if(i<fileLength/1764)
    {
        tempBuffer.append(fileBuffer+i*1764,1764);

        if(audioOutput&&audioOutput->state()!=QAudio::StoppedState&&
                audioOutput->state()!=QAudio::SuspendedState)
        {
            int chunks = audioOutput->bytesFree()/audioOutput->periodSize();
            while (chunks)
            {
                if (tempBuffer.length() >= audioOutput->periodSize())
                {
                    //写入到扬声器.
                    streamOut->write(tempBuffer.data(),audioOutput->periodSize());
                    tempBuffer = tempBuffer.mid(audioOutput->periodSize());
                }
                else
                {
                    //写入到扬声器.
                    streamOut->write(tempBuffer);
                    tempBuffer.clear();
                    break;
                }
                --chunks;
            }
        }
    }
    i++;
}

PCM裸流1秒的数据量是44100×16×2/8=176400字节,那么10毫秒的数据量就是1764字节,所以定时器定时10毫秒,每隔10毫秒从数组中顺序读取1746个字节。

不幸的是,方法二会有杂音,感谢评论区winwingy的提醒。方法三如下:

方法三

#ifndef MYDEVICE_H
#define MYDEVICE_H

#include <QIODevice>

class MyDevice : public QIODevice
{
private:
    QByteArray data_pcm; //存放pcm数据
    int        len_written; //记录已写入多少字节
public:
    MyDevice(QByteArray pcm); //创建对象传递pcm数据
    ~MyDevice();

    qint64 readData(char *data, qint64 maxlen); //重新实现的虚函数
    qint64 writeData(const char *data, qint64 len); //它是个纯虚函数, 不得不实现
};

#endif // MYDEVICE_H
#include "MyDevice.h"

#include <QDebug>

MyDevice::MyDevice(QByteArray pcm) : data_pcm(pcm)
{
    this->open(QIODevice::ReadOnly); // 为了解决QIODevice::read (QIODevice): device not open.
    len_written = 0;
}

MyDevice::~MyDevice()
{
    this->close();
}
// data为声卡的数据缓冲区地址, maxlen为声卡缓冲区最大能存放的字节数.
qint64 MyDevice::readData(char *data, qint64 maxlen)
{
    if (len_written >= data_pcm.size())
        return 0;
    int len;

    //计算未播放的数据的长度.
    len = (len_written+maxlen) > data_pcm.size() ? (data_pcm.size() - len_written) : maxlen;

    memcpy(data, data_pcm.data()+len_written, len); //把要播放的pcm数据存入声卡缓冲区里.
    len_written += len; //更新已播放的数据长度.
    return len;
}

qint64 MyDevice::writeData(const char *data, qint64 len)
{
    return len;
}
#ifndef TESTAUDIO_H
#define TESTAUDIO_H

#include <QWidget>

class TestAudio : public QWidget
{
    Q_OBJECT

public:
    explicit TestAudio(QWidget *parent = nullptr);
    ~TestAudio();
};

#endif // TESTAUDIO_H
#include "TestAudio.h"
#include "MyDevice.h"

#include <QDebug>
#include <QAudioFormat>
#include <QAudioOutput>
#include <QApplication>
#include <QFile>

TestAudio::TestAudio(QWidget *parent) :
    QWidget(parent)
{
    QAudioFormat fmt;
    fmt.setSampleRate(44100);
    fmt.setChannelCount(2);
    fmt.setSampleSize(16);
    fmt.setCodec("audio/pcm");
    fmt.setCodec("audio/pcm");
    fmt.setByteOrder(QAudioFormat::LittleEndian);
    fmt.setSampleType(QAudioFormat::UnSignedInt);

    QAudioOutput *out = new QAudioOutput(fmt, this);

    QByteArray ba;
    QFile f(qApp->applicationDirPath()+"/NorwayForest-500.pcm");
    if (!f.open(QIODevice::ReadOnly))
        exit(0);
    ba = f.readAll();
    f.close();

    MyDevice *dev = new MyDevice(ba);
    out->start(dev);
}

TestAudio::~TestAudio()
{

}

方法三虽然是播放的本地文件,如果要播放网络流的话,实时给QByteArray填充数据就行。

更详细的可参考Qt自带例子Audio Output:

 

PCM音频文件的制作参考:http://blog.csdn.net/caoshangpa/article/details/51218994

音频参数解析参考:http://blog.csdn.net/caoshangpa/article/details/51218597

测试用PCM音频文件下载链接:http://download.csdn.net/detail/caoshangpa/9497751

原创不易,转载请标明出处:https://blog.csdn.net/caoshangpa/article/details/51224678


版权声明:本文为caoshangpa原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。