文章目录
- 总体流程
- Android读取MP3文件
- 调用FFmpeg进行MP3文件解码
- AudioTrack播放PCM原理
- 工作原理
- 1. 缓冲区和流模式
- 2. 缓冲区管理
- 3. 音频渲染流程
- 4. 缓冲区大小和延迟
- 5. 线程和同步
- 使用示例
- 使用JNI调用AudioTrack播放PCM
- 1.通过JNI创建AudioTrack对象
- 2.调用AudioTrack的write方法,进行播放
- AudioTrack播放杂音爆音问题
- native层完整代码
总体流程
前面部分基本上都是FFmpeg基础—解码MP3文件,输出PCM交给播放,本文重点放在如何与Android交互上,
关于FFmpeg 部分可以看我之前的文章
音视频开发—FFmpeg打开麦克风,采集音频数据_ffmpeg 麦克风采集-CSDN博客
音视频开发—FFmpeg 音频重采样详解-CSDN博客
Android读取MP3文件
自从Android6之后,读写权限必须交给用户手动授权
首先在manifest 清单注册读写权限
注意:Android10 之后,只有读写权限,仍然不行,如果想要完全访问外部存储,必须是声明请求外部存储权限
android:requestLegacyExternalStorage=“true”
完整如下所示
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/><applicationandroid:allowBackup="true"android:dataExtractionRules="@xml/data_extraction_rules"android:fullBackupContent="@xml/backup_rules"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:requestLegacyExternalStorage="true"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.FirstJni"tools:targetApi="31"><activityandroid:name=".MainActivity"android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application>
动态请求用户权限,并回调 (MainActivity完整代码)
package com.marxist.firstjni;import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
import android.widget.TextView;
import android.widget.Toast;import com.marxist.firstjni.databinding.ActivityMainBinding;
import com.marxist.firstjni.player.MusicPlayer;import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;public class MainActivity extends AppCompatActivity {private static final int PERMISSION_REQUEST_CODE = 1000;private ActivityMainBinding binding;private MusicPlayer musicPlayer; //播放类private File musicRootFile = new File(Environment.getExternalStorageDirectory(), "backups/test.mp3"); //修改成你的MP3文件所在位置@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);binding = ActivityMainBinding.inflate(getLayoutInflater());setContentView(binding.getRoot());checkAndRequestPermissions();TextView tv = binding.sampleText;tv.setText("");musicPlayer = new MusicPlayer();musicPlayer.setDataSource(musicRootFile.getAbsolutePath()); //获取文件路径musicPlayer.play();}private void checkAndRequestPermissions() {if (ContextCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {// Should we show an explanation?if (ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)) {Toast.makeText(this, "The app needs storage permissions to write files.", Toast.LENGTH_LONG).show();}// Request the permission.ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},PERMISSION_REQUEST_CODE);} else {// Permission has already been grantedproceedWithFileOperations();}}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);if (requestCode == PERMISSION_REQUEST_CODE) {if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {proceedWithFileOperations();} else {Toast.makeText(this, "Permission Denied!", Toast.LENGTH_SHORT).show();}}}private void proceedWithFileOperations() {// Permission is granted, continue with file operationsToast.makeText(this, "Permission granted!", Toast.LENGTH_SHORT).show();}
}
注意修改成你想要调试的MP3文件位置,我这里是放在backups文件夹下
调用FFmpeg进行MP3文件解码
首先声明jni 播放接口,参数为文件路径
private native void nPlay(String url);
在native层实现C++ 代码,与传统的FFmpeg 解码方式一致,代码也一致,这里不过多赘述
唯一需要注意的是,AudioTrack对16位采样格式数据比较好,建议重采样为16位,目前MP3文件大多数格式为FLTP,32位浮点格式。
// 初始化重采样函数 输入原始帧, 输出swrFrame格式为 双声道,S16 ,44100
int initialize_resampler(SwrContext **swr_ctx, AVCodecContext *codec_ctx) {*swr_ctx = swr_alloc_set_opts(NULL, // ctxAV_CH_LAYOUT_STEREO, // 输出的channel 的布局AV_SAMPLE_FMT_S16, // 输出的采样格式44100, // 输出的采样率codec_ctx->channel_layout, // 输入的channel布局codec_ctx->sample_fmt, // 输入的采样格式codec_ctx->sample_rate, // 输入的采样率0, NULL);if (!(*swr_ctx)) {fprintf(stderr, "Could not allocate resampler context\n");return -1;}if (swr_init(*swr_ctx) < 0) {fprintf(stderr, "Could not initialize the resampling context\n");swr_free(swr_ctx);return -1;}return 0; // 成功
}//MP3 解码成 PCM 并交给AudioTrack 播放
extern "C"
JNIEXPORT void JNICALL
Java_com_marxist_firstjni_player_MusicPlayer_nPlay(JNIEnv *env, jobject thiz, jstring url) {// TODO: implement nPlay()const char *url_ = env->GetStringUTFChars(url, 0);avformat_network_init();AVFormatContext *pFormatContext = NULL;int formatOpenInputRes = 0;int formatFindStreamInfoRes = 0;int audioStramIndex = -1;AVCodecParameters *pCodecParameters;AVCodec *pCodec = NULL;AVCodecContext *pCodecContext = NULL;SwrContext *swrCtx = NULL;int codecParametersToContextRes = -1;int codecOpenRes = -1;AVPacket *pPacket = NULL;AVFrame *pFrame = NULL;jobject jAudioTrackObj;jmethodID jWriteMid;jclass jAudioTrackClass;//单通道最大存放转码数据 所占字节 = 采样率*量化格式 / 8int out_size = 44100 * 16 / 8;uint8_t *out = (uint8_t *) (av_malloc(out_size));formatOpenInputRes = avformat_open_input(&pFormatContext, url_, NULL, NULL);if (formatOpenInputRes != 0) {LOGE("format open input error: %s", av_err2str(formatOpenInputRes));goto __av_resources_destroy;}formatFindStreamInfoRes = avformat_find_stream_info(pFormatContext, NULL);if (formatFindStreamInfoRes < 0) {LOGE("format find stream info error: %s", av_err2str(formatFindStreamInfoRes));// 这种方式一般不推荐这么写,但是的确方便goto __av_resources_destroy;}// 查找音频流的 indexaudioStramIndex = av_find_best_stream(pFormatContext, AVMediaType::AVMEDIA_TYPE_AUDIO, -1, -1,NULL, 0);if (audioStramIndex < 0) {LOGE("format audio stream error: %s");goto __av_resources_destroy;}// 查找解码pCodecParameters = pFormatContext->streams[audioStramIndex]->codecpar;pCodec = avcodec_find_decoder(pCodecParameters->codec_id);if (pCodec == NULL) {LOGE("codec find audio decoder error");goto __av_resources_destroy;}// 打开解码器pCodecContext = avcodec_alloc_context3(pCodec);if (pCodecContext == NULL) {LOGE("codec alloc context error");goto __av_resources_destroy;}codecParametersToContextRes = avcodec_parameters_to_context(pCodecContext, pCodecParameters);if (codecParametersToContextRes < 0) {LOGE("codec parameters to context error: %s", av_err2str(codecParametersToContextRes));goto __av_resources_destroy;}codecOpenRes = avcodec_open2(pCodecContext, pCodec, NULL);if (codecOpenRes != 0) {LOGE("codec audio open error: %s", av_err2str(codecOpenRes));goto __av_resources_destroy;}//源MP3格式文件的采样格式为FLTP,转换为S16initialize_resampler(&swrCtx, pCodecContext);jAudioTrackClass = env->FindClass("android/media/AudioTrack");jWriteMid = env->GetMethodID(jAudioTrackClass, "write", "([BII)I");jAudioTrackObj = initCreateAudioTrack(env);pPacket = av_packet_alloc();pFrame = av_frame_alloc();while (av_read_frame(pFormatContext, pPacket) >= 0) {if (pPacket->stream_index == audioStramIndex) {// Packet 包,压缩的数据,解码成 pcm 数据int codecSendPacketRes = avcodec_send_packet(pCodecContext, pPacket);if (codecSendPacketRes == 0) {while (avcodec_receive_frame(pCodecContext, pFrame) == 0) {//直接将原始frame 帧数据转换为PCM 码流 交给AudioTrack来播放swr_convert(swrCtx, &out, out_size,(const uint8_t **) (pFrame->data), pFrame->nb_samples);int dataSize = av_samples_get_buffer_size(NULL, pFrame->channels,pFrame->nb_samples,AV_SAMPLE_FMT_S16, 0);LOGE("dataSize is %d",dataSize);// native 创建 c 数组jbyteArray array = env->NewByteArray(dataSize);env->SetByteArrayRegion(array, 0, dataSize, (const jbyte *) (out));env->CallIntMethod(jAudioTrackObj, jWriteMid, array, 0, dataSize);// 解除 jPcmDataArray 的持有,让 javaGC 回收env->DeleteLocalRef(array);}}}// 解引用av_packet_unref(pPacket);av_frame_unref(pFrame);}// 1. 解引用数据 data , 2. 销毁 pPacket 结构体内存 3. pPacket = NULLav_packet_free(&pPacket);av_frame_free(&pFrame);env->DeleteLocalRef(jAudioTrackObj);__av_resources_destroy:if (pCodecContext != NULL) {avcodec_close(pCodecContext);avcodec_free_context(&pCodecContext);pCodecContext = NULL;}if (pFormatContext != NULL) {avformat_close_input(&pFormatContext);avformat_free_context(pFormatContext);pFormatContext = NULL;}avformat_network_deinit();}
AudioTrack播放PCM原理
AudioTrack
是 Android 提供的一个低级音频播放接口,允许应用直接向音频硬件发送原始 PCM 音频数据流。AudioTrack
主要用于那些需要精确控制音频输出缓冲时序的场景,比如音乐播放器、音频效果应用和游戏音效等。
工作原理
1. 缓冲区和流模式
AudioTrack
通过一个或多个内部的音频缓冲区来管理音频数据。当你创建一个 AudioTrack
实例时,你可以选择其运行在静态模式或流模式:
- 静态模式:预先将全部音频数据加载到
AudioTrack
的内部缓冲区中,适用于播放短音频文件如音效。 - 流模式:动态地将音频数据“流”到
AudioTrack
,音频数据可以边播边加载,适用于播放长音频流如音乐。
2. 缓冲区管理
在流模式下,应用需要持续不断地向 AudioTrack
的缓冲区填充数据。AudioTrack
将这些数据提供给音频硬件进行播放。这个过程需要应用程序持续监控缓冲区状态,确保缓冲区始终有数据可供播放,避免音频播放出现间断。
3. 音频渲染流程
以下是 AudioTrack
渲染音频数据的大致流程:
- 初始化:通过构造函数创建
AudioTrack
实例,并设置相关参数,如采样率、通道配置、音频格式等。 - 填充数据:应用向
AudioTrack
缓冲区填充 PCM 数据。- 在静态模式下,通常在播放前一次性填充所有数据。
- 在流模式下,通过循环调用
write()
方法动态填充数据。
- 播放控制:调用
play()
方法开始播放。可以通过pause()
、stop()
和flush()
方法控制播放过程。 - 数据播放:音频硬件从
AudioTrack
缓冲区读取数据,并将其转换为模拟信号输出到扬声器。
4. 缓冲区大小和延迟
AudioTrack
的缓冲区大小直接影响音频播放的延迟。较大的缓冲区可以减少因缓冲区下溢而导致的音频中断,但会增加音频响应时间(延迟)。AudioTrack.getMinBufferSize()
方法提供了一个推荐的最小缓冲区大小,旨在平衡这两方面的需求。
5. 线程和同步
在使用 AudioTrack
时,音频播放通常应该在一个单独的线程中处理,以避免阻塞 UI 线程。此外,应用可能需要同步多个音频流,或者在音频播放过程中与视觉元素保持同步。
使用示例
下面是一个简单的使用 AudioTrack
播放 PCM 音频数据的示例代码(Java):
int sampleRate = 44100; // 采样率
int channelConfig = AudioFormat.CHANNEL_OUT_STEREO; // 立体声
int audioFormat = AudioFormat.ENCODING_PCM_16BIT; // 16位 PCM 数据
int bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,sampleRate,channelConfig,audioFormat,bufferSize,AudioTrack.MODE_STREAM);byte[] audioData = ...; // 假设这是已经解码的 PCM 数据audioTrack.play();
audioTrack.write(audioData, 0, audioData.length);
使用JNI调用AudioTrack播放PCM
调用AudioTrack的方式有多种,可以先在JAVA层创建好AudioTrack对象,将对象的引用传递给native层,也可以在native层 调用JAVA 直接创建AudioTrack,本文使用的是后者。
通过C++调用Java对象教程传送地址:Android 下C++调用Java 类中的方法详解-CSDN博客
1.通过JNI创建AudioTrack对象
// 在native 层直接new 一个Audio Track
jobject initCreateAudioTrack(JNIEnv *env) {jclass jAudioTrackClass = env->FindClass("android/media/AudioTrack");jmethodID jAudioTackCMid = env->GetMethodID(jAudioTrackClass, "<init>", "(IIIIII)V");int streamType = 3;int sampleRateInHz = 44100;int channelConfig = (0x4 | 0x8);int audioFormat = 2;int mode = 1;// int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)jmethodID getMinBufferSizeMid = env->GetStaticMethodID(jAudioTrackClass, "getMinBufferSize","(III)I");int bufferSizeInBytes = env->CallStaticIntMethod(jAudioTrackClass, getMinBufferSizeMid,sampleRateInHz, channelConfig, audioFormat);LOGE("bufferSizeInBytes = %d", bufferSizeInBytes);if (bufferSizeInBytes == -2) {LOGE("Invalid parameter !");}jobject jAudioTrackObj = env->NewObject(jAudioTrackClass, jAudioTackCMid, streamType,sampleRateInHz, channelConfig, audioFormat,bufferSizeInBytes, mode);// playjmethodID playMid = env->GetMethodID(jAudioTrackClass, "play", "()V");env->CallVoidMethod(jAudioTrackObj, playMid);return jAudioTrackObj;
}
代码分析: 与Java创建对象基本一致,分析AudioTrack Java源码
public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes, int mode)
创建AudioTrack对象,需要六个基本变量,分别是音频流类型,采样率,声道配置,采样格式,最小缓冲区大小,以及在播放模式
其中AudioTrack的音频流类型有以下几种:
/** Used to identify the volume of audio streams for phone calls */public static final int STREAM_VOICE_CALL = AudioSystem.STREAM_VOICE_CALL;/** Used to identify the volume of audio streams for system sounds */public static final int STREAM_SYSTEM = AudioSystem.STREAM_SYSTEM;/** Used to identify the volume of audio streams for the phone ring */public static final int STREAM_RING = AudioSystem.STREAM_RING;/** Used to identify the volume of audio streams for music playback */public static final int STREAM_MUSIC = AudioSystem.STREAM_MUSIC;/** Used to identify the volume of audio streams for alarms */public static final int STREAM_ALARM = AudioSystem.STREAM_ALARM;/** Used to identify the volume of audio streams for notifications */public static final int STREAM_NOTIFICATION = AudioSystem.STREAM_NOTIFICATION;
这里采用音乐类型 AudioSystem.STREAM_MUSIC;
对应的整数值为3
音频参数可以从解码器上下文获得,这里为了简化开发, 直接指定了S16采样格式,采样率为44100,声道为立体声(左右声道)
int sampleRate = 44100; // 采样率
int channelConfig = 12; // 立体声 AudioFormat.CHANNEL_OUT_STEREO =12 ;
int audioFormat = 2; // 16位 PCM 数据 AudioFormat.ENCODING_PCM_16BIT =2;
最小缓冲区可以通过getMinBufferSize
方法得到,因此可以直接通过jni调用AudioTrack对象的getMinBufferSize方法,传入的为音频三元素。
最后一个参数为播放模式,俩种模式,一种是静态模式,直接传入所有的PCM数据,一次性播放,另外一种是流模式,通过循环调用 write()
方法动态填充数据进行播放。
这里模式选择流模式,对应的整数值为1
public static final int MODE_STREAM = 1;
至此,AudioTrack对象就创建好了,并且要把AudioTrack对象的状态设置为播放,等待接收PCM数据。调用对象的paly方法。
在解码的时候调用write方法,往缓冲区写数据,调用底层硬件播放即可。
2.调用AudioTrack的write方法,进行播放
分析AudioTrack源码
public int write(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes) {return write(audioData, offsetInBytes, sizeInBytes, WRITE_BLOCKING);
}
需要传入的参数是字节数组,偏移量,以及数组大小。
swr_convert(swrCtx, &out, out_size,(const uint8_t **) (pFrame->data), pFrame->nb_samples);
通过重采样可以得到输出缓冲区的指针
out
:输出缓冲区的指针。
out_size
:输出缓冲区能够接收的最大样本数。
//单通道最大存放转码数据 所占字节 = 采样率*量化格式 / 8
int out_size = 44100 * 16 / 8;
uint8_t *out = (uint8_t *) (av_malloc(out_size));
通过av_samples_get_buffer_size可以得出一帧音频数据的大小,也就是数组大小。
得到以上参数,就可以调用write方法进行写入数据了
jbyteArray array = env->NewByteArray(dataSize);env->SetByteArrayRegion(array, 0, dataSize, (const jbyte *) (out));env->CallIntMethod(jAudioTrackObj, jWriteMid, array, 0, dataSize);// 解除 jPcmDataArray 的持有,让 javaGC 回收env->DeleteLocalRef(array);
至此,AudioTrack就能正常播放了
AudioTrack播放杂音爆音问题
1.检查是否为16位的采样格式, 有些机型不支持32位采样格式。
2.播放杂音的本质问题就是缓冲区内数据无法解析;检查write方法的字节数组是否正确,使用sws上下文转换,建议使用swr_convert函数,转换出的便是16位采样的格式数据指针,在native层更加方便操作数据。
native层完整代码
java层只是写了一个播放接口,本质还是通过C++来调用FFmpeg和AudioTrack的,只是Android播放音频的小Demo,没有画UI,因此不贴java代码了
#include <jni.h>
#include <string>
#include <iostream>
#include <android/log.h>#define LOG_TAG "NativeLib"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libavutil/log.h>
#include <libswresample/swresample.h>
}
// 错误处理和资源释放
#define CLEANUP(label) \do \{ \goto label; \} while (0)extern "C" JNIEXPORT jstring JNICALL
Java_com_marxist_firstjni_MainActivity_stringFromJNI(JNIEnv *env,jobject /* this */) {std::string hello = "Java 调用 C++ 静态注册方法";return env->NewStringUTF(hello.c_str());
}jstring getStringFromNative(JNIEnv *env, jobject /* this */) {std::string hello = "Java 调用 C++ 动态注册方法";return env->NewStringUTF(hello.c_str());
}jint getNumber(JNIEnv *env, jobject /* this */) {int number = 0;//第一步 通过env找到Java类jclass clazz = env->FindClass("com/marxist/firstjni/MyClass");if (!clazz) {std::cerr << "Failed to find class" << std::endl;return -1;}//第二步 获取实例方法ID 获取静态方法IDjmethodID instanceMethodID = env->GetMethodID(clazz, "instanceMethod", "(Ljava/lang/String;)V");if (!instanceMethodID) {std::cerr << "Failed to get method ID for instanceMethod" << std::endl;return -1;}jmethodID staticMethodID = env->GetStaticMethodID(clazz, "staticMethod", "(I)I");if (!staticMethodID) {std::cerr << "Failed to get method ID for staticMethodID" << std::endl;return -1;}//第三步 创建对象 有两种方式,1,立即申请对象并初始化 使用NewObject 必须要传入构造参数 2、alloc 分配内存,但先不初始化jstring message = env->NewStringUTF("Hello from myClass message");jobject myObj = env->AllocObject(clazz);//第四步 调用方法env->CallVoidMethod(myObj, instanceMethodID, message);//处理异常if (env->ExceptionCheck()) {env->ExceptionDescribe();env->ExceptionClear();}//调用静态方法传入的是类 而不是对象number = env->CallStaticIntMethod(clazz, staticMethodID, 42);// 处理异常if (env->ExceptionCheck()) {env->ExceptionDescribe();env->ExceptionClear();}// 返回结果return number;}// 动态注册表
static JNINativeMethod methods[] = {};//JVM启动的时候调用的函数
jint JNI_OnLoad(JavaVM *vm, void *reserved) {JNIEnv *env = NULL;if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {return JNI_ERR;}jclass clazz = env->FindClass("com/marxist/firstjni/MainActivity"); //调用native 方法的Java 类if (clazz == nullptr) {return JNI_ERR;}if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) {return JNI_ERR;}return JNI_VERSION_1_6;
}// 在native 层直接new 一个Audio Track
jobject initCreateAudioTrack(JNIEnv *env) {jclass jAudioTrackClass = env->FindClass("android/media/AudioTrack");jmethodID jAudioTackCMid = env->GetMethodID(jAudioTrackClass, "<init>", "(IIIIII)V");int streamType = 3;int sampleRateInHz = 44100;int channelConfig = (0x4 | 0x8);int audioFormat = 2;int mode = 1;// int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)jmethodID getMinBufferSizeMid = env->GetStaticMethodID(jAudioTrackClass, "getMinBufferSize","(III)I");int bufferSizeInBytes = env->CallStaticIntMethod(jAudioTrackClass, getMinBufferSizeMid,sampleRateInHz, channelConfig, audioFormat);LOGE("bufferSizeInBytes = %d", bufferSizeInBytes);if (bufferSizeInBytes == -2) {LOGE("Invalid parameter !");}jobject jAudioTrackObj = env->NewObject(jAudioTrackClass, jAudioTackCMid, streamType,sampleRateInHz, channelConfig, audioFormat,bufferSizeInBytes, mode);// playjmethodID playMid = env->GetMethodID(jAudioTrackClass, "play", "()V");env->CallVoidMethod(jAudioTrackObj, playMid);return jAudioTrackObj;
}// 初始化重采样函数 输入原始帧, 输出swrFrame格式为 双声道,S16 ,44100
int initialize_resampler(SwrContext **swr_ctx, AVCodecContext *codec_ctx) {*swr_ctx = swr_alloc_set_opts(NULL, // ctxAV_CH_LAYOUT_STEREO, // 输出的channel 的布局AV_SAMPLE_FMT_S16, // 输出的采样格式44100, // 输出的采样率codec_ctx->channel_layout, // 输入的channel布局codec_ctx->sample_fmt, // 输入的采样格式codec_ctx->sample_rate, // 输入的采样率0, NULL);if (!(*swr_ctx)) {fprintf(stderr, "Could not allocate resampler context\n");return -1;}if (swr_init(*swr_ctx) < 0) {fprintf(stderr, "Could not initialize the resampling context\n");swr_free(swr_ctx);return -1;}return 0; // 成功
}//MP3 解码成 PCM 并交给AudioTrack 播放
extern "C"
JNIEXPORT void JNICALL
Java_com_marxist_firstjni_player_MusicPlayer_nPlay(JNIEnv *env, jobject thiz, jstring url) {// TODO: implement nPlay()const char *url_ = env->GetStringUTFChars(url, 0);avformat_network_init();AVFormatContext *pFormatContext = NULL;int formatOpenInputRes = 0;int formatFindStreamInfoRes = 0;int audioStramIndex = -1;AVCodecParameters *pCodecParameters;AVCodec *pCodec = NULL;AVCodecContext *pCodecContext = NULL;SwrContext *swrCtx = NULL;int codecParametersToContextRes = -1;int codecOpenRes = -1;AVPacket *pPacket = NULL;AVFrame *pFrame = NULL;jobject jAudioTrackObj;jmethodID jWriteMid;jclass jAudioTrackClass;//单通道最大存放转码数据 所占字节 = 采样率*量化格式 / 8int out_size = 44100 * 16 / 8;uint8_t *out = (uint8_t *) (av_malloc(out_size));formatOpenInputRes = avformat_open_input(&pFormatContext, url_, NULL, NULL);if (formatOpenInputRes != 0) {LOGE("format open input error: %s", av_err2str(formatOpenInputRes));goto __av_resources_destroy;}formatFindStreamInfoRes = avformat_find_stream_info(pFormatContext, NULL);if (formatFindStreamInfoRes < 0) {LOGE("format find stream info error: %s", av_err2str(formatFindStreamInfoRes));// 这种方式一般不推荐这么写,但是的确方便goto __av_resources_destroy;}// 查找音频流的 indexaudioStramIndex = av_find_best_stream(pFormatContext, AVMediaType::AVMEDIA_TYPE_AUDIO, -1, -1,NULL, 0);if (audioStramIndex < 0) {LOGE("format audio stream error: %s");goto __av_resources_destroy;}// 查找解码pCodecParameters = pFormatContext->streams[audioStramIndex]->codecpar;pCodec = avcodec_find_decoder(pCodecParameters->codec_id);if (pCodec == NULL) {LOGE("codec find audio decoder error");goto __av_resources_destroy;}// 打开解码器pCodecContext = avcodec_alloc_context3(pCodec);if (pCodecContext == NULL) {LOGE("codec alloc context error");goto __av_resources_destroy;}codecParametersToContextRes = avcodec_parameters_to_context(pCodecContext, pCodecParameters);if (codecParametersToContextRes < 0) {LOGE("codec parameters to context error: %s", av_err2str(codecParametersToContextRes));goto __av_resources_destroy;}codecOpenRes = avcodec_open2(pCodecContext, pCodec, NULL);if (codecOpenRes != 0) {LOGE("codec audio open error: %s", av_err2str(codecOpenRes));goto __av_resources_destroy;}//源MP3格式文件的采样格式为FLTP,转换为S16initialize_resampler(&swrCtx, pCodecContext);jAudioTrackClass = env->FindClass("android/media/AudioTrack");jWriteMid = env->GetMethodID(jAudioTrackClass, "write", "([BII)I");jAudioTrackObj = initCreateAudioTrack(env);pPacket = av_packet_alloc();pFrame = av_frame_alloc();while (av_read_frame(pFormatContext, pPacket) >= 0) {if (pPacket->stream_index == audioStramIndex) {// Packet 包,压缩的数据,解码成 pcm 数据int codecSendPacketRes = avcodec_send_packet(pCodecContext, pPacket);if (codecSendPacketRes == 0) {while (avcodec_receive_frame(pCodecContext, pFrame) == 0) {//直接将原始frame 帧数据转换为PCM 码流 交给AudioTrack来播放swr_convert(swrCtx, &out, out_size,(const uint8_t **) (pFrame->data), pFrame->nb_samples);int dataSize = av_samples_get_buffer_size(NULL, pFrame->channels,pFrame->nb_samples,AV_SAMPLE_FMT_S16, 0);LOGE("dataSize is %d",dataSize);// native 创建 c 数组jbyteArray array = env->NewByteArray(dataSize);env->SetByteArrayRegion(array, 0, dataSize, (const jbyte *) (out));env->CallIntMethod(jAudioTrackObj, jWriteMid, array, 0, dataSize);// 解除 jPcmDataArray 的持有,让 javaGC 回收env->DeleteLocalRef(array);}}}// 解引用av_packet_unref(pPacket);av_frame_unref(pFrame);}// 1. 解引用数据 data , 2. 销毁 pPacket 结构体内存 3. pPacket = NULLav_packet_free(&pPacket);av_frame_free(&pFrame);env->DeleteLocalRef(jAudioTrackObj);__av_resources_destroy:if (pCodecContext != NULL) {avcodec_close(pCodecContext);avcodec_free_context(&pCodecContext);pCodecContext = NULL;}if (pFormatContext != NULL) {avformat_close_input(&pFormatContext);avformat_free_context(pFormatContext);pFormatContext = NULL;}avformat_network_deinit();
}