播放音頻數據對一個播放器來說是不可或缺的,索性SDL2支持播放視頻數據之外,也支持播放音頻數據。廢話不多說,先來試試吧!
使用FFmpeg命令提取PCM數據
這裏從網上下了一首歌Forevermore,非常好聽,是mp3格式的:Forevermore.mp3。
在提取Forevermore中的pcm數據時,爲了使提取數據的基本採樣率編解碼格式等不發生改變,先用ffprobe命令探測一下該原數據的基本屬性:
Superli-2:Desktop superli$ ffprobe Forevermore.mp3
ffprobe version 4.0 Copyright (c) 2007-2018 the FFmpeg developers
built with Apple LLVM version 9.1.0 (clang-902.0.39.1)
configuration:
libavutil 56. 14.100 / 56. 14.100
libavcodec 58. 18.100 / 58. 18.100
libavformat 58. 12.100 / 58. 12.100
libavdevice 58. 3.100 / 58. 3.100
libavfilter 7. 16.100 / 7. 16.100
libswscale 5. 1.100 / 5. 1.100
libswresample 3. 1.100 / 3. 1.100
Input #0, mp3, from 'Forevermore.mp3':
Metadata:
encoder : Lavf56.4.101
disc : 1
track : 10
comment : 163 key(Don't modify):L64FU3W4YxX3ZFTmbZ+8/S+68Aeva5aBGT9F8SijqsT8pNFuHKdV06BJS4v1A0bF1vFBqTyVaKDutQdoGroCvrYOOp2lT2DptMegHoLrOJSUlP8HjfG33C8WnxFZSTkpXKPuJpy2SCMpxN8Yls8kDVk0AyuBfxcgRa1C4sW5TuaaWAu70R1vzzOfeibXvpitDog/fydkmjeWDFCwEKgibbykqnCVnsMCGPJHF+0bJ
artist : Katie Herzig
title : Forevermore
album : Live In Studio: Acoustic Trio
Duration: 00:02:22.00, start: 0.025056, bitrate: 327 kb/s
Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 320 kb/s
Stream #0:1: Video: mjpeg, yuvj444p(pc, bt470bg/unknown/unknown), 640x640 [SAR 72:72 DAR 1:1], 90k tbr, 90k tbn, 90k tbc
Metadata:
comment : Other
可以用到的音頻基本數據爲:
- 聲道數:stereo(雙聲道)
- 採樣率爲:44100Hz
- 音頻格式爲:fltp
下面使用ffmpeg提取pcm數據
ffmpeg -y -i Forevermore.mp3 -acodec pcm_s16le -f s16le -ac 2 -ar 44100 ForeverMore.pcm
參數 | 說明 |
---|---|
-y | 允許覆蓋 |
-i Forevermore.mp3 | 源文件 |
-acodec pcm_s16le | 編碼器 |
-f s16le | 強制文件格式 |
-ac 2 | 雙聲道 |
-ar 44100 | 採樣率 |
用ffplay播放一下:
ffplay -ar 44100 -channels 2 -f s16le -i ForeverMore.pcm
可以正常播放,沒啥問題。
數據準備好了,準備用SDL播放吧。
播放PCM數據的調用邏輯
邏輯不是很複雜,如下:
- SDL初始化音頻模塊:
SDL_Init
- 打開音頻設備:
SDL_OpenAudioDevice
- 開始等待播放:
SDL_PauseAudioDevice
- 填充數據:
SDL_QueueAudio
下面一個個分開說。
SDL初始化音頻模塊
SDL初始化個大模塊的函數原型是:
int SDL_Init(Uint32 flags)
參數:是指定需要初始化的模塊類型,可供選擇的模塊定義如下:
flags | 子系統 |
---|---|
SDL_INIT_TIMER | 計時器子系統 |
SDL_INIT_AUDIO | 音頻子系統 |
SDL_INIT_VIDEO | 視頻子系統; 自動初始化事件子系統 |
SDL_INIT_JOYSTICK | 操縱桿子系統; 自動初始化事件子系統 |
SDL_INIT_HAPTIC | 觸覺(力反饋)子系統 |
SDL_INIT_GAMECONTROLLER | 控制子系統; 自動初始化操縱桿子系統 |
SDL_INIT_EVENTS | 事件子系統 |
SDL_INIT_EVERYTHING | 所有上述子系統 |
SDL_INIT_NOPARACHUTE | 兼容性; 這個標誌被忽略了 |
返回值:如果是0表示初始化成功,非0表示失敗,通過SDL_GetError()
調用查看失敗信息。
###打開音頻設備:SDL_OpenAudioDevice
函數原型爲:
SDL_AudioDeviceID SDL_OpenAudioDevice(const char* device,
int iscapture,
const SDL_AudioSpec* desired,
SDL_AudioSpec* obtained,
int allowed_changes)
功能:該函數打開一個指定的音頻設備。
參數列表:
device | 一個UTF-8類型的字符串,可以通過SDL_GetAudioDeviceName函數獲取,不關心的傳NULL就可以了 |
---|---|
iscapture | 該值非0的話,表示打開設備的目的是爲了錄製,而不是播放,你看SDL功能真強大 |
desired | 一個SDL_AudioSpec結構體,用來表示期望的輸出格式。 |
obtained | 一個SDL_AudioSpec結構體,用來表示設備實際的輸出格式。沒用的話,傳NULL也是可以的 |
allowed_changes | 0或者其它如下表的值,用於表示播放時音頻允許什麼樣的變化 |
allowed_changes | |
---|---|
SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | 允許幀率改變 |
SDL_AUDIO_ALLOW_FORMAT_CHANGE | 允許格式改變 |
SDL_AUDIO_ALLOW_CHANNELS_CHANGE | 允許聲道數改變 |
SDL_AUDIO_ALLOW_ANY_CHANGE | 允許以上任何改變 |
返回值:0表示出現異常,合法的設備ID值應該大於等於2。
SDL_AudioSpec
這裏需要說明一下SDL_AudioSpec結構體,原型如下:
/**
* The calculated values in this structure are calculated by SDL_OpenAudio().
*
* For multi-channel audio, the default SDL channel mapping is:
* 2: FL FR (stereo)
* 3: FL FR LFE (2.1 surround)
* 4: FL FR BL BR (quad)
* 5: FL FR FC BL BR (quad + center)
* 6: FL FR FC LFE SL SR (5.1 surround - last two can also be BL BR)
* 7: FL FR FC LFE BC SL SR (6.1 surround)
* 8: FL FR FC LFE BL BR SL SR (7.1 surround)
*/
typedef struct SDL_AudioSpec
{
int freq; /**< DSP頻率 -- 即每秒的採樣數 */
SDL_AudioFormat format; /**< 音頻格式 */
Uint8 channels; /**< 聲道數: 1 mono, 2 stereo */
Uint8 silence; /**< 音頻緩衝區靜音值(calculated) */
Uint16 samples; /**< 樣本幀中的音頻緩衝區大小(總樣本除以通道數)*/
Uint16 padding; /**< 一些編譯環境所必需的 */
Uint32 size; /**< 音頻緩衝區大小(以字節爲單位) */
SDL_AudioCallback callback; /**< 爲音頻設備提供的回調(NULL表示使用SDL_QueueAudio()). */
void *userdata; /**< Userdata傳遞給回調(忽略NULL回調). */
} SDL_AudioSpec;
SDL_AudioFormat
,決定了PCM數據採樣位深以及存儲方式,這個以後單獨拎出來講。在SDL中,SDL_AudioFormat可能值如下表:
8-bit support | 支持8比特的格式有 |
---|---|
AUDIO_S8 | 有符號,8bit採樣深度 |
AUDIO_U8 | 無符號,8bit採樣深度 |
16-bit support | 支持16比特的格式有 |
AUDIO_S16LSB | 有符號16-bit,小端字節序 |
AUDIO_S16MSB | 有符號16-bit,大端字節序 |
AUDIO_S16SYS | 有符號16-bit,本地字節序 |
AUDIO_S16 | AUDIO_S16LSB |
AUDIO_U16LSB | 無符號16-bit,小端字節序 |
AUDIO_U16MSB | 無符號16-bit,大端字節序 |
AUDIO_U16SYS | 無符號16-bit,本地字節序 |
AUDIO_U16 | AUDIO_U16LSB |
32-bit support (new to SDL 2.0) | 支持32比特的格式有 |
AUDIO_S32LSB | 32-bit整數採樣深度,小端字節序 |
AUDIO_S32MSB | 32-bit整數採樣深度,大端字節序 |
AUDIO_S32SYS | 32-bit整數採樣深度,本地字節序 |
AUDIO_S32 | AUDIO_S32LSB |
float support (new to SDL 2.0) | 支持浮點型的格式有 |
AUDIO_F32LSB | 32-bit浮點型採樣,小端字節序 |
AUDIO_F32MSB | 32-bit浮點型採樣,大端字節序 |
AUDIO_F32SYS | 32-bit浮點型採樣,本地字節序 |
AUDIO_F32 | AUDIO_F32LSB |
自定義回調函數SDL_AudioCallback
單獨說一下SDL_AudioCallback函數吧,函數原型爲:
/**
* \param userdata 應用指定的參數。保存在SDL_AudioSpec的userdata字段,該函數回調時會回傳。
* \param stream 一個存儲音頻的buffer指針,回調時應該指向需要播放的數據指針。
* \param len 音頻數據buffer長度,bit爲單位。
*/
typedef void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream,
int len);
功能:該函數被音頻設備回調,用於填充需要播放的音頻數據。
參數的作用已在函數doc中表明。
注意:在SDL2中,共提供了兩套用於播放音頻數據的API:
SDL_AudioSpec | 打開音頻設備 | 等待數據播放 | 數據填充 | |
---|---|---|---|---|
第一套 | callback = mCallback | SDL_OpenAudio | SDL_PauseAudio | SDL_AudioCallback |
第二套 | callback = NULL | SDL_OpenAudioDevice | SDL_PauseAudioDevice | SDL_QueueAudio |
兩套API不能相互混用,會播放不起來的。
確定用那套API,從SDL_AudioSpec
的callback
字段就能確定。爲NULL
的使用第二套API,否則使用第一套。
優缺點:
- 第一套:第一套實際上已經被SDL2打上了遺留函數(Legacy)的標籤,好處只有一個兼容性了。缺點是,需要自己實現回調函數,顯得很麻煩,結構不簡潔。
- 第二套:優點和缺點除了和第一套相反外,填充數據更加高效。本文使用的就是這種,不僅少了實現回調函數的代碼,而且不用考慮填充數據的時間。
目前網絡上大量流行的是第一套API,感覺源頭差不多都是雷神。我只能說,膜拜大神,但不能盲從。所以本文使用了第二套API,估計這是網上首次使用第二套API的博客了。
想要看第一套API demo的小夥伴,可以去雷神的博客:傳送門
想要看第二套API demo的,就老實留下來,因爲你可能找不到別的了。
等待播放:SDL_PauseAudioDevice
函數原型:
void SDL_PauseAudioDevice(SDL_AudioDeviceID dev, int pause_on)
功能:使用此功能暫停或取消暫停指定設備上的音頻播放。
參數列表:
dev | 設備ID,調用SDL_OpenAudioDevice函數返回值可以獲取設備ID |
---|---|
pause_on | 0取消暫停,非0表示暫停 |
填充數據:SDL_QueueAudio
函數原型:
int SDL_QueueAudio(SDL_AudioDeviceID dev, const void* data, Uint32 len)
功能:使用此函數可以在回調設備(即使用了第一套API需要回調函數填充數據的設備)上緩存更多音頻,而不必通過回調函數填充音頻數據。
參數列表:
dev | 設備ID |
---|---|
data | 需要被填充的數據指針 |
len | 數據buffer長度,byte爲單位 |
返回值:0表示工程,非零表示出現異常,可以通過SDL_GetError()獲取更多異常信息。
接下來,上代碼
代碼
CMakeLists.txt文件:
cmake_minimum_required(VERSION 3.10)
project(PlaySDL)
set(CMAKE_CXX_STANDARD 11)
set(MY_LIBRARY_DIR /usr/local/Cellar)
set(SDL_DIR ${MY_LIBRARY_DIR}/sdl2/2.0.9_1/)
set(SDL_IMAGE_DIR ${MY_LIBRARY_DIR}/sdl2_image/2.0.4/)
include_directories(${SDL_DIR}/include/SDL2/
${SDL_IMAGE_DIR}/include/)
link_libraries(${SDL_DIR}/lib/
${SDL_IMAGE_DIR}/lib/)
add_executable(PlaySDL main.cpap)
target_link_libraries(PlaySDL SDL2)
Main.cpp文件:
#include <iostream>
#include <SDL.h>
using namespace std;
int main() {
if (SDL_Init(SDL_INIT_AUDIO) < 0) {
cout << "SDL could not initialized with error: " << SDL_GetError() << endl;
return -1;
}
SDL_AudioSpec desired_spec;
desired_spec.freq = 44100;
desired_spec.format = AUDIO_S16SYS;
desired_spec.channels = 2;
desired_spec.silence = 0;
desired_spec.samples = 1024;
desired_spec.callback = NULL;
SDL_AudioDeviceID deviceID;
if ((deviceID = SDL_OpenAudioDevice(NULL, 0, &desired_spec, NULL, SDL_AUDIO_ALLOW_ANY_CHANGE)) < 2) {
cout << "SDL_OpenAudioDevice with error deviceID : " << deviceID << endl;
return -1;
}
FILE *pFile = fopen("ForeverMore_44100_2_16.pcm", "rb");
if (pFile == NULL) {
cerr << "ForeverMore_44100_2_16.pcm open failed" << endl;
}
Uint32 buffer_size = 4096;
char *buffer = (char *) malloc(buffer_size);
SDL_PauseAudioDevice(deviceID, 0);
while (true) {
if (fread(buffer, 1, buffer_size, pFile) != buffer_size) {
cerr << "end of file" << endl;
break;
}
SDL_QueueAudio(deviceID, buffer, buffer_size);
}
SDL_Delay(3*1000*60); // 暫停3分鐘,等待播放完成。在做播放器時,可以通過ffmpeg獲取duration時間,做適當延遲。
SDL_CloseAudio();
SDL_Quit();
fclose(pFile);
}
已經提供提出PCM數據的方法了,這裏就不把PCM數據放出來吧,有興趣玩玩兒的自己動動手。