Android音频开发之OpenSL ES

开发Android上的音频应用,最常见的是使用MediaRecorderMediaPlayer来实现音频的录制和播放,更基础点的会使用AudioRecordAudioTrack来实现。用这两种方式已经能应对绝大部分的音频开发需求了。更底层的API,如NDK层的OpenSL ES则鲜有问津。

最近因为工作需要,接触了NDK层相关API,这里简要记录下OpenSL ES相关的知识。

关于OpenSL ES

HelloWorld

不同于传统的HelloWorld程序,这个示例稍微复杂一点,而且这回我们的实现,将让我们听到这句经典的编程入门欢迎语。

实现思路

此处应有图,一图顶万言
  1. 创建并初始化Audio Engine(音频引擎,是和底层交互的入口)
  2. 打开OutputMix(音频输出),配置相关参数、Buffer Queue(缓冲队列),以便进行音频播放
  3. 打开Audio Input(音频输入),配置相关参数,配置Buffer Queue,以便获取音频输入
  4. 设置输出、输入的Callback(回调函数),实现将输入传给输出的逻辑
  5. 启动音频录制

也就是,这个程序实现的功能是:将话筒录制的声音,再播放出来,也就是返听的效果。

代码实现

1. 创建并初始化Audio Engine

// 创建Audio Engine
result = slCreateEngine(&openSLEngine, 0, NULL, 0, NULL, NULL);

// 初始化上一步得到的openSLEngine
result = (*openSLEngine)->Realize(openSLEngine, SL_BOOLEAN_FALSE);

// 获取SLEngine接口对象,后续的操作将使用这个对象
SLEngineItf openSLEngineInterface = NULL;
result = (*openSLEngine)->GetInterface(openSLEngine, SL_IID_ENGINE, &openSLEngineInterface);

2. 音频输出

2.1 打开音频输出设备

// 相关参数
const SLInterfaceID ids[] = {SL_IID_VOLUME};
const SLboolean req[] = {SL_BOOLEAN_FALSE};

// 使用第一步的openSLEngineInterface,创建音频输出Output Mix
result = (*openSLEngineInterface)->CreateOutputMix(openSLEngineInterface, &outputMix, 0, ids, req);

// 初始化outputMix
result = (*outputMix)->Realize(outputMix, SL_BOOLEAN_FALSE);

// 由于不需要操作到ouputMix,所以这一步就不去获取它的接口对象

2.2 配置相关参数

// Buffer Queue的参数
SLDataLocator_AndroidSimpleBufferQueue outputLocator = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 1 };

// 设置音频格式
SLDataFormat_PCM outputFormat = { SL_DATAFORMAT_PCM, 2, samplerate, SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, SL_BYTEORDER_LITTLEENDIAN };

// 输出源
SLDataSource outputSource = { &outputLocator, &outputFormat };

// 输出管道
SLDataLocator_OutputMix outputMixLocator = { SL_DATALOCATOR_OUTPUTMIX, outputMix };
SLDataSink outputSink = { &outputMixLocator, NULL };

2.3 创建播放器

// 参数
const SLInterfaceID outputInterfaces[1] = { SL_IID_BUFFERQUEUE };
const SLboolean requireds[2] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_FALSE };

// 创建音频播放对象AudioPlayer
result = (*openSLEngineInterface)->CreateAudioPlayer(openSLEngineInterface, &audioPlayerObject, &outputSource, &outputSink, 1, outputInterfaces, requireds);

// 初始化AudioPlayer
result = (*audioPlayerObject)->Realize(audioPlayerObject, SL_BOOLEAN_FALSE);

// 获取音频输出的BufferQueue接口
SLAndroidSimpleBufferQueueItf outputBufferQueueInterface = NULL;
result = (*audioPlayerObject)->GetInterface(audioPlayerObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &outputBufferQueueInterface); 

// 获取播放器接口
SLPlayItf outputPlayInterface;
result = (*audioPlayerObject)->GetInterface(audioPlayerObject, SL_IID_PLAY, &audioPlayerInterface);

3. 音频输入

3.1 配置参数

// 参数
SLDataLocator_IODevice deviceInputLocator = { SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT, NULL };
SLDataSource inputSource = { &deviceInputLocator, NULL };

SLDataLocator_AndroidSimpleBufferQueue inputLocator = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 1 };
SLDataFormat_PCM inputFormat = { SL_DATAFORMAT_PCM, 2, samplerate * 1000, SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, SL_BYTEORDER_LITTLEENDIAN };

SLDataSink inputSink = { &inputLocator, &inputFormat };

const SLInterfaceID inputInterfaces[2] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_ANDROIDCONFIGURATION };

const SLboolean requireds[2] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_FALSE };

3.2 创建录制器

// 创建AudioRecorder
result = (*openSLEngineInterface)->CreateAudioRecorder(openSLEngineInterface, &andioRecorderObject, &inputSource, &inputSink, 2, inputInterfaces, requireds);

// 初始化AudioRecorder
result = (*andioRecorderObject)->Realize(andioRecorderObject, SL_BOOLEAN_FALSE);

// 获取音频输入的BufferQueue接口
result = (*andioRecorderObject)->GetInterface(andioRecorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &inputBufferQueueInterface);

// 获取录制器接口
SLRecordItf audioRecorderInterface;
(*andioRecorderObject)->GetInterface(andioRecorderObject, SL_IID_RECORD, &audioRecorderInterface);

4. 配置回调并启动输入、输出

4.1 配置输入、输出回调

// 输出回调
result = *outputBufferQueueInterface)->RegisterCallback(outputBufferQueueInterface, outputCallback, NULL);

// 输入回调
result = (*inputBufferQueueInterface)->RegisterCallback(inputBufferQueueInterface, inputCallback, NULL);

4.2 启动输入输出

// 设置为播放状态
result = (*audioPlayerInterface)->SetPlayState(audioPlayerInterface, SL_PLAYSTATE_PLAYING);
// 设为录制状态
result = (*andioRecorderObject)->SetRecordState(andioRecorderObject, SL_RECORDSTATE_RECORDING);

// 启动回调机制
(*inputBufferQueueInterface)->Enqueue(inputBufferQueueInterface, inputBuffers[0], buffersize * 4);
(*outputBufferQueueInterface)->Enqueue(outputBufferQueueInterface, outputBuffers[0], buffersize * 4);

4.3 回调函数的实现

// 音频输入回调
static void inputCallback(SLAndroidSimpleBufferQueueItf bufferQueue, void *pContext) {
    // 获取同步锁
    pthread_mutex_lock(&mutex);
    
    // 取一个可用的缓存
    short int *inputBuffer = inputBuffers[inputBufferWrite];
    if (inputBuffersAvailable == 0) 
        inputBufferRead = inputBufferWrite;
    // 可用缓存+1
    inputBuffersAvailable++;
    if (inputBufferWrite < numBuffers - 1) 
        inputBufferWrite++; 
    else
        inputBufferWrite = 0;

    pthread_mutex_unlock(&mutex);
    
    // 调用BufferQueue的Enqueue方法,把输入数据取到inputBuffer
    (*bufferQueue)->Enqueue(bufferQueue, inputBuffer, buffersize * 4);
}

// 音频输出回调
static void outputCallback(SLAndroidSimpleBufferQueueItf bufferQueue, void *pContext) {
    short int *outputBuffer = outputBuffers[outputBufferIndex];
    pthread_mutex_lock(&mutex);

    if (inputBuffersAvailable < 1) {
        pthread_mutex_unlock(&mutex);
        memset(outputBuffer, 0, buffersize * 4);
    } else {
        short int *inputBuffer = inputBuffers[inputBufferRead];
        if (inputBufferRead < numBuffers - 1) inputBufferRead++; else inputBufferRead = 0;
        inputBuffersAvailable--;
        pthread_mutex_unlock(&mutex);
        memcpy(outputBuffer, inputBuffer, buffersize * 4);
    }

    (*bufferQueue)->Enqueue(bufferQueue, outputBuffer, buffersize * 4);

    if (outputBufferIndex < numBuffers - 1) outputBufferIndex++; else outputBufferIndex = 0;
}

回调函数使用生产者-消费者机制实现,当输入可用的时候,就从输入的缓冲队列里取数据出来,放到inputBuffers里;然后当输出准备就绪的时候,再从inputBuffers里取出数据,复制一份,然后放入输出的缓冲队列里。就这样实现了把音频输出转到音频输出的效果。

关于OpenSL的使用

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

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

OpenSL使用回调机制来访问音频IO,但不像跟Jack、CoreAudio那些音频异步IO框架,OpenSL 的回调里并不会把音频数据作为参数传递,回调方法仅仅是告诉我们:BufferQueue已经就绪,可以接受/获取数据了。

使用SLBufferQueueItf. Enqueue函数从(往)音频设备获取(放入)数据。完整的函数签名是:SLresult (*Enqueue) (SLBufferQueueItf self, const void *pBuffer, SLuint32 size);

当BufferQueue就绪,这个方法就应被调用。当开启录制或开始播放时,BufferQueue就可以接受数据。这之后,回调机制通过回调来告知应用程序它已经准备好,可以消费(提供)数据。

Enqueue方法可以在回调里调用,可以不。

如果选择在回调里调用,那么在开始播放(录制)的时候,需要先调用Enqueue来启动回调机制,否则回调将不会被调用到。

如果选择不在回调里调用,回调则用于通知程序,它准备就绪了。程序可以在得到足够的数据缓存之后,再把数据给它处理。这示例使用的是前一种方式,在回调里调用Enqueue

That's all

使用OpenSL ES可以更高效的使用Android的音频系统,尤其是需要低延迟的场景,如返听。随着Android设备性能的提升,以及Android系统的不断优化,音频延迟的问题已经有了可观的性能提升。而在游戏领域,OpenSL ES的高性能也能提供更棒的游戏体验,甚至让移动平台也有打造《吟诵者(In Verbis Virtus)》这种语音类游戏的可能。

But not ALL

对于OpenSL ES也仅仅是草草接触,如有不对或疏漏的地方,还请大家指正。

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

推荐阅读更多精彩内容