理解PCM音頻數據,使用QAudioOutput播放音頻的兩種方法

【寫在前面】

因爲最近需要寫FFmpeg播放音頻的文章,所以就先寫了這篇文章。

並且,FFmpeg解碼出來的音頻是PCM原始音頻數據。

然後,我使用 Qt 的 QAudioOutput 作爲底層音頻輸出(輸出設備)。

本篇主要內容:

1、音頻基礎概念

2、PCM數據格式

3、QAudioOutput 的使用方法( 兩種 )


【正文開始】

  • 在介紹PCM之前,必須先了解一些音頻基礎概念:

採樣率( Sample Rate ):每秒採樣的頻率,即每秒樣本數,單位HZ ,常見的頻率有22050HZ、44100HZ( CD級 )。並且人耳的聽力範圍是20HZ ~ 20000HZ,因此超過48000HZ就沒有意義了( 根據採樣定理 )。

採樣定理:採樣在進行模擬/數字信號的轉換過程中,當採樣頻率fs.max大於信號中最高頻率fmax的2倍時(fs.max>2fmax),採樣之後的數字信號完整地保留了原始信號中的信息,一般實際應用中保證採樣頻率爲信號最高頻率的2.56~4倍,採樣定理又稱奈奎斯特定理。

樣本大小( Sample Size ):每個樣本的大小,也就是振幅,假如樣本大小爲16 Bit = 2 ^ 16 = 65536,即能夠記錄 65535 個數,常見的有 8Bit、16Bit( CD級 ),而到了 32Bit 意義就不大了。

聲道( Sound Channel ):指聲音在錄製或播放時,在不同空間位置採集或回放的相互獨立的音頻信號,常見的有單聲道,雙聲道,四聲道,這個倒是聲道越多效果越好。

比特率( Bit Rate ):單位( bps 比特每秒 ),可以使用 比特率 = 採樣率 x 樣本大小 x 聲音通道數 來計算。

字節序( Byte Ordering ):字節序,這個大家應該都知道,分大端和小端,一般都是小端字節序(低位低地址,高位高地址)。

  • 好了,現在開始介紹 PCM 格式:

PCM 全名脈衝編碼調製( Pulse Code Modulation ),它是原始的音頻脈衝數據,在一般情況下,一幀PCM數據包含2048個樣本( 數據幀,注意是一幀而不是一秒 )。

因此,對於一幀PCM數據,使用小端字節序存儲爲(每個 [ ] 爲一個樣本):

內存地址:    |  低地址  |  >>>>>>>  |  高地址  |

8Bit 單聲道[ 8 bit ] [ 8 bit ] - [ 8 bit ] - [ 8 bit ]   } 總 2048個。

8Bit 雙聲道:[ 8 bit 聲道1 ] [ 8 bit 聲道2 ] - [ 8 bit 聲道1 ] - [ 8 bit 聲道2 ]  } 總 2048個。

16Bit 單聲道[ 8 bit 低位 + 8 bit 高位 ] [ 8 bit 低位 + 8 bit 高位 ] - [ 8 bit 低位 + 8 bit 高位 ] - [ 8 bit 低位 + 8 bit 高位 ]   } 總 2048個。

16Bit 雙聲道:[ 8 bit 低位 + 8 bit 高位 聲道1 ] [ 8 bit 低位 + 8 bit 高位 聲道2 ] - [ 8 bit 低位 + 8 bit 高位 聲道1 ] - [ 8 bit 低位 + 8 bit 高位 聲道2 ]  } 總 2048個。

到這裏,我們已經大概瞭解了PCM數據格式,現在可以嘗試手動生成一份PCM數據:

QByteArray generateRandomPCM()
{
    qsrand(uint(time(nullptr)));
    //幅度,因爲sampleSize = 16bit
    qint16 amplitude = INT16_MAX;
    //單聲道
    int channels = 1;
    //採樣率
    int samplerate = 8000;
    //持續時間ms
    int duration = 20000;
    //總樣本數
    int n_samples = int(channels * samplerate * (duration / 1000.0));

    QByteArray data;
    QDataStream out(&data, QIODevice::WriteOnly);
    out.setByteOrder(QDataStream::LittleEndian);
    for (int i = 0; i < n_samples; i++) {
        qint16 sample = qrand() % amplitude;
        out << sample;
    }

    QFile file("raw");
    file.open(QIODevice::WriteOnly);
    file.write(data);
    file.close();

    return data;
}

利用這個函數,可以生成一份 8000HZ,16Bit,單聲道,持續 20s的隨機PCM數據,部分波形如下:

呃。。這種都是噪音,聽不出什麼來,於是我嘗試生成一些有規律的: 

QByteArray generatePCM()
{
    //幅度,因爲sampleSize = 16bit
    qint16 amplitude = INT16_MAX;
    //單聲道
    int channels = 1;
    //採樣率
    int samplerate = 8000;
    //持續時間ms
    int duration = 20;
    //總樣本數
    int n_samples = int(channels * samplerate * (duration / 1000.0));
    //聲音頻率
    int frequency = 100;

    bool reverse = false;
    QByteArray data;
    QDataStream out(&data, QIODevice::WriteOnly);
    out.setByteOrder(QDataStream::LittleEndian);
    for (int i = 0; i < 1000; i++) {
        for (int j = 0; j < n_samples; j++) {
            qreal radians = qreal(2.0 * M_PI * j  * frequency / qreal(samplerate));
            qint16 sample = qint16(qSin(radians) * amplitude);
            out << sample;
        }

        if (!reverse) {
            if (frequency < 2000) {
                frequency += 100;
            } else reverse = true;
        } else {
            if (frequency > 100) {
                frequency -= 100;
            } else reverse = false;
        }
    }

    QFile file("raw");
    file.open(QIODevice::WriteOnly);
    file.write(data);
    file.close();

    return data;
}

這種生成的音頻就是,頻率(聲調)由低到高再到低,部分波形如下:

到這裏,PCM相關的就講解完了。

  • 現在PCM數據有了,那麼如何在Qt中播放它呢?答案是 QAudioOutput

QAudioOutput是Qt中播放音頻的類( 另一個是QSound,但只支持WAV ),要使用它,需要在pro中加入  QT += multimedia

我們先來看看代碼:

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    
    QByteArray pcm1 = generatePCM();

    QAudioFormat format;
    format.setCodec("audio/pcm");
    format.setSampleRate(8000);
    format.setSampleSize(16);
    format.setSampleType(QAudioFormat::SignedInt);
    format.setChannelCount(1);
    format.setByteOrder(QAudioFormat::LittleEndian);

    QAudioOutput output(format, qApp);
    QIODevice *device = output.start();

    QTimer *timer_play = new QTimer(qApp);
    timer_play->setTimerType(Qt::PreciseTimer);
    QObject::connect(timer_play, &QTimer::timeout, [&]{
        if (pcm1.size() > 0) {
            int readSize = output.periodSize();
            int chunks = output.bytesFree() / readSize;
            while (chunks) {
                QByteArray samples = pcm1.mid(0, readSize);
                int len = samples.size();
                pcm1.remove(0, len);

                if (len) device->write(samples);
                if (len != readSize) break;

                chunks--;
            }
        }
    });
    timer_play->start(100);

    return a.exec();
}

1、創建一個 QAudioFormat ,它描述了音頻的格式,setCodec() 只支持 PCM,而其他的參數根據前面所講,你應該知道是什麼了吧。

2、根據 QAudioFormat 創建一個 QAudioOutput,它是用來管理音頻設備( 即聲卡 )的。

3、使用 start() 返回一個指向實際音頻設備的指針,往這個裏面寫入數據就能夠播放出聲音了。

4、我使用定時器來定時寫入數據,這樣可以保證聲音是連續的。

5、periodSize() 返回一個週期所必需要的數據量,而 bytesFree() 返回內部緩衝區的空閒空間的字節數,也就是說,我們只需要每次寫入所需的數據量 periodSize(),然後直到填充滿內部緩沖 bytesFree() 即可實現連續播放。

至此,第一種 QAudioOutput 的使用方法講解完畢。

  • 第二種 QAudioOutput 的使用方法:

QAudioOutput 的 start() 函數的重載版本 start(QIODevice *),它需要傳入一個QIODevice指針,然後由 QAudioOutput 進行讀取。

因此,我們需要實現一個自己的IODevice,並實現 readData() writeData() (純虛函數,必須實現):

class AudioDevice : public QIODevice
{
public:
    AudioDevice(const QByteArray &data, QObject *parent = nullptr) : QIODevice(parent), m_data(data) { }
    ~AudioDevice() { }

    virtual qint64 readData(char *data, qint64 maxlen);

    virtual qint64 writeData(const char *data, qint64 len){
        Q_UNUSED(data);
        Q_UNUSED(len);
        return 0;
    }

private:
    QByteArray m_data;
};

qint64 AudioDevice::readData(char *data, qint64 maxlen)
{
    if (m_data.size() >= maxlen) {
        QByteArray d = m_data.mid(0, int(maxlen));
        memcpy(data, d.data(), size_t(d.size()));
        m_data.remove(0, int(maxlen));
        return d.size();
    } else {
        QByteArray d = m_data;
        memcpy(data, d.data(), size_t(d.size()));
        m_data.clear();
        return d.size();
    }
}

因爲 QAudioOutput 需要數據時在提供的 QIODdevice 中讀取,所以實現 readData() 即可。

然後我們就可以使用它了:

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QAudioFormat format;
    format.setCodec("audio/pcm");
    format.setSampleRate(8000);
    format.setSampleSize(16);
    format.setSampleType(QAudioFormat::SignedInt);
    format.setChannelCount(1);
    format.setByteOrder(QAudioFormat::LittleEndian);

    QAudioOutput output(format, qApp);
    QByteArray pcm1 = generatePCM();
    AudioDevice *device = new AudioDevice(pcm1, qApp);
    device->open(QIODevice::ReadOnly);
    output.start(device);

    return a.exec();
}

【結語】

其實說起來,QAudioOutput的兩種方並沒有太大的區別,所以使用哪種就看個人喜好了。

然後本篇已經很詳細的講了音頻基礎,PCM的格式以及如何簡單的生成它。(啊。寫了一天,累死我了。。)

最後,所有的代碼在:https://download.csdn.net/download/u011283226/11789311 (可能需要點積分,但我不想放在Github上Ծ‸ Ծ )。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章