一篇文章助你入門FFmpeg編程

1. 前言

FFmpeg是一個強大的音視頻處理庫,可是一般接觸時以命令形式較多。本篇文章講了FFmpeg相關api的使用,尤爲是它強大的過濾器filter庫的使用。html

1.1 能學到什麼

  • Android下集成FFmpeg
  • 使用avcodec解碼庫解碼音頻
  • 使用avfilter過濾器對音頻作變速,調音,混音等處理
  • C/C++下多線程編程,生產者/消費者實現
  • NDK下經過OpenSL ES進行音頻播放
  • NDK下對音頻進行播放控制

1.2 實現了啥

本項目主要素材爲five hundred mile吉他,尤克里裏,鼓等4個音軌素材。實現多音軌實時播放,多音軌音量調節,變速播放,進度調節等功能java

1.3 項目地址

github.com/iamyours/FF…linux

2. FFmpeg動態庫編譯

2.1 下載NDK和FFmpeg

Android Studio默認下載對NDK版本會出現一些兼容問題,所以咱們這裏使用ndk-r15c(win64|linux64|mac64)版本。 FFmpeg官網下載源碼,我用的是3.2.12 android

2.2 解壓文件

首先解壓NDK和ffmpeggit

tar -zxf ffmpeg-3.2.12.tar.gz
unzip android-ndk-r15c-darwin-x86_64.zip -d android-ndk-r15c
複製代碼

2.3 修改FFmpeg配置,適配Android

進入ffmpeg目錄,修改configure文件github

SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'
SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR)$(SLIBNAME)'
複製代碼

替換爲編程

SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'
複製代碼

2.4 編寫FFmpeg腳本,生成動態so庫

新建build_android.sh腳本windows

#!/bin/sh
NDK=/Users/xxx/Desktop/soft/android-ndk-r15c
SYSROOT=$NDK/platforms/android-21/arch-arm
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64
function build_one
{
./configure \
--prefix=$PREFIX \
--enable-shared \
--disable-static \
--disable-doc \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-avdevice \
--disable-doc \
--disable-symver \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--target-os=linux \
--arch=arm \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $ADDI_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
$ADDITIONAL_CONFIGURE_FLAG
make clean
make
make install
}
CPU=arm
PREFIX=$(pwd)/android/$CPU
ADDI_CFLAGS="-marm"
build_one
複製代碼

添加執行權限,執行sh腳本api

chmod +x build_android.sh
./build_android.sh
複製代碼

整個編譯花了10分鐘左右(mbp i5配置),編譯完成後,能夠在android目錄看到相關so文件和頭文件 數組

so文件目錄

3. 將FFmpeg加入到Android項目中

3.1 新建Android項目,添加C++支持

打開Android Studio,新建項目FFmpegAudioPlayer,添加C++支持

NDK支持

3.2 配置FFmpeg動態庫

在src下的main文件中建立jniLibs文件夾,在jniLibs建立armeabi文件夾,將ffmpeg下android/arm/lib/目錄下的so文件(libavcodec-57.so/libavfilter-6.so/libavformat-57.so/libavutil-55.so/libswresample-2.so/libswscale-4.so)拷貝至此目錄。將android/arm/include 整個目錄拷貝至jniLibs下,最終目錄以下

工程目錄
修改app/build.gradle文件,添加abiFilters

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            ndk{
                abiFilters "armeabi"
            }
        }
    }
...
}
複製代碼

打開app目錄下的CMakeLists.txt文件,修改爲以下配置

cmake_minimum_required(VERSION 3.4.1)
add_library( native-lib
             SHARED
             src/main/cpp/native-lib.cpp)
find_library( log-lib
              log )
find_library( android-lib
              android )
set(distribution_DIR ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})


add_library( avutil-55
             SHARED
             IMPORTED )
set_target_properties( avutil-55
                       PROPERTIES IMPORTED_LOCATION
                       ${distribution_DIR}/libavutil-55.so)

...
# 同上還要經過add_library,set_target_properties 
# 設置swresample-2,avcodec-57,avfilter-6,swscale-4avformat-57 
...

set(CMAKE_VERBOSE_MAKEFILE on)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
include_directories(src/main/cpp)
include_directories(src/main/jniLibs/include)

target_link_libraries(native-lib
                      avutil-55       #工具庫
                      swresample-2    #音頻採樣數據格式轉換
                      avcodec-57      #編解碼
                      avfilter-6      #濾鏡特效處理
                      swscale-4       #視頻像素數據格式轉換
                      avformat-57     #封裝格式處理
                      OpenSLES
                      ${log-lib}
                      ${android-lib})
複製代碼

配置完成後,咱們先編譯運行一次,若是能順利成功安裝到手機上,正常運行,則說明配置正確。

4. 解碼mp3爲pcm

FFmpeg的第一個強大之處是它的編解碼能力。它能夠將市面上的任意一種音頻格式(mp3,wav,aac,ogg等)和視頻格式(mp4,avi,rm,rmvb,mov等)解碼。經過解碼器將音頻視頻解碼成一個個AVFrame,每一個frame包含了音頻的pcm信息或視頻的yuv信息。經過編碼器,FFmpeg可將frame編碼成不一樣格式的音視頻文件。所以咱們能夠用FFmpeg很簡單的實現格式轉換,而不須要了解各類格式的相關協議。

4.1 解碼流程

爲了可以解碼mp3文件,須要經過ffmpeg讀取音頻信息,而後獲得對應的解碼器,而後循環讀取每一幀音頻數據,而且經過解碼器解碼。大體解碼流程以下:

解碼流程

4.2 完整代碼

在MainActivity.kt中引入so庫

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun decodeAudio(v: View) {
        val src = "${Environment.getExternalStorageDirectory()}/test1.mp3"
        val out = "${Environment.getExternalStorageDirectory()}/out.pcm"
        decodeAudio(src, out)
    }

    external fun decodeAudio(src: String, out: String)
    companion object {
        init {
            System.loadLibrary("avutil-55")
            System.loadLibrary("swresample-2")
            System.loadLibrary("avcodec-57")
            System.loadLibrary("avfilter-6")
            System.loadLibrary("swscale-4")
            System.loadLibrary("avformat-57")
            System.loadLibrary("native-lib")
        }
    }
}
複製代碼

在native-lib.cpp中編寫音頻解碼代碼

#include <jni.h>
#include <android/log.h>
#include <string>

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
}
#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"FFmpegAudioPlayer",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"FFmpegAudioPlayer",FORMAT,##__VA_ARGS__);
extern "C" JNIEXPORT void JNICALL Java_io_github_iamyours_ffmpegaudioplayer_MainActivity_decodeAudio( JNIEnv *env, jobject /* this */, jstring _src, jstring _out) {
    const char *src = env->GetStringUTFChars(_src, 0);
    const char *out = env->GetStringUTFChars(_out, 0);

    av_register_all();//註冊全部容器解碼器
    AVFormatContext *fmt_ctx = avformat_alloc_context();

    if (avformat_open_input(&fmt_ctx, src, NULL, NULL) < 0) {//打開文件
        LOGE("open file error");
        return;
    }
    if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {//讀取音頻格式文件信息
        LOGE("find stream info error");
        return;
    }
    //獲取音頻索引
    int audio_stream_index = -1;
    for (int i = 0; i < fmt_ctx->nb_streams; i++) {
        if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            audio_stream_index = i;
            LOGI("find audio stream index");
            break;
        }
    }
    //獲取解碼器
    AVCodecContext *codec_ctx = avcodec_alloc_context3(NULL);
    avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[audio_stream_index]->codecpar);
    AVCodec *codec = avcodec_find_decoder(codec_ctx->codec_id);
    //打開解碼器
    if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
        LOGE("could not open codec");
        return;
    }
    //分配AVPacket和AVFrame內存,用於接收音頻數據,解碼數據
    AVPacket *packet = av_packet_alloc();
    AVFrame *frame = av_frame_alloc();
    int got_frame;//接收解碼結果
    int index = 0;
    //pcm輸出文件
    FILE *out_file = fopen(out, "wb");
    while (av_read_frame(fmt_ctx, packet) == 0) {//將音頻數據讀入packet
        if (packet->stream_index == audio_stream_index) {//取音頻索引packet
            if (avcodec_decode_audio4(codec_ctx, frame, &got_frame, packet) <
                0) {//將packet解碼成AVFrame
                LOGE("decode error:%d", index);
                break;
            }
            if (got_frame > 0) {
                LOGI("decode frame:%d", index++);
                fwrite(frame->data[0], 1, static_cast<size_t>(frame->linesize[0]),
                       out_file); //想將單個聲道pcm數據寫入文件

            }
        }
    }
    LOGI("decode finish...");
    //釋放資源
    av_packet_unref(packet);
    av_frame_free(&frame);
    avcodec_close(codec_ctx);
    avformat_close_input(&fmt_ctx);
    fclose(out_file);
}
複製代碼

注意添加文件權限,將測試音頻test1.mp3放入手機sd卡中,點擊解碼按鈕,完成後,咱們就能夠看到pcm文件了,能夠經過Audition打開(mac下能夠經過Parallels Desktop裝xp使用軟件,融合模式不要太好用),選擇48000hz,1聲道(只寫入了一個聲道),打開後,就能夠經過Audition查看和播放pcm文件了。

Adobe Audition CS6打開pcm文件

5. 單輸入AVFilter過濾器

FFmpeg另外一個強大之處在於它實現了各式各樣的filter,能夠將音視頻出來成不一樣的效果,視頻能夠裁剪、縮放、旋轉、合併、添加水印等效果,音頻能夠去噪、回聲、延遲、混音、變速等效果。一個filter的輸出能夠做爲另外一個filter的輸入。經過filter組合使用,咱們能夠定製本身想要的音視頻特效。這次分兩節講兩種音頻filter的api用法,一種是單個輸入volume(音量調節),atempo(變速)

5.1 單輸入音頻過濾處理流程

音頻解碼後,能夠avfilter api對解碼出來的AVFrame進行效果處理,如音量調節,變速處理。多個音頻輸入還能夠進行混音處理(見6.1) 單輸入過濾器解碼流程

解碼出AVFrame -> abuffer-> 其餘過濾器(volume)...->aformat->abuffersink->過濾後的AVFrame
複製代碼

這裏看到有三個通用的過濾器,abuffer,aformat,abuffersink。 abuffer用於接收輸入frame,造成待處理的數據緩存,abuffersink用於傳出輸出Frame,aformat過濾器約束最終的輸出格式(採樣率,聲道數,存儲位數等),這三個不可缺乏。 而中間的其餘過濾器能夠串聯多個filter,如volume,atempo

5.2 過濾器初始化

這裏咱們先要知道三個重要的結構體 AVFilterGraph (管理全部的filter) AVFilterContext (filter上下文) AVFilter(具體過濾器)

過濾器初始化

5.3 過濾器初始化代碼

經過value做爲音量調節參數,具體代碼以下

int init_volume_filter(AVFilterGraph **pGraph, AVFilterContext **src, AVFilterContext **out, char *value) {

    //初始化AVFilterGraph
    AVFilterGraph *graph = avfilter_graph_alloc();
    //獲取abuffer用於接收輸入端
    AVFilter *abuffer = avfilter_get_by_name("abuffer");
    AVFilterContext *abuffer_ctx = avfilter_graph_alloc_filter(graph, abuffer, "src");
    //設置參數,這裏須要匹配原始音頻採樣率、數據格式(位數)
    if (avfilter_init_str(abuffer_ctx, "sample_rate=48000:sample_fmt=s16p:channel_layout=stereo") <
        0) {
        LOGE("error init abuffer filter");
        return -1;
    }
    //初始化volume filter
    AVFilter *volume = avfilter_get_by_name("volume");
    AVFilterContext *volume_ctx = avfilter_graph_alloc_filter(graph, volume, "volume");
    //這裏採用av_dict_set設置參數
    AVDictionary *args = NULL;
    av_dict_set(&args, "volume", value, 0);//這裏傳入外部參數,能夠動態修改
    if (avfilter_init_dict(volume_ctx, &args) < 0) {
        LOGE("error init volume filter");
        return -1;
    }

    AVFilter *aformat = avfilter_get_by_name("aformat");
    AVFilterContext *aformat_ctx = avfilter_graph_alloc_filter(graph, aformat, "aformat");
    if (avfilter_init_str(aformat_ctx,
                          "sample_rates=48000:sample_fmts=s16p:channel_layouts=stereo") < 0) {
        LOGE("error init aformat filter");
        return -1;
    }
    //初始化sink用於輸出
    AVFilter *sink = avfilter_get_by_name("abuffersink");
    AVFilterContext *sink_ctx = avfilter_graph_alloc_filter(graph, sink, "sink");
    if (avfilter_init_str(sink_ctx, NULL) < 0) {//無需參數
        LOGE("error init sink filter");
        return -1;
    }
    //連接各個filter上下文
    if (avfilter_link(abuffer_ctx, 0, volume_ctx, 0) != 0) {
        LOGE("error link to volume filter");
        return -1;
    }
    if (avfilter_link(volume_ctx, 0, aformat_ctx, 0) != 0) {
        LOGE("error link to aformat filter");
        return -1;
    }
    if (avfilter_link(aformat_ctx, 0, sink_ctx, 0) != 0) {
        LOGE("error link to sink filter");
        return -1;
    }
    if (avfilter_graph_config(graph, NULL) < 0) {
        LOGI("error config filter graph");
        return -1;
    }
    *pGraph = graph;
    *src = abuffer_ctx;
    *out = sink_ctx;
    LOGI("init filter success...");
    return 0;
}
複製代碼

5.4 使用過濾器,模擬實時音量調節

完成過濾器初始化後,就能夠在解碼後使用過濾器處理音頻了。使用方法很簡單,將解碼後的AVFrame經過av_buffersrc_add_frame(abuffer_ctx,frame)加入到輸入過濾器上下文abuffer_ctx中,經過av_buffersink_get_frame(sink_ctx,frame)獲取處理完成的frame。這裏每一個1000個音頻幀修改一次過濾器,模擬實時音量調節。 代碼以下

AVFilterGraph *graph;
    AVFilterContext *in_ctx;
    AVFilterContext *out_ctx;
    //註冊全部過濾器
    avfilter_register_all();
    init_volume_filter(&graph, &in_ctx, &out_ctx, "0.5");
    //初始化
    while (av_read_frame(fmt_ctx, packet) == 0) {//將音頻數據讀入packet
        if (packet->stream_index == audio_stream_index) {//取音頻索引packet
           ... 解碼音頻
            if (got_frame > 0) {
                LOGI("decode frame:%d", index++);
               if (index == 1000) {//模擬動態修改音量
                    init_volume_filter(&graph, &in_ctx, &out_ctx, "0.01");
                }
                if (index == 2000) {
                    init_volume_filter(&graph, &in_ctx, &out_ctx, "1.0");
                }
                if (index == 3000) {
                    init_volume_filter(&graph, &in_ctx, &out_ctx, "0.01");
                }
                if (index == 4000) {
                    init_volume_filter(&graph, &in_ctx, &out_ctx, "1.0");
                }
                if (av_buffersrc_add_frame(in_ctx, frame) < 0) {//將frame放入輸入filter上下文
                    LOGE("error add frame");
                    break;
                }
                while (av_buffersink_get_frame(out_ctx, frame) >= 0) {//從輸出filter上下文中獲取frame
                    fwrite(frame->data[0], 1, static_cast<size_t>(frame->linesize[0]),
                           out_file); //想將單個聲道pcm數據寫入文件
                }
            }
        }
    }
複製代碼

最終解碼出來pcm和原始mp3波形對比

修改音量前

修改音量後
能夠明顯看出音量已經發生變化。

5.5 使用swr_convert從新採樣

在播放音頻時,能夠聽見有一些噪聲,須要swr_convert來從新採樣,取出完整的pcm數據。

//初始化SwrContext
    SwrContext *swr_ctx = swr_alloc();
    enum AVSampleFormat in_sample = codec_ctx->sample_fmt;
    enum AVSampleFormat out_sample = AV_SAMPLE_FMT_S16;
    int inSampleRate = codec_ctx->sample_rate;
    int outSampleRate = inSampleRate;
    uint64_t in_ch_layout = codec_ctx->channel_layout;
    uint64_t outChannelLayout = AV_CH_LAYOUT_STEREO;
    swr_alloc_set_opts(swr_ctx, outChannelLayout, out_sample, outSampleRate, in_ch_layout, in_sample,
                       inSampleRate, 0, NULL);
    swr_init(swr_ctx);
    int out_ch_layout_nb = av_get_channel_layout_nb_channels(out_ch_layout);//聲道個數
    uint8_t *out_buffer = (uint8_t *) av_malloc(MAX_AUDIO_SIZE);//重採樣數據
複製代碼

寫入pcm數據以前,用swr_convert從新採樣一下

while (av_buffersink_get_frame(out_ctx, frame) >= 0) {//從輸出filter上下文中獲取frame
// fwrite(frame->data[0], 1, static_cast<size_t>(frame->linesize[0]),
// out_file); //想將單個聲道pcm數據寫入文件
  swr_convert(swr_ctx, &out_buffer, MAX_AUDIO_SIZE,
                                (const uint8_t **) frame->data, frame->nb_samples);
  int out_size = av_samples_get_buffer_size(NULL,out_ch_layout_nb,frame->nb_samples,out_sample_fmt,0);
                    fwrite(out_buffer,1,out_size,out_file);
}
複製代碼

此次咱們寫入的是完整2個聲道的數據,並且也沒有噪聲了。

5.6 使用atempo過濾器實現變速不變調

將volumefilter改爲atempo,注意參數設置名爲tempo

//初始化volume filter
    AVFilter *volume = avfilter_get_by_name("atempo");
    AVFilterContext *volume_ctx = avfilter_graph_alloc_filter(graph, volume, "atempo");
    //這裏採用av_dict_set設置參數
    AVDictionary *args = NULL;
    av_dict_set(&args, "tempo", value, 0);//調節音量爲原先的一半
    if (avfilter_init_dict(volume_ctx, &args) < 0) {
        LOGE("error init volume filter");
        return -1;
    }
複製代碼

解碼時模擬動態修改速度改爲以下

if (index == 1000) {//模擬動態修改音量
    init_volume_filter(&graph, &in_ctx, &out_ctx, "1.0");
}
if (index == 2000) {
    init_volume_filter(&graph, &in_ctx, &out_ctx, "0.8");
}
if (index == 3000) {
    init_volume_filter(&graph, &in_ctx, &out_ctx, "1.5");
}
if (index == 4000) {
    init_volume_filter(&graph, &in_ctx, &out_ctx, "2.0");
}
複製代碼

成功後,就能夠一個不一樣速度的音頻,使用Audition打開,選擇48000,2通道播放,能夠聽出它先是按照0.5,1.0,0.8,1.5,2.0的播放的,並且音調保持不變,沒有由於速度的改變而變高或者變低。

6. 多輸入AVFilter過濾器

FFmpeg使用過濾器filter的另一個場景就是處理多個輸入數據,好比視頻添加水印,添加字幕,音視頻合併等。這類場景須要兩個及以上輸入端。本節講amix,它能夠將多個音頻混音。

6.1 多輸入filter處理流程

輸入AVFrame1 -> abuffer  
                         -> amix -> aformat -> abuffersink -> 輸出AVFrame
輸入AVFrame2 -> abuffer  
複製代碼

處理流程和單輸入過濾器大體相同,只不過接收了多個輸入端。所以須要多個filter上下文做爲輸入端。

6.2 amix過濾器初始化

//初始化amix filter
int init_amix_filter(AVFilterGraph **pGraph, AVFilterContext **srcs, AVFilterContext **pOut,
                     jsize len) {
    AVFilterGraph *graph = avfilter_graph_alloc();
    for (int i = 0; i < len; i++) {
        AVFilter *filter = avfilter_get_by_name("abuffer");
        char name[50];
        snprintf(name, sizeof(name), "src%d", i);
        AVFilterContext *abuffer_ctx = avfilter_graph_alloc_filter(graph, filter, name);
        if (avfilter_init_str(abuffer_ctx,
                              "sample_rate=48000:sample_fmt=s16p:channel_layout=stereo") < 0) {
            LOGE("error init abuffer filter");
            return -1;
        }
        srcs[i] = abuffer_ctx;
    }
    AVFilter *amix = avfilter_get_by_name("amix");
    AVFilterContext *amix_ctx = avfilter_graph_alloc_filter(graph, amix, "amix");
    char args[128];
    snprintf(args, sizeof(args), "inputs=%d:duration=first:dropout_transition=3", len);
    if (avfilter_init_str(amix_ctx, args) < 0) {
        LOGE("error init amix filter");
        return -1;
    }
    AVFilter *aformat = avfilter_get_by_name("aformat");
    AVFilterContext *aformat_ctx = avfilter_graph_alloc_filter(graph, aformat, "aformat");
    if (avfilter_init_str(aformat_ctx,
                          "sample_rates=48000:sample_fmts=s16p:channel_layouts=stereo") < 0) {
        LOGE("error init aformat filter");
        return -1;
    }
    AVFilter *sink = avfilter_get_by_name("abuffersink");
    AVFilterContext *sink_ctx = avfilter_graph_alloc_filter(graph, sink, "sink");
    avfilter_init_str(sink_ctx, NULL);
    for (int i = 0; i < len; i++) {
        if (avfilter_link(srcs[i], 0, amix_ctx, i) < 0) {
            LOGE("error link to amix");
            return -1;
        }
    }
    if (avfilter_link(amix_ctx, 0, aformat_ctx, 0) < 0) {
        LOGE("error link to aformat");
        return -1;
    }
    if (avfilter_link(aformat_ctx, 0, sink_ctx, 0) < 0) {
        LOGE("error link to sink");
        return -1;
    }
    if (avfilter_graph_config(graph, NULL) < 0) {
        LOGE("error config graph");
        return -1;
    }
    *pGraph = graph;
    *pOut = sink_ctx;
    return 0;
}
複製代碼

這裏用一個數組保存輸入AVFilterContex,經過遍歷循環將每一個輸入端連接到amix過濾器,這樣就能夠接收多個輸入端了。

6.3 使用amix實現多音軌合成

爲了可以傳入多個音頻數據,咱們須要同時解碼多個音頻文件,所以在Java層,傳入字符串數組。

external fun mixAudio(arr: Array<String>,out:String)
複製代碼
val path = "${Environment.getExternalStorageDirectory()}/test"
val paths = arrayOf(
                    "$path/a.mp3",
                    "$path/b.mp3",
                    "$path/c.mp3",
                    "$path/d.mp3"
)
mixAudio(paths,"$path/mix.pcm")
複製代碼

在jni層使用多個解碼器解碼每一個文件

extern "C" JNIEXPORT void JNICALL Java_io_github_iamyours_ffmpegaudioplayer_MainActivity_mixAudio( JNIEnv *env, jobject /* this */, jobjectArray _srcs, jstring _out) {
    //將java傳入的字符串數組轉爲c字符串數組
    jsize len = env->GetArrayLength(_srcs);
    const char *out_path = env->GetStringUTFChars(_out, 0);
    char **pathArr = (char **) malloc(len * sizeof(char *));
    int i = 0;
    for (i = 0; i < len; i++) {
        jstring str = static_cast<jstring>(env->GetObjectArrayElement(_srcs, i));
        pathArr[i] = const_cast<char *>(env->GetStringUTFChars(str, 0));
    }
    //初始化解碼器數組
    av_register_all();
    AVFormatContext **fmt_ctx_arr = (AVFormatContext **) malloc(len * sizeof(AVFormatContext *));
    AVCodecContext **codec_ctx_arr = (AVCodecContext **) malloc(len * sizeof(AVCodecContext *));
    int stream_index_arr[len];
    for (int n = 0; n < len; n++) {
        AVFormatContext *fmt_ctx = avformat_alloc_context();
        fmt_ctx_arr[n] = fmt_ctx;
        ...
        //依次打開每一個文件,獲取音頻索引,獲取每一個解碼器
        ...
        AVCodecContext *codec_ctx = avcodec_alloc_context3(NULL);
        codec_ctx_arr[n] = codec_ctx;
        ...
    }
    //初始化SwrContext
    SwrContext *swr_ctx = swr_alloc();
    ...
    //設置swr_ctx參數
    ...
    swr_init(swr_ctx);
    //初始化amix過濾器
    ...
    init_amix_filter(&graph, srcs, &sink, len);
    //開始解碼
    FILE *out_file = fopen(out_path, "wb");
    AVFrame *frame = av_frame_alloc();
    AVPacket *packet = av_packet_alloc();
    int ret = 0, got_frame;
    int index = 0;
    while (1) {
        for (int i = 0; i < len; i++) {
            ret = av_read_frame(fmt_ctx_arr[i], packet);
            if (ret < 0)break;
            if (packet->stream_index == stream_index_arr[i]) {
                ret = avcodec_decode_audio4(codec_ctx_arr[i], frame, &got_frame, packet);//解碼音頻
                if (ret < 0)break;
                if (got_frame > 0) {
                    ret = av_buffersrc_add_frame(srcs[i], frame);//將解碼後的AVFrame加入到amix輸入端
                    if (ret < 0) {
                        break;
                    }
                }
            }
        }
        while (av_buffersink_get_frame(sink, frame) >= 0) {//從sink輸出端獲取處理完成的AVFrame
            swr_convert(swr_ctx, &out_buffer, MAX_AUDIO_SIZE, (const uint8_t **) frame->data,
                        frame->nb_samples);
            int out_size = av_samples_get_buffer_size(NULL, out_ch_layout_nb, frame->nb_samples,
                                                      out_sample_fmt, 0);
            fwrite(out_buffer, 1, out_size, out_file);
        }
        if (ret < 0) {
            break;
        }
        LOGI("decode frame :%d", index);
        index++;
    }
    LOGI("finish");
}
複製代碼

使用audition打開輸出文件mix.pcm,能夠聽到四個文件混音後的音頻。 具體的音頻在assets目錄下,能夠自行對比下效果

7. 使用OpenSLES播放音頻

爲了可以在Android播放pcm格式的音頻,咱們使用OpenSLES庫。在cmke的target_link_libraries中加入OpenSLES,使用時加入頭文件<SLES/OpenSLES_Android.h>

7.1 OpenSLES播放器流程

7.1.1.建立而且實現引擎對象
SLObjectItf engineObject;
slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
 (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
複製代碼
7.1.2.獲取引擎接口
SLEngineItf engineItf;
 (*enginObject)->GetInterface(engineObject,SL_IID_ENGINE,&engineItf);
複製代碼
7.1.3.建立而且實現輸出混音器對象
SLObjectItf mixObject;
(*engineItf)->CreateOutputMix(engineItf, &mixObject, 0, 0, 0);
複製代碼
7.1.4.設置播放器參數,建立初始化播放器對象
SLDataLocator_AndroidSimpleBufferQueue
        android_queue = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
//pcm格式
SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM,
                        2,//兩聲道
                        SL_SAMPLINGRATE_48,//48000採樣率
                        SL_PCMSAMPLEFORMAT_FIXED_16,
                        SL_PCMSAMPLEFORMAT_FIXED_16,
                        SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//
                        SL_BYTEORDER_LITTLEENDIAN};

SLDataSource slDataSource = {&android_queue, &pcm};

//輸出管道
SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, mixObject};
SLDataSink audioSnk = {&outputMix, NULL};

const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};
const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};

SLObjectItf playerObject;//播放器對象
(*engineItf)->CreateAudioPlayer(engineItf, &playerObject,&slDataSource,&audioSnk,1,ids,req);
(*playerObject)->Realize(playerObject,SL_BOOLEAN_FALSE);
複製代碼
7.1.5.經過播放器對象獲取相關接口
//獲取播放接口
SLPlayItf playItf;
(*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playItf);
//獲取緩衝接口
SLBufferQueueItf bufferQueueItf;
(*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &bufferQueueItf);
複製代碼
7.1.6. 註冊回調緩衝,設置播放狀態,調用回調函數
//註冊緩衝回調
(*bufferQueueItf)->RegisterCallback(bufferQueueItf, playCallback, NULL);
//設置播放狀態
(*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PLAYING);
playCallback(bufferQueueItf, NULL);
複製代碼

具體回調以下,getPCM在後面會有實現

void playCallback(SLAndroidSimpleBufferQueueItf bq, void *args) {
    //獲取pcm數據
    uint8_t *data;
    int size = getPCM(&data);
    if (size > 0) {
        (*bq)->Enqueue(bq, data, size);
    }
}
複製代碼

7.2 多線程解碼播放音頻

爲了可以獲取pcm數據,咱們使用多線程進行音頻解碼,經過條件變量,實現一個生產者消費者的模型,解碼過程爲生產過程,回調播放爲消費過程。將解碼獲得的AVFrame加入到vector隊列中,而後在播放回調時取出AVFrame,使用swr_convert轉成pcm數據。

7.2.1.初始化同步鎖,條件變量,啓動解碼線程

申明全局的變量

static pthread_mutex_t mutex;
//條件變量
static pthread_cond_t notfull; //隊列未達到最大緩衝容量,存
static pthread_cond_t notempty;//隊列不爲空,取
複製代碼

初始化同步鎖和條件變量,啓動解碼線程(放在建立播放器前面)

//初始化同步鎖和條件變量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&notfull, NULL);
pthread_cond_init(&notempty, NULL);

//初始化解碼線程
pthread_t pid;
char *path = (char *) env->GetStringUTFChars(_path, 0);
pthread_create(&pid, NULL, decodeAudio, path);
複製代碼
7.2.2.解碼音頻,將AVFrame加入vector隊列

申明全局變量

static std::vector<AVFrame *> queue;
static SwrContext *swr_ctx;
static int out_ch_layout_nb;
static enum AVSampleFormat out_sample_fmt;
#define QUEUE_SIZE 5
#define MAX_AUDIO_SIZE 48000*4
複製代碼

解碼音頻和[第4節]相似,只不過把解碼出來的AVFrame加入到隊列了。

void *decodeAudio(void *args) {
    //打開文件,獲取初始化上下文,解碼器,分配packet/frame內存
    ...
    while (av_read_frame(fmt_ctx, packet) == 0) {//將音頻數據讀入packet
        if (packet->stream_index == audio_stream_index) {//取音頻索引packet
            if (avcodec_decode_audio4(codec_ctx, frame, &got_frame, packet) <
                0) {//將packet解碼成AVFrame
                LOGE("decode error:%d", index);
                break;
            }
            if (got_frame > 0) {
                LOGI("decode frame:%d", index++);
                addFrame(frame);
            }
        }
    }
    //釋放資源
    ...
}
複製代碼

爲了保證音頻播放的實時性,隊列裏的AVFrame數量不能太多。在後面章節中,咱們會將AVFrame經過Filter過濾,而後才加入到隊列中。所以在addFrame方法中,若是超出最大緩衝大小,須要經過pthread_cond_wait阻塞住,等待消費,代碼以下:

void addFrame(AVFrame *src) {
    AVFrame *frame = av_frame_alloc();
    if (av_frame_ref(frame, src) >= 0) {//複製frame
        pthread_mutex_lock(&mutex);
        if (queue.size() == QUEUE_SIZE) {
            LOGI("wait for add frame...%d", queue.size());
            pthread_cond_wait(&notfull, &mutex);//等待隊列不爲滿信號
        }
        queue.push_back(frame);
        pthread_cond_signal(&notempty);//發送不爲空信號
        pthread_mutex_unlock(&mutex);
    }
}
複製代碼
7.2.3.獲取pcm數據,經過opensles回調函數播放pcm

經過以前註冊的緩衝回調,咱們能夠把加入到隊列的AVFrame消費掉。 首先要有一個getFrame方法

AVFrame *getFrame() {
    pthread_mutex_lock(&mutex);
    while (true) {
        if (!queue.empty()) {
            AVFrame *out = av_frame_alloc();
            AVFrame *src = queue.front();
            if (av_frame_ref(out, src) < 0)break;
            queue.erase(queue.begin());//刪除元素
            av_free(src);
            if (queue.size() < QUEUE_SIZE)pthread_cond_signal(&notfull);//發送notfull信號
            pthread_mutex_unlock(&mutex);
            return out;
        } else {//爲空等待添加
            LOGI("wait for get frame");
            pthread_cond_wait(&notempty, &mutex);
        }
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}
複製代碼

而後是實現最初的getPCM方法,以下:

int getPCM(uint8_t **out) {
    AVFrame *frame = getFrame();
    if (frame) {
        uint8_t *data = (uint8_t *) av_malloc(MAX_AUDIO_SIZE);
        swr_convert(swr_ctx, &data, MAX_AUDIO_SIZE, (const uint8_t **) frame->data,
                    frame->nb_samples);
        int out_size = av_samples_get_buffer_size(NULL, out_ch_layout_nb, frame->nb_samples,
                                                  out_sample_fmt, 0);
        *out = data;
        return out_size;
    }
    return 0;
}
複製代碼

這裏,經過swr_convert將AVFrame數據轉化成uint8_t數組,而後就能夠緩衝隊列接口裏的Enqueue播放了。

8. FFmpeg播放器實現

有了前面一系列的準備知識,能夠開始打造FFmpeg音頻播放器了。主要需求,多個音頻混音播放,每一個音軌音量控制,合成音頻變速播放。

8.1 AudioPlayer類

首先咱們建立一個C++ Class名爲AudioPlayer,爲了可以實現音頻解碼,過濾,播放功能,咱們須要解碼、過濾、隊列、輸出pcm相關、多線程、Open SL ES相關的成員變量,代碼以下:

//解碼
int fileCount;                  //輸入音頻文件數量
AVFormatContext **fmt_ctx_arr;  //FFmpeg上下文數組
AVCodecContext **codec_ctx_arr; //解碼器上下文數組
int *stream_index_arr;          //音頻流索引數組
//過濾
AVFilterGraph *graph;
AVFilterContext **srcs;         //輸入filter
AVFilterContext *sink;          //輸出filter
char **volumes;                 //各個音頻的音量
char *tempo;                    //播放速度0.5~2.0

//AVFrame隊列
std::vector<AVFrame *> queue;   //隊列,用於保存解碼過濾後的AVFrame

//輸入輸出格式
SwrContext *swr_ctx;            //重採樣,用於將AVFrame轉成pcm數據
uint64_t in_ch_layout;
int in_sample_rate;            //採樣率
int in_ch_layout_nb;           //輸入聲道數,配合swr_ctx使用
enum AVSampleFormat in_sample_fmt; //輸入音頻採樣格式

uint64_t out_ch_layout;
int out_sample_rate;            //採樣率
int out_ch_layout_nb;           //輸出聲道數,配合swr_ctx使用
int max_audio_frame_size;       //最大緩衝數據大小
enum AVSampleFormat out_sample_fmt; //輸出音頻採樣格式

// 進度相關
AVRational time_base;           //刻度,用於計算進度
double total_time;              //總時長(秒)
double current_time;            //當前進度
int isPlay = 0;                 //播放狀態1:播放中

//多線程
pthread_t decodeId;             //解碼線程id
pthread_t playId;               //播放線程id
pthread_mutex_t mutex;          //同步鎖
pthread_cond_t not_full;        //不爲滿條件,生產AVFrame時使用
pthread_cond_t not_empty;       //不爲空條件,消費AVFrame時使用

//Open SL ES
SLObjectItf engineObject;       //引擎對象
SLEngineItf engineItf;          //引擎接口
SLObjectItf mixObject;          //輸出混音對象
SLObjectItf playerObject;       //播放器對象
SLPlayItf playItf;              //播放器接口
SLAndroidSimpleBufferQueueItf bufferQueueItf;   //緩衝接口
複製代碼

8.2 播放器解碼播放流程

解碼播放流程
整個音頻處理播放流程如上圖,首先,咱們須要兩個線程一個是解碼線程,一個是播放線程。解碼線程負責多個音頻文件的解碼,過濾,加入隊列操做,播放線程則須要從隊列中取出處理後的AVFrame,而後轉成pcm輸入,經過緩衝回調播放音頻。 爲了初始化這些成員變量,咱們按照每塊成員列表定義了對於的初始化方法。

int createPlayer();                     //建立播放器
int initCodecs(char **pathArr);         //初始化解碼器
int initSwrContext();                   //初始化SwrContext
int initFilters();                      //初始化過濾器
複製代碼

而在構造函數中傳入音頻文件數組,和文件數量,初始化相關方法

AudioPlayer::AudioPlayer(char **pathArr, int len) {
    //初始化
    fileCount = len;
    //默認音量1.0 速度1.0
    volumes = (char **) malloc(fileCount * sizeof(char *));
    for (int i = 0; i < fileCount; i++) {
        volumes[i] = "1.0";
    }
    tempo = "1.0";

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&not_full, NULL);
    pthread_cond_init(&not_empty, NULL);

    initCodecs(pathArr);
    avfilter_register_all();
    initSwrContext();
    initFilters();
    createPlayer();
}
複製代碼

這裏咱們還初始化了控制各個音頻音量和速度的變量,同步鎖和條件變量(生產消費控制)。

8.3 具體實現

8.3.1 初始化解碼器數組
int AudioPlayer::initCodecs(char **pathArr) {
    LOGI("init codecs");
    av_register_all();
    fmt_ctx_arr = (AVFormatContext **) malloc(fileCount * sizeof(AVFormatContext *));
    codec_ctx_arr = (AVCodecContext **) malloc(fileCount * sizeof(AVCodecContext *));
    stream_index_arr = (int *) malloc(fileCount * sizeof(int));
    for (int n = 0; n < fileCount; n++) {
    	//初始化上下文,打開文件,獲取音頻索引
        ...

        stream_index_arr[n] = audio_stream_index;
        //獲取解碼器
        AVCodecContext *codec_ctx = avcodec_alloc_context3(NULL);
        codec_ctx_arr[n] = codec_ctx;
        AVStream *stream = fmt_ctx->streams[audio_stream_index];
        avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[audio_stream_index]->codecpar);
        AVCodec *codec = avcodec_find_decoder(codec_ctx->codec_id);
        if (n == 0) {//獲取輸入格式
            in_sample_fmt = codec_ctx->sample_fmt;
            in_ch_layout = codec_ctx->channel_layout;
            in_sample_rate = codec_ctx->sample_rate;
            in_ch_layout_nb = av_get_channel_layout_nb_channels(in_ch_layout);
            max_audio_frame_size = in_sample_rate * in_ch_layout_nb;
            time_base = fmt_ctx->streams[audio_stream_index]->time_base;
            int64_t duration = stream->duration;
            total_time = av_q2d(stream->time_base) * duration;
            LOGI("total time:%lf", total_time);
        } else {//若是是多個文件,判斷格式是否一致(採用率,格式、聲道數)
            if (in_ch_layout != codec_ctx->channel_layout
                || in_sample_fmt != codec_ctx->sample_fmt
                || in_sample_rate != codec_ctx->sample_rate) {
                LOGE("輸入文件格式不一樣");
                return -1;
            }
        }
        //打開解碼器
        if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
            LOGE("could not open codec");
            return -1;
        }
    }
    return 1;
}
複製代碼

這裏將輸入音頻的格式信息保存起來,用於SwrContext初始化、Filter初始化。

8.3.2 初始化filter數組
int AudioPlayer::initFilters() {
    LOGI("init filters");
    graph = avfilter_graph_alloc();
    srcs = (AVFilterContext **) malloc(fileCount * sizeof(AVFilterContext **));
    char args[128];
    AVDictionary *dic = NULL;
    //混音過濾器
    AVFilter *amix = avfilter_get_by_name("amix");
    AVFilterContext *amix_ctx = avfilter_graph_alloc_filter(graph, amix, "amix");
    snprintf(args, sizeof(args), "inputs=%d:duration=first:dropout_transition=3", fileCount);
    if (avfilter_init_str(amix_ctx, args) < 0) {
        LOGE("error init amix filter");
        return -1;
    }

    const char *sample_fmt = av_get_sample_fmt_name(in_sample_fmt);
    snprintf(args, sizeof(args), "sample_rate=%d:sample_fmt=%s:channel_layout=0x%" PRIx64,
             in_sample_rate, sample_fmt, in_ch_layout);

    for (int i = 0; i < fileCount; i++) {
    	//這裏初始化每一個輸入端對應的abuffer,volume過濾器
    	...
    	//接着鏈接到amix
        if (avfilter_link(volume_ctx, 0, amix_ctx, i) < 0) {
            LOGE("error link to amix filter");
            return -1;
        }
    }

    //變速過濾器atempo
    AVFilter *atempo = avfilter_get_by_name("atempo");
    //設置變速參數
    ...
    //初始化aformat過濾器用於輸出端格式轉換

    AVFilter *aformat = avfilter_get_by_name("aformat");
    AVFilterContext *aformat_ctx = avfilter_graph_alloc_filter(graph, aformat, "aformat");
    snprintf(args, sizeof(args), "sample_rates=%d:sample_fmts=%s:channel_layouts=0x%" PRIx64,
             in_sample_rate, sample_fmt, in_ch_layout);
    if (avfilter_init_str(aformat_ctx, args) < 0) {
        LOGE("error init aformat filter");
        return -1;
    }
    //輸出緩衝
    AVFilter *abuffersink = avfilter_get_by_name("abuffersink");
    //設置abuffersink參數
    ...
    //將amix連接到atempo
    if (avfilter_link(amix_ctx, 0, atempo_ctx, 0) < 0) {
        LOGE("error link to atempo filter");
        return -1;
    }
    if (avfilter_link(atempo_ctx, 0, aformat_ctx, 0) < 0) {
        LOGE("error link to aformat filter");
        return -1;
    }
    if (avfilter_link(aformat_ctx, 0, sink, 0) < 0) {
        LOGE("error link to abuffersink filter");
        return -1;
    }
    if (avfilter_graph_config(graph, NULL) < 0) {
        LOGE("error config graph");
        return -1;
    }

    return 1;
}
複製代碼

經過初始化解碼器獲取的輸入音頻格式信息,能夠初始化abuffer輸入filter(採樣率、格式、聲道必須匹配),而後能夠連接volume ,amix,atempo filter。這樣音頻就能夠實現調音,混音,變速的效果。

8.3.3 初始化SwrContext
int AudioPlayer::initSwrContext() {
    LOGI("init swr context");
    swr_ctx = swr_alloc();
    out_sample_fmt = AV_SAMPLE_FMT_S16;
    out_ch_layout = AV_CH_LAYOUT_STEREO;
    out_ch_layout_nb = 2;
    out_sample_rate = in_sample_rate;
    max_audio_frame_size = out_sample_rate * 2;

    swr_alloc_set_opts(swr_ctx, out_ch_layout, out_sample_fmt, out_sample_rate, in_ch_layout,
                       in_sample_fmt, in_sample_rate, 0, NULL);
    if (swr_init(swr_ctx) < 0) {
        LOGE("error init SwrContext");
        return -1;
    }
    return 1;
}
複製代碼

爲了能使得解碼出來的AVFrame能在OpenSL ES下播放,咱們將採用格式固定爲16位的AV_SAMPLE_FMT_S16,聲道爲立體聲AV_CH_LAYOUT_STEREO,聲道數爲2,採樣率和輸入同樣。緩衝回調pcm數據最大值爲採樣率*2。

8.3.4 初始化OpenSL ES播放器
int AudioPlayer::createPlayer() {
    //建立播放器
    //建立而且初始化引擎對象
// SLObjectItf engineObject;
    slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
    (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    //獲取引擎接口
// SLEngineItf engineItf;
    (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineItf);
    //經過引擎接口獲取輸出混音
// SLObjectItf mixObject;
    (*engineItf)->CreateOutputMix(engineItf, &mixObject, 0, 0, 0);
    (*mixObject)->Realize(mixObject, SL_BOOLEAN_FALSE);

    //設置播放器參數
    SLDataLocator_AndroidSimpleBufferQueue
            android_queue = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
    SLuint32 samplesPerSec = (SLuint32) out_sample_rate * 1000;
    //pcm格式
    SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM,
                            2,//兩聲道
                            samplesPerSec,
                            SL_PCMSAMPLEFORMAT_FIXED_16,
                            SL_PCMSAMPLEFORMAT_FIXED_16,
                            SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//
                            SL_BYTEORDER_LITTLEENDIAN};

    SLDataSource slDataSource = {&android_queue, &pcm};

    //輸出管道
    SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, mixObject};
    SLDataSink audioSnk = {&outputMix, NULL};

    const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};
    const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
    //經過引擎接口,建立而且初始化播放器對象
// SLObjectItf playerObject;
    (*engineItf)->CreateAudioPlayer(engineItf, &playerObject, &slDataSource, &audioSnk, 1, ids,
                                    req);
    (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);

    //獲取播放接口
// SLPlayItf playItf;
    (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playItf);
    //獲取緩衝接口
// SLAndroidSimpleBufferQueueItf bufferQueueItf;
    (*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &bufferQueueItf);

    //註冊緩衝回調
    (*bufferQueueItf)->RegisterCallback(bufferQueueItf, _playCallback, this);
    return 1;
}
複製代碼

這裏的pcm格式和SwrContext設置的參數要一致

8.3.5 啓動播放線程和解碼線程
void *_decodeAudio(void *args) {
    AudioPlayer *p = (AudioPlayer *) args;
    p->decodeAudio();
    pthread_exit(0);
}

void *_play(void *args) {
    AudioPlayer *p = (AudioPlayer *) args;
    p->setPlaying();
    pthread_exit(0);
}

void AudioPlayer::setPlaying() {
    //設置播放狀態
    (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PLAYING);
    _playCallback(bufferQueueItf, this);
}

void AudioPlayer::play() {
    isPlay = 1;
    pthread_create(&decodeId, NULL, _decodeAudio, this);
    pthread_create(&playId, NULL, _play, this);
}
複製代碼

play方法中咱們pthread_create啓動播放和解碼線程,播放線程經過播放接口設置播放中狀態,而後回調緩衝接口,在回調中,取出隊列中的AVFrame轉成pcm,而後經過Enqueue播放。解碼線程負責解碼過濾出AVFrame,加入到隊列中。

8.3.6 緩衝回調
void _playCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {
    AudioPlayer *player = (AudioPlayer *) context;
    AVFrame *frame = player->get();
    if (frame) {
        int size = av_samples_get_buffer_size(NULL, player->out_ch_layout_nb, frame->nb_samples,
                                              player->out_sample_fmt, 1);
        if (size > 0) {
            uint8_t *outBuffer = (uint8_t *) av_malloc(player->max_audio_frame_size);
            swr_convert(player->swr_ctx, &outBuffer, player->max_audio_frame_size,
                        (const uint8_t **) frame->data, frame->nb_samples);
            (*bq)->Enqueue(bq, outBuffer, size);
        }
    }
}
複製代碼
8.3.7 解碼過濾
void AudioPlayer::decodeAudio() {
    LOGI("start decode...");
    AVFrame *frame = av_frame_alloc();
    AVPacket *packet = av_packet_alloc();
    int ret, got_frame;
    int index = 0;
    while (isPlay) {
        LOGI("decode frame:%d", index);
        for (int i = 0; i < fileCount; i++) {
            AVFormatContext *fmt_ctx = fmt_ctx_arr[i];
            ret = av_read_frame(fmt_ctx, packet);
            if (packet->stream_index != stream_index_arr[i])continue;//不是音頻packet跳過
            if (ret < 0) {
                LOGE("decode finish");
                goto end;
            }
            ret = avcodec_decode_audio4(codec_ctx_arr[i], frame, &got_frame, packet);
            if (ret < 0) {
                LOGE("error decode packet");
                goto end;
            }
            if (got_frame <= 0) {
                LOGE("decode error or finish");
                goto end;
            }
            ret = av_buffersrc_add_frame(srcs[i], frame);
            if (ret < 0) {
                LOGE("error add frame to filter");
                goto end;
            }
        }
        LOGI("time:%lld,%lld,%lld", frame->pkt_dts, frame->pts, packet->pts);
        while (av_buffersink_get_frame(sink, frame) >= 0) {
            frame->pts = packet->pts;
            LOGI("put frame:%d,%lld", index, frame->pts);
            put(frame);
        }
        index++;
    }
    end:
    av_packet_unref(packet);
    av_frame_unref(frame);
}
複製代碼

這裏有一個注意的點是,經過av_read_frame讀取的packet不必定是音頻流,因此須要經過音頻流索引過濾packet。在av_buffersink_get_frame獲取的AVFrame中,將pts修改成packet裏的pts,用於保存進度(過濾後的pts時間進度不是當前解碼的進度)。

8.3.8 AVFrame存和取
/** * 將AVFrame加入到隊列,隊列長度爲5時,阻塞等待 * @param frame * @return */
int AudioPlayer::put(AVFrame *frame) {
    AVFrame *out = av_frame_alloc();
    if (av_frame_ref(out, frame) < 0)return -1;//複製AVFrame
    pthread_mutex_lock(&mutex);
    if (queue.size() == 5) {
        LOGI("queue is full,wait for put frame:%d", queue.size());
        pthread_cond_wait(&not_full, &mutex);
    }
    queue.push_back(out);
    pthread_cond_signal(&not_empty);
    pthread_mutex_unlock(&mutex);
    return 1;
}

/** * 取出AVFrame,隊列爲空時,阻塞等待 * @return */
AVFrame *AudioPlayer::get() {
    AVFrame *out = av_frame_alloc();
    pthread_mutex_lock(&mutex);
    while (isPlay) {
        if (queue.empty()) {
            pthread_cond_wait(&not_empty, &mutex);
        } else {
            AVFrame *src = queue.front();
            if (av_frame_ref(out, src) < 0)return NULL;
            queue.erase(queue.begin());//刪除取出的元素
            av_free(src);
            if (queue.size() < 5)pthread_cond_signal(&not_full);
            pthread_mutex_unlock(&mutex);
            current_time = av_q2d(time_base) * out->pts;
            LOGI("get frame:%d,time:%lf", queue.size(), current_time);
            return out;
        }
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}
複製代碼

經過兩個條件變量,實現一個緩衝過小爲5的生產消費模型,用於AVFrame隊列的存和取。 經過以上代碼就能夠實現音量爲1,速度爲1的多音頻播放了

9. NDK播放控制

前面一節咱們已經建立了一個基於FFmpeg的播放器,這一節開始對播放器進行各類控制操做。主要有調音,變速,暫停,播放,進度切換,中止(釋放資源)。

9.1 建立FFmpegAudioPlayer

首先在java層建立FFmpegAudioPlayer.kt(kotlin),加入如下方法用於jni

class FFmpegAudioPlayer {
    /** * 初始化 */
    external fun init(paths: Array<String>)
    /** * 播放 */
    external fun play()

    /** * 暫停 */
    external fun pause()

    /** * 釋放資源 */
    external fun release()

    /** * 修改每一個音量 */
    external fun changeVolumes(volumes: Array<String>)

    /** * 變速 */
    external fun changeTempo(tempo: String)

    /** * 總時長 秒 */
    external fun duration(): Double

    /** * 當前進度 秒 */
    external fun position(): Double

    /** * 進度跳轉 */
    external fun seek(sec: Double)

    companion object {
        init {
            System.loadLibrary("avutil-55")
            System.loadLibrary("swresample-2")
            System.loadLibrary("avcodec-57")
            System.loadLibrary("avfilter-6")
            System.loadLibrary("swscale-4")
            System.loadLibrary("avformat-57")
            System.loadLibrary("native-lib")
        }
    }
}
複製代碼

而後在jni層,實現對應的方法。

#include "AudioPlayer.h"
static AudioPlayer *player;
extern "C" JNIEXPORT void JNICALL Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_init( JNIEnv *env, jobject /* this */, jobjectArray _srcs) {
    jsize len = env->GetArrayLength(_srcs);
    char **pathArr = (char **) malloc(len * sizeof(char *));
    int i = 0;
    for (i = 0; i < len; i++) {
        jstring str = static_cast<jstring>(env->GetObjectArrayElement(_srcs, i));
        pathArr[i] = const_cast<char *>(env->GetStringUTFChars(str, 0));
    }
    player = new AudioPlayer(pathArr, len);
}

extern "C" JNIEXPORT void JNICALL Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_changeVolumes( JNIEnv *env, jobject /* this */, jobjectArray _volumes) {
    jsize len = env->GetArrayLength(_volumes);
    int i = 0;
    for (i = 0; i < len; i++) {
        jstring str = static_cast<jstring>(env->GetObjectArrayElement(_volumes, i));
        char *volume = const_cast<char *>(env->GetStringUTFChars(str, 0));
        player->volumes[i] = volume;
    }
    player->change = 1;//經過change標記表面參數發生變化,從而修改過濾器參數
}

extern "C" JNIEXPORT void JNICALL Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_changeTempo( JNIEnv *env, jobject /* this */, jstring _tempo) {
    char *tempo = const_cast<char *>(env->GetStringUTFChars(_tempo, 0));
    player->tempo = tempo;
    player->change = 1;
}
extern "C" JNIEXPORT void JNICALL Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_play( JNIEnv *env, jobject /* this */) {
    player->play();
}

extern "C" JNIEXPORT void JNICALL Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_pause( JNIEnv *env, jobject /* this */) {
    player->pause();
}

extern "C" JNIEXPORT void JNICALL Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_release( JNIEnv *env, jobject /* this */) {
    player->release();
}
extern "C" JNIEXPORT void JNICALL Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_seek( JNIEnv *env, jobject /* this */, jdouble secs) {
    player->seek(secs);
}

extern "C" JNIEXPORT jdouble JNICALL Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_duration( JNIEnv *env, jobject /* this */) {
    return player->total_time;
}

extern "C" JNIEXPORT jdouble JNICALL Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_position( JNIEnv *env, jobject /* this */) {
    return player->current_time;
}
複製代碼

最終的實如今AudioPlayer.cpp中

9.2 調音,變速

爲了可以實現變速,調音,咱們要在解碼以前從新修改過濾器的參數。這裏使用一個change參數做爲標記,代表須要從新初始化filter,初始化完成後,把change從新修改爲0。

int AudioPlayer::initFilters() {
    LOGI("init filters");
    if (change)avfilter_graph_free(&graph);
    graph = avfilter_graph_alloc();
    ...
    change = 0;
    return 1;
}
複製代碼

這裏須要將以前的過濾器資源釋放掉,以避免內存溢出。 在解碼以前,經過change標誌,從新初始化。

void AudioPlayer::decodeAudio() {
    ...
    while (isPlay) {
        LOGI("decode frame:%d", index);
        if (change) {
            initFilters();
        }
        for (int i = 0; i < fileCount; i++) {
            AVFormatContext *fmt_ctx = fmt_ctx_arr[i];
            ret = av_read_frame(fmt_ctx, packet);
            if (packet->stream_index != stream_index_arr[i])continue;
           ...
            ret = av_buffersrc_add_frame(srcs[i], frame);
            if (ret < 0) {
                LOGE("error add frame to filter");
                goto end;
            }
        }
        while (av_buffersink_get_frame(sink, frame) >= 0) {
            frame->pts = packet->pts;
            put(frame);
        }
        index++;
    }
    end:
   ...
}
複製代碼

這樣就能夠實現音量和速度的控制了。

9.3 暫停,播放

暫停能夠經過OpenSLES播放器接口經過設置暫停狀態來暫停播放。設置此狀態後,緩衝回調就會暫停回調。

void AudioPlayer::pause() {
    (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PAUSED);
}
複製代碼

而從新播放咱們也只須要設置播放中SL_PLAYSTATE_PLAYING狀態

void AudioPlayer::play() {
    LOGI("play...");
    if (isPlay) {
        (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PLAYING);
        return;
    }
    isPlay = 1;
    seek(0);
    pthread_create(&decodeId, NULL, _decodeAudio, this);
    pthread_create(&playId, NULL, _play, this);
}
複製代碼

9.4 進度控制

進度控制是使用av_seek_frame來實現,使用av_q2d將秒數轉爲ffmpeg內部的時間戳

void AudioPlayer::seek(double secs) {
    pthread_mutex_lock(&mutex);
    for (int i = 0; i < fileCount; i++) {
        av_seek_frame(fmt_ctx_arr[i], stream_index_arr[i], (int64_t) (secs / av_q2d(time_base)),
                      AVSEEK_FLAG_ANY);
    }
    current_time = secs;
    queue.clear();
    pthread_cond_signal(&not_full);
    pthread_mutex_unlock(&mutex);
}
複製代碼

9.5 釋放資源

設置播放器狀態爲中止,釋放Open SLES相關資源,釋放過濾器資源,釋放解碼器資源,關閉輸入流。

void AudioPlayer::release() {
    pthread_mutex_lock(&mutex);
    isPlay = 0;
    pthread_cond_signal(&not_full);
    pthread_mutex_unlock(&mutex);
    if (playItf)(*playItf)->SetPlayState(playItf, SL_PLAYSTATE_STOPPED);
    if (playerObject) {
        (*playerObject)->Destroy(playerObject);
        playerObject = 0;
        bufferQueueItf = 0;
    }
    if (mixObject) {
        (*mixObject)->Destroy(mixObject);
        mixObject = 0;
    }
    if (engineObject) {
        (*engineObject)->Destroy(engineObject);
        engineItf = 0;
    }
    if (swr_ctx) {
        swr_free(&swr_ctx);
    }
    if (graph) {
        avfilter_graph_free(&graph);
    }
    for (int i = 0; i < fileCount; i++) {
        avcodec_close(codec_ctx_arr[i]);
        avformat_close_input(&fmt_ctx_arr[i]);
    }
    free(codec_ctx_arr);
    free(fmt_ctx_arr);
    LOGI("release...");
}
複製代碼

9.6 最終效果

具體效果能夠運行項目查看

播放測試
相關文章
相關標籤/搜索