一鸳玩、使用 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é)束";
}