使用SDL播放PCM音頻數(shù)據(jù)

一鸳玩、使用 ffplay 命令行程序播放

首先使用 ffmpeg 命令行程序抽出 pcm 數(shù)據(jù):

$ ffmpeg -i 那又如何.mp3 -ar 44100 -ac 2 -f s16le -acodec pcm_s16le out.pcm

使用 ffplay 命令行程序播放 pcm 數(shù)據(jù):

$ ffplay -ar 44100 -ac 2 -f s16le out.pcm

-ar 采樣率
-ac 聲道數(shù)
-f 采樣格式

在 Mac 平臺使用 ffmpeg -formats | grep PCM 查看更多采樣格式:

DE alaw            PCM A-law
DE f32be           PCM 32-bit floating-point big-endian
DE f32le           PCM 32-bit floating-point little-endian
DE f64be           PCM 64-bit floating-point big-endian
DE f64le           PCM 64-bit floating-point little-endian
DE mulaw           PCM mu-law
DE s16be           PCM signed 16-bit big-endian
DE s16le           PCM signed 16-bit little-endian
DE s24be           PCM signed 24-bit big-endian
DE s24le           PCM signed 24-bit little-endian
DE s32be           PCM signed 32-bit big-endian
DE s32le           PCM signed 32-bit little-endian
DE s8              PCM signed 8-bit
DE u16be           PCM unsigned 16-bit big-endian
DE u16le           PCM unsigned 16-bit little-endian
DE u24be           PCM unsigned 24-bit big-endian
DE u24le           PCM unsigned 24-bit little-endian
DE u32be           PCM unsigned 32-bit big-endian
DE u32le           PCM unsigned 32-bit little-endian
DE u8              PCM unsigned 8-bit
DE vidc            PCM Archimedes VIDC
二、使用 SDL 編程實現(xiàn) PCM 播放

1笨篷、安裝 sdl2(如果已安裝忽略這一步著瓶,如果是使用Homebrew安裝的 FFmpeg 也可以省略這一步联予,因為通過 brew 安裝的 FFmpeg 依賴了 sdl2):

$ brew install sdl2

2、然后使用 Qt 新建一個名為 02_play_pcm_example 的工程

$ tree
.
|____playthread.cpp
|____mainwindow.h
|____mainwindow.ui
|____mainwindow.cpp
|____02_play_pcm_example.pro
|____main.cpp
|____playthread.h

3材原、在 04_sdl_play_pcm.pro 文件中配置 SDL 頭文件和靜態(tài)庫的位置(如果沒有安裝 SDL 需要先安裝沸久,):

INCLUDEPATH += -I "/usr/local/Cellar/sdl2/2.0.14_1/include"
LIBS += -L /usr/local/Cellar/sdl2/2.0.14_1/lib -lSDL2

接著在 mainwindow.cpp 中引入 sdl2 頭文件:

#include <SDL2/SDL.h>

打開 mainwindow.ui 文件添加一個 Push Button ,并且更名為 playButton余蟹,然后右鍵選擇轉(zhuǎn)到槽:

在槽函數(shù)中添加打印 SDL版本號的代碼:

void MainWindow::on_playButton_clicked()
{
    SDL_version version;
    SDL_VERSION(&version);
    qDebug() << version.major << version.minor << version.patch;
}

運行程序點擊播放PCM按鈕卷胯,會打印:

13:52:48: Starting /Users/mac/Desktop/QtWorkSpace/build-04_sdl_play_pcm-Desktop_Qt_6_0_2_clang_64bit-Debug/04_sdl_play_pcm.app/Contents/MacOS/04_sdl_play_pcm ...
2 0 14

說明我們引入 sdl2 成功了威酒!

4窑睁、接下來初始化 SDL 子系統(tǒng):

if (SDL_Init(SDL_INIT_AUDIO)) {
    qDebug() << "初始化 SDL 失敗:" << SDL_GetError();
    return;
}
atexit(SDL_Quit);

初始化成功后,就可以使用 SDL 子系統(tǒng)完成相應(yīng)的任務(wù)了葵孤,當完成所有工作需要退出程序時担钮,必須使用 SDL_Quit 清除所有子系統(tǒng)。如果初始化失敗尤仍,使用 SDL_GetError 函數(shù)獲取錯誤原因箫津。atexit 是 C 語言標準庫函數(shù),作用是向系統(tǒng)注冊傳進來的函數(shù)宰啦,以便程序結(jié)束時調(diào)用該函數(shù)苏遥,此處希望程序結(jié)束時清空 SDL 所有子系統(tǒng)。

flags 參數(shù)取值:

// 定時器
#define SDL_INIT_TIMER          0x00000001u  
// 音頻
#define SDL_INIT_AUDIO          0x00000010u
// 視頻
#define SDL_INIT_VIDEO          0x00000020u  /**< SDL_INIT_VIDEO implies SDL_INIT_EVENTS */
// 游戲控制桿
#define SDL_INIT_JOYSTICK       0x00000200u  /**< SDL_INIT_JOYSTICK implies SDL_INIT_EVENTS */
// 觸摸屏
#define SDL_INIT_HAPTIC         0x00001000u
// 游戲控制器
#define SDL_INIT_GAMECONTROLLER 0x00002000u  /**< SDL_INIT_GAMECONTROLLER implies SDL_INIT_JOYSTICK */
// 事件
#define SDL_INIT_EVENTS         0x00004000u
// 傳感器
#define SDL_INIT_SENSOR         0x00008000u
// 錯誤捕獲
#define SDL_INIT_NOPARACHUTE    0x00100000u  /**< compatibility; this flag is ignored. */
// 全部子系統(tǒng)
#define SDL_INIT_EVERYTHING ( \
                SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_EVENTS | \
                SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER | SDL_INIT_SENSOR \
            )

初始化成功返回 0赡模,初始化失敗函數(shù)返回值為 -1田炭,函數(shù)只接受各個子系統(tǒng)的常量作為參數(shù)。初始化音頻子系統(tǒng)漓柑,傳入?yún)?shù) SDL_INIT_AUDIO教硫;初始化視頻子系統(tǒng)傳入 SDL_INIT_VIDEO;并且可初始化一個或者多個子系統(tǒng)欺缘,例如同時初始化音頻和視頻子系統(tǒng)栋豫,傳入 SDL_INIT_AUDIO | SDL_INIT_VIDEO

5谚殊、打開音頻設(shè)備:

SDL_AudioSpec spec;
spec.freq = sampleRate;
spec.format = format; 
spec.channels = channels;
spec.samples = 1024;
spec.callback = call_back;

// 打開音頻設(shè)備
if (SDL_OpenAudio(&spec, nullptr)) {
    qDebug() << "打開音頻設(shè)備失敗:" << SDL_GetError();
    SDL_Quit();
    return;
}

SDL_OpenAudio 有兩個參數(shù):

extern DECLSPEC int SDLCALL SDL_OpenAudio(SDL_AudioSpec * desired, SDL_AudioSpec * obtained);

desired:期望參數(shù),播放的音頻對應(yīng)的參數(shù)蛤铜;
obtained:實際硬件設(shè)備參數(shù)嫩絮,可傳 nullptr丛肢;

SDL_AudioSpec 結(jié)構(gòu)體:

typedef struct SDL_AudioSpec
{
    // 采樣率
    int freq;                   /**< DSP frequency -- samples per second */
    // 音頻數(shù)據(jù)格式 
    SDL_AudioFormat format;     /**< Audio data format */
    // 聲道數(shù)
    Uint8 channels;             /**< Number of channels: 1 mono, 2 stereo */
    // 音頻緩沖區(qū)靜音值
    Uint8 silence;              /**< Audio buffer silence value (calculated) */
    // 采樣幀大小
    Uint16 samples;             /**< Audio buffer size in sample FRAMES (total samples divided by channel count) */
    // 兼容性參數(shù)
    Uint16 padding;             /**< Necessary for some compile environments */
    // 音頻緩沖區(qū)大小
    Uint32 size;                /**< Audio buffer size in bytes (calculated) */
    // 填充音頻緩沖區(qū)回調(diào)函數(shù)
    SDL_AudioCallback callback; /**< Callback that feeds the audio device (NULL to use SDL_QueueAudio()). */
    // 用戶自定義數(shù)據(jù),
    void *userdata;             /**< Userdata passed to callback (ignored for NULL callbacks). */
} SDL_AudioSpec;

回調(diào)函數(shù):

typedef void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream, int len);

當音頻設(shè)備需要更多數(shù)據(jù)時會調(diào)用該函數(shù)剿干;

6蜂怎、打開 PCM 文件:

QFile file(filePath);
if (!file.open(QFile::ReadOnly)) {
    qDebug() << "打開 PCM 文件失敗:" << filePath;
    SDL_CloseAudio();
    SDL_Quit();
    return;
}

7置尔、開始播放:

SDL_PauseAudio(0);

參數(shù) pause_on 設(shè)置為 0 開始播放音頻數(shù)據(jù)杠步;設(shè)置為 1 播放靜音值;設(shè)置為 0 時 SDL 會調(diào)用我們提供的回調(diào)函數(shù):

void call_back(void *userdata, Uint8 * stream, int len)
{
    // SDL2之后需要先清空需要填充的音頻緩沖區(qū)
    SDL_memset(stream, 0, len);
    if (bufferLen <= 0) return;
    int readLen = len < bufferLen ? len : bufferLen;
    // 填充音頻緩沖區(qū)
    SDL_MixAudio(stream, (Uint8 *)bufferData, readLen, SDL_MIX_MAXVOLUME);
    bufferData += readLen;
    bufferLen -= readLen;
}

回調(diào)函數(shù):

typedef void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream, int len);

userdata:SDL_AudioSpec 結(jié)構(gòu)體中用戶自定義的數(shù)據(jù)榜轿,可不用幽歼;
stream:指向音頻緩沖區(qū)的指針;
len:音頻緩沖區(qū)大忻巍甸私;

混音函數(shù):

extern DECLSPEC void SDLCALL SDL_MixAudio(Uint8 * dst, const Uint8 * src, Uint32 len, int volume);

dst:目標數(shù)據(jù),這里傳入音頻緩沖區(qū)指針 stream飞傀;
src:音頻數(shù)據(jù)皇型,這里傳入我們讀出的 PCM 數(shù)據(jù);
len:音頻數(shù)據(jù)長度砸烦,這里傳入音頻緩沖區(qū)大小 len弃鸦;
volume:音量,范圍 0~128幢痘,這里我們傳入 SDL_MIX_MAXVOLUME唬格,注意此參數(shù)并不會修改硬件音量;

8雪隧、讀取 PCM 文件:

char data[BUFFER_SIZE]; // 4096
while (!isInterruptionRequested()) {
    bufferLen = file.read(data, BUFFER_SIZE);
    if (bufferLen < 0) break;
    bufferData = data;
    // 延時等待音頻播放完畢 
    while (bufferLen > 0) {
        SDL_Delay(1);
    }
}

9西轩、播放結(jié)束,最后關(guān)閉音頻設(shè)備脑沿,清除 SDL 子系統(tǒng):

// 停止音頻處理藕畔,關(guān)閉音頻設(shè)備
SDL_CloseAudio();
// 清除所有初始化的 SDL 子系統(tǒng)
SDL_Quit();
三、代碼
#include <QDebug>
#include <QFile>
#include <SDL2/SDL.h>

#define BUFFER_SIZE 4096

QString filePath;
int sampleRate; // 44100
int format; // AUDIO_S16LSB
int channels; // 2

int bufferLen;
char *bufferData;

// SDL 回調(diào)取每一幀播放數(shù)據(jù)

void call_back(void *userdata, Uint8 * stream, int len)
{
    // SDL2之后需要先清空需要填充的音頻緩沖區(qū)
    SDL_memset(stream, 0, len);
    if (bufferLen <= 0) return;
    int readLen = len < bufferLen ? len : bufferLen;
    // 填充音頻緩沖區(qū)
    SDL_MixAudio(stream, (Uint8 *)bufferData, readLen, SDL_MIX_MAXVOLUME);
    bufferData += readLen;
    bufferLen -= readLen;
}

PlayThread::PlayThread(QObject *parent) : QThread(parent)
{
    connect(this, &PlayThread::finished, this, &PlayThread::deleteLater);
}

PlayThread::~PlayThread()
{
    disconnect();
    requestInterruption();
    quit();
    wait();

    qDebug() << "PlayThread 析構(gòu)函數(shù)";
}

void PlayThread::run()
{
    qDebug() << "開始播放";

    // 初始化 SDL 音頻子系統(tǒng)
    if (SDL_Init(SDL_INIT_AUDIO)) {
        qDebug() << "初始化 SDL 失敗:" << SDL_GetError();
        return;
    }
    // atexit 是 C 語言標準庫函數(shù)庄拇,作用是向系統(tǒng)注冊傳進來的函數(shù)注服,以便程序結(jié)束時調(diào)用該函數(shù)
    // 程序結(jié)束時清空 SDL 所有子系統(tǒng)
    atexit(SDL_Quit);

    SDL_AudioSpec spec;
    spec.freq = sampleRate;
    spec.format = format; //AUDIO_S16LSB
    spec.channels = channels;
    spec.samples = 1024;
    spec.callback = fill_audio;

    // 打開音頻設(shè)備
    if (SDL_OpenAudio(&spec, nullptr)) {
        qDebug() << "打開音頻設(shè)備失敗:" << SDL_GetError();
        SDL_Quit();
        return;
    }

    // 打開 PCM 文件
    QFile file(filePath);
    if (!file.open(QFile::ReadOnly)) {
        qDebug() << "打開 PCM 文件失敗:" << filePath;
        SDL_CloseAudio();
        SDL_Quit();
        return;
    }

    // 開始播放
    SDL_PauseAudio(0);

    // 讀取 PCM 文件
    char data[BUFFER_SIZE];
    while (!isInterruptionRequested()) {
        bufferLen = file.read(data, BUFFER_SIZE);
        if (bufferLen < 0) break;
        bufferData = data;
        // 延時等待音頻播放完畢
        while (bufferLen > 0) {
            SDL_Delay(1);
        }
    }

    // 停止音頻處理措近,關(guān)閉音頻設(shè)備
    SDL_CloseAudio();
    // 關(guān)閉所有 SDL 子系統(tǒng)溶弟,清理 SDL 所占資源
    SDL_Quit();

    qDebug() << "播放結(jié)束";
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市瞭郑,隨后出現(xiàn)的幾起案子辜御,更是在濱河造成了極大的恐慌,老刑警劉巖屈张,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件擒权,死亡現(xiàn)場離奇詭異袱巨,居然都是意外死亡,警方通過查閱死者的電腦和手機碳抄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門愉老,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人剖效,你說我怎么就攤上這事嫉入。” “怎么了璧尸?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵咒林,是天一觀的道長。 經(jīng)常有香客問我逗宁,道長映九,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任瞎颗,我火速辦了婚禮件甥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘哼拔。我一直安慰自己引有,他們只是感情好,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布倦逐。 她就那樣靜靜地躺著譬正,像睡著了一般。 火紅的嫁衣襯著肌膚如雪檬姥。 梳的紋絲不亂的頭發(fā)上曾我,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機與錄音健民,去河邊找鬼抒巢。 笑死,一個胖子當著我的面吹牛秉犹,可吹牛的內(nèi)容都是我干的蛉谜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼崇堵,長吁一口氣:“原來是場噩夢啊……” “哼型诚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鸳劳,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤狰贯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體暮现,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡还绘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年楚昭,在試婚紗的時候發(fā)現(xiàn)自己被綠了栖袋。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡抚太,死狀恐怖塘幅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情尿贫,我是刑警寧澤电媳,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站庆亡,受9級特大地震影響匾乓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜又谋,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一拼缝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧彰亥,春花似錦咧七、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至废酷,卻和暖如春瘟檩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背澈蟆。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工墨辛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人丰介。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓背蟆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哮幢。 傳聞我的和親對象是個殘疾皇子带膀,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容