Android OpenGL ES 十四.ffmpeg+opensl es音频渲染

介绍

一. 声音的物理性质

声音是波

说到声音我相信只要听力正常的人都听见过声音,那么声音是如何产生的呢?记得初中物理课本上的描述 - 声音是由物体的振动而产生的。其实声音是一种压力波,当敲打某个物体或演奏某个乐器时,它们的振动都会引起空气有节奏的振动,使周围的空气产生疏密变化,形成疏密相间的纵波,由此就产生了声波,这种现象会一直延续到振动消失为止。

  • 声波的三要素
    声波的三要素是频率、振幅、和波形,频率代表音阶的高低,振幅代表响度,波形代表音色。

  • 声音的传播介质
    声音的传播介质很广,它可以通过空气、液体和固体进行传播;而且介质不同,传播的速度也不同,比如声音在空气中的传播速度为 340m/s , 在蒸馏水中的传播速度为 1497 m/s , 而在铁棒中的传播速度则可以高达 5200 m/s ;不过,声音在真空中时无法传播的。

  • 回声
    当我们在高山或者空旷地带高声大喊的时候,经常会听到回声,之所以会有回声是因为声音在传播过程中遇到障碍物会反弹回来,再次被我们听到。
    但是,若两种声音传到我们的耳朵里的时差小于 80 毫秒,我们就无法区分开这两种声音了,其实在日常生活中,人耳也在收集回声,只不过由于嘈杂的外接环境以及回声的分贝比较低,所以我们的耳朵分辨不出这样的声音,或者说是大脑能接收到但分辨不出。
  • 共鸣
    自然界中有光能,水能,生活中有机械能,电能,其实声音也可以产生能量,例如两个频率相同的物体,敲打其中一个物体时另一个物体也会振动发生。这种现象称为共鸣,共鸣证明了声音传播可以带动另一个物体振动,也就是说,声音的传播过程也是一种能量的传播过程。

数字音频

首先要对模拟信号进行采样,所谓采样就是在时间轴上对信号进行数字化。根据奈奎斯特定理(也称采样定理),按比声音最高频率高 2 倍以上的频率对声音进行采样,对于高质量的音频信号,其频率范围在 20Hz ~ 20kHz ,所以采样频率一般为 44.1kHz ,这样就保证采样声音达到 20kHz 也能被数字化,从而使得经过数字化处理之后,人耳听到的声音质量不会被降低。而所谓的 44.1 kHz 就是代表 1 s 会采样 44100 次
那么,具体的每个采样又该如何表示呢?这就涉及到将要讲解的第二个概念: 量化。量化是指在幅度轴上对信号进行数字化,比如用16 bit 的二进制信号来表示声音的一个采样,而 16 bit 所表示的范围是 [-32768 , 32767] , 共有 65536 个可能取值,因此最终模拟的音频信号在幅度上也分为了 65536 层。
既然每一个分量都是一个采样,那么这么多的采样该如何进行存储呢?这就涉及将要讲解的第三个概念: 编码。所谓编码,就是按照一定的格式记录采样和量化后的数字数据,比如顺序存储或压缩存储等等
这里涉及了很多中格式,通常所说的音频的裸数据就是 PCM (Pulse Code Modulation) 数据。描述一段 PCM 数据一般需要以下几个概念:量化格式(sampleFormat)、采样率(sampleRate)、声道数 (channel) 。以 CD 的音质为例:量化格式为 16 bit (2 byte),采样率 44100 ,声道数为 2 ,这些信息就描述了 CD 的音质。而对于声音的格式,还有一个概念用来描述它的大小,称为数据比特率,即 1s 时间内的比特数目,它用于衡量音频数据单位时间内的容量大小。而对于 CD 音质的数据,比特率为多少呢? 计算如下:
44100 * 16 * 2 = 1378.125 kbps
复制代码那么在一分钟里,这类 CD 音质的数据需要占据多大的存储空间呢?计算如下:
1378.125 * 60 / 8 / 1024 = 10.09 MB
复制代码当然,如果 sampleFormat 更加精确 (比如用 4 个字节来描述一个采样),或者 sampleRate 更加密集 (比如 48kHz 的采样率), 那么所占的存储空间就会更大,同时能够描述的声音细节就会越精确。存储的这段二进制数据即表示将模拟信号转为数字信号了,以后就可以对这段二进制数据进行存储,播放,复制,或者进行其它操作。
音频编码
上面提到了 CD 音质的数据采样格式,曾计算出每分钟需要的存储空间约为 10.09 MB ,如果仅仅是将其存储在光盘或者硬盘中,可能是可以接受的,但是若要在网络中实时在线传输的话,那么这个数据量可能就太大了,所以必须对其进行压缩编码。压缩编码的基本指标之一就是压缩比,压缩比通常小于 1 。压缩算法包括有损压缩和无损压缩。无损压缩是指解压后的数据可以完全复原在常用的压缩格式中,用的较多的是有损压缩,有损压缩是指解压后的数据不能完全恢复,会丢失一部分信息,压缩比越小,丢失的信息就比越多,信号还原后的失真就会越大。根据不同的应用场景 (包括存储设备、传输网络环境、播放设备等),可以选用不同的压缩编码算法,如 PCM 、WAV、AAC 、MP3 、Ogg 等。

  • WAV 编码
    WAV 编码就是在 PCM 数据格式的前面加了 44 个字节,分别用来存储 PCM 的采样率、声道数、数据格式等信息。
    特点: 音质好,大量软件支持。
    场景: 多媒体开发的中间文件、保存音乐和音效素材。
  • MP3 编码
    MP3 具有不错的压缩比,使用 LAME 编码 (MP3 编码格式的一种实现)的中高码率的 MP3 文件,听感上非常接近源 WAV 文件,当然在不同的应用场景下,应该调整合适的参数以达到最好的效果。
    特点: 音质在 128 Kbit/s 以上表现还不错,压缩比比较高,大量软件和硬件都支持,兼容性好。
    场景: 高比特率下对兼容性有要求的音乐欣赏。
  • AAC 编码
    AAC 是新一代的音频有损压缩技术,它通过一些附加的编码技术(比如 PS 、SBR) 等,衍生出了 LC-AAC 、HE-AAC 、HE-AAC v2 三种主要的编码格式。LC-AAC 是比较传统的 AAC ,相对而言,其主要应用于中高码率场景的编码 (>=80Kbit/s) ; HE-AAC 相当于 AAC + SBR 主要应用于中低码率的编码 (<= 80Kbit/s); 而新推出的 HE-AAC v2 相当于 AAC + SBR + PS 主要用于低码率场景的编码 (<= 48Kbit/s) 。事实上大部分编码器都设置为 <= 48Kbit/s 自动启用 PS 技术,而 > 48Kbit/s 则不加 PS ,相当于普通的 HE-AAC。
    特点: 在小于 128Kbit/s 的码率下表现优异,并且多用于视频中的音频编码。
    场景: 128 Kbit/s 以下的音频编码,多用于视频中音频轨的编码。
  • Ogg 编码
    Ogg 是一种非常有潜力的编码,在各种码率下都有比较优秀的表现,尤其是在中低码率场景下。Ogg 除了音质好之外,还是完全免费的,这为 Ogg 获得更多的支持打好了基础,Ogg 有着非常出色的算法,可以用更小的码率达到更好的音质,128 Kbit/s 的 Ogg 比 192kbit/s 甚至更高码率的 MP3 还要出色。但是目前因为还没有媒体服务软件的支持,因此基于 Ogg 的数字广播还无法实现。Ogg 目前受支持的情况还不够好,无论是软件上的还是硬件上的支持,都无法和 MP3 相提并论。
    特点: 可以用比 MP3 更小的码率实现比 MP3 更好的音质,高中低码率下均有良好的表现,兼容性不够好,流媒体特性不支持。
    场景: 语言聊天的音频消息场景。

二. OpengGL ES

官网OpenSL ES Overview - The Khronos Group Inc

OpenSL ES 全称(Open Sound Library for Embedded System) ,即嵌入式音频加速标准。OpenSL ES 是无授权费、跨平台、针对嵌入式系统精心优化的硬件音频加速 API ,它能为嵌入式移动多媒体设备上的本地应用程序开发者提供了标准化、高性能、低响应时间的音频功能实现方法,同时还实现了软/硬音频性能的直接跨平台部署,不仅降低了执行难度,而且还促进了高级音频市场的发展。


上图描述了 OpenSL ES 的架构,在 Android 中,High Level Audio Libs 是音频 Java 层 API 输入输出,属于高级 API , 相对来说,OpenSL ES 则是比价低层级的 API, 属于 C 语言 API 。在开发中,一般会直接使用高级 API , 除非遇到性能瓶颈,如语音实时聊天、3D Audio 、某些 Effects 等,开发者可以直接通过 C/C++ 开发基于 OpenSL ES 音频的应用。

标准 OpenSL ES 头文件 <SLES/OpenSLES.h> 和 <SLES/OpenSLES_Platform.h> 允许音频输入和输出。<SLES/OpenSLES_Android.h> 和 <SLES/OpenSLES_AndroidConfiguration.h> 中提供了其他 Android 专用功能。

如何在Android中使用OpenSL ES

  • 在CMakeList.txt中添加OpenSL ES的引用
// 单独加入OpenSLES
target_link_libraries( # Specifies the target library.

        OpenSLES

        )

  • 添加必要的头文件
// 这是标准的OpenSL ES库
#include <SLES/OpenSLES.h>
// 这里是针对安卓的扩展,如果要垮平台则需要注意
#include <SLES/OpenSLES_Android.h>

代码示例

GitHub 页面提供了以下示例应用:

  • audio-echo 创建从输入到输出的往返循环。
  • native-audio 是一个简单的音频录制器/播放器。

OpenSL ES简单使用

  • 使用OpenSL相关API的通用步骤是:

1,创建对象(通过带有create的函数)
2,初始化(通过Realize函数)
3,获取接口来使用相关功能(通过GetInterface函数)

  • OpenSL使用回调机制来访问音频IO,回调方法仅仅是告诉我们:BufferQueue已经就绪,可以接受/获取数据了

  • OpenSL使用SLBufferQueueItf. Enqueue函数从(往)音频设备获取(放入)数据。

关于使用FFmpeg + opensl 实现Android播放器播放音频, 可以在回调函数里面获取解码后的音频数据,并调用Enqueue函数进行播放. 在开始播放的时候,需要手动启动回调机制,否则回调将不会被调用到。

  • 一个简单的音频播放大概需要以下的object:
   //引擎
    SLObjectItf engineObject = 0;
    //引擎接口
    SLEngineItf engineInterface = 0;
    //混音器
    SLObjectItf outputMixObject = 0;
    //播放器
    SLObjectItf bqPlayerObject = 0;
    //播放器接口
    SLPlayItf bqPlayerPlay = 0;
    //播放器队列接口
    SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue = 0;

  • 基本流程

    image

OpenSLES部分代码,有固定流程,在android native video有该示例

void AudioChannel::audio_play() {
    /**
     * 1、创建引擎并获取引擎接口
     */
    SLresult result;
    // 1.1 创建引擎对象:SLObjectItf engineObject
    result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    // 1.2 初始化引擎
    result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    // 1.3 获取引擎接口 SLEngineItf engineInterface
    result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineInterface);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    /**
     * 2、设置混音器
     */
    // 2.1 创建混音器:SLObjectItf outputMixObject
    result = (*engineInterface)->CreateOutputMix(engineInterface, &outputMixObject, 0,
                                                 0, 0);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    // 2.2 初始化混音器
    result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    /**
     * 3、创建播放器
     */
    //3.1 配置输入声音信息
    //创建buffer缓冲类型的队列 2个队列
    SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
                                                       2};
    //pcm数据格式
    //SL_DATAFORMAT_PCM:数据格式为pcm格式
    //2:双声道
    //SL_SAMPLINGRATE_44_1:采样率为44100
    //SL_PCMSAMPLEFORMAT_FIXED_16:采样格式为16bit
    //SL_PCMSAMPLEFORMAT_FIXED_16:数据大小为16bit
    //SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT:左右声道(双声道)
    //SL_BYTEORDER_LITTLEENDIAN:小端模式
    SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, 2, SL_SAMPLINGRATE_44_1,
                                   SL_PCMSAMPLEFORMAT_FIXED_16,
                                   SL_PCMSAMPLEFORMAT_FIXED_16,
                                   SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
                                   SL_BYTEORDER_LITTLEENDIAN};

    //数据源 将上述配置信息放到这个数据源中
    SLDataSource audioSrc = {&loc_bufq, &format_pcm};

    //3.2 配置音轨(输出)
    //设置混音器
    SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
    SLDataSink audioSnk = {&loc_outmix, NULL};
    //需要的接口 操作队列的接口
    const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};
    const SLboolean req[1] = {SL_BOOLEAN_TRUE};
    //3.3 创建播放器
    result = (*engineInterface)->CreateAudioPlayer(engineInterface, &bqPlayerObject, &audioSrc,
                                                   &audioSnk, 1, ids, req);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    //3.4 初始化播放器:SLObjectItf bqPlayerObject
    result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    //3.5 获取播放器接口:SLPlayItf bqPlayerPlay
    result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    /**
     * 4、设置播放回调函数
     */
    //4.1 获取播放器队列接口:SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue
    (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, &bqPlayerBufferQueue);

    //4.2 设置回调 void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
    (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, playerCallback, this);

    /**
     * 5、设置播放器状态为播放状态
     */
    (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);

    /**
     * 6、手动激活回调函数
     */
    bqPlayerCallback(bqPlayerBufferQueue, this);
}

// 该方法会重复调用
void playerCallback(SLAndroidSimpleBufferQueueItf caller, void *pContext) {
    DZAudio *pAudio = (DZAudio *) pContext;
   
    int dataSize = pAudio->resampleAudio();

    // 对接mediacodec录制视频添加3
    pAudio->pJniCall->callCallbackPcm(THREAD_CHILD,pAudio->resampleOutBuffer,dataSize);
    // 解码拿到的byte[]数据放入播放器中
    (*caller)->Enqueue(caller, pAudio->resampleOutBuffer, dataSize);
}

三、整体代码

整体需要以下步骤:

  • 初始化播放器,java层
  • 播放器prepare()或者prepareAsync()
  • 播放器播放play()
  • 播放器停止stop()

1. 编写java层调用代码

    mPlayer = new DarrenPlayer();
    mPlayer.setDataSource(mMusicFile.getAbsolutePath());

    mPlayer.setOnErrorListener(new MediaErrorListener() {
        @Override
        public void onError(int code, String msg) {
            Log.e("TAG", "error code: " + code);
            Log.e("TAG", "error msg: " + msg);
            // Java 的逻辑代码
        }
    });
    mPlayer.setMediaInfoListener(new MediaInfoListener() {
        @Override
        public void musicInfo(int sampleRate, int channel) {
        }
        @Override
        public void callBackPcm(byte[] pcmData, int size) {
        }
    });

    mPlayer.setOnPreparedListener(new MediaPreparedListener() {
        @Override
        public void onPrepared() {
            Log.e("TAG", "准备完毕");
            mPlayer.play();
        }
    });
    mPlayer.prepareAsync();

2. 结合CMakeList,加入了OpenSLES和ffmpeg,OpenSLES用于播放,ffmpeg用于解码和重采样等

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)

# 需要引入我们头文件,以这个配置的目录为基准
include_directories(src/main/jniLibs/include)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

# 添加共享库搜索路径
LINK_DIRECTORIES(${CMAKE_SOURCE_DIR}/src/main/jniLibs/arm64-v8a)

# 指定源文件目录
AUX_SOURCE_DIRECTORY(${CMAKE_SOURCE_DIR}/src/main/cpp SRC_LIST)
# FILE(GLOB SRC_LIST "src/main/cpp/*.cpp")

add_library(
        # Sets the name of the library.
        music-player
        # Sets the library as a shared library.
        SHARED
        # Provides a relative path to your source file(s).
        src/main/cpp/DZJniCall.cpp
        src/main/cpp/DZFFmpeg.cpp
        src/main/cpp/DZAudio.cpp
        src/main/cpp/DZPacketQueue.cpp
        src/main/cpp/music-player.cpp
        src/main/cpp/DZPlayerStatus.cpp
        ${SRC_LIST}
)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        # 链接额外的 ffmpeg 的编译
        music-player
        # 编解码(最重要的库)
        avcodec
        # 滤镜特效处理库
        avfilter
        # 封装格式处理库
        avformat
        # 工具库(大部分库都需要这个库的支持)
        avutil
        # 后期处理
        postproc
        # 音频采样数据格式转换库
        swresample
        # 视频像素数据格式转换
        swscale
        # 链接 android ndk 自带的一些库
        android
        # 链接 OpenSLES
        OpenSLES
        # Links the target library to the log library
        # included in the NDK.
        log)

需要放入编译好的fffmpeg源码,这里使用arm64-v8a的,为了兼容更多的版本,可以谨慎点自己编译选择armeabi-v7a的。

QQ截图20211002140048.jpg

至于v7a还是v8a,如下可以得知,基本上从2015年2016年后,占据了手机的绝对主流市场

红米note3分为全网通和双网通两个版本。双网通版本使用联发科x10这款芯片,
采用8核A53,属于ARMv8-A,是64位CPU。全网通版本采用高通晓龙650,2xA72+4
xA53的六核架构,于2015年初发布的A72核心同样属于ARMv8-A架构,故这款CPU也是同样ARMv8-A架

3. prepare分析

3.1. 做DZFFmpeg和DZJNICall的初始化

  • DZFFmpeg:ffmpeg的封装调用
  • DZJNICall:会调java的代码,如播放器准备好了,及后期的音频pcm数据回调出去做音频和视频的录制等。
extern "C"
JNIEXPORT void JNICALL
Java_com_darren_media_DarrenPlayer_nPrepareAsync(JNIEnv *env, jobject instance, jstring url_) {
    const char *url = env->GetStringUTFChars(url_, 0);
    LOGE("nPrepareAsync");
    if (pFFmpeg == NULL) {
        pJniCall = new DZJNICall(pJavaVM, env, instance);
        pFFmpeg = new DZFFmpeg(pJniCall, url);
        pFFmpeg->prepareAsync();
    }
    env->ReleaseStringUTFChars(url_, url);
}

这里会调用prepareAsync(),开一个线程来初始化ffmpeg,并且找到音频流索引,接下来的活给到Audio,因为后期需要Video部分接进来,做下分层

//DZFFmpeg.cpp
DZFFmpeg::DZFFmpeg(DZJNICall *pJniCall, const char *url) {
    this->pJniCall = pJniCall;
    // 赋值一份 url ,因为怕外面方法结束销毁了 url
    this->url = (char *) malloc(strlen(url) + 1);
    memcpy(this->url, url, strlen(url) + 1);
}

void DZFFmpeg::prepare() {
    prepare(THREAD_MAIN);
}

void *threadPrepare(void *context) {
    DZFFmpeg *pFFmpeg = (DZFFmpeg *) context;
    pFFmpeg->prepare(THREAD_CHILD);
    return 0;
}

void DZFFmpeg::prepareAsync() {
    // 创建一个线程去播放,多线程编解码边播放
    pthread_t prepareThreadT;
    // threadPrepare相当于thread run的方法
    pthread_create(&prepareThreadT, NULL, threadPrepare, this);
    // 解除prepareThreadT,这里thread结束
    pthread_detach(prepareThreadT);
}

void DZFFmpeg::prepare(ThreadMode threadMode) {
    // 讲的理念的东西,千万要注意
    av_register_all();
    avformat_network_init();
    int formatOpenInputRes = 0;
    int formatFindStreamInfoRes = 0;

    formatOpenInputRes = avformat_open_input(&pFormatContext, url, NULL, NULL);
    if (formatOpenInputRes != 0) {
        // 第一件事,需要回调给 Java 层
        // 第二件事,需要释放资源
        LOGE("format open input error: %s", av_err2str(formatOpenInputRes));
        callPlayerJniError(threadMode, formatOpenInputRes, av_err2str(formatOpenInputRes));
        return;
    }

    formatFindStreamInfoRes = avformat_find_stream_info(pFormatContext, NULL);
    if (formatFindStreamInfoRes < 0) {
        LOGE("format find stream info error: %s", av_err2str(formatFindStreamInfoRes));
        // 这种方式一般不推荐这么写,但是的确方便
        callPlayerJniError(threadMode, formatFindStreamInfoRes,
                av_err2str(formatFindStreamInfoRes));
        return;
    }

    // 查找音频流的 index
    int audioStramIndex = av_find_best_stream(pFormatContext, AVMediaType::AVMEDIA_TYPE_AUDIO, -1,
            -1,
            NULL, 0);
    if (audioStramIndex < 0) {
        LOGE("format audio stream error.");
        // 这种方式一般不推荐这么写,但是的确方便
        callPlayerJniError(threadMode, FIND_STREAM_ERROR_CODE, "format audio stream error");
        return;
    }

    // 不是我的事我不干,但是大家也不要想得过于复杂
    pAudio = new DZAudio(audioStramIndex, pJniCall, pFormatContext);
    pAudio->analysisStream(threadMode, pFormatContext->streams);

    // ---------- 重采样 end ----------
    // 回调到 Java 告诉他准备好了
    pJniCall->callPlayerPrepared(threadMode);
}
// DZAudio.cpp
void DZAudio::analysisStream(ThreadMode threadMode, AVStream **streams) {
    // 查找解码
    AVCodecParameters *pCodecParameters = pFormatContext->streams[audioStreamIndex]->codecpar;
    AVCodec *pCodec = avcodec_find_decoder(pCodecParameters->codec_id);
    if (pCodec == NULL) {
        LOGE("codec find audio decoder error");
        callPlayerJniError(threadMode, CODEC_FIND_DECODER_ERROR_CODE,
                "codec find audio decoder error");
        return;
    }
    // 打开解码器
    pCodecContext = avcodec_alloc_context3(pCodec);
    if (pCodecContext == NULL) {
        LOGE("codec alloc context error");
        callPlayerJniError(threadMode, CODEC_ALLOC_CONTEXT_ERROR_CODE, "codec alloc context error");
        return;
    }
    int codecParametersToContextRes = avcodec_parameters_to_context(pCodecContext,
            pCodecParameters);
    if (codecParametersToContextRes < 0) {
        LOGE("codec parameters to context error: %s", av_err2str(codecParametersToContextRes));
        callPlayerJniError(threadMode, codecParametersToContextRes,
                av_err2str(codecParametersToContextRes));
        return;
    }

    int codecOpenRes = avcodec_open2(pCodecContext, pCodec, NULL);
    if (codecOpenRes != 0) {
        LOGE("codec audio open error: %s", av_err2str(codecOpenRes));
        callPlayerJniError(threadMode, codecOpenRes, av_err2str(codecOpenRes));
        return;
    }

    // ---------- 重采样 start ----------
    int64_t out_ch_layout = AV_CH_LAYOUT_STEREO;
    enum AVSampleFormat out_sample_fmt = AVSampleFormat::AV_SAMPLE_FMT_S16;
    int out_sample_rate = AUDIO_SAMPLE_RATE;
    int64_t in_ch_layout = pCodecContext->channel_layout;
    enum AVSampleFormat in_sample_fmt = pCodecContext->sample_fmt;
    int in_sample_rate = pCodecContext->sample_rate;
   //需要构建用于重采样的context
    pSwrContext = swr_alloc_set_opts(NULL, out_ch_layout, out_sample_fmt,
            out_sample_rate, in_ch_layout, in_sample_fmt, in_sample_rate, 0, NULL);
    if (pSwrContext == NULL) {
        // 提示错误
        callPlayerJniError(threadMode, SWR_ALLOC_SET_OPTS_ERROR_CODE, "swr alloc set opts error");
        return;
    }
    int swrInitRes = swr_init(pSwrContext);
    if (swrInitRes < 0) {
        callPlayerJniError(threadMode, SWR_CONTEXT_INIT_ERROR_CODE, "swr context swr init error");
        return;
    }

    resampleOutBuffer = (uint8_t *) malloc(pCodecContext->frame_size * 2 * 2);
    // 返回java层,音频的信息
    pJniCall->callMediaInfo(THREAD_CHILD,AUDIO_SAMPLE_RATE,2);
    // ---------- 重采样设定 end ----------
}

3.2 mPlayer.play()调用
java层play(),会调用链接的native方法

// music-player.cpp
extern "C" JNIEXPORT void JNICALL
Java_com_darren_media_DarrenPlayer_nPlay(JNIEnv *env, jobject instance) {
    if (pFFmpeg != NULL) {

        pFFmpeg->play();
    }
}

pFFmpeg->play(),调用DZAudio的play()
读packet比较耗时间,和视频的读packet需要能集成在一起,也为了性能考虑,需要开一个线程用于读取 packet,然后播放也在线程中进行

//DZAudio.cpp
void DZAudio::play() {
    // 一个线程去读取 Packet
    // 对接mediacodec录制视频添加2
    pthread_create(&readPacketThreadT, NULL, threadReadPacket, this);
//    pthread_detach(readPacketThreadT);

    // 一个线程去解码播放
    pthread_t playThreadT;
    pthread_create(&playThreadT, NULL, threadPlay, this);
    pthread_detach(playThreadT);
}

void *threadReadPacket(void *context) {
    DZAudio *pAudio = (DZAudio *) context;
    while (pAudio->pPlayerStatus != NULL && !pAudio->pPlayerStatus->isExit) {
        AVPacket *pPacket = av_packet_alloc();
        if (av_read_frame(pAudio->pFormatContext, pPacket) >= 0) {
            if (pPacket->stream_index == pAudio->audioStreamIndex) {
                pAudio->pPacketQueue->push(pPacket);
            } else {
                // 1. 解引用数据 data , 2. 销毁 pPacket 结构体内存  3. pPacket = NULL
                av_packet_free(&pPacket);
            }
        } else {
            // 1. 解引用数据 data , 2. 销毁 pPacket 结构体内存  3. pPacket = NULL
            av_packet_free(&pPacket);
            // 睡眠一下,尽量不去消耗 cpu 的资源,也可以退出销毁这个线程
            // break;
        }
    }
    return 0;
}

void *threadPlay(void *context) {
    DZAudio *pAudio = (DZAudio *) context;
    // 上方的opengles设定
    pAudio->initCreateOpenSLES();
    return 0;
}

void DZAudio::initCreateOpenSLES() {
    /*OpenSLES OpenGLES 都是自带的
    XXXES 与 XXX 之间可以说是基本没有区别,区别就是 XXXES 是 XXX 的精简
    而且他们都有一定规则,命名规则 slXXX() , glXXX3f*/
    // 1、 创建引擎接口对象
    SLObjectItf engineObject = NULL;
    SLEngineItf engineEngine;
    // 1.1 创建引擎对象:SLObjectItf engineObject
    slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
    // 1.2 realize the engine
    (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    // 1.3 获取引擎接口 SLEngineItf engineInterface
    (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);

    // 2、 设置混音器
    static SLObjectItf outputMixObject = NULL;
    const SLInterfaceID ids[1] = {SL_IID_ENVIRONMENTALREVERB};
    const SLboolean req[1] = {SL_BOOLEAN_FALSE};
    // 2.1 创建混音器:SLObjectItf outputMixObject
    (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, ids, req);
    // 2.2 初始化混音器
    (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);

    SLEnvironmentalReverbItf outputMixEnvironmentalReverb = NULL;
    (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB,
            &outputMixEnvironmentalReverb);
    SLEnvironmentalReverbSettings reverbSettings = SL_I3DL2_ENVIRONMENT_PRESET_STONECORRIDOR;
    (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(outputMixEnvironmentalReverb,
            &reverbSettings);
    // 3、 创建播放器
    SLObjectItf pPlayer = NULL;
    pPlayItf = NULL;
    SLDataLocator_AndroidSimpleBufferQueue simpleBufferQueue = {
            SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
    //pcm数据格式
    //SL_DATAFORMAT_PCM:数据格式为pcm格式
    //2:双声道
    //SL_SAMPLINGRATE_44_1:采样率为44100
    //SL_PCMSAMPLEFORMAT_FIXED_16:采样格式为16bit
    //SL_PCMSAMPLEFORMAT_FIXED_16:数据大小为16bit
    //SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT:左右声道(双声道)
    //SL_BYTEORDER_LITTLEENDIAN:小端模式
    SLDataFormat_PCM formatPcm = {
            SL_DATAFORMAT_PCM,
            2,
            SL_SAMPLINGRATE_44_1,
            SL_PCMSAMPLEFORMAT_FIXED_16,
            SL_PCMSAMPLEFORMAT_FIXED_16,
            SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
            SL_BYTEORDER_LITTLEENDIAN};
    //3.1 数据源 将上述配置信息放到这个数据源中
    SLDataSource audioSrc = {&simpleBufferQueue, &formatPcm};

    //3.2 配置音轨(输出)
    SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
    SLDataSink audioSnk = {&outputMix, NULL};
    // 需要的接口 操作队列的接口
    SLInterfaceID interfaceIds[3] = {SL_IID_BUFFERQUEUE, SL_IID_VOLUME, SL_IID_PLAYBACKRATE};
    SLboolean interfaceRequired[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
    //3.3 创建播放器
    (*engineEngine)->CreateAudioPlayer(engineEngine, &pPlayer, &audioSrc, &audioSnk, 3,
            interfaceIds, interfaceRequired);
    //3.4 初始化播放器:SLObjectItf bqPlayerObject
    (*pPlayer)->Realize(pPlayer, SL_BOOLEAN_FALSE);
    //3.5 获取播放器接口:SLPlayItf bqPlayerPlay
    (*pPlayer)->GetInterface(pPlayer, SL_IID_PLAY, &pPlayItf);

    // 4、 设置缓存队列和回调函数
    SLAndroidSimpleBufferQueueItf playerBufferQueue;
    //4.1 获取播放器队列接口:SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue
    (*pPlayer)->GetInterface(pPlayer, SL_IID_BUFFERQUEUE, &playerBufferQueue);
    //4.2 每次回调 this 会被带给 playerCallback 里面的 context
    (*playerBufferQueue)->RegisterCallback(playerBufferQueue, playerCallback, this);
    // 5、 设置播放器状态为播放状态
    (*pPlayItf)->SetPlayState(pPlayItf, SL_PLAYSTATE_PLAYING);
    // 6、 手动激活回调函数
    playerCallback(playerBufferQueue, this);
}

// 该方法会重复调用
void playerCallback(SLAndroidSimpleBufferQueueItf caller, void *pContext) {
    DZAudio *pAudio = (DZAudio *) pContext;

    int dataSize = pAudio->resampleAudio();

    // 对接mediacodec录制视频添加3
    pAudio->pJniCall->callCallbackPcm(THREAD_CHILD,pAudio->resampleOutBuffer,dataSize);
    // 解码拿到的byte[]数据放入播放器中
    (*caller)->Enqueue(caller, pAudio->resampleOutBuffer, dataSize);
}

在resampleAudio中,拆解pPacket包,进行重采样,数据存入resampleOutBuffer,这也就是录音用的音频数据,会不断循环调用该方法

//DZAudio.cpp
int DZAudio::resampleAudio() {
    int dataSize = 0;
    AVPacket *pPacket = NULL;
    AVFrame *pFrame = av_frame_alloc();

    while (pPlayerStatus != NULL && !pPlayerStatus->isExit) {
        pPacket = pPacketQueue->pop();
        // Packet 包,压缩的数据,解码成 pcm 数据
        int codecSendPacketRes = avcodec_send_packet(pCodecContext, pPacket);
        if (codecSendPacketRes == 0) {
            int codecReceiveFrameRes = avcodec_receive_frame(pCodecContext, pFrame);
            if (codecReceiveFrameRes == 0) {
                // AVPacket -> AVFrame
                // 调用重采样的方法,返回值是返回重采样的个数,也就是 pFrame->nb_samples
                dataSize = swr_convert(pSwrContext, &resampleOutBuffer, pFrame->nb_samples,
                        (const uint8_t **) pFrame->data, pFrame->nb_samples);
                dataSize = dataSize * 2 * 2;
                // write 写到缓冲区 pFrame.data -> javabyte
                // size 是多大,装 pcm 的数据
                // 1s 44100 点  2通道 ,2字节    44100*2*2
                // 1帧不是一秒,pFrame->nb_samples点
                break;
            }
        }
        // 解引用
        av_packet_unref(pPacket);
        av_frame_unref(pFrame);
    }
    // 1. 解引用数据 data , 2. 销毁 pPacket 结构体内存  3. pPacket = NULL
    av_packet_free(&pPacket);
    av_frame_free(&pFrame);
    return dataSize;
}

3.3 mPlayer.stop
上方的detach,会在读取完成后结束,不符合录制音频的需求,这里让主线程等待readPacketThreadT结束,再结束

// DZAudio.cpp
// 对接mediacodec录制视频添加1
void DZAudio::stop() {

    if(pPlayerStatus != NULL && !pPlayerStatus->isExit){
        pPlayerStatus->isExit = true;

        //设置opensl es 状态停止
        (*pPlayItf)->SetPlayState(pPlayItf, SL_PLAYSTATE_STOPPED);

        // 等待readPacketThreadT结束
        pthread_join(readPacketThreadT,NULL);
    }
}

这样整体的音频播放就完成了。

官网
https://www.khronos.org/opensles/
https://developer.android.google.cn/ndk/guides/audio/opensl

参考
https://www.jianshu.com/p/2b8d2de9a47b
https://blog.csdn.net/Poisx/article/details/78336404

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容