Android音视频(五) OpenSL ES录制、播放音频

Android音视频(一) Camera2 API采集数据

Android音视频(二)音频AudioRecord和AudioTrack

Android音视频(三)FFmpeg Camera2推流直播

Android音视频(四)MediaCodec编解码AAC

OpenSL ES (Open Sound Library for Embedded Systems)是无授权费、跨平台、针对嵌入式系统精心优化的硬件音频加速API。它为嵌入式移动多媒体设备上的本地应用程序开发者提供标准化, 高性能,低响应时间的音频功能实现方法,并实现软/硬件音频性能的直接跨平台部署,降低执行难度,促进高级音频市场的发展。简单来说OpenSL ES是一个嵌入式跨平台免费的音频处理库。

在Android中一般使用AudioRecord、MediaRecorder对音频进行采集,使用MediaPlayer、AudioTrack、SoundPool进行音频播放。但这些都是在Java层上的接口,如果使用FFmpeg在C/C++层做音视频处理,那么调用这几个方法就比较麻烦了,所以Android NDK也提供了一个叫做OpenSL的C语言引擎用于声音的处理,这篇博客就是简单使用OpenSL去录制、播放音频。

开发流程

OpenSL ES 的开发流程主要有如下6个步骤:

1、创建接口对象

2、设置混音器

3、创建播放器(录音器)

4、设置缓冲队列和回调函数

5、设置播放状态

6、启动回调函数

其中第4步和第6步是OpenSL ES 播放PCM等数据格式的音频是需要用到的。

代码实现

定义Native方法


    //播放音频
    public native int play(String filePath);

    //停止播放音频
    public native int playStop();
    
    //录制音频
    public native int record(String filePath);
    
    //停止录制音频
    public native int stopRecod();
    

录音

参数配置

    //设置IO设备(麦克风)
    SLDataLocator_IODevice io_device = {
            SL_DATALOCATOR_IODEVICE,         //类型 这里只能是SL_DATALOCATOR_IODEVICE
            SL_IODEVICE_AUDIOINPUT,          //device类型  选择了音频输入类型
            SL_DEFAULTDEVICEID_AUDIOINPUT,   //deviceID 对应的是SL_DEFAULTDEVICEID_AUDIOINPUT
            NULL                             //device实例
    };
    SLDataSource data_src = {
            &io_device,                      //SLDataLocator_IODevice配置输入
            NULL                             //输入格式,采集的并不需要
    };

    //设置输出buffer队列
    SLDataLocator_AndroidSimpleBufferQueue buffer_queue = {
            SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,    //类型 这里只能是SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE
            2                                           //buffer的数量
    };
    //设置输出数据的格式
    SLDataFormat_PCM format_pcm = {
            SL_DATAFORMAT_PCM,                             //输出PCM格式的数据
            1,                                             //输出的声道数量
            SL_SAMPLINGRATE_44_1,                          //输出的采样频率,这里是44100Hz
            SL_PCMSAMPLEFORMAT_FIXED_16,                   //输出的采样格式,这里是16bit
            SL_PCMSAMPLEFORMAT_FIXED_16,                   //一般来说,跟随上一个参数
            SL_SPEAKER_FRONT_LEFT,  //双声道配置,如果单声道可以用 SL_SPEAKER_FRONT_CENTER
            SL_BYTEORDER_LITTLEENDIAN                      //PCM数据的大小端排列
    };
    SLDataSink audioSink = {
            &buffer_queue,                   //SLDataFormat_PCM配置输出
            &format_pcm                      //输出数据格式
    };

录音流程

    //1 创建引擎
    SLEngineItf eng = CreateRecordSL();
    if (eng) {
        LOGE("CreateSL success! ");
    } else {
        LOGE("CreateSL failed! ");
    }


    //创建录制的对象,并且指定开放SL_IID_ANDROIDSIMPLEBUFFERQUEUE这个接口
    const SLInterfaceID id[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
    const SLboolean req[1] = {SL_BOOLEAN_TRUE};
    re = (*eng)->CreateAudioRecorder(eng,        //引擎接口
                                     &recorder_object,   //录制对象地址,用于传出对象
                                     &data_src,          //输入配置
                                     &audioSink,         //输出配置
                                     1,                  //支持的接口数量
                                     id,                 //具体的要支持的接口
                                     req                 //具体的要支持的接口是开放的还是关闭的
    );

    if (re != SL_RESULT_SUCCESS) {
        LOGE("CreateAudioRecorder failed!");
        return -1;
    }

    //实例化这个录制对象
    re = (*recorder_object)->Realize(recorder_object, SL_BOOLEAN_FALSE);
    if (re != SL_RESULT_SUCCESS) {
        LOGE("Realize failed!");
    }

    //获取录制接口
    re = (*recorder_object)->GetInterface(recorder_object, SL_IID_RECORD, &recordItf);
    if (re != SL_RESULT_SUCCESS) {
        LOGE("GetInterface1 failed!");
    }
    //获取Buffer接口
    re = (*recorder_object)->GetInterface(recorder_object, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
                                          &recorder_buffer_queue);
    if (re != SL_RESULT_SUCCESS) {
        LOGE("GetInterface2 failed!");
    }

    //申请一块内存,注意RECORDER_FRAMES是自定义的一个宏,指的是采集的frame数量,具体还要根据你的采集格式(例如16bit)计算
    pcm_data = malloc(BUFFER_SIZE_IN_BYTES);

    //设置数据回调接口bqRecorderCallback,最后一个参数是可以传输自定义的上下文引用
    re = (*recorder_buffer_queue)->RegisterCallback(recorder_buffer_queue, bqRecorderCallback, 0);
    if (re != SL_RESULT_SUCCESS) {
        LOGE("RegisterCallback failed!");
    }
    //设置录制器为录制状态 SL_RECORDSTATE_RECORDING
    re = (*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_RECORDING);
    if (re != SL_RESULT_SUCCESS) {
        LOGE("SetRecordState failed!");
    }
    //在设置完录制状态后一定需要先Enqueue一次,这样的话才会开始采集回调
    re = (*recorder_buffer_queue)->Enqueue(recorder_buffer_queue, pcm_data, 8192);
    if (re != SL_RESULT_SUCCESS) {
        LOGE("Enqueue failed!");
    }

回调函数

//数据回调函数
void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {

    fwrite(pcm_data, BUFFER_SIZE_IN_BYTES, 1, gFile);
    //取完数据,需要调用Enqueue触发下一次数据回调
    (*bq)->Enqueue(bq, pcm_data, BUFFER_SIZE_IN_BYTES);

}

播放

    //1 创建引擎
    SLEngineItf eng = CreateSL();
    if (eng) {
        LOGE("CreateSL success! ");
    } else {
        LOGE("CreateSL failed! ");
        return -1;
    }

    //2 创建混音器
    SLObjectItf mix = NULL;
    SLresult re = 0;

    re = (*eng)->CreateOutputMix(eng, &mix, 0, 0, 0);
    if (re != SL_RESULT_SUCCESS) {
        LOGE("SL_RESULT_SUCCESS failed!");
        return -1;
    }

    re = (*mix)->Realize(mix, SL_BOOLEAN_FALSE);
    if (re != SL_RESULT_SUCCESS) {
        LOGE("(*mix)->Realize failed!");
        return -1;
    }


    SLDataLocator_OutputMix outmix = {SL_DATALOCATOR_OUTPUTMIX, mix};
    SLDataSink audioSink = {&outmix, 0};

    //3 配置音频信息
    //数据定位器 就是定位要播放声音数据的存放位置,分为4种:内存位置,输入/输出设备位置,缓冲区队列位置,和midi缓冲区队列位置。
    SLDataLocator_AndroidSimpleBufferQueue que = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 10};
    //音频格式
    SLDataFormat_PCM pcm = {
            SL_DATAFORMAT_PCM,
            1,//    声道数
            SL_SAMPLINGRATE_44_1,
            SL_PCMSAMPLEFORMAT_FIXED_16,
            SL_PCMSAMPLEFORMAT_FIXED_16,
            SL_SPEAKER_FRONT_LEFT,
            SL_BYTEORDER_LITTLEENDIAN //字节序,小端
    };
    SLDataSource ds = {&que, &pcm};


    //4 创建播放器
    SLObjectItf player = NULL;
    SLPlayItf iplayer = NULL;
    SLAndroidSimpleBufferQueueItf pcmQue = NULL;
    const SLInterfaceID ids[] = {SL_IID_BUFFERQUEUE};
    const SLboolean req[] = {SL_BOOLEAN_TRUE};
    re = (*eng)->CreateAudioPlayer(eng, &player, &ds, &audioSink,
                                   sizeof(ids) / sizeof(SLInterfaceID), ids, req);
    if (re != SL_RESULT_SUCCESS) {
        LOGE("CreateAudioPlayer failed!");
    } else {
        LOGE("CreateAudioPlayer success!");
    }
    (*player)->Realize(player, SL_BOOLEAN_FALSE);
    //获取player接口
    re = (*player)->GetInterface(player, SL_IID_PLAY, &iplayer);
    if (re != SL_RESULT_SUCCESS) {
        LOGE("GetInterface SL_IID_PLAY failed!");
    }
    re = (*player)->GetInterface(player, SL_IID_BUFFERQUEUE, &pcmQue);
    if (re != SL_RESULT_SUCCESS) {
        LOGE("GetInterface SL_IID_BUFFERQUEUE failed!");
    }

    //设置回调函数,播放队列空调用
    (*pcmQue)->RegisterCallback(pcmQue, pcmCallBack, 0);

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

    //6 启动队列回调
    (*pcmQue)->Enqueue(pcmQue, "", 1);

回调保存数据

 //回调函数
void pcmCallBack(SLAndroidSimpleBufferQueueItf bf, void *contex) {
    static char buf[1024 * 1024] = "";
    if (feof(File) == 0) { //没到结尾
        int len = (int) fread(&buf, 1, 1024, File);
        if (len > 0) {
            // 加入队列
            (*bf)->Enqueue(bf, &buf, len);
        }
    }
}

如有问题欢迎留言,Github源码-AudioDemo-openSLActivity

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

推荐阅读更多精彩内容