開篇
在學(xué)習(xí)了《Android使用OpenGL渲染ffmpeg解碼的YUV數(shù)據(jù)》一文之后垦沉,我們的播放器計(jì)劃對于視頻的處理暫時(shí)先告一段落。后面的幾篇文章我們主要介紹ffmpeg解碼音頻并且搭配AudioTrack以及OpenSLES播放PCM原始音頻數(shù)據(jù)劣像。
音頻解碼
對于使用ffmpeg進(jìn)行音視頻的解碼過程乡话,我們來回憶一下這張圖:
其實(shí)音頻的解碼和視頻的解碼差不多,同樣要經(jīng)過解封裝耳奕,獲取流索引绑青、初始化解碼器上下文诬像、打開解碼器、av_read_frame讀取包數(shù)據(jù)闸婴、avcodec_send_packet發(fā)送到解碼器進(jìn)行解碼坏挠、avcodec_receive_frame接收解碼器數(shù)據(jù),這幾個(gè)過程邪乍。
不同的是降狠,視頻在avcodec_receive_frame接收到解碼數(shù)據(jù)是YUV,需要經(jīng)歷YUV轉(zhuǎn)換成RGB渲染出來庇楞。對于YUV不了解的同學(xué)榜配,可參考一下這篇文章《Android使用ffmpeg解碼視頻為YUV》。
而對于音頻則在avcodec_receive_frame接收到的解碼數(shù)據(jù)是PCM原始聲音數(shù)據(jù)吕晌,而一般PCM數(shù)據(jù)都是無法直接播放的蛋褥,要按照播放的設(shè)備標(biāo)準(zhǔn)進(jìn)行重采樣之后生成新的PCM數(shù)據(jù)才能在特定的設(shè)備上播放。例如你解碼出來的PCM數(shù)據(jù)是32位的睛驳,但是你的設(shè)備只支持16位烙心,這就需要重采樣了。又比如說你的設(shè)備只支持播放44100的采樣率的乏沸,而你解碼出來的PCM卻不是44100的采樣率淫茵,那也是需要重采樣的,當(dāng)然實(shí)際情況不止這兩種蹬跃,這就是重采樣的意義所在匙瘪。
對于音頻的重采樣主要使用的是ffmpeg的swr_convert
函數(shù),重采樣的主要代碼:
.....省略若干代碼
//準(zhǔn)備音頻重采樣的參數(shù)
int dataSize = av_samples_get_buffer_size(NULL, av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO) , cc_ctx->frame_size,AV_SAMPLE_FMT_S16, 0);
uint8_t *resampleOutBuffer = (uint8_t *) malloc(dataSize);
//音頻重采樣上下文初始化
SwrContext *actx = swr_alloc();
actx = swr_alloc_set_opts(actx,
AV_CH_LAYOUT_STEREO,
AV_SAMPLE_FMT_S16,44100,
cc_ctx->channels,
cc_ctx->sample_fmt,cc_ctx->sample_rate,
0,0 );
re = swr_init(actx);
if(re != 0)
{
LOGE("swr_init failed:%s",av_err2str(re));
return re;
}
.....省略若干代碼
//這里為什么要使用一個(gè)for循環(huán)呢炬转?
// 因?yàn)閍vcodec_send_packet和avcodec_receive_frame并不是一對一的關(guān)系的
//一個(gè)avcodec_send_packet可能會出發(fā)多個(gè)avcodec_receive_frame
for (;;) {
// 接受解碼的數(shù)據(jù)
re = avcodec_receive_frame(cc_ctx, frame);
if (re != 0) {
break;
} else {
//音頻重采樣
int len = swr_convert(actx,&resampleOutBuffer,
frame->nb_samples,
(const uint8_t**)frame->data,
frame->nb_samples);
}
}
使用AudioTrack播放PCM
對于AudioTrack如何使用還不了解的同學(xué)建議先自行谷歌學(xué)習(xí)一下辆苔,也就是幾個(gè)簡單的API,這里就不多做介紹了扼劈。
因?yàn)閒fmpeg的解碼是在JNI代碼中執(zhí)行的驻啤,所以我們直接在JNI通過反射調(diào)用Java的方法構(gòu)造出AudioTrack對象,然后傳遞數(shù)據(jù)給AudioTrack對象即可實(shí)現(xiàn)播放荐吵。
JNI構(gòu)建AudioTrack對象代碼:
// JNI創(chuàng)建AudioTrack
jclass jAudioTrackClass = env->FindClass("android/media/AudioTrack");
jmethodID jAudioTrackCMid = env->GetMethodID(jAudioTrackClass,"<init>","(IIIIII)V"); //構(gòu)造
// public static final int STREAM_MUSIC = 3;
int streamType = 3;
int sampleRateInHz = 44100;
// public static final int CHANNEL_OUT_STEREO = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT);
int channelConfig = (0x4 | 0x8);
// public static final int ENCODING_PCM_16BIT = 2;
int audioFormat = 2;
// getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)
jmethodID jGetMinBufferSizeMid = env->GetStaticMethodID(jAudioTrackClass, "getMinBufferSize", "(III)I");
int bufferSizeInBytes = env->CallStaticIntMethod(jAudioTrackClass, jGetMinBufferSizeMid, sampleRateInHz, channelConfig, audioFormat);
// public static final int MODE_STREAM = 1;
int mode = 1;
//創(chuàng)建了AudioTrack
jobject jAudioTrack = env->NewObject(jAudioTrackClass,jAudioTrackCMid, streamType, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, mode);
//play方法
jmethodID jPlayMid = env->GetMethodID(jAudioTrackClass,"play","()V");
env->CallVoidMethod(jAudioTrack,jPlayMid);
// write method
jmethodID jAudioTrackWriteMid = env->GetMethodID(jAudioTrackClass, "write", "([BII)I");
AudioTrack構(gòu)造好之后骑冗,我們在重采樣PCM數(shù)據(jù)之后直接再通過JNI傳遞PCM數(shù)據(jù)到AudioTrack就可以播放了。
下面貼一下完整的代碼:
extern "C"
JNIEXPORT jint JNICALL
Java_com_flyer_ffmpeg_FFmpegUtils_playAudio(JNIEnv *env, jclass clazz, jstring audio_path) {
const char *path = env->GetStringUTFChars(audio_path, 0);
AVFormatContext *fmt_ctx;
// 初始化格式化上下文
fmt_ctx = avformat_alloc_context();
// 使用ffmpeg打開文件
int re = avformat_open_input(&fmt_ctx, path, nullptr, nullptr);
if (re != 0) {
LOGE("打開文件失斚燃濉:%s", av_err2str(re));
return re;
}
//探測流索引
re = avformat_find_stream_info(fmt_ctx, nullptr);
if (re < 0) {
LOGE("索引探測失斣羯:%s", av_err2str(re));
return re;
}
//尋找視頻流索引
int audio_idx = av_find_best_stream(
fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
if (audio_idx == -1) {
LOGE("獲取音頻流索引失敗");
return -1;
}
//解碼器參數(shù)
AVCodecParameters *c_par;
//解碼器上下文
AVCodecContext *cc_ctx;
//聲明一個(gè)解碼器
const AVCodec *codec;
c_par = fmt_ctx->streams[audio_idx]->codecpar;
//通過id查找解碼器
codec = avcodec_find_decoder(c_par->codec_id);
if (!codec) {
LOGE("查找解碼器失敗");
return -2;
}
//用參數(shù)c_par實(shí)例化編解碼器上下文,薯蝎,并打開編解碼器
cc_ctx = avcodec_alloc_context3(codec);
// 關(guān)聯(lián)解碼器上下文
re = avcodec_parameters_to_context(cc_ctx, c_par);
if (re < 0) {
LOGE("解碼器上下文關(guān)聯(lián)失敗:%s", av_err2str(re));
return re;
}
//打開解碼器
re = avcodec_open2(cc_ctx, codec, nullptr);
if (re != 0) {
LOGE("打開解碼器失敗:%s", av_err2str(re));
return re;
}
//數(shù)據(jù)包
AVPacket *pkt;
//數(shù)據(jù)幀
AVFrame *frame;
//初始化
pkt = av_packet_alloc();
frame = av_frame_alloc();
//音頻重采樣
int dataSize = av_samples_get_buffer_size(NULL, av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO) , cc_ctx->frame_size,AV_SAMPLE_FMT_S16, 0);
uint8_t *resampleOutBuffer = (uint8_t *) malloc(dataSize);
//音頻重采樣上下文初始化
SwrContext *actx = swr_alloc();
actx = swr_alloc_set_opts(actx,
AV_CH_LAYOUT_STEREO,
AV_SAMPLE_FMT_S16,44100,
cc_ctx->channels,
cc_ctx->sample_fmt,cc_ctx->sample_rate,
0,0 );
re = swr_init(actx);
if(re != 0)
{
LOGE("swr_init failed:%s",av_err2str(re));
return re;
}
// JNI創(chuàng)建AudioTrack
jclass jAudioTrackClass = env->FindClass("android/media/AudioTrack");
jmethodID jAudioTrackCMid = env->GetMethodID(jAudioTrackClass,"<init>","(IIIIII)V"); //構(gòu)造
// public static final int STREAM_MUSIC = 3;
int streamType = 3;
int sampleRateInHz = 44100;
// public static final int CHANNEL_OUT_STEREO = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT);
int channelConfig = (0x4 | 0x8);
// public static final int ENCODING_PCM_16BIT = 2;
int audioFormat = 2;
// getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)
jmethodID jGetMinBufferSizeMid = env->GetStaticMethodID(jAudioTrackClass, "getMinBufferSize", "(III)I");
int bufferSizeInBytes = env->CallStaticIntMethod(jAudioTrackClass, jGetMinBufferSizeMid, sampleRateInHz, channelConfig, audioFormat);
// public static final int MODE_STREAM = 1;
int mode = 1;
//創(chuàng)建了AudioTrack
jobject jAudioTrack = env->NewObject(jAudioTrackClass,jAudioTrackCMid, streamType, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, mode);
//play方法
jmethodID jPlayMid = env->GetMethodID(jAudioTrackClass,"play","()V");
env->CallVoidMethod(jAudioTrack,jPlayMid);
// write method
jmethodID jAudioTrackWriteMid = env->GetMethodID(jAudioTrackClass, "write", "([BII)I");
while (av_read_frame(fmt_ctx, pkt) >= 0) {//持續(xù)讀幀
// 只解碼音頻流
if (pkt->stream_index == audio_idx) {
//發(fā)送數(shù)據(jù)包到解碼器
avcodec_send_packet(cc_ctx, pkt);
//清理
av_packet_unref(pkt);
//這里為什么要使用一個(gè)for循環(huán)呢遥倦?
// 因?yàn)閍vcodec_send_packet和avcodec_receive_frame并不是一對一的關(guān)系的
//一個(gè)avcodec_send_packet可能會出發(fā)多個(gè)avcodec_receive_frame
for (;;) {
// 接受解碼的數(shù)據(jù)
re = avcodec_receive_frame(cc_ctx, frame);
if (re != 0) {
break;
} else {
//音頻重采樣
int len = swr_convert(actx,&resampleOutBuffer,
frame->nb_samples,
(const uint8_t**)frame->data,
frame->nb_samples);
jbyteArray jPcmDataArray = env->NewByteArray(dataSize);
// native 創(chuàng)建 c 數(shù)組
jbyte *jPcmData = env->GetByteArrayElements(jPcmDataArray, NULL);
//內(nèi)存拷貝
memcpy(jPcmData, resampleOutBuffer, dataSize);
// 同步刷新到 jbyteArray ,并釋放 C/C++ 數(shù)組
env->ReleaseByteArrayElements(jPcmDataArray, jPcmData, 0);
LOGE("解碼成功%d dataSize:%d ",len,dataSize);
// 寫入播放數(shù)據(jù)
env->CallIntMethod(jAudioTrack, jAudioTrackWriteMid, jPcmDataArray, 0, dataSize);
// 解除 jPcmDataArray 的持有占锯,讓 javaGC 回收
env->DeleteLocalRef(jPcmDataArray);
}
}
}
}
//關(guān)閉環(huán)境
avcodec_free_context(&cc_ctx);
// 釋放資源
av_frame_free(&frame);
av_packet_free(&pkt);
avformat_free_context(fmt_ctx);
LOGE("音頻播放完畢");
env->ReleaseStringUTFChars(audio_path, path);
return 0;
}
結(jié)束
最后如果你對音視頻開發(fā)感興趣可掃碼關(guān)注袒哥,筆者在各個(gè)知識點(diǎn)學(xué)習(xí)完畢之后也會使用ffmepg從零開始編寫一個(gè)多媒體播放器缩筛,包括本地播放及網(wǎng)絡(luò)流播放等等。歡迎關(guān)注堡称,后續(xù)我們共同探討瞎抛,共同進(jìn)步。