其实之前很长一段时间都在研究音视频的知识,只是没有总结知识。后来太忙导致以前学的都忘了好多,最近买了音视频开发的书来系统学习,但是里头的部分代码是eclipse的,现在基本上不用eclipse开发了,所以我还是选择在Android Studio中进行实践,在此记录一下学习中的要点。代码均是参考https://github.com/zhanxiaokai,也就是该书的源码仓库。本文所用环境为Android Studio3.1.2,目前的最新稳定版。本文不会从编译开始讲起,只会提一些自己认为的关键代码,以及项目构建时候的一些坑,读者需要有一定ndk开发知识。
搭建Android Studio的c++项目
新建项目的时候选择包含c++项目
最后选上支持c++11特性、异常以及运行时类型识别
接下来我们在build.gralde中添加如下代码
apply plugin: 'com.android.application'
ext {
boolean isWindow = isWindowsOS();
if(isWindow){
native_sdk_path = rootDir.getAbsolutePath().replace('\\','/') + "/app"
}else{
native_sdk_path = file(rootDir.getAbsolutePath() + "/app")
}
}
/** 判断是否为Windows操作系统 */
boolean isWindowsOS(){
boolean isWindowsOS = false;
String osName = System.getProperty("os.name");
println("os.name=" + osName);
if(osName == null || "".equals(osName)) {
return false;
}
if(osName.toLowerCase().indexOf("windows") > -1){
isWindowsOS = true;
}
return isWindowsOS;
}
android {
...
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
ndk {
abiFilters 'armeabi-v7a'
}
buildTypes {
debug{
debuggable true
jniDebuggable true
externalNativeBuild {
cmake {
arguments '-DANDROID_PLATFORM=android-16', '-DANDROID_TOOLCHAIN=clang',
'-DANDROID_ARM_NEON=TRUE', '-DANDROID_STL=gnustl_static',
"-DPATH_TO_MEDIACORE:STRING=${native_sdk_path}"
cFlags '-O3', '-DGL_GLEXT_PROTOTYPES', '-DEGL_EGLEXT_PROTOTYPES', '-fsigned-char', "-I${native_sdk_path}", '-Wformat','-mfpu=neon', '-mfloat-abi=softfp -frtti' // full optimization, char data type is signed
// 编译优化,设置函式是否能被 inline 的伪指令长度
cppFlags '-O3', '-fexceptions', '-fsigned-char',
"-frtti -std=c++11", '-Wformat'
}
}
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
externalNativeBuild {
cmake {
arguments '-DANDROID_PLATFORM=android-16', '-DANDROID_TOOLCHAIN=clang',
'-DANDROID_ARM_NEON=TRUE', '-DANDROID_STL=gnustl_static',
'-DCMAKE_BUILD_TYPE=RelWithDebInfo',
"-DPATH_TO_MEDIACORE:STRING=${native_sdk_path}"
cFlags '-O3', '-DGL_GLEXT_PROTOTYPES', '-DEGL_EGLEXT_PROTOTYPES', '-fsigned-char', "-I${native_sdk_path}", '-Wformat','-mfpu=neon', '-mfloat-abi=softfp -frtti' // full optimization, char data type is signed
// 编译优化,设置函式是否能被 inline 的伪指令长度
cppFlags '-O3', '-fexceptions', '-fsigned-char',
"-frtti -std=c++11", '-Wformat'
}
}
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
}
简单说一下配置,这里我们需要将根app目录添加到cmake的cache中方便获取,arguments 中的-D就是在cmake构建的时候生成的CMakeCache.txt中定义的变量,可以直接在camke脚本中获取。这里的比如-DANDROID_PLATFORM定义是内部特殊属性,可以参考官网https://developer.android.com/ndk/guides/cmake。然后下面是一些c和cxx的编译参数,大部分是一些优化选项,具体可以参考我的这篇文章https://blog.csdn.net/a568478312/article/details/79195218。接着我们指定了主要CMakeLists.txt的路径,我这里直接将它放到了cpp目录下,如果有多个目录也是用cmake,单独一个放在cpp外面感觉有点怪。最后将jni的lib目录指定在了根目录的libs目录下。build.gradle添加的配置就这些了。
CMakeLists.txt配置
刚才说道我将原本项目的CMakeLists.txt移动到了cpp目录下,那么我们cpp目录下的外层文件作为与java层的交互使用,再新建一个子目录来包含ffmpeg的核心功能。我们首先把ffmpeg的静态库以及头文件放入libs目录中。
然后在cpp目录下新建子目录以及cpp文件。
Mp3Decoder.cpp 作为与java层交互的文件。libffmpeg_decoder包含ffmpeg的核心功能。common中包含日志以及文件操作等通用功能。我们先看外层的CMakeLists.txt。
cmake_minimum_required(VERSION 3.4.1)
#这个写在子目录总是提示找不到头文件,只能写在这里了
include_directories(${PATH_TO_MEDIACORE}/libs/ffmpeg/include)# 包含.h文件include目录
add_subdirectory(./libffmpeg_decoder)#包含子目录
add_library( native-lib
SHARED
Mp3Decoder.cpp)
target_link_libraries( native-lib
log
z
libffmpeg_decoder
)
这里很简单,包含了一个子目录,以及一些Android的库,加上子目录的 libffmpeg_decoder。
libffmpeg_decoder下的CMakeLists.txt
#初始化变量
set(PATH_TO_PRE_BUILT ${PATH_TO_MEDIACORE}/libs/${ANDROID_ABI})
file(GLOB FFMPEG_DECODE_SOURCE "*.cpp")#包含当前目录的cpp文件
add_library( libffmpeg_decoder
STATIC
${FFMPEG_DECODE_SOURCE}
)
target_link_libraries( libffmpeg_decoder
#导入ffmpeg静态库,这样导入必须使用绝对路径,相对路径失效
${PATH_TO_PRE_BUILT}/libavfilter.a
${PATH_TO_PRE_BUILT}/libavformat.a
${PATH_TO_PRE_BUILT}/libavcodec.a
${PATH_TO_PRE_BUILT}/libpostproc.a
${PATH_TO_PRE_BUILT}/libswresample.a
${PATH_TO_PRE_BUILT}/libswscale.a
${PATH_TO_PRE_BUILT}/libavutil.a
${PATH_TO_PRE_BUILT}/libfdk-aac.a
${PATH_TO_PRE_BUILT}/libvo-aacenc.a
${PATH_TO_PRE_BUILT}/libx264.a
)
这样导入静态库不用写繁琐的add_library,set_target_properties,比较方便,但是相对路径总是报错,所以只能使用绝对路径了。
Native代码
Java层的代码本来不太想贴的,就简单贴一下吧。
MainActivity.java
public class MainActivity extends AppCompatActivity {
private static String TAG = "MainActivity";
/** 原始的文件路径 **/
private static String mp3FilePath = "/mnt/sdcard/131.mp3";
/** 解码后的PCM文件路径 **/
private static String pcmFilePath = "/mnt/sdcard/131.pcm";
private Button mp3_encoder_btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mp3_encoder_btn = (Button) findViewById(R.id.mp3_encoder_btn);
mp3_encoder_btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
long startTimeMills = System.currentTimeMillis();
Mp3Decoder decoder = new Mp3Decoder();
int ret = decoder.init(mp3FilePath, pcmFilePath);
if(ret >= 0) {
decoder.decode();
decoder.destroy();
} else {
Log.i(TAG, "Decoder Initialized Failed...");
}
int wasteTimeMills = (int)(System.currentTimeMillis() - startTimeMills);
Log.i(TAG, "Decode Mp3 Waste TimeMills : " + wasteTimeMills + "ms");
}
});
}
}
然后我们直接看核心代码。获取mp3以及pcm的输出路径之后。我们首先获取mp3文件的采样率和比特率。并且计算出每次需要读取的buffer大小。
void AccompanyDecoderController::Init(const char *accompanyPath, const char *pcmFilePath) {
//初始化两个decoder
AccompanyDecoder *tempDecoder = new AccompanyDecoder();
int accompanyMetaData[2];
tempDecoder->GetMusicMeta(accompanyPath, accompanyMetaData);
delete tempDecoder;
accompanySampleRate = accompanyMetaData[0];
int accompanyByteCountPerSec =
accompanySampleRate * CHANNEL_PER_FRAME * BITS_PER_CHANNEL / BITS_PER_BYTE;
accompanyPacketBufferSize = static_cast<int>(accompanyByteCountPerSec / 2 * 0.2);
accompanyDecoder = new AccompanyDecoder();
accompanyDecoder->Init(accompanyPath, accompanyPacketBufferSize);
pcmFile = fopen(pcmFilePath, "wb+");
}
接下来是初始化ffmpeg关键上下文
int AccompanyDecoder::Init(const char *audioFile) {
LOGI("enter AccompanyDecoder::init");
audioBuffer = nullptr;
position = -1.0f;
audioBufferCursor = 0;
audioBufferSize = 0;
swrContext = nullptr;
swrBuffer = nullptr;
swrBufferSize = 0;
seek_success_read_frame_success = true;
isNeedFirstFrameCorrectFlag = true;
firstFrameCorrectionInSecs = 0.0f;
av_register_all();
avFormatContext = avformat_alloc_context();
// 打开输入文件
LOGI("open accompany file %s....", audioFile);
if (nullptr == audioFile) {
int length = strlen(audioFile);
accompanyFilePath = new char[length + 1];
memset(accompanyFilePath, 0, length + 1);
memcpy(accompanyFilePath, audioFile, length + 1);
return -1;
}
int result = avformat_open_input(&avFormatContext, audioFile, nullptr, nullptr);
if (result != 0) {
LOGI("can't open file %s result is %d", audioFile, result);
return -1;
} else {
LOGI("open file %s success and result is %d", audioFile, result);
}
avFormatContext->max_analyze_duration = 50000;
//检查在文件中的流的信息
result = avformat_find_stream_info(avFormatContext, nullptr);
if (result < 0) {
LOGI("fail avformat_find_stream_info result is %d", result);
return -1;
} else {
LOGI("sucess avformat_find_stream_info result is %d", result);
}
stream_index = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
LOGI("stream_index is %d", stream_index);
//未找到音频
if (stream_index < 0) {
LOGI("no audio stream");
stream_index = 0;
return -1;
}
//拿到音频流
AVStream *audioStream = avFormatContext->streams[stream_index];
if (audioStream->time_base.den && audioStream->time_base.num) {
timeBase = av_q2d(audioStream->time_base);
} else if (audioStream->codec->time_base.den && audioStream->codec->time_base.num) {
timeBase = av_q2d(audioStream->codec->time_base);
}
//获得音频流的解码器上下文
avCodecContext = audioStream->codec;
// 根据解码器上下文找到解码器
LOGI("avCodecContext->codec_id is %d AV_CODEC_ID_AAC is %d", avCodecContext->codec_id,
AV_CODEC_ID_AAC);
AVCodec *avCodec = avcodec_find_decoder(avCodecContext->codec_id);
if (avCodec == nullptr) {
LOGI("Unsupported codec ");
return -1;
}
//打开解码器
result = avcodec_open2(avCodecContext, avCodec, nullptr);
if (result < 0) {
LOGI("fail avformat_find_stream_info result is %d", result);
return -1;
} else {
LOGI("sucess avformat_find_stream_info result is %d", result);
}
//4判断是否需要resampler
if (!AudioCodecISSupported()) {
LOGI("because of audio Codec Is Not Supported so we will init swresampler...");
/**
* 初始化resampler
* @param s Swr context, can be NULL
* @param out_ch_layout output channel layout (AV_CH_LAYOUT_*)
* @param out_sample_fmt output sample format (AV_SAMPLE_FMT_*).
* @param out_sample_rate output sample rate (frequency in Hz)
* @param in_ch_layout input channel layout (AV_CH_LAYOUT_*)
* @param in_sample_fmt input sample format (AV_SAMPLE_FMT_*).
* @param in_sample_rate input sample rate (frequency in Hz)
* @param log_offset logging level offset
* @param log_ctx parent logging context, can be NULL
*/
swrContext = swr_alloc_set_opts(nullptr,
av_get_default_channel_layout(OUT_PUT_CHANNELS),
AV_SAMPLE_FMT_S16,
avCodecContext->sample_rate,
avCodecContext->channel_layout,
avCodecContext->sample_fmt,
avCodecContext->sample_rate,
0,
nullptr
);
if (!swrContext || swr_init(swrContext)) {
if (swrContext) {
swr_free(&swrContext);
}
avcodec_close(avCodecContext);
LOGI("init resampler failed...");
return -1;
}
}
LOGI("channels is %d sampleRate is %d", avCodecContext->channels, avCodecContext->sample_rate);
pAudioFrame = avcodec_alloc_frame();
return 0;
}
这里只做解码,但是代码中还包含了后面几章需要的代码,比如seek。简单起见对于导出数据来说,可以不用管关于position的计算。扇面的代码基本上都是模板代码,具体就是打开对应的文件,获取流以及解码器等等。接下来是最关键的解码阶段。
void AccompanyDecoderController::Decode() {
while (true) {
AudioPacket *accompanyPacket = accompanyDecoder->DecodePacket();
if (-1 == accompanyPacket->size) {
break;
}
fwrite(accompanyPacket->buffer, sizeof(short), accompanyPacket->size, pcmFile);
}
}
我们不断读取数据并写入文件,完成后退出。再来看DecodePacket的代码。
AudioPacket *AccompanyDecoder::DecodePacket() {
short *samples = new short[packetBufferSize];
int stereoSampleSize = ReadSameple(samples, packetBufferSize);
AudioPacket *samplePacket = new AudioPacket();
if (stereoSampleSize > 0) {
//构造成一个packet
samplePacket->buffer = samples;
samplePacket->size = stereoSampleSize;
/** 这里由于每一个packet的大小不一样有可能是200ms 但是这样子position就有可能不准确了 **/
samplePacket->position = position;;
} else {
samplePacket->size = -1;
}
return samplePacket;
}
通过不断读取数据,并且包装来进行返回。再看ReadSameple
int AccompanyDecoder::ReadSameple(short *samples, int size) {
if (seek_req) {
audioBufferCursor = audioBufferSize;
}
int sampleSize = size;
while (size > 0) {
if (audioBufferCursor < audioBufferSize) {
int audioBufferDataSize = audioBufferSize - audioBufferCursor;
int copySize = MIN(size, audioBufferDataSize);
memcpy(samples + (sampleSize - size), audioBuffer + audioBufferCursor, copySize * 2);
size -= copySize;
audioBufferCursor += copySize;
} else {
if (ReadFrame() < 0) {
break;
}
}
}
int fillSize = sampleSize - size;
if (fillSize == 0) {
return -1;
}
return fillSize;
}
这里要稍微理解一下流程,首先sampleSize是初始化的short类型数组大小。audioBufferCursor作为解码出来的裸数据的游标,标记读取的位置。因为每次解码出来的数据可能不止一帧,可能数据会超过给定的samples数组的大小,需要分几次写入文件。所以使用audioBufferCursor来保存一下读取的位置,并且如果samples没有填充满的时候,会继续去解码数据,保证将samples填充完全并返回,直到文件末尾。最后查看ReadFrame()方法。
int AccompanyDecoder::ReadFrame() {
if (seek_req) {
this->Seek_frame();
}
int ret = 1;
av_init_packet(&packet);
int gotframe = 0;
int readFrameCode = -1;
while (true) {
readFrameCode = av_read_frame(avFormatContext, &packet);
if (readFrameCode >= 0) {
if (packet.stream_index == stream_index) {
int len = avcodec_decode_audio4(avCodecContext, pAudioFrame, &gotframe, &packet);
if (len < 0) {
LOGI("decode audio error, skip packet");
}
if (gotframe) {
//解码数据成功
int numChannels = OUT_PUT_CHANNELS;
int numFrames = 0;
void *audioData;
//需要转换数据
if (swrContext) {
//提供更多的转换输出空间
const int ratio = 2;
const int bufSize = av_samples_get_buffer_size(
nullptr,
numChannels,
pAudioFrame->nb_samples * ratio,
AV_SAMPLE_FMT_S16,
1
);
if (!swrBuffer || swrBufferSize < bufSize) {
swrBufferSize = bufSize;
swrBuffer = realloc(swrBuffer, swrBufferSize);
}
byte *outbuf[2] = {(byte *) swrBuffer, nullptr};
numFrames = swr_convert(swrContext,
outbuf,
pAudioFrame->nb_samples * ratio,
(const uint8_t **) pAudioFrame->data,
pAudioFrame->nb_samples
);
if (numFrames < 0) {
LOGI("fail resample audio");
ret = -1;
break;
}
audioData = swrBuffer;
} else {
if (avCodecContext->sample_fmt != AV_SAMPLE_FMT_S16) {
LOGI("bucheck, audio format is invalid");
ret = -1;
break;
}
audioData=pAudioFrame->data[0];
numFrames=pAudioFrame->nb_samples;
}
...
audioBufferSize=numFrames*numChannels;
audioBuffer=(short*)audioData;
audioBufferCursor=0;
break;
}
}
} else {
ret = -1;
break;
}
}
av_free_packet(&packet);
return ret;
}
这里的主要流程就是,将packet中的数据解码成pcm裸数据。并且如果格式不是我们需要的,进行转换,这里实际我们会将AV_SAMPLE_FMT_S16P转为AV_SAMPLE_FMT_S16。ffmpeg的两种打包模式packed和planar,可以看一下这篇文章http://www.voidcn.com/article/p-xqbotnas-bgz.html 。关于这里有部分时间矫正的代码,在以后几章会讲到的时候再说。最后提一下swrBuffer和swr_convert。 pAudioFrame->nb_samples指的是这个frame中每个声道的采样数,我们初始化swrBuffer是需要更大的空间,所以最后使用了两倍的大小。最后我们计算解码的裸数据大小,使用 采样数*声道数。一个采样数是两个字节,所以计算出来的大小就是short类型的数据大小。最后的最后不要忘了加上权限,6.0以上的系统请手动开启权限。
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
有疑问或者错误请在评论区提出。
源码:https://github.com/TYGitHubPersonal/ffmpegdecodePcm