七、FFmpeg 4.0.2 + SDL2 播放音訊

Mirs發表於2019-04-10

開始前的BB

播完視訊播音訊,不著急我們一步步的來,胖子不是一口吃成的

七、FFmpeg 4.0.2 + SDL2 播放音訊

本章開始之前希望大家有基本的音訊的一些基礎知識,包括且不限於(我這句真官方?)

  • 音訊取樣
  • 取樣率
  • 取樣格式(位寬/位數)
  • 聲道數
  • 聲道佈局 (單聲道,雙聲道,5.1 7.1)

SDL2 音訊播放流程簡介

SDL2 音訊播放比視訊播放要複雜一點,我儘量通俗點解釋流程

  1. 首先初始化一個結構體SDL_AudioSpec,這個結構體放著你想要播放的音訊的格式
  2. 宣告一個函式void fill_audio(void * codecContext, Uint8 *stream, int len),這個函式指標被SDL_AudioSpec.callback引用,第一個void*的引數為SDL_AudioSpec.userdata,這個是使用者自己設定的一個資料的指標,第二個就是當前音訊裝置需要喂入的資料指標,第三個是需要的喂入的音訊取樣長度,這個方法會在裝置需要播放音訊的時候被動呼叫去獲取需要播放的音訊取樣
  3. 呼叫SDL_OpenAudio(desired,obtained)開啟音訊,這個方法比較奇怪,desired 傳進去之後,開啟裝置,如果obtained不為null,則硬體的實際引數將賦值到obtained,如果它為NULL,則傳遞出去的音訊資料將自動轉為硬體的音訊格式,如果返回-1,則說明開啟裝置或者設定音訊執行緒失敗
  4. 在FFmpeg解碼返回一幀PCM資料後,需要把這個資料播完才能繼續播下一幀資料

大概就是這麼個流程,我們上程式碼對應程式碼看的話就比較清晰點了

FFmpeg 解析音訊 + SDL2 播放

這邊為了大家整塊的看的清楚 我直接貼上來所有的程式碼,裡面有詳細的註釋

//
// Created by MirsFang on 2019-03-20.
//
namespace sdl_audio {

#include <iostream>

extern "C" {

#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
#include <SDL2/SDL.h>

}
using namespace std;

/**
 * 準備 ffmpeg
 * @param url 視訊路徑
 */
void preparFFmpeg(const char *url);

/**
 * 釋放記憶體
 */
void free();

#define MAX_AUDIO_FRAME_SIZE 192000 // 1 second of 48khz 32bit audio   48000 * (32/8)

//一幀PCM的資料長度
unsigned int audioLen = 0;
unsigned char *audioChunk = nullptr;
//當前讀取的位置
unsigned char *audioPos = nullptr;

/** 被SDL2呼叫的回撥函式 當需要獲取資料喂入硬體播放的時候呼叫 **/
void fill_audio(void *codecContext, Uint8 *stream, int len) {
    //SDL2中必須首先使用SDL_memset()將stream中的資料設定為0
    SDL_memset(stream, 0, len);
    if (audioLen == 0)
        return;

    len = (len > audioLen ? audioLen : len);
    //將資料合併到 stream 裡
    SDL_MixAudio(stream, audioPos, len, SDL_MIX_MAXVOLUME);

    //一幀的資料控制
    audioPos += len;
    audioLen -= len;
}



/** ########### SDL初始化 ############## **/
/** 自己想要的輸出的音訊格式 **/
SDL_AudioSpec wantSpec;
/** 重取樣上下文 **/
SwrContext *auConvertContext;

/** ########### FFmpeg 相關 ############# **/
AVFormatContext *formatContext;
AVCodecContext *codecContext;
AVCodec *codec;
AVPacket *packet;
AVFrame *frame;
int audioIndex = -1;

uint64_t out_chn_layout = AV_CH_LAYOUT_STEREO;  //輸出的通道佈局 雙聲道
enum AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16; //輸出的聲音格式
int out_sample_rate = 44100;   //輸出的取樣率
int out_nb_samples = -1;        //輸出的音訊取樣
int out_channels = -1;        //輸出的通道數
int out_buffer_size = -1;   //輸出buff大小
unsigned char *outBuff = NULL;//輸出的Buffer資料
uint64_t in_chn_layout = -1;  //輸入的通道佈局

/** 外部呼叫方法 **/
void playAudio(const char *url) {
    /** ########### 初始化FFmpeg ############# **/
    preparFFmpeg(url);
    /** ########## 獲取實際音訊的引數 ##########**/
    //單個通道中的取樣數
    out_nb_samples = codecContext->frame_size;
    //輸出的聲道數
    out_channels = av_get_channel_layout_nb_channels(out_chn_layout);
    //輸出音訊的佈局
    in_chn_layout = av_get_default_channel_layout(codecContext->channels);

    /** 計算重取樣後的實際資料大小,並分配空間 **/
    //計算輸出的buffer的大小
    out_buffer_size = av_samples_get_buffer_size(NULL, out_channels, out_nb_samples, out_sample_fmt, 1);
    //分配輸出buffer的空間
    outBuff = (unsigned char *) av_malloc(MAX_AUDIO_FRAME_SIZE * 2); //雙聲道

    //初始化SDL中自己想設定的引數
    wantSpec.freq = out_sample_rate;
    wantSpec.format = AUDIO_S16SYS;
    wantSpec.channels = out_channels;
    wantSpec.silence = 0;
    wantSpec.samples = out_nb_samples;
    wantSpec.callback = fill_audio;
    wantSpec.userdata = codecContext;

    //開啟音訊之後wantSpec的值可能會有改動,返回實際裝置的引數值
    if (SDL_OpenAudio(&wantSpec, NULL) < 0) {
        cout << "[error] open audio error" << endl;
        return;
    }
    //初始化重取樣器
    auConvertContext = swr_alloc_set_opts(NULL, out_chn_layout, out_sample_fmt, out_sample_rate,
                                          in_chn_layout, codecContext->sample_fmt, codecContext->sample_rate, 0,
                                          NULL);
    //初始化SwResample的Context
    swr_init(auConvertContext);

    //開始播放 呼叫這個方法硬體才會開始播放
    SDL_PauseAudio(0);

    //迴圈讀取packet並且解碼
    int sendcode = 0;
    while (av_read_frame(formatContext, packet) >= 0) {
        if (packet->stream_index != audioIndex)continue;
        //接受解碼後的音訊資料
        while (avcodec_receive_frame(codecContext, frame) == 0) {
            swr_convert(auConvertContext, &outBuff, MAX_AUDIO_FRAME_SIZE, (const uint8_t **) frame->data,
                        frame->nb_samples);
            //如果沒有播放完就等待1ms
            while (audioLen > 0)
                SDL_Delay(1);
            //同步資料
            audioChunk = (unsigned char *) outBuff;
            audioPos = audioChunk;
            audioLen = out_buffer_size;
        }
        //傳送解碼前的包資料
        sendcode = avcodec_send_packet(codecContext, packet);
        //根據傳送的返回值判斷狀態
        if (sendcode == 0) {
            cout << "[debug] " << "SUCCESS" << endl;
        } else if (sendcode == AVERROR_EOF) {
            cout << "[debug] " << "EOF" << endl;
        } else if (sendcode == AVERROR(EAGAIN)) {
            cout << "[debug] " << "EAGAIN" << endl;
        } else {
            cout << "[debug] " << av_err2str(AVERROR(sendcode)) << endl;
        }

        av_packet_unref(packet);
    }


}

/** 準備FFmpeg **/
void preparFFmpeg(const char *url) {
    int retcode;
    //初始化FormatContext
    formatContext = avformat_alloc_context();
    if (!formatContext) {
        cout << "[error] alloc format context error!" << endl;
        return;
    }

    //開啟輸入流
    retcode = avformat_open_input(&formatContext, url, nullptr, nullptr);
    if (retcode != 0) {
        cout << "[error] open input error!" << endl;
        return;
    }

    //讀取媒體檔案資訊
    retcode = avformat_find_stream_info(formatContext, NULL);
    if (retcode != 0) {
        cout << "[error] find stream error!" << endl;
        return;
    }

    //分配codecContext
    codecContext = avcodec_alloc_context3(NULL);
    if (!codecContext) {
        cout << "[error] alloc codec context error!" << endl;
        return;
    }

    //尋找到音訊流的下標
    audioIndex = av_find_best_stream(formatContext, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    //將視訊流的的編解碼資訊拷貝到codecContext中
    retcode = avcodec_parameters_to_context(codecContext, formatContext->streams[audioIndex]->codecpar);
    if (retcode != 0) {
        cout << "[error] parameters to context error!" << endl;
        return;
    }

    //查詢解碼器
    codec = avcodec_find_decoder(codecContext->codec_id);
    if (codec == nullptr) {
        cout << "[error] find decoder error!" << endl;
        return;
    }

    //開啟解碼器
    retcode = avcodec_open2(codecContext, codec, nullptr);
    if (retcode != 0) {
        cout << "[error] open decodec error!" << endl;
        return;
    }

    //初始化一個packet
    packet = av_packet_alloc();
    //初始化一個Frame
    frame = av_frame_alloc();

}

void free(){
    if (formatContext != nullptr) avformat_close_input(&formatContext);
    if (codecContext != nullptr) avcodec_free_context(&codecContext);
    if (packet != nullptr) av_packet_free(&packet);
    if (frame != nullptr) av_frame_free(&frame);
    if (auConvertContext != nullptr) swr_free(&auConvertContext);
    SDL_CloseAudio();
    SDL_Quit();

}

}
複製程式碼

我們在main方法中

  sdl_audio::playAudio(url);
複製程式碼

我們就能聽到聲音了

未完持續 。。。

相關文章