采集音频并对音频编码
文章对应的项目地址
https://github.com/cuiyaoDroid/AndroidFFmpegAac
ffmpeg编译静态库的部分只有编译脚本,ffmpeg的源码可以从官方获取。
一、使用ndk编译ffmpeg
获取ffmpeg源码。
git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg
然后我们修改一下里面的configure文件,让我们编译出来的文件不会带有奇怪的名字,不被Android识别。只要把
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)'
我编译时使用的脚本。需要根据自己的ndk的路径配置交叉编译脚本。
android_config_armeabi_v7a.sh,此脚本需要是编译armv7版本的静态库使用,编译armv64版本需要修改脚本。
#!/bin/bash
NDK=/Users/yaocui/Documents/adt-bundle-mac-x86_64-20140702/android-ndk-r13b
#NDK=/Users/yaocui/Documents/adt-bundle-mac-x86_64-20140702/sdk/ndk-bundle
SYSROOT=$NDK/platforms/android-21/arch-arm64
TOOLCHAIN=$NDK/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64
#TOOLCHAIN=$NDK/prebuilt/darwin-x86_64
function build_one {
./configure \
--prefix=$PREFIX \
--cc=$TOOLCHAIN/bin/aarch64-linux-android-gcc \
--nm=$TOOLCHAIN/bin/aarch64-linux-android-nm \
--enable-asm \
--enable-neon \
--enable-static \
--disable-shared \
--disable-doc \
--disable-asm \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-postproc \
--disable-avdevice \
--disable-symver \
--disable-stripping \
--disable-muxers \
--disable-encoders \
--enable-encoder=aac \
--disable-decoders \
--enable-decoder=aac \
--disable-demuxers \
--enable-demuxer=aac \
--disable-parsers \
--enable-parser=aac \
--cross-prefix=$TOOLCHAIN/bin/aarch64-linux-android- \
--target-os=linux \
--arch=aarch64 \
--cpu=armv8-a \
--enable-runtime-cpudetect \
--enable-gpl \
--enable-small \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-fPIC -DANDROID -I$NDK/platforms/android-21/arch-arm64/usr/include -I$SYSROOT/usr/include" \
--extra-ldflags="$ADDI_LDFLAGS"
sed -i '' 's/HAVE_LRINT 0/HAVE_LRINT 1/g' config.h
sed -i '' 's/HAVE_LRINTF 0/HAVE_LRINTF 1/g' config.h
sed -i '' 's/HAVE_ROUND 0/HAVE_ROUND 1/g' config.h
sed -i '' 's/HAVE_ROUNDF 0/HAVE_ROUNDF 1/g' config.h
sed -i '' 's/HAVE_TRUNC 0/HAVE_TRUNC 1/g' config.h
sed -i '' 's/HAVE_TRUNCF 0/HAVE_TRUNCF 1/g' config.h
sed -i '' 's/HAVE_CBRT 0/HAVE_CBRT 1/g' config.h
sed -i '' 's/HAVE_RINT 0/HAVE_RINT 1/g' config.h
sed -i '' 's/HAVE_LOG2 1/HAVE_LOG2 0/g' config.h
sed -i '' 's/HAVE_LOG2F 1/HAVE_LOG2F 0/g' config.h
make clean
make -j4
make install
}
#CPU=arm
#PREFIX=$(pwd)/android/$CPU
# arm v7vfp
CPU=armv8-a
OPTIMIZE_CFLAGS="-marm -march=$CPU "
PREFIX=./android/$CPU
ADDI_CFLAGS="-marm"
build_one
关键几个脚本
--disable-shared --enable-static 关闭动态库的生成,开启静态库的生成,本例中使用静态库,想要使用动态库也可以,但动态库最终生成的程序安装包会相对大一些。
--disable-encoders --disable-decoders... 关闭所有编码器解码器等,将无用的功能关闭掉,减小生成的库文件的大小。
--enable-encoder=aac --enable-decoder=aac 开启aac的编码器和解码器,我们只用到了aac的编解码器。
--disable-ffmpeg --disable-ffplay --disable-ffprobe --disable-ffserver --disable-postproc --disable-avdevice 关闭一些我们不需要的功能,减小库文件大小。
执行编译脚本
./android_config_armeabi_v7a.sh
生成的库文件在android目录下。
arm64的编译过程一样,可以查看我的项目。
https://github.com/cuiyaoDroid/AndroidFFmpegAac/blob/master/android_config_arm64_v8a.sh
二、创建android项目
1、创建一个android项目,添加c++支持。
2、将编译ffmpeg生成的静态库和头文件复制进项目目录下。
3、编写Application.mk,支持armeabi-v7a arm64-v8a两种架构。
APP_STL := gnustl_static
APP_LDFLAGS := -latomic
APP_ABI := armeabi-v7a arm64-v8a
APP_PLATFORM := android-21
4、编写Android.mk文件,根据不同的cpu架构使用不同的静态库。
LOCAL_PATH := $(call my-dir)
# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a arm64-v8a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := avcodec
ifeq ($(APP_ABI), armeabi-v7a)
LOCAL_SRC_FILES := libarmv7a/libavcodec.a
else
LOCAL_SRC_FILES := libarm64/libavcodec.a
endif
include $(PREBUILT_STATIC_LIBRARY)
# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a arm64-v8a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := avfilter
ifeq ($(APP_ABI), armeabi-v7a)
LOCAL_SRC_FILES := libarmv7a/libavfilter.a
else
LOCAL_SRC_FILES := libarm64/libavfilter.a
endif
include $(PREBUILT_STATIC_LIBRARY)
# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a arm64-v8a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := avformat
ifeq ($(APP_ABI), armeabi-v7a)
LOCAL_SRC_FILES := libarmv7a/libavformat.a
else
LOCAL_SRC_FILES := libarm64/libavformat.a
endif
include $(PREBUILT_STATIC_LIBRARY)
# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a arm64-v8a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := avutil
ifeq ($(APP_ABI), armeabi-v7a)
LOCAL_SRC_FILES := libarmv7a/libavutil.a
else
LOCAL_SRC_FILES := libarm64/libavutil.a
endif
include $(PREBUILT_STATIC_LIBRARY)
# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a arm64-v8a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := swresample
ifeq ($(APP_ABI), armeabi-v7a)
LOCAL_SRC_FILES := libarmv7a/libswresample.a
else
LOCAL_SRC_FILES := libarm64/libswresample.a
endif
include $(PREBUILT_STATIC_LIBRARY)
# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a arm64-v8a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := swscale
ifeq ($(APP_ABI), armeabi-v7a)
LOCAL_SRC_FILES := libarmv7a/libswscale.a
else
LOCAL_SRC_FILES := libarm64/libswscale.a
endif
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a arm64-v8a
LOCAL_MODULE := ffmpeg_aac_jni
LOCAL_SRC_FILES := FFmpegAacJni.cpp AacRecoder.cpp AacPlayer.cpp
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
LOCAL_CFLAGS := -D__STDC_CONSTANT_MACROS -Wno-sign-compare -Wno-switch -Wno-pointer-sign -DHAVE_NEON=1 -mfpu=neon -mfloat-abi=softfp -fPIC -DANDROID
LOCAL_STATIC_LIBRARIES := avfilter avformat avcodec swresample swscale avutil
LOCAL_LDLIBS := -L$(NDK_ROOT)/platforms/$(APP_PLATFORM)/arch-arm/usr/lib -L$(LOCAL_PATH) -llog -ljnigraphics -landroid -lz -ldl -lm
include $(BUILD_SHARED_LIBRARY)
脚本中使用LOCAL_STATIC_LIBRARIES配置静态库的链接,对顺序有要求,不同版本ffmpeg的顺序可能不同,可以查看ffmpeg目录下的makefile文件,查看库文件加载顺序。
...
# $(FFLIBS-yes) needs to be in linking order
FFLIBS-$(CONFIG_AVDEVICE) += avdevice
FFLIBS-$(CONFIG_AVFILTER) += avfilter
FFLIBS-$(CONFIG_AVFORMAT) += avformat
FFLIBS-$(CONFIG_AVCODEC) += avcodec
FFLIBS-$(CONFIG_AVRESAMPLE) += avresample
FFLIBS-$(CONFIG_POSTPROC) += postproc
FFLIBS-$(CONFIG_SWRESAMPLE) += swresample
FFLIBS-$(CONFIG_SWSCALE) += swscale
FFLIBS := avutil
...
5、创建c++文件实现编解码。
三、编解码逻辑的实现。
1、首先创建java的native方法,这里我们一共需要6个方法,分别是
编码部分:初始化编码器、编码pcm数据得到aac数据、关闭编码器。
解码部分:初始化解码器、使用解码器读取pcm数据、关闭解码器。
public class FFmpegAacNativeLib
{
public long mNativeContextRecoder;
public long mNativeContextPlayer;
static {
System.loadLibrary("ffmpeg_aac_jni");
}
//declare the jni functions
public native String audioPlayerOpenFile(String path);//初始化解码器
public native int audioPlayerGetPCM(byte[] pcmbuffer);//使用解码器读取pcm数据
public native int audioPlayerStop();//关闭解码器
public native int audioEncodePCMToAACInit();//初始化编码器
public native int audioEncodePCMToAAC(byte[] pcmbuf,int len,byte[] amrbuf);//使用解码器读取pcm数据
public native void audioEncodeStop();//关闭解码器
//Singleton
private static FFmpegAacNativeLib instance=null;
public static FFmpegAacNativeLib getInstance() {
if(instance==null)
instance=new FFmpegAacNativeLib();
return instance;
}
}
2、编写jni接口。
FFmpegAacJni.cpp
...
#include <jni.h>
...
static jint audioEncodePCMToAACInit(JNIEnv *env, jobject thiz) {
return 0;
}
static jint audioEncodePCMToAAC(JNIEnv *env, jobject thiz, jbyteArray pcmbuf_,jint len,
jbyteArray amrbuf_) {
return 0;
}
static void audioEncodeStop(JNIEnv *env, jobject thiz) {
}
static jstring audioPlayerOpenFile(JNIEnv *env, jobject thiz, jstring path) {
return NULL;
}
static jint audioPlayerGetPCM(JNIEnv *env, jobject thiz, jbyteArray pcmbuffer_) {
return 0;
}
static jint audioPlayerStop(JNIEnv *env, jobject thiz) {
// TODO
return 0;
}
static JNINativeMethod gMethods[] = {
{ "audioEncodePCMToAACInit", "()I", (void *)audioEncodePCMToAACInit },
{ "audioEncodePCMToAAC", "([BI[B)I", (void *)audioEncodePCMToAAC },
{ "audioEncodeStop", "()V", (void *)audioEncodeStop },
{ "audioPlayerOpenFile", "(Ljava/lang/String;)Ljava/lang/String;", (void *)audioPlayerOpenFile },
{ "audioPlayerGetPCM", "([B)I", (void *)audioPlayerGetPCM },
{ "audioPlayerStop", "()I", (void *)audioPlayerStop }
};
jint JNI_OnLoad(JavaVM* vm, void* reserved){
ALOGD("JNI_OnLoad");
JNIEnv* env = NULL;
jint result = -1;
jclass clazz;
if ((*vm).GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
ALOGE("GetEnv fail");
return result;
}
assert(env != NULL);
clazz = (*env).FindClass("com/cuiyao/ffmpegaac/lib/FFmpegAacNativeLib");
if (clazz == NULL) {
ALOGE("com/cuiyao/ffmpegaac/lib/FFmpegAacNativeLib not found");
return result;
}
// 注册native方法到java中
if ((*env).RegisterNatives(clazz, gMethods, sizeof(gMethods)/sizeof(gMethods[0])) < 0) {
ALOGE("RegisterNatives methods fail");
return result;
}
// 返回jni的版本
return JNI_VERSION_1_4;
}
3、关键代码编码器初始化,设置目标音频各项参数,初始化编码器和缓冲区。在后面编写java层AudioRecord的参数和缓存区需要一一对应。
ALOGW("start");
// av_register_all();
avcodec_register_all();
mAVCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);//查找AAC编码器
if(!mAVCodec){
ALOGE("encoder AV_CODEC_ID_AAC not found");
return -1;
}
mAVCodecContext = avcodec_alloc_context3(mAVCodec);
if(mAVCodecContext != NULL){
mAVCodecContext->codec_id = AV_CODEC_ID_AAC;
mAVCodecContext->codec_type = AVMEDIA_TYPE_AUDIO;
mAVCodecContext->bit_rate = 12200;
mAVCodecContext->sample_fmt = AV_SAMPLE_FMT_FLTP;
mAVCodecContext->sample_rate = 8000;
mAVCodecContext->channel_layout = AV_CH_LAYOUT_MONO;
mAVCodecContext->channels = av_get_channel_layout_nb_channels(mAVCodecContext->channel_layout);
}else {
ALOGE("avcodec_alloc_context3 fail");
return -1;
}
ALOGW("start 3 channels %d",mAVCodecContext->channels);
if(avcodec_open2(mAVCodecContext, mAVCodec, NULL) < 0){
ALOGE("aac avcodec open fail");
av_free(mAVCodecContext);
mAVCodecContext = NULL;
return -1;
}
mAVFrame = av_frame_alloc();
if(!mAVFrame) {
ALOGE("avframe alloc fail");
avcodec_close(mAVCodecContext);
av_free(mAVCodecContext);
mAVCodecContext = NULL;
return -1;
}
mAVFrame->nb_samples = mAVCodecContext->frame_size;
mAVFrame->format = mAVCodecContext->sample_fmt;
mAVFrame->channel_layout = mAVCodecContext->channel_layout;
mBufferSize = av_samples_get_buffer_size(NULL, mAVCodecContext->channels, mAVCodecContext->frame_size, mAVCodecContext->sample_fmt, 0);
if(mBufferSize < 0){
ALOGE("av_samples_get_buffer_size fail");
av_frame_free(&mAVFrame);
mAVFrame = NULL;
avcodec_close(mAVCodecContext);
av_free(mAVCodecContext);
mAVCodecContext = NULL;
return -1;
}
mEncoderData = (uint8_t *)av_malloc(mBufferSize);
if(!mEncoderData){
ALOGE("av_malloc fail");
av_frame_free(&mAVFrame);
mAVFrame = NULL;
avcodec_close(mAVCodecContext);
av_free(mAVCodecContext);
mAVCodecContext = NULL;
return -1;
}
avcodec_fill_audio_frame(mAVFrame, mAVCodecContext->channels, mAVCodecContext->sample_fmt, (const uint8_t*)mEncoderData, mBufferSize, 0);
4、使用编码器对pcm数据进行编码得到aac数据
int AacRecoder::encode_pcm_data(void* pIn, int frameSize,jbyte * pOut){
int encode_ret = -1;
int got_packet_ptr = 0;
AVPacket pkt;
av_init_packet(&pkt);
pkt.data = NULL;
pkt.size = 0;
if(mAVCodecContext && mAVFrame){
short2float((int16_t *)pIn, mEncoderData, frameSize/2);
mAVFrame->data[0] = mEncoderData;
mAVFrame->pts = 0;
//音频编码
encode_ret = avcodec_encode_audio2(mAVCodecContext, &pkt, mAVFrame, &got_packet_ptr);
if(encode_ret < 0){
ALOGE("Failed to encode!\n");
return encode_ret;
}
if(pkt.size > 0){
int length = pkt.size + ADTS_HEADER_LENGTH;
void *adts = malloc(ADTS_HEADER_LENGTH);
//添加adts header 可以正常播放。
addADTSheader((uint8_t *)adts, pkt.size+ADTS_HEADER_LENGTH);
// ALOGW("header ---- =%s",adts);
memcpy(pOut,adts, ADTS_HEADER_LENGTH);
free(adts);
memcpy(pOut+ADTS_HEADER_LENGTH,pkt.data,pkt.size);
// ALOGW("data ---- =%s",pkt.data);
av_free_packet(&pkt);
return length;
}
av_free_packet(&pkt);
return 0;
}
return encode_ret;
}
对每一帧的音频加adts头,不加adts头的音频无法直接播放。
void AacRecoder::addADTSheader(uint8_t * in, int packet_size){
int sampling_frequency_index = 11; //采样率下标
int channel_configuration = mAVCodecContext->channels; //声道数
in[0] = 0xFF;
in[1] = 0xF9;
in[2] = 0x40 | (sampling_frequency_index << 2) | (channel_configuration >> 2);//0x6c;
in[3] = (channel_configuration & 0x3) << 6;
in[3] |= (packet_size & 0x1800) >> 11;
in[4] = (packet_size & 0x1FF8) >> 3;
in[5] = ((((unsigned char)packet_size) & 0x07) << 5) | (0xff >> 3);
in[6] = 0xFC;
}
5、解码器初始化,得到音频的音道数量和采样率。
ALOGW("start");
// myinput = (char*)malloc(sizeof(input));
// strcpy(myinput,input);
ALOGI("%s", "sound");
//注册组件
av_register_all();
pFormatCtx = avformat_alloc_context();
//打开音频文件
if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) {
ALOGI("%s", "无法打开音频文件");
return NULL;
}
//获取输入文件信息
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
ALOGI("%s", "无法获取输入文件信息");
return NULL;
}
//获取音频流索引位置
int i = 0;
for (; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
audio_stream_idx = i;
break;
}
}
//获取解码器
codecCtx = pFormatCtx->streams[audio_stream_idx]->codec;
AVCodec *codec = avcodec_find_decoder(codecCtx->codec_id);
if (codec == NULL) {
ALOGI("%s", "无法获取解码器");
return NULL;
}
//打开解码器
if (avcodec_open2(codecCtx, codec, NULL) < 0) {
ALOGI("%s", "无法打开解码器");
return NULL;
}
//压缩数据
//解压缩数据
frame = av_frame_alloc();
//frame->16bit 44100 PCM 统一音频采样格式与采样率
swrCtx = swr_alloc();
//重采样设置参数-------------start
//输入的采样格式
enum AVSampleFormat in_sample_fmt = codecCtx->sample_fmt;
//输出采样格式16bit PCM
out_sample_fmt = AV_SAMPLE_FMT_S16;
//输入采样率
int in_sample_rate = codecCtx->sample_rate;
//输出采样率
int out_sample_rate = in_sample_rate;
//获取输入的声道布局
//根据声道个数获取默认的声道布局(2个声道,默认立体声stereo)
//av_get_default_channel_layout(codecCtx->channels);
uint64_t in_ch_layout = codecCtx->channel_layout;
//输出的声道布局(立体声)
uint64_t out_ch_layout = AV_CH_LAYOUT_MONO;
swr_alloc_set_opts(swrCtx,
out_ch_layout, out_sample_fmt, out_sample_rate,
in_ch_layout, in_sample_fmt, in_sample_rate,
0, NULL);
ALOGI("in_samplde_rate %d",in_sample_rate);
swr_init(swrCtx);
//输出的声道个数
out_channel_nb = av_get_channel_layout_nb_channels(out_ch_layout);
sprintf(info,"%d,%d",in_sample_rate,out_channel_nb);
6、得到解码后的pcm数据。
int AacPlayer::getpcmbuff(uint8_t* out_buffer){
//16bit 44100 PCM 数据
AVPacket packet;
av_init_packet(&packet);
int got_frame = 0, index = 0, ret;
int out_buffer_size = 0;
//不断读取压缩数据
if(av_read_frame(pFormatCtx, &packet) >= 0){
//解码音频类型的Packet
if (packet.stream_index == audio_stream_idx) {
//解码
ret = avcodec_decode_audio4(codecCtx, frame, &got_frame, &packet);
if (ret < 0) {
ALOGI("%s", "解码完成");
}
//解码一帧成功
if (got_frame > 0) {
ALOGI("解码:%d", index++);
swr_convert(swrCtx, &out_buffer, MAX_AUDIO_FRME_SIZE,
(const uint8_t **) frame->data, frame->nb_samples);
//获取sample的size
out_buffer_size = av_samples_get_buffer_size(NULL, out_channel_nb,
frame->nb_samples, out_sample_fmt,
1);
}
}
}else{
av_free_packet(&packet);
return -1;
}
av_free_packet(&packet);
return out_buffer_size;
}
这部分只贴出了关键的编解码的代码,使用c++编写aac编解码部分的代码,并通过jni接口向java层提供编解码方法,配合java中的AudioRecord和AudioTrack即可以实现录音和播放。
四、录音和播放的实现
这部分是使用AudioRecord和AudioTrack配合已经写好的native方法进行录音和播放的实现。
1、录音的流程
初始化AudioRecord和编码器—>采集pcm音频数据—>使用编码器进行编码—>将编码后的数据写入文件—>关闭AudioRecord和编码器
2、播放流程是两条线
初始化解码器加载文件—>解码文件读取解码后的pcm数据—>将解码后的数据加入播放缓冲区—>关闭解码器
初始化AudioTrack—>读取缓冲区中的pcm数据—>播放—>关闭AudioTrack
这一部分代码就不贴出来了,如果想看具体的实现可以到我的项目,里面有完整的aac音频录制和播放的demo。