Android native音频:录制播放的实现以及低延迟音频方案

Posted zuguorui

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android native音频:录制播放的实现以及低延迟音频方案相关的知识,希望对你有一定的参考价值。

文章目录

1. 前言

android提供了很多的多媒体接口,通常在java层,我们常用的就是AudioTrack和MediaPlayer进行音频播放。MediaPlayer不光可以播放音频,也可以播放视频,并支持少部分的解码。

而由于音视频通常计算量都很大,所以很多音视频方面的工作都会放在native层进行。Android在native层同样提供了一些组件来进行音频的播放和录制:

  • OpenSL ES:这是Android从很早就开始支持的,它类似于OpenGL ES,所不同的是它完全服务于音频。可以说现在所有的Android手机都会支持。兼容性最好。它无法指定录制或者播放设备。
  • AAudio:这是谷歌在Android 8.0后引入的,它支持一些简单的解码、音频录制和播放以及一些效果等。官方网页为AAudio-Google。它可以指定录制或播放设备。
  • Oboe:这个并不是新的音频框架,而只是为了方便使用OpenSL ES和AAudio,对两者进行的封装,隐藏了大部分琐碎的细节,并且其api也是基于c++风格的,更加方便。日常使用基本只依赖这个即可。

native部分的音频框架相对于java部分的音频框架来说,性能是更高的,所以如果你希望降低app的音频通路延迟(游戏等),基本上只能选择native。

本篇文章会分别使用三者,演示如何构建录制器和播放器,并实现低延迟的echo功能。

2. 工程准备

工程源码放在我的github上:FastPathAudioEcho

新建一个工程,并使其支持c++。

对于OpenSL ES和AAudio,这些库在系统中是被包含的,因此只要在CMakeLists中链接即可:

target_link_libraries( # Specifies the target library.
        native-lib
        OpenSLES
        aaudio
        oboe
        # Links the target library to the log library
        # included in the NDK.
        $log-lib)

至于oboe,它并未被包含在Android SDK中,因此需要到github上搜索,然后使用仓库或下载源码进行配置。地址是:Oboe-Google。编译方法也可以从这找到。
由于使用仓库对构建工具版本有要求,因此我选择的是直接下载源码进行编译。下面是我的配置:

CMakeLists.txt,位置在源码的cpp文件夹中。


cmake_minimum_required(VERSION 3.4.1)

file(GLOB CPP_FILES "./*.cpp", "./SLESEcho/*.cpp", "./AAudioEcho/*.cpp", "./OboeEcho/*.cpp")

include_directories("./", "./SLESEcho/", "./AAudioEcho/", "./OboeEcho/")


add_library( # Sets the name of the library.
        native-lib

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        $CPP_FILES)

# Set the path to the Oboe directory.

set (OBOE_DIR "D:\\\\workspace\\\\AndroidProject\\\\oboe")

# Add the Oboe library as a subdirectory in your project.
# add_subdirectory tells CMake to look in this directory to
# compile oboe source files using oboe's CMake file.
# ./oboe specifies where the compiled binaries will be stored

add_subdirectory ($OBOE_DIR ./oboe)

# Specify the path to the Oboe header files.
# This allows targets compiled with this CMake (application code)
# to see public Oboe headers, in order to access its API.

include_directories ($OBOE_DIR/include)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        native-lib
        OpenSLES
        aaudio
        oboe
        # Links the target library to the log library
        # included in the NDK.
        $log-lib)

然后,指定一些常量和结构体放在一个头文件中,方便使用。

// Constants.h
#ifndef FASTPATHAUDIOECHO_CONSTANTS_H
#define FASTPATHAUDIOECHO_CONSTANTS_H

#include <iostream>
#include <stdlib.h>
#include <string.h>

#define NANO_SEC_IN_MILL_SEC 100000
struct AudioFrame
    int64_t pts;
    int16_t *data;
    int32_t sampleCount;

    int32_t maxDataSizeInByte = 0;

    AudioFrame(int32_t dataLenInByte)
    
        this->maxDataSizeInByte = dataLenInByte;
        pts = 0;
        sampleCount = 0;
        data = (int16_t *)malloc(maxDataSizeInByte);
        memset(data, 0, maxDataSizeInByte);
    

    ~AudioFrame()
        if(data != NULL)
        
            free(data);
        
    
;
#endif //FASTPATHAUDIOECHO_CONSTANTS_H

3. 低延迟音频原理及功能实现方案

官方文档分别在OpenSL ES和AAudio页面介绍了如何启用低延迟音频。总结起来共有以下四点:

  • 获取硬件最佳采样率
  • 获取硬件最佳buffer长度
  • 对框架启用低延迟模式
  • 使用回调函数而非客户端主动读取或写入数据

其中,最佳采样率和最佳buffer长度是针对硬件的。每个手机厂商的每款机型,可能由于硬件芯片的不同,芯片的原生采样率和音频buffer长度都不同,因此这两个数据需要程序运行时动态查询。使用硬件原生采样率和buffer长度的目的,就是避免系统中对音频数据进行的重采样或帧缓冲等操作。
而启用低延迟模式可以理解为,不要让系统在音频中添加效果。并且提高音频线程的优先级。

查询最佳采样率和buffer长度可以通过AudioManager进行:

val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
val sampleRate = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE).toInt()
val framesPerBuffer = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER).toInt()

这个工程三种API的实现思路是一样的。首先,实现一个线程安全的数据结构,方便从recorder到player传输数据。然后依据每种api实现对应的recorder和player。Echo则是对回放功能的实现类,它使用了player和recorder,并管理两者之间的数据传输,以及播放状态。

因为练手的原因,我一共实现了两种线程安全的数据结构:BlockQueueBlockRingBuffer。前者基于一个list实现,后者是基于数组实现的环形buffer。

两者的基本特性是一样的:线程安全,内部空时get操作会被阻塞,内部满时put操作会被阻塞。

为了尽可能降低延迟,在启动时都是先启动player,这个时候由于buffer是空的,所以player会被阻塞,然后启动recorder,一旦recorder将数据放到buffer里,那player就能立即开始播放。

4. 使用OpenSL ES

4.1 播放器实现

源码文件是SLESPlayer。

对于OpenSL ES来说,尽管官方文档中说明了它的输出流也可以设置低延迟模式SL_ANDROID_PERFORMANCE_LATENCY,但是我在一加手机上无法配置成功。在官方的android-echo示例中也并没有找到相关的配置代码。

这里贴一下建立播放器的源码,因为OpenSL ES用起来还是挺复杂的,一是文档不是很全,二是源码没有注释。


    /**
     * 初始化播放器。
     * engine:OpenSLES引擎。
     * dataCallback:数据回调接口。为空时,需要客户端自行向播放器填充数据。
     * sampleRate:采样率
     * channelCount:声道数
     * framesPerBuffer:一个buffer包含多少帧,通常这是在使用低延迟音频时会设置。
     * return:是否成功初始化。
     * */
bool SLESPlayer::init(SLESEngine &engine, ISLESPlayerCallback *dataCallback, int32_t sampleRate, int32_t channelCount,
                      int32_t framesPerBuffer) 
    this->dataCallback = dataCallback;
    this->sampleRate = sampleRate;
    this->framesPerBuffer = framesPerBuffer;
    this->channelCount = channelCount;

    // 初始化一个buffer。
    if(audioBuffer)
    
        free(audioBuffer);
    
    audioBuffer = (int16_t *)calloc(framesPerBuffer * channelCount, sizeof(int16_t));

    SLEngineItf engineEngine = engine.getEngine();
    SLresult result;

    // 初始化一个outputMix
    SLInterfaceID ids1[1] = SL_IID_OUTPUTMIX;
    SLboolean reqs1[1] = SL_BOOLEAN_FALSE;
    result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, ids1, reqs1);
    if(result != SL_RESULT_SUCCESS)
    
        return false;
    


    result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
    if(result != SL_RESULT_SUCCESS)
    
        return false;
    


    // Create player
    SLDataLocator_AndroidSimpleBufferQueue bufferQueue = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2;
    SLDataFormat_PCM pcmFormat = SL_DATAFORMAT_PCM, (uint32_t)channelCount, (uint32_t)sampleRate * 1000, SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
                                  SL_SPEAKER_FRONT_CENTER, SL_BYTEORDER_LITTLEENDIAN;

    SLDataSource audioSrc = &bufferQueue, &pcmFormat;

    SLDataLocator_OutputMix locOutputMix = SL_DATALOCATOR_OUTPUTMIX, outputMixObject;
    SLDataSink audioSink = &locOutputMix, NULL;

    SLInterfaceID ids2[2] = SL_IID_BUFFERQUEUE, SL_IID_VOLUME;
    SLboolean reqs2[2] = SL_BOOLEAN_TRUE, SL_BOOLEAN_FALSE;

    result = (*engineEngine)->CreateAudioPlayer(engineEngine, &playerObject, &audioSrc, &audioSink, 2, ids2, reqs2);
    if(result != SL_RESULT_SUCCESS)
    
        return false;
    


    result = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);
    if(result != SL_RESULT_SUCCESS)
    
        return false;
    

    // 这是配置低延迟模式的代码,但是configItf一直是null。系统log打印W/libOpenSLES: Leaving Object::GetInterface (SL_RESULT_FEATURE_UNSUPPORTED)
    SLAndroidConfigurationItf configItf = nullptr;
    result = (*playerObject)->GetInterface(playerObject, SL_IID_ANDROIDCONFIGURATION, &configItf);
    if(result == SL_RESULT_SUCCESS && configItf != nullptr)
    
        // Set the performance mode.
        SLuint32 performanceMode = SL_ANDROID_PERFORMANCE_LATENCY;
        result = (*configItf)->SetConfiguration(configItf, SL_ANDROID_KEY_PERFORMANCE_MODE,
                                                &performanceMode, sizeof(performanceMode));
        if(result != SL_RESULT_SUCCESS)
        
            LOGE("failed to enable low latency of player");
        
     else
    
        LOGE("failed to get config obj");
    

    result = (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playerPlay);
    if(result != SL_RESULT_SUCCESS)
    
        return false;
    


    result = (*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &playerBufferQueue);
    if(result != SL_RESULT_SUCCESS)
    
        return false;
    

    if(dataCallback)
    
        result = (*playerBufferQueue)->RegisterCallback(playerBufferQueue, playerCallback, this);
        if(result != SL_RESULT_SUCCESS)
        
            return false;
        
    

    // 要注意,OpenSLES在创建好播放器或者录音器后,需要手动Enqueue一次,才能触发主动回调。
    result = (*playerBufferQueue)->Enqueue(playerBufferQueue, audioBuffer, framesPerBuffer * channelCount * sizeof(int16_t));
    if(result != SL_RESULT_SUCCESS)
    
        return false;
    

    (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_STOPPED);



    return true;

这里的采样率和buffer长度就要设置为之前通过AudioManager查询得到的数据。

特别注意的是,OpenSL ES每次在start时需要手动Enqueue一次空buffer,这样它才会主动回调给它设置的callback。否则,不光是可能不会回调,还有可能出现杂音等一系列问题。

4.2 录音器实现

源码为SLESRecorder

对于recorder来说,设置recorder的config为SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION,是降低录音延迟的设置。

	/**
     * 初始化。
     * engine:引擎
     * dataCallback:输出录音数据的回调。为空时,需要客户端主动从录音器读取数据
     * sampleRate:采样率
     * framesPerBuffer:buffer可以容纳多少帧数据。对于录音器来说,该选项并不会影响延迟。录音器总是以尽可能快的方式进行。
     * return:是否成功初始化
     * */
bool SLESRecorder::init(SLESEngine &engine, ISLESRecorderCallback *dataCallback, int32_t sampleRate, int32_t framesPerBuffer) 
    this->dataCallback = dataCallback;
    this->sampleRate = sampleRate;
    this->framesPerBuffer = framesPerBuffer;

    if(audioBuffer)
    
        free(audioBuffer);
    
    audioBuffer = (int16_t *)calloc(framesPerBuffer, sizeof(int16_t));

    const SLEngineItf engineEngine = engine.getEngine();

    if(!engineEngine)
    
        LOGE("engineEngine null");
        return false;
    

    SLresult result;
    SLDataLocator_IODevice deviceInputLocator =  SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT, NULL ;
    SLDataSource inputSource =  &deviceInputLocator, NULL ;

    SLDataLocator_AndroidSimpleBufferQueue inputLocator =  SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2 ;
    SLDataFormat_PCM inputFormat =  SL_DATAFORMAT_PCM, 1, (SLuint32)sampleRate * 1000, SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, SL_SPEAKER_FRONT_LEFT, 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_TRUE ;

    // 创建AudioRecorder
    result = (*engineEngine)->CreateAudioRecorder(engineEngine, &recorderObject, &inputSource, &inputSink, 2, inputInterfaces, requireds);
    if(result != SL_RESULT_SUCCESS)
    
        LOGE("create recorder error");
        return false;
    

    // 设置recorder的config为SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION,这是开启录音器低延迟的方法。
    SLAndroidConfigurationItf recordConfig;
    result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDCONFIGURATION, &recordConfig);
    if(result == SL_RESULT_SUCCESS)
    
        SLuint32 presentValue = SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION;
        (*recordConfig)->SetConfiguration(recordConfig, SL_ANDROID_KEY_RECORDING_PRESET, &presentValue, sizeof(SLuint32));
    

// 初始化AudioRecorder
    result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);
    if(result != SL_RESULT_SUCCESS)
    
        LOGE("realise recorder object error");
        return false;
    




    // 获取录制器接口

    result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord);
    if(result != SL_RESULT_SUCCESS)
    
        LOGE("get interface error");
        return false;
    


    // 获取音频输入的BufferQueue接口
    result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &recorderBufferQueue);
    if(result != SL_RESULT_SUCCESS)
    
        LOGE("get buffer queue error");
        return false;
    


    if(dataCallback)
    
        result = (*recorderBufferQueue)->RegisterCallback(recorderBufferQueue, recorderCallback, this);
        if(result != SL_RESULT_SUCCESS)
        
            LOGE("register callback error");
            return false;
        
    



    // 要注意,OpenSLES在创建好播放器或者录音器后,需要手动Enqueue一次,才能触发主动回调。
    result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, audioBuffer, framesPerBuffer * sizeof(int16_t));
    if(result != SL_RESULT_SUCCESS)
    
        return false;
    

    (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED);
    return true;

4.3 Echo实现

源码是SLESEcho

它使用上面的player和recorder,因此这部分的代码已经相当简单,注意启动时的顺序,以及结束时的防止死锁:

void SLESEcho::start() 
    // 为了降低延迟,先启动播放器,让它的回调函数阻塞,一旦录音器有数据填充进来,可以立刻开始播放。
    player.start();
    recorder.start();


void SLESEcho::stop() 
    // 首先调用对应部件的stop方法,该方法是异步的,不会阻塞。但是回调函数可能仍然在阻塞,因此要对buffer进行设置,
    // 解除等待状态,然后再恢复阻塞功能。
    recorder.stop();
    buffer.setWaitPutState(false);

    player.stop();
    buffer.setWaitGetState(false);

    buffer.setWaitGetState(true);
    buffer.setWaitPutState(true);

5. 使用AAudio

使用AAudio的回调函数模式时要注意:AAudio回调函数需要返回AAUDIO_CALLBACK_RESULT_CONTINUEAAUDIO_CALLBACK_RESULT_STOP,来指示流是否继续进行。但是我发现如果你返回了stop,那么之后再调用start时,回调函数并不会被调用,也就无法正常录制或播放。而且AAudio的回调函数中并没有任何方式可以告诉AAudio你从它的audioData这个buffer里读取或写入了多少帧数据,所以它默认应该是你将buffer全都写满或者全都读取了。因此我建议,在录音端并不需要做任何特殊处理,但是在播放端,回调函数应该一直返回AAUDIO_CALLBACK_RESULT_CONTINUE,如果你因为播放功能已经停止,或者其他什么原因,导致你无法给到AAudio播放需要的那么多数据,只要简单将传到回调函数的audioData这个buffer全部置0,再尽可能写入即可,这样表现出的只是静音而已,并不会出现异常情况。对于流的状态,只需要通过流本身的start或stop进行即可。

5.1 播放器实现

源码是AAudioPlayer

相比于OpenSL ES,AAudio则要清楚很多。依旧只贴一下创建的代码。AAudio的输出流可以设置低延迟模式,为AAUDIO_PERFORMANCE_MODE_LOW_LATENCY

bool AAudioPlayer::init(IAAudioPlayerCallback *dataCallback, int32_t sampleRate, int32_t channelCount, PERFORMANCE_MODE mode, int32_t framesPerBuffer) 
    this->dataCallback = dataCallback;
    this->sampleRate = sampleRate;
    this->channelCount = channelCount;
    this->framesPerBuffer = framesPerBuffer;

    emptyBuffer = (int16_t *)calloc(framesPerBuffer * channelCount, sizeof(int16_t));

    aaudio_result_t result;

    AAudioStreamBuilder *outputBuilder;
    result = AAudio_createStreamBuilder(&outputBuilder);
    if(result != AAUDIO_OK)
    
        LOGE("create output stream builder error");
        AAudioStreamBuilder_delete(outputBuilder);
        return false;
    
    AAudioStreamBuilder_setDirection(outputBuilder, AAUDIO_DIRECTION_OUTPUT);
    AAudioStreamBuilder_setFormat(outputBuilder, AAUDIO_FORMAT_PCM_I16);
    AAudioStreamBuilder_setSamplesPerFrame(outputBuilder, framesPerBuffer);
    AAudioStreamBuilder_setSampleRate(outputBuilder, sampleRate);
    AAudioStreamBuilder_setChannelCount(outputBuilder, channelCount);
    if(dataCallback)
    
        AAudioStreamBuilder_setDataCallback(outputBuilder, output_callback, this);
    

    AAudioStreamBuilder_setPerformanceMode(outputBuilder, mode); // 在这里设置低延迟模式
    result = AAudioStreamBuilder_openStream(outputBuilder, &outputStream);

    AAudioStreamBuilder_delete(outputBuilder);
    if(result != AAUDIO_OK)
    
        LOGE("open play stream failed");
        return false;
    

    return true;

5.2 录音器实现

源码是AAudioRecorder

同输出流一样,AAudio的输入流可以设置低延迟模式,为AAUDIO_PERFORMANCE_MODE_LOW_LATENCY

bool AAudioRecorder::init(IAAudioRecorderCa

以上是关于Android native音频:录制播放的实现以及低延迟音频方案的主要内容,如果未能解决你的问题,请参考以下文章

Android音频的录制与播放

Android音频的录制与播放

Android:使用 MediaRecorder 录制音频 - 文件不播放

从 URL React Native 录制音频(广播流)

Android录制声音文件(音频),并播放

Android:使用 audiorecord 类录制音频播放快进