ffplay
可以使用ffplay播放《音頻錄制02_編程》中錄制好的PCM文件,測試一下是否錄制成功。
播放PCM需要指定相關參數:
- ar:采樣率
- ac:聲道數
- f:采樣格式
- s16le:PCM signed 16-bit little-endian
- 更多PCM的采樣格式可以使用命令查看
- Windows:ffmpeg -formats | findstr PCM
- Mac:ffmpeg -formats | grep PCM
ffplay -ar 44100 -ac 2 -f s16le out.pcm
接下來演示一下,如何通過編程的方式播放PCM數據。
SDL
ffplay是基於FFmpeg、SDL兩個庫實現的。通過編程的方式播放音視頻,也是需要用到這2個庫。FFmpeg大家都已經清楚了,比較陌生的是SDL。
簡介
SDL(Simple DirectMedia Layer),是一個跨平台的C語言多媒體開發庫。
- 支持Windows、Mac OS X、Linux、iOS、Android
- 提供對音頻、鍵盤、鼠標、游戲操縱桿、圖形硬件的底層訪問
- 很多的視頻播放軟件、模擬器、受歡迎的游戲都在使用它
- 目前最新的穩定版是:2.0.14
- API文檔:wiki
下載
SDL官網下載地址:download-sdl2。
Windows
由於我們使用的是MinGW編譯器,所以選擇下載SDL2-devel-2.0.14-mingw.tar.gz。
解壓后的目錄結構如下圖所示,跟FFmpeg的目錄結構類似,因此就不再贅述每個文件夾的作用。
Mac
從brew官網可以看得出來:之前執行brew install ffmpeg時,已經順帶安裝了SDL,安裝目錄是:/usr/local/Cellar/sdl2。
如果沒有這個目錄,就執行brew install sdl2進行安裝即可。
HelloWorld
來個簡單的SDL HelloWorld吧,打印一下SDL的版本號。
.pro文件
win32 {
FFMPEG_HOME = F:/Dev/ffmpeg-4.3.2
SDL_HOME = F:/Dev/SDL2-2.0.14/x86_64-w64-mingw32
}
macx {
FFMPEG_HOME = /usr/local/Cellar/ffmpeg/4.3.2
SDL_HOME = /usr/local/Cellar/sdl2/2.0.14_1
}
INCLUDEPATH += $${FFMPEG_HOME}/include
LIBS += -L$${FFMPEG_HOME}/lib \
-lavdevice \
-lavcodec \
-lavformat \
-lavutil
INCLUDEPATH += $${SDL_HOME}/include
LIBS += -L$${SDL_HOME}/lib \
-lSDL2
在Windows環境中,還需要處理一下dll文件,參考:《dll文件處理》。
cpp代碼
#include <SDL2/SDL.h>
SDL_version v;
SDL_VERSION(&v);
// 2 0 14
qDebug() << v.major << v.minor << v.patch;
播放PCM
初始化子系統
SDL分成好多個子系統(subsystem):
- Video:顯示和窗口管理
- Audio:音頻設備管理
- Joystick:游戲搖桿控制
- Timers:定時器
- ...
目前只用到了音頻功能,所以只需要通過SDL_init函數初始化Audio子系統即可。
// 初始化Audio子系統
if (SDL_Init(SDL_INIT_AUDIO)) {
// 返回值不是0,就代表失敗
qDebug() << "SDL_Init Error" << SDL_GetError();
return;
}
打開音頻設備
/* 一些宏定義 */
// 采樣率
#define SAMPLE_RATE 44100
// 采樣格式
#define SAMPLE_FORMAT AUDIO_S16LSB
// 采樣大小
#define SAMPLE_SIZE SDL_AUDIO_BITSIZE(SAMPLE_FORMAT)
// 聲道數
#define CHANNELS 2
// 音頻緩沖區的樣本數量
#define SAMPLES 1024
// 用於存儲讀取的音頻數據和長度
typedef struct {
int len = 0;
int pullLen = 0;
Uint8 *data = nullptr;
} AudioBuffer;
// 音頻參數
SDL_AudioSpec spec;
// 采樣率
spec.freq = SAMPLE_RATE;
// 采樣格式(s16le)
spec.format = SAMPLE_FORMAT;
// 聲道數
spec.channels = CHANNELS;
// 音頻緩沖區的樣本數量(這個值必須是2的冪)
spec.samples = SAMPLES;
// 回調
spec.callback = pull_audio_data;
// 傳遞給回調的參數
AudioBuffer buffer;
spec.userdata = &buffer;
// 打開音頻設備
if (SDL_OpenAudio(&spec, nullptr)) {
qDebug() << "SDL_OpenAudio Error" << SDL_GetError();
// 清除所有初始化的子系統
SDL_Quit();
return;
}
打開文件
#define FILENAME "F:/in.pcm"
// 打開文件
QFile file(FILENAME);
if (!file.open(QFile::ReadOnly)) {
qDebug() << "文件打開失敗" << FILENAME;
// 關閉音頻設備
SDL_CloseAudio();
// 清除所有初始化的子系統
SDL_Quit();
return;
}
開始播放
// 每個樣本占用多少個字節
#define BYTES_PER_SAMPLE ((SAMPLE_SIZE * CHANNELS) / 8)
// 文件緩沖區的大小
#define BUFFER_SIZE (SAMPLES * BYTES_PER_SAMPLE)
// 開始播放
SDL_PauseAudio(0);
// 存放文件數據
Uint8 data[BUFFER_LEN];
while (!isInterruptionRequested()) {
// 只要從文件中讀取的音頻數據,還沒有填充完畢,就跳過
if (buffer.len > 0) continue;
buffer.len = file.read((char *) data, BUFFER_SIZE);
// 文件數據已經讀取完畢
if (buffer.len <= 0) {
// 剩余的樣本數量
int samples = buffer.pullLen / BYTES_PER_SAMPLE;
int ms = samples * 1000 / SAMPLE_RATE;
SDL_Delay(ms);
break;
}
// 讀取到了文件數據
buffer.data = data;
}
回調函數
// userdata:SDL_AudioSpec.userdata
// stream:音頻緩沖區(需要將音頻數據填充到這個緩沖區)
// len:音頻緩沖區的大小(SDL_AudioSpec.samples * 每個樣本的大小)
void pull_audio_data(void *userdata, Uint8 *stream, int len) {
// 清空stream
SDL_memset(stream, 0, len);
// 取出緩沖信息
AudioBuffer *buffer = (AudioBuffer *) userdata;
if (buffer->len == 0) return;
// 取len、bufferLen的最小值(為了保證數據安全,防止指針越界)
buffer->pullLen = (len > buffer->len) ? buffer->len : len;
// 填充數據
SDL_MixAudio(stream,
buffer->data,
buffer->pullLen,
SDL_MIX_MAXVOLUME);
buffer->data += buffer->pullLen;
buffer->len -= buffer->pullLen;
}
釋放資源
// 關閉文件
file.close();
// 關閉音頻設備
SDL_CloseAudio();
// 清理所有初始化的子系統
SDL_Quit();