文章目录
总体流程
前面部分基本上都是FFmpeg基础—解码MP3文件,输出PCM交给播放,本文重点放在如何与Android交互上,
关于FFmpeg 部分可以看我之前的文章
音视频开发—FFmpeg打开麦克风,采集音频数据_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"/>
<application
android: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">
<activity
android: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文件所在位置
@Override
protected 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 granted
proceedWithFileOperations();
}
}
@Override
public 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 operations
Toast.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, // ctx
AV_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;
//单通道最大存放转码数据 所占字节 = 采样率*量化格式 / 8
int 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;
}
// 查找音频流的 index
audioStramIndex = 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,转换为S16
initialize_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 = NULL
av_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);
// play
jmethodID 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 获取静态方法ID
jmethodID 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);
// play
jmethodID 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, // ctx
AV_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;
//单通道最大存放转码数据 所占字节 = 采样率*量化格式 / 8
int 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;
}
// 查找音频流的 index
audioStramIndex = 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,转换为S16
initialize_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 = NULL
av_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();
}