Android AAudio高性能音频接口

一、简介

1.AAudio

Android Java层的提供的音频接口有MediaPlayer、MediaRecorder、AudioTrack、AudioRecord。AAudio则是Google在Android O(8.0版本-API level 26-2017年)引入的低延迟、高性能的JNI录放音接口,采用最精简设计,不负责音频设备管理 , 文件I/O, 音频编解码等操作,只提供写入音频流进行放音、录音的功能。属于NDK开发范围,应用层使用JNI封装c++接口调用。
AAudio-demo

2.API接口部分特点总结

(1)发起录音或者放音后,能从回调函数中直接读取一帧一帧的音频流数据;
(2)线程不安全,为提高性能,AAudio设计上避免使用互斥量,在不同线程中同时调用读写流会导致crash异常,从而需要使用者自己控制并发;
(3)AAudio是轻量级接口,也没有文件I/O,录音API通过回调不断返回帧数据,如果需要保存成文件需自行实现,直接保存录音数据是原始PCM文件,可自己再写头成WAV文件格式播放。

二、架构

AAudio通过mmap内存映射实现音频传输,提升效率,减小延迟


aaudio

3.AAudio音频流三要素(音频设备、共享模式、数据格式

(1)AAudio音频流设备:数据从耳机输入,数据输出到发音设备;
① 音频输入(声音来源):从话筒等音频输入设备中采集音频数据,然后可使用 AAudio读取音频流;
② 音频输出(声音接收):将音频流写入到 AAudio, AAudio 会以极高性能方式将音频流输出到发音设备中;从输入端获取数据 ( 麦克风 -> 音频流 -> 内存 ),将音频数据写出到输出端 ( 内存 -> 音频流 -> 喇叭 );
(2)音频流读写数据格式:使用AAudioStream 结构表示音频流 , 读取和写出音频流数据都使用该数据结构;
AAudio样本格式:

aaudio_format_t C 数据类型 备注
AAUDIO_FORMAT_PCM_I16 int16_t 通用 16 位样本,Q0.15 格式
AAUDIO_FORMAT_PCM_FLOAT 浮点数 -1.0 ~ +1.0

(3)共享模式:可设置独占模式(表示流对音频设备独占访问)和共享模式(允许混合音频,AAudio会将同一设备的所有共享流混合)。独占模式下可能会有音频抢占问题,比如通话场景。

AAudio.h
enum {
    /**
     * This will be the only stream using a particular source or sink.
     * This mode will provide the lowest possible latency.
     * You should close EXCLUSIVE streams immediately when you are not using them.
     */
    AAUDIO_SHARING_MODE_EXCLUSIVE,
    /**
     * Multiple applications will be mixed by the AAudio Server.
     * This will have higher latency than the EXCLUSIVE mode.
     */
    AAUDIO_SHARING_MODE_SHARED
};

三、API说明

1.头文件 #include <aaudio/AAudio.h>

2.通过Builder模式初始化参数

AAudioStreamBuilder *builder;
aaudio_result_t result = AAudio_createStreamBuilder(&builder);
AAudioStreamBuilder_setDeviceId(builder, recordingDeviceId_);
AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_INPUT);  // 录音,AAUDIO_DIRECTION_OUTPUT表示放音
AAudioStreamBuilder_setSampleRate(builder, 48000);
AAudioStreamBuilder_setChannelCount(builder, inputChannelCount_);  // 可设置单声道(Mono)或立体声(Stereo),参数分别为1和2
AAudioStreamBuilder_setDataCallback(builder, ::recordDataCallback, this);
AAudioStreamBuilder_setErrorCallback(builder, ::recordErrorCallback, this);
AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_I16);
AAudioStreamBuilder_setSharingMode(builder, AAUDIO_SHARING_MODE_EXCLUSIVE);
AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);

3.使用音频流

注:接口都是异步函数

// 创建流
aaudio_result_t result = AAudioStreamBuilder_openStream(builder, &recordingStream_);
if (result == AAUDIO_OK && recordingStream_ != nullptr) {
    // open success
}

aaudio_result_t result;
result = AAudioStream_requestStart(stream);  // 开始
result = AAudioStream_requestPause(stream);  // 暂停流
result = AAudioStream_requestFlush(stream);  // 刷新
result = AAudioStream_requestStop(stream);  // 停止
result = AAudioStream_close(stream);  // 关闭流

4.读取和写入音频流(读取录到的音频数据 | 写入音频数据播放)

录音数据回调接口实现样例:
通过AAudioStreamBuilder_setDataCallback(builder, ::recordDataCallback, this);设置音频数据回调
注意:recordDataCallback()函数返回AAUDIO_CALLBACK_RESULT_STOP流会立刻停止,一般在处理到异常时返回,正常则返回AAUDIO_CALLBACK_RESULT_CONTINUE录音流数据会被持续接收到

aaudio_data_callback_result_t recordDataCallback(AAudioStream *stream, void *userData,
                                                 void *audioData, int32_t numFrames) {
    if (userData == nullptr) {
        LOGI("AAudioEngineCPP %s", "userDatauserData == nullptr");
        return AAUDIO_CALLBACK_RESULT_CONTINUE;
    }

    if (audioData == nullptr) {
        LOGI("AAudioEngineCPP %s", "audioData == nullptr");
        return AAUDIO_CALLBACK_RESULT_CONTINUE;
    }
    AAudioEngine *audioEngine = reinterpret_cast<AAudioEngine *>(userData);
    return audioEngine->dataToRecordCallback(stream, audioData, numFrames);
}

aaudio_data_callback_result_t AAudioEngine::dataToRecordCallback(AAudioStream *stream,
                                                                 void *audioData,
                                                                 int32_t numFrames) {
    static uint64_t logging_flag;
    int recordDataTemp = 0;
    if (recordFile && isRecordIng) {
        // 可以选择直接将数据写入文件,也可内存中直接存成原始录音数据的数组,方便某些特殊业务直接解析录音数据
        fwrite(audioData, 1, 2 * numFrames * sizeof(short), recordFile);
        
        // 直接存到Vector
        for(int32_t i = 0; i < numFrames * 2; i++)
        {
            recordDataTemp = *((int16_t *) audioData + i);
            if(i%2 == 0)
            {
                recordData1.push_back((double) recordDataTemp / 32768);
            }
            else
            {
                recordData2.push_back((double) recordDataTemp / 32768);
            }
        }
        if((logging_flag++) % 100 == 0)
        {
            LOGI("AAudioEngineCPP recordIng");
        }
        return AAUDIO_CALLBACK_RESULT_CONTINUE;
    } else {
        LOGI("AAudioEngineCPP isRecordIng status: %d", isRecordIng);
    }
    return AAUDIO_CALLBACK_RESULT_STOP;
}

放音回调接口实现样例:

aaudio_data_callback_result_t AAudioEngine::dataToPlayCallback(AAudioStream *stream,
                                                               void *audioData,
                                                               int32_t numFrames) {
    static uint64_t logging_flag;
    if (playFile && isPlaying) {
        size_t treadsize = fread(audioData, 2 * numFrames * sizeof(short), 1, playFile);
        if((logging_flag++)%20==0)
        {
            LOGI("AAudioEngineCPP %s", "playing");
        }
        if (treadsize > 0) {
            return AAUDIO_CALLBACK_RESULT_CONTINUE;
        }
    }
    fclose(playFile);
    LOGI("AAudioEngineCPP %s", "dataToPlayCallback fclose");

    if (listener != nullptr) {
        LOGI("AAudioEngineCPP %s", "onPlayingEnd");
    }
    LOGI("AAudioEngineCPP %s", "AAUDIO_CALLBACK_RESULT_STOP");
    return AAUDIO_CALLBACK_RESULT_STOP;
}

四、音频流状态码

创建、打开、关闭...流返回的结果码

返回值处理方式举例

aaudio_result_t result = AAudioStream_close(stream);
        if (result != AAUDIO_OK) {
            LOGE("Error closing stream. %s", AAudio_convertResultToText(result));
        }
}
enum { // aaudio_result_t  int32_t
    OK = 0, // AAUDIO_OK
    ErrorBase = -900, // AAUDIO_ERROR_BASE,
    ErrorDisconnected = -899, // AAUDIO_ERROR_DISCONNECTED,
    ErrorIllegalArgument = -898, // AAUDIO_ERROR_ILLEGAL_ARGUMENT,
    ErrorInternal = -896, // AAUDIO_ERROR_INTERNAL,
    ErrorInvalidState = -895, // AAUDIO_ERROR_INVALID_STATE,
    ErrorInvalidHandle = -892, // AAUDIO_ERROR_INVALID_HANDLE,
    ErrorUnimplemented = -890, // AAUDIO_ERROR_UNIMPLEMENTED,
    ErrorUnavailable = -889, // AAUDIO_ERROR_UNAVAILABLE,
    ErrorNoFreeHandles = -888, // AAUDIO_ERROR_NO_FREE_HANDLES,
    ErrorNoMemory = -887, // AAUDIO_ERROR_NO_MEMORY,
    ErrorNull = -886, // AAUDIO_ERROR_NULL,
    ErrorTimeout = -885, // AAUDIO_ERROR_TIMEOUT,
    ErrorWouldBlock = -884, // AAUDIO_ERROR_WOULD_BLOCK,
    ErrorInvalidFormat = -883, // AAUDIO_ERROR_INVALID_FORMAT,
    ErrorOutOfRange = -882, // AAUDIO_ERROR_OUT_OF_RANGE,
    ErrorNoService = -881, // AAUDIO_ERROR_NO_SERVICE,
    ErrorInvalidRate = -880 // AAUDIO_ERROR_INVALID_RATE
};

若无录音权限,则AAudioStream_requestStart返回AAUDIO_ERROR_ILLEGAL_ARGUMENT ErrorIllegalArgument = -898 AAUDIO_ERROR_ILLEGAL_ARGUMENT错误码

音频流状态值

可通过此方法读取aaudio_stream_state_t AAudioStream_getState(AAudioStream* stream)

enum { // aaudio_stream_state_t  int32_t
    Uninitialized = 0, // AAUDIO_STREAM_STATE_UNINITIALIZED,
    Unknown = 1, // AAUDIO_STREAM_STATE_UNKNOWN,
    Open = 2, // AAUDIO_STREAM_STATE_OPEN,
    Starting = 3, // AAUDIO_STREAM_STATE_STARTING,
    Started = 4, // AAUDIO_STREAM_STATE_STARTED,
    Pausing = 5, // AAUDIO_STREAM_STATE_PAUSING,
    Paused = 6, // AAUDIO_STREAM_STATE_PAUSED,
    Flushing = 7, // AAUDIO_STREAM_STATE_FLUSHING,
    Flushed = 8, // AAUDIO_STREAM_STATE_FLUSHED,
    Stopping = 9, // AAUDIO_STREAM_STATE_STOPPING,
    Stopped = 10, // AAUDIO_STREAM_STATE_STOPPED,
    Closing = 11, // AAUDIO_STREAM_STATE_CLOSING,
    Closed = 12, // AAUDIO_STREAM_STATE_CLOSED,
    Disconnected = 13 // AAUDIO_STREAM_STATE_DISCONNECTED
};

对应的状态图

audio_lifecycle

发起录音或者放音,执行对应AAudioStream_相关流方法时日志会打印对应的状态码,日志如下:

AAudioStream: setState(s#7) from 0 to 2  打开
AAudioStream: setState(s#7) from 2 to 3  正在打开
AAudioStream: setState(s#7) from 3 to 4  已经打开,开始录音或放音
AAudioStream: setState(s#7) from 4 to 9  停止
AAudioStream: setState(s#7) from 9 to 11  关闭中
AAudioStream: setState(s#7) from 11 to 11
AAudioStream: setState(s#7) from 11 to 12  已关闭

ps:
1.AAudio API线程不安全,并发调用可能导致程序崩溃,上层调用可加锁保护;
2.注意添加录音权限<uses-permission android:name="android.permission.RECORD_AUDIO" />
3.若调用了关闭流AAudioStream_close,仍有dataCallback数据回调在执行完,会等待数据回调执行完才关闭流,故这两个函数中无需加锁
4.在发起AAudio音频流时,若设备中有其它进程也在发起录放音可能存在音频焦点抢占回调recordErrorCallback异常,比如华为手机多屏协同场景通过DMSDP将音频切换到PC可能导致此情况
日志如下

I/AUDIO-APP: AAudioEngineCPP recordIng, numFrames: 96.
D/AAudioStream: setState(s#1) from 4 to 13
W/AudioStreamInternal_Client: onEventFromServer - AAUDIO_SERVICE_EVENT_DISCONNECTED - FIFO cleared
E/AudioStreamInternalCapture_Client: callbackLoop: read() returned -899
I/AUDIO-APP: AAudioEngineCPP recordErrorCallback has result: AAUDIO_ERROR_DISCONNECTED
D/AudioStreamInternalCapture_Client: callbackLoop() exiting, result = -899, isActive() = 0
I/AUDIO-APP: AAudioEngineCPP Resetting RecordStreams
I/AUDIO-APP: AAudioEngineCPP closeRecordStreams begin
D/AAudio: AAudioStream_close(s#1) called ---------------

异常回调函数代码如下

void AAudioEngine::recordErrorCallback(AAudioStream *stream,
                                       aaudio_result_t error) {
    LOGI("AAudioEngineCPP recordErrorCallback has result: %s", AAudio_convertResultToText(error));
    aaudio_stream_state_t streamState = AAudioStream_getState(stream);
    if (streamState == AAUDIO_STREAM_STATE_DISCONNECTED) {
        // 收到异常断连,需主动关闭音频流,否则可导致流泄露产生额外功耗,同时注意加锁避免并发关闭音频流
        std::function<void(void)> resetRecordStreams = std::bind(&AAudioEngine::resetRecordStreams,this);
        std::thread recordStreamResetThread(resetRecordStreams);
        recordStreamResetThread.detach();
    }
}

void AAudioEngine::resetRecordStreams() {
    LOGI("AAudioEngineCPP Resetting RecordStreams");
    if (resetRecordLock_.try_lock()) {
        closeRecordStreams();
        resetRecordLock_.unlock();
    } else {
        LOGW("AAudioEngineCPP Reset RecordStreams operation already in progress");
    }
}

参考:
AAudio使用说明
AAudio API文档
AAudio架构
AAudio架构讲解以及实现范例
Android Audio音频框架
AAudio-demo

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

推荐阅读更多精彩内容