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"/><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();
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/3246140.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

QT实现滑动页面组件,多页面动态切换

这篇文章主要介绍了Qt实现界面滑动切换效果&#xff0c;对大家的学习或工作具有一定的参考借鉴价值&#xff0c;需要的朋友可以参考下。 一、简述 一个基于Qt的动态滑动页面组件。 二、 设计思路 1、自定义StackWidget类&#xff0c;继承自QWidget&#xff0c;实现一个堆叠…

乐尚代驾项目概述

前言 2024年7月17日&#xff0c;最近终于在低效率的情况下把java及其生态的知识点背的差不多了&#xff0c;投了两个礼拜的简历&#xff0c;就一个面试&#xff0c;总结了几点原因。 市场环境不好 要知道&#xff0c;前两年找工作&#xff0c;都不需要投简历&#xff0c;把简历…

《绝区零》公测“翻车”

“《绝区零》重塑米哈游荣光”到“《绝区零》翻车米哈游没招了”,这样极与极的舆论反转,只用了不到一天的时间。 7月,米哈游自研游戏《绝区零》上线公测。虽然品类略显小众,主打动作手游,但系出名门的优势还是让《绝区零》在公测前预下载阶段大放异彩——直接登上了百余国…

Ubuntu/Kali简洁高效安装最新版的docker-compose

基于docker已安装的情况下&#xff0c;通过执行一下代码完成docker-compose的安装 sudo curl -L "https://github.com/docker/compose/releases/download/$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep \"tag_name\": | sed …

区块链资料

Quantstamp - Public Security Assessments smart-contract-sanctuary-bsc/contracts/mainnet at master tintinweb/smart-contract-sanctuary-bsc GitHub https://github.com/slowmist/Cryptocurrency-Security-Audit-Guide/blob/main/README_CN.md sFuzz: 高效自适应的智…

有赞群团团开团大团长跑路,用户资金保护并没有保障到位!

近日群团团的开团大团长“肥肥购物”已经跑路了&#xff0c;留下了一堆烂账。有的团长5月21号买的购物卡到现在还没有进行成功交付&#xff0c;据帮卖团长反馈该笔订单一直没有确认收货且处于未完结的状态&#xff0c;但是这笔订单货款钱却迟迟无法进行成功退款。 下方是帮卖团…

Windows搭建RTMP视频流服务器

参考了一篇文章&#xff0c;见文末。 博客中nginx下载地址失效&#xff0c;附上一个有效的地址&#xff1a; Index of /download/ 另外&#xff0c;在搭建过程中&#xff0c;遇到的问题总结如下&#xff1a; 1 两个压缩包下载解压并重命名后&#xff0c;需要 将nginx-rtmp…

若依微服务集成手机短信验证码登陆

为了响应公司项目的特定需求&#xff0c;增强用户体验与安全性&#xff0c;集成手机短信验证码登录功能至基于若依微服务框架开发的应用中&#xff0c;故创作此篇为未来类似项目提供了可借鉴的实施范例。 文章目录 1.设计思路2.发送手机验证码接口3.发送手机验证码接口4.post请…

云动态摘要 2024-07-16

给您带来云厂商的最新动态&#xff0c;最新产品资讯和最新优惠更新。 最新优惠与活动 数据库上云优选 阿里云 2024-07-04 RDS、PolarDB、Redis、MongoDB 全系产品新用户低至首年6折起&#xff01; [免费体验]智能助手ChatBI上线 腾讯云 2024-07-02 基于混元大模型打造&…

开放式耳机性价比推荐!免费总结功课给你参考!

在选择适合自己的耳机时&#xff0c;确实需要考虑多方面的因素&#xff0c;包括音质、舒适度、佩戴方式、续航能力、防水性能等。开放式耳机因其独特的设计&#xff0c;不仅能够提供良好的音质体验&#xff0c;还能让你在享受音乐的同时&#xff0c;保持对周围环境的感知&#…

AI数字人直播源码解析:灰豚私有化部署背后的技术分析

随着AI数字人技术的应用潜力不断显现&#xff0c;与AI数字人相关的多个项目逐渐成为创业者们的重点关注对象&#xff0c;作为当前AI数字人典型应用场景之一的数字人直播意向人数更是屡创新高&#xff0c;AI数字人直播源码部署的热度也因此不断飙升&#xff0c;与各大数字人源码…

python-区间内的真素数(赛氪OJ)

[题目描述] 找出正整数 M 和 N 之间&#xff08;N 不小于 M&#xff09;的所有真素数。真素数的定义&#xff1a;如果一个正整数 P 为素数&#xff0c;且其反序也为素数&#xff0c;那么 P 就为真素数。 例如&#xff0c;11&#xff0c;13 均为真素数&#xff0c;因为 11 的反序…

Python入门------pycharm加载虚拟环境

pycharm虚拟环境配置&#xff1a; 在按照前面的办法&#xff0c;配置好虚拟环境后,如果我们需要到虚拟环境开发&#xff0c;就需要给编译器配置虚拟环境 1.打开编译器&#xff0c;点击右下角的interpreter选项 2. 点击ADD Interpreter,添加虚拟环境 3. 因为我们使用的是原始…

Hadoop安装报错

报错&#xff1a;ERROR 2023-03-09 21:33:00,178 NetUtil.py:97 - SSLError: Failed to connect. Please check openssl library versions. 解决方案: 在安装失败得客户端执行 编辑 /etc/python/cert-verification.cfg 配置文件&#xff0c;将 [https] 节的 verify 项 设为禁用…

linux上Mysql的安装

1.先检查有没有安装mariadb&#xff0c;有的话将其卸载&#xff0c;不然会和mysql冲突 [rootweb1 ~]# yum list | grep mariadb 2.卸载mariadb,按Y确认 [rootweb1 ~]# yum remove mariadb-libs.x86_64 3.下载mysql [rootweb1 ~]# wget https://downloads.mysql.com/archi…

大模型训练数据白皮书

大模型训练数据白皮书 关键要点一、合成数据解决方案探讨二、ChatGPT的案例分析三、大模型训练所需数据及特点四、多模态、知识性和安全性五、中文大模型发展受限于中式价值观类语料短缺六、高质量数据的重要性及其对模型的影响七、三重不确定性和有效搭配八、从质量、规模、多…

Self-Attention 自注意力机制(二)——实例过程说明

一、自注意力机制核心过程 自注意力机制&#xff08;Self-Attention Mechanism&#xff09;&#xff0c;也称为内部注意力机制&#xff0c;是一种在序列模型中用于捕捉序列内部不同位置之间依赖关系的技术。这种机制允许模型在处理序列时&#xff0c;对序列中的每个元素分配不…

pytorch-pytorch之LSTM

目录 1. nn.LSTM2. nn.LSTMCell 1. nn.LSTM 初始化函数输入参数与RNN相同&#xff0c;分别是input_size&#xff0c;hidden_size和num_layer foward函数也与RNN类似&#xff0c;只不过返回值除了out外&#xff0c;ht变为(ht,ct) 代码见下图&#xff1a; 2. nn.LSTMCell 初…

SAP ABAP性能优化

1.前言 ABAP作为SAP的专用的开发语言&#xff0c;衡量其性能的指标主要有以下两个方面&#xff1a; 响应时间&#xff1a;对于某项特定的业务请求&#xff0c;系统在收到请求后需要多久返回结果 吞吐量&#xff1a;在给定的时间能&#xff0c;系统能够处理的数据量 2. ABAP语…

React工程化笔记

脚手架可以帮助我们快速的搭建一个项目结构&#xff0c;在我们之前学习 webpack 的过程中&#xff0c;每次都需要配置 webpack.config.js 文件&#xff0c;用于配置我们项目的相关 loader 、plugin&#xff0c;这些操作比较复杂&#xff0c;但是它的重复性很高&#xff0c;而且…