SDL2:第五個程序:播放pcm數據

播放音頻數據對一個播放器來說是不可或缺的,索性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_AudioSpeccallback字段就能確定。爲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數據放出來吧,有興趣玩玩兒的自己動動手。

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