Open SL ES 調用ffmpeg 播放聲音(有註釋用於回看)

資料

https://blog.csdn.net/ywl5320/article/details/78503768
http://www.cnblogs.com/renhui/p/9567332.html

AndroidStudio使用OpenSL ES官方配置文檔
https://developer.android.com/ndk/guides/audio/getting-started.html

Open SL ES 和Open GL ES
一個是音頻 一個是視頻
OpenSLES官網
https://www.khronos.org/opensles/

官方文檔
https://www.khronos.org/files/opensl-es-1-1-quick-reference.pdf

原生播放

OpenSL ES

OpenSL ES 全稱是:Open Sound Library for Embedded Systems,

是一套無授權費、跨平臺、針對嵌入式系統精心優化的硬件音頻加速API

它爲嵌入式移動多媒體設備上的本地應用程序開發者提供標準化, 高性能, 低響應時間的音頻功能實現方法
,並實現軟硬件音頻性能的直接跨平臺部署,降低執行難度,促進高級音頻市場的發展

OpenSL ES 可以錄音、播放音頻URI和PCM數據,後面將編寫 OpenSL ES 相關代碼:
1、OpenSL ES 音頻URI 播放
2、OpenSL ES 音頻PCM數據 播放
3、OpenSL ES 錄音 PCM數據

特點

  1. C 語言接口,兼容 C++,需要在 NDK 下開發,能更好地集成在 native 應用中
  2. 運行於 native 層,需要自己管理資源的申請與釋放,沒有 Dalvik 虛擬機的垃圾回收機制
  3. 支持 PCM 數據的播放,支持的配置:8bit/16bit 位寬,單通道/雙通道,小端模式,採樣率(8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000 Hz)
  4. 支持播放的音頻數據來源:res 文件夾下的音頻、assets 文件夾下的音頻、sdcard 目錄下的音頻、在線網絡音頻、代碼中定義的音頻二進制數據等等

不支持:
(1)不支持版本低於 Android 2.3 (API 9) 的設備
(2)沒有全部實現 OpenSL ES 定義的特性和功能
(4)不支持直接播放 DRM 或者 加密的內容

我認爲 openSL ES 主要的工作就是讀取數組數據後直接調用驅動的程序,讓音箱發出聲音.

  • openSL ES 的so 庫在 sdk\ndk-bundle\platforms\android-9\arch-arm\usr\lib\libOpenSLES.so

狀態機制:

https://blog.csdn.net/brandon2015/article/details/51814657

image

三種狀態:

  1. unrealized初始化狀態
  2. realized 實現狀態. 能夠用的只能是這種狀態
  3. suspended 暫停狀態

舉例: 正在播放音樂的時候 來電話了,音樂會中斷播放手機鈴聲.

代碼.
新建FFmpegMusic.cpp 把ffmpeg相關的操作都放在這裏面. 主要是三個部分.

  1. 初始化 包括註冊組件,初始化AVFormatContext,解碼器AVCodecContext上下文,解碼器AVCodec等等.
  2. 獲取pcm數據
  3. 釋放指針 回收內存

頭文件FFmpegMusic.h

//
// Created by liuml on 2018/9/11.
//
#include <jni.h>
#include <string>
#include <jni.h>
#include <string>
#include <android/log.h>
//extern "C" 主要作用就是爲了能夠正確實現C++代碼調用其他C語言代碼 加上extern "C"後,會指示編譯器這部分代碼按C語言的進行編譯,而不是C++的。
extern "C" {
//編碼
#include "libavcodec/avcodec.h"
//封裝格式處理
#include "libavformat/avformat.h"
//像素處理
#include "libswscale/swscale.h"
//用於android 繪製圖像的
#include <android/native_window_jni.h>
#include <unistd.h>
#include <libswresample/swresample.h>
}

#ifndef FFMPEGDEMO_FFMPEGMUSIC_H
#define FFMPEGDEMO_FFMPEGMUSIC_H
#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"jnilib",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"jnilib",FORMAT,##__VA_ARGS__);
#endif //FFMPEGDEMO_FFMPEGMUSIC_H

int createFFmpge();

int getPcm();

void realseFFmpeg();

實現 FFmpegMusic.cpp

//
// Created by liuml on 2018/9/11.
//
#include <jni.h>
#include <string>
#include <jni.h>
#include <string>
#include <android/log.h>
#include "FFmpegMusic.h"

AVFormatContext *pContext;
AVCodecContext *pCodecCtx;
AVCodec *pCodex;
AVPacket *packet;
AVFrame *frame;
SwrContext *swrContext;
uint8_t *out_buffer;
int out_sample_rate;
int out_channer_nb = -1;
//找到視頻流
int audio_stream_ids = -1;
/**
 * 初始化.   openSL ES 調用這個函數
 * @param rate 採樣率
 * @param channel 通道數
 * @return
 */
int createFFmpeg(int *rate, int *channel) {
    //前面和之前一樣 獲取輸入的信息 但是現在是找到音頻的AVMEDIA_TYPE_AUDIO
    // TODO
    //無論編碼還是解碼 都要調用這個 註冊各大組件
    av_register_all();

    //獲取AVFormatContext  比特率 時長 文件路徑 流的信息(nustream) 都封裝在這裏面
    pContext = avformat_alloc_context();

    char *input = "/sdcard/input.mp3";
    //AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options
    //上下文  文件名  打開文件格式 獲取信息(AVDictionary)  凡是AVDictionary字典 都是獲取視頻文件信息
    if (avformat_open_input(&pContext, input, NULL, NULL) < 0) {
        LOGE("打開失敗");
        return -1;
    }

    //給nbstram填充信息
    if (avformat_find_stream_info(pContext, NULL) < 0) {
        LOGE("獲取信息失敗");
        return -1;
    }


    for (int i = 0; i < pContext->nb_streams; ++i) {
        LOGE("循環 %d", i);
        //如果填充的視頻流信息 -> 編解碼,解碼器 -> 流的類型 == 視頻的類型
        //codec 每一個流 對應的解碼上下文 codec_type 流的類型
        if (pContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
            audio_stream_ids = i;
            LOGE("找到音頻id %d", i);
            break;
        }
    }

    //解碼器. mp3的解碼器 解碼器
    // ----------  獲取到解碼器上下文 獲取視音頻解碼器
    pCodecCtx = pContext->streams[audio_stream_ids]->codec;
    LOGE("獲取解碼器上下文");
    //----------解碼器
    pCodex = avcodec_find_decoder(pCodecCtx->codec_id);
    LOGE("獲取解碼器");
    //打開解碼器  爲什麼avcodec_open2 版本升級的原因
    if (avcodec_open2(pCodecCtx, pCodex, NULL) < 0) {
        LOGE("解碼失敗");
        return -1;
    }

    //得到解封裝 讀取 解封每一幀 讀取每一幀的壓縮數據
    //初始化avpacket 分配內存  FFMpeg 沒有自動分配內存 必須手動分匹配手動釋放  不過有分配函數
    packet = (AVPacket *) av_malloc(sizeof(AVPacket));

    //初始化AVFrame
    frame = av_frame_alloc();

    //mp3 裏面所包含的編碼格式 轉化成pcm
    //#include <libswresample/swresample.h>
    swrContext = swr_alloc();//獲取轉換的上下文

    int frame_count = 0;

    //AVFormatContext *s, AVPacket *pkt  上下文,avpacket 是數據包的意思
    //packet 入參
    int got_frame;

    int length = 0;

    //定義緩衝區輸出的  需要多大的採樣率  採樣 44100  多少個字節.  雙通道需要乘以2
    out_buffer = static_cast<uint8_t *>(av_malloc(44100 * 2));//一秒的緩衝區數量


    /**
     * struct SwrContext *swr_alloc_set_opts(struct SwrContext *s,
     *                                  //輸出的
                                      int64_t out_ch_layout, enum AVSampleFormat out_sample_fmt, int out_sample_rate,
                                      //輸入的
                                      int64_t  in_ch_layout, enum AVSampleFormat  in_sample_fmt, int  in_sample_rate,
                                      //偏移量
                                      int log_offset, void *log_ctx);

     */
    //輸出聲道佈局
    uint64_t out_ch_layout = AV_CH_LAYOUT_STEREO;//立體聲
    //輸出採樣位數 16位 現在基本都是16位  位數越高 聲音越清晰
    enum AVSampleFormat out_formart = AV_SAMPLE_FMT_S16;
    //輸出的採樣率 必須與輸入的相同
    out_sample_rate = pCodecCtx->sample_rate;



    //https://blog.csdn.net/explorer_day/article/details/76332556  文檔
    //swr_alloc_set_opts將PCM源文件的採樣格式轉換爲自己希望的採樣格式
    swr_alloc_set_opts(swrContext, out_ch_layout, out_formart, out_sample_rate,
                       pCodecCtx->channel_layout, pCodecCtx->sample_fmt, pCodecCtx->sample_rate,
                       0, NULL);

    //初始化轉換器
    swr_init(swrContext);

    //求出通道數
    out_channer_nb = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);

    *rate = pCodecCtx->sample_rate;//通過解析出來的 採樣率 賦值給調用的openSL ES 的採樣率
    *channel = pCodecCtx->channels;//同理賦值 通道數.
    return 0 ;

}

/**
 * 獲取pcm數據
 * @param pcm 二級指針 可以修改(緩衝區數組的地址)
 * @param size  大小
 * @return
 */
int getPcm(void **pcm,size_t *pcm_size) {
    int got_frame;
    int count = 0;
    //讀取frame
    while (av_read_frame(pContext, packet) >= 0) {
        if (packet->stream_index == audio_stream_ids) {
            //解碼  現在編碼格式frame 需要轉化成pcm
            int ret = avcodec_decode_audio4(pCodecCtx, frame, &got_frame, packet);
            LOGE("正在解碼 %d", count++);
            if (ret < 0) {
                LOGE("解碼完成");
            }
            //解碼一幀
            if (got_frame > 0) {
                //真正的解碼
                LOGE("開始解碼");
                //轉換得到out_buffer
                swr_convert(swrContext, &out_buffer, 44100 * 2,
                            (const uint8_t **) (frame->data), frame->nb_samples);

                LOGE("轉換得到out_buffer");
                //求緩衝區實際的大小  通道數  frame->nb_samples 採樣的點
                int size = av_samples_get_buffer_size(NULL, out_channer_nb, frame->nb_samples,
                                                      AV_SAMPLE_FMT_S16, 1);
                LOGE("求緩衝區實際的大小 %d", size);
                *pcm = out_buffer;
                *pcm_size = size;
                break;
            }
        }

    }
    LOGE("完成")

}

/**
 * 釋放
 */
void releaseFFmpeg() {
    //回收
    av_free_packet(packet);
    av_free(out_buffer);

    av_frame_free(&frame);
    swr_free(&swrContext);
    avcodec_close(pCodecCtx);
    avformat_free_context(pContext);


}
  • 這裏有個點 都是通過入參從而改變傳遞過來的參數.

一個概念 調用OpenSL ES 的方法 最終調用的是他的一個接口

例如:

創建一個播放器接口
 sLresult=(*bgPlayerObject)->GetInterface(bgPlayerObject, SL_IID_PLAY, &bqPlayerPlay);

OpenSL ES 的開發流程主要有如下6個步驟:

1、 創建接口對象

2、設置混音器

3、創建播放器(錄音器)

4、設置緩衝隊列和回調函數

5、設置播放狀態

6、啓動回調函數

上代碼:

#include <jni.h>
#include <string>
#include <android/log.h>

//extern "C" 主要作用就是爲了能夠正確實現C++代碼調用其他C語言代碼 加上extern "C"後,會指示編譯器這部分代碼按C語言的進行編譯,而不是C++的。
extern "C" {
//編碼
#include "libavcodec/avcodec.h"
//封裝格式處理
#include "libavformat/avformat.h"
//像素處理
#include "libswscale/swscale.h"
//用於android 繪製圖像的
#include <android/native_window_jni.h>
#include <unistd.h>
#include "libswresample/swresample.h"
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
}

#include "FFmpegMusic.h"

#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"jnilib",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"jnilib",FORMAT,##__VA_ARGS__);

SLObjectItf engineObject = NULL;//用SLObjectItf聲明引擎接口對象
SLEngineItf engineEngine = NULL;//聲明具體的引擎對象實例
//混音器
SLObjectItf outputMixObject = NULL; //聲明混音器接口對象
SLEnvironmentalReverbItf outputMixEnvironmentalReverbItf = NULL;//環境混響接口
SLEnvironmentalReverbSettings settings = SL_I3DL2_ENVIRONMENT_PRESET_DEFAULT;//設置默認環境
//播放器
SLObjectItf bqPlayerObject;
//播放器接口
SLPlayItf bqPlayerPlay;
//緩衝器隊列
SLAndroidSimpleBufferQueueItf bqPalyerQueue;

//音量對象
SLVolumeItf bqPlayerVolume;
//buffer數據
size_t bufferSize = 0;
void *buffer;

//當喇叭播放完聲音回調此函數,添加pcm數據到緩衝區
void bqPlayerCallBack(SLAndroidSimpleBufferQueueItf bq, void *context) {
    bufferSize = 0;
    getPcm(&buffer, &bufferSize);//獲取pcm數據
    if (NULL != buffer && 0 != bufferSize) {
        SLresult result;//結果
        //播放幀
        result = (*bqPalyerQueue)->Enqueue(bqPalyerQueue, buffer, bufferSize);
        LOGE("回調 bqPlayerCallback : %d", result);
    } else {
        LOGE("獲取PCM失敗")
    }
}

//openSL ES 播放音頻
extern "C"
JNIEXPORT void JNICALL
Java_androidrn_ffmpegdemo_AudioPlayer_OpenSLEsPlay(JNIEnv *env, jobject instance) {

    SLresult sLresult;
    //初始化一個引擎
    sLresult = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
    LOGE("初始化引擎 %d", sLresult);
    //改變成Realize狀態,參數false 代表非異步,就是同步
    sLresult = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    LOGE("引擎改變成Realize狀態 %d", sLresult);
    //獲取到引擎接口 利用GetInterface 調用函數
    sLresult = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
    LOGE("獲取到引擎接口 %d", sLresult);
//    LOGE("引擎地址 &p", engineEngine);

    // ====混音器 設置 開始=====
    sLresult = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, 0, 0);
    LOGE("混音器 設置 %d", sLresult);
    //同樣切換狀態  和上面同樣的套路
    sLresult = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
    LOGE("混音器 同樣切換狀態 %d", sLresult);
    //設置環境混響
    sLresult = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB,
                                                &outputMixEnvironmentalReverbItf);
    LOGE("設置環境混響 %d", sLresult);
    //每個函數都會返回sLresult 用於判斷是否調用成功
    if (SL_RESULT_SUCCESS == sLresult) {
        LOGE("環境混響成功");
        //設置環境
        (*outputMixEnvironmentalReverbItf)->SetEnvironmentalReverbProperties(
                outputMixEnvironmentalReverbItf, &settings);
    } else {
        LOGE("環境混響設置不成功 %d", sLresult);
//        return;
    }
    //===混音器設置結束 ====

    //初始化ffmpeg
    int rate;
    int channers;
    createFFmpeg(&rate, &channers);
    LOGE("初始化ffmpeg");


    //命名規則 都是SL 開頭 比如像這個 把前面SLDataLocator 打出來了即可
    //pLocator -> SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE 這個是讀取本地的 如果是網絡的 則是 SL_DATALOCATOR_IODEVICE
    SLDataLocator_AndroidBufferQueue android_queue = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
                                                      2};

    //SLDataSource的參數  *pAudioSrc
    /**
     * 	void *pLocator; 緩衝區隊列 SLDataLocator_BufferQueue
	void *pFormat; 緩衝區隊列裝載什麼數據格式 比如PCM 採樣率 通道數 等
     */
    //pFormat 參數
    /*
     * SLuint32 		formatType; 源數據類型 PCM
	SLuint32 		numChannels; 通道數
	SLuint32 		samplesPerSec; 開始的採樣率
	SLuint32 		bitsPerSample; 採樣位數
	SLuint32 		containerSize;  包含位數
	SLuint32 		channelMask;    聲道類型  立體聲 左聲道 右聲道 環繞聲
	SLuint32		endianness;  end結束標誌位

     */
    SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM, channers, SL_SAMPLINGRATE_44_1,
                            SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
                            SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
                            SL_BYTEORDER_LITTLEENDIAN};
    //    SLDataLocator_AndroidBufferQueue
//    SLDataLocator_AndroidSimpleBufferQueue
    //==CreateAudioPlayer 參數3
    SLDataSource slDataSource = {&android_queue, &pcm};

// 輸出管道
    SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
    //混音器和播放器關聯起來 鏈接方式
//    void *pLocator; 參數
//    void *pFormat;
    //和上面一樣的參數
    SLDataSink audioSnk = {&outputMix, NULL};
    //聲音增大減小 音量 調節輸出
    const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};
    const SLboolean req[3]={SL_BOOLEAN_TRUE,SL_BOOLEAN_TRUE,SL_BOOLEAN_TRUE};
    LOGE("引擎  %p",engineEngine);
    /**
     * SLEngineItf self,    引擎
		SLObjectItf * pPlayer,  播放器
		SLDataSource *pAudioSrc,  數據源
		SLDataSink *pAudioSnk, 混音器和播放器關聯起來 鏈接方式
		SLuint32 numInterfaces, 數量
		const SLInterfaceID * pInterfaceIds, 用於聲音增大減小
		const SLboolean * pInterfaceRequired 和上面對應 如果是true 證明要被實體化  這裏是3個boolean值
     */
    //得到播放器
    sLresult = (*engineEngine)->CreateAudioPlayer(engineEngine, &bqPlayerObject, &slDataSource,
                                                  &audioSnk, 3,
                                                  ids, req);
    LOGE("  播放器 sLresult  %d ", sLresult);
    //bqPlayerObject 修改狀態
    sLresult = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
    LOGE("  播放器 修改狀態 sLresult  %d ", sLresult);
    //    得到接口後調用  獲取Player接口 bqPlayerPlay
    sLresult = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);
    LOGE("    得到接口後調用  獲取Player接口 bqPlayerPlay sLresult  %d ", sLresult);
    //    註冊回調緩衝區 //獲取緩衝隊列接口
    sLresult = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,
                                               &bqPalyerQueue);
    LOGE("  註冊回調緩衝區 //獲取緩衝隊列接口 sLresult  %d ", sLresult);
    //緩衝區接口回調  第二個參數是個函數
    sLresult = (*bqPalyerQueue)->RegisterCallback(bqPalyerQueue, bqPlayerCallBack,
                                                  NULL);
    LOGE("  緩衝區接口回調  sLresult  %d ", sLresult);
    //獲取音量接口
    sLresult = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume);
    LOGE("  獲取音量接口 sLresult  %d ", sLresult);
    //獲取播放狀態接口 設置播放狀態
    sLresult = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
    LOGE("  獲取播放狀態接口 sLresult  %d ", sLresult);
    //播放第一幀
    bqPlayerCallBack(bqPalyerQueue, NULL);

}

//openSL ES 停止音頻
extern "C"
JNIEXPORT void JNICALL
Java_androidrn_ffmpegdemo_AudioPlayer_OpenSlESStop(JNIEnv *env, jobject instance) {

    // destroy buffer queue audio player object, and invalidate all associated interfaces
    if (bqPlayerObject != NULL) {
        (*bqPlayerObject)->Destroy(bqPlayerObject);
        bqPlayerObject = NULL;
        bqPlayerPlay = NULL;
        bqPalyerQueue = NULL;
        bqPlayerVolume = NULL;
    }

    // destroy output mix object, and invalidate all associated interfaces
    if (outputMixObject != NULL) {
        (*outputMixObject)->Destroy(outputMixObject);
        outputMixObject = NULL;
        outputMixEnvironmentalReverbItf = NULL;
    }
    // destroy engine object, and invalidate all associated interfaces
    if (engineObject != NULL) {
        (*engineObject)->Destroy(engineObject);
        engineObject = NULL;
        engineEngine = NULL;
    }
    // 釋放FFmpeg解碼器相關資源
    releaseFFmpeg();

}

發現個問題 混響設置失敗了一樣能播放

解決:

 // ====混音器 設置 開始=====
    const SLInterfaceID mids[1] = {SL_IID_ENVIRONMENTALREVERB};
    const SLboolean mreq[1] = {SL_BOOLEAN_FALSE};
    sLresult =(*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, mids, mreq);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章