Android OpenSL ES 开发:Android OpenSL 录制 PCM 音频数据

Stella981
• 阅读 715

一、实现说明

OpenSL ES的录音要比播放简单一些,在创建好引擎后,再创建好录音接口基本就可以录音了。在这里我们做的是流式录音,所以需要用至少2个buffer来缓存录制好的PCM数据,这里我们可以动态创建一个二维数组,里面有2个buffer,然后每次录音取出一个,录制好后再写入文件就可以了,2个buffer依次来存储PCM数据,这样就可以连续录制流式音频数据了,二维数组里面自己维护了一个索引,来标识当前处于哪个buffer录制状态,暴露给外部的只是调用方法而已,细节对外也是隐藏的。

二、编码实现

1、编写缓存buffer队列:RecordBuffer.h、RecordBuffer.cpp

#ifndef OPENSLRECORD_RECORDBUFFER_H
#define OPENSLRECORD_RECORDBUFFER_H
 
class RecordBuffer {
 
public:
    short **buffer;
    int index = -1;
public:
    RecordBuffer(int buffersize);
    ~RecordBuffer();
    /**
     * 得到一个新的录制buffer
     * @return
     */
    short* getRecordBuffer();
    /**
     * 得到当前录制buffer
     * @return
     */
    short* getNowBuffer();
};
 
#endif //OPENSLRECORD_RECORDBUFFER_H

#include "RecordBuffer.h"
 
RecordBuffer::RecordBuffer(int buffersize) {
    buffer = new short *[2];
    for(int i = 0; i < 2; i++)
    {
        buffer[i] = new short[buffersize];
    }
}
 
RecordBuffer::~RecordBuffer() {
}
 
short *RecordBuffer::getRecordBuffer() {
    index++;
    if(index > 1)
    {
        index = 0;
    }
    return buffer[index];
}
 
short *RecordBuffer::getNowBuffer() {
    return buffer[index];
}

这个队列其实就是PCM存储的buffer,getRecordBuffer()为即将要录入PCM数据的buffer,getNowBuffer()是当前录制好的PCM数据的buffer,可以写入文件,即我们得到的PCM数据。

2、使用OpenSL ES录制PCM数据

过程分为:创建引擎->初始化IO设备(自动检测麦克风等音频输入设备)->设置缓存队列->设置录制PCM数据规格->设置录音器接口->设置队列接口并设置录音状态为录制->开始录音。

const char *path = env->GetStringUTFChars(path_, 0);
    /**
     * PCM文件
     */
    pcmFile = fopen(path, "w");
    /**
     * PCMbuffer队列
     */
    recordBuffer = new RecordBuffer(RECORDER_FRAMES * 2);
    SLresult result;
    /**
     * 创建引擎对象
     */
    result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
    result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
 
    /**
     * 设置IO设备(麦克风)
     */
    SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT,
                                      SL_DEFAULTDEVICEID_AUDIOINPUT, NULL};
    SLDataSource audioSrc = {&loc_dev, NULL};
    /**
     * 设置buffer队列
     */
    SLDataLocator_AndroidSimpleBufferQueue loc_bq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
    /**
     * 设置录制规格:PCM、2声道、44100HZ、16bit
     */
    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};
    SLDataSink audioSnk = {&loc_bq, &format_pcm};
 
    const SLInterfaceID id[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
    const SLboolean req[1] = {SL_BOOLEAN_TRUE};
 
    /**
     * 创建录制器
     */
    result = (*engineEngine)->CreateAudioRecorder(engineEngine, &recorderObject, &audioSrc,
                                                  &audioSnk, 1, id, req);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord);
    result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
                                             &recorderBufferQueue);
    finished = false;
    result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(),
                                             recorderSize);
    result = (*recorderBufferQueue)->RegisterCallback(recorderBufferQueue, bqRecorderCallback, NULL);
    LOGD("开始录音");
    /**
     * 开始录音
     */
    (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING);
    env->ReleaseStringUTFChars(path_, path);

录音回调如下:

void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{
    // for streaming recording, here we would call Enqueue to give recorder the next buffer to fill
    // but instead, this is a one-time buffer so we stop recording
    LOGD("record size is %d", recorderSize);
 
    fwrite(recordBuffer->getNowBuffer(), 1, recorderSize, pcmFile);
 
    if(finished)
    {
        (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED);
        fclose(pcmFile);
        LOGD("停止录音");
    } else{
        (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(),
                                        recorderSize);
    }
}

这样就完成了OPenSL ES的PCM音频数据录制,我们这里拿到了录制的PCM数据可以用mediacodec或ffmpeg来编码成aac格式的音频,也可以直接用推流到服务器来实现音频直播。

完整代码如下:

#include <jni.h>
#include <string>
#include "AndroidLog.h"
#include "RecordBuffer.h"
#include "unistd.h"
 
extern "C"
{
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
}
 
//引擎接口
static SLObjectItf engineObject = NULL;
//引擎对象
static SLEngineItf engineEngine;
 
//录音器接口
static SLObjectItf recorderObject = NULL;
//录音器对象
static SLRecordItf recorderRecord;
//缓冲队列
static SLAndroidSimpleBufferQueueItf recorderBufferQueue;
 
//录制大小设为4096
#define RECORDER_FRAMES (2048)
static unsigned recorderSize = RECORDER_FRAMES * 2;
 
//PCM文件
FILE *pcmFile;
//录音buffer
RecordBuffer *recordBuffer;
 
bool finished = false;
 
void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{
    // for streaming recording, here we would call Enqueue to give recorder the next buffer to fill
    // but instead, this is a one-time buffer so we stop recording
    LOGD("record size is %d", recorderSize);
 
    fwrite(recordBuffer->getNowBuffer(), 1, recorderSize, pcmFile);
 
    if(finished)
    {
        (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED);
        fclose(pcmFile);
        LOGD("停止录音");
    } else{
        (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(),
                                        recorderSize);
    }
}
 
extern "C"
JNIEXPORT void JNICALL
Java_com_renhui_openslrecord_MainActivity_rdSound(JNIEnv *env, jobject instance, jstring path_) {
    const char *path = env->GetStringUTFChars(path_, 0);
    /**
     * PCM文件
     */
    pcmFile = fopen(path, "w");
    /**
     * PCMbuffer队列
     */
    recordBuffer = new RecordBuffer(RECORDER_FRAMES * 2);
    SLresult result;
    /**
     * 创建引擎对象
     */
    result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
    result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
 
    /**
     * 设置IO设备(麦克风)
     */
    SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT,
                                      SL_DEFAULTDEVICEID_AUDIOINPUT, NULL};
    SLDataSource audioSrc = {&loc_dev, NULL};
    /**
     * 设置buffer队列
     */
    SLDataLocator_AndroidSimpleBufferQueue loc_bq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
    /**
     * 设置录制规格:PCM、2声道、44100HZ、16bit
     */
    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};
    SLDataSink audioSnk = {&loc_bq, &format_pcm};
 
    const SLInterfaceID id[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
    const SLboolean req[1] = {SL_BOOLEAN_TRUE};
 
    /**
     * 创建录制器
     */
    result = (*engineEngine)->CreateAudioRecorder(engineEngine, &recorderObject, &audioSrc,
                                                  &audioSnk, 1, id, req);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord);
    result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
                                             &recorderBufferQueue);
    finished = false;
    result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(),
                                             recorderSize);
    result = (*recorderBufferQueue)->RegisterCallback(recorderBufferQueue, bqRecorderCallback, NULL);
    LOGD("开始录音");
    /**
     * 开始录音
     */
    (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING);
    env->ReleaseStringUTFChars(path_, path);
}extern "C"
JNIEXPORT void JNICALL
Java_com_renhui_openslrecord_MainActivity_rdStop(JNIEnv *env, jobject instance) {
 
    // TODO
    if(recorderRecord != NULL)
    {
        finished = true;
    }
}

三、验证录制成果

有两种方法:

1. 使用Android OpenSL ES 开发:使用 OpenSL 播放 PCM 数据的demo进行播放。

2. 使用 ffplay 命令播放,命令为:ffplay -f s16le -ar 44100 -ac 2 temp.pcm (命令由来:在录制代码里的参数为录制规格:PCM、2声道、44100HZ、16bit)

四、参考源码

https://github.com/renhui/OpenSLRecord

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这