Android 使用FFmpeg 解码 MP3 文件并利用 AudioTrack 播放详细操作指南

文章目录

  • 总体流程
  • 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"/>
        <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();
    }
    

    作者:Trump. yang

    物联沃分享整理
    物联沃-IOTWORD物联网 » Android 使用FFmpeg 解码 MP3 文件并利用 AudioTrack 播放详细操作指南

    发表回复