自学内容网 自学内容网

Android调用FFmpeg解码MP3文件并使用AudioTrack播放操作详解

总体流程

在这里插入图片描述

前面部分基本上都是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"/>
    <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();
}

原文地址:https://blog.csdn.net/weixin_46999174/article/details/140490273

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!