FFmpeg是一個強大的音視頻處理庫,可是一般接觸時以命令形式較多。本篇文章講了FFmpeg相關api的使用,尤爲是它強大的過濾器filter庫的使用。html
本項目主要素材爲five hundred mile吉他,尤克里裏,鼓等4個音軌素材。實現多音軌實時播放,多音軌音量調節,變速播放,進度調節等功能java
Android Studio默認下載對NDK版本會出現一些兼容問題,所以咱們這裏使用ndk-r15c(win64|linux64|mac64)版本。 FFmpeg官網下載源碼,我用的是3.2.12 android
首先解壓NDK和ffmpeggit
tar -zxf ffmpeg-3.2.12.tar.gz
unzip android-ndk-r15c-darwin-x86_64.zip -d android-ndk-r15c
複製代碼
進入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)'
複製代碼
新建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文件和頭文件 數組
打開Android Studio,新建項目FFmpegAudioPlayer,添加C++支持
在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下,最終目錄以下
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})
複製代碼
配置完成後,咱們先編譯運行一次,若是能順利成功安裝到手機上,正常運行,則說明配置正確。
FFmpeg的第一個強大之處是它的編解碼能力。它能夠將市面上的任意一種音頻格式(mp3,wav,aac,ogg等)和視頻格式(mp4,avi,rm,rmvb,mov等)解碼。經過解碼器將音頻視頻解碼成一個個AVFrame,每一個frame包含了音頻的pcm信息或視頻的yuv信息。經過編碼器,FFmpeg可將frame編碼成不一樣格式的音視頻文件。所以咱們能夠用FFmpeg很簡單的實現格式轉換,而不須要了解各類格式的相關協議。
爲了可以解碼mp3文件,須要經過ffmpeg讀取音頻信息,而後獲得對應的解碼器,而後循環讀取每一幀音頻數據,而且經過解碼器解碼。大體解碼流程以下:
在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文件了。
FFmpeg另外一個強大之處在於它實現了各式各樣的filter,能夠將音視頻出來成不一樣的效果,視頻能夠裁剪、縮放、旋轉、合併、添加水印等效果,音頻能夠去噪、回聲、延遲、混音、變速等效果。一個filter的輸出能夠做爲另外一個filter的輸入。經過filter組合使用,咱們能夠定製本身想要的音視頻特效。這次分兩節講兩種音頻filter的api用法,一種是單個輸入volume(音量調節),atempo(變速)
音頻解碼後,能夠avfilter api對解碼出來的AVFrame進行效果處理,如音量調節,變速處理。多個音頻輸入還能夠進行混音處理(見6.1) 單輸入過濾器解碼流程
解碼出AVFrame -> abuffer-> 其餘過濾器(volume)...->aformat->abuffersink->過濾後的AVFrame
複製代碼
這裏看到有三個通用的過濾器,abuffer,aformat,abuffersink。 abuffer用於接收輸入frame,造成待處理的數據緩存,abuffersink用於傳出輸出Frame,aformat過濾器約束最終的輸出格式(採樣率,聲道數,存儲位數等),這三個不可缺乏。 而中間的其餘過濾器能夠串聯多個filter,如volume,atempo
這裏咱們先要知道三個重要的結構體 AVFilterGraph (管理全部的filter) AVFilterContext (filter上下文) AVFilter(具體過濾器)
經過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;
}
複製代碼
完成過濾器初始化後,就能夠在解碼後使用過濾器處理音頻了。使用方法很簡單,將解碼後的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波形對比
在播放音頻時,能夠聽見有一些噪聲,須要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個聲道的數據,並且也沒有噪聲了。
將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的播放的,並且音調保持不變,沒有由於速度的改變而變高或者變低。
FFmpeg使用過濾器filter的另一個場景就是處理多個輸入數據,好比視頻添加水印,添加字幕,音視頻合併等。這類場景須要兩個及以上輸入端。本節講amix,它能夠將多個音頻混音。
輸入AVFrame1 -> abuffer
-> amix -> aformat -> abuffersink -> 輸出AVFrame
輸入AVFrame2 -> abuffer
複製代碼
處理流程和單輸入過濾器大體相同,只不過接收了多個輸入端。所以須要多個filter上下文做爲輸入端。
//初始化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過濾器,這樣就能夠接收多個輸入端了。
爲了可以傳入多個音頻數據,咱們須要同時解碼多個音頻文件,所以在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目錄下,能夠自行對比下效果
爲了可以在Android播放pcm格式的音頻,咱們使用OpenSLES庫。在cmke的target_link_libraries中加入OpenSLES,使用時加入頭文件<SLES/OpenSLES_Android.h>
SLObjectItf engineObject;
slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
複製代碼
SLEngineItf engineItf;
(*enginObject)->GetInterface(engineObject,SL_IID_ENGINE,&engineItf);
複製代碼
SLObjectItf mixObject;
(*engineItf)->CreateOutputMix(engineItf, &mixObject, 0, 0, 0);
複製代碼
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);
複製代碼
//獲取播放接口
SLPlayItf playItf;
(*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playItf);
//獲取緩衝接口
SLBufferQueueItf bufferQueueItf;
(*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &bufferQueueItf);
複製代碼
//註冊緩衝回調
(*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);
}
}
複製代碼
爲了可以獲取pcm數據,咱們使用多線程進行音頻解碼,經過條件變量,實現一個生產者消費者的模型,解碼過程爲生產過程,回調播放爲消費過程。將解碼獲得的AVFrame加入到vector隊列中,而後在播放回調時取出AVFrame,使用swr_convert轉成pcm數據。
申明全局的變量
static pthread_mutex_t mutex;
//條件變量
static pthread_cond_t notfull; //隊列未達到最大緩衝容量,存
static pthread_cond_t notempty;//隊列不爲空,取
複製代碼
初始化同步鎖和條件變量,啓動解碼線程(放在建立播放器前面)
//初始化同步鎖和條件變量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(¬full, NULL);
pthread_cond_init(¬empty, NULL);
//初始化解碼線程
pthread_t pid;
char *path = (char *) env->GetStringUTFChars(_path, 0);
pthread_create(&pid, NULL, decodeAudio, path);
複製代碼
申明全局變量
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(¬full, &mutex);//等待隊列不爲滿信號
}
queue.push_back(frame);
pthread_cond_signal(¬empty);//發送不爲空信號
pthread_mutex_unlock(&mutex);
}
}
複製代碼
經過以前註冊的緩衝回調,咱們能夠把加入到隊列的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(¬full);//發送notfull信號
pthread_mutex_unlock(&mutex);
return out;
} else {//爲空等待添加
LOGI("wait for get frame");
pthread_cond_wait(¬empty, &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播放了。
有了前面一系列的準備知識,能夠開始打造FFmpeg音頻播放器了。主要需求,多個音頻混音播放,每一個音軌音量控制,合成音頻變速播放。
首先咱們建立一個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; //緩衝接口
複製代碼
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(¬_full, NULL);
pthread_cond_init(¬_empty, NULL);
initCodecs(pathArr);
avfilter_register_all();
initSwrContext();
initFilters();
createPlayer();
}
複製代碼
這裏咱們還初始化了控制各個音頻音量和速度的變量,同步鎖和條件變量(生產消費控制)。
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初始化。
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。這樣音頻就能夠實現調音,混音,變速的效果。
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。
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設置的參數要一致
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,加入到隊列中。
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);
}
}
}
複製代碼
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時間進度不是當前解碼的進度)。
/** * 將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(¬_full, &mutex);
}
queue.push_back(out);
pthread_cond_signal(¬_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(¬_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(¬_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的多音頻播放了
前面一節咱們已經建立了一個基於FFmpeg的播放器,這一節開始對播放器進行各類控制操做。主要有調音,變速,暫停,播放,進度切換,中止(釋放資源)。
首先在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中
爲了可以實現變速,調音,咱們要在解碼以前從新修改過濾器的參數。這裏使用一個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:
...
}
複製代碼
這樣就能夠實現音量和速度的控制了。
暫停能夠經過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);
}
複製代碼
進度控制是使用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(¬_full);
pthread_mutex_unlock(&mutex);
}
複製代碼
設置播放器狀態爲中止,釋放Open SLES相關資源,釋放過濾器資源,釋放解碼器資源,關閉輸入流。
void AudioPlayer::release() {
pthread_mutex_lock(&mutex);
isPlay = 0;
pthread_cond_signal(¬_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...");
}
複製代碼
具體效果能夠運行項目查看