添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
精彩文章免费看

Android音频开发之OpenSL ES

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

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

关于OpenSL ES

  • OpenSL ES 官网
  • OpenSL ES Wiki
  • Google 官方的 OpenSL ES 介绍
  • Android OpenSL ES 编程要点
  • 高性能音频基础
  • HelloWorld

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

  • 创建并初始化Audio Engine(音频引擎,是和底层交互的入口)
  • 打开OutputMix(音频输出),配置相关参数、Buffer Queue(缓冲队列),以便进行音频播放
  • 打开Audio Input(音频输入),配置相关参数,配置Buffer Queue,以便获取音频输入
  • 设置输出、输入的Callback(回调函数),实现将输入传给输出的逻辑
  • 启动音频录制
  • 也就是,这个程序实现的功能是:将话筒录制的声音,再播放出来,也就是返听的效果。

    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++; 
            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的通用步骤是:

  • 创建对象(通过带有create的函数)
  • 初始化(通过Realize函数)
  • 获取接口来使用相关功能(通过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也仅仅是草草接触,如有不对或疏漏的地方,还请大家指正。

    最后编辑于:2017-12-03 14:25